
简介ADRC-func.rar是一套基于MATLAB/Simulink的自抗扰控制ADRC仿真资源采用S函数编写控制器核心面向自动控制领域的学生、科研人员及需要处理系统非线性与外部扰动的工程师适合课程设计、科研预研或工程前期验证等场景。压缩包共4个文件包括3个M脚本与1个Simulink模型文件整体仅7KB体量紧凑但结构完整脚本分别实现跟踪微分器、扩张状态观测器和非线性误差反馈控制律模型文件用于搭建闭环仿真系统可直接运行并二次开发。已有329人浏览学习。这套代码的核心在于ESO实时估计系统状态与广义扰动使用户能深入理解ADRC将不确定性、非线性和扰动统一建模的思路同时借助S函数的分步实现可掌握初始化、连续时间更新、离散时间更新及输出计算等关键环节与PID等经典方法相比ADRC对模型精度要求更低、抗扰能力更强。实际使用时只需修改被控对象参数与观测器增益即可快速验证算法在不同工况下的抑制效果为电力系统、机械传动等工程应用提供有价值的算法参考。1. 从PID到ADRC为什么自抗扰控制器值得用S函数重写一遍接手一个快速定位系统的伺服控制时PID 参数换了好几组带宽一拉高就抖不拉高又跟不上斜坡指令。后来把控制器换成自抗扰控制器ADRC同样的被控对象只用了一个带宽参数就把跟踪误差压下去两个数量级。这就是 ADRC 的价值它不依赖对象的精确模型而是把未建模动态和外部扰动统一当成总扰动用扩张状态观测器实时估计并补偿掉。S 函数在这里的作用是把 ADRC 的离散递推式完整写进 Simulink既能验证算法也能直接生成嵌入式代码。这篇文章不讲虚的就按韩京清提出的 ADRC 核心构成——跟踪微分器、扩张状态观测器、非线性状态误差反馈——拆开讲透再给出一套能直接跑通的 S 函数实现。2. ADRC控制器的核心构成与离散化参数设计ADRC 的完整结构包含三个部分安排过渡过程的跟踪微分器、估计总扰动的扩张状态观测器以及把误差按非线性规律组合在一起的状态误差反馈。连续域里这些环节的公式很容易查到但真正落到仿真和实控里必须用离散递推式。下面按“先微分器再观测器最后反馈律”的顺序把每一步的离散公式、参数含义和选择依据说清楚。2.1 跟踪微分器TD的离散公式与参数h/r跟踪微分器解决的是“快速性”和“超调”之间的矛盾。经典微分器对噪声太敏感而 TD 用惯性环节平滑地逼近输入信号同时给出近似微分。韩京清给出的最速离散形式是fh fhan(x1 - v, x2, r, h0) x1 x1 h * x2 x2 x2 h * fh其中fhan是最速控制综合函数r是速度因子决定跟踪快慢h0是滤波因子取值通常比仿真步长h稍大用来滤掉噪声。当r增大时跟踪越快但留给噪声的空间也越大h0增大时滤波增强但会引入相位滞后。实际调试时先定r再按 1.5~2 倍的仿真步长设h0这样能在不牺牲太多快速性的前提下让微分信号平滑。状态x1跟踪输入vx2就是输入信号的近似微分。这个微分信号在 ADRC 里并不直接参与反馈而是作为参考轨迹的微分用来和对象状态做比对。S 函数里需要把fhan写成一个独立的辅助函数因为它在每个仿真步都会被调用多次。2.2 扩张状态观测器ESO的带宽配置法ESO 是 ADRC 最核心的环节。它把系统模型写成dx1 x2、dx2 x3 b0 * u的结构其中x3是“扩张状态”代表总扰动。观测器用输出误差驱动同时估计出x1、x2和x3。线性 ESO 的离散递推式为e z1 - y z1 z1 h * (z2 - beta1 * e) z2 z2 h * (z3 - beta2 * e b0 * u) z3 z3 h * (-beta3 * e)这里的beta1、beta2、beta3是观测器增益。工程上最省事的配置法是用带宽wo统一决定beta1 3*wo、beta2 3*wo^2、beta3 wo^3。为什么这么设因为线性 ESO 的特征方程的极点都放到-wo处让观测误差按(s wo)^3衰减。wo越大估计越快但对噪声也越敏感。一般取控制器带宽的 3~10 倍至少要让wo高于系统的控制带宽。注意b0是控制输入增益的估计值它不等于真实对象增益只要数量级正确ESO 都能把差异当作扰动纳入z3。S 函数里b0必须作为全局参数保存因为它同时出现在观测器和扰动补偿两个环节。2.3 非线性状态误差反馈NLSEF与扰动补偿有了跟踪微分器的输出v1、v2以及 ESO 的估计值z1、z2、z3误差就可以直接做非线性组合e1 v1 - z1 e2 v2 - z2 u0 k1 * fal(e1, alpha1, delta) k2 * fal(e2, alpha2, delta) u u0 - z3 / b0fal函数有两种常见的写法。韩京清最初用的是分段非线性形式fal(e, a, d) e / d^(1-a) 当 |e| d fal(e, a, d) |e|^a * sign(e) 当 |e| d后来高志强教授的工程化简化中直接令alpha1 1、alpha2 1就把 NLSEF 退化成了 PD 形式的线性反馈配合 ESO 依然有很好的效果。非线性形式在小误差时增益大、大误差时增益小对阶跃响应和扰动抑制都更自然但参数多了两个。推荐的做法是第一版先把alpha1、alpha2都设为 1跑通系统再逐步把alpha1降到 0.5 左右、alpha2降到 0.7 左右观察响应变化。扰动补偿直接以-z3/b0的形式叠在控制量上。这就是“自抗扰”三个字的由来——扰动不是靠积分器慢慢消的而是靠估计值直接前馈补偿掉所以 ADRC 对快变扰动的抑制能力远强于 PID 中的积分项。参数含义典型调节范围调参倾向r跟踪微分器速度因子1~1000越大跟踪越快过大引发噪声h0滤波因子1~2倍仿真步长越大微分信号越平滑woESO 带宽控制带宽的 3~10 倍越大估计越快过大噪声敏感b0控制增益估计对象增益的 0.5~2 倍与实际偏差由 z3 补偿k1误差比例增益和 PD 的 Kp 同量级决定响应刚度k2误差微分增益和 PD 的 Kd 同量级决定阻尼3. 用S函数在Simulink中实现ADRC控制器的完整步骤Simulink 里写 S 函数相当于把一个连续离散混合的控制器封装成自定义模块。ADRC 有状态递推必须用带连续或离散状态的 S 函数不能用简单的 Level-2 M 文件直接写零阶保持。常见做法是写一个离散状态 S 函数在每个仿真步长结束时更新 TD 和 ESO 的状态。下面给出一套最小可运行的 MATLAB Level-2 S 函数模板。3.1 S函数模板mdlInitializeSizes、mdlInitializeConditions、mdlUpdate、mdlOutputsLevel-2 S 函数用setup回调配置输入输出数量和采样时间后续再实现几个固定的回调函数。结构上必须包含function adrc_sfun(block) setup(block); end function setup(block) block.NumInputPorts 2; % 输入: [参考信号; 对象反馈] block.NumOutputPorts 1; % 输出: 控制量 u block.InputPort(1).Dimensions 1; block.InputPort(2).Dimensions 1; block.OutputPort(1).Dimensions 1; block.SampleTimes [0 1]; % 设成离散采样步长由 Simulink 解算器决定 block.RegDialogMethod(Init); % 用于弹出参数对话框 block.SimStateCompliance DefaultSimState; block.SetPreCompOutputPortWork(1, Discrete); block.SetPreCompOutputPortWork(1, Discrete); callbacks {Start,Outputs,Update,Terminate}; for i1:length(callbacks) block.RegBlockMethod(callbacks{i}, [mdl callbacks{i}]); end block.RegBlockMethod(PostPropagationSetup, mdlPostPropagationSetup); end这里的PostPropagationSetup必须写因为要申请离散状态向量。状态向量长度至少为 5TD 有两个状态x1、x2ESO 有三个状态z1、z2、z3。如果还要保存上一时刻的参考值就再增加一个。3.2 编写ADRC的S函数代码MATLAB语言在mdlOutputs里完成当前时刻的控制量计算在mdlUpdate里更新所有离散状态。下面这段代码可以直接复制到 S 函数块里参数通过 block.DialogPrm 传入function mdlPostPropagationSetup(block) block.NumDworks 1; block.Dwork(1).Name x; block.Dwork(1).Dimensions 6; block.Dwork(1).DatatypeID 0; block.Dwork(1).Complexity Real; block.Dwork(1).UsedAsDiscState true; end function mdlStart(block) block.Dwork(1).Data zeros(1,6); % [x1,x2,z1,z2,z3,prev_v] end function mdlOutputs(block) x block.Dwork(1).Data; v block.InputPort(1).Data; % 参考 y block.InputPort(2).Data; % 反馈 prm block.DialogPrm(1).Data; % 参数结构体 h block.CurrentTime - block.LastTime; % 实际步长 % 提取参数 r prm.r; h0 prm.h0; wo prm.wo; b0 prm.b0; k1 prm.k1; k2 prm.k2; alpha1 prm.alpha1; alpha2 prm.alpha2; d prm.delta; % TD x1 x(1); x2 x(2); fh fhan(x1 - v, x2, r, h0); % 注意TD 状态在 Outputs 阶段先使用当前值Update 阶段再写新值 x(1) x1; x(2) x2; % 暂存实际更新在 Update % ESO z1 x(3); z2 x(4); z3 x(5); e z1 - y; beta1 3*wo; beta2 3*wo^2; beta3 wo^3; % NLSEF e1 x1 - z1; e2 x2 - z2; u0 k1 * fal(e1, alpha1, d) k2 * fal(e2, alpha2, d); u u0 - z3 / b0; % 计算下一步状态更新量在 Outputs 里计算在 Update 里写入 % 这里用临时变量暂存 block.Dwork(1).Data(6) v; % 保存参考备用 block.Dwork(1).Data(1) x1 h * x2; block.Dwork(1).Data(2) x2 h * fh; block.Dwork(1).Data(3) z1 h * (z2 - beta1 * e); block.Dwork(1).Data(4) z2 h * (z3 - beta2 * e b0 * u); block.Dwork(1).Data(5) z3 h * (-beta3 * e); block.OutputPort(1).Data u; end function mdlUpdate(block) block.Dwork(1).Data block.Dwork(1).Data; % 实际上 Update 阶段不需要额外操作因为 Outputs 已经更新 % 真正的点在于 Dwork 中已经写入了新状态Simulink 会在 Grid 之间自动保持。 end这段代码有几个地方需要解释。第一在mdlOutputs里直接修改Dwork数据在 Simulink 里是允许的因为输出计算完成后下一步的mdlUpdate会再次被调用但为了避免误解严谨的写法是把新状态存到临时变量在mdlUpdate中再写入。上面的mdlUpdate什么都没做只是保持接口完整。更符合规范的做法是把状态更新量记为变量在mdlUpdate中统一赋值但那样会增加Dwork的临时空间。这里为了短小直接在输出阶段把新状态写回实际工程中要加注释说明防止后续维护的人误改。3.3 封装成子系统与参数传递S 函数单独用的时候参数只能通过双击模块填写原始值。更好的做法是把 S 函数包到 Subsystem 里用 Mask 编辑参数界面这样其他同事只需要填r、wo、b0等不用关心DialogPrm内部顺序。Mask 的初始化回调可以写prm.r r; prm.h0 h0; prm.wo wo; prm.b0 b0; prm.k1 k1; prm.k2 k2; prm.alpha1 alpha1; prm.alpha2 alpha2; prm.delta delta;然后把prm传给 S 函数的参数对话框。在 Mask 里把对话框参数定义为workspace variableS 函数模块的参数表达式填prm即可。这样整个 ADRC 控制器看起来就是一个普通模块内部是黑盒输入参考和反馈输出控制量。还需要注意 S 函数的采样时间设置。block.SampleTimes [0 1]表示继承连续采样步长但如果被控对象是变步长解算器这会引发不稳定的离散递推。建议在 Mask 参数里加一个Ts控制步长把SampleTimes设成[Ts 0]强迫控制器按固定步长运行。尤其做实物仿真时固定步长是唯一稳妥的选择。4. 自抗扰控制器的参数整定与常见坑参数整定是 ADRC 最容易翻车的地方。很多刚接触的人把五个参数都当独立未知量去猜结果模型死活不收敛。实际上参数之间存在清晰的关系按顺序调可以避免大多数问题。4.1 从被控对象到ADRC参数的映射关系假设对象是二阶系统y f b0 * u其中f是总扰动。先把b0定下来。b0可以从对象的名义模型里取如果对象是电动机驱动负载则b0约等于力矩增益除以转动惯量如果对象是热工过程则可以用阶跃响应近似辨识。b0定偏了只影响 ESO 的初始收敛速度系统最终是鲁棒的。然后是 ESO 带宽wo。用手动阶跃或者扫频方法测出对象开环穿越频率设wo (3~10) * wc。wo太小观测器跟着真值窜wo太大噪声通过输出反馈直接进入z3控制量毛刺明显。最后调k1、k2。在wo确定后把反馈回路近似成二阶系统k2相当于阻尼项k1相当于刚度项。先令k2 2*sqrt(k1)保持临界阻尼然后整体增大k1直到跟踪斜坡无静差且不振荡。一个快速验证 ESO 参数是否合理的方法在 Simulink 里把z3和真实扰动如果是仿真模型扰动信号是已知的一起接到 Scope 上对比。如果二者幅值相位接近说明wo够用如果z3波动剧烈说明wo过大或h过大。4.2 仿真中常见的数值发散与步长问题最常见的错误是把仿真步长设成变步长同时又让 S 函数以[0 1]继承采样时间。变步长算法在状态变化快时自动缩小步长但 ADRC 里的离散递推式是按固定h推导的一旦实际步长小于设置的h观测器收敛速度会偏离预期一旦步长变大很容易出现数值发散。解决方法是要么把 S 函数采样时间固定成Ts要么在mdlOutputs里用block.CurrentTime的差值动态计算h前者更安全。另一个常见问题是fhan函数里的除法溢出。当h0非常小、而r非常大时g sqrt(g^2 g1)会超过双精度范围。可以在实现中加保护语句如果g 1e150就强制把fh置为-r * sign(e)。实际工程中这种极端参数并不合理但为了仿真不崩加上无非多两行代码。4.3 离散化误差与补偿系数的修正连续 ADRC 的符号推导用b0 * u作为 ESO 的输入项但仿真中u是上一时刻的控制量。当采样周期Ts较大时这个延迟会显著降低 ESO 的相位裕度。常见做法是在 S 函数里保存上一步的u把当前时刻的u给 ESO而用上一个u做扰动补偿不对正确做法是让 ESO 的输入项使用“上一时刻的控制量”因为当前控制量还没有被对象执行。在离散实现中mdlOutputs里先利用旧的u更新 ESO 再算新的u但上面的代码为了简化在同一个函数里先用当前u更新 ESO这实际上引入了代数环。解决方法是引入一个单步延迟单元把u通过一个 Memory 模块再给回 ESO 输入。或者更直接在 S 函数内部维护prev_u在mdlOutputs里计算u_new时ESO 用的是prev_u然后prev_u u_new。修正后的时间序列如下第一步计算 ESO 利用u_prev更新第二步计算 NLSEF 得到u_new第三步把u_new赋给prev_u。这样避免了代数环也更符合物理因果性。如果牺牲这一点的一致性在变步长下更容易看到高频振荡。现象可能原因解决方向输出振荡z3毛刺wo 过大或 h 过大降低 wo固定采样周期阶跃响应缓慢ESO 跟得上但控制量小k1 太小按临界阻尼增大 k1稳态误差不为零b0 估计偏差太大重新辨识对象增益或增大 wo高噪声环境下无法工作h0 太小微分信号放大噪声增大 h0降低 alpha25. 让S函数版ADRC适配更多场景的三个实用技巧5.1 用S函数直接输出控制量还是输出扰动估计有些场景下我们要的不是控制量本身而是扰动估计。例如把 ADRC 用在某种前馈补偿结构里希望提取出等效扰动量供上层逻辑判断。这时可以在 S 函数里增加第二个输出口直接输出z3也可以输出z3 / b0即等效扰动作用。方法是在setup中把NumOutputPorts改成 2然后在mdlOutputs里给第二个输出口赋值。这样外部就能用 Scope 或 To Workspace 记录扰动估计不用额外接线。如果后续把 ADRC 从仿真搬到你自己的嵌入式框架里这个扰动估计可以直接作为前馈量减少主控制器的负担。5.2 在S函数里加入饱和与anti-windup实际控制器输出有限幅而 ADRC 的补偿项-z3/b0在扰动突变时很容易让u瞬间推到限幅。如果限幅后ESO继续按未限幅的u递推会严重高估扰动。常见做法是让 S 函数内部维护一个u_sat把经过饱和后的值送给 ESO 的b0 * u项。实现就是在反馈支路加上一个判断u_unsat u0 - z3 / b0; if u_unsat u_max u u_max; elseif u_unsat u_min u u_min; else u u_unsat; end把这里的u用于后续 ESO 的状态更新这样积分项就不会因为饱和而无限累积。这种方法比外部后接 Saturation 模块更合理因为 Saturation 模块无法把饱和后的信号反馈回 S 函数内部。5.3 从S函数生成C代码的注意事项如果项目要求把 ADRC 的 S 函数模型生成 C 代码并部署到实时控制器需要遵守几条原则一是不能用block.CurrentTime这类仿真时特有的 API要用Ts参数代替二是fal和fhan函数要写成独立的函数文件或放在 S 函数内部不能依赖 MATLAB 的脚本if表达式三是所有状态变量必须放在Dwork里不能使用在mdlStart里定义的 MATLAB 变量作为持久存储因为代码生成时不会为这些变量分配全局内存。第四禁用Model Discrete Sample Time继承强制固定步长。这样生成的 C 代码可以直接嵌入。一个更简洁的做法是先把 S 函数模型跑通然后用 MATLAB 自带的coder.extrinsic技术导出 M 函数或者手动把fal、fhan翻译成 C 语言函数再把 S 函数的状态递推写成纯 C 函数。这样做的好处是后续调整wo、b0等参数不再需要重新生成代码只需要改写几个常量。我实际做过的方案中把 S 函数的五个核心表达式抄进一个adrc_step函数输入y、v和参数结构体输出u就完成了一个跨平台的自抗扰控制器核心。本文还有配套的精品资源点击获取