ESP32 RTOSでタスクをつくる (Arduino)

ESP32 RTOSでタスクをつくる (Arduino)

今回からしばらく Arduino のフレームワークで RTOS を使ってみます。

この記事は JTAG でデバッグすることを前提にして書いています。
環境構築については こちら をご覧になってください。

投稿時の開発環境を記しておきます。

PC:
Windows10 OS

開発ボード :
ESP32-DevKitCーVE
(Soc : ESP32-D0WD-V3)

デバッガー(H/W):
FT2232D

デバッガー (S/W) :
Visual Studio Code + PlatformIO + Arduino Framework

FreeRTOS

こちら が本家ですが、ESP32用としては このへん を見るのが良さそうです。

プロジェクトをつくる

VSCodeで、もし使っていたプロジェクトを開いていたら、File – Close Folder して閉じておきます。

その後にVSCodeからPlatformIOをOpenします。

以下の内容でプロジェクトを新規に作成します。

Name : ESP32A-RTOS1
Board : Espressif ESP32 Dev Module
Framework : Arduino Framework

Name : ESP32A の “A” は Framework (Arduino Framework)の頭文字を示しています。

(後から見てわかるように、Arduinoを使うことを明示しています)

platformio.ini に以下の4行を追加して、 Ctrl + s で保存しておきます。

COM[8]の8の部分はデバイスマネージャーのポート(COMとLPT)で Silicon Labs CP210x から始まるCOMの番号を記述します。

upload_port はプログラムをアップロードする時に使うCOMポートを指定します。
monitor_port は printf()出力をモニターするCOMポートを指定します。

debug_tool = minimodule
upload_port = COM[8]
monitor_port = COM[8]
monitor_speed = 115200

タスク(スレッド)をつくる

RTOSを使うということはタスクをいくつか作り、マルチタスクで動作させることになります。
私はタスクとスレッドを同じ意味で捉えています。
私自身はスレッドの方が好きな表現ですが、API関数にはTaskが頻繁に出てくるのでタスクという表現を使うことにします。

SMP

ESP32は SMPSoc です。
SMPとは Symmetric Multiprocessing の略で、日本語では対称型マルチプロセッシングです。

SMPは、2つ以上の同一のCPU(コア)が単一の共有メインメモリに接続され、単一のオペレーティングシステムによって制御されるコンピューティングアーキテクチャです。

詳しくは SMP をご覧ください。

ESP32は2つのコアが動作します。
コアが2つあるのでタスクを作るには、それ用のAPIを使う必要があります。

通常、ワイヤレスネットワーク(WiFiやBluetoothなど)の処理を担当するタスクはCPU0(したがってPRO_CPUという名前)に固定されて、アプリケーションの残りの部分を処理するタスクはCPU1(したがってAPP_CPUという名前)に固定されます。

xTaskCreatePinnedToCore() API

タスクの作成には xTaskCreatePinnedToCore() を使います。

タスクのAPIについての詳細は Task API をご覧になってください。

コーディングする

まずコア0で動作するタスクを2つつくってみます。
xPortGetCoreID()で自タスクのコア番号が分かるそうなので使ってみました。

xTaskCreatePinnedToCore()の7番目(最後)の引数に PRO_CPU_NUM(==コア0) を指定します。

#include <Arduino.h>

TaskHandle_t taskHandle[4];

void Task0a(void *args) {
  while (true) {
    printf("Task0a, my core = %d\r\n", xPortGetCoreID());
  }
}

void Task0b(void *args) {
  while (true) {
    printf("Task0b, my core = %d\r\n", xPortGetCoreID());
  }
}

void setup() {
  xTaskCreatePinnedToCore(Task0a, "Task0a", 4096, NULL, 10, &taskHandle[0], PRO_CPU_NUM);
  xTaskCreatePinnedToCore(Task0b, "Task0b", 4096, NULL, 10, &taskHandle[1], PRO_CPU_NUM);
}

void loop() {
}

ビルドして実行する

メニューから Run – Start Debugging (またはF5キー)すると少ししてから

loopTaskWDTEnabled = false; の行で停止します。

もう一度 F5キーを押すとプログラムを実行します。

その後アクティビティバーのPlatformIOのアイコンを選択してツリー表示させて Monitor を選択します。

するとTERMINAL に実行結果が表示されます。

Task0a, my core = 0
Task0b, my core = 0
Task0b, my core = 0
Task0a, my core = 0
Task0b, my core = 0
Task0a, my core = 0
Task0b, my core = 0
Task0a, my core = 0
Task0b, my core = 0

xTaskCreatePinnedToCore()の5番目の引数が優先順位です。
優先順位を同じにしているのでタイムスライスによるタスク切り替えが起こります。

コア1で動かしてみる

次にソースコードを以下のように編集します。
xTaskCreatePinnedToCore()の7番目(最後)の引数に APP_CPU_NUM(==コア1) を指定します。

最後の引数の指定は、PRO_CPU_NUM、APP_CPU_NUMの他に tskNO_AFFINITY を指定することもできます。

詳しい説明は省きますが tskNO_AFFINITY を指定するとコアが固定されずに動くそうです。

void Task1a(void *args) {
  while (true) {
    printf("Task1a, my core = %d\r\n", xPortGetCoreID());
  }
}

void Task1b(void *args) {
  while (true) {
    printf("Task1b, my core = %d\r\n", xPortGetCoreID());
  }
}

void setup() {
  xTaskCreatePinnedToCore(Task1a, "Task1a", 4096, NULL, 10, &taskHandle[0], APP_CPU_NUM);
  xTaskCreatePinnedToCore(Task1b, "Task1b", 4096, NULL, 10, &taskHandle[1], APP_CPU_NUM);
}
Task1a, my core = 1
Task1a, my core = 1
Task1a, my core = 1
Task1a, my core = 1
Task1a, my core = 1
Task1a, my core = 1
Task1a, my core = 1
Task1a, my core = 1

なんとコア1でタスクをつくるとタイムスライスによるタスク切り替えが起こりませんでした。

ESP32では無線を使うケースが多いと思います。

そうすると無線側のコアに配慮してコア1側でタスクをつくることが多くなると思います。
その際には気をつけておきたい挙動です。

時間待ちする

自タスク内でタスクを切り替えるには以下の方法をとります。

・ディレイ
・イベント
 割り込みなどの特定の事象が発生するまでブロックする
・リソース取得
 キュー、セマフォ、ミューテックスが使えるようになるまでブロックする

今回はディレイを使ってタスクを切り替えてみます。

void Task1a(void *args) {
  while (true) {
    printf("Task1a, my core = %d\r\n", xPortGetCoreID());
    vTaskDelay(100);
  }
}

void Task1b(void *args) {
  while (true) {
    printf("Task1b, my core = %d\r\n", xPortGetCoreID());
    vTaskDelay(100);
  }
}

vTaskDelayは引数で指定した時間(ミリ秒)ブロックし、タスク切り替えが起こります。

delay()関数

delay()関数はタスク切り替えが起こらないと思って以下のコードで試してみました。

void Task1a(void *args) {
  while (true) {
    printf("Task1a, my core = %d\r\n", xPortGetCoreID());
    delay(100);
  }
}

void Task1b(void *args) {
  while (true) {
    printf("Task1b, my core = %d\r\n", xPortGetCoreID());
    delay(100);
  }
}

なんとdelay()でもタスク切り替えが起こってしまいます。
SNSで有識者の方々に教えて頂いたのですが、delay()の正体は以下の通りです。

void delay(uint32_t ms)
{
    vTaskDelay(ms / portTICK_PERIOD_MS);
}

す、すげぇ。。やるなぁArduino。

ではタスク切り替えしたくない場合には時間待ちの処理をどのように記述しましょうか。

delayMicroseconds()を使う方法があります。

マイクロセカンドなので、ミリ秒単位を指定する場合 1000倍した値を引数に渡します。

(delay()とdelayMicroseconds()でタスク切り替えの挙動が異なるのはどうかと思いますけれど)

私はDual Coreのマイコンを使うのはESP32が初めてなので、いくつかの新しい発見がありました。
皆さまは、いかがでしたか。

RTOSカテゴリの最新記事