简介:这份资源面向电池管理系统(BMS)方向的学习者与工程人员,提供基于扩展卡尔曼滤波器(EKF)与二阶RC等效电路模型的电池SOC预测仿真方案,适合已掌握MATLAB基础、希望深入理解SOC估算算法的中高级读者。压缩包共3个文件,包含2个mat实验数据文件与1个m脚本文件,整体约23KB,其中mat文件存放不同SOC状态下的电压与SOC对应数据,m脚本则完整实现二阶RC模型的动态方程、观测方程以及EKF的初值、噪声设定与状态更新流程。目前已有4218人学习下载,说明该仿真在同类资源中具有较高参考热度。读者可借助这套代码与数据,直观理解EKF如何对非线性电池模型进行线性化处理,掌握二阶RC模型参数拟合与SOC递推估计的完整链路,并在此基础上尝试模型校准、算法移植或实时性优化,为BMS算法研究与工程实践提供可复用的仿真起点。
1. 从一条“电压对不上”的曲线说起:EKF 二阶 RC 模型到底在算什么
如果你在电池 SOC 估算这个方向待过一阵,大概率见过这样的场景:同一组 HPPC 工况数据,有人用安时积分跑出来 SOC 一路飘,有人套了个一阶 RC 加扩展卡尔曼滤波,末端误差还是能到 5% 以上。问题往往不在滤波算法本身,而在电池模型——一阶 RC 把极化过程压成了一根指数衰减,而真实电芯在放电中后期存在明显的双时间尺度极化:电化学极化快、浓差极化慢。二阶 RC 模型就是给这两个过程各配一组 RC 并联环节,再用扩展卡尔曼滤波(EKF)在非线性观测方程下做状态递推。
这套方案解决的核心诉求很具体:在嵌入式算力有限、电流采样有噪声、初始 SOC 不确定的条件下,把 SOC 估算误差压到 3% 以内,并且能在线修正。它适合做 BMS 算法验证的工程师、做储能 EMS 策略的研究生,以及需要给仿真平台搭一套可信电池模型的开发者。往下我会把建模、参数辨识、EKF 离散化、仿真验证和踩坑点一层层拆开,代码全部用 Python 写,能直接跑。
2. 二阶 RC 等效电路建模:从电芯到状态空间方程
2.1 为什么不是一阶 RC,也不是电化学模型
选模型本质是在精度和算力之间找平衡点。电化学 P2D 模型精度高,但偏微分方程组在单片机上根本跑不动;一阶 RC 只有一个极化时间常数,在脉冲工况下能凑合,但遇到连续变电流或者低温大倍率放电,端电压拟合残差会明显变大。二阶 RC 多了一组 RC,能同时刻画秒级和分钟级的极化恢复过程,状态量从 2 个变成 4 个(SOC、两个极化电压),EKF 的雅可比矩阵规模仍然很小,算力开销对 Cortex-M4 级别的主控是可接受的。
我一般会这样判断:如果你的工况以恒流为主、温度稳定,一阶 RC 够用;如果要做动态工况、要在线辨识、要覆盖低温,直接上二阶 RC,省得后面返工。
2.2 电路方程与状态空间形式
二阶 RC 等效电路的结构是:开路电压源 Uoc(SOC) 串联欧姆内阻 R0,再串联两组并联 RC(R1//C1、R2//C2)。端电压方程:
U = Uoc(SOC) - I·R0 - U1 - U2
两个极化电压的动态:
dU1/dt = -U1/(R1·C1) + I/C1 dU2/dt = -U2/(R2·C2) + I/C2
SOC 的动态就是安时积分:
dSOC/dt = -I/(Qn·3600)
把这三个微分方程写成状态空间,状态向量 x = [SOC, U1, U2]^T,输入 u = I,输出 y = U。这就是后面 EKF 要离散化的连续模型。
2.3 用 Python 搭出模型骨架
先把模型写成可复用的类,参数后面辨识出来直接塞进去。
import numpy as np class SecondOrderRC: """二阶 RC 等效电路模型,输入电流约定放电为正""" def __init__(self, R0, R1, C1, R2, C2, Qn, ocv_func): self.R0 = R0 self.R1, self.C1 = R1, C1 self.R2, self.C2 = R2, C2 self.Qn = Qn # 额定容量,单位 Ah self.ocv_func = ocv_func # OCV-SOC 查表函数 def step(self, soc, u1, u2, I, dt): # 欧姆压降与极化压降 tau1 = self.R1 * self.C1 tau2 = self.R2 * self.C2 # 一阶后向差分离散化,保证数值稳定 u1_new = u1 * np.exp(-dt / tau1) + self.R1 * I * (1 - np.exp(-dt / tau1)) u2_new = u2 * np.exp(-dt / tau2) + self.R2 * I * (1 - np.exp(-dt / tau2)) soc_new = soc - I * dt / (self.Qn * 3600.0) u_ocv = self.ocv_func(soc_new) u_term = u_ocv - I * self.R0 - u1_new - u2_new return soc_new, u1_new, u2_new, u_term这段代码里有两个关键点。第一,极化电压的离散化用的是精确解形式(指数项),比前向欧拉稳定得多,dt 稍大也不会发散。第二,SOC 更新用放电为正的约定,如果你拿到的数据充电为正,记得在入口统一符号,否则 SOC 会反向跑。参数 dt 建议和实际采样周期一致,常见是 0.1s 或 1s,不要为了“看起来平滑”去插值改 dt,会引入额外误差。
3. 参数辨识:OCV-SOC 曲线和 RC 参数怎么拿
3.1 OCV-SOC 曲线的获取与拟合
OCV-SOC 是模型的基准,它错了后面全错。常见做法是小电流(0.02C~0.05C)间歇放电,每次放 5% SOC 静置 2 小时以上,记录静置末端电压作为该 SOC 点的 OCV。如果条件有限,也可以用低倍率充放电的平均值近似,但精度会差一些。
拿到数据点后,用单调三次样条或者分段线性插值建表。我一般用 numpy 的 interp1d 做线性插值,简单可靠,不会出现样条过冲。
from scipy.interpolate import interp1d # soc_pts 从 0 到 1 递增,ocv_pts 是对应静置电压 soc_pts = np.array([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) ocv_pts = np.array([3.00, 3.45, 3.55, 3.60, 3.65, 3.70, 3.78, 3.88, 4.00, 4.12, 4.20]) ocv_func = interp1d(soc_pts, ocv_pts, kind='linear', fill_value='extrapolate')注意 OCV 曲线必须单调递增,如果辨识出来有回环,说明静置时间不够或者温度变化太大,要重新测。另外 SOC 点不要只取 10 个,中段电压平台区建议加密到 5% 一个点,否则 EKF 在平台区会“感觉不到”SOC 变化。
3.2 用脉冲工况数据辨识 R0、R1、C1、R2、C2
R0 从脉冲瞬间的电压跳变直接算:R0 = ΔU / ΔI。两个 RC 参数用脉冲结束后的电压弛豫曲线拟合,常见做法是先用最小二乘拟合双指数函数,再反解出 R1、C1、R2、C2。
from scipy.optimize import curve_fit def dual_exp(t, u_inf, a1, tau1, a2, tau2): return u_inf - a1 * np.exp(-t / tau1) - a2 * np.exp(-t / tau2) # t_relax 是弛豫时间序列,u_relax 是对应端电压 popt, _ = curve_fit(dual_exp, t_relax, u_relax, p0=[u_relax[-1], 0.05, 10, 0.05, 200], bounds=([0, 0, 0.1, 0, 1], [5, 1, 100, 1, 2000])) u_inf, a1, tau1, a2, tau2 = popt R1, C1 = a1 / I_pulse, tau1 / (a1 / I_pulse) R2, C2 = a2 / I_pulse, tau2 / (a2 / I_pulse)这里 p0 的初值很关键,tau1 给 10 秒左右、tau2 给 200 秒左右,符合大多数锂电芯的双极化时间尺度。如果拟合不收敛,先检查弛豫数据有没有去掉前几个毫秒的采样毛刺。辨识出来的参数建议按 SOC 分段存表,因为 R0 和极化参数随 SOC 变化明显,尤其是两端。
3.3 参数随 SOC 和温度变化的处理策略
工程上不会只辨识一组参数。常见做法是按 SOC 每 10% 一个区间、温度每 10℃ 一个区间做二维查表,EKF 运行时根据当前 SOC 和温度插值取参。如果算力紧张,至少把 R0 做成 SOC 的函数,因为欧姆压降对端电压影响最直接。
提示:参数表不要用高阶多项式去拟合,外推到 SOC 两端容易出负值,查表加线性插值最稳。
4. EKF 离散化与仿真闭环:从方程到能跑的代码
4.1 状态转移矩阵和雅可比矩阵的推导
把第 2 章的连续模型离散化,状态转移用精确解形式:
SOC(k+1) = SOC(k) - I(k)·dt/(Qn·3600) U1(k+1) = U1(k)·exp(-dt/τ1) + R1·I(k)·(1-exp(-dt/τ1)) U2(k+1) = U2(k)·exp(-dt/τ2) + R2·I(k)·(1-exp(-dt/τ2))
状态转移矩阵 F 对状态求偏导,SOC 对 SOC 的偏导是 1,极化电压对自身的偏导是指数项,其余交叉项为 0。观测方程 y = Uoc(SOC) - I·R0 - U1 - U2,观测矩阵 H 对 SOC 的偏导是 dUoc/dSOC,对 U1、U2 的偏导是 -1。
dUoc/dSOC 用 OCV 表的数值差分算,不要解析求导,查表函数没有解析式。
4.2 EKF 的五个方程和代码实现
EKF 的标准流程是预测状态、预测协方差、算增益、更新状态、更新协方差。下面是一个可直接调用的实现。
class EKF: def __init__(self, model, soc0=1.0, P0=None, Q=None, R=None): self.m = model self.x = np.array([soc0, 0.0, 0.0]) # [SOC, U1, U2] self.P = np.diag([1e-3, 1e-4, 1e-4]) if P0 is None else P0 # 过程噪声:SOC 通道小,极化通道稍大 self.Q = np.diag([1e-8, 1e-6, 1e-6]) if Q is None else Q self.R = np.array([[1e-4]]) if R is None else R # 电压测量噪声 def predict(self, I, dt): tau1 = self.m.R1 * self.m.C1 tau2 = self.m.R2 * self.m.C2 a1 = np.exp(-dt / tau1) a2 = np.exp(-dt / tau2) # 状态预测 soc, u1, u2 = self.x soc_p = soc - I * dt / (self.m.Qn * 3600.0) u1_p = u1 * a1 + self.m.R1 * I * (1 - a1) u2_p = u2 * a2 + self.m.R2 * I * (1 - a2) self.x = np.array([soc_p, u1_p, u2_p]) # 状态转移矩阵 F = np.array([[1, 0, 0], [0, a1, 0], [0, 0, a2]]) self.P = F @ self.P @ F.T + self.Q return F def update(self, I, u_meas): soc, u1, u2 = self.x # 数值差分求 dUoc/dSOC eps = 1e-4 dUoc = (self.m.ocv_func(soc + eps) - self.m.ocv_func(soc - eps)) / (2 * eps) H = np.array([[dUoc, -1.0, -1.0]]) u_pred = self.m.ocv_func(soc) - I * self.m.R0 - u1 - u2 y = u_meas - u_pred S = H @ self.P @ H.T + self.R K = self.P @ H.T @ np.linalg.inv(S) self.x = self.x + (K @ y).flatten() # SOC 限幅,防止越界 self.x[0] = np.clip(self.x[0], 0.0, 1.0) self.P = (np.eye(3) - K @ H) @ self.P return self.x[0]Q 和 R 的整定是玄学也是血泪经验。R 取电压传感器的实际噪声方差,一般 1e-4 到 1e-3 之间。Q 的 SOC 通道取小一点(1e-8 量级),表示对安时积分比较信任;极化通道可以大一些,因为极化参数辨识误差大。如果发现 SOC 收敛慢,先调大 Q 的 SOC 通道;如果 SOC 抖动厉害,调大 R。
4.3 用动态工况数据跑闭环验证
拿一段 DST 或 US06 工况的电流电压数据,把 EKF 跑起来,对比估算 SOC 和参考 SOC。
def run_simulation(current, voltage, dt, model, ekf): soc_est = [] for k in range(len(current)): ekf.predict(current[k], dt) soc = ekf.update(current[k], voltage[k]) soc_est.append(soc) return np.array(soc_est) # 假设 current、voltage 已按放电为正对齐,dt 为采样周期 soc_est = run_simulation(current, voltage, 1.0, model, ekf) rmse = np.sqrt(np.mean((soc_est - soc_ref) ** 2)) print(f"SOC RMSE: {rmse*100:.2f}%")验证时重点看三个指标:RMSE、最大误差、末端误差。RMSE 反映整体,最大误差反映极端工况,末端误差反映有没有累积漂移。如果末端误差比最大误差还大,说明模型有系统性偏差,回去查 OCV 曲线和 R0 的 SOC 分段。
注意:参考 SOC 如果是安时积分算的,只能用来对比趋势,不能当绝对真值。有条件的话用充放电机的 SOC 记录做参考。
5. 避坑与排查:仿真跑不通时先看这几条
5.1 SOC 一路发散到负值
现象是仿真跑几十秒后 SOC 估算值冲到 0 以下或者超过 1。原因通常是电流符号约定和模型不一致,放电为正的模型喂了充电为正的数据,安时积分方向反了。解决方法是统一在数据入口做符号翻转,并在 EKF 里加 clip 限幅作为兜底。另外检查 Qn 单位,如果容量给的是 mAh 而公式里按 Ah 算,SOC 变化速度会差 1000 倍。
5.2 端电压拟合很好但 SOC 误差很大
这是最迷惑人的情况。端电压拟合好说明模型输出跟得上测量,但 SOC 可能被 OCV 曲线的平台区“吞掉”了。在电压平台区,dUoc/dSOC 很小,H 矩阵对 SOC 的敏感度低,EKF 几乎不修正 SOC。解决办法是在平台区适当调大 Q 的 SOC 通道,或者引入安时积分的强约束,别让滤波把 SOC 拉偏。另一个原因是 OCV 曲线在平台区取点太稀,加密到 2% 一个点会有改善。
5.3 协方差矩阵不收敛或报奇异
现象是 np.linalg.inv(S) 报奇异矩阵,或者 P 矩阵出现负特征值。原因通常是 R 给得太小,或者 H 矩阵在某一步全零。检查 dUoc 是否算出来是 0,如果 OCV 表在某个 SOC 区间是平的,数值差分就是 0。解决方法是给 dUoc 设一个下限,比如 1e-3,保证 H 矩阵始终有非零元素。另外 P 更新用 Joseph 形式更稳,但标准形式在大多数情况下够用。
5.4 参数辨识出来的 C1 或 C2 是负数
双指数拟合没有加边界约束时,tau 可能拟合出负值,反解 C 就成负数。解决方法是给 curve_fit 加 bounds,tau 下限设 0.1,上限设几千。如果还是负,检查弛豫数据是不是包含了脉冲期间的电压,那段数据不符合弛豫模型,要去掉。
5.5 仿真步长和实际采样周期不一致
有人拿 0.1s 采的数据,仿真时为了“平滑”重采样到 1s,结果 SOC 误差变大。原因是重采样改变了电流的积分量,安时积分对电流的积分是线性的,但极化电压的指数响应不是。正确做法是仿真步长严格等于采样周期,如果数据有缺失,用插值补点而不是降采样。
6. 进阶技巧:让 EKF 在嵌入式上跑得更稳的两个习惯
第一个习惯是给 EKF 加一个“SOC 开路电压回正”的慢修正。车辆静置时电流为 0,端电压约等于 OCV,这时候可以用 OCV 反查 SOC 去缓慢修正 EKF 的 SOC 估计,时间常数给几百秒,避免停车后 SOC 慢慢漂。实现上就是在电流绝对值小于阈值时,把 OCV 反查的 SOC 作为一个虚拟观测,用很小的增益更新。
第二个习惯是参数在线辨识和 EKF 解耦。不要试图用 EKF 同时估 SOC 和所有 RC 参数,状态维数一高,协方差矩阵条件数就爆炸。常见做法是用递推最小二乘(RLS)单独在线辨识 R0 和极化参数,辨识结果每隔一段时间喂给 EKF。RLS 的遗忘因子取 0.98 到 0.995 之间,太小会导致参数抖动,太大则跟踪不上。
class RLS: def __init__(self, n, lam=0.99, delta=1e4): self.theta = np.zeros(n) self.P = np.eye(n) * delta self.lam = lam def update(self, phi, y): # phi 是回归向量,y 是观测值 K = self.P @ phi / (self.lam + phi @ self.P @ phi) self.theta = self.theta + K * (y - phi @ self.theta) self.P = (self.P - np.outer(K, phi @ self.P)) / self.lam return self.theta验证方法上,我习惯做两组对比:一组用固定参数跑 EKF,一组用 RLS 在线辨识参数跑 EKF,看 SOC 的 RMSE 和最大误差有没有改善。如果改善不明显,说明参数时变不是主要误差源,问题在 OCV 曲线或者电流采样。另外做蒙特卡洛测试,给初始 SOC 加 ±20% 的随机偏差,跑 100 次看收敛时间分布,收敛时间超过 200 秒的工况要单独分析。
最后说个我自己的教训:早期做这套仿真时,我花了两周调 EKF 的 Q 和 R,结果发现真正的问题是 OCV 曲线测的时候静置时间不够,曲线整体偏低 20mV,导致 SOC 在低端一直偏大。后来养成习惯,任何 SOC 估算的异常,先回去查 OCV 曲线和电流符号,这两个对了,EKF 的参数整定才有意义。希望帮到你。
本文还有配套的精品资源,点击获取