前阵子调一套无人机姿态控制器,算法侧落在 MATLAB/Simulink 里,飞控固件用 PX4,手头没有合适的硬件飞控板,就想着先跑软件在环仿真把控制逻辑打通。结果发现一个问题:PX4 SITL 最舒服的环境是 Ubuntu,而我的控制律模型、数据分析和可视化全在 Windows 的 MATLAB 里。两边各有所长,但数据怎么实时拉通,成了绕不开的第一步。
折腾了两三天,总算把这条链路完整跑通:Ubuntu 里的 PX4 SITL 持续往外发姿态数据,Windows 里的 MATLAB/Simulink 实时收、实时解析、实时喂给控制律。整个过程踩了不少坑,有些坑在文档里根本找不到,网上的资料又只讲半截。所以我把这套“Ubuntu PX4 SITL 发送姿态数据至 Windows MATLAB/Simulink”的完整过程整理成文,从方案选型、环境准备、数据转发,到 Simulink 接收解析和联调排错,尽量把每一步背后的“为什么”也说清楚。如果你也在做无人机控制算法仿真,或者需要在两套系统之间搬 MAVLink 数据,这篇文章应该能帮你省下不少时间。
1. 跨平台联合仿真的方案选型:为什么是 PX4 SITL + UDP 桥
1.1 仿真需求拆解
做控制算法验证,通常需要一个“被控对象”和一个“控制器”。控制器放在 Simulink 里开发,利用它的信号处理、自动代码生成和数据分析能力;被控对象则希望尽量接近真实飞行,能用 PX4 的各种状态估计、姿态控制和传感器模型。PX4 SITL(Software In The Loop)正好满足后者:它在 Ubuntu 上把整条飞控固件编译成 Linux 原生程序,配合 Gazebo 仿真环境模拟传感器和物理模型,不需要真实飞控硬件就能跑出接近实机的姿态响应。
问题是,PX4 SITL 和 MATLAB/Simulink 默认并不认识对方。PX4 的对外接口主要是 MAVLink 协议,Simulink 虽然能通过 UDP 接收数据,但不知道如何去理解和组织 MAVLink 字节流。所以中间需要一层“翻译”,或者更准确地说,需要一个跨平台、跨协议的数据通路。
1.2 为什么不用共享文件或 ROS 桥接
我一开始考虑过几种方案,简单对比一下:
| 方案 | 优点 | 明显问题 |
|---|---|---|
| 共享文件/日志回放 | 实现简单,离线可用 | 延迟秒级以上,做闭环控制完全不可用 |
| ROS/ROS2 桥接 | 生态完整,数据格式清晰 | 为了两个节点的通信,得装一整套 ROS,维护成本高 |
| Windows 下跑 PX4 SITL | 环境统一,不用跨机器 | 编译依赖多,Gazebo 性能差,和实机环境差距大 |
| UDP 转发 MAVLink | 轻量、延迟低、可扩展 | 需要自己写转发和解析,但工作量可控 |
最终选了 UDP 转发 MAVLink。原因很直接:SITL 本来就在通过 UDP 对外发送 MAVLink,Ubuntu 上写个小脚本就能读;MATLAB/Simulink 的 UDP Receive 模块又是现成的,只要把姿态消息从 MAVLink 报文里解析出来就能用。中间没有第三方框架,问题排查路径非常短。
1.3 整体数据通路
整套链路长这样:
- Ubuntu 侧:PX4 SITL 启动后,把 MAVLink 遥测数据写向本机回环地址
127.0.0.1:14550。 - Ubuntu 侧:一个 Python 转发脚本连接这个本地端口,读取 MAVLink 消息,从中提取姿态消息,再通过 UDP 单播发到 Windows 主机指定端口。
- Windows 侧:Simulink 里的 UDP Receive 模块接收数据,通过 MATLAB Function 块解析出 roll、pitch、yaw 等姿态信息,送给控制律或记录到 Workspace。
这套结构的核心价值在于,它把 PX4 的生态和 MATLAB 的生态解耦了。PX4 侧不用关心谁在消费数据,Simulink 侧也不用去理解 PX4 内部实现,两边通过一个极简的 UDP 通道对接。对仿真验证来说,这种“低耦合”是最重要的——任何一侧坏了,都能单独调试。
2. Ubuntu 侧启动 PX4 SITL,先把姿态流跑起来
2.1 环境准备与编译
PX4 SITL 需要 Ubuntu 环境,我用的是 20.04 和 22.04 都试过,官方工具链脚本基本无痛。大致流程是先准备编译工具链、Gazebo 依赖,然后拉取 PX4 固件源码、初始化子模块。第一次编译时间会比较长,建议给机器留足磁盘和内存。
启动 SITL 的方式很简单,在 PX4 源码目录下执行:
make px4_sitl gazebo-classic如果编译和模型都正常,会看到一个 Gazebo 窗口弹出,同时终端进入 PX4 shell,出现pxh>命令提示符。这时候 PX4 已经在运行,并且默认创建了 MAVLink 实例。其中127.0.0.1:14540是给外部地面站用的,127.0.0.1:14550是常用的遥测通道。这里注意,SITL 默认监听的是本机回环地址,也就是说只有在 Ubuntu 本机上才能直接读取这些端口。
2.2 确认 ATTITUDE 消息确实在往外发
在写任何转发脚本之前,一定要先在 Ubuntu 侧验证姿态数据是否存在。这一步很多人跳过,后面一联调就抓瞎。我习惯用一个很小的 pymavlink 脚本确认:
from pymavlink import mavutil mav = mavutil.mavlink_connection("127.0.0.1:14550") while True: msg = mav.recv_msg() if msg is not None and msg.get_type() == "ATTITUDE": print(msg.to_dict())如果终端里不断打印出 ATTITUDE 消息,说明姿态流已经建立。如果没有输出,多半是 SITL 没有正常起飞状态,或者端口对不上。可以先用 MAVProxy 连上master=127.0.0.1:14550手动检查,再用这个小脚本。
2.3 MAVLink ATTITUDE 报文的字段构成
要解析姿态数据,必须知道 ATTITUDE 消息长什么样。MAVLink v1 报文的整体结构是:报文前缀、长度、序列号、系统 ID、组件 ID、消息 ID,之后是载荷,最后是两位校验字节。
其中 ATTITUDE 的消息 ID 是 30,载荷固定 28 字节,也就是 7 个 4 字节的小端字段:
| 字段 | 类型 | 意义 |
|---|---|---|
| time_boot_ms | uint32_t | 飞控启动以来的毫秒数 |
| roll | float | 横滚角,单位弧度 |
| pitch | float | 俯仰角,单位弧度 |
| yaw | float | 偏航角,单位弧度 |
| rollspeed | float | 横滚角速度,单位 rad/s |
| pitchspeed | float | 俯仰角速度,单位 rad/s |
| yawspeed | float | 偏航角速度,单位 rad/s |
加上 6 位固定头、1 位长度字段和 2 位校验码,一帧完整的 ATTITUDE 报文总共 36 字节。这个长度是固定的,听起来不起眼,但后面在 Simulink 里配置 UDP 接收缓冲区时,它特别关键。
3. 姿态数据转发脚本:从回环地址搬到局域网
3.1 为什么非要在 Ubuntu 侧做转发
有人会问:既然 PX4 SITL 已经通过 UDP 发 MAVLink,为什么不直接在 Windows 的 Simulink 里连 Ubuntu 的 IP 和端口?
关键就在前面提到的那句:PX4 SITL 默认把遥测端口绑在127.0.0.1,也就是只监听本机回环。Windows 那台机器无论在物理网络上怎么配置,都访问不到另一个机器的回环地址。就算你把端口配置改到0.0.0.0,让 SITL 对局域网广播,也会引入很多不可控因素,比如防火墙拦截、多网卡选择、数据风暴。
所以我选择在 Ubuntu 本地先用脚本读取 MAVLink 流,再把需要的姿态消息用新的 UDP 连接发往 Windows 的指定 IP 和端口。这样 PX4 的行为完全不用动,转发层是独立的,后来排错也方便。
3.2 Python 转发脚本实现
脚本不复杂,核心逻辑就是“读 MAVLink,筛 ATTITUDE,发给 Windows”:
#!/usr/bin/env python3 import socket import time from pymavlink import mavutil MAVLINK_SRC = "127.0.0.1:14550" WINDOWS_IP = "192.168.1.100" WINDOWS_PORT = 14560 mav = mavutil.mavlink_connection(MAVLINK_SRC) udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) print("forwarding ATTITUDE ->", WINDOWS_IP, WINDOWS_PORT) while True: msg = mav.recv_msg() if msg is None: time.sleep(0.001) continue if msg.get_type() == "ATTITUDE": buf = msg.get_msgbuf() udp.sendto(buf, (WINDOWS_IP, WINDOWS_PORT))其中msg.get_msgbuf()返回的是包含完整 MAVLink 头部、载荷和校验字节的原始报文。直接用这个字节流,Windows 侧就能按标准 MAVLink 格式解析,不依赖自定义协议。
我在实际跑的时候,会在转发脚本里加一个打印计数,比如每 100 帧打一次日志,用来确认数据是否持续在发。这个习惯对后面联调很有用,尤其当 Simulink 端没数据时,你至少能判断是转发脚本挂了还是解析端挂了。
3.3 只转 ATTITUDE 还是转全部消息
这个问题取决于你后面要做什么。如果只是拿姿态角做控制,强烈建议只转 ATTITUDE,理由有两点。
第一,固定长度好解析。每一帧 36 字节,Simulink 端不需要处理变长包,UDP 缓冲区配置会简单很多。
第二,减少无效流量。MAVLink 里大量消息是心跳、电池状态、GPS 信息等,全转发到 Windows 不仅浪费带宽,还容易在 Simulink 端造成消息错位。
当然,如果以后想在 MATLAB 里做更多数据融合,比如同时要角速度、加速度计原始值,那就得扩展转发逻辑。一个稳妥做法是在转发脚本里对多个消息 ID 做过滤,比如同时转发 ATTITUDE 和 LOCAL_POSITION_NED。Simulink 端可以靠消息 ID 区分数据类型。
3.4 提高姿态消息频率
默认情况下,PX4 的 MAVLink 流速率可能不是很高,做控制律验证时会感觉姿态数据“很肉”。我一般会再把 ATTITUDE 的频率拉高。PX4 支持通过MAV_CMD_SET_MESSAGE_INTERVAL命令动态设置消息间隔,单位是微秒。比如想让它 100 Hz 发送,间隔就是 10000 微秒。
在转发脚本里,可以在连接建立后给飞控发送一条命令请求:
mav.mav.command_long_send( mav.target_system, mav.target_component, mavutil.mavlink.MAV_CMD_SET_MESSAGE_INTERVAL, 0, 30, 10000, 0, 0, 0, 0, 0 )具体参数在 MAVLink 标准里都有明确定义。这条命令在 SITL 环境里执行非常安全,毕竟不是真机,不用太担心设置错误导致不可逆的后果。
4. Simulink 接收与解析:UDP 字节到控制信号的最后一公里
4.1 UDP Receive 模块的参数配置
到 Windows 侧之后,事情就开始变得直观了。Simulink 里接收 UDP 数据最常用的模块是 UDP Receive,一般在 DSP System Toolbox 或 Communications Toolbox 的模块库里能找到。核心参数就三四个:
Local IP port:Windows 侧监听端口,要和转发脚本里的WINDOWS_PORT一致,比如 14560。Output data type:选uint8,因为 MAVLink 就是字节流。Maximum packet size:设成 36,正好一帧 ATTITUDE。如果担心以后扩展消息类型,可以设成 255,但解析时要用第一个字节判断长度。Sample time:根据控制周期来,比如 0.01 秒,也就是 100 Hz。
这里有个容易被忽略的点:Simulink 的 UDP Receive 默认可能把输出当成一个矩阵。解析前最好在 MATLAB Function 块里用pkt = pkt(:)把它拉成一维列向量,免得代码里因为维度问题报错。
4.2 在 MATLAB Function 里解析 ATTITUDE
我习惯在 Simulink 里加一个 MATLAB Function 块,输入是 UDP Receive 输出的uint8向量,输出是解析好的姿态角和时间戳。解析代码大致这样:
function [time_boot_ms, roll, pitch, yaw, roll_rate, pitch_rate, yaw_rate] = parse_mavlink_attitude(pkt) pkt = pkt(:); if length(pkt) < 36 [time_boot_ms, roll, pitch, yaw, roll_rate, pitch_rate, yaw_rate] = deal(0, NaN, NaN, NaN, NaN, NaN, NaN); return; end % MAVLink v1 header: STX=0xFE, len, seq, sysid, compid, msgid msgid = pkt(6); % ATTITUDE message ID = 30 if msgid ~= 30 [time_boot_ms, roll, pitch, yaw, roll_rate, pitch_rate, yaw_rate] = deal(0, NaN, NaN, NaN, NaN, NaN, NaN); return; end % payload starts at offset 7 (MATLAB index 7), 28 bytes for ATTITUDE payload = pkt(7:34); % first 4 bytes: uint32 time_boot_ms (little-endian) time_boot_ms = double(typecast(uint8(payload(1:4)), 'uint32')); % next 24 bytes: 6 x float32 (little-endian) f = typecast(uint8(payload(5:28)), 'single'); roll = double(f(1)); pitch = double(f(2)); yaw = double(f(3)); roll_rate = double(f(4)); pitch_rate = double(f(5)); yaw_rate = double(f(6)); end这里要特别说明的是,MAVLink 是小端字节序,而 x86 架构的 Windows 主机也是小端,所以直接typecast没问题。如果你用的是苹果电脑上的 MATLAB,就需要考虑字节序反转,否则解析出来的数值会非常离谱。
严格来说,完整解析还要校验报文末尾的两个 CRC 字节。我在联调过程中是先抓包确认链路干净之后,才在解析函数里跳过校验,这样能少写一版 CRC 逻辑。但如果你要让这套东西长期稳定跑,或者数据要用于安全关键场合,CRC 校验不能省。
4.3 把解析结果接进控制模型
解析出 roll、pitch、yaw 之后,链路基本就通了。你可以直接把信号接到 Scope 观察,或者接到控制器输入端,也可以记录到 Workspace 里做离线分析。
我建议第一步先只接 Scope 和 To Workspace,把模型跑起来,手动推一下 Gazebo 里的无人机姿态,看曲线是否跟着变。这一步能快速确认整个链路都是通的,之后再去接控制器。不要一上来就接控制闭环,否则问题定位会很痛苦。
5. 联调阶段最容易翻车的五个细节
5.1 Windows 防火墙默认会拦 UDP
第一次联调时,Ubuntu 侧脚本噼里啪啦往前发,Windows 侧 Simulink 静悄悄,一个包都收不到。查了半天,是 Windows 防火墙把入站 UDP 包默拒了。
解决办法很简单:在 Windows 防火墙里新建一条入站规则,允许 UDP 14560 端口,或者临时允许 Ubuntu 主机 IP。调试阶段也可以选择“专用网络”范围。改完防火墙后最好先别急着跑 Simulink,用 Wireshark 或者一个小 UDP 回显脚本测一下能不能收到包,确认通了再开模型。
5.2 UDP 接收缓冲区太小导致丢包
姿态数据如果是 100 Hz,36 字节一包,看起来流量不大,但 Simulink 的 UDP 模块默认缓冲区未必够。如果控制器里发现姿态信号时不时跳变几拍,大概率是缓冲区满之后丢包或者读到了旧包。
我把 UDP Receive 的Receive buffer size直接调到了 65535 字节,足够放下几秒钟的数据量。同时 Sample time 设置得和数据源频率匹配,别让 Simulink 采样频率比发送频率慢太多。姿态数据更新频率 100 Hz,控制器采样频率设在 100 Hz 或者 200 Hz 都是合理选择。
5.3 消息长度和消息 ID 对齐
如果某天你发现解析出来的偏航角偶尔变成离谱值,先检查是不是混入了别的 MAVLink 消息。转发脚本只过滤 ATTITUDE,理论上不会,但一旦以后扩展转发了其他消息,Simulink 端一定要先判断消息 ID,再决定怎么解析。
我遇到过一种情况:UDP Receive 配置的包大小大于实际包长时,输出向量末尾会被填零。如果你直接用length(pkt)判断长度,可能得到很大的值,导致解析位置错乱。所以解析函数里必须先检查消息 ID,并在取 payload 时严格指定pkt(7:34)这一段。
5.4 time_boot_ms 和 PC 时钟不要混用
姿态消息里的time_boot_ms是 PX4 飞控启动以来的毫秒数,不是 UTC,也不是 MATLAB 仿真时间。如果只是想看数据曲线,问题不大;但如果要在 Simulink 里做时间同步、数据记录和算法辨识,千万不要把time_boot_ms直接当仿真时钟用,它的零点在 Ubuntu 侧 SITL 启动那一刻。
我通常的做法是:在 Simulink 里维护一个自己的仿真时钟,把time_boot_ms仅作为数据包的时间戳记录,用于排序和丢包判断。跨平台时间同步非常麻烦,SITL 场景下用相对时间就足够了。
5.5 弧度、角度、欧拉角顺序不是小事
PX4 的姿态角是弧度,MAVLink 的 float 字段也是弧度。但 MATLAB 的很多 Aerospace 模块和示波器默认使用度数。如果你直接把 roll/pitch/yaw 接给一个期望角度是度的模块,仿真结果会非常“有意思”。
另一个坑是欧拉角顺序。PX4 使用的姿态表示通常对应机体坐标系到 NED 坐标系的旋转,roll-pitch-yaw 的顺序是明确规定的。Simulink 里很多姿态转换模块默认的旋转顺序不一定相同。做控制律时,务必确认坐标系和旋转顺序是一致的,否则看起来每个角度都合理,合在一起姿态就是错。
6. 把单向往闭环延伸:后续能做的事
6.1 从姿态数据到控制指令
打通“PX4 SITL 到 Simulink”这一条单向链路之后,最自然的下一步是把控制指令从 Simulink 发回给 PX4。做法和转发类似,只是方向反过来:Simulink 里用 UDP Send 模块把自定义的控制指令发给 Ubuntu 上的 Python 脚本,脚本再把指令转换成对应 MAVLink 消息,比如SET_ATTITUDE_TARGET,灌回 PX4。
这个闭环如果在 SITL 环境里跑通,后面做真机移植会更有谱。但要注意,真机上回灌控制指令的风险等级完全不同,SITL 里怎么折腾都行,真机一定要有急停和安全保护。
6.2 数据记录与离线辨识
建立了数据链路之后,我顺手把姿态角和角速度记录到了 Workspace,然后用系统辨识工具箱做了一下飞机模型的离线辨识。这套组合非常合适:PX4 SITL 提供高度可控的激励环境和相对真实的飞行动力学,Simulink 做信号采集和辨识计算,两边数据通过 UDP 无缝衔接。
如果按固定幅值、固定频率的扫频信号激励飞机,记录攻角、姿态角和角速度响应,可以得到一个用于控制器设计的简化模型。这种做法比纯数学建模更贴近实际。
6.3 一点个人体会
整套链路做完,我最大的体会是:跨平台联合仿真真正难的不是某一项技术,而是出了问题之后怎么快速定位。以前我会急着去写复杂的解析代码、直接上控制器,后来发现最省时间的方式反而是先把数据通路单独跑通,再用最简单的 Scope 验证,最后才接控制逻辑。UDP 和 MAVLink 本身都不复杂,复杂的是你没有给自己留出足够的观测点。所以我强烈建议,转发脚本里多加打印、Simulink 里多加 Scope、防火墙改完先测再跑。每一步都看得见数据,联调就会顺利得多。这套链路跑顺之后,后面的控制算法验证基本就无感了。