1. 问题现象与背景分析
最近在基于nRF5 SDK开发蓝牙低功耗(BLE)设备时,遇到了一个典型问题:当我在设备固件中添加了自定义的Link Loss Service(LBS)后,使用nRF Connect手机应用扫描设备时,服务列表里并没有立即显示这个新添加的服务。必须手动点击"Refresh services"按钮后,才能看到完整的服务列表。这种现象在BLE开发中并不罕见,但背后涉及BLE协议栈的服务发现机制和nRF SDK的实现细节。
LBS是蓝牙SIG定义的一个标准服务,用于监测设备间的连接丢失情况。按照常规理解,当设备广播中包含完整的服务UUID,或者建立连接后通过服务发现过程,客户端应该能自动获取到完整的服务列表。但实际测试发现,nRF Connect应用在首次连接时,服务发现过程似乎没有完整执行。
2. BLE服务发现机制解析
2.1 标准的服务发现流程
在BLE协议中,服务发现通常遵循以下步骤:
- 主设备(手机)连接从设备(你的nRF设备)
- 主设备发送Primary Service Discovery请求
- 从设备返回包含服务UUID和句柄范围的响应
- 对于每个服务,主设备可以进一步发现其包含的特征和描述符
这个过程中,GATT客户端(nRF Connect)会缓存发现的服务信息。理论上,只要服务正确定义并包含在设备的GATT表中,就应该能被自动发现。
2.2 nRF SDK的服务注册机制
在nRF5 SDK中,添加自定义服务通常需要以下步骤:
c复制// 1. 定义服务结构体
BLE_LBS_DEF(m_lbs);
// 2. 初始化服务参数
ble_lbs_init_t lbs_init = {0};
lbs_init.p_link_ctx = &m_link_loss_ctx;
// 3. 添加服务到GATT表
err_code = ble_lbs_init(&m_lbs, &lbs_init);
APP_ERROR_CHECK(err_code);
关键点在于ble_lbs_init函数内部会调用sd_ble_gatts_service_add这个SoftDevice API,将服务注册到GATT表中。如果这个过程中有任何参数配置不当,就可能导致服务虽然被添加,但无法被正确发现。
3. 问题排查与解决方案
3.1 检查服务UUID声明
首先确认LBS服务的UUID是否正确声明。标准LBS服务的UUID应该是:
c复制#define LBS_UUID_BASE {0x00, 0x00, 0x18, 0x03, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}
#define LBS_UUID_SERVICE 0x1803
在nRF SDK中,通常这样定义:
c复制BLE_UUID_BLE_ASSIGN(ble_uuid, BLE_UUID_LINK_LOSS_SERVICE);
注意:如果使用自定义UUID而非SIG标准UUID,必须确保在广播数据或扫描响应数据中包含完整的128位UUID,否则客户端无法在连接前识别服务。
3.2 验证GATT表结构
通过添加以下调试代码,可以打印GATT表信息:
c复制ble_gatts_db_hash_get(&m_ble_evt, &hash);
NRF_LOG_INFO("GATT Table Hash: 0x%08X", hash);
如果每次修改服务后这个哈希值不变,说明GATT表更新可能没有生效。常见原因是:
- 服务初始化代码没有被执行
- 服务添加在广播启动之后
- 没有正确调用
ble_advertising_restart_without_whitelist
3.3 广播数据配置检查
确保广播数据中包含完整的服务UUID信息。在advertising_init函数中:
c复制static ble_advdata_t advdata = {
.flags = BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE,
.uuids_complete = {
.uuid_cnt = sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]),
.p_uuids = m_adv_uuids
}
};
其中m_adv_uuids应该包含你的LBS服务UUID:
c复制static ble_uuid_t m_adv_uuids[] = {
{BLE_UUID_LINK_LOSS_SERVICE, BLE_UUID_TYPE_BLE}
};
3.4 服务可见性设置
检查服务初始化时是否设置了正确的属性权限:
c复制ble_gatts_attr_md_t attr_md = {0};
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.read_perm);
BLE_GAP_CONN_SEC_MODE_SET_OPEN(&attr_md.write_perm);
如果权限设置过于严格(如NO_ACCESS),可能导致服务虽然存在但不可见。
4. 深入分析nRF Connect的行为
4.1 缓存机制的影响
nRF Connect为了提高效率,会缓存之前发现的服务信息。这可能导致以下现象:
- 修改固件后,手机端仍然显示旧的服务列表
- 必须手动刷新才能获取更新后的服务列表
- 不同版本的nRF Connect表现可能不一致
解决方案包括:
- 修改设备名称或MAC地址强制客户端刷新缓存
- 在开发阶段禁用nRF Connect的缓存功能(如果支持)
- 每次测试前清除应用数据
4.2 服务发现超时问题
在复杂的GATT表结构中,服务发现过程可能因为超时而中断。可以通过以下方式优化:
- 减少初始服务数量,逐步添加
- 检查每个服务的特征和描述符数量是否合理
- 增加连接参数中的间隔时间
5. 完整解决方案与最佳实践
基于以上分析,推荐以下解决方案:
5.1 固件端修改
- 确保服务初始化顺序正确:
c复制void services_init(void)
{
ble_lbs_init(&m_lbs, &lbs_init); // 先初始化服务
ble_advertising_init(&m_advertising, &advdata); // 再初始化广播
}
- 添加GATT表变更检测:
c复制static void on_ble_evt(ble_evt_t const * p_ble_evt)
{
if (p_ble_evt->header.evt_id == BLE_GATTS_EVT_SYS_ATTR_MISSING) {
NRF_LOG_INFO("GATT Table changed, restart advertising");
advertising_restart();
}
}
- 强制广播数据更新:
c复制void advertising_restart(void)
{
uint32_t err_code;
err_code = sd_ble_gap_adv_stop(m_advertising.adv_handle);
APP_ERROR_CHECK(err_code);
err_code = ble_advertising_start(&m_advertising, BLE_ADV_MODE_FAST);
APP_ERROR_CHECK(err_code);
}
5.2 客户端应对策略
- 修改nRF Connect扫描参数:
javascript复制// 在nRF Connect的扫描配置中增加以下参数
{
"scanMode": "lowLatency",
"callbackType": "allMatches",
"reportDelay": 0
}
- 清除客户端缓存:
bash复制adb shell pm clear no.nordicsemi.android.mcp
6. 验证与测试方法
6.1 使用nRF Sniffer抓包分析
通过nRF Sniffer工具捕获空中数据包,可以验证:
- 广播数据中是否包含LBS UUID
- 连接建立后的服务发现过程是否完整
- 服务响应是否包含所有预期字段
6.2 日志分析技巧
在SDK配置中启用以下日志级别:
c复制#define NRF_LOG_DEFAULT_LEVEL 4
#define NRF_SDH_BLE_LOG_LEVEL 4
#define NRF_SDH_LOG_LEVEL 4
关键日志信息包括:
code复制<info> app: Services initialized
<info> nrf_ble_gatt: Connection parameters updated
<info> ble_gatts: New service added with handle 0x0001
7. 进阶问题排查
如果按照上述步骤仍然存在问题,可以考虑:
7.1 检查SoftDevice版本兼容性
不同版本的nRF5 SDK使用的SoftDevice可能有差异:
c复制// 在sdk_config.h中确认
#define S132 7.2.0
#define NRF_SDH_BLE_VS_UUID_COUNT 2
7.2 验证内存分配
服务添加失败可能是内存不足导致的:
c复制// 增加堆大小
#define NRF_SDH_BLE_GATT_MAX_MTU_SIZE 247
#define NRF_SDH_BLE_PERIPHERAL_LINK_COUNT 1
#define NRF_SDH_BLE_TOTAL_LINK_COUNT 1
7.3 使用第三方工具验证
除了nRF Connect,还可以使用以下工具交叉验证:
- LightBlue - 基础GATT操作验证
- BLE Scanner - 原始广播数据分析
- Wireshark + nRF Sniffer - 协议级分析
8. 经验总结与避坑指南
在实际项目中,我总结了以下经验教训:
-
服务初始化顺序很重要:一定要在广播启动前完成所有服务初始化,否则可能出现服务不可见的情况。
-
广播数据更新需要显式触发:修改GATT表后,简单的重启广播可能不够,需要先停止再启动。
-
客户端缓存是个大坑:不同版本的手机和BLE应用对缓存的处理方式不同,开发阶段最好每次都清除缓存。
-
日志是你的好朋友:合理配置日志级别,可以在出现问题时快速定位原因。
-
逐步验证法:先实现最基本的服务框架,验证可见性后再逐步添加特征和描述符。
-
跨平台测试:不要只依赖nRF Connect,多测试几款不同的BLE应用,特别是iOS和Android的不同实现。
通过以上系统性的分析和解决方案,应该能够解决nRF Connect中服务需要手动刷新的问题。如果仍有疑问,建议从最简单的示例项目开始,逐步添加功能,确保每一步都能正确工作。