3、GAP层详解:广播、扫描、连接、绑定、角色(Central/Peripheral)

GAP 层,全称是 Generic Access Profile。说白了,它就是 BLE 世界里的人设和社交规则。你想想看,两个设备要认识、要打招呼、要交换名片、要建立关系,甚至要记住对方——这些事全归 GAP 管。

我刚开始做 BLE 项目时,总觉得 GAP 就是配个对、设个名字,没什么好深究的。直到有一次,一个智能门锁项目在量产前发现手机连不上——折腾了两天,最后发现是广播间隔设得太短,导致扫描器收包率暴跌。嗯,从那以后,我再也不敢小看 GAP 了。

3.1 角色模型:Central 与 Peripheral

GAP 定义了两种核心角色:Central(中心设备)和 Peripheral(外设设备)。

角色 典型设备 职责
Peripheral 传感器、手环、标签 广播自身存在,等待连接
Central 手机、平板、网关 扫描广播,发起连接

我个人习惯把 Peripheral 想象成「举着牌子的人」,Central 是「在人群中找牌子的人」。Peripheral 不停地喊「我在这里!我是谁!我能干什么!」,Central 则负责听、筛选、然后走过去说「咱俩聊聊」。

关键点:一个 Central 可以同时连接多个 Peripheral,但一个 Peripheral 通常只能连接一个 Central。这不是硬件限制,而是协议栈的默认行为——当然,你可以通过配置让它支持多连接,但功耗和复杂度都会上升。

3.2 广播:Peripheral 的自我介绍

广播是 BLE 最基础的行为。Peripheral 在三个广播信道(37/38/39)上周期性发送数据包,告诉周围的世界「我在这儿」。

广播包分两种:

  • 广播包(Advertising Packet):必须发送,包含设备地址和少量数据。
  • 扫描响应包(Scan Response Packet):可选,当 Central 主动请求时回复,可以带更多信息。

广播数据里能放什么?我列一下常用的 AD Type:

AD Type 含义 示例值
0x01 Flags(BR/EDR 支持、LE 支持等) 0x06
0x09 Complete Local Name "MySensor"
0xFF Manufacturer Specific Data 自定义数据

我的经验:广播包最大 31 字节,扫描响应包也是 31 字节。别想着把所有数据都塞进去。我曾经在一个项目中把设备名称、电量、温度、序列号全塞进广播包,结果名称被截断了,手机端显示乱码。后来我学乖了——广播包只放必要信息,完整数据等连接后再通过 GATT 读取。

3.3 扫描:Central 的侦察行动

扫描就是 Central 在三个广播信道上监听广播包的过程。扫描分两种模式:

  • 被动扫描(Passive Scanning):只听,不发请求。省电,但拿不到扫描响应数据。
  • 主动扫描(Active Scanning):收到广播包后,发送扫描请求,获取扫描响应。信息更全,但多了一次交互。

扫描参数里有两个关键值:

  • 扫描窗口(Scan Window):每次监听持续多久。
  • 扫描间隔(Scan Interval):两次监听开始之间的时间。

窗口越接近间隔,扫描越密集,发现设备越快,但功耗也越高。我一般建议窗口设为间隔的 50%~80%。

避坑指南:我曾经在一个项目中把扫描窗口设得和间隔一样大(100% duty cycle),结果手机发热严重,电池一小时掉 30%。后来改成 50%,发现设备发现速度几乎没变——因为广播包本身就在三个信道上轮发,密集扫描的边际收益其实很低。

3.4 连接:建立通信链路

当 Central 发现感兴趣的 Peripheral 后,就会发送连接请求(CONNECT_REQ)。这个请求里包含了后续通信的参数:

参数 说明 典型值
Connection Interval 两次数据交互的间隔 7.5ms ~ 4s
Slave Latency 从设备可跳过的连接事件数 0 ~ 499
Supervision Timeout 连接超时时间 100ms ~ 32s

连接间隔越小,实时性越好,但功耗越高。我做过一个数据采集项目,要求每 10ms 传一次数据,连接间隔设成了 7.5ms——结果 Peripheral 的电池撑不过半天。后来改成 30ms,配合 Slave Latency 跳过一个事件,功耗降了 60%,数据延迟还在可接受范围内。

3.5 绑定:记住对方

绑定(Bonding)就是配对 + 密钥存储。两个设备第一次连接时交换密钥,之后重连时可以直接用存储的密钥恢复加密连接,不用再走一遍配对流程。

绑定涉及三个关键步骤:

  1. 配对(Pairing):交换临时密钥(TK),生成短期密钥(STK)或长期密钥(LTK)。
  2. 密钥分发:交换 LTK、IRK(身份解析密钥)、CSRK(连接签名密钥)等。
  3. 持久化存储:把密钥写进 Flash 或 NVS,下次开机还能用。

注意:绑定不是必须的。如果你的设备是公共设备(比如 beacon),不需要绑定。但如果是个人设备(比如手环),绑定能大幅提升用户体验——用户不用每次开 App 都重新配对。

我遇到过一个问题:某款手环绑定后,换手机连接时死活连不上。查了半天,发现是旧手机的绑定信息没清除,手环端还在用旧密钥。解决方案是让手环支持「恢复出厂设置」时清除绑定信息。

3.6 GAP 状态机与流程图

GAP 层的行为可以用一个状态机来概括。下面这张图展示了 Peripheral 从待机到连接的核心流程:

GAP Peripheral 状态机 待机(Standby) 广播(Advertising) 已连接(Connected) 已绑定(Bonded) 断开连接 说明: 绑定是可选的。设备可以在「已连接」状态下直接断开,不进入绑定状态。

这张图我画得比较简洁,实际项目中状态机要复杂得多——比如广播超时、扫描请求处理、连接参数更新请求等等。但核心骨架就是这四个状态。

3.7 实际项目中的 GAP 配置示例

下面是一段基于 Nordic nRF5 SDK 的 GAP 初始化代码。我拿它来展示实际项目中怎么配这些参数:

// 初始化 GAP 参数
ble_gap_conn_params_t gap_conn_params = {
    .min_conn_interval = MSEC_TO_UNITS(30, UNIT_0_625_MS),   // 30ms
    .max_conn_interval = MSEC_TO_UNITS(50, UNIT_0_625_MS),   // 50ms
    .slave_latency     = 0,
    .conn_sup_timeout  = MSEC_TO_UNITS(4000, UNIT_10_MS)     // 4s
};

// 设置广播数据
ble_advdata_t advdata = {
    .name_type          = BLE_ADVDATA_FULL_NAME,
    .flags              = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE,
    .include_appearance = true
};

// 启动广播
ble_gap_adv_params_t adv_params = {
    .type        = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED,
    .interval    = MSEC_TO_UNITS(100, UNIT_0_625_MS),  // 100ms
    .timeout     = 0                                    // 永不超时
};

sd_ble_gap_adv_start(&adv_params, BLE_CONN_CFG_TAG_DEFAULT);

个人习惯:广播间隔我一般设 100ms~200ms。太短(比如 20ms)会让扫描端收包率下降,因为广播信道会拥堵;太长(比如 1s)又会让设备发现变慢。100ms 是个不错的折中。

3.8 角色切换与多角色

有些设备可以同时扮演 Central 和 Peripheral。比如一个智能手表,它作为 Peripheral 连接手机,同时又作为 Central 连接心率胸带。这种场景叫「多角色并发」。

多角色并发对协议栈要求很高。我记得有个项目,手表同时连手机和胸带,结果广播和扫描的时序冲突,导致手机端经常断连。解决方案是错开广播和扫描的时序,或者用双协议栈芯片。

避坑指南:不要轻易在产品里用多角色并发,除非你的协议栈经过充分验证。我见过太多项目因为多角色时序问题导致连接不稳定,最后不得不砍掉一个角色。

3.9 本章小结

GAP 层是 BLE 的社交层。它定义了设备怎么亮相(广播)、怎么找人(扫描)、怎么建立关系(连接)、怎么记住对方(绑定)。

做项目时,我建议你先把角色定清楚——谁是 Central,谁是 Peripheral。然后根据功耗和实时性需求,调好广播间隔、扫描窗口、连接间隔这三个核心参数。绑定功能按需开启,别盲目加。

嗯,GAP 层就讲到这里。下一层我们要深入 GATT——设备连上之后,数据到底怎么传。