标签: 控制执行

  • 相机帧号的三重编号:request_id、IFE 帧号与发布帧号为什么不能混用

    结论:相机链路里同时存在三个不同的编号——request_id(req_mgr 队列请求号)、内核 IFE frame_id(每次 SOF 递增的内核计数)与归一化后的published frame_id。只有第三个才是跨相机配对可用的键;直接拿原始 IFE 计数当配对键,在座舱与路面这类错相路之间会整体错位。三路 SOF 的预期关系是:非错相路之间相差 0,座舱与路面路之间相差 25 ms,误差阈值 0.2 ms。此外 timestamp_eof 是「SOF 加读出时间」推算的值,不是硬件 EOF 事件,processing_time 也不能当作曝光时长。以下均为固定快照的静态结构分析,未运行相机、模型或实板测试。

    三个编号:request_id、IFE 帧号与发布帧号

    handle_camera_event()从 req_mgr 的 SOF 事件里一次读出三样东西:request_id、内核 IFE frame_id 和内核 IRQ 时间戳。request_id 是队列请求号,IFE frame_id 是每次 SOF 递增的内核计数,两者都不是最终发布给模型的帧号。validateEvent()在 skip_expected 为假时分别检查两种序号是否各自连续;request_id == 0 会被跳过,连续无效超过环深加二就触发重排。

    真正决定跨相机对齐的是 syncFirstFrame():它为每一路记录首次 SOF 时间,并把 ife_frame_id + 1 存为该路的帧号偏移;等所有已启用相机都到齐且时间关系满足后,才设置共享的同步完成标志。processFrame()此后才计算 published frame_id = ife_frame_id - frame_id_offset。换句话说,进入同步状态的那一次调用仍返回 false,下一帧才可能成为首个发布帧。

    编号 来源 是否可作跨相机配对键
    request_id req_mgr SOF 事件读出的队列请求号 否;request_id == 0 会被跳过,仅供本路连续性检查。
    IFE frame_id 每次 SOF 递增的内核计数 否;三路 IFE 计数可以不同,不能直接当配对键。
    published frame_id ife_frame_id - frame_id_offset 是;偏移在首帧对齐时按路记录。

    还有一条恢复边界:若某路的 IFE 帧号已超过 40 且仍未对齐,代码会记错误并把共享同步标志置为已同步,但没有在该分支证明各路满足了 0.2 ms 关系。此时若某已启用路尚无首帧记录,它后续首次进入已同步分支时用的偏移状态需要另行动态验证。

    时间域:SOF、推算 EOF 与处理时间

    发布出去的几个时间字段语义差别很大。timestamp_sof 来自内核 SOF IRQ,是 modeld 配对窄路与广路图像的依据;timestamp_eof 则是 processFrame() 用 SOF 加 readout_time_ns 推算出来的,读出的只是估计值而非硬件 EOF 事件。

    字段 来源与含义 下游影响
    timestamp_sof handle_camera_event()读取的 SOF IRQ 时间 modeld 按它配对窄/广路图像。
    timestamp_eof processFrame() 用 SOF 加 readout_time_ns 推算 cameraOdometry 携带它,locationd 再减模型姿态延迟并做时间校验。
    processing_time 等待 fence 后,以当前单调时间减估计 EOF 写入相机状态;不能当曝光时长或端到端控制延迟。
    VisionIpcBufExtra sendFrameToVipc() 附带归一化帧号、SOF、EOF encoderd 核对缓冲帧号与通知帧号,失配时跳帧。

    读出时间本身是传感器相关常量:OS04C10 与 OX03C10 各有自己的取值。开发版的 webcamerad._send_yuv() 把 frame_id * 0.05 s 同时当作 SOF 和 EOF,状态消息只带帧号和单位变换——它能替代输入用于开发,却不能验证硬件时钟同源、真实读出时间或三路同步。

    同步建立:25 ms 错相、0.2 ms 阈值与 40 帧兜底

    同步阈值集中在一处配置里,含义需要分清「预期差值」与「允许误差」。非错相路之间预期相差 0;座舱路与路面路之间预期相差 25 ms;可接受的误差是 0.2 ms。这些是当前实现与测试里的常量,不是任意硬件的通用标准。

    量 取值 说明
    非错相路预期差 0 同相两路应同时曝光。
    座舱与路面预期差 25 ms 错相路的标称错位。
    同步误差阈值 0.2 ms 判断某路是否满足时间关系。
    未对齐放弃门 40 帧 超过后置同步标志,但不证明各路满足 0.2 ms。

    重排会记录 last_requeue_ts,并允许预期中的序号空洞出现。收到过旧 SOF、故障注入跳过、序号不连续,或 waitForFrameReady() 等待 IFE/BPS fence 失败时,当前帧都不会发布。这些行为的共同点是:链路并不保证「图像流始终连续」。

    消费者各用各的同步策略

    同一个相机流,三个消费方用的对齐策略并不统一。modeld 先取得比上一广角 SOF 至少晚 25 ms 的主帧,再读取广角帧;两路 SOF 相差超过 10 ms 时记录错误日志,但该分支仍会进入后续推理,没有直接丢弃这对图像。模型 frameId 来自 VisionIPC 主帧,frameIdExtra 来自另一路,而 frameAge 是拿异步收到的 narrowRoadCameraState.frameId 与主帧比较,不能误读成同一缓冲的自身年龄。

    encoderd.sync_encoders() 在启动时选跨路共同起始帧,并在 encoder_thread() 里按帧号决定切段,它不按 SOF 重新校正物理曝光时刻。因此相机帧号重置、断流重连或缺一路时,编码器启动门与模型配对门是否仍符合预期,需要单独验证。相机状态消息则先发布 VisionIPC、再填充发送状态消息,两条传输路径不应仅凭到达顺序关联,而应以帧号和时间戳核对。

    硬件插件接口与验收

    建议相机插件输出同一采集 epoch 下的 stream_id、request_id、原始硬件帧号、归一化帧号、单调时钟域 SOF、EOF 来源(实测或估计)、读出时间、缓冲有效性和同步状态,并把 sync_lost、重排代数与丢帧原因作为可观测事件。平台适配器负责时钟换算、帧号归一化和缓冲所有权;模型与编码插件只消费明确标注时间质量的帧,不可把 webcam 合成时间或日志发布时间暗中转成硬件 SOF。

    现有 test_camerad.py 是硬件门控测试,检查帧号连续、窄/广路 SOF 差小于 1.1 ms、座舱错相 20–30 ms、EOF 晚于 SOF,并有故障注入下的序号跳变检查;这些测试本轮未运行。新硬件还应在台架与实车分别测试启动缺一路、40 帧超时、掉帧与重排、时钟漂移、读出时间误差、IPC 复用以及编码与模型重连。

    常见问题

    IFE 帧号能直接当两路相机的配对键吗?

    不能。IFE frame_id 是三路各自递增的内核计数,彼此可以不同;跨相机配对要用减去首帧偏移后的 published frame_id,而偏移由 syncFirstFrame() 在首帧对齐时逐路记录。

    timestamp_eof 是硬件 EOF 事件吗?

    不是。它是 processFrame() 用 SOF 加 readout_time_ns 推算出来的估计值,读出时间随传感器不同(OS04C10、OX03C10 各有取值);processing_time 也不是曝光时长。

    座舱与路面路的错相预期是多少?

    非错相路之间预期相差 0,座舱路与路面路之间预期相差 25 ms,误差阈值 0.2 ms。这是当前实现与测试的常量,不是任意硬件的通用标准。

    两路 SOF 相差超过 10 ms,模型会丢帧吗?

    源码在该分支记录错误日志,但仍进入后续推理,并没有直接丢弃这对图像。实际的丢帧根因需要区分采集、IPC 与模型消费落后三种情况。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAMERA_FRAME_TIME_SYNC_CONTRACT_DEEP_DIVE.md(取用了三个编号的定义、首帧对齐偏移、25 ms/0.2 ms/40 帧阈值、SOF 与推算 EOF 字段表、消费者同步策略差异与硬件测试项)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 公共运行时原语:DT 常量、Ratekeeper 与 I2C 契约

    结论:common/ 里的时间常量、滤波器、PID、系统统计与 I2C 只是被复用的基础函数,它们不是独立进程,也不能单独证明端到端实时性。DT_CTRL 等定义的是名义时间步长,不是操作系统保障的周期;monitor_time() 只记录进度与超期,keep_time() 才在剩余时间为正时睡眠。因此「没有 lag 日志」并不能理解为满足控制时限。

    一、时间常量、优先级与调度

    原语 具体行为 调用边界
    时间常量 DT_CTRL 等定义 100/20/2/20 Hz 的名义时间步长。 控制器、模型、hardwared 和驾驶员监测用作状态更新或滤波参数;不是操作系统保障的周期。
    Priority 定义控制进程两级 FIFO 数值。 controlsd、card 等选择 CPU/优先级;数值只对当前平台布局有意义。
    config_realtime_process() 禁用 GC;非 PC Linux 调用 SCHED_FIFO 并设 CPU 亲和性。 modeld、controlsd、selfdrived、card 等入口;PC 上仍禁用 GC,但跳过内核调度设置。
    drop_realtime() / set_core_affinity() 恢复 SCHED_OTHER;单独改 CPU 亲和性。 UI 部分操作、Athena、上传器等避免抢占高优先级控制任务。
    Ratekeeper.monitor_time() / keep_time() 前者用单调时间累积下一帧截止点、记录移动平均和超期;后者仅在剩余时间为正时睡眠。 card、controlsd、selfdrived 调用 monitor_time(),不调用 keep_time();轮询传感器调用 keep_time()(sensord.py)。

    MovingAverage.add_value()/get_average() 保留 100 个间隔的移动平均;Ratekeeper.lagging 在均值超过名义间隔约 11.1% 时为真——该布尔值不等于最坏延迟或丢消息率。

    二、滤波与 PID

    原语 关键公式 / 行为 使用注意
    FirstOrderFilter.update() 使用 alpha=dt/(rc+dt);initialized=False 时直接接受首样本。 调用方传固定 dt,无自动适配实际帧间隔;hardwared 温度链采用 initialized=False。
    BounceFilter.update() 在一阶位置滤波上加速度/阻尼状态,并按 60 FPS 基准缩放。 主要用于按钮、滚动、键盘等界面动画;不应误用作车辆动态滤波。
    PIDController.update() 按速度插值 P/I/D 增益,积分项按 rate 固定步长推进并做抗积分饱和,最后限幅。 横向/纵向控制和风扇;实际调用周期偏离配置速率时,积分动态会变化。
    LinuxSystemStats cpu_usage_percent()/memory_usage_percent() 读 /proc/stat 与 /proc/meminfo。 CPU 新出现或计数回退时报告 0;缺字段会抛异常;它是遥测输入,不能代替控制循环的 99.9 分位时延。

    三、运行时约束与故障边界

    原生线程侧还有一组 C++ 入口:set_realtime_priority() 与 set_core_affinity() 对当前 Linux TID 设置 FIFO/CPU,非 Linux 返回 -1。源文档给出两种态度:pandad 在非 PC 上要求设置成功(使用断言),而 camerad 只设亲和性、驻车时允许失败。这也说明「能否设置实时优先级」是平台能力问题,普通插件不应默认拥有该权限。

    Ratekeeper 的 _next_frame_time 按固定间隔递增,循环落后时不会跳帧重设截止点;连续超期可能积累负的 remaining。当 print_delay_threshold=None 时,它不会打印超期,也不会让 monitor_time() 返回 True,但仍计算 remaining 和窗口均值——所以不能把「无 lag 日志」理解为满足控制时限。config_realtime_process() 的内核调用失败会抛异常;C++ 侧对 pandad 使用断言要求成功,而 camerad 只设亲和性、驻车时允许失败。

    源文档的六项原函数/假时钟与调度探针给出一个直观例子:10 Hz 下首次正余量约 0.1 秒,后续某轮 0.25 秒的迟到仍让下一截止点只加 0.1 秒,连续轮次继续报迟到;keep_time() 只为正余量调用睡眠。config_realtime_process() 先禁用 GC,再尝试 FIFO,最后设置 CPU 亲和性——若 FIFO 调用抛错,GC 已禁用而亲和性尚未设置。探针不使用真实内核优先级或时钟,因此不证明车端尾延迟。

    FirstOrderFilter 默认把 x0 当有效初值,固定 dt 意味着跳帧/超时后仍按一次名义更新,不会自动按真实经过时间补偿。PIDController 的积分步长也在构造时固定为 1/rate,所以硬件移植和插件执行线程必须记录实际间隔,再用回放/故障注入验证误差与饱和恢复。SMBus.read_i2c_block_data() 在发 ioctl 前限制请求为 0–32 字节,回读长度上限为请求长度;底层没有统一重试、设备健康位或锁,共享总线上的驱动冲突与电气隔离由上层/平台负责。

    四、插件化落点与验证

    新增可运行插件应在 manifest 中声明输入频率、最大处理时长、消息最大年龄、CPU/内存上限和故障处理;受信任调度器保留唯一控制权。SCHED_FIFO/亲和性仅供经过基线测试的核心进程申请,普通插件默认低优先级、进程隔离。传感器插件必须明确总线地址、单位、时间基准、错误/重试语义,并在断连时发布无效/超时状态,不能把 0 遥测值当作健康证据。

    现有 common/tests/ 未找到专门覆盖这几个调度/滤波/I2C 原语的测试文件;本篇已对 Python 调度分支做隔离探针,未测真实内核调度权限、总线时序或车辆级截止时间。建议先在台架采集控制和传感器消息年龄、循环延迟分布、CPU 热限流与故障恢复,再考虑调整优先级或将插件接入热路径。若确实要把这类原语接入热路径,应记录实际调用间隔并据此校准滤波与 PID 的 dt,而不是沿用名义值。

    常见问题

    标称 100 Hz 是不是操作系统保障的周期?

    不是。DT_CTRL 等只是名义时间步长。控制类循环调用 monitor_time() 而非 keep_time(),受输入/工作负载驱动;源文档明确 100 Hz 是期望节拍,而非主动节流,更不是操作系统保障。

    没有 lag 日志是否说明满足控制时限?

    不能说明。当 print_delay_threshold=None 时既不打印超期,也不让 monitor_time() 返回 True,但仍计算 remaining 和窗口均值。因此「无 lag 日志」不能理解为满足控制时限。

    FirstOrderFilter 会在跳帧后按真实时间补偿吗?

    不会。调用方传固定 dt,alpha=dt/(rc+dt),跳帧/超时后仍按一次名义更新,不会自动按真实经过时间补偿。同理 PIDController 的积分步长在构造时固定为 1/rate。

    I2C 一次最多能读多少字节?有统一重试吗?

    SMBus.read_i2c_block_data() 在发 ioctl 前限制请求为 0–32 字节,回读长度上限为请求长度;SMBus 打开 /dev/i2c-N 后经 ioctl 读写,force=True 选 I2C_SLAVE_FORCE。底层没有统一重试、设备健康位或锁,错误透传给调用方。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/COMMON_RUNTIME_PRIMITIVES_DEEP_DIVE.md(取用了时间常量与调度、滤波与 PID、系统统计与 I2C 的函数级契约及插件化要求)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 大众 MQB panda safety:双总线按钮、冗余制动与纵向编译门

    结论:Volkswagen MQB 的 panda safety 用同一张 RX 表区分两种模式,差异几乎全在 TX 白名单:Stock ACC 模式允许在 bus 0 与 bus 2 发 GRA_ACC_01,自控纵向模式则完全不发该帧,改为允许 ACC_02/06/07。纵向配置依赖 FLAG_VOLKSWAGEN_LONG_CONTROL,且只有 ALLOW_DEBUG 构建才读取它,所以上层 CarParams 设置了纵向能力,并不能单独证明发布固件允许发送 ACC。本文为固定源码快照 c8fb906 的静态阅读,bus 号是单个 safety 实例的本地编号。

    模式与总线矩阵

    volkswagen_mqb_init() 初始化共享状态与同一张 RX 表;仅在 ALLOW_DEBUG 构建中读取纵向参数位,从而选择 Stock 或 Long 的 TX 白名单。两种模式都没有专属转发钩子,通用转发只在 bus 0 与 bus 2 之间,check_relay 地址在目标总线上被静态阻断。

    一个容易误读的点是方向:LH_EPS_03 的 RX 在 bus 0,候选 TX 在 bus 2,不能因为 ID 相同就把两个方向混为一个。

    模式 RX 关键 TX 使能来源
    Stock ACC 七组均在 bus 0 HCA_01、LDW_02 在 bus 0;LH_EPS_03 在 bus 2;GRA_ACC_01 在 bus 0 及 bus 2 TSK_06 原车巡航接合沿;主开关关闭或 GRA Cancel 退出
    自控纵向 同一七组均在 bus 0 HCA_01、LDW_02、ACC_02/06/07 在 bus 0;LH_EPS_03 在 bus 2;无 GRA_ACC_01 TX TSK_06 主开关可用时,GRA Set/Resume 释放沿进入;Cancel 退出

    RX 完整性与状态来源

    volkswagen_mqb_rx_hook() 更新移动状态、驾驶员扭矩、巡航许可、按钮、油门和两种制动观测。制动状态是 Motor_14 踏板开关或 ESP_05 压力检测的组合,之后由共享安全层处理制动退出。ESP_19 的四轮原始值求和大于零即判移动,它不形成第二车速估计。

    需要注意,忽略 checksum 与计数器的两帧仍留在 RX 存活表里,因此不能写成「完全不检查」。频率是检查配置,并非对每帧的实时 deadline 保证。

    RX 帧 检查配置 钩子消费
    ESP_19 0x0B2 8 字节、100 Hz;忽略 checksum/计数 四轮原始值求和大于 0 判移动
    LH_EPS_03 0x09F 8 字节、100 Hz;CRC + 4 位计数 带符号驾驶员扭矩采样
    ESP_05 0x106 8 字节、50 Hz;CRC + 4 位计数 制动压力超过门限的布尔值
    TSK_06 0x120 8 字节、50 Hz;CRC + 4 位计数 ACC 状态 3/4/5 视为接合,2 视为主开关可用
    Motor_20 0x121 8 字节、50 Hz;CRC + 4 位计数 原始油门字段非零
    Motor_14 0x3BE 8 字节、10 Hz;忽略 checksum/计数 踏板开关位
    GRA_ACC_01 0x12B 8 字节、33 Hz;CRC + 4 位计数 纵向 Set/Resume 释放沿,任意模式 Cancel 退出

    TX 字段门与空白

    volkswagen_mqb_tx_hook() 检查 HCA_01 的扭矩与请求位、ACC_06/07 的加速度,以及在未允许控制时 GRA 的设置/恢复位。HCA_01 由共享扭矩钩子检查最大 300、每帧上升 4 与下降 10、实时差 75、驾驶员扭矩余量 80 和请求位;LH_EPS_03 bus 2 虽在 TX 表中用于 Emergency Assist 兼容,但本模式 TX 钩子没有该帧的专属扭矩或载荷检查,其准入只受白名单和继电器故障约束。

    GRA_ACC_01 在 Stock 模式允许 bus 0/2 与 8 字节;未允许控制时只禁止字节 2 的 Set/Resume 对应位,钩子并不要求 Cancel 必须置位,也不全面约束其余按钮位。已允许控制时,这一专属按钮门不再限制这些位。ACC_06/07 只在纵向 TX 表中,两帧主加速度都用 -3.5 至 2.0 m/s² 活动范围与 3.01 m/s² 未激活编码,ACC_07 的另一加速度字段固定要求 3.02 m/s²。

    TX 帧 专属检查 空白与边界
    HCA_01 最大 300、每帧上升 4/下降 10、实时差 75、驾驶员扭矩余量 80、请求位 共享扭矩钩子生效
    LH_EPS_03(bus 2) 仅白名单与继电器故障约束 本模式无专属扭矩/载荷检查
    GRA_ACC_01 Stock 模式;未允许时禁字节 2 Set/Resume 位 不要求 Cancel 置位,不全面约束其余按钮位
    ACC_06/07 仅纵向表;主加速度 -3.5 至 2.0,未激活 3.01 ACC_07 另一字段固定要求 3.02
    LDW_02 / ACC_02 无本模式专属载荷分支 白名单不证明仪表语义或 ECU 接受

    面向自制硬件的插件契约

    车型插件应提交 MQB 平台、DBC、网关或相机线束、固件构建及纵向授权证据,未知组合保持只读。总线插件要分别标注 LH_EPS_03 的 bus 0 RX 与 bus 2 TX,以及 Stock 模式 GRA_ACC_01 的 bus 0/2 TX,不能按单一 ID 推断方向。

    转向与纵向插件只提供意图;可信任网关负责编码、节拍、HUD 与 LH_EPS_03 模板及 ECU 静默回执,最终 TX 仍由独立 MCU safety 决定。故障注入须覆盖两路制动输入失效、主开关或按钮沿错序、CRC 与计数故障、拒帧、错总线,以及纵向固件不支持时的降级。

    证据与缺口

    三项模式离线探针直接运行编译版 safety:调试构建的 Stock 模式允许 bus 2 的 GRA_ACC_01,纵向模式拒绝它;用 DBC 打包的未激活加速度 ACC_06=3.01 m/s² 仅在调试纵向模式通过。以同一 LONG 参数但无 ALLOW_DEBUG 编译后,bus 2 的 GRA_ACC_01 再次可通过、该合法未激活 ACC_06 被拒,说明该测试构建实际退回 Stock TX 表。

    这里说的「无调试宏」只表示本机 C 测试库的预处理条件,不等于目标 MCU 固件已验证。现有 test_volkswagen_mqb.py 覆盖扭矩、冗余制动、Stock 按钮、纵向加速度与按钮使能;本机运行该模块 65 项,9 项跳过,其余通过,但仍没有目标板或实车数据,不能宣称车载可用。

    常见问题

    Stock ACC 与自控纵向模式的 TX 白名单差在哪?

    Stock 模式在 bus 0 与 bus 2 发 GRA_ACC_01;自控纵向模式完全不发该帧,改为在 bus 0 发 ACC_02/06/07。两种模式的 RX 都是同一七组、都在 bus 0。

    为什么 LH_EPS_03 的 RX 在 bus 0、TX 却在 bus 2?

    这是该实例的本地总线角色分工:RX 取自 bus 0,候选 TX 在 bus 2,用于 Emergency Assist 兼容。不能因为 ID 相同就把两个方向混为一个。

    忽略 checksum 与计数器的两帧是不是等于完全没检查?

    不等于。ESP_19 与 Motor_14 虽然忽略 checksum 与计数,但仍留在 RX 存活表中,仍受地址、总线、长度与存活检查约束。

    上层 CarParams 开了纵向能力就代表固件允许发 ACC 吗?

    不代表。纵向配置依赖 FLAG_VOLKSWAGEN_LONG_CONTROL,且只有 ALLOW_DEBUG 构建才读取它;上层能力位不能单独证明发布固件允许发送 ACC。探针显示无调试宏编译时会退回 Stock TX 表。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VOLKSWAGEN_MQB_PANDA_SAFETY_DEEP_DIVE.md(取用了双模式总线矩阵、RX 完整性表、TX 字段门与空白、模式离线探针结论)。
    • 项目 02 —— openpilot-research/VOLKSWAGEN_MEB_PANDA_SAFETY_DEEP_DIVE.md(同品牌关联专题,用于对照 MQB 与 MEB 的差异,本文未展开其 Gen2 CRC 细节)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 横纵向控制算法:四种横向单位与纵向「最小加速度」边界

    结论:横向的四种控制器不共享相同单位——角度、曲率与归一化扭矩只能在各自的 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 停用时仍追加目标,短停再启用可能先读到旧设定值,再使能状态需实测。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CONTROL_ALGORITHMS_DEEP_DIVE.md(取用了四种横向控制器、纵向 MPC 与候选合成、变道与警告状态、测试覆盖与插件边界)。
    • 项目 02 —— openpilot-research/PERCEPTION_CONTROL_DEEP_DIVE.md(仅用于确认感知到控制主链路与控制器位置)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 巡航设定速度:VCruiseHelper 的 PCM 与非 PCM 两条路径

    结论:巡航设定速度(vCruise)是规划与显示的输入,不是车端已执行车速,也不代表系统已使能。源文档把它的产生拆成两条互斥路径:CP.pcmCruise=True 时直接读原车 cruiseState.speed/speedCluster 转 km/h,按键调速由原车系统负责;CP.pcmCruise=False 时由 VCruiseHelper 自己处理按键并步进。两条路径的单位(km/h)、哨兵(255/0/-1)与边界(8–145 km/h)都必须逐车型核对,不能按品牌名称猜测。

    一、数据流与单位

    Car.state_update() 在车型接口解析车况后,先用当前 sm['carControl'].enabled 调用 update_v_cruise(),在 carControl.enabled 从假变真时再用 CS_prev 调用 initialize_v_cruise()。源文档注释明确这是为了使用 selfdrived 使能那一步的按钮状态,避免按键边沿与 card 的下一次车况更新错位。

    随后发布的 CS.vCruise、CS.vCruiseCluster 单位都是 km/h;原始 cruiseState.speed 是 m/s,在 PCM 路径中乘 MS_TO_KPH。LongitudinalPlanner.update() 对 carState.vCruise 上限裁剪到 V_CRUISE_MAX 再转 m/s,并区分 unset;Controls.state_control() 将 CS.vCruise 转 m/s 传给车型 PID 加速度限幅,而 HUD 的设定速度取 vCruiseCluster。两种速度值可能不同,不能把仪表显示值当作规划器原始目标。

    二、PCM 与非 PCM 两条路径

    条件 update_v_cruise() 行为 影响
    原车巡航不可用 设定值及仪表值均为 255;非 PCM 按键计时器不在该分支清零 规划器将其识别为未初始化;不代表车辆实际时速 255 km/h。恢复时旧计时器可能仍在。
    CP.pcmCruise=True 直接使用 CS.cruiseState.speed/speedCluster 转 km/h 按键调速由原车系统负责,helper 不自行步进;speed==0 改为 unset,speed==-1 保留 -1 特殊值。
    CP.pcmCruise=False 且原车巡航可用 从本地状态处理按钮,然后令仪表设定值等于内部设定值 自管设定速度;只有 enabled=True 时按键会改目标。

    PCM 读取路径没有在本 helper 内对正常速度统一裁剪到 145 km/h;下游纵向规划器有自己的上限处理。量产适配需要分别核对原车报文中的 speed 和 speedCluster 含义、无效哨兵和单位,不能把任意 0 或 -1 当作普通速度。

    三、非 PCM 按键算法与阈值

    _update_v_cruise_non_pcm() 先找增/减速按钮的松开边沿;若找不到,再检查按住计时是否达到 50 帧倍数,将其视作长按重复。执行变更前有两个防重入门:增速键用于从静止巡航恢复时不调高设定值;若按钮按下时尚未 enabled,而松开前才完成使能,也不把同一次按键再次解释为调速。update_button_timers() 在当帧调速判断之后更新计时和按下时的 standstill/enabled 快照,这是边沿语义的一部分。

    参数 / 规则 取值 说明
    V_CRUISE_UNSET 255 未设定哨兵。
    V_CRUISE_MIN 8 km/h 结果下限。
    V_CRUISE_MAX 145 km/h 结果上限;上限注释指出模型训练分布外的速度预测可能不可靠。
    短按步长 公制 1 km/h;英制 IMPERIAL_INCREMENT=1.6 km/h 英制由 1 mph 转换并取一位小数。
    长按倍率 ×5 长按时若当前值不在 5 倍步长网格,增速先向上取整、减速先向下取整,再继续步进。
    长按判定 按住 50 帧倍数 最终结果取一位小数并限制在 8–145 km/h。

    油门覆盖时按减速键,会把新目标至少抬到当前 vEgo,随后仍受 145 km/h 上限约束。源码条件还写有 setCruise,但该函数的按键选择集合仅含增速/减速,因此当前路径不会选择到 setCruise;不能据该条件宣称 SET 键在此处有同样效果。

    四、使能初始化、上游门与测试

    initialize_v_cruise() 对 PCM 直接返回;非 PCM 首次使能时默认从当前车速与初始下限中取较大者,普通模式初始下限 40 km/h,实验模式 105 km/h,再限制到 145 km/h。若是增速/恢复按键触发且已有旧设定值,则恢复 v_cruise_kph_last,不是重新用当前车速初始化。这里的 105 km/h 是设定速度的初始下限/上界提示语义,实验模式仍由模型和下游规划裁决实际加速,不能理解为车辆会立即开到该速度。

    非 PCM 的按键使能来自 CarState.buttonEnable(基础接口由增/减速键松开边沿产生,车型可覆写),经 CarEvents 转成 buttonEnable 事件,再由状态机决定使能。VCruiseHelper 只管设定值,不负责判断驾驶员监测、车辆故障或 panda 的 controlsAllowed;取消键、刹车与安全事件仍可让系统退出。

    源文档的输入清单里还列出 IsMetric 与 ExperimentalMode:前者参与决定短按步长取公制 1 km/h 还是英制 1.6 km/h,后者决定首次使能的初始下限取 40 还是 105 km/h。二者都只影响设定值的产生,不影响车端是否已执行,也不替代使能与安全门。

    测试方面,test_cruise_speed.py 覆盖非 PCM 短按松开、使能中按键、静止 resume、油门覆盖和初始化,文件 TODO 明确 PCM 路径尚无对应测试;本工作区的八项原类隔离探针使用假 NumPy 裁剪和假车况,不依赖实际 capnp、CAN 或 UI,也不等于实车按键与执行器测试。

    常见问题

    设定速度和实际车速是一回事吗?

    不是。设定速度是规划/显示输入,不是车端已执行车速,也不代表系统已使能。源文档还强调 vCruise 与 HUD 取的 vCruiseCluster 可能不同,不能把仪表显示值当作规划器原始目标。

    PCM 车型的长按调速逻辑由谁负责?

    由原车系统负责。CP.pcmCruise=True 时 helper 直接使用 CS.cruiseState.speed/speedCluster 转 km/h,本 helper 不自行步进;因此 50 帧长按、5 倍步进这类算法只存在于非 PCM 路径。

    实验模式初始下限 105 km/h,是不是车会立刻加速到 105?

    不是。源文档明确 105 km/h 是设定速度的初始下限/上界提示语义,实验模式仍由模型和下游规划裁决实际加速,不能理解为车辆会立即开到该速度。

    按增速键从静止恢复会调高设定值吗?

    不会。源文档给出的防重入门之一就是:增速键用于从静止巡航恢复时不调高设定值;另一个是若按钮按下时尚未 enabled,而松开前才完成使能,也不把同一次按键再次解释为调速。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CRUISE_SET_SPEED_DEEP_DIVE.md(取用了数据流与单位、PCM 与非 PCM 路径、非 PCM 按键算法、阈值常量与测试边界)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • openpilot 感知到控制:四条数据流与 100 Hz 闭环

    结论:openpilot 的感知到执行不是一个同步函数调用栈,而是 modeld、radard、plannerd、controlsd 四条进程经消息松耦合的数据流:modeld 随图像帧运行,radard 与 plannerd 以 modelV2 为轮询节拍,controlsd 按 100 Hz 的 Ratekeeper 循环并最多等待 15 ms。同一轮控制里各输入可能来自不同采样时刻,因此不能按顺序函数调用推断一致性。本文为固定源码快照 c8fb906 的静态阅读,未加载模型、未连接车辆。

    闭环与进程时序:四条数据流而不是一条调用栈

    把整条链路理解成「摄像头 → 模型 → 规划 → 控制」的一次顺序调用,是最容易出错的心智模型。源码里 modeld 随图像帧运行,radard 和 plannerd 的 SubMaster 以 modelV2 为轮询主题,controlsd 则按 100 Hz 的 Ratekeeper 循环,以 selfdriveState 为轮询主题并有最多 15 ms 的更新等待。三条速率彼此不同,服务频率的定义见 services.py。

    进程 驱动节拍 主要输入 主要输出
    modeld 随图像帧 VisionIPC 相机流、CarParams、标定、车辆状态、驾驶员监测 modelV2、drivingModelData、cameraOdometry
    radard 轮询 modelV2 carState、radarTracks、modelV2.leadsV3 radarState
    plannerd 轮询 modelV2 modelV2、radarState、carState、CarParams longitudinalPlan、driverAssistance
    controlsd 100 Hz carState、selfdriveState、modelV2、计划 controlsState、carControl

    四者之间没有共享的同步函数栈,消息来自不同采样时刻是常态而非异常。这一点是后面所有边界讨论的前提:讨论「某一帧模型输出是否导致本轮转向」时,必须先确定这些消息是否属于同一次采样。

    感知:modeld 从图像到结构化预测

    main() 等待并连接 VisionIPC 相机流,读取 CarParams、标定、车辆状态与驾驶员监测信息,同步主相机与广角帧,计算图像变换和动作延迟,最后发布 modelV2、drivingModelData、cameraOdometry,并跟踪丢帧比例与相机时间戳。真正的模型装载在 ModelState.__init__():加载 tinygrad 序列化模型与 warp JIT,建立输入输出缓冲和递归状态对;模型文件、设备或编译任一步失败,初始化便无法继续。

    函数或类 读入与处理 输出与边界
    ModelState.pack_inputs() 将变换矩阵、动作条件、图像帧和状态输入装入连续缓冲 为外接 GPU 路径减少上传次数;缓冲布局本身是模型契约的一部分
    ModelState.run() 复制 NV12 帧,按上升沿形成 desire 脉冲,上传输入,执行 warp 与模型,按切片解析输出 大模型路径显式拒绝非有限输出;可选保留原始预测
    Parser.parse_outputs() 对轨迹、车道线、道路边缘、前车、意图和元信息执行 MDN / sigmoid / softmax 解码 结果供消息填充,不是最终车辆执行命令
    get_action_from_model() 从 action 头或 plan 回退提取目标曲率/加速度,按速度与延迟平滑 生成 ModelDataV2.Action:desiredCurvature、desiredAcceleration、shouldStop

    大模型选择逻辑在 main():检测到 Chestnut 后异步加载大模型,等待最多 60 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,交由进程管理层处理。这个回退是运行时选择,不能解释为对任意模型插件的兼容承诺。此外 extrinsics_calibration_seen 随标定数据更新并随模型消息发布,接入新相机时必须保留时间戳、图像几何和有效性语义。

    前车融合与纵向规划:radard 与 plannerd

    radard 不是「只转发雷达」的组件。即使没有雷达匹配,它也可能根据视觉预测产生 radar=False 的前车;反过来,低速场景可能用较近的雷达轨迹覆盖第一前车。Track.update() 按雷达 track ID 更新相对距离、横向位置、相对速度和滤波后的前车速度/加速度;match_vision_to_track() 用距离、横向位置与速度的概率分数选择轨迹;get_lead() 优先视觉与雷达匹配,无匹配但视觉概率足够时使用视觉前车。RadarD.update() 对齐自车速度与雷达延迟,结合 modelV2.leadsV3 形成 leadOne / leadTwo,RadarD.publish() 发布 radarState,有效位取自输入主题检查结果。

    plannerd.main() 等待 CarParams,以 modelV2 为节拍更新纵向计划与车道偏离警告,分别发布 longitudinalPlan 与 driverAssistance。LongitudinalMpc.update() 依据两辆前车和驾驶风格设定跟车时距、障碍距离及约束,运行 acados MPC;run() 调求解器保存速度、加速度与 jerk 轨迹,非零求解状态会记录告警并 reset(),此时该轨迹不能当成成功解。LongitudinalPlanner.update() 从 MPC、巡航及实验模式的端到端动作候选中选择最小加速度作为目标,shouldStop 对各候选取逻辑或。

    值得注意的是,所选加速度与 shouldStop 不一定来自同一候选,这种「最小加速度」合成是偏保守的候选合成,但不等于独立安全证明;求解器失败、输入无效、车型加速度能力和实际制动执行仍分别由其他层处理。车道偏离警告由 LaneDepartureWarning.update() 在高车速、近 5 秒未打转向灯且横向控制不活动时,结合车道线置信度、距离与变道意图触发,它不是转向控制命令。

    controlsd:从计划到 carControl

    Controls.__init__() 等待 CarParams,实例化车型接口、LongControl 与车辆动力学模型,并依转向控制类型或车型调参选择横向控制器;Controls.run() 以 100 Hz 调用 update()、state_control()、publish()。state_control() 计算使能、横向与纵向活动条件,执行纵向 PID 与横向控制,对目标曲率限幅,并将非有限执行器字段清零。

    横向实现 算法要点 边界
    角度接口 LatControlAngle.update() 用车辆模型把目标曲率转为目标转角,加入学习角偏置 执行器变化率仍由车型控制器与 panda 策略约束
    曲率接口 LatControlCurvature.update() 从当前转角估计曲率,配置 PID 时做误差闭环,否则前馈 输出限到 MAX_CURVATURE 的 PID 范围,不能当扭矩
    转角 PID LatControlPID.update() 以目标/当前转角误差闭环 安全限幅、驾驶员施力或低速时冻结积分
    扭矩接口 LatControlTorque.update() 在横向加速度域闭环,补偿摩擦、坡度与响应延迟 使用 1 秒目标加速度环形缓冲与 jerk 滤波

    state_control() 在 lateralManeuverPlan 有效时优先取其目标曲率,否则用 modelV2.action;停止横向活动时把目标重置为当前曲率以避免使能瞬间跳变。纵向 LongControl.update() 有 off / stopping / pid 三种状态,根据制动、巡航静止与 shouldStop 切换。需要特别注意的是,carControl 与 controlsState 的有效位当前仅赋值为 CS.canValid,源码注释指出若改为 sm.all_checks() 会形成循环依赖——因此消息有效位不能被误读为所有模型、计划与姿态输入都有效,下游状态机和 panda 安全门仍必须独立检查。

    插件化时的接口边界

    如果要按插件方式替换上述任一层,至少要保留该层的输入契约与有效性语义,而不是只替换一个数值输出。

    可替换层 至少保留的契约 不应开放的能力
    感知模型 VisionIPC 帧时间戳、相机标定、输出 schema、有效性/丢帧/延迟、启动预热与回退 直接发送车辆 CAN
    雷达融合 radarTracks 与 modelV2 对齐、leadOne/leadTwo 来源标志、错误与时延 绕过状态机强制使能
    规划器 longitudinalPlan 的轨迹、aTarget、停止/油门标志、求解状态和超时 把非成功解当成正常计划
    横纵向控制器 CarParams 能力、活动条件、饱和反馈、非有限数处理、控制频率 修改 panda 安全模式或白名单

    可迁移的路径是先建立消息契约与进程回放基线,再逐层替换算法实现。实车输出仍经 card 的车型接口和独立 panda 固件安全钩子,因此插件层的正确性并不等于执行链的完整性。

    常见问题

    为什么不能把 modelV2 到 carControl 当成一次函数调用?

    因为各进程速率不同:modeld 随图像帧运行,radard 与 plannerd 以 modelV2 为轮询主题,controlsd 按 100 Hz 循环。消息可能来自不同采样时刻,不存在保证它们同属一次采样的同步栈。

    radard 是雷达数据的转发器吗?

    不是。即使没有雷达匹配,也可能根据视觉预测产生 radar=False 的前车;低速场景还可能用较近的雷达轨迹覆盖第一前车。插件化时必须保留 track ID、延迟、误差和 present/radar 语义。

    carControl.valid 为真就代表所有输入都有效吗?

    不代表。当前 carControl 与 controlsState 的有效位仅取 CS.canValid;源码注释指出改成 sm.all_checks() 会形成循环依赖。因此它不能替代对模型、计划与姿态输入的独立检查。

    大模型加载失败会怎样?

    检测到 Chestnut 后异步加载大模型并等待最多 60 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,由进程管理层处理。这是运行时选择,不是对任意模型插件的兼容承诺。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PERCEPTION_CONTROL_DEEP_DIVE.md(取用了四条进程数据流、modeld 感知链、radard 融合、纵向 MPC 与 controlsd 输出,以及插件化接口边界)。
    • 项目 02 —— openpilot-research/CONTROL_ALGORITHMS_DEEP_DIVE.md(仅用于确认四种横向控制器与纵向执行状态的分层位置)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 8 个扭矩桶、4000 点门槛:横向扭矩在线估计与三层「可用」

    结论:横向扭矩响应的在线估计把样本分进八个扭矩桶,用全最小二乘拟合「扭矩—横向加速度」的斜率与偏置,并以残差离散度近似摩擦。但它对外暴露的「可用」其实是三层不同的门:顶层消息有效位来自输入通信检查,内层参数有效位要求各桶达到 [100, 300, 500, 500, 500, 500, 300, 100] 的最小点数且总数达到 4000,「是否使用参数」则只是品牌白名单与调校类型门。三者互不推导,控制侧甚至在未检查内层有效位的情况下就可能使用估计值。

    采样门:从健康位到扭矩桶

    采样横向加速度按「车速 × 校准偏航率 − sin(滚转) × 重力」计算。进入桶之前要逐层过关:上游各健康位全真、扭矩历史缓冲填满、横向控制已连续激活并稳定 2 秒且驾驶员未覆盖、车速大于 15 m/s、扭矩绝对值大于 0.02、横向加速度绝对值不超过 1。入桶时扭矩值还要落在八个开右端点的 [−0.5, 0.5) 区间之一,区间外的点会被静默忽略。

    门 条件 备注
    健康位 / 历史缓冲 全真且历史填满 前置门,不满足则不入桶
    激活稳定性 连续激活 2 秒、无驾驶员覆盖 —
    车速 >15 m/s 严格大于
    扭矩绝对值 >0.02 严格大于
    横向加速度 ≤1 m/s² 超限不入正常桶

    有个容易被忽略的边界:给车控、执行输出与车况的时间戳加的是「当时」的延迟,而设备观测用的是消息内部时间戳。若延迟被置成负值,用于插值的时间窗可能为空;由于对空序列的判断特性,即便激活历史全假、驾驶员覆盖全真,样本也可能被放行。正常延迟名义上是正的,这只是一个「函数本身没有拒绝异常延迟」的说明,并不代表生产环境会收到负延迟。

    八个桶与 4000 点门槛

    「能算」和「算得可信」是两个不同的门:只要每个桶里至少有一个点,就可以先输出原始拟合;但要标记为有效,各桶必须达到各自的最小点数,且总点数不少于 4000。进度百分比取「总量完成率」与「最低桶完成率」的平均后取整——这意味着某些桶偏少时,整体进度会被拉低。

    桶(扭矩区间,左闭右开) 最小点数
    第 1 桶 100
    第 2 桶 300
    第 3 ~ 6 桶 各 500
    第 7 桶 300
    第 8 桶 100
    合计 ≥4000

    全最小二乘与负斜率的坑

    拟合在「扭矩、常数 1、横向加速度」组成的点阵上做全最小二乘(SVD),把样本随机抽到最多 2000 点,只捕获线性代数异常。斜率与摩擦分别被约束在离线调校的 ±30% 与 ±50% 之内,偏置没有同类限幅。合成验证给出一个值得记住的结论:当斜率为负时(例如拟合 y = −2x + 0.5),斜率本身能被正确恢复,但摩擦却显著非零——原因是把斜率转成旋转角时用了平方根,结果恒为非负,负斜率下的残差没有沿直线方向被消去。这是合成负斜率分支的源码/数值结论,并不证明正常车型会出现负扭矩因子。

    数值健壮性还有一处边界:发布与滤波前只用「空值 / 非数」判断拟合结果,没有显式检查正负无穷;在合成输入下,偏置为无穷仍可能带着内层有效位进入滤波。这提示序列化与滤波层都需要独立的有限值校验。

    三层「可用」不是同一门

    层 来源 实际含义
    顶层消息有效 输入主题的通信/频率/有效性检查 不表示拟合已经成熟
    内层参数有效 桶分布与数量达标、拟合非空非非数 估计是否成熟
    是否使用参数 品牌白名单 + 调校类型 与样本量、内层有效位无关

    关键消费事实是:控制侧在扭矩车型上只要求「主题通信健康」与「是否使用参数」为真,就调用参数更新,没有检查内层有效位。因此未完成新标定时,也可能使用初始离线值或历史滤波值——不能把「内层无效」误读为「控制器必然停用」。滤波本身按一阶方式更新,衰减时间从 50 持续增大到最多 250;一次更新里会先用旧系数更新滤波值,再用增大后的衰减重算系数。整个过程最终仍要经过车辆接口与 panda 安全策略,估计插件不得直接发送 CAN。

    持久化与恢复的边界

    缓存键是持久参数。恢复时先核对车型指纹与恢复键:匹配且内层有效时恢复三个滤波值与缓存点;兼容但内层无效时,仍会加载缓存点并恢复衰减值。恢复不是事务式提交——一旦解析中途异常,内存中已经装载的部分状态可能保留,而已写入的缓存键会被删除,形成一个「条件性部分恢复」的风险;车型指纹或版本不匹配时则跳过缓存但不删除键。

    插件化边界

    做插件化时,扭矩估计器宜先作为只读影子实例,输出独立候选主题,控制端只允许单一权威发布者。插件契约需要显式携带车型/调校版本、延迟时间语义、坐标系、采样覆盖率、顶层通信有效位、内层拟合有效位与输出年龄。切换为权威源前必须证明缓存恢复、缺失或异常输入、横向激活与驾驶员覆盖门以及回放等价性,且不得绕开既有安全许可。

    常见问题

    每个扭矩桶里有点,就等于参数可用了吗?

    不等于。「能算」只要求每个桶非空,可以先输出原始拟合;「可信」还要求各桶达到 100/300/500 的最小点数且总数达到 4000,才会把内层参数有效位标为真并更新滤波值。

    从缓存恢复的估计点一定是本次标定的吗?

    不一定。兼容时无论内层有效位真假,都会加载缓存点并恢复衰减值;缓存恢复不是事务式提交,解析中途异常可能留下部分内存状态,因此只能视为条件性部分恢复。

    为什么负斜率的摩擦估计会明显偏大?

    因为把斜率转成旋转角时使用了平方根,结果恒为非负,负斜率下的残差没有沿直线方向被消去,于是垂直残差偏大、摩擦被高估。这是合成负斜率分支的结论,不证明正常车型会出现负扭矩因子。

    内层参数标记为无效,控制器会不会停用估计值?

    不会仅凭这一点停用。控制侧只检查主题通信健康与「是否使用参数」,不检查内层有效位,因此未完成新标定时仍可能使用初始离线值或历史滤波值;最终输出还要经过车辆接口与安全策略。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TORQUED_LATERAL_TORQUE_LEARNING_DEEP_DIVE.md(采样门、八个扭矩桶与 4000 点门槛、全最小二乘与滤波、三层有效位、持久化与插件边界)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有桶阈值、点数门槛与配置均出自源文档,标注为未验证的结论保留其原有边界;合成点拟合结论不代表实车扭矩响应。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 横向目标曲率有三道限幅:jerk 5、加速度 3、曲率 0.2 各拦什么

    结论:从目标曲率到车轮,controlsd 要经过「来源仲裁 → 变化率/加速度/绝对曲率三道限幅 → 选定的横向控制器 → 车型编码 → panda 固件」这几层。两个容易被误读的边界是:来源选择只看 lateralManeuverPlan 的消息 valid 位,不看 updated、alive 或消息年龄,因此发布者停发而进程不重启时,最后一条有效计划可能被持续选为目标;另外变化率限幅本身不会置 curvature_limited 标志,不能用它判断「是否发生过裁剪」。以下为固定源码快照 c8fb906 的静态追踪。

    两条目标来源与 valid / alive 的差别

    正常目标来自 modelV2.action.desiredCurvature;若 controlsd 看到 lateralManeuverPlan 的消息 valid 位为真,则优先采用该计划;横向未激活时两条来源都改用实测曲率,以降低再使能时的阶跃。模型侧 get_action_from_model() 有两条版本兼容路径:没有 action 输出时从预测轨迹的 yaw/yaw rate 估算曲率,有 action 时把输出横向量除以 max(1,vEgo)^2;纵向加速度作 0.3 s 平滑,横向平滑常量为 0,但 vEgo<=0.3 m/s 时保持上一帧曲率。

    实验来源 lateral_maneuversd 只有机动 active 且未完成才把计划标为有效,曲率以基线曲率加「目标横向加速度 / max(vEgo,1)^2」形成;进程表只在横向机动条件下管理它,生产运行不应把它当作常驻规划器。

    失效语义的核心在消息健康位:SubMaster.update_msgs() 只在收到新消息时更新 valid;alive 按收包时间重新计算;all_checks() 才同时检查存活、频率与有效位。而 controlsd 的来源选择只检查 sm.valid['lateralManeuverPlan'],未检查该主题的 updated、alive 或消息年龄。因此曾收到有效实验计划后,若发布者停发而进程不重启,最后一条计划仍可能被持续选为目标——其后仍经过限幅、latActive、车型与 panda 门,不等于可以任意发送转向。

    controlsd 的逐层处理

    Controls.__init__() 读取 CarParams,按转向接口类型优先选 angle/curvature,否则按横向调参选 PID/torque;update() 以 selfdriveState 为轮询主题更新异步输入与姿态数据。state_control() 更新车辆模型、活动门、目标曲率与执行器,并对数值字段做有限性检查。VehicleModel.calc_curvature() 把扣除在线角偏置的方向盘角换成实测曲率,考虑转向比、速度与路面横滚;controlsd 在调用前取负号,方向约定需用实车校验。

    函数/组件 源码作用 契约或边界
    Controls.__init__() 按转向接口类型优先选 angle/curvature,否则按横向调参选 PID/torque。 不同输出单位不能在插件间直接互换。
    state_control() 更新车辆模型、活动门、目标曲率与执行器,做有限性检查。 生成的是 CarControl,不是 CAN 或执行许可。
    VehicleModel.calc_curvature() 扣除在线角偏置的转角换算实测曲率,考虑转向比/速度/横滚。 调用前取负号,方向约定需实车校验。
    clip_curvature() 顺序限制变化率、考虑 roll 的横向加速度范围、绝对曲率。 curvature_limited 只由后两道限幅标记,变化率限幅不置位。
    Controls.publish() 发布 controlsState 与 carControl,比较请求与 carOutput。 顶层 valid=CS.canValid,不是 sm.all_checks()。
    Car.controls_update() carControl 存活且非被动/已初始化时调用车型 apply() 并发布 sendcan。 仍须通过车型编码与 panda 固件安全约束。

    三道限幅与标志位陷阱

    clip_curvature() 按顺序做三件事:用至少 1 m/s 的速度计算 5 m/s^3 的近似横向 jerk 限幅;再按 ±3 m/s^2 加 roll 补偿限加速度;最后按 ±0.2 1/m 限绝对曲率。源码明确标注 jerk 公式为近似,这些常量不是可以脱离车型和硬件直接复用的认证约束。

    隔离探针给出一个典型陷阱:当变化率单独生效时,输出已从 0.1 改成 0.0005,但 curvature_limited=False。也就是说这个标志不能作为「无裁剪」的诊断依据。探针只验证纯数值函数,不代表 controlsd 消息时序、车型控制器或板端限幅已通过。

    限幅 阈值 是否置 curvature_limited
    变化率(近似横向 jerk) 5 m/s^3,速度下限 1 m/s 否
    横向加速度(含 roll 补偿) ±3 m/s^2 是
    绝对曲率 ±0.2 1/m 是

    输出单位与有限性清理

    控制器输出按 steerControlType 分流:通用第一返回值写 actuators.torque,angle/普通 PID 的第二返回值写 steeringAngleDeg,curvature 型写 actuators.curvature,扭矩控制的第二返回值为零。LatControlAngle 与 LatControlCurvature 都返回 steer=0,有效输出分别是角度与曲率字段;LatControlPID 在施力/低速/安全限幅时冻结积分;LatControlTorque 把目标曲率换横向加速度后返回时会翻转符号,不能把 pid_log.output 与 actuators.torque 直接同号比较。

    state_control 对 Actuators 的数值字段逐个检查非有限值并清零,但这只是末端值清理,不是上游模型输入、车辆参数或中间 PID 状态的完整非有限传播防护。publish() 在 selfdriveState.active 时比较本次请求与已收到的 carOutput,把差异留给后续循环的控制器饱和判断,这不是同步的本帧硬件确认。

    控制器 有效输出字段 边界
    LatControlAngle steeringAngleDeg(steer=0) 曲率反算目标方向盘角并加角偏置。
    LatControlCurvature actuators.curvature(steer=0) 曲率误差 PID 或前馈,驾驶员施力时用前馈。
    LatControlPID steeringAngleDeg 或扭矩 施力/低速/安全限幅时冻结积分。
    LatControlTorque actuators.torque 返回时翻转符号;不能把 pid_log.output 与扭矩直接同号比较。

    自制硬件与插件契约

    源文档给出的约束是:候选规划插件应输出带来源、时间戳、年龄上限、曲率单位 1/m、车辆坐标/正方向、适用车速范围与显式失效状态的目标;仲裁层独占来源选择与 TTL,失效时回退模型或安全降级。车辆学习参数、转角编码与 carOutput 的实际执行反馈也应按车型/硬件版本记录。

    不能让普通插件修改 latActive、车型极限或 panda 固件;CarControl 及其 valid 位也不能替代板级安全门。现有横向饱和测试覆盖部分车型与控制器,扭矩缓冲测试覆盖停用后缓冲清零,但未证明四类控制器全部车型、计划抢占超时、模型丢帧、车况异步或实车符号正确,仍需补齐来源有效→停发/无效切换的回放测试与 NaN/Inf 注入。

    常见问题

    实验计划停了不发,为什么车还在用它?

    因为 controlsd 的来源选择只检查 sm.valid['lateralManeuverPlan'],未检查 updated、alive 或消息年龄。曾收到有效计划后,若发布者停发而进程不重启,最后一条计划仍可能被持续选为目标;但它仍经过限幅、latActive、车型与 panda 门。

    curvature_limited=False 就代表没有裁剪吗?

    不代表。这个标志只由横向加速度与绝对曲率两道限幅设置;变化率限幅本身不置位。探针显示变化率单独生效时输出已从 0.1 改成 0.0005,但标志仍是 False。

    三道限幅的阈值分别是多少?

    按顺序是:用至少 1 m/s 计算 5 m/s^3 的近似横向 jerk、±3 m/s^2 的横向加速度(含 roll 补偿)、±0.2 1/m 的绝对曲率。源码标注 jerk 公式为近似,不能直接当认证约束复用。

    扭矩控制器的输出符号可以直接对比吗?

    不能。LatControlTorque 返回时会翻转符号,不能把 pid_log.output 与 actuators.torque 直接同号比较;angle 与 curvature 两类控制器则返回 steer=0,有效输出在角度或曲率字段。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LATERAL_TARGET_ACTUATION_DEEP_DIVE.md(两条来源仲裁、controlsd 逐层处理、三道限幅与四类控制器)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:五项 clip_curvature() NumPy 数值契约隔离探针结论。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 60 秒窗与 0.15–0.65 秒区间:横向执行延迟的在线估计

    结论:横向执行延迟的在线估计,本质是「用一个 60 秒滑动窗、凑够 25 秒有效样本,再用 FFT 互相关找响应滞后」的过程。它默认在 dt=0.05 s 的离散点上搜索 0.15 秒到小于 0.65 秒的延迟,要求相关峰 ≥0.95、峰宽置信度 ≥0.7;只有攒够 5 个完整块、块间标准差 ≤0.1 秒,状态才标为已估计,否则回退车型初值。进度百分比只是「被接受的估计块数」的量度,不是传感器或控制器的安全认证。

    采样窗与节拍:60 秒窗、25 秒门槛

    采样用一个 60 秒滑动窗保存时间、目标横向加速度、实际横向加速度与「样本是否可用」的掩码。一个容易踩的细节是:窗口一创建就被零值预填满,队列长度从一开始就是满的,究竟有多少有效数据要看「有效样本计数」而不是队列长度。估计类初始延迟取车型转向执行延迟再加 0.2 秒,并以名义 4 Hz 的节拍对外发布。

    估计用的两个量分别是目标横向加速度(目标曲率乘车速平方)与实际横向加速度(校准偏航率乘车速)。只有实际横向加速度有足够动态跨度(不小于 0.5 m/s²)且自上次估计后有新的有效点时,才允许提交新估计。

    参数 取值 说明
    滑动窗长度 60 秒 创建即被零值与无效掩码预填
    有效样本门槛 25 秒 不足则即使执行拟合并算相关也不提交
    动态跨度门槛 ≥0.5 m/s² 实际横向加速度跨度
    分块粒度 每 100 次接受汇成 1 块,最多 50 块 区分「已完整块」与「含当前未满块的预览值」

    采样资格:一组门的组合

    每个样本要同时满足多项条件才被标记为可用:横向控制处于激活、驾驶员未介入方向盘、控制未饱和、车速高于 50 mph、偏航率绝对值达到默认门、外参已标定、位姿有效且偏航率与其标准差都小于 1、实际横向加速度不超过 2、目标与实际横向加速度之差不超过 0.6 m/s²。激活中断、驾驶员覆盖、饱和或姿态/加速度非法之后,还需要连续恢复至少 2 秒。

    门 阈值 备注
    车速 >50 mph 严格大于
    偏航率及其标准差 两者均 <1 姿态有效才参与
    实际横向加速度 ≤2 超限不入窗
    目标与实际之差 ≤0.6 m/s² —
    恢复时间 连续 ≥2 秒 覆盖、饱和、姿态非法后重新计时

    有一个反直觉的边界:默认的偏航率绝对值门是 0,因此零偏航率也能通过这道门,静态直道并不会被它筛掉——真正限制静态样本的是拟合阶段对动态跨度的要求。

    相关性拟合:互相关与「置信度」

    拟合先用长度为 5、标准差为 1 的高斯权重对有效样本做掩码平滑(无有效邻域时输出无效值),再用 FFT 做带掩码的归一化互相关来搜索响应延迟,峰位置做抛物线插值。FFT 长度由一组只含 2、3、5、7、11 因子的「下一个合适尺寸」决定。这里的「置信度」来自高相关候选连续区间的宽度,不是统计置信区间,因此不能按统计意义解读。

    还有两个数值层面的坑需要说明:其一,全常量信号在部分位移处会出现接近 1 的数值伪峰,所以不能只凭单个高相关值就认定存在有效动态证据(完整流程另有动态跨度门);其二,互相关实现可能原地修改调用方传入的浮点数组,这是实现细节层面的副作用,而非估计语义。

    状态与回退:estimated 需要 5 个完整块

    估计状态分三种:未估计、已估计、无效。只有至少 5 个完整块、块均值与标准差都不是无效值、且标准差不超过 0.1 秒,才会给出「已估计」;此时生效的延迟会被夹在 0.15 到 0.65 秒之间。未估计或块间离散度过大时,生效延迟回退为车型初值,但仍会把单独的估计值与标准差填进消息供观察。

    状态 条件 生效延迟
    unestimated 有效样本或完整块不足 车型初值(转向执行延迟 + 0.2 秒)
    estimated ≥5 个完整块、标准差 ≤0.1 秒 估计均值,夹到 [0.15, 0.65] 秒
    invalid 块间离散度过大等异常 回退车型初值

    下游消费与缓存恢复的坑

    控制侧直接读取生效延迟并叠加一个平滑量传给横向控制器,在使用点没有显式检查这条消息的顶层有效位或内层状态;而另一个消费者在整体输入检查失败时则不处理输入。两者对同一条消息的消费门并不相同,因此插件化时不能只给一个泛化的 healthy 字段。

    缓存恢复还有两个必须知道的边界:其一,恢复的是缓存里的「估计值」与「块数」,而不是生效延迟,恢复校验既不检查估计值是否有限、是否落在搜索区间内,也不要求状态为已估计;其二,块数字段是有符号整数且只检查上界,负数可以通过,从而在界面上产生负的进度百分比。此外,一个标为「未估计」但已携带 5 个块的缓存,在恢复后可能直接让下一次输出报告为「已估计」,即使本次启动并未接受任何新的估计——这是缓存状态与重新计算结果之间的差异,不等于真实延迟已被重新标定。

    插件化边界

    延迟估计器适合先作为影子主题与原有输出比对,保留输入时间域、掩码、相关度与置信度、块数、状态、回退初值与缓存版本。权威源必须只有一个;异常或过期数据应有独立的消费门与保守回退,且插件不得改变安全策略。上述结论均来自固定源码快照的静态与合成数值分析,合成测试不能证明真实的时序对齐、乱序多主题消息或实车延迟稳定性。

    常见问题

    相关度达到 0.95 就算估计成功了吗?

    不是。0.95 只是相关峰的门槛,还要峰宽置信度 ≥0.7、样本门通过,并攒够 5 个完整块且块间标准差 ≤0.1 秒,状态才会标为已估计;任一条件不满足都不会提交新估计。

    为什么估计进度到 100% 也不能当安全认证?

    因为进度百分比是「被接受的估计块数」的量度,反映的是估计成熟度,与传感器健康、控制器安全性没有直接关系。估计值随时可能因离散度过大而回退车型初值。

    从缓存恢复的延迟一定是本次重新标定的吗?

    不一定。恢复的是缓存里的估计值与块数,校验既不要求状态为已估计,也不检查数值是否落在搜索区间;一个标为未估计但已带 5 个块的缓存,甚至可能让下一次输出直接报告为已估计。

    车辆一直直线行驶,能估出延迟吗?

    不能单靠直道。默认偏航率门虽然允许零偏航率通过,但拟合阶段另要求实际横向加速度有足够动态跨度(不小于 0.5 m/s²),因此持续静态行驶无法形成有效估计。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LAGD_LATERAL_DELAY_ESTIMATION_DEEP_DIVE.md(采样窗与门、FFT 互相关与置信度、状态与回退、下游消费与缓存恢复)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、窗口与配置均出自源文档,标注为未验证的结论保留其原有边界;合成数值验证不代表实车时序结果。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 变道意图与 LDW:20 mph 与 31 mph 两道阈值各管什么

    结论:变道意图和车道偏离预警是两条不同的辅助链路。变道意图靠转向灯新沿、驾驶员同向扭矩与盲区位,在车速不低于 20 mph 时从 off 进入准备态;车道偏离预警则要求车速严格大于 31 mph、近 5 秒无转向灯且横向未激活。两者都只产生提示或候选,不直接授权转向或制动执行器。本文为固定源码快照 c8fb906 的静态分析。

    消息与时序:本帧状态、下一帧脉冲

    modeld.main() 在本帧推理前读取上一次 DH.desire,编码到 desire_pulse;ModelState.run() 只将 desire 从低到高的变化送入模型。推理后才从本帧 desireState 左右变道项之和计算 lane_change_prob,调用 DH.update(),并把更新后的状态/方向写入本帧 modelV2.meta。因此状态提示可在本帧出现,新意图要到下一帧才进入模型;不能把它们当同一时刻的闭环输出。

    plannerd.main() 以 modelV2 轮询,只有该主题更新时计算 LDW 并发布 driverAssistance;消息顶层 valid=sm.all_checks(),所以计算出的左右布尔值与可供消费的有效性是两回事。服务表配置 driverAssistance 为 20 Hz,这不是端到端时延保证。desirePrediction 是 LDW 使用的另一组模型值,不能与 desireState 混写。

    组件/入口 主要逻辑 下游或边界
    DesireHelper.get_lane_change_direction() 从左转向灯决定左,否则决定右;调用者先要求仅单侧灯亮。 准备态方向可随灯变化。
    DesireHelper.update() 转向灯新沿、速度、横向活动、驾驶员同向扭矩、盲区和模型完成概率推进状态。 只设置意图/状态,不直接写转向执行器。
    ModelState.run() 在推理输入中把非零 desire 的上升沿转为脉冲。 持续保持同一意图不会每帧重发脉冲。
    LaneDepartureWarning.update() 根据车速、灯冷却、横向活动、线可见度/距离及预测概率设置左右偏离。 仅产生布尔结果,不制动或转向。
    Controls.state_control() laneChangeState!=off 时设置 carControl 转向灯请求,按有效位设置 HUD 偏离方向。 车型是否实现灯控需逐车型检查。

    变道意图的状态门

    DesireHelper.update() 只有横向激活、单侧转向灯出现新沿、速度不低于 20 mph,才从 off 进入 preLaneChange。准备态要求 steeringPressed 且扭矩方向与转向灯一致,并且该侧无盲区报警,才进入 laneChangeStarting。熄灯或降到门槛以下会关闭准备态;已启动后源码不再以盲区位直接撤销,而是依赖模型概率和超时等分支,因此不能把盲区位当成持续的安全许可。

    完成门要求启动至少 0.5 s 且左右变道 desireState 概率和严格小于 0.02,之后若灯仍亮回准备态,否则关闭。横向失活或累计计时超过 10 s 关闭。laneChangeFinishing 虽在 log.capnp 中定义、selfdrived 也接受,本版本 helper 没有赋值该状态。五项原 DesireHelper 类假消息枚举探针确认:20 mph 恰好允许新沿进入准备态;横向失活会把上一灯沿记忆清零,再激活时即使灯一直亮也可视为新沿;同向扭矩在盲区亮时不能启动,但进入 laneChangeStarting 后熄灯/盲区亮且模型完成概率仍高时,意图继续输出。十秒上限在每次调用开头检查,首次越过十秒的该帧仍处于启动态,下一次调用才复位。

    车道偏离预警的判定条件

    LaneDepartureWarning.update() 要求车速严格大于 31 mph、按模型帧估算的近 5 s 内没有转向灯、carControl.latActive=False,且 desirePrediction 非空。左/右车道线各自概率大于 0.5;以相机横向偏移 0.04 m 修正后的近线条件分别是左侧 y[0]>-1.12 m、右侧 y[0]<1.04 m;对应方向的模型变道预测概率还须大于 0.1。last_blinker_frame 初值为零,冷却按 SubMaster.frame * DT_MDL 估算,不是单独的墙钟计时。四项原 LDW 类假模型字段探针确认:初始帧号 99 仍在五秒冷却内,帧号 100 恰好结束;31 mph 等值不允许 LDW,模型概率/线可见度/近线距离的等值也不触发;空 desirePrediction 会清左右状态,但非空且短于所需索引的向量直接抛 IndexError。

    维度 变道意图 车道偏离预警
    车速门槛 不低于 20 mph 才能进入准备态。 严格大于 31 mph。
    转向灯 需要单侧灯的新沿。 近 5 s 内没有转向灯。
    横向活动 要求横向激活。 要求 latActive=False。
    驾驶员输入 要求 steeringPressed 且扭矩同向。 不依赖驾驶员扭矩,依赖线可见度与模型预测。
    输出 变道状态/方向、转向灯请求与提示。 左右偏离布尔值、HUD 方向与声音提示。

    提示与执行边界

    selfdrived 仅在 IsLdwEnabled 和 driverAssistance 有效时生成 ldw;开关由参数轮询更新。该事件是 PERMANENT 小型提示及声音,不是 disengage 或执行器指令。准备变道、盲区阻挡和变道中分别是 WARNING 提示;其中盲区阻挡带提示音。controlsd 也会在 driverAssistance 有效时把左右偏离送入 HUD。

    两项原消费者条件组合探针确认了旧值消费的边界:先收 driverAssistance.valid=True,随后假时钟推进到超出 20 Hz 服务的十倍存活窗口且无新消息时,updated=False、alive=False,但 valid=True 与旧左右字段保留,隔离执行 SelfdriveD.update_events() 的 LDW 分支仍会添加 ldw;若收到新消息且外层 valid=False,两个分支都跳过。转向灯、驾驶员扭矩/按压、盲区都来自车型解码,不是模型自己「看见了」即可替代,新硬件接口必须逐车型证明同向扭矩和盲区位的确切含义。

    常见问题

    变道是车辆自己判断安全后完成的吗?

    不是。这里的变道是驾驶员参与的 L2 辅助行为,DesireHelper 只设置意图/状态,不直接写转向执行器;车型是否实现灯控还需逐车型检查。它不自动判断周围交通安全后自行变道。

    为什么变道用 20 mph、LDW 用 31 mph?

    两条链路的门槛是独立定义的:DesireHelper.update() 要求速度不低于 20 mph 才进入准备态;LaneDepartureWarning.update() 要求车速严格大于 31 mph。源码没有把两者合并为同一策略函数。

    盲区位会一直阻止变道吗?

    不会。准备态要求该侧无盲区报警,但一旦进入 laneChangeStarting,源码不再以盲区位直接撤销,而依赖模型概率和超时等分支,因此不能把盲区位当成持续的安全许可。

    LDW 的 5 秒冷却按墙钟算吗?

    不是。冷却按 SubMaster.frame * DT_MDL 估算,last_blinker_frame 初值为零;探针确认灯亮后的第 99 帧仍冷却、第 100 帧可再次触发。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LANE_CHANGE_LDW_DEEP_DIVE.md(DesireHelper 状态机、LDW 阈值、消息发布与提示边界)。
    • 项目 02 —— 同目录探针记录:五项 DesireHelper 状态、四项 LDW 阈值、两项 plannerd 发布、两项消费者分支隔离探针结论。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11