BLE 连接建立与连接参数详解:从 CONNECT_IND 到断开
- 一、连接建立:CONNECT_IND 与第一次握手窗口
- 1.1 CONNECT_IND 逐字段解析
- 1.2 Access Address 与 CRC Init:连接前后的分界线
- 1.3 时间单位:1.25 ms 与 10 ms
- 1.4 第一次握手窗口:Window Size / Offset
- 二、连接事件循环
- 2.1 一问一答:T_IFS = 150 μs
- 2.2 Empty PDU:给从机"说话的机会"
- 2.3 MD 位:连接事件可以提前结束
- 2.4 Slave Latency:从机可以"睡过"几个事件
- 三、连接后的六次协商
- 3.1 Feature Exchange:互相通报能力
- 3.2 Version Exchange:实际版本取双方较小值
- 3.3 DLE 协商:一个数据包能装多少字节
- 3.4 连接参数更新:30 ms → 7.5 ms
- 3.4.1 为什么切换需要 Window Size/Offset + Instant?
- 3.4.2 为什么手机要加快?
- 3.4.3 Offset 和 Preferred Periodicity 是干嘛的?
- 3.5 PHY 更新:切到 2M 或 Coded
- 3.6 Channel Map 更新:自适应跳频(AFH)
- 四、断开连接:LL_TERMINATE_IND
全文脉络:
广播阶段 连接建立 连接后的协商(LL 控制过程) ────────── ───────── ───────────────────────── ADV_IND ──► CONNECT_IND ──► ① Feature Exchange(特性交换) (广播者) (发起者/手机) ② Version Exchange(版本交换) ③ DLE 协商(数据长度扩展) SCAN_RSP ◄── ④ Connection Parameter Update (可选) ▼ ⑤ PHY Update(切 2M / Coded) 连接事件循环 ⑥ Channel Map Update(AFH 自适应跳频) (每 connInterval 一次) ⑦ Terminate(断开连接) │ ▼ GATT 层:服务发现 / 订阅 / 通知一句话概括:CONNECT_IND把连接参数一次性定下来,之后双方按connInterval周期碰头;碰头过程中再通过 LL 控制包不断"谈判"优化参数。
一、连接建立:CONNECT_IND 与第一次握手窗口
CONNECT_IND是手机(Initiator / Central)向板子(Advertiser / Peripheral)发出的连接请求。
它的 Payload 由三段组成:InitA(6) + AdvA(6) + LLData(22)——参数全在那 22 字节的 LLData 里。
1.1 CONNECT_IND 逐字段解析
| 字段 | 抓到的值 | 含义 |
|---|---|---|
| Initiator Address | 4e:b2:86:dd:a2:3d | 手机的 MAC(随机地址) |
| Advertising Address | 7c:e8:b1:aa:da:36 | 板子 的 MAC |
| Access Address | 0x742e6ed6 | ★ 连接后的新同步字,后续数据包全用它 |
| CRC Init | 0xb248a4 | 连接后 CRC 的初始值 |
| Window Size | 2(2.5 ms) | 第一个传输窗口的大小 |
| Window Offset | 16(20 ms) | 从 CONNECT_IND 结束到第一个窗口的偏移 |
| Interval | 24(30 ms) | 连接间隔,双方每 30 ms 通信一次 |
| Latency | 0 | 从机可以跳过的连接事件数 |
| Timeout | 500(5000 ms) | 连接超时,5 秒没收到就断开 |
| Channel Map | 1fff000000 | ★ 可用数据信道图 |
| Hop | 5 | 跳频步长,规范要求是5~16的随机值 |
| SCA | 31~50 ppm | 中央设备(手机)的睡眠时钟精度 |
LLData 的完整字段布局:
AA CRCInit WinSize WinOffset Interval Latency Timeout ChM Hop SCA (4 octets) (3 octets) (1 oct) (2 oct) (2 oct) (2 oct) (2 oct) (5 oct) (5 bits) (3 bits)1.2 Access Address 与 CRC Init:连接前后的分界线
| 广播阶段(信道 37/38/39) | 连接阶段(信道 0~36) | |
|---|---|---|
| Access Address | 固定0x8E89BED6 | 随机 32-bit,由 CONNECT_IND 指定 |
| CRC 初值 | 固定0x555555 | 随机值,由 CONNECT_IND 的 CRCInit 指定 |
两者每次连接都不同—— 所以它们天然可以当作 “这是哪一次连接” 的指纹。
1.3 时间单位:1.25 ms 与 10 ms
BLE 物理层时隙是625 μs,连接参数以两个时隙为基本单位:
1.25 ms = 2 × 625 μs| 参数 | 单位 | 抓到的原始值 | 实际时间 |
|---|---|---|---|
| transmitWindowSize | 1.25 ms | 2 | 2 × 1.25 =2.5 ms✅ |
| transmitWindowOffset | 1.25 ms | 16 | 16 × 1.25 =20 ms✅ |
| connInterval | 1.25 ms | 24 | 24 × 1.25 =30 ms✅ |
| connSupervisionTimeout | 10 ms | 500 | 500 × 10 =5000 ms = 5 s✅ |
| connSlaveLatency | 无单位(次数) | 0 | 0 次 |
⚠️唯一例外:Timeout 的步长是10 ms,不是 1.25 ms。这是最容易记混的一个。
1.4 第一次握手窗口:Window Size / Offset
连接建立的瞬间有个根本问题:广播者和发起者的时钟不同步。
手机发完 CONNECT_IND 后,从机并不知道手机会在精确的哪一刻发来第一个数据包。所以 BLE 设计了一个容差窗口:
手机说:“我会在 CONNECT_IND 结束后约20 ms开始,在接下来的2.5 ms窗口内,找个时间发第一个包。你(从机)在那 2.5 ms 里把接收机打开等着。”
| 要点 | 说明 |
|---|---|
| 只用一次 | Window Size/Offset只用于第一个连接事件。之后双方已同步,按精确 Interval 走 |
| 从机怎么做 | 收到 CONNECT_IND → 等约 20 ms → 开接收机持续 2.5 ms → 收到就同步成功 |
| 窗口内没收到 | 从机在下一个 Interval(30 ms 后)再开一次窗口,继续等 |
二、连接事件循环
连接建立后,主从周期性碰头。每一次碰头的机会就叫一个Connection Event:
|<-- connInterval 30ms -->|<-- connInterval 30ms -->| ▲ ▲ 连接事件 #1 连接事件 #2在每个事件里:主机先发一个包 → 从机回一个包 → 如果还有数据就继续收发 → 双方都没数据了就关闭射频睡觉,等下一个 Interval。
2.1 一问一答:T_IFS = 150 μs
|<──────── 一个 Connection Event ────────>| 主机 ────[包]────► 从机 ← 主机发起(必须有) T_IFS=150μs 主机 ◄───[包]───── 从机 ← 从机回应(必须有)规范原文:“The Inter Frame Space is designatedT_IFSand shall be150 µs.”
从机收到后必须在 150 μs 内回应,否则超时重传。
2.2 Empty PDU:给从机"说话的机会"
核心规则:BLE 连接建立后,从机永远不能主动发起通信,必须等主机先发包,然后在 150 μs 内回应。
所以就有了 Empty PDU:
主机 ────[Empty PDU]────► 从机 ← 主机:"我没啥要发的,你呢?" 主机 ◄───[心率 Notification]── 从机 ← 从机:"正好,我有数据!"没有这个机制,从机有数据也只能干等着(BLE 不允许从机主动"打断"主机)。
💡 这也解释了为什么connInterval 直接决定了数据上报延迟。
2.3 MD 位:连接事件可以提前结束
数据信道 PDU 的 Header 里有个MD(More Data)位:MD = 1表示 “我还有数据,继续传”,MD = 0表示 “我没数据了”。
主机 ──[数据包 MD=1]──► 从机 主机 ◄─[数据包 MD=1]─── 从机 ← 还有数据,继续 主机 ──[数据包 MD=0]──► 从机 主机 ◄─[数据包 MD=0]─── 从机 ← 都没数据了 → 事件关闭 ┌──────────────────┐ │ 射频关闭,睡觉 │ │ 等下一个 30ms │ └──────────────────┘所以连接事件不一定占满整个 Interval—— 传完就睡,这是 BLE 省电的关键之一。
2.4 Slave Latency:从机可以"睡过"几个事件
问题:如果每 30 ms 都要唤醒射频收发一次(哪怕只是空包),功耗很可观。
解决:允许从机跳过一些连接事件。
Latency = 0(每次都醒):
主机: ● ● ● ● ● ● ● ● 从机: ● ● ● ● ● ● ● ● ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 每次都醒,每次都收发(哪怕只是空包) 有效通信周期 = 30 ms 功耗:最高 延迟:最低Latency = 4(可跳 4 次,每 5 个事件响应 1 次):
主机: ● ● ● ● ● ● ● ● ● ● ● ← 主机每个事件都发 从机: ● ● ● ← 从机只醒第 1、6、11... |<----- 150 ms ----->| (5 × 30 ms = 从机实际醒来的间隔) 从机少醒了 4/5 = 80% 功耗:大幅降低 延迟:最高 150 ms关键理解:Slave Latency 不是"主机不发",而是"从机可以不听"。
主机每个连接事件照常发(否则连接就断了),从机可以睡过 N 个事件,只在第 N+1 个醒来收。
约束公式:
connSupervisionTimeout > (1 + connSlaveLatency) × connInterval × 2Latency 设得太大,导致左边接近甚至超过 Timeout,连接就会被误判为超时断开。
从机什么时候不能睡?有数据要上报时(比如心率有新值)必须立刻醒来发;主机发了需要响应的包也必须回。
所以Latency 是"没事的时候可以睡",不是"强制睡"。
工程上怎么选?核心权衡永远是「延迟 ↔ 功耗」:
| 场景 | 推荐参数 |
|---|---|
| 心率手环(省电优先,延迟不敏感) | Interval 大(100 ms ~ 1 s)+ Latency 高(4~20) |
| 游戏手柄 / 遥控器(低延迟) | Interval 小(7.5~15 ms)+ Latency = 0 |
| 固件升级 OTA(要吞吐) | Interval 小(7.5 ms)+ Latency 0 + 开 DLE |
| 传感器定时上报 | Interval 中等 + Latency 适中 |
三、连接后的六次协商
连接建立后,双方会按需进行一系列 LL 控制过程。它们有一个共同的套路:
① REQ ── 发起方探测/提议 ② RSP ── 回应方答复 ③ IND ── 中央设备拍板,并指定 Instant(从第几个连接事件起生效)(只有"必须双方同时切换"的过程才需要第 ③ 步,比如 PHY 和连接参数。)
3.1 Feature Exchange:互相通报能力
连接建立后第一个协商过程:
FeatureSet = 0xf9ff,逐位解读:
| Bit | 特性 | 值 | 说明 |
|---|---|---|---|
| 0 | LE Encryption | ✅ | 支持加密(配对 / bonding 的基础) |
| 1 | Connection Parameters Request | ✅ | 支持连接参数更新流程 |
| 2 | Extended Reject Indication | ✅ | 拒绝时能带原因码 |
| 3 | Peripheral Initiated Features Exchange | ✅ | 从机可以主动发起特性交换 |
| 4 | LE Ping | ✅ | 链路层保活探测 |
| 5 | LE Data Packet Length Extension (DLE) | ✅ | 数据长度扩展 |
| 6 | LL Privacy | ✅ | 链路层隐私(随机地址) |
| 7 | Extended Scanner Filter Policies | ✅ | 扩展扫描过滤策略 |
| 8 | LE 2M PHY | ✅ | 2M 速率(需切换才生效) |
| 9 | Stable Modulation Index - Tx | ❌ | 发射机稳定调制指数 |
| 10 | Stable Modulation Index - Rx | ❌ | 接收机稳定调制指数 |
| 11 | LE Coded PHY | ✅ | 长距离模式(BLE 5.0) |
| 12 | LE Extended Advertising | ✅ | 扩展广播 |
| 13 | LE Periodic Advertising | ✅ | 周期性广播 |
| 14 | Channel Selection Algorithm #2 | ✅ | CSA #2 |
| 15 | LE Power Class 1 | ✅ | 支持 Class 1 发射功率 |
注意 bit 3 = 1,所以从机才会主动回一个
LL_PERIPHERAL_FEATURE_REQ。
另外,LL_FEATURE_RSP的 FeatureSet 字段 =发送这个 RSP 的那一方自己支持的特性。
3.2 Version Exchange:实际版本取双方较小值
手机: BLE 6.0 板子: BLE 5.0 ──────────────── 有效版本: BLE 5.03.3 DLE 协商:一个数据包能装多少字节
DLE =Data Length Extension(数据长度扩展)。
| 无 DLE(BLE 4.0/4.1) | 有 DLE(BLE 4.2+) | |
|---|---|---|
| 数据信道 PDU 最大 Payload | 27 字节 | 251 字节 |
| 单包有效数据(ATT 层) | 20 字节 | 244 字节 |
| 传 1 KB 数据需要 | ~51 个包 | ~5 个包 |
协商用的四个字段:
| 字段 | 含义 |
|---|---|
| Max RX octets | 我作为接收方,最多能收的 payload 字节数 |
| Max RX time | 我作为接收方,最多能收的射频时长 |
| Max TX octets | 我作为发送方,最多能发的 payload 字节数 |
| Max TX time | 我作为发送方,最多能发的射频时长 |
① 手机 → 板子
② 板子 → 手机
③ 手机 → 板子
④ 板子 → 手机
★ 关键公式:每个方向的有效长度
某方向的有效长度 = min( 对端的 MaxRX , 本端的 MaxTX )套用到本次抓包:
手机 → 板子 = min(板子 MaxRX=251, 手机 MaxTX=27) = 27 字节 板子 → 手机 = min(手机 MaxRX=251, 板子 MaxTX=251) = 251 字节结论:这次连接是「非对称」的—— 板子给手机发(心率通知)可以一次发 251 字节,手机给板子发(写特征值)一次只能 27 字节。
3.4 连接参数更新:30 ms → 7.5 ms
这是抓包里连接参数更新的完整流程(手机主动要求把连接加快):
手机 ── LL_CONNECTION_PARAM_REQ ──► 板子 "我想把连接间隔改成 7.5ms" 手机 ◄── LL_CONNECTION_PARAM_RSP ── 板子 "同意" 手机 ── LL_CONNECTION_UPDATE_IND ──► 板子 "从现在起第 17 个连接事件开始生效"① LL_CONNECTION_PARAM_REQ(手机发起)
| 字段 | 值 | 含义 |
|---|---|---|
| Interval Min / Max | 6 / 6 | 6 × 1.25 ms =7.5 ms |
| Latency | 0 | 不跳过任何连接事件 |
| Timeout | 500 | 500 × 10 ms = 5000 ms |
| Preferred Periodicity | 0 | 手机对事件偏移"无偏好" |
| Reference Connection Event Count | 6 | 计算偏移的参考锚点 |
| Offset 0~5 | 5, 0, 1, 3, 4, 65535 | 建议的连接事件偏移(65535 = 无效) |
② LL_CONNECTION_PARAM_RSP(板子回应)
大部分和 REQ 相同,只有Preferred Periodicity变成1(板子表示"我接受你的参数,并且偏好周期性为 1")。
板子接受了手机提议的 7.5 ms 间隔,所以 RSP 几乎照抄 REQ。
如果板子不接受,RSP 里会填不同的 Interval Min/Max—— 这时手机可以选择接受板子的提议,或者发起断开。
③ LL_CONNECTION_UPDATE_IND(手机下发更新指令)
| 字段 | 值 | 含义 |
|---|---|---|
| Window Size | 1 | 更新后的第一个事件窗口 = 1.25 ms |
| Window Offset | 5 | 更新后的第一个事件偏移 = 6.25 ms |
| Interval | 6 | 新连接间隔 = 7.5 ms |
| Latency | 0 | 不变 |
| Timeout | 500 | 5000 ms,不变 |
| Instant | 17 | 第 17 个连接事件时切换新参数 |
3.4.1 为什么切换需要 Window Size/Offset + Instant?
参数从 30 ms 切到 7.5 ms,时序会发生跳变。切换的那一瞬间,双方需要一个 “重新对齐的窗口”:
- Instant = 17:双方都从 CONNECT_IND 之后开始数连接事件,数到第 17 个时同时切换。这样不需要额外通信就能保证同步。
- Window Size = 1(1.25 ms):板子在这个窗口内打开接收机,等待新参数的第一个包。
3.4.2 为什么手机要加快?
最可能的原因是:nRF Connect App 进入了活跃状态。
| 阶段 | 典型 Interval | 场景 |
|---|---|---|
| 刚连接 | 30~50 ms | 低功耗,后台维持 |
| 用户打开 App 交互 | 7.5~15 ms | 低延迟,服务发现、读写 |
| 稳定传输数据 | 7.5~30 ms | 根据应用调整 |
| 长时间空闲 | 100~1000 ms | 超低功耗 |
反过来,很多产品连接成功后,从机会主动发起参数更新,把 Interval 拉大(100 ms ~ 1 s)、Latency 设成 4~10,进入省电模式。
3.4.3 Offset 和 Preferred Periodicity 是干嘛的?
这是给手机同时连接多个 BLE 设备时用的 —— 它希望这些设备的连接事件错开,不要撞在一起:
设备 A 的连接事件:t=0, 7.5ms, 15ms, ... 设备 B 的连接事件:t=2.5ms, 10ms, 17.5ms, ... ← 错开- Preferred Periodicity = 0:发起方(手机)没偏好
- Preferred Periodicity = 1:从机希望按某个周期错开
- Offset 0 ~ 5:具体偏置值列表,65535 表示 “这个选项无效”
本例的 Offset 列表是[5, 0, 1, 3, 4, 65535],说明有5 个有效偏置建议,单位 1.25 ms。
从LL_CONNECTION_UPDATE_IND的 Window Offset = 5 可以看出:主机最终接受了 Offset0 = 5(6.25 ms)。
3.5 PHY 更新:切到 2M 或 Coded
连接建立后想把 1M 换成 2M(或 Coded)
① LL_PHY_REQ(手机 → 板子)
| 字段 | 值 | 含义 |
|---|---|---|
| TX PHYs | 0x07 | 手机发送时支持 1M / 2M / Coded |
| RX PHYs | 0x02 | 手机希望接收时只用 2M |
② LL_PHY_RSP(板子 → 手机)
| 字段 | 值 | 含义 |
|---|---|---|
| TX PHYs | 0x07 | 板子发送时支持 1M / 2M / Coded |
| RX PHYs | 0x07 | 板子接收时支持 1M / 2M / Coded |
③ LL_PHY_UPDATE_IND(手机 → 板子,最终决定)
| 字段 | 值 | 含义 |
|---|---|---|
| Central → Peripheral PHY | 0x02 | 手机发给板子用2M |
| Peripheral → Central PHY | 0x02 | 板子发给手机用2M |
| Instant | 60 | 在第 60 个连接事件时切换 PHY |
3.6 Channel Map 更新:自适应跳频(AFH)
手机(Central)告诉板子(Peripheral):
“新的可用数据信道图是
0xffcff1f,从第 125 个连接事件开始生效。”
| 字段 | 值 | 含义 |
|---|---|---|
| Control Opcode | 0x01 | LL_CHANNEL_MAP_IND |
| Channel Map | 0xffcff1f | 数据信道 0~36 的可用性位图 |
| Instant | 125 | 新信道图在第 125 个连接事件生效 |
Channel Map 的正确解码方法
① bit N ←→ 数据信道 N (N = 0 ~ 36) ② 1 = 可用(Used),0 = 不可用(Unused) ③ 字节按【小端】拼成一个整数(octet0 对应信道 0~7)以0x0ffcffff1f(即0x0FFCFFFF1F,10 位十六进制 = 5 字节)为例,逐字节拆开:
| 字节(小端序) | 值 | 二进制 | 对应信道 | 状态 |
|---|---|---|---|---|
| octet0(最右字节) | 0x1F | 0001 1111 | 0~7 | 0~4 ✅ 可用;5~7 ❌ |
| octet1 | 0xFF | 1111 1111 | 8~15 | 全部 ✅ |
| octet2 | 0xFF | 1111 1111 | 16~23 | 全部 ✅ |
| octet3 | 0xFC | 1111 1100 | 24~31 | 26~31 ✅ 可用;24~25 ❌ |
| octet4(最左字节) | 0x0F | 0000 1111 | 32~36 | 32~35 ✅ 可用;36 ❌ |
四、断开连接:LL_TERMINATE_IND
| 字段 | 值 | 含义 |
|---|---|---|
| LLID | 0x3 | Control PDU(链路层控制报文) |
| Control Opcode | 0x02 | LL_TERMINATE_IND |
| Error Code | 0x13 | Remote User Terminated Connection |
| Length | 2 | 只有 Opcode + Error Code 两个字节 |
Error Code 0x13=Remote User Terminated Connection—— 有一方(手机或板子)的用户/应用层主动点了断开,然后通过这个链路层 PDU 通知对方。
注意:这里的 “Error” 不是"故障",BLE 里断开原因统一用这个字段表示。