はじめに
TL;DR:
-
Raspberry Pi Pico 2 と PC を pw_rpc (Pigweed) で繋ぎ、「gRPCのように型安全な」Lチカを実現しました。
-
Zephyr + Pigweedの組み合わせは超大変でした。。
続きも書いたので、こちらもご覧いただければと思います。
動機 & Pigweedについて
Pigweed という、Googleが開発している組み込みFW向けのライブラリ(実際にはフレームワーク)があります。
様々な機能がありますが、pw_rpcという機能を使うと、Protocol Buffers ベースのRPCサーバーをマイコン上で簡単に実現でき、gRPCのような感覚でPCと通信できます。
今回は、この pw_rpc を使って、PCから Raspberry Pi Pico 2 (RP2350) と通信し、Pythonから関数を叩いてLチカを制御してみました。
今回はReal Time OSに、Zephyr RTOSを使いました。
最初はFreeRTOS + pico-sdkを使おうとしたのですが、環境構築で力尽きました。
Zephyr RTOSについては、こちらのアドベントカレンダーを参照ください。
参考にしたRepositoryはこちらです。
システム構成
PC (Python) と Pico 2 (Zephyr) が USB シリアル経由で通信します。プロトコルには HDLC を使用しています。
今回はEchoとLED ON/OFFをサービスとして実装します。
実装
まずはこちらのように、モノを用意しました。
ご存知、Raspberry Pi Pico2(w)とLEDが接続されています。
RPC 定義
RPCはgRPCと同じように定義します。ただし組み込み用のProtocolBufferであるNanopbを使うため、特lに可変長配列を扱う場合は工夫が必要です。
syntax = "proto3";
package practice.rpc;
import "nanopb.proto";
message LedRequest {
bool on = 1;
}
message LedResponse {}
message EchoRequest {
string msg = 1 [(nanopb).max_size = 128];
}
message EchoResponse {
string msg = 1 [(nanopb).max_size = 128];
}
service DeviceService {
rpc SetLed(LedRequest) returns (LedResponse);
rpc Echo(EchoRequest) returns (EchoResponse);
}
最初は max_size を指定せずにビルドしたところ、C++側で生成された構造体のメンバーが char msg[N] ではなく pb_callback_t msgになってしまい、メッセージのデコード処理で詰みました。
Nanopb を使う場合、.optionsファイルまたはインライン記述でサイズ制限を設けるのが定石のようです。
環境構築
環境構築はWSL2 + Docker(devcontainer)で行いました。
PigweedはLinuxが必須のようです。
- Zephyr SDKのインストール
- Pigweedの環境構築
- PigweedにZephyrを投入
- ZephyrにPigweedを入れるのではなく、PigweedがZephyrを入れる形
で環境構築できました。
のDockerfileとReadMe.mdを参照ください。
pigweedは独自の環境を構築させることで確実なビルドを成功させるもののようです。
Device TreeとFWの実装
USBで外部と通信し、Raspi pico2のGPIO 5に取り付けたLEDを点灯させるために、app.overlayを下記の通り記載します。
/ {
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
pigweed,rpc-uart = &cdc_acm_uart0;
};
leds {
compatible = "gpio-leds";
led_gp5: led_gp5 {
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>;
label = "External LED on GP5";
};
};
aliases {
led0 = &led_gp5;
};
};
&uart0 {
status = "okay";
current-speed = <115200>;
};
&zephyr_udc0 {
status = "okay";
cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart";
status = "okay";
};
};
そして実装に移ります。
*実際にはZephyrやPigweedの設定が大量に必要です。。。
FWのサービスの実装
FW上でサービスを実装します。
class DeviceService
: public pw_rpc::nanopb::DeviceService::Service<DeviceService> {
public:
char msg_buf[128];
DeviceService() {
if (!gpio_is_ready_dt(&led)) {
return;
}
if (gpio_pin_configure_dt(&led, GPIO_OUTPUT_LOW) < 0) {
return;
}
}
::pw::Status SetLed(const ::practice_rpc_LedRequest& request,
::practice_rpc_LedResponse& response) {
if (request.on) {
PW_LOG_INFO("LED ON requested");
gpio_pin_set_dt(&led, 1);
} else {
PW_LOG_INFO("LED OFF requested");
gpio_pin_set_dt(&led, 0);
}
return ::pw::OkStatus();
}
::pw::Status Echo(const ::practice_rpc_EchoRequest& request,
::practice_rpc_EchoResponse& response) {
memcpy(response.msg, request.msg, sizeof(response.msg));
response.msg[sizeof(response.msg) - 1] = '\0';
PW_LOG_INFO("Echo requested: %d", (int)strlen(response.msg));
return ::pw::OkStatus();
}
};
} // namespace practice::rpc
USB通信処理も別途実装します。
そして今回はZephyrのwestコマンドでビルドします。
west build -p -b rpi_pico2/rp2350a/m33/w apps/pw_rpc
zephyr.uf2ができました。これをRaspi pico2に書き込みます。
Pythonクライアントの実装
SchemaからPythonの型定義を生成します。
protoc --python_out=./tools --pyi_out=./tools apps/pw_rpc/proto/service.proto
そこ(中略)から、service clientを生成したら、このようにRPCを実行できるようになります。
status, response = service.Echo(msg="Hello Pigweed!")
status, response = service.SetLed(on=True)
動作確認
単体スクリプト
PCとRaspi pico2をUSBで接続後、Pythonスクリプトを実行します。
python tools/client.py
Connected to /dev/ttyACM1
Sending Echo...
INFO:pw_rpc.callback_client:PendingRpc(channel=1, method=practice.rpc.DeviceService.Echo, call_id=1) completed: Status.OK
Echo Response: Hello Pigweed! 16
Turning LED ON...
INFO:pw_rpc.callback_client:PendingRpc(channel=1, method=practice.rpc.DeviceService.SetLed, call_id=2) completed: Status.OK
LED ON Success
Turning LED OFF...
INFO:pw_rpc.callback_client:PendingRpc(channel=1, method=practice.rpc.DeviceService.SetLed, call_id=3) completed: Status.OK
LED OFF Success
...
Echoで、Hello Pigweed!が返ってきています!
PCからのRPCでLEDもON/OFFしました!!!
*PCから0.5s毎にON/OFFしています。
追加: pw_console によるコンソール機能
Pigweedには、pw_consoleというコンソール機能があるので使ってみました。
ざっくり、下記のようにPwConsoleEmbedにほしい変数を渡して起動すればよさそうです。
console = PwConsoleEmbed(
global_vars={
**globals(),
"client": client,
},
local_vars=locals(),
loggers=loggers,
app_title="Pigweed RPC pw_console",
repl_startup_message='Type: client.Echo(msg="Hello") or client.SetLed(on=True)',
)
console.setup_python_logging()
console.embed()
こちらに実装してみました。
python tools/console.py
で実行すると下記のような画面が出て、Consoleからデバイスと通信できます。
-
RPCのコード補間が通るようになりました
*逆に言うと、pythonコード上ではコード補間ないのです。 -
ログとPython Repl結果が分離されて表示されます
デバイスのログも分離してキレイに見られるらしいですが、それは次の課題とします。
開発者用のデバッガにはなりそうですね。
そもそもの話
苦労した過程は基本省略していますが、あまりにも大変でした・・・
- 構成設定にはKconfig、CMake、Device Treeが必要なのだが、自動補完がない or 弱い
- 上記の構成に対するエラーの意味がパッと見でわからない
- Zephyr, Pigweed共に仕様が頻繁に変わっているためか、AI検索でも誤情報を掴まされる Vibe Codingなんて夢のまた夢
- 業務に使うなら、Zephyr + Pigweedを理解するのための専門部隊が必要なレベル? 本末転倒では??
Zephyrはしんどいです。でも、Pigweedのほうがもっとしんどいです。
なぜここまで苦労してPigweedを使うのか??
それは、『通信仕様書』がそのまま『動くコード』になるからです。
Excelでポチポチ通信パケットの仕様を書く代わりに、.protoを1枚書くだけで、型安全なコードが生成されるというWebと同じ堅牢な開発ができます。
Protocol BufferとNanopbだけ使って、自作すればよくね?
なぜここまで苦労してZephyrを使うのか
今回Zephyrを使った理由は下記です。
- 究極のハードウェア抽象化 (DeviceTree)
HWをDevice Treeで抽象化させることで、LEDを光らせるロジックは gpio_pin_set_dt(&led, 1) で共通です
HAL呼ぶとこだけ書き直せばよくね?
- Pigweedとの相性の良さ
実は、GoogleのPigweedチームもZephyrを公式にサポートしており、相性は抜群です。Zephyrが持つ強力なドライバ層の上に、Pigweedを載せることで、「最強のOS」×「最強のミドルウェア」 という布陣が完成します
Google主導で、Github Star数500以下って採用して大丈夫?
まとめ
Zephyr + Pigweedというモダンな次世代のファームウェア開発環境を構築して、gRPCのようにスキーマがしっかり決まった通信を行いLチカをすることができました。
今回はLチカとUnary RPCだけでしたが、今後は、
-
Server Streamingによる常時通信
-
pw_logとLogのtokenizer による、効率的なログ出力
といった、様々な機能を使って、よりモダンなファームウェア開発が 気力が続けば できればと思います!
ZephyrとPigweed成長してマイコンにもLinuxのようなスマートな開発環境ができるといいですね!


