2026/10/22 6:31:48

EKF与CKF在锂电池SOC估算中的C语言实现与对比

EKF与CKF在锂电池SOC估算中的C语言实现与对比 1. 从开发经历讲起为什么需要同时对比EKF和CKF做BMS电池管理系统相关开发的同行应该都有体会SOC荷电状态估算是整个系统里最让人头疼的一块。安时积分法简单但电流传感器一有偏置误差积分下去SOC就不知道飘到哪里去了查OCV表法在磷酸铁锂这种电压平台非常平的电池上又几乎没法用SOC从20%到80%电压变化都不到100mV查表误差大得离谱。所以现在稍微正规一点的项目都会上卡尔曼滤波这一类的状态观测器。我最早接触SOC估算的时候第一反应当然是扩展卡尔曼滤波EKF毕竟资料多、案例多论坛上随便一搜就是一堆代码。后来在仿真里跑着跑着发现有些工况下EKF在状态突变或者强非线性段会抖得比较厉害于是开始研究容积卡尔曼滤波CKF。CKF相比EKF不需要算雅可比矩阵理论上在强非线性下精度更好但代价是计算量大、代码也更绕。抱着“都是卡尔曼家族干脆放一起对比”的想法我基于C语言搭了一套完整的锂电池SOC计算仿真模型把EKF和CKF都塞了进去并且针对参数处理做了“定参”和“FF”两条路线。所谓定参就是离线辨识出电池模型参数后固定使用FF则是在线递推辨识实时更新模型参数。文章后面我会把两种滤波器的C实现细节、参数辨识的数据组织方式、仿真结果对比和嵌入式部署的坑一次性说完希望能给正在做SOC估算或者准备从MATLAB模型往C代码迁移的朋友一些参考。这套模型适用于谁如果你只是想在Simulink里拖拖模块可能没必要看C版本但如果你像我一样要做MCU落地或者想知道EKF和CKF在同样条件下到底谁强谁弱这篇应该能省你不少弯路。模型本身也不复杂二阶RC等效电路模型加上两种滤波器所有代码我都是纯C写的数据结构用结构体封装跑在普通Cortex-M4级别的单片机上也能满足实时性要求。2. 电池等效电路模型与“定参/FF”的参数处理2.1 二阶RC模型与离散状态方程SOC估算的第一步不是滤波器而是电池模型。我用的是最常见的二阶RC等效电路模型一个理想电压源OCV开路电压随SOC变化串联欧姆内阻R0再串两个RC并联网络分别模拟电化学极化浓度差和极化电压的动态特性。这个模型在精度和计算量之间比较均衡工程上用得最多。离散化之后的状态方程是SOC(k) SOC(k-1) - (η * Δt / Qmax) * I(k-1) U1(k) exp(-Δt/τ1) * U1(k-1) R1 * (1 - exp(-Δt/τ1)) * I(k-1) U2(k) exp(-Δt/τ2) * U2(k-1) R2 * (1 - exp(-Δt/τ2)) * I(k-1)其中Qmax是最大可用容量η是库仑效率充电时小于1放电时近似为1τ1R1C1τ2R2C2。观测方程就是端电压等于OCV减去欧姆压降再减去两个RC网络的电压U(k) OCV(SOC(k)) - I(k)*R0 - U1(k) - U2(k)这里最麻烦的是OCV(SOC)这个函数。它不是线性的而是像一条S曲线。C语言里我用的是一个查表加线性插值的方法标定时每隔2% SOC取一个OCV点存成数组运行时根据当前SOC查表插值。这个表在定参模式下是固定不变的在FF模式下则会被在线辨识结果不断修正。2.2 离线定参与在线遗忘因子递推辨识的分工参数辨识这块我做了两条路线对应标题里的“定参”和“FF”。定参路线好理解就是先用HPPC混合脉冲特性实验或者模拟工况下采集的电压电流数据用最小二乘拟合出R0、R1、C1、R2、C2这五个参数。拟合完成之后这些参数在模型运行过程中保持不变。好处是代码简单、逻辑清晰坏处是电池老化或者温度剧烈变化之后参数不再准确模型精度会下降。FF路线用的是带遗忘因子Forgetting Factor的递推最小二乘FFRLS在模型运行过程中实时用当前电压电流数据更新欧姆内阻和RC参数。遗忘因子的作用类似一个滑动窗口越靠近当前时刻的数据权重越高让参数能跟着电池状态慢慢演化。一般来说FF的取值在0.95到0.995之间取值越小参数更新越快但对噪声越敏感取值越大参数越稳定但跟踪速度越慢。我实测下来对于动态工况比较频繁的电池包FF取0.98左右是个不错的平衡点。有没有必要两条路线都做我的看法是如果你做的是量产BMS最好两条都保留。出厂初期用定参跑参数稳定、故障率低跑到一定循环次数之后切换到FF在线更新能显著降低SOC估算误差。仿真模型里两者可以一键切换方便对比。2.3 参数辨识在C工程中的数据组织方式在C语言里我把电池参数封装成一个结构体方便两种模式共用typedef struct { float R0; // 欧姆内阻单位mΩ float R1; // 电化学极化内阻 float C1; // 电化学极化电容单位kF float R2; // 浓度差极化内阻 float C2; // 浓度差极化电容单位kF float Qmax; // 最大可用容量单位Ah uint8_t mode; // 0定参1FF在线辨识 } BatteryParams_t;定参模式下这个结构体里的值在初始化时一次性写入后面算法只读不写。FF模式下每次采样周期结束FFRLS模块会更新R0、R1、R2等参数然后写回这个结构体下一拍卡尔曼滤波再用新参数。这里有一个关键点参数的更新频率不能和滤波周期完全一样不然容易造成数值震荡。我的做法是参数辨识跑得慢一点比如每100ms更新一次参数而EKF/CKF每20ms跑一步这样各自的时间常数不会互相干扰。3. EKF在C语言中的落地状态预测、雅可比矩阵与数值稳定3.1 EKF的状态方程和观测方程设计EKF的思路是把非线性的电池模型在当前工作点做一阶泰勒展开得到线性近似然后套用标准卡尔曼滤波的预测-更新框架。状态变量我选的是SOC、U1、U2三个状态向量x [SOC, U1, U2]^T。控制输入是电流I观测量是端电压U。预测步就是直接把上一拍的状态代入离散方程// 状态预测 float soc_hat x[0] - (eta * dt / params.Qmax) * I_meas; float u1_hat exp(-dt / tau1) * x[1] params.R1 * (1 - exp(-dt / tau1)) * I_meas; float u2_hat exp(-dt / tau2) * x[2] params.R2 * (1 - exp(-dt / tau2)) * I_meas;协方差矩阵的预测是P A * P * A^T Q这里A就是状态转移矩阵。因为三个状态里SOC的转移方程不涉及U1和U2U1和U2也不互相耦合所以A是一个对角占优的3x3矩阵A [1, 0, 0; 0, exp(-dt/τ1), 0; 0, 0, exp(-dt/τ2)]实际代码里不需要真的做3x3矩阵乘法直接用标量乘法和加法就能算完。这个细节看起来不起眼在算力有限的MCU上能省不少时间。3.2 雅可比矩阵的解析推导与C实现EKF和标准KF最大的不同就是这里观测方程OCV(SOC)是非线性的所以要把观测方程对状态向量求偏导得到雅可比矩阵H。在二阶RC模型里观测方程对SOC的偏导就是OCV曲线的斜率dOCV/dSOC这也是EKF实现中最重要的一个值。我分两种方式处理解析式和数值差分。解析式需要提前拟合一个OCV(SOC)的多项式函数比如5阶多项式然后传进去一个SOC值就能算出导数。优点是计算快、数值稳定缺点是多项式拟合在高SOC和低SOC两端容易过冲导致OCV曲线失真。数值差分更简单给SOC加一个微小增量epsilon用(OCV(SOCepsilon) - OCV(SOC-epsilon)) / (2*epsilon)来近似斜率。优点是只要OCV查表函数写得对任何SOC点都能用缺点是epsilon选不好会引入数值误差。我实测下来epsilon取0.001比较合适再小的话单精度浮点下容易消掉有效位。C代码里我实际用的是数值差分配合查表插值函数float ocv_slope(float soc) { float eps 0.001f; float ocv_plus get_ocv_by_soc(soc eps); float ocv_minus get_ocv_by_soc(soc - eps); return (ocv_plus - ocv_minus) / (2.0f * eps); }这样H矩阵就是H [dOCV/dSOC, -1, -1]注意U1和U2对应的偏导都是-1因为端电压是减去这两个值的。如果这里符号搞错滤波结果会直接发散而且是那种怎么调Q/R都救不回来的发散。3.3 实测中最容易踩的两个数值坑第一个坑是协方差矩阵P的非对称和负定问题。EKF迭代久了由于浮点舍入误差P矩阵会慢慢变得不对称甚至出现特征值为负的情况。P一旦不正定滤波增益K就失去意义整个估计就乱了。解决方法是每个滤波周期结束后强制对称化P (P P^T) / 2。三个状态变量3x3矩阵对称化运算量不大但能显著提升长期运行的稳定性。第二个坑是初值敏感性。EKF的协方差初始值P0如果给得太小滤波器会“过于自信”后面测量值怎么拉都拉不回来给得太大前几步的估计值会剧烈波动。我的经验是P0对角线取1到10之间对应状态变量可能的不确定度。SOC初值的误差如果大于20%EKF通常需要几十秒才能收敛这是正常现象不要一看到前期误差大就以为是代码写错了。还有一个容易被忽略的是Q矩阵和R矩阵的匹配问题。Q是过程噪声协方差反映模型误差和电流噪声R是测量噪声协方差反映电压传感器噪声。Q给得太大滤波结果会跟着测量值剧烈跳动Q给得太小滤波结果会很平滑但响应迟钝。我习惯先固定R再根据实际电流噪声水平反向调整Q这样调试起来更有方向感。4. CKF在C语言中的落地容积点传播、Cholesky分解与性能表现4.1 CKF的核心思想容积点绕开雅可比CKF和EKF走的完全是另一条路不推导雅可比矩阵而是用一组确定性的采样点容积点来近似状态分布经过非线性变换后的结果。我最早看CKF的推导也是一头雾水后来想通了本质就是一个近似积分的问题用多个采样点经过非线性函数传播后的均值和协方差来近似真实分布的均值和协方差。对n维状态向量CKF需要2n个容积点。我这里状态量是3维所以共6个容积点。这些点对称分布在状态均值两侧权重都相同1/2n。采样点生成完之后通过非线性状态方程和观测方程传播再统计传播后样本的加权均值和加权协方差。CKF不需要计算雅可比矩阵这一点在实现上其实是一种解脱。EKF要求你对电池模型做线性化推导模型一改就要重新推一遍改个OCV拟合函数可能就要把导数重算一次。CKF只需要你把状态方程和观测方程写对剩下的交给采样点去传播工程维护成本低很多。4.2 容积点生成、传播与C代码骨架CKF的第一步是生成容积点公式是X_i x_k sqrt_chol(P_k) * zeta_i其中zeta_i是基本容积点集。3维状态下基本容积点是zeta { [1,0,0]^T, [-1,0,0]^T, [0,1,0]^T, [0,-1,0]^T, [0,0,1]^T, [0,0,-1]^T } * sqrt(3)sqrt_chol(P_k)是协方差矩阵P_k的Cholesky分解下三角矩阵。这一步是CKF里面最绕的代码Cholesky分解要求P_k正定否则直接报错。我在C语言里实现的时候就用了一般的3x3矩阵Cholesky分解函数// 3x3 Cholesky分解L满足 P L * L^T bool cholesky_3x3(const float P[3][3], float L[3][3]) { float temp; // L[0][0] if (P[0][0] 0) return false; L[0][0] sqrtf(P[0][0]); // L[0][1], L[0][2] L[0][1] P[0][1] / L[0][0]; L[0][2] P[0][2] / L[0][0]; // L[1][1] temp P[1][1] - L[0][1] * L[0][1]; if (temp 0) return false; L[1][1] sqrtf(temp); // L[1][2] L[1][2] (P[1][2] - L[0][1] * L[0][2]) / L[1][1]; // L[2][2] temp P[2][2] - L[0][2] * L[0][2] - L[1][2] * L[1][2]; if (temp 0) return false; L[2][2] sqrtf(temp); L[1][0] L[2][0] L[2][1] 0.0f; return true; }容积点通过状态方程传播之后计算采样均值和协方差得到预测值。然后每个容积点再过一遍观测方程得到预测的电压值再计算新息协方差和互协方差最后算出卡尔曼增益并更新状态。整体骨架和EKF很像只是EKF用雅可比矩阵算协方差而CKF用传播后的采样点来算。4.3 CKF在MCU上的算力开销评估CKF计算量确实比EKF大不少但具体大多少还是需要有量化概念。我在模型里加了计时统计跑在一块主频168MHz的Cortex-M4芯片上纯浮点运算的时间大概是算法单次滤波周期耗时(μs)占用RAM(状态相关)是否需要雅可比EKF约28约1.2KB需要CKF约65约3.5KB不需要也就是说CKF的耗时大概是EKF的2.3倍左右。这主要是因为每个容积点都要单独传播状态方程和观测方程6个点就相当于6次非线性函数调用。如果你的控制周期是100ms那么这点开销完全不是问题但如果你的BMS控制周期只有10ms或者MCU主频不高就需要认真考虑选型了。内存上CKF额外需要保存6个容积点的状态向量和对应的观测值3维状态下也就是多几十个浮点数还不至于吃紧。主要压力在Cholesky分解这一块但3x3矩阵的临时变量非常有限所以我对CKF“内存开销巨大”的说法持保留态度——至少对3阶系统来说完全可控。5. 仿真实验设置与结果对比精度、收敛速度、鲁棒性5.1 仿真场景设计初值偏差、电流噪声、动态工况模型搭好之后不能只看理想工况下的表现那没有说服力。我设计了三组仿真场景来压测两种滤波器。第一组是初值偏差测试SOC真实初始值是90%但滤波器初值故意设成70%看算法要用多久把误差收回来。这个测试很重要因为实际装车时睡眠唤醒后的SOC往往存在一定偏差收敛速度直接决定用户体验。第二组是电流噪声测试在电流采样上叠加高斯白噪声标准差取满量程的2%看两种算法对噪声的抑制能力差异。第三组是动态工况测试跑了一段类似城市工况的随机电流序列高低电流跳变频繁观察两种算法的跟踪精度。这三组测试统一下来基本能看出两种算法的性格差异。5.2 EKF与CKF的结果对比表格我把三组试验的稳态SOC误差统计滤波收敛之后的平均绝对误差整理成一张表测试场景EKF稳态误差(%)CKF稳态误差(%)说明理想工况0.80.5CKF略优差距不大初值偏移20%收敛约30s期间最大误差12%收敛约18s期间最大误差8%CKF收敛明显更快电流噪声2%1.81.1CKF噪声抑制能力更好动态工况2.21.4高动态下CKF优势更明显这个结果和我预期基本一致如果电池工作在温和的充放电条件下EKF和CKF差别不大杀鸡用牛刀完全没必要。但在强噪声、剧烈动态、初值不准这三大“实战条件”下CKF的优势就体现出来了。原因也不难理解EKF在强非线性区域用一阶线性近似误差直接反映在估计上而CKF的采样点传播保留的高阶信息更多系统越偏离线性CKF的优势就越明显。需要注意这里说的“优势明显”是相对概念不是CKF把EKF碾压。如果你看绝对值EKF在动态工况下稳态误差2.2%很多BMS项目也是能接受的而CKF的1.4%确实更漂亮但代价是计算量翻倍。选型时这不是一个纯技术问题而是成本问题。5.3 定参与FF模式下模型参数变化对SOC估计的影响仿真里我还做了一个对比定参模式下模型参数固定不变FF模式下参数每拍更新遗忘因子0.98。在跑满2000个循环模拟老化场景后定参模式的SOC估算误差从1%左右慢慢涨到3%以上这说明模型参数已经和真实电池状态不匹配了而FF模式由于参数在线更新稳态误差一直保持在1.5%以内几乎没有随老化而明显变差。但FF模式也不能一味迷信。我把遗忘因子调成0.95之后参数更新过快在电流长时间为0的平台期会出现参数漂移R0的值跳来跳去反而让SOC估计变差。这就是所谓“在线辨识双刃剑”老化跟踪和参数漂移之间的平衡完全靠遗忘因子这个旋钮来控制。工程上比较稳妥的做法是大电流放电时允许FFRLS更新小电流或静置时冻结参数更新避免在数据信息量不足时乱调参数。6. 嵌入式部署时的注意事项与我的个人经验6.1 代码结构、定点化与实时性优化在MCU上落地这套模型我总结了几个优化点。第一所有矩阵维度在编译期固定不用动态内存分配。EKF和CKF的状态维度都是3容积点数量是6这些可以全部用宏定义或者枚举写死。动态内存分配在嵌入式BMS里是禁忌一旦内存碎片化后果很难排查。第二能用标量运算的地方绝不写通用矩阵函数。比如状态预测公式里每个状态变量的更新都可以拆成独立的标量表达式不必套用3x3矩阵乘法框架。我最初写代码时图省事调了通用的矩阵乘法库结果跑1000次仿真耗时是优化后的近2倍。后来改成手动展开的标量运算只保留真正的矩阵运算比如P矩阵更新用通用函数。第三如果需要进一步压算力可以把关键计算从float改成定点数。定点化之后的精度损失对于SOC估算来说通常可以接受但需要仔细设计Q格式避免中间变量溢出。不过我的建议是先确认float版本能不能满足实时性要求能满足就别折腾定点化那是个大坑。6.2 协方差发散与状态异常的工程处理无论是EKF还是CKF跑久了都可能出现协方差矩阵P发散的情况表现就是P的对角线元素变得极大或者变成NaN。这个问题在电池老化、传感器断线、电流突变等场景下更容易触发。我在代码里加了一个协方差钳位函数每N个周期检查P矩阵对角线如果某个值超过预设阈值比如远大于正常值的一个经验上限就把整个P重置为初始化矩阵P0。这算是一种“粗暴但有效”的恢复策略。同时每次状态更新之后检查估计的SOC值是否落在0到100%区间内超出范围就用上一拍的SOC值替代避免异常值被积分进滤波器状态。另一个容易被忽视的是电流传感器失效的保护。如果电流一直保持在异常值卡尔曼滤波会跟着错误输入走SOC很快就偏了。我实现里加入了电流变化率异常检测如果连续几个周期电流变化率超过物理上限就冻结滤波更新只保留电压观测校正等技术员处理。6.3 关于选型到底该用EKF还是CKF这个问题我被问过很多次每次我的回答都是反问一句你的应用场景是什么如果你做的是储能电站BMSSOC不需要高频更新系统有充足的算力冗余电池长期工作在相对稳定的工况下那么EKF完全够用代码简单、调试方便、现场好维护。如果你的目标是电动汽车BMS电流动态大、启动阶段SOC偏差可能很大、还有快充这种强非线性场景那么建议上CKF虽然前期工程量多一些但多出来的那分精度在充电策略和续航估算上都能体现价值。如果你和我一样卡在中间——算力一般又想提升精度——可以先按EKF把整个链路跑通然后用同一套电池模型和数据结构把滤波器核心换成CKF甚至可以做成编译期宏开关两套都保留实测后灵活切换。我最终交付的模型就是这样做的。6.4 最后再分享两个小技巧一个是OCV插值表的边界处理。查表OCV的时候SOC小于0或者大于1属于非法区间但滤波过程中估计值可能短暂越界。不要直接硬截断到边界而是做线性外推这样可以避免OCV函数在边界处出现斜率突变影响雅可比矩阵和CKF的容积点传播。另一个是滤波周期和参数辨识周期的分离。不要为了省事把两个周期设成完全一样。我踩过的坑是FFRLS参数更新太快导致模型参数每拍跳变卡尔曼滤波的Q矩阵意义就被削弱了。后来把参数更新降频到滤波周期的5倍之后系统稳定了很多。这个经验不一定适合所有场景但值得做成一个可配置参数方便标定时自己尝试。这套C语言仿真模型我已经跑了大半年从最初MATLAB原型到嵌入式移植踩过的坑基本都是上面这些。希望这篇东西能帮你少走一点弯路如果你手头也正在做锂电SOC估算欢迎把实测结果和参数设置拿出来交流数据比理论更有说服力。