4. 广播与扫描:广播类型、数据格式与扫描机制
好,咱们今天聊聊 BLE 里最基础、也最容易被忽视的两个动作——广播和扫描。说白了,广播就是设备在喊“我在这儿,谁要连我?”,扫描就是另一台设备在听“谁在喊?”。
我在做第一个 BLE 项目时,就栽在广播类型上。当时想让设备既能广播数据,又能被手机连接,结果配错了广播类型,手机死活搜不到。嗯,从那以后,我对广播类型就格外上心。
4.1 广播类型:四种模式,各有各的用途
ESP32 的 BLE 广播,按连接性和扫描响应能力,分成四种类型。我习惯把它们分成两组来记:
| 广播类型 | 可连接 | 可扫描 | 典型场景 |
|---|---|---|---|
| Connectable Undirected | 是 | 是 | 普通外设(心率计、温湿度计) |
| Connectable Directed | 是 | 否 | 快速重连(已知设备) |
| Non-connectable Undirected | 否 | 是 | 纯广播(信标、iBeacon) |
| Scannable Undirected | 否 | 是 | 广播数据+扫描响应(极少用) |
关键点:Non-connectable 广播不能建立连接,只能单向发数据。如果你想让手机连上你的 ESP32,千万别选这个。
我个人最常用的是 Connectable Undirected。它最通用,既能广播设备信息,又能接受连接请求。你想想看,一个传感器节点,既要告诉手机“我是谁”,又要允许手机连上来读数据,就得用这个。
4.2 广播数据格式:AD Structure 拆解
广播数据不是随便塞一堆字节就完事的。BLE 协议规定了一套严格的格式——AD Structure(Advertising Data Structure)。
每个 AD Structure 由三部分组成:
- Length(1 字节):表示后面 AD Type + AD Data 的总长度
- AD Type(1 字节):标识数据类型(比如设备名、UUID、Tx Power)
- AD Data(N 字节):具体数据内容
举个例子,假设我们要广播设备名 "ESP32":
// 广播数据:设备名 "ESP32"
// Length = 0x06 (AD Type 1字节 + "ESP32" 5字节)
// AD Type = 0x09 (Complete Local Name)
// AD Data = "ESP32" (0x45 0x53 0x50 0x33 0x32)
0x06 0x09 0x45 0x53 0x50 0x33 0x32
你看,就这么简单。但实际项目中,广播包往往包含多个 AD Structure,拼在一起。比如同时广播设备名和 UUID:
// 第一个 AD Structure:设备名
0x06 0x09 0x45 0x53 0x50 0x33 0x32
// 第二个 AD Structure:16-bit Service UUID
0x03 0x03 0xAA 0xFE
小技巧:广播包最大 31 字节。别贪心,放不下所有数据时,把核心信息放广播包,次要信息放扫描响应数据里。
4.3 扫描响应数据:广播的“第二页”
扫描响应数据(Scan Response Data)是啥?我打个比方:广播包就像名片上的名字和电话,扫描响应数据就是名片背面的详细地址和公司介绍。
主动扫描时,扫描设备会发送扫描请求,广播设备收到后,回复扫描响应数据。这个数据也是 31 字节,格式和广播数据完全一样(AD Structure)。
我曾经在一个项目中,把设备名和主要服务 UUID 放广播包,把序列号、固件版本、电池电量这些“次要但有用”的信息放扫描响应数据里。这样,手机扫描时就能快速获取设备信息,不用连接就能知道设备状态。
4.4 主动扫描 vs 被动扫描
这两个概念,很多初学者容易搞混。我直接说人话:
- 被动扫描:只听不回应。扫描设备只接收广播包,不发送任何请求。
- 主动扫描:听到广播后,发送扫描请求,获取扫描响应数据。
你想想看,被动扫描就像你在街上听别人喊话,只听不回应。主动扫描就像你听到有人喊,你走过去问“你还有啥信息?”
| 特性 | 被动扫描 | 主动扫描 |
|---|---|---|
| 功耗 | 低 | 较高(多一次收发) |
| 获取数据量 | 仅广播包(最多31字节) | 广播包+扫描响应(最多62字节) |
| 典型场景 | 信标检测、低功耗监听 | 设备发现、信息采集 |
注意:主动扫描会增加空中包数量,在多设备密集场景下可能造成碰撞。我遇到过在展会 demo 时,几十台设备同时主动扫描,结果丢包严重。后来改成被动扫描+定时主动扫描才解决。
4.5 知识结构图:广播与扫描全景
下面这张图,把广播和扫描的完整流程、数据流向、类型关系都串起来了。建议你多看几遍,把概念和流程对应上。
4.6 ESP32 实战:配置广播与扫描
光说不练假把式。下面给一段 ESP32 上配置广播的代码片段,你直接拿去改改就能用:
#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 = 0x100, // 广播间隔最小值 (约200ms)
.adv_int_max = 0x200, // 广播间隔最大值 (约400ms)
.adv_type = ADV_TYPE_IND, // Connectable Undirected
.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, // Flags
0x03, 0x03, 0xFF, 0x00, // 16-bit Service UUID
0x06, 0x09, 'E', 'S', 'P', '3', '2' // 设备名 "ESP32"
};
// 启动广播
esp_ble_gap_config_adv_data_raw(adv_data, sizeof(adv_data));
esp_ble_gap_start_advertising(&adv_params);
经验之谈:广播间隔别设太短(比如 20ms),否则功耗飙升。我一般设 100ms~500ms,兼顾响应速度和功耗。如果是信标类应用,甚至可以设到 1 秒以上。
扫描端的代码也不复杂,核心就是注册回调,处理扫描结果:
// 启动扫描
esp_ble_scan_params_t scan_params = {
.scan_type = BLE_SCAN_TYPE_ACTIVE, // 主动扫描
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.scan_filter_policy= BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = 0x50, // 扫描间隔
.scan_window = 0x30, // 扫描窗口
};
esp_ble_gap_set_scan_params(&scan_params);
esp_ble_gap_start_scanning(10); // 扫描10秒
回调里处理扫描结果时,记得解析 AD Structure。我习惯写一个解析函数,按 AD Type 提取数据,这样代码清晰又好维护。
避坑:ESP32 的 BLE 栈默认使用静态内存。如果你在回调里分配大块内存,小心堆溢出。我曾经在扫描回调里 malloc 了一个 1KB 的 buffer,结果跑了几小时就挂了。后来改成预分配+复用,问题解决。
好了,广播和扫描的核心内容就这些。记住:广播类型决定了设备的行为边界,AD Structure 是数据组织的“语法”,主动/被动扫描是功耗和功能的权衡。把这些搞清楚了,BLE 开发你就入门了一半。