某公司在自研 MCU 平台(基于 RT-Thread)时,驱动架构参考了 Linux device tree 的设计思路,固件升级采用 Loader + App 双分区方案。本文整理这套框架的设计要点。
一、框架设计思路
三个原则:
- 简洁易懂
- 兼容有 RTOS 和无 RTOS 两种系统
- 模块化,参考 PX4 的模块思路
二、运行框架(FreeRTOS)
任务调度、初始化、任务退出采用统一模式:
void task(void *pvParameters)
{
init();
while (1)
{
}
vTaskDelete(NULL);
}
任务同步与信息广播:
- 一对一信息传递:queue;多组信息:queue sets
- 广播:mailbox,FreeRTOS 里用 queue 实现(overwrite 模式、长度为 1 的 queue),接收方用 peek 方法获取数据
三、中断与缓存管理
- 中断统一入口:所有中断走一个入口,并维护中断链表,方便各模块解耦挂载/卸载
- 缓存管理:环形缓冲区
四、设备管理:仿 Linux 设备树
借鉴 device tree 的思路描述和注册设备:
- gpio
- pinctrl:负责 IO 复用等配置,其他模块引用 pinctrl 来处理对应 IO
- uart-中断 / uart-dma
- i2c-中断 / i2c-dma
- spi-中断 / spi-dma
- timer-hwtime / timer-capture / timer-pwm / timer-encoder
配合 OSAL(操作系统抽象层)屏蔽 RTOS 差异。
驱动的三类划分
OS 中的驱动分成三类:
- 平台相关驱动(driver):和具体芯片绑定的底层驱动
- 驱动框架(driver):某类外设的通用框架
- 平台无关驱动(component):
- 总线设备
- 应用抽象的模块
- 抽象设备,例如传感器
五、Loader 与固件升级
为什么不用 U-Boot
U-Boot 是为嵌入式 Linux 设计的 bootloader(Cortex-A/MIPS/PowerPC),需要几 MB 的 RAM 和 Flash、完整的 C 运行时环境、DDR 初始化、网卡驱动、文件系统。Cortex-M 单片机有自己的一套方案。
三种可选 Loader 方案
方案 A:MCUboot — 最成熟的开源方案
- Linux Foundation 旗下项目,Zephyr/ESP-IDF/Mynewt/NuttX 均集成
- 专为 32 位 MCU 设计
核心能力:
| 需求 | MCUboot 能力 |
|---|---|
| 加密固件 | AES-128/256 加密镜像,密钥通过 RSA-OAEP / ECIES / AES-KW 加密分发 |
| 串口升级 | boot_serial 模块通过任意串口提供 SMP 协议服务,用 mcumgr CLI 工具上传镜像 |
| 签名验证 | RSA2048/3072、ECDSA、Ed25519 |
| 升级策略 | swap / overwrite / direct-xip / ram-load |
分区管理(双槽位模型):
Flash 布局:
┌──────────────┐
│ Bootloader │ ← MCUboot 本身(20-30KB)
├──────────────┤
│ Slot 0 │ ← Primary(当前运行固件)
│ (App A) │
├──────────────┤
│ Slot 1 │ ← Secondary(新固件暂存区)
│ (App B) │
├──────────────┤
│ Scratch │ ← 可选,swap 算法用
└──────────────┘
三种升级策略:
- Swap(交换):新固件放 slot 1,bootloader 把 slot 0/1 互换,失败可回滚
- Overwrite(覆盖):直接把 slot 1 覆盖到 slot 0,简单但无回滚
- Direct-XIP:两个槽位平等,bootloader 直接选版本号最高的启动
方案 B:ESP-IDF Bootloader — ESP32 生态首选
- 二级引导:ROM Bootloader(固化)→ 2nd Stage Bootloader(可定制)
- 分区表(CSV 定义)极灵活:factory + ota_0 + ota_1 + otadata
- Secure Boot V2(RSA-3072 签名验证)
- Flash Encryption(AES-XTS-256 透明加解密)
- Anti-rollback(eFuse 安全版本号)
方案 C:自研轻量 Loader
- 最小可行:UART 轮询驱动 + Flash 擦写 + 帧解析器,~2KB
- 适用于 flash 极度受限或需要极致简化的场景
自研平台上实际采用的就是方案 C:Loader + App 双分区,固件传输使用 Ymodem 协议。
独立 Loader vs A/B 分区
核心矛盾:
- A/B 分区:升级逻辑在 App 里,外设不耦合,但 App 可用空间减半
- 独立 Loader:App 可用空间接近全部,但外设需实现 Loader 协议
Flash 约束下的算术(1MB Flash):
A/B 分区: Loader 8KB + App A 504KB + App B 504KB → App 最大 504KB
独立 Loader: Loader 16KB + App 1008KB → App 最大 1008KB
Flash 越小,独立 Loader 优势越明显。256KB Flash:A/B 的 App 只有 120KB,独立 Loader 有 240KB。
结论:Flash 紧张时选独立 Loader。 Loader 协议可以极简化(三个命令),外设负担 30 行 C,远小于省出来的半个 Flash。
极简 Loader 串口协议设计
帧格式:
┌──────────┬──────────┬──────────┬──────────────┬──────────┐
│ 魔数 │ 命令 │ 地址 │ 数据 │ CRC16 │
│ 0xA55A │ 1B │ 4B LE │ N bytes │ 2B │
│ 2B │ │ │ │ │
└──────────┴──────────┴──────────┴──────────────┴──────────┘
三个命令:
1. ERASE <addr> <len> → ACK
2. WRITE <addr> <data> → ACK
3. CHECK <crc32> → ACK / NAK
回复帧:
[0xA55A] [status 1B] [CRC16 2B]
ESP32 实现只需 30 行 C。
Loader 体积估算:
| 组件 | 体积 |
|---|---|
| 向量表 + 启动 + 时钟 | ~500B |
| UART 轮询驱动 | ~200B |
| Flash 擦写 | ~300B |
| 帧接收状态机 | ~150B |
| CRC16 | ~80B |
| App 有效性检查 | ~100B |
| 小计(不含加密验签) | ~1.3KB |
| ECDSA 验签(micro-ecc,可选) | ~3KB |
| 含验签 | ~4.5KB |
App 触发升级机制
1. App 收到 OTA 请求
2. 写标志区:{MAGIC=0xA55A, CMD=UPGRADE, ...}
3. NVIC_SystemReset() → 软复位
4. Loader 启动 → 读标志区 → 发现升级命令
5. 进入串口接收模式
6. 升级完成 → 清标志区 → 跳转 App
断电保护:升级中断电 → 重启 → Loader 读标志区发现 CMD=UPGRADE,但 App CRC 不对 → 继续等上位机重发。Loader 永远不会丢,App 写坏了标志区还留着。
App 有效性检查
bool is_app_valid(void) {
// 1. 栈指针检查
uint32_t sp = *(uint32_t *)APP_ADDR;
if (sp < 0x20000000 || sp > 0x20080000) return false;
// 2. Reset Handler 检查
uint32_t reset = *(uint32_t *)(APP_ADDR + 4);
if (reset < 0x08000000 || reset > 0x083C0000) return false;
// 3. CRC 校验(可选)
return true;
}
硬件加速:GD32H7 的 RTDEC
GD32H7 FMC 内置硬件 AES 解密引擎(Real-Time Decryption):
- EFUSE 中烧录 AES 密钥(不可读出)
- 写入 Flash 的数据预先加密
- 读取时硬件自动解密,CPU 看到明文
- 对软件完全透明,不需要调用 crypto 库
这意味着固件加密升级,Loader 端零代码:上位机加密后直接写入 Flash → 硬件自动解密 → CPU 执行明文。签名验证(micro-ecc ECDSA)是可选的第二层安全。
另有 BOOT_ADDR 选项字节(FMC_BTADDR 寄存器),BOOT 管脚高低分别对应 BOOT_ADDR0/BOOT_ADDR1 两个启动地址,出厂默认指向片内 Bootloader;修改选项字节改变启动地址,可实现软件 A/B 切换。
推荐方案组合:
Flash 布局:
┌──────────┬──────────┬──────────────────────────┐
│ Loader │ 标志区 │ App │
│ 8KB │ 4KB │ 3828KB │
│ 0x0800 │ 0x0802 │ 0x0803 │
│ 0000 │ 0000 │ 0000 │
└──────────┴──────────┴──────────────────────────┘
安全链:
上位机:app.bin → AES加密(用设备密钥) → app.enc.bin
串口传输 → Loader 写入 Flash(密文)
CPU 执行 → RTDEC 自动解密 → 运行明文代码
六、MCUmgr 与 SMP 协议(开源生态参考)
MCUmgr 是 MCUboot 生态里的设备管理协议和工具:
- SMP 协议层:Simple Management Protocol,极简请求/响应
- mcumgr CLI:PC 端 Go 语言客户端
SMP 串口帧格式(源码验证)
线缆上的格式:
[0x06 0x09] base64(frame) \n ← 新帧开始
[0x04 0x14] base64(continuation) \n ← 后续分片
base64 解码后的 frame:
┌──────────┬──────────────────────────────┬──────────┐
│ totlen │ nmgr_hdr + CBOR data │ CRC16 │
│ 2B BE │ 8B + N bytes │ 2B BE │
└──────────┴──────────────────────────────┴──────────┘
└─── CRC16 覆盖这段 ──────────┘
nmgr_hdr 结构(8 字节):
struct nmgr_hdr {
uint8_t nh_op:3; // 0=READ, 2=WRITE
uint8_t nh_version:2;
uint8_t _res1:3;
uint8_t nh_flags;
uint16_t nh_len; // CBOR payload 长度 (大端)
uint16_t nh_group; // 0=OS, 1=IMG, 64=PERUSER
uint8_t nh_seq;
uint8_t nh_id; // 组内命令ID
} __packed;
关键特性:
- 有 CRC16:覆盖 header + data,CCITT 多项式 0x1021,初始值 0x0000
- 有 base64 编码层:让二进制数据变成可打印 ASCII,兼容终端
- 分片 MTU 127 字节/行:兼容 128 字节行缓冲区的终端
- boot_serial 是裸机程序:不需要 RTOS,只需 UART 驱动 + Flash HAL
SMP 支持的命令:echo(OS,测通断)、reset(OS,复位)、image list(IMG,查看槽位固件)、image upload(IMG,上传新固件)。
二进制序列化格式选型
| 格式 | 编码大小 | 解析器体积 | 自描述 | 适用场景 |
|---|---|---|---|---|
| CBOR | 小 | ~500B | 是 | 通用嵌入式 |
| MessagePack | 小 | ~1KB | 是 | 嵌入式、Redis |
| Protobuf (nanopb) | 极小 | ~2KB | 否(需 .proto) | 带宽极度受限 |
| FlatBuffers | 零拷贝 | ~3KB | 否 | 零解析开销 |
- 通用场景、需人类调试 → CBOR / MessagePack
- 设备端和 PC 端协议固定 → nanopb,最省字节
- 数据量大、不想反复解析 → FlatBuffers
设备管理/固件升级协议生态
| 协议 | 标准化 | 编码 | 传输 | 代码体积 | 适用场景 |
|---|---|---|---|---|---|
| SMP | Mynewt 项目规范 | CBOR | 任意串口 | ~2KB | 本地串口升级 |
| LwM2M | OMA 标准 | CBOR/TLV/JSON | CoAP over UDP | ~20KB+ | 云端管理百万设备 |
| SUIT (RFC 9019) | IETF RFC | CBOR | 不限定 | ~30KB+ | 需过认证的产品 |
经典串口传输协议:XMODEM(128B,停等)、YMODEM(1024B,批量)、ZMODEM(可变包,流式、断点续传)。
七、帧格式设计:字节冲突与 COBS
二进制帧协议的核心难题:帧头魔术字节(如 0xA55A)可能出现在数据中,导致误判。
| 方案 | 开销 | 帧同步安全性 | 代码量 |
|---|---|---|---|
| base64 | 33% | 天然安全(数据变 ASCII) | ~80行 |
| COBS | <0.4% | 0x00 边界保证 | ~40行 |
| SLIP | <0.1%(典型) | 0xC0 边界保证 | ~50行 |
| 裸二进制 + 长度定界 | 0% | CRC 兜底(概率性) | ~30行 |
COBS(Consistent Overhead Byte Stuffing)把数据中的 0x00 替换为偏移量,编码后数据内部不存在 0x00,用 0x00 做帧边界标记绝不误判;实际固件开销 <1%(全零数据是理论最坏情况 100%,但固件不会全零)。被 PX4 Autopilot、Zephyr shell 等实际使用。
推荐:
- 短距 TTL 链路(PCB 走线)→ 长度定界 + CRC16 + 超时,最简单
- 长线缆 / 强干扰环境 → COBS,确定性安全
- 不用 base64(33% 开销太大,仅当需要终端兼容时才考虑)
八、签名工具链:imgtool
MCUboot 的 PC 端工具,即使不用 MCUboot 运行时也可单独使用:
# 生成签名密钥对
imgtool keygen -t ecdsa-p256 -k priv.pem
# 签名固件
imgtool sign --key priv.pem --align 4 --version 1.2.0 \
--header-size 0x200 --slot-size 0x3C0000 \
app.bin app.signed.bin
# 加密固件(如果不用 RTDEC)
imgtool encrypt --key enc.pem app.signed.bin app.encrypted.bin
生成带 TLV trailer 的标准格式签名镜像,自己写的 Loader 解析 TLV 做验签即可。
九、补充:设备树与 U-Boot/Linux
- U-Boot 和 Linux 共用同一套 .dts/.dtb 格式、同一个 dtc 编译器、同一个 libfdt 库
- 工程中通常用同一份 .dts,通过
u-boot,*前缀属性标记 U-Boot 特殊行为;Linux 遇到不认识的属性直接忽略 compatible写多个值是硬件兼容层级(从具体到通用),不是给 U-Boot 和 Linux 分的:
compatible = "rockchip,rk3588-uart", "rockchip,rk3399-uart", "snps,dw-apb-uart";
// ↑ 最具体的芯片 ↑ 兼容的上一代 ↑ 通用 IP 核
两边的驱动匹配表各自维护,碰巧用同一个 compatible 字符串查自己的驱动。
十、题外:Protobuf + Zenoh 的适用边界
- 不适合单片机串口固件升级(过度复杂)
- 适合多节点机器人系统、分布式数据采集、ROS 2 生态整合