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)就是配对 + 密钥存储。两个设备第一次连接时交换密钥,之后重连时可以直接用存储的密钥恢复加密连接,不用再走一遍配对流程。
绑定涉及三个关键步骤:
- 配对(Pairing):交换临时密钥(TK),生成短期密钥(STK)或长期密钥(LTK)。
- 密钥分发:交换 LTK、IRK(身份解析密钥)、CSRK(连接签名密钥)等。
- 持久化存储:把密钥写进 Flash 或 NVS,下次开机还能用。
注意:绑定不是必须的。如果你的设备是公共设备(比如 beacon),不需要绑定。但如果是个人设备(比如手环),绑定能大幅提升用户体验——用户不用每次开 App 都重新配对。
我遇到过一个问题:某款手环绑定后,换手机连接时死活连不上。查了半天,发现是旧手机的绑定信息没清除,手环端还在用旧密钥。解决方案是让手环支持「恢复出厂设置」时清除绑定信息。
3.6 GAP 状态机与流程图
GAP 层的行为可以用一个状态机来概括。下面这张图展示了 Peripheral 从待机到连接的核心流程:
这张图我画得比较简洁,实际项目中状态机要复杂得多——比如广播超时、扫描请求处理、连接参数更新请求等等。但核心骨架就是这四个状态。
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——设备连上之后,数据到底怎么传。