藍牙遙控器連接流程分析

背景

最近在一個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會有稍許不同,但是基本流程類似。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章