引言
在动力电池管理系统(BMS)中,SOC(State of Charge,荷电状态)估算是最核心的功能之一。它直接关系到电池的使用安全、续航里程预测以及充放电策略的制定。在众多SOC估算算法中,安时积分法(也称库仑计法)因其原理简单、实时性好而成为工程应用中的基础算法。
然而,看似简单的"电流乘以时间"背后,隐藏着大量工程实现细节:如何在没有浮点运算单元的MCU上高效计算?如何设计单位换算避免精度损失?如何处理累积误差?本文将基于一个真实的车规级BMS项目(基于NXP芯片),深入剖析安时积分法的完整工程实现。
一、安时积分法的理论基础
1.1 基本原理
安时积分法的核心思想源于电荷守恒定律。电池的SOC本质上是当前剩余电量与总容量的比值:
SOC(t) = SOC(t0) + (1/Crated) × ∫[t0→t] η(I) × I(τ) dτ
其中:
SOC(t0):初始荷电状态Crated:电池额定容量I(τ):τ时刻的电流(充电为负,放电为正)η(I):充放电效率(通常充电效率0.95-1.0,放电效率接近1.0)
在数字系统中,连续积分被离散化为累加:
ΔCap = I × Δt
SOC(k) = SOC(k-1) - ΔCap / Crated
1.2 算法优势与局限
优势:
- 实时性强:每个采样周期都能更新SOC
- 动态响应快:能准确跟踪大电流充放电过程
- 计算简单:适合资源受限的嵌入式系统
局限:
- 误差累积:传感器漂移、量化误差会不断叠加
- 依赖初值:需要准确的SOC初始值
- 无自校正能力:必须配合其他算法(如OCV法)定期修正
正因为这些局限,工程实现中需要精心设计数据结构、单位换算和误差控制策略。
二、工程实现的单位级联设计
2.1 为什么不直接用浮点数?
在这个项目中,采用的是NXP芯片,虽然它配备了FPU(浮点运算单元),但工程师选择了整数运算方案。原因有三:
- 确定性:整数运算的执行时间固定,不会因为数值大小产生波动,这对实时系统至关重要
- 资源分配:FPU资源可以留给更复杂的算法(如卡尔曼滤波、神经网络SOC估算)
- 精度可控:通过合理的单位设计,整数运算可以达到足够的精度
2.2 三级单位级联架构
代码中设计了一个巧妙的三级单位系统:
typedef struct {
u32 chgCap10ma1ms; // 充电累积器(单位:10mA·ms)
u32 dhgCap10ma1ms; // 放电累积器(单位:10mA·ms)
u32 chgCap1ma1h; // 充电容量(单位:1mAh)
u32 dhgCap1ma1h; // 放电容量(单位:1mAh)
u32 chgCap1a1h; // 充电容量(单位:1Ah)
u32 dhgCap1a1h; // 放电容量(单位:1Ah)
s32 changCap1ma1h; // 净变化量(单位:1mAh)
} t_CAPINT;
第一级:10mA·ms(微观累积器)
电流采样值的单位是10mA,采样周期是1ms。每次采样后执行:
void ChgCapIntTask(u32 curr, u8 cycle) {
sCapInt.chgCap10ma1ms += (curr * cycle);
if(sCapInt.chgCap10ma1ms >= MULT_MAH_TO_10MAMS) { // 360000
sCapInt.changCap1ma1h -= (s32)(sCapInt.chgCap10ma1ms / MULT_MAH_TO_10MAMS);
sCapInt.chgCap1ma1h += (sCapInt.chgCap10ma1ms / MULT_MAH_TO_10MAMS);
sCapInt.chgCap10ma1ms %= MULT_MAH_TO_10MAMS;
}
}
这里的关键常数是360000,它的来历是:
1 mAh = 1 mA × 1 h = 1 mA × 3600 s = 3600 mA·s
= 3600 × 1000 mA·ms = 3,600,000 mA·ms
= 360,000 × 10mA·ms
第二级:1mAh(中观容量单位)
当累积器达到360000时,进位到mAh级别。这个单位适合小型储能系统(如电动自行车、小型UPS)。
第三级:1Ah(宏观容量单位)
对于大型储能系统(如电动汽车、储能柜),代码还支持进一步进位到Ah:
#if(0 == SMALL_ESS_EN)
if(sCapInt.chgCap1ma1h >= MULT_AH_TO_MAH) { // 1000
sCapInt.chgCap1a1h += (sCapInt.chgCap1ma1h / MULT_AH_TO_MAH);
sCapInt.chgCap1ma1h %= MULT_AH_TO_MAH;
}
#endif
2.3 单位设计的精度分析
采用10mA·ms作为最小单位,理论精度是:
1个计数单位 = 10mA × 1ms = 0.01 mA·s = 0.01/3600 mAh ≈ 2.78 μAh
对于一个100Ah的电池包,相对精度为:
2.78 μAh / 100000 mAh = 2.78 × 10^-8 ≈ 0.0000028%
这个精度远超实际需求(通常SOC精度要求在1%以内),因此整数方案完全可行。
三、充放电方向处理与净变化量
3.1 双向累积的必要性
电池既可以充电也可以放电,代码中分别维护了两个独立的累积器:
sCapInt.chgCap10ma1ms // 充电累积
sCapInt.dhgCap10ma1ms // 放电累积
为什么不用一个有符号变量?因为:
- 统计需求:需要分别记录累计充电量和累计放电量,用于电池健康度分析
- 效率计算:充放电效率不同,分开记录便于后续修正
- 溢出保护:无符号整数的溢出行为更可预测
3.2 净变化量的巧妙设计
代码中有一个关键变量changCap1ma1h,它记录的是充放电的净变化:
// 充电时(电流为负)
sCapInt.changCap1ma1h -= (s32)(sCapInt.chgCap10ma1ms / MULT_MAH_TO_10MAMS);
// 放电时(电流为正)
sCapInt.changCap1ma1h += (s32)(sCapInt.dhgCap10ma1ms / MULT_MAH_TO_10MAMS);
这个变量在SOC计算中起到关键作用:
static void CalcGroupNowCapHandle(void) {
s32 nowChangCap = GetChgDhgChangCapAPI();
s32 realChangCap = nowChangCap - sHisChangCap;
// 异常检测:变化量超过额定容量
if((realChangCap > (s32)sCapForm.standCap) ||
(realChangCap < (0 - (s32)sCapForm.standCap))) {
realChangCap = 0; // 丢弃异常值
}
sCapForm.nowCap -= realChangCap; // 更新当前容量
}
这里的差分计算(nowChangCap - sHisChangCap)是一个重要的工程技巧,它能够:
- 自动处理充放电切换
- 检测异常跳变(如传感器故障)
- 避免累积误差在短时间内爆发
四、能量积分的并行实现
4.1 为什么需要能量积分?
除了电量(Ah),BMS还需要统计能量(Wh),用于:
- 能量效率分析(充入能量 vs 放出能量)
- 电费计算(储能系统)
- 热管理(能量损耗 = 发热)
能量积分的公式是:
E = ∫ V(t) × I(t) dt
4.2 四级单位级联
能量积分采用了更复杂的四级单位系统:
typedef struct {
u32 chgEner1w1ms; // 充电能量缓存(单位:1W·ms = 1AV·ms)
u32 dhgEner1w1ms; // 放电能量缓存(单位:1W·ms)
u32 chgEner1w1h; // 充电能量(单位:1Wh)
u32 dhgEner1w1h; // 放电能量(单位:1Wh)
u32 chgEner100w1h; // 充电能量(单位:100Wh = 0.1kWh)
u32 dhgEner100w1h; // 放电能量(单位:100Wh)
s32 changEner1w1h; // 净变化量(单位:1Wh)
} t_ENERINT;
代码实现:
void ChgEnerIntTask(u32 curr, u16 volt, u8 cycle) {
static u32 sChg1mw1ms = 0;
// 10mA × 100mV × 1ms = 1mAV·ms = 1mW·ms
sChg1mw1ms += (curr * (u32)volt * cycle);
if(sChg1mw1ms >= MULT_WMS_TO_MWMS) { // 1000
sEnerInt.chgEner1w1ms += (sChg1mw1ms / MULT_WMS_TO_MWMS);
sChg1mw1ms %= MULT_WMS_TO_MWMS;
}
if(sEnerInt.chgEner1w1ms >= MULT_WH_TO_WMS) { // 3600000
sEnerInt.changEner1w1h -= (s32)(sEnerInt.chgEner1w1ms / MULT_WH_TO_WMS);
sEnerInt.chgEner1w1h += (sEnerInt.chgEner1w1ms / MULT_WH_TO_WMS);
sEnerInt.chgEner1w1ms %= MULT_WH_TO_WMS;
}
}
单位换算链:
1 Wh = 1 W × 1 h = 1 W × 3600 s = 3600 W·s = 3,600,000 W·ms
4.3 电压采样的同步问题
注意代码中的一个细节:
// 在CurrSample.c中
if(ABS(GetGCellSumVoltAPI(), GetGSampSumVoltAPI()) <= gGBmuHigLevPara_103[eBmuHigLevPara103_SumDiffV]) {
ChgEnerIntTask((0 - sRealCurr10mA), GetGCellSumVoltAPI(), 1);
} else {
ChgEnerIntTask((0 - sRealCurr10mA), GetGSampSumVoltAPI(), 1);
}
这里做了电压源的选择:
- 优先使用AFE芯片采集的单体电压总和(
GetGCellSumVoltAPI()) - 如果总压差异过大,则使用总压传感器的值(
GetGSampSumVoltAPI())
这是因为单体电压求和的精度通常高于总压传感器,但在某些异常情况下(如单体采样失败),需要降级使用总压传感器。
五、积分触发时机与采样同步
5.1 采样完成标志的作用
在实时系统中,ADC采样是异步的。代码中通过标志位确保积分使用的是稳定的采样值:
if((0 == SampGetCurrSampExpFlagAPI(eCCHAN_Num)) // 电流采样正常
&& (sRealCurr10mA > sCurrZeroMax)
&& (sRealCurr10mA >= (s16)gGHardPara_104[eGHardPara104_DhgIntPoint])) {
DhgCapIntTask(sRealCurr10mA, 1);
}
SampGetCurrSampExpFlagAPI()检查采样异常标志,只有在采样正常时才执行积分。这避免了使用脏数据导致的SOC跳变。
5.2 积分起始点的设定
代码中定义了积分起始点:
gGHardPara_104[eGHardPara104_ChgIntPoint] // 充电积分起始点
gGHardPara_104[eGHardPara104_DhgIntPoint] // 放电积分起始点
这是为了避免小电流噪声的累积。例如,如果积分起始点设为50(即500mA),那么小于500mA的电流不会被积分,这在静置状态下能有效抑制零漂误差。
六、累积误差来源与控制策略
6.1 误差来源分析
1. 电流传感器零漂
霍尔传感器的零点会随温度漂移,典型值为±50mA。假设零漂为+50mA(实际电流为0,但传感器输出50mA),在24小时内累积的误差为:
误差 = 50mA × 24h = 1200 mAh = 1.2 Ah
对于100Ah电池,这相当于1.2%的SOC误差。
2. ADC量化误差
假设ADC分辨率为12位,电流测量范围为±500A,则量化步长为:
LSB = 1000A / 4096 ≈ 0.244 A = 244 mA
每次采样的量化误差为±122mA,在随机分布假设下,N次采样后的累积误差为:
σ_累积 = σ_单次 × √N
3. 采样周期不均匀
在RTOS环境下,任务调度会产生抖动。假设标称周期为1ms,实际周期在0.9-1.1ms之间波动,那么在1小时内:
理论采样次数 = 3600000 实际采样次数 = 3600000 ± 360000(±10%)
这会导致时间基准误差。
6.2 工程中的误差控制策略
策略1:异常值检测与丢弃
if((realChangCap > (s32)sCapForm.standCap) ||
(realChangCap < (0 - (s32)sCapForm.standCap))) {
realChangCap = 0; // 丢弃异常值
}
如果单次变化量超过额定容量,显然是异常数据(传感器故障或通信错误),直接丢弃。
策略2:边界限制
if(sCapForm.nowCap > sCapForm.topCap) {
sCapForm.nowCap = sCapForm.topCap; // 上限钳位
}
if(sCapForm.nowCap < sCapForm.baseCap) {
sCapForm.nowCap = sCapForm.baseCap; // 下限钳位
}
防止SOC超出0-100%范围。
策略3:基于电压的定期校正
代码中实现了复杂的电压-SOC校正逻辑(在SocDisplay.c中):
// 充电末端校正
if((GetGCellMaxVoltAPI() >= SOC_V_CHG_UP_MAX_V) && (nowSoc < SOC_V_CHG_UP_LES_SOC)) {
CorrGNowCapBySocAPI(SOC_V_CHG_UP_LES_SOC / 10);
aimSoc = SOC_V_CHG_UP_LES_SOC;
}
// 放电末端校正
if((GetGCellMinVoltAPI() <= SOC_V_DHG_DN_MAX_V) && (nowSoc > SOC_V_DHG_DN_MOS_SOC)) {
CorrGNowCapBySocAPI(SOC_V_DHG_DN_MOS_SOC / 10);
aimSoc = SOC_V_DHG_DN_MOS_SOC;
}
这是利用电池的OCV特性:在充电末端(高电压)和放电末端(低电压),电压与SOC有较强的相关性,可以用来修正累积误差。
策略4:静置状态下的电压校正
if(eCURR_IDLE == GetGChgDhgStateAPI()) {
if(sIdleTime >= SOC_SLOW_CORR_IDLE_T) { // 静置足够长时间
if(GetGCellMaxVoltAPI() < SOC_V_SEC_DN_MAX_V) {
if(nowSoc > SOC_V_SEC_DN_MOS_SOC) {
CorrGNowCapBySocAPI(SOC_V_SEC_DN_MOS_SOC / 10);
}
}
}
}
在静置状态下,电池电压会逐渐趋近于OCV,此时可以根据电压-SOC查找表进行校正。
策略5:EEPROM定期存储
static void StoreGroupNowCapToEEP(void) {
u16 changeSoc = (u16)(ABS(nowCap, sHisCap) * 1000 / GetGroupTotalCapAPI());
if((changeSoc >= SOC_CHANG_TO_WRITE_EEP) // SOC变化≥0.1%
|| ((ABS(nowCap, sHisCap) >= 10) // 或容量变化≥10mAh
&& (eCURR_IDLE == nowState) // 且当前静置
&& (eCURR_IDLE != sHisState))) { // 刚从充放电转为静置
EnerChangEepGNowCapHook(nowCap);
sHisCap = nowCap;
}
}
定期将SOC存入EEPROM,防止掉电后丢失。存储策略很巧妙:
- 正常情况下,SOC变化0.1%就存储一次
- 充放电结束时立即存储(即使变化量小于0.1%)
这样既保证了数据安全,又避免了频繁写EEPROM导致寿命问题。
七、SOC平滑显示算法
7.1 为什么需要平滑?
安时积分法计算的SOC是"真实SOC",它会随着电流波动而快速变化。但对于用户界面,频繁跳变的SOC会造成不良体验。因此需要一个平滑算法。
7.2 分段变速追踪策略
代码中实现了一个精妙的分段变速追踪算法:
if(copySoc < aimSoc) { // 显示值低于真实值(充电中)
if(aimSoc >= 9000) { // 目标SOC≥90%(需要减速)
if(aimSoc >= 9750) {
stepSoc = (aimSoc - copySoc) / 50; // 5s追上
} else if(aimSoc >= 9500) {
stepSoc = (aimSoc - copySoc) / 150; // 15s追上
} else if(aimSoc >= 9300) {
stepSoc = (aimSoc - copySoc) / 200; // 20s追上
} else {
stepSoc = (aimSoc - copySoc) / 300; // 30s追上
}
} else if(aimSoc >= 8000) {
stepSoc = (aimSoc - copySoc) / 600; // 60s追上
} else {
stepSoc = (aimSoc - copySoc) / 1200; // 120s追上
}
changSoc += stepSoc;
copySoc += changSoc;
}
这个算法的设计思想是:
- 高SOC区域(90%-100%):用户最关心"何时充满",因此加快追踪速度(5-30秒)
- 中SOC区域(80%-90%):适中的追踪速度(60秒)
- 低SOC区域(0%-80%):较慢的追踪速度(120秒),避免频繁跳变引起焦虑
放电时采用对称的策略,在低SOC区域(0%-20%)加快追踪,提醒用户及时充电。
7.3 基于电量变化的追踪
除了时间因素,算法还考虑了实际电量变化:
inteCap = GetChgDhgChangCapAPI(); // 获取净变化量
changCap = ABS(sHisCap, inteCap);
changSoc = (u16)(changCap * 10000 / totalCap);
if(0 == changSoc) {
inteCap = sHisCap; // 变化太小,不更新历史值
}
只有当电量变化足够大时,才更新显示SOC。这避免了小电流下的频繁跳变。
八、工程实践中的经验总结
8.1 单位设计的黄金法则
- 最小单位要足够小:确保精度满足需求
- 进位常数要合理:避免频繁进位导致的计算开销
- 中间单位要实用:匹配实际应用场景(mAh vs Ah)
8.2 误差控制的层次化策略
- 硬件层:选择高精度传感器,做好温度补偿
- 采样层:滤波、异常检测、同步控制
- 算法层:边界限制、差分计算、定期校正
- 系统层:多算法融合(安时积分 + OCV + 卡尔曼滤波)
8.3 实时性与精度的平衡
- 快速路径:安时积分(1ms周期),用于实时响应
- 慢速路径:电压校正(1s周期),用于误差修正
- 超慢路径:容量学习(数小时),用于长期优化
九、结语
安时积分法看似简单,但要在资源受限的嵌入式系统中实现高精度、高可靠性的SOC估算,需要在单位设计、误差控制、用户体验等多个维度进行精心设计。
本文基于真实的车规级BMS项目,展示了从理论到工程实现的完整链条。希望这些经验能帮助读者理解:优秀的嵌入式软件不仅需要扎实的理论基础,更需要对硬件特性、实时约束、用户需求的深刻理解。
在实际项目中,安时积分法通常不会单独使用,而是与开路电压法、扩展卡尔曼滤波、神经网络等算法融合,形成多层次的SOC估算体系。但无论算法如何演进,安时积分法作为基础,其工程实现的质量直接决定了整个系统的下限。
关键常数:
MULT_MAH_TO_10MAMS = 360000:mAh到10mA·ms的换算系数MULT_WH_TO_WMS = 3600000:Wh到W·ms的换算系数SOC_CHANG_TO_WRITE_EEP = 1:EEPROM存储阈值(0.1%)