• BlueZ双模蓝牙音频卡顿问题优化


    一、问题现象

    由于设备支持双模蓝牙,设备的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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    从上述代码分析可知,开启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);
        ......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    至于为什么会查到内核的这段代码,是因为事先使用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);
    	}
    	......
    }					  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    上面的函数会检查连接参数的合法性,如下所示,简单看一下就会发现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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们修改一下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);
    	}
    	......
    }		
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    修改后,再次抓取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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    从上面的函数可以看出,内核的处理是在建立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);	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
  • 相关阅读:
    .NET WebAPI 基础 FromRoute、FromQuery、FromBody 用法
    协作乐高 All In One:DAO工具大全
    IDEA的安装与使用
    JavaScript遭嫌弃,“反JS”主义者兴起
    设计模式篇(Java):装饰者模式
    算法练习-LeetCode 剑指 Offer 33. 二叉搜索树的后序遍历序列
    Rn使用FlatList导航栏自动回到中间
    论文阅读笔记(一)
    Linux:管道
    C++编译期循环获取变量类型
  • 原文地址:https://blog.csdn.net/qq_27575841/article/details/127581151