第四节:开路电压法原理
开路电压法,简称OCV法。说白了,就是利用电池静置时的端电压来推算剩余电量。这个方法在BMS里属于最基础、最直观的SOC估算手段。我刚开始做BMS那会儿,第一个接触的算法就是它。
为什么能用电压推算电量?因为锂电池的化学特性决定了——在开路状态下,电池正负极的电势差和内部锂离子浓度有明确的对应关系。锂离子浓度高,电压就高;浓度低,电压就低。这个关系虽然不是严格的线性,但足够稳定、可重复。
核心思想:开路电压 ≈ SOC的一一映射函数。只要标定好这个函数,就能通过测电压反推SOC。
4.1 OCV-SOC曲线标定
标定曲线,是开路电压法的第一步,也是最耗时的一步。我记得在项目初期,我们为了拿到一条准确的OCV-SOC曲线,整整在实验室泡了一周。
标定流程大致是这样的:
- 满充:把电池充到4.2V(或厂家规定的截止电压),静置2小时以上
- 阶梯放电:以0.05C的小电流放电,每放5%或10%的SOC就停下来
- 充分静置:每次放电后静置1-2小时,让电池内部达到电化学平衡
- 记录电压:静置结束前10秒记录端电压,作为该SOC点的OCV值
- 重复:直到放电到截止电压(比如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估算。