简介:自抗扰控制(ADRC)程序面向汽车引擎控制方向的学习者与初入控制算法实践的工程师,提供一套已在MATLAB中完成仿真验证的控制器实现,可用于理解ADRC如何在不依赖精确系统模型的前提下估计并补偿扰动。对控制算法入门者而言,可借此建立对自抗扰控制框架的直观认识。压缩包为zip格式,内含1个m脚本文件,体积约563B,体量轻便,适合直接阅读与二次修改;脚本围绕扩展状态观测器展开,体现韩京清自抗扰理论在汽车引擎控制场景中的落地思路。目前已有1934人学习下载,具备一定参考热度。读者可从中获得ADRC核心环节的实现样例、ESO状态与扰动估计的基本写法,以及将控制理论转化为仿真程序的案例,便于对照学习控制器结构、参数调试与仿真验证流程。
1. 从一个调不好的巡航车速环说起:这套 adrc自抗扰 仿真程序能干什么
某开发者做整车纵向巡航,PID 在平路上跟得很稳,一到长坡就掉速三公里,回头把积分拉大,平路又开始来回窜。这套 adrc自抗扰 仿真程序就是冲着这种“模型说不清、扰动躲不掉”的工况写的。它用 MATLAB 纯脚本搭了一条完整的自抗扰回路:跟踪微分器先给目标车速安排一段过渡过程,扩张状态观测器把坡道、风阻、载荷变化统统塞进一个“总扰动”里实时估出来,非线性反馈再把这坨扰动抵掉。适合正在做汽车纵向速度控制、又不想把整车模型辨识到小数点后三位的开发者,也适合刚接触自抗扰、想找一份能直接跑出曲线的代码上手的人。整份程序不依赖任何工具箱,几十行核心代码,改两个参数就能换对象。
2. 拆开程序看骨架:TD、ESO、NLSEF 三段式与工程目录
自抗扰最容易被讲玄的地方,是它被拆成一堆听起来很唬人的名词。落到代码里其实就三个函数,各自干一件很朴素的事:TD 负责“目标别突变”,ESO 负责“扰动我来猜”,NLSEF 负责“猜出来就抵掉”。这套仿真程序就是照这个分工切的文件,一个函数一个文件,没有嵌套调用,也没有藏在回调里的黑匣子,你可以从主脚本一行一行往下读,读到哪一步就知道当前在算哪一段。
2.1 目录结构与调用顺序
工程目录长这样,没有多余的东西:
adrc_sim/ ├── run_sim.m % 主脚本,一键跑完并出图 ├── init_params.m % 所有参数、对象参数、工况时间表 ├── plot_result.m % 四条曲线的绘制与指标打印 ├── adrc_core/ │ ├── fhan.m % 最速控制综合函数,TD 的内核 │ ├── td_fhan.m % 二阶跟踪微分器 │ ├── fal.m % 非线性函数,ESO 和 NLSEF 共用 │ ├── eso2.m % 二阶扩张状态观测器 │ └── adrc_step.m % 单步控制律组装,对外只暴露这一个入口 └── plant/ └── veh_speed.m % 整车纵向动力学,被控对象调用链是单向的:run_sim.m每个采样周期调一次adrc_step.m,adrc_step.m内部依次调td_fhan.m和eso2.m,两个函数又都依赖fhan.m和fal.m。这种“一个总入口 + 若干纯函数”的写法有个实在的好处:你想换成 Simulink 的 MATLAB Function 模块,直接把adrc_step.m的函数体粘进去就能用,状态用persistent或者 Data Store Memory 接一下即可,不用重写逻辑。
我一般会把adrc_core整个目录加进 MATLAB 路径(addpath(genpath('adrc_core'))),这样换工作目录也不会报“未定义函数”。这件事听着很小,但第一次跑别人的代码,十次里有三次是路径没加对。
2.2 三个核心函数各自的职责边界
把职责说清楚,后面调参才不会乱。很多教程讲 ADRC 讲成一团,是因为没划清边界。下面这张表是我自己梳理的,也对应代码里的函数签名:
| 模块 | 输入 | 输出 | 真正负责的事 | 不负责什么 |
|---|---|---|---|---|
TD(td_fhan.m) | 目标值ref、上一拍状态v1,v2 | 平滑参考v1及其导数v2 | 把阶跃变成有限斜率的过渡过程,压超调 | 不知道对象是什么,也不看反馈 |
ESO(eso2.m) | 量测y、上一拍控制量u_prev | 状态估计z1、总扰动估计z2 | 把模型误差、外扰、未建模动态合成一个量估出来 | 不参与决策,只“报告” |
NLSEF(adrc_step.m内的u0) | v1、z1、z2 | 控制量u | 用误差算控制力,再用z2/b0抵掉扰动 | 不做限幅之外的任何保护 |
一个常见的误解是“ESO 能估扰动,所以对象模型可以随便写”。不对。ESO 估的是你写成b0*u之后剩下的那部分,所以b0的量级必须先对,符号更不能错,否则观测器从第一拍就开始跑偏。这也是后面避坑章节里最常出现的一条。
2.3 参数集中管理:一份 init_params.m 解决什么
参数全部塞进一个结构体p,所有函数都靠p.xxx取值,函数签名干净,也不会出现“这个 0.5 是哪来的”这种考古问题。
function p = init_params() % 全部参数集中在这里,改这一处,全工程生效 %% 仿真步长与时长 p.h = 0.01; % 采样步长 s,必须与积分步长一致 p.T = 60; % 仿真总时长 s %% 整车参数(名义值,用于算 b0) p.m = 1500; % 整车质量 kg p.g = 9.81; % 重力加速度 p.f = 0.015; % 滚动阻力系数 p.rho = 1.225; % 空气密度 kg/m^3 p.Cd = 0.32; % 风阻系数 p.Af = 2.2; % 迎风面积 m^2 p.m_true = 1800; % 真实质量,故意比名义值大 20%,用来验证 ESO 的鲁棒性 %% 被控对象输入 p.b0 = 1 / p.m; % 控制增益名义值,u 是驱动力 N,量纲 1/kg p.umax = 8000; % 驱动力限幅 N %% ESO 参数(带宽参数化,w_o 越大估得越快、越怕噪声) p.w_o = 6; p.beta1 = 2 * p.w_o; % 二阶 ESO 极点配置 (s+w_o)^2 p.beta2 = p.w_o^2; p.delta = 0.05; % fal 的线性区宽度,按噪声幅值取 %% 控制器参数 p.kp = 1.2; % 一阶对象用 P 就够,闭环时间常数约 1/kp p.delta_u = 0.1; % NLSEF 里 fal 的线性区宽度 p.ref_speed = [0 20 20 25 25 15]; % 目标车速关键点 m/s p.ref_time = [0 1 25 26 40 41]; % 对应时刻 s end说明几个关键取值。p.b0 = 1/p.m是因为控制量u直接取驱动力(单位 N),除以质量就是加速度,量纲对得上。p.beta1、p.beta2用带宽参数化写,是为了让你只调一个w_o而不是到处试三个数,这个手法在第 6 章还会展开。p.m_true故意和p.m差 20%,就是给 ESO 出难题:如果你把b0设成真实值的倒数,这组仿真反而看不出自抗扰的价值。
提示:
p.h同时被控制器和对象积分使用,这一条是刻意设计的。后面你会看到,只要这两处步长不一致,曲线立刻开始飘,这是新手最容易忽略的一类错。
3. 核心代码逐段读:跟踪微分器、扩张状态观测器与非线性反馈
三个函数加起来不到四十行,但没有一行是凑数的。这一章按信号流顺序读:先看目标信号怎么被“软化”,再看反馈怎么被“消化”,最后看两者怎么合成控制量。
3.1 最速跟踪微分器 fhan:把阶跃变成一段可用的过渡过程
fhan(最速控制综合函数)是整份代码里公式最长的一个,也是唯一一个不建议自己手推的。它的作用是:给定当前位置和速度,算出“以最短时间、不超调地到达目标”所需的最优加速度。
function f = fhan(x1, x2, r, h) % 最速控制综合函数: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; % 判断 y 是否落在 |y|<d 的过渡区 a = (a0 + y - a2) * sy + a2; % 过渡区内取 a2,区外取 a0+y sa = (sign(a + d) - sign(a - d)) / 2; f = -r * (a / d - sign(a)) * sa - r * sign(a); % 线性区用 a/d,饱和区直接给 ±r end这段的核心不是那几个sign,而是a的分段构造:当误差还很大时,函数输出饱和的 ±r,也就是“全速冲”;当误差落进以d = r*h^2划定的窄带里时,输出切换成线性比例,避免在目标附近来回抖。r是唯一需要调的参数,物理含义是“最大加速度”,单位是 m/s²(车速环里)或者 V/s²(电压环里)。
包在它外面的是一层两行代码的跟踪微分器:
function [v1, v2] = td_fhan(v1, v2, v, r, h) % 二阶跟踪微分器:v1 跟踪目标 v,v2 是 v1 的导数 fh = fhan(v1 - v, v2, r, h); % 只把“误差”喂进去 v1 = v1 + h * v2; % 欧拉积分:位置 v2 = v2 + h * fh; % 欧拉积分:速度 end注意fhan的第一个入参是v1 - v而不是v1,这是把“目标当作原点”处理,等价于把系统平移到误差坐标下求解,代码上少一次减法。写到控制器里之后,v1就是平滑后的目标车速,v2是它的变化率,后者可以直接当前馈用,也可以只拿来看——在这个一阶对象的速度环里,v2主要起诊断作用,如果v2抖得厉害,说明r给太大了。
r的取值有个经验:目标车速从 0 跳到 20 m/s,如果你希望 3 秒左右跟上,r取 20/3 的量级、再放大两三倍,实际我一般从 10 开始试。r太大,TD 输出就接近阶跃,等于白加;r太小,跟踪会明显滞后,闭环响应看起来“肉”,容易被误判成控制器不给力。
3.2 二阶扩张状态观测器:把坡道和风阻一起估出来
这里最关键。整车纵向动力学的名义模型是dv/dt = b0*u + f,其中f包含了滚动阻力、风阻、坡道分量、质量误差、传动损失等等一切你不想建模的东西。ESO 的任务就是让z1咬住实测车速,让z2咬住f。
function [z1, z2] = eso2(y, u, z1, z2, p) % 二阶扩张状态观测器 % y 本拍量测车速 m/s % u 上一拍实际施加的驱动力 N(注意是“上一拍”) % z1 车速估计,z2 总扰动估计(量纲 m/s^2) e = z1 - y; % 观测误差 z1 = z1 + p.h * (z2 - p.beta1 * e + p.b0 * u); % 状态通道 z2 = z2 + p.h * (-p.beta2 * fal(e, 0.5, p.delta)); % 扰动通道 end逐项说。e = z1 - y的符号是“估计减实测”,所以后面beta1*e前面是负号,这是负反馈;如果写成y - z1,那beta1前面就得是正号,两种写法都对,但混用就一定发散,这是抄代码时最常见的翻车点。z2那一行用了fal而不是线性增益,目的是在小误差时把增益压下去,噪声大的时候不至于让z2跟着量测抖。
fal本身很朴素:
function f = fal(e, alpha, delta) % 非线性函数:|e|<=delta 时退化为线性,否则按 |e|^alpha 缩放 if abs(e) <= delta f = e / delta^(1 - alpha); % 线性区,斜率与幂函数在 delta 处相切 else f = sign(e) * abs(e)^alpha; % 幂次区,小误差增益大、大误差增益小 end endalpha取 0 到 1 之间,越小越“非线性”,z2通道常用 0.25 或 0.5,z1通道一般直接用线性(等价alpha = 1)。delta是线性区半宽,取值要跟量测噪声幅值对得上:噪声标准差如果是 0.02 m/s,delta取 0.05 到 0.1 比较稳,取 0.001 就等于把噪声原封不动放大进z2。
还有一个容易忽略的细节:eso2里用的是u而不是u_prev命名对了但传参传错。控制量必须先限幅、再进观测器,观测器看到的必须是对象真正吃到的那个值。如果你在观测器之后才限幅,那么控制量一顶到饱和,z2立刻开始错误地累积,等退出饱和时车速会莫名其妙地窜一下。
3.3 非线性误差反馈与扰动补偿:u = u0 - z3/b0
最后一步是把前面两块的输出合起来。这段代码短,但顺序不能乱:先更新 TD,再更新 ESO,最后算控制量。
function [u, td, eso] = adrc_step(ref, y, u_prev, td, eso, p) % 单步 ADRC:对外只暴露这一个函数,Simulink 里也是粘这一段 % 1) 安排过渡过程 [td.v1, td.v2] = td_fhan(td.v1, td.v2, ref, p.r, p.h); % 2) 估计状态与总扰动(喂进去的是上一拍真正执行的控制量) [eso.z1, eso.z2] = eso2(y, u_prev, eso.z1, eso.z2, p); % 3) 误差反馈:一阶对象用 P 就够,fal 让大误差时增益不至于过猛 e1 = td.v1 - eso.z1; u0 = p.kp * fal(e1, 0.75, p.delta_u); % 4) 扰动补偿:把 z2 折算成控制量抵掉,b0 必须与对象标称增益一致 u_raw = (u0 - eso.z2) / p.b0; % 5) 限幅,注意限幅后的值才是下一拍进观测器的 u u = min(max(u_raw, -p.umax), p.umax); end把第 4 步代回对象方程就能看懂补偿的意义:dv/dt = b0*u + f = b0*(u0 - z2)/b0 + f = u0 + (f - z2)。只要z2估得准,括号里那一项就趋近于零,闭环就退化成dv/dt = u0 = kp*e1,一个干净的一阶惯性环节,时间常数约1/kp。这就是自抗扰“把不确定对象变成确定积分器”的全部秘密,没有更玄的东西。
kp的取法很直接:想让车速跟上目标的时间常数是 0.8 秒,kp就取 1.25。u0那一步用fal而不是纯线性,是因为大误差时(比如目标从 20 跳到 25)线性 P 会给出一个很大的u0,再除以b0之后驱动力直接顶到 8000 N 的限幅,反而拖慢恢复;fal把大误差段的等效增益压下来,控制量更早退出饱和。alpha取 0.75、delta_u取 0.1,是我在这套参数下试出来比较平衡的一组,你可以从纯线性(alpha = 1)开始,确认能跑通再换成非线性。
注意:一阶对象(车速环)只需要 P,不要照抄二阶模板硬加
kd*(v2 - z2)项。加了之后相当于给一个一阶系统串了个微分,噪声一大立刻表现为驱动力高频抖动,很多人以为是 ESO 参数没调好,其实是多了一项不该有的东西。
4. 把仿真跑起来:从空文件夹到三条曲线的完整操作
代码读完了,接下来是真跑。这套程序不依赖 Simulink、不依赖 Control System Toolbox,标准 MATLAB 装完就能跑,这一点对只是想验证算法的场景很友好。
4.1 环境与文件放置
把adrc_sim整个文件夹放到任意英文路径下,路径里不要有中文和空格——MATLAB 对含空格路径的处理时好时坏,尤其涉及addpath和genpath的时候。然后在命令行里做两件事:
cd('D:/work/adrc_sim'); % 切到工程根目录 addpath(genpath(pwd)); % 把 adrc_core 和 plant 一起加进搜索路径 which adrc_step % 应该返回 .../adrc_core/adrc_step.m,返回空就是没加对which那一行是自检,返回路径说明路径通了;如果返回'adrc_step' not found,先别急着往下跑,八成是genpath之前cd没成功,或者文件夹名字拼错了。我见过不止一次是解压出来多了一层同名目录,结果adrc_core变成了adrc_sim/adrc_sim/adrc_core。
4.2 主脚本的装配顺序
主脚本是整份代码的“说明书”,它的结构决定了你改起来顺不顺手。
%% run_sim.m 一键跑完整个仿真并出图 clear; clc; close all; p = init_params(); % 所有参数集中在这里 % ---- 状态初始化 ---- x.v = 0; % 车速 m/s td.v1 = 0; td.v2 = 0; % TD 状态 eso.z1 = 0; eso.z2 = 0; % ESO 状态 u_prev = 0; % 上一拍实际控制量 N = round(p.T / p.h); % 总步数 log = zeros(N, 6); % [t, ref, v, z2, u, 扰动量] for k = 1:N t = (k - 1) * p.h; ref = ref_profile(t, p); % 目标车速 [u, td, eso] = adrc_step(ref, x.v, u_prev, td, eso, p); % 控制律 x.v = veh_speed(x.v, u, t, p); % 对象推进一步 u_prev = u; % 关键:存真正执行的值 log(k, :) = [t, ref, x.v, eso.z2, u, disturbance(t, p)]; end plot_result(log, p);ref_profile就是按p.ref_time、p.ref_speed做线性插值,十行以内,不必单开文件。这里要点出三处顺序上的讲究:状态初始化必须把td和eso的内部状态一起置零,只置x.v会在第一拍得到一个巨大的e,虽然能恢复,但图上前 0.2 秒会很难看;u_prev存的是限幅之后的u,不是u_raw;log里单独存了一列z2,后面判断观测器有没有跟上全靠它。
4.3 被控对象模型:整车纵向动力学怎么写
对象模型不用复杂,但阻力的三项要齐,否则 ESO 没有东西可估,你也就看不到自抗扰的价值。
function v_next = veh_speed(v, Fd, t, p) % 整车纵向动力学:驱动力 - 滚动阻力 - 空气阻力 - 坡道阻力 theta = p.theta_fun(t); % 坡度,t=30s 时开始上坡 Fres = p.m_true * p.g * p.f * cos(theta) ... % 滚动阻力 + 0.5 * p.rho * p.Cd * p.Af * v^2 ... % 空气阻力 + p.m_true * p.g * sin(theta); % 坡道阻力 a = (Fd - Fres) / p.m_true; % 注意用真实质量 v_next = v + p.h * a; % 前向欧拉积分 end三处细节值得说。第一,质量用的是p.m_true而不是p.m,这样对象和控制器之间天然存在 20% 的参数失配,b0偏小,ESO 必须自己把差额补上。第二,0.5*rho*Cd*Af*v^2这一项是速度平方,属于典型的非线性未建模项,线性控制器在高速段会明显吃力,正好用来对比。第三,坡度用p.theta_fun给,写成匿名函数放在init_params.m里,比如p.theta_fun = @(t) 0.087 * (t > 30);,表示 30 秒后突然出现 5 度坡。
想加执行器滞后也很简单,把Fd换成一个一阶惯性环节的输出,状态多一个F,但这时候控制增益b0就要重新推,不能再取1/m,因为控制量到加速度之间多了一层滞后。这一条留到第 6 章讲换对象的时候再说。
4.4 出图与四个必看指标
plot_result.m画四张子图:目标车速与实测车速、ESO 估计的z2、驱动力、跟踪误差。看的时候别只看第一张,四个指标串起来看才有意义:
| 指标 | 看哪张图 | 合格线(本工况) | 不合格说明什么 |
|---|---|---|---|
| 上升时间 | 车速图 | 0→20 m/s 约 2.5 s | 偏慢多半是kp小或umax限死 |
| 超调量 | 车速图 | 小于 5% | TD 的r太大,或者kp过冲 |
| 稳态误差 | 车速图 | 平路段接近 0 | b0量级错,z2补不回来 |
| 抗扰恢复时间 | 30 s 处的掉速深度 | 掉速小于 1 m/s,4 s 内回正 | w_o太低,z2反应不过来 |
30 秒那一脚坡道是全场重点。如果z2曲线在上坡瞬间迅速抬升并稳定在一个新值,说明观测器工作正常;如果z2迟迟不动,那说明beta2太小,或者fal的delta把有效信号也当噪声压掉了。第二张图比第一张更能反映你的 ADRC 到底有没有在工作。
5. 避坑与排查:五个让曲线当场翻车的细节
这一章的条目全部来自实际调试,每一条都能让图从“像模像样”变成“根本不能看”。顺序按出现频率排。
5.1 第一拍就给 NaN,曲线一根都画不出来
现象:点下运行,命令行刷一堆NaN警告,log矩阵里从第二行开始全是NaN。
原因:九成是b0写成了 0 或者写反了符号。b0 = 0时(u0 - z2)/b0直接除零;b0取负号时补偿项变成正反馈,两三拍之内就能把状态推到无穷。剩下的一成是p.beta1、p.beta2过大,配上h = 0.01时离散观测器直接不稳定——判据是beta1 * h < 2量级。
解决:先disp(p.b0)打印出来确认量级,车速环里它应该是6.67e-4左右。再看p.w_o * p.h,这个乘积建议控制在 0.1 以内,w_o = 6、h = 0.01时是 0.06,安全;想加带宽就先把h减小,别硬怼w_o。
5.2 稳态一直差那么一点,误差始终归不了零
现象:平路巡航段车速稳定在 19.2 m/s 左右,目标 20,误差纹丝不动,z2也已经收敛到一个常数。
原因:z2收敛了但还是有残差,说明控制量没把z2补干净。最常见的是u被umax限住了,尤其是上坡段;另一种是这个对象里存在你写在b0*u之外、但又被z2的fal非线性区压掉的常值分量。还有一种容易被忽略:u_prev传的是限幅前的值,观测器以为执行器吃到了 9000 N,实际只有 8000 N,差额就变成稳态误差。
解决:先把p.umax放大到 15000 跑一遍,误差消失就说明是饱和问题,回头去改控制器带宽或者减kp;误差还在,就把u_prev = u这一行往上提,确保限幅在观测器之前。另外p.delta别取太小,常值扰动被fal的幂次区一压,等效增益会掉。
5.3 控制量高频抖动,驱动力曲线像锯齿
现象:车速曲线看着还行,第四张图里驱动力上下抖动,抖幅能到 500 N,放大看是每个采样点都在变。
原因:z2在放大噪声。fal的delta给得太小,或者干脆用了alpha = 0.25配delta = 0.001,小误差段的斜率1/delta^(1-alpha)大到几十,量测里那点噪声被放大后直接进控制量。
解决:按噪声幅值定delta,量测噪声标准差 0.02 m/s 就取 0.05 到 0.1。还压不住就加一个一阶低通滤z2,截止频率取w_o的三倍左右,别取太低否则抗扰变慢。最后才是降w_o,那是最后的手段,因为降带宽等于放弃抗扰速度。
5.4 阶跃初期驱动力顶死限幅,恢复特别慢
现象:目标从 20 跳到 25 的瞬间,u直接打到 8000 N,持续一秒多才退出来,期间车速冲过 25 再慢慢回落。
原因:TD 的r给太大,v1几乎等于阶跃,e1在第一拍就有 5 m/s,kp * e1 * m算出来一万多牛,必然饱和;饱和期间误差还在积分(如果你加了积分项),退出时就会过冲。
解决:先把r从 50 降到 8 到 15 之间,让过渡过程本身占掉两三秒,控制量自然不饱和。如果业务上要求快速响应不能降r,那就得在adrc_step里加抗饱和:饱和时把e1冻结或者把u0按u的实际比例回算,别让误差持续累积。
5.5 换了个对象就完全不对,怀疑代码有 bug
现象:把veh_speed.m换成自己写的电机模型,跑出来要么发散要么纹丝不动,怎么调kp都没用。
原因:b0没跟着换。控制增益是由对象结构决定的,一阶惯性对象的b0是“输入到状态导数的增益”,二阶对象(带执行器滞后、悬架、位置环)的b0量纲完全不同,直接沿用1/m等于给观测器喂了一个错三个数量级的数。
解决:换对象之前先做一次开环辨识——给一个恒定的u,看状态导数的稳态增量,b0 = Δ(状态导数) / Δu。用这套程序验证一遍:给定u = 1500 N,加速度应该是约1 m/s²,b0算出来正好1/1500,对上了再闭环保平安。
提示:这五条里有四条最终都指向同一件事——把
b0、h、u_prev这三个量核对一遍。真跑不通的时候,与其怀疑算法,不如先把这三个值打印出来看看。
6. 二次开发:把被控对象换成你自己的,以及 b0 与带宽的整定手法
换对象这件事,改动面比想象中小。这套程序里只有plant/veh_speed.m和init_params.m里的对象参数是绑死在整车上的,adrc_core四个文件一行都不用动,前提是b0和对阶数对得上。
先做一件事:确定你的对象是几阶。一阶(车速、电压、流量这类“输入直接改变状态导数”的)用现在的二阶 ESO 加 P 控制;二阶(位置环、悬架、带执行器滞后的转速环)就得把eso2.m扩成三阶,z3估扰动,控制律换成u0 = kp*(v1-z1) + kd*(v2-z2),补偿项变成-z3/b0。扩的时候记住极点配置:三阶 ESO 取(s+w_o)^3,展开就是beta1 = 3w_o、beta2 = 3w_o^2、beta3 = w_o^3,别一项一项试。
整定顺序我一般固定成三步。第一步定b0,用开环法测,误差控制在 30% 以内就行,剩下的交给 ESO。第二步定kp,先关掉补偿(把z2强制置零)当纯 P 跑,调到闭环时间常数符合业务要求,比如巡航想 1 秒跟上就取kp ≈ 1。第三步开补偿、加w_o,从3*kp起步往上加,加到z2能在一个坡道内跟上就不再动,再往上加只是拿噪声换速度。
有一个技巧是我踩过坑之后一直用的:在log里多存一列u_raw。限幅前后两个值一起看,能立刻分清“控制器算多了”和“执行器吃不下”,这两种情况的处理方向完全相反,光看u是分不出来的。
另一个是离散化的坑。这套程序用的是前向欧拉,h = 0.01时对车速环足够了,但如果你的对象时间常数只有几十毫秒,前向欧拉会引入额外相位滞后,表现出来就是“怎么加w_o都振”。这种时候把veh_speed.m换成ode4(RK4)一步,控制器那边的h保持不动,问题通常就消失了。控制器离散化方式和对象积分方式可以不一致,这一点别被“步长必须统一”的说法带偏——统一的是采样周期,不是积分方法。
从那以后我每次换对象,都强制走一遍开环测b0、闭环空跑、再加扰动的三步,哪怕多花二十分钟,也比后面花一晚上怀疑算法强。希望帮到你。
本文还有配套的精品资源,点击获取