ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

二阶RC等效电路模型参数辨识与BMS嵌入式落地全流程

二阶RC等效电路模型参数辨识与BMS嵌入式落地全流程

做电池管理算法的人,大概率都在某个阶段被同一件事卡住:手上有一份电流电压数据,也知道要建个等效电路模型,但真到参数辨识那一步,拟合出来的R和C要么是负数,要么一组参数在25度能用、换个温度就崩,仿真跑起来端电压越漂越远。二阶RC等效电路模型是个说起来简单、做起来细节极多的东西,它的门槛不在公式,而在测试怎么做、数据怎么截、初值怎么给、离散化怎么选、查表怎么插这些琐碎环节上。这篇就把这套流程从测试台架一路讲到嵌入式代码,把我自己踩过的坑和参数自检的方法都摊开说,适合刚接手BMS算法、准备把模型落地到仿真或控制器里的朋友,也适合做了几年但精度一直上不去的同行对照排查。

1. 为什么我选二阶RC:方案选型与整体思路

电池建模这条路有很多岔口,选错了模型,后面所有工作都是白费力气。我见过不少项目一上来就追求"高精度",直接上电化学模型或者神经网络,结果标定周期拖了半年,最后连个稳定的在线估计都跑不起来。选型的核心不是谁更准,而是"在当前算力、当前测试条件、当前精度要求下,哪个模型的性价比最高"。二阶RC等效电路模型之所以在车载和储能BMS里成为主流,本质上是它在精度、算力和可解释性之间找到了一个很好的平衡点。

1.1 三类模型摆在一起比一比

电化学模型走的是第一性原理路线,把正负极颗粒内部的锂离子浓度分布、固相扩散、Butler-Volmer动力学都写成偏微分方程来解。它的好处是能预测内部状态,比如负极析锂倾向、电解液浓度分布,做安全预警很有价值。但代价是参数多、方程刚性大,实时求解基本不现实,通常只能在离线仿真或者云端做数字孪生时用。而且它的参数,比如固相扩散系数、反应速率常数,普通实验室根本测不准,往往要靠文献值凑。

数据驱动模型是另一条路,把电池当成黑箱,用大量工况数据训练网络或者做高斯过程回归。它在训练集覆盖的范围内精度可以很高,但外推能力差,温度、老化状态一变就要重新采数据重训,而且中间过程完全不可解释。BMS这种要做功能安全认证的场景,很难接受一个说不出内部状态的模型。

等效电路模型居中。它用电学元件去等效电池内部的物理过程,参数有明确物理含义,可以随SOC和温度做表,在线计算量只有几个一阶差分方程,单片机上跑毫无压力。它的短板是精度上限受限于拓扑结构,且参数依赖测试标定。下面这张表是我自己在选型时整理的对比,供参考:

维度电化学模型等效电路模型数据驱动模型
在线算力需求极高,通常离线极低,微秒级中等,需推理引擎
参数可测性差,多依赖文献好,HPPC即可标定无需物理参数
可解释性强中,但有物理对应弱
外推能力强中差
温度/老化适配需改参数查表即可需重训
工程落地成熟度低高中

看这张表就能明白,如果你的目标是车载BMS或者储能柜的实时SOC/SOP估计,等效电路几乎是唯一务实的选择。二阶RC又是其中用得最广的一档。

1.2 二阶RC里多出来的那一阶到底在干什么

一阶RC模型只有一个RC并联环节,意味着它只能描述一个时间尺度的极化过程。但锂离子电池在电流激励撤掉之后,电压恢复是一条明显的"快回落加慢爬升"的双段曲线。前半段是秒级的,来自双电层充放电和电荷转移阻抗;后半段能拖几分钟甚至更久,来自电极颗粒内部的固相扩散。你用一个时间常数去拟合两个过程,结果一定是顾此失彼——要么快的那段跟不上,要么慢的那段收不住,端电压在工况切换点必然出现明显偏差。

二阶RC用两个RC环节分别对应这两个时间尺度,典型取值上,第一个环节的时间常数在几秒到几十秒,第二个环节在几百秒到上千秒。这两个数差一个数量级以上,物理上才说得通。三阶及以上虽然理论上拟合更细,但实际做下来我是不推荐的:一是参数之间的相关性太强,辨识出来经常出现两组完全不同的参数给出几乎一样的拟合曲线,参数不唯一;二是高阶环节的时间常数往往落进噪声频段,辨识结果抖得厉害;三是嵌入式里多一阶就多一组状态和一次查表,收益和成本完全不成比例。二阶是那种"再加就没必要,再减就不够用"的位置。

1.3 全流程拆成四段:测试、辨识、验证、落地

我把整个工作拆成四段,每一段都有明确的输入输出,这样出了问题能快速定位是哪一段的锅。

第一段是测试与数据准备,输出是干净的、带时间戳对齐的电流电压序列,以及各SOC点下的OCV。这一段做不好,后面全是垃圾进垃圾出。第二段是参数辨识,输出是R0、R1、C1、R2、C2随SOC(以及温度)变化的表。第三段是模型验证,用没参与辨识的动态工况数据跑一遍,输出端电压误差指标。第四段是仿真与嵌入式落地,把连续方程离散化、定点化,塞进目标平台。

很多人栽在顺序上——先写代码再想数据,或者拿辨识用的同一段数据去做验证。这两件事都是自欺欺人。辨识和验证必须用不同的数据集,就跟训练集和测试集不能混一样。

2. 模型结构与数学表达:把电路写成代码的前提

电路图谁都会画,但从电路图到能跑的代码中间隔着一层符号约定和离散化选择。这一层如果约定不清楚,后面调试会非常痛苦,尤其是电流方向的正负号,几乎每个人都在这上面栽过一次。

2.1 电路拓扑与端电压方程

二阶RC的拓扑是:一个理想电压源OCV串联欧姆内阻R0,再串联两个RC并联环节。端电压等于OCV减去各部分的压降。这里必须先定死一个符号约定,我习惯用放电电流为正,因为BMS里放电是主要工况,数据看起来顺眼。按这个约定:

U(t) = OCV(SOC) - I(t)·R0 - U1(t) - U2(t) dU1/dt = -U1(t)/(R1·C1) + I(t)/C1 dU2/dt = -U2(t)/(R2·C2) + I(t)/C2 dSOC/dt = -η·I(t)/(3600·Q)

其中U1、U2是两个RC环节上的极化电压,Q是额定容量(Ah),η是库仑效率(放电一般取1,充电取0.98~0.99)。四个式子合起来就是模型的状态空间描述,三个状态量分别是SOC、U1、U2。

这里有个容易忽略的细节:OCV是SOC的函数,而SOC是通过电流积分得到的。如果仿真步长较大,SOC在每个步长内的变化会让OCV取值产生偏差。我的做法是在每个步长开始时先更新SOC,再用新的SOC去查OCV,这叫"先更新状态再算输出",能减少一点滞后误差。

另外要提醒的是,R0这个参数在真实电池里并不是一个纯电阻,它包含了极耳、集流体、电解液、接触电阻等一堆东西的集合效应,而且随温度和SOC变化明显。低温下R0能涨到常温的三到五倍,这是冬天电动车续航打折、动力变弱的重要原因之一。所以R0绝对不能拍一个常数了事,必须做表。

2.2 连续到离散:三种离散化方式怎么选

仿真和嵌入式都是离散时间系统,连续方程必须转成差分。常见的三种做法差别不小。

前向欧拉是最直观的,U1(k+1) = U1(k) + Δt·(-U1(k)/τ1 + I(k)/C1)。它的优点是形式简单、计算量小。缺点是有稳定性条件,当Δt/τ1大于2时会发散。对于τ1取10s、Δt取1s的情况,Δt/τ1=0.1,完全稳定。但如果某个SOC点辨识出的τ1只有0.5s,而你的步长是1s,前向欧拉立刻振荡。这是很多人在低SOC段仿真发散的真正原因。

后向欧拉无条件稳定,但精度是一阶的,且形式稍复杂。真正推荐的是零阶保持精确离散化,也叫ZOH离散。对一阶RC环节,设τ=RC,a=exp(-Δt/τ),则:

U(k+1) = a·U(k) + R·(1-a)·I(k)

这个式子的好处是:无论Δt和τ是什么比例,都不会发散;而且当电流恒定时,它给出的是精确解而不是近似解。代价是要算一次指数,但在初始化阶段把每个SOC点的a预计算好存成表,在线就只剩乘加运算了。我后来的项目全部切到这种离散方式,仿真稳定性问题再没出现过。

离散方式稳定性精度在线计算量适用场景
前向欧拉条件稳定,Δt/τ<2一阶最小步长远小于τ
后向欧拉无条件稳定一阶小步长接近τ
ZOH精确离散无条件稳定精确小(系数预计算)通用,推荐

2.3 OCV-SOC曲线:整个模型的地基

如果只让我保留模型里的一个要素,我会选OCV-SOC曲线。因为它直接决定了SOC估计的绝对基准,曲线错了,后面所有极化参数的拟合都是在错误的基线上做补偿,参数会变得毫无物理意义——你会看到R1在某个SOC点突然变得巨大,其实那只是因为那个点的OCV查错了,拟合算法在用极化压降去硬凑那部分偏差。

获取OCV曲线有三条路。第一条是小电流充放电法,用C/20甚至C/30的倍率慢充慢放,把测得的端电压近似当作OCV。它的优点是快,缺点是残余极化仍然存在,而且充和放两条曲线不重合,形成迟滞环。第二条是间歇静置法,每走2%~5%的SOC就静置一到两小时,等电压变化率小于1mV/min再记录。这个最准,但耗时极长,一个完整的充放循环要做好几天。第三条是GITT,分段脉冲加静置,理论上能同时得到OCV和动态参数,但数据处理麻烦。

工程上我一般用第一种加校正:小电流充放各做一遍,取两条曲线的平均作为OCV,然后把迟滞的一半作为模型不确定度记下来。磷酸铁锂的迟滞特别明显,中段平台区能达到几十毫伏,这种情况下用平均曲线在充放电切换时会出现明显误差,需要额外的迟滞状态量来补偿,这个后面单独说。

曲线取点也有讲究。三元电芯的OCV曲线比较陡,10%一个点线性插值误差就能控制在10mV以内;磷酸铁锂中段几乎是平的,电压随SOC变化只有几毫伏,这时候取点必须密,2%一个点都不算多。我吃过这个亏——早期按5%取点做LFP,结果平台区SOC估计误差能到8%以上。

2.4 参数随SOC与温度变化:二维查表怎么设计

R0、R1、C1、R2、C2这五个参数,每一个都是SOC和温度的函数。工程上不可能建解析式,只能做表加插值。

SOC维度我通常取0、0.05、0.1……1.0共21个点,低SOC段(0~0.1)和高SOC段(0.9~1.0)适当加密到0.02。温度维度取-20、-10、0、10、25、40、55摄氏度七个点,覆盖车用和储能的常见范围。这个表的规模是5个参数×21×7,大约735个数,全部存flash也就几KB,完全不是负担。

插值方式上,我强烈建议双线性插值,而不是最近邻。最近邻在表点之间会造成参数阶跃,端电压曲线出现肉眼可见的折角,动态工况下误差会被放大。双线性插值多几次乘加,代价可以忽略。外插要关掉——超出表的温度范围时钳位到边界值,否则插值出来的R0可能是负数,仿真直接崩。

注意:温度维度不要用电池表面温度直接查表。表面温度和内部温度在大倍率下能差5到10度,而R0对这个差值非常敏感。有条件的话用表面温度加一阶热模型估算内部温度,没条件至少加一个随电流大小变化的偏置。

3. 数据从哪来:HPPC测试与数据预处理

参数辨识的质量,八成取决于数据质量。我见过有人拿一段随意的行车数据去辨识,拟合优度看着还行,但参数完全没有规律,换一段数据就是另一套值。规范的HPPC(混合脉冲功率特性)测试是绕不开的。

3.1 HPPC激励序列的设计要点

标准做法是:把电池充满,静置足够长时间,然后放电10%的SOC,静置,再施加一个放电脉冲,静置,再施加一个充电脉冲,静置,进入下一个SOC点。脉冲的幅值和时长决定了你能辨识出什么。

脉冲幅值上,我一般用1C做放电脉冲、0.75C做充电脉冲。为什么不对称?因为充电时电池更容易触发上限保护,而且高SOC点大倍率充电有安全风险。幅值太小(比如0.2C)会导致极化压降只有几毫伏,淹没在采样噪声里,辨识出来的参数信噪比很差。

脉冲时长上,放电脉冲取10s到30s,充电脉冲取10s。这个时长要足够让第一个RC环节基本充满(3倍τ1以上),同时又要远小于第二个RC环节的时间常数,这样两个环节在响应曲线上才能拉得开。如果脉冲只有2s,第一个环节还没充满就撤了,R1和C1会严重耦合,辨识出来一堆病态解。

静置时长是新手最容易缩水的地方。我见过只静置60s就开始下一个脉冲的,结果第二个RC环节根本没恢复完,极化电压累积,后面的OCV基准全错。正确的做法是静置到电压变化率小于1mV/min,实际工况下通常需要40分钟到一个半小时,低温和低SOC点更久。测试时间成本很高,但这段省不得。

采样率方面,用来做辨识的数据建议不低于10Hz,也就是0.1s一个点。要想准确抓到欧姆压降,理想情况是100Hz以上,因为R0引起的跳变发生在一瞬间,10Hz采样下跳变往往在一两个点内完成,R0只能靠外推估。我的折中方案是:整体用10Hz记录,在脉冲跳变的头1秒内切到100Hz。有些测试设备支持变速率记录,不支持的话就整体上一个更高的采样率,事后降采样。

3.2 采样对齐、零点校正与异常剔除

电流和电压如果不是同一时刻采的,或者两个通道有相位差,辨识出来的R0会系统性偏大或偏小。测试前一定要确认设备的多通道是同步采样的,不是的话要做时间对齐——方法是找一个电流阶跃点,看电压跳变的起始时刻,把两条曲线按这个点对齐。

零点漂移是另一个隐形杀手。电流传感器在零电流时读数不是严格的0,可能有个几十毫安的偏置。这个偏置在长时间积分里会累积成巨大的SOC误差,在静置段则会让"零电流"其实变成微小充放电,影响OCV判定。做法很简单:测试前后各做一段零电流记录,取平均作为偏置扣掉。

异常点用3σ准则剔除就够,但要注意别把真实的脉冲跳变当成异常值剔了。我的做法是先按电流幅值分段,只在静置段和恒流段内部做异常检测,跳变点附近三个点以内不动。

3.3 脉冲段自动截取与静置终点判定

手工切数据效率太低,而且容易切歪。我写了个自动分段的逻辑,思路是:先算电流的差分,绝对值超过某个阈值(比如0.05C)的点标记为跳变点,相邻跳变点之间就是一个阶段。然后根据电流的符号和幅值给每个阶段打标签:零电流是静置,正电流是放电,负电流是充电。

静置终点判定用电压变化率。对每个静置段,从后往前扫,找到第一个满足|dU/dt| < 1mV/min的位置作为有效终点。注意平滑一下再算导数,原始数据噪声大,直接差分会让判据失效。用滑动窗口做线性回归取斜率,窗口长度取60秒比较合适。

这里还有一个坑:静置初期电压恢复很快,如果你的数据在脉冲结束的瞬间有采样延迟或者滤波滞后,曲线起点会失真。我的处理是把脉冲结束后的前两秒数据丢弃不用,从第三秒开始参与拟合。丢掉的这两秒对慢环节影响很小,但能避开传感器的动态响应。

4. 参数辨识实操:从脉冲响应里把R和C抠出来

到这一步,数据干净了,接下来是把五个参数从曲线里挖出来。这里我用两套方法:离线用非线性最小二乘,精度高、可以加约束;在线用递推最小二乘,用于参数自适应。两套方法各有各的坑。

4.1 欧姆内阻R0:跳变点从哪里取

R0的定义是电流阶跃瞬间的电压跳变量除以电流阶跃量。听上去一步除法就完事,实际操作里全是细节。

第一个问题是采样率不够导致的跳变被"抹平"。真实跳变是阶跃,采样后变成斜坡,你直接拿第一个采样点和最后一个采样点相减,得到的R0会偏小。正确做法是取跳变前最后一个点和跳变后第一个稳定点(通常在跳变后50到200ms)做线性外推,外推回跳变时刻。我一般用跳变前后各5个点做线性回归,取t=0+处的值。

R0 = (U_before_extrapolated - U_after_extrapolated) / ΔI

第二个问题是R0里混进了线束和接触电阻。测试台架的线缆、夹具、极耳接触都会贡献毫欧级电阻,而且这部分不随SOC和温度按同样规律变化。如果直接用台架数据标定,装车后会发现模型预测的压降比实际偏大。缓解办法是:测试时用四线法把电压采样点尽量靠近电芯极柱;或者在整包层面标定时,把线束电阻当作一个单独的常数项,不并入R0。

第三个问题是充电和放电的R0不对称。同一SOC点,用放电脉冲辨识的R0通常比充电脉冲略小,这是因为电荷转移过程在两个方向上动力学不对称。我的处理是取两者的平均值作为模型参数,同时把差异记录下来——如果差异超过20%,说明电芯状态可能有问题,值得复查。

4.2 双RC参数的离线拟合

脉冲撤掉之后的弛豫过程是最有信息量的。以放电脉冲为例,脉冲期间两个RC环节被充电,脉冲结束后电流为零,两个环节各自按指数衰减。此时端电压为:

U(t) = OCV - U1(0)·exp(-t/τ1) - U2(0)·exp(-t/τ2)

这是一个双指数衰减加一个直流偏置的形式,四个待定系数:OCV、U1(0)、τ1、U2(0)、τ2。用非线性最小二乘拟合这条曲线,就能把两个时间常数和初始极化电压都拿到。

初值给得好不好,直接决定拟合是收敛到物理合理的解还是发散到天文数字。我的初值策略是这样的:

  • τ1初值取脉冲时长的三分之一,比如10s脉冲就取3s左右
  • τ2初值取静置段总时长的三分之一,比如静置3000s就取1000s
  • U1(0)初值取脉冲结束瞬间电压跳变量的一半
  • U2(0)初值取脉冲结束瞬间电压跳变量的另一半
  • OCV初值取静置段最后一个点的电压

拟合完成后,反算R和C:

  • R1 = U1(0) / I_pulse(前提是脉冲时长超过3τ1,此时第一个环节基本充满)
  • C1 = τ1 / R1
  • 对第二个环节,脉冲时长远小于τ2,它基本是线性充电,所以 U2(0) ≈ I_pulse·t_pulse/C2,即 C2 = I_pulse·t_pulse/U2(0),R2 = τ2/C2

下面是我常用的一段拟合代码,用Python写,思路可以直接搬到MATLAB:

import numpy as np from scipy.optimize import curve_fit def relax_model(t, ocv, u1_0, tau1, u2_0, tau2): return ocv - u1_0 * np.exp(-t / tau1) - u2_0 * np.exp(-t / tau2) def identify_rc(t, u, i_pulse, t_pulse, dt_total): # t: 静置段相对时间, u: 端电压, i_pulse: 脉冲电流(A), t_pulse: 脉冲时长(s) p0 = [u[-1], 0.5 * (u[-1] - u[0]), t_pulse / 3.0, 0.5 * (u[-1] - u[0]), dt_total / 3.0] bounds = ([u[-1] - 1.0, 0.0, 0.5, 0.0, 50.0], [u[-1] + 1.0, 2.0, 200.0, 2.0, 20000.0]) popt, _ = curve_fit(relax_model, t, u, p0=p0, bounds=bounds, maxfev=20000) ocv, u1_0, tau1, u2_0, tau2 = popt r1 = u1_0 / i_pulse c1 = tau1 / r1 c2 = i_pulse * t_pulse / max(u2_0, 1e-6) r2 = tau2 / c2 return dict(ocv=ocv, r1=r1, c1=c1, r2=r2, c2=c2, tau1=tau1, tau2=tau2)

这段代码里加了边界约束,这是关键。不加约束的话,τ1和τ2互换、或者τ2跑到无穷大,都会出现,而且拟合残差可能一样小。约束的上下限根据电芯类型设,三元电芯τ1一般在2到30秒,τ2在200到5000秒,磷酸铁锂的τ2往往更长一些。

4.3 辨识结果的自检清单

拟合残差小不代表参数可信。我每次辨识完都会过一遍下面这张清单,任何一条不过就回去查数据或者查初值。

检查项合理范围不过关的常见原因
R0 > 0常温1~5mΩ(按容量折算)采样不同步、跳变点取错
R1、R2 > 0均小于R0的10倍初值差、边界没约束
τ1 < τ2至少相差5倍两个环节被强行对调
τ1范围2~30s脉冲时长太短
τ2范围200~5000s静置时间不够
参数随SOC平滑相邻点变化<30%某个SOC点的OCV基准错了
充放电差异<20%迟滞未补偿、数据噪声

我印象最深的一次排查:某批数据在SOC=0.2附近R1突然翻了三倍,曲线拟合得还挺好。查了半天发现那个SOC点的静置时间只有20分钟,第二个环节没恢复完,OCV被高估了大概15mV。拟合算法为了凑这15mV,硬是把R1放大了。把这段数据扔掉重新测,参数立刻恢复正常。所以参数的平滑性检查比拟合优度更可靠,R²到0.99但参数跳变,那一定是数据有问题。

4.4 在线辨识:带遗忘因子的递推最小二乘

离线参数表适合稳态工况,但电池是会老化的,内阻随循环次数增加,只靠出厂标定,用两年误差就大了。在线辨识能跟上这种慢变化,代价是计算量和鲁棒性风险。

做法是把模型写成ARX形式。对二阶RC,端电压的差分方程可以整理成:

U(k) = θ1·U(k-1) + θ2·U(k-2) + θ3·I(k) + θ4·I(k-1) + θ5·I(k-2) + θ6

其中θ6吸收了(1-θ1-θ2)·OCV这一项。用带遗忘因子的RLS在线更新这五个系数,遗忘因子λ取0.995到0.998,对应的时间常数大约在几百到一千个采样点,也就是十几分钟到半小时,正好匹配内阻变化的速率。λ太小会导致参数被瞬时工况带跑,λ太大会跟不上老化。

class RLS: def __init__(self, n, lam=0.997, p0=1e4): self.theta = np.zeros(n) self.P = np.eye(n) * p0 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

拿到五个系数之后反解物理参数,关系是这样的(令a1=θ1,a2=θ2):

τ1·τ2 = Δt²/(1 + a1 + a2) τ1 + τ2 = Δt·(1 - a2)/(1 + a1 + a2) R0 = -θ3 R1 + R2 = (θ3 - θ4 - ... ) 的反解形式

说实话,反解公式推起来不难,但数值上很容易出问题,尤其是当a1+a2接近-1时,分母趋零,参数爆炸。工程上的做法是:不要每次都反解,只在线更新R0(因为它和θ3直接对应,最稳定),其他参数仍然用离线表。这样既跟上了老化,又避免了参数发散。我后来所有的在线方案都这么简化,效果反而比全参数更新更稳。

5. 仿真落地:从脚本到可跑的模型

参数表建好之后,要把它变成能跑的东西。仿真验证和嵌入式移植是两个不同的战场,前者关注精度和调试便利,后者关注实时性和数值稳定。

5.1 Python参考实现的骨架

做算法验证我习惯先用Python写一版,因为改起来快。核心就是一个类,内部维护SOC、U1、U2三个状态,输入是电流序列和时间步长,输出是端电压。

import numpy as np class ECM2RC: def __init__(self, soc_axis, temp_axis, param_table, ocv_table, capacity): # param_table: dict, key in ['R0','R1','C1','R2','C2'], shape (n_temp, n_soc) self.soc_axis = soc_axis self.temp_axis = temp_axis self.pt = param_table self.ocv_table = ocv_table # shape (n_temp, n_soc) 或 (n_soc,) self.q = capacity self.soc = 1.0 self.u1 = 0.0 self.u2 = 0.0 def _interp2d(self, grid, soc, temp): # 先按温度插值,再按SOC插值 row = np.array([np.interp(temp, self.temp_axis, grid[:, j]) for j in range(len(self.soc_axis))]) return np.interp(soc, self.soc_axis, row) def step(self, current, temp, dt): # 放电为正 r0 = self._interp2d(self.pt['R0'], self.soc, temp) r1 = self._interp2d(self.pt['R1'], self.soc, temp) c1 = self._interp2d(self.pt['C1'], self.soc, temp) r2 = self._interp2d(self.pt['R2'], self.soc, temp) c2 = self._interp2d(self.pt['C2'], self.soc, temp) # 先更新SOC,再用新SOC查OCV self.soc = np.clip(self.soc - current * dt / (3600 * self.q), 0.0, 1.0) ocv = self._interp2d(self.ocv_table, self.soc, temp) # ZOH离散 a1 = np.exp(-dt / (r1 * c1)) a2 = np.exp(-dt / (r2 * c2)) self.u1 = a1 * self.u1 + r1 * (1 - a1) * current self.u2 = a2 * self.u2 + r2 * (1 - a2) * current return ocv - current * r0 - self.u1 - self.u2

这段代码有两个地方值得说。一是先更新SOC再查OCV,减少一个步长的滞后;二是用ZOH离散,避免了步长和τ不匹配时的振荡。实测下来,1秒步长下这段代码跑WLTC工况,端电压RMSE能控制在15mV以内(常温、新鲜电芯)。

5.2 Simulink建模的坑

Simulink做仿真验证很方便,但有几个坑我踩过不止一次。

第一是代数环。如果你的模型是电压输出接回电流输入形成闭环(比如做恒功率放电),端电压的计算依赖电流、电流的计算又依赖端电压,Simulink会报代数环错误。解决办法是在端电压输出后加一个单位延迟,或者用离散求解器并把状态更新和输出计算拆开。我一般直接加延迟,一个步长的延迟对结果影响可以忽略。

第二是求解器设置。模型里全是离散环节的话,一定要用固定步长求解器,步长和BMS实际采样周期一致(一般是0.1s或1s)。用变步长的话,查表插值会产生非光滑,求解器会疯狂缩步长,仿真慢得让人想砸键盘。

第三是查表模块的外插设置。Lookup Table模块默认可能是线性外插,超出表范围时给出荒谬的值。一定要设成Clip,并且把表的两端做平延拓——也就是在SOC=0和1处各多加一个重复点,防止在边界附近插值出负的R0。

5.3 嵌入式移植:定点化的取舍

移植到MCU上,最大的约束是浮点性能。有FPU的芯片还好,没有的话双精度是灾难,单精度勉强,定点最稳。

我的做法是先评估:如果MCU有单精度FPU,直接用float就行,二阶RC的计算量大概是每步十几次乘加加两次exp,cortex-M4F跑1kHz毫无压力。如果没有FPU,就把exp预计算成表——反正a1、a2只跟τ和步长有关,而τ是从参数表里插值出来的,可以在初始化时把每个SOC×温度格点的a1、a2都算好存起来,在线只做插值。

定点化的话,SOC用Q15(0到1映射到0到32767),电压用Q10或者毫伏整数,电流用0.1A为单位的int16。要特别注意R1·(1-a1)·I这个乘法,中间结果的位宽容易溢出,建议用int32或者int64做中间累加。

还有一个容易被忽略的点:状态初值。车辆上电时,U1和U2应该是多少?如果直接置零,第一个工况循环的端电压会明显偏移,因为真实的极化电压在停车前可能还没完全消散。我的做法是上电后测量开路电压,查表反推SOC,然后根据停车时长做一个指数衰减估计初值——停车超过两小时基本可以置零,短时间停车则保留上次下电时的状态存到NVM。

6. 精度验证:别拿辨识数据自证清白

模型建好之后,一定要用独立数据验证。用辨识同一段数据跑出来的误差是假的,那个数字只能说明拟合算法收敛了。

6.1 动态工况的选择与构造

验证工况要能覆盖实际使用场景的频谱。标准工况里,DST偏城市低速,FUDS是动态应力测试,US06是高速激进驾驶,WLTC是综合。做储能的可以用调频调峰的功率曲线。我的习惯是至少选三个不同特性的工况,外加一段自建的随机电流序列。

随机序列的构造有个技巧:不要用白噪声。真实的电流是带自相关性的,用一阶低通滤波后的白噪声更接近真实,截止频率设在0.01到0.1Hz之间比较合适。这样能测出模型在长期持续充放电下的漂移特性。

验证时的初始条件也要注意。仿真开始时把SOC设成和实测一致,U1、U2设成零,然后跑。如果数据开头正好是静置段,这个设置是合理的;如果数据开头就在放电中,前几十秒的误差会偏大,这段时间应该排除在统计之外。

6.2 误差指标的分解方式

只看一个总RMSE没有诊断价值。我一般按下面几个维度拆开看:

按SOC区间分段统计。低SOC(0~0.2)和高SOC(0.8~1.0)通常是误差最大的地方,因为那里OCV曲线斜率大,SOC的微小偏差会放大成电压误差,同时参数表的边界插值也更容易出问题。中间段如果误差也大,那多半是极化参数的事。

按工况阶段分段统计。恒流段的误差主要反映R0的准确性,脉冲跳变点的误差反映R1C1,长静置段的尾部误差反映R2C2。哪一段误差大,就回去查对应参数。

按电流方向分段。充电和放电误差不对称,说明迟滞或者充放电参数不对称没处理好。磷酸铁锂这个问题特别突出,我的经验是,如果充放电误差差异超过20mV,就要考虑加迟滞状态量了。

一个健康的模型,常温下动态工况的端电压RMSE应该在10到20mV,最大误差不超过50mV。低温下会差一些,-10度时RMSE到30mV也算正常。如果常温下RMSE超过30mV,肯定是哪里有问题,别急着调参数,先查数据和表。

6.3 温度维度与老化维度的验证

温度验证最好用温箱做,把电池在不同温度下各跑一遍HPPC和一个动态工况,然后看模型在温度维度的插值是否合理。这里最常见的错误是温度表点太稀——只做了0度和25度,然后线性插值出10度的参数,结果在10度实测偏差很大。因为R0随温度不是线性变化的,低温区变化剧烈,高温区趋于平缓,接近指数或Arrhenius关系。我的建议是低温段(-20到10度)每10度一个点,高温段每15到20度一个点。

老化验证最麻烦,因为要真的循环几百次。替代方案是用不同健康状态的旧电芯做测试,或者用加速老化。老化对模型的影响体现在三方面:R0和R1增大(通常增加30%到100%)、可用容量下降导致SOC-OCV曲线横轴缩放、OCV曲线本身也有轻微形变。如果模型要覆盖全生命周期,参数表得做成随健康状态变化的二维甚至三维。工程上的简化是:出厂标定一张表,然后在线用RLS持续修正R0,其他参数按R0的增量等比例修正,虽然不是严格准确,但比不修好得多。

7. 常见问题速查与踩坑记录

这一节把我和同行们踩过的坑集中列出来,方便对号入座。大部分模型精度问题都不是理论问题,而是这些细节。

7.1 参数辨识类问题

症状一:辨识出的R是负数。九成是初值给反了,或者脉冲电流的符号搞混了。先检查电流约定,放电为正的约定下,放电脉冲期间端电压应该下降。如果你看到电压上升,说明符号反了,把电流乘-1再拟合一次。少数情况是边界没加约束,优化器跑飞了。

症状二:τ1和τ2几乎相等。这是两个环节耦合的典型表现。原因通常是脉冲时长太短或者静置段太短,两个时间尺度在数据里没分开。改测试方案,别在算法上死磕。我试过用各种正则化去强行分离,最后发现还是重测一次最省事。

症状三:某个SOC点参数剧烈跳变。先查这个点的OCV。方法很简单:把这个点的OCV手动加或减10mV,看参数是不是就平滑了。如果是,说明OCV基准有问题,回去检查那个SOC点的静置是否足够。如果不是,查采样数据里有没有异常点或者丢包。

症状四:充电和放电辨识结果差很多。三元电芯差异在10%以内算正常,磷酸铁锂差30%都见过。这是迟滞的物理表现,不是错误。处理方式是各存一张表,按电流方向查,或者取平均加迟滞补偿项。

7.2 仿真与移植类问题

症状五:仿真端电压越跑越偏,最后SOC到0还在放电。这是SOC积分没有钳位,或者容量参数填错了。检查Q的单位,是Ah还是mAh,是额定还是实际可用。另外SOC的更新要在每个步长内完成,别在循环外面更新。

症状六:低SOC段仿真发散或振荡。两个原因:一是用了前向欧拉且那个点的τ1很小,二是参数表在低SOC外插出了负值。换成ZOH离散,查表设成钳位,基本能解决。

症状七:嵌入式上跑出来和仿真不一致。按这个顺序查:先看采样周期是不是真的和你以为的一样(用示波器打点,别信配置);再看定点化的位宽有没有溢出,尤其是R1·I这种乘积;最后看参数表的存储顺序,行优先列优先搞反了,参数全错但代码不报错,非常难查。

症状八:模型在恒流段准,在脉冲跳变处偏差大。这是R0的问题。要么采样率不够导致R0被低估,要么线束电阻被算进了R0导致偏大。拿一段小电流阶跃数据单独校一下R0,别混在整体里一起调。

7.3 问题速查表

现象最可能原因排查动作
R为负值电流符号反、初值错检查约定,加边界约束
两个τ接近脉冲或静置太短重测,脉冲≥10s、静置≥40min
参数跳变某点OCV基准错手动偏置OCV复测平滑性
电压漂移SOC积分未钳位、容量错查Q单位,加clip
低频振荡前向欧拉+小τ换ZOH离散
嵌入式与仿真不符定点溢出、表序错逐项对比中间变量
跳变处误差大R0不准单独用小阶跃校R0
低温误差大温度表点太稀低温段加密到10度

最后分享一个我自己用得很顺手的调试技巧:把模型预测的U1和U2单独存出来画图,而不是只看总端电压。因为端电压误差是三个分量误差的叠加,混在一起看不出是谁的问题。U1的波形应该和电流的快速变化同步,U2应该是缓慢的包络。如果U1看起来像U2,那多半是两个时间常数搞反了或者参数表插错了行。这个习惯帮我省了大量排查时间,比盯着总电压曲线瞎猜有效得多。

另外,参数表建完之后别急着封版。留一个接口,允许手动覆盖任意SOC点的任意参数,方便现场遇到异常时快速验证假设。我现在的做法是所有参数走一个结构体,默认从flash加载,调试模式下可以通过串口在线改写。这个小设计在后期调车的时候救过好几次急。

返回列表