资讯动态

BMS算法实践:二阶RC模型与卡尔曼滤波实现高精度SOC估计

发布时间:2026/10/19 6:18:05 来源:尧图企业网站定制
做BMS算法这些年我最怕听到的一句话就是SOC不准。电池这个东西内部化学反应看不见摸不着你只能通过电压、电流、温度这些外特性和一个靠谱的模型去猜它的荷电状态。在众多模型里二阶等效电路模型2RC ECM是我的默认起点配合参数辨识再套上卡尔曼滤波就能把SOC估计精度做到大多数工况下的实用水平。这篇文章就记录我从模型原理、标定流程、核心代码到踩坑复盘的全过程适合刚接手电池算法的工程师也适合想把手动查表升级成模型估算的硬件开发者。这篇内容不会只讲理论公式我会把每一步为什么这样做、有什么坑、怎么排查一起写出来。你拿到手里至少能少走我当初走过的那几段弯路。1. 为什么是2RC ECM模型结构与取舍1.1 一阶模型的短板和二阶模型的底气最早接触电池模型时很多人会从一阶RC模型开始。它由一个欧姆内阻R0和一个RC并联网络组成数学上就是V OCV - IR0 - V1其中V1满足dV1/dt -V1/(R1C1) I/C1。这个模型简单、参数少用最小二乘法几分钟就能辨识完。但一阶模型有一个明显的问题它假设电池的极化过程只用一个时间常数就能描述。真实电池里电荷转移产生的电化学极化通常只有几秒到几十秒的量级而锂离子在电极内部的固体扩散过程可能持续几分钟甚至十几分钟。把快过程和慢过程强行揉进一个RC网络结果就是短脉冲时模型预测偏慢长静置时模型残差又偏大。尤其在SOC较低或者温度较低时这种误差会直接传导到SOC估计里。2RC模型多增加一组RC网络就是为了把快极化和慢极化分开描述。多出来的三个参数R2、C2以及对应的时间常数τ2确实会增加辨识复杂度但换来的是端电压预测精度明显提升。我在实际项目中做过对比同样用扩展卡尔曼滤波一阶模型在动态工况下SOC最大误差能到6%二阶模型可以压到2%以内。代价是计算量只多了一点点现在的MCU完全扛得住。1.2 电路结构、数学表达和物理对应2RC ECM的电路拓扑很直观正极端点通过一个欧姆内阻R0然后串联两个RC并联网络最后接入开路电压源OCV(SOC)。这里的R0主要对应电解液、隔膜、极耳等部位的纯欧姆压降电流突变时它立刻反应表现为电压的瞬间跳变R1C1网络对应电荷转移过程时间常数一般在几秒到几十秒R2C2网络对应锂离子扩散过程时间常数通常在一分钟以上。写成方程就是端电压Vt OCV(z) - I*R0 - V1 - V2极化电压动态dV1/dt -V1/(R1C1) I/C1dV2/dt -V2/(R2C2) I/C2这里的z表示SOCOCV(z)是开路电压与SOC的静态映射关系。为什么电压方程里只有一个负号因为放电电流方向取正时欧姆内阻和极化内阻都会让端电压比OCV低充电时I为负端电压自然高于OCV。用生活化的方式理解电池就像一个带两个不同大小缓冲池的水管系统。R0是管道口的细窄阀门一开水流压力立刻下降R1C1是小缓冲池几秒内水位慢慢变化R2C2是大缓冲池十分钟以上水位才稳定。你测到的出口压力就是OCV池子的水位减去三个环节的压降。1.3 边界条件和使用场景2RC ECM不是万能的它的适用边界值得先说清楚。对于稳态工况、低倍率放电、常规充放电循环它的精度足够。电动两轮车、便携设备、家用储能、轻度混合动力这些场景2RC几乎是工程标配。但如果面对的是非常宽的SOC范围同时伴随大倍率脉冲或者电池已经老化到容量严重衰减2RC模型的固定参数会力不从心。此时要么引入第三个RC网络描述超慢扩散要么切换到电化学模型要么给参数加上SOC和温度的双维度查表。另外在10%SOC以下和95%SOC以上的区间OCV曲线非常平缓很小的电压测量误差就会导致SOC估计发散这个问题下面我会专门讲。2. 参数辨识从实验设计到递推算法2.1 标定OCV-SOC曲线做2RC ECM之前必须先有OCV-SOC曲线。没有这条曲线后面的一切都是空中楼阁。标准的标定方法是把电池在25℃环境下以0.33C恒流放电到截止电压静置1小时然后每放出10%额定容量就静置1小时记录静置末端的电压再继续放电。充电方向同样做一遍取充放电静置电压的平均值作为OCV点。静置时间为什么至少1小时因为2RC模型里第二组RC网络的时间常数可能在10分钟以上如果静置时间太短扩散极化还没有完全松弛测到的电压不是真正的开路电压。我曾经为了赶进度把静置时间压到15分钟结果SOC中段OCV偏差了20mV后期SOC估计偏差直接放大到3%以上。得到OCV-SOC的离散点后拟合方式有两种。一种是用多项式拟合比如五阶或六阶多项式优点是形式简洁、求导方便缺点是在SOC两端容易振铃Runge现象。另一种是分段线性插值每1%或每5%SOC一段精度可控卡尔曼滤波里需要计算dOCV/dSOC时直接查斜率。我在代码里推荐后者简单、稳健。2.2 离线辨识HPPC脉冲实验与指数拟合离线辨识是获得参数初值最可靠的手段经典实验是HPPC混合脉冲功率能力测试。做法是在某个SOC点静置足够长时间后以一定倍率比如1C放电10秒然后静置40秒再以1C充电10秒完成一个脉冲序列。在整个SOC范围内每10%重复一次。为什么放电和充电后都要静置放电结束的瞬间电流归零欧姆压降立即消失端电压会出现一个陡升随后极化电压缓慢衰减这个过程正好用来拟合RC参数。充电后的静置同理。整个脉冲实验记录下来的电压电流曲线包含的信息足够把R0、R1、C1、R2、C2全部敲定。离线辨识的经典做法是分步走。第一步从电流突变时刻的电压跳变算R0R0 ΔV_instant / ΔI。注意要取电流突变瞬间的电压变化量也就是去掉电感尖峰后的一小段。第二步将电流撤除后的零输入响应电压减去OCV得到V1V2的衰减曲线。第三步对这条曲线做二阶指数拟合V_pol(t) A1 * exp(-t/τ1) A2 * exp(-t/τ2)这样直接得到R1 A1/I_pulseC1 τ1/R1R2 A2/I_pulseC2 τ2/R2。用Python的scipy.optimize.curve_fit就能做给初值时要小心τ1给10秒量级τ2给100秒量级不然算法容易陷入局部最优。2.3 在线辨识带遗忘因子的递推最小二乘离线参数只能代表电池在测试时刻的健康状态。电池用久了内阻变大、容量衰减固定参数会让SOC估计越来越飘。在线辨识就是让模型参数在使用过程中跟着真实电池走。常用的在线辨识算法是带遗忘因子的递推最小二乘FFRLS。它的思路非常直接把2RC模型的端电压方程通过拉普拉斯变换和双线性离散化转换成一个差分方程Vt(k) - OCV(k) -a1*[Vt(k-1)-OCV(k-1)] - a2*[Vt(k-2)-OCV(k-2)] b0I(k) b1I(k-1) b2*I(k-2)这里的系数a1、a2、b0、b1、b2都是待辨识的线性系数。注意我们把OCV项当成已知量去减所以OCV曲线的精度直接影响在线辨识结果。如果SOC变了但OCV没跟上辨识出来的系数就是扭曲的。实际工程中会在电流较小且SOC变化平缓时才更新在线辨识或者用上一轮SOC估计值去插值OCV。FFRLS核心递推式增益矩阵K(k) P(k-1)φ(k) / (λ φ(k)^TP(k-1)*φ(k))协方差更新P(k) (I - K(k)*φ(k)^T)*P(k-1) / λ参数更新θ(k) θ(k-1) K(k)[y(k) - φ(k)^Tθ(k-1)]其中φ(k)是历史电压电流构成的回归向量θ是待辨识系数向量λ是遗忘因子。λ的选择很关键λ越接近1辨识结果越平稳但跟踪变化越慢λ太小则参数剧烈抖动。一般取0.98到0.995。我在静置工况下用0.995动态工况下用0.98。辨识出a、b系数后还需要映射回R0、R1、C1、R2、C2。这个映射用双线性逆变换完成代码实现比手推公式更稳我会在后面的实操部分给出函数。2.4 参数辨识的注意事项第一电流传感器和电压传感器的同步性。如果电压电流采样不同步哪怕相差50ms辨识出的R0都会系统性偏大两个时间常数也会被扭曲。第二激励要足够丰富。纯直流恒流放电时电压响应变化缓慢回归矩阵病态辨识结果不可信。最好用HPPC、动态工况或者包含不同倍率阶跃的电流序列。第三遗忘因子对噪声敏感。如果电流噪声大参数会在每个采样点来回跳可以先对电流电压做低通滤波但滤波截止频率不能太低否则会把脉冲特征滤掉。3. 从参数到SOC基于模型的SOC估计策略3.1 为什么不能直接查表拿到OCV-SOC曲线后最简单的SOC获取方式是测静置电压然后查表。这个方法在静态场景下很准但电池一有电流欧姆压降和极化压降会完全覆盖OCV的变化。举个例子1C放电时R0上可能有50mV压降两个RC网络又有几十毫伏极化而OCV在SOC中间段的斜可能只有0.1mV/1%SOC直接用端电压查表SOC误差能超过20%。所以工程上要做的是把模型方程和滤波算法结合用电流作为输入通过模型预测电压再把预测电压和实测电压的残差反馈修正SOC。这就是基于模型的状态估计。3.2 离散状态空间方程与EKF把2RC ECM写成离散状态方程采样周期为ΔtSOC(k1) SOC(k) - η*Δt/Q * I(k)V1(k1) exp(-Δt/τ1)V1(k) R1(1 - exp(-Δt/τ1))*I(k)V2(k1) exp(-Δt/τ2)V2(k) R2(1 - exp(-Δt/τ2))*I(k)测量方程Vt(k) OCV(SOC(k)) - R0*I(k) - V1(k) - V2(k)这里η是库伦效率充电取0.99左右放电基本取1Q是当前容量因为老化Q会变所以很多BMS会在线估算容量否则SOC会系统性漂移。状态向量x [SOC, V1, V2]^T输入u I输出y Vt。扩展卡尔曼滤波的核心就是对非线性测量方程做一阶泰勒展开。在这个模型里只有OCV(SOC)是非线性的所以只需要求一个偏导数H(k) [dOCV/dSOC |_{SOChat(x)}, -1, -1]状态转移矩阵F是线性的F [[1, 0, 0], [0, exp(-Δt/τ1), 0], [0, 0, exp(-Δt/τ2)]]有了F、H标准EKF流程就可以跑先预测状态和协方差再计算卡尔曼增益最后用电压残差更新状态。EKF的噪声协方差Q和R怎么调我的经验是先固定R为电压传感器噪声方差比如(5mV)^2再调Q的对角元素。Q的顺序量级是SOC对应的过程噪声很小我常用(0.1%)^2数量级V1、V2对应的噪声取决于参数误差可以设成(20mV)^2数量级。调参原则是Q越大滤波越相信测量但SOC会跟着电压噪声乱跳Q越小滤波越平滑但模型误差大时收敛慢。3.3 为什么UKF也能用EKF处理OCV这种非线性时需要实时计算dOCV/dSOC。如果OCV曲线分段线性有折点导数在折点处不连续EKF会在SOC进入折点附近时出现抖动。无迹卡尔曼滤波UKF不需要求导直接用一组sigma点做非线性传播对强非线性系统适应性更好。代价是UKF计算量大约是EKF的三到四倍。对于主频几十兆赫兹的普通MCU如果采样周期是100msUKF的压力确实更大。我的经验是OCV曲线如果做了高次多项式平滑EKF完全够用如果坚持分段线性表且SOC可能在斜率突变区间穿越UKF更稳。两者对SOC估计的最终精度差异通常不到0.5%更多是稳定性偏好问题。3.4 参数与SOC的耦合问题在线参数辨识和SOC估计在一个算法循环里同时跑会互相影响。SOC不准导致OCV不准OCV不准导致辨识参数出偏参数出偏又反过来污染SOC估计。这个正反馈回路在工程上是个隐形炸弹。破法有一个很实用的原则把辨识和估计放在不同时间尺度。SOC滤波每个采样周期都跑参数辨识只在电流较大且SOC变化不大时触发。此外对辨识结果做平滑比如用一阶低通避免瞬时突变直接进入滤波模型。我的做法是参数辨识的输出经过一个时间常数5分钟的一阶滤波器后才更新到SOC估计器的模型参数里。这样的系统表现非常稳。4. 实操过程完整的2RC ECM落地示例4.1 工具链与数据准备我用Python做整个算法验证原因就两个字方便。NumPy做矩阵运算SciPy做离线拟合Matplotlib画图检查。实际芯片移植用C语言重写但算法逻辑完全一致。为了演示我用一块标称3.7V、2Ah的磷酸铁锂软包电池这个只是示例你可以换成任何手头电池。实验数据由一个可编程电子负载和一台直流电源采集电流和电压采样频率1Hz离线数据对应一个简化的HPPC序列1000s内交替加载0.5C、1C、2C电流脉冲并穿插静置。演示里我先用这段数据离线辨识出一组参数作为初值再模拟在线辨识和SOC滤波。4.2 离线辨识代码从电压曲线提取R0和RC时间常数下面这段代码演示了HPPC放电脉冲静置段的指数拟合。核心是用curve_fit把极化电压V_pol拆成两个指数项。import numpy as np from scipy.optimize import curve_fit # t: 静置时间序列秒V_pol: 电压相对OCV的极化偏差 # V_pol OCV - Vt 注意放电撤电流后Vt逐渐上升V_pol逐渐减小 def two_exp(t, A1, tau1, A2, tau2): return A1 * np.exp(-t / tau1) A2 * np.exp(-t / tau2) # 示例数据实际从实验取 t np.linspace(0, 60, 500) V_pol 0.08 * np.exp(-t / 3) 0.05 * np.exp(-t / 35) 0.002 # 初值很关键tau1给几秒tau2给几十秒 p0 [0.1, 5, 0.1, 50] popt, pcov curve_fit(two_exp, t, V_pol, p0p0, maxfev5000) A1, tau1, A2, tau2 popt print(fA1{A1:.4f}, tau1{tau1:.3f}s, A2{A2:.4f}, tau2{tau2:.3f}s) # 若电流脉冲幅值为I_pulse I_pulse 2.0 R1 A1 / I_pulse C1 tau1 / R1 R2 A2 / I_pulse C2 tau2 / R2 print(fR1{R1:.4f}Ω, C1{C1:.1f}F, R2{R2:.4f}Ω, C2{C2:.1f}F)这段代码里最容易被忽略的是V_pol定义方向。放电端电压低于OCV所以V_pol OCV - Vt正值。拟合出的A1、A2也是正数对应撤流后极化电压从正衰减到零。如果你把V_pol算成负的拟合结果会出负电容乍一看参数也能用但物理上完全不对。4.3 FFRLS在线辨识核心代码离线初值到位后在线辨识用FFRLS更新差分方程系数。差分方程前面已经给出需要构造回归向量φ(k) [y(k-1), y(k-2), u(k), u(k-1), u(k-2)]特别注意y Vt - OCV。每次采样时用当前SOC估计值查OCV。import numpy as np class FFRLS: def __init__(self, lamb0.98, theta_dim5): self.lamb lamb self.theta np.zeros(theta_dim) # [a1, a2, b0, b1, b2] self.P np.eye(theta_dim) * 1000 # 初始协方差 self.history_u [0, 0, 0] self.history_y [0, 0, 0] def step(self, u, y): # y Vt - OCV, u I self.history_u [u] self.history_u[:2] self.history_y [y] self.history_y[:2] phi np.array([ self.history_y[1], self.history_y[2], self.history_u[0], self.history_u[1], self.history_u[2] ]) # 增益 Pphi self.P phi K Pphi / (self.lamb phi Pphi) # 更新 err y - phi self.theta self.theta self.theta K * err self.P (self.P - np.outer(K, phi) self.P) / self.lamb return self.theta.copy()有了差分方程系数后还需要反推2RC参数。这个映射我用一个函数实现先用双线性逆变换得到连续传递函数系数再解一元二次方程得到τ1、τ2最后解线性方程组得到R0、R1、R2。4.4 参数映射函数从差分方程到电路参数def bilinear_inverse(a1, a2, b0, b1, b2, Ts): # 离散Hz (b0b1 z^-1b2 z^-2)/(1a1 z^-1a2 z^-2) # 双线性变换 z(1s*Ts/2)/(1-s*Ts/2) # 这里直接数值构建多项式系数法避免手写长公式 from numpy.polynomial import polynomial as P # 分母离散系数 - 连续s多项式 # 具体实现见下文注释此处是结果 # 用sympy推导一次后固化的代码省略临时验证 a0_n 4 / Ts**2 2 * a1? # 实际需按标准公式 # 为了节省篇幅用scipy.signal中的连续系统转换代替手算 from scipy.signal import d2c, TransferFunction num [b0, b1, b2] den [1, a1, a2] sys_d TransferFunction(num, den, dtTs) sys_c d2c(sys_d, methodtustin) return sys_c实际写工程代码时我不会在每次递推里都调用scipy因为符号推导的计算量偏大。更常见的做法是把逆映射公式用代数推导好固化成一个静态函数。这里为了演示易懂直接用scipy.transfer函数把离散传递函数转回连续传递函数再从连续传递函数中提取电路参数。连续传递函数整理后是H(s) (p2s^2 p1s p0) / (s^2 q1*s q0)接着按前面推导的关系求解τ1、τ2是方程λ² - (q1/q0)λ 1/q0 0的两个根R0 -p2R1 R2 -p0/q0 - R0R1τ2 R2τ1 -p1τ1τ2 - R0*(τ1τ2)。联立就能算出R1、R2、C1、C2。我在实际代码流程中优先用FFRLS在线跑系数每20个采样点做一次参数映射更新然后低通滤波再送入SOC估计器。4.5 SOC估计的Python骨架SOC估计器我用一个EKF类封装核心部分如下class SOC_EKF: def __init__(self, R0, R1, C1, R2, C2, capacity, ocv_func, dt): self.R0 R0; self.R1 R1; self.C1 C1 self.R2 R2; self.C2 C2 self.cap capacity # Ah self.ocv_func ocv_func self.dt dt self.x np.array([0.5, 0, 0]) # SOC, V1, V2 self.P np.diag([0.01, 0.01, 0.01]) self.Q np.diag([1e-6, 4e-4, 4e-4]) self.R np.array([[5e-5]]) # (5mV)^2 def predict(self, current): dt self.dt; x self.x soc x[0] - current * dt / (self.cap * 3600) v1 x[1] * np.exp(-dt / (self.R1 * self.C1)) self.R1 * (1 - np.exp(-dt / (self.R1 * self.C1))) * current v2 x[2] * np.exp(-dt / (self.R2 * self.C2)) self.R2 * (1 - np.exp(-dt / (self.R2 * self.C2))) * current self.x np.array([soc, v1, v2]) F np.array([ [1, 0, 0], [0, np.exp(-dt / (self.R1 * self.C1)), 0], [0, 0, np.exp(-dt / (self.R2 * self.C2))], ]) self.P F self.P F.T self.Q def update(self, vt_measured): # 测量残差 soc self.x[0] ocv self.ocv_func(soc) vt_pred ocv - self.R0 * current_from_input - self.x[1] - self.x[2] # 求dOCV/dSOC dsoc 1e-3 docv_dsoc (self.ocv_func(soc dsoc) - self.ocv_func(soc - dsoc)) / (2 * dsoc) H np.array([[docv_dsoc, -1.0, -1.0]]) y vt_measured - vt_pred S H self.P H.T self.R K self.P H.T np.linalg.inv(S) self.x self.x K.flatten() * y self.P (np.eye(3) - K H) self.P注意代码里的current_from_input是外部变量实际应该在update时把电流作为参数传入。我在这里省略了部分参数传递细节是为了让核心计算公式更突出。真实工程里一定要把电流和电压同时传入否则SOC会在负载突变时出现瞬态扰动。5. 常见问题与排查技巧实录5.1 离线辨识出的R1、R2出现负值这个问题的原因绝大多数是OCV取错了。如果你在脉冲静置段取的基准电压不是真正的稳态OCV比如静置时间不足V_pol里还藏着一层未松驰的扩散电压拟合出的A1或A2就可能为负。另外如果电荷方向定义反了明显正值的极化会变成负。排查时先画V_pol随时间变化的曲线确认它是单调衰减且始终为正如果不是先把基准电压往上提。5.2 在线辨识参数抖动剧烈FFRLS对激励要求很高。你在匀速小电流放电时电压变化极小回归矩阵的信息量不够参数自然乱跳。我遇到这类问题时第一反应不是调遗忘因子而是检查激励条件。如果没有足够大的脉冲激励再小的遗忘因子也救不回来。工程里可以在参数更新前加一个门槛只有当电流持续大于某个阈值比如0.2C且电压变化超过几毫伏时才执行递推否则冻结参数。5.3 SOC估计在SOC两端发散或收敛慢前面提过OCV曲线在SOC10%和95%时非常平dOCV/dSOC趋近于零。这时EKF的增益K几乎为零SOC预测完全靠电流积分如果容量不准或库伦效率有偏差SOC就会慢慢飘。我的处理办法是在SOC两端启用电压截止辅助校正同时限制SOC估计值的更新步长。还有一招是增加温度修正因为低温下OCV平台会更平更容易发散。5.4 参数对温度变化不敏感2RC参数随温度变化非常明显低温下R0增大两个时间常数也会变长。如果你的BMS不做温度补偿冬天和夏天同一块电池辨识出的参数差出一倍都不奇怪。简单做法是把参数表做成二维查表横轴SOC纵轴温度每个格子存一组R0、R1、C1、R2、C2。在线辨识可以只做一个方向的微调比如在5℃到35℃区间用插值超出区间再启用在线更新这样兼顾精度和稳定性。5.5 一套实用的排查清单我把这几年踩坑的要点整理成一张表方便你遇到问题时对照。现象可能原因排查方向电压残差恒定正偏R0偏大或OCV偏低检查脉冲阶跃段的电压变化量和电流同步性电压残差长时间缓慢衰减RC时间常数不匹配检查静置段拟合窗口是否足够长辨识参数持续漂移容量Q未更新定期用满充满放或安时积分结合OCV校准容量SOC预测值抖动Q矩阵过大或测量噪声方差过小重新调谐EKF噪声参数SOC收敛极慢dOCV/dSOC很小增益低增加OCV斜率下限约束或启用状态耦合校正低温整体偏差参数未温度补偿建立温度-参数插值表还有一个我一直强调的细节采样频率。2RC模型的快RC时间常数如果只有几秒采样频率至少要10Hz以上才能捕捉到它。如果BMS的AD采样只有1Hz快极化的信息会被严重混叠在线辨识几乎不可能得到可信的快RC参数。我的经验是对于车规级BMS采样频率至少5Hz最好10Hz对于低速两轮车1Hz勉强可用但动态工况误差会大不少。做这一整套流程我最大的体会是参数辨识和SOC估计不是两个孤立模块而是一个闭环。模型再好参数不准滤波结果也不会好滤波再强OCV曲线不准也是白搭。你在搭建自己的系统时不要急着把算法调到最复杂先把离线辨识、静态OCV、动态工况验证这三步走扎实再上在线辨识和自适应滤波。后面每次踩坑都会比前面更好定位。最后分享一个小技巧在调EKF时不要只盯着SOC最终曲线多画几个中间变量图特别是端电压预测残差和卡尔曼增益。电压残差是模型缺陷最直接的镜子增益是滤波健康的指标。我每次调试新电池都会先看这两个信号再决定改参数还是改模型。这套方法论适用于任何电池材料体系和任何BMS平台。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑