鳥なき里のマイコン屋(133) ラズパイPico、C/C++SDKでマルチコアもお手軽

Joseph Halfmoon

別シリーズのMicroPythonの方でマルチコアやっていたので、C/C++SDKでもやった気になっていました。仕切り直し。C/C++SDK環境(VS Codeでリモート接続)でマルチコアを動かして、デバッガを使ってみます。お手軽にデバッグできるんだが、腑に落ちない挙動が一つ。概ねOKだから、また今度ね。いい加減な。

(末尾に実験で使用したコード全文を掲げました。)

ラズパイPicoは、Armコアを2個搭載のマルチコア・マイコンです。といってもラズパイ4のようなマルチコアでの並列コアでプログラム処理性能UP狙いとは違い、より低いレベルでの組み込み向けに2個のコアで仕事を分担、という感じでしょうか。

Core0と呼ばれるコアがまず走っていてmain()関数を実行、Core0は必要に応じてCore1を起動して一部の仕事をお任せするようなスタイルかと。別シリーズのMicroPythonネタでマルチコアが簡単に使えることは分かっておりましたが、C/C++SDKではどうでしょうか。

SDKの中では、High Level APIに位置付けられる以下のライブラリを使用することでマルチコア、簡単に使うことができます。

pico_multicore

とりあえずCore1を起動して処理すべき関数を渡すだけであれば、以下の関数でことたります。

multicore_launch_core1()

コア間のやり取り不要であれば究極的には上記でたりますが、さすがにそういうことはないでしょう。リソースの使用や同期のためにlock、mutex、semaphoreといった一般的な排他制御の仕組みがSDKに存在します。しかし、ラズパイPicoの特徴として、コア間通信のためのハードウエアFIFOが存在するのです。これを使えば、「軽く」コア間通信が可能です。基本はブロッキングですが、FIFOのステータス確認用の関数があるので、実質ノンブロキングで使うことも可能です。

ラズパイPicoはOSなしですが、SDKのサポートは充実、面倒なことは皆SDKに任せてしまって簡単コーディングが可能と思われます。そこでコア0からコア1に一部のお仕事をオフロードするような雰囲気(実際になにもしやしないですが)のサンプルを書いて動かしてみました。

そこでちょっと気になるのがデバッガの使い易さです。当方でのC/C++SDKによるラズパイPico向け開発の道具だては以下の構成です。

  • Raspberry Pi 4 を開発母艦としている。ツールチェーン、ビルドツリーはPi 4上にある。
  • Pi 4とラズパイPicoは SWDで接続(他にUSBでも接続、UARTも接続可)
  • Pi 4の開発環境には、PC上のVS Codeからリモート接続している

他のマイコンでは、マルチコアのデバッグが少し難儀なものもあるので、ラズパイPicoの場合はどんなんでしょうか?結論から言えば、

マルチコアだからといってデバッグに特に面倒なことはない

と思います。ほとんど何も考えずにリモートのVS Code上のGUIでマルチコアのデバッグが可能です。デバッグを開始したところが以下に。

vscodeCapture00起動するとmain()関数の冒頭でブレークがかかります。main()関数のローカルスコープの中の変数が列挙されていますが、まだ有効になる前です。ここでコールスタックを見てみると以下のような感じ。core0側はmain()内のブレークポイントで止まっており、core1(まだ処理を起動していない)も停止していることが分かります。

ここで、core1側で実行するコード部分にブレークポイントを設定して、実行継続してみます。core0側からcore1側のコードを起動し、core1の関数の途中でブレークにかかって止まったときのコールスタックはこんな感じ。

Core1breakこんどは、core1側はブレークポイントでとまり、それと「連動」してcore0側も停止していることが分かります。コールスタックをみれば、core0はcore1側を待つところで止まっています。ここは予定どおり。

core1側のブレークポイントなので、ローカル変数はcore1側の関数の変数が見えてます。これまた期待通り。

Core1Localここまでのところ、楽だな~

ちょっとしたトラブル?

マルチコアの起動も、FIFOを使ったコア間通信も簡単で、特に問題もなくマニュアル通りに使えたのですが、1点不可解な現象が発生。Raspberry Pi 4とラズパイPicoをUSB接続しているのですがRaspberry Pi OS上に

/dev/ttyACM0 が見当たりません。

/dev/ttyACM0は、USBシリアルのデバイスです。ラズパイPicoのオブジェクトコードのSTDIOをUSBシリアルに向けていたら見えるはずなのに見当たりません。以前からラズパイPicoのSTDIOはUSBシリアルに向けて使っており、printfの出力は/dev/ttyACM0で拾っていたので困りました。

以前生成した別なプログラムの uf2 ファイルをBOOTSELモードで書き込むと /dev/ttyACM0 が現れます。勿論printfから送られてくる文字列を受信可能です。何が問題?ちゃんとCMakeList.txtはSTDIOをUSBに向けるようにしてあるのだけれど。。。

ちょっと調べて分からなかったので、「また後で」ということで調査はペンディング、STDIOを通常のUART0(GP0/GP1)に接続するように変更してしまいました(根性無。)

元々Raspberry Pi 4のUART0(GPIO14/15)にはシリアルケーブルを取り付けてあります。RX/TXの「クロス」に気をつけてラズパイPicoと接続しました。

そのためのCMakeList.txtの変更を以下にしめします。なお、target_link_libraries()で、pico_multicoreを設定しておかないと、今回使用のAPIは見えないようです。

pico_enable_stdio_uart(multTST0 1)
pico_enable_stdio_usb(multTST0 0)

target_link_libraries(multTST0 pico_stdlib pico_multicore)

末尾のプログラムを走らせたところがこちら。やっているのは、

  1. Core0からCore1を起動する
  2. Core1は「起動したよ」とメッセージをCore0に送る(以下すべてFIFOを使った通信)
  3. Core0は上記の起動メッセージをもらったら仕事を開始
  4. Core0からは仕事のトリガとして1を送る
  5. Core1はトリガの1を受け取ったら、受信したこと、受信の積算回数を報告(本当はここで何か仕事をする)
  6. Core0から仕事終わりの指令として2を送ると、Core1はそれに反応して「仕事あがり」のメッセージをCore0に送り返す
  7. Core0は上記の「あがり」メッセージを受け取ったら終了
CORE0: start.
CORE0: Send 1
CORE1: Received 1 (1)
CORE0: Send 1
CORE1: Received 1 (2)
CORE0: Send 1
CORE1: Received 1 (3)
CORE0: Send 1
CORE1: Received 1 (4)
CORE0: Send 1
CORE1: Received 1 (5)
CORE0: Send 1
CORE1: Received 1 (6)
CORE0: Send 1
CORE1: Received 1 (7)
CORE0: Send 1
CORE1: Received 1 (8)
CORE0: Send 1
CORE1: Received 1 (9)
CORE1: IDLE. 
End of multicoreTST0.

ま、マルチコアのサンプル動いたね。でもシリアルの件、気になるのだけれど。。。

鳥なき里のマイコン屋(132) ラズパイPico、何気に便利なpicotool に戻る

鳥なき里のマイコン屋(134) ラズパイPico、C/C++SDKでプログラマブルIO に進む

実験コード全文
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"

#define HELLO_MSG   (99999)
#define EXIT_MSG    (99998)
#define MAX_LOOP    (10)
#define DO_COMMAND  (1)
#define EXIT_LOOP   (2)

void core1_start() {
    uint32_t p1Counter = 0;
    multicore_fifo_push_blocking(HELLO_MSG);

    while (true) {
        uint32_t rcvDat = multicore_fifo_pop_blocking();
        if (rcvDat == EXIT_LOOP) break;
        printf("CORE1: Received %d (%d)\r\n", rcvDat, ++p1Counter);
    }
    printf("CORE1: IDLE.\r\n");
    multicore_fifo_push_blocking(EXIT_MSG);
    while (true) {
        tight_loop_contents();
    }
}

int main()
{
    uint32_t p0Counter = 0;
    uint32_t command = DO_COMMAND;
    stdio_init_all();
    printf("CORE0: start.\r\n");
    multicore_launch_core1(core1_start);
    uint32_t hello = multicore_fifo_pop_blocking();
    if (hello != HELLO_MSG) {
        printf("Unexpected CORE1 HELLO MESSAGE.\r\n");
        return 1;
    }

    while(p0Counter++ < MAX_LOOP) {
        while (!multicore_fifo_wready()) {
            printf("Waiting CORE1.\r\n");
            sleep_ms(100);
        }
        if (p0Counter == MAX_LOOP) {
            command = EXIT_LOOP;
        }
        printf("CORE0: Send %d\n\r",command);
        multicore_fifo_push_blocking(command);
        sleep_ms(2000);
    }
    hello = multicore_fifo_pop_blocking();
    if (hello != EXIT_MSG) {
        printf("Unexpected CORE1 EXIT MESSAGE.\r\n");
        return 1;
    }
    //multicore_reset_core1();
    puts("End of multicoreTST0.");
    return 0;
}