1030 lines
54 KiB
Markdown
1030 lines
54 KiB
Markdown
# 视觉跟踪控制律(visual_track_control)技术路线
|
||
|
||
> 本文档把 `visual_track_control/` 子项目已实现的控制律抽象为**与编程语言无关**的
|
||
> 技术路线:只讲逻辑、数据结构、算法与实现方式,不依赖 C++ 语法特性。
|
||
> 可作为任意语言(C++/Rust/Python 仿真/嵌入式 C)重新实现或移植的规格说明。
|
||
>
|
||
> **文档关系**(三份文档各司其职,不重复抄录):
|
||
>
|
||
> | 文档 | 定位 | 权威性 |
|
||
> |------|------|--------|
|
||
> | `docs/物理追踪思路.md` | 控制律**物理与行为规格**(为什么这样控) | 数学与行为的唯一权威 |
|
||
> | `docs/视觉跟踪控制律技术路线.md`(本文) | **语言无关实现路线**(逻辑怎么落地) | 实现结构与算法流程的权威 |
|
||
> | `visual_track_control/视觉跟踪控制律实现文档.md` | **C++ 实现说明**(构建、日志、排查) | 当前 C++ 落地的权威 |
|
||
>
|
||
> 文中出现的字段名(如 `track_time_ms`、`yaw_rate_dps`)取自当前实现,仅作为
|
||
> **跨文档可追溯的标识符**,不构成语言绑定;重写时可自由改名,但语义与单位必须一致。
|
||
>
|
||
> ⚠️ **全部数值均为占位值**:相机内参/畸变未标定、增益与 PID 未整定。标定完成前
|
||
> 本路线**仅限回放/SITL 验证数学链路,不得用于实飞**(见第 11 章红线)。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [概述与系统定位](#1-概述与系统定位)
|
||
2. [输入输出接口定义](#2-输入输出接口定义)
|
||
3. [核心构件与状态清单](#3-核心构件与状态清单)
|
||
4. [坐标系与符号约定](#4-坐标系与符号约定)
|
||
5. [单周期控制流程(主流程伪代码)](#5-单周期控制流程主流程伪代码)
|
||
6. [各通道算法细节](#6-各通道算法细节)
|
||
7. [安全语义:超龄、判丢、Coast、无距离](#7-安全语义超龄判丢coast无距离)
|
||
8. [输出打包与链路](#8-输出打包与链路)
|
||
9. [配置参数全量表](#9-配置参数全量表)
|
||
10. [设计取舍记录(为什么不是别的做法)](#10-设计取舍记录为什么不是别的做法)
|
||
11. [实现与重写指引](#11-实现与重写指引)
|
||
12. [验证路线与待定项](#12-验证路线与待定项)
|
||
|
||
---
|
||
|
||
## 1. 概述与系统定位
|
||
|
||
### 1.1 模块职责
|
||
|
||
本模块是**视觉跟踪阶段(状态机 S7 VISUAL_TRACKING)的单周期控制律**,职责可归纳为
|
||
"三算一限一降级一打包":
|
||
|
||
1. **三路控制律计算**(每拍一次,固定周期)
|
||
- **水平航向**:目标像素偏移 → 水平视线角 β → 增益 → 偏航指令;
|
||
- **垂直高度**:目标像素偏移 → 垂直视线角 α → 垂直速度指令;
|
||
- **距离**:雷达目标距离与期望射程之差 Δd → PID → 前向速度指令。
|
||
2. **统一限幅**:每个通道内联完成"变化率限幅 + 幅值限幅",任何输出都不允许
|
||
跳过限幅直达链路(含切换后的**首条**指令)。
|
||
3. **安全降级**:输入超龄、目标判丢、距离不可用时,输出降级意图而不是继续
|
||
用旧数据算正常指令。
|
||
4. **单出口打包**:控制意图 → MAVLink 字节流 → 注入的传输实现;控制律本身
|
||
不认识串口、电台或飞控。
|
||
|
||
### 1.2 在系统中的位置
|
||
|
||
```text
|
||
┌─────────────┐ 检测框 ┌──────────────┐ 追踪结果 ┌───────────────────┐
|
||
│ YOLO 识别 │────────────▶│ TargetTracker│───────────▶│ 本模块:控制律 │
|
||
│ (感知线程)│ 每帧 │ (滤波器) │ 主目标+ │ TrackingControlLaw│
|
||
└─────────────┘ └──────────────┘ Coast标志 └─────────┬─────────┘
|
||
│
|
||
┌─────────────┐ 前向距离 │ 控制意图
|
||
│ 激光测距雷达│──────────────────────┐ ▼
|
||
└─────────────┘ │ ┌──────────────────────┐
|
||
┌─────────────┐ 姿态/航向/速度 ├─────────────▶│ 打包网关 │
|
||
│ PX4 链路 │──────────────────────┘ 控制快照 │ (控制意图→MAVLink) │
|
||
└─────────────┘ (由调用方采集打戳) └─────────┬────────────┘
|
||
▲ │ 字节流
|
||
│ MAVLink offboard 指令流 ▼
|
||
└────────────────────────────────────────────── ┌──────────────────┐
|
||
│ 传输抽象实现 │
|
||
│ (串口/电台/回环)│
|
||
└──────────────────┘
|
||
```
|
||
|
||
**上游依赖**:追踪库提供主目标中心与 `tracked` / `is_predicted` 两个行为标志
|
||
(追踪库技术路线见 `docs/视觉追踪技术路线.md`);雷达提供机头前向单点距离与
|
||
**关联标志**;飞控链路提供自机姿态(至少航向角)。
|
||
|
||
**下游依赖**:只有一个——传输抽象。打包与传输分离,控制律不直接写飞控。
|
||
|
||
**上层归属**:状态机(S6/S7/S8 切换、目标丢失策略、障碍接管)在**本模块之外**。
|
||
本模块只按输入标志产生"当拍应该做什么",不决定"要不要继续视觉闭环"。
|
||
|
||
### 1.3 设计特点
|
||
|
||
| 特点 | 含义 | 带来的约束 |
|
||
|------|------|-----------|
|
||
| **纯值输入 / 纯值输出** | 每拍传入一个不可变快照,返回一个输出值对象 | 无隐藏输入、无回调、可离线回放 |
|
||
| **单线程固定周期** | 由控制线程按配置频率调用,构件内部**无锁** | 不得从多线程并发调用同一实例 |
|
||
| **热路径零动态分配** | 所有系数、缓冲在构造期备妥;每拍只做算术 | 便于实时性分析与嵌入式移植 |
|
||
| **热路径零日志** | 仅在输出模式发生切换时打一条日志 | 日志量与状态变化次数成正比,不与频率成正比 |
|
||
| **配置全量外置** | 所有阈值/增益/限幅来自启动期加载的配置对象 | 代码内**不得**出现任何写死的控制参数 |
|
||
| **无状态纯函数构件** | 视线角转换、超龄判定、坐标旋转均为纯函数 | 可独立单元测试,无需夹具 |
|
||
| **单一 offboard 消息类型** | 全周期只发一种消息(ID 84) | 避免消息类型逐拍翻转账扰动飞控(见 §10.1) |
|
||
|
||
### 1.4 明确不做的事(边界)
|
||
|
||
- **不做感知**:不跑检测/光流/雷达驱动,只消费调用方填入的快照;
|
||
- **不做目标关联**:相机—雷达空间关联由感知融合层给出布尔标志,本模块只读取;
|
||
- **不做状态机仲裁**:不判断何时进入/退出视觉跟踪,不实现目标丢失策略;
|
||
- **不做链路 IO**:不打开串口、不管理重连、不解析任何入向 MAVLink 消息;
|
||
- **不做配置热更新**:启动期加载一次,调参重启生效;
|
||
- **不做多目标**:只处理追踪库选定的主目标。
|
||
|
||
---
|
||
|
||
## 2. 输入输出接口定义
|
||
|
||
### 2.1 输入:控制快照(每拍一个,由调用方采集并打戳)
|
||
|
||
| 分组 | 字段 | 单位 / 类型 | 语义与约束 |
|
||
|------|------|------------|-----------|
|
||
| 视觉 | `track.center` | 像素 (x, y) | 主目标中心;原点为画面**左上角**,x 右正、y 下正 |
|
||
| 视觉 | `track.tracked` | 布尔 | 是否存在有效轨道;`false` → 判丢刹车 |
|
||
| 视觉 | `track.is_predicted` | 布尔 | 本拍是否为滤波器预测值(Coast);`true` → 清距离积分 |
|
||
| 视觉 | `track_time_ms` | 单调时钟 ms | **取样瞬间**打戳;`<= 0` 视为从未取样,直接判超龄 |
|
||
| 雷达 | `radar_distance_m` | m | 机头前向单点距离;`<= 0` 视为不可用 |
|
||
| 雷达 | `associated_to_target` | 布尔 | 相机—雷达关联成立标志;**未关联不得当作目标距离** |
|
||
| 雷达 | `radar_time_ms` | 单调时钟 ms | 雷达取样瞬间打戳 |
|
||
| 姿态 | `attitude_present` | 布尔 | 姿态源当前是否可用;`false` → 禁止控制 |
|
||
| 姿态 | `attitude_time_ms` | 单调时钟 ms | 姿态取样瞬间打戳 |
|
||
| 姿态 | `yaw_rad` | rad (NED) | 自机航向;**参与计算**:位置式航向合成 + 输出端坐标旋转 |
|
||
| 姿态 | `roll_rad` / `pitch_rad` | rad (FRD) | 预留(机体补偿),当前不参与计算 |
|
||
| 姿态 | `yaw_rate_radps` | rad/s | 预留(机体补偿),当前不参与计算 |
|
||
| 自机速度 | `velocity_x/y/z_mps` | m/s | 预留,接入主工程后由飞控状态填充 |
|
||
|
||
**关键约定:追踪结果自身不带时间戳。** 追踪库输出的是"最新一帧的结果",
|
||
调用方必须在读取结果的**那一瞬间**用单调时钟打戳,写入 `track_time_ms`。
|
||
所有超龄判定一律采用"取样时刻 + 有效期"模型(§7.1),不使用"消息序号"或
|
||
"到达时刻"。
|
||
|
||
**时钟要求:单一单调时钟源。** 快照内三类时间戳与 `Update()` 的 `now_ms`
|
||
必须来自同一个不受墙钟调整影响的时钟(如系统启动后毫秒数)。混用墙钟会
|
||
导致 `now < sample` 的不可核实情形,实现按"宁可降级"处理(§7.1)。
|
||
|
||
### 2.2 输出:控制意图(每拍一个,已限幅)
|
||
|
||
| 字段 | 单位 | 语义 |
|
||
|------|------|------|
|
||
| `mode` | 枚举 | `kVelocityHeading` / `kBrakeHover` / `kDegradedHold` |
|
||
| `valid` | 布尔 | 是否可交付链路发送;`kDegradedHold` 恒为 `false` |
|
||
| `vx_mps` | m/s | **局部 NED 北向**速度(机体系前向经航向旋转得到) |
|
||
| `vy_mps` | m/s | **局部 NED 东向**速度(机体系侧向恒 0,旋转后可非零) |
|
||
| `vz_mps` | m/s | **NED 下正**垂直速度 |
|
||
| `yaw_deg` | deg (NED) | **绝对**目标航向,回绕到 [-180, 180);仅位置式模式非零 |
|
||
| `yaw_rate_dps` | deg/s | 偏航角速度指令(相对量);仅速率式模式非零 |
|
||
| `degraded_reason` | 枚举 | `kNone` / `kAttitudeStale` / `kVisionStale` / `kNoDistanceExit` |
|
||
| `coast_active` | 布尔 | 本拍处于 Coast 预测跟踪 |
|
||
|
||
**输出坐标系已在库内完成旋转**:速度分量对外一律是局部 NED(北/东/地),
|
||
与打包时声明的坐标框一致。调用方**不得**再做二次旋转。
|
||
|
||
### 2.3 输出模式与可发送性矩阵
|
||
|
||
| 模式 | `valid` | 速度分量 | 航向字段 | 链路动作 |
|
||
|------|---------|---------|---------|---------|
|
||
| `kVelocityHeading` | `true` | 三路限幅后旋转值 | 按配置的航向模式**二选一**非零 | 发送速度+航向指令 |
|
||
| `kBrakeHover` | `true` | 全零 | 全零(掩码忽略 yaw 与 yaw_rate) | 发送零速刹车 |
|
||
| `kDegradedHold` | `false` | 全零 | 全零 | **不发送**,仅告警/记录降级原因 |
|
||
|
||
### 2.4 传输抽象契约(唯一出向依赖)
|
||
|
||
```text
|
||
接口 CommandTransport:
|
||
Send(data: 字节序列, len: 长度) -> 布尔
|
||
返回 true = 已交付底层链路
|
||
返回 false = 发送失败(调用方可计数/告警,控制律不重试)
|
||
```
|
||
|
||
契约要点:
|
||
|
||
- **只吃字节流**:串口、电台、回环打印、文件录制均可注入实现;换传输层
|
||
不改动打包与控制律;
|
||
- **同步交付**:实现方不得长期持有传入的字节指针(同步拷贝或同步写出),
|
||
因此打包侧可以复用同一块固定缓冲;
|
||
- **无反向通道**:本模块不接收任何入向消息,传输实现无需提供接收能力。
|
||
|
||
---
|
||
|
||
## 3. 核心构件与状态清单
|
||
|
||
控制律由 1 个主控律 + 6 个构件组成。下表列出**每个构件需要持久化的状态**,
|
||
这是重写/移植时最容易漏掉的部分——状态漏了,行为就变了。
|
||
|
||
| 构件 | 职责 | 构造期预计算 | 运行期持久状态 | 复位入口 |
|
||
|------|------|-------------|--------------|---------|
|
||
| 主控律 | 八步流程编排 | 各构件实例、配置值拷贝 | 上拍时刻 `last_now_ms`、上拍模式 `last_mode` | `Reset()` 清全部 |
|
||
| 视线角转换器 ×2 | 像素偏移 → β / α | 针孔系数 `2·tan(θ/2)/size` | 无(纯函数) | 不需要 |
|
||
| 距离 PID | Δd → 前向速度 | PID 参数、输出限幅 | 积分累计、上拍误差、低通后微分值、上次输出、"是否存在有效微分历史"标志 | `ResetIntegral()` 只清积分;`Reset()` 清全部 |
|
||
| 变化率限幅器 ×4 | 限制每拍变化量 | 变化率上限 | 上次输出值 | `Reset(value)` |
|
||
| 静态限幅 | 幅值钳制 | 无 | 无(纯函数) | 不需要 |
|
||
| 超龄判定器 | 输入是否过期 | 无 | 无(纯函数) | 不需要 |
|
||
| 坐标旋转 | 机体系 → NED | 无 | 无(纯函数) | 不需要 |
|
||
|
||
**四个变化率限幅器分别守护什么**:
|
||
|
||
| 限幅器 | 守护量 | 单位 | 生效通道 |
|
||
|--------|-------|------|---------|
|
||
| 航向角限幅器 | 位置式航向修正量的变化率 | deg/s(见 §12.2 量纲待澄清项) | 水平航向(位置式) |
|
||
| 航向角速率限幅器 | 偏航角速度指令的变化率 | deg/s² | 水平航向(速率式,默认) |
|
||
| 前向速度限幅器 | 前向加速度 | m/s² | 距离通道 |
|
||
| 垂直速度限幅器 | 垂直加速度 | m/s² | 垂直通道 |
|
||
|
||
**为什么正好是 4 个**:机体系侧向速度恒为 0(本控制律不做侧移),因此
|
||
`ay_mps2` 配置项当前为预留、无对应限幅器实例。
|
||
|
||
---
|
||
|
||
## 4. 坐标系与符号约定
|
||
|
||
### 4.1 三个坐标系
|
||
|
||
| 坐标系 | 轴定义 | 用途 |
|
||
|--------|-------|------|
|
||
| 像素系 | 原点左上角,x 右正、y 下正 | 追踪库输出的原始语义 |
|
||
| 机体系 (FRD) | x 前、y 右、z 下 | **控制律内部计算**(三路通道各自算出机体量) |
|
||
| 局部 NED | x 北、y 东、z 地 | **对外输出与打包声明** |
|
||
|
||
### 4.2 像素偏移与视线角符号
|
||
|
||
```text
|
||
x_offset = center.x − W/2 # 目标偏右 → 正
|
||
y_offset = center.y − H/2 # 目标偏下 → 正
|
||
|
||
β = 水平视线角(弧度):与 x_offset 同号 # 目标偏右 → β 正 → 需右转
|
||
α = 垂直视线角(弧度):与 y_offset 同号 # 目标偏下 → α 正
|
||
```
|
||
|
||
**垂直符号链**(NED 下正约定):目标偏低 → α 为正 → 期望向下运动 → NED 垂直
|
||
速度 `vz` 为正(下正)。因此垂直通道**不需要额外取反**,增益为正值即可。
|
||
|
||
**水平符号链**(速率式):目标偏右 → β 为正 → 偏航角速度指令为正。按 NED
|
||
航向"自北顺时针(右转为正)"约定,正角速度即右转,趋近目标。因此水平通道
|
||
同样不需要额外取反。
|
||
|
||
### 4.3 机体系 → NED 旋转(只随航向角)
|
||
|
||
```text
|
||
给定机体速度 (v_fwd, v_right, v_down) 与自机航向 ψ(rad,自北顺时针为正):
|
||
|
||
v_north = v_fwd·cos ψ − v_right·sin ψ
|
||
v_east = v_fwd·sin ψ + v_right·cos ψ
|
||
v_down = v_down # 绕下轴旋转不影响垂直分量
|
||
```
|
||
|
||
**为什么只随航向旋转、不含滚转/俯仰**:速度设定值的姿态耦合项由飞控内环
|
||
处理,这是 offboard 速度设定值的标准做法;滚转/俯仰对水平速度指令是二阶小量。
|
||
|
||
**正方向自检**(实现时必须通过的两条断言):
|
||
|
||
| 输入 | ψ | 期望输出 | 说明 |
|
||
|------|---|---------|------|
|
||
| 机体前向 (1, 0, 0) | 90°(机头朝东) | (0, 1, 0) = 东 | 前向随航向转向东 ✓ |
|
||
| 机体右向 (0, 1, 0) | 0°(机头朝北) | (0, 1, 0) = 东 | 机体右侧即地理东 ✓ |
|
||
|
||
### 4.4 限幅与旋转的先后顺序(正确性论证)
|
||
|
||
**顺序:限幅在机体系完成 → 旋转在输出端执行。**
|
||
|
||
论证:绕下轴旋转是**正交变换**,保持向量范数不变,即
|
||
`√(v_north² + v_east²) = √(v_fwd² + v_right²)`。因此机体系内满足的幅值约束
|
||
在旋转后仍然成立,先限幅后旋转**不破坏限幅语义**;反过来(先旋转后限幅)
|
||
则会让限幅作用在地理轴上,与"机体能力限制"的物理含义不符。
|
||
|
||
推论:输出的 `vy_mps` **可以非零**(机体系侧向恒 0,旋转后东向分量非零),
|
||
这不是缺陷,调用方不得把它当作异常。
|
||
|
||
### 4.5 姿态不可用时的处理
|
||
|
||
旋转与位置式航向合成都依赖 `yaw_rad`。因此**姿态过期/缺失必须在任何坐标
|
||
运算之前拦截**(流程第 ① 步),直接进入降级;不允许"用上一拍航向凑合旋转",
|
||
因为那会产生坐标系标注与实际不符的输出(规格明令禁止混用未标注坐标系数据)。
|
||
|
||
---
|
||
|
||
## 5. 单周期控制流程(主流程伪代码)
|
||
|
||
### 5.1 节拍推算
|
||
|
||
```text
|
||
函数 计算dt(now_ms) -> 秒:
|
||
dt ← 1.0 / 配置.control.frequency_hz # 默认:配置频率兜底
|
||
若 last_now_ms > 0 且 now_ms > last_now_ms: # 时钟正常前进才信任差值
|
||
dt ← min( (now_ms − last_now_ms) / 1000.0, DT_MAX )
|
||
last_now_ms ← now_ms
|
||
返回 dt
|
||
|
||
常量 DT_MAX ← 0.5 秒 # 异常长间隔上限
|
||
```
|
||
|
||
**三条兜底规则的动机**:
|
||
|
||
| 情形 | 处理 | 动机 |
|
||
|------|------|------|
|
||
| 首拍(`last_now_ms == 0`) | 用配置频率推算 dt | 无上拍参考,不允许 dt=0 触发除零 |
|
||
| 时钟回拨或同值(`now <= last`) | 用配置频率推算 dt | 差值不可信;不引入额外状态去纠错 |
|
||
| 间隔异常长(`> 0.5 s`) | 截断到 0.5 s | 防止限幅器一步放过巨大跳变 |
|
||
|
||
### 5.2 八步主流程
|
||
|
||
```text
|
||
函数 Update(快照, now_ms) -> 控制意图:
|
||
dt ← 计算dt(now_ms)
|
||
输出 ← 新建控制意图()
|
||
输出.mode ← kVelocityHeading # 正常路径默认;分支可覆盖为降级/刹车
|
||
|
||
# ---- ① 超龄判定(最高优先级:数据不可信就不算) ----
|
||
若 (非 快照.attitude_present) 或 已超龄(快照.attitude_time_ms, now_ms, 配置.attitude_stale_ms):
|
||
输出.mode ← kDegradedHold; 输出.degraded_reason ← kAttitudeStale
|
||
返回 收尾并记录模式切换(输出, "姿态过期/缺失,禁止控制")
|
||
|
||
若 已超龄(快照.track_time_ms, now_ms, 配置.visual_stale_ms):
|
||
输出.mode ← kDegradedHold; 输出.degraded_reason ← kVisionStale
|
||
返回 收尾并记录模式切换(输出, "视觉追踪结果超龄")
|
||
|
||
# ---- ② 判丢 → 零速刹车 ----
|
||
若 非 快照.track.tracked:
|
||
若 last_mode ≠ kBrakeHover: # 仅在"进入沿"清状态
|
||
距离PID.全清(); 四个限幅器.复位(0)
|
||
输出.mode ← kBrakeHover; 输出.valid ← true # 速度/航向字段保持全零
|
||
返回 收尾并记录模式切换(输出, "判丢,零速刹车")
|
||
|
||
# ---- ③ Coast → 清距离积分 ----
|
||
若 快照.track.is_predicted:
|
||
距离PID.清积分() # 只清积分,保留微分历史
|
||
输出.coast_active ← true
|
||
# 航向通道为纯比例映射,无积分项,无需清理
|
||
|
||
# ---- ④ 像素偏移 → 视线角 ----
|
||
x_offset ← 快照.track.center.x − 配置.image_width / 2
|
||
y_offset ← 快照.track.center.y − 配置.image_height / 2
|
||
β_deg ← 水平视线角转换器(x_offset) × 弧度转度 # 针孔模型,见 §6.1
|
||
α_deg ← 垂直视线角转换器(y_offset) × 弧度转度
|
||
|
||
# ---- ⑤ 水平航向通道 ----
|
||
若 配置.heading.mode == 速率式:
|
||
目标角速率 ← 配置.heading.gain × β_deg
|
||
限速后 ← 航向角速率限幅器.推进(目标角速率, dt) # 先变化率
|
||
输出.yaw_rate_dps ← 钳制(限速后, ±配置.yaw_rate_limit_dps) # 再幅值
|
||
否则: # 位置式
|
||
目标修正量 ← 配置.heading.gain × β_deg
|
||
修正量 ← 航向角限幅器.推进(目标修正量, dt)
|
||
# ID 84 的 yaw 字段是"绝对航向",必须与当前航向合成,不能直发修正量
|
||
输出.yaw_deg ← 回绕180( 快照.yaw_rad × 弧度转度 + 修正量 )
|
||
|
||
# ---- ⑥ 垂直速度通道(统一速度支路) ----
|
||
目标垂直速度 ← 配置.vertical.vz_gain_mps_per_deg × α_deg
|
||
限速后 ← 垂直速度限幅器.推进(目标垂直速度, dt) # 加速度限幅
|
||
输出.vz_mps ← 钳制(限速后, ±配置.vertical.vz_limit_mps)
|
||
|
||
# ---- ⑦ 距离通道 ----
|
||
距离可用 ← 快照.associated_to_target
|
||
且 快照.radar_distance_m > 0
|
||
且 非 已超龄(快照.radar_time_ms, now_ms, 配置.radar_stale_ms)
|
||
|
||
若 距离可用:
|
||
Δd ← 快照.radar_distance_m − 配置.distance.d_exp_m # 正=偏远,需接近
|
||
前向原始 ← 距离PID.更新(Δd, dt)
|
||
限速后 ← 前向速度限幅器.推进(前向原始, dt) # 加速度限幅
|
||
# 非对称限幅:接近可以快,后退必须慢
|
||
输出.vx_mps ← 钳制(限速后, −配置.retreat_velocity_limit_mps,
|
||
+配置.approach_velocity_limit_mps)
|
||
否则:
|
||
距离PID.清积分() # 防饱和
|
||
按 配置.distance.no_distance_action 分支:
|
||
保持: 输出.vx_mps ← 0; 前向速度限幅器.复位(0)
|
||
限速接近: 输出.vx_mps ← 前向速度限幅器.推进(配置.no_distance_approach_limit_mps, dt)
|
||
退出近距操作: 输出.mode ← kDegradedHold
|
||
输出.degraded_reason ← kNoDistanceExit
|
||
返回 收尾并记录模式切换(输出, "距离未关联,退出近距操作")
|
||
|
||
# ---- ⑧ 机体系 → 局部 NED 旋转(输出端) ----
|
||
旋转机体到NED(输出.vx_mps, 输出.vy_mps, 输出.vz_mps, 快照.yaw_rad)
|
||
|
||
输出.valid ← true
|
||
返回 收尾并记录模式切换(输出, 输出.coast_active ? "Coast 预测跟踪" : "正常跟踪")
|
||
```
|
||
|
||
### 5.3 提前返回的语义差异(易错点)
|
||
|
||
三条提前返回路径**跳过第 ⑧ 步旋转**,这是安全的:这些路径的速度分量全为零,
|
||
零向量旋转后仍是零向量。但重写时必须注意:
|
||
|
||
| 返回路径 | 是否清 PID | 是否清限幅器 | 速度是否旋转 | `valid` |
|
||
|---------|-----------|------------|------------|---------|
|
||
| 姿态超龄降级 | 否 | 否 | 否 | `false` |
|
||
| 视觉超龄降级 | 否 | 否 | 否 | `false` |
|
||
| 判丢刹车 | 是(进入沿) | 是(进入沿) | 否 | `true` |
|
||
| 无距离退出 | 是(仅积分) | 否 | 否 | `false` |
|
||
| 正常/Coast | 视情况清积分 | 否 | **是** | `true` |
|
||
|
||
**降级路径不清状态的理由**:降级只是"这一拍不输出",输入恢复后应当接着上次的
|
||
积分/限幅历史平滑续接;若每次降级都清状态,输入抖动会导致指令反复从零起爬。
|
||
|
||
**刹车路径必须清状态的理由**:判丢是一次真实的控制中断,恢复跟踪时的首条指令
|
||
必须从零渐变(避免位置→速度切换突变),因此积分与全部限幅器历史归零。
|
||
|
||
### 5.4 模式切换日志(唯一日志点)
|
||
|
||
```text
|
||
函数 收尾并记录模式切换(输出, 原因文本) -> 控制意图:
|
||
若 输出.mode ≠ last_mode:
|
||
记录 INFO 日志: "[ControlLaw] 状态切换: {旧模式名} -> {新模式名} ({原因文本})"
|
||
last_mode ← 输出.mode
|
||
返回 输出
|
||
```
|
||
|
||
同一模式连续多拍**不重复打印**。这是本模块唯一的运行期日志;每拍的三路通道
|
||
数学计算不打任何日志。
|
||
|
||
---
|
||
|
||
## 6. 各通道算法细节
|
||
|
||
### 6.1 像素 → 视线角:全程针孔模型
|
||
|
||
```text
|
||
构造期(水平、垂直各一个实例):
|
||
校验 size_px > 0,否则报参数错误
|
||
校验 0 < fov_rad < π,否则报参数错误
|
||
系数 ← 2 · tan(fov_rad / 2) / size_px # 预计算,热路径不再算 tan
|
||
|
||
每拍:
|
||
视线角(弧度) ← atan( offset_px × 系数 )
|
||
```
|
||
|
||
| 通道 | size | fov | 含义 |
|
||
|------|------|-----|------|
|
||
| 水平 | `image_width` (W) | `fov_h_deg` (θ_h) | 偏移 → β |
|
||
| 垂直 | `image_height` (H) | `fov_v_deg` (θ_v) | 偏移 → α |
|
||
|
||
**为什么不用线性近似**:线性式 `angle ≈ offset · θ/size` 只在小偏移成立。
|
||
若做"小偏移走线性、大偏移走针孔"的双路切换,两式在切换边界**函数值不相等**
|
||
(θ=60°、边界比例 0.2 处约 8% 角度台阶),目标跨越边界的那一拍会向控制环路
|
||
注入一次角度突变 → 限幅器被迫吸收 → 机体抽动。`atan` 的计算代价在此频率下
|
||
可忽略,故全程使用精确针孔式,配置项也随之少一个(原 `linear_ratio` 已移除)。
|
||
|
||
**未建模项(红线相关)**:镜头畸变、相机安装外参(俯仰/偏航安装角)、
|
||
像素非正方形均未补偿。标定完成前,视线角存在系统性偏差。
|
||
|
||
### 6.2 水平航向通道
|
||
|
||
**两种输出映射,二选一,由配置决定**:
|
||
|
||
| 模式 | 输出字段 | 量纲 | 优点 | 风险 |
|
||
|------|---------|------|------|------|
|
||
| **速率式(默认)** | `yaw_rate_dps` | (deg/s)/deg = 1/s | 指令天然是**相对量**,无坐标系歧义;不依赖自机航向精度 | 稳态需 β→0 才停转,存在小幅极限环可能 |
|
||
| 位置式 | `yaw_deg` | deg/deg(无量纲) | 稳态无累积漂移 | 输出是**绝对 NED 航向**,必须与当前航向合成;依赖新鲜姿态 |
|
||
|
||
**限幅顺序:先变化率、后幅值**(不可颠倒):
|
||
|
||
```text
|
||
目标 ← 增益 × β_deg
|
||
限速 ← 变化率限幅器.推进(目标, dt) # 限制"每拍能变多少"
|
||
输出 ← 钳制(限速, ±幅值上限) # 限制"最大能到多少"
|
||
```
|
||
|
||
颠倒顺序会导致:幅值限幅先切掉超限部分,变化率限幅器记录的"上次输出"变成
|
||
被切过的值,下一步以此为基准继续推进,实际变化率可超过配置上限。
|
||
|
||
**位置式必须合成绝对航向**:链路消息的 `yaw` 字段语义是绝对 NED 航向。
|
||
若直接把修正量填进去,等于命令机体"转向修正角那个方向"(例如修正量 5° 会被
|
||
理解为"转向北偏东 5°"),与当前航向无关——这是典型的坐标系语义误用。正确做法:
|
||
|
||
```text
|
||
输出.yaw_deg ← 回绕180( 当前航向(度) + 限幅后修正量 )
|
||
|
||
函数 回绕180(deg):
|
||
d ← fmod(deg + 180, 360)
|
||
若 d < 0: d ← d + 360
|
||
返回 d − 180 # 结果落在 [-180, 180)
|
||
```
|
||
|
||
### 6.3 垂直通道:统一速度支路
|
||
|
||
```text
|
||
目标垂直速度 ← vz_gain_mps_per_deg × α_deg
|
||
限速 ← 垂直速度限幅器.推进(目标垂直速度, dt) # 即垂直加速度限幅
|
||
输出.vz_mps ← 钳制(限速, ±vz_limit_mps)
|
||
```
|
||
|
||
**小误差自然映射为小垂直速度**:α = 1° 且增益 0.1 → vz = 0.1 m/s,行为连续,
|
||
不存在"小误差走一条支路、大误差走另一条支路"的模式翻转。原设计的双支路
|
||
(小误差发姿态微调消息)已取消,理由见 §10.1。
|
||
|
||
### 6.4 距离通道
|
||
|
||
```text
|
||
Δd ← 实测距离 − 期望射程 d_exp
|
||
Δd > 0:目标比期望远 → 前向接近(vx 为正)
|
||
Δd < 0:目标比期望近 → 减速/后退(vx 为负)
|
||
|
||
vx ← 非对称钳制( 加速度限幅( PID(Δd) ), −后退限速, +接近限速 )
|
||
```
|
||
|
||
**非对称限幅的物理动机**:接近方向允许较快(追击需要),后退方向必须保守
|
||
(后退意味着放弃射程优势,且后方通常未观测)。默认占位值 3 m/s vs 1 m/s。
|
||
|
||
**PID 输出限幅取两者较大值**:PID 内部用对称限幅 `±max(接近, 后退)` 做
|
||
anti-windup 判据,外部再做非对称钳制。这样内部限幅不会先于外部约束生效,
|
||
保证 anti-windup 判据基于"真实饱和边界"。
|
||
|
||
**可用性三重门**(缺一不可):关联标志成立 **且** 距离为正 **且** 未超龄。
|
||
"未关联的雷达距离"只是机头前方某个障碍物的距离,把它当作目标距离会导致
|
||
朝着错误目标调整射程。
|
||
|
||
### 6.5 PID 内部算法
|
||
|
||
```text
|
||
函数 更新(error, dt) -> 输出:
|
||
若 dt <= 0: 返回 上次输出 # 异常节拍保护
|
||
|
||
# ---- 积分:候选值先钳位 ----
|
||
积分候选 ← 积分累计 + error × dt
|
||
若 积分限幅 > 0: 积分候选 ← 钳制(积分候选, ±积分限幅)
|
||
|
||
# ---- 微分:一阶低通 ----
|
||
若 存在有效微分历史:
|
||
微分原始 ← (error − 上拍误差) / dt
|
||
微分 ← 滤波系数 × 上次滤波微分 + (1 − 滤波系数) × 微分原始
|
||
否则:
|
||
微分 ← 0 # 首拍不做微分(避免冲击)
|
||
上次滤波微分 ← 微分; 上拍误差 ← error; 存在有效微分历史 ← true
|
||
|
||
# ---- 位置式合成 ----
|
||
原始输出 ← kp·error + ki·积分候选 + kd·微分
|
||
|
||
# ---- anti-windup:条件积分 ----
|
||
已饱和 ← (输出限幅 > 0) 且 |原始输出| > 输出限幅
|
||
增量推深饱和 ← (error × dt) × 原始输出 > 0 # 本拍积分增量方向与饱和同向
|
||
若 非 (已饱和 且 增量推深饱和):
|
||
积分累计 ← 积分候选 # 否则冻结本拍累加
|
||
|
||
# ---- 输出限幅 ----
|
||
上次输出 ← (输出限幅 > 0) ? 钳制(原始输出, ±输出限幅) : 原始输出
|
||
返回 上次输出
|
||
```
|
||
|
||
**条件积分判据的三种候选与取舍**(这是 anti-windup 最容易写错的地方):
|
||
|
||
| 判据 | 饱和时行为 | 误差反号后 | 结论 |
|
||
|------|-----------|-----------|------|
|
||
| `积分项与输出同向` | 冻结 | **仍冻结**(积分残留同号)→ 无法回卷 | ✗ 会留下饱和残留 |
|
||
| `原始输出 × error > 0` | 冻结 | 可回卷,但大 kp 项会掩盖饱和方向 → 回卷被拖延 | △ 不够干脆 |
|
||
| **`(error × dt) × 原始输出 > 0`** | 冻结 | 增量立即反向 → **首拍即回卷** | ✓ 采用 |
|
||
|
||
**两种复位的语义差别**(必须分开实现):
|
||
|
||
| 方法 | 清空内容 | 保留内容 | 使用场景 |
|
||
|------|---------|---------|---------|
|
||
| 清积分 | 积分累计 | 微分历史、上次输出 | Coast 每拍、距离不可用每拍 |
|
||
| 全清 | 积分、上拍误差、滤波微分、上次输出、微分历史标志 | 参数 | 判丢进入刹车、停止视觉闭环 |
|
||
|
||
Coast 只清积分而不清微分:预测值仍在连续演化,微分历史有效,清掉会让微分项
|
||
在恢复跟踪瞬间失去参考、产生一次冲击。
|
||
|
||
### 6.6 变化率限幅器
|
||
|
||
```text
|
||
函数 推进(target, dt) -> 输出:
|
||
若 dt <= 0 或 变化率上限 <= 0: 返回 当前输出 # 异常节拍/冻结保护
|
||
最大步长 ← 变化率上限 × dt
|
||
当前输出 ← 钳制(target, 当前输出 − 最大步长, 当前输出 + 最大步长)
|
||
返回 当前输出
|
||
```
|
||
|
||
**两个必须知道的性质**:
|
||
|
||
1. **不可能超调**:输出被钳制在以"上次输出"为中心、`±最大步长` 为半径的区间内,
|
||
因此单步变化严格不超过配置上限。调试时若观察到"限幅后数值超过目标",
|
||
一定是别处的问题,不是限幅器超调。
|
||
2. **初值为 0 即天然实现首条指令渐变**:构造时输出置 0,切换进入视觉跟踪后的
|
||
第一条指令会从 0 按加速度限幅爬升,**不需要专门写切换分支**。这也是"判丢
|
||
刹车时把限幅器复位到 0"的原因——复位后恢复跟踪同样从零爬升。
|
||
|
||
---
|
||
|
||
## 7. 安全语义:超龄、判丢、Coast、无距离
|
||
|
||
### 7.1 超龄判定(纯函数,三条判据)
|
||
|
||
```text
|
||
函数 已超龄(取样时刻, 当前时刻, 有效期) -> 布尔:
|
||
若 取样时刻 <= 0: 返回 true # 从未取样
|
||
若 当前时刻 < 取样时刻: 返回 true # 时钟源错配/打戳错误,年龄不可信
|
||
返回 (当前时刻 − 取样时刻) > 有效期
|
||
```
|
||
|
||
**第二条判据是"宁可降级"的显式表达**:单一单调时钟下不应出现 `now < sample`。
|
||
一旦出现,说明取样与判定用了不同时钟源(或打戳位置错误),此时年龄**不可核实**,
|
||
不能因为"看起来没超龄"就继续用它算正常指令。
|
||
|
||
**三个有效期独立配置**,反映各输入的更新特性:
|
||
|
||
| 输入 | 有效期占位值 | 依据 |
|
||
|------|-------------|------|
|
||
| 视觉追踪结果 | 200 ms | 与推理帧率/追踪输出节拍匹配,约 4 拍(20 Hz) |
|
||
| 自机姿态 | 300 ms | 姿态链路更新较快,但缺失即禁止坐标运算 |
|
||
| 雷达距离 | 500 ms | 雷达更新较慢,且距离不可用有独立降级动作 |
|
||
|
||
### 7.2 降级优先级(顺序即语义)
|
||
|
||
```text
|
||
姿态不可用 > 视觉超龄 > 判丢 > 无距离(退出档)
|
||
```
|
||
|
||
| 优先级 | 检查 | 理由 |
|
||
|-------|------|------|
|
||
| 1 | 姿态过期/缺失 | 连坐标旋转与绝对航向合成都做不了,任何空间量都不可信(规格:禁止空间转换和控制) |
|
||
| 2 | 视觉超龄 | 视觉是"是否判丢"的唯一来源;数据过期时连 `tracked` 标志本身都不可信,因此**必须先于判丢**处理 |
|
||
| 3 | 判丢 | 视觉可信且明确报告无轨道 → 零速刹车 |
|
||
| 4 | 无距离 | 只有在"视觉可信且正在跟踪"前提下才谈得上距离降级 |
|
||
|
||
### 7.3 判丢刹车与状态清理
|
||
|
||
```text
|
||
触发: tracked == false(且视觉未超龄、姿态可用)
|
||
动作: 输出 kBrakeHover(速度全零、航向全零)、valid = true
|
||
进入沿清:距离 PID 全清 + 四个限幅器复位到 0
|
||
```
|
||
|
||
**为什么用"进入沿"而不是每拍清**:每拍清会把限幅器输出反复钉在 0,虽然结果
|
||
相同,但会掩盖"进入刹车"这一事件;用进入沿判定还能保证只清一次,语义清晰。
|
||
判定依据是"上拍模式 ≠ 刹车",因此不需要额外的布尔标志。
|
||
|
||
**为什么刹车要 `valid = true`**:刹车是一条**真实要发出去的指令**(零速),
|
||
与降级"什么都不发"有本质区别。
|
||
|
||
### 7.4 Coast(预测跟踪)
|
||
|
||
```text
|
||
触发: is_predicted == true(追踪库用滤波器预测值补帧)
|
||
动作: 输出照常计算(目标中心为预测值),仅清距离 PID 积分;coast_active = true
|
||
```
|
||
|
||
**为什么 Coast 期间仍照常算航向与垂直**:Coast 是短暂补帧(数帧量级),预测中心
|
||
连续可用,此时停止控制反而会造成机体停顿—恢复的抖动。清积分的理由是:预测期间
|
||
误差不反映真实世界,积分累加会把预测偏差固化成饱和量。
|
||
|
||
**Coast 期间距离通常不可用**:目标短暂消失往往伴随雷达关联失效,因此第 ⑦ 步会
|
||
走无距离分支(默认"保持"→ 前向清零)。这是设计上的自然降级,不需要额外分支。
|
||
|
||
### 7.5 无距离三档动作
|
||
|
||
| 配置值 | 行为 | 适用场景 |
|
||
|--------|------|---------|
|
||
| `hold`(默认) | 前向速度清零,限幅器复位到 0 | 安全占位语义:不知道距离就不前冲 |
|
||
| `slow_approach` | 以配置的低速限幅缓速接近(仍经加速度限幅) | 需要持续压迫、且低速可接受风险 |
|
||
| `exit` | 整体降级保持(`kDegradedHold` / `kNoDistanceExit`),不发送 | 距离是安全前提(如避撞),无距离必须退出近距操作 |
|
||
|
||
三档中只有 `exit` 会中断整拍输出;`hold` 与 `slow_approach` 仍输出航向与垂直
|
||
通道(继续把目标保持在画面中心)。
|
||
|
||
---
|
||
|
||
## 8. 输出打包与链路
|
||
|
||
### 8.1 控制意图 → 消息映射
|
||
|
||
| 模式 | 消息 | 关键填法 |
|
||
|------|------|---------|
|
||
| `kVelocityHeading` | ID 84 `SET_POSITION_TARGET_LOCAL_NED`(速度模式) | 位置三字段填 0 并被掩码忽略;加速度三字段填 0 并被掩码忽略;速度填 NED 三分量;航向按掩码三分支二选一 |
|
||
| `kBrakeHover` | ID 84(同上,全零速度) | 速度全零,位置/加速度/航向/航向角速率字段**全部**被掩码忽略 |
|
||
| `kDegradedHold` | —— | **不打包、不发送** |
|
||
|
||
当前实现已收敛为**只使用一种消息类型**(ID 84)。原设计中的姿态目标消息
|
||
(ID 82)随姿态微调支路一并取消(§10.1)。
|
||
|
||
### 8.2 忽略掩码语义与位定义
|
||
|
||
掩码语义:**置位 = 忽略该字段**。基础位组合(速度模式必备):
|
||
|
||
```text
|
||
基础掩码 = 忽略位置 x/y/z + 忽略加速度 afx/afy/afz
|
||
= 1 + 2 + 4 + 64 + 128 + 256 = 455 = 0x01C7
|
||
```
|
||
|
||
航向两位取自**本仓库随附的 MAVLink 定义**(v3,`common.xml` / `common.h`):
|
||
|
||
| 枚举 | 值 | 位序 |
|
||
|------|---|------|
|
||
| `YAW_IGNORE` | 1024 = `0x0400` | bit 10 |
|
||
| `YAW_RATE_IGNORE` | 2048 = `0x0800` | bit 11 |
|
||
|
||
> ⚠️ **版本偏移风险(必须实测确认)**:这两个值之所以是 1024/2048,是因为该
|
||
> 版本的枚举在 `AZ_IGNORE=256` 之后插入了 `FORCE_SET=512`。**较旧的 MAVLink
|
||
> 版本没有 `FORCE_SET`,其 `YAW_IGNORE=512`、`YAW_RATE_IGNORE=1024`**,
|
||
> 位序整体低一位。若飞控端(PX4 1.11.3)随附的是旧枚举,本模块发出的掩码会被
|
||
> 解读为"使用 yaw、忽略 yaw_rate",即把速率式指令误解为绝对航向 0(指向正北)。
|
||
> **上线前必须在飞控端核对枚举值,或在链路侧抓包比对行为。**
|
||
|
||
### 8.3 三个掩码与三分支选择
|
||
|
||
| 掩码 | 组成 | 语义 |
|
||
|------|------|------|
|
||
| 速度+航向角 | 基础 \| `0x0800` | 使用 `yaw`,忽略 `yaw_rate`(位置式) |
|
||
| 速度+角速率 | 基础 \| `0x0400` | 使用 `yaw_rate`,忽略 `yaw`(速率式,默认) |
|
||
| 刹车 | 基础 \| `0x0C00` | `yaw` 与 `yaw_rate` 均忽略 |
|
||
|
||
**分支选择规则(含"两者皆零"的处理)**:
|
||
|
||
```text
|
||
有角速率 ← |yaw_rate_dps| > 极小阈值
|
||
有航向角 ← |yaw_deg| > 极小阈值
|
||
|
||
若 (非 有角速率) 且 有航向角: 用 速度+航向角掩码
|
||
否则: 用 速度+角速率掩码
|
||
```
|
||
|
||
**为什么"两者皆零"必须落到角速率掩码**:此时若选航向角掩码,`yaw = 0` 会被
|
||
飞控当作**有效绝对航向 0(正北)**执行——机体无故转向正北。落到角速率掩码配
|
||
零角速率,语义是"保持当前航向",才是期望行为。这是速率式默认配置下的常态
|
||
路径(β≈0 时角速率自然为 0),影响面很大。
|
||
|
||
### 8.4 单位换算清单(打包边界上的全部换算)
|
||
|
||
| 量 | 控制律内部 | 链路上 | 换算 |
|
||
|----|-----------|-------|------|
|
||
| 航向角 | deg | rad | × π/180 |
|
||
| 偏航角速率 | deg/s | rad/s | × π/180 |
|
||
| 速度 | m/s | m/s | 无 |
|
||
| 时间戳 | 单调 ms | 系统启动后 ms | 直接传入(由调用方保证语义一致) |
|
||
|
||
**内部统一用度、边界统一转弧度**的理由:控制参数(视场角、增益、限幅)在
|
||
度单位下便于人工整定与阅读;弧度只是链路协议要求。换算点集中在打包层,
|
||
控制律内部不出现弧度制航向。
|
||
|
||
### 8.5 打包与传输分离
|
||
|
||
```text
|
||
控制律 ──控制意图──▶ 打包网关 ──字节流──▶ 传输抽象 ──▶ 物理链路
|
||
(懂协议) (只懂字节)
|
||
```
|
||
|
||
分离带来的三个好处:
|
||
|
||
1. **可回放/可录制**:把传输实现换成"写文件"即可离线录制全量指令流,
|
||
换成"回环打印"即可台架观测,控制律零改动;
|
||
2. **链路可替换**:串口 ↔ 电台 ↔ 网络透明切换;
|
||
3. **协议知识集中**:所有 MAVLink 细节只存在于打包层一处,控制律与配置层
|
||
完全不感知协议。
|
||
|
||
**与实现语言相关的工程约束**(重写时可忽略,但当前 C++ 落地必须遵守):
|
||
随附的 MAVLink C 库把辅助函数以外部链接直接定义在头文件中(`MAVLINK_HELPER`
|
||
默认为空宏),**多个编译单元包含同一协议头会产生重复定义链接错误**。因此
|
||
协议头只允许被打包层那**一个**编译单元包含,对外头文件不得出现任何协议类型,
|
||
打包/发送缓冲以固定尺寸字节存储承载并在编译期断言尺寸足够。
|
||
|
||
### 8.6 发送节奏
|
||
|
||
- 控制频率由配置给定(占位 20 Hz),**每拍一条**指令;
|
||
- offboard 模式要求指令流**持续**:断流超过飞控侧超时阈值会触发 offboard
|
||
失效保护。因此 `kDegradedHold`(不发送)会让飞控自行进入失效保护——
|
||
这是有意的"交还控制权"语义,但见 §12.2 的待确认项;
|
||
- 本模块不做重传、不做发送失败重试;发送失败仅返回布尔由调用方计数告警。
|
||
|
||
---
|
||
|
||
## 9. 配置参数全量表
|
||
|
||
配置在**启动期一次性加载**,运行期只读。缺字段、类型错误、非法取值一律
|
||
在加载期报错并**携带完整字段路径**(如 `heading.gain`),不允许带着半套
|
||
默认值进入运行期。
|
||
|
||
### 9.1 control 组:周期与超龄
|
||
|
||
| 键 | 单位 | 占位值 | 作用 |
|
||
|----|------|-------|------|
|
||
| `frequency_hz` | Hz | 20.0 | 控制频率;同时作为 dt 兜底值 |
|
||
| `visual_stale_ms` | ms | 200 | 视觉结果超龄阈值 |
|
||
| `radar_stale_ms` | ms | 500 | 雷达距离超龄阈值 |
|
||
| `attitude_stale_ms` | ms | 300 | 自机姿态超龄阈值 |
|
||
|
||
### 9.2 camera 组:画面与视场
|
||
|
||
| 键 | 单位 | 占位值 | 作用 |
|
||
|----|------|-------|------|
|
||
| `image_width` | px | 1920 | 画面宽 W(β 计算与 x 偏移基准) |
|
||
| `image_height` | px | 1080 | 画面高 H(α 计算与 y 偏移基准) |
|
||
| `fov_h_deg` | deg | 60.0 | 水平视场角 θ_h |
|
||
| `fov_v_deg` | deg | 35.0 | 垂直视场角 θ_v |
|
||
|
||
> 画面尺寸必须与追踪库实际使用的画面尺寸一致;若推理在缩放图上运行,
|
||
> 这里填**缩放后**的尺寸,否则视线角会系统性偏差。
|
||
|
||
### 9.3 heading 组:水平航向
|
||
|
||
| 键 | 单位 | 占位值 | 作用 |
|
||
|----|------|-------|------|
|
||
| `mode` | 枚举 | `rate` | `rate` = 速率式(默认)/ `position` = 位置式 |
|
||
| `gain` | 见 §6.2 | 1.0 | 视线角 → 航向指令增益 |
|
||
| `yaw_rate_limit_dps` | deg/s | 30.0 | 偏航角速度幅值上限 |
|
||
| `yaw_slew_limit_dps2` | deg/s² | 60.0 | 航向指令变化率上限(两种模式共用,量纲待澄清) |
|
||
|
||
### 9.4 vertical 组:垂直
|
||
|
||
| 键 | 单位 | 占位值 | 作用 |
|
||
|----|------|-------|------|
|
||
| `vz_gain_mps_per_deg` | (m/s)/deg | 0.1 | 视线角 → 垂直速度增益 |
|
||
| `vz_limit_mps` | m/s | 2.0 | 垂直速度幅值上限 |
|
||
|
||
> 原双支路所需的"切换阈值"与"姿态微调增益/限幅"三项已随支路取消而移除。
|
||
|
||
### 9.5 distance 组:距离
|
||
|
||
| 键 | 单位 | 占位值 | 作用 |
|
||
|----|------|-------|------|
|
||
| `d_exp_m` | m | 10.0 | 期望射程(由拦截设备接口给定) |
|
||
| `kp` / `ki` / `kd` | —— | 0.5 / 0.05 / 0.1 | PID 三增益 |
|
||
| `integral_limit` | m·s | 2.0 | 积分项限幅(<=0 表示不限幅) |
|
||
| `derivative_filter_coef` | ∈[0,1) | 0.2 | 微分一阶低通系数(0 = 不滤波) |
|
||
| `approach_velocity_limit_mps` | m/s | 3.0 | 接近速度上限 |
|
||
| `retreat_velocity_limit_mps` | m/s | 1.0 | 后退速度上限 |
|
||
| `no_distance_action` | 枚举 | `hold` | `hold` / `slow_approach` / `exit` |
|
||
| `no_distance_approach_limit_mps` | m/s | 0.5 | `slow_approach` 档的限速值 |
|
||
|
||
### 9.6 accel_limit 组:加速度
|
||
|
||
| 键 | 单位 | 占位值 | 作用 |
|
||
|----|------|-------|------|
|
||
| `ax_mps2` | m/s² | 2.0 | 前向加速度上限(距离通道限幅器) |
|
||
| `ay_mps2` | m/s² | 2.0 | 侧向(**预留**:本控制律不做侧移,无对应限幅器) |
|
||
| `az_mps2` | m/s² | 1.5 | 垂直加速度上限(垂直通道限幅器) |
|
||
|
||
---
|
||
|
||
## 10. 设计取舍记录(为什么不是别的做法)
|
||
|
||
### 10.1 取消垂直"姿态微调"双支路,统一走速度支路
|
||
|
||
**原设计**:垂直误差小于切换阈值时发**姿态目标消息**做微调,大于阈值时发
|
||
**速度设定值**。
|
||
|
||
**取消理由(三条,任一条都足以否决)**:
|
||
|
||
1. **丢弃同周期的距离通道输出**:姿态消息里没有前向速度字段。一旦本拍切成
|
||
姿态消息,距离通道算出的 `vx` 就被整拍丢弃 → 目标在画面中心附近时逼近
|
||
**停滞**(而"目标居中"恰恰是最常见的稳态)。
|
||
2. **消息类型逐拍翻转扰动飞控**:目标在切换阈值附近抖动时,offboard 会收到
|
||
交替的两种消息类型,飞控侧设定值来源反复切换,行为难以预测。
|
||
3. **需要额外整定一套参数**:切换阈值、微调增益、姿态角限幅、姿态角速率限幅
|
||
四项全部需要标定,而收益仅是"小误差时更细腻"。
|
||
|
||
**替代方案的效果**:小误差经增益线性映射为小垂直速度(1° → 0.1 m/s),行为
|
||
连续、无模式翻转、全周期单一消息类型,且少 4 个待标定参数。
|
||
|
||
### 10.2 航向默认改为速率式
|
||
|
||
**理由**:速率式指令是**相对量**,不存在"这是绝对航向还是修正量"的坐标系歧义;
|
||
位置式的 `yaw` 字段是绝对 NED 航向,一旦合成环节出错(漏加当前航向、姿态过期
|
||
用了旧航向),后果是机体转向一个完全无关的地理方向——**故障模式危险且不直观**。
|
||
速率式最坏情况是转速不对,不会指向错误方位。
|
||
|
||
**代价**:稳态需 β→0 才停转,可能有小幅极限环(可通过死区或降低增益抑制)。
|
||
位置式保留为可配置选项,但**姿态接线未验证前不得用于实飞**。
|
||
|
||
### 10.3 输出端补齐机体系 → NED 旋转
|
||
|
||
**原状态**:速度分量以机体系语义直发,而打包声明的坐标框是局部 NED——
|
||
**声明与实际不符**。机头不朝北时,"前向 3 m/s"会被飞控理解为"北向 3 m/s",
|
||
机体航向一变,速度方向就错。
|
||
|
||
**修正**:在控制律输出端(第 ⑧ 步)随快照航向旋转为 NED,使对外语义与打包
|
||
声明一致。限幅仍在机体系完成(正交变换保范,见 §4.4)。
|
||
|
||
### 10.4 视线角全程针孔、取消线性双路
|
||
|
||
见 §6.1:双路切换边界处约 8% 角度台阶,会向控制环注入突变;`atan` 代价可忽略。
|
||
同时消除了一个待标定参数。
|
||
|
||
### 10.5 PID anti-windup 采用"增量方向"条件积分
|
||
|
||
见 §6.5 判据对比表:另两种判据要么导致误差反号后无法回卷(积分残留),
|
||
要么被大比例项掩盖饱和方向、拖延回卷。
|
||
|
||
### 10.6 限幅器初值为 0 兼作首条指令突变抑制
|
||
|
||
见 §6.6:不需要为"位置模式 → 速度模式切换"写专门分支,限幅器从零起步天然
|
||
满足"首条视觉速度设定值必须经过限幅"的要求;判丢刹车时复位到 0,恢复跟踪
|
||
同样从零爬升。
|
||
|
||
### 10.7 降级路径不清状态、刹车路径清状态
|
||
|
||
见 §5.3:降级是"这一拍不输出",恢复后应平滑续接;刹车是真实控制中断,
|
||
恢复后必须从零渐变。两者语义不同,清理策略必须不同。
|
||
|
||
---
|
||
|
||
## 11. 实现与重写指引
|
||
|
||
### 11.1 必须持久化的状态(漏一个就变行为)
|
||
|
||
| 状态 | 所属 | 初值 | 何时改变 |
|
||
|------|------|------|---------|
|
||
| 上拍时刻 | 主控律 | 0(表示首拍) | 每拍写入当前时刻 |
|
||
| 上拍输出模式 | 主控律 | 降级保持 | 仅模式切换时写入(并触发日志) |
|
||
| 积分累计 | PID | 0 | 每拍(除冻结);清积分/全清时归零 |
|
||
| 上拍误差 | PID | 0 | 每拍 |
|
||
| 低通后微分 | PID | 0 | 每拍 |
|
||
| 上次输出 | PID | 0 | 每拍 |
|
||
| 微分历史有效标志 | PID | false | 首次更新后置 true;全清时回 false |
|
||
| 上次输出 ×4 | 四个限幅器 | 0 | 每拍;复位时归零 |
|
||
|
||
**构造期预计算量**(不应在热路径重复计算):针孔系数 ×2、PID 参数拷贝、
|
||
限幅器变化率上限 ×4、配置值拷贝。
|
||
|
||
### 11.2 时序与线程模型
|
||
|
||
```text
|
||
推理侧(每帧,独立线程):
|
||
追踪库.更新(检测框, 画面尺寸) # 内部维护轨道与主目标
|
||
|
||
控制线程(固定周期,配置频率):
|
||
结果 ← 追踪库.取最新结果() # 线程安全拷贝
|
||
快照 ← 采集(结果, 雷达, 姿态)
|
||
快照.track_time_ms ← 当前单调时钟() # 就在取结果的那一刻打戳
|
||
意图 ← 控制律.更新(快照, 当前单调时钟())
|
||
按 意图.mode 分发:
|
||
速度航向 → 网关.发送速度航向(意图, 启动后毫秒)
|
||
零速刹车 → 网关.发送刹车(启动后毫秒)
|
||
降级保持 → 不发送(记录降级原因/告警)
|
||
|
||
停止视觉闭环(状态机决定):
|
||
控制律.复位() # 清积分、清限幅器、清节拍与模式历史
|
||
```
|
||
|
||
**约束**:
|
||
|
||
- 控制律实例**只由控制线程调用**,构件无锁;
|
||
- 采集快照与打戳必须尽量贴近(打戳晚于采集会把采集耗时算进"新鲜度");
|
||
- 追踪库取结果必须是**拷贝**而非引用,避免推理线程并发改写;
|
||
- 复位与更新不得并发。
|
||
|
||
### 11.3 数值与精度注意事项
|
||
|
||
| 事项 | 处理方式 | 理由 |
|
||
|------|---------|------|
|
||
| 角度回绕 | 位置式合成后回绕到 [-180, 180) | 链路侧与人工读数都按此区间 |
|
||
| 浮点"是否为零" | 用极小阈值(如 1e-9)比较,不用 `== 0` | 掩码分支选择依赖"航向/角速率是否有效" |
|
||
| dt 异常 | `dt <= 0` → 保持上次输出;`dt > 0.5 s` → 截断 | 除零保护与限幅器保护 |
|
||
| 首拍微分 | 不做微分(无历史) | 避免首拍微分冲击 |
|
||
| 除法基准 | 画面尺寸、`dt` 均在构造期/入口校验为正 | 防除零 |
|
||
| 配置校验 | 加载期全量校验并抛错(含字段路径) | 不允许带非法参数进入运行期 |
|
||
|
||
### 11.4 资源与日志纪律
|
||
|
||
- **热路径零动态分配**:所有缓冲与系数在构造期备妥(若实现语言有 GC,
|
||
也应避免每拍新建对象造成抖动);
|
||
- **热路径零日志**:仅模式切换打一条 INFO;
|
||
- **异常日志节流**:若实现中加入异常日志,必须按"第 1 次与每满 N 次"节流,
|
||
20 Hz 下不节流会在数秒内刷满日志;
|
||
- **RAII / 确定性释放**:传输实现、配置源在构造期注入,生命周期由调用方保证。
|
||
|
||
### 11.5 可测试性设计(构件边界即测试边界)
|
||
|
||
| 构件 | 是否纯函数 | 测试要点 |
|
||
|------|-----------|---------|
|
||
| 超龄判定 | 是 | 三条判据各一例;边界(恰好等于有效期不超龄) |
|
||
| 坐标旋转 | 是 | §4.3 两条正方向自检;旋转保范;垂直分量不变 |
|
||
| 视线角转换 | 是 | 中心偏移为 0;针孔结果与线性近似明显偏离(证明未走线性);非法参数报错 |
|
||
| 静态限幅 | 是 | 上/下界、界内不变 |
|
||
| 变化率限幅器 | 否(有状态) | 单步不超限;**不可超调**;复位后从零爬升;`dt<=0` 保持 |
|
||
| PID | 否(有状态) | 各项独立;积分钳位;**误差反号后能回卷**(旧错误判据在此会留在同侧饱和值);两种复位语义 |
|
||
| 主控律 | 否(有状态) | 八步流程各分支;降级优先级;模式切换日志只打一次 |
|
||
| 打包网关 | 否 | 字节头(起始字节、长度、消息 ID);掩码三分支;单位换算;**两者皆零走角速率掩码** |
|
||
|
||
**测试策略**:构件级用纯函数/小状态测试覆盖数学与边界;主控律级用构造快照
|
||
驱动分支覆盖;打包级**直接解析字节**验证线格式(不依赖协议库的解码函数,
|
||
避免"用同一份错误理解自我验证")。
|
||
|
||
---
|
||
|
||
## 12. 验证路线与待定项
|
||
|
||
### 12.1 分阶段验证路线
|
||
|
||
| 阶段 | 目标 | 方法 | 通过判据 |
|
||
|------|------|------|---------|
|
||
| 1. 构件级 | 数学正确 | 单元测试(无硬件) | 全部用例通过 |
|
||
| 2. 控制律级 | 分支与时序正确 | 构造快照驱动 + 端到端合成场景回放 | 三分支(正常/Coast/判丢)行为符合 §7 |
|
||
| 3. 链路级 | 线格式正确 | 注入"回环打印/写文件"传输实现,解析字节 | 帧头、消息 ID、掩码、单位换算符合 §8 |
|
||
| 4. SITL | 闭环行为 | 仿真飞控 + 回放/合成目标 | 目标居中收敛、无抖动、无积分饱和 |
|
||
| 5. 台架 | 实链路 | 真实串口/电台 + 飞控上电不转桨 | offboard 建立、指令被接受、无失效保护 |
|
||
| 6. 实飞 | 标定整定 | 逐项标定增益与限幅 | **仅在 1–5 全部通过后** |
|
||
|
||
当前已完成阶段 1–3(构建零告警、单元测试全绿、端到端示例三阶段完整运行)。
|
||
阶段 4 起需要飞控与标定数据,属后续工作。
|
||
|
||
### 12.2 待定项清单
|
||
|
||
| 项 | 现状 | 需要谁确认 | 风险 |
|
||
|----|------|-----------|------|
|
||
| **全部数值参数** | 占位值 | 标定 + 台架整定 | 高:未标定不得实飞 |
|
||
| **忽略掩码位序** | 按随附 MAVLink v3 枚举(`YAW_IGNORE=1024`、`YAW_RATE_IGNORE=2048`) | 核对飞控端 PX4 1.11.3 随附枚举 | **高**:旧枚举无 `FORCE_SET=512`,位序低一位 → 速率指令被误解为绝对航向 0(§8.2) |
|
||
| **坐标框取值** | 局部 NED | 台架实测 | 中:与旋转语义必须一致 |
|
||
| **降级时是否应主动刹车** | 降级**不发送**,依赖飞控 offboard 断流失效保护 | 与安全策略负责人确认 | 中:断流后飞控行为(悬停/返航)取决于其配置,不由本模块决定 |
|
||
| **`yaw_slew_limit_dps2` 量纲** | 两种航向模式共用一个键;速率式下含义为 deg/s²,位置式下实际含义为 deg/s | 启用位置式前拆分为两个键或重命名 | 低(默认速率式,位置式当前休眠) |
|
||
| **侧向通道** | `ay_mps2` 预留、机体侧向恒 0 | 若需要侧移则新增通道 | 低 |
|
||
| **机体补偿** | 快照的滚转/俯仰/角速度字段预留未用 | 接入主工程姿态源后评估 | 低 |
|
||
| **相机标定** | 内参/畸变/安装外参未标定,针孔模型无畸变补偿 | 标定 | 中:视线角系统性偏差 |
|
||
| **雷达关联判据** | 由感知融合层给出标志,本模块不实现 | 融合层设计 | 中:关联错误 = 距离通道朝错误目标调射程 |
|
||
|
||
### 12.3 红线
|
||
|
||
1. **未标定不得实飞**:配置与代码默认值中所有数值均为占位,标定与实测整定
|
||
完成前,仅限 SITL / 台架 / 录制数据回放验证数学链路。
|
||
2. **不得混用未标注坐标系的数据**:对外输出的速度分量已声明为局部 NED,
|
||
任何改动都必须同步更新声明与打包侧坐标框。
|
||
3. **不得用超龄数据生成正常指令**:超龄只能触发降级或告警。
|
||
4. **不得绕过限幅**:任何新增通道都必须内联"变化率 + 幅值"双重限幅,
|
||
包括切换后的首条指令。
|
||
5. **未关联的雷达距离不得当作目标距离**。
|
||
|
||
---
|
||
|
||
## 附:与追踪库的行为衔接速查
|
||
|
||
| 追踪库输出 | 本模块响应 | 位置 |
|
||
|-----------|-----------|------|
|
||
| `tracked == true`、`is_predicted == false` | 三路正常计算 | §5.2 |
|
||
| `tracked == true`、`is_predicted == true` | 三路照常计算 + 清距离积分 + `coast_active` | §7.4 |
|
||
| `tracked == false` | 零速刹车 + 清积分与限幅器历史 | §7.3 |
|
||
| 结果超过 `visual_stale_ms` 未更新 | 降级保持(视觉超龄),**不刹车**(标志不可信) | §7.2 |
|
||
| 主目标切换(追踪库内部选举) | 本模块无感知:像素偏移自然跳变,由限幅器吸收 | §6.6 |
|