蓝牙遥控器连接流程分析

背景

最近在一个Linux系统的ARM板子上移植一款蓝牙芯片,因为我们做的是机顶盒,所以首要功能就是能连接上蓝牙遥控器,并且能正常的接收按键。之前在安卓平台,连接上蓝牙遥控器后,会自动创建/dev/input/eventX和/dev/hidrawX节点,通过读取这两个节点,能看到我们机顶盒接收到的按键数据。但是最近在Linux平台,连接上蓝牙遥控器后,并没有创建什么节点,所以我也不知道怎么将遥控器数据上抛给上层应用去读取。网上尝试找一些资料,不过这方面的文章比较少,所以决定自己加些打印,跟一下代码流程,下面的文章记录一下我的跟踪思路。

正文

一、bus、driver、device总线部分

在正式开始分析代码前,我们先了解一个概念:match函数,一个由具体的bus driver实现的回调函数。当任何属于该Bus的device或者device_driver添加到内核时,内核都会调用该接口,如果新加的device或device_driver匹配上了自己的另一半的话,该接口要返回非零值,此时Bus模块的核心逻辑就会执行后续的处理。

0、在调用probe函数之前,会先调用match函数

代码目录:drivers\hid\hid-core.c

函数调用关系:
hid_bus_match
->hid_match_device
    ->hid_match_one_id

具体看一下hid_match_one_id函数,会通过判断vendor和product等值,来确定是否匹配成功,这两个值就是连接的蓝牙设备传过来的。

static bool hid_match_one_id(struct hid_device *hdev,
		const struct hid_device_id *id)
{
	return (id->bus == HID_BUS_ANY || id->bus == hdev->bus) &&
		(id->group == HID_GROUP_ANY || id->group == hdev->group) &&
		(id->vendor == HID_ANY_ID || id->vendor == hdev->vendor) &&
		(id->product == HID_ANY_ID || id->product == hdev->product);
}

1、当调用到probe函数,会有下面的所有流程发生

代码目录:
1)hid_device_probe:drivers\hid\hid-core.c
2)hid_hw_start:include\linux\hid.h

函数调用关系:
hid_device_probe
->hid_hw_start
  ->hid_connect

 

2、上面一步可以看出来最后调用到connect函数,字面意思就是开始正式连接,这个函数比较重要,我们进到具体代码看一下

代码目录:drivers\hid\hid-core.c

int hid_connect(struct hid_device *hdev, unsigned int connect_mask)
{
    ...
    /* 下面会创建/dev/input/eventX节点 */
    if ((connect_mask & HID_CONNECT_HIDINPUT) && !hidinput_connect(hdev,
				connect_mask & HID_CONNECT_HIDINPUT_FORCE))
		hdev->claimed |= HID_CLAIMED_INPUT;
    
    /*下面会创建/dev/hidrawX节点*/
    if ((connect_mask & HID_CONNECT_HIDRAW) && !hidraw_connect(hdev))
		hdev->claimed |= HID_CLAIMED_HIDRAW;
    ...
    hid_info(hdev, "%s: %s HID v%x.%02x %s [%s] on %s\n",
		 buf, bus, hdev->version >> 8, hdev->version & 0xff,
		 type, hdev->name, hdev->phys);

	return 0;
}

在connect函数中有两个比较重要的函数调用hidinput_connect和hidraw_connect,下面我们分别看一下:

2.1、

代码目录:
1)hidinput_connect:drivers\hid\hid-core.c
2)input_register_device:common\drivers\input\input.c:

函数调用关系:
hidinput_connect
->input_register_device   //input_register_device就是我们熟悉的,将设备注册到Input子系统

int input_register_device(struct input_dev *dev)
{
    ...
    path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL);
    pr_info("%s as %s\n",dev->name ? dev->name : "Unspecified device",path ? path : "N/A");
    kfree(path);
    ...
    error = device_add(&dev->dev);
    if (error)
	    goto err_free_vals;
    ...
}

上面的代码我们又遇到一个很关键的函数:device_add(),下面继续跟踪到device_add函数里面去。

2.1.1、

代码目录:
1)device_add:drivers\base\core.c
2)bus_probe_device:drivers\base\bus.c
3)device_attach:drivers\base\dd.c
4)__device_attach:drivers\base\dd.c
5)driver_match_device:drivers\base\base.h

函数调用关系:
device_add
->bus_probe_device
    ->device_attach
        ->__device_attach
            ->driver_match_device //最终会调用driver的match函数

在设备指定总线,且允许自动匹配的前提下(可以通过节点查看:cat /sys/bus/hid/drivers_autoprobe),bus_probe_device调用device_attach(dev),而在device_attach中又分两个分支:
第一、设备指定了驱动,那么device_attach直接调用device_bind_driver(dev)将驱动和设备绑定完事。
第二、设备没有指定驱动,那么device_attach通过bus_for_each_drv(dev->bus, NULL, dev, __device_attach)枚举总线上的驱动与设备进行匹配
具体代码如下所示:

int device_attach(struct device *dev)
{
    int ret = 0;

    device_lock(dev);
    if (dev->driver) { /*指定了驱动*/
        if (klist_node_attached(&dev->p->knode_driver)) {
            ret = 1;
            goto out_unlock;
        }
        ret = device_bind_driver(dev);
        if (ret == 0)
            ret = 1;
        else {
            dev->driver = NULL;
            ret = 0;
        }
    } else {
        /* 通过枚举总线上的驱动和驱动进行匹配 */
        ret = bus_for_each_drv(dev->bus, NULL, dev, __device_attach);
        pm_request_idle(dev);
    }
out_unlock:
    device_unlock(dev);
    return ret;
}

2.2、

代码目录:
hidraw_connect:drivers\hid\hidraw.c

函数调用关系:
hidraw_connect
->device_create

int hidraw_connect(struct hid_device *hid)
{
    ...
    dev->dev = device_create(hidraw_class, &hid->dev, MKDEV(hidraw_major, minor),NULL, "%s%d", "hidraw", minor);
    ...
}

我们都知道,device_create()函数就是/dev下创建节点的,所以调用完hidraw_connect()后,就会有/dev/hidrawX节点了

 

3、

到目前为止,我们已经知道设备是怎么通过总线和驱动对应上了,但是问题又来了,怎么才能调用到.match函数呢?这个问题我们接下来分析

二、uhid驱动部分

通过加日志和搜索大量的代码,终于找到了怎么才能调用到上面的.match函数——hid_bus_match。下面我们看一下内核中的uhid驱动代码。

1、

代码目录:
drivers\hid\uhid.c

函数调用关系:
uhid_char_write
->uhid_dev_create
    ->hid_add_device
        ->device_add
    
static const struct file_operations uhid_fops = {
    .owner		= THIS_MODULE,
    .open		= uhid_char_open,
    .release	= uhid_char_release,
    .read		= uhid_char_read,
    .write		= uhid_char_write,
    .poll		= uhid_char_poll,
    .llseek		= no_llseek,
};

static struct miscdevice uhid_misc = {
    .fops		= &uhid_fops,
    .minor		= UHID_MINOR,
    .name		= UHID_NAME,
};

static int __init uhid_init(void)
{
    return misc_register(&uhid_misc);
}

从函数调用关系来看,又来到我们上面讲到的device_add函数了,就是在bus总线上匹配对应的驱动。翻看uhid这个驱动的源代码,我们可以发现这是一个misc杂散设备,实际上也确实创建了一个/dev/uhid节点。所以回到我们文章最开始的问题,怎么创建到/dev/input/eventX和/dev/hidrawX节点?流程大概应该是这样:
 

int fd = open(/dev/uhid);
write(fd, ...);

最终就会调用到uhid_char_write,并且最终匹配到对应的驱动程序,并且过程中创建了/dev/input/eventX和/dev/hidrawX节点。有了这个思路,我们自然就去查找,到底谁负责打开这个节点呢?自然而然我们就会想到是蓝牙协议栈做这个工作了,搜索代码后,果然是这样。我搜索了mtk的蓝牙协议栈代码,就发现了有打开/dev/uhid的动作。
 

2、

这里再提一下uhid设备匹配到的驱动程序——hid-generic。

代码目录:
drivers\hid\hid-generic.c

static const struct hid_device_id hid_table[] = {
    { HID_DEVICE(HID_BUS_ANY, HID_GROUP_GENERIC, HID_ANY_ID, HID_ANY_ID) },
    { }
};
MODULE_DEVICE_TABLE(hid, hid_table);

static struct hid_driver hid_generic = {
    .name = "hid-generic",
    .id_table = hid_table,
};
module_hid_driver(hid_generic);

注册完uhid设备后,会遍历所有的HID的驱动,因为从hid-generic的id_table来看是匹配任何设备的,所以最后就匹配到了hid-generic。不过大概看了一下这个驱动,好像并没有实际做什么。

结语

到目前为止就简单的跟踪了一下流程,还有很多细节没有细究,有兴趣的同学可以分析的更详细,但是有了上面的流程指引,我相信分析起来也会顺利很多了。另外,上面的代码流程是基于Linux 3.14.29版本,对于我现在准备做的Linux 4.9.113会有稍许不同,但是基本流程类似。
 

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
时下智能语音交互市场火热,越来越多的设备都开始支持远场AI语音交互。 例如:智能音箱,智能电视等等。但这类产品的识别率和误唤醒率还需再不断的优化提升,以至于日常生活中人们还是离不开各式各样的遥控器。而蓝牙语音遥控器这一产品,作为远场语音交互的一个近场配件,也搭上了这趟语音交互的快速列车,成长速度令人惊讶。基于Actions炬芯的ATB1103芯片的语音遥控器,打造了一个AIoT时代的高性价比精品。 一、遥控器应用总体架构 遥控器总体架构分四层,从上到下依次为应用层、应用框架层、硬件抽象层、底层驱动层 1.1、应用层 • 应用状态机– 事件触发让遥控器应用处理不同的状态 • 应用定时器– 定时触发不同的事件,驱动遥控器正常运行 • 应用输入处理– 处理底层来的不同按键消息 • 应用音频输入处理– 将底层的音频处理消息,进行编码,然后通过蓝牙发送给对端设备 • BLE profile – HID profile,提供按键输入输出接口服务 – BAS service,提供电池服务 – DIS service,提供读取设备基本信息的接口服务 – ota profile,提供OTA 升级服务 1.2、应用框架层 • 输入管理– 按键映射处理、按键过滤机制 • 消息管理– 消息分配和释放、 消息发送和接收 • 内存管理– 动态内存管理 • 闪灯管理– 灯资源分配和释放 • 电池管理– 电量读取、电量管理策略 1.3、硬件抽象层 将应用层和驱动层剥离开的中间件层 1.4、底层驱动层 底层硬件操作接口 二、遥控器模块流程概述 2.1、系统启动 系统相关初始化、板级相关外设初始化、蓝牙协议栈相关初始化、HidApp 应用初始化,并进入Main 主循环,等待消息处理 2.2、遥控器状态机 遥控器在运行过程中,主要靠如下3 种状态维持他的正常运行。 2.2.1. 触发遥控器进去空闲状态的事件: > 广播状态,没有连接成功,出现超时事件,进入idle > 连接状态,断开连接,如无操作主动断开连接,然后进入idle 2.2.2. 触发遥控器进入激活状态的事件: > 空闲状态,有按键、首次上电,进入激活状态 > 连接状态,出现异常断开,需要回连,进入激活状态 2.2.3. 触发遥控器进入工作状态的事件: > 激活状态下,配对成功或者回连成功,进入工作状态。 2.3、按键处理 由于遥控器的键值较多,通常用矩阵键盘方式以节省pin 的使用。当使能Key 模块后,Key 控制器就会处于矩阵扫描状态,当检测到外部按键有值时,就会产生中断,中断就将按键信息上报给应用。 2.4、红外处理 • 在非连接状态下,按下按键,就会发射红外码,进而通过红外操作对端设备,如使用红外进行配对. • IRC 协议上,最短的红外码重发时间为108 ms,而按键的重复上报时间,可能小于108ms,也可能大于108ms,因此按键输入和红外发送模块时间上存在三种可能: 2.4.1. 慢速点按 慢速点按动作特征是在大于Trpc 时间后有多次的按键输入。在Ta 时刻,发出初次按键值,在Tb 时刻,不做任何响应,在Tc 时刻,继续发送检测到的按键值,不会发送重复码. 2.4.2. 快速点按 快速点按的动作特征是用户在Trpc 时间内有两次或者以上的按键按下弹起的动作. 在Ta 时刻,将发送出初次按键,而Tb 时刻并不发送按键值,在Tc 时刻,如果按键仍然是按下状态,将发出按键值,否则将丢掉按键值. 2.4.3. 长按 长按的动作特征是按键按下后,一直不放开。此时CPU 检测到按键的持续按下,则在Ta 时刻发送出初次按键,发送出此时按键对应的红外键码,而在Trpc 时间内没有检测到按键的松开,则在Tb 时间输出重复码,直至检测到按键弹起为止. 2.5、语音采集 • 当启动Voice Key 后,ADC 开始采集 • 采集的数据通过DMA 搬运到应用的循环buffer 中,同时发送消息给Main 线程,让其处理语音数据。 • 如果Main 线程处理速度不够快,audioin 驱动就会因为分不到buffer,而将采集的语音数据丢弃。 2.6、BLE数据传送 • 将audioin 驱动发送上来的数据进行编码压缩。 • 然后将编码后的压缩数据切成几个20byte 的数据包 • 最后通过hid profile notify 接口发送给BLE 协议栈 2.7、应用软件Timer管理模块 2.8、LED管理模块 遥控器定义了几种LED 指示灯,用于指示遥控器的一些状态,如下表所示 通常遥控器只有一个物理的LED 灯用于各种场景的指示,这就需要软件上让其分时复用,如果同时需要显示两种状态,状态需要定义优先级,优先级高的状态先指示。如在配对模式下,处于闪灯状态下,这时候按下按键,那么灯还是处于快闪状态。 2.9、O
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值