2. ULog文件格式详解

ULog是PX4自v1.7.0起采用的统一日志格式,设计目标是在嵌入式系统上实现低开销、高可靠、可扩展的飞行数据记录。与早期PX4使用的SDLog2格式相比,ULog在写入效率、数据压缩、元数据自描述性和跨平台兼容性上均有显著提升。理解ULog的底层结构,是进行日志回放、控制指令对齐和故障分析的前提。本节将从文件组成、消息类型、数据块布局、元数据定义和时间戳机制五个维度展开,并结合实际飞行日志进行拆解。

文件头 magic + version + timestamp 定义区 消息类型 + 主题元数据 数据区 消息数据块 + 索引 索引区 msg_info + 数据索引 消息类型 · B: bool · c/i/l: int8/16/32 · C/I/L: uint8/16/32 · f/d: float/double · M: 数组/字符串 数据块类型 · 添加/移除日志 · 订阅/取消订阅 · 参数设置 · 消息数据 · 同步/丢帧标记 时间戳机制 · 硬件时间戳 (µs) · 软件时间戳 (µs) · 时间同步 (PX4/ROS) · 时间偏移补偿 · 时间基准 (boot time) 元数据 · 版本信息 · 系统参数 · 硬件配置 · 固件版本 · 消息定义 ULog 解析流程 读取文件头 解析定义区 构建消息映射 读取数据区 时间戳对齐 图:ULog文件整体结构、消息类型、数据块、元数据与时间戳机制关系图

2.1 ULog的组成结构与文件头

ULog文件在物理上是一个二进制字节流,逻辑上由四个连续区域构成:文件头(Header)、定义区(Definition Section)、数据区(Data Section)和索引区(Index Section)。文件头固定为16字节,包含三个关键字段:魔数(Magic)、版本号和初始时间戳。魔数为ULog的ASCII码加上0x01,用于快速识别文件类型;版本号当前为0x01;初始时间戳是系统上电后的微秒计数,作为整个日志的时间基准点。

定义区紧随文件头之后,记录了所有消息类型的完整描述。这部分是ULog自描述能力的核心——解析器无需外部配置文件,仅凭定义区即可还原所有数据字段的含义。定义区由若干条定义消息组成,每条定义消息包含消息类型、消息名称、字段列表(字段名+字段类型)以及可选的初始值。例如,传感器加速度计的消息定义会声明timestamp(uint64_t)、x/y/z(float)等字段。

数据区是日志的主体,按时间顺序存储所有实际飞行数据。每条数据消息包含一个8字节的消息头(消息ID+时间戳)和紧随其后的消息体。索引区位于文件末尾,提供快速跳转能力,包含msg_info列表和数据块索引,使得日志工具可以在不扫描全部数据的情况下定位特定主题。

核心原理:ULog采用“先定义、后使用”的流式写入策略。飞行控制器在记录日志时,首先写入定义区(描述所有可能出现的消息类型),然后持续写入数据区。这种设计使得日志文件可以在飞行过程中实时生成,无需预先分配固定大小的缓冲区,也无需在结束时重写文件头。

2.2 消息类型与数据块详解

ULog定义了丰富的数据块类型,每种类型通过一个字节的msg_type字段区分。下表汇总了主要的数据块类型及其用途:

数据块类型msg_type值用途说明典型示例
Flag Bits0x01标记日志属性(如是否包含消息信息)日志格式版本标记
Add Logged Message0x02定义一个新的消息类型定义sensor_combined消息
Remove Logged Message0x03移除不再使用的消息类型飞行中动态取消订阅
Add Subscription0x04订阅一个uORB主题订阅vehicle_attitude
Remove Subscription0x05取消订阅uORB主题取消订阅debug_value
Parameter Set0x06记录参数值变更记录MC_ROLL_P参数调整
Parameter Default0x07记录参数默认值记录出厂默认参数
Message Data0x08实际飞行数据消息IMU采样数据、姿态估计值
Sync0x09字节对齐同步标记确保数据流对齐
Dropout Mark0x0A标记数据丢失区间SD卡写入延迟导致的数据缺口
Logging Tag0x0B日志标签(用于过滤)标记特定调试会话

其中,Message Data是最核心的数据块,其结构为:2字节的消息ID(对应定义区中的消息编号)、8字节的时间戳(微秒)、以及按定义区字段顺序排列的消息体。消息体采用紧凑二进制编码,不进行任何对齐填充,因此解析时必须严格按照字段类型和顺序逐字节读取。

2.3 元数据:自描述机制与消息定义

ULog的元数据体系由消息定义和消息信息(msg_info)两部分构成。消息定义在定义区中通过Add Logged Message数据块声明,每条定义包含:消息名称(如sensor_combined)、消息ID(2字节)、字段数量(1字节)、以及每个字段的名称和类型。字段类型采用类C语言的紧凑表示:b表示bool,c/i/l分别表示int8_t/int16_t/int32_t,C/I/L表示对应的无符号类型,f/d表示float/double,M表示数组或字符串。

消息信息(msg_info)则存储在索引区,以键值对形式记录飞行器的全局信息,包括:固件版本(ver_sw)、硬件版本(ver_hw)、系统名称(sys_name)、飞行模式(flight_mode)、GPS坐标(ref_lat/ref_lon)等。这些信息对于日志回放时还原飞行环境至关重要。

实战经验:在分析PX4日志时,建议首先使用ulog_info命令查看msg_info中的固件版本和飞行模式。不同固件版本的消息定义可能存在差异,如果使用不匹配的解析工具,会导致字段错位或解析失败。PX4的pyulog库会自动处理版本兼容问题,但自定义解析脚本时务必检查ver_sw字段。

2.4 时间戳机制与时间同步

ULog的时间戳机制是日志回放和指令对齐的核心。每条Message Data消息都携带一个64位无符号整数时间戳,单位是微秒(µs),表示该消息从uORB发布到日志系统的时间。这个时间戳来源于PX4的hrt_absolute_time()函数,它基于系统上电后的硬件定时器计数,精度可达微秒级。

在PX4中,存在两种时间基准:硬件时间(基于ARM Cortex-M内核的DWT周期计数器或外部RTC)和软件时间(基于FreeRTOS的tick计数)。ULog记录的是硬件时间,因为它不受任务调度延迟影响,能更真实地反映传感器采样时刻。然而,不同传感器(如IMU、GPS、气压计)可能具有不同的时间基准,PX4通过时间同步模块(time_sync)进行补偿,将外部传感器时间戳转换到本地时间基准。

时间戳对齐在日志回放中具有关键意义。当我们需要将模型输出(如期望姿态)与控制指令(如电机PWM)进行对比时,必须确保两者使用相同的时间基准。PX4的vehicle_attitude和actuator_outputs消息都带有硬件时间戳,但它们的采样时刻可能相差数毫秒。在回放工具中,通常采用线性插值或最近邻匹配的方式,将不同频率的消息对齐到统一时间轴上。

# 使用pyulog检查时间戳一致性
from pyulog import ULog
log = ULog("flight.ulg")
for d in log.data_list:
    if d.name == "vehicle_attitude":
        timestamps = d.data['timestamp']
        print(f"第一条时间戳: {timestamps[0]} µs")
        print(f"最后一条时间戳: {timestamps[-1]} µs")
        print(f"采样间隔: {(timestamps[-1]-timestamps[0])/(len(timestamps)-1):.2f} µs")

2.5 案例拆解:一次传感器数据丢失的定位

某飞手反馈,在飞行日志回放时发现sensor_combined数据在特定时间段出现明显缺口,导致姿态估计异常。我们使用ULog分析工具进行排查:

初始现象:日志中sensor_combined消息在T+120秒至T+125秒之间完全缺失,而vehicle_attitude消息仍然存在,但姿态角出现跳变。

分析过程:首先检查Dropout Mark数据块,发现该时间段内存在一个持续5秒的丢帧标记。进一步查看SD卡写入状态,发现sd_io主题的write_speed字段在该时段内从平均1.2 MB/s骤降至0.3 MB/s。结合battery_status数据,发现电池电压在此时段内从11.8V降至11.2V,存在电压跌落。

根因定位:SD卡在低电压下写入速度下降,导致日志缓冲区溢出,部分传感器消息被丢弃。PX4的日志系统在缓冲区满时会优先保留高优先级消息(如vehicle_attitude),而丢弃低优先级消息(如sensor_combined)。

解决方案:更换为高速SD卡(U3等级),并在飞控参数中增大SDLOG_BUFFER_SIZE(从默认的8KB增至16KB)。同时,在电池管理中将低电压报警阈值从3.5V/节提高至3.6V/节,避免电池在低电量下继续高负载飞行。

常见误区:初学者常将sensor_combined数据缺失误判为传感器硬件故障。实际上,ULog的丢帧机制是正常的数据保护行为——当写入速度跟不上采样速度时,系统会主动丢弃部分数据。在分析时,应优先检查Dropout Mark和sd_io主题,确认是否为存储瓶颈而非传感器故障。

2.6 常见误区与总结复盘

在ULog文件解析过程中,以下误区值得特别注意:

  • 误区一:忽略字节对齐。ULog消息体不进行对齐填充,但定义区中的字段描述需要按1字节对齐读取。如果使用C语言结构体直接映射,可能因编译器对齐导致读取错误。
  • 误区二:混淆时间戳单位。ULog时间戳单位是微秒,但部分PX4消息(如vehicle_gps_position)的timestamp字段可能使用毫秒。解析时必须查看消息定义中的字段类型和注释。
  • 误区三:忽略消息ID重映射。在日志记录过程中,如果动态添加/移除消息,消息ID可能被重新分配。解析时不能假设消息ID固定不变,必须根据定义区的Add/Remove Logged Message动态更新映射表。
  • 误区四:未处理时间跳变。如果飞控在飞行中重启(如看门狗触发),时间戳会从0重新计数。此时日志中会出现时间戳回退,回放工具需要检测并分段处理。

总结而言,ULog格式的设计体现了嵌入式日志系统的核心权衡:在有限的存储带宽下,通过自描述元数据、紧凑二进制编码和分层时间戳机制,实现了高密度数据记录与灵活解析的平衡。掌握ULog结构,是后续进行模型输出与控制指令对齐分析的基础。建议读者使用ulog_info、ulog_messages等命令行工具,结合Python的pyulog库,亲手解析一份真实日志,加深对格式细节的理解。