Photonicat 2 SOC 计算:修订间差异

来自Photonicat Wiki
跳转到导航 跳转到搜索
C2h2留言 | 贡献
Create: how the coulomb counter computes SOC (hardware, integration, OCV fusion, self-learning, temperature, Linux sysfs/RTC chain)
 
C2h2留言 | 贡献
Tighten prose: state how each stage works rather than motivating it; note k(T) is a piecewise-linear NCA approximation clamped outside -20..40C; link the MCU protocol page
 
(未显示同一用户的3个中间版本)
第7行: 第7行:


指示灯说明见 [[Photonicat 2 LED 指示灯]],sysfs 路径见 [[Photonicat 2 sysfs]]。
指示灯说明见 [[Photonicat 2 LED 指示灯]],sysfs 路径见 [[Photonicat 2 sysfs]]。
English: [[How Photonicat 2 Calculates SOC]]。
English: [[Photonicat 2 SOC Calculation]]。


== 为什么不能只看电压 ==
== 电压与电量的关系 ==


这是所有电量计要解决的第一个问题。锂电池的"电压—电量"曲线中段非常平。
锂电池的"电压—电量"曲线中段非常平。下面取固件里的'''放电 OCV 表'''
下面取固件里的'''放电 OCV 表'''(去 IR 后的整包电压):
(去 IR 后的整包电压):


{| class="wikitable"
{| class="wikitable"
第28行: 第28行:
|}
|}


从 70 % 掉到 30 %,电压只降了 '''0.30 V'''——平均每 1 % 电量只对应约
从 70 % 掉到 30 %,电压只降了 '''0.30 V''',平均每 1 % 电量约 '''6 mV'''
'''6 mV'''。而电池一带负载,内阻上的压降(I×R)轻松就有几十毫伏。也就是说:
而带负载时内阻压降(I×R)可达几十毫伏,误差会超过 10 %,负载越大偏差越大。
'''只看电压,误差可以轻松超过 10 %''',而且负载越大错得越离谱。


反过来,两端却很陡:这张表里 8.34 V 是 100 %、6.34 V 是 0 %。所以正确做法是
两端则很陡:这张表里 8.34 V 是 100 %、6.34 V 是 0 %。因此固件'''两端用电压定位,
'''两端用电压定位,中间靠积分推进'''——这正是下面这套系统在做的事。
中间靠电流积分推进'''


== 一句话原理 ==
== 总体结构 ==


   电量 = 起点电量 − ∫(放电电流)dt + ∫(充电电流)dt
   电量 = 起点电量 − ∫(放电电流)dt + ∫(充电电流)dt


积分本身很准(电流测得准的话),但有两个毛病:'''起点从哪来''',以及
积分需要一个起点,且误差会随时间累积,因此还要配合一套修正机制。
'''误差会累积'''。所以真正的电量计 = 积分 + 一套修正机制。光影猫2 的做法是:
光影猫2 分为以下几层:


{| class="wikitable"
{| class="wikitable"
第60行: 第59行:
|}
|}


== 一、硬件:怎么测 ==
== 一、硬件测量 ==


测量芯片是 '''TPA626'''(I²C 电流/电压监测器,与 INA226 同类)。它串一个
测量芯片是 '''TPA626'''(I²C 电流/电压监测器,与 INA226 同类)。它串一个
第67行: 第66行:
* '''采样周期''':约 100 ms 一次(<code>VD1_Collect</code>)
* '''采样周期''':约 100 ms 一次(<code>VD1_Collect</code>)
* '''中值滤波''':电压和电流各过一个中值滤波器,滤掉 I²C 偶发抖动和负载尖峰
* '''中值滤波''':电压和电流各过一个中值滤波器,滤掉 I²C 偶发抖动和负载尖峰
* '''原始值另存一份''':中值滤波有约 3 s 滞后,会把"充电器插拔的电压台阶"
* '''原始值另存一份''':中值滤波有约 3 s 滞后,会抹平充电器插拔的电压台阶;
   抹平,而内阻自学习恰恰要看这个瞬间台阶,所以固件同时保留未滤波的原始值
   内阻自学习需要该瞬间台阶,故固件同时保留未滤波的原始值


=== 电流校准 ===
=== 电流校准 ===
第88行: 第87行:
|}
|}


几个工程细节值得一提:
实现细节:


* '''先夹再算''':抛物线在标定范围外会"反向弯"(顶点约 +11.7 A),所以先把
* '''先夹再算''':抛物线在标定范围外会反向弯(顶点约 +11.7 A),故先把输入夹到
   输入夹到 ±3 A,域外按端点斜率线性延伸,不让二次项跑飞。
   ±3 A,域外按端点斜率线性延伸。
* '''int64 运算''':x² 最大到 9×10⁶,乘上系数会溢出 int32。
* '''int64 运算''':x² 最大到 9×10⁶,乘系数后会溢出 int32。
* '''55 mA 死区''':小于这个值直接当 0,避免待机时账本被噪声慢慢推歪。
* '''55 mA 死区''':小于该值按 0 处理,避免待机时噪声累积到账本。


== 二、库仑积分:账本 ==
== 二、库仑积分 ==


每 '''200 ms''' 调一次 <code>coulomb_counter_update()</code>,把电流乘以时间
每 '''200 ms''' 调一次 <code>coulomb_counter_update()</code>,把电流乘以时间
累加进一个 '''µAs(微安秒)'''的 64 位账本。用 µAs 而不是 mAh 是为了避免定点
累加进一个 '''µAs(微安秒)'''的 64 位累加器。使用 µAs 而非 mAh,可避免定点
除法的舍入误差在长时间运行后累积。
除法的舍入误差长期累积。


* 放电减、充电加
* 放电减、充电加
第105行: 第104行:
* 充电方向另有一个自学习的'''增益系数'''(充电效率不是 100 %)
* 充电方向另有一个自学习的'''增益系数'''(充电效率不是 100 %)


== 三、OCV 融合:把漂移拉回来 ==
== 三、OCV 融合 ==


积分误差会累积,所以在'''电池接近静置'''时,用电压反查电量表来轻轻纠正。
'''电池接近静置'''时,用电压反查电量表对积分做小幅修正。触发条件如下,
 
全部满足才执行:
触发条件很严格,因为静置判断错了反而会把正确的积分带偏:


{| class="wikitable"
{| class="wikitable"
! 条件 !! 阈值 !! 为什么
! 条件 !! 阈值 !! 说明
|-
|-
| 电流足够小 || \|I\| ≤ 340 mA(约 0.05C) || 电流大时内阻压降的模型误差会放大
| 电流足够小 || &#124;I&#124; ≤ 340 mA(约 0.05C) || 电流大时内阻压降的模型误差会放大
|-
|-
| 持续足够久 || ≥ 30 s || 让极化电压和表面电荷弛豫掉
| 持续足够久 || ≥ 30 s || 让极化电压和表面电荷弛豫掉
第125行: 第123行:
|}
|}


满足后也不是直接覆盖,而是'''加权融合'''
修正采用'''加权融合'''而非直接覆盖:


* 权重上限 '''α = 0.07'''(OCV 最多只占 7 %),且在 8 s 内从 0 线性爬升
* 权重上限 '''α = 0.07'''(OCV 最多只占 7 %),且在 8 s 内从 0 线性爬升
* 单次修正最多 '''0.20 %'''
* 单次修正最多 '''0.20 %'''


换句话说,OCV 是"温柔地把积分往回拽",而不是"一把拽过去"——这样用户看到的
因此 OCV 只对积分做渐进修正,显示电量不会跳变。
电量不会跳变。


=== 三张 OCV 表 ===
=== 三张 OCV 表 ===


锂电池有'''滞回'''现象:同样的电量,充电时端电压偏高、放电时偏低(约
锂电池存在'''滞回''':同一电量下充电时端电压偏高、放电时偏低(约 30 mV/芯,
30 mV/芯,2S 约 ±60 mV)。所以固件带三张表:
2S 约 ±60 mV)。固件为此设三张表:


* '''放电表''':进入放电模式(\|I\| > 900 mA)时用
* '''放电表''':进入放电模式(&#124;I&#124; > 900 mA)时用
* '''充电表''':进入充电模式时用
* '''充电表''':进入充电模式时用
* '''静置表(REST)''':充放两表的平均,小电流(\|I\| ≤ 600 mA)时用
* '''静置表(REST)''':充放两表的平均,小电流(&#124;I&#124; ≤ 600 mA)时用


模式切换带'''滞回'''(进 900 mA / 出 600 mA),避免在阈值附近反复横跳。
模式切换带'''滞回'''(进 900 mA / 出 600 mA),避免在阈值附近反复切换。


查表前还要先'''去 IR''':OCV = 端电压 − I×R_pack,用的就是下面自学习到的内阻。
查表前先'''去 IR''':OCV = 端电压 − I×R_pack,其中内阻取自下面的自学习值。


== 四、锚点:两端归位 ==
== 四、锚点 ==


中段靠积分,两端靠锚点直接钉死:
中段靠积分,两端由锚点直接置位:


* '''满电锚点''':充电器在位、且 vbus ≥ 8 V、且电流已经很小 → 电量 = 100 %。
* '''满电锚点''':充电器在位、vbus ≥ 8 V 且电流已很小 → 电量 = 100 %。
   要求充电器在位是有原因的:'''95 % 的静置 OCV 约 8.16 V''',一块高电量的
   必须要求充电器在位:'''95 % 的静置 OCV 约 8.16 V''',高电量静置电池易被误判为满电;
   电池静置着很容易被误判成满电。劣质或欠压的充电器推不到真满,也不给锚。
   劣质或欠压充电器无法充到真满,同样不给锚。
* '''欠压锚点''':放到欠压关机点 → 电量 = 0 %,同时触发空点学习。
* '''欠压锚点''':放到欠压关机点 → 电量 = 0 %,同时触发空点学习。


== 五、自学习:每台机器都不一样 ==
== 五、自学习 ==


这是这套系统最有意思的部分。同一批电池、同一批板子,个体差异也能有
同一批电池和板子的个体差异可超过 10 %,因此固件'''为每台机器单独学习'''
10 % 以上。所以固件会'''为每台机器单独学习'''四个量,学到的值存进 data flash,
四个量,结果存入 data flash,掉电不丢。
掉电不丢。


=== 5.1 满容量学习(电池老化) ===
=== 5.1 满容量学习(电池老化) ===


标称 6800 mAh 的电池,用两年可能只剩 5500 mAh。如果满容量写死,老化后
标称 6800 mAh 的电池用两年可能只剩 5500 mAh。满容量若写死,老化后 SOC 会偏乐观。
SOC 就会偏乐观——显示还有电,实际已经关机了。
学习流程:
 
学习方法:


# 满电锚点触发时开窗口,累计放电清零
# 满电锚点触发时开窗口,累计放电清零
第173行: 第167行:
# 中途检测到充电 → 本次窗口作废
# 中途检测到充电 → 本次窗口作废


那个 '''700 mAh''' 是关键:整机不会把电池放到真正的 0 V,在关机点
其中 '''700 mAh''' 是关机点余量:整机不会把电池放到 0 V,在关机点
(约 6.3–6.45 V)时电池包里还剩约 700 mAh 没放出来,必须补回去。
(约 6.3–6.45 V)包内仍剩约 700 mAh,需补回。


学到的值还会换算成'''健康度''':
学到的值换算成'''健康度''':


   健康度 = 学习容量 / 6800 mAh
   健康度 = 学习容量 / 6800 mAh


健康度采用 EMA 缓慢融入(Apple 式缓变),不会因为一次测量就跳水。
健康度按 EMA 缓慢融入,单次测量不会使其大幅变化。


=== 5.2 内阻学习(电压台阶法) ===
=== 5.2 内阻学习(电压台阶法) ===


去 IR 计算需要知道内阻。固件用一个很巧的办法:'''充电器插拔的瞬间'''
内阻由'''充电器插拔瞬间'''的电压台阶测得:电流突变 ΔI,电压随之突变 ΔV,
电流突变 ΔI,电压随之突变 ΔV,那么


   R = ΔV / ΔI
   R = ΔV / ΔI
第193行: 第186行:
范围限制在 10–200 mΩ,默认值 50 mΩ。
范围限制在 10–200 mΩ,默认值 50 mΩ。


注意这测到的是'''整包回路电阻''' = 电芯本体 + 线缆/接插件(20 mΩ)+
该值是'''整包回路电阻''' = 电芯本体 + 线缆/接插件(20 mΩ)+ 保护板 MOSFET(15 mΩ)。
保护板 MOSFET(15 mΩ)。去 IR 需要的正是这个整包串联值,不做扣减;
IR 使用整包串联值,不做扣减;上位机显示"电芯本体内阻"时才减去这 35 mΩ 板级寄生。
上位机若要显示"电芯本体内阻",才减掉那 35 mΩ 的板级寄生。


=== 5.3 显示空点学习 ===
=== 5.3 显示空点学习 ===


实测踩过的坑:'''显示还有 20 % 时整机突然断电'''。原因是每台机器的电流计
每台机器的电流计增益与实际容量存在偏差,内部 SOC 归零的时刻与硬件实际断电的
增益和实际容量都有偏差,内部 SOC 到 0 的那一刻和硬件真正撑不住的那一刻对不齐。
时刻可能不一致(表现为显示尚有 20 % 时断电)。固件不修改电流链路,而是在'''显示层'''
 
套一个每台机器自学习的线性映射:
解法不是去改电流链路(那会引入新的误差),而是在'''显示层'''加一个
每台机器自学习的线性映射:


   显示值 = (内部SOC − 空点) × 100 / (100 − 空点)
   显示值 = (内部SOC − 空点) × 100 / (100 − 空点)
第210行: 第200行:
低压关机命令、低压下主机意外死机、欠压锚点。
低压关机命令、低压下主机意外死机、欠压锚点。


还有'''向下自纠''':如果放电时电压还很健康(> 6.8 V)但内部 SOC 已经低于
并带'''向下自纠''':放电时电压仍健康(> 6.8 V)而内部 SOC 已低于空点,
空点,说明空点学高了,就让空点跟着降。这样换了新电池或学过头,都能在一个
说明空点偏高,空点随之下调。换电池或学习过头可在一个放电周期内恢复。
放电周期内自愈。


=== 5.4 充电增益学习 ===
=== 5.4 充电增益学习 ===


充电效率不是 100 %,充进去的电荷和实际存住的电荷有差异。这个增益在满电
充电效率不足 100 %,充入电荷与实际存入电荷存在差异。该增益在满电锚点学习,
锚点时学习,变化 ≥1 % 才落盘。
变化 ≥1 % 时落盘。


== 六、温度补偿 ==
== 六、温度补偿 ==


锂电池怕冷。板载 NTC 每 200 ms 把温度喂给库仑计,做两件事:
板载 NTC 每 200 ms 把温度送入库仑计,用于两项修正:


=== 6.1 内阻温度倍率 k(T) ===
=== 6.1 内阻温度倍率 k(T) ===


内阻随温度显著变化,直接影响去 IR 的准确性:
内阻随温度变化,直接影响去 IR 的准确性。倍率取 NCA 典型温度特性的分段线性近似:


{| class="wikitable"
{| class="wikitable"
第243行: 第232行:
|}
|}


也就是说 −20 °C 时内阻是常温的'''三倍'''。学习到的内阻归一在 25 °C 域,
−20 °C 时内阻约为常温的'''三倍'''。学习到的内阻归一到 25 °C,使用时再乘 k(T)。
用的时候再乘 k(T)。
超出 −20…40 °C 的部分按端点截断。


=== 6.2 低温可用容量补偿 ===
=== 6.2 低温可用容量补偿 ===


冷天电池的可用容量会缩水——不是电"没了",而是低温下拿不出来。固件的处理是
低温下电池可用容量下降(电量并未消失,只是低温下无法取出)。固件据此
'''把显示空点上移''',让显示电量提前走低,用户就不会在 −10 °C 的户外被
'''把显示空点上移''',使显示电量提前走低:
"还有 15 %"骗了:


{| class="wikitable"
{| class="wikitable"
第266行: 第254行:
NTC 读数超出 −30…70 °C 视为故障,保持上一次有效温度(初值 25 °C)。
NTC 读数超出 −30…70 °C 视为故障,保持上一次有效温度(初值 25 °C)。


== 七、显示曲线:为什么是"特斯拉式" ==
== 七、显示曲线 ==


内部算出来的 SOC 不会直接给用户看,中间还有一层映射:
内部 SOC 不直接显示,中间还有一层映射:


* '''4 % → 显示 1 %'''
* '''4 % → 显示 1 %'''
第274行: 第262行:
* 中段线性(斜率 99/92),两端截断
* 中段线性(斜率 99/92),两端截断


这样做的意思是:'''显示到 0 % 时,电池里其实还留着约 4 % 的余量'''
因此'''显示 0 % 时电池仍留有约 4 % 余量'''(另加空点以下的硬件余量),
(再加上空点以下的硬件余量),用户有时间保存工作、正常关机。同理,充到
可供保存工作并正常关机;充到 96 % 即显示 100 %。这是消费电子的常规做法。
96 % 就显示 100 %,避免用户盯着最后几个百分点焦虑——这些都是消费电子的
常规做法。


另外固件上报的百分比'''下限钳在 1 %''':只要还没关机,就不显示 0 %。
固件上报的百分比'''下限钳在 1 %''':未关机时不显示 0 %。


== 八、关机之后:电量怎么不丢 ==
== 八、关机后的电量保持 ==


这是很多电量计做不好的地方。光影猫2 有三条路径:
共三条路径:


=== 8.1 SOC 快照 ===
=== 8.1 SOC 快照 ===
第293行: 第279行:


* '''≤12 小时''':SOC = 快照 − 2.0 mA × Δt(按待机电流扣减),并'''禁止'''
* '''≤12 小时''':SOC = 快照 − 2.0 mA × Δt(按待机电流扣减),并'''禁止'''
   OCV 重估覆盖。这条修的是"短时重启后 10 % 变 1 %"的老问题。
   OCV 重估覆盖。
* '''>12 小时''':快照过期,走冷启动 OCV 重估。
* '''>12 小时''':快照过期,走冷启动 OCV 重估。


=== 8.2 深睡放电估计 ===
=== 8.2 深睡放电估计 ===


电池深睡时 TPA626 会掉电,采不到电流。固件在进睡前记下 RTC 分钟戳,
电池深睡时 TPA626 掉电,无法采集电流。固件进睡前记录 RTC 分钟戳,
醒来后按整机待机约 '''2.0 mA''' 恒流扣减。
唤醒后按整机待机约 '''2.0 mA''' 恒流扣减。


=== 8.3 换电池识别 ===
=== 8.3 换电池识别 ===


电池拔掉 → RTC 掉电重置 → 时间倒退。固件据此判定"换过电池",让快照作废,
电池拔出后 RTC 掉电重置,时间倒退。固件据此判定为换电池,快照作废并重新冷启动估计。
重新冷启动估计。(这里留了 10 分钟容差,否则升级重启后一条有效快照会被误判
判定留有 10 分钟容差,避免升级重启时误判丢弃有效快照。
丢掉,SOC 就会出现台阶跳变。)


== 九、数据怎么到 Linux ==
== 九、数据如何到达 Linux ==


MCU 算完,通过 '''UART''' 把状态帧发给主机(RK3576)。主机侧有两个消费者:
MCU 算完,通过 '''UART''' 把状态帧发给主机(RK3576)。主机侧有两个消费者:
第319行: 第304行:
* '''RTC''' 类 → <code>/dev/rtc</code>
* '''RTC''' 类 → <code>/dev/rtc</code>


因为注册的是'''标准子系统''',所以任何现成的 Linux 工具都能直接用,
由于注册为'''标准子系统''',现成 Linux 工具可直接使用,无需了解私有协议:
不需要认识光影猫的私有协议:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
第340行: 第324行:
</syntaxhighlight>
</syntaxhighlight>


'''MCU 就是硬件 RTC。'''板子上没有单独的 RTC 芯片——MCU 一直有电,它自己
'''MCU 即硬件 RTC。'''板上没有独立 RTC 芯片,MCU 常供电并自行走时,
就在走时,时间随状态帧一起送上来。驱动把它注册成 <code>rtc_class_ops</code>
时间随状态帧上送。驱动将其注册为 <code>rtc_class_ops</code>,标准工具可直接使用:
于是标准工具直接可用:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
第349行: 第332行:
</syntaxhighlight>
</syntaxhighlight>


这也是为什么'''断网断电放几天,开机时间还是准的'''。前面 8.3 讲的
因此'''断网断电数日后开机时间仍准确'''。8.3 的换电池识别也依赖它:
"换电池识别"也是靠它——RTC 掉电重置,时间就会倒退。
RTC 掉电重置后时间会倒退。


=== 9.2 用户态守护进程:pcat-manager ===
=== 9.2 用户态守护进程:pcat-manager ===


内核驱动只认状态帧里的电源部分。'''其余的 UART 协议由 pcat-manager 处理''':
内核驱动只处理状态帧中的电源部分。'''其余 UART 协议由 pcat-manager 处理''':
调制解调器管理、看门狗、开关机策略、按键事件、充电阈值下发、MCU 固件升级等。
调制解调器管理、看门狗、开关机策略、按键事件、充电阈值下发、MCU 固件升级等。
完整协议见 [[Photonicat 2 MCU 通讯协议]]。


分工可以这么理解:
分工如下:


{| class="wikitable"
{| class="wikitable"
第369行: 第353行:
|}
|}


一个具体例子是'''充电上限''':你在网页里设 80 %,pcat-manager 通过 UART 下发给
'''充电上限'''为例:网页设置 80 % 后,pcat-manager 通过 UART 下发给 MCU,
MCU,MCU 把它存进 data flash 并直接控制充电 MOSFET。固件
MCU 存入 data flash 并直接控制充电 MOSFET。自固件 '''RA2E1260726005''' 起该设置在
'''RA2E1260726005''' 起这个设置在 MCU 侧持久化,MCU 复位或 OTA 之后不会丢;
MCU 侧持久化,复位或 OTA 后不丢失;pcat-manager 同时保留镜像,检测到 MCU 重启时重新下发。
同时 pcat-manager 也保留一份镜像并在检测到 MCU 重启时重新下发,双保险。


== 十、常见问题 ==
== 十、常见问题 ==


;为什么电量偶尔会"跳一下"?
;电量偶尔小幅跳动
:多半是 OCV 融合或锚点在纠正积分漂移。单次修正被限制在 0.20 %,所以正常情况
:OCV 融合或锚点正在修正积分漂移。单次修正上限 0.20 %,正常情况下不会出现大跳变;
  你不会看到大跳变;如果出现大跳变,通常是快照过期后走了冷启动重估。
  若出现大跳变,通常是快照过期后走了冷启动重估。


;为什么冬天电量掉得快?
;冬天电量下降更快
:两个原因叠加:低温下电池可用容量本来就少,加上固件主动把显示空点上移
:低温下可用容量下降,同时固件把显示空点上移(−10 °C 约 +8 %)。回暖后恢复。
(−10 °C 约 +8 %),让你提前知道该充电了。回暖后会恢复。


;为什么充到 96 % 就显示 100 %
;充到 96 % 即显示 100 %
:显示曲线刻意这么设计的,见第七节。同理显示 0 % 时电池还有余量。
:显示曲线的设定,见第七节。同理显示 0 % 时电池仍有余量。


;新电池要不要"校准"?
;新电池是否需要校准
:不用刻意做。完整放一次电(从满电放到自动关机)会让固件学到真实满容量和
:不需要专门操作。完整放电一次(满电到自动关机)可让固件学到真实满容量和空点,
  空点;平时正常使用也会慢慢学到。
  日常使用也会逐步学到。


;健康度多久更新一次?
;健康度的更新频率
:需要一次完整的放电窗口(满电锚点 欠压),中途充电会作废。所以它更新得慢,
:需要一次完整放电窗口(满电锚点 欠压),中途充电则本次作废,因此更新较慢,
  而且是 EMA 缓变的,这是刻意的——避免单次误测就让健康度跳水。
  且按 EMA 缓变。

2026年7月26日 (日) 20:01的最新版本

光影猫2 不靠"看电压猜电量",而是用一套完整的库仑计(Coulomb counter,又叫电量计 / fuel gauge)来算电量:像给电池装了一个水表, 进出的每一毫安秒都记账。

本页讲清楚这套系统怎么工作——从硬件怎么测,到固件怎么积分、怎么自学习、 怎么做温度补偿,再到 Linux 内核怎么把它变成 sysfs 文件。

指示灯说明见 Photonicat 2 LED 指示灯,sysfs 路径见 Photonicat 2 sysfs。 English: Photonicat 2 SOC Calculation

电压与电量的关系

锂电池的"电压—电量"曲线中段非常平。下面取固件里的放电 OCV 表 (去 IR 后的整包电压):

整包 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 分为以下几层:

作用
硬件测量 TPA626 每 100 ms 测一次电压/电流
库仑积分 每 200 ms 把电流累加进 µAs 账本
OCV 融合 静置时用电压表反查电量,轻轻拉回积分漂移
锚点 充满 / 欠压时直接把电量钉死到 100 % / 0 %
自学习 满容量、内阻、空点、充电增益,每台机器各学各的
温度补偿 冷天内阻变大、可用容量变少,都要修正
显示层 把内部电量映射成用户看到的 1–100 %

一、硬件测量

测量芯片是 TPA626(I²C 电流/电压监测器,与 INA226 同类)。它串一个 毫欧级采样电阻在电池回路上,测电阻两端压降换算电流,同时测总线电压。

  • 采样周期:约 100 ms 一次(VD1_Collect
  • 中值滤波:电压和电流各过一个中值滤波器,滤掉 I²C 偶发抖动和负载尖峰
  • 原始值另存一份:中值滤波有约 3 s 滞后,会抹平充电器插拔的电压台阶;
 内阻自学习需要该瞬间台阶,故固件同时保留未滤波的原始值

电流校准

采样电阻和运放都有个体误差,出厂靠精密万用表实测标定,拟合成二次多项式:

 I_真实(mA) = C2·x² + C1·x + C0

当前系数(2026-07-24 二轮标定,四点残差 ≤3 mA):

说明
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 调一次 coulomb_counter_update(),把电流乘以时间 累加进一个 µAs(微安秒)的 64 位累加器。使用 µAs 而非 mAh,可避免定点 除法的舍入误差长期累积。

  • 放电减、充电加
  • 电流绝对值小于 30 mA 的部分不计入(死区,防噪声漂移)
  • 充电方向另有一个自学习的增益系数(充电效率不是 100 %)

三、OCV 融合

电池接近静置时,用电压反查电量表对积分做小幅修正。触发条件如下, 全部满足才执行:

条件 阈值 说明
电流足够小 |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 会偏乐观。 学习流程:

  1. 满电锚点触发时开窗口,累计放电清零
  2. 一路放电到欠压关机点
  3. 满容量 = 累计放出的电量 + 700 mAh
  4. 中途检测到充电 → 本次窗口作废

其中 700 mAh 是关机点余量:整机不会把电池放到 0 V,在关机点 (约 6.3–6.45 V)包内仍剩约 700 mAh,需补回。

学到的值换算成健康度

 健康度 = 学习容量 / 6800 mAh

健康度按 EMA 缓慢融入,单次测量不会使其大幅变化。

5.2 内阻学习(电压台阶法)

内阻由充电器插拔瞬间的电压台阶测得:电流突变 Δ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 显示空点学习

每台机器的电流计增益与实际容量存在偏差,内部 SOC 归零的时刻与硬件实际断电的 时刻可能不一致(表现为显示尚有 20 % 时断电)。固件不修改电流链路,而是在显示层 套一个每台机器自学习的线性映射:

 显示值 = (内部SOC − 空点) × 100 / (100 − 空点)

空点(empty)= 上一次"电池真的没电了"时的内部 SOC。学习事件包括: 低压关机命令、低压下主机意外死机、欠压锚点。

并带向下自纠:放电时电压仍健康(> 6.8 V)而内部 SOC 已低于空点, 说明空点偏高,空点随之下调。换电池或学习过头可在一个放电周期内恢复。

5.4 充电增益学习

充电效率不足 100 %,充入电荷与实际存入电荷存在差异。该增益在满电锚点学习, 变化 ≥1 % 时落盘。

六、温度补偿

板载 NTC 每 200 ms 把温度送入库仑计,用于两项修正:

6.1 内阻温度倍率 k(T)

内阻随温度变化,直接影响去 IR 的准确性。倍率取 NCA 典型温度特性的分段线性近似:

温度 内阻倍率 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)。 超出 −20…40 °C 的部分按端点截断。

6.2 低温可用容量补偿

低温下电池可用容量下降(电量并未消失,只是低温下无法取出)。固件据此 把显示空点上移,使显示电量提前走低:

温度 空点上移
≥ 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 %。

八、关机后的电量保持

共三条路径:

8.1 SOC 快照

运行时 SOC 每漂移 ≥5 % 就自动落盘;关机、主机断电、进待机、OTA 复位前 强制落盘并刷新时间戳。

开机恢复策略:

  • ≤12 小时:SOC = 快照 − 2.0 mA × Δt(按待机电流扣减),并禁止
 OCV 重估覆盖。
  • >12 小时:快照过期,走冷启动 OCV 重估。

8.2 深睡放电估计

电池深睡时 TPA626 掉电,无法采集电流。固件进睡前记录 RTC 分钟戳, 唤醒后按整机待机约 2.0 mA 恒流扣减。

8.3 换电池识别

电池拔出后 RTC 掉电重置,时间倒退。固件据此判定为换电池,快照作废并重新冷启动估计。 判定留有 10 分钟容差,避免升级重启时误判丢弃有效快照。

九、数据如何到达 Linux

MCU 算完,通过 UART 把状态帧发给主机(RK3576)。主机侧有两个消费者:

9.1 内核驱动:photonicat-pm

这是一个 serdev 驱动(990-photonicat-pm-add-driver.patch, 内核 6.12),直接挂在串口上解析状态帧,然后注册成标准 Linux 设备:

  • power_supply 类 → /sys/class/power_supply/battery
  • RTC 类 → /dev/rtc

由于注册为标准子系统,现成 Linux 工具可直接使用,无需了解私有协议:

# 电量(%),这就是上面整套算法的最终输出
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

MCU 即硬件 RTC。板上没有独立 RTC 芯片,MCU 常供电并自行走时, 时间随状态帧上送。驱动将其注册为 rtc_class_ops,标准工具可直接使用:

hwclock -r          # 读 MCU 里的时间
hwclock -w          # 把系统时间写回 MCU

因此断网断电数日后开机时间仍准确。8.3 的换电池识别也依赖它: RTC 掉电重置后时间会倒退。

9.2 用户态守护进程:pcat-manager

内核驱动只处理状态帧中的电源部分。其余 UART 协议由 pcat-manager 处理: 调制解调器管理、看门狗、开关机策略、按键事件、充电阈值下发、MCU 固件升级等。 完整协议见 Photonicat 2 MCU 通讯协议

分工如下:

组件 负责
MCU 固件 测量、积分、学习、补偿——所有算法
photonicat-pm(内核) 电源/RTC 数据 → 标准 sysfs 接口
pcat-manager(用户态) 其余协议:调制解调器、看门狗、电源策略、OTA

充电上限为例:网页设置 80 % 后,pcat-manager 通过 UART 下发给 MCU, MCU 存入 data flash 并直接控制充电 MOSFET。自固件 RA2E1260726005 起该设置在 MCU 侧持久化,复位或 OTA 后不丢失;pcat-manager 同时保留镜像,检测到 MCU 重启时重新下发。

十、常见问题

电量偶尔小幅跳动
OCV 融合或锚点正在修正积分漂移。单次修正上限 0.20 %,正常情况下不会出现大跳变;
若出现大跳变,通常是快照过期后走了冷启动重估。
冬天电量下降更快
低温下可用容量下降,同时固件把显示空点上移(−10 °C 约 +8 %)。回暖后恢复。
充到 96 % 即显示 100 %
显示曲线的设定,见第七节。同理显示 0 % 时电池仍有余量。
新电池是否需要校准
不需要专门操作。完整放电一次(满电到自动关机)可让固件学到真实满容量和空点,
日常使用也会逐步学到。
健康度的更新频率
需要一次完整放电窗口(满电锚点 → 欠压),中途充电则本次作废,因此更新较慢,
且按 EMA 缓变。