1. 认识蓝牙GAP的四大核心角色刚接触蓝牙开发时很多人会被GAPGeneric Access Profile的各种角色搞晕。其实用生活中的场景来理解就简单多了Broadcaster就像街头发传单的人Observer是接传单的路人Peripheral类似便利店收银台Central则是排队结账的顾客。NimBLE协议栈把这四种角色的实现封装得非常清晰我们先来看看它们的典型应用场景。Broadcaster最常见的使用场景是ibeacon。我去年做过一个博物馆导览项目每个展品旁边放一个广播设备持续发送展品ID。游客手机上的AppObserver角色收到信号后就能自动弹出讲解页面。这种单向通信模式对功耗要求极低一节纽扣电池可以工作2年以上。Peripheral和Central的配对使用就更普遍了。比如智能手环Peripheral和手机Central的连接手环会广播自己的存在手机扫描到后发起连接之后就可以传输心率、步数等数据。在NimBLE中这两种角色的API设计有明显区别后面我们会用实际代码展示。2. Broadcaster实战从配置到广播2.1 广播参数详解先来看如何配置一个Broadcaster。关键是要理解广播数据包的结构它由AD Type和AD Data组成。比如要创建一个ibeacon需要这样设置广播数据static uint8_t beacon_data[] { 0x02, // Length 0x01, // Flags AD Type 0x1A, // Flags值 0x1A, // 长度 0xFF, // Manufacturer Specific Data 0x4C, 0x00, // 苹果公司标识符 0x02, 0x15, // iBeacon子类型 // UUID 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x01, // Major 0x00, 0x02 // Minor };广播间隔是个需要特别注意的参数。实测发现设置100ms间隔时手机平均1秒内就能发现设备但功耗会达到300μA改为500ms后发现时间延长到2-3秒但功耗降到80μA。在医疗设备等对实时性要求不高的场景建议选择更长的间隔。2.2 广播事件处理广播启动后可能会遇到这些典型事件广播超时BLE_GAP_EVENT_ADV_COMPLETE广播被连接BLE_GAP_EVENT_CONNECT广播参数更新BLE_GAP_EVENT_ADV_PARAMS_CHANGE建议在事件回调函数里添加日志输出方便调试。我曾经遇到过广播突然停止的问题后来发现是广播缓冲区溢出导致的添加以下错误处理就稳定了case BLE_GAP_EVENT_ADV_COMPLETE: if (event-adv_complete.reason ! 0) { MODLOG_DFLT(ERROR, 广播异常停止错误码%d, event-adv_complete.reason); } break;3. Observer模式开发技巧3.1 扫描参数优化Observer的核心任务是扫描并解析广播数据。扫描窗口scan_window和扫描间隔scan_interval的比值直接影响功耗和响应速度。经过多次测试我总结出这些经验值应用场景窗口/间隔平均功耗发现延迟实时定位10ms/10ms1.2mA100ms低频检测30ms/300ms0.15mA1-2s后台扫描10ms/1000ms0.05mA随机在NimBLE中启动扫描的代码示例struct ble_gap_disc_params disc_params { .itvl BLE_GAP_SCAN_FAST_INTERVAL, .window BLE_GAP_SCAN_FAST_WINDOW, .filter_policy 0, .limited 0, .passive 0, .filter_duplicates 1 }; int rc ble_gap_disc(0, BLE_HS_FOREVER, disc_params, NULL, NULL);3.2 广播数据解析收到广播包后需要用ble_hs_adv_parse函数解析。这里有个实用技巧先检查AD Type再处理数据避免内存越界。比如获取设备名称的正确方式static void parse_adv_data(struct ble_hs_adv_fields *fields) { if (fields-name ! NULL) { printf(设备名称: %.*s\n, fields-name_len, fields-name); } // 解析厂商特定数据 if (fields-mfg_data_len 4 fields-mfg_data[0] 0x4C fields-mfg_data[1] 0x00) { printf(发现iBeacon设备\n); } }4. Peripheral与Central的深度配合4.1 Peripheral服务注册Peripheral需要先注册GATT服务才能被连接。建议把服务初始化放在单独的函数中比如创建一个心率监测服务static int gatt_svr_init(void) { int rc; static const struct ble_gatt_svc_def svc_defs[] { { .type BLE_GATT_SVC_TYPE_PRIMARY, .uuid BLE_UUID16_DECLARE(0x180D), .characteristics (struct ble_gatt_chr_def[]) { { .uuid BLE_UUID16_DECLARE(0x2A37), .access_cb hr_measure_cb, .flags BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, }, {0} } }, {0} }; rc ble_gatts_count_cfg(svc_defs); if (rc ! 0) return rc; rc ble_gatts_add_svcs(svc_defs); return rc; }4.2 Central连接管理Central端需要处理连接参数协商。在项目中我发现iOS设备对连接间隔有特殊要求需要这样设置连接参数static struct ble_gap_conn_params params { .scan_itvl 16, // 扫描间隔 10ms .scan_window 16, // 扫描窗口 10ms .itvl_min 24, // 最小连接间隔 15ms .itvl_max 40, // 最大连接间隔 25ms .latency 0, // 从机延迟 .supervision_timeout 100, // 超时 1s .min_ce_len 16, // 最小连接事件长度 .max_ce_len 32 // 最大连接事件长度 }; rc ble_gap_connect(0, peer_addr, 30000, params, NULL, NULL);连接成功后建议立即启动服务发现流程。NimBLE提供了ble_gattc_disc_all_svcs函数来发现所有服务但更高效的做法是指定目标服务的UUIDstatic void discover_services(uint16_t conn_handle) { ble_uuid16_t svc_uuid BLE_UUID16_INIT(0x180D); ble_gattc_disc_svc_by_uuid(conn_handle, svc_uuid.u, NULL, NULL); }5. 安全与性能优化实践5.1 配对加密实现当传输敏感数据时需要启用BLE安全功能。NimBLE支持多种配对方式推荐使用LE Secure Connectionsstatic int pair_init(void) { ble_hs_cfg.sm_io_cap BLE_SM_IO_CAP_DISP_ONLY; ble_hs_cfg.sm_bonding 1; ble_hs_cfg.sm_mitm 1; ble_hs_cfg.sm_sc 1; static const struct ble_store_config store_config { .our_sec ble_store_config_our_sec, .peer_sec ble_store_config_peer_sec }; ble_store_config_conf(store_config); return 0; }5.2 低功耗优化技巧在电池供电设备上这几个参数调整可以显著降低功耗适当增大连接间隔connection interval合理设置从机延迟slave latency优化广播数据长度使用BLE_GAP_DISC_MODE_NONCONNECTABLE非连接模式实测数据表明将连接间隔从15ms调整到100ms功耗可以从800μA降到120μA而数据传输实时性仍能满足大多数场景需求。