搞控制的人早晚都会遇到那种场景系统模型里一堆参数根本测不准模型本身还非线性得厉害PID调到怀疑人生。我前几年做机械臂轨迹跟踪和电机伺服的时候这个问题几乎是绕不开的。后来我把自适应机制和滑模控制糅在一起发现效果确实稳而且用Matlab做闭环仿真验证起来特别顺手。这篇文章就把我实际用的这套方案从原理拆解到代码实现到调试经验完整捋一遍适合正在做非线性控制、鲁棒控制方向的研究生和工程党参考。参数不确定再加上非线性意味着传统基于精确模型的控制器很容易失效。普通滑模控制要起作用必须先知道不确定性的上界但这个上界往往根本估不准。自适应滑模的思路是让控制器自己在线估计那些未知参数再用估计值去构造滑模控制律既保留了滑模控制的强鲁棒性又不用手动整定一堆保守参数。这套东西用Matlab跑通之后换到任何二阶非线性系统上改改就能用复用价值很高。1. 参数不确定的非线性系统到底难在哪1.1 一个看起来简单、实际很麻烦的测试模型为了把问题说清楚我用一个非常典型的二阶非线性模型来讲\dot{x}_1 x_2\dot{x}_2 \theta_1 \sin(x_1) \theta_2 x_2^2 b u d(t)其中 x_1 是角度或者位置x_2 是速度u 是控制输入d(t) 是外界扰动。\theta_1 和 \theta_2 是系统的物理参数它们对应的物理意义很直观\theta_1 \sin(x_1) 可以看作重力项或者弹性恢复项\theta_2 x_2^2 可以看作和速度平方有关的摩擦或者阻尼项。这种模型在单关节机械臂、倒立摆、某些伺服机构里都见得到。参数 \theta_1、\theta_2 为什么不确定主要原因有三个一是模型本身就是对真实物理的简化摩擦系数、惯量这些很难精确测量二是负载变化比如机械臂末端抓了不同重量的东西等效惯量和重力矩都会变三是运行环境变化温度、磨损都会让参数缓慢漂移。所以在实际工程里我们几乎不可能拿到精确的 \theta 值最多知道它大概在某个范围里。控制目标很直接设计控制器 u让 x_1 从任意初值收敛到 0并且 x_2 也跟着收敛到 0。这个目标听起来简单难点在于 \theta 未知、d(t) 未知但有界控制器不能依赖这些参数的真值。后文所有推导和代码都是围绕这一条展开的。1.2 常规滑模控制的尴尬处境如果系统参数完全已知滑模控制是很漂亮的设计一个滑模面构造等效控制把系统拉上滑模面再用切换控制抵消不确定性系统状态就在滑模面上保持滑动最终收敛到原点。问题在于常规滑模在设计切换增益 k 时必须知道不确定性上界。比如想让控制器满足 s\dot{s} 0通常要求 k 大于所有模型误差和外界扰动的总上界。但你想想如果 \theta、d(t) 都未知这个上界怎么取取小了条件不满足滑模面可能到达不了系统会残留在滑模面附近一个不收敛的区域内控制精度变差。取大了切换控制项 k \cdot sign(s) 幅值非常大控制量高频抖振严重执行器受不了系统还可能激发未建模动态。这就是常规滑模最尴尬的地方切换增益太小不鲁棒太大又抖得厉害而你不知道的偏偏就是到底该取多大。自适应滑模解决的核心问题就是这个用自适应律把“不确定性上界”在线估计出来切换增益和反馈增益根据系统实际表现实时调整。一开始估计不准没关系状态稍微偏离滑模面自适应律就加大估计值把状态“压”回滑模面等状态稳在滑模面上估计值也就趋于稳定了。这样一来你不需要事先知道不确定性上界控制器自动替你找。2. 自适应滑模控制的核心设计2.1 滑模面的选择与滑模动态我先定滑模面。对这个二阶系统一个最常用的线性滑模面是s x_2 c x_1其中 c 0是你需要整定的控制器参数。这个式子理解起来很直观如果状态被限制在 s 0 上那么 x_2 -c x_1系统退化成 \dot{x}_1 -c x_1这是一个一阶线性系统x_1 以指数速度收敛到零收敛速度由 c 决定。c 越大收敛越快但控制量也会越大实际中要平衡。滑模控制的任务分两步第一步是“到达阶段”想办法让系统从任意初始状态到达滑模面 s 0 附近第二步是“滑动阶段”系统在滑模面上滑动沿着一阶系统轨迹回到原点。整个闭环控制的本质就是设计一个控制律保证 s 被驱动到零。2.2 控制律的结构等效控制加切换控制设计控制律的时候我先算 s 的导数\dot{s} \dot{x}_2 c \dot{x}_1 \theta_1 \sin(x_1) \theta_2 x_2^2 b u d(t) c x_2控制律里需要包含几个作用抵消已知或估计出来的非线性项抵消 c x_2 项对付未知扰动以及切到滑模面上所需的切换项。我的控制律取u -\frac{1}{b} \left( \hat{\theta}_1 \sin(x_1) \hat{\theta}_2 x_2^2 c x_2 k \cdot sign(s) \hat{D} \cdot sign(s) \right)这里 b 是控制增益我假设它可以通过离线标定拿到一个可靠的常数比如电机驱动器电流到转矩的比例系数或者液压阀的开度增益。\hat{\theta}_1、\hat{\theta}_2 是对两个模型参数的自适应估计值\hat{D} 是对扰动上界的估计值k 0 是固定切换增益。把控制律代入 \dot{s}得到\dot{s} \tilde{\theta}_1 \sin(x_1) \tilde{\theta}_2 x_2^2 d(t) - k \cdot sign(s) - \hat{D} \cdot sign(s)其中 \tilde{\theta}_i \theta_i - \hat{\theta}_i 是参数估计误差。可以看到只要自适应律能让参数误差和扰动误差被后面两项盖住\dot{s} 的符号就会被 sign(s) 主导s 自然趋近于零。这就是滑模控制核心的开关结构。2.3 自适应律的设计思路自适应律怎么设计这里需要用到李雅普诺夫函数。我构造V \frac{1}{2} s^2 \frac{1}{2\gamma_1} \tilde{\theta}_1^2 \frac{1}{2\gamma_2} \tilde{\theta}_2^2 \frac{1}{2\gamma_3} \tilde{D}^2其中 \gamma_1、\gamma_2、\gamma_3 是自适应增益\tilde{D} D - \hat{D}D 是扰动 d(t) 的某个上界满足 |d(t)| \leq D。这个 V 的物理含义是滑模偏差的能量加参数估计误差的能量如果它的导数始终小于等于零就说明系统整体在朝着滑模面收敛同时估计误差不会爆炸。对 V 求导代入 \dot{s}再整理一下就可以设计出满足 V 导数非正的参数更新律\dot{\hat{\theta}}_1 \gamma_1 s \sin(x_1)\dot{\hat{\theta}}_2 \gamma_2 s x_2^2\dot{\hat{D}} \gamma_3 |s|也正是这三条自适应律成了整个算法的核心。它们的作用用大白话说就是当状态偏离滑模面时也就是 s 不为零的时候自适应律持续调整估计值一旦状态滑上了滑模面s 趋近于零估计值也就基本不再变化。整个系统就像是一个会自我修正的滑模控制器不需要你之前去估计不确定性上界。最后一项 \hat{D} 的更新律尤其关键。它保证扰动上界估计值只会单调增大除非 s 为零。在工程上这意味着如果实际扰动比你预想的大自适应项会自动把切换增益补上去保证鲁棒性如果扰动很小\hat{D} 也不会一直涨因为当 s 稳定在滑模面附近后更新率会自然降下来。2.4 稳定性证明的工程意义很多同学看到李雅普诺夫证明就觉得头大但从工程角度理解它特别值。这个证明给你的是一个很强的保证不管参数真值是多少只要扰动有界、自适应增益为正、切换增益 k 为正闭环系统就一定收敛到滑模面附近。它不要求参数估计收敛到真值这一点非常重要。所以哪怕你的模型结构跟真系统有一定偏差只要控制器结构合适状态稳定性和鲁棒性依然有保障。这也是自适应滑模区别于自适应反步法或者滑模观测器的一个关键优势控制性能和参数辨识性能解耦了。我做的机械臂实验中\theta_1 的估计值经常不精确等于真实转动惯量但轨迹跟踪照样很好原因就在这里。3. Matlab代码实现3.1 代码架构与文件划分仿真代码我强烈建议拆成几个函数文件而不是一个脚本从头写到尾。我一般分三个部分main_adaptive_smc.m主脚本负责设置参数、定义初值、调用 ode45、绘制结果closed_loop.m闭环系统函数返回状态导数内部调用控制器逻辑可选draw_results.m结果绘图脚本这样拆的好处很明显哪天你想换一个被控对象模型只需要改closed_loop.m里的动力学方程想调控制器参数主脚本里面改想单独研究某个自适应律的收敛行为也不用把整个仿真逻辑翻出来重看。代码的复用性对于控制仿真是第一位的因为实验阶段你会不停地改参数把逻辑散在一个超长脚本里改一次翻一次车。3.2 主程序代码先看主脚本。我写的测试模型是真实参数\theta_1 1.2\theta_2 -0.4b 1.5扰动 d(t) 0.3 \sin(2t)。初始状态 x_1 0.6x_2 -0.5。控制器参数 c 3\gamma_1 5\gamma_2 5\gamma_3 2k 0.1。clear; close all; clc; % 真实系统参数 par.theta_true [1.2; -0.4]; par.b 1.5; par.dist_amp 0.3; par.dist_freq 2.0; % 控制器参数 ctrl.c 3.0; ctrl.gamma1 5.0; ctrl.gamma2 5.0; ctrl.gamma3 2.0; ctrl.k 0.1; % 初始状态 x0 [0.6; -0.5]; theta_hat0 [0; 0]; D_hat0 0; z0 [x0; theta_hat0; D_hat0]; % ode45 配置 opts odeset(RelTol, 1e-6, AbsTol, 1e-7, MaxStep, 0.01); % 仿真 [t, z] ode45((t,z) closed_loop(t, z, par, ctrl), [0 25], z0, opts); % 提取状态和估计值 x1 z(:,1); x2 z(:,2); s x2 ctrl.c*x1; theta_hat z(:,3:4); D_hat z(:,5); % 重算控制量用于绘图 u zeros(size(t)); for i 1:length(t) xi [sin(x1(i)); x2(i)^2]; si x2(i) ctrl.c*x1(i); u(i) -(theta_hat(i,:)*xi ctrl.c*x2(i) ctrl.k*sign(si) D_hat(i)*sign(si)) / par.b; end % 绘图 figure(Position, [100 100 800 800]); subplot(3,1,1); plot(t, x1, LineWidth, 1.5); hold on; plot(t, x2, LineWidth, 1.5); grid on; legend(x_1, x_2); title(State trajectories); xlabel(t); ylabel(x); subplot(3,1,2); plot(t, s, LineWidth, 1.5); grid on; title(Sliding surface s); xlabel(t); ylabel(s); subplot(3,1,3); plot(t, u, LineWidth, 1.2); grid on; title(Control input u); xlabel(t); ylabel(u); figure(Position, [100 100 800 500]); subplot(1,2,1); plot(t, theta_hat(:,1), LineWidth, 1.5); hold on; plot(t, theta_hat(:,2), LineWidth, 1.5); legend(\theta_1 hat, \theta_2 hat); grid on; title(Parameter estimates); xlabel(t); subplot(1,2,2); plot(t, D_hat, LineWidth, 1.5); grid on; title(Disturbance bound estimate D hat); xlabel(t);3.3 闭环系统函数实现closed_loop.m是这个仿真的心脏它返回状态向量 z 的导数。z 的前两位是系统状态 x_1、x_2第 3、4 位是 \hat{\theta}_1、\hat{\theta}_2第 5 位是 \hat{D}。写的时候注意顺序跟主脚本里初始状态一致不然 ode45 算出来的结果就是错位的。function dz closed_loop(t, z, par, ctrl) % 解析状态 x1 z(1); x2 z(2); theta_hat z(3:4); D_hat z(5); % 回归向量和滑模面 xi [sin(x1); x2^2]; s x2 ctrl.c * x1; % 自适应滑模控制律 u -(theta_hat*xi ctrl.c*x2 ctrl.k*sign(s) D_hat*sign(s)) / par.b; % 真实被控对象 d_true par.dist_amp * sin(par.dist_freq * t); dx1 x2; dx2 par.theta_true*xi par.b*u d_true; % 自适应律 theta_hat_dot [ctrl.gamma1 * s * xi(1); ctrl.gamma2 * s * xi(2)]; D_hat_dot ctrl.gamma3 * abs(s); % 返回状态导数 dz [dx1; dx2; theta_hat_dot; D_hat_dot]; end这里要解释几个容易踩坑的细节。第一控制律里我用par.b做除法也就是控制器知道真实增益。如果控制增益也有很大的不确定性需要把这个参数也放进自适应估计里方法是估计 \beta 1/b控制律变成 u -\hat{\beta} \cdot U其中 U 是括号里的部分。代码改动不复杂但李雅普诺夫推导要加一项对初学者来说先固定 b 更容易理解全貌。第二sign(s)是理想切换函数Matlab里对应sign函数。但后面你会看到直接用sign(s)会让控制量在 s0 附近剧烈来回切换这是抖振的来源。实际仿真中为了数值稳定我通常会加一个饱和函数sat(s, phi)代替 sign这个在后面的调试章节详细讲。第三ode45 的MaxStep我设置了 0.01这不是随便拍的。带 sign 函数的系统微分方程右端不连续ode45 的自适应步长会在不连续点附近疯狂缩小如果步长上限太大积分效率会特别低而且可能漏掉切换瞬间导致结果看起来有毛刺。设成 0.01 之后仿真速度和精度都在可控范围内。3.4 参数设置经验控制器参数怎么给我一般按照以下顺序c 先定c 是滑模面的倾斜系数决定了滑模面上的收敛速度。从 2~5 开始试如果你发现状态收敛太慢就调大如果控制量超限就调小。这个参数不要一开始就给得太大否则自适应律还没反应过来控制量已经巨大了。\gamma_1、\gamma_2 跟着系统激励量级走\gamma_1 乘以 s \sin(x_1)\gamma_2 乘以 s x_2^2。如果 x_2 很大\gamma_2 给太大会造成估计值大幅振荡如果 x_2 很小\gamma_2 给小又会让估计收敛太慢。一般先给 1~5观察参数估计曲线再微调。\gamma_3 控制扰动上界的估计速度这个直接影响对扰动的响应速度太大则 D hat 快速拉高、控制量偏大太小则扰动补偿滞后。我习惯从 1~3 开始扰动强的系统适当加大。k 不要设得太大既然有 \hat{D} 在估计扰动上界k 只需要一个很小的正数来保证 s 微弱的吸引性就行了我常见 0.05~0.5。k 越大抖振越严重这是自适应滑模里最需要控制住的量。4. 仿真结果怎么看4.1 状态收敛曲线跑完上面的代码你会看到 x_1 和 x_2 在大约 3~5 秒内收敛到零附近之后虽然还有很小的波动但整体是稳定的。这里有一个标志性的转折点滑模面 s 从初始值快速下降到零然后一直贴着零轴走。这个转折点对应的就是“到达阶段”结束、“滑动阶段”开始。滑动阶段的状态响应不再是原来非线性系统的样子而是被滑模面降阶成了一阶系统 \dot{x}_1 -c x_1。所以你看到的收敛速度其实是由 c 决定的指数衰减跟原来的 \theta、d(t) 基本无关。这就是滑模控制的降阶特性也是它鲁棒性的来源之一系统在滑模面上的动态非常简单不受原模型不确定性影响。4.2 控制量与抖振如果你第一次跑这个仿真看到 u 的曲线可能会吓一跳一堆锯齿状的高频跳变幅度还不小。这是 sign(s) 项在作用。s 在零点附近来回穿过sign(s) 就在 1 和 -1 之间跳变控制量立刻跟着来回切换。这就是滑模控制著名的“抖振”现象。抖振在仿真里不影响稳定性但在实物上是执行器寿命的大敌。后面我专门讲怎么用边界层或者饱和函数缓和它。在仿真阶段你至少要知道一点如果 u 的抖动幅度明显大于你实际允许的指令范围那就是切换增益或者 \hat{D} 的稳态值过大了需要回头调参数而不是等上了实物再后悔。4.3 参数估计曲线打开参数估计图你会看到 \hat{\theta}_1 和 \hat{\theta}_2 不会严格收敛到真实的 1.2 和 -0.4。它们可能收敛到某个固定值也可能缓慢波动这取决于系统的持续激励条件。这个现象是正常的千万不要以为代码错了。自适应控制的参数辨识收敛需要激励信号持续激励系统而滑模控制的目标只是让状态收敛状态都收敛了激励自然就减弱了参数估计自然不一定收敛到真值。重点看的是在状态收敛过程中\hat{\theta} 和 \hat{D} 是否保持有界。只要估计值不发散、不震荡到离谱控制就是成功的。D hat 那条曲线通常呈现阶梯式的单调上升趋势最后稳定在一个固定值附近它对应的就是系统能够容忍的扰动上界。5. 常见问题与调试经验实录5.1 抖振过大的两个处理手段抖振是滑模控制绕不开的话题我调试时最常用的两个处理手段如下。第一个是边界层法用饱和函数替换 sign 函数function out sat(s, phi) out min(max(s/phi, -1), 1); end当 s 在边界层 |s| phi 内时sat(s) 是连续线性函数控制量平滑过渡超出边界层时它等价于 sign(s)。这个 phi 就是你允许的滑模面误差范围通常取 0.01~0.1。代价是系统在边界层内只剩“准滑动模态”状态不会精确收敛到零而是收敛到零点附近一个与 phi 成正比的邻域内。如果你对稳态精度要求高phi 就取小一点对控制平滑度要求高phi 就取大一点本质是在精度和抖振之间做个折中。第二个是加大自适应增益让系统更快回到滑模面从而减少在切换面附近的停留时间。这个方法在仿真里很有效但增益太大会诱发参数估计振荡。我建议先调 phi再考虑动 gamma。5.2 自适应参数发散另一个高频问题是 \hat{\theta} 或 \hat{D} 越跑越大最后整个仿真直接 NaN。原因通常有三个自适应增益过大、仿真时间过长、系统没有持续激励。参数估计在滑模面附近的本职工作是“收紧上界”而不是一直被激励所以长时间仿真有时会让估计值慢慢漂移。应对办法是在自适应律里加 sigma 修正\dot{\hat{\theta}}_1 \gamma_1 (s \sin(x_1) - \sigma \hat{\theta}_1)\dot{\hat{\theta}}_2 \gamma_2 (s x_2^2 - \sigma \hat{\theta}_2)\dot{\hat{D}} \gamma_3 (|s| - \sigma \hat{D})其中 \sigma 是很小的正数一般取 0.01~0.05。sigma 项的作用是给估计值一个向零拉回的“泄漏”防止漂移。代价是引入了有界稳态误差但滑模控制本来就不要求参数完全收敛所以这点误差在工程上可以接受。代码里实现很简单把自适应行的除法部分减掉一个 sigma 项即可。5.3 ode45跑很慢或者结果异常如果你发现仿真速度极慢或者出来的曲线锯齿密集到没法看多半是数值积分配置的问题。带 sign 函数的系统是典型的刚性系统ode45 是显式 RK 法在开关点附近会非常吃力。我通常检查三件事MaxStep是否设置过小导致步数爆炸或过大导致采样点跳过开关瞬间。一般 0.005~0.05 都算合理区间自己试一次就看得到差异。是否在系统动力学里用了randn之类的随机扰动这会彻底破坏 ode45 的误差估计导致步长来回震荡。仿真里确定性扰动够了随机扰动留给 Monte Carlo 循环再测。是否因为AbsTol太严导致步长被压缩。对控制仿真RelTol1e-6、AbsTol1e-7已经非常充足再严只是白烧 CPU。下面这个速查表是我调试时候经常对照的症状可能原因解决对策控制量抖振剧烈锯齿密sign 切换频繁换成 sat(s, phi)增大 phi参数估计持续漂移sigma 修正缺失或 gamma 过大加入 sigma0.02 修正s 一直不到零附近切换增益 k 太小适当增大 k或增大 gamma3状态收敛太慢c 太小增大 c但观察控制量是否超限仿真结果出现 NaN自适应增益过大估计值爆炸降低 gamma加 sigma 修正曲线看起来毛刺但实际很稳定MaxStep 过大漏采样把 MaxStep 调到 0.005控制输入出现周期性振荡扰动频率高且 D hat 增益大减小 gamma3用低频扰动重测6. 从仿真到实际部署的几个重要提醒6.1 控制器离散化Matlab 里的 ode45 仿真模拟的是连续时间控制器但实物上控制器都是离散的。把上面的连续控制律搬到嵌入式系统时一定要按采样周期 Ts 做离散化[ u_k -\frac{1}{b} \left( \hat{\theta}{1,k} \sin(x{1,k}) \hat{\theta}{2,k} x{2,k}^2 c x_{2,k} k \cdot sat(s_k, \phi) \hat{D}_k \cdot sat(s_k, \phi) \right) ]参数更新也用离散版本[ \hat{\theta}{1,k1} \hat{\theta}{1,k} Ts \cdot \gamma_1 (s_k \sin(x_{1,k}) - \sigma \hat{\theta}_{1,k}) ]这个其实就是一个欧拉积分器Ts 一般取 1~10 毫秒。我在实际项目里发现Ts 越小自适应律和滑模切换越接近连续仿真的效果但执行器有响应延迟太小的 Ts 反而会放大延迟造成的抖动。通常建议先按 1 毫秒跑仿真再逐步拉大到 10 毫秒验证鲁棒性。6.2 执行器饱和与抗积分饱和自适应律有一个隐蔽的风险执行器饱和。当控制输入达到执行器上限时实际施加的系统输入比控制器输出小状态误差一直存在自适应律还在一个劲地增大估计值这时候就出现了类似 PID 积分饱和的问题。等到状态真的接近目标控制量释放出来这些被“积攒”的估计项会让系统猛烈过冲。我踩过这个坑以后习惯在自适应律里加一个条件判断当控制器输出超出执行器限幅时暂停参数更新。实现很简单比如加一个状态标志位或者在 \dot{\hat{D}} 上乘一个系数当 |u| 超限时系数为 0。这个操作很多教科书不提但实物上非常关键。6.3 持续激励与控制目标要分清最后我要强调一个经常被误解的点自适应参数估计不等于参数辨识。你把自适应滑模扔到实际系统上它会给出满足控制目的的估计值但那个值本身不一定有明确的物理意义。如果项目真正的目标是辨识系统参数那要加专门设计的持续激励信号让系统状态充分振荡直到参数估计收敛到真值附近如果项目目标是轨迹跟踪或抗扰那只要状态稳定、估计有界就完全可以交付了。我自己经历过一次项目对方要求控制器同时做到“参数辨识精度 98%”和“跟踪误差小于 0.01”两个指标除了加持续激励信号之外还用了一个分段策略——先让系统在辨识信号下跑一段把 \hat{\theta} 收敛到比较靠谱的值再切换到滑模控制模式做跟踪。混合方案比单纯一种方法管用得多。7. 我的一点实操体会自适应滑模控制这个概念听起来比普通 PID 高级不少但真正上手之后你会发现它的门槛主要在推导那部分代码实现也就一百来行。最大的价值在于它把工程师从“猜不确定性上界”这件事里解放出来了。我做了这么多个非线性控制项目最深的体会是很多时候系统模型并不精确但你不需要追求模型精确因为自适应机制会替你补齐那部分偏差。最后再分享一个小技巧代码里不要用 magic number把所有参数在脚本开头集中写清楚注释上它的物理含义和调参方向。我见过太多人在仿真失败后一个个数字去试效率极低。把参数表记好比如 c 管收敛速度、gamma1 管第一参数估计速度、phi 管抖振抑制力度调试的时候你就知道该动谁而不是瞎试。这套代码跑通之后换个被控对象只需要改closed_loop.m里的动力学方程和自适应律半天时间就能迁移到新项目上。
阅读完成 · 觉得有帮助?