Files
linux_sentry_chassis/ARCHITECTURE.md
T

1173 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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
```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<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 编译期校验
```
**核心设计**:
```rust
// 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)
```
**核心实现**:
```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<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 # 电机注册表(管理所有电机)
```
**核心设计**:
```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<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 # 自动整定流程
```
**核心设计**:
```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<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 # 诊断输出
```
**任务模型**:
```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<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 调度策略
```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, &param) != 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<DcMotorModel> │
│ ↓ │
│ 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)时暂停并请求人工确认。