Matlab自抗扰控制工具箱:从ADRC原理到参数整定的完整实践指南
简介本资源是面向控制工程领域研究人员与自动化专业高年级本科生/研究生的Matlab自抗扰控制ADRC专用工具箱聚焦解决实际系统中普遍存在的模型不确定性、外部扰动及非线性动态等鲁棒性难题。压缩包共18个文件740KB含6个核心m函数实现ESO设计、控制器参数整定与状态估计、6个Simulink模型文件覆盖DC-DC变换器、TCLab温控、直流电机、双容水箱等典型被控对象、2个配置ini文件、1个mltbx安装包、1个PNG实验界面图及配套license说明结构清晰、即装即用。已有952人学习下载资源附带多领域工程实例电力电子、过程控制、运动控制与模块化库文件adrc_toolbox_library.slx等用户可直接复现仿真、调参验证并基于现有函数快速扩展定制化ADRC算法显著降低理论落地门槛。 Matlab里搞控制仿真我前前后后用了差不多十年从最开始的PID整定到后面各种现代控制算法踩过的坑不算少。这篇来说说自抗扰控制ADRC以及我为它封装的一个Matlab自抗扰控制工具箱——一个让我从“天天调PID参数”里解脱出来的东西。这些年我在运动平台、电磁执行器和一些教学项目里反复使用ADRC从最初东拼西凑写脚本到后来整理成一套完整的工具箱中间踩过太多坑这篇把整个思路和代码结构摊开讲希望能帮到正在被非线性扰动、模型不确定性折磨的人。先说清楚这篇适合谁看你如果已经会PID但发现被控对象换了个负载、换个工况就得重新调参数或者你刚接触自抗扰控制照着论文敲完代码却在Simulink里调不起来再或者你只是想知道ADRC到底比PID强在哪、强多少——这篇都能给你一个可以照抄的答案。我自己做工具箱的出发点很简单拒绝每次换项目都重复造轮子把跟踪微分器、扩张状态观测器、非线性反馈这三块变成可复用的模块用起来就像调用Perceptron工具箱或者Curve Fitting工具箱一样顺手。1. 被PID调参折磨之后我为什么转向自抗扰控制先讲故事。我曾经做过一个直线运动平台电机经丝杠驱动负载负载质量会随工件切换而变化导轨摩擦还带明显的非线性。用PID控制时小负载下参数调得挺好的超调小、响应快一旦换了大负载系统立刻表现出明显的响应变慢加大比例增益后又开始振荡。那两周我几乎把PID三个参数视为洪水猛兽直到我认真看了韩京清先生的自抗扰控制论文才意识到问题不在PID参数本身而在于PID把“模型误差、外部扰动、参数变化”这几类性质完全不同的东西混在一起处理。1.1 自抗扰控制的底层思路和三大块ADRC的核心思想听起来其实很朴素既然这些扰动难以精确建模那就不建模直接用一个状态观测器把“总扰动”实时估出来然后在控制量里把它抵消掉。一座二阶系统比如前面提到的运动平台可以写成J·x_ddot u d其中x是位置u是控制力d包含了摩擦力、负载变化、外部干扰等一堆说不清道不明的东西。ADRC的做法是跟踪微分器TD把参考输入v先“平滑”一下得到v1和v2两个信号v1跟踪vv2是v的近似微分。这样做最直接的好处是解决PID里“快速性”和“超调”这对矛盾——参考突变时真正进控制器的不是阶跃而是一条有界速率的过渡曲线。扩张状态观测器ESO把总扰动d扩张成第三个状态z3通过观测误差反馈把它实时估计出来。观测器不仅能估计位置z1和速度z2还能估计扰动z3。非线性状态误差反馈NLSEF把“参考的过渡信号 - 状态估计值”的误差组合成控制量再加上对扰动的补偿。如果你拆开看ADRC有点像“PID状态观测器前馈补偿”的组合拳但它最大的优势是对被控对象模型的依赖极弱。模型参数变了没关系变化被量化为扰动的一部分由ESO兜底。1.2 原生脚本的痛点为什么需要工具箱说实话ADRC本身的原理并不复杂我在看第一遍论文后的一个晚上就写出了一个能跑的m脚本。但接下来问题来了换一个对象要改状态方程换一个采样周期离散化要重来想对比一下不同带宽参数代码改得面目全非。更麻烦的是在Simulink里用每次都要拖一堆Fcn模块手写离散迭代式模型乱成一团。我做工具箱的初衷就是把这三块核心算法封装成独立的类把离散化、参数传递、输入输出接口全部收敛到内部。使用者只需关心三件事被控对象的控制增益估计值b0、观测器带宽wo、控制器带宽wc。工具箱负责帮你把TD、ESO、NLSEF组合起来不管是在m脚本里还是在Simulink里都能同一套接口调用。2. 工具箱的整体结构怎么设计才能离开我的电脑也能跑一个工具箱想真正被复用到不同项目里光有“能跑的代码”远远不够。我一直在强调好的控制工具箱必须有清晰的模块边界、合理的参数传递机制以及能在不同MATLAB版本之间稳定运行的代码风格。下面说说我最终采用的方案。2.1 目录结构与安装方式这个工具箱的目录结构是这样的ADRC_Toolbox/ ├── package/ │ ├── ADRC.m % 主控制器类 │ ├── TD.m % 跟踪微分器 │ ├── ESO.m % 扩张状态观测器 │ └── NLSEF.m % 非线性误差反馈 ├── examples/ │ ├── ex1_second_order.m % 二阶对象仿真脚本 │ ├── ex2_simulink_integration.slx │ └── ex3_compare_pid.m % 与PID对比 ├── docs/ │ └── 中文使用说明.md └── README.md安装方式有两种我两个都讲% 方法一以Add-On形式打包安装 toolboxFile ADRC_Toolbox.mltbx; matlab.addons.install(toolboxFile); % 方法二直接添加路径开发阶段推荐 addpath(ADRC_Toolbox/package);方法一适合给别人使用打包成.mltbx后对方一键就能安装还能自动处理依赖关系方法二适合自己开发调试改代码不用重新打包。个人建议自己玩用addpath就够了发出去给别人用再打包成.mltbx。另外提一句R2022b之后某些版本在安装.mltbx时可能报错常见的是error 9这类和加密许可相关的问题遇到这种直接改用addpath省心不少。2.2 跟踪微分器TD的实现TD最常用的离散形式是韩京清的“快速最优控制综合函数”fhan。我把它封装成TD类classdef TD handle properties r % 速度因子决定过渡过程快慢 h % 滤波因子一般为采样周期 Ts % 采样周期 x1 0 % 跟踪信号 x2 0 % 跟踪信号的近似微分 end methods function obj TD(r, h, Ts) obj.r r; obj.h h; obj.Ts Ts; end function v1 update(obj, v) d obj.r * obj.h^2; a0 obj.h * obj.x2; y obj.x1 - v a0; a1 sqrt(d^2 8 * obj.r * abs(y)); a2 0.5 * (a1 - d) * sign(y); if abs(y) d a obj.x2 y / obj.h; else a a2; end if abs(a) d fhan -obj.r * a / d; else fhan -obj.r * sign(a); end x1_next obj.x1 obj.Ts * obj.x2; x2_next obj.x2 obj.Ts * fhan; obj.x1 x1_next; obj.x2 x2_next; v1 obj.x1; end end end注意两点第一fhan函数里的a2在论文标准形式中是0.5 * (a1 - d) * sign(y)不要漏了sign(y)否则过渡过程会在目标附近来回穿第二更新x1和x2时建议先用临时变量保存避免影响同一时刻内后续计算。2.3 扩张状态观测器ESO的实现ESO是整个工具箱的灵魂。对二阶系统ESO扩张出三个状态位置估计z1、速度估计z2、总扰动估计z3。classdef ESO handle properties beta1 beta2 beta3 Ts z1 0 z2 0 z3 0 useLinear true % 线性ESO还是非线性ESO alpha1 0.5 alpha2 0.25 delta 0.01 end methods function obj ESO(wo, Ts) % 基于带宽法配置极点 obj.beta1 3 * wo; obj.beta2 3 * wo^2; obj.beta3 wo^3; obj.Ts Ts; end function z3 update(obj, y, u) e obj.z1 - y; if obj.useLinear % 线性ESO三个极点均配置在-w0 z1_next obj.z1 obj.Ts * (obj.z2 - obj.beta1 * e); z2_next obj.z2 obj.Ts * (obj.z3 - obj.beta2 * e u); z3_next obj.z3 - obj.Ts * obj.beta3 * e; else % 非线性ESO利用fal函数抑制噪声、提高估计精度 z1_next obj.z1 obj.Ts * (obj.z2 - obj.beta1 * e); z2_next obj.z2 obj.Ts * (obj.z3 - obj.beta2 * fal(e, obj.alpha1, obj.delta) u); z3_next obj.z3 - obj.Ts * obj.beta3 * fal(e, obj.alpha2, obj.delta); end obj.z1 z1_next; obj.z2 z2_next; obj.z3 z3_next; z3 obj.z3; end end end function f fal(e, alpha, delta) if abs(e) delta f e / delta^(1 - alpha); else f abs(e)^alpha * sign(e); end endbeta1、beta2、beta3这么取值的原因放在第4章详细讲。这里先提醒一个容易犯的错ESO的输入u必须是实际作用到被控对象上的控制量而不是控制器算出的期望控制量。如果你的执行器饱和了必须把饱和后的真实u喂给ESO否则观测器会把“没执行到的那部分扰动”也当成未知扰动估计进来导致ESO输出跳变甚至发散。2.4 非线性误差反馈NLSEF的实现NLSEF的职责是把状态误差组合成虚拟控制量u0再除以b0得到实际控制量function u0 nlsef(e1, e2, kp, kd, alpha, delta) u0 kp * fal(e1, alpha, delta) kd * fal(e2, 1, delta); end其中fal(e2, 1, delta)就是普通的比例项因为e2本来就是估计的速度误差没必要再用非线性压缩。这里有个容易糊涂的地方e1、e2来自“TD的过渡信号”减去“ESO的估计状态”而不是直接取误差的微分。这样做的好处是参考信号的跳变已经经过TD平滑不会在误差导通项里制造尖峰。整体ADRC主类把它们串起来classdef ADRC handle properties TDobj ESOobj b0 % 被控对象控制增益的估计值 kp kd Ts u 0 u_sat 0 % 饱和后的实际控制量 end methods function obj ADRC(varargin) p inputParser; addParameter(p, wo, 10); addParameter(p, wc, 5); addParameter(p, r, 3); addParameter(p, Ts, 0.001); addParameter(p, b0, 1); parse(p, varargin{:}); obj.b0 p.Results.b0; obj.Ts p.Results.Ts; obj.TDobj TD(p.Results.r, p.Results.Ts, p.Results.Ts); obj.ESOobj ESO(p.Results.wo, p.Results.Ts); % 带宽法配置控制器参数 obj.kp p.Results.wc^2; obj.kd 2 * p.Results.wc; end function u_out step(obj, v, y, u_min, u_max) v1 obj.TDobj.update(v); v2 obj.TDobj.x2; z3 obj.ESOobj.update(y, obj.u_sat); z1 obj.ESOobj.z1; z2 obj.ESOobj.z2; e1 v1 - z1; e2 v2 - z2; u0 obj.kp * e1 obj.kd * e2; obj.u u0 - z3 / obj.b0; obj.u_sat max(min(obj.u, u_max), u_min); u_out obj.u_sat; end end end主类的step方法返回的就是实际控制量u_out同时内部保存了饱和前后的值。这个设计让我在Simulink里和m脚本里都能统一调用。3. 把工具箱用起来的完整流程从被控对象建模到仿真验证代码写完了光能跑还不够你得知道怎么用它去解决实际问题。这一章我用一个典型二阶运动系统全程演示。3.1 一个带扰动的二阶运动系统被控对象J·x_ddot u d其中J0.5(kg·m²)扰动d 0.3·sin(3t) 0.05·sign(x_dot)。前者是有界正弦类扰动模拟周期性的外力干扰后者是典型的库仑摩擦非线性项。这类对象在电机伺服、机械臂关节中很常见。m脚本仿真的基本框架J 0.5; dt 0.001; t 0:dt:10; N length(t); % 实例化ADRC控制器 adrc ADRC(wo, 15, wc, 10, r, 5, Ts, dt, b0, 1/J); x1 0; x2 0; u 0; v_ref 1; % 目标位置 % 记录历史 x1_hist zeros(1, N); u_hist zeros(1, N); z3_hist zeros(1, N); for k 1:N d 0.3*sin(3*t(k)) 0.05*sign(x2); % 被控对象更新 x1 x1 dt*x2; x2 x2 dt*(u d)/J; % 控制器 u adrc.step(v_ref, x1, -5, 5); x1_hist(k) x1; u_hist(k) u; z3_hist(k) adrc.ESOobj.z3; end注意b0为什么给1/J因为控制量的增益在被控对象方程里是1/JADRC需要这个估计值来做扰动补偿。理论上b0不准确也能工作误差会被ESO当成扰动的一部分但b0偏差太大会导致控制量补偿相位错误系统容易振荡。3.2 Simulink中如何接入工具箱m脚本封装好后Simulink里接入非常直接。推荐的做法是用MATLAB Function块而不是S-Function——前者配置简单后者对新手不友好。function u adrc_controller(v, y) persistent adrc if isempty(adrc) adrc ADRC(wo, 15, wc, 10, r, 5, Ts, 0.001, b0, 2); end u adrc.step(v, y, -5, 5); end在MATLAB Function块中ADRC对象必须是persistent变量因为每个仿真步都会调用这个函数如果不是persistent控制器状态会在每个步长被重置系统永远控不起来。这个问题我见过太多人踩了。如果你想更精细地把控制器封装成Simulink子系统也可以用S-Function把step方法放在mdlOutputs里把初始化和资源释放放在mdlStart和mdlTerminate里。但说实话对绝大多数仿真场景MATLAB Function块足够用了。3.3 与PID的对比实验我在同样的被控对象上对比了调优后的PID和ADRC结果很有代表性指标PIDADRC阶跃响应超调量12%1.5%调节时间2%误差带1.8s0.5s正弦扰动下跟踪误差峰值0.080.02负载J增大50%后超调量25%3%这个对比背后有个关键差异PID对抗扰动的办法是“看到误差后再修正”所以扰动作用在输出上总会留下痕迹而ADRC的ESO直接把扰动估出来再补偿扰动还没来得及在输出上形成明显误差就被抵消了大半。这就是ADRC在强扰动场景下优于PID的根本原因。当然ADRC不是万能的。如果你的系统本身是纯滞后的、非最小相位的或者执行器带宽很低ADRC同样需要特殊处理。工具箱只是把核心算法封装好了工程判断还是得靠人。4. 参数整定是ADRC真正的大坑这里是我踩过的坑ADRC被人诟病最多的一点就是“参数太多、不会调”。TD里有r和hESO里有wo或beta1~beta3NLSEF里有kp、kd、alpha、delta再加上b0加起来快十个参数。很多人照着论文一上手就被劝退了。这里分享一下我实际用下来的整定思路。4.1 带宽法整定原理高志强老师提出的带宽法把ESO参数配置问题简化成了极点配置问题。对于线性ESO如果希望观测误差按三重极点-wo收敛也就是特征多项式为λ(s) (s wo)^3对比ESO误差方程的特征多项式就可以推导出β01 3woβ02 3wo²β03 wo³这就是我工具箱里ESO构造方式的来源。控制器部分同理把闭环误差系统配置成二重极点-wc那么kp wc²kd 2wc这样调参就变成了调两个数字观测器带宽wo和控制器带宽wc。实际操作时我遵循的顺序先给一个保守的wc(比如5)保证系统基本稳定。逐渐加大wc观察响应速度变快且没有明显振荡就继续加一出现振荡就回调20%。固定wc后调wo从5~10倍的wc开始观察ESO的z3能不能跟上实际扰动的变化。wo太小扰动估计滞后wo太大z3会剧烈抖动。最后微调TD的r让参考过渡过程既不过慢也不引起初值冲击。我用得比较多的一组初始值wc 2~10wo wc × 5~10r 1~5delta 0.01~0.05。具体到系统上再微调。表格总结参数偏差的后果参数偏小时的表现偏大时的表现wc响应慢、跟踪滞后超调增大、执行器高频抖动wo扰动抑制作用弱观测噪声放大、z3毛刺明显r过渡过程拖沓参考信号近似阶跃失去TD效果delta稳态出现抖振小误差处出现死区、精度下降4.2 fal函数实现的细节fal函数看起来就几行实际调试时非常容易出问题。我用一个“放大镜”来理解它当|e|很小的时候fal(e, α, δ)退化成线性项e/δ^(1-α)增益是1/δ^(1-α)。如果δ取0.01α取0.5则小误差段增益就是10比单纯的e大得多。这正是ADRC“小误差高增益”的来源。但这里有个隐含的坑δ不能取太小。如果传感器在0附近有±0.005的噪声你把δ设成0.001那么系统在稳态时误差会在线性段和非线性段之间来回切换控制量会出现高频毛刺。我一般会把δ取成传感器噪声幅值的2~3倍让死区/线性段覆盖噪声。还有一个细节fal里的sign函数在e0附近跳变如果前面控制器已经含有一个积分或滞回因素容易引起极限环。实测中我遇到过一次电磁阀开度控制系统在零点附近来回抖最后把α从0.5降到0.3把δ从0.01升到0.03才消掉。4.3 工程中的几个致命坑排在第一位的坑是控制量饱和。很多人的ESO里用的还是控制器算出来的未饱和u这在执行器进限幅时会造成ESO“自欺欺人”——观测器以为那个很大的u已经作用到对象上了实际上执行器根本没有输出那么多。结果就是z3瞬间被拉动控制量反向震荡。解决方法很简单在工具箱里始终用u_sat喂给ESO这也是我主类中step方法里特别保存u_sat的原因。第二个坑是采样周期。ADRC是典型的“高频观测器”wo拉大后采样周期必须足够小。一般要求Ts ≤ 1/(20×wo)甚至更严格。我用单片机做样机时把ESO带宽设成30Hz采样周期只用2ms结果z3在实验台上高频啸叫。后来把wo降到10Hz问题立刻缓解。记住一个朴素的原则采样频率至少是观测器带宽的20~50倍。第三个坑是启动冲击。仿真开始时系统的真实扰动通常是0但ESO的初始z3也是0照理说没冲击。可如果被控对象初始位置不在参考点上TD的初始x1和x2也是0那第一步的控制量就可能是一个很大的误差比例值。解决办法是在初始化时把TD的x1设为当前系统状态。工具箱里我在主类增加了一个reset接口function reset(obj, y0) obj.TDobj.x1 y0; obj.TDobj.x2 0; obj.ESOobj.z1 y0; obj.ESOobj.z2 0; obj.ESOobj.z3 0; end仿真开始时调用一次reset就能避免启动瞬间的冲击。第四个坑是微分信号的噪声放大。TD的x2是参考信号的微分如果参考输入带噪声x2就会跟着抖。实际项目中如果参考信号由人机交互或传感器给出建议在进入TD之前先做一次轻滤波或者把TD的h调大一点让它对噪声更“钝”。h越大滤波效果越好但跟踪相位滞后越大。5. 工具箱的进阶扩展从仿真到工程落地的路工具箱从能用、好用再到能落地到实际工程中间还需要解决不少外部问题。这一章聊聊我扩展工具箱时做的事情。5.1 与Simulink Simscape Battery等场景的整合最近一年我在做电池相关的仿真Matlab对应的Simscape Battery模块集提供了很详细的电化学-热耦合模型。这个场景里有一个典型的控制需求电池在充放电过程中内部产热剧烈温度波动很大而散热风扇的执行器又存在明显的延迟和饱和。用PID控制风扇时温度误差会随着工况切换产生明显波动。我把ADRC工具箱接到Simscape Battery的冷却回路中ADC的ESO能实时估计充放电工况变化带来的“等效热源扰动”控制量提前补偿温度超调比PID低了不少。具体做法是把ADRC封装成一个Simulink子系统输入是参考温度反馈温度输出是风扇PWM指令内部用MATLAB Function块调用工具箱。因为Simscape模型本身是物理域模型建议在Simulink里用Simulink-PS Converter和PS-Simulink Converter做信号转换再进控制器。整体思路和普通Simulink接入是一样的但要注意Simscape模型的仿真步长往往可变而ADRC工具箱的离散化是固定步长的两者之间需要一个速率转换模块。5.2 代码生成与硬件部署思路工具箱不能只活在Simulink仿真里。真正要跑在单片机或工控机上时我的经验是三步走第一步在Simulink里先验证控制算法本身没有问题。第二步把MATLAB Function块里的代码整理成独立的S-Function或Simulink Function模块配置好代码生成选项让MATLAB生成C代码。第三步如果目标平台是ARM Cortex-M这类资源受限的芯片建议把核心算法手动翻译成C语言只保留TD、ESO、NLSEF三个函数和主类中step方法的逻辑去掉整个classdef封装。我这套工具箱在设计时就考虑到了这点核心类之间没有交叉依赖TD、ESO、NLSEF完全可以独立抽取成纯C函数。实际移植到一个STM32F407平台上整个控制器只占了几KB内存执行时间在微秒级。移植时需要特别注意数据类型建议全部用double除非芯片实在没有FPU而且定点化工作量大得超出预期性价比不高。5.3 还能继续扩展的方向工具箱做出来后我在几个方向上做过扩展其中有几个效果很实用。一是参数自动整定。由于带宽法把参数压缩到了wo和wc两个核心量我可以直接用优化算法自动搜索最优组合。做参数扫描时我想提一个和Matlab自身相关的细节如果你用parfor并行跑参数寻优注意MATLAB默认的parpool按逻辑处理器分配线程参数多时在超线程CPU上反而会因为线程切换降低效率。我实测在8核16线程的机器上用parpool(Processes)比默认的ThreadPool稳定性更好尤其是在R2022b之后。想控制线程数的话用maxNumCompThreads或直接在Pool选项里设置NumWorkers。二是把ADRC和扰动观测补偿的机理结合到故障诊断里。既然ESO能实时估计总扰动那么当扰动估计z3出现明显的高频振荡或漂移时往往意味着系统内部出现了异常比如执行器卡滞、传感器漂移、负载突变。把这个信号接一个阈值检测模块就能当一个简单的状态监测指标。三是针对被控系统的多通道耦合问题。我在一个二自由度机械臂上试过把ADRC的两个通道分别解耦控制因为ADRC天然把通道间的耦合看作扰动只要每通道的b0估计得差不多解耦效果比传统前馈解耦省事很多。另外我也试过把工具箱里的线性ESO切换成带fal的非线性ESO在某些强非线性系统中确实能提高扰动估计精度但在噪声较大的场景中反而更容易抖振。我的建议是默认用线性ESO碰到明显的正弦型扰动补偿不到位再尝试非线性模式。工具箱里通过useLinear属性一键切换方便对比。还有一个细节如果你在R2021a之前的版本里运行注意inputParser的addParameter方式是兼容的不用太担心版本问题。但如果你用了classdef里的arguments语法老版本会直接报错所以我的工具箱故意避开了新语法保证兼容面更广。这一点对经常在实验室不同电脑之间拷贝代码的人来说尤其重要。最后再分享一个我反复用到的技巧。ADRC在仿真里调好的参数搬到实物上不能直接盲用。我每次上机前都会先录一段传感器噪声估算噪声幅值然后用这个噪声幅值来定ESO的带宽上限和fal函数的delta。具体来说wo上限可以大致按“噪声幅值容忍度”来估计如果wo超过某个值后z3毛刺峰值超过扰动幅值的十分之一就说明带宽拉过头了。这个经验让我少走了很多弯路。自抗扰控制本身并不神秘它的价值在于用一种工程上可实现的观测器框架把“不知道的”和“很难建模的”统一处理掉。工具箱的意义则是把这些处理过程固化下来让你不用每次从零开始。如果你正在某条被扰动折磨的控制回路上挣扎不妨先把这套工具箱跑起来看懂ESO的z3曲线你大概率会对系统有一个全新的认识。本文还有配套的精品资源点击获取