# RoboMaster CAN 控制系统架构文档 **版本**: 1.0 **目标平台**: Allwinner T153 (Cortex-A7 @ 1.6GHz, Linux) **控制频率**: 500Hz **文档目的**: 指导执行 Agent 完成项目拆分、结构整理与模块设计 --- ## 目录 1. [系统概述](#1-系统概述) 2. [硬件拓扑](#2-硬件拓扑) 3. [架构原则](#3-架构原则) 4. [Crate 拆分](#4-crate-拆分) 5. [各 Crate 详细设计](#5-各-crate-详细设计) 6. [数据流与任务模型](#6-数据流与任务模型) 7. [实时性设计](#7-实时性设计) 8. [仿真与自动整定](#8-仿真与自动整定) 9. [测试策略](#9-测试策略) 10. [部署与配置](#10-部署与配置) 11. [执行 Agent 工作流](#11-执行-agent-工作流) 12. [附录](#12-附录) --- ## 1. 系统概述 ### 1.1 目标 构建一个运行在 T153 Linux 平台上的 RoboMaster 机器人控制系统,实现: - 对 **9 个 CAN 电机**(5 个原生 CAN + 4 个 SLCAN)的 500Hz 闭环控制 - 对 **delta10 IMU** 的姿态数据采集(通过 CAN 连接) - 对机架不同部分的**完整功能抽象**(云台、发射机构、底盘) - 支持**仿真环境下的自动 PID 整定**,并将参数迁移到实机 - 保证每一层的**可维护性、可验证性、可移植性** ### 1.2 关键约束 | 约束 | 值 | 影响 | |------|-----|------| | 控制频率 | 500Hz (2ms) | 任务预算分配、调度策略 | | 平台 | T153 Cortex-A7 Linux | 非硬实时,需 SCHED_FIFO | | CAN 接口 | 2 原生 + 1 SLCAN | 三种不同的访问路径 | | IMU 接口 | CAN | 与电机共享 can0 总线 | | 仿真目标 | 纯算法验证 | 不需要全系统数字孪生 | --- ## 2. 硬件拓扑 ### 2.1 物理连接图 ``` ┌─────────────────────────────────────────────────────────────────┐ │ T153 (Linux, Cortex-A7) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ CAN0 (原生) │ │ CAN1 (原生) │ │ USB CDC │ │ │ │ 1Mbps │ │ 1Mbps │ │ /dev/ttyACM0 │ │ │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ └─────────┼──────────────────┼────────────────────┼──────────────┘ │ │ │ │ │ │ (SLCAN 协议) │ │ ▼ │ │ ┌──────────────────┐ │ │ │ candlelite2 │ │ │ │ (slcan 固件) │ │ │ └────────┬─────────┘ │ │ │ ▼ ▼ ▼ ┌──────────────────┐ ┌──────────┐ ┌───────────────────────┐ │ can0 总线 │ │can1 总线 │ │ SLCAN CAN 总线 │ │ │ │ │ │ │ │ ├─ GM6020 (yaw) │ │ ├─ M2006 │ │ ├─ M3508 (底盘 FL) │ │ ├─ GM6020(pitch)│ │ │ (拨弹) │ │ ├─ M3508 (底盘 FR) │ │ ├─ M3508 (摩擦L)│ │ │ │ │ ├─ M3508 (底盘 RL) │ │ ├─ M3508 (摩擦R)│ │ │ │ │ └─ M3508 (底盘 RR) │ │ └─ delta10 IMU │ │ │ │ │ └──────────────────┘ └──────────┘ └───────────────────────┘ ``` ### 2.2 设备清单 | 设备 | 数量 | 总线 | 协议 | 控制帧 ID | 反馈帧 ID | |------|------|------|------|-----------|-----------| | GM6020 (yaw) | 1 | can0 | 标准帧 | 0x1FF 或 0x2FF | 0x205-0x208 | | GM6020 (pitch) | 1 | can0 | 标准帧 | 0x1FF 或 0x2FF | 0x205-0x208 | | M3508 (摩擦轮) | 2 | can0 | 标准帧 | 0x200 或 0x1FF | 0x201-0x204 | | M2006 (拨弹轮) | 1 | can1 | 标准帧 | 0x200 或 0x1FF | 0x201-0x204 | | M3508 (底盘) | 4 | SLCAN | 标准帧 | 0x200 | 0x201-0x204 | | delta10 IMU | 1 | can0 | FDILINK/标准 | TBD | TBD | **注意**:GM6020 的 ID 分配需在初始化时固定,避免与 M3508 冲突。建议 GM6020 使用 0x205-0x206(对应 ID 1-2),M3508 摩擦轮使用 0x201-0x202(对应 ID 1-2),预留 ID 3-4。 ### 2.3 总线负载估算 | 总线 | 设备数 | 帧/秒(500Hz) | 1Mbps 负载 | |------|--------|----------------|-----------| | can0 | 4 电机 + delta10 | ~5000 | ~12% | | can1 | 1 电机 | ~1000 | ~2% | | SLCAN | 4 电机 | ~4000 | USB CDC 带宽充裕 | --- ## 3. 架构原则 ### 3.1 分层原则 系统分为 **7 层**,每层只依赖其下层,不允许跨层依赖: ``` ┌─────────────────────────────────────────┐ │ L7: rm-app 应用层(任务编排) │ ├─────────────────────────────────────────┤ │ L6: rm-sim 仿真层(纯算法验证) │ ├─────────────────────────────────────────┤ │ L5: rm-control 控制层(PID/自整定) │ ├─────────────────────────────────────────┤ │ L4: rm-devices 设备抽象层(Motor) │ ├─────────────────────────────────────────┤ │ L3: rm-drivers 驱动层(SocketCAN/SLCAN)│ ├─────────────────────────────────────────┤ │ L2: rm-protocol 协议层(帧编解码) │ ├─────────────────────────────────────────┤ │ L1: rm-hal 硬件抽象层(trait) │ └─────────────────────────────────────────┘ ``` **依赖方向**:L7 → L6 → L5 → L4 → L3 → L2 → L1。不允许反向依赖,不允许跳层依赖(如 L7 直接使用 L2)。 ### 3.2 Rust 特性使用原则 | 特性 | 使用场景 | 示例 | |------|---------|------| | `const` 泛型 | 编译期 ID/通道校验 | `Gm6020Id` | | `newtype` | 类型安全的值封装 | `Current(i16)`、`Angle(f32)` | | `async fn in trait` | 异步接口抽象 | `CanBus::send` | | `PhantomData` | 标记通道归属 | `SocketCanBus` | | `enum` + `match` | 电机类型的 ad-hoc 多态 | `MotorKind` | | `#[derive]` | 序列化/调试 | `zerocopy::FromBytes` | | `no_std` | 纯算法层 | `rm-control`、`rm-protocol` | ### 3.3 单一职责原则 - **一个 crate 一个职责**:`rm-protocol` 只做编解码,不含 IO - **一个模块一个概念**:`rm-devices/motor/gm6020.rs` 只处理 GM6020 - **一个函数一个动作**:`send_current` 只负责发送,不负责校验以外的逻辑 ### 3.4 可移植性原则 - **硬件无关性**:`rm-control` 和 `rm-protocol` 不含任何平台相关代码 - **trait 边界**:所有硬件访问通过 `rm-hal` 的 trait - **仿真对等**:`SimPlant` 和 `RealPlant` 实现同一 trait,可无缝切换 --- ## 4. Crate 拆分 ### 4.1 Workspace 结构 ``` rm-can-robot/ ├── Cargo.toml # workspace 根 ├── rust-toolchain.toml # 工具链固定 ├── .cargo/ │ └── config.toml # 交叉编译配置 ├── crates/ │ ├── rm-hal/ # L1: 硬件抽象 │ ├── rm-protocol/ # L2: 协议编解码 │ ├── rm-drivers/ # L3: 驱动实现 │ ├── rm-devices/ # L4: 设备抽象 │ ├── rm-control/ # L5: 控制算法 │ ├── rm-sim/ # L6: 仿真 │ └── rm-app/ # L7: 应用 ├── config/ # 运行时配置 (TOML) │ ├── robot.toml # 机器人拓扑 │ ├── motors.toml # 电机参数 │ └── pid.toml # PID 参数 ├── scripts/ # 部署与调试脚本 │ ├── setup_can.sh # CAN 接口初始化 │ ├── udev_rules.sh # 串口权限 │ └── deploy.sh # 交叉编译 + 部署 ├── tests/ # 集成测试 │ ├── vcan_loopback.rs # 虚拟 CAN 回环 │ └── sim_pid_tune.rs # 仿真整定测试 └── docs/ # 架构文档 ├── ARCHITECTURE.md # 本文档 ├── PROTOCOL.md # CAN 协议说明 └── TUNING.md # 整定指南 ``` ### 4.2 Workspace Cargo.toml ```toml [workspace] resolver = "2" members = [ "crates/rm-hal", "crates/rm-protocol", "crates/rm-drivers", "crates/rm-devices", "crates/rm-control", "crates/rm-sim", "crates/rm-app", ] [workspace.package] version = "0.1.0" edition = "2021" rust-version = "1.75" license = "MIT" [workspace.dependencies] # 异步 tokio = { version = "1", features = ["rt-multi-thread", "time", "sync", "macros"] } # CAN socketcan = { version = "3.6", features = ["tokio"] } slcan_fd = { version = "0.1", features = ["tokio"] } tokio-serial = "5" # 控制 discrete_pid = "0.2" nalgebra = { version = "0.33", default-features = false, features = ["libm"] } # 实时 libc = "0.2" # 序列化 zerocopy = "0.8" serde = { version = "1", features = ["derive"] } toml = "0.8" # 日志 tracing = "0.1" tracing-subscriber = "0.3" # 错误处理 thiserror = "1" anyhow = "1" # 测试 proptest = "1" ``` --- ## 5. 各 Crate 详细设计 ### 5.1 `rm-hal` — 硬件抽象层 **职责**:定义与硬件无关的 trait,作为所有硬件访问的抽象边界。 **依赖**:无(`no_std` 兼容) **文件结构**: ``` rm-hal/ ├── Cargo.toml └── src/ ├── lib.rs ├── can.rs # CanBus trait + CanFrame ├── imu.rs # Imu trait ├── time.rs # 时间抽象 └── error.rs # 错误类型 ``` **核心 trait**: ```rust // can.rs #[derive(Clone, Debug)] pub struct CanFrame { pub id: u32, pub data: [u8; 8], pub dlc: u8, pub is_extended: bool, } pub trait CanBus { type Error; async fn send(&mut self, frame: &CanFrame) -> Result<(), Self::Error>; async fn recv(&mut self) -> Result; fn try_recv(&mut self) -> Result, Self::Error>; } // imu.rs pub struct ImuData { pub accel: [f32; 3], pub gyro: [f32; 3], pub mag: [f32; 3], pub roll: f32, pub pitch: f32, pub yaw: f32, pub timestamp_ns: u64, } pub trait Imu { type Error; async fn read(&mut self) -> Result; } ``` **验收标准**: - [ ] 所有 trait 不依赖任何具体硬件 - [ ] `no_std` 下编译通过(`cargo build --no-default-features`) - [ ] 有完整的文档注释 --- ### 5.2 `rm-protocol` — 协议编解码层 **职责**:CAN 帧的编解码,纯逻辑,无 IO。 **依赖**:`rm-hal`(仅用于 `CanFrame` 类型)、`zerocopy` **文件结构**: ``` rm-protocol/ ├── Cargo.toml └── src/ ├── lib.rs ├── frame.rs # CanFrame 扩展 ├── motor/ │ ├── mod.rs │ ├── gm6020.rs # GM6020 编解码 │ ├── m3508.rs # M3508 编解码 │ ├── m2006.rs # M2006 编解码 │ └── common.rs # 电机反馈结构 ├── delta10.rs # delta10 编解码 └── id.rs # CAN ID 编译期校验 ``` **核心设计**: ```rust // id.rs — 编译期 ID 校验 pub struct MotorId; impl MotorId { pub const NEW: Self = { assert!(ID >= 1 && ID <= 8, "Motor ID must be 1-8"); Self }; } // motor/gm6020.rs #[derive(Debug, Clone, Copy)] pub struct Gm6020Feedback { pub angle: f32, // 0-360 度 pub speed: f32, // rpm pub current: i16, // 原始电流值 pub temperature: u8, } #[derive(Debug, Clone, Copy)] pub struct Gm6020Command { pub current: i16, // -30000 ~ 30000 } impl Gm6020Command { pub fn to_frame(&self, id: MotorId) -> CanFrame { // 编码到 0x1FF 或 0x2FF 广播帧 } } pub fn decode_feedback(frame: &CanFrame) -> Option<(u8, Gm6020Feedback)> { // 从 0x205-0x208 解码 } ``` **验收标准**: - [ ] 所有电机的编解码有单元测试 - [ ] 使用 `proptest` 做模糊测试 - [ ] `no_std` 兼容 - [ ] 无任何 IO 操作 --- ### 5.3 `rm-drivers` — 驱动层 **职责**:实现 `rm-hal` 的 trait,对接具体硬件。 **依赖**:`rm-hal`、`rm-protocol`、`socketcan`、`slcan_fd`、`tokio-serial` **文件结构**: ``` rm-drivers/ ├── Cargo.toml └── src/ ├── lib.rs ├── socketcan.rs # SocketCAN 实现 (can0, can1) ├── slcan.rs # SLCAN 实现 (candlelite2) ├── delta10.rs # delta10 IMU 驱动 └── mock.rs # 测试用 mock(feature-gated) ``` **核心实现**: ```rust // socketcan.rs pub struct SocketCanBus { socket: socketcan::tokio::CanSocket, } impl CanBus for SocketCanBus { type Error = socketcan::Error; async fn send(&mut self, frame: &CanFrame) -> Result<(), Self::Error> { let sock_frame = to_socketcan_frame(frame); self.socket.write_frame(&sock_frame).await } async fn recv(&mut self) -> Result { let f = self.socket.read_frame().await?; Ok(from_socketcan_frame(f)) } } // slcan.rs pub struct SlcanBus { socket: slcan_fd::tokio::CanSocket, } impl CanBus for SlcanBus { // 类似实现,但底层使用 SLCAN 协议 } // delta10.rs pub struct Delta10Driver { bus: B, can_id: u16, last_data: Option, } impl Imu for Delta10Driver { async fn read(&mut self) -> Result { // 等待 delta10 帧,解码 FDILINK/标准协议 } } ``` **验收标准**: - [ ] 在 T153 上能成功打开 `can0`、`can1`、SLCAN 串口 - [ ] 能用 `candump` 验证帧收发 - [ ] 有 `mock` feature 用于测试 --- ### 5.4 `rm-devices` — 设备抽象层 **职责**:将协议层的数据封装为有状态的电机设备,提供统一的 `Motor` trait。 **依赖**:`rm-hal`、`rm-protocol` **文件结构**: ``` rm-devices/ ├── Cargo.toml └── src/ ├── lib.rs ├── traits.rs # Motor trait ├── gm6020.rs ├── m3508.rs ├── m2006.rs └── registry.rs # 电机注册表(管理所有电机) ``` **核心设计**: ```rust // traits.rs pub trait Motor { type Feedback; type Command; fn id(&self) -> u8; fn set_command(&mut self, cmd: Self::Command) -> Result<(), MotorError>; fn feedback(&self) -> Option<&Self::Feedback>; fn update_feedback(&mut self, frame: &CanFrame) -> Result<(), MotorError>; fn is_online(&self, now: Instant) -> bool; fn latency_hint(&self) -> Duration; // SLCAN 返回较大值 } // registry.rs — 统一管理所有电机 pub struct MotorRegistry { gm6020: [Gm6020; 2], m3508_arm: [M3508; 2], m3508_chassis: [M3508; 4], m2006: M2006, } impl MotorRegistry { pub fn update_from_frame(&mut self, bus: BusId, frame: &CanFrame) { // 根据总线 ID 和帧 ID 分发到对应电机 } } ``` **验收标准**: - [ ] 每个电机类型有独立的单元测试 - [ ] `MotorRegistry` 能正确分发帧 - [ ] 在线检测逻辑正确(超时阈值可配置) --- ### 5.5 `rm-control` — 控制算法层 **职责**:PID 控制、串级控制、前馈补偿、自动整定。纯算法,无 IO。 **依赖**:`discrete_pid`、`nalgebra`(可选) **文件结构**: ``` rm-control/ ├── Cargo.toml └── src/ ├── lib.rs ├── pid.rs # PID 封装(基于 discrete_pid) ├── cascade.rs # 串级控制 ├── feedforward.rs # 前馈补偿 ├── plant.rs # ControlPlant trait └── tuner/ ├── mod.rs ├── relay.rs # Relay feedback 整定 └── zn.rs # Ziegler-Nichols 公式 ``` **核心设计**: ```rust // plant.rs — 抽象被控对象,使仿真和实机共用 PID pub trait ControlPlant { fn step(&mut self, input: f32, dt: f32) -> f32; // 返回测量输出 fn reset(&mut self); } // pid.rs pub struct PidController { inner: discrete_pid::Pid, output_limit: (f32, f32), integral_limit: (f32, f32), } impl PidController { pub fn compute(&mut self, setpoint: f32, measured: f32, dt: f32) -> f32 { let raw = self.inner.compute(setpoint, measured, dt); raw.clamp(self.output_limit.0, self.output_limit.1) } } // cascade.rs — 位置-速度串级 pub struct CascadeController { outer: PidController, // 位置环 inner: PidController, // 速度环 } impl CascadeController { pub fn compute(&mut self, pos_sp: f32, pos_fb: f32, vel_fb: f32, dt: f32) -> f32 { let vel_sp = self.outer.compute(pos_sp, pos_fb, dt); self.inner.compute(vel_sp, vel_fb, dt) } } // tuner/relay.rs pub struct RelayTuner { amplitude: f32, hysteresis: f32, state: RelayState, measurements: Vec, } pub struct OscillationPoint { pub peak: f32, pub trough: f32, pub period: f32, } impl RelayTuner { pub fn step(&mut self, measured: f32, dt: f32) -> f32 { // 返回 relay 输出,记录振荡点 } pub fn result(&self) -> Option { // Ziegler-Nichols 计算 Kp, Ki, Kd } } ``` **验收标准**: - [ ] PID 在阶跃输入下无超调(可配置) - [ ] 抗积分饱和逻辑正确 - [ ] Relay tuner 在标准二阶系统上能收敛 - [ ] `no_std` 兼容 --- ### 5.6 `rm-sim` — 仿真层 **职责**:提供电机模型、总线模型、测试场景,用于算法验证和自动整定。 **依赖**:`rm-control`、`rm-protocol` **文件结构**: ``` rm-sim/ ├── Cargo.toml └── src/ ├── lib.rs ├── motor_model/ │ ├── mod.rs │ ├── dc_motor.rs # 通用直流电机模型 │ ├── gm6020.rs # GM6020 参数化 │ ├── m3508.rs # M3508 参数化 │ └── m2006.rs # M2006 参数化 ├── bus_model.rs # CAN 总线模型(延迟/丢帧) ├── plant.rs # SimPlant 实现 ├── scenario.rs # 测试场景 └── tuner_runner.rs # 自动整定流程 ``` **核心设计**: ```rust // motor_model/dc_motor.rs pub struct DcMotorParams { pub resistance: f32, // 欧姆 pub inductance: f32, // 亨利 pub torque_constant: f32, // N·m/A pub back_emf_constant: f32, // V·s/rad pub gear_ratio: f32, pub rotor_inertia: f32, // kg·m² pub current_loop_bw: f32, // 电流环带宽 Hz } pub struct DcMotorModel { params: DcMotorParams, state: MotorState, } impl DcMotorModel { pub fn step(&mut self, current_cmd: f32, dt: f32) -> MotorOutput { // 电流环一阶滞后 → 转矩 → 加速度 → 速度 → 位置 } } // plant.rs — 仿真 plant 实现 ControlPlant pub struct SimPlant { model: M, noise: Option, // 编码器噪声 } impl ControlPlant for SimPlant { fn step(&mut self, input: f32, dt: f32) -> f32 { let out = self.model.step(input, dt); self.noise.map(|n| n.apply(out.position)).unwrap_or(out.position) } fn reset(&mut self) { self.model.reset(); } } // tuner_runner.rs pub async fn auto_tune( plant: &mut P, config: TuneConfig, ) -> TuneResult { let mut tuner = RelayTuner::new(config.amplitude, config.hysteresis); let mut t = 0.0; while t < config.max_time { let output = tuner.step(plant.step(tuner.last_output(), config.dt), config.dt); // ... t += config.dt; } tuner.result().expect("Tuning failed") } ``` **验收标准**: - [ ] 电机模型在阶跃输入下的响应符合物理直觉 - [ ] 仿真整定出的 PID 在实机上只需 ±20% 微调 - [ ] 支持批量扫描不同参数 --- ### 5.7 `rm-app` — 应用层 **职责**:任务编排、配置加载、实时调度、主循环。 **依赖**:所有下层 crate **文件结构**: ``` rm-app/ ├── Cargo.toml └── src/ ├── main.rs ├── config.rs # TOML 配置加载 ├── realtime.rs # SCHED_FIFO + mlockall ├── tasks/ │ ├── mod.rs │ ├── gimbal.rs # 云台任务 (can0) │ ├── shooter.rs # 发射机构任务 (can0 + can1) │ └── chassis.rs # 底盘任务 (SLCAN) ├── messages.rs # 任务间通信消息 └── telemetry.rs # 诊断输出 ``` **任务模型**: ```rust // main.rs #[tokio::main] async fn main() -> anyhow::Result<()> { // 1. 实时调度 realtime::enable(80)?; // 2. 加载配置 let config = config::load("config/robot.toml")?; // 3. 打开 CAN 总线 let can0 = SocketCanBus::open("can0")?; let can1 = SocketCanBus::open("can1")?; let slcan = SlcanBus::open("/dev/ttyACM0")?; // 4. 启动任务 let gimbal = tokio::spawn(tasks::gimbal::run(can0, config.gimbal)); let shooter = tokio::spawn(tasks::shooter::run(can1, config.shooter)); let chassis = tokio::spawn(tasks::chassis::run(slcan, config.chassis)); // 5. 等待 tokio::try_join!(gimbal, shooter, chassis)?; Ok(()) } // tasks/gimbal.rs pub async fn run(mut bus: SocketCanBus, config: GimbalConfig) -> anyhow::Result<()> { let mut motors = GimbalMotors::new(); let mut pid_yaw = PidController::new(config.yaw_pid); let mut pid_pitch = PidController::new(config.pitch_pid); let mut imu = Delta10Driver::new(&mut bus, config.imu_can_id); let mut ticker = tokio::time::interval(Duration::from_millis(2)); loop { ticker.tick().await; // 1. 非阻塞读取 delta10 帧 if let Some(data) = imu.try_read()? { // 更新姿态 } // 2. 读取电机反馈 while let Some(frame) = bus.try_recv()? { motors.update_from_frame(&frame)?; } // 3. 计算控制 let yaw_out = pid_yaw.compute(setpoint.yaw, imu.yaw, 0.002); let pitch_out = pid_pitch.compute(setpoint.pitch, imu.pitch, 0.002); // 4. 发送控制帧 motors.send_commands(&mut bus, yaw_out, pitch_out).await?; } } ``` **验收标准**: - [ ] 三个任务独立运行,互不阻塞 - [ ] 每个任务在自己的 2ms 周期内完成 - [ ] 配置可通过 TOML 热加载(或至少无需重新编译) --- ## 6. 数据流与任务模型 ### 6.1 任务划分 | 任务 | 频率 | 总线 | 设备 | 输出 | |------|------|------|------|------| | `gimbal_task` | 500Hz | can0 | 2× GM6020 + delta10 | 云台电流 | | `shooter_task` | 500Hz | can0 + can1 | 2× M3508 + 1× M2006 | 摩擦轮 + 拨弹电流 | | `chassis_task` | 500Hz | SLCAN | 4× M3508 | 底盘电流 | ### 6.2 任务间通信 ``` ┌──────────────┐ │ 遥控器输入 │ (未来扩展) └──────┬───────┘ │ watch ▼ ┌──────────────────────────────────────────┐ │ tokio channels │ │ ┌────────────┐ ┌────────────┐ │ │ │ gimbal_cmd │ │chassis_cmd │ │ │ └─────┬──────┘ └─────┬──────┘ │ └────────┼────────────────┼────────────────┘ │ │ ┌────▼────┐ ┌────▼────┐ │ gimbal │ │ chassis │ │ task │ │ task │ └─────────┘ └─────────┘ ``` **通信原语选择**: - **遥控器输入 → 任务**:`tokio::sync::watch`(单生产者多消费者,只关心最新值) - **任务 → 诊断输出**:`tokio::sync::mpsc`(有背压) - **任务间同步**:`tokio::sync::Barrier`(如需要) ### 6.3 每个周期的执行顺序 以 `gimbal_task` 为例: ``` t=0ms: 1. ticker.tick() 返回 2. 非阻塞读取 delta10 帧 (try_recv) 3. 非阻塞读取所有 pending CAN 帧 (while try_recv) 4. 更新电机反馈 5. 计算 PID 输出 6. 发送控制帧 (await) 7. 记录诊断 t=2ms: 重复 ``` **关键**:步骤 2-5 必须是**非阻塞**的,只有步骤 6 可以 `await`。这保证任务不会因为等待 CAN 帧而错过下一个 tick。 --- ## 7. 实时性设计 ### 7.1 调度策略 ```rust // realtime.rs use libc::{sched_param, sched_setscheduler, SCHED_FIFO, mlockall, MCL_CURRENT, MCL_FUTURE}; pub fn enable(priority: i32) -> anyhow::Result<()> { unsafe { // 1. 锁定内存,避免页错误 if mlockall(MCL_CURRENT | MCL_FUTURE) != 0 { return Err(std::io::Error::last_os_error().into()); } // 2. 设置实时调度 let param = sched_param { sched_priority: priority }; if sched_setscheduler(0, SCHED_FIFO, ¶m) != 0 { return Err(std::io::Error::last_os_error().into()); } } Ok(()) } ``` **优先级分配**: - `gimbal_task`: 80(最高,因为云台抖动直接影响射击) - `shooter_task`: 75 - `chassis_task`: 70 ### 7.2 抖动测量 在每个任务的 tick 中记录实际唤醒时间: ```rust let expected = start + Duration::from_millis(2 * tick_count); let actual = Instant::now(); let jitter = actual.duration_since(expected); telemetry::record_jitter(jitter); ``` **目标**:抖动 < 200μs(T153 标准 Linux 的典型值)。 ### 7.3 降级策略 如果抖动超过阈值(如 500μs),采取: 1. **降低非关键任务频率**:chassis_task 降到 250Hz 2. **合并任务**:shooter 和 gimbal 合并 3. **异构核卸载**:将 CAN 收发卸载到 E907(远期) --- ## 8. 仿真与自动整定 ### 8.1 仿真流程 ``` ┌─────────────────────────────────────────────────┐ │ 1. 定义电机参数 (config/motors.toml) │ │ ↓ │ │ 2. 构建 SimPlant │ │ ↓ │ │ 3. 运行 auto_tune │ │ ↓ │ │ 4. 输出 PID 参数 → config/pid.toml │ │ ↓ │ │ 5. 在 SimPlant 上验证阶跃响应 │ │ ↓ │ │ 6. 部署到实机,微调 ±20% │ └─────────────────────────────────────────────────┘ ``` ### 8.2 电机参数配置 ```toml # config/motors.toml [gm6020] resistance = 0.35 inductance = 0.0002 torque_constant = 0.024 back_emf_constant = 0.024 gear_ratio = 1.0 rotor_inertia = 1.5e-5 current_loop_bw = 1000.0 [m3508] resistance = 0.15 inductance = 0.0001 torque_constant = 0.015 back_emf_constant = 0.015 gear_ratio = 19.2 rotor_inertia = 2.0e-5 current_loop_bw = 800.0 [m2006] # ... ``` ### 8.3 自动整定示例 ```rust #[tokio::test] async fn tune_m3508_speed_loop() { let params = load_motor_params("config/motors.toml", "m3508"); let mut plant = SimPlant::new(DcMotorModel::new(params)); let config = TuneConfig { amplitude: 5000.0, // 电流指令幅值 hysteresis: 100.0, dt: 0.002, max_time: 5.0, }; let result = auto_tune(&mut plant, config).await; // 验证整定结果 assert!(result.kp > 0.0); assert!(result.ki > 0.0); assert!(result.ku > 0.0); assert!(result.tu > 0.0); // 在仿真中验证阶跃响应 let mut pid = PidController::new(result.into()); let mut plant = SimPlant::new(DcMotorModel::new(params)); let mut max_overshoot = 0.0f32; for _ in 0..500 { // 1 秒 let out = pid.compute(1000.0, plant.step(0.0, 0.002), 0.002); let _ = plant.step(out, 0.002); // 记录超调 } assert!(max_overshoot < 0.1, "超调过大: {}", max_overshoot); } ``` --- ## 9. 测试策略 ### 9.1 分层测试 | 层 | 测试类型 | 工具 | |----|---------|------| | `rm-protocol` | 单元测试 + 模糊测试 | `#[test]`, `proptest` | | `rm-control` | 单元测试 + 仿真测试 | `#[test]`, `rm-sim` | | `rm-devices` | 集成测试(mock CAN) | `mock` feature | | `rm-drivers` | 硬件在环测试 | `vcan` 回环 | | `rm-app` | 端到端测试 | `vcan` + 模拟电机 | ### 9.2 vcan 回环测试 ```bash # 创建虚拟 CAN 接口 sudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set up vcan0 ``` ```rust #[tokio::test] async fn test_socketcan_loopback() { let mut bus = SocketCanBus::open("vcan0").unwrap(); let frame = CanFrame { id: 0x1FF, data: [1,2,3,4,5,6,7,8], dlc: 8, is_extended: false }; bus.send(&frame).await.unwrap(); let received = bus.recv().await.unwrap(); assert_eq!(received.id, 0x1FF); } ``` ### 9.3 仿真回归测试 每次修改 `rm-control` 后,运行完整的仿真测试套件: ```bash cargo test -p rm-sim --release ``` **回归基准**:阶跃响应、抗扰响应、整定收敛时间。任何指标退化 >5% 视为失败。 --- ## 10. 部署与配置 ### 10.1 CAN 接口初始化脚本 ```bash #!/bin/bash # scripts/setup_can.sh # 原生 CAN (T153) ip link set can0 type can bitrate 1000000 restart-ms 100 ip link set can0 up ip link set can1 type can bitrate 1000000 restart-ms 100 ip link set can1 up # SLCAN (candlelite2) — 由应用层直接打开串口,无需 ip link # 但需要设置串口权限 chmod 666 /dev/ttyACM0 echo "CAN interfaces ready" ip -details link show can0 ip -details link show can1 ``` ### 10.2 交叉编译 ```toml # .cargo/config.toml [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" rustflags = [ "-C", "target-cpu=cortex-a7", "-C", "target-feature=+neon", ] ``` ```bash # 构建 cargo build --release --target armv7-unknown-linux-gnueabihf # 部署 scp target/armv7-unknown-linux-gnueabihf/release/rm-app root@t153:/opt/rm/ scp -r config/ root@t153:/opt/rm/ ``` ### 10.3 运行时配置 ```toml # config/robot.toml [realtime] gimbal_priority = 80 shooter_priority = 75 chassis_priority = 70 mlock = true [can] can0_bitrate = 1000000 can1_bitrate = 1000000 slcan_device = "/dev/ttyACM0" slcan_bitrate = 1000000 [gimbal] yaw_pid = { kp = 10.0, ki = 0.5, kd = 0.1 } pitch_pid = { kp = 12.0, ki = 0.6, kd = 0.15 } imu_can_id = 0x100 [shooter] friction_pid = { kp = 5.0, ki = 0.2, kd = 0.05 } trigger_pid = { kp = 3.0, ki = 0.1, kd = 0.02 } [chassis] speed_pid = { kp = 8.0, ki = 0.3, kd = 0.05 } ``` --- ## 11. 执行 Agent 工作流 ### 11.1 阶段划分 | 阶段 | 任务 | 验收 | |------|------|------| | **P0: 骨架** | 创建 workspace + 所有 crate 的空骨架 | `cargo build` 通过 | | **P1: 协议层** | 实现 `rm-hal` + `rm-protocol` | 单元测试 + 模糊测试通过 | | **P2: 驱动层** | 实现 `rm-drivers`(SocketCAN + SLCAN) | vcan 回环测试通过 | | **P3: 设备层** | 实现 `rm-devices` | mock 测试通过 | | **P4: 控制层** | 实现 `rm-control` | PID 单元测试通过 | | **P5: 仿真层** | 实现 `rm-sim` | 整定测试通过 | | **P6: 应用层** | 实现 `rm-app` | 在 T153 上运行 | | **P7: 实机联调** | 在真实机器人上验证 | 所有电机可控 | ### 11.2 每个阶段的交付物 - **代码**:所有实现文件 - **测试**:单元测试 + 集成测试 - **文档**:模块级文档注释 + 必要的 README - **配置**:对应的 TOML 配置文件 ### 11.3 质量门禁 每个阶段完成后必须通过: 1. `cargo build --workspace` 无警告 2. `cargo test --workspace` 全绿 3. `cargo clippy --workspace -- -D warnings` 无警告 4. `cargo fmt --check` 通过 5. 新增代码有文档注释 6. 关键路径有测试覆盖 ### 11.4 决策点 以下决策需要人工确认后才能继续: - **D1**: delta10 的 CAN ID 和协议模式(需查阅数据手册) - **D2**: GM6020 和 M3508 的具体 ID 分配(需确认硬件接线) - **D3**: candlelite2 的串口设备名(需在 T153 上 `ls /dev/ttyACM*`) - **D4**: 是否启用 E907 异构核(基于 P7 的抖动测试结果) --- ## 12. 附录 ### 12.1 术语表 | 术语 | 含义 | |------|------| | SLCAN | Serial CAN,一种通过串口传输 CAN 帧的 ASCII 协议 | | SocketCAN | Linux 内核的 CAN 子系统 | | SLCAN 固件 | candlelite2 上运行的非 SocketCAN 固件 | | vcan | Linux 虚拟 CAN 接口,用于测试 | | FDILINK | delta10 的一种 CAN 协议模式 | | ZN | Ziegler-Nichols,一种 PID 整定方法 | ### 12.2 参考链接 - [socketcan-rs](https://github.com/socketcan-rs/socketcan-rs) - [slcan_fd](https://crates.io/crates/slcan_fd) - [discrete_pid](https://crates.io/crates/discrete_pid) - [candleLight](https://github.com/candle-usb/candleLight_fw) - [DETA10 手册](https://fdisystems.github.io/DETA10_Docs/) ### 12.3 变更日志 | 版本 | 日期 | 变更 | |------|------|------| | 1.0 | 2026-09-23 | 初始版本,基于 T153 + SLCAN + delta10 确认 | --- **文档结束** 执行 Agent 应按照第 11 节的阶段划分逐步推进,每个阶段完成后提交验收报告,遇到决策点(D1-D4)时暂停并请求人工确认。