简介:该资源是一份可直接运行的ADRC自抗扰控制MATLAB程序,面向自动化、车辆控制方向的学习者与工程实践者,用于理解并复现不依赖精确模型的控制方案,尤其聚焦汽车引擎控制这一典型场景。程序参考韩京清教授的自抗扰理论,将扩展状态观测器作为核心环节,在线估计系统状态与未知扰动并加以补偿,从而提升系统在不确定性、时变参数与内外干扰下的稳定性,可与传统PID控制形成对照。压缩包共1个文件,为m脚本,体积约563B,体量轻巧,适合直接放入MATLAB中阅读、修改与仿真验证。资源已有1934人学习下载,说明该实现受到一定关注。通过这份材料,读者可以掌握ADRC的基本结构与参数调试思路,理解ESO的估计与补偿机制,并借助已通过仿真验证的案例,为车辆控制或其他抗扰场景的建模与仿真提供可借鉴的起点。
1. 从台架上那个「跟不上也压不住」的二阶对象说起
台架上一个二阶对象,给定阶跃,输出慢悠悠爬上来,中途突加一个负载,直接掉一截,回弹还带两下振。比例项加大就抖,减小又软得像面条,PID 三个参数来回改了两天。这类现场我见得不少,问题往往不在参数上,而在于我们把「模型说不清的那部分」当成噪声去躲——ADRC 自抗扰反过来,把它当成一个能估出来的状态,估出来再一次性减掉。
这份程序就是围绕这个思路写的:MATLAB 脚本加 Simulink 模型两套入口,跟踪微分器 TD、扩张状态观测器 ESO、非线性状态误差反馈 NLSEF 三件套齐全,被控对象是二阶系统,仿真已经跑通。落到汽车上,这个二阶对象能对应四分之一车主动悬架的簧上质量位移,也能对应驱动电机转速环、车速跟随回路。适合手上有二阶对象、模型不精确、又想同时把跟踪速度和抗扰做掉的人,更适合拿它当模板改成自己对象的人。
2. 工程骨架:三件套分别落在哪个文件、信号怎么串起来
2.1 目录结构与每个文件的职责
拿到压缩包先别急着点运行,花两分钟看一眼目录,后面改什么、动哪里心里就有数了。
adrc_demo/ ├── run_sim.m # 主入口:初始化、循环推进、出图,一条命令跑完 ├── params_adrc.m # 所有可调参数集中在这,改参只动这一个文件 ├── adrc_core/ │ ├── fhan.m # 最速控制综合函数,TD 的内核 │ ├── td.m # 跟踪微分器:出 v1、v2,同时把给定信号柔化 │ ├── fal.m # 非线性函数,ESO 与 NLSEF 共用 │ ├── eso.m # 扩张状态观测器:出 z1、z2、z3 │ └── nlsef.m # 非线性状态误差反馈:出 u0 ├── plant/ │ └── plant_step.m # 二阶被控对象,换对象只改这一个文件 ├── models/ │ └── adrc_sim.slx # Simulink 版本,接线和脚本一一对应 └── results/ └── plot_result.m # 跟踪曲线、误差、控制量、z3 四张图拆成这个粒度不是为了好看。控制器四个函数、对象一个函数、参数一个文件,边界划得很干净:调参只碰params_adrc.m,换被控对象只碰plant/,核心算法那一层基本不用动。我见过太多把 TD、ESO、对象、画图全塞进一个两百行的main.m里的写法,第一次改还凑合,第二次想对比两个对象的响应,就得整段复制粘贴,改到第三版自己都分不清哪个是哪个。
另外注意params_adrc.m里没有任何计算逻辑,全是赋值。这个约定在后期做参数扫描的时候特别值钱——写个外层 for 循环反复调用它,只改 b0 或 wo,其余一行不动。
2.2 一条信号从给定走到执行器
整个 ADRC 的信号流其实就是五个变量在转圈,理清楚了代码基本就懂了一半。
| 变量 | 物理含义 | 从哪来 |
|---|---|---|
| v1 / v2 | 柔化后的给定、给定的微分 | TD 输出 |
| z1 / z2 | 输出量及其一阶导的估计 | ESO 输出 |
| z3 | 总扰动的估计 | ESO 扩张出来的那一位 |
| e1 / e2 | 位置误差、速度误差 | v1-z1、v2-z2 |
| u0 | 不含扰动补偿的「名义控制量」 | NLSEF 输出 |
| u | 最终给到执行器的控制量 | (u0 - z3) / b0 |
给定v(k)进 TD,得到平滑过的v1和它的微分v2;量测y(k)和控制量u(k-1)一起进 ESO,得到z1 z2 z3;两个误差e1 = v1 - z1、e2 = v2 - z2送进 NLSEF 得到u0;最后一步用(u0 - z3)/b0把扰动抵掉,限幅之后喂给对象。
关键在z3这一位。它不是 PID 里的积分项——积分项是拿误差慢慢攒,攒到够了才起作用;z3是「总扰动」的实时估计,模型里没写的摩擦、没建模的高阶动态、外部突加负载,全被打包进这一个状态里。所以它的量纲是「最高阶导数」级别的,必须除以b0才能换算成控制量。理解这一点,后面调beta和b0就不会凭感觉瞎试。
2.3 为什么全部写成离散递推而不是连续模块
车用控制器最后都要落到 ECU 的定步长任务里,5ms 或 10ms 一个周期,连续模块只能活在仿真里。这份程序全程离散递推:h是积分步长同时就是控制周期,h0是 TD 的滤波因子,两者独立。Simulink 那边也把 solver 设成 Fixed-step discrete,步长和h对齐,脚本和模型两条路才能对得上数。
h怎么定:比对象最快时间常数小 5 到 10 倍。悬架簧上质量的固有频率在 1~2Hz 量级,10ms 绰绰有余;电机电流环时间常数只有几毫秒,就得 1ms 甚至更小。h0一般取h的 5~20 倍,它是压微分噪声的旋钮,取太大跟踪会变钝,取太小等于没滤波。
b0是另一个必须提前想清楚的量。二阶对象写成y'' = f + b*u,b0就是b的估计值。常见做法是先在标称工况下打一次阶跃,用稳态增益反推,再留 20% 余量。符号千万不能错——符号反了 ESO 会往反方向补偿,系统直接变正反馈,这是新手最容易翻的车,第 5 章第一条就讲这个现象。
3. 核心代码逐段读:fhan、ESO、NLSEF 的三段实现
3.1 TD 与 fhan:最速跟踪的离散写法
TD 的作用有两个:把阶跃给定柔化成一条加速度有界的曲线,顺便把给定的一阶导也解出来,省掉对误差直接求导这种自杀式操作。
function f = fhan(x1, x2, r, h) %FHAN 最速控制综合函数 % x1: 位置偏差 x2: 速度 r: 跟踪速度因子 h: 滤波因子 d = r * h^2; a0 = h * x2; y = x1 + a0; a1 = sqrt(d * (d + 8 * abs(y))); a2 = a0 + sign(y) * (a1 - d) / 2; sy = (sign(y + d) - sign(y - d)) / 2; a = (a0 + y - a2) * sy + a2; sa = (sign(a + d) - sign(a - d)) / 2; f = -r * (a / d - sign(a)) * sa - r * sign(a); endfunction [v1, v2] = td(v, v1, v2, r, h, h0) %TD 离散跟踪微分器,一步递推 % v: 本拍给定 v1/v2: 上一拍状态 r: 跟踪速度 h: 步长 h0: 滤波因子 f = fhan(v1 - v, v2, r, h0); % 第一项是 v1 - v,不是 v - v1 v1 = v1 + h * v2; v2 = v2 + h * f; endr决定跟踪快慢,越大越快,但大到一定程度就失去柔化意义,等于把阶跃原样塞进去。实操里按「给定信号的最大加速度」来估,从 100 起步往上加,看v1曲线还圆不圆滑。h0是抗噪旋钮,和h解耦,这是这份实现比很多简化版严谨的地方——简化版里直接令h0 = h,噪声一大v2就没法看了。
注意:
fhan第一个入参写成v1 - v还是v - v1,不同资料里两种写法都出现过,混用会让v2的整体符号反过来。表现出来就是「给定上升时微分是负的」,然后e2项把控制量往回拉,阶跃响应上先看到一下反向冲击。我第一次照着抄就栽在这,对符号对了半小时。
3.2 ESO:把「说不清的部分」塞进 z3
function f = fal(e, alpha, delta) %FAL 小误差线性、大误差幂次,兼顾零点平滑与收敛速度 if abs(e) <= delta f = e / delta^(1 - alpha); else f = abs(e)^alpha * sign(e); end end function [z1, z2, z3] = eso(y, u, z1, z2, z3, b0, beta, alpha, delta, h) %ESO 三阶扩张状态观测器(非线性 fal 版) % y: 本拍量测 u: 上一拍控制量 z1/z2/z3: 上一拍观测状态 e = z1 - y; fe = fal(e, alpha(1), delta); fe1 = fal(e, alpha(2), delta); z1 = z1 + h * (z2 - beta(1) * e); z2 = z2 + h * (z3 - beta(2) * fe + b0 * u); z3 = z3 + h * (-beta(3) * fe1); end三个beta的整定不用盲试。线性版有现成的带宽公式:设观测器带宽为wo,则beta1 = 3*wo、beta2 = 3*wo^2、beta3 = wo^3,把三个参数压缩成一个,调起来省事。非线性版一般在这个基础上乘 0.8~1.2 微调,先线性跑通再切非线性,是稳的路子。
wo取多少合适:控制带宽wc的 3 到 5 倍。观测器必须比控制器快,不然扰动估出来的时候已经晚了,补偿上去反而添乱。delta是fal的线性区半宽,取 0.01~0.1 量级;太小则零点附近斜率极大,z3出毛刺;太大会让z3变钝,抗扰能力下降。
还有一点:eso里的u必须是上一拍的控制量。这个执行顺序在脚本里容易写对,搬到 Simulink 或者 C 代码里就特别容易错,第 5 章会专门说。
3.3 NLSEF 与扰动补偿:u0 到 u 的最后一步
function u0 = nlsef(e1, e2, alpha, delta, k) %NLSEF 非线性状态误差反馈 u0 = k(1) * fal(e1, alpha(3), delta) + k(2) * fal(e2, alpha(4), delta); endu0 = nlsef(v1 - z1, v2 - z2, alpha, delta, k); u = (u0 - z3) / b0; % 扰动补偿:估出来的总扰动直接抵掉 u = max(umin, min(umax, u)); % 执行器限幅,实车上这一层不能省u0回答的是「如果没有扰动,该给多少」,z3/b0回答的是「为了抵掉扰动,还得多给多少」,两者一减就是最终控制量。这也是 ADRC 和 PID 最本质的差别:PID 靠积分项慢慢试错,试到误差归零为止;ADRC 直接把扰动估出来一次抵掉,所以扰动恢复快,也不存在积分饱和那一堆麻烦事。
k(1)、k(2)形式上像 PD 增益,但作用在fal之后的误差上:小误差时fal的等效增益自动变大,精度上得去;大误差时增益被压住,不容易超调。初值可以用k = [wc^2, 2*wc]给,正好对应一个阻尼比 1 的二阶闭环。
限幅那一行别嫌多余。仿真里执行器是无限的,实车上力、电流、力矩都有顶,一旦顶住而 ESO 还在往上估,z3会越飘越远,退出饱和之后要好几拍才缓过来。加上限幅,至少能让现象暴露在曲线上。
4. 跑起来:从参数文件到出图的一条完整链路
4.1 参数文件怎么填
% params_adrc.m —— 全部可调参数集中在这 h = 0.01; % 仿真步长 = 控制周期 10ms T = 5; % 仿真时长 b0 = 1.2; % 控制增益估计,符号必须和对象一致 r = 200; % TD 跟踪速度因子 h0 = 0.05; % TD 滤波因子,一般取 5~20 倍 h wc = 8; % 期望闭环带宽 rad/s wo = 4 * wc; % 观测器带宽,取控制带宽的 3~5 倍 beta = [3*wo, 3*wo^2, wo^3]; % 带宽法给的初值 k = [wc^2, 2*wc]; % NLSEF 增益初值 alpha = [0.5, 0.25, 0.75, 1.25]; % fal 的四个幂次 delta = 0.05; % fal 线性区半宽 umin = -50; umax = 50; % 执行器限幅,按实际执行器填alpha四个值分两组用:前两个给 ESO,后两个给 NLSEF。ESO 那边习惯取 0.5 和 0.25,NLSEF 这边取 0.75 和 1.25,这套组合在二阶对象上比较通用,但别当成圣旨——对象阶次变了要重调。b0建议先用一次开环阶跃实测反推,比拍脑袋填靠谱得多。
4.2 主仿真脚本逐步讲
% run_sim.m params_adrc; N = round(T / h); v1 = 0; v2 = 0; z1 = 0; z2 = 0; z3 = 0; u = 0; y = 0; log = zeros(N, 6); for kk = 1:N t = kk * h; v = ref_signal(t); % 给定:阶跃 + 中途扰动 [v1, v2] = td(v, v1, v2, r, h, h0); [z1, z2, z3] = eso(y, u, z1, z2, z3, b0, beta, alpha, delta, h); u0 = nlsef(v1 - z1, v2 - z2, alpha, delta, k); u = (u0 - z3) / b0; u = max(umin, min(umax, u)); y = plant_step(y, u, h); % 对象推进一步 log(kk, :) = [t, v, y, u, z3, v1]; end plot_result(log);顺序是有讲究的:eso用的是循环里上一轮留下的u,而u在eso之后才更新,正好对应「本拍观测用上拍控制量」。如果图省事把u的计算挪到eso前面,仿真里往往也能跑出差不多的曲线,但搬上实车就会自激——这是最阴的一类 bug,仿真阶段完全看不出来。
被控对象长这样,换对象就是换这个函数:
function y = plant_step(y, u, h) % 二阶对象:y'' = -a0*y - a1*y' + b*u + d persistent x1 x2 if isempty(x1), x1 = 0; x2 = 0; end a0 = 4; a1 = 0.6; b = 1.0; d = 0; % d 可以设成中途突加的负载 x2 = x2 + h * (-a0*x1 - a1*x2 + b*u + d); x1 = x1 + h * x2; y = x1; end这里用了前向欧拉,h = 0.01对a0 = 4的对象够用。对象再快一点就得换 RK4,否则离散化误差会混进观测结果里,你会误以为是 ESO 没调好。
4.3 Simulink 版本的接线方式
模型里最省事的做法是用一个 MATLAB Function 块把整个控制器包起来:输入v和y,输出u,块内部用persistent存v1 v2 z1 z2 z3 u_prev。这样一个块就顶掉五个子系统,接线只有三根线,改起来也快。
solver 一定设成 Fixed-step discrete,固定步长填h,别用变步长。变步长下 ESO 的递推次数和时间不对应,z3的物理意义就丢了,曲线看着还行,数值全是错的。
Scope 至少接三路:v和y叠在一起看跟踪,u单独一路看有没有抖振和饱和,z3单独一路看扰动估得准不准。这三张图基本能覆盖 90% 的调参判断。
4.4 怎么判断「跑对了」
| 检查项 | 合格表现 | 不合格先看哪 |
|---|---|---|
| 跟踪 | 上升时间约 1/wc 量级,超调小于 5% | TD 的 r 是否过大 |
| 稳态 | 阶跃稳态误差接近 0 | beta3 是否偏小、u 是否顶限幅 |
| 抗扰 | 扰动加入后 2~3 个 1/wc 周期回到 5% 带内 | wo 是否够快、b0 是否偏 |
| z3 | 收敛到实际扰动量级,曲线平滑 | delta 是否过小、h0 是否缺失 |
| u | 无持续等幅振荡 | 有振荡基本就是自激,查符号 |
最后一行值得单独说:等幅振荡在控制系统里几乎等于「环路符号错了」或者「相位滞后太多」,别指望靠调小增益压下去。压下去了也只是把振荡藏起来,扰动一来照样发散。
5. 避坑排查:仿真发散、观测器抖振和 b0 填错的现场记录
5.1 仿真跑三步就发散,u 一路顶到限幅
现象:前 0.1 秒还算正常,之后输出指数放大,控制量直接撞上umax不动了。
原因:九成是b0符号写反,或者beta(3)大了整整一个量级。b0符号反了,z3/b0的补偿方向就是反的,负反馈变正反馈,发散是必然的。
解决:先把z3强制清零跑一次,看纯 NLSEF 能不能稳住。能稳住说明问题在观测器侧,回头单独核对b0符号和wo的量级;稳不住说明 NLSEF 的k本身就给大了,先把wc降下来。
5.2 z3 曲线像毛刺,控制量跟着一起抖
现象:z3高频抖动,u跟着抖,实车上会听到执行器发出响声。
原因:delta给得太小,fal在零点附近的斜率极大,量测噪声被放大;或者h0干脆没设,等于h0 = h,TD 的微分信号把噪声全带进来了。
解决:delta从 0.01 加到 0.05~0.1,h0取 10 倍h。还抖就把 ESO 临时换成线性版(fal换成直接乘e)跑一遍——线性版跑得干净就说明是非线性函数参数的问题,不是结构问题。
5.3 跟踪上了但永远差一点,稳态误差去不掉
现象:输出稳定在给定下方 3%,怎么调k都下不去。
原因:ADRC 里没有积分器,稳态误差全靠z3顶着。beta(3)太小,常值扰动估不到位;或者u已经被限幅卡死,补偿量根本给不出去。
解决:先看u有没有贴着umax,贴住了就是执行器能力不够,调参救不了。没贴住就把wo提上去。这点和 PID 的直觉完全相反,用惯了 PID 的人第一反应总是去加积分,加完发现更糟。
5.4 仿真好使,代码搬到控制器上就振
现象:同一套参数,Simulink 里干干净净,C 代码烧进去就开始周期性抖。
原因:两个可能——采样周期和h对不上;或者 ESO 用了本拍u,执行顺序反了。
解决:把h改成和 task 周期严格相等(不是「差不多」),eso的调用放在u计算之前。这个顺序在脚本里是天然对的,搬到嵌入式里就特别容易被打乱,尤其是几个任务并行的时候。
5.5 给定一变,控制量先往反方向打一下
现象:阶跃来的时候u先反向冲一下,然后再转正。
原因:TD 里fhan(v1 - v, ...)写成了fhan(v - v1, ...),v2的整体符号反了,e2项把控制量往回拽。
解决:对着符号检查一遍。快速定位的办法是把k(2)临时置零,如果反向冲击消失,就是这个坑。v2的符号单看曲线也能发现——给定上升时v2应该是正的。
6. 二次开发:换对象、加回路和一套验证流程
6.1 换被控对象只动三处
第一处是plant/plant_step.m,把里面的a0 a1 b换成你对象的实际参数,或者直接换成从数据辨识出来的差分方程。第二处是params_adrc.m里的b0,用新对象的开环阶跃重新测一次,别沿用旧值。第三处是umin/umax,按新执行器的物理上限填。控制器四个函数一行都不用改,这是前面那套目录结构的直接回报。
6.2 从单回路扩到多回路
多回路不是简单复制几份,b0的估法每个场景都不一样:
| 场景 | 被控量 | b0 量级 | 要额外注意 |
|---|---|---|---|
| 四分之一车主动悬架 | 簧上质量位移 | 约 1/ms | 作动器带宽与力饱和 |
| 驱动电机转速环 | 转速 | 约 Kt/J | 电流环必须显著快于转速环 |
| 车速跟随 | 车速 | 随挡位、载荷变 | 建议分挡位给 b0 或在线辨识 |
| 转向电流环 | 相电流 | 约 1/L | PWM 死区与采样延迟 |
b0随工况变化大的场景,可以做成增益调度:按挡位或载荷查表给不同的b0,ESO 那边不用动,它自己会把剩下的偏差当扰动消化掉——这正是 ADRC 比依赖精确模型的方法好用的地方。
6.3 三组对照实验做验证
改完之后我一般固定跑三组:标称工况阶跃,看跟踪;参数摄动 ±30%(质量、电感、增益),看鲁棒;中途突加负载,看恢复时间。每组记三个数——上升时间、最大超调、扰动恢复时间,三组数据放在一张表里对比,改动的收益和代价一目了然。
我现在每改一次b0或wo,都强制先跑一遍「z3强制清零」的对照组,看纯反馈能不能稳住,再放开观测器。这个习惯是从 5.1 那条坑里换来的,当时查了大半天才怀疑到符号上。控制这东西,能拆开单独验证的环节就别合在一起调。希望帮到你。
本文还有配套的精品资源,点击获取