設計上の教訓
このプロジェクトのソースコードのコメントや開発記録に、実際に踏んだ問題として残っているものを、テーマ別に集めました。
似た車を作る人が同じ穴に落ちないためのチェックリストとして読んでください。
各項目の末尾に出どころ(ファイル)を書いています。
【設計の説明】
確認した版: 上位 19291eb、ファーム 2a2e9a8。「実機で起きた」と書いてあるものは、コード中のコメントや開発記録にそう書かれているものです。
安全・リアルタイム制御
| 教訓 |
何が起きたか/なぜか |
出どころ |
| 「送信を止めて惰行」は後退する |
MD は通信が途絶えても 0.5 秒は直前のトルクを出し続ける。減速中の負トルクが残って停車後に後退する。0 トルクを送り続ける |
drive.c の Coast |
| 緊急停止で駆動電源を切らない |
MD が制動できず惰行に入り、止まるまでの距離が伸びる。電源遮断は「流し続けること自体が危険」な過電流のときだけ |
vehicle.c |
| ARM を外したらすぐ電源を切らない |
同上。走行中に外れたら停車まで最大制動してから切る |
vehicle.c の UpdateDisarmBraking |
| MD の起動前に車速 PI を回さない |
MD は給電から約 1 秒指令を受け付けない。その間に積分が振り切れ、動き出した瞬間に最大トルクが出る |
vehicle.c |
| トルク上限は MD の絶対上限を超えない |
0.15 N·m を設定したら MD が受け入れず、ARM の 5 秒後に md_fault でラッチして走れなくなった |
drive.h |
| IWDG は初期化が全部終わってから起動 |
一度起動すると止められない。ブロッキング初期化中にリセットが掛かる。裏を返すと Setup() 中のハングは検出できない |
app.c |
| I2C が張り付くとハートビート監視まで止まる |
メインループが長く止まると、直後にエッジを取りこぼして誤って緊急停止をラッチしうる。長く止まったときだけ基準時刻をリセットする |
app.c |
| 外部割り込みのラインを共有していないか確認 |
RAS_SIG(PB12)と ECHO_FRONT(PA12)は同じ EXTI ライン 12。前者はポーリングにするしかなかった |
ファーム CLAUDE.md |
Serial_Write は進行中の DMA を中断する |
Pi 向けには非同期版を使う。MD 向けの 1 回の送信(6 バイト = 60 µs @ 1 Mbps)は次の送信までに完了させる |
ファーム CLAUDE.md |
| ボーレートは相手と同時に変える |
片側だけ変えると全フレームが化ける |
ファーム CLAUDE.md |
| 原因不明の回避策は、不明だと書いておく |
LiDAR の左右鏡像補正を STM32 側でやると、上位で通信エラーが増発した。原因は未解明で、補正は上位に置いたままにしている |
ファーム CLAUDE.md |
通信・内部バス
| 教訓 |
何が起きたか/なぜか |
出どころ |
| 指令の発行元は 1 つ |
2 つのノードが同じ cmd に publish すると、購読側からは区別できず 50 Hz で交互に上書きし合う。「勝手にハンドルが戻る」になり、止める側が負けることがある |
planning_node.py |
| ZMQ の CONFLATE は multipart 非対応・ソケット単位 |
トピックを 1 本の SUB にまとめて CONFLATE を掛けると、LiDAR が来るたびに車両状態が消える。見た目は動くので気づきにくい |
bus/zbus.py |
| ipc の bind は二重でもエラーにならない |
2 つ目が黙って endpoint を奪い、「動いているのにデータが来ない」。排他ロックで起動を拒否する |
bus/zbus.py |
| トピック名の前方一致に注意 |
scan/cam が scan の前方一致に誤って拾われ、ftg_cam が動かなかった |
bus/zbus.py の TOPIC_OWNER |
| 共有メモリが作り直されたことを検出する |
書き手を再起動すると、既に attach している読み手が古い領域を掴んだまま、同じ画像を延々と処理し続ける(エラーも出ない) |
core/jpeg.py、core/frame_reader.py |
systemd の RemoveIPC=yes が共有メモリを消す |
そのユーザーの最後のセッションが終わると全消しされる。実際に起きた。RemoveIPC=no にしてある |
core/jpeg.py |
| ARM64 の弱いメモリモデル |
seqlock は理屈の上で壊れうる。Python ではロックで代用しており、「証明された」とは言えない。段数を十分取るのが実効的な守り |
bus/shm_ring.py |
| Wi-Fi の瞬断で走行中に DISARM した |
メッシュ Wi-Fi の中継機の切り替え(2026-10-06)。自律走行中だけ待ち時間を 5 秒に延ばした |
vehicle.toml の [safety] |
Wi-Fi 端末は .local が引けないことがある |
無線端末に mDNS のマルチキャストが届かない。Pi がユニキャストで返すが、Pi の IP が変わると Mac の名前解決が固まることがあった。Mac 側は Pi に直接問い合わせて IP で開く |
raspi/tools/mdns_unicast.py、tools/find_pi.py |
| 再生と実機で同じコードを通す |
2 つ書けば必ずズレて「再生した結果が実機と違う」になる |
core/link_tracker.py、core/bus_bridge.py |
| 時刻同期が静かに壊れていた |
条件によって最大 178 ms 未来側へずれた。回帰を保持する形に直した(0.36 ms 以下) |
PROGRESS.md 2026-10-14 |
制御
| 教訓 |
何が起きたか/なぜか |
出どころ |
| 効きの小さい系に高ゲインの閉ループを掛けない |
トルクベクタリングのヨーレート PI は、ループゲインが 0.05 しかなく、ノイズだけで上限に張り付いた。内輪へ足す向きの修正は TC に打ち消された |
torque_vectoring.h |
| スリップ判定の基準速度は専用のフィルタで |
PI 用のフィルタを流用すると、加速中に「常時空転」と誤判定 |
drive.h |
| センサ故障のフォールバックを持つ |
前輪エンコーダの故障で ABS が制動を抜き続ける。片輪浮き対策は前輪に依らない見張りとして独立に持つ |
drive.h |
| 削ったトルクを反対輪に載せない |
総和を保つ「親切」は、要求していないヨーモーメントを生む |
drive.h |
| 生のエンコーダ角を微分しない |
12 bit の 1 LSB が 0.09 m/s に化け、静止中でも ±12 m/s 振れる |
drive.h |
| PI だけでは加速度が出るのが遅れる |
約 0.15 秒。コーナー手前の減速が遅れて速すぎる速度で入った。目標の傾きが分かっているのでフィードフォワードを足す |
drive.h、nav/speedctl.py |
| 下位の PI の遅れを上位が先回りする |
実車のヘアピン進入で、目標 0.7 m/s に対し 1.2〜1.3 m/s だった |
nav/speedctl.py |
| 前方注視距離はギャップの奥行きにしない |
奥まで開けた交差点で急がなくていいと判断し、曲がりきれず壁に当たった。速度比例にした |
auto/follow_the_gap.py |
| Pure Pursuit は経路上でも内側を切る |
注視区間で平均した曲率で曲がるため。経路の曲率との差をフィードフォワードで足す |
nav/purepursuit.py |
| 反復法が効かない最適化がある |
条件数 10⁵ の QP。射影ガウス・ザイデルは 20 万反復しても改善せず、400 変数なら直接解が 1 ms 以下 |
nav/raceline.py |
| FISTA は条件数が悪いと収束しない |
MPC の QP。40 反復でも収束せず舵が振動。射影ニュートン法にした |
nav/mpc.py |
| 走行中のグリップ学習は、実機の雑音で誤検出する |
初版で 1 周 5.7 s → 13.1 s に悪化。前兆の判定は検出の穴の両側へ外れた |
PROGRESS.md、nav/grip.py |
| 手当てを足し続けると複雑系になる |
減速の学習は定数が約 35 個になり、捨てて 8 個で作り直した |
nav/grip.py、PROGRESS.md |
地図・自己位置
| 教訓 |
何が起きたか/なぜか |
出どころ |
| 全体の地図に合わせると壁が二重になる |
周回して戻ったとき古い壁と新しい壁が混ざる。地図作成中は直近のキーフレームだけの局所地図に合わせる |
slam2d/core/localmap.py |
| ループ検出を全体の地図に照合すると、ドリフトを追認する |
ループ閉じ後に地図の精度が悪化したコースがあった。古いキーフレームだけで作った部分地図に照合する |
slam2d/backend/loop_detection.py |
| 距離変換の「台地」は姿勢を拘束しない |
壁の厚みの内側は残差も勾配も 0。2 周で向きが 17° ずれた。面要素の法線を残差にする |
slam2d/core/surfmap.py |
| ジャイロのゼロ点ずれは走行中には観測できない |
局所地図が一緒に回る。停止中(ZUPT)に学ぶ |
slam2d/core/motion.py |
| 点ごとの脱スキューが要る |
高速のヘアピンで、1 周期一定の近似では自己位置が 5 m 以上ずれた |
slam2d/core/deskew.py、_slam2d_nav.py |
| まばらな点を「角」と見ると進行方向を誤認する |
平行な壁の通路で進行方向が決まっているかのような情報が出る。裏付けの無いセルは使わない |
slam2d/core/surfmap.py |
| 粗い尺度の 1 位だけで自己位置を決めない |
差 0.03 だった候補が、仕上げ後の当たり率で 0.98 対 0.60 に分かれた |
slam2d/core/localize.py |
| 誤差が成功として現れる |
駐車でドリフトすると、planner は誤った場所で「完了」と言う。評価は申告ではなく真値で |
sim/park_bench.py |
| 穴のない壁でも、急カーブの法線は暴れる |
接ベクトルがわずかに傾くだけで法線が何 m も先へ抜ける |
nav/centerline.py |
開発の進め方
- 検証器を先に持つ。 解析が正しい値を返すか、真値の分かっているシムで確かめる(同定で利得が 9 倍に出ていた不具合は、これで見つかる種類のものだった)。
- 同じ事実を 2 箇所に書かない。 生成する、または読む口を 1 つに絞る。それでも文書は腐る(このサイトについて)。
- ラッチは人が解除する。 原因が消えたら自動復帰、にしない。
- 「分からない」ときは止まる。
ready=False は制動に読み替える。