4. GAP层移植:GAP角色定义与配置
好,咱们进入GAP层移植的核心环节。GAP层说白了就是定义你的设备「怎么被发现、怎么连别人、怎么被连」。我刚开始接触BLE时,觉得GAP就是个角色选择,后来踩了不少坑才发现——角色选错了,后面全白干。
4.1 GAP角色定义:你的手环到底扮演谁?
BLE协议栈定义了四种GAP角色,但手环项目里常用的就两种:Peripheral和Central。我直接说结论——手环通常是Peripheral,手机是Central。但有些高级手环会同时支持两种角色,比如用手环控制手机拍照时,手环就临时充当Central。
| 角色 | 职责 | 手环场景 |
|---|---|---|
| Broadcaster | 只发广播,不连接 | 低功耗信标模式 |
| Observer | 只扫描,不广播 | 手环扫描附近设备 |
| Peripheral | 广播自己,等待连接 | 手环默认角色 |
| Central | 扫描并主动连接 | 手机端角色 |
关键点:手环作为Peripheral时,广播间隔建议设在100ms~500ms之间。太短了费电,太长了手机搜不到。我一般取200ms,兼顾功耗和响应速度。
4.2 广播与扫描参数配置
广播参数配置是GAP移植的第一步。说白了就是告诉协议栈:「我要怎么喊话、喊多大声、喊多久」。
先看广播参数的结构体,我习惯这样配:
// 广播参数配置示例
typedef struct {
uint16_t interval_min; // 最小广播间隔,单位0.625ms
uint16_t interval_max; // 最大广播间隔
uint8_t adv_type; // 广播类型
uint8_t own_addr_type; // 本地地址类型
uint8_t channel_map; // 广播信道
uint8_t filter_policy; // 过滤策略
} gap_adv_params_t;
// 手环典型配置
gap_adv_params_t adv_params = {
.interval_min = 320, // 200ms (320 * 0.625ms)
.interval_max = 320, // 200ms
.adv_type = ADV_IND, // 可连接非定向广播
.own_addr_type = ADDR_PUBLIC,
.channel_map = 0x07, // 三个信道全开
.filter_policy = 0 // 不过滤
};
这里有个坑——广播间隔的单位是0.625ms。我刚开始移植时直接填了200,结果广播间隔只有125ms,功耗直接翻倍。后来查了spec才反应过来。
我的经验:广播类型选ADV_IND就够了。ADV_DIRECT虽然连接快,但只针对特定设备,不适合手环这种通用场景。我曾经试过用ADV_DIRECT做手环广播,结果手机死活搜不到——因为广播包只发给指定MAC地址。
扫描参数配置相对简单,手环作为Central时才会用到:
// 扫描参数
gap_scan_params_t scan_params = {
.interval = 160, // 100ms
.window = 80, // 50ms
.scan_type = SCAN_ACTIVE, // 主动扫描
.own_addr_type = ADDR_PUBLIC,
.filter_policy = 0
};
扫描窗口和间隔的比例很关键。我一般让window是interval的一半,这样扫描占空比50%,功耗和响应比较均衡。
4.3 连接参数更新
连接参数更新是GAP层移植里最容易出问题的地方。手环连上手机后,默认的连接参数往往不适合实际应用——比如心率监测需要频繁传数据,但默认的连接间隔可能太大。
连接参数包括:
- 连接间隔:两个连接事件之间的时间,单位1.25ms
- 延迟:从设备可以跳过多少个连接事件
- 超时时间:多久没收到包就断开连接
我一般这样发起连接参数更新请求:
// 连接参数更新请求
gap_conn_update_params_t update_params = {
.interval_min = 12, // 15ms (12 * 1.25ms)
.interval_max = 24, // 30ms
.latency = 0, // 不允许延迟
.timeout = 400, // 5秒超时 (400 * 10ms)
.min_ce_length = 2, // 最小连接事件长度
.max_ce_length = 6 // 最大连接事件长度
};
// 调用协议栈接口发起更新
ble_gap_conn_param_update(conn_handle, &update_params);
注意:连接参数更新请求不一定被接受。手机端(Central)有权拒绝。我遇到过某品牌手机死活不接受15ms的连接间隔,最后妥协到30ms才通过。所以建议interval_min和interval_max留点余量。
还有个细节——连接参数更新的时机。不要在连接建立后立刻发起更新,等个1~2秒。我见过有人连接刚建立就发更新请求,结果手机还没准备好,直接拒绝。后来我加了个定时器,连接稳定后再更新。
4.4 GAP事件回调处理
GAP事件回调是协议栈和应用程序之间的桥梁。说白了就是协议栈告诉你:「有人连你了」、「连接断了」、「参数更新了」。
回调函数的注册方式各协议栈不同,但处理逻辑大同小异。我以典型的回调结构为例:
// GAP事件回调函数
void gap_event_handler(gap_event_t *event) {
switch (event->type) {
case GAP_EVT_CONNECTED:
// 连接建立
handle_connected(event->conn_handle);
break;
case GAP_EVT_DISCONNECTED:
// 连接断开,重新开始广播
handle_disconnected(event->conn_handle);
start_advertising();
break;
case GAP_EVT_CONN_PARAM_UPDATE:
// 连接参数更新完成
handle_param_update(event->conn_handle,
&event->params.conn_params);
break;
case GAP_EVT_ADV_COMPLETE:
// 广播结束(比如超时)
handle_adv_complete();
break;
case GAP_EVT_SCAN_REPORT:
// 扫描到设备
handle_scan_report(&event->params.scan_report);
break;
default:
break;
}
}
这里我重点说几个容易忽略的点:
- 断开重连:断开后一定要重新启动广播。我见过有人忘了这步,手环连一次就再也搜不到了。
- 参数更新回调:更新请求发出后,不管成功还是失败,都会触发这个回调。记得检查返回状态。
- 扫描报告处理:扫描到的设备信息要尽快处理,否则缓冲区满了会丢包。
避坑指南:我曾经在GAP_EVT_DISCONNECTED回调里直接调用start_advertising(),结果死机了。后来发现是协议栈要求在事件回调返回后才能调用协议栈API。解决办法是设置一个标志位,在主循环里检查并启动广播。
嗯,GAP层移植的核心就这些。说白了就是三件事:配好广播参数、处理好连接参数、写对回调逻辑。下一章咱们进入GATT层,那才是真正传数据的地方。