home1.gif

next1.gif
back1.gif
 

Timer

 

基礎編
 PORT入出力
 シリアル通信
 タイマー機能
 A/Dコンバータ
 D/Aコンバータ
 外部割り込み
 液晶モニタ

応用編
 アウトプットコンペア
 インプットキャプチャ
 SRAM増設

Tips
 関数集
 シリアルEEPROM
 20桁×4行液晶モニタ
  (SC2004C)
 加速度センサを読む

タイマー機能

 イントロダクション
 タイマー機能とは何か? その名の通り時間を計る機能です。たとえばある処理の後に一定時間待たなければいけないであるとか、ある入力信号の長さを測るのに使うであるとか、そういう使い方をします。
 ここで、簡単なC言語の知識のある人なら、「それだったらforループで変数の値をカウントしたらいいんじゃないの?」と思うかも知れません。では聞き返しますが、そのforループで作った1カウントは何nsecですか? これの答えは容易には出ません。なぜなら、使うコンパイラによってforループのアルゴリズムが違うため、いったい何クロック分の時間が消費されているかはっきりしないからです。もちろん、オシロスコープなどを用いて調べれば分かることではありますが、本当に厳密なタイミング計算が欲しいときなどに、オシロで見た値はどこまで信用できるでしょうか? その誤差が数分後に積分されて積もってしまった場合には?

 H8に限らず、マイコンのタイマー機能という物はクロックに同期しています。したがって、タイマーの値が1増えるということは、すなわち1クロック進んだと言うことに他ならず、1%の誤差も決して生じません。

 ただし、往々にしてマイコンのクロックという物は高速です。今回対象としているAKI-H8は16MHzで駆動していますから、1クロックはわずか0.0000625msecです。カウンタは16ビットですから、65535までカウントできますが、それでも4.096msecまでしか計れません。これではいくらなんでも短すぎます。そこで、「プリスケーラ」という機能を使います。これはタイマーの1カウンタをクロック数回に1つしかカウントアップさせないようにするもので、最大8分1まで遅くすることができます。これならば最長32.7msecまで測定できるので、信号の解析などには十分な時間です。次の表はH8/3048Fで使える分周比(=プリスケーラ)と測定可能な最長時間の対応表です。

分周比

0から65535(0xffff)までの時間

1:1

4.096[msec]

1:2

8.192[msec]

1:4

16.384[msec]

1:8

32.768[msec]

 まずは使ってみる
 簡単な例から行ってみましょう。
 LEDを1Hzで点滅させるプログラムを例に取ってみます。このファイルをダウンロードしてください。

  void main(void)
  {
      int cnt;
      PB.DDR = 0xff;
      PB.DR.BYTE = 0x00;

      initITU();                         // タイマーを初期化

      while(1){

          PB.DR.BYTE = ~PB.DR.BYTE;      // LEDの状態を反転

          for(cnt=0; cnt<50; cnt++){
              ITU0.GRA = 20000;          // タイマでカウントする値の設定
              ITU0.TCNT = 0;             // Reset ITU0.TCNT
              ITU.TSTR.BIT.STR0 = 1;     // タイマ スタート
              while(!ITU0.TSR.BIT.IMFA); // 時間待ち
              ITU0.TSR.BIT.IMFA = 0;     // IMFAをクリア
              ITU.TSTR.BIT.STR0 = 0;     // タイマを停止
          }
      }   
  }

 この中のinitITU()という関数は後で見ていくとして、全体の流れをまず説明します。
 mainルーチンが始まったらまずはタイマーを初期化しておきます。ここではまだタイマー割り込みを使いませんので、難しく考えなくてもOKです。次に一瞬「え?」と思うのが、

     PB.DR.BYTE = ~PB.DR.BYTE;

の一文ではないでしょうか。私が初めてこの書式に出会ったときには目から鱗でしたが、何のことはありません。「次のポートBの状態は、今のポートBの状態のNOTですよ」と言っているのですから、ようはHighだったポートはLowに、LowだったポートはHighに反転するのです。

 さて、今度のfor文の中身はなかなか難解ですね。とりあえずはTCNTとGRAという物がどういうものであるか説明します。

 TCNTとはカウンタの事です。これは現在値(初期値が0とは限りません。使うときは0にリセットします)から順番に増えて行き、0xffff(65535)になると、設定により止まるか、0に戻るかのどちらかの動作をします。
 GRAは、このレジスタに設定した値とTCNTの値が等しくなるとIMFAというフラグが0から1に変わります。他に同等の機能を持つGRBと、その対応するIMFBというレジスタもあります。TCNTが上がるたびに常に比較されますので、まさか気づかず通り過ぎてしまうなどということはありませんから安心してください。

 そして、タイマーはSTRというレジスタに「1」を書き込むとスタートし、「0」を書き込むと止まります。

 以上からこのプログラムの流れをまとめると、
 GRAに20000という値をセットします。これは時間で言うとかっきり10msecです。
 次にカウンタをリセットしてからタイマーをスタートさせます。
 そして、設定した20000という値と、カウンタの値が等しくなるまで待ちます。等しくなるとIMFAの値は「1」になりますから、それのNOT(!)で括弧の中は0となり、whileループから抜けれます。
 IMFAフラグをクリアして、タイマーも停止します。これを50回繰り返すと約0.5秒です。約0.5秒間隔でLEDを付けたり消したりするわけですから、結局1Hzで明滅していることになります。

 ところで、なぜ「約」なんて言葉を使ってお茶を濁したのか。勘のいい人は分かったと思いますが、タイマーはたしかにカッキリ10msecでフラグを立ててくれます。しかし、その後でタイマーを止めたりフラグを解除したりするにも当然時間が掛かります。その分が計算に入っていないのですね。実際にはかなり微々たる時間差ですが、本当に厳密な作業を要求されるときにはこんなズボラをかましちゃいけません。

 また、H8/3048Fにはタイマーが5系統あります。それぞれITU0,ITU1,ITU2,ITU3,ITU4と名前が付いています。そして、それぞれにGRA,GRBがありますので、全部で10個のタイマーがあると考えることができます。当然それら全部に割り込み機能もあるわけで、フルに使えばよほど複雑なタイミング計算もハードウェアが正確にやってくれます。

 では次に、initITU()の中身ですが、次のような記述になっています。

  void initITU(void)
  {
      ITU.TSNC.BIT.SYNC0 = 0;         // 他チャンネルとの同期無し(独立動作)
      ITU.TSNC.BIT.SYNC1 = 0;         // 他チャンネルとの同期無し(独立動作)
      ITU.TSNC.BIT.SYNC2 = 0;         // 他チャンネルとの同期無し(独立動作)
      ITU.TSNC.BIT.SYNC3 = 0;         // 他チャンネルとの同期無し(独立動作)
      ITU.TSNC.BIT.SYNC4 = 0;         // 他チャンネルとの同期無し(独立動作)

      ITU0.TCR.BYTE = 0x03;           // クロックの1/8で動作。TCNTのクリア禁止
      ITU1.TCR.BYTE = 0x03;           // クロックの1/8で動作。TCNTのクリア禁止
      ITU2.TCR.BYTE = 0x03;           // クロックの1/8で動作。TCNTのクリア禁止
      ITU3.TCR.BYTE = 0x03;           // クロックの1/8で動作。TCNTのクリア禁止
      ITU4.TCR.BYTE = 0x03;           // クロックの1/8で動作。TCNTのクリア禁止

      ITU0.TIOR.BYTE = 0;             // GRの制御
      ITU1.TIOR.BYTE = 0;             // GRの制御
      ITU2.TIOR.BYTE = 0;             // GRの制御
      ITU3.TIOR.BYTE = 0;             // GRの制御
      ITU4.TIOR.BYTE = 0;             // GRの制御

      ITU0.TIER.BIT.IMIEA = 0;        // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU1.TIER.BIT.IMIEA = 0;        // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU2.TIER.BIT.IMIEA = 0;        // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU3.TIER.BIT.IMIEA = 0;        // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU4.TIER.BIT.IMIEA = 0;        // 割り込み禁止(オーバーフロー/IMFA/IMFB)
  }

 上の5つはたぶん変更することは無いでしょう。他チャンネルと同期させながら使うという用途はそうそうある物ではありません。
 2項目目は分周比の設定であったり、TCNTのクリア条件の設定ですから、ちょくちょく変更すると思います。詳しくはハードウェアマニュアルの10章25ページを読んでください。本講座でもアウトプットコンペアの解説をする時にはまた改めて記述します。
 3項目目のGRの制御とは、ハードウェア的にPWM信号を生成したい時に変更することになります。これもアウトプットコンペアの解説の時に詳しく説明します。
 最後の項目はタイマー割り込みを使うかどうかの選択です。「1」を書けば割り込み機能が有効になり、「0」にすると無効になります。

  タイマー割り込みを使う
 タイマー割り込みを使うとどういう恩恵があるのでしょう。単に待ち時間を正確に計りたいならば上で述べた方法で問題ありません。プログラムは馬鹿正直なくらいシンプルな方がバグが少なく、流用も利きます。また、待ち時間が少ないならばわざわざ貴重なリソースを使うまでもありません。

 タイマー割り込みのメリットはなんと言っても時間待ちの間も他の作業が並行して進められる事です。TCNTとGRA,GRBの比較はハードウェアレベルで勝手にやってくれていますし、設定時間になってIMFA,IMFBのフラグが立てば、そのときになって思い出したように時間が来たらやるはずだった仕事を始めます。図で書くと次のようなイメージです。

 では実際にタイマー割り込みを使ったプログラムを紹介します。このファイルをダウンロードしてください。

 割り込み機能を使うと言うことはメインルーチンのファイル以外に3つ編集するファイルが存在するということは本能に刷り込ませてください。シリアル通信でも説明した

  • hwsetup.c
  • intprg.c
  • resetprg.c

です。resetprg.cはもう一度言うと

         set_imask_ccr(1); 

 56行目でこのようになっている部分を

         set_imask_ccr(0); 

 このようにします。忘れがちな部分ですから特に注意してください。

 hwsetup.cはシリアルの時と変更場所が違います。といっても、どの割り込み処理を有効にするのかでコメント文を外すわけですから、当たり前と言えば当たり前ですね。必要箇所だけ残して他をばっさり削除すると次のような感じです。

hwsetup.c

  #include "iodefine.h"
  #ifdef __cplusplus
  extern "C" {
  #endif

  extern void HardwareSetup();
  #ifdef __cplusplus
  }
  #endif

  void HardwareSetup()
  {
    SYSCR.BYTE = 0;
    SYSCR.BIT.SSBY  = 1;
    SYSCR.BIT.STS   = 7;
    SYSCR.BIT.UE    = 1;
    SYSCR.BIT.NMIEG = 1;
    SYSCR.BIT.RAME  = 1;

    INTC.IPRA.BYTE = 0;
    INTC.IPRA.BIT.B2 = 1;  // Set ITU0      Priority Level
 }

 intprg.cは割り込み処理の本体を記述するファイルでしたね。今回は上で述べたのと同じ機能を割り込みでやらせてみました。次のようになります。

intprg.c

    #include    <machine.h>

    #include    "vect.h"

    #pragma section IntPRG
    #include "iodefine.h"
    extern int cnt;

    void    INT_IMIA0(void)
    {
      ITU0.TSR.BIT.IMFA = 0;   // IMFAをクリア
      ITU.TSTR.BIT.STR0 = 0;   // タイマを停止
      ITU0.GRA = 20000;        // タイマでカウントする値の設定
      ITU0.TCNT = 0;           // Reset ITU0.TCNT
      ITU.TSTR.BIT.STR0 = 1;   // タイマ スタート

      cnt++;
    }

 まさか複数のファイルをまたがってfor文を使うわけにもいかないので、グローバル変数cntを仲介させて、メインルーチンの方ではcntの値を評価する方式にしています。

main.c

    void main(void)
    {
        int cnt;
        PB.DDR = 0xff;
        PB.DR.BYTE = 0x00;
        
        initITU();                    // タイマーを初期化
        
        ITU0.GRA = 20000;             // タイマでカウントする値の設定
        ITU0.TCNT = 0;                // Reset ITU0.TCNT
        ITU.TSTR.BIT.STR0 = 1;        // タイマ スタート

        while(1){
            if(cnt == 50){
                PB.DR.BYTE = !PB.DR.BYTE; // LEDの状態を反転
                cnt = 0;
            }
        }
    }

 また、initITU()にも1カ所だけ変更があります。タイマー割り込みを使うわけですから、ITU0のIMIEAは1にしておかなければいけません。ですから、

  void initITU(void)
  {
      ITU.TSNC.BIT.SYNC0 = 0;     // 他チャンネルとの同期無し(独立動作)
    (中略)
      ITU0.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU1.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU2.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU3.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU4.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
  }

 これを、

  void initITU(void)
  {
      ITU.TSNC.BIT.SYNC0 = 0;     // 他チャンネルとの同期無し(独立動作)
    (中略)
      ITU0.TIER.BIT.IMIEA = 1;    // 割り込み許可(オーバーフロー/IMFA/IMFB)
      ITU1.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU2.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU3.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
      ITU4.TIER.BIT.IMIEA = 0;    // 割り込み禁止(オーバーフロー/IMFA/IMFB)
  }

 このように変更します。

 

  その他
 
このような使い方以外にも、H8のタイマーには実にさまざまな使い方があります。その一つは後で解説するアウトプットコンペアやインプットキャプチャという方法ですが、それ以外にもまだあります。解説しようにも私自身がすべて把握してるわけではないので必要が出れば自分でやってもらうしかありませんが、一度使い方が分かってしまうと後々笑ってしまうほど便利であることは間違いありません。ぜひ挑戦してみてください。たぶん、複雑な位相関係を持つ波形を出す場合なんかはソフトウェアで頑張るよりも賢明です。


home1.gif
 next1.gif back1.gif