结论:横向的四种控制器不共享相同单位——角度、曲率与归一化扭矩只能在各自的 CarParams 契约下解释,不能互相套用阈值;纵向的「最小加速度」合成是偏保守的候选合成,shouldStop 与所选加速度甚至不一定来自同一候选。本文还特别指出:非零求解状态会记录告警并 reset(),但清零后的轨迹仍会被规划器继续处理。以下数值均为源码设定,不是新硬件或新车型的合格阈值;本地缺少 acados 生成代码与模型实体,未构建求解器、未在车辆上验证。
控制器选择与横向实现
Controls.__init__() 按 CarParams.steerControlType 优先选择角度或曲率接口,否则按 lateralTuning 选择 PID 或扭矩控制器。Controls.state_control() 根据使能、转向故障、静止条件计算 latActive,由 clip_curvature() 限制曲率变化率、横向加速度和绝对曲率后再调用控制器;这一路仍不是最终 CAN 发送许可。
| 实现 | 关键函数与算法 | 状态或故障边界 |
|---|---|---|
| 公共基类 | LatControl._check_saturation() 对控制器饱和或曲率受限按 dt 积分;速度、驾驶员施力及安全限幅可抑制计时 |
reset() 仅清 sat_time,不会自动清子类 PID、延迟缓冲或滤波器 |
| 转角接口 | LatControlAngle.update() 用车辆模型把目标曲率转为方向盘角并加学习角偏置 |
返回的是目标角;执行器变化率仍由车型控制器与 panda 策略约束;角偏置与车辆模型必须随车型校准 |
| 曲率接口 | LatControlCurvature.update() 从当前转角估计曲率;配置 PID 时做误差闭环,未配置或驾驶员施力时用前馈 |
输出限到 MAX_CURVATURE 的 PID 范围,不能把曲率值当作扭矩 |
| 转角 PID | LatControlPID.update() 以目标/当前转角误差闭环,转角前馈不含角偏置 |
安全限幅、驾驶员施力或低速时冻结积分;停用期间不调用 PID update(),原积分未由此清零 |
| 扭矩控制 | LatControlTorque.update() 把目标曲率换算成横向加速度,在该域做 PID,并补偿 roll、学习偏置、摩擦和前瞻 jerk |
使用 1 秒目标加速度环形缓冲及 jerk 滤波;停用时仍更新缓冲/滤波但不调用 PID,继承的 reset() 不清积分 |
PIDController.update() 按速度插值增益,组合 P/I/D 与前馈,对输出限幅并在已限幅方向抑制积分增长;其积分候选也直接以输出上下限裁剪,因此正下限或动态收紧限幅时的内部状态不能按普通「剩余裕度」积分推断。还应注意扭矩控制器的延迟 deque 在停用时仍追加目标,短停再启用可能先读到旧设定值;原测试只覆盖停用满一秒后的缓冲清零。四种横向实现中,扭矩控制的返回值还有符号翻转。
还有两个跨层边界需要记住:其一,基类 reset() 只清 sat_time,不会清子类 PID、延迟缓冲或滤波器,因此「调用过 reset()」不等于内部状态全部归零;其二,selfdrived 还有独立的欠跟踪、转弯与驾驶员施力门,它们与控制器自身的饱和计时不是同一套判据。插件替换任一横向实现时,若不保留这些内部状态与门控语义,就可能出现停用再启用后的状态残留。
纵向 MPC、候选合成与执行
纵向链的关键在于层层限幅与状态门:MPC 生成轨迹,规划器在候选里选最小加速度并做车辆范围限幅,执行层再按车型加速度范围裁剪。任何一层都可能把上游的意图收窄,因此「规划器输出」与「最终发给车型控制器的目标」并不等价。此外,无前车时使用虚拟高速前车是为了维持同一求解模式,而不是真的存在一个前车目标。
| 层次 | 关键函数与数据流 | 失效语义 |
|---|---|---|
| 求解器构建 | gen_long_model() 设位置/速度/加速度状态和 jerk 控制量;gen_long_ocp() 配目标距离、速度/加速度/障碍约束和软约束成本;SConscript 生成/链接 acados Cython 扩展 |
生成代码本地缺失;仅阅读 Python 建模不足以证明求解器数值、收敛或耗时 |
| 前车目标 | LongitudinalMpc.process_lead() 处理雷达前车;无前车时用虚拟高速前车维持同一求解模式 |
crash_cnt 用 5 秒内预测距离和前车模型概率累积,规划层再阈值化 FCW;不是独立碰撞检测认证 |
| 求解失败 | LongitudinalMpc.run() 设置每节点参数与初始约束、取解及耗时 |
非零求解状态按条件记录警告并 reset();解轨迹被清零,规划器仍会处理清零轨迹 |
| 候选计划 | LongitudinalPlanner.update() 用 MPC 轨迹、巡航加速度及实验模式 e2e 加速度组成候选,选最小加速度并做车辆范围限幅 |
所选加速度与 shouldStop 不一定来自同一候选;这是保守合成,不是可绕开车型限制的插件接口 |
| 纵向执行 | long_control_state_trans() 在 off、stopping、pid 间切换;LongControl.update() 积分/前馈控制并在车型加速度范围裁剪 |
输出仍需车型编码及硬件安全门,不能当成直接制动扭矩 |
get_accel_from_plan() 与 get_curvature_from_plan() 将模型离散轨迹插值到动作时刻;get_cruise_accel() 综合巡航误差、弯道横向加速度、滑行/油门许可与 jerk 限制。插件替换规划器必须保留时间轴、单位、滞后补偿和输出有效位语义。
变道与警告状态
DesireHelper.update() 要求横向控制活动、单侧转向灯新沿和最低车速进入准备状态;驾驶员同向施力且无盲区报警后进入变道开始,再结合模型概率、时间与灯状态回到准备或关闭。它输出模型 desire,不直接授权转向。LaneDepartureWarning.update() 在足够车速、近期无转向灯且横向控制未活动时,按车道置信度、距离和变道意图置警告,输出同样不是执行器命令。
测试与插件边界
现有纵向状态测试、横向饱和测试与扭矩缓冲测试只覆盖指定车型和局部状态,本轮运行了纵向隔离探针但未运行上述原仓库测试。重构时应分别记录四种横向控制器的内部 PID/缓冲状态、规划器 MPC 状态、车型学习参数和最终 carOutput,并用进程回放、SIL/HIL 及故障注入验证停用、重启和切换。需要明确:selfdrived 的使能/退出与 panda 固件安全限制不能由普通控制插件覆盖。
常见问题
横向控制器的曲率值可以直接当扭矩用吗?
不能。四种横向实现不共享单位:角度输出的是方向盘目标角,曲率输出被限到 MAX_CURVATURE 的 PID 范围,扭矩控制输出的是归一化扭矩,只能各自在对应 CarParams 契约下解释。
纵向求解失败后会发生什么?
run() 遇到非零求解状态会按条件记录警告并调用 reset(),将解轨迹与告警时间戳清零;探针确认连续失败可逐次记录,规划器仍会处理清零后的轨迹,实机故障频率未验证。
「最小加速度」合成等于安全保证吗?
不等于。它是偏保守的候选合成,所选加速度与 shouldStop 甚至可能来自不同候选;求解器失败、输入无效、车型加速度能力和实际制动执行仍分别由其他层处理。
停用再启用横向控制会保留旧的积分或缓冲吗?
可能。继承的 reset() 不清子类 PID 积分、延迟缓冲或滤波器;扭矩控制器的延迟 deque 停用时仍追加目标,短停再启用可能先读到旧设定值,再使能状态需实测。
延伸阅读
- openpilot 的 100 Hz 控制回路:开源 ADAS 能给车载设备什么 —— 承接本文控制频率与实时性约束。
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 解释控制器输出之后为何还有独立安全门。
- openpilot 感知到控制:四条数据流与 100 Hz 闭环 —— 本文算法所处的主链路上下文。
- 车辆消息字段契约:CarState、CarControl、CarOutput 与 CarParams —— 本文
carOutput与调参字段的来源。 - 同一个「转向灯」在三家车厂是三种实现:车辆状态归一化的坑 —— 与本文「四种横向单位不可互套用」的差异来源同源。
参考资料
- 项目 02 ——
openpilot-research/CONTROL_ALGORITHMS_DEEP_DIVE.md(取用了四种横向控制器、纵向 MPC 与候选合成、变道与警告状态、测试覆盖与插件边界)。 - 项目 02 ——
openpilot-research/PERCEPTION_CONTROL_DEEP_DIVE.md(仅用于确认感知到控制主链路与控制器位置)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11