こんばんは。
今回は、とにかくスゴイです。
ようやく、趣味の電子工作でこんな時代がやってきました!!!
ESP32 ( ESP-WROOM-32 ) のマルチタスク ( Dual Core ) です。
ただし、Arduino core for ESP32 についてです。
ESP32 は CPU がデュアルコアになっているので、1つのCPUコアの動作中に別のCPUを同時に動かすことができる優れものです。
これを使ってしまうと、他のマイコンには移れないくらいの素晴らしさを感じてしまいます。
何度も言いますが、とにかくスゴイです!!!
電子工作でデュアルコアを自在に操作できるなんて、一昔前までは思っても見ませんでした。
前回の記事でも述べましたが、Arduino core for ESP32 ではマルチタスクを使う前提で Arduino 関数が組まれています。
FreeRTOS というリアルタイム OS で Arduino core プログラムが作られていたというわけです。
FreeRTOS についてはまだ勉強中で良く分かりませんが、Arduino core for ESP32 上ではその関数を直接呼ぶことができます。
ちょっと特殊なところもありますが、あるポイントを押さえておくと、それほど難解なこともなく使えます。
では、私なりに Arduino – ESP32 のマルチタスクを検証してみたので説明してみたいと思います。
なお、今回の記事を書くにあたって、以下のサイトもたいへん参考にさせていただきましたので、合わせてご参照ください。
KERIさんとgarretlabさんの記事は頻繁に参考にさせていただいております。
改めて感謝いたします。m(_ _)m
マルチタスク・デュアルコアの実験(ESP-WROOM-32)
準備するもの
ESP-WROOM-32 ( ESP32 )開発ボード
スイッチサイエンスさんの以下のボードは、パーツの保護機能が充実していてお薦めです。
ESPr® Developer 32
このボードをレビューした以下の記事もご参照ください。
ESPr Developer 32 ( スイッチサイエンス製 ) を使ってみました
ESP32-DevKitC でも特に問題無く使えます。
Arduino IDE 等の設定
Arduino IDE は 1.8.3 以上を推奨します。
Arduino IDE で ESP32 開発ができるようにするための方法は以下の記事を参照してください。
Arduino core for the ESP32 のインストール方法
Arduino core for ESP32 でのマルチタスクの組み方
では、Arduino core for ESP32 のスケッチ上でFree RTOS ライブラリを使う方法を説明します。
メインloopとは別の、もう1つのタスクプログラムを組む方法は以下のような感じになります。
void タスク名(void *pvParameters){ while(1){ 任意のプログラム } } void setup(){ xTaskCreatePinnedToCore( タスク名, "タスク名", スタックメモリサイズ, NULL, タスク優先順位, タスクハンドルポインタ, Core ID ); } void loop(){ }
これで、setup関数でメインloop とは別のタスクが生成され、メインloop と同時に 新タスクの while ループが実行されるわけです。
他のサイトで、xTaskCreate という関数でマルチタスクプログラムを組んでいるところがありますが、Arduino core for ESP32 の場合は、xTaskCreatePinnedToCore でないとダメです。
このことについては、以前、GitHub のESP-IDF のページで述べられていました。
(現在はなぜか削除されてしまいました)
要するに、xTaskCreate は下位互換性のためのもので、1番目のコアのみ動作する関数です。
他のCPUコアを使うためには、xTaskCreatePinnedToCore を使うこと、というような記載がされています。
(英訳が間違えていたらスイマセン)
タスク名は好きな名前にすることができます。
スタックメモリサイズは、あまり大きくし過ぎないようにした方が賢明です。
因みに次の節で述べますが、メインループのデフォルトでは 8192 byte (2019/11月時点)です。
タスク優先順位は 1~25 1~24の範囲です。
FreeRTOSの仕様を見てみると、優先順位は、
0〜(configMAX_PRIORITIES - 1)
となっていて、configMAX_PRIORITIESについては、Arduino core for the ESP32 の中の以下のファイル
FreeRTOSConfig.h
にこのように定義されています。
#define configMAX_PRIORITIES ( 25 )
よって、Arduino core ESP32 のタスク優先順位は0~24と言えそうです。
そして、大きい方が優先順位が高まります。
シングルコアで同時に実行するプログラムがある場合、優先順位が高い方が優先され、もう一方のプログラムは実行されないようです。
タスクハンドルポインタは、TaskHandle_t型の引数を定義して、そのアドレスを指定すれば良いです。
これについては、後述します。
CoreID は CPU のコア番号です。
ESP32 ( ESP-WROOM-32 )の場合は、デュアルコアなので、0 か 1 のどちらかになります。
そのタスク中のコア番号( Core ID ) を取得するためには以下の関数を使えば取得できます。
xPortGetCoreID()
また、そのタスクの優先順位を取得するためには、以下の関数を使います。
uxTaskPriorityGet( タスクハンドルポインタ )
Arduino core for ESP32 の メイン loop 関数のマルチタスク構成について
では、上記を踏まえて、Arduino core for ESP32 のメイン loop 関数の構成をテキストエディタで見てみましょう。
Windows10 の場合は以下のフォルダの main.cpp というファイルです。
C:\Users\User-Name\Documents\Arduino\hardware\espressif\esp32\cores\esp32
main.cpp (2018/02/21時点)
#include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "Arduino.h" #if CONFIG_AUTOSTART_ARDUINO #if CONFIG_FREERTOS_UNICORE #define ARDUINO_RUNNING_CORE 0 #else #define ARDUINO_RUNNING_CORE 1 #endif void loopTask(void *pvParameters) { setup(); for(;;) { micros(); //update overflow loop(); } } extern "C" void app_main() { initArduino(); xTaskCreatePinnedToCore(loopTask, "loopTask", 8192, NULL, 1, NULL, ARDUINO_RUNNING_CORE); } #endif
これを見ても分かる通り、FreeRTOS ライブラリがインクルードされていて、
xTaskCreatePinnedToCore
という FreeRTOS の関数でタスクが生成されて、loopTask という関数内で無限ループが構成されています。
7行目の CONFIG_FREERTOS_UNICORE はどこで定義されているか突き止められなかったのですが、値は常時 false となっています。
これは、Serial.print などでシリアルモニターに出力すると false になっていることがわかります。
ということは、UNICORE ではないということになって、デュアルコア対応ではないのかな? と疑問に思ってしまいますね。
これは、Twitter で つきしまみなつさんから教えてもらったのですが、CONFIG_FREERTOS_UNICORE の設定は ESP-IDF で設定できるそうで、Arduino-ESP32 では、IDF で設定したものを使っているようです。
( ESP-IDF についてはこちらの記事を参照してください )
そして、FreeRTOSConfig.h というファイルにこんな記述がありました。
/* ESP31 and ESP32 are dualcore processors. */ #ifndef CONFIG_FREERTOS_UNICORE #define portNUM_PROCESSORS 2 #else #define portNUM_PROCESSORS 1 #endif
この、portNUM_PROCESSORS という定数をシリアルモニタに表示させると、2と表示されました。
これから恐らく、Arduino-ESP32 では常時デュアルコアで動いていると言えそうです。
また、スタックサイズはデフォルトで 4096 byteとなっています。
ちょっと小さいですよね。
8192 byteとなっています。
2017年7月では4096だったのですが、今は2倍になっています。
今まで、HALT エラーになっていた場合は、このスタックサイズを超えてメモリーを使っていた可能性があります。
これを大きくすれば解消される場合がありますが、大きすぎると他のプログラムが動かなくなってしまうので、要注意です。
前回の記事で述べましたが、SSL の Webページから記事を取得する場合はこのスタックサイズが小さすぎるので変える必要があります。
タスク優先度は1となっていて、一番低い値です。
新たにタスクを生成する場合には1以上にすれば優先されます。
タスクハンドルは NULL となっています。
これについては良く分かりませんが、メインループは NULL と覚えておけばよいと思います。
CoreID は
CONFIG_FREERTOS_UNICORE = false
なので、
ARDUINO_RUNNING_CORE = 1
となります。
とすると、別のタスクをデュアルコアで動かしたい場合は 0 とすれば良いということになります。
ただ、まだよく調べていないのですが、ESP32 の場合は Core 0 で WiFi や Bluetooth などの制御用のプログラムが動いている可能性があります。
そう考えると、Core 0 に大量のユーザープログラムを入れてしまうと、うまく動作しない場合があります。
これは要注意したいところですね。
コメント
お疲れ様です!
こちらのファイルにはdelay(), delayMicroseconds() が定義されています。
https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-misc.c
delayMicroseconds()はただ時間を潰して待つのであまり良くありませんが、delay()は問題なく使えそうです。
けりさん
コメントいただき、感謝いたします。
あ、そうなんですね。
delay() はあまり深くチェックしてなかったので、助かります。
早速記事を修正したいと思います。
そういえば、当方でも delay() はあまりエラーが出なかったので、不思議に思っておりました。
けりさんの記事を参考にさせていただきながら、情報もいただき、感謝感謝です。
ありがとうございました。
m(_ _)m
今、確認しました。
確かに、esp32-hal-misc.c に、delay() は vTaskDelay で作られていました。
改めて、情報ありがとうございました。
m(_ _)m
検証していないのですが、「予想に反して、Task1 だけが実行され、Task2 は常時 HIGHレベルになっています。なぜかタスク優先順位が効いていません。」の部分は、Task1がTask2より先に実行され、Task1中の無限ループのままOS側に制御を戻していないからだと思われます。
while(1){}中にvTaskDelay(Delay1);を入れて、一度OS側に処理を明け渡してみてはどうでしょうか。
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()が実行される為ではないかと想像します。)
以上、想像ですがコメントさせていただきました。
組み込みプログラマさん
私の様なアマチュアの記事をご覧いただき、誠にありがとうございます。
この記事は随分昔に書いていて、自分の書いた文章を理解するのに時間がかかってしまいました。
確かに、組み込みプログラマさんのおっしゃる通りだと思います。
私も最近、なんとなく分かってきました。
正にその通りで、setup関数とloop関数は同じcore 1 です。
すると、xTaskCreatePinnedToCore を実行した時点でtask1がloopタスクよりも優先順位が高いので、task1が実行されてしまい、task2が実行される余地が無かったのだと思います。
今考えると分かるのですが、この記事を書いた当初はサッパリわかりませんでした。
現役プログラマの方からご意見いただけると、このブログの読者の方々の参考にもなりますし、とっても有り難いです。
感謝感謝感謝でございます。
m(_ _)m
>相変わらず メインloop 関数の速度は遅いですね
少し上で loop の外側で micros(); されてるせいで遅いと書かれておきながら、相変わらず・・・っていうネガティブ表現には違和感を感じます。
速くなりようがないですから。
micros(); が1を返すまで、つまりそこで強制的な1μsのウェイトが入ってるのと同じなので、相変わらずも何も、loop を抜けてから次に入るまで1μsかかるのが仕様だと思います。
通りがかりさん
記事をご覧いただき、ありがとうございます。
おっしゃる通りでした。
今読み返すと、かなりネガティブ表現でした。
この記事を書いた当初はもうちょっと速くできそうなものなのに、と思ってましたが、今思えばそういう仕様だということがよく分かって来ました。
今では、Arduino-ESP32 によくぞ FreeRTOS を取り入れたものだと感心していて、開発チームの皆さんの凄さが身に染みて分かってきました。
これは開発チームの方々にとっても失礼な表現でした。
早速、修正します!!!
開発チームの皆さま、申し訳ございませんでした。
m(_ _)m
古い記事に突っ込むのも恐縮ですが。
「GPIO 出力は2つ分のタスクしか実行できない」という結論の意図が分かりかねます。
while ループの中で、 vTaskDelay 等を呼び出すようにすれば、GPIO 出力のタスクを3つでも4つでも並列に実行可能なのではないでしょうか(もちろん少なくともdelay分、GPIO 出力の切り替わり速度は低下するでしょう)。
逆に、記事中のようにビジーループのタスクを作ってしまうと、ほかの処理(例えば WiFiの処理や画面の処理)ができなくなってしまうので、あまり実用的ではないように思います。
素人考えですが、継続して ON/OFFを高速で切り替える必要があるなら、PWM等を使用したほうがよいように思います。
あと、ウォッチドッグタイマについての説明が不自然なように思います。
ウォッチドッグタイマとは、基本的には「ウォッチドッグタイマが指定された間隔以内で定期的にリセットされなかった場合にシステムを再起動する」ものです。
ウォッチドッグタイマが「動作しないとプログラムが落ちます」と書かれていますが、個人的には逆に「ウォッチドッグタイマが動作した」というと、「ウォッチドッグタイマがタイムアウトしてシステムが再起動した」ことを指すイメージがあるので違和感があります。
また、「Task watchdog got triggered」のメッセージは、ウォッチドッグタイマの定期的なリセットをしなかった結果、ウォッチドッグタイマにより表示されているものなので、
「ウォッチドッグタイマが介入できない」というより、むしろ「ウォッチドッグタイマが介入した」という方が自然なように思います。
まとめると、「Task watchdog got triggered」のメッセージは、 while ループのために ウォッチドッグタイマがリセットされず、ウォッチドッグタイマが発動してしまったことにより出力されています(このウォッチドッグタイマはシステムの再起動をしないということですね)。ウォッチドッグタイマがリセットされるよう、while ループ中に vTaskDelay 等を入れる必要があります。
teruさん
記事をご覧いただき、そしてとても詳しいご指摘、ありがとうございます。
(別記事へ誤って投稿されたものは削除させていただきました。)
この記事を書いた当初はFreeRTOSの仕組みもマルチタスクも良く分からずに、ただ「できた」というだけで書いたものです。
それに、ウォッチドッグタイマも良く知らずに適当に書いてました。
実は今も良く知りません。
時たま出るウォッチドッグメッセージが邪魔で、最近はウォッチドッグを無効にしているくらいです。
このブログでは結構頻繁に出る用語なのに、ろくに調べもしませんでした。
実際、ウォッチドッグを動作させないようにプログラミングしていますので、その機能についてあまり調べる必要性に駆られていませんでした。
自分的には、とりあえず作りたいものが完成すればいいや的な感じでした。
何分、独学で見切り発車的な工作が多々ありますので、まだまだ誤って覚えていることが多いと思います。
最近はプロ方々から手厳しいご指摘を受けることもしばしばありますので、記事中に「自分は素人で誤ってる可能性があります」と謳うようにしてます。
覚えたばかりの専門用語も、できるだけ「知ったかぶり」しない様に最近は気を付けるようにしています。
今回はご指摘を頂いたおかげで、また勉強になりました。
この記事を見た方が間違えて覚えてしまっては困りますので、手が空いたら徐々に修正していこうと思います。
いろいろありがとうございました。
m(_ _)m
ESP32の情報を調べていたらまたmgo-tecさんのページにたどり着きましたので少しコメントを。
まず、mgo-tecさんの発信する情報は皆さんに大変役立っているとおもいますので、我々の様な者のツッコミにめげずに続けていただければと思います。
次に、情報を参照させて頂いているお礼に少しだけウォッチドッグタイマについて自分の知っている知識を書き込みます。
(長いので不要であれば掲載せず無視してください。)
ウォッチドッグタイマについてですが、この機能は元々プログラムがうまく動かない状態になってしまった場合にCPUにリセットをかける為に実装されています。
通常、マルチタスクで実装する場合は、各タスクはセマフォ取得(xSemaphoreTake())やキュー受信(xQueueReceive())のAPIでイベントが発生するまで待ちます。そして、UARTやSPI/I2Cの受信割り込みやタイマのインターバル割り込み、GPIOの状態変化割り込み等が発生した際に先ほどのセマフォを開放(xSemaphoreGiveFromISR())したりキューにデータを入れたり(xQueueSendFromISR())して、担当のタスクを動かします。担当のタスクはセマフォ取得等から復帰してデータなどを処理し、再びセマフォ取得等を実施して次のイベントを待ちます。
上記の通り「各タスクはイベントが発生した時にその処理を実施して再び待つという動作を繰り返す」という様に設計するので、まず(今回の記事の例はお試しだったのであえてツッコミませんでしたが)通常は今回の記事の様なタスクの使い方はしません。
上記の通り、各タスクはイベントが発生すると動き出しますが、その際にバグ等でループから抜けなくなってしまった際などに、それを検知してCPUにリセットをかけるのがウォッチドッグタイマの役目です。
ウォッチドッグタイマは通常は最低の優先度のタスクに実装し、タイマやDelay等で定期的に起きてウォッチドッグタイマのカウンタをクリアします。上記のバグ等で他の優先度の高いタスクが動きっぱなしになり、ウォッチドッグタスクが起動できない状態が続くとカウンタがオーバーフローしてリセットがかかるという仕組みになります。
(マルチタスク設計では、今回の記事の様にCPU性能をフルに使い切って動き続ける様な設計をする事は無いので、必ずウォッチドッグのタイムアウトまでにウォッチドッグタスクが起動できる隙間が出来る様に各タスクの処理やウォッチドッグのタイムアウト時間を調整します。)
趣味の電子工作ではあまり意識しませんが、製品として世の中に出ていくシステムは止まってしまっては困るのでこういった仕組みを実装します。(もちろん、通常はウォッチドッグありきの設計はせず、万が一動かなくなってしまった場合に自動で復旧させる為という意味で実装します。又、マルチタスク処理ではデッドロック等が発生してしまった際にはウォッチドッグは意味をなさないので、他の異常検出処理と併用して使用する事もあります。)
以上、お役に立てれば幸いです。
組み込みプログラマ さん
以前、コメントいただいた方と同じ「組み込みプログラマ」さんですね。
再度コメントいただき、そして、とてもウォッチドッグタイマの詳細な解説頂き、ありがとうございます。
最近の記事で、ウォッチドッグタイマ動作が無視できなくなってきたので、私もつい最近ようやく勉強し始めました。
なるほど、そうやってプロの現場ではタスク管理をやっているんですね。
ネット上ではある程度情報があったので、大体は理解したつもりだったんですが、プロの方から生の声を頂くと、とっても解りやすいです。
私はまだFreeRTOSのセマフォやキューによるタスク管理は全く無知だったんですが、これからは避けて通れなくなりそうです。
因みに、この記事も修正しました。
実はArduino core ESP32のloop()関数等のCPU core1 は、デフォルトでウォッチドッグタイマ動作無効になっていました。
Twitterで@tnkmasayukiさんから情報を頂きました。
この記事を書いた当初は有効だったと思ったのですが、単なる勘違いだったか、修正されてしまったみたいです。
あれれ??? って感じでした。
そんなわけで、とっても有益な情報ありがとうございました。
このコメントは他の読者の助けになること間違いなしです。
自分もツッコミにめげずに、記事を書き続けていきたいと思います。
今後とも何かお気づきの点がありましたら、コメント頂けると幸いです。
m(_ _)m
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タスクがあり、そのタスクが動く隙間を作ってあげないと終了したタスクのリソースが開放されないのではないかと思われます。
今回の様なタスクを使い捨てる構成だと発生してしまうと思いますので、もし同様の現象があったら疑ってみてください。
以上、お邪魔しました。
組み込みプログラマさん
再びコメントいただき、ありがとうございます。
タスクの使い捨てについては、随分前にちょっとだけ試してみたことがあります。
たしか、SSLサーバーをESP32で作った時、うまくいかなくてvTaskDelete()を使ってみたけど、うまく行かなかった覚えがあります。
なるほど、vTaskDelayを置けば良いというのは良い情報をいただきました。
更にIdleタスクがあるというのは、ありえそうですね。
(私はFreeRTOSを全く辿れていないので、何も言えませんが。。。)
自分の現在取組中のプログラミングではvTaskDeleteを試す機会が無いのですが、頭にインプットしておこうと思います。
このコメントを見てくれた読者の方々には役立つと思います。
有益な情報、ありがとうございました。
m(_ _)m
因みに、私は試していないのですが、Twitterでは、
vTaskDelete(NULL);
にすると良いという情報がありましたが、これではダメなんですかね?
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で自分で開発環境を構築するよりはお手軽に開発できるのであまり文句も言えませんけど。)
それでは失礼します。
組み込みプログラマさん
お返事ありがとうございます。
やっぱNULLでもダメでしたか。。。
やっぱりメインloop内はなぜかウォッチドッグが無効になっていることも関係しているっぽいですし、不具合がある場合はできるだけdelay(1)以上を置いた方が良いということですね。
私は現在、ブラウザに画像をストリーミング送信することを試していますが、core 0でウォッチドッグを有効にしてWiFi処理するようにしています。
やっぱり、適度にdelayを入れないとブラウザがダンマリしてしまうので、難しかったです。
loop内のcore 1ではWiFi処理しないようにしていましたが、適度にdelayを入れればloop内でもうまくできそうですね。
私も時間がある時に良く調べてみたいと思います。
貴重な情報ありがとうございました。
m(_ _)m