第一次看到ADRC那一堆公式的时候,我一度怀疑自己是不是不适合做控制。韩京清先生提出的ADRC,核心构成就是TD、ESO、NLSEF三个模块,名字一个比一个抽象,网上资料又大多是“从原理到证明”的学术写法,初学者很容易卡在第二步就放弃。
但后来我用MATLAB/Simulink把这三个模块一个个搭起来、跑通参数之后,发现ADRC的调参逻辑其实比想象中清晰。它不是玄学,只是需要先搞清楚三个模块各自在干什么、参数各自影响什么,再按照一个固定顺序去调。这篇东西我就按自己实际调试的顺序写,从模块拆解、Simulink搭建到完整参数表,最后把仿真里踩过的坑一并列出来。适合正在做控制类毕设、刚接触ADRC的工程师,或者想快速用Simulink验证一下这个控制器的同学。
1. 先用一句话拆掉ADRC的心理门槛
1.1 PID调不顺的问题,ADRC到底改了什么
先说个老生常谈的问题。PID不是不好,而是误差直接驱动这件事本身有先天矛盾:目标值一阶跃,初始误差瞬间变得很大,比例项为了快速响应会把控制量推得很猛,超调就跟着来了。你想减小超调就得降低比例增益,但响应又慢了。积分项能消稳态误差,可它反应慢,而且积分饱和在工程里特别烦人。更麻烦的是,PID默认控制器面对的是一个固定模型,一旦对象参数变化或者外部扰动进来,原先调好的那组参数可能立刻失效。
ADRC的思路完全不一样。它不跟误差较劲,而是先把目标信号“处理”得平缓一点,再把系统内部的不确定性、外部扰动统统当成一个“总扰动”给估计出来,最后在控制量里主动把它们抵消掉。整个控制结构可以看作三个分工明确的模块:
- TD负责给目标信号安排过渡过程,同时把信号和微分提取出来;
- ESO负责盯着系统输出,把状态和“总扰动”一起观测出来;
- NLSEF负责把跟踪误差组合成控制量,再把扰动补偿进去。
这三块拼在一起,就构成了完整的ADRC。很多初学者一上来就去啃非线性函数的数学证明,其实完全没必要。先用Simulink把回路跑通,再回过头看公式,会顺得多。
1.2 TD、ESO、NLSEF三个缩写分别管什么
为了好记,我喜欢打个比方:控制就像开车去一个目的地。
TD是导航里的“路线规划器”,它不让车速瞬间拉满,而是根据当前路况给出一条平滑的速度曲线。它输入一个阶跃目标,输出一条不突变的跟踪轨迹和对应的“速度”信号。
ESO是副驾驶,它不掌握真实路况,但通过对比车辆实际表现和模型预期,能估算出当前风阻、坡度、路面摩擦这些“干扰”到底有多大,然后实时报告给你。
NLSEF是你踩油门和打方向的手脚,它把导航目标、车辆当前状态、副驾驶报上来的干扰综合在一起,输出一个最终的控制动作。
这样一拆,ADRC的调参就变成了三件独立的事:把导航调平顺,把副驾驶调准,把手脚调协调。只要逐个解决,整体控制效果自然就出来了。下面我按这个顺序把每个模块的公式、参数和Simulink实现拆开讲。
2. 三个核心模块逐个拆解,公式和参数不再劝退
2.1 TD(跟踪微分器):先给信号穿一件“平滑衣”
TD最直观的作用,是把一个阶跃信号变成一条平滑的过渡曲线,并且顺便给出它的“速度”。为什么要这样做?因为真实物理系统里面的执行器、机械结构都有惯性,你直接给它一个阶跃电压,它容易冲过头;但如果目标信号是从0平缓爬到1的,系统就能比较从容地跟上,超调自然会小。
离散化之后,我习惯直接在MATLAB Function里写这样的TD:
function [v1, v2] = TD(v0, r, h) persistent x1 x2 if isempty(x1) x1 = 0; x2 = 0; end e = x1 - v0; % 最速控制综合函数 fhan d = r * h; d0 = h * d; y = e + h * x2; a0 = sqrt(d * d + 8 * r * abs(y)); if abs(y) <= d0 a = x2 + y / h; else a = x2 + 0.5 * (a0 - d) * sign(y); end if abs(a) <= d fh = -r * a / d; else fh = -r * sign(a); end x1 = x1 + h * x2; x2 = x2 + h * fh; v1 = x1; v2 = x2; end这里r是快速因子,决定过渡过程快慢。r越大,v1跟踪目标越快,但微分信号v2的噪声也会被放大。h是采样步长,必须和Simulink里设置的求解器步长一致。实际使用时,我还习惯在TD内部用一个比h稍大的滤波步长h0来抑制微分噪声,常见取法是h0 = 2*h ~ 10*h,这样既能保留过渡过程,又不会让v2毛刺太多。
初调的时候,建议先把TD单独拿出来看。给一个阶跃输入,观察v1是否光滑地爬到目标值,观察v2是否像一个“速度曲线”。如果v1有明显超调,说明r偏大;如果响应太拖沓,说明r偏小。这个模块调好了,后面两个模块才有好的输入信号可用。
2.2 ESO(扩张状态观测器):全场最重要的观测员
ESO是整个ADRC的灵魂。它的任务是把系统的状态和“总扰动”一起估出来。这里的“总扰动”是个广义概念,既包含外部干扰,也包含模型参数不准、未建模动态等内部不确定性。
对于常见的二阶对象:
x1_dot = x2 x2_dot = f(x1, x2, w, t) + b0 * u其中f就是总扰动,b0是控制增益的估计值。ESO把f扩张成第三个状态z3,通过输出y的反馈不断修正估计。离散化之后我常用的代码是这样的:
function [z1, z2, z3] = ESO(y, u, h, b0, beta01, beta02, beta03, delta) persistent z1 z2 z3 if isempty(z1) z1 = 0; z2 = 0; z3 = 0; end e = z1 - y; z1 = z1 + h * (z2 - beta01 * e); z2 = z2 + h * (z3 - beta02 * fal(e, 0.5, delta) + b0 * u); z3 = z3 + h * (-beta03 * fal(e, 0.25, delta)); end function f = fal(e, alpha, delta) if abs(e) <= delta f = e / (delta^(1 - alpha)); else f = abs(e)^alpha * sign(e); end end这里的fal是一个非线性函数,在误差小时近似线性,误差大时保持非线性特性,作用是让ESO在误差较小时也能有较好的估计精度,同时又不会因为误差太大而发飙。参数alpha一般取0.5和0.25,delta取0.01左右,这两个值不需要频繁改动。
真正需要花心思的是beta01、beta02、beta03这三个观测器增益。我建议不要手拍,直接用“带宽法”起步。把观测器期望带宽记为wo,则:
beta01 = 3 * wo beta02 = 3 * wo^2 beta03 = wo^3先给wo一个20左右的值,这样beta01=60、beta02=1200、beta03=8000。跑起来之后,如果ESO跟踪输出慢,就提高wo;如果出现高频振荡,就降低wo。wo越大,ESO响应越快,但对噪声越敏感,而且仿真步长不够小时很容易数值发散,这点后面还会展开说。
2.3 NLSEF(非线性状态误差反馈):把误差按非线性捏成控制量
TD给出了目标信号v1和微分v2,ESO给出了状态估计z1、z2和扰动估计z3。NLSEF要做的,是把这些信号组合成一个控制量。
我常用的离散形式是这样的:
function u = NLSEF(v1, v2, z1, z2, z3, b0, beta1, beta2, alpha1, alpha2, delta, u_max) e1 = v1 - z1; e2 = v2 - z2; u0 = beta1 * fal(e1, alpha1, delta) + beta2 * fal(e2, alpha2, delta); u = (u0 - z3) / b0; if abs(u) > u_max u = sign(u) * u_max; end ende1是位置误差,e2是速度误差。beta1相当于比例增益,负责“推一把”;beta2相当于阻尼,负责“拉住它”。alpha1取0.5左右,alpha2取1.5左右,delta同样取0.01。控制量最后由(u0 - z3) / b0给出,这里减掉z3就是把ESO估计出来的总扰动主动补偿掉,也是ADRC叫“自抗扰”的原因:它不是被动等扰动影响输出,而是先把扰动估出来怼回去。
注意,u_max必须根据执行器能力设置。如果不限幅,扰动估计阶段z3稍微抖一下,控制量就可能冲到天上去,仿真直接崩掉。我在实际调试中,限幅这一行从来没省略过。
3. MATLAB/Simulink实操:搭一套完整的ADRC并跑通
3.1 被控对象与仿真模型框架
为了演示,我选一个典型的二阶对象:
G(s) = 133 / (s^2 + 25s)对应状态方程可以写成:
x1_dot = x2 x2_dot = -25 * x2 + 133 * u + d(t)其中d(t)是外部扰动,仿真时我习惯叠加一个正弦扰动sin(10t)或者阶跃扰动。这个对象在实际里很像一个电机速度环或者简单机械系统,参数不强求精确,主要用来验证ADRC的鲁棒性。
Simulink模型框架不复杂,核心是四条信号线:目标值进TD,TD输出和ESO输出进NLSEF,NLSEF出控制量给被控对象,被控对象输出再回给ESO。我建议在Simulink里用MATLAB Function模块分别封装TD、ESO、NLSEF,而不是用一堆积分器和加法器手动搭,因为代码方式改参数更直观,也方便以后移植到嵌入式平台。
被控对象本身可以用传递函数模块,也可以直接用积分器搭:
- 给
dx2加一个常值增益,模拟-25*x2; - 加一个
133*u输入; - 外面再加一个“扰动注入”的加法器,这样方便观察ESO对扰动的估计。
仿真求解器我强烈建议设置成固定步长,步长0.001秒,离散求解器或者ode4都行。ADRC的TD和ESO本质上是离散迭代算法,如果Simulink用变步长,MATLAB Function被调用的间隔不稳定,persistent变量里的h就和实际采样间隔对不上,参数再对也容易出问题。
3.2 TD和ESO模块的MATLAB Function实现
打开Simulink,新建一个模型,按下面的步骤操作:
从Simulink库拖入三个MATLAB Function模块,分别命名为TD、ESO、NLSEF。双击TD模块,把上面2.1节的代码粘进去。需要注意,MATLAB Function模块的输入输出端口要和函数签名一一对应。TD模块的输入是v0、r、h,输出是v1、v2。r和h可以直接在模型里用常量模块给定,也可以在MATLAB Function内写成局部常量,但为了调参方便,我建议把r、h、b0这些核心参数全部从模型外部接进去,这样用Simulink的Signal Editor或者直接在WorkSpace里改参数都能实时生效。
ESO模块同样。输入端口我习惯按照y、u、h、b0、beta01、beta02、beta03、delta的顺序接。注意u这里必须接限幅之后、真正送入被控对象的那个控制量,而不是NLSEF计算出来的原始值。这个细节非常关键,如果接错了,ESO估计的扰动会偏大,严重的时候整个回路会振荡。
TD和ESO里的persistent变量初始值建议设成0。如果被控对象初始状态不是0,ESO会有一段收敛过程,这是正常的。想要起始阶段更平顺,可以把z1的初始值设成对象输出的初始值。
3.3 NLSEF与整体回路连接
NLSEF模块的输入是v1、v2、z1、z2、z3、b0、beta1、beta2、alpha1、alpha2、delta、u_max,输出是u。接线顺序建议按照信号流向从左到右排列,方便阅读。
整个模型连接完成后,还要加一个饱和模块。我建议在NLSEF内部用代码限幅,但同时在Simulink里再加一道Saturation保险,这样即使NLSEF代码改逻辑,也不会真的把控制量送到执行器范围之外。
给被控对象加扰动时,注意把扰动信号和对象内部累加的地方接好。我想检查ESO的扰动估计能力,一般会在0.5秒时加一个幅值为1的阶跃扰动,然后在示波器里同时看z3和实际扰动。如果ESO正常,z3应该在大约0.1~0.3秒内追到扰动附近,而且曲线平稳。
3.4 完整参数表:可以直接抄的第一组参数
下面给出针对上述对象的第一组可运行参数。这个表可以直接抄进Simulink,跑通之后再根据你的实际对象微调。
| 模块 | 参数 | 推荐初值 | 初始调整思路 |
|---|---|---|---|
| TD | r | 200 | 响应太慢加大,超调/噪声大减小 |
| TD | h | 0.001 | 与Simulink固定步长一致 |
| TD | h0 | 0.01 | 抑制微分噪声,可调大但不要超过过渡过程尺度 |
| ESO | b0 | 120 | 由对象增益粗略定,实际133,故意留一点模型失配让ESO补偿 |
| ESO | beta01 | 60 | 对应wo=20,响应慢可提高 |
| ESO | beta02 | 1200 | 对应wo=20,响应慢可提高 |
| ESO | beta03 | 8000 | 对应wo=20,注意过大会振荡 |
| ESO | delta | 0.01 | 一般不动,太小容易抖,太大会损失精度 |
| NLSEF | beta1 | 10 | 响应慢加大,振荡减小 |
| NLSEF | beta2 | 5 | 阻尼作用,振荡加大 |
| NLSEF | alpha1 | 0.5 | 一般不动 |
| NLSEF | alpha2 | 1.5 | 一般不动 |
| NLSEF | delta | 0.01 | 一般不动 |
| NLSEF | u_max | 10 | 按执行器限幅设 |
这套参数跑出来的典型指标大致是:阶跃响应上升时间0.3秒出头,无超调或者只有轻微超调,加入阶跃扰动后输出波动能在0.2秒内恢复。如果你希望响应更快,可以尝试把r提高到400、wo提高到40,对应beta01=120、beta02=4800、beta03=64000,并把beta1提高到20。但要注意,步长0.001秒在高带宽下可能不够稳,遇到振荡先把步长降到0.0005秒试试。
4. 调参顺序、踩坑记录和问题速查表
4.1 我的推荐调参顺序:TD -> ESO -> NLSEF
无数次仿真之后,我总结出一个固定调参顺序,这个顺序能避免90%的“调不出来”。
第一步,只调TD。把NLSEF断开,让TD输出直接接示波器,给一个阶跃输入。观察v1是不是平滑上升、v2是不是光滑。先不用管上升时间多快,只要不超调、不抖,r就基本合格。如果r太大,v1会冲过头,这时候调小;如果v2毛刺很多,把h0调大。
第二步,调ESO。把TD的输出撤掉,或者直接给控制器一个很小的常值控制量,让被控对象开环运行。在Simulink里观察ESO的z1是否紧紧贴住对象输出y,再看z3是否能估出外加扰动。如果z1跟不上,提高wo;如果z3振荡发散,降低wo或减小仿真步长。这个阶段不要管控制效果,只看观测器跟得准不准。
第三步,闭合回路调NLSEF。先把beta1和beta2都设得很小,比如beta1=2、beta2=1,保证回路稳定不炸,然后逐步加大beta1提高响应速度,出现振荡后适当加大beta2压住。一次只动一个参数,每动一次看一次波形,这比同时改三个参数然后盲目试要高效得多。
4.2 仿真现场最常见的5个问题
我把自己还有身边人跑ADRC时踩过的坑整理成了一张速查表,按“问题现象 -> 可能原因 -> 处理方向”的顺序给出来,排查的时候直接对照。
| 问题现象 | 可能原因 | 处理方向 |
|---|---|---|
| 仿真直接发散,变量跑到10的几十次方 | 步长过大、ESO带宽太高、b0与真实增益差太多 | 先确认固定步长0.001;把wo降到20以下;把b0调到接近实际模型增益 |
| 闭环出现持续高频振铃 | 采样步长不够小、beta1过大、delta太小 | 步长降到0.0005;降低beta1;delta从0.005试到0.05 |
| 阶跃响应慢,但加大beta1后一上来就振 | TD的r太小导致误差长期偏大,反馈一味加大会触发非线性段 | 先加大r缩短过渡过程,再小幅增加beta1 |
| 存在明显的稳态误差,比如目标1实际只有0.98 | ESO补偿不干净,或控制量进入饱和 | 检查ESO的u是不是用了限幅后的值;确认b0符号和数量级;查看控制量是否提前顶到u_max |
| 加入扰动后输出波动很大且恢复慢 | ESO带宽不够,z3收敛慢 | 提高wo;同时检查z3波形是否被噪声淹没,必要时把delta稍微调大 |
这里特别想强调一下b0的问题。很多初学者把b0理解成模型参数,非要精确定义成133。实际上ADRC的鲁棒性恰恰体现在b0不准也能工作,因为偏差会算进总扰动里被ESO补偿。但b0不能错一个数量级。如果实际增益是133,你填了13,ESO会严重高估控制量对应的加速度,z3会被强行拉偏,整个回路很容易崩。我调试时习惯先用模型粗算一下b0,然后把参数表里的120和133都试一遍,观察哪个更稳,再在这个范围附近微调。
4.3 几个不太会写进论文的排错细节
除了参数,Simulink里还有一些“看起来不是问题的问题”,我第一次排查时花了不少时间。
第一个是代数环。如果ESO的u接的是NLSEF输出,而被控对象是连续模块,有时Simulink会因为信号流关系报代数环,报错信息很吓人。解决办法很简单:在NLSEF输出到ESO输入之间加一个Memory模块,或者把ESO作为离散模块并设置采样时间,用离散采样打破连续直通。加了之后不影响控制效果,但能消除代数环。
第二个是MATLAB Function模块的采样时间设置。默认情况下,MATLAB Function可能会被当作连续模块来处理,但如果里面用了persistent和h递推,必须显式设置采样时间为0.001。我通常在每个MATLAB Function模块的参数设置里,把采样时间填成跟固定步长一样的值。这个细节不做到位,有时候模型也能跑,但结果每次都不一样,特别容易产生“薛定谔的仿真结果”。
第三个是对ESO的z3不要抱太完美的预期。z3估计的是总扰动,但如果扰动是突变阶跃,z3的收敛总是需要时间的,不可能瞬间追上。只要z3能在被控对象输出明显变化之前给出大致趋势,ESO就是合格的。想让z3追得更快,只能提高wo,而wo提高会放大噪声,这不是一个可以无限调大的参数。实际工程里,我在做系统设计时通常先定执行器和传感器采样周期,再反推ESO带宽能设计到多少,而不是一上来就把带宽拉满。
5. 写在最后:一些个人习惯和提醒
5.1 我最常用的一组调试习惯
做ADRC仿真这么多次,我自己最依赖的一个习惯是:先把ESO单独“钓”出来看曲线,不急着闭环。具体操作是把NLSEF的输出旁路掉,给被控对象一个手动控制量,或者干脆让控制量为0,然后在示波器里同时放y、z1、z3。看到z1贴合y、z3能跟踪扰动,我再闭环保准没问题。ESO看得准,ADRC就成功了一大半;ESO看不准,后面NLSEF参数怎么调都白搭。
第二个习惯是参数一次只动一个。听起来像废话,但实际操作时因为Simulink改参数太方便,人特别容易同时动r、wo、beta1,结果问题出在哪都不知道。每次改完参数,记录一下改前改后的波形,这个习惯在复杂对象上能省出好几倍的时间。
第三个习惯是用脚本批量扫参。把r、wo、beta1这些参数定义成MATLAB工作区变量,然后用for循环配合sim命令跑多次仿真,最后把阶跃响应的超调量、上升时间、稳态误差汇总成表格挑选。这样虽然粗暴,但比一个人盯着示波器猜要快得多。Simulink还支持parsim并行跑,多个参数组合同时仿真,几分钟就能扫完一组初值范围。
5.2 一个小工具建议
最后再分享一个实用小技巧:不管你最后要不要做实物,我都建议在Simulink里把ESO的z3和NLSEF的u同时接进示波器,控制量饱和标志也拉出来。这两个信号能直接告诉你系统是不是已经被推到了边界。很多时候ADRC参数看起来不错,但控制量一直在饱和边界上反复横跳,这种情况上实物大概率会出问题。提前看到这一点,比在硬件上发现问题再去调要省太多时间。
ADRC的调参没有想象中那么神秘,本质还是“一个模块一个模块看曲线、改参数、比效果”。把TD当成给信号整形,把ESO当成一个观测仪,把NLSEF当成最后的反馈增益组合,整个过程就顺了。希望这篇实操分享能让你在Simulink里少走几步弯路,早日调出满意的曲线。