☰
先进PID控制算法ADRC工程化:TD、ESO、NLSEF代码实现与参数整定
2026/10/22 3:17:09 网站建设 项目流程

简介:这份资料包聚焦先进PID控制算法的工程实现,面向自动化、机器人学与过程控制方向的学习者与工程师,帮助突破传统PID在复杂、不确定系统中的性能瓶颈。包内围绕自抗扰控制(ADRC)、跟踪微分器(TD)与扩张状态观测器(ESO)展开,涵盖理论模型、仿真案例与代码实现,可用于理解非模型依赖控制、预测控制及误差估计等进阶思路。资源共15个文件,以11个m脚本、2个slx仿真模型为主,辅以1个md说明与1个gitignore配置,压缩包约66KB,体积轻便却覆盖控制器设计、差分进化寻优、伺服关节与发电机仿真等模块。目前已有181人学习下载。读者可借助脚本与Simulink模型对照复现ADRC、LADRC及PID改进算法,掌握从状态观测到参数整定的完整链路,为科研仿真与工业控制项目提供可复用的代码骨架与排错参考。

1. 先进PID控制算法:从ADRC、TD到ESO,一套能跑通的工程化路径

如果你做过电机控制、无人机姿态稳定或者温控系统,大概率经历过这样的场景:传统PID参数调了三天,超调还是压不住,负载一变响应就拉胯,积分项一加就振荡,不加又有稳态误差。这时候有人会告诉你,试试自抗扰控制(ADRC)。但打开资料一看,跟踪微分器(TD)、扩张状态观测器(ESO)、非线性状态误差反馈(NLSEF)一堆名词砸过来,公式密密麻麻,代码却找不到几行能直接跑的。这个标题指向的正是这样一套东西:把ADRC及其核心组件TD、ESO用代码实现出来,让工程师能直接在自己的项目里替换掉传统PID。它适合两类人:一是被PID调参折磨到想换方案的控制工程师,二是想理解ADRC到底怎么落地、而不是停留在仿真论文里的开发者。接下来我会按“先跑通最小系统、再拆解每个模块、最后说清楚什么场景该用哪个”的顺序,把这条路走一遍。

2. 拆开ADRC:TD、ESO、NLSEF各自在干什么活

2.1 为什么传统PID在负载突变时会翻车

传统PID的核心逻辑是“误差驱动”:测量值与设定值之间的偏差经过比例、积分、微分运算后输出控制量。问题在于,它把所有外部扰动和内部动态变化都当成“误差”来被动响应。负载突然增加时,电机转速下降,误差出现,PID才开始加大输出。这个响应是有滞后的,而且积分项在恢复过程中容易积累过量,导致超调甚至振荡。

ADRC的思路完全不同。它不区分“内部动态”和“外部扰动”,而是把系统所有不确定因素——包括未建模动态、参数摄动、外部负载变化——统一打包成一个“总扰动”,然后用扩张状态观测器(ESO)去实时估计这个总扰动,再在控制量里直接把它抵消掉。这样一来,剩下的部分就近似变成一个积分器串联系统,控制起来简单得多。

这个思路的工程价值在于:你不需要精确的系统模型,不需要知道负载到底变了多少,只要ESO估计得够快够准,控制器就能自动补偿。对于负载频繁变化、模型难以精确获取的场景,这比反复整定PID参数要省事得多。

2.2 跟踪微分器TD:给阶跃信号安排一个过渡过程

TD的作用是安排过渡过程并提取微分信号。传统PID里,如果设定值是阶跃信号,误差瞬间跳变,微分项会产生一个极大的脉冲,容易激发振荡。TD的做法是让设定值不直接跳变,而是通过一个非线性跟踪器平滑地逼近目标值,同时输出这个过渡过程的微分。

最常见的离散TD形式如下:

import numpy as np def td_track(v_set, v_prev, dv_prev, r, h): """ 离散跟踪微分器 v_set: 当前设定值 v_prev: 上一时刻的跟踪值 dv_prev: 上一时刻的微分值 r: 跟踪速度因子,越大跟踪越快 h: 采样周期 返回: (v_new, dv_new) """ # 计算最速控制综合函数 fhan d = r * h d0 = d * h y = v_prev - v_set + h * dv_prev a0 = np.sqrt(d**2 + 8 * r * abs(y)) if abs(y) <= d0: a = dv_prev + y / h else: a = dv_prev + 0.5 * (a0 - d) * np.sign(y) # 计算新的跟踪值和微分值 if abs(a) <= d: dv_new = -r * a / d else: dv_new = -r * np.sign(a) v_new = v_prev + h * dv_new return v_new, dv_new

这段代码里,r是跟踪速度因子,直接决定过渡过程有多快。r越大,跟踪越快,但太大就失去了平滑意义;r越小,过渡越柔和,但响应变慢。h是采样周期,必须和实际控制周期一致。fhan是最速控制综合函数,它的作用是在“尽快到达目标”和“不产生超调”之间找平衡。

实际使用时,TD的输出v_new作为控制器的参考输入,dv_new作为参考微分。这样即使设定值是阶跃,控制器收到的也是平滑信号,微分项不会爆掉。

2.3 扩张状态观测器ESO:把扰动“看”出来

ESO是ADRC最核心的部分。它把系统输出和输入都利用起来,构造一个观测器来估计系统状态和总扰动。以二阶系统为例,假设系统可以写成:

y'' = f(y, y', w) + b*u

其中f是总扰动(包含内部动态和外部扰动),b是控制增益的估计值,u是控制量。ESO把f扩张成一个新的状态变量,然后用观测器估计出来。

离散ESO的常见实现:

def eso_update(y, u, z1_prev, z2_prev, z3_prev, b0, beta1, beta2, beta3, h): """ 三阶扩张状态观测器(用于二阶系统) y: 当前系统输出 u: 上一时刻控制量 z1_prev, z2_prev, z3_prev: 上一时刻观测器状态 b0: 控制增益估计值 beta1, beta2, beta3: 观测器增益 h: 采样周期 返回: (z1, z2, z3) """ e = z1_prev - y # 观测器状态更新 z1 = z1_prev + h * (z2_prev - beta1 * e) z2 = z2_prev + h * (z3_prev - beta2 * e + b0 * u) z3 = z3_prev + h * (-beta3 * e) return z1, z2, z3

z1估计系统输出,z2估计输出的微分,z3估计总扰动f。beta1、beta2、beta3是观测器增益,通常按带宽配置法来设:把观测器带宽设为wo,则beta1 = 3*wo,beta2 = 3*wo^2,beta3 = wo^3。wo越大,估计越快,但对噪声越敏感。工程上一般取wo为控制器带宽的3到5倍。

b0是控制增益的估计值,它不需要很精确,但数量级要对。如果b0设得比实际增益小很多,控制量会偏大;设得太大,控制量会偏小。一般可以通过阶跃响应粗略估算:给一个已知的控制量阶跃,看输出变化速率,反推b0。

2.4 非线性状态误差反馈NLSEF:怎么把误差“压”下去

NLSEF的作用是把TD输出的参考值和ESO估计的状态之间的误差,通过非线性函数组合成控制量。常见的非线性函数是fal函数:

def fal(e, alpha, delta): """ 非线性函数 fal e: 输入误差 alpha: 非线性度,0<alpha<1 delta: 线性区间宽度 """ if abs(e) <= delta: return e / (delta**(1 - alpha)) else: return (abs(e)**alpha) * np.sign(e)

然后控制量由两部分组成:一部分是误差反馈,一部分是扰动补偿。

def nlsef_control(v1, v2, z1, z2, z3, b0, kp, kd, alpha1, alpha2, delta): """ 非线性状态误差反馈控制 v1, v2: TD输出的参考值和微分 z1, z2, z3: ESO估计的状态和扰动 b0: 控制增益估计值 kp, kd: 比例、微分增益 """ e1 = v1 - z1 e2 = v2 - z2 u0 = kp * fal(e1, alpha1, delta) + kd * fal(e2, alpha2, delta) u = (u0 - z3) / b0 return u

kp和kd的整定可以参照PD控制器的思路,但因为有了扰动补偿项-z3/b0,这两个增益不需要太大。alpha1和alpha2通常取0.5和0.25,delta取0.01到0.1之间。fal函数在小误差时近似线性,大误差时增益降低,这样既保证了稳态精度,又避免了大幅值误差引起的过冲。

把TD、ESO、NLSEF串起来,就是一个完整的ADRC控制器。每个控制周期执行一次:TD更新参考轨迹,ESO更新状态估计,NLSEF计算控制量。三个模块的计算量都不大,在常见的MCU上跑几百微秒的控制周期完全没问题。

3. 从零搭一个ADRC控制器:参数整定与代码落地

3.1 最小可跑系统的代码结构

把前面三个模块组装起来,一个完整的ADRC控制循环如下:

class ADRCController: def __init__(self, r, h, b0, wo, kp, kd, alpha1=0.5, alpha2=0.25, delta=0.01): self.r = r self.h = h self.b0 = b0 self.wo = wo self.kp = kp self.kd = kd self.alpha1 = alpha1 self.alpha2 = alpha2 self.delta = delta # 观测器增益按带宽配置 self.beta1 = 3 * wo self.beta2 = 3 * wo**2 self.beta3 = wo**3 # 初始化状态 self.v1 = 0.0 self.v2 = 0.0 self.z1 = 0.0 self.z2 = 0.0 self.z3 = 0.0 self.u_prev = 0.0 def update(self, setpoint, measurement): # TD更新 self.v1, self.v2 = td_track(setpoint, self.v1, self.v2, self.r, self.h) # ESO更新 self.z1, self.z2, self.z3 = eso_update( measurement, self.u_prev, self.z1, self.z2, self.z3, self.b0, self.beta1, self.beta2, self.beta3, self.h ) # NLSEF计算控制量 u = nlsef_control( self.v1, self.v2, self.z1, self.z2, self.z3, self.b0, self.kp, self.kd, self.alpha1, self.alpha2, self.delta ) self.u_prev = u return u

这个类可以直接嵌入到控制循环里。每个控制周期调用一次update,传入设定值和测量值,返回控制量。注意u_prev的更新时机:ESO用的是上一时刻的控制量,所以要在计算完新控制量之后再更新u_prev。

3.2 参数整定的顺序和量级参考

ADRC的参数比PID多,但整定有明确的顺序,不需要盲目试凑。

第一步:确定采样周期h。这个由你的控制平台决定,一般和实际控制周期一致。比如电机控制常用1ms或0.1ms。

第二步:确定b0。给系统一个已知的控制量阶跃,记录输出的初始变化率。b0近似等于输出变化率除以控制量。如果不方便做实验,可以根据物理模型估算。比如电机转速对电压的响应,b0大约等于转矩常数除以转动惯量。

第三步:确定观测器带宽wo。一般取wo = (3~5) * wc,其中wc是你期望的控制器带宽。如果系统噪声大,wo取小一些;如果要求快速估计扰动,wo取大一些。但wo太大时,z3会引入高频噪声,反而影响控制效果。

第四步:确定kp和kd。按PD控制器的方式整定,但目标带宽可以比传统PID高一些,因为有扰动补偿。一般kp = wc^2,kd = 2*wc,wc是期望带宽。

第五步:确定TD的r。r决定过渡过程快慢,一般取r = wc * 10到wc * 50之间。如果设定值变化频繁,r取大一些;如果希望过渡非常平滑,r取小一些。

第六步:微调alpha1、alpha2、delta。这三个参数影响非线性特性,默认值0.5、0.25、0.01在多数场景下够用。如果稳态有微小振荡,适当增大delta;如果动态响应偏慢,适当减小alpha1。

3.3 用阶跃响应验证TD和ESO是否正常工作

在接入实际被控对象之前,先单独验证TD和ESO的行为。TD的验证很简单:给一个阶跃设定值,观察v1是否平滑逼近设定值,v2是否在过渡过程中有合理的微分输出。

ESO的验证需要实际系统的输入输出数据。一个实用的方法是:先开环给系统一系列控制量,记录输出,然后用这些数据离线跑ESO,看z3是否能跟踪上扰动变化。如果z3在系统稳态时接近零,在负载突变时有明显响应,说明ESO工作正常。

# 离线验证ESO的示例 def validate_eso(y_data, u_data, b0, wo, h): beta1 = 3 * wo beta2 = 3 * wo**2 beta3 = wo**3 z1, z2, z3 = 0.0, 0.0, 0.0 z3_history = [] for i in range(len(y_data)): e = z1 - y_data[i] z1 = z1 + h * (z2 - beta1 * e) z2 = z2 + h * (z3 - beta2 * e + b0 * u_data[i]) z3 = z3 + h * (-beta3 * e) z3_history.append(z3) return z3_history

如果z3_history在稳态时波动很大,说明wo太大,需要降低。如果z3_history对扰动的响应明显滞后,说明wo太小,需要提高。

3.4 和传统PID的替换对照:接线与代码改动量

把传统PID替换成ADRC,硬件接线不变,只需要改控制器代码。传统PID的代码通常是这样:

# 传统PID error = setpoint - measurement integral += error * h derivative = (error - prev_error) / h u = kp * error + ki * integral + kd * derivative prev_error = error

替换成ADRC后,kp和kd的含义变了,不再需要积分项。原来的ki参数去掉,新增b0、wo、r三个参数。代码改动量大约几十行,主要是把PID计算替换成ADRC的update调用。

需要注意的是,ADRC的输出u可能比PID大,因为里面有扰动补偿项。如果执行器有饱和限制,需要在输出后加限幅,并且把限幅后的值反馈给ESO的u_prev,否则ESO会估计错误。

4. 避坑与排查:ADRC落地时最容易翻车的五个地方

4.1 现象:系统持续高频振荡,z3输出噪声很大

原因:观测器带宽wo设得太大,导致ESO对测量噪声过度敏感,z3把噪声当成扰动去补偿,形成正反馈。

解决:先把wo降低到原来的1/3到1/2,观察振荡是否减弱。如果减弱但响应变慢,说明需要在ESO前面加低通滤波,或者适当降低kp。另一个办法是把fal函数的delta增大,让ESO在小误差区间更线性,减少噪声放大。

4.2 现象:稳态误差始终存在,z3在稳态时不为零

原因:b0估计不准。如果b0比实际增益小,控制量会偏大,但ESO的补偿项-z3/b0也会偏大,导致稳态时z3需要维持一个非零值来平衡。如果b0偏差太大,稳态误差会很明显。

解决:重新估算b0。方法是在开环状态下给一个控制量阶跃,记录输出的变化率,用b0 = 输出变化率 / 控制量计算。如果系统不允许开环实验,可以在闭环下微调b0,观察稳态误差的变化方向,逐步逼近。

4.3 现象:设定值突变时超调很大,TD的过渡过程没有起作用

原因:TD的跟踪速度因子r设得太大,过渡过程太快,相当于没有平滑。或者TD的输出没有正确接入NLSEF,控制器仍然在用原始设定值。

解决:检查代码中NLSEF的输入是否用的是v1而不是setpoint。然后把r降低到原来的1/2到1/3,观察超调是否减小。如果r已经很小但超调仍然大,可能是kp太大,适当降低kp。

4.4 现象:控制量在启动阶段饱和,系统响应出现明显滞后

原因:ADRC的扰动补偿项在启动时z3还没有收敛,-z3/b0可能产生一个很大的初始控制量。如果执行器有限幅,控制量饱和后系统实际响应变慢,ESO的估计进一步偏离,形成恶性循环。

解决:在启动阶段对z3加限幅,或者让z3从一个合理的初始值开始。另一个办法是启动时先用较小的kp,等ESO收敛后再切换到正常参数。实际工程中,也可以在启动前几个周期只更新ESO但不输出控制量,让观测器先“预热”。

4.5 现象:从PID切换到ADRC后,系统对负载扰动的恢复时间反而变长

原因:ADRC的扰动补偿依赖于ESO的估计速度。如果wo设得偏小,ESO对扰动的响应滞后,补偿就不及时。另外,如果b0偏大,补偿量-z3/b0会偏小,扰动抑制效果减弱。

解决:适当提高wo,但要注意噪声问题。如果噪声不允许提高wo,可以尝试提高kp,让误差反馈承担更多扰动抑制任务。同时检查b0是否偏大,适当降低b0可以增大补偿量。实际调试时,可以先固定kp和kd,单独调wo和b0,观察扰动恢复时间的变化。

5. 进阶技巧:把ADRC用到非线性场景和嵌入式平台

5.1 非线性ADRC的fal函数参数怎么调

fal函数的两个参数alpha和delta决定了非线性特性的形状。alpha越小,大误差时的增益压缩越明显,有利于减少超调,但太小会导致响应变慢。delta决定线性区间的宽度,delta越大,小误差时越接近线性,稳态越平滑,但抗小扰动能力会下降。

一个实用的调参策略是:先固定delta=0.01,把alpha1从0.5逐步降到0.3,观察超调是否减小。如果超调减小但上升时间明显增加,说明alpha1太小,回调到0.4左右。alpha2通常取alpha1的一半,比如alpha1=0.5时alpha2=0.25。

对于不同的被控对象,alpha的最优值不同。一般来说,对象非线性越强,alpha可以取越小。如果对象近似线性,alpha取接近1的值,此时fal函数近似线性,ADRC退化为线性ADRC。

5.2 把ADRC部署到MCU上的定点化注意事项

在嵌入式平台上,浮点运算可能比较慢,尤其是没有FPU的MCU。这时候需要考虑定点化。定点化的关键是确定每个变量的Q格式。

z1和z2的量级和系统输出一致,可以按输出的动态范围来定。z3的量级取决于扰动大小,一般比输出大一个数量级。b0通常是一个较小的数,需要较高的精度。beta1、beta2、beta3在wo较大时数值很大,需要特别注意溢出。

一个实用的做法是:先用浮点跑通,记录每个变量的实际范围,然后根据范围选择Q格式。比如z1范围是0到1000,用Q12格式(整数部分12位,小数部分4位)可以覆盖。z3范围是-5000到5000,用Q8格式。beta3可能达到10^6量级,需要Q20以上。

定点化后,乘法和除法需要特别处理。两个Q格式的数相乘,结果的Q格式是两者之和,需要右移相应的位数。除法则是Q格式之差。这些操作在代码里要仔细核对,否则会出现精度损失或溢出。

5.3 用Python仿真验证ADRC性能的完整脚本

在部署到实际硬件之前,用Python做一次仿真验证可以省去很多调试时间。下面是一个完整的仿真脚本,模拟一个二阶系统:

import numpy as np import matplotlib.pyplot as plt # 被控对象:二阶系统 y'' = -a*y' - b*y + c*u + disturbance a, b, c = 2.0, 5.0, 1.0 # ADRC参数 h = 0.001 r = 100.0 b0 = c # 控制增益估计值 wo = 50.0 kp = 100.0 kd = 20.0 controller = ADRCController(r, h, b0, wo, kp, kd) # 仿真 T = 2.0 steps = int(T / h) time = np.arange(steps) * h y = np.zeros(steps) dy = 0.0 u = 0.0 setpoint = 1.0 disturbance = np.zeros(steps) disturbance[int(1.0/h):int(1.2/h)] = 5.0 # 在1秒时加扰动 for i in range(steps): # 被控对象更新 ddy = -a * dy - b * y[i] + c * u + disturbance[i] dy += ddy * h y[i] += dy * h # ADRC控制 u = controller.update(setpoint, y[i]) # 绘图 plt.figure() plt.plot(time, y, label='Output') plt.plot(time, setpoint * np.ones(steps), '--', label='Setpoint') plt.plot(time, disturbance, ':', label='Disturbance') plt.legend() plt.xlabel('Time (s)') plt.ylabel('Output') plt.title('ADRC Simulation') plt.grid(True) plt.show()

这个脚本可以直接运行,观察ADRC在阶跃设定值和负载扰动下的表现。如果超调大,降低kp或增大r;如果扰动恢复慢,提高wo或降低b0。仿真通过后再移植到实际平台,参数可以作为初始值。

5.4 什么场景该用ADRC,什么场景继续用PID

ADRC的优势在于抗扰动和模型不确定性,但它的参数更多,调试门槛比PID高。如果被控对象模型精确、负载稳定、PID已经能满足要求,没必要换ADRC。但如果遇到以下情况,ADRC值得一试:负载频繁变化且幅度大、系统模型难以精确获取、传统PID在动态和稳态之间难以兼顾、执行器有非线性特性。

我自己的习惯是:先用PID跑一版,记录超调、调节时间、稳态误差。如果调参超过半天仍然达不到要求,就切到ADRC,按b0、wo、kp、kd的顺序整定。多数情况下,ADRC能在不增加硬件成本的前提下,把扰动恢复时间缩短一半以上。但前提是ESO的带宽和噪声要平衡好,否则高频振荡会比PID更难处理。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询