用 Python 实现电池一阶 RC 等效电路模型:参数辨识与仿真
用 Python 实现电池一阶 RC 等效电路模型:参数辨识与仿真
专栏:《电池上云笔记》第 3 篇 | 分类:开发语言
关键词:等效电路模型、Thevenin 模型、一阶 RC、HPPC、脉冲法、参数辨识、精确离散化、Python、numpy、pandas
一、为什么需要一个"电池模型"
前两篇我们完成了两件事:第 1 篇画出了"电芯 → 设备 → 云端"的数据链路全景图,第 2 篇把测试仪的放电曲线变成了可计算的结构化数据。数据有了,下一个问题是:
给定任意一段电流,电池的端电压会怎么变?
这个问题背后是很多工程需求:
- 电量估算:卡尔曼滤波(EKF)估 SOC 时(参见我之前的镍氢电池 EKF 实战文章),需要一个"电流 → 电压"的预测模型,模型越准,SOC 越准;
- 低电压预警:设备瞬间大电流(电机启动、无线模块发射)会不会把电压拉到关机阈值以下?
- 续航仿真:新产品还没做出来,先用模型 + 负载曲线预估能用多久;
- 数据上云后的"数字孪生":云端用模型对比实测,偏差变大就意味着电池老化或异常。
电化学机理模型(如 P2D 模型)精度高,但参数多、计算重,不适合 MCU 和大规模云端批量计算。工程上最常用的是等效电路模型(ECM,Equivalent Circuit Model)——用电压源、电阻、电容"模仿"电池的外特性。其中最经典的就是一阶 RC 模型(Thevenin 模型)。
本文目标:用约 80 行 numpy/pandas 代码,完成
脉冲测试数据 ──► ① 库仑计数算 SOC
──► ② 自动定位脉冲
──► ③ 辨识 R0 / R1 / C1 / OCV(随 SOC 变化的查找表)
──► ④ 仿真任意电流工况的端电压
──► ⑤ 与实测对比 RMSE / MAE / 最大误差
最后还会回答一个很实际的问题:一阶模型的误差到底是"参数没辨识准",还是"模型本身不够"?
说明:为了让大家能直接运行和验证,本文使用模拟生成的实验数据(文末附生成脚本)。虚拟电池以 NiMH AA 2000 mAh 为原型,内部"真值"是一个二阶 RC 模型并加入测量噪声与量化,这样我们能拿辨识结果和真值做对照。数值仅用于演示方法,不代表任何具体产品的性能。 方法本身同样适用于锂离子电池,换成真实测试仪数据即可。
二、Thevenin 模型:三个元件,三种物理过程
2.1 电路结构
R0 R1
┌───/\/\/───┬───/\/\/───┐
│ │ │
(+) └────||─────┤
OCV(SOC) C1 │
(−) │
│ │
└──────── 负载 ◄────────┘ 端电压 Vt
| 元件 | 物理含义 | 时间尺度 | 在曲线上的表现 |
|---|---|---|---|
| OCV(SOC) | 开路电压,由电极材料的热力学平衡决定,是 SOC 的函数 | 静态 | 长时间静置后的电压 |
| R0 | 欧姆内阻:电解液、隔膜、集流体、极耳、接触电阻 | 微秒~毫秒 | 加载/卸载瞬间的电压台阶 |
| R1 ∥ C1 | 极化:电荷转移 + 双电层(以及部分扩散) | 秒~几十秒 | 台阶之后的指数型缓慢下降/回升 |
一句话理解:R0 决定"一下子掉多少",R1C1 决定"之后还会慢慢掉多少、掉多快"。时间常数 τ = R1·C1 就是这个"慢慢"的快慢。
2.2 数学方程
约定放电电流 I 为正,极化电压为 V1,则:
恒流 I 放电 t 秒后,极化电压为 ;卸载后 按 衰减。这两条指数曲线,就是参数辨识的全部依据。
2.3 离散化:别用前向欧拉
代码里要把微分方程变成递推。很多教程直接写前向欧拉:
它在 Δt 远小于 τ 时没问题,但当采样间隔变大(比如云端只存 1 min 一个点,或 MCU 低功耗模式下 10 s 采一次),Δt > 2τ 时会直接发散。
更好的做法是假设两次采样之间电流不变(零阶保持),直接用解析解:
这个公式对任意 Δt 都无条件稳定且精确(在电流分段恒定的前提下),计算量也只多一个 exp,MCU 上还可以预先把 算好存起来。后文代码全部使用这种形式。
三、测试设计:HPPC 式脉冲 + 长静置
参数辨识的数据来自脉冲测试,思路借鉴锂电行业常用的 HPPC(Hybrid Pulse Power Characterization)。本文的测试步骤:
- 满电后静置 30 min;
- 每个 SOC 点执行:2 A 放电脉冲 10 s → 静置 60 s → 1 A 放电 12 min(消耗 10% SOC)→ 静置 30 min;
- 重复 9 次,覆盖 SOC 100% → 10%;
- 采样率 10 Hz,记录时间、电流、电压。
每个设计都有原因:
| 设计 | 目的 |
|---|---|
| 脉冲前静置 30 min | 让极化完全消失,脉冲前电压 ≈ OCV |
| 2 A 短脉冲 10 s | 电流足够大,电压台阶明显(信噪比高);时间短,SOC 几乎不变 |
| 脉冲后静置 60 s | 观察弛豫曲线,拟合 τ 和 R1 |
| 10 Hz 采样 | 保证"台阶"前后各有一个干净的点,R0 才测得准 |
| 每次放掉 10% | 得到参数随 SOC 变化的表格 |

图1 左:完整脉冲测试的电流/电压;右:单个脉冲放大,标注了 R0 台阶(瞬时压降/回升)和 R1C1 弛豫段。
四、辨识原理
以一个脉冲为例(脉冲电流 I、持续 Tp):
① OCV:取脉冲开始前 5 个点的平均电压。
② R0:加载瞬间压降和卸载瞬间回升各算一次,取平均,可以抵消一部分噪声和"台阶前后点"不对称的影响:
③ τ 与 R1:卸载后的弛豫段满足
这里有个容易忽略的点:10 s 脉冲并没有让 RC 支路"充满"(τ=8 s 时只充到 1−e^(−10/8) ≈ 71%),如果直接用 A/I 当 R1,会低估约 30%。所以要除以 做修正。
④ C1:。
拟合方法上,这是一个"一个非线性参数 τ + 两个线性参数 V∞、A"的问题。不需要 scipy,用一个小技巧即可:对 τ 做网格搜索,每个 τ 下用线性最小二乘求 V∞ 和 A,取残差最小的那组。400 个网格点,单个脉冲不到 0.1 s,结果稳定,也不存在初值敏感、不收敛的问题。
五、完整代码
"""电池一阶 RC(Thevenin)等效电路模型:脉冲法参数辨识 + 任意工况仿真 + 误差评估(仅依赖 numpy/pandas)"""
import numpy as np
import pandas as pd
Q_AH = 2.0 # 额定容量 (Ah)
# ---------- 1. 库仑计数得到 SOC ----------
def coulomb_soc(t, i, soc0=1.0):
dq = np.concatenate([[0], (i[1:] + i[:-1]) / 2 * np.diff(t)]) / 3600 # Ah,梯形积分
return soc0 - np.cumsum(dq) / Q_AH
# ---------- 2. 找脉冲:电流从 ≈0 跳到 ≥ 阈值 ----------
def find_pulses(i, i_th=1.5, rest_th=0.05):
on = i > i_th
starts = np.where(on[1:] & ~on[:-1])[0] + 1
ends = np.where(~on[1:] & on[:-1])[0] + 1
return [(s, e) for s, e in zip(starts, ends) if abs(i[s - 1]) < rest_th]
# ---------- 3. 弛豫曲线拟合:V(t) = V∞ - A·exp(-t/τ),τ 网格搜索 + 线性最小二乘 ----------
def fit_relaxation(t, v, taus=np.geomspace(0.5, 200, 400)):
best = None
for tau in taus:
X = np.column_stack([np.ones_like(t), -np.exp(-t / tau)])
coef, res, *_ = np.linalg.lstsq(X, v, rcond=None)
sse = res[0] if res.size else np.sum((X @ coef - v) ** 2)
if best is None or sse < best[0]:
best = (sse, tau, *coef)
_, tau, v_inf, amp = best
return tau, v_inf, amp
# ---------- 4. 脉冲法辨识 R0 / R1 / C1 / OCV ----------
def identify(df, relax_s=60.0):
t, i, v = df["t_s"].to_numpy(), df["current_A"].to_numpy(), df["voltage_V"].to_numpy()
soc = coulomb_soc(t, i)
rows = []
for s, e in find_pulses(i):
I = i[s:e].mean(); Tp = t[e - 1] - t[s] + (t[1] - t[0])
ocv = v[s - 5:s].mean() # 脉冲前长时间静置 ≈ OCV
r0_on = (v[s - 1] - v[s]) / I # 加载瞬间压降
r0_off = (v[e] - v[e - 1]) / I # 卸载瞬间回升
r0 = (r0_on + r0_off) / 2
m = (t >= t[e]) & (t <= t[e] + relax_s) # 卸载后的弛豫段
tau, v_inf, amp = fit_relaxation(t[m] - t[e], v[m])
r1 = amp / (I * (1 - np.exp(-Tp / tau))) # 修正脉冲期间 RC 未充满
rows.append(dict(soc=round(soc[s], 3), ocv=ocv, r0_mohm=r0 * 1e3,
r1_mohm=r1 * 1e3, tau_s=tau, c1_F=tau / r1))
return pd.DataFrame(rows).sort_values("soc").reset_index(drop=True)
# ---------- 5. 仿真:参数按 SOC 查表插值,RC 支路用精确离散化 ----------
def simulate(params, t, i, soc0, use_rc=True):
soc = coulomb_soc(t, i, soc0)
lut = lambda col: np.interp(soc, params["soc"], params[col])
ocv, r0 = lut("ocv"), lut("r0_mohm") / 1e3
r1, tau = lut("r1_mohm") / 1e3, lut("tau_s")
v1, out = 0.0, np.empty_like(i)
for k in range(len(i)):
if use_rc and k > 0:
a = np.exp(-(t[k] - t[k - 1]) / tau[k])
v1 = v1 * a + i[k - 1] * r1[k] * (1 - a) # 零阶保持下的精确解
out[k] = ocv[k] - i[k] * r0[k] - (v1 if use_rc else 0.0)
return out, soc
def metrics(v_meas, v_sim):
err = (v_sim - v_meas) * 1e3
return dict(RMSE_mV=np.sqrt(np.mean(err ** 2)), MAE_mV=np.mean(np.abs(err)), MAX_mV=np.max(np.abs(err)))
if __name__ == "__main__":
pulse = pd.read_csv("pulse_test.csv")
params = identify(pulse)
print("== 辨识结果 ==")
print(params.round({"ocv": 4, "r0_mohm": 1, "r1_mohm": 1, "tau_s": 1, "c1_F": 0}).to_string(index=False))
params.to_csv("ecm_params.csv", index=False)
drive = pd.read_csv("drive_test.csv")
t, i, v = (drive[c].to_numpy() for c in ("t_s", "current_A", "voltage_V"))
v_1rc, soc = simulate(params, t, i, soc0=0.9)
v_r0, _ = simulate(params, t, i, soc0=0.9, use_rc=False)
print("\n== 验证工况误差 ==")
for name, vs in (("仅内阻 Rint 模型", v_r0), ("一阶 RC 模型", v_1rc)):
print(name, {k: round(x, 2) for k, x in metrics(v, vs).items()})
np.save("sim.npy", np.vstack([t, i, v, v_1rc, v_r0, soc]))
几个实现细节说明:
find_pulses只认"从静置(|I|<0.05 A)跳到 >1.5 A"的边沿,所以 1 A 的长放电段不会被误判为脉冲;identify输出的是一张以 SOC 为索引的参数表,simulate用np.interp线性插值查表,SOC 超出范围时自动取端点值;simulate中use_rc=False时退化为最简单的内阻模型(Rint),用来做对照;- 电流采样有噪声,所以 SOC 用梯形积分,避免左矩形法的系统偏差(第 2 篇做过对比实验)。
运行:
python gen_data.py # 生成模拟数据(真实项目替换为测试仪导出的 CSV)
python ecm.py # 辨识 + 仿真 + 误差
六、辨识结果:和真值对一对
| SOC | OCV 辨识 / 真值 (V) | R0 辨识 / 真值 (mΩ) | R1 辨识 / 真值 (mΩ) | τ1 (s) | C1 (F) |
|---|---|---|---|---|---|
| 17.8% | 1.2155 / 1.2158 | 45.2 / 45.2 | 23.3 / 23.2 | 8.0 | 345 |
| 28.1% | 1.2318 / 1.2320 | 41.0 / 40.9 | 22.2 / 22.2 | 8.0 | 362 |
| 38.3% | 1.2453 / 1.2448 | 38.4 / 37.8 | 21.2 / 21.2 | 8.0 | 379 |
| 48.6% | 1.2557 / 1.2561 | 35.3 / 35.4 | 20.5 / 20.1 | 8.2 | 399 |
| 58.9% | 1.2658 / 1.2659 | 33.7 / 33.4 | 19.4 / 19.1 | 8.2 | 420 |
| 69.2% | 1.2743 / 1.2742 | 32.5 / 31.9 | 18.3 / 18.1 | 8.2 | 446 |
| 79.4% | 1.2816 / 1.2813 | 31.3 / 30.9 | 17.2 / 17.1 | 8.2 | 475 |
| 89.7% | 1.2901 / 1.2900 | 30.5 / 30.2 | 16.2 / 16.0 | 8.0 | 497 |
| 100% | 1.3195 / 1.3200 | 30.1 / 30.0 | 15.3 / 15.0 | 8.2 | 533 |
(虚拟电池真值:τ1 = 8 s)

图2 OCV、R0、R1 随 SOC 的变化,虚线为真值,圆点为辨识结果。
从表中可以读出三点:
- OCV 误差 < 0.6 mV,R0 误差 < 0.6 mΩ,R1 误差 < 0.4 mΩ,τ1 误差 < 3%。在 0.5 mV 噪声、0.1 mV 量化的条件下,这个精度说明方法是可靠的;
- 低 SOC 时内阻明显上升(R0 从 30 mΩ 升到 45 mΩ,R1 从 15 mΩ 升到 23 mΩ)。这正是电池"快没电时一用大电流就关机"的原因——不只是 OCV 低了,压降也更大了;
- R1 的辨识值普遍比真值略高 0.1~0.4 mΩ:虚拟电池里还有一个 τ2 = 120 s 的慢支路,10 s 脉冲对它也有一点激励,被"算进"了 R1。这个小偏差是下一节误差分析的伏笔。
七、模型验证:在"没见过"的工况上跑一遍
辨识用的是脉冲数据,验证必须用另一套工况,否则就是"自己考自己"。验证数据是一段 90 min 的随机混合负载(模拟玩具、手持设备、IoT 设备的间歇大电流),电流在 0~2 A 之间随机切换,每段 5~120 s,SOC 从 90% 降到约 21%,平均电流约 0.92 A。
drive = pd.read_csv("drive_test.csv")
v_1rc, soc = simulate(params, t, i, soc0=0.9)
v_r0, _ = simulate(params, t, i, soc0=0.9, use_rc=False)

图3 上:验证工况电流(局部 15 min);中:测量电压与三种模型仿真;下:全程 90 min 误差。
| 模型 | RMSE (mV) | MAE (mV) | 最大误差 (mV) | 参数个数 / SOC 点 |
|---|---|---|---|---|
| 仅内阻 Rint | 31.18 | 26.37 | 60.14 | 2(OCV、R0) |
| 一阶 RC(本文) | 9.32 | 8.45 | 18.70 | 4(OCV、R0、R1、τ1) |
| 二阶 RC(进阶) | 0.68 | 0.54 | 2.79 | 6 |
结论很直观:
- Rint 模型只会"台阶式"跳变,完全跟不上电流变化后的缓慢下降/回升,误差最大 60 mV。对 NiMH 来说,60 mV 大约对应 20%~30% 的 SOC 偏差(NiMH 的 OCV 曲线很平),基本没法用;
- 一阶 RC 把 RMSE 降到 9.3 mV,降幅 70%,动态过程的形状已经基本吻合;
- 二阶 RC 进一步降到 0.68 mV,已经接近 0.5 mV 的测量噪声。
八、误差从哪里来:参数问题还是结构问题?
一阶模型还有 9 mV 的误差,第一反应往往是"参数没调好,再优化一下"。在优化之前,先做一个对照实验:直接把虚拟电池的真值参数(OCV、R0、R1、τ1)装进一阶模型,看误差是多少。
# check.py 核心:真值参数 + 一阶结构(去掉慢支路 R2/τ2)
for k in range(len(i)):
if k:
a = np.exp(-(t[k]-t[k-1]) / g.TAU1)
v1 = v1*a + i[k-1]*g.r1(s[k])*(1-a)
out[k] = g.ocv(s[k]) - i[k]*g.r0(s[k]) - v1
| 方案 | RMSE (mV) | 最大误差 (mV) |
|---|---|---|
| 真值参数 + 一阶结构 | 9.88 | 20.08 |
| 辨识参数 + 一阶结构 | 9.32 | 18.70 |
用真值参数反而更差。 这说明一阶模型剩下的误差几乎全部来自模型结构——它缺少一个慢时间常数的扩散支路,参数再怎么调也补不上(辨识值略好,恰恰是因为 R1 "吸收"了一点慢支路的影响)。
再按 SOC 区间看误差的均值:
| SOC 区间 | RMSE (mV) | 误差均值 (mV) |
|---|---|---|
| 70%~90% | 8.88 | +7.78 |
| 50%~70% | 9.49 | +8.31 |
| 20%~50% | 9.50 | +9.08 |
误差是稳定的正偏差(模型电压偏高约 8 mV),并且各个 SOC 区间都差不多。做个估算:慢支路 R2 = 10 mΩ,平均电流 0.92 A,稳态下它贡献的压降约 9 mV——和偏差几乎一致。
经验:先判断误差是"随机的"还是"有结构的"。 有稳定偏差、或者误差和电流历史相关,通常是模型阶数不够;误差在零附近随机抖动、幅度接近噪声,才说明模型已经到头了。
九、进阶:从长静置段辨识二阶 RC
为什么一阶辨识"看不见"慢支路?因为辨识窗口决定了你能看到哪个时间常数:10 s 脉冲 + 60 s 静置,只够看清 τ1 = 8 s;τ2 = 120 s 的过程在 60 s 内只走了一小半,被当成了"缓慢漂移"。
好在测试设计里每个 SOC 点都有一段 1 A 放电 12 min + 静置 30 min,长放电让慢支路充分建立,长静置让它完整衰减。对这段弛豫曲线做双指数拟合:
方法和一阶一样:τ1、τ2 做二维网格搜索,每组 (τ1, τ2) 下线性最小二乘求 V∞、A1、A2。为减少计算量,前 60 s 保留 10 Hz,之后降到 1 Hz。
def fit_relaxation2(t, v, tau1s=np.geomspace(1, 40, 40), tau2s=np.geomspace(45, 800, 40)):
best = None
for t1 in tau1s:
e1 = np.exp(-t / t1)
for t2 in tau2s:
X = np.column_stack([np.ones_like(t), -e1, -np.exp(-t / t2)])
coef, res, *_ = np.linalg.lstsq(X, v, rcond=None)
sse = res[0] if res.size else np.inf
if best is None or sse < best[0]:
best = (sse, t1, t2, *coef)
return best[1:] # τ1, τ2, V∞, A1, A2
长放电 12 min 远大于 τ1、τ2,两条支路都已充满,所以这里直接 R1 = A1/I、R2 = A2/I,不需要做"未充满修正"。辨识结果:τ1 ≈ 8.01 s,τ2 ≈ 117 s(真值 120 s),R2 ≈ 9.9~10.1 mΩ(真值 10 mΩ)。仿真时多加一条 RC 支路即可,误差从 9.3 mV 降到 0.68 mV。
但要注意代价:二阶模型需要更长的测试时间(每个 SOC 点要 30 min 以上静置)、更多参数、更多计算。是否值得,取决于应用:
| 应用场景 | 推荐模型 |
|---|---|
| 简单低电压报警、粗略电量格数 | Rint 也许就够 |
| MCU 上的 EKF 估 SOC、峰值功率预测 | 一阶 RC(性价比最高) |
| 续航仿真、云端数字孪生、老化分析 | 二阶 RC |
十、工程落地的 7 条建议
- R0 对采样率最敏感。1 Hz 采样时"台阶后第一个点"已经包含了部分极化,R0 会偏大。辨识 R0 建议 ≥ 10 Hz,有条件用 100 Hz 以上;
- 静置时间决定能看到的时间常数。经验上静置时长 ≥ 5τ;想看 τ2 = 120 s 的过程,至少要 10 min;
- 参数强烈依赖温度。R0、R1 在 0 ℃ 时可能是 25 ℃ 的 2~3 倍。产品级模型要做成 SOC × 温度的二维表;
- NiMH 有明显的电压迟滞:充电后和放电后的 OCV 不同,本文只做了放电方向,充电工况要单独辨识,或者在模型里加迟滞项;
- 参数表就是 MCU 上的查找表。9 个 SOC 点 × 4 个参数 = 36 个 float,不到 150 字节,查表 + 一次指数递推,Cortex-M0 也轻松跑;
- 离散化系数可以预计算。采样周期固定时,
a = exp(-Δt/τ)可以离线算好存表,MCU 上连exp都省了; - 用独立工况验证,并记录误差指标。RMSE、最大误差、按 SOC 分区的偏差应随参数表一起入库,后续上云做老化跟踪时,"误差变大"本身就是一个健康度指标。
附一个 MCU 端的 C 语言实现参考,逻辑与 Python 版 simulate 完全一致:
/* 一阶 RC 模型端电压预测(放电电流为正),参数表按 SOC 升序 */
#define N_PT 9
static const float SOC_T[N_PT] = {0.178f,0.281f,0.383f,0.486f,0.589f,0.692f,0.794f,0.897f,1.0f};
static const float OCV_T[N_PT] = {1.2155f,1.2318f,1.2453f,1.2557f,1.2658f,1.2743f,1.2816f,1.2901f,1.3195f};
static const float R0_T[N_PT] = {0.0452f,0.0410f,0.0384f,0.0353f,0.0337f,0.0325f,0.0313f,0.0305f,0.0301f};
static const float R1_T[N_PT] = {0.0233f,0.0222f,0.0212f,0.0205f,0.0194f,0.0183f,0.0172f,0.0162f,0.0153f};
static const float TAU_T[N_PT] = {8.0f,8.0f,8.0f,8.2f,8.2f,8.2f,8.2f,8.0f,8.2f};
static float lut(const float *y, float soc) {
if (soc <= SOC_T[0]) return y[0];
if (soc >= SOC_T[N_PT-1]) return y[N_PT-1];
int k = 1; while (SOC_T[k] < soc) k++;
float w = (soc - SOC_T[k-1]) / (SOC_T[k] - SOC_T[k-1]);
return y[k-1] + w * (y[k] - y[k-1]);
}
typedef struct { float v1, i_prev; } ecm_state_t;
float ecm_step(ecm_state_t *s, float i_now, float soc, float dt) {
float a = expf(-dt / lut(TAU_T, soc));
s->v1 = s->v1 * a + s->i_prev * lut(R1_T, soc) * (1.0f - a); /* 零阶保持精确解 */
s->i_prev = i_now;
return lut(OCV_T, soc) - i_now * lut(R0_T, soc) - s->v1;
}
十一、总结
| 要点 | 一句话 |
|---|---|
| 模型结构 | OCV(SOC) + R0 + R1∥C1,R0 管"瞬时台阶",R1C1 管"指数弛豫" |
| 离散化 | 用零阶保持精确解,任意采样间隔都稳定 |
| 测试设计 | 静置 → 短脉冲 → 静置,10 Hz 采样,每 10% SOC 一个点 |
| 辨识方法 | R0 取加/卸载台阶平均;τ 网格搜索 + 线性最小二乘;R1 做"未充满修正" |
| 验证结果 | 独立工况 RMSE:Rint 31.2 mV → 一阶 RC 9.3 mV → 二阶 RC 0.7 mV |
| 误差分析 | 用真值参数做对照,判断误差来自参数还是结构 |
| 核心认知 | 辨识窗口决定能看到的时间常数;模型阶数按应用选,一阶 RC 性价比最高 |
整套代码只依赖 numpy 和 pandas,约 80 行核心逻辑。换成你自己的测试仪数据时,只需要保证 CSV 有 t_s、current_A、voltage_V 三列(第 2 篇的解析脚本可以直接输出这个格式),并按电池容量修改 Q_AH 和脉冲阈值 i_th。
我之前写过一篇《镍氢电池 SOC 估算:二阶 RC + 迟滞 + 5 状态 EKF》,有读者问"模型参数从哪来"——答案就是本文这套测试和辨识流程。EKF 的状态方程就是第 2.3 节的离散递推,观测方程就是端电压公式。模型、SOC 估算、云端数字孪生,靠的都是这张参数表。
下一篇预告(物联网):《MCU 用 ADC 测电池电压的 7 个坑》——模型再准,输入的电压测不准也白搭。分压电阻漏电、采样时机、参考电压漂移、ADC 输入阻抗……每个坑都给出现象、原因和 C 代码修正示例。
欢迎在评论区留言:你在做电池建模时用的是一阶还是二阶模型?遇到过哪些"参数怎么调都不准"的情况?
附录:模拟数据生成脚本(gen_data.py)
虚拟电池内部是"二阶 RC + 噪声",真值参数写在脚本里,便于读者自己修改、验证辨识算法。
"""生成模拟实验数据:用"二阶 RC + 噪声"的虚拟电池充当被测对象(真实项目中替换为测试仪数据)"""
import numpy as np, pandas as pd
Q_AH, DT = 2.0, 0.1 # NiMH AA 2000 mAh,10 Hz 采样
rng = np.random.default_rng(7)
def ocv(s): # 虚拟电池的 OCV-SOC 曲线(NiMH 典型形态)
return 1.19 + 0.17*s - 0.07*s**2 + 0.03*np.exp((s-1)/0.05) - 0.08*np.exp(-s/0.05)
def r0(s): return 0.030 + 0.020*(1-s)**2 + 0.010*np.exp(-s/0.1)
def r1(s): return 0.015 + 0.010*(1-s)
TAU1, R2, TAU2 = 8.0, 0.010, 120.0 # 快极化 τ1=8 s;慢扩散 R2=10 mΩ, τ2=120 s
def simulate_truth(current, soc0):
n = len(current); v = np.empty(n); soc = np.empty(n)
s, v1, v2 = soc0, 0.0, 0.0
for k, i in enumerate(current):
a1, a2 = np.exp(-DT/TAU1), np.exp(-DT/TAU2)
v1 = v1*a1 + i*r1(s)*(1-a1)
v2 = v2*a2 + i*R2*(1-a2)
v[k] = ocv(s) - i*r0(s) - v1 - v2
soc[k] = s
s -= i*DT/3600/Q_AH
return v, soc
def measure(current, v): # 加入测量噪声与 0.1 mV 量化
vm = np.round((v + rng.normal(0, 0.0005, len(v))) / 1e-4) * 1e-4
im = current + rng.normal(0, 0.002, len(current))
return vm, im
def seg(i, sec): return np.full(int(round(sec/DT)), i)
if __name__ == "__main__":
# ① HPPC 式脉冲测试:每个 SOC 点 → 2 A 放电脉冲 10 s → 静置 60 s → 1 A 放电 12 min(-10% SOC)→ 静置 30 min
parts = [seg(0, 1800)]
for _ in range(9):
parts += [seg(2.0, 10), seg(0, 60), seg(1.0, 720), seg(0, 1800)]
cur = np.concatenate(parts)
v, soc = simulate_truth(cur, 1.0)
vm, im = measure(cur, v)
pd.DataFrame({"t_s": np.arange(len(cur))*DT, "current_A": im.round(4), "voltage_V": vm.round(4)}).to_csv("pulse_test.csv", index=False)
# ② 验证工况:随机"混合负载"(模拟玩具/IoT/手持设备),从 SOC 90% 开始
levels, parts, total = [0, 0.1, 0.3, 0.6, 1.0, 1.5, 2.0], [], 0
while total < 5400:
sec = rng.uniform(5, 120); parts.append(seg(rng.choice(levels), sec)); total += sec
cur = np.concatenate(parts)
v, soc = simulate_truth(cur, 0.9)
vm, im = measure(cur, v)
pd.DataFrame({"t_s": np.arange(len(cur))*DT, "current_A": im.round(4), "voltage_V": vm.round(4)}).to_csv("drive_test.csv", index=False)
print("pulse_test.csv", len(cur), "drive SOC 结束于", round(soc[-1], 3))
完整代码包(gen_data.py、ecm.py、rc2.py、check.py、plots.py 及数据)见文末附件。
参考资料
- Plett G. L., Battery Management Systems, Volume I: Battery Modeling, Artech House, 2015(等效电路模型与离散化)
- Idaho National Laboratory / U.S. DOE, Battery Test Manual for Electric Vehicles(HPPC 测试方法)
- Hu X., Li S., Peng H., “A comparative study of equivalent circuit models for Li-ion batteries”, Journal of Power Sources, 2012
- numpy 官方文档:
numpy.linalg.lstsq、numpy.interp - 《电池上云笔记》第 1 篇、第 2 篇
- 点赞
- 收藏
- 关注作者
评论(0)