引言

在动力电池管理系统(BMS)中,SOC(State of Charge,荷电状态)估算是最核心的功能之一。它直接关系到电池的使用安全、续航里程预测以及充放电策略的制定。在众多SOC估算算法中,安时积分法(也称库仑计法)因其原理简单、实时性好而成为工程应用中的基础算法。

然而,看似简单的”电流乘以时间”背后,隐藏着大量工程实现细节:如何在没有浮点运算单元的MCU上高效计算?如何设计单位换算避免精度损失?如何处理累积误差?本文将基于一个真实的车规级BMS项目(基于NXP芯片),深入剖析安时积分法的完整工程实现。

一、安时积分法的理论基础

1.1 基本原理

安时积分法的核心思想源于电荷守恒定律。电池的SOC本质上是当前剩余电量与总容量的比值:

$$
SOC(t) = SOC(t0) + \frac{1}{C_{rated}} \times \int_{t0}^{t} \eta(I) \times I(\tau) d\tau
$$

其中:

  • **SOC(t0)**:初始荷电状态
  • C_rated:电池额定容量
  • **I(τ)**:τ时刻的电流(充电为负,放电为正)
  • **η(I)**:充放电效率(通常充电效率0.95-1.0,放电效率接近1.0)

在数字系统中,连续积分被离散化为累加:

$$
\Delta Cap = I \times \Delta t
$$

$$
SOC(k) = SOC(k-1) - \frac{\Delta Cap}{C_{rated}}
$$

1.2 算法优势与局限

优势:

  • 实时性强:每个采样周期都能更新SOC
  • 动态响应快:能准确跟踪大电流充放电过程
  • 计算简单:适合资源受限的嵌入式系统

局限:

  • 误差累积:传感器漂移、量化误差会不断叠加
  • 依赖初值:需要准确的SOC初始值
  • 无自校正能力:必须配合其他算法(如OCV法)定期修正

正因为这些局限,工程实现中需要精心设计数据结构、单位换算和误差控制策略。

二、工程实现的单位级联设计

2.1 为什么不直接用浮点数?

在这个项目中,采用的是 NXP 芯片,虽然它配备了FPU(浮点运算单元),但工程师选择了整数运算方案。原因有三:

  1. 确定性:整数运算的执行时间固定,不会因为数值大小产生波动,这对实时系统至关重要
  2. 资源分配:FPU资源可以留给更复杂的算法(如卡尔曼滤波、神经网络SOC估算)
  3. 精度可控:通过合理的单位设计,整数运算可以达到足够的精度

2.2 三级单位级联架构

代码中设计了一个巧妙的三级单位系统:

1
2
3
4
5
6
7
8
9
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。每次采样后执行:

1
2
3
4
5
6
7
8
9
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
2
3
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

1
2
3
4
5
6
#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
1个计数单位 = 10mA × 1ms = 0.01 mA·s = 0.01/3600 mAh ≈ 2.78 μAh

对于一个100Ah的电池包,相对精度为:

1
2.78 μAh / 100000 mAh = 2.78 × 10^-8 ≈ 0.0000028%

这个精度远超实际需求(通常SOC精度要求在1%以内),因此整数方案完全可行。

三、充放电方向处理与净变化量

3.1 双向累积的必要性

电池既可以充电也可以放电,代码中分别维护了两个独立的累积器:

1
2
sCapInt.chgCap10ma1ms  // 充电累积
sCapInt.dhgCap10ma1ms // 放电累积

为什么不用一个有符号变量?因为:

  1. 统计需求:需要分别记录累计充电量和累计放电量,用于电池健康度分析
  2. 效率计算:充放电效率不同,分开记录便于后续修正
  3. 溢出保护:无符号整数的溢出行为更可预测

3.2 净变化量的巧妙设计

代码中有一个关键变量 changCap1ma1h,它记录的是充放电的净变化:

1
2
3
4
5
// 充电时(电流为负)
sCapInt.changCap1ma1h -= (s32)(sCapInt.chgCap10ma1ms / MULT_MAH_TO_10MAMS);

// 放电时(电流为正)
sCapInt.changCap1ma1h += (s32)(sCapInt.dhgCap10ma1ms / MULT_MAH_TO_10MAMS);

这个变量在SOC计算中起到关键作用:

1
2
3
4
5
6
7
8
9
10
11
12
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 = \int V(t) \times I(t) dt
$$

4.2 四级单位级联

能量积分采用了更复杂的四级单位系统:

1
2
3
4
5
6
7
8
9
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;

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
1 Wh = 1 W × 1 h = 1 W × 3600 s = 3600 W·s = 3,600,000 W·ms

4.3 电压采样的同步问题

注意代码中的一个细节:

1
2
3
4
5
6
// 在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采样是异步的。代码中通过标志位确保积分使用的是稳定的采样值:

1
2
3
4
5
if((0 == SampGetCurrSampExpFlagAPI(eCCHAN_Num))  // 电流采样正常
&& (sRealCurr10mA > sCurrZeroMax)
&& (sRealCurr10mA >= (s16)gGHardPara_104[eGHardPara104_DhgIntPoint])) {
DhgCapIntTask(sRealCurr10mA, 1);
}

SampGetCurrSampExpFlagAPI() 检查采样异常标志,只有在采样正常时才执行积分。这避免了使用脏数据导致的SOC跳变。

5.2 积分起始点的设定

代码中定义了积分起始点:

1
2
gGHardPara_104[eGHardPara104_ChgIntPoint]  // 充电积分起始点
gGHardPara_104[eGHardPara104_DhgIntPoint] // 放电积分起始点

这是为了避免小电流噪声的累积。例如,如果积分起始点设为 50(即500mA),那么小于500mA的电流不会被积分,这在静置状态下能有效抑制零漂误差。

六、累积误差来源与控制策略

6.1 误差来源分析

1. 电流传感器零漂

霍尔传感器的零点会随温度漂移,典型值为 ±50mA。假设零漂为 +50mA(实际电流为0,但传感器输出50mA),在24小时内累积的误差为:

1
误差 = 50mA × 24h = 1200 mAh = 1.2 Ah

对于100Ah电池,这相当于 1.2% 的SOC误差。

2. ADC量化误差

假设ADC分辨率为12位,电流测量范围为 ±500A,则量化步长为:

1
LSB = 1000A / 4096 ≈ 0.244 A = 244 mA

每次采样的量化误差为 ±122mA,在随机分布假设下,N次采样后的累积误差为:

$$
\sigma_{累积} = \sigma_{单次} \times \sqrt{N}
$$

3. 采样周期不均匀

在RTOS环境下,任务调度会产生抖动。假设标称周期为1ms,实际周期在0.9-1.1ms之间波动,那么在1小时内:

1
2
理论采样次数 = 3,600,000
实际采样次数 = 3,600,000 ± 360,000(±10%)

这会导致时间基准误差。

6.2 工程中的误差控制策略

策略1:异常值检测与丢弃

1
2
3
4
if((realChangCap > (s32)sCapForm.standCap) ||
(realChangCap < (0 - (s32)sCapForm.standCap))) {
realChangCap = 0; // 丢弃异常值
}

如果单次变化量超过额定容量,显然是异常数据(传感器故障或通信错误),直接丢弃。

策略2:边界限制

1
2
3
4
5
6
7
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 中):

1
2
3
4
5
6
7
8
9
10
11
// 充电末端校正
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:静置状态下的电压校正

1
2
3
4
5
6
7
8
9
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定期存储

1
2
3
4
5
6
7
8
9
10
11
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 分段变速追踪策略

代码中实现了一个精妙的分段变速追踪算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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秒 用户最关心”何时充满”,加快追踪
80%-90% 60秒 适中追踪速度
0%-80% 120秒 较慢追踪,避免频繁跳变引起焦虑

放电时采用对称的策略,在低SOC区域(0%-20%)加快追踪,提醒用户及时充电。

7.3 基于电量变化的追踪

除了时间因素,算法还考虑了实际电量变化:

1
2
3
4
5
6
7
inteCap = GetChgDhgChangCapAPI();  // 获取净变化量
changCap = ABS(sHisCap, inteCap);
changSoc = (u16)(changCap * 10000 / totalCap);

if(0 == changSoc) {
inteCap = sHisCap; // 变化太小,不更新历史值
}

只有当电量变化足够大时,才更新显示SOC。这避免了小电流下的频繁跳变。

八、工程实践中的经验总结

8.1 单位设计的黄金法则

  1. 最小单位要足够小:确保精度满足需求
  2. 进位常数要合理:避免频繁进位导致的计算开销
  3. 中间单位要实用:匹配实际应用场景(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%)