# 视觉跟踪控制律(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`);双目模块提供目标距离 (检测框自身的立体测距,前向分量 Z)与有效标志 `distance_valid`;飞控链路 提供自机姿态(至少航向角)。 **方位源(接入层约定)**:双目模组两枚相机分列模组中线两侧(各偏 ±B/2), 单目像素存在近场视差偏差(3m 处约 0.57°)。匹配成功时接入层应以**虚拟双目 中心**像素 `x_mid = (xL + xR) / 2` 填充 `track.center`(推导见 `docs/双目深度估计思路.md` §11.3)。虚拟中心相机与单目共用同一 FOV 配置, 本模块"像素偏移 → 视线角"逻辑与配置**零改动**。 **下游依赖**:只有一个——传输抽象。打包与传输分离,控制律不直接写飞控。 **上层归属**:状态机(S6/S7/S8 切换、目标丢失策略、障碍接管)在**本模块之外**。 本模块只按输入标志产生"当拍应该做什么",不决定"要不要继续视觉闭环"。 ### 1.3 设计特点 | 特点 | 含义 | 带来的约束 | |------|------|-----------| | **纯值输入 / 纯值输出** | 每拍传入一个不可变快照,返回一个输出值对象 | 无隐藏输入、无回调、可离线回放 | | **单线程固定周期** | 由控制线程按配置频率调用,构件内部**无锁** | 不得从多线程并发调用同一实例 | | **热路径零动态分配** | 所有系数、缓冲在构造期备妥;每拍只做算术 | 便于实时性分析与嵌入式移植 | | **热路径零日志** | 仅在输出模式发生切换时打一条日志 | 日志量与状态变化次数成正比,不与频率成正比 | | **配置全量外置** | 所有阈值/增益/限幅来自启动期加载的配置对象 | 代码内**不得**出现任何写死的控制参数 | | **无状态纯函数构件** | 视线角转换、超龄判定、坐标旋转均为纯函数 | 可独立单元测试,无需夹具 | | **单一 offboard 消息类型** | 全周期只发一种消息(ID 84) | 避免消息类型逐拍翻转账扰动飞控(见 §10.1) | ### 1.4 明确不做的事(边界) - **不做感知**:不跑检测/光流/双目立体匹配,只消费调用方填入的快照; - **不做距离有效性判定**:`distance_valid` 由双目链路自身的一致性校验给出, track_id 一致性校验为调用方约定(§12.2),本模块只读取标志; - **不做状态机仲裁**:不判断何时进入/退出视觉跟踪,不实现目标丢失策略; - **不做链路 IO**:不打开串口、不管理重连、不解析任何入向 MAVLink 消息; - **不做配置热更新**:启动期加载一次,调参重启生效; - **不做多目标**:只处理追踪库选定的主目标。 --- ## 2. 输入输出接口定义 ### 2.1 输入:控制快照(每拍一个,由调用方采集并打戳) | 分组 | 字段 | 单位 / 类型 | 语义与约束 | |------|------|------------|-----------| | 视觉 | `track.center` | 像素 (x, y) | 主目标中心;原点为画面**左上角**,x 右正、y 下正;双目匹配成功时由接入层填虚拟双目中心 `x_mid`(§1.2) | | 视觉 | `track.tracked` | 布尔 | 是否存在有效轨道;`false` → 判丢刹车 | | 视觉 | `track.is_predicted` | 布尔 | 本拍是否为滤波器预测值(Coast);`true` → 清距离积分 | | 视觉 | `track_time_ms` | 单调时钟 ms | **取样瞬间**打戳;`<= 0` 视为从未取样,直接判超龄 | | 距离 | `target_distance_m` | m | 双目目标距离(前向分量 Z);`<= 0` 视为不可用 | | 距离 | `distance_valid` | 布尔 | 本帧测距可用标志:距离来自目标检测框自身的双目测距;**无效帧的距离不得当作目标距离** | | 距离 | `distance_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`、无距离计时(`exit` 门限用,§7.5) | `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: 无距离计时.清除() # 刹车期间不计 kExit 时长 若 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) # ---- ⑦ 距离通道 ---- 距离可用 ← 快照.distance_valid 且 快照.target_distance_m > 0 且 非 已超龄(快照.distance_time_ms, now_ms, 配置.distance_stale_ms) 若 距离可用: 无距离计时.清除() # 任一有效帧重置 kExit 计时 Δd ← 快照.target_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) 退出近距操作: 若 非 无距离计时.进行中: 无距离计时.启动(now_ms) # 首个无效拍起计时 若 now_ms − 无距离计时.起始时刻 ≥ 配置.no_distance_exit_grace_ms: 输出.mode ← kDegradedHold 输出.degraded_reason ← kNoDistanceExit 返回 收尾并记录模式切换(输出, "持续无距离达门限,退出近距操作") # grace 期内按"限速接近"执行(复用加速度限幅,vx 平滑无跳变) 输出.vx_mps ← 前向速度限幅器.推进(配置.no_distance_approach_limit_mps, dt) # ---- ⑧ 机体系 → 局部 NED 旋转(输出端) ---- 旋转机体到NED(输出.vx_mps, 输出.vy_mps, 输出.vz_mps, 快照.yaw_rad) 输出.valid ← true 返回 收尾并记录模式切换(输出, 输出.coast_active ? "Coast 预测跟踪" : "正常跟踪") ``` ### 5.3 提前返回的语义差异(易错点) 三条提前返回路径**跳过第 ⑧ 步旋转**,这是安全的:这些路径的速度分量全为零, 零向量旋转后仍是零向量。但重写时必须注意: | 返回路径 | 是否清 PID | 是否清限幅器 | 速度是否旋转 | `valid` | |---------|-----------|------------|------------|---------| | 姿态超龄降级 | 否 | 否 | 否 | `false` | | 视觉超龄降级 | 否 | 否 | 否 | `false` | | 判丢刹车 | 是(进入沿) | 是(进入沿) | 否 | `true` | | 无距离退出(grace 期满) | 是(仅积分) | 否 | 否 | `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 判据基于"真实饱和边界"。 **可用性三重门**(缺一不可):`distance_valid` 成立 **且** 距离为正 **且** 未超龄。 双目测距取自目标检测框自身,不存在"回波打到别处"的跨模态关联问题;标志为 `false` 表示本帧测距不可用(有效域外/未匹配/一致性校验失败),不得把任何数值 当作目标距离。 ### 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 姿态不可用 > 视觉超龄 > 判丢 > 无距离(exit 档,持续达门限) ``` | 优先级 | 检查 | 理由 | |-------|------|------| | 1 | 姿态过期/缺失 | 连坐标旋转与绝对航向合成都做不了,任何空间量都不可信(规格:禁止空间转换和控制) | | 2 | 视觉超龄 | 视觉是"是否判丢"的唯一来源;数据过期时连 `tracked` 标志本身都不可信,因此**必须先于判丢**处理 | | 3 | 判丢 | 视觉可信且明确报告无轨道 → 零速刹车 | | 4 | 无距离 | 只有在"视觉可信且正在跟踪"前提下才谈得上距离降级;`exit` 档还需持续无距离达门限(§7.5),默认档为 `slow_approach`(§10.8) | ### 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 期间距离通常不可用**:目标短暂消失时双目失去匹配(`distance_valid == false`),第 ⑦ 步走无距离分支;默认 `slow_approach` 下前向按低速定速推进, 方位闭环继续(§7.5)。这是设计上的自然降级,不需要额外分支。 ### 7.5 无距离三档动作 | 配置值 | 行为 | 适用场景 | |--------|------|---------| | `slow_approach`(默认) | 以配置的低速限幅缓速接近(仍经加速度限幅) | **默认档**:双目是机载唯一距离源,有效域外无距离是常态而非异常;即远场策略"方位闭环 + 定速接近"(D4) | | `hold` | 前向速度清零,限幅器复位到 0 | 安全占位语义:不知道距离就不前冲 | | `exit` | 持续无距离达 `no_distance_exit_grace_ms` 才整体降级保持(`kDegradedHold` / `kNoDistanceExit`),不发送 | 距离是安全前提(如避撞)时的可配置安全策略;持续无距离 ≈ 传感器级故障 | 三档中只有 `exit` 会中断整拍输出,且**延迟到 grace 期满**;其余情况(含 `exit` 的 grace 期)仍输出航向与垂直通道(继续把目标保持在画面中心)。 **`exit` 门限语义**(替代旧版"首个无效拍即退出"): - `distance_available` 首次为 `false` 起计时,任一有效距离帧**重置**计时; - grace 期内前向按 `slow_approach` 低速推进,复用既有加速度限幅,`vx` 平滑 过渡无跳变; - 判丢刹车期间不计时(每拍清计时状态),`Reset()` 同样清除; - `no_distance_exit_grace_ms = 0` 保留旧版行为(首个无效拍即退出)。 --- ## 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 | 视觉结果超龄阈值 | | `distance_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` | 枚举 | `slow_approach` | `hold` / `slow_approach` / `exit`(默认档依据见 §10.8) | | `no_distance_approach_limit_mps` | m/s | 0.5 | `slow_approach` 档(含 `exit` 的 grace 期)的限速值 | | `no_distance_exit_grace_ms` | ms | 3000 | `exit` 档持续无距离门限(0 = 首个无效拍即退出) | ### 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:降级是"这一拍不输出",恢复后应平滑续接;刹车是真实控制中断, 恢复后必须从零渐变。两者语义不同,清理策略必须不同。 ### 10.8 距离输入从雷达切换为双目:语义简化、默认动作与退出门限(2026-09-10) **背景**:目标距离的机载唯一来源改为双目立体测距(决策 D2,替代激光雷达; 见 `docs/双目深度估计思路.md`)。 1. **关联标志语义简化(S2)**:`associated_to_target`(相机—雷达空间关联) → `distance_valid`(本帧测距可用)。双目距离来自目标检测框自身,"回波未 打到目标"的跨模态关联问题消失,标志退化为纯粹的本帧可用性(域外/未匹配/ 一致性校验失败);track_id 一致性校验转为调用方约定(§12.2)。 2. **默认无距离动作 `hold` → `slow_approach`(D4)**:单雷达时代无距离多为 瞬时丢关联,`hold` 占位安全;双目时代有效域外无距离是**常态**,`hold` 会让 远场逼近停滞。`slow_approach` 即"方位闭环 + 定速接近"远场策略的库内承载。 3. **`exit` 增加持续门限(S1)**:旧版首个无效拍即降级,会把单帧丢检误判为 传感器级故障;改为持续无距离达 `no_distance_exit_grace_ms` 才退出,grace 期 按 `slow_approach` 行为执行(复用加速度限幅,无跳变),任一有效帧重置。 `exit` 语义收敛为"持续无距离 ≈ 传感器级故障"。 --- ## 11. 实现与重写指引 ### 11.1 必须持久化的状态(漏一个就变行为) | 状态 | 所属 | 初值 | 何时改变 | |------|------|------|---------| | 上拍时刻 | 主控律 | 0(表示首拍) | 每拍写入当前时刻 | | 上拍输出模式 | 主控律 | 降级保持 | 仅模式切换时写入(并触发日志) | | 无距离计时(进行中 + 起始时刻) | 主控律 | 未计时 | 首个无距离拍启动(`exit` 档);有效距离帧 / 判丢刹车 / 复位() 清除 | | 积分累计 | 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 | 若需要侧移则新增通道 | 低 | | **机体补偿** | 快照的滚转/俯仰/角速度字段预留未用 | 接入主工程姿态源后评估 | 低 | | **相机标定** | 内参/畸变/安装外参未标定,针孔模型无畸变补偿 | 标定 | 中:视线角系统性偏差 | | **track_id 一致性校验** | 调用方约定:装配层校验双目距离与追踪主目标的 `track_id` 一致并告警,库内不实现(S3) | 主程序装配层落地时加一处校验 | 中:错配会把其他目标的距离当作本目标,距离通道朝错误目标调射程 | ### 12.3 红线 1. **未标定不得实飞**:配置与代码默认值中所有数值均为占位,标定与实测整定 完成前,仅限 SITL / 台架 / 录制数据回放验证数学链路。 2. **不得混用未标注坐标系的数据**:对外输出的速度分量已声明为局部 NED, 任何改动都必须同步更新声明与打包侧坐标框。 3. **不得用超龄数据生成正常指令**:超龄只能触发降级或告警。 4. **不得绕过限幅**:任何新增通道都必须内联"变化率 + 幅值"双重限幅, 包括切换后的首条指令。 5. **`distance_valid == false` 的距离不得当作目标距离**(v1.0 仅单目检出时 不发布距离,方位退化为该目偏移,见 `docs/双目深度估计思路.md` §11.3)。 --- ## 附:与追踪库的行为衔接速查 | 追踪库输出 | 本模块响应 | 位置 | |-----------|-----------|------| | `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 |