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 Mbps | 125Kbps~2Mbps(BLE 5) |
| 功耗 | 高(持续传输) | 极低(突发传输,平均 μA 级) |
| 连接建立 | 约 100ms | 约 3ms |
| 典型应用 | 音频(耳机/音箱)、文件传输 | 传感器、健康设备、Beacon |
| Profile 标准 | A2DP(音频)、HFP(通话) | GATT(自定义服务)、HRS/HID 等 |
BLE 5.0(2016年)引入了两个重要新特性:LE Audio(低延迟音频,适合助听器等)和 扩展广播(Extended Advertising)(广播包最大 255 字节,旧版仅 31 字节)。ESP32 原生支持 BLE 4.2,乐鑫新款芯片(ESP32-S3等)支持 BLE 5.0。
BLE 协议栈分层架构
GATT 核心概念
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 设备通常不需要配对(用于消费电子),但某些场景(如门锁、医疗设备)需要配对加密,防止未授权访问:
① 以为没有配对就是不安全的: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。