admin管理员组

文章数量:1580427

一、问题现象

由于设备支持双模蓝牙,设备的BLE需求中,既需要支持作为从机被手机等设备连接,也支持作为主机连接蓝牙手柄等外设,即在播放音频时,允许同时进行低功耗蓝牙相关的功能。实际开发过程中发现在播放音频时,如果发起BLE扫描、连接了蓝牙手柄或其他设备之后,蓝牙音频会变的十分卡顿。

二、问题分析

设备使用的蓝牙模组是单天线模组,性能较弱。实测播放蓝牙音频时,手机BLE连接设备时,并不会造成音频卡顿,因此怀疑是手机BLE连接设备时,连接间隔(连接间隔就是通信间隔)较大,因此不会频繁占用天线,天线资源能合理地分配给蓝牙音频。反过来说,会造成卡顿的情况,必定是占住了天线资源,因此优化的方向就是在满足使用需求的前提下,尽可能地少占用天线。需要按照不同的BLE功能场景,逐点优化。

三、扫描优化

在内核net/bluetooth/hci_request.c中如下代码:

static int active_scan(struct hci_request *req, unsigned long opt)
{
	......
	memset(&param_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), &param_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(&param_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), &param_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);	
}

本文标签: 蓝牙双模音频BlueZ