课程概述
广播是所有低功耗蓝牙连接的关键环节。我们需要从基础层面理解各类广播参数、参数的含义,以及如何对这些参数进行调优,从而实现你的应用目标 —— 无论是实现低功耗、提升可靠性,还是加快设备发现速度。
低功耗蓝牙的广播主要有两大用途:向周边设备广播数据;或者宣告自身的存在,等待其他设备与之建立连接。 在本课中,我们首先研究广播工作流程,之后学习不同的广播类型以及广播数据包的结构。本课程仅介绍传统广播(legacy advertising)。
在本课的实操练习部分,我们首先学习将设备配置为广播器,让设备发送广播数据包,由智能手机进行扫描;接着学习如何动态配置广播数据包的内容;最后讲解可连接广播,以及如何手动配置从机(外设)的低功耗蓝牙地址。
课程目标
- 理解传统广播流程的基础知识以及相关的各项广播参数(广播间隔、发射功率、信道以及所使用的物理层 PHY)。
- 学习广播类型(可连接与不可连接、可扫描与不可扫描等)。
- 研究低功耗蓝牙的地址类型(公共地址、随机地址、静态地址、可解析地址等)。
- 学习如何使用 nRF Connect SDK API 对低功耗蓝牙广播的各项内容进行配置。
- 学习如何在低功耗蓝牙广播中承载数据以及 UUID。
- 通过动手实操练习创建低功耗蓝牙信标(beacon)。
- 通过动手实操练习,在设备发现阶段配置外设(Peripheral)设备的广播数据包。
广播流程
广播与设备发现
当低功耗蓝牙设备处于广播状态时,它会发送广播数据包,宣告自身的存在,并可与其他设备建立连接。这些广播数据包会按照广播间隔周期性地向外发送。
定义
广播间隔:发送广播数据包的时间间隔。取值范围为 20 ms~10.24 s,步进增量为 0.625 ms。
广播间隔越小,广播数据包的发送频率就越高,相应地功耗也就越大。因此这里需要在功耗与扫描设备接收广播器广播包的速度(通常称为可发现性)之间做出权衡。 为避免数据包冲突,每发送一个广播数据包之前都会增加一段 0‑10 ms 的随机延时。这样可以确保具有相同广播间隔的设备不会持续发生广播数据包冲突。
广播信道
低功耗蓝牙设备通过 40 个不同的频率信道进行通信。这些信道分为 3 个主要广播信道和 37 个辅助数据信道。 主要广播信道是专门主要用于广播的信道。辅助信道有时也可以用于广播,但主要用于建立连接之后的数据传输。本课程将仅围绕主要广播信道展开学习。

为保证一定程度的冗余性,广播数据包会在全部三条主要广播信道(37、38、39 信道)上进行发送。与此同时,扫描设备也会扫描这三条信道,搜寻广播设备。
由于广播数据包对于建立连接至关重要,主要广播信道是经过审慎选择的。37、38 和 39 信道虽然编号是连续的,但实际上并不是相邻信道,如上图所示。这三条信道之间留有间隔,用以避免邻频干扰。此外,这三条信道受到 ISM 频段内其他技术(例如 Wi‑Fi)带来的噪声干扰也最小。
扫描间隔与扫描窗口
与广播间隔类似,扫描间隔是指扫描设备多久启动一次扫描来搜寻广播数据包。扫描窗口则是扫描设备用于接收数据包的扫描持续时间;实际上它代表了在每一个扫描间隔周期内,设备处于扫描状态和非扫描状态之间的占空比。
定义
扫描间隔:设备搜寻广播数据包的时间间隔。
扫描窗口:在每一次扫描间隔内,设备执行数据包扫描所持续的时间。
二者的取值范围均为 2.5 ms~10.24 s,步进增量为 0.625 ms。
由于设备会在三条不同的广播信道上发送广播,扫描设备将轮换扫描各个广播信道,即在每一个扫描间隔结束之后切换信道。

广播设备(外设)与扫描设备(主机)示例
较短的广播间隔可以缩短设备发现时间,但会增加功耗。同样,扫描窗口与扫描间隔的比值越大,设备发现时间越短,但功耗也会随之上升。 连接建立耗时与功耗之间始终需要做出权衡;不过如果你可以同时控制扫描设备和外设,则可以选择相互适配的间隔参数。 请注意:总体而言,扫描所消耗的电量远大于广播,因此扫描设备(主机)通常是电池容量更大的那台设备。
扫描请求与扫描响应
当外设正在进行广播时,主机还可以选择向外设发送扫描请求,以此获取广播数据包中没有包含的额外信息。 如果扫描请求被接收,外设就会回复一份所谓的扫描响应,该响应同样通过三条主要广播信道进行发送。
定义
扫描请求:由主机设备发送给外设设备的报文,用于请求获取广播数据包中未携带的额外信息。
扫描响应:针对扫描请求所回复的报文,其中包含附加的用户数据。
这是外设无需先与主机建立连接即可发送额外数据的一种方式。如果外设没有更多附加信息可以提供,也可以选择返回一个空的扫描响应。

另外一种可以让外设单次广播更多数据的方式是使用 扩展广播(extended advertising) 这一功能。借助该功能,在主要广播信道上发出的广播数据包会指向在辅助信道上广播的补充信息。扩展广播不在本课程的讲解范围之内,我们将只学习传统广播。
广播类型
外设可以采用多种不同的方式进行广播。
定义
可连接 vs 不可连接:决定主机是否能够与外设建立连接。
可扫描 vs 不可扫描:决定外设是否接收来自扫描设备的扫描请求。
定向 vs 非定向:决定广播数据包是否专门发送给某一台指定的扫描设备。
所使用的广播类型是在广播数据包中进行设置的。我们将在本课的实操练习部分学习如何配置广播类型。
在传统广播当中主要包含四种广播类型;此外还有第五种类型用于扫描响应,本课程不做讲解。
可扫描且可连接(ADV_IND):这是最常用的广播类型。如果外设使用该类型广播,则代表它同时支持可扫描与可连接。也就是说外设向外宣告自身存在,允许主机发送扫描请求,并且外设会回复扫描响应(即可扫描特性);之后还可以建立连接(即可连接特性)。
定向可连接(ADV_DIRECT_IND):该广播类型用于定向广播,广播设备不接收扫描请求。它属于定向、可连接,但不可扫描。适用于广播设备已经知晓扫描设备,仅希望快速重连的场景。一个很好的例子:蓝牙鼠标与电脑断开连接之后,希望重新建立连接。这种场景下无需接收扫描请求;发送定向广播数据包可以缩短连接流程,实现更快重连。
不可连接且可扫描(ADV_SCAN_IND):使用该广播类型的广播设备仅接收扫描请求,但不允许与之建立连接(因此为不可连接)。
不可连接且不可扫描(ADV_NONCONN_IND):该广播类型既不接收扫描请求,也不允许建立连接。这种广播类型的典型应用场景是信标(beacon)。由于设备不需要把射频切换到接收模式(不接收任何数据),因此可以降低电池功耗。
由此可以看出,广播类型分为可连接或不可连接、可扫描或不可扫描、定向或非定向。下表概述了四种传统广播类型及其属性。
| Connectable 可连接 | Scannable 可扫描 | Directed 定向 |
ADV_IND | x | x | |
ADV_DIRECT_IND | x | | x |
ADV_SCAN_IND | | x | |
ADV_NONCONN_IND | | | |
蓝牙地址
每一台低功耗蓝牙设备都由一个唯一的 48 位地址进行标识。蓝牙地址分为公共地址和随机地址两大类。随机地址根据地址是否会发生变化,可以进一步划分为静态地址和私有地址。最后,私有地址又分为可解析私有地址与不可解析私有地址。下图展示了蓝牙地址的分类方式。请注意:随机地址和私有地址只是分类类别,并不是实际的地址类型。

一台低功耗蓝牙设备至少会使用以下地址类型中的一种:
- 公共地址
- 随机静态地址
- 随机私有可解析地址
- 随机私有不可解析地址
注意
分配给设备的公共地址取自与 MAC 地址(例如以太网、Wi‑Fi 所使用的 MAC 地址)相同的 IEEE 地址池,因此它通常也被称作蓝牙 MAC 地址。
公共地址
公共地址是一种固定地址,由厂商在生产阶段烧录进设备。该地址必须向 IEEE 注册机构完成注册;它在全球范围内对该设备保持唯一,并且在设备的整个使用周期内不可更改。获取这类地址需要支付相关费用。
随机地址
随机地址的使用更为普遍,因为它不需要向 IEEE 注册,并且可以由用户手动配置。它也是 nRF Connect SDK 示例工程里的默认地址类型。随机地址既可以预先烧录在设备内部,也可以在设备运行时生成。随机地址又分为静态地址或者私有地址。
随机静态地址
随机静态地址一经分配,在设备的整个生命周期内保持固定。它可以在设备上电启动时被修改,但运行期间无法更改。该地址是公共地址的低成本替代方案,无需进行注册。
如上所述,所有低功耗蓝牙设备必须使用公共地址或者随机静态地址,其中后者的使用要普遍得多。
随机私有地址
当设备希望保护自身隐私时,可以使用私有地址。这类地址会周期性地发生变化,用于隐藏设备身份,防止设备被追踪。
随机私有地址又分为可解析私有地址与不可解析私有地址,具体说明如下。
可解析随机私有地址
顾名思义,可解析私有地址是支持解析的。授权的接收设备持有预共享密钥,每当该地址发生变化时,接收设备就可以凭借该密钥得出新地址。该预共享密钥即为身份解析密钥(IRK),可用于生成随机地址,同时也用于解析随机地址。
对端设备主要依靠该随机地址来解析这台低功耗蓝牙设备的实际地址;而设备的真实地址仍然是公共地址或者随机静态地址。IRK 使对端设备能够将随机私有地址转换为设备真实的低功耗蓝牙地址。
不可解析随机私有地址
不可解析私有地址无法被其他设备解析,其作用仅用于防止设备被追踪。这种地址类型并不常用。
总结,一共有四种不同的地址类型:
- 公共地址: 由厂商烧录至设备内部,并且已向 IEEE 完成注册。
- 随机静态地址: 可在上电启动时配置,在设备整个生命周期内保持固定。无需向 IEEE 注册,是公共地址的常用替代方案。
- 可解析随机私有地址:(可选)会周期性变化的地址,但可以借助预共享密钥完成解析。
- 不可解析随机私有地址:(可选)会周期性变化,并且无法被解析的地址。
广播数据包
下面我们来分析广播数据包的结构。
低功耗蓝牙数据包如下图所示,数据包的主要部分为协议数据单元(PDU)。根据该低功耗蓝牙数据包是用于广播还是用于数据传输,PDU 分为广播 PDU(有时也称作广播信道 PDU)或者数据 PDU(有时也称作数据信道 PDU)。

低功耗蓝牙数据包结构
从图中可以看到,广播 PDU 由头部(header)和有效载荷(payload)组成。
广播 PDU 的头部包含:

广播 PDU header
- PDU 类型:用于指定我们在“广播类型”中讨论过的广播类型,例如 ADV_IND。
- RFU:保留,供未来使用(Reserved for Future Use)。
- ChSel:如果支持低功耗蓝牙信道选择算法 2,则该字段置 1。
- TxAdd(发送端地址):取值 0 或 1,代表发送设备的地址是公共地址还是随机地址。
- RxAdd(接收端地址):取值 0 或 1,代表接收设备的地址是公共地址还是随机地址。
- Length:有效载荷的长度
有关这些字段的更多信息,可以在 Bluetooth SIG 官网的低功耗蓝牙核心规范 中查阅。
广播 PDU 的有效载荷分为两部分:前 6 个字节代表广播设备地址(AdvA),剩余部分则为实际的广播数据(AdvData)。

广播PDU负载
- AdvA:发送广播设备的蓝牙地址
- AdvData:广播数据包
注意
有效载荷结构取决于广播的类型。当使用定向广播(ADV_DIRECT_IND)时,还需要预留部分空间用于指定接收方地址。因此,AdvData 字段会被一个大小为 6 字节的接收方地址字段所取代。该类型(ADV_DIRECT_IND)的广播数据包不包含有效载荷。
广播数据段的结构如下图所示。

广播数据包有效载荷
广播数据包由多个被称为广播数据结构(AD 结构)的结构体组成。每个 AD 结构包含一个长度字段、用于指定类型的字段(AD Type)以及存放实际数据本身的字段(AD Data)。请注意,最常用的 AD 类型为 1 字节长度。
广播数据类型由蓝牙规范进行定义,并在 nRF Connect SDK 的如下位置提供文档说明,位于 “EIR/AD data type definitions(EIR/AD 数据类型定义)” 条目下。
以下列出若干常用的广播数据类型,我们会在后续练习中使用到。
- 完整本地名称(BT_DATA_NAME_COMPLETE):即设备名称。用户扫描周边设备时(例如通过智能手机)就可以看到该名称。
- 缩短本地名称(BT_DATA_NAME_SHORTENED):完整本地名称的精简版本。
- 统一资源标识符(BT_DATA_URI):用于对外广播 URI,例如网站地址(URL)。
- 服务 UUID:服务通用唯一标识符是专属于某项特定服务的全局唯一编号。扫描端可借助该标识识别出值得建立连接的设备。
- 厂商自定义数据(BT_DATA_MANUFACTURER_DATA):这是一种很常用的数据类型,允许厂商定义自有自定义广播数据,iBeacon 就是该类型的应用实例。
- 标志位(Flags):1 位变量,用于标记设备的某项属性或者工作模式。
下面是设置标志位 BT_LE_AD_NO_BREDR 的广播数据结构示例。

广播数据结构示例
标志位
广播标志位是封装在 1 个字节内的单比特标志,意味着最多可以设置 8 个标志位。下面我们来看一些最常用的标志位。
- BT_LE_AD_LIMITED:设置蓝牙低功耗有限可发现模式。用于可连接广播,向中心设备表明该设备仅在限定时长内可被发现,广播超时之后即不再对外广播。
- BT_LE_AD_GENERAL:设置蓝牙低功耗通用可发现模式。用于可连接广播,表示广播会长时间持续有效(超时值 = 0)。
- BT_LE_AD_NO_BREDR:表示不支持经典蓝牙(BR/EDR)。
BT_LE_AD_LIMITED 和 BT_LE_AD_GENERAL 这两个标志均适用于外设角色的设备。
练习 1 – 设置广播数据
在本练习中,我们将学习 nRF Connect SDK 中低功耗蓝牙的核心应用程序接口(API)。首先我们将了解如何启用低功耗蓝牙协议栈以及相关的默认配置。
接着我们将深入学习如何配置硬件,发送不可连接广播,让设备充当信标(beacon),以便周边正在扫描的设备可以收到该广播数据。
练习步骤
0.准备工程,完成编译并将程序烧录至你的开发板
0.1 克隆本课程对应的 GitHub 代码仓库。
重要提示
请注意,该课程代码仓库包含多个不同分支,分别对应本课程所支持的各个 nRF Connect SDK 版本。

默认的 main 分支支持已发布的最新 nRF Connect SDK 版本标签。如果你正在使用旧版本的 nRF Connect SDK,请务必切换签出与你所用版本相对应的分支。
请不要将代码仓库存放在路径较长的位置;在部分操作系统(Windows)上,如果应用程序路径过长,编译系统可能会报错。
0.2 在 VS Code 的 nRF Connect 扩展插件中,选择 “打开现有应用程序”,打开本练习的基础代码,路径位于 bt‑fund/l2/l2_e1。
导航
部分练习的说明文字会根据你所使用的(SDK)版本有所差异,该差异会通过练习正文开头的选项卡进行区分展示。
1. 在工程中引入低功耗蓝牙协议栈。
若要在你的 nRF Connect SDK 工程里启用低功耗蓝牙协议栈,需要在 prj.conf 文件中开启 CONFIG_BT。后续所有练习中该配置项均已预先开启。
CONFIG_BT=y
启用该配置符号后,将会为协议栈应用一组默认配置。
下面列出默认配置的重点项:
- 广播发送支持(BT_BROADCASTER)已启用。
- 使用 SoftDevice 控制器(BT_LL_CHOICE = BT_LL_SOFTDEVICE)。
- 发射功率设置为 0 dBm(BT_CTLR_TX_PWR = BT_CTLR_TX_PWR_0)
注意
对于 nRF5340 系统级芯片这款双核器件,SoftDevice 控制器需要运行在网络核(net core)上。构建系统可以借助 Sysbuild 自动完成该部署。
每个练习中自带的 Kconfig.sysbuild 文件可以确保:当针对 nRF5340 系统级芯片编译练习工程时,SoftDevice 控制器运行在网络核上。
source "${ZEPHYR_BASE}/share/sysbuild/Kconfig"
config NRF_DEFAULT_IPC_RADIO
default y
config NETCORE_IPC_RADIO_BT_HCI_IPC
default y
对于单核系统级芯片(例如 nRF52840),该文件里的内容会直接被忽略;原因很简单,这类系统级芯片没有开启 SUPPORT_NETCORE_IPC_RADIO。
如需进一步了解 Sysbuild,请参阅第 8 课 —— nRF Connect SDK 中级课程。
2. 设置低功耗蓝牙设备名称。
设备名称是一个 C 语言字符串,理论上最长可达 248 字节(不包含末尾的 NULL 结束符)。在实际使用时,强烈建议尽量缩短设备名称。这是因为当我们把设备名称放置到广播数据中时,广播数据仅有 31 字节,并且这 31 字节需要由全部广播数据共同占用。设备名称也可以设置为空字符串。在本练习中,我们将设备命名为 Nordic_Beacon。
在 prj.conf 文件中添加下面这一行配置:
CONFIG_BT_DEVICE_NAME="Nordic_Beacon"
我们将在后续步骤中把设备名称添加到广播数据里。
3. 引入所需的低功耗蓝牙协议栈头文件
引入下面这些头文件,用于启用协议栈、填充广播数据以及启动广播。
在 main.c 文件中添加以下代码行:
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/gap.h>
4. 准备广播数据。
对于广播数据,我们将同时使用广播数据包与扫描响应数据包。
4.1 准备广播数据包。
4.1.1 声明一个 ad[] 数组,类型为 struct bt_data,该数组将作为待发送的广播数据包。
在 main.c 文件中添加以下代码行:
static const struct bt_data ad[] = {
};
4.1.2 使用辅助宏 BT_DATA_BYTES() 填充标志位。
我们需要在广播数据包中首先准备的内容是广播标志位 BT_DATA_FLAGS。
为便于填充这些标志位,我们将使用辅助宏 BT_DATA_BYTES(),该宏的函数原型如下:

在本练习中,我们要创建一个使用不可连接广播的广播发送设备。因此,我们仅设置广播标志位 BT_LE_AD_NO_BREDR,用来表示本设备不支持传统蓝牙(BR/EDR)。
注意
由于 Nordic 的产品仅支持低功耗蓝牙,因此该标志位应当始终设置为此值。
在 main.c 文件中添加下面这一行代码:
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR),
4.1.3 使用辅助宏 BT_DATA() 设置广播数据包数据。
广播数据(既可以存放在广播数据包,也可以存放在扫描响应包)中可以包含许许多多标准数据类型。这些数据类型在《蓝牙核心规范补充文档》中进行了定义。
下面列出了几种常用的数据类型,后续练习中我们将会用到。完整的广播数据类型列表可以在此处查看。
- 完整本地名称(BT_DATA_NAME_COMPLETE):即设备名称。本练习中,我们会将完整本地名称放置到广播数据包内。
- 缩写本地名称(BT_DATA_NAME_SHORTENED):完整本地名称的简短版本。
- 统一资源标识符(BT_DATA_URI):可使用该数据类型广播统一资源标识符,例如网页地址(URL)。本练习里,我们将把 Nordic Developer Academy 的网址(https://academy.nordicsemi.com/)存放到扫描响应数据包中。
- 服务 UUID:此处提供多种可选形式。如果希望中心设备根据服务筛选广播设备,该类型就非常有用。在后续的练习中,我们将会加入 LBS 和 NUS 服务的 UUID。
- 厂商自定义数据(BT_DATA_MANUFACTURER_DATA):这是一种很常用的数据类型,允许厂商自行定义专属广播数据,iBeacon就是该类型的应用实例。下一课的练习中我们将会讲解该数据类型的使用。
现阶段,我们需要在广播数据包中加入完整本地名称 BT_DATA_NAME_COMPLETE。
我们将使用宏 BT_DATA() 向广播数据包填充数据。该宏需要传入三个参数,其原型如下所示。

在 main.c 文件中添加下面这一行代码:
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
注意
在传统广播模式下,仅可以使用 31 个八位字节。如果需要在广播中发送超过 31 个八位字节的数据,就必须使用扫描响应包,接下来我们就会讲到这一点。
4.2 准备扫描响应包。
作为演示,我们将在扫描响应包里面填入 Nordic Developer Academy 的网址。
4.2.1 声明扫描响应数据包。
和前面处理广播数据包的做法一样,将它声明为 sd[] 数组,类型为 struct bt_data。
在 main.c 文件中添加以下代码:
static const struct bt_data sd[] = {
};
4.2.2 声明需要包含在扫描响应数据包内的 URL 数据。
我们将该 URL 声明为静态无符号字符数组。数组的第一个字节需要指定 URI 协议名称字符串映射,该映射在 Bluetooth SIG 的《Assigned Numbers(分配编号)文档》中有定义。 该协议映射用于减少空中传输的数据量。举个例子:不需要在空中广播传输 6 个字节的 “https:”,我们只需要发送一个字节(0x17)即可。

由 Bluetooth SIG 在《分配编号文档》中定义的 URI 协议名称字符串映射
在 main.c 文件中添加以下几行代码:
static unsigned char url_data[] ={0x17,'/','/','a','c','a','d','e','m','y','.',
'n','o','r','d','i','c','s','e','m','i','.',
'c','o','m'};
4.2.3 将 URL 数据添加至扫描响应数据包中。在 sd 数据包数组内部添加下面这一行。
BT_DATA(BT_DATA_URI, url_data,sizeof(url_data)),
5. 启用低功耗蓝牙协议栈。
函数 bt_enable()用于在应用程序中启用低功耗蓝牙协议栈。该函数必须在调用其他所有需要与低功耗蓝牙硬件进行通信的接口之前执行(例如,启动广播)。
当向 bt_enable() 传入 NULL 参数时,该函数为阻塞调用;如果传入 bt_ready_cb_t 回调函数,则该函数为非阻塞调用。

在 main () 函数中添加以下几行代码:
err = bt_enable(NULL);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)\n", err);
return -1;
}
LOG_INF("Bluetooth initialized\n");
6. 启动广播。
既然我们已经准备好了广播数据(包括广播数据包以及扫描响应数据包),接下来就可以启动广播了。
通过调用 bt_le_adv_start() 函数完成该操作,该函数的原型如下:

bt_le_adv_start () 函数
该函数的第一个入参为广播参数。此处我们既可以使用覆盖大多数常用场景的预定义宏,也可以定义一个 struct bt_le_adv_param 类型的变量,手动设置各项参数。
现阶段我们将使用其中一个预定义宏。在练习 2 中,我们会手动设置该参数。本例中我们使用 BT_LE_ADV_NCONN—— 不可连接广播,最小广播间隔 100 ms,最大广播间隔 150 ms。
第二个与第三个参数分别是(在 4.1 小节创建的)广播数据包及其长度;而第四、第五个参数则是(在 4.2 小节创建的)扫描响应包及其长度。
在 main () 函数内部添加以下几行代码:
err = bt_le_adv_start(BT_LE_ADV_NCONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (err) {
LOG_ERR("Advertising failed to start (err %d)\n", err);
return -1;
}
7. 在开发板上编译并烧录该应用程序。
你应当可以看到开发板上的 LED1(在 nRF54 系列设备上为 LED0)正在闪烁,这表明开发板正在执行蓝牙广播。
8. 在你的智能手机上打开 nRF Connect for Mobile。
安卓:
在【SCANNER(扫描器)】选项卡中按下 SCAN(扫描)按钮,开始扫描。

安卓
9. 点击 Nordic_Beacon,查看广播数据。

安卓
首先可以注意到页面上没有显示 CONNECT(连接) 按钮。这是因为我们发送的是不可连接广播数据包(BT_LE_ADV_NCONN)。接下来我们来解读这些广播数据。
1. RSSI 值:接收端的接收信号强度指示。
2. 广播间隔:由于我们选用了 BT_LE_ADV_NCONN,广播间隔取值范围为 100‑150 ms。
3. 广播标志位:即我们在 4.1.2 步骤中设置的广播标志(不支持传统蓝牙 BR/EDR)。
4. 完整本地名称:也就是我们在第 2 步设置的设备本地名称。
5. URI 数据:存放在扫描响应数据包当中。请注意,你可以点击 OPEN 按钮,直接在浏览器打开该链接。
建议你尝试在代码中修改上述各项参数,重新编译并烧录到开发板,亲身体验控制广播数据是多么简便。
iOS:
在【Scanner(扫描器)】选项卡中,点击播放图标开始扫描。

iOS
9. 点击 Nordic_Beacon,查看广播数据。

iOS
首先可以注意到界面上没有显示 “Connect(连接)” 按钮。这是因为我们正在发送不可连接广播数据包(BT_LE_ADV_NCONN)。接下来我们花一点时间解读这些数据。
1. RSSI 值是接收端的接收信号强度指示。
2. 广播间隔:由于我们选用了 BT_LE_ADV_NCONN,广播间隔介于 100‑150 ms 之间。
由于 iOS 的蓝牙协议栈会过滤掉大量广播数据,以上就是我们能够看到的全部信息。
建议你尝试在代码中修改这些参数,编译并烧录至开发板,亲身体验控制广播数据十分简便。
练习 2 — 广播厂商自定义数据
在本练习中,我们将在上一个练习的基础之上,重点学习广播参数、厂商自定义数据以及动态更新广播数据。
本练习依旧发送不可连接广播。但与上一个练习不同,我们不再使用预定义宏来设置广播参数,而是对广播间隔进行精细控制。
同时我们还将学习如何动态修改广播数据的内容。我们将创建自定义广播数据,用来记录开发板上按键 1(nRF54 系列设备上为按键 0)的按下次数,并把该数据放置到广播数据包当中。
练习步骤
在本课程的 GitHub 代码库中,找到本练习的基础代码,路径为 l2/l2_e2。
1. 创建 adv_param 变量,类型为 bt_le_adv_param。
该变量类型可以对广播的多项属性进行配置。下面我们来看如何使用它设置广播间隔。
创建该变量时,我们将使用辅助宏 BT_LE_ADV_PARAM(),该宏的原型如下:

BT_LE_ADV_PARAM() 辅助宏
- 广播选项:用于配置广播选项的专用宏。例如,可以选择在哪一个信道(37、38、39)上发送广播。
- 最小广播间隔:(N × 0.625 毫秒):小于或等于最大广播间隔。N 的允许取值范围为 32‑16384,换算后对应 20 ms ~ 10.24 s。该 API 提供了可供直接使用的广播间隔预定义值。
- 最大广播间隔:(N × 0.625 毫秒):大于或等于最小广播间隔。N 的允许取值范围为 32‑16384,换算后对应 20 ms ~ 10.24 s。该 API 提供了可供直接使用的广播间隔预定义值。
- 对端地址:使用定向广播时填写该字段;其他情况下设置为 NULL。
在 main.c 文件中添加以下几行代码:
static const struct bt_le_adv_param *adv_param =
BT_LE_ADV_PARAM(BT_LE_ADV_OPT_NONE,
800,
801,
NULL);
上述代码会将广播间隔设置为约 500 毫秒(500 ms ÷ 0.625 ms = 800(N 值)。正如我们之前所讲,系统还会额外增加一段随机延时,用来避免数据包发生冲突。
2. 声明厂商自定义数据
厂商自定义数据用于在低功耗蓝牙广播中创建并放入自定义数据。所要填入的数据可以采用适配你的使用场景或应用的任意格式。
2.1 声明厂商标识符(Company ID)
厂商自定义数据的前两个字节是厂商标识符,该标识符在蓝牙技术联盟(Bluetooth SIG)的《分配编号文档(Assigned Numbers Document)》中注册。出于教学和测试的目的,我们将使用 Nordic Semiconductor 的厂商标识符。
在 main.c 文件中添加下面这行代码:
#define COMPANY_ID_CODE 0x0059
注意
低功耗蓝牙产品如果并非蓝牙技术联盟(Bluetooth SIG)的会员,则不允许广播自定义的厂商标识 ID。当你准备发布产品时,必须申请成为 Bluetooth SIG 会员,以获取属于自己的唯一厂商标识 ID。
2.2 声明自定义数据的结构体
在本练习中,我们采用简单的实现方式,将自定义数据声明为无符号短整型(2 字节)。该数据用于记录按键 1 的按下次数。因此我们需要创建一个名为 adv_mfg_data_type 的结构体,它包含两个成员。第一个成员自然是厂商标识 ID(Company ID);第二个成员为 number_press,用来保存开发板上按键 1 的按下次数。
在 main.c 文件中添加下面的结构体定义:
typedef struct adv_mfg_data {
uint16_t company_code;
uint16_t number_press;
} adv_mfg_data_type;
2.3 定义并初始化一个上一步所创建的 adv_mfg_data_type 类型的变量。
static adv_mfg_data_type adv_mfg_data = { COMPANY_ID_CODE, 0x00 };
厂商自定义数据的声明部分已经全部完成。下一步,我们将添加一个按键按下回调函数;该函数会更新 number_press(按键按下次数),并通过 bt_le_adv_update_data() 函数更新广播数据。
3. 将厂商自定义数据添加到广播数据包中。
在广播数据包 ad 的定义内部添加下面这行代码:
BT_DATA(BT_DATA_MANUFACTURER_DATA, (unsigned char *)&adv_mfg_data, sizeof(adv_mfg_data)),
4. 初始化按键库并设置回调函数
4.1 我们将使用 DK 按键与 LED 库来配置中断(回调函数),使得每次按下按键 1(nRF54 系列设备上为按键 0)时都会触发该回调函数。
在 main.c 文件中添加 init_button() 函数的定义:
static int init_button(void)
{
int err;
err = dk_buttons_init(button_changed);
if (err) {
printk("Cannot init buttons (err: %d)\n", err);
}
return err;
}
该函数内部调用 dk_buttons_init() 函数对开发板的按键进行初始化,并且将在第 5 步中定义的 button_changed() 指定为回调函数;每次按下按键时就会执行该回调函数。
4.2 在 main() 函数中调用 init_button()
在 main() 里面添加下面这行代码:
err = init_button();
if (err) {
printk("Button init failed (err %d)\n", err);
return -1;
}
5. 添加回调函数的定义,并动态更新广播数据
我们将在 button_changed() 回调函数内部实现广播数据的动态更新。这里会使用 bt_le_adv_update_data()函数。

bt_le_adv_update_data() API 函数
bt_le_adv_update_data() 与上一个练习中学过的 bt_le_adv_start() 非常相似,区别是该函数不需要传入广播参数选项。它依赖于之前调用 bt_le_adv_start() 时所设置的广播参数。
添加按键回调函数的定义。
static void button_changed(uint32_t button_state, uint32_t has_changed)
{
if (has_changed & button_state & USER_BUTTON) {
adv_mfg_data.number_press += 1;
bt_le_adv_update_data(ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
}
}
在该回调函数中,我们将完成以下操作:
- 判断是哪一个按键被按下:
if (has_changed & button_state & USER_BUTTON)
- 更新按键的按下计数:
adv_mfg_data.number_press += 1;
- 动态更新广播数据:
bt_le_adv_update_data(ad, ARRAY_SIZE(ad),sd, ARRAY_SIZE(sd));
6. 在开发板上编译并烧录该应用程序。
此时你应该可以看到开发板上的 LED1(nRF54 系列设备上为 LED0)正在闪烁,表明开发板正在发送广播。
7. 在你的智能手机上打开 nRF Connect for Mobile。
注意
在 nRF54 DK 开发套件(例如 nRF54L15 DK)上,板载 LED 与按键的 PCB 丝印标号从 0 开始(LED0‑LED3 和 BUTTON0‑BUTTON3)。而前一代开发套件的编号则是从 1 开始(LED1‑LED4、BUTTON1‑BUTTON4)。
安卓:
在**SCANNER(扫描器)选项卡中,点击SCAN(扫描)**按钮开始扫描。

安卓
注意对比,连接间隔和上一个练习相比已经发生了变化;上一个练习使用 BT_LE_ADV_NCONN 宏时连接间隔为 100‑150 ms,现在大约是 500 ms。
8. 点击 Nordic_Beacon,查看广播数据。
此时你应该可以看到广播数据包中已经包含厂商数据,里面是 Nordic Semiconductor 的厂商标识 ID。
9. 点击Manufacturer数据,修改数据的显示方式,选择 Unsigned Int16(无符号 16 位整数) 或者 Manufacturer data (Bluetooth Core 4.1)
数据显示为厂商数据(蓝牙核心规范 4.1)

补充说明
nRF Connect for Mobile 应用会调用蓝牙编号数据库(Bluetooth Numbers Database),将已注册的厂商 UUID 与其对应的厂商名称进行匹配。因此我们可以看到 UUID:0x0059(89)连同厂商名称:Nordic Semiconductor ASA一同显示出来。
将数据切换为无符号 16 位整数

该数据将以无符号 16 位整数形式显示:

10. 在你的开发板上按下按键 1(nRF54 系列设备上为按键 0)
你应当可以观察到,每按一次按键,计数值就加 1。手机需要处于扫描模式才可以看到该数值变化。请在 nRF Connect for Mobile 右上角点击SCAN(扫描),即可查看更新后的数据。

默认情况下,nRF Connect for Mobile 的扫描时长设置为 45 秒。你可以进入 设置‑> 扫描器‑> 扫描时长 修改扫描时长。
iOS:
在**扫描器(Scanner)**选项卡中,点击播放图标开始扫描。

iOS
补充说明
nRF Connect for Mobile 应用会借助蓝牙编号数据库(Bluetooth Numbers Database),把已注册的厂商 UUID 和对应的注册厂商名称进行匹配。因此我们可以看到 UUID:0x0059(89)连同厂商名称:Nordic Semiconductor ASA 一起显示出来。
留意观察连接间隔相较于上一个练习发生的变化;上一练习使用 BT_LE_ADV_NCONN 宏时连接间隔为 100‑150 ms,现在大约为 500 ms。
8. 使用筛选器,仅显示 Nordic_Beacon 设备
在扫描器选项卡中,选中筛选功能,并在名称(Name)输入框内键入 Nordic_Beacon。

此时在扫描器(Scanner)选项卡中应当只会显示 Nordic_Beacon 设备。

注意此时厂商自定义数据的值为 0000。
9. 在开发板上按下按键 1(nRF54 系列设备上为按键 0)。
这会将厂商自定义数据的值加 1。
10. 重新开始扫描设备
若要观察到该数值变化,你需要在手机上重新启动扫描。你可以在扫描器(Scanner)窗口内向下滑动,调出如下窗口。

这时你会发现该数据已经加 1。

蓝牙低功耗在 GAP 和 GATT 层采用小端序表示数据,这也就是数值会从 0x00 00 增加到 0x01 00 这类情况的原因。
进行下一个练习之前,请务必关闭筛选器。
练习 3 — 可连接广播与地址配置
在本练习中,我们将从练习 1 和练习 2 介绍过的非可连接广播切换为可连接广播。外设设备使用可连接广播对外通告自身的存在,并允许中心设备与其建立连接。
我们还将在广播数据中加入 LBS 服务的 UUID;中心设备可以据此根据可用服务来判断是否需要发起连接。最后,我们将学习如何手动配置该外设的蓝牙低功耗地址。请注意:该地址仅在安卓手机上可以显示,iOS 手机无法看到。
LED 按键服务(LBS)是由 Nordic 创建的一项自定义服务,第 4 课将会对其进行更为深入的讲解。
练习步骤
在本课程的 GitHub 代码库中,定位到本练习的基础代码,路径为 l2/l2_e3。
1. 在蓝牙低功耗协议栈中添加对外设角色的支持。
在 prj.conf 文件中添加下面这一行代码:
CONFIG_BT_PERIPHERAL=y
启用该配置标志后,即可获得外设角色所需的各项功能,其中包括 GATT 和 ATT。同时连接数的配置项 CONFIG_BT_MAX_CONN 的默认值为 1。
2. 将蓝牙低功耗设备名称从 Nordic_Beacon 修改为 Nordic_Peripheral。
CONFIG_BT_DEVICE_NAME="Nordic_Peripheral"
3. 准备广播数据
3.1 设置标志位,并在广播数据包中填入设备名称
与之前的操作相同,我们将设备名称放置到广播数据包内。同时开启发现模式标志位,因为本设备将作为蓝牙低功耗外设,而不再是信标。我们把发现模式设置为 BT_LE_AD_GENERAL。
请在 ad(广播)的定义内部添加以下几行代码:
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
3.2 在扫描响应数据包中填入 LBS 服务 UUID。
3.2.1 引入 UUID 辅助宏与相关定义的头文件。
#include <zephyr/bluetooth/uuid.h>
3.2.2 将 LBS 服务的 128 位 UUID 添加至扫描响应数据包。
该 UUID 可在此处查阅:LED Button Service (LBS) – Service UUID。
请在 sd 的定义内部添加下面这一行代码:
BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_128_ENCODE(0x00001523, 0x1212, 0xefde, 0x1523, 0x785feabcd123)),
4. 设置静态随机地址。
4.1 引入用于管理蓝牙低功耗地址的头文件。
#include <zephyr/bluetooth/addr.h>
4.2 修改静态随机地址。
在前两个练习中,所生成的蓝牙低功耗地址为不可解析随机私有地址,该地址每次上电启动时都会随机生成。这是非可连接广播所设置的默认地址类型。对于可连接广播,默认使用静态随机地址。在本步骤中,我们将学习如何手动配置静态随机地址,并将其设置为地址 FF:EE:DD:CC:BB:AA。
我们将调用 bt_id_create()函数来设置新的静态随机地址。并使用 bt_addr_le_from_str()函数把字符串转换为蓝牙低功耗地址。该函数支持 “random(随机)” 和 “public(公共)” 这两种地址类型。

在 main.c 文件中添加下面的代码片段:
bt_addr_le_t addr;
err = bt_addr_le_from_str("FF:EE:DD:CC:BB:AA", "random", &addr);
if (err) {
printk("Invalid BT address (err %d)\n", err);
}
err = bt_id_create(&addr, NULL);
if (err < 0) {
printk("Creating new ID failed (err %d)\n", err);
}
5. 启动可连接广播。
该操作通过调用 bt_le_adv_start() 函数完成。若要指定为可连接广播,我们既可以传入 BT_LE_ADV_CONN_FAST_2 或者 BT_LE_ADV_CONN_FAST_1(替代练习 1 中所使用的 BT_LE_ADV_NCONN 宏)作为第一个入参;也可以像练习 2 那样自行设置广播参数。自行创建广播参数的优势在于可以对广播实现更多控制。
5.1 创建可连接广播的广播参数。同样,该步骤为可选操作;你也可以直接将 BT_LE_ADV_CONN 作为第一个参数传递给 bt_le_adv_start()。
static const struct bt_le_adv_param *adv_param = BT_LE_ADV_PARAM((BT_LE_ADV_OPT_CONN|BT_LE_ADV_OPT_USE_IDENTITY),
800,
801,
NULL);
5.2 使用可连接广播时有一项重要注意事项:当外设与中心设备断开连接之后,是否需要恢复广播。
从 nRF Connect SDK 3.0.0 版本开始,当外设连接上中心设备时,广播会自动关闭;并且断开连接之后广播依旧保持关闭状态。 如果希望断开连接之后重新恢复广播,我们需要捕获蓝牙低功耗主机侧的 .recycled 事件,并触发工作队列以重新启动广播,具体见下文。
请注意:不要直接在 .recycled 回调内部调用 bt_le_adv_start ()。该回调源自蓝牙协议栈,应当将其视作中断服务程序(ISR);因此建议如示例所示,提交至工作队列来执行重启广播的操作。
* STEP 5.2 - Resume advertising after a disconnection */
static void adv_work_handler(struct k_work *work)
{
int err = bt_le_adv_start(adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (err) {
printk("Advertising failed to start (err %d)\n", err);
return;
}
printk("Advertising successfully started\n");
}
static void advertising_start(void)
{
k_work_submit(&adv_work);
}
static void recycled_cb(void)
{
printk("Connection object available from previous conn. Disconnect is complete!\n");
advertising_start();
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.recycled = recycled_cb,
};
5.3 调用 advertising_start() 启动广播,传入广播参数、广播数据包、扫描响应数据包以及对应的数据包长度。
k_work_init(&adv_work, adv_work_handler);
advertising_start();
6. 在开发板上编译并烧录该应用程序。
此时你应当可以看到开发板上的 LED1(nRF54 系列设备上为 LED0)正在闪烁,表示开发板正在发送广播。
7. 在智能手机上打开 nRF Connect for Mobile,然后开始扫描。
安卓:

nRF Connect for Android
此时我们应当可以看到设备地址已经设置为 FF:EE:DD:CC:BB:AA,并且 LBS 服务的 UUID 现已对外广播。同时还可以观察到设备名称旁边出现 CONNECT(连接) 按钮,这表明该设备正以可连接模式进行广播。后续几课我们将会讲解设备连接相关内容。
iOS:

iOS
我们可以看到 LBS 服务的 UUID 现在已经对外广播。此外,可以留意到设备名称旁边出现了**Connect(连接)**按钮,说明该设备正工作在可连接广播模式。后续几课我们将讲解设备连接相关内容。
补充说明
由于 iOS 的蓝牙低功耗协议栈会过滤掉设备的蓝牙地址,因此无法使用 iOS 手机查看到该地址。在第 6 课中,我们将使用蓝牙抓包器捕获并分析数据包,届时就可以看到设备地址。
测验
请确保在参加测验前已学完所有小节内容。测验是完成本课学习的最后一步。如果未按既定顺序完成本课内容,请务必重新作答测验,才能将本课标记为已完成。
点击开始测验 (英文)
系列课程概览
课程介绍
低功耗蓝牙基础课程为线上自定进度课程,依托 Nordic Semiconductor 器件(nRF54、nRF53 以及 nRF52 系列),讲授低功耗蓝牙的基础知识。学员将通过实操学习,快速搭建低功耗蓝牙原型,并在两台低功耗蓝牙设备之间建立单向、双向无线数据通道。
完成本课程后,学员将深入理解低功耗蓝牙协议及其协议层,掌握基于 Zephyr RTOS 的 nRF Connect SDK 所提供的各类应用程序接口。此外,学员可获得配置低功耗蓝牙广播与连接的实操经验,学习低功耗蓝牙连接的安全防护方法,并学会使用 nRF Sniffer 抓取空中数据包。
你将学到的内容
- 深入理解最新的低功耗蓝牙应用架构。
- 掌握低功耗蓝牙广播及其各项功能。
- 了解低功耗蓝牙连接,以及如何通过低功耗蓝牙传输数据。
- 学习低功耗蓝牙连接的安全防护相关知识。
- 熟悉 nRF Connect SDK 中的低功耗蓝牙应用程序接口。
- 学习用于调试低功耗蓝牙应用的工具(nRF Connect for Mobile、nRF Sniffer)。
- 通过实操练习学以致用,在两台支持低功耗蓝牙的设备之间建立单向与双向数据通道。
课程适合人群
如果你想要学习低功耗蓝牙协议,并希望使用 Nordic Semiconductor 的产品搭建低功耗蓝牙原型,那么适合报名本课程。本课程面向嵌入式软件工程师、固件开发人员,以及所有对微控制器和嵌入式系统感兴趣的学习者。
预计时长
完成本课程总共大约需要 8 至 10 小时。实际所需完成时间很大程度上取决于你的技术背景与相关经验。
学习目标
研究低功耗蓝牙协议,并理解各协议层的作用
- 了解低功耗蓝牙应用架构概况
- 学习蓝牙通用访问配置文件(GAP)所定义的设备角色与通信拓扑方案
- 实操练习:使用 nRF Connect for Mobile 应用建立低功耗蓝牙连接,通过手机读取 Nordic 设备上按键的状态
- 实操练习:使用 nRF Connect for Mobile 应用建立低功耗蓝牙连接,通过手机控制 Nordic 设备上 LED 的亮灭
掌握低功耗蓝牙广播
- 理解传统广播流程的基础知识以及相关的各类广播参数(广播间隔、发射功率、信道、所用物理层)
- 学习广播类型(可连接型与非连接型、可扫描型与非扫描型等)
- 研究低功耗蓝牙地址类型(公共地址、随机地址、静态地址、可解析地址等)
- 学习如何使用 nRF Connect SDK 应用程序接口,对低功耗蓝牙传统广播的各项参数进行配置
- 通过实操练习创建低功耗蓝牙信标
- 通过实操练习,在设备发现阶段配置从机设备的广播数据包
学习低功耗蓝牙连接相关知识
- 理解低功耗蓝牙的连接流程
- 了解各类连接参数及其作用
- 学习修改连接参数的方法
- 通过实操练习建立连接,并修改各类连接参数
研究低功耗蓝牙中的各类数据交互方式
- 理解通用属性协议(GATT)与属性协议(ATT)的作用
- 了解如何在低功耗蓝牙连接中通过 GATT 表征、交换数据
- 查看低功耗蓝牙外设设备的属性表
- 掌握创建 GATT 服务与特征值的方法
- 学习 nRF Connect SDK 中内置的标准低功耗蓝牙服务
- 通过实操练习创建并添加自定义以及标准低功耗蓝牙服务和特征值
- 通过实操练习使用 GATT 应用程序接口(读、写、无响应写入、通知、指示)
学习如何保护低功耗蓝牙连接
- 理解低功耗蓝牙的安全模型
- 研究低功耗蓝牙的认证与加密流程
- 研究绑定流程以及数据在闪存中的存储方式
- 了解 nRF Connect SDK 中与安全相关的 Kconfig 配置项
- 通过实操练习,调用 nRF Connect SDK 的应用程序接口来对低功耗蓝牙连接进行安全防护
研究低功耗蓝牙空中数据包
- 学习使用蓝牙抓包工具捕获广播数据包
- 学习安装并运行 Nordic 蓝牙抓包工具 nRF Sniffer,捕获并分析低功耗蓝牙数据包
- 学习使用 nRF Sniffer 跟踪连接、检查通信内容
- 观察并解密安全连接下的低功耗蓝牙通信数据
课程设置
课程细节
- 基础级别
- 共6节课
- 预计8-10小时完成
- 结业证书
前置要求
- C 编程语言基础知识。
- 具备一定嵌入式系统软件开发经验。
- nRF Connect SDK 基础课程
- (必修)第 1 课
- (推荐学习)第 2 课~第 8 课
硬件要求
系统要求
- 搭载 Windows、macOS 或 Linux 系统的电脑
- 最新版网页浏览器
软件要求
支持的 SDK 版本
- nRF Connect SDK v3.4.0 – v2.3.0
- nRF54LS05 DK 需要 nRF Connect SDK v3.3.0 或更高版本
- nRF54LM20 DK 需要 nRF Connect SDK v3.1.1 或更高版本
- nRF54L15 DK 需要 nRF Connect SDK v2.8.0 或更高版本