
说起ADRC自抗扰控制这几年搞控制的人肯定不陌生。仿真论坛上一搜机械臂、电机驱动、过程控制、四旋翼到处都在聊扩张状态观测器ESO、带宽整定这套玩法。我最初接触这个东西是因为做电机伺服仿真时被负载突变和模型参数漂移折磨得不行PID参数换一组工况就得重新调一遍实在烦人。后来把ADRC的仿真模型搭起来跑通才真正体会到“把模型误差当成扰动来估计”这个思路有多香——但我也得实话实说ADRC上手门槛不算低尤其是扩张状态观测器这部分公式看着简单真放到仿真里调试时波形一出问题就不知道是带宽不够还是b0参数没给对。这篇博客我打算不按教材套路讲完全从一个做仿真、调试控制器的工程师视角把ADRC里最核心的扩张状态观测器从原理到建模、再到调参实操和问题排查一步一步讲清楚。如果你正准备做ADRC仿真或者已经在Simulink里搭了模型但波形一直不理想这篇内容应该能帮你少走不少弯路。1. 开始之前为什么大家突然都在折腾ADRC1.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̇ x2x2̇ f b·u问题来了f是未知的而且它直接影响系统行为。如果我能把当前时刻的f实时估计出来那么在控制量u上减去这个估计值就可以把系统“掰回”到近似无扰动的理想积分串联型然后再用简单的比例微分控制就能搞定了。这个思路就是ADRC的核心主张。2.2 扰动是怎么被“扩张”出来的要把f也当成一个状态来估计最简单粗暴的办法是把它“升格”成第三个状态变量x3并且假设它的变化率h ḟ是有限的或者至少是分段有界的。这样一来原本的二阶系统就变成了一个三阶系统x1̇ x2x2̇ x3 b·ux3̇ h其中x3就是“扩张状态”它的物理含义就是那一坨总扰动。当我们为这个三阶系统设计观测器时输入只有y也就是x1有直接测量但通过观测器的误差反馈我们可以把x2、x3都估计出来。这就是“扩张状态观测器”这个名字的由来——状态变量没有增加物理维度却把扰动变成了一个可观测的状态。具体写出来以线性ESO为例观测器长这样e z1 - yz1̇ z2 - β1·ez2̇ z3 b0·u - β2·ez3̇ -β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/radk 1 N·m/V标称参数我们估计成Jn 0.02kn 0.9也就是b0 45。控制目标是从零开始阶跃跟踪到角度1 rad并在仿真中途t3s时加一个5 N·m的外部负载突变。第一步定控制器带宽ωc。这个参数决定了闭环响应速度你可以把它理解成ADRC版的“PID比例增益”。我做仿真一般从ωc 10开始试这个值对应的kp 100kd 20。如果你希望系统更快就往上加如果执行机构有饱和限制则要降下来。这里有一点经验之谈ωc给定的响应频率最好不要超过系统执行器有效带宽的1/5到1/3否则控制量会频繁撞饱和限幅波形出现锯齿。第二步定观测器带宽ω0。通常经验是ω0取ωc的3到5倍因为观测器必须比控制器收敛快才能及时把扰动信息反馈给控制律。如果ω0太低ESO的估计滞后明显你会看到系统输出虽然有阻尼但就是拉不到目标——那是因为它根本还没看清扰动长什么样如果ω0太高观测器噪声放大厉害控制量毛刺多。仿真里没有测量噪声时可以把ω0大胆调高一些反正不会报警但如果你是做半实物仿真传感器信号是有噪声的这时候ω0就得收敛一点。实际跑这组参数时阶跃响应大约在0.5秒左右进入稳态t3s突加负载后系统能在0.1秒左右恢复z3的值也从0跃迁到约5成功估计出外部力矩折算后的等效扰动。这个过程可以从Scope波形里直观看到z3一路追踪着真实扰动控制律再把它抵消掉。看到这个波形的瞬间我对ADRC的好感度直接拉满。3.4 用脚本搭一套快速验证的ESO有时候我也不想每次都在Simulink里拖动鼠标调参尤其是要批量测试不同b0、ω0组合时写脚本比GUI操作高效得多。下面这段Matlab代码是我常用的一个极简验证环境不用任何工具箱新手上手也不难。% 一个极简的ADRCESO仿真示例二阶对象 % 可以在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; % 外部扰动在 t3s 后加入 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从“论文上的公式”变成“手上真正能用的工具”。