1. 为什么是DEKF:状态和参数捆在一起估计时,单滤波器根本玩不转
1.1 联合EKF的维度灾难与耦合问题
做状态估计的工程朋友应该都有过这种体验:系统模型里有几个参数拿不准,于是顺手把它们塞进状态向量,搞一个联合EKF(Joint EKF,也叫增广EKF),觉得"反正都是估计,多估几个量有什么关系"。结果一跑仿真就傻眼——状态看着收敛了,参数却一直在漂;或者参数刚有点收敛的苗头,状态又开始抖。你开始怀疑是不是Q矩阵没调对,然后陷入"调Q、调初值、再看曲线、再调Q"的死循环。
以我之前做过的一个弹簧-质量-阻尼系统参数辨识为例。质量m已知,刚度k和阻尼c未知,想同时估计位移x1、速度x2以及k、c。联合EKF的状态向量是[x1, x2, k, c]^T,4维。状态转移函数的雅可比矩阵变成4×4,而且最关键的是,k和c在状态方程里是以乘积形式混进位移和速度通道的。这意味着参数和状态在协方差矩阵里强耦合,你单独调整任何一个通道的P矩阵初值,都会牵一发动全身。
联合EKF还有一个更隐蔽的问题——可辨识性和激励相关。当系统输入激励不足时,P矩阵里对应参数的位置虽然会因为Q的注入缓慢增长,但卡尔曼增益算出来接近零,滤波器"固执地"认为参数已经收敛了,实际上参数完全不可观测。你盯着误差曲线看,状态误差很小,以为一切正常,可一旦工况改变,模型立刻失真。
1.2 DEKF的总体框架:两个滤波器各管一摊
DEKF(Dual Extended Kalman Filter,双扩展卡尔曼滤波)的思路跟联合EKF完全不同。它不把参数塞进同一个状态空间,而是用两套EKF并行跑:状态滤波器(State EKF)把参数当成已知量,只处理状态估计;参数滤波器(Parameter EKF)把状态当成已知量,只处理参数更新。两个滤波器在每个采样周期交换一次信息——状态滤波器用最新的参数估计值做预测,参数滤波器用最新的状态估计值做残差修正。
这种解耦的好处是实打实的。首先,每个滤波器的状态维度都降低了一半,协方差矩阵的尺寸小了,矩阵运算的数值稳定性明显改善。其次,你可以在两个滤波器里分别设置过程噪声和测量噪声,参数通道和状态通道的Q、R可以独立调节,整定灵活性高了很多。最后,从工程调试角度看,你可以分别观察状态滤波器和参数滤波器的创新序列,快速定位是哪一路出了问题。
当然,DEKF也有代价。两套滤波器级联之后,误差传播路径变长了。参数滤波器给出的参数如果偏了,状态滤波器会带着偏差做预测;状态滤波器的估计残差又会反过来污染参数滤波器的更新。这就是常说的"互相污染"问题,我在第5节会专门展开讲怎么缓解。
1.3 DEKF适合解决的典型工程场景
在实际工程里,DEKF最常见的用武之地有三类:一是电池管理系统里的SOC和容量/内阻联合估计,SOC(状态)随电流积分变化,容量和内阻(参数)随寿命和温度缓慢漂移;二是电机控制里的转子位置/转速(状态)与定子电阻、磁链(参数)联合辨识;三是车辆动力学里的车速、质心侧偏角(状态)与质量、坡度(参数)的联合估计。
这些场景的共同特点是:状态方程里含有不确定参数,而且参数随时间缓变或随工况跳变;你既需要高精度的状态估计,又需要跟踪参数的变化趋势。DEKF把这两个需求拆开处理,是这类问题里工程上最稳妥的解法之一。接下来,我会用Simulink完整搭建并验证这套流程,把每一步的关键逻辑讲清楚。
2. DEKF递推方程与灵敏度传播:从论文公式到可落地的离散化
2.1 状态滤波器与参数滤波器的完整递推式
在Simulink里写DEKF之前,先把递推式捋清楚。假设离散非线性系统为:
x_k = f(x_{k-1}, u_{k-1}, θ) + w_{k-1} y_k = h(x_k, u_k, θ) + v_k
其中x是状态,u是输入,θ是未知参数,w和v为互不相关的高斯白噪声,协方差分别为Q_x和R_x。
状态滤波器的递推和标准EKF几乎一样,只是在预测时把参数替换成最新的参数估计值θ̂:
x̂_k^- = f(x̂_{k-1}, u_{k-1}, θ̂_{k-1}) P_x,k^- = A_{k-1} P_x,k-1 A_{k-1}^T + Q_x
其中A_{k-1} = ∂f/∂x,在(x̂_{k-1}, θ̂_{k-1})处求值。测量更新:
K_x,k = P_x,k^- H_x^T (H_x P_x,k^- H_x^T + R_x)^(-1) x̂_k = x̂_k^- + K_x,k (y_k - h(x̂_k^-, θ̂_{k-1})) P_x,k = (I - K_x,k H_x) P_x,k^- (I - K_x,k H_x)^T + K_x,k R_x K_x,k^T
H_x是输出函数对状态的雅可比。P_x的更新写成Joseph形式而不是简单的(I-KH)P^-,是为了在浮点运算中更好地维持对称正定性,这个细节后面细说。
参数滤波器这边,参数的演化模型通常假设为随机游走:θ_k = θ_{k-1} + r_{k-1},其中r是过程噪声,协方差为Q_θ。因此参数预测这一项就是:
θ̂_k^- = θ̂_{k-1} P_θ,k^- = P_θ,k-1 + Q_θ
参数滤波器的测量更新用的是同一个测量值y_k,但残差计算要借助状态滤波器的后验估计x̂_k:
K_θ,k = P_θ,k^- H_θ,k^T (H_θ,k P_θ,k^- H_θ,k^T + R_θ)^(-1) θ̂_k = θ̂_{k-1} + K_θ,k (y_k - h(x̂_k, θ̂_{k-1})) P_θ,k = (I - K_θ,k H_θ,k) P_θ,k^- (I - K_θ,k H_θ,k)^T + K_θ,k R_θ K_θ,k^T
这里的关键就在H_θ,k。它不只是输出函数对参数的显式偏导,还包含状态估计对参数的隐式依赖,即:
H_θ,k = ∂h/∂θ + H_x · L_k
其中L_k = ∂x̂_k/∂θ被称为灵敏度矩阵,描述了"参数变化一丁点,状态估计会跟着变多少"。这层关系不处理好的话,DEKF的参数更新方向就是错的。
2.2 灵敏度项:为什么"简化版DEKF"容易翻车
网上很多DEKF的简化实现直接把H_θ写成∂h/∂θ,把L_k忽略掉。如果输出方程显式包含参数(比如y = k·x1),那还好说;但如果参数只在状态方程里出现,测量方程不直接含参数,比如我们这里的弹簧阻尼系统,测量只有位移y = x1,∂h/∂θ = 0。这时候如果无视L_k,参数滤波器的H_θ就是零矩阵,参数更新完全失效。
L_k的递推需要分两步。先看预测步,对x̂_k^- = f(x̂_{k-1}, θ̂_{k-1})两边求θ的偏导:
L_k^- = A_{k-1} L_{k-1} + B_θ,k-1
其中B_θ,k-1 = ∂f/∂θ,在(x̂_{k-1}, θ̂_{k-1})处求值。再看更新步,对x̂_k = x̂_k^- + K_x,k(y_k - h(x̂_k^-))求θ的偏导,通常忽略K_x,k对θ的依赖(这是二阶小量,标准DEKF推导里都这么做),得到:
L_k = (I - K_x,k H_x) L_k^-
于是最终:
H_θ,k = H_x · L_k
在实际代码里,这个递推要多维护一个n_x×n_θ的矩阵变量。如果你用的是联合EKF,根本不会有这个额外的矩阵;但DEKF要想在参数慢变的同时保持稳定,这个灵敏度矩阵是必须的。我的经验是,跳过灵敏度传播的"简化DEKF"在小范围参数摄动下偶尔能跑通,但一旦参数发生阶跃变化,参数滤波器的收敛方向就会乱掉,估计值直接飞出去。
2.3 连续模型离散化与雅可比矩阵的计算选择
在Simulink里搭被控对象通常用连续积分器,但DEKF递推本身是离散的,必须做离散化。弹簧-质量-阻尼系统的状态方程为:
dx1/dt = x2 dx2/dt = (F - k·x1 - c·x2)/m
用前向欧拉离散化,取采样周期Ts:
x1_k = x1_{k-1} + Ts·x2_{k-1} x2_k = x2_{k-1} + Ts·(F_{k-1} - k·x1_{k-1} - c·x2_{k-1})/m
对应的两个雅可比矩阵为:
A = [1, Ts; -Ts·k/m, 1 - Ts·c/m]
B_θ = ∂f/∂θ = [0, 0; -Ts·x1/m, -Ts·x2/m]
A是一个2×2矩阵,B_θ是一个2×2矩阵(列分别对应k和c)。这种手算出来的解析雅可比,在Simulink的MATLAB Function块里直接硬编码就行,效率和精度都比数值差分好很多。
有一点要提醒:如果你用Symbolic Math Toolbox自动求雅可比,注意离散化后的表达式里x1、x2是上一时刻的状态,不要代入成了当前时刻,否则灵敏度递推的时序会错。我见过不止一次这种"差一个时刻"的bug,表现是滤波结果基本正常,但参数估计有微小的相位滞后,很难排查。
3. Simulink建模路线:MATLAB Function快原型,S-Function做工程化
3.1 三条路线对比
在Simulink里实现DEKF,主流有三条路:MATLAB Function块、Level-2 S-Function、以及纯Simulink原生模块搭矩阵运算。我直接给结论:验证阶段首选MATLAB Function,工程化落地阶段转S-Function或生成C代码,纯原生模块搭建只有教学意义,别在生产模型里折腾。
三条路线的核心差异见下表:
| 路线 | 开发效率 | 仿真性能 | 代码生成支持 | 调试友好度 | 适用阶段 |
|---|---|---|---|---|---|
| MATLAB Function | 高 | 中等 | 可生成C/C++ | 高,可直接断点 | 算法验证、快速原型 |
| Level-2 S-Function | 中 | 高 | 需封装TLC | 中 | 复杂系统集成 |
| 原生Simulink模块 | 低 | 低 | 一般 | 低 | 教学演示 |
MATLAB Function最爽的地方在于persistent变量可以直接保存协方差矩阵和灵敏度矩阵,不用像原生模块那样把状态拉出一堆连线。它的另一个优势是在Simulink里能直接调用MATLAB丰富的矩阵函数,调试时双击进编辑器就能设断点,变量工作区一目了然。
如果目标是嵌入式部署,建议在验证完成后把MATLAB Function里的核心递推封装成S-Function,或者直接生成C代码。MATLAB Function本身也支持C代码生成,但前提是里面不能有动态数组、不能用某些不支持的函数,矩阵求逆也要替换成固定尺度的实现。这个细节我在第6节的工程扩展里再说。
3.2 推荐架构与MATLAB Function核心代码骨架
我推荐的Simulink验证架构分三层:最左边是被控对象模型(连续系统,用真实参数),中间是DEKF估计器(MATLAB Function块),最右边是性能监测模块(示波器、误差计算)。输入信号源和测量噪声源各自独立,这样便于分别控制激励条件和噪声水平。
DEKF的核心代码骨架如下,这段代码可以直接粘进MATLAB Function块:
function [xhat, thetahat] = DEKF_update(F, y, Ts) % DEKF: 同时估计弹簧质量阻尼系统的状态和参数 % 状态 x = [位移; 速度], 参数 theta = [刚度k; 阻尼c] persistent x Px th Pth L m = 2; % 已知质量 if isempty(x) x = [0; 0]; Px = diag([1e-2, 1e-2]); th = [60; 3]; % 初始猜测,故意离真值远 Pth = diag([200, 10]); % 参数初始不确定度 L = zeros(2, 2); % 灵敏度矩阵 end % 噪声协方差阵 Qx = diag([1e-6, 1e-4]); Rx = 1e-6; Qth = diag([1e-4, 1e-6]); Rth = 1e-4; % 状态滤波器预测 k = th(1); c = th(2); x_pred = x + Ts * [x(2); (F - k*x(1) - c*x(2))/m]; A = [1, Ts; -Ts*k/m, 1 - Ts*c/m]; Px_pred = A * Px * A' + Qx; % 状态滤波器更新 H = [1, 0]; % 测量位移 Sx = H * Px_pred * H' + Rx; Kx = Px_pred * H' / Sx; inn = y - H * x_pred; x_new = x_pred + Kx * inn; Px_new = (eye(2) - Kx*H) * Px_pred * (eye(2) - Kx*H)' + Kx * Rx * Kx'; % 灵敏度矩阵递推 Bth = [0, 0; -Ts*x(1)/m, -Ts*x(2)/m]; L_pred = A * L + Bth; L_new = (eye(2) - Kx*H) * L_pred; % 参数滤波器 Pth_pred = Pth + Qth; Hth = H * L_new; % 输出不显含theta Sth = Hth * Pth_pred * Hth' + Rth; Kth = Pth_pred * Hth' / Sth; th_new = th + Kth * (y - H * x_new); Pth_new = (eye(2) - Kth*Hth) * Pth_pred * (eye(2) - Kth*Hth)' + Kth * Rth * Kth'; % 保证对称性 Px_new = (Px_new + Px_new') / 2; Pth_new = (Pth_new + Pth_new') / 2; % 保存状态 x = x_new; Px = Px_new; th = th_new; Pth = Pth_new; L = L_new; xhat = x; thetahat = th; end有几个细节值得解释。第一,状态滤波器的P更新用了Joseph形式而不是(I-KH)P^-,因为Joseph形式在浮点计算里对舍入误差的抵抗力更强,协方差不容易退化。第二,参数滤波器的更新残差用的y - H*x_new,注意这里用的是状态更新后的x_new,不是预测值,DEKF的标准结构就是参数滤波器观察状态滤波器的后验估计。第三,灵敏度矩阵的递推里Bth用了老状态x(1)、x(2),这在时序上是对的,因为∂f/∂θ在上一时刻的工作点求值。
MATLAB Function块的采样时间务必设置成离散的Ts,否则仿真用变步长时它会被乱调用。具体做法是在函数块的属性里把Sample Time设为Ts,或者把它放进一个Triggered Subsystem里,用固定周期的脉冲触发。
4. 验证场景设计:怎么证明你的DEKF真的有用
4.1 充分激励是参数可辨识的前提
很多人在仿真里随便给个阶跃输入就开始跑DEKF,然后发现参数估计乱飘,第一反应是滤波器写错了。其实大概率是激励不足。
回到弹簧-质量-阻尼系统。如果输入F是恒值,系统稳态后加速度为零,位移恒定。这时候从测量y = x1里只能推算出"外力/刚度"这一个组合信息,阻尼c对稳态位移没有任何贡献,c完全不可辨识。DEKF的参数滤波器会在这种工况下迷茫,P_θ的方差慢慢变大,估计值随机游走。
要让k和c都暴露出来,输入信号必须持续激励系统的动态。对于这个二阶系统,理想的激励是带限白噪声或者多频正弦组合,让能量覆盖固有频率附近。我用的激励是:
F = 10·sin(2π·0.8t) + 5·sin(2π·2.2t)
系统固有频率ωn = sqrt(k/m) = sqrt(100/2) ≈ 7.07 rad/s ≈ 1.13 Hz,两个正弦频率0.8Hz和2.2Hz分布在固有频率两侧,足够让位移和速度都持续波动。再加一个小幅随机扰动模拟真实激励的宽带特性,效果更好。
判断激励是否充分的定量手段是看信息矩阵,感兴趣的朋友可以查Fisher信息矩阵和CRLB的概念。简单说,当输入信号能量集中在系统动态最敏感的频段时,参数估计方差的下界最小。实操中不用算那么细,盯着参数估计曲线看就行:激励不足时参数会缓慢漂移且方差大,激励充分时参数快速收敛并稳定在一个窄带内。
4.2 参数阶跃与缓变两种测试工况
验证DEKF不能只跑一种工况。我强烈建议至少设计两组实验。
第一组是参数阶跃:仿真跑5秒后,把真实k从100突变到80,模拟系统发生结构性变化(比如弹簧断裂、螺栓松动)。这个工况考验参数滤波器的动态跟踪能力,看它能不能在几百毫秒内捕捉到突变并把参数估计拉过去。阶跃是对DEKF灵敏度项最严苛的考验——如果L_k被忽略,参数估计遇到阶跃往往会反应迟钝,或者在跳变瞬间产生大的超调。
第二组是参数缓变:让c从5随时间缓慢变化,比如c = 5 + 0.2t,模拟阻尼随温度升高的场景。这个工况考验参数滤波器在低速漂移下能否无偏跟踪,同时不把噪声放大。缓变工况下Q_θ如果设置得当,参数估计应该紧紧贴住真实值,而不是滞后一大截。
我个人的做法是把两种工况放到同一个仿真里:0-5秒参数恒定,5秒时k阶跃,5秒后c开始缓变。一个仿真能同时看到收敛、阶跃响应、缓变跟踪三种表现,信息量很足。
4.3 量化指标怎么定
看曲线图只能"感觉"收敛了,要说服别人(包括说服自己),得有几组量化数据。我一般会统计这几个指标:
参数估计收敛时间:从初始值上升到进入真实值±5%误差带且不再离开的时间。这个指标反映参数滤波器的收敛速度。
阶跃响应时间:参数阶跃发生后,估计值重新进入±5%误差带的时间。这个指标反映DEKF对突变的适应能力。
状态估计RMSE:把x̂与真实状态比较,在稳态段算均方根误差。这个指标衡量状态滤波器的精度有没有被参数误差拖累。
创新序列均值:状态滤波器的创新序列(y_k - H·x̂_k^-)在收敛后应该近似零均值白噪声。如果均值明显不为零,说明模型存在偏差,参数还没收敛到位。
我自己的习惯是仿真结束后在MATLAB里用脚本统计这些指标,而不是在Simulink里放一堆Display模块。仿真模型保持简洁,后处理全部放到工作区脚本里,调参效率高很多。
5. Simulink里DEKF最容易翻车的几个坑
5.1 采样时间不一致:滤波器以为的步长和仿真实际步长不同
这是DEKF里最常见也是最隐蔽的一个坑。如果你把MATLAB Function块直接连进连续被控对象的模型里,默认情况下它可能继承连续采样时间,每个仿真步都调用一次。如果你的求解器是变步长,每一步的时间增量都是变化的,滤波器却用了固定的Ts递推,那算出来的预测就完全错位。
正确做法是把MATLAB Function块设为离散采样时间,并让Ts和它的采样周期一致。在Simulink里,点开MATLAB Function块的参数面板,把Sample Time设成Ts的值。或者更稳妥一点,把DEKF模块放到一个触发子系统(Triggered Subsystem)里,用Periodic Trigger信号控制触发间隔。我推荐后者的原因是触发子系统结构更清晰,以后要改成事件触发或变周期触发也方便。
另一个相关问题是数值求解器的选择。仿真被控对象本身是连续系统,用ode4固定步长求解器,步长取Ts或Ts/10都可以。如果Ts取得很小,直接用ode4步长等于Ts,整个仿真就是等步长推进,DEKF和对象模型在时间上严格对齐,排查问题最简单。
5.2 协方差矩阵失去对称正定性的处理
卡尔曼滤波的协方差矩阵理论上必须是正定对称的,但浮点运算的舍入误差会让它慢慢偏离。尤其当测量噪声很小、增益很大时,(I-KH)P^- 这种更新方式很容易让P变得不对称,甚至出现负的特征值。P一旦不正定,卡尔曼增益计算出来就是错的,滤波很快发散。
Joseph形式比简单形式好很多,但也不能百分之百保证对称性,因为K和H的乘积本身有数值误差。我在代码末尾加了一行强制对称化:
Px_new = (Px_new + Px_new') / 2; Pth_new = (Pth_new + Pth_new') / 2;这一步的计算量几乎可以忽略,但能避免很多莫名其妙的数值发散。如果还要更稳健,可以把P分解成LDL'或UD形式,直接在分解后的因子上递推,不过对于状态维度只有2-3个的DEKF验证,强制对称就够用了。
5.3 Q_θ与R_θ的整定逻辑:别直接照抄R_x
三个噪声协方差矩阵Qx、Qθ、Rθ,再加上两个初始协方差Px、Pθ,DEKF的整定自由度确实比普通EKF多,这也是很多人调崩的原因。我总结一下自己的整定顺序。
先定Qx和Rx。Qx反映了你对模型动态建模误差的信心,Rx可以由传感器数据手册里的噪声方差换算得到。这两个值先让它合理,状态滤波器的表现先要正常。判断标准是创新序列的方差要和Rx基本匹配,创新太大了说明Qx/Rx设置偏小,滤波器过于自信。
再定Qθ和Rθ。Qθ对应参数随机游走的"速度",它控制了参数跟踪快慢和抖动幅度的权衡。Qθ取得大,参数能快速跟上阶跃变化,但稳态抖动也大;Qθ取得小,参数曲线平滑,但遇到突变时反应迟钝。我的经验是Qθ先取一个很小的数,比如k通道对应标准差的平方,然后逐步增大,直到阶跃响应时间满足要求为止。
最后是Rθ。很多新手直接把Rθ设成和Rx一样,这是最大的坑。参数滤波器的残差(y - h(x̂_k, θ̂))里包含了状态滤波器估计误差的传播,所以残差方差实际上远大于传感器噪声方差。你要是把Rθ设小了,参数滤波器会过度信任残差,导致参数估计剧烈抖动。实操中我会把Rθ比Rx大1到2个数量级,然后看参数曲线的平滑度再微调。
一个额外建议是分阶段整定:先固定一组合理的初始协方差Px、Pθ,在给定激励下把Qθ、Rθ调得让参数收敛;然后再改初始Pθ,让它对收敛时间的影响符合预期。不要同时动所有旋钮,否则出了问题根本不知道是哪一步导致的。
6. 完整案例:弹簧-质量-阻尼系统的参数与状态联合估计
6.1 被控对象模型与DEKF参数配置
下面是一个完整的验证案例。被控对象在Simulink里用两个积分器搭建连续二阶系统,真实参数k=100,c=5,m=2。输入信号用三个信号源合路:两个正弦发生器(0.8Hz和2.2Hz,幅值分别为10和5),加一个幅度很小的随机扰动。测量输出是位移x1,叠加一个零均值、标准差sqrt(1e-6)的高斯白噪声模拟传感器噪声。
DEKF的初始配置如下:
| 配置项 | 数值 | 说明 |
|---|---|---|
| 初始位移估计 | 0 | 已知初始静止 |
| 初始速度估计 | 0 | 已知初始静止 |
| 初始参数估计 | k=60, c=3 | 故意偏离真值 |
| Px初值 | diag([1e-2, 1e-2]) | 状态不确定度较小 |
| Pθ初值 | diag([200, 10]) | 参数不确定度较大 |
| Qx | diag([1e-6, 1e-4]) | 过程噪声 |
| Qθ | diag([1e-4, 1e-6]) | 参数随机游走 |
| Rx | 1e-6 | 测量噪声方差 |
| Rθ | 1e-4 | 参数滤波残差噪声 |
| 采样周期Ts | 1e-3 s | DEKF递推步长 |
仿真时长设为10秒,其中5秒处把真实k从100阶跃到80,同时让真实c按c = 5 + 0.2·(t-5)缓慢增大,考察参数滤波器的阶跃跟踪和缓变跟踪能力。
6.2 MATLAB Function核心代码
MATLAB Function块的代码已经在第3节给出,把它原封不动放进去就行。接口上,F输入端连接激励信号,y端连接含噪测量信号,Ts端接一个常量模块值为1e-3。输出端xhat和thetahat分别连到示波器,同时可以用To Workspace模块导出到工作区做后处理。
注意在Simulink里,被控对象的参数切换要用阶跃信号控制。实现方式是在真实参数通道上放一个Switch模块,用0-1阶跃信号控制选择调整前还是调整后的值。不要用MATLAB Function块里的if配合仿真时间来做切换,那样会引入不必要的代数环风险。
6.3 仿真结果与解读
按上述配置跑完之后,你会看到几个典型的收敛过程。
0-2秒是初始收敛阶段。参数估计从k=60、c=3开始,k会首先被拉向100,c随后跟上。这个阶段状态滤波器的精度较差,因为参数偏差导致模型预测不准,但状态滤波器本身会通过测量更新做补偿,所以位移估计的误差并不会看起来特别大——这正是DEKF里"状态滤波器能暂时掩盖参数误差"的现象,也是我们必须同时看状态和参数两组结果的原因。
2-5秒是稳态阶段。参数估计应该稳定在k≈100、c≈5附近,误差带控制在±2%以内。创新序列在零均值附近波动,状态RMSE在1e-3量级,和传感器噪声标准差匹配。
5秒后是核心看点。真实k阶跃到80的位置,参数滤波器的响应通常会在0.5秒左右追上,出现一定的超调但会收敛。c的缓变跟踪则会让c估计曲线呈现一条缓慢上升的近似直线,和真值贴住。如果灵敏度项L被忽略,你会在阶跃时刻看到k估计明显的滞后,甚至出现一段"僵住不动"的区间,因为参数滤波器的梯度信息断了。
我个人习惯把状态和参数画成上下两张图,用subplot同屏显示,并且把真实值曲线画成虚线叠加。这样能一眼看出收敛、阶跃、缓变三种特征,排查问题也直观。
6.4 工程扩展:从Simulink验证到代码生成
验证通过之后,下一步通常是代码生成或部署。如果你的目标是把DEKF跑在嵌入式控制器上,有几个问题要提前想清楚。
第一,MATLAB Function里用到了矩阵除法/,这在中高端处理器上没问题,但在低端MCU上用普通C语言生成可能会变成通用矩阵求逆,开销很大。建议把2×2或1×2的矩阵运算展开成标量表达式,比如卡尔曼增益的标量写法。这个改写很简单,但性能差距巨大。
第二,Qθ、Rθ、Pθ这些初始化和递推里的矩阵乘法,在定点嵌入式目标上要小心溢出。DEKF的协方差量级可能差很多(Px在1e-6量级,Pθ在100量级),统一用定点数表示很考验标定功夫。如果芯片支持浮点运算,尽量保留double或single;只有资源极度紧张时才考虑定点化。
第三,代码生成时persistent变量会变成静态变量,没问题;但MATLAB Function里的diag、eye这类函数在嵌入式代码生成里通常能展开成常量赋值,不用担心。真正的坑是如果你用了coder.extrinsic或者动态数组,代码生成会失败或生成低效代码,所以验证模型和部署模型的代码风格尽量从一开始就保持一致。
最后分享一个我自己踩过几次的经验:DEKF仿真跑通之后,先别急着接真实数据。先用同样的模型,把被控对象的真实参数改为另一个值(比如k=120、c=8),重新跑一遍,确认滤波器能从新的初值重新收敛。这个"参数蹦来蹦去"的回环测试能暴露很多只在特定参数工作点才会出现的数值问题。我在几次项目预研里发现,DEKF的稳定性对参数工作点是敏感的,某个参数组合下收敛良好,换一组参数就发散。提前做这个测试,比等到现场再发现问题要划算得多。