Chapter 15
ソフトウェアタイマ — タイマタスクという「専用のスタッフ」
この章がなぜ必要なのか——「タイマ」という言葉が 3 つのものを指している.
ハードウェアタイマ、ティック割り込み、そしてソフトウェアタイマ。 この 3 つは全く別物である。
そして FreeRTOS のソフトウェアタイマには、独特の制約がある。 コールバックは「割り込みの中」ではなく「タイマタスクという普通のタスクの中」で走る。 ここを理解しないと、応答時間の見積りが根本から狂う。
この章で使う既出の用語(定義は各リンク先). コンテキスト(01 章 6 節)、スタック(02 章 2 節)、タスク(02 章 2 節)、スケジューラ(03 章 2 節)、ハードウェア(03 章 2 節)、イベントリスト(04 章 4 節)、イベント待ち(04 章 4 節)、時間待ち(04 章 4 節)、FreeRTOS(06 章 4 節)、RAM(06 章 4 節)、削除(07 章 2 節)、追加(07 章 2 節)、ブロック(13 章 1 節)、キュー(14 章 7 節)
1. 3 つの「タイマ」の違い
| 種類 | 実体 | 精度 | コールバックの実行場所 |
|---|---|---|---|
| ハードウェアタイマ | CPU 内蔵のペリフェラル | ns〜µs | 割り込みハンドラ |
| ティック | ハードウェアタイマ 1 本を OS が占有 | ティック周期 | カーネル内部 |
| ソフトウェアタイマ | ティックの上にソフトで実装 | ティック周期 | タイマタスク(普通のタスク) |
ソフトウェアタイマは「ハードウェアを消費せずに、いくつでもタイマが持てる」のが利点である。 100 個作っても、使うハードウェアタイマはティック用の 1 本だけである。
代わりに、精度と即時性を捨てている.
ハードウェアタイマなら µs 精度で、割り込みで即座にコールバックが走る。 ソフトウェアタイマはティック周期の精度で、しかもタイマタスクが走るまで実行されない。
正確なタイミングが要るならハードウェアタイマを使うこと。 ソフトウェアタイマは「だいたい 1 秒後に何かする」用途のものである。
2. タイマタスク(デーモンタスク)
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY 3 /* タイマタスクの優先度 */
#define configTIMER_QUEUE_LENGTH 10 /* コマンドキューの長さ */
#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZEvTaskStartScheduler() の中で、タイマタスクが自動的に生成される(03 章)。
このタスクがやることは、たった 2 つである。
static portTASK_FUNCTION( prvTimerTask, pvParameters )
{
for( ;; )
{
/* (1) 次に期限が来るタイマの時刻を調べる */
xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
/* (2) その時刻まで寝る。起きたら期限が来たタイマを処理する */
prvProcessTimerOrBlockTask( xNextExpireTime, xListWasEmpty );
/* (3) コマンドキューに溜まった要求を処理する */
prvProcessReceivedCommands();
}
}構造の全体像
アプリケーションのタスク / ISR
│
│ xTimerStart() / xTimerStop() / xTimerChangePeriod()
↓
┌──────────────────┐
│ タイマコマンドキュー │ ← ただのキュー([11 章](11_キュー.md#キューセット-複数のキューを同時に待つ))
└──────────────────┘
│
↓
┌──────────────────┐
│ タイマタスク │ ← 普通のタスク。優先度 configTIMER_TASK_PRIORITY
│ (デーモンタスク) │
└──────────────────┘
│
├─→ アクティブタイマリスト(期限の昇順ソート)
│
└─→ 期限が来たら pxCallbackFunction() を呼ぶこれが最も重要な事実である.
xTimerStart()はタイマを開始しない。「開始してくれ」というコマンドをキューに送るだけである。実際に開始されるのは、タイマタスクがそのコマンドを取り出したときである。 だから——
- タイマタスクより優先度の高いタスクが走っている間、コマンドは処理されない
- コールバックも、タイマタスクが走るまで実行されない
configTIMER_TASK_PRIORITYの設定が、タイマの応答性を決める。
3. タイマの作り方と使い方
TimerHandle_t xTimer = xTimerCreate(
"MyTimer", /* 名前(デバッグ用) */
pdMS_TO_TICKS( 1000 ), /* 周期 */
pdTRUE, /* pdTRUE = 自動リロード、pdFALSE = ワンショット */
( void * ) 0, /* タイマ ID(ユーザ任意) */
vTimerCallback ); /* コールバック関数 */
xTimerStart( xTimer, 0 ); /* 開始(第 2 引数はコマンドキューの待ち時間) */
void vTimerCallback( TimerHandle_t xTimer )
{
/* ここはタイマタスクのコンテキスト */
uint32_t id = ( uint32_t ) pvTimerGetTimerID( xTimer );
...
}主な API
| API | 動作 | ISR 版 |
|---|---|---|
xTimerStart | 開始(既に動いていればリセット) | xTimerStartFromISR |
xTimerStop | 停止 | xTimerStopFromISR |
xTimerReset | 現時刻から周期を数え直す | xTimerResetFromISR |
xTimerChangePeriod | 周期を変えて開始 | xTimerChangePeriodFromISR |
xTimerDelete | 削除 | なし |
xTimerIsTimerActive | 動いているか(キュー経由でない、即座に返る) | — |
pvTimerGetTimerID | ID を取得 | — |
ソフトウェアタイマの動作を見る
タイマの周期・モード・タイマタスクの優先度を変えて、 コールバックが実際にいつ走るかを確かめられる。
2 つのモード
| モード | 第 3 引数 | 動作 |
|---|---|---|
| 自動リロード | pdTRUE | 周期的に呼ばれ続ける |
| ワンショット | pdFALSE | 1 回だけ呼ばれて停止する |
xTimerReset()はワンショットで真価を発揮する.「最後の入力から 3 秒間何もなかったら実行する」—— デバウンスやアイドル検出の典型パターンである。
/* 入力があるたびに呼ぶ */ void on_input( void ) { xTimerReset( xIdleTimer, 0 ); /* 3 秒を数え直す */ } /* 3 秒間 on_input が呼ばれなければ、これが実行される */ void vIdleTimeout( TimerHandle_t t ) { enter_sleep_mode(); }
4. コールバックの掟
コールバックはタイマタスクのコンテキストで実行される。 つまり「タイマタスクという 1 つのタスクの中で、全タイマのコールバックが順に実行される」。
このことから、2 つの絶対的な掟が導かれる。
掟 1: 絶対にブロックしてはいけない
void vTimerCallback( TimerHandle_t t )
{
vTaskDelay( 100 ); /* ← 絶対にダメ */
xSemaphoreTake( xMutex, portMAX_DELAY ); /* ← 絶対にダメ */
}ブロックするとタイマタスクが止まる。 その間、すべてのタイマのコールバックが実行されず、すべてのコマンドが処理されない。
システム全体のタイマ機能が停止する。これは非常に見つけにくい不具合になる。
掟 2: 短く済ませる
void vTimerCallback( TimerHandle_t t )
{
heavy_computation(); /* ← 他のタイマが全部遅れる */
}タイマタスクは 1 本しかない。長い処理は他のタイマの期限を遅らせる。
正しくは、通知を送って別のタスクに処理させる。
void vTimerCallback( TimerHandle_t t )
{
xTaskNotifyGive( xWorkerTask ); /* 通知するだけ。すぐ戻る */
}タイマタスクのスタックサイズにも注意.
#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE既定値は最小サイズである。コールバックの中で深い関数を呼ぶと溢れる。 実際、
printfを 1 行入れただけで落ちることがある。コールバックに何か書くなら、このサイズを見直すこと(17 章)。
5. xTimerPendFunctionCall() — 遅延実行
タイマ機能のもう 1 つの顔である。「この関数を、タイマタスクに実行してもらう」。
BaseType_t xTimerPendFunctionCallFromISR( PendedFunction_t xFunctionToPend,
void *pvParameter1,
uint32_t ulParameter2,
BaseType_t *pxHigherPriorityTaskWoken );これは ISR の中で長い処理をしたくないときに使う。
void vDeferredHandler( void *pvParam1, uint32_t ulParam2 )
{
/* ここはタイマタスクのコンテキスト。ブロックしなければ何でもできる */
process_packet( (uint8_t*)pvParam1, ulParam2 );
}
void DMA_IRQHandler( void )
{
BaseType_t woken = pdFALSE;
xTimerPendFunctionCallFromISR( vDeferredHandler, dma_buffer, dma_len, &woken );
portYIELD_FROM_ISR( woken );
}これが「遅延割り込み処理 (deferred interrupt processing)」の FreeRTOS 版である.
Linux では下半分 (bottom half) / ワークキューと呼ばれる仕組みに相当する。 「割り込みの中では最小限のことだけして、残りは後で普通のコンテキストでやる」—— これは OS 設計の普遍的なパターンである。
14 章で見た
xEventGroupSetBitsFromISR()も、内部でこれを使っている。
専用タスクとの比較
| 方法 | 利点 | 欠点 |
|---|---|---|
xTimerPendFunctionCall | タスクを追加しなくてよい | タイマタスクを共有するので互いに遅らせ合う |
| 専用のワーカータスク | 優先度を独立に設定できる | タスク 1 つ分の RAM を使う |
頻度が低く軽い処理なら前者、頻度が高いか重い処理なら後者である。
6. 内部実装 — アクティブタイマリスト
タイマも、期限の昇順にソートされたリストで管理される(10 章の遅延リストと同じ発想)。
static List_t xActiveTimerList1;
static List_t xActiveTimerList2;
static List_t *pxCurrentTimerList;
static List_t *pxOverflowTimerList;ティックの折り返し対策も、遅延リストと同じ「リスト 2 本の交換」である。
static void prvProcessTimerOrBlockTask( const TickType_t xNextExpireTime,
BaseType_t xListWasEmpty )
{
vTaskSuspendAll();
{
xTimeNow = prvSampleTimeNow( &xTimerListsWereSwitched );
if( xTimerListsWereSwitched == pdFALSE )
{
if( ( xListWasEmpty == pdFALSE ) && ( xNextExpireTime <= xTimeNow ) ) {
( void ) xTaskResumeAll();
prvProcessExpiredTimer( xNextExpireTime, xTimeNow ); /* 期限到来 */
}
else {
/* 次の期限まで、コマンドキューで待つ */
vQueueWaitForMessageRestricted( xTimerQueue,
( xNextExpireTime - xTimeNow ),
xListWasEmpty );
if( xTaskResumeAll() == pdFALSE ) { portYIELD_WITHIN_API(); }
}
}
else { ( void ) xTaskResumeAll(); }
}
}
vQueueWaitForMessageRestricted()という工夫.タイマタスクは、2 つのことを同時に待つ必要がある。
- 次のタイマの期限が来ること(時間待ち)
- コマンドキューに要求が届くこと(イベント待ち)
この関数は「キューに対して、指定時間だけ待つ。ただし受信はしない」という 特殊な待ち方をする。10 章で見た「2 つのリストへの同時登録」がそのまま効いている。
- 期限が来たら → 遅延リスト経由で起きる
- コマンドが届いたら → イベントリスト経由で起きる
どちらでも起きて、起きてから何が起きたか判断する。 きれいな設計である。
自動リロードの再登録
static void prvProcessExpiredTimer( const TickType_t xNextExpireTime,
const TickType_t xTimeNow )
{
TIMER_t * const pxTimer = listGET_OWNER_OF_HEAD_ENTRY( pxCurrentTimerList );
( void ) uxListRemove( &( pxTimer->xTimerListItem ) );
if( ( pxTimer->ucStatus & tmrSTATUS_IS_AUTORELOAD ) != 0 )
{
/* ★ 期限「基準」で再登録する。実行時刻基準ではない */
if( prvInsertTimerInActiveList( pxTimer,
xNextExpireTime + pxTimer->xTimerPeriodInTicks,
xTimeNow, xNextExpireTime ) != pdFALSE )
{ ... }
}
else {
pxTimer->ucStatus &= ~tmrSTATUS_IS_ACTIVE;
}
pxTimer->pxCallbackFunction( ( TimerHandle_t ) pxTimer ); /* コールバック */
}再登録が「実行時刻 + 周期」ではなく「前回の期限 + 周期」である点に注目.
これは 08 章の
vTaskDelayUntil()と同じ考え方である。 コールバックの実行が遅れても、期限の格子はずれない。期限: 0 ─── 100 ─── 200 ─── 300 ← 常に 100 刻み 実行: 5 108 205 310 ← 遅れるが、期限は動かないだから長期的な累積誤差が生じない。 よくできている。
7. よくある落とし穴
| 落とし穴 | 症状 | 対策 |
|---|---|---|
configTIMER_TASK_PRIORITY が低すぎる | タイマが遅れる・止まる | 高優先度タスクより上に置く |
configTIMER_QUEUE_LENGTH が小さい | xTimerStart が失敗する | 長くする。戻り値を確認する |
| コールバックでブロック | 全タイマが停止 | 通知して別タスクで処理する |
| タイマタスクのスタック不足 | 原因不明のクラッシュ | configTIMER_TASK_STACK_DEPTH を増やす |
xTimerStart の戻り値を見ない | 静かに失敗している | 必ず確認する |
スケジューラ開始前に xTimerStart | コマンドは溜まるが処理されない | 開始後に呼ぶ(09 章のデーモン起動フックが便利) |
if( xTimerStart( xTimer, pdMS_TO_TICKS( 10 ) ) != pdPASS ) {
/* コマンドキューが満杯だった */
log_error( "timer start failed" );
}
configTIMER_TASK_PRIORITYをどこに置くか.悩ましい判断である。
- 高くする → タイマの応答が良い。だがコールバックが他のタスクを止める
- 低くする → 他のタスクを邪魔しない。だがタイマが遅れる
原則としては、高めに置いて、コールバックを極端に軽くするのがよい。 コールバックが軽ければ、高優先度でも実害がない。
典型的には「アプリケーションの最高優先度タスクと同じか、1 段上」に置く。
8. この章のまとめ
| ポイント | 内容 |
|---|---|
| 実体 | タイマタスク(デーモンタスク) という普通のタスク |
| API の正体 | xTimerStart() はコマンドキューに要求を送るだけ |
| 応答性 | configTIMER_TASK_PRIORITY が決める |
| コールバックの掟 | ブロック禁止・短く済ませる。全タイマが道連れになる |
| 重い処理 | 通知を送って別タスクで処理する |
xTimerPendFunctionCall | ISR からの遅延実行。Linux の下半分に相当 |
| 内部構造 | 期限の昇順リスト + 2 本交換によるオーバーフロー対策 |
| 同時待ち | vQueueWaitForMessageRestricted で「時間 or コマンド」を待つ |
| 再登録 | 前回の期限 + 周期。累積誤差が出ない |
| 精度が要るなら | ハードウェアタイマを使うこと |
次章では、これまで何度も出てきた「ヒープ」—— FreeRTOS のメモリ管理を見る。