蓝牙Mesh基于广播泛洪组网,单包仅7字节可用,配网到通信须严格按序配置

蓝牙 Mesh 与传统 BLE 的本质区别

传统 BLE 是点对点连接:手机连耳机、手环连手机,一对一,连接建立后才能通信。蓝牙 Mesh 换了一种思路:它不建立连接,直接基于广播 (flooding) 构建多对多网络。每个节点在 3 个广播信道上发送和接收消息,听到消息后如果自己是目标就处理,不是就转发(如果开启了 Relay),消息像涟漪一样扩散到整个网络。

这个设计带来了几个直接影响:

网络中的四种角色

蓝牙 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(配网器)发起。协议支持两种承载方式:

配网流程:

未配网设备发 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,        // 节点被重置回调
};

配网完成后,设备拿到了三个关键凭证

第三阶段:配置 (Configuration)

配网只是拿到了"入场券",节点还不能通信。Provisioner 需要通过 Configuration Model 进行三步配置,顺序严格不可变

① NetKey Add     → 确认网络密钥
② AppKey Add     → 分发应用层密钥
③ Model-App Bind → 将 AppKey 绑定到具体 Model
④ Subscription Add → 给 Model 设置订阅地址(可选但实际必做)

app.ccfg_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) 模型进行通信:

在 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(&param, 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_APPKEYSTATUS_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 设得很大,一条消息会被反复中继,造成广播风暴。实践中:

6. Model 回调中不要做耗时操作

Model 的消息处理回调是在协议栈的上下文中执行的,做耗时操作(如 Flash 写入、长时间计算)会阻塞协议栈处理后续消息。应该把耗时任务通过事件或队列异步处理。

总结

蓝牙 Mesh 用广播代替连接、用 flooding 代替路由,换来了多对多通信和网络自愈能力,代价是带宽和功耗。理解它的流程(初始化 → 配网 → 配置 → 发布/订阅)和约束(7 字节小包、严格配置顺序、角色互斥),是写好 Mesh 应用的前提。

CH585 的 MESH_LIB 把这些协议细节封装得很好,你主要需要关注三件事:app_mesh_config.c 里的资源配置、app.c 里的配网回调处理、以及 Model 的消息收发逻辑。对照 SDK 提供的十几个示例工程,基本可以覆盖大多数应用场景。