ドットマトリクスディスプレイ(割込みなし)では,ドットマトリクスディスプレイが表示されますが,他の動作を同時に行うことができません.そこで,一定時間ごとにドットマトリクスディスプレイの表示を行うプログラムに改造します.そのために使用するのがタイマによる割込みです.ここでは,PIC16F1936でタイマを使用する方法,割込みの方法そしてそれを用いたドットマトリクスディスプレイの表示方法について説明します.

  1. 目標

    ここで行いたい処理の仕様を次のようにします.
    125[us]に一度,割り込みを発生させてドットマトリクスディスプレイの1列を更新するようにします.
    8列で構成されているドットマトリクスディスプレイでは,1000[us]=1[ms]で1枚の画像が表示できるようになります.

  2. 事前準備

    割込みなしのドットマトリクスディスプレイを改造することで,今回のプログラムを作成していきます.もし,割込みなしのプログラムをとっておきたい場合には,コピーしておいてください.

  3. タイマ

    PIC16F1936には8ビットタイマが4個,16ビットタイマが1個備わっています.「xビットタイマ」とは,最大で2xまで計数できることを表しており,8ビットタイマの場合には255まで計数できるということです.今回,8ビットタイマのうちの1つであるTimer0を使うことにします.
    PIC16F1936のデータシートをご覧ください.この中の図15-1にTimer0のブロック図があります.下にそれを示します.
    Timer0
    (出典: PIC16F193X/LF193X データシート)

    最初に示した仕様を満たすには,動作周波数(Fosc)を4分周したものをタイマのクロックとして使用し,Prescalarによる分周を行わなければ125[us]を計時することができます.もう少し解説すると,Fosc=4[MHz]であり,Fosc/4=1[MHz]ですので,1クロックでは1[us]となります.これを125回数えればよいわけです.このようにするには,上記の図にあるTMR0CS=0,PSA=1にすればTMR0に対して1[us]のクロックを送ることができます.Timer0には,オーバフローすると割込ませることができますので,TMR0レジスタを131から数えさせれば125回カウント時にオーバフローしてくれます.


    1. IntervalTimerForDMD_initialize関数

      以上のように初期設定をする「IntervalTimerForDMD_initialize関数」を作成しましょう.今回,Timer0を一定間隔ごとに割込みを発生するものとして扱います.このため,ファイル名をIntervalTimerForDMD.cとし,その初期化関数としてIntervalTimerForDMD_initialize関数としました.この関数で行う処理をアクティビティ図で表します.まずTMR0CSレジスタに動作周波数(Fosc)を4分周したクロックをTimer0で使用するよう設定します.次にプリスケーラで分周しないよう,PSAに設定します.次に,125カウントしたらオーバフローさせるようにするため,TMR0に131(=255-125)をセットします.最後に,一定時間ごとに呼び出す関数を設定します.今回,その関数を「_callbackedFunction」とします.これは後ほど説明する「Interrupt_setTimer0Callback関数」によりセットされる関数で,Timer0がオーバフローし割込みがかかったときに呼び出される関数です.
      IntervalTimerForDMD initialize
      以上をC言語で書きますと次のようになります.131を定数「_125US」として定義しています.

      #include <stdio.h>
      #include <xc.h>
      #include "IntervalTimerForDMD.h"
      #include "Interrupt.h"
      
      /* 125[us]でオーバフローさせるための定数 */
      #define _125US (131)
      
      static void (*_callbackFunction)(void) = NULL;
      
      static void _callbackedFunction(void);
      
      void IntervalTimerForDMD_initialize(void){
          /* Fosc/4をクロックとして使用する */
          TMR0CS = 0;
          /* Prescalarで分周しない */
          PSA = 1;
          /* 125回カウントしたらオーバフローさせるようにする */
          TMR0 = _125US;
          /* 割込み時に呼び出すコールバック関数を設定する */
          Interrupt_setTimer0Callback(_callbackedFunction);
      }
      

       

    2. _callbackedFunction関数

      次に_callbackedFunction関数について説明します.この関数はIntervalTimerForDMD.c内でしか使用できないスタティック関数で,Timer0による割込みが発生した時に呼び出される関数です.下図にこの関数のアクティビティ図を示します.最初に関数ポインタ_callbackFunctionを呼び出します.関数ポインタとは関数の先頭アドレスを記憶するためのもので,これがあれば設定されているアドレスの関数を呼び出すことができます.今回,_callbackFunctionにはドットマトリクスディスプレイの1列を描画する関数を設定しておくことで,結果として125[ns]ごとに1列描画することができます.次に125回カウントしたらオーバフローさせるようにするため,TMR0に_125USをセットします.最後に割込みフラグTMR0IFを0にし,次の割り込みに備えます.
       callbackedFunction
      以上の処理をC言語で書くと次のようになります.なお,プログラム中にあるNULLを用いるには,#include<stdio.h>が必須ですので忘れずにこれも追加してください.
      /* #includeの次あたりに書いておく */
      static void (*_callbackFunction)(void) = NULL;
      
      static void _callbackedFunction(void){
      
          /* 一定時間ごとに呼び出したい関数を実行する */
          (*_callbackFunction)();
          /* 125回カウントしたらオーバフローさせるようにする */
          TMR0 = _125US;
          /* 割込みフラグをクリアする */
          TMR0IF = 0;
      }

       

    3. IntervalTimerForDMD_setCallback関数

      次に,_callbackFunction関数ポインタに設定するための関数「IntervalTimerForDMD_setCallback関数」をIntervalTimerForDMD.c内に作りましょう._callbackFunctionはスタティックであり,IntervalTimerForDMD.cの外からはアクセスできません.そのため,DotMatrixDisplay.cからでもアクセスできるようにするため,setterを付けるということです.下の図のように関数を作りましょう.
      IntervalTimerForDMD setCallback
      C言語で書くと次のようになります.
      void IntervalTimerForDMD_setCallback(void (*callback)(void)) {
          _callbackFunction = callback;
      }

       

    4. IntervalTimerForDMD_start関数,IntervalTimerForDMD_stop関数

      あと2つ,IntervalTimerForDMD.cに関数を作ります.それは,タイマを開始する「IntervalTimerForDMD_start関数」と,停止する「IntervalTimerForDMD_stop関数」です.Timer0の開始・停止をつかさどるのはTIMR0IEレジスタですので,それぞれの関数は次のようになります.なお,stop関数には現状で転倒している1列分のパターンを消すために_clearColumn()関数を呼び出しています.

      void IntervalTimerForDMD_start(void){
          TMR0IE = 1;
      }
      
      void IntervalTimerForDMD_stop(void){
          TMR0IE = 0;
      }

       

    5. IntervalTimerForDMD.h

      以上でIntervalTimerForDMD.cのプログラムは終わりです.グローバル関数については,IntervalTimerForDMD.hに定義しておいてください.
      #ifndef INTERVALTIMER_FOR_DMD_H
      #define	INTERVALTIMER_FOR_DMD_H
      
      extern void IntervalTimerForDMD_initialize(void);
      
      extern void IntervalTimerForDMD_setCallback(void (*callback)(void));
      
      extern void IntervalTimerForDMD_start(void);
      
      extern void IntervalTimerForDMD_stop(void);
      
      #endif	/* INTERVALTIMER_FOR_DMD_H */
      

       


  4. 割込み

    次に割込みを行うためのソースを書きましょう.PIC16F1936では,割込みの優先順位の概念はなく,割込みが発生したら必ず決まったアドレスを呼び出すようになっています.XC8コンパイラでは,そのアドレスに処理(つまり割込み関数)を配置するとき,キーワード「interrupt」を関数の頭につける決まりとなっています.


    1. Interrupt_interrupt関数

      Timer0の割り込みが発生していて,加えてTimer0が割込みされた時に呼び出してもらいたいコールバック関数_timer0Callbackが設定されていたら,_timer0Callback関数を呼び出します.
      Interrupt interrupt
      以上をC言語で書くと次のようになります.Interrupt_interrupt関数の先頭に「__interrupt()」が付いていることを確認してください.
      #include <stdio.h>
      #include <xc.h>
      #include "Interrupt.h"
      
      static void (*_timer0Callback)() = NULL;
      
      void __interrupt() Interrupt_interrupt(void){
      
          /* タイマ0の割り込みが発生したら */
          if (TMR0IF == 1 && _timer0Callback != NULL) {
              /* コールバック関数を呼び出す */
              (*_timer0Callback)();
          }
      
      }

       

    2. Interrupt_setTimer0Callback関数

      この関数では,Timer0が割り込まれた時に呼び出したい関数を設定するためのものです.スタティックな関数ポインタ_timer0Callbackに仮引数callbackを設定します.
      Interrupt setTimer0Callback


      C言語で書くと次のようになります.
      static void (*_timer0Callback)(void) = NULL;
      
      void Interrupt_setTimer0Callback(void (*callback)(void)) {
          _timer0Callback = callback;
      }
    3. Interrupt.h

      これまで書いた2つのグローバルな関数をInterrupt.hに宣言します.
      #ifndef INTERRUPT_H
      #define	INTERRUPT_H
      
      extern void __interrupt() Interrupt_interrupt(void);
      extern void Interrupt_setTimer0Callback(void (*callback)(void));
      
      #endif	/* INTERRUPT_H */
      

       

  5. ドットマトリクスディスプレイの表示

    ドットマトリクスディスプレイの基本的なプログラムは既に書いてある前提で,それを改造することで割込みに対応したものにしていきましょう.
    1. スタティック変数

      ドットマトリクスディスプレイでは,表示するパターンをPattern構造体のポインタ_displayPatternを用い,_PATTERN配列に定義されている各パターンの先頭アドレスを格納します.従いまして,DotMatrixDisplay.cの最初の方に次の文を追加しておいてください.
      static const Pattern *_displayPattern = NULL;
    2. DotMatrixDisplay_initialize関数

      この関数はこれまで書いたものに2行付け加えるだけです.これまでのプログラムも含めて,アクティビティ図で関数内の処理を示します.最初にピンの設定をした後,IntervalTimerForDMDを初期化します.そのためにはIntervalTimerForDMD_initialize関数を呼び出してください.最後に一定時間ごとに呼び出されるコールバック関数を設定します.今回は「_refresh関数」です.この関数は後ほど説明しますが,1列描画する関数だと思ってください.
      DotMatrixDisplay initialize
      C言語では次のようになります.一部,省略したinclude文やプロトタイプ宣言があることに注意してください.
      #include "IntervalTimerForDMD.h"
      
      static void _refresh(void);
      
      void DotMatrixDisplay_initialize(void) {
          /* ポートAビット6を出力端子とする */
          TRISA6 = 0;
          /* ポートCビット0を出力端子とする */
          TRISC0 = 0;
          /* ポートCビット1を出力端子とする */
          TRISC1 = 0;
          /* ポートCビット2を出力端子とする */
          TRISC2 = 0;
          /* ポートCビット5を出力端子とする */
          TRISC5 = 0;
          /* ポートAビット7を出力端子とする */
          TRISA7 = 0;
          /* ポートBビット1をデジタルピンとして使用する */
          ANSB1 = 0;
          /* ポートBビット1を出力端子とする */
          TRISB1 = 0;
          /* ポートBビット2をデジタルピンとして使用する */
          ANSB2 = 0;
          /* ポートBビット2を出力端子とする */
          TRISB2 = 0;
      
          /* ポートAビット4を出力端子とする */
          TRISA4 = 0;
          /* ポートAビット5を出力端子とする */
          TRISA5 = 0;
      
          /* タイマの初期化 */
          IntervalTimerForDMD_initialize(); /*!追加*/
          /* 一定時間ごとに呼び出したい関数 */
          IntervalTimerForDMD_setCallback(_refresh); /*!追加  */
      }
      
    3. _refresh関数

      この関数は,呼ばれるたびに1列分の描画を行います.1度呼ばれると次に呼ばれた時には一つ右側の列を描画し,一番右まで描画したら,次は一番左側の列を描画します.下にアクティビティ図で処理の流れを示します.要点としては,column変数です.この変数は関数内しかスコープの無いスタティック変数となっております.このため,_refresh関数が呼ばれるたびに初期化されません.つまり,以前の列を記憶しておいてくれるわけです.
       refresh
      プログラムで書くと次のようになります.
      static void _refresh(void){
          static uint8_t column = 0;
      
          /* 表示を消す */
          /* このようにしないと,前の列の残像が残ってしまう */
          _clearColumn();
      
          /* 列の表示/非表示を外部のシフトレジスタで行っているので */
          /* そのシフトレジスタを操作する */
          _controlShiftRegister(column);
          _setColumnData(_displayPattern->_column[column]);
      
          /* 列をずらす */
          column++;
          if(column == 8){
              column=0;
          }
      }
    4. DotMatrixDisplay_setPattern関数とDotMatrixDisplay_setNumberPattern関数

      これらの関数は以前もありましたが,大幅に内容を変えます.というのも,以前のプログラムではこれらの関数が呼ばれるたびに1画面分の描画を行っていましたが,今回作成するプログラムでは表示するパターンを_displayPattern変数に記憶しておくだけの機能しかなくします._displayPattern変数はスタティックな変数で,DotMatrixDisplay.c内からしか参照できません.下にアクティビティ図を示します.要点としては,setNumberPattern関数で使用する_NUMBER_PATTERN_OFFSET定数です.数字を表すパターンは0x1Cから始まっていますので,0x1Cを定数としたものがこれです.与えられる仮引数valueにこの値を足すことで,表示したいパターンの要素番号が得られます.
      DotMatrixDisplay setPattern
      DotMatrixDisplay setNumberPatternプログラムは次のようになります.1行目の#define文は,プログラムの最初の方に書いておいてください.
      #define _NUMBER_PATTERN_OFFSET    (0x1C) // 数字を表すパターンのオフセット
      
      void DotMatrixDisplay_setPattern(ShapeType type) {
          /* 表示したいパターンを記憶する */
          _displayPattern = &_PATTERN[type];
      }
      
      void DotMatrixDisplay_setNumberPattern(uint8_t value) {
          /* 表示したいパターンを記憶する */
          _displayPattern = &_PATTERN[value + _NUMBER_PATTERN_OFFSET];
      }
    5. DotMatrixDisplay_show関数とDotMatrixDisplay_hide関数

      DotMatrixDisplay_show関数では,表示を開始するためにインターバルタイマを開始するようにし,DotMatrixDisplay_hide関数では,表示を停止するためにインターバルタイマを停止します.従って,show関数では,IntervalTimerForDMD_start関数を,hide関数ではIntervalTimerForDMD_stop関数をそれぞれ呼び出せばよいです.下のように追記してください.
      void DotMatrixDisplay_show(void) {
          /* インターバルタイマを開始する */
          IntervalTimerForDMD_start();
      }
      
      void DotMatrixDisplay_hide(void) {
          /* インターバルタイマを停止する */
          IntervalTimerForDMD_stop();
      }
      

      課題
      上記2つの関数をDotMatrixDisplay.hに宣言しておいてください.また,IntervalTimerForDMD.hをインクルードしておいてください.


  6. メイン関数

    メイン関数で行うことをアクティビティ図で示します.ドットマトリクスディスプレイの初期化,パターンの設定,表示開始そしてwhile文による待機をしています.待機をしないとPICはスリープ状態に陥り,割込みが発生しても処理をしてくれなくなってしまいますので気を付けましょう.
    Main

    プログラムを下に示します.メインで行うことは非常にシンプルになっているかと思います.
    void main() {
        /* 十字 */
        ShapeType type = UPPER_LEFT_ARROW;
    
        /* 内部オシレータの速度を4[MHz]とする */
        IRCF0 = 1;
        IRCF1 = 0;
        IRCF2 = 1;
        IRCF3 = 1;
    
        /* LCD制御モジュールを停止する */
        LCDEN = 0;
    
        /* すべての割込みを許可する */
        GIE = 1; /*!追加 */
    
        /* ドットマトリクスディスプレイを初期化する */
        DotMatrixDisplay_initialize();
    
        /* パターンを設定する */
        DotMatrixDisplay_setPattern(type); /* 追加 */
        
        /* パターンを表示する */
        DotMatrixDisplay_show(); /* 追加 */
    
        /* 特に何もしない */
        while(1){ /* 追加 */
        }
    }

     

  7. コールバック関数の関係

    今回,コールバック関数が複数できており,処理の流れがややわかりにくくなっています.そこで,シーケンス図を使って処理の大まかな流れを確認しましょう.このシーケンス図では,各ソースファイルにある関数のうち,接頭辞を省略し,代わりにクラス名で表現しています.
    まず,DotMatrixDisplayからIntervalTimerForDMD_initialize関数を呼び出します.この中からさらに,Interrupt_setTimer0Callback関数を呼び出すことで,一定時間ごとに呼び出される関数がIntervalTimerForDMD.c内の_callbackedFunctionと設定しています.その後,DotMatrixDisplayからIntervalTimerForDMD_setCallback関数により,_refresh関数を設定しています.そして,割り込みが発生するとInterrupt_interrupt関数が呼び出され,その中から_callbackedFunctionが呼び出されます._callbackedFunctionでは,_refresh関数を呼び出してくれますので,結果として,ドットマトリクスディスプレイの_refresh関数が呼び出されるようになるわけです.
    コールバック関数の関係
    このように,いくつものコールバック関数をバケツリレーのようにしている理由は,DotMatrixDisplay,IntervalTimerForDMDそしてInterruptをそれぞれ分割し,独立性を高めるためです.同じようなことをC++ではリスナというインタフェースを作成していますね.考え方は基本的に同じです.ただ実装方法が異なるだけなのです.