4. 链路层连接建立与连接参数

好,咱们继续往下走。上一章我们把链路层的状态机、空中包格式和CRC讲透了。这一章,咱们要真正进入连接的世界。

说白了,BLE协议栈里最核心、也最容易出bug的地方,就是连接建立之后的那一套机制。我见过不少工程师,能把广播和扫描调通,但一到连接阶段就抓瞎——要么连不上,要么连上之后丢包严重,要么功耗高得离谱。

为什么会这样?

因为连接不是简单的“你发我收”。它有一套精密的时序约定、跳频规则和重传逻辑。不理解这些,你写出来的协议栈就是纸老虎。

4.1 连接建立过程:从广播到连接的握手

连接建立,本质上是一次精心设计的“双向确认”过程。我们来看看它到底怎么发生的。

核心流程:

  1. 发起者( Initiator ) 在指定的广播通道上,收到 广播者( Advertiser ) 的 ADV_IND 或 ADV_DIRECT_IND 包。
  2. 发起者发送 CONNECT_REQ 包。这个包不是简单的“你好”,它包含了后续连接的全部参数:接入地址、CRC初始值、窗口偏移、窗口大小、连接间隔、从机延迟、监督超时、跳频算法的初始信道映射和跳频增量。
  3. 广播者收到 CONNECT_REQ 后,解析参数,在指定的窗口偏移时间后,切换到连接状态的数据通道上,等待第一个连接事件。
  4. 发起者也切换到连接状态,在同样的时间点发送第一个连接事件的数据包。

嗯,这里要注意:CONNECT_REQ 包是在广播通道上发送的,但一旦握手完成,双方立刻跳转到数据通道。这个切换是硬实时的,差一个微秒都不行。

我记得有一次调试一个第三方模组,发现它总是连上之后立刻断连。抓空包一看,CONNECT_REQ 里的窗口偏移参数算错了,导致主从双方的时间基准没对齐。这种问题,光看代码是看不出来的,必须用逻辑分析仪或者BLE嗅探器去抓时序。

避坑指南:

我曾经在实现发起者状态机时,忽略了 CONNECT_REQ 发送后需要立即停止扫描。结果导致同一个连接被重复发起,从机端收到了两个不同的接入地址,直接崩溃。记住:发送 CONNECT_REQ 后,发起者必须立刻退出扫描状态,进入连接状态。

4.2 连接事件与连接间隔:BLE的“心跳”

连接建立之后,主从双方并不是一直在收发数据。它们按照约定的时间表,周期性地“碰面”。这个碰面,就叫 连接事件( Connection Event )。

每个连接事件,主机在约定的时间点发送一个数据包,从机必须在150微秒内回复。如果从机没回复,主机可以重传,但重传次数有限。如果连续多个连接事件都没有交互,连接就超时了。

连接间隔( Connection Interval ),就是两个相邻连接事件起始点之间的时间差。单位是1.25ms的整数倍,范围从7.5ms到4.0s。

参数 范围 说明
连接间隔 6 ~ 3200 ( ×1.25ms ) 实际时间 7.5ms ~ 4.0s
从机延迟 0 ~ 499 从机可以跳过最多N个连接事件不回复
监督超时 10 ~ 3200 ( ×10ms ) 100ms ~ 32s,超过此时间无交互则断连

你想想看,连接间隔设得越小,实时性越好,但功耗也越高。设得大,省电,但延迟就上去了。我个人的习惯是:对于交互频繁的场景(比如OTA升级),用15ms~30ms;对于传感器数据上报(比如温度计),用100ms~500ms;对于纯待机的心跳维持,用1s以上。

注意:

连接间隔不是越小越好。我曾经在一个项目中把连接间隔设成了7.5ms,结果发现主从双方都在疯狂收发,CPU占用率飙升,导致其他任务被饿死。后来改成15ms,问题解决。BLE不是实时操作系统,别把它当CAN总线用。

4.3 数据通道跳频算法:为什么BLE不怕干扰?

BLE在广播阶段只用3个通道(37/38/39),但进入连接后,会在37个数据通道上跳来跳去。这个跳频算法,是保证BLE在2.4GHz这个拥挤频段里存活的关键。

算法其实不复杂:

// 跳频算法核心逻辑
// unmappedChannel 是当前要计算的下一个通道索引
// hopIncrement 是跳频增量,由主机在 CONNECT_REQ 中指定
// numUsedChannels 是可用通道的数量
// channelMap 是一个40位的位图,标记哪些通道可用

uint8_t nextChannel = (lastUsedChannel + hopIncrement) % 37;

// 如果 nextChannel 在 channelMap 中被标记为“坏通道”
// 则执行重映射
if (!isChannelGood(nextChannel, channelMap)) {
    // 重新映射算法:将 unmappedChannel 映射到可用通道集合中
    uint8_t remappedIndex = nextChannel % numUsedChannels;
    nextChannel = getNthGoodChannel(remappedIndex, channelMap);
}

// 最后,将逻辑通道号映射到物理频率
// 通道 0~36 对应 2402MHz + 2MHz * channelIndex
uint16_t frequency = 2402 + 2 * nextChannel;

说白了,就是每次连接事件结束后,双方都按照这个公式计算下一个连接事件应该用哪个通道。如果算出来的通道被标记为“坏”(比如被Wi-Fi占了),就重新映射到下一个可用的好通道上。

我个人觉得,这个算法的精妙之处在于:主从双方不需要协商,各自独立计算就能得到相同的结果。只要初始参数一致,跳频序列就是确定的。这避免了额外的控制包开销。

关键点:

  • 跳频增量( hopIncrement )必须是 5~16 之间的奇数。为什么?因为奇数和37互质,能保证遍历所有通道。
  • 通道映射( channelMap )可以在连接过程中通过 LL_CHANNEL_MAP_REQ 更新。比如检测到某个通道干扰严重,可以动态把它踢出去。
  • 重新映射算法要保证均匀分布,不能把多个逻辑通道挤到同一个物理通道上。

我曾经遇到过一个坑:某个芯片的跳频实现有bug,当通道映射更新后,重新映射的索引计算溢出了。结果导致主从双方算出来的下一个通道不一致,连接直接断掉。查了三天才定位到是整数溢出问题。所以,实现跳频算法时,务必注意边界条件。

4.4 LLID与SN/NESN机制:可靠传输的基石

连接事件里,主从双方交换的数据包,靠什么来保证不丢、不重、不乱序?靠的就是 LLID 和 SN/NESN 这两个字段。

LLID(Logical Link Identifier)占2个bit,告诉接收方这个包是什么类型:

LLID值 含义 说明
00 LL Data PDU ( 保留 ) 规范中未使用
01 LL Data PDU ( 继续 ) L2CAP分片后的后续片段
10 LL Data PDU ( 开始 ) 完整的L2CAP包或分片的第一个片段
11 LL Control PDU 链路层控制包(如更新连接参数)

SN(Sequence Number)和 NESN(Next Expected Sequence Number)各占1个bit。它们共同实现了一个简单的停等协议:

  • SN:当前发送包的序列号(0或1)。
  • NESN:发送方期望接收到的下一个包的序列号。

工作原理是这样的:

  1. 主机发送一个包,SN=0,NESN=0。意思是:我发的这个包编号是0,我期望你下次发给我编号为0的包。
  2. 从机收到后,回复一个包,SN=0,NESN=1。意思是:我发的这个包编号是0,我收到了你的SN=0的包,下次请发SN=1的包给我。
  3. 主机收到回复,发现NESN=1,说明从机已经正确收到了SN=0的包。于是主机发下一个包,SN=1,NESN=0。
  4. 如果主机没收到回复,或者回复中的NESN不对,主机就重传上一个包(SN不变)。

你想想看,只有1个bit的序列号,够用吗?

够。因为BLE的停等协议是“发一个等一个”,不会出现连续发送多个包的情况。1个bit就能区分“当前包”和“下一个包”。

个人经验:

我刚开始实现SN/NESN时,犯过一个低级错误:在重传时忘了把SN置回原来的值,而是递增了。结果接收方发现序列号跳变,以为是新包,导致数据重复。记住:重传时,SN必须和第一次发送时一模一样。NESN也要保持不变,直到收到正确的回复。

另外,LLID=11的Control PDU是不受SN/NESN保护的。控制包有自己独立的确认机制(通过Opcode和响应包)。这个设计是为了避免控制面被数据面的重传阻塞。

4.5 本章知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个快速参考。

BLE链路层连接核心机制总览 BLE 链路层连接核心机制 连接建立 ADV → CONNECT_REQ → 切换 连接事件与间隔 周期碰面 · 150μs回复窗口 数据通道跳频 37通道 · 重映射 · 抗干扰 可靠传输机制:LLID + SN/NESN LLID: 01/10/11 SN/NESN: 停等协议 重传: SN不变 核心原则:连接建立后,双方在约定的时间、约定的通道上,通过SN/NESN保证数据不丢不重 跳频算法确保在拥挤的2.4GHz频段中保持通信质量

这张图把连接建立、连接事件、跳频和可靠传输串在了一起。你可以看到,这四个部分不是孤立的,而是环环相扣的。连接建立时协商的参数,决定了后续连接事件的时序;连接事件的时序,决定了跳频的节奏;而跳频和SN/NESN共同保证了数据的可靠传输。