简介:面向控制理论与自动化领域的自抗扰控制仿真资源,采用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设为目标跟踪带宽的2~5倍,再微调。h0是滤波因子,取仿真步长的1~3倍比较稳妥,太小会引入噪声,太大跟踪会滞后。实际仿真时,建议把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秒,先用w0=30, wc=20, b0=1,观察响应。这个测试的好处是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.5~2倍去测试,但不要一上来就给b0一个数量级偏差。
我常用的标定方法是:先把ADRC中的TD输出直接接到被控对象输入,旁路掉ESO和反馈,给对象一个单位阶跃,记录对象加速度的稳态值。比如对象为y'' = 2u + d,单位阶跃u=1时,稳态加速度接近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越小。实际运行时,建议从b0=0.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通常在10~100之间。下面给出一个自动化的时域指标输出函数,帮你判断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/3~1/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限制在仿真步长的1~2倍,并检查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-Function(Level-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仿真笔记能帮你少走弯路,不管是课程设计还是实际工程,先把仿真里的坑踩平,再谈移植,祝顺利。
本文还有配套的精品资源,点击获取