简介:一份围绕自适应ADRC(自抗扰控制)与PID干扰抑制的控制项目资料,面向自动化与控制工程方向的工程师和研究者,专注于处理系统不确定性及外部扰动下的控制器设计。压缩包共10个文件,集成Matlab脚本(.m)、Simulink模型(.slx)、PDF论文和6张公式/结果图,整体大小仅1.25MB,兼顾源码、文档和可视化参考。目前已有274人学习下载,适合从理论到仿真快速上手。通过研究其中的ADRC控制器实现代码与Simulink仿真模型,可理解扩展状态观测器(ESO)、自适应参数调节及PID协同抗扰的核心机制;结合PDF论文与公式截图,还能梳理稳定性分析思路,为实际系统控制开发提供可迁移的样例。
1. 自适应ADRC进入工程视野:从PID在强扰动下翻车说起
把一套带负载扰动的电机转速环交给 PID,参数整定得再漂亮,也躲不开一个尴尬局面:扰动来得越猛,恢复时间越慢,超调还跟着放大,快速性和抗扰性在同一个环节上互相打架。这就是为什么一看到 adaptive_adrc-master 这类工程示例,懂行的人会优先关注它背后的 ADRC 控制器思路——自抗扰控制的核心是把“模型误差 + 外部扰动”打包成一种可观测的状态,再用控制量直接补偿掉,而不是像 PID 那样靠误差积分慢慢磨。这篇笔记打算把 ADRC 控制从原理拆到实现,再落到一套可复现的仿真实验上,和 PID 做同工况干扰对比,最后把工程落地时最容易翻车的几个坑一并说清楚。适合正在做运动控制、过程控制,或者被扰动和参数漂移折磨过的开发者。
2. ADRC控制器三大件拆解:TD、ESO与误差反馈各自解决什么问题
2.1 ESO是ADRC的灵魂:把“不知道的扰动”变成“看得见的状态”
扩张状态观测器是 ADRC 区别于 PID 的核心。它不依赖精确的被控对象模型,只要求你给一个控制增益的粗略估计 b0,然后把“模型没建准的部分”和“外部干扰”合并成一个扩张状态 z2,用观测器的形式实时估出来。一阶对象配上二阶 ESO,常见写法是:
e = z1 - y z1_dot = z2 - β1·e + b0·u z2_dot = -β2·e
其中 z1 跟踪系统输出,z2 跟踪总扰动。β1 和 β2 按带宽法取 β1 = 2ωo、β2 = ωo²,这样观测器极点都落在 ωo 附近,调一个 ωo 就能控制观测速度。ESO 的工程意义在于:扰动被估出来之后,控制量里直接减掉 z2,系统就从“靠误差积分反复修正”变成了“预判扰动并提前抵消”。这也是 PID 干扰对比中 ADRC 恢复速度明显占优的原因。
但 ESO 不是万能的。z2 能估出来的前提是 b0 和真实控制增益不能偏差太大,否则系统等效扰动里会混入 b0 偏差引起的成分,观测器负担加重。更关键的是 ωo 取值要平衡——调大了观测快但噪声也跟着放大,调小了扰动估计滞后。这就是后面自适应要介入的理由。
2.2 跟踪微分器(TD)与误差反馈:快速性与低超调兼得
PID 的 D 项在工程里经常被白瞎掉,因为测量噪声被微分放大,不得不加滤波,滤波一加相位又滞后。ADRC 的做法是用 TD 先从参考信号里解出“安排好的过渡过程”和它的微分,再从 ESO 里拿被控对象的估计状态,两者做差生成误差反馈。
TD 的连续形式大致是:
v1_dot = v2 v2_dot = -r²·(v1 - ref) - 2r·v2
这里 r 决定跟踪速度。r 越大,v1 越逼近参考信号,但相应地对参考突变越敏感。工程上通常让 r 和控制器带宽保持合理比例,让输出既不丢失快速性,又不会因为阶跃参考而产生过大的初始控制量。误差反馈部分则用 v1 - z1 做比例项,必要时加上 v2 - z2 的微分项,输出 u0。因为扰动已经被 z2 补偿了,u0 只需要承担“让状态跟上参考”的任务,控制律形式可以比 PID 简单得多。
实际项目里,TD 还有一个隐蔽价值:它让目标信号变得平滑,大幅降低了执行机构在急加速和急减速时的冲击。这个好处在伺服和运动控制上体现非常明显,同样是位置阶跃,带 TD 的系统和直接怼参考值的系统,减速机磨损和抖动完全两回事。
2.3 线性还是非线性:参数个数与调参直觉的工程取舍
ADRC 有线性与非线性的分别。非线性 ADRC 在误差反馈和 ESO 误差修正里用 fal(e, α, δ) 这类函数,小误差时增益高,理论性能更漂亮,但参数多出一截,α 和 δ 调起来全靠经验,对初入门的团队并不友好。线性 ADRC(LADRC)则直接把 ESO 和反馈都做成线性结构,参数收敛到 ωo 和 ωc 两个带宽,整定直觉非常清晰。
我一般会建议先上线性 ADRC,把系统跑明白之后,再决定要不要在某个环节引入非线性修正。原因很现实:非线性 ADRC 的优势在强扰动大误差场景下明显,但它的参数敏感度也高,稍有不慎就出现极限环或高频颤振。下面这张表是选型时的常用对照:
| 维度 | 线性ADRC(LADRC) | 非线性ADRC(NLADRC) |
|---|---|---|
| 需整定参数 | 以 ωo、ωc、b0 为主 | ωo、ωc、b0、α、δ、TD的r等 |
| 调参难度 | 低,带宽法直接套用 | 高,非线性参数相互影响 |
| 抗扰性能 | 中上,波形平滑 | 大误差区响应更快 |
| 噪声敏感性 | 依赖带宽选择 | 小误差区增益高,噪声容易被放大 |
| 适合场景 | 电机、伺服、过程量控制 | 强冲击、大范围工况变化的研究型系统 |
从 adaptive_adrc 这类工程名也能看出,主流方向是给 LADRC 加上参数自适应,而不是一上来就上非线性。理由很简单:线性结构的参数物理意义清楚,自适应更新规则容易设计和验证;非线性结构本身已经复杂,再叠加自适应,排查问题时会很痛苦。
3. 把自适应ADRC控制器跑起来:电机模型、参数初值与在线增益辨识实现
3.1 被控对象与离散递推:一套模拟电机转速环
做 ADRC 仿真的第一步是把被控对象搭好。选一个带负载扰动的电机转速环作为对象,方程写为:
J·dw/dt = Kt·u - B·w + d(t)
其中 J 是转动惯量,Kt 是力矩系数,B 是粘性摩擦系数,d(t) 是外部负载扰动。真实控制增益 b_true = Kt/J,但控制器并不知道,它手里只有一个初始估计 b0,这个偏差就是自适应的用武之地。仿真采用固定步长欧拉递推,步长 dt = 0.001 秒,总时长 10 秒。
为了公平验证自适应效果,b0 初始值故意给得和真实值差很多。真实 b_true 约为 60,初始 b0 设置为 15,相当于控制器对对象增益的理解只有真实的四分之一。在这种情况下,固定参数的 ADRC 通常会出现明显的稳态误差或振荡,而自适应 ADRC 应该能在几秒内把 b_est 修正到接近真实值。
3.2 自适应参数更新实现:带死区的增益在线辨识
自适应这里不搞复杂的神经网络或模糊推理,用最容易解释、也最容易在单片机上演算的梯度法做 b_est 在线更新。核心思想是:如果 ESO 的观测误差和当前控制量同号持续存在,说明 b_est 和真实增益之间有系统偏差,按梯度方向修正它。更新规则为:
b_est = b_est - γ·dt·e_eso·u
其中 e_eso = z1 - w 是观测器转速估计与实际转速的误差,γ 是更新步长。为了防止微小噪声积累导致 b_est 漂移,更新前必须经过死区和限幅。完整仿真核心代码如下:
import numpy as np dt = 0.001 # 仿真步长,1ms T = 10.0 # 仿真时长10s N = int(T / dt) # 对象参数 J = 0.02 # 转动惯量 Kt = 1.2 # 力矩系数 B = 0.05 # 粘性摩擦系数 # 控制器初始参数 b0 = 15.0 # 初始控制增益估计,真实约为60 wc = 12.0 # 控制器带宽 wo = 40.0 # ESO带宽 beta1 = 2.0 * wo beta2 = wo * wo # 自适应参数 b_min, b_max = 5.0, 200.0 # b_est的物理上下限 gamma = 0.2 # 更新步长,不能太大 deadzone = 0.02 # 观测误差死区 b_est = b0 # 在线增益估计值 # 状态初始化 w = 0.0 # 实际转速 z1 = 0.0 # ESO估计转速 z2 = 0.0 # ESO估计总扰动 v1 = 0.0 # TD跟踪参考 v2 = 0.0 # TD输出的参考微分 u = 0.0 # 记录 logs = [] for k in range(N): t = k * dt # 负载扰动:0-3s无扰动,3-6s加恒定负载,6-10s加正弦扰动 d = 0.0 if 3.0 < t < 6.0: d = 8.0 elif t >= 6.0: d = 4.0 * np.sin(2.0 * np.pi * 0.8 * t) # 1) TD:安排过渡过程并提取参考微分 e_td = v1 - 100.0 v1 = v1 + dt * v2 v2 = v2 + dt * (-50.0 * e_td - 10.0 * v2) # 2) ESO:用b_est估计转速和总扰动 e_eso = z1 - w z1 = z1 + dt * (z2 - beta1 * e_eso + b_est * u) z2 = z2 + dt * (-beta2 * e_eso) # 3) 误差反馈 + 扰动补偿 u = (wc * (v1 - z1) - z2) / b_est u = np.clip(u, -10.0, 10.0) # 4) 自适应更新b_est:死区 + 限幅 if abs(e_eso) > deadzone and abs(u) > 1e-3: b_est = b_est - gamma * dt * e_eso * u b_est = np.clip(b_est, b_min, b_max) # 5) 对象递推 w_dot = (Kt * u - B * w + d) / J w = w + dt * w_dot logs.append([t, w, z1, u, z2, b_est]) logs = np.array(logs)这段代码的关键在于第 4 步。e_eso 是 ESO 对转速的观测误差,u 是当前控制量。如果 b_est 偏小,ESO 会用偏大的控制量解释转速变化,于是观测误差与控制量保持同号,b_est 就会向真实方向移动。死区避免噪声让 b_est 来回抖,限幅则保证即使更新方向错了,也不会把增益拉出物理合理的范围。
gamma 的选取直接影响自适应收敛速度。gamma 太大,b_est 会出现振荡甚至发散;gamma 太小,修正需要十几秒,扰动来了跟不上。工程上的经验值是 gamma 取 ESO 带宽的 5% 到 10%,这里 wo = 40,gamma = 0.2 在安全区间内。
3.3 参数初始值怎么定:一张表把b0、ωo、ωc与自适应限幅说清楚
仿真模型有了,接下来的问题是参数初值从哪来。这里给一组工程默认值和调节方向:
| 参数 | 初始值 | 作用 | 调大效果 | 调小效果 |
|---|---|---|---|---|
| b0 | 按对象模型估算 | 控制增益的名义值 | 控制量偏小 | 控制量偏大 |
| ωc | 8~15 rad/s | 反馈控制带宽 | 响应变快 | 响应变缓 |
| ωo | 3~5倍ωc | ESO观测带宽 | 扰动估计快但噪声大 | 观测平滑但滞后 |
| γ | 0.05~0.2ωo | 自适应更新步长 | 收敛快但易振荡 | 收敛慢但稳定 |
| deadzone | 传感器噪声幅值 | 防止参数漂移 | 抗噪好但更新迟钝 | 更新灵敏但易漂移 |
| b_min/b_max | 名义b0的50%~300% | 约束b_est范围 | 适应性更强 | 稳定性更好 |
b0 的初值最好还是花几分钟算一下。对电机来说就是 Kt 除 J,如果你在真机上做,通常可以通过一次开环阶跃测试来标定:给一个固定控制量,测稳态转速变化率,反推 b0。这个动作值得做,b0 初值越接近真实,ESO 的负担越轻,后面自适应的修正量也越小。
4. PID干扰对比实验设计:同工况下用IAE和ITAE量化抗扰差距
4.1 三种干扰注入方式与PID整定口径
空口说 ADRC 抗扰强没有说服力,要和 PID 在同一条跑道上比。干扰注入设成三类,分别对应工程里最常见的负载突变、周期性波动和测量噪声背景下的扰动:
| 工况 | 时间段 | 扰动形式 | 模拟场景 |
|---|---|---|---|
| 阶跃负载 | 3s~6s | 恒值 d=8.0 | 负载突然加载或卸载 |
| 周期扰动 | 6s~10s | d=4·sin(2π·0.8t) | 偏心负载、齿轮啮合脉动 |
| 随机扰动 | 全时段可加 | 高斯白噪声 | 风载、流场脉动等 |
PID 参数按极点配置法整定,对象模型已知,kp、ki、kd 能直接算出来。这里给一组可用参数:kp=6.0、ki=0.5、kd=0.02。不推荐用 Ziegler-Nichols 来定,因为临界振荡法只能给出稳定性附近的参数,用于性能对比会给 PID 拖后腿。
关键是对比必须保证两者控制量限幅一致、采样周期一致、扰动注入序列一致。控制量饱和会掩盖控制器之间的真实差别,所以两边都限幅在 ±10。
4.2 对比脚本与指标计算:IAE、ITAE与控制量方差一起算
完整对比脚本这里给出一版,它把 PID 和 ADRC 放在同一个对象模型上跑,并计算三个指标:IAE(误差绝对积分)、ITAE(时间加权误差绝对积分)和控制量方差。IAE 反映全程误差总量,ITAE 加重稳态长尾部分的权重,控制量方差反映执行机构付出的代价。
import numpy as np dt = 0.001 T = 10.0 N = int(T / dt) J, Kt, B = 0.02, 1.2, 0.05 ref = 100.0 # PID参数(极点配置法整定) kp, ki, kd = 6.0, 0.5, 0.02 # ADRC参数 b0, wc, wo = 15.0, 12.0, 40.0 beta1, beta2 = 2.0 * wo, wo * wo def load_disturbance(t): if 3.0 < t < 6.0: return 8.0 elif t >= 6.0: return 4.0 * np.sin(2.0 * np.pi * 0.8 * t) return 0.0 def run_pid(): w, e_int, e_prev, u = 0.0, 0.0, 0.0, 0.0 out = [] for k in range(N): t = k * dt d = load_disturbance(t) e = ref - w e_int += e * dt u = kp * e + ki * e_int + kd * (e - e_prev) / dt u = np.clip(u, -10.0, 10.0) w = w + dt * ((Kt * u - B * w + d) / J) e_prev = e out.append([t, w, u]) return np.array(out) def run_adrc(): w = z1 = z2 = v1 = v2 = u = 0.0 out = [] for k in range(N): t = k * dt d = load_disturbance(t) e_td = v1 - ref v1 += dt * v2 v2 += dt * (-50.0 * e_td - 10.0 * v2) e_eso = z1 - w z1 += dt * (z2 - beta1 * e_eso + b0 * u) z2 += dt * (-beta2 * e_eso) u = (wc * (v1 - z1) - z2) / b0 u = np.clip(u, -10.0, 10.0) w += dt * ((Kt * u - B * w + d) / J) out.append([t, w, u]) return np.array(out) def metrics(data): t, w, u = data[:, 0], data[:, 1], data[:, 2] e = ref - w iae = np.sum(np.abs(e) * dt) itae = np.sum(np.abs(e) * t) * dt u_var = np.var(u) return iae, itae, u_var pid_data = run_pid() adrc_data = run_adrc() pid_m = metrics(pid_data) adrc_m = metrics(adrc_data) print(f"PID IAE={pid_m[0]:.4f} ITAE={pid_m[1]:.4f} u_var={pid_m[2]:.4f}") print(f"ADRC IAE={adrc_m[0]:.4f} ITAE={adrc_m[1]:.4f} u_var={adrc_m[2]:.4f}")脚本里的 run_adrc 故意用了固定 b0,没有加自适应,这能先看清纯 ADRC 相对 PID 的抗扰优势。跑完你会发现,ADRC 的 ITAE 通常会显著低于 PID,尤其在 3s 阶跃负载注入时,转速跌落的幅度更小、恢复到稳态更快。控制量方差上 ADRC 一般也不算差,因为它的补偿动作是“一次性到位”,而不是 PID 那种反复修正。
4.3 结果分析顺序:先看峰值偏离,再看恢复时间,最后算控制代价
对比结果出来后容易犯一个错:只盯着 IAE、ITAE 两个数字就开始下结论。我习惯的分析顺序是三步。第一步看阶跃扰动瞬间的峰值偏离,这反映 ESO 观测扰动的速度和补偿的及时性;第二步看恢复时间,以进入 ±2% 误差带计算,这是现场最关心的指标,也是 ADRC 和 PID 拉开差距最明显的地方;第三步看控制量方差和峰值,控制量震荡太猛说明参数偏激进,就算误差指标好看也不能直接上真机。
如果按这个顺序回看,PID 和 ADRC 的行为差异就非常清楚了。PID 的恢复过程是一条慢慢收拢的衰减曲线,ADRC 则是先跌一个小坑然后干脆利落地拉回来。周期扰动工况下,ADRC 的 z2 能持续跟踪正弦扰动,控制量里会主动叠加一个反相分量,而 PID 只能靠积分被动追赶,表现在误差曲线上就是相位滞后和幅值放大。
5. ADRC控制落地避坑:ESO欠观测、高频颤振与参数跳变的排查记录
5.1 现象一:ESO带宽拉高后控制量高频振荡
现象是转速输出看起来还算平滑,但控制量 u 出现了小幅度高频锯齿,执行机构发出明显的嗡嗡声,电机电流波形在高频段有毛刺。初学者第一反应是反馈通道有噪声,实际上根因往往是 ESO 带宽 ωo 设置过高,把传感器噪声放大后进入了控制量。
解决方法是先确认噪声来源和频谱:对转速反馈做一次功率谱估计,找出噪声能量集中的频段,把 ωo 压到噪声主频的 1/3 以下。另外可在 ESO 误差通道加一阶低通,或者在转速反馈进 ESO 之前做滑动平均滤波。滤波会牺牲一点观测速度,但换来的控制量平稳在工程上更值。
5.2 现象二:自适应更新导致控制量出现台阶
b_est 在线更新过程中,转速反馈平稳时控制量突然跳变一个台阶,随后又慢慢回落。原因很简单:自适应更新规则里少了死区和限幅,e_eso 和 u 的乘积即使很小,经过多个周期积分后也会累积成一个明显的偏移,一旦叠加到控制量上就是台阶。
解决方式是严格按第 3 章的更新结构来:abs(e_eso) 小于 deadzone 时冻结 b_est,更新方向不确定时不动作;b_est 的上下限也要贴合对象物理边界。这一步偷懒不得,无数仿真炫技最后翻车都翻在这。
5.3 现象三:仿真步长变大后ESO观测值发散
同一个模型,步长从 1ms 改成 5ms,ESO 的 z2 开始震荡甚至发散,固定参数 PID 反而还稳定。原因是连续域极点配置在离散化后稳定边界变了,欧拉法对 ωo·dt 的乘积有限制,步长变大后等效极点跑出单位圆。
解决方法是先校验 ωo·dt 的值,经验上限是 0.1 到 0.2。超过这个范围就必须减小仿真步长,或者改用双线性变换法离散 ESO。上真机时这一步尤其重要,单片机中断周期和仿真步长不是一回事,务必按实际执行周期重新校验离散后的 ESO 极点位置。
5.4 现象四:TD在阶跃参考下输出微分尖峰
参考信号直接给阶跃时,v1 的跟踪曲线没问题,但 v2 会在阶跃瞬间冒出一个尖峰,反馈控制量被瞬间拉高,执行机构承受一次冲击。原因是连续 TD 的离散实现里,参考突变幅度超过了差分格式的平滑能力。
解决这类问题有两个常用手法:一是参考信号在进入 TD 之前先做斜坡化或一阶惯性滤波,二是改用专门的离散快速 TD 公式。从工程习惯上,前者更直接,因为现场上位机下发的目标值本身就应该带斜率限制,这既保护执行机构,也让 ADRC 有更充分的时间建立观测。
5.5 现象五:阶跃扰动第一拍转速仍被拉走
即使 ESO 正常工作,3s 负载突加时转速还是会先跌一下,然后在 200ms 内恢复。有人以为是 ESO 不给力,其实这是物理必然——ESO 对扰动的估计和补偿天然滞后一拍:扰动作用到对象后,转速或状态发生变化,ESO 才能把这部分变化解释为扰动并进行补偿。第一拍的偏离是躲不掉的。
合理的目标不是让第一拍完全没有偏离,而是让偏离幅度小、恢复时间短。方法上可以适当提高 β2 加快扰动收敛,但更推荐把速度环采样周期缩短,或在电流环上叠加载荷前馈。仿真里看到的 50ms 滞后,放到真机上可能被放大三倍,调试时别只盯着稳态指标。
6. 从仿真走向真机:自适应ADRC上设备前的验证清单与一个收盘习惯
6.1 上真机前的5项验证清单
仿真通过只说明控制算法在理想采样和精确对象条件下成立,离真机还有一段距离。我这几年的习惯是固定走一套验证流程,五步都过了才允许自己把自适应 ADRC 放进正式固件。
第一步确认传感器噪声水平,用转速反馈的功率谱估计确定 ωo 的上限,超出噪声主频的带宽设定直接否掉。第二步标定 b0 的名义值并测出它随工况的变化范围,如果超过 ±30%,自适应就不是可选功能而是必选功能。第三步用硬件在环平台把采样周期压到和真机一致,跑 1 小时的连续工况,观察 b_est 是否在某个区间内缓慢漂移,漂移量过大说明死区或更新率偏大。第四步做阶跃扰动扫描实验,从 10% 到 100% 负载逐级加载,记录不同幅值下的恢复时间,检查性能是否单调。第五步把自适应更新产生的中间变量全部做成可读参数,至少要能看到 b_est、z2 和 ESO 观测误差的实时曲线。
这五步里最容易被跳过的是第一步和第二步。很多开发者拿着仿真参数直接上真机,结果 ωo 超出传感器可承受范围,控制量全是噪声;还有人不管 b0 漂移,固件里留一个固定值,结果自适应一直处在极限边缘工作。
6.2 一个让自适应ADRC少翻车的调试顺序
最后一个习惯是我自己的血泪经验换来的:所有自适应参数更新,必须先死区、后限幅,再整定更新率。顺序不能反。先有死区,噪声不会累积成参数漂移;再有限幅,即使整定方向错了也不会把系统推离稳定边界;最后调 γ,从保守的 5% ωo 开始逐步加大,观察收敛速度和振荡的权衡点。
另外要清楚一点,自适应不是越快越好。仿真里你可以用大 γ 让 b_est 两秒内收敛,看起来性能很漂亮,但真机上传感器噪声、执行机构饱和、离散误差都会让大 γ 变成不稳定源。一个运行稳定、参数收敛慢 5 秒但整个生命周期内不出问题的自适应环,远好过一个仿真里闪闪发光但一周后现场报警的激进方案。
如果调试时间紧,还有个取舍技巧:先把 b0 和 ωo 固定住,用纯 ADRC 跑通各项功能指标,确认 ESO 观测和扰动补偿这条主链路是好的,再打开自适应更新。这样即使自适应部分出问题,你也能确定问题集中在参数更新模块而不是整个算法框架。把自适应当成最后一块拼图,而不是第一块。希望你调试时少走这些弯路。
本文还有配套的精品资源,点击获取