2026/10/22 13:35:02

从PID调参困境到ADRC自抗扰:核心原理、代码实现与工程避坑

从PID调参困境到ADRC自抗扰:核心原理、代码实现与工程避坑 简介传统比例积分微分控制在面对强扰动、模型不确定性和非线性对象时往往难以同时兼顾快速性与稳定性该学习资源围绕自抗扰控制、跟踪微分器和扩张状态观测器三条主线提供了一套完整的MATLAB脚本与Simulink仿真模型实现帮助自动化、机器人及航空航天控制方向的初学者系统理解先进PID的改进思路。压缩包共含十八个文件主体为十一个m脚本、两个slx仿真模型及一个Markdown说明文档另有少量备份文件整体大小仅六十八KB便于快速下载与本地实验。脚本覆盖自抗扰控制、线性自抗扰控制、跟踪微分器、扩张状态观测器、比例积分微分控制器以及差分进化、神经网络等辅助算法仿真模型则构建了伺服关节与永磁同步发电机场景。通过对照传统比例积分微分控制与改进算法的响应曲线读者能够直观掌握扰动估计与补偿的机理并在此基础上进行参数调试和二次开发。目前已有九十二人学习下载适合作为课程设计或科研入门的基础代码库。1. 先进PID控制算法当PID调参变成玄学ADRC凭什么接棒现场调试自动化设备时PID调参经常被当成一门玄学给定一大就超调微分一大噪声跟着放大外部扰动一来好不容易调出来的参数又要推翻重来。先进PID控制算法里的自抗扰控制ADRC就是为了打破这个循环出现的——它保留PID的误差反馈思想用跟踪微分器TD解决参考信号微分提取的难题用扩张状态观测器ESO把内扰、外扰、未建模动态打包成总扰动实时估计并补偿掉。适合谁做伺服、运动控制、温控、电源、飞行控制的工程师尤其是被模型不确定性和外部扰动反复折磨的现场调试人员。下面我从TD、ESO讲到完整闭环代码最后收在现场调参踩过的坑上。2. 跟踪微分器(TD)把参考信号和它的微分一起干净地提取出来2.1 为什么PID的微分项总被噪声绑架参考信号求导为什么难PID里的D环节理论上要的是误差的导数但实际工程里几乎没人敢直接对误差做差分。两个原因一是参考信号通常是阶跃或者梯形切换瞬间误差导数是一个无限大的脉冲差分出来的D输出直接打满执行机构二是测量噪声高频分量在差分运算里被大幅放大原本为了抑制超调加的D最后变成把抖动灌进控制量的元凶。所以很多现场干脆把D设成0用PI硬扛代价是超调和振荡只能靠手动折中。跟踪微分器的思路和低通滤波再差分完全不同。它构造一个二阶动态系统让它的状态 x1 去跟踪参考信号 v状态 x2 自然就是 x1 的导数也就是 v 的广义微分。关键在跟踪两个字x1 是被安排的过渡过程拉过去的不是直接跳过去的所以 x2 不会出现阶跃对应的无穷大脉冲同时这个跟踪过程本身有滤波作用x2 对高频噪声的响应远没有直接差分那么敏感。这样做还有一个附带收益——安排过渡过程起步时控制量不会瞬间顶满对执行机构和机械冲击都友好得多。很多工程师试过用一阶惯性滤波对参考求导发现滞后大、噪声还是压不住。区别在于一阶惯性滤波只对幅值衰减不改变突变输入必然对应尖峰输出的本质而TD是在加速度受限的约束下做最速跟踪它输出的微分天然被限幅噪声响应也被结构化地压下去。这也是为什么ADRC里TD既承担微分提取又承担参考整形两个任务一个模块就做完了。2.2 用fhan实现跟踪微分器最速控制综合函数的离散代码经典非线性TD的离散形式核心是一个叫fhan的函数它解决的是二阶积分器在控制量受限时如何最快到达平衡点的问题。这里最快是数学上的最速控制不是调参调出来的快。fhan的输出是加速度指令幅度被限制在 r 以内r 就是跟踪速度上限。import math def fhan(x1, x2, r, h): # x1: 位置误差v1 - ref # x2: 速度 # r: 跟踪速度上限加速度约束 # h: 采样周期 d r * h * h a0 h * x2 y x1 a0 a1 math.sqrt(d * d 8.0 * r * abs(y)) if abs(y) d: a2 x2 a0 / h else: a2 x2 0.5 * (a1 - d) * math.copysign(1.0, y) if abs(a2) d: return -r * a2 / d return -r * math.copysign(1.0, a2)这个函数看起来绕其实它是在算一条切换边界位置误差 y 在边界内还是边界外决定用线性段还是饱和段输出。d r·h² 是边界宽度a1 是考虑目标可达性后的修正项a2 是虚构的控制量最后根据 a2 落在边界内还是边界外决定返回值是线性比例还是直接拉满。参数 r 决定了加速度上限h 必须是实际运行周期。有了fhanTD本身就是一个两步的欧拉积分def update_td(v1, v2, ref, r, h): # v1: 跟踪信号, v2: 跟踪信号的微分, ref: 参考目标 fh fhan(v1 - ref, v2, r, h) v1_new v1 h * v2 v2_new v2 h * fh return v1_new, v2_new注意传入fhan的第一个参数是 v1 - ref不是 v1。这样做的意义是让 v1 去追 refv2 作为中间速度自然承接下来。很多初写TD的人把符号搞反结果 v1 一路追飞。代码逻辑不复杂先用当前的误差和速度算出加速度 fh再按欧拉法推进两个状态。x1即v1跟踪参考信号x2即v2跟踪参考信号的变化率这两个量后面会直接喂给控制律。2.3 r和h怎么设跟踪速度、相位滞后与噪声放大的取舍r 是最容易误导人的参数。它代表跟踪的加速度上限r 越大 v1 逼近 ref 越快v2 的峰值也越高参考信号里携带的量化噪声和测量毛刺会被 v2 成比例放大再进入控制律的 e2 项。所以 r 的取值原则不是想多快就设多大而是按执行机构能承受的能力来定。运动控制里r 大致对应最大加速度取设备标称加速度的 70%~80% 比较安全温度控制这类执行机构功率有限的场合r 通常取很小让 v1 慢速爬升避免加热棒一上电就全功率冲击。给一个工程近似对幅值为 ref 的阶跃TD的过渡时间大约在 4·sqrt(ref/r) 量级。比如 ref1、r8 时过渡时间约1.4秒。想验证这个公式对不对跑一次仿真看 v1 的上升沿就行。h 必须等于实际控制周期不能拿仿真步长凑数。h 一旦和实际中断周期不一致fhan 里 d r·h² 的边界就是错的现象是跟踪要么明显变慢要么在目标附近小幅振荡。另外TD不仅用在参考侧也常用在反馈侧把编码器位置送入TD取速度比直接对位置差分干净得多这是很多人忽略的用法。3. 扩张状态观测器(ESO)把总扰动当第三个状态实时估出来3.1 总扰动到底是什么内扰、外扰、未建模动态统一进一个变量先看一个二阶对象的一般写法y f(y, y, w, t) b0 * u。这里的 u 是控制量b0 是输入系数f 就是总扰动——它把摩擦力、阻尼变化、未建模非线性、外部负载、参数漂移全部打包成一个标量。ADRC的核心假设是我不需要知道 f 长什么样我只需要一个状态把它实时估出来然后在控制量里把它抵消掉。这相当于把整段不确定部分当成一个黑匣子只关心它的总效果不关心内部结构。为什么敢这么干因为对反馈控制来说系统内部的耦合和非线性最终都体现在输出加速度上外部扰动也一样。只要能估计出这个等效加速度扰动控制律就能补回去。这也是ADRC对模型精度要求低的原因它不需要精确建模只需要一个大致准确的 b0。b0 是输入增益的数量级现场标定方法很简单——开环给一个已知控制量阶跃测稳态加速度变化b0 ≈ Δy/Δu。运动控制里用电流指令和加速度计就能标温控里用功率和温度变化率也能标。b0 不必很准但不能差一个数量级否则补偿方向对不上后面会讲到这会引发什么毛病。3.2 非线性ESO离散方程与fal函数三个状态各在干什么ESO比对象多一个状态这就是扩张二字的来历。拿3.1的二阶对象说ESO有三个状态z1 估计 yz2 估计 yz3 专门估计总扰动 f。离散递推如下def update_eso(z1, z2, z3, y, u, h, wo, b0, delta): # z1/z2/z3: 观测器三个状态 # y: 测量输出, u: 上一拍实际施加的控制量 # wo: 观测器带宽, b0: 对象输入系数估计, delta: fal线性段宽度 e z1 - y beta1 3.0 * wo beta2 3.0 * wo * wo beta3 wo * wo * wo z1 z1 h * (z2 - beta1 * e) z2 z2 h * (z3 - beta2 * fal(e, 0.5, delta) b0 * u) z3 z3 h * (-beta3 * fal(e, 0.25, delta)) return z1, z2, z3 def fal(e, alpha, delta): # 非线性函数大误差快追小误差细调 if abs(e) delta: return e / (delta ** (1.0 - alpha)) return math.copysign(abs(e) ** alpha, e)逐项看第一行是误差z1 和测量 y 的差z1 的更新用线性误差这是普通状态观测器的标准形式z2 的更新里出现了 b0·u这是把已知的控制输入注入进观测器否则 z3 永远估不对z3 的更新只用误差驱动没有输入项靠的就是扰动会在输出上留下痕迹这个原理误差里天然携带扰动信息观测器沿着误差方向把 z3 逼到真实总扰动上。两个 fal 的 alpha 分别取 0.5 和 0.25这是经典非线性ESO的取值。它的作用是误差大的时候增益高收敛快误差小的时候增益低抗噪好。但注意 fal 在零点附近有线性段由 delta 决定目的就是防止 e 接近0时增益过大导致抖振。delta 一般取测量噪声幅度的2~3倍常见范围0.005~0.05具体怎么权衡第5章细说。3.3 观测器带宽wo3个增益一次定完的参数化方法给非线性ESO配参数最怕拍脑袋。如果三个 beta 一个一个试很难试出稳定组合。工程上通行做法是带宽参数化把 beta1、beta2、beta3 按表里这样绑定只留 wo 一个自由度。这样ESO的特征方程近似为 (swo)³wo 直接决定三个状态收敛的带宽物理意义非常清楚。参数表达式作用现场初值beta13·woz1 的误差反馈增益wo 取 wc 的 3~5 倍beta23·wo²z2 的误差反馈增益同上beta3wo³z3 的误差反馈增益从 3 倍 wc 往上调wo观测器带宽估计收敛速度以控制量不抖为上限wo 越大z1、z2、z3 收敛越快对扰动的响应越迅速但测量噪声也被放大得越狠最终体现在控制量毛刺上。wo 取控制器带宽 wc 的3~5倍是跟得上又不至于把噪声灌满的均衡点。还有一条硬约束wo·h 必须明显小于1经验上别超过0.3~0.5这关系到离散化之后ESO是否还能稳定第5章会专门讲。另外提一句如果现场噪声实在压不住把 fal 替换成纯线性 e整个ESO就退化成线性扩张状态观测器这就是LADRC的雏形第6章展开。4. 从方程到可运行代码自抗扰控制器完整闭环仿真4.1 控制律合成PD组合加扰动补偿u(u0-z3)/b0的物理含义TD和ESO都就位后控制律反而很简单。先算一个中间量 u0形式上和PD一样u0 kp·(v1 - z1) kd·(v2 - z2)。注意这里用的是TD的 v1、v2 和ESO的 z1、z2不是直接用测量反馈和它的差分。kp、kd 也可以用带宽法绑定kp wc²kd 2·wc这样闭环特性近似一个带宽为 wc 的二阶系统还是只有一个参数。得到 u0 之后真正的控制量是u (u0 - z3) / b0。这句话是整个ADRC的精华z3 是总扰动估计把它除以 b0就是抵消扰动需要额外补多少控制量减掉它之后对象在控制器眼里变成了一个干净的纯双积分系统 y ≈ u0。这也是为什么上面PD那一串可以放心地按 wc 设计——它面对的不是那个又带摩擦又带负载的对象而是一个被人工掰直的对象。只要ESO收敛z3 跟得上真实总扰动这个补偿就一直有效这就是自抗扰三个字的落地形式。4.2 最小Python闭环仿真TD、ESO、控制量一次跑通下面是一份可以直接复制运行的完整仿真。被控对象我故意放了一个带阻尼、带弹簧项、还叠加正弦扰动的二阶系统模拟模型大概知道但又不完全准确的现场情况。外部扰动在第3秒注入一个0.5的阶跃用来观察ESO的补偿效果。import math h 0.001 # 控制周期/仿真步长单位秒 T 6.0 # 仿真总时长 N int(T / h) # 控制器参数整定思路见3.3和第5章 r 8.0 # TD跟踪速度 wc 6.0 # 控制器带宽 wo 30.0 # 观测器带宽一般取3~5倍wc b0 1.0 # 对象输入系数估计值 delta 0.01 # fal线性段宽度 def fal(e, alpha, d): if abs(e) d: return e / (d ** (1.0 - alpha)) return math.copysign(abs(e) ** alpha, e) def fhan(x1, x2, r, h): d r * h * h a0 h * x2 y x1 a0 a1 math.sqrt(d * d 8.0 * r * abs(y)) if abs(y) d: a2 x2 a0 / h else: a2 x2 0.5 * (a1 - d) * math.copysign(1.0, y) if abs(a2) d: return -r * a2 / d return -r * math.copysign(1.0, a2) def plant(x1, x2, u, t): # 对象: y -1.8*y - 0.9*y u 0.4*sin(3t) 外部扰动 d_ext 0.5 if t 3.0 else 0.0 return -1.8 * x2 - 0.9 * x1 u 0.4 * math.sin(3.0 * t) d_ext # 状态初始化 x1, x2 0.0, 0.0 # 对象真实状态 z1, z2, z3 0.0, 0.0, 0.0 # ESO三个状态 v1, v2 0.0, 0.0 # TD输出 u 0.0 ref 1.0 rec {t: [], y: [], z3: [], u: []} for i in range(N): t i * h # 1) 先测量再用ESO估计顺序和实机一致 e_obs z1 - x1 z1 z1 h * (z2 - 3.0 * wo * e_obs) z2 z2 h * (z3 - 3.0 * wo * wo * fal(e_obs, 0.5, delta) b0 * u) z3 z3 h * (-wo * wo * wo * fal(e_obs, 0.25, delta)) # 2) TD跟踪参考并提取微分 fh fhan(v1 - ref, v2, r, h) v1 v1 h * v2 v2 v2 h * fh # 3) 控制律PD组合 扰动补偿 e1 v1 - z1 e2 v2 - z2 u0 wc * wc * e1 2.0 * wc * e2 u (u0 - z3) / b0 # 4) 推进对象欧拉法 ddx plant(x1, x2, u, t) x1 x1 h * x2 x2 x2 h * ddx if i % 20 0: rec[t].append(t) rec[y].append(x1) rec[z3].append(z3) rec[u].append(u)跑完把 rec 里的 t 和 y 画出来会看到三条值得关注的曲线。输出 y 约在1.5秒到达 ref1.0超调很小这是TD安排了过渡过程的结果z3 在0~3秒之间稳定在对象内部扰动附近反映的是阻尼项、弹簧项和正弦扰动合并后的总扰动实时值第3秒外部阶跃注入时z3 在0.3秒内跟上新扰动y 的偏差被压在0.1以内这就是扰动补偿在工作。想验证ADRC的鲁棒性最粗暴的办法是改对象把 plant 换成y -5y - 3y 1.5u sin(20t)b0 保持1.0不动你会发现系统依然稳得住只是动态响应略变。这套代码我建议保留下来当测试台以后遇到新对象先改 plant 看ESO和补偿行为再上实机比直接在设备上试错安全得多。4.3 嵌入式C实现定时中断里的代码与输出限幅抗饱和仿真通了落地大多是C代码。下面是定时中断里跑的核心逻辑控制周期由中断保证等于 H/* 定时中断里每 H 秒调用一次H0.001s */ #define WC 6.0f #define WO 30.0f #define B0 1.0f #define DELTA 0.01f #define R 8.0f #define H 0.001f #define U_MAX 10.0f /* 执行机构输出上限 */ #define U_MIN -10.0f static float z1, z2, z3, v1, v2; static float u_out; static float fal(float e, float alpha, float d) { if (e -d e d) return e / powf(d, 1.0f - alpha); return copysignf(powf(fabsf(e), alpha), e); } float adrc_run(float y_meas, float ref) { float e_obs, fh, e1, e2, u0, tmp; /* ESO用上一拍实际输出的 u_out */ e_obs z1 - y_meas; z1 H * (z2 - 3.0f * WO * e_obs); z2 H * (z3 - 3.0f * WO * WO * fal(e_obs, 0.5f, DELTA) B0 * u_out); z3 H * (-WO * WO * WO * fal(e_obs, 0.25f, DELTA)); /* TD */ fh fhan(v1 - ref, v2, R, H); v1 H * v2; v2 H * fh; /* 控制率 */ e1 v1 - z1; e2 v2 - z2; u0 WC * WC * e1 2.0f * WC * e2; tmp (u0 - z3) / B0; /* 限幅并把实际输出回馈给ESO这是抗饱和的关键 */ if (tmp U_MAX) u_out U_MAX; else if (tmp U_MIN) u_out U_MIN; else u_out tmp; return u_out; }这段代码里最容易被忽略的是最后那个限幅。如果只做限幅而不把限幅后的值回写给ESO观测器会一直按未受限的控制量积分z3 越积越大等扰动过去或给定变化时控制量会猛冲一下才恢复这就是积分饱和现象很像过冲特别大。回写 u_out 之后ESO 知道真实执行情况z3 不会被虚高的补偿量带偏。现场调试新板子时还要确认中断周期和 H 一致我见过不止一次仿真步长0.001、实机中断2ms但代码里H还是0.001的情况结果就是系统莫名振荡。5. ADRC参数整定避坑5个现场最常见故障与排查下面这5条是我在多个项目里反复踩过的坑每条都按现象、原因、解决讲清楚。碰上问题先对照现象定位别一上来就动所有参数。5.1 现象ESO高频抖动控制量毛刺多现象运行后 z2、z3 有高频小幅度振荡控制量 u 上叠加明显毛刺执行机构有嗡鸣声。原因wo 相对采样噪声取得太高或者 delta 太小fal 在零点附近的增益过大把测量噪声和量化误差放大成了估计振荡。很多人一上来就把 wo 拉到很大觉得观测器越快越好结果观测器把噪声全接进来了。解决先把 wo 压到 wc 的3~4倍看抖动是否收敛再把 delta 提到测量噪声幅度的2~3倍常见取0.01~0.05。还不行就在反馈路径上给 y 加一阶低通截止频率放到5倍 wc 以上别吃掉太多相位。这三步按顺序试大多数毛刺问题都能压住。5.2 现象b0估不准系统要么发散要么迟钝现象同样的 wc、wo换一台设备就不稳定或者扰动来了半天补不回去阶跃响应慢得离谱。原因b0 直接参与除法补偿。b0 偏大补偿量偏小系统对扰动反应迟钝b0 偏小补偿过量闭环等效增益被放大轻则振荡重则发散。b0 的误差不会被ESO完全吸收因为ESO估的只是对象里的 fb0 不准等于把一部分模型误差也塞进了总扰动它补不干净。解决先做开环标定。把扰动补偿禁用u 直接给阶跃记录输出的加速度变化b0 ≈ Δy/Δu。实机标完别用理论值留20%左右裕度现场通常取偏大一点更安全偏大只是迟钝偏小会振荡。换设备、换负载工况后 b0 要重新确认这是ADRC现场必做的一步。5.3 现象fal的delta太大抗扰能力明显退化现象整定正常但外部扰动一加入输出偏差比预期大很多z3 明显跟不上扰动。原因fal 的 delta 设得太大运行范围内 |e| 始终落在线性段fal(e,0.5,delta) 变成固定线性增益非线性ESO退化成线性观测器误差大时收敛速度变慢。反过来 delta 太小零点附近增益过高又容易抖。delta 这个参数卡在非线性作用的有效范围上。解决先记录 e_obsz1-y在正常工况下的历史幅度delta 取这个幅度的5%~10%。调试顺序上先把 delta 设得偏小确认非线性起作用z3 对扰动响应明显变快再往上加直到不抖为止。这个参数值得单独花半小时试它决定了ESO大误差快追的能力边界。5.4 现象仿真稳定、实机振荡怎么调都没用现象仿真里阶跃响应很漂亮搬到实机上同一个参数直接振荡或者在高频处来回甩。原因最常见的是离散步长不匹配。ESO 的连续设计假设步长足够小实机中断周期如果比仿真步长大一个量级wo·h 就逼近稳定边界。经验上 wo·h 要小于0.3~0.5超过这个范围欧拉法的相位损失足以让观测器本身失稳。解决先确认实机中断周期反推 wo 上限wo 0.3/h。想要更高带宽优先提高采样率而不是硬拉 wo。如果采样率动不了把 beta2、beta3 按0.8~0.9系数往下压一点这是一个很实用的降增益技巧能救回不少边缘稳定的系统。这条是最没有后悔药可吃的坑——上实机前先算一下 wo·h。5.5 现象TD的r越调越大跟踪快了噪声也大了现象为了加快响应把 r 往上提结果 v2 明显变噪控制量里出现高频分量执行机构发热。原因r 决定跟踪加速度上限r 越大 v1 逼近 ref 越快但 v2 的峰值随之增大。参考信号里的量化噪声和微小的给定抖动全被 v2 带进控制律的 e2 项。r 太大时TD 几乎等于直接把 ref 交给控制律安排过渡过程的平滑作用就消失了。解决r 按执行机构能力定不按想多快定。期望上升时间先算出来用过渡时间约 4·sqrt(ref/r) 反推 r。要更快的闭环响应提 wc 比提 r 更直接因为 wc 决定的是闭环带宽不牺牲参考侧的平滑性。v2 还抖的话在 TD 输出后加一阶滤波或直接换LADRC的线性形式。6. 进阶LADRC线性化、三参数整定顺序与z3的额外价值如果现场噪声底噪高、调试时间紧我通常建议直接把非线性ESO退化掉用线性自抗扰。具体做法是把两个 fal 全部换成 e 本身控制律和TD结构一字不动。好处是参数只剩 wc、wo、b0 三个整定顺序非常固定第一步开环标定 b0这是地基b0 错了一切白调第二步按期望响应速度定 wc闭环带宽大约就是 wc第三步 wo 从3倍 wc 起步往上调以控制量不抖为上限。整个过程每次只动一个变量记录一次阶跃响应避免几个参数一起调然后互相甩锅。z3 是总扰动估计别只当它是个补偿量。现场调试时把 z3 接到趋势记录里负载突变、机构卡涩、摩擦增大都会在 z3 上体现为突变或慢变分量。有一回某设备出现周期性顿挫位置闭环看不出问题z3 曲线上却清晰出现同频率的台阶顺着找下去发现是导轨润滑不均——这个免费的传感器值得拉一条曲线长期看着。我自己的习惯是新对象先花半小时把 b0 标定准然后把 r、wc、wo、delta 全部写成配置表现场只改表不动代码。每动一个参数先在仿真测试台上重放一遍历史工况确认不翻车再上实机。ADRC不是万能药但对模型大概知道、扰动说不清的对象它省掉的是反复调PID的那几个通宵。希望帮到你。本文还有配套的精品资源点击获取