feat: 添加了自动超参数整定

This commit is contained in:
Horldsence
2026-09-23 00:42:26 +08:00
parent 7d87987e41
commit 680adae339
56 changed files with 13664 additions and 245 deletions
+748
View File
@@ -0,0 +1,748 @@
# 底盘开发文档
**适用对象**:哨兵机器人麦轮底盘(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 | 首版:底盘接口约定、标定流程、自动整定与超参数预设、安全与故障处理 |