コンテンツにスキップ

CPU ファームウェア

CPU ボード (AP-RZT-01, Alpha Project) は Renesas RZ/T1 (Arm Cortex-R4F) を搭載している. これの役割は, ホストから UDP (IPv6 リンクローカル) で受け取ったフレームを解釈し, FPGA 内の BRAM へ書き込むことである. RZ/T1 内蔵の Ethernet スイッチ (ETHSW) の 2 ポートを使い, デバイスをデイジーチェーンで接続する.

ソースは firmware/cpu/ 以下にあり, no_std Rust で記述されている. ファームウェア本体は 2 つのクレートに分かれており, ホストとの通信の取り決め (コマンド ID・ペイロードやヘッダのレイアウト・エラーコード・既定値など) はクライアントと共有するクレート wire/ (autd3-cpu-wire) に置かれている.

  • fw/ (autd3-cpu-fw): プロトコル解釈・コマンド処理・FPGA 書き込み, および IPv6 / UDP / ICMPv6 の解析と組み立て (net), PTP のメッセージとサーボ (ptp) などの可搬ロジック. ハードウェアには Port trait (コマンド層) 越しにのみアクセスし, unsafe を含まない. ホストテストと autd3-rs-firmware-emulator からも同じコードが使われる.
  • board/ (autd3-cpu): 実機ターゲット (armv7r-none-eabi) の staticlib. レジスタ定義・BSP・ETHSW / GMAC のドライバ・Port trait の実機実装・列挙と受信振り分け (udp/node.rs)・PTP の状態機械 (udp/ptp.rs)・main・panic handler を持ち, unsafe はここに閉じ込める.

CPU ファームウェアに関する作業はすべて autd3-sdk/ ディレクトリ内で cargo xtask 経由で実行する.

cargo xtask cpu build # 実機用 .bin を生成
cargo xtask cpu flash # build 後, firmware-writer (J-Link) で実機へ書き込む
cargo xtask cpu test # ホスト上の単体テスト (cargo test -p autd3-cpu-fw)
cargo xtask cpu test --loom # 受信割り込みとメインループの FIFO の受け渡しを loom でモデル検査する
cargo xtask cpu lint # clippy (fw + board)
cargo xtask cpu format # rustfmt --check (fw + board)
cargo xtask cpu format --fix # rustfmt 実書き換え
用途 ツール
実機ビルド Rust ツールチェイン + rustup target add armv7r-none-eabi, rustup component add llvm-tools (rust-lld, llvm-objcopy)
実機書き込み J-Link Commander (JLinkExe)
ホストテスト Rust ツールチェイン (cargo test)
lint / format clippy, rustfmt

JLinkExe が PATH に無い場合は環境変数 JLINK でパスを指定できる.

cargo xtask cpu build は以下を行う.

  1. cargo xtask cpu gen-param で FPGA の params.svh から fw/src/fpga_params.rs (と wire/src/fpga_params.rs) を生成する.
  2. board/ を armv7r-none-eabi ターゲットでビルドする. アプリの staticlib (libautd3_cpu.a) とローダの実行ファイル (autd3-cpu-loader) ができる.
  3. rust-lld でリンカスクリプト platform/autd3-cpu.ld を用いてアプリをリンクする.
  4. llvm-objcopy でローダとアプリをそれぞれバイナリにし, ローダを先頭, アプリをスロット A に置いて build/autd3-cpu.bin に合成する.
  5. スロット A のヘッダにアプリの長さと CRC32 を書き込む.

ローダ (board/src/bin/loader.rs)・スタートアップ (board/src/startup.rs)・シリアルフラッシュドライバ (board/src/sflash.rs) も Rust で書かれている. ローダはブート ROM が BTCM に展開して実行するプログラムで, 起動するスロットを選んで ATCM にコピーする. スタートアップは board/src/lib.rs の main を呼び, 以後の初期化とメインループを行う.

cargo xtask cpu flash は tools/firmware-writer に --cpu build/autd3-cpu.bin を渡して呼ぶ. writer は J-Link を JTAG 接続し, autd3-cpu.bin を 0x30000000 へロードする. 書き込み後は電源を入れ直して起動する. デバイスは R7S910018_R4F として認識される.

fw/ は Port trait (fpga_write / fpga_read / memory_barrier など) 以外のハードウェア依存を持たない. cargo xtask cpu test では fw/src/test_utils/mock.rs の MockPort がこれを置き換え, FPGA への書き込みを記録する. fw/src/app と fw/src/cmd の各モジュールのテストがプロトコル層 (Cpu::recv_frame / Cpu::process_one) と各コマンドの振る舞いを, fw/src/net と fw/src/ptp のテストがパケットの解析・組み立てと PTP のサーボを検証する.

ホストは送るものがあるときに, 各デバイスへ 1 つの UDP データグラム (デバイス側ポート 44336) を送る. データグラムは 4 byte のヘッダ (版数・種別・メッセージ ID) の後ろに最大 1448 byte のフレームを持つ. 送るものが無い間は, TransportOption::heartbeat_interval を指定して開いた Client からフレームの代わりに生存確認 (Heartbeat) が周期的に届く (read_state も 1 回送る).

受信フレームは先頭が予約 (1 byte, ホストは 0 を送り, デバイスは読まない), cmd (1 byte), exec_at (4 byte) で, 残り (最大 1442 byte) がペイロードである. exec_at が 0 でなければ, メインループはデバイス時刻がその時刻 (µs) に達するまでそのフレームの処理を待つ. ペイロードの長さはデータグラムの長さだけで決まり, ペイロードの中に長さのフィールドは無い. デバイスは受け取ったペイロードを長さごとコマンド層 (Cpu::recv_frame) へ渡し, 各コマンドはその長さを検査する (固定長のコマンドは完全一致, 可変長のデータはヘッダの後ろの残り全部). 各フレームの扱いを終えた時点で, status (エラーコード, 1 byte)・読み出し値 (最大 40 byte) を載せた返信を 1 回送る. 返信にはこのほか, デバイスの状態フラグ・ID・デバイスの時刻が載る. 返信の先頭 1 byte は予約 (0) である. ホストは返信をデータグラムのヘッダのメッセージ ID (msg_id) だけで送ったフレームに対応づける.

デバイスは届いたフレームを届いた順にすべて処理する. シーケンス番号による欠落検出・順序保護・重複排除は持たないので, 途中のフレームが欠けても後続のフレームはそのまま処理され, 同じフレームが 2 回届けば 2 回処理する. 前のフレームの成否に依存するフレームは, 前のフレームの返信を待ってから送るのがホスト側 (呼び出し側) の責務である.

受信割り込みはフレームを深さ 8 のリングバッファ (FIFO. 同時に積めるのは 7 フレーム) に積むだけで抜け, メインループが処理して返信する. コマンドを受信割り込みの中で処理する経路は無い. フレームと Heartbeat のうち受信割り込みの中で返信するのは Heartbeat だけで, その返信は直前に処理したフレームの結果 (スロットに残した返信) を写す. Heartbeat への返信は割り込みの中で返るので, メインループが長い処理で止まっていても返り続ける. FIFO に積んだフレームはメインループが処理した後に, 割り込みを禁止した区間で返信する.

フレームの処理経路

メインループは待ちを挟まずに FIFO を見続けるので, FIFO を通すことによる遅延は 1 フレームあたり 10 us 未満である (実測).

FIFO が満杯の場合, フレームは捨てられ (fifo_drop に数える), 返信も送らない. 返信が来ないため, クライアント側では Timeout になり, 呼び出し側がコマンドを送り直す (クライアントは 7 フレームまでしか同時に送らないので, 通常は溢れない).

handle_frame はコマンドを実行し, エラーがあればエラーコードを返信の status に載せる. コードはデバイス側に保持せず, 回数も数えない (エラーの回数はホストが返信の status から数える).

コード 名前 意味
0x00 ERR_NONE エラーなし
0x01 ERR_UNKNOWN_CMD 未知のコマンド
0x02 ERR_INVALID_PAYLOAD ペイロードの長さ・オフセットが不正
0x03 (欠番) 再利用しない
0x04 ERR_INVALID_SILENCER_SETTING Silencer の設定が厳格モードの条件を満たさない
0x05 (欠番) Firmware v0.14.x までの ERR_INVALID_TRANSITION_MODE. 遷移モードと繰り返し回数の組み合わせは検査しない
0x06 (欠番) Firmware v0.14.x までの ERR_MISS_TRANSITION_TIME. SysTime 遷移の時刻は検査しない
0x07 ERR_FPGA_TIMEOUT FPGA が制御フラグを期限内にクリアしない
0x08 ERR_SYNC_NOT_READY 同期パルスがまだ動いていない (時刻合わせ前)
0x09 (欠番) Firmware v0.9.0 までの ERR_INVALID_SYNC0_CYCLE. 同期パルスの周期はファームが決めるので発生しない
0x0A ERR_UPDATE_NOT_STARTED 更新セッションが開いていない (UpdateBegin が必要)
0x0B ERR_UPDATE_IMAGE_INVALID 書き込んだイメージの読み戻し CRC32 が一致しない
0x0C ERR_UPDATE_FLASH シリアルフラッシュの消去・書き込み・読み出しが失敗した
0x0D ERR_UPDATE_NOT_COMMITTED Commit 済みのイメージが無い
0x0E ERR_UPDATE_NOTHING_TO_CONFIRM 確定の対象 (今回起動したイメージ) が不明, または書き直されている
0x0F ERR_UPDATE_UNSUPPORTED 稼働中の FPGA が自身のコンフィギュレーションフラッシュを書けない
0x10 ERR_FPGA_UPDATE_IN_PROGRESS FPGA 更新のロック中に出力系コマンドを受けた
0x12 ERR_UPDATE_ACTIVATING Reboot 受理後のリセット待ちの間に更新の開始を受けた
0x14 ERR_SYNC_MISSED Synchronize の書き込みが同期パルスの立ち上がりに間に合わなかった. 送り直せばよい
0x15 ERR_FAILSAFE_CONDITION_ACTIVE ReleaseFailsafe を受けたが, フェイルセーフの発火条件がまだ成り立っている

FPGA は CS1 空間 (0x44000000) にマップされた 幅のメモリとして見える. Port trait の fpga_write / fpga_read (実機実装は board/src/port.rs の HwPort) がこの領域への volatile アクセスを担う.

fpga::write(select, addr, value) は select (BRAM 選択, 2 bit) と addr (14 bit) を連結してアドレスを作る. 詳細は Memory を参照されたい.

設定値の反映は制御フラグ経由のハンドシェイクで行う. set_and_wait_update(flag) が ADDR_CTL_FLAG の該当ビットを立て, FPGA の Controller がそれをクリアするまでポーリングする.

FPGA の同期入力 (SYNC_IN ピン) には, ETHSW のタイマから作った同期パルスを入れる. タイマの SYNCOUT ( 周期) から ELC → MTU → PPG を経て, 立ち上がりが 境界に乗る 周期のパルスを作る.

ETHSW のタイマはデバイス間で PTP (IEEE 1588, L2, E2E, two-step) によって揃える (状態機械は board/src/udp/ptp.rs, メッセージとサーボは fw/src/ptp). 以下の周期・閾値・回数は既定値であり, SetCpuConfig で変えられる.

  • ID 0 のデバイスが grandmaster (GM) になる. ホストは Client を開くときに (TransportOption::sync_on_open が真のとき) GM にだけ時刻を送る (SetTime. 送る値は 0 で, デバイスの時刻は開いた時点から数える). GM 以外は SetTime を NotGrandmaster で拒否する
  • GM は ごとに下流ポートへ Sync / Follow_Up を送り, Delay_Req に Delay_Resp で答える. 中継する各デバイスの ETHSW が Transparent Clock として滞留時間を correctionField に足す
  • 他のデバイス (slave) は上流ポートから届く Sync と自分の Delay_Req から GM とのずれを求め, 固定小数点の PI サーボでタイマの周波数を補正する. ずれが を超えるとタイマをステップする
  • ずれが 未満の状態が 64 回続くとロックとみなし, 同期パルスを張り直す. ステップするとロックを外して同期パルスを止める
  • PTP の処理は受信割り込みと 1 ms のタイマ割り込みだけで行うので, メインループが止まっても (フラッシュの消去など) 同期は保たれる

Synchronize は以下を行う.

  1. 次に同期パルスが立ち上がるデバイス時刻 (現在時刻から 以上先の最初の 境界) を求める.

    • 同期パルスがまだ動いていない場合は ERR_SYNC_NOT_READY を返す
  2. デバイス時刻がその立ち上がりの 前を過ぎるまで待つ (最長 ).

    • FPGA はフラグを受けたあと最初の立ち上がりで SYS_TIME をセットするので, 1 つ手前の立ち上がりより前にフラグを立てると SYS_TIME が 進む
  3. 求めた立ち上がりの時刻を FPGA 内部の SYS_TIME の単位 () へ変換し, ADDR_SYNC_TIME_* へ書き込む.

  4. SYNC_SET フラグを立て, FPGA の Synchronizer が SYS_TIME をセットするのを待つ.

  5. デバイス時刻を読み直し, 書き込んだ立ち上がりより手前であることを確かめる.

    • フラグが立ち上がりより後に届くと SYS_TIME が 遅れる
    • 手前でなければ ERR_SYNC_MISSED を返す. やり直しはしない (受け取ったホストが送り直す)

CPU ファームは遷移モードと繰り返し回数の組み合わせも, SysTime 遷移の時刻も検査せず, そのまま FPGA へ書く. FPGA は, 無限ループ (REP == 0xFFFF) のバンクへはどの遷移モードでも即時に切り替える. 有限ループのバンクへは SyncIdx / SysTime / Gpio の条件を待って切り替え, Immediate / Ext では切り替わらずに待ち続ける. SysTime の時刻が過ぎていれば即時に切り替える.