35 KiB
RoboMaster CAN 控制系统架构文档
版本: 1.0 目标平台: Allwinner T153 (Cortex-A7 @ 1.6GHz, Linux) 控制频率: 500Hz 文档目的: 指导执行 Agent 完成项目拆分、结构整理与模块设计
目录
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验证帧收发 - 有
mockfeature 用于测试
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, ¶m) != 0 {
return Err(std::io::Error::last_os_error().into());
}
}
Ok(())
}
优先级分配:
gimbal_task: 80(最高,因为云台抖动直接影响射击)shooter_task: 75chassis_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),采取:
- 降低非关键任务频率:chassis_task 降到 250Hz
- 合并任务:shooter 和 gimbal 合并
- 异构核卸载:将 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 质量门禁
每个阶段完成后必须通过:
cargo build --workspace无警告cargo test --workspace全绿cargo clippy --workspace -- -D warnings无警告cargo fmt --check通过- 新增代码有文档注释
- 关键路径有测试覆盖
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)时暂停并请求人工确认。