ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ADRC自抗扰控制算法:C语言实现与MCU部署全攻略

ADRC自抗扰控制算法:C语言实现与MCU部署全攻略 简介本资源为面向嵌入式控制工程师与自动化专业学习者的自抗扰控制ADRC算法C语言工程化实现聚焦电机调速、伺服系统等实时控制场景解决传统PID在模型不确定、外部扰动强时鲁棒性不足的问题。压缩包共36个文件约32KB包含10个可直接运行的sample示例、4个master主控逻辑模板、2个头文件ADRC.h等核心接口定义以及ADRC.c核心算法实现——完整涵盖改进型最速跟踪微分器TD、三阶扩张状态观测器ESO、非线性反馈组合与扰动补偿模块并支持h0N*h步长调节、beta与alpha系列参数在线配置。已有128人下载学习读者可即刻集成至STM32、GD32等MCU平台无需重写底层结构配套参数注释详尽便于理解各模块耦合关系与调试逻辑是掌握ADRC从原理到落地的关键实践材料。 写这套代码的起因是我在一个温控项目上把PID参数调到怀疑人生。负载一变温度就大幅波动最后还是咬牙把自抗扰控制ADRC搬到单片机上用C语言从头撸了一套才算稳住。最初以为要先啃半本现代控制理论才能动手实际做下来发现ADRC的工程门槛比想象中低但坑也确实不少尤其是离散化、参数整定、观测器稳定性这些环节光看理论文章根本碰不到。这篇博文把我整理的完整C代码框架、参数整定方法和踩坑记录都放出来适合在MCU上做运动控制、温度控制、电源控制的开发者参考。你不需要把扩张状态观测器的收敛性证明推一遍只需要照着代码改参数就能用起来。1. 项目概述与核心需求解析1.1 为什么要用ADRC而不是继续调PIDPID在大多数工业场景里都够用我早期做温控也是先上PID参数调两三个小时就能稳定在±0.5℃以内看起来没什么问题。但一旦遇到负载突变比如加热腔里突然放进一批常温物料温度会掉下去一截PID要过很久才慢慢拉回来期间还容易超调。用行话说这是“外部扰动抑制能力不足”。ADRC解决的核心问题是把这个“外部扰动”和“模型不准确”打包成一个总扰动然后用扩张状态观测器实时估计出来再在控制量上反向抵消。它的设计思路一句话就能说清楚不再试图精确建模扰动来源而是直接观测并干掉它。C语言实现的优势也很直接——嵌入式、工控、无人机、伺服驱动这些领域C语言仍然是绝对主力代码可以直接烧进STM32、DSP这类芯片里没有运行时依赖没有解释器开销实时性有保证。说白了这个项目要干的事就是把韩京清老师那套ADRC理论从数学公式翻译成能跑在真实控制器上的C代码。不只是给出代码片段还要解决工程里最头疼的几件事ESO离散化之后稳不稳定、参数怎么调才能快速收敛、噪声会不会把微分信号淹没。1.2 这套C代码能做什么、适合谁我给你一个最直观的结论如果控制对象可以近似成一阶或者二阶系统比如加热器、电机转速环、舵机角度环、开关电源电压环ADRC的C代码实现基本都能套用。你只需要改采样周期、量程、输出限幅和几个关键参数就能把PID替换掉换来的是更硬的抗扰能力。具体的环境大概是这样的主控芯片STM32F103级别就能跑不需要额外的DSP库采样周期1ms到20ms都可以取决于你的系统响应速度数据类型float足够不需要double低端MCU跑double会慢得怀疑人生代码量完整实现大概300行以内加上注释也没多少适合人群包括正在做机电控制的嵌入式工程师、做温控或电源的开发者、学控制理论但想动手验证的学生。如果你是纯算法研究员可能更关心收敛性推导但如果你想在真机上看到ADRC的效果这篇博文应该能帮你少走很多弯路。1.3 整体实现规划TD、ESO、SEF三件套ADRC的完整结构由三个核心模块组成代码实现时也必须按这三个模块来组织不能混在一起写TD跟踪微分器给定期望值安排一个平滑的过渡过程同时提取期望值的微分信号ESO扩张状态观测器用系统的输出和控制量估计出系统状态和总扰动SEF状态误差反馈把TD的过渡状态和ESO的估计状态做差形成反馈控制律再把ESO估计的总扰动补偿掉后面第4章的完整代码就是围绕这三个模块来写的。为什么强调“必须分开”因为三个模块有各自的离散化要求、参数特性和调试手段混在一起写出问题很难排查。我见过有人把TD和ESO写成一个函数结果调试时根本分不清到底是哪个模块发散了这是典型的反面教材。2. ADRC控制算法的核心原理拆解2.1 跟踪微分器安排过渡过程化解超调与快速性的矛盾先看一个生活例子。你开车接近一个路口如果盯着目标点直接踩油门快到路口才发现速度降不下来只能急刹。更合理的做法是提前规划一条减速曲线让车平缓地停下来。TD干的就是这件事——把突变的给定值重新“规划”成一条平滑的过渡轨迹。数学上TD接收原始给定值v_ref内部维护两个状态跟踪值v1和它的导数v2。每一拍更新一次让v1以固定速率追赶v_ref同时输出平滑的v2作为目标速度。这样控制器面对的不再是突然跳变的误差而是一条有上限变化率的误差曲线系统自然不容易超调。TD里最经典的实现是韩京清教授的最速控制综合函数fhan。它的核心公式我放到了第4章的代码里这里先解释它的物理意义输入是当前的跟踪误差和误差导数输出是一个加速度指令这个指令会让跟踪状态以“先匀速后减速”的方式接近目标避免出现速度超调。工程上TD的好处很明显——它就是一个数字滤波器不依赖被控对象模型参数只有一个r也就是跟踪速度。r越大过渡越快但也会放大对噪声的敏感度r越小过渡越慢但系统更稳。我实际使用中TD不仅能抑制超调还能在给定值频繁变化时减少执行机构比如阀门、电机的机械磨损。你可以把它理解成一个带阻尼的软启动器这是PID没有办法直接做到的。2.2 扩张状态观测器把“总扰动”实时估计出来ESO是ADRC的灵魂也是最难看懂的部分。我换个角度讲假设你管理的系统是一个“黑箱”你只能看到输入端控制量u和输出端被控量y黑箱内部有复杂的物理过程、有外部扰动、还有你建模时的误差。ESO就是黑箱旁边的“审计员”它不关心黑箱内部到底是什么而是通过输入输出数据实时推算出一个等效的“总扰动”数值。ESO到底怎么工作它把二阶系统y f(y, y, w, t) b0*u中的f当成一个“扩张状态”x3也就是总扰动。然后构造一个三阶观测器用z1跟踪输出y用z2跟踪输出的导数y用z3跟踪总扰动f。只要观测器带宽w0取得合适z1就会快速逼近真实输出z3就会快速逼近总扰动。这里有个关键点控制量u前面的系数b0不需要精确等于真实输入增益只要在同一个数量级就可以。ESO会把(b - b0) * u和真实扰动一起估计进z3由控制器补偿。这个特性带来的工程价值非常大——我们做嵌入式控制的精确建模往往是奢望但有了ESO对模型精度要求可以大幅放宽。ESO的离散化实现每一拍要做的事就是根据当前的观测误差e z1 - y更新三个内部状态。看起来只是几行乘加运算但这里藏着一个非常隐蔽的坑如果u经过限幅后和实际执行值不一致ESO还是用未限幅的u计算那么观测器会“看到”一个错误输入z3开始漂移最后控制器发散。我的做法是始终使用限幅后的u进行ESO更新这个细节后面还会再强调。2.3 状态误差反馈与扰动补偿把系统变成“纯积分器串联”有了TD提供的过渡状态v1、v2有了ESO估计的状态z1、z2和总扰动z3控制器剩下的任务就比较简单了把反馈误差算出来线性组合成虚拟控制量u0再减去扰动估计最后除以b0就得到真实控制量。e1 v1 - z1 e2 v2 - z2 u0 kp * e1 kd * e2 u (u0 - z3) / b0为什么最后是“减去”z3因为系统方程是y f b0*u我们想让y等于u0就必须让f b0*u u0所以u (u0 - f) / b0用z3代替f就是(u0 - z3)/b0。符号搞反的话整个闭环会变成正反馈输出直接飞掉。这一招把原来的“被控对象模型 扰动 输出”的结构等效成了“积分器串联 微小观测误差”的结构。反馈控制器只需要对积分器串联这种最基础的对象设计kp和kd的意义一下就清楚了kp决定系统刚度kd决定阻尼。你不用再费劲辨认“这个PID参数到底在与哪个极点配对”调参直觉直接建立起来了。2.4 为什么ADRC的工程门槛其实不高很多人一看到“自抗扰”就觉得高不可攀我一开始也是这么想的。但实际上把ADRC拆成TD、ESO、SEF三个模块后每个模块单独看都很简单。TD是一个滤波器ESO是一个状态观测器SEF是一个PD控制器加前馈补偿。每一个都是控制理论入门课的内容。真正要花时间琢磨的不是推导过程而是离散化参数、采样周期、噪声处理这些工程问题。这一点和PID完全一样——PID的公式三行就写完但调起参来能让人崩溃。ADRC的优势在于它把“干扰抑制”和“跟踪性能”分离了跟踪性能主要由TD和反馈增益决定干扰抑制主要由ESO带宽决定。你调其中一个不会严重恶化另一个这是PID的根本性优势。下面这个表是我整理的三模块作用对照表可以帮助建立整体概念模块输入输出核心作用主要参数TD给定值过渡状态及微分滤波、抑制超调r, hESO输出、控制量状态估计、扰动估计抗扰、容错建模w0, b0SEF误差、扰动估计控制量反馈、补偿kp, kd3. 参数整定方法与调参实战3.1 带宽法将参数压缩成三个核心量ADRC乍一看有六个参数r、h、w0、b0、kp、kd直接比PID多了一倍很多人看到这里就劝退了。但用带宽法可以把这些参数拆成几组让调参思路变得非常清晰。先说ESO的增益。线性ESO的三个增益可以直接用带宽w0计算beta1 3 * w0 beta2 3 * w0^2 beta3 w0^3这套公式来自极点配置把观测器特征方程配置成(s w0)^3。只要w0定了三个增益就定了不需要单独调三个数。TD那边同理跟踪速度r单独负责过渡过程的锋度h一般是采样周期的两到三倍作为滤波因子也不用反复去动。所以真正要调的“大参数”只有两个半w0控制扰动估计速度和噪声放大r控制过渡过程快慢b0是控制系统对象特性的输入增益。kp、kd在ESO稳定后往往只需要给一组初步值就能工作远没有传说中那么难调。3.2 先调ESO、再调TD、最后调反馈的完整流程我调ADRC的顺序基本固定这条顺序是从多次失败教训里总结出来的第一步调ESO。把反馈控制器的kp、kd暂时设成0也就是开环状态。手动给被控对象一个恒定控制量让系统运行起来然后用串口把y、z1、z2、z3打印出来。正常情况下z1应该快速跟上yz2接近y的导数z3收敛到一个相对平稳的常数值——这个常数值就是当前控制量下的总扰动。如果z1跟不上y调大w0如果z3高频抖动调小w0。第二步调TD。给一个阶跃给定值观察v1的过渡曲线。r太小过渡过程太长影响响应速度r太大过渡过程和原始给定几乎一样失去滤波意义。一般我取“系统期望最大加速度”的0.1到1倍比如温度系统希望5秒内完成爬坡就可以根据初始误差和期望时间推一个粗略的r。第三步调反馈。恢复kp、kd先只给一个较小的kp观察系统响应。如果响应慢加大kp如果出现振荡增加kd。由于ESO已经把扰动抵消掉了这一步通常比调PID快得多10分钟基本能搞定。3.3 一个温控实例的调参过程和参数结果我拿最近一个加热器温控项目举个具体例子。控制对象加热管电压驱动0-10V反馈是PT100温度量程0-200℃控制周期10ms也就是100Hz采样。先估算b0。我手动给5V阶跃记录温度变化率把系统近似成二阶推算出输入增益大约在120附近这个值是“温度每秒平方变化 / 电压”具体推导不展开了。然后初始化参数adrc_init(adrc, 0.01f, 0.4f, 8.0f, 0.6f, 0.08f, 120.0f, 10.0f);意思是dt0.01、r0.4、w08、kp0.6、kd0.08、b0120、限幅10V。运行后的效果是设定值从50℃跳变到80℃温度曲线无超调约8秒进入稳态稳态误差±0.2℃。最让我满意的是负载突变测试在稳态时往加热腔里加入一块常温金属温度大概掉了4℃但在2秒内就被拉回设定值。之前纯PID做到这个效果至少需要加前馈补偿才能实现。3.4 参数调整的避坑经验调ESO的时候我踩过一次大坑w0从8调到20试图让扰动估计更快结果z3直接开始高频振荡整个系统发出尖锐的啸叫。原因是采样周期10ms下w020意味着w0 * dt 0.2这个值虽然勉强在线性稳定性区间内但和电磁噪声、传感器噪声耦合之后观测器就会放大高频分量。后来我把w0降到10啸叫消失扰动抑制反而更好了因为ESO不再对噪声敏感。还有一次我把b0从120改成200想着“输入增益大一点控制量再大一点”结果系统很快发散。原因是我忘了ESO会把(b - b0)*u当成扰动的一部分b0偏差过大时ESO需要更高的带宽才能追踪这个“伪扰动”带宽上去之后噪声又来了形成恶性循环。所以b0宁可小一点让ESO多干点活也不要大到让ESO“发疯”。提示调w0时留意采样周期。经验值是w0 * dt最好不超过0.3超过0.5往往就会离散化失稳。如果希望观测器很快优先缩短采样周期而不是无限调大w0。4. C语言工程实现从公式到可运行代码4.1 结构体封装一个通道一个对象工程上写控制算法最忌讳的就是大量全局变量。ADRC内部的状态变量有七八个如果全部用全局变量做多通道控制时基本没法复用代码也会越改越乱。我强烈建议用结构体把ADRC的状态和参数封装成一个对象。typedef struct { /* 采样周期相关 */ float dt; float u_max; float u_min; /* TD 状态与参数 */ float v1, v2; float r, h; /* ESO 状态与参数 */ float z1, z2, z3; float w0; float b0; /* ESO 增益预先计算节省时间 */ float beta1, beta2, beta3; /* 反馈控制参数 */ float kp, kd; /* 上一拍实际使用并限幅后的控制量 */ float u_prev; } ADRC;这样设计的好处是一眼就能看出这个“对象”有哪些内部状态。实际做多路控制时只需要定义多个ADRC变量互不干扰ADRC temp_ctrl1, temp_ctrl2;结构体传参还有一个额外的好处调试的时候方便把对象里的所有字段通过串口输出不污染全局命名空间。这在嵌入式工程里是基本职业素养我见过太多因为全局变量导致的诡异BUG了。4.2 核心函数实现fhan、fal、TD、ESO、SEF先是最速控制综合函数fhan它内部要算几次乘法和一次sqrtf是ADRC里计算量最大的部分但也是TD稳定性的关键。static float adrc_fhan(float x1, float x2, float r, float h) { float d r * h; float d0 h * d; float y x1 h * x2; float a0 sqrtf(d * d 8.0f * r * fabsf(y)); float a; if (fabsf(y) d0) { a x2 y / h; } else { a x2 0.5f * (a0 - d) * (y 0.0f ? 1.0f : -1.0f); } if (fabsf(a) d) { return -r * a / d; } else { return -r * (a 0.0f ? 1.0f : -1.0f); } }看一下里面的含义d相当于一个加速度约束下的速度阈值d0是距离阈值。|y| d0意味着当前误差足够小可以进入线性收敛段否则就按最速控制来减速。最后输出的fhan是一个加速度指令被TD用来更新内部状态。然后是非线性函数fal在经典ADRC里用于ESO和反馈。不过我实际项目基本用LESO线性ESO因为powf在MCU上开销很大而且非线性E在噪声环境下未必有明显优势。这里把fal的实现一并给出方便有需要的读者参考static float adrc_fal(float e, float alpha, float delta) { if (fabsf(e) delta) { return powf(fabsf(e), alpha) * (e 0.0f ? 1.0f : -1.0f); } else { return e / powf(delta, 1.0f - alpha); } }delta决定了“线性段”的宽度它防止e接近0时非线性函数的导数趋于无穷大从而避免在零点附近产生抖振。工程上delta一般取0.01到0.1倍量程。接下来是三个核心更新函数。TD的更新逻辑是标准的离散积分先算fhan加速度再更新v1和v2void adrc_td_update(ADRC *adrc, float ref) { float err adrc-v1 - ref; float f adrc_fhan(err, adrc-v2, adrc-r, adrc-h); adrc-v1 adrc-dt * adrc-v2; adrc-v2 adrc-dt * f; }ESO的更新用线性写法最直接也最稳定。注意内部用了限幅后的u_prev而不是外界传入的原始控制量void adrc_eso_update(ADRC *adrc, float y, float u_used) { float e adrc-z1 - y; adrc-z1 adrc-dt * (adrc-z2 - adrc-beta1 * e); adrc-z2 adrc-dt * (adrc-z3 - adrc-beta2 * e adrc-b0 * u_used); adrc-z3 adrc-dt * (-adrc-beta3 * e); }最后是完整的控制周期更新函数把TD、ESO、控制律串在一起float adrc_control(ADRC *adrc, float ref, float y) { /* 先更新TD */ adrc_td_update(adrc, ref); /* 再用上一拍实际执行的控制量更新ESO */ adrc_eso_update(adrc, y, adrc-u_prev); /* 状态误差反馈 扰动补偿 */ float e1 adrc-v1 - adrc-z1; float e2 adrc-v2 - adrc-z2; float u0 adrc-kp * e1 adrc-kd * e2; float u (u0 - adrc-z3) / adrc-b0; /* 限幅并保存限幅后的值给下一拍ESO使用 */ if (u adrc-u_max) { u adrc-u_max; } else if (u adrc-u_min) { u adrc-u_min; } adrc-u_prev u; return u; }4.3 主循环调度定时执行与控制周期抖动算法写得再好如果主循环里的调用频率不稳定整个系统还是会出问题。ADRC对采样周期的敏感度比PID高因为ESO的离散化方程里dt直接参与了积分运算如果dt抖动z3估计的扰动就会抖动。我建议在MCU上用定时器中断或者RTOS的高优先级任务来固定控制周期。不要在主循环里用delay来凑时间那会被其他任务卡住。示例调度逻辑volatile uint8_t ctrl_flag 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); ctrl_flag 1; } } int main(void) { ADRC adrc; adrc_init(adrc, 0.01f, 0.4f, 8.0f, 0.6f, 0.08f, 120.0f, 10.0f); while (1) { if (ctrl_flag) { ctrl_flag 0; float y read_temperature(); float u adrc_control(adrc, target_temp, y); set_power(u); /* 可选串口打印调试数据 */ } } }初始化函数要把beta1、beta2、beta3预先算好减少每个控制周期里的重复乘法void adrc_init(ADRC *adrc, float dt, float r, float w0, float kp, float kd, float b0, float u_max) { adrc-dt dt; adrc-r r; adrc-h 2.0f * dt; /* 滤波因子通常取2倍采样周期 */ adrc-w0 w0; adrc-kp kp; adrc-kd kd; adrc-b0 b0; adrc-u_max u_max; adrc-u_min -u_max; adrc-v1 adrc-v2 0.0f; adrc-z1 adrc-z2 adrc-z3 0.0f; adrc-u_prev 0.0f; adrc-beta1 3.0f * w0; adrc-beta2 3.0f * w0 * w0; adrc-beta3 w0 * w0 * w0; }4.4 代码优化与移植注意事项在资源紧张的MCU上ADRC可能遇到计算量偏大的问题主要开销就是sqrtf和powf。S TM32F103主频72MHz100Hz控制周期完全够用如果做到1kHz甚至更高就要考虑优化。优化优先级我的建议是用LESO代替非线性ESO去掉fal里的powfESO变成纯线性乘加计算量大幅下降实际抗扰性能在大多数工业场景下没有明显区别。开启FPU和硬件数学库如果芯片支持编译选项加上-mfpufpv5-d16 -mfloat-abihardsqrtf会快很多。很多STM32F4以上芯片自带硬件FPU不用白不用。预计算常数把beta1、beta2、beta3在初始化时算好而不是每个控制周期重复计算。如果还是卡把h设成固定倍数不参与运行期计算。移植时还有一个容易踩的坑不同MCU的浮点精度和数学库实现略有差异特别是有没有支持FTZ刷新置零模式。这些细节可能导致同一套参数在板A上稳定在板B上出现小幅度抖动。不要慌先检查控制周期是不是准确再检查ADC采样是否抖动最后看数学库的sqrtf实现。5. 常见问题与排查技巧实录5.1 ESO发散或振荡这是ADRC调试里最常遇见的症状z3不收敛甚至越来越大系统输出跟着乱飘。排查顺序我建议这样走检查w0 * dt是否过大经验上限是0.3。如果超了优先减小w0或者缩短采样周期。检查b0符号是否正确。把b0符号搞反是最隐蔽的低级错误ESO永远不可能稳定。检查是不是用了未限幅的u进入ESO。比如限幅值是10V但控制量已经到15VESO按15V积分和真实系统的执行值10V不一致z3必然疯狂漂移。检查反馈通道有没有突发噪声。传感器偶发毛刺会让ESO瞬间产生大的观测误差如果滤波没做好z3会被毛刺带飞。我自己的调试经验是先把输出限幅值设大让系统暂时不触发限幅看看ESO能不能恢复能恢复说明是限幅问题不能恢复再回头看参数。5.2 微分信号噪声被放大ADRC里z2是输出的估计微分如果传感器采回来的是带噪声的温度信号或带毛刺的电流信号z2会异常活跃。这个问题在温控里不算致命但在电机转速环里严重的微分噪声会导致电流忽大忽小听起来就像电机在“发抖”。处理办法有三个层次降低w0。这是最直接的因为ESO带宽降低对高频噪声不再敏感。但扰动估计速度也会变慢所以要在抗噪和抗扰之间找平衡。在反馈通道加一阶低通滤波。比如温控信号本身是ADC采样的可以先做一次滑动平均再送给ESO。如果w0必须很高那就只能升级传感器或信号调理电路从源头降低噪声。我之前遇到过一种特殊情况w0已经很低了z2还是抖。排查了半天发现是ADC采样没有用定时器触发采样时刻不均匀导致输入信号出现“假噪声”。换成定时器触发ADC后问题立刻消失。5.3 控制量发烫或频繁限幅如果控制器输出长时间顶在限幅值说明系统“使不上劲”。我一开始以为是kp不够大拼命加大kp结果系统开始振荡但控制量还是经常限幅。后来才意识到问题出在b0上——b0估得太小控制器以为需要很大的u才能驱动系统实际上对象根本没这么“钝”。b0的整定建议用开环阶跃实验固定输出一个控制量观察输出变化率反推输入增益。不要拍脑袋乱设。另外如果u频繁限幅ESO的估计也会跟着出问题——限幅后系统实际输入和计算输入不一致扰动估计不准确形成恶性循环。所以必须把限幅后的u_prev回传给ESO代码里已经做了这一步。关于“控制量发烫”还有一个场景执行机构是PWM驱动如果控制频率很高PWM一直在快速变化开关管损耗会增大。这时候可以在控制量输出后面加一级限速或简单的PWM占空比平滑处理对系统响应速度的影响通常可接受。5.4 低端MCU上跑不动的性能优化记录我最早在STM32F103上实现了完整非线性ADRC控制频率只能跑到500Hz左右再高就会超过10%的CPU占用影响其他任务。后来我做了三件事把控制频率提到了2kHz第一把ESO从非线性改成线性去掉了所有powf调用。第二使用单精度浮点编译器开启FPU相关的优化选项。第三把sqrtf保留在TD里——实际上TD里只有fhan需要一次sqrtf影响不大。本文还有配套的精品资源点击获取
返回列表