ESP32 低功耗蓝牙广播中自定义厂商数据段的 MTU 协商实战

引言

在物联网嵌入式场景中,ESP32凭借其双模蓝牙和丰富外设成为热门选择。当我们需要在BLE广播中携带自定义厂商数据(如传感器读数、设备状态)时,常遇到数据长度受限问题。默认情况下,BLE广播数据包最大31字节,而厂商数据段仅能占用其中一部分。若想传输更长数据,必须理解并利用MTU协商机制。本文将基于ESP-IDF v5.x,演示如何在广播中自定义厂商数据段,并通过MTU协商扩展其长度,同时提供可运行的代码和避坑指南。

原理剖析:BLE广播与MTU协商

广播数据包结构

BLE广播信道PDU包含:前导码、访问地址、PDU头(2字节)和有效载荷。有效载荷中,广播数据由多个AD Structure组成,每个结构含长度字节、类型字节和数据。厂商数据段的AD Type为0xFF,格式为:长度(1字节)+类型(0xFF)+公司ID(2字节)+自定义数据。默认广播有效载荷上限为31字节,因此厂商数据长度最多为31-2(AD头)-2(公司ID)=27字节。

MTU协商机制

MTU(Maximum Transmission Unit)指链路层单次传输的最大数据包字节数。BLE 4.2+支持通过ATT MTU协商提升至247字节。协商发生在连接建立后,由客户端(Central)发起Exchange MTU请求,服务端(Peripheral)响应。但广播阶段不涉及连接,因此MTU协商仅影响后续连接中的GATT通信,而非广播包本身。

关键点:广播包长度受限于广播信道PDU,与MTU无关。若需在广播中发送更多数据,只能通过扩展广播(LE Advertising Extensions,BLE 5.0)实现,其最大可达255字节。但ESP32经典蓝牙和BLE 4.2仅支持传统广播,因此我们需另辟蹊径:利用连接后的GATT通知,将长数据分块传输,而广播中仅放关键短信息。

实战策略

  • 广播中:放置固定短厂商数据(如设备ID),用于快速发现和过滤。
  • 连接后:通过MTU协商提升吞吐量,使用GATT自定义服务发送长数据。

配置步骤

1. 初始化BLE并设置广播数据

在ESP-IDF中,使用esp_ble_gap_config_adv_data()配置广播数据。需构造esp_ble_adv_data_t结构体,其中manufacturer_lenp_manufacturer_data字段用于厂商数据。

2. 实现MTU协商

连接建立后,ESP32作为Peripheral时,可调用esp_ble_gattc_send_mtu_req()(客户端角色)或响应MTU请求。但更常见的是,ESP32作为Server,在收到客户端MTU请求后,通过esp_ble_gatts_set_attr_value更新属性值长度,并利用ESP_GATTS_MTU_EVT事件获取协商结果。

3. 代码示例

以下代码展示:广播中发送厂商数据(设备ID),连接后协商MTU,并通过GATT特征发送长数据。

// 广播数据定义
static uint8_t manufacturer_data[] = {0x01, 0x02, 0x03, 0x04}; // 自定义4字节
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp = false,
    .include_name = true,
    .include_txpower = false,
    .min_interval = 0x20,
    .max_interval = 0x40,
    .appearance = 0x00,
    .manufacturer_len = sizeof(manufacturer_data),
    .p_manufacturer_data = manufacturer_data,
    .service_data_len = 0,
    .p_service_data = NULL,
    .service_uuid_len = 0,
    .p_service_uuid = NULL,
    .flag = 0x06,
};

// 初始化GATT服务
static void gatts_profile_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_attr_tab(gatts_attr_tab, gatts_if, sizeof(gatts_attr_tab)/sizeof(esp_gatts_attr_db_t));
            break;
        case ESP_GATTS_CONNECT_EVT:
            // 连接后,客户端会发起MTU协商,我们无需主动发送,只需监听MTU事件
            break;
        case ESP_GATTS_MTU_EVT:
            ESP_LOGI(TAG, "MTU negotiated: %d", param->mtu.mtu);
            // 可在此调整特征值最大长度,例如更新属性值长度
            break;
        // ... 其他事件处理
    }
}

// 发送长数据(假设特征值句柄为char_handle)
void send_long_data(uint8_t *data, uint16_t len) {
    esp_ble_gatts_set_attr_value(char_handle, len, data);
    // 通知客户端,需确保len <= (mtu-3),否则需分块
    esp_ble_gatts_send_indicate(gatts_if, conn_id, char_handle, len, data, false);
}

4. 注意事项

  • MTU协商时机:MTU协商在连接后自动进行,但需客户端主动发起。若客户端不支持,则保持默认23字节。
  • 数据分块:即使MTU协商到247,单次通知最大为MTU-3(ATT头),即244字节。若数据更长,需手动分块发送。
  • 广播长度限制:传统广播最多31字节,若需更多,考虑使用扩展广播(ESP32-C3等支持BLE 5.0的芯片)。
  • 内存管理:厂商数据缓冲区需全局或静态,避免栈溢出。
  • 兼容性:部分手机BLE栈不支持MTU协商,需回退到默认长度。

完整示例代码

以下为完整可编译的ESP-IDF示例骨架(省略部分回调细节):

#include <stdio.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"

#define TAG "BLE_MTU_DEMO"

// 厂商数据(设备ID)
static uint8_t manuf_data[4] = {0xAA, 0xBB, 0xCC, 0xDD};

// GATT属性表(简化)
static esp_gatts_attr_db_t gatts_attr_tab[] = {
    // 服务声明
    [IDX_SVC] = {
        .attr_control = {.auto_rsp = ESP_GATT_AUTO_RSP},
        .att_desc = {
            .uuid_length = ESP_UUID_LEN_16,
            .uuid_p = (uint8_t *)&primary_service_uuid,
            .perm = ESP_GATT_PERM_READ,
            .max_length = ESP_GATT_MAX_ATTR_LEN,
            .length = sizeof(uint16_t),
            .value = (uint8_t *)&gatt_svc_uuid,
        }
    },
    // 特征声明(可读可写)
    [IDX_CHAR] = {
        .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 = 512, // 预留空间
            .length = 4,
            .value = (uint8_t *)&char_prop,
        }
    },
    // 特征值
    [IDX_CHAR_VAL] = {
        .attr_control = {.auto_rsp = ESP_GATT_AUTO_RSP},
        .att_desc = {
            .uuid_length = ESP_UUID_LEN_16,
            .uuid_p = (uint8_t *)&char_val_uuid,
            .perm = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
            .max_length = 512,
            .length = 4,
            .value = (uint8_t *)initial_value,
        }
    },
};

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_ADV_DATA_SET_COMPLETE_EVT:
            esp_ble_gap_start_advertising(&adv_params);
            break;
        // ... 其他
    }
}

void app_main() {
    // 初始化NVS、蓝牙等
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
        nvs_flash_erase();
        nvs_flash_init();
    }
    esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
    esp_bt_controller_init();
    esp_bt_controller_enable(ESP_BT_MODE_BTDM);
    esp_bluedroid_init();
    esp_bluedroid_enable();

    // 注册GAP和GATTS回调
    esp_ble_gap_register_callback(gap_event_handler);
    esp_ble_gatts_register_callback(gatts_profile_event_handler);
    esp_ble_gatts_app_register(0);

    // 设置广播数据
    esp_ble_gap_config_adv_data(&adv_data);
}

总结

本文澄清了广播数据与MTU的关系,指出传统广播无法通过MTU扩展长度,而需在连接后利用GATT。通过ESP32 IDF,我们实现了广播中携带厂商数据,并在连接后协商MTU以传输更大数据。实际开发中,应根据需求选择广播或连接通信,并注意分块和兼容性。希望这篇实战能帮助你解决BLE数据长度难题。