嵌入式项目进阶:安全 OTA、平台化架构、性能优化与低功耗设计


图片中的内容不是若干孤立的技术名词,而是一条从“产品可以运行”走向“产品可以长期演进”的工程路线:首先建立断电安全、可回滚、可验证的 OTA;然后通过分层、抽象和自动化测试形成可复用平台;最后利用 DMA、Cache、存储器布局和低功耗状态机获得稳定、可测量的系统性能。

关键词: OTA、Bootloader、A/B 分区、PKI、断点续传、链接脚本、嵌入式架构、HIL、DMA、Cache、中断优化、低功耗、协议设计


一、图片涉及的三条进阶路线

图片中的知识点可以归纳为三组。

1. OTA 与产品持续交付

  • Bootloader 到 APP 的启动链;
  • 断电保护与失败回滚;
  • A/B 分区;
  • 固件签名、密钥管理与 PKI;
  • 差分升级、断点续传;
  • 分散加载文件或链接脚本;
  • 固件 DevOps。

2. 平台化与架构设计

  • 用 C 语言模拟面向对象;
  • 分层和依赖倒置;
  • MCU、RTOS 与外设可替换;
  • 接口契约、设计模式和组件组合;
  • 单元、回归、集成、冒烟和 HIL 测试;
  • CI/CD、跨平台时序监控、数字孪生与团队流程。

3. 性能与低功耗

  • 发挥 MCU 内部并行资源;
  • DMA 与 CPU 并行;
  • Cache 深度应用;
  • 中断延迟和吞吐优化;
  • 内存与运行域优化;
  • 低功耗状态机;
  • 通信协议设计。

这三组能力相互关联。例如一个安全 OTA 不仅需要 Bootloader,还需要链接布局、密钥服务、自动化测试、HIL 断电注入和发布监控。


二、OTA 系统的基本组成

一个可维护的 OTA 系统通常包含:

云端发布服务

    ├── 固件仓库
    ├── 版本和设备策略
    ├── 签名及密钥服务
    └── 发布监控


设备通信与下载模块


升级管理器
    ├── 下载校验
    ├── 分区写入
    ├── 状态记录
    └── 请求重启


Bootloader
    ├── 验证镜像
    ├── 选择启动分区
    ├── 回滚判断
    └── 跳转 APP

应当把以下职责分开:

模块 主要职责
下载模块 网络连接、分片下载、断点续传
升级管理器 版本检查、空间检查、写入候选分区
Bootloader 启动选择、镜像认证、回滚与防降级
APP 运行自检、确认新版本、报告状态
云端服务 设备分组、签名、灰度发布和审计

Bootloader 应尽量小、稳定和少改动。网络协议、复杂文件系统和业务逻辑尽量留在 APP 或独立升级服务中。


三、Bootloader 跳转 APP 的完整过程

Bootloader 验证 APP 后,不能只把 PC 改成 APP 入口。以 Cortex-M 为例,通常要处理:

  1. 停止 Bootloader 使用的定时器和 DMA;
  2. 关闭或清除相关中断;
  3. 恢复需要交给 APP 重新配置的时钟与外设;
  4. 读取 APP 向量表第 0 项作为新 MSP;
  5. 读取向量表第 1 项作为复位入口;
  6. 将向量表地址写入 SCB->VTOR
  7. 设置 MSP;
  8. 跳转到 APP 的 Reset_Handler

概念代码如下:

typedef void (*app_entry_t)(void);

void jump_to_app(uint32_t app_base)
{
    uint32_t app_msp   = *(uint32_t *)(app_base + 0U);
    uint32_t app_reset = *(uint32_t *)(app_base + 4U);

    disable_and_clear_interrupts();
    stop_bootloader_peripherals();

    SCB->VTOR = app_base;
    __DSB();
    __ISB();

    __set_MSP(app_msp);
    ((app_entry_t)app_reset)();
}

正式使用前还应检查:

  • app_msp 是否落在合法 RAM 范围;
  • app_reset 是否落在合法 APP Flash 范围;
  • Reset 向量最低位是否满足 Thumb 状态要求;
  • 镜像哈希与签名是否正确;
  • 目标硬件型号是否匹配;
  • 安全版本计数器是否允许启动。

四、OTA 遇到断电应该怎样处理

断电安全的核心不是“尽量避免断电”,而是设计一个无论在哪条 Flash 写入指令后复位,都能恢复到确定状态的升级状态机。

1. 最重要的不变量

升级过程中应始终满足:

在新镜像完成验证和确认以前,至少保留一个可启动的旧镜像。

因此不能先擦除唯一可运行的 APP,再下载新版本。

2. 升级状态机

可以使用如下状态:

EMPTY
  │ 下载完成

DOWNLOADED
  │ 哈希与签名通过

VERIFIED
  │ Bootloader 选择试运行

TESTING
  ├── APP 自检成功 → CONFIRMED
  └── 看门狗复位/未确认 → REVERT

APP 只有在完成关键自检后才确认新版本,例如:

  • 关键外设初始化成功;
  • 配置和数据库能够读取;
  • 与主控或服务器建立通信;
  • 没有连续 HardFault 或看门狗复位;
  • 数据格式迁移完成。

MCUboot 的测试升级允许新镜像先运行一次;如果 APP 没有把自己标记为 OK,下次启动时回滚旧镜像。这种机制用于降低错误固件造成设备变砖的风险。MCUboot 设计文档

3. 元数据必须可恢复

升级状态、版本号和下载进度不能只写在一个普通变量中。常用方式包括:

  • 两份或多份状态记录;
  • 序号加 CRC;
  • 追加式日志;
  • Flash 写入前后状态分离;
  • 使用只能从 1 写成 0 的状态位;
  • 擦除操作与有效状态切换分开。

恢复时选择 CRC 正确且序号最新的一份记录。

4. 断电测试要自动化

HIL 测试台应在以下步骤随机切断电源:

  • 擦除分区时;
  • 写镜像数据时;
  • 写状态字时;
  • 完成交换但尚未确认时;
  • APP 数据迁移时。

每次重新上电后检查设备是否仍能启动已确认镜像,或能继续未完成的升级。


五、A/B 分区设计

A/B 分区是设备中同时保存两份 APP 镜像:

Flash
┌──────────────────────┐
│ Bootloader           │
├──────────────────────┤
│ Slot A:当前版本      │
├──────────────────────┤
│ Slot B:候选/旧版本   │
├──────────────────────┤
│ 升级状态与配置        │
└──────────────────────┘

常见升级策略

策略 做法 优点 代价
Direct XIP A、B 都能直接执行 切换快、少复制 两个槽位可能需要不同链接地址或硬件重映射
Swap 将候选镜像交换到主槽 APP 可使用统一链接地址 交换逻辑复杂,必须支持中断恢复
Overwrite 验证后覆盖主槽 实现相对简单 需要额外备份,否则回滚能力弱
RAM Load 从槽位装载到 RAM 运行 灵活 需要足够 RAM,启动时间增加

MCUboot 将升级结果抽象为 TEST、PERM、REVERT 等状态,并记录交换进度以恢复中途中断的操作。MCUboot 设计文档

A/B 分区需要提前计算

  • 两个镜像最大尺寸;
  • Flash 擦除扇区边界;
  • 镜像头、签名和 Trailer 大小;
  • 差分包或解压临时空间;
  • 配置、日志和故障转储空间;
  • Bootloader 自身升级策略;
  • Flash 擦写寿命。

不能只按当前 APP 大小切分分区,应给未来功能增长和签名元数据留出余量。


六、固件签名、密钥存储与 PKI

1. 签名与加密不是一回事

能力 解决的问题
哈希 检测固件是否被修改
数字签名 验证固件是否由受信发布者授权
加密 防止固件内容被读取
防回滚计数器 防止安装旧的、存在漏洞的合法固件

只做 CRC 不能证明固件来源,因为攻击者可以修改固件后重新计算 CRC。

2. 签名密钥怎样管理

建议采用分层密钥体系:

离线 Root Key
      │ 授权

发布/Targets 签名密钥
      │ 签署

固件清单与镜像哈希

基本原则:

  • Root 私钥离线保存,使用频率尽量低;
  • 生产签名私钥放入 HSM 或受控签名服务;
  • CI 构建节点不能直接保存长期签名私钥;
  • 设备中通常保存公钥或公钥哈希,而不是发布私钥;
  • 每次签名操作都应有身份、审批和审计记录;
  • 支持签名密钥轮换与撤销;
  • 开发、测试和生产环境使用不同密钥。

TUF 将 Root、Targets、Snapshot 和 Timestamp 角色分离,Root 角色负责授权其他角色的验证密钥,从而降低单个在线密钥泄露的影响。TUF 官方说明

3. 设备中的根信任放在哪里

按安全性从高到低,常见位置包括:

  • 芯片 ROM;
  • OTP 或 eFuse;
  • TrustZone 安全区;
  • 安全元件;
  • 受读写保护的内部 Flash;
  • 普通 APP Flash。

设备中可保存:

  • 固件签名公钥;
  • Root 公钥哈希;
  • 允许的密钥版本;
  • 防回滚安全计数器;
  • 设备身份私钥。

固件发布签名私钥与设备身份私钥应当分离。前者授权固件,后者用于设备认证和签署设备报告。

4. 防回滚

即使旧固件签名有效,也可能包含已修复漏洞,因此需要安全版本计数器:

候选镜像安全版本 >= 设备已接受的安全版本

计数器应保存在可信非易失区域。MCUboot 支持在签名镜像中携带安全计数器,并与设备保存的可信计数进行比较。MCUboot 设计文档

5. 设备群的 PKI

大型设备群还要管理:

  • 每台设备的唯一身份;
  • 硬件型号与允许固件的对应关系;
  • 设备证书签发、更新和撤销;
  • 发布角色与审批权限;
  • 安全时间或可信版本新鲜度;
  • 设备版本清单和升级审计。

车载等多 ECU 场景可以参考 Uptane。它使用 Root、Targets、Snapshot、Timestamp 等角色,并要求 ECU 在安装前验证元数据和镜像。Uptane Standard


七、差分升级与断点续传

1. 断点续传

完整镜像应切分为固定大小的数据块:

Image
├── Chunk 0
├── Chunk 1
├── Chunk 2
└── ...

升级清单至少包含:

  • 目标硬件型号;
  • 固件版本和安全版本;
  • 镜像总长度;
  • 整体哈希;
  • 分块大小;
  • 每块哈希或认证信息;
  • 签名;
  • 压缩或差分算法;
  • 所需基础版本。

设备下载每个块后应:

  1. 校验块编号和长度;
  2. 校验块数据;
  3. 写入候选分区;
  4. 更新带 CRC 的进度记录;
  5. 重启后从第一个未完成块继续。

进度记录不宜每收到很小的数据包就擦写一次 Flash,可以使用位图、追加日志或周期性检查点降低磨损。

2. 差分升级

差分包保存“旧版本到新版本的变化”,可以显著减少传输量,但增加了设备端复杂度。

需要解决:

  • 基础版本必须完全匹配;
  • 差分包本身需要签名;
  • 重建后的完整镜像仍需再次校验和签名验证;
  • 需要 Patch 工作区;
  • Patch 过程中断电必须可恢复;
  • 随机读取旧镜像和写新镜像可能影响 Flash 寿命;
  • 差分算法需要受限内存和流式处理实现。

不要直接在当前唯一可运行镜像上原地打补丁。更安全的方式是在候选槽位中重建完整新镜像,验证通过后再切换。


八、分散加载文件与链接脚本

“分散加载文件”通常指 Arm/Keil Scatter File;GNU 工具链中对应 Linker Script。它们决定每个段的运行地址和装载地址。

典型 Flash 布局:

0x08000000  Bootloader
0x08020000  APP Slot A
0x080A0000  APP Slot B
0x08120000  OTA Metadata
0x08122000  User Config

APP 链接地址必须与实际执行地址一致,除非硬件支持地址重映射、镜像使用位置无关代码,或 Bootloader 先把镜像交换到固定主槽。

链接脚本需要管理:

  • .isr_vector
  • .text.rodata
  • .data 的 Flash 装载地址和 RAM 运行地址;
  • .bss
  • 栈和堆;
  • RAM 函数;
  • DMA 缓冲区;
  • 不可缓存区;
  • OTA 元数据;
  • NoInit 或掉电保留区。

示意:

MEMORY
{
    FLASH_APP (rx)  : ORIGIN = 0x08020000, LENGTH = 512K
    RAM      (rwx)  : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .isr_vector : { KEEP(*(.isr_vector)) } > FLASH_APP
    .text       : { *(.text*) *(.rodata*) } > FLASH_APP
    .data       : { *(.data*) } > RAM AT > FLASH_APP
    .bss        : { *(.bss*) *(COMMON) } > RAM
}

构建流水线应自动检查:

  • Bootloader 和 APP 是否重叠;
  • 镜像是否超过分区;
  • 向量表是否位于正确地址;
  • 签名 Trailer 是否预留;
  • RAM 区域是否溢出;
  • DMA 缓冲区是否位于 DMA 可访问内存。

九、嵌入式固件 DevOps

固件 DevOps 不只是“编译成功后生成一个 BIN”,而是建立从代码提交到设备反馈的闭环:

代码提交

格式、静态检查

主机单元测试

交叉编译与链接检查

模拟器/目标板集成测试

HIL 与故障注入

生成 SBOM 和版本清单

受控签名

制品仓库

灰度发布

设备遥测、失败率与回滚监控

固件流水线应保存什么

  • ELF、HEX、BIN;
  • Map 文件;
  • 调试符号;
  • 编译器和工具链版本;
  • Git 提交号;
  • 构建参数;
  • 固件版本和安全版本;
  • 镜像哈希与签名;
  • SBOM;
  • 测试报告;
  • Flash/RAM 使用报告。

发布策略

建议按批次进行:

内部设备 → 试点设备 → 1% → 10% → 50% → 全量

每个阶段观察:

  • 下载成功率;
  • 安装成功率;
  • 回滚率;
  • 启动时间;
  • 看门狗和 HardFault 数量;
  • 电量和流量消耗;
  • 新旧协议兼容情况。

连接真实测试板的 CI 可以通过自托管 Runner 驱动烧录器、电源控制器和测试仪器。GitHub 自托管 Runner 文档


十、用 C 语言实现面向对象

C 语言没有类和虚函数,但可以用结构体、函数指针、上下文指针和组合实现接口多态。

1. 接口与实现分离

typedef struct {
    int  (*read)(void *ctx, uint8_t *buf, size_t len);
    int  (*write)(void *ctx, const uint8_t *buf, size_t len);
    void *ctx;
} storage_if_t;

Flash 实现:

static int flash_read(void *ctx, uint8_t *buf, size_t len)
{
    flash_device_t *dev = ctx;
    return low_level_flash_read(dev, buf, len);
}

RAM 模拟实现:

static int fake_read(void *ctx, uint8_t *buf, size_t len)
{
    fake_storage_t *fake = ctx;
    memcpy(buf, fake->data, len);
    return 0;
}

上层升级模块只依赖 storage_if_t,既可以连接真实 Flash,也可以在主机测试中连接 RAM Fake。

2. 封装

头文件只暴露不透明句柄:

typedef struct ota_agent ota_agent_t;

ota_agent_t *ota_agent_create(const ota_config_t *cfg);
int ota_agent_start(ota_agent_t *self);

结构体具体成员放在 .c 文件中,避免外部模块直接修改内部状态。

3. 组合优于复杂继承

嵌入式 C 中更适合把能力组合起来:

OTA Agent
├── Transport 接口
├── Storage 接口
├── Crypto 接口
├── Clock 接口
└── Reboot 接口

这样每个接口都可以独立替换和测试。


十一、分层解耦与 MCU 可替换架构

推荐的依赖方向:

Application            产品业务

Service                OTA、日志、存储、诊断、协议

Device Abstraction     设备接口和领域模型

Driver                 传感器、Flash、通信芯片驱动

HAL / BSP              MCU 外设和板级资源

Hardware               MCU、PCB、外设

上层不直接包含具体 MCU 寄存器头文件。例如业务代码不应直接调用:

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);

而应依赖领域接口:

status_led_set(STATUS_LED_UPDATING);

平台层再把状态映射到具体 GPIO。

换 MCU 时真正可复用的条件

不是增加更多 #ifdef STM32,而是:

  • 上层只依赖稳定接口;
  • MCU 相关代码集中在 HAL/BSP/Port;
  • 接口明确返回值、超时、并发和所有权;
  • 设备能力可以查询;
  • 时钟、DMA、Cache 等差异由平台层处理;
  • 自动化测试覆盖接口行为。

接口契约至少包含

  • 输入参数范围;
  • 返回值;
  • 调用上下文:任务还是 ISR;
  • 是否阻塞;
  • 最大执行时间;
  • 缓冲区所有权;
  • 是否线程安全;
  • 初始化与反初始化顺序;
  • 错误恢复方式。

十二、单元、回归、集成、冒烟与 HIL 测试

测试类型 主要目标 运行位置
单元测试 单个模块逻辑 PC 主机或模拟环境
回归测试 防止旧功能被破坏 CI 自动执行
集成测试 多模块接口是否正确 主机或目标板
冒烟测试 固件基本启动和关键路径 真实目标板
HIL 硬件、时序、电源和故障场景 自动化测试台

OTA 必须覆盖的 HIL 用例

  • 下载任意分片时断网;
  • 任意 Flash 擦写点断电;
  • 候选镜像签名错误;
  • 镜像目标硬件不匹配;
  • 尝试降级到旧安全版本;
  • APP 首次启动看门狗复位;
  • A/B 分区一侧损坏;
  • 下载空间不足;
  • 状态元数据一份损坏;
  • 密钥轮换;
  • 新版本未确认后自动回滚;
  • 多次重复下发同一个升级命令。

测试平台可以包括:

  • 可编程电源;
  • 继电器或电子开关;
  • J-Link/ST-Link;
  • UART/CAN/USB 采集;
  • 示波器或逻辑分析仪;
  • 温箱和负载设备;
  • 自动扫码与设备身份管理。

十三、跨平台时序监控与数字孪生

1. 时序监控

仅记录“功能成功”不足以发现退化,还应记录:

  • 中断最大延迟;
  • 任务唤醒延迟;
  • 队列最大深度;
  • 协议往返时间;
  • DMA 完成时间;
  • Flash 擦写时间;
  • 启动时间;
  • CPU 利用率;
  • 栈高水位;
  • 最小剩余 Heap。

常用测量手段:

  • GPIO 翻转加示波器;
  • Cortex-M DWT 周期计数器;
  • ITM/SWO;
  • ETM Trace;
  • RTOS Trace 工具;
  • 设备遥测和离线日志。

测试报告应比较不同 MCU、编译器、优化等级和固件版本的统计分布,而不只是一次平均值。

2. 数据驱动的数字孪生

数字孪生可以理解为“用真实设备数据持续校准的系统模型”。它可以包含:

  • 电池模型;
  • 电机和负载模型;
  • 网络丢包与延迟模型;
  • 温度和功耗模型;
  • 传感器误差模型;
  • 故障状态机。

它的价值在于提前验证:

  • 升级期间掉电概率;
  • 不同网络条件下的升级时间;
  • 低电量是否允许升级;
  • 新控制算法对功耗和温升的影响。

模型必须由真实 HIL 和现场数据校准,否则只是一个看起来精致但缺乏预测能力的仿真。

3. 大模型辅助开发

大模型可以用于:

  • 生成测试用例草稿;
  • 阅读数据手册和代码;
  • 生成接口文档;
  • 分析日志;
  • 辅助代码审查;
  • 生成模拟对象和测试桩。

但不能让大模型直接决定:

  • 密钥和安全策略;
  • Flash 最终布局;
  • 中断优先级安全性;
  • 功率器件保护参数;
  • 未经验证的量产发布。

所有生成内容仍需经过编译、静态分析、测试和人工审查。


十四、性能优化的正确方法

性能优化应遵循:

确定指标 → 测量 → 定位瓶颈 → 修改 → 再测量 → 回归验证

不要仅凭感觉修改代码。

常见指标

  • 最坏响应时间;
  • 平均与 P99 延迟;
  • 最大中断关闭时间;
  • 每秒数据吞吐量;
  • CPU 周期;
  • 总线利用率;
  • Cache Miss;
  • 内存峰值;
  • 单次操作能量。

性能预算示例

1 ms 控制周期
├── ADC + DMA              100 μs
├── 数据预处理              120 μs
├── 控制算法                300 μs
├── 通信和日志              150 μs
└── 余量                    330 μs

有了预算,才能判断优化目标是算法、总线、DMA、Cache 还是任务调度。


十五、发挥芯片并行与并发能力

MCU 内部不仅有一个 CPU,还可能包括:

  • DMA;
  • 多个总线矩阵;
  • ADC 自动扫描;
  • 定时器触发链;
  • CRC、AES、HASH 加速器;
  • JPEG、DMA2D、CORDIC、FMAC;
  • 双核或协处理器。

并发和并行的区别

  • 并发: 多个工作在时间上交叠;
  • 并行: 多个硬件单元在同一时刻真正执行。

例如:

DMA 接收缓冲区 A
CPU 处理缓冲区 B
外设同时采集下一批数据

这是典型 Ping-Pong 双缓冲。

正确的 DMA 双缓冲状态

FREE → DMA_OWNED → CPU_READY → CPU_OWNED → FREE

必须明确每个缓冲区当前属于 DMA 还是 CPU,防止两者同时读写同一区域。

DMA 并不意味着传输没有成本。它仍会占用内存总线,与 CPU、显示控制器和其他 DMA 竞争带宽。


十六、DMA 与 Cache 一致性

很多 Cortex-M7/STM32H7 问题不是 DMA 配置错误,而是 CPU D-Cache 与 DMA 看到的数据不一致。

CPU 写、DMA 读

CPU 修改发送缓冲区后,数据可能仍在 D-Cache:

CPU Cache:新数据
RAM:旧数据
DMA:从 RAM 读取旧数据

启动 DMA 前通常需要 Clean:

SCB_CleanDCache_by_Addr(buffer, length);

DMA 写、CPU 读

DMA 已把新数据写入 RAM,但 CPU Cache 中仍有旧副本:

DMA → RAM:新数据
CPU Cache:旧数据

DMA 完成后通常需要 Invalidate:

SCB_InvalidateDCache_by_Addr(buffer, length);

注意事项

  • 缓冲区按 Cache Line 对齐;
  • 操作地址和长度扩展到完整 Cache Line;
  • 避免 DMA 缓冲区与普通变量共享 Cache Line;
  • Clean、Invalidate 的方向不能写反;
  • 使用内存屏障保证操作完成;
  • 某些 TCM 区域 DMA 无法访问;
  • 可以通过 MPU 将专用 DMA 区配置为不可缓存。

ST 的 AN4839 专门讨论了 STM32F7/H7 一级 Cache 和 DMA 数据一致性问题。ST AN4839


十七、中断性能优化

中断性能包括:

  • 中断响应延迟;
  • ISR 执行时间;
  • 中断嵌套;
  • 中断抖动;
  • 中断导致的任务唤醒延迟;
  • 中断风暴下的吞吐和丢包。

常见优化原则

  1. ISR 中只做必须立即完成的工作;
  2. 快速读取并清除硬件状态;
  3. 把复杂处理下放到任务;
  4. 避免 ISR 中动态分配内存;
  5. 避免打印、文件系统和长循环;
  6. 使用 DMA 降低逐字节中断频率;
  7. 合理设置优先级和抢占关系;
  8. 缩短全局关中断临界区;
  9. 使用 FromISR API;
  10. 测量最坏时间而不是只测平均时间。

FreeRTOS 中的延后处理

void UART_IRQHandler(void)
{
    BaseType_t wake = pdFALSE;

    read_and_clear_uart_status();
    vTaskNotifyGiveFromISR(uart_task, &wake);
    portYIELD_FROM_ISR(wake);
}

ISR 只通知任务,协议解析放在 uart_task 中执行。

延迟来源

  • 当前正在执行更高优先级 ISR;
  • 全局关中断;
  • BASEPRI 临界区;
  • Flash Wait State;
  • Cache Miss;
  • 总线竞争;
  • FPU 上下文保存;
  • RTOS 调度和任务切换。

十八、内存与运行域优化

1. 把关键代码和数据放到合适区域

不同存储器适合不同用途:

存储区域 典型用途
Flash 普通代码和常量
ITCM 高频关键代码
DTCM CPU 高频访问、低延迟数据
AXI SRAM 大缓冲、通用数据
不可缓存 SRAM DMA 描述符和共享缓冲
外部 SDRAM 帧缓冲和大数据

具体可访问关系取决于芯片总线架构。例如某些 DMA 无法访问 DTCM。

2. 减少复制

可使用:

  • 环形缓冲;
  • 零拷贝队列;
  • 指针传递;
  • Scatter-Gather DMA;
  • Ping-Pong 缓冲。

但指针传递必须定义所有权和生命周期,避免发送方提前释放或修改数据。

3. 栈与 Heap

  • 使用栈高水位测量实际需求;
  • 避免盲目给每个任务分配大栈;
  • 高频对象使用固定块池;
  • 长生命周期对象优先静态分配;
  • 避免频繁分配不同尺寸对象造成碎片;
  • 对 DMA 对象使用明确的对齐和段属性。

4. 算法与数据布局

  • 连续访问优于随机访问;
  • 小型结构体数组和数组结构体要根据访问模式选择;
  • 热数据与冷数据分离;
  • 减少不必要的浮点和除法;
  • 使用硬件加速器前先计算搬运开销;
  • 编译器优化后用反汇编和周期测量验证。

十九、低功耗设计

低功耗不是简单调用一次 sleep(),而是系统级状态机。

ACTIVE
  │ 无任务

IDLE
  │ 长时间无事件

SLEEP / STOP
  │ RTC、GPIO、通信唤醒

RESTORE
  │ 恢复时钟和外设

ACTIVE

低功耗优化层次

  1. 降低无效计算;
  2. 使用 DMA 和硬件自动触发;
  3. 关闭未使用外设时钟;
  4. 降低 CPU 和总线频率;
  5. 使用 Tickless Idle;
  6. 进入更深睡眠模式;
  7. 关闭外部传感器和电源域;
  8. 减少无线唤醒和网络重连。

进入低功耗前

  • 判断是否有未完成 Flash 写入;
  • 判断 DMA 是否完成;
  • 配置唤醒源;
  • 清除历史唤醒标志;
  • 处理可能立即到来的事件;
  • 保存需要保留的状态;
  • 保证中断与睡眠之间没有竞态。

唤醒后

  • 判断真正的唤醒原因;
  • 恢复系统时钟;
  • 恢复外设和引脚;
  • 校正 RTOS 时间;
  • 处理睡眠期间发生的事件。

功耗应以能量衡量:

Energy = Voltage × Current × Time

提高瞬时性能有时反而可以让任务更快完成并进入深睡眠,因此最低频率不一定带来最低总能耗。


二十、嵌入式协议设计

可靠协议至少需要定义:

帧头
版本
消息类型
长度
序号
载荷
校验或认证标签

1. 分帧

串口、TCP 等字节流没有天然消息边界,需要使用:

  • 固定帧头加长度;
  • TLV;
  • COBS;
  • SLIP;
  • 转义字符;
  • 固定长度帧。

解析器必须能从乱码、丢字节和半包中重新同步。

2. 可靠性

  • 序列号;
  • ACK/NACK;
  • 超时和重试;
  • 流量控制;
  • 重复包检测;
  • 分片和重组;
  • 最大报文长度限制。

3. 幂等性

升级命令可能因超时而重复发送。协议应让以下操作可重复:

  • 开始下载同一版本;
  • 重发同一数据块;
  • 查询进度;
  • 请求安装;
  • 请求确认。

设备应根据事务 ID、版本和块编号识别重复请求,而不是重复擦除或重复执行危险动作。

4. 兼容性

  • 明确协议版本;
  • 新字段尽量可忽略;
  • 使用长度而不是固定结构体内存布局;
  • 定义字节序和对齐;
  • 区分必选和可选能力;
  • 在 OTA 前检查 Bootloader、APP 和云端协议兼容矩阵。

5. 安全性

CRC 只能检测随机错误,不能抵抗恶意修改。安全协议还需要:

  • 身份认证;
  • 消息完整性;
  • 防重放随机数或计数器;
  • 会话密钥;
  • 权限检查;
  • 安全错误处理和速率限制。

二十一、建议的项目进阶顺序

阶段一:可恢复

  • 固定 Bootloader 与 APP 分区;
  • 镜像哈希和签名验证;
  • A/B 或可靠备份;
  • 看门狗和回滚;
  • 断电故障注入。

阶段二:可维护

  • 分层架构;
  • 接口契约;
  • 静态分析;
  • 主机单元测试;
  • Map 和内存使用自动检查。

阶段三:可持续交付

  • 自动构建和签名;
  • HIL 流水线;
  • 制品仓库;
  • 灰度发布;
  • 设备遥测和自动停止发布。

阶段四:可度量优化

  • 时序与功耗基线;
  • DMA、Cache 和内存域优化;
  • 中断延迟监控;
  • 数字孪生与现场数据闭环;
  • 跨 MCU 回归测试。

二十二、项目评审检查表

OTA 与安全

  • Bootloader 是否受写保护?
  • 是否验证镜像哈希、签名和硬件型号?
  • 是否支持防回滚?
  • 任意 Flash 操作点断电后能否恢复?
  • 新 APP 未确认时能否自动回滚?
  • 私钥是否脱离普通 CI 节点?
  • 是否支持密钥轮换和撤销?
  • 是否有灰度发布和自动停止策略?

架构与测试

  • 上层是否依赖具体 MCU HAL?
  • 接口是否定义超时、线程安全和所有权?
  • 是否能用 Fake 驱动在 PC 上测试?
  • 是否有单元、集成、回归和 HIL 测试?
  • 链接布局是否由 CI 自动检查?
  • 是否保存 ELF、Map 和调试符号?

性能与低功耗

  • 是否建立最坏延迟和功耗基线?
  • DMA 缓冲区是否处理 Cache 一致性?
  • ISR 是否足够短?
  • 关键代码和数据是否位于合适存储区域?
  • 是否监控栈高水位和 Heap 最低余量?
  • 低功耗进入和唤醒是否有明确状态机?
  • 协议是否支持重试、幂等和版本兼容?

参考资料

  1. MCUboot,《Bootloader Design》。
  2. MCUboot,《Test Plan》。
  3. The Update Framework,《Frequently Asked Questions》。
  4. Uptane,《Uptane Standard》。
  5. STMicroelectronics,《AN4839: Level 1 Cache on STM32F7 and STM32H7》。
  6. GitHub,《Self-hosted Runners Reference》。