资讯详情 ADRC核心原理与工程落地:扩张状态观测器与参数整定全解析
📅 2026/10/22 15:13:18
简介面向控制理论与MATLAB/Simulink仿真学习者这是一份自抗扰控制ADRC应用于二阶系统的仿真资源包聚焦扩张状态观测器ESO设计与Bode图频率特性分析可用于理解ADRC对内部扰动和外部扰动的实时估计与补偿机制。压缩包共5个文件以.m脚本作为LESO阶跃响应和Bode图绘制工具配合.mdl与.slx模型搭建二阶被控对象、ADRC控制环及观测器结构整体仅41KB轻量易用。目前已获得809人次学习/浏览。资源中既有基于脚本的状态观测器计算与频率响应分析代码也有对应不同Simulink版本的模型文件可帮助学习者完整走通二阶系统建模、ESO状态方程设定、控制器参数整定与幅值裕度/相位裕度评估流程直观观察扰动抑制效果。尤其适合课程设计、毕业设计以及需要快速上手ADRC算法的工程实践者参考。1. ADRC不简单是PID的替代品它把扰动当状态来观测做运动控制或者伺服驱动的人大概率都遇到过这种局面用PID调一个二阶特性明显的对象超调压下去了抗扰变差了抗扰提上来了系统又开始抖。调来调去本质上是在跟“模型误差加外部扰动”这个看不见的东西搏斗。ADRC自抗扰控制的思路完全不一样——它不跟扰动硬拼而是把“所有说不清楚的部分”合并成一个总扰动再用一个扩张状态观测器ESO把它实时估出来然后在控制量里直接抵消掉。这篇文章会把二阶ADRC从数学结构、仿真代码到参数整定和嵌入式落地讲透适合正在做电机、小车、机械臂或者控制仿真课题的从业者。你能得到一份能直接跑通的最小Python仿真以及一套不用靠运气的调参顺序。2. 二阶ADRC的结构拆解三阶ESO为什么是核心2.1 二阶对象为什么需要三阶ESO先统一描述对象。绝大多数你能碰到的二阶被控对象可以写成下面这种形式y f(y, y, w, t) b * u这里的f就是“总扰动”它既包含模型里的非线性项、参数偏差、状态耦合也包含外部负载扰动。关键在于f里面通常含有y和y也就是说它跟系统状态是耦合的你不能把它当单纯的外部干扰去前馈抵消。ADRC的做法是把这个f当作一个新的状态变量x3于是原来的二阶系统变成了一个三阶系统x1 x2 x2 x3 b0 * u x3 f f的变化率未知但可以被观测器估计这就是“ADRC二阶”的标准含义被控对象是二阶扩张出第三个状态去观测总扰动。所以我见到有人问“二阶ADRC是不是ESO也是二阶”答案是对象二阶ESO至少三阶。ESO的作用就是根据y和u这两个可测/已知信号把x1、x2、x3三个状态全部估计出来。z1跟踪位置z2跟踪速度z3跟踪总扰动f。2.2 跟踪微分器是怎么解决超调问题的PID面对阶跃给定时常有超调根源之一是初始误差太大积分和比例项还没反应过来控制量已经饱和。ADRC里专门放了一个跟踪微分器TD它不直接参与反馈而是把阶跃参考r“软化”成一条平滑的轨迹v1同时给出v1的微分v2。最常用的是韩式最简TD离散形式如下fh -r * sat((v1 - ref) v2*|v2| / (2*r), delta) v1 dt * v2 v2 dt * fh这里的r是跟踪速度参数r越大v1逼近参考越快但过渡过程的“加速度”也越大delta是线性区间避免符号函数在原点附近造成抖振。TD的存在让误差反馈看到的不是“目标变化了1.0”这种突变而是一段有斜率、有加速度的曲线。注意TD只影响给定通道不影响扰动通道所以它不会牺牲抗扰性能。我一般会提醒别人不要觉得TD是可有可无的在参考频繁变化或者执行器容易饱和的场合TD是ADRC不发疯的保障。如果你把TD去掉直接用z1和r相减仿真可能还能跑但实物的超调和执行器磨损会让你很快后悔。2.3 误差反馈与控制律三个参数如何合流ESO把状态估出来以后控制律仍然是一个类似PD的结构只不过用的不是测量值而是观测值e1 z1 - v1 e2 z2 - v2 u0 kp * e1 kd * e2 u (u0 - z3) / b0kp和kd就是控制器带宽ωc的映射kp ωc²kd 2ωc。最后一步是关键z3是ESO估计出的总扰动把它除以b0后直接减掉等于在控制量里做了一个实时补偿。只要z3估得准f再大也不会明显影响输出。到这里你会发现整个二阶ADRC真正要用户给的参数只有四个b0控制增益估计、ωo观测器带宽、ωc控制器带宽、rTD速度。其中r只是过渡过程的“整形参数”不碰稳定性边界。这种参数数量对工程太友好了这也是ADRC能在嵌入式上落地的原因。3. ADRC仿真从零跑通最小Python代码与ADRC图怎么看3.1 仿真对象设定带内扰外扰的二阶系统为了让仿真贴近现实我不会用一个干净的线性对象。下面这个二阶系统自带状态耦合和外部阶跃扰动y -25*y - 10*y 5*u d(t)其中-25y和-10y是对象自身的动态可以理解为机械刚度项和阻尼项d(t)是外部扰动我设在4秒时突加一个3.0的阶跃负载。注意ADRC完全不知道-25和-10这两个系数的存在它只知道自己给了一个u、看到了一个y。这种“假装不知道模型”的仿真才有说服力。系统的开环自然频率约5 rad/s、阻尼比1.0是一个响应偏慢的典型二阶对象。PID要压超调就得砍增益砍完抗扰就垮这正是ADRC发挥优势的场景。3.2 完整Python仿真代码与逐段说明import numpy as np import matplotlib.pyplot as plt # 仿真步长与时长 dt 0.001 # 步长1ms接近嵌入式控制周期 T 8.0 # 总时长8秒4秒加扰动 n int(T / dt) t np.arange(n) * dt # ---- ADRC参数 ---- b0 5.0 # 控制增益估计对象真实增益b5 wo 30.0 # 观测器带宽 rad/s wc 8.0 # 控制器带宽 rad/s r 5.0 # TD过渡过程速度 delta 0.01 # sat函数线性区间 # 带宽法配置观测器与控制器增益 beta1 3.0 * wo beta2 3.0 * wo**2 beta3 wo**3 kp wc**2 kd 2.0 * wc # 状态初始化 y, yd 0.0, 0.0 # 对象位置、速度 v1, v2 0.0, 0.0 # TD输出平滑参考、参考速度 z1, z2, z3 0.0, 0.0, 0.0 # ESO位置估计、速度估计、总扰动估计 u 0.0 # 记录数据 y_hist np.zeros(n) u_hist np.zeros(n) z3_hist np.zeros(n) f_hist np.zeros(n) ref 1.0 # 阶跃参考 def sat(v, d): if v d: return 1.0 if v -d: return -1.0 return v / d def dist(tt): return 3.0 if tt 4.0 else 0.0 for i in range(n): tt t[i] d dist(tt) # 1) TD安排过渡过程 fh -r * sat((v1 - ref) v2 * abs(v2) / (2.0 * r), delta) v1 dt * v2 v2 dt * fh # 2) ESO用上一时刻的u和当前y更新状态估计 e z1 - y z1 dt * (z2 - beta1 * e) z2 dt * (z3 - beta2 * e b0 * u) z3 dt * (-beta3 * e) # 3) 线性误差反馈 扰动补偿 u0 kp * (z1 - v1) kd * (z2 - v2) u (u0 - z3) / b0 u np.clip(u, -30.0, 30.0) # 执行器限幅 # 4) 二阶对象欧拉积分f是总扰动 f -25.0 * y - 10.0 * yd d ydd f 5.0 * u y dt * yd yd dt * ydd y_hist[i] y u_hist[i] u z3_hist[i] z3 f_hist[i] f # 画图 fig, axes plt.subplots(3, 1, figsize(9, 10)) axes[0].plot(t, y_hist, labely) axes[0].plot(t, np.ones(n), --, labelref) axes[0].set_ylabel(output) axes[0].legend() axes[0].grid(True) axes[1].plot(t, u_hist, labelu) axes[1].set_ylabel(control) axes[1].legend() axes[1].grid(True) axes[2].plot(t, z3_hist, labelz3 (ESO)) axes[2].plot(t, f_hist, --, labeltrue f) axes[2].set_xlabel(time (s)) axes[2].set_ylabel(disturbance) axes[2].legend() axes[2].grid(True) plt.tight_layout() plt.show() print(稳态输出:, y_hist[-1], z3估计:, z3_hist[-1])代码里最需要注意的顺序关系ESO更新用的是“上一时刻的u”然后控制律再用“刚更新的z3”算出当前u最后对象才积分。这对应离散系统里的一个自然时序延迟实际嵌入式代码也是这样写的。如果你把ESO更新放到u计算之后等价于同一拍内用新u算ESO会出现代数环问题仿真会偏乐观。参数上ωo30对应观测器极点大约在30 rad/s采样率1kHz下3ωo/fs0.09处于前向欧拉的稳定范围内。ωc8让闭环响应比开环快但又不至于逼出噪声。TD的r5让参考过渡大约在0.6到0.8秒内完成输出不会对着阶跃猛冲。3.3 从曲线和观测器状态里看ADRC图的行为跑完代码后看三张图这是理解ADRC的关键。第一张是输出和参考前4秒你应该看到y以平滑的S形曲线逼近1.0超调几乎为零这就是TD过渡过程的作用。第二张是控制量u它在启动阶段会先冲上去再收回来4秒扰动加入后u会立刻跳高约3.0/5.00.6来对抗负载这个跳变不是来自误差反馈而是来自z3的补偿。第三张图最能说明问题z3实线和真实总扰动f虚线在扰动发生后的几百毫秒内重合说明ESO成功地把“-25y-10yd”这一整坨东西全部观测出来了。在真实的工程里ADRC图核心就是四条信号通路参考进TD、u和y进ESO、ESO的状态进误差反馈、z3反向补偿到u端口。你可以把这四条通路画成一张框图贴在自己工位前调参时对照着看能少走很多弯路。观测器估计的z3常常让人感觉像个黑匣子但其实每一路状态都可以拉出来对比这是ADRC排查问题比PID友好的地方。3.4 画一张ADRC图信号流与对应关系模块输入输出代码对应TD参考refv1平滑参考、v2参考速度v1/v2两行递推ESO控制量u、测量yz1位置估计、z2速度估计、z3扰动估计z1/z2/z3三行递推误差反馈z1-v1, z2-v2u0u0 kpe1 kde2扰动补偿u0, z3最终控制量uu (u0 - z3)/b0这张表就是你调试时对照的“ADRC图”。无论你后面换用C、C还是别的语言结构一模一样变的只是差分方程的写法。我习惯先在仿真里把这张图的每一路信号都打印出来确认z3跟f对得上再去碰实物参数这个习惯能省掉至少一个星期的现场调试时间。4. ADRC参数整定b0、ωo、ωc的调整顺序和典型范围4.1 b0先定量级不要求精确b0是ADRC里唯一涉及对象信息的参数但它不需要精确等于真实b。真实b5你设b04或者6系统都能稳住差别只是抗扰补偿的力度和相位。b0偏大会怎样补偿过头控制量里出现“多余的反向分量”系统会有一点减幅振荡b0偏小呢扰动来了之后输出会先跌下去一截再慢慢回来抗扰看起来“肉肉的”。工程上估算b0的办法很直接对象开环手动给一个小的阶跃u0等速率稳定后测加速度增量b0近似等于Δy/Δu。没有加速度计就用编码器差分两次凑合能用。如果连这个都不想测就按执行器增益折算一个量级然后仿真里上下浮动30%看鲁棒性。b0标定这块经常被说成“玄学”其实它是唯一值得你在现场花时间测准的参数。现象大概率是b0问题控制量高频轻微振荡减b0后变好b0偏大扰动跌落深、恢复慢加b0后改善b0偏小无论怎么调ωc都振且振频较低优先怀疑b0偏大4.2 ωo和ωc观测器带宽与控制器带宽的关系ωo决定ESO的估计速度ωc决定闭环跟踪速度。两者不是独立选的经验关系是ωo取ωc的3到10倍。ωo太小z3跟不上真实扰动补偿滞后ωo太大测量噪声被放大控制量开始抖。更硬的约束来自采样率对前向欧拉离散ωo最好不超过0.1倍的采样频率也就是1kHz采样下ωo别超过100 rad/s我一般控制在30到50。ωc则受执行器物理极限约束。你把ωc提上去小误差会被更强地放大执行器很快进入饱和。所以正确的认知是ωc不是越大越好它应该略快于你能接受的过渡时间同时留出执行器裕量。调ωc时盯着阶跃响应看等出现第一个小超调就退回去20%这个位置通常就是当前硬件条件下的甜点。4.3 调参顺序和典型范围我自己的调试顺序是固定死的照着做基本不会翻车先定采样步长dt仿真里能多小就多小至少比嵌入式目标周期小一个量级。开环测出b0的量级填进代码不动它。给一组保守初值ωc5ωo25r3先跑纯跟踪确认曲线S形上升、无振荡。逐步提高ωc每次增加30%到50%出现超调就退回。加阶跃扰动逐步提高ωo观察跌落深度和恢复时间。最后调r和delta把超调和启动控制量峰值修到满意。这套顺序是血泪经验换来的核心原则是“先定b0再升ωc最后升ωo”。反过来先动ωo你会发现系统波形变化不明显因为ESO估计快了但控制器带宽不够补偿根本送不进去白调。一张参数范围参考表覆盖大多数运动控制场景采样1kHz参数典型范围单位/说明b00.5~2倍真实b先估算后微调ωc3~20rad/s受执行器限制ωo(3~10)ωcrad/s不超过0.1fsr2~10越大过渡越快启动冲击越大delta0.01~0.1过小易抖振过大则TD变慢5. ADRC仿真与落地的五个坑发散、抖振和残差的排查5.1 仿真直接NaN步长、带宽和欧拉法的稳定域现象跑仿真没几步y直接变成NaN控制量爆到10的几十次方。原因最常见的组合是dt太大、ωo太高。前向欧拉对三阶ESO有隐式稳定域约束3ωo乘上dt接近1的时候观测器递推就会发散。我做模拟项目X时把ωo从30改成120、dt还是1ms三秒内必炸。解决先缩小dt到0.2ms甚至0.1ms验证算法本身没写错然后把ωo压回0.1fs以内。如果你必须用大步长就把对象和ESO都改成RK4积分但嵌入式上一般不值得降低ωo更简单。5.2 控制量高频抖振观测器噪声的放大器现象仿真里加了测量噪声后控制量出现密集的毛刺幅值不大但频率很高实物上表现为电机滋滋响。原因ωo抬高以后ESO把测量噪声当成了真实状态变化z2和z3里混入高频分量再除以b0送进控制量。b0越小这种放大越狠。解决第一步是给测量信号加一阶低通截止频率取ωo的五分之一别去动观测器结构第二步是把ωo降回合理范围第三步是别为了追求“扰动零跌落”而把b0压得过小。这个坑在实物上特别容易跟机械共振混在一起排查时先断开控制回路看噪声底再决定动哪里。5.3 跟踪滞后和稳态残差别急着改控制器现象输出能到目标但慢半拍或者到了之后还留着0.05的稳态误差。原因跟踪滞后先分清位置——如果v1本身就跟不上ref那是TD的r太小和控制器无关如果v1跟上了而y跟不上才是ωc不够。稳态残差则九成是b0和真实b有偏差ESO把“静态补偿误差”也当成扰动的一部分但补偿增益不对留下残差。解决按上面顺序排查先调r再看ωc最后回头标定b0。千万不要一看到残差就往控制器里塞积分项那会把ADRC的相位裕量吃掉系统迟早振荡。我吃过这个亏加完积分项第二天就翻车了。5.4 仿真好但实物翻车执行器饱和与未建模动态现象仿真里波形完美一上实物就低频振荡、执行器发烫甚至带载后直接啸叫。原因仿真没建模执行器饱和和未建模高频模态。ADRC的补偿逻辑是“扰动多大我反向给多大”如果执行器已经饱和这个反向给不出去ESO还在按线性模型往深处积分就会造成类似积分饱和的过冲。而高频未建模模态一旦落在ωo附近观测器会把它当成扰动去抵消反而把模态激励起来。解决仿真里务必加限幅和测量噪声再加一阶未建模动态看看鲁棒性。更实用的一招是给ESO加“饱和感知”执行器限幅时暂停z3的积分更新等退出饱和再恢复。别问为什么问就是现场被这问题教育过。5.5 观测器输出和实际扰动对不上现象把z3和真实f画一起发现扰动加入后z3要过很久才追上或者一直追不上。原因排除参数问题后多半是你的“真实f”算得不全。比如对象里有执行器死区、摩擦项这些都在f里而你在仿真里只画了负载扰动d那当然对不上。解决对比时把对象方程里除b*u以外的所有项加在一起作为真实f包括死区、摩擦、状态耦合。z3跟这个“全扰动”对上了才说明ESO正常工作。这个习惯能避免你拿着错误的对比图去调一个本来就没问题的观测器。6. 把ADRC搬上MCU固定步长与抗饱和的最后一公里仿真跑通只是第一步落到MCU上要走完最后三件事固定步长、抗饱和、逐级验证。固定步长是第一原则。ADRC的差分方程对dt敏感中断里绝不能做变步长。我习惯在1kHz定时中断里调用一个ADRC更新函数所有状态打包成结构体typedef struct { float v1, v2; // TD状态 float z1, z2; // ESO状态 float z3; // 扩张状态总扰动估计 } adrc_state; void adrc_step(adrc_state *s, float ref, float y, float b0, float beta1, float beta2, float beta3, float kp, float kd, float r, float delta, float u_max, float *u_out) { float fh, e, u0, u_sat; // TD fh -r * satf((s-v1 - ref) s-v2*fabsf(s-v2)/(2.0f*r), delta); s-v1 DT * s-v2; s-v2 DT * fh; // ESO e s-z1 - y; s-z1 DT * (s-z2 - beta1*e); s-z2 DT * (s-z3 - beta2*e b0*(*u_out)); s-z3 DT * (-beta3*e); // 误差反馈与补偿 u0 kp*(s-z1 - s-v1) kd*(s-z2 - s-v2); u_sat clampf((u0 - s-z3)/b0, -u_max, u_max); // 抗饱和限幅时停止z3积分 if (u_sat ! (u0 - s-z3)/b0) { s-z3 - DT * (-beta3*e); // 回退本拍z3更新 } *u_out u_sat; }这段代码里的抗饱和处理是“回退本拍z3更新”简单有效。你如果不想回退也可以用条件判断跳过z3的积分效果接近。关键认知是饱和状态下ESO还按无约束模型积分z3会越积越大退出饱和时给你一个巨大的反向控制量这就是实物过冲的来源。最后验证有一招很实用先在MCU里同时跑ADRC和一个虚拟对象模型也就是硬件在环确认固定步长下波形跟Python仿真一致再接真实执行器。我现在的习惯是任何控制代码都先过这一道虚拟对象测试再上真实负载这算是我从几次翻车里保下来的习惯。ADRC参数不多但每一步都值得较真希望帮到你。本文还有配套的精品资源点击获取