microcbor 入门:MCU 上用 CBOR 编码遥测消息
声明: microcbor 是一个开源项目,作者为 Vanderhell。本文是阅读该项目源码和文档后整理的学习笔记,用于理解嵌入式场景下 CBOR 编解码的工程实现方式。本文作者不是该项目的开发者,未参与该项目的任何代码贡献。 文中所有工程细节均来自对开源代码的分析,不代表本文作者的设计决策。
项目仓库:github.com/Vanderhell/microcbor(C99、零依赖、零堆分配的 CBOR 编解码库,面向 MCU 遥测场景裁剪了 RFC 8949 子集)
CBOR 在 MCU 遥测场景的优势
MQTT、CoAP、BLE GATT、LoRa 这几类链路传输的典型载荷是键值型遥测消息:几个传感器数值加一个设备 ID。JSON 在 PC 端是默认选项,搬到 Cortex-M0/M3 这类小 MCU 上会遇到几个具体问题:
- 每个字符串键、字符串值两端都要加引号;冒号、逗号、花括号是纯开销字节。
- 浮点数转成 ASCII 文本要走
sprintf,在无 FPU 的芯片上是最耗时的一段;数字"23.4"占 4 个 ASCII 字节,CBOR 里是 4 字节定长二进制 float32。 - 解析器要处理任意嵌套和字符串转义,代码体积和栈占用都偏大。
CBOR(RFC 8949)是二进制 JSON 替代品,主要思路是用一个头字节同时携带「类型 + 长度信息」,去掉所有结构标点,整数和浮点数走二进制定长编码。同一条消息通常比 JSON 短 30%–50%,在 LoRa 51 字节 payload 限制下这个差距直接决定能不能一帧发完。
microcbor 的定位
microcbor 面向遥测场景裁剪了 RFC 8949 子集,只实现:
- uint/int 32 位整数、bool、null、float32;
- UTF-8 文本串、字节串;
- 定长 array 和 map;
- 未知字段的迭代式 skip(用于前向兼容)。
砍掉的部分也是刻意的:不支持 64 位整数、不定长编码、Tag、半精度/双精度浮点。换来的是两个 .c/.h 就能集成、没有任何 malloc、编码端有 sticky overflow 标志可以连写后统一检查。
集成方式
代码只有一个实现文件和两个头文件:
include/mcbor.h:公共 APIinclude/mcbor_config.h:一个开关MCBOR_ENABLE_FLOAT32(默认 1,关闭后 float API 返回MCBOR_ERR_UNSUPPORTED)src/mcbor.c:实现
把这三个文件拷进工程,#include "mcbor.h" 即可,没有第三方依赖,C99 可编译。也可以直接用 CMake 里的 add_subdirectory 链接。
入门 Demo:编码一条传感器消息
下面这段代码编码一条 {dev:"s-01", temp:23.4, hum:55, ok:true},验证完整性后再解回来。完整可编译文件见仓库 README 的 Quick Start,下面给出跑通的版本。
#include <stdio.h>
#include <string.h>
#include "mcbor.h"
int main(void) {
uint8_t buf[128];
mcbor_enc_t enc;
size_t used = 0;
/* 编码:4 个键值对的 map */
mcbor_enc_init(&enc, buf, sizeof(buf));
mcbor_enc_map(&enc, 4);
mcbor_enc_str(&enc, "dev"); mcbor_enc_str(&enc, "s-01");
mcbor_enc_str(&enc, "temp"); mcbor_enc_float(&enc, 23.4f);
mcbor_enc_str(&enc, "hum"); mcbor_enc_uint(&enc, 55);
mcbor_enc_str(&enc, "ok"); mcbor_enc_bool(&enc, true);
mcbor_enc_size(&enc, &used);
/* 编码后 used=30 字节,hex:
a4 63646576 64732d3031 6474656d70 fa41bb3333
6368756d 1837 626f6b f5 */
/* 一次性验证:顶层 1 个完整项、无 trailing 字节 */
mcbor_validate_one(buf, used);
/* 解码:按 key 查表,未知 key 用 skip 跳过 */
mcbor_dec_t dec;
mcbor_dec_init(&dec, buf, used);
size_t n;
mcbor_dec_map(&dec, &n);
for (size_t i = 0; i < n; i++) {
char key[16]; size_t klen;
mcbor_dec_str(&dec, key, sizeof(key), &klen);
if (strcmp(key, "temp") == 0) {
float v; mcbor_dec_float(&dec, &v);
printf("temp=%.1f\n", v);
} else if (strcmp(key, "hum") == 0) {
uint32_t v; mcbor_dec_uint(&dec, &v);
printf("hum=%u\n", v);
} else if (strcmp(key, "ok") == 0) {
bool v; mcbor_dec_bool(&dec, &v);
printf("ok=%d\n", v);
} else {
mcbor_dec_skip(&dec); /* 不识别的 key 直接跳过 value */
}
}
return 0;
}编译运行(仓库自带测试文件也用同一条命令验证过,43/43 测试通过):
gcc -std=c99 -Iinclude src/mcbor.c demo.c -lm -o demo
./demo几个阅读这段代码时需要注意的点:
mcbor_enc_map(n)只写容器头,不校验后面是否真的写了 n 对。编码器状态机不跟踪结构完整性,交给调用方或mcbor_validate_one收尾校验。- 每个
mcbor_enc_*返回mcbor_err_t,buf 写满时会置enc->overflow = true(sticky,置位后所有后续写入直接返回 overflow),可以在链尾用mcbor_enc_overflow一次检查。 mcbor_dec_str把文本拷进调用方提供的 buffer 并补 NUL 终止符;mcbor_dec_bytes_ref则返回指向输入 buffer 的零拷贝指针,二进制 payload 走这个接口。- 解码未知字段一定要用
mcbor_dec_skip,它内部是迭代式计数跳过,嵌套 array/map 也不会递归消耗栈。
API 速查
编码端(写入调用方提供的 uint8_t buf[]):
| 函数 | 作用 |
|---|---|
mcbor_enc_init |
绑定 buffer,重置 pos/overflow |
mcbor_enc_uint / mcbor_enc_int |
32 位无符号 / 有符号整数,自动选最短宽度 |
mcbor_enc_bool / mcbor_enc_null / mcbor_enc_float |
简单值 |
mcbor_enc_str / mcbor_enc_text / mcbor_enc_bytes |
以 NUL 结尾串 / 指定长度文本 / 字节串 |
mcbor_enc_array(n) / mcbor_enc_map(n) |
定长容器头 |
mcbor_enc_size / mcbor_enc_overflow |
链尾查询已写字节 / 是否溢出 |
解码端(顺序读,指针前移):
| 函数 | 作用 |
|---|---|
mcbor_dec_init |
绑定输入 buffer |
mcbor_dec_next |
通用取下一项,返回 mcbor_value_t tagged union |
mcbor_dec_uint / mcbor_dec_int / mcbor_dec_bool / mcbor_dec_float |
按类型取,类型不匹配返回 MCBOR_ERR_TYPE |
mcbor_dec_str |
拷贝到调用方 buffer 并补 NUL |
mcbor_dec_bytes_ref |
零拷贝拿字节串指针 |
mcbor_dec_array / mcbor_dec_map |
读容器头,返回元素个数 |
mcbor_dec_skip |
跳过当前项(含嵌套容器),用于前向兼容 |
mcbor_dec_remaining / mcbor_dec_done |
剩余字节 / 是否读完 |
校验:
| 函数 | 作用 |
|---|---|
mcbor_validate_one |
校验 buffer 包含且仅包含一个完整受支持的 CBOR 项,无 trailing 字节 |
mcbor_err_str |
错误码转字符串,日志用 |
适用边界
适合:传感器遥测、配置项导出、日志结构化字段、BLE GATT characteristic 载荷、MQTT/CoAP 短消息。
不适合:
- 载荷里需要 64 位整数、大整数、半精度/双精度浮点、Tag(比如日期 Tag 1)。
- 需要流式编码(encoder 只写定长容器头,必须提前知道元素个数)。
- 接收端需要 canonical CBOR(确定性编码):库不排序 map key,不做最短浮点表示,NaN payload 原样出。
跑通 demo 之后,下一篇 microcbor 原理:头字节编码、sticky overflow 与迭代式 skip 展开内部实现,重点看 enc_head 宽度选择、dec_skip_core 的迭代计数、以及 UTF-8 校验放在编码端还是解码端这个取舍。