标签: 电源功耗

  • 设备功耗验收:hwmon 增量不是逐进程瓦特计

    结论:设备功耗测试 test_power_draw.py 测的是同一 hwmon 节点读数的阶段增量,不是逐进程独立瓦特计:每个阶段把当前 get_power(1) 均值与前一阶段比较,prev 每阶段更新。因此它测到的是「依次加负载后的边际电源通道读数」,可能混入相机预热、GPU 频率、风扇、后台进程或上个阶段尚未稳定的影响。

    本文基于固定源码快照的静态分析及五项原测试类方法/假时钟探针;test_power_draw.py 依赖特定硬件、固定 sysfs 功率节点、真实进程和消息总线,本次没有运行设备测试、读取 hwmon、启动相机/模型或修改设备电源状态。底层功率采样的窗口边界属于相邻专题。

    硬件读数与函数索引

    read_power() 读取固定的 hwmon 功率节点,数值除 1e6 转为瓦特;节点缺失或读数不合法直接抛异常,没有探测或热插拔恢复。sample_power() 用 Ratekeeper(123) 读取约 123 × seconds 个样本,未返回每个样本时间戳;get_power() 对指定秒数样本取算术平均,测试基线默认 5 秒,候选窗口按每次 1 秒测。

    函数/数据 静态契约
    read_power() 读固定 hwmon 节点,除 1e6 转瓦;缺失或不合法直接抛异常,无恢复
    sample_power() Ratekeeper(123) 读约 123 倍秒数样本,未返回每样本时间戳
    get_power() 对指定秒数样本取算术平均;基线默认 5 秒,候选窗口每次 1 秒
    Proc 与 PROCS 进程名、期望功率增量、需要消息名及 np.isclose 容差;顺序为 camerad、modeld、dmonitoringmodeld、encoderd
    valid_power_draw() 比较同一 hwmon 节点读数的增量,不是进程独立瓦特计
    get_expected_messages() / valid_msg_count() 各服务标称频率乘 2 秒求和;仅比较总条数
    get_power_with_warmup_for_target() 为当前进程预订其消息并先清队列;最多 30 秒反复测 1 秒功率,保留最近两个样本及消息数
    test_camera_procs() 先测基线再逐个启动进程,当前平均功率作为下一阶段 prev;函数名虽指 camera,实际涵盖模型和编码器

    阶段增量与容差

    每一阶段比较 get_power(1) 的最近两次均值与前一阶段读数,prev 在每阶段完成后更新。测试既不把单个进程功耗隔离,也不减去同一时刻的独立空载对照。绝对容差分别是 camerad 0.4 W、modeld 0.2 W、dmonitoringmodeld 0.35 W、encoderd 0.12 W;相对容差默认 5%,实际 np.isclose 取两者合成范围。

    阶段进程 期望功率增量 绝对容差 消息门
    camerad 1.65 W(三相机消息) 0.4 W 按标称频率合计条数
    modeld 1.5 W(modelV2) 0.2 W 按标称频率合计条数
    dmonitoringmodeld 0.65 W(driverStateV2) 0.35 W 按标称频率合计条数
    encoderd 0.23 W(无消息门) 0.12 W msgs=[],期望数与实收数都为 0,恒通过

    valid_msg_count() 允许约 2% 相对误差或绝对 2 条,不逐主题核对;某一路缺失若被其它路超量抵消,仍可能通过总数门。

    测试真实覆盖与失效路径

    TestPowerDraw 继承 OpenpilotTestCase,标记为硬件测试。其 setup_method() 写入演示 CarParams;继承的 setUp() 会初始化硬件、关闭省电并终止 athena,teardown_method() 和 run() 的 finally 还会做 manager 清理。mock_messages(['deviceMotion']) 在测试期间另起后台发布线程。因此这项测试不是无副作用的算法单元测试。

    失效/边界 行为 后果
    30 秒超时退出 仍返回最后的 now/msg_counts 返回的 warmup_time 不是成功标志
    首样本就越过截止 循环在不足两个样本后退出 返回变量尚未赋值而抛 UnboundLocalError
    encoderd 的 msgs=[] 期望数与实收数都为 0 消息门恒通过,只靠功率增量间接判断
    合计消息门 只比较总条数 单主题缺失可被其它主题超量抵消

    隔离执行原 TestPowerDraw 方法后,假服务各 10 Hz、两秒窗口的 left=0 与 right=40 仍通过 40 条合计消息门;空主题列表也通过。假时钟在第一份功率样本返回后越过 30 秒,使 now/msg_counts 尚未赋值而抛 UnboundLocalError。已有两份样本但功率不达标时,超时返回最近两份均值而非错误;若两份样本达标则提前退出。该探针未执行 test_camera_procs(),不能证明真实服务频率、传感器功率或 manager 清理行为。

    manager_cleanup() 在测量尾部、测试 teardown 及继承的 run() 清理路径多次调用,主要作用是停止所有受管进程;若中途抛异常,测试体末尾的清理不会到达,但外层测试辅助类的 finally 仍尝试清理。它不能回滚硬件电源、温度或持久参数的外部状态。

    面向硬件插件的验收建议

    自研计算板/相机/推理加速器要把功耗基线、传感器/模型负载和安全 MCU 电源域分开测量,记录电压、电流、温度、频率、环境与样本时间戳,并对稳态/启动峰值/异常回退分别设门。

    1. 插件的「功率达标」不能替代图像帧有效性、模型输出数值一致性、消息新鲜度或 CAN 发送安全。
    2. 固定路径 hwmon 工具只适用于已知设备布局,不应作为跨硬件插件通用驱动接口。
    3. 验收应把电源通道、消息条数和进程启动结果分开记录,避免用单一通过条件覆盖多类风险。

    验证范围上,本专题已静态核对测试全部函数、hwmon 工具、消息模拟器和 manager 清理链,但未读取实际电源节点、运行硬件或统计噪声分布;探针只复现 Python 控制流,不表示已在具体设备上测得这些失效。

    常见问题

    这个测试能测出单个进程的独立功耗吗?

    不能。valid_power_draw() 比较的是同一 hwmon 节点读数的增量,不是进程独立瓦特计;每阶段把当前均值与前一阶段读数比较,可能混入相机预热、GPU 频率、风扇、后台进程或上个阶段尚未稳定的影响。

    warmup_time 达标就代表测试成功吗?

    不是。get_power_with_warmup_for_target() 到 30 秒退出后仍返回最后的 now/msg_counts,再由测试函数的最终断言决定失败;返回的 warmup_time 不是成功标志。

    为什么 encoderd 阶段的消息门形同虚设?

    因为该阶段 msgs=[],期望数与实收数都为 0,消息门恒通过;这一阶段只靠功率增量和进程启动无异常间接判断,没有核对编码帧、输出文件或码流有效性。

    消息条数达标就能说明各服务都正常吗?

    不能。门控只比较所列服务的合计条数,某一路缺失若被其它路超量抵消仍可能通过;valid_msg_count() 允许约 2% 相对误差或绝对 2 条,不逐主题核对。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/POWER_DRAW_HW_MONITOR_PROCESS_INCREMENT_TEST_DEEP_DIVE.md(取用了 hwmon 读数索引、阶段增量与容差表、失效路径及五项探针结论)。
    • 项目 02 —— openpilot-research/POWER_SAMPLE_WAIT_WINDOW_CONTRACT_DEEP_DIVE.md(仅用于确认底层功率采样的窗口边界,未取用其未公开内容)。

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

    更新记录

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

  • PowerMonitoring 电量估算与 11.8 V 低压关机契约

    结论:PowerMonitoring 的「电量」是估算值而非 BMS 读数:容量单位是 uWh,代码里的 30,000,000 uWh 只是设定的 30 Wh 上限,不是车辆电池实际容量;Params 缺失或过低时按 3 Wh 起步。估算器的失败路径不向调用方返回失效状态——它在积分前裁剪容量、缺测时不更新低通电压、写盘失败时延后积分时钟,而关机判定表达式不检查这些输入的有效位与年龄。它只写一个 DoShutdown 请求位,本身不切断电源;真正断电由 manager 接力。固定快照基线为 c8fb906。

    输入、容量估算与数据来源

    估算器读 CarBatteryCapacity,初始容量至少 3,000,000 uWh;电压滤波器初值 12,000 mV。电量积分的核心式子是 power_used = power_W * 1,000,000 * elapsed_h:驻车时按硬件功率积分耗电并减少估算容量,点火时按固定的 45 W 增加估算容量,并把本次驻车累计耗电清零。

    功率来源因平台而异。comma 平台从 hwmon1/power1_input 读微瓦并转瓦;read_param_file() 读取失败默认返回 0,PC 基类也返回 0。这意味着「功率缺测」和「真实零功率」在数值上无法区分——这是估算偏差风险的来源之一,而不代表设备上已测到该偏差。

    入口 实际行为
    PowerMonitoring.__init__() 读 CarBatteryCapacity,容量至少 3,000,000 uWh;电压滤波器初值 12,000 mV。
    calculate() 电压可用时做固定增益低通、容量裁剪与约 10 秒保存;点火按 45 W 充电,驻车按硬件功率积分;异常只记录,不返回失效状态。
    _perform_integration() 用单调时间差把瓦换算为 uWh,增加驻车耗电并减少估算容量;负耗电抛错后在内部被捕获。
    should_shutdown() 根据驻车时长、低通电压、容量、点火、线束、Params 开关和启动历史返回布尔判定。
    hardware_thread() peripheralState.pandaType=unknown 时传 voltage=None;把估算值写 deviceState,判定后写 DoShutdown。
    manager.main() 看到 DoShutdown 后清理受管进程,再调用 HARDWARE.shutdown()。

    采样时序与低通滤波的实际行为

    calculate() 在积分之前把容量裁剪至 [0, 30 Wh],所以一次积分之后可能暂时高于 30 Wh 或低于 0;现有上下限测试也按一帧越界的行为断言。发布到 deviceState.carBatteryCapacityUwh 时又用 max(0, ...) 遮掉负值,这不代表内部估算已回到零。

    当 voltage=None,函数清空 last_measurement_time、next_pulsed_measurement_time 和 power_used_uWh,但不更新容量、低通电压或瞬时电压;下一次有效电压只重新设置积分时间,不补算缺测区间。离线探针确认旧的低通电压可在缺测后继续触发 should_shutdown(),因为关机表达式不检查电压年龄。

    电压低通系数固定 0.011,每次有效 calculate() 调用推进一次,代码不按实际 dt 重算系数;探针把两次有效样本间隔拉长到 1000 秒,第二次仍只推进一次固定系数。注释的约 45 秒时间常数依赖近似稳定的调用频率。hardware_thread() 名义上按约 2 Hz 更新,但点火边沿会额外触发,调度延迟也会改变低通的实际物理时间。新平台若更换采样频率或电压测点,11.8 V 阈值和滤波参数必须重新标定。

    关机布尔门与边界

    若 offroad_timestamp is None,函数立即返回 False,即使 ForcePowerDown=True 也不走后续判定。普通关机要求:驻车超过 300 秒、未点火、已连接车端线束、DisablePowerDown=False,并且三个触发条件至少一项为真。

    条件 阈值 / 备注
    驻车时长 超过 300 秒(普通门共同前提)
    触发条件一 驻车超过 30 小时
    触发条件二 低通电压低于 11.8 V 且驻车超过 60 秒
    触发条件三 估算容量不大于零
    强制位 ForcePowerDown 以 OR 加入,可绕过普通门,但仍受最终启动门约束
    最终启动门 started_seen 或系统单调时间已超过 3600 秒

    ForcePowerDown 在这些普通门之后以 OR 方式加入,故可绕过点火、线束、禁用关机、五分钟和三个触发条件;但仍受最末尾的 started_seen or now > 3600 约束。离线探针确认强制位在点火中、无车端线束且禁用普通关机时仍可通过,但无驻车时间或未过最终启动门时不能通过。这里的 now 是系统单调时钟绝对值,不是本进程或本次开机管理器的局部计时;启动后已超过一小时的设备即使 started_seen=False 也通过该门。

    两条失败路径都不提供失败区间的功率历史:put() 抛错时积分时刻都不推进,下次成功时把当下功率应用到从上次积分起的整段间隔;负功率会让 _perform_integration() 记录错误并保留旧积分时刻,后续正功率同样覆盖这段间隔。hardwared 写入 DoShutdown 后由 manager 接力;该 Params 位是动作请求,不是电源已经切断的回执。

    面向新硬件与插件化的接口建议

    新硬件电源插件应返回 {voltage, current_power, sample_time, valid, source},明确瓦数符号、供电位置和校准精度;缺测不能静默映射为 0 W。估算器与关机策略应分离,低压门使用有界年龄的有效电压、实际 dt 和可追踪阈值,强制关机另设授权与审计。

    台架至少测试点火抖动、USB / 线束脱落、功率传感器读失败、低压瞬降、长时间驻车、manager 重启、请求已写但硬件未关机,以及真实 12 V 电气负载下的最坏关机延迟。现有单测覆盖无 peripheral、点火充电、驻车积分、上下限、低压、禁用关机、线束与延迟门;本轮未运行。

    常见问题

    30 Wh 是车辆电池的真实容量吗?

    不是。30,000,000 uWh 是代码里的设定上限,不是车辆电池 BMS 读数;若 Params 缺失或过低,初始化按 3 Wh 起步。容量单位是 uWh,且积分前会裁剪到 [0, 30 Wh],一次积分后可能暂时越界。

    为什么功率读不到时估算不会停?

    因为两种平台行为不同:comma 读 sysfs 失败默认 0 W,有效电压但功率缺测会被当成零耗电;若平台实现直接抛错,calculate() 捕获后不推进积分时刻,恢复后的一个功率读数会被应用到整个失败间隔。两条路径都不提供失败区间的功率历史。

    电压缺测后还能触发低压关机吗?

    可能。当 voltage=None,函数不更新低通电压;离线探针确认旧的低通电压可在缺测后继续参与 should_shutdown(),因为关机表达式不检查电压年龄。反过来,缺功率读数会让容量消耗被低估。

    写了 DoShutdown 就等于断电了吗?

    不等于。DoShutdown 是一个动作请求位,PowerMonitoring 本身不切断电源;hardwared 写入后由 manager 看到并清理受管进程,再调用 HARDWARE.shutdown()。请求已写但硬件未关机属于必须单独台架验证的场景。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/POWER_MONITORING_CAPACITY_SHUTDOWN_CONTRACT_DEEP_DIVE.md(取用了容量估算公式、采样时序、低通滤波行为、关机布尔门与硬件插件建议)。

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

    更新记录

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

  • AGNOS A/B 升级:sparse 解包与 64 字节尾标记

    结论:AGNOS 的 A/B 系统更新是一条「拉取 staging → 标非活动槽不可启动 → 流式解压并解 sparse → 写分区与哈希校验 → abctl 切槽 → 重启后标记成功」的链。校验分两层:system 等非全量校验分区只比较尾部 64 字节 marker,其余分区重算 SHA-256。因此 marker 是性能优化,不是加密签名或全量介质自检。本文为固定源码快照的静态分析,不是可在车辆上照做的刷机手册。

    平台辅助函数:设备路径、GPIO 与功放

    Paths 按 PC/AGNOS 选择日志、持久化、下载缓存和共享内存路径;LOG_ROOT、COMMA_CACHE、OPENPILOT_PREFIX 可影响部分路径。GPIO 统一列出 panda、GNSS、蜂窝、相机、蜂鸣器和 IMU 中断 GPIO 编号;注释明确某些 _RST_N 名称与实际高有效复位相反,不能按名字推电平。功放侧 Amplifier._set_configs() 对 MAX98089 执行 I2C 读改写,初始化时先全局关闭、配置 DAC/I2S/扬声器、再开启;Amplifier.set_configs() 最多 15 次重试并递增等待,仅捕获 OSError,全部失败返回 False。

    省电策略 HardwareComma.set_power_save() 在驻车时关闭功放、大核下线并调整 CPU governor/IRQ,恢复时重新初始化功放;它紧耦合 SoC sysfs 与 IRQ 名称。台架功率工具 read_power()/sample_power() 从固定 I2C hwmon 节点读取瓦特,按 123 Hz 定时采样,这是测量工具,不是 hardwared 中的 PowerMonitoring 驻车电量估计器。

    组件 源码入口 行为与移植边界
    路径选择 Paths 按 PC/AGNOS 选择日志、持久化、下载缓存与共享内存路径。
    引脚表 GPIO 列出各外设 GPIO 编号;_RST_N 名称与实际复位极性可能相反。
    功放配置 Amplifier._set_configs() MAX98089 I2C 读改写;失败返回 False,调用方未检查布尔值。
    省电策略 HardwareComma.set_power_save() 驻车关功放、大核下线并调整 governor/IRQ。
    台架功率工具 read_power()/sample_power() 按 123 Hz 读取 I2C hwmon 瓦特,非驻车电量估计器。

    A/B 更新调用链

    updated.handle_agnos_update() 只在 AGNOS 版本变化时触发,清除 staging 的一致标记后刷目标 slot;updated 由 manager 作为驻车进程运行。launch_chffrplus.sh 则在启动时先验证/切换已下载的目标系统,或循环调用设备更新器。清单在 common/hardware/comma/agnos.json,system/hardware/comma/agnos.json 是指向它的符号链接,故两处路径并非两份独立配置。

    选择槽时 get_target_slot_number() 读取 abctl --boot_slot,选择另一槽;get_partition_path() 按清单 has_ab 拼分区标签。下载解压由 StreamingDecompressor.read() 流式处理 HTTP/XZ,同时计算解压后字节的 SHA-256;unsparsify() 解析 Android sparse 的 raw/fill/don’t-care 三类 chunk。分区写入 flash_partition() 先快检;非全量分区先清尾部 hash 标记,再由 extract_compressed_image() 写设备、核对 raw/hash/大小,成功后回写标记。重试与切槽上,flash_agnos_update() 先 --set_unbootable,仅下载类异常最多重试 10 次;swap() 清 marker 后调用 abctl --set_active。

    阶段 核心函数与契约
    选择槽与路径 get_target_slot_number() 读 abctl --boot_slot 选另一槽;get_partition_path() 按 has_ab 拼标签。
    下载解压 StreamingDecompressor.read() 流式 HTTP/XZ 解压并计算解压后 SHA-256。
    稀疏镜像 unsparsify() 解析 Android sparse 的 raw/fill/don’t-care chunk。
    分区写入 flash_partition() 快检、清尾标记、写设备并核对 raw/hash/大小、回写标记。
    校验策略 verify_partition() 全量校验分区重算 SHA-256;其他分区只比较末尾 64 字节 marker。
    重试与切槽 flash_agnos_update() 先 --set_unbootable,下载类异常最多重试 10 次;swap() 清 marker 后 abctl --set_active。

    清单中的 hash 指解压后的稀疏输入流,hash_raw 指写入的原始分区数据;对于非稀疏分区两者相同。system 是 sparse 且采用尾部 hash 标记,其余本快照清单分区采用全量哈希。

    静态发现的故障边界

    unsparsify() 遇到 don’t-care chunk 只 yield b"",extract_compressed_image() 对空字节不推进输出文件位置;用 1 个 4096 字节 don’t-care 块的合成 sparse 输入验证过该函数只输出 0 字节。若发布镜像实际包含此类 chunk,写入布局/大小不会保留空洞,并可能因大小或哈希校验失败——但当前未下载真实镜像,不能断言发布镜像触发了该路径。

    swap() 在 abctl 返回不含成功标记的输出时无限循环,源码中没有退避或截止时间;若命令直接抛异常则退出。隔离探针还显示:前两次刷写抛 RequestException、第三次成功时,同一分区被调用三次、前两次各睡 10 秒;刷写抛非下载类 ValueError 时第一次异常直接传播,不进入十次重试;abctl --set_unbootable 假返回码为 1 时仍继续调用刷写,因为源码调用 subprocess.run() 后未检查 returncode。

    verify_partition() 对非全量校验分区只信任尾部 marker,无法检测 marker 写成之后发生的实体损坏;隔离探针验证了「载荷已变而尾部仍放预期哈希字符串」时非全量 verify_partition() 返回真、强制全量检查返回假。test_manifest() 只检查 URL 的 HEAD 可达性、内容类型和非稀疏分区的两个 hash 字段相等,不覆盖 sparse 解包、分区写入、切槽或电源中断。

    常见问题

    AGNOS 更新时断电会怎样?

    源码在写入前先 abctl --set_unbootable 标记目标槽不可启动,写入后由 swap() 用 abctl --set_active 切槽,新系统启动后再以 abctl --set_success 标记成功。本文只做静态分析,未做断电注入实板验证;插件化更新服务应把「镜像校验通过」「目标槽可启动」「切槽完成」作为分离状态回执。

    为什么有些分区只比较 64 字节尾部标记?

    因为 verify_partition() 对 full_check 分区重算指定大小的 SHA-256,其他分区只比较末尾 64 字节 marker,并不每次重读分区实体。这个 marker 是性能优化,无法检测写标记之后的实体损坏。

    sparse 镜像里的 don’t-care 块会被保留吗?

    源码里 unsparsify() 遇到 don’t-care chunk 只 yield b"",而 extract_compressed_image() 对空字节不推进输出位置;隔离探针用合成输入验证了它只输出 0 字节,所以空洞不会按原布局保留。

    A/B 切槽失败会重试吗?

    源码中 swap() 在 abctl 输出不含成功标记时无限循环,没有退避或截止时间;隔离探针连续尝试三次、中间无 sleep()。新平台应对切槽失败设置有界重试、告警和恢复策略。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/AGNOS_PLATFORM_UPDATE_DEEP_DIVE.md(平台辅助组件、A/B 安装与校验策略、sparse/XZ 边界)。
    • 项目 02 —— 同目录探针记录:四项更新控制流、五项 sparse/校验、四项 XZ 流隔离探针结论。

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

    更新记录

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