PICマイコン(PIC12F675)のAD変換とタイマーの組み合わせ

組み込みエンジニア

こんにちは、ENGかぴです。

マイコンのソフト開発においてAD変換開始のタイミングをマイコンのタイマーをトリガーにすることが多くあります。PIC12F675のタイマー機能とAD変換を組み合わせて一定の時間間隔でAD変換を行う方法について説明しています。

マイコンによってはAD変換の開始トリガーにタイマを指定できるものもあるためレジスタの設定だけで一定間隔でAD変換を行うことができますが、PIC12F675には指定できるレジスタがありません。PIC12F675のAD変換の使い方は下記記事にまとめています。

PICマイコン(PIC12F675)のAD変換の使い方

PIC12F675を使ってマイコンの動きを勉強するためにPIC12F675の機能でできることについてまとめています。

PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ

AD変換とタイマーについて

アナログ入力には直流の波形であったりCTなどの変流器によって電圧変換された信号が入力されます。直流の波形であればサンプリングタイミングがあまり問題になることはありませんが、正弦波などであればサンプリングタイミングが重要になります。

AD変換のサンプリング

正弦波をアナログ入力にするためには、正弦波に対してサンプリング確度を考えておく必要があります。例えば45度サンプリングや30度サンプリングです。正弦波は0度~360度の周期で表現されるものなので45度サンプリングというのは360度を45度ずつに分けてサンプリングするという意味になります。

45度サンプリングの例
45度サンプリングの例
30度サンプリングの例
30度サンプリングの例

データのプロットするを見ればわかるように30度サンプリングの方が細かくデータが取れていることからデータの精度はよくなります。その分多くのデータを保存する必要とAD変換を頻繁に行う必要があります。

データの中心を2.5としているのはマイコンにアナログ入力として入力する際に中央値である2.5Vを基準にして正弦波を入力することを前提としているからです。

最近の技術動向はサンプリング確度を15度以下で管理することを定められた規格などもあり準拠する際は注意が必要です。

今回例に出しているPIC12F675においてはマイコンの仕様上込み入った演算ができないこと変数を多くできないことから直流波形でサンプリングしたデータの管理の仕方をメインに説明したいと思います。サンプリング時間については50msとします。

移動平均の考え方

AD変換するアナログ入力は基本的にはノイズの影響を受けていることを前提に処理する必要があります。マイコンに取り込む前に周辺回路でノイズキャンセリングを行いますが、ソフトにおいてもフィルタを実装することが多くなります。

インバーター制御においてはノイズ分である高調波成分を利用して処理を判断させるケースもありますが、通常の計測機器であればフィルタを実装して計算します。

移動平均の効果

DC3Vを入力したときにノイズが入っていたことを想定しています。

AD変換した値が黒の系統で移動平均によって計算した値がオレンジの系統になります。移動平均をすることによってDC3Vに近くなっていることが分かります。

これは、アナログ入力のノイズが除去できたともいえます。一般的にノイズは高周波であることが多いのでノイズが除去されていることから移動平均はローパスフィルタの役割として使われます。

リングポインタの考え方

リングポインタの考え方
リングポインタの考え方

今回はAD変換した値をwpを更新しながら配列に入れていき配列の中のデータを使って移動平均で判定値を計算します。

PICマイコン(PIC12F675)の初期化

PICマイコンを使用するためには、マイコンをどのような機能で使用するのかを選択する初期化を行う必要があります。今回はTMR1とADCを使用するまでの初期化について説明していきます。

TMR1の初期化とADCの初期化

TMR1とADCの初期化と使い方については下記記事を参考にしてください。

PICマイコン(PIC12F675)の割り込みの設定と使い方

PICマイコン(PIC12F675)のAD変換の使い方

TMR1はAD変換を開始するトリガーとして使用します。TMR1がオーバフローした時にAD変換を開始してサンプリング時間を一定にします。

初期部分を実装する

#include <xc.h>
//--------------定数定義----------------------------
#define _XTAL_FREQ	4000000
#define VTH3 0x02FF		//電源電圧の3/4
#define VTH2 0x01FF		//電源電圧の2/4
#define VTH1 0x0FF		//電源電圧の1/4
#define	TIME_START_H 0x3C	//タイマスタート位置	65536- 15536 = 50000
#define	TIME_START_L 0xB0	//タイマスタート位置	65536- 15536 = 50000
#define AD_NUM 4		//移動平均フィルタのサンプリング数
#define	BIT_SET	1
#define	BIT_CLR	0

typedef struct{
	char			wp; //buf配列の番号を示すもの
	unsigned short	buf[ AD_NUM ]; //AD変換は10ビットなので2バイトデータとして定義
}FILT_DATA;
//--------------変数定義----------------------------
unsigned short vdd;
FILT_DATA AdData;   //ユーザーが作った構造体の変数
char filtflg;		//ADフィルタ起動
(AD変換完了割り込みでセット)
//--------------関数定義----------------------------
void AdDataSet(void);

void main(void) {
    char i;
   //各種初期化  
    ADCON0 = 0x81;  //ADコンバータの電源をON。結果は右詰。
    ANSEL = 0x11;   //GP0をアナログ入力に使用。変換クロックは8TOSC
    CMCON = 0x07;   //コンパレータ使用しない
    TRISIO = 0x09;  //GP0,GP3入力、その他出力
    GPIO = 0x00;    //ポートの設定 1:High 0:Low
    OPTION_REG = 0x4F;  //プリスケーラ128 WDTで使用
    //割り込み設定
    INTCON = 0xC0;  //PIE1割り込みを許可
    PIE1 = 0x41;    //AD変換終了,タイマ1割り込みを許可
    __delay_ms(10); //ADC起動のウェイト
    //初期化
    vdd = 0;
    filtflg = BIT_CLR;
    AdData.wp = 0;

    for( i = 0; i < AD_NUM; i++ ){
	AdData.buf[ i ] = 0; //初期時に不定値が入っている場合があるのでクリア
    }
    //AD変換起動
    ADIF = BIT_CLR;	//AD変換終了フラグクリア
    //タイマ起動
    T1CON = BIT_CLR;
    TMR1H = TIME_START_H;//タイマ初期化
    TMR1L = TIME_START_L;//タイマ初期化
    TMR1ON = BIT_SET;//タイマ1ON
    TMR1IF = BIT_CLR;//割り込みクリア

リングポインタを使用するためにFILT_DATAという構造体を定義しています。構造体にすると同じ変数内で管理できるため変数のAdDataをDILT_DATAの構造体になるように宣言しています。

マイコンによっては、セクションクリアがメイン関数に入る前に行われるため初期化する必要がないものもありますが、不定値が入っていることで不良動作とならないように初期値でクリアしておくことは重要(確実に初期化がされていることが分かっている不要)です。

ADCとTMR1を実装する

TMR1のタイミングでADCする動作確認の回路図
TMR1のタイミングでADCする動作確認の回路図

AN0に可変抵抗で分圧した抵抗値を入力するようにしています。入力電圧に対する動作のパターンは以下の通りとします。

  1. 1.25V未満:LED1とLED2はともに消灯
  2. 1.25V以上、2.5V未満:LED1点灯、LED2消灯
  3. 2.5V以上、3.75V未満:LED1消灯、LED2点灯
  4. 3.75V以上:LED1とLED2はともに点灯

ADCとTMR1割り込みを実装する

    while( 1 ){
        CLRWDT();
        AdDataSet();

        if( vdd >= VTH1 && vdd < VTH2){ //2.の条件
            GPIO2 = 1;
            GPIO5 = 0;
        }
        else if( vdd >= VTH2 && vdd < VTH3 ){ //3.の条件
            GPIO2 = 0;
            GPIO5 = 1;
        }
        else if( vdd >= VTH3){ //4.の条件
            GPIO2 = 1;
            GPIO5 = 1;         
        }else{ //4.の条件
            GPIO2 = 0;
            GPIO5 = 0;             
        }
    }
//----------------割り込み関数---------------------------------
void __interrupt() intr( void){
    
    if( TMR1IE == BIT_SET && TMR1IF == BIT_SET){ //タイマ1の割り込みであるか
        TMR1IF = BIT_CLR;//割り込みフラグをクリア
        TMR1ON = BIT_CLR;
        TMR1H = TIME_START_H;//タイマ初期化
        TMR1L = TIME_START_L;//タイマ初期化
        TMR1ON = BIT_SET;//タイマ1ON
        GO_DONE = BIT_SET; //タイマーオーバーフロー割り込み内でAD変換開始をセット
    }
    if( ADIE == BIT_SET && ADIF == BIT_SET){
        ADIF = BIT_CLR;
        filtflg = BIT_SET;  //AD変換完了を通知(メイン関数内のAdDataSet()で処理するため)
    }
}

PIC12F675においては割り込みは共通なので割り込みの内容を区別する処理が必要です。TMR1割り込みの処理は50msのタイマーとなるように初期化を行いAD変換開始をスタートします。

AD変換が完了すると割り込みに入るのでAD変換が完了した場合にfiltflgをセットしてメイン関数内部のAdDataSet関数で処理するようにしています。

filtflgでなくGO_DONEのビットで確認してもよいのですが、GO_DONEはAD変換スタートに使用するビットでため別のフラグを用意して区別しています。

移動平均の計算を行う

void AdDataSet(void){
    char i;
    
    if( filtflg == BIT_SET){
        filtflg = BIT_CLR;
        di();
        AdData.buf[ AdData.wp ] = ADRESH;
        AdData.buf[ AdData.wp ] = (AdData.buf[ AdData.wp ] << 8 )+ADRESL;
        //シフト演算を利用して2バイトデータとする
        ei();
        vdd = 0;
        for( i = 0; i < AD_NUM; i++ ){
            vdd += AdData.buf[ i ]; //配列の総和
        }
        vdd = vdd >> 2; //平均の計算のため4で割る(右に2回シフトする)
        if( ++AdData.wp >= AD_NUM ){ //リングポインタの更新
            AdData.wp = 0;
        }
    }    
}

AD変換が完了し割り込みでセットしたfiltflgがセットされていることを確認してAD変換の値をAdData.buf[]の中に格納しています。格納する場所はAdData.wpの示す値になります。

ADデータを格納した後は4つのAD変換値の平均を計算するため合計値を出してから4で割っています。

ポイントは4で割る処理を右に2回のシフトをしていることです。マイコンでは2で割り切れる計算はビットをシフトするだけでよいので計算する負担が軽減できます。移動平均を計算したい場合は配列を2の倍数になるように定義しておくと良いでしょう。

計算が終わった後はAdData.wpの値を更新する処理を入れます。AdData.wpの値が配列からオーバーしないように管理することが重要です。

オーバーしてもコンパイルは通ることが多く想定外の不良動作の原因となることがあります。

移動平均後の値を使ってLEDを点灯/消灯の判定を行っています。初期の段階の4回まではAD変換のデータがそろっていないことから200ms間は正確な判定はできませんが、問題になるようなシステムの場合はメイン関数の無限ループの前に値を確定する処理を行っておくと良いでしょう。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

// CONFIG
#pragma config FOSC = INTRCIO
#pragma config WDTE = ON
#pragma config PWRTE = OFF
#pragma config MCLRE = OFF
#pragma config BOREN = OFF
#pragma config CP = OFF
#pragma config CPD = OFF

#include <xc.h>
//--------------定数定義----------------------------
#define _XTAL_FREQ 4000000
#define VTH3 0x02FF		//電源電圧の3/4
#define VTH2 0x01FF		//電源電圧の2/4
#define VTH1 0x0FF		//電源電圧の1/4
#define	TIME_START_H 0x3C	//タイマスタート位置 65536- 15536 = 50000
#define	TIME_START_L 0xB0	//タイマスタート位置 65536- 15536 = 50000
#define AD_NUM 4		//移動平均フィルタのサンプリング数
#define	BIT_SET	1
#define	BIT_CLR	0

typedef struct{
    char wp; //buf配列の番号を示すもの
    unsigned short buf[ AD_NUM ]; //AD変換は10ビットなので2バイトデータとして定義
}FILT_DATA;
//--------------変数定義----------------------------
unsigned short    vdd;
FILT_DATA AdData;   //ユーザーが作った構造体の変数
char filtflg;		//ADフィルタ起動
//--------------関数定義----------------------------
void AdDataSet(void);

void main(void) {
    char i;
   //各種初期化  
    ADCON0 = 0x81;  //ADコンバータの電源をON。結果は右詰。
    ANSEL = 0x11;   //GP0をアナログ入力に使用。変換クロックは8TOSC
    CMCON = 0x07;   //コンパレータ使用しない
    TRISIO = 0x09;  //GP0,GP3入力、その他出力
    GPIO = 0x00;    //ポートの設定 1:High 0:Low
    OPTION_REG = 0x4F;  //プリスケーラ128 WDTで使用
    //割り込み設定
    INTCON = 0xC0;  //PIE1割り込みを許可
    PIE1 = 0x41;    //AD変換終了,タイマ1割り込みを許可
    __delay_ms(10); //ADC起動のウェイト
    //初期化
    vdd = 0;
    filtflg = BIT_CLR;
    AdData.wp = 0;

    for( i = 0; i < AD_NUM; i++ ){
	AdData.buf[ i ] = 0;
    }
    //AD変換起動
    ADIF = BIT_CLR;	//AD変換終了フラグクリア
    //タイマ起動
    T1CON = BIT_CLR;
    TMR1H = TIME_START_H;//タイマ初期化
    TMR1L = TIME_START_L;//タイマ初期化
    TMR1ON = BIT_SET;//タイマ1ON
    TMR1IF = BIT_CLR;//割り込みクリア

    while( 1 ){
        CLRWDT();
        AdDataSet();

        if( vdd >= VTH1 && vdd < VTH2){
            GPIO2 = 1;
            GPIO5 = 0;
        }
        else if( vdd >= VTH2 && vdd < VTH3 ){
            GPIO2 = 0;
            GPIO5 = 1;
        }
        else if( vdd >= VTH3){
            GPIO2 = 1;
            GPIO5 = 1;         
        }else{
            GPIO2 = 0;
            GPIO5 = 0;             
        }
    }
}
//----------------割り込み関数---------------------------------
void __interrupt() intr( void){
    
    if( TMR1IE == BIT_SET && TMR1IF == BIT_SET){ //タイマ1の割り込みであるか
        TMR1IF = BIT_CLR;//割り込みフラグをクリア
        TMR1ON = BIT_CLR;
        TMR1H = TIME_START_H;//タイマ初期化
        TMR1L = TIME_START_L;//タイマ初期化
        TMR1ON = BIT_SET;//タイマ1ON
        GO_DONE = BIT_SET; //タイマーオーバーフロー割り込み内でAD変換開始をセット
    }
    if( ADIE == BIT_SET && ADIF == BIT_SET){
        ADIF = BIT_CLR;
        filtflg = BIT_SET;  //AD変換完了を通知(メイン関数内のAdDataSet()で処理するため)
    }
}
//----------------ADCデータセット関数--------------------------
void AdDataSet(void){
    char i;
    
    if( filtflg == BIT_SET){
        filtflg = BIT_CLR;
        di();
        AdData.buf[ AdData.wp ] = ADRESH;
        AdData.buf[ AdData.wp ] = (AdData.buf[ AdData.wp ] << 8 )+ADRESL;
        //シフト演算を利用して2バイトデータとする
        ei();
        vdd = 0;
        for( i = 0; i < AD_NUM; i++ ){
            vdd += AdData.buf[ i ]; //配列の総和
        }
        vdd = vdd >> 2; //平均の計算のため4で割る(右に2回シフトする)
        if( ++AdData.wp >= AD_NUM ){ //リングポインタの更新
            AdData.wp = 0;
        }
    }    
}
//---------------------end file----------------------------

関連リンク

PICマイコンを使ってマイコンのレジスタの設定やMPLAB X IDEのプラグインであるMCCを使用して動作確認したことについてまとめています。

PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ

PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ

テックジム-将来のためにプログラミングを学ぶ

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました