第四节:开路电压法原理

开路电压法,简称OCV法。说白了,就是利用电池静置时的端电压来推算剩余电量。这个方法在BMS里属于最基础、最直观的SOC估算手段。我刚开始做BMS那会儿,第一个接触的算法就是它。

为什么能用电压推算电量?因为锂电池的化学特性决定了——在开路状态下,电池正负极的电势差和内部锂离子浓度有明确的对应关系。锂离子浓度高,电压就高;浓度低,电压就低。这个关系虽然不是严格的线性,但足够稳定、可重复。

核心思想:开路电压 ≈ SOC的一一映射函数。只要标定好这个函数,就能通过测电压反推SOC。

4.1 OCV-SOC曲线标定

标定曲线,是开路电压法的第一步,也是最耗时的一步。我记得在项目初期,我们为了拿到一条准确的OCV-SOC曲线,整整在实验室泡了一周。

标定流程大致是这样的:

  1. 满充:把电池充到4.2V(或厂家规定的截止电压),静置2小时以上
  2. 阶梯放电:以0.05C的小电流放电,每放5%或10%的SOC就停下来
  3. 充分静置:每次放电后静置1-2小时,让电池内部达到电化学平衡
  4. 记录电压:静置结束前10秒记录端电压,作为该SOC点的OCV值
  5. 重复:直到放电到截止电压(比如3.0V)

这里有个坑——静置时间不够,电压会漂。我曾经遇到过,静置30分钟就记录数据,结果曲线和厂家给的差了将近50mV。后来才发现,电池内部的极化效应需要更长时间才能消除。

标定完成后,你会得到类似这样的数据表:

SOC (%) OCV (V) 备注
100 4.200 满充状态
90 4.050 平台区开始
80 3.950 平台区
70 3.880 平台区
60 3.820 平台区
50 3.770 平台区
40 3.720 平台区
30 3.650 平台区
20 3.550 开始陡降
10 3.350 陡降区
5 3.150 接近截止
0 3.000 截止电压

你想想看,磷酸铁锂的曲线在20%-80%区间几乎是一条平线,电压变化只有几十毫伏。这时候用OCV法,误差会非常大。三元锂就好很多,曲线斜率明显。

4.2 查表法实现

标定完曲线,接下来就是怎么在代码里用。最直接的方法就是查表法。说白了,就是把标定数据做成一张表,MCU运行时根据测到的电压去表里找对应的SOC。

我个人习惯用线性插值,因为标定点之间的电压变化可以近似为线性。代码实现也不复杂:

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

/* 标定表,按OCV升序排列 */
const OCV_SOC_Entry ocv_table[] = {
    {3.000, 0.0},
    {3.150, 5.0},
    {3.350, 10.0},
    {3.550, 20.0},
    {3.650, 30.0},
    {3.720, 40.0},
    {3.770, 50.0},
    {3.820, 60.0},
    {3.880, 70.0},
    {3.950, 80.0},
    {4.050, 90.0},
    {4.200, 100.0}
};

#define TABLE_SIZE  (sizeof(ocv_table) / sizeof(ocv_table[0]))

/* 线性插值查表函数 */
float ocv_to_soc(float measured_ocv)
{
    uint8_t i;
    float soc_result;

    /* 边界检查 */
    if (measured_ocv <= ocv_table[0].ocv) {
        return ocv_table[0].soc;
    }
    if (measured_ocv >= ocv_table[TABLE_SIZE - 1].ocv) {
        return ocv_table[TABLE_SIZE - 1].soc;
    }

    /* 查找区间 */
    for (i = 0; i < TABLE_SIZE - 1; i++) {
        if (measured_ocv >= ocv_table[i].ocv &&
            measured_ocv < ocv_table[i + 1].ocv) {
            break;
        }
    }

    /* 线性插值 */
    soc_result = ocv_table[i].soc +
        (measured_ocv - ocv_table[i].ocv) *
        (ocv_table[i + 1].soc - ocv_table[i].soc) /
        (ocv_table[i + 1].ocv - ocv_table[i].ocv);

    return soc_result;
}

小技巧:查表时记得做边界保护。如果测到的电压低于表中最小值,直接返回0%;高于最大值,返回100%。别让程序跑飞了。

嗯,这里要注意——查表法虽然简单,但精度受限于标定点密度。标定点越多,插值误差越小,但占用的Flash也越大。我一般建议在平台区加密标定点,陡降区可以稀疏一些。

4.3 温度对OCV的影响

温度是OCV法的头号敌人。为什么?因为电池内部的化学反应速率受温度影响很大。低温下,电解液粘度增加,锂离子迁移变慢,导致开路电压偏低。高温下则相反。

我记得有一次冬天做路试,BMS报出来的SOC比实际低了将近8%。排查了半天,才发现是温度补偿没做好。从那以后,我每次做OCV标定都会同时记录温度。

温度对OCV的影响,大致有这几个规律:

  • 低温(-20°C ~ 0°C):OCV整体偏低,尤其在低SOC区域偏差更大,可达50-100mV
  • 常温(0°C ~ 45°C):OCV相对稳定,偏差在10mV以内
  • 高温(45°C ~ 60°C):OCV略微偏高,但影响比低温小

实际工程中,我们通常的做法是建立多组温度下的OCV-SOC表。比如在-20°C、0°C、25°C、45°C四个温度点分别标定,然后根据当前温度做二维插值。

/* 温度补偿示例:根据温度选择不同的OCV表 */
typedef struct {
    float temperature;          /* 标定温度,单位°C */
    const OCV_SOC_Entry *table; /* 该温度下的OCV表 */
    uint8_t table_size;         /* 表长度 */
} Temp_OCV_Table;

const Temp_OCV_Table temp_ocv_tables[] = {
    {-20, ocv_table_minus20, 12},
    {  0, ocv_table_0,       12},
    { 25, ocv_table_25,      12},
    { 45, ocv_table_45,      12}
};

/* 根据温度选择最近的OCV表 */
const OCV_SOC_Entry* select_ocv_table(float temp)
{
    if (temp < -10) return temp_ocv_tables[0].table;
    if (temp < 12)  return temp_ocv_tables[1].table;
    if (temp < 35)  return temp_ocv_tables[2].table;
    return temp_ocv_tables[3].table;
}

避坑指南:我曾经遇到过,在低温下直接用常温的OCV表,结果SOC估算偏差超过15%。后来加了温度补偿,误差才降到3%以内。记住——没有温度补偿的OCV法,在极端温度下基本不可用

4.4 静置时间判定

最后一个关键点——什么时候才能用OCV法?答案是:电池必须充分静置

电池在充放电后,内部会有极化效应。极化电压会叠加在开路电压上,导致测出来的电压偏高或偏低。只有等极化完全消退,测到的才是真正的OCV。

那静置多久才够?这取决于电池的化学体系和当前状态。我总结了一个经验法则:

  • 小电流充放电后(比如0.1C):静置30分钟基本够用
  • 大电流充放电后(比如1C以上):建议静置1-2小时
  • 极端温度下(低于-10°C或高于50°C):静置时间要加倍

在代码里,我们通常用电流积分+计时器来判断静置状态:

/* 静置状态判定 */
#define QUIESCENT_CURRENT_THRESHOLD  0.01  /* 静置电流阈值,单位A */
#define QUIESCENT_TIME_THRESHOLD     1800  /* 静置时间阈值,单位秒(30分钟) */

static uint32_t quiescent_timer = 0;
static uint8_t  is_quiescent = 0;

void check_quiescent_status(float current)
{
    if (fabs(current) < QUIESCENT_CURRENT_THRESHOLD) {
        /* 电流小于阈值,开始计时 */
        quiescent_timer++;
        if (quiescent_timer >= QUIESCENT_TIME_THRESHOLD) {
            is_quiescent = 1;  /* 达到静置条件 */
        }
    } else {
        /* 有电流流过,重置计时器 */
        quiescent_timer = 0;
        is_quiescent = 0;
    }
}

/* 仅在静置状态下使用OCV法更新SOC */
void update_soc_by_ocv(float measured_voltage, float temperature)
{
    if (is_quiescent) {
        const OCV_SOC_Entry *table = select_ocv_table(temperature);
        float soc = ocv_to_soc(measured_voltage);
        /* 更新SOC值 */
        battery_soc = soc;
    }
}

个人经验:静置时间判定不能只看电流。我建议同时监测电压变化率——如果电压在1分钟内变化小于1mV,基本可以认为极化已经消除。这个双重判断比单纯计时更可靠。

好了,开路电压法的核心内容就这些。总结一下:标定曲线是基础,查表法是手段,温度补偿是保障,静置判定是前提。这四个环节缺一不可。下一节我们会讲怎么把OCV法和安时积分法结合起来,做出更稳定的SOC估算。