安时积分法完整工程实现:单位换算、累积误差与修正策略

1 阅读13分钟

引言

在动力电池管理系统(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(浮点运算单元),但工程师选择了整数运算方案。原因有三:

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

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^-80.0000028%

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

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

3.1 双向累积的必要性

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

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

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

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

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 / 40960.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 单位设计的黄金法则

  1. 最小单位要足够小:确保精度满足需求
  2. 进位常数要合理:避免频繁进位导致的计算开销
  3. 中间单位要实用:匹配实际应用场景(mAh vs Ah)

8.2 误差控制的层次化策略

  1. 硬件层:选择高精度传感器,做好温度补偿
  2. 采样层:滤波、异常检测、同步控制
  3. 算法层:边界限制、差分计算、定期校正
  4. 系统层:多算法融合(安时积分 + 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%)