今回はPICマイコンのタイマ機能でコンペアモードを使用する方法の紹介です。
コンペアモードとは?
PICマイコンにはタイマ機能という時間を測定する機能(クロックの数を数える機能)があります。
タイマ機能を利用すると、一定数カウントした時点で外部のピンの出力を変化させたり割込みを発生させたりすることができます。
コンペアモードはこの機能をほぼそのまま使うようなもので、あらかじめ設定したカウント数に到達したら指定の処理をさせたい場合に使えます。
今回使用したマイコン
今回使用したマイコンはPIC16F819になります。
データシートは下記の秋月電子様のサイトから入手可能です。
ソースコード
最初にソースコード全体を共有します。
以下のgithubに上がっているのでzipファイルか何かでダウンロードすれば使用可能です。
※今回のソースファイルでは1、0ではなくSET、CLEAR等のマクロを使っているので、下記のソースファイル全体をダウンロードしないとコンパイルエラーになります。(SET→1、CLEAR→0にマクロを設定しました)。
ソースコードの構成は下記のようになっています。
ソースファイルの追加方法や空のプロジェクト作成方法は以前の記事で紹介しているので必要があれば確認をお願いします。
基本的には以下のソースをダウンロード・解凍し、MPLABので作成した空のプロジェクトにドラッグ&ドロップするだけです。
-
【PIC】MPLABで作成したプログラムをPICKIT3で書き込む方法を紹介する
あまり新しい情報がなかったのでまとめてみました。 開発環境(ソフト)->開発環境(ハード)->プロジェクトの作成->実際に書き込みの順で書いてあります。 多少参考になるかもしれん。 環境整備とかからち ...
続きを見る
設定コードの解説:register_setup.c / timer1_setup(void)
今回はTimer1を使って試してCCPモードを設定しました。今回作成したソースについてサラッと解説してみることにします。
ソース全体
void timer1_setup(void)
{ /*T1CON*/ /*datasheet P.59*/
/*T1CON*/
T1CONbits.T1CKPS1 = SET; /*(Fsoc/4)の8分周 */ /* 2MHz/8 = 250kHz(0.000,004 sec) */
T1CONbits.T1CKPS0 = SET;
T1CONbits.T1OSCEN = CLEAR; /*for external clock*/
T1CONbits.nT1SYNC = SET; /*for external clock*/
T1CONbits.TMR1CS = CLEAR; /*Internal clock*/
T1CONbits.TMR1ON = CLEAR;
/*CCP1CON*/ /*datasheet P.67*/
/*CCP1CONbits_t.CCP1X = SET/CLEAR;*/ /* コンペア・モードでは特に使わない */
/*CCP1CONbits_t.CCP1Y = SET/CLEAR;*/ /* コンペア・モードでは特に使わない */
CCP1CONbits.CCP1M3 = SET;
CCP1CONbits.CCP1M2 = CLEAR;
CCP1CONbits.CCP1M1 = SET;
CCP1CONbits.CCP1M0 = SET;
/*CCPR1H,CCPR1L*/
/* 動作周波数設定 = 10ms = 0.000,004 sec * 25000(0x61A8) */
/* 0までロールオーバー(MAXの次に0を数えて1カウントする)ので、1引く */
CCPR1H = 0x61;
CCPR1L = 0xA7; /* Timer0でも十分 */
}
コンペア・モード使用時に設定するレジスタ
timer1でコンペア・モードを使用するには以下のレジスタを設定する必要があります。
・T1CON
・CCP1CON
・CCPR1H
・CCPR1L
レジスタの設定関連なので少し難しく見えますが、基本的にはレジスタの各ビットを1(ON)か0(OFF)に設定して経路を選択するだけです。
内容の解説を上から順にサラッとやってみます。
クロックのソースを内蔵発振器に設定
今回使用したTimer1のブロック図は以下のようになっています。(データシートP.58)
まずはTimer1のクロックを選択するために、TMR1CSをを設定します。
今回はPICマイコン内蔵の発振器、Internal Clockを使うため、TMR1CS=1に設定しました。
T1CONbits.TMR1CS = CLEAR; /*Internal clock*/
クロックのプリスケーラ(分周比)の設定
次にプリスケーラを設定します。
タイマではカウントできるクロックの上限に限界があるため(timer1は16bitなので、2^16=65535)、長い時間を図りたい場合はクロックを分周して1クロック当たりの時間を長くする必要があります。
今回は8分周を利用したいため、T1CKPS1=1、T1CKPS0=1に設定しました。
※Foscで設定した内蔵発振器の周波数をすでに4分周したものをさらに8分周(32分周)している点に注意が必要です。
今回の場合内蔵発振器の周波数を8MHzに設定しています。
まず、8MHzの1/4が入ってくるので、2MHzになります。
それを8分割するので、Timer1のカウントアップクロックは250kHz(0.000,004 sec)に設定された、ということになります。
T1CONbits.T1CKPS1 = SET; /*(Fsoc/4)の8分周 */ /* 2MHz/8 = 250kHz(0.000,004 sec) */
T1CONbits.T1CKPS0 = SET;
クロックのシンクロ(タイミング補正)設定
次にクロックのシンクロ設定を行います。
PIC16F819ではクリスタル発振子などを利用して外部からクロックを得る場合があります。(この場合20MHz程度まで行けます。)
こういった場合にTimer0、Timer1、Timer2などでクロックの基準がずれないようにそろえる機能がSynchronizeになります。
ただしデータシートを読んだ限りでは、内蔵発振器使用時には特に意味がないようです。
なので無効化しておきます。
データシートを見ていると、ビットを負論理で設定するものがあります。
これらは先頭にnがついています。
T1CONbits.nT1SYNC = SET; /*for external clock*/
カウント上限値の設定
次にカウント上限値の設定を行います。
タイマ動作許可・停止の設定
次にタイマのカウント動作の許可・停止の設定を行います。
タイマのクロックを設定して動作を開始した場合、指定のクロック時間ごとにカウントされ、何回カウントしたかどうかがTMR1HとTMR1Lレジスタに保存されます。
PIC16F819は8bitマイコンなので、この二つのレジスタをつなげて使用して16bitタイマとして動作させています。
今回は指定のタイミング(メイン処理に入るまで)は割込みを発生させたくないので、初期値としてOFFに設定しておきました。
こうすることでTMR1HとTMR1Lには何もカウントがたまりません。
T1CONbits.TMR1ON = CLEAR;
カウント上限値を設定する
次にカウント上限値を設定します。
今回はコンペア・モードでの割込みを使用します。コンペアモードのの割込み要求はCCP1IFになります。
動作としては、timer1のカウントアップでたまっていくTMR1H/TMR1Lの値と、CCPR1H/CCPR1Lにあらかじめ設定した値が一致した際にCCP1IFビットが1にセットされます。
今回は100ms間隔で定期的に処理を実行させたい場合を考えます。
まず1クロックで経過する時間はプリスケーラの設定で0.000,004 secに設定しました。
100msec (0.1sec)を作り出したい場合、0.1 / 0.000,0004 = 25000カウントに設定すれば良いことになります。
※注意:timer1は16bitタイマなので、65535カウントを超えたカウントを設定することができません。
25000は10進数ですが、16進数に直すと0x61A8になります。
またカウントは25000で一致して次に0にリセットされるため、25000→0→1→・・・→25000という動作になり、実際には25001回カウントされてしまいます。
なので1引いた24999、16進数だと0x61A7に設定をします。
CCPR1H = 0x61;
CCPR1L = 0xA7;
コンペア・モードで動作するように設定する
次にtimer1の動作モードをコンペア・モードに設定します。
timer1の動作モードは、キャプチャ、コンペア、PWMの3つ(頭文字をとると"C""C""P")がありますが、そのうちコンペアモードは3つの動作があります。
①TMR1H/LとCCPR1H/Lの中身が一致したらCCP1ポートを1にセット、CCP1IF割込み要求を1にセット
②TMR1H/LとCCPR1H/Lの中身が一致したらCCP1ポートを1にクリア、CCP1IF割込み要求を1にセット
③TMR1H/LとCCPR1H/Lの中身が一致したらCCP1ポートは変更なし、CCP1IF割込み要求を1にセット
④TMR1H/LとCCPR1H/Lの中身が一致したらCCP1ポートは変更なし、CCP1IF割込み要求を1にセット、TMR1H/Lのカウント値をリセット
CCP1ピンは8ピンか9ピンのどちらかになります。
どちらのピンにCCP1の出力を割り当てるかは、config_bits.h(#pragma関連の設定ファイル)にて設定してあります。今回はCCP1です。
今回はCCP1割込みが発生したらTMR1H/Lレジスタをリセットして定期的に(100ms間隔で)処理を実行させたいので、④のモードを使用することにします。
CCP1CONbits.CCP1M3 = SET;
CCP1CONbits.CCP1M2 = CLEAR;
CCP1CONbits.CCP1M1 = SET;
CCP1CONbits.CCP1M0 = SET;
・外部ピンを変化させることができるのは①と②のモードの時だけです。
・①と②のモードではピンの状態を0→1または1→0に設定するだけで、トグル動作(1⇔0の切り替え)をするわけではありません。
・TMR1H/LとCCPR1H/Lが一致した場合、ソフトウェア割込み(CCP1IF)は①~④のすべてのモードでセットされます。
・③のモードではソフトウェア割込みだけが発生します。
・④のモードのみ、TMR1H/LとCCPR1H/Lが一致した場合にTMR1H/Lの内容をリセットし、一定周期での繰り返し動作が可能です。
④のモード以外で指定の周期で動作させたい場合、TMR1H/Lレジスタをソフト上で0にリセットする記述をする必要があります。
使わない機能について:クロックシンクロ設定、外部クロック許可
このほかに外部クロックの入力の許可・不許可を決めるOSCENビットがありますが、今回はそもそも内蔵発振器を使用するのでどっちに設定しても問題ありません。
未使用の機能です。
割込み処理の解説:interrupt.c
次にCCP1割込み(CCP1IF=1にセット)があった場合の割込み処理について解説します。
ソース全体
static void interrupt isr(void)
{ /*割込みベクタは1本、全部ここに呼ばれる*/ /*datasheet P.98*/
/*割込みされた時点で他の割込み禁止*/
INTCONbits.GIE = CLEAR; // グローバル割り込み禁止
INTCONbits.PEIE = CLEAR; // リフェラル割込み禁止
if(INTCONbits.TMR0IF == SET)
{
interrupt_timer0_tmr0if();
INTCONbits.TMR0IF = CLEAR;
}
if(PIR1bits.TMR1IF == SET)
{
interrupt_timer1_tmr1if();
PIR1bits.TMR1IF = CLEAR;
}
if(PIR1bits.CCP1IF == SET)
{
interrupt_timer1_ccp1if();
PIR1bits.CCP1IF = CLEAR;
}
INTCONbits.GIE = SET; // グローバル割り込み許可
INTCONbits.PEIE = SET; // リフェラル割込み許可
}
static void interrupt_timer1_ccp1if(void)
{
//TMR1H = 0x00; /*現在のカウンタリセット*/ /*④のモードでは自動でリセットされるので不要*/
//TMR1L = 0x00; /*現在のカウンタリセット*/ /*④のモードでは自動でリセットされるので不要*/
// 出力ポートを反転させる
if(PORTAbits.RA0 == SET)
{
PORTAbits.RA0 = CLEAR;
}
else
{
PORTAbits.RA0 = SET;
}
}
割込み処理の発生と呼び出しについて
割込み要求が発生した場合の処理ですが、データシートのP.98にある図を見ると度の割込み要求でどの割り込みが実行されるのかわかります。
ですが、PIC16F819の場合、割込みベクタが1本しかなく、その下に連なるように各割込み要求が配置されています。
なのですべての割込み要求は1つの割込みに集約されます。
この集約された結果の1本の割込みですが、下記のような記述で呼び出すことができます。
static void interrupt isr(void)
{ /*割込みベクタは1本、全部ここに呼ばれる*/ /*datasheet P.98*/
/* ここに割込み時に動作させたい処理を記述する */
}
しかしこの記述だけだと、何の割込み要求は発生した割込みなのか分からず、「この割込みが発生したら、この処理を実行!」みたいなことができません。
なのでこの割込み処理内に以下のように追記します。
static void interrupt isr(void)
{ /*割込みベクタは1本、全部ここに呼ばれる*/ /*datasheet P.98*/
/*割込みされた時点で他の割込み禁止*/
INTCONbits.GIE = CLEAR; // グローバル割り込み禁止
INTCONbits.PEIE = CLEAR; // リフェラル割込み禁止
if(INTCONbits.TMR0IF == SET) /*timer0のオーバーフロー割込み要求*/
{
interrupt_timer0_tmr0if();
INTCONbits.TMR0IF = CLEAR;
}
if(PIR1bits.TMR1IF == SET) /*timer1のオーバーフロー割込み要求*/
{
interrupt_timer1_tmr1if();
PIR1bits.TMR1IF = CLEAR;
}
if(PIR1bits.CCP1IF == SET) /*timer1のコンペア割込み要求*/
{
interrupt_timer1_ccp1if();
PIR1bits.CCP1IF = CLEAR;
}
INTCONbits.GIE = SET; // グローバル割り込み許可
INTCONbits.PEIE = SET; // リフェラル割込み許可
}
これでどの割込み要求がセットされているかを見て、処理を分岐させることができます。
ただし多重割込みが発生した場合(割込み中に別の割込みが発生した場合)については、最初に配置した方の割込みに入ってしまうので、別途工夫が必要になります。
今回はそのような処理を追加したくなかったので、割込みがあった時点で他の割込みを禁止しています。
割込み処理内の関数
今回はCCP割込みが発生した場合の処理を下記のようにしてみました。
static void interrupt_timer1_ccp1if(void)
{
//TMR1H = 0x00; /*現在のカウンタリセット*/
//TMR1L = 0x00; /*現在のカウンタリセット*/
// 出力ポートを反転させる
if(PORTAbits.RA0 == SET)
{
PORTAbits.RA0 = CLEAR;
}
else
{
PORTAbits.RA0 = SET;
}
}
今回はコンペア・モードのうち、TMR1H/LレジスタがCCP割込みで自動でリセットされるモード(先ほどの④)を使用しています。
なのでTMR1HとTMR1Lをゼロクリアさせる処理は不要です。
今回はオシロスコープで波形確認を行いたかったので、RA0ポート(17番ピン)を割込みのたびに反転させるようにしてみました。
オシロスコープで波形確認
最後に動作確認として、オシロスコープで波形確認を行ってみます。
今回はピンからの出力確認だけなのでPIC直接プローブを当てて測定しました。
波形としては以下のような感じです。
100msちょうどでRA0(17番ピン)の出力が反転していることが確認できます。
動作確認は以上です。
まとめ・疑問点など
今回はPICのタイマ機能について、CCPのうちのコンペア・モードについて紹介してみました。
PICは設定関連が非常に少ない上に、マイコンの種類が違っても似たような記述でいいのでとても勉強になりそうです。
今後も更新していきたいと思います。
唯一残った疑問点としては、timer1のクロックがすでに1/4されている点でしょうか?
1命令の実行に必要なサイクル数よりも短い周期で動作するように設定すると、たとえ外部ピンの出力変化だけでもどうだ周期が不安定になるからでしょうか?
時間がある時に調べておきたいです。
それでは、また。