news 2026/8/31 6:17:32

从PID到ADRC:嵌入式自抗扰控制C代码落地实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从PID到ADRC:嵌入式自抗扰控制C代码落地实战

简介:本资源为面向嵌入式控制工程师与自动化专业学习者的自抗扰控制(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>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 6:15:41

技术面试八股文指南:从原理到实战,把背题变成加分项

最近在技术社群里经常看到类似“现在面试怎么全是八股文”、“背了一个月八股文还是挂了”的吐槽。我自己这些年也面试过不少人&#xff0c;也被面过不少次&#xff0c;对“八股文”这三个字的感情其实挺复杂的。今天这篇随笔&#xff0c;不站队、不抱怨&#xff0c;就想认认真…

作者头像 李华
网站建设 2026/8/31 6:15:01

基于MATLAB的弧长法实现与结构非线性有限元分析

简介&#xff1a;本资源是一份面向计算力学、结构非线性分析及数值方法学习者的MATLAB弧长法&#xff08;Arc-Length Method&#xff09;核心实现脚本&#xff0c;专为解决强非线性方程组收敛困难问题而设计&#xff0c;适用于研究生、科研人员及高年级本科生开展数值仿真与算法…

作者头像 李华
网站建设 2026/8/31 6:13:26

对于C语言的初始理解和困境(混沌期)

示例 #include <stdio.h>int main() {printf("hello world\n");return 0; }这是我打出的第一个程序&#xff0c;也可以说每个初学者都会进行的最简单的代码&#xff0c;但要是只会跟着敲代码&#xff0c;是永远都学不会编程的&#xff0c;需要理解自己打的每个…

作者头像 李华
网站建设 2026/8/31 6:13:24

ai全栈软件开发day18

今日学习内容今天是 Day 18&#xff0c;继续在第三阶段「AI 应用 Web 开发」中深度前进。昨天我们学了 MongoDB 文档数据库、Spring Data MongoDB 集成与 Docker 镜像构建&#xff0c;今天按照 Day 17 结尾的计划&#xff0c;重点攻克两个非常实用的工程化主题&#xff1a;Mave…

作者头像 李华
网站建设 2026/8/31 6:13:19

Kali Linux 安装与渗透测试实战指南:从入门到合法使用

Kali Linux 是安全测试和数字取证领域使用频率最高的 Linux 发行版之一。它可以理解成一个预先装好大量安全工具的操作系统&#xff0c;基础系统来自 Debian&#xff0c;工具集覆盖信息收集、漏洞扫描、Web 应用测试、无线网络审计、逆向分析和报告输出等环节。很多零基础学习者…

作者头像 李华
网站建设 2026/8/31 6:12:44

网易2019秋招笔试编程题全解析:从贪心到动态规划的实战策略

秋招季又到了刷题的季节。如果你正在准备后端、算法岗的校招笔试&#xff0c;"网易2019秋招笔试编程题合集&#xff08;一&#xff09;"这套题应该是绕不开的。不管你是不是把网易作为目标公司&#xff0c;这套题都值得认真做一遍&#xff0c;因为它的出题风格非常典…

作者头像 李华