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,问题解决。
这里我总结了一套实车适配的通用做法:
- 定义统一的数据字典:所有信号名称、类型、范围、精度,写清楚。别让算法工程师和嵌入式工程师各搞一套。
- 分层序列化:高频信号(如车速、加速度)用FlatBuffers,低频信号(如诊断信息)用Protobuf,日志用JSON。
- 加版本号:每个数据包头部加一个字节的版本号。格式变了,旧设备能识别并丢弃,不会解析出错。
- 端序统一: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。最后看要不要人读,要的话用JSON。别纠结,选型就这么简单。
好了,关于序列化与反序列化,我就讲这么多。记住:没有银弹,只有最适合你场景的方案。动手测一测,比看一百篇文章都管用。
本章要点回顾:
- JSON适合调试和低频配置,但别用在实时路径上
- Protobuf是跨平台通信的好选择,体积小、有类型约束
- FlatBuffers是高频数据的王者,零拷贝、直接访问
- 实车适配要统一数据字典、分层序列化、加版本号、统一端序
- 性能测试是选型的唯一标准,别靠猜