BLE的广播、扫描和连接等工作机制总结

基础参考:

nRF52832蓝牙概述-CSDN博客

概述

先了解 BLE 的核心工作机制。

一、整体架构概览

BLE 的工作模式可以概括为:广播者(Advertiser) 发出信号,扫描者(Scanner) 监听并发现,根据需要建立连接 ,连接后通过通知(Notification) 等方式进行数据交互。

复制代码
[广播者] --广播包--> [扫描者] --发起连接--> [连接建立] --通知/读写--> 数据传输

二、广播(Advertising)

1. 什么是广播

广播是 BLE 设备宣告自身存在的方式。广播设备定期在 3 个广播信道(37、38、39) 上发送数据包,不需要与任何设备建立连接。

2. 广播的类型

广播类型 说明
可连接非定向广播 最常见类型,任何扫描者都可以发起连接
可连接定向广播 针对特定设备(已知地址)的广播,用于快速重连
不可连接非定向广播 只广播数据,不允许建立连接(如信标 Beacon)
可扫描非定向广播 可被扫描但不可连接,扫描者可请求更多数据

3. 广播的结构

一个广播包最大 31 字节,包含:

  • 设备地址:6 字节

  • 广播数据:如设备名称、服务 UUID、发射功率、厂商自定义数据等

4. 广播参数

  • 广播间隔:20ms ~ 10.24s(典型值 100ms ~ 1s)

  • 间隔越小,发现越快,但功耗越高

三、扫描(Scanning)

1. 什么是扫描

扫描是设备主动侦听广播信道的动作,用于发现周围的 BLE 设备。扫描设备会在 3 个广播信道之间跳频监听。

2. 扫描的类型

扫描类型 说明
主动扫描 收到广播包后,发送扫描请求(Scan Request),对方回复扫描响应(Scan Response),可获取更多数据
被动扫描 只监听广播包,不发送任何请求,功耗更低

3. 扫描参数

  • 扫描窗口:每次扫描持续的时间

  • 扫描间隔:两次扫描开始的间隔

  • 例如:窗口 30ms,间隔 100ms,表示每 100ms 内扫描 30ms

4. 扫描结果

扫描设备会收集:

  • 广播包中的设备地址、广播数据

  • 扫描响应中的扩展数据(主动扫描时)

  • RSSI(信号强度),用于估算距离或定位

四、连接(Connection)

1. 连接建立流程

当扫描者(此时称为发起者 Initiator)决定与一个可连接广播设备建立连接时:

复制代码
广播者                          发起者
   |                              |
   |-------- 广播包 ------------->|
   |                              | (决定连接)
   |<------- 连接请求 (CONNECT_REQ)|
   |                              |
   |======= 连接建立 =============|
   |                              |
   |--- 数据信道跳频通信开始 ----->|

2. 连接请求的内容

发起者发送的连接请求中包含关键参数:

  • 接入地址:用于区分不同连接的 32 位标识

  • 跳频映射:哪些数据信道可用

  • 跳频增量:跳频算法的步长

  • 连接间隔:两个连接事件之间的时间(1.25ms 的整数倍,范围 7.5ms ~ 4s)

  • 监督超时:多长时间没收到包就判定断开

  • 延迟次数:允许从设备跳过多少个连接事件(省电)

3. 连接的角色

  • 主设备(Master):发起连接的一方,负责决定连接时序

  • 从设备(Slave):接受连接的一方,跟随主设备的时序

在连接中,主设备决定节奏:每个连接间隔到达时,主设备在数据信道上发送包,从设备在固定时间后回复。

4. 连接间隔的意义

复制代码
连接间隔 = 30ms

主设备: [TX]----等待----[TX]----等待----[TX]
从设备: ----[RX]----TX----[RX]----TX----[RX]
  • 连接间隔越短,延迟越低,功耗越高

  • 连接间隔越长,延迟越高,功耗越低

  • 双方可以在连接建立后协商调整连接参数

五、通知(Notification)与指示(Indication)

这是连接建立后最重要的数据交互机制,属于 GATT(通用属性协议) 层的操作。

1. 基本概念

在 BLE 中,数据通过 属性(Attribute) 组织,形成 服务(Service)特征(Characteristic) 的层次结构。

角色 说明
GATT 客户端 发起数据读写、接收通知的设备(通常是手机、网关)
GATT 服务器 存储数据、响应请求的设备(通常是传感器、外设)

2. 四种数据交互方式

操作 方向 需要确认 说明
Read 客户端 → 服务器 客户端主动读取特征值
Write 客户端 → 服务器 可选 客户端写入特征值
Notify 服务器 → 客户端 服务器主动推送数据,无需确认,速度快
Indicate 服务器 → 客户端 服务器主动推送数据,需客户端确认,可靠性高

3. 通知(Notify)的典型场景

复制代码
传感器(服务器)                    手机(客户端)
     |                               |
     |  (数据变化,如心率 72 -> 73)    |
     |------- Notify (心跳数据) ----->|
     |                               | (应用直接收到)
     |                               |
     |  (无需回复,继续发送下一包)      |
  • 特点:无需确认,速度快,适合高频数据流

  • 典型应用:心率监测、加速度计数据、串口透传

4. 指示(Indicate)的典型场景

复制代码
传感器(服务器)                    手机(客户端)
     |                               |
     |------- Indicate (重要告警) --->|
     |<------- Confirmation ---------|
     |                               | (确认收到后才发下一条)
  • 特点:需要确认,可靠但速度较慢

  • 典型应用:关键状态变化、设备告警

5. 客户端如何接收通知

客户端需要完成以下步骤才能接收通知:

  1. 发现服务:扫描到设备 → 建立连接 → 发现服务 UUID

  2. 发现特征:在服务中找到特征

  3. 配置 CCCD(客户端特征配置描述符):向特征的 CCCD 写入 0x0001(启用通知)或 0x0002(启用指示)

  4. 接收数据:服务器开始推送数据

六、概念关系图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        广播阶段                              │
│  ┌──────────────┐     广播包      ┌──────────────┐          │
│  │  广播者       │ ─────────────→ │  扫描者       │          │
│  │ (Advertiser) │                 │ (Scanner)    │          │
│  └──────────────┘                 └──────────────┘          │
└─────────────────────────────────────────────────────────────┘
                                    │
                                    │ 发起连接请求
                                    ↓
┌─────────────────────────────────────────────────────────────┐
│                        连接阶段                              │
│  ┌──────────────┐                 ┌──────────────┐          │
│  │  从设备       │ ←─连接事件────→ │  主设备       │          │
│  │  (Slave)     │   数据信道跳频   │  (Master)    │          │
│  │ GATT Server  │                 │ GATT Client  │          │
│  └──────────────┘                 └──────────────┘          │
└─────────────────────────────────────────────────────────────┘
                                    │
                                    │ GATT 操作
                                    ↓
┌─────────────────────────────────────────────────────────────┐
│                        数据传输                              │
│                                                             │
│   客户端主动读取:Read ──────────────────→ 返回特征值          │
│   客户端主动写入:Write ─────────────────→ 写入成功/失败       │
│   服务器主动推送:Notify ─────────────────→ 客户端收到         │
│   服务器可靠推送:Indicate ──→ 确认 ←──→ 客户端收到           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

七、典型应用场景

场景 工作模式 说明
信标(Beacon) 广播 只发不连,单向推送标识信息
传感器(心率、温湿度) 广播 + 连接 + 通知 先广播等待连接,连接后持续通知数据
手机配对(手环、耳机) 广播 + 连接 + 读写 连接后进行参数配置、状态同步
设备配网(IoT) 广播 + 连接 + 指示 使用指示确保配网指令可靠送达
资产标签 广播 + 扫描 双向广播,各自发送位置/状态

八、关键概念总结

概念 本质 发生阶段
广播 单向宣告存在 未连接时
扫描 被动/主动发现设备 未连接时
连接 建立跳频通信链路 连接建立时
通知 服务器主动推送(无确认) 连接后
指示 服务器主动推送(有确认) 连接后
读/写 客户端主动操作 连接后

理解这些概念的关系,就能掌握 BLE 的核心工作流程:广播被发现 → 连接建立链路 → GATT 操作交换数据,其中"通知"是连接后最常用的数据推送方式。
广播事件

广播是从设备发起的,可以被主设备扫描到,并能对扫描进行响应,即扫描响应。
扫描事件

扫描是主机的行为。

处于扫描态的设备能够接收广播报文。扫描态有两个子状态:主动扫描和被动扫描。主动扫描可以发送扫描请求给广播态设备,并获取附加的扫描响应数据;而被动扫描仅仅接收广播报文。
关于扫描响应

BLE的扫描响应是咋回事

简单来说,扫描响应 是低功耗蓝牙(BLE)中一种**"二次握手"的机制** 。它允许从设备在常规广播包放不下的情况下,通过回复扫描请求,向主设备发送额外的、补充的设备信息

以下是关于扫描响应的详细解释:

1. 为什么要扫描响应?

BLE 的广播包数据长度非常有限(有效数据部分最大为 31 字节)。如果设备想把所有信息(如设备全名、更多服务 UUID、厂商自定义数据等)都塞进广播包,这 31 字节往往是不够用的。

  • 广播包:类似于一张"大字报",用来快速宣告自己的存在,通常只包含必要的标志位和设备简称。

  • 扫描响应:类似于给主动询问的人提供的"宣传手册",包含更详细的设备信息。

2. 扫描响应的工作流程

扫描响应涉及两个角色(广播态设备、扫描态设备)和三个步骤:

  1. 广播 :外设(如传感器)在广播态发送广播报文。这个报文里只包含最核心的信息,比如设备地址和一小部分数据。

  2. 主动扫描 :中央设备(如手机)收到广播报文后,如果对设备感兴趣,就会主动发送一个扫描请求给广播设备。

  3. 回复 :广播设备收到这个扫描请求后,会回复一个扫描响应报文。这个报文同样拥有 31 字节的独立数据空间,用来填充广播包里放不下的那些详细信息。

3. 常见的区别示例

在 BLE 应用中,开发者通常这样分配数据:

  • 广播包通常包含

    • Flags(标志位,表示设备是否可连接等)。

    • 主要的 16 位服务 UUID(如果只有 1-2 个)。

    • 设备的缩写本地名称(为了省空间)。

  • 扫描响应通常包含

    • 设备的完整本地名称。

    • 额外的服务 UUID。

    • 用于快速配对的厂商自定义数据(如 Beacon 中的次级数据,或设备外观信息)。

4. 被动扫描与主动扫描的区别

结合你刚才上传的图片内容来看:

  • 被动扫描:只听不发。设备只是接收广播报文,不会去询问更多信息,所以只能拿到广播包里的 31 字节数据,拿不到扫描响应包。

  • 主动扫描:既听也问。设备发送扫描请求,从而获取广播包 + 扫描响应包两份数据,总共最多 62 字节的有效信息。

总结

扫描响应是 BLE 为了在保持广播低功耗特性的同时,解决信息容量有限问题的一种补充机制。它不会增加连接的开销,只有当扫描者主动询问时,设备才会发送这些额外数据,从而避免了始终发送大量数据造成的电量浪费。

注意:

扫描响应走的是广播信道(Advertising Channel),而不是数据信道(Data Channel),因为此时连接还没有建立。
连接事件

注意,建立连接之前,走的都是广播信道,建立连接之后,就会走数据信道。

广播详解

我来详细介绍 BLE 广播的核心机制及其帧数据结构。广播是 BLE 最基础也最重要的功能,理解广播包的结构是理解 BLE 协议栈的起点。


一、广播概述

1. 什么是广播

广播是 BLE 设备在 3 个广播信道(37、38、39) 上周期性发送数据包的行为。广播设备不需要与任何设备建立连接,任何在范围内的扫描设备都可以接收这些包。

2. 广播的用途

  • 设备发现(让其他设备知道自己存在)

  • 发送连接参数(供其他设备发起连接)

  • 单向数据广播(Beacon、传感器数据广播)

  • 发出连接请求(定向广播用于快速重连)

3. 广播的物理层特性

参数
广播信道 37 (2402MHz)、38 (2426MHz)、39 (2480MHz)
每个广播信道发送间隔 依次轮询,每次一个信道
广播间隔 20ms ~ 10.24s,步长 0.625ms
最大广播包长度 31 字节(数据部分)
最大扫描响应包长度 31 字节

二、广播的四种类型

BLE 定义了四种广播类型,由广播包头部中的 PDU Type 字段标识:

类型 PDU Type 值 名称 说明
可连接非定向 0x00 ADV_IND 最常用,允许任何设备发起连接
可连接定向 0x01 ADV_DIRECT_IND 指定目标设备地址,用于快速重连
不可连接非定向 0x02 ADV_NONCONN_IND 只广播数据,不允许连接(Beacon 类型)
可扫描非定向 0x06 ADV_SCAN_IND 可被扫描但不可连接,可请求更多数据

1. ADV_IND(可连接非定向广播)

复制代码
广播者                     扫描者
   |                         |
   |--- ADV_IND ------------>|  (广播包)
   |                         |  (扫描者可选择忽略或请求连接)
   |<--- SCAN_REQ -----------|  (主动扫描请求更多数据)
   |--- SCAN_RSP ----------->|  (扫描响应)
   |                         |
   |<--- CONNECT_REQ --------|  (发起连接)
  • 最通用的广播类型

  • 支持主动扫描(获取更多数据)

  • 支持连接请求

2. ADV_DIRECT_IND(定向广播)

复制代码
广播者                     指定目标设备
   |                         |
   |--- ADV_DIRECT_IND ----->|  (包含目标设备地址)
   |                         |
   |<--- CONNECT_REQ --------|  (只有目标设备能响应)
  • 广播包中携带目标设备地址

  • 只有被指定的设备才能发起连接

  • 通常用于快速重新连接(如 AirPods 靠近手机时)

  • 广播间隔很短(通常 ≤ 30ms),功耗较高但连接快

3. ADV_NONCONN_IND(不可连接广播)

复制代码
广播者                     任何扫描者
   |                         |
   |--- ADV_NONCONN_IND ---->|  (只广播数据)
   |                         |
   |  (不支持 SCAN_REQ 和 CONNECT_REQ) |
  • 纯单向广播,不接收任何请求

  • 典型应用:Eddystone、iBeacon 等信标

  • 功耗最低

4. ADV_SCAN_IND(可扫描不可连接广播)

复制代码
广播者                     扫描者
   |                         |
   |--- ADV_SCAN_IND ------->|  (广播包)
   |                         |
   |<--- SCAN_REQ -----------|  (主动扫描请求更多数据)
   |--- SCAN_RSP ----------->|  (扫描响应)
   |                         |
   |  (不支持 CONNECT_REQ)   |
  • 允许主动扫描获取更多数据

  • 但不允许建立连接

  • 用于需要发送较多数据但又不想建立连接的场景


三、广播帧结构详解

1. 完整的链路层数据包结构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    链路层数据包结构                          │
├──────────┬──────────┬──────────────────┬────────────────────┤
│ 前导码    │ 接入地址  │    协议数据单元   │       CRC         │
│ (Preamble)│ (Access Address) │   (PDU)      │  (循环冗余校验)   │
│ 1 字节    │ 4 字节   │   2-39 字节      │    3 字节         │
└──────────┴──────────┴──────────────────┴────────────────────┘
字段 长度 说明
前导码 1 字节 01010101 或 10101010,用于同步和增益调整
接入地址 4 字节 广播包固定为 0x8E89BED6
PDU 2-39 字节 协议数据单元,包含头部和负载
CRC 3 字节 校验和

2. PDU 结构(广播包)

复制代码
┌─────────────────────────────────────────────────────────────┐
│                         PDU 结构                             │
├──────────────────────┬──────────────────────────────────────┤
│      Header          │              Payload                 │
│      2 字节          │          最多 37 字节                 │
└──────────────────────┴──────────────────────────────────────┘

2.1 Header(2 字节,16 位)

字段名 说明
0-3 PDU Type 广播类型(0x00~0x06)
4 RFU 保留
5 ChSel 信道选择算法(1=支持)
6 TxAdd 发送方地址类型(0=公共,1=随机)
7 RxAdd 接收方地址类型(仅定向广播有效)
8-15 Length Payload 长度(6-37 字节)

PDU Type 详细值:

类型 名称
0x0 ADV_IND 可连接非定向广播
0x1 ADV_DIRECT_IND 可连接定向广播
0x2 ADV_NONCONN_IND 不可连接非定向广播
0x3 SCAN_REQ 扫描请求
0x4 SCAN_RSP 扫描响应
0x5 CONNECT_REQ 连接请求
0x6 ADV_SCAN_IND 可扫描非定向广播

2.2 Payload(广播数据)

广播包 Payload 结构(ADV_IND / ADV_NONCONN_IND / ADV_SCAN_IND):

复制代码
┌─────────────────────┬─────────────────────────────────────┐
│   AdvA              │              AdvData               │
│   6 字节            │          最多 31 字节               │
│   广播设备地址       │          广播数据                    │
└─────────────────────┴─────────────────────────────────────┘

定向广播的 Payload 不同

复制代码
┌─────────────────────┬─────────────────────┐
│   AdvA              │      TargetA        │
│   6 字节            │      6 字节          │
│   广播设备地址       │   目标设备地址        │
└─────────────────────┴─────────────────────┘
字段 说明
AdvA 广播设备地址,6 字节(公共或随机)
TargetA 目标地址(仅定向广播)
AdvData 广播数据,最多 31 字节,由 AD Structure 组成

四、AD Structure(广播数据结构)

AdvData 由一个或多个 AD Structure 组成,每个结构包含:

复制代码
┌───────────────────┬───────────────────┬─────────────────────┐
│   Length          │   AD Type         │     AD Data         │
│   1 字节          │   1 字节          │   Length-1 字节     │
└───────────────────┴───────────────────┴─────────────────────┘

1. AD Type 常用值

AD Type 说明
Flags 0x01 标志位(LE 可发现、LE 不支持 BR/EDR 等)
Incomplete List of 16-bit Service UUIDs 0x02 服务 UUID 列表(不完整)
Complete List of 16-bit Service UUIDs 0x03 服务 UUID 列表(完整)
Shortened Local Name 0x08 设备短名称
Complete Local Name 0x09 设备完整名称
Tx Power Level 0x0A 发射功率(用于距离估算)
Slave Connection Interval Range 0x12 从设备连接间隔范围
Service Solicitation 0x15 服务请求
Service Data 0x16 服务数据
Manufacturer Specific Data 0xFF 厂商自定义数据

2. Flags(标志位)详解

Flags 的 AD Type = 0x01,数据为 1 字节,位定义:

含义
0 LE Limited Discoverable Mode
1 LE General Discoverable Mode
2 BR/EDR Not Supported
3 Simultaneous LE and BR/EDR to Same Device Capable
4 Simultaneous LE and BR/EDR to Same Device Capable (Controller)

典型值:

  • 0x06(二进制 0110):通用可发现模式,不支持 BR/EDR

  • 0x02(二进制 0010):有限可发现模式


五、完整广播包示例解析

示例:iBeacon 广播包

原始十六进制数据

复制代码
02 01 06 1A FF 4C 00 02 15 63 6F 3F 8F 64 91 4B EE 95 F7 D8 CC 64 A8 63 B5 00 00 00 00 C8 00

解析过程

链路层 Header(前 2 字节)

  • PDU Type = 0x00(ADV_IND,可连接非定向)

  • Length = 0x1E = 30 字节

Payload(后面数据)

  • AdvA(6 字节):假设为某 MAC 地址

  • AdvData(剩余字节):AD Structures

AD Structure 1

字段 说明
Length 0x02 2 字节数据
AD Type 0x01 Flags
AD Data 0x06 通用可发现,不支持 BR/EDR

AD Structure 2

字段 说明
Length 0x1A 26 字节数据
AD Type 0xFF 厂商数据
Company ID 0x004C Apple 公司 ID
Beacon Type 0x02 iBeacon
Beacon Length 0x15 21 字节
UUID 63 6F 3F 8F 64 91 4B EE 95 F7 D8 CC 64 A8 63 B5 16 字节 Beacon UUID
Major 0x0000 主编号
Minor 0x0000 次编号
Tx Power 0xC8 (-56 dBm) 校准发射功率

六、扫描请求和扫描响应

1. SCAN_REQ(扫描请求)

扫描者收到广播包后,可选择发送扫描请求获取更多数据。

复制代码
┌─────────────────────────────────────────────┐
│              SCAN_REQ PDU                    │
├─────────────────────┬───────────────────────┤
│      ScanA          │        AdvA           │
│      6 字节         │       6 字节           │
│   扫描者地址         │    广播者地址          │
└─────────────────────┴───────────────────────┘

2. SCAN_RSP(扫描响应)

广播者收到扫描请求后,回复扫描响应。

复制代码
┌─────────────────────┬───────────────────────┐
│      AdvA           │       ScanRspData     │
│      6 字节         │      最多 31 字节      │
│   广播者地址         │      响应数据          │
└─────────────────────┴───────────────────────┘

扫描响应数据的结构与 AdvData 完全相同(AD Structure 列表),用于发送广播包放不下的额外信息(如完整设备名称)。


七、广播参数配置

参数 范围 典型值 说明
广播间隔 20ms ~ 10.24s 100ms ~ 1s 间隔越小,功耗越高,发现越快
广播类型 ADV_IND / ADV_DIRECT_IND / ADV_NONCONN_IND / ADV_SCAN_IND ADV_IND 决定是否可连接/可扫描
广播信道 37, 38, 39 全部 可选择性关闭某些信道
Tx 功率 -20dBm ~ +10dBm 0dBm 影响覆盖距离
广播数据 最多 31 字节 自定义 设备名称、服务 UUID、厂商数据

八、广播状态机

复制代码
                    ┌─────────────────┐
                    │   空闲状态       │
                    └────────┬────────┘
                             │ 启动广播
                             ↓
                    ┌─────────────────┐
                    │   广播状态       │
                    │  发送 ADV_IND   │
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        ↓                    ↓                    ↓
┌───────────────┐  ┌───────────────┐  ┌───────────────┐
│ 收到 SCAN_REQ │  │ 收到 CONNECT  │  │ 广播停止      │
│ 发送 SCAN_RSP │  │     _REQ      │  │ 命令          │
└───────────────┘  └───────┬───────┘  └───────────────┘
                           ↓
                    ┌─────────────────┐
                    │   连接状态       │
                    │  数据信道跳频    │
                    └─────────────────┘

九、常见广播应用示例

1. iBeacon(Apple)

  • 广播类型:ADV_NONCONN_IND 或 ADV_IND

  • AD Type:0xFF(厂商数据)

  • 内容:UUID + Major + Minor + Tx Power

  • 用途:室内定位、消息推送

2. Eddystone(Google)

  • 广播类型:ADV_NONCONN_IND

  • AD Type:0x16(服务数据),Service UUID = 0xFEAA

  • 多种帧类型:UID(信标ID)、URL(网页地址)、TLM(遥测)

  • 用途:物理网页、信标

3. 设备广播(传感器)

  • 广播类型:ADV_IND

  • AD Type:0x02/0x03(服务 UUID)+ 0x09(设备名称)

  • 用途:让手机发现并连接

4. 快速配对(耳机)

  • 广播类型:ADV_DIRECT_IND(定向)

  • 内容:目标设备地址(已配对过的手机)

  • 用途:开盖即连


十、总结

要点 说明
广播信道 37、38、39,三个特定频率避让 Wi-Fi
广播类型 4 种,决定是否可连接、可扫描
PDU 结构 Header(2B)+ Payload(最多 37B)
AdvData 由 AD Structure 组成,每个 AD 结构为 Length + Type + Data
最大数据 广播包 31 字节,扫描响应另有 31 字节
常见 AD Type Flags(0x01)、设备名(0x09)、厂商数据(0xFF)、服务 UUID(0x03)
典型应用 iBeacon(单向)、传感器广播(可连接)、定向广播(快速配对)

广播是 BLE 协议的入口,掌握广播包的结构就能理解 BLE 设备如何被发现、如何宣告自己的能力、以及如何携带应用数据。

关于广播设备地址

我们在BLE解析APP上看到的MAC地址,不一定都是实际的地址,为啥呢?

BLE 设备在广播时使用的地址 ≠ 设备真实的 MAC 地址

这是一个非常经典且容易让人困惑的问题!你观察到的现象完全正确,而且这正是 BLE 隐私保护机制的核心体现。


一、为什么对不上?核心原因

BLE 设备在广播时使用的地址 ≠ 设备真实的 MAC 地址

这不是 bug,而是 BLE 协议故意设计 的隐私保护机制。BLE 设备可以使用多种地址类型,而绝大多数消费类 BLE 设备(手环、耳机、传感器)都使用随机地址,而非真实的工厂 MAC 地址。


二、BLE 广播地址的三种情况

情况 1:使用随机地址(最常见)

复制代码
┌─────────────────────────────────────────────────────────────┐
│                   设备真实的工厂 MAC                         │
│                   AA:BB:CC:DD:EE:FF                         │
│                   (烧录在芯片中,永不改变)                  │
└─────────────────────────────────────────────────────────────┘
                          ↓
                    BLE 软件生成随机地址
                          ↓
┌─────────────────────────────────────────────────────────────┐
│              广播时使用的地址(你看到的)                    │
│              12:34:56:78:9A:BC                              │
│              (完全不同的地址!)                            │
└─────────────────────────────────────────────────────────────┘

为什么这样做?

  • 防止恶意追踪:如果设备始终使用固定地址,任何人都可以追踪你的位置

  • 隐私保护:商场、街道上的扫描设备无法通过地址识别你的设备

情况 2:使用可解析私有地址(RPA)

这是已配对设备重连时使用的方式:

复制代码
┌─────────────────────────────────────────────────────────────┐
│  广播地址每 15 分钟变化一次:                                │
│  9:00  →  A3:45:67:89:AB:CD                                 │
│  9:15  →  B7:89:AB:CD:EF:01  ← 地址变了!                   │
│  9:30  →  C1:23:45:67:89:AB  ← 又变了!                     │
└─────────────────────────────────────────────────────────────┘

只有已配对的设备(持有 IRK)才能解析出真实身份,陌生设备看到的只是不断变化的地址。

情况 3:使用公共地址(少见)

少数设备会使用真实的工厂 MAC 地址作为广播地址,例如:

  • 某些开发板(未配置随机地址)

  • 某些鼠标、键盘

  • 传统蓝牙设备


三、如何判断地址类型?

方法 1:看地址第一个字节的最高两位

地址首字节 二进制(高2位) 地址类型 与实际 MAC 的关系
0x3E (0011 1110) 00 不可解析私有地址 完全不同
0xA3 (1010 0011) 10 可解析私有地址 (RPA) 完全不同
0xC5 (1100 0101) 11 静态随机地址 完全不同
任意值 无固定模式 公共地址 就是实际 MAC

方法 2:通过 APP 显示判断

大多数 BLE 扫描 APP 会直接告诉你地址类型:

APP 显示示例
nRF Connect AA:BB:CC:DD:EE:FF (Random)(Public)
LightBlue 会标注 "Random" 或 "Public"
BLE Scanner 会显示地址类型图标

四、实际案例分析

案例 1:小米手环

复制代码
实际工厂 MAC:  88:0F:10:5A:3B:7C  (标签上印刷的)
广播时地址:    D3:2A:5F:8C:1E:47  (你看到的)

地址首字节 0xD3 = 1101 0011 → 最高两位 11 → 静态随机地址

为什么?

  • 防止被追踪:如果使用固定 MAC,任何人都可以在商场追踪你的手环

  • 每次重启后地址会变化,重新配对时需要重新识别

案例 2:AirPods

复制代码
实际工厂 MAC:  5C:8B:4A:2F:6E:19
广播时地址:    A7:3C:8E:1D:4F:62  (开盖时)
地址首字节 0xA7 = 1010 0111 → 最高两位 10 → 可解析私有地址 (RPA)

为什么?

  • 快速重连:只有配对的 iPhone 能解析出真实身份

  • 自动连接:手机在后台扫描,看到 RPA 后解析,确认是 AirPods 后自动连接

案例 3:Nordic 开发板(未配置)

复制代码
实际工厂 MAC:  E4:7C:2A:5B:8D:3F
广播时地址:   E4:7C:2A:5B:8D:3F  (完全一样!)
地址首字节 0xE4 = 1110 0100 → 没有固定高位模式 → 公共地址

为什么?

  • 开发板默认使用公共地址(便于调试)

  • 未启用隐私保护功能


五、如何获取设备的真实 MAC 地址?

方法 1:连接后读取

重要 :一旦建立连接,客户端可以通过 GATT 读取设备信息服务的序列号或制造商信息,但这些不一定等于 MAC 地址

复制代码
连接后,读取设备信息服务 (DIS, 0x180A):
- 制造商名称 (0x2A29): "Xiaomi"
- 型号 (0x2A24): "Mi Band 5"
- 序列号 (0x2A25): "A1B2C3D4E5"  ← 这也不是 MAC 地址

真实情况 :许多 BLE 设备永远不会暴露工厂 MAC 地址,因为这正是隐私保护的目的。

方法 2:通过配对信息

如果设备已配对,手机会存储:

复制代码
配对信息:
- 地址类型:RPA
- IRK:0x12345678...
- 解析后的身份标识(不是 MAC)

方法 3:查看设备标签

唯一可靠的方式是看设备外壳标签包装盒上的 MAC 地址,但这个地址通常不会在广播中出现。


六、为什么 APP 显示的地址总在变化?

动态随机地址的变化规律

地址类型 变化时机 示例
静态随机地址 设备重启/重新上电 重启后地址改变
可解析私有地址 每 15 分钟(可配置) 15 分钟后地址改变
不可解析私有地址 每几分钟 频繁变化

实际场景

复制代码
9:00  手环广播地址:D3:2A:5F:8C:1E:47
9:16  手环广播地址:E5:4B:7A:9D:2F:38  ← 15分钟后变化
9:32  手环广播地址:A1:C3:5E:7B:9D:2F  ← 又变了

你每次打开 APP 看到的可能都是不同的地址!


七、如何正确识别设备?

既然地址会变化,那应该怎么识别?

方法 1:通过广播数据识别(最可靠)

复制代码
不看地址,看广播数据中的内容:

广播包示例:
- Service UUID: 0x180D (心率服务) ← 表明这是心率设备
- Device Name: "Mi Band 5"        ← 设备名称
- Manufacturer Data: 0x...         ← 厂商数据

组合判断:
if (服务包含 0x180D AND 名称包含 "Mi Band") {
    这是小米手环
}

方法 2:通过 RSSI 和位置

复制代码
扫描结果:
- 地址 A3:45:67...  RSSI -45dBm  ← 最近
- 地址 B7:89:AB...  RSSI -78dBm  ← 较远

用户可能选择信号最强的那个

方法 3:首次连接后存储 IRK

复制代码
第一次连接:
1. 用户手动选择设备(通过名称)
2. 完成配对
3. 手机存储设备的 IRK

后续重连:
手机扫描 → 看到 RPA → 用 IRK 解析 → 确认身份 → 自动连接

八、特殊情况:为什么有些设备地址对得上?

情况 1:开发模式

某些开发板或测试设备禁用了隐私功能,直接使用公共地址。

情况 2:经典蓝牙设备

传统蓝牙设备(如耳机)可能使用固定地址。

情况 3:Beacon 信标

某些 Beacon 使用静态随机地址或固定地址,但地址仍然不是工厂 MAC。


九、总结

问题 答案
为什么对不上? BLE 设备通常使用随机地址(隐私保护),而非工厂 MAC
实际 MAC 在哪? 在芯片中烧录,通常不对外广播
怎么判断地址类型? 看首字节最高两位:00/10/11 = 随机,无固定模式 = 公共
如何识别设备? 通过广播数据中的服务 UUID、设备名称、厂商数据
地址会一直变吗? 取决于类型:静态随机地址重启才变,RPA 每 15 分钟变
配对后怎么重连? 通过 IRK 解析 RPA,自动识别

关键要点

  1. 不要依赖地址来唯一标识设备------它会变

  2. 广播数据才是识别的关键------服务 UUID、设备名称

  3. RPA 是隐私保护的核心------地址变化但已配对设备能识别

  4. 连接后可以读取身份信息------但通常也不是 MAC 地址

你遇到的现象是 BLE 隐私保护机制正常工作的表现,不是 bug!

扫描详解

扫描是 BLE 设备发现周围设备的核心机制。让我从工作原理、扫描类型、参数配置、帧交互细节等方面全面解析。


一、扫描的本质

扫描 = 设备在广播信道上被动监听主动探测,以发现周围的广播设备。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      扫描设备                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 在广播信道 37、38、39 之间跳频监听                    │   │
│  │ 每隔一段时间打开射频接收器                           │   │
│  │ 捕获并解析收到的广播包                              │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              ↓
                    发现广播设备列表

二、扫描的两种类型

类型 英文 行为 功耗 获取信息
被动扫描 Passive Scanning 只监听,不发送 仅广播包数据
主动扫描 Active Scanning 监听 + 发送扫描请求 较高 广播包 + 扫描响应

1. 被动扫描(Passive Scanning)

复制代码
扫描者                              广播者
   |                                  |
   |   监听广播信道                    |
   |   (只接收,不发送)                |
   |                                  |
   |<-------- ADV_IND ----------------|
   |                                  |
   |<-------- ADV_IND ----------------|
   |                                  |
   |   (不发送 SCAN_REQ)              |
   |                                  |
   |   收集广播包中的信息              |

特点

  • 只接收,不发送任何数据

  • 无法获取扫描响应中的数据

  • 功耗更低

  • 适用于只需要基本广播信息的场景

2. 主动扫描(Active Scanning)

复制代码
扫描者                              广播者
   |                                  |
   |   监听广播信道                    |
   |                                  |
   |<-------- ADV_IND ----------------|  (收到广播包)
   |                                  |
   |-------- SCAN_REQ --------------->|  (发送扫描请求)
   |                                  |
   |<-------- SCAN_RSP ----------------|  (收到扫描响应)
   |                                  |
   |   获得更多数据                    |

特点

  • 可以获取扫描响应中的额外数据

  • 功耗略高(需要发送)

  • 适用于需要完整设备信息的场景


三、扫描参数详解

扫描性能由三个核心参数决定:

复制代码
时间轴:
|<-- 扫描间隔 -->|
┌────────┐       ┌────────┐
│ 窗口   │       │ 窗口   │
└────────┘       └────────┘
参数 英文 单位 说明
扫描窗口 Scan Window 0.625ms 每次扫描持续的时间
扫描间隔 Scan Interval 0.625ms 两次扫描开始的间隔
扫描周期 Scan Period 取决于实现 整体扫描时长

参数关系

复制代码
扫描占空比 = 扫描窗口 / 扫描间隔 × 100%

示例:
窗口 = 30ms,间隔 = 100ms
占空比 = 30 / 100 = 30%
功耗 ≈ 30% × 接收功耗

参数配置示例

场景 窗口 间隔 占空比 发现速度 功耗
快速发现 30ms 30ms 100% 最快
平衡模式 30ms 100ms 30% 中等
后台扫描 10ms 500ms 2%
省电模式 5ms 1000ms 0.5% 很慢 极低

广播间隔与扫描窗口的关系

为了成功发现设备,必须满足:

复制代码
广播间隔 < 扫描窗口 + 扫描间隔
复制代码
示例:
广播间隔 = 100ms(广播者每 100ms 发一次)
扫描窗口 = 30ms
扫描间隔 = 100ms

时间轴:
广播者:  [B]    [B]    [B]    [B]    [B]
扫描者:   [====]      [====]      [====]
         30ms       30ms        30ms
         ← 100ms →  ← 100ms →

至少有一次扫描窗口与广播包重合 ✓

四、扫描帧交互详解

1. 被动扫描的帧交互

复制代码
扫描者                              广播者
   |                                  |
   |   (打开射频接收器)                |
   |   (跳到广播信道 37)               |
   |                                  |
   |<-------- ADV_IND ----------------|  (信道 37)
   |   (解析并记录)                   |
   |                                  |
   |   (跳到广播信道 38)               |
   |                                  |
   |<-------- ADV_IND ----------------|  (信道 38)
   |   (解析并记录)                   |
   |                                  |
   |   (跳到广播信道 39)               |
   |                                  |
   |<-------- ADV_IND ----------------|  (信道 39)
   |   (解析并记录)                   |
   |                                  |
   |   (关闭射频,等待下一个扫描窗口)  |

2. 主动扫描的帧交互

SCAN_REQ(扫描请求)帧结构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                   SCAN_REQ 帧结构                            │
├─────────────────────────────────────────────────────────────┤
│ 前导码(1B) | 接入地址(0x8E89BED6) | PDU | CRC(3B)          │
└─────────────────────────────────────────────────────────────┘

PDU Header(2字节):
┌─────────────────────────────────────────────────────────────┐
│ PDU Type = 0x03 (SCAN_REQ) | RFU | TxAdd | RxAdd | Length  │
└─────────────────────────────────────────────────────────────┘

PDU Payload(12 字节):
┌─────────────────────────────────────────────────────────────┐
│                     ScanA (6 字节)                          │
│                     扫描者设备地址                          │
├─────────────────────────────────────────────────────────────┤
│                     AdvA (6 字节)                           │
│                     广播者设备地址                          │
└─────────────────────────────────────────────────────────────┘

关键字段

  • ScanA:扫描者的地址(用于广播者识别)

  • AdvA:目标广播者的地址(从 ADV_IND 中获取)

SCAN_RSP(扫描响应)帧结构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                   SCAN_RSP 帧结构                            │
├─────────────────────────────────────────────────────────────┤
│ 前导码(1B) | 接入地址(0x8E89BED6) | PDU | CRC(3B)          │
└─────────────────────────────────────────────────────────────┘

PDU Header(2字节):
┌─────────────────────────────────────────────────────────────┐
│ PDU Type = 0x04 (SCAN_RSP) | RFU | TxAdd | RxAdd | Length  │
└─────────────────────────────────────────────────────────────┘

PDU Payload:
┌─────────────────────────────────────────────────────────────┐
│                     AdvA (6 字节)                           │
│                     广播者设备地址                          │
├─────────────────────────────────────────────────────────────┤
│                 ScanRspData (0-31 字节)                     │
│                     扫描响应数据                            │
└─────────────────────────────────────────────────────────────┘

ScanRspData 格式与广播数据相同(AD Structure 列表),用于传输广播包放不下的额外信息(如完整的设备名称)。


五、扫描的完整时序图

复制代码
广播者 (Advertiser)                  扫描者 (Scanner)
    |                                     |
    |  === 广播信道 37 ===                 |
    |                                     |
    |  ADV_IND (AdvA=AA:BB:CC...)         |
    |------------------------------------>| (被动扫描:记录)
    |                                     |
    |                                     | (主动扫描:发送 SCAN_REQ)
    |                                     |
    |  <---------------------------------| SCAN_REQ (ScanA=手机地址)
    |                                     |
    |  SCAN_RSP (AdvData扩展)             |
    |------------------------------------>|
    |                                     |
    |  === 广播信道 38 ===                 |
    |                                     |
    |  ADV_IND (同上)                     |
    |------------------------------------>| (记录或请求)
    |                                     |
    |  === 广播信道 39 ===                 |
    |                                     |
    |  ADV_IND (同上)                     |
    |------------------------------------>| (记录或请求)
    |                                     |
    |                                     | 扫描窗口结束
    |                                     | 处理扫描结果

六、扫描过滤器

扫描时可以设置过滤器,只接收感兴趣的设备:

过滤器类型 说明 应用场景
服务 UUID 只接收包含特定服务 UUID 的设备 扫描心率带
设备名称 只接收名称匹配的设备 连接特定设备
MAC 地址 只接收特定地址的设备 重连已配对设备
RSSI 阈值 只接收信号强度超过阈值的设备 近距离连接
厂商数据 只接收包含特定厂商数据的设备 信标识别

Android 扫描过滤器示例

复制代码
// 只扫描心率服务设备
ScanFilter filter = new ScanFilter.Builder()
    .setServiceUuid(new ParcelUuid(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB")))
    .build();

// 只扫描信号强度 > -60dBm 的设备
ScanSettings settings = new ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
    .build();

七、扫描结果处理

扫描结果通常包含以下信息:

字段 说明
设备地址 广播者的 MAC 地址(可能是随机地址)
地址类型 公共地址/随机地址
RSSI 信号强度(dBm),用于距离估算
广播数据 ADV_IND 中的 AdvData
扫描响应数据 SCAN_RSP 中的 ScanRspData(主动扫描)
时间戳 收到包的时间
信道索引 收到包的广播信道(37/38/39)

RSSI 与距离的关系

复制代码
RSSI (dBm)      距离 (近似)
    -30          0.5 米
    -50          1-2 米
    -60          3-5 米
    -70          5-10 米
    -80          10-20 米
    -90          30+ 米(不稳定)

八、扫描类型对比总结

对比维度 被动扫描 主动扫描
发送数据 是(SCAN_REQ)
接收数据 仅 ADV_IND ADV_IND + SCAN_RSP
功耗 中等
发现速度 相同 相同
获取信息量 少(最多 31 字节) 多(最多 62 字节)
隐私影响 低(不暴露扫描者地址) 中(暴露扫描者地址)
适用场景 简单发现、信标 需要完整设备信息

九、常见问题与优化

问题 1:扫描不到设备

可能原因

  • 扫描窗口太小,错过了广播包

  • 广播间隔太长,扫描窗口覆盖不到

  • 射频干扰(Wi-Fi、微波炉)

  • 距离太远

解决方案

复制代码
增大扫描窗口(如 30ms → 100ms)
减小扫描间隔(如 500ms → 100ms)
检查广播者是否在广播

问题 2:扫描结果重复

原因:同一设备在三个广播信道重复发送

解决方案

  • 去重:根据设备地址过滤

  • 合并:将同一设备的多条广播合并为一个结果

问题 3:功耗过高

原因:扫描占空比太高

解决方案

复制代码
降低扫描占空比:
窗口 30ms,间隔 500ms → 占空比 6%
功耗大幅降低,但发现延迟增加

十、总结

要点 说明
扫描类型 被动(只监听)和主动(发送请求)
核心参数 扫描窗口 + 扫描间隔,决定功耗和发现速度
帧交互 SCAN_REQ(请求)+ SCAN_RSP(响应)
数据获取 主动扫描可获得最多 62 字节数据
过滤机制 服务 UUID、设备名称、RSSI 等
扫描结果 地址、RSSI、广播数据、扫描响应数据

一句话总结

扫描是 BLE 设备在广播信道上周期性监听的过程,通过调整扫描窗口和间隔来平衡发现速度和功耗,主动扫描可以发送 SCAN_REQ 获取额外的扫描响应数据。

连接详解

BLE 的连接建立是一个精密的握手过程,涉及广播、扫描、连接请求和跳频同步。让我从链路层帧交互的角度详细拆解。


一、连接建立的整体流程

复制代码
广播者 (Advertiser)              发起者 (Initiator)
        |                               |
        |  (1) ADV_IND 广播包           |
        |------------------------------>|
        |                               |
        |  (2) SCAN_REQ 扫描请求        |
        |<------------------------------|
        |                               |
        |  (3) SCAN_RSP 扫描响应        |
        |------------------------------>|
        |                               |
        |  (4) CONNECT_REQ 连接请求     |
        |<------------------------------|
        |                               |
        |  (5) 连接建立,数据通道跳频    |
        |<=============================>|

注意 :主动扫描(SCAN_REQ/RSP)是可选的。发起者可以直接发送 CONNECT_REQ 而不先扫描。


二、详细帧交互解析

阶段 1:广播阶段


阶段 2:扫描阶段(可选)


阶段 3:连接请求(关键)

3.1 CONNECT_REQ 帧结构

发起者决定连接后,发送 CONNECT_REQ,这是整个连接建立的最关键帧

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                      CONNECT_REQ 帧结构                                  │
├─────────────────────────────────────────────────────────────────────────┤
│ 前导码(1B) | 接入地址(0x8E89BED6) | PDU | CRC(3B)                       │
└─────────────────────────────────────────────────────────────────────────┘

PDU Header(2字节):
┌─────────────────────────────────────────────────────────────────────────┐
│ PDU Type = 0x05 (CONNECT_REQ) | RFU | TxAdd | RxAdd | Length = 0x22    │
└─────────────────────────────────────────────────────────────────────────┘

PDU Payload(34 字节):
┌─────────────────────────────────────────────────────────────────────────┐
│                        InitA (6 字节)                                   │
│                        发起者设备地址                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                        AdvA (6 字节)                                    │
│                        广播者设备地址                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                        LLData (22 字节)                                 │
│                        链路层连接参数                                    │
└─────────────────────────────────────────────────────────────────────────┘

3.2 LLData(22 字节)详细结构

这是连接建立的核心参数,决定了后续数据通道的行为:

复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                        LLData (22 字节)                                 │
├──────────────┬──────────────────────────────────────────────────────────┤
│ 字段         │ 长度(bit) │ 说明                                         │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ AA           │ 32        │ 接入地址(Access Address)                   │
│              │           │ 用于区分不同连接的 32 位标识                  │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ CRCInit      │ 24        │ CRC 初始值                                   │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ WinSize      │ 8         │ 传输窗口大小(单位 1.25ms)                  │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ WinOffset    │ 16        │ 传输窗口偏移(单位 1.25ms)                  │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ Interval     │ 16        │ 连接间隔(单位 1.25ms)                      │
│              │           │ 范围:6 ~ 3200(7.5ms ~ 4s)                │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ Latency      │ 16        │ 从设备延迟(跳过的连接事件数)               │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ Timeout      │ 16        │ 监督超时(单位 10ms)                        │
│              │           │ 范围:10 ~ 3200(100ms ~ 32s)              │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ ChM          │ 40        │ 信道映射(37 个数据信道的可用性)            │
│              │           │ 5 字节 = 40 位,每位代表一个数据信道         │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ Hop          │ 5         │ 跳频增量                                     │
│              │           │ 范围 5 ~ 16                                  │
├──────────────┼───────────┼──────────────────────────────────────────────┤
│ SCA          │ 3         │ 主设备时钟精度                               │
└──────────────┴───────────┴──────────────────────────────────────────────┘

四、连接参数详解

1. 接入地址(Access Address, AA)

这是一个 32 位随机值,由发起者生成,用于:

  • 在数据通道上区分不同的连接

  • 过滤掉其他连接的包

特点

  • 广播通道使用固定接入地址:0x8E89BED6

  • 数据通道使用协商的接入地址

  • 必须是随机值,且不能是 0x8E89BED6 或其他特定值

2. 连接间隔(Connection Interval)

复制代码
连接间隔 = Interval × 1.25ms

示例:
Interval = 24 → 连接间隔 = 30ms
Interval = 100 → 连接间隔 = 125ms

作用

  • 主设备在每个连接间隔开始时发送一个包

  • 从设备在对应时间窗口回复

  • 决定功耗和响应速度

复制代码
时间轴:
[连接事件 N]     [连接事件 N+1]     [连接事件 N+2]
    |                  |                  |
    |<── 连接间隔 ────>|<── 连接间隔 ────>|
    |                  |                  |
  主发 从回          主发 从回          主发 从回

3. 从设备延迟(Slave Latency)

允许从设备跳过最多 N 个连接事件而不回复,用于省电。

复制代码
示例:Latency = 3

连接事件: 0    1    2    3    4    5    6
主设备:  发   发   发   发   发   发   发
从设备:  回   睡   睡   睡   回   睡   睡
         ↑                   ↑
    必须回复            必须回复

4. 信道映射(Channel Map)

40 位掩码,每个位对应一个数据信道(0-36):

复制代码
Channel Map = 5 字节 = 40 位

位 0-36: 1 = 信道可用,0 = 信道不可用(坏信道或避开 Wi-Fi)
位 37-39: 保留(广播信道,不用于数据)

5. 跳频增量(Hop Increment)

用于计算下一个数据信道的公式:

复制代码
next_channel = (last_channel + hop) mod 37

其中 hop 是 5-16 之间的奇数

这确保了跳频序列能遍历所有可用信道。


五、连接建立后的同步过程

1. 时间同步

CONNECT_REQ 在广播信道上发送后,双方进入传输窗口

复制代码
广播者收到 CONNECT_REQ 的时刻 = T0

传输窗口:
[T0 + WinOffset, T0 + WinOffset + WinSize]

在这个窗口内,双方切换到第一个数据信道,准备发送第一个包

2. 第一个数据包

连接建立后,主设备发送第一个数据包(通常是空包或 LL_CONNECTION_UPDATE_REQ):

复制代码
主设备                           从设备
   |                               |
   |  (数据通道,接入地址=AA)       |
   |------- LL_DATA (空包) ------->|
   |                               | (计算下一个信道)
   |<------ LL_DATA (空包) --------|
   |                               |
   |  (连接已建立,开始数据交互)    |
   |<=============================>|

六、完整交互时序图(带帧细节)

复制代码
广播者 (Advertiser)                    发起者 (Initiator)
    |                                        |
    |  === 广播通道 37 (2402 MHz) ===        |
    |                                        |
    |  ADV_IND                               |
    |  ├─ Header: Type=0x00, Len=0x1E        |
    |  ├─ AdvA: 06:1A:FF:4C:00:02            |
    |  └─ AdvData: [iBeacon 数据]            |
    |--------------------------------------->|
    |                                        |
    |  === 广播通道 38 (2426 MHz) ===        |
    |                                        |
    |  ADV_IND (同上)                        |
    |--------------------------------------->|
    |                                        |
    |  === 广播通道 39 (2480 MHz) ===        |
    |                                        |
    |  ADV_IND (同上)                        |
    |--------------------------------------->|
    |                                        |
    |                              (发起者决定连接)
    |                                        |
    |  === 广播通道 39 (接收到的信道) ===    |
    |                                        |
    |  CONNECT_REQ                           |
    |  ├─ Header: Type=0x05, Len=0x22        |
    |  ├─ InitA: 手机地址                    |
    |  ├─ AdvA: 06:1A:FF:4C:00:02            |
    |  └─ LLData:                            |
    |      ├─ AA: 0x12345678                 |
    |      ├─ Interval: 24 (30ms)            |
    |      ├─ Latency: 0                     |
    |      ├─ Timeout: 1000 (10s)            |
    |      ├─ ChannelMap: 0x1FFFFFFFFF       |
    |      └─ Hop: 5                         |
    |<---------------------------------------|
    |                                        |
    |  (广播者切换到数据通道)                 |
    |                                        |
    |  === 数据通道(根据跳频算法)===       |
    |                                        |
    |  第一个连接事件(通道计算后)           |
    |                                        |
    |  LL_DATA (空包,接入地址=0x12345678)   |
    |<---------------------------------------|
    |                                        |
    |  LL_DATA (空包)                        |
    |--------------------------------------->|
    |                                        |
    |  连接建立完成,开始 GATT 交互           |
    |<======================================>|

七、连接参数协商

连接建立后,双方可以通过 LL_CONNECTION_UPDATE_REQ 调整参数:

复制代码
主设备                              从设备
   |                                  |
   |  LL_CONNECTION_UPDATE_REQ        |
   |  ├─ Interval: 新连接间隔          |
   |  ├─ Latency: 新延迟              |
   |  ├─ Timeout: 新超时              |
   |  └─ ChannelMap: 新信道映射        |
   |--------------------------------->|
   |                                  |
   |  LL_CONNECTION_UPDATE_RSP        |
   |<---------------------------------|
   |                                  |
   |  新参数生效                       |

八、常见连接参数示例

设备类型 连接间隔 延迟 超时 功耗
鼠标 7.5ms 0 1s
键盘 15ms 0 2s
手环(活动时) 30ms 0 4s
手环(待机) 125ms 4 6s
传感器 500ms 8 10s 很低
资产标签 1s 10 20s 极低

九、总结

阶段 帧类型 作用 关键参数
广播 ADV_IND 宣告存在 AdvA, AdvData
扫描 SCAN_REQ/RSP 获取更多信息 ScanA, ScanRspData
连接 CONNECT_REQ 建立连接 AA, Interval, ChannelMap, Hop
同步 LL_DATA 确认连接 第一个数据包
协商 LL_CONNECTION_UPDATE_REQ 优化参数 新连接参数

核心要点

  1. CONNECT_REQ 在广播信道 发送,但之后切换到数据信道

  2. 接入地址跳频算法确保多个连接互不干扰

  3. 连接间隔从设备延迟是功耗和延迟的关键

  4. 连接建立后,双方通过相同的跳频算法独立计算下一个信道

连接后的数据收发

连接建立后,双方可以互相发送数据,但角色不对称------主设备(Master)和从设备(Slave)的收发机制有明确分工。让我详细拆解。


一、连接后的角色与职责

角色 职责 收发能力
主设备 (Master) 决定连接时序,定期发起连接事件 可发可收
从设备 (Slave) 跟随主设备时序,在指定窗口回复 可发可收,但不能主动发起

关键 :从设备只能在主设备发起的连接事件中回复,不能主动发送数据。


二、数据收发的核心机制:连接事件

连接建立后,双方在数据信道 上以连接事件为单位进行通信。

复制代码
时间轴:
                   连接事件 N                连接事件 N+1
                        |                         |
  主设备:  [TX]----等待----[RX]   空闲    [TX]----等待----[RX]
  从设备:  [RX]----等待----[TX]   空闲    [RX]----等待----[TX]
          |<---连接间隔--->|               |<---连接间隔--->|

每个连接事件中:

  1. 主设备先发送(可以是数据包或空包)

  2. 从设备在 150μs 后回复(可以是数据包或空包)


三、数据包结构详解

1. 链路层数据包格式

复制代码
┌─────────────────────────────────────────────────────────────┐
│                   链路层数据包                               │
├──────────┬──────────────┬──────────────────┬────────────────┤
│ 前导码    │ 接入地址      │ 协议数据单元(PDU) │ CRC            │
│ 1 字节   │ 4 字节       │ 2-258 字节       │ 3 字节         │
└──────────┴──────────────┴──────────────────┴────────────────┘

2. 数据信道 PDU 结构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                   数据信道 PDU                               │
├──────────────────────┬──────────────────────────────────────┤
│      Header          │              Payload                 │
│      2 字节          │          0-251 字节                  │
└──────────────────────┴──────────────────────────────────────┘

Header 详细(16 位)

复制代码
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 15 │ 14 │ 13 │ 12 │ 11 │ 10 │ 9  │ 8  │ 7  │ 6  │ 5  │ 4  │ 3  │ 2  │ 1  │ 0  │
├────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┤
│                         Length (8 bits)                       │
│                         Payload 长度                          │
├───────────────────────────────────────────────────────────────────────────────┤
│  RFU │  MD │  SN  │  NESN │  LLID (2 bits) │  RFU  │  LLID (cont)  │
└───────────────────────────────────────────────────────────────────────────────┘

关键字段

字段 位数 说明
LLID 2 链路层 ID:标识数据类型
NESN 1 下一个期望序列号(确认机制)
SN 1 序列号(防重放)
MD 1 更多数据标志
Length 8 Payload 长度

LLID 值含义

LLID 含义
01 LL 数据(续传)
10 LL 数据(起始)
11 LL 控制包

四、数据发送机制

1. 主设备 → 从设备

复制代码
主设备                              从设备
   |                                  |
   |  (连接事件开始)                   |
   |                                  |
   |  LL_DATA (主设备数据)            |
   |  ├─ SN = 0                       |
   |  ├─ NESN = 0                     |
   |  └─ Payload: "Hello"             |
   |--------------------------------->|
   |                                  |
   |                      (从设备回复确认)
   |  LL_DATA (从设备数据/空包)        |
   |  ├─ SN = 0                       |
   |  ├─ NESN = 1 (确认收到主设备包)   |
   |  └─ Payload: "Hi back"           |
   |<---------------------------------|
   |                                  |
   |  (连接事件结束)                   |

2. 从设备 → 主设备

从设备不能主动发起 发送,但可以在主设备发起的连接事件中携带数据

复制代码
主设备                              从设备
   |                                  |
   |  (连接事件开始)                   |
   |                                  |
   |  LL_DATA (空包,仅用于维持连接)   |
   |  ├─ SN = 0                       |
   |  ├─ NESN = 0                     |
   |  └─ Payload: 空                  |
   |--------------------------------->|
   |                                  |
   |                      (从设备利用这个窗口发送数据)
   |  LL_DATA (从设备数据)             |
   |  ├─ SN = 0                       |
   |  ├─ NESN = 1                     |
   |  └─ Payload: "Data from slave"   |
   |<---------------------------------|

关键:从设备只能在主设备发起的连接事件中回复数据,不能主动发起传输。


五、可靠传输机制:SN 和 NESN

BLE 使用 SN(序列号)NESN(下一个期望序列号) 实现可靠传输和确认。

1. 工作机制

复制代码
主设备                              从设备
(SN=0, NESN=0)                    (SN=0, NESN=0)
   |                                  |
   |  LL_DATA (SN=0, NESN=0)          |
   |  发送数据包 A                     |
   |--------------------------------->|
   |                                  |
   |                      (从设备收到包 A)
   |                      (SN=0 匹配期望)
   |                      (处理数据)
   |                      (翻转 NESN 表示确认)
   |  LL_DATA (SN=0, NESN=1)          |
   |  确认收到包 A,并发送自己的数据    |
   |<---------------------------------|
   |                                  |
   |  (主设备看到 NESN=1)              |
   |  (确认从设备收到了包 A)           |
   |  (翻转自己的 SN)                  |
   |                                  |
   |  LL_DATA (SN=1, NESN=1)          |
   |  发送新数据包 B                   |
   |--------------------------------->|

2. 重传机制

如果包丢失,对方不会翻转 NESN,发送方会重传:

复制代码
主设备                              从设备
(SN=0, NESN=0)                    (SN=0, NESN=0)
   |                                  |
   |  LL_DATA (SN=0, NESN=0)          |
   |  发送数据包 A                     |
   |--------------------------------->| (包丢失!)
   |                                  |
   |  (超时,没有收到回复)             |
   |  (NESN 没有翻转)                 |
   |                                  |
   |  LL_DATA (SN=0, NESN=0)          |
   |  重传数据包 A                     |
   |--------------------------------->|
   |                                  |
   |                      (从设备收到包 A)
   |  LL_DATA (SN=0, NESN=1)          |
   |<---------------------------------|

3. SN/NESN 状态机

复制代码
初始状态:
发送方 SN=0, 接收方期望 NESN=0

发送方发送 SN=0 的包:
- 接收方收到 SN 匹配期望 → 处理数据,NESN 翻转为 1
- 接收方收到 SN 不匹配 → 丢弃包,NESN 不变

发送方收到回复:
- NESN 翻转 → 确认对方收到,SN 翻转
- NESN 不变 → 对方没收到,重传(SN 不变)

六、GATT 层的数据收发

链路层之上是 GATT 层,应用数据通过 GATT 协议传输。

1. 客户端 → 服务器(写操作)

复制代码
客户端(手机)                      服务器(心率带)
      |                                  |
      |  (连接事件中)                     |
      |                                  |
      |  ATT_WRITE_REQ                    |
      |  ├─ Handle: 0x0004 (CCCD)        |
      |  └─ Value: 0x0001 (启用通知)     |
      |--------------------------------->|
      |                                  |
      |  ATT_WRITE_RSP                    |
      |<---------------------------------|

2. 服务器 → 客户端(通知)

复制代码
客户端(手机)                      服务器(心率带)
      |                                  |
      |  (连接事件中)                     |
      |                                  |
      |  LL_DATA (空包,维持连接)         |
      |--------------------------------->|
      |                                  |
      |                      (服务器有数据要发送)
      |  ATT_HANDLE_VALUE_NTF             |
      |  ├─ Handle: 0x0003 (心率值)      |
      |  └─ Value: 72 (当前心率)         |
      |<---------------------------------|
      |                                  |
      |  (通知不需要确认,直接发下一个)    |

3. 服务器 → 客户端(指示)

指示需要确认,确保可靠传输:

复制代码
客户端(手机)                      服务器(心率带)
      |                                  |
      |  (连接事件中)                     |
      |                                  |
      |  LL_DATA (空包)                   |
      |--------------------------------->|
      |                                  |
      |  ATT_HANDLE_VALUE_IND             |
      |  ├─ Handle: 0x0005               |
      |  └─ Value: 重要告警              |
      |<---------------------------------|
      |                                  |
      |  ATT_HANDLE_VALUE_CFM             |
      |--------------------------------->|

七、多包传输与流控

1. MD 位(更多数据)

MD 位告诉对方还有更多数据要发送:

复制代码
主设备                              从设备
   |                                  |
   |  LL_DATA (MD=1, 还有数据)        |
   |--------------------------------->|
   |                                  |
   |  LL_DATA (MD=0, 这是最后一个)    |
   |--------------------------------->|
   |                                  |
   |  LL_DATA (回复)                  |
   |<---------------------------------|

2. 数据长度扩展(DLE)

BLE 4.2+ 支持数据长度扩展,可发送更大的包:

版本 最大 Payload 说明
BLE 4.0/4.1 27 字节 默认
BLE 4.2+ 251 字节 启用 DLE 后
复制代码
主设备                              从设备
   |                                  |
   |  LL_LENGTH_REQ (请求 DLE)        |
   |--------------------------------->|
   |                                  |
   |  LL_LENGTH_RSP (同意)            |
   |<---------------------------------|
   |                                  |
   |  LL_DATA (251 字节大包)          |
   |--------------------------------->|

八、完整收发示例:手机与心率带

复制代码
手机 (主设备)                      心率带 (从设备)
   |                                  |
   |  ===== 连接建立 =====             |
   |                                  |
   |  --- 发现服务 ---                 |
   |  ATT_READ_BY_GROUP_REQ            |
   |--------------------------------->|
   |  ATT_READ_BY_GROUP_RSP            |
   |<---------------------------------|
   |                                  |
   |  --- 发现特征 ---                 |
   |  ATT_READ_BY_TYPE_REQ             |
   |--------------------------------->|
   |  ATT_READ_BY_TYPE_RSP             |
   |<---------------------------------|
   |                                  |
   |  --- 启用通知 ---                 |
   |  ATT_WRITE_REQ (CCCD=0x0001)     |
   |--------------------------------->|
   |  ATT_WRITE_RSP                    |
   |<---------------------------------|
   |                                  |
   |  ===== 数据收发 =====             |
   |                                  |
   |  LL_DATA (空包,维持连接)         |
   |--------------------------------->|
   |                      (心率变化 72)
   |  ATT_HANDLE_VALUE_NTF (心率=72)   |
   |<---------------------------------|
   |                                  |
   |  LL_DATA (空包)                  |
   |--------------------------------->|
   |                      (心率变化 73)
   |  ATT_HANDLE_VALUE_NTF (心率=73)   |
   |<---------------------------------|
   |                                  |
   |  --- 读取电池电量 ---             |
   |  ATT_READ_REQ (Handle 0x0010)    |
   |--------------------------------->|
   |  ATT_READ_RSP (电量=85%)         |
   |<---------------------------------|

九、总结

问题 答案
双方可以互发数据吗? 可以,但从设备只能在主设备发起的连接事件中回复
主设备怎么发? 在每个连接事件开始时发送 LL_DATA 包
从设备怎么发? 在主设备发送后,在 150μs 内回复时携带数据
如何保证可靠? SN/NESN 机制:发送方等待确认,无确认则重传
通知和指示的区别? 通知无确认,速度快;指示有确认,可靠但慢
一次能发多少? 默认 27 字节,启用 DLE 后可达 251 字节
怎么流控? MD 位告诉对方还有更多数据

一句话总结

连接建立后,主设备在每个连接事件中先发送数据,从设备在 150μs 内回复;通过 SN/NESN 机制保证可靠传输,从设备不能主动发起,但可以在回复中携带数据。

相关推荐
我才是一卓2 小时前
linux 安装简易 git 服务端并使用
linux·运维·git
Wanliang Li2 小时前
AArch64虚拟化——virtio-mmio实现
linux·虚拟化·virtio·hypervisor·mmio
嵌入式-老费3 小时前
vivado hls的应用(第一个axi接口的ip)
linux·服务器·tcp/ip
旺仔.2913 小时前
Linux系统基础详解(二)
linux·开发语言·网络
x***r1513 小时前
Notepad++ 8.6 安装教程:详细步骤+自定义安装路径(附注意事项)
linux·前端·javascript
big_rabbit05023 小时前
JVM堆内存查看命令
java·linux·算法
王小义笔记4 小时前
WSL(Linux)如何安装conda
linux·运维·conda
偷懒下载原神4 小时前
【linux操作系统】信号
linux·运维·服务器·开发语言·c++·git·后端
源远流长jerry4 小时前
RDMA 传输服务详解:可靠性与连接模式的深度剖析
linux·运维·网络·tcp/ip·架构