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 知识结构图:广播与扫描全景

下面这张图,把广播和扫描的完整流程、数据流向、类型关系都串起来了。建议你多看几遍,把概念和流程对应上。

广播与扫描知识结构图 广播设备 (Advertiser) 扫描设备 (Scanner) 广播包 (31字节) 扫描请求 (主动扫描时) 扫描响应 (31字节) 广播类型 • Connectable Undirected (可连接+可扫描) • Connectable Directed (可连接+不可扫描) • Non-connectable Undirected (不可连接+可扫描) • Scannable Undirected (不可连接+可扫描) 扫描类型 • 被动扫描:只收广播包,不发请求 • 主动扫描:发扫描请求,收扫描响应 • 功耗对比:被动 < 主动 • 数据量:31字节 vs 62字节 AD Structure 数据格式 Length (1B) | AD Type (1B) | AD Data (N B) 示例:0x06 0x09 0x45 0x53 0x50 0x33 0x32 → "ESP32" 广播包+扫描响应 = 最多62字节数据

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 开发你就入门了一半。