一、ESP32 蓝牙 BLE 协议栈概览

ESP32 的 BLE 协议栈基于 Bluedroid 和 NimBLE 两种实现。Bluedroid 是官方默认方案,功能全面但资源占用较高;NimBLE 则更轻量,适合资源受限场景。本文以 Bluedroid 为例,通过 ESP-IDF 框架进行开发。

BLE 协议栈分层如下:

  • 物理层(PHY):2.4GHz 频段,支持 1Mbps/2Mbps 速率
  • 链路层(LL):负责连接、广播、扫描等底层操作
  • 主机控制接口(HCI):连接控制器与主机
  • 逻辑链路控制与适配协议(L2CAP):数据封装与复用
  • 属性协议(ATT):定义数据交互的规则
  • 通用属性协议(GATT):基于 ATT,定义服务、特征等高级数据结构
  • 通用访问协议(GAP):管理设备发现、连接和安全性

二、广播(Advertising)开发

广播是 BLE 设备向外发送数据的过程,用于让周围设备发现自己。广播数据分为广播包(Advertising Packet)和扫描响应包(Scan Response),前者必须发送,后者可选。

2.1 广播参数配置

主要参数包括:

  • 广播间隔(adv_interval):范围 20ms~10.24s,间隔越短功耗越高
  • 广播类型:可连接非定向、可连接定向、不可连接、可扫描等
  • 广播信道:默认使用 37、38、39 三个信道

2.2 广播数据格式

广播数据由多个 AD Structure 组成,每个结构包含长度、类型和数据。常见类型有:

  • 0x01:标志位(LE 通用可发现模式等)
  • 0x02/0x03:16 位服务 UUID 列表
  • 0xFF:制造商特定数据

2.3 代码示例:配置广播

#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"

static esp_ble_adv_params_t adv_params = {
    .adv_int_min = 0x20,        // 40ms
    .adv_int_max = 0x40,        // 80ms
    .adv_type = ADV_TYPE_IND,   // 可连接非定向
    .own_addr_type = BLE_ADDR_TYPE_PUBLIC,
    .channel_map = ADV_CHNL_ALL,
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

static uint8_t adv_data[] = {
    0x02, 0x01, 0x06,                     // 标志:LE 通用可发现模式
    0x03, 0x03, 0xFF, 0x00,               // 16位服务 UUID 0x00FF
    0x05, 0x09, 'E', 'S', 'P', '3', '2'   // 完整本地名称 "ESP32"
};

void ble_adv_start(void) {
    esp_ble_gap_set_device_name("ESP32");
    esp_ble_gap_config_adv_data(sizeof(adv_data), adv_data);
    esp_ble_gap_start_advertising(&adv_params);
}

三、扫描(Scanning)开发

扫描是发现周围 BLE 设备的过程。扫描分为主动扫描和被动扫描,主动扫描会发送扫描请求以获取扫描响应数据。

3.1 扫描参数配置

  • 扫描间隔(scan_interval):两次扫描之间的间隔
  • 扫描窗口(scan_window):每次扫描持续的时间
  • 扫描类型:主动(0x01)或被动(0x00)

3.2 扫描结果处理

扫描结果通过 GAP 回调函数返回,包括设备地址、RSSI、广播数据等。需要解析广播数据以获取设备名称、服务 UUID 等信息。

3.3 代码示例:发起扫描

static esp_ble_scan_params_t scan_params = {
    .scan_type = BLE_SCAN_TYPE_ACTIVE,   // 主动扫描
    .scan_interval = 0x50,               // 50ms
    .scan_window = 0x30,                 // 30ms
    .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
    .own_addr_type = BLE_ADDR_TYPE_PUBLIC,
};

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
    switch (event) {
        case ESP_GAP_BLE_SCAN_RESULT_EVT:
            if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
                uint8_t *adv_data = param->scan_rst.ble_adv;
                uint8_t adv_len = param->scan_rst.adv_data_len;
                // 解析广播数据,提取设备名称等
                printf("Found device, RSSI: %d\n", param->scan_rst.rssi);
            }
            break;
        default:
            break;
    }
}

void ble_scan_start(void) {
    esp_ble_gap_set_scan_params(&scan_params);
    esp_ble_gap_start_scanning(10); // 扫描10秒
}

四、GATT 服务开发

GATT 定义了数据交互的结构,包括服务(Service)、特征(Characteristic)和描述符(Descriptor)。服务是一组特征的集合,特征包含值和属性,描述符用于描述特征。

4.1 GATT 服务架构

  • 服务:由 UUID 标识,如 0x180D 表示心率服务
  • 特征:包含值(Value)和属性(Properties),如读、写、通知等
  • 描述符:可选,用于配置特征,如 CCCD(客户端特征配置描述符)

4.2 创建 GATT 服务步骤

  1. 注册 GATT 回调函数
  2. 创建服务表(包括服务、特征、描述符)
  3. 启动服务
  4. 处理读写请求和通知发送

4.3 完整代码示例:自定义 GATT 服务

#include "esp_gatts_api.h"

#define GATTS_TAG "GATTS"
#define SERVICE_UUID 0x00FF
#define CHAR_UUID 0xFF01

static uint8_t char_value = 0;

static esp_gatt_srvc_id_t service_id = {
    .id = {
        .uuid = {.len = ESP_UUID_LEN_16, .uuid = {.uuid16 = SERVICE_UUID}},
        .inst_id = 0,
    },
    .is_primary = true,
};

static esp_attr_value_t char_val = {
    .attr_max_len = 1,
    .attr_len = 1,
    .attr_value = &char_value,
};

static esp_gatts_attr_db_t gatts_db[] = {
    // 服务声明
    {
        .attr_control = {.auto_rsp = ESP_GATT_AUTO_RSP},
        .att_desc = {
            .uuid_length = ESP_UUID_LEN_16,
            .uuid_p = (uint8_t *)&SERVICE_UUID,
            .perm = ESP_GATT_PERM_READ,
            .max_length = 0,
            .length = 0,
            .value = NULL,
        }
    },
    // 特征声明
    {
        .attr_control = {.auto_rsp = ESP_GATT_AUTO_RSP},
        .att_desc = {
            .uuid_length = ESP_UUID_LEN_16,
            .uuid_p = (uint8_t *)&CHAR_UUID,
            .perm = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
            .max_length = 1,
            .length = 1,
            .value = &char_value,
        }
    },
};

static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
    switch (event) {
        case ESP_GATTS_REG_EVT:
            esp_ble_gatts_create_service(gatts_if, &service_id, 2);
            break;
        case ESP_GATTS_CREATE_EVT:
            esp_ble_gatts_start_service(param->create.service_handle);
            break;
        case ESP_GATTS_WRITE_EVT:
            char_value = param->write.value[0];
            printf("Write value: %d\n", char_value);
            break;
        default:
            break;
    }
}

void gatts_init(void) {
    esp_ble_gatts_register_callback(gatts_event_handler);
    esp_ble_gatts_app_register(0);
}

五、注意事项

  • 内存管理:BLE 协议栈占用较多 RAM,在内存受限时考虑使用 NimBLE
  • 功耗优化:合理设置广播间隔和扫描窗口,避免频繁唤醒
  • 安全机制:生产环境应启用配对和加密,防止数据泄露
  • 错误处理:检查每个 API 的返回值,处理超时和异常情况
  • 多连接:ESP32 支持最多 9 个 BLE 连接,但需注意资源分配

六、总结

本文从 BLE 协议栈基础出发,详细介绍了 ESP32 的广播、扫描和 GATT 服务开发。通过代码示例,你可以快速上手并构建自己的 BLE 应用。实际项目中,还需结合具体需求调整参数和架构,并深入理解协议细节。希望这篇文章能成为你 BLE 开发路上的有力参考。