4、消息定义文件(.msg):.msg文件语法、基本类型、数组、嵌套消息

好,咱们今天聊聊 .msg 文件。这是 uORB 通信的基石,说白了就是定义数据结构的模板。我在 PX4 里折腾了这么多年,发现很多新手上来就写代码,结果消息格式没定义好,后面调试起来那叫一个痛苦。所以这节课,咱们把 .msg 文件彻底讲透。

4.1 .msg 文件长什么样?

先看个最简单的例子。你随便找个 PX4 源码目录,比如 msg/ 文件夹下,打开 vehicle_attitude.msg,大概长这样:

# 这是注释,用 # 号开头
uint64 timestamp        # 时间戳,单位微秒
float32[4] q            # 四元数 [w, x, y, z]
float32[3] gyro         # 陀螺仪数据
float32 roll            # 横滚角
float32 pitch           # 俯仰角
float32 yaw             # 偏航角

嗯,结构很简单。每行一个字段,格式是:类型 变量名。变量名后面可以加注释,用 # 隔开。我个人习惯在注释里写清楚单位,比如 # 单位:弧度,这样别人看代码时不用猜。

4.2 基本类型有哪些?

uORB 支持的基本类型,说白了就是 C/C++ 里那些常见类型。我整理了一张表,你直接看:

类型 C 对应类型 说明
bool uint8_t 布尔值,0 或 1
int8 / uint8 int8_t / uint8_t 8 位有/无符号整数
int16 / uint16 int16_t / uint16_t 16 位有/无符号整数
int32 / uint32 int32_t / uint32_t 32 位有/无符号整数
int64 / uint64 int64_t / uint64_t 64 位有/无符号整数
float32 float 单精度浮点数
float64 double 双精度浮点数
char char 单个字符

这里有个坑,我提醒一下:没有 string 类型。你想传字符串?只能用 char[] 数组。我曾经在项目里想传个设备名称,直接写了 string device_name,结果编译报错。后来改成 char[32] device_name 才搞定。

4.3 数组怎么用?

数组有两种写法:固定长度和可变长度。

固定长度数组

float32[4] quaternion    # 四元数,固定4个元素
int32[3] position        # 位置坐标 [x, y, z]
uint8[6] mac_addr        # MAC 地址

可变长度数组(PX4 里叫 varlen array):

uint8[] data             # 可变长度数据,最大长度由系统限制
float32[] samples        # 采样数据

我个人建议:能用固定长度就别用可变长度。为什么?可变长度数组在内存分配上更复杂,容易出性能问题。我记得有一次调试一个传感器驱动,用了可变长度数组,结果在高速发布时内存碎片化严重,飞控直接卡死。换成固定长度后,问题就解决了。

注意:可变长度数组在 uORB 中最大长度受 ORB_MESSAGE_MAX_BYTES 限制(默认 8KB)。如果你传的数据超过这个值,消息会被截断,不会报错。这坑我踩过,调试了半天才发现数据不全。

4.4 嵌套消息

有时候一个消息里需要包含另一个消息。比如你想定义一个 gps_data.msg,里面包含位置、速度、时间等信息。你可以先定义子消息,再在父消息里引用。

举个例子,先定义 gps_position.msg

float64 latitude    # 纬度
float64 longitude   # 经度
float32 altitude    # 海拔

再定义 gps_data.msg

uint64 timestamp
gps_position position   # 嵌套引用
float32 speed           # 速度
uint8 satellites        # 卫星数量

注意:嵌套消息的引用方式就是直接写子消息的类型名。编译时,uORB 会自动把子消息展开成扁平结构。说白了,最终生成的 C 结构体里,gps_data 会包含 gps_position 的所有字段。

核心要点:嵌套消息不能循环引用。比如 A 引用 B,B 又引用 A,这会导致编译死循环。PX4 的代码生成器会检测这种情况并报错。

4.5 实战:定义一个自定义消息

咱们动手写一个。假设你要定义一个 my_sensor.msg,包含加速度计、陀螺仪和温度数据:

# my_sensor.msg
# 自定义传感器数据

uint64 timestamp        # 时间戳,微秒

# 加速度计数据
float32[3] accel        # [x, y, z],单位 m/s^2

# 陀螺仪数据
float32[3] gyro         # [x, y, z],单位 rad/s

# 温度
float32 temperature     # 单位 摄氏度

# 状态标志
uint8 flags             # bit0: 数据有效, bit1: 校准完成

写完之后,把这个文件放到 msg/ 目录下。然后在 CMakeLists.txt 里加上 my_sensor.msg,重新编译。PX4 会自动生成对应的 C 结构体和发布/订阅接口。

小技巧:我习惯在消息定义里加一个 uint8 flags 字段,用来存各种状态位。比如 bit0 表示数据是否有效,bit1 表示是否校准过。这样不用每个状态都单独定义一个 bool,省空间又方便。

4.6 消息定义的注意事项

  • 字段顺序很重要:uORB 消息的内存布局是扁平的,字段顺序决定了结构体里的偏移量。如果你改了字段顺序,所有订阅者都得重新编译。所以,一旦发布,就别改顺序
  • 对齐问题:uORB 不会自动做内存对齐。比如你写 uint8 a; float32 b;,在 C 结构体里 b 可能没对齐到 4 字节边界,导致性能下降。我建议把大类型(float32、int32)放在前面,小类型(uint8、bool)放在后面。
  • 命名规范:变量名用蛇形命名法(snake_case),比如 accel_xgyro_y。不要用驼峰,因为 PX4 的代码生成器默认转成蛇形,不一致会出问题。

4.7 知识体系图

下面这张图帮你理清 .msg 文件的核心逻辑:

.msg 文件知识体系 .msg 文件 基本类型 int8/16/32/64 uint8/16/32/64 float32/float64 bool, char 数组 固定长度: float32[4] 可变长度: uint8[] 注意内存限制 嵌套消息 引用其他 .msg 自动展开成扁平结构 禁止循环引用 注意事项 字段顺序不可改 | 注意内存对齐 | 使用蛇形命名法

嗯,这张图把咱们讲的内容串起来了。基本类型是砖块,数组是组合方式,嵌套消息是模块化设计。你写 .msg 文件时,就按这个思路来:先选基本类型,再决定用不用数组,最后考虑要不要嵌套。

好了,关于 .msg 文件的语法就讲到这里。记住:消息定义是通信的第一步,定义好了后面省一半的功夫。下次你写自定义消息时,多花 5 分钟把类型和注释写清楚,后面调试时你会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321