← 回專案總覽與驗證系統
第五支:DM-J4310 準直驅(C1 決策、C5 CAN 協定)
四顆致動器裡唯一會回報力矩的。 那是驗證階梯上比位置回授高一階的格子: 機器回報它產生的力,而不只是它在哪裡。
C1:遠端驅動買到 +69% 伸距,仍然被拒絕
| 酬載 | 直驅 | 遠端驅動 | 差 |
|---|---|---|---|
| 500 g | 173 mm | 293 mm | +69% |
| 1000 g | 109 mm | 157 mm | +44% |
325 g 與 NEMA 17 同級,單看質量,答案應該和 A1 一樣:搬到底座。 +69% 遠比 FT5425BL 的 +28% 有份量。
拒絕的理由不是質量,是力矩路徑。 它回報的 T 是馬達端、由繞組電流推估、位在自身 10:1 減速的另一側。 加一組皮帶,感測器與負載之間就變成兩級未被量測的摩擦: 原廠至少刻畫過的內建齒輪箱,加上沒有人量過的列印皮帶。 遠端驅動等於用「讓這支手臂存在的那個量測失去意義」換 120 mm 伸距。
這與 B1 是不同的論證:B1 拒絕遠端驅動是因為它買太少; 這裡它買很多而仍被拒絕。結論相同、理由相反——所以這個問題必須重問,不能沿用。
C1 定案:直驅、伸距 173 mm、酬載 500 g、 肩關節 2.10 N·m = 70% 連續額定、30% 峰值。
C5:位元打包的協定,錯一格仍是合法指令
前面每一支手臂的韌體錯了都會大聲失敗:Feetech 校驗和不對就沒有回應、
Servo.write() 打錯會超出範圍。MIT 模式沒有這種餘裕:
p(16) + v(12) + Kp(12) + Kd(12) + t_ff(12) = 64 位元 = 8 位元組
沒有一位餘裕
欄位寫錯一個 nibble不會溢位——它落進鄰居, 馬達就用一個沒人選的增益執行一個沒人下的指令。
而且 DAMIAO 沒有公布範例封包,所以 B'5 對照原廠位元組的做法在這裡用不了。 能用的只有結構:位元預算、逐欄往返、欄位隔離(唯一抓得到 nibble 錯位的檢查)、 以及超範圍必須夾住而非回繞(回繞會把關節送到行程的另一端,而回繞後的值完全合法)。
寫明這一層的盲點:一致套用到打包與解包的佈局錯誤在這裡是隱形的—— 往返完美、欄位隔離、位元預算都對。只有實機或原廠範例能分辨。
#59 我的變異測試方法本身有 bug,而且兩個方向都會報錯 靜默 已修正
C5 的套件單獨跑 17/17,下一次跑卻失敗 3 項,中間沒有任何編輯。 那種結果必須追,不能重跑到它同意為止。
追出來的根因:CPython 的 __pycache__ 以來源檔 mtime 為鍵,
解析度是一秒。我的變異腳本在同一秒內把 dm_can.py 重寫了七次,
於是 Python 服務的是快取住的變異版本。
它在兩個方向都會騙人:
- 報「抓到」——但變異其實從未被載入,看到的失敗來自上一個還在快取裡的變異
- 報「乾淨」——但還原其實沒生效,磁碟上的檔案是對的、被匯入的模組不是
這影響先前每一次變異測試的可信度。修法是把它抽成
tests/mutate.py,明確清除該檔的 .pyc 並呼叫
importlib.invalidate_caches()。
用修好的方法重跑,立刻挖出一個真的洞: B'4 的「拿掉編碼器量化」沒有被抓到——因為我的檢查只驗 「感測誤差 ≤ 半個計數」,量化拿掉後誤差變成 0, 當然通過,而模型的編碼器就變成無限精確。
只從上方設界的檢查,抓不到「那個現象根本不存在」。 A4 為此加過「過載後確實失步」,這一層我沒加。已補上 「回報值確實是整數個計數」——量化一消失它立刻失敗。
修好之後:C5 變異 7/7、B'4 變異 4/4。
C2 CAD:這支手臂沒有關節結構
前面三支手臂都得造一個關節:叉臂夾住舵機、輪轂跨在外面、 另一側加惰輪軸承讓輸出軸只受扭矩不受彎矩。 FT5425BL/STS3215 那份設計裡大約一半的幾何、以及它大部分辛苦換來的教訓, 存在的目的都是把一顆致動器變成一個關節。
QDD 模組本來就是。它自帶軸承、一面是機殼法蘭、另一面是輸出法蘭, 連桿直接鎖上去。所以這支手臂沒有叉臂、沒有跨接槽、沒有耳片、沒有惰輪—— 零件只是托架,載荷由致動器承擔。
| 零件 | 質量 | 做什麼 |
|---|---|---|
| base | 214.9 g | 底板 + J1 機殼法蘭 |
| shoulder_bracket | 48.8 g | J1 輸出法蘭 → J2 機殼法蘭,把迴轉軸轉成俯仰軸 |
| upper_arm | 37.2 g | J2 輸出 → J3 機殼 |
| forearm | 27.9 g | J3 輸出 → 工具板 |
| 動件 113.9 g | +致動器 3×325 g | |
簡單是有代價的,而且原廠沒有給那個數字
輸出法蘭承受全部懸臂彎矩,而 DAMIAO 沒有公布彎矩額定值。
舵機手臂的惰輪軸承存在的理由,正是讓輸出軸永遠不受彎—— 這裡沒有地方放惰輪,除非把「模組本來就要省掉」的那套結構加回去。 在 C1 的 173 mm/500 g 下,肩部法蘭除了 2.10 N·m 的軸向扭矩, 還要承受約 0.95 N·m 的彎矩, 而資料表對這件事完全沒有說法。標記,不猜。
#60 手冊與 STEP 對機殼螺栓圓的說法不一致 靜默
| 特徵 | STEP 實測 | 手冊 p.5 |
|---|---|---|
| 輸出法蘭 | 4ר2.70 @ Ø38 | 4×M3▽4 @ Ø38 ✓ |
| 機殼法蘭 | 6ר3.50 @ Ø53.51 | 6×M3▽5 @ Ø50 |
孔數(6)與螺紋(M3)都對得上,螺栓圓差 3.51 mm。 STEP 裡另有 4ר3.00 落在 Ø50 圓上、但在不同的軸向站位—— 手冊那個「Ø50」可能標的是那個特徵或機殼台階,而不是這組孔。
照 STEP 造,因為 STEP 才是托架真正要鎖上去的東西。 但衝突記在這裡、也寫進程式碼常數旁邊, 免得有人憑一張沒人調和過的圖面標註去做一個 Ø50 的件。
#61 T8「可組裝性」對三支手臂是空的,而我把它當證據發布過 靜默 已修正
DM-J4310 手臂第一次跑 T8 就 5/5 通過,耗時 0.2 秒。 0.2 秒對一個要做起子路徑掃掠的層來說太快了,所以去看它到底檢查了什麼:
每根螺絲的旋入長度 ≥ 3.0 mm 0/0 根不足 螺絲長度與孔深相符 全部相符 沒有緊固件明顯突出整機輪廓 全部在輪廓內 緊固件之間互不干涉 無 每根螺絲在該裝配步驟都有起子路徑 0/0 根仍被擋住
「0/0」——根本沒有緊固件可以檢查。 這五個「通過」什麼都沒證明。而且不只這一支: FT5425BL 與 STS3215 兩支手臂也一樣, 我把它們的「T8 5/5」當成「可組裝性已驗證」發布出去了。
真空的通過比缺口更糟,因為缺口是看得見的。 修法是讓 T8 在沒有緊固件模型時跳過而不是通過。 改完之後總數從「502/502 通過、8 項跳過」變成 「487/487 通過、23 項跳過」——少的 15 項正是 三支手臂各 5 項的空檢查。
這個數字下降不是退步,是把先前虛報的部分還回去。 那三支手臂的可組裝性目前未驗證, 要驗就得替它們建緊固件模型,跟 SG90 與 NEMA 17 兩支一樣。
#62 補上緊固件模型之後,T8 立刻找到兩個真缺陷 已修正
#61 把三支手臂的 T8 從「真空通過」改成跳過。
這次替它們建了緊固件模型(cad/fasteners_generic.py),
讓那一層有東西可檢查——第一次跑就抓到兩個真的問題。
(a) M4 螺絲鑽進 M3 的孔位。
STS3215 手臂是用注入的方式共用 FT5425BL 的設計,而
servo_tab_holes() 把讓位孔硬寫成 M4——
對 Ø4.50 耳片的 FT5425BL 是對的,對 Ø3.80(M3)的 STS3215 是錯的。
T8 報出加大的孔與舵盤螺栓干涉 45 mm³,三處。
改成由 SERVO.mount_hole_d 推導。
(b) 螺栓圓與耳片樣式幾乎重疊。 改成 M3 之後干涉降到 27 mm³ 但沒有消失——所以根因不只是直徑。 算下去:STS3215 的耳片孔在半徑 11.74 mm、127.2°, 舵盤螺栓在半徑 11.10 mm、135°—— 幾乎在同一個圓上,只差 7.8°。 FT5425BL 的孔在 −36.25/+13.25,遠得多,所以從來不會撞。
修法是把螺栓方陣轉 45°(0/90/180/270 而非 45/135/225/315), 而且由耳片樣式決定要不要轉,不是全域改掉—— 決定這件事的是舵機的孔位,不是這個檔案。
| 手臂 | 緊固件 | BOM |
|---|---|---|
| FT5425BL | 14 | M4×16 ×5、M4×10 ×1、M3×12 ×8 |
| STS3215 | 10 | M3×8 ×2、M3×10 ×4、M3×12 ×4 |
| DM-J4310 | 26 | M3×12 ×12、M3×10 ×4、M3×8 ×7、M3×6 ×3 |
誠實標註:這些數量比「4 顆致動器 × 4 孔 + 舵盤螺栓」的直覺值低, 代表收集器把跨零件的同軸孔合併了。方向是保守的(少算不會漏掉干涉), 但不代表完整——與 NEMA 17 那支手寫的 28 項相比, 這三支的緊固件模型仍然比較粗。
C3:連桿慣性、關節極限,與一個負寬度的關節
cad/make_links_dm4310.py 把 C2 的托架做成四個連桿並量出質量與慣性張量:
base_link 539.9 g、shoulder 373.8 g、upper_arm 362.2 g、forearm 27.9 g,合計 1303.8 g——
其中 975 g 是三顆模組本身,印出來的塑膠只有 329 g。這支手臂的重量幾乎就是致動器的重量。
失敗 #63:j3 的行程掃出來是 [+5°, −5°],寬度是負的
關節極限用碰撞掃描量。j3 回報下限 +5°、上限 −5°——一個負寬度的關節。 負寬度不是量測誤差,是量測到了一個不存在的東西。
根因在 C2,不在掃描:兩個托架都被畫在 Y≈0 的同一個平面上。 法蘭對法蘭的關節,兩側連桿必須接在模組的相對兩面—— 我把它們接在同一面,所以掃描一開始就處在互相穿透的狀態,於是「第一個不撞的角度」永遠找不到。 修法是把前臂沿軸向推開一個模組厚度:
FLANGE_SEP = OUTPUT_AXIAL - HOUSING_AXIAL # 36.0 mm,模組兩面的距離
part = Pos(0, -FLANGE_SEP, 0) * _bracket(Z_J3, Z_TIP, width)
這是一個 CAD 錯誤被下游的模擬抓出來——而不是被眼睛抓出來。渲染圖上兩個托架看起來完全正常。
修好之後 j2 量到 ±55°,j1 與 j3 則在 ±180° 內沒有撞到任何東西。 那是關於幾何的真話,也是關於機器的廢話:CAN 線與電源要沿手臂往上走, 可以無限旋轉的偏航關節會把自己的線扯下來。所以套用 ±150° 的線材限制, 並且在資料裡標明它的來源是線材而不是機構:
out[n + "_source"] = ["sweep" if blo else "cable", ...]
把「掃不到」寫成「量到 ±180°」是這個專案最容易犯的那類錯——一個看起來像數據的預設值。
C4:第一支用力矩指令的手臂
前三支手臂都是位置指令,驗證問題是「到了沒、差多少、掉不掉步」。
這支不是。DM-J4310 收到的是 (p_des, v_des, Kp, Kd, τ_ff),
驅動器自己算 τ = Kp·(p_des−p) + Kd·(v_des−v) + τ_ff。
增益是我們給的,而錯的增益會把 7 N·m 灌進鋁件裡。
所以 MJCF 用的是 motor 致動器(純力矩輸入),不是 position——
把阻抗律交給 MuJoCo 的位置伺服,正好會藏掉這支手臂唯一值得看的東西。
失敗 #64:j3 用 233% 額定扭矩「靜止」在距目標 0.14° 的地方
第一次跑,j3 的 RMS 力矩是 6.991 N·m —— 額定的 233%,而且 98.7% 的時間貼在 ±7 N·m 峰值上。 這個關節的重力負載是 0.011 N·m。同時「尾段振盪」在 Kp=20、60、200、500 四個值下 都是 0.102°,一模一樣。一個不隨增益改變 25 倍而動的數字,量的不是增益。
不猜,直接把 tau() 的輸入印出來:
axis2 kp=59.95 p_des=-0.61087 p=-0.60836 kd=2.000 v=+3.520493 ... t=-7.19422
位置只動 0.1°,速度卻是 3.52 rad/s——關節每一個時間步就反向一次。 Kd·v = 2 × 3.52 = 7.04 N·m,整個 −7 都是阻尼項在追自己造成的抖動。
根因是算術不是調參:j3 的慣量最小(0.99 g·m²),
Kd·dt/J = 2×0.0005/0.00099 = 1.01——顯式阻尼的穩定界線是 2,我已經越過一半以上,
而在 CAN 週期上施加它就直接越界。
但這也是我的模型錯:真實硬體不是這樣運作的。主機以 ~1 kHz 送 CAN, 驅動器用自己數 kHz 的內迴圈對著新鮮的編碼器讀數算那條式子。 會變舊的是設定點,不是回授。我把整條律都跑在 CAN 率上了。修正後 j3 從 233% 降到 8.3%,尾段振盪 0.0000°。
失敗 #65:修好之後,掃描每一格都通過——那是另一種空的通過
修完 Kd 問題,Kp 從 20 掃到 500、CAN 率從 1000 掃到 200,每一格都「穩定」, 而且大多是 0.000°。上一個 bug 是每格都相同的壞值,這個是每格都相同的好值, 同樣沒有測到任何東西。
原因:對固定目標掃 CAN 率,設定點只會變舊一次。之後主機送什麼都一樣。 改成追 ±25° 1 Hz 正弦——設定點持續變舊,200 Hz 時驅動器追的目標最多舊 5 ms——數字才開始有斜率:
| CAN 率 | j2 RMS 誤差 | j2 最大誤差 | j3 RMS 誤差 |
|---|---|---|---|
| 2000 Hz | 0.0477° | 0.0796° | 0.0145° |
| 1000 Hz | 0.0557° | 0.0937° | 0.0189° |
| 500 Hz | 0.0971° | 0.1396° | 0.0737° |
| 200 Hz | 0.2578° | 0.3569° | 0.2428° |
| 100 Hz | 0.5366° | 0.7562° | 0.5250° |
| 50 Hz | 1.0974° | 1.6210° | 1.0898° |
同一個教訓第三次出現(#59 的 .pyc、#61 的 0/0、這次的固定設定點): 一個不會失敗的檢查不是檢查。所以 C4 的穩定界線是靠故意越界來驗證的—— Kd = 1.99 尾段振盪 0.0074°,Kd = 4.77 尾段 0.2287° 且力矩飽和到 6.99 N·m,相差 31 倍。 只在界線好的一側取樣,等於沒測界線。
C4 的結果
| 量 | 結果 |
|---|---|
| 全工作空間最壞保持扭矩 | j2 0.295 N·m=額定的 9.8%(掃過整個包絡,非單一姿態) |
| 階躍終點誤差 | 最大 0.0023°(有重力前饋,穩態只剩量化) |
| RMS 力矩(熱) | 最大 0.681 N·m=額定的 22.7% |
| CAN 量化後的增益 | 主機下 Kp=60.000 → 驅動器收到 59.9512 |
| j3 顯式阻尼 Kd 上限 | 3.97,設計用 2.0(50% 餘裕) |
最後一列有個誠實的反面。armature(轉子慣量反射到關節)佔 j3 總慣量的 92.9%,
而 DAMIAO 沒有公布轉子慣量——那個數字是我從外形推的,在程式裡標成
ROTOR_J_TRUSTWORTHY = False。掃它 ×0.5 到 ×2.0:
| armature 倍率 | 最壞保持扭矩 | j3 Kd 上限 | 正弦追蹤 j3 RMS |
|---|---|---|---|
| ×0.5 | 0.295 N·m | 2.13 | 0.0116° |
| ×1.0 | 0.295 N·m | 3.97 | 0.0189° |
| ×1.5 | 0.295 N·m | 5.82 | 0.0205° |
| ×2.0 | 0.295 N·m | 7.67 | 0.0232° |
靜態結論完全不依賴它(保持扭矩變化 0.00e+00)。 但 Kd 上限與它成正比:如果轉子慣量被我高估一倍,真正的上限只有 2.13, 而建議的 Kd = 2.0 就用掉 94% 的餘裕。這個結論真的靠一個沒人量過的數字—— 所以它被標成推估,而不是被寫成數據。
C 系列目前的狀態
| 階段 | 狀態 |
|---|---|
| C1 傳動決策 | 完成(173 mm / 500 g / 直驅) |
| C2 CAD | 完成(法蘭對法蘭,動件 113.9 g);補上緊固件模型後 T8 由「跳過」變成實測 |
| C3 URDF/MJCF | 完成(連桿 1303.8 g,關節極限含線材來源標註);T0 由 8/8+2 跳過 → 32/32 |
| C4 模擬 | 完成,12 項檢查全過(含故意越界的界線驗證) |
| C5 CAN 協定層 | 完成,17 項檢查、變異 7/7 |
全套驗證由 502 項成長到 567 項全過、4 項跳過。 當時剩下的兩項跳過是 STS3215 手臂還沒產出網格與連桿慣性;補完之後 (見 STS3215 頁的 B'6)只剩四項跳過, 而且四項都是真的不適用——端面安裝的致動器沒有座槽薄壁這回事——不再有任何一項是缺口。