# 底盘开发文档 **适用对象**:哨兵机器人麦轮底盘(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(有新值则更新,记录时间戳) ├─ 指令超时判定(默认 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 | 首版:底盘接口约定、标定流程、自动整定与超参数预设、安全与故障处理 |