Arduino – ESP32 のマルチタスク ( Dual Core ) を試す

ESP32 ( ESP-WROOM-32 )

マルチタスク ( デュアルコア )スケッチを作る

では、実際にマルチタスク ( デュアルコア )のプログラムを作ってみたいと思います。

ここでは、三角関数のサイン波の値をArduino IDE のシリアルモニターに出力するプログラムを組んでみます。

なぜ、三角関数のサイン波にしたかというと、これを Arduino IDE のシリアルプロッタに出力すれば、滑らかな曲線が連続しているので、途中でプログラムに割込みが入れば、マルチタスクで動作していないことが容易に判別できると思ったからです。

シリアルプロッタは最近のバージョンで Arduino IDE に搭載された機能で、リアルタイムにグラフ化できるので、これを使わない手はありません。

シリアルプロッタで表示する方法は後の節で述べますが、まず、以下のスケッチを入力してみてください。
サイン波を見るというよりは、Core ID や、タスク優先度をシリアルモニタで確認するためのプログラムを動かしてみます。
ディレイ時間を多くして、数値を見やすくしています。

ここで注意していただきたいのは、マルチタスク中ではArduino標準のdelay() や delayMicroseconds() 関数が使えないので、vTaskDelay という関数を使います。
FreeRTOS のマルチタスク中では、Tick という単位で時間をカウントしているようです。

Arduino の delay 関数は、vTaskDelay で作られていました。コメント欄でけりさんから教えていただきました。
ありがとうございます。m(_ _)m

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

float pi = 3.14159; //円周率
float radA = (2 * pi) / 360; //ラジアンを角度(0-360度)単位にする

int mainAngle = 0; //メインloop のサイン波の初期角度
float mainSin = 0; //メインloop のサイン波の値

portTickType Delay1000 = 1000 / portTICK_RATE_MS; //freeRTOS 用の遅延時間定義
TaskHandle_t th[2];

void Task1(void *pvParameters) {
  int task1Angle = 45; //Task1 のサイン波の初期角度
  float task1Sin = 0; //Task1 のサイン波の値
  
  while(1) {
    Serial.printf("Task1 coreID = %d, Task1 priority = %d, task1Sin = %F\r\n", xPortGetCoreID(), uxTaskPriorityGet(th[0]), task1Sin);
    task1Sin = float(sin(radA * task1Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task1Angle++;
    if(task1Angle >= 360) task1Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay1000); //freeRTOS用のディレイタイム実行
  }
}

void Task2(void *pvParameters) {
  int task2Angle = 90; //Task2 のサイン波の初期角度
  float task2Sin = 0; //Task2 のサイン波の値
  
  while(1) {
    Serial.printf("Task2 coreID = %d, Task2 priority = %d, task2Sin = %F\r\n", xPortGetCoreID(), uxTaskPriorityGet(th[1]), task2Sin);
    task2Sin = float(sin(radA * task2Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task2Angle++;
    if(task2Angle >= 360) task2Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay1000); //freeRTOS用のディレイタイム実行
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println();

  xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, &th[0], 0); //Task1実行
  xTaskCreatePinnedToCore(Task2,"Task2", 4096, NULL, 5, &th[1], 1); //Task2実行
}

void loop() {
  Serial.printf("loop  coreID = %d, loop  priority = %d, mainSin = %F\r\n", xPortGetCoreID(), uxTaskPriorityGet(NULL), mainSin);
  mainSin = float(sin(radA * mainAngle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
  mainAngle++;
  if(mainAngle >= 360) mainAngle = 0; //角度が360度になったらゼロ
  delay(1000);
}

【解説】

前節を踏まえて、見ていただければ分かると思うのですが、一応説明します。

●2行目:
ここで、三角関数でいうラジアン 2π を 360°で割って、1°あたりのラジアン値を求めておきます。

●7行目:
FreeRTOS の portTickType 型で Delay1000 という引数を定義します。
先にも述べたように、マルチタスク中の関数内では、時間のカウンタは Tick という単位だそうですので、それをミリセコンド相当にするためには、portTICK_TATE_MS という定数で割ります。
ですから、
1000/ portTICK_TATE_MS
という値が 1000ms と等しい時間になります。
実は、Arduino-ESP32 の delay関数は vTaskDelay で作られています。

●8行目:
タスクハンドルを格納するためのものです。
TaskHandle_t 型の配列で定義しています。

●10-21行目:
一つ目のタスクです。
Task1 というのは好きな名前にすることができます。
*pvParameters は変更不可です。
11行目で、メインループのサイン波と異なって、初期角度を45°ずらしています。
14-20行で無限ループにしています。
15行目で、xPortGetCorID() で CPU core ID を取得できます。
そして、uxTaskPriorityGet にタスクハンドルポインタを代入すれば、そのタスクの優先順位を取得できます。
19行目で、vTaskDelay 関数を使い、Tick カウントを 1000ms 相当にしています。

●23-34行目:
2つ目のタスクです。
ここでは、サイン波を90°ずらしています。

●40-41行目:
セットアップ関数内で、タスクを生成します。
前々節で述べたように、xTaskCreatePinnedToCore 関数を使う必要があります。
スタックサイズはメインループの半分の 4096 としています。
優先順位は Task1 が 3、Task2 が 5 としました。
タスクハンドルはポインタなので、アドレスを渡せばOKです。
Task1 の CPU core は 0 とし、Task2 のcore は 1 としました。つまり、Task2 はメインloop と同じ core になります。

●44-50行目;
45行目の uxTaskPriorityGet では、メインループのタスクハンドルは NULL なのでそれを代入します。
メインループでは、delay関数をそのまま使えます。

以上です。

では、これをコンパイル実行して、シリアルモニタを 115200bps で起動してみてください。
こんな感じになります。

この、xPortGetCorID() という関数が正しいのであれば、デュアルコアで確実に動いていると言えます。

ただ、これから分かる通り、Task2 と メインloop の CPU core が 1 で同じになっています。
つまり、3つのタスクが同時に動くのか???
・・・って思ってしまいますよね。
同じ Core でもマルチタスクにできるのかという疑問が出てきます。

では、次にそれを検証してみます

シリアルプロッタのサイン波形で、マルチタスクを検証

では、シリアルプロッタに連続したサイン波形を表示させてみます。

下図の様なスケッチを入力してみてください。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

float pi = 3.14159; //円周率
float radA = (2 * pi) / 360; //ラジアンを角度(0-360度)単位にする

int mainAngle = 0; //メインloop のサイン波の初期角度
float mainSin = 0; //メインloop のサイン波の値

portTickType Delay10 = 10 / portTICK_RATE_MS; //freeRTOS 用の遅延時間定義
TaskHandle_t th[2];

void Task1(void *pvParameters) {
  int task1Angle = 45; //Task1 のサイン波の初期角度
  float task1Sin = 0; //Task1 のサイン波の値
  
  while(1) {
    Serial.println(task1Sin);
    task1Sin = float(sin(radA * task1Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task1Angle++;
    if(task1Angle >= 360) task1Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay10); //freeRTOS用のディレイタイム実行
  }
}

void Task2(void *pvParameters) {
  int task2Angle = 90; //Task2 のサイン波の初期角度
  float task2Sin = 0; //Task2 のサイン波の値
  
  while(1) {
    Serial.println(task2Sin);
    task2Sin = float(sin(radA * task2Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task2Angle++;
    if(task2Angle >= 360) task2Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay10); //freeRTOS用のディレイタイム実行
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println();

  xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, &th[0], 0); //Task1実行
  xTaskCreatePinnedToCore(Task2,"Task2", 4096, NULL, 5, &th[1], 1); //Task2実行
}

void loop() {
  Serial.println(mainSin);
  mainSin=float(sin(radA * mainAngle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
  mainAngle++;
  if(mainAngle >= 360) mainAngle = 0; //角度が360度になったらゼロ
  delay(10);
}

【解説】

先ほどのスケッチと変えたところは、以下の通りです。

●7行目:
10ms 相当にして、連続的な表示ができるようにしました。

●15行目:
Task1 のsin値だけをシリアルモニタに出力するように変更

●28行目:
Task2 のsin値だけをシリアルモニタに出力するように変更

●45行目:
メインloop の sin値だけをシリアルモニタに出力するように変更

●49行目:
delay を 10ms に変更

以上です。

では、コンパイル実行してみてください。
今度はシリアルモニタではなく、シリアルプロッタを起動します。

すると以下の動画のようになります。

シリアルプロッタはマルチタスクには対応していないので、別のタスクでシリアル出力したものが同じ色のグラフに融合されてしまっています。

でも、よく見ると、45°づつズレた波形がボヤ~ッと薄ら見えますね。

薄ら見えているところに線を引いてみるとこんな感じで見えています。

見事に45°ズレたサイン波形が永遠と連続して表示されているのがわかります。

プログラムを見ても分かる通り、Task1 とTask2 は Setup 関数内で実行されていて、loop中の時間差で出力されているわけではありませんので、完全にマルチタスクが実行されていると言えるのではないでしょうか?

しかも、3つ異なるタスクが同時です。

こりゃぁ、スゴイですよ!!!

・・・と思ったのですが、実は裏がありました!

実際に GPIO に電圧を出力するとどうなるのでしょうか?
次で見ていきます。

コメント

  1. けり より:

    お疲れ様です!
    こちらのファイルにはdelay(), delayMicroseconds() が定義されています。
    https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-misc.c
    delayMicroseconds()はただ時間を潰して待つのであまり良くありませんが、delay()は問題なく使えそうです。

    • mgo-tec mgo-tec より:

      けりさん

      コメントいただき、感謝いたします。
      あ、そうなんですね。
      delay() はあまり深くチェックしてなかったので、助かります。
      早速記事を修正したいと思います。
      そういえば、当方でも delay() はあまりエラーが出なかったので、不思議に思っておりました。
      けりさんの記事を参考にさせていただきながら、情報もいただき、感謝感謝です。
      ありがとうございました。
      m(_ _)m

    • mgo-tec mgo-tec より:

      今、確認しました。
      確かに、esp32-hal-misc.c に、delay() は vTaskDelay で作られていました。
      改めて、情報ありがとうございました。
      m(_ _)m

  2. yitabashi より:

    検証していないのですが、「予想に反して、Task1 だけが実行され、Task2 は常時 HIGHレベルになっています。なぜかタスク優先順位が効いていません。」の部分は、Task1がTask2より先に実行され、Task1中の無限ループのままOS側に制御を戻していないからだと思われます。
    while(1){}中にvTaskDelay(Delay1);を入れて、一度OS側に処理を明け渡してみてはどうでしょうか。

    • mgo-tec mgo-tec より:

      yitabashi さん

      記事をご覧いただきありがとうございます。

      おっしゃる通りでした。
      Twitter でも つきしまみなつさんが検証されていて、vTaskDelay の間に優先度の低い他の Task が実行されているとの報告がありました。
      まだまだ FreeRTOS は勉強不足なのですが、 vTaskDelay の使い方は何となく分かって来ました。
      記事も修正しようと思います。
      情報ありがとうございました。
      m(_ _)m

      • 組み込みプログラマ より:

        興味があったので拝見させていただきました。

        通常であればmgo-tecさんの予想通りTask2のみが動き続けるはずですが、なぜかTask1だけが動作しています。
        yitabashiさんのコメントにあります「Task1がTask2より先に実行され」という事は、プリエンプションが有効なリアルタイムOSで両方ともReady状態であれば通常はありえません。
        そこで考えたのですが、Setup()がloop()と同じタスクから実行されていたとするとxTaskCreatePinnedToCore()でtask1が生成されたタイミングでCPU実行権がloop()タスクより優先度の高いtask1に移行して、Setup()の次の行のtask2のxTaskCreatePinnedToCore()が実行できなくてtask1のみが走り続けているのではないかと思いました。
        (繰り返しますが、プリエンプションの有効なリアルタイムOSでは、優先度が低いタスクが他の優先度の高いタスクを待たせて動作する事はあり得ません。vTaskDelayを入れると動作するのは、そのタイミングでSetup()のtask2のxTaskCreatePinnedToCore()が実行される為ではないかと想像します。)
        以上、想像ですがコメントさせていただきました。

        • mgo-tec mgo-tec より:

          組み込みプログラマさん

          私の様なアマチュアの記事をご覧いただき、誠にありがとうございます。
          この記事は随分昔に書いていて、自分の書いた文章を理解するのに時間がかかってしまいました。

          確かに、組み込みプログラマさんのおっしゃる通りだと思います。
          私も最近、なんとなく分かってきました。
          正にその通りで、setup関数とloop関数は同じcore 1 です。
          すると、xTaskCreatePinnedToCore を実行した時点でtask1がloopタスクよりも優先順位が高いので、task1が実行されてしまい、task2が実行される余地が無かったのだと思います。
          今考えると分かるのですが、この記事を書いた当初はサッパリわかりませんでした。
          現役プログラマの方からご意見いただけると、このブログの読者の方々の参考にもなりますし、とっても有り難いです。
          感謝感謝感謝でございます。
          m(_ _)m

  3. 通りかがり より:

    >相変わらず メインloop 関数の速度は遅いですね

    少し上で loop の外側で micros(); されてるせいで遅いと書かれておきながら、相変わらず・・・っていうネガティブ表現には違和感を感じます。
    速くなりようがないですから。

    micros(); が1を返すまで、つまりそこで強制的な1μsのウェイトが入ってるのと同じなので、相変わらずも何も、loop を抜けてから次に入るまで1μsかかるのが仕様だと思います。

    • mgo-tec mgo-tec より:

      通りがかりさん

      記事をご覧いただき、ありがとうございます。

      おっしゃる通りでした。
      今読み返すと、かなりネガティブ表現でした。
      この記事を書いた当初はもうちょっと速くできそうなものなのに、と思ってましたが、今思えばそういう仕様だということがよく分かって来ました。

      今では、Arduino-ESP32 によくぞ FreeRTOS を取り入れたものだと感心していて、開発チームの皆さんの凄さが身に染みて分かってきました。
      これは開発チームの方々にとっても失礼な表現でした。
      早速、修正します!!!

      開発チームの皆さま、申し訳ございませんでした。
      m(_ _)m

  4. teru より:

    古い記事に突っ込むのも恐縮ですが。

    「GPIO 出力は2つ分のタスクしか実行できない」という結論の意図が分かりかねます。
    while ループの中で、 vTaskDelay 等を呼び出すようにすれば、GPIO 出力のタスクを3つでも4つでも並列に実行可能なのではないでしょうか(もちろん少なくともdelay分、GPIO 出力の切り替わり速度は低下するでしょう)。
    逆に、記事中のようにビジーループのタスクを作ってしまうと、ほかの処理(例えば WiFiの処理や画面の処理)ができなくなってしまうので、あまり実用的ではないように思います。
    素人考えですが、継続して ON/OFFを高速で切り替える必要があるなら、PWM等を使用したほうがよいように思います。

    あと、ウォッチドッグタイマについての説明が不自然なように思います。
    ウォッチドッグタイマとは、基本的には「ウォッチドッグタイマが指定された間隔以内で定期的にリセットされなかった場合にシステムを再起動する」ものです。
    ウォッチドッグタイマが「動作しないとプログラムが落ちます」と書かれていますが、個人的には逆に「ウォッチドッグタイマが動作した」というと、「ウォッチドッグタイマがタイムアウトしてシステムが再起動した」ことを指すイメージがあるので違和感があります。
    また、「Task watchdog got triggered」のメッセージは、ウォッチドッグタイマの定期的なリセットをしなかった結果、ウォッチドッグタイマにより表示されているものなので、
    「ウォッチドッグタイマが介入できない」というより、むしろ「ウォッチドッグタイマが介入した」という方が自然なように思います。
    まとめると、「Task watchdog got triggered」のメッセージは、 while ループのために ウォッチドッグタイマがリセットされず、ウォッチドッグタイマが発動してしまったことにより出力されています(このウォッチドッグタイマはシステムの再起動をしないということですね)。ウォッチドッグタイマがリセットされるよう、while ループ中に vTaskDelay 等を入れる必要があります。

    • mgo-tec mgo-tec より:

      teruさん

      記事をご覧いただき、そしてとても詳しいご指摘、ありがとうございます。
      (別記事へ誤って投稿されたものは削除させていただきました。)

      この記事を書いた当初はFreeRTOSの仕組みもマルチタスクも良く分からずに、ただ「できた」というだけで書いたものです。
      それに、ウォッチドッグタイマも良く知らずに適当に書いてました。
      実は今も良く知りません。
      時たま出るウォッチドッグメッセージが邪魔で、最近はウォッチドッグを無効にしているくらいです。
      このブログでは結構頻繁に出る用語なのに、ろくに調べもしませんでした。
      実際、ウォッチドッグを動作させないようにプログラミングしていますので、その機能についてあまり調べる必要性に駆られていませんでした。
      自分的には、とりあえず作りたいものが完成すればいいや的な感じでした。

      何分、独学で見切り発車的な工作が多々ありますので、まだまだ誤って覚えていることが多いと思います。
      最近はプロ方々から手厳しいご指摘を受けることもしばしばありますので、記事中に「自分は素人で誤ってる可能性があります」と謳うようにしてます。
      覚えたばかりの専門用語も、できるだけ「知ったかぶり」しない様に最近は気を付けるようにしています。

      今回はご指摘を頂いたおかげで、また勉強になりました。
      この記事を見た方が間違えて覚えてしまっては困りますので、手が空いたら徐々に修正していこうと思います。
      いろいろありがとうございました。
      m(_ _)m

      • 組み込みプログラマ より:

        ESP32の情報を調べていたらまたmgo-tecさんのページにたどり着きましたので少しコメントを。
        まず、mgo-tecさんの発信する情報は皆さんに大変役立っているとおもいますので、我々の様な者のツッコミにめげずに続けていただければと思います。
        次に、情報を参照させて頂いているお礼に少しだけウォッチドッグタイマについて自分の知っている知識を書き込みます。
        (長いので不要であれば掲載せず無視してください。)
        ウォッチドッグタイマについてですが、この機能は元々プログラムがうまく動かない状態になってしまった場合にCPUにリセットをかける為に実装されています。
        通常、マルチタスクで実装する場合は、各タスクはセマフォ取得(xSemaphoreTake())やキュー受信(xQueueReceive())のAPIでイベントが発生するまで待ちます。そして、UARTやSPI/I2Cの受信割り込みやタイマのインターバル割り込み、GPIOの状態変化割り込み等が発生した際に先ほどのセマフォを開放(xSemaphoreGiveFromISR())したりキューにデータを入れたり(xQueueSendFromISR())して、担当のタスクを動かします。担当のタスクはセマフォ取得等から復帰してデータなどを処理し、再びセマフォ取得等を実施して次のイベントを待ちます。
        上記の通り「各タスクはイベントが発生した時にその処理を実施して再び待つという動作を繰り返す」という様に設計するので、まず(今回の記事の例はお試しだったのであえてツッコミませんでしたが)通常は今回の記事の様なタスクの使い方はしません。
        上記の通り、各タスクはイベントが発生すると動き出しますが、その際にバグ等でループから抜けなくなってしまった際などに、それを検知してCPUにリセットをかけるのがウォッチドッグタイマの役目です。
        ウォッチドッグタイマは通常は最低の優先度のタスクに実装し、タイマやDelay等で定期的に起きてウォッチドッグタイマのカウンタをクリアします。上記のバグ等で他の優先度の高いタスクが動きっぱなしになり、ウォッチドッグタスクが起動できない状態が続くとカウンタがオーバーフローしてリセットがかかるという仕組みになります。
        (マルチタスク設計では、今回の記事の様にCPU性能をフルに使い切って動き続ける様な設計をする事は無いので、必ずウォッチドッグのタイムアウトまでにウォッチドッグタスクが起動できる隙間が出来る様に各タスクの処理やウォッチドッグのタイムアウト時間を調整します。)
        趣味の電子工作ではあまり意識しませんが、製品として世の中に出ていくシステムは止まってしまっては困るのでこういった仕組みを実装します。(もちろん、通常はウォッチドッグありきの設計はせず、万が一動かなくなってしまった場合に自動で復旧させる為という意味で実装します。又、マルチタスク処理ではデッドロック等が発生してしまった際にはウォッチドッグは意味をなさないので、他の異常検出処理と併用して使用する事もあります。)
        以上、お役に立てれば幸いです。

        • mgo-tec mgo-tec より:

          組み込みプログラマ さん

          以前、コメントいただいた方と同じ「組み込みプログラマ」さんですね。
          再度コメントいただき、そして、とてもウォッチドッグタイマの詳細な解説頂き、ありがとうございます。

          最近の記事で、ウォッチドッグタイマ動作が無視できなくなってきたので、私もつい最近ようやく勉強し始めました。
          なるほど、そうやってプロの現場ではタスク管理をやっているんですね。
          ネット上ではある程度情報があったので、大体は理解したつもりだったんですが、プロの方から生の声を頂くと、とっても解りやすいです。
          私はまだFreeRTOSのセマフォやキューによるタスク管理は全く無知だったんですが、これからは避けて通れなくなりそうです。

          因みに、この記事も修正しました。
          実はArduino core ESP32のloop()関数等のCPU core1 は、デフォルトでウォッチドッグタイマ動作無効になっていました。
          Twitterで@tnkmasayukiさんから情報を頂きました。
          この記事を書いた当初は有効だったと思ったのですが、単なる勘違いだったか、修正されてしまったみたいです。
          あれれ??? って感じでした。

          そんなわけで、とっても有益な情報ありがとうございました。
          このコメントは他の読者の助けになること間違いなしです。
          自分もツッコミにめげずに、記事を書き続けていきたいと思います。
          今後とも何かお気づきの点がありましたら、コメント頂けると幸いです。
          m(_ _)m

  5. 組み込みプログラマ より:

    mgo-tecさん、お久しぶりです。組み込みプログラマです。

    今回はマルチタスク関連で私がハマった件について報告させてください。(どこかに書いておかないと忘れてしまいそうなので報告させて下さい。ただ、既知の内容でしたら掲載しないで頂いてもかまいません。)
    今回はHTTPサーバを構築する為に、TCP(80)のサーバソケットを開いてクライアントから接続される度に子スレッドを起こしてクライアントの処理をその子スレッドで実施させる構成(子スレッドは処理が終わったら終了する)をお試しで組んでみたのですが、10回程度接続があるとスレッドの生成がエラーになったりaccept()がエラーになり、最終的にはリセットが発生するという現象が発生しました。(ソケット操作はlwipのAPI(socket,bind、listen,acceptなど)を直接叩いています。)
    色々調べてみたのですが、最終的には「マイコン徹底入門」さんの「3.11. タスクの削除(自身の削除)」の記述にあった「タスクのためのメモリはvTaskDelete関数を実行した時点で解放されるのではなくその後Idleタスクが実行された時点で解放されます。」の記述が引っかかり、ArduinoのESP32のmain.cppを確認してみた所、loopタスク内で制御が握りっぱなしになっている事が分かりました。なので、loop()処理の中にvTaskDelay(100)をいれた所、症状が治まりました。多分ですが、Arduino的にはloopタスクが最低プライオリティなので走りっぱなしにしている様ですが、FreeRTOS内にはさらにプライオリティの低いIdleタスクがあり、そのタスクが動く隙間を作ってあげないと終了したタスクのリソースが開放されないのではないかと思われます。
    今回の様なタスクを使い捨てる構成だと発生してしまうと思いますので、もし同様の現象があったら疑ってみてください。
    以上、お邪魔しました。

    • mgo-tec mgo-tec より:

      組み込みプログラマさん

      再びコメントいただき、ありがとうございます。
      タスクの使い捨てについては、随分前にちょっとだけ試してみたことがあります。
      たしか、SSLサーバーをESP32で作った時、うまくいかなくてvTaskDelete()を使ってみたけど、うまく行かなかった覚えがあります。
      なるほど、vTaskDelayを置けば良いというのは良い情報をいただきました。
      更にIdleタスクがあるというのは、ありえそうですね。
      (私はFreeRTOSを全く辿れていないので、何も言えませんが。。。)
      自分の現在取組中のプログラミングではvTaskDeleteを試す機会が無いのですが、頭にインプットしておこうと思います。
      このコメントを見てくれた読者の方々には役立つと思います。
      有益な情報、ありがとうございました。
      m(_ _)m

      因みに、私は試していないのですが、Twitterでは、
      vTaskDelete(NULL);
      にすると良いという情報がありましたが、これではダメなんですかね?

  6. 組み込みプログラマ より:

    mgo-tecさん、こんにちは。組み込みプログラマです。

    > 更にIdleタスクがあるというのは、ありえそうですね。

    ちょっと調べてみたらprvIdleTask()というタスクがありました。これがIdleタスクの様で、終了タスクの削除処理以外にもモロモロの処理(vApplicationIdleHook()の呼び出しなど)をしている様なので、FreeRTOSの機能を使用するのであればloop()の中では特に支障が無い限りはdelayを入れたておいた方がよさそうです。

    > 因みに、私は試していないのですが、Twitterでは、
    > vTaskDelete(NULL);
    > にすると良いという情報がありましたが、これではダメなんですかね?

    私も実際にはタスクを終了する直前にvTaskDelete(NULL)を実行していましたがダメでした。(ちなみに、vTaskDeleteのパラメータは削除するタスクのハンドルで、NULLを指定するとこのAPIを実行したタスク自体が削除対象になります。)
    vTaskDelete(NULL)はあくまでタスクの削除を予約するだけの様なので、Idleタスクが動かないと実際の削除処理(スタックメモリやTCB等のタスクが使用していたリソースの開放や削除)が実行されないのでリソース不足が発生してしまうと思われます。

    そもそも、ArduinoのフレームワークやライブラリはESP32をマルチタスクでプログラミングする前提では考えられていない様なので、マルチタスクで実装する際にはどこまでできるか探りながらやる必要がある様ですね。
    今回もWebServerクラスで画像を載せたhtmlファイルを返した際に複数コネクションの接続要求が発生して(WebServerクラスでは複数コネクションは処理できず)ブラウザがダンマリになるので、コネクション毎に別タスクで処理する方法で実装できるかをお試しする為に実装していたらこんな現象に当たってしまいました。
    この後もSDカードへの複数タスクからの複数ファイルアクセスが可能かどうかなど、また探りながらお試しする必要がありそうです。
    (それでも、ESP-IDFで自分で開発環境を構築するよりはお手軽に開発できるのであまり文句も言えませんけど。)
    それでは失礼します。

    • mgo-tec mgo-tec より:

      組み込みプログラマさん

      お返事ありがとうございます。
      やっぱNULLでもダメでしたか。。。

      やっぱりメインloop内はなぜかウォッチドッグが無効になっていることも関係しているっぽいですし、不具合がある場合はできるだけdelay(1)以上を置いた方が良いということですね。

      私は現在、ブラウザに画像をストリーミング送信することを試していますが、core 0でウォッチドッグを有効にしてWiFi処理するようにしています。
      やっぱり、適度にdelayを入れないとブラウザがダンマリしてしまうので、難しかったです。
      loop内のcore 1ではWiFi処理しないようにしていましたが、適度にdelayを入れればloop内でもうまくできそうですね。
      私も時間がある時に良く調べてみたいと思います。
      貴重な情報ありがとうございました。
      m(_ _)m

タイトルとURLをコピーしました