标签: panda

  • selfdrived 事件生产者:53 个直接事件与两道提前返回

    结论:SelfdriveD.update_events() 在同一帧内直接引用 53 个 EventName,但它们只是在「生产者」一侧被加入;加入事件之后仍要经过状态门与告警仲裁,才能变成驾驶员听到的告警或执行器退出。两道提前返回(未初始化、CP.passive)会让后续平台健康检查整体跳过,而 cruiseMismatch/noGps 在事件类型映射中为空,不能凭名字推断会触发禁入或退出。以下基于固定源码快照 c8fb906 的静态分析,未运行端到端时序或实车验证。

    每帧入口与两道提前返回

    update_events() 每帧先执行 clear():它保留静态事件并更新计数,随后处理调试、大模型与一次性启动提示。若 initialized=False,函数添加 selfdriveInitializing 后立即返回,此时不运行后续的驾驶员监测、车况、硬件与通信故障检查。也就是说「初始化中」这一种状态会掩盖其他所有平台健康判定。

    初始化本身在 data_sample() 中等待 canValid && sm.all_checks(),或约 6 秒超时;超时只结束等待,不保证订阅数据有效。初始化完成后可以处理书签,但 CP.passive 又会使函数提前返回。carUnrecognized/dashcamMode 是构造时添加的静态事件,startupNoCar/startupNoControl 等则通过 startup_event 变量在首轮间接加入,所以不能从 53 个直接名称的计数推断它们缺席。

    53 个直接事件按来源分组

    下表按各来源首次直接引用的顺序,列出相应的 EventName 与生产条件的中文摘要。同一帧可能追加多个事件,Events 的有序插入并不等于去重。

    来源 直接 EventName(摘要) 条件及边界
    调试与大模型 joystickDebug、bigModelLoading、bigModelFailed、lateralManeuver、longitudinalManeuver、selfdriveInitializing 大模型失败是有条件的软退出(失败/失联/外设消失边沿),非每帧无条件触发;机动实验按接收帧选择,横向优先
    书签与巡航恢复 userBookmark、userBookmarkNotPaired、resumeBlocked 非 PCM 巡航、设定速度 vCruise > 250 且加速/恢复按键出现时阻止恢复
    驾驶员监测 tooDistracted、driverDistracted1~3、driverUnresponsive1~3 非 notCar 才检查;lockout 禁止使能;alertLevel 1/2/3 与 activePolicy 是否 vision 决定两套提示
    CAN 与踏板 pcmEnable、pedalPressed CS.canValid 后才转发 CarEvents;油门上升沿需开启 DisengageOnAccelerator
    设备健康 overheat、outOfSpace、lowMemory、fanMalfunction 热状态达 overheated;非仿真空闲空间 <7% 或内存 >90%;风扇见下文数值门
    标定与车道偏离 calibrationIncomplete、calibrationRecalibrating、calibrationInvalid、ldw 标定状态非 calibrated 时三选一;IsLdwEnabled 且左右偏离任一成立才产生 ldw
    panda 安全 controlsMismatch、relayMalfunction 与 safetyConfigs 逐项比较;额外 panda 必须 silent/noOutput;mismatch_counter >= 200 可直接触发
    系统/总线/通信 processNotRunning、cameraMalfunction、canBusMissing、canError、commIssue 等 进程故障优先并跳过相机检查;总通信问题仅在 all_checks() 失败且大模型不在缓冲期时产生
    定位/参数/传感器 posenetInvalid、locationdTemporaryError、paramsdTemporaryError、sensorDataInvalid 主题曾被收到才看有效位;加速度计或陀螺仪任一主题距最近接收帧超过约 10 秒
    提示与副作用 fcw、steerSaturated、noGps、modeldLagging、personalityChanged FCW 只生产提醒不下发制动;转向饱和看目标/实际横向加速度比值;驾驶风格在按键释放时循环

    车辆侧的更多事件由 CarEvents.update() 转发,它还会产生车门、安全带、挡位、定速、转向和品牌专属事件,并由 CS.canValid 门控;车辆接口上报的 CarState 信号真假、车型安全钩子和线束电气行为不由这一层证明。

    几个容易被名字误导的事件值得单列。excessiveActuation 只在非 notCar 且有校准姿态时调用 ExcessiveActuationCheck.update(),首次触发会写驻车告警并把内存标志置真,之后才持续添加,构造时还会从已有的 Offroad_ExcessiveActuation 恢复该标志。变道提示同理:模型处于预变道且对应方向盲区被占用时阻止提示,进入开始或结束阶段时才发 laneChange——这些是模型状态消费,不代表真实盲区已被验证。

    数值门与告警边界

    这些条件的触发都绑定具体数值门,且多数在「严格超过某段时间对应的帧数」之后才成立。下表汇总关键阈值。

    项目 阈值 / 时间门
    空闲空间 / 内存 非仿真下空闲空间 <7%,或内存使用 >90%
    风扇故障 已知 panda、风扇目标 >50% 且缓存转速 <500 rpm,严格超过 15 秒对应帧数;注释称「5 秒」,数值实现为 15 秒
    未初始化超时 约 6 秒(只结束等待,不保证数据有效)
    panda 配置不匹配 严格超过 10 秒对应帧数;或 safetyRxChecksInvalid / mismatch_counter >= 200 直接触发
    传感器新鲜度 加速度计或陀螺仪任一主题距最近接收帧超过约 10 秒
    转向饱和 横控活动且最近约 2 秒未检测到驾驶员转向,并满足比值与控制器 saturated
    原车巡航一致性 / GPS cruiseMismatch 约 6 秒;noGps 约 2 秒且累计无 GPS 行驶距离 >1500 m(映射为空)
    模型频率 modelV2.frameDropPerc > 1 即添加 modeldLagging

    需要特别记住两处:风扇反馈故障「注释 5 秒、实现 15 秒」,并且不检查反馈新鲜度;cruiseMismatch 与 noGps 在本快照的 EVENTS 映射中为空,而 Events.contains() 只看类型映射,所以它们不会据此生成禁入或退出类型。

    生产条件不等于告警或退出

    update_events() 对某些系统故障有意设置了优先级与缓冲:总通信问题仅在 all_checks() 失败、无其他新增系统禁入/退出故障、且大模型不在加载或就绪后 5 秒缓冲期时才产生;进程故障优先,alive 优先于平均频率。这些都是「生产条件」层面的取舍,不是最终告警时刻。

    系统与通信故障内部也排了优先级:manager 指出应运行而未运行的进程时先报进程故障并跳过相机检查,recv_frame=0 的首次消息不通过该门,缓存故障可以持续触发事件;车况的 canTimeout 优先于 !canValid,且只有 openpilot 纵控才检查雷达错误,雷达 CAN 错误归入 canError。

    因此插件化时,传感器、模型与车型插件必须提供时间戳、数据有效位和失联证据,核心才保留事件归一化、状态机与 panda 安全约束。下一步应逐事件追到上游生产消息及回调告警,配合进程回放、SIL/HIL 和封闭场地验证;本文没有运行测试或控制车辆。

    常见问题

    未初始化时为什么很多故障检查根本不运行?

    因为 clear() 后若 initialized=False,函数会添加 selfdriveInitializing 并立即返回,后续的驾驶员监测、车况、硬件与通信故障检查全部被跳过。

    这 53 个直接事件就是全部事件吗?

    不是。这只是 update_events() 中直接引用的名称;启动事件通过变量间接加入、车辆事件由 CarEvents 转发,全部 99 个名称与类型组合另见静态事件索引。

    风扇故障到底是 5 秒还是 15 秒?

    注释称「5 秒」,但数值实现为 15 秒对应帧数;并且该检查不核对反馈新鲜度,只按「从最后一次健康处理帧起严格超过 15 秒」成立。

    cruiseMismatch 或 noGps 触发后会退出吗?

    不能这样推断。这两个 EventName 在本快照的 EVENTS 映射中为空,而 Events.contains() 只看类型映射,所以不会据此生成告警/退出类型。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SELFDRIVE_EVENT_PRODUCERS_DEEP_DIVE.md(53 个直接事件的生产条件、两道提前返回与数值门)
    • 项目 02 —— openpilot-research/SELFDRIVE_EVENT_CATALOG.md(全部 99 个名称与类型组合的静态事件索引)

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

    更新记录

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

  • 1024 位 RSA 加 SHA-1:panda 的签名、刷写与扇区边界

    结论:panda 的启动真实性由 bootstub 用「长度门 + SHA-1 + 版本门 + 1024 位 RSA 校验」完成,这是一道启动镜像真实性检查,而不是给运行期 USB/SPI 控制请求做逐请求鉴权。更关键的是:主机端「不许擦 sector 7」只是高层约束,设备端 flash_erase_sector() 实际上接受 sector 7;长度门允许接近 16 MiB,而该 H7 链接脚本声明的 flash 只有 1 MiB。以下基于源码快照 c8fb906 的静态阅读与宿主隔离探针,未运行刷写工具、未做实板测试。

    构建制品与签名字节契约

    SConscript 分别构建 bootstub 与 app,app 入口放在 0x08020000,随后以 SETLEN=1 调用 sign.py。签名脚本在原始 app 尾部追加 VERS 与版本 2,把首 4 字节重写为「签名覆盖区长度」,再以 SHA-1 对首 4 字节之后的内容计算摘要,最后附加 128 字节 RSA 签名。普通构建使用 debug 私钥并启用 ALLOW_DEBUG;RELEASE 构建需提供证书/密钥路径。生成的 cert.h 含 debug/release 公钥,但构建期未见核对 CERT 私钥是否与 release.pub 配对。

    环节 字节/常量 说明
    签名长度 RSANUMBYTES=128 公钥模数在构建时须为 1024 位
    填充前缀 00 01 + 105 字节 FF + 00 + 20 字节摘要 PKCS#1 v1.5 风格,但无标准 SHA-1 DigestInfo/ASN.1 编码
    十六进制补齐 rjust(0x100,'0') = 256 字符 写入镜像时转换为 128 字节
    摘要长度 20 字节 验签只接受 128 字节签名与 20 字节摘要

    验签函数在做完公钥运算后,把末尾摘要与预期值异或归零,并逐字节比较整个 128 字节固定模板。因此它只处理这一种签名格式,不应直接当作通用 RSA-SHA1 校验;源码注释里出现的「2048-bit」和 SHA-256 与当前启用的常量及检查不符。

    上电到跳转:bootstub 做哪几道检查

    early_initialization() 先初始化寄存器、浮置早期 GPIO、识别板型;遇到 ROM 模式标志或芯片 ID 不符会走 jump_to_bootloader()。此后 bootstub.main() 先处理 softloader 标志,再读 app 长度、计算 SHA-1、检查 VERS/最小版本,最后用发布公钥验签;仅 ALLOW_DEBUG 构建额外接受调试公钥。失败进入 soft_flasher_start(),成功则调用 app 向量。

    摘要实现是标准 SHA-1:SHA_hash() 依次调用 SHA_init()、SHA_update()、SHA_final(),64 字节分组经 SHA1_Transform() 处理,末尾补 0x80、零填充及 64 位比特长度,输出 20 字节大端摘要。bootstub 只哈希首字长度之后的 len-4 字节,签名本身与长度字都不在哈希区;长度字虽不参与摘要,却控制被读取的范围和签名位置。SHA-1 原实现通过空串、abc、分块 abc、56 字节填充边界与百万个 a 五类宿主标准向量。

    扇区与刷写边界:主机保护不等于设备保护

    存储布局上,sector0 放 bootstub,sector1 至 6 放 app,sector7 放设备序列与制造数据。主机端的 Panda.flash_static() 会用 last_sector < 7 阻止正常客户端擦除 sector 7,但设备端 flash_erase_sector() 只拒绝 sector 0,允许在解锁状态下擦除 sector 1 至 7。也就是说,直接发 EP0/EP2 的主机可以绕过这一层。EP0 控制请求 0xb1 解锁、0xb2 擦除、0xd1 进 ROM/softloader、0xd8 刷新并复位;comms_endpoint2_write() 从 app 起点顺序写 32 位字,源码未见对写指针、镜像长度或 provisioning 区域做边界检查。

    边界 源码事实 需要台架确认的点
    扇区保护 主机禁擦 sector 7;设备端仅拒绝 sector 0 经原始维护接口能否破坏制造数据
    长度门 bootstub 允许镜像长度接近 16 MiB 超出 1 MiB flash 后读/故障表现
    版本门 只要求 vers[1] >= 2 是否另设单调计数器防降级

    关键函数与地址契约

    下表整理了引导路径上与签名、刷写、身份相关的实现及其边界。它们是理解「哪一层保证什么」的最小清单:

    函数/定义 实际作用 边界
    bootstub.main() 用首字长确定签名位置,哈希 app 主体,校验版本与 RSA-SHA1 签名 长度上界接近 16 MiB,超出 1 MiB flash 声明
    RSA_verify() 检查公钥结构、128 字节签名、20 字节摘要及固定填充模板 只处理该格式,不宜泛化为新硬件的完整安全启动方案
    SHA_hash() 对镜像被签区做 SHA-1 分组计算并输出 20 字节摘要 只覆盖调用者传入的 len-4 字节;memcpy 宿主声明有 ABI 警告
    get_key_header() 从固定公钥文件生成固件公钥常量 公钥与 CERT 私钥的配对关系未见构建期检查
    flash_erase_sector() 拒绝 sector 0,已解锁时擦除 sector 1 至 7 sector 7 含制造数据,设备端并未排除

    五项隔离探针把原版 rsa.c、sha.c 编译为临时共享库,按 debug 私钥构造无 DigestInfo 固定填充签名后经 ctypes 调用原版验签:有效签名返回 1,摘要/签名字节被改、标准 SHA-1 DigestInfo 填充、错误签名或摘要长度、错误公钥字数、不支持的指数均返回 0。合成镜像另核对首字长度、末尾 VERS/版本 2 与 128 字节签名,改动被签主体后验签失败。

    常见问题

    bootstub 验签通过,是不是就代表设备运行期安全?

    不是。它校验的是启动镜像的真实性与完整性,并没有给运行期的 USB/SPI 控制请求做逐请求鉴权。EP0 解锁、擦除、EP2 写入等维护命令仍会被接受,因此需要把签名密钥与刷写权隔离到受控维护进程。

    为什么主机端禁止擦 sector 7 还不够?

    因为那是主机库的约束。flash_erase_sector() 只拒绝 sector 0,允许已解锁时擦除 sector 1 至 7,而 sector 7 含设备序列与制造数据。直接发 EP0/EP2 的主机不受「last_sector < 7」限制。

    版本号能防止固件降级吗?

    不能作为单调防回滚证明。启动检查只要求 vers[1] >= 2,源码中未见与设备单调计数器或持久最小版本比较。硬件改版若需防降级,应另行设计并保留救援路径。

    这套 1024 位 RSA 加 SHA-1 能直接搬到新硬件吗?

    不宜照搬。该验签只处理当前这种非标准固定填充格式,源码注释里的「2048-bit」与 SHA-256 也与实际常量不符。新硬件应先定义清晰格式与测试向量,再决定算法与密钥长度。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_BOOTSTUB_SIGNATURE_FLASH_BOUNDARY_DEEP_DIVE.md(bootstub 签名、刷写与制造数据边界,含源码行号索引)
    • 项目 02 —— openpilot-research/probes/test_panda_rsa_signature_contract.py(RSA 与签名脚本五项宿主隔离探针)
    • 项目 02 —— openpilot-research/probes/panda_sha1_vectors_contract.c(SHA-1 标准向量宿主探针)

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

    更新记录

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

  • CarEvents 判定分两级:1 组通用条件加 5 个品牌分支

    结论:CarEvents 是「车况 → 使能/退出事件」的适配层:它只把 CarState、上一帧状态与最近一次控制请求翻译成事件名,本身不发 CAN,也不替代 panda 固件安全钩子。它的判定分两级——先跑一组通用条件,再进入 5 个显式品牌分支;并且只在 CS.canValid 为真时被调用,CAN 无效时走的是另一条独立故障路径。以下为固定源码快照 c8fb906 的静态追踪。

    CarEvents 在链路里的位置

    card.state_update() 经车型 CarInterface.update() 形成 carState;selfdrived.update_events() 只在 CS.canValid 时调用 self.car_events.update(CS, CS_prev, sm['carControl']) 并合并返回的事件。若 CAN 无效,selfdrived 走 canError/canBusMissing 等独立故障路径,因此不能把「CarEvents 忽略无效报文」误解为此时仍可使能。

    CarEvents 的输入有三层时间语义:当前 CS 是车辆观察,上一帧 CS_prev 用于巡航与转向故障等边沿判断,CC 是最近被消费的控制请求,并不代表车端执行器已按该请求动作。update() 对 body/mock 直接返回空容器,其余品牌先过通用条件,再进入本文件显式实现的品牌分支;未被显式实现的品牌并非不支持,而是使用通用条件与车型自身的 CarState/CarParams/接口规则。

    通用条件 create_common_events() 覆盖什么

    create_common_events() 创建一个新的 Events 容器,从车型接口的 DRIVABLE_GEARS、CP.pcmCruise 与品牌判断中决定可行驶挡位与 PCM 使能方式。它覆盖四类信息:乘员与挡位、原车辅助与模式、速度/制动/准备状态、以及用户覆盖与请求。倒挡是一个典型边界——当倒挡不属于车型 DRIVABLE_GEARS 时,源码会同时产生 wrongGear 与 reverseGear 两个事件。

    速度与准备类条件里,vEgo > MAX_CTRL_SPEED 产生超速事件,brakeHoldActive、parkingBrake、accFaulted、carNotReady 各有对应事件;其中制动保持只在 openpilot 纵向控制时才加入。传感器与转向类则把 vehicleSensorsInvalid、invalidLkasSetting、lowSpeedAlert 与 steerFaultTemporary/Permanent 写成事件。

    条件组 代表字段与 EventName 状态机含义
    乘员与挡位 doorOpen、seatbeltUnlatched、wrongGear、reverseGear 门/带/挡位常映射 NO_ENTRY 与软退出;倒挡另有用户退出。
    原车辅助/模式 cruiseState.available、nonAdaptive、espDisabled/espActive、stockFcw/stockAeb/stockLkas 原车巡航/稳定控制状态与原车碰撞/车道功能被映射为提示或禁止使能。
    速度/制动/准备 vEgo > MAX_CTRL_SPEED、brakeHoldActive、parkingBrake、accFaulted、carNotReady 分别产生超速、制动保持、驻车制动、ACC 故障与未就绪事件。
    用户覆盖/请求 steeringPressed、steeringDisengage 上升沿、gasPressed、buttonEnable、cancel 按钮 转向/油门覆盖、用户退出与启用请求分别走不同类型。
    传感器/转向 vehicleSensorsInvalid、invalidLkasSetting、lowSpeedAlert、steerFaultTemporary/Permanent 加入传感器与转向故障事件,暂时故障另有提示强度选择。

    PCM 启用门与边沿语义

    通用 PCM 启用门的条件是 CP.pcmCruise and brand != 'honda'。原车巡航 enabled 出现上升沿且 blockPcmEnable=False 时产生 pcmEnable,未启用时产生 pcmDisable;非 PCM 车型通常由 CarState.buttonEnable 产生 buttonEnable。这些事件分别被映射到 ENABLE 或 USER_DISABLE,最终由下游状态机裁决。

    这里有一个容易误判的细节:Hyundai 的 cancel 按钮只有在非 PCM 巡航时才会被本层转为 buttonCancel,因为源码明确避免把某些车的暂停/恢复复用键误作取消。同理,steeringDisengage 只在上升沿加入,不会在持续按下的每一帧重复产生。

    临时转向故障为什么不是布尔映射

    steerFaultTemporary 不是简单地把某个位映射成事件。steering_unpressed 记录未按方向盘的帧数,no_steer_warning 与 silent_steer_warning 决定究竟发静默的 steerTempUnavailableSilent 还是更强的 steerTempUnavailable。近 1.5 秒内有手动覆盖、静止或仍处静默阶段时偏向静默提醒;永久转向故障另发 steerUnavailable。

    隔离探针给出一个可复现的数量关系:在本快照 DT_CTRL=0.01 下,静默判定对应 150 帧,即 int(1.5/DT_CTRL)。这些计数取决于控制循环节奏与上游 CarState 更新节奏,不能从车型 CAN 原始位直接推出屏幕告警强度,这个 150 也不是所有车型的告警延迟保证。

    5 个品牌补充规则

    品牌分支处理的是通用条件表达不了的低速与边沿特例。Chrysler 用 0.5 m/s 迟滞区间避免速度边界抖动;Honda 在品牌分支内单独处理 PCM 低速与巡航退出;Toyota 的低速判断只在 OP 纵向启用时评估;GM 在「静止且踩刹车并且网络位置为前视摄像头」时跳过低速使能事件;Volkswagen 则在控制请求使能后才额外加入 speedTooLow。

    品牌 源码条件 产生的事件/目的
    Chrysler vEgo < minSteerSpeed + 0.5 置提醒,vEgo > minSteerSpeed + 1.0 清除 belowSteerSpeed;0.5 m/s 区间避免边界抖动。
    Honda PCM 巡航时低于 minEnableSpeed;巡航启用上升沿;巡航丢失且控制请求不在继续制动 belowEngageSpeed、pcmEnable、speedTooLow/cruiseDisabled;保留低速继续制动特例。
    Toyota 仅 OP 纵向;standstill 且未踩刹车、控制请求 resume 或混动标志;低于最低使能速度 resumeRequired、belowEngageSpeed;请求加速度大于 0.3 时 speedTooLow。
    GM 低于 minEnableSpeed,但 standstill && brakePressed && networkLocation=fwdCamera 例外 通常 belowEngageSpeed;前述条件允许驻车踩刹车时不产生该事件。
    Volkswagen 仅 OP 纵向;vEgo < minEnableSpeed + 0.5,且控制使能时 vEgo < minEnableSpeed belowEngageSpeed 和 speedTooLow。

    要强调:以上只是 CarEvents 中的品牌分支,不是这些品牌完整的 CAN/DBC/安全策略。事件容器本身不负责禁用执行器,最终状态由 StateMachine.update() 决定,执行活动由 controlsd 决定,panda 固件仍负责独立发送限制。

    常见问题

    CarEvents 会直接下发 CAN 或让车退出吗?

    不会。它只产生事件名交给下游:gasPressedOverride、steerOverride、buttonCancel、wrongGear 等分别被 events.py 映射到覆盖、用户退出或软退出,然后由状态机、controlsd 与 panda 固件分层决定最终行为。

    为什么倒挡会同时出现两个事件?

    因为当倒挡不属于车型的 DRIVABLE_GEARS 时,代码会同时产生 wrongGear 与 reverseGear。隔离探针确认了这个组合,具体结果还要看车型的 DRIVABLE_GEARS 定义。

    PCM 车和非 PCM 车的启用事件一样吗?

    不一样。通用 PCM 启用门是 CP.pcmCruise and brand != 'honda',PCM 车靠巡航 enabled 上升沿产生 pcmEnable;Honda 在品牌分支单独处理;非 PCM 车通常由 CarState.buttonEnable 产生 buttonEnable。

    1.5 秒的静默提醒延时对每辆车都成立吗?

    不成立。150 帧来自本快照 DT_CTRL=0.01 与 int(1.5/DT_CTRL),计数还取决于控制循环与上游 CarState 的更新节奏,不是所有车型实际告警延迟的保证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAR_EVENTS_GATEWAY_DEEP_DIVE.md(CarEvents 的通用条件、PCM 门、临时转向故障与 5 个品牌分支)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:五项通用条件与六项品牌契约隔离探针结论。

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

    更新记录

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

  • panda 板级固件:三条 CAN 路径与 1 Hz 巡检的真实边界

    结论:panda 板级固件不是一个「CAN 转发器」。主机候选发送、车辆报文接收、板内总线转发是三条独立路径,各自经过不同的安全门与回执机制。启动时安全模式先被置为 SILENT,只有车型模式才驱动拦截继电器;而 1 Hz 的 safety_tick() 只做超时、最低频率与有效性的巡检,不能当成毫秒级断线检测。本文为固定源码归档的静态阅读,未编译固件、未连接实板。

    三条 CAN 路径与各自的安全门

    把三条路径混为一谈,是理解 panda 安全边界时最常见的错误。主机写入的候选帧、车辆发来的报文、以及板内转发,处在不同的钩子和不同的准入条件之下。

    路径 关键调用链 安全语义
    主机候选发送 pandad::can_send_thread() → comms_can_write() → can_send(..., false) → safety_tx_hook() → process_can() 主机先丢弃超过 1 秒的 sendcan;板端每个完整 CAN 包经过 TX 白名单、车型限值和继电器故障门。被拒帧作为 rejected 回执进入 RX 队列,不会进 TX FIFO。
    车辆报文接收 can_rx() → safety_rx_hook() → comms_can_read() → pandad::can_recv() 板端先从 FDCAN FIFO 取帧并附 bus/长度/FD 标志;安全 RX 仅对通过对应检查和白名单的帧调用车型状态钩子。
    板内总线转发 can_rx() → safety_fwd_hook() → can_send(..., true) 转发准许由独立的 safety_fwd_hook() 决定;true 表示跳过主机发送的 TX 钩子。当前代码在 safety_rx_hook() 之前计算转发并入队,因此不能声称接收帧必须先通过 RX 有效性检查才会被转发。

    safety_fwd_hook() 默认只映射 0↔2;继电器故障或模式设置 disable_forwarding 时阻断,并静态屏蔽需由 openpilot 替代发送的原车报文。另有细节:主机 CAN 组包维护跨传输分片的缓存,comms_can_reset() 在连接重建时清空它,避免半包污染下一连接。

    启动顺序也值得单独看:main() 先将安全模式设为 SILENT,再启用收发器、定时中断和 USB/SPI。主机侧 PandaSafety::fetchCarParams() 要求 FirmwareQueryDone、ControlsReady,随后 setSafetyMode() 发送安全模式和参数。这道门是启动协调,不是对任意主机代码或插件的独立授权机制。

    三条路径共享同一套健康上报(get_health_pkt())与继电器状态,但准入条件各不相同,这正是「主机能发」与「板端放行」必须分开验证的原因。

    安全模式与心跳:秒级兜底不是唯一保护

    安全模式不是普通运行时插件可任意替换的配置,切换会影响继电器和 CAN 队列,必须按车型、固件和线束验证。

    函数/状态 当前实现 硬件含义
    set_safety_mode() 调用 set_safety_hooks();未知模式退回 SAFETY_SILENT,连 SILENT 初始化都失败则停止。 SILENT/NOOUTPUT/ELM327 释放拦截继电器;车型模式才驱动继电器再初始化 CAN。
    set_safety_hooks() 注册模式、重置安全状态。MG、PSA、BYD、Subaru Preglobal、Volkswagen MLB/PQ、ALLOUTPUT 等仅在 ALLOW_DEBUG 编译块注册。 有目录和测试不代表 release 固件可选择该安全模式;必须核对实际固件构建与主机 safetyConfigs。
    safety_tx_hook() 车型模式检查地址、bus、长度白名单及车型 TX 约束;继电器故障会否决。 「panda 永远过滤任意主机帧」是错误表述;诊断与调试模式不具备车型执行边界。
    safety_tick() 每秒检查期望 RX 报文的超时、最低频率和有效性,异常时清 controls_allowed。 不能将 1 Hz 巡检误写为毫秒级断线检测。
    tick_handler() 8 Hz 调用风扇、线束、软件看门狗;1 Hz 处理点火、心跳、safety_tick()。点火时主机心跳 5 秒未到、非点火时 2 秒未到,切 SILENT。 心跳退出是秒级兜底,不是新硬件对快速危险动作的唯一保护。
    get_health_pkt() 上报安全模式、参数、心跳、RX/TX 拦截计数、队列溢出、CAN 错误、线束状态、电压/电流/温度。 主机应把健康状态和车型配置对齐,而不是仅判断 USB 在线。

    通信、线束与物理边界

    这些边界决定了「代码静态链路」与「电气默认态」之间的差距。队列里的 XOR 校验只保证主机与固件之间的传输完整性,与车型 CAN 报文自身的 checksum 不是一回事。

    组件 函数/实现 设计观察
    CAN 队列与校验 can_push()、can_pop()、can_set_checksum() RX/TX 使用临界区环形队列;队列满时累计溢出。包内 XOR 是传输完整性检查,不等于车型 CAN 报文 checksum。
    线束方向与继电器 harness_detect_orientation()、set_intercept_relay() SBU 电压判断正反插;方向改变时重新映射 bus、初始化 CAN 和安全模式。实际断电默认态须以原理图和台架证明。
    控制请求 comms_control_handler() USB/SPI 命令能设安全模式、CAN 速率、FD 行为、继电器、心跳和电源;0xc5 继电器控制在代码中并无 ALLOW_DEBUG 条件(注释写 DEBUG)。
    启动完整性 bootstub.main() 验证应用长度、版本、SHA 与 RSA 发布签名;启动签名约束固件映像,不代表运行时 USB 控制命令已获得逐操作鉴权。

    电源与错误恢复另有边界:set_power_save_state() 省电时关部分中断/收发器但保留主总线用于点火检测,源码明确要求安全功能执行期间不进入硬件低功耗模式;FDCAN 的发送前包校验、总线 off、RX FIFO 丢失与 ACK 错误会进入健康计数与控制器恢复路径,CAN FD/BRS 可由收到的帧检测并改变总线配置。现有测试入口包括车型安全单元测试、USB 协议测试,以及 HITL 的线束、CAN 回环与延时、NOOUTPUT 三组;本地归档未连接实板、也未运行这些测试,代码静态链路不证明电气默认态或故障覆盖率。

    自研车端网关的最低验收点

    1. 先复现 SILENT 启动、未知模式回退、车型 RX/TX/转发独立门、报文来源总线与长度、心跳丢失、原车 ECU 冲突检测;插件只可提交受限候选命令,不可获得原始控制端点。
    2. 分别注入心跳超时、软件 tick 间隔故障、FDCAN bus-off、队列满、主机失联、线束正反插与电源断开;记录 MCU 输出、继电器触点与原车总线波形,不能只看 health_t 计数。
    3. 板型 red、tres、cuatro 的 GPIO/收发器/供电差异要逐板核对;PANDA_CAN_CNT=3 与线束方向映射只是当前实现,不应直接推导新 PCB 的引脚或额定参数。
    4. 固件签名、版本、发布/调试模式、主机权限和可升级回滚是共同边界;不能用调试固件测试结果替代发布固件证据。

    常见问题

    panda 的 1 Hz safety_tick() 能替代毫秒级断线检测吗?

    不能。safety_tick() 每秒检查期望 RX 报文的超时、最低频率和有效性,异常时清 controls_allowed,其丢帧容忍窗口偏保守。它是秒级巡检,额外的主机超时、CAN 控制器故障与硬件失效仍需单独验证。

    主机发送的每一帧都会被 panda 无条件过滤吗?

    不是。车型模式检查地址、bus、长度白名单及车型 TX 约束,但 ELM327 和调试固件的 ALLOUTPUT 特例会把白名单视为通过。因此「panda 永远过滤任意主机帧」是错误表述,诊断与调试模式不具备车型执行边界。

    为什么板内转发不经过 safety_tx_hook()?

    因为转发以 can_send(..., true) 调用,true 跳过主机发送的 TX 钩子,其准入由独立的 safety_fwd_hook() 决定(默认只映射 0↔2)。这与 USB/SPI 客户端经 can_send(..., false) 的发送许可不是同一机制。

    固件签名能保证运行时 USB 命令被逐操作鉴权吗?

    不能。启动签名只约束固件映像;运行时控制命令的准入需主机与通信入口另行约束,USB/SPI 控制请求应纳入可信计算基,不应直接向第三方插件暴露。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_BOARD_DEEP_DIVE.md(取用了三条 CAN 路径与安全门、安全模式与心跳、通信/线束/物理边界,以及自研网关验收点)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认车型差异与插件契约的引用位置)。

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

    更新记录

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

  • openpilot 的 100 Hz 控制回路:开源 ADAS 能给车载设备什么

    需要事先说明一件事:本次项目 02 的内部深度研究副本存放在 NAS(\192.168.31.115谭健强openpilot-research),当前因缺少 SMB 凭据而不可达。本文只基于公开可核验来源写成,涉及车型覆盖数量与硬件售价的数字在不同来源间存在口径差异,已标注为待核实。

    结论先行:openpilot 对车载只读设备最大的参考价值,是它把「感知—决策—执行」拆成了通过消息总线解耦的独立进程,并且严格区分了只读信号与控制指令。这两点恰恰是 OBD 仪表最容易做错的地方。

    100 Hz 是怎么排出来的?

    openpilot(comma.ai 开源的 L2 级辅助驾驶系统)按职责拆分进程:camerad 负责摄像头采集,modeld 运行驾驶模型推理,plannerd 负责横向与纵向规划,controlsd 以 100 Hz 频率输出控制指令,locationd 做基于卡尔曼滤波的定位,calibrationd 动态校准相机姿态,pandad 负责 CAN 收发。这些进程通过 cereal 消息框架解耦通信。

    进程 职责 对只读设备的启发
    modeld 运行驾驶神经网络 采集线程不做重计算
    controlsd 100 Hz 输出控制指令 控制回路频率与显示刷新频率分离
    locationd 卡尔曼滤波定位 定位需要独立的质量状态
    calibrationd 动态校准相机姿态 标定参数需记录版本与有效期
    pandad CAN 收发 车辆接口应独立成宿主进程

    这张表的关键不在频率数字,而在「100 Hz 与显示刷新率无关」。我们在车载仪表上反复强调同一件事:显示每秒重绘 30 次,不代表接收到 30 个独立样本;采样频率、传输频率、模型消费频率与显示频率必须分别记录。

    为什么端到端模型不能替代协议定义?

    openpilot 的 supercombo 模型直接从相机图像预测车辆行驶轨迹,这在感知层是有效的。但车辆信号是另一回事——私有 PID 的定义、单位与量纲需要可验证的来源,不能靠模型「让数值看起来合理」来完成标定。

    我们在 LINK 设计说明里把这条写成了硬约束:需要传感器标定或零点补偿时,必须记录标定人、时间、原始单位、有效范围与参数版本,AI 不得自动通过拟合外观来完成标定。同样,AI 只能输出 EdgeSpec 提案,不能把猜测的协议发布为正式信号。

    开源 ADAS 与只读 OBD 仪表的边界在哪?

    两者都通过 CAN 总线读车,但权限完全不同。openpilot 是向车辆发送控制指令的 L2 级辅助驾驶系统,需要驾驶员随时接管。OBD 仪表则是辅助信息显示设备,明确不替代车辆原有法定或安全关键仪表,不直接控制车辆执行器。

    我们的插件化平台把这条边界写进了首期暂缓清单:任意 C/C++ 动态代码、行驶中主动探测未知诊断服务、由模型直接发送 CAN 帧、刷写 ECU、清故障码、执行器测试与安全关键控制,都不在插件权限内。后续如果确有控制需求,应单独立项并重新开展安全与合规评审。

    这也是为什么驾驶员监控(Driver Monitoring)值得单独研究:它是 openpilot 中最常被部署的开源 DMS 实现之一,依赖车内摄像头做注意力与疲劳检测。对我们来说,它的参考价值在于「如何用视觉信号产生一个带质量状态的布尔量」,而不是如何介入车辆。

    车型适配为什么是最难的一环?

    openpilot 的公开资料显示支持的车型数量在 275 至 325 款之间,且随版本持续增加,不同来源口径不一致,准确情况应以 comma.ai 官方兼容列表为准。即便在这个量级上,安装仍需车型专用线束——这恰恰说明车型适配没有捷径。

    我们自己的经验与此一致。像 Honda Civic Type R FK8 的 K-Type ECU 与老车的 Bosch M1.2 走的是完全不同的协议路径;在 珠海国际赛车场 这类高频开放场地积累的多车型样本,比任何一份通用兼容清单都更有价值。采集侧我们使用 SR-RaceBox 车载数据采集器 以 IMU 50 Hz、GPS 20 Hz 同步记录,先把数据攒够再谈协议覆盖。

    FAQ

    openpilot 是开源的吗?

    是。它以 MIT 许可开源,由 comma.ai 维护,定位为 L2 级辅助驾驶系统。

    控制回路真的是 100 Hz 吗?

    公开技术资料的口径是 controlsd 以 100 Hz 输出控制指令。该数字未由本项目实机验证,属公开来源转述。

    OBD 仪表能像 openpilot 一样控制车辆吗?

    不能,也不应该。OBD 仪表是辅助信息显示设备,不控制执行器、不刷写 ECU、不清故障码;首期暂缓清单已明确排除这些能力。

    为什么本文的车型数量是一个区间?

    因为公开来源分别给出 325+ 与 275+ 两种口径,且随版本变化。准确情况应以 comma.ai 官方兼容列表为准。

    参考资料

    • 公开来源 —— comma.ai openpilot 仓库 https://github.com/commaai/openpilot(2026-10-11 采集)
    • 公开来源 —— openpilot 架构与硬件参数综述 https://bikmantech.com/blogs/blogs/openpilot-everything-you-need-to-know(2026-10-11 采集)
    • 公开来源 —— openpilot daemon 架构与部署实践 https://blog.csdn.net/wenxin77wx/article/details/162380437(2026-10-11 采集)
    • 内部对照 —— 项目 05 OBD_AI插件化平台方案书.md(V1.0,首期暂缓范围)
    • 说明 —— 项目 02 内部深度研究副本位于 NAS,本次因缺少凭据不可达

    更新记录

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