用 Python 实现电池一阶 RC 等效电路模型:参数辨识与仿真

举报
yd_287299255 发表于 2026/10/31 09:14:53 2026/10/31
【摘要】 专栏:《电池上云笔记》第 3 篇 | 分类:开发语言 关键词:等效电路模型、Thevenin 模型、一阶 RC、HPPC、脉冲法、参数辨识、精确离散化、Python、numpy、pandas

用 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,则:

Vt=OCV(SOC)−I R0−V1V_t = \mathrm{OCV}(\mathrm{SOC}) - I\,R_0 - V_1

dV1dt=−V1R1C1+IC1\frac{dV_1}{dt} = -\frac{V_1}{R_1C_1} + \frac{I}{C_1}

SOC(t)=SOC0−1Qn∫0tI dτ\mathrm{SOC}(t) = \mathrm{SOC}_0 - \frac{1}{Q_n}\int_0^t I\,d\tau

恒流 I 放电 t 秒后,极化电压为 V1(t)=IR1(1−e−t/τ)V_1(t) = I R_1\left(1-e^{-t/\tau}\right);卸载后 V1V_1 按 e−t/τe^{-t/\tau} 衰减。这两条指数曲线,就是参数辨识的全部依据。

2.3 离散化:别用前向欧拉

代码里要把微分方程变成递推。很多教程直接写前向欧拉:

V1[k]=V1[k−1](1−Δtτ)+ΔtC1I[k−1]V_1[k] = V_1[k-1]\left(1-\frac{\Delta t}{\tau}\right) + \frac{\Delta t}{C_1} I[k-1]

它在 Δt 远小于 τ 时没问题,但当采样间隔变大(比如云端只存 1 min 一个点,或 MCU 低功耗模式下 10 s 采一次),Δt > 2τ 时会直接发散。

更好的做法是假设两次采样之间电流不变(零阶保持),直接用解析解:

V1[k]=V1[k−1] e−Δt/τ+R1(1−e−Δt/τ)I[k−1]V_1[k] = V_1[k-1]\,e^{-\Delta t/\tau} + R_1\left(1-e^{-\Delta t/\tau}\right) I[k-1]

这个公式对任意 Δt 都无条件稳定且精确(在电流分段恒定的前提下),计算量也只多一个 exp,MCU 上还可以预先把 e−Δt/τe^{-\Delta t/\tau} 算好存起来。后文代码全部使用这种形式。

三、测试设计:HPPC 式脉冲 + 长静置

参数辨识的数据来自脉冲测试,思路借鉴锂电行业常用的 HPPC(Hybrid Pulse Power Characterization)。本文的测试步骤:

  1. 满电后静置 30 min;
  2. 每个 SOC 点执行:2 A 放电脉冲 10 s → 静置 60 s → 1 A 放电 12 min(消耗 10% SOC)→ 静置 30 min;
  3. 重复 9 次,覆盖 SOC 100% → 10%;
  4. 采样率 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:加载瞬间压降和卸载瞬间回升各算一次,取平均,可以抵消一部分噪声和"台阶前后点"不对称的影响:

R0=ΔVon+ΔVoff2IR_0 = \frac{\Delta V_{on} + \Delta V_{off}}{2I}

③ τ 与 R1:卸载后的弛豫段满足

V(t)=V∞−A e−t/τ,A=IR1(1−e−Tp/τ)V(t) = V_\infty - A\,e^{-t/\tau}, \qquad A = I R_1\left(1-e^{-T_p/\tau}\right)

这里有个容易忽略的点:10 s 脉冲并没有让 RC 支路"充满"(τ=8 s 时只充到 1−e^(−10/8) ≈ 71%),如果直接用 A/I 当 R1,会低估约 30%。所以要除以 (1−e−Tp/τ)(1-e^{-T_p/\tau}) 做修正。

④ C1:C1=τ/R1C_1 = \tau / R_1。

拟合方法上,这是一个"一个非线性参数 τ + 两个线性参数 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 的变化,虚线为真值,圆点为辨识结果。

从表中可以读出三点:

  1. OCV 误差 < 0.6 mV,R0 误差 < 0.6 mΩ,R1 误差 < 0.4 mΩ,τ1 误差 < 3%。在 0.5 mV 噪声、0.1 mV 量化的条件下,这个精度说明方法是可靠的;
  2. 低 SOC 时内阻明显上升(R0 从 30 mΩ 升到 45 mΩ,R1 从 15 mΩ 升到 23 mΩ)。这正是电池"快没电时一用大电流就关机"的原因——不只是 OCV 低了,压降也更大了;
  3. 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,长放电让慢支路充分建立,长静置让它完整衰减。对这段弛豫曲线做双指数拟合:

V(t)=V∞−A1e−t/τ1−A2e−t/τ2V(t) = V_\infty - A_1 e^{-t/\tau_1} - A_2 e^{-t/\tau_2}

方法和一阶一样:τ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 条建议

  1. R0 对采样率最敏感。1 Hz 采样时"台阶后第一个点"已经包含了部分极化,R0 会偏大。辨识 R0 建议 ≥ 10 Hz,有条件用 100 Hz 以上;
  2. 静置时间决定能看到的时间常数。经验上静置时长 ≥ 5τ;想看 τ2 = 120 s 的过程,至少要 10 min;
  3. 参数强烈依赖温度。R0、R1 在 0 ℃ 时可能是 25 ℃ 的 2~3 倍。产品级模型要做成 SOC × 温度的二维表;
  4. NiMH 有明显的电压迟滞:充电后和放电后的 OCV 不同,本文只做了放电方向,充电工况要单独辨识,或者在模型里加迟滞项;
  5. 参数表就是 MCU 上的查找表。9 个 SOC 点 × 4 个参数 = 36 个 float,不到 150 字节,查表 + 一次指数递推,Cortex-M0 也轻松跑;
  6. 离散化系数可以预计算。采样周期固定时,a = exp(-Δt/τ) 可以离线算好存表,MCU 上连 exp 都省了;
  7. 用独立工况验证,并记录误差指标。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 及数据)见文末附件。


参考资料

  1. Plett G. L., Battery Management Systems, Volume I: Battery Modeling, Artech House, 2015(等效电路模型与离散化)
  2. Idaho National Laboratory / U.S. DOE, Battery Test Manual for Electric Vehicles(HPPC 测试方法)
  3. Hu X., Li S., Peng H., “A comparative study of equivalent circuit models for Li-ion batteries”, Journal of Power Sources, 2012
  4. numpy 官方文档:numpy.linalg.lstsq、numpy.interp
  5. 《电池上云笔记》第 1 篇、第 2 篇
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。