简介本资源是一套面向STM32与ARM嵌入式开发者的nanopb轻量级Protocol Buffers移植实战例程专为内存受限的单片机平台设计解决传统protobuf库体积大、依赖动态内存等嵌入式适配难题。压缩包共47个文件含7个C源码如pb_encode.c、test_update.c、5个头文件pb.h、simple.pb.h等、1个核心.proto定义文件及配套编译中间产物obj、tlog、pdb等完整覆盖从.proto定义→nanopb代码生成→STM32工程集成→序列化/反序列化验证的全流程包体仅3.54MB便于嵌入式项目快速复用。已有1437人学习下载资源结构清晰含ConsoleApplication1示例工程含sln/vcxproj与my_pro.zip备用方案提供可直接编译运行的测试入口、预分配缓冲区实践及跨平台适配要点助开发者避开内存溢出、字段截断等典型坑点高效实现嵌入式设备间紧凑、可靠的数据交换。1. 一个跑在 STM32F407 上的 protobuf 序列化黑匣子不用 Linux、不靠 glibc纯裸机 C 实现结构体到二进制的双向翻译你有没有试过在没有文件系统、没有动态内存分配、RAM 只有 192KB 的 STM32F407VGT6 上把一个带嵌套、带 repeated 字段、含枚举和可选字段的传感器数据结构原样打包成二进制发给上位机解析不是用 sprintf 拼字符串不是手写 memcpy 偏移计算而是真正用.proto文件定义、自动生成 C 结构体和编解码函数——nanopb 就是干这个的。它不是 protobuf 的简化版而是为嵌入式量身重写的“精简内核”零 malloc、栈分配优先、可配置字段最大长度、支持 packed 编码、甚至能关掉浮点数支持来省 Flash。本项目就是一套完整可运行的移植例程覆盖从 CubeMX 初始化、nanopb 工具链集成、.proto到pb_encode.c/pb_decode.c的生成到最终在 Keil MDK 下实测通过 UART 发送/接收并校验 CRC 的全流程。适合所有正在做工业传感器节点、LoRa 终端、电池供电遥测设备的嵌入式开发者——尤其当你第 3 次因为协议字段对不上而抓包到凌晨两点你会明白为什么说 nanopb 是嵌入式通信里最值得提前埋下的“后悔药”。2. 移植架构拆解从 CubeMX 配置到 nanopb 编译单元的完整链路2.1 硬件平台与工程基线STM32F407VGT6 Keil MDK-ARM v5.38本例程基于标准 STM32F407VGT6 开发板100-pin LQFP主频 168MHz使用 STM32CubeMX v6.12.0 生成初始化代码。关键配置如下RCCHSE 8MHz 外部晶振PLL 倍频至 168MHzSYSDebug → Serial WireTimebase Source → TIM6避免与 HAL_Delay 冲突USART1Asynchronous 模式Baud Rate 115200Word Length 8 bitsNo Parity1 Stop bitHardware Flow Control DisabledGPIOPA9/PA10 复用为 USART1_TX/RXProject ManagerToolchain → ARM GCC但实际使用 Keil故后续需手动适配头文件路径提示Keil 下必须关闭__MICROLIB即不使用 microlib否则 nanopb 的pb_encode.c中部分size_t类型推导会失败。在Options for Target → Target → Use MicroLIB前打钩取消。生成工程后目录结构已预置 nanopb 源码Middlewares/Third_Party/nanopb/包含nanopb/ ├── pb.h ← 主头文件定义核心宏与类型 ├── pb_encode.h ├── pb_decode.h ├── pb_common.h ├── pb_encode.c ← 编码实现无 malloc ├── pb_decode.c ← 解码实现带字段跳过逻辑 └── extras/ └── pb_syshdr.h ← Keil 兼容头重定义 stdint.h/stddef.h 等2.2 nanopb 工具链集成Python 脚本驱动.proto到 C 的自动化生成nanopb 不像官方 protobuf 那样依赖protoc插件它提供独立的generator/nanopb_generator.py脚本用 Python 3.7 即可运行。本项目已将该脚本及依赖protobufPython 包打包进/tools/nanopb-gen/目录。我们定义了一个典型传感器协议sensor_data.protosyntax proto2; package sensor; message SensorHeader { required uint32 timestamp_ms 1; required uint8 node_id 2; optional uint8 firmware_ver 3 [default 1]; } message SensorReading { required float temperature_c 1; required float humidity_rh 2; repeated int32 adc_values 3 [packedtrue]; // packed 编码省空间 } message SensorPacket { required SensorHeader header 1; required SensorReading reading 2; optional bytes signature 3; // 用于 OTA 校验 }生成命令Windows CMD 下执行cd tools\nanopb-gen python nanopb_generator.py -I ..\proto -D ..\Src\generated ..\proto\sensor_data.proto参数说明-I ..\proto指定.proto文件搜索路径-D ..\Src\generated输出 C/H 文件到Src/generated/目录sensor_data.proto输入文件名执行后生成Src/generated/ ├── sensor_data.pb.h ← 定义 struct sensor_SensorPacket 等含字段偏移宏 ├── sensor_data.pb.c ← 含 pb_field_t 数组、encode/decode 函数桩 └── sensor_data.options ← 可选配置文件如字段 size 限制关键点sensor_data.pb.h中每个结构体字段都带PB_FIELD_1,PB_FIELD_2宏对应.proto中字段编号不是数组下标。例如/* sensor_data.pb.h */ typedef struct _sensor_SensorPacket { struct _sensor_SensorHeader header; struct _sensor_SensorReading reading; pb_bytes_array_t *signature; // 注意指针类型但实际由栈上 buffer 托管 } sensor_SensorPacket;而sensor_data.pb.c中最关键的Fields数组定义了每个字段的编码规则// sensor_data.pb.c const pb_field_t sensor_SensorPacket_fields[4] { PB_FIELD(1, MESSAGE, POINTER, FIRST, 0, sensor_SensorPacket, header, sensor_SensorHeader_fields), PB_FIELD(2, MESSAGE, POINTER, OTHER, 0, sensor_SensorPacket, reading, sensor_SensorReading_fields), PB_FIELD(3, BYTES, POINTER, OPTIONAL, 0, sensor_SensorPacket, signature, NULL), PB_LAST_FIELD };PB_FIELD宏展开后包含字段编号、类型、是否可选、是否 repeated、内存布局方式等——这是 nanopb 运行时解析的核心元数据不能手改。2.3 Keil 工程配置头文件路径、宏定义与内存模型适配在 Keil 中需显式添加以下 Include PathsOptions for Target → C/C → Include Paths.\Middlewares\Third_Party\nanopb .\Middlewares\Third_Party\nanopb\extras .\Src\generated .\Core\Inc .\Drivers\STM32F4xx_HAL_Driver\Inc .\Drivers\CMSIS\Device\ST\STM32F4xx\Include .\Drivers\CMSIS\Include同时定义必要宏C/C → DefinePB_ENABLE_MALLOC0 // 强制禁用 malloc全部栈分配 PB_NO_PACKED_STRUCTS1 // 关闭 packed structKeil 对 __attribute__((packed)) 支持不稳定 PB_WITHOUT_FLOAT0 // 保留 float 支持若 MCU 有 FPU PB_BUFFER_SIZE256 // 全局最大 buffer 长度根据最大 packet 设定注意PB_BUFFER_SIZE必须 ≥ 最大可能序列化后的字节数。本例中SensorPacket最大长度经估算约 210 字节含 signature 最长 32 字节故设为 256 安全。最后nanopb默认使用stdint.h但 Keil 的core_cm4.h会先于stdint.h定义uint32_t等导致重定义警告。解决方案是在pb.h顶部插入#ifndef __STDINT_H #include stdint.h #endif并在pb_syshdr.h中确保#include stddef.h #include stdint.h3. 核心模块拆解一个请求从结构体填充到 UART 发送的完整链路3.1 数据准备栈上分配 手动填充字段无 malloc无 heapnanopb 要求所有结构体在栈上或静态分配且repeated/bytes类型字段需额外提供 buffer。本例中我们定义全局 buffer 和 packet 实例// main.c #define MAX_PACKET_SIZE 256 static uint8_t tx_buffer[MAX_PACKET_SIZE]; static uint8_t rx_buffer[MAX_PACKET_SIZE]; // 栈上分配完整 packet含嵌套结构体内存 static sensor_SensorPacket packet { .header { .timestamp_ms 0, .node_id 0x01, .firmware_ver 2 }, .reading { .temperature_c 25.3f, .humidity_rh 65.8f, .adc_values_count 0, // repeated 字段计数器 .adc_values {0} // 栈上数组最多 8 个 int32 }, .signature NULL // bytes 类型暂不填 };注意.adc_values_count必须显式赋值否则 decode 时无法知道有多少元素.adc_values是固定长度数组由.options文件或#define控制此处默认为 8。填充 ADC 值的典型操作// 采集 5 路 ADC填入 packet.reading.adc_values for (int i 0; i 5; i) { packet.reading.adc_values[i] HAL_ADC_GetValue(hadc1); } packet.reading.adc_values_count 5; // 关键必须更新计数3.2 编码流程pb_encode如何把结构体压成紧凑二进制编码分三步准备 stream、调用 encode、获取长度。核心是pb_ostream_t—— 它是一个回调函数容器不关心数据去哪只负责“写一个字节”。// main.c static bool write_callback(pb_ostream_t *stream, const uint8_t *buf, size_t count) { // 将 buf 中 count 字节写入 UART阻塞式 HAL_UART_Transmit(huart1, (uint8_t*)buf, count, HAL_MAX_DELAY); return true; // true 表示成功 } void send_sensor_packet(void) { pb_ostream_t stream pb_ostream_from_callback(write_callback, NULL); // 关键调用自动生成的 encode 函数 bool status pb_encode(stream, sensor_SensorPacket_fields, packet); if (!status) { // 编码失败常见原因包括 buffer 不足、float NaN、repeated 超限 Error_Handler(); } // stream.bytes_written 即实际编码字节数可用于 CRC 计算 uint16_t encoded_len stream.bytes_written; printf(Sent %d bytes\n, encoded_len); }pb_encode()内部逻辑遍历sensor_SensorPacket_fields数组对每个字段根据类型MESSAGE/BYTES/FLOAT调用对应子 encoderrepeated字段会循环调用其元素 encoder并自动加length-delimited前缀packedtrue的 repeated 字段如adc_values会合并为单个 tag 连续值流比非 packed 节省约 30% 空间提示pb_ostream_from_buffer()更常用写入内存 buffer但本例用 callback 直接发 UART省去中间 copy适合小包高频场景。3.3 解码流程从 UART 接收流到结构体字段还原解码需先接收完整 packet需帧头/长度/CRC 校验再喂给pb_decode()。本例使用简单定长 header payload 方式// 假设已通过 UART 接收 rx_buffer且已校验 CRCrx_len 已知 bool parse_received_packet(uint8_t *rx_buffer, size_t rx_len) { pb_istream_t stream pb_istream_from_buffer(rx_buffer, rx_len); // 清零结构体重要nanopb 不自动清空未设置字段 memset(packet, 0, sizeof(packet)); bool status pb_decode(stream, sensor_SensorPacket_fields, packet); if (!status) { printf(Decode failed: %s\n, PB_GET_ERROR(stream)); return false; } // 验证关键字段是否被正确设置optional 字段需检查 has_xxx if (!packet.header.has_timestamp_ms || !packet.reading.has_temperature_c) { printf(Missing required fields!\n); return false; } printf(Recv: T%.1f°C, H%.1f%%, ADC cnt%d\n, packet.reading.temperature_c, packet.reading.humidity_rh, packet.reading.adc_values_count); return true; }pb_decode()关键行为自动跳过未知字段前向兼容性保障对optional字段设置has_xxx成员为 true/false如packet.header.has_firmware_ver对repeated字段严格按xxx_count值读取超限则返回 false若 buffer 不足stream.bytes_left为 0 时返回 false血泪经验永远在 decode 前memset(packet, 0, sizeof(packet))。nanopb 不初始化未设置字段残留栈垃圾会导致has_xxx错误或浮点数异常。4. 避坑指南Keil STM32 nanopb 移植中踩过的 5 个真实坑4.1 现象编译报错error: #error Compiler does not support stdint.h原因Keil 默认不启用 C99 模式stdint.h未被识别。解决Options for Target → C/C → Misc Controls中添加--c99同时确认stdio.h等标准头文件路径已加入见 2.3 节。4.2 现象pb_encode返回 false串口无输出调试发现stream.state为NULL原因pb_ostream_from_callback()第二个参数传了NULL但 callback 函数内部试图访问stream-state如做超时计数。解决要么改 callback 不依赖state要么传入有效指针如tx_state结构体。本例 callback 简单直接删掉对state的引用即可。4.3 现象repeated int32 adc_values解码后adc_values_count0但 buffer 里明明有数据原因.proto中未声明packedtrue而 nanopb 默认对 repeated 使用 length-delimited 编码每个值带独立 tag但发送端误用了 packed 格式。解决统一在.proto中显式声明repeated int32 adc_values 3 [packedtrue];并确保 generator 重新运行。用pb_print工具nanopb extrasdump 二进制可验证格式。4.4 现象float字段解码后为0.000000但发送端确认值正常原因Keil 的printf默认不支持 floatuse float未勾选且 nanopb 的 float 编码依赖 IEEE754若 MCU 无 FPU 或编译器未启用软浮点memcpy读取字节序可能错乱。解决Options for Target → Target → Floating Point Hardware → Not Used强制软浮点C/C → Misc Controls添加--fpmodeieee_full同时确认PB_WITHOUT_FLOAT0已定义。4.5 现象signature字段bytes类型解码后signature-size0但原始 buffer 有 32 字节原因bytes类型在 nanopb 中需手动分配 buffer 并挂载到结构体指针。packet.signature是pb_bytes_array_t*但未指向有效内存。解决定义栈上 buffer 并赋值static uint8_t sig_buffer[32]; static pb_bytes_array_t sig_array {.size 0}; // size 在 decode 时自动填 sig_array.size sizeof(sig_buffer); // 先设最大容量 packet.signature sig_array;decode 后sig_array.size即为实际接收长度sig_array.bytes指向数据起始。5. 二次开发实战为 OTA 固件升级增加签名验证与分片传输支持5.1 场景需求通过 UART 接收 512KB 固件镜像需防篡改、断点续传、内存受限原例程仅处理百字节级传感器包而 OTA 需应对 MB 级数据。nanopb 本身不解决大文件传输但可复用其协议框架通过分片 状态机实现。核心改造点有三1扩展.proto定义分片消息message FirmwareChunk { required uint32 chunk_index 1; // 从 0 开始 required uint32 total_chunks 2; // 总片数首包携带 required bytes data 3; // 每片 ≤ 256 字节适配 PB_BUFFER_SIZE optional uint32 crc32 4; // 片内 CRC快速校验 } message FirmwareHeader { required uint32 firmware_size 1; // 总大小 required bytes sha256_hash 2; // 全镜像哈希32 字节 required uint32 version 3; // 固件版本号 }2实现分片状态机伪代码// 全局状态 typedef enum { FW_IDLE, FW_WAITING_HEADER, FW_RECEIVING_CHUNKS, FW_VERIFYING } fw_state_t; static fw_state_t fw_state FW_IDLE; static uint32_t expected_chunk 0; static uint32_t total_chunks 0; static uint8_t fw_image[FW_MAX_SIZE]; // 外部 SPI Flash 映射或 RAM buffer static uint32_t fw_offset 0; void handle_firmware_packet(const uint8_t *buf, size_t len) { pb_istream_t stream pb_istream_from_buffer(buf, len); if (fw_state FW_WAITING_HEADER) { firmware_FirmwareHeader header {0}; if (pb_decode(stream, firmware_FirmwareHeader_fields, header)) { total_chunks (header.firmware_size CHUNK_SIZE - 1) / CHUNK_SIZE; expected_chunk 0; fw_state FW_RECEIVING_CHUNKS; } } else if (fw_state FW_RECEIVING_CHUNKS) { firmware_FirmwareChunk chunk {0}; if (pb_decode(stream, firmware_FirmwareChunk_fields, chunk)) { if (chunk.chunk_index expected_chunk) { memcpy(fw_image fw_offset, chunk.data.bytes, chunk.data.size); fw_offset chunk.data.size; expected_chunk; if (expected_chunk total_chunks) { fw_state FW_VERIFYING; verify_firmware_image(); // 计算 SHA256 并比对 } } } } }3内存优化技巧用pb_istream_from_buffer() 偏移读取替代整包 decode对FirmwareChunk若data字段长达 256 字节pb_decode()会将其拷贝到栈上chunk.data.bytes需预分配 buffer。更省内存的做法是只 decode header再用 raw buffer 偏移读取 data// 先 decode 前 12 字节chunk_index total_chunks tag for data firmware_FirmwareChunk_header_only hdr {0}; pb_istream_t hdr_stream pb_istream_from_buffer(buf, 12); pb_decode(hdr_stream, firmware_FirmwareChunk_header_only_fields, hdr); // data 字段起始位置 tag(1 byte) length varint(1~5 bytes) data size_t data_offset 12; // 粗略估计实际需解析 varint uint8_t *data_ptr buf data_offset; size_t data_len len - data_offset; // 直接写入 flash跳过 nanopb 的 bytes 拷贝 flash_write_page(fw_offset, data_ptr, data_len);此法将栈消耗从256字节降至20字节对 RAM 紧张的 MCU 至关重要。5.2 调试与验证用 Python 脚本模拟上位机生成测试包脱离硬件调试 nanopb 协议最快方式是用 Python 生成合法二进制再用逻辑分析仪或串口助手注入。本项目附带test_gen.pyimport nanopb_pb2 # 由 protoc 生成非 nanopb_generator from google.protobuf import text_format # 构造 Python 对象 msg nanopb_pb2.SensorPacket() msg.header.timestamp_ms 1717171717 msg.header.node_id 0x0A msg.reading.temperature_c 23.5 msg.reading.humidity_rh 45.0 msg.reading.adc_values.extend([1024, 2048, 3072]) # 序列化为二进制 binary msg.SerializeToString() print(Generated binary:, binary.hex()[:64] ...) # 写入文件供串口发送 with open(test_packet.bin, wb) as f: f.write(binary)运行后得到test_packet.bin用 XModem 或自定义串口工具发送即可在 STM32 端验证 decode 正确性。这招让我少烧了 3 块板子——协议问题在 PC 端就能闭环。从那以后我每次新增.proto字段都强制走一遍test_gen.py → 串口注入 → STM32 log 输出全链路哪怕只是加个注释字段。嵌入式协议没有银弹只有一次又一次的字节对齐。希望帮到你。本文还有配套的精品资源点击获取