35 KiB
底盘开发文档
适用对象:哨兵机器人麦轮底盘(4 × GM3508 + C620 电调) 目标平台:Allwinner T153(Cortex-A7,Linux,armv7)+ CANable2(slcan)或板载 SocketCAN 控制频率:1 kHz(2ms 是旧方案;本项目实际为 1ms 节拍) 文档目的:从零到上车的完整开发路径——接口约定、标定步骤、自动整定与超参数、安全与故障处理
目录
- 范围与文档分工
- 快速上手
- 硬件与接线
- 坐标、单位与符号约定
- 软件架构
- 关键机制(实车踩过的坑)
- 控制设计
- 配置参考(chassis.toml)
- 标定与上车流程
- 自动整定与超参数预设
- 台架工具
- 安全设计
- 测试与质量门禁
- 部署
- 故障排查
- 术语表
1. 范围与文档分工
| 文档 | 回答的问题 | 读者 |
|---|---|---|
ARCHITECTURE.md |
职责型 workspace、允许依赖、std/unsafe 策略、实现边界 | 架构决策者 |
docs/architecture.md |
架构文档入口,不复制规范 | 改代码的人 |
| 本文 | 底盘这一块怎么开发、标定、整定、上车与排障 | 底盘开发 / 调试 / 上车的人 |
本文只覆盖四轮底盘切片;云台、发射机构与 DETA10 真实载荷布局不在当前范围内。
2. 快速上手
2.1 无硬件(macOS / Linux / CI 均可)
cargo test --workspace --all-targets # 单测 + 闭环仿真 + 整定,全部无需硬件
cargo run -p chassisd -- --mock # 进程内电机模型的全链路演示
cargo run -p chassis-tune -- presets # 看有哪些整定超参数预设
just tune-smoke # 确定性整定冒烟(CI 同款)
just lint # fmt + clippy(与 CI 同标准)
2.2 上车(详细步骤见第 9 节)
# 1) 只验链路与反馈,绝不发非零电流(接线排查第一步)
cargo run --release -p spin -- chassis --device /dev/ttyACM0 --dry-run
# 2) 四轮定性旋转(轮子必须架空或整车固定,急停可及)
cargo run --release -p spin -- chassis --device /dev/ttyACM0 --rpm 500
# 3) 先离线整定,再上台架复验(用版本化的超参数预设)
cargo run --release -p chassis-tune -- sweep --preset config/tune.toml --preset-name sim_bench_model
cargo run --release -p chassis-tune -- sweep --preset config/tune.toml --preset-name bench_noload \
--backend bench --device /dev/ttyACM0 --i-know-the-wheel-spins
# 4) 把结果写回配置并跑守护进程
cargo run --release -p chassis-tune -- sweep --config config/chassis.toml.example --motor front_left \
--preset config/tune.toml --preset-name sim_bench_model --write-config /tmp/chassis.tuned.toml
chassisd --config /tmp/chassis.tuned.toml --interface can0
3. 硬件与接线
3.1 拓扑
T153 (Linux) Gm3508 × 4 + C620
├── USB CDC /dev/ttyACM0 ──► CANable2 (slcan 固件) ──► CAN 总线 ──► 4 个电调
└── 板载 CAN 控制器 can0 ──────────────────────────► (二选一)
两条路径互斥地二选一,应用层无感知(都由 CanLink 抽象屏蔽):
| 路径 | 打开方式 | 适用 |
|---|---|---|
slcan(slcan) |
用户态直接读写 /dev/ttyACM0(Lawicel ASCII 协议) |
免 ip link、绕开 gs_usb 驱动;台架与初始联调首选 |
SocketCAN(socketcan) |
ip link set can0 up type can bitrate 1000000 restart-ms 100 |
板载原生 CAN;生产部署 |
3.2 设备与协议常量(C620 手册)
| 项目 | 值 |
|---|---|
| 控制帧 ID | 0x200(电调 1–4)、0x1FF(电调 5–8) |
| 控制帧格式 | 8 字节 = 4 × i16 大端电流命令,按电调 ID 顺序 |
| C620 电流标度 | ±16384 ↔ ±20 A |
| C610 电流标度 | ±10000 ↔ ±10 A(M2006 组合;接硬件前需核对) |
| 反馈帧 ID | 0x200 + 电调ID,即 0x201–0x208 |
| 反馈帧格式 | u16 角度(0..8191) + i16 转速 rpm + i16 转矩电流 + i8 温度℃,均大端 |
| 反馈频率 | 每电调 1 kHz |
| 减速比 | GM3508 = 3591 / 187 ≈ 19.203(转子 : 输出轴) |
C620 内部只闭电流环:转速环 / 位置环由主控侧实现(motor-control::Pid,挂在 EscBus 的 1 kHz 节拍上)。
3.3 电调 ID 与安装方向
默认约定(config/chassis.toml.example、spin chassis 默认值一致):
| 轮位 | 轮序下标 | 电调 ID | 安装方向 |
|---|---|---|---|
| 前左 FL | 0 | 1 | +1 |
| 前右 FR | 1 | 2 | -1 |
| 后左 RL | 2 | 3 | +1 |
| 后右 RR | 3 | 4 | -1 |
左右镜像安装(direction 相反)使四轮在"底盘前进"时物理同向。direction = -1 的电机
由驱动层在写命令与读反馈两个方向上统一翻转符号,上层永远看到"正转 = 前进方向"。
3.4 电流限幅
| 场景 | 建议限幅(raw) | 说明 |
|---|---|---|
空载台架(spin chassis 默认) |
1500(≈1.8 A) | 需越过静摩擦(实测 400~600 raw)并留整定余量 |
| 整车实车 | 6000 ~ 12000 | 视供电与机械而定;同时是 PID 抗饱和的钳位点 |
限流不是"越大越安全":它同时是 PID 输出的钳位点。设得远大于实际所需, 积分会在堵转期间持续积累,一旦起动就冲过头。必须贴近实际所需电流。
3.5 接线前置条件(缺一不可)
- 电调供电与主控共地;CAN_H / CAN_L 不接反,两端 120Ω 终端电阻齐全。
- 轮子架空或整车固定;急停开关在手可及处。
- 首次上电先
--dry-run,确认四轮反馈都在刷(spin chassis会打印每个电调的 转速 / 温度 / 反馈年龄)。
4. 坐标、单位与符号约定
4.1 机器人坐标系(ISO)
- x 前、y 左、ω 绕 z 逆时针为正(
mecanum::Twist); - 轮序固定为 前左 FL、前右 FR、后左 RL、后右 RR(常量
mecanum::FL/FR/RL/RR,chassis.toml的[[motor]]顺序、--esc-ids/--directions的顺序都遵循它); - 辊子 45°、俯视 X 型布置(RoboMaster 标准布局)。
4.2 运动学(mecanum)
设 a = half_length + half_width,四轮线速度:
v_FL = vx - vy - a·ω
v_FR = vx + vy + a·ω
v_RL = vx + vy - a·ω
v_RR = vx - vy + a·ω
正解是逆解的左伪逆(JᵀJ 对角,可解析求平均)。实现见
mecanum:Mecanum::wheel_speeds / twist /
wheel_rpms / twist_from_wheel_rpms。
4.3 单位与层级(最容易出错的地方)
| 量 | 所在层 | 单位 |
|---|---|---|
Twist.vx/vy |
应用 / 控制 | m/s |
Twist.omega |
应用 / 控制 | rad/s |
| 轮端转速 | OutputShaft 呈现 |
rpm(输出轴) |
| 转子转速 | EscBus / EscMotor / MotorState::rotor_speed_rpm |
rpm(转子) |
| 控制量 effort | 驱动 / PID | 协议 raw(ESC = 电流,C620 ±16384) |
kp/ki/kd/feedforward |
速度环 | raw/rpm、raw/(rpm·s)、raw/(rpm/s)、raw/rpm |
- 减速比换算只发生在一个地方:
motor / esc-bus::OutputShaft包装(转子 = 输出轴 × gear_ratio)。EscBus/EscMotor只认转子,绝不碰减速比;EscConfig::gear_ratio仅作配置元数据。 - PID 的接线顺序:
ChassisController算出轮端 rpm →OutputShaft换算成转子 rpm →EscMotor的速度环以转子 rpm 闭环。
5. 软件架构
5.1 职责边界
完整 crate 清单和允许依赖见 ARCHITECTURE.md。底盘路径上的关键职责是:
| 职责 | crate |
|---|---|
| CAN 帧、总线接口、链路与 Mock | can |
| 电调协议 | motor-protocol |
| 设备契约、命令/状态/减速比/注册表 | motor |
| 帧泵、租约、反馈新鲜度、零电流 | esc-bus |
| PID、速度/位置环、组合策略 | pid、motor-control |
| 麦轮运动学 | mecanum |
| 配置 schema | chassis-config |
| 台架安全与采样 | bench |
| 应用入口 | chassisd、spin、chassis-tune |
crate 内源码保持扁平,不建立 src/<module>/ 子目录。
5.2 依赖与组合
esc-bus 不依赖 PID 或 motor-control。motor::EffortPolicy 是两者的组合点:
应用构造 motor-control::MotorPolicy,并把它传给 EscBus::spawn。
总线仍然在 1 kHz 节拍上运行策略,因此控制时序不变;变化的是控制律不再被封在总线驱动里。 反馈离线、命令租约到期和模式切换会把对应策略状态复位并强制零输出。
bench 只产生试验轨迹和安全中止原因;指标、成本、搜索和报告适配由
tuning 与 chassis-tune 完成。
6. 关键机制(实车踩过的坑)
6.1 命令是租约,不是设定值
EscMotor::set_velocity / set_effort_raw / set_position 每次调用即续租。超过
BusOptions::command_timeout(默认 50 ms)没有新命令,驱动强制 0 电流。
- 这是"上层任务死亡后旧指令继续输出"的防线;
- 因此上层必须逐拍重发同一个设定值(
chassisd的 1 kHz 循环天然满足); - 只在开头下发一次的台架脚本会在 50 ms 后自动失电——这是预期行为,不是 bug。
6.2 命令队列必须浅
驱动按 1 kHz 生产命令,而 slcan 一次 write 在 CDC ACM 上约 2 ms(下行只有几百帧/秒)。 若命令方向缓冲很深,队列会被过时命令灌满,ESC 执行的是一秒多以前算出的电流—— 速度环等于开了 1.4 s 延迟,轮子必然跑飞。
因此 slcan 桥的 CHANNEL_BOUND = 8:send().await 立即回压,每一帧都按最新反馈现算;
反馈方向是采样流,来不及就丢(try_send),绝不阻塞泵线程。
6.3 桥的两个方向不能互相饿死
- 发送有上界(
TX_BURST),一帧写不动时立刻让位给接收; - 接收批量排空(
RX_BURST); serialcrate 的TTYPort读写共用同一个超时,写超时属正常现象,不能当致命错误 (否则一次写超时就会把接收方向一起带停)。
6.4 反馈离线
任一电机反馈超时(feedback_timeout,默认 100 ms)→ 该电机命令强制 0 电流并告警。
顶层另有上电门控:任一电机从未收到过反馈前,整组不下发速度命令。
7. 控制设计
7.1 PID(motor-control::Pid)
并联式 PID,显式时间步 dt(秒),特性:
- 输出限幅
[out_min, out_max](速度环里取±current_limit); - 积分抗饱和(clamping):输出已饱和且误差继续同向时暂停积分;
- 微分作用于测量值(不是误差),避免设定值阶跃的微分冲击;
- 可选微分一阶低通(
with_derivative_tau),抑制编码器量化噪声; dt <= 0或非有限输入 → 返回上次输出且不更新内部状态(防御跳帧 / 首帧)。
7.2 速度环(motor-control::VelocityController)
effort_raw = clamp( PID(target_rpm, measured_rpm, dt) + feedforward × target_rpm, ±limit )
feedforward 单位 raw / rpm:稳态下所需控制量与转速近似成正比。
- 空载台架必须 ff = 0:轮子架空时维持转速只需克服摩擦的极小电流,任何接近
1/K(GM3508 空载K ≈ 0.57 rpm/raw→1/K ≈ 1.77)的前馈都会把空载轮子直接推到最高转速。 - 带载才需要正前馈(抵消地面摩擦),典型 ≈ 1.77 raw/rpm,必须按实车整定。
- 前馈可能把输出推出限幅,因此
update在加完前馈后再夹一次。
7.3 增益的物理直觉(空载台架)
| 现象 | 原因 | 方向 |
|---|---|---|
高 kp(8~18)震荡 |
被控对象近似积分器(≈12 rpm/s per raw)+ 反馈延迟 | 降 kp、提 ki |
ki 太小长期停在 300~400 rpm |
静摩擦需要积分缓慢"顶"到起动电流(stick-slip) | 提 ki(有 current_limit 钳位兜底) |
| 一起动就飞 | 前馈过大 or 限流远大于实际所需 | ff → 0、限流贴近实际 |
| 稳态有静差 | 摩擦未被积分吃掉 | 检查 ki 是否被抗饱和长期冻结 |
7.4 被控对象模型(motor-model::MotorPlant)
逐拍离散(dt 秒,电调坐标系:正电流 → 正转速):
- 命令延迟:
current_raw进 FIFO,delay_ticks拍后才作用; - 摩擦:运动时
net = u − sign(rpm)·coulomb − viscous·rpm;静止且|u| > static时net = u − sign(u)·coulomb;静止且|u| ≤ static时net = 0(粘住); - 一阶转速:
rpm_target = clamp(net × gain, ±max_rpm) − load_rpm,以时间常数tau一阶趋近; - 量化:机械角按转速积分并对 8192 取模,角度截断为
u16、转速为i16(量化由协议类型自带)。
PlantParams::gm3508() 是无摩擦无延迟的理想模型;gm3508_bench() 叠加实测量级的
静 / 库仑摩擦(400~600 raw)与 2 拍延迟,是整定的默认被控对象。
8. 配置参考(chassis.toml)
样例:config/chassis.toml.example。
schema 由 chassis-config 定义,守护进程与整定工具共用同一份——
这样"整定工具用的限流"和"守护进程用的限流"不可能悄悄不一致。
8.1 顶层
| 键 | 含义 |
|---|---|
[can] interface |
底盘总线 SocketCAN 接口名(如 can0) |
[can.function] interface |
预留的功能总线(云台 / 拨弹),必须与底盘总线不同名 |
[remote] |
可选 DR16/D-BUS 遥控输入;缺省时保持静止 |
[chassis] |
控制循环与安全超时、速度上限 |
[mecanum] |
half_length / half_width / wheel_radius(米) |
[[motor]] × 4 |
顺序固定 FL / FR / RL / RR |
8.2 [remote](可选)
| 键 | 默认 | 含义 |
|---|---|---|
kind |
必填 | 当前支持 dr16 |
device |
/dev/ttyS2 |
D-BUS UART 设备 |
timeout_ms |
100 | 合法帧断流后立即失能 |
deadband |
0.05 | 摇杆满量程死区,之后使用平方曲线 |
ramp_ms |
500 | 零到满量程逐轴斜坡;失能/断流立即归零 |
invert_vx/vy/omega |
false | 实车方向校正 |
8.3 [chassis](缺省值)
| 键 | 默认 | 含义 |
|---|---|---|
control_period_ms |
1 | 控制节拍(1 kHz) |
command_timeout_ms |
200 | 上层 twist 超时 → 目标速度归零 |
feedback_timeout_ms |
100 | 反馈离线 → 强制 0 电流 |
motor_command_timeout_ms |
50 | 驱动层命令租约(必须 ≥ control_period_ms) |
max_vx / max_vy / max_omega |
3.0 / 3.0 / 6.0 | 速度限幅(m/s、m/s、rad/s) |
8.4 [[motor]]
| 键 | 含义 |
|---|---|
name |
轮名(front_left …),整定的 --motor 用它选目标 |
esc_id |
电调 ID(1–8,即 CAN 拨码) |
model |
C620 / C610 |
direction |
安装方向 +1 / -1 |
gear_ratio |
减速比(转子 : 输出轴),GM3508 ≈ 19.203 |
current_limit |
effort 限幅(raw),同时是 PID 钳位点 |
kp / ki / kd / feedforward |
速度环增益(整定的产物写回这里) |
8.5 校验规则(Config::validate)
- 电机数必须恰好 4;麦轮几何必须为正;四个超时必须 > 0;
motor_command_timeout_ms ≥ control_period_ms;速度上限 > 0;gear_ratio > 0; - 底盘总线只允许一种协议族(四轮不能混用 C620 / C610);
- 功能总线接口名不得与底盘总线相同(双 CAN 物理隔离);
[remote]若启用:设备名非空,timeout_ms/ramp_ms> 0,deadband ∈ [0, 0.5), 且遥控断流超时不得晚于底盘指令超时;deny_unknown_fields:拼错的键直接报错,不会被静默忽略。
9. 标定与上车流程
每一步都有明确的通过判据。不要跳步——跳过的步骤会在后面以"轮子跑飞"的形式还回来。
| # | 步骤 | 命令 | 通过判据 |
|---|---|---|---|
| 1 | 链路与反馈 | spin chassis --dry-run |
四个电调都在刷反馈(转速 ≈ 0、温度正常、反馈年龄 < 100 ms)。缺一个就查线 / 电调 ID |
| 2 | 电调 ID 对应 | 同 1,逐个拔插 | 拔掉哪个电调,哪个 ID 的反馈消失 → ID 映射正确 |
| 3 | 方向 | spin chassis --rpm 200(低速) |
四个轮子都朝"底盘前进"方向转;反了就改 direction |
| 4 | 减速比 / 轮径 | 手转一圈 + 读反馈角度 | 反馈角度 × 减速比 ≈ 轮子转过角度;不符则改 gear_ratio |
| 5 | 几何参数 | 卷尺量 half_length / half_width / wheel_radius |
填入 chassis.toml(影响运动学精度,不影响稳定性) |
| 6 | 限流 | 从 1500 raw 起,逐步加到"能可靠起动且不冲" | 越小越好;实车再按需上调 |
| 7 | 空载整定 | chassis-tune sweep --preset-name bench_noload |
出一组增益:超调 ≤ 25%、稳态误差 ≤ 8%、三次复验一致 |
| 8 | 带载复验 | 整车落地,--preset-name bench_loaded |
超调 ≤ 15%、稳态误差 ≤ 5%;空载参数直接上车通常不行 |
| 9 | 双向验收 | --preset-name field_updown |
升速 / 降速 / 回零三个方向都达标,重复性 ≥ 4/5 |
| 10 | 固化 | --write-config + 人工评审 |
增益写入 chassis.toml,限流 / 方向 / 减速比未被改动 |
关于"仿真整定能不能直接上车":不能。仿真最优解只在被控对象模型内最优,模型里的 摩擦 / 延迟是量级估计且不含测量噪声,因此可能偏激进。必须用同一场景在台架上复验 (第 10.6 节的迁移证据)。
10. 自动整定与超参数预设
手动试凑 kp/ki/kd/ff 既慢又难复现。chassis-tune 把它变成参数空间搜索 + 版本化超参数。
10.1 搜索算法
两阶段:
- 粗扫:Latin Hypercube 分层采样 +
rayon并行评估,用有限次数铺满四维空间; - 精修:以成本最低的 top-k 为起点,在坐标空间做多起点
argmin局部搜索 (Nelder-Mead 默认,可选 PSO)。
坐标空间是关键的数值技巧:kp/ki/kd 合法取值跨数量级且必须为正,每个轴映射到
(对数轴取 ln,线性轴恒等)之后——
- 无约束优化器不会走出正增益区域;
- 步长在不同数量级上等价(±0.1 坐标步长 = ×1.1 比例步长);
LogOrZero轴在粗扫阶段可精确取 0(ff = 0是台架常态),在精修阶段退化为对数轴 (零不可导,本地搜索无收益)。
10.2 成本函数与硬约束
成本 = 归一化加权(调节时间 / 超调 / IAE / 电流),各项已归一化到 O(1),因此权重在不同 场景 / 电机之间可解释。
硬约束不进权重:过速、饱和占比过高、不收敛 → 直接给罚值 1e9 并记录判定。
否则搜索会主动挑一个"飞车但其它项很好看"的解。
| 判定 | 触发条件 |
|---|---|
rejected |
增益含 NaN/inf,或场景退化 |
diverged |
数值发散 |
overspeed |
峰值转速 > 目标 × 3 + 500 |
saturated |
饱和占比 > 40% |
no-settle |
场景内未收敛 |
10.3 鲁棒优化(默认)
单点模型整定出的增益对增益 / 时间常数 / 延迟高度敏感,因此默认在 PlantEnsemble
(域随机化:增益 ±20%、时间常数 ±30%、延迟 0~3 拍、摩擦 ±50%,8 个成员)上评估,
成本按 robust_weight 在"均值"与"最坏成员"之间折衷(默认 0.5)。
10.4 超参数预设(config/tune.toml)
这是"实验条件"的版本管理:搜什么区间、用哪个场景、什么算好、什么算达标、搜多细,
全部写在 config/tune.toml 里,评审一次、长期复用、任何机器上可复现。
优先级(逐字段覆盖):
内置默认 < 预设文件里的预设 < 显式命令行参数
因此"用预设跑一次 + 临时改一个权重"是安全的,不会把其它超参数打回硬编码值。
chassis-tune presets # 列出全部预设(只读)
chassis-tune sweep --preset config/tune.toml # 用 default_preset
chassis-tune sweep --preset config/tune.toml --preset-name bench_loaded
chassis-tune sweep --preset-name bench_loaded # 缺省路径即 config/tune.toml
chassis-tune sweep --preset config/tune.toml --samples 128 # CLI 覆盖预设的单项
随仓库提供的预设:
| 预设名 | 后端 | 场景 | 用途 |
|---|---|---|---|
sim_bench_model(默认) |
sim | ramp-and-hold 1500 rpm | 台架量级模型全量搜索:评审、CI、代码回归的基准 |
sim_ideal |
sim | ramp-and-hold 1500 rpm | 理想模型秒级回归;结果不可直接上车 |
bench_noload |
bench | ramp-and-hold 800 rpm | 空载台架:小样本局部搜索,前馈固定为 0 |
bench_loaded |
bench | ramp-and-hold 1200 rpm | 带载(整车落地):前馈区间上调至 0.5~2.5 |
field_updown |
bench | up-down 1200 rpm | 实战验收:升 / 降 / 回零双向,阈值收紧到 12% / 3% |
预设 schema(version = 1,未知键报错,见
crates/tooling/tuning/src/lib.rs):
version = 1
default_preset = "sim_bench_model"
[preset.bench_noload]
description = "空载台架:摩擦小、静差靠积分压制"
backend = "bench"
[preset.bench_noload.space]
kp = { min = 0.2, max = 12.0, scale = "log" } # scale: linear | log | log0
feedforward = { min = 0.0, max = 0.0, scale = "linear" } # 单点区间 = 搜索时恒定
[preset.bench_noload.cost] # settle / overshoot / iae / effort
[preset.bench_noload.acceptance] # max_overshoot_pct / max_ss_error_pct
[preset.bench_noload.search] # seed / samples / top_k / solver / max_iters / particles / max_evals / threads
[preset.bench_noload.sim] # plant / ensemble / robust_weight / gain_pct / tau_pct / delay_ticks_max / friction_pct
[preset.bench_noload.bench] # max_rpm / max_temp_c / trial_timeout_ms / between_trials_ms /
# startup_timeout_ms / continue_on_abort / validate_runs
每个小节都可缺省,只写"与默认不同"的项;刻度(scale)与区间可以分别覆盖
(--kp-scale linear 不改区间)。
安全边界:
current_limit、direction、gear_ratio、电调 ID、串口设备名 不是预设字段——写进预设文件会直接报错。它们只来自chassis.toml或显式 CLI, 否则预设文件就成了绕过限流的口子。
新增一个预设:在 config/tune.toml 里加一节 → 跑
chassis-tune presets 确认能解算 → 跑一次小样本
chassis-tune sweep --preset config/tune.toml --preset-name <新名> --samples 16 确认可用。
测试会强制:每个预设都有 description;bench_* / field_* 必须 backend = "bench"
(防止"以为在台架验过、其实是离线跑的")。
10.5 产物与判读(--out-dir)
| 文件 | 内容 |
|---|---|
trials.csv |
全部候选 + 判定(可复现性 diff 的基准) |
best.toml |
可直接粘贴的增益片段 |
best-trace.csv |
最优解的轨迹 |
report.md |
人类可读报告:参数空间、权重、验收结论、边界提示 |
summary.json |
机器可读摘要 |
hil-validation.csv |
仅真机后端:同一组增益的重复性复验 |
判读要点:
- 最优解贴到参数空间边界时报告会明确提示——那是"约束在决定结果",应放宽
--kp/--ki再搜,而不是把边界值当最优解交付; - 达标数 / 评估数太低(如 9/198)说明大部分候选被硬约束拦下,通常意味着限流太小 或参数空间太宽;
- 真机
hil-validation.csv的离散度比均值更重要:三次结果差很多 = 参数贴着噪声,不该上车。
10.6 真机后端与迁移证据
--backend bench 与仿真实现同一个 Evaluator trait,区别只是"评估一组增益"从浮点积分
换成真机试验:打开链路 → 上电门控 → 逐次热更新增益(不重启总线)→ 逐拍重发设定值 +
安全包络 → 任何退出路径先 0 电流 → 试验间静默。搜索强制单线程(只有一条总线)。
仿真整定 → 台架复验(同场景、同阈值)→ 重复性达标 → 写回 chassis.toml → 整车验收
可复现性:同一 seed + 同一参数空间 + 同一后端 → 逐位一致。CI 的
just tune-smoke 依赖这一点(同一命令跑两次,diff trials.csv 必须为空)。
11. 台架工具
11.1 spin chassis(spin,slcan,全平台)
四轮同时以固定转子转速旋转。关键参数:
| 参数 | 默认 | 说明 |
|---|---|---|
--device |
/dev/ttyACM0 |
slcan 串口设备 |
--serial-baud |
2000000 | CDC ACM 一般忽略;CH340 类需与固件一致 |
--serial-timeout-ms |
50 | 读写共用 |
--can-speed |
1m |
1m / 500k |
--esc-ids |
1,2,3,4 |
顺序 = FL、FR、RL、RR |
--directions |
1,-1,1,-1 |
各轮安装方向 |
--rpm |
500 | 目标转子转速(轮端 = 转子 ÷ 减速比) |
--gear-ratio |
19.203 | 减速比 |
--current-limit |
1500 | raw;同时是 PID 钳位点 |
--ramp-ms |
3000 | 缓升时间(让积分在起动前平滑建立,避免"先憋住、一动就飞") |
--hold-secs |
10 | 目标转速保持时长 |
--startup-timeout-ms |
3000 | 等待全部电调反馈的超时 |
--dry-run |
— | 只验证链路与反馈,不发任何非零电流 |
--allow-partial |
— | 允许缺失部分电调(默认要求四个全在线) |
--kp/--ki/--kd/--feedforward |
2 / 10 / 0 / 0 | 空载台架默认值(见 7.3) |
--max-rpm |
目标 × 2 + 200 | 过速保护上限 |
安全与使用要点:
- 轮子必须架空或整车固定;ctrl-c 随时安全停机(先 0 电流再关总线);
- 过速保护:任一电调超过
--max-rpm持续 60 ms 即立即停机; - 上电门控:四个电调都上报新鲜反馈前绝不下发速度;
- 空载的合适整定与带载完全不同,空载不要加前馈。
11.2 spin motor(spin,仅 Linux)
单电机链路验证,走 SocketCAN。用于板载原生 CAN 的接线排查:
ip link set can0 up type can bitrate 1000000 restart-ms 100
cargo run --release -p spin -- motor -- --interface can0 --dry-run
12. 安全设计
按"故障点 → 防线"排列,每一层都独立生效:
| # | 故障 | 防线 | 默认阈值 |
|---|---|---|---|
| 1 | 上层任务死亡,旧指令继续输出 | 命令租约:超时未刷新 → 0 输出 | 50 ms |
| 2 | 电机 / CAN 掉线 | 反馈离线:超时 → 该电机 0 电流 + 告警 | 100 ms |
| 3 | 上电瞬间反馈未到就发速度 | 上电门控:全部见到反馈前全体 disable |
— |
| 4 | 上层不再发指令 | 指令超时:twist 归零 | 200 ms |
| 5 | 轮子飞车 | 台架 过速保护(超过上限持续 60 ms 停机) | 目标 × 2 + 200 rpm |
| 6 | 堵转烧电机 | 限流(同时是 PID 钳位点,抗饱和) | 台架 1500 / 实车 6000~12000 raw |
| 7 | 电调过热 | 台架温度保护 | 70 ℃(空载预设 60 ℃) |
| 8 | 整定 / 台架脚本失控 | 试验超时 + 立即 0 电流 + 试验间静默 | 6 s / 500 ms |
| 9 | 误触真机模式 | --i-know-the-wheel-spins 强制显式确认(预设也无法绕过) |
— |
| 10 | 命令队列积压导致延迟 | slcan 桥 CHANNEL_BOUND = 8 + send().await 回压 |
— |
| 11 | 进程被杀 | systemd Restart=always(板端) |
1 s |
出事时的正确操作顺序:① 急停 / 断电 → ② ctrl-c 让进程走优雅停机(先 0 电流)→
③ 记下 --dry-run 与 hil-validation.csv 的现象 → ④ 再分析。
13. 测试与质量门禁
13.1 分层测试
| 层级 | 内容 | 平台 |
|---|---|---|
| 单元 | 帧编解码字节级样例 / 饱和、多圈回绕、PID(阶跃、抗饱和、滤波)、运动学解析解 + 属性测试、slcan 帧形状过滤 | 全平台 |
| 闭环仿真 | 内存通道 + 一阶电机模型:4 电机速度环收敛、群帧组装、离线安全、命令租约到期与续租、增益热更新、守护进程全链路 | 全平台 |
| 模型一致性 | 同步 harness 与 EscBus 异步闭环在同一组增益下的稳态 / effort 对齐(防止"对着错模型调参") |
全平台 |
| 整定 | 指标(合成轨迹解析解)、参数空间(界内 / 确定性 / 对数分布 / 分层覆盖)、成本(硬约束罚值)、搜索(同 seed 逐位复现、优于手调基线)、报告(CSV/TOML/JSON 往返) | 全平台(CI 跑 just tune-smoke) |
| 预设 | schema 往返、逐字段覆盖、优先级(CLI > 预设 > 内置)、未知键 / 越界 / 缺失报错、config/tune.toml 全部预设可解算、命名与后端一致、安全参数不可注入 |
全平台 |
| vcan 集成 | vcan0 双 socket 收发,真实内核 CAN 栈 | Linux(#[ignore],CI 显式跑) |
| 实车台架 | spin chassis 四轮定速:链路/反馈、方向、转速稳态、过速保护、安全停机;spin rc 只读监视 DR16;chassis-tune --backend bench 逐次试验 + 重复性复验 |
armv7 + slcan/SocketCAN(人工) |
| 交叉检查 | aarch64-unknown-linux-gnu 全 workspace check;armv7 静态构建 chassisd |
CI |
13.2 每个阶段完成后的门禁(just lint + 以下)
cargo build --workspace # 无警告
cargo test --workspace # 全绿
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
cargo check -p chassis-tune --features plot --all-targets # 可选 feature 防腐烂
just tune-smoke # 同 seed 逐位一致 + 达标
新增代码要求:有文档注释;关键路径有测试覆盖;跨 crate 的常量(如限流、减速比)只有一个来源。
14. 部署
14.1 交叉编译(musl 静态,无需交叉工具链)
just build-armv7 # cargo build --release -p chassisd --target armv7-unknown-linux-musleabihf
just deploy # 编译 + 推送 + mock 试运行(默认 root@192.168.2.2)
目标板实测:创龙 T153 / Ubuntu 22.04(armv7)。发布二进制使用 musl 静态链接。
14.2 板端拉起 CAN
# SocketCAN(板载原生控制器)
ip link set can0 type can bitrate 1000000 restart-ms 100
ip link set can0 up
ip -d link show can0
# slcan(USB 适配器):不需要 ip link,应用层直接打开串口
chmod 666 /dev/ttyACM0
14.3 systemd
ExecStart=/usr/local/bin/chassisd --interface can0 --config /etc/chassisd/chassis.toml
Restart=always
RestartSec=1
# 实时性(按需打开):
# CPUSchedulingPolicy=fifo
# CPUSchedulingPriority=20
# LimitRTPRIO=95
电调未上电 / CAN 线未接时,SocketCAN 侧会因无 ACK 进入 bus-off (
ip -d link show can0可见),chassisd侧表现为四电机反馈离线、强制零电流—— 这正是安全设计的预期行为,不是故障。调度策略:应用不调用 libc FFI。 应用不调用 libc FFI。
SCHED_FIFO与资源限制由 systemd 服务单元设置;std 程序不调用mlockall。
15. 故障排查
| 症状 | 可能原因 | 处理 |
|---|---|---|
/dev/ttyACM0 打不开 |
权限 / 设备名不符 / 固件非 slcan | ls -l /dev/ttyACM*;chmod 666;确认适配器是 Lawicel 固件(不是 gs_usb) |
| 四个电调都无反馈 | CAN_H/L 接反、无终端电阻、电调未上电、波特率不符 | 万用表量差分电阻(约 60Ω);--dry-run 复查;确认 --can-speed 1m |
| 只有部分电调有反馈 | 电调 ID 冲突 / 拨码错 / 该段线断 | 逐个拔插定位(流程第 2 步) |
| 上电后轮子纹丝不动 | 正常:上电门控未满足 | 等反馈同时打印"waiting for first feedback";查反馈丢失原因 |
| 台架脚本 50 ms 后失电 | 命令租约到期(只下发了一次) | 每拍重发命令;这不是 bug |
| 轮子"先憋住、一动就飞" | 前馈过大 / 限流远大于实际所需 / 缓升太短 | ff → 0;限流贴近实际;--ramp-ms 加大 |
| 速度环高频震荡 | kp 太大(近似积分器 + 延迟) |
降 kp、提 ki;参考第 7.3 节 |
| 稳态长期差 100+ rpm | 静摩擦未被积分吃掉 | 提 ki;确认 current_limit 大于静摩擦(400~600 raw) |
| 整定报告说"贴边界" | 约束在决定结果 | 放宽对应轴区间再搜,别交付边界值 |
| 仿真整定的参数上车就飞 | 模型不含摩擦 / 噪声,仿真解偏激进 | 用 bench_loaded / field_updown 复验;以台架结果为准 |
trials.csv 两次不一致 |
改了参数空间 / 预设 / 后端 | 检查 超参数来源 打印行;同 seed 必须同空间同后端 |
| 守护进程起来就退出 | 配置校验失败 / --config 缺失 |
看 stderr;Config::validate 的报错会指出具体键 |
| 抖动大(>500 µs) | 标准 Linux 非硬实时 | 先降 chassis 控制频率或合并任务;再考虑调度优先级 / 异构核(ARCHITECTURE.md 决策点 D4) |
16. 术语表
| 术语 | 含义 |
|---|---|
| SLCAN | Serial CAN,通过串口传输 CAN 帧的 ASCII(Lawicel)协议 |
| SocketCAN | Linux 内核 CAN 子系统 |
| raw | 协议原始控制量单位(C620 电流 ±16384 ↔ ±20 A) |
| 命令租约 | set_* 每次调用续租,超时未刷新即强制 0 输出 |
| 上电门控 | 未见全部电机反馈前不下发速度命令 |
| 转子 / 轮端 | 电机轴 / 减速器输出轴;换算只在 OutputShaft 发生 |
| 域随机化 | 在一组扰动参数(增益 / 时间常数 / 延迟 / 摩擦)上做鲁棒优化 |
| 代价空间 | 对数(或恒等)映射后的参数坐标,使正增益约束天然满足 |
| 超参数预设 | 版本化的搜索条件:空间 / 场景 / 权重 / 阈值 / 预算 / 被控对象 / 安全包络 |
| HIL | Hardware-in-the-loop,此处指 --backend bench 的真机逐次试验 |
相关文档
- 整体架构规划
ARCHITECTURE.md - 已实现架构与解耦点
docs/architecture.md - 配置样例
config/chassis.toml.example - 整定超参数预设
config/tune.toml - 项目 README
| 版本 | 日期 | 变更 |
|---|---|---|
| 1.0 | 2026-09-23 | 首版:底盘接口约定、标定流程、自动整定与超参数预设、安全与故障处理 |