Chapter 05

蓝牙 BLE 开发

深入理解 BLE 协议栈架构,实现 GATT 服务与手机 App 的无线通信

BLE vs 经典蓝牙

蓝牙技术联盟(Bluetooth SIG)维护两套截然不同的蓝牙标准:经典蓝牙(Classic Bluetooth / BR/EDR)低功耗蓝牙(Bluetooth Low Energy / BLE)。ESP32 同时支持两者,但 IoT 设备绝大多数场景使用 BLE。

特性经典蓝牙BLE(低功耗蓝牙)
频段2.4GHz,79 个 1MHz 信道2.4GHz,40 个 2MHz 信道
数据速率1~3 Mbps125Kbps~2Mbps(BLE 5)
功耗高(持续传输)极低(突发传输,平均 μA 级)
连接建立约 100ms约 3ms
典型应用音频(耳机/音箱)、文件传输传感器、健康设备、Beacon
Profile 标准A2DP(音频)、HFP(通话)GATT(自定义服务)、HRS/HID 等
BLE 5.0 新特性

BLE 5.0(2016年)引入了两个重要新特性:LE Audio(低延迟音频,适合助听器等)和 扩展广播(Extended Advertising)(广播包最大 255 字节,旧版仅 31 字节)。ESP32 原生支持 BLE 4.2,乐鑫新款芯片(ESP32-S3等)支持 BLE 5.0。

BLE 协议栈分层架构

BLE 协议栈层次结构: 应用层(App) │ ┌───▼───────────────────────────┐ │ GATT(通用属性协议) │ ← 定义数据格式和读写操作 └───────────────────────────────┘ │ ┌───▼───────────────────────────┐ │ ATT(属性协议) │ ← Client/Server 数据交换 └───────────────────────────────┘ │ ┌───▼───────────────────────────┐ │ SMP(安全管理协议) │ ← 配对、绑定、加密 └───────────────────────────────┘ │ ┌───▼───────────────────────────┐ │ GAP(通用访问规范) │ ← 广播、扫描、连接管理 └───────────────────────────────┘ │ ┌───▼───────────────────────────┐ │ L2CAP(逻辑链路控制) │ ← 数据分段重组 └───────────────────────────────┘ │ ┌───▼───────────────────────────┐ │ LL(链路层) │ ← 物理信道访问、CRC └───────────────────────────────┘ │ ┌───▼───────────────────────────┐ │ PHY(物理层) │ ← 1M/2M/Coded 调制 └───────────────────────────────┘

GATT 核心概念

Profile(规范)
由蓝牙联盟或自定义的一套 Service 集合,定义特定功能(如心率监测 HRS Profile、电池服务 BAS Profile)。
Service(服务)
功能单元,用 128-bit UUID 标识(标准服务有 16-bit 简短 UUID)。一个 Profile 可包含多个 Service,如 HRS Profile 包含心率服务 (0x180D) 和设备信息服务 (0x180A)。
Characteristic(特征值)
实际数据载体,每个 Service 包含一个或多个 Characteristic。每个 Characteristic 有属性(Properties):READ、WRITE、NOTIFY、INDICATE 等。
Descriptor(描述符)
Characteristic 的元数据。最重要的是 CCCD(Client Characteristic Configuration Descriptor,UUID 0x2902),客户端写入 0x0001 启用 Notify,写入 0x0002 启用 Indicate。
Notify vs Indicate
Notify:服务器主动推送数据,不等待客户端确认(可能丢失)。Indicate:服务器推送后等待客户端 ATT_HANDLE_VALUE_CONFIRM 确认,更可靠但开销更大。

NimBLE vs Bluedroid

ESP-IDF 提供两套蓝牙协议栈可供选择:

NimBLE(推荐)

Apache 开源项目,代码更小(约 65KB vs Bluedroid 的 200KB+),配置更简单,ESP-IDF v4.4+ 起稳定,API 更现代。新项目推荐使用 NimBLE。

Bluedroid

来自 Android 的完整蓝牙协议栈,同时支持 BLE 和经典蓝牙(A2DP/HFP 等)。需要音频功能时必须使用 Bluedroid,内存占用更大。

在 menuconfig 中选择协议栈

# 打开配置菜单
idf.py menuconfig

# 路径:Component config → Bluetooth → Bluetooth controller → ...
# 选择 NimBLE - BLE only(仅需 BLE)
# 或   Bluedroid - Dual-mode(需要经典蓝牙/音频)

BLE 广播与扫描

BLE 设备在未连接时可以持续广播(Advertising)数据包,最大 31 字节(BLE 5 扩展广播可达 255 字节)。其他设备扫描(Scanning)时可以接收这些广播,无需建立连接。这是 iBeacon、EddyStone 等定位技术和 Mesh 网络的基础。

/* NimBLE 广播示例(BLE Beacon,无需连接) */
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"

static void ble_advertise(void)
{
    struct ble_gap_adv_params adv_params = {
        .conn_mode = BLE_GAP_CONN_MODE_NON,    // 不可连接的广播
        .disc_mode = BLE_GAP_DISC_MODE_GEN,    // 通用可发现
        .itvl_min  = 160,    // 100ms(单位 0.625ms)
        .itvl_max  = 160,
    };

    struct ble_hs_adv_fields fields = {
        .flags          = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP,
        .name           = (uint8_t *)"ESP32-Sensor",
        .name_len       = strlen("ESP32-Sensor"),
        .name_is_complete = 1,
    };
    ble_gap_adv_set_fields(&fields);
    ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
                      &adv_params, NULL, NULL);
}

心率计 HRS Profile 实现

心率服务(Heart Rate Service,UUID 0x180D)是蓝牙联盟标准化的 GATT Profile,包含心率测量特征值(0x2A37),手机健康 App 可直接识别。

#include "services/hrs/ble_svc_hrs.h"
#include "nimble/nimble_port.h"

/* Heart Rate Measurement 数据格式(第一字节为 Flags):
   Bit 0: 0=uint8, 1=uint16 心率值
   Bit 1: 传感器接触状态
   Bit 4: RR 间隔是否存在                                */

static void heart_rate_task(void *arg)
{
    uint8_t hr = 60;
    while (1) {
        /* 模拟心率变化 */
        hr = 60 + (esp_random() % 40);
        ble_svc_hrs_heart_rate_set(hr);    // 更新心率值
        ESP_LOGI("HRS", "心率: %d bpm", hr);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

static void ble_host_task(void *param)
{
    nimble_port_run();    // 阻塞运行 BLE 主机任务
    nimble_port_freertos_deinit();
}

void app_main(void)
{
    nvs_flash_init();
    nimble_port_init();

    ble_svc_gap_init();     // 初始化 GAP 服务
    ble_svc_gatt_init();    // 初始化 GATT 服务
    ble_svc_hrs_init();     // 初始化心率服务

    ble_svc_gap_device_name_set("ESP32-HeartRate");

    nimble_port_freertos_init(ble_host_task);
    xTaskCreate(heart_rate_task, "hr_task", 2048, NULL, 5, NULL);
}
手机端测试工具推荐

nRF Connect(Nordic Semiconductor 出品)是最专业的 BLE 调试 App,可以扫描设备、查看 Service/Characteristic UUID、手动读写、启用 Notify,iOS 和 Android 均有,强烈推荐。

自定义 GATT 服务完整示例

标准化 Profile(如心率 HRS)限制较多,大多数 IoT 产品需要自定义 GATT 服务。下面以一个"环境监测服务"为例,展示从 UUID 定义到数据通知的完整流程:

#include "nimble/nimble_port.h"
#include "host/ble_hs.h"
#include "host/ble_uuid.h"

/* 自定义 128-bit UUID(用 UUID 生成器生成,保证全球唯一)
 * 服务 UUID:        12345678-1234-5678-1234-56789ABCDEF0
 * 温度特征值:       12345678-1234-5678-1234-56789ABCDEF1
 * 湿度特征值:       12345678-1234-5678-1234-56789ABCDEF2
 * 控制特征值(写):   12345678-1234-5678-1234-56789ABCDEF3 */

static const ble_uuid128_t svc_uuid =
    BLE_UUID128_INIT(0xF0,0xDE,0xBC,0x9A,0x78,0x56,0x34,0x12,
                    0x78,0x56,0x34,0x12,0x78,0x56,0x34,0x12);

/* 全局句柄,用于主动通知 */
static uint16_t temp_chr_handle;
static uint16_t humi_chr_handle;
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;

/* 温度特征值读取回调 */
static int temp_read_cb(uint16_t conn_hdl, uint16_t attr_hdl,
                         struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    float temp = sensor_get_temperature();
    int16_t temp_int = (int16_t)(temp * 100);  /* 放大 100 倍,精度 0.01°C */
    return os_mbuf_append(ctxt->om, &temp_int, sizeof(temp_int));
}

/* 控制特征值写入回调(接收手机发来的命令)*/
static int ctrl_write_cb(uint16_t conn_hdl, uint16_t attr_hdl,
                          struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    uint8_t cmd;
    if (ctxt->om->om_len != 1) return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
    os_mbuf_copydata(ctxt->om, 0, 1, &cmd);
    switch (cmd) {
        case 0x01: gpio_set_level(LED_PIN, 1); break;   /* 开 LED */
        case 0x00: gpio_set_level(LED_PIN, 0); break;   /* 关 LED */
        default: return BLE_ATT_ERR_UNLIKELY;
    }
    return 0;
}

/* GATT 服务定义表(静态声明,框架自动注册)*/
static const struct ble_gatt_svc_def gatt_services[] = {
  {
    .type            = BLE_GATT_SVC_TYPE_PRIMARY,
    .uuid            = &svc_uuid.u,
    .characteristics = (struct ble_gatt_chr_def[]) {
      {
        /* 温度特征值:只读 + Notify */
        .uuid        = BLE_UUID128_DECLARE(...),   /* 温度 UUID */
        .access_cb   = temp_read_cb,
        .flags       = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
        .val_handle  = &temp_chr_handle,           /* 保存句柄,用于主动通知 */
      },
      {
        /* 控制特征值:只写(无需响应)*/
        .uuid        = BLE_UUID128_DECLARE(...),
        .access_cb   = ctrl_write_cb,
        .flags       = BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_WRITE,
      },
      { 0 }   /* 结束标记 */
    }
  },
  { 0 }  /* 服务结束标记 */
};

/* 主动 Notify:温度变化时推送给已订阅的手机 */
void ble_notify_temperature(float temp)
{
    if (conn_handle == BLE_HS_CONN_HANDLE_NONE) return;  /* 无连接,不发送 */
    int16_t temp_int = (int16_t)(temp * 100);
    struct os_mbuf *om = ble_hs_mbuf_from_flat(&temp_int, sizeof(temp_int));
    if (om) {
        /* ble_gattc_notify_custom 发送 Notify(不等确认)*/
        ble_gattc_notify_custom(conn_handle, temp_chr_handle, om);
    }
}

BLE 配对与安全

IoT 设备通常不需要配对(用于消费电子),但某些场景(如门锁、医疗设备)需要配对加密,防止未授权访问:

Just Works(无确认配对)
最简单的配对方式,双方自动生成临时密钥,无需用户确认。提供加密但不验证身份(MITM 攻击无法防御)。适合非安全敏感设备。
Passkey Entry(密钥输入)
设备显示一个 6 位数字,用户在手机上输入确认。防止 MITM 攻击,安全性更高。适合有显示屏的设备。
Numeric Comparison(数字比较)
BLE 4.2+ 支持,双方同时显示相同的 6 位数字供用户确认。安全性最高,需要双方都有显示设备。
BLE 安全的常见误区

① 以为没有配对就是不安全的:BLE 可以在没有配对(Bonding)的情况下加密通信(使用临时 LTK),未配对不意味着明文传输。
② 自定义 UUID 可以保密:BLE 广播包和 GATT 服务一旦可发现,任何扫描方都能读取 UUID,不能依靠 UUID 隐藏来提高安全性。
③ Notify 一定可靠:Notify 是"Fire and Forget",BLE 底层有 CRC 保证单包完整,但如果连接中断,通知会丢失。重要数据应使用 Indicate(有确认机制),或在应用层实现序列号+重传。

本章小结

BLE 协议栈分层结构从物理层到 GATT,每层各司其职:GAP 管理广播和连接,ATT 负责属性读写,GATT 在 ATT 之上定义 Profile/Service/Characteristic 语义。ESP-IDF 推荐使用 NimBLE(轻量,约 65KB),需要经典蓝牙/音频才用 Bluedroid。开发自定义 GATT 服务的关键步骤:定义 128-bit UUID → 声明 ble_gatt_svc_def 表 → ble_gatts_count_cfg + ble_gatts_add_svcs 注册 → 保存特征值句柄用于主动 Notify。调试工具首选 nRF Connect。