組み込みまするβ

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

組み込みソフト版Chronoを作成する

フリーランカウンタを利用した時間経過待ち、タイムアウト判定と時間計測の仕組みは以前の記事で説明していますので、 本記事ではそれをサービスとして提供するコンポーネント(Chrono)を作成します。 時間に関するサービスを提供するコンポーネントはシングルインスタンスとしたいのでSingletonパターンを使いますが、 組み込みソフトはC言語での実装を求められる場合が多いのでC言語版Singletonで実装します。 なお、組み込みシステムはプログラムの終了が電源断であることも多く、 シングルタスクベースで設計することも多いため、これらの条件に合致する組み込みソフトウェアはSingletonパターンと相性が良いと考えています。

Singletonパターンはグローバル変数という悪評の推測

classを変数の入れ物として用いてアクセッサ(set/getメンバ関数)を作成すると、 何処からでもread/writeできてしまうのでグローバル変数として扱っているのと同じです(データホルダ的な使い方)。 そのようなclassに対してSingletonパターンが適用された実装を見た人が、 「Singletonパターンはグローバル変数的でよくない」と短絡的に認識してしまったのではないでしょうか。 Singletonはインスタンスを1つに保証するパターンなので、Singletonパターンの適用によってグローバル変数化されるわけではありません。

構成要素

Chronoの構成要素はカウントアップまたはカウントダウンするフリーランカウンタ1つだけです。

Singletonにしたい理由

何の制御もなしに動作しているカウンタを利用可能なCPUはあまりありません。 このため、通常は汎用タイマをフリーランカウンタのように動かして代用することになります。 具体的には32bitタイマでカウントダウン方式を採用する場合はアンダーフローしたら0xFFFFFFFFにリロードする設定でスタートさせ、以後フリーランカウンタとして扱います。 このような初期設定がカウント動作を開始する前に必要になるということですね。

カウントアップ方式の場合はオーバーフローしたら0x00000000にリロードする設定とします。

また、Chronoの使用目的は時間待ちやタイムアウト判定、時間計測等の時間に関するものですから、 プログラム中の様々な箇所で呼び出して使用します。 しかしながら使用する度に別々にインスタンス化する必要はないので、実体を1つ(初期化も1回となる)に制限したいのです。

以上より次の条件が求められます。

  1. 初期化が必要
  2. プログラム中の様々な箇所で使用したい
  3. 実体を1つに制限したい

初期化が不要ならオブジェクト概念を使わずに関数ライブラリにします。

タイマ

利用するタイマについてですが、CPU環境によってデバイスが異なりますので抽象化したインターフェイスで制御します。 この抽象化したインターフェイスで扱う実装もC言語なのですが、それは後の記事で書こうと思っていますので今回は解説もコード例も記載しません。

インターフェイス

関数インターフェイスフリーランカウンタで時間サービスを実現する - 組み込みまするβで既に決まっていますが、 それに加えてタイマの初期化をするinitialize関数を追加しました。 しかしながら他の関数呼び出しに制約もないので、このままでは未初期化で使用される可能性があります。

/* emb_chrono.h */

int Chrono_initialize(void);

uint32_t Chrono_now(void);
uint32_t Chrono_convertUsecToCount(uint32_t usec);
bool Chrono_timeout(uint32_t base_count, uint32_t timeout_count);
void Chrono_waitUsec(uint32_t usec);
uint32_t Chrono_measureDurationUsec(uint32_t start_count, uint32_t end_count);

プログラム中の様々な箇所で使用されるChronoのようなコンポーネントの場合、 それを利用するコンポーネントやコードが移植されることも意識しておかなければなりません。 未初期化で使用される可能性を残したままでは初期化されているかの確認が常についてまわり、 非常に使い勝手の悪いコンポーネントとなってしまうのです。

オブジェクト概念

Chronoとは時間に関するサービスを提供するコンポーネントと定義していますが、 それを実現するために内部パラメータや内部関数及び使用者側へサービスを提供するための外部関数が必要となります。 内部パラメータは未初期化では使えませんので、Chronoという名称の構造体で管理することを考えましょう。 ひとまず構造体の定義は未定のままでかまいませんが、 これによって「Chrono構造体の初期化」を「Chronoコンポーネントの初期化」と見なすことができるようになります。

そして初期化されたChrono構造体の実体へのポインタを得るため、initialize関数の返却値をChronoへのポインタに変更します。

/* emb_chrono.h */

typedef struct Chrono Chrono;
Chrono* Chrono_initialize(void);

インスタンスの取得

一般に実体(ここではChrono構造体の実体)のことをインスタンスと呼びますので、 initialize関数の名称もgetInstance関数に変更しましょう(Singletonでは定番の関数名称ですね)。

/* emb_chrono.h */

typedef struct Chrono Chrono;
Chrono* Chrono_getInstance(void);

未初期化での実行を防ぐ

未初期化で実行できてしまう関数に制約を課すため、各関数の第一引数にChronoへのポインタ(self)を追加します。 これでChronoインスタンスへのポインタを得なければ各関数は実行不可となるため、安全にChronoを利用することができるようになりましたね。 なお、Chrono構造体の定義を知る必要はありませんのでオペークポインタのままです。

/* emb_chrono.h */

typedef struct Chrono Chrono;
Chrono* Chrono_getInstance(void);

uint32_t Chrono_now(Chrono* self);
uint32_t Chrono_convertUsecToCount(Chrono* self, uint32_t usec);
bool Chrono_timeout(Chrono* self, uint32_t base_count, uint32_t timeout_count);
void Chrono_waitUsec(Chrono* self, uint32_t usec);
uint32_t Chrono_measureDurationUsec(Chrono* self, uint32_t start_count, uint32_t end_count);

次のように利用することができます。

/*! usage */

/* get instance */
Chrono* const self = Chrono_getInstance();

/* wait */
Chrono_waitUsec(self, 5);

/* timeout */
const uint32_t baseCount = Chrono_now(self);
const uint32_t timeoutCount = Chrono_convertUsecToCount(self, 100);
while (...) {
    if (Chrono_timeout(self, baseCount, timeoutCount)) { break; }
}

/* measure */
const uint32_t startCount = Chrono_now(self);
(時間計測したい処理)
const uint32_t endCount = Chrono_now(self);
const uint32_t durationTimeUsec = Chrono_measureDurationUsec(self, startCount, endCount);

関数引数のChronoインスタンスポインタを取り払う

未初期化ガードのために導入したChronoインスタンスへのポインタですが、 本来シングルインスタンスであればインスタンスを指定する必要はないため、 各関数の引数にインスタンスポインタを渡すのは煩雑に感じます。 では初期化の義務化対応を残しつつ引数のインスタンスポインタを取り払うにはどのような実装にすればよいでしょうか。

Chronoインスタンスへのポインタ引数を関数実行の制約にしていましたので、 インスタンスへの引数を取り払うには関数の直接実行をできないようにしなければなりません。 具体的には各関数ポインタをChrono構造体のメンバとすることで間接的に各関数を呼び出すことにします。 但し、これによって初期化の義務を残すことはできますが、Chrono構造体は公開することになってしまいますね。 メンバの中で関数ポインタ以外の変数については公開したくないので、Chrono構造体での管理を止めてソース側で大域変数としてstaticで隠蔽します。 以上の対応によって、初期化の義務を残して関数引数のChronoインスタンスへのポインタを取り払うことができます。

関数ポインタをメンバに持つ構造体をインターフェイスとして扱う実装は、 C言語でpublic継承を実現する際の基礎知識になります。

/* emb_chrono.h */

typedef struct Chrono Chrono;
const Chrono* Chrono_getInstance(void);

struct Chrono {
    uint32_t (*now)(void);
    uint32_t (*convertUsecToCount)(uint32_t usec);
    bool (*timeout)(uint32_t base_count, uint32_t timeout_count);
    void (*waitUsec)(uint32_t usec);
    uint32_t (*measureDurationUsec)(uint32_t start_count, uint32_t end_count);
};

ポインタ名をselfからchronoに変更しました。 以下のように使うことが出来ます。

/*! usage */

/* get instance */
const Chrono* const chrono = Chrono_getInstance();

/* wait */
chrono->waitUsec(5);

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

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

インスタンスへのポインタ変数を宣言せずに使用することもできますが使い方として推奨しません。 その理由は未初期化だった場合に初期化処理の時間も含まれてしまうからです。

/* wait */
Chrono_getInstance()->waitUsec(5);

実装

では実装をみてみましょう。 インターフェイスとして公開している関数についてはインライン化できないため、 関数呼び出しをせずに展開して記述している箇所もあります。 ごく短いコードなのでインライン用の関数を作成しても返って読み難くなるのが理由です。

/* emb_chrono.c */

static void ctor(void);

static uint32_t now(void);
static uint32_t convertUsecToCount(uint32_t usec);
static bool timeout(uint32_t base_count, uint32_t timeout_count);
static void waitUsec(uint32_t usec);
static uint32_t measureDurationUsec(uint32_t start_count, uint32_t end_count);

static uint32_t countsPerUsec_;
static uint32_t measurementUnitUsec_;
static struct Timer* timer_;

/* マルチタスク環境や割り込み処理内でも利用する場合はgetInstance関数に排他制御が必要になります。 */
const Chrono* Chrono_getInstance(void)
{
    static const Chrono instance = {
        now,
        convertUsecToCount,
        timeout,
        waitUsec,
        measureDurationUsec
    };
    static int initialized = 0;

    if (initialized) { return &instance; }

    ctor();
    initialized = 1;

    return &instance;
}

/* コンストラクタ */
static void ctor(void)
{
    /* タイマを生成して抽象タイマへのポインタを取得 */
    timer_ = CrnTimerFactory_create();

    /* タイマのカウントパラメータ */
    const TimerCountParams params = {
        kTIMER_COUNT_METHOD_DOWN,   /* カウントダウン方式 */
        kTIMER_RELOAD_ENABLE,       /* リロード有効 */
        0xFFFFFFFFUL               /* リロードカウンタ値 */
    };

    /* タイマのカウント設定 */
    Timer_setup(timer_, &params);

    /* タイマの動作周波数から1usecのカウント数を算出 */
    countsPerUsec_ = ((Timer_getFrequency(timer_) + (1000000 - 1)) / 1000000);
    measurementUnitUsec_ = (Timer_getFrequency(timer_) / 1000000);

    /* タイマのカウント動作を開始 */
    Timer_start(timer_);
}

/* 経過カウント算出のインライン関数 */
static inline uint32_t elapsed_count(const uint32_t start_count, const uint32_t end_count) {
    return (start_count - end_count);
}

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

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

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

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

static 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 + (measurementUnitUsec_ - 1)) / measurementUnitUsec_);
}

まとめ

時間に関するサービスをChronoコンポーネントとして実装しておくことで最低限の移植性を確保できるようになりました。 ChronoはどのようなCPUでも利用できると思いますが、実用化に向けてエラー処理が必要です。 また、Timerを動かすだけでもドライバを作成しなければなりませんが、 新規に実装するにしてもカウント設定と動作周波数及びカウンタ値読み出しだけなので簡単だと思います。

Timerの生成について

Timerオブジェクトを生成するには(C言語での実装ですが)具象classを知らなければなりませんが、 CPU環境に応じてタイマデバイスが異なるため、Chronoのソースで生成してしまうと移植の度に修正が入ってしまいます。 シングルトンによる自動初期化の利便性を捨て、Chronoの初期化関数にTimerの抽象classを渡す実装も可能ですが、 本記事ではTimerオブジェクトを生成する関数(CrnTimerFactory_create)を別途設けてソースを分離しています。

16bitタイマのみの場合

[emb_chrono.c]内でのカウンタ値の扱いを32bitから16bitにすれば使うことができます。 ただし、16bitで表現できる範囲になりますので、動作周波数によってはmsec単位の関数を追加しても実用的ではない可能性が高いと思います。 また、16bitの表現範囲なので通常は分周してカウントクロックを下げているでしょうから、 nsec単位の関数を追加したとしても当然精度は悪くなるので使い物になるかはアプリケーション次第でしょう。