说起ADRC(自抗扰控制),这几年搞控制的人肯定不陌生。仿真论坛上一搜,机械臂、电机驱动、过程控制、四旋翼,到处都在聊扩张状态观测器(ESO)、带宽整定这套玩法。我最初接触这个东西,是因为做电机伺服仿真时被负载突变和模型参数漂移折磨得不行,PID参数换一组工况就得重新调一遍,实在烦人。后来把ADRC的仿真模型搭起来跑通,才真正体会到“把模型误差当成扰动来估计”这个思路有多香——但我也得实话实说,ADRC上手门槛不算低,尤其是扩张状态观测器这部分,公式看着简单,真放到仿真里调试时,波形一出问题就不知道是带宽不够还是b0参数没给对。
这篇博客我打算不按教材套路讲,完全从一个做仿真、调试控制器的工程师视角,把ADRC里最核心的扩张状态观测器从原理到建模、再到调参实操和问题排查,一步一步讲清楚。如果你正准备做ADRC仿真,或者已经在Simulink里搭了模型但波形一直不理想,这篇内容应该能帮你少走不少弯路。
1. 开始之前:为什么大家突然都在折腾ADRC
1.1 一堆仿真关键词背后的核心焦虑
我最近在一个控制技术社群里潜水,发现一个很有意思的现象:大家搜索最多的仿真关键词,已经不只是“PID控制”“Simulink”这类基础词了,而是大量出现“ADRC”、“扩张状态观测器”、“panda机械臂gazebo仿真”、“四旋翼仿真 滑模控制 simulink”、“系统辨识与自适应控制matlab仿真”这种偏现代控制算法的词。
这说明什么问题?说明越来越多工程师和研究生做仿真的时候,已经不再满足于“把PID参数调稳”,而是想解决真实系统里的“边跑边变”的问题。比如机械臂关节负载随姿态变化、无人机飞行中气动参数随速度变化、电机驱动系统里负载突变和摩擦非线性。这些场景如果每个都用PID单独整定,工作量极大,而且鲁棒性往往没保证。大家四处搜ADRC,本质上是同一个焦虑:有没有一种控制器,可以不完全依赖精确模型,又能对付各种不确定性?
ADRC刚好踩在这个痛点上。它的设计哲学就是“不求模型精确,只求把模型不确定的部分实时估出来然后抵消掉”。这就好比你要搬一块石头,不需要知道石头的准确重量,只要你一只手能感知到石头有多重、再用另一只手反向补偿,就能稳稳搬起来。ESO就是那只“感知重量的手”。
1.2 ADRC的“广告”和现实边界
不过我也要说句公道话,ADRC不是万能的。我在仿真和半实物验证中踩过不少坑,其中最核心的一点是:扩张状态观测器的估计能力是有边界的,它没法无中生有,观测器的工作也讲究“信号激励”和“参数匹配”。
大家通常看到宣传语说“ADRC不依赖精确模型”,这话得分开理解。它确实不要求你建立一个精确的、基于物理定律的白箱模型,但它要求你知道系统的“控制增益”大概是多少,也就是那个常被称作b0的标称参数。这个参数在仿真里怎么取、取错了会发生什么,是很多论文里含糊带过、但实际调试时第一个卡住人的地方。
另外,ADRC里的ESO本质上是一个状态观测器,它观测的是“扩张出来的扰动状态”。既然是观测器,就一定涉及收敛速度和噪声抑制的权衡。你在仿真里把带宽调得特别高,波形估计会特别“跟手”,但一旦在信号里加点测量噪声,观测值就抖成筛子。这个道理明白得越早,你调参花的无效时间就越少。
我建议所有准备开始ADRC仿真的朋友,先放下“这个算法很高级”的滤镜,把它当成一个“带扰动估计的PD控制器”来理解——你先把这句话吃透,后面ESO的原理、参数整定、乃至到实物移植,都会顺理成章得多。
2. 扩张状态观测器到底在“看”什么
2.1 别被“扩张”两个字吓住
好多朋友一听“扩张状态观测器”,第一反应是这玩意儿肯定很高深,一堆矩阵、一堆秩条件。其实核心思想非常简单,就是从你实际测量到的输出信号里,把系统的内部状态和扰动状态一并“重构”出来。
先看一个最普通的二阶系统。假设我们的被控对象可以写成这种形式:
ÿ = f(y, ẏ, w, t) + b·u
这里u是控制输入,y是输出,f代表系统里所有说不清、道不明的部分——包括未建模动态、外部扰动、非线性摩擦、参数漂移等等,反正在我们的控制设计里不想一个个去精确建模的东西,全部塞进f里面。b是控制增益,就是你给多少输入能产生多少加速度的那个系数。
如果把状态变量定义为x1 = y(位置),x2 = ẏ(速度),那么这个系统可以写成:
x1̇ = x2
x2̇ = f + b·u
问题来了:f是未知的,而且它直接影响系统行为。如果我能把当前时刻的f实时估计出来,那么在控制量u上减去这个估计值,就可以把系统“掰回”到近似无扰动的理想积分串联型,然后再用简单的比例微分控制就能搞定了。这个思路就是ADRC的核心主张。
2.2 扰动是怎么被“扩张”出来的
要把f也当成一个状态来估计,最简单粗暴的办法是把它“升格”成第三个状态变量x3,并且假设它的变化率h = ḟ是有限的(或者至少是分段有界的)。这样一来,原本的二阶系统就变成了一个三阶系统:
x1̇ = x2
x2̇ = x3 + b·u
x3̇ = h
其中x3就是“扩张状态”,它的物理含义就是那一坨总扰动。当我们为这个三阶系统设计观测器时,输入只有y(也就是x1)有直接测量,但通过观测器的误差反馈,我们可以把x2、x3都估计出来。这就是“扩张状态观测器”这个名字的由来——状态变量没有增加物理维度,却把扰动变成了一个可观测的状态。
具体写出来,以线性ESO为例,观测器长这样:
e = z1 - y
z1̇ = z2 - β1·e
z2̇ = z3 + b0·u - β2·e
z3̇ = -β3·e
其中z1是y的估计,z2是ẏ的估计,z3就是对总扰动f的估计,b0是我们对真实控制增益b的估计。β1、β2、β3是观测器增益,它们决定了估计误差收敛的速度。整个ESO的工作方式,可以理解成“用一个虚拟的系统副本去跟踪真实系统,只要虚拟系统和真实系统之间的输出有误差,就用这个误差去修正虚拟系统的内部状态,直到两者一致”。
2.3 为什么大家都用线性ESO起步
很多早期文献一上来就摆非线性函数fal(x, α, δ),参数多不说,整定还带着玄学色彩。我自己做仿真时,除非复现某篇论文的原始算法,否则一律推荐用线性ESO,也就是上面写的那组线性微分方程。
线性ESO最大的优点就是增益可以解析化处理。如果我们把观测器的三个极点都放在s = -ω0处(ω0通常称为观测器带宽),那么增益就非常清爽:
β1 = 3·ω0
β2 = 3·ω0²
β3 = ω0³
你只要关心一个参数ω0,就能把整个ESO的增益定下来。这个表达式背后的逻辑是让观测器误差微分方程的极点配置在左半平面同一位置,保证误差收敛且没有振荡。第一次搭模型的时候,直接用这套带宽公式,少掉一大半调参麻烦。
我一直觉得,做仿真研究最忌讳的就是“堆参数”。一上来就加五个非线性因子,波形坏了你根本不知道是哪里出了问题。线性ESO把参数数量压缩到最小,让调试者可以专注于理解模型结构和系统动态,这是入门阶段最优的选择。
3. 仿真模型搭建与参数调节实操
3.1 把被控对象写成ESO看得懂的形式
仿真第一步不是急着拖Simulink模块,而是先把你手上的被控对象整理成ESO能够处理的标准形式。我之前用一个简化直流电机模型来演示ADRC,模型表达式是:
J·θ̈ + c·θ̇ = k·u
其中J是等效转动惯量,c是粘滞摩擦系数,k是力矩系数。如果我们把它改写成ÿ = f + b·u的形式,就有:
ÿ = -(c/J)·ẏ + (k/J)·u
这里可以取标称模型参数Jn、cn、kn,那么b0 = kn/Jn,而f = -(c/J)·ẏ + (k/J - kn/Jn)·u,也就是说系统自身的动态、模型偏差、甚至外部负载扰动全都被归入f。这也是为什么我们不需要精确知道J、c到底是多少——只要大概知道b0的范围,剩下的统统让ESO去估计。
这个“整理过程”在实际工程里帮你省掉大量辨识时间。我以前做一个电机实验台的时候,光是辨识转动惯量就折腾了大半天。换用ADRC结构以后,J取一个大致合理的值就可以让系统跑起来,精度由控制器闭环保证,这是传统PID完全不具备的优势。
不过要注意一点:b0不能随便取。如果你对真实b的估计偏差太大,相当于控制器对执行机构“出力”的理解是错的,ESO虽然能估计出补偿量,但补偿的精度和瞬态表现都会受影响。我的经验是b0偏差30%以内问题不大,如果偏差到两三倍,即使仿真能稳住,实际系统中的表现也会明显变差。
3.2 Simulink模型架构与核心连线思路
在Simulink里搭建ADRC,我习惯把整个控制结构分成三层:被控对象层、ESO层、控制律层。
被控对象层就是你自己的动力学模型,可以是基于微分方程搭建的,也可以直接调用Simscape Multibody这种多体仿真模块。如果是机械臂仿真,这一步通常直接用机械臂模型,关节力矩作为输入,关节角度或末端位姿作为输出。
ESO层是这个结构里的核心。它接受的输入是:控制量u(你发给被控对象的那个信号)和测量输出y(对象的实际角度或位置),经过ESO的微分方程运算后,输出z1(位置估计)、z2(速度估计)、z3(扰动估计)。注意,ESO里用到的b0必须和对象模型里的系数对应,不能接错线。
控制律层是ADRC的“大脑”,通常采用如下形式:
u = (u0 - z3) / b0
其中u0是一个普通PD控制器算出来的中间控制量:
u0 = kp·(r - z1) + kd·(ṙ - z2)
这里r是期望轨迹,ṙ是期望速度(如果目标指令只有阶跃位置,ṙ通常为0即可)。kp和kd可以按控制器带宽ωc来设计:kp = ωc²,kd = 2·ωc。一整套下来,你需要手动调的参数实际上只有两个:ωc和ω0,再加上b0这个模型参数。
在我自己的Simulink实践里,ESO层我通常不用积分模块堆砌,而是直接用一个S-Function或者Matlab Function模块写微分方程,这样仿真步长和离散逻辑更可控,调试起来也好加断点。如果纯粹用积分器和增益搭,模型一大就乱,尤其是你想观察中间量z1、z2、z3时,得拉一堆线,非常容易看错。
3.3 带宽整定的具体数值示例
为了把调参流程说清楚,我拿一个具体的仿真参数实例来演示。假设系统参数为J = 0.02 kg·m²,c = 0.1 N·m·s/rad,k = 1 N·m/V,标称参数我们估计成Jn = 0.02,kn = 0.9,也就是b0 = 45。控制目标是从零开始阶跃跟踪到角度1 rad,并在仿真中途t=3s时加一个5 N·m的外部负载突变。
第一步定控制器带宽ωc。这个参数决定了闭环响应速度,你可以把它理解成ADRC版的“PID比例增益”。我做仿真一般从ωc = 10开始试,这个值对应的kp = 100,kd = 20。如果你希望系统更快,就往上加;如果执行机构有饱和限制,则要降下来。这里有一点经验之谈:ωc给定的响应频率最好不要超过系统执行器有效带宽的1/5到1/3,否则控制量会频繁撞饱和限幅,波形出现锯齿。
第二步定观测器带宽ω0。通常经验是ω0取ωc的3到5倍,因为观测器必须比控制器收敛快,才能及时把扰动信息反馈给控制律。如果ω0太低,ESO的估计滞后明显,你会看到系统输出虽然有阻尼但就是拉不到目标——那是因为它根本还没看清扰动长什么样;如果ω0太高,观测器噪声放大厉害,控制量毛刺多。仿真里没有测量噪声时,可以把ω0大胆调高一些,反正不会报警;但如果你是做半实物仿真,传感器信号是有噪声的,这时候ω0就得收敛一点。
实际跑这组参数时,阶跃响应大约在0.5秒左右进入稳态,t=3s突加负载后,系统能在0.1秒左右恢复,z3的值也从0跃迁到约5,成功估计出外部力矩折算后的等效扰动。这个过程可以从Scope波形里直观看到:z3一路追踪着真实扰动,控制律再把它抵消掉。看到这个波形的瞬间,我对ADRC的好感度直接拉满。
3.4 用脚本搭一套快速验证的ESO
有时候我也不想每次都在Simulink里拖动鼠标调参,尤其是要批量测试不同b0、ω0组合时,写脚本比GUI操作高效得多。下面这段Matlab代码是我常用的一个极简验证环境,不用任何工具箱,新手上手也不难。
% 一个极简的ADRC+ESO仿真示例(二阶对象) % 可以在MATLAB或Octave中运行 dt = 0.001; % 仿真步长,单位秒 T = 6; % 总仿真时长 N = T / dt; t = (0:N-1)' * dt; % 被控对象参数(真值) J = 0.02; c = 0.1; k = 1.0; % ADRC参数 b0 = 45; % 控制增益的标称估计 wc = 10; % 控制器带宽 w0 = 40; % 观测器带宽 kp = wc^2; kd = 2*wc; beta1 = 3*w0; beta2 = 3*w0^2; beta3 = w0^3; % 状态初始化: x = [角度; 角速度], z = [z1; z2; z3] x = [0; 0]; z = [0; 0; 0]; u_hist = zeros(N, 1); y_hist = zeros(N, 1); z3_hist = zeros(N, 1); r = 1.0; % 目标角度 for i = 1:N y = x(1); % 控制量:PD + 扰动补偿 u0 = kp * (r - z(1)) + kd * (0 - z(2)); u = (u0 - z(3)) / b0; % 外部扰动在 t=3s 后加入 disturb = 0; if t(i) >= 3.0 disturb = 5.0; % 等效扰动折算进状态方程 end % 真实对象更新(显式欧拉,步长足够小即可) x1_dot = x(2); x2_dot = -(c/J)*x(2) + (k/J)*u + disturb/J; x = x + dt * [x1_dot; x2_dot]; % ESO更新 e = z(1) - y; z1_dot = z(2) - beta1 * e; z2_dot = z(3) + b0 * u - beta2 * e; z3_dot = -beta3 * e; z = z + dt * [z1_dot; z2_dot; z3_dot]; u_hist(i) = u; y_hist(i) = y; z3_hist(i) = z(3); end % 画图 subplot(3,1,1); plot(t, y_hist, 'b'); grid on; ylabel('角度'); subplot(3,1,2); plot(t, u_hist, 'r'); grid on; ylabel('控制量'); subplot(3,1,3); plot(t, z3_hist, 'k'); grid on; ylabel('扰动估计 z3'); xlabel('时间/s');这段脚本把ADRC最核心的四行更新代码完全暴露了出来,你只要改参数就能做一整轮的对比实验。这种“脚本先跑通、模型再封装”的开发顺序,是我个人做算法验证时最推荐的做法——它逼着你理解每一步的数学含义,而不是只会在Simulink里点鼠标。
4. 仿真结果怎么看:正常波形、异常波形与排查
4.1 常见“看不懂”的波形盘点
仿真跑通只是第一步,大部分人卡在“波形不对但不知道哪里不对”。我把这三年被问得最多、自己也踩过的几个典型现象整理了一下,你不妨对照检查。
第一个典型现象:输出能跟踪目的标,但z3波形有高频振铃。出现这种波形,十有八九是观测器带宽ω0取太高了。虽然仿真里纯数学信号没有噪声,但数值积分离散化误差在高带宽下也会被放大,表现出来就是z3在高频小幅抖。如果是在Simulink里用变步长求解器,这个问题尤其明显,建议把求解器改成固定步长(比如ode4,步长1e-4到1e-3),很多时候“治好”了振铃。
第二个典型现象:输出慢腾腾爬向目标,几乎没有快速响应,但你明明把ωc调得很大。这种时候别急着继续加kp,先看z3是不是一直往一个方向偏。如果z3慢慢爬升到很大值还没停下来,基本可以断定是b0设置和真实值偏差过大,ESO正在拼命补偿这个偏差。你把b0往真实b的方向修正一下,响应速度马上就回来了。
第三个典型现象:扰动加入后,系统能稳住但会先冲出去一个大大的尖峰。这往往不是ESO的问题,而是扰动估计的收敛跟不上实际扰动的突变。解决办法是适当提高ω0,或者把ωc和ω0的差距拉大一些。还有一种情况是控制量限幅导致ESO“看错”了输入——你发给对象的真实控制量已经被饱和了,但ESO内部用的却是不饱和的u,这会导致z3估计失真,在机械臂这类执行器容易饱和的场景里尤其值得注意。
4.2 调参数顺序的实测经验
ADRC参数虽然少,但它最终表现是多个参数耦合的结果。我的实战经验是,调参顺序比调参数值本身更重要,乱调只会让人越调越没信心。
第一步固定b0。如果你真不知道b0,可以用一个简单的开环仿真来辨识:给对象一个已知的阶跃控制量u,测出响应的加速度,近似b = ÿ/u。仿真是理想环境,这个值很好测,千万不要跳过。
第二步调ωc。在ESO还不太准的情况下,先用小ωc让系统稳下来,比如从5到10开始。这时候不追求快,追求系统不振荡。等系统稳了,再逐步加大ωc,直到你看到一个“又快又稳但宁可稍微慢一点”的临界响应。
第三步调ω0。ω0的底线是让z3能跟上真实扰动,上线是别让噪声发毛。你可以通过给测量信号人为叠加白噪声来做测试,找到那个“估计能跟上、但波形还不太抖”的折中位置。
我个人的小习惯是在调参时把z3和真实扰动信号一起画在同一张图上。很多人只关注输出曲线和控制量曲线,从来不关心z3是不是跟随真实扰动,结果参数错了都不知道哪里错了。如果你能看到z3波形稳定逼近真实扰动曲线,那么闭环表现基本就差不了。
4.3 ESO与模型失配时的表现
模型失配问题是仿真到实物之间最大的一道鸿沟。很多人在仿真里用精确的模型参数测试ADRC,觉得性能无敌,一到硬件上就发现“这套参数全废了”。核心原因就是仿真时没有主动引入模型失配和外界干扰,ESO根本没发挥出它的价值。
我在做仿真时,甚至故意把b0设得和真实b相差个百分之二三十,再引入摩擦死区、饱和、测量噪声等非理想因素,看ESO能不能扛住。结果是,线性ESO对于缓变的失配和不确定性处理得相当漂亮,但对于突然的、局部性的非线性变化,比如库仑摩擦在速度换向瞬间的突变,它会有短暂的估计过渡期。这也是为什么有研究者引入非线性ESO或者级联扩张状态观测器来应对更复杂的扰动,但工程上一级线性ESO其实已经能解决大部分问题。
如果你想评估一个ADRC方案到底靠不靠谱,建议在仿真里同时做两组测试:一组是理想模型下的性能验证,一组是加入模型参数拉偏20%加突加扰动的鲁棒性测试。只看第一组测试,你永远不知道这套参数在现场会不会翻车。这一条也送给所有准备去做机械臂Gazebo仿真或半实物仿真的朋友,非理想环境下的验证永远比理想环境下的演示更有价值。
5. 从仿真走向实物前,还要补哪些东西
5.1 采样时间、离散化与观测器实现
仿真中我们经常用连续微分方程想象整个系统,但真实的控制器都是离散运行的,采样时间决定了一切。ESO从连续到离散也需要重新审视。
最常用的离散化方法是一阶欧拉法,就是把上节代码里的更新公式直接写进控制器代码里,形式非常简单。但如果采样时间较大,比如5毫秒或10毫秒,一阶欧拉法可能会引入明显滞后。这种情况下可以改用二阶龙格库塔法对ESO的状态更新做数值积分,或者干脆用双线性变换法设计离散版的ESO。
我自己做嵌入式移植时最常踩的坑是:在Simulink里仿真步长设成了1e-5量级,跑得很漂亮;一搬到实物控制器上,采样周期只有1毫秒,直接在C代码里套用连续域算出来的β1、β2、β3,结果整定曲线越跑越飘。这是因为离散化后的等效极点位置已经偏移了,高速的连续域极点在离散域里可能不再稳定。建议你至少在仿真的最后阶段,把Simulink求解器改成离散求解器,把采样步长设成你实际控制器硬件的周期,跑通了再去移植,会稳妥很多。
离散化带来的另一个问题是ESO反馈增益的调节空间更少了。你连续域可以取ω0 = 100,采样周期1毫秒时对应奈奎斯特频率是500 rad/s,ω0离这个上限太近,ESO会出各种诡异现象,比如估计值震荡、相位抖动。工程上一般取ω0不超过采样角频率的1/10到1/20,也就是说采样周期1毫秒时,ω0的实用上限大约在50到100之间。
5.2 传感器噪声与控制器限幅对ESO的影响
仿真的世界里,传感器输出是理想信号。实物世界里,哪怕一个便宜的电位器都会给你叠上一层噪声。这层噪声进入ESO以后会被β1、β2、β3放大,尤其是β3对应着ω0的三次方,放大作用是灾难性的。这就是为什么很多人在实物上照搬仿真的ω0参数,电机一转起来,控制量抖得跟电钻似的。
应对方案主要有三条路。第一是在传感器信号进入ESO之前加低通滤波,但要注意滤波相位滞后会拉低ESO的估计速度,所以滤波截止频率也不能太低。第二是通过降ω0来降低观测器对高频噪声的敏感度,这是最简单粗暴的方案,代价是扰动估计会慢半拍。第三是在控制器输出端加幅值限幅、变化率限幅,防止ESO输出的大扰动估计值驱动执行器做出危险动作。
再说一个很多人会忽略的细节:控制器限幅和ESO内部的u必须保持一致。如果对象实际收到了一个被限幅后的u,而你的ESO还在用没限幅的u作为输入,那z3估计出来的是“假的扰动”。解决方式很简单,把限幅环节放到ESO输入和对象输入的前面,保证ESO拿到的是真实进入对象的那个控制信号。这个细节在不少开源代码里都忽略了,但现场调试时它就是现象诡异的元凶之一。
5.3 与机械臂仿真等场景结合的思路
再回到热词里的“panda机械臂gazebo仿真”这类场景。机械臂控制的难点在于多关节耦合、负载随位形变化、基座振动干扰多。很多人一上来就想在Gazebo里给每个关节都装一个独立ADRC,思路没有错,但要注意:每个关节的b0会随机械臂位形变化而变化,如果你把b0当成一个固定值,那ADRC的“总扰动”估计项f里就会包含大范围的b偏差,这会增加ESO的负担。
在实际项目中,我更推荐两级方案:外层用ADRC处理轨迹跟踪和负载变化,内层识别并实时更新b0的标称值。很多现成的开源机器人库里有摩擦辨识和惯量估计模块,你可以把它们的输出拿来做b0的实时更新,这样ADRC的ESO只需要处理高频残差扰动,不需要硬扛模型剧烈变化。
和仿真平台打通的时候还要注意接口时序问题。Gazebo这类物理仿真有它自己的仿真步长(通常250Hz到1000Hz),你的控制器节点如果跑了更高频率,数据不同步会导致观测器看到“奇怪的测量跳变”。做机器人仿真时,控制器频率和物理引擎频率的配合,往往是比算法本身更折磨人的工程问题。
最后再说一个小建议:如果你打算把ADRC真正用到一个项目里,最好不要从零手写一大堆观测器代码。先依赖仿真环境把参数整定的直觉建立起来,然后写一个模块化的ESO,输入输出接口就三个信号——u、y、以及你估计的z3。等仿真跑通、参数行为你都摸透了,再把这个模块搬进实际控制器,整个过程会顺畅很多。我自己做过一次完整的“仿真到实物”迁移后,最深的感觉是ADRC的核心难点从来不在数学推导,而在于对各种非理想因素的觉知——你越早意识到仿真和实物之间的差距,越能把ESO从“论文上的公式”变成“手上真正能用的工具”。