第11回 ADコンバータ(5) 〜プログラムを完成させる〜

今回はADコンバータ機能を利用したプログラムを完成させます。

目次

ADコンバータプログラムまとめ

ちょっとしつこいですが…

ADコンバータ機能を利用するための必要なことをもう一度確認しておきましょう。

  1. ADコンバータを利用するピンを「アナログ入力」に設定
  2. ADコンバータ機能設定
  3. ADコンバータで電圧値読み取り

今回はアナログ入力ピンを5番ピンにしましたので、(1)ではANSELAレジスタとTRISAレジスタで、5番ピンを「アナログ入力」に設定します。

ANSELA = 0b00000100;  // RA2ピンをアナログ、それ以外のピンはデジタルに設定
TRISA  = 0b00001100;  // RA2とRA3を入力、それ以外は出力に設定

このようにプログラムを書けばOKです。

次に(2)ではADCON0レジスタとADCON1レジスタで、ADコンバータ機能の設定をします。

ADCON0 = 0b00001001;  // RA2(AN2)をADコンバータピンに設定し、ADコンバータ機能をEbableにする
ADCON1 = 0b10000000;  // 結果数値は右寄せ、ADコンバータクロックはFOSC/2、基準電圧はVDD

これでOKです。

(3)で、実際にADコンバータ機能を使ってアナログ値を読み取る場合は、

GO = 1;

でADコンバータの読み取り処理が開始されます。読み取りが終わるとこの「GO」レジスタが0になりますので、

while( GO ) ;

で処理を待ちます。このwhile文を抜けると、結果は「ADRES」レジスタ(16-bit)に入っていますので、この数値に応じた処理をします。

 

アナログ値の判定方法

ADコンバータで読み取った10-bit、つまり0〜1023の値は16-bitレジスタADRESに右詰で格納されています。

今回ピンに接続したスイッチの回路は、一つのスイッチを押すと0V、もう一つのスイッチを押すと電源電圧の半分になるようにしました。スイッチを押していない時は電源電圧になります。

ということは、0Vになるスイッチを押すと、ADRESの値は「0」、電源電圧の半分になるスイッチを押すと「511」か「512」、スイッチを押していない時は「1023」っていう判定でいいのでしょうか。

実は、ADRESの値をこのように判定するとうまく動きません。

というのは、回路にはいろいろな要因があり、スイッチを押したり離したりしても、きっちり0V、電源電圧の半分、電源電圧になりません。

例えば、電源電圧の半分にするために同じ抵抗を2本つなげましたが、この2本の抵抗の値はぴったり同じではありません。今回使用している抵抗はプラスマイナス5%の誤差がある抵抗です。そのため、抵抗を2本つなげて電源電圧の半分にしたつもりでも、結構ずれていることがあります。

ということで、0〜1023の値をどのように判別するかというと、以下のようにスイッチOFF、スイッチ(黒=0V)をON、スイッチ(白=電源電圧の半分)をONの3ブロックで分けて判定することにします。

Pic app 11 voltage range

ADRESレジスタの値の判定はこのようにします。

 

プログラム動作仕様

早速プログラムの動作仕様を考えましょう。

自分のブレッドボードでは、黒と白のスイッチを取り付けました。

なんとなくのイメージですが、黒がマイナス、白がプラスって感じるので、黒がタイマー時間を減らすスイッチ、白が増やすスイッチにします。

動作確認のために、タイマー時間の単位は秒として、デフォルト5秒、黒のスイッチを押すとマイナス1秒、白のスイッチを押すとプラス1秒というプログラム動作仕様にしたいと思います。

基本的な考え方は、上のセクションで説明したADコンバータ機能を利用する場合の設定をプログラムに追加、スタートスイッチが押されるまでのループ、

while( RA3 ) ;

このループの中で5番ピンのアナログ値を読み取って、タイマー時間の増減処理をするようにします。

ただ、スイッチを押してタイマー時間を増減したら、無反応だと実際に処理されたかわかりませんので、LEDの点滅回数でタイマー時間を表現したいと思います。

電源投入直後はLEDを点灯状態にしていますので、以下のように点滅パターンでタイマー時間を表現してみます。

Pic app 11 led pattern

ということで、プログラムの仕様を決めましたので、あとはこの通りにプログラムするだけです。(だけです、なんて書きましたが、自分でサンプルプログラムを作るときは一発で動作せず、バグ取りが必要でしたが…)

 

プログラム

ここまで説明した動作仕様のプログラムです。特に解説はいらないと思いますが、わからないところなどありましたら、コメント欄かお問い合わせフォームでご連絡いただければと思います。

/*
 * File:   main.c
 * 変更履歴
 *    2016.11.20: スイッチ制御部分を追加
 *    2016.12.05: 時間計測とブザー制御部分を追加
 *    2017.01.15: 設定時間が来たらLEDをPWM制御に変更
 *    2017.07.30: 時間設定ボタン処理を追加
 */

#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を入力、それ以外は出力に設定

    // ADコンバータ設定
    ADCON0 = 0b00001001;  // RA2(AN2)をADコンバータピンに設定し、ADコンバータ機能をEbableにする
    ADCON1 = 0b10000000;  // 結果数値は右寄せ、ADコンバータクロックはFOSC/2、基準電圧はVDD
    
    // 変数宣言
    unsigned short timer;   // 時間計測
    unsigned short duty;    // PWMのデューティーサイクル
    unsigned short i;       // for文で使う変数

    // LEDを点灯する
    LATA5 = 1;
    
    // ブザーをOFFにする
    LATA4 = 0;
    
    // タイマー時間変数
    // デフォルトは5秒
    unsigned short timerValue = 5;
    
    // スイッチが押されたらタイマースタートする
    // タイマースタート前にタイマー時間設定ボタンが押されたらその処理を行う
    while(RA3){

        // ADコンバータ読み取り開始
        GO = 1;
        
        // 読み取り完了待ち
        while(GO);
        
        // 読み取りが終わると結果の数値はADRESレジスタに入っている
        // ADRESの数値に応じてタイマー時間の増減を行う
        //
        // タイマー時間を減らすスイッチの処理
        // (ADコンバート値が250未満の場合)
        if( ADRES < 250 ) {
            
            // タイマー時間を減らす
            timerValue--;

            // タイマー時間をLEDの点滅回数で表現する
            //
            // いったんLEDを消して、
            RA5 = 0;
            __delay_ms(500);
            // 設定時間分LEDを点滅して、
            for(i=0; i<timerValue; i++) {
                RA5 = 0;
                __delay_ms(200);
                RA5 = 1;
                __delay_ms(200);
            }
            // ちょっとの間LEDを消して、
            RA5 = 0;
            __delay_ms(800);
            // LEDを点灯する
            RA5 = 1;
            
        } else {

            // タイマー時間を増やすスイッチの処理
            // (ADコンバート値が250以上750未満)
            if( 250 <= ADRES && ADRES < 750 ) {
                
                // タイマー時間を増やす
                timerValue++;
                
                // タイマー時間をLEDの点滅回数で表現する
                // 点滅制御方法は減らす場合と同じ
                RA5 = 0;
                __delay_ms(800);
                for(i=0; i<timerValue; i++) {
                    RA5 = 0;
                    __delay_ms(200);
                    RA5 = 1;
                    __delay_ms(200);
                }
                RA5 = 0;
                __delay_ms(800);
                RA5 = 1;
            }
        }
    }
    
    // タイマー計測開始
    //
    // LED点滅処理
    for(timer=0; timer<timerValue; timer++){
        // LEDを950ms消灯する
        LATA5 = 0;
        __delay_ms(950);
        // LEDを50ms点灯する
        LATA5 = 1;
        __delay_ms(50);            
    }
    
    // ブザーをONにする
    LATA4 = 1;

    // LEDをPWM制御してスムーズな点滅制御をする
    // PWM機能のピン割り当て設定
    APFCONbits.CCP1SEL = 1;     // PWM機能をRA5ピンに設定
    CCP1CONbits.CCP1M = 0b1100; // PWM機能を有効、active-highに設定
    CCP1CONbits.P1M = 0b00;     // RA2ピンはGPIOに設定

    // 周期(1ms)とデューティーサイクル(0.5ms)の設定
    T2CONbits.T2CKPS = 0b00;    // プリスケーラを1:1に設定
    PR2 = 249;                  // 周期を1msに設定 (249 + 1) x 4 x 1us = 1000us = 1ms
    CCPR1L = 500/4;             // デューティーサイクルを0.5msに設定
    CCP1CONbits.DC1B = 500;

    // PWM制御スタート
    T2CONbits.TMR2ON = 1;

    // LEDをPWM制御
    // デューテー比は10%〜100%の制御にする
    while(1) {
        // 5% -> 100%の制御
        for(duty=50; duty<=1000; duty++) {
            CCPR1L = duty / 4;          // 上位8ビット
            CCP1CONbits.DC1B = duty;    // 下位2ビット
            __delay_ms(1);
        }
        // 100% -> 5%の制御
        for(duty=1000; duty>=50; duty--) {
            CCPR1L = duty / 4;          // 上位8ビット
            CCP1CONbits.DC1B = duty;    // 下位2ビット
            __delay_ms(1);
        }

    }
    
    // 以下の命令は実行されない
    return;

}

 

更新履歴

日付 内容
2017.7.29 新規投稿
2018.12.2 プログラムテンプレートをMAPABX IDE v5.10のものに変更
通知の設定
通知タイミング
guest
5 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
sasa
sasa
2 月 前

いつも素晴らしい知識をありがとうございます。
誤字と思われる箇所がありましたので、お知らせします。
第11回(この回)ーアナログ値の判定方法ー 〜スイッチを押していない時は電源で暑(電源電圧?)
このサイトがますます素晴らしいサイトになるようお祈りします。

安弘石垣
安弘石垣
5 月 前

ADコンバーターに出てくる、ADRES = ADRESH << 8 + ADRESL と言う式が、全く腑に落ちません。 私の解釈では、ADRESHに入ったデータは、枠外に押し出して、ADRESLのデータを入れるという解釈です。 何故こんな面倒くさいことをするのかがいまいち理解できません。 PICマイコンはこうしなければならないように、製造会社が作ったのなら、そうしなければ成りませんが、手を抜けるところは抜きたくなるのが人間の性です。 結局のところ、ADコンバーターのデータは、10桁で取り込みますが、上下二桁を除いた中間の6桁しか使ってないのではないかと邪推しています。

管理者
管理者
返信  安弘石垣
2 月 前

ご質問としては、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; というように変数の前に型変換を明示的に書く場合もあります。ただ、型が違う変数の代入は一般的なため、型変換の修飾子を書かない方が一般的になっています。

gonta
gonta
6 月 前

初歩的な質問なんですが、PWM第4回の中間あたりで、次のように書かれています。

これで、PWM機能のピン割り当てとPWM信号のパラメータを設定しましたので、あとはPWM制御をスタートさせると、PWM機能を割り当てたピンにPWM信号が出力されます。なお、PWM制御スタート後でも、周期とデューティサイクルは変更可能です。

このスタート後に可変する方法は、ポテンショメータなどで連続的に可変出来ると言うことでしょうか。では、ボリュームの値をどのように読み取るのかと悩んでしまいます。

全く的外れの質問かもしれませんが、よろしくお願いいたします。

wakazo
wakazo
3 年 前

こんばんわ。自分手持ちのPIC16F1825に合わせてブレッドボードで回路を作成し基本動作させることはできました。実力不足でよくわからない点が1点あります。

if( ADRES < 250 )

if( 250 <= ADRES && ADRES < 750 )
でADコンバート値を確認しているのですが、「ADRES」としているそのものがよくわからないです。ADRESHやADRESLを使用しなくてよいのがピンとこないです。PWM設定のときは、デューティーサイクルを上位ビット、下位ビットで分けて設定したいたので・・・その感覚が・・・
たとえば、もっと詳しくなるとデータシートから読み取れるものなのでしょうか?

PS Webで丁寧に解説しているので問題なくプリント基板も作って見れたのですが、手元で紙も見たいので「基礎からのプリント基板製作」も購入しました。自分で作った回路でのプリント基板を作る日を目指して力をつけたいと思います。

管理者
管理者
返信  wakazo
3 年 前

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マイコン他、マイコン系のプログラミングでは出てくることが多いので、お時間のあるときに確認されてみてください。

wakazo
wakazo
返信  管理者
3 年 前

アドバイスありがとうございました。PIC16F1825環境では、右寄せのADRES読み取りや、チャレンジ課題にでてくる左よせのADRESHで読み取り値も変えてうまくいきました。

ここまでの経験で、プリント基板を作ったときの、PIC12F683でも同時にデータシートで差を確認しながら改造し実験しているのですが、ADRESではBUILDが正常に終了せず、ADRESHならうまくいきました。接続は、GP2 LED,GP3 SW,GP4(SW1,SW2),GP5 BUZZ 

回路、プログラム、データシートを自分なりに見ているのですがどうも???です。 理解不足ですぐつかえてしまってます。(楽しみながらですが) 

情報不足ですみません。この動作だけでなにかわかることはありましすでしょうか?

管理者
管理者
返信  wakazo
3 年 前

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のようにマイコン依存がないようにして欲しいところです。

この辺りはデータシートには記載されていませんので、ヘッダファイルを地道に調べていくしかなさそうです。あまにも細かいところになってくると「自分は何をしているんだろう」という疑問も出てきたりしますので、調べるという行為は重要ですが、時間がかかりそうでしたらご質問いただければと思います。

wakazo
wakazo
返信  管理者
3 年 前

なるほど!
アドレスが離れていることをデータシートのメモリマップを見て納得しました。ヘッダファイルですか!気になりますが、深入りせずに次に進んでみます。いつも丁寧にありがとうございます。

PS 以前WEBを参考に作成したプリント基板の拡張用パターンは役に立っています。12F683を搭載して、PWMやADの実験用に改造して使用しています。

管理者
管理者
返信  wakazo
3 年 前

早速のご確認どうもありがとうございます。

はい、あまり深入りせずに全体像をつかんでいただいて、それから細かいところを調べていくといいと思いますよ。

プリント基板の拡張部分を有効に使っていただいているようでよかったです! いろいろ実験していただいて、いずれはご自分のオリジナル作品を作ってみてください!

目次
閉じる