Skip to content

BLE 简介 ​

蓝牙是一种广泛使用的无线通信技术标准,由蓝牙技术联盟(SIG)管理。蓝牙低功耗(LE)协议被视为与经典蓝牙不同的协议,其设计目的是以相对较低的数据速率传输较少量的数据,从而实现更低的功耗。

本课程将首先介绍蓝牙低功耗及其协议栈层级。随后我们将简要讨论蓝牙低功耗连接的建立过程,涵盖广播和扫描等概念。接着会介绍可能的网络拓扑结构及物理层(PHY)模式。

什么是 Bluetooth LE(蓝牙低功耗)? ​

蓝牙 LE 的基本定义与背景 ​

Bluetooth LE(Bluetooth Low Energy,蓝牙低功耗)是自 Bluetooth Core Specification 4.0 版本起引入的蓝牙标准。它专为低功耗物联网(IoT)应用设计,旨在极大地降低能耗,使得对续航要求高、传输需求低的设备(如可穿戴设备、传感器等)能够长时间运行。

类比理解:Bluetooth LE 就像是用“省电模式”打电话,牺牲部分通话质量(数据速率)来大幅拉长电池续航。

Bluetooth Classic 与 Bluetooth LE 的主要区别 ​

  • Bluetooth Classic 适合高数据速率场景,比如无线耳机音乐流媒体。虽然功耗较高,但可以满足大带宽需求,设备通常有条件规律充电。

  • Bluetooth LE 通过减少数据包大小(27~251 字节)和降低发送频率来实现低能耗。适合仅需偶尔传输少量数据、且需长时间待机的设备,比如健身手环、智能传感器等。

此外,Bluetooth LE 支持不同的网络拓扑结构和节点类型,以适应新的 IoT 应用场景。

Bluetooth LE 的技术参数概览 ​

参数说明
工作频段2400 MHz – 2483.5 MHz(约 2.4 GHz)
信道带宽2 MHz
射频信道数40
最大发射功率20 dBm(0.1 W)
最大应用层数据吞吐量1.4 Mbps
低速率(125/500 kbps)下最大距离约 1000 米(实际取决于环境和配置)

注意:蓝牙实际通信距离受多种因素影响,如软硬件配置、天线设计和物理环境。可参考 Bluetooth 官方在线距离估算工具 得到更合理的预估。

Bluetooth LE 的优势 ​

  • 低成本:与其他低功耗个人区域网(PAN)技术相比,Bluetooth LE 方案更加经济,适合大规模部署。

  • 普及率高,易测试:大多数智能手机均支持 Bluetooth Classic 和 Bluetooth LE,开发、测试、原型验证门槛低,不需要额外专用硬件。

  • 灵活实现:以 Nordic Semiconductor 为代表的厂商为 Bluetooth LE 提供了丰富的实现选择、开源文档及持续技术支持。

Bluetooth LE 协议栈结构 ​

Bluetooth LE 协议栈分为三大部分:应用层、主机(Host)、控制器(Controller)。

应用层 ​

  • 用户通过 API 与应用层交互,主要包括 Profile、Service 以及 Characteristic 的管理与操作。

主机(Host)各层功能 ​

  • L2CAP(逻辑链路控制与适配协议):为上层协议提供数据封装服务。

  • SMP(安全管理协议):定义和实现安全通信机制。

  • ATT(属性协议):允许设备暴露特定数据给其他设备。

  • GATT(通用属性配置文件):定义如何使用 ATT 层进行数据交互。

  • GAP(通用访问配置文件):负责设备发现、连接等核心服务,直接与应用层对接。

Zephyr Bluetooth Host 实现了上述所有主机层协议,并为应用提供 API。

控制器(Controller)各层功能 ​

  • PHY(物理层):定义数据如何调制到射频信号上,以及信号的收发方式。

  • LL(链路层):管理射频的状态,包括待机、广告、扫描、初始化和连接等。

nRF Connect SDK 主要提供两种控制器实现:SoftDevice Controller 和 Zephyr Bluetooth LE Controller。推荐在 nRF52、nRF53 和 nRF54 系列芯片上使用专为其设计的 SoftDevice Controller。

GAP:设备角色与网络拓扑 ​

两种通信模式 ​

Bluetooth LE 协议支持两种通信模式:

  • 面向连接的通信(Connection-oriented communication):设备之间建立专门的连接,实现双向数据通信。

  • 广播通信(Broadcast communication):设备无需建立连接,通过广播数据包向所有覆盖范围内的设备传递信息。

类比理解:连接通信像是点对点打电话,广播通信则像是用大喇叭对着广场喊话,所有在场的人都能听到。

设备角色定义 ​

GAP(通用访问配置文件)层为 Bluetooth LE 网络中的节点定义了不同的设备角色,这些角色决定了设备如何广播自己的存在、如何扫描和连接其他节点。

广告与扫描 ​

  • Advertising(广播):设备主动发送广告包,用于被其他设备发现,或直接发布信息。

  • Scanning(扫描):设备监听并接收广告包,以发现其他设备。

Central 与 Peripheral ​

  • Peripheral(外围设备):主动广播自己,等待其他设备连接。

  • Central(中心设备):扫描广告包,主动向外围设备发起连接请求。

如果 central 扫描到 peripheral 的广播包,可以选择向其发起连接请求,连接建立后两者进入双向通信状态。

  • central 可以同时与多个 peripheral 建立连接,并负责连接管理和大部分数据处理;peripheral 一般功耗更低。

  • 典型场景:资源有限、需低功耗的 IoT 设备作为 peripheral,手机等高性能设备作为 central。

注意:本课程主要以 peripheral 角色为例进行介绍。

Broadcaster 与 Observer ​

  • Broadcaster(广播者):特殊的 peripheral,只发送广告包,不接受连接请求(如 beacon 设备)。

  • Observer(观察者):特殊的 central,只监听广告包,不发起连接。

网络拓扑 ​

广播拓扑 ​

  • 数据通过广告包广播,无需建立连接。

  • 任何在覆盖范围内的设备(observer/central)都能接收广播内容。

  • 应用举例:蓝牙信标(beacon)、室内导航等。

  • 优点:没有接收设备数量限制,能效高。

  • 缺点:广告包数据量有限,吞吐量低,且无接收确认机制。

连接拓扑 ​

  • 在数据交换前先建立连接,通信为双向。

  • central 可同时与多个 peripheral 连接;peripheral 也可与多个 central 建立连接。

  • 优点:吞吐量提升,支持双向通信。

注:虽然 Bluetooth LE 标准未限制连接数量,但受带宽和硬件资源约束,实际连接数有限。

多角色拓扑 ​

  • 单个设备可同时扮演多种角色,例如同时作为 central 和 peripheral。

  • 典型应用:网关(hub)设备既作为 central 采集多个传感器(peripherals)数据,又作为 peripheral 向手机(centrals)转发数据。

ATT 与 GATT:数据表示与交换 ​

区分角色:GAP 与连接后数据交换 ​

在前文中,我们介绍了 GAP 层如何定义 Bluetooth LE 设备在广播与连接阶段的通信方式。需要注意的是,广播通信仅用于设备发现或单向广播数据,由 GAP 层负责。而在设备建立连接后,需要实现真正的双向数据交换,这时就需要专门的数据结构和协议:即 ATT(属性协议)层和其上的 GATT(通用属性配置文件)层。

ATT(Attribute Protocol,属性协议) ​

基本概念 ​

ATT 层负责连接建立后,Bluetooth LE 设备间的数据传输和处理。ATT 采用客户端-服务器架构:

  • 服务器(Server):持有数据,可以主动发送,或等待客户端请求读取。

  • 客户端(Client):请求读取或写入服务器上的数据。

需要特别注意:ATT 层的客户端与服务器角色,与 GAP 层的 central/peripheral 角色是独立的。一个 central 或 peripheral 都可以是 ATT 的客户端或服务器,取决于应用场景。

  • 实际应用中,peripheral 往往作为服务器(如采集数据并存储),central 通常作为客户端(如读取手环数据的手机)。

定义 ​

  • GATT 服务器:存储数据,并提供接口让 GATT 客户端访问这些数据。

  • GATT 客户端:通过 GATT 操作访问服务器上的数据。

Attribute(属性) ​

ATT 协议定义了一种标准化数据结构,称为属性(Attribute),用于在服务器端存储数据。服务器可以同时持有多个属性。

属性(Attribute)可以理解为“数据库中的一行”,每个属性都有唯一的标识符、类型、权限和数据内容。

GATT(Generic Attribute Profile,通用属性配置文件) ​

基本概念 ​

GATT 层位于 ATT 层之上,对属性进行分层和组织,将属性划分为Profile(配置文件)、Service(服务)和 Characteristic(特征),形成了层级式的数据结构。

Profiles、Services 与 Characteristics 的关系 ​

以心率监测设备为例:

  1. Characteristic(特征)

    1. 例如“心率测量”特征(Heart Rate Measurement Characteristic),其核心数据(心率值)和元数据(声明等)以属性形式存储,合起来就是一个特征。
  2. Service(服务)

    1. 一个服务由多个特征组成。例如“心率服务”(Heart Rate Service)下包含“心率测量”特征、“传感器位置”特征等。
  3. Profile(配置文件)

    1. 一个 Profile 通常包含一个或多个服务,面向实际应用场景。例如“心率 Profile”包含“心率服务”和“设备信息服务”。

类比:Profile 就像是一个“应用”,Service 是其中的“模块”,Characteristic 则是“模块中的功能点”。

客户端与服务端的交互流程 ​

在客户端与服务器交互前,客户端并不知道服务器有哪些服务和特征。

因此,客户端首先需要进行 Service Discovery(服务发现),查询服务器支持哪些服务与特征,然后才能进行具体的数据读写操作。

规范与扩展 ​

  • 所有官方 GATT Profiles 由 Bluetooth SIG 定义,完整列表可查阅 SIG 官网。

  • 协议也允许厂商自定义 Profile,以满足特殊应用需求。

小结 ​

ATT 与 GATT 层共同定义了 Bluetooth LE 连接后数据的标准化表示和高效交换机制。

  • ATT 提供底层的数据结构与传输协议;

  • GATT 则按照 Profile → Service → Characteristic 的层次结构,组织和管理数据。

正确理解 ATT/GATT,有助于开发者设计既易于扩展又标准兼容的 BLE 应用。

PHY:射频模式(Radio Modes) ​

PHY 层简介 ​

在 Bluetooth LE 协议栈的最底层是 物理层(PHY,Physical Layer)。PHY 层规定了蓝牙无线电的射频规范,包括不同的调制和编码方案。这些方案直接影响无线电的吞吐量、通信距离和设备的电池消耗。

1M PHY ​

  • 1M PHY(1 Megabit PHY)是所有 Bluetooth LE 设备都必须支持的基础物理层模式。

  • 顾名思义,1M PHY 的数据速率为 1 Mbps(1 兆比特每秒)。

  • 在两个 Bluetooth LE 设备刚建立连接时,默认使用 1M PHY。之后,如果双方都支持其他模式,可以协商切换。

2M PHY ​

  • 2M PHY(2 Megabit PHY)是在 Bluetooth 5.0 中引入的新模式。

  • 其数据速率提升至 2 Mbps,比 1M PHY 快一倍。

  • 高速率带来一个优势:数据更快传完,无线电开启时间更短,从而降低电池消耗。

  • 但代价是接收灵敏度降低,即通信距离缩短。

类比理解:2M PHY 就像用更快的语速说话,可以省时间,但听得清楚的距离变短了。

Coded PHY ​

  • 相对于 2M PHY 追求速度,Coded PHY 设计用于追求更远的通信距离,哪怕牺牲数据速率。

  • Coded PHY 采用特殊的编码方式来增强抗干扰和纠错能力——每个比特由多个符号表示。

  • 包含两种编码模式:

    • S=2 模式:2 个符号表示 1 个比特,实际数据速率为 500 kbps。

    • S=8 模式:8 个符号表示 1 个比特,数据速率降为 125 kbps。

  • 更低的数据速率换来更强的信号纠错能力和更远的通信距离。

类比理解:Coded PHY 就像慢慢地、重复地说话,即使在远处或嘈杂环境下,对方也能更容易听清楚。

小结 ​

Bluetooth LE 物理层(PHY)通过 1M、2M 和 Coded 多种模式,为不同应用场景提供了灵活的选择。开发者可以根据对速度、距离和能耗的需求,在不同 PHY 模式之间权衡取舍。

实验 1:测试 Bluetooth LE 连接 ​

实验目标 ​

本实验将使用 Nordic 开发板运行的 Bluetooth: Peripheral LBS 示例,通过与智能手机建立 Bluetooth LE 连接,实现外围设备与中心设备的基本交互。开发板作为 Peripheral 广播自身,手机作为 Central 扫描并连接。连接建立后,开发板作为 GATT 服务器,暴露 LED Button Service(LBS),手机作为 GATT 客户端,读取按钮状态并控制板载 LED。

步骤 1:构建并烧录 Peripheral LBS 示例程序 ​

1.1 在 Visual Studio Code 中,欢迎面板选择 “Browse samples”,搜索 “Bluetooth LE LED Button service”。 ​

选择该示例后,它会显示在 APPLICATIONS 面板下。

1.2 添加构建配置,选择你使用的开发板型号。 ​

1.3 构建并烧录应用到设备。 ​

烧录成功后,你应能看到开发板上的 LED1(nRF54 DKs 上为 LED0)闪烁,表示设备正在广播。

步骤 2:用手机测试应用 ​

2.1 从应用商店下载并启动 nRF Connect for Mobile。 ​

2.2 打开手机的蓝牙和定位服务。 ​

步骤 3(可选):通过串口终端查看日志输出 ​

开发板的 UART 外设通过 SEGGER 调试器/编程芯片(interface MCU)映射为 USB-CDC 虚拟串口,可直接在 PC 上连接。

在 VS Code 的 Connected Devices 面板下展开你的设备,选择对应的 COM 端口。端口号依据 PC 环境可能不同。

注意:nRF5340 DK 或 nRF54L15 DK 会有两个 COM 端口,分别对应应用核和网络核。需选择应用核的端口。

使用默认串口设置 115200 8n1 rtscrs:off,重启设备后查看完整日志输出。

成功广播时日志示例:

text
*** Booting nRF Connect SDK ***
Starting Bluetooth Peripheral LBS example
...
Bluetooth initialized
Advertising successfully started

LED1(nRF54 DKs 为 LED0)闪烁表示正在广播。

补充:nRF54 DKs 的 LED 和按钮标号从 0 开始(LED0-LED3, BUTTON0-BUTTON3),早期开发板为从 1 开始。

步骤 4:建立与 Nordic 开发板的连接 ​

4.1 在 nRF Connect for Mobile 应用中点击 SCAN,手机作为 Central 扫描周围蓝牙设备。 ​

4.2 选择名为 Nordic_LBS 的设备,点击 CONNECT。 ​

如有提示再次点击 Connect。

部分 Android 手机上,可能需先点击右上角菜单(三点),选择 “Bond”,然后再连接。

4.4 当 LED2(nRF54 DKs 上为 LED1)常亮时,表示连接建立成功。 ​

步骤 5:在手机应用中观察服务与特征 ​

  • 检查是否连接到正确设备。

  • 查看设备支持的服务,此处应显示 Nordic LED Button Service。

  • Service 下包含:

    • “Button” 特征(Blinky Button State):反映开发板按钮 1 状态,具有 “Read” 和 “Notify” 属性。

    • “LED” 特征(Blinky LED State):用于控制开发板的 LED3(nRF54 DKs 为 LED2),具有 “WRITE” 属性。

步骤 6:读取按钮 1 状态 ​

  • 打开 “Button” 特征的 “Value” 标签页(点击单下箭头)。

  • 若要接收按钮状态通知,点击多下箭头图标,开启通知。

  • 此时应能看到按钮 1(nRF54 DKs 为按钮 0)状态为 released。

步骤 7:观察按钮状态变化 ​

7.1 按下开发板上的按钮 1。 ​

7.2 应用中显示的按钮状态应变为 “Button pressed”。 ​

步骤 8:控制 LED3(nRF54 DKs 上为 LED2) ​

8.1 在 LED 特征旁点击上箭头图标。 ​

8.2 写入数值以点亮 LED ​

  • 选择 ON 并点击 SEND,点亮 LED3。

8.3 观察板上 LED3 被点亮。 ​

8.4 选择 OFF,关闭 LED3。 ​

BLE 广播 ​

广告是任何蓝牙低功耗连接的关键环节。理解不同的广告参数及其含义,并学会调整这些参数以实现应用目标(无论是低功耗、可靠性还是快速设备发现)至关重要。蓝牙低功耗中的广告主要有两个用途:向邻近设备广播数据,或是宣告自身存在以便其他设备与之连接。本课程首先解析广告流程,接着学习不同类型的广告方式以及广告数据包的结构。本课程仅涵盖传统广告模式。

广播流程详解 ​

广播与设备发现 ​

当 Bluetooth LE 设备处于广播状态时,会周期性地发送广播包,以宣布自身存在,并为可能的连接做准备。

这些广播包以一定的广播间隔定时发送。

广播间隔(Advertising Interval) ​

  • 定义:设备发送广播包的时间间隔。

  • 取值范围:20 ms 到 10.24 s,步进为 0.625 ms。

广播间隔越短,广播包发送越频繁,设备被扫描器发现的速度就越快(可发现性更高),但功耗也随之增加。相反,间隔越长,功耗更低,但被发现速度变慢。

为防止多个设备使用相同广播间隔时广播包总是碰撞,协议规定每个广播包前会随机增加 0~10 ms 的延迟。

广播信道 ​

Bluetooth LE 有 40 个射频信道,分为:

  • 3 个主广播信道(Primary Advertisement Channels):信道 37、38、39,主要用于广播。

  • 37 个次级信道(Secondary Advertisement Channels):主要用于建立连接后的数据传输,部分高级广告可用,但本课程只关注主广播信道。

广播包会在所有 3 个主广播信道上轮流发送,扫描设备也会轮询这 3 个信道寻找广播设备。

这 3 个信道虽然编号相邻,但在频谱上互不相邻,目的是减少相邻带干扰。同时,它们也避开了 ISM 频段内 Wi-Fi 等其他技术干扰最小的区域,提高广播可靠性。

扫描间隔与扫描窗口 ​

扫描器(如 central 设备)参数设置决定了它如何接收广播包:

  • 扫描间隔(Scan Interval):设备扫描广播包的周期(多久开始一次扫描)。

  • 扫描窗口(Scan Window):每个扫描间隔内实际用于扫描的时长。

  • 取值范围:2.5 ms 到 10.24 s,步进为 0.625 ms。

实际工作中,扫描器会在 3 个主广播信道间循环切换,每个扫描间隔切换一次信道。

广播间隔越短、扫描窗口/扫描间隔比值越大,设备发现时间越短,但功耗越高。扫描动作本身比广播更耗电,因此通常扫描器(central)由电池容量更大的设备承担(如手机)。

扫描请求与响应 ​

  • 在外围设备(peripheral)广播时,中心设备(central)可以发送扫描请求(Scan Request),请求获取更多未包含在广播包中的信息。

  • 如果外围设备接受请求,会通过扫描响应(Scan Response)返回附加数据,同样在 3 个主广播信道上发送。

这样,外围设备可以在不建立连接的情况下将更多数据提供给中心设备。如果没有额外信息,也可以发送空响应。

广播数据量扩展 ​

  • 若需进一步扩展广播数据量,可使用扩展广播(Extended Advertising)功能——主信道广播包指向次级信道上的补充信息。

  • 本课程只关注传统(legacy)广播,不涉及扩展广播。

小结 ​

Bluetooth LE 广播流程通过主广播信道、灵活的间隔参数及扫描机制,实现了低功耗、高可发现性的设备发现与连接流程。理解这些参数及其权衡,有助于优化设备的发现速度与功耗表现,提升实际应用体验。

广播类型 ​

广播类型的基本属性 ​

Bluetooth LE 外围设备(peripheral)可采用多种方式进行广播,每种广播方式都由特定属性组合决定:

  • Connectable(可连接)/Non-connectable(不可连接):中央设备(central)是否可以与外围设备建立连接。

  • Scannable(可扫描)/Non-scannable(不可扫描):外围设备是否接受扫描器发来的扫描请求。

  • Directed(定向)/Undirected(非定向):广播包是否专门针对某个特定扫描器发送。

广播属性的组合由广播包中的参数设定,在实际开发中可根据需求调整。

主要的传统(legacy)广播类型 ​

1. 可连接且可扫描(ADV_IND) ​

  • 特性:可连接、可扫描、非定向

  • 应用:最常用的广播类型。外围设备既允许 central 发起连接,又接受扫描请求并回复扫描响应。

  • 典型场景:绝大多数需要被手机等 central 发现、连接的外围设备。

2. 定向可连接(ADV_DIRECT_IND) ​

  • 特性:可连接、不可扫描、定向

  • 应用:发送给特定 central 的定向广播,不接受扫描请求。适用于已知 central 的快速重连。

  • 典型场景:如蓝牙鼠标与配对过的电脑断连后,需迅速恢复连接,无需等待扫描请求。

3. 不可连接但可扫描(ADV_SCAN_IND) ​

  • 特性:不可连接、可扫描、非定向

  • 应用:只接受扫描请求并返回扫描响应,但不允许建立连接。

  • 典型场景:只需向 central 提供额外信息,但不需要持续连接的设备。

4. 不可连接且不可扫描(ADV_NONCONN_IND) ​

  • 特性:不可连接、不可扫描、非定向

  • 应用:既不接受连接,也不接受扫描请求。

  • 典型场景:Beacon 等纯广播型设备,只推送数据,不接受任何外部数据,最大程度降低功耗。

广播类型属性总览表 ​

广播类型可连接可扫描定向
ADV_IND✔✔
ADV_DIRECT_IND✔✔
ADV_SCAN_IND✔
ADV_NONCONN_IND

蓝牙地址(Bluetooth Address) ​

基本概念 ​

每个 Bluetooth LE 设备都有一个唯一的 48 位地址。蓝牙地址分为两大类:公有地址(Public Address) 和 随机地址(Random Address)。随机地址又细分为静态(Static)和私有(Private),私有地址再划分为可解析(Resolvable)和不可解析(Non-resolvable)。下文将逐一介绍各类地址的特点与用途。

注意:随机与私有仅为分类方式,实际蓝牙地址类型只有下述几种。

蓝牙设备可用的地址类型 ​

  • 公有地址(Public Address)

  • 随机静态地址(Random Static Address)

  • 随机私有可解析地址(Resolvable Random Private Address)

  • 随机私有不可解析地址(Non-resolvable Random Private Address)

公有地址(Public Address) ​

  • 定义:由设备厂商烧录,全球唯一,并需向 IEEE 注册,类似以太网/Wi-Fi 的 MAC 地址。

  • 特性:设备终身不变,适用于需全球唯一身份识别的场景。

  • 成本:获取需付费。

随机地址(Random Address) ​

  • 定义:无需 IEEE 注册,可由用户手动设置。是实际开发和样例中最常用的地址类型。

  • 生成方式:可在出厂时写入,也可以由设备在运行时动态生成。

  • 细分为两种:

1. 随机静态地址(Random Static Address) ​

  • 特性:分配后终身固定(可在设备启动时更改,但运行中不可变),无需注册,低成本。

  • 应用:除公有地址外,所有 Bluetooth LE 设备必须采用随机静态地址之一,且实际应用中更常见。

2. 随机私有地址(Random Private Address) ​

  • 特性:为保护设备隐私而设计,地址定期变更,防止被跟踪。

  • 分为两类:

    • 可解析私有地址(Resolvable Random Private Address)

      • 利用预共享密钥(身份解析密钥,IRK)生成和解析,允许被授权设备识别真实身份。

      • 实际通信中,配对设备可用 IRK 将临时私有地址还原为真实 Bluetooth LE 地址(公有或静态)。

    • 不可解析私有地址(Non-resolvable Random Private Address)

      • 不可被外部设备解析,仅用于防止跟踪,实际中较少使用。

各类蓝牙地址总结 ​

地址类型特性与用途
公有地址(Public)由厂商烧录,全球唯一,需 IEEE 注册,终身不变,成本高
随机静态地址(Static)启动时配置,整个生命周期内固定,无需注册,最常用,低成本
可解析私有地址(Resolvable)周期性变化,可被授权设备用 IRK 解析,兼顾隐私与身份识别(可选)
不可解析私有地址(Non-resolvable)周期性变化,无法被识别,仅用于防跟踪,实际很少用(可选)

小结 ​

Bluetooth LE 设备的地址类型设计,兼顾唯一性、隐私和实际应用需求。实际开发中,随机静态地址和可解析私有地址因其高灵活性与隐私保护能力被广泛采用。公有地址则因唯一性和注册成本,主要用于对身份要求极高的场合。正确选择和管理蓝牙地址,是设备互联和安全的基础。

广播包结构(Advertisement Packet) ​

BLE 数据包基础结构 ​

Bluetooth LE 数据包由多个部分组成,最核心部分称为 协议数据单元(PDU, Protocol Data Unit)。PDU 根据用途分为两类:

  • 广播 PDU(Advertising PDU):用于广播过程,也称为广告信道 PDU。

  • 数据 PDU(Data PDU):用于连接后的数据传输,也称为数据信道 PDU。

本节主要关注广播 PDU 及其结构。

广播 PDU 结构 ​

广播 PDU 头部(Header) ​

广播 PDU 的头部包含以下字段:

  • PDU Type:指定广播类型(如 ADV_IND),参见前文“广播类型”。

  • RFU:保留位,留作将来扩展使用。

  • ChSel:若支持 LE 信道选择算法 #2,则此位为 1。

  • TxAdd(发射地址类型):指示 PDU 中的发射方地址是公有(0)还是随机(1)。

  • RxAdd(接收地址类型):指示 PDU 中的接收方地址是公有(0)还是随机(1)。

  • Length:负载长度,表示后续有效数据部分的字节数。

详细字段定义可查阅 Bluetooth SIG 官网的核心规范文档。

广播 PDU 负载(Payload) ​

负载部分分为两部分:

  • AdvA(6 字节):广播设备的蓝牙地址。

  • AdvData:实际的广播数据内容。

注意:针对不同广播类型(如定向广播 ADV_DIRECT_IND),负载结构也会调整。例如定向广播包需指定接收方地址(6 字节),此时 AdvData 区域被接收方地址替换,不包含其他数据。

广告数据结构(AdvData) ​

广告数据区域由一个或多个广告数据结构(AD Structure)组成。每个 AD Structure 包含:

  1. 长度字段(1 字节):本结构体总长度。

  2. 类型字段(AD Type)(1 字节):定义数据类型,如设备名称、服务 UUID、厂商数据等。

  3. 数据字段(AD Data):实际数据内容。

常见的 AD Type 仅 1 字节。

常见广告数据类型(AD Types) ​

  • 完整本地名称(BT_DATA_NAME_COMPLETE):即设备名,用户在手机上扫描时看到的名称。

  • 缩短本地名称(BT_DATA_NAME_SHORTENED):完整名称的缩略版。

  • 统一资源标识符(BT_DATA_URI):如网站地址等 URI 信息。

  • 服务 UUID(Service UUID):用于唯一标识某项服务,便于扫描器筛选目标设备。

  • 厂商特定数据(BT_DATA_MANUFACTURER_DATA):允许厂商自定义数据格式(如 iBeacon 协议)。

  • 标志(Flags):用于指示设备状态或功能的 1 位标志变量(封装在 1 字节内)。

广告标志(Flags) ​

标志类型用于描述设备的一些工作模式或特性,最多可以设置 8 个(1 字节 8 位)。常用标志包括:

  • BT_LE_AD_LIMITED:LE 限定可发现模式,配合可连接广播,表示设备只在有限时间内可被发现(如广告超时)。

  • BT_LE_AD_GENERAL:LE 一般可发现模式,配合可连接广播,表示设备长时间可被发现(广告超时时间为 0)。

  • BT_LE_AD_NO_BREDR:表明设备不支持传统蓝牙(BR/EDR),仅支持低功耗蓝牙(LE)。

BT_LE_AD_LIMITED 和 BT_LE_AD_GENERAL 常用于外围设备(peripheral)角色。

广告数据结构示例 ​

以下为设置 BT_LE_AD_NO_BREDR 标志的广告数据结构示例:

text
| Length | AD Type |   AD Data   |
|--------|---------|-------------|
|   2    |   0x01  |   0x04      |
  • Length: 2(1 字节 AD Type + 1 字节 AD Data)

  • AD Type: 0x01(Flags)

  • AD Data: 0x04(BT_LE_AD_NO_BREDR)

小结 ​

BLE 广播包结构采用灵活的“头部 + 负载”分层设计,负载部分可由多个类型的数据结构拼接而成。开发者可通过设置不同类型和内容的 AD Structure,实现丰富的设备标识、功能发布与自定义扩展,同时兼顾功耗与兼容性。

实验 1:设置广播数据 ​

实验目标 ​

本实验围绕 nRF Connect SDK 的核心 Bluetooth LE API,指导你如何配置硬件实现不可连接广播(Beacon 模式),让周围的扫描设备能够看到自定义的广播数据。

步骤 0:准备工程并烧录 ​

0.1 克隆课程 GitHub 仓库 ​

  • 访问仓库:https://github.com/NordicDeveloperAcademy/bt-fund

  • 选择与你的 nRF Connect SDK 版本对应的分支。

  • 使用 VS Code 的命令面板(View → Command Palette → Git Clone),粘贴仓库地址,建议将仓库克隆至根目录附近(如 C:\myfw\btfund),避免因路径过长导致构建失败。

0.2 打开实验基础代码 ​

  • 在 nRF Connect 扩展中,选择“Open an existing application”,打开 bt-fund/l2/l2_e1 目录中的工程。

步骤 1:包含 Bluetooth LE 协议栈 ​

  • 在 prj.conf 文件中启用 Bluetooth LE 协议栈(本实验已默认开启)。
text
CONFIG_BT=y
  • 默认配置要点包括:

    • 启用广播支持(BT_BROADCASTER)

    • 使用 SoftDevice Controller(BT_LL_SOFTDEVICE)

    • 发射功率设为 0 dBm

步骤 2:设置 Bluetooth LE 设备名称 ​

  • 在 prj.conf 中添加设备名(本地名称):
text
CONFIG_BT_DEVICE_NAME="Nordic_Beacon"

设备名建议尽量简短,因广播数据总长度仅 31 字节。

步骤 3:包含必要头文件 ​

在 main.c 顶部添加:

c
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/gap.h>

步骤 4:准备广播数据 ​

4.1 广播包 ​

4.1.1 声明广告数据数组 ad[]:

js
static const struct bt_data ad[] = {
    /* 4.1.2 设置广告标志 */
    /* 4.1.3 设置广告包数据 */
};

4.1.2 设置广告标志(Flags),仅包含 BT_LE_AD_NO_BREDR(不支持经典蓝牙):

text
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR),

4.1.3 设置完整本地名称(Complete Local Name):

text
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),

注意:广告包最大可用字节为 31,超出部分需用扫描响应补充。

4.2 扫描响应 ​

4.2.1 声明扫描响应数据数组 sd[]:

js
static const struct bt_data sd[] = {
    /* 4.2.3 包含 URL 数据 */
};

4.2.2 声明 URL 数据(以 URI Scheme Name Mapping 编码,节省空间):

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 在扫描响应包内加入 URI:

text
BT_DATA(BT_DATA_URI, url_data, sizeof(url_data)),

步骤 5:使能 Bluetooth LE 协议栈 ​

在 main() 中添加:

c
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_NCONN):

c
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;
}

BT_LE_ADV_NCONN:不可连接广播,广播间隔 100–150 ms。

步骤 7:构建并烧录程序到开发板 ​

构建并烧录后,板上的 LED1(nRF54 系列为 LED0)应闪烁,表示设备正在广播。

步骤 8:用 nRF Connect for Mobile 测试 ​

  • 打开手机上的 nRF Connect for Mobile。

  • 在 SCANNER 标签页点击 SCAN 按钮开始扫描。

步骤 9:查看广播内容 ​

  • 点击列表中的 Nordic_Beacon 查看详情。

  • 你会发现此设备没有 CONNECT 按钮,因为属于不可连接广播(Beacon)。

  • 广播数据内容包含:

    • RSSI(信号强度)

    • 广播间隔(100–150 ms)

    • 广播标志(不支持 BR/EDR)

    • 完整本地名称(Nordic_Beacon)

    • 扫描响应中的 URI(可点击 OPEN 直接在浏览器打开)

实验 2:自定义广播参数、厂商数据与动态更新广告内容 ​

实验目标 ​

本实验在上一实验基础上,深入学习如何自定义广播参数(如广播间隔)、添加厂商自定义数据(Manufacturer Specific Data),以及在运行时动态更新广播内容。设备依然以不可连接广播方式工作,但本次你将通过按钮控制,实时修改并广播自定义数据。

步骤 1:自定义广播参数 ​

在 main.c 中定义广播参数变量 adv_param,使用辅助宏 BT_LE_ADV_PARAM():

js
static const struct bt_le_adv_param *adv_param =
    BT_LE_ADV_PARAM(BT_LE_ADV_OPT_NONE, /* 不指定特殊选项 */
            800, /* 最小广播间隔:800*0.625ms = 500ms */
            801, /* 最大广播间隔:801*0.625ms = 500.625ms */
            NULL); /* 非定向广播,Peer 地址设为 NULL */

这样设置后,广播间隔大约为 500ms。协议栈会自动添加 0~10ms 的随机延迟,以减少碰撞。

步骤 2:声明厂商自定义数据(Manufacturer Specific Data) ​

2.1 定义公司标识符(Company ID) ​

c
#define COMPANY_ID_CODE 0x0059  /* Nordic Semiconductor Company ID */

注意:只有 Bluetooth SIG 成员可以合法广播自定义的 Company ID,正式产品需申请专属编号。

2.2 定义自定义数据结构 ​

c
typedef struct adv_mfg_data {
    uint16_t company_code;  /* 公司标识符 */
    uint16_t number_press;  /* 按钮按下次数 */
} adv_mfg_data_type;

2.3 初始化自定义数据变量 ​

c
static adv_mfg_data_type adv_mfg_data = { COMPANY_ID_CODE, 0x00 };

步骤 3:将厂商自定义数据加入广告包 ​

在广告包 ad[] 数组中添加如下条目:

c
BT_DATA(BT_DATA_MANUFACTURER_DATA, (unsigned char *)&adv_mfg_data, sizeof(adv_mfg_data)),

这样,广播包中就包含了 Nordic 公司 ID 及按钮按下次数。

步骤 4:初始化按键库与注册回调函数 ​

4.1 定义初始化函数 init_button() ​

c
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() 用于初始化开发板按键,并注册回调函数 button_changed。

4.2 在主函数 main() 中调用初始化 ​

c
err = init_button();
if (err) {
    printk("Button init failed (err %d)\n", err);
    return -1;
}

步骤 5:实现按键回调函数并动态更新广告数据 ​

c
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));  /* 动态刷新广播内容 */
    }
}
  • 检查目标按钮是否被按下(例如 USER_BUTTON 宏对应 Button 1 或 Button 0)。

  • 每次按下按钮,自定义数据中的计数加一。

  • 调用 bt_le_adv_update_data() 实时更新广播数据,无需重启广播。

步骤 6:编译并烧录程序 ​

烧录成功后,开发板上的 LED1(nRF54 系列为 LED0)应开始闪烁,表示设备正在以新的参数进行广播。

步骤 7:用 nRF Connect for Mobile 扫描观察 ​

  • 打开手机上的 nRF Connect for Mobile。

  • 进入 SCANNER 标签页,点击 SCAN 开始扫描周围设备。

步骤 8:查看广播内容 ​

  • 点击 “Nordic_Beacon” 设备,查看详细的广告数据。

  • 你会看到 Manufacturer Data 字段,包含 Nordic 的公司标识符。

  • 点击 Manufacturer Data,可切换显示为 Unsigned Int16 或 Bluetooth Core 4.1 格式,方便查看自定义数据。

步骤 9:动态验证 ​

  • 按下开发板上的 Button 1(nRF54 系列为 Button 0)。

  • 观察 Manufacturer Data 字段中计数值实时递增。

  • 若数据未立刻刷新,可在手机端重新点击 SCAN 刷新数据。

默认扫描时长为 45 秒,可在 nRF Connect for Mobile 设置中调整。

小结 ​

本实验实现了:

  • 精细控制广播参数(如自定义广播间隔)

  • 在广播包中添加并实时更新自定义厂商数据

  • 通过按键事件动态修改广播内容,实现交互式 Beacon 功能

通过本实验,你掌握了 BLE 广播参数设置、厂商扩展数据封装以及运行时动态数据更新的核心技能,为开发自定义物联网设备打下坚实基础。

实验 3:可连接广播、服务 UUID 与静态随机地址配置 ​

实验目标 ​

本实验将引导你从不可连接广播(Beacon)切换到可连接广播(Connectable Advertising),让外围设备(Peripheral)可被中央设备(Central)连接。同时,你将学会如何在广播数据中加入服务 UUID(以 LED Button Service 为例),以及手动设置设备的静态随机蓝牙地址。

步骤 1:启用外围设备支持 ​

在 prj.conf 文件中添加:

text
CONFIG_BT_PERIPHERAL=y

启用后,自动引入外围设备所需的 GATT/ATT 功能,默认最大连接数为 1(CONFIG_BT_MAX_CONN)。

步骤 2:修改设备名称 ​

将设备名从 "Nordic_Beacon" 改为 "Nordic_Peripheral":

text
CONFIG_BT_DEVICE_NAME="Nordic_Peripheral"

步骤 3:准备广播数据 ​

3.1 设置广播包 ​

在 ad[] 数组中,设置 Flags 和设备名称:

text
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),
  • BT_LE_AD_GENERAL:开启一般可发现模式

  • BT_LE_AD_NO_BREDR:仅支持 BLE,不支持传统蓝牙

3.2 扫描响应包加入 LBS 服务 UUID ​

  1. 包含 UUID 辅助宏头文件:
c
#include <zephyr/bluetooth/uuid.h>
  1. 在 sd[] 数组中加入 128 位 LBS 服务 UUID:
text
BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_128_ENCODE(0x00001523, 0x1212, 0xefde, 0x1523, 0x785feabcd123)),

这让中央设备可依据服务 UUID 快速筛选目标设备。

步骤 4:配置静态随机蓝牙地址 ​

4.1 包含地址管理头文件 ​

c
#include <zephyr/bluetooth/addr.h>

4.2 手动设置静态随机地址 ​

在 main.c 中添加如下代码(需在协议栈初始化前执行):

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);
}

该地址格式为:"FF:EE:DD:CC:BB:AA",类型为 random。手动设置静态随机地址后,设备每次启动时地址不会变化。

步骤 5:启动可连接广播 ​

5.1 创建可连接广播参数 ​

js
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, /* 最小广播间隔 500ms */
            801, /* 最大广播间隔 500.625ms */
            NULL); /* 非定向广播 */
  • BT_LE_ADV_OPT_CONN:可连接

  • BT_LE_ADV_OPT_USE_IDENTITY:使用已设置的身份地址

5.2 断开连接后自动恢复广播 ​

nRF Connect SDK 3.0.0 起,外围设备连接后广播会自动停止,断开后不会自动恢复。需监听 .recycled 事件并重新启动广播:

c
/* 用于恢复广播的工作处理函数 */
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 启动广播 ​

在 main() 中初始化工作队列并启动广播:

c
k_work_init(&adv_work, adv_work_handler);
advertising_start();

步骤 6:编译并烧录 ​

烧录程序后,板上 LED1(nRF54 系列为 LED0)将闪烁,表示设备正在广播。

步骤 7:使用 nRF Connect for Mobile 扫描 ​

  • 打开 nRF Connect for Mobile,点击 SCAN

  • 可在 Android 手机上看到设备地址为 FF:EE:DD:CC:BB:AA

  • 广播包中包含设备名、服务 UUID

  • 设备旁会出现 CONNECT 按钮,表明设备为可连接广播

总结 ​

本实验实现了:

  • 切换到外围设备可连接广播模式

  • 广播包中包含标志、设备名和服务 UUID,便于中央设备筛选

  • 手动配置设备的静态随机地址,保证设备身份一致性

  • 断开连接后自动恢复广播,提升用户体验

通过本实验,你已掌握 BLE 可连接广播的基础流程,以及如何在广播中发布特定服务和控制设备身份,为后续开发 BLE 外围设备打下坚实基础。

BLE 连接 ​

在蓝牙低功耗中实现双向通信最常见的方式是使用连接。此外,连接还能带来诸多优势,例如更高的数据吞吐量、处理数据包丢失的机制以及增强的安全性。

当建立蓝牙低功耗连接时,会显示一组连接参数。对于简单应用,默认连接参数可能完全适用,但您也可以调整这些参数以最适合您的应用,实现最低功耗或达到所需的吞吐量。本课程中,我们将探讨这些参数的作用以及如何调整它们。我们仅从外设角度介绍连接,并了解其含义和局限性。

连接过程(Connection Process) ​

广播与连接的关系 ​

在前面的课程中,我们学习了 Bluetooth LE 外围设备(peripheral)通过广播方式让中央设备(central)发现自己。最终,外围设备与中央设备建立连接,实现数据的双向通信。

连接的建立 ​

要建立一条 Bluetooth LE 连接,需要两类设备:

  • 外围设备:正在进行广播(尤其是可连接广播)

  • 中央设备:正在扫描周围设备

当中央设备扫描到外围设备的广播包后,会解析内容,并根据需要决定是否发起连接请求。如果决定连接,中央设备会发送连接请求(connection request)。此时,外围和中央正式建立面向连接的双向通信通道。

广播与连接请求时机 ​

外围设备在每次广播包之后,会保留一个很短的接收窗口(RX window),监听中央的连接请求。只要符合条件,外围设备就必须接受连接请求(除非使用了接收列表过滤器,仅允许特定中央设备连接)。一旦连接建立,外围设备可以随时主动断开连接。

类比:可连接广播就像“在门口挂招牌并定时开门”,中央设备则像“路过的访客”,看到招牌后决定是否进门敲门(发起连接)。

接收列表过滤器(Accept List Filter) ​

外围设备可以配置接收列表过滤器(以前称为白名单 whitelisting),限制只有特定的中央设备可以连接。这在安全或专用场景下非常有用。相关内容将在后续课程详细介绍。

连接建立后的数据传输 ​

连接建立后,设备将不再使用广播信道(37、38、39),而是切换到数据信道(0 到 36)。Bluetooth LE 采用信道跳变(channel hopping),即每次传输都随机选择不同的数据信道,减少干扰、提升吞吐量。

  • 数据包可靠性:每个数据包都需收到确认,否则将持续重传,直到收到确认或连接断开。

低功耗机制与连接间隔 ​

Bluetooth LE 连接能实现极低功耗的关键在于:大部分时间都在休眠。

  • 连接双方会约定一个连接间隔(connection interval),即每隔多长时间才唤醒一次(进行一次“连接事件”)。

  • 在连接事件(connection event)期间,双方交换数据,之后又进入休眠直至下一个间隔。

  • 如果没有数据要发,也要发送空包,以保证时钟同步。

概念定义 ​

  • 连接间隔(Connection interval):两端唤醒进行数据交换的时间间隔。

  • 连接事件(Connection event):每次唤醒时进行的一次通信。

连接间隔由中央设备在发起连接请求时初始设定,后续可以协商更改。如果数据量大,单个间隔内可交换多个数据包;若数据量超限,则需分多次连接事件完成。

断开连接(Disconnecting) ​

两个设备一旦连接,将一直保持连接,除非发生以下两种断开方式:

1. 应用主动断开 ​

任一设备可主动发送终止包(termination packet)断开连接,常见原因有:

  • 用户主动断开

  • 发现安全或协议异常(如加密验证失败)

  • 断开包中携带“断开原因”字段供分析

2. 监督超时断开(Supervision Timeout) ​

如果一方长时间未响应(如设备重启、掉电或超出无线范围),连接会因监督超时(supervision timeout)而被动断开。该超时时间可设定,后续章节将详细说明该参数。

小结 ​

Bluetooth LE 连接过程高度自动化且高效:

  1. 外围设备通过可连接广播暴露自身

  2. 中央设备扫描并发起连接请求

  3. 连接建立后,双方在数据信道上通过信道跳变和周期性唤醒,实现低功耗高可靠性通信

  4. 连接可通过主动指令或长时间无响应(监督超时)方式断开

掌握这些机制,有助于理解 BLE 设备的发现、连接与断开全流程。

连接参数(Connection Parameters) ​

连接参数概述 ​

当外围设备(Peripheral)和中央设备(Central)建立连接时,会协商一组连接参数。这些参数有的为兼容旧设备而采用标准初始值,有的则由中央设备在连接请求包中指定。主要连接参数包括:连接间隔、监督超时、外围延迟、PHY 无线模式、数据长度和 MTU 等。

主要连接参数详解 ​

连接间隔(Connection Interval) ​

  • 定义:连接双方约定多久“唤醒”一次进行数据交换。

  • 作用:大部分时间都处于睡眠状态,仅在连接事件时唤醒通信。这样可以极大地降低功耗。

  • 设置:由中央设备在连接请求包中指定,也可后续协商更改。应用开发者需根据需求平衡通信实时性与功耗。

监督超时(Supervision Timeout) ​

  • 定义:连接双方约定,若多长时间未收到对方数据包,即判定连接丢失并断开。

  • 典型场景:设备掉电、重启、超出通信范围等都会触发监督超时断开。

  • 设置:同样由中央设备指定。

外围延迟(Peripheral Latency) ​

  • 定义:允许外围设备在若干连接事件中不唤醒(即“跳过”),前提是没有数据要发送。

  • 意义:兼顾低延迟和超低功耗,尤其适用于鼠标、键盘等 HID 设备。这样既能保证响应速度,又能减少不必要的唤醒,降低电池消耗。

PHY 无线模式(PHY Radio Mode) ​

  • 默认值:1M PHY(1 Mbps),为兼容性考虑。

  • 可选模式(Bluetooth 5.0 引入):

    • 2M PHY:2 Mbps,更高传输速率,适合需要大带宽但可牺牲部分距离的场景。

    • Coded PHY:编码模式,降低速率(更远距离),适合长距离应用。

  • 切换时机:连接建立后任意设备可请求切换 PHY,双方协商最终模式。

数据长度与 MTU ​

  • 数据长度(Data Length):单个 BLE 数据包可携带的最大字节数,默认 27 字节。BLE 4.2 引入 DLE(Data Length Extension),允许扩展至 251 字节。

  • MTU(Maximum Transfer Unit):一次 GATT 操作可发送的最大字节数,默认 23 字节。

  • 关系:MTU 通常大于数据长度,实际传输时应用层数据会被分割为多个数据包传输。例如:

    • MTU = 140,数据长度 = 27,则一条 140 字节的信息会被拆分成多个包。
  • 数据包实际有效载荷:251 字节 Data PDU 包含 4 字节 L2CAP 头和 3 字节 Attribute 头,实际最多可发送 244 字节应用数据。

简单理解:MTU 决定应用层单次操作“可发多少字节”,数据长度决定“单个底层包能装多少字节”。MTU 或数据长度越大,意味着更高吞吐、更低功耗。

连接参数的协商与更新 ​

  • 连接间隔、监督超时、外围延迟:由中央设备主导,外围设备可以请求更改,但中央有最终决定权(如手机操作系统)。

  • PHY、数据长度、MTU:连接建立后,任意一端均可发起参数更新请求,对方可以同意、协商或拒绝。初始默认值为 1M PHY、数据长度 27、MTU 23。

  • 实际协商流程:

    • 连接初始时参数为默认值。

    • 如外围希望提升数据长度为 200,发起请求。

    • 中央回复支持 180,则双方最终协商为 180。

    • 后续也可多次更新。

发送大数据包的好处 ​

  • 减少空中包数量,降低无线占用与能耗。

  • 提升有效吞吐,更快完成数据交换。

  • 简化应用层逻辑,减少分包/重组负担。

小结 ​

Bluetooth LE 连接参数的设计,旨在兼顾兼容性、低功耗与高性能。理解并灵活调整这些参数,是开发高效、低功耗 BLE 应用的关键:

  1. 连接间隔与外围延迟:平衡功耗与响应速度

  2. 监督超时:确保连接可靠性

  3. PHY、数据长度、MTU:决定最大传输速率与数据包大小

  4. 参数协商机制:满足不同设备能力、应用场景的需求

合理设置这些参数,将极大提升 BLE 设备的用户体验和电池寿命。

与智能手机的 BLE 连接实践 ​

实验目标 ​

本实验将实现 Nordic 开发板(外围设备)与智能手机(中央设备)的连接,并通过回调函数追踪连接状态变化。同时,添加一个临时服务(LED Button Service, LBS),以实现连接建立后按键数据的实时通知。

步骤 1:包含连接事件处理头文件 ​

在 main.c 顶部添加:

c
#include <zephyr/bluetooth/conn.h>

步骤 2:设置连接/断开回调 ​

2.1 声明连接回调结构体 ​

c
struct bt_conn_cb connection_callbacks = {
    .connected    = on_connected,
    .disconnected = on_disconnected,
    .recycled     = on_recycled,
};
  • connected/disconnected:分别在建立/断开连接时回调。

  • recycled:连接对象回收时触发(通常表示断开后可重新广播)。

2.2 定义回调函数 ​

c
void on_connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        LOG_ERR("Connection error %d", err);
        return;
    }
    LOG_INF("Connected");
    my_conn = bt_conn_ref(conn);
    dk_set_led(CONNECTION_STATUS_LED, 1);   /* STEP 3.2 连接时点亮状态灯 */
}

void on_disconnected(struct bt_conn *conn, uint8_t reason)
{
    LOG_INF("Disconnected. Reason %d", reason);
    bt_conn_unref(my_conn);
    dk_set_led(CONNECTION_STATUS_LED, 0);   /* STEP 3.3 断开时熄灭状态灯 */
}

void on_recycled(void)
{
    advertising_start();    /* 断开后恢复广播 */
}

my_conn 用于追踪当前连接对象。

2.3 注册回调结构体 ​

在启动广播前注册回调:

c
err = bt_conn_cb_register(&connection_callbacks);
if (err) {
    LOG_ERR("Connection callback register failed (err %d)", err);
}

建议在启动广播前注册,避免漏掉连接事件。

步骤 3:配置 LED 指示连接状态 ​

3.1 定义连接状态指示灯 ​

在 main.c 顶部添加:

c
#define CONNECTION_STATUS_LED   DK_LED2

3.2/3.3 在回调中控制指示灯 ​

  • 连接时点亮:dk_set_led(CONNECTION_STATUS_LED, 1);

  • 断开时熄灭:dk_set_led(CONNECTION_STATUS_LED, 0);

步骤 4:编译并烧录程序 ​

烧录到开发板后,打开串口终端(115200 8n1,建议试用较小号的 COM 端口),重启设备,观察如下启动日志:

text
*** Booting nRF Connect SDK ***
*** Using Zephyr OS ***
[00:00:00.004,302] <inf> Lesson3_Exercise1: Starting Lesson 3 - Exercise 1
[00:00:00.007,659] <inf> Lesson3_Exercise1: Bluetooth initialized
[00:00:00.008,605] <inf> Lesson3_Exercise1: Advertising successfully started

步骤 5:使用手机连接开发板 ​

  • 打开 nRF Connect for Mobile,扫描并连接 "Nordic_Peripheral"。

  • 成功连接后,开发板指示灯点亮,终端输出如下:

text
[00:00:26.831,720] <inf> Lesson3_Exercise1: Connected
  • 断开连接(手机端点击 Disconnect),终端输出:
text
[00:00:38.627,004] <inf> Lesson3_Exercise1: Disconnected. Reason 19
  • 断开原因码可在 hci_types.h 中查询。例如,19 (0x13) 代表远程终端主动断开,8 (0x08) 代表监督超时。

步骤 6:添加 LED Button Service 实现数据交互 ​

6.1 在 prj.conf 添加 LBS 支持 ​

text
CONFIG_BT_LBS=y
CONFIG_BT_LBS_POLL_BUTTON=y
  • 第一句启用服务,第二句允许手动读取按钮值。

6.2 包含 LBS 服务头文件 ​

在 main.c 顶部添加:

c
#include <bluetooth/services/lbs.h>

6.3 设置按钮按下回调 ​

c
static void button_changed(uint32_t button_state, uint32_t has_changed)
{
    int err;
    bool user_button_changed = (has_changed & USER_BUTTON) ? true : false;
    bool user_button_pressed = (button_state & USER_BUTTON) ? true : false;
    if (user_button_changed) {
        LOG_INF("Button %s", (user_button_pressed ? "pressed" : "released"));
        err = bt_lbs_send_button_state(user_button_pressed);
        if (err) {
            LOG_ERR("Couldn't send notification. (err: %d)", err);
        }
    }
}

6.4 在 init_button() 内注册回调 ​

c
err = dk_buttons_init(button_changed);
if (err) {
    LOG_ERR("Cannot init buttons (err: %d)", err);
}

步骤 7:编译、烧录并连接测试 ​

  • 烧录后用手机连接。

  • 在 nRF Connect 的 Client(Cli…)标签页中找到 "Nordic LED and Button Service",展开后可以:

    • 单箭头下:读取一次按钮状态

    • 多箭头下:订阅按钮状态变化的通知

  • 按下开发板的 Button 1(nRF54 系列为 Button 0),Value 字段会在 “Button Pressed”/“Button Released” 间切换。

注意:只有手机端已订阅通知时,按键事件才能成功推送。否则会报错 Couldn't send notification. err: -13。

小结 ​

本实验实现了:

  1. 外围设备与中央设备的完整连接流程

  2. 通过回调函数追踪连接建立与断开,并用 LED 实时指示状态

  3. 利用 LBS 服务,实现按键数据的通知推送,为 BLE 数据交互打下基础

后续课程将进一步讲解 BLE 服务与通知等高级用法。

动态更新连接参数实验 ​

实验目标 ​

本实验将在上一节的基础上,详细观察与动态调整 Bluetooth LE 连接参数,包括:连接间隔、监督超时、PHY、数据长度和 MTU。通过 log 输出实时追踪这些参数的协商和变更过程,帮助你理解 BLE 连接的灵活性和优化空间。

步骤 1:获取当前连接参数 ​

1.1 获取并存储连接参数 ​

在 on_connected() 回调内声明并填充 bt_conn_info 结构体:

c
struct bt_conn_info info;
err = bt_conn_get_info(conn, &info);
if (err) {
    LOG_ERR("bt_conn_get_info() returned %d", err);
    return;
}

1.2 日志输出主要参数(单位换算) ​

c
double connection_interval = info.le.interval * 1.25; // ms
uint16_t supervision_timeout = info.le.timeout * 10;  // ms
LOG_INF("Connection parameters: interval %.2f ms, latency %d intervals, timeout %d ms",
    connection_interval, info.le.latency, supervision_timeout);

注意:CONFIG_FPU=y 已在 prj.conf 启用,支持 log 输出浮点数。

步骤 2:编译、烧录并连接 ​

烧录后用手机连接,串口日志会显示如下参数(以实际为准):

text
[00:00:03.989,349] <inf> Lesson3_Exercise2: Connected
[00:00:03.989,379] <inf> Lesson3_Exercise2: Connection parameters: interval 30.00 ms, latency 0 intervals, timeout 240 ms

步骤 3:监听连接参数更新 ​

3.1 在回调结构体中添加 le_param_updated 回调 ​

text
.le_param_updated = on_le_param_updated,

3.2 实现 on_le_param_updated() 回调 ​

c
void on_le_param_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency, uint16_t timeout)
{
    double connection_interval = interval * 1.25; // ms
    uint16_t supervision_timeout = timeout * 10;  // ms
    LOG_INF("Connection parameters updated: interval %.2f ms, latency %d intervals, timeout %d ms",
        connection_interval, latency, supervision_timeout);
}
  • 连接参数可能在连接后几秒内被自动更新(由外围设备的首选参数触发)。

  • 默认首选参数(可在 Kconfig.gatt 查到):

    • CONFIG_BT_PERIPHERAL_PREF_MIN_INT=24(30ms)

    • CONFIG_BT_PERIPHERAL_PREF_MAX_INT=40(50ms)

    • CONFIG_BT_PERIPHERAL_PREF_LATENCY=0

    • CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=42(420ms)

步骤 4:自定义首选连接参数 ​

在 prj.conf 增加如下内容:

text
CONFIG_BT_PERIPHERAL_PREF_MIN_INT=800
CONFIG_BT_PERIPHERAL_PREF_MAX_INT=800
CONFIG_BT_PERIPHERAL_PREF_LATENCY=0
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400
CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=y
  • 这样设置后,首选连接间隔为 1 秒,监督超时为 4 秒,外围延迟为 0。

  • 中央设备(如手机)有最终决定权,实际参数可能略有出入。

步骤 5:切换 PHY(无线模式) ​

5.1 实现 update_phy() 函数,将 PHY 设置为 2M ​

js
static void update_phy(struct bt_conn *conn)
{
    int err;
    const struct bt_conn_le_phy_param preferred_phy = {
        .options = BT_CONN_LE_PHY_OPT_NONE,
        .pref_rx_phy = BT_GAP_LE_PHY_2M,
        .pref_tx_phy = BT_GAP_LE_PHY_2M,
    };
    err = bt_conn_le_phy_update(conn, &preferred_phy);
    if (err) {
        LOG_ERR("bt_conn_le_phy_update() returned %d", err);
    }
}
  • 在 on_connected() 末尾调用 update_phy(my_conn);。

5.2 监听 PHY 变更 ​

prj.conf 增加:

text
CONFIG_BT_USER_PHY_UPDATE=y

回调结构体添加:

text
.le_phy_updated = on_le_phy_updated,

实现回调:

c
void on_le_phy_updated(struct bt_conn *conn, struct bt_conn_le_phy_info *param)
{
    if (param->tx_phy == BT_CONN_LE_TX_POWER_PHY_1M) {
        LOG_INF("PHY updated. New PHY: 1M");
    } else if (param->tx_phy == BT_CONN_LE_TX_POWER_PHY_2M) {
        LOG_INF("PHY updated. New PHY: 2M");
    } else if (param->tx_phy == BT_CONN_LE_TX_POWER_PHY_CODED_S8) {
        LOG_INF("PHY updated. New PHY: Long Range");
    }
}
  • 连接成功后,日志会提示当前 PHY 状态。

步骤 6:设置数据长度扩展与 MTU ​

6.1 数据长度扩展 ​

prj.conf 增加:

text
CONFIG_BT_USER_DATA_LEN_UPDATE=y
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
CONFIG_BT_BUF_ACL_RX_SIZE=251
CONFIG_BT_BUF_ACL_TX_SIZE=251
CONFIG_BT_L2CAP_TX_MTU=247

6.2 实现 update_data_length() 函数 ​

c
static void update_data_length(struct bt_conn *conn)
{
    int err;
    struct bt_conn_le_data_len_param my_data_len = {
        .tx_max_len = BT_GAP_DATA_LEN_MAX,
        .tx_max_time = BT_GAP_DATA_TIME_MAX,
    };
    err = bt_conn_le_data_len_update(my_conn, &my_data_len);
    if (err) {
        LOG_ERR("data_len_update failed (err %d)", err);
    }
}

6.3 实现 update_mtu() 函数及相关声明 ​

c
static struct bt_gatt_exchange_params exchange_params;

static void exchange_func(struct bt_conn *conn, uint8_t att_err,
                          struct bt_gatt_exchange_params *params)
{
    LOG_INF("MTU exchange %s", att_err == 0 ? "successful" : "failed");
    if (!att_err) {
        uint16_t payload_mtu = bt_gatt_get_mtu(conn) - 3;
        LOG_INF("New MTU: %d bytes", payload_mtu);
    }
}

static void update_mtu(struct bt_conn *conn)
{
    int err;
    exchange_params.func = exchange_func;

    err = bt_gatt_exchange_mtu(conn, &exchange_params);
    if (err) {
        LOG_ERR("bt_gatt_exchange_mtu failed (err %d)", err);
    }
}

6.4 回调结构体添加数据长度更新回调 ​

text
.le_data_len_updated = on_le_data_len_updated,

实现:

c
void on_le_data_len_updated(struct bt_conn *conn, struct bt_conn_le_data_len_info *info)
{
    uint16_t tx_len  = info->tx_max_len;
    uint16_t tx_time = info->tx_max_time;
    uint16_t rx_len  = info->rx_max_len;
    uint16_t rx_time = info->rx_max_time;
    LOG_INF("Data length updated. Length %d/%d bytes, time %d/%d us", tx_len, rx_len, tx_time, rx_time);
}

6.5 在 on_connected() 中依次调用参数更新函数 ​

c
k_sleep(K_MSEC(1000));  // 延迟避免冲突
update_data_length(my_conn);
update_mtu(my_conn);

步骤 7:编译并测试 ​

烧录后,连接开发板,串口日志将依次输出:

text
<inf> Lesson3_Exercise2: Bluetooth initialized
<inf> Lesson3_Exercise2: Advertising successfully started
<inf> Lesson3_Exercise2: Connected
<inf> Lesson3_Exercise2: Connection parameters: interval 30.00 ms, latency 0 intervals, timeout 240 ms
<inf> Lesson3_Exercise2: Data length updated. Length 251/251, time 2120/2120
<inf> Lesson3_Exercise2: PHY updated. New PHY: 2M
<inf> Lesson3_Exercise2: MTU exchange successful
<inf> Lesson3_Exercise2: New MTU: 244 bytes
<inf> Lesson3_Exercise2: Connection parameters updated: interval 1005.00 ms, latency 0 intervals, timeout 4000 ms

每个参数的变化都能在 log 中实时看到。

小结 ​

本实验实现了:

  • 读取并实时 log 输出连接参数(间隔、延迟、超时)

  • 动态请求和监听连接参数、PHY、数据长度、MTU 的更新

  • 通过 prj.conf 优化连接参数,体验 BLE 连接灵活调整的机制

通过实践,你将理解 BLE 链路参数对功耗、延迟和吞吐量的影响,并掌握在 Zephyr/nRF Connect SDK 中动态调优这些关键参数的方法。

低功耗蓝牙数据传输 ​

本课程将深入讲解通用属性配置文件(GATT)及其底层属性协议(ATT)。我们将学习如何使用不同的 GATT 操作在两个连接的蓝牙低功耗设备之间表示和交换数据。同时还将探讨蓝牙低功耗中的重要概念,如服务和特征。

GATT 操作详解 ​

GATT 基本结构与服务发现 ​

在 Bluetooth LE 协议栈中,GATT 层负责定义服务(Service)和特征(Characteristic),它们由一系列属性(Attribute)组成并保存在 GATT 服务器上。设备间的数据交换就是围绕这些属性进行的,不同的操作方式取决于属性的权限和应用需求。

服务发现(Service Discovery) ​

  • 定义:服务发现是 GATT 客户端在连接后,通过遍历 GATT 服务器的属性表,获知其支持的服务和特征的过程。

  • 作用:只有在服务发现后,客户端才能明确地知道可以与哪些服务/特征交互,进而发起后续的数据操作。

数据访问方式 ​

Bluetooth LE GATT 通信遵循典型的客户端-服务器架构:

  • 服务器(GATT Server):持有数据(属性),并可主动推送或被动响应数据访问请求。

  • 客户端(GATT Client):主动发起数据请求,也可接收服务器的主动推送。

GATT 操作分为两大类:

  1. 客户端发起的操作(Client-initiated)

  2. 服务器发起的操作(Server-initiated)

客户端发起的操作 ​

1. 读取(Read) ​
  • 流程:客户端发送读取请求(Read Request),服务器返回指定属性的值。

  • 典型场景:手机 App 读取传感器数值。

2. 写入(Write) ​
  • 流程:客户端发送写入请求(Write Request),附带与目标属性格式一致的数据。

  • 响应:服务器收到后,若接受写入,将返回确认(acknowledgement)。

  • 典型场景:手机 App 控制灯光开关。

3. 无响应写入(Write Without Response) ​
  • 流程:客户端写入数据后不等待服务器确认。

  • 适用场景:对时延和带宽要求高、可容忍丢包的场合,比如快速数据流。

服务器发起的操作 ​

服务器也可以主动推送属性数值给客户端,这属于“服务器发起”操作,分为两种:

1. 通知(Notify) ​
  • 特征:服务器主动推送属性值变更,无需客户端请求,也无需客户端回复确认。

  • 应用:如传感器数据实时更新,服务器主动通知客户端最新数值。

  • 性能:可以高频率推送。

2. 指示(Indicate) ​
  • 特征:与通知类似,但需要客户端回复确认。

  • 限制:每个连接间隔仅允许一次指示操作,速度较通知慢,但可靠性更高。

  • 应用:适合关键数据传递,要求客户端必须收到。

类比:Notify 像广播通知(发完即走),Indicate 像挂号信(必须收件人签收)。

使能服务器推送(通知/指示) ​

  • 尽管通知/指示是服务器主动发起,但客户端必须先订阅(Subscribe),即明确表示对该特征的通知或指示感兴趣。

  • 订阅方法涉及“客户端特征配置描述符(CCCD)”,将在后续章节详细说明。

服务与特征(Services and Characteristics) ​

ATT 层与属性(Attribute) ​

ATT(Attribute Protocol)层定义了 BLE 服务器中数据的存储与访问方式。属性(Attribute)是 ATT 和 GATT 层的基本数据单元。所有的数据交换(包括服务和特征的组织、读写等)最终都以属性为载体。

属性结构(Attribute Structure) ​

每个属性包含以下四个部分:

  1. Handle(句柄)16 位唯一索引,用于在属性表中定位该属性(类似表格中的行号,但句柄不一定连续)。

  2. Type(类型/UUID)

  • 用 UUID 标识属性类型,例如声明服务用 0x2800,声明特征用 0x2803。

  • UUID 有两种:

  1. 16 位 SIG 标准 UUID(如 0x180D 为心率服务)

  2. 128 位自定义 UUID(如 4A98-xxxx-1CC4-E7C1-C757-F1267DD021E8,便于厂商扩展)

  • Permissions(权限)

  • 指定访问该属性所需的安全级别(如“只读”“可写”“需加密”等)。

  1. Value(值)
  • 实际存储的数据(如传感器数值、字符串等),也可存放关于其他属性的元信息。

服务(Service)与特征(Characteristic) ​

GATT 层在 ATT 基础上,定义了“服务-特征”分层结构,使 BLE 设备之间可以标准化、互操作地交互数据。

服务(Service) ​

  • 定义:由一组有序属性组成的集合,描述某类功能(如心率、温湿度等)。

  • 开头:每个服务都以服务声明属性(UUID: 0x2800)开头。

    • 其 Value 字段存放被声明服务的 UUID(如 0x180D 代表心率服务)。

    • 权限通常为只读,无需认证或加密。

服务声明属性举例 ​
HandleType (UUID)PermissionsValue
0x00200x2800只读0x180D

特征(Characteristic) ​

  • 定义:服务下的功能单元,代表一个具体数据点(如电池电量、按键状态)。

  • 组成:每个特征由至少两个属性组成:

  1. 特征声明属性(UUID: 0x2803)
  • 声明了该特征的属性,如支持的操作(可读/可写/可通知等)、数据句柄、特征自身的 UUID。

  • 只读权限。

  1. 特征值属性
  • 存实际数据(如传感器数值)。

  • 权限由业务需求决定(如只读、可写等)。

  1. 可选:特征描述符
  • 存放额外元数据(如单位、说明),也可用于控制客户端行为(见下文 CCCD)。

特征声明属性内容详解 ​
  • Value 字段结构(3 部分):

    • 特征属性(Properties):支持哪些 GATT 操作(如 Read/Write/Notify/Indicate)

    • 特征值句柄:指向实际存储数据的属性句柄

    • 特征 UUID:本特征的唯一标识

特征描述符(Descriptors) ​
  • 作用:为特征提供补充说明或配置参数。

  • 类型:分为 GATT 标准描述符与自定义描述符。

  • 最常用:客户端特征配置描述符(CCCD,UUID: 0x2902)

客户端特征配置描述符(CCCD) ​

  • 定义:当特征支持服务器主动推送(Notify/Indicate)时,CCCD 允许客户端订阅或取消订阅这些功能。

  • 权限:必须可读可写。

  • Value 字段:仅用 2 bit

    • 第 0 位:通知(Notify)使能

    • 第 1 位:指示(Indicate)使能

  • 应用流程:如心率服务,手机通过写入 CCCD 使能通知后,无需轮询即可实时收到心率更新。

CCCD 属性结构举例 ​
HandleType (UUID)PermissionsValue(2 bit)
0x00250x2902读/写0b00000011

总结 ​

  • 属性(Attribute)是最底层的存储和交互单元,包含句柄、类型、权限和实际值。

  • 服务(Service)是功能的集合,以服务声明属性(UUID 0x2800)开头,包含若干特征。

  • 特征(Characteristic)是具体的数据点,由声明属性、值属性以及可选描述符组成。

  • 描述符(Descriptor)扩展特征元数据,最常用的是 CCCD,用于控制通知/指示。

  • UUID 是唯一标识,分为 16 位标准和 128 位自定义两类。

  • CCCD 是 BLE 通知/指示机制的关键,客户端通过写入该描述符订阅服务器推送。

理解服务和特征的分层结构,是 BLE 设备互操作和应用开发的基础。

属性表结构解析 ​

属性表(Attribute Table)概述 ​

GATT 服务器通过属性表(Attribute Table)存储和管理所有属性。属性表是一张有序列表,每一行就是一个属性,内容包括:句柄、类型(UUID)、权限和值。通过属性表可以清晰地看到服务、特征及其关系。

下面以一个自定义服务 my_lbs 为例,分析其属性表结构。该服务包含三个特征:

  • Button 特征

  • LED 特征

  • MySensor 特征

服务声明(Service Declaration) ​

属性表的第一行为服务声明属性:

  • Type=0x2800:表明这是一个服务声明。

  • Permissions:只读,无需认证。

  • Value:待声明服务的 UUID(如自定义服务 0x1234)。

Button 特征定义 ​

1. 特征声明属性(Declaration) ​

  • Type=0x2803:声明新特征。

  • Permissions:只读。

  • Value:

    • 属性位(如支持 Indicate)

    • 指向下一个“特征值属性”的句柄

    • 该特征的 UUID

2. 特征值属性(Value) ​

HandleType (UUID)PermissionsValue(实际数据)
0x00030x…Read按钮状态(如 0/1)
  • Type:特征的 UUID

  • Permissions:只读

  • Value:实际数据(如按钮是否按下)

3. CCCD(Client Characteristic Configuration Descriptor) ​

HandleType (UUID)PermissionsValue(bit)
0x00040x2902Read/Write0b10
  • Type=0x2902:CCCD

  • Permissions:可读可写

  • Value:Indicate 使能(bit1=1,bit0=0)

说明:CCCD 用于客户端使能 Indicate 功能。虽然 CCCD 没有直接被特征声明引用,但它紧跟在特征下,中央设备可通过句柄归属关系识别。

LED 特征定义 ​

1. 特征声明属性 ​

HandleType (UUID)PermissionsValue(属性、句柄、UUID)
0x00050x2803Read属性位、值属性句柄、UUID
  • 支持 Write 操作(属性位设置)

2. 特征值属性 ​

HandleType (UUID)PermissionsValue(实际数据)
0x00060x…WriteLED 状态(如 0/1)
  • 无 CCCD:只支持写入,不支持 Notify/Indicate。

MySensor 特征定义 ​

1. 特征声明属性 ​

HandleType (UUID)PermissionsValue(属性、句柄、UUID)
0x00070x2803Read属性位、值属性句柄、UUID
  • 支持 Notify 操作(属性位设置)

2. 特征值属性 ​

HandleType (UUID)PermissionsValue(实际数据)
0x00080x…Read传感器数据

3. CCCD ​

HandleType (UUID)PermissionsValue(bit)
0x00090x2902Read/Write0x01
  • Value=0x01:Notify 使能

属性归属和次序规则 ​

  • 每次遇到新特征声明(Type=0x2803),就开始归属于新的特征。

  • 一个特征的所有相关属性(包括 CCCD)都列在该声明之后,直到下一个特征声明出现。

总结 ​

  • 服务声明属性(Type=0x2800)定义服务起点。

  • 特征声明属性(Type=0x2803)每次出现表示新特征开始,描述支持的操作/UUID/值的句柄。

  • 特征值属性存储实际业务数据。

  • CCCD(Type=0x2902)用于客户端使能 Notify/Indicate。

  • 属性表顺序决定了归属关系,中央设备通过句柄顺序解析各特征结构。

熟悉属性表的结构与顺序,有助于你调试、设计和理解 BLE 服务的底层实现。

实验 1:自定义 BLE 服务与特征的创建 ​

实验目标 ​

本实验将教你如何在 nRF Connect SDK(基于 Zephyr RTOS)中创建自定义 BLE 服务和特征。我们将实现一个名为 my_lbs 的自定义 LED Button Service,包括两个特征:一个可读的 Button 特征和一个可写的 LED 特征,练习 GATT 的 Read 和 Write 操作。

步骤 1:定义 128 位 UUID ​

在 my_lbs.h 文件中添加如下代码:

c
/** @brief LBS Service UUID. */
#define BT_UUID_LBS_VAL \
    BT_UUID_128_ENCODE(0x00001523, 0x1212, 0xefde, 0x1523, 0x785feabcd123)

/** @brief Button Characteristic UUID. */
#define BT_UUID_LBS_BUTTON_VAL \
    BT_UUID_128_ENCODE(0x00001524, 0x1212, 0xefde, 0x1523, 0x785feabcd123)

/** @brief LED Characteristic UUID. */
#define BT_UUID_LBS_LED_VAL \
    BT_UUID_128_ENCODE(0x00001525, 0x1212, 0xefde, 0x1523, 0x785feabcd123)

#define BT_UUID_LBS         BT_UUID_DECLARE_128(BT_UUID_LBS_VAL)
#define BT_UUID_LBS_BUTTON  BT_UUID_DECLARE_128(BT_UUID_LBS_BUTTON_VAL)
#define BT_UUID_LBS_LED     BT_UUID_DECLARE_128(BT_UUID_LBS_LED_VAL)
  • 采用一组 base UUID,前三位依次自增,分别表示服务、Button 特征和 LED 特征。

步骤 2:用宏定义服务并添加到属性表 ​

在 my_lbs.c 中,使用 BT_GATT_SERVICE_DEFINE() 静态注册服务:

text
BT_GATT_SERVICE_DEFINE(my_lbs_svc,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_LBS),
    /* STEP 3 - 添加 Button 特征 */
    /* STEP 4 - 添加 LED 特征 */
);

步骤 3:添加 Button 特征 ​

在服务定义中插入如下代码:

text
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_BUTTON,
                       BT_GATT_CHRC_READ,
                       BT_GATT_PERM_READ,
                       read_button, NULL, &button_state),
  • 只支持 Read 操作,回调函数为 read_button,实际数据通过 button_state 传递。

步骤 4:添加 LED 特征 ​

在服务定义中插入如下代码:

text
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_LED,
                       BT_GATT_CHRC_WRITE,
                       BT_GATT_PERM_WRITE,
                       NULL, write_led, NULL),
  • 只支持 Write 操作,写回调为 write_led,数据由客户端写入。

步骤 5:实现 Button 特征的读回调 ​

在 my_lbs.c 实现如下:

js
static ssize_t read_button(struct bt_conn *conn,
                          const struct bt_gatt_attr *attr,
                          void *buf,
                          uint16_t len,
                          uint16_t offset)
{
    // 获取 button_state 数据指针
    const char *value = attr->user_data;

    LOG_DBG("Attribute read, handle: %u, conn: %p", attr->handle, (void *)conn);

    if (lbs_cb.button_cb) {
        // 调用应用层回调,获取当前按钮状态
        button_state = lbs_cb.button_cb();
        return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(*value));
    }

    return 0;
}

步骤 6:实现 LED 特征的写回调 ​

在 my_lbs.c 实现如下:

js
static ssize_t write_led(struct bt_conn *conn,
                         const struct bt_gatt_attr *attr,
                         const void *buf,
                         uint16_t len, uint16_t offset, uint8_t flags)
{
    LOG_DBG("Attribute write, handle: %u, conn: %p", attr->handle, (void *)conn);

    if (len != 1U) {
        LOG_DBG("Write led: Incorrect data length");
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
    }

    if (offset != 0) {
        LOG_DBG("Write led: Incorrect data offset");
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }

    if (lbs_cb.led_cb) {
        uint8_t val = *((uint8_t *)buf);
        if (val == 0x00 || val == 0x01) {
            lbs_cb.led_cb(val ? true : false);
        } else {
            LOG_DBG("Write led: Incorrect value");
            return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
        }
    }

    return len;
}

步骤 7:在主程序包含头文件 ​

在 main.c 顶部添加:

c
#include "my_lbs.h"

步骤 8:LED 控制逻辑 ​

8.1 定义 LED3 ​

c
#define USER_LED DK_LED3

8.2 实现 LED 控制回调 ​

c
static void app_led_cb(bool led_state)
{
    dk_set_led(USER_LED, led_state);
}

步骤 9:按钮状态监控 ​

9.1 定义 Button 1 ​

c
#define USER_BUTTON DK_BTN1_MSK

9.2 实现按钮读取回调 ​

c
static bool app_button_cb(void)
{
    return app_button_state;
}

app_button_state 由 button_changed() 更新,全局变量。

步骤 10:声明应用回调结构 ​

c
static struct my_lbs_cb app_callbacks = {
    .led_cb    = app_led_cb,
    .button_cb = app_button_cb,
};

步骤 11:注册应用回调 ​

在主程序初始化时调用:

c
err = my_lbs_init(&app_callbacks);
if (err) {
    printk("Failed to init LBS (err:%d)\n", err);
    return -1;
}

步骤 12:编译、烧录与初步验证 ​

烧录程序,板载 LED1(或 nRF54L15 DK 上的 LED0)应闪烁,表示设备正在广播。

步骤 13:用手机连接并测试服务 ​

  • 打开 nRF Connect for Mobile,连接名为 “MY_LBS1” 的设备。

  • App 会自动识别服务和特征(Button 可读,LED 可写)。

步骤 14:用 App 控制 LED ​

  • 在 LED 特征右侧点击写入箭头,弹窗选择 ON(发送 0x01)或 OFF(发送 0x00),可实时控制板载 LED3。

步骤 15:读取按钮状态 ​

  • 按住开发板上的 Button 1,同时在 Button 特征右侧点击读取箭头。

  • App 会显示 Button released 或 Button pressed(底层实际是 0x00/0x01)。

小结 ​

本实验实现了一个完整的自定义 BLE 服务 my_lbs,包含两个基础特征:

  • Button 特征:客户端可读,实时反映按钮状态。

  • LED 特征:客户端可写,远程控制板载 LED。

本例重点演示了 GATT 的 Read/Write 基础操作,为后续实现 Notify/Indicate 等复杂交互打下基础。

实验 2:添加 Notification 和 Indication 支持 ​

实验目标 ​

本实验在自定义服务的基础上,分两部分实现 BLE 服务器主动推送数据的两种方式:

  1. Button 特征支持 Indicate(指示),每次按钮状态变化时向客户端发送指示信息。

  2. 新增 MYSENSOR 特征,仅支持 Notify(通知),周期性推送模拟传感器数据。

第一部分:Button 特征支持 Indicate ​

1. 修改 Button 特征声明,支持 Indicate ​

在 my_lbs.c 中,将特征声明属性的属性位设置为可读 + 支持 Indicate:

text
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_BUTTON,
                       BT_GATT_CHRC_READ | BT_GATT_CHRC_INDICATE,
                       BT_GATT_PERM_READ, read_button, NULL,
                       &button_state),

2. 添加 CCCD(Client Characteristic Configuration Descriptor) ​

在 Button 特征定义下方添加 CCCD,用于客户端订阅 Indicate:

text
BT_GATT_CCC(mylbsbc_ccc_cfg_changed,
            BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),

3. 实现 CCCD 配置变更回调 ​

该回调用于检测客户端是否使能了 Indicate 功能:

js
static void mylbsbc_ccc_cfg_changed(const struct bt_gatt_attr *attr,
                                    uint16_t value)
{
    indicate_enabled = (value == BT_GATT_CCC_INDICATE);
}

4. 定义 Indication 参数 ​

在 my_lbs.c 顶部定义:

text
static struct bt_gatt_indicate_params ind_params;

5. 实现发送 Indicate 的函数 ​

c
int my_lbs_send_button_state_indicate(bool button_state)
{
    if (!indicate_enabled) {
        return -EACCES;
    }

    ind_params.attr    = &my_lbs_svc.attrs[2]; // 指向 Button 特征值属性
    ind_params.func    = indicate_cb;          // 可选:Indicate 完成后的回调
    ind_params.destroy = NULL;
    ind_params.data    = &button_state;
    ind_params.len     = sizeof(button_state);

    return bt_gatt_indicate(NULL, &ind_params);
}

indicate_cb 可用于 log Indicate 结果。

6. 在按钮事件发生时发送 Indicate ​

在 main.c 的 button_changed() 函数内添加:

text
my_lbs_send_button_state_indicate(user_button_state);
  • 这样每次按钮状态变化(按下或释放)时,都会向已订阅 Indicate 的客户端推送最新状态。

7. 编译、烧录并连接测试 ​

  • 烧录后,LED1 闪烁表示正在广播。

  • 用 nRF Connect for Mobile 连接设备 “MY_LBS2”。

  • Button 特征显示可读并支持 Indicate(双箭头图标)。

  • 客户端点击双箭头图标订阅 Indicate。

  • 按下/释放 Button 1,App 实时收到状态变化(无需手动读取)。

第二部分:增加 MYSENSOR 特征,支持 Notification ​

11. 添加 MYSENSOR 特征 UUID ​

在 my_lbs.h 添加:

c
#define BT_UUID_LBS_MYSENSOR_VAL \
    BT_UUID_128_ENCODE(0x00001526, 0x1212, 0xefde, 0x1523, 0x785feabcd123)
#define BT_UUID_LBS_MYSENSOR       BT_UUID_DECLARE_128(BT_UUID_LBS_MYSENSOR_VAL)

12. 在服务声明中添加 MYSENSOR 特征及其 CCCD ​

在 my_lbs.c 的服务定义处:

text
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_MYSENSOR,
                       BT_GATT_CHRC_NOTIFY,
                       BT_GATT_PERM_NONE, NULL, NULL, NULL),

BT_GATT_CCC(mylbsbc_ccc_mysensor_cfg_changed,
            BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),

13. 实现 MYSENSOR 的 CCCD 配置变更回调 ​

js
static void mylbsbc_ccc_mysensor_cfg_changed(const struct bt_gatt_attr *attr,
                                             uint16_t value)
{
    notify_mysensor_enabled = (value == BT_GATT_CCC_NOTIFY);
}

14. 实现发送 Notification 的函数 ​

c
int my_lbs_send_sensor_notify(uint32_t sensor_value)
{
    if (!notify_mysensor_enabled) {
        return -EACCES;
    }

    return bt_gatt_notify(NULL, &my_lbs_svc.attrs[7],
                          &sensor_value, sizeof(sensor_value));
}
  • my_lbs_svc.attrs[7] 指向 MYSENSOR 特征的值属性。

15. 定义用于推送的传感器数据 ​

在 main.c 添加:

c
static uint32_t app_sensor_value = 100;

16. 实现数据模拟函数 ​

c
static void simulate_data(void)
{
    app_sensor_value++;
    if (app_sensor_value == 200) {
        app_sensor_value = 100;
    }
}
  • 每次调用,数据加 1,达到 200 时回到 100。

17. 定义推送周期 ​

c
#define NOTIFY_INTERVAL 500
  • 推送周期为 500ms。

18. 建立线程定时推送数据 ​

18.1 线程函数 ​
c
void send_data_thread(void)
{
    while (1) {
        simulate_data();
        my_lbs_send_sensor_notify(app_sensor_value);
        k_sleep(K_MSEC(NOTIFY_INTERVAL));
    }
}
18.2 线程定义与启动 ​
text
K_THREAD_DEFINE(send_data_thread_id, STACKSIZE, send_data_thread, NULL, NULL,
    NULL, PRIORITY, 0, 0);
  • 线程定期推送数据到所有已订阅客户端。

19/20. 编译、烧录并测试 Notification ​

  • 烧录后,用手机连接设备。

  • 在 App 中找到 MYSENSOR 特征(会显示为 Unknown Characteristic)。

  • 订阅 Notification(点击多箭头图标)。

  • 你将看到传感器数据每 500ms 自动更新。

重点总结 ​

  • Indicate:服务器主动推送数据,需客户端回复确认(适合关键事件,速度受限)。

  • Notify:服务器主动推送数据,无需客户端确认(适合周期性数据流,效率高)。

  • CCCD:实现客户端订阅推送的关键机制。

  • 通过本实验,你已掌握如何用 Zephyr/nRF Connect SDK 实现 BLE GATT 的主动推送能力,适应多样化的物联网场景。

备注:实际开发可直接使用 SDK 自带的 LBS 服务(CONFIG_BT_LBS),无需重复造轮子;本实验的手动实现旨在帮助你深入理解 BLE GATT 服务设计与操作原理。

实验 3:通过 UART 与 Bluetooth LE 连接收发数据(Nordic UART Service) ​

实验目标 ​

本实验将带你实践 Nordic UART Service(NUS),它是一个经典的自定义 GATT 服务,实现 BLE 与 UART(或 USB 虚拟串口)间的双向数据转发。你将实现如下功能:

  • 手机 App 发送数据 → BLE → 开发板 → UART(串口/虚拟串口/PC 终端)

  • PC 端通过串口发数据 → UART → 开发板 → BLE → 手机 App

服务结构简介 ​

NUS 服务包含两个特征:

  • RX 特征(Write/Write Without Response):BLE 客户端(如手机)发数据到开发板

  • TX 特征(Notify):开发板通过 BLE 主动推送数据到客户端

串口(UART0)通常通过 SEGGER 芯片接入 PC,表现为 USB 虚拟串口。

步骤 1:在工程中启用 NUS ​

在 prj.conf 中添加:

text
CONFIG_BT_NUS=y
  • Kconfig 会自动引入 nus.c 和 nus.h,服务和特征会自动注册到属性表,无需手动定义。

步骤 2:认识 NUS 服务实现(代码分析) ​

  • 服务和特征的声明见于 nus.c,无需修改。

  • RX 特征的写回调为 on_receive(),每次 BLE 客户端写入数据时被调用。

  • bt_nus_init() 和 bt_nus_cb 用于注册你的应用层回调,实现 BLE 和 UART 业务解耦。

  • bt_nus_send() 用于通过 TX 特征主动推送(Notify)数据到 BLE 客户端,支持多连接。

步骤 3:定义 FIFO 缓存结构(用于数据转发) ​

3.1 定义两个 FIFO ​

c
static K_FIFO_DEFINE(fifo_uart_tx_data); // BLE → UART
static K_FIFO_DEFINE(fifo_uart_rx_data); // UART → BLE

3.2 FIFO 数据项结构 ​

c
struct uart_data_t {
    void *fifo_reserved; // FIFO 保留
    uint8_t data[UART_BUF_SIZE];
    uint16_t len;
};
  • UART_BUF_SIZE 默认 40 字节,可根据实际需求调整。

步骤 4:初始化 UART 外设 ​

在 main() 中调用:

c
err = uart_init();
if (err) {
    error();
}
  • UART 初始化过程可参考 Zephyr/nRF Connect SDK 基础课程。

步骤 5:实现 BLE → UART 转发 ​

5.1 定义并初始化 NUS 回调结构体 ​

c
static struct bt_nus_cb nus_cb = {
    .received = bt_receive_cb,
};

5.2 注册回调到 NUS 服务 ​

c
err = bt_nus_init(&nus_cb);
if (err) {
    LOG_ERR("Failed to initialize UART service (err: %d)", err);
    return 0;
}

5.3 实现 BLE 数据到 UART 的转发回调 ​

js
static void bt_receive_cb(struct bt_conn *conn, const uint8_t *const data, uint16_t len)
{
    struct uart_data_t *tx = k_malloc(sizeof(*tx));
    if (!tx) {
        LOG_ERR("No memory for UART TX buffer");
        return;
    }
    memcpy(tx->data, data, len);
    tx->len = len;

    int err = uart_tx(uart, tx->data, tx->len, SYS_FOREVER_MS);
    if (err) {
        k_fifo_put(&fifo_uart_tx_data, tx);
    }
}
  • 当 BLE 客户端写入 RX 特征时,数据通过 UART 发出。若 UART 忙,则暂存于 FIFO,待空闲时重发。

步骤 6:UART → BLE 转发 ​

6.1 UART 回调内将接收到的数据送入 FIFO ​

在 UART 的回调函数 uart_cb 的 UART_RX_BUF_RELEASED 事件下:

text
k_fifo_put(&fifo_uart_rx_data, buf);

6.2 定义 BLE 发送线程 ​

text
K_THREAD_DEFINE(ble_write_thread_id, STACKSIZE, ble_write_thread, NULL, NULL, NULL, PRIORITY, 0, 0);

6.3 线程函数实现 ​

c
void ble_write_thread(void)
{
    k_sem_take(&ble_init_ok, K_FOREVER); // 等待 BLE 初始化完成
    struct uart_data_t nus_data = { .len = 0 };

    for (;;) {
        struct uart_data_t *buf = k_fifo_get(&fifo_uart_rx_data, K_FOREVER);

        int plen = MIN(sizeof(nus_data.data) - nus_data.len, buf->len);
        int loc = 0;

        while (plen > 0) {
            memcpy(&nus_data.data[nus_data.len], &buf->data[loc], plen);
            nus_data.len += plen;
            loc += plen;

            // 满包或遇到换行/回车就推送
            if (nus_data.len >= sizeof(nus_data.data) ||
                (nus_data.data[nus_data.len - 1] == '\n') ||
                (nus_data.data[nus_data.len - 1] == '\r')) {
                if (bt_nus_send(NULL, nus_data.data, nus_data.len)) {
                    LOG_WRN("Failed to send data over BLE connection");
                }
                nus_data.len = 0;
            }
            plen = MIN(sizeof(nus_data.data), buf->len - loc);
        }

        k_free(buf);
    }
}
  • 线程不断从 FIFO 取数据,拼包后通过 bt_nus_send() 以 Notification 发送到所有已连接 BLE 客户端。

步骤 7:nRF54L15 DK 特殊配置 ​

如使用 nRF54L15 DK,需在 prj.conf 中添加:

text
CONFIG_ZMS=y

步骤 8:编译、烧录与日志验证 ​

  • 烧录后,LED1 闪烁表示正在广播。

  • 打开串口终端,复位开发板观察启动日志:

text
*** Booting nRF Connect SDK ***
Starting Nordic UART service example

步骤 9:手机端连接测试 ​

  • 在 nRF Connect for Mobile 中找到并连接 “Nordic_UART_Service”。

步骤 10:手机 → 板端(BLE → UART) ​

  • 在 RX 特征右侧点击写入箭头,输入数据后发送(Request/Command 均可)。

  • 观察开发板串口终端输出,接收到手机发来的数据:

text
Hello from phone!

步骤 11:PC → 手机(UART → BLE) ​

  1. 在 nRF Connect for Mobile 订阅 TX 特征(点击 Notify 图标)。

  2. 在 PC 串口终端输入一行数据(如 Hello from PC!),按回车。

  3. 手机端 App 实时收到板端通过 BLE 推送的数据。

附注 ​

  • 默认 ATT MTU 为 23 字节,单次通知最大长度有限。如需更高吞吐量,可提升 MTU,详见前述 Lesson 3 – Exercise 2。

  • NUS 支持多连接,bt_nus_send() 可指定单一连接或 NULL 发送至所有连接。

蓝牙低功耗通信中的安全性 ​

确保蓝牙低功耗链路安全是开发蓝牙低功耗应用的核心需求之一。蓝牙低功耗协议提供多项安全相关功能,包括身份验证、完整性、机密性和隐私保护。

本课程将介绍蓝牙低功耗提供的安全特性(包括传统安全模式和 LE 安全连接),并详解两种方法的配对流程。我们将探讨蓝牙低功耗中的主要安全隐患,如何通过不同安全等级进行防护,以及在建立加密连接时如何使用过滤接受列表。

配对流程详解 ​

概述 ​

无线通信常用加密连接来保护数据安全,确保只有拥有密钥的设备才能解读通信内容。在 BLE(Bluetooth Low Energy)中,双方需要拥有相同的加密密钥。配对(Pairing)过程就是用来生成、分发并验证这些密钥的标准流程。

如果设备不仅在本次连接配对,还将密钥保存起来,以便下次快速加密重连,这叫做绑定(Bonding)。绑定时,设备还会交换和保存身份密钥,便于通过随机可解析私有地址(RPA)在未来识别对方。

核心定义 ​

  • 配对(Pairing):为加密连接生成、分发和认证密钥的过程。

  • 绑定(Bonding):配对后,双方保存密钥用于未来连接的加密。

BLE 配对三阶段 ​

第一阶段:发起配对(Initiate Pairing) ​

  1. 发起流程

    1. 仅中央设备(Central)能主动发送 Pairing Request,外设(Peripheral)只能被动响应。

    2. 外设也可发送 Security Request,要求中央发起配对(但实际用得较少)。

  2. 交换配对参数

    1. 设备互相通报各自的 I/O 能力(输入/输出方式),如:

      1. 仅显示(DisplayOnly)

      2. 显示+Yes/No 按键(DisplayYesNo)

      3. 仅键盘(KeyboardOnly)

      4. 无输入/输出(NoInputNoOutput)

      5. 键盘+显示(KeyboardDisplay)

    2. 还会交换安全特性、是否请求绑定等信息。

类比:像两人约定如何沟通——是打字、显示验证码,还是只能点同意。

第二阶段:执行配对(Perform Pairing) ​

  1. 密钥生成

    1. 根据第一阶段交换的信息,双方协商具体的配对方式和密钥生成流程。
  2. 配对方式

    1. BLE 早期(Legacy pairing)基于临时密钥(TK)生成短期密钥(STK),用于加密连接。但 STK 安全性有限。

    2. 自 Bluetooth 4.2 起,新增 LE Secure Connections,支持更安全的密钥协商,生成长期密钥(LTK)。

    3. 典型配对方式(Pairing Methods)Just Works:双方通过明文协商,不验证用户身份(无认证,最弱安全)。

    4. Passkey Entry:一方显示 6 位数字,另一方输入,适合有显示或键盘的设备。

    5. Out of Band(OOB):通过 BLE 以外的方式(如 NFC)安全交换密钥。

    6. Numeric Comparison(仅 LE Secure Connections 支持):双方都显示 6 位数字,用户确认是否一致。

  3. 选择哪种方式,取决于 OOB/MITM 标志和双方 I/O 能力。

  4. 安全性说明

    1. MITM(中间人攻击)防护能力依赖于配对方式:Just Works 无法防护,Passkey Entry 和 Numeric Comparison 能防护。

第三阶段:密钥分发(Key Distribution) ​

  1. 密钥类型

    1. LTK(Long Term Key):用于今后加密连接。

    2. 其他身份密钥(如 IRK, Identity Resolving Key)等,用于身份识别和地址解析。

  2. 过程

    1. Legacy pairing 在此阶段生成 LTK;LE Secure Connections 在第二阶段已生成 LTK。

    2. 双方交换并保存密钥,便于未来重连时自动加密和身份识别。

小结与流程图 ​

  1. 配对=密钥生成+分发+认证,保证链路加密安全。

  2. 绑定=配对+保存密钥,便于未来自动重连加密。

  3. 三阶段流程:

    1. 交换配对参数(I/O 能力、安全需求等)

    2. 协商并生成密钥(方法由双方能力和安全需求决定)

    3. 分发并保存密钥(如需绑定)

理解 BLE 配对流程,是构建安全蓝牙应用的基础。

Legacy Pairing 与 LE Secure Connections 对比 ​

Legacy Pairing(传统配对) ​

在 Bluetooth v4.2 之前,BLE 仅支持 Legacy Pairing。这种配对方式实现简单,但安全性较弱,主要风险在于用于加密链路的短期密钥(STK)容易被破解。

主要方式与安全性分析 ​

  • Just Works

    • TK(Temporary Key,临时密钥)被直接设为 0

    • 没有任何防窃听或中间人攻击(MITM)能力

    • 攻击者极易暴力破解 STK,窃听通信内容

  • Passkey Entry

    • TK 为 6 位数字,由用户在设备间手动输入/显示

    • 虽比 Just Works 稍强,但攻击者可通过监听数据包轻易获取 TK

    • 就算无法直接获得 TK,也可通过遍历 100 万种组合(0~999999)暴力破解

  • Out of Band(OOB)

    • TK 通过 BLE 以外的安全通道(如 NFC)交换

    • TK 最长可达 128 位,提高安全性

    • 其安全性取决于 OOB 通道本身是否能防御窃听或 MITM

    • 若 OOB 通道安全,则 BLE 配对也安全

注意:Bluetooth SIG 不推荐使用 Legacy pairing。若必须使用,推荐仅采用 OOB 配对方式。

LE Secure Connections(安全连接配对) ​

为解决 Legacy pairing 的安全隐患,Bluetooth v4.2 引入了 LE Secure Connections。它采用椭圆曲线 Diffie-Hellman(ECDH)算法,极大提升了安全性。

LE Secure Connections 的优势 ​

  • 公私钥协商与密钥生成

    • 设备间通过 ECDH 算法各自生成公私钥对

    • 仅交换公钥,不暴露任何秘密信息

    • 双方基于 Diffie-Hellman 密钥和认证数据生成 LTK(长期密钥)

  • 配对方式支持

    • 支持四种方式:Just Works、Passkey Entry、OOB、Numeric Comparison

    • Numeric Comparison 是新增方式,通过在两个设备上显示同一 6 位数字,用户人工确认,防止 MITM

  • 安全性提升

    • Just Works 方式下,虽然安全性提升,但仍无认证能力,理论上仍可能遭受 MITM

    • Passkey Entry 方式下,认证结合 ECDH 公钥和 128 位随机数,破解难度大幅提升

    • OOB 方式依然安全,只要 OOB 通道本身安全

    • Numeric Comparison 增加了人工确认环节,显著增强抗 MITM 能力

  • 数据保护

    • 仅交换公钥,LTK 通过 ECDH 算法生成,暴力破解几乎不可能

兼容性建议 ​

尽管 LE Secure Connections 已被大多数新设备支持,部分 BLE 设备仍只支持 Legacy pairing。为保证互操作性,通常建议应用同时支持 Legacy pairing 与 LE Secure Connections。

总结对比表 ​

配对方式安全性推荐场景
Legacy Just Works极低(无认证)不推荐,测试用
Legacy Passkey Entry低(易被监听/暴力破解)仅在无更好选择时
Legacy OOB中~高(依赖 OOB 通道)推荐,需安全通道
LE Secure Connections (Just Works)中(无认证,难破解)只用在无输入/输出场合
LE Secure Connections (Passkey/Numeric/OOB)高(强认证,ECDH 安全)强烈推荐

理解两种配对机制的安全差异,有助于你为应用选择合适的安全策略,最大化 BLE 连接的安全性和兼容性。

安全模式与 BLE 安全机制 ​

BLE 安全威胁与防护目标 ​

BLE 的安全性不能简单一概而论,实际效果高度依赖于配对方式及设备的 I/O 能力。常见的三类攻击包括:

  • 身份跟踪(Identity tracking)

攻击者利用设备的蓝牙地址进行跟踪。防护方法是启用隐私保护,比如使用可解析私有地址(Resolvable Private Address, RPA),该地址定期随机变化,只有配对/绑定关系的设备(即已获得 IRK,身份解析密钥)才能解析出真实身份。

  • 被动窃听(Passive eavesdropping / sniffing)

攻击者监听设备间的通信内容。防护方法是加密通信链路,但关键在于如何安全生成与交换加密密钥。Legacy pairing 的漏洞主要就在于密钥交换方式不安全,LE Secure Connections 则极大提升了安全性。

  • 主动窃听(Active eavesdropping / Man-in-the-middle, MITM)

攻击者伪装成双方设备,分别与两边建立连接,窃取或篡改数据。防护方法是确保通信对方的真实性,即认证机制(如 Passkey/Numeric Comparison/OOB)。

安全等级(Security Levels) ​

BLE 在安全模式 1(Security Mode 1)下定义了四个安全等级:

  1. Level 1:无安全(明文通信)

无认证、无加密,任何人都可以监听和交互。

  1. Level 2:加密,未认证配对

只要求通信加密,未验证对方身份(例如 Just Works)。

  1. Level 3:加密,已认证配对

要求配对时经过用户认证(如 Passkey Entry/OOB),可防御 MITM。

  1. Level 4:加密,LE Secure Connections 认证

双方都支持 LE Secure Connections 且认证配对(Passkey/Numeric/OOB/Numeric Comparison),安全性最高。

每次 BLE 连接初始时处于 Level 1,配对后根据方式自动提升到更高安全等级。

安全等级与配对方式关系 ​

  • Just Works(Legacy 或 Secure Connections):仅加密,无认证,最高只能到 Level 2,不能防 MITM。

  • Passkey Entry/OOB(Legacy):有认证,能到 Level 3,有一定 MITM 防护。

  • Passkey Entry/Numeric Comparison/OOB(LE Secure Connections):能到 Level 4,极强的 MITM 防护。

特征权限与安全等级 ​

属性表中每个特征的 Permissions 字段不仅决定了可读/可写性,还决定了访问该属性所需的最低安全等级。例如:

  • 如果连接仅加密(Level 2),则无法访问要求 Level 3 或 4 的特征。

  • 这样开发者可精细控制哪些数据在何种安全条件下可被访问。

其他安全机制 ​

Filter Accept List(原 Whitelisting,过滤接受列表) ​

  • 作用:限制设备只允许列表内的指定设备发起连接或扫描交互。

    • 广播时,仅列表内设备可请求连接或扫描回复,其他设备请求直接忽略。

    • 扫描时,仅列表内设备的广播包会被接收和上报。

  • 列表基于配对阶段三(Key Distribution)分发的地址和身份密钥(如 IRK)动态维护。

  • 常用操作:

    • “配对模式”下临时关闭 Filter Accept List,允许新增设备配对;配对后再开启,仅允许已授权设备连接。

扩展说明 ​

  • BLE 共定义了三种安全模式:

    • Security Mode 1:基于加密/认证(主流,本文聚焦)。

    • Security Mode 2:基于数据签名(罕用)。

    • Security Mode 3:面向 LE Audio 的广播安全(新特性,暂不涉及)。

  • 实际开发中,安全模式 1 是绝大多数 BLE 应用的首选。

小结 ​

  • BLE 安全性涉及身份隐私、通信加密、身份认证三大维度。

  • 配对方式和设备 I/O 能力直接决定安全等级。

  • 通过合理设置安全等级和 Filter Accept List,可有效防御常见威胁,保障设备和数据的安全。

实验 1:为 BLE 应用添加配对与认证支持 ​

实验目标 ​

本实验基于无安全保护的 BLE 外设 LBS 示例(自定义 LED Button Service),逐步实现如下安全能力:

  1. 要求加密:LED 特征写入需加密(安全等级 2)。

  2. 支持配对:添加 SMP 协议,支持自动配对和加密。

  3. 提升认证级别:要求 MITM 防护(安全等级 3/4),并通过串口日志显示配对验证码(Passkey),确保用户输入正确 PIN 码完成认证。

步骤 1:为 LED 特征添加加密权限 ​

1.1 修改 LED 特征写入权限,要求加密 ​

在 lbs.c 的 LED 特征声明中,将权限从普通写入改为需加密:

text
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_LED,
                       BT_GATT_CHRC_WRITE,
                       BT_GATT_PERM_WRITE_ENCRYPT,
                       NULL, write_led, NULL),
  • 这样写入 LED 特征时,连接需升级到安全等级 2(加密但未认证)。

步骤 2:编译并烧录固件 ​

将应用烧录到开发板。

步骤 3:测试写入(此时无配对支持) ​

  • 用 nRF Connect for Mobile 连接设备 Nordic_LBS。

  • 尝试写 LED 特征,发现 LED 无反应——因手机尝试写入时发现未加密,连接被拒绝。

  • 说明当前固件还未支持配对,下一步需添加配对能力。

步骤 4:启用 BLE 安全管理协议(SMP) ​

在 prj.conf 中添加:

text
CONFIG_BT_SMP=y
  • 加载 Security Manager Protocol,支持 BLE 配对和加密。

步骤 5:添加安全等级变更回调 ​

5.1 在连接回调结构体中添加新成员 ​

在 main.c 的连接回调结构体中:

text
.security_changed = on_security_changed,

5.2 实现回调函数 ​

c
static void on_security_changed(struct bt_conn *conn, bt_security_t level,
                               enum bt_security_err err)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

    if (!err) {
        LOG_INF("Security changed: %s level %u\n", addr, level);
    } else {
        LOG_INF("Security failed: %s level %u err %d\n", addr, level, err);
    }
}
  • 用于串口输出当前安全等级及加密状态。

步骤 6:再次编译并烧录 ​

烧录并打开终端监控串口日志。

步骤 7:测试配对与加密 ​

  • 手机再次写入 LED 特征,此时会弹出配对提示。

  • 选择 “配对(Pair)”,手机与开发板完成配对,连接升级为加密状态。

  • 串口日志输出类似如下:

text
Security changed: 7B:9E:28:DB:38:7A level 2
  • 此时可正常控制 LED,链路已加密。

步骤 8:提升 LED 特征写入权限至认证级别 ​

将 LED 特征权限改为需认证配对:

text
BT_GATT_CHARACTERISTIC(BT_UUID_LBS_LED,
                       BT_GATT_CHRC_WRITE,
                       BT_GATT_PERM_WRITE_AUTHEN,
                       NULL, write_led, NULL),
  • 要求认证配对(安全等级 3/4),仅加密不足以访问该特征。

步骤 9:定义认证配对回调函数 ​

9.1 显示配对码回调 ​

c
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    LOG_INF("Passkey for %s: %06u\n", addr, passkey);
}

9.2 取消配对回调 ​

c
static void auth_cancel(struct bt_conn *conn)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    LOG_INF("Pairing cancelled: %s\n", addr);
}

9.3 注册认证配对回调结构体 ​

c
static struct bt_conn_auth_cb conn_auth_callbacks = {
    .passkey_display = auth_passkey_display,
    .cancel = auth_cancel,
};

步骤 10:注册认证回调 ​

c
err = bt_conn_auth_cb_register(&conn_auth_callbacks);
if (err) {
    LOG_INF("Failed to register authorization callbacks.\n");
    return -1;
}

步骤 11:编译并全擦写烧录 ​

  • 烧录时选择“Erase And Flash”,清除之前的配对信息。

  • 手机蓝牙设置中,找到设备,选择“取消配对/移除绑定”,确保无历史配对。

步骤 12:测试认证配对 ​

  • 手机用 nRF Connect for Mobile 连接设备,写 LED 特征,会弹出配对窗口,并请求输入 PIN 码。

  • 查看开发板串口输出,例如:

text
Passkey for 48:18:67:01:CC:A8 (random): 043166
  • 在手机界面输入该 6 位 PIN 码,完成认证配对。配对成功后,LED 可被正常控制。

  • 日志输出安全等级提升到 3(Legacy pairing)或 4(LE Secure Connections):

text
Security changed: 48:18:67:01:CC:A8 (random) level 4

小结 ​

  1. 通过修改 GATT 特征权限,灵活控制不同数据的安全访问级别。

  2. 启用 SMP 后,BLE 支持自动配对与链路加密。

  3. 通过显示 Passkey,实现 MITM 防护,提升到认证配对(安全等级 3/4)。

  4. 串口日志实时反馈安全等级和配对状态,有助于调试和安全追溯。

本实验为 BLE 应用加固打下坚实基础,实现了从“完全开放”到“加密+认证”的逐步安全升级。

实验 2:实现 Bonding 与 Filter Accept List ​

实验目标 ​

本实验在 BLE 外设应用基础上,逐步实现以下功能:

  1. Bonding(绑定):持久化存储配对密钥,实现断电/重启后自动恢复加密连接。

  2. Filter Accept List(过滤接受列表):仅允许已绑定设备发起连接,其他设备请求将被忽略,提高连接安全性和专属性。

  3. Pairing Mode(配对模式):允许临时开放广告,便于新设备配对后再恢复只允许已绑定设备连接。

步骤 1:添加 Bonding 支持 ​

1.1 启用密钥存储功能 ​

在 prj.conf 添加以下配置,支持密钥保存至 Flash:

text
CONFIG_SETTINGS=y
CONFIG_BT_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
  • 这样 BLE 协议栈可自动将配对密钥等信息写入并恢复到 Flash。

1.2 包含头文件 ​

在 main.c 顶部添加:

c
#include <zephyr/settings/settings.h>

1.3 蓝牙初始化后加载设置 ​

在蓝牙初始化完成后,主动调用:

text
settings_load();
  • 这一步确保重启后自动恢复已绑定设备信息。

1.4 烧录并测试 ​

  • 编译烧录后,手机首次配对成功,即可断电重启开发板,再次连接手机应无需重新配对,链路能自动加密。

  • 说明密钥已持久保存,实现了 Bonding。

步骤 2:支持删除 Bond 信息 ​

2.1 定义删除 Bond 的按键 ​

如用 DK 板,定义:

c
#define BOND_DELETE_BUTTON DK_BTN2_MSK

2.2 按键处理删除所有 Bond ​

在 button_changed() 中添加:

c
if (has_changed & BOND_DELETE_BUTTON) {
    uint32_t bond_delete_button_state = button_state & BOND_DELETE_BUTTON;
    if (bond_delete_button_state == 0) {
        int err = bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY);
        if (err) {
            LOG_INF("Cannot delete bond (err: %d)\n", err);
        } else {
            LOG_INF("Bond deleted succesfully\n");
        }
    }
}
  • 按下按键删除所有已保存的 Bond 信息。

2.3 烧录并验证 ​

  • 手机与开发板配对后,断开连接,按下删除键。

  • 此时再次连接将无法自动加密,需要重新配对(需先在手机端"忘记此设备")。

步骤 3:实现 Filter Accept List ​

3.1 启用 Filter Accept List 与隐私特性 ​

在 prj.conf 添加:

text
CONFIG_BT_FILTER_ACCEPT_LIST=y
CONFIG_BT_PRIVACY=y
  • 支持生成/使用可解析私有地址,提升防跟踪能力。

3.2 定义带 Accept List 的广播参数 ​

在代码中添加:

c
#define BT_LE_ADV_CONN_ACCEPT_LIST \
    BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_FILTER_CONN, \
        BT_GAP_ADV_FAST_INT_MIN_2, BT_GAP_ADV_FAST_INT_MAX_2, NULL)
  • BT_LE_ADV_OPT_FILTER_CONN 开启连接请求过滤,仅允许列表内设备连接。

3.3 构建 Filter Accept List ​

  1. 添加回调,遍历 Bond 列表并加入 Accept List:
js
static void setup_accept_list_cb(const struct bt_bond_info *info, void *user_data)
{
    int *bond_cnt = user_data;
    if ((*bond_cnt) < 0) {
        return;
    }
    int err = bt_le_filter_accept_list_add(&info->addr);
    LOG_INF("Added peer to accept list: %x %x\n", info->addr.a.val[0], info->addr.a.val[1]);
    if (err) {
        LOG_INF("Cannot add peer to Accept List (err: %d)\n", err);
        (*bond_cnt) = -EIO;
    } else {
        (*bond_cnt)++;
    }
}
  1. 遍历所有 Bond 并添加到 Accept List:
c
static int setup_accept_list(uint8_t local_id)
{
    int err = bt_le_filter_accept_list_clear();
    if (err) {
        LOG_INF("Cannot clear Accept List (err: %d)\n", err);
        return err;
    }
    int bond_cnt = 0;
    bt_foreach_bond(local_id, setup_accept_list_cb, &bond_cnt);
    return bond_cnt;
}

3.4 修改广播处理,以 Accept List 控制连接 ​

将广播启动逻辑改为:

c
int allowed_cnt = setup_accept_list(BT_ID_DEFAULT);
if (allowed_cnt < 0) {
    LOG_INF("Accept list setup failed (err:%d)\n", allowed_cnt);
} else {
    if (allowed_cnt == 0) {
        LOG_INF("Advertising with no Accept list\n");
        err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_2, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    } else {
        LOG_INF("Advertising with Accept list\n");
        LOG_INF("Accept list device count = %d\n", allowed_cnt);
        err = bt_le_adv_start(BT_LE_ADV_CONN_ACCEPT_LIST, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    }
    if (err) {
        LOG_INF("Advertising failed to start (err %d)\n", err);
        return;
    }
    LOG_INF("Advertising successfully started\n");
}
  • 若 Accept List 为空,则开放所有设备连接;否则只允许列表内已绑定设备连接。

3.5 验证 Accept List 效果 ​

  • 用手机 A 配对并断开,再用手机 B 尝试连接,应无法连接。

  • 用手机 A 可正常连接,验证只允许已绑定设备访问。

步骤 4:实现“配对模式”(Pairing Mode) ​

4.1 增加最大绑定设备数 ​

在 prj.conf 添加:

text
CONFIG_BT_MAX_PAIRED=5
  • 允许最多 5 个设备绑定。

4.2 实现“配对模式”按键与逻辑 ​

  1. 定义配对模式按键与标志位:
c
#define PAIRING_BUTTON DK_BTN3_MSK
static bool pairing_mode = false;
  1. 按键回调中处理配对模式:
c
if (has_changed & PAIRING_BUTTON) {
    uint32_t pairing_button_state = button_state & PAIRING_BUTTON;
    if (pairing_button_state == 0) {
        pairing_mode = true;
        int err_code = bt_le_adv_stop();
        if (err_code) {
            LOG_INF("Cannot stop advertising err= %d\n", err_code);
            return;
        }
    }
}
  1. 在广播处理函数中响应配对模式:

在 adv_work_handler() 开头添加:

c
if (pairing_mode == true) {
    err = bt_le_filter_accept_list_clear();
    if (err) {
        LOG_INF("Cannot clear accept list (err: %d)\n", err);
    } else {
        LOG_INF("Accept list cleared successfully");
    }
    pairing_mode = false;
    err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_2, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    if (err) {
        LOG_INF("Advertising failed to start (err %d)\n", err);
        return;
    }
    LOG_INF("Advertising successfully started\n");
    return;
}
  • 按下配对模式按键后,清空 Accept List,开启开放广告,允许新设备连接配对。

  • 新配对完成后,Accept List 自动更新为包含所有已绑定设备,广告重启后恢复“仅允许已绑定设备”模式。

步骤 5:完整验证 ​

  1. 清空开发板和手机的 Bond 信息。

  2. 用手机 A 绑定并断开,确保可重连。

  3. 用手机 B 连接,应被拒绝(Accept List 生效)。

  4. 按下“配对模式”按键,手机 B 可连接并绑定。

  5. 现在两部手机都可连接,验证 Accept List 实现多设备授权。

总结 ​

  • Bonding 实现密钥持久化,提高用户友好性和安全性。

  • Filter Accept List 限定只有授权设备能连接 BLE 外设,防止未配对设备接入。

  • Pairing Mode 兼顾安全与易用性,支持动态添加新授权设备。

  • 这些机制共同构成了高安全、高专属性的蓝牙设备接入体系。

低功耗蓝牙嗅探器 ​

调试蓝牙低功耗(BLE)应用时的主要挑战在于通信是实时进行的。数据交换以毫秒级间隔发生,若 CPU 因调试暂停,通信就会中断。此外,许多 BLE 数据包由蓝牙协议栈底层处理,从应用层监控这些数据包可能相当困难。

蓝牙嗅探器正是解决这些痛点的利器。它不仅能呈现空中传输的全局视图,还能帮助您深入理解协议规范。即使连接处于加密状态,该工具仍能以近实时方式精确展示设备间每个数据交换细节。作为卓越的抓包工具,它捕获的嗅探轨迹可提交给技术支持团队,使其无需复现完整测试环境就能快速分析问题数据,从而大幅提升故障排查效率。

Nordic 提供了一款名为 nRF Sniffer 的蓝牙低功耗嗅探器,使用简单且易于设置。您只需额外准备一个开发套件或适配器,即可将其作为嗅探器的硬件后端使用。

截获(Sniffing)Bluetooth LE 数据包 ​

BLE 抓包工具简介 ​

Bluetooth Sniffer(蓝牙抓包器)是一种专用工具,用于在无线环境下实时截获和查看 Bluetooth LE 数据包。通过抓包,你可以:

  • 全面了解 BLE 设备间的通信过程

  • 查看每一个协议数据包的详细内容(包括已加密的连接)

  • 快速定位开发或调试中的协议问题

  • 向技术支持团队提供抓包跟踪,便于远程协助分析故障

类比:蓝牙抓包器类似于网络抓包工具(如 Wireshark),但用于无线 BLE 信号。

nRF Sniffer for Bluetooth LE ​

Nordic 官方提供了易用的 BLE 抓包工具——nRF Sniffer for Bluetooth LE。该工具需要一块额外的 nRF52 系列开发板或 USB Dongle,作为硬件抓包器,放置在目标设备的无线范围内即可工作。

主要特点 ​

  • 易于部署,不需昂贵的专业蓝牙抓包仪

  • 基于 Nordic 芯片专用固件,不依赖本课程用到的 BLE 协议栈

  • 可与 Wireshark 等主流抓包分析软件配合使用

  • 适合开发、调试和售后支持场景

nRF Sniffer 工作原理 ​

  • 抓包固件直接控制 Nordic SoC 的无线射频(RF)模块,监听并分析 BLE 设备间的所有空中数据包。

  • 该固件为“裸机”实现(Bare metal,无操作系统),因此对射频控制极为灵活、高效。

信道监听机制 ​

  • Nordic 芯片仅有一根天线和一个射频模块,因此一次只能监听一个信道。

  • 广播包监听:

    • BLE 广播分别在 37、38、39 三个信道发送。

    • 抓包器会按顺序轮询监听这三个信道:先监听 37,捕获到广播包后切换到 38,再切到 39,如此循环。

  • 连接包监听:

    • 建立连接后,通信会在 0~36 共 37 个信道间跳频。

    • 抓包器可自动解析连接请求包和信道跳变参数,实时“跟踪”连接,确保抓取到后续所有数据包。

限制 ​

  • 由于硬件限制(单天线、单射频),同一时间只能完整跟踪一个连接。

  • 举例:如果你正在跟踪某一对设备的连接通信,无法同时捕获其他设备的广播包或连接包。

应用价值 ​

  • 协议学习:可直观观察 BLE 各阶段的协议细节(如配对、加密、特征读写等)。

  • 开发调试:快速定位兼容性、协议实现或安全问题。

  • 售后支持:技术支持团队可远程分析抓包日志,无需现场复现复杂问题。

小结 ​

nRF Sniffer for Bluetooth LE 是 BLE 开发、调试及安全分析的重要工具。利用一块 nRF52 开发板,即可低成本、高效率地获取空中 BLE 数据包,为 BLE 协议学习与故障排查提供有力保障。

nRF Sniffer for Bluetooth LE 安装与使用流程 ​

概述 ​

本节将详细介绍如何安装、配置 nRF Sniffer for Bluetooth LE,烧录抓包固件,并通过 Wireshark 实时捕获并分析 BLE 数据包。该工具支持主流的 Nordic 开发板与 Dongle,可极大提升 BLE 协议开发、调试与安全分析效率。

一、准备硬件与固件 ​

1. 支持的硬件列表 ​

  • nRF52840 DK

  • nRF52840 Dongle

  • nRF52833 DK

  • nRF52 DK

  • nRF51 DK

  • nRF51 Dongle

注意

nRF52840 DK v3:需使用 nRF USB 口(不要用 Interface IC USB 口)。

nRF52833 DK v3:暂不兼容 Sniffer 固件,建议用 v2 或更早版本。

2. 下载 sniffer 固件 ​

  1. 访问 Nordic 官网,下载 nRF Sniffer for Bluetooth LE v4.x 或更新版本。

  2. 解压到任意目录,假定为 Sniffer_Software。

  3. 固件 HEX 文件在 Sniffer_Software/hex 文件夹中。

硬件型号固件文件名前缀
nRF52840 DKsniffer_nrf52840dk_nrf52840_*.hex
nRF52840 Donglesniffer_nrf52840dongle_nrf52840_*.hex
nRF52833 DKsniffer_nrf52833dk_nrf52833_*.hex
nRF52 DKsniffer_nrf52dk_nrf52832_*.hex
nRF51 DKsniffer_nrf51dk_nrf51422_*.hex
nRF51 Donglesniffer_nrf51dongle_nrf51422_*.hex

3. 烧录 sniffer 固件 ​

  1. 打开 nRF Connect for Desktop,安装并启动 Programmer 应用。

  2. macOS/Linux 用户:请先安装 SEGGER J-Link 驱动。M1 Mac 需装 x86 版本。

  3. 左上角选择你的开发板型号。

  4. 点击 “Add file” > “Browse”,选中合适的 HEX 固件文件。

  5. 点击 “Erase & write” 烧录固件。

二、安装 Wireshark ​

  1. 访问 Wireshark 官网。

  2. 下载与你操作系统匹配的稳定版,按提示安装。

  3. Ubuntu 用户请参考官方文档单独安装说明。

三、安装 nRF Sniffer 捕获工具 ​

nRF Sniffer 捕获工具以 Wireshark 插件(extcap)或独立方式运行。

1. 安装 Python 依赖 ​

  1. 打开命令行,进入 Sniffer_Software/extcap。

  2. 安装依赖(需 Python 3.6+):

text
pip3 install -r requirements.txt

2. 安装 extcap 插件到 Wireshark ​

  1. 打开 Wireshark。

  2. 菜单栏依次选择 “Help > About Wireshark”(Windows/Linux)或 “Wireshark > About Wireshark”(macOS)。

  3. 切换到 Folders 选项卡,找到 Personal Extcap 路径,双击打开该文件夹。

  4. 将 Sniffer_Software/extcap/ 下的所有文件复制到该文件夹内。

3. 启用 nRF Sniffer 插件 ​

  1. 在 Wireshark,点击 “Capture > Refresh Interfaces” 或按 F5 刷新接口列表。

  2. 菜单选择 “View > Interface Toolbars > nRF Sniffer for Bluetooth LE”,启用 nRF Sniffer 工具栏。

  3. 确认主界面 “接口” 列表中出现 “nRF Sniffer for Bluetooth LE”。

四、开始 BLE 抓包 ​

1. 硬件连接与布置 ​

  • 将已烧录 sniffer 固件的 nRF 板/ Dongle 插入电脑,放置在目标 BLE 设备间距较近的位置。

2. 启动实时抓包 ​

  1. 在 Wireshark “Capture” 界面,双击 “nRF Sniffer for Bluetooth LE” 接口(对应串口名)。

  2. 屏幕将实时显示无线范围内所有 BLE 数据包。

五、Wireshark 抓包界面解析 ​

1. 三窗格结构 ​

  • Packet List(数据包列表):显示所有实时捕获的数据包,每行一个包。

  • Packet Details(包详情):显示当前选中包的分层详细结构。

  • Packet Bytes(包字节流):以十六进制格式显示选中包的原始数据。

可通过 “View” 菜单,确保上述三项已勾选显示。

2. 常见列头说明 ​

列名含义说明
No.数据包序号
Time抓包会话内的相对时间戳
Source源设备地址
Protocol协议层类型(如 LE LL、L2CAP、ATT、SMP 等)
Length数据包字节数
Event counter连接事件计数(连接建立后从 0 开始递增)
Channel Index抓包信道号
Delta time相邻两个包起始间隔
Info数据包关键信息简述
  • 若缺少某列,可在 Packet Details 中右键对应字段,选择 “Apply as Column” 添加。

3. 交互技巧 ​

  • 在 Packet Bytes 区点击某字节,Packet Details 会高亮对应字段,反之亦然,便于定位协议字段。

六、常见问题与帮助 ​

  • 若遇到 nRF Sniffer 无法识别、抓包界面无数据、接口不显示等问题,请查阅 官方故障排查文档。

Powered by VitePress