查看“︁Photonicat 2 SOC 计算”︁的源代码
←
Photonicat 2 SOC 计算
跳转到导航
跳转到搜索
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
'''光影猫2''' 不靠"看电压猜电量",而是用一套完整的'''库仑计'''(Coulomb counter,又叫电量计 / fuel gauge)来算电量:像给电池装了一个水表, 进出的每一毫安秒都记账。 本页讲清楚这套系统怎么工作——从硬件怎么测,到固件怎么积分、怎么自学习、 怎么做温度补偿,再到 Linux 内核怎么把它变成 sysfs 文件。 指示灯说明见 [[Photonicat 2 LED 指示灯]],sysfs 路径见 [[Photonicat 2 sysfs]]。 English: [[How Photonicat 2 Calculates SOC]]。 == 为什么不能只看电压 == 这是所有电量计要解决的第一个问题。锂电池的"电压—电量"曲线中段非常平。 下面取固件里的'''放电 OCV 表'''(去 IR 后的整包电压): {| class="wikitable" ! 整包 OCV(开路电压) !! 对应电量 |- | 7.58 V || 70 % |- | 7.50 V || 60 % |- | 7.44 V || 50 % |- | 7.36 V || 40 % |- | 7.28 V || 30 % |} 从 70 % 掉到 30 %,电压只降了 '''0.30 V'''——平均每 1 % 电量只对应约 '''6 mV'''。而电池一带负载,内阻上的压降(I×R)轻松就有几十毫伏。也就是说: '''只看电压,误差可以轻松超过 10 %''',而且负载越大错得越离谱。 反过来,两端却很陡:这张表里 8.34 V 是 100 %、6.34 V 是 0 %。所以正确做法是 '''两端用电压定位,中间靠积分推进'''——这正是下面这套系统在做的事。 == 一句话原理 == 电量 = 起点电量 − ∫(放电电流)dt + ∫(充电电流)dt 积分本身很准(电流测得准的话),但有两个毛病:'''起点从哪来''',以及 '''误差会累积'''。所以真正的电量计 = 积分 + 一套修正机制。光影猫2 的做法是: {| class="wikitable" ! 层 !! 作用 |- | 硬件测量 || TPA626 每 100 ms 测一次电压/电流 |- | 库仑积分 || 每 200 ms 把电流累加进 µAs 账本 |- | OCV 融合 || 静置时用电压表反查电量,轻轻拉回积分漂移 |- | 锚点 || 充满 / 欠压时直接把电量钉死到 100 % / 0 % |- | 自学习 || 满容量、内阻、空点、充电增益,每台机器各学各的 |- | 温度补偿 || 冷天内阻变大、可用容量变少,都要修正 |- | 显示层 || 把内部电量映射成用户看到的 1–100 % |} == 一、硬件:怎么测 == 测量芯片是 '''TPA626'''(I²C 电流/电压监测器,与 INA226 同类)。它串一个 毫欧级采样电阻在电池回路上,测电阻两端压降换算电流,同时测总线电压。 * '''采样周期''':约 100 ms 一次(<code>VD1_Collect</code>) * '''中值滤波''':电压和电流各过一个中值滤波器,滤掉 I²C 偶发抖动和负载尖峰 * '''原始值另存一份''':中值滤波有约 3 s 滞后,会把"充电器插拔的电压台阶" 抹平,而内阻自学习恰恰要看这个瞬间台阶,所以固件同时保留未滤波的原始值 === 电流校准 === 采样电阻和运放都有个体误差,出厂靠精密万用表实测标定,拟合成二次多项式: I_真实(mA) = C2·x² + C1·x + C0 当前系数(2026-07-24 二轮标定,四点残差 ≤3 mA): {| class="wikitable" ! 项 !! 值 !! 说明 |- | C2 || −4.444×10⁻⁵ || 二次项,修大电流段的非线性 |- | C1 || 1.0411 || 一次项(增益) |- | C0 || +20 mA(放电)/ 0 mA(充电) || 零点偏移,分方向 |} 几个工程细节值得一提: * '''先夹再算''':抛物线在标定范围外会"反向弯"(顶点约 +11.7 A),所以先把 输入夹到 ±3 A,域外按端点斜率线性延伸,不让二次项跑飞。 * '''int64 运算''':x² 最大到 9×10⁶,乘上系数会溢出 int32。 * '''55 mA 死区''':小于这个值直接当 0,避免待机时账本被噪声慢慢推歪。 == 二、库仑积分:账本 == 每 '''200 ms''' 调一次 <code>coulomb_counter_update()</code>,把电流乘以时间 累加进一个 '''µAs(微安秒)'''的 64 位账本。用 µAs 而不是 mAh 是为了避免定点 除法的舍入误差在长时间运行后累积。 * 放电减、充电加 * 电流绝对值小于 '''30 mA''' 的部分不计入(死区,防噪声漂移) * 充电方向另有一个自学习的'''增益系数'''(充电效率不是 100 %) == 三、OCV 融合:把漂移拉回来 == 积分误差会累积,所以在'''电池接近静置'''时,用电压反查电量表来轻轻纠正。 触发条件很严格,因为静置判断错了反而会把正确的积分带偏: {| class="wikitable" ! 条件 !! 阈值 !! 为什么 |- | 电流足够小 || \|I\| ≤ 340 mA(约 0.05C) || 电流大时内阻压降的模型误差会放大 |- | 持续足够久 || ≥ 30 s || 让极化电压和表面电荷弛豫掉 |- | 电压变化率 || ≤ 20 mV/s || 还在动态过程中就不算静置 |- | 电流变化率 || ≤ 2000 mA/s || 同上 |- | 插拔宽限期 || 10 s || 充电器刚插拔,电压还没稳 |} 满足后也不是直接覆盖,而是'''加权融合''': * 权重上限 '''α = 0.07'''(OCV 最多只占 7 %),且在 8 s 内从 0 线性爬升 * 单次修正最多 '''0.20 %''' 换句话说,OCV 是"温柔地把积分往回拽",而不是"一把拽过去"——这样用户看到的 电量不会跳变。 === 三张 OCV 表 === 锂电池有'''滞回'''现象:同样的电量,充电时端电压偏高、放电时偏低(约 30 mV/芯,2S 约 ±60 mV)。所以固件带三张表: * '''放电表''':进入放电模式(\|I\| > 900 mA)时用 * '''充电表''':进入充电模式时用 * '''静置表(REST)''':充放两表的平均,小电流(\|I\| ≤ 600 mA)时用 模式切换带'''滞回'''(进 900 mA / 出 600 mA),避免在阈值附近反复横跳。 查表前还要先'''去 IR''':OCV = 端电压 − I×R_pack,用的就是下面自学习到的内阻。 == 四、锚点:两端归位 == 中段靠积分,两端靠锚点直接钉死: * '''满电锚点''':充电器在位、且 vbus ≥ 8 V、且电流已经很小 → 电量 = 100 %。 要求充电器在位是有原因的:'''95 % 的静置 OCV 约 8.16 V''',一块高电量的 电池静置着很容易被误判成满电。劣质或欠压的充电器推不到真满,也不给锚。 * '''欠压锚点''':放到欠压关机点 → 电量 = 0 %,同时触发空点学习。 == 五、自学习:每台机器都不一样 == 这是这套系统最有意思的部分。同一批电池、同一批板子,个体差异也能有 10 % 以上。所以固件会'''为每台机器单独学习'''四个量,学到的值存进 data flash, 掉电不丢。 === 5.1 满容量学习(电池老化) === 标称 6800 mAh 的电池,用两年可能只剩 5500 mAh。如果满容量写死,老化后 SOC 就会偏乐观——显示还有电,实际已经关机了。 学习方法: # 满电锚点触发时开窗口,累计放电清零 # 一路放电到欠压关机点 # 满容量 = 累计放出的电量 + '''700 mAh''' # 中途检测到充电 → 本次窗口作废 那个 '''700 mAh''' 是关键:整机不会把电池放到真正的 0 V,在关机点 (约 6.3–6.45 V)时电池包里还剩约 700 mAh 没放出来,必须补回去。 学到的值还会换算成'''健康度''': 健康度 = 学习容量 / 6800 mAh 健康度采用 EMA 缓慢融入(Apple 式缓变),不会因为一次测量就跳水。 === 5.2 内阻学习(电压台阶法) === 去 IR 计算需要知道内阻。固件用一个很巧的办法:'''充电器插拔的瞬间''', 电流突变 ΔI,电压随之突变 ΔV,那么 R = ΔV / ΔI 条件:电压 > 7.8 V(近满电区,否则 OCV 本身会随电量移动而测不准)、 电流台阶 ≥ 300 mA、电压台阶 ≥ 15 mV。测量结果按 3/10 的权重 EMA 融入, 范围限制在 10–200 mΩ,默认值 50 mΩ。 注意这测到的是'''整包回路电阻''' = 电芯本体 + 线缆/接插件(20 mΩ)+ 保护板 MOSFET(15 mΩ)。去 IR 需要的正是这个整包串联值,不做扣减; 上位机若要显示"电芯本体内阻",才减掉那 35 mΩ 的板级寄生。 === 5.3 显示空点学习 === 实测踩过的坑:'''显示还有 20 % 时整机突然断电'''。原因是每台机器的电流计 增益和实际容量都有偏差,内部 SOC 到 0 的那一刻和硬件真正撑不住的那一刻对不齐。 解法不是去改电流链路(那会引入新的误差),而是在'''显示层'''加一个 每台机器自学习的线性映射: 显示值 = (内部SOC − 空点) × 100 / (100 − 空点) '''空点'''(empty)= 上一次"电池真的没电了"时的内部 SOC。学习事件包括: 低压关机命令、低压下主机意外死机、欠压锚点。 还有'''向下自纠''':如果放电时电压还很健康(> 6.8 V)但内部 SOC 已经低于 空点,说明空点学高了,就让空点跟着降。这样换了新电池或学过头,都能在一个 放电周期内自愈。 === 5.4 充电增益学习 === 充电效率不是 100 %,充进去的电荷和实际存住的电荷有差异。这个增益在满电 锚点时学习,变化 ≥1 % 才落盘。 == 六、温度补偿 == 锂电池怕冷。板载 NTC 每 200 ms 把温度喂给库仑计,做两件事: === 6.1 内阻温度倍率 k(T) === 内阻随温度显著变化,直接影响去 IR 的准确性: {| class="wikitable" ! 温度 !! 内阻倍率 k(T) |- | −20 °C || ×3.00 |- | −10 °C || ×2.30 |- | 0 °C || ×1.80 |- | 10 °C || ×1.35 |- | 25 °C || ×1.00(基准) |- | 40 °C || ×0.85 |} 也就是说 −20 °C 时内阻是常温的'''三倍'''。学习到的内阻归一在 25 °C 域, 用的时候再乘 k(T)。 === 6.2 低温可用容量补偿 === 冷天电池的可用容量会缩水——不是电"没了",而是低温下拿不出来。固件的处理是 '''把显示空点上移''',让显示电量提前走低,用户就不会在 −10 °C 的户外被 "还有 15 %"骗了: {| class="wikitable" ! 温度 !! 空点上移 |- | ≥ 15 °C || 0(不补偿) |- | 0 °C || 约 +4 % |- | −10 °C || 约 +8 % |- | ≤ −20 °C || +12 %(封顶) |} NTC 读数超出 −30…70 °C 视为故障,保持上一次有效温度(初值 25 °C)。 == 七、显示曲线:为什么是"特斯拉式" == 内部算出来的 SOC 不会直接给用户看,中间还有一层映射: * '''4 % → 显示 1 %''' * '''96 % → 显示 100 %''' * 中段线性(斜率 99/92),两端截断 这样做的意思是:'''显示到 0 % 时,电池里其实还留着约 4 % 的余量''' (再加上空点以下的硬件余量),用户有时间保存工作、正常关机。同理,充到 96 % 就显示 100 %,避免用户盯着最后几个百分点焦虑——这些都是消费电子的 常规做法。 另外固件上报的百分比'''下限钳在 1 %''':只要还没关机,就不显示 0 %。 == 八、关机之后:电量怎么不丢 == 这是很多电量计做不好的地方。光影猫2 有三条路径: === 8.1 SOC 快照 === 运行时 SOC 每漂移 ≥5 % 就自动落盘;关机、主机断电、进待机、OTA 复位前 强制落盘并刷新时间戳。 开机恢复策略: * '''≤12 小时''':SOC = 快照 − 2.0 mA × Δt(按待机电流扣减),并'''禁止''' OCV 重估覆盖。这条修的是"短时重启后 10 % 变 1 %"的老问题。 * '''>12 小时''':快照过期,走冷启动 OCV 重估。 === 8.2 深睡放电估计 === 电池深睡时 TPA626 会掉电,采不到电流。固件在进睡前记下 RTC 分钟戳, 醒来后按整机待机约 '''2.0 mA''' 恒流扣减。 === 8.3 换电池识别 === 电池拔掉 → RTC 掉电重置 → 时间倒退。固件据此判定"换过电池",让快照作废, 重新冷启动估计。(这里留了 10 分钟容差,否则升级重启后一条有效快照会被误判 丢掉,SOC 就会出现台阶跳变。) == 九、数据怎么到 Linux == MCU 算完,通过 '''UART''' 把状态帧发给主机(RK3576)。主机侧有两个消费者: === 9.1 内核驱动:photonicat-pm === 这是一个 '''serdev''' 驱动(<code>990-photonicat-pm-add-driver.patch</code>, 内核 6.12),直接挂在串口上解析状态帧,然后注册成标准 Linux 设备: * '''power_supply''' 类 → <code>/sys/class/power_supply/battery</code> * '''RTC''' 类 → <code>/dev/rtc</code> 因为注册的是'''标准子系统''',所以任何现成的 Linux 工具都能直接用, 不需要认识光影猫的私有协议: <syntaxhighlight lang="bash"> # 电量(%),这就是上面整套算法的最终输出 cat /sys/class/power_supply/battery/capacity # 电压 µV / 电流 µA / 功率 µW cat /sys/class/power_supply/battery/voltage_now cat /sys/class/power_supply/battery/current_now cat /sys/class/power_supply/battery/power_now # 能量 µWh:energy_full 是学习到的满容量,对比 energy_full_design 看老化 cat /sys/class/power_supply/battery/energy_now cat /sys/class/power_supply/battery/energy_full cat /sys/class/power_supply/battery/energy_full_design # 充电器在线 cat /sys/class/power_supply/charger/online </syntaxhighlight> '''MCU 就是硬件 RTC。'''板子上没有单独的 RTC 芯片——MCU 一直有电,它自己 就在走时,时间随状态帧一起送上来。驱动把它注册成 <code>rtc_class_ops</code>, 于是标准工具直接可用: <syntaxhighlight lang="bash"> hwclock -r # 读 MCU 里的时间 hwclock -w # 把系统时间写回 MCU </syntaxhighlight> 这也是为什么'''断网断电放几天,开机时间还是准的'''。前面 8.3 讲的 "换电池识别"也是靠它——RTC 掉电重置,时间就会倒退。 === 9.2 用户态守护进程:pcat-manager === 内核驱动只认状态帧里的电源部分。'''其余的 UART 协议由 pcat-manager 处理''': 调制解调器管理、看门狗、开关机策略、按键事件、充电阈值下发、MCU 固件升级等。 分工可以这么理解: {| class="wikitable" ! 组件 !! 负责 |- | MCU 固件 || 测量、积分、学习、补偿——所有算法 |- | photonicat-pm(内核) || 电源/RTC 数据 → 标准 sysfs 接口 |- | pcat-manager(用户态) || 其余协议:调制解调器、看门狗、电源策略、OTA |} 一个具体例子是'''充电上限''':你在网页里设 80 %,pcat-manager 通过 UART 下发给 MCU,MCU 把它存进 data flash 并直接控制充电 MOSFET。固件 '''RA2E1260726005''' 起这个设置在 MCU 侧持久化,MCU 复位或 OTA 之后不会丢; 同时 pcat-manager 也保留一份镜像并在检测到 MCU 重启时重新下发,双保险。 == 十、常见问题 == ;为什么电量偶尔会"跳一下"? :多半是 OCV 融合或锚点在纠正积分漂移。单次修正被限制在 0.20 %,所以正常情况 你不会看到大跳变;如果出现大跳变,通常是快照过期后走了冷启动重估。 ;为什么冬天电量掉得快? :两个原因叠加:低温下电池可用容量本来就少,加上固件主动把显示空点上移 (−10 °C 约 +8 %),让你提前知道该充电了。回暖后会恢复。 ;为什么充到 96 % 就显示 100 %? :显示曲线刻意这么设计的,见第七节。同理显示 0 % 时电池还有余量。 ;新电池要不要"校准"? :不用刻意做。完整放一次电(从满电放到自动关机)会让固件学到真实满容量和 空点;平时正常使用也会慢慢学到。 ;健康度多久更新一次? :需要一次完整的放电窗口(满电锚点 → 欠压),中途充电会作废。所以它更新得慢, 而且是 EMA 缓变的,这是刻意的——避免单次误测就让健康度跳水。
返回
Photonicat 2 SOC 计算
。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
查看
阅读
查看源代码
查看历史
更多
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
特殊页面
工具
链入页面
相关更改
页面信息