SNSへはこちら

ESP8266でベアメタル(12) - 逐次タスク機能(その2)

タスク機能を実際に NonOS SDK で使う方法を解説します。

タスク登録のための準備

タスク1つにつき以下をすべてのタスクに行ってください。

タスク用の変数等を準備

タスク機能を使う時に SDK および呼ばれたユーザー関数側で使う変数を準備します。API リファレンス上では確か「キュー」と言われています。連続的に配列としてメモリを確保して使います。

まず、ポインタを宣言。多分どこでも良いとは思いますが、取り敢えずグローバル変数として於いたほうが良さげ。

os_event_t *queue;

タスク実行用の関数も用意しておきましょう。取り敢えず空で。

static void task(os_event_t *e) {

}

初期化用の記述

その上で、以下を user_init 関数内に記述します。#include <stdlib.h> をお忘れなく。

queue = (os_event_t *)malloc(sizeof(os_event_t) * 4);
system_os_task(task, USER_TASK_PRIO_0, queue, 4);

この解説です。まず malloc は領域確保なのでわかりますよね。取り敢えず4つ確保しておきました。
2行目では引数として、タスク実行用関数のポインタ(型は void (*)(os_event_t *))、優先順位、キューへのポインタ、キューの数を記述します。

この内優先順位ですが、マクロとして USER_TASK_PRIO_0, USER_TASK_PRIO_1, USER_TASK_PRIO_2 が用意されていて、最大3つまでタスクを登録できます。0〜2の各優先順位に、タスクを割り振るというイメージです。なので 優先順位0のタスクを複数登録するということは出来ません。
なお、数字が大きいほど優先順位は上なようです。

以上で準備完了です。

タスク実行のリクエスト

実際にリクエストを送る処理は、user_init 関数を抜けた後に呼んだほうが良いでしょう。例えば定期的に実行したければタイマーのコールバック関数内で呼んでも良いかも。僕は OS タイマーで呼んでいます。

例えばこうやって user_init 内に記述することで OS タイマーをセットして...

    os_timer_disarm(&os_timer);
    os_timer_setfn(&os_timer, (os_timer_func_t *)os_timer_cb, NULL);
    os_timer_arm(&os_timer, 1000, 1); // 1000msec

以下のように呼ぶことで、OS タイマーのコールバック関数が実行されるごとにリクエストを送れます。

static void os_timer_cb(void *arg) {
    system_os_post(USER_TASK_PRIO_0, 0, 'x');
}

上の例では、先程登録した優先順位0のタスクを実行せよ、と言っています。当然ここではタスクを実行をリクエストする(正確には先程用意したキューにデータを追加する)だけですので、OS タイマーのコールバック関数はすぐに終了します。身軽ですね。

この system_os_post ですが、以下のようなプロトタイプ宣言になっています。

bool system_os_post (
uint8 prio, 
os_signal_t sig, 
os_param_t par
);

uint8 prio は優先順位です。残りに関してですが、os_signal_t, os_param_t に関して、これは uint32_t と同等です。ですのであまり身構えないでくださいね。
これらは呼び出されたタスク関数内で、引数としてユーザーが自由に利用できます

実践編

では実際にLチカをするタスクを作ってみます。これだけだとタスク機能の利便性が伝わらないかもですが、とにかく動かしてみます。

コードはこんな感じ。

static os_timer_t os_timer;
static void os_timer_cb(void *arg) {
    system_os_post(USER_TASK_PRIO_0, 0, 'x');
    os_printf("\r\nTask Registered!\r\n");
}

os_event_t *queue;
static void task(os_event_t *e) {
    switch(e->sig) {
        case SIG_OFF:
            gpio_output_set(0, BIT4, 0, 0);
            os_printf("SIG_OFF...");
            break;
        case SIG_ON:
            gpio_output_set(BIT4, 0, 0, 0);
            os_printf("SIG_ON...");
            break;
        default:
            break;
    }
    os_printf("Wait for 200msec...");
    os_delay_us(1000 * 200); // wait for 200msec
    os_printf("\r\nTask Done!\r\n");
}

void user_init(void) {
    uart_init(BIT_RATE_115200, BIT_RATE_115200); // For debug output
    os_printf("\r\n");

    queue = (os_event_t *)malloc(sizeof(os_event_t) * 4);
    system_os_task(task, USER_TASK_PRIO_0, queue, 4);

    gpio_output_set(0, 0, BIT4, 0);
    os_timer_disarm(&os_timer);
    os_timer_setfn(&os_timer, (os_timer_func_t *)os_timer_cb, NULL);
    os_timer_arm(&os_timer, 1000, 1); // 1000msec
}

これを書き込んで実行すると、
LED ON→200msec 待ち→LED OFF→200msec 待ち→残りの600msec待ち
と言う処理がなされます。シリアル通信データは以下のようです。

...
Task Registered!
SIG_ON...Waiting for 200msec...
Task Done!
SIG_OFF...Waiting for 200msec...
Task Done!

Task Registered!
SIG_ON...Waiting for 200msec...
Task Done!
SIG_OFF...Waiting for 200msec...
Task Done!

...

ある一定間隔の間に処理を行わせたいだとか、重い処理なので割り込みコールバック関数内に記述したくないとか、そういう場面で上手く使えそうな機能です。
比較低レイヤの SDK ですので、登録タスク数が少ないのが若干残念かなあと思いました。まあ沢山のタスクを並列で回したいとかの場合は RTOS SDK を使えってことなんですかね。いずれにしろ、今回は逐次処理でしたがタスク機能を利用することが出来ました。