← 回專案總覽與驗證系統

第四支手臂:STS3215 匯流排舵機(換取閉環回授)

前三支手臂都是開環——韌體只知道自己下了什麼指令。 所以這個專案的每一層檢查,都是拿一個模型去比另一個模型, 整套可以自洽地描述一台不存在的機器。 #54 的 frictionloss=0 就是活生生的例子: 零摩擦是自洽的,只是不真實,12 層測試沒有一條抓得到。

為什麼換掉 FT5425BL

項目FT5425BLSTS3215
回授無(3 線 PWM,read() 是指令的回音) 位置/速度/負載/電壓/電流/溫度
額定扭矩9.5 kg·cm,單一經銷商推得 10 kg·cm,原廠公布
編碼器—12-bit 磁編碼,0.0879°/計數
本體40.6×20.0×30.9 mm(含包絡 62.6×34.9×20.0)45.2×37.8×24.7 mm
質量67.6 g55 g
花鍵25T25T(相同,舵盤介面沿用)
伸距(100 g、70% 額定)190 mm212 mm

更輕、扭矩略高、依據更硬、還多了回授。 FT5425BL 那支的紀錄保留為被取代的設計—— 「同級舵機,一顆有回授有公布額定、一顆沒有」本身就是這個專案要比較的東西。

CAD 沒有複製一份

arm_ft5425bl.py 改成可以在執行前注入致動器與伸距, 所以同一份 395 行的推導直接長出第二支手臂。 結果是這支手臂第一次跑 T0/T8/T10 就全過—— 那支付出過的幾何教訓(叉臂的掃掠對角、耳片要包過關節軸、 Cylinder 吃的是半徑)全部繼承,不必重學。

而且這麼一改就暴露了一個原本就存在的潛在錯誤: REACH、L_UPPER、L_FORE 是三個獨立常數, 剛好滿足 105 + 85 = 190。注入 212 mm 之後 REACH 變了、 連桿還是 190——手臂按一個數字造、文件按另一個數字寫, 而零件在兩種長度下都是合法的,沒有任何檢查會發現。已改為由 REACH 推導。

閉環回授層:兩種誤差,兩個不同的界限

這支舵機的回授路徑有兩個確定性的誤差源,而它們不是同一件事:

誤差來源界限
感測誤差12-bit 量化,與機構無關半個計數 = 43.9 毫度
停止誤差韌體死區 10 計數,與感測器無關0.879°

死區是感測解析度的 10 倍——這支舵機的精度上限是韌體, 不是那顆 12-bit 編碼器。拿「12-bit」當精度規格是把兩件事搞混。

#57 我把停止誤差的界限寫成死區本身,漏了兩個誤差源會疊加 已修正

驗證層第一次跑,「停止誤差不超過死區」失敗:最壞 0.9218°, 界限 0.8789°。查下去不是模型錯,是我的界限錯:

韌體用「量化後的讀值」判斷是否到位
→ 真實角可以再多偏半個計數
→ 界限 = 死區 + 半計數 = 0.8789 + 0.0439 = 0.9228°
                                       實測最壞 0.9218°

兩個誤差源會疊加,不是二選一。這也正是為什麼它們要分開記錄—— 混成一個「精度」數字,就分不出該修韌體還是換感測器。

唯一一處「模型」對上「實體量測」

第三方(robonine.com)實測一台 STS3215:

量測值對照
虛隙0.87°規格書寫 ≤0.5°
韌體死區10 計數= 10 × 0.0879 = 0.879°
重複性(空載)±0.17°≈2 個計數
位置偏差(1.5 kg × 100 mm)2.6°那是順應性,不是虛隙

0.87 與 0.879 幾乎相等。這支持一個具體結論: 這顆舵機的「虛隙」主要是韌體死區而不是齒隙—— 所以它可以被精確建模,不必像 #55 那樣 因為沒有實測參數而留成缺口。

但這仍然不是 validation:樣本數 1,量的不是我們的手臂、 不在我們的台子上。它比另外三支手臂高一階,如此而已。 這句話寫在 tests/suite_bus_servo.py 的檔頭,不是寫在這裡就算。

驗證結果

層結果
T0 幾何完整性8/8(2 跳過:尚未產出網格與連桿屬性)
T8 可組裝性5/5
T10 可列印性4/4
B'4 閉環回授層7/7

全套 445/445。接下來是 B'5 韌體(半雙工序列匯流排協定) 與四支手臂的對照報告。

B'6:連桿網格與慣性,以及它揭穿的一件事

這支手臂之前只有 CAD,沒有連桿網格與慣性張量,所以 T0 的兩項檢查一直顯示 「這支手臂還沒產出網格」。那個跳過訊息讀起來像「不適用」,其實是「沒做」。

補的方式不是複製 FT5425BL 的產生器。這支手臂本來就是 同一個設計模組換一顆舵機——arm_sts3215.py 是把致動器注入 arm_ft5425bl.py,而不是抄一份幾何程式碼。質量特性的產生器如果用抄的, 等於把同一個錯誤往下搬一層,兩份實作會在任何一支手臂改動時分岔。 所以產生器也改成可注入:同一張 LINKS 表、同一組 _gprops/_parallel_axis、同一個從尺寸預算讀出的夾爪假設值, 只換舵機質量與輸出路徑。

連桿STS3215(212 mm/55 g 舵機)FT5425BL(190 mm/67.6 g 舵機)
base_link170.4 g211.0 g
turntable193.7 g184.2 g
upper_arm151.1 g145.1 g
forearm182.4 g180.6 g
合計697.7 g720.9 g

連桿更長卻更輕,不是同一支手臂的小擾動——舵機輕了 12.6 g、連桿長了 22 mm, 對肩關節的慣量一個往下一個往上。

失敗 #66:FT5425BL 的連桿慣性過期了十天,而 T0 一路回報 32/32

把產生器改成可注入之後,重跑 FT5425BL 應該得到逐位元組相同的輸出—— 畢竟只是換了取值來源。結果 git diff 顯示質量與慣性都變了, upper_arm.stl 甚至變大了 1200 位元組。

不是重構壞掉。是磁碟上的產物本來就過期:

cad/arm_ft5425bl.py     最後修改 2026-08-11(#62 緊固件修正)
urdf/meshes_ft5425bl/   最後產生 2026-08-01

URDF 描述的是 #62 修正之前的幾何,整個 C 系列都是。 upper_arm 差 0.178 mg——M4 鑽進 M3 孔位的那 45 mm³ 干涉、 以及螺栓圓轉 45° 的修正,都沒有進到模擬裡。

T0 在這段期間一直回報 32/32:網格 watertight、慣性張量對稱、正定、 主慣性滿足三角不等式。每一項都通過,而且每一項都在驗證一個十天前的物件。 那些檢查問的是「這個 JSON 是不是一個合法的剛體」, 從來沒有問過「這個 JSON 是不是現在這支手臂」。

修法是加一項會把答案重算出來的檢查:

fresh = {q["name"]: q for q in arm.link_gen()}   # 從現在的 cad/ 重算
ok = worst_m < 1e-12 and worst_i < 1e-15         # 容差是 JSON 的往返精度,
                                                 # 不是工程餘裕

容差刻意取得極緊:檔案存的是完整雙精度,所以「沒過期」的意思是逐位元相同, 任何高於這個的差異都是真的幾何差異。並且照 #65 學到的規矩, 這項檢查必須被證明會失敗——把 STS3215 的 upper_arm 質量改掉 0.178 mg (正好是真實過期造成的差),它抓到了:

FAIL 連桿慣性與現行 CAD 一致(非過期產物)
     最大質量差 178.000 mg(upper_arm)

四支手臂全部接上這項檢查後,重建 FT5425BL 的網格、URDF 與 MJCF, 全套由 538 項成長到 567 項全過、4 項跳過, 而且四項跳過都是真的不適用,不再有任何一項是「還沒做」。

這是這個專案裡反覆出現的同一個形狀第四次:#59 的 .pyc 快取、#61 的 0/0、 #65 的固定設定點,現在是過期的產物。共同點不是檢查寫錯了, 是檢查通過的對象不是它宣稱的對象。