拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Simulink的电池等效电路模型搭建与参数辨识实战指南

基于Simulink的电池等效电路模型搭建与参数辨识实战指南

我一开始搭电池仿真模型的时候,纯粹是被BMS算法逼的——总不能每次验证SOC估算都去拆真电池、做满充满放的破坏性实验。后来用Matlab/Simulink从头搭了一套电池等效电路模型,从单体到电池组,从常温到变温,踩过的坑比写过的代码还多。这篇文章就把我这段搭建之旅的完整思路、关键步骤和排查心得写出来,给正在搞电池建模、BMS仿真或者毕业设计的朋友做个参考。你不需要刚开始就能搭出商用电芯级精度的模型,但跟着这条路线走,至少能有一个可靠、可扩展、能对自己的算法说清道理的Simulink电池仿真模型。

1. 为什么选等效电路模型,而不是电化学模型或纯查表模型

1.1 模型复杂度和精度的平衡

电池仿真模型大致有三条路:电化学模型、等效电路模型和纯查表模型。电化学模型从锂离子在电极内部的嵌入/脱出、扩散和电解液浓度分布出发,物理意义最强,但参数动辄几十个,还要解偏微分方程,放到Simulink里仿真速度很慢,也不适合做控制算法的实时验证。纯查表模型就是把OCV、内阻和温度、SOC的关系建一张大表,输入电流和SOC直接插值出电压,简单到不能再简单,但它没有“记忆效应”,电流突变的瞬间电压跳变得特别生硬,无法反映电池极化弛豫的过程。等效电路模型刚好卡在中间——用几个RC并联网络表示电池内部极化的动态特性,既能复现电压的瞬态响应,参数又可以用常规充放电实验辨识出来,计算量也小。这正是我最终选它的原因。

1.2 我在选型时的一个实际对比

当时我手上有两个备选:直接用Simulink自带Simscape Electrical库里的Battery模块,或者自己用积分器搭一个数学模型。自带模块确实方便,打开库拖出来就能仿真,内部包含了热效应、老化寿命这些高级特性,适合系统级电能分析。但如果你像我一样想控制算法、观察每个状态变量,甚至后期要改模型方程,自带模块就像一个黑盒子,出了问题你只能干瞪眼。所以我决定自己搭一个透明的等效电路模型。后面证明这个决定对了,因为我在调参数和理解OCV-SOC曲线之间的关系时,每一步都知道值是怎么算出来的。

2. 动手前的两件事:环境准备与数据来源

2.1 需要哪些工具箱和库

我的环境是MATLAB R2022b,Simulink是必须的。如果模型里用到更底层电学模块,再装Simscape Electrical,但我自己搭等效电路模型,只用基本Simulink模块就够了。参数辨识阶段需要用lsqcurvefit或者nlinfit,这两个分别来自Optimization Toolbox和Statistics and Machine Learning Toolbox,建议装上。如果你要用SIMULINK仿真后自动生成C代码,还需要Simulink Coder,但那是后话,初期可以跳过。

安装这块没什么好说的,正常安装就完了。但是我建议在开始搭模型前,先在Matlab命令行跑一遍ver确认一下工具箱是否完全识别,尤其是Optimization Toolbox,否则后面辨识时会报“Undefined function 'lsqcurvefit'”。

2.2 实验数据怎么组织才便于参数辨识

模型搭出来没有数据就是空中楼阁。最常用的电池建模测试是HPPC(混合脉冲功率特性)测试:把电池放在恒温箱里,先静置很久,然后用一定倍率电流放电脉冲几秒,再静置,再充脉冲,逐个小SOC点重复,最后记录时间、电流、电压。如果你没有实验条件,也可以用公开的电池数据集,比如马里兰大学CALCE电池数据集,或者网上常见的Panasonic 18650测试数据。

数据格式我强烈建议统一为三列CSV:时间(秒)、电流(安培)、电压(伏特)。电流正负号按习惯定义,我用正表示放电负表示充电。SOC曲线单独用一个Excel表存放,横轴是SOC(0到1),纵轴是OCV(伏特)。表头必须清晰,因为后面要脚本批量读取。

我当时是直接从实验室的充放电柜导出的原始数据,里面有很多噪声和异常跳点。建议导入Matlab后第一时间画图核对,剔除明显不合理的点,并做一次移动平均滤波,但注意不要过度平滑掉电流突变的响应,否则参数辨识中的R0会被低估。

3. 在Simulink里搭出第一个2RC等效电路模型

3.1 模型方程与模块布局

2RC等效电路模型的端电压方程是这样的:

V_t = OCV(SOC) - I * R0 - V1 - V2

其中V1和V2是两个RC网络的极化电压,各自满足一阶微分方程:

dV1/dt = I/C1 - V1/(R1C1)
dV2/dt = I/C2 - V2/(R2
C2)

更常用的是写成传递函数形式,每个RC网络相当于一个一阶惯性环节:V1/I = R1/(tau1s + 1),tau1 = R1C1。两个RC并联网络串联起来,就能同时表达快极化和慢极化——一个时间常数几秒,另一个几十到几百秒。只用1个RC网络拟合精度往往不够,特别是长弛豫阶段电压回不到基线,所以直接上2RC是工程上的主流选择。

在Simulink里布局的时候,我从左到右排:电流输入端、容量积分和SOC计算、OCV查表、两个RC支路、求和输出。电流作为模型的输入,这个非常关键,因为电压是状态变量的函数而不是直接由代数方程求出来的,这样能避开代数环问题。

3.2 逐步搭建的实操细节

我按照下面这个顺序搭建:

  1. 放一个Constant模块作为初始SOC,设成0.8。
  2. 放一个Integrator模块计算SOC变化。积分器的输入是电流除以容量再除以3600,因为容量是Ah,电流是A,时间是s,换算后得到SOC的变化率。例如2Ah电池,增益就是1/(2*3600)。
  3. 把Integrator输出接到一维Lookup Table,查OCV-SOC表,得到当前开路电压。
  4. 在电流通路并联两条RC支路。我习惯用“Transfer Fcn”模块,每个支路设置成R/(tau*s+1)的形式,比如快支路R1=0.02欧姆,tau1=10秒,慢支路R2=0.03欧姆,tau2=200秒。这样比手动搭积分器省事,但注意Transfer Fcn初值默认是0,如果不希望有初始极化,这样刚刚好。
  5. 用Sum块做加减,V_t = OCV - I*R0 - V1 - V2。R0用Gain模块,乘以电流。
  6. 最后用Scope或者To Workspace将电压输出。

搭完之后,用Signal Editor或From Workspace加载一个脉冲电流信号,跑一下,你应该能看到电压在电流突变时先瞬间跳变(R0产生),然后缓慢指数过渡(RC产生)。这个瞬态形状对不对,一眼就能看出来。

4. 参数辨识:用最小二乘把RC参数“喂”准

4.1 参数辨识的原理和步骤

模型搭好了,里面的参数还是拍脑袋给的,必须从实验数据里提出来。基本思路是分段辨识:

第一步,辨识R0。在脉冲电流突变的瞬间,电压会有一个阶跃跳变,跳变幅值除以电流变化量就是欧姆内阻R0。比如从0A跳到-10A(放电),电压从4.0V降到3.95V,那么R0 = (4.0-3.95)/10 = 0.005欧姆。注意要读取突变瞬间那个采样点的电压,而不是突变后几秒的电压,因为RC极化马上就会叠加进去。

第二步,辨识RC参数。脉冲结束后进入弛豫阶段,电流为0,端电压会指数回升,回升过程由两个RC网络组成。这段电压响应可以用双指数函数拟合:

V_relax = OCV + a1 * exp(-t/tau1) + a2 * exp(-t/tau2)

这里的a1和a2代表弛豫开始时两个RC网络的极化电压值。如果用最小二乘法拟合出a1、a2、tau1、tau2,再结合刚才脉冲放电时的电流大小,就能反算R1和R2。如果放电脉冲足够长,极化接近稳态,则稳态极化电压约等于IR1和IR2,所以R1 = a1/I,R2 = a2/I(注意符号和方向)。更严谨的做法是结合脉冲施加阶段的完整响应一起拟合,但双指数拟合对初值敏感,需要多次尝试。

4.2 在Matlab里做辨识的代码片段

下面是我用的一个简单拟合代码框架,直接使用弛豫段数据:

% 加载HPPC数据 data = readtable('hppc_data.csv'); t = data.Time; I = data.Current; V = data.Voltage; % 常数:找弛豫段起始索引 idx_start = find(t > t_relax_start, 1); idx_end = find(t > t_relax_end, 1); t_relax = t(idx_start:idx_end); V_relax = V(idx_start:idx_end); % OCV为弛豫结束后的电压,取最后几十个点的均值 OCV = mean(V_relax(end-50:end)); % 定义双指数函数:V = OCV + a1*exp(-t/tau1) + a2*exp(-t/tau2) fitfun = @(p, t) OCV + p(1)*exp(-t/p(2)) + p(3)*exp(-t/p(4)); % 初值:电阻极化0.02V,tau快速5s;慢极化0.04V,tau慢速100s p0 = [0.02, 5, 0.04, 100]; % 上下界限制 lb = [0, 0.5, 0, 10]; ub = [1, 100, 1, 1000]; % 最小二乘拟合 pfit = lsqcurvefit(fitfun, p0, t_relax, V_relax, lb, ub); a1 = pfit(1); tau1 = pfit(2); a2 = pfit(3); tau2 = pfit(4);

拟合完之后,根据电流方向求出电阻初值。如果放电电流为正,弛豫起点极化电压为负,那么实际上a1和a2的绝对值就是放电时产生的极化电压,所以R1 = abs(a1)/I_pulse,R2 = abs(a2)/I_pulse。这样每个SOC点都做一次,就能得到一组随SOC变化的R0、R1、R2、C1、C2参数表。C = tau/R。

这里有个很容易犯的错:弛豫段OCV必须取足够长的静置后的电压,否则SOC还没有稳定,估算的OCV有偏差,拟合出的双指数曲线就会整体偏移。我一般至少静置15分钟以上再取OCV。

5. 让模型具备“智慧”:SOC、温度与老化的耦合

5.1 SOC估算引入方式

前面的模型核心是电流积分计算SOC,这就是安时积分法:

SOC(t) = SOC0 - (1/(3600*Q)) * ∫ I dt

在Simulink里用一个积分器实现,初始条件设成SOC0。但是安时积分有累积误差,尤其是长时间仿真或者电流传感器有偏置时,SOC会越漂越远。因此模型内部要能够接受外部修正,方便后面和BMS算法对接。我通常在SOC积分器旁边加一个“修正输入”端口,当外部估算算法给出权威SOC时,可以把这个值以很小的增益反馈到积分器,保持模型和真实状态同步。

如果你做的是纯算法验证,也可以把SOC作为直接输入,不通过积分器,这样省事且不会因为SOC漂移影响OCV输出。但如果你要模拟真实情况下SOC估算不准时的电压响应,就必须保留积分器,并允许SOC误差的存在。

5.2 温度修正通过查表或解析式实现

电池参数受温度影响非常大,冬天和夏天的内阻能差两倍。最简单的温度修正方式是在前面得到的参数表基础上,再加一个温度维度。比如把HPPC测试在0℃、10℃、25℃、40℃都跑一遍,建立四个温度下的参数表。然后在Simulink里使用二维Lookup Table,横轴SOC,纵轴温度,输出R0、R1等参数。

我实际用过一种更省事的方法:只测25℃基准参数表,然后用一个温度修正系数查表或解析式。常用的阿伦尼乌斯修正形式是:

R(T) = R_ref * exp(A * (1/T - 1/T_ref))

其中T和T_ref是绝对温度,A是活化能常数。这个公式用在锂离子电池内阻修正上还挺有效。但要小心,温度范围过大时,单指数修正不够准确,最好还是多温度测试。

把温度耦合进模型时,注意两条:第一,温度信号必须也是连续信号,不能突变,否则电压也会有阶跃;第二,仿真中温度通常来自热模型或实测数据,如果没有热模型,就干脆固定温度,别让温度成为假动态。

老化的引入更简单,把容量改成SOH乘以标称容量,内阻也可以加一个随循环增长的修正因子。对模型搭建来说,不用追求精确老化模型,重点是理解容量和内阻变化对端电压、SOC估算的影响。

6. 仿真验证:误差、鲁棒性与步长敏感性

6.1 与实测数据对比的流程

模型参数辨识完,必须拿到一组没参与辨识的动态工况数据来做交叉验证。我常用一个自定义的混合工况:几分钟大电流放电,再平台小电流,再脉冲,叠加温度变化(如果有温度数据)。把这个工况的电流信号导入Simulink的Signal Editor,跑完仿真后把输出的电压序列和实测电压序列画在一起,计算误差。

误差指标我习惯看这几个:平均绝对误差MAE、均方根误差RMSE、最大绝对误差。对于2RC模型,在25℃下动态工况电压RMSE做到20mV以内就算不错,最大误差不要超过50mV。如果误差超过这个值,首先检查参数表在极端SOC区间是否平滑,其次检查模型里R0是否取的采样点过晚。

注意对比时要把仿真的初始SOC和真实SOC对齐。如果仿真从SOC=1开始,实际数据电池从SOC=0.9开始,那前面电压会一直差一个OCV偏移。这个不算模型误差,是初始化问题。

6.2 步长选择和求解器设置

Simulink默认变步长求解器 ode45在大多数情况下没问题,但电池模型里有快速RC时间常数和查表插值,如果步长控制不好,电压响应会失真。我的经验是:

  • 设置求解器为变步长,容差RelTol=1e-4,AbsTol=1e-6,最大步长限制在0.1s。原因是电流突变后RC快速支路可能只有几秒时间常数,步长太大不够采样;查表在SOC变化剧烈时也可能有插值尖峰。
  • 如果模型要跑硬件在环(HIL)或代码生成,必须转成定步长离散模型。我建议从一开始就用定步长梯形法或者四阶龙格库塔,步长10ms或1ms。这样结果可重复,也方便与外部离散算法交互。
  • 查表模块里设置“平滑”插值,不要用线性插值。锂离子电池OCV-SOC曲线在低SOC段有个拐点,线性插值会出现折点,影响微分阶跃。用平滑插值(例如三次样条)能让电压响应更平缓。

7. 踩坑实录:代数环、单位混乱和初始状态

7.1 代数环问题及解法

我第一次搭模型时就遇到红颜色的“Algebraic Loop”警告,虽然仿真也能跑,但速度慢而且偶尔死循环。原因是我在电压输出端用了“受控电压源”和“电流传感器”把电压再反馈到输入端,导致电流和电压形成一个瞬间闭合的因果环。解决方法是改变建模思路:保证“电流进,电压出”,电流是输入,电压是状态变量的线性组合,而不是电压决定电流。如果你确实需要闭环,可以在反馈路径加一个有延迟的Memory模块,但更推荐重构信号流。

7.2 单位混乱是个大坑

容量单位换算是我见过最多人翻车的地方。电池容量是Ah,而Simulink里积分时间是秒,所以1Ah = 3600As。如果你的电池容量是2Ah,积分增益实际是1/(2*3600),不是1/2。我曾经写成1/2,结果SOC每秒就归零,整个模型完全不正常。

另外一个常见错误是把电压毫伏当伏特,或者参数内阻用毫欧直接填欧姆。建议在Simulink模型里给每个信号加上单位标签:电流(A)、电压(V)、SOC(0-1),在模型文档里写清楚每个参数的物理量纲。如果使用Bus,还可以用Simulink.BusElement定义单位属性,至少能防止自己后面忘掉。

7.3 初始状态与“零漂移”问题

假如你要仿真的工况是从电池满电荷但已静置很久的状态开始,那RC极化电压初始为0,没有问题。但如果你从正在大电流放电的状态开始,V1、V2的初值不设的话,模型开头会出现一个虚假的过渡过程。我一般在参数辨识完之后,额外跑一遍“预仿真”:先给模型加载一个恒定电流跑足够长的时间,让RC极化电压稳定,再把这个稳定状态的V1、V2以及SOC值作为正式仿真的初始化条件。这在Simulink里可以通过工作区传递初始状态,或者直接把积分器初始条件设成变量。

还有一个小坑:OCV-SOC曲线从0到1,如果你用线性插值,在SOC=0和1的位置会把OCV拉得很夸张,影响仿真收敛。所以我建议给OCV曲线两端各外插一小段,让导数平滑。

8. 从单体到电池包,扩展模型时的架构建议

8.1 打包成子系统还是Simscape

当你需要仿真几十串电芯的电池组时,一个个搭建肯定不可行。我采用的办法是把上面的2RC模型封装成一个带Mask的子系统,参数表通过Mask参数传递。每个子系统有电流输入、SOC输出、电压输出,还有温度输入端口。然后用总线箭头把这些子系统的输入输出串联起来,或者做成一个单独的“BatteryCube”子系统,内部再铺网络。这样做的好处是BMS算法可以直接读取每一串的SOC和电压,方便做均衡策略仿真。

如果你用的是Simscape Electrical,它有自己的Battery模块支持串并联,甚至能和热网络联合仿真。但用Simscape的代价是仿真步长要求更严格,且状态变量不好直接观测。我的建议是:先做纯Simulink版本验证算法,等到需要做热-电耦合高精度仿真时,再把底层换成Simscape,外面留一样的接口。

8.2 批量仿真和自动代码生成注意点

做完电池组模型,我经常要扫参数,比如扫容量、内阻、OCV曲线偏移量来测试BMS算法鲁棒性。在脚本里用Simulink.SimulationInput批量跑非常方便,比如:

for k = 1:length(SOC_init_list) simIn(k) = Simulink.SimulationInput('battery_model'); simIn(k) = setVariable(simIn(k), 'SOC_init', SOC_init_list(k)); simOut(k) = sim(simIn(k)); end

注意变量名一定要和工作区中的模型参数一致,否则静默不生效。

如果要生成C代码,我的经验是先转成定步长离散模型,把连续积分器换成离散积分器,再把查表模块设置好,确保支持代码生成。生成的代码里,你可能会发现有些数学函数在嵌入式环境没有完全等效,所以要做好验证。不过这个阶段比较进阶,初期不需要考虑。

最后再啰嗦一句,模型搭建的核心不是堆模块,而是每一步物理意义要清楚。你先用一个最简单的1RC模型跑通全流程,再逐步加慢RC、温度、老化,你会发现每次加复杂度之前都要问自己一个问题:这个复杂度真的能改善误差吗?如果加了温度修正,误差反而变大,那就说明你的温度数据或修正公式有问题,而不是模型越复杂越好。我后来做电池模型最大的感受是:稳扎稳打,永远比追求花哨模型更高效。希望这篇经验能让你少走点弯路。

返回列表