蓝牙Mesh基于广播泛洪组网,单包仅7字节可用,配网到通信须严格按序配置
蓝牙 Mesh 与传统 BLE 的本质区别
传统 BLE 是点对点连接:手机连耳机、手环连手机,一对一,连接建立后才能通信。蓝牙 Mesh 换了一种思路:它不建立连接,直接基于广播 (flooding) 构建多对多网络。每个节点在 3 个广播信道上发送和接收消息,听到消息后如果自己是目标就处理,不是就转发(如果开启了 Relay),消息像涟漪一样扩散到整个网络。
这个设计带来了几个直接影响:
- 没有连接,没有主从。任何节点可以和任何节点通信,只要它们在同一个网络里。
- 范围可扩展。两个节点距离太远?中间节点会帮你转发。
- 天然支持组控。发一条消息到一个组播地址,订阅了该地址的所有节点同时响应。
- 代价是带宽极低。广播信道速率有限,加上 flooding 带来的消息冗余,实际可用吞吐量很低。
网络中的四种角色
蓝牙 Mesh 定义了四种可选功能角色,组合使用来适应不同场景:
| 角色 | 功能 | 典型场景 |
|---|---|---|
| Relay(中继) | 接收并转发消息,扩展网络覆盖 | 所有需要多跳的节点,如灯网中的每个灯泡 |
| Proxy(代理) | 通过 GATT 连接让非 Mesh 设备(如手机)接入网络 | 网关、带手机 App 控制的设备 |
| Friend(朋友) | 为低功耗节点缓存消息,等它醒来再转发 | 常供电的灯或网关 |
| Low Power(低功耗) | 大部分时间休眠,定时唤醒向 Friend 要消息 | 电池供电的传感器、开关 |
约束:Friend 和 Low Power 是互斥的,一个节点不能同时充当两者。另外,Relay 功能会显著增加功耗,电池设备通常不开 Relay。
在 CH585 SDK 中,这些功能通过编译宏控制(注意 LPN/Friend 的注册要先于 Relay 初始化):
// app.c 中的功能初始化
#if(CONFIG_BLE_MESH_FRIEND)
friend_init_register(bt_mesh_friend_init, friend_state);
#endif
#if(CONFIG_BLE_MESH_LOW_POWER)
lpn_init_register(bt_mesh_lpn_init, lpn_state);
#endif
// ... 中间还有 cfg_set、comp_register 等步骤 ...
#if(CONFIG_BLE_MESH_RELAY)
bt_mesh_relay_init();
#endif对应的 SDK 示例工程覆盖了所有角色组合:adv_vendor(基础节点)、adv_vendor_friend(Friend)、adv_vendor_low_power(LPN)、adv_proxy(Proxy)。
四大阶段:从入网到通信
一个蓝牙 Mesh 节点的完整生命周期分为四个阶段。
第一阶段:初始化
初始化的顺序是固定的,不能随意调换。以 CH585 SDK 的 blemesh_on_sync() 为例(为可读性做了精简,保留核心调用和顺序;实际使用时务必检查返回值,参考 SDK 原版 app.c 中的完整错误处理):
void blemesh_on_sync(void)
{
mem_info_t info;
info.base_addr = MESH_MEM;
info.mem_len = ARRAY_SIZE(MESH_MEM);
// 1. Friend/LPN 功能注册(如有),位于 cfg_set 之前
#if(CONFIG_BLE_MESH_FRIEND)
friend_init_register(bt_mesh_friend_init, friend_state);
#endif
#if(CONFIG_BLE_MESH_LOW_POWER)
lpn_init_register(bt_mesh_lpn_init, lpn_state);
#endif
// 2. 设置 Mesh 全局参数(内存池、Flash、RF 参数)
GetMACAddress(MACAddr);
tmos_memcpy(dev_uuid, MACAddr, 6);
bt_mesh_cfg_set(&app_mesh_cfg, &app_dev, MACAddr, &info);
hal_rf_init();
// 3. 注册 Composition Data——告诉协议栈这个设备有什么能力
bt_mesh_comp_register(&app_comp);
// 4. 按需初始化各功能模块
#if(CONFIG_BLE_MESH_RELAY)
bt_mesh_relay_init();
#endif
#if(CONFIG_BLE_MESH_PROXY || CONFIG_BLE_MESH_PB_GATT)
// ... proxy beacon / gatts notify 等注册 ...
bt_mesh_proxy_init();
#endif
bt_mesh_prov_init(&app_prov);
// 5. 初始化协议栈各层
bt_mesh_mod_init();
bt_mesh_net_init();
bt_mesh_trans_init();
bt_mesh_beacon_init();
bt_mesh_adv_init();
// 6. 从 Flash 恢复配网信息
#if(CONFIG_BLE_MESH_SETTINGS)
bt_mesh_settings_init();
settings_load();
#endif
// 7. 判断是否已配网
if (bt_mesh_is_provisioned()) {
APP_DBG("Mesh network restored from flash");
} else {
prov_enable(); // 发送未配网 Beacon,等待 Provisioner 发现
}
}关键点:bt_mesh_cfg_set 中传入的 ble_mesh_cfg_t 结构体决定了整个协议栈的运行时资源,包括消息缓存数量、分段参数、Friend/LPN 队列大小等。这些参数配小了会丢消息,配大了浪费 RAM。
// app_mesh_config.c 中的资源配置
const ble_mesh_cfg_t app_mesh_cfg = {
.common_cfg.adv_buf_count = CONFIG_MESH_ADV_BUF_COUNT_DEF,
.net_cfg.msgcache_count = CONFIG_MESH_MSG_CACHE_DEF, // 消息去重缓存
.net_cfg.subnet_count = CONFIG_MESH_SUBNET_COUNT_DEF, // 子网数量
.net_cfg.appkey_count = CONFIG_MESH_APPKEY_COUNT_DEF, // AppKey 数量
.net_cfg.rxseg_count = CONFIG_MESH_RX_SEG_COUNT_DEF, // 接收分段数
.net_cfg.rxsdu_max = CONFIG_MESH_RX_SDU_DEF, // 最大接收 SDU
.friend_cfg.frndlpn_count = CONFIG_MESH_FRIEND_LPN_COUNT_DEF, // Friend 能服务几个 LPN
// ...
};第二阶段:配网 (Provisioning)
配网是新设备加入 Mesh 网络的唯一入口,由 Provisioner(配网器)发起。协议支持两种承载方式:
- PB-ADV:通过广播信道配网,是纯 Mesh 设备的默认方式
- PB-GATT:通过 GATT 连接配网,手机 App 配网时使用
配网流程:
未配网设备发 Beacon → Provisioner 扫描并邀请 → 建立 provisioning link
→ ECDH 交换公钥 → OOB 认证(可选:数字比对、输入密码等)
→ Provisioner 分发 NetKey + Unicast Address + IV Index
→ prov_complete 回调触发在代码中体现为:
static const struct bt_mesh_prov app_prov = {
.uuid = dev_uuid, // 设备 UUID,用于被 Provisioner 识别
.link_open = link_open, // 链路打开回调
.link_close = link_close, // 链路关闭回调
.complete = prov_complete, // 配网完成回调——此时设备正式入网
.reset = prov_reset, // 节点被重置回调
};配网完成后,设备拿到了三个关键凭证:
- NetKey:网络层加密密钥,整个子网共用
- Unicast Address:设备的唯一地址(0x0001 ~ 0x7FFF)
- IV Index:用于防重放攻击的序列号基础
第三阶段:配置 (Configuration)
配网只是拿到了"入场券",节点还不能通信。Provisioner 需要通过 Configuration Model 进行三步配置,顺序严格不可变:
① NetKey Add → 确认网络密钥
② AppKey Add → 分发应用层密钥
③ Model-App Bind → 将 AppKey 绑定到具体 Model
④ Subscription Add → 给 Model 设置订阅地址(可选但实际必做)在 app.c 的 cfg_srv_rsp_handler 中可以清楚看到这个顺序逻辑:
static void cfg_srv_rsp_handler(const cfg_srv_status_t *val)
{
if (val->cfgHdr.status) {
// 操作执行失败
APP_DBG("warning opcode 0x%02x", val->cfgHdr.opcode);
return;
}
if (val->cfgHdr.opcode == OP_APP_KEY_ADD) {
APP_DBG("App Key Added");
tmos_start_task(App_TaskID, APP_DELETE_LOCAL_NODE_EVT, APP_WAIT_ADD_APPKEY_DELAY);
}
else if (val->cfgHdr.opcode == OP_MOD_APP_BIND) {
APP_DBG("Vendor Model Binded");
tmos_start_task(App_TaskID, APP_DELETE_LOCAL_NODE_EVT, APP_WAIT_ADD_APPKEY_DELAY);
}
else if (val->cfgHdr.opcode == OP_MOD_SUB_ADD) {
APP_DBG("Vendor Model Subscription Set");
tmos_stop_task(App_TaskID, APP_DELETE_LOCAL_NODE_EVT);
}
}第四阶段:发布/订阅通信
配置完成后,节点通过发布 (Publish) / 订阅 (Subscribe) 模型进行通信:
- Server 模型向某个地址发布状态数据
- Client 模型****订阅该地址来接收数据
- 地址可以是单播、组播或虚拟地址
在 CH585 SDK 中发送一条消息:
static int vendor_model_srv_send(uint16_t addr, uint8_t *pData, uint16_t len)
{
struct send_param param = {
.app_idx = vnd_models[0].keys[0], // 使用绑定的 AppKey 加密
.addr = addr, // 目标地址(组播或单播)
.trans_cnt = 0x01, // 协议层重传次数
.period = K_MSEC(400), // 应用层发送间隔
.rand = (0), // 随机延迟
.tid = vendor_srv_tid_get(), // 事务 ID,用于去重
.send_ttl = BLE_MESH_TTL_DEFAULT, // 跳数限制
};
return vendor_message_srv_send_trans(¶m, pData, len);
}period 不能太小:协议规定重传间隔不应小于 200 + 50 × TTL ms。如果你的 TTL 是 7,那最小间隔就是 550ms。设太小会导致消息拥塞。
协议约束速查
这些是实际开发中最常碰到的硬限制:
| 约束项 | 数值 | 说明 |
|---|---|---|
| 未分段单包用户数据 | 7 字节 | SDK 标注 BLE_MESH_SDU_UNSEG_MAX = 11,减去层开销后剩 7 |
| 分段最大有效载荷 | 32 × 12 = 384 字节 | SDK 标注用户可用约 376 字节(减 8 字节头) |
| 最大 TTL | 127 | 实际默认 3~7 |
| 单播地址范围 | 0x0001 ~ 0x7FFF | 最多 32767 个节点 |
| 组播地址范围 | 0xC000 ~ 0xFEFF | 用户可配置的组播地址 |
| 固定地址 | 0xFFFC ~ 0xFFFF | all-proxies (0xFFFC)、all-friends (0xFFFD)、all-relays (0xFFFE)、all-nodes (0xFFFF) |
| 虚拟地址范围 | 0x8000 ~ 0xBFFF | 基于 Label UUID 生成的哈希地址 |
| 广播信道 | 37、38、39 | 与 BLE 广播共用 |
| 单个 Model 可绑定的 AppKey 数 | CONFIG_MESH_MOD_KEY_COUNT_DEF |
SDK 可配置 |
| 单个 Model 可订阅的组地址数 | CONFIG_MESH_MOD_GROUP_COUNT_DEF |
SDK 可配置 |
7 字节的限制意味着什么?控制开关灯只需要 1 个字节,没问题。但如果想透传一段自定义数据,几乎一定会触发分段传输。分段传输涉及 Segment Acknowledgment,延迟和丢包风险都会增加。
常见开发坑点
1. 配置顺序不能乱
NetKey → AppKey → Bind → Subscription,跳步或乱序都会收到 STATUS_INVALID_APPKEY 或 STATUS_CANNOT_BIND 错误。这不是 SDK 的设计选择,是蓝牙 Mesh 协议规范强制要求的。
2. 消息发送间隔不能太小
.period = K_MSEC(400), // 不小于 (200 + 50 * TTL) ms如果周期太快,协议栈来不及处理重传确认,会返回 EBUSY。需要高速上报的场景,蓝牙 Mesh 不适合,考虑用 BLE 直连。
3. Friend 和 LPN 互斥
一个节点不能同时启用 Friend 和 Low Power 功能。这是协议的定义:Friend 是"帮忙的人",LPN 是"被帮忙的人",一个节点不能同时是两者。
4. 配网信息要持久化
配网完成后,NetKey、AppKey、Unicast Address、IV Index、Sequence Number 等都需要写入 Flash。settings_load() 在每次启动时恢复这些信息。如果存储失败或丢失,节点只能重置后重新配网。
5. 中继风暴
在一个密集网络中,如果所有节点都开启 Relay 且 TTL 设得很大,一条消息会被反复中继,造成广播风暴。实践中:
- TTL 不要超过网络直径 + 2
- 不是所有节点都需要开 Relay
- 利用
msgcache_count配置消息缓存来过滤已处理过的消息
6. Model 回调中不要做耗时操作
Model 的消息处理回调是在协议栈的上下文中执行的,做耗时操作(如 Flash 写入、长时间计算)会阻塞协议栈处理后续消息。应该把耗时任务通过事件或队列异步处理。
总结
蓝牙 Mesh 用广播代替连接、用 flooding 代替路由,换来了多对多通信和网络自愈能力,代价是带宽和功耗。理解它的流程(初始化 → 配网 → 配置 → 发布/订阅)和约束(7 字节小包、严格配置顺序、角色互斥),是写好 Mesh 应用的前提。
CH585 的 MESH_LIB 把这些协议细节封装得很好,你主要需要关注三件事:app_mesh_config.c 里的资源配置、app.c 里的配网回调处理、以及 Model 的消息收发逻辑。对照 SDK 提供的十几个示例工程,基本可以覆盖大多数应用场景。