第二讲:安时积分法实现——电流采样与积分、初始SOC标定、时间离散化处理
好,咱们接着往下聊。上一讲我们把安时积分法的原理讲清楚了,说白了就是「电荷进出计数器」。但原理归原理,真要把它跑在单片机上,你会发现一堆坑等着你。今天这一讲,我就带你把这些坑一个一个填上。
2.1 电流采样:精度与噪声的博弈
先说说电流采样。这是安时积分法的「眼睛」,眼睛要是花了,后面算出来的SOC全是扯淡。
我在项目中遇到过最典型的情况:采样电阻选大了,发热严重;选小了,小电流工况下信噪比惨不忍睹。你想想看,电池在休眠状态下的自放电电流可能只有几毫安,如果采样分辨率不够,积分出来的SOC每天都在「自动增长」——这显然不合理。
2.1.1 采样电阻与ADC选型
我个人习惯的做法是这样的:
- 采样电阻:选1mΩ~5mΩ的合金电阻,温漂要控制在50ppm/℃以内
- ADC分辨率:至少16位,推荐24位Σ-Δ型ADC
- 采样率:100Hz~1kHz,别太低也别太高
为什么采样率不能太高?嗯,这里要注意:ADC的噪声是随带宽增加的。你采样率提到10kHz,噪声功率直接翻10倍,积分出来的SOC抖得像心电图。我一般用100Hz,够用。
关键公式:最小可检测电流
I_min = V_ref / (2^N * R_sense)
举例:V_ref=2.5V, N=16, R_sense=2mΩ
I_min = 2.5 / (65536 * 0.002) ≈ 19mA
看到没?16位ADC配2mΩ电阻,只能分辨到19mA。如果你的电池休眠电流只有5mA,那这19mA以下的电流你根本测不到。怎么办?要么换24位ADC,要么加一个低功耗的电流检测芯片。
2.1.2 采样数据的预处理
ADC采回来的原始数据,千万别直接拿去积分。我吃过这个亏——有一次项目急着交付,我偷懒没做滤波,结果SOC在静置状态下跳了3%。
我的标准流程是:
- 中值滤波:取连续5个采样点的中值,剔除毛刺
- 滑动平均:对中值滤波后的数据做16点滑动平均
- 零点校准:系统上电时,测量零电流时的ADC值,作为偏移量扣除
避坑指南:我曾经在零点校准时犯过一个低级错误——只校准了一次。后来发现温度变化会导致ADC偏移漂移。现在我每10分钟做一次零点校准,前提是检测到电流确实为零(比如继电器断开时)。
2.2 初始SOC标定:别让起点就错了
安时积分法有个致命弱点:它是个相对测量。你只知道从起点开始充了多少、放了多少,但起点本身是多少?如果起点错了,后面全错。
初始SOC标定,说白了就是回答一个问题:「电池现在有多少电?」
2.2.1 开路电压法标定
最常用的方法是开路电压法。锂电池的OCV与SOC有对应关系,查表就能得到初始值。
但这里有个坑:电池需要静置足够长时间,让极化电压消失。我一般要求静置2小时以上,磷酸铁锂电池甚至要4小时。
| 电池类型 | 静置时间 | OCV-SOC曲线特点 |
|---|---|---|
| 三元锂 | ≥1小时 | 中间段较陡,精度高 |
| 磷酸铁锂 | ≥4小时 | 中间段平坦,精度差 |
| 钛酸锂 | ≥30分钟 | 整体线性度好 |
特别注意:磷酸铁锂电池的OCV-SOC曲线在20%~80%区间几乎是一条直线,电压变化不到50mV。这时候用开路电压法标定,误差可能达到10%以上。我的做法是:如果检测到电池处于这个区间,就结合上次下电时的SOC做加权平均。
2.2.2 满充/满放标定
最可靠的标定方式,其实是让电池跑到满充或满放状态。满充时SOC=100%,满放时SOC=0%(当然要留保护余量)。
我在项目中是这样设计的:
- 充电到截止电压且电流小于0.05C时,强制标定SOC=100%
- 放电到保护电压且负载断开时,强制标定SOC=0%
每次标定完,我都会把当前的积分值清零,重新开始。这就像你跑步时每公里重置一下计步器,误差不会累积。
2.3 时间离散化处理:从连续到离散的跨越
理论上的安时积分是连续的:
SOC(t) = SOC(0) - (1/Q_n) * ∫I(t)dt
但单片机是数字系统,只能做离散计算。说白了,就是把积分变成累加:
SOC[k] = SOC[k-1] - (I[k] * Δt) / Q_n
这里Δt就是采样周期。我一般用100ms,也就是0.1秒。
2.3.1 离散化带来的误差
你想想看,电流是连续变化的,但我们每100ms才采一个点。这中间的变化被忽略了,这就是离散化误差。
举个例子:一个脉冲电流,峰值100A,持续50ms。如果采样周期是100ms,你可能刚好错过这个脉冲,或者只采到一半。积分出来的结果跟实际差了50%。
我的解决方案:
- 对于高频脉冲工况(如电机驱动),把采样率提高到1kHz
- 在硬件上加一个模拟积分器,用运放对电流做实时积分,MCU只读取积分结果
- 或者用电流传感器的Vout引脚直接接ADC,同时用其频率输出引脚做硬件计数
2.3.2 代码实现示例
下面是我常用的安时积分代码框架,你可以直接拿去用:
// 安时积分主函数,每10ms调用一次
void SOC_Update(void)
{
static float soc_integral = 0.0f; // 积分累积量,单位:Ah
static uint32_t last_tick = 0;
float current_A = Get_Current(); // 获取当前电流,单位:A
uint32_t now_tick = Get_Tick(); // 获取当前系统滴答,单位:ms
float dt = (now_tick - last_tick) / 1000.0f; // 时间差,单位:s
last_tick = now_tick;
// 安时积分:ΔAh = I * Δt / 3600
float delta_ah = current_A * dt / 3600.0f;
soc_integral += delta_ah;
// 计算SOC百分比
// 注意:Q_n是电池额定容量,单位:Ah
float soc_percent = (soc_integral / Q_n) * 100.0f;
// 加上初始SOC
float soc_final = soc_initial - soc_percent;
// 限幅处理
if(soc_final > 100.0f) soc_final = 100.0f;
if(soc_final < 0.0f) soc_final = 0.0f;
// 更新全局变量
g_soc = soc_final;
}
经验之谈:注意代码中的dt计算。我见过有人直接用固定值Δt,但实际系统中定时器可能有抖动。用系统滴答实时计算dt,虽然多花几个CPU周期,但精度提升明显。
2.3.3 累积误差的初步抑制
离散化带来的误差会随着时间累积。我常用的几个小技巧:
- 定期清零:每次检测到满充或满放,强制重置积分值
- 死区处理:电流绝对值小于10mA时,认为电流为零,不积分
- 温度补偿:容量Q_n随温度变化,查表修正
这些方法虽然不能完全消除误差,但能把误差控制在5%以内。下一讲我们会专门讲误差补偿,到时候再深入展开。
小结
这一讲我们聊了三个核心问题:
- 电流采样:选好ADC和采样电阻,做好滤波和零点校准
- 初始SOC标定:开路电压法+满充满放标定,双保险
- 时间离散化:把连续积分变成离散累加,注意采样率和dt的实时计算
说实话,安时积分法本身不复杂,难的是把每个细节都做到位。我刚开始做BMS时,觉得这玩意儿太简单了,结果被现场问题打得鼻青脸肿。后来才明白,嵌入式开发就是这样——细节决定成败。
下一讲我们聊误差补偿,我会把压箱底的卡尔曼滤波和自适应算法拿出来。到时候见。