# 设备发现

[English]

本文档为低功耗蓝牙 (Bluetooth LE) 入门教程其二，旨在对低功耗蓝牙设备发现过程进行简要介绍，包括广播与扫描相关的基本概念。随后，本教程会结合 NimBLE_Beacon 例程，基于 NimBLE 主机层协议栈，对低功耗蓝牙广播的代码实现进行介绍。

## 学习目标

- 学习广播的基本概念

- 学习扫描的基本概念

- 学习 NimBLE_Beacon 例程的代码结构

广播 (Advertising) 与扫描 (Scanning) 是低功耗蓝牙设备在进入连接前在设备发现 (Device Discovery) 阶段的工作状态。下面，我们先了解与广播有关的基本概念。

## 广播的基本概念

广播是设备通过蓝牙天线，向外发送广播数据包的过程。由于广播者在广播时并不知道环境中是否存在接收方，也不知道接收方会在什么时候启动天线，所以需要周期性地发送广播数据包，直到有设备响应。在上述过程中，对于广播者来说存在以下几个问题，让我们一起来思考一下

1. 向哪里发送广播数据包？ (Where?)

2. 发送广播数据包的周期取多久？ (When?)

3. 广播数据包里包含哪些信息？ (What?)

### 向哪里发送广播数据包？

#### 蓝牙的无线电频段

第一个问题指向的是，广播数据包应发送到哪一无线电频段。这个回答由蓝牙核心规范给出，答案是 2.4 GHz ISM 频段。2.4 GHz ISM 频段是一个全球可用的免费无线电频段，不被任何国家以军事用途等理由管控，也无需向任何组织支付许可费用，因此该频段的可用性极高，且没有任何使用成本。不过，这也意味着 2.4 GHz ISM 频段非常拥挤，可能会与其他无线通信协议发生数据冲突，如 2.4 GHz WiFi。

#### 蓝牙信道

与经典蓝牙相同，蓝牙技术联盟为了解决数据冲突的问题，在低功耗蓝牙上也应用了自适应跳频技术 (Adaptive Frequency Hopping, AFH) ，该技术可以判断 RF 信道的拥挤程度，通过跳频避开拥挤的 RF 信道，以提高通信质量。不过低功耗蓝牙与经典蓝牙的不同之处在于，所使用的 2.4 GHz ISM 频段被划分为 40 个 2 MHz 带宽的射频 (Radio Frequency, RF) 信道，中心频率范围为 2402 MHz - 2480 MHz ，而经典蓝牙则是将这一频段划分为 79 个 1MHz 带宽的 RF 信道。

在蓝牙核心规范 4.2 (Bluetooth Core Specification 4.2) 中， RF 信道分为两种类型，如下

类型 | 数量 | 编号 | 作用
--- | --- | --- | ---
广播信道 (Advertising Channel) | 3 | 37-39 | 用于发送广播数据包和扫描响应数据包
数据信道 (Data Channel) | 37 | 0-36 | 用于发送数据通道数据包

广播者在广播时，会在 37-39 这三个广播信道中进行广播数据包的发送。在三个广播信道的广播数据包均发送完毕后，可以认为一次广播结束，广播者会在下一次广播时刻到来时重复上述过程。

#### 扩展广播特性

蓝牙核心规范 4.2 中，广播数据包允许搭载最多 31 字节广播数据，这无疑限制了广播的功能。为了提高广播的可用性，蓝牙 5.0 标准引入了 扩展广播 (Extended Advertising) 特性，这一特性将广播数据包分为

类型 | 简称 | 单包最大广播数据字节数 | 最大广播数据字节数
--- | --- | --- | ---
主广播数据包 (Primary Advertising Packet) | Legacy ADV | 31 | 31
扩展广播数据包 (Extended Advertising Packet) | Extended ADV | 254 | 1650

扩展广播数据包由 ADV_EXT_IND 和 AUX_ADV_IND 组成，分别在主广播信道 (Primary Advertising Channel) 和次广播信道 (Secondary Advertising Channel) 上传输。其中，主广播信道对应于信道 37-39 ，次广播信道对应于信道 0-36 。由于接收方总是在主广播信道中接收广播数据，因此发送方在发送扩展广播数据包时，应在主广播信道中发送 ADV_EXT_IND ，在次广播信道中发送 AUX_ADV_IND ，并在 ADV_EXT_IND 中指示 AUX_ADV_IND 所在的次广播信道；通过这种机制，接收方能够在接收到主广播信道的 ADV_EXT_IND 以后，根据指示到指定的次广播信道去接收 AUX_ADV_IND ，从而得到完整的扩展广播数据包。

类型 | 信道 | 作用
--- | --- | ---
主广播信道 (Primary Advertising Channel) | 37-39 | 用于传输扩展广播数据包的 ADV_EXT_IND
次广播信道 (Secondary Advertising Channel) | 0-36 | 用于传输扩展广播数据包的 AUX_ADV_IND

### 发送广播数据包的周期取多久？

#### 广播间隔

对于第二个问题，即发送广播数据包的周期怎么取，蓝牙标准中也给出了一个明确的参数定义，即广播间隔 （Advertising Interval）。广播间隔可取的范围为 20 ms 到 10.24 s ，取值步长为 0.625 ms。

广播间隔的取值决定了广播者的可发现性 （Discoverability） 以及设备功耗。当广播间隔取得太长时，广播数据包被接收方接收到的概率就会变得很低，此时广播者的可发现性就会变差。同时，广播间隔也不宜取得太短，因此频繁发送广播数据需要消耗更多的电量。所以，广播者需要在可发现性和能耗之间进行取舍，根据应用场景的需求选择最合适的广播间隔。

值得一提的是，如果在同一空间中存在两个广播间隔相同的广播者，那么有概率出现重复性的撞包 （Packet Collision） 现象，即两个广播者总是在同一时刻向同一信道发送广播数据。由于广播是一个只发不收的过程，广播者无法得知是否发生了广播撞包。为了降低上述问题的发生概率，广播者应在每一次广播事件后添加 0-10 ms 的随机时延。

### 广播数据包里包含哪些信息？

#### 广播数据包结构

对于第三个问题，即广播数据包内含有什么信息，在蓝牙核心规范 4.2 中给出了广播数据包的格式定义，如下图所示

蓝牙核心规范 4.2 广播数据包结构

看起来非常复杂，让我们来逐层分解。广播数据包的最外层包含四个部分，分别是

序号 | 名称 | 字节数 | 功能
--- | --- | --- | ---
1 | 预置码 (Preamble) | 1 | 特殊的比特序列，用于设备时钟同步
2 | 访问地址 (Access Address) | 4 | 标记广播数据包的地址
3 | 协议数据单元 (Protocol Data Unit, PDU) | 2-39 | 有效数据的存放区域
4 | 循环冗余校验和 (Cyclic Redundancy Check, CRC) | 3 | 用于循环冗余校验

广播数据包是蓝牙数据包的一种类型，由 PDU 类型决定。下面我们将对 PDU 展开详细的介绍

#### PDU

PDU 段为有效数据存放的区域，其结构如下

序号 | 名称 | 字节数
--- | --- | ---
1 | 头 (Header) | 2
2 | 有效负载 (Payload) | 0-37

#### PDU 头

PDU 头中含有较多信息，可以分为以下六个部分

序号 | 名称 | 比特位数 | 备注
--- | --- | --- | ---
1 | PDU 类型 (PDU Type) | 4 |
2 | 保留位 (Reserved for Future Use, RFU) | 1 |
3 | 通道选择位 (Channel Selection Bit, ChSel) | 1 | 标记广播者是否支持 LE Channel Selection Algorithm #2 通道选择算法
4 | 发送地址类型 (Tx Address, TxAdd) | 1 | 0/1 分别表示公共地址/随机地址
5 | 接收地址类型 (Rx Address, RxAdd) | 1 | 0/1 分别表示公共地址/随机地址
6 | 有效负载长度 (Payload Length) | 8 |

PDU 类型位反映了设备的广播行为。在蓝牙标准中，共有以下三对广播行为

- 可连接 (Connectable) 与 不可连接 (Non-connectable)

    - 是否接受其他设备的连接请求

- 可扫描 (Scannable) 与 不可扫描 (Non-scannable)

    - 是否接受其他设备的扫描请求

- 不定向 (Undirected) 与 定向 (Directed)

    - 是否发送广播数据至指定设备

上述广播行为可以组合成以下四种常见的广播类型，对应四种不同的 PDU 类型

可连接？ | 可扫描？ | 不定向？ | PDU 类型 | 作用
--- | --- | --- | --- | ---
是 | 是 | 是 | ADV_IND | 最常见的广播类型
是 | 否 | 否 | ADV_DIRECT_IND | 常用于已知设备重连
否 | 否 | 是 | ADV_NONCONN_IND | 作为信标设备，仅向外发送广播数据
否 | 是 | 是 | ADV_SCAN_IND | 作为信标设备，一般用于广播数据包长度不足的情况，此时可以通过扫描响应向外发送额外的数据

#### PDU 有效负载

PDU 有效负载也分为两部分

序号 | 名称 | 字节数 | 备注
--- | --- | --- | ---
1 | 广播地址 (Advertisement Address, AdvA) | 6 | 广播设备的 48 位蓝牙地址
2 | 广播数据 (Advertisement Data, AdvData) | 0-31 | 由若干广播数据结构 (Advertisement Data Structure) 组成

先看广播地址，即蓝牙地址，可以分为

类型 | 说明
--- | ---
公共地址 (Public Address) | 全球范围内独一无二的固定设备地址，厂商必须为此到 IEEE 组织注册并缴纳一定费用
随机地址 (Random Address) | 随机生成的地址

随机地址又根据用途分为两类

类型 | 说明
--- | ---
随机静态地址 (Random Static Address) | 可以随固件固化于设备，也可以在设备启动时随机生成，但在设备运行过程中不得变更；常作为公共地址的平替
随机私有地址 (Random Private Address) | 可在设备运行过程中周期性变更，避免被其他设备追踪

若使用随机私有地址的设备要与其他受信任的设备通信，则应使用身份解析秘钥 (Identity Resolving Key, IRK) 生成随机地址，此时其他持有相同 IRK 的设备可以解析并得到设备的真实地址。此时，随机私有地址又可以分为两类

类型 | 说明
--- | ---
可解析随机私有地址 (Resolvable Random Private Address) | 可通过 IRK 解析得到设备真实地址
不可解析随机私有地址 (Non-resolvable Random Private Address) | 完全随机的地址，仅用于防止设备被追踪，非常少用

然后看广播数据。一个广播数据结构的格式定义如下

序号 | 名称 | 字节数 | 备注
--- | --- | --- | ---
1 | 数据长度 (AD Length) | 1 |
2 | 数据类型 (AD Type) | n | 大部分数据类型占用 1 字节
3 | 数据 (AD Data) | (AD Length - n) |

### 广播流程

#### 使用公共地址进行广播

使用公共地址进行广播时，需要将 `esp_ble_adv_params_t` 成员 `own_addr_type` 设置为 `BLE_ADDR_TYPE_PUBLIC`。广播流程图如下 （点击图片放大）：

广播流程图——使用公共地址

#### 使用可解析随机私有地址进行广播

使用可解析随机私有地址进行广播时，底层协议栈在可解析随机私有地址超时时更新广播地址，默认超时设置为 15 分钟。可解析随机私有地址的超时时间可以通过 menuconfig 中的 `BT_BLE_RPA_TIMEOUT` 选项进行配置。需要将 `esp_ble_adv_params_t` 成员 `own_addr_type` 设置为 `BLE_ADDR_TYPE_RPA_PUBLIC` 或 `BLE_ADDR_TYPE_RPA_RANDOM`。广播流程图如下 （点击图片放大）：

广播流程图——使用可解析随机私有地址

备注

当使用可解析随机私有地址进行广播时，需要等待 `esp_ble_gap_config_local_privacy` 事件返回后，才能开始广播，并且需要将广播参数里的 `own_addr_type` 类型设置为 `BLE_ADDR_TYPE_RPA_PUBLIC` 或 `BLE_ADDR_TYPE_RPA_RANDOM`。

要使用 `BLE_ADDR_TYPE_RPA_RANDOM` 地址类型，必须首先通过 API `esp_ble_gap_set_rand_addr` 设置一个随机静态地址。如果使用 `BLE_ADDR_TYPE_RPA_PUBLIC`，则无需设置地址。

`BLE_ADDR_TYPE_RPA_PUBLIC` 的操作如下：控制器基于解析列表中的本地身份解析密钥 (IRK) 生成可解析随机私有地址 (RPA)。如果解析列表缺乏匹配条目，则使用公共地址。

对于 `BLE_ADDR_TYPE_RPA_RANDOM`，如果解析列表中没有匹配的条目，则使用随机静态地址。

#### 使用随机静态地址进行广播

与使用可解析随机私有地址进行广播一样，使用随机静态地址进行广播也需要将 `esp_ble_adv_params_t` 成员 `own_addr_type` 设置为 `BLE_ADDR_TYPE_RANDOM`。广播流程图如下 （点击图片放大）：

广播流程图——使用随机静态地址

### 扫描的基本概念

在广播章节，我们通过回答与广播过程相关的三个问题，了解了广播的相关基本概念。事实上，扫描过程中也存在类似的三个问题，让我们一起思考一下

1. 到什么地方去扫描？ (Where?)

2. 多久扫描一次？一次扫描多久？ (When?)

3. 扫描的过程中需要做什么？ (What?)

第一个问题已经在广播的介绍中说明了。对于蓝牙 4.2 设备来说，广播者只会在广播信道，即编号为 37-39 的三个信道发送广播数据；对于蓝牙 5.0 设备来说，如果广播者启用了扩展广播特性，则会在主广播信道发送 ADV_EXT_IND ，在次广播信道发送 AUX_ADV_IND ，并在 ADV_EXT_IND 指示 AUX_ADV_IND 所在的次广播信道。所以相应地，对于蓝牙 4.2 设备来说，扫描者只需在广播信道接收广播数据包即可。对于蓝牙 5.0 设备来说，扫描者应在主广播信道接收主广播数据包和扩展广播数据包的 ADV_EXT_IND ； 若扫描者接收到了 ADV_EXT_IND ，且 ADV_EXT_IND 指示了一个次广播信道，那么还需要到对应的次广播信道去接收 AUX_ADV_IND ，以获取完整的扩展广播数据包。

#### 扫描窗口与扫描间隔

第二个问题分别指向扫描窗口 (Scan Window) 和 扫描间隔 (Scan Interval) 概念。

- 扫描窗口：扫描者在同一个 RF 信道持续接收蓝牙数据包的持续时间，例如扫描窗口参数设定为 50 ms 时，扫描者在每个 RF 信道都会不间断地扫描 50 ms。

- 扫描间隔：相邻两个扫描窗口开始时刻之间的时间间隔，所以扫描间隔必然大于等于扫描窗口。

下图在时间轴上展示了扫描者的广播数据包接收过程，其中扫描者的扫描间隔为 100 ms ，扫描窗口为 50 ms ；广播者的广播间隔为 50 ms ，广播数据包的发送时长仅起到示意作用。可以看到，第一个扫描窗口对应 37 信道，此时扫描者恰好接收到了广播者第一次在 37 信道发送的广播数据包，以此类推。

广播与扫描时序示意图

#### 扫描请求与扫描响应

从目前的介绍来看，似乎广播过程中广播者只发不收，扫描过程中扫描者只收不发。事实上，扫描行为分为以下两种

- 被动扫描 (Passive Scanning)

    - 扫描者只接收广播数据包

- 主动扫描 (Active Scanning)

    - 扫描者在接收广播数据包以后，还向可扫描广播者发送扫描请求 (Scan Request)

可扫描广播者在接收到扫描请求之后，会广播扫描响应 (Scan Response) 数据包，以向感兴趣的扫描者发送更多的广播信息。扫描响应数据包的结构与广播数据包完全一致，区别在于 PDU 头中的 PDU 类型不同。

在广播者处于可扫描广播模式、扫描者处于主动扫描模式的场景下，广播者和扫描者的数据发送时序变得更加复杂。对于扫描者来说，在扫描窗口结束后会短暂进入 TX 模式，向外发送扫描请求，随后马上进入 RX 模式以接收可能的扫描响应；对于广播者来说，每一次广播结束后都会短暂进入 RX 模式以接收可能的扫描请求，并在接收到扫描请求后进入 TX 模式，发送扫描响应。

扫描请求的接收与扫描响应的发送

## 例程实践

在掌握了广播与扫描的相关知识以后，接下来让我们结合 NimBLE_Beacon 例程代码，学习如何使用 NimBLE 协议栈构建一个简单的 Beacon 设备，对学到的知识进行实践。

### 前提条件

1. 一块 ESP32 开发板

2. ESP-IDF 开发环境

3. 在手机上安装 nRF Connect for Mobile 应用程序

若你尚未完成 ESP-IDF 开发环境的配置，请参考 IDF 快速入门。

### 动手试试

#### 构建与烧录

本教程对应的参考例程为 NimBLE_Beacon 。

你可以通过以下命令进入例程目录

    $ cd <ESP-IDF Path>/examples/bluetooth/ble_get_started/nimble/NimBLE_Beacon

注意，请将 <ESP-IDF Path> 替换为你本地的 ESP-IDF 文件夹路径。随后，你可以通过 VSCode 或其他你常用的 IDE 打开 NimBLE_Beacon 工程。以 VSCode 为例，你可以在使用命令行进入例程目录后，通过以下命令打开工程

    $ code .

随后，在命令行中进入 ESP-IDF 环境，完成芯片设定

    $ idf.py set-target <chip-name>

你应该能看到以下命令行

    ...
    -- Configuring done
    -- Generating done
    -- Build files have been written to ...

等提示结束，这说明芯片设定完成。接下来，连接开发板至电脑，随后运行以下命令，构建固件并烧录至开发板，同时监听 ESP32 开发板的串口输出

    $ idf.py flash monitor

你应该能看到以下命令行

    ...
    main_task: Returned from app_main()

等提示结束。

#### 查看 Beacon 设备信息

打开手机上的 nRF Connect for Mobile 程序，在 SCANNER 标签页中下拉刷新，找到 NimBLE_Beacon 设备，如下图所示

找到 NimBLE Beacon 设备

若设备列表较长，建议以 NimBLE 为关键字进行设备名过滤，快速找到 NimBLE_Beacon 设备。

观察到 NimBLE Beacon 设备下带有丰富的设备信息，甚至还带有乐鑫的网址（这就是信标广告功能的体现）。点击右下角的 RAW 按钮，可以看到广播数据包的原始信息，如下

广播数据包原始信息

Details 表格即广播数据包和扫描响应数据包中的所有广播数据结构，可以整理如下

名称 | 长度 | 类型 | 原始数据 | 解析值
--- | --- | --- | --- | ---
标志位 | 2 Bytes | 0x01 | 0x06 | General Discoverable, BR/EDR Not Supported
完整设备名称 | 14 Bytes | 0x09 | 0x4E696D424C455F426561636F6E | NimBLE_Beacon
发送功率等级 | 2 Bytes | 0x0A | 0x09 | 9 dBm
设备外观 | 3 Bytes | 0x19 | 0x0002 | 通用标签
LE 角色 | 2 Bytes | 0x1C | 0x00 | 仅支持外设设备
设备地址 | 8 Bytes | 0x1B | 0x46F506BDF5F000 | F0:F5:BD:06:F5:46
URI | 17 Bytes | 0x24 | 0x172F2F6573707265737369662E636F6D | https://espressif.com

值得一提的是，前五项广播数据结构长度之和为 28 字节，此时广播数据包仅空余 3 字节，无法继续装载后续的两项广播数据结构。所以后两项广播数据结构必须装填至扫描响应数据包。

你可能还注意到，对应于设备外观的 Raw Data 为 0x0002，而代码中对 Generic Tag 的定义是 0x0200；还有，设备地址的 Raw Data 除了最后一个字节 0x00 以外，似乎与实际地址完全颠倒。这是因为，低功耗蓝牙的空中数据包遵循小端 (Little Endian) 传输的顺序，所以低字节的数据反而会在靠前的位置。

另外，注意到 nRF Connect for Mobile 程序并没有为我们提供 CONNECT 按钮以连接至此设备。这符合我们的预期，因为 Beacon 设备本来就应该是不可连接的。下面，让我们深入代码细节，看看这样的一个 Beacon 设备是怎样实现的。

## 代码详解

### 工程结构综述

NimBLE_Beacon 的根目录大致分为以下几部分

- README*.md

    - 工程的说明文档

- sdkconfig.defaults*

    - 不同芯片对应开发板的默认配置

- CMakeLists.txt

    - 用于引入 ESP-IDF 构建环境

- main

    - 工程主文件夹，含本工程的源码、头文件以及构建配置

### 程序行为综述

在深入代码细节前，我们先对程序的行为有一个宏观的认识。

第一步，我们会对程序中使用到的各个模块进行初始化，主要包括 NVS Flash、NimBLE 主机层协议栈以及 GAP 服务的初始化。

第二步，在 NimBLE 主机层协议栈与蓝牙控制器完成同步时，我们先确认蓝牙地址可用，然后发起不定向、不可连接、可扫描的广播。

之后持续处于广播状态，直到设备重启。

### 入口函数

与其他工程一样，应用程序的入口函数为 main/main.c 文件中的 app_main 函数，我们一般在这个函数中进行各模块的初始化。本例中，我们主要做以下几件事情

1. 初始化 NVS Flash 与 NimBLE 主机层协议栈

2. 初始化 GAP 服务

3. 启动 NimBLE 主机层的 FreeRTOS 线程

ESP32 的蓝牙协议栈使用 NVS Flash 存储相关配置，所以在初始化蓝牙协议栈之前，必须调用 nvs_flash_init API 以初始化 NVS Flash ，某些情况下需要调用 nvs_flash_erase API 对 NVS Flash 进行擦除后再初始化。

    void app_main(void) {
        ...

        /* NVS flash initialization */
        ret = nvs_flash_init();
        if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
            ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
            ESP_ERROR_CHECK(nvs_flash_erase());
            ret = nvs_flash_init();
        }
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "failed to initialize nvs flash, error code: %d ", ret);
            return;
        }

        ...
    }

随后，可以调用 nimble_port_init API 以初始化 NimBLE 主机层协议栈。

    void app_main(void) {
        ...

        /* NimBLE host stack initialization */
        ret = nimble_port_init();
        if (ret != ESP_OK) {
            ESP_LOGE(TAG, "failed to initialize nimble stack, error code: %d ",
                    ret);
            return;
        }

        ...
    }

然后，我们调用 gap.c 文件中定义的 gap_init 函数，初始化 GAP 服务，并设定设备名称与外观。

    void app_main(void) {
        ...

        /* GAP service initialization */
        rc = gap_init();
        if (rc != 0) {
            ESP_LOGE(TAG, "failed to initialize GAP service, error code: %d", rc);
            return;
        }

        ...
    }

接下来，设定 NimBLE 主机层协议栈的配置，这里主要涉及到一些回调函数的设定，包括协议栈重置时刻的回调、完成同步时刻的回调等，然后保存配置。

    static void nimble_host_config_init(void) {
        /* Set host callbacks */
        ble_hs_cfg.reset_cb = on_stack_reset;
        ble_hs_cfg.sync_cb = on_stack_sync;
        ble_hs_cfg.store_status_cb = ble_store_util_status_rr;

        /* Store host configuration */
        ble_store_config_init();
    }

    void app_main(void) {
        ...

        /* NimBLE host configuration initialization */
        nimble_host_config_init();

        ...
    }

最后，启动 NimBLE 主机层的 FreeRTOS 线程。

    static void nimble_host_task(void *param) {
        /* Task entry log */
        ESP_LOGI(TAG, "nimble host task has been started!");

        /* This function won't return until nimble_port_stop() is executed */
        nimble_port_run();

        /* Clean up at exit */
        vTaskDelete(NULL);
    }

    void app_main(void) {
        ...

        /* Start NimBLE host task thread and return */
        xTaskCreate(nimble_host_task, "NimBLE Host", 4*1024, NULL, 5, NULL);

        ...
    }

### 开始广播

使用 NimBLE 主机层协议栈进行应用开发时的编程模型为事件驱动编程 (Event-driven Programming)。

例如，在 NimBLE 主机层协议栈与蓝牙控制器完成同步以后，将会触发同步完成事件，调用 ble_hs_cfg.sync_cb 函数。在回调函数设定时，我们令该函数指针指向 on_stack_sync 函数，所以这是同步完成时实际被调用的函数。

在 on_stack_sync 函数中，我们调用 adv_init 函数，进行广播操作的初始化。在 adv_init 中，我们先调用 ble_hs_util_ensure_addr API ，确认设备存在可用的蓝牙地址；随后，调用 ble_hs_id_infer_auto API ，获取最优的蓝牙地址类型。

    static void on_stack_sync(void) {
        /* On stack sync, do advertising initialization */
        adv_init();
    }

    void adv_init(void) {
        ...

        /* Make sure we have proper BT identity address set */
        rc = ble_hs_util_ensure_addr(0);
        if (rc != 0) {
            ESP_LOGE(TAG, "device does not have any available bt address!");
            return;
        }

        /* Figure out BT address to use while advertising */
        rc = ble_hs_id_infer_auto(0, &own_addr_type);
        if (rc != 0) {
            ESP_LOGE(TAG, "failed to infer address type, error code: %d", rc);
            return;
        }

        ...
    }

接下来，将蓝牙地址数据从 NimBLE 协议栈的内存空间拷贝到本地的 addr_val 数组中，等待后续调用。

    void adv_init(void) {
        ...

        /* Copy device address to addr_val */
        rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
        if (rc != 0) {
            ESP_LOGE(TAG, "failed to copy device address, error code: %d", rc);
            return;
        }
        format_addr(addr_str, addr_val);
        ESP_LOGI(TAG, "device address: %s", addr_str);

        ...
    }

最后，调用 start_advertising 函数发起广播。在 start_advertising 函数中，我们先将广播标志位、完整设备名、发射功率、设备外观和 LE 角色等广播数据结构填充到广播数据包中，如下

    static void start_advertising(void) {
        /* Local variables */
        int rc = 0;
        const char *name;
        struct ble_hs_adv_fields adv_fields = {0};

        ...

        /* Set advertising flags */
        adv_fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;

        /* Set device name */
        name = ble_svc_gap_device_name();
        adv_fields.name = (uint8_t *)name;
        adv_fields.name_len = strlen(name);
        adv_fields.name_is_complete = 1;

        /* Set device tx power */
        adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
        adv_fields.tx_pwr_lvl_is_present = 1;

        /* Set device appearance */
        adv_fields.appearance = BLE_GAP_APPEARANCE_GENERIC_TAG;
        adv_fields.appearance_is_present = 1;

        /* Set device LE role */
        adv_fields.le_role = BLE_GAP_LE_ROLE_PERIPHERAL;
        adv_fields.le_role_is_present = 1;

        /* Set advertiement fields */
        rc = ble_gap_adv_set_fields(&adv_fields);
        if (rc != 0) {
            ESP_LOGE(TAG, "failed to set advertising data, error code: %d", rc);
            return;
        }

        ...
    }

ble_hs_adv_fields 结构体预定义了一些常用的广播数据类型。我们可以在完成数据设置后，通过令对应的 is_present 字段为 1 ，或将对应的长度字段 len 设定为非零值，以启用对应的广播数据结构。例如在上述代码中，我们通过 adv_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; 来配置设备发送功率，然后通过 adv_fields.tx_pwr_lvl_is_present = 1; 以启用该广播数据结构；若仅配置设备发送功率而不对相应的 is_present 字段置位，则该广播数据结构无效。同理，我们通过 adv_fields.name = (uint8_t *)name; 配置设备名，然后通过 adv_fields.name_len = strlen(name); 配置设备名的长度，从而将设备名这一广播数据结构添加到广播数据包中；若仅配置设备名而不配置设备名的长度，则该广播数据结构无效。

最后，调用 ble_gap_adv_set_fields API ，完成广播数据包的广播数据结构设定。

同理，我们可以将设备地址与 URI 填充到扫描响应数据包中，如下

    static void start_advertising(void) {
        ...

        struct ble_hs_adv_fields rsp_fields = {0};

        ...

        /* Set device address */
        rsp_fields.device_addr = addr_val;
        rsp_fields.device_addr_type = own_addr_type;
        rsp_fields.device_addr_is_present = 1;

        /* Set URI */
        rsp_fields.uri = esp_uri;
        rsp_fields.uri_len = sizeof(esp_uri);

        /* Set scan response fields */
        rc = ble_gap_adv_rsp_set_fields(&rsp_fields);
        if (rc != 0) {
            ESP_LOGE(TAG, "failed to set scan response data, error code: %d", rc);
            return;
        }

        ...
    }

最后，设置广播参数，并通过调用 ble_gap_adv_start API 发起广播。

    static void start_advertising(void) {
        ...

        struct ble_gap_adv_params adv_params = {0};

        ...

        /* Set non-connectable and general discoverable mode to be a beacon */
        adv_params.conn_mode = BLE_GAP_CONN_MODE_NON;
        adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;

        /* Start advertising */
        rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
                            NULL, NULL);
        if (rc != 0) {
            ESP_LOGE(TAG, "failed to start advertising, error code: %d", rc);
            return;
        }
        ESP_LOGI(TAG, "advertising started!");
    }

## 总结

通过本教程，你了解了广播和扫描的基本概念，并通过 NimBLE_Beacon 例程掌握了使用 NimBLE 主机层协议栈构建低功耗蓝牙 Beacon 设备的方法。

你可以尝试对例程中的数据进行修改，并在 nRF Connect for Mobile 调试工具中查看修改结果。例如，你可以尝试修改 adv_fields 或 rsp_fields 结构体，以修改被填充的广播数据结构，或者交换广播数据包和扫描响应数据包中的广播数据结构。但需要注意的一点是，广播数据包和扫描响应数据包的广播数据上限为 31 字节，若设定的广播数据结构大小超过该限值，调用 ble_gap_adv_start API 将会失败。

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。