ESP32のFreeRTOS入門 その2 タスクの作成

概要

前回は概要紹介で終わりましたが、今回はプログラムの実行から、タスクの作成まで説明したいと思います。

Arduinoプログラムの構造

Arduinoのスケッチ

void setup() {
}

void loop() {
}

上記のような何も処理しないプログラムで考えてみます。上記を実行するための裏では別のプログラムがあります。

esp32/main.cpp

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_task_wdt.h"
#include "Arduino.h"

TaskHandle_t loopTaskHandle = NULL;

#if CONFIG_AUTOSTART_ARDUINO

bool loopTaskWDTEnabled;

void loopTask(void *pvParameters)
{
    setup();
    for(;;) {
        if(loopTaskWDTEnabled){
            esp_task_wdt_reset();
        }
        loop();
    }
}

extern "C" void app_main()
{
    loopTaskWDTEnabled = false;
    initArduino();
    xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, &loopTaskHandle, CONFIG_ARDUINO_RUNNING_CORE);
}

#endif

上記がArduinoの最初に実行されるコードです。loopTask()関数と、app_main()関数があります。最初に構造だけかんたんに説明してから、後ほど詳しく解説を行います。

app_main()関数

まず、一般的なC言語ではmain()関数から実行されますが、ESP-IDFでは初期化処理が行われたあとに、app_main()関数が呼び出されます。

app_main()関数では、loopTaskWDTEnabled変数を設定しています。これはWDT(WatchDog Timer)の略で、ウォッチドッグタイマーに関連するフラグです。詳細は後ほど解説します。

次に、initArduino()関数を呼び出しています。

上記に定義がありますが、Arduino系の初期化を行っていました。OTAまわりの動作や、PSRAMなどが搭載されている場合には、こちらで初期化されます。

次にxTaskCreateUniversal()関数で、loopTaskというタスクを作成しています。この関数についてもあとで解説したいと思います。

loopTask()関数

この関数が実際の処理をする関数になります。最初にsetup()関数を呼び出しています。この関数はArduinoのスケッチにあるsetup()関数です。

その後にfor文の無限ループがあり、loopTaskWDTEnabledがtrueの場合にesp_task_wdt_reset()を呼び出しています。ここの処理は後ほど解説しますが、ウォッチドッグタイマのリセットをしています。

次にloop()関数を呼び出しています。この関数はArduinoのスケッチにあるloop()関数ですね。この関数はfor文の無限ループの中にあるので、何度も呼び出されます。

FreeRTOSの命名規則

		#define xTaskGenericCreate				MPU_xTaskGenericCreate
		#define vTaskAllocateMPURegions			MPU_vTaskAllocateMPURegions
		#define vTaskDelete						MPU_vTaskDelete
		#define vTaskDelayUntil					MPU_vTaskDelayUntil
		#define vTaskDelay						MPU_vTaskDelay
		#define uxTaskPriorityGet				MPU_uxTaskPriorityGet
		#define vTaskPrioritySet				MPU_vTaskPrioritySet
		#define eTaskGetState					MPU_eTaskGetState
		#define vTaskSuspend					MPU_vTaskSuspend
		#define vTaskResume						MPU_vTaskResume
		#define vTaskSuspendAll					MPU_vTaskSuspendAll
		#define xTaskResumeAll					MPU_xTaskResumeAll
		#define xTaskGetTickCount				MPU_xTaskGetTickCount
		#define uxTaskGetNumberOfTasks			MPU_uxTaskGetNumberOfTasks
		#define vTaskList						MPU_vTaskList
		#define vTaskGetRunTimeStats			MPU_vTaskGetRunTimeStats
		#define vTaskSetApplicationTaskTag		MPU_vTaskSetApplicationTaskTag
		#define xTaskGetApplicationTaskTag		MPU_xTaskGetApplicationTaskTag
		#define xTaskCallApplicationTaskHook	MPU_xTaskCallApplicationTaskHook
		#define uxTaskGetStackHighWaterMark		MPU_uxTaskGetStackHighWaterMark
		#define xTaskGetCurrentTaskHandle		MPU_xTaskGetCurrentTaskHandle
		#define xTaskGetSchedulerState			MPU_xTaskGetSchedulerState
		#define xTaskGetIdleTaskHandle			MPU_xTaskGetIdleTaskHandle
		#define uxTaskGetSystemState			MPU_uxTaskGetSystemState

上記は一部ですが、xとかvなどの小文字ではじまっているものは、FreeRTOSの命名規則の関数になります。

タスク作成

xTaskCreateUniversal()関数でタスクの作成を行っていました。xからはじまっているので、FreeRTOSの関数になります。(厳密にはESP32で拡張されたFreeRTOSの関数だと思います)

ESP32には3つのタスク作成関数があります。

  • xTaskCreate() – シングルコア向けタスク作成
  • xTaskCreatePinnedToCore() – コア指定タスク作成
  • xTaskCreateUniversal() – 万能ラッピングタスク作成

どう使い分けるかというと、xTaskCreateUniversal()だけ使えば大丈夫です。最初はシングルコア向けのxTaskCreate()だけしかなくデュアルコアで使えず、その後にコア指定ができて、最後に万能版ができています。

xTaskCreateUniversal()は変なコアを指定しても、環境に応じて正しいコアを指定しなおしてくれます。シングルコアのESP32-SOLO-1の場合はCore0以外を指定してもCore0として作成します。デュアルコアのESP32の場合にはCore0とCore1以外を指定した場合でもCore0で作成してくれるラッピング関数です。

BaseType_t xTaskCreateUniversal( TaskFunction_t pxTaskCode,
                        const char * const pcName,
                        const uint32_t usStackDepth,
                        void * const pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t * const pxCreatedTask,
                        const BaseType_t xCoreID ){
#ifndef CONFIG_FREERTOS_UNICORE
    if(xCoreID >= 0 && xCoreID < 2) {
        return xTaskCreatePinnedToCore(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask, xCoreID);
    } else {
#endif
    return xTaskCreate(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask);
#ifndef CONFIG_FREERTOS_UNICORE
    }
#endif
}

上記のようなコードになっています。シングルコアの場合には常にxTaskCreate()関数が呼ばれています。

xTaskCreateUniversal()関数は比較的最近できたため、他のサイトやスケッチ例などを見ると、xTaskCreatePinnedToCore()関数を利用している例が多いですが、基本的にはxTaskCreateUniversal()関数を利用してください。

xTaskCreateUniversal()関数

引数

引数型引数名備考
TaskFunction_tpxTaskCode作成するタスク関数
const char * constpcName表示用タスク名
const uint32_tusStackDepthスタックメモリ量
void * constpvParameters起動パラメータ
UBaseType_tuxPriority優先度
TaskHandle_t * constpxCreatedTaskタスクハンドル
const BaseType_txCoreID実行するコア

pxTaskCode : 作成するタスク関数

作成されて実行されるタスクの関数を指定します。ここはわかりやすいと思います。

pcName : 表示用タスク名

ここがわかりにくいのですが、タスク名はあまり意味がありません。タスク一覧を取得した場合に表示される文字列です。同じタスク名のタスクが複数あっても問題ありません。

usStackDepth : スタックメモリ量

ここのパラメータが一番むずかしいです。タスクに割り当てるスタックメモリの大きさになります。少なすぎるとエラーでプログラムが止まります。多すぎるとメモリが無駄になります。

基本的には少し多めに割り当てるのが安全だと思いますが、どれぐらい割り当てればいいのかの目安がありません。loopTaskの設定値は8192ですので、この数値を基準として、足りなくなったら数値を増やすようにしてください。

pvParameters : 起動パラメータ

作成したタスクに渡される起動パラメータです。あまり使うことはありませんのでNULLが指定されることが多いです。複数のGPIOを対象にして、複数のタスクを起動する場合などに、タスク関数自体は共通ですが、起動パラメータで対応するGPIOを指定するなどが可能です。

uxPriority : 優先度

優先度も指定が難しいパラメータになります。優先度は0が一番低く、configMAX_PRIORITIES – 1が一番高くなります。

void setup() {
    Serial.begin(115200);
    delay(50);
    Serial.println(configMAX_PRIORITIES);
}

void loop() {
}

上記で試したところ、25となりましたので、0から24までが指定できます。

/* This has impact on speed of search for highest priority */
#ifdef SMALL_TEST
#define configMAX_PRIORITIES			( 7 )
#else
#define configMAX_PRIORITIES			( 25 )
#endif

宣言部を見たところ、7と25の環境があるみたいですが、普通に使っている分にはおそらく25だと思います。

基本的には自由な値にしてもらって構いません。ArduinoのloopTaskのように標準的なタスクは1にして、それより低い優先度のタスクは0に、優先度が高いものは2でも構いません。

ただし、ESP32のライブラリの中で作成されるタスクで優先度が高いものは23で作られています。他のどんな処理よりも優先されるべきタスクは最高の優先度である24で作る必要があります。

優先度についてはマルチタスクの説明の際に、再度詳しく解説をする予定です。

pxCreatedTask : タスクハンドル

作成したタスクを管理するハンドルです。この変数を利用してタスクを停止したり、優先度を変更したりすることができます。

xCoreID : 実行するコア

コア番号定義
0PRO_CPU_NUM
1APP_CPU_NUM
シングルコア=0
デュアルコア=1
CONFIG_ARDUINO_RUNNING_CORE

こちらも詳細はマルチタスクで説明しますが、実行するコアを指定します。特徴的なのがCONFIG_ARDUINO_RUNNING_COREという定義があり、基本的にはこれか、数値の0が指定されている場合が多いです。

CONFIG_ARDUINO_RUNNING_COREは通常APP_CPU_NUMの1になっています。シングルコアの特殊なESP32の場合には0です。

xTaskCreateUniversal()は無効なコアを指定しても、いい感じに補正してくれるので、アプリと同じコアはAPP_CPU_NUM、バックグラウンドで動かすのはPRO_CPU_NUMという指定でも問題ないと思います。

無線系の処理がPRO_CPU_NUMで動いていますので、無線を利用している場合にはPRO_CPU_NUMであまりタスクを実行しないほうが好ましいです。

逆に無線を利用していない場合には、PRO_CPU_NUMのコアはあまり利用されていないので、積極的に使ったほうがいいと思います。

戻り値 – BaseType_t

状態
成功pdPASS
失敗pdPASS以外

この戻り値のBaseType_t型はわかりにくいのですが、中身をみると単なるint型ですので型名には意味がありません。ちなみにportという接頭語もFreeRTOSの命名規則になります。

#define portBASE_TYPE	int

typedef portSTACK_TYPE			StackType_t;
typedef portBASE_TYPE			BaseType_t;
typedef unsigned portBASE_TYPE	UBaseType_t;

さて成功するとpdPASSが返却され、それ以外の場合にはエラーとなっています。

#define pdFALSE			( ( BaseType_t ) 0 )
#define pdTRUE			( ( BaseType_t ) 1 )

#define pdPASS			( pdTRUE )
#define pdFAIL			( pdFALSE )
#define errQUEUE_EMPTY	( ( BaseType_t ) 0 )
#define errQUEUE_FULL	( ( BaseType_t ) 0 )

/* Error definitions. */
#define errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY	( -1 )
#define errQUEUE_BLOCKED						( -4 )
#define errQUEUE_YIELD							( -5 )

こんな感じの定義になっていました。タスク作成の戻り値を見ているスケッチはあまり存在していませんが、本当は確認したほうがいいと思います。

基本的にはパラメーターがおかしいか、スタックメモリ量が大きすぎてメモリ割り当てができなかった場合になると思います。

loopTask作成を見直してみる

xTaskCreateUniversal(
    loopTask,                    // 作成するタスク関数
    "loopTask",                  // 表示用タスク名
    8192,                        // スタックメモリ量
    NULL,                        // 起動パラメータ
    1,                           // 優先度
    &loopTaskHandle,             // タスクハンドル
    CONFIG_ARDUINO_RUNNING_CORE  // 実行するコア
    );

コメントを追加していますが、上記のパラメータで作成しています。この値を基本的な設定値とし、loopTaskより大きいのか小さいのかで、調整をするとよいと思います。

上記で重要なのは、実行するコアがCONFIG_ARDUINO_RUNNING_COREが選択されていることです。つまり、setup()関数と、loop()関数はAPP_CPUであるCore1で実行されています。

タスクは常にどちらのコアで実行されているのかを意識しながら使ってください。

資料

日本語リファレンス

まとめ

タスクの作成までですが、なんとか終わりました。タスク周りはいろいろハマりどころがあるので、複数回にわけて解説をしていきたいと思います。

続編

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。