ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

四旋翼ADRC控制器仿真程序深度解析:从拆解到调参扩展

四旋翼ADRC控制器仿真程序深度解析:从拆解到调参扩展

1. 拿到"已调好"的仿真程序,第一件事不是跑,而是拆

做四旋翼控制的人应该都有这种经历:从某个渠道拿到一份别人调好的仿真程序,压缩包解压出来,里面躺着几个.m文件、一个.slx模型、一个README.txt,或者干脆什么都没有。这时候大多数人的第一反应是双击主脚本,看到曲线出来,就觉得自己已经"掌握"了。但实际上,如果你只停留在"能跑通"这个层面,那这份程序对你的价值连十分之一都发挥不到。

我拿到这份四旋翼 ADRC 控制器仿真程序时,第一件事是把目录结构完整看了一遍。这里给所有准备复现这份程序的朋友一个建议:拿到任何仿真工程,先建立一张"程序地图",搞清楚哪个文件是入口、哪个文件是模型、哪个文件是参数初始化、哪个文件是控制器,再动手运行。这不只是为了跑通,而是为了后面你能自主修改、调参、移植。

这份程序的结构大致如下:

文件类型作用说明
主脚本仿真入口和结果绘图设置仿真时长、初始状态、干扰方式
参数初始化脚本定义四旋翼物理参数和控制器参数质量、惯量、臂长、控制器带宽等
动力学模型四旋翼六自由度运动方程一般用子函数或 Simulink 模块实现
ADRC控制器包含跟踪微分器、扩张状态观测器、非线性状态误差反馈控制器独立成函数,方便替换
状态观测器脚本用于估计总扰动ADRC 的"秘密武器"所在

README.txt里的信息很简洁,只说明了两件事:在什么版本的 Matlab 下运行,以及主脚本名字。这其实暴露了一个很常见的问题——很多分享出来的程序,作者默认使用者已经具备足够背景知识,省略掉了大量"为什么这样写"的信息。这篇文章想做的,就是把缺的部分补上。

另外,如果你是首次在 Matlab 中打开这个程序,建议先运行一遍checkEnv或者直接执行主脚本,看看会不会因为中文字符编码、路径空格、工具箱缺失等问题报错。我曾经遇到过一次很尴尬的情况:程序本身没问题,但因为解压路径里带了一个中文文件夹名,导致脚本读取模型文件失败。这种问题排查起来不难,但很浪费时间。纯净的英文路径、干净的 workspace,是跑通一切 Matlab 仿真的前提。

2. 四旋翼的动力学模型:这份程序到底在模拟什么物理过程

很多初学者拿到仿真代码后,直接从控制器开始看,这其实是个误区。四旋翼仿真程序里,最值钱的往往不是控制器,而是那个看起来不起眼的"动力学模型"部分——因为模型写得好不好,直接决定控制器参数能不能平移迁移到真机上。ADRC 这种控制器,本质上是在跟模型"较劲",如果模型少了一项耦合力矩,你在仿真里调出来的观测器带宽,在真机上大概率是废的。

这份程序采用的模型,是标准的三维刚体六自由度方程。需要强调的是,它做了一些在四旋翼仿真中非常通用的简化假设:

  • 机体视为刚体,不考虑弹性形变;
  • 电机响应简化为二阶或一阶惯性环节,不考虑更复杂的电调特性;
  • 空气阻力用简单的阻尼系数近似,而不是查表的气动模型;
  • 忽略地面效应、螺旋桨陀螺效应等次要因素。

用公式表达就是经典的牛顿-欧拉方程。位置环节由三个方向的合力驱动,姿态环节由机体的力矩驱动,两者通过旋转矩阵耦合。你可能觉得这只是"标准操作",但实际写代码时会发现几个容易出鬼的地方:

第一,坐标系定义。程序里常见的定义是:机体坐标系 z 轴向上,x 轴指向机头方向;世界坐标系采用 NED(北东地)或者 NWU(北西天)都会带来很大区别。我强烈建议你在复现时不只看方程本身,而是确认程序里的旋转矩阵方向和正负号约定。这个坑几乎每个做四旋翼仿真的人都会踩一遍。有的程序里俯仰角是"抬头为正",有的是"低头为正",如果沿用你熟悉的那套控制外环,很可能在仿真里表现为姿态发散。

第二,转动惯量的数值。惯性矩直接决定了姿态动力学的时间尺度。这份程序里的默认参数是某小型航模级别四旋翼的典型数值,质量在 1kg 上下、轴距 250mm 左右。如果你换了一台更大的平台,需要按比例调整惯量,否则 ADRC 的观测器增益和控制器带宽全都要跟着变。

第三,电机模型。程序里的控制输出,通常会被映射为四个电机的拉力或转速指令。大多数简化模型里,螺丝推力与转速平方成正比,电机响应再用一阶惯性环节近似。这个惯性时间常数(常见取值 0.01s 到 0.02s)对仿真结果影响超大——如果取值太大,高频姿态修正指令会被严重滞后,ADRC 的高带宽优势根本发挥不出来。

所以看这份程序时,不要跳过quad_params.m这类参数文件。里面可能没有注释,但每个数字背后都有物理含义。我的习惯是拿到参数文件后,先在草稿纸上列一遍:这些数是否符合量纲?惯量比是否符合几何常识?把这些搞清楚了,后面调参才有底气。

2.1 模型参数与初始化脚本里那些"魔法数字"

直接看程序的话,你大概率会看到类似这样的代码块:

m = 1.2; % 质量 kg l = 0.25; % 机臂长度 m Ixx = 0.02; Iyy = 0.02; Izz = 0.035; kF = 1.5e-5; % 拉力系数 N/(rad/s)^2 kM = 2e-7; % 力矩系数 N.m/(rad/s)^2 g = 9.81;

这些数字不是拍脑袋写的。kF和kM的选择,会直接影响电机转速的饱和边界。你可以自己算一算:悬停时每个电机需要的拉力大约为m*g/4 ≈ 2.94N,那么对应的转速大约是sqrt(2.94 / 1.5e-5) ≈ 443 rad/s,也就是大概 4200 RPM。这个转速在一个小型无刷电机的工作范围内,合理。如果你发现程序里的kF小了一个数量级,那么同样的拉力会要求电机转速高到物理上不可能实现,仿真曲线就会看起来很"虚"。

这类数值在注释缺失的情况下,就是所谓的"魔法数字"。对初学者来说,我的建议是:不要轻易改模型参数,除非你能说清楚这个改动会带来什么物理量的变化。先保持默认,跑通之后再做参数敏感性分析。

2.2 从"仿真程序"到"实物移植"之间,隔着一个电机模型的距离

严格来说,很多"已调好"的四旋翼仿真程序,控制器的参数并不能直接搬到真机上。原因就出在电机模型上。仿真里常用的电机响应模型是:

T * s + 1 的惯性环节

而在真机上,电机响应还包含电池电压波动、电调算法差异、桨叶气动延时等。ADRC 最厉害的地方在于,它能把所有这些"未建模动态"统统视为总扰动的一部分,并通过扩张状态观测器估计出来。所以你会发现一个很有意思的现象:用同一组 ADRC 参数,在仿真和真机上,都能飞——但轨迹质量会有差异。这正是扩张状态观测器的功劳,它不需要精确的模型,就能把模型偏差兜住。

不过这里有个前提:仿真模型里的"未建模动态"不能比真机离谱太多。如果仿真里电机用了一阶惯性,而真机上的实际动态特性更接近二阶振荡,那你在仿真里调好的观测器带宽,在真机上可能表现为高频噪声放大。我的经验是,仿真阶段可以把电机模型故意调得"笨"一点——一阶惯性常数设到 0.02s 以上——这样调出来的 ADRC 参数,会比用理想电机模型调出来的更能扛干扰。

3. ADRC 的本质:为什么它比串级 PID 更适合四旋翼这种强扰动对象

写到这里,必须把话题转到控制器本身。很多人第一次接触 ADRC,会被它的名字吓到——"自抗扰",听起来像某种玄学控制。我用一句话解释清楚:ADRC 的核心思想,是把"不知道的模型偏差"和"外部扰动"合在一起,估算出来,然后直接在前向通道里把它抵消掉。

这句话意味着什么?你不需要精确知道四旋翼的气动阻力矩是多少,不需要精确知道电池电压变化对电机升力有多大影响,只需要通过观测器实时估计出一个"等效总扰动",然后把它的作用抵消。这种"用估计代替建模"的思路,和经典 PID 依赖误差积分的思路,是完全不同的哲学。

传统串级 PID 里,外环和风扰动都在"拉扯"内环,内环 PID 的积分项需要一点点消除稳态误差,这个过程本质上很慢。ADRC 的做法是:内环直接用扩张状态观测器把扰动估计出来,不等误差累积就把它消除。体现在阶跃响应上,就是超调更小、抗扰恢复更快、相位滞后更小。四旋翼本质上是一个高度非线性、强耦合、多扰动并存的被控对象,正好是 ADRC 的优势区。

这份程序实现的是二阶 ADRC。所谓"二阶",指的是被控对象的相对阶次是二阶——在四旋翼姿态控制场景下,输入是力矩,输出是角度,中间经过角速度积分,所以相对阶次是 2。二阶 ADRC 的核心组件有三个,我逐个说明。

3.1 跟踪微分器:给期望信号"降噪"

TD模块的作用很简单:对期望姿态角进行平滑处理,并提取其微分信号。例如你给一个阶跃的目标俯仰角 10°,TD 不会直接让你看到一条陡峭的跳变,而是输出一条快速但平滑的过渡曲线;同时,它还输出这条曲线的导数,作为期望角速度送给控制器。这样做的目的,是避免输出突变给控制通道造成过大的初始误差,减少超调。

在程序里,TD 一般写成这样:

function [x1, x2] = TD(r, x1, x2, h, r1) x1 = x1 + h * x2; x2 = x2 + h * -r1 * (x1 - r) - 2 * r1 * x2 / 3; end

注意这里的r1是跟踪速率因子,它决定过渡过程快慢。速率因子太小,响应就慢;太大,过渡过程就接近阶跃,失去了平滑意义。这份程序默认的参数,大概取的是"适度激进"——大约 0.5 秒内完成阶跃过渡。你可以通过观察角度响应曲线的上升沿来验证。

3.2 扩张状态观测器:这份程序真正值钱的地方

ESO(扩张状态观测器)是 ADRC 的心脏。它做的事情,可以类比成一个"内部传感器":你只测量了姿态角,但 ESO 能顺带估计出姿态角速度和总扰动。它把一个二阶系统扩展成三阶状态方程——真实位置、真实速度、总扰动。然后通过观测器增益把估计误差收敛到很小。

程序里常见的 ESO 离散形式:

e = z1 - y; z1 = z1 + h * (z2 - beta1 * e); z2 = z2 + h * (z3 - beta2 * e + b0 * u); z3 = z3 + h * (-beta3 * e);

这里z1是角度估计值,z2是角速度估计值,z3是总扰动估计值。beta1、beta2、beta3是观测器增益。正常情况下,它们应当按照"带宽化"的方式整定——也就是把观测器极点配置到同一个位置,让整个观测器只有一个可调的带宽参数wo。这就是"参数少、好调"的来源。

我见过很多人调试 ADRC 时,盯着beta1一个参数来回试,其实正确的做法是:只调wo,然后按比例关系计算出三个 beta。公式通常是:

beta1 = 3 * wo; beta2 = 3 * wo^2; beta3 = wo^3;

这样做的好处是,你不需要同时瞎试三个参数,而是只关心一个物理意义明确的带宽值。这份程序之所以敢说"已调好",它的 ESO 参数大概率就是按带宽化公式配置的,而不是随手填的。

3.3 非线性状态误差反馈:被过誉的"非线性"

第三个模块是 NLSEF,它接收 TD 的期望信号和 ESO 的估计信号之间的误差,生成控制量。经典 ADRC 里会用fal函数做非线性变换:

function f = fal(e, alpha, delta) if abs(e) > delta f = abs(e)^alpha * sign(e); else f = e / delta^(1 - alpha); end end

当alpha < 1时,小误差对应的增益更高,相当于在小误差范围内"放大响应"。但说实话,我调试过很多次 ADRC 之后,发现一个事实:fal的非线性增益在仿真中和线性 PD 形式的 NLSEF 差异很小。真正让 ADRC 表现优异的,是 ESO 的扰动估计和补偿,而不是那一点点非线性增益。很多工程实践里,直接把 NLSEF 简化成u0 = Kp * e1 + Kd * e2,效果几乎不变。这份程序保留了正统的fal形式,可能是为了教学一致性,也可能是因为原作者最初就是从教材代码改过来的。无论哪种,你都可以在理解原理后,自己尝试简化并对比曲线。

4. 仿真配置与运行细节:步长、解算器、干扰注入方式

当你确定了模型和控制器结构都没问题之后,接下来要面对的就是仿真本身的配置问题。这部分往往被文档忽略,却是最容易让"已调好"程序变得"一跑就散"的地方。

先说步长。这份程序用的是固定步长仿真,步长h = 0.001s,也就是 1kHz 的控制更新率。对于四旋翼这种自动驾驶频率通常落在 500Hz 到 1kHz 的场景,这是一个很现实的选择。如果你把步长改成 0.0001s,曲线会变"更好看",但那是假象——真机上你不可能跑得起 10kHz 的控制频率。如果你把步长改成 0.01s,那就要小心了:数值积分误差、离散化误差、以及 ESO 采样延迟会急剧增加,原本调好的参数很可能发散。这个道理很简单:ADRC 的观测器带宽如果比控制频率高一个数量级,离散化误差就会吃掉它的稳定性裕度。

再说解算器。在 Simulink 中,如果控制器是用 S 函数或子系统实现的离散逻辑,那么主模型用定步长四阶龙格库塔(ode4)会比较合适。如果你看到的是纯.m脚本仿真,那就是作者手动写了循环积分,常见形式是:

for k = 1:N % 计算控制器输出 % 计算状态导数 % 使用RK4或欧拉法更新六自由度状态 end

这里有一个经验:纯脚本仿真中,状态更新的顺序千万不能乱。我见过一份程序,作者把电机响应更新放在姿态更新后面,导致每个控制周期多出一个步长的延迟。看似一个小问题,但如果你把 ADRC 的观测器带宽调得很高,这种延迟会让收敛性变差。所以复现时,仔细核对每个更新的先后顺序。

然后是关于干扰注入的配置。这份程序里应该有几种默认的干扰设置:

干扰方式实现方式观察指标
常值风扰在每个控制周期对姿态动力学施加固定力矩稳态误差、ESO 的扰动估计曲线
阶跃力矩干扰在某个时刻突然注入 0.5N·m 的力矩姿态角的动态恢复时间和超调量
传感器噪声在测量角度上叠加高斯白噪声观测器的噪声抑制能力

强烈建议你验证一下:先跑无干扰版本,确认姿态输出零稳态误差;再依次注入上述干扰,观察 ESO 输出的z3曲线是否和注入干扰形状一致。如果z3不能准确复现注入的扰动曲线,说明 ESO 带宽不够或者参数没配对。这是检验"已调好"的一个硬性标准。不夸张地说,这一步比看姿态曲线还重要。

5. "已调好"到底调的是什么:参数标定顺序和典型问题排查

下面这部分是整篇文章最"值钱"的部分。很多人拿到的程序是可以跑。但一旦你想把参数改变,或者换一个工况,问题就来了:到底该调整哪个参数?先调哪个?那些"已调好"的参数是一组怎样存在?

我给大家梳理一个调试路径。这套路径来自于我自己复现多份 ADRC 仿真程序的经验,也可以理解为这份程序原作者很可能走过的路。

第一步,先定模型参数,后定控制器参数。模型参数本身不能随便动,但你需要确认:电机模型时间常数是 0.01s,还是默认用了 0.005s?b0(控制输入增益)的取值是否和系统方程一致?在 ADRC 里,b0是一个至关重要的参数——它是你对被控对象输入增益的估计值。如果b0比真实值大了 50%,ESO 的扰动估计会连带偏移很多,你以为的"扰动"里其实混入了模型失配的部分。检查方式是在仿真里切除 ESO 的补偿通道,只保留前向控制,然后看系统的阶跃响应是否在一个合理的范围内。

第二步,从观测器开始调,而不是从控制器开始。先整定 ESO 的带宽wo。把控制器增益设得很保守,甚至在仿真中把 NLSEF 的输出缩小到原来的十分之一,只观察 ESO 的估计是否准确。观察目标:给系统加一个阶跃干扰,看z3跟踪扰动曲线的速度和超调。如果z3滞后太明显,可以提高wo,但注意wo超过一个阈值后,观测器会在噪声下抖动。这个阈值和步长有关:实测下来,在 1kHz 仿真步长下,wo设置在 30~100 rad/s 之间是比较稳的。超过 200 就属于激进调法,会放大模型噪声。

第三步,再调控制器带宽。控制器部分带宽(记为wc)决定了追踪参考信号的带宽。通常设置wc比wo低三到五倍以上,这样观测器能及时跟上控制回路的要求。这里有一个常用公式:

Kp = wc^2 Kd = 2 * wc

初步调好的标志是阶跃响应无超调或小超调,上升时间符合你的期望。这份程序里那些"看起来整齐"的姿态响应曲线,就是这样按顺序一步步撞出来的,而不是把参数随手一填得到的运气。

5.1 常见问题一:姿态发散,到底哪里错了?

如果你把参数改了之后,仿真在 3 秒内爆掉,先别急着改回来。排查步骤如下:

  1. 先看角度是否正确,如果角度瞬间冲上 90°以上,说明控制器输出方向不对——旋转矩阵正负号、电机编号顺序、油门方向任一错了都会这样。
  2. 再看角速度。如果角度还没发但角速度剧烈振荡,多半是Kd过小、阻尼不足。
  3. 如果角度和角速度都正常,但控制量饱和,检查电机转速上下限是否被触发。ADRC 补偿扰动时,如果扰动估计值巨大,控制量瞬间顶到饱和,会造成类似 bang-bang 控制的极限环振荡。

5.2 常见问题二:稳态误差消不掉,ESO 却显示扰动为零

这个现象很有迷惑性。程序可以跑,姿态响应似乎"差不多",但就是有 1° 左右的稳态误差。很多人会去调 ESO 的beta3,但问题往往不在这里。最可能的原因是:你的控制器输出没有真正作用到模型上。

仔细检查:你计算出的期望总的控制量,是直接加到u上了,还是被限幅函数截断了?很多程序为了模拟真实执行机构,在电机分配前加了饱和函数。如果你在调试过程中把控制增益调大,导致控制量频繁饱和,那么输出的有效作用力矩比期望值小,稳态误差就会一直挂在那里。此时你应该把z3的估计值与实际干扰对比,如果两者差了一个偏置量,那基本可以锁定是饱和造成的。

5.3 常见问题三:为什么加了一点噪声,仿真就开始抖?

这是 ADRC 的一个典型局限:ESO 用高带宽估计扰动的同时,也会把量测噪声高频分量放大。给你的建议有三个层面:

  • 降低wo,牺牲一点扰动估计速度,换取噪声抑制能力;
  • 在测量通道加一阶低通滤波器,但这会引入相位延迟,所以滤波截止频率应不低于wo的三倍;
  • 改用降阶观测器。当不需要估计全状态时,降阶 ESO 对噪声的敏感度更低,只是参数整定复杂一些。

这个问题的本质是在和"观测器带宽-控制闭环带宽-传感器噪声"三者之间做权衡,没有免费的午餐。

6. 在"已调好"的基础上继续扩展,把仿真价值榨干

程序能跑、参数能调,这只是起点。真正有价值的工作,是在这份程序基础上做"增量开发"。我给你几个明确的扩展方向,都是我在类似项目上实际走过的路。

方向一:把姿态控制升级为位置控制。目前的程序大概率只做了姿态环(内环)仿真,或者是简单的单通道角度跟踪。你可以在此基础上加入位置外环,用位置误差计算期望姿态角,再把姿态角期望送给内环 ADRC。注意外环的控制频率可以比内环低很多,通常 50Hz~100Hz 就够,因为位置环的动力学比姿态环慢一个量级。这种内外环结构是工程上最主流的四旋翼控制方案,代码改动不大,但逻辑上会复杂不少。

方向二:把阶跃测试改造成轨迹跟踪。默认的时域阶跃响应只能验证姿态稳定性,无法充分检验控制器在连续机动中的表现。你可以设计一条螺旋上升轨迹(同时改变 x、y、z 和偏航角),强制四旋翼在高非线性区域运行,观察 ADRC 在连续变化工况下的估计质量和误差曲线。实测下来,ADRC 在连续轨迹跟踪中的优势比阶跃响应里体现得更明显,因为参考信号持续变化时,PID 的积分项往往会拖后腿。

方向三:加执行器饱和限制和分配逻辑。真机上每个电机的推力和力矩响应都有上下限,如果你想看控制器在贴近真实的条件下是否还"已调好",建议在程序里加入控制分配模块:把期望的总推力、滚转力矩、俯仰力矩、偏航力矩,分配为四个电机的转速指令,再加入转速限制。你会发现,加了饱和之后的 ADRC 依然比 PID 更容易出现"抗饱和"的问题——因为 ESO 会把饱和造成的执行偏差当作扰动估计出来,这反而是一件好事。

方向四:从仿真走向半物理。如果你手头有飞控硬件,可以在 Matlab 里把控制器生成 C 代码,嵌入硬件运行,然后把传感器数据通过串口回传。这一步把 ADRC 的"扰动估计"放到了真实噪声环境中。我个人的建议是:仿真里先别追求极致的曲线,把参数调到偏保守的区间,给真机留裕量,因为真机上的未建模动态至少比仿真多 20% 以上。

最后再分享一个小技巧。扩展完后,你可以在程序的绘图区同时画出四条曲线:参考信号、实际输出、控制器输出、ESO 总扰动估计。这四条线几乎能解释所有调试问题。如果扰动估计曲线噪声大,看观测器参数;如果控制器输出频繁饱和,看执行机构限制;如果实际输出偏离参考,看闭环带宽。这种"一眼定位法"比反复调参碰运气要高效得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进