# 审计 + 电机抽象层改造 + 实车台架单轮测试 ## 目标(用户已确认) 1. **审计**现有抽象层级与抽象功能是否正确。 2. **改造抽象层**(本次就做),为后续派生铺路: - 云台 = **GM6020**(位置环,绝对/多圈角度) - 拨弹轮 = **M2006 + C610**(速度环 + 定量拨弹位置环) 3. **小车轮子测试** = 实车台架单轮验证(现有 4× GM3508 轮子)。 4. **硬约束**:合适的 Rust 抽象层与 crate,**明确单一职责**。 5. **架构决策**(本轮确认): - **双 CAN 总线**:底盘一条 CAN,云台/拨弹另一条 CAN(协议隔离,不用避让 ID)。 - **必须预留 IMU 融合口**:云台绝对角需要 `HeadingSource` 类 seam。 - **wheel-bench 分阶段自动跑**。 --- ## 一、审计结论 ### 1.1 分层现状(正确的部分,保留) ``` chassisd ──► mecanum ──► motor-core::Motor ◄── dji-esc (EscBus) ──► CanLink ──► dji-esc-socketcan ``` | 做对的事 | 证据 | |---|---| | 协议与 I/O 解耦 | `CanLink` = tokio mpsc 对;`motor-core`/`dji-esc` 零真实 I/O | | 命令 / 反馈分离 | `Command` enum vs `MotorState` struct | | 方向修正对称 | `EscConfig::direction=±1`,读写双向翻转(`bus.rs:163-203`) | | 纯算法可测 | `Pid`(抗饱和、D-on-measurement、滤波)、`VelocityController` | | 多圈累计 | `TurnTracker` 8192 编码器回绕(`turn.rs`) | | 安全三件套 | 上电门控 / 反馈离线 0 电流 / 命令超时归零 | | 仿真闭环 | `sim::spawn_plant` + `closed_loop.rs` + `--mock` 全平台可跑 | | 运动学纯数学 | `mecanum` 正逆解 + proptest | | 组合根 | `chassisd` 装配 + `TwistSource` seam | **结论:轮子(C620 电流环 + 主机速度环)路径是通的。** ### 1.2 单一职责违规 / 抽象缺口(必须改) | # | 问题 | 位置 | SRP 违规 | |---|---|---|---| | G1 | `Motor` 无位置 API | `motor-core/src/lib.rs:51-79` | 能力面不足 | | G2 | `MotorState.wheel_speed_rpm` | `lib.rs:32`, `bus.rs:171` | **底盘/减速比泄漏进协议无关层** | | G3 | 无 `PositionController` | `motor-core` | 云台/拨弹阻塞 | | G4 | 主机速度环写死在 `EscBus::compute_command` | `bus.rs:181-203` | **控制律与协议组帧/安全耦合** | | G5 | `EscModel` 无 GM6020 | `codec.rs:23-30` | 电压协议族缺失 | | G6 | `tx_loop` 硬编码 0x200/0x1FF 电流槽 | `bus.rs:369-388` | 协议族绑定过死 | | G7 | `ChassisController` 依赖 `EscMotor` + 自己乘 gear | `controller.rs:25,83` | 应用层依赖协议 crate | | G8 | `Command` 切模式不复位 PID | `bus.rs:192-199` | 拨弹速度↔位置积分冲击 | | G9 | 无 `MockMotor` | — | 上层无法脱离 EscBus 测 | | G10 | 配置写死恰好 4 电机 | `config.rs:181` | 无法扩设备段 | | G11 | `CanFrame`/`CanLink` 在 `dji-esc::frame` | `frame.rs` | **传输被单个协议族占有** | | G12 | 无航向/绝对角抽象 | — | 云台场地绝对角需 IMU 融合(用户要求预留) | ### 1.3 CAN ID 问题 —— 由「双 CAN」架构决策消解 `0x1FF` / `0x205` 在 C620 与 GM6020 两族中含义不同(电流 vs 电压)。**用户选择双 CAN 总线**: | 总线 | 设备 | 协议 | ID | 命令帧 | |---|---|---|---|---| | **can0 底盘** | FL/FR/RL/RR 轮 | C620+GM3508 | 1–4 | `0x200` | | **can1 功能** | 拨弹轮 | C610+M2006 | 1–4(本总线内) | `0x200` | | **can1 功能** | 云台 | GM6020 | 1–4(本总线内) | `0x1FF` | - 每条 CAN 一个独立 `CanLink` + 一个协议族 bus;**物理隔离**后 ID 空间独立,无需避让。 - 仍建议配置层按「每总线一协议族」校验,避免同总线混接时踩坑。 - 组合根按接口名装配:`chassisd --interface can0 --gimbal-interface can1`(或配置 `[can]`/`[can.function]`)。 --- ## 二、目标设计(单一职责) ### 2.1 目标 crate / 模块树 ``` crates/ motor-core/ # 协议无关电机域:能力 trait、状态、命令寄存器、控制算法、mock(零依赖、无 tokio) src/ lib.rs # 导出 + 单位/方向约定 state.rs # MotorState(转子轴物理量,无 gear_ratio) command.rs # Command + CommandMode traits.rs # MotorFeedback / EffortDrive / VelocityDrive / PositionDrive / Disable heading.rs # HeadingSource(IMU/绝对角融合 seam,新增) pid.rs # Pid(保持) velocity.rs # VelocityController(输出泛化为 effort_raw) position.rs # PositionController(新增:Direct | Cascade) gearbox.rs # OutputShaft:输出轴单位适配(新增) testing.rs # MockMotor / MockHeading(新增) can-link/ # CAN 传输:CanFrame + CanLink(新增;无协议知识) dji-esc/ # C620/C610 电流协议族:编解码 + 总线调度/安全 + sim dji-gm6020/ # GM6020 电压协议族(本次只留 seam 文档,实现=后续) dji-esc-socketcan/ # SocketCAN 桥(只依赖 can-link;支持多接口) examples/ spin.rs # 保留:最小抽象演示 wheel-bench.rs # 新增:实车台架单轮验证(分阶段自动) mecanum/ # 纯运动学(不动) chassisd/ # 组合根:配置 / 1kHz 底盘循环 / 安全 / --mock docs/ architecture.md # 更新分层图(双 CAN) motor-abstraction.md # trait 契约与单位(新增) bench-single-wheel.md# 台架流程(新增) ``` **裁剪原则**:只新增 `can-link` + `dji-gm6020` seam + `heading.rs` 模块。不拆 `motor-controllers`、暂不建 `gimbal`/`feeder` crate。 ### 2.2 motor-core:能力拆分(小 trait) ```rust // traits.rs —— 能力面,可组合,静态分发 pub trait MotorFeedback: Send + Sync { fn id(&self) -> u8; fn state(&self) -> MotorState; } pub trait EffortDrive: Send + Sync { /// 开环 raw(ESC=电流,GM6020=电压)。 fn set_effort_raw(&self, raw: i16); } pub trait VelocityDrive: Send + Sync { /// 目标转子转速(rpm,物理方向)。 fn set_velocity(&self, rotor_rpm: f32); } pub trait PositionDrive: Send + Sync { /// 目标转子多圈位置(圈,物理方向)。 fn set_position(&self, rotor_rev: f32); } pub trait Disable: Send + Sync { fn disable(&self); } /// 便利别名(blanket impl,零成本)。 pub trait Motor: MotorFeedback + VelocityDrive + EffortDrive + Disable {} impl Motor for T {} ``` **为什么不是胖 `Motor` trait**:能力是硬件事实。消费者只声明用到的能力: - 底盘 → `VelocityDrive + MotorFeedback` - 云台 → `PositionDrive + EffortDrive`(+ 上层 `HeadingSource`) - 拨弹 → `VelocityDrive + PositionDrive` - 台架 → `VelocityDrive + MotorFeedback + Disable` **`EffortDrive` 统一命名**:两族都是 i16 开环命令,仅物理量不同(A vs V);协议 crate 文档化单位。 ### 2.3 IMU / 绝对角融合 seam(用户要求) ```rust // heading.rs —— 不属于电机,属于「航向观测」 /// 绝对航向观测源(IMU / 磁力计 / 视觉等)。 pub trait HeadingSource: Send { /// 机体航向角(rad,连续;定义与 chassis ISO 约定一致)。 fn heading_rad(&self) -> impl Future + Send; } /// 云台目标角的参考系。 pub enum HeadingFrame { /// 电机转子多圈(上电相对)。 RotorMultiTurn, /// 场地绝对角(经 HeadingSource 对齐)。 AbsoluteField, } ``` - **motor 层只保证 `rotor_revolutions` 连续**;场地绝对角由未来 `gimbal` crate 融合 IMU(`HeadingSource`)与电机多圈。 - `PositionDrive::set_position` 仍是转子圈;绝对角 → 转子圈的映射在云台控制器,不在 motor-core。 - `MockHeading` 放 `testing.rs`。 ### 2.4 `Command` / `MotorState` ```rust pub enum Command { Disabled, EffortRaw(i16), // 原 CurrentRaw Velocity(f32), // rotor rpm Position(f32), // rotor revolutions(圈,连续) } pub enum CommandMode { Disabled, EffortRaw, Velocity, Position } pub struct MotorState { pub rotor_angle_raw: u16, pub rotor_revolutions: f32, pub rotor_speed_rpm: f32, pub effort_raw: i16, // 原 torque_current_raw pub temperature_c: i8, pub feedback_count: u64, } // 删除 wheel_speed_rpm(G2) ``` **模式切换复位**(G8):`compute_effort` 在 `cmd.mode()` 变化时 reset 控制器。 ### 2.5 motor-core:控制算法 | 类型 | 模式 | 输入 → 输出 | |---|---|---| | `Pid` | — | setpoint, meas, dt → f32 | | `VelocityController` | — | target_rpm, meas_rpm, dt → effort_raw | | `PositionController` **新增** | `Direct(Pid)` | pos_rev, meas_rev, dt → effort_raw(云台电压) | | | `Cascade{pos, vel}` | 位置→限速→速度环→effort_raw(拨弹定量) | 控制律纯函数放 motor-core;**执行节拍/安全/方向/限幅**留在各协议族 bus。 ### 2.6 gearbox:`OutputShaft` ```rust /// 输出轴单位 ↔ 转子单位。gear_ratio = 转子 : 输出轴。 pub struct OutputShaft { inner: T, gear_ratio: f32 } impl VelocityDrive for OutputShaft { fn set_velocity(&self, output_rpm: f32) { self.inner.set_velocity(output_rpm * self.gear_ratio); } } // Position 同理;Feedback/Effort/Disable 透传 ``` ### 2.7 can-link(新 crate) `CanFrame`、`CanLink`、`CanLink::pair`。**不含 DJI ID 常量**。`motor-core` 保持无 tokio。 ### 2.8 dji-esc(电流协议族) - `EscConfig::m2006(name, esc_id, direction)`(C610、36:1 待核)。 - 算法改调 motor-core;`compute_effort` 加 `Position` 分支 + 模式 reset。 - 电流槽分组保持(协议事实)。 ### 2.9 dji-gm6020(设计 seam,不实机) - 实现 5 个能力 trait。 - 命令 `0x1FF`(ID1–4)/`0x2FF`(ID5–7) 电压槽;反馈 `0x204+id`;`TurnTracker` 复用。 - 云台位置环:`PositionController` → voltage raw。 - 绝对角:未来 `gimbal` crate 用 `HeadingSource` + `HeadingFrame::AbsoluteField` 对齐到 `rotor_revolutions` 基准后再 `set_position`。 ### 2.10 拨弹 M2006/C610 派生路径(设计,不实现) - 复用 `dji-esc` + `EscConfig::m2006`;定量 = `Position` + Cascade;连续 = `Velocity`。 - FSM 在上层,不在 motor-core。 ### 2.11 chassisd(组合根) ```rust pub struct ChassisController { kinematics: Mecanum, wheels: [W; 4], limits: (f32, f32, f32), command_timeout: Duration, } impl ChassisController { ... } ``` - **不 import `dji_esc`**。 - 配置:底盘 `[can]` + 四轮;预留 `[can.function]` / `[gimbal]` / `[feeder]` seam 注释(双 CAN 接口名)。 - 不写云台/拨弹逻辑。 --- ## 三、实车台架单轮验证(小车轮子测试) ### 3.1 工具:`wheel-bench`(分阶段自动跑) ```sh cargo run --release -p dji-esc-socketcan --example wheel-bench -- \ --interface can0 --esc-id 1 --direction 1 --gear-ratio 19.203 \ --current-limit 4000 --rpm 300 --secs 3 ``` 一条命令顺序执行 stage 0–6,输出 CSV + 结论;ctrl-c 任意点安全停机。 | Stage | 动作 | 观察 | 通过标准 | |---|---|---|---| | 0 Link | `disable()` | 反馈帧率 ~1kHz、温度 | 2s 内有反馈 | | 1 Direction | `set_effort_raw(±limit/4)` 点动 | `rotor_speed_rpm` 符号 | 与 `direction`/前进约定一致 | | 2 Vel step | `set_velocity(+target)` | 超调、稳态误差 | 1s 后 \|err\|<5% | | 3 gear_ratio | 对比转子 rpm 与手测轮端 | 比值 | ≈19.203 ±3% | | 4 Reverse | `set_velocity(-target)` | 对称性 | 对称 | | 5 Disable | `disable()` | rpm→0 | 停转 | | 6 Offline(可选) | 运行中断电调 | 离线强制 0 电流 | 无重启冲击 | 安全内建:默认低限流、总时限、退出路径 `disable()+50ms` 刷帧、无反馈拒启动、单电机、打印「架空禁止落地」。保留 `spin.rs`。 ### 3.2 流程(`docs/bench-single-wheel.md`) 1. **架空车轮**,急停可及;`ip link set can0 up type can bitrate 1000000 restart-ms 100`。 2. 首次 `--current-limit 3000~5000`。 3. 一次只驱一台;顺序 FL→FR→RL→RR。 4. 跑完自动 stage;方向错**改配置 `direction`**。 5. 记录 `direction` 终值、实测 gear_ratio、PID 备注。 6. 填表归档后才考虑落地四轮联动(不在本次)。 **通过标准**:反馈无连续丢帧;方向一致;无持续振荡、稳态 <5%;减速比偏差 <3%;`disable`/离线停转、电流 0。 --- ## 四、迁移步骤(每步可编译、测试绿) | 步 | 内容 | |---|---| | M0 | 基线 `cargo test --workspace && just lint` | | M1 | 新建 `can-link`;`dji-esc::frame` re-export 兼容 | | M2 | `dji-esc-socketcan` 改依赖 `can-link`;收缩 `frame.rs` | | M3 | motor-core 拆 `state/command/traits/heading`;**一次破坏性重命名** `CurrentRaw→EffortRaw`、`torque_current_raw→effort_raw`;删 `wheel_speed_rpm` | | M4 | 能力 trait 落地;`EscMotor` 实现;`Motor` blanket 别名 | | M5 | `OutputShaft`;`ChassisController` 泛型化;daemon 测试 gear 换算 | | M6 | `Command::Position` + `CommandMode` 模式 reset;`PositionController` + 单测;`dji-esc` Position 分支 | | M7 | `MockMotor`/`MockHeading`;改写 1 个 chassis 单测证明 seam | | M8 | `EscConfig::m2006()`;双 CAN 配置 seam + 每总线单协议族校验 | | M9 | `wheel-bench`(分阶段自动)+ `docs/bench-single-wheel.md` | | M10 | 文档:`architecture.md`(双 CAN 图)、`motor-abstraction.md`、README | | M11(后续) | `dji-gm6020` codec+bus+sim | | M12(后续) | `gimbal` crate:`HeadingSource` 融合 + 云台控制 | --- ## 五、关键文件 **新建** - `crates/can-link/{Cargo.toml,src/lib.rs}` - `crates/motor-core/src/{state,command,traits,heading,position,gearbox,testing}.rs` - `crates/dji-esc-socketcan/examples/wheel-bench.rs` - `docs/{motor-abstraction,bench-single-wheel}.md` **修改** - `Cargo.toml`(workspace) - `crates/motor-core/src/{lib,velocity}.rs` - `crates/dji-esc/src/{lib,bus,codec,sim}.rs`、`tests/closed_loop.rs` - `crates/dji-esc-socketcan/src/bridge.rs`、`examples/spin.rs`、`Cargo.toml` - `crates/chassisd/src/{controller,config,main}.rs`、`tests/daemon.rs` - `config/chassis.toml.example`、`docs/architecture.md`、`README.md`、`justfile` **删除 / 迁出** - `crates/dji-esc/src/frame.rs`(类型迁 can-link) - `MotorState.wheel_speed_rpm`、`Command::CurrentRaw`、`torque_current_raw` --- ## 六、验证 ```sh cargo test --workspace just lint cargo run -p chassisd -- --mock just vcan-test just build-armv7 cargo build --release -p dji-esc-socketcan --example wheel-bench cargo build --release -p dji-esc-socketcan --example spin # 实车台架见 §3 ``` **Definition of Done** 1. `cargo test --workspace` + `just lint` 绿。 2. `chassisd --mock` 行为不变。 3. `rg 'wheel_speed_rpm|CurrentRaw|torque_current_raw'` 为空。 4. `rg 'dji_esc' crates/chassisd/src/controller.rs` 为空。 5. `motor-core` 无运行时依赖。 6. `wheel-bench` + 台架文档就位;实车四轮台架表归档。 7. `HeadingSource` seam + 双 CAN 配置 seam 就位;**`chassisd` 无云台/拨弹逻辑**。 --- ## 七、风险 | 风险 | 缓解 | |---|---| | 双 CAN 板端接口(can1)是否可用 | 配置可单 CAN 起步(只填 `[can]`);功能段可选 | | GM6020 电压/反馈标度未核手册 | 常量隔离在 `dji-gm6020::codec`;上电前台架核对 | | C610/M2006 36:1?±10000↔±10A | `m2006()` 标注接入前核对 | | `OutputShaft` 单位混淆 | 参数名 `output_rpm`/`output_rev` + 文档 | | 删 `wheel_speed_rpm` 破坏日志/测试 | M3/M5 同步改 | | 模式切换 reset 惊吓 | 文档 + `debug!` | | `HeadingSource` 的异步形态是否合适 | 先定 trait 契约;M12 可按 IMU 驱动改 `async fn` / watch | | 同总线误接混协议 | 配置校验「每总线一协议族」+ 文档警告 | --- ## 八、不在本次范围 - 云台 / 拨弹控制逻辑与整定 - GM6020 实机驱动完整实现(M11) - IMU 驱动与航向融合实现(M12,仅 seam) - 四轮落地联动测试 - 遥控器 / 导航 TwistSource