简介:一套基于Matlab/Simulink的四旋翼飞行器仿真程序,面向无人机控制、自动化和相关专业的高年级本科生、研究生,以及需要快速验证控制算法的研发人员。四旋翼飞行器在航拍、物流配送、环境监测等领域应用广泛,本程序可在无硬件条件下开展飞行控制仿真研究。压缩包共17个文件,其中15个为.m脚本、2个为.mdl模型,占用空间仅44KB;脚本部分覆盖动力学建模、PID控制器设计、姿态与高度控制、传感器滤波以及三维轨迹与姿态角绘图等功能模块,mdl模型则提供图形化的系统级仿真入口,便于直观搭建和修改仿真流程。程序允许用户自定义初始条件与仿真参数,运行后可查看电机转速、位置轨迹和姿态角变化,有助于深入理解四旋翼飞行原理、评估算法稳定性和优化控制策略。目前已有525人学习使用,同类资源中较为轻量、结构清晰,适合作为课程设计、毕业设计或新型控制算法验证的基础工具。
1. 拆开四旋翼Simulink仿真包,先读InitParam与dinamica两个m文件
很多人拿到带 .mdl 的四旋翼仿真包,第一反应是双击模型文件直接点绿色的仿真按钮。但这套程序的逻辑恰好反过来:Simulink 模型 systema2.mdl 和 untitled.mdl 只是把脚本串起来的壳,真正的飞行器动力学、控制律、传感器滤波和结果绘图全部在 m 文件里。模型里大概率是若干个 S-Function 模块,每个模块回调一个脚本,模型本身只负责数据流和时间推进。
这套程序覆盖了四旋翼仿真链路中最关键的几段:初始化参数、全局参数传递、气动与刚体动力学、三通道控制律、转速编码映射、传感器滤波,以及俯仰/滚转/偏航/三维轨迹的绘图。对需要验证 PID 参数、对比不同控制结构或者做课程设计的工程师来说,这是个可以逐步拆开重组的样本。下文从 m 文件调用链开始说起,再落回 Simulink 模型里的模块配置,最后给出验证控制器边界的具体做法。
2. m文件调用链与四旋翼动力学建模:从初始化到电机转速映射
2.1 初始化脚本与全局参数的作用域
InitParam.m 是整套仿真的入口,通常放在模型回调 InitFcn 或者主脚本的第一行。它负责生成一个参数结构体 P,里面包括质量、惯性矩、力臂长度、电机升力系数、反扭距系数、油门限幅、传感器采样周期等。这些参数后续被 aero.m、dinamica.m、控制律脚本共同读取,所以参数命名必须统一,否则很容易出现 "Undefined function or variable" 这类报错。
glob.m 的存在意味着这套代码大量使用了全局变量。老式的 MATLAB 四旋翼仿真喜欢用 global 声明把状态向量或参数结构体广播给所有函数,比如在 glob.m 里写 global P x_ref src_data。这样做在模型层面很简单,但调试时要想清楚变量生命周期:sim 命令执行前必须调用一次 InitParam 和 glob,否则 S-Function 回调函数里访问不到已声明的全局变量。建议在模型的 PreLoadFcn 或 InitFcn 回调里加上这两行。
| 脚本 | 在调用链里的位置 | 一般输出 |
|---|---|---|
| InitParam.m | 最先执行 | 参数结构体 P,包含质量、Ixx/Iyy/Izz、升力系数等 |
| glob.m | 被各脚本回调 | 全局共享的变量声明 |
| aero.m | 每步仿真回调 | 气动力与力矩 |
| dinamica.m | 每步仿真回调 | 状态导数 xd,供积分器使用 |
| U2bin.m / bin2Om.m | 控制量输出到电机之间 | 油门定标、转速映射 |
| sam5_filter.m | 传感器输出后 | 降采样的平滑姿态/速度 |
2.2 动力学内核:欧拉角刚体模型与旋翼力
dinamica.m 是核心中的核心。它接收 12 维状态向量和四个电机的转速输入,返回状态导数。标准四旋翼模型的状态一般取为 [u v w p q r phi theta psi X Y Z],前三个是机体系线速度,中间三个是角速度,最后六个是欧拉角和世界系位置。这里用的是 Z-Y-X 欧拉角顺序,也就是先偏航再俯仰最后滚转。
我在这里按常见做法给出一版可复用的动力学骨架,它和 aero.m 的分工是:aero.m 负责根据转速计算升力和力矩,dinamica.m 负责把力/力矩代入牛顿-欧拉方程求导。
function xd = dinamica(x, u, P) % x : 状态向量 [uvw pqr phi theta psi X Y Z] % u : 四个电机转速平方 (rad/s)^2 % P : InitParam 生成的参数结构体 vx = x(1); vy = x(2); vz = x(3); p = x(4); q = x(5); r = x(6); phi = x(7); th = x(8); psi = x(9); % 旋翼总升力,方向沿机体 -Z 轴 T = P.k * (u(1) + u(2) + u(3) + u(4)); % 机体系重力分量,注意 Z-Y-X 顺序的旋转矩阵 gx = -P.g * sin(th); gy = P.g * sin(phi) * cos(th); gz = P.g * cos(phi) * cos(th); % X 型布局的力矩分配 Mbx = P.k * P.l * (u(1) - u(2) - u(3) + u(4)); Mby = P.k * P.l * (u(1) + u(2) - u(3) - u(4)); Mbz = P.b * (u(1) - u(2) + u(3) - u(4)); xd = zeros(12, 1); xd(1) = r*vy - q*vz + gx; xd(2) = p*vz - r*vx + gy; xd(3) = q*vx - p*vy + gz - T/P.m; xd(4) = (P.Iyy - P.Izz)/P.Ixx * q*r + Mbx/P.Ixx; xd(5) = (P.Izz - P.Ixx)/P.Iyy * p*r + Mby/P.Iyy; xd(6) = (P.Ixx - P.Iyy)/P.Izz * p*q + Mbz/P.Izz; xd(7) = p + q*sin(phi)*tan(th) + r*cos(phi)*tan(th); xd(8) = q*cos(phi) - r*sin(phi); xd(9) = (q*sin(phi) + r*cos(phi)) / max(cos(th), 1e-3); % 世界系速度由旋转矩阵转出,此处省略,直接给状态 10~12 end这段代码里的关键在于力矩分配系数。X 型布局下,滚转力矩由对角线电机转速差产生,俯仰力矩由另一条对角线产生,偏航力矩则依赖正反桨的反扭距差。注意古怪的符号,可别嫌看着令人眼花:这反映了四旋翼内置飞行器的两种相反互联特性。不同机架的电机编号规则不同,拿到现成脚本时首先要确认 u(1)~u(4) 的编号方式和实际电机的旋转方向一致,否则仿真结果会出现滚转和俯仰通道颠倒的现象。
2.3 U2bin 与 bin2Om:整数编码到转速的映射
U2bin.m 和 bin2Om.m 表面看只是数据格式转换,实际模拟的是控制链路里的定标和量化环节。真实飞控输出的是固定占空比的 PWM 或数字信号,不是连续浮点数。U2bin 把 -1~1 的归一化油门映射成一个无符号整数,bin2Om 再把整数解码成电机角速度指令。这样做的意义在于:Simulink 仿真可以对数量化误差,提前验证控制律对转速分辨率是否敏感。
我一般会这样实现它们,可以直接替换进模型里。
function u_bin = U2bin(u, P) % 油门信号 [-1 1] 映射到 16bit 无符号整数 u_sat = max(min(u, P.thr_limit(2)), P.thr_limit(1)); u_bin = uint16((u_sat - P.thr_limit(1)) / ... (P.thr_limit(2) - P.thr_limit(1)) * (2^P.bits - 1)); endfunction w = bin2Om(u_bin, P) % 二进制编码转换成电机角速度,线性插值 w = P.w_min + (P.w_max - P.w_min) * (double(u_bin) / 2^P.bits); end两个脚本配合起来,相当于把控制器的浮点输出强制压低到 16bit 分辨率。如果控制律增益很大而 P.w_min/w_max 范围设计过窄,限幅就会频繁触发,表现成响应曲线在大角度指令下变慢。排查办法很简单:把 U2bin 的输入和 bin2Om 的输出接到同一个工作区变量里,绘制归一化曲线,检查是否在中间段出现明显非线性台阶。
2.4 sam5_filter:降采样与轻量滤波
sam5_filter.m 从命名看是每 N 次采样做一次平滑处理的轻量滤波器。四旋翼仿真里传感器输出往往以较高频率更新,而控制器并不需要那么高的带宽,于是在传感器与控制律之间插一个降采样加平均的环节,一方面模拟真实 IMU 的更新率,另一方面防止高频噪声进入微分项。类似结构在 Pixhawk 等开源飞控的 attitude 估计里也能看到,差别只是开源飞控用互补滤波或 Mahony 滤波,这里用滑动平均降低实现复杂度。
要注意的是,降采样后的信号存在相位延迟。如果把滤波器的采样周期设置成和控制器步长一样,延迟会直接影响姿态环的相位裕度,严重时仿真里能看到持续的极限环振荡。建议把 sam5_filter 的采样周期设为控制周期的 2~4 倍,并在控制律微分项里减小 Kd 系数来匹配滤波引入的滞后。
3. 控制律实做:高度PID、速度外环与反步姿态控制器
3.1 高度控制器原型:油门到竖直加速度的换算
alt_control.m 负责的高度通道,是所有控制器里最容易验证的一个。高度环的输出不是直接给油门,而是给一个竖直加速度指令,再用力学关系反算出总油门。竖直方向方程为 mz_ddot = Tcos(phi)cos(theta) - mg,因此总推力等于质量乘以重力加期望加速度,再除以姿态角的投影系数。
这里给出一版标准高度 PID,微分项直接使用竖直速度,避免用高度差分放大传感器噪声。
function u_thrust = alt_control(h_ref, phi, theta, h, vh, P) persistent int_h if isempty(int_h) int_h = 0; end e = h_ref - h; int_h = int_h + e * P.dt; int_h = max(min(int_h, P.int_sat), -P.int_sat); % 积分限幅 acc_cmd = P.Kp_h * e + P.Ki_h * int_h - P.Kd_h * vh; F_cmd = P.m * (P.g + acc_cmd); u_thrust = F_cmd / (cos(phi)*cos(theta) + 1e-6); end最后的除法是关键。当飞行器倾斜时,总推力的一部分被用于水平方向,竖直升力分量只有推力乘以 cos(phi)cos(theta)。直接把高度环输出当油门,会导致大姿态角下高度掉高,这是初学者最容易犯的错误。分母上加 1e-6 是为了防止姿态角接近 90 度时除零。实测中如果发现高度超调大,优先调 Kp_h,不要动 Ki_h,积分限幅一般取油门最大输出的 20% 以内。
3.2 速度外环:把水平速度偏差转成姿态指令
spd_control.m 是位置环和姿态环之间的桥梁。简单的四旋翼控制器不会直接对位置输出电机转速,而是把水平速度误差转换为期望滚转角或俯仰角。X 方向速度偏差通常对应滚转角,Y 方向速度偏差对应俯仰角,具体耦合关系取决于机头朝向和定义坐标系。
常见做法是使用带限幅的比例控制或 PI 控制:
function phi_ref = spd_control(vx_ref, vx, vy_ref, vy, P) e_vx = vx_ref - vx; phi_ref = P.Kp_v * e_vx; % 速度偏差 -> 滚转角指令 phi_ref = max(min(phi_ref, deg2rad(30)), -deg2rad(30)); % 如果还包含 Y 轴速度误差,则生成 theta_ref,形成级联外环 end这里有个实际调参的顺序问题:必须先让内环姿态控制器能稳定跟踪 30 度的阶跃指令,再去调外环的 Kp_v。如果内环阻尼不足,外环增益稍微加大就会引发振荡,而且无论怎么调外环都压不下去。排查时可以用 Simulink 里的 Data Inspector 同时看 phi_ref 和反馈的 phi,两者如果始终差一个固定相位,说明内环带宽不足,应该增大内环 Kp 或减小期望角速度限幅。
3.3 back_rot_control:反步姿态控制器的构造
back_rot_control.m 对应反步控制法在姿态环的应用。相比 PID,反步控制的优势在于通过李雅普诺夫函数直接构造力矩指令,能够保证闭环稳定性,对姿态角指令的跟踪响应也更平滑。控制结构分两层:第一层定义姿态角误差,并通过虚拟控制律生成期望角速度;第二层定义角速度误差,用力矩指令把角速度误差驱向零。
function tau = back_rot_control(ref_att, x, P) phi = x(7); theta = x(8); psi = x(9); p = x(4); q = x(5); r = x(6); e_att = ref_att - [phi; theta; psi]; % 第一层:虚拟控制给出期望角速度 p_ref = P.c1 * e_att(1); q_ref = P.c2 * e_att(2); r_ref = P.c3 * e_att(3); % 第二层:角速度误差,并构造力矩 ep = p - p_ref; eq = q - q_ref; er = r - r_ref; tau_phi = P.Ixx * (-P.c4*ep - e_att(1)) - (P.Izz - P.Iyy)*q*r; tau_theta = P.Iyy * (-P.c5*eq - e_att(2)) - (P.Ixx - P.Izz)*p*r; tau_psi = P.Izz * (-P.c6*er - e_att(3)) - (P.Iyy - P.Ixx)*p*q; tau = [tau_phi; tau_theta; tau_psi]; end反步控制的时间常数控制逻辑:c1/c2/c3 决定姿态角误差的收敛速度,c4/c5/c6 决定角速度误差的收敛速度。调参时先固定 c1/c2/c3,增大 c4 直到出现高频振荡,再回退到 50% 附近。这套方法的坑在于它对模型参数比较敏感,尤其是 Ixx、Iyy、Izz 这三个惯性矩如果有 20% 以上的偏差,反步效果会明显劣于鲁棒 PID。所以拿到别人给的 back_rot_control.m 后,先确认 InitParam.m 里的惯性矩和自己的机架匹配,否则控制器可能把抖振当成稳定。
4. 在systema2.mdl里组装仿真:S-Function、求解器与常用排错
4.1 模型文件里该有什么模块
打开 systema2.mdl 之前,先理清模型应该包含哪几类模块,再看实际文件是否吻合。最小可用的四旋翼仿真模型至少要有参考指令输入、控制器模块、动力学模块、传感器模块和信号记录模块。控制器和动力学如果写成独立 m 文件,在模型里通常以 S-Function 或 MATLAB Function 块形式出现。
| 模块类型 | 模型内放置方式 | 对应的 m 文件 / 配置 |
|---|---|---|
| 参考指令 | Step 或 From Workspace | 工作区里的 h_ref、vel_ref 等变量 |
| 控制器 | MATLAB Function 或 S-Function | alt_control.m、spd_control.m、back_rot_control.m |
| 动力学 | Level-2 S-Function | dinamica.m、aero.m |
| 转速映射 | 普通子系统或 MATLAB Function | U2bin.m、bin2Om.m |
| 传感器滤波 | Discrete 子系统或 S-Function | sam5_filter.m |
| 数据记录 | To Workspace / Scope | yout、sout 等变量 |
如果模型里只有 S-Function 块而没有直接画出的框图,不要慌。双击 S-Function 块,在 Parameters 栏里会看到调用的脚本名和传参列表。例如一个名为 sf_dinamica 的块,参数可能是P,函数内部调用dinamica(x, u, P)。修改模型结构前先确认这些参数是存在的,否则运行时会报 "Invalid input argument"。
4.2 求解器、步长与初始化顺序
四旋翼动力学是连续的刚体运动方程加上离散的采样滤波和控制律,混合系统。Simulink 默认的可变步长求解器 ode45 虽然仿真速度快,但在姿态角接近奇异点或力矩突变时容易让误差估计失控,从而导致步长骤降甚至仿真卡死。更稳妥的做法是选用固定步长求解器 ode4(四阶龙格库塔),固定步长设成 1e-3 秒。
mdl = 'systema2'; load_system(mdl); set_param(mdl, 'Solver', 'ode4'); set_param(mdl, 'FixedStep', '1e-3'); set_param(mdl, 'StopTime', '30'); out = sim(mdl);固定步长的含义是每一拍都执行固定次数的函数求值,不会因为状态变化剧烈而自动缩小步长,因此必须保证步长小于系统最小时间常数的十分之一。四旋翼姿态环的时间常数一般不低于 0.05 秒,1e-3 的步长足够。如果模型里还有 PWM 生成或 CAN 报文发送模块,比如在 simulink 里做故障注入或与外部设备联合仿真,固定步长还需要和那个模块的采样时间对齐,否则会看到周期性的尖峰干扰。
初始化的顺序也很关键。直接在工作区里运行InitParam; glob;再点运行,和把这两行写到模型的 InitFcn 回调里,两者的时序不同。写进回调的好处是每次仿真开始前自动重置全局变量,不会因为上一次仿真残留数据而污染本次结果。代价是回调里的错误更难定位,一旦 InitParam 内部报错,Simulink 会直接终止整个模型加载流程。
4.3 常见编译与运行错误排查
第一类错误是找不到函数。表现为 "Undefined function 'dinamica'",原因要么是脚本不在 MATLAB 路径里,要么是当前工作目录不对。用 addpath 把脚本目录加到路径集,然后在命令窗口执行 which dinamica 确认路径生效。
第二类错误出现在 S-Function 配置上。提示 "Block 'systema2/...' cannot be used in a normal mode simulation" 或 "S-Function does not exist" 时,检查 S-Function 块的函数参数个数是否与脚本定义一致。比如 dinamica.m 定义的是三个参数(x, u, P),而模型参数里填的是(x, u, P, dt),就会直接报错。还有一个隐蔽点:如果脚本文件名和函数名不一致,MATLAB 会按脚本名查找,查不到就报错。代码里经常出现文件名是 alt_control 但函数第一行写成 function out = alt_ctrl,这种不一致在配置文件里最难发现。
第三类错误是仿真发散。表现是输出曲线在某个瞬间突然跳到 NaN 或正负无穷。先用debug模式单步执行动力学脚本,检查输入转速是否出现负值或超大值。负转速的原因是控制量经过限幅后仍然小于电机最低转速门槛,bin2Om 按线性插值时可能映射出负值,这时要在 bin2Om 入口加 max 限幅。
5. 用draw_results系列校验收敛性,再叠加传感器噪声与风扰
5.1 把批处理仿真和绘图脚本接起来
draw_results_roll.m、draw_results_pitch.m、draw_results_yaw.m 和 draw_results_3D.m 这几个脚本分别对应三种姿态角和三维轨迹的绘图。手工点一次仿真后再逐个运行这几个脚本,只能看到单组曲线。更高效的做法是把仿真和绘图放进同一个批处理循环里,对控制器参数做扫描对比,这样才能看出来哪一组增益在超调量和收敛时间上更合适。
kp_range = 0.5:0.5:2.5; for i = 1:length(kp_range) P.Kp_h = kp_range(i); assignin('base', 'P', P); sim('systema2'); draw_results_roll; if i == 1 hold on; end legend_info{i} = sprintf('Kp=%0.1f', kp_range(i)); end legend(legend_info);这个循环把每组 Kp_h 对应的滚转响应叠画在同一张图里。注意 assignin 的作用是把 P 写回基础工作区,因为 sim 命令从基础工作区读取参数。如果不写这句,循环里改的 P 只在当前函数作用域有效,Simulink 读到的永远是旧值。仿真的结果还会存储在 out 变量里,等循环结束后再统一处理,避免反复调用绘图脚本覆盖图形窗口。
5.2 叠加噪声后验证控制器鲁棒性
只调试纯动力学模型的控制器参数,得到的结果偏乐观。实际飞行里陀螺仪和加速度计都有噪声,传感器输出的姿态角误差通常在 0.1 度到 1 度之间。验证鲁棒性的简单做法,是在传感器反馈路径上叠加一个 Band-Limited White Noise 模块,噪声功率参考真实 IMU 的数据手册,采样时间与 sam5_filter 的降采样周期保持一致。
考察指标不只看响应曲线是否贴合理想指令,还要看控制量的平滑程度。如果噪声功率加大后电机转速指令开始高频抖动,说明微分项增益过高,需要降低 Kd 或者在微分项前再串一个低通滤波器。这里能直观体会到 U2bin 量化带来的影响,16bit 编码的量化噪声通常不明显,但换成 8bit 编码后,悬停状态下的转速指令会持续跳动,这属于量化极限环,不是控制算法问题。
5.3 用风场突变判断姿态环裕度
最后推荐一个可复现的测试:在仿真进行到 10 秒时加入水平风场突变,观察控制器能否在 2 秒内恢复姿态。具体做法是在动力学脚本里增加一个外部力输入,风力简化模型取 F_wind = 0.5rhoCdAv_wind^2,方向沿机体轴投影。风场可以先从常量开始测试,再加随机阵风段。
把同一组风场数据轮流喂给 PID 姿态控制器和反步姿态控制器,比较滚转角误差的积分值,就能直观看到反步控制在模型参数准确时对姿态扰动的抑制效果更好。这个测试还能用来判断当前姿态环的相位裕度:如果风突变后姿态角出现两个以上周期的衰减振荡,说明控制参数偏软,应该同步增加内环增益而不是单独调大外环增益。
本文还有配套的精品资源,点击获取