这两年跟ADRC打交道打多了,我反而觉得这名字起得有点唬人。自抗扰控制,听着像是要把系统里的扰动人肉摘除,其实本质就一句话:用一个观测器把“当前真实扰动+模型失配”一起估出来,然后在控制量里做反向补偿,让被控对象在控制器眼里始终保持一副“清爽”模样。二阶非线性ADRC的核心,就是跟踪微分器、扩张状态观测器和非线性反馈控制律这三段式结构。网上教程不少,但很多人以为是“多调几个参数撞大运”,从b0开始瞎摸,结果调了三个月还在极限环里抖。今天这篇,我想把结构、公式、离线仿真和调参套路一次性串起来,尽量把每一步背后的“为什么”也说明白。
后面的内容会基于一个典型的二阶对象来展开,比如质量-弹簧-阻尼系统。但结论完全可以平移到自平衡小车、电驱控制、飞行器姿态这类二阶模型上。适合刚接触ADRC的同行,也适合PID调多了想换个路子的老手。我不会给你堆一堆数学证明,只会给你能直接跑的仿真脚本和一套完整的调参流程。
1. 先把ADRC的骨架画清楚再动手
1.1 为什么二阶非线性ADRC能抗扰动
先说清楚一个容易被忽略的事实:ADRC最核心的部分不是那个“非线性反馈”,而是它内置的扩张状态观测器,缩写叫ESO。你如果以前调过PID,一定被“积分饱和”“扰动导致静差”这类问题折磨过。PID手里的武器是积分项,它通过积累误差来消除稳态误差,可积分本身就是个容易引发振荡的滞后环节。ADRC的思路完全不同,它把被控对象里的内部动态、外部扰动、未建模部分全部打包成一个新的状态,叫“扩张状态”,然后用ESO实时观测出这个扩张状态的大小。
听起来有点抽象,我打个比方。你开车走高速,路面有风、上坡、轮胎磨损,这些都会让车速偏离设定值。老司机并不是靠死记油门踏板位置来控制速度,而是边开边体感“今天这车阻力大约有多大”,然后在这个估计值上多踩一点、少踩一点。ESO干的就是这个活,它时刻估计车辆当前的总阻力,控制器只负责处理“估计完之后剩下的那点偏差”。所以ADRC对模型依赖不高,对一个模型不准的对象也能稳得住,就是因为它把模型失配也当成扰动一块儿估掉、补偿掉了。
二阶ADRC里,被控对象写成:
x1_dot = x2 x2_dot = f(x, w, t) + b0 · u这里的x1可以理解为位置/角度,x2是速度/角速度,f(...)表示系统内部动力学和外部扰动的和,b0是控制增益的估计值。注意这个b0很关键,后面调参会跟它反复纠缠。控制器并不知道真实的f是什么样,它只告诉ESO:“你帮我把f随时盯住,我来做剩下的工作。”
1.2 结构清爽背后的调参难点在哪
传统PID三个参数,Kp、Ki、Kd,已经让一堆人焦头烂额。ADRC结构上也是三层:TD安排过渡过程,ESO观测状态和总扰动,非线性状态误差反馈控制律负责生成输入。你数一数,至少十个参数跑不掉。TD有两个参数,ESO有增益矩阵和非线性函数参数,反馈律还有alpha、delta、kp、kd,再加上b0。参数变多了,自由度大大增加,但好处也在这里——每个参数的物理意义比PID更明确,调起来其实更有章法。
难就难在“章法”二字。如果你把每个参数都当独立的旋钮瞎拧,十四个旋钮的组合空间大到没法收敛。我第一次上手的时候,就是在Simulink里改一个参数跑一次仿真,跑了几十版,最后发现只是换了一种振法。后来琢磨明白一个道理:ADRC调参不能平铺直叙一个个调,要分成“粗调结构参数”和“精调性能参数”两步。先把ESO调到能准确估计状态,再调控制器带宽,最后再用非线性参数做性能打磨。这个顺序错一步,后面的工作基本白费。
另外还需要注意,ESO的估计效果和质量直接在控制效果上反映出来。ESO如果抖得厉害,整条控制回路就会把观测噪声放大成执行器抖动。这也就是为什么很多人说“ADRC高分不清噪声”,其实不是ADRC分不清,是观测器带宽和噪声抑制之间的平衡没处理好。调参阶段必须拿着闭环响应曲线和观测器的状态估计曲线一起看,不能只看输出。
2. 仿真模型搭建:从连续方程到离散代码
2.1 标准二阶被控对象与方程
为了把问题锁死在一个具体场景里,我选了一个最经典也最好复现的对象——质量-弹簧-阻尼系统。运动方程是:
m·x_ddot + c·x_dot + k·x = u + d其中m是质量,c是阻尼系数,k是弹簧刚度,u是控制力,d是外界扰动。归一化之后,令m=1,c=1.5,k=1,得到状态方程:
x1_dot = x2 x2_dot = -1.5·x2 - x1 + d + b0·u这里的“真实”对象里面,控制增益的真实值是1,但我们仿真时故意假装不知道,给ADRC一个略微偏离的b0估计值,比如0.8或1.2,这样更贴近工程实际中模型参数不准确的场景。扰动d我习惯用阶跃信号来测“抗扰能力”,再用正弦信号来测“跟踪能力”,两个测试分开做比较清晰。
建仿真模型我建议用脚本而不要太早依赖Simulink。脚本的好处是参数化方便,改参之后能批量跑出一组对比曲线,还能把过程数据留着做后处理。Simulink做交互演示很方便,但批量调参和自动化搜索不如脚本顺手。刚开始学,直接写一个仿真循环,每个采样周期内更新一次TD、ESO、控制律和被控对象,逻辑反而更清晰。
2.2 观测器与非线性函数的离散化实现
连续系统没法直接搬进数字控制,仿真必须离散化。这里我用最简单的欧拉法,采样周期Ts取0.001秒,对于带宽在几十rad/s的系统来说精度足够。关键是,控制器里所有的积分都用累加方式实现,而不是用复杂的积分器库函数。
扩张状态观测器的连续形式是:
e = z1 - y z1_dot = z2 - beta01·e z2_dot = z3 - beta02·fal(e, alpha1, delta) + b0·u z3_dot = -beta03·fal(e, alpha2, delta)这里z1估计x1,z2估计x2,z3估计总扰动x3=f(...)。fal是非线性函数,定义常见版本是:
fal(e, alpha, delta) = e / delta^(1-alpha) 当 |e| <= delta fal(e, alpha, delta) = |e|^alpha · sign(e) 当 |e| > deltadelta的作用是在零点附近做一个线性区间,防止在极点处增益无穷大导致抖振。alpha越小,非线性增益越强,对小误差反应越敏锐。ESO里一般alpha1取0.5,alpha2取0.25,delta取0.01到0.1之间。
如果从零开始敲代码,建议先把ESO三个状态方程写成一个更新函数,每一步都显式更新。需要注意顺序:先根据当前状态更新ESO,得到z1、z2、z3,再计算控制律u,最后用u去更新被控对象的状态。如果顺序搞反了,控制量用到的是上一拍的观测值,会在系统里引入额外一拍的延迟,对仿真精度有影响,虽然影响不大,但建议养成正确习惯。
2.3 一个可以直接跑的仿真脚本骨架
我用M语言风格写一个最小可跑骨架,参数后面再解释,先保证逻辑看得明白。
% 二阶非线性ADRC仿真骨架 Ts = 0.001; T = 5; steps = round(T/Ts); % 对象与ADRC参数 m = 1; c = 1.5; k = 1; b0 = 0.8; % 真实控制增益是1,我们故意估错 kp = 100; kd = 20; % 反馈律增益,对应带宽10rad/s beta01 = 150; beta02 = 7500; beta03 = 125000; alpha1 = 0.5; alpha2 = 0.25; delta = 0.01; r = 10; h = 0.01; % TD的快速因子与滤波因子 % 状态初始化 x1 = 0; x2 = 0; z1 = 0; z2 = 0; z3 = 0; v1 = 0; v2 = 0; u = 0; % 记录 x1_log = zeros(1, steps); x2_log = zeros(1, steps); z3_log = zeros(1, steps); u_log = zeros(1, steps); for i = 1:steps % 期望信号:阶跃1,在2.5s的时候加入扰动d=0.5 v = 1.0; d = 0.5 * ( (i*Ts) >= 2.5 ); % 1) TD更新:安排过渡过程 e_v = v1 - v; v1 = v1 + Ts*v2; v2 = v2 + Ts*fhan(e_v, v2, r, h); % 2) ESO更新:观测状态和总扰动 e = z1 - x1; z1 = z1 + Ts*(z2 - beta01*e); z2 = z2 + Ts*(z3 - beta02*fal(e, alpha1, delta) + b0*u); z3 = z3 + Ts*(-beta03*fal(e, alpha2, delta)); % 3) 控制律 e1 = v1 - z1; e2 = v2 - z2; u0 = kp*fal(e1, 0.5, delta) + kd*fal(e2, 0.75, delta); u = (u0 - z3) / b0; % 4) 被控对象更新 x1_ddot = -c*x2 - k*x1 + d + u; % 真实对象,控制增益为1 x1 = x1 + Ts*x2; x2 = x2 + Ts*x1_ddot; % 记录 x1_log(i) = x1; x2_log(i) = x2; z3_log(i) = z3; u_log(i) = u; end用这个骨架跑出来的数据,画一下x1_log看阶跃响应,正常情况应该是无超调或者极少量超调地逼近目标值。z3_log这条线应该逼近真实扰动d的曲线,扰动在2.5秒加入后,z3能快速跟上0.5左右的数值。如果z3跟不上,后面补偿就无从谈起。这个脚本里我用了fhan这个非线性函数,它比单纯的线性微分器更平缓,具体公式展开有点长,是工程设计中最常用的离散化跟踪微分器实现。函数定义可以搜“最速跟踪微分器”,也可以先用一个简单的线性TD替代,效果差别不大。
3. 手把手调参:从零开始让系统稳定
3.1 调参前必须弄懂的三个核心关系
动参数之前,先把三个关系锁死在脑子里。第一个关系是控制器带宽omega_c与控制性能的关系。在ADRC的线性化设计方法里,反馈增益kp和kd可以由一个统一的带宽参数omega_c确定:kp = omega_c^2,kd = 2·omega_c。omega_c越大,响应越快,但对观测噪声和模型误差的放大越明显,执行机构也越容易饱和。所以omega_c不是越大越好,它是性能与鲁棒性之间的一把尺子。
第二个关系是ESO带宽omega0与观测精度/噪声的关系。同样地,线性ESO的增益可以取beta01=3·omega0,beta02=3·omega0^2,beta03=omega0^3。omega0越大,ESO跟踪真实状态和扰动的速度越快,但高频噪声会被大幅放大。经验上omega0取omega_c的3到10倍,刚开始建议取5倍。这个比例不是天上掉下来的,它保证控制器在把输出叫到目标之前,ESO已经把总扰动看清楚,避免“边打边看错方向”。
第三个关系是b0控制增益的准确性。这是一票否决的关系。b0如果估得明显偏大,实际补偿力度就会偏小,系统反应迟钝,ESO会一直累积估计误差;b0估小了,补偿过度,容易在扰动作用点周围振荡。很多刚上手的人一上来就调kp、kd,发现怎么调都抖,最后才发现是b0差得太远。正确顺序是:先粗调b0到输出量级合理,再锁住b0,专心调带宽关系,最后才做非线性参数的微调。
3.2 粗调阶段:先定b0和带宽,把环路闭合
最简单可行的办法是:先把ADRC的所有非线性参数全部“线性化”。非线性函数fc这个alpha都设为1,delta设得足够大,让fal函数在整个工作区间内退化成线性函数。也就是说,ESO退化成线性扩张状态观测器,反馈律退化成线性PD。这样ADRC就变成了一个线性控制器,所有参数完全由omega_c和omega0和b0决定。
我习惯先给被控对象拍一个开环响应,手动给一个控制量,比如u=1,持续1秒,看看输出x1的加速度大约是多少。对质量-弹簧-阻尼系统,如果u=1,那么x2_dot大约接近1(理想情况下都折算到归一化方程里),于是b0可以从1.0开始试。实际中如果b0设成真实值的1.2倍,通常系统能稳定,只是动态略偏软;如果设成0.5倍,可能会在阶跃起步时出现明显回弹。我的经验是b0先取真实值的1.2倍或0.9倍都行,关键是稳定后看控制量u的“均值基准线”是否合理。如果u长期偏大超过饱和限幅,说明b0偏大了,补偿力度不足;如果u长期震荡且幅值不大,b0可能偏小。
粗调时omega_c从10rad/s开始推。对很多机电对象,这个带宽不算激进。调试目标是阶跃响应2%调节时间约0.4秒到0.6秒之间。omega_c先给20,跑仿真,如果出现振荡就减半;如果太迟钝就加倍。每次只改一个参数,记录调节时间和超调量,至少跑十条曲线之后,你会感觉到带宽参数的作用方向非常清晰。
3.3 精调阶段:处理稳态误差、超调与扰动脉冲
粗调之后,系统基本能稳住,但可能残留几个小问题。第一,稳态误差不为零。这时不要急着调kp,先看z3曲线是不是把真实扰动给追出来了。如果阶跃扰动加入后,z3慢慢逼近真实扰动但有一点滞后,稳态误差就是残差累积的结果。这不是kp的问题,是ESO跟踪扰动速度不够,优先把omega0再往上提一档。如果z3追得上了但闭环还有静差,再看反馈律的kp通道,适当增加alpha1到0.7左右,用非线性增益把小误差区的放大倍数提上去。
第二个常见问题是超调。二阶系统超调的原因通常是反馈带宽相对TD安排的目标轨迹来说太激进了。这里有一个很优雅的处置思路:不要猛减omega_c,因为那会牺牲响应速度,而是调整TD的r和h。r越大过渡过程越快,h越大滤波效果越强,过渡过程越平滑。跑超调问题时,先把r减小一半看效果,你会发现输出超调很快被削掉。这就是安排过渡过程带来的红利,PID没有这个工具。
第三是扰动脉冲。系统在恒速运行中突然来一个冲击载荷,输出会先跌下去再拉回来。这个回落深度和恢复速度,主要考验ESO峰值的跟踪速度。如果你希望快速恢复,就提高omega0;但omega0提上去之后噪声会进来,这时再看输出曲线有没有微小的高频毛刺。如果有毛刺,就稍微调大delta,让ESO在误差小时的增益不再那么敏感。这本质上是“观测带宽”和“噪声容忍度”的权衡,没有什么万能参数,只能在现场反复看曲线。
3.4 不同工况下的参数修正思路
工程对象很少只在一个工作点运行。比如一台电机,低速重载和高速轻载两个工况下,模型参数可能变化很大。这时候ADRC的优势就出来了——它的总扰动估计可以自动吸收大部分模型变化,不需要换控制器结构。但b0如果变化很大,还是需要做“在线增益修正”,或者至少选择一个中间偏保守的b0值。
我的做法是做一个简单的参数表,记录不同工况下b0的大致范围,然后在主控流程里做分段切换。比如低速段b0=0.7,中速段b0=0.9,高速段b0=1.05。切换时加一个简单的低通滤波或者斜坡过渡,避免参数跳变引发瞬态冲击。不要指望一套万能参数打天下,尤其在模型非线性比较强的对象上,分段增益是最务实的手段。
如果追求更省事的办法,可以把b0交给一个在线辨识模块去估计,这是另一套话题了,这里不展开。但记住一点,ADRC参数再强也顶不住执行器饱和。如果你发现响应一开始很猛,然后控制量被限幅打住,后面输出就拖泥带水,那十有八九是omega_c给的过大,导致需求加速度超出了物理可实现范围。先看执行机构极限,再看是否把跟踪微分器的加速度上限设进去,不要跟参数硬刚。
4. 仿真里的坑与排查实录
4.1 ESO数值震荡的解决
这是我最常遇到的坑,也是最容易误导人的坑。ESO在仿真里经常出现z2或z3的数值震荡,输出曲线看着还行,但控制量u高频抖动。一旦看到控制量像锯齿一样来回摆,第一反应不是调反馈律,而是看ESO状态曲线。
震荡根源多半是采样周期过大,或者beta03取得过大。你把Ts从0.001改成0.0005,如果震荡明显减轻,那就是离散化步长问题。工程上采样周期受硬件限制没法随便改,那就只能降低omega0。另外也可以先检查delta。非线性ESO在误差很小时,fal曲线增益是1/delta^(1-alpha),如果delta取得太小,比如0.001,零点附近的等效增益会非常大,相当于一个超高的线性放大环节,数值上一不小心就爆了。这种情况把delta调到0.05左右,震荡基本能被压下去。
4.2 初值冲击与观测器峰值
系统启动的一瞬间,z1从0开始,但x1可能一开始就是1,误差e瞬间等于-1,这个突变会让ESO的z3瞬间冲出一个大尖峰。很多仿真里看到的第一个脉冲就是这个问题。解决办法是给ESO的初值赋成跟被控对象初值一致,至少在仿真里手动初始化。更稳妥的办法是开启ESO后,先让控制器在很小增益下运行几百毫秒,让观测器收敛了再提增益。这在实际嵌入式系统里就是“软启动”的一个变体。
工程上还有一种做法:给ESO的误差信号加一个一阶惯性滤波,但这样会引入相位延迟,削弱观测速度。我个人的经验是,优先保证初值匹配,再考虑滤波。因为初值冲击是启动时的单次问题,而滤波是持续的重度性能影响,两者轻重分明。
4.3 非线性参数 alpha、delta 的调节误区
很多人听说“alpha越小抗扰越强”,于是直接把alpha设成0.1,结果发现系统变得特别神经质,一点噪声就能让控制量抖得飞起。这是因为alpha越小,小误差区的增益被抬得越高,等于在小信号范围内设置了一个极高的比例放大。实际上alpha的合理区间一般在0.5附近,不要低于0.3。delta则不要小于系统噪声幅度的10倍,否则零点附近的“线性死区”保护作用就没意义了。
我把这两个参数当作“性能微调旋钮”,而不是“主调旋钮”。线性ADRC往往已经能解决95%的问题,非线性参数只是把那最后一点超调、静差再啃掉。如果基础参数都不合理,指望用非线性参数弥补,大概率会更糟。每次微调只改一个参数,比如alpha1从0.5往0.4降,跑一次看超调变化,再往0.6提,跑一次看噪声变化,这个小循环必须有记录。
为了给刚入坑的朋友一个速查表,我把常见现象和优先处置方向整理如下。
| 现象 | 优先检查项 | 其次处理方向 |
|---|---|---|
| 阶跃响应超调大 | TD的r是否过大,omega_c是否过高 | 降低r,或降低kd |
| 稳态残余误差 | z3是否准确跟踪真实扰动 | 提高omega0,微调alpha1 |
| 控制量高频抖动 | ESO噪声放大,delta太小 | 增大delta,降低omega0,注意离散步长 |
| 扰动后恢复慢 | ESO对扰动的观测滞后 | 提高omega0,检查b0是否偏大 |
| 初始启动尖峰 | ESO初值不匹配 | 初始化z1=x1,或软启动 |
| 大误差响应迟钝 | b0偏大,补偿不足 | 降低b0,或提高omega_c |
| 小误差来回抖动 | alpha太小或delta太小 | 适当增大alpha,增大delta |
4.4 关于参数整定的一点个人习惯
仿真跟实机差别很大。仿真里你改一个参数立刻能看到结果,实机上可能还要担心噪声、通信延迟、执行器饱和保护。所以我的习惯是仿真阶段把参数放在设计值附近跑出一组“理想曲线”,再用实机数据回填到仿真模型里,把模型失配项加大一些,重新整定一遍。ADRC的优势在这种“模型不完全准确”的场景里最突出。
如果你连仿真阶段都被参数折磨到怀疑人生,有一个笨但有效的方法:写一个脚本,在omega_c的网格或b0的网格里跑循环,把所有参数组合的阶跃响应记录下,然后挑出超级调量最小、调节时间最短的几组做人工筛选。早期我会有一种“调参数是玄学”的错觉,跑完参数扫描之后就会明白,其实每个参数的作用方向非常稳定,只是同时变化的相互作用让它看起来像谜。批量扫描能帮你快速建立对参数作用方向的直觉。
最后说一句,我到现在每次调ADRC还是会把TD、ESO、控制律的参数分组写在纸面上,先锁定一组参数把所有曲线打出来贴在屏幕上,再动下一组。ADRC这玩意儿,结构框架不难,难的是你愿意花时间跟它的状态观测曲线较劲。如果你把一个新对象的ESO观测曲线调到跟真实状态曲线基本重合,你会发现后面的反馈环节随便整一整都能稳定,那才是真正“调明白了”的感受。