MFAC这名字乍一听像是学术圈黑话,但真正把CFDL-MFAC这一套跑通之后,你会发现它的核心思想特别朴素:不需要知道被控对象内部长什么样,只看输入输出数据,在线估计出一个叫“伪偏导数(PPD)”的时变增益,再用这个增益去算控制量。这次我完整做的是最经典的紧格式动态线性化无模型自适应控制(CFDL-MFAC),被控对象选了一个强非线性离散系统,先用Matlab脚本验证算法,再用Simulink搭仿真模型,重点啃的就是伪偏导数PPD在线估计的重置逻辑、参数整定和防发散处理。
这篇内容适合三类人:刚接触MFAC、想搞懂CFDL格式和控制律推导逻辑的研究生;课程设计或毕业设计需要一套可复现Matlab+Simulink代码、能直接跑出跟踪效果的同学;现场做过程控制、不想花大力气建立精确模型、又想稳定控制非线性对象的工程师。看完你能直接复现整套仿真,也能把PPD估计模块迁移到你自己的被控对象上。
1. 项目概述:CFDL-MFAC到底解决了什么问题
1.1 “无模型”不是没有参数,而是不基于机理模型
传统控制方法大概分两类:PID不依赖模型,但参数基本靠经验和试凑;MPC这类方法很依赖对象模型,模型不准性能就崩。MFAC走的是另一条路:它把被控对象看成一条“输入变化到输出变化”的增益链路,这个增益随时间变化、随工作点变化,不能直接测量,只能通过输入输出数据在线估计。因此整个控制器不需要机理建模,也不需要离线辨识。
这在工程里的价值很直观。比如化工过程中很多对象机理复杂,想建一个能用的机理模型要花几个月;还比如带强非线性、慢时变的流程,模型刚建好过段时间又失效了。MFAC在这种场景下能把建模环节整个绕开,只要闭环过程中的I/O数据质量够,算法就能持续自我调整。
不过必须把话说清楚:“无模型”不等于“无参数”。算法本身有一堆系数要整定,后面会专门讲。如果没建立这个观念,调参时会特别痛苦,因为你会潜意识里觉得“既然无模型,参数应该随便乱设”。
1.2 CFDL、PFDL、FFDL三种动态线性化格式,为什么先从CFDL下手
MFAC里的“动态线性化”其实不止一种格式。CFDL(紧格式)只用当前输入增量的增益,得到一个标量PPD;PFDL(偏格式)把最近几个时刻的输入增量组成矢量,得到一组PPD;FFDL(全格式)连输出导数的贡献也一起估。它们的共同点,是把非线性系统在每一个采样时刻等价成某个线性表达式,而不是像泰勒展开那样围绕某个静工作点近似。
CFDL是三种格式里结构最简单的,PPD退化为一个标量,估计公式和控制律都非常直观。对SISO系统,绝大多数场景下CFDL已经能给出不错的跟踪效果。我个人强烈建议第一次接触MFAC的人直接从CFDL入手,因为代码量最小,调试时可以一个变量一个变量观察,把整定逻辑吃透之后,再去玩PFDL和FFDL会顺手很多。本次项目也只用了CFDL格式,后续要提升性能,改成PFDL只是把标量扩成矢量,核心思想完全一致。
1.3 适合什么对象,不适合什么对象
CFDL-MFAC能工作,通常要求被控对象的输入到输出方向特性在局部是可逆的、相对稳定的,并且输出对输入的响应不能无界。具体到仿真项目,常用的测试对象是带平方项、乘积项、甚至三次方项的非线性离散系统,我这次选的就是MFAC文献里的典型测试对象,表达式在3.1节给出。
不适合的情况也要提一下:如果系统本身开环发散极其严重,或者执行机构有严重滞环、传感器噪声特别大,单靠标准CFDL-MFAC会比较吃力。通常要先做数据预处理、加执行机构饱和限制,或者换成带扰动补偿的改进型MFAC。了解边界在哪,后面才不至于对着一个谁都控制不了的对象瞎调参。
2. 核心原理拆解:伪偏导数估计与控制律是怎么来的
2.1 紧格式动态线性化的数学逻辑
把被控对象写成一般离散非线性系统的形式:
$$ y(k+1)=f\big(y(k),y(k-1),\dots,u(k),u(k-1),\dots\big) $$
在不清楚 $f$ 具体表达式的情况下,只要系统满足两个基本假设——输出对控制输入的偏导存在且连续、系统满足广义Lipschitz条件——那么当 $\Delta u(k)=u(k)-u(k-1)\ne 0$ 时,数学上可以证明存在一个有界的时变标量 $\phi(k)$,使得
$$ \Delta y(k+1)=\phi(k)\Delta u(k) $$
注意,这里的等号不是近似,而是在假设条件下存在精确的动态线性化表达式。$\phi(k)$ 就是伪偏导数PPD,它综合了系统当前工作点的增益、惯性、非线性特性。你可以把它想象成开车时的“等效转向增益”:车速、路面附着系数都会影响这个增益,但驾驶员不需要懂轮胎的物理模型,只要观察“我打多少方向盘、车头实际动多少”,就能反推当前的等效增益,下一步转弯就有谱了。CFDL-MFAC的控制器本质上就是在做这件事。
有一个特别容易踩的误区:PPD并不是唯一的,它取决于具体的线性化方式和实时数据,估计出来的是“某个可行值”,而不是对象的物理参数。我看到不少同学把PPD理解成模型辨识参数,方向一错,后面调试就全乱套。
2.2 PPD在线估计的递推公式与重置机制
PPD是时变且未知的,必须在线估计。标准做法是最小化一个带正则项的准则函数:
$$ J(\phi)=\left[\Delta y(k)-\phi,\Delta u(k-1)\right]^2+\mu\left(\phi-\hat\phi(k-1)\right)^2 $$
第一部分衡量“用当前PPD预测上一拍输出变化”的误差,第二部分防止 $\phi$ 离上一拍的估计值太远,$\mu$ 就是惩罚因子。对 $\phi$ 求导并令导数为零,得到递推式:
$$ \hat\phi(k)=\hat\phi(k-1)+\frac{\eta,\Delta u(k-1)}{\mu+\Delta u(k-1)^2}\left(\Delta y(k)-\hat\phi(k-1)\Delta u(k-1)\right) $$
这里有个相位关系必须说清楚:CFDL关系写在 $(k)$ 时刻是 $\Delta y(k+1)=\phi(k)\Delta u(k)$,但在估计当前 $\hat\phi(k)$ 时,能用的最新数据是 $\Delta y(k)$ 和 $\Delta u(k-1)$,所以递推式里用的是这两个量,相当于用上一拍的输入变化去解释当前拍输出变化,这是完全合理的。
各项作用分别理解:
- $\eta$ 是PPD估计步长,决定修正速度;
- 分母里的 $\mu+\Delta u(k-1)^2$ 是关键,如果 $\Delta u$ 特别小,直接相除会得到巨大的修正量,PPD会剧烈抖动,$\mu$ 在这里起到兜底作用;
- 方括号里是模型预测误差,本质上是带遗忘机制的最小均方估计思想:误差大就多修一点,误差小就少修一点。
只靠递推公式还不稳定,仿真里几乎都要加重置机制:
$$ \begin{aligned} &\text{若 }|\Delta u(k-1)|\le\varepsilon\ &\text{或 }\hat\phi(k)\le\phi_{\min}\ \text{或}\ \hat\phi(k)\ge\phi_{\max},\ &\text{则令 }\hat\phi(k)=\hat\phi(k-1) \end{aligned} $$
为什么要重置?第一,$\Delta u$ 太小意味着“输入几乎没动,输出却在波动”,用这种数据反推增益,结果会被数值噪声淹没,必须跳过;第二,估计出界的PPD说明当前数据不可信,硬算下去可能导致控制量方向性发散,此时恢复上一拍值是最安全的操作。我调试时见过很多次PPD冲到上限后系统开始震荡,把重置打开、阈值调对,曲线一下子就安静了。
2.3 控制律的推导:从CFDL模型到一步预测控制
有了当前PPD估计值,控制器设计就很自然了。控制目标通常写成:
$$ J(u)=\left[y_d(k+1)-y(k+1)\right]^2+\lambda\left[u(k)-u(k-1)\right]^2 $$
把CFDL关系代入 $y(k+1)=y(k)+\hat\phi(k)\left(u(k)-u(k-1)\right)$,展开后对 $u(k)$ 求导并令导数为零,得到控制律:
$$ u(k)=u(k-1)+\frac{\rho,\hat\phi(k)}{\lambda+\hat\phi(k)^2}\left(y_d(k+1)-y(k)\right) $$
这个增益 $\frac{\rho,\hat\phi(k)}{\lambda+\hat\phi(k)^2}$ 有一个很有意思的性质:当 $|\hat\phi(k)|$ 很大时,分母近似 $\hat\phi(k)^2$,整体近似 $\rho/\hat\phi(k)$,相当于用当前增益的倒数做一步前馈;当 $|\hat\phi(k)|$ 很小时,分母里的 $\lambda$ 兜底,避免除零。换句话说,控制器是带增益自适应的“一步预测控制”,完全不需要知道系统内部机理。
从推导过程能看出,整个控制器只依赖两样东西:PPD的在线估计值,以及参考信号与当前输出之间的误差。这就是“无模型”三个字的真正含义。
2.4 参数整定:五个常调参数的作用与调节方向
PPD估计里有两个参数($\eta$、$\mu$),控制律里有 $\rho$、$\lambda$,再加上PPD初值、上下界和重置阈值 $\varepsilon$,一共得面对七八个量。整理成一张表方便查:
| 参数 | 含义 | 典型范围 | 调大的效果 | 调小的效果 |
|---|---|---|---|---|
| $\eta$ | PPD估计步长 | $(0,2]$,常用1 | PPD收敛快,但易抖动 | PPD更新慢,收敛也慢 |
| $\mu$ | PPD估计惩罚因子 | $0.001\sim1$ | PPD更平稳,但跟踪变钝 | PPD更敏感,小$\Delta u$时易发散 |
| $\rho$ | 控制律步长 | $(0,1]$,常用0.5~1 | 收敛快,容易震荡 | 收敛慢,但稳定 |
| $\lambda$ | 控制量变化惩罚 | $0.1\sim10$ | 控制量平滑,跟踪变慢 | 跟踪快,控制量易锯齿 |
| $\varepsilon$ | 重置阈值 | $10^{-5}\sim10^{-3}$ | 重置更频繁,更保守 | 可能错过危险的小$\Delta u$ |
我自己的整定顺序是:先固定 $\eta=1,\mu=0.1,\rho=0.6,\lambda=1$,跑一遍看PPD曲线。如果PPD高频抖动,优先增大 $\mu$;如果跟踪慢,增大 $\rho$ 或减小 $\lambda$;如果控制量呈锯齿状,增大 $\lambda$。PPD初值不知道取多少时,取1~3都行,配合界 $\phi_{\min}=0.01,\phi_{\max}=10$ 基本能启动。换了被控对象后这些参数都要重新试,千万别指望一套参数通吃所有系统。
3. 从零到仿真:Matlab脚本与Simulink实现
3.1 先跑一个完整的M脚本,把算法跑通
我建议先在纯Matlab脚本里验证CFDL-MFAC,因为循环逻辑一目了然,调试信息随便打印,比直接进Simulink折腾快得多。被控对象用文献里常见的强非线性离散系统:
$$ y(k+1)=\frac{2.5y(k)y(k-1)}{1+y(k)^2+y(k-1)^2}+u(k)+1.2u(k-1)+0.5u(k)^3 $$
这个对象既有乘积项、分母项,又有输入三次方项,PID通常要反复试凑,CFDL-MFAC却能靠I/O数据把PPD逐步估计出来。脚本我完整贴在下面,可直接复制运行,我用的是R2023b,老版本里基本也能跑。
%% CFDL-MFAC 紧格式无模型自适应控制仿真 clc; clear; close all; % 1) 算法参数 eta = 1.0; % PPD估计步长 mu = 0.1; % PPD估计惩罚因子 rho = 0.6; % 控制律步长 lambda = 1.0; % 控制量变化惩罚因子 epsilon = 1e-4; % PPD重置阈值 phi_min = 0.01; % PPD下限 phi_max = 10; % PPD上限 phi0 = 2; % PPD初值 N = 2000; % 仿真步数 % 2) 初始化 y = zeros(1,N); u = zeros(1,N); yd = zeros(1,N); phi = zeros(1,N); y(1)=0; y(2)=0; u(1)=0; u(2)=0; phi(1) = phi0; % 3) 参考信号:周期方波 for k = 1:N if mod(k, 400) < 200 yd(k) = 0.5; else yd(k) = -0.5; end end % 4) 被控对象匿名函数 obj = @(u_k, u_km1, y_k, y_km1) ... 2.5*y_k*y_km1/(1+y_k^2+y_km1^2) ... + u_k + 1.2*u_km1 + 0.5*u_k^3; % 5) 主循环:PPD估计 -> 控制律 -> 对象更新 for k = 2:N-1 du = u(k-1) - u(k-2); dy = y(k) - y(k-1); if abs(du) <= epsilon phi(k) = phi(k-1); else phi_prev = phi(k-1); phi(k) = phi_prev + ... eta * du / (mu + du^2) * (dy - phi_prev*du); end if phi(k) < phi_min || phi(k) > phi_max phi(k) = phi_prev; end u(k) = u(k-1) + ... rho * phi(k) / (lambda + phi(k)^2) * (yd(k+1) - y(k)); y(k+1) = obj(u(k), u(k-1), y(k), y(k-1)); end % 6) 绘图 figure('Color','w'); subplot(3,1,1); plot(1:N, yd, 'k--', 'LineWidth', 1); hold on; plot(1:N, y, 'b', 'LineWidth', 1.2); legend('参考 y_d','输出 y','Location','best'); title('CFDL-MFAC 输出跟踪'); xlabel('k'); ylabel('y'); subplot(3,1,2); plot(1:N, u, 'r', 'LineWidth', 1); title('控制输入 u(k)'); xlabel('k'); ylabel('u'); subplot(3,1,3); plot(1:N, phi, 'm', 'LineWidth', 1); title('伪偏导数 PPD 在线估计'); xlabel('k'); ylabel('\phi(k)');这段代码三个关键点:
- 主循环从 $k=2$ 开始,因为PPD估计需要 $u(k-1)$、$u(k-2)$、$y(k)$、$y(k-1)$,最开头必须有历史值;
- PPD估计里先检查 $|\Delta u|\le\varepsilon$,再检查出界,二者都触发重置。顺序有讲究:先保证数据有效,再保证数值有效;
- 对象更新放在循环最后,保证第 $k$ 拍的 $u_k$、$y_k$ 不会提前污染历史值。
跑完你会看到典型曲线:方波阶跃附近输出有少量超调但能快速收敛,稳态无静差;PPD曲线在阶跃后有一次快速调整,随后在某个有界范围内小幅波动,这是它在“在线学习”系统增益的体现。如果PPD频繁突破上下界,不要急着改系统,先看4.1节的排查思路。
3.2 Simulink实现:MATLAB Function方案
脚本跑通后再进Simulink,心里就有底了。Simulink实现CFDL-MFAC有几种方案,我最推荐先把方案A跑起来:用MATLAB Function模块写控制器,被控对象用Simulink基础模块搭。等到要做C代码生成或更复杂的对象时,再换成S-Function方案也不迟。
控制器函数的核心逻辑,就是把上面主循环里“当前拍”的部分抽出来:
function [u_k, phi_k] = mfac_controller(yd_next, y_k, y_km1, u_km1, u_km2, phi_prev) % CFDL-MFAC 离散控制器:输入当前/历史数据,输出控制量和PPD估计值 eta = 1.0; mu = 0.1; rho = 0.6; lambda = 1.0; epsilon = 1e-4; phi_min = 0.01; phi_max = 10; du = u_km1 - u_km2; dy = y_k - y_km1; if abs(du) <= epsilon phi_k = phi_prev; else phi_k = phi_prev + eta*du/(mu+du^2)*(dy - phi_prev*du); end if phi_k < phi_min || phi_k > phi_max phi_k = phi_prev; end u_k = u_km1 + rho*phi_k/(lambda + phi_k^2)*(yd_next - y_k); end在Simulink模型里,$u_{k-1}$、$u_{k-2}$、$y_{k-1}$、$\phi_{k-1}$ 这些历史状态全部用Unit Delay或Memory模块保存。搭建时我会用下面这批模块:
| 模块 | 作用 | 备注 |
|---|---|---|
| Repeating Sequence | 生成方波参考信号 | 设置周期和幅值 |
| MATLAB Function | MFAC控制器 | 按上式连接输入输出端口 |
| Unit Delay | 保存 $u_{k-1},u_{k-2},y_{k-1},\phi_{k-1}$ | 注意离散采样时间设置 |
| Product / Sum | 被控对象中的乘加运算 | 搭 $f$ 的代数表达式 |
| Math Function | 平方、立方、倒数 | 处理分母和三次方 |
| Scope / To Workspace | 观测输出、控制量、PPD | 便于后处理 |
连接时最容易忽略的点:如果MATLAB Function模块的输入直接包含自己的输出,Simulink会报代数环。解决办法是给反馈路径加一个Memory或Unit Delay,让控制器计算完全基于上一拍值,而不是当前拍直接反馈。别问我怎么知道的,我第一次搭的时候被这个代数环提示折磨了整整一下午。
3.3 Simulink求解器与采样时间的正确配置
MFAC是离散控制算法,控制器必须按固定采样周期执行。建议在Configuration Parameters里做三个设置:
- Solver选Fixed-step,Type选Discrete;
- Fixed-step size填 $0.01$(或与控制器采样周期一致);
- 整个模型尽可能都用离散模块搭建,避免连续/离散混用时要额外加零阶保持器。
如果被控对象本身就是连续对象,需要在对象输出端加一个Zero-Order Hold(ZOH)采样,再送进控制器。控制器输出的 $u(k)$ 在保持区间内恒定,这正好对应算法里 $u(k)$ 在一步内不变的假设。第一次搭Simulink的同学最容易在这里犯迷糊:离散控制器输出直接连连续对象,仿真步长一变,波形全是锯齿,看起来像算法不收敛,其实是接口没有隔离。
另外,如果你后续有生成嵌入式C代码的需求,建议在S-Function方案上直接用Embedded Coder做,MATLAB Function模块虽然方便,但代码生成时对数据类型的约束更多,提前规划能省不少返工时间。
3.4 仿真结果怎么判断好坏
仿真跑完,不要只看一条输出曲线。我习惯把三张图画在一起:输出跟踪曲线、控制输入曲线、PPD曲线。判断标准通常有四条:
- 输出能跟踪参考信号,稳态误差小;
- 阶跃点附近没有持续发散的高频振荡;
- 控制输入没有过大尖峰,执行机构可承受;
- PPD始终落在设定的上下界内,不会长期贴在上限或下限附近。
如果PPD长期“贴边”,说明当前界设置或初值不合理,也可能是对象增益估计方向不对,需要回头调整 $\phi_{\min}/\phi_{\max}$ 和初值。实践中我遇到过很多次输出曲线看着不错、PPD却已经出界的“亚健康”状态,参考信号一变就容易发散,所以一定要把PPD曲线纳入常规观测指标。
4. 常见问题与排查技巧:调参、发散、代数环一次说清
4.1 PPD发散或频繁重置怎么查
现象:PPD曲线频繁触到界,输出抖动,严重时控制量越来越大。排查顺序是先看 $\Delta u$ 是否在大部分时间内接近零。如果是,说明系统在微小输入变化下输出仍然有波动,PPD估计会被数值噪声放大。此时增大 $\varepsilon$,让 $\Delta u$ 太小时直接跳过估计;同时增大 $\mu$,降低PPD对单拍误差的敏感度。如果PPD还是顶到界,就要检查代码里有没有写重置逻辑——很多初版代码只写了递推式,没加重置分支,仿真迟早出问题。
我印象最深的一次案例是,PPD从第二十拍开始直接冲顶,排查后发现对象表达式里的分母在某个区域接近零,输出突发跳变,而 $\Delta u$ 又很小,PPD瞬间爆掉。把 $\mu$ 从0.01改到0.5、$\varepsilon$ 从1e-5改到1e-4后,曲线彻底安静下来。这个案例比较典型,以后遇到类似现象,可以先从这两个参数下手。
4.2 方波跟踪末端振荡、控制量锯齿
现象:阶跃到来后的前几拍跟踪还行,接近稳态时控制量还在来回摆动。原因通常是 $\lambda$ 偏小、$\rho$ 偏大,控制器对误差反应过于激烈。解决思路是把 $\rho$ 降到0.3~0.5,再把 $\lambda$ 提到2左右,观察锯齿是否衰减。另外要注意,MFAC在阶跃后需要重新学习增益,短暂振荡是学习过程的一部分,给算法几十拍适应时间再看。
如果想进一步压振荡,可以在参考信号前加一阶惯性滤波,把方波改成渐变斜坡。工程上这招非常实用,执行机构会感谢你的。纸上谈兵的人都觉得直接阶跃才“够劲”,真上设备控制就知道,能少一次冲击都是好的。
4.3 Simulink代数环与离散/连续混合问题
现象:一编译仿真就报“Algebraic loop”,或者波形出现高频锯齿。代数环的根因是控制器输出直接参与了自己的输入计算,比如 $u(k)$ 依赖 $y(k)$,而 $y(k)$ 又由 $u(k)$ 通过对象模块直接算出来,Simulink需要迭代求解这个闭环。解法很简单:在反馈路径插一个Unit Delay或Memory,让控制器使用上一拍值。如果对象是连续模型,还要在对象输出端加ZOH、求解器设成Fixed-step。
我也见过另一个问题:控制器和对象明明都是离散模块,但Simulink默认用了变步长连续求解器,导致控制器模块和对象的更新节奏对不上,波形错乱。把Solver Type改成Discrete后问题立即消失。这种配置问题,报错信息往往不会直接讲清楚,只能靠经验排查。
4.4 系统发散到NaN或Inf怎么处理
仿真跑到一半输出变成NaN或Inf,先别急着怀疑算法,多半是对象表达式在某个 $u$ 或 $y$ 取值下产生了无效运算,比如分母为零、指数溢出。排查方法是在对象函数或控制器里对输入输出加Saturation限幅,并打印异常发生时的 $u$、$y$、$\phi$。
我调试带三次方项的对象时,$u$ 一旦超过某个门槛,对象输出直接指数爆炸,MFAC根本来不及反应,仿真一步就NaN了。后来给控制量加饱和限幅、把PPD界收窄到 $[0.01,5]$,问题消失。这个经验分享出来,希望你能少走这个弯路。
4.5 新对象参数快速启动“三步法”
如果要对一个全新对象调参数,不要五个参数一轮乱试。我建议用三步走:
- 用默认参数跑通:$\eta=1,\mu=0.1,\rho=0.5,\lambda=1,\phi_0=2$,先看输出是否发散;
- 如果发散,先加控制量饱和、把 $\rho$ 砍半,再看能否恢复稳定;
- 如果稳定但跟踪差,按照“$\rho$ 上升、$\lambda$ 下降”微调,每次只动一个参数,并记录效果。
这流程看起来笨,但能避免两个参数互相抵消的迷茫。我是调了好几轮才总结出来的,一步到位的参数设置只存在于想象中。
5. 关于PPD调试,最后想分享的个人经验
说实话,我在项目开始前对“无模型”三个字是持怀疑态度的,觉得不建模怎么可能控制得好。但把CFDL-MFAC在多个非线性对象上跑下来,我的体会是:它确实不需要机理模型,但对数据质量和I/O通道特性非常敏感。伪偏导数估计本质上是用“过去一步的输入输出变化”去近似“当前等效增益”,数据稍微脏一点,估计值就会剧烈跳动。所以我现在的习惯是:优先保证 $\Delta u$ 不要长时间为零,把PPD曲线当成仪表盘随时看,先把参数整定流程走顺,再谈扩展。
最后分享一个小技巧:调试时把每个采样拍的 $\hat\phi(k)$、$u(k)$、$y(k)$ 全部存到workspace,画成三行子图,一眼就能定位问题出在估计端还是控制端。这个方法帮我至少省了五次盲目试参的时间。如果你下一步想进阶,可以沿着偏格式动态线性化PFDL、带输出噪声抑制的MFAC方向继续走,底层逻辑还是这套在线估计与自适应反馈。