目录

蓝牙 Mesh 组网实战:nRF52 系列开发笔记

搞物联网的朋友都知道,Wi-Fi 功耗太高,LoRa 又需要网关,而蓝牙 Mesh 正好填补了中等距离、低功耗、自组网的空白。今天我们就来聊聊如何用 nRF52 系列芯片搭建一个蓝牙 Mesh 网络,让多个设备自动组网通信。

需要准备什么?

物品 型号/规格 价格
开发板 nRF52832/nRF52840 ¥25-45
下载器 J-Link OB 或 DAPLink ¥15-30
USB 转串口 CH340/CP2102 ¥5
面包板 标准尺寸 ¥10
杜邦线 公对公/母对母 ¥5
LED 模块 共阴 RGB ¥3
按键模块 轻触开关 ¥2
**总计** **¥65-100**

我手头用的是 Nordic 官方的 nRF52840 DK,但淘宝上那些 ¥25 一块的 nRF52832 最小系统板完全够用。记得选带晶振的版本(16MHz 或 32MHz),蓝牙对时钟精度要求比较高。

步骤 1:环境搭建

安装 Nordic nRF Connect SDK

Nordic 现在主推 nRF Connect SDK(基于 Zephyr RTOS),比老的 nRF5 SDK 更现代化。我们用 Docker 来搭建环境,避免污染主机:

# 拉取官方开发镜像
docker pull nordicsemiconductor/nrfconnect-toolkit

# 创建开发容器
docker run -it --rm \
  -v $(pwd)/projects:/workspace/projects \
  -v $(pwd)/output:/workspace/output \
  nordicsemiconductor/nrfconnect-toolkit \
  bash

注意事项: ⚠️ 如果用 Windows,路径要用绝对路径,比如 -v C:\projects:/workspace/projects。另外第一次拉取镜像比较慢(2GB+),耐心等它发热吧,别发光就好。

安装 VS Code 插件

在容器内或者主机上安装以下插件:

  • Nordic Semiconductor nRF Connect for VS Code
  • C/C++ (Microsoft)
  • CMake Tools

打开命令面板 (Ctrl+Shift+P),选择 nRF Connect: New Application,模板选 bluetooth_mesh

步骤 2:理解蓝牙 Mesh 架构

蓝牙 Mesh 和传统蓝牙点对点不一样,它是基于发布/订阅模型的网状网络。核心概念:

  • Element(元素):设备中的可寻址单元,一个设备可以有多个元素。比如一个智能开关可能有 3 个元素(分别控制 3 路灯光)
  • Model(模型):定义设备功能的标准接口,比如 On/Off(开关)、Level(调光)、Lightness(亮度)、Sensor(传感器)。Nordic SDK 内置了 20+ 标准模型
  • AppKey:应用层密钥,用于加密消息内容,不同 AppKey 的设备无法互相通信(隔离不同应用)
  • NetKey:网络层密钥,用于路由消息,同一网络内的设备共享 NetKey
  • Address:单播地址(0x0001-0x7FFF,每个设备唯一)、组地址(0xC000-0xFFFF,一组设备)、虚拟地址(128-bit UUID)

蓝牙 Mesh vs 其他无线技术

特性 蓝牙 Mesh Wi-Fi Zigbee LoRa
传输距离 10-50m (可中继) 30-100m 10-50m 1-10km
功耗 极低 (μA 级) 高 (mA 级) 低 极低
速率 125kbps-2Mbps 150Mbps+ 250kbps 0.3-50kbps
组网 自组网 Mesh 星型 (需路由器) Mesh/星型 星型 (需网关)
手机直连 ✅ ✅ ❌ ❌
成本 ¥25-45 ¥15-25 ¥10-20 ¥30-60
适用场景 智能家居 高带宽 工业/家居 远距离

选型建议:

  • 智能家居、办公室照明 → 蓝牙 Mesh(手机直连、低功耗)
  • 视频监控、文件传输 → Wi-Fi(高带宽)
  • 工业传感器、楼宇自动化 → Zigbee(成熟生态)
  • 农业监测、远程抄表 → LoRa(超远距离)

Mesh 网络拓扑示例

                    ┌──────────────┐
                    │  手机 App    │
                    │ (配网器)     │
                    └──────┬───────┘
                           │ 蓝牙
                           ↓
                    ┌──────────────┐
                    │   网关       │
                    │ (nRF52840)   │
                    └──────┬───────┘
                           │ Mesh 网络
         ┌─────────────────┼─────────────────┐
         ↓                 ↓                 ↓
   ┌──────────┐     ┌──────────┐     ┌──────────┐
   │ 节点 1   │────→│ 节点 2   │────→│ 节点 3   │
   │ (开关)   │ 中继 │ (中继)   │ 中继 │ (灯)     │
   │ 电池供电 │     │ 常电     │     │ 常电     │
   └──────────┘     └──────────┘     └──────────┘

   覆盖范围:约 100 米 (3 跳)
   延迟:约 100-300ms (取决于跳数)
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   Node A    │      │   Node B    │      │   Node C    │
│  (开关)     │──────│  (中继)     │──────│  (LED 灯)    │
│ Addr:0x0001 │      │ Addr:0x0002 │      │ Addr:0x0003 │
└─────────────┘      └─────────────┘      └─────────────┘
       │                    │                    │
       └────────────────────┴────────────────────┘
                    组地址:0xC001 (所有灯)

步骤 3:核心代码实现

设备配置(prj.conf)

CONFIG_BT=y
CONFIG_BT_MESH=y
CONFIG_BT_MESH_PROVISIONER=y
CONFIG_BT_MESH_GATT_PROXY=y
CONFIG_BT_MESH_RELAY=y
CONFIG_BT_MESH_FRIEND=y
CONFIG_BT_MESH_LOW_POWER=y
CONFIG_BT_MESH_IV_UPDATE_TEST=y
CONFIG_BT_MESH_ADV_INT_MS=30

原理解析: 这些配置启用了蓝牙 Mesh 的核心功能。PROVISIONER 让设备能配网新节点,RELAY 允许消息中继(扩大覆盖范围),FRIENDLOW_POWER 用于低功耗场景。

主程序(main.c)

#include 
#include 
#include 
#include 

#define LED0_PIN    0
#define LED1_PIN    1
#define LED2_PIN    2
#define LED3_PIN    3

static struct bt_mesh_model_pub gen_onoff_pub;
static u16_t net_idx;
static u16_t addr;

// GPIO 配置
static const struct gpio_dt_spec led[] = {
    GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios),
    GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios),
    GPIO_DT_SPEC_GET(DT_ALIAS(led2), gpios),
    GPIO_DT_SPEC_GET(DT_ALIAS(led3), gpios),
};

// On/Off 模型回调
static void gen_onoff_cb(struct bt_mesh_model *model,
                         struct bt_mesh_msg_ctx *ctx,
                         struct net_buf_simple *buf)
{
    u8_t onoff = net_buf_simple_pull_u8(buf);
    printk("收到 On/Off 消息:%d\n", onoff);

    // 控制 LED
    if (led[0].port) {
        gpio_pin_set_dt(&led[0], onoff);
    }

    // 发布状态更新
    struct bt_mesh_msg_ctx msg_ctx = {
        .addr = ctx->addr,
        .send_ttl = BT_MESH_TTL_DEFAULT,
    };
    bt_mesh_model_publish(model);
}

static const struct bt_mesh_model_op gen_onoff_op[] = {
    { BT_MESH_MODEL_OP_GEN_ONOFF_SET, 2, gen_onoff_cb },
    { BT_MESH_MODEL_OP_GEN_ONOFF_GET, 0, gen_onoff_cb },
    BT_MESH_MODEL_OP_END,
};

// 模型定义
struct bt_mesh_model models[] = {
    BT_MESH_MODEL_CFG_SRV,
    BT_MESH_MODEL(BT_MESH_MODEL_ID_GEN_ONOFF_SRV, gen_onoff_op, &gen_onoff_pub, NULL),
};

// 元素定义
struct bt_mesh_elem elements[] = {
    BT_MESH_ELEM(0, models, BT_MESH_MODEL_NONE),
};

// Mesh 设备结构
BT_MESH_MODEL_PUB_DEFINE(gen_onoff_pub, 2 + 4);
static struct bt_mesh_comp comp = {
    .cid = BT_COMP_ID_LINUX,
    .elem = elements,
    .elem_count = ARRAY_SIZE(elements),
};

static struct bt_mesh_prov prov = {
    .uuid = NULL,
    .output_size = 0,
    .input_size = 0,
    .output_actions = 0,
    .input_actions = 0,
};

// 配网完成回调
static void prov_complete(u16_t net_idx, u16_t addr)
{
    printk("配网完成!地址:0x%04x\n", addr);
    net_idx = net_idx;
    addr = addr;

    // 订阅组地址
    bt_mesh_model_subscribe(&models[1], 0xC001);
}

static const struct bt_mesh_prov_cb prov_cb = {
    .complete = prov_complete,
};

void main(void)
{
    int err;

    // 初始化 GPIO
    for (int i = 0; i < ARRAY_SIZE(led); i++) {
        if (!device_is_ready(led[i].port)) {
            printk("LED%d 设备未就绪\n", i);
            continue;
        }
        gpio_pin_configure_dt(&led[i], GPIO_OUTPUT_ACTIVE);
    }

    // 启用蓝牙
    err = bt_enable(NULL);
    if (err) {
        printk("蓝牙启用失败:%d\n", err);
        return;
    }

    // 配网
    err = bt_mesh_init(&prov, &prov_cb, &comp);
    if (err) {
        printk("Mesh 初始化失败:%d\n", err);
        return;
    }

    // 开始配网
    err = bt_mesh_provision(&prov, &prov_cb);
    if (err) {
        printk("配网失败:%d\n", err);
        return;
    }

    printk("蓝牙 Mesh 节点已启动,等待配网...\n");
}

原理解析: 这段代码创建了一个简单的 On/Off 服务器模型。设备启动后进入配网模式,等待配网器(手机或网关)将其加入网络。配网完成后,设备订阅组地址 0xC001,接收该组的控制消息。

步骤 4:编译与烧录

# 进入项目目录
cd projects/mesh-light

# 构建(目标板 nRF52840 DK)
west build -b nrf52840dk_nrf52840

# 烧录
west flash

如果是第三方开发板,需要修改 board/nrf52840dk_nrf52840 下的引脚配置文件,或者自己创建 board 配置。

编译输出示例:

[100%] Built target app
[100%] Generating hex file
[100%] Flashing device
Erasing page at address 0x00000000
Writing page at address 0x00000000

步骤 5:测试验证

使用 nRF Mesh 手机 App 配网

  1. 下载 nRF Mesh (iOS/Android),这是 Nordic 官方的调试工具
  2. 打开 App,授予蓝牙权限,点击右下角 "+" 添加网络
  3. 长按开发板上的 Button 1 进入配网模式(LED 快闪表示可配网)
  4. App 会自动扫描并发现设备,显示 UUID 和 RSSI 信号强度
  5. 点击 Provision,设置网络密钥(默认随机生成即可)
  6. 设置设备名称(如 "Living Room Light"),方便识别
  7. 分配单播地址(如 0x0001),每个设备地址必须唯一

配网过程日志:

[00:00] 开始扫描未配网设备...
[00:02] 发现设备:UUID=0x1234567890ABCDEF, RSSI=-45dBm
[00:03] 发送配网请求...
[00:04] 配网成功!分配地址:0x0001
[00:05] 绑定 AppKey,配置模型...
[00:06] 设备就绪,可以控制

创建组和场景

  1. 在 App 中点击 "Groups" → "+" 创建新组
  2. 组地址设为 0xC001(自定义组地址范围 0xC000-0xFFFF)
  3. 组名称设为 "All Lights"
  4. 将设备 1、2、3 都添加到这个组
  5. 点击组开关,观察所有 LED 同步响应

效果展示:

手机 App → 发送 On 命令 → 组地址 0xC001
                    ↓
        ┌───────────┼───────────┐
        ↓           ↓           ↓
    设备 1       设备 2       设备 3
   (客厅灯)    (卧室灯)    (书房灯)
   全部亮起!

多设备中继测试

蓝牙 Mesh 的核心优势是中继功能。我们来测试消息能否通过中继节点传输:

# 在三个设备上分别查看日志
# 设备 1(发送端)
[INFO] 发送 On 命令到组地址 0xC001, TTL=3

# 设备 2(中继节点)
[INFO] 收到消息,源地址 0x0001, 目标 0xC001
[INFO] 中继消息,剩余 TTL=2

# 设备 3(接收端,距离发送端较远)
[INFO] 收到组消息,执行 On 操作
[INFO] LED 状态:ON

测试技巧: 把设备 2 放在设备 1 和 3 中间,先关闭设备 2 的中继功能,观察设备 3 是否还能收到消息(应该收不到)。然后重新启用中继,消息应该能正常传输。这能直观验证 Mesh 网络的自组网能力。

常见问题排查

问题 1: 设备无法被发现

  • 原因: 配网模式未激活或蓝牙未启用
  • 解决: 检查 bt_mesh_provision() 是否调用,LED 是否快闪。用 nrfutil 查看蓝牙广播:
    nrfutil ble scan

    如果看不到广播,检查电源和晶振。有些便宜的开发板晶振焊接不良,导致蓝牙无法工作。

问题 2: 配网成功但无法控制

  • 原因: AppKey 未绑定或地址配置错误
  • 解决: 在 App 中检查设备是否绑定了正确的 AppKey,确认订阅地址匹配。打印日志查看:
    printk("订阅地址:0x%04x\n", 0xC001);

    另外检查 Model ID 是否正确,On/Off 服务器的 Model ID 是 0x1000。

问题 3: 消息延迟高或丢失

  • 原因: 中继节点不足或 TTL 设置过小
  • 解决: 增加中继节点(配置 CONFIG_BT_MESH_RELAY=y),提高 TTL 值:
    .send_ttl = 7,  // 默认 3,增加到 7

    蓝牙 Mesh 的消息跳数限制是 TTL,每经过一个中继减 1。TTL=3 意味着最多经过 2 个中继。大户型建议 TTL=7 或更高。

问题 4: 功耗过高

  • 原因: 未启用低功耗模式或广播间隔太短
  • 解决: 启用 Friend 特性,延长广播间隔:
    CONFIG_BT_MESH_ADV_INT_MS=100  # 从 30ms 增加到 100ms

    对于电池供电设备,使用 LPN(Low Power Node)模式,让 Friend 节点帮忙存储消息,定期唤醒拉取。

问题 5: 多设备同时响应冲突

  • 原因: 组内设备太多,同时回复造成信道拥塞
  • 解决: 禁用不必要的回复,使用随机延迟:
    // 只在主设备回复
    if (addr == MASTER_ADDR) {
      send_status_response();
    }
    // 或者加随机延迟
    k_msleep(rand() % 100);

问题 6: 设备重启后丢失配网信息

  • 原因: 存储配置未持久化
  • 解决: 启用 Settings 模块,自动保存配网信息:
    CONFIG_SETTINGS=y
    CONFIG_SETTINGS_FILE=y
    CONFIG_BT_MESH_SETTINGS_FILE=y

    配网信息会保存到 Flash,重启后自动恢复。

问题 7: 与某些手机兼容性差

  • 原因: 手机蓝牙栈实现差异
  • 解决: 尝试调整广播参数或使用 GATT 代理:
    CONFIG_BT_MESH_GATT_PROXY=y

    GATT 代理模式兼容性更好,但功耗略高。iPhone 用户建议开启此选项。

实战项目:智能照明系统

光说不练假把式,我们来做一个完整的智能照明系统,包含 3 个节点:

  • 节点 1:玄关开关(低功耗,电池供电)
  • 节点 2:客厅中继(常电,带 RGB 灯带)
  • 节点 3:卧室灯(常电,可调光)

硬件连接

节点 1(开关):

nRF52832          按键模块
VCC    ─────────  VCC (3.3V)
GND    ─────────  GND
P0.11  ─────────  OUT (按键信号)

节点 2(中继 +RGB 灯):

nRF52840          WS2812B 灯带
VCC    ─────────  VCC (5V, 外部供电)
GND    ─────────  GND (共地)
P0.25  ─────────  DIN (数据输入)

节点 3(可调光灯):

nRF52832          MOSFET 模块
VCC    ─────────  VCC
GND    ─────────  GND
P0.13  ─────────  PWM (亮度控制)

开关节点代码(低功耗版)

// 按键中断回调
static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins)
{
    // 唤醒设备,发送消息
    k_sem_give(&button_sem);
}

// 低功耗循环
void main(void)
{
    // 配置 GPIO 和 Mesh(同上)

    // 配置按键中断
    gpio_init_callback(&button_cb, button_pressed, BIT(11));
    gpio_add_callback(gpio_dev, &button_cb);
    gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE);

    while (1) {
        // 进入低功耗模式,等待按键唤醒
        k_sem_take(&button_sem, K_FOREVER);

        // 去抖动
        k_msleep(50);
        if (gpio_pin_get_dt(&button) == 0) {
            continue;
        }

        // 发送开关命令
        send_onoff_command(!current_state);

        // 重新进入低功耗
        printk("进入 Deep Sleep...\n");
        k_sleep(K_SECONDS(300));  // 睡 5 分钟
    }
}

功耗测试数据:

模式 电流 电池寿命 (CR2032)
广播中 8mA -
待机 (Deep Sleep) 3μA 约 5 年
每天按 10 次 平均 15μA 约 3 年

调光节点代码(PWM 控制)

// PWM 配置
static struct pwm_dt_spec pwm_led = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led0));

// 亮度级别 (0-255)
static u8_t brightness = 0;

// 处理 Level 模型消息
static void level_cb(struct bt_mesh_model *model,
                     struct bt_mesh_msg_ctx *ctx,
                     struct net_buf_simple *buf)
{
    s16_t level = (s16_t)net_buf_simple_pull_le16(buf);

    // 转换 Level 到亮度 ( -100~100 → 0~255)
    brightness = (u8_t)((level + 100) * 255 / 200);

    // 设置 PWM 占空比
    pwm_set_dt(&pwm_led, 1000, brightness * 1000 / 255);

    printk("亮度设置为:%d%%\n", brightness * 100 / 255);
}

static const struct bt_mesh_model_op gen_level_op[] = {
    { BT_MESH_MODEL_OP_GEN_LEVEL_SET, 4, level_cb },
    { BT_MESH_MODEL_OP_GEN_LEVEL_GET, 0, level_cb },
    BT_MESH_MODEL_OP_END,
};

手机控制界面

用 nRF Mesh App 创建场景:

  1. 回家模式:玄关灯 100%,客厅灯 80%,色温 3000K
  2. 观影模式:客厅灯 20%,卧室灯关闭
  3. 睡眠模式:所有灯关闭,仅保留夜灯 5%

场景切换效果:

[20:00] 触发"回家模式"
  → 玄关灯渐亮 (0→100%, 1 秒)
  → 客厅灯渐亮 (0→80%, 2 秒)
  → 色温调整到 3000K (暖黄)

[23:00] 触发"睡眠模式"
  → 所有灯渐灭 (2 秒)
  → 夜灯保持 5% 亮度

总结

蓝牙 Mesh 适合智能家居、工业传感器网络等场景。相比 Wi-Fi,它功耗更低(纽扣电池能用几年);相比 Zigbee,它可以直接用手机配网,无需额外网关。

关键要点回顾:

  1. 理解发布/订阅模型和地址分配(单播/组播/虚拟地址)
  2. 正确配置 Zephyr 的 Mesh 选项(中继/低功耗/Friend)
  3. 使用 nRF Mesh App 快速测试和调试
  4. 注意中继节点布局和低功耗配置
  5. 实际项目中考虑功耗、延迟、可靠性的平衡

扩展建议:

  • 添加传感器模型(温度、湿度、光照)
  • 实现场景联动(开门自动开灯、人来灯亮)
  • 对接 Home Assistant(通过 MQTT 桥接或自定义网关)
  • 研究蓝牙 Mesh over GATT(通过手机中转,扩展覆盖范围)
  • 尝试 ESP32 接入(ESP-IDF 支持蓝牙 Mesh,成本更低)

成本对比:

方案 单节点成本 网关需求 手机直连
Wi-Fi ¥15-25 需要路由器 ✅
Zigbee ¥10-20 需要网关 ❌
蓝牙 Mesh ¥25-45 可选 ✅

下一步可以试试把 ESP32 也加入网络(ESP-IDF 也支持蓝牙 Mesh),或者用 nRF52840 做网关,把 Mesh 数据转发到云端。我后续会写一篇《蓝牙 Mesh 对接 Home Assistant 实战》,敬请期待。

希望这篇博客文章对您有所帮助!


相关资源: