Mesh 模型与 Mesh DFU 固件升级
本文档介绍 AC63 系列 SDK 中 SIG Mesh 应用(apps/mesh)的模型框架与 Mesh DFU(Distributed Firmware Update,分布式固件升级)实现,涵盖 Distributor(分发器)与 Target(目标节点)两种角色、APP 命令协议、固件文件读取/校验/擦写流程及状态机。
Purpose and Scope
本页覆盖以下内容:
apps/mesh应用工程的启动流程与 Mesh 模型配置入口(CONFIG_MESH_MODEL)。- Mesh DFU 的两种角色实现:Distributor(
mesh_distributor_loader.c、mesh_dfu_app_cmd_protocol.c)与 Target(mesh_target_node_ota.c/h)。 - Distributor 与 APP 之间的私有命令协议(帧头、SN 防重放、应答机制)。
- 固件文件从 flash 配置区读取、CRC-16-XMODEM 校验、经 Mesh 网络下发、目标节点写 flash 的完整链路。
- Target 节点的 OTA 状态机、命令类型与错误码。
不在本页范围内的相关内容:SIG Mesh 协议栈底层实现(apps/common/third_party_profile/sig_mesh 下的 API 与适配层)、Tencent LLsync Mesh(llsync_mesh)私有实现,以及各芯片板级 BLE 配置,请参见对应的协议栈与板级文档页。
Overview
Mesh DFU 是 SIG Mesh 标准中用于对网络中多个节点进行固件升级的机制。在本 SDK 中,其被实现为一种"分发器-目标节点"(Distributor–Target)架构:
- APP(手机端) 通过私有串口/BLE 通道把固件升级请求发给 Distributor;
- Distributor 从 flash 配置区读取预先烧录的固件 bin 文件,按命令协议组包,并通过 Mesh 模型消息广播/单播给网络中的 Target 节点;
- Target 节点接收数据包,按序写入自身 flash,完成校验后重启生效。
该设计把"固件文件管理"与"无线传输"分离:Distributor 只负责把固件文件切分成 Mesh 消息并跟踪下发进度;Target 只负责接收、落盘与校验。这样可以在不改变 Mesh 协议栈的前提下,通过 CONFIG_MESH_MODEL 编译开关选择设备角色,实现同一套代码仓库支持"升级发起方"和"升级执行方"两种形态。
应用入口位于 app_mesh.c:app_start() 中调用 btstack_init() 启动蓝牙协议栈并注册按键事件,随后由 state_machine() 处理 APP_STA_CREATE/APP_STA_START 生命周期。
Architecture
下图展示了 Mesh DFU 的整体架构与数据流向:
flowchart TD
subgraph sg_App["APP / 手机端"]
AppClient["APP Client"]
end
subgraph sg_Dist["Distributor 设备 (SIG_MESH_DFU_DISTRIBUTOR_DEMO)"]
CmdProto["mesh_dfu_app_cmd_protocol.c<br/>命令协议 / SN 校验 / 应答"]
Loader["mesh_distributor_loader.c<br/>读取固件文件 + CRC-16-XMODEM"]
FlashCfg["Flash 配置区<br/>mnt/sdfile/app/mesh"]
MeshTx["Mesh 模型发送<br/>(sig_mesh_api)"]
CmdProto --> Loader
Loader --> FlashCfg
Loader --> MeshTx
CmdProto --> MeshTx
end
subgraph sg_Mesh["BLE Mesh 网络"]
MeshNet["Mesh 消息 (广播/单播)"]
end
subgraph sg_Target["Target 设备 (SIG_MESH_DFU_TARGET_DEMO)"]
MeshRx["mesh_target_node_ota.c<br/>状态机 / 分包接收"]
FlashWrite["Flash 写入 (sfc)"]
MeshRx --> FlashWrite
end
AppClient -->|"升级命令/固件信息"| CmdProto
MeshTx --> MeshNet
MeshNet --> MeshRx
MeshRx -->|"应答/状态上报"| MeshNet
MeshNet -->|"应答"| CmdProto
各组件职责:
- mesh_dfu_app_cmd_protocol.c:定义 Distributor 与 APP 间的命令帧格式(帧头
0x5A、6 字节协议头、命令/响应标志位),维护opcode_state_t dfu_app_cmd做 SN 连续性校验,防止重放与乱序攻击;同时提供广播过滤数据供发现设备使用。 - mesh_distributor_loader.c:负责从 flash 配置区(路径
mnt/sdfile/app/mesh)读取目标固件文件,计算 CRC-16-XMODEM(多项式0x1021),按 4096+16 字节的读块大小切分,并驱动擦写与下发进度。 - mesh_target_node_ota.c/h:Target 侧实现,按
dfu_node_cmd_type_t命令类型(REQ/FILE_INFO/DATA/END/FAIL)驱动MESH_TARGET_OTA_st_e状态机,把收到的数据写入 flash。 - app_mesh.c:应用主入口,初始化蓝牙协议栈,根据
CONFIG_MESH_MODEL加载对应的 Mesh Demo 配置。
角色选择通过编译宏
CONFIG_MESH_MODEL == SIG_MESH_DFU_DISTRIBUTOR_DEMO或SIG_MESH_DFU_TARGET_DEMO决定,相关代码均以该宏包裹(见 mesh_dfu_app_cmd_protocol.c、mesh_distributor_loader.c、mesh_target_node_ota.c)。
命令协议:Distributor 与 APP 的交互
帧格式与常量
mesh_dfu_app_cmd_protocol.c 定义了 Distributor 与 APP 之间的私有命令协议,关键常量如下:
#define CMD_HEADER_FLAG 0x5A
/* total length of Head flag, Length, Flag, OP, SN, CRC */
#define CMD_PROTOCOL_LENGTH 6
#define COMMAND_PACKET 0x81
#define NO_RESPONSE_PACKET 0x01
#define RESPONSE_PACKET 0x00
#define BUFFER_SIZE_TARGET 4096
Source: mesh_dfu_app_cmd_protocol.c
协议头固定 6 字节,依次为:帧头标志(0x5A)、长度、标志字节、操作码(OP)、序号(SN)、CRC。标志字节区分三类帧:
| 标志值 | 含义 |
|---|---|
COMMAND_PACKET(0x81) | APP 下发的命令帧 |
RESPONSE_PACKET(0x00) | Distributor 的应答帧 |
NO_RESPONSE_PACKET(0x01) | 无需应答的帧 |
命令处理过程中,固件数据被累积到全局缓冲区 global_buffer(global_buffer_size / global_buffer_used 跟踪使用量),global_file_size 记录固件总大小;dfu_cfg_block_data[200] 用于承载固件配置块信息。
SN 防重放校验
SN(序号)校验由 opcode_state_t dfu_app_cmd 维护,逻辑如下:
static bool check_sn(opcode_state_t *op, uint8_t sn)
{
if (!op->valid) {
/* If the op was not previously recorded, it is considered valid (initialized) */
op->prev_sn = sn;
op->valid = true;
return true;
}
/* Check the serial number for legitimacy */
if (sn == (op->prev_sn + 1) % 256) {
/* Sequence number is legal, update prev_sn */
op->prev_sn = sn;
return true;
}
/* Sequence number is not legal, could be a retransmission or an attack */
return false;
}
Source: mesh_dfu_app_cmd_protocol.c
设计意图:SN 按 (prev_sn + 1) % 256 严格递增。首次收到任意 SN 即认为合法(初始化);此后非连续序号一律拒绝,从而同时防御重放攻击(重复的旧序号)和乱序/丢包(跳过序号)。clear_sn() 用于会话结束或复位时把 valid 置为 false,重新开始计数。由于 SN 只有 8 位,循环 256 个包后复用,适合每包数据量较小、单次升级包数可控的固件传输场景。
广播过滤与设备发现
Distributor 通过 get_mesh_dfu_adv_filter() 向广播数据中附加厂商过滤字段,用于让 APP 识别可升级设备:
void get_mesh_dfu_adv_filter(u8 *len, u8 **data)
{
u8 adv_filter[] = {0x04, 0x16, 0x00, 0xBD, 0x01};
u8 adv_len = 0x5;
memcpy((*data) + (*len), adv_filter, adv_len);
*len += adv_len;
}
Source: mesh_dfu_app_cmd_protocol.c
该字段格式为 BLE AD Type 0x16(Service Data)+ 16 位 Company ID 0x01BD(杰理/JieLi 的 SIG 公司标识),配合 0x04 长度字节。APP 侧扫描到该 Service Data 即可判定这是支持 Mesh DFU 的 Distributor 设备。
固件文件读取与校验:mesh_distributor_loader.c
Distributor 侧固件管理由 mesh_distributor_loader.c 承担。它把目标固件 bin 文件预先存放在 flash 配置区(路径 mnt/sdfile/app/mesh,由 isd_config_rule.c 配置),运行时按块读取并下发。
CRC-16-XMODEM 校验
每个下发块都带 CRC 校验,采用 CRC-16-XMODEM(多项式 0x1021):
static uint16_t crc16_xmodem(const uint8_t *data, size_t length, uint16_t crc)
{
for (size_t i = 0; i < length; i++) {
crc ^= (uint16_t)data[i] << 8; // 将当前字节移入CRC高位
for (int j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ POLY; // 如果最高位为1,左移并异或多项式
} else {
crc <<= 1; // 否则直接左移
}
}
}
return crc;
}
Source: mesh_distributor_loader.c
逐字节、逐位(8 轮)的软件实现,每次先左移 8 位再按最高位决定是否异或多项式。它同时被 Distributor(计算发送块 CRC)和 Target(校验接收块 CRC)使用,保证两端算法一致。
Flash 配置区擦除与读取
下发前需要确保目标 flash 区域可写,mesh_dis_loader_info_zone_earse() 负责擦除:
static bool mesh_dis_loader_info_zone_earse(u32 fsize, u32 sclust)
{
if (!fsize || !sclust) {
log_error("please enter the correct file size and address!");
return false;
}
if (sfc_erase_zone(sclust, fsize)) {
log_info("erase ok!");
return true;
} else {
log_error("erase fail!");
return false;
}
}
Source: mesh_distributor_loader.c
要点:
- 参数为文件大小
fsize与起始簇地址sclust,两者任一为 0 直接失败——这是防止误擦除全盘的保护性检查; - 通过
sfc_erase_zone()(串行 flash 控制器擦除接口)按扇区擦除; - 文件句柄
mesh_distributor_loader_fp、读取偏移mesh_dist_loader_cur_offset(volatile,供回调/中断上下文读取)以及完成回调mesh_dist_complete_cb构成文件读取的三要素。
分块读取
读块大小常量:
#define OTA_READ_FLASH_SIZE (4096 + 16)
Source: mesh_distributor_loader.c
每次从 flash 读取 4096 字节固件数据外加 16 字节头/校验信息,装入 test_buffer[OTA_READ_FLASH_SIZE] 后交由上层组包发送。4096 字节与 BUFFER_SIZE_TARGET 保持一致,保证单次读出的数据块恰好是 Target 单次可处理的最大负载。
Core Flow:一次完整的 Mesh DFU 升级
以下时序图展示了从 APP 发起升级到 Target 写入完成的完整链路:
sequenceDiagram
participant APP as APP / 手机端
participant D as Distributor<br/>(cmd_protocol + loader)
participant M as Mesh 网络
participant T as Target 节点<br/>(mesh_target_node_ota)
APP->>D: 命令帧 (0x5A + OP + SN + CRC)
D->>D: check_sn 校验序号连续性
D->>D: 解析 OP / 累积数据到 global_buffer
D->>D: 读取固件文件 (mnt/sdfile/app/mesh)
D->>D: crc16_xmodem 计算发送块 CRC
D->>M: Mesh 模型消息 (DFU_NODE_OTA_DATA)
M->>T: 广播/单播转发
T->>T: 状态机推进 (IDLE→REQ→CHECK_FILESIZE→WRITE_DATA)
T->>T: 校验 CRC / 写入 flash (sfc)
T-->>M: 应答帧 (RESPONSE_PACKET 0x00 / 错误码)
M-->>D: 应答消息
D-->>APP: 应答帧 (含状态/错误码)
流程要点:
- 命令接收:APP 发送以
0x5A开头的命令帧,Distributor 的check_sn()先做序号校验,非法序号(重放/乱序)直接丢弃。 - 文件装载:确认升级意图后,
mesh_distributor_loader.c打开mnt/sdfile/app/mesh下的固件文件,必要时先sfc_erase_zone擦除目标区域。 - 分块发送:每次读取 4096+16 字节,计算 CRC-16-XMODEM 后组包,经 Mesh 模型发送接口(
sig_mesh_api)下发。 - Target 接收:
mesh_target_node_ota.c按dfu_node_cmd_type_t解析命令,驱动状态机完成写 flash;每步可回送应答(RESPONSE_PACKET 0x00)或错误码。 - 会话结束:收到
DFU_NODE_OTA_END后 Target 进入MESH_TARGET_OTA_STATE_COMPLETE;任一步骤失败则进入MESH_TARGET_OTA_STATE_WRITE_ERROR并上报DFU_NODE_OTA_FAIL。
Target 节点状态机
mesh_target_node_ota.h 定义了 Target 侧 flash 写入状态:
stateDiagram-v2
[*] --> IDLE
IDLE --> REQ: DFU_NODE_OTA_REQ (1)
REQ --> CHECK_FILESIZE: DFU_NODE_OTA_FILE_INFO (2)
CHECK_FILESIZE --> WRITE_DATA: DFU_NODE_OTA_DATA (3)
WRITE_DATA --> WRITE_DATA: 连续数据包 / CRC 通过
WRITE_DATA --> WRITE_ERROR: CRC 失败 / 写失败 / 序号错误
WRITE_DATA --> COMPLETE: DFU_NODE_OTA_END (4)
WRITE_ERROR --> IDLE: DFU_NODE_OTA_FAIL (5) / 复位
COMPLETE --> [*]
对应枚举(mesh_target_node_ota.h):
typedef enum {
MESH_TARGET_OTA_STATE_IDLE = 0,
MESH_TARGET_OTA_STATE_REQ,
MESH_TARGET_OTA_STATE_CHECK_FILESIZE,
MESH_TARGET_OTA_STATE_WRITE_DATA,
MESH_TARGET_OTA_STATE_COMPLETE,
MESH_TARGET_OTA_STATE_WRITE_ERROR,
} MESH_TARGET_OTA_st_e;
命令类型(mesh_target_node_ota.h):
typedef enum {
DFU_NODE_OTA_REQ = 1,
DFU_NODE_OTA_FILE_INFO,
DFU_NODE_OTA_DATA,
DFU_NODE_OTA_END,
DFU_NODE_OTA_FAIL,
} dfu_node_cmd_type_t;
设计意图:把"请求-文件信息-数据-结束-失败"五类命令显式建模,使 Target 的状态迁移完全由命令驱动、无歧义;WRITE_DATA 状态可自循环接收连续数据包,直到 END 命令收敛到 COMPLETE,任何异常统一汇入 WRITE_ERROR,避免悬挂状态。
错误码定义
Target 操作结果通过以下枚举返回给 Distributor/APP(mesh_target_node_ota.h):
| 错误码 | 值 | 含义 |
|---|---|---|
MESH_TARGET_OTA_OP_SUCC | 0 | 成功 |
MESH_TARGET_OTA_OP_NO_SPACE | 1 | flash 空间不足 |
MESH_TARGET_OTA_OP_INIT_FAIL | 2 | 初始化失败 |
MESH_TARGET_OTA_OP_WRITE_FAIL | 3 | flash 写入失败 |
MESH_TARGET_OTA_OP_CRC_FAIL | 4 | CRC 校验失败 |
MESH_TARGET_OTA_OP_MALLOC_FAIL | 5 | 内存申请失败 |
MESH_TARGET_OTA_OP_PKT_NUM_ERR | 6 | 包序号错误 |
MESH_TARGET_OTA_OP_LEN_ERR | 7 | 长度错误 |
MESH_TARGET_OTA_OP_OTHER_ERR | 8 | 其他错误 |
Configuration Options
Mesh 模型与 DFU 的配置集中在编译宏与 flash 配置规则中:
| 配置项 | 类型 | 默认/可选值 | 说明 |
|---|---|---|---|
CONFIG_MESH_MODEL | 编译宏 | SIG_MESH_DFU_DISTRIBUTOR_DEMO / SIG_MESH_DFU_TARGET_DEMO | 选择设备角色:Distributor(升级分发)或 Target(升级执行);决定 mesh_dfu 目录下哪份代码被编译 |
mesh_dist_loader_test_en | 宏 | 0 | 是否开启 loader 自测试(见 mesh_distributor_loader.c) |
mesh_dist_loader_path | 字符串 | mnt/sdfile/app/mesh | 固件 bin 在 flash 配置区中的存放路径(见 mesh_distributor_loader.c) |
OTA_READ_FLASH_SIZE | 宏 | 4096 + 16 | 每次从 flash 读取的固件块大小 |
BUFFER_SIZE_TARGET | 宏 | 4096 | Target 单包可处理的最大负载 |
CMD_HEADER_FLAG | 宏 | 0x5A | 命令帧帧头标志 |
CMD_PROTOCOL_LENGTH | 宏 | 6 | 协议头长度(Head flag/Length/Flag/OP/SN/CRC) |
POLY | 宏 | 0x1021 | CRC-16-XMODEM 多项式 |
注意:mesh_dfu_app_cmd_protocol.c 与 mesh_distributor_loader.c 整体被 #if (CONFIG_MESH_MODEL == SIG_MESH_DFU_DISTRIBUTOR_DEMO) 包裹,Target 侧文件同理,因此配置宏直接决定链接进固件的 DFU 代码路径。
API Reference
以下为 Mesh DFU 模块对外暴露的关键接口(均为 C 函数,实现在 apps/mesh/mesh_dfu 目录)。
get_mesh_dfu_adv_filter(u8 *len, u8 **data)
向广播数据末尾追加 Mesh DFU 厂商 Service Data 过滤字段(0x16 + Company ID 0x01BD)。
参数:
len(u8 *):输入为当前广播数据长度,输出为追加后的总长度;data(u8 **):指向广播数据缓冲区的指针,函数在(*data) + (*len)处写入过滤字段。
返回: 无(通过 len 输出新长度)。实现在 mesh_dfu_app_cmd_protocol.c。
clear_sn(void)
复位 SN 校验状态:将 dfu_app_cmd.valid 置为 false,使下一个到达的 SN 重新作为基准。用于升级会话开始/结束时重置防重放计数。实现在 mesh_dfu_app_cmd_protocol.c。
check_sn(opcode_state_t *op, uint8_t sn)
校验序号连续性(详见"SN 防重放校验"一节)。
参数:
op(opcode_state_t *):序号状态结构体,含prev_sn与valid;sn(uint8_t):本次接收的序号。
返回: true 表示序号合法(首次或 (prev_sn + 1) % 256 连续);false 表示重放/乱序/攻击,调用方应丢弃该帧。实现在 mesh_dfu_app_cmd_protocol.c。
crc16_xmodem(const uint8_t *data, size_t length, uint16_t crc)
计算 CRC-16-XMODEM 校验值(多项式 0x1021),Distributor 发送块与 Target 接收块共用同一实现。
参数:
data(const uint8_t *):输入数据指针;length(size_t):数据长度;crc(uint16_t):初始 CRC 值(通常为0x0000)。
返回: 计算后的 16 位 CRC。实现在 mesh_distributor_loader.c。
mesh_dis_loader_info_zone_earse(u32 fsize, u32 sclust)
擦除 flash 配置区中用于存放目标固件的区域。
参数:
fsize(u32):固件文件大小;sclust(u32):起始簇地址。
返回: true 擦除成功;false 参数非法或 sfc_erase_zone 失败。实现在 mesh_distributor_loader.c。
mesh_dist_loader_complete_cb(回调)
mesh_distributor_loader.c 中保存的 static void (*mesh_dist_loader_complete_cb)(void),在固件文件装载完成后由 loader 调用,用于通知命令协议层可以开始发送(见 mesh_distributor_loader.c)。
失败模式、边界情况与并发
重放/乱序攻击:check_sn 的严格递增策略可拒绝重放旧包;但代价是丢包会导致会话中断,需要 APP 侧重发整个会话或重新建立 SN 基准(调用 clear_sn)。
CRC 失败:Target 侧对每个数据块做 CRC-16-XMODEM 校验,失败返回 MESH_TARGET_OTA_OP_CRC_FAIL(4)并进入 WRITE_ERROR 状态,上报 DFU_NODE_OTA_FAIL——此时 Distributor 需要重新发起升级,不提供块级重传。
flash 擦写失败:mesh_dis_loader_info_zone_earse 对 fsize == 0 || sclust == 0 直接拒绝,防止空参数误擦除;sfc_erase_zone 失败返回 false,上层应终止流程。Target 写 flash 失败返回 MESH_TARGET_OTA_OP_WRITE_FAIL(3)。
内存压力:Target 侧接收使用 global_buffer(动态分配,global_buffer_size/global_buffer_used 跟踪),内存申请失败对应 MESH_TARGET_OTA_OP_MALLOC_FAIL(5);dfu_cfg_block_data[200] 为栈上固定缓冲,承载配置块。
并发/中断:mesh_dist_loader_cur_offset 声明为 volatile,因为读取进度可能被中断回调或事件循环并发访问;mesh_dist_loader_fp 为模块级文件句柄,同一时刻只允许一个升级会话,避免并发打开导致文件指针错乱。SN 状态 dfu_app_cmd 为模块级静态变量,天然串行化命令处理。
包序/长度:MESH_TARGET_OTA_OP_PKT_NUM_ERR(6)与 MESH_TARGET_OTA_OP_LEN_ERR(7)覆盖 Mesh 层分包乱序或长度不符的场景,Target 依据 DFU_NODE_OTA_DATA 内的包号/长度字段做校验。
性能与运维注意点
- 分块粒度:单次读取/发送 4096 字节(+16 字节开销),在 Mesh 低功耗广播带宽下是较保守的块大小,兼顾吞吐与丢包影响面;增大块大小可提升吞吐但会放大单包失败损失。
- CRC 计算开销:逐位软件实现,CPU 占用随块大小线性增长;如性能敏感可换用查表法,但需保证 Distributor 与 Target 两端算法一致。
- 广播过滤字段:
0x16Service Data + Company ID0x01BD用于 APP 侧设备发现,修改需与 APP 端同步,否则设备无法被识别。 - 升级中断恢复:状态机没有持久化断电续传机制,断电/复位后回到
IDLE,需重新发起完整升级。 - 日志开关:各文件通过
mesh_log.h的LOG_INFO_ENABLE/LOG_DEBUG_ENABLE/LOG_ERROR_ENABLE/LOG_CLI_ENABLE控制日志级别,排查问题时可按需打开LOG_DEBUG_ENABLE观察分包与状态迁移。
扩展点
- 新命令类型:在
dfu_node_cmd_type_t中追加枚举值,并在 Target 状态机中增加对应迁移分支,即可扩展协议(如块级重传、断点续传)。 - 新校验算法:
crc16_xmodem是唯一校验入口,替换为其他 CRC 或哈希只需改动该函数,协议帧格式不受影响。 - 新固件存放介质:
mesh_dist_loader_path与mesh_dist_loader_fp抽象了文件来源,可替换为其他介质(如外部存储)而不改命令协议层。 - 角色扩展:
CONFIG_MESH_MODEL的#if隔离使新增角色(如同时具备 Distributor 与 Target 能力的中继节点)只需新增一个宏值并复用现有源文件。