结论:风扇链是一条 命令加故障告警,而不是以 RPM 调速的闭环:hardwared 按温区与点火状态算目标百分比,经 deviceState → pandad → 0xb1 → panda 固件 执行,再由 peripheralState 回报约 2 Hz 转速,selfdrived 只做告警。两个反直觉细节必须记住:点火的 30% 下限同时被用作积分项下界,因此首次点火可能直接把积分项夹到 30,而不是缓慢爬升;selfdrived 的代码注释写「5 seconds」,实际判断是严格超过 15.0 秒,且事件只含 ET.PERMANENT,不等于自动降额、软禁用或关机。
命令链:从复合温度到 8 Hz PWM
下表把整条链的关键环节与职责列出。要注意主机侧目标与固件侧执行是两段判断,deviceState 里的百分比并不等于最终 PWM 占空。
| 环节 | 源码 | 实现职责 |
|---|---|---|
FanController.update() |
fan_controller.py |
输入当前复合温度和点火状态,更新 PID 上下限;点火状态变化时重置积分;返回整数百分比。 |
PIDController.update() |
pid.py |
k_p=0、k_i=0.004、无微分,积分时间步长由传入 rate 固定;输出等于积分项与前馈之和再限幅。 |
hardware_thread() |
hardwared.py |
用内存/CPU/GPU/PMIC 的滤波温度生成目标;过热告警时在 PID 结果之后覆盖为 100%,随后发布 deviceState。 |
process_peripheral_state() |
pandad.cc |
在 deviceState 更新且未设置 NO_FAN_CONTROL 时下发改变的目标;还在满足 frame 模条件时重发同一目标。 |
Panda::set_fan_speed() / get_fan_speed() |
panda.cc |
通过控制请求 0xb1 写目标、0xb2 读 RPM。 |
fan_set_power() / fan_tick() |
fan.h |
非零目标先夹到 20–100%,8 Hz tick 更新 PWM、测速滤波及共享供电门。 |
send_peripheral_state() |
pandad.cc |
读取 panda 状态与转速,约 2 Hz 发布 peripheralState;外层 valid 来自通信健康。 |
selfdrived 风扇检查 |
selfdrived.py、events.py |
已知 panda、RPM 小于 500 且期望大于 50 时,自最后健康处理帧起严格超过 15 秒对应帧数后添加永久 fanMalfunction;本分支不校验反馈新鲜度。 |
主机控制不是 RPM 闭环:30% 下限直接夹积分
风扇控制器在导入模块时按设备类型设定 OFFSET:mici 为 0,其他为 5 °C。因此设定点分别为 75 / 80 °C,前馈从 60 / 65 °C 的 0% 线性增加到 100 / 105 °C 的 100%。FanController 只读温度和点火,不读实际 RPM;点火时输出限在 30–100%,驻车限在 0–30%;点火边沿重置 PID 状态。积分步长固定为 1/rate,hardwared 传 int(1/DT_HW),调度不均匀时仍按名义步长积分。
探针揭示了上下界实现的一个关键组合:PIDController.update() 的限幅不是单纯「最终输出至少 30%」——点火下限 neg_limit=30 同时被用作积分项的下界。mici 在 80 °C 时前馈为 50%、误差为 5 °C;首次点火更新的名义积分增量仅 0.004 × 0.5 × 5 = 0.01,但被直接夹到 i=30,最终输出为 80%。即使把 rate 从 2 改为 10,此样例首步仍为 i=30;只有把 PID 下限单独改成 0 后,才显出 1/rate 的 0.01 / 0.002 增量。不应把它误读为缓慢积分爬升。
另一个容易误判的地方是高温覆盖:hardwared 对正常目标计算之后把 deviceState 里的期望值改为 100%,PID 内部积分并不知道这次覆盖。因此告警解除时目标可能回到此前积分/前馈决定的值,而不是从 100% 受控斜坡下降。此外 panda 固件有非零最小 20% 的夹限,主机驻车期望 1–19% 与实际 PWM 目标并不线性一致,期望 0% 才是真正的停机请求。
| 量 | 取值 / 边界 | 含义 |
|---|---|---|
平台偏移 OFFSET |
mici 0 °C,其他 5 °C | 设定点 75 / 80 °C,前馈 60–100 / 65–105 °C |
| 点火输出限 | 30–100% | 下限同时作为积分项下界 |
| 驻车输出限 | 0–30% | 期望 0% 才是真停机请求 |
| 固件非零最小 | 20% | 主机期望 1–19% 与实际 PWM 不线性一致 |
| 告警阈值 | 期望 > 50 且 RPM < 500 | 配合 15 秒条件触发永久告警 |
故障语义:15 秒永久告警不等于停机
固件 tach 每个 8 Hz tick 根据四个边沿/转计算瞬时 RPM,再以新值 1/4、旧值 3/4 滤波。send_peripheral_state() 即使从 panda 读到 0 RPM,也由通信健康而非风扇健康设置外层 valid;selfdrived 的事件条件检查 pandaType、目标和 RPM,没有直接对 RPM 字段的采样年龄或质量做独立门控。代码注释写「5 seconds」,实际判断是 > 15.0 秒,事件定义只含 ET.PERMANENT——这不等于自动降额、软禁用或关机。整条链是命令加故障告警,不是以 RPM 调速的闭环。
指令新鲜度:frame 模条件不是看门狗
pandad 以 100 Hz 主循环运行、约 20 Hz 调用 process_peripheral_state();只有 sm.updated("deviceState") 为真才进入风扇下发分支。目标变化会立即发送,未变化时的 sm.frame % 100 == 0 只是周期性重发尝试,但仍受新 deviceState 到达及其与 frame 余数的相位约束,不是独立、可靠的五秒看门狗。NO_FAN_CONTROL 环境变量只要存在就跳过主机风扇命令;源码也没有把 deviceState 过期、0xb1 写失败或固件实际占空确认转换为主机可观察的执行状态。
新硬件验收
自制硬件应把 has_fan、PWM 可用、tach 可用、电源门共享关系和转速量程作为能力声明;指令含序号、期望值与有效期,设备返回接受/执行状态。RPM 观测需带采样时间与有效位,堵转或断线要由可信热安全策略处理,不能靠 UI 永久告警替代降额。台架需测 0→20% 门槛、点火边沿、温度源缺失、100% 强制覆盖、主机或 USB 失联、tach 开短路、供电门与 IR 干扰,以及最坏情况下的 SoC 温升和恢复时间。探针不覆盖 panda 指令丢失、RPM 零值、无风扇板型、NO_FAN_CONTROL、状态消息中断、15 秒事件时序或高温覆盖后的恢复。
常见问题
为什么点火后风扇输出会直接从 30% 起跳?
因为点火下限 neg_limit=30 同时被用作积分项的下界。探针显示,mici 在 80 °C 时名义积分增量只有 0.01,但被直接夹到 i=30,最终输出 80%;即使把 rate 从 2 改为 10,首步仍是 i=30。这不是缓慢积分爬升。
高温告警触发后,温度下降会平滑降速吗?
不一定。hardwared 是在正常目标计算之后把 deviceState 期望值覆盖为 100%,PID 内部积分并不知道这次覆盖,因此告警解除时目标可能直接回到此前积分/前馈决定的值,而不是从 100% 做受控斜坡下降。
停机时把期望设成 1% 到 19% 有用吗?
不等于停机。panda 固件对非零目标有最小 20% 的夹限,所以主机驻车期望 1–19% 与实际 PWM 目标并不线性一致;期望 0% 才是真正的停机请求。
出现 fanMalfunction 就代表风扇停了吗?
不代表。触发条件是已知 panda、期望大于 50 且 RPM 小于 500,并自最后健康处理帧起严格超过 15 秒后添加永久事件;事件只含 ET.PERMANENT,不等于自动降额、软禁用或关机。而且 peripheralState 的外层 valid 来自通信健康而非风扇健康。
延伸阅读
- pandad 原生 CAN 编解码与健康契约 —— 承接口令下发与健康位从哪来。
- pandad 硬件测试与 DFU/SPI 回环的安全边界 —— 承接 0xb1/0xb2 之外的外设与硬件操作边界。
- pandad 主机复位与 DFU 恢复循环 —— 解释主机或 USB 失联后链路的恢复语义。
- 车载信号为什么会说谎:TTL、源时间与 500 ms 新鲜度阈值 —— RPM 观测缺采样年龄门控是同类问题。
- 热区与温度带:启动与退出契约 —— 从热管理侧看温度源与阈值的设定。
参考资料
- 项目 02 ——
openpilot-research/FAN_HOST_PANDA_FEEDBACK_CONTRACT_DEEP_DIVE.md(取用了风扇命令链的组件索引、主机 PID 与 30% 下限夹积分、15 秒永久告警边界、指令新鲜度与新硬件验收要求)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复