从 MCU 到云:使用 Nanopb 的轻量级序列化

1. 引言

在现代嵌入式系统中,微控制器常常需要与 PC、智能手机或云服务交换结构化数据。这可能是遥测数据、配置参数或传感器读数。挑战在于,这些平台使用不同的编程语言和环境,而微控制器的 RAM 和闪存非常有限。为了使通信可靠且可移植,原始数据必须转换为结构化、与机器无关的格式------这个过程称为序列化。

2. 何为序列化?

序列化是将结构化数据(如带有时间戳和值的传感器读数)转换为可移植字节流的过程,这些字节流可以通过串行连接、网络套接字或无线信道发送。在接收端,数据会被反序列化回原始结构。

简单方法,比如直接发送原始字节,在不同处理器、字节序或编程语言环境下往往会失败。标准化的序列化方法可以确保设备间兼容性和长期可维护性。

以下为序列化/反序列化示例:

3. Protobuf 与嵌入式困境

Protocol Buffers(简称 Protobuf)是现代软件中最广泛使用的序列化格式之一。它与语言无关、平台独立,并提供成熟的工具链:

  • .proto 文件中定义消息,运行编译器,即可得到可直接使用的 C++、Java、Python 或 Go 代码。

这使得Protobuf非常适合需要跨平台交换结构化数据的系统。

以下为protobuf工作流示意图:

问题出现在试图将标准 Protobuf 引入深度嵌入式系统时。官方实现是针对服务器和移动设备设计的,这些平台可以接受动态内存分配和相对较大的代码体积。在只有几十 KB RAM 和闪存的小型 MCU 上,庞大的运行时、动态分配以及对 C++ 的依赖成为了显著障碍。即使设法适配,性能也可能下降,因为在实时环境中内存分配和解析开销很高。

这种不匹配通常被称为"嵌入式困境"。Protobuf 优雅地解决了数据交换问题,但其默认实现对于许多微控制器而言资源消耗过大。因此,开发者需要更轻量的替代方案,在兼容 Protobuf 生态的同时,满足嵌入式硬件的约束条件。

4. 嵌入式友好选项:Nanopb

将 Protobuf 引入小型设备的最流行方案之一是 Nanopb(https://github.com/nanopb/nanopb)。它是一个微型 C 库,可以直接从 .proto 文件生成代码,同时避免了标准 Protobuf 实现中的庞大运行时。与完整版不同:

  • Nanopb 不使用动态内存,而是通过静态缓冲区处理所有内容。
    • 这使得内存使用可预测且安全,对于资源紧张的 MCU 至关重要。

使用流程非常简单。在 .proto 文件中定义好数据结构,运行 Nanopb 的生成器,然后将生成的 C 代码包含到固件中。在微控制器端,填充结构体,调用编码器生成字节流,并通过选定的通信方式发送。在主机端,相同的字节流可以通过任何标准 Protobuf 库解码,从而确保跨平台兼容性。

Nanopb 被广泛采用,因为其实现了恰当的平衡:

  • 对于只有几 KB RAM 的 32 位 MCU 足够轻量,同时仍保持与 Protobuf 生态系统的兼容。

如,假设希望发送包含时间戳和传感器读数的遥测数据。首先在 .proto 文件中描述结构,如:

proto 复制代码
message Telemetry {
 uint32 timestamp = 1;
 float value = 2;
}

运行 Nanopb 生成器后,会产生两个文件(telemetry.pb.ctelemetry.pb.h),定义了 C 结构体和编码/解码函数。在 MCU 代码中,包含这些文件,填充 Telemetry 结构体,并使用 Nanopb 的 API 进行消息序列化和反序列化:

c 复制代码
/** Encode Telemetry message */
Telemetry msg = Telemetry_init_zero;
msg.timestamp = get_time();
msg.value = read_sensor();

uint8_t buffer[64];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));

if (pb_encode(&stream, Telemetry_fields, &msg)) {
    send_uart(buffer, stream.bytes_written);
}

/** Decode Telemetry message */
Telemetry recv_msg = Telemetry_init_zero;
pb_istream_t istream = pb_istream_from_buffer(buffer, stream.bytes_written);

if (pb_decode(&istream, Telemetry_fields, &recv_msg)) {
    printf("Time: %u, Value: %f\n", recv_msg.timestamp, recv_msg.value);
}

以上这个小示例展示了完整的工作流程:

  • 填充结构体,
  • 将其编码为字节,
  • 传输,
  • 然后在另一端将其解码回相同的结构。

Nanopb在 MCU 上保持通信高效,同时在更大系统上仍然完全兼容 Protobuf。

另一个值得注意的选项是 upb(https://github.com/protocolbuffers/protobuf/tree/main/upb),这是一个轻量级的 C 实现,现在已成为官方 Protobuf 项目的一部分。它提供高效的解析和编码,内存使用比完整的 C++ 运行时更低,非常适合资源比最小 MCU 多但仍需要精简代码的中端设备。对于希望在保持嵌入式友好的同时,实现与上游 Protobuf 更接近的功能特性的项目,upb 提供了一个可靠的替代方案。

5. 结论

序列化是让嵌入式设备与 PC、手机和云系统"讲同一种语言"的关键。虽然完整的 Protobuf 对小型 MCU 来说过于庞大,但像 Nanopb 这样的轻量级库以最小的开销带来了相同的好处。通过在 .proto 文件中一次性定义消息并跨平台共享,开发者可以获得可靠、可维护的通信,而无需重新发明协议。

参考资料

1\] Wadix Technologies 2025年9月博客 [From MCU to Cloud: Lightweight Serialization with Nanopb](https://medium.com/embedworld/from-mcu-to-cloud-lightweight-serialization-with-nanopb-856bea75fe07)

相关推荐
黑客思维者6 天前
嵌入式系统DevSecOps深度设计:构建固件级漏洞免疫体系的自动化管道
运维·自动化·devsecops·嵌入式系统
黑客思维者7 天前
智能配电系统用户敏感数据脱敏详细设计:从静态遮盖到动态策略
c++·python·嵌入式系统·数据脱敏·智能配电系统
BreezeJuvenile2 个月前
实验二 呼吸灯功能实验
stm32·单片机·嵌入式系统·流水灯·实验
一枝小雨2 个月前
FreeRTOS内存分配与STM32内存布局详解
stm32·单片机·嵌入式·freertos·嵌入式系统·cortex-m3/m4
玉~你还好吗4 个月前
【嵌入式电机控制#34】FOC:意法电控驱动层源码解析——HALL传感器中断(不在两大中断内,但重要)
单片机·嵌入式系统·电机控制
白帽小野4 个月前
微控制器的工作原理和应用
mcu·嵌入式系统·微控制器
玉~你还好吗4 个月前
【嵌入式电机控制#补充3】SDK电机控制台的功能
单片机·嵌入式硬件·嵌入式系统·电机控制·控制算法
Learn-Share_HY5 个月前
[Raspberry Pi]如何將無頭虛擬顯示器服務(headless display)建置在樹莓派的Ubuntu桌面作業系統中?
物联网·ubuntu·bash·树莓派·嵌入式系统·无头headless·vnc服务
德思特6 个月前
德思特新闻 | 德思特与es:saar正式建立合作伙伴关系
嵌入式系统·嵌入式产品开发·新品