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 上会遇到几个具体问题:

CBOR(RFC 8949)是二进制 JSON 替代品,主要思路是用一个头字节同时携带「类型 + 长度信息」,去掉所有结构标点,整数和浮点数走二进制定长编码。同一条消息通常比 JSON 短 30%–50%,在 LoRa 51 字节 payload 限制下这个差距直接决定能不能一帧发完。

JSON 与 CBOR 字节占用对比

microcbor 的定位

microcbor 面向遥测场景裁剪了 RFC 8949 子集,只实现:

砍掉的部分也是刻意的:不支持 64 位整数、不定长编码、Tag、半精度/双精度浮点。换来的是两个 .c/.h 就能集成、没有任何 malloc、编码端有 sticky overflow 标志可以连写后统一检查。

集成方式

代码只有一个实现文件和两个头文件:

把这三个文件拷进工程,#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

几个阅读这段代码时需要注意的点:

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 短消息。

不适合:

跑通 demo 之后,下一篇 microcbor 原理:头字节编码、sticky overflow 与迭代式 skip 展开内部实现,重点看 enc_head 宽度选择、dec_skip_core 的迭代计数、以及 UTF-8 校验放在编码端还是解码端这个取舍。