Arduino IDERaspberry Pi PicoESP32のマルチコア(デュアルコア)機能を使い,それぞれHUB75E規格の128*64のフルカラーLEDパネルを動かしています。

Raspberry Pi Picoのマルチコアで RGB LEDディスプレイを動かしてみた
ESP32のマルチコア機能を使って128*64のフルカラーLEDパネルを動かしてみた

マルチコア(デュアルコア)機能を使う時に必要な事をそれぞれ少しまとめてみました。




下図はコアの役割分担の構成図です。

・Arduino のsetup()とloop()はRaspberry Pi PicoではCore0で動き,
 ESP32ではAPP_CPU(Core1)で動いている。
・PICOとESP32ともにArduinoのメインのコアと異なる方に画像表示出力部を
 置く。
・PICOはコア間連絡用に専用FIFOがあるが,ESP32ではキュー(FIFO)を
 作成して使う。
・画像バッファはグローバル領域に置いて,両コアからアクセスする。






HUB75(E)規格のフルカラーLEDパネルは1行分(正確には画面上半分用の1行と下半分用の1行)のデータをシリアルに送ってはそれを各行に分配していくダイナミック駆動の表示器です。

各行にはバッファなど記憶する機構はないので,LEDが一瞬光った残像を頼りに1画面を60Hzぐらいで表示し続ける必要があり,表示出力専用に動く部分があればCPUは楽になります。

そのため,DMA(Direct Memory Access)やマルチタスク機能がよく使われているようです。



[Raspberry Pi Picoでのマルチコアの設定]

私の使っているarduino-picoライブラリ(ボード)での設定です。
(参考)
Raspberry Pi Pico のマルチコアの使い方 imo Lab. 
earlephilhower/arduino-pico Multicore Processing
raspberrypi.com rp2040-datasheet 2.3.1. SIO


1) デュアルコア(マルチコア)の書き方

・いつものsetup(), loop()はcore0で動いている
setup1(), loop1()の中のプログラムはcore1で並列に動く

・include や両方のcoreで使う変数は初頭のグローバル領域に書いておけば良い

・一方のcoreを止めてその間にデータを書き換えたり,待たせたりもできる
Pausing Cores
 void rp2040.idleOtherCore()
 void rp2040.resumeOtherCore()
 void rp2040.restartCore1()



2) コア間のコミュニケーション用FIFOの書き方

・core間のコミュニケーション用にFIFO(FirstIn, FirstOut)が双方向に
 置かれている
・FIFOは32bitx8(deep)の容量で,Mailboxと呼ばれている

・Communicating Between Cores

 送信:void rp2040.fifo.push(uint32_t)
 例)
 unit32_t bfn=0;
 rp2040.fifo.push(bfn);

 受信あり?:int rp2040.fifo.available()
 受信:uint32_t rp2040.fifo.pop()
 例)ただし受信する変数はグローバル領域で宣言しておく

 uint32_t buffer_Number=0;
//--------------------------------------
 if (rp2040.fifo.available()>0){
   buffer_Number=rp2040.fifo.pop();
 }





[ESP32でのマルチコアの設定]

 ESP32は基本的にFreeRTOS(Free Real Time Operating System)で動いていて,マルチタスクはこのOSの機能です。マルチコアやマルチスレッドに対応していて,ArduinoもこのOS上で動いています

(参考)
ESP32のFreeRTOS入門 その2 タスクの作成 (Lang-ship)
ESP32でマルチコアを試す12行 (Qiita)
ESP32でマルチスレッド:キューを送る(Qiita)
ESP32 Technical Reference Manual (Espressif Systems)


1) マルチタスク用の関数を作る
メインと並行して動かしたいプログラム部をマルチタスク用に独立させておく。

タスク関数の最小構成
ーーーーーーーーーーーーーーーーーー
void タスク名(void *pvParameters){
  while (1) {
    // ここにプログラムを書く
    delay(1);
   }
}

ーーーーーーーーーーーーーーーーーー
・引数はvoid型(不定型?)のポインタ変数としておく
 (void *pvParameters) と書く例が多い
 (void *args),(viod *arg),(void* arg )の例もあり,これも動いた

・while(1){ } は無限ループの定番

・delay(1)は無限ループ中に他にdelayが無い時は必須
 delayはWDT(Watch Dog Timer)をリセットし,ループを継続させる
 ESP32のWDTは3秒以上反応がないとお出ましするらしい
 Arduinoのloop()はWDTを無効にして動いている



2) タスクの作成 xTaskCreateUniversal() 
 無限ループなどとして独立させたプログラムをマルチタスク機構に一つのタスクとして登録する。

 xTaskCreate() – シングルコアでマルチスレッドのタスクを作成
 xTaskCreatePinnedToCore() – コアを指定してタスクを作成
 xTaskCreateUniversal() – マルチコア・スレッドに対応してタスクを作成


ーーーーーーーーーーーーーーーーーー
void setup(){    // setup{}内でタスクを作成

 xTaskCreateUniversal(
  タスク名,
  "タスク名",
  スタックメモリサイズ,
  起動パラメータ,
  タスク優先順位,
  タスクハンドル,
  Core ID
 );

}
ーーーーーーーーーーーーーーーーーー
・スタックメモリサイズ:タスク動作時の作業用に4096か8192にしておく
            loop()の設定は8192(以前は4096)とのこと

・起動パラメータ:あまり使わないので NULL にしておく

・タスク優先順位:(低)0<-->24(高),setup()やloop()の優先順位は1

・タスクハンドル:マルチスレッドなら設定すると良い。NULLでも良い
         TaskHandle_t thp[2];  //のように変数を作成し
         xTaskCreateUniversal(・・, &thp[0],1);  //などと使う

・Core ID:タスクの動くコアを指定。0か1

プログラムでの設定例
xTaskCreateUniversal(task1, "task1", 4096, NULL, 1, NULL, 0);



3) キュー(Queue, FIFO)の作成と利用関数

Pico (rp2040)ではコア間の通信用に専用のFIFO(First In First Out)があるが,ESP32ではFIFOのバッファとしてキュー(Queue)を作成してPicoと同様にコア間通信に使う。

(1) キューのハンドル名をグローバル領域で宣言
 例:ハンドル名  xQueue_1
 QueueHandle_t xQueue_1;

(2) setup()内でキューをハンドル名で作成・登録
 例:(データバッファ1, 1byte単位)
 xQueue_1 = xQueueCreate( 1, 1 );

(3) キューに送信
 例:(キューのハンドル名,送る変数のアドレス,待機時間 0ms)
 byte qvS=0;
 xQueueSend( xQueue_1, &qvS, 0 );

(4)タスクでキューにデータが来ているかをチェック
(5) データが来ていたら受信
 例:(キューのハンドル名,受け取る変数のアドレス,待機時間 0ms)
 byte qvR=0;
 if (uxQueueMessagesWaiting( xQueue_1 )>0){
  xQueueReceive( xQueue_1, &qvR, 0);
 }



以上,Raspberry Pi PicoとESP32のマルチタスクはやってみると案外素直に動いてくれました(^^)。

HUB75(E)LEDパネルのように,ずっと動き続ける必要がある場合などには有効な手段の一つだと思います。

ただ,Picoと異なりESP32はコア0をwifiなどの無線関係が使うとのことなので,そういう機能を使うプログラムとの両立はかなり考えないといけないようです。