組み込みまするβ

組み込みソフトウェア初学者向けブログ

時間経過を待つ

組み込みソフトウェアで時間経過を待つにはどうしたらよいでしょうか。

組み込みOSを採用している場合は所謂sleep系関数、あるいは開発環境でwait、delay等の関数ライブラリが用意されていることもあります。 ですがOSレスかつ関数ライブラリも用意されていない場合、時間経過を待つ仕組みを作成しなければなりません。

単純な時間待ちの関数

単純な時間待ちは以下のように最適化を抑制してカウントすることで実現できますが、 待ち時間が明確ではないので移植性がなく業務レベルでは使い物にはなりません。 しかしながら、その場限りと自分に言い訳をしたコードが散見されるのが実情ではないでしょうか。

void wait(const unsigned int count)
{
    volatile unsigned int vcount = count;
    while (vcount--);
}

Timerを利用した待ちの関数

Timerを利用できるなら、カウントするクロックとカウント数で指定時間の待ちを実現できます。 例えば100MHzで動作するTimerならば、100,000,000カウントで1sec、100,000カウントで1msec、100カウントで1usecとなります。 このTimerを利用して指定時間待ちを実現してみましょう。

void Timer_wait(const unsigned int usec)
{
    /* 100MHz動作のTimerでカウントダウンさせる(リロード及び割り込みは未使用) */
    const unsigned int initialCount = (usec * 100);
    Timer_setLoadRegister(initialCount); /* TimerのloadレジスタにinitialCountを設定する */
    Timer_start();                       /* Timerのカウントを開始する */
    while (Timer_readCounter());         /* 0で止まるのを待つ */
}

Timerを利用することで指定した時間を待つことはできますが、 Timerを占有しているので複数のコンテキストで同時に利用することはできません。

アセンブリ記述の待ちの関数

Timerを利用できない場合でも、CPUの動作周波数と命令実行のサイクル数を時間に換算することで時間経過を待つことができます。 以下は最初のwait関数をMicroBlazeアセンブリコードにしたものです(他のRISCプロセッサでも同じようなコードになるでしょう)。

    .align  4
    .global mb_asm_wait
    .ent    mb_asm_wait

mb_asm_wait:
    add     r3, r5, r0
1:  /* ここから */
    add     r4, r3, r0      /* r0は常に0のレジスタなのでr4 = r3の意味 */
    bneid   r4, 1b          /* 分岐遅延スロットを使用、r4が非0なら次のカウントダウンを実行して1:へ */
    addi    r3, r4, -1      /* 遅延スロットでカウントダウン r3 = r4 - 1 */
    /* ここまで */
    rtsd    r15, 8
    or      r0, r0, r0

    .end    mb_asm_wait

ここで重要なのはカウントダウンするループが一周するのに何サイクルかかるかで、 この例ではaddで1、bneidで2、addiで1の計4サイクルかかることになります。 CPUが100MHzで動作しているとしたら、1secは100,000,000サイクルなので、4サイクルで割った25,000,000ループで1secとみなすことができ、 同じように25,000ループで1msec、25ループで1usecとみなせます。

アセンブリ記述

アセンブリ言語での記述に慣れていない場合は、最初のvolatileループのwait関数に最適化をかけてアセンブリ出力してみましょう。 C/C++コンパイラの設定でオブジェクト出力をアセンブリ出力に変更すると、 C/C++ソースファイル単位にアセンブリのソースファイルが出力されると思います。

そのアセンブリコードの各命令をプロセッサ(インストラクション)マニュアル等で調べ、ループしている箇所を見つけてください。 あとはループに何サイクルかかっているかがわかれば、アセンブリコードを書く必要もなく、どのようなCPUでも対応することができると思います。

wait関数をアセンブリ言語のソースファイルとして独立させる目的

C言語/C++コードのままでは最適化の設定によってサイクル数が変わる可能性があるので、 C言語/C++ではなくアセンブリ言語のソースファイルで管理します。 アセンブリ言語のソースファイルであれば他のCPU環境へ移植の際に未対応のままでは使えませんので、 移植対象CPUでのアセンブリコード作成及びループサイクル数の確認を義務化することができます。

アセンブリ記述を利用した待ちの関数

このアセンブリの待ちルーチン(mb_asm_wait)を利用することで、以下のように時間単位の待ちを実現できるようになります。 但し、この実装はCPUの動作周波数によって端数がでてしまうことになるので、 指定時間を確実に待つようにするためにkLOOP_CNT_PER_USECを算出する際の割り算では端数を切り上げています。 なお、より精度を求めるならば割り切れるようにnop命令等を追加してサイクル数の調整をしてもよいですが、 割り込みやタスクスイッチ等の外部要因により精度を保証するのが難しいので期待した効果は得られないかもしれません。

enum { kCPU_FREQ_HZ = 100000000 };
enum { kCYCLES_PER_LOOP = 4 };
static const unsigned int kLOOP_CNT_PER_USEC = (kCPU_FREQ_HZ + ((1000000 * kCYCLES_PER_LOOP) - 1)) / (1000000 * kCYCLES_PER_LOOP); /* 端数切り上げ */

void waitUsec(const unsigned int usec)
{
    mb_asm_wait(kLOOP_CNT_PER_USEC * usec);
}

待ち時間の精度が悪い場合

  • 命令キャッシュがない環境では命令フェッチ等のメモリアクセスで多くのサイクル数を消費するため、計算したサイクル数よりも余分に待つことになります(重要でない用途のソフトマクロCPUでは、命令キャッシュなしで構成する場合もあります)
  • 時間経過待ち中に割り込みが入るとループカウントが止まるので、割り込みの頻度と処理時間に応じて遅れることになります

Timerによる待ちの場合はカウントが止まらないので、 遅れは待ち時間経過後に再びTimerを見に行くまでの時間に抑えられます。

まとめ

アセンブリ記述ループでの時間経過待ちはTimerを利用できない状況での苦肉の策です。 既存のプログラムを引き継いだ際に利用可能なTimerが余ってなく、 より短い時間待ちが必要な場合等に良い選択となるでしょう。

CPUの動作周波数にもよるのですが、usec単位以下の短い時間に対してはアセンブリ記述ループでの待ちを使用し、 msecやsec単位以上の時間についてはTickを利用するのが一般的だと思います(16bitのタイマのみという環境もあるので)。

なお、この記事のTimerを使った実装ではTimerを占有してしまいますが、 32bit-Timerまたは16bit-Timerを動かしっぱなしにすることで、 待ちやタイムアウト判定を複数個所で使用できる実装もあります(フリーランカウンタを利用した実装)。

embed.hatenablog.com