☰
Matlab四旋翼无人机PID控制仿真:从零构建代码与调参实战
2026/9/28 13:40:44 网站建设 项目流程

搞控制或者说搞机器人的朋友,应该都经历过这个阶段:看了无数篇四旋翼无人机 PID 控制的文章,手里的代码却要么跑不起来,要么一跑就发散,对着曲线完全不知道问题出在哪。我当年入门的时候也在这种坑里趴了很久,后来用 Matlab 从零写了一个完整的四旋翼无人机 PID 控制仿真,把模型、控制律、参数整定、结果可视化全部串在一起,才算真正把“PID 是怎么把飞机稳住”这件事吃透了。

这篇教程就是按我当时的学习路径整理的。整体用 Matlab 脚本实现,不依赖 Simulink,也不需要额外工具箱,完整代码直接贴在后面,复制保存为.m文件就能运行。仿真对象是一个简化版四旋翼模型,包含高度和俯仰两个通道,控制律采用位置式 PID,并加入了重力补偿。跑通之后你能清楚看到高度、姿态在 PID 控制下的动态响应,也能直观理解 kp、ki、kd 这三个参数到底在干什么,为后续做级联 PID 或者真机调参打个底。

这个仿真适合三类读者:刚接触控制理论、想找个具体对象练手的学生;想快速验证 PID 调参思路、又不想一上来就碰硬件的入门工程师;以及准备做真机实验,但想先在低成本环境里把逻辑理清的小伙伴。接下来我按“从思路到代码,再到排错”的顺序,把整个搭建过程完整讲一遍。

1. 仿真的整体思路:新手为什么要从模型入手

1.1 仿真闭环帮你建立控制直觉

很多新手拿到一段控制代码,第一件事就是到处调 Kp、Kd,调了半天也不知道为什么发散、为什么稳态误差消不掉。归根结底,是不清楚被控对象长什么样。仿真最大的价值,不是真去复现一架无人机,而是用数学方程把你对“物理系统如何运动”的理解,变成一个可以随时观测的闭环:给一个期望值,看输出怎么响应,控制量怎么变化,误差怎么收敛。

这个闭环一旦在脑子里成形,你再去碰真机、看飞控日志、调参,就不会全靠猜。仿真里的每一次发散,都是在帮你建立“参数和响应之间的直觉”:某个环节加大会有什么趋势,某个环节减小会不会稳一点。这种直觉是任何文档都教不会的,只能自己跑出来。

我在这个项目里刻意没有用 Simulink,原因有两点。一是脚本代码的每一步都显式可见,误差怎么算、PID 怎么输出、模型怎么更新,一目了然,对新手来说没有“黑盒子”;二是代码方便改造,想换控制律、加扰动、改成级联 PID,都是在文本层面直接操作,比拖拽 Simulink 模块直观得多。

1.2 简化模型是从哪来的

完整四旋翼无人机模型是六自由度的,三个平动位置(x、y、z)加三个姿态角(roll、pitch、yaw),方程之间耦合严重,非线性项也不少。对新手来说,一上来就把完整的牛顿-欧拉方程糊在脸上,很容易被劝退;就算代码写出来了,一个小错误也会导致完全无法收敛,排错难度很高。

所以这个仿真里我做了一个经典简化:只考虑悬停附近的运动,把高度通道和俯仰通道解耦出来。换句话说,我们先控制 z 方向高度和俯仰角 phi,忽略横滚、偏航以及它们之间的耦合,把四旋翼当成两个独立的单输入单输出系统来处理。

高度通道很简单,就是牛顿第二定律。记无人机质量为 m,四个电机产生的总升力对应的等效加速度为 u1,竖直方向运动方程为:

m * z'' = u1 * m - m * g

两边同时除以 m,得到:

z'' = u1 - g

这里 u1 的单位已经是加速度单位,不是力。好处是控制量跟质量解耦,后面调参数的时候不用反复改质量。

俯仰通道对应刚体转动定律。记俯仰转动惯量为 I_yy,控制俯仰的力矩为 u2,则:

phi'' = u2 / I_yy

这两个方程已经足够演示 PID 的核心控制逻辑:给高度一个目标,PID 算出需要的加速度,再加上重力补偿项 g,就是油门指令;给俯仰角一个目标,PID 算出需要的角加速度,乘上转动惯量,就是力矩指令。

有读者可能会问:简化掉横滚和偏航,仿真还有意义吗?我的看法是,对于入门阶段,先把两个通道吃透,远比一次性面对六个通道的耦合要有效。而且等你理解了内环外环的分层思想后,把横滚、偏航通道往代码里补,只是同样的模式复制几遍而已。

1.3 模型、控制器、可视化缺一不可

一套完整的仿真,至少包含三个部分:被控对象模型、控制器算法、数据可视化。模型是你对物理世界的近似描述,控制器是你要验证的算法,而可视化是帮助你判断算法好坏的“眼睛”。三者缺一不可。

这个仿真里还有第四个隐含部分,就是主循环本身。它模拟了控制器的运行节拍:每隔一个固定周期 dt,读取一次状态,计算一次控制量,更新一次模型。这个“周期性采样”的概念对理解数字控制很关键,真机飞控就是在固定的控制频率下不断重复这个过程,和仿真主循环没有本质区别。

2. PID控制的原理与参数设计逻辑

2.1 比例、积分、微分到底在干什么

PID 三个字母对应的三个环节,可以用一句不太严谨但很好记的话概括:比例管现在,积分管过去,微分管未来。

比例项 Kp * e(t) 直接放大当前误差。误差越大,控制量越大,系统回正的趋势就越强。只有比例项时,系统经常会出现稳态误差或者来回振荡。打个比方,你开车看到偏离车道,打方向盘的幅度只跟当前偏差成正比,那速度一快就容易冲过头,速度慢了又回不来。

积分项 Ki * ∫e(t)dt 把历史上累计的所有误差加回来。它专门处理稳态误差。比如这个仿真里,飞机悬停时重力始终向下,如果控制量里只有比例项,油门指令里没有多出来的重力补偿部分,飞机就会持续往下掉,最终停在某个高度再也上不去。积分项就是为了把这种“持续存在的小偏差”累积成足够的控制量去抵消它。缺点是积分容易过头,积分项太大会导致超调明显。

微分项 Kd * de(t)/dt 看的是误差变化趋势。误差在快速减小,微分项就产生反向力,提前“踩刹车”,抑制超调。它会放大高频噪声,所以真机上微分项通常要配低通滤波。

把这三项合在一起,就形成了完整的位置式 PID 控制律:

u = Kp * e + Ki * integral(e * dt) + Kd * de/dt

2.2 离散化与代码里的实现细节

因为计算机和微控制器都是离散系统,需要把连续 PID 离散化。我用的位置式离散形式是:

积分累加:err_int += err * dt; 微分近似:err_dot = (err - err_prev) / dt; 控制输出:u = Kp*err + Ki*err_int + Kd*err_dot;

积分项直接累加误差与步长的乘积,这是最简单的矩形积分;微分项用一阶差分近似,虽然对噪声敏感,但在仿真环境下完全够用,而且逻辑最直白。

代码里我实际写的微分项有一点变化,值得单独说明。因为期望高度和期望俯仰角是常数,误差的导数就等于状态量的负导数:

e = z_ref - z; e_dot = 0 - zdot = -zdot;

所以微分项直接写成Kd_z * (-zdot)。这个写法和Kd * (err - err_prev) / dt在数学上等价,但直接调用速度状态,不会因为差分放大数值噪声,数值上更干净。

2.3 参数整定为什么是那个顺序

网上一搜 pid 参数整定方法,Ziegler-Nichols、临界比例度、衰减曲线法一大堆。但对新手来说,最有用的还是先建立“参数和响应之间的直觉”。我的建议顺序永远是:先调 P,再调 D,最后调 I。

第一步,把 Ki、Kd 全部设成 0,只加 Kp。从小往大慢慢加,观察曲线。Kp 太小,响应慢,稳态误差大;Kp 太大,系统开始等幅振荡。这一步能让你直观感受系统的开环特性和临界振荡点。

第二步,加 Kd。当曲线出现明显超调时开始加,Kd 的作用是“刹车”,让响应更快稳定下来。这里要注意 Kd 方向千万不能反,方向反了会加速振荡甚至直接发散。

第三步,加 Ki。在 P、D 已经把系统基本稳定住的条件下,如果发现最终的稳定值和期望值有偏差,再加入 Ki 去消除稳态误差。

我代码里给的高度环参数是 Kp=8、Ki=0.5、Kd=5,俯仰环是 Kp=60、Ki=10、Kd=8。这些参数不是拍脑袋写的,可以做个粗略推导。高度环忽略积分项后,闭环特征方程近似为:

s^2 + Kd_z * s + Kp_z = 0

代入 Kp=8、Kd=5,自然频率 wn = sqrt(8) ≈ 2.83 rad/s,阻尼比 zeta = 5 / (2sqrt(8)) ≈ 0.88。这个阻尼比接近临界阻尼,意味着响应快且几乎不超调,收敛时间约 4 / (zetawn) ≈ 1.6 秒。俯仰环 Kp=60、Kd=8,wn ≈ 7.75 rad/s,zeta ≈ 0.52,姿态响应更快但会有少量超调,实际调试时把 Kd 提高到 12 左右可以让姿态更稳。

像这样先根据二阶系统公式估一个范围,再微调参数,比盲目试要高效得多。

3. 从零搭建:完整代码实现与调试

3.1 环境准备与代码结构

这个项目对 Matlab 版本没有特殊要求,R2018a 以上都可以,不需要安装额外工具箱。如果你机器上还没装 Matlab,安装过程注意一下许可证激活,装好以后新建一个脚本文件,命名为quad_pid_sim.m,把完整代码粘贴进去,点击运行就行。

整个代码分成四段:

  • 第一段是参数初始化:物理参数、仿真步长、期望值、PID 参数、初始状态,全部集中在这里,方便维护。
  • 第二段是主循环:每过一个控制周期,计算误差、PID 输出、更新模型、记录数据。
  • 第三段是绘图:把高度、姿态、控制量画出来,直观判断控制效果。
  • 第四段是终端输出:直接打印最终的收敛结果,方便快速验证。

这样的组织方式对后期扩展非常友好。比如你想改期望高度为方波,只需要定义z_ref_k放进循环;想加扰动,只需要在模型更新那行叠加一个外力项。

3.2 主循环的四个关键步骤

主循环是整个仿真器的核心,先看骨架,再看细节:

for k = 1:N 计算当前误差 PID 计算控制量 模型积分更新状态 记录数据 end

每一步都对应真实控制器的工作流程。dt=0.01 就是控制周期,对应 100Hz 的控制频率。真实飞控一般在 500Hz 到 1kHz,100Hz 对入门仿真足够了。

第一步,计算误差。直接用期望值减当前状态:err_z = z_ref - z、err_phi = phi_ref - phi。

第二步,PID 计算。这里高度通道和俯仰通道的写法略有不同,高度通道因为要克服重力,在 PID 输出之外必须叠加一个重力补偿项:

a_z_des = Kp_z * err_z + Ki_z * err_z_int + Kd_z * (-zdot); u1 = a_z_des + g;

新手最容易忽略的就是这个重力补偿。只把 PID 输出当成油门,控制量里没有重力对应部分,飞机永远飞不到目标高度,最终会出现很大的稳态误差。加了 g 之后,PID 输出的加速度全部用来纠正高度偏差,物理含义更清晰。

俯仰通道的控制量是力矩,所以把 PID 输出的期望角加速度乘上转动惯量:

phi_ddot_des = Kp_phi * err_phi + Ki_phi * err_phi_int + Kd_phi * (-phidot); u2 = phi_ddot_des * I_yy;

第三步,模型更新。这一步本质是数值积分,我用了最简单的欧拉法:

zdot = zdot + (u1 - g) * dt; z = z + zdot * dt; phidot = phidot + (u2 / I_yy) * dt; phi = phi + phidot * dt;

欧拉法精度不算高,但对这个仿真场景完全够用。如果后面你把模型改复杂了,建议至少换成四阶 Runge-Kutta,否则大步长下容易出现数值发散,让你误以为是控制器的问题。

第四步,记录数据。把每个周期的高度、姿态、控制量存到数组里,仿真结束后一次性绘图。

3.3 完整代码:复制保存直接跑

下面是完整可运行的 Matlab 代码。新建脚本保存为quad_pid_sim.m,直接运行即可:

%% 四旋翼无人机PID控制仿真(简化模型:高度+俯仰) % 使用说明:保存为 quad_pid_sim.m,直接运行。 % 模型:高度通道 z'' = u1 - g,俯仰通道 phi'' = u2 / I_yy % 控制律:位置式PID + 重力补偿 clear; clc; close all; %% 1. 参数初始化 m = 1.2; % 无人机质量 kg g = 9.8; % 重力加速度 m/s^2 I_yy = 0.015; % 俯仰转动惯量 kg*m^2 dt = 0.01; % 仿真步长 s(控制周期) t_end = 20; % 仿真时长 s t = 0:dt:t_end; N = length(t); % 期望值 z_ref = 10; % 期望高度 m phi_ref = 0; % 期望俯仰角 rad(悬停) % PID参数(高度通道) Kp_z = 8; Ki_z = 0.5; Kd_z = 5; % PID参数(俯仰通道) Kp_phi = 60; Ki_phi = 10; Kd_phi = 8; % 状态初始化 z = 0; zdot = 0; phi = 0; phidot = 0; % 误差积分 err_z_int = 0; err_phi_int = 0; % 数据存储 z_hist = zeros(1, N); phi_hist = zeros(1, N); u1_hist = zeros(1, N); u2_hist = zeros(1, N); %% 2. 主循环 for k = 1:N % ---------- 高度通道 ---------- err_z = z_ref - z; err_z_int = err_z_int + err_z * dt; a_z_des = Kp_z * err_z + Ki_z * err_z_int + Kd_z * (-zdot); u1 = a_z_des + g; % 重力补偿 % ---------- 俯仰通道 ---------- err_phi = phi_ref - phi; err_phi_int = err_phi_int + err_phi * dt; phi_ddot_des = Kp_phi * err_phi + Ki_phi * err_phi_int + Kd_phi * (-phidot); u2 = phi_ddot_des * I_yy; % ---------- 模型更新(欧拉积分) ---------- zdot = zdot + (u1 - g) * dt; z = z + zdot * dt; phidot = phidot + (u2 / I_yy) * dt; phi = phi + phidot * dt; % ---------- 记录数据 ---------- z_hist(k) = z; phi_hist(k) = phi; u1_hist(k) = u1; u2_hist(k) = u2; end %% 3. 绘图 figure('Name', '四旋翼无人机PID控制仿真结果', 'Position', [100 100 1000 700]); subplot(2,2,1); plot(t, z_hist, 'b-', 'LineWidth', 1.5); hold on; plot(t, z_ref*ones(1,N), 'r--', 'LineWidth', 1.2); xlabel('时间 (s)'); ylabel('高度 (m)'); title('高度响应'); legend('实际高度', '期望高度', 'Location', 'southeast'); grid on; subplot(2,2,2); plot(t, phi_hist*180/pi, 'b-', 'LineWidth', 1.5); hold on; plot(t, phi_ref*ones(1,N), 'r--', 'LineWidth', 1.2); xlabel('时间 (s)'); ylabel('俯仰角 (°)'); title('姿态响应'); legend('实际俯仰角', '期望俯仰角', 'Location', 'southeast'); grid on; subplot(2,2,3); plot(t, u1_hist, 'b-', 'LineWidth', 1.5); xlabel('时间 (s)'); ylabel('等效加速度控制量 u1 (m/s^2)'); title('高度通道控制量'); grid on; subplot(2,2,4); plot(t, u2_hist, 'b-', 'LineWidth', 1.5); xlabel('时间 (s)'); ylabel('俯仰力矩 u2 (N·m)'); title('俯仰通道控制量'); grid on; %% 4. 终端输出 fprintf('仿真完成!\n'); fprintf('最终高度:%.2f m(期望 %.2f m)\n', z_hist(end), z_ref); fprintf('最终俯仰角:%.2f°(期望 %.2f°)\n', phi_hist(end)*180/pi, phi_ref*180/pi);

3.4 运行结果怎么读

跑完代码,你会看到四张图和一行终端输出。

高度响应那幅图,蓝色的实际高度应该在 1 到 2 秒内平滑地爬到 10 米,几乎没有超调,这是因为高度环阻尼比接近 0.88,接近临界阻尼。如果曲线爬得很慢,多半是 Kp 太小;如果冲过头再回来,就是 Kd 不够。

俯仰角响应那幅图,初始可能有微小的角度偏差,随后会快速收敛到 0 附近。这里动态要比高度快,因为俯仰环的自然频率更高。

高度通道控制量 u1,起始值约等于 9.8,也就是重力加速度。因为无人机要保持 10 米悬停,控制量里必须先有重力补偿这部分,然后 PID 才在它附近做小幅度调整,修正高度误差。你如果看到 u1 远大于 g,说明加速度指令里有很大一部分在爬升,会对应高度快速上升的阶段。

俯仰控制量 u2,基本是小幅振荡后回到 0,因为稳态时不需要额外的俯仰力矩修正角度。

终端会输出最终高度和最终俯仰角。如果跟期望值差得比较多,首先看仿真时间是不是不够长,再把 Kp、Ki 适当加大。

注意:改参数之前先想清楚改的是哪一环、预期会发生什么变化,再动手。改完一次只动一个参数,这是调试的基本纪律。

4. 常见问题与排查技巧实录

4.1 仿真发散怎么办

仿真发散的三个最常见原因,按出现频率排序:

第一个是比例增益过大或微分方向错误。Kp 太大会让系统进入正反馈式的增幅振荡,Kd 方向接反会让原本该“刹车”的环节变成“踩油门”。遇到发散,先把 Ki、Kd 全设为 0,只留一个小 Kp,确认系统能稳定下来,再逐项恢复。

第二个是仿真步长太大。欧拉法本身是近似积分,步长越大误差越大。判定方法很简单:把 dt 从 0.01 缩小到 0.001,再跑一遍,如果两条曲线差异明显,说明步长不够小,需要减小。

第三个是模型本身写错了。比如高度更新里忘了减重力,或者俯仰力矩符号反了。这种情况需要回头逐行检查模型方程和 PID 计算的符号。

判断发散原因有个经验:看发散速度。一般 0.5 秒内直接冲到无穷大,多数是符号问题;缓慢增长然后振荡发散,多半是参数问题;高频抖动发散,优先怀疑步长太小或微分项被噪声放大。

4.2 稳态误差、振荡与参数微调

稳态误差很典型:高度停在 9.7 米或者 10.3 米,再也不动。这通常说明积分项不够,或者积分被限幅了。我在代码里没有给积分加限幅,但真实系统里积分饱和是个大坑,建议你加上一个简单的饱和函数,比如err_z_int = max(min(err_z_int, 10), -10);,防止积分量过大导致大超调。

振荡问题分两种:

  • 低频大幅振荡,周期比较长,多半是 Kp 偏大或者 Kd 偏小。先把 Kd 按 1.5 到 2 倍往上调,看超调是否明显减小。
  • 高频小幅振荡,曲线毛刺多,通常是微分项被噪声放大。仿真里没有噪声,如果你自己加了传感器噪声,就需要对微分项做低通滤波。

高度环如果出现等幅振荡,可以用我前面提到的公式反推参数。先固定 Kd,调小 Kp 让阻尼比回到 0.7 到 1 之间,振荡自然消失。

下面整理了一个我调参时常用的速查表,新手可以直接对照使用:

现象优先调整调整方向预期效果
响应慢,爬不到目标Kp增大更快接近目标
超调大,来回振荡Kd增大抑制超调,更快稳定
有小幅高频抖动Kd减小或加滤波降低噪声放大
存在稳态误差Ki增大消除残余偏差
积分导致过冲Ki减小或加限幅降低超调
系统直接发散Kp / 符号调小 / 改正恢复稳定
收敛但速度太慢Kp 和 Kd 同步同时增大提高响应速度

4.3 从仿真到实机还有多远

仿真跑通只是第一步。仿真和实机最大的差异,在于仿真里没有传感器噪声、没有执行器饱和、没有气动扰动、也没有通信延迟。代码里 u1 没有做饱和限制,但真机上电机的油门必然是 0% 到 100%,不会出现负油门,也不会无限大。所以到实机阶段,至少要做三件事:

第一,给 PID 输出加饱和限幅,防止控制量超出执行器范围。第二,微分项必须加低通滤波,不然飞控的陀螺仪噪声会被放大到没法用。第三,控制频率要提高,真实飞控的内环角速度控制通常在 1kHz 左右,外环姿态控制在 500Hz 左右。

这也是为什么完整飞控普遍采用级联 PID,而不是单级 PID。单级 PID 在简化仿真里够用,但真机上的姿态控制通常分成角度环和角速度环,内环角速度环响应更快,外环角度环的输出是内环的期望角速度。你先把这个简化版跑熟,后面再升级到级联 PID,思路是连贯的。

提示:仿真里随便怎么改参数都不会烧东西,但真机上调参前一定要保证桨叶周边空旷、飞机固定牢靠,安全意识比任何算法都重要。

4.4 我的个人调试习惯

最后分享几个我自己实践下来很管用的习惯。

改参数之前,先把期望高度从阶跃改成方波,比如 10 米和 5 米来回跳,看系统能否来回跟踪。这个测试能同时暴露出响应速度、超调、稳态误差三方面问题,比只跑一次阶跃响应信息量大多了。

再一个是控制量曲线比状态曲线更能说明问题。有时候高度曲线看起来挺好,但控制量在剧烈抖振,说明参数其实偏临界了,真机上根本飞不出来。我习惯每次仿真后先看 u1 和 u2 的曲线是否平滑,再判断参数是否真的合适。

还可以试着给自己制造麻烦,比如在模型更新那行加入随机扰动或者阵风项,看 PID 能不能扛住。这算是最简单的鲁棒性测试,做一遍之后你对控制器的理解会深一层。

这个代码我后来扩展过好几个版本,加了横滚通道、加了积分限幅、加了方波跟踪和扰动测试,都是在今天这个基础版上一点点补出来的。你跑通之后,建议也按这个路子继续玩下去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询