Day 7:系统串联 + 调参实战指南
0. Day 7 定位
前几天是"读懂代码",今天是"会用代码"。目标是两件事:
- 脑内完整走通整个系统的控制流(不用看代码,能说出来)
- 知道怎么调参------改哪个参数、观察哪个现象、预期什么结果
1. 系统串联总复习:用"问题链"的方式记住整个架构
不要死记硬背。用一串递进式问题把整个系统串起来:
问题链
Q1: 上位机发了一条 ">30,-20,90,0,0,0" ,这串字符去哪了?
→ USB-CDC 中断 → usbIrqTask → usbServerTask → ASCII_protocol_parse_stream
→ OnUsbAsciiCmd → commandHandler.Push(字符串) → commandFifo 消息队列
→ Respond("15") 回到上位机
Q2: 谁把这个命令从队列里取出来?
→ ControlLoopUpdate 线程(Normal 优先级)
→ commandHandler.Pop(osWaitForever) 阻塞等待
→ ParseCommand(字符串)
Q3: 这个命令最终变成什么发到了 CAN 总线上?
→ sscanf → 6 个 float → MoveJ(30,-20,90,0,0,0)
→ 校验限位 → 算 deltaJoints → AbsMaxOf6 找最慢关节
→ time = maxAngle × reduction / jointSpeed
→ 反算其余关节速度 → dynamicJointSpeeds
→ MoveJoints(targetJoints)
→ 逐个关节 CtrlStepMotor::SetAngleWithVelocityLimit(angle, vel)
→ CAN 0x07 帧:[NodeID<<7 | 0x07] + [position_float] + [velocity_float]
Q4: 关节 F103 收到 CAN 帧后,谁在处理?
→ HAL_CAN_RxFifo0MsgPendingCallback
→ id 匹配 → OnCanCmd(0x07, data, 8)
→ SetCtrlMode(COMMAND_POSITION) + SetPositionSetPoint(pos × 51200)
Q5: 关节的 20kHz 循环里发生了什么?
→ TIM4 中断 @50μs:
encoder->UpdateAngle() → SPI 读 MT6816 → 校验 → 查校准表 → rectifiedAngle
CloseLoopControlTick():
- 跨圈检测:realLapPositionLast→realLapPosition, delta→realPosition
- 速度估计:指数滤波, estVelocity
- 超前角补偿:CompensateAdvancedAngle → estLeadPosition
- estPosition = realPosition + estLeadPosition
- 安全检测:堵转?→ Sleep; 过载?→ 标记
- PositionTracker.CalcSoftGoal(goalPosition) → 梯形速度曲线 → softPosition, softVelocity
- CalcDceToOutput(softPosition, softVelocity):
Kp × pError + (Ki × pError + Kv × vError)积分 + Kd × vError → outputCurrent
- CalcCurrentToOutput(outputCurrent) → estPosition ± 90° → SetFocCurrentVector
- TB67H450: sin_map 查表 → PWM CCR → RC滤波 → VREF → H桥 → 线圈电流
Q6: 关节怎么告诉核心"我到了没"?
→ 每 50μs 自动回复 CAN 0x23:[当前位置_float] + [完成标志_byte]
→ 完成标志 = (state == STATE_FINISH) ? 1 : 0
Q7: 核心怎么知道 6 个关节全到了?
→ HAL_CAN_RxFifo0MsgPendingCallback → OnCanMessage
→ cmd=0x23: motorJ[id]->UpdateAngleCallback(pos, isFinished)
→ UpdateJointAnglesCallback():
for i=1..6:
if motorJ[i]->state == FINISH → jointsStateFlag |= (1<<i)
→ IsMoving() = (jointsStateFlag != 0b1111110)
Q8: 全部到了之后发生了什么?
→ [Sequential 模式] while(IsMoving()) 退出 → Respond("ok")
→ [Interruptable 模式] 早就在第一步返回 "ok" 了
如果你能不看代码、不看我写的文档,完整回答这 8 个问题,你就掌握了这个项目 80% 的核心逻辑。
2. 调参实战:DCE 四参数怎么调
2.1 调参前的准备工作
步骤 1:切换到 Tuning 模式
上位机发: #CMDMODE 4
步骤 2:选择要调的关节,设正弦运动
上位机发: #SET_TUNING_FLAG 1 (只让 J1 动,bit0=1)
上位机发: #SET_TUNING_FREQ_AMP 1 10 (1Hz, ±10°)
步骤 3:观察 OLED 或通过 #GETJPOS 读取实时角度
上位机发: #GETJPOS
回复: ok 9.87 -19.52 89.93 0.12 -0.05 0.01
(J1 在 10° 附近正弦摆动,说明正常)
2.2 调 kp(位置比例项)--- 影响"追得快不快"
默认值:J1 = 1000(量产版),200(35 电机默认)
实验:
#SET_DCE_KP 1 200 → 跟踪很慢,实际角度明显滞后于目标正弦波
#SET_DCE_KP 1 500 → 跟踪快了一些
#SET_DCE_KP 1 1000 → 跟踪基本跟上了
#SET_DCE_KP 1 2000 → 开始出现轻微振荡(kp 太大)
#SET_DCE_KP 1 5000 → 明显振荡!电机会发出"滋滋"声
观察方法 :用 #GETJPOS 定时读取 J1 角度,和目标正弦值(1Hz, ±10°)对比。如果实际值总是比目标小(幅度不足)→ kp 太小。如果实际值来回跳(超过目标后又弹回来)→ kp 太大。
物理直觉:kp 就是弹簧的劲度系数。kp 太小 = 弹簧太软,拉不到位;kp 太大 = 弹簧太硬,拉过头来回弹。
2.3 调 kv(速度前馈项)--- 影响"跟得及时不及时"
默认值:80(所有关节相同)
实验:
#SET_DCE_KV 1 0 → kv=0,去掉速度前馈。在正弦波速度最快处(过零点),
位置跟踪出现明显滞后
#SET_DCE_KV 1 160 → kv 翻倍。跟踪滞后减少,但可能出现高频微振
观察方法:在正弦波的"过零点"(速度最大处),观察实际位置是否滞后于目标。滞后越大 → kv 太小(或需要更大)。在"峰值处"(速度=0 处),观察是否有微振。微振 → kv 太大。
物理直觉:kv 是"预判"。你知道目标速度是 30r/s,不等位置出偏差你就提前按这个速度发力。kv=0 就是"永远等偏差出现了再反应"(慢半拍)。
2.4 调 ki(位置积分项)--- 影响"最后一点追不追得上"
默认值:200~300
实验:
#SET_DCE_KI 1 0 → ki=0。在正弦波峰值处(速度=0),可能存在 ±0.5° 的稳态误差
#SET_DCE_KI 1 500 → ki 加大。稳态误差很快消除,但 ki 太大可能在换向时过冲
观察方法:在正弦波峰值(速度刚好降到 0 的瞬间),此时只有位置误差、没有速度误差。如果实际值停在目标值附近但差一点(如目标 10°,实际 9.7° 就不动了)→ ki 太小。
物理直觉:ki 是"耐心"。kp 说"还差 0.1°,算了不追了",ki 说"虽然只差 0.1°,但已经持续 100ms 了,我再推一把"。
易错提醒:ki 不要和 kp 一起大幅调!先定 kp/kv,最后微调 ki。ki 最容易引起振荡。
2.5 调 kd(速度阻尼项)--- 影响"会不会抖"
默认值:200~250
实验:
#SET_DCE_KD 1 0 → kd=0。电机在换向时可能出现"嘎嘎"的振动声
#SET_DCE_KD 1 500 → kd 加大。振动消失,但跟踪响应变迟钝
观察方法:听声音!kd 不够时会有高频的机械振动声。这是 DCE 控制律的高速振荡传导到了机械结构上。
物理直觉:kd 是"减震器"。路面颠簸(速度突变)时,减震器吸收冲击不让车身(位置)跟着抖。
2.6 调参顺序(重要!)
第 1 步:ki=0, kd=0, kv=80(固定 kv)
第 2 步:逐步加大 kp,直到跟踪够快但还没振荡 ← 定 kp
第 3 步:微调 kv,减少高速跟踪滞后 ← 定 kv
第 4 步:加 ki,消除稳态误差 ← 定 ki
第 5 步:加 kd,抑制可能的振荡 ← 定 kd
绝不要上来四个参数一起调------你不知道是哪个参数引起的效果。
3. 调试速查手册:常见问题 → 解决方法
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 电机完全不转 | IsEnabled()=false 或堵转保护触发 |
发 !START 使能;检查 jointsStateFlag |
| 电机转了但位置不对 | 编码器未校准 | 触发校准(双键同按或 CAN 0x02) |
| 电机抖动/异响 | kp 太大 或 kd 太小 | 按上面方法调 DCE 参数 |
| 跟踪慢半拍 | kv 太小 | 加大 kv |
| 到位后有小偏差 | ki 太小 | 加大 ki |
| 换向时"嘎嘎"响 | kd 太小 或 齿隙未消除 | 加大 kd,检查校准是否完成 |
| CAN 通信超时 | 总线负载太高或硬件故障 | 用示波器看 CAN_H/CAN_L 波形 |
| OLED 显示 FPS 很低 | 某线程栈溢出或死循环 | 检查 FreeRTOS 各线程栈使用量 |
| 机械臂到不了某些位置 | 关节限位或 IK 无解 | #GETJPOS 看当前角度,和 angleLimitMin/Max 对比 |
4. 完整的启动→运动→停止→关机流程
1. 上电
├─ 核心 F407 启动 FreeRTOS → 初始化通信 → 等待 Fibre 协议树
├─ 6 个关节 F103 各自启动 → 读 DIP 开关确定 CAN ID → 加载 EEPROM → 使能 TIM4 20kHz
└─ F103 进入闭环循环(未使能状态,只读编码器不回传电流)
2. 使能
├─ 发 `!START` → SetEnable(true) → CAN 0x01(id=0广播)
├─ 各关节收到 → modeRunning = VELOCITY(或上次的 defaultMode)
└─ FOC 开始输出电流 → 电机有力(但不转,因为 goalPosition=当前位置)
3. 运动
├─ 发 `>30,-20,90,0,0,0`
├─ MoveJ → 算同步速度 → MoveJoints → CAN 0x07
├─ 关节 PositionTracker → DCE → FOC → 电机转动
└─ 全部完成 → Respond("ok")
4. 连续运动
├─ Interruptable 模式:上位机 100Hz 持续发 > 命令
├─ 每次 ParseCommand 更新 targetJoints
└─ 后台 200Hz ControlLoopFixUpdate 持续 MoveJoints
5. 停止
├─ 发 `!STOP` → EmergencyStop → MoveJ(当前位置) → MoveJoints → SetEnable(false)
└─ 所有关节 Sleep → 无力 → 机械臂可手动推动
6. 关机
├─ 发 `!DISABLE` → SetEnable(false)
├─ 发 `#REBOOT 0` → 全部关节重启(可选)
└─ 断电
5. 代码中的"调试钩子"------你可以利用的观察点
| 观察点 | 位置 | 怎么看 |
|---|---|---|
| OLED 上的 6 关节角度 | ThreadOledUpdate 每帧刷新 |
直接看 OLED 屏幕 |
| OLED 上的末端 XYZABC | ThreadOledUpdate 调用 currentPose6D |
直接看 OLED 屏幕 |
| OLED 上的 FPS | 1000000 / (micros() - t) |
正常应该 ≥ 80 FPS |
| 串口 printf | _write() 同时发到 USB + UART4 |
在关节端代码里加 printf() |
jointsStateFlag |
dummy.jointsStateFlag 8 位 |
看 0b1111110 = 全完成 |
| CAN 回复的完成标志 | data[4] 在 0x23 回复中 |
在 OnCanMessage 里加日志 |
| 堆剩余空间 | xPortGetMinimumEverFreeHeapSize() |
启动时输出一次 |
最实用的调试技巧 :在关节端 CloseLoopControlTick() 里,每 1000 次循环(= 50ms)printf 一次 estPosition, softPosition, goalPosition,用这三个值画图就能看出跟踪效果。
6. Day 7 检查清单
- 能不看文档,从"上位机发命令"到"电机转动"完整口述整个数据流
- 能说出 DCE 四参数各自的作用和调参顺序
- 知道 Tuning 模式下怎么设频率和幅度
- 知道怎么通过 #GETJPOS 和 OLED 观察跟踪效果
- 知道常见问题(不动/抖动/慢半拍/异响)的排查方向
- 能说出 FreeRTOS 8 个线程中哪几个和运动控制相关
- 知道 jointsStateFlag 什么时候变成 0b1111110
- 能把 CAN 0x07(发位置+速度)、0x23(回位置+完成)、0x01(使能)三条最常用的命令和代码位置对应上
7. 六天学习总结:你最需要记住的 10 个文件
按学习优先级排序:
| 优先级 | 文件 | 为什么重要 |
|---|---|---|
| ★★★ | motor.cpp (F103) |
整个闭环控制的大脑------CloseLoopControlTick() |
| ★★★ | dummy_robot.cpp (F407) |
MoveJ/MoveL/ParseCommand------多轴协调核心 |
| ★★★ | interface_can.cpp (F103) |
CAN 命令全集------关节和核心的通信协议 |
| ★★ | 6dof_kinematic.cpp (F407) |
FK + IK------运动学数学核心 |
| ★★ | motion_planner.cpp (F103) |
5 种 Tracker------运动平滑的关键 |
| ★★ | tb67h450_base.cpp (F103) |
FOC 电流矢量输出------硬件驱动 |
| ★★ | main.cpp (F407 UserApp) |
FreeRTOS 线程创建------系统启动入口 |
| ★ | communication.cpp (F407) |
通信初始化------USB/UART/CAN 全部在此启动 |
| ★ | encoder_calibrator_base.cpp (F103) |
编码器校准------系统精度的基础 |
| ★ | can.c (F103 Core) |
CAN 硬件配置 + 接收中断------通信底层 |
8. 结语
六天的精读计划到此结束。总结一句话:这个项目的精髓在于分布式 FOC 闭环(关节端 20kHz)+ CAN 实时通信 + 6-DOF 运动学协调(核心端 200Hz) 三层架构。把这三层的接口和职责边界理解清楚,你就掌握了这个机械臂固件的全部核心思想。