組み込みまするβ

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

フリーランカウンタで時間サービスを実現する

現実世界における時計の使われ方をイメージしてみましょう。 学校や公園、駅等の大きな時計を例として、ある人が2時17分から30分待ちたい場合、2時47分(厳密には2時48分)になるまで待つことになります。 起点となる時刻"2時17分"を憶えておいて、

(現在時刻ー2時17分)>30分

を毎回計算しても同じ判定ができますよね。

  • 先に30分後の時刻を計算しておき、その時刻が過ぎたかを確認する
  • 30分経過したかを毎回計算する

という実現方法の違いだけです。 これを複数の人たちが同時に時計を利用しても、時刻を確認するだけなので何の問題もありませんよね(相互に影響がない)。

さて、RTC(リアルタイムクロック)を搭載したシステムならば時刻で同様のことができますが、 ミリ秒やマイクロ秒、ナノ秒を扱うことが多いコンピュータ上では現実的ではありません(必ずしもRTCを搭載しているわけではないことも理由の1つ)。 ではどうするかというと、ほぼ確実に搭載されているタイマ(またはフリーランカウンタ)を時計代わりに利用し、 プログラム中の様々な箇所での時間待ちやタイムアウト判定と時間計測を実現してみましょう。

4bitの擬似ダウンカウンタ作成

実際のタイマを使ったとしてもカウンタ値を読み出すだけなので、 以下の関数をカウントダウン方式の4bitフリーランカウンタに見立てます。 この関数は呼び出す度にカウントダウンし、0x0の次はラップアラウンドして0xFを返します。

unsigned int Timer_readCounter(void)
{
    static unsigned int counter = 8; /* ラップアラウンド確認のため、適当に8からスタート */
    const unsigned int rc = counter--;
    counter &= 0xFU;
    return rc;
}

カウント差分の変化

次に起点となるカウンタ値からのカウント差分(diff)の変化をHexadecimalでみてみましょう。

const unsigned int baseCount = Timer_readCounter();

for (int i = 0; i < 16; i++) { /* 4bitの全パターンを確認するために16回としています */
    const unsigned int now = Timer_readCounter();
    const unsigned int diffCount = (baseCount - now);
    printf("base:%X, now:%X, diff:%X\n", baseCount, now, diffCount);
}

実行結果(実行環境はint:32bit)

base:8, now:7, diff:1
base:8, now:6, diff:2
base:8, now:5, diff:3
base:8, now:4, diff:4
base:8, now:3, diff:5
base:8, now:2, diff:6
base:8, now:1, diff:7
base:8, now:0, diff:8
base:8, now:F, diff:FFFFFFF9
base:8, now:E, diff:FFFFFFFA
base:8, now:D, diff:FFFFFFFB
base:8, now:C, diff:FFFFFFFC
base:8, now:B, diff:FFFFFFFD
base:8, now:A, diff:FFFFFFFE
base:8, now:9, diff:FFFFFFFF
base:8, now:8, diff:0

baseとnowの大小関係が逆転した場合は0xFFFFFFF9 ~ 0xFFFFFFFFのように符号無し整数のラップアラウンドが起きていますが、 今回のタイマは4bitなので下位4bitだけをみてみると0x9 ~ 0xFとなっています。 つまり符号無し整数のラップアラウンドを意図的に利用することで、 大小関係が逆転しても起点からのカウント差分は経過カウントとして使えることがわかりましたね。 なお、32bitで動作確認したい場合は「unsigned int」を「uint32_t」にして「counter &= 0xFU;」をコメントアウトしてください。

符号無し整数のラップアラウンドについてはJPCERT-CC:INT30-Cを参照してください。

減算の違反コードの解説に「減算時に符号無し整数のラップアラウンドを引き起こす可能性がある。 この動作を想定していない場合、攻撃可能な脆弱性につながる恐れがある。」とありますが、 本記事ではこの動作を想定しているということです。

経過カウントの判定可能範囲

次に経過カウントの判定範囲ですが、経過カウントは1周すると0に戻るので、1周を超える経過カウントの判定はできないことがわかると思います。 また、経過判定するカウント数が大きくなるに従って、タイムアウト判定可能な時間も短くなることが理解できるでしょうか。

例えば13カウント経過したことを判定するには、経過カウントが14~15(0xE~0xF)の間に判定しなければなりません。 この判定可能範囲から遅れると次に判定可能になるのはカウンタ1周分遅れてしまうことになるので、 経過カウントの判定可能範囲に対して十分に余裕を持った経過カウントかつ時間間隔で読みにいかなければなりません。

経過カウントがわかると何ができるのか

起点となるカウンタ値を保持することで、タイマを専有することなく経過カウント判定ができることがわかりましたね。 この経過カウント数をタイマの動作周波数から時間に換算することで経過した時間がわかります。 逆に時間からカウント数に変換することで経過時間を経過カウントとして扱えるようになり、 カウント指定のタイムアウト判定もできるようになります。 タイムアウト判定ができるということは、それを使って時間指定の待ちも実現可能ということです。

関数インターフェイス

「時間待ち、タイムアウト判定、時間計測」サービスを提供するためのインターフェイスが見えてきたでしょうか。

関数名 内容
now 現在のカウンタ値を取得する
convertUsecToCount 時間からカウント数に変換する
timeout 起点から指定カウント数が経過したかを判定する
waitUsec※ 指定時間の経過を待つ
measureDurationUsec 始点から終点までを時間に換算する

※「指定時間の経過を待つ」は先の3つの関数(now、convertUsecToCount、timeout)で実現できますが、使いやすさを考慮してwaitUsec関数を設けています。

何かのモジュール(コンポーネント)を作成する際、内部実装の変更が利用する側へ影響を与えない関数インターフェイスにしなければなりません。 それには実装を隠蔽(カプセル化)し、抽象化された操作(関数インターフェイス)を与え、モジュールの凝集度を高めることが重要です。 本記事のフリーランカウンタを利用した時間サービスのようなモジュールの場合は、 実装するための原理を十分に理解した上で関数インターフェイスを設計しなければなりません。

次のような順序での開発が望ましいですが、実際には戻ったりすることもあります。 ですが一般的に内部設計前であれば手戻りも少ないため、作業が無駄になることも少ないのです。

  1. 原理の理解

  2. 関数インターフェイス設計

  3. 内部設計

32bitダウンカウンタでの実装

static inline uint32_t elapsed_count(const uint32_t start_count, const uint32_t end_count) {
    return (start_count - end_count);
}

uint32_t now(void)
{
    return Timer_readCounter();
}

uint32_t convertUsecToCount(const uint32_t usec)
{
    return (countsPerUsec_ * usec);
}

bool timeout(const uint32_t base_count, const uint32_t timeout_count)
{
    return (elapsed_count(base_count, Timer_readCounter()) > timeout_count) ? true : false;
}

void waitUsec(const uint32_t usec)
{
    const uint32_t baseCount = Timer_readCounter();
    const uint32_t timeoutCount = (countsPerUsec_ * usec);
    while (elapsed_count(baseCount, Timer_readCounter()) <= timeoutCount);
}

uint32_t measureDurationUsec(const uint32_t start_count, const uint32_t end_count)
{
    const uint32_t elapsedCount = elapsed_count(start_count, end_count);
    return ((elapsedCount + (countsPerUsec_ - 1)) / countsPerUsec_);
}

1usecのカウント数であるcountsPerUsec_が何カウントになるかはTimerの動作周波数から予め計算しておきます。 また、使用される前にタイマをスタートさせておく必要があります。

使い方

/* wait */
waitUsec(50);

/* timeout */
const uint32_t baseCount = now();
const uint32_t timeoutCount = convertUsecToCount(100);
while (...) {
    if (timeout(baseCount, timeoutCount)) { break; }
}

/* measure */
const uint32_t startCount = now();
(計測したい処理)
const uint32_t endCount = now();
const uint32_t durationTimeUsec = measureDurationUsec(startCount, endCount);

注意点

タイマの停止

一度動作させたタイマを停止するとすべての時間待ちやタイムアウト、時間計測に影響しますので、 カウント開始後は停止させてはいけません。

タイマカウンタの読み出し

タイマカウンタの読み出しがアトミックでない場合は排他制御が必要になります。

例えば32bitタイマであっても、カウンタレジスタが上位16bitと下位16bitに分かれていて、 上位と下位を同時に取り出すために別のレジスタへのライトアクセスが必要といった仕様のタイマもあります。 このような場合、とあるコンテキストAがライトアクセス後から上位16bitと下位16bitを読み出すまでの間に割り込まれ、 別のコンテキストBからタイマカウンタ読み出し(ここで読み出しのためのライトアクセスを伴う)をされると、 先にカウンタの読み出しを行ったコンテキストAでは、本来読み出したかったタイミングからかけ離れたカウンタ値を読み出してしまうことがあるでしょう。

  • A:ライトアクセス
  • A:カウンタの上位16bitを読み出す
  • (コンテキストBに割り込まれた)
    • B:ライトアクセス(※Aが取り出したタイミングの上位16bit、下位16bitが上書きされる)
    • B:カウンタの上位16bitを読み出す
    • B:カウンタの下位16bitを読み出す
  • (コンテキストAに戻ってきた)
  • A:カウンタの下位16bitを読み出す(※Bが取り出したタイミングの下位16bitを読み出すことになる)

他には16bitタイマを2つ使用してカスケード接続にし、32bitタイマとして扱うような場合もあります。 この場合は上位と下位を同時に取り出すことができないため、 読み出しの度にラップアラウンドチェックが必要になります(ルネサスSH系の16bitタイマのカスケード接続等)。

シングルタスクかつ割り込みコンテキストで使用しないのであれば排他制御は不要となりますが、 タイマ仕様によって制限ができてしまうので移植性を考慮すると必須となるでしょう。

当然ですが本記事の4bit擬似ダウンカウンタも同様です

カウント数と時間の相互変換

カウント数と時間を相互変換するcountsPerUsec_には注意が必要です。 端数が出るような動作周波数だった場合、ウェイト・タイムアウト関連なら、より長く待つ仕様にすることで本来の目的を果たさなければなりません。 時間計測の場合は、実際にかかった時間よりも計測結果が短いと守らなければならない処理時間をわずかに越えた場合でもパスしてしまう可能性があります。 このため長い結果となる仕様にしたいのですが、countsPerUsec_を使うわけにはいかないので計測用の変換パラメータが別に必要となります。 但し、これは考え方の1つで正解というものではなく、用途に応じて仕様を定め、仕様を理解した上で利用しなければなりません。

まとめ

プログラム中の様々な箇所で時間待ちやタイムアウト判定、時間計測ができるようになりました。 しかしながら本記事の実装はフリーランカウンタを利用した時間サービス実現方法の一つなので、 他にもっと良い実現方法があるか等、是非いろいろと考えてみてください。

浮動小数点数型を使わない理由

組み込み向けにはFPUを持たないCPUがあり、 浮動小数点型を使うために多くのサイクル数を消費したくないので整数型のみで実装しています(時間に関するコンポーネントというのも理由の1つです)。

さて、ここまではintが32bitの環境で動作確認をしてきましたが、 タイマは32bitのまま、intが64bitの環境ではどうなるかわかるでしょうか。 まずはどのように動作するのかを考え、実際に64bit環境で試してみてください。 ※printfの64bit整数の書式に注意

embed.hatenablog.com