105.タイマー割込みで効率的な制御


2001/03/21 【ソフトウエア編TOPに戻る】

ここでは、タイマー割込みを使った制御について、以下の内容をご紹介します。

1.割込みとは

2.割込みを使わない場合のタイマー動作

3.割込みを使ったタイマー動作

4.割込みをグローバル変数に対応したSTARTUPオブジェクト

5.tmtest22.cはどんな動作をするか


105-1.割込みとは

マイコンのプログラムは通常、記述された命令をひとつひとつ順番に実行していきます。では、予測できないタイミングで飛び込んでくる外からの情報には、どうやって対応したら良いのでしょうか?

ひとつにはポーリングという方法があります。これは、入ってくるかもしれない外からの情報を、一定間隔で常にチェックしにく方法です。入るか入らないか分からない情報を、いつも気に掛けていなければならないので、効率が悪いのです。

そして、もうひとつの方法が割込みです。割込みとは、良く仕事中に掛かってきた電話に例えられます。仕事中はその作業に専念していますが、電話のベルが鳴ると仕事の手を止め、電話に出ます。そして電話が終わったら、再び先ほどの仕事の続きに戻ります。

マイコンの世界でもこれと同じことをします。割込み要因が発生したら主となるプログラムの処理を一旦中断し、割込みの要因に合わせた分岐先に飛んで必要な処理を行います。そして、その処理が終わったら、再び主となるプログラムの続きに戻ります。つまり、割込みが発生するまでは、主となるプログラムの実行に専念できるのです。

割込みには沢山の要因があります。タイマー割込みはその中のひとつで、内蔵されるタイマーで設定してある時間が経過した時に、割込みを発生させます。

タイマー機能については、ソフトウエア編の103.タイマーで正確な時間稼ぎをするを参照して下さい。


105−2.割込みを使わない場合のタイマー動作

割込みを使用した場合のすばらしさ(?)を実感して頂くため、先ずは割込みを使用しない場合の事例をご紹介しようと思います。

以下のプログラムは、ポート5の2bitのLEDを1秒ごとに点滅させます。同時に、ポート2のディップスイッチの状態を、ポート1の8bitのLEDに表示させます。ここをクリックするとダウンロードできます。(tmtest21.c)

ハードウエアについては、電子回路編の3.AKI-H8開発キットの回路を参照して下さい。

/************************************************/
/* 割込みを使わないタイマー例  RIKIYA 2001/03/25*/
/*                                   tmtest21.c */
/* H8-3048F                                     */
/* ポート5のLEDをwait( )関数で1秒おきに点滅   */
/* させ、同時にポート2のディップスイッチの状態 */
/* を、ポート1の8bitのLEDに表示させる          */
/* ディップスイッチを切り替えてから、8bitのLED  */
/* に状態が表示されるタイミングに注目           */
/************************************************/

#include <3048f.h>

/* 待ち時間発生初期化 *********************************/
void timer_init(void){
     ITU0.TCR.BYTE = 0x23;   /* GRAコンペアマッチ clock 1/8 */
     ITU0.GRA = 0x07d0;      /* GRAを2000に設定       */
     ITU.TSTR.BIT.STR0 = 0;  /* カウント停止状態          */
return;
}

/* 待ち時間発生 引数に、必要なミリ秒を指定する***********/
void wait(int msec){
int i;
     ITU.TSTR.BIT.STR0 = 1;      /* ITU0 TCNTカウント開始    */
     for(i=0;i<msec;i++){
          do{           /* TCNT = GRAになるまで待つ(1mS) */
          }while(ITU0.TSR.BIT.IMFA == 0);
          ITU0.TSR.BIT.IMFA = 0; /* 検知フラグを戻して再開 */
     }
     ITU.TSTR.BIT.STR0 = 0;      /* ITU0 TCNTカウント終了    */
return;
}

/* メイン関数 *****************************************/
/* Port5のLED1とLED2を、1秒ごとに点滅させる */
/* Port2のDIPSW入力を、Port1の8bit LEDに表示させる */

void main(void){

     timer_init();               /* timerの初期化            */
     P1.DDR = 0xff;              /* port1出力に設定 表示LED */
     P2.DDR = 0x00;              /* port2入力に設定 DIPSW   */
     P2.PCR.BYTE = 0xff;         /* port2プルアップon        */
     P5.DDR = 0xff;              /* port5出力に設定 表示LED  */
     P5.PCR.BYTE = 0x00;         /* port5プルアップoff       */

     while(1){
          P5.DR.BYTE = 0x01;     /* LED1を点灯               */
          wait(1000);            /* 1000mS待つ               */

          P5.DR.BYTE = 0x02;     /* LED2を点灯               */
          wait(1000);            /* 1000mS待つ               */

          P1.DR.BYTE = P2.DR.BYTE;   /* DIPSWを8bitLEDに表示 */
     }
}

【先頭に戻る】


105−2−1.プログラムtmtest21.cの説明

このプログラムは、ソフトウエア編の103.タイマーで正確な時間稼ぎをすると、ほとんど一緒ですので、細かい説明はそちらを参照して下さい。

違う点は、main( )関数の中身で、以下の部分が追加されている点です。

■PIOポートの初期化

    P1.DDR = 0xff;          /* port1出力に設定 表示LED */
    P2.DDR = 0x00;          /* port2入力に設定 DIPSW   */
    P2.PCR.BYTE = 0xff;     /* port2プルアップon        */

■ディップスイッチの状態を8bitLEDに表示

    P1.DR.BYTE = P2.DR.BYTE; /* DIPSWを8bitLEDに表示 */

ここでは、ポート5の2bitのLEDを点滅させる仕事の合間に、ディップスイッチの状態を8bitのLEDに表示させています。

【先頭に戻る】


15−2−1.tmtest21.cはどんな動作をするか

では、どんな動作をするか見てみましょう。tmtest21.cを実行すると、2bitのLEDが1秒間隔で点滅を開始します。8bitのLEDは、ディップスイッチのon/offの状態を表示してるはずです。

ここで、ディップスイッチのon/offを適当に切り替えてみましょう。8bitのLEDの点灯状態もそれに合わせて変わりましたか?でも、ここで注意して頂きたいのが、8bitのLEDの表示が変わるタイミングです。

ディップスイッチを切り替えたタイミングで表示が切り替わるのではなく、2bitのLEDが1回点滅を繰り返したタイミングで表示が切り替わっている様子が分かりますか?

tmtest21.cのプログラムを見れば当たり前の動作で、別に不思議なことはありません。でも、ディップスイッチを切り替えたタイミングで表示も変化させたい場合はどうしたら良いのでしょうか?

wait( )関数では、指定の時間が経過するまで、CPUは他の仕事を全て止めて待っています。なので、ディップスイッチの状態を見て、即座に8bitのLEDに表示させるような余裕が無いのです。wait( )関数を使う以上、これはどうしようもないのです。

for(i=0;i<1000;i++){

     wait(1);

     P1.DR.BYTE = P2.DR.BYTE;

}

上記のようなことをすると、1/1000秒ごとにディップスイッチの状態を表示してくれますが、今度は正確に1秒間を計測することが難しくなるでしょう。第一、効率的な処理とは言えませんよね。

【先頭に戻る】


105−3.割込みを使ったタイマー動作

では次に、タイマー割込みを使って同じことをやってみましょう。ポート5の2bitのLEDを1秒ごとに点滅させ、同時にポート2のディップスイッチの状態を、ポート1の8bitのLEDに表示させます。ここをクリックするとダウンロードできます。(tmtest22.c)

/**************************************************/
/* 割込みによるタイマー例       RIKIYA 2000/03/25 */
/*                                     tmtest22.c */
/* H8-3048/F                                      */
/* ポート5のLEDを割込み処理で1秒おきに点滅      */
/* させ、同時にポート2のディップスイッチの状態   */
/* を、ポート1の8bitのLEDに表示させる            */
/* ディップスイッチを切り替えてから、8bitのLED    */
/* に状態が表示されるタイミングに注目             */
/**************************************************/

#include <3048f.h>

#pragma interrupt(intimia1)
extern int cnt;     /*グローバル変数 startup.marでもexport宣言*/
extern int mode;    /*グローバル変数 startup.marでもexport宣言*/

/* メイン関数************************************************/
void main(void){

     P1.DDR = 0xff;          /* port1出力に設定 表示LED */
     P2.DDR = 0x00;          /* port2入力に設定 DIPSW   */
     P2.PCR.BYTE = 0xff;     /* port2プルアップon        */
     P5.DDR = 0xff;          /* port5出力に設定 表示LED  */
     P5.PCR.BYTE = 0x00;     /* port5プルアップoff       */

     ITU1.TCR.BYTE = 0x23;   /* GRAコンペアマッチ clock 1/8    */
     ITU1.GRA = 0x4e20;      /* GRAを4e20に設定 約10ms   */
     ITU1.TIER.BYTE = 0xF9;  /* ITU1のGRAによるコンペアマッチ割込みを許可*/
     ITU.TSTR.BIT.STR1 = 0;  /* カウント停止状態             */
     ITU.TSTR.BIT.STR1 = 1;  /* ITU1 TCNTカウント開始        */

     cnt = 0;      /* 割込み発生の回数を0にセット        */
     mode =1;      /* 2bitLEDの表示のさせ方を1にセット  */

     while(1){
          P1.DR.BYTE = P2.DR.BYTE;  /* DIPSWを8bitLEDに表示 */
     }
}

/*割り込み処理*********************************************/
void intimia1(void){

     cnt++;                        /* cntの数を1つ増やす   */

     if(cnt == 100){               /*100(1秒)なら2bitLEDの表示を変化させる*/
          cnt = 0;                 /*カウンタを0に戻す     */
          switch(mode){
               case 1:mode = 0x02; /*表示が01なら10にする  */
                     break;
               case 2:mode = 0x01; /*表示が10なら01にする  */
                     break;
               default:mode = 0x01;/*それ以外なら01にする  */
          }
          P5.DR.BYTE = mode;       /*2bitLEDを変化させる   */
     }
     ITU1.TSR.BIT.IMFA = 0;        /* 割込み検知フラグを戻して再開 */
}

【先頭に戻る】


105−3−1.プログラムtmtest22.cの説明

このプログラムでは、AKI-H8/3048Fに内蔵されるタイマーITU1を使用しています。ITUのGRAだけを単純に使用した場合、最長で約32mSまでのタイマーが限界なので、1秒間(1000mS)を計測するためにITU1のGRAで10mSごとに割込みを発生させ、100回目の割込みで2bitのLEDの表示を点滅させます。

以下にプログラムの詳細をご紹介します。

■3048fヘッダファイルの読み込み

#include <3048f.h>

 

■割込み関数の定義

#pragma interrupt(intimia1)

定義といって良いのかよくわかりませんが、割込み発生時にintimia1という関数を実行することを設定しています。このintimia1という関数がITUGRAによる割込みだ、というのは、専用のstartupオブジェクトで設定していますので、後でご説明します。

 

■グローバル変数の定義

extern int cnt;     /*グローバル変数 startup.marでもexport宣言*/
extern int mode;    /*グローバル変数 startup.marでもexport宣言*/

グローバル変数というのは、このプログラム全体で共通に参照、書き換えができる変数のことです。では他の変数は?というと、例えばmain( )関数の中で宣言した変数は、main( )関数の中でしか使用できないのです。ここではcntmodeというグローバル変数を宣言しています。これらも、専用のstartupオブジェクトで宣言する必要があります。

 

■PIOポートの初期化

    P1.DDR = 0xff;       /* port1出力に設定 表示LED */
    P2.DDR = 0x00;       /* port2入力に設定 DIPSW   */
    P2.PCR.BYTE = 0xff;  /* port2プルアップon        */
    P5.DDR = 0xff;       /* port5出力に設定 表示LED  */
    P5.PCR.BYTE = 0x00;  /* port5プルアップoff       */

今回使用するPIOポートを初期化しています。

 

■ITU1タイマーの初期化

     ITU1.TCR.BYTE = 0x23; /* GRAコンペアマッチ clock 1/8 */

システムクロック16MHzを1/8に分周したクロック2MHzを、タイマー用のクロックにして、GRAのコンペアマッチでTCNTタイマカウンタをクリアするように設定しています。

     ITU1.GRA = 0x4e20;      /* GRAを4e20に設定 約10ms*/

ITU1のGRAを0x4e20に設定します。0x4e20は10進数で20,000になるので、2MHzのクロックでタイマーを動作させた場合、10mSになります。

 

     ITU1.TIER.BYTE = 0xF9; /* ITU1のGRAによるコンペアマッチ割込みを許可*/

TIER(タイマーインタラプトイネーブルレジスタ)は、以下の意味があります。

7 6 5 4 3 2 1 0
- - - - - OVIE IMIEB IMIEA

IMIEAは、IMFAフラグ(GRAによるコンペアマッチ)による割込みを許可/禁止します。

IMIEBは、IMFBフラグ(GRBによるコンペアマッチ)による割込みを許可/禁止します。

OVIEは、OVFフラグ(オーバーフロー)による割込みを許可/禁止します。

いずれも許可は1、禁止は0です。

使用していない7〜3bitは1として扱われます。

今回はGRAによる割込みだけを許可するので、以下の設定になります。

7 6 5 4 3 2 1 0
- - - - - OVIE IMIEB IMIEA
1 1 1 1 1 0 0 1

1111 1001は16進数でF9hなので、ITU1.TIER.BYTE = 0xF9; となります。

 

     ITU.TSTR.BIT.STR1 = 0; /* カウント停止状態          */
     ITU.TSTR.BIT.STR1 = 1; /* ITU1 TCNTカウント開始     */

TCNTタイマーカウンタのカウント動作を開始させます。

 

■グローバル変数の初期化

    cnt = 0; /* 割込み発生の回数を0にセット       */
    mode =1; /* 2bitLEDの表示のさせ方を1にセット */

変数cntは、10mSごとの割込み発生の数を数えるための変数です。変数modeは、2bitのLEDをどう点灯させるかを握る変数です。

 

■main( )関数の主要部分

    while(1){
          P1.DR.BYTE = P2.DR.BYTE; /* DIPSWを8bitLEDに表示 */
     }

main( )関数中、今までの部分は初期化の部分でした。main( )関数で行なう処理の主要部分は、このwhile文だけです。つまり、ディップスイッチの内容を8bitのLEDに表示させようということだけです。

 

■割込み処理intimia1( )関数

ITU1のGRAコンペアマッチで割込みが発生すると、intimia1( )関数を実行します。

    cnt++; /* cntの数を1つ増やす */

変数cntの値を1増やします。

     if(cnt == 100){                /*100(1秒)なら2bitLEDの表示を変化させる*/
          cnt = 0;                  /*カウンタを0に戻す   */
          switch(mode){
               case 1:mode = 0x02;  /*表示が01なら10にする*/
                      break;
               case 2:mode = 0x01;  /*表示が10なら01にする*/
                      break;
               default:mode = 0x01; /*それ以外なら01にする*/
          }
          P5.DR.BYTE = mode;        /*2bitLEDを変化させる */
     }
     

cntの値が100になると、100回目の割込み発生、つまり1000mS経過したことになります。cntの値が100の時だけ、上の処理を実行しLEDの表示を変化させます。

 

ITU1.TSR.BIT.IMFA = 0;  /* 割込み検知フラグを戻して再開 */

割込みが発生するとIMFAビットが1になるので、割り込み処理が終わったら、最後にIMFAビットを0にクリアしておきます。

【先頭に戻る】


105−4.割込みとグローバル変数に対応したSTARTUP.MAR

tmtest22.cのプログラムは、これだけをコンパイルしても動作しません。割込み処理とグローバル変数を使用している場合は、それに対応したstartupオブジェクトが必要です。

以下に、今回のtmtest22.cを動作させるために必要なstartup.marをご紹介します。ここをクリックするとダウンロードできます。(startup2.mar)

ファイル名はstartup2.marにしてあるので、ダウンロード後はstartup.marに名前を変更してから、ソフトウエア編の3.コンパイル手順 3−1.スタートアップオブジェクトを作るを参照して、startup.objファイルを作成し、tmtest22とリンクして下さい。

;STARTUP ROUTINE H8/300H
;
.cpu 300ha:20                  ;H8/300H advanced 1Mbyte MODE
.import _main                  ;外部関数mainを定義
.import _intimia1              ;外部関数intimia1を定義
.export _cnt                   ;グローバル変数cntを定義
.export _mode                  ;グローバル変数modeを定義
;
.section vect,data,locate=h'00000 ;割込みベクトルの頭
;
;割り込みベクトルテーブル
.data.l init                   ;1 reset vectのジャンプ先
;
.org h'0070                    ;ITU1 GRAによる割込みテーブル位置
.data.l _intimia1              ;ITU1 GRAによる割込み処理先
;
.org h'00100                   ;主プログラムの開始
init: mov.l #h'fff10,er7       ;SPの設定
ldc #0,ccr                     ;CLEAR INTERRUPT MASK,NOT USE UI BIT
jmp @_main                     ;関数mainへのジャンプ
.section D,DATA,locate=h'ffd00 ;変数用アドレス
_cnt: .res.w 1                 ;cnt変数のアドレスを確保
_mode: .res.w 1                ;mode変数のアドレスを確保
;
.end

このstartup2.marでは、ソフトウエア編の3.コンパイル手順 3−1.スタートアップオブジェクトを作るでご紹介しているstartup.marに比べて、以下の部分を追加しています。

 

.import _intimia1             ;外部関数intimia1を定義
.export _cnt                  ;グローバル変数cntを定義
.export _mode                 ;グローバル変数modeを定義

intimia1という外部関数を定義し、cntとmodeというグローバル変数を定義しています。

 

.org h'0070                   ;ITU1 GRAによる割込みテーブル位置
.data.l _intimia1             ;ITU1 GRAによる割込み処理先

ITU1のGRAによって割込みが発生した場合、0070h番地に記載されているアドレスにジャンプして、所定の処理を行なうことが決まっています。(割込みテーブル) ここでは、0070h番地に、intimia1というラベルで記載して、intimia1関数を実行するように指定しています。

 

.section D,DATA,locate=h'ffd00 ;変数用アドレス
_cnt: .res.w 1                 ;cnt変数のアドレスを確保
_mode: .res.w 1                ;mode変数のアドレスを確保

グローバル変数のための記憶領域を、FFD00h番地から確保しています。

 

このstartupで定義している外部関数名とグローバル変数名は、C言語で使用する関数名とグローバル変数名に合わせておかなければなりません。

【先頭に戻る】


105−5.tmtest22.cはどんな動作をするか

だらだらと前置きが長くなってしまいましたが、タイマー割込みを利用すると、以下のような動きとなります。

まず、tmtest21.cと同様に、2bitのLEDが1秒間隔で点滅を開始します。8bitのLEDは、ディップスイッチのon/offの状態を表示してるはずです。

ここで、ディップスイッチのon/offを適当に切り替えてみましょう。8bitのLEDの点灯状態が、2bitのLEDのタイミングに関係なく、ディップスイッチを切り替えたタイミングで変化することがお分かり頂けますか?

今回、1秒間の計測という、マイコンにとっては比較的長い期間のタイマーが必要だったため、100回目の割込みでようやく処理を実行するという、ちょっとスマートではない方法になってしまいました。でも、このように割込みを利用すると、時間待ちのためにCPUの処理を止めてしまうといった、非効率的なことを回避できます。これは、リアルタイム処理をする上で、欠かせない技術でしょう。

 

【ソフトウエア編TOPに戻る】

 

 

 


【表紙に戻る】