Simple Task Scheduler
今回は、StellarisWareのSimple Task Schedulerライブラリをみてみます。
Simple Task Schedulerは、utils/scheduler.c で提供されます。ユーザーズガイドによると、
- 一定間隔での関数呼び出しす必要のあるアプリケーションを簡単に実装する方法を提供する
- 呼び出す関数は、タスクテーブルとして、関数ポインタのリストで定義する
- タスクテーブルと、タスク数をアプリケーションがグローバル変数として定義する
- スケジューラは、SysTickへの排他的アクセスが必要
- アプリケーション責で、SysTick割り込みにスケジューラの割り込みハンドラを登録する必要がある
サンプルコードも含まれていましたが、擬似コードのようでそのままでは動きません。
scheduler.c の実装はシンプルで、SysTickを初期化するSchedulerInit()、SysTick割り込みハンドラのSchedulerSysTickIntHandler()、タスクを呼び出すSchedulerRun()、主要な関数はこれくらいです。その他、タスク呼び出しの有効/無効を制御する関数、チックカウントのgetter等があります。
アプリ側からの使い方も簡単で、まず、SchedulerInit()で、秒間のチック数を与えて初期する。次に、ループでSchedulerRun()を呼び続けるのみです。そして前述のとおり、グローバル変数としてg_psSchedulerTableにタスクテーブル、g_ulSchedulerNumTasksにタスク数を定義し、割り込みハンドラSchedulerSysTickIntHandler()の登録も必要です。
タスクテーブルは、関数ポインタ、呼び出し時引数、何チック毎に呼び出すかの頻度、呼び出し有効/無効を設定します。
サンプルを書いてみました。
#include "inc/hw_types.h" #include "inc/hw_memmap.h" #include "driverlib/interrupt.h" #include "driverlib/gpio.h" #include "driverlib/pin_map.h" #include "driverlib/uart.h" #include "driverlib/systick.h" #include "utils/scheduler.h" #include "utils/uartstdio.h" #define TICKS_PER_SECOND 100 static void uartEcho1(void *pvParam); // タスク呼び出し関数1 static void uartEcho2(void *pvParam); // タスク呼び出し関数2 tSchedulerTask g_psSchedulerTable[] = { {uartEcho1, (void *)1, 30, 0, true}, // タスク1 30チック毎 {uartEcho2, (void *)2, 50, 0, true}, // タスク2 50チック毎 {uartEcho2, (void *)3, 100, 0, true}, // タスク3 100チック毎 }; unsigned long g_ulSchedulerNumTasks = // タスク数 (sizeof(g_psSchedulerTable) / sizeof(tSchedulerTask)); static void uartEcho1(void *pvParam) { UARTprintf("[task%u][%u] echo1 called\n", (unsigned long)(pvParam), SchedulerTickCountGet()); } static void uartEcho2(void *pvParam) { UARTprintf("[task%u][%u] echo2 called\n", (unsigned long)(pvParam), SchedulerTickCountGet()); } int main(void) { // SysTick割り込みへのハンドラ登録 SysTickIntRegister(SchedulerSysTickIntHandler); UARTStdioInit(0); // SchedulerをSysTick100cnt/secで初期化、即ちSchedulerのチックは10msec SchedulerInit(TICKS_PER_SECOND); IntMasterEnable(); while(1) { SchedulerRun(); // 以降はScheduler実行し続ける } }
実行結果はシリアルで受けています。
[task1][30] echo1 called [task2][50] echo2 called [task1][60] echo1 called [task1][90] echo1 called [task2][100] echo2 called [task3][100] echo2 called [task1][120] echo1 called [task1][150] echo1 called [task2][150] echo2 called [task1][180] echo1 called [task2][200] echo2 called [task3][200] echo2 called
SchedulerRun()は、main()のコンテキストで、タスクテーブルの関数を順次実行するだけですので、下記のように、タスク3の実行後に700msecのディレイを入れた場合は、もちろん以降の処理が遅延します。
static void uartEcho2(void *pvParam) { UARTprintf("[task%d][%u] echo2 called\n", (int)pvParam, SchedulerTickCountGet()); if ((int)pvParam == 3) SysCtlDelay((SysCtlClockGet() / 3) / 1000 * 700); }
ディレイの影響で、1回めのタスク3の実行以降、タスク1とタスク2は、秒間に、タスク3ディレイ後の1回しか、呼び出されるタイミングがなくなっています。
[task1][30] echo1 called [task2][50] echo2 called [task1][60] echo1 called [task1][90] echo1 called [task2][100] echo2 called [task3][100] echo2 called [task1][170] echo1 called [task2][170] echo2 called [task3][200] echo2 called [task1][270] echo1 called [task2][270] echo2 called
ということで、あくまでも"Simple"。厳密な時間管理は、タイマ割り込みや、コンテキストスイッチを使わないといけません。