今回はADコンバータ機能を利用したプログラムをタイマーのプログラムに追加します。
ADコンバータのプログラミングまとめ
ちょっとしつこいですが…
ADコンバータ機能を利用するための必要なことをもう一度確認しておきましょう。
- ADコンバータを利用するピンを「アナログ入力」に設定
- ADコンバータ機能設定
- ADコンバータで電圧値読み取り
今回はアナログ入力ピンを5番ピンにしました。
❶の設定では、次のようにANSELA
レジスタとTRISA
レジスタで、5番ピンを「アナログ入力」に設定します。
ANSELA = 0b00000100; // RA2ピンをアナログ、それ以外のピンはデジタルに設定
TRISA = 0b00001100; // RA2とRA3を入力、それ以外は出力に設定
❷の設定では、次のようにADCON0
レジスタとADCON1
レジスタで、ADコンバータ機能の設定をします。
ADCON0 = 0b00001001; // RA2(AN2)をADコンバータピンに設定し、ADコンバータ機能をEbableにする
ADCON1 = 0b10000000; // 結果数値は右寄せ、ADコンバータクロックはFOSC/2、基準電圧はVDD
❸の動作では、実際にADコンバータ機能を使ってアナログ値を読み取ります。
最初に、次のようにPWMモジュールに読み取りを依頼します。
GO = 1; // PWMモジュールに読み取り依頼
ピン電圧の読み取りには少し時間がかかりますので、次のようにGOレジスタを確認して読み取りが終わるまで待ちます。
while( GO ) {
}
このwhile文を抜けると、結果はADRES
レジスタに入っていますので、この数値に応じた処理をします。
アナログ値の判定方法
ADコンバータで読み取った10ビットの値、つまり0〜1023の値は16ビットのレジスタADRES
に右詰で格納されています。
今回ピンに接続したスイッチの回路は、スイッチの状態に応じて次の電圧になるようにしています。
スイッチ状態 | ピンの電圧 |
---|---|
スイッチ1をON | 0V |
スイッチ2をON | 電源電圧の半分 |
両方ともOFF | 電源電圧 |
ということは、ADコンバータで読み取った結果のADRESレジスタの値は次のようになるはずです。
スイッチ状態 | ADRESの値 |
---|---|
スイッチ1をON | 0 |
スイッチ2をON | 511か512 |
両方ともOFF | 1023 |
ただ、このようになるからといって、プログラムでADRESレジスタの値に応じた処理をするときに、次のように判定してもうまく動きません。
// スイッチ2の判定
if( (ADRES == 511) || (ADRES == 512) ) {
// スイッチ2が押されたときの処理
}
理由ですが、回路にはいろいろな要因によって、スイッチを押したり離したりしても、きっちり0V、電源電圧の半分、電源電圧にならないんです。
例えば、電源電圧の半分にするために同じ抵抗を2本つなげましたが、この2本の抵抗の値はぴったり同じではありません。
今回使用している抵抗はプラスマイナス5%の誤差がある抵抗です。
そのため、同じ値の抵抗を2本つなげて電源電圧の半分にしたつもりでも、結構ずれていることがあります。
また、他にも回路周囲の電気的なノイズの影響を受けて、ADコンバータで読み取るたびにADRES
の値が変動してしまいます。
そこで、このような影響を受けずにADRES
の値に応じてスイッチ状態を判別するために、次のようにスイッチ状態を判別することにします。
このようにすれば、抵抗値のばらつきや周囲の電気的ノイズがあっても、その影響を少なくすることができます。
プログラム動作仕様
次に、プログラムの動作仕様を考えます。
私は今回の回路で黒と白のスイッチを取り付けました。
このスイッチでタイマー時間の増減を行いますが、どちらがプラスかマイナスか決めておく必要がありますね。
なんとなくのイメージですが、黒がマイナス、白がプラスって感じるので、黒(0V)がタイマー時間を減らすスイッチ、白(2.5V)が増やすスイッチにします。
また、動作確認をしやすくするために、タイマー時間の単位は秒として、デフォルト5秒、黒のスイッチを押すとマイナス1秒、白のスイッチを押すとプラス1秒というプログラム動作仕様にしたいと思います。
次に、タイマー時間の増減処理はプログラムのどこで行うか検討します。
現在のプログラムでは、次のコードでスタートボタンが押されるまで何もしない、という処理にしています。
while( RA3 ) {
}
この部分を利用して、スタートボタンが押されるまでの間、タイマー時間の増減処理を行うという動作に変更することにします。
つまり、このwhile
処理で、5番ピンのアナログ値を読み取ってタイマー時間の増減処理をするようにします。
ただ、スイッチを押してプログラムでタイマー時間の増減処理をしても、外見は何も変わらないので本当に増減処理をしたかどうかわかりませんよね。
そこで、増減処理をした、LEDの点滅回数でタイマー時間を表現したいと思います。
電源投入直後はLEDを点灯状態にしていますので、次のようにLEDの点滅パターンでタイマー時間を表現してみます。
ブログラムの動作仕様はこのよう決めましたので、あとはこの通りにプログラムするだけです!
「だけです」なんて書きましたが、実は実際に自分でプログラムを作るときは一発で動作せず、何度も書き直しました…!
プログラム
ということで、何度も書き直した結果、次のようなプログラムになりました!
/*
* PICマイコン電子工作入門 応用編 第11回
* スイッチ状態をADコンバータで読み取る
*/
#include <xc.h>
// PIC12F1822 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = INTOSC // Oscillator Selection (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config CPD = OFF // Data Memory Code Protection (Data memory code protection is disabled)
#pragma config BOREN = ON // Brown-out Reset Enable (Brown-out Reset enabled)
#pragma config CLKOUTEN = OFF // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF // Internal/External Switchover (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)
// CONFIG2
#pragma config WRT = OFF // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = OFF // PLL Enable (4x PLL disabled)
#pragma config STVREN = OFF // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset)
#pragma config BORV = LO // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LVP = OFF // Low-Voltage Programming Enable (High-voltage on MCLR/VPP must be used for programming)
// クロック周波数指定
// __delay_ms()関数が使用する
#define _XTAL_FREQ 1000000
void main(void) {
// PICマイコン設定
OSCCON = 0b01011010; // 内部クロック周波数を1MHzに設定
ANSELA = 0b00000100; // RA2ピンをアナログ、それ以外のピンはデジタルに設定
TRISA = 0b00001100; // RA2とRA3を入力、それ以外は出力に設定(RA3ピンは設定にかかわらず入力ピン)
// ADコンバータ設定
ADCON0 = 0b00001001; // RA2(AN2)をADコンバータピンに設定し、ADコンバータ機能をEbableにする
ADCON1 = 0b10000000; // 結果数値は右寄せ、ADコンバータクロックはFOSC/2、基準電圧はVDD
// タイマー時間保持用の変数(動作確認しやすいようにデフォルトは5秒)
uint16_t timerValue = 5;
// ブザーをOFF、LEDをONにする
LATA4 = 0; // ブザー
LATA5 = 1; // LED
// スイッチが押されるまで、タイマー時間増減スイッチ処理を行う
while( RA3 ) {
// ADコンバータ読み取り
GO = 1; // 読み取り依頼
while(GO) { // 読み取り完了待ち
}
// ADRESの数値に応じてタイマー時間の増減を行う
//
// タイマー時間を減らすスイッチの処理(ADRESが250未満の場合)
if( ADRES < 250 ) {
// タイマー時間を減らす(マイナスにならないようにする)
if( timerValue > 0 ) {
timerValue--;
}
// タイマー時間をLEDの点滅回数で表現する
//
// 少しの間LEDをOFFにする
LATA5 = 0;
__delay_ms(500);
// 設定時間分LEDを点滅する
for(uint16_t i=0; i<timerValue; i++) {
LATA5 = 0;
__delay_ms(200);
LATA5 = 1;
__delay_ms(200);
}
// 少しの間LEDをOFFにする少しの間LEDをOFFにする
LATA5 = 0;
__delay_ms(800);
// LEDを点灯する
LATA5 = 1;
} else {
// タイマー時間を増やすスイッチの処理(ADRESが250以上750未満)
if( ADRES < 750 ) {
// タイマー時間を増やす(上限チェックはしない)
timerValue++;
// タイマー時間をLEDの点滅回数で表現する
LATA5 = 0;
__delay_ms(500);
for(uint16_t i=0; i < timerValue; i++) {
LATA5 = 0;
__delay_ms(200);
LATA5 = 1;
__delay_ms(200);
}
LATA5 = 0;
__delay_ms(800);
LATA5 = 1;
}
}
}
// LEDをtimerValue分点滅する
for(uint16_t timer=0; timer<timerValue; timer++) {
// LEDを950ms消灯する
LATA5 = 0;
__delay_ms(950);
// LEDを50ms点灯する
LATA5 = 1;
__delay_ms(50);
}
// アラーム音を3回鳴らす
for(uint8_t i=0; i<3; i++) {
// ピッ
LATA4 = 1;
__delay_ms(70);
LATA4 = 0;
__delay_ms(70);
// ピッ
LATA4 = 1;
__delay_ms(70);
LATA4 = 0;
__delay_ms(70);
// ピーッ
LATA4 = 1;
__delay_ms(70);
LATA4 = 0;
// 1回分の音パターンの間を少し空ける
__delay_ms(800);
}
// LEDをPWM制御してスムーズな点滅制御をする
//
// PWM機能のピン割り当て設定
APFCONbits.CCP1SEL = 1; // PWM機能をRA5ピンに設定
CCP1CONbits.CCP1M = 0b1100; // PWM機能を有効、active-highに設定
CCP1CONbits.P1M = 0b00; // RA2ピンはGPIOに設定
// 周期とデューティーサイクルの設定
// 周期は1ms
// デューティーサイクルは100%から開始するので1msに設定
T2CONbits.T2CKPS = 0b00; // プリスケーラ値を1に設定
PR2 = 249; // 周期を1msに設定 (249 + 1) x 4 x 1us = 1000us = 1ms
CCPR1L = 1000/4; // デューティーサイクルを0.5msに設定
CCP1CONbits.DC1B = 1000 & 0b11;
// PWM制御スタート
T2CONbits.TMR2ON = 1;
// LEDをPWM制御
// デューテー比は10%〜100%の制御にする
while(1) {
// 100% -> 5%の制御
for(uint16_t duty=1000; duty>=50; duty--) {
CCPR1L = (uint8_t) (duty / 4); // 上位8ビット
CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
__delay_ms(1);
}
// 5% -> 100%の制御
for(uint16_t duty=50; duty<=1000; duty++) {
CCPR1L = (uint8_t) (duty / 4); // 上位8ビット
CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
__delay_ms(1);
}
}
// 以下の命令は実行されない
return;
}
更新履歴
日付 | 内容 |
---|---|
2017.7.29 | 新規投稿 |
2018.12.2 | プログラムテンプレートをMAPABX IDE v5.10に変更 |
2025.5.16 | プログラム変更(アラーム音を3回に変更) |
いつも参考にさせていただき大変助かっています
疑問に思ったことがあるので質問させてください
可変抵抗(つまみ)を使用しております。
1つならうまく動作しましたが
可変抵抗を増やしたい場合、2つ以上の場合はどうすれば拾えますでしょうか
例:
可変抵抗1
抵抗値の変化に合わせてLEDの点滅速度を変化させる
可変抵抗2
抵抗値の変化に合わせてLEDのカラー(RGB)を変化させる
可変抵抗3
抵抗値の変化に合わせてその他の動作を行う
※現状PIC18857を使用しておりますので設定が1822と若干違います
———入出力———
TRISA = 0b00100000;
TRISB = 0b00000100;
———アナログ———
ANSELA = 0b00100000;
ANSELB = 0b00000100;
———ADC 変換クロック———
ADCLK = 0b00000000; //16Mhz 125n
———電圧比較基準———-
ADREF= 0b00000000; //VDD
———ADC 入力チャンネルの選択———
ADPCH = 0b00000101; //RA5
ADPCH = 0b00001010; //RB2
———ADC モジュールをオンにする———
ADCON0 = 0b10000100;
ADCチャンネル設定「ADPCH」が一つしかなく
結果格納及び呼び出しの「ADRES」も一つしかないので
1つの可変抵抗しかADC変換できないのでしょうか
それとも複数の読み取りと格納方法があるのでしょうか
お手すきの際にでもお返事頂けると幸いです
ご質問どうもありがとうございます!
ADCは読み取り結果のレジスタは1つしかありませんが、
入力を切り替えて利用する、というイメージです。
例えば、RA0とRA1に可変抵抗を接続してその電圧を読み取る場合、
1) ADPCHレジスタに読み取り端子をRA0に設定
ADPCH = 0;
2) ADC読み取り
ADCON0bits.ADGOを1にして、読み取りが終わるまで待つ(ADGOが0になるまで待つ)
3) 結果を読み取る
ADRESHとADRESLの値を保存
4) ADPCHレジスタに読み取り端子をRA1に設定
ADPCH = 1;
5) (2)と(3)でADC読み取り
このような感じで、ADPCHで読み取り端子を設定→ADC読み取りを繰り返す感じです。
実機確認していないのですが、この手順で読み取れると思います。
念のためMCCでADCのコードを生成してみました。
読み取り関数は次のようになっていて、ADGOで読み取りする前にチャンネル(端子)を設定していました。
足りない情報などございましたら、追加でご質問いただければと思います。
ご連絡いただきありがとうございます
ご指示の通りやったら何とか出来ました、感謝感謝です!
MCC苦手なので使わず
変数を2つ用意して、繰り返すたびにADRESを代入する
それぞれ代入した変数から読み出せました
マイコンとプログラム処理スピードの凄さにいつも感動します
また引き続き参考にさせていただきます。
ご連絡どうもありがとうございます!
うまく動いてよかったです!!
マイコンのクロック周波数はPCにくらべるとすごく遅いですが、
それでも十分すぎるぐらい早いですよね
また何かありましたらご質問いただければと思います。
同じ質問をしようと思ったところでした。ありがとうございます。参考にさせていただきます!
コメントどうもありがとうございます!
このようにコメントいただけると、記事内容に足りない部分が分かりますのでとても参考になります。
今年中(2025年中)に、PICマイコン入門シリーズの記事を最新情報に書き換え予定なので、そのときの参考にさせていただきます。
どうもありがとうございます。
第8回 ADコンバータ(2) 〜回路を設計する〜の回で1回押すたびにタイマー時間を1分ずつ増やすスイッチと、1分ずつ減らすスイッチの2個のスイッチを付けることにします。
と有りますが、本回では1秒ずつ増やす、減らすスイッチになっていますこれを1分単位に変更するためにはプログラムをどの様に変更したら良いですか
ご質問どうもありがとうございます。
タイマー時間の増減は、増やす場合はプログラム107行目、減らす場合は80行目でtimerValueを1増やしたり(=1秒増やしたり)、1減らしたり(=1秒減らしたり)しています。
これをヒントにどうすればよいか、考えてみていただければと思います。
わからないようでしたらまたお手数ですがコメントをいただければと思います。
いつも素晴らしい知識をありがとうございます。
誤字と思われる箇所がありましたので、お知らせします。
第11回(この回)ーアナログ値の判定方法ー 〜スイッチを押していない時は電源で暑(電源電圧?)
このサイトがますます素晴らしいサイトになるようお祈りします。
ご指摘どうもありがとうございました。
なかなか本人は気づかないので、ご指摘いただいて大変助かります。
何かわからないことなど出てきましたらコメントいただければと思います。
引き続きよろしくお願いいたします。
ADコンバーターに出てくる、ADRES = ADRESH << 8 + ADRESL と言う式が、全く腑に落ちません。 私の解釈では、ADRESHに入ったデータは、枠外に押し出して、ADRESLのデータを入れるという解釈です。 何故こんな面倒くさいことをするのかがいまいち理解できません。 PICマイコンはこうしなければならないように、製造会社が作ったのなら、そうしなければ成りませんが、手を抜けるところは抜きたくなるのが人間の性です。 結局のところ、ADコンバーターのデータは、10桁で取り込みますが、上下二桁を除いた中間の6桁しか使ってないのではないかと邪推しています。
ご質問としては、ADRESHレジスタは8ビット幅なので、8ビット左にシフトすると全て押し出されて0になるのではないか、と思われているのではないかと思います。
これはPICマイコンに限らず、一般的なC言語の仕様になりますが、代入式の場合は、左辺の変数に合わせて、右辺が計算されます。
極端な例を説明します。
ADRESが16ビット、ADRESHが8ビットとします。このとき、
ADRES = ADRESH;
とプログラムに書くと、実際の実行コードは、いったんADRESHを16ビットに変換してから、ADRESに代入されます。
それでは、
ADRES = ADRESH * 2;
と書いた場合、実行コードはどうなるかというと、
1) ADRESHを16ビットにする (上位8ビットは0に設定される)
2) ADRESH * 2を計算する
3) ADRESHをADRSに代入する
という動作をします。
ここで、ADRESH * 2というのはADRESH << 1のことですよね。 ということで、ADRESHが8ビットの場合、ADRESH<<1としても、16ビット変数に代入する場合は、ADRESHが8ビットのまま左側から押し出されてなくなる、ということはありません。 繰り返しになりますが、これは一般的なC言語の動作になります。なおADRESHを 16ビットとして扱うのはXC8コンパイラがルールに則り変換しています。ただ、コンパイル時のエラーを厳密にチェックするようにすると、この式はエラーになる場合があります。 そこで、このような誤解を避けるために、正式に書く場合は、 ADRES = (uint16_t)ADRESH << 8 + (uint16_t)ADRESL; というように変数の前に型変換を明示的に書く場合もあります。ただ、型が違う変数の代入は一般的なため、型変換の修飾子を書かない方が一般的になっています。
初歩的な質問なんですが、PWM第4回の中間あたりで、次のように書かれています。
これで、PWM機能のピン割り当てとPWM信号のパラメータを設定しましたので、あとはPWM制御をスタートさせると、PWM機能を割り当てたピンにPWM信号が出力されます。なお、PWM制御スタート後でも、周期とデューティサイクルは変更可能です。
このスタート後に可変する方法は、ポテンショメータなどで連続的に可変出来ると言うことでしょうか。では、ボリュームの値をどのように読み取るのかと悩んでしまいます。
全く的外れの質問かもしれませんが、よろしくお願いいたします。
こんばんわ。自分手持ちのPIC16F1825に合わせてブレッドボードで回路を作成し基本動作させることはできました。実力不足でよくわからない点が1点あります。
if( ADRES < 250 )
や
if( 250 <= ADRES && ADRES < 750 )
でADコンバート値を確認しているのですが、「ADRES」としているそのものがよくわからないです。ADRESHやADRESLを使用しなくてよいのがピンとこないです。PWM設定のときは、デューティーサイクルを上位ビット、下位ビットで分けて設定したいたので・・・その感覚が・・・
たとえば、もっと詳しくなるとデータシートから読み取れるものなのでしょうか?
PS Webで丁寧に解説しているので問題なくプリント基板も作って見れたのですが、手元で紙も見たいので「基礎からのプリント基板製作」も購入しました。自分で作った回路でのプリント基板を作る日を目指して力をつけたいと思います。
wakazoさま、こんにちは。ご質問どうもありがとうございます。
ADRES、ADRESH、ADRESLですが、これらのレジスタは前回の記事(ADコンバータ(4) 〜ADコンバータ設定方法〜)の真ん中あたりの図をご覧いただければと思います。
ADRESは16ビットのレジスタで、上位8ビットがADRESH、下位8ビットがADRESLという名前が付いています。ADコンバータの結果は10ビットですが、8ビットのレジスタには収まらないので、16ビットのレジスタに代入されます。
これらのレジスタの関係は以下の式で示すこともできます。
ADRES = ADRESH * 256 + ADRESL
または、
ADRES = ADRESH << 8 + ADRESL
PWM制御の場合は、10ビットの数値について、上位8ビットをCCPR1Lに代入、下位2ビットをDC1Bに代入、というようにそれぞれ異なるレジスタに代入するため、どうしても分割して代入する必要がありました。
ADコンバータの場合は、ADRESHとADRESLを連結したものがADRESになっています。
C言語でunionというデータ構造がありますが、ADRES、ADRESH、ADRESLはunionで考えることもできます。unionは普通のアプリケーションのプログラミングではあまり出てきませんが、PICマイコン他、マイコン系のプログラミングでは出てくることが多いので、お時間のあるときに確認されてみてください。
アドバイスありがとうございました。PIC16F1825環境では、右寄せのADRES読み取りや、チャレンジ課題にでてくる左よせのADRESHで読み取り値も変えてうまくいきました。
ここまでの経験で、プリント基板を作ったときの、PIC12F683でも同時にデータシートで差を確認しながら改造し実験しているのですが、ADRESではBUILDが正常に終了せず、ADRESHならうまくいきました。接続は、GP2 LED,GP3 SW,GP4(SW1,SW2),GP5 BUZZ
回路、プログラム、データシートを自分なりに見ているのですがどうも???です。 理解不足ですぐつかえてしまってます。(楽しみながらですが)
情報不足ですみません。この動作だけでなにかわかることはありましすでしょうか?
wakazoさま、
ご質問どうもありがとうございます。
PIC12F683でADRESでビルドエラーになる件ですが、調べたところ、ADRESレジスタはヘッダファイルに定義されていませんでした。今回の2個のスイッチの判定のように、ADコンバータの精度がそれほど必要ない場合は、結果を左寄せにしてADRESHで判定すれば問題ないと思います。温度センサの場合など精度が必要な場合はADRESH x 256 + ADRESLで計算するしかなさそうです。
ところで、PIC12F683にADRESレジスタが定義されていない背景について説明いたします。
まず、PIC12F1822のADRESHとADRESLは、メモリ上の連続したアドレスに配置されています。この連続して配置されている2バイト(=8ビット x 2)を、連続した16ビットの値として読み取ればADRESレジスタとして読めることになります。
一方、PIC12F683は、ADRESHとADRESLレジスタは離れたアドレスに配置されていますので、連続した2バイトとして読むことはできないため、ヘッダファイルにはADRESレジスタが定義されていません。
ただ、ADRESを#defineなどで定義しておけば擬似的にADRESが使用できますので、定義しておいてくれてもいいのに、とも思います。PICマイコンによってADRESが使用できたりできなかったりすると、調べる手間がかかりますので、Arduinoのようにマイコン依存がないようにして欲しいところです。
この辺りはデータシートには記載されていませんので、ヘッダファイルを地道に調べていくしかなさそうです。あまにも細かいところになってくると「自分は何をしているんだろう」という疑問も出てきたりしますので、調べるという行為は重要ですが、時間がかかりそうでしたらご質問いただければと思います。
なるほど!
アドレスが離れていることをデータシートのメモリマップを見て納得しました。ヘッダファイルですか!気になりますが、深入りせずに次に進んでみます。いつも丁寧にありがとうございます。
PS 以前WEBを参考に作成したプリント基板の拡張用パターンは役に立っています。12F683を搭載して、PWMやADの実験用に改造して使用しています。
早速のご確認どうもありがとうございます。
はい、あまり深入りせずに全体像をつかんでいただいて、それから細かいところを調べていくといいと思いますよ。
プリント基板の拡張部分を有効に使っていただいているようでよかったです! いろいろ実験していただいて、いずれはご自分のオリジナル作品を作ってみてください!