Files
linux_sentry_chassis/docs/chassis.md
T

749 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 底盘开发文档
**适用对象**:哨兵机器人麦轮底盘(4 × GM3508 + C620 电调)
**目标平台**:Allwinner T153(Cortex-A7,Linux,armv7)+ CANable2(slcan)或板载 SocketCAN
**控制频率**:1 kHz(2ms 是旧方案;本项目实际为 1ms 节拍)
**文档目的**:从零到上车的完整开发路径——接口约定、标定步骤、自动整定与超参数、安全与故障处理
---
## 目录
1. [范围与文档分工](#1-范围与文档分工)
2. [快速上手](#2-快速上手)
3. [硬件与接线](#3-硬件与接线)
4. [坐标、单位与符号约定](#4-坐标单位与符号约定)
5. [软件架构](#5-软件架构)
6. [关键机制(实车踩过的坑)](#6-关键机制实车踩过的坑)
7. [控制设计](#7-控制设计)
8. [配置参考(chassis.toml)](#8-配置参考chassistoml)
9. [标定与上车流程](#9-标定与上车流程)
10. [自动整定与超参数预设](#10-自动整定与超参数预设)
11. [台架工具](#11-台架工具)
12. [安全设计](#12-安全设计)
13. [测试与质量门禁](#13-测试与质量门禁)
14. [部署](#14-部署)
15. [故障排查](#15-故障排查)
16. [术语表](#16-术语表)
---
## 1. 范围与文档分工
| 文档 | 回答的问题 | 读者 |
| --- | --- | --- |
| [`ARCHITECTURE.md`](../ARCHITECTURE.md) | 整套 RoboMaster CAN 控制系统的**全系统分层规划**(含云台、发射机构、delta10 IMU、T153 实时性、9 电机) | 架构决策者 |
| [`docs/architecture.md`](architecture.md) | 本仓库**已实现**部分的解耦点:`CanLink` seam、被控对象模型、自动整定两阶段搜索、测试策略 | 改代码的人 |
| **本文** | **底盘这一块**怎么开发、怎么标定、怎么整定、怎么上车、出事怎么办 | 底盘开发 / 调试 / 上车的人 |
`ARCHITECTURE.md` 里的 `rm-*` crate 划分是**规划**;本仓库实际落地为下面这套 crate 命名
(功能对应关系见第 5 节)。本文只覆盖底盘切片,云台 / 发射 / IMU 不在范围内。
---
## 2. 快速上手
### 2.1 无硬件(macOS / Linux / CI 均可)
```sh
cargo test --workspace # 单测 + 闭环仿真 + 整定,全部无需硬件
cargo run -p chassisd -- --mock # 进程内电机模型的全链路演示
cargo run -p chassis-tune -- presets # 看有哪些整定超参数预设
just tune-smoke # 确定性整定冒烟(CI 同款)
just lint # fmt + clippy(与 CI 同标准)
```
### 2.2 上车(详细步骤见第 9 节)
```sh
# 1) 只验链路与反馈,绝不发非零电流(接线排查第一步)
cargo run --release -p chassis-bench --bin chassis-spin -- --device /dev/ttyACM0 --dry-run
# 2) 四轮定性旋转(轮子必须架空或整车固定,急停可及)
cargo run --release -p chassis-bench --bin chassis-spin -- --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**(`dji-esc-slcan`) | 用户态直接读写 `/dev/ttyACM0`(Lawicel ASCII 协议) | 免 `ip link`、绕开 `gs_usb` 驱动;台架与初始联调首选 |
| **SocketCAN**(`dji-esc-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-core::Pid`,挂在 `EscBus` 的 1 kHz 节拍上)。
### 3.3 电调 ID 与安装方向
默认约定(`config/chassis.toml.example`、`chassis-spin` 默认值一致):
| 轮位 | 轮序下标 | 电调 ID | 安装方向 |
| --- | --- | --- | --- |
| 前左 FL | 0 | 1 | `+1` |
| 前右 FR | 1 | 2 | `-1` |
| 后左 RL | 2 | 3 | `+1` |
| 后右 RR | 3 | 4 | `-1` |
左右镜像安装(`direction` 相反)使四轮在"底盘前进"时物理同向。`direction = -1` 的电机
由**驱动层**在写命令与读反馈两个方向上统一翻转符号,上层永远看到"正转 = 前进方向"。
### 3.4 电流限幅
| 场景 | 建议限幅(raw) | 说明 |
| --- | --- | --- |
| 空载台架(`chassis-spin` 默认) | **1500**(≈1.8 A) | 需越过静摩擦(实测 400~600 raw)并留整定余量 |
| 整车实车 | 6000 ~ 12000 | 视供电与机械而定;同时是 PID 抗饱和的钳位点 |
> **限流不是"越大越安全"**:它同时是 PID 输出的钳位点。设得远大于实际所需,
> 积分会在堵转期间持续积累,一旦起动就冲过头。**必须贴近实际所需电流。**
### 3.5 接线前置条件(缺一不可)
- 电调供电与主控**共地**;CAN_H / CAN_L 不接反,两端 120Ω 终端电阻齐全。
- 轮子**架空**或整车固定;急停开关在手可及处。
- 首次上电**先 `--dry-run`**,确认四轮反馈都在刷(`chassis-spin` 会打印每个电调的
转速 / 温度 / 反馈年龄)。
---
## 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 对角,可解析求平均)。实现见
[`crates/mecanum`](../crates/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-core::OutputShaft` 包装(`转子 = 输出轴 × gear_ratio`)。
`EscBus` / `EscMotor` 只认转子,绝不碰减速比;`EscConfig::gear_ratio` 仅作配置元数据。
- PID 的接线顺序:`ChassisController` 算出**轮端** rpm → `OutputShaft` 换算成转子 rpm →
`EscMotor` 的速度环以转子 rpm 闭环。
---
## 5. 软件架构
### 5.1 Crate 分层与职责
**依赖只能向下**(上层可依赖下层,反之不行):
```
第 4 层 应用 / 工具 chassisd chassis-tune chassis-bench
(含两个 bin)
第 3 层 领域 chassis-config mecanum dji-esc-sim
(schema / 运动学 / 被控对象 + 离线 harness)
第 2 层 协议 / 设备 dji-esc(C620 编解码 + EscBus 驱动 + 命令租约)
第 1 层 抽象 can-link(CanFrame + 通道对) motor-core(能力 trait + PID)
第 0 层 传输 dji-esc-slcan(串口) / dji-esc-socketcan(内核 CAN)
```
`dji-esc-slcan` / `dji-esc-socketcan` 只实现 `can-link` 的通道语义,**不含任何应用逻辑**;
它们被第 4 层的工具与守护进程选用,而不是被协议层依赖。
| Crate | 职责 | 平台 |
| --- | --- | --- |
| [`motor-core`](../crates/motor-core) | 能力 trait(`VelocityDrive` 等)、`MotorState`、`Pid`、`VelocityController`、`OutputShaft` | 纯逻辑,全平台 |
| [`can-link`](../crates/can-link) | `CanFrame` + `CanLink` 通道对(**无协议知识**) | 纯逻辑,全平台 |
| [`dji-esc`](../crates/dji-esc) | C620/C610 编解码、多圈累计、`EscBus` 异步驱动、命令租约 | 纯逻辑,全平台 |
| [`dji-esc-sim`](../crates/dji-esc-sim) | 被控对象模型、场景、**离线 harness**、异步假电调 | 纯逻辑,全平台 |
| [`dji-esc-slcan`](../crates/dji-esc-slcan) | slcan(Lawicel)串口 ↔ `CanLink` 桥接 | 全平台 |
| [`dji-esc-socketcan`](../crates/dji-esc-socketcan) | SocketCAN ↔ `CanLink` 桥接 | Linux |
| [`mecanum`](../crates/mecanum) | 麦轮正 / 逆运动学(纯数学) | 全平台 |
| [`chassis-config`](../crates/chassis-config) | `chassis.toml` schema:解析 / 校验 / 写回 | 全平台 |
| [`chassis-bench`](../crates/chassis-bench) | 台架:安全包络、采样、`chassis-spin` / `socketcan-spin` | 全平台(SocketCAN 工具需 Linux) |
| [`chassis-tune`](../crates/chassis-tune) | 速度环 PID 自动整定 + **超参数预设** | 全平台 |
| [`chassisd`](../crates/chassisd) | 底盘守护进程:1 kHz 循环、安全逻辑、指令源 seam | 全平台(实车需 Linux) |
### 5.2 依赖方向(硬约束)
```
chassisd ─┬─► chassis-config ─► motor-core, mecanum
├─► dji-esc ──► can-link, motor-core
├─► dji-esc-sim ─► dji-esc(协议类型), can-link
└─► dji-esc-socketcan ─► can-link
chassis-tune ─┬─► dji-esc-sim(离线 harness)
├─► chassis-bench(安全包络 + 采样)
├─► chassis-config(共用 schema)
└─► dji-esc ─► can-link, motor-core
chassis-bench ─┬─► dji-esc ─► can-link, motor-core
├─► dji-esc-sim(场景 / 轨迹类型)
├─► dji-esc-slcan / dji-esc-socketcan
└─► chassis-config
```
**传输后端 crate 只做桥接,不含应用逻辑**:`chassis-spin` 曾住在 `dji-esc-slcan` 的
example 里,现在统一收归 `chassis-bench`;整定逻辑住在 `chassis-tune`。要换总线介质
(串口转 CAN、MCU 转发),只需**新增一个桥接 crate**,协议 / 控制 / 整定全部不动。
### 5.3 一个 1 ms 拍里发生了什么
```
tick (1ms)
├─ 读取 watch<Twist>(有新值则更新,记录时间戳)
├─ 指令超时判定(默认 200ms)→ 超时则 Twist::ZERO
├─ 幅值限幅 Twist::clamped(max_vx, max_vy, max_omega)(按比例收缩,保方向)
├─ 上电门控:四轮是否都收到过反馈?否则全部 disable 并告警
├─ 麦轮逆解 → 四轮轮端 rpm
├─ OutputShaft 换算 → 转子 rpm
└─ set_velocity(续租命令)→ EscBus 在自身 1 kHz 节拍上组帧、发送
```
同时 `EscBus` 侧持续:接收反馈帧 → 分发到各 `EscMotor` → 更新 `MotorState`
→ 检查反馈离线(默认 100 ms)→ 检查命令租约(默认 50 ms)。
---
## 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`);
- `serial` crate 的 `TTYPort` 读写**共用同一个超时**,写超时属正常现象,**不能当致命错误**
(否则一次写超时就会把接收方向一起带停)。
### 6.4 反馈离线
任一电机反馈超时(`feedback_timeout`,默认 **100 ms**)→ 该电机命令强制 0 电流并告警。
顶层另有**上电门控**:任一电机从未收到过反馈前,整组不下发速度命令。
---
## 7. 控制设计
### 7.1 PID(`motor-core::Pid`)
并联式 PID,显式时间步 `dt`(秒),特性:
1. **输出限幅** `[out_min, out_max]`(速度环里取 `±current_limit`);
2. **积分抗饱和**(clamping):输出已饱和且误差继续同向时**暂停积分**;
3. **微分作用于测量值**(不是误差),避免设定值阶跃的微分冲击;
4. 可选**微分一阶低通**(`with_derivative_tau`),抑制编码器量化噪声;
5. `dt <= 0` 或非有限输入 → 返回上次输出且不更新内部状态(防御跳帧 / 首帧)。
### 7.2 速度环(`motor-core::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 被控对象模型(`dji-esc-sim`)
逐拍离散(`dt` 秒,电调坐标系:正电流 → 正转速):
1. **命令延迟**:`current_raw` 进 FIFO,`delay_ticks` 拍后才作用;
2. **摩擦**:运动时 `net = u − sign(rpm)·coulomb − viscous·rpm`;静止且 `|u| > static`
时 `net = u − sign(u)·coulomb`;静止且 `|u| ≤ static` 时 `net = 0`(粘住);
3. **一阶转速**:`rpm_target = clamp(net × gain, ±max_rpm) − load_rpm`,以时间常数 `tau` 一阶趋近;
4. **量化**:机械角按转速积分并对 8192 取模,角度截断为 `u16`、转速为 `i16`
(量化由协议类型自带)。
`PlantParams::gm3508()` 是**无摩擦无延迟**的理想模型;`gm3508_bench()` 叠加实测量级的
静 / 库仑摩擦(400~600 raw)与 2 拍延迟,是整定的默认被控对象。
---
## 8. 配置参考(chassis.toml)
样例:[`config/chassis.toml.example`](../config/chassis.toml.example)。
schema 由 [`chassis-config`](../crates/chassis-config) 定义,**守护进程与整定工具共用同一份**——
这样"整定工具用的限流"和"守护进程用的限流"不可能悄悄不一致。
### 8.1 顶层
| 键 | 含义 |
| --- | --- |
| `[can] interface` | 底盘总线 SocketCAN 接口名(如 `can0`) |
| `[can.function] interface` | 预留的功能总线(云台 / 拨弹),必须与底盘总线**不同名** |
| `[chassis]` | 控制循环与安全超时、速度上限 |
| `[mecanum]` | `half_length` / `half_width` / `wheel_radius`(米) |
| `[[motor]]` × 4 | 顺序固定 FL / FR / RL / RR |
### 8.2 `[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.3 `[[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.4 校验规则(`Config::validate`)
- 电机数必须**恰好 4**;麦轮几何必须为正;四个超时必须 > 0;
`motor_command_timeout_ms ≥ control_period_ms`;速度上限 > 0;`gear_ratio > 0`;
- 底盘总线**只允许一种协议族**(四轮不能混用 C620 / C610);
- 功能总线接口名不得与底盘总线相同(双 CAN 物理隔离);
- `deny_unknown_fields`:**拼错的键直接报错**,不会被静默忽略。
---
## 9. 标定与上车流程
> 每一步都有明确的**通过判据**。不要跳步——跳过的步骤会在后面以"轮子跑飞"的形式还回来。
| # | 步骤 | 命令 | 通过判据 |
| --- | --- | --- | --- |
| 1 | **链路与反馈** | `chassis-spin --dry-run` | 四个电调都在刷反馈(转速 ≈ 0、温度正常、反馈年龄 < 100 ms)。缺一个就查线 / 电调 ID |
| 2 | **电调 ID 对应** | 同 1,逐个拔插 | 拔掉哪个电调,哪个 ID 的反馈消失 → ID 映射正确 |
| 3 | **方向** | `chassis-spin --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 搜索算法
两阶段:
1. **粗扫**:Latin Hypercube 分层采样 + `rayon` 并行评估,用有限次数铺满四维空间;
2. **精修**:以成本最低的 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`](../config/tune.toml) 里,评审一次、长期复用、任何机器上可复现。
优先级(**逐字段**覆盖):
```
内置默认 < 预设文件里的预设 < 显式命令行参数
```
因此"用预设跑一次 + 临时改一个权重"是安全的,不会把其它超参数打回硬编码值。
```sh
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/chassis-tune/src/preset.rs`](../crates/chassis-tune/src/preset.rs)):
```toml
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` | 仅真机后端:同一组增益的重复性复验 |
判读要点:
1. **最优解贴到参数空间边界**时报告会明确提示——那是"约束在决定结果",应放宽
`--kp` / `--ki` 再搜,而不是把边界值当最优解交付;
2. **达标数 / 评估数**太低(如 9/198)说明大部分候选被硬约束拦下,通常意味着限流太小
或参数空间太宽;
3. 真机 `hil-validation.csv` 的**离散度**比均值更重要:三次结果差很多 = 参数贴着噪声,不该上车。
### 10.6 真机后端与迁移证据
`--backend bench` 与仿真实现同一个 `Evaluator` trait,区别只是"评估一组增益"从浮点积分
换成真机试验:打开链路 → 上电门控 → 逐次**热更新**增益(不重启总线)→ 逐拍重发设定值 +
安全包络 → 任何退出路径先 0 电流 → 试验间静默。搜索强制单线程(只有一条总线)。
```
仿真整定 → 台架复验(同场景、同阈值)→ 重复性达标 → 写回 chassis.toml → 整车验收
```
**可复现性**:同一 `seed` + 同一参数空间 + 同一后端 → **逐位一致**。CI 的
`just tune-smoke` 依赖这一点(同一命令跑两次,`diff trials.csv` 必须为空)。
---
## 11. 台架工具
### 11.1 `chassis-spin`(`chassis-bench`,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 `socketcan-spin`(`chassis-bench`,仅 Linux)
单电机链路验证,走 SocketCAN。用于板载原生 CAN 的接线排查:
```sh
ip link set can0 up type can bitrate 1000000 restart-ms 100
cargo run --release -p chassis-bench --bin socketcan-spin -- --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 显式跑) |
| 实车台架 | `chassis-spin` 四轮定速:链路/反馈、方向、转速稳态、过速保护、安全停机;`chassis-tune --backend bench` 逐次试验 + 重复性复验 | armv7 + slcan/SocketCAN(人工) |
| 交叉检查 | `aarch64-unknown-linux-gnu` 全 workspace check;armv7 静态构建 `chassisd` | CI |
### 13.2 每个阶段完成后的门禁(`just lint` + 以下)
```sh
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 静态,无需交叉工具链)
```sh
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)。二进制静态链接,不依赖板端 libc。
### 14.2 板端拉起 CAN
```sh
# 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
[`deploy/chassisd.service`](../deploy/chassisd.service):
```ini
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` 侧表现为四电机反馈离线、强制零电流——
> **这正是安全设计的预期行为**,不是故障。
>
> 调度策略:`ARCHITECTURE.md` 规划了 `SCHED_FIFO` + `mlockall`(对应 `rm-app::realtime`),
> 本仓库目前**未实现**实时优先级提升,当前只依赖 1 kHz tokio 定时器 +
> `MissedTickBehavior::Skip`。板端抖动实测与是否需要加固见第 15 节的抖动条目。
---
## 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`](../ARCHITECTURE.md)
- [已实现架构与解耦点 `docs/architecture.md`](architecture.md)
- [配置样例 `config/chassis.toml.example`](../config/chassis.toml.example)
- [整定超参数预设 `config/tune.toml`](../config/tune.toml)
- [项目 README](../README.md)
| 版本 | 日期 | 变更 |
| --- | --- | --- |
| 1.0 | 2026-09-23 | 首版:底盘接口约定、标定流程、自动整定与超参数预设、安全与故障处理 |