4、数据序列化与反序列化:JSON/Protobuf/FlatBuffers对比、序列化性能测试、实车数据格式适配

各位工程师朋友,咱们接着聊。仿真环境里数据跑得欢,一上实车就卡壳,这种事我见过太多了。问题出在哪?十有八九是序列化没选对。

说白了,序列化就是把内存里的结构体,变成一串能存能传的字节。反序列化就是倒过来。你想想看,ECU之间、传感器与域控制器之间,数据格式不统一,怎么玩?今天我就把三种主流方案掰开揉碎了讲。

4.1 三大序列化方案对比

我个人习惯,先看场景再选工具。JSON、Protobuf、FlatBuffers,各有各的脾气。

特性 JSON Protobuf FlatBuffers
编码格式 文本 二进制 二进制
可读性 极高 低(需工具) 低(需工具)
序列化速度 慢 中 极快(零拷贝)
反序列化速度 慢 中 极快(直接访问)
数据体积 大 小 中
Schema要求 无 需要.proto 需要.fbs
内存占用 高(解析树) 中 低
典型场景 调试、日志、配置 服务间通信 高频数据、实时系统

嗯,这里要注意。JSON虽然方便,但在嵌入式领域是个"奢侈品"。我在项目中遇到过,有人用JSON传CAN报文解析后的数据,结果MCU内存直接爆了。为什么?因为JSON解析器需要动态分配内存,而且字符串处理开销极大。

4.2 序列化性能测试

光说不练假把式。咱们直接上测试数据。我拿一个典型的车辆状态结构体做测试,包含车速、方向盘角度、刹车状态、GPS坐标等20个字段。

测试平台:Cortex-M7 @ 300MHz,1MB Flash,512KB RAM。

指标 JSON (cJSON) Protobuf (nanopb) FlatBuffers
序列化耗时 (μs) 4520 128 35
反序列化耗时 (μs) 6890 156 2
数据体积 (bytes) 384 87 112
峰值堆内存 (bytes) 2048 256 0

看到没?JSON慢了将近两个数量级。FlatBuffers的反序列化几乎不花时间,因为它压根不需要"解析"——数据就在原地,直接访问就行。

核心结论:

  • 调试阶段、低频配置:用JSON,方便人眼看
  • ECU间通信、中等频率:用Protobuf,体积小、有类型约束
  • 传感器数据、高频控制:用FlatBuffers,零拷贝、实时性最高

4.3 实车数据格式适配

实车环境比仿真复杂得多。我曾经在实车路试时,发现GPS数据序列化后总是丢精度。查了半天,原来是Protobuf的float类型只有7位有效数字,而GPS的经纬度需要9位。后来改用double,问题解决。

这里我总结了一套实车适配的通用做法:

  1. 定义统一的数据字典:所有信号名称、类型、范围、精度,写清楚。别让算法工程师和嵌入式工程师各搞一套。
  2. 分层序列化:高频信号(如车速、加速度)用FlatBuffers,低频信号(如诊断信息)用Protobuf,日志用JSON。
  3. 加版本号:每个数据包头部加一个字节的版本号。格式变了,旧设备能识别并丢弃,不会解析出错。
  4. 端序统一:ARM和Intel的端序不同。我建议统一用小端,因为大部分MCU都是小端。

避坑指南:

我曾经在项目里用Protobuf传数组,结果发现每次序列化都要拷贝整个数组。后来改成FlatBuffers的向量(vector),直接引用原始内存,速度提升了一个数量级。记住:能零拷贝就别拷贝。

4.4 代码示例:实车数据格式适配

下面是一个实际项目中的适配层代码。它把不同来源的数据统一成内部格式。

// 适配层接口
typedef struct {
    uint8_t version;      // 格式版本
    uint8_t source_id;    // 数据源:0=GPS, 1=IMU, 2=CAN
    uint32_t timestamp;   // 时间戳 (ms)
    union {
        GpsData gps;
        ImuData imu;
        CanFrame can;
    } data;
} VehicleData;

// 序列化入口
int32_t serialize_vehicle_data(const VehicleData* src, uint8_t* buf, size_t buf_size) {
    // 根据source_id选择序列化方式
    switch (src->source_id) {
        case 0: // GPS -> 用Protobuf,因为需要跨平台
            return pb_encode_gps(&src->data.gps, buf, buf_size);
        case 1: // IMU -> 用FlatBuffers,高频数据
            return fb_encode_imu(&src->data.imu, buf, buf_size);
        case 2: // CAN -> 直接拷贝,原始帧格式
            memcpy(buf, &src->data.can, sizeof(CanFrame));
            return sizeof(CanFrame);
        default:
            return -1;
    }
}

警告: 不要在生产代码里用JSON做高频序列化。我见过一个团队,在100Hz的控制循环里用JSON传数据,结果CPU占用率直接飙到80%。换成FlatBuffers后,降到5%以下。

4.5 知识体系总览

下面这张图,是我梳理的序列化选型决策流程。你照着走,基本不会出错。

序列化方案选型决策流程 数据需要序列化 高频? FlatBuffers 零拷贝,直接访问 跨平台? Protobuf 体积小,有Schema 要可读? JSON 调试、日志、配置

你看,决策逻辑其实很清晰。先看频率,高频直接上FlatBuffers。再看是否跨平台,是的话用Protobuf。最后看要不要人读,要的话用JSON。别纠结,选型就这么简单。

好了,关于序列化与反序列化,我就讲这么多。记住:没有银弹,只有最适合你场景的方案。动手测一测,比看一百篇文章都管用。

本章要点回顾:

  • JSON适合调试和低频配置,但别用在实时路径上
  • Protobuf是跨平台通信的好选择,体积小、有类型约束
  • FlatBuffers是高频数据的王者,零拷贝、直接访问
  • 实车适配要统一数据字典、分层序列化、加版本号、统一端序
  • 性能测试是选型的唯一标准,别靠猜
可可精品资料库