openpilot横向控制怎么做到的?期望曲率限速+延迟补偿,把方向盘扭矩误差压进米级车道
【免费下载链接】openpilotopenpilot is an operating system for robotics. Currently, it upgrades the driver assistance system on 300+ supported cars.项目地址: https://gitcode.com/GitHub_Trending/op/openpilot
高速过弯时车身明显偏向车道外侧,出弯又猛地拉回来;或者方向盘在直线上一阵阵地抖动——这些你大概率体验过的车道保持毛病,在 openpilot 的横向控制里对应三个工程问题:目标曲率从哪来、模型输出有延迟怎么补偿、300 多台不同转向执行器的车怎么共用一套控制器。下文沿着一条控制环路把这三件事拆开,代码全部来自 selfdrive/controls 的真实实现。
从神经网络输出到被"钳住"的期望曲率
先回答目标从哪来。controlsd 的 100Hz 主循环里,期望曲率直接取自模型服务的输出,再经过一道限速处理:
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature self.desired_curvature, curvature_limited = clip_curvature(CS.vEgo, self.desired_curvature, new_desired_curvature, lp.roll)也就是说,横向控制器的输入不是车道线位置,而是一个标量:期望曲率(单位 1/m,数值越大弯越急)。车道线到曲率的换算被上移到了模型层,控制器只管跟踪。
两级钳位:先限jerk,再限横向加速度
clip_curvature 是整个环路上唯一会修改目标值的地方,逻辑就三层:
max_curvature_rate = MAX_LATERAL_JERK / (v_ego ** 2) # EU guidelines new_curvature = np.clip(new_curvature, prev_curvature - max_curvature_rate * DT_CTRL, prev_curvature + max_curvature_rate * DT_CTRL) roll_compensation = roll * ACCELERATION_DUE_TO_GRAVITY max_lat_accel = MAX_LATERAL_ACCEL_NO_ROLL + roll_compensation new_curvature, limited_accel = clamp(new_curvature, min_lat_accel / v_ego ** 2, max_lat_accel / v_ego ** 2)第一层用 5 m/s³ 的横向加加速度(jerk,即横向加速度变化的快慢)限制曲率每帧的最大变化量——曲率变化率上限按 v² 反比缩放,车速越快,允许的方向变化越缓。第二层用 3 m/s² 的横向加速度上限(EU 法规推荐值)封顶,并叠加车身 roll 角产生的重力分量做补偿:坡道上的车本来就有一部分侧倾,这部分不该算进"控制量"里。第三层是 0.2 1/m 的绝对曲率上限,约等于大多数车都达不到的小转弯半径。
这里的取舍值得说清楚:openpilot 没有在控制环内再跑一个曲率优化器,而是用三次 O(1) 的 clamp。原因是这段代码每 10ms 执行一次,优化器解出来的"最优曲率"收益有限,而硬钳位保证任何时刻输出都合规,行为可预测、可调试。舒适性约束在目标端一次解决,后面所有控制器继承同样的上限。
curvature_limited 信号:被钳位本身是信息
注意 clip 函数返回的第二个值limited_accel。曲率被加速度上限截断说明"目标比车当前能力更急",这个布尔量会一路传到控制器的饱和检测里(后文会看到),用来区分"我没跟上"和"物理上做不到"。
延迟补偿:1秒环形缓冲与摩擦前馈
目标曲率到手了,为什么不能直接拿它和当前状态做 PID?因为从摄像头成像到 CAN 报文发出,模型链路有几十毫秒的固有延迟。如果你用"现在"的期望值去对"现在"的车况,控制器永远在追一个已经过去的目标——这就是出弯猛拉回的元凶。
用过去缓冲里的值当 setpoint
latcontrol_torque.py 维护了一个 1 秒长的横向加速度请求环形缓冲,setpoint 不是最新值,而是按实测延迟倒查出来的历史值:
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 self.lat_accel_request_buffer.append(future_desired_lateral_accel) delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.lat_accel_request_buffer_len)) expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] setpoint = expected_lateral_accel error = setpoint - measurementlat_delay由独立的 delay 估计进程给出,再叠加模型平滑时间LAT_SMOOTH_SECONDS。倒查delay_frames帧的意思是:当前车态是由这个"过去时刻的期望"造成的,用它当参考点,误差才是真实误差。这是整个控制器里最容易被低估的一段——它没有增加任何计算量,却把开环延迟从误差里拿掉了。
误差为什么开在横向加速度空间
文件头部的注释写明了设计假设:车速超过约 25mph 时,同一辆车施加相同转向扭矩获得的横向加速度基本恒定,与轮侧打滑和车速都近似无关。因此 PID 的 P 项可以按"低速大、高速小"插值,而不是按角度误差:
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] # KP = 0.81 m/s 时 P 增益是 250,30 m/s 时降到 0.8,差两个数量级——低速时方向盘摩擦占比极高,必须硬掰;高速时小角度就够。如果直接在方向盘角度空间做 PID,低速摩擦会让增益曲线完全失真,这是选加速度空间的核心原因。
摩擦、roll 与 jerk 前馈
误差项只解决"差多少",前馈项负责"车本来就要花多少力":
ff = gravity_adjusted_future_lateral_accel - self.torque_params.latAccelOffset ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)get_friction按方向叠加摩擦估计,lateral_accel_deadzone由方向盘死区角steeringAngleDeadzoneDeg换算而来,专门吃掉机械间隙;latAccelOffset修正设备与车身 roll 不一致带来的固定偏置。另外注意到 jerk 项:desired_lateral_jerk是从缓冲里对曲率请求做中心差分再经 1.2Hz 低通滤波得到的,乘以 JERK_GAIN=0.3 提前加进摩擦阈值判断——弯道里摩擦方向会突变,提前 0.19s 看 jerk 趋势可以少抖一下。PID 输出的是目标横向加速度,最后才用torque_from_lateral_accel映射回扭矩,这样扭矩-加速度的非线性曲线在闭环外侧被消化,环路内部始终是线性空间。
一套控制器接口,四种执行通道
按转向控制类型分派
300 多台车的执行器接口并不一样:有的 CAN 通道下发扭矩,有的下发方向盘转角,有的(特斯拉系)直接下发曲率。controlsd 里按 CarParams 一次性分派:
if self.CP.steerControlType == car.CarParams.SteerControlType.angle: self.LaC = LatControlAngle(self.CP, self.CI, DT_CTRL) elif self.CP.steerControlType == car.CarParams.SteerControlType.curvature: self.LaC = LatControlCurvature(self.CP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'pid': self.LaC = LatControlPID(self.CP, self.CI, DT_CTRL) elif self.CP.lateralTuning.which() == 'torque': self.LaC = LatControlTorque(self.CP, self.CI, DT_CTRL)四种实现共享 LatControl 基类的update签名和饱和检测。角度型走纯前馈(曲率经车辆模型换算成转角,不做闭环);曲率型在曲率空间做 PID 或纯前馈;扭矩型就是上一节的完整实现。取舍很实际:与其写一个能兼容所有接口的通用控制器,不如让每个控制器贴着自家执行器特性写,把公共部分收敛到基类的十几行里。
饱和检测:什么时候该报警
所有控制器共用同一段饱和计数逻辑,它决定 UI 是否提示"方向盘扭矩受限":
if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed \ and not steer_limited_by_safety and not CS.steeringPressed: self.sat_time += self.dt else: self.sat_time -= self.dt self.sat_time = np.clip(self.sat_time, 0.0, self.sat_limit)三个排除条件各有含义:steer_limited_by_safety是安全模块(panda)主动限幅,说明控制器没失控、是策略限制;steeringPressed是驾驶员在抢方向盘;低速(<10 m/s)不统计,因为摩擦会让输出天然顶满。饱和必须持续累计到CP.steerLimitTimer才上报,瞬时顶满不算事。
在线标定参数热更新
扭矩型控制器的三个参数(latAccelFactor、latAccelOffset、friction)不是静态配置文件,而是独立的lateralTorqueParameters服务在行车中持续估计(斜跑、转弯工况下回归),controlsd 每帧检查并热更新。换车、方向盘老化、摩擦系数漂移,环路自己会跟上。
参数与权衡
集中在 drive_helpers.py 和 latcontrol_torque.py 里的关键可调项:
| 参数 | 作用域 | 当前值 | 调整效果 |
|---|---|---|---|
MAX_LATERAL_JERK | 曲率限速 | 5.0 m/s³ | 调小→入弯更柔但出弯滞后更长;调大→响应快、侧倾感强 |
MAX_LATERAL_ACCEL_NO_ROLL | 曲率限速 | 3.0 m/s² | 舒适/稳定边界,接近 EU 推荐值 |
MAX_CURVATURE | 曲率限速 | 0.2 1/m | 绝对转弯能力上限,基本不动 |
KP_INTERP/KI | 扭矩 PID 增益 | 250→0.8 / 0.15 | 低速段主导"掰得动",高速段主导"不抖" |
JERK_GAIN | jerk 前馈 | 0.3 | 弯道摩擦预判强度,过大会引起方向微振 |
JERK_LOOKAHEAD_SECONDS | jerk 前馈 | 0.19 s | 提前量,与链路延迟匹配 |
LAT_ACCEL_REQUEST_BUFFER_SECONDS | 延迟补偿 | 1.0 s | 缓冲长度上限,lat_delay 不可超过它 |
LP_FILTER_CUTOFF_HZ | jerk 滤波 | 1.2 Hz | 截止频率,低→前馈迟钝,高→噪声进前馈 |
sat_check_min_speed | 饱和统计 | 10 m/s | 低于此速不统计饱和,避免摩擦误报 |
STEER_ANGLE_SATURATION_THRESHOLD | 角度型饱和 | 2.5° | 指令角与实际角差值超此值判饱和 |
friction/latAccelOffset | 前馈(在线) | 运行时估计 | 由 torqued 回归,一般不手调 |
规律是:舒适性参数(jerk/accel 上限)全局一处生效,执行参数(增益/摩擦)按车型分派。动全局参数前,先看曲率限速是否成为瓶颈——curvature_limited被置位的频率可以直接从 controlsState 日志里统计。
验证与实测
量化边界本身就是这套系统的成绩单:横向加加速度 ≤5 m/s³、横向加速度 ≤3 m/s²(不含 roll)、曲率 ≤0.2 1/m,全部在 drive_helpers.py 里以常数形式硬约束;控制环周期 100Hz(controlsd.py 中Ratekeeper(100)),延迟补偿窗口覆盖 1 秒链路延迟。
复现与回归测试在 selfdrive/controls/tests/:
- test_latcontrol.py:四种控制器的输出范围与饱和行为
- test_latcontrol_torque_buffer.py:环形缓冲的 setpoint 倒查逻辑
- test_torqued_lat_accel_offset.py:roll 偏置在线回归的正确性
- test_following_distance.py:横向与纵向耦合下的跟随边界
想在自己的车上验证,最直接的路径是回放:用 tools/replay 跑历史 qlog,或用 tools/plotjuggler 打开 controlsState 流,desiredCurvature、actualLateralAccel、output、saturated四条曲线对齐看,延迟补偿是否生效一眼可见(setpoint 与 error 应该在弯道建立期同相,而不是滞后一个弯)。
局限与演进
当前方案的两个已知边界:一是低速(<5 m/s)时 PID 积分器冻结、且多数车minSteerSpeed直接禁用横向控制,蠕行跟弯靠的是原车 ACC 或驾驶员;二是延迟补偿完全依赖lateralDelay估计的准确性,延迟估计失准时 setpoint 会整体偏帧,表现成规律性的 S 形过冲。社区方向上,扭矩参数在线回归(torqued)正在吸收这类缓慢漂移,后续把延迟估计也并入同一套在线辨识,是控制环路收敛的下一步。
一句话收束
openpilot 横向控制的工程内核,是把"舒适性约束钳在目标端、链路延迟查表补偿、执行器非线性关在环外"这三件事做到 100Hz 稳定,换来曲率、jerk、摩擦三条曲线全部可观测、可回归。想动手,从 openpilot/selfdrive/controls/lib/ 四个控制器文件入手,再配合 docs/how-to 里的 replay-a-drive 跑一遍回放,半天就能把整套环路的波形摸清楚。
【免费下载链接】openpilotopenpilot is an operating system for robotics. Currently, it upgrades the driver assistance system on 300+ supported cars.项目地址: https://gitcode.com/GitHub_Trending/op/openpilot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考