Files
linux_sentry_chassis/ARCHITECTURE.md
T

35 KiB
Raw Blame History

RoboMaster CAN 控制系统架构文档

版本: 1.0 目标平台: Allwinner T153 (Cortex-A7 @ 1.6GHz, Linux) 控制频率: 500Hz 文档目的: 指导执行 Agent 完成项目拆分、结构整理与模块设计


目录

  1. 系统概述
  2. 硬件拓扑
  3. 架构原则
  4. Crate 拆分
  5. 各 Crate 详细设计
  6. 数据流与任务模型
  7. 实时性设计
  8. 仿真与自动整定
  9. 测试策略
  10. 部署与配置
  11. 执行 Agent 工作流
  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<const ID: u8>
newtype 类型安全的值封装 Current(i16)、Angle(f32)
async fn in trait 异步接口抽象 CanBus::send
PhantomData 标记通道归属 SocketCanBus<const CH: u8>
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

[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:

// 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<CanFrame, Self::Error>;
    fn try_recv(&mut self) -> Result<Option<CanFrame>, 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<ImuData, Self::Error>;
}

验收标准:

  • 所有 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 编译期校验

核心设计:

// id.rs — 编译期 ID 校验
pub struct MotorId<const ID: u8>;

impl<const ID: u8> MotorId<ID> {
    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<const ID: u8>(&self, id: MotorId<ID>) -> 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)

核心实现:

// 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<CanFrame, Self::Error> {
        let f = self.socket.read_frame().await?;
        Ok(from_socketcan_frame(f))
    }
}

// slcan.rs
pub struct SlcanBus {
    socket: slcan_fd::tokio::CanSocket<tokio_serial::SerialStream>,
}

impl CanBus for SlcanBus {
    // 类似实现,但底层使用 SLCAN 协议
}

// delta10.rs
pub struct Delta10Driver<B: CanBus> {
    bus: B,
    can_id: u16,
    last_data: Option<ImuData>,
}

impl<B: CanBus> Imu for Delta10Driver<B> {
    async fn read(&mut self) -> Result<ImuData, Self::Error> {
        // 等待 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         # 电机注册表(管理所有电机)

核心设计:

// 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 公式

核心设计:

// 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<f32>,
    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<OscillationPoint>,
}

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<TuneResult> {
        // 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     # 自动整定流程

核心设计:

// 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<M: MotorModel> {
    model: M,
    noise: Option<NoiseModel>,  // 编码器噪声
}

impl<M: MotorModel> ControlPlant for SimPlant<M> {
    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<P: ControlPlant>(
    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        # 诊断输出

任务模型:

// 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<RemoteCommand>
       ▼
┌──────────────────────────────────────────┐
│              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 调度策略

// 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, &param) != 0 {
            return Err(std::io::Error::last_os_error().into());
        }
    }
    Ok(())
}

优先级分配:

  • gimbal_task: 80(最高,因为云台抖动直接影响射击)
  • shooter_task: 75
  • chassis_task: 70

7.2 抖动测量

在每个任务的 tick 中记录实际唤醒时间:

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<DcMotorModel>                  │
│     ↓                                            │
│  3. 运行 auto_tune                               │
│     ↓                                            │
│  4. 输出 PID 参数 → config/pid.toml              │
│     ↓                                            │
│  5. 在 SimPlant 上验证阶跃响应                    │
│     ↓                                            │
│  6. 部署到实机,微调 ±20%                        │
└─────────────────────────────────────────────────┘

8.2 电机参数配置

# 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 自动整定示例

#[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 回环测试

# 创建虚拟 CAN 接口
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0
#[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 后,运行完整的仿真测试套件:

cargo test -p rm-sim --release

回归基准:阶跃响应、抗扰响应、整定收敛时间。任何指标退化 >5% 视为失败。


10. 部署与配置

10.1 CAN 接口初始化脚本

#!/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 交叉编译

# .cargo/config.toml
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
rustflags = [
    "-C", "target-cpu=cortex-a7",
    "-C", "target-feature=+neon",
]
# 构建
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 运行时配置

# 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 参考链接

12.3 变更日志

版本 日期 变更
1.0 2026-09-23 初始版本,基于 T153 + SLCAN + delta10 确认

文档结束

执行 Agent 应按照第 11 节的阶段划分逐步推进,每个阶段完成后提交验收报告,遇到决策点(D1-D4)时暂停并请求人工确认。