チップの上で — ファームウェアはC、画面は定義で

6 分
目標評価ボードに画面を付けるには普通、グラフィックススタックを載せてファームウェアを焼き直す。その代わりにボードが自分の能力を道具として、自分の画面をリソースとして差し出すようにした。机の上の STM32H723 に繋いで LED を点け、ボードが渡してきた 656 バイトの画面をそのままレンダーした。

チップを作る会社はチップだけを売らない。チップと一緒に 評価ボードを出す。「うちのチップはこういうことができます」と手に握らせる小さな基板だ。

そのボードに画面を付けた瞬間、仕事が大きくなる。グラフィックスライブラリを載せ、フォントを入れ、タッチドライバを繋ぎ、レイアウトを組み、その全部をフラッシュに焼く。ボタンの位置ひとつ変えるのにまた焼く。だからたいていの評価ボードは 画面無しで出て行き、画面が要る顧客は別の会社を探す。

この記事はそれを違うやり方でやる。ボードに GUI を載せない。 代わりにボードが二つを差し出す — 自分にできることの一覧と、それを見せる画面の定義。描くのは外だ。

机の上の WeAct STM32H723 ボードで実際にやった。

ボードが差し出すもの

USB シリアルで繋いで問い合わせると、こう答える。

[serial] connected via serial_bridge /dev/cu.usbmodem365D395E33331 115200
[serial] tools: led.set, sys.info
[serial] resources: ui://app, ui://app/info, bundle://manifest.json

道具二つ、リソース三つ。 これがこのボードの表面の全部だ。

道具はこういう形をしている。

{"tools":[
  {"name":"led.set","description":"Turn the on-board LED on or off",
   "inputSchema":{"type":"object","properties":{"on":{"type":"boolean"}},"required":["on"]}},
  {"name":"sys.info","description":"Report LED state and uptime",
   "inputSchema":{"type":"object","properties":{}}}]}

led.set と sys.info。このボードはほかにもできることが多い — フラッシュを消し、クロックを変え、ブートローダに落ちられる。その能力は一覧に無く、一覧に無ければ呼ぶ名が無い。

画面もボードが持っている

ui://app を読むと画面定義が出てくる。

[serial] ui://app — 656 B, type "page", title "WeAct H723 MCP Node"

656 バイト。 画面ひとつでその程度だ。クライアントにはこのボード用の UI コードが一行も無い — 受け取って描く。

WeAct H723 MCP Node — ボードが渡した定義をランタイムが描いた画面

押せばボードが反応する

画面のボタンが led.set を呼ぶ。実際の往復だ。

[serial] led.set({"on":true})  -> "LED on"                      (1 ms)
[serial] sys.info({})          -> "LED=on uptime=205491208ms"   (10 ms)
[serial] led.set({"on":false}) -> "LED off"                     (10 ms)
[serial] sys.info({})          -> "LED=off uptime=205491232ms"  (11 ms)
[serial] uptime advanced 205491208 -> 205491232 ms

机の上のボードの LED が点いて消えた。 そして最後の行を見てほしい — uptime が 205491208 から 205491232 へ、24 ミリ秒前進している。あらかじめ入れておいた答えではなく、その瞬間ボードが数えていた値だ。録画した応答なら二度問えば同じ数字が出る。

往復は 1〜11 ミリ秒。UART の上だからだ。

同じクライアントが別のチップにも繋がる

これがこの構造の値打ちだ。同じコードで Wi-Fi 越しの ESP32 に繋いだ。

[tcp] connected via tcp_bridge mcp-esp32.local 6270
[tcp] tools: led.set, sys.info
[tcp] ui://app — 158 B, type "application", title "ESP32 MCP Node"
[tcp] application — initialRoute "/" -> ui://page/main
[tcp] ui://page/main — 1894 B
[tcp] led.set({"on":true}) -> "LED on"  (89 ms)
[tcp] uptime advanced 67004328 -> 67004391 ms

ESP32 MCP Node — 別会社のチップ、別の伝送、同じクライアント

別会社のチップ、別の伝送、同じクライアント。 変わったのはブリッジプログラムひとつだ。

そして二台のボードで画面の構造が違う。STM32 はページひとつ(type: "page"、656 B)を、ESP32 はルートを持つアプリケーション(type: "application"、158 B + ページ 1894 B)を差し出す。ボードが自分の画面の形を自分で決めている。

遅延は違う。UART が 1〜11 ms、Wi-Fi TCP が 17〜89 ms。桁が違い、その差が UI 設計を分ける — 一桁の側は「押せばすぐ」を期待していいが、数十ミリ秒の側は待ち状態を画面に描かねばならない。

伝送はプログラムひとつだ

クライアントがシリアルを知らなくて済むようにした方法だ。伝送を 別プログラムに出した。

mcp_client  ──stdio──▶  serial_bridge  ──UART──▶  STM32H723
mcp_client  ──stdio──▶  tcp_bridge     ──TCP───▶  ESP32

ブリッジは行単位の JSON をそのまま流す。シリアル側は cfmakeraw で端末加工を切り、TCP 側は TCP_NODELAY で短い要求がバッファに溜まらないようにする。

/* 同じ規則:JSON の行だけがプロトコルだ。ボードが同じストリームに
 * メモを打っても、クライアントが詰まってはいけない。 */
if (up[0] == '{') { printf("%s\n", up); fflush(stdout); }
else              { fprintf(stderr, "[board] %s\n", up); }

伝送を選ぶ仕事が、どのプログラムを起動するかに縮んだ。 クライアントは一バイトも変わらない。

作る途中でクライアントが仕様に違反した

実機に繋いだ途端 listResources() が即死した。

type 'Null' is not a subtype of type 'String'

ボードがリソースを description 無しで返したのに、mcp_client 1.1.1 がそのフィールドを非 null にキャストしていた。MCP 仕様上 description は任意で、ボードが正しかった。 2.1.0 で既に直っていたので、そのバージョンに上げて解決した。

シミュレータだけで回していたら出なかった欠陥だ。自作のシミュレータなら必ず description を埋めて送っていただろうから。

検証

$ bash verify.sh
   [1/6] bridges (C)
   [2/6] serial board?  /dev/cu.usbmodem365D395E33331 -> WeAct H723 MCP Node
   [3/6] network board? (mDNS _mcp._tcp)  ESP32 MCP Node -> mcp-esp32.local:6270
   [4/6] probe + real render
   [5/6] checks
   2 board(s) · 2 transports · LED round-tripped on both · uptime advanced on both

このサンプルにシミュレータは無い。ボードが繋がっていなければ どこを探したかを言って失敗する。

[ -n "$SERIAL" ] || echo "   none found on /dev/cu.usbmodem* or /dev/cu.usbserial*"

ハードウェア無しで通るハードウェア検証は検証ではないからだ。

このボードで何を売るのか

評価ボードに画面を付ける費用が、グラフィックススタック+再ビルドから 道具二つ+定義 656 バイトに変わった。

チップ会社にとってこれがなぜ違うのか。画面を付けるために認証済みのファームウェアを焼き直さなくていい。顧客が画面を変えたいと言ったときにファームウェアを出し直さなくていい。画面は外で描かれ、ボードは自分にできることだけを言う。

灌水コントローラでも温室ゲートウェイでも、繋ぐ側が変わってもボードの言うことは同じだ。

自分で動かす

cd serial_bridge && cc -O2 -o serial_bridge serial_bridge.c && cd ..
cd tcp_bridge    && cc -O2 -o tcp_bridge    tcp_bridge.c    && cd ..

bash verify.sh          # find board → connect → tools/resources → open it in AppPlayer

持ち帰るもの

  1. ブリッジ二種 — serial_bridge.c ・ tcp_bridge.c。そのままコンパイルすればどのクライアントにも繋がる
  2. ボード側の最小応答 — initialize・tools/list・resources/read の三つで画面が出る
  3. ハードウェア検証スクリプト — ボードが無ければ通さない verify.sh

自分のボードに合わせるには — ファームウェアの道具名と ui://app の JSON だけを自分のものに変える。ブリッジもクライアントも触らない。伝送が違えばブリッジを選ぶだけだ。

練習課題

例にデバイスをもうひとつ追加してください。変更したファイルをすべて挙げてください。ホストがその中にあってはいけません。

関連記事On a Chip — Firmware in C, the Screen as a Definition