第三讲:开路电压法(OCV)——OCV-SOC曲线标定、查表法实现、温度补偿

各位同行,今天我们来聊聊开路电压法。说白了,就是利用电池静置时的端电压来反推SOC。这个方法在BMS里属于“压舱石”级别的存在——它不依赖历史积分,没有累积误差,只要电池静置时间够长,精度就非常可靠。

但这里有个坑:OCV-SOC曲线不是一条“万能曲线”。不同化学体系、不同老化程度、不同温度下,曲线都会漂移。我最早做项目时,直接拿厂家给的25℃曲线去算SOC,结果冬天现场数据一塌糊涂。嗯,从那以后我就明白了——OCV法看着简单,但要做好,得下功夫。

一、OCV-SOC曲线标定:从实验室到量产

标定曲线,说白了就是给电池拍一张“电压-电量”的X光片。怎么做?我习惯分三步走:

  1. 准备电池:选全新电芯,先做几个充放循环活化。我个人习惯至少循环3次,让电化学状态稳定下来。
  2. 分段静置:以5% SOC为步长,从100%开始放电到0%。每放5%电量,就静置2小时以上。为什么要这么久?因为极化电压需要时间消散。我在项目中遇到过,有些磷酸铁锂电池静置1小时电压还在缓慢下降,必须等它稳定。
  3. 记录数据:记录每个静置点结束时的端电压,然后做插值或拟合,得到完整的OCV-SOC曲线。

你想想看,如果只标定25℃一条曲线,到了零下10℃会怎样?电压整体下移,查表查出来的SOC可能偏大10%以上。所以,我建议至少标定三个温度点:-20℃、25℃、55℃。中间温度用线性插值就行。

核心要点:标定曲线时,静置时间必须足够。我曾经用1小时静置数据做曲线,结果查表误差达到3%。后来改成2小时,误差降到0.5%以内。这个时间成本,值得花。

二、查表法实现:代码怎么写才高效?

曲线标定好了,接下来就是查表。嵌入式系统里,查表法比实时计算快得多。我常用的方法是二分查找加线性插值。直接上代码:

/* OCV-SOC 查表结构体 */
typedef struct {
    float ocv;   /* 开路电压,单位V */
    float soc;   /* 对应的SOC,单位% */
} OCV_Table_t;

/* 示例:25℃下的OCV-SOC表,共21个点 */
const OCV_Table_t ocv_table_25C[21] = {
    {3.000, 0.0}, {3.100, 5.0}, {3.200, 10.0}, /* ... 中间省略 ... */
    {4.200, 100.0}
};

/* 二分查找 + 线性插值 */
float OCV_To_SOC(float voltage, const OCV_Table_t *table, uint8_t size)
{
    uint8_t low = 0, high = size - 1, mid;
    float soc;

    /* 边界检查 */
    if (voltage <= table[low].ocv)  return table[low].soc;
    if (voltage >= table[high].ocv) return table[high].soc;

    /* 二分查找 */
    while (high - low > 1) {
        mid = (low + high) >> 1;
        if (voltage >= table[mid].ocv)
            low = mid;
        else
            high = mid;
    }

    /* 线性插值 */
    soc = table[low].soc + 
          (voltage - table[low].ocv) * 
          (table[high].soc - table[low].soc) / 
          (table[high].ocv - table[low].ocv);

    return soc;
}

这段代码我用了很多年,基本没出过问题。注意几点:

  • 表格点要按电压升序排列,否则二分查找会乱套。
  • 边界检查不能省。万一ADC采到4.3V(过压),直接返回100% SOC,别让它插值插到天上去。
  • 二分查找的循环条件用 high - low > 1,这样退出时low和high就是相邻的两个点,直接插值。

我的小技巧:如果MCU资源紧张,可以把表格点压缩到16个以内。我试过,只要曲线形状不是太陡,16个点插值误差能控制在1%以内。省下来的Flash空间,可以放别的算法。

三、温度补偿:别让冬天“冻住”你的SOC

温度对OCV的影响,说白了就是电池内阻和电化学活性的变化。低温下,电压整体偏低;高温下,电压偏高。如果不补偿,查表结果会严重偏离真实值。

我常用的补偿方法是:建立多温度OCV表,运行时根据当前温度选择或插值

举个例子,假设我们标定了-20℃、25℃、55℃三张表:

温度区间 使用策略
T < -10℃ 直接使用-20℃表
-10℃ ≤ T ≤ 40℃ 在-20℃表和25℃表之间线性插值
40℃ < T ≤ 70℃ 在25℃表和55℃表之间线性插值
T > 70℃ 直接使用55℃表

代码实现也不复杂:

float OCV_To_SOC_With_Temp(float voltage, float temperature)
{
    float soc_low, soc_high, soc;
    float ratio;

    if (temperature < -10.0f) {
        /* 直接用低温表 */
        return OCV_To_SOC(voltage, ocv_table_N20C, TABLE_SIZE);
    }
    else if (temperature <= 40.0f) {
        /* 在-20℃和25℃之间插值 */
        soc_low  = OCV_To_SOC(voltage, ocv_table_N20C, TABLE_SIZE);
        soc_high = OCV_To_SOC(voltage, ocv_table_25C, TABLE_SIZE);
        ratio = (temperature + 20.0f) / 45.0f;  /* 归一化到[0,1] */
        soc = soc_low + ratio * (soc_high - soc_low);
    }
    else if (temperature <= 70.0f) {
        /* 在25℃和55℃之间插值 */
        soc_low  = OCV_To_SOC(voltage, ocv_table_25C, TABLE_SIZE);
        soc_high = OCV_To_SOC(voltage, ocv_table_55C, TABLE_SIZE);
        ratio = (temperature - 25.0f) / 30.0f;
        soc = soc_low + ratio * (soc_high - soc_low);
    }
    else {
        /* 直接用高温表 */
        return OCV_To_SOC(voltage, ocv_table_55C, TABLE_SIZE);
    }

    return soc;
}

注意:温度补偿不是万能的。如果电池温度低于-30℃或高于60℃,OCV法的可靠性会大幅下降。我建议在这种极端温度下,改用安时积分法为主,OCV法只做辅助校准。另外,温度传感器必须紧贴电芯表面,我曾经见过把传感器贴在模组外壳上的设计,测出来的温度偏差5℃以上,补偿全白做。

四、实战中的几个坑

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 静置时间不够:电池刚充完电,极化电压可能高达几十毫伏。如果不等它消散就测OCV,查出来的SOC可能偏大5%。我建议至少静置30分钟,磷酸铁锂要更久。
  • 曲线老化漂移:电池用了两年后,OCV-SOC曲线会整体下移。我习惯每半年或每100次满充后,重新标定一次曲线。或者用算法在线修正,这个后面章节会讲。
  • ADC精度不够:OCV法对电压测量精度要求很高。1mV的误差,可能带来0.5%的SOC偏差。我建议用16位以上的ADC,并且定期校准。

好了,这一讲就到这里。OCV法看着简单,但要做好,需要细心和耐心。下一讲我们会聊安时积分法,那个又是另一番天地了。

课后思考:如果你的电池在低温下OCV曲线变得非常平坦(比如磷酸铁锂在20%-80%区间),查表法会有什么问题?该怎么解决?欢迎在评论区讨论。