Files
ASER-NAV/Doc/HANDOFF.md

774 lines
35 KiB
Markdown
Raw Permalink 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.
# ARES 项目交接文档
> **最后更新**: 2025 年
> **项目性质**: B类"马铃薯捡拾机器人竞技"走廊导航系统
> **本文目标**: 让任何人 (人类或 AI) 能在 30 分钟内理解整个项目并开始工作
---
## 目录
1. [一句话概述](#1-一句话概述)
2. [比赛规则速览](#2-比赛规则速览)
3. [硬件架构](#3-硬件架构)
4. [软件架构总览](#4-软件架构总览)
5. [代码目录结构](#5-代码目录结构)
6. [数据流水线 (核心)](#6-数据流水线-核心)
7. [各模块详解](#7-各模块详解)
8. [FreeRTOS 任务一览](#8-freertos-任务一览)
9. [CAN 通信协议](#9-can-通信协议)
10. [EKF 算法详解](#10-ekf-算法详解)
11. [控制器与安全层](#11-控制器与安全层)
12. [比赛流程状态机](#12-比赛流程状态机)
13. [全部可调参数](#13-全部可调参数)
14. [已知问题与待办](#14-已知问题与待办)
15. [构建与烧录](#15-构建与烧录)
16. [实车调试流程](#16-实车调试流程)
17. [文件快速索引](#17-文件快速索引)
---
## 1. 一句话概述
一个运行在 **STM32H743 (FreeRTOS)** 上的走廊跟踪导航系统——通过 **4 个 VL53L0X 侧向测距** 做横向/航向定位(鲁棒 EKF**4 个前后激光** 做防撞和到端检测PD 控制器纠偏,安全状态机兜底,最终通过 **CAN 总线** 将速度指令发给 STM32F407 底盘控制器。
---
## 2. 比赛规则速览
| 项目 | 规格 |
|------|------|
| 场地尺寸 | 390 cm × 300 cm |
| 垄沟(走廊)宽度 | **40 cm**(允许 ±5% 误差) |
| 田垄数量 | 5 条 |
| 垄间模式需遍历 | **6 条垄沟** |
| 机器人尺寸 | **20 cm × 20 cm**(含外壳) |
| 驱动方式 | 四轮差速 |
| 比赛时间 | 5 分钟 |
| 任务 | 在走廊中往返行驶,捡拾目标物 |
**关键约束**: 走廊 40cm - 车体 20cm = 每边仅 **10cm 余量**。1cm 的传感器偏差就是 10% 的误差。
### 2.1 赛道拓扑理解(垄间模式)
本项目当前最相关的正式比赛模式是 **垄间作业模式**
#### 赛道几何
- 整个比赛场地尺寸为 `390cm × 300cm`
- 场地内部共有 **5 条田垄**
- 每条田垄:长 `220cm`、宽 `30cm`、高 `12cm`
- 围栏与田垄之间、以及相邻田垄之间,均形成宽 `40cm` 的可通行垄沟
- 场地仅有 **1 个出入口**,宽 `40cm`
- 出入口外侧紧邻 **比赛启动区**,尺寸 `40cm × 100cm`
#### 为什么是 6 条垄沟,不是 5 条
虽然场地内只有 **5 条田垄**,但在垄间模式下,机器人实际需要遍历的是 **6 条垄沟**
1. 最左侧围栏与第 1 条田垄之间的垄沟
2. 第 1 条田垄与第 2 条田垄之间的垄沟
3. 第 2 条田垄与第 3 条田垄之间的垄沟
4. 第 3 条田垄与第 4 条田垄之间的垄沟
5. 第 4 条田垄与第 5 条田垄之间的垄沟
6. 第 5 条田垄与最右侧围栏之间的垄沟
也就是说:
- **田垄数 = 5**
- **垄间可通行垄沟数 = 6**
这是赛道理解里最容易搞错的地方。若只按“5 条走廊”建模,会与正式比赛要求不一致。
#### 垄间模式下的导航任务本质
在垄间模式下,导航任务不是“单条走廊往返”,而是:
1. 从启动区进入场地
2. 进入某一条垄沟
3. 沿垄沟稳定行驶并完成作业
4. 到达端部后完成转向和换沟
5. 依次遍历 **全部 6 条垄沟**
6. 最终从唯一出入口驶离场地
7. 自主停在启动区内
从导航角度看,赛道是一个 **6 段平行窄走廊 + 端部换沟动作 + 单出入口回停** 的组合问题。
#### 对导航系统的直接要求
- **窄走廊居中跟踪**
走廊宽 `40cm`,若车体宽 `20cm`,理论左右余量各仅 `10cm`
- **到端检测**
能识别走到垄沟尽头,避免撞围栏
- **端部转向**
在垄沟末端稳定完成姿态调整
- **换沟**
从当前垄沟切换到相邻垄沟,而不是原地掉头后回到同一条垄沟
- **赛道级状态机**
知道当前是第几条垄沟、下一条目标是哪条、何时结束遍历
- **最终退出与停区**
驶离场地后必须自主停在启动区,否则可能判 0 分
#### 当前项目与赛道要求的关系
当前 ARES 项目已经具备:
- 单条走廊内的相对定位
- 走廊跟踪控制
- 到端检测
- 原地转向
- 安全停车与防撞
但当前脚本状态机更接近:进入一条走廊、沿走廊前进、到端后转向、再走一趟、然后退出。
它更像 **单垄沟闭环验证系统**,还不是完整的 **6 垄沟遍历赛道导航系统**
后续若要适配正式比赛,重点不是重写 EKF而是补上
- 多垄沟遍历状态机
- 端部换沟策略
- 最终出场与启动区停车逻辑
---
## 3. 硬件架构
### 3.1 双 MCU 体系
```
┌─────────────────────────────────┐ CAN Bus ┌─────────────────────────┐
│ STM32H743 (上位机/主控) │ ◄──────────────► │ STM32F407 (底盘控制器) │
│ 480MHz Cortex-M7, DP-FPU │ 0x100 TX │ LADRC 电机控制 │
│ FreeRTOS + 传感器 + 导航算法 │ 0x181/200 RX │ 四路编码器 + 四路电机 │
└─────────────────────────────────┘ └─────────────────────────┘
```
### 3.2 传感器清单
| 传感器 | 数量 | 用途 | 接口 | 量程 |
|--------|------|------|------|------|
| **VL53L0X** ToF | 4 (左前/左后/右前/右后) | 侧向墙壁测距,走廊跟踪 | I2C1 + I2C2 | 2cm ~ 2m |
| **STP-23L** 激光 | 2 (前/后) | 远距离测距,到端检测 | UART2 / UART4 (230400bps) | 7cm ~ 7.5m |
| **ATK-MS53L1M** 激光 | 2 (前/后) | 近距离补盲 (填 STP 的 7cm 盲区) | UART3 / UART6 (115200bps) | ~4m |
| **HWT101** IMU | 1 | 单轴陀螺仪 (航向角速度 wz) | UART7 | ±2000°/s |
### 3.3 VL53L0X 接线
```
左侧 (I2C2): 右侧 (I2C1):
左前 LF: XSHUT=PB1, 地址=0x62 右前 RF: XSHUT=PD13, 地址=0x56
左后 LR: XSHUT=PC5, 地址=0x64 右后 RR: XSHUT=PD14, 地址=0x58
```
### 3.4 其他 GPIO
| 引脚 | 功能 |
|------|------|
| PE3 | CAN 活动 LED (100ms 翻转) |
---
## 4. 软件架构总览
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ FreeRTOS 任务层 │
│ canTxTask monitorTask navTask laserTestTask vl53Task imuTask │
│ (20ms) (100ms) (20ms) (50ms) (100ms) (10ms) │
└──────┬──────────┬──────────┬──────────┬───────────────┬──────────┬─────────┘
│ │ │ │ │ │
│ CmdSlot │ │ Blackboard (全局传感器数据中心) │
│ Pop │ │ taskENTER_CRITICAL 保护 │
▼ ▼ ▼ │
┌─────────┐ ┌────────┐ ┌───────────────────────────────────┐ │
│CAN 0x100│ │Odom │ │ 导航流水线 (navTask, 20ms) │ │
│→ 底盘 │ │积分 │ │ Preproc → EKF → Script │ │
│ │ │→黑板 │ │ → Ctrl → SafetyFSM → CmdSlot │ │
└─────────┘ └────────┘ └───────────────────────────────────┘ │
┌───────────────────────────────────────────────────────────┘
各传感器驱动hwt101.c / vl53_board.c / laser_manager.c
→ Blackboard_Update*()
```
---
## 5. 代码目录结构
```
D:\ARES\
├── App/ ← 【所有业务代码在这里】
│ ├── robot_params.h ← ★ 全局参数配置中心 (唯一的调参入口)
│ ├── app_tasks.c/.h ← 所有 FreeRTOS 任务实现 + 系统初始化
│ ├── retarget.c/.h ← printf → USB CDC 重定向
│ │
│ ├── Can/
│ │ └── snc_can_app.c/.h ← CAN 协议层 (编解码/CRC8/滤波器)【已冻结,勿动】
│ │
│ ├── Contract/ ← 模块间数据契约
│ │ ├── robot_blackboard.c/.h ← 全局传感器数据黑板 (线程安全)
│ │ ├── robot_odom.c/.h ← 差速里程计积分
│ │ ├── robot_cmd_slot.c/.h ← 导航→CAN 指令传递槽
│ │ └── chassis_can_msg.h ← CAN 帧结构体定义
│ │
│ ├── est/ ← 状态估计
│ │ ├── corridor_ekf.c/.h ← ★ 鲁棒 EKF 核心 (3 状态)
│ │ └── corridor_filter.c/.h ← EKF 的兼容包装层
│ │
│ ├── nav/ ← 导航与控制
│ │ ├── corridor_ctrl.c/.h ← PD 走廊控制器
│ │ ├── segment_fsm.c/.h ← 安全状态机 (防撞/急停)
│ │ └── nav_script.c/.h ← 比赛流程编排
│ │
│ ├── preproc/ ← 传感器预处理
│ │ ├── corridor_preproc.c/.h ← 数据清洗 + 互补融合
│ │ └── corridor_msgs.h ← 所有模块间消息结构体
│ │
│ ├── IMU/
│ │ └── hwt101.c/.h ← HWT101 IMU 驱动 (UART DMA)
│ │
│ ├── laser/
│ │ └── laser_manager.c/.h ← 前后激光驱动 (STP+ATK, UART DMA)
│ │
│ └── VL53L0X_API/
│ ├── platform/
│ │ ├── vl53_board.c/.h ← 多传感器管理 + 卡尔曼滤波
│ │ └── vl53l0x_platform.c/.h ← ST API HAL 适配
│ └── core/ ← ST 官方 VL53L0X API (不修改)
├── Core/ ← CubeMX 生成的 HAL 初始化代码
├── Drivers/ ← STM32H7 HAL 驱动
├── Middlewares/ ← FreeRTOS + USB
├── USB_DEVICE/ ← CDC 虚拟串口
├── ARES.ioc ← CubeMX 工程文件
├── CMakeLists.txt ← 构建入口
└── STM32H743XX_FLASH.ld ← 链接脚本
```
---
## 6. 数据流水线 (核心)
这是整个系统的心脏,运行在 `navTask` 中,**每 20ms 执行一次**
```
Step 1: Blackboard_GetSnapshot(&board) 拍摄传感器快照 (无撕裂)
Step 2: CorridorPreproc_ExtractObs() 数据清洗
│ • VL53L0X: mm→m, 范围校验 (0.02~2.0m)
│ • STP+ATK: 互补融合 (ATK 填 STP 的 7cm 盲区)
│ • 前后激光偏移补偿 (d -= PARAM_FRONT_LASER_OFFSET)
│ → 输出: CorridorObs_t {d_lf, d_lr, d_rf, d_rr, d_front, d_back, valid_mask}
Step 3: CorridorFilter_Update() EKF 状态估计
│ → Predict(odom_vx, imu_wz, dt) 预测步 (IMU wz + 里程计)
│ → Update(obs) 观测步 (VL53 侧向测距)
│ → UpdateIMUYaw(yaw_cont, ref) IMU 航向观测 (独立 1DOF 更新)
│ → 输出: CorridorState_t {e_y, e_th, s, conf}
Step 4: NavScript_Update() 比赛流程编排
│ → 决定当前阶段: 入口对准/走廊跟踪/转向/退出
│ → 180° 转弯判定使用 IMU 连续 yaw (非 EKF e_th)
│ → 输出: NavScriptOutput_t {use_override, override_v/w, request_corridor}
Step 5: CorridorCtrl_Compute() PD 控制律 (仅在走廊跟踪模式)
│ → w = kp_theta·e_th + kd_theta·(-wz) + kp_y·e_y
│ → v = v_cruise × (1 - 0.4 × |w/w_max|)
│ → 输出: RawCmd_t {v, w}
Step 6: SegFsm_Update() 安全仲裁
│ → 前方太近? → 减速/停车
│ → 置信度太低? → 紧急停车
│ → 输出: SegFsmOutput_t {safe_v, safe_w}
Step 7: CmdSlot_Push(safe_v, safe_w) 推入指令槽 → canTxTask 取走
```
---
## 7. 各模块详解
### 7.1 传感器驱动层
#### VL53L0X 侧向测距 (`vl53_board.c`)
- 上电时所有 XSHUT 拉低,逐个拉高分配 I2C 地址
- 测距预算 100ms (100000μs),连续测距模式
- 每个传感器读数经过独立的 **1D 卡尔曼滤波** (Q=10.0, R=14.1)
- 输出 `Vl53BoardSnapshot_t``Blackboard_UpdateVl53()`
#### 前后激光测距 (`laser_manager.c`)
- DMA 环形缓冲区轮询,内部自有 FreeRTOS 任务 `LaserTsk` (10ms)
- **STP-23L**: 二进制协议含校验和3 帧中值滤波去尖刺
- **ATK-MS53L1M**: ASCII 文本 `"d: 1234\r\n"`3 帧中值滤波
- 互补融合策略:
- ATK 距离 < 8cm 时无条件信任 ATK (STP 盲区)
- STP 卡在 7cm 时用 ATK
- 两者都活着取更近的 (保守防撞)
- STP 独活但在盲区 → 报失效 (宁可急停也不信假数据)
#### IMU (`hwt101.c`)
- UART DMA 11 字节二进制帧
- 输出: `yaw` (°)、`yaw_continuous` (°) 和 `wz` (°/s)
- `yaw`: 原始偏航角,范围 [-180°, +180°),跨界时跳变
- `yaw_continuous`: **unwrap 后的连续偏航角**,消除 ±180° 跳变,可直接做差计算任意转角
- 注意: **输出单位是°/s不是 rad/s**
- `HWT101_ZeroYaw()` 会同时重置 unwrap 状态
### 7.2 数据契约层
#### 黑板 (`robot_blackboard.c`)
- 全局结构体 `g_blackboard`,所有传感器数据汇聚于此
- 所有读写都在 `taskENTER_CRITICAL()` 临界区内
- 消费者调用 `Blackboard_GetSnapshot()` 获取无撕裂快照
- IMU 字段: `imu_yaw` (原始°)、`imu_yaw_continuous` (连续°)、`imu_wz` (°/s)
#### 里程计 (`robot_odom.c`)
- 从 CAN 0x200 获取四轮编码器增量,经 **ISR 侧累加器** 汇聚后由 monitorTask 一次性取走积分
- 消费链路BUG-8 修复后):
```
CAN Rx ISR → snc_parse_odom_delta() 每帧累加到 odom_accum不覆盖
monitorTask → SNC_CAN_ConsumeOdomDelta() 原子取走累计值并清零
Odom_Update() 仅在有新帧时调用(杜绝重复积分)
```
- 差速运动学:
```
v_left = (fl_delta + rl_delta) / 2 ×× 0.060 / 500) / dt
v_right = (fr_delta + rr_delta) / 2 ×× 0.060 / 500) / dt
vx = (v_left + v_right) / 2
wz = (v_right - v_left) / 0.140
```
- 3 帧连续零增量 → 强制 vx=wz=0 (静止检测)
#### 指令槽 (`robot_cmd_slot.c`)
- 单槽无锁设计,`CmdSlot_Push()` 写入,`CmdSlot_Pop()` 读取
- 超过 100ms 未更新 → canTxTask 自动发零速指令 (看门狗)
### 7.3 估计层
详见 [第10节 EKF 算法详解](#10-ekf-算法详解)。
### 7.4 控制层
详见 [第11节 控制器与安全层](#11-控制器与安全层)。
### 7.5 导航层
详见 [第12节 比赛流程状态机](#12-比赛流程状态机)。
---
## 8. FreeRTOS 任务一览
| 任务名 | 周期 | 优先级 | 栈大小 | 职责 |
|--------|------|--------|--------|------|
| `canTxTask` | 20ms | AboveNormal (28) | 2048B | CAN 0x100 硬实时发送;指令槽看门狗 |
| `navTask` | 20ms | AboveNormal (28) | 4096B | 完整导航流水线 (7步) |
| `LaserTsk` (内部) | 10ms | AboveNormal (28) | 4096B | 4路 UART DMA 解析 |
| `monitorTask` | 100ms | Normal (24) | 4096B | CAN 健康监控;里程计更新 |
| `laserTestTask` | 50ms | Normal (24) | 16384B | 激光轮询 + 推送黑板 |
| `vl53Task` | 100ms | Normal (24) | 4096B | VL53L0X 读取 + 推送黑板 |
| `imuTask` | 10ms | BelowNormal (20) | 2048B | IMU 解析 + 推送黑板 |
| `defaultTask` | — | Normal (24) | 512B | USB CDC 初始化;空闲循环 |
**全局配置**: Tick 频率 1000Hz, 堆 65536 字节, 最大优先级 56
---
## 9. CAN 通信协议
### 9.1 TX: H743 → F407
#### 0x100 — 速度指令 (每 20ms 发送)
| 字节 | 字段 | 类型 | 编码 |
|------|------|------|------|
| 0-1 | vx | int16 LE | vx (m/s) × 1000 |
| 2-3 | wz | int16 LE | wz (rad/s) × 1000 |
| 4 | ctrl_flags | uint8 | 控制标志位 |
| 5 | reserved | uint8 | 固定 0 |
| 6 | rolling_counter | uint8 | 单调递增 |
| 7 | crc8 | uint8 | CRC8-SAE-J1850 (bytes[0..6]) |
**硬性约束**: 必须每 ≤150ms 发一次,否则底盘进入 `SAFE_FAULT`。即使停车也要发 vx=0, wz=0。
#### 0x080 — 心跳 (每 20ms, DLC=0)
### 9.2 RX: F407 → H743
#### 0x181 — 底盘状态 (20ms)
| 字节 | 字段 |
|------|------|
| 0 | system_state: 0=BOOTING, 1=OPERATIONAL, 2=SAFE_FAULT |
| 1 | system_health: 0=OK, 1=WARNING, 2=FAULT |
| 2-5 | diag_bits (uint32 LE): 通信超时/CAN BUS OFF/电机堵转等 |
| 6 | cmd_age_10ms: 上次有效指令距今 (×10ms) |
| 7 | status_counter |
#### 0x200 — 里程计增量 (~60ms)
| 字节 | 字段 |
|------|------|
| 0-1 | fl_delta_ticks (int16 LE) — 左前轮编码器增量 |
| 2-3 | rl_delta_ticks (int16 LE) — 左后轮 |
| 4-5 | fr_delta_ticks (int16 LE) — 右前轮 |
| 6-7 | rr_delta_ticks (int16 LE) — 右后轮 |
#### 0x184 — 通信诊断 (100ms)
CRC 错误计数、计数器拒绝计数、CAN 丢包等统计信息。
---
## 10. EKF 算法详解
### 状态向量
```
x = [e_y, e_th, s]ᵀ
```
| 状态 | 含义 | 单位 |
|------|------|------|
| `e_y` | 车体中心相对走廊中心线的横向偏差 | m (左偏为正) |
| `e_th` | 车头相对走廊方向的航向偏差 | rad (左偏为正) |
| `s` | 沿走廊行驶里程 | m |
### 预测步 (每 20ms)
```
输入: odom_vx (m/s), imu_wz (rad/s), dt (s)
e_y_new = e_y + vx × sin(e_th) × dt
e_th_new = e_th + wz × dt
s_new = s + vx × cos(e_th) × dt
雅可比矩阵 F:
[1, vx·cos(e_th)·dt, 0]
[0, 1, 0]
[0, -vx·sin(e_th)·dt, 1]
P_pred = F·P·Fᵀ + Q
```
### 观测步
```
d_center = (W - Rw) / 2 + inset
= (0.40 - 0.20) / 2 + 0 = 0.10m (车居中时传感器到墙的距离)
左侧观测:
z_ey = d_center - (d_lf + d_lr)/2 横向偏差
z_eth_L = atan2(d_lr - d_lf, L_s) 航向偏差
右侧观测:
z_ey = (d_rf + d_rr)/2 - d_center 横向偏差
z_eth_R = atan2(d_rf - d_rr, L_s) 航向偏差
双侧有效时: z_ey 取两侧平均 = (d_right - d_left) / 2
→ d_center 被消掉,结果只取决于左右差值
单侧退化时: z_ey = d_center - d_one_side
→ d_center 的准确性至关重要!
IMU 航向观测 (侧墙更新之后独立执行):
z_eth_imu = (imu_yaw_continuous - yaw_ref) × DEG2RAD
yaw_ref 在 EKF 置信度 ≥ 0.5 时首次锁定
使用 1DOF 标量 EKF 更新R 值 (PARAM_EKF_R_ETH_IMU) 远大于侧墙 R
→ 侧墙有效时 IMU 影响小;侧墙丢失时 IMU 提供航向约束
```
### 鲁棒拒绝
每个观测独立做 **χ² 马氏距离检验**:
- 1 自由度门限 3.84 (95% 置信度)
- 超过门限的观测被标记为异常并跳过更新
### 置信度计算
```
conf = f(协方差迹) × 侧面因子 × (1 - 拒绝比例 × 0.5)
侧面因子: 双侧=1.0, 单侧=0.7
两侧全失效: 协方差膨胀conf 趋向 0 → 触发 E-STOP
```
---
## 11. 控制器与安全层
### 11.1 PD 走廊控制器
```
w_cmd = kp_theta × e_th + kd_theta × (-imu_wz) + kp_y × e_y
─────────────── ───────────────────── ────────────
航向比例纠偏 航向微分阻尼(用IMU) 横向比例纠偏
w_cmd = clamp(w_cmd, ±1.5 rad/s)
v_cmd = v_cruise × (1 - 0.4 × |w/w_max|) 弯道减速
v_cmd = clamp(v_cmd, 0, 0.3 m/s)
置信度保护:
conf < 0.3 → v × 0.3 (三折)
conf < 0.6 → v × 0.7 (七折)
```
**参数**: kp_theta=2.0, kd_theta=0.1, kp_y=3.0, v_cruise=0.15m/s
### 11.2 安全状态机 (`segment_fsm.c`)
```
conf < 0.1
┌──────────────────┐
▼ │
┌──────┐ Start ┌──────────┐ │ d_front ≤ 0.25m ┌──────────┐
│ IDLE ├────────►│ CORRIDOR ├──┴──────────────────►│ APPROACH │
└──────┘ └────┬─────┘ ◄────────────────── └────┬─────┘
│ d_front > 0.25m │
│ d_front ≤ 0.08m
conf ≥ 0.5 │
┌────┴─────┐ ┌──────▼─────┐
│ E-STOP │ │ STOP │
│ v=0,w=0 │ │ v=0,w=0 │
└──────────┘ └────────────┘
```
| 状态 | 行为 |
|------|------|
| **CORRIDOR** | 放行控制器输出 |
| **APPROACH** | 线性减速: v 从 raw_v 衰减至 0.05m/s (d 从 25cm→8cm) |
| **STOP** | 强制零速 |
| **E-STOP** | 强制零速conf ≥ 0.5 时自动恢复 |
---
## 12. 比赛流程状态机
> 注意:以下状态机描述的是 **当前固件已实现的单垄沟往返流程**,用于验证走廊跟踪、到端检测、转向与退出链路。
> 它 **不等价于** 正式比赛垄间模式要求的“遍历全部 6 条垄沟”的完整赛道级导航状态机。
```
IDLE → ENTRY_ALIGN → CORRIDOR_FORWARD → TURN_AT_END → CORRIDOR_BACKWARD
(pass_count ≥ 2)
EXIT → FINISHED
```
| 阶段 | 触发条件 | 行为 | 退出条件 |
|------|----------|------|----------|
| **IDLE** | 初始状态 | 零速 | `NavScript_Start()` |
| **ENTRY_ALIGN** | Start() | 慢速前进 0.08m/s等侧向雷达找到墙 | 双侧有效 + conf≥0.8;或超时 30s |
| **CORRIDOR_FWD** | 入口对准完成 | 走廊控制器跟踪 | d_front ≤ 0.10m (到端) |
| **TURN_AT_END** | 到端 | 原地转 180° (1.0 rad/s, 接近时减速);使用 IMU 连续 yaw 判定转角 | 转角 ≥ π-0.1 rad |
| **CORRIDOR_BWD** | 第1次转完 | 走廊控制器跟踪 | d_back ≤ 0.10m (到端) |
| **EXIT** | 第2次转完 | 0.5m/s 直线冲出 | 双侧 VL53 全丢 + 行驶 ≥ 2m |
| **FINISHED** | 冲出完成 | 零速停车 | 终态 |
---
## 13. 全部可调参数
所有参数集中在 **`App/robot_params.h`** 中,修改后需重新编译烧录。
### P0 — 几何参数 (必须实测)
| 参数名 | 当前值 | 单位 | 说明 |
|--------|--------|------|------|
| `PARAM_ROBOT_WIDTH` | **0.200** | m | 车体外轮廓宽度 |
| `PARAM_ROBOT_LENGTH` | **0.200** | m | 车体外轮廓长度 |
| `PARAM_WHEEL_DIAMETER` | **0.060** | m | 驱动轮外径 |
| `PARAM_WHEEL_TRACK` | **0.140** | m | 左右轮中心距 |
| `PARAM_SENSOR_BASE_LENGTH` | **0.120** | m | 同侧前后 VL53 间距 |
| `PARAM_CORRIDOR_WIDTH` | **0.40** | m | 走廊宽度 |
| `PARAM_FRONT_LASER_OFFSET` | **0.0** | m | 前激光到车头前缘的内缩距离 |
| `PARAM_REAR_LASER_OFFSET` | **0.0** | m | 后激光到车尾后缘的内缩距离 |
| `PARAM_VL53_SIDE_INSET` | **0.0** | m | 侧向 VL53 到车体外壳的内缩距离 |
| `PARAM_ENCODER_CPR` | **500** | — | 编码器每转脉冲数 |
### P2 — EKF 滤波器
| 参数名 | 当前值 | 说明 |
|--------|--------|------|
| `PARAM_EKF_Q_EY` | **0.01** | 横向过程噪声 (越大越信观测) |
| `PARAM_EKF_Q_ETH` | **0.001** | 航向过程噪声 |
| `PARAM_EKF_Q_S` | **0.1** | 里程过程噪声 |
| `PARAM_EKF_R_EY` | **0.002** | 横向观测噪声 (越大越不信雷达) |
| `PARAM_EKF_R_ETH` | **0.001** | 航向观测噪声 (侧墙) |
| `PARAM_EKF_R_ETH_IMU` | **0.01** | 航向观测噪声 (IMU yaw),远大于侧墙,用于长时约束 |
| `PARAM_EKF_P0_EY` | **0.1** | e_y 初始不确定度 |
| `PARAM_EKF_P0_ETH` | **0.1** | e_th 初始不确定度 |
### P3 — 控制器
| 参数名 | 当前值 | 单位 | 说明 |
|--------|--------|------|------|
| `PARAM_CTRL_KP_THETA` | **2.0** | — | 航向比例增益 |
| `PARAM_CTRL_KD_THETA` | **0.1** | — | 航向微分增益 |
| `PARAM_CTRL_KP_Y` | **3.0** | — | 横向比例增益 |
| `PARAM_CTRL_V_CRUISE` | **0.15** | m/s | 巡航速度 |
| `PARAM_CTRL_W_MAX` | **1.5** | rad/s | 最大角速度 |
| `PARAM_CTRL_V_MAX` | **0.3** | m/s | 最大线速度 |
### P4 — 安全与脚本
| 参数名 | 当前值 | 单位 | 说明 |
|--------|--------|------|------|
| `PARAM_SAFE_D_FRONT_STOP` | **0.08** | m | 前向强制停车距离 |
| `PARAM_SAFE_D_FRONT_APPROACH` | **0.25** | m | 前向减速预警距离 |
| `PARAM_SAFE_APPROACH_MIN_V` | **0.05** | m/s | 减速区最低速度 |
| `PARAM_SAFE_CONF_ESTOP` | **0.10** | — | E-Stop 置信度阈值 |
| `PARAM_SCRIPT_ENTRY_TIMEOUT` | **30000** | ms | 入口对准超时 |
| `PARAM_SCRIPT_TURN_OMEGA` | **1.0** | rad/s | 转向角速度 |
| `PARAM_SCRIPT_EXIT_RUNOUT` | **2.0** | m | 退出场地后冲距离 |
### P5 — 传感器驱动
| 参数名 | 当前值 | 说明 |
|--------|--------|------|
| `PARAM_VL53_TIMING_BUDGET` | **100000** | VL53L0X 测距预算 (μs) |
| `PARAM_VL53_KALMAN_Q` | **10.0** | VL53 卡尔曼过程噪声 |
| `PARAM_VL53_KALMAN_R` | **14.1** | VL53 卡尔曼观测噪声 |
| `PARAM_IMU_YAW_OFFSET` | **0.0** | IMU 零位偏置 (rad) |
---
## 14. 已知问题与待办
### 严重 (已全部修复 ✅)
| # | 问题 | 状态 | 修复位置 |
|---|------|------|----------|
| **BUG-1** | IMU 输出 `wz` 单位是 **°/s**,但 EKF 预测步按 **rad/s** 使用,导致航向积分放大 57.3 倍 | ✅ 已修复 | `app_tasks.c:305` — 加了 `PARAM_DEG2RAD()` 转换 |
| **BUG-2** | `corridor_filter.c` 中 EKF 的 Q/R/P0 参数全部硬编码,修改 `robot_params.h` 中的 `PARAM_EKF_*` 不生效 | ✅ 已修复 | `corridor_filter.c:40-56` — 改为读取 `PARAM_EKF_*` 宏 |
| **BUG-3** | `AppTasks_Init()` 只调用 `SegFsm_Init()`,未调用 `SegFsm_Start()`,导致安全状态机长期处于 IDLE输出始终为零速 | ✅ 已修复 | `app_tasks.c:382` — 补加 `SegFsm_Start()` |
| **BUG-4** | `CORRIDOR_BACKWARD` 段转 180° 后正向行驶,但到端检测错误使用 `d_back`(后向雷达),应为 `d_front` | ✅ 已修复 | `nav_script.c:CORRIDOR_BACKWARD` — 改为检查 `d_front` |
| **BUG-5** | `ENTRY_ALIGN` 段超时保护硬编码了 `30000``entry_align_timeout` 参数实际不生效 | ✅ 已修复 | `nav_script.c:155` — 改为 `s_cfg.entry_align_timeout` |
| **BUG-6** | `EXIT` 段退出直线速度误用角速度参数 `turn_omega * 0.5f`,调转向速度会意外影响退出速度 | ✅ 已修复 | `nav_script.h` 加 `exit_v` 字段;`nav_script.c:262` 读取它;`app_tasks.c` 传入 `PARAM_SCRIPT_EXIT_V``robot_params.h` 加宏 |
| **BUG-7** | EKF 卡尔曼增益计算 `K = P·H^T·S⁻¹` 只乘 `S_inv` 对角项,忽略观测间相关性,增益不正确 | ✅ 已修复 | `corridor_ekf.c:576-607` — 改为完整两步矩阵乘法:先算 `PHT = P·H^T`,再算 `K = PHT·S_inv` |
| **BUG-8** | `0x200 Odom Delta` 按"状态型最新值"消费,实为"事件型增量数据"ISR 直接覆盖旧帧导致**漏积分**60ms帧率 vs 100ms消费必然丢帧monitorTask 无条件调用 `Odom_Update()` 导致**重复积分**(无新帧时同一份 delta 被积分两次) | ✅ 已修复 | `snc_can_app.h` 新增 `SNC_OdomDeltaAccum_t``snc_can_app.c` ISR 改为累加并实现 `SNC_CAN_ConsumeOdomDelta()` 原子取走;`app_tasks.c:monitorTask` 改用新 API仅在有新帧时调用积分 |
| **BUG-9** | IMU `yaw` 输出范围 [-180°, +180°),跨界时跳变,且项目完全未使用 `yaw`180° 转弯判定纯依赖 EKF `e_th` (无侧墙观测时靠 wz 积分漂移) | ✅ 已修复 | `hwt101.c` 新增 yaw unwrap → `yaw_continuous``robot_blackboard.h` 新增 `imu_yaw_continuous``nav_script.c` 转弯判定改用 IMU 连续 yaw 差值;`corridor_ekf.c` 新增 `CorridorEKF_UpdateIMUYaw()` 1DOF 标量观测更新;`corridor_filter.c` 管理 yaw_ref 参考值并在侧墙更新后注入 IMU 航向观测 |
### 需要实车标定
| # | 问题 | 说明 |
|---|------|------|
| **CAL-1** | `PARAM_FRONT_LASER_OFFSET = 0.0` | 如果传感器在车体内部,停车距离会比预期偏大 |
| **CAL-2** | `PARAM_REAR_LASER_OFFSET = 0.0` | 同上 |
| **CAL-3** | `PARAM_VL53_SIDE_INSET = 0.0` | 如果传感器内缩,单侧退化时会有系统偏差 |
| **CAL-4** | `PARAM_IMU_YAW_OFFSET = 0.0` | 声明了但代码中未使用 |
### 设计风险 / 尚未处理
| # | 问题 | 当前影响 | 建议方向 |
|---|------|----------|----------|
| **RISK-1** | `TURN_AT_END` 原地转向阶段与 `SegFsm` 前向防撞逻辑存在潜在冲突 | `nav_script` 在到端后会输出 `v=0, w!=0` 的原地转向命令;但 `segment_fsm` 仍按“前向距离过近 → STOP”处理可能把转向角速度也清零导致机器人到端后想转却被安全层按住 | 后续应为安全层引入“动作语义”或“模式感知”,区分走廊前进与原地转向;转向阶段允许 `v=0` 时保留受限 `w`,而不是沿用普通前向防撞全停策略 |
补充说明:
- 该问题的根因不是传感器噪声,而是 **脚本层与安全层的仲裁语义不一致**
- 侧向 VL53 在端部转向时本来也会偏离正常走廊几何,因此当前已经用 IMU `yaw_continuous` 来判定转角,这部分方向是对的
- 真正需要补的是:**转向阶段的专用安全策略**,否则后续扩展到多垄沟遍历时,端部动作会成为卡点
### 代码质量 (已全部修复 ✅)
| # | 问题 | 状态 | 修复位置 |
|---|------|------|----------|
| **Q-1** | `PARAM_CTRL_SPEED_REDUCTION` 未被控制器读取,硬编码了 `0.4f` | ✅ 已修复 | `corridor_ctrl.h` 加字段 `speed_reduction_k``corridor_ctrl.c:59` 读取它;`app_tasks.c` 传入 `PARAM_CTRL_SPEED_REDUCTION` |
| **Q-2** | `PARAM_SCRIPT_ENTRY_V` 未被脚本读取,硬编码了 `0.08f` | ✅ 已修复 | `nav_script.h` 加字段 `entry_align_v``nav_script.c:146` 读取它;`app_tasks.c` 传入 `PARAM_SCRIPT_ENTRY_V` |
| **Q-3** | `PARAM_SCRIPT_EXIT_RUNOUT` 未被脚本读取,硬编码了 `2.0f` | ✅ 已修复 | `nav_script.h` 加字段 `exit_runout_m``nav_script.c:275` 读取它;`app_tasks.c` 传入 `PARAM_SCRIPT_EXIT_RUNOUT` |
| **Q-4** | `PARAM_VL53_TIMING_BUDGET` 未被使用,直接传字面量 `100000` | ✅ 已修复 | `app_tasks.c:222,232` 改为传 `PARAM_VL53_TIMING_BUDGET` |
| **Q-5** | `exit_start_s` 是函数内 `static` 局部变量,`NavScript_Reset()` 无法清除它 | ✅ 已修复 | 移入 `s_internal` 结构体,`memset(&s_internal, 0, ...)` 统一清零 |
| **Q-6** | 转向角度通过 EKF 的 `e_th` 差值测量,不是绝对角度。如果 EKF 发散或走廊参照丢失,转向判断可能不准 | ✅ 已修复 | `nav_script.c` — 改用 IMU `yaw_continuous` (unwrap 后的连续偏航角) 判定转角,不再依赖 EKF `e_th` |
| **Q-7** | `g_snc_can_app` 被中断写、被任务读,没有互斥保护 (中等风险) | ✅ 部分修复 | `odom_accum` 的取走/清零已通过 `taskENTER_CRITICAL` 保护BUG-8 修复其余字段status/rpm/diag仍无保护但读写均为原子宽度或单次赋值风险可接受 |
---
## 15. 构建与烧录
### 构建
```bash
# 确保 gcc-arm-none-eabi 在 PATH 中
cmake --preset Debug
cmake --build build/Debug
```
- 输出: `build/Debug/ARES.elf`
- 工具链: `cmake/gcc-arm-none-eabi.cmake`
- C 标准: C11
- 浮点 printf: 已开启 (`-u _printf_float`)
### 烧录
使用 **STM32CubeProgrammer** 或 **OpenOCD** 烧录 `.elf` 文件到 STM32H743。
### 注意事项
- `CMakeLists.txt` 第 38 行有一个大写 `.C` 后缀 (`laser_manager.C`),在大小写敏感的文件系统上可能会构建失败
- MPU Region 1 配置了 `0x30000000` 32KB 为非缓存区,所有 DMA 缓冲区必须放在此区域 (`.dma_buffer` section)
- USB CDC 用于 `printf` 输出 (通过 `retarget.c`)
---
## 16. 实车调试流程
### 建议顺序
```
P0: 几何参数 → 用卷尺实测,填入 robot_params.h
P1: 里程计标定 → 直线跑 10m对比编码器累积距离
P2: EKF 调优 → 从保守 Q/R 开始,观察走廊跟踪稳定性
P3: 控制器调参 → 先调 kp_theta/kd_theta (航向),再调 kp_y (横向)
P4: 安全阈值 → 根据实际场地微调停车/减速距离
```
### 常见问题诊断
| 现象 | 可能原因 | 调整方法 |
|------|----------|----------|
| 车头左右摆动 | kp_theta 过大 或 kd_theta 过小 | 减小 kp_theta 或增大 kd_theta |
| 横向纠偏太慢 | kp_y 过小 | 增大 kp_y |
| 到端刹不住 | d_front_stop 过小 或 approach 区间太短 | 增大 PARAM_SAFE_D_FRONT_STOP |
| 总是急停 | EKF 置信度低,可能是 VL53 数据不稳定 | 增大 R (降低观测信任) 或检查传感器接线 |
| EKF 发散 | Q 过小 (不信观测) 或 R 过小 (过信噪声数据) | 增大 Q 或增大 R |
| 单侧靠墙走 | PARAM_VL53_SIDE_INSET 未校准 | 实测传感器内缩距离并填入 |
---
## 17. 文件快速索引
| 你想做什么 | 去看哪个文件 |
|-----------|-------------|
| 改任何调参数值 | `App/robot_params.h` |
| 理解系统初始化 | `App/app_tasks.c` → `AppTasks_Init()` |
| 理解导航流水线 | `App/app_tasks.c` → `AppTasks_RunNavTask_Impl()` |
| 看 EKF 数学 | `App/est/corridor_ekf.c` |
| 看控制律 | `App/nav/corridor_ctrl.c` |
| 看安全逻辑 | `App/nav/segment_fsm.c` |
| 看比赛编排 | `App/nav/nav_script.c` |
| 看传感器预处理 | `App/preproc/corridor_preproc.c` |
| 看 CAN 协议 | `App/Can/snc_can_app.c` + `App/Contract/chassis_can_msg.h` |
| 看全局数据结构 | `App/Contract/robot_blackboard.c/.h` |
| 看里程计 | `App/Contract/robot_odom.c` |
| 看激光驱动 | `App/laser/laser_manager.c` |
| 看 VL53 驱动 | `App/VL53L0X_API/platform/vl53_board.c` |
| 看 IMU 驱动 | `App/IMU/hwt101.c` |
| 看消息结构定义 | `App/preproc/corridor_msgs.h` |
---
> **给接手者的最后提醒**:
> 1. **先修 BUG-1** (IMU 单位转换),否则 EKF 完全不可用
> 2. **再修 BUG-2** (EKF 参数硬编码),否则调参改了白改
> 3. 拿到实车后第一件事:用卷尺量 CAL-1 ~ CAL-3 的三个偏移量
> 4. CAN 协议层 (`snc_can_app.c`) 已冻结,**不要修改**
> 5. IMU `yaw_continuous` 已接入转弯判定,实车调试时对比 `wz` 积分与 `yaw` 哪个更稳,参见 `IMU_YAW_WZ_ISSUE.md`