标签: panda

  • Ford panda 安全模式:双车速曲率门与 CAN FD 纵向的编译门

    结论:Ford safety 的核心不是「有没有白名单」,而是双车速源、质量位、曲率/jerk 门与构建变体共同构成的判据。ABS 与 PCM 第二车速名义 50 Hz、yaw 100 Hz,曲率由 yaw 除以经下界保护的车速得到;CAN FD 的自控纵向 TX 白名单只在 ALLOW_DEBUG 且纵向参数位置位时开放。因此官方测试套件默认带 ALLOW_DEBUG,不能用来证明 release 固件开放该能力。

    函数与责任

    函数 输入、状态或约束
    ford_get_counter()、ford_get_checksum()、ford_compute_checksum() 从 ABS 车速与 yaw 帧提取/计算计数和校验;其它 RX 消息靠检查表选择忽略。
    ford_get_quality_flag_valid() ABS、PCM 第二车速和 yaw 的指定质量位必须为 3;其余 RX 帧在配置表中显式忽略质量位。
    ford_rx_hook() ABS 与 PCM 双车速进入共享状态;yaw 除以经过下界保护的车速后形成实测曲率;另更新停车、油门、刹车和原车巡航状态。
    ford_tx_hook() 检查 ACCDATA、方向盘按钮、Lane Assist 和两种 LateralMotionControl;返回 false 拒绝候选帧。
    ford_init() 选择 6 条 RX 健康规则、经典 CAN/CAN FD TX 白名单及调试编译门;没有 Ford 专属转发钩子。

    RX 数据与新鲜度

    总线 0 上,ABS 车速和 PCM 第二车速名义 50 Hz、yaw 100 Hz、刹车/巡航 10 Hz、油门 100 Hz、停车状态 50 Hz。ABS 帧检查校验、4 位计数和质量位;yaw 帧检查校验、8 位计数和质量位;PCM 第二车速帧忽略计数器和校验和,但仍检查质量位,源码注释将此与某些 ECU 的跳计数/短暂校验异常联系起来。其余三类无计数/校验帧在配置中也忽略质量位。

    共享 rx_msg_safety_check() 先更新检查状态;只有当前帧有效且匹配 RX 检查表时,safety_rx_hook() 才调用 ford_rx_hook()。坏校验/质量帧不能按这条调用链更新 Ford 车况,而且有效性失败可撤销 controls_allowed。共享曲率检查中的 speed_mismatch_check() 再比较双速度源;自制网关若合并来源、丢失质量位或用单一速度伪造两路,将破坏这层判据。一个关键细节:yaw 可能先于新车速到达,此时曲率使用的是已保存车速,不能声称两个采样硬件同步。

    ford_init() 选择 6 条 RX 健康规则;FORD_COMMON_TX_MSGS 对两个按钮总线设 check_relay=false,其余列出的发送帧设为 true;共享 safety_fwd_hook() 依据当前 TX 表静态阻断从相机总线向主总线转发的受控地址,因此经典 CAN 与 CAN FD 的阻断集合也随模式变化。这些白名单都含 Steering_Data_FD1 的 0/2 两路和 ACCDATA_3、Lane_Assist_Data1、IPMA_Data 的 0 路;Ford 函数单独调用不能代表完整安全判定。

    TX 模式矩阵与位级门

    选择 ACCDATA 横向报文 依据
    经典 CAN,任意纵向参数位 白名单包含 LateralMotionControl FORD_LONG_TX_MSGS;并非参数位才开放。
    CAN FD,默认或 release 固件 白名单不含 LateralMotionControl2 FORD_CANFD_STOCK_TX_MSGS。
    CAN FD,ALLOW_DEBUG 且纵向参数位置位 白名单包含 LateralMotionControl2 FORD_CANFD_LONG_TX_MSGS 与 ford_init() 的编译门。

    ford_tx_hook() 对 ACCDATA 同时检查请求油门、预测油门、制动加速度及制动预充/减速位;CmbbDeny_B_Actl 为 1 一律拒绝,以免禁止原车 AEB。get_longitudinal_allowed() 还要求使能且驾驶员未踩油门;禁用时仅允许各字段的中性编码。cancel 只在前次原车巡航已接合时放行,resume 只在 controls_allowed 时放行;Lane Assist 的执行动作必须为零。

    两个横向格式分别解析曲率、曲率率、路径角/偏移,后面三项必须保持源码规定的 inactive 原始编码。steer_curvature_cmd_checks() 再检查绝对曲率、随车速变化的横向加速度/jerk、与实测曲率的偏差及非使能中性值;它会维护 desired_last,拒帧时清零,因而更换插件或安全模式时不能把拒帧当作完全无状态事件。另外,ACCDATA_3 与 IPMA_Data 在 Ford TX 钩子中没有专属载荷检查,不能把白名单解释成逐信号验证。

    证据、缺口与插件边界

    test_ford.py 定义经典 CAN 纵向、CAN FD 原车纵向和 CAN FD 自控纵向三类具体测试配置。2026-10-04 在本地用原版 CANPacker、CFFI 和编译的 libsafety 运行 python -m unittest opendbc.safety.tests.test_ford -q:102 项中 91 项通过、11 项跳过。跳过的是三种具体配置均无 _user_regen_msg 的九个通用再生制动用例,以及两个抽象基类的 setUpClass,不是 Ford 曲率或纵向边界失败。

    但要注意:测试库的 _build_libsafety() 默认加 ALLOW_DEBUG,所以这次官方套件不能证明 release 固件开放 CAN FD 自控纵向 TX 白名单。七项帧级离线探针分别编译不带/带 ALLOW_DEBUG 的原 libsafety,确认:界内曲率帧通过;坏 ABS 质量位不更新车速并撤销 controls_allowed;两路合法但相差 5 m/s 的车速在曲率 TX 时触发失配退出。面向「万物即插件」,Ford 车型插件应声明经典/CAN FD、总线拓扑、DBC、纵向能力、固件构建变体和 safetyParam,并把两路车速及 yaw 的来源、质量位、时间戳分别传到受信任安全层。

    常见问题

    为什么 yaw 先到会用到旧车速?

    因为源码注释明确指出 yaw 可能先于新车速到达,此时曲率使用的是已保存车速;因此不能声称两个采样硬件同步。探针也确认,先更新 ABS 车速再更新 yaw 时,实测曲率在新 yaw 到达前保留旧值。

    PCM 第二车速为什么忽略计数器与校验和?

    源码注释将此与某些 ECU 的跳计数/短暂校验异常联系起来。但该帧仍检查质量位,质量位不为 3 时不能更新车速。

    CAN FD 的自控纵向在 release 固件下能开吗?

    默认或 release 固件的白名单不含 ACCDATA;只有 ALLOW_DEBUG 且纵向参数位置位时才使用 FORD_CANFD_LONG_TX_MSGS。官方测试套件默认带 ALLOW_DEBUG,所以不能据此证明 release 固件已开放该能力。

    白名单里出现 ACCDATA,说明每个信号都被校验了吗?

    不是。白名单只核对地址/总线/长度并调用 ford_tx_hook(),而 ACCDATA_3 与 IPMA_Data 在该钩子中没有专属载荷检查。白名单准入不等于逐信号验证,也不等于 ECU 接受。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/FORD_PANDA_SAFETY_MODE_DEEP_DIVE.md(取用了 RX 数据与新鲜度、TX 模式矩阵与位级门、测试证据与缺口,以及插件边界)。
    • 项目 02 —— openpilot-research/PLUGIN_REFACTOR.md(仅用于确认车型插件与安全模式归位的引用位置)。

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

    更新记录

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

  • 100 Hz 的 card 网关:sendcan 不按 valid 拦截

    结论:card 是「车型发现 → 车况发布 → 控制帧网关」的中央进程,但它发出的东西按语义要分成四层:控制请求已生成、sendcan 已发布、panda 接受、车端确认动作。最容易踩的坑是:card 把 sendcan.valid 赋为 CS.canValid,而 pandad.can_send_thread() 本身并没有根据 Event.valid 字段筛掉 sendcan——这个元数据不是板端的发送阻断。以下基于源码快照 c8fb906 的静态阅读与假依赖探针,未构建、未做车型回放、未做实车验证。

    初始化与车型身份

    Car.__init__() 创建 can 订阅、pandaStates/carControl/onroadEvents 的 SubMaster,以及 sendcan/carState/carParams/carOutput/radarTracks 发布端。正常路径等首个非空 CAN,读取 AlphaLongitudinalEnabled 与 CarParamsCache,调用 get_car() 做识别、固件查询与接口构建,再按指纹取得 RadarInterface;识别失败在 get_car() 内回退 MOCK,这不是允许未知车发送通用控制帧。

    阶段 关键函数/参数 含义
    固件查询复用 obd_callback() 写 ObdMultiplexingEnabled 请求,先删 ObdMultiplexingChanged,再阻塞等待 pandad 写确认位;四项假 Params 探针确认假确认返回假仍记成功,且无超时或板端模式读回。
    指纹完成 FirmwareQueryDone get_car() 返回后写真;pandad 在该位之前不取最终车型参数。
    被动模式 OpenpilotEnabledToggle、CI.CC、CP.dashcamOnly 没有控制器、用户关闭或车型只支持 dashcam 时,CP.passive=True,并把 safetyConfigs 改为 noOutput。
    SecOC secOcRequired、SecOCKey 可从指定缓存文件导入 32 个字符,再把十六进制字符串解析为 16 字节;源码只检查长度与解析结果,不等于密钥来源或权限已验证。
    参数缓存 CarParamsCache、CarParamsPersistent、CarParamsPrevRoute 缓存用于本次 manager 生命周期的快速指纹;不能拿旧缓存代替当前 CAN/固件匹配。

    SecOCKey 解析处调用 bytes.fromhex(),没有局部捕获非法十六进制的 ValueError;静态上长度正确但内容错误的键可令初始化异常。此处尚未构造故障注入测试,量产硬件应验证密钥导入、轮换、无效格式与清除时的安全默认态。

    一次 card 步进的消息顺序

    把单次 step() 展开,顺序是固定的:

    1. pandad.can → Car.state_update();
    2. 调用 CI.update(can_list) 与 RI.update(can_list)(雷达可返回 None);
    3. VCruiseHelper 更新 CS.vCruise、CS.vCruiseCluster;
    4. Car.state_publish() 依次发布 carOutput、carState、radarTracks;
    5. onroadEvents 不含 selfdriveInitializing 时进入 Car.controls_update();
    6. 首次调用 CI.init() 后写 ControlsReady;
    7. CI.apply(carControl) → sendcan → pandad.can_send_thread → panda 固件安全门。

    can_comm_callbacks() 把 cereal 的 can/sendcan 消息与 CanData 列表互转,供指纹、接口初始化与诊断查询使用。state_update() 等待并批量读取原始 CAN,再分别调用车型与雷达 update();空 CAN 轮次累加 can_rcv_cum_timeout_counter,回放时使用 CAN 消息中的单调时间戳。三项原 CAN 回调探针确认:接收回调保留 Event 批次但不返回外层有效位/时间戳;诊断发送回调沿编码器默认值生成 valid=True 的 sendcan——这些回调没有按 carControl 活性或 CS.canValid 过滤。

    车况发布:四个 valid 位各有定义

    state_publish() 约每 50 秒发布一次 carParams;每步先发布 carOutput(上次 CI.apply() 的执行器输出回报),再发布 carState。四个有效位不能合并成一个「整车健康」标志:

    字段 取值来源 边界
    carState.valid CS.canValid 只反映车况信号是否有效
    carOutput.valid carControl 的 SubMaster.all_checks() 与车况有效性各自独立
    radarTracks.valid 雷达错误字段;None 时不发布该主题 错误与点仍在消息内
    carParams 节拍 sm.frame 对 50/DT_CTRL 取模 不是独立墙钟定时器

    六项原 state_update()/state_publish() 探针还确认:空原始接收批次仍会调用 CI.update()、RI.update() 并累加超时计数;只要原始事件列表非空,即使替身解码成空 CAN 列表也不累加;巡航 helper 先更新当前 CS,控制使能上升沿再用 CS_prev 初始化设定值;carOutput/carState/radarTracks 的有效位可分别为假/真/假。

    控制门链:谁真正拦住了发送

    step() 在发布车况后,要求 CP.passive=False、已收到 onroadEvents、且其中不含 selfdriveInitializing,才进入控制更新。首次 controls_update() 会调用 CI.init() 做车型接口/ECU 初始化,然后写 ControlsReady;pandad 的 configureSafetyMode() 只有在 FirmwareQueryDone 与 ControlsReady 两个门都满足后才读取 CarParams 配置安全模式。若 carControl 仍活跃,CI.apply() 产出请求执行器回报和 CAN 帧并发送 sendcan;否则本分支不发新帧。

    七项原 controls_update()/step() 探针确认:carControl 活跃而 CS.canValid=False 时,仍会编码并发布带 valid=False 的候选帧;initialized_prev=True 时跳过重复初始化;回放模式给 CI.apply() 使用记录的 CAN 时间。这就直接指向前述结论——sendcan.valid 只是元数据,车型 CarController、panda 固件的 TX 安全钩子和 controlsAllowed 才是另外的强制约束,硬件插件不能绕过这些层。

    故障或差异 当前代码可证实的处理 待验证
    无非空 CAN 初始化持续等待;运行期空批次累加超时计数 线束断开、总线睡眠与线程退出时序
    未识别车型或禁用控制 回退 MOCK/被动 noOutput 实板继电器与线束默认态
    OBD 复用确认未到 obd_callback() 阻塞等待 Params 确认 固件重连/断电恢复、超时策略
    carControl 丢失 all_alive 不满足时不再调用 CI.apply() 最后一帧、panda 超时与原厂系统恢复时延
    CAN 无效但已有控制帧 发布 sendcan.valid=False;发送线程未按 valid 拦截 车型控制器与 panda 安全钩子的拒绝/回退

    时序上,card 由 CAN 输入驱动,配置 100 Hz Ratekeeper 监测节拍,实际迭代频率取决于总线输入与处理时间;params_thread() 约每 100 毫秒刷新单位与实验模式,cumLagMs 来自 Ratekeeper 剩余时间。不同进程不是原子事务:不能假定最新 carState、最新 carControl 和本轮 CI.apply() 精确一一对应。

    常见问题

    sendcan.valid=False 会阻止 panda 发送吗?

    不会自动阻止。pandad.can_send_thread() 只检查时间戳是否小于 1 秒,本身没有根据 Event.valid 字段筛掉 sendcan。真正的强制约束在车型 CarController、panda 固件的 TX 安全钩子和 controlsAllowed。

    收到 carState 就说明整车数据都健康吗?

    不能。carState.valid、carOutput.valid、radarTracks.valid 各自有定义,可以分别为假/真/假。它们不能合并成一个「整车健康」标志。

    OBD 复用握手为什么会卡住?

    obd_callback() 写请求后阻塞等待 pandad 写确认位,而这个握手没有显式超时,也没有板端模式读回。四项假 Params 探针确认假确认返回假仍记成功,因此固件重连/断电恢复与超时策略需要额外设计。

    识别失败回退 MOCK 会不会乱发控制帧?

    不会直接发送。回退只发生在识别阶段,仍由后续参数与安全配置决定输出;被动路径会把 safetyConfigs 改为 noOutput,而不是允许未知车发送通用控制帧。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CARD_VEHICLE_GATEWAY_DEEP_DIVE.md(card 车型发现、车况发布与控制帧网关,含源码行号索引)
    • 项目 02 —— openpilot-research/CARD_OBD_MULTIPLEXING_HANDSHAKE_DEEP_DIVE.md(源文档指向的 OBD 复用握手专题)
    • 项目 02 —— openpilot-research/PANDAD_PYTHON_CAN_CAPNP_ADAPTER_VALIDITY_DEEP_DIVE.md(源文档指向的 CAN 适配与有效位专题)

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

    更新记录

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

  • UIState.update() 在绘制之后:告警时延与 5 Hz 刷新

    结论:车载 UI 是呈现与交互层,不裁决自动驾驶使能,权威状态来自 hardwared、selfdrived、card 与模型进程。决定告警时延的是一条容易被忽略的顺序:GuiApplication.render() 的生成器在绘制顶层 Widget 之后才结束本帧绘制,本轮 UIState.update() 因此发生在绘制之后,新消息通常在下一帧才用于 Widget——所以 uiDebug.frameTimeMillis 不能当成「消息发布到屏幕显示」的端到端时延。本文为对本地固定源码快照的静态阅读,逐个像素、全部语言与 GPU 合成均未验证。

    入口、数据与帧顺序

    ui.main() 初始化窗口后,按屏幕尺寸创建 MainLayout 或 MiciMainLayout,每次从 GuiApplication.render() 的生成器取得帧状态再调用 UIState.update()。屏幕关闭时生成器仍周期性 yield False,让 UIState 有机会更新并重新唤醒屏幕。触摸事件经 MouseState 由顶层 Widget 分发到子 Widget 或导航栈。

    组件/关键函数 实现作用
    UIState._initialize() 建立单例与大量 cereal 主题订阅,初始化 started、status、车型、实验模式和 Chestnut 状态
    UIState.update() 非阻塞 SubMaster.update(0),刷新状态/告警枚举,再调用屏幕设备管理;约 5 Hz 后台线程读取较慢的 Params
    UIState._update_state() / _update_status() 从 pandaStates、deviceState、selfdriveState 派生点火/行车/使能状态并通知边沿回调;started 要求 deviceState.started 且自身缓存点火为真
    UIState._update_chestnut_state() 根据设备发现、模型编译产物、新鲜度与 Params 显示断开/加载/激活/失败,与固件刷写状态不是同一状态机
    Device._update_brightness() 行车时按相机曝光估算环境亮度并平滑,后台线程设置背光;触摸、点火、交互超时控制屏幕电源
    Widget.render() / _process_mouse_events() 先更新状态再绘制,然后处理触摸;只接受从自身命中区域开始的触摸,长按达 0.45 秒后清除点击跟踪

    需要单独记住的是:UIState.update() 里的非阻塞订阅只刷新枚举与屏幕设备管理,较慢的参数项由约 5 Hz 的后台线程承担,这两者的节奏并不相同。

    页面与关键交互

    MainLayout._set_mode_for_state() 管理首页、设置、行车三态;行车切换会自动进入摄像头画面,侧边栏可由行车画面触摸切换。SettingsLayout 分为 Device、Network、Toggles、Software、Firehose、Developer 六面板,其中 TogglesLayout._update_toggles() 会根据车型纵向能力、分支和 engaged 状态限制开关。

    页面/组件 关键入口 行为
    大屏主导航 MainLayout._set_mode_for_state() 首页、设置、行车三态;行车切换自动进入摄像头画面,body/notCar 特殊处理
    大屏首页/侧边栏 HomeLayout._update_state() / Sidebar._update_state() 显示更新/驻车告警、网络/温度/连接/设备状态,可进入设置或发布书签
    大屏设置 SettingsLayout Device、Network、Toggles、Software、Firehose、Developer 六面板,开关受车型与状态限制
    首次引导 OnboardingWindow 及其 mici 版本 条款版本与培训版本写 Params;mici 另延长交互超时并将亮度设为 100,退出后恢复
    mici 主导航 MiciMainLayout._handle_transitions() 横向滚动驻车告警、首页、行车/Body;点火后约 2.5 秒切换行车,静止时返回行车

    行车画面与告警

    AugmentedRoadView._render() 先绘制摄像头,再按顺序叠加模型路径、HUD、告警和驾驶员状态,且仅在 ui_state.started 时执行。CameraView._render() 非阻塞读取 VisionIPC:没有新帧时保留上一帧,连接丢失时清空并绘制占位;comma 硬件走 EGL 直接缓冲,PC 走 Y/UV 纹理拷贝。ModelRenderer._render() 要求模型和标定至少在本次行车之后收到过。

    相机画面的连接管理也不简单:_ensure_connection() 对重连做了节流,_handle_switch() 要等目标流有帧才完成宽/窄切换,而宽/窄流的选择取决于实验模式与速度门限;_update_calibration() 只消费有效标定矩阵。屏幕上的路径只是视觉呈现,不是控制输出的独立证明。

    告警侧的两个时间门值得单列:AlertRenderer.get_alert() 在 10 秒未收到首次行车 selfdriveState 时显示等待;在 comma 硬件上,已收过状态但断流超过 5 秒时先显示接管/系统失联,再显示重启提示;大屏实现还会拒绝前一行车周期的旧告警。mici 的 AlertRenderer.get_alert() 有相同超时分支,但正常告警路径没有大屏的 recv_frame < started_frame 过滤——这是一个需要回放验证的旧告警可见性风险,不是已实测问题。语音失联告警由 soundd 负责,不能由普通 UI 插件替代。

    硬件与插件化边界

    显示平台适配需提供可靠触摸、屏幕电源、背光、GPU/EGL 或纹理路径和相机图像零拷贝能力,并监测触摸到反馈、消息到危险告警、屏幕唤醒与画面丢帧的真实时延。装饰性页面与指标可以插件化,但接管告警、失联占位、条款/培训、驾驶员监测提示及设置权限应由受信任核心约束;插件不能覆盖 selfdriveState 权威主题,也不能在未完成车型和安全校验时直接开启控制选项。

    现有 test_raylib_ui 只检查 UI 进程初始化;截图差异回放覆盖若干页面/告警视觉状态,但本次未运行。两种屏幕、触摸竞争、前后帧时延、长期断流与各语言文本溢出均尚未实测。

    因此插件化要区分「装饰」与「权威」:装饰性页面与指标可自由替换,而告警呈现、失联占位、条款培训与设置权限必须留在受信任核心;插件也不得在未完成车型与安全校验时开启任何控制选项。

    常见问题

    uiDebug.frameTimeMillis 是端到端显示时延吗?

    不是。生成器在绘制顶层 Widget 之后才结束本帧,本轮 UIState.update() 发生在绘制之后,新消息通常在下一帧才用于 Widget;因此该指标只是帧数据,不等于从消息发布到屏幕显示的端到端时延。

    UIState 的状态刷新是实时的吗?

    不是。状态订阅用非阻塞 SubMaster.update(0),而较慢的 Params 项由约 5 Hz 的后台线程刷新;屏幕关闭时生成器仍周期性 yield False 让 UIState 有机会更新并重新唤醒屏幕。

    UI 的 started 状态是怎么定的?

    UIState._update_state() 要求 deviceState.started 为真且自身缓存的点火为真,同时从 pandaStates/deviceState/selfdriveState 派生状态并通知边沿回调。

    行车告警的等待与失联阈值是多少?

    AlertRenderer.get_alert() 在 10 秒未收到首次行车 selfdriveState 时显示等待;comma 硬件上已收过状态但断流超过 5 秒时先显示接管/系统失联,再显示重启提示。mici 缺少大屏那层旧告警过滤,尚需回放验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UI_RUNTIME_DEEP_DIVE.md(UI 入口、UIState 聚合、页面导航与行车告警时延)
    • 项目 02 —— openpilot-research/CHESTNUT_USB_FIRMWARE_DEEP_DIVE.md(源文档指向的 Chestnut 固件刷写状态专题)
    • 项目 02 —— openpilot-research/UI_SIDEBAR_STATUS_FRESHNESS_ACTION_CONTRACT_DEEP_DIVE.md(源文档指向的侧栏状态新鲜度与动作门专题)

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

    更新记录

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

  • 横向限幅四种算法:主机候选与 panda 判决为什么不是一件事

    结论:横向限幅分成扭矩、转角、曲率三条路径,共享默认横向加速度约 3.6 m/s²、jerk 约 3.6 m/s³(含道路横滚容差)。它们只是主机侧的候选值计算,不能等同于 panda 的发送许可;同一场景下主机函数输出并不保证每个瞬时输入同时满足加速度与 jerk 两项物理限值。插件可以输出带单位的候选扭矩/角度/曲率,但固件必须保留独立最终许可。

    三类配置与四条算法路径

    AngleSteeringLimits、AngleSteeringLimitsVM、CurvatureSteeringLimits 分别对应速度表转角、车辆模型转角和曲率。它们的共同特点是:配置类不检查车型身份、传感器健康或 CAN 权限,具体值由车型提供。下面是四条主要路径:

    入口 主机行为 主要消费者
    apply_driver_steer_torque_limits() 据驾驶员力矩与 allowance/factor/multiplier 算上下限,再对上次请求作非对称上升/下降速率限制,最后取整 Hyundai、GM、Subaru、Mazda、MG、Rivian、Volkswagen 等扭矩车型
    apply_dist_to_meas_limits() / apply_meas_steer_torque_limits() 先约束命令与实测量的误差带和绝对上限,再按上次请求限制每帧变化;误差带以 STEER_ERROR_MAX 给零附近保留范围 Toyota、Chrysler 等 EPS 电机力矩测量
    apply_std_steer_angle_limits() 按请求是否同向增大绝对值,在两张车速-角速度表间选择;插值后限单步变化,停用横向时改用实测转角,再限绝对角度 Nissan、PSA、Toyota 角度路径
    apply_steer_angle_limits_vm() 用 MAX_LATERAL_JERK/v² 反算每次可变转角,用 MAX_LATERAL_ACCEL/v² 反算绝对角;车速至少取 1 m/s Tesla、BYD 以固定安全车型参数建模

    注意 apply_driver_steer_torque_limits() 里的 steer_max 可动态覆盖静态上限。VM 路径还会用 STEER_STEP*DT_CTRL 将角速度换算为每次发送增量——调用周期由车型控制器决定,不是函数自己假设的常量。

    非扭矩路径的数值顺序

    曲率路径 CurvatureSteeringLimits.apply_limits() 的裁剪顺序固定,且这个顺序会产生反直觉的结果:

    步骤 动作
    1 按 a_max/v² 限曲率
    2 按 jerk_max/v² * steer_step*DT_CTRL 限相对上次请求的增量
    3 停用时以调用方传入的当前曲率替换
    4 最后限固定 CURVATURE_MAX

    当上次命令在当前车速下已经超出新的加速度界限时,jerk 区间可能与加速度区间不相交;后一道 np.clip 会把先前的加速度限值「拉回」上次命令附近。源文档给了一个可复算的例子:用 Ford 量级的 CURVATURE_MAX=0.02 1/m、MAX_LATERAL_ACCEL=MAX_LATERAL_JERK=3、车速 20 m/s、上次与本次请求均为 0.02 1/m、steer_step=1,本地原函数输出 0.019925 1/m——它高于当前加速度导出的 3/20²=0.0075 1/m,但仍低于固定绝对曲率上限。

    这说明了什么?主机此函数不保证每个瞬时输入同时满足两项物理限值。源文档很克制地补了一句:这并非证明真实 Ford 配置或 panda 固件会放行该帧;实际车型的速度、加速度配置、帧步长及固件独立检查需另核对。停用时的行为也值得记住:曲率和两种转角函数先前的动态限幅结果会被当前测量值替换,再只受最终绝对上限约束。

    主机不等于固件:同一场景的 safety helper 对照

    板端 steer_curvature_cmd_checks() 对启用的曲率命令分别 OR 累积固定绝对曲率、按板端车速推导的横向加速度、每帧 jerk 和实时发报频率等违例——加速度检查不会被后续 jerk 检查覆盖。这与主机的顺序差异,正是「主机候选」和「固件判决」不是同一件事的根源。

    Ford FORD_STEERING_LIMITS 将 0.02 1/m 映射为 1000 CAN 单位,20 Hz 发报;主机 CarController 每五个 100 Hz 周期调用一次共享曲率限幅。跨层探针的对照结果是:车速 20 m/s、上次曲率 0.02 1/m 的候选值被拒;同速度下界内的 0.008 1/m 候选通过。源文档强调这是共享函数与独立板端 helper 的对照证据,不是实际整车拒帧率或驾驶性能证明。

    辅助逻辑:故障规避与摩擦前馈

    common_fault_avoidance() 只在连续 request && fault_condition 时累计;超过阈值暂时拉低请求位,到 max_above_limit_frames+max_mismatching_frames 重置计数。Hyundai 的大转角、Toyota/Subaru 的高转角速度等复用此逻辑。它有两条边界:返回的是新计数和请求位,不清除同帧扭矩,也不自行核验 MCU 对请求位不匹配的容忍度。

    摩擦侧,apply_center_deadzone() 在死区内把误差归零,get_friction() 在 +/-FRICTION_THRESHOLD 之间线性插值摩擦前馈、区间外饱和。摩擦输出按 friction*latAccelFactor 缩放,不是原始扭矩命令——把它当作扭矩值会算错整整一个量纲。

    插件与硬件建议

    源文档给出的插件契约是:把横向算法插件限制为输出带单位的候选扭矩/角度/曲率;宿主负责车型身份和参数绑定、消息新鲜度、活动门及主机限幅,panda 固件保留独立最终许可。每个参数包应带车型、总线布局、固件安全模式、版本和发帧周期,拒绝跨车型或不完整配置;动态标定不能自动改变固件许可。

    在验证缺口上,test_lateral_limits.py 只对非 mock、非 notCar 的扭矩平台做静态估算,且明确跳过非扭矩平台,不直接执行限幅函数,也不验证 CAN 帧或 panda 判决;CarController.update() 全路径、NaN、真实车辆 CAN、目标固件和实车反馈均尚未验证。硬件验证的顺序应是:离线回放/隔离台架比较主机候选值与固件判决,覆盖驾驶员抢方向、车速跳变、实测量失联、请求位切断以及停用瞬间,再讨论受控场地测试。

    常见问题

    主机限幅通过就等于车辆会执行吗?

    不等于。主机函数只产生候选值;steer_torque_cmd_checks() 与 steer_curvature_cmd_checks() 在 panda 内另有状态化检查(固件车速、双车速差、曲率绝对值、加速度、jerk、实测误差等)。源文档明确主机函数不是这些状态化检查的替身。

    为什么曲率限幅可能不满足加速度门?

    因为 CurvatureSteeringLimits.apply_limits() 的顺序是加速度 → 相对上次命令的 jerk → 停用时实测曲率 → 固定绝对曲率。当上次命令已超出新的加速度界限时,后一道 np.clip 会把先前的加速度限值拉回上次命令附近,因此不保证每个瞬时输入同时满足两项限值。

    3.6 m/s² 和 3.6 m/s³ 是通用限制吗?

    它们是三类配置共享的默认量级(含道路横滚容差),但具体值由车型提供。例如 VM 路径用固定安全车型参数,Ford 量级例子用的是 CURVATURE_MAX=0.02 1/m 与 MAX_LATERAL_ACCEL=MAX_LATERAL_JERK=3。所以不能把它当成跨车型的通用上限。

    摩擦前馈的输出是扭矩命令吗?

    不是。get_friction() 的输出按 friction*latAccelFactor 缩放,由 LatControlTorque.update() 消费,不是原始扭矩命令。混淆两者的单位会得到错误结果。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LATERAL_LIMITS_HOST_SAFETY_PLUGIN_CONTRACT_DEEP_DIVE.md(取用了四条算法路径、曲率裁剪顺序与数值例子、板端 helper 对照与插件建议)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 safety_tx_hook 与 CANPacker 在横向候选之后的分工)。

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

    更新记录

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

  • VIN 只校验 17 位格式:车型识别、ECU 固件匹配与启动边界

    结论:车型识别收集的是识别证据,不是车辆身份认证。is_valid_vin() 只检查 17 位字符格式,既不校验它与实车绑定,也不证明响应来源可信;CAN 指纹只是报文地址与长度的消元,不是对内容或 ECU 身份的认证。更关键的是,FirmwareQueryDone 只表示「固件查询这一步写完了」,此时 CarParams 尚未写入,pandad 还要等 ControlsReady 才读取它——它不等于车型可信,也不等于允许执行。以下基于源码快照 c8fb906 的静态阅读,未做诊断总线隔离、VIN 欺骗或双证据冲突验证。

    启动时序:等 CAN、识别、两个握手门

    Car.__init__() 先等首个非空 CAN、解析缓存,再调用 get_car()。can_comm_callbacks() 把 can/sendcan 消息转成识别函数需要的收发接口;obd_callback() 写参数并等待 pandad 确认,而这个握手没有显式超时。识别完成后,FirmwareQueryDone 在车型接口创建后写入,但此时 CarParams 还没写;pandad 的 fetchCarParams() 必须再等 ControlsReady 才读取它。随后 card 依据控制器是否存在、用户开关和 dashcamOnly 决定 passive,被动时改成 noOutput。

    阶段 静态行为 含义与边界
    缓存 非 mock、含固件、VIN 非未知且未设 DISABLE_FW_CACHE 时复用 CarParamsCache;仍重做 CAN 指纹 缓存是加速来源,不是当前车辆身份认证;使用时没有比对本次 VIN
    主动诊断 无合格缓存且未设 SKIP_FW_QUERY 时打开 OBD 复用,查询 VIN、ECU 地址和固件 会发诊断 CAN;结果可能因超时或局部失败不完整
    CAN FPv1 维护 bus 0/1 候选,对 11-bit 非排除地址按长度消元;frame>100 且唯一则成功,frame>200 终止 frame 是收到的 CAN 包计数,不是严格墙钟两秒
    固件结果 固件候选恰好一个时覆盖 CAN 结果;多个或零个不覆盖 exact 与 fuzzy 都可形成单一候选;不要求两类证据一致
    强制覆盖 非空 FINGERPRINT 最后覆盖所选车型,来源记为 fixed 开发便利入口,不适合作为实车执行许可
    未知车型 get_car() 将无候选回退到 MOCK 仍由后续参数/安全配置决定输出,回退不等于具备控制能力

    还有一个容易被误读的细节:FINGERPRINT 覆盖时只改车型和来源,exact_match 保留此前固件匹配阶段的值。因此 fuzzyFingerprint 也不能单独解释为「固定车型的可信等级」——来源与模糊标记是追踪元数据,不是交叉验证结果。

    VIN 与固件查询细节

    get_vin() 会在 bus 0/1 与 UDS、OBD 及品牌特例请求之间重试,找不到则返回 VIN_UNKNOWN。固件侧的查询则分几步:

    1. get_present_ecus() 按 OBD 复用模式分组,普通地址并行、子地址逐个探测;get_brand_ecu_matches() 统计可能品牌,但注释明确尚未检查响应总线。
    2. get_fw_versions_ordered() 按响应 ECU 数量及匹配比例排序;对某品牌查询后,只要该品牌固件给出单一候选就提前停止,因此查询结果未必覆盖所有品牌。
    3. get_fw_versions() 依据品牌的 FwQueryConfig 形成地址、总线、请求/响应前缀及 ECU 白名单;异常记录后继续,返回已取得的局部 CarFw。被 logging 或 extra_ecus 标记的响应不用于后续匹配。
    4. IsoTpParallelQuery.get_data() 并行发送请求,多帧接收会延长单地址超时,负响应 0x78 使用更长的 pending 超时;其余坏响应/异常/超时只影响相应地址,返回字典只含完成的响应。
    5. match_fw_to_car_exact() 要求数据库列出的必要 ECU 存在且命中版本;非必要 ECU 可缺失,但一旦出现就必须匹配。Ecu.debug 特殊跳过。
    6. 通用 fuzzy 排除摄像头、雷达、EPS 与 debug,只在至少两个不同 ECU 地址唯一指向同一车型时给出匹配;match_fw_to_car() 先 exact 后 fuzzy,最终可能多个候选,只有唯一候选才在外层覆盖 CAN。
    匹配策略 成立条件 结果
    exact 数据库列出的必要 ECU 存在且命中版本 形成候选;非必要 ECU 可缺失,但一旦出现就必须匹配
    通用 fuzzy 至少两个不同 ECU 地址唯一指向同一车型 形成候选;品牌自定义回调也可能参与
    外层覆盖 match_fw_to_car() 先 exact 后 fuzzy 后只剩一个候选 只有唯一候选才覆盖 CAN 结果

    重新设计识别时的边界

    以下为设计建议,不是现有代码已实现的保障。可以把 VIN、ECU、CAN 和硬件拓扑检测设计成只读的「证据插件」,结果携带采集时间、总线、来源、完整性及冲突信息;插件不得直接设置车型安全模式或发 sendcan。由受信任的启动策略统一裁决:缓存只作性能优化,实车执行前需本次车辆证据复核;fuzzy、环境强制车型、证据冲突或版本不明时默认只读或 noOutput。OBD 复用、诊断请求和 CAN 发送应视为能力而非普通插件 API,限定唯一所有者、允许的总线/地址/速率与生命周期。最后,要保留 panda MCU 的独立限幅、心跳与安全模式——车型识别成功不等于安全配置可执行。

    验证缺口同样明确:现有 CAN 指纹测试用离线 FPv1 样本检查识别与包计数,固件指纹测试覆盖 exact、fuzzy 与数据库一致性;但本文未运行测试,也未验证诊断总线隔离、VIN 欺骗、缓存跨车误用、双证据冲突、OBD 握手阻塞与掉电恢复。此外 fingerprintSource/fuzzyFingerprint 在本快照中主要被写入 CarParams,未见 card 或 pandad 用这两个字段实施独立许可,因此不能把它们当成现成安全门。

    常见问题

    VIN 校验通过,能说明识别到了真车吗?

    不能。is_valid_vin() 只检查 17 位字符格式,既不校验它与实车绑定,也不证明响应来源可信。识别结果应携带来源与完整性信息,由上层策略裁决。

    FirmwareQueryDone 是不是代表车型已经可信?

    不是。它只表示固件查询这一步写完,此时 CarParams 尚未写入;pandad 还要等 ControlsReady 才读取。它既不代表车型可信,也不代表允许执行。

    CAN 指纹里的 frame>100 是两秒计时吗?

    不是。frame 是收到的 CAN 包计数,不是严格墙钟两秒;frame>100 且唯一则成功,frame>200 终止。采集字典也不等于可信身份。

    固件匹配唯一,就能直接覆盖 CAN 指纹结论吗?

    只有唯一候选才会在外层覆盖 CAN 结果。exact 与 fuzzy 都能形成单一候选,当前不要求两类证据一致;多个或零个候选时不覆盖。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAR_FINGERPRINT_FW_MATCH_DEEP_DIVE.md(车型识别、VIN 与 ECU 固件匹配,含源码行号索引)
    • 项目 02 —— openpilot-research/ACTUATION_GATE_CHAIN_DEEP_DIVE.md(源文档指向的最终执行门专题,供识别与执行的边界延伸)
    • 项目 02 —— openpilot-research/SOFTWARE_MINDMAP.md(源文档指向的主系统结构)

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

    更新记录

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

  • 600 秒预算与三次重试:外置模块刷写为何不能只看退出码

    结论:外置计算模块的固件刷写里,脚本退出码 0 不等于升级完成。刷写脚本对镜像只做长度上限、魔数、字节校验和与 CRC32 的格式/传输完整性校验,它不是来源认证或数字签名验证;更关键的是,ROM 恢复已提交但设备没有重新出现时,脚本仍会正常返回,上层据退出码 0 记为「刷写成功」,而此时运行态镜像并未被回读校验。正确的做法是把「发现、写入、激活、运行」拆成六种状态,不让一个布尔值吞掉它们。

    它是什么,也不是什么

    该模块通过 USB/PCIe 接入外置 GPU,服务于大模型推理,属于推理设备。它不是车辆 CAN 安全网关:车辆控制输出仍须经过原有的车辆接口、进程与 panda 安全链。这条定位决定了后面所有设计约束——外置模块可以失联、可以回退,但不能绕过控制安全链。

    发现与状态:USB 快照不是同步断线检测器

    设备发现从系统总线读取 VID/PID、链路速率、产品字符串、链路错误与接口朝向,再写入设备状态;其中「模块在位」的判定只识别运行态 VID/PID,不要求产品字符串版本匹配。状态线程约每 10 秒或总线拓扑变化时更新一次快照,队列容量为 1,主线程可以沿用旧值。因此它是一份采样快照,不是同步的断线检测器——真正判断「用哪个模型」的是另外两层证据。

    刷写路径:格式校验不等于来源认证

    刷写入口读取内置封装镜像,检验封装格式、产品字符串与调用方期望版本;已匹配则跳过,并设定 600 秒全局预算。镜像校验检查的是长度上限、魔数、字节校验和与 CRC32——这些字段能发现传输或格式损坏,但不能证明镜像来源或版本内容不可替换。固件写入按 4 KiB 扇区擦除、128 B 页写入,并逐页与整扇区回读验证;配置页备份为 0x100 字节,且备份文件以主机名确定路径,已有备份会优先于当前设备配置。

    校验 / 步骤 检查什么 不保证什么
    格式与产品字符串 封装结构、目标产品名、期望版本 不证明镜像内容可信
    长度上限 / 魔数 / 校验和 / CRC32 传输与格式完整性 不是来源认证或数字签名
    页 / 扇区回读 写入范围与内存目标一致 不证明新固件已启动
    配置页备份(0x100 字节) 保留设备侧配置 备份与具体设备的绑定需台架确认

    六种状态不该合并成一个退出码

    刷写与恢复会经过多条分支,每条分支得到的「证据」并不相同:首次发现时无设备会直接正常返回(没写入、没激活);产品字符串已匹配且未强制刷写会直接返回(只核对枚举字符串,没读闪存);普通路径完成写入并回读一致后才调用激活;而 ROM 路径若已提交但仍停留在 ROM 或已离线,恢复函数返回失败,外层却正常返回,于是退出码 0 被记成「成功」。此外,激活函数的所有普通完成分支都返回空值——调用方无法从返回值判断设备到底是否已激活并重枚举。

    源码分支 脚本退出码 / 上层状态 实际得到的证据
    首次发现时无设备 正常返回,记为已刷写 仅说明发现时无设备,未写入、未激活
    产品字符串已匹配且未强制 正常返回,记为已刷写 只核对枚举字符串,未读闪存、未验证 GPU/PCIe
    普通路径完成写入并回读 调用激活,正常返回 写入范围与内存目标一致,不一定代表已启动
    ROM 已提交但仍停留 ROM 或离线 恢复返回失败,外层正常返回 未完成运行态回读校验,属「提交成功 / 验证完成」混淆
    镜像、备份、读写或预算抛异常 退出码 1,记为失败 明确失败,但不自动说明设备是否仍可恢复

    重试额度同样是状态的一部分:尝试次数在进程生命周期内不复位,产品字符串恢复匹配只清「已刷写」标志,不清尝试次数;同一进程用满三次后,即使后来版本又出现不匹配,也不会再自动重试。600 秒预算是协作式收敛预算,只在重试/恢复循环的调用点检查,不是能中断所有 USB 操作的硬截止。

    模型回退与插件约束

    推理侧的设计是分层的:先尝试加载并预热大模型,超时或失败则走小模型;运行中大模型异常也会回退,并把状态写入消息供上层生成加载/失败事件。监测主题标称 10 Hz,但 USB 不存在时监测线程不会按 10 Hz 发送有效消息。插件化时,固件维护接口应返回分离的状态:未找到、已匹配、已写入并验证、等待激活、运行态已验证、失败,并附目标设备身份、镜像摘要、采样时间与失败阶段;重试额度应绑定设备/版本/维护事务,而不是复用进程内布尔值。安全侧只有运行态重新枚举、产品版本与推理健康全部确认后,才能把模块标记为可用——这仍不能替代车辆控制安全链的验证,外置模块也不得获得发送 CAN 的权限。

    常见问题

    镜像通过 CRC32 校验,就能说明固件可信吗?

    不能。长度上限、魔数、字节校验和与 CRC32 属于传输与格式完整性校验,用于发现损坏或截断;它不是来源认证或数字签名验证,改变镜像内容并重新计算校验值后仍可通过校验,因此不能据此判断镜像来源或版本不可替换。

    刷写脚本返回成功,就代表升级完成了吗?

    不一定。ROM 恢复已提交但设备未重新出现时会返回失败,外层却正常返回、退出码 0 被记成成功,而此时还没有完成运行态镜像校验;激活函数也无法从返回值告知调用方设备是否真的已激活并重枚举。

    为什么要有三次尝试上限?

    为了限制自动刷写的次数与风险。尝试次数在进程生命周期内不复位,产品字符串恢复匹配也只清「已刷写」标志、不清次数,因此同一进程用满三次后不会再自动重试,避免在异常设备上反复写入。

    外置计算模块能接管车辆控制吗?

    不能。它只是推理设备,车辆控制输出仍须经过原有的车辆接口、进程与 panda 安全链;固件升级是驻车维护事务,模块本身不得获得发送 CAN 的权限,也不能替代独立安全网关。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CHESTNUT_USB_FIRMWARE_DEEP_DIVE.md(USB 发现、固件刷写路径、镜像与备份、ROM 恢复、模型回退与插件约束)

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

    更新记录

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

  • Hyundai CAN FD safety:LKA 与 LFA 的总线矩阵差在哪

    结论:Hyundai CAN FD safety 的「模式」不是一个开关,而是LKA/LFA × 纵向/非纵向 × 按钮格式 × SCC 来源的组合,每一组对应不同的 RX 检查表与 TX 白名单。纵向(LONG)只在 ALLOW_DEBUG 构建下生效:无该宏的构建强制 hyundai_longitudinal=false,因此上层设置 openpilotLongitudinalControl 不会自动改变固件事实。此外,替代转向地址是条件性接口不匹配,不能靠扩充普通插件的 CAN 发送权限来规避。

    车型资格与总线编号:LKA 与 LFA 是两套拓扑

    资格判定在 CarInterface._get_params():根据相机指纹判定 LKA/LFA,确定按钮、SCC 来源和 safetyParam。源文档特别强调,alphaLongitudinalAvailable、实际启用和 ECU 静默成功是三个不同状态,不能混为一谈。

    总线编号由 CanBus.__init__() 决定,而且两种模式恰好相反:

    模式 A-CAN E-CAN 相机
    LKA 0 1 2
    LFA 1 0 2

    相机总线固定为 2,再叠加多设备偏移。这里要注意:表中 bus 号是单个 panda safety 实例的本地编号,多 panda 场景的偏移由上层配置处理,不能把逻辑编号硬编码成自制板引脚。

    模式与总线矩阵

    hyundai_canfd_init() 用 LKA/LFA、LONG、替代按钮、相机 SCC 和替代转向地址选择 RX 检查与 TX 白名单。下面这张表是整篇的核心:

    模式 动力总线 / 原车 SCC RX 关键 TX 白名单
    LKA 非纵向 1 / 1 0x50+0x2A4 或替代 0x110+0x362 在 bus 0;0x1CF 在 bus 1
    LKA 纵向 1 / 不检查原车 SCC LKA 0x50+0x2A4 在 bus 0;LFA、HUD、SCC、若干 ADRV 与 0x730 在 bus 1;0x51 在 bus 0
    LFA 雷达 SCC 非纵向 0 / 0 LFA、HUD、0x1A0 在 bus 0;0x1CF 在 bus 2
    LFA 相机 SCC 非纵向 0 / 2 上行相同,另有 0x160 在 bus 0
    LFA 雷达 SCC 纵向 0 / 不检查原车 SCC LFA、HUD、SCC、0x160、0x7D0 在 bus 0;0x1CF 在 bus 2
    LFA 相机 SCC 纵向 0 / 不检查原车 SCC LFA、HUD、SCC、0x160 在 bus 0;0x1CF 在 bus 2

    表头里的「原车 SCC RX」指的是 safety 必需 RX 检查与巡航状态消费,不是该总线上物理帧是否存在。几个细节:LKA 两种分支的按钮 RX 固定使用 0x1CF;LFA 分支依据替代按钮标志,选择动力总线上的 0x1CF 或 0x1AA。所有配置均监视油门备选组、制动 0x175、轮速 0xA0、驾驶员扭矩 0xEA——油门三种地址是同一检查组的备选格式,不等于三种都必须到达。

    构建门:LONG 参数只在 ALLOW_DEBUG 下生效

    hyundai_common_init() 只在 ALLOW_DEBUG 下读取 LONG 参数;无该宏的构建强制 hyundai_longitudinal=false。这是一道编译期门,而不是运行期开关。

    校验侧码分布同样要记住:hyundai_canfd_get_counter() 对 8 字节普通按钮取字节 1 高四位的计数,其余帧取字节 2;校验值取前两字节。hyundai_common_canfd_compute_checksum() 对字节 2 起的载荷与地址执行 CRC16,24/32 字节分别异或长度常数。特别之处在于:按钮 RX 明确忽略 checksum,但仍检查计数。

    权限与失效边界

    这些边界在源文档里逐条列出,直接决定「白名单里有这帧」和「这帧会被执行」之间的差距:

    1. 按钮:普通按钮 TX 0x1CF 仅允许「原车巡航已接合时 Cancel」或「controls_allowed 时 Resume」。替代按钮 RX 0x1AA 不在 TX 白名单中;但设置替代按钮并不会从全部 TX 表删除 0x1CF,不能误写成「该模式彻底禁止发送任何按钮帧」。
    2. 转向:非纵向 LKA 对 0x50/0x110 检查扭矩;LKA 纵向及全部 LFA 对 0x12A 检查。共享扭矩门限制最大 270、实时变化 112、每帧增加 2/减少 3,并含驾驶员反扭矩与转向请求位节拍。
    3. SCC:非纵向 LFA 的 0x1A0 仅允许 ACCMode=4 且 aReqRaw/aReqValue 同时为零;该钩子未逐位审核 0x1A0 的其它信号。
    4. 诊断与附属帧:0x730/0x7D0 的 8 字节数据仅可为 02 3E 80 00 00 00 00 00,且仅相应模式在白名单中。0x51、0x160、0x1EA、0x200、0x345、0x1DA、HUD 和相机抑制帧没有同等级的专属载荷校验。
    5. 转发:Hyundai CAN FD 没有专属 fwd 钩子,使用通用 0↔2 转发规则及 check_relay 静态阻断;bus 1 不在该通用转发对内。

    第 4 条尤其重要:白名单准入不等于 ECU 接受,也不等于 AEB 不受影响。缺少逐字段校验的帧,只能保证「被允许发送」,不能保证「被接受后效果正确」。

    替代 LKA 与纵向的跨层组合

    CarInterface._get_params() 可从相机指纹识别 0x110 替代 LKA,并把该标志传到 safety 参数;若有 ADAS ECU 且未被其他条件禁止,纵向能力仍可能开启。此时主机侧 create_steering_messages() 继续生成 0x110 LKAS_ALT,create_suppress_lfa() 继续生成 0x362。

    但 hyundai_canfd_init() 的纵向 LKA 分支固定选择普通 0x50/0x2A4 白名单,而非按替代标志切换;原测试只有普通 LKA 纵向类,没有替代 LKA 纵向类。离线探针确认:同一替代标志加 LONG 后,调试构建拒绝 0x110/0x362、接受 0x50/0x2A4;无调试宏构建则忽略 LONG,采用非纵向替代 LKA 白名单。也就是说该组合在两类构建中的失败形态不同,均不能仅凭主机 CarParams 宣称纵向或替代转向可执行。

    源文档对此的定性是关键:这是条件性接口不匹配,不应通过扩充普通插件的 CAN 发送权限来规避;正确做法是先确定该车型/纵向组合是否应被允许,再联合修订主机消息选择、固件白名单、转向限制和回归测试。现有 test_hyundai_canfd.py 在测试基座默认编译 ALLOW_DEBUG;本机运行该模块共 981 项、117 项跳过,跳过项及未覆盖的替代 LKA 纵向组合不能视为验证通过。

    常见问题

    LKA 和 LFA 的总线编号为什么会相反?

    因为 CanBus.__init__() 明确按模式分配:LKA 时 A-CAN/E-CAN 为 0/1,LFA 时为 1/0,相机固定为 2。这是逻辑编号,多 panda 场景的偏移由上层配置处理,不能直接当作物理引脚。

    启用纵向控制后,固件就一定允许吗?

    不一定。hyundai_common_init() 只在 ALLOW_DEBUG 下读取 LONG 参数,无该宏的构建会强制 hyundai_longitudinal=false。所以上层设置 openpilotLongitudinalControl 不会自动改变固件事实,alphaLongitudinalAvailable、实际启用和 ECU 静默成功是不同状态。

    0x1A0 在白名单里就可以自由发送吗?

    不可以。非纵向 LFA 的 0x1A0 仅允许 ACCMode=4 且 aReqRaw/aReqValue 同时为零;纵向则分别调用共享加速度检查。而且该钩子并未逐位审核 0x1A0 的全部其它信号。

    为什么设置「替代按钮」不等于禁用 0x1CF?

    因为替代按钮 RX 0x1AA 虽不在 TX 白名单,但设置替代按钮并不会从全部 TX 表删除 0x1CF。现有 test_button_sends() 构造的是替代格式 0x1AA,因此没有证明 0x1CF 被禁用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HYUNDAI_CANFD_PANDA_SAFETY_DEEP_DIVE.md(取用了模式与总线矩阵、构建门、权限与失效边界、替代 LKA 与纵向跨层组合)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 Hyundai CAN FD 分支与 set_safety_hooks() 的注册位置)。

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

    更新记录

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

  • panda 主机 CAN 组包:70 字节线格式与 XOR 校验为什么晚于安全钩子

    结论:主机到 MCU 的 CAN 字节流是一台分片组包机:CANPacket_t 头部固定 6 字节,DLC 查表最长对应 70 字节线格式,packed, aligned(4) 结构在本机编译为 72 字节。校验顺序是「先安全钩子、后 XOR 校验」:can_send() 先运行 safety_tx_hook(),坏 XOR 帧虽然不会上总线,却可能先触达有状态的安全钩子。returned 回执只代表已提交硬件 FIFO,不等于车端已接收。

    6 字节头与 72 字节结构:线格式到底传多少

    报文的字节布局由 CANPacket_t 与 pack_can_buffer() 定义:6 字节头包含 DLC、bus、FD、地址、标志与 XOR;DLC 查表对应 0–64 字节载荷,因此最长有效线格式为 70 字节。C 结构带 packed, aligned(4),在本机编译为 72 字节,尾部的对齐字节不应传输。

    Python 编码显式组出首字节 DLC:4 | bus:3 | fd:1、随后四字节小端地址/状态位和 XOR;原生 C++ can_header 则把首位命名为 reserved,发送时零初始化。两者的 6 字节头部形状相同,但发送 FD 的策略不同——这一点在下一节展开。

    环节 关键值 含义
    头部长度 6 字节 DLC、bus、FD、地址、标志与 XOR
    载荷范围 0–64 字节 由 DLC 查表决定
    最长线格式 70 字节 6 字节头 + 最大载荷
    本机 C 结构 72 字节 尾部对齐字节不传输
    MCU 写缓冲 72 字节数组 容纳一个最大协议帧

    主机到车辆:一半包留在哪里、什么时候补齐

    Python 把完整消息串接成字节流:can_send_many() 经 USB EP3,SPI 由 endpoint3 自身事务长度承载,两者进入同一 comms_can_write()。MCU 侧用 can_write_buffer 保存不完整帧及剩余字节数;补齐后构造 CANPacket_t 并调用 can_send(..., false),其余输入逐帧处理。

    环节 实现 行为
    主机分块写入 can_send_many() / USB EP3 / SPI endpoint3 USB 按客户端块切分,SPI 按自身事务长度;进入同一 comms_can_write()
    MCU 写入组包 comms_can_write() 半包存于 72 字节缓冲;补齐后调用 can_send(..., false)
    MCU 读出分片 comms_can_read() 先发上次帧尾,再取 RX 队列;按 max_len 返回实际字节数
    主机接收重组 can_recv() / unpack_can_buffer() 拼接残包,逐帧验证 XOR 并保留下次所需残包
    状态清空 comms_can_reset() 连接时主机发 0xc0,清 MCU 读/写半包指针;不清 CAN RX/TX 队列
    容量流控 refresh_can_tx_slots_available() 三路 TX 队列同时满足 USB 51 帧或 SPI 170 帧余量时恢复接收

    两个边界必须分清:读出侧不保证传输块与 CAN 帧边界重合,所以主机必须能拼接残包;主机缓存与 MCU 的两个缓存(写半包、读残包)是三份不同状态。另外,51 帧 / 170 帧是背压门槛,不代表安全钩子放行或总线已发送。

    校验与许可的真实顺序

    这是整篇文章最容易被写反的一点。对完整帧,comms_can_write() 直接调用 can_send();can_send() 先运行 safety_tx_hook(),再决定入 TX 软件队列或生成 rejected 回执。直到 process_can() 从队列取出时,才运行 can_check_checksum();校验失败只增加 total_tx_checksum_error_cnt。

    顺序 环节 结果
    1 USB/SPI 字节进入 comms_can_write 组出完整包
    2 can_send 调用 safety_tx_hook 拒绝则 rejected 回执入 RX 队列
    3 允许则入 TX 软件队列 尚未校验 XOR
    4 process_can 校验 XOR 错误则增加 tx checksum error
    5 正确则提交 FDCAN TX FIFO 产生 returned 回执

    由此得到两个结论:坏 XOR 帧不会由这条发送路径上总线,但可能先触达有状态的安全钩子——例如 Toyota 的 toyota_tx_hook() 会调用 steer_torque_cmd_checks(),其中可能更新 desired_torque_last。源文档对此的措辞很克制:具体可触发条件及后续有效帧影响需要隔离测试,本文不宣称已复现控制偏差。另一个结论是:returned 仅代表提交硬件 FIFO,不能解释为车端已确认接收。

    读出侧还有一个实现细节:comms_can_read() 用自定义 memcpy() 把残包向左搬移。当前实现是正向复制,因此该方向可工作;但换成标准库 memcpy 后,重叠内存没有可移植保证。

    CAN FD 位:主机两条路径的差异

    原生 pandad connect() 给三路总线打开 canfd_auto,而原生 pack_can_buffer() 不显式置 FD 位。板端 process_can() 据 canfd_auto ? canfd_enabled : to_send.fd 决定 FDF,其中 canfd_enabled 在收到 CAN FD 帧后才设置。因此在首次收到 FD 帧之前,原生主机候选帧的 FDF 取决于板端尚未观察到 FD 的状态;即使 DLC 表可编码 12–64 字节,也不能据此断言硬件已经按 CAN FD 发送。

    Python 侧相反:Panda.connect() 关闭自动切换,pack_can_buffer(..., fd=...) 明确设置首位。此外固件 0xdd 暴露的 CAN 包版本由 can.h 原文字节 SHA-256 前四字节形成;Python 的 can_send_many()/can_recv() 用装饰器比对这个值,而原生 C++ pandad 不显式调用 0xdd,主要依赖固件签名一致性门。该哈希不是对主机 can_header 源文件或运行时硬件 FD 状态的证明。

    插件化边界

    源码 test_comms.py 有半包 reset、随机 64 字节块写入和 5 万条接收帧重组用例,但这些用例在 allOutput 模式下验证主要组包行为,没有直接证明坏 XOR 帧经过车型安全钩子前后状态不变,也不覆盖中断并发、物理 CAN 时序或全部车型许可分支。

    对自制硬件或「万物即插件」重构,主机 CAN 流适配、分块与遥测可以插件化,但需要在受信任入口先验证包长、DLC、bus、XOR/传输完整性,再交给有状态的安全策略;最终发送仍须固件独立许可。插件不得通过原始 USB/SPI 端点绕过验证,也不能把 returned 回执解释为车端已确认接收。

    常见问题

    为什么说 XOR 校验晚于安全钩子?

    因为完整帧进入 comms_can_write() 后直接调用 can_send(),而 can_send() 先运行 safety_tx_hook(),之后才入 TX 队列;can_check_checksum() 要等到 process_can() 从队列取出时才执行。所以坏 XOR 帧不会上总线,却可能先触达有状态的安全钩子。

    6 字节头和 72 字节结构到底哪个上总线?

    线格式只传 6 字节头加 DLC 对应的有效载荷,最长 70 字节;data 偏移为 6。带 packed, aligned(4) 的完整 C 结构在本机编译为 72 字节,但其尾部对齐字节不应传输。

    returned 回执能说明车端收到了吗?

    不能。returned 仅代表已提交硬件 TX FIFO,不等于车端 ECU 已确认接收。源文档明确要求不要把它解释为车端确认。

    为什么主机两条路径的 CAN FD 行为不同?

    原生 pandad 给三路总线打开 canfd_auto,且原生 pack_can_buffer() 不显式置 FD 位,FDF 由板端在收到 FD 帧后才置的 canfd_enabled 决定;Python 的 Panda.connect() 关闭自动切换,由 pack_can_buffer(..., fd=...) 明确设置首位。新硬件须分别验证这两条路径及冷启动。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_CAN_COMMS_FRAGMENT_ASSEMBLY_DEEP_DIVE.md(取用了 CANPacket_t 字节布局、组包函数契约、安全钩子与 XOR 的真实顺序、CAN FD 位差异与插件边界)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 can_comms.h 调用 can_send(..., false) 与 safety_tx_hook 的衔接位置)。

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

    更新记录

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

  • 163 个 IRQ 槽与寄存器巡检:panda 诊断契约里的四处不一致

    结论:panda 的故障、寄存器巡检与制造身份是一条观测链,不是执行许可链。故障路径只把「记录过的 fault」送到 cereal.PandaState;「记录了 fault」不等于执行器已被独立切断。并且源码里至少存在四处静态契约不一致:寄存器哈希上界越界 1 项、故障位图与状态不是同一真值、位 27 的故障无法被正确表达、TIM1 更新中断的向量符号与 C 包装器不匹配。以下基于源码快照 c8fb906,未做实机故障注入、内存安全与制造写入验证。

    故障路径:从位图到 PandaState

    故障聚合由 fault_occurred() 与 fault_recovered() 完成,维护位图、首次打印与状态级别;本版本 PERMANENT_FAULTS=0。get_health_pkt() 把 fault_status 与 32 位 faults 作为两个独立字段返回,fill_panda_state() 再把健康字段映射进 PandaState,故障位转列表,枚举定义在 log.capnp。整条链的职责是观测:实际 CAN 许可仍须另追 safety hook、心跳与主机执行门。

    组件 关键实现 作用与边界
    临界区 ENTER/EXIT_CRITICAL、enable/disable_interrupts 嵌套深度与全局使能状态共同决定退出时是否恢复 IRQ;这不是跨核锁。
    IRQ 统计 handle_interrupt()、interrupt_timer_handler() 中断调用计数与忙闲时间;每秒换算负载,超出注册上限记故障。
    寄存器跟踪 register_set()、check_registers() 写寄存器时记录期望值/掩码,主固件每秒检查漂移;仅覆盖经此 API 登记且掩码不含硬件自变位的寄存器。
    故障聚合 fault_occurred()/fault_recovered() 位图聚合、首次打印与状态级别;本版本 PERMANENT_FAULTS=0。
    身份读取 get_provision_chunk()、控制请求 0xd0 读固定地址的 32 字节制造块或 16 字节序列号;全 0xff 时返回内置 unprovisioned 占位值。

    中断速率与负载的真实语义

    临界区实现值得单说:ENTER_CRITICAL() 直接执行 __disable_irq() 并把全局 uint8_t global_critical_depth 加一,EXIT_CRITICAL() 减一,仅在深度回到零且 interrupts_enabled 为真时才 __enable_irq()。这是单核全局 IRQ 屏蔽与嵌套约定,不是自动保存/恢复进入时的 PRIMASK,也不是按外设分别关中断;若绕过包装器直接改 PRIMASK,或进入/退出不配对,8 位深度可下溢或回绕;源码没有运行期断言阻止这两种误用。enter_stop_mode() 刻意使用原始 __disable_irq(),清理 NVIC、只留唤醒源,WFI 返回后立即请求系统复位——这段代码不能当作可返回的通用临界区。

    本版本为 IRQ 分配 163 个槽位,init_interrupts(true) 先把每个外设槽设为「未使用」处理器,再启动 TIM6 每秒统计中断。每次中断先增 call_counter 再调用处理器,若当前秒内次数大于登记上限就 fault_occurred();interrupt_timer_handler() 每秒把计数复制到 call_rate 并清零。这里没有对速率故障调用 fault_recovered(),所以即使下一秒速率恢复,故障位仍可能保留,不能把这一位解释为「当前这一秒仍超限」。

    字段/入口 取值与换算 不能代表什么
    call_rate / EP0 0xc4 上一个完整统计窗的 IRQ 调用次数,Python 侧再解码为整数 不是当前秒的实时值
    interrupt_load busy_time/(busy_time+idle_time),量化到 0..255,主机除以 255 是 ISR 占用比例,不是 MCU 总 CPU 使用率
    速率故障位 超限即置位,不做自动恢复 不等于设备已停止 CAN 或复位

    普通主循环或非 IRQ 的阻塞时间会计入「闲」,因此不能仅靠 interruptLoad 证明控制任务有足够计算余量。无效 IRQ 号在固件侧没有 4 字节响应。

    寄存器巡检与哈希上界

    register_set() 在写寄存器时记录期望值与掩码,主固件每秒调用 check_registers() 检查漂移。它的覆盖范围有明确前提:只包含经此 API 登记、且掩码不含硬件自变位的寄存器。更值得记录的是数组容量与索引的错位:容量是 0x3ff 个元素,合法索引为 0..0x3fe;但 hash_addr() 用 & 0x3ff 计算,冲突检查又先以该值索引。若某个寄存器地址或再探测值哈希到 0x3ff,就会越界,且 check_registers() 不会扫描该槽。触发地址及硬件后果需要针对实际地址清单验证。

    四处静态契约差异

    下面这些差异都属于「代码能编译、单看一侧也自洽,但两侧对不上」的类型:

    差异 源码事实 含义
    寄存器哈希上界 容量 0x3ff,但 & 0x3ff 可算出 0x3ff 该槽越界且不被巡检
    状态与位图 fault_recovered() 仅清位图,不重算 fault_status 位图可为 0 而状态仍为 TEMPORARY
    故障枚举范围 固件位 27 有定义,但 cereal 与 pandad 止于位 26 该故障不能被正确表达
    TIM1 更新向量 汇编指向 TIM1_UP_IRQHandler,C 包装器却是 TIM1_UP_TIM10_IRQHandler 当前 NVIC 该通道被禁用,未触发

    关于 TIM1:汇编向量槽与弱别名把它默认映射到死循环,而 C 包装器定义的是另一个符号名。静态比对 140 个非保留外设向量与 140 个 C 包装器后,除芯片头文件明确支持的 COMP_IRQHandler 别名外,只有这一组不匹配。当前时钟输出初始化虽置了 TIM1 更新中断使能位,却明确 NVIC_DisableIRQ(TIM1_UP_TIM10_IRQn),因此不能表述为已触发的运行期故障;若后续启用该通道,须先校正向量符号并在无负载台架验证。另外 body 的诊断能力不同:它用 tick 与中断计数,但没有普通 panda tick 中的寄存器巡检,其 EP0 请求分派也不是普通 get_health_pkt() 契约。

    制造身份与插件界线

    制造身份应被当作受控的只读能力:MCU UID、OTP 序列号与 provision chunk 分开建模;未知或全 0xff 必须显式标记「未灌录」,不能用测试占位值生成生产身份或信任根。诊断插件可以订阅、分类和呈现故障,但不得单凭 fault_status 清除控制禁用——安全控制权属于独立固件与经验证的主机门控。台架验证应覆盖寄存器哈希边界、故障出现/恢复的双字段序列、位 27 的序列化,以及 IRQ 过载下健康包的发布时延,且都在无车辆执行器的隔离环境完成。

    常见问题

    panda 记录了 fault,执行器就已被切断了吗?

    没有。故障路径只是把故障位与状态送到 PandaState,属于观测链;实际 CAN 许可由 safety hook、心跳与主机执行门决定。

    速率故障位会自己恢复吗?

    不会自动恢复。超限时置位,但代码没有对速率故障调用 fault_recovered(),所以即使下一秒速率恢复,故障位仍可能保留,不能当作当前秒的实时读数。

    interruptLoad 能代表 MCU 的总 CPU 占用吗?

    不能。它只统计 ISR 的忙闲比例,普通主循环或非 IRQ 的阻塞时间会计入「闲」。因此它不能单独证明控制任务有足够计算余量。

    为什么故障位图是 0,状态却仍显示临时故障?

    因为 fault_recovered() 只清位图,不重算 fault_status。消费端应把位图与状态分别解释,不能只读状态来推断当前故障。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_FAULT_REGISTER_PROVISION_CONTRACT_DEEP_DIVE.md(panda 故障、寄存器巡检与制造身份契约,含源码行号索引)
    • 项目 02 —— openpilot-research/PANDA_BOOTSTUB_SIGNATURE_FLASH_BOUNDARY_DEEP_DIVE.md(同目录 panda 启动与维护面专题,供制造身份边界的延伸阅读)

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

    更新记录

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

  • selfdrived 告警仲裁:99 个 EventName 里只留 1 条上屏

    结论:同一帧里可以同时成立多个故障事件,但最终屏幕文字、声音与 HUD 图像只会呈现一条告警。selfdrived 用「事件容器(有序但不去重)→ 状态机(按类型优先级)→ AlertManager(按 alert_type 每帧留一条)」这三段流水线完成仲裁,任何一段的边界都会让结果和直觉不同。本文为固定源码快照 c8fb906 的静态分析,不等于独立硬件安全保证。

    从车辆状态到消息输出

    SelfdriveD.__init__() 等待或接收 CarParams,创建车辆事件适配、消息订阅、告警管理器和状态机。carState 用独立 socket 接收,以它作为步进输入;data_sample() 再非阻塞更新其余主题。初始化须满足 CS.canValid && sm.all_checks(),或约 6 秒超时进入初始化完成路径;这不是「超时后所有输入都变有效」,后续仍会创建故障事件。

    step() 每次先采样、聚合事件;只有 CP.passive=False 且完成初始化才调用状态机;随后更新告警并发布消息。params_thread() 约每 100 毫秒刷新单位、车道偏离、油门踏板退出、实验模式和驾驶风格。异步参数刷新与控制步进并行,不能假设 UI 写完 Params 后下一条 selfdriveState 已同步使用全部设置。

    publish_selfdriveState() 将 enabled、active、状态、engageable、告警文案/声音/HUD 图像写入 selfdriveState;engageable 的条件是当前没有 NO_ENTRY 事件,并不等于当前已使能。onroadEvents 每约 1 秒或事件名单变化时发布。controlsd 用 enabled/active 再结合车辆故障、静止门和纵向覆盖决定 latActive/longActive;pandad 还要求 selfdriveState 消息 alive/valid 才把 engaged 告知 panda。软件状态机、控制器活动门和 panda 固件安全门是三个不同层次。

    事件容器为什么不去重

    Events 保存当前帧有序事件、静态事件以及每个 EventName 的连续出现计数。clear() 先更新计数再仅保留静态事件,随后 SelfdriveD.update_events() 按来源追加动态事件。contains(ET) 查询事件的类型映射;create_alerts() 只为本状态允许的类型创建 Alert,并用 creation_delay 抑制短暂出现的告警。

    add() / add_from_msg() 用 bisect.insort 排序,但不去重;重复事件会产生重复的 create_alerts() 结果,计数器仍按「该事件名是否出现」每帧只加一次。static=True 会同时加入静态列表和当前列表,连续 clear() 会保留静态项并继续累计它的计数。创建延迟按 DT_CTRL * (之前连续帧数 + 1) 判断当前帧,等号也允许产生告警;回调在延迟判断前调用,异常直接向调用方传播。

    生产路径并非完全没有重复机会:bigModelFailed 可由失败边沿与活动模型故障段同帧添加,canError 可由雷达错误与车辆 CAN 无效同帧添加;这四项分支由隔离探针确认,但不证明实车同帧共现。同一 EventName 还可以有多个类型,例如 controlsMismatch 同时映射 IMMEDIATE_DISABLE 与 NO_ENTRY,cameraMalfunction 有 PERMANENT/SOFT_DISABLE/NO_ENTRY;哪个 Alert 真正参与显示取决于状态机当前的 current_alert_types。

    事件类型 状态意义 典型例子
    ENABLE / NO_ENTRY 启用请求 / 禁止进入 按键或 PCM 启用;系统初始化、车型能力、故障。
    PRE_ENABLE 进入预使能等待 需要条件继续满足才进入 enabled。
    OVERRIDE_LATERAL / OVERRIDE_LONGITUDINAL 驾驶员覆盖某执行维度 overriding 状态。
    SOFT_DISABLE 给驾驶员接管窗口 摄像头故障、进程失联、通信问题。
    IMMEDIATE_DISABLE / USER_DISABLE 立即退出 / 用户主动退出 安全模式不匹配、CAN 总线丢失、踏板/取消。
    WARNING / PERMANENT 活动状态展示 / 始终候选 转向临时不可用、FCW/LDW、启动与硬件提示。

    状态机的优先级

    StateMachine.update() 每步先递减软退出计时器,并始终加入 PERMANENT 告警类型。非 disabled 状态下,IMMEDIATE_DISABLE 优先于 USER_DISABLE,两者优先于软退出或覆盖。disabled 只有在出现 ENABLE 且没有 NO_ENTRY 时才能进入 preEnabled、overriding 或 enabled。ENABLED_STATES 包含 preEnabled、enabled、softDisabling、overriding;ACTIVE_STATES 不含 preEnabled,所以「enabled」与「执行器正在活动」并非同义词。

    当前状态 条件 结果
    enabled SOFT_DISABLE 进入 softDisabling,计时约 3 秒。
    enabled 横/纵覆盖 进入 overriding,继续 enabled=True, active=True。
    softDisabling 故障消失 回 enabled;若持续至计时结束则转 disabled。
    preEnabled PRE_ENABLE 消失 转 enabled;否则停留预使能。
    overriding 覆盖消失 回 enabled;若出现软退出则转 softDisabling。
    任意非 disabled IMMEDIATE_DISABLE 或 USER_DISABLE 直接变成 disabled。

    SOFT_DISABLE_TIME 是按 DT_CTRL 换算的步数,取决于循环是否按预期执行;最坏墙上时间和驾驶员实际听到提醒的延迟需要实测。源码还有两个易误判的边界:处于 disabled 且同帧有 ENABLE + IMMEDIATE_DISABLE、却没有 NO_ENTRY 时,代码只走 disabled 分支、结果进入 enabled;处于 preEnabled 时若 PRE_ENABLE 消失但 SOFT_DISABLE 仍在,会先进入 enabled 且返回 active=True,下一次调用才进入 softDisabling。

    告警仲裁:一帧只留一条

    AlertEntry.active() / just_added() 用包含端点的 frame <= end_frame 判定有效;只有当前帧紧挨上次加入帧且原条目仍有效才保留旧 start_frame。AlertManager.add_many() 以 alert_type 为键保留告警:每次覆盖该槽的负载和 added_frame,结束帧取 max(frame + 1, start_frame + duration),duration 单位是控制帧而非墙上秒。间隔一帧以上的重加即使旧告警还有效,也会重置 start_frame。

    process_alerts() 先把匹配 clear_event_types 的条目 end_frame 设为 -1,再从仍有效的条目按 (priority, start_frame) 最大值选择。完全同分时使用严格 >,字典中先插入的 alert_type 胜出;同一键的不同负载则由最后一次 add_many() 覆盖,并不产生第二个候选。SelfdriveD.update_alerts() 在不活动时清理 WARNING,已使能时清理 NO_ENTRY。因此同一帧可有多个事件,但只有一个当前显示/声音告警。set_offroad_alert() 另把驻车提示写入 Params,属于另一呈现通道,不参加本次 onroad AlertManager 排序。

    常见问题

    同一帧有多个故障,屏幕上为什么只显示一条告警?

    因为 process_alerts() 只从有效条目里按 (priority, start_frame) 最大值选一条;完全同分时用严格 >,字典中先插入的 alert_type 胜出。事件列表可以有多条,最终呈现的告警只有一条。

    enabled=True 是不是就代表执行器已经动了?

    不是。ENABLED_STATES 含 preEnabled,而 ACTIVE_STATES 不含它;enabled 只表示状态机处于使能族。controlsd 还要结合车辆故障、静止门和纵向覆盖决定 latActive/longActive,pandad 另要求 selfdriveState 消息 alive/valid。

    重复添加的事件会被自动去重吗?

    不会。add() / add_from_msg() 用 bisect.insort 排序但不去重,重复事件会产生重复的 create_alerts() 结果,计数仍按「该事件名是否出现」每帧只加一次。插件接入不应依赖容器自动去重。

    软退出(soft disable)给的 3 秒是按墙钟算的吗?

    不是。SOFT_DISABLE_TIME 是按 DT_CTRL 换算的步数,取决于循环是否按预期执行;最坏墙上时间和驾驶员实际听到提醒的延迟需要实测。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SELFDRIVE_EVENTS_STATE_ALERTS_DEEP_DIVE.md(事件容器、状态机与 AlertManager 的三段仲裁机制与故障边界)。
    • 项目 02 —— 同目录探针记录:五项 Events 容器、五项状态机转移、五项 AlertManager 仲裁隔离探针结论。

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

    更新记录

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