タイマ割り込みでLチカを実現

前回は初期化ルーチンをとりあえず組み込んで、動作するところまで確認しました。これで下準備は整いましたので、いよいよ割り込みを使ったプログラムを作っていきます。と、いってもタイマ割り込みでLEDをチカチカさせるだけですので、それほど難しくはないはずですが・・・。

タイマ割り込みの選択

RZ/A1には周期的に割り込みをかける機能がいくつかあります。マルチファンクションパルスユニット、OSタイマ、リアルタイムクロックなど、この中からどれを選択するかですが、Lチカの用途的(?)にも難易度的にもOSタイマが妥当な気がします。

ところでOSタイマって何でしょう。一般的なRTOS、µITRON、Linuxなどにはシステムタイマ(チックタイマ)と呼ばれるタイマが動いています。このタイマが一定間隔で割り込みを発生させて、OSがさまざまな処理を行うときのタイミングとしています。OSタイマは「OSに使え」とはっきり書かれているドキュメントを見つけられなかったのですが、おそらく、それに使うタイマなんでしょうね。

RZ/A1Lには2チャネル(つまり2つ)のOSタイマがありますので、チャネル0を使って割り込みを発生させましょう。

OSタイマドライバの組込み

前回の記事では割り込みコントローラ以外のドライバを捨ててきましたが、今回、改めてOSタイマのドライバを組込んでいきます。前回ダウンロードしたファイルの以下のソースをプロジェクトエクスプローラーに追加していきます。

kpitgcc\common\src\drivers\ostm\inc\devdrv_ostm.h
kpitgcc\common\src\drivers\ostm\ostm_driver\ostm.c
kpitgcc\common\src\drivers\ostm\userdef\ostm_userdef.c
kpitgcc\common\src\common_settings\rza_io_regrw.c
kpitgcc\common\inc\rza_io_regrw.h

以下のiobitmasksについてはフォルダごと追加します。

kpitgcc\common\inc\iobitmasks

追加するソースファイルは以上です。

ここでコンパイルすると、ostm_userdef.cでmain.hが見つからないというエラーになります。main.hをインクルードしているようですので、main.hの内容を確認しますと、

void Sample_OSTM0_Interrupt(uint32_t int_sense);

とりあえず、この宣言だけがあればいいようですので、以下のソースのようにmain.hをインクルードせずに、この宣言をostm_userdef.cの先頭に入れることにします。

/******************************************************************************
Includes <System Includes> , "Project Includes"
******************************************************************************/
#include "r_typedefs.h"
#include "dev_drv.h"       /* Device Driver common header */
#include "devdrv_ostm.h"   /* OSTM Driver header */
#include "devdrv_intc.h"   /* INTC Driver Header */
#include "iodefine.h"
//#include "main.h"         // 削除
#include "rza_io_regrw.h"

   :(途中省略)

/******************************************************************************
Imported global variables and functions (from other files)
******************************************************************************/
void Sample_OSTM0_Interrupt(uint32_t int_sense);   // 追加

タイマ割り込みハンドラを実装する

この時点でコンパイルすると、先ほど宣言を追加したSample_OSTM0_Interrupt関数が見つからないというリンクエラーになります。実はこれがタイマ割り込みで呼び出される関数(割り込みハンドラ)です。

この関数の中で、LEDを付けたり消したりすれば、周期ごとに点滅する「Lチカ」を実現できそうです。

では、さっそくこの関数を作りましょう。test.cを開いて、適当な場所(ソースの一番最後でOK)に以下のような空の関数を追加します。

void Sample_OSTM0_Interrupt(uint32_t int_sense)
{
}

これだけではまだ割り込みハンドラは呼び出されませんので、OSタイマのドライバを初期化してやる必要があります。それにはまず、ドライバの使用方法を理解する必要がありますので、ソースファイルを見て使用方法を確認します。

OSタイマドライバの使用方法

ソースファイルostm.cを見ると、主な関数が3つあることが分かります。

int32_t R_OSTM_Init(uint32_t channel, uint32_t mode, uint32_t cycle)
int32_t R_OSTM_Open(uint32_t channel)
int32_t R_OSTM_Close(uint32_t channel, uint32_t * count)

上から順番に、初期化、オープン、クローズです。とてもシンプルでいいですね。

R_OSTM_Initの引数は、channel、mode、cycle、となっています。channelはDEVDRV_CH_0またはDEVDRV_CH_1を指定します。今回はチャネル0を使用しますので、DEVDRV_CH_0を指定します。

modeはインターバルか、コンペアの指定ができます。インターバルは指定時間ごとに呼び出され、コンペアは指定時間で1回呼び出されて終わりとなります。インターバルの場合はOSTM_MODE_INTERVAL、コンペアの場合はOSTM_MODE_COMPAREを指定します。今回は点滅を繰り返しますのでOSTM_MODE_INTERVALを指定します。

cycleはミリ秒単位の時間を数値で指定します。

R_OSTM_Openは、指定されたチャネルのタイマを起動させます。タイマを停止させるにはR_OSTM_Closeを呼び出します。R_OSTM_Closeのcountという引数は、カウントしたところまでのタイマ値が返ってくるようです。

今回のプログラムではmain関数の先頭で、初期化とオープンを呼び出します。また、これらのドライバを呼び出す場合は、ヘッダファイルをインクルードする必要があります。ドライバの呼び出しとインクルードを追加すると、以下のようなソースコードになります。

#include "iodefine.h"
#include "dev_drv.h"         // 追加
#include "devdrv_ostm.h"     // 追加

   :(途中省略)

int main(void)
{
    SystemInit();
    R_OSTM_Init(DEVDRV_CH_0, OSTM_MODE_INTERVAL, 500); // 追加
    R_OSTM_Open(DEVDRV_CH_0);                          // 追加

R_OSTM_Init関数のインターバル時間の指定は500(ミリ秒)としておきます。

これでLEDを点滅させるコードを入れれば終わりのはずです。それではビルドして割り込みハンドラ(Sample_OSTM0_Interrupt関数)が呼び出されるか確認しましょう。

割り込みハンドラの動作確認

ビルドしたオブジェクトファイルをシリアルフラッシュに書き込みます。PALMiCE3でシリアルフラッシュメモリに書き込む方法は、第3回の「パワーオン起動に挑戦」に記載されていますので、そちらをご覧ください。

さて、シリアルフラッシュへの書き込みが完了すれば、OSタイマの初期化まで実行します。CSIDEのシステムメニューからハードウェアの初期化(つまりターゲットリセット)を行って、R_OSTM_Init関数を呼び出している34行目まで実行します。コードウィンドウのカーソルを34行目に移動させて、ファンクションキーのF7を押せば、カーソル位置まで実行されます。

OSタイマの初期化まで実行

ここまで暴走することなく実行できましたので、引き続き、割り込みハンドラ「Sample_OSTM0_Interrupt」が呼び出されることを確認します。この関数にブレークポイントを設定して、ちゃんと止まれば成功です。

Sample_OSTM0_Interruptの行にカーソルを移動させて、ファンクションキーのF9を押します。緑の▼マークが表示されて、ブレークポイントが設定されます。51行目と52行目に▼が表示されますが、これはこの2つの行が同じアドレスを示しているためです。

ブレークポイント設定

では、実行してみましょう。ファンクションキーのF5を押します。

・・・ あれっ、止まりません。以下のように実行アドレスの表示がFFFF000Cで止まってしまいました。

実行

やはり、そううまくは行かないようです。ちょっとデバッグしてみましょう。

暴走の原因を突き止める

0xFFFF000C番地はプリフェッチアボート例外が発生したときに飛んでくるアドレスです。まあ、普通に暴走したと考えてよいと思います。

ここでざっくりと2通りの原因が考えられます。一つ目はOSタイマの初期化ルーチンで暴走するケース。もう一つは割り込みがかかった後に暴走するケースです。

まず初期化ルーチンとタイマの起動までで暴走していないか、最初からもう一度確認します。

タイマ起動関数(R_OSTM_Open)の次の行まで実行します。ここで暴走すれば初期化ルーチンに問題があるということになります。

デバッグ1

ちゃんと来ましたね。初期化ルーチンは正常に動いているようです。ではその後、正しく動くかステップ実行で検証してみます。ファンクションキーのF8を押しますと、

暴走した

先ほどと同じように暴走してしまいました。ステップ実行を行った瞬間に暴走するのは、割り込みか、例外がいきなり発生したときに見られるパターンです。(Armでは割り込みも例外ですが。)

何が起こっているのか、ちょっとベクタアドレスの0xFFFF000Cのコードを見てみましょう。ツールバーの強制ブレークボタンを押して停止させ、コードウィンドウのメニューの[PCからの表示]を選択、またはCTRL+Oを押します。すると現在の実行位置の画面が表示されます。

強制ブレーク後

現在の命令が「LDR PC,FFFF002C」となっています。この命令は0xFFFF002C番地の内容をPCにロードし、そのアドレスにジャンプする命令です。0xFFFF002C番地の内容を見ると0xFFFF0084となっていますので、結果として0xFFFF0084番地へジャンプします。続いて0xFFFF0084番地の内容は「B FFFF0084」と自分自身へのジャンプとなっていますので、これでは動くはずがありません。

この領域はCPUの固定のROMですので、自分のプログラムへジャンプするように書き換えることは不可能です。おそらくこの手の問題は、CPUのマニュアルに何か書いてあるはずです。ちょっとRZ/A1Lのユーザーズマニュアルを見てみましょう。

・・・ありました。ブートモードの注意事項に「ハイベクタ設定でブートモードが1、2、3の場合、内蔵ROM内で自番地ループします」とあり、「例外が発生する前にCP15(SCTLR)のVビットを0にしてローベクタに切り替えろ」と書かれてあります。なるほど納得です。CEVボードはブートモード1で起動しますので、ローベクタに切り替える必要があるということですね。

ベクタはどこに?

しかしまだ疑問が残ります。前回、vector_tables.sというベクタテーブルのソースを追加しましたが、あれはどこで使うんでしょう。

答えはSystemInit関数の中のVBARの設定にありました。VBARとはベクタベースアドレスレジスタの略で、ベクタを好きなアドレスに移動させることができるレジスタです。これはVbarInit関数の呼び出しで設定されているようですので、ソースファイルvbar_init.sを見てみましょう。

    LDR r0, =Image$$VECTOR_MIRROR_TABLE$$Base
    MCR p15, 0, r0, c12, c0, 0
    BX lr

実体は3行でした。VBARはコプロセッサのレジスタですので、MCR命令で値を設定します。これでVBARに「Image$$VECTOR_MIRROR_TABLE$$Base」のアドレスが代入されるわけですが、このシンボルは何でしょう。ソースファイルを検索してみると、リンカスクリプトファイルで定義されていました。

リンカスクリプトファイルのtest.ldを開いてみると、.dataセクションに以下のような記述があります。

 .data : {

    :(途中省略)

    Image$$VECTOR_MIRROR_TABLE$$Base = .;
    * (VECTOR_MIRROR_TABLE)    /*asm*/

Image$$VECTOR_MIRROR_TABLE$$Baseに「.」ドットを代入していますが、リンカスクリプトではこうすることで、現在の配置アドレスをシンボルに代入することができます。ここでは「VECTOR_MIRROR_TABLE」というセクションの先頭アドレスが代入されます。

ということで、結論としてVBARにはVECTOR_MIRROR_TABLEのアドレスが設定されるようです。でもVECTOR_MIRROR_TABLEって何でしょう?

これも、ちゃんとサンプルコードのドキュメント
「reference_document\INIT\r01an1864jj0101_rza1h.pdf」に書いてありました。
VECTOR_MIRROR_TABLEは例外処理ベクタテーブルで、内蔵RAMに転送されて使用されるとのことです。

RAMにベクタテーブルを置くことで、例外処理先のアドレスを動的に変更することができ、柔軟な処理ができるようになる、という意味があるのでしょう。ちなみにVECTOR_MIRROR_TABLEのベクタの設定内容はVECTOR_TABLEと全く同じものです。

ローベクタに設定する

話を戻しまして、コプロセッサのVビットを0にして、ローベクタに設定しましょう。

しかし、ここでもまだ疑問が・・・。VBARがVECTOR_MIRROR_TABLEに設定されているのに、なぜハイベクタの0xFFFF000Cにジャンプしてくるんでしょう。

ひょっとしてVBARの仕様でしょうか。Armのアーキティクチャリファレンスを調べてみましょう。

・・・VBARのことが書いてません! どうやら最初にダウンロードしたArmのアーキティクチャにはVBARは無いようです。はじめに「古いドキュメントでも全然問題ないよ」と書きましたが、問題ありました。申し訳ありません。m(_ _)m

改めてARMv7-AのアーキティクチャリファレンスマニュアルをArm社のWebサイトからゲットしてください。以下のようにVBARの説明が書かれています。

VBAR,Vector Base Address Register, Security Extensions
 :(途中省略)
----- Note --------
The high exception vectors always have the base address 0xFFFF0000 and are not affected by the value of VBAR.

これの意味としては、ハイベクタでは常にベクタアドレスは0xFFFF0000となり、VBARの影響を受けないということですね。やはりVビットを0にする必要があります。

ソースコードのどこでVビットを設定するかですが、VBARの設定の前にやってしまうのが手っ取り早い気がしますので、そうしましょう。ソースファイルはvbar_init.sです。

VbarInit: @FUNCTION
    MRC p15, 0, r0, c1, c0, 0    @ SCTLR読み出し(追加)
    BIC r0, r0, #(0x1 << 13)     @ Vビットを0にする(追加)
    MCR p15, 0, r0, c1, c0, 0    @ SCTLRへ書き戻し(追加)

@===================================================================
@ Set Vector Base Address Register (VBAR) to point to this application's vector table
@===================================================================
    LDR r0, =Image$$VECTOR_MIRROR_TABLE$$Base
    MCR p15, 0, r0, c12, c0, 0

    BX lr

VbarInitに3行追加して、SCTLRのVビットを0に変更します。

再び割り込みハンドラの動作確認

再度、割り込みハンドラの動作確認をするため、ビルドしてシリアルフラッシュへ書き込みます。先ほどと同じ手順で、割り込みハンドラ関数「Sample_OSTM0_Interrupt」にブレークポイントを設定し、ハードウェアの初期化後、ファンクションキーのF5を押して実行します。

再度実行

おおっ、やっと割り込みハンドラで止まることが確認できました。さらにF5キーを何度か押すと、繰り返して停止するようですので、タイマ割り込みがちゃんとかかっているようです。

割り込みハンドラでLEDを点滅

では、割り込みハンドラ内にLEDの点滅をプログラムを追加していきます。最初の呼び出しでLED1をON、LED2をOFFにして、2回目の呼び出しでLED2をON、LED1をOFFにします。これを繰り返します。

最初と次の呼び出しの状態を保持するためにstatic変数のフラグを一つ追加します。

void Sample_OSTM0_Interrupt(uint32_t int_sense)
{
    static int led_flag = 0;
    if (led_flag == 0) {
        GPIO.P7 |= 0x0100;   // LED1 ON
        GPIO.P7 &= ~0x0200;  // LED2 OFF
    } else {
        GPIO.P7 |= 0x0200;   // LED2 ON
        GPIO.P7 &= ~0x0100;   // LED1 OFF
    }
    led_flag ^= 1;
} 

こんな感じで作ってみました。led_flagをXORすることで0→1→0→1・・・と切り替えます。

あとはmain関数ですが、main関数でLEDを点滅させる役目は終わりましたので、無限ループを実行させて、割り込み待ちとします。また、sleep関数も不要ですので削除します。

int main(void)
{
    SystemInit();
    R_OSTM_Init(DEVDRV_CH_0, OSTM_MODE_INTERVAL, 500);
    R_OSTM_Open(DEVDRV_CH_0);

    GPIO.PMC7 &= ~0x0300;
    GPIO.PM7 &= ~0x0300;

    while (1); // 割り込み待ち
    return 0;
} 

これでLEDが500ミリ秒周期で点滅するはずです。では、ビルドしてシリアルフラッシュメモリに書き込んで試してみましょう。

あ、そういえば、LED1とLED2のチカチカの周期は1:2でしたよね。これは、フラグの操作でなんとかしてみましょう。

led_flag ^= 1;

この行を、

led_flag = (led_flag + 1) % 3;

としてみます。これで、led_flagの値が0→1→2→0を繰り返すので、「if (led_flag == 0)」のelse条件で、LED2の点灯時間が2倍になります。

LEDチカチカ

ご覧のようにLEDの点滅に成功しました。以上で、タイマ割り込みでのLチカは完了です。コンパイラの最適化やキャッシュに依存することなく、安定した時間で点滅を繰り返します。


さて、今回までの内容は結果としてLEDをチカチカさせただけでしたが、組込みシステムとして必要な、初期化ルーチン、GPIO操作、割り込みなどをおさえてきたと思います。今後は、もう少しハードルを上げて、周辺I/Oを使った、もう少し動きのあるものを作ってみたいと思います。

割り込み待ちについて

今回のプログラムでは、割り込み待ちにwhile(1)を使いましたが、実は無限ループというのは、CPUパワーを使います。Armには割り込み待ちの専用命令、WFI(Wait For Interrupt)が用意されています。この命令を使えば、割り込み待ちの間CPUが省電力モードに入るため、消費電力を抑えることができます。

GNU Cでは、インラインアセンブラを使って以下のように呼び出します。

__asm("wfi");