今回は、下の動画にあるような正確な時間を刻む時計を作っていきます。
割り込み
PICなどのマイコンには、割り込みと呼ばれる機能が付いています。これは、その名の通り他のプログラムが動いている途中に違う処理を割り込ませるという処理をしてくれます。
例えば、while文の中で常にLEDの点滅をする処理をしていたとしましょう。ここで、ボタンを押したらブザーを鳴らすという処理を追加したい場合、while文の中にif文を入れますね。例えばRA0ピンにボタンがつながっていて、RA1にLEDが、RA2にブザーがつながっているとすると、
while(1){
RA1 = 1;
__delay_ms(1000);
RA1 = 0;
__delay_ms(1000);
if(RA0 == 1){
RA2 = 1; //ブザーを鳴らす
__delay_ms(500);
RA2 = 0; //ブザーをオフ
}
}
としますよね。これを実行してみると、非常に使い勝手が悪いことが分かります。LEDが点灯した時にボタンを押したとしましょう。すると、if文に到達するまで__delay_ms(1000)が2回ありますから、2秒は待っていないとブザーはなってくれません。できれば押した瞬間にブザーが鳴ってほしいですよね(それはそう)。
これを実現するのが、割り込みです。具体的には、別の割り込み関数を用意しておいて、割り込み条件が満たされた場合即座にその関数に飛ぶという動作をします。
割り込みの種類
割り込み条件は複数用意されています。例えば、先程示したピンの入力による割り込み。他にはTimerカウンタのオーバーフロー時の割り込みなどはよく使われます。今回は、正確なタイマーを作るので、Timer割り込みを使っていきます。
Timer1
PIC16F1938には、Timer1割り込み機能が付いています。
Timer割り込みとは、もとから用意されているTMR1レジスタがオーバーフローしたときに割り込み関数に飛ぶ機能です。割り込み機能がオンになった瞬間から、自動的にTMR1レジスタがカウントアップしていきます。このカウントアップはクロック周波数数回に1回行われるので、逆算すれば正確な時間が数えられます。
カウントアップは、他のプログラムが動いていようが関係なく確実に行われます。
Timer割り込み
と、なんだか難しい言葉が出てきましたが、動けばいいんです。プログラムの例を見てみましょう。
void intrInit(){ //割り込みの初期設定
T1CON = 0b00110001; //内部クロックの4分の1で,プリスケーラ1:8でカウント
TMR1H = (55536 >>8); //タイマー1の初期化(65536-10000=55536);
TMR1L = (55536 & 0x00ff);
TMR1IF = 0; //タイマー1割り込みフラグを0にする
TMR1IE = 1; //タイマー1割り込みを許可する
INTCONbits.GIE = 1; //グローバル割り込みを許可
INTCONbits.PEIE = 1; //割り込みを許可
}
これが、割り込み処理の初期化関数です。T1CONは何秒に1回カウントアップするかを設定するレジスタです。
T1CON = 0baabb0001; としたとき、
aa
00 = Fosc/4 01 = Fosc 10 = 外部クロック 11 = CAPOSC
bb
00 = 1倍 01 = 2倍 10 = 4倍 11 = 8倍
となっています。Foscはクロック周波数です。
クロックが32MHzの場合を考えてみます。ここで、aa = 00, bb = 11と設定しましょう。1クロックにかかる時間は1/32MHzです。aaは00,つまりFosc/4で、これは4クロックで1回カウントアップを表します。また、bbは11で8倍,つまりカウントアップする周期を8倍することになります。
よって、1回カウントアップするのに4*8 = 32クロックを要する設定です。1クロック1/32MHz 秒かかるので、1/32MHz * 32 = 1μs経つごとにカウントアップされます!
「さっきからカウントアップって、いったい何がカウントアップされるんだよ…」とお思いでしょう。これが、TMR1レジスタです。少し長いのでTMR1HとTMR1Lに分かれています。TMR1は16bit,つまり0~65535までの値をとります。これが65535を超えた、つまりオーバーフローした時に割り込みがかかります。上のプログラムでは65536から10000を引いた数をデフォルト値としてTMR1レジスタに代入しています。ここからカウントアップがスタートするので、10000*1μs = 10ms経ったところで割り込みがかかります。
その後ろのTMR1IF等は、ほぼ呪文です。そのままコピペで問題ありません。TMR1IFに関しては後で少し説明があります。
void interrupt isr(){ //割り込み関数
volatile static int intr_counter;
GIE = 0;
if(TMR1IF == 1){
TMR1H = (55536 >>8); //タイマー1の初期化(65536-10000=55536);
TMR1L = (55536 & 0x00ff);
intr_counter++;
if(intr_counter == 100){ //1sec周期でtimerに1を足す
timer++;
intr_counter = 0;
}
TMR1IF = 0; //割り込みフラグを落とす
}
GIE = 1;
}
これは、割り込みがかかった時に強制的に飛ばされる関数です。関数名の先頭にinterruptと付けることで割り込み関数になります。
今回は、秒数を数えるintr_counterという定数を定義しました。割り込みがかかったら、まずしなければいけないのは割り込みの禁止です。そうしないと割り込み中に割り込まれてしまいます。これは、GIE = 0;とすればOKです。
次に、割り込みがTimer1割り込みかどうかを調べます。もしそうならば、TMR1IFレジスタに1が勝手に代入されています。これを、フラグが立つといいます。if文で確認したら、まずTMR1レジスタを巻き戻しましょう。同じように55536を入れればいいだけですね。
次に、intr_counterをカウントアップします。この割り込み関数は10msごとに呼ばれるので、intr_counterは10ms刻みの時間を記録していきます。これが100,つまり1秒になった時、timerという変数をカウントアップします(timerは関数の前に定義されている必要あり)。これで、開始からの正確な時間をtimerという変数で取得できるようになりました!
最後に、TMR1IFを0にしましょう(フラグを落とす、といいます)。これをしないと、この関数が終わってすぐにまた割り込み関数が呼び出されてしまいます。最後に割り込み許可GIE = 1;も忘れずに。
完成プログラム
今回は、I2CLEDをPICで使うを参考にtimerの値をLCDに表示させてみました。
下記のプログラムをコピペして書き込めば、正確な秒数がLCDに表示されるでしょう。といっても、実はクロックの製品誤差があるので、本当に正確にするにはTMR1から引く数を10000より少し増減させる必要があったりします。スマホのストップウォッチなどを横に置き、1時間計測→3600よりどれだけ外れているかを調べ、校正する等の方法もあります。
// PIC16F1938 Configuration Bit Settings
// 'C' source line config statements
// 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 = OFF // Brown-out Reset Enable (Brown-out Reset disabled)
#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 VCAPEN = OFF // Voltage Regulator Capacitor Enable (All VCAP pin functionality is disabled)
#pragma config PLLEN = ON// PLL Enable (4x PLL disabled)
#pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LVP = ON // Low-Voltage Programming Enable (Low-voltage programming enabled)
// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.
#include <xc.h>
#include <pic16f1938.h>
#include <math.h>
#include <stdio.h>
#define _XTAL_FREQ 32000000
#define LCD_ADD 0x7C
#define sound RA1
int timer = 0;
void I2C_Master_Init(const unsigned long c)
{
SSPCON1 = 0b00101000;
SSPCON2 = 0;
SSPADD = (_XTAL_FREQ/(4*c))-1;
SSPSTAT = 0b00000000 ; // 標準速度モードに設定する(100kHz)
}
void I2C_Master_Wait()
{
while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F));
}
void I2C_Master_Start()
{
I2C_Master_Wait();
SEN = 1;
}
void I2C_Master_RepeatedStart()
{
I2C_Master_Wait();
RSEN = 1;
}
void I2C_Master_Stop()
{
I2C_Master_Wait();
PEN = 1;
}
void I2C_Master_Write(unsigned d)
{
I2C_Master_Wait();
SSPBUF = d;
}
void writeData(char t_data){
I2C_Master_Start();
I2C_Master_Write(LCD_ADD);
I2C_Master_Write(0x40);
I2C_Master_Write(t_data);
I2C_Master_Stop();
__delay_ms(10);
}
void writeCommand(char t_command){
I2C_Master_Start();
I2C_Master_Write(LCD_ADD);
I2C_Master_Write(0x00);
I2C_Master_Write(t_command);
I2C_Master_Stop();
__delay_ms(10);
}
void PICinit(){
OSCCON = 0b01110000;
ANSELA = 0b00000000;
ANSELB = 0b00000000;
TRISA = 0b00000000;
TRISB = 0b00000000;
TRISC = 0b00011000;
PORTA = 0b00000000; //2進数で書いた場合
PORTB = 0x00; //16進数で書いた場合
}
void LCD_Init(){ //LCDの初期化
I2C_Master_Init(100000);
__delay_ms(400);
writeCommand(0x38);
__delay_ms(20);
writeCommand(0x39);
__delay_ms(20);
writeCommand(0x14);
__delay_ms(20);
writeCommand(0x73);
__delay_ms(20);
writeCommand(0x52);
__delay_ms(20);
writeCommand(0x6C);
__delay_ms(250);
writeCommand(0x38);
__delay_ms(20);
writeCommand(0x01);
__delay_ms(20);
writeCommand(0x0C);
__delay_ms(20);
}
void LCD_str(char *c) { //LCDに配列の文字を表示
unsigned char i,wk;
for (i=0 ; ; i++) {
wk = c[i];
if (wk == 0x00) {break;}
writeData(wk);
}
}
void interrupt isr(){ //割り込み関数
volatile static int intr_counter;
GIE = 0;
if(TMR1IF == 1){
TMR1H = (55536 >>8); //タイマー1の初期化(65536-10000=55536);
TMR1L = (55536 & 0x00ff);
intr_counter++;
if(intr_counter == 100){ //1sec周期でtimerに1を足す
timer++;
intr_counter = 0;
}
TMR1IF = 0; //割り込みフラグを落とす
}
GIE = 1;
}
void intrInit(){ //割り込みの初期設定
T1CON = 0b00110001; //内部クロックの4分の1で,プリスケーラ1:8でカウント
TMR1H = (55536 >>8); //タイマー1の初期化(65536-10000=55536);
TMR1L = (55536 & 0x00ff);
TMR1IF = 0; //タイマー1割り込みフラグを0にする
TMR1IE = 1; //タイマー1割り込みを許可する
INTCONbits.GIE = 1; //グローバル割り込みを許可
INTCONbits.PEIE = 1; //割り込みを許可
}
int main(void){
PICinit(); //PICを初期化
LCD_Init();
intrInit();
char time[16];
while(1){
sprintf(time,"Timer:%d",timer);
writeCommand(0x02);
LCD_str(time);
}
}
今回はある程度正確な時間を作りました。これを使えば、普段は電子蛍プログラムが常に動いているけれど、ある秒数経ったらブザーが鳴る目覚まし時計なんかも作れそうです。必ず7時間寝ると決めているんだ!でも夜は少しLEDの点滅が欲しいなんて場合には、while文の中でLEDを点滅させておいて、中にif(timer==3600*7)という文を入れておきましょう。このif文の中にスピーカーを鳴らす命令を書いておけば、いい感じの目覚まし時計ができます。
割り込み
難しいが これを理解すればレベルアップできそうだな
がんばるぞ
65536 ?
> > ?
tmr1 レジスタが難しい
>> tmr1h ??
1101 1000 1111 0000
これになおしたら分かった気がする????