4. 广播(Advertising)实战:配置广播包与扫描响应数据

广播,是蓝牙设备向外界说的第一句话。你想想看,一个蓝牙设备要被人发现,总得先喊一嗓子吧?这一嗓子怎么喊、喊什么内容、喊多大声,就是本章要聊的事。

我个人习惯把广播比作「名片」。你的设备在广播信道里不断散发名片,周围的主机(手机、平板、树莓派)捡到这张名片,就知道你是谁、能干什么。而扫描响应数据,相当于对方主动来问你要的「详细简历」。

4.1 广播包的结构:AD Structure

广播包不是随便塞一堆字节进去的。BLE 规范规定,广播数据由若干个 AD Structure 组成。每个 AD Structure 长这样:

字段长度说明
Length1 字节表示后面 AD Type + AD Data 的总长度
AD Type1 字节数据类型标识,比如 0x01 = Flags,0x09 = Complete Local Name
AD DataLength - 1 字节具体数据内容

说白了,这就是一个 TLV(Type-Length-Value)结构。我在项目中遇到过不少新手,自己拼广播数据时把 Length 算错了,结果手机死活扫描不到设备。嗯,这里要注意:Length 不包含自身那个字节。

4.2 广播包与扫描响应数据的区别

很多初学者搞混这两个概念。我简单解释一下:

  • 广播包(Advertising Data):设备主动发出的,31 字节上限。所有扫描设备都能收到。
  • 扫描响应数据(Scan Response Data):也是 31 字节上限。只有当主机主动发起扫描请求时,设备才回复这个包。

为什么要分两个?因为广播信道很拥挤,31 字节往往不够用。你可以把核心信息放广播包里,把补充信息(比如完整的设备名称、更多的服务 UUID)放扫描响应里。这样既节省了广播信道带宽,又保留了完整信息。

经验之谈: 我一般把设备名称和主要服务 UUID 放广播包,把制造商自定义数据、附加服务列表放扫描响应。这样大部分场景下,主机扫一次就能拿到关键信息,不需要额外交互。

4.3 Zephyr 中的广播配置 API

Zephyr 的广播配置,核心围绕 bt_le_adv_param 和 bt_le_adv_data 这两个结构体展开。先看参数结构:

struct bt_le_adv_param {
    uint8_t  id;          // 控制器 ID,默认 0
    uint32_t options;     // 选项位掩码
    uint32_t interval_min;// 最小广播间隔,单位 0.625ms
    uint32_t interval_max;// 最大广播间隔
    bt_addr_le_t *peer;   // 定向广播时的对端地址
};

options 字段里常用的几个标志:

宏定义含义
BT_LE_ADV_OPT_CONNECTABLE可连接广播
BT_LE_ADV_OPT_SCANNABLE可扫描广播(支持扫描响应)
BT_LE_ADV_OPT_ONE_TIME只广播一次
BT_LE_ADV_OPT_USE_IDENTITY使用身份地址而非随机地址

数据部分用 bt_data 结构体数组来组织:

struct bt_data {
    uint8_t type;
    uint8_t data_len;
    const uint8_t *data;
};

然后调用 bt_le_adv_start() 启动广播。这个函数签名是:

int bt_le_adv_start(const struct bt_le_adv_param *param,
                    const struct bt_data *ad, size_t ad_len,
                    const struct bt_data *sd, size_t sd_len);

ad 是广播数据数组,sd 是扫描响应数据数组。传 NULL 和 0 表示不提供对应数据。

小技巧: 如果你只需要广播包,不需要扫描响应,直接把 sd 参数传 NULL、sd_len 传 0 即可。Zephyr 内部会帮你处理好。

4.4 实战:配置一个完整的广播

下面我写一个完整的例子。这个设备会广播自己的名字叫 "ZephyrDev",同时暴露一个自定义服务 UUID,扫描响应里再带一段制造商数据。

#include <zephyr/types.h>
#include <stddef.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>

/* 自定义服务 UUID:随便编一个 16-bit UUID */
#define CUSTOM_SVC_UUID  0xFEED

static const struct bt_data ad[] = {
    /* Flags:通用可发现模式,BR/EDR 不支持 */
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    /* 完整设备名称 */
    BT_DATA(BT_DATA_NAME_COMPLETE, "ZephyrDev", 9),
    /* 16-bit 服务 UUID 列表 */
    BT_DATA_BYTES(BT_DATA_UUID16_SOME, 0xED, 0xFE),
};

static const struct bt_data sd[] = {
    /* 制造商数据:公司 ID 0x0059(Nordic),后面跟两个字节 0x01 0x02 */
    BT_DATA_BYTES(BT_DATA_MANUFACTURER_DATA, 0x59, 0x00, 0x01, 0x02),
};

void main(void)
{
    int err;

    err = bt_enable(NULL);
    if (err) {
        printk("蓝牙初始化失败: %d\n", err);
        return;
    }

    printk("蓝牙初始化成功\n");

    err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
                          sd, ARRAY_SIZE(sd));
    if (err) {
        printk("广播启动失败: %d\n", err);
        return;
    }

    printk("广播已启动,设备名: ZephyrDev\n");

    /* 主循环保持运行 */
    while (1) {
        k_sleep(K_SECONDS(1));
    }
}

这段代码里,BT_LE_ADV_CONN 是一个预定义的广播参数宏,等价于可连接、可扫描的通用广播,间隔 100ms 左右。如果你需要更精细的控制,可以自己构造 bt_le_adv_param。

注意: 广播数据总长度不能超过 31 字节,扫描响应数据也是 31 字节。我曾经见过有人把完整的设备描述塞进广播包,结果超了,Zephyr 直接返回 -EINVAL。调试了半天才发现是数据超长。

4.5 广播参数的高级配置

有时候默认的广播间隔不够用。比如你的设备需要快速被发现,或者为了省电想拉长间隔。这时候就要自己构造参数了:

static const struct bt_le_adv_param adv_param = {
    .id = BT_ID_DEFAULT,
    .options = BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_SCANNABLE,
    .interval_min = BT_GAP_ADV_FAST_INT_MIN_2,  // 约 30ms
    .interval_max = BT_GAP_ADV_FAST_INT_MAX_2,  // 约 60ms
};

err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));

Zephyr 预定义了几组广播间隔宏:

宏值(单位 0.625ms)实际间隔适用场景
BT_GAP_ADV_FAST_INT_MIN_14830ms快速可发现,高功耗
BT_GAP_ADV_FAST_INT_MAX_18050ms快速可发现
BT_GAP_ADV_FAST_INT_MIN_2160100ms默认推荐
BT_GAP_ADV_FAST_INT_MAX_2240150ms默认推荐
BT_GAP_ADV_SLOW_INT_MIN800500ms省电模式
BT_GAP_ADV_SLOW_INT_MAX16001000ms深度省电

我个人习惯在产品开发阶段用快速间隔(30~60ms),方便调试。量产时再根据功耗要求调整到 100ms 或更慢。

4.6 广播数据动态更新

有时候广播数据需要动态变化。比如设备状态变了,想更新广播里的某个字段。Zephyr 提供了 bt_le_adv_update_data() 函数:

int bt_le_adv_update_data(const struct bt_data *ad, size_t ad_len,
                          const struct bt_data *sd, size_t sd_len);

调用这个函数不需要先停止广播。我在做一个传感器项目时,就用这个接口把实时温度值塞进制造商数据字段里,手机端扫描就能看到温度变化,不用建立连接。

避坑指南: 我曾经在更新广播数据时,忘了同步更新扫描响应数据,结果手机端读到的制造商数据还是旧的。后来我养成了习惯:每次更新都同时传 ad 和 sd,哪怕 sd 没变化也传原来的指针。

4.7 广播类型的选择

BLE 广播分好几种类型,Zephyr 通过 options 字段控制:

广播类型options 设置说明
可连接广播BT_LE_ADV_OPT_CONNECTABLE主机可以发起连接,最常用
可扫描广播BT_LE_ADV_OPT_SCANNABLE主机可以扫描获取更多数据,但不能连接
不可连接广播不设置上述选项纯广播,常用于 Beacon
定向广播BT_LE_ADV_OPT_DIR_ADDR只对特定设备广播,连接速度快

你想想看,如果你的设备只是发个 Beacon 信息,不需要连接,那就用不可连接广播,省电又省带宽。如果需要被手机连接,那就用可连接广播。

4.8 本章知识结构图

下面这张图把广播配置的核心流程串起来了:

广播配置核心流程 广播配置核心流程 bt_enable() 构造 bt_data 数组 (ad / sd) 选择广播参数 bt_le_adv_param bt_le_adv_start() 广播运行中 bt_le_adv_update_data() 广播数据总长度 ≤ 31 字节,扫描响应数据总长度 ≤ 31 字节 动态更新时,ad 和 sd 建议同时传入,避免数据不一致

4.9 常见问题与调试技巧

最后分享几个我在调试广播时踩过的坑:

  • 广播启动失败返回 -EINVAL:八成是数据超长,或者 options 组合不合法。比如同时设置了可连接和不可连接,Zephyr 会直接拒绝。
  • 手机扫描不到设备:先检查 Flags 字段有没有设置 BT_LE_AD_GENERAL。没有这个标志,很多手机不会显示你的设备。
  • 扫描响应数据没生效:确认广播参数里设置了 BT_LE_ADV_OPT_SCANNABLE。不可扫描的广播不会响应扫描请求。
  • 广播间隔不准确:蓝牙协议允许控制器在一定范围内调整实际间隔,你设的 min/max 只是建议值。不要期望精确到毫秒级。

调试利器: 用 nRF Connect 或者 LightBlue 这类 App 扫描,可以直观看到广播包和扫描响应数据的每个字节。我每次调广播数据都会开着它,比看日志高效多了。

广播配置说难不难,说简单也不简单。核心就是搞清楚 31 字节的限制、AD Structure 的拼法、以及广播参数的含义。把这些基础打牢了,后面做 GATT 服务广播、iBeacon、Eddystone 都会顺手很多。