一、问题现象
由于设备支持双模蓝牙,设备的BLE需求中,既需要支持作为从机被手机等设备连接,也支持作为主机连接蓝牙手柄等外设,即在播放音频时,允许同时进行低功耗蓝牙相关的功能。实际开发过程中发现在播放音频时,如果发起BLE扫描、连接了蓝牙手柄或其他设备之后,蓝牙音频会变的十分卡顿。
二、问题分析
设备使用的蓝牙模组是单天线模组,性能较弱。实测播放蓝牙音频时,手机BLE连接设备时,并不会造成音频卡顿,因此怀疑是手机BLE连接设备时,连接间隔(连接间隔就是通信间隔)较大,因此不会频繁占用天线,天线资源能合理地分配给蓝牙音频。反过来说,会造成卡顿的情况,必定是占住了天线资源,因此优化的方向就是在满足使用需求的前提下,尽可能地少占用天线。需要按照不同的BLE功能场景,逐点优化。
三、扫描优化
在内核net/bluetooth/hci_request.c中如下代码:
static int active_scan(struct hci_request *req, unsigned long opt)
{
......
memset(¶m_cp, 0, sizeof(param_cp));
param_cp.type = LE_SCAN_ACTIVE;
param_cp.interval = cpu_to_le16(interval);
param_cp.window = cpu_to_le16(DISCOV_LE_SCAN_WIN);
param_cp.own_address_type = own_addr_type;
hci_req_add(req, HCI_OP_LE_SET_SCAN_PARAM, sizeof(param_cp), ¶m_cp);
......
}
static void start_discovery(struct hci_dev *hdev, u8 *status)
{
......
hci_req_sync(hdev, active_scan, DISCOV_LE_SCAN_INT, HCI_CMD_TIMEOUT, status);
......
}
// DISCOV_LE_SCAN_WIN和DISCOV_LE_SCAN_INT定义如下:
#define DISCOV_LE_SCAN_WIN 0x12
#define DISCOV_LE_SCAN_INT 0x12
从上述代码分析可知,开启BLE扫描后,内核使用的扫描间隔和扫描窗口都是0x12,而扫描的占空比等于扫描窗口 / 扫描间隔,此时扫描占空比为100%,所以严重占用了天线,修改代码如下。实际上hdev中携带了扫描间隔和扫描窗口这两个参数,直接使用这两个参数即可,内核中默认的扫描间隔为0x60,扫描窗口为0x30,这两个值也可通过上层API进行修改。不知为何,内核直接使用了上面的两个宏,是个不小的BUG。
static int active_scan(struct hci_request *req, unsigned long opt)
{
......
memset(¶m_cp, 0, sizeof(param_cp));
param_cp.type = LE_SCAN_ACTIVE;
param_cp.interval = cpu_to_le16(interval);
param_cp.window = cpu_to_le16(req->hdev->le_scan_window);
param_cp.own_address_type = own_addr_type;
hci_req_add(req, HCI_OP_LE_SET_SCAN_PARAM, sizeof(param_cp), ¶m_cp);
......
}
static void start_discovery(struct hci_dev *hdev, u8 *status)
{
......
hci_req_sync(hdev, active_scan, hdev->le_scan_interval, HCI_CMD_TIMEOUT, status);
......
}
至于为什么会查到内核的这段代码,是因为事先使用hcidump工具抓到了扫描参数配置:
四、BLE主机优化
当与手柄配对完成后,手柄会主动发起连接参数请求更新,请求将连接间隔改到0x0C以提高操作实时性,且设备选择了接收该参数并更新,如下图所示。
手柄操纵的实时性固然重要,但也要分场景,如果手柄是用来玩游戏,这对实时性的要求会比较高,而在我的设备上,手柄只是用来远程控制设备,对实时性的要求相对来说没有那么高。因此我们可以考虑拒绝手柄发起的连接参数更新请求,即使用设备在连接时指定的连接参数。
在内核net/bluetooth/l2cap_core.c中有如下函数:
static inline int l2cap_conn_param_update_req(struct l2cap_conn *conn,
struct l2cap_cmd_hdr *cmd,
u16 cmd_len, u8 *data)
{
......
// 检查连接参数合法性
err = hci_check_conn_params(min, max, latency, to_multiplier);
if (err) //参数不合法应答reject
rsp.result = cpu_to_le16(L2CAP_CONN_PARAM_REJECTED);
else //参数合法应答accept
rsp.result = cpu_to_le16(L2CAP_CONN_PARAM_ACCEPTED);
// 发送应答结果
l2cap_send_cmd(conn, cmd->ident, L2CAP_CONN_PARAM_UPDATE_RSP,
sizeof(rsp), &rsp);
// 如果接受该连接参数更新请求,发起连接参数更新流程
if (!err) {
u8 store_hint;
store_hint = hci_le_conn_update(hcon, min, max, latency,
to_multiplier);
mgmt_new_conn_param(hcon->hdev, &hcon->dst, hcon->dst_type,
store_hint, min, max, latency,
to_multiplier);
}
......
}
上面的函数会检查连接参数的合法性,如下所示,简单看一下就会发现hci_check_conn_params只是简单判断了连接参数是否在蓝牙spec规定的范围内,并不会根据自身设备的允许的最小、最大连接间隔进行判断。
static inline int hci_check_conn_params(u16 min, u16 max, u16 latency,
u16 to_multiplier)
{
u16 max_latency;
if (min > max || min < 6 || max > 3200)
return -EINVAL;
if (to_multiplier < 10 || to_multiplier > 3200)
return -EINVAL;
if (max >= to_multiplier * 8)
return -EINVAL;
max_latency = (to_multiplier * 4 / max) - 1;
if (latency > 499 || latency > max_latency)
return -EINVAL;
return 0;
}
我们修改一下l2cap_conn_param_update_req函数,判断从机发起的连接参数更新请求参数是否在设备允许的范围内,如果不在该范围内,我们应答拒绝!
static inline int l2cap_conn_param_update_req(struct l2cap_conn *conn,
struct l2cap_cmd_hdr *cmd,
u16 cmd_len, u8 *data)
{
......
// 检查从机请求的连接间隔是否在主机允许的范围内
if(min < hcon->le_conn_min_interval || max > hcon->le_conn_max_interval)
{
err = -EINVAL;
}
else
{
// 检查连接参数合法性
err = hci_check_conn_params(min, max, latency, to_multiplier);
}
if (err) //参数不合法应答reject
rsp.result = cpu_to_le16(L2CAP_CONN_PARAM_REJECTED);
else //参数合法应答accept
rsp.result = cpu_to_le16(L2CAP_CONN_PARAM_ACCEPTED);
// 发送应答结果
l2cap_send_cmd(conn, cmd->ident, L2CAP_CONN_PARAM_UPDATE_RSP,
sizeof(rsp), &rsp);
// 如果接受该连接参数更新请求,发起连接参数更新流程
if (!err) {
u8 store_hint;
store_hint = hci_le_conn_update(hcon, min, max, latency,
to_multiplier);
mgmt_new_conn_param(hcon->hdev, &hcon->dst, hcon->dst_type,
store_hint, min, max, latency,
to_multiplier);
}
......
}
修改后,再次抓取hcilog,如下所示,设备拒绝了手柄发起的连接参数更细请求,后续的通信会保持设备在连接请求中指定的连接间隔,即45ms。
五、BLE从机优化
当设备作为从机时,会被对段主机设备连接。初始的连接参数由主机指定。
一些主机的初始连接连接和手柄一样,也是12(15ms),但设备马上会发起连接参数更新请求,将其改大。
我们的目的就是将连接间隔避免音频卡顿,但是这里这样做是有问题的,问题在于这个连接参数请求太早了,在BLE中,主机连接从机后,主机一般需要发起发现从机服务的流程,这个流程的快慢很大程度上取决于连接间隔。如果发现服务的过程较慢,从客观角度来看,主机与从机的连接过程是很慢的。即问题就在于主机还没有发现设备的服务,设备就已经发起了连接参数更新。因此需要延迟发起连接参数更新。
来看一下内核是如何处理这一流程的:
static void l2cap_le_conn_ready(struct l2cap_conn *conn)
{
struct hci_conn *hcon = conn->hcon;
......
/* For LE slave connections, make sure the connection interval
* is in the range of the minium and maximum interval that has
* been configured for this connection. If not, then trigger
* the connection update procedure.
*/
// 作为BLE从机时,如果主机指定的连接间隔不在从机允许的范围内时,发起连接参数更新请求
if (hcon->role == HCI_ROLE_SLAVE &&
(hcon->le_conn_interval < hcon->le_conn_min_interval ||
hcon->le_conn_interval > hcon->le_conn_max_interval)) {
struct l2cap_conn_param_update_req req;
req.min = cpu_to_le16(hcon->le_conn_min_interval);
req.max = cpu_to_le16(hcon->le_conn_max_interval);
req.latency = cpu_to_le16(hcon->le_conn_latency);
req.to_multiplier = cpu_to_le16(hcon->le_supv_timeout);
l2cap_send_cmd(conn, l2cap_get_ident(conn),
L2CAP_CONN_PARAM_UPDATE_REQ, sizeof(req), &req);
}
}
从上面的函数可以看出,内核的处理是在建立L2CAP连接后立即判断主机指定的连接间隔是否在从机允许的范围内,如果不在范围内就会立即发起更新,和我们sniffer抓取的结果相符。
既然要延迟连接参数更新的流程,我们利用内核中的延迟工作队列来完成,一般来说,主机发现从机服务只需要2秒,我们预留一些余量,4秒后再发起连接参数更新请求流程,修改代码如下:
static void l2cap_le_conn_ready(struct l2cap_conn *conn)
{
struct hci_conn *hcon = conn->hcon;
......
/* For LE slave connections, make sure the connection interval
* is in the range of the minium and maximum interval that has
* been configured for this connection. If not, then trigger
* the connection update procedure.
*/
if (hcon->role == HCI_ROLE_SLAVE &&
(hcon->le_conn_interval < hcon->le_conn_min_interval ||
hcon->le_conn_interval > hcon->le_conn_max_interval)) {
// 4秒后再发起连接
schedule_delayed_work(&conn->update_timer, msecs_to_jiffies(4000));
}
}
static void l2cap_update_timeout(struct work_struct *work)
{
struct l2cap_conn *conn = container_of(work, struct l2cap_conn,
update_timer.work);
struct hci_conn *hcon = conn->hcon;
struct l2cap_conn_param_update_req req;
req.min = cpu_to_le16(hcon->le_conn_min_interval);
req.max = cpu_to_le16(hcon->le_conn_max_interval);
req.latency = cpu_to_le16(hcon->le_conn_latency);
req.to_multiplier = cpu_to_le16(hcon->le_supv_timeout);
l2cap_send_cmd(conn, l2cap_get_ident(conn),
L2CAP_CONN_PARAM_UPDATE_REQ, sizeof(req), &req);
}