Seeeduino XIAO(ARM Cortex-M0+)でマルチタスク:FreeRTOSを使って動作確認/おさらい

Arduino/SAMDCortex-M0+,FreeRTOS,SAMD21,Seeeduino,XIAO,チャタリング,マルチタスク,割り込み

先日XIAOでハロワを動かしてみましたが、その際にvTaskDelayを書いたらエラーになってしまいまして

ESP32では何にも考えずに使えた訳ですが、、

delayを入れると全体が停止してしまいますので、マイコンにいくつかの機能を持たせる場合にはRTOSは必須と言ってもいいと思います。

てな訳で、Seeeduino XIAOでもRTOSを一通り使える様にしておこうと思います。

利用するのはオフィシャルSeeed-StudioのFreeRTOSライブラリ。

GitHub: Seeed-Studio / Seeed_Arduino_FreeRTOS

他にもRTOSはいくつかある様ですが、、

Arduinoな脳の感覚では敷居が高く、きっと躓くに違いない。

私の様なnewbieにはとりあえずFreeRTOSが良いと思います。

さてライブラリのページを見ていきなり使おうとしても意味判らないかもしれませんが、、、

ちゃんとチュートリアルが用意されておりました。

Arduino FreeRTOS Tutorial – How to use FreeRTOS to Multi-tasking in Arduino

というわけで、基本中の基本、Lチカで動かしてみて感じを掴もうと思います。

チュートリアルの方が判り易いかもしれませんが・・・

シングルタスクだと何が不便なのか、どう困るのか

さて、いきなりシングルタスクだのマルチタスクだのと言われてもピンと来ないかもしれませんので、何故わざわざFreeRTOSを使うのかという辺りを。

例えば1秒毎にLチカをするプログラムを書いたとします。

簡単に、LEDに繋いだGPIOをHIGHにして、1秒まってGPIOをLOWにする

こんな感じの動作をさせたとします。

void loop()
{
    digitalWrite(LED_PIN_A, HIGH);  // LED_A 点灯
    digitalWrite(LED_PIN_B, HIGH);  // LED_B 点灯

    delay(1000);    // 1秒待つ

    digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
    digitalWrite(LED_PIN_B, LOW); // LED_B 消灯

    delay(1000);    // 1秒待つ
}

凄くシンプルですが、とりあえずこれで1秒毎に2つのLEDは点灯と消灯を繰り返すと思います。

しかしこの1秒(1000ms)待つディレイの間、マイコンは他の仕事を全くしません。

さてここで、LED_Aは1秒毎に、LED_Bは2秒毎に点灯させたい場合はどうしましょう。

1秒と2秒なら、シングルタスクでも出来ない事はありませんが、、、

int led_count = 0;

void loop()
{
    // led_countインクリメント
    led_count++;

    digitalWrite(LED_PIN_A, HIGH);  // LED_A 点灯
    if(led_count > 1) {
        digitalWrite(LED_PIN_B, HIGH);  // LED_B 点灯
    }

    delay(1000);    // 1秒待つ

    digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
    if(led_count > 1) {
        digitalWrite(LED_PIN_B, LOW); // LED_B 消灯
        led_count = 0;
    }

    delay(1000);    // 1秒待つ
}

フラグでも持たせて、2回目の時にだけ実行、カウンタリセット なんて泥臭い事で動くには動きますけども、、

こんなのを積み重ね増やしていけば、正直ワケわからんくなります。

LED_Bは0秒~1秒の間のランダムで点灯する、みたいな事をしたい時、シングルタスクではちょっと直ぐにロジックが浮かんできません。

簡単に済ますには、各LEDが裏で個別に勝手に動いていて欲しいわけです。

FreeRTOSを使ってマルチタスクで実行する

さてマルチタスクと言っても処理を物凄く細切れにして交互に動かしているだけですので、厳密には同時実行では無いのですが、、

片方の処理の終了までもう片方が動けないという事態にはならず、あたかも同時に実行しているかの様に動いてくれます。

マルチタスクでプログラムを動かしたい時は、以下の様な感じで書いていくと良いと思います。

・マルチタスクで動かしたい分だけ、タスクハンドラを用意
・RTOSで実行する関数をFreeRTOS的に作成
・必要であれば割り込み処理等を利用し、FreeRTOSのタスクを操作させる関数を作成
・タスクを登録・実行

ではまずは一個目から

ライブラリの読み込みと、マルチタスクで動かしたい分だけタスクハンドラを用意

2個のLEDをマルチタスクで点灯させるので、2個のタスクハンドラを用意します。

// Seeed FreeRTOSライブラリ
#include <Seeed_Arduino_FreeRTOS.h>

TaskHandle_t pLed_A;    //  タスクハンドラ構造体のポインタLED_A
TaskHandle_t pLed_B;    //  ポインタLED_A

ハンドラをグローバルに用意しておく事で、この処理を中断させたり別の動作をさせたりし易くなります。

どこかの処理の中で作ってガベージされたりスコープ外で行方不明になったりするのも防げますので、放りっぱなしの単純なLチカであっても私は問答無用でグローバル。

このサイズのマイコンならカプセル化に拘らなくてもワケわからんくなったりしないでしょうし。

そもそも仕事じゃ無いですし(ぇ

RTOSで実行する関数をFreeRTOS的に作成

// 0.5秒毎に点灯を繰り返すLED_A
static void Task_Led_A(void *pvParameters)
{
    while (1)
    {
        digitalWrite(LED_PIN_A, HIGH); // LED_A 点灯
        delay(500);                // 0.5秒待つ

        digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
        delay(500);               // 0.5秒待つ
    }
    // ループを抜ける事は無いが、あった場合にはタスクを削除しておく
    vTaskDelete(NULL);
}

// n秒毎にランダムに点灯を繰り返すLED_B
static void Task_Led_B(void *pvParameters)
{
    while (1)
    {
        digitalWrite(LED_PIN_B, HIGH); // LED_B 点灯
        delay(random(1000));           // 0~999ms のランダムミリ秒待つ

        digitalWrite(LED_PIN_B, LOW); // LED_B 消灯
        delay(random(1000));          // 0~999ms のランダムミリ秒待つ
    }

    // ループを抜ける事は無いが、あった場合にはタスクを削除しておく
    vTaskDelete(NULL);
}

裏でずっと動かしておくタスクですので、無限ループで作成します。

一回きりのタスクや、ある条件を満たしたら終了させる場合には、タスクを終了させておくと良いです。

その場合には終了させるタイミングでvTaskDeleteを。

NULLを渡す事で自爆します。

ちなみに終了させないで放置すると何か動作がおかしくなる様で・・・

ESP32だと再起動を繰り返してたっけ・・・( ノД`)

また、前述のタスクハンドラがグローバルにあるので、外部からvTaskDeleteをかけられます。

void vTaskDelete(TaskHandle_t xTaskToDelete)
例:  vTaskDelete(pLed_B);

必要であれば割り込み処理等を利用し、FreeRTOSのタスクを操作させる関数を作成

少し調べたところ、ArduinoのattachInterruptがそのまま動作する様でしたので、簡単にボタンを押したらLEDの点滅を一時停止させる処理を書いてみます。

// チャタリング防止の為のフラグ
volatile bool untiChattering = false;

// ボタンが押された時の割り込み処理
void SuspendLed()
{

    // チャタリングフラグがfalseの時のみ実行
    if (!untiChattering)
    {
        // チャタリング防止の為、チャタリングフラグを上げる
        untiChattering = true;

        // LED_Aの点滅を一時停止/停止解除する
        // 実行状態を確認 返り値はenumの為そのまま比較
        if (eTaskGetState(pLed_A) == eSuspended)
        {
            // 停止中だったので再開する
            vTaskResume(pLed_A);
            digitalWrite(LED_SUSPEND, LOW); // LED_SUSPEND 消灯
        }
        else
        {
            // 停止中以外だったので停止する
            vTaskSuspend(pLed_A);
            digitalWrite(LED_SUSPEND, HIGH); // LED_SUSPEND 点灯
        }

        // 200msほどボタン動作無効化する
        //  ISRの中ではdelay vTaskDelay millis等が動作しない為、影響を与えず動作するdelayMicrosecondsを使う
        for (int i = 0; i < 200; i++)
        {
            // 10万us=100msなんですが、、桁が多過ぎて目盛りを喰いそうなのでループで減らす
            // delayMicroseconds(100000);
            delayMicroseconds(1000);
        }

        // チャタリングフラグを下げる
        untiChattering = false;
    }
}
// setupの中に記載
void setup() {
    :
    :

    // SW_PINピンのプルアップがLOWレベルに落とされた時に割り込みイベント発生、SuspendLedを呼び出す
    attachInterrupt(SW_PIN, SuspendLed, FALLING);

    :
    :
}

チャタリングによる超連打で割り込みが発生しない様に、untiChatteringフラグがfalseの時にしか動作しない様にし、中でもディレイをかけてからフラグを戻す様にしています。

このくらいやればチャタリングは殆ど出ないと思います。

尚、割り込みハンドラの中での処理では通常のディレイやタイマーの類が動作しませんので、FreeRTOSの外で動くdelayMicrosecondsをわざわざ使っています。

ISRの中で長時間停止すると何やら動作がおかしくなる様ですので、長時間停止しない様に設計して下さい。

ESP32だとwatchdogタイマーにやられます・・・・

タスクを登録・実行

setupの中でタスクを登録し、vTaskStartSchedulerで実行します。

void setup() {
    :
    :

    // LED_Aのタスクの登録
    xTaskCreate(
            Task_Led_A,     // タスクの関数名
            "Task LED_A"    // 名前 テキトーに
            256             // 割り当てるメモリサイズ bytes
            NULL            // タスクの関数に渡すパラメータへのポインタ 特にパラメータを受け取る処理は書いてないのでNULLで。
            tskIDLE_PRIORITY + 1    // プライオリティ 0-20 アイドリングよりも3ほど優先させる
            &pLed_A         // タスクハンドラを渡して関連付ける
        );

    // LED_Bのタスクの登録
    xTaskCreate(
            Task_Led_B,
            "Task LED_B",
            256,
            NULL,
            tskIDLE_PRIORITY + 1,
            &pLed_B
        );

    // タスク実行開始
    vTaskStartScheduler();

    :
    :
}

void loop() {
    // メインループは何もしない ずっと動いていて欲しい・・・
}

長いので畳んでおきますが、全部まとめるとこんな感じに。