资讯详情 ADRC自抗扰控制Simulink仿真:从原理到参数整定避坑指南
📅 2026/10/13 18:26:18
简介面向控制理论与自动化领域的自抗扰控制仿真资源采用MATLAB/Simulink搭建适合正在学习自抗扰控制、希望深入理解扩张状态观测器与扰动补偿技术的工程师和研究人员。资料包内含完整的ADRC仿真实现核心部分是基于S函数编写的控制器同时配备非线性扩张状态观测器、被控对象模型以及参数调校脚本能够完整演示误差观测、扩展状态建模、控制器设计、参数整定等关键步骤帮助读者从原理推导到仿真验证建立系统认知。资源包共26个文件涵盖m脚本、mat数据、mdl模型、c与h源码、mexw64预编译模块等多种文件类型压缩包大小仅191KB结构紧凑且便于在不同环境中复用。目前已有3018人学习下载足以说明其参考价值。运行仿真模型后读者可以直观观察系统在外部扰动下的动态响应通过修改观测器增益和控制参数对比性能变化为后续工程实践打下基础。1. ADRC不是玄学一套Matlab仿真帮你把扰动压下去做控制仿真的人大多遇到过这种场景PID参数调了半天负载一变化或者外界扰动一来曲线就开始抖甚至发散。自抗扰控制ADRC正是冲着这个问题来的它把模型不确定性和外部扰动统一当成“总扰动”通过扩张状态观测器实时估计并补偿掉在Matlab/Simulink里搭建一套ADRC仿真模型往往能比传统PID更快把扰动压下去还不依赖精确的被控对象模型。这篇笔记围绕adrc simulink的完整落地展开从三件套原理讲到参数整定最后给出我实际踩过的坑和进阶用法。适合正在做控制仿真课程设计、电机调速或运动控制仿真的从业者也适合想从PID转向ADRC却不知道第一步怎么下手的人。2. 自抗治控制的三件套跟踪微分器、扩张状态观测器与误差反馈ADRC的核心结构是“三件套”跟踪微分器TD安排过渡过程扩张状态观测器ESO估计状态和总扰动非线性状态误差反馈NLSEF生成控制量。先把这个结构在Matlab/Simulink里搭出来再谈参数整定才有意义。2.1 为什么是三个环节而不是直接用一个观测器传统状态观测器只能估计系统的内部状态对模型误差和外部扰动是无能为力的。ADRC聪明在把“模型未知项外部扰动”当成一个额外的状态——扩张状态用ESO把它观测出来再在控制量里减去这个估计值于是系统就被“线性化”成一个串联积分器。TD的作用则更朴实给期望输入规划一个平滑过渡过程避免阶跃跳变直接冲击被控对象也让ESO的估计压力小一些。NLSEF则负责把状态误差组合成控制量组合方式可以用线性也可以用非线性考虑到工程实现和调参复杂度目前大多数仿真都先用线性组合效果已经很不错。在Simulink里搭建ADRC有三种常见做法纯模块拖拽用积分器、增益、求和搭建、使用S-Function、使用MATLAB Function模块。我一般推荐第三种。纯模块拖拽模型会很凌乱改参数要层层点开S-Function需要额外处理编译设置MATLAB Function模块既保留图形化信号线又能在代码里写清数学表达式非常适合快速迭代。下面给出三个核心模块的Matlab函数代码。2.2 跟踪微分器TD的离散实现TD的离散形式有很多种最常用的是最速控制综合函数fhan。在MATLAB Function模块里状态方程写成如下形式function [x1, x2] fcn(v, x1_prev, x2_prev, r, h0) % 最速跟踪微分器离散迭代 % v: 期望输入阶跃或时变信号 % r: 跟踪速度因子越大跟踪越快 % h0: 滤波因子通常取略大于仿真步长 persistent x1 x2 if isempty(x1) x1 0; x2 0; else x1 x1_prev; x2 x2_prev; end d r * h0 * h0; a0 h0 * x2; y x1 - v a0; a1 sqrt(d * (d 8 * abs(y))); a2 a0 sign(y) * (a1 - d) / 2; sa sign(y d) - sign(y - d); % 处理符号切换 a (a0 y - a2) * sa a2; sa2 sign(a d) - sign(a - d); % 最速控制量 fst -r * (a / d - sign(a)) * sa2 - r * sign(a); x1 x1 h0 * x2; x2 x2 h0 * fst; end这段代码实现了最速跟踪微分器的离散递推。注意persistent的使用会限制MATLAB Function的代码生成后面第5章讲代码生成时我会建议改用S-Function或外部m函数。参数r决定跟踪快慢我通常先把r设为目标跟踪带宽的25倍再微调。h0是滤波因子取仿真步长的13倍比较稳妥太小会引入噪声太大跟踪会滞后。实际仿真时建议把TD输出speed形状的x1信号连到示波器和原始阶跃信号对比观察过渡过程是否平滑。2.3 扩张状态观测器ESO的线性与非线性写法ESO是ADRC的心脏。对于二阶被控对象y f bu线性ESO可以写成如下离散形式function [z1, z2, z3] fcn(y, u, dt, w0, b0) % 线性扩张状态观测器 % y: 被控量测量值 % u: 控制量输出 % w0: 观测器带宽rad/s % b0: 控制量增益的估计值 persistent z1p z2p z3p if isempty(z1p) z1p 0; z2p 0; z3p 0; end % 误差 e z1p - y; % 增益配置: 通常取 3*w0, 3*w0^2, w0^3 l1 3 * w0; l2 3 * w0 * w0; l3 w0 * w0 * w0; % 状态更新 z1 z1p dt * (z2p - l1 * e); z2 z2p dt * (z3p - l2 * e b0 * u); z3 z3p dt * (-l3 * e); % 更新持久变量 z1p z1; z2p z2; z3p z3; end这个线性ESO的代码简洁参数只有w0和b0两个。增益取3w0, 3w0^2, w0^3是为了让观测误差特征方程的极点都落在-w0处这是高增益观测器的典型配置。b0是被控对象中控制量系数b的估计整定ADRC的大部分“玄学”其实就集中在b0上后面第3章会详细说。如果被控对象有明显非线性可以把上面代码中的误差项改为非线性函数fal(e, alpha, delta)但我建议先线性起步线性ESO配合适当带宽已经能应对大多数仿真场景。实际把这段函数放进Simulink时注意u要取上一时刻的控制量避免代数环具体避坑在第4章。2.4 误差反馈与补偿NLSEF和扰动补偿的合流得到状态估计z1, z2, z3之后控制量由两部分构成误差反馈部分和扰动补偿部分。典型的线性ADRC控制律是function u fcn(v, x1, x2, z2, z3, b0, wc) % 仅使用ESO估计的z2速度估计和z3总扰动估计 % 误差: 期望状态与估计状态之差 e1 v - x1; % 位置误差v是经过TD平滑后的期望 e2 0 - z2; % 速度误差期望速度为0对设定值跟踪 % 控制量: 误差反馈 扰动补偿 u0 wc * wc * e1 2 * wc * e2; u u0 - z3 / b0; end这里u0是PD形式的反馈控制wc是控制器带宽z3/b0是扰动补偿项。注意这个模块的输入中x1是TD输出的平滑位置信号而不是被控对象的输出y这是ADRC的典型结构用TD输出做期望轨迹用ESO的z2做微分信号避免直接对测量信号求微分的噪声放大问题。如果被控对象是速度系统而不是位置系统那么TD就输出平滑速度ESO的z1对应速度测量反馈律相应调整。把这三段代码分别放进三个MATLAB Function模块连线就得到最简ADRC闭环。为了验证搭建正确我给一个最简单的二阶积分对象G(s)1/s^2做测试期望输入阶跃1仿真步长0.001秒先用w030, wc20, b01观察响应。这个测试的好处是b0精确已知如果响应发散几乎可以肯定是ESO或TD的代码错了。我会在Simulink里加一个阶跃源一个积分串积分然后把控制量接到对象输入示波器看y和v曲线。第一次跑通时你会看到y在约0.5秒内无超调地跟上v扰动被z3估计出来并补偿掉这就是ADRC最直观的效果。3. 参数整定实战从Simulink模型到阶跃响应曲线ADRC参数比PID多核心参数是w0观测器带宽、wc控制器带宽、b0控制增益估计和TD的r、h0。很多人一开始把参数凑不出来就把问题归咎于“玄学”其实参数之间是有主次关系的。我的整定思路是先定b0再调w0然后调wc最后回头修正b0。这一章给你一套可以照抄的流程。3.1 参数优先级b0排第一w0排第二先解释为什么b0最优先。观察ESO的更新方程z3是对总扰动的估计而总扰动中包含(b-b0)*u项。如果b0和真实b偏差太大那么控制量本身就会被当成“扰动”的一部分导致观测器状态紊乱。经典论文里常说“b0不需要精确误差可以靠ESO补偿”这句话在仿真里容易误导人——仿真模型是确定的b可以精确知道你完全可以先把b0设成真实值的0.52倍去测试但不要一上来就给b0一个数量级偏差。我常用的标定方法是先把ADRC中的TD输出直接接到被控对象输入旁路掉ESO和反馈给对象一个单位阶跃记录对象加速度的稳态值。比如对象为y 2u d单位阶跃u1时稳态加速度接近2那么b0就取2。对于不明确的对象可以尝试几个b0值观察ESO的z3估计稳定值。下面给一个简单的参数扫描脚本用sim函数批处理% 参数扫描: 固定w0, 改变b0, 记录ITAE指标 clear; clc; b0_list [0.5, 1, 2, 4]; w0 30; wc 20; results zeros(length(b0_list), 2); for i 1:length(b0_list) b0 b0_list(i); % 设置Simulink模型工作区变量 assignin(base, b0, b0); assignin(base, w0, w0); assignin(base, wc, wc); % 运行模型 sim(adrc_test_model, 5); % 仿真时长5秒 % 从out结构获取时间与输出 t out.tout; y out.yout{1}.Values.Data; e 1 - y; % 期望为1 ITAE sum(t .* abs(e)) * (t(2)-t(1)); % 离散积分 results(i,:) [b0, ITAE]; end % 打印结果 disp(table(b0_list, results(:,2), VariableNames, {b0, ITAE}));这个脚本的关键在于通过assignin把工作区变量注入模型避免手动改Simulink对话框。out.yout{1}是信号记录格式如果你的模型配置不同可能需要换成out.logsout或其他名称。ITAE指标用时间乘以绝对误差再积分超调越小、跟踪越快ITAE越小。实际运行时建议从b00.5开始你会发现当b0偏小时阶跃响应出现低频振荡偏大时响应迟钝且z3波动变大。选定ITAE最小的b0后再进入下一步。3.2 w0的整定太快会抖太慢会滞后观测器带宽w0决定ESO跟踪状态和扰动的速度。w0太小z3跟不上实际扰动补偿滞后闭环会表现出“低频慢摆”或稳态偏差w0太大z1和z2会被测量噪声放大控制量剧烈颤动。一个实用的整定步骤是把wc固定在一个中等值比如20从较小的w0开始每次乘以1.5观察控制量u曲线。如果u出现周期性的毛刺说明w0超过上限。对于步长0.001秒的仿真w0通常在10100之间。下面给出一个自动化的时域指标输出函数帮你判断w0是否合适function [overshoot, settling_time, u_max] analyze_resp(t, y, u, step_final) % 计算超调量, 调节时间(进入5%误差带), 控制量峰值 final_val step_final; overshoot max(y(2:end)) - final_val; if overshoot 0, overshoot 0; end overshoot overshoot / final_val * 100; idx find(abs(y - final_val) 0.05 * final_val, 1, last); if isempty(idx) settling_time t(end); else settling_time t(idx); end u_max max(abs(u)); end把每次仿真的t,y,u传入该函数就能得到三个量化指标。整定w0时我通常观察overshoot是否小于5%u_max是否在可接受范围。注意如果噪声是固定幅值的白噪声w0增大一倍u_max可能增大好几倍这是高增益观测器的固有代价。在实际项目中如果传感器噪声明显你甚至要降低w0来换取平滑控制量这就是带宽和鲁棒性的取舍。3.3 wc和TD参数的最后微调当前两步确定后wc相对好调。wc越大反馈越激进响应越快但鲁棒性变差。我的习惯是wc取w0的1/31/2先保证ESO比控制器快否则控制器用到的状态估计不准。wc的调整可以直接观察阶跃响应的超调和上升时间通常每次增加20%。TD参数r和h0的影响在阶跃响应早期最明显。r越大v跟踪阶跃越快但v的加速度越大对ESO的负担越重。如果r设得太大你会发现u在初始时刻有一个很大的尖峰因为控制器想立刻消除v和y之间的误差。h0主要是滤波作用在仿真里不要超过步长的3倍否则TD会有相位滞后。最后微调阶段我经常使用循环脚本同时扫描wc和r% 联合扫描wc和r wc_list [10, 20, 40]; r_list [50, 100, 200]; best_score inf; for wc_val wc_list for r_val r_list assignin(base, wc, wc_val); assignin(base, r, r_val); sim(adrc_test_model, 5); % 计算指标略 score 0.5 * overshoot 0.1 * settling_time 0.4 * u_max; if score best_score best_score score; disp([wc_val, r_val]); end end end注意这里的score是我自定义的加权指标实际项目中你可能更关注超调或者控制量峰值权重需要调整。联调完成后我建议保存一组参数到mat文件方便后续加载。对于非线性对象还可以在仿真中加一个正弦扰动信号验证ESO的估计能力在对象输入端叠加0.1*sin(2*pi*t)然后看z3曲线是否与扰动幅值接近。这一步能帮你确认ESO带宽是否足够。如果z3波形的幅值明显小于实际扰动说明w0偏低。4. 避坑指南ADRC仿真里最常见的五个翻车现场ADRC仿真报错和曲线异常的原因比PID多。这一章列出我实际调试中遇到的五个典型问题每个都按照“现象-原因-解决”来写给正在复现的人省点时间。4.1 代数环报错MATLAB Function输入输出直接相连现象Simulink提示“Algebraic loop detected”或者仿真运行极慢计算不出来结果。原因ESO的控制量输入u经过控制律模块又回到ESO的输入两者在同一仿真步内互相依赖构成代数环。在我第2章的ESO代码中u直接使用了当前时刻的控制量而控制量又依赖ESO输出的z3这就必然导致代数环。解决最常见的处理是将控制量延迟一个周期。在ESO的输入侧串联一个Unit Delay模块或者修改ESO函数使用上一时刻的控制量。我习惯把控制律模块计算出的u送到一个Memory模块再从那里面给ESO提供u。仿真步长固定时这种延迟带来的误差可以忽略。4.2 ESO输出发散符号函数导致的高频振荡现象仿真开始时正常几秒后z3突然变成巨大值或NaN控制量也剧烈跳动。原因TD函数中的sign符号函数在离散跳变时会引起振荡尤其当a接近0时sign的切换导致控制量不连续状态更新出现数值超调。有些情况下是fac函数的delta参数设得过大使得非线性函数进入线性区间等价于高增益比例造成ESO极点偏移。解决把TD里的sign函数替换成饱和函数saturation或者直接用线性TD——跟踪微分器其实不一定要最速形式一个简单的惯性环节加限幅也能用。对于ESO检查delta取值或者干脆使用线性ESO避免非线性函数的参数敏感性。4.3 调节时间比PID还长TD滤波因子h0过大现象阶跃响应曲线平滑得“过头”上升时间长达好几秒即使加大wc也没用。原因TD的h0相当于一个低通滤波器如果h0取得比仿真步长大很多TD输出的v本身就成了一个慢变信号反馈控制器一直在追踪一个被严重滞后的期望轨迹。解决把h0限制在仿真步长的12倍并检查r是否过小。如果需求是快速跟踪可以绕过TD直接把阶跃信号作为v输入ESO仍然能给出平滑的微分估计。注意ADRC的TD不是必须的很多场合直接用原信号加ESO微分也能工作TD更重要的作用是处理超调敏感的设定值变化。4.4 b0参数不对ESO的z3估计值偏离真实扰动现象把扰动信号叠加进系统后z3的波形幅值和相位都和实际扰动对不上甚至方向相反。原因b0偏差过大时ESO把部分控制量误认为扰动。表现就是z3包含了(b-b0)*u的分量而这个分量是控制量的函数所以z3波形看起来乱糟糟。解决先断开扰动源给系统一个阶跃控制量记录z3的稳态值。理想情况下如果b0正确z3应该接近0或只包含模型误差。如果z3有个明显的偏置说明b0偏小或偏大。按照第3章的标定流程用单位阶跃测出对象的增益系数把b0设到那个值附近。对于时变对象这个标定需要在多个工作点重复做。4.5 仿真步长自适应模式下结果不一致现象同一套参数固定步长和可变步长仿真结果差异很大有时固定步长发散可变步长正常。原因ADRC本质是连续时间的但我们在MATLAB Function里写离散递推隐含使用了固定步长dt。把dt设成1e-3但Simulink实际使用变步长步长跳到1e-4或5e-3时ESO的增益配置和离散模型都会失调。解决官方推荐的做法是在Simulink模型配置中设置fixed-step步长选择0.001或更小。如果你的模型必须使用变步长不要把dt作为常量写在函数里而是使用y信号的时间戳或者把dt作为外部输入实时更新。我一般直接从仿真配置里去掉“变步长”选项从根源上避免这个坑。5. 进阶把ADRC模型从Simulink搬到代码生成与硬件在环仿真跑通只是第一步实际工程里最终要把ADRC写成可部署的代码。这一章讲两个我常用的进阶操作将ESO和控制律导出为独立m函数然后用MATLAB Coder生成C代码以及在Simulink外部通过S-Function接入已有ADRC算法做硬件在环前的一致性验证。5.1 把离散ADRC整理成无状态依赖的m函数在Simulink的MATLAB Function里写persistent变量虽然方便但代码生成时会变成静态变量可读性差。更好的方式是写一个标准的离散状态更新函数显式把状态作为输入输出这样既能用于模拟也能生成整洁的C代码。下面是一个线性ESO的示例function [z1_next, z2_next, z3_next] eso_step(z1, z2, z3, y, u, dt, w0, b0) % 显式状态更新的ESO e z1 - y; l1 3 * w0; l2 3 * w0^2; l3 w0^3; z1_next z1 dt * (z2 - l1 * e); z2_next z2 dt * (z3 - l2 * e b0 * u); z3_next z3 dt * (-l3 * e); end这个函数没有内部状态每次调用需要调用方保存并回传当前状态值。在Simulink里可以用两对Delay模块实现状态闭环。如果要生成C代码只需要在MATLAB Coder里指定这份m函数和输入参数的类型比如z1是double标量dt是double。生成后的C函数完全可控没有全局变量非常适合嵌入到单片机或者PLC中。注意生成代码时MATLAB Coder要求函数的所有分支和数组大小可静态推断所以循环和persistent都要避免。5.2 用S-Function封装ADRC避免Simulink采样时间问题如果你要把自己写的ADRC算法和现有Simulink模型整合但不喜欢MATLAB Function的采样时间继承策略可以写一个S-FunctionLevel-2。在S-Function里你可以在mdlUpdate中明确配置离散采样周期Ts这比在MATLAB Function里依赖Simulink步长更可靠。下面是一段简化的S-Function更新函数function m eso_sfun(block) % Level-2 S-Function: 线性ESO setup(block); end function setup(block) block.NumInputPorts 2; % y, u block.NumOutputPorts 3; % z1, z2, z3 block.SetPreCompInpPortInfoToDynamic; block.SetPreCompOutPortInfoToDynamic; block.NumContStates 0; block.NumDworks 3; % 保存z1,z2,z3 block.DworkSize 1; block.DworkIdx 0; block.DworkSize 1; block.DworkIdx 1; block.DworkSize 1; block.DworkIdx 2; block.SampleTimes [0.001 0]; % 固定采样周期 block.RegBlockMethod(Update, Update); end function Update(block) y block.InputPort(1).Data; u block.InputPort(2).Data; w0 block.DialogPrm(1).Data; b0 block.DialogPrm(2).Data; z1 block.Dwork(1).Data; z2 block.Dwork(2).Data; z3 block.Dwork(3).Data; dt block.Dwork(4).Data; % 或者从采样时间直接获取 e z1 - y; l1 3*w0; l2 3*w0^2; l3 w0^3; z1_new z1 dt*(z2 - l1*e); z2_new z2 dt*(z3 - l2*e b0*u); z3_new z3 dt*(-l3*e); block.Dwork(1).Data z1_new; block.Dwork(2).Data z2_new; block.Dwork(3).Data z3_new; % 输出 block.OutputPort(1).Data z1_new; block.OutputPort(2).Data z2_new; block.OutputPort(3).Data z3_new; end这里的关键是block.SampleTimes [0.001 0]指定固定采样周期代码生成的效率比MATLAB Function高而且Dwork变量在代码生成中是静态变量但语义清晰。实际项目中我经常把ESO和控制律分开写成两个S-Function这样单个模块的测试和替换都更方便。S-Function的调试略繁琐建议先用MATLAB Function把算法跑通再转移到S-Function减少排错成本。5.3 验证技巧用MATLAB脚本做闭环一致性测试从Simulink模型迁移到S-Function或C代码后最重要的一步是验证一致性。我通常在迁移完成后写一个纯脚本测试用同样的参数跑同样的阶跃响应对比Simulink输出和S-Function输出。脚本大致如下% 一致性测试: 对比Simulink输出和m函数输出 dt 0.001; T 5; N floor(T/dt); % 初始化 z1 0; z2 0; z3 0; x1 0; x2 0; u 0; y_rec zeros(1,N); z3_rec zeros(1,N); for k 1:N y y_rec(max(k-1,1)); % 上一时刻输出 [z1, z2, z3] eso_step(z1, z2, z3, y, u, dt, w0, b0); u control_law(x1, x2, z2, z3, wc, b0, v); % 被控对象更新 y y dt*u; % 简化积分对象 y_rec(k) y; z3_rec(k) z3; end把脚本生成的曲线与Simulink里的曲线叠在同一张图上如果误差在1e-6量级就说明迁移成功。如果误差较大优先检查采样时间是否匹配Simulink的MATLAB Function模块的继承采样时间可能与脚本的固定dt不一致。从那以后我每次从仿真模型走到代码生成都会强制走一遍这个对比流程省掉了很多次硬件调试时“算法莫名失效”的尴尬。希望这份ADRC仿真笔记能帮你少走弯路不管是课程设计还是实际工程先把仿真里的坑踩平再谈移植祝顺利。本文还有配套的精品资源点击获取