前段时间我把一个横列式双旋翼矢量飞行器的仿真模型在Simulink里搭了一遍。原以为参照常规飞行器六自由度建模就够了,结果发现只要涉及旋翼倾转,坐标系、力臂、执行机构限幅全都要重新捋一遍,甚至差点被一个单位换算坑到模型直接发散。趁周末把这次建模和调试的过程整理一下,今天这篇主要写给想用Simulink做类似飞行器仿真的朋友,也顺便记录一下我自己踩过的坑。
这种构型名字有点长,但不复杂:两个旋翼在机身两侧横向排布,每个旋翼可以绕横向轴倾转,让拉力方向在竖直和水平之间变化。于是它既有垂直起降能力,又有高速前飞潜力;控制上又不像四旋翼那样有四个独立升力点,而是需要在“拉力大小”和“倾转角度”之间做分配。Simulink仿真的意义就在这里:先把动力学和控制的逻辑验证清楚,避免一上来就做实物摔机。
1. 为什么折腾这个构型:横列式双旋翼到底香在哪
1.1 从四旋翼和固定翼的痛点说起
四旋翼飞控成熟、机械结构简单,但平飞效率低:所有升力都靠旋翼向下吹,平飞时需要倾斜机身,拉力既要克服重力又要分出一部分向前推,整个能量都耗在旋翼尾流里。固定翼巡航效率高,但垂直起降能力基本没有,要么用跑道,要么靠弹射或伞降回收。横列式双旋翼矢量飞行器走的是中间路线:垂直起飞阶段,旋翼拉力朝上,像直升机一样起降;进入巡航后,旋翼倾转,拉力水平向前,从“升力发动机”切换成“推进发动机”,如果能配上机翼提供巡航升力,效率会非常接近固定翼。
这里需要说明,我不是做气动出身的,所以这个项目的定位很明确:不是验证真实气动性能,而是先把飞行力学和控制律在Simulink里跑通,为后续电路、机构和控制样机打底子。横列式布局在结构上比四旋翼更简单,两个旋翼、两个倾转机构、一套飞控,这个构型的结构简洁特性让它很适合作为矢量飞行器概念验证的起点。
从结构对比角度看,四旋翼需要四个电机四个螺旋桨,任何一个失效就是灾难;纵列式双旋翼需要处理前后旋翼的下洗干扰;横列式双旋翼左右对称,差动油门可以直接产生滚转力矩,两个旋翼镜像反转后反扭矩天然抵消,偏航也可以靠差动拉力实现。虽然气动干扰依然存在,但整体结构比四旋翼更轻,比纵列式更对称,适合做小型化样机。
1.2 矢量倾转让飞行模式从一个变成三个
常规旋翼飞行器的拉力方向基本固定,矢量倾转等于多了一个“控制维度”:倾转角从0度到90度变化,拉力方向从竖直指向水平。这样飞行器可以工作在三种模式下:垂直模式(倾转角接近0度)、过渡模式(倾转角介于两者之间)、平飞模式(倾转角接近90度,由机翼承担大部分升力)。
这三种模式的动力学特征差异很大。垂直模式下,旋翼拉力主要用于平衡重力,机体像一个“胖直升机”,控制目标是姿态和垂向速度;平飞模式下,旋翼拉力主要用来克服阻力,控制目标变成空速和航迹;过渡模式最麻烦,拉力的水平分量和垂向分量同时存在,机体姿态、速度、高度互相耦合,往往还会伴随低头、掉高等现象。
模式多了,建模就麻烦,但这也是最有探索价值的地方。若用Simulink先搭一个参数化模型,就可以把这三个模式用一种统一的动力学方程表达,靠切换控制器结构来覆盖不同模态,不用每个模式单独写一套模型。我就把倾转角当成一个连续状态,从0到90度都允许;控制策略则在内外环结构上做调度,而不是设成硬切换。
我认为做仿真的最大收益就在这里:先把各种逻辑和参数在虚拟环境里试错,等模型行为符合预期了,再迁移到代码生成和实物测试。后面讲到的模型结构,就是围绕“统一方程+模式切换”这个思路搭起来的。
2. 先把方程写清楚:建模前我做了哪些假设
2.1 坐标系和角度定义,以及最容易搞反的符号
实际建模中,坐标系定义往往是第一个绊脚石。我采用的是航空上常用的北东地惯性坐标系,机体系则是x轴沿机头向前、y轴沿机体向右、z轴垂直向下。这样重力加速度在惯性系下是[0; 0; 9.81],机体系坐标变换时只要用常规的欧拉角旋转矩阵就能对应起来。
倾转角我定义成一个绕机体y轴转动的角,正方向为旋翼向前倾转。倾转角δ=0代表拉力垂直向上(沿机体z轴负方向),δ=90°代表拉力水平向前(沿机体x轴正方向)。这个定义的好处是,悬停时倾转角小,控制量接近零;巡航时倾转角接近90度,逻辑清楚。
需要注意的是,Simulink里很多模块用弧度,而控制器和调试界面我习惯用角度。我在初始化脚本里专门做了一组转换常量,后面所有模块的参数都用标准国际单位,只在显示层做角度转换。最开始我吃过一次亏,把角度直接当成弧度送进MATLAB Function,结果悬停时倾转角差了一个数量级,模型直接往一边冲。这个细节看起来小,实际排查起来特别费时间。
2.2 旋翼力与力矩:拉力、反扭矩、倾转角的关系
单个旋翼简化为一个能产生拉力大小T的推力源,方向由倾转角决定,作用点在旋翼安装位置。在机体坐标系下,拉力矢量可以写成:
F_body = T * [sin(δ); 0; -cos(δ)]
这里z分量带负号,是因为坐标z轴向下,而悬停时拉力向上。旋翼在旋转时还会产生反扭矩,方向与旋翼旋转方向相反。两个旋翼采用镜像反转,正常状态下反扭矩互相抵消;一旦两侧拉力不同,反扭矩差就会形成偏航力矩。这个力矩在建模时不能丢掉,否则偏航通道会完全失去控制依据。
旋翼拉力的执行机构(电机和电调)不是瞬时响应的,我给它加了一个一阶惯性环节:T_dot = (T_cmd - T) / τ。τ取0.08秒,代表旋翼升力响应滞后。实际电机和桨的时间常数需要台架测试,但仿真阶段可以先这样处理,至少避免控制律把模型当成理想执行器。
这里还要补充一个容易忽略的点:旋翼拉力的作用点不一定在重心位置。我的安装点在机体坐标系的x方向偏心约0.1米,所以水平拉力会对机体产生俯仰力矩。很多人建模时只关心拉力大小而忽略作用点,到控制阶段就会发现实际力矩和预期完全对不上。我建议从一开始就把安装位置、重心位置全部纳入参数表,不要图省事简化成“所有力都作用在重心”。
2.3 机体动力学:刚体运动和简化气动
把两个旋翼的力和力矩叠加,再加上重力、机身气动力,得到机体坐标系下的合力和合力矩。刚体运动方程用牛顿-欧拉公式,平动部分要记得机体系加速度与惯性系加速度之间的旋转矩阵变换,转动部分则要考虑角速度叉乘惯性张量带来的非线性项:
m * a_b = F_gravity_b + F_rotor_b + F_aero_b I * ω_dot + ω × (I * ω) = M_rotor_b + M_aero_b
气动部分我没有做CFD,而是用一个简单阻力模型:低速时只保留平动阻力,阻力与速度平方成正比,阻力系数按照飞行器的迎风面积粗略估计;角速度阻尼也做了简化处理。这个量级对控制律验证足够,但对气动设计不够,后续会用更精细的查表模型替换。
在仿真初始阶段,我建议把气动阻力先关掉,只保留重力、旋翼力和惯性力。这样可以单独验证控制律是否正确;等姿态控制稳定后,再逐步加入气动项。否则一上来模型就带全套气动,出现问题很难定位到底是控制参数不对还是气动模型有误。
3. 把这些方程塞进Simulink:模型架构和关键模块
3.1 顶层结构:四个模块怎么分工
Simulink模型我拆成了四块:环境模块、旋翼/执行机构模块、六自由度运动模块、控制模块。环境模块放重力、大气密度和风场(后续扩展用);旋翼模块接收控制律给出的拉力指令和倾转角指令,做一阶滞后后再算力与力矩;六自由度模块是核心,输入合力与合力矩,输出位置、速度、姿态和角速度;控制模块则根据状态反馈计算执行器指令。
信号流从控制模块出发,经过旋翼模块生成力与力矩,再进入六自由度模块更新状态,状态反馈回控制模块。环境模块是全局参数总线,所有模块都可以从里面读取重力、密度等常量。这样的分层让每个模块都可以独立测试,后续做硬件在环时也容易把某个模块替换成真实部件。
我强烈建议在顶层模型里使用Simulink的Bus对象来梳理信号,不要一条一条信号线满天飞。横列式双旋翼的状态量很多,比如位置、速度、四元数、角速度、倾转角、执行器饱和标志,如果用离散信号线连接,模型看起来就像蜘蛛网,调试时根本分不清谁是谁。Bus相当于把相关信号打包成“结构体”,在MATLAB Function里也能直接按字段读写,好看也好查错。
3.2 力与力矩合成模块的MATLAB Function实现
力与力矩合成我用一个MATLAB Function来实现,输入是左侧拉力T_L、右侧拉力T_R、左右倾转角δ_L、δ_R、当前机体系角速度Ω,输出是机体系合力F和合力矩M。核心代码只要十几行:
function [Ftotal, Mtotal] = rotor_force_sum(T_L, T_R, delta_L, delta_R) % 左旋翼安装位置 [x; y; z],右旋翼关于xOz平面对称 rL = [0.35; -0.6; 0.1]; rR = [0.35; 0.6; 0.1]; dir_L = [sin(delta_L); 0; -cos(delta_L)]; dir_R = [sin(delta_R); 0; -cos(delta_R)]; FL = T_L * dir_L; FR = T_R * dir_R; Ftotal = FL + FR; % 反扭矩与拉力近似成正比,左右旋翼方向相反 k_torque = 0.02; Mtip_L = [0; 0; -k_torque * T_L]; Mtip_R = [0; 0; k_torque * T_R]; ML = cross(rL, FL) + Mtip_L; MR = cross(rR, FR) + Mtip_R; Mtotal = ML + MR; end注意这里的方向定义是我自己项目里的约定,不一定适合所有布局,但思路可以复用:先定义清楚安装位置和拉力方向,再用力臂叉乘得到力矩,反扭矩单独叠加。这样以后改成不同臂展或不同重心位置,只需要改rL、rR,不需要动控制逻辑。
如果每个旋翼不只是单轴倾转,而是具备两个方向的矢量能力,代码里的拉力方向向量就需要改成两个角度的组合。可以用旋转矩阵把拉力矢量从旋翼坐标系转到机体坐标系。矩阵连乘看起来复杂,但逻辑比手写三角函数清晰很多,后续也容易扩展。
3.3 六自由度运动模块与初始化脚本
六自由度运动模块我用Simulink的经典结构:由力矩方程解角加速度,积分得到机体系角速度,再用姿态运动学方程得到姿态。姿态参数我一开始用欧拉角,后来发现大角度过渡飞行时会出现万向节锁问题,于是换成了四元数,控制回路里再转成欧拉角供调试观察。Simulink自带的姿态计算模块虽然有的版本不太稳定,但至少比自己用欧拉角公式积分要可靠。
仿真开始前,需要有个初始化脚本把所有参数算好。我习惯把所有参数集中在一个结构体变量中,比如param.mass、param.inertia、param.arm、param.rotor_tau,脚本里再写好param到各模块的赋值。这样改参数不用一个个去点模块对话框,而且可以一次性扫描不同参数组合跑批量仿真,后期做参数优化很方便。
% 初始化脚本示例 param.mass = 8.5; % kg param.inertia = diag([0.25; 0.32; 0.42]); % kg*m^2 param.gravity = 9.81; param.rotor_tau = 0.08; % 旋翼升力响应时间常数 param.k_torque = 0.02; % 反扭矩系数 param.rL = [0.35; -0.6; 0.1]; param.rR = [0.35; 0.6; 0.1]; % 控制参数 param.kp_roll = 45; param.kd_roll = 8; param.kp_pitch = 45; param.kd_pitch = 8; param.kp_yaw = 12; param.kd_yaw = 3;所有模块对话框里的常量都直接用param.xxx的形式引用,不单独在界面里填数值。这样仿真结果才能和参数一一对应,而不是改了一大堆界面数字,最后都不知道哪个参数生效。Simulink里可以用模型工作空间或数据字典管理这些参数,我用的模型工作空间,简单够用。
4. 控制回路设计:先跟姿态较劲,再谈飞行
4.1 内环姿态PID:为什么横列式双旋翼缺了阻尼不行
控制我采用内外环结构。内环是姿态控制,外环是位置/速度控制。姿态环通常用三个通道独立PID:滚转通过两侧拉力差控制,俯仰通过两侧倾转角的同步变化控制,偏航通过两侧反扭矩差控制。和四旋翼相比,滚转通道的响应很快,因为两侧拉力差直接产生滚转力矩;俯仰通道则慢得多,因为倾转机构有响应滞后,而且倾转角变化还会扰动拉力方向。
这里必须强调阻尼的重要性。横列式双旋翼如果没有角速度阻尼,光靠比例控制是很难稳定的。倾转角稍微偏一点,水平拉力就会带出俯仰力矩,姿态越偏,力矩越大,最后发散。我在姿态环里把角速度反馈的微分增益加大,相当于模拟一个阻尼器。角速度反馈不能直接从姿态微分拿,最好用单独的陀螺仪信号,在Simulink里就是直接接角速度状态,不要等微分。
悬停状态下,我的初始增益大概是滚转和俯仰比例45,微分8,偏航比例12,微分3。这只是个起点,具体参数还要根据惯量、臂长来调。但经验是:横列式双旋翼的比例增益不需要太大,因为推力矢量产生的力矩往往已经很“直接”,反而微分增益要给足,才能压制倾转机构带来的相位延迟。
4.2 控制分配:四个“操作量”如何映射到六自由度
假设需要期望力矩M_des和期望总升力F_z_des,分配目标就是求解T_L、T_R、δ_L、δ_R。由于倾转角既影响力方向又影响力臂,方程本质上是非线性的,无法像多旋翼那样用一个常值矩阵直接反解。我采用的方法是:在每个采样周期把当前倾转角作为工作点线性化,得到雅可比矩阵,然后用伪逆求解控制量增量。
伪逆的好处是,当系统有冗余执行器时,会自动把控制量分散到各执行器上。比如某时刻需要的俯仰力矩较小,伪逆可能会让左右倾转角各动一点,而不是单边猛推。这种分配方式让控制更柔和,但要注意限幅:倾转角速度限制、拉力变化率限制都要在分配后再次检查,否则控制器算出来的目标值执行机构根本跟不上,反而激发高频振荡。
我实际写分配器时,先把四个输出量归一化:拉力范围0到最大拉力,倾转角范围0到90度,然后再算伪逆。这样避免拉力数值太大时倾转角变化量被忽略。分配完成后再做限幅和反饱和处理,把实际能执行的控制量反馈回去。这一步是后面调试稳定性的关键,我调了整整两天才意识到问题出在这里。
4.3 垂直转巡航的过渡状态怎么处理
垂直起飞时目标点是0速度,固定高度,让倾转角保持在0附近。巡航状态则希望前向速度渐增,高度不变,随着速度增加,机翼升力(如果有)逐渐参与,旋翼倾转角慢慢增加。我在Simulink里把过渡过程做成一个由前向速度触发的状态机:空速低于某阈值时用垂直控制器,高于某阈值时切换到巡航控制器,中间区域用一个线性插值系数混合两组指令。
这个过渡逻辑看起来简单,实际仿真里很容易出现一个问题:如果切换条件只看速度,而速度有波动,控制器会在两个模式之间来回切换,导致倾转角抖动。后来我给切换加了迟滞,低速阈值设为6米每秒,高速阈值设为8米每秒,中间保留2米每秒的死区;并且过渡期间不直接切姿态给定,而是把给定值也做平滑滤波。
过渡过程中高度保持也是个难点。倾转角增大后,拉力垂直分量下降,高度会掉;如果高度控制器反应太快,又会把拉力拉满,产生过大的垂直加速度。我在过渡时把高度误差的积分项稍微降低,让高度回路“慢半拍”,优先保证速度建立,等速度接近目标后再恢复高度权重。这样仿真出来的高度曲线会有一个小的下坠,但总体可控。
5. 仿真调试实录:模型从发散到能飞的排查过程
5.1 第一步就翻车:求解器步长和离散模型的问题
我把模型刚搭好,跑了一个1秒钟的悬停仿真,结果位置曲线直接飞到天上。第一反应是控制参数错了,后来发现根本不是控制器问题:我用了定步长求解器,步长设置成0.1秒,但姿态环需要的控制周期是0.01秒,控制器采样周期跟不上模型动力学,数值积分误差被积累放大。解决办法很简单:把求解器步长改成0.001秒,并把控制律模块做成离散状态更新,采样时间设为0.01秒。这才让积分稳定下来。
另外,反馈信号的处理也要小心。姿态传感器模型如果直接输出连续信号,控制率中的微分项会对噪声特别敏感。我加了一个截止频率5Hz的低通滤波器,让角速度反馈平滑一点,仿真结果接近真实机上传感器的表现。
我后来用不同步长跑了一次对比,步长0.01秒时姿态发散,0.005秒时勉强稳定但有小幅振荡,0.001秒时才完全平滑。这并不意味着所有模型都必须用0.001秒,但至少说明在加入执行机构一阶滞后和控制器离散采样之后,步长必须小于所有动态特征中最快的时间常数,否则仿真结果没有参考意义。
5.2 疑点重重的力臂:一个y轴方向引起的深度排查
有一次仿真里滚转通道突然发散,我以为是增益太高,做增益扫描后依然发散。后来我把左右旋翼安装位置打印出来,才发现rL和rR的y方向符号搞反了。我的机体y轴向右,左旋翼的y坐标应该是负的,右旋翼是正的。但因为左右镜像安装要配合反扭矩方向,代码里我把扭矩符号和坐标一起反了,导致绕z轴的偏航力矩自相矛盾,间接让滚转通道持续发散。
这个问题的排查花了我一整个下午,最后是逐个模块单测时发现的。建议大家在Simulink里做模块级测试,单独给单位拉力、零倾转角,看力与力矩是不是符合手算预期。比如左旋翼拉力10牛,作用点x=0.35、y=-0.6、z=0.1,方向垂直向上,那么对重心的力矩应该只有滚转方向(绕x轴)的项,而俯仰和偏航方向应该为0。如果仿真结果不是这样,那坐标系或力臂定义一定有问题。
这类低级错误在纯代码工程里很容易通过单元测试暴露,在Simulink图形界面下反而容易忽略。我后来专门建了一个“模块级测试”的测试模型,每次改完参数就跑一遍,断言力和力矩的数值与手算一致。这几乎能排除一半以上的低级错误。
5.3 执行机构限幅导致的高频振荡,以及最终的稳定
模型基本稳定后,我开始给执行机构加限幅:倾转角速度限制每秒60度,拉力变化率限制对应脉冲信号。结果一加限幅,悬停不到三秒就开始等幅振荡。原因分析下来是控制分配没有考虑限幅,PID输出被截断后产生积分饱和,控制器还在继续增大积分项,等误差反向时输出又过大,形成了一个极限环。
这在真实无人机上非常常见,也提醒我不要把控制律和限幅分开看待。解决方法是给PID加入积分限幅与抗积分饱和,同时把限幅信息反馈给控制分配器,让分配器知道哪些执行器已经饱和,主动削减对应通道的权重。改完以后,振荡明显减弱,模型可以从任意初始姿态回稳。
具体实现上,我把四个执行器的饱和标志作为布尔量传到控制分配函数。如果某个执行器饱和,分配矩阵就把它对应的列乘以一个很小的权重(比如0.05),让伪逆自动把控制量转移到其他未饱和执行器。这个思路类似于航空飞控里的“控制效能降级”,虽然简单,但在倾斜双旋翼这类强耦合系统里效果很明显。
我整理了一个排查表,方便后续遇到类似问题快速定位:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 高频振荡 | 微分增益过大或反馈噪声 | 增大滤波器时间常数,降低kd |
| 低频极限环 | 执行机构饱和后控制积分继续增加 | 加入抗积分饱和,饱和信息反馈给分配器 |
| 位置缓慢漂移 | 坐标系或单位错误 | 模块级单测,检查重力分量符号 |
| 过渡切换抖动 | 状态机无迟滞 | 增加切换死区,对给定值做平滑 |
6. 跑出来的数据与后续玩法
6.1 一组典型的悬停到前飞过渡数据
修改完限幅后,我让模型从悬停开始,给定2米每秒的前向速度指令,观察倾转角、拉力、姿态角和高度的曲线。整个过程大概分成:前3秒悬停稳定,之后倾转角从0度逐渐增加到约18度,两侧拉力总和先是接近总重,随后因为拉力有水平分量,垂向分量减小,高度控制器会自动加大总拉力来维持高度。水平速度到达目标后,倾转角回减,配平到一个比较小的角度。
这组数据直接验证了前面说的耦合:倾转角一变,高度和速度都在动。如果不把控制分配和过渡逻辑处理好,单独只调高度环,很难调稳。仿真结果也让我确定了后续实物测试哪些参数需要重点标定,比如旋翼力臂、反扭矩系数和拉力响应时间常数。
还有一个小发现:两侧倾转角在过渡过程中并不完全同步。由于机体有滚转扰动,控制分配器会给左右旋翼不同的倾转角修正量,用来抑制滚转引起的耦合力矩。这说明如果我在物理样机上把两个倾转机构做成机械联动锁死,控制性能会差不少。点开这个细节,也是Simulink仿真比纯数学推导直观的地方。
6.2 模型还能怎么扩展:风场、弹性体、硬件在环
这套Simulink模型目前是刚性机体+简化气动,但预留了扩展接口。下一步我打算加两个扩展:一是风场模型,在环境模块里加入平均风和紊流,看看倾转飞行过程中侧风对偏航通道的扰动有多大;二是把旋翼部分替换成更精细的动力模型,包含电机电流、电调和电池电压变化,为续航估算做支撑。
如果后面要做实物,Simulink模型还可以通过自动代码生成部署到飞控板上,做硬件在环测试。这也是模型化设计的优势,之前所有参数、调优、控制分配代码都能复用,而不是每阶段都从头写一遍。
我个人做下来的体会是:横列式双旋翼矢量飞行器的仿真难点不在某一个单独的模块,而在于各模块之间的耦合关系。Simulink的价值恰恰在于可以把这些耦合放到同一个时间域里去看,哪个通道先发散、哪个通道饱和,一目了然。如果你也准备在这个构型上做探索,我建议先花时间把坐标系和分配矩阵搞对,再来谈控制参数,否则后面全是坑。