【仕様】ミリ秒オーダーのタイマ機能を設計する。また、タイマ要求は同時に10以上設定可能としコンフィグレーション可能とする。
【安易なソフトウェア設計】
ミリ秒オーダーなので1ミリ秒ごとにタイマ割り込みなどで起床して、それぞれのタイマ要求時のタイマ値から1づつ減算して処理する設計とする。
この設計のメリットは開発コード量が少ないことくらいで、デメリットは実行時に無駄なCPUコストが掛かる(無駄な割り込み処理を何度も繰り返し実行してしまう)こと、要求時間より前に発火通知してしまう、消費電力が掛かってしてしまう(IOT機器には向かない)こと。
【上級なソフトウェア設計】
タイマ要求の中から直近にタイマ発火(T.O)するまで待ち状態(割り込みなどによる起床を行わない)にする。この設計のメリット・デメリットは【安易なソフトウェア設計】であげた事項の真逆となる。
タイマドライバの設計方針
当サイトでは、【上級なソフトウェア設計】を採用して設計することにします。設計する上でハードウェアは必須ではないが、タイマ機能の心臓部である時間管理について検討するためにもタイマコントローラをどのように動作させるか?動作イメージの機能がハードウェアに実装されているかを確認する必要がある。ハードウェアはarm Cortex-M3 STM32F3マイコンのタイマコントローラを使用する前提で記載します。
タイマコントローラ
【使用するタイマコントローラの選択および動作設定】
CPUがスリープ状態でも動作する16bitタイマコントローラを選択することにする。
タイマコントローラのカウンタは16bit、カウンタ動作はカウントアップ、カウンタのコンペアマッチ割り込み(許可)、カウンタオーバーフロー割り込み(許可)を使用(動作設定)することにする。
仕様はミリ秒オーダータイマなので、カウンタ値は 1カウント=0.125ミリ秒(1ミリ秒の1/8)(または 1カウント=0.25ミリ秒(1ミリ秒の1/4))の精度をチョイス(動作設定)することにする。※カウンタがオーバーフローする際に内部カウンターが端数(1/8以外)でオーバーフローしないようにすること。(10進数で考えるのではなく16進数で考えること)
因みに、1カウント=0.125ミリ秒に設定する場合のタイマコントローラの動作周波数は、T(秒)=1/f(Hz)から算出して 8MHzが入力されるように設定する。
以降、タイマコントローラが刻むカウントの事を「TICK」と記す。
上記設定は、タイマコントローラ、割り込みコントローラ、クロックコントローラに設定することになると思います。
プログラム設計
まずは、タイマを使用する側プログラムとのインタフェース(API)について基本的な機能を抽出してみると、
・タイマ要求
・タイマ取消(キャンセル)
・タイマ発火(T.O)通知
・タイマカウンタ(TICK)値取得
位が思いつきますよね。これらについて設計することにします。
ざっくり設計を進める過程でプログラム構成が明確になると思います。その際に図示しましょう。
まずタイマ要求から設計したいところですが、それより先に「タイマ発火(T.O)通知」方法をアクター(APIを使用する側)と相談して決めておく必要があります。タイマ要求時のパラメータに要素を盛り込むことになるからです。
【タイマ発火(T.O)通知】方法の検討
タイマのデータシートが英文の場合、”Time(r) Expire”と記載されている事からタイマ発火という名称にしました。以前はタイムアウト通知などと言われてました。
さて本題に入ります。
まずタイマが発火すると、タイマ割り込みハンドラが呼び出されますが、タイマ依頼元への通知も合わせて行うと割り込み処理時間が掛かるため、おすすめしません! 理由としては、多重割り込み可能なハードでもタイマ割り込みより優先度が低い割り込みは処理が終わる迄ペンディングされるからです。
システムにもよりますがタイマ機能にタスクを設けてそのタスクから通知する方法をおすすめします。通知方法は、RTOSリソースのメッセージやイベントフラグで通知するか、タイマ依頼元の関数をコールバックするか、になるでしょう。当サイトでは、タイマ依頼元へのコールバック通知とします。
通知方法が決定したら、タイマ要求の設計に取り掛かります。
【タイマ要求】の設計
タイマ要求時のパラメータを格納するメモリは、当機能にて内部管理するメモリプールを用いますが、メモリプールは動的メモリallocate関数のmalloc()など使わず静的変数として宣言したほうが無難です。要求ごとにメモリプールからallocateして、発火や取消ごとにfreeします。メモリプール数は、タイマ要求が同時稼働する最大数となります。タイマ機能を利用するアクターがコンフィグレーションできるようにヘッダーファイルなどに定義しましょう。
時間管理について、タイマ要求時のパラメータにてミリ秒を指定してもらいますが、内部ではタイマカウンタのカウント(TICK)値に変換すべきです。理由は、ミリ秒をそのまま使用すると要求時間前に通知することになり兼ねないからです。またタイマコントローラから取得できるカウンタはTICK値ですから、タイマ値はTICK値で演算したほうがシンプルになります。
時間計算方法ですが、最低でも2通りが考えられると思います。
①到達時間を計算して用いる方法
②経過時間を計算して用いる方法
今回は、「①到達時間を計算して用いる方法」にします。1つのタイマ要求に対して何度も計算する必要がないのでシンプルになります。
タイマ要求時に現在のTICK値を読み出し、要求タイマ値(ミリ秒)をTICK変換した値と加算したTICK値(=到達時間)をリストデータ内に保存して、最小値を次発火するタイマカウンタ値としてコンペアマッチレジスタにセットして発火させる方法。
タイマ要求時のパラメータはリストデータ内に保存します。その他生成するデータとしてタイマ発火までの到達時間(TICK値)を保存します。リストデータは、到達時間で昇順につなげて管理しましょう。このタイマ要求リストポインタとは別にタイマ発火通知用のリストポインタも設けたほうが無難かもしれません。
リストデータ構造(リソース)を使う場合の注意事項として、リスト操作(追加・取り外し)は一つのタスクで行うのがベストです。複数のタスクからリソース操作するとトラブルリスクを負うことになりますので、単一タスクでリソース操作するように設計しましょう。リスクを負う覚悟でリソース操作を行う場合、操作中はCritical Sectionで囲み他のタスクが実行されないようにて、ポインタの付け替えを確実に行えますがディスパッチ禁止時間による他タスクへの影響のリスクを負うことになります。
リスト操作は、タイマ要求APIで行わずタイマ機能のタスクで行うため、識別子(タイマハンドル識別子)を用いることにします。タイマハンドル識別子取得は、ミューテックスで競合回避します。
メモリプールに格納するメンバ変数は、タイマ要求時のパラメータの「タイマ要求時間(ミリ秒)」、「コールバック関数アドレス」と、内部管理データの「発火到達TICK値」、「タイマ要求リストポインタ」、「タイマ発火処理用のリストポインタ」、「メモリプールの使用状態」、「タイマハンドル識別子」になります。
メモリプールに格納するメンバ変数を構造体で定義します。変数の定義順はアライメントに気を留めながら、お好みに定義してよろしいかと思います。 typedef struct TIMER_HDL_t { uint16_t used; /* メモリプールの使用状態 */ uint16_t req_time; /* タイマ値(ミリ秒) */ int32_t exp_tick; /* 発火到達時間(TICK値) */ void (*func_cb)(uint16_t timer_id); /* 依頼元へのコールバック関数アドレス */ struct TIMER_HANDLE_t *p_req; /* タイマ要求のリストポインタ */ struct TIMER_HANDLE_t *p_exp; /* タイマ発火処理用のリストポインタ */ uint16_t timer_id; /* タイマハンドル識別子 */ } TIMER_HDL_t;
タイマ要求APIのプロトタイプは以下になります。 uint16_t timer_req( uint16_t req_time, void (*func_cb)(uint16_t timer_id) ); [parameter] req_time:タイマ値(ミリ秒) func_cb :依頼元へのコールバック関数アドレス [return] タイマハンドル識別子
【タイマ取消】の設計
タイマ取消のパラメータは、タイマ要求の戻り値(ハンドル)を指定してもらい、タイマ機能のタスクにてリストから外す方法がよいと思います。タイマ発火と同時のタイマ取消のクロス(交差)が起こる可能性をゼロにはできないので、アクター側で対処してもらうように「制限付け」しょう。そのための情報としてアクターにハンドルを返してますよね。
リストデータ構造は、片方向リスト/両方向リストがありますが、お好みで採用して下さい。但し、メモリプールから空きメモリを取得する際、毎回先頭から空きをサーチするのではなく、リングバッファ形状にしてリストデータ取得ポインタをどんどん更新して新しいメモリ(先頭に戻ったら最古のメモリ)から取得するようにしましょう。アクター側が短期間で同一ハンドルを扱う可能性をなくすためです。
タイマ取消APIのプロトタイプは以下になります。 void timer_cancel( uint16_t timer_id ); [parameter] timer_id:タイマハンドル識別子(タイマ要求の戻り値)
【タイマ発火通知】の設計
タイマ発火の本体処理は、タイマ機能のタスクで行います。ハードウェア動作は、「タイマコントローラ」から「割り込みコントローラ」を経由して「タイマ割り込みハンドラ」がキック(コール)されます。要因は冒頭で割り込み許可した「カウンタのコンペアマッチ割り込み」「カウンタオーバーフロー割り込み」になります。
タイマ割り込みハンドラがキックされたら、まず割り込み要因をクリアして、タイマ機能のタスクにイベントかメッセージを通知するようにします。
タイマ機能のタスクがその通知を受け取ったら、タイマ要求の全リストデータからTICK値を調べて発火しているか否かを判定して発火している場合、一旦発火用リストに連結(チェイン)します。全てのリストデータのタイムアウト(T.O)判定完了後にアクターが用意したコールバック関数を順番にコールして発火通知します。
要求リストポインタを操作する間は、Critical Sectionで囲んで処理します。
タイマ発火時のコールバック通知形式は以下になります。 void (*func_cb)(uint16_t timer_id); [parameter] timer_id:タイマハンドル識別子(タイマ要求時の戻り値)
【タイマカウンタ値取得】
経験上、タイマ要求の残り発火時間をミリ秒で返すAPIを作っても利用する場面は殆どないので、タイマコントローラのTICK値をレジスタから読みだして戻り値で返すAPIを用意ます。
このAPIは、RTOSのディスパッチタイミングでTICKを格納したログ機能に使えます。カウンタ値が16bitで足りない場合は、カウンタオーバーフロー割り込み要因を利用して、タイマ機能内部管理のタイマカウンタ(32bitまたは64bit)に加算して戻り値で返すようにします。
タイマカウンタ値(32bit)取得APIのプロトタイプは以下になります。 uint32_t timer_tick(void); [return] 32bitカウンタ(TICK)値
【タイマ機能のタスク】
タイマ機能のタスク(以降タイマタスクと記す)の設計は、APIで設計したことを取りまとめることにより設計の手戻りが少なくなると思います。
タイマタスクは、メッセージを待ち受けるメッセージキューを受け口としたイベント駆動方式のタスクにします。
受信するメッセージを整理すると以下になります。
- タイマ要求APIからのタイマ要求メッセージ(REQ_MSG)。付属情報はタイマ値(ミリ秒) 、発火到達時間(TICK値)、コールバック関数アドレス、タイマハンドル識別子。
- タイマ取消APIからのタイマ取消メッセージ(CAN_MSG)。付属情報はタイマハンドル識別子。
- タイマ割り込みからの発火メッセージ(EXP_MSG)。付属情報は割り込み要因。
各メッセージを受信した際の処理内容を箇条書きします。
【タイマ要求メッセージ】
- メモリプールからメモリを獲得して、メッセージ付属情報を格納する。
- 要求リストにメモリプールから獲得した領域を発火到達時間(TICK値)昇順にして連結する。
【タイマ取消メッセージ】
- 要求リストから取り消すタイマハンドル識別子を走査してリストから外す。
- リストから外した領域をメモリプールに解放する。
【発火メッセージ】
- 付属情報の割り込み要因別に処理する。
- コンペアマッチ割り込みの場合。
- 現在のタイマカウンタ値取得APIをコールして現在のTICK値より小さいデータを要求リストからすべて外して、発火リストに連結する。
- 発火リストに連結されたリストデータに格納されているコールバック関数を順次コールする。
- リストデータをメモリプールに解放する。
- カウンタオーバーフロー割り込みの場合。
- タイマ機能内部管理のタイマカウンタ値の16bit以上の部分に+1加算(+0x10000)する。
- コンペアマッチ割り込み処理と同じ処理を行う。
- コンペアマッチ割り込みの場合。
設計は以上で終わります。
実装規模は定義も含めて500ステップ程度でしょう。