问题现象:我们的一款HID键盘硬件一直都工作的很好,连接配对后使用起来和原装键盘效果差不多,但是后面陆续有用户反馈家里的电视等蓝牙设备配对连接我们的键盘后,虽然显示已连接,但实际上控制不了。设备涉及到了好些品牌,比如坚果投影、海信电视等。
SDK版本: nRF5_SDK_17.0.2_d674dde
SoftDevice: S132
问题分析:
我们买了坚果投影回来测试,发现的确如用户反馈的现象一致,而且是必现的,必显就好分析。我们将nRF log日志级别改为debug,抓取了坚果配对过程的log,然后以同样方式抓取了正常使用的极米这一过程的log,对比日志发现连接都是正常的,日志都打印了Connected信息:
00>
00> <info> app: Connected
00>
00> <info> app: Connected
00>
在main.c中我们对BLE事件注册了一个handler,针对BLE_GAP_EVT_CONNECTED和BLE_GAP_EVT_DISCONNECTED等事件进行了处理,上面的日志也说明了协议栈向我们的handler上报了已和设备连接上的事件。
在日志后面按下按键发送数据的地方,坚果返回了错误码
00> <info> app: sd_ble_gatts_hvx err_code = 8.
极米此处无错误
00> <info> app: sd_ble_gatts_hvx err_code = 0.
00>
00> <info> app: hid send key err_code = 0
所以问题点就是此处,先来看一下报错位置的代码
红色框中的代码就是当按下按键, 将HID键值通知到设备的处理,也就是说sd_ble_gatts_hvx返回了错误码0x08(NRF_ERROR_INVALID_STATE)。
根据sd_ble_gatts_hvx的方法定义,可以看到0x08错误的条件有三个:
* @retval ::NRF_ERROR_INVALID_STATE One or more of the following is true:
* - Invalid Connection State
* - Notifications and/or indications not enabled in the CCCD
* - An ATT_MTU exchange is ongoing
这段代码最开始的conn_handle != BLE_CONN_HANDLE_INVALID判断可以排除条件1,设备当前的连接是正常的,而且日志中也没有disconnected事件。
在Nordic论坛搜到类似的问题,说是CCCD没有使能,所以我们一开始也是从这个方向着手排查,在main.c中hids_init(void)方法里我们有对HID服务添加事件回调:
hids_init_obj.evt_handler = on_hids_evt;
在坚果和极米的日志中都有输出相同数量的BLE_HIDS_EVT_NOTIF_ENABLED事件,所以HID 服务的CCCD应该已经使能了。
00> <info> app: ble_srv_is_notification_enabled = 1 p_hids->evt_handler = 295104
00> <info> app: BLE_HIDS_EVT_NOTIF_ENABLED
接下来看an ATT_MTU exchange is ongoing这个情况是否存在。在两份日志的最开始处都有请求更新ATT MTU的信息,并且是在打印Connected之前:
00> <debug> nrf_ble_gatt: Requesting to update ATT MTU to 185 bytes on connection 0x0.
00>
00> <info> app: HID on_connect conn_handle = 0.
00>
00> <info> app: Connected
极米在后面的日志中有ATT MTU交换完成的信息
00> <debug> nrf_ble_gatt: ATT MTU updated to 185 bytes on connection 0x0 (response).
00>
00> <info> app: Data len is set to 0xB6(182)
00>
00> <debug> app: ATT MTU exchange completed. central 0xB9 peripheral 0xB9
而坚果没有,即使等待很长时间也没有,所以sd_ble_gatts_hvx发送数据会因为 An ATT_MTU exchange is ongoing 直接返回NRF_ERROR_INVALID_STATE,这样看很有可能就是我们目前碰到的问题的原因所在。那为什么要在连接后立马发MTU交换请求呢?为什么在坚果这里就没有交换完成的日志?
我们先分析第一个疑问。
蓝牙核心规范中只提到GATT client可以向GATT server发起ATT_EXCHANGE_MTU_REQ请求以告诉对方自己能够接收的最大数据长度(Client Rx MTU),server端收到请求后,需要通过ATT_EXCHANGE_MTU_RSP应答也告诉client自己这边的接收最大值(Server Rx MTU)。在MTU应答后两者的数据交互就使用两者能接收的MTU最小值了,一般MTU请求在连接后只会发一次。
规范并没有明确禁止server也可以发送ATT_EXCHANGE_MTU_REQ做同样的事情,即client 和server都能发出ATT MTU交换请求,所以Nordic协议栈会按照蓝牙规范处理MTU交换请求和应答。
这部分的处理主要在nrf_ble_gatt.c中
前面提到在connected打印前发出了MTU交换请求,上图红框部分on_connected_evt(p_gatt, p_ble_evt)就是发出请求的地方,正好是在BLE_GAP_EVT_CONNECTED事件里调用的。
先判断了当前设备的GAP角色,如果是peripheral, 将当前periph设备所需要的MTU赋值给当前连接conn_handle对应的p_link ->att_mtu_desired,目前我们的硬件是HID键盘,所以这里是peripheral。
接着判断了当前连接需要的mtu是否大于生效的mtu,满足条件就请求MTU交换。可以看到在Nordic中,如果使用gatt库,默认设备连接上后满足这个条件就会发出交换请求。
那么需要看下p_link ->att_mtu_desired和p_link->att_mtu_effective值分别是多少,在哪里初始化的。
p_link->att_mtu_desired = p_gatt->att_mtu_desired_periph, 在当前文件中搜索att_mtu_desired_periph,找到下面这个方法
nrf_ble_gatt_init有点眼熟,我们main.c中gatt_init()方法里调用这个方法进行了gatt初始化。
NRF_BLE_GATT_DEF(m_gatt); /**< GATT module instance. */
static void gatt_init(void)
{
ret_code_t err_code = nrf_ble_gatt_init(&m_gatt, gatt_evt_handler);
APP_ERROR_CHECK(err_code);
}
我们这个工程NRF_BLE_GATT_LINK_COUNT=1, 会继续调用link_init(&p_gatt->links[i]);
p_link ->att_mtu_desired默认值是NRF_SDH_BLE_GATT_MAX_MTU_SIZE
p_link->att_mtu_effective默认值是BLE_GATT_ATT_MTU_DEFAULT
//sdk_config.h
// <o> NRF_SDH_BLE_GATT_MAX_MTU_SIZE - Static maximum MTU size.
#ifndef NRF_SDH_BLE_GATT_MAX_MTU_SIZE
#define NRF_SDH_BLE_GATT_MAX_MTU_SIZE 185
#endif
//ble_gatt.h
/** @brief Default ATT MTU, in bytes. */
#define BLE_GATT_ATT_MTU_DEFAULT 23
综上,我们这个工程需要的mtu是185, 生效的是23(蓝牙协议允许的最小值, 蓝牙设备默认会使用这个值),所以连接上后会发出MTU交换请求,告诉对方本地能够接收的最大数据长度为185。但是不知道坚果为什么没有应答,导致HID设备的MTU交换状态一直处于进行中,蓝牙规范有提及如果发出MTU请求之后,在没有接收到应答之前,不可以向对端设备发送通知或指示。
我们看了下原始的sdk例子工程,NRF_SDH_BLE_GATT_MAX_MTU_SIZE是23, 改成23后, 重新和坚果配对,连接上后可以正常控制了,此时的log里也没有看到之前的请求交换的信息,证明了我们的分析是正确的,就是因为MTU交换没有完成造成的。
至于坚果为什么没有回复交换请求应答,是没有收到还是交换请求时机不对,我们也不清楚,不过作为Peripheral来说,本身也不用主动去请求MTU交换,交给Master来负责就好了,所以,最终决定连接后不发送这个请求,如果过后想要交换MTU, 调用sd_ble_gattc_exchange_mtu_request()即可。
改成23带来了新的问题,HID键盘除了要控制蓝牙设备外,还要和app连接进行通信,默认23字节会使得两者之间数据交互需要更长的时间,之前改成185就是为了缩短这个耗时。所以不能简单的改NRF_SDH_BLE_GATT_MAX_MTU_SIZE成23。因为如果是23,即使app这边请求185大小的client_mtu, HID键盘收到交换请求后,在这个逻辑中会使用p_link->att_mtu_desired(默认初始化为NRF_SDH_BLE_GATT_MAX_MTU_SIZE)来调用sd_ble_gatts_exchange_mtu_reply做应答,sd_ble_gatts_exchange_mtu_reply传入的是server_mtu,双方最终使用的是client_mtu和server_mtu的最小值,和这个方法里的p_link->att_mtu_effective = MIN(client_mtu, p_link->att_mtu_desired)一样。
我们的需求是连接上后不要自动发交换请求,所以在sdk_config.h里定义了一个新常量
// NRF_BLE_GATT_MTU_EXCHANGE_INITIATION_ENABLED 是否启用连接后发MTU交换请求, 1为启用,我们这里关闭
#ifndef NRF_BLE_GATT_MTU_EXCHANGE_INITIATION_ENABLED
#define NRF_BLE_GATT_MTU_EXCHANGE_INITIATION_ENABLED 0
#endif
NRF_SDH_BLE_GATT_MAX_MTU_SIZE还是保持185,这样app可以申请最大为185的MTU用来和设备通信。
修改nrf_ble_gatt.c中的on_connected_evt方法,在之前的p_link->att_mtu_desired > p_link->att_mtu_effective条件前增加了启用开关:
这样即实现了作为HID外设时不主动发MTU交换请求,又能正确处理Master请求交换MTU大小。