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层,那才是真正传数据的地方。