ESP32-S3 蓝牙 BLE 从零到一:广播、服务、特征,用一个智能灯的例子全讲透

ESP32-S3 蓝牙 BLE 从零到一:广播、服务、特征,用一个智能灯的例子全讲透

前言:蓝牙,比我想象中复杂得多

之前学完了 Wi-Fi 配网、HTTP、MQTT、OTA,我觉得 ESP32-S3 的网络部分已经差不多了。接下来自然就是蓝牙。

我原以为蓝牙跟 Wi-Fi 差不多------调几个 API、连上、收发数据。结果打开官方文档,直接懵了:PHY、LL、GAP、GATT、ATT、L2CAP、SMP......一堆缩写砸过来,每个都是一层协议。

但静下心看完后发现,对于应用开发者来说,真正需要关心的只有两层:GAP(管连接)和 GATT(管数据)。 其余的都被协议栈封装好了。

今天就用一个智能蓝牙灯的例子,从广播到服务到特征,一步步把 BLE 开发讲透。


一、BLE 协议栈:看起来复杂,其实我们只用顶上两层

先看一下 BLE 的分层架构:

灰色部分全部由芯片原厂(乐鑫)封装好了,我们不需要碰。 我们只需要关注:

全称 干什么的 类比
GAP Generic Access Profile 管广播、扫描、连接 "我在这里!要不要连我?"
GATT/ATT Generic Attribute Profile 管数据的读写通知 "连上了,你要什么数据?"

GAP 层:谁在广播?谁来连接?

GAP 定义了设备的角色和连接状态:

  • 广播者 → 外围设备(Peripheral):就是我们的 ESP32-S3,被动等待连接
  • 扫描者 → 中央设备(Central):就是手机,主动发起连接

GATT 层:数据怎么组织?

GATT 用三个层次来组织数据:

  • 特征数据 (Characteristic)
  • 服务 (Service)
  • 规范 (Profile)

我们假设用智能灯来类比:

GATT 概念 智能灯举例
Service(服务) 灯服务(UUID: 0x00FF)
Characteristic(特征) 灯颜色(可读)、灯开关(可写)、故障报警(可通知)
Descriptor(描述符) 通知开关(开启/关闭通知功能)

二、ESP-IDF 蓝牙协议栈的选择

ESP-IDF 支持两个蓝牙协议栈:

Bluedroid NimBLE
来源 Google(Android 系统) Apache(Mynewt 系统)
支持 经典蓝牙 + BLE 仅 BLE
内存占用 较大 更小
适用场景 需要经典蓝牙时必选 仅用 BLE 时推荐

本文用的是 Bluedroid (默认),因为文档和例程最丰富。对于用户来说底层逻辑都是一样的,API有差异而已。所有的例程都在esp-idf/examples/bluetooth/bluedroid下方

需要手动开启蓝牙和 BLE 4.2:
idf.py menuconfig

复制代码
Component config → Bluetooth → [*] Bluetooth
                             → Host (Bluedroid - Dual-mode)
                             → Bluedroid Options → [*] Enable BLE 4.2 features

你也可以直接在sdkconfig,找到对应的打开。


三、iBeacon 广播:让世界知道你的存在

什么是 iBeacon?

iBeacon 是苹果推出的基于 BLE 广播的定位协议。本质就是一个不停向外发送数据的蓝牙设备,不需要连接,任何人都能收到。

商场定位、门店推送、资产追踪......都靠它。

iBeacon 数据包结构

字段 字节范围 长度 (字节) 说明
Flags 0 1 广播标志,0x02表示LE General Discoverable Mode,0x06表示不支持BR/EDR
Length 1 1 后续厂商数据长度(不含Flags)
Type 2 1 0xFF 表示 Manufacturer Specific Data(厂商自定义数据)
Company ID 3-4 2 Apple 公司ID,固定 0x004C
iBeacon Type 5 1 固定值 0x02,标识这是 iBeacon 数据
iBeacon Length 6 1 固定值 0x15(UUID+Major+Minor+TxPower 共21字节)
UUID 7-22 16 自定义 UUID,用于唯一标识 iBeacon
Major 23-24 2 分组 ID,用于逻辑分组
Minor 25-26 2 设备 ID,用于区分不同设备
Tx Power (RSSI) 27 1 发射功率,用于距离估算(RSSI @1m)

iBeacon 实现流程

核心代码

c 复制代码
// iBeacon 数据配置
#define ESP_UUID    {0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, \
                     0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25}
#define ESP_MAJOR   10167
#define ESP_MINOR   61958

// 广播参数:不可连接,纯广播
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min     = 0x20,                              // 最小间隔 20ms
    .adv_int_max     = 0x40,                              // 最大间隔 40ms
    .adv_type        = ADV_TYPE_NONCONN_IND,              // 不可连接广播
    .own_addr_type   = BLE_ADDR_TYPE_PUBLIC,              // 公共地址
    .channel_map     = ADV_CHNL_ALL,                      // 使用所有通道
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

广播参数中几个关键字段说明:

参数 说明
adv_int_min/max 广播间隔,单位 0.625ms。0x20 = 20ms,0x40 = 40ms
adv_type 广播类型,决定是否可连接
own_addr_type 地址类型,PUBLIC 为固定地址
channel_map 广播通道,ALL = 37/38/39 三个通道

广播类型一览:

类型 说明 典型场景
ADV_TYPE_IND 可连接可扫描 需要建立连接的设备
ADV_TYPE_NONCONN_IND 不可连接 iBeacon、传感器广播
ADV_TYPE_SCAN_IND 可扫描不可连接 需要广播更多数据
ADV_TYPE_DIRECT_IND_HIGH 定向高占空比 快速重连
ADV_TYPE_DIRECT_IND_LOW 定向低占空比 省电重连

GAP 回调函数:

c 复制代码
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
        // 广播数据设置完成 → 开始广播
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(TAG, "广播启动失败");
        }
        break;
    default:
        break;
    }
}

app_main 中的初始化流程:

c 复制代码
void app_main(void)
{
    // 1. 初始化 NVS
    ESP_ERROR_CHECK(nvs_flash_init());

    // 2. 释放经典蓝牙内存(只用 BLE,省内存)
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    // 3. 初始化并启用蓝牙控制器
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    // 4. 初始化并启用 Bluedroid 协议栈
    esp_bluedroid_init();
    esp_bluedroid_enable();

    // 5. 注册 GAP 回调
    esp_ble_gap_register_callback(esp_gap_cb);

    // 6. 组装并设置 iBeacon 广播数据
    esp_ble_ibeacon_t ibeacon_adv_data;
    esp_ble_config_ibeacon_data(&vendor_config, &ibeacon_adv_data);
    esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
}

完整代码

c 复制代码
#include <stdint.h>
#include <stdio.h>

#include "esp_bt.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "esp_err.h"
#include "esp_gap_ble_api.h"
#include "esp_ibeacon_api.h"
#include "esp_log.h"
#include "nvs_flash.h"

static const char *TAG = "BLE_IBEACON";
extern esp_ble_ibeacon_vendor_t vendor_config;

static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min = 0x20,
    .adv_int_max = 0x40,
    .adv_type = ADV_TYPE_NONCONN_IND,
    .own_addr_type = BLE_ADDR_TYPE_PUBLIC,
    .channel_map = ADV_CHNL_ALL,
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
        ESP_ERROR_CHECK(esp_ble_gap_start_advertising(&ble_adv_params));
        break;

    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(TAG, "Advertising start failed: %s",
                     esp_err_to_name(param->adv_start_cmpl.status));
        } else {
            ESP_LOGI(TAG, "Advertising started");
        }
        break;

    default:
        break;
    }
}

void app_main(void)
{
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg));
    ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));

    ESP_ERROR_CHECK(esp_bluedroid_init());
    ESP_ERROR_CHECK(esp_bluedroid_enable());
    ESP_ERROR_CHECK(esp_ble_gap_register_callback(esp_gap_cb));

    esp_ble_ibeacon_t ibeacon_adv_data;
    ESP_ERROR_CHECK(esp_ble_config_ibeacon_data(&vendor_config, &ibeacon_adv_data));
    ESP_ERROR_CHECK(
        esp_ble_gap_config_adv_data_raw((uint8_t *)&ibeacon_adv_data, sizeof(ibeacon_adv_data)));
}

烧录后用手机蓝牙扫描工具就能看到这个 iBeacon 设备了。如何确认是自己的设备? 修改 UUID、Major、Minor 为自定义值即可:

c 复制代码
#define ESP_UUID  {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, \
                   0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}
#define ESP_MAJOR 0xAABB
#define ESP_MINOR 0x5566

并且将esp-idf/examples/bluetooth/bluedroid/ble/ble_ibeacon/main/esp_ibeacon_api.h

和esp-idf/examples/bluetooth/bluedroid/ble/ble_ibeacon/main/esp_ibeacon_api.c都复制到main文件夹下。

从 iBeacon 规范看,关键部分是从 1A FF 4C 00 02 15 开始:

  • FF:类型 = Manufacturer Specific Data
  • 4C 00:厂商 ID = Apple(0x004C)
  • 02 15:iBeacon 类型 & 长度标识
  • 接下来 16 字节:UUID
  • 再后面 2 字节:Major
  • 再后面 2 字节:Minor

四、自定义广播:不用 iBeacon 格式也行

iBeacon 是苹果定义的格式,如果不需要兼容 iBeacon 协议,完全可以自定义广播内容。

结构化方式

esp_ble_adv_data_t 结构体,清晰直观:

c 复制代码
static uint8_t service_uuid[16] = {
    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80,
    0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
};

static uint8_t manufacturer_data[3] = {0x11, 0x22, 0x33};

/* The length of adv data must be less than 31 bytes */
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp        = false, // 这是广播数据
    .include_name        = true, // 包含设备名称
    .include_txpower     = true,// 包含发射功率
    .min_interval        = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval        = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec
    .appearance          = 0x00,
    .manufacturer_len = sizeof(manufacturer_data),
    .p_manufacturer_data = manufacturer_data,
    .service_data_len    = 0,
    .p_service_data      = NULL,
    .service_uuid_len    = sizeof(service_uuid),
    .p_service_uuid      = service_uuid,
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

主函数

c 复制代码
#define SAMPLE_DEVICE_NAME    "HUAINING"

void app_main(void){
    .....
    ....
    ...
    esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
    esp_ble_gap_config_adv_data(&scan_rsp_data);
    esp_ble_gap_config_adv_data(&adv_data);
}

⚠️ 广播数据和扫描响应数据各自不能超过 31 字节! 如果装不下,可以把部分数据放到扫描响应包里,或者减少字段。

编译烧录后,用蓝牙工具扫描可看到:

原始字节方式

如果想完全控制每个字节:

c 复制代码
static uint8_t raw_adv_data[] = {
    0x02, ESP_BLE_AD_TYPE_FLAG, 0x06,              // 标志
    0x02, ESP_BLE_AD_TYPE_TX_PWR, 0xEB,            // 发射功率
    0x03, ESP_BLE_AD_TYPE_16SRV_CMPL, 0xFF, 0x00,  // 服务 UUID
    0x06, ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE,    // 制造商数据
          0x01, 0x02, 0x03, 0x04, 0x05
};

设置广播数据的 API 也有两套对应:

结构化方式 原始方式
esp_ble_gap_config_adv_data(&adv_data) esp_ble_gap_config_adv_data_raw(data, len)
回调:ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT 回调:ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT

五、GATT 服务:从"我在这里"到"我能干什么"

广播只是告诉别人"我存在"。但别人连上我之后,想知道"你能干什么?"------这就需要 GATT 服务。

用智能灯来理解

我要做一个蓝牙智能灯,需要提供这些功能:

功能 对应特征 UUID 属性
读取灯颜色 特征 A 0xFF01 只读
控制灯开关 特征 B 0xFF02 只写
读取/设置亮度 特征 C 0xFF03 读写
故障报警推送 特征 D 0xFF04 读写+通知

它们都属于一个灯服务,UUID 为 0x00FF。

GATT 服务创建流程

属性表:ESP-IDF 推荐的 handle_table 方式

ESP-IDF 用一个枚举 + 数组来定义整个服务结构,非常清晰:

c 复制代码
// 枚举定义每个属性的索引
enum {
    IDX_SVC,          // 服务声明
    IDX_CHAR_A,       // 特征 A 声明
    IDX_CHAR_VAL_A,   // 特征 A 值
    IDX_CHAR_B,       // 特征 B 声明
    IDX_CHAR_VAL_B,   // 特征 B 值
    IDX_CHAR_C,       // 特征 C 声明
    IDX_CHAR_VAL_C,   // 特征 C 值
    IDX_CHAR_D,       // 特征 D 声明
    IDX_CHAR_VAL_D,   // 特征 D 值
    IDX_CHAR_CFG_D,   // 特征 D 的通知描述符
    HRS_IDX_NB,       // 总数
};

uint16_t handle_table[HRS_IDX_NB];  // 运行时存储句柄

对应的属性表结构:

属性表结构

Index 描述
IDX_SVC 服务声明:UUID = 0x00FF
IDX_CHAR_A 特征 A 声明:可读
IDX_CHAR_VAL_A 特征 A 值:{0x11,0x22,0x33,0x44}
IDX_CHAR_B 特征 B 声明:可写
IDX_CHAR_VAL_B 特征 B 值:空
IDX_CHAR_C 特征 C 声明:可读写
IDX_CHAR_VAL_C 特征 C 值:{0x11,0x22,0x33,0x44}
IDX_CHAR_D 特征 D 声明:可读写 + 通知
IDX_CHAR_VAL_D 特征 D 值:{0x11,0x22,0x33,0x44}
IDX_CHAR_CFG_D 特征 D 描述符:通知开关

每种特征怎么写?逐个拆解

基础定义(所有特征共用):

c 复制代码
static const uint16_t primary_service_uuid         = ESP_GATT_UUID_PRI_SERVICE;  
// GATT特征标识 
static const uint16_t character_declaration_uuid   = ESP_GATT_UUID_CHAR_DECLARE;  
// GATT特征描述符标识
static const uint16_t character_client_config_uuid = ESP_GATT_UUID_CHAR_CLIENT_CONFIG; 

// 自定义服务的UUID
static const uint16_t GATTS_SERVICE_UUID_TEST      = 0x00FF;
// 自定义特征的UUID
static const uint16_t GATTS_CHAR_UUID_TEST_A       = 0xFF01;
static const uint16_t GATTS_CHAR_UUID_TEST_B       = 0xFF02;    
static const uint16_t GATTS_CHAR_UUID_TEST_C       = 0xFF03;    
static const uint16_t GATTS_CHAR_UUID_TEST_D       = 0xFF04;    

static const uint8_t char_prop_read          =  ESP_GATT_CHAR_PROP_BIT_READ; // 读属性
static const uint8_t char_prop_write         =  ESP_GATT_CHAR_PROP_BIT_WRITE; // 写属性
static const uint8_t char_prop_read_write    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE; // 读写属性
static const uint8_t char_prop_read_write_notify    =  ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY; // 读写通知属性

static uint8_t char_value[4]            = {0x11, 0x22, 0x33, 0x44};  // 特征值初始值
static const uint8_t char_d_ccc[2]      = {0x00, 0x00};  // 客户端配置描述符初始值 0x0000关闭通知和指示 0x0001开启通知,0x0002开启指示。

完整属性表:

c 复制代码
/* 完整的数据库描述 - 用于将属性添加到数据库 */
static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
{
    // 服务声明
    [IDX_SVC]        =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
      sizeof(uint16_t), sizeof(GATTS_SERVICE_UUID_TEST), (uint8_t *)&GATTS_SERVICE_UUID_TEST}},

    /* 特征 A 声明 */
    [IDX_CHAR_A]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read), (uint8_t *)&char_prop_read}},

    /* 特征 A 值 */
    [IDX_CHAR_VAL_A] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_A, ESP_GATT_PERM_READ, 
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 B 声明 */
    [IDX_CHAR_B]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_write), (uint8_t *)&char_prop_write}},

    /* 特征 B 值 */
    [IDX_CHAR_VAL_B] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_B, ESP_GATT_PERM_WRITE, 
      500, 0, NULL}},

    /* 特征 C 声明 */
    [IDX_CHAR_C]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write}},

    /* 特征 C 值 */
    [IDX_CHAR_VAL_C] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_C, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE, 
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 D 声明 */
    [IDX_CHAR_D]     =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
      sizeof(uint8_t), sizeof(char_prop_read_write), (uint8_t *)&char_prop_read_write_notify}},

    /* 特征 D 值 */
    [IDX_CHAR_VAL_D] =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&GATTS_CHAR_UUID_TEST_D, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE, 
      500, sizeof(char_value), (uint8_t *)char_value}},

    /* 特征 D 描述符 */
    [IDX_CHAR_CFG_D]  =
    {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
      sizeof(uint16_t), sizeof(char_d_ccc), (uint8_t *)char_d_ccc}},
};

每个属性条目的参数含义:

复制代码
{{自动响应}, {UUID长度, UUID值, 权限, 最大长度, 当前长度, 值指针}}
参数 说明
ESP_GATT_AUTO_RSP BLE 协议栈自动响应读写请求
ESP_GATT_RSP_BY_APP 需要应用程序手动调用 esp_ble_gatts_send_response()
ESP_GATT_PERM_READ 允许读
ESP_GATT_PERM_WRITE 允许写
500 最大传输长度,对应 MTU 设置

六、GATTS 事件回调:BLE 服务的灵魂

所有服务交互都通过 GATTS 事件回调驱动:

c 复制代码
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_LOGI(GATTS_TABLE_TAG, "gatts_if %d, app_id %d", gatts_if, param->reg.app_id);
            // 设置设备名称
            esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(SAMPLE_DEVICE_NAME);
            if (set_dev_name_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "set device name failed, error code = %x", set_dev_name_ret);
            }
            // 配置广播数据
            esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));
            if (raw_adv_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);
            }
            adv_config_done |= ADV_CONFIG_FLAG;
            // 配置扫描响应数据
            esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));
            if (raw_scan_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);
            }
            adv_config_done |= SCAN_RSP_CONFIG_FLAG;
            // 创建属性表
            esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
            if (create_attr_ret){
                ESP_LOGE(GATTS_TABLE_TAG, "create attr table failed, error code = %x", create_attr_ret);
            }
        }
        break;
        case ESP_GATTS_READ_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_READ_EVT");
        break;
        case ESP_GATTS_WRITE_EVT:{
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_WRITE_EVT");
            if (!param->write.is_prep){
                // 处理非准备写入事件
                ESP_LOGI(GATTS_TABLE_TAG, "prep write, handle = %d, value len = %d, value :", param->write.handle, param->write.len);
                ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->write.value, param->write.len);
                if (handle_table[IDX_CHAR_CFG_D] == param->write.handle && param->write.len == 2){
                    uint16_t descr_value = param->write.value[1]<<8 | param->write.value[0];
                    if (descr_value == 0x0001){
                        ESP_LOGI(GATTS_TABLE_TAG, "notify enable");
                    }else if (descr_value == 0x0002){
                        ESP_LOGI(GATTS_TABLE_TAG, "indicate enable");
                    }else if (descr_value == 0x0000){
                        ESP_LOGI(GATTS_TABLE_TAG, "notify/indicate disable ");
                    }
                }
            }else{
                // 处理准备写入事件,适合大数据分段写入
                ESP_LOGI(GATTS_TABLE_TAG, "prepare write, handle = %d, value len = %d", param->write.handle, param->write.len);
                ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->write.value, param->write.len);
            }
            // 如果需要响应,则发送响应
            if (param->write.need_rsp){
                esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
            }
        }
        break;
        case ESP_GATTS_EXEC_WRITE_EVT:
            // 如果客户端执行了ESP_GATTS_WRITE_EVT分段写入,再触发ESP_GATTS_EXEC_WRITE_EVT则是提交所有数据
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_EXEC_WRITE_EVT");
            if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
                //此处可以执行数据提交操作
            }
            break;
        case ESP_GATTS_MTU_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
            break;
        case ESP_GATTS_CONF_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONF_EVT, status = %d, attr_handle %d", param->conf.status, param->conf.handle);
            break;
        case ESP_GATTS_START_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "SERVICE_START_EVT, status %d, service_handle %d", param->start.status, param->start.service_handle);
            break;
        case ESP_GATTS_CONNECT_EVT:
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONNECT_EVT, conn_id = %d", param->connect.conn_id);
            g_gatts_if = gatts_if;
            gatt_conn_id = param->connect.conn_id;
            is_connect = true;
            ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, param->connect.remote_bda, 6);
            // 更新连接参数
            esp_ble_conn_update_params_t conn_params = {0};
            memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
            /* 对于 iOS 系统,请参考 Apple 官方文档关于 BLE 连接参数限制。 */
            conn_params.latency = 0;
            conn_params.max_int = 0x20;    // max_int = 0x20*1.25ms = 40ms
            conn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20ms
            conn_params.timeout = 400;    // timeout = 400*10ms = 4000ms
            // 开始向对等设备发送更新连接参数请求
            esp_ble_gap_update_conn_params(&conn_params);
            break;
        case ESP_GATTS_DISCONNECT_EVT:
            is_connect = false;
            ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_DISCONNECT_EVT, reason = 0x%x", param->disconnect.reason);
            esp_ble_gap_start_advertising(&adv_params);
            break;
        case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
            if (param->add_attr_tab.status != ESP_GATT_OK){
                ESP_LOGE(GATTS_TABLE_TAG, "create attribute table failed, error code=0x%x", param->add_attr_tab.status);
            }
            else if (param->add_attr_tab.num_handle != HRS_IDX_NB){
                ESP_LOGE(GATTS_TABLE_TAG, "create attribute table abnormally, num_handle (%d) \
                        doesn't equal to HRS_IDX_NB(%d)", param->add_attr_tab.num_handle, HRS_IDX_NB);
            }
            else {
                ESP_LOGI(GATTS_TABLE_TAG, "create attribute table successfully, the number handle = %d",param->add_attr_tab.num_handle);
                memcpy(handle_table, param->add_attr_tab.handles, sizeof(handle_table));
                esp_ble_gatts_start_service(handle_table[IDX_SVC]);
            }
            break;
        }
        case ESP_GATTS_STOP_EVT:
        case ESP_GATTS_OPEN_EVT:
        case ESP_GATTS_CANCEL_OPEN_EVT:
        case ESP_GATTS_CLOSE_EVT:
        case ESP_GATTS_LISTEN_EVT:
        case ESP_GATTS_CONGEST_EVT:
        case ESP_GATTS_UNREG_EVT:
        case ESP_GATTS_DELETE_EVT:
        default:
            break;
    }
}

七、通知功能:设备主动推送数据

通知是 BLE 的精华------服务器端主动给客户端推数据,不需要客户端轮询。

用定时器每秒推送一次通知:

c 复制代码
// 定时器回调函数
void timer_callback(void *arg) {
    // 修改特征值
    char_value[0]++;
    char_value[1]++;
    char_value[2]++;
    char_value[3]++;

    if(is_connect){
        uint8_t notify_data[15];
        for (int i = 0; i < sizeof(notify_data); ++i)
        {
            notify_data[i] = i;
        }
        ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_CONNECT_EVT, g_gatts_if = %d conn_id = %d", g_gatts_if,gatt_conn_id);

        //用于发送指示(Indicate)或通知(Notify)。通知和指示的区别在于,指示需要客户端确认,而通知不需要,也就是最后一个参数true/false。
        esp_ble_gatts_send_indicate(g_gatts_if, gatt_conn_id, handle_table[IDX_CHAR_VAL_D],
            sizeof(notify_data), notify_data, false);
    }
}

// 定时器初始化函数
static void timer_init(void)
{
    const esp_timer_create_args_t periodic_timer_args = {
        .callback = &timer_callback,
        .name = "periodic_timer"
    };

    esp_timer_handle_t periodic_timer;
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1000000)); // 1秒
}

通知 vs 指示:

通知 (Notify) 指示 (Indicate)
need_confirm false true
是否需要客户端确认 不需要 需要
可靠性 可能丢失 保证送达
速度 更快 稍慢

八、手机端测试

推荐使用 LightBlue (iOS/Android 都有)或 nRF Connect 进行测试。

连接设备后可以看到:

示例图片

显示 "Unknown Service" 是因为 0x00FF 是我们自定义的 UUID,第三方 APP 不认识。如果是自己开发的 APP,可以根据 UUID 显示具体名称。


九、完整知识结构

BLE 应用开发
广播
服务与特征
iBeacon 广播
自定义广播

结构化/原始字节
广播参数

间隔/类型/通道/过滤
服务声明

UUID 标识
只读特征

如灯颜色
只写特征

如灯开关
读写特征

如灯亮度
读写+通知特征

如故障报警
描述符

2902 通知开关


十、总结

BLE 开发的核心套路

复制代码
初始化 NVS
  ↓
释放经典蓝牙内存(省内存)
  ↓
初始化蓝牙控制器 → 启用 BLE 模式
  ↓
初始化 Bluedroid → 启用
  ↓
注册 GAP 回调(管广播和连接)
注册 GATTS 回调(管服务和数据)
  ↓
注册 GATT 应用 → 触发 REG_EVT
  ↓
创建属性表 → 启动服务 → 开始广播
  ↓
等待连接 → 处理读/写/通知

关键 API 速查

API 功能
esp_bt_controller_init/enable 初始化蓝牙控制器
esp_bluedroid_init/enable 初始化协议栈
esp_ble_gap_register_callback 注册 GAP 回调
esp_ble_gatts_register_callback 注册 GATTS 回调
esp_ble_gatts_app_register 注册 GATT 应用
esp_ble_gatts_create_attr_tab 创建属性表
esp_ble_gatts_start_service 启动服务
esp_ble_gap_start_advertising 开始广播
esp_ble_gatts_set_attr_value 更新特征值
esp_ble_gatts_send_indicate 发送通知/指示

踩坑备忘

  1. menuconfig 必须开启 BLE 4.2,否则编译报错
  2. 广播数据不能超过 31 字节,装不下就分到扫描响应包里
  3. 每个特征 = 声明 + 值(+ 可选描述符),漏了任何一个都不行
  4. 通知需要描述符配合,客户端必须先写 0x0001 到 2902 描述符才能收到通知
  5. 断开连接后要重新开始广播,否则手机找不到设备
  6. ESP_GATT_AUTO_RSP 可以省很多事,协议栈自动处理读写响应

一个类比帮你记住 GATT

把 BLE 设备想象成一个餐厅

  • 服务 (Service) = 菜单分类(中餐、西餐、饮品)
  • 特征 (Characteristic) = 具体菜品(宫保鸡丁、牛排、可乐)
  • 属性 (Attribute) = 菜品的详细信息(价格、配料、图片)
  • 描述符 (Descriptor) = 备注选项(加辣、少糖、要发票)
  • 读操作 = 看菜单
  • 写操作 = 点菜
  • 通知 = 服务员主动告诉你"您的菜好了"

BLE 的内容确实比 Wi-Fi 复杂不少,但只要抓住 GAP(管连接)和 GATT(管数据)这两个核心,其他都是细节。但是我还是觉得有些难,大家遇到不懂的问题多问问AI,写代码多用AI工具解放双手

相关推荐
三佛科技-187366133975 小时前
FT32F030F6AP7高性能32位RISC内核MCU解析(兼容STM32F030K6TP7)
stm32·单片机·嵌入式硬件
LCMICRO-133108477465 小时前
长芯微LDC90810完全P2P替代ADC128D818,是一款八通道系统监控器,专为监控复杂系统状态而设计。
stm32·单片机·嵌入式硬件·fpga开发·硬件工程·模数转换芯片adc
嵌入式老菜鸟qq1252427736 小时前
关于S2-LP休眠
单片机·嵌入式硬件·mcu·射频工程
somi76 小时前
ARM-01-硬件基础
arm开发·嵌入式硬件
weixin_462901976 小时前
ESP32 LED控制器
单片机·嵌入式硬件
observe1017 小时前
51单片机学习
嵌入式硬件·学习·51单片机
惶了个恐8 小时前
嵌入式硬件第一弹——51单片机(1)
单片机·嵌入式硬件·51单片机
电子工程师成长日记-C519 小时前
51单片机语音实时采集系统
单片机·嵌入式硬件·51单片机
csaaa20059 小时前
STM32F103 开发USB设备端点超过ENDP4以上时崩溃问题的解决
stm32·单片机·嵌入式硬件