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_len和p_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数据长度难题。