简介:本资源为面向嵌入式控制工程师与自动化专业学习者的自抗扰控制(ADRC)算法C语言工程化实现,聚焦电机调速、伺服系统等实时控制场景,解决传统PID在模型不确定、外部扰动强时鲁棒性不足的问题。压缩包共36个文件,约32KB,包含10个可直接运行的示例工程(sample)、4个主控逻辑模板(master)、2个核心头文件(ADRC.h等)、1个参数配置说明文本,以及.git版本控制相关元数据文件;其中ADRC.c与ADRC.h封装了改进型跟踪微分器(TD)、三阶扩张状态观测器(ESO)、非线性反馈组合及扰动补偿模块,支持r、h、N、beta系列参数在线调节。已有128人下载学习,提供开箱即用的轻量级C代码,无需依赖第三方库,便于移植至STM32、GD32等MCU平台,配合注释清晰的参数接口与典型调参说明,显著降低ADRC算法落地门槛。 三年前调试一台高速贴片机X/Y轴,我遇到一件特别拧巴的事:位置环用PID,把给定跟随调快了,阶跃超调;把超调压下去,追踪S曲线时又甩出明显滞后;更头疼的是更换负载后整个增益都要重来一遍。当时翻到自抗扰控制(ADRC)的资料,第一反应是公式太多、能直接落地的工程资料太少。直到自己把C代码跑起来才明白,它解决的就是这类“跟随和抗扰两头不讨好”的根子问题——把内部动态、外部扰动、未建模特性统统归成一个总扰动,再用扩张状态观测器观测出来,最后一步补偿掉。
这篇文章把我整理的ADRC的C代码实现过程完整写出来,从数学直觉、最小线性版本到非线性fal版本,再到嵌入式里真正能跑的工程细节,包括参数整定顺序和一系列容易翻车的坑。适合已经会PID、想在单片机或控制卡上把ADRC落地的人参考。我不会只给代码,会给每个设计选择背后的理由,这样你才能真正在项目里改得动、调得通。
1. 我为什么在嵌入式控制里换掉PID
1.1 PID调不动的那台电机
先还原一下当时的场景。X/Y轴是典型的二阶运动对象,位置环输出速度指令,速度环再带电流环。原来位置环就是PD加个前馈。为了抑制胶头在高速停止时的位置过冲,我把比例增益压得很低;结果追踪高速轨迹时,动态误差大得肉眼可见。把增益提上去,阶跃又有明显的超调,甚至在某几个速度段出现机械共振的呻吟声。
这就是单环PID的结构性矛盾:给定跟随带宽和抗扰动带宽被锁在同一个误差信号里。输出误差大时,比例项同时承担“追参考”和“顶扰动”的功能,但追参考需要高增益,顶扰动在高频段反而被积分项拖后腿。你调出来的每一个参数,都是在给一个耦合问题打补丁。
后来我尝试在PID基础上加各种前馈、摩擦补偿、陷波器,能跑通,但每换一台设备、换一套机械负载,整套参数就要重调一遍。这种“每个项目都从零开始”的滋味,做过运动控制的工程师都懂。
1.2 主动抗扰到底在“抗”什么
ADRC的全称是Active Disturbance Rejection Control,核心思想就是“别去分辨扰动是摩擦、重力还是负载突变,把它们的总和当成一个状态量,直接观测,然后抵消”。
这句话听上去简单,做起来有一套完整闭环:先把被控对象写成积分串联型,把系统内部动态和外部扰动合并成一个总扰动项,然后用扩张状态观测器(ESO)把总扰动估出来,最后用估计值做前向补偿。补偿完之后,原本复杂的对象在你的控制器眼里就变成了一个“积分串联型”的标称系统,再给它配一个简单的PD控制器,就可以同时兼顾跟随带宽和抗扰能力。
相比PID,ADRC不需要高精度的模型,也不需要精确的扰动特性,只需要知道一个大概的控制增益b0。这个特性在嵌入式环境里非常实用,因为单片机里根本跑不了复杂系统辨识算法。
1.3 哪些场景最适合先上ADRC
我实践下来,这几类场景ADRC收益最大:伺服/步进位置环、转速环,尤其是有明显负载突变或摩擦非线性的工况;机器人关节力矩和位置控制;无人机姿态角控制;以及所有“模型不精确但有可用的控制输入和输出测量”的系统。
如果你只是做一个实验平台、控制对象是理想的一阶水箱,或者没有测量反馈的纯开环系统,那ADRC优势不大。ADRC需要实时测量输出y,ESO才有意义。特别提醒一句:ADRC的C代码实现本身并不复杂,真正的功夫在采样周期、参数收敛和抗测量噪声这些工程细节上,后文会逐个展开。
2. ADRC三件套的数学直觉,先别急着写代码
2.1 把任意被控对象写成积分串联型
所有ADRC推导都从这种形式出发:
y'' = f(y, y', w, t) + b0 * u
其中u是控制输入,y是输出,f代表所有“不知道的东西”:未建模动态、摩擦、重力、负载突变,统统打包。b0是控制输入的增益估计,代表“我给一份控制量,对象大概能产生多少加速度”。之所以写成y''而不是更高阶,是因为绝大多数机械系统本质上是二阶的:位置到速度、速度到加速度。
这个公式的工程含义是:不管对象多复杂,只要我把扰动f估计出来并在控制量里减掉,剩下的就是一个“单位增益的积分器串联模型”:
y'' ≈ u0
这就是ADRC整个思想的落脚点。PID是在“不知道扰动是多少”的时候被动纠正误差,而ADRC是主动把扰动抠出来补偿掉。
2.2 跟踪微分器TD:给信号装一个不超调的引路人
阶跃信号直接送给控制器,本质上是一种很暴力的请求。老司机起步不会一脚油门踩到底,而会先缓后急地给油门。TD(Tracking Differentiator)就是干这个的:对参考信号安排一个过渡过程,同时输出参考信号的一阶导数和二阶导数。
数学上,TD是一个快速最优控制系统。它有两个状态:v1跟踪参考信号,v2是v1的导数。通过一个非线性函数fst,让v1以尽量快的速度到达参考值,同时不产生超调。这样,位置环面对的不再是生硬的阶跃,而是一条平滑的目标轨线,过冲从源头就被消灭了。
实践中有人会问:参考资料本身已经是S曲线,为什么还要TD?我的经验是,TD更多用来解决“测量信号不光滑”的问题,比如旋变或者光栅尺读数毛刺多时,TD输出的微分比直接差分稳定得多。即使参考信号已经平滑,TD的微分信号也可以直接拿来做前馈。
2.3 扩张状态观测器ESO:把总扰动“看”出来
ESO是ADRC的地基。它的输入是输出测量y和控制量u,输出是三个估计值:z1估计y,z2估计y',z3估计总扰动f。关键就在这个第三阶状态上——它把原来模型中“没有建模”的f扩张成了新的状态,所以叫扩张状态观测器。
以线性ESO为例,形式是:
z1' = z2 - β1(z1 - y) z2' = z3 - β2(z1 - y) + b0*u z3' = -β3(z1 - y)
其中β1、β2、β3是观测器增益。z1追y,z2追y',z3就把跟踪过程中的残差一路累积成对总扰动的估计。可以理解为:一个有经验的仪表工,不用打开机器,只看输出仪表读数与控制量,就能判断设备当前的负载状态,z3就是那个“状态估计”。
2.4 控制律:补偿之后用最朴素的PD接管
有了z3,控制量设计就变成了:
u = (u0 - z3) / b0
如果z3≈f,代回原方程,y''≈u0,扰动被抵消。剩下的u0就采用最经典的形式:
u0 = kp*(v1 - z1) + kd*(v2 - z2)
把控制器极点配置到目标带宽上。因为对象已经被改造成“单位增益积分串联型”,所以同样的kp、kd规则可以跨平台复用,这就是ADRC参数通用性的由来。
3. 最小可行LADRC:三个参数就能跑起来的C代码
3.1 数据结构与初始化
先不碰非线性,起步用线性ADRC(LADRC),参数少、行为可预测,能跑通再升级。我的C实现用一个结构体把所有状态和参数装起来:
typedef struct { float dt; // 采样周期, 单位s float r; // TD速度因子, 决定过渡过程快慢 float b0; // 控制增益估计 float w0; // 观测器带宽 rad/s float wc; // 控制器带宽 rad/s float umax; // 控制量限幅 // TD状态 float v1, v2; // ESO状态 float z1, z2, z3; // 由带宽算出的增益 float beta1, beta2, beta3; float kp, kd; } ADRC;初始化函数:
void adrc_init(ADRC *p, float dt, float r, float b0, float w0, float wc, float umax) { p->dt = dt; p->r = r; p->b0 = b0; p->w0 = w0; p->wc = wc; p->umax = umax; p->v1 = p->v2 = 0.0f; p->z1 = p->z2 = p->z3 = 0.0f; // 线性ESO采用带宽法配置极点 p->beta1 = 3.0f * w0; p->beta2 = 3.0f * w0 * w0; p->beta3 = w0 * w0 * w0; // 控制器极点配置在wc, 临界阻尼 p->kp = wc * wc; p->kd = 2.0f * wc; }带宽法把观测器和控制器的极点分别放在w0和wc上。这样我只需要关心两个物理意义明确的值,而不是beta1、kp等六个没有直觉的数字。
3.2 跟踪微分器的fst函数实现
TD用的fst函数是韩京清先生给出的离散最速控制综合函数。我直接用标准形式:
static inline float sgn(float x) { return (x > 0.0f) - (x < 0.0f); } // 最速跟踪微分器的离散综合函数 // 输入x1,x2为TD状态, v为参考, r为速度因子, h为采样周期 static void fst(float x1, float x2, float v, float r, float h, float *fh) { float d = r * h; float d0 = h * d; float y = x1 - v + h * x2; float a0 = sqrtf(d * d + 8.0f * r * fabsf(y)); float a = (fabsf(y) <= d0) ? (x2 + y / h) : (x2 + 0.5f * (a0 - d) * sgn(y)); *fh = (fabsf(a) <= d) ? (-r * a / d) : (-r * sgn(a)); }看到没有,整个非线性体现在这个a的折线处理上。|y|小时进入线性区,相当于阻尼;|y|大时使用最大加速度饱和,相当于限幅。这个函数的输出就是TD给状态v2的加速度指令。
3.3 线性扩张状态观测器与控制律
最重要的单步计算函数,我放在这里:
static inline float clipf(float x, float lo, float hi) { return (x < lo) ? lo : (x > hi) ? hi : x; } float adrc_step(ADRC *p, float ref, float y) { float fh; float e, u0, u_cmd, u_real; // 1. 跟踪微分器安排过渡过程 fst(p->v1, p->v2, ref, p->r, p->dt, &fh); p->v1 += p->dt * p->v2; p->v2 += p->dt * fh; // 2. 控制律: 加总扰动补偿 e = p->z1 - y; u0 = p->kp * (p->v1 - p->z1) + p->kd * (p->v2 - p->z2); u_cmd = (u0 - p->z3) / p->b0; u_real = clipf(u_cmd, -p->umax, p->umax); // 3. 扩张状态观测器更新, 注意一定要用限幅后的u_real p->z1 += p->dt * (p->z2 - p->beta1 * e); p->z2 += p->dt * (p->z3 - p->beta2 * e + p->b0 * u_real); p->z3 += p->dt * (-p->beta3 * e); return u_real; }这个函数每周期调用一次即可。你只需要保证调用周期和p->dt一致,这是最关键的约束,后面会专门说。
3.4 一个完整的位置环调用示例
以位置环为例,被控对象是电机+负载:
void control_loop_1kHz(void) { static ADRC pos_adrc; float ref_pos, actual_pos, torque; // 每1ms调用一次 adrc_init(&pos_adrc, 0.001f, 50.0f, 800.0f, 80.0f, 20.0f, 10.0f); while (1) { ref_pos = get_target_position(); // 来自上位机 actual_pos = get_encoder_position(); torque = adrc_step(&pos_adrc, ref_pos, actual_pos); set_motor_torque(torque); delay_until_next_1ms(); } }在这个例子里,r=50代表过渡速度,b0=800是估计的“单位力矩产生的加速度”,w0=80是观测器带宽,wc=20是控制带宽,umax=10是限幅。这些不是拍脑袋,整定方法放到第五章。这个最小框架在STM32、TMS320、甚至很多国产MCU上都能跑,只要支持浮点运算。
4. 非线性ADRC完整实现:fal函数与离散化取舍
4.1 fal函数:死区、增益和抖振的权衡
非线性ADRC和线性版本最主要的区别,是把观测器误差e的处理从“固定增益”变成“小误差大增益、大误差小增益”的fal函数,让观测器在误差小的时候更敏感、误差大的时候更从容:
static inline float fal(float e, float a, float delta) { if (fabsf(e) <= delta) { return e / powf(delta, 1.0f - a); // 原点附近线性段 } return powf(fabsf(e), a) * sgn(e); // 非线性段 }delta是线性段宽度,通常取0.01到0.1,太大会把fal退化成线性,太小会在零点附近产生抖振。a取0.5附近,指数越低,小误差下的增益越高,但数字化实现时的浮点舍入问题也更明显。
4.2 非线性ESO与非线性反馈律的C实现
把线性ESO里的e替换成fal(e, 0.5)和fal(e, 0.25),再给反馈律也用fal,就成了完整的非线性ADRC:
float adrc_step_nl(ADRC *p, float ref, float y) { float fh, e, fe, fe1; float ev, ed, u0, u_cmd, u_real; fst(p->v1, p->v2, ref, p->r, p->dt, &fh); p->v1 += p->dt * p->v2; p->v2 += p->dt * fh; e = p->z1 - y; fe = fal(e, 0.5f, 0.01f); fe1 = fal(e, 0.25f, 0.01f); ev = p->v1 - p->z1; ed = p->v2 - p->z2; u0 = p->kp * fal(ev, 0.5f, 0.01f) <p> <a href="https://download.csdn.net/download/2401_84572204/91681498" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>