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"。厳密な時間管理は、タイマ割り込みや、コンテキストスイッチを使わないといけません。