設(shè)備驅(qū)動(dòng)模型的需求
總線、設(shè)備和驅(qū)動(dòng)模型,如果把它們之間的關(guān)系比喻成生活中的例子是比較容易理解的。舉個(gè)例子,充電墻壁插座安靜的嵌入在墻面上,無(wú)論設(shè)備是電腦還是手機(jī),插座都能依然不動(dòng)的完成它的使命——充電,沒(méi)有說(shuō)為了滿足各種設(shè)備充電而去更換插座的。其實(shí)這就是軟件工程強(qiáng)調(diào)的高內(nèi)聚、低耦合概念。
所謂高內(nèi)聚低耦合是模塊內(nèi)各元素聯(lián)系越緊密就代表內(nèi)聚性就越高,模塊間聯(lián)系越不緊密就代表耦合性低。所以高內(nèi)聚、低耦合強(qiáng)調(diào)的就是內(nèi)部要緊緊抱團(tuán)。設(shè)備和驅(qū)動(dòng)就是基于這種模型去實(shí)現(xiàn)彼此隔離不相干的。這里,有的讀者就要問(wèn)了,高內(nèi)聚、低耦合的軟件模型理解,可設(shè)備和驅(qū)動(dòng)為什么要采用這種模型呢?沒(méi)錯(cuò),好問(wèn)題。下面進(jìn)入今天的話題——總線、設(shè)備和驅(qū)動(dòng)模型的探究。
設(shè)想一個(gè)叫 GITCHAT 的網(wǎng)卡,它需要接在 CPU 的內(nèi)部總線上,需要地址總線、數(shù)據(jù)總線和控制總線,以及中斷 pin 腳等。
那么在 GITCHAT 的驅(qū)動(dòng)里需要定義 GITCHAT 的基地址、中斷號(hào)等信息。假設(shè) GITCHAT 的地址為0x0001,中斷號(hào)是 2,那么:
#define GITCHAT_BASE 0x0001
#define GITCHAT_INTERRUPT 2
int gitchat_send()
{
writel(GITCHAT_BASE + REG, 1);
...
}
int gitchat_init()
{
request_init(GITCHAT_INTERRUPT, ...);
...
}
但是世界上的板子千千萬(wàn),有三星、華為、飛思卡爾……每個(gè)板子的信息也都不一樣,站在驅(qū)動(dòng)的角度看,當(dāng)每次重新?lián)Q板子的時(shí)候,GITCHAT_BASE 和 GITCHAT_INTERRUPT 就不再一樣,那驅(qū)動(dòng)代碼也要隨之改變。這樣的話一萬(wàn)個(gè)開(kāi)發(fā)板要寫(xiě)一萬(wàn)個(gè)驅(qū)動(dòng)了,這就是文章剛開(kāi)始提到的高內(nèi)聚、低耦合的應(yīng)用場(chǎng)景。
驅(qū)動(dòng)想以不變應(yīng)萬(wàn)變的姿態(tài)適配各種設(shè)備連接的話就要實(shí)現(xiàn)設(shè)備驅(qū)動(dòng)模型。基本上我們可以認(rèn)為驅(qū)動(dòng)不會(huì)因?yàn)?CPU 的改變而改變舞臺(tái)設(shè)備模型,所以它應(yīng)該是跨平臺(tái)的。自然像 “#define GITCHAT_BASE 0x0001,#define GITCHAT_INTERRUPT 2” 這樣描述和 CPU 相關(guān)信息的代碼不應(yīng)該出現(xiàn)在驅(qū)動(dòng)里。
設(shè)備驅(qū)動(dòng)模型的實(shí)現(xiàn)
現(xiàn)在 CPU 板級(jí)信息和驅(qū)動(dòng)分開(kāi)的需求已經(jīng)刻不容緩。但是基地址、中斷號(hào)等板級(jí)信息始終和驅(qū)動(dòng)是有一定聯(lián)系的,因?yàn)轵?qū)動(dòng)畢竟要取出基地址、中斷號(hào)等。怎么???有一種方法是 GITCHAT 驅(qū)動(dòng)滿世界去詢問(wèn)各個(gè)板子:請(qǐng)問(wèn)你的基地址是多少?中斷號(hào)是幾?細(xì)心的讀者會(huì)發(fā)現(xiàn)這仍然是一個(gè)耦合的情況。
對(duì)軟件工程熟悉的讀者肯定立刻想到能不能設(shè)計(jì)一個(gè)類(lèi)似接口適配器的類(lèi)(adapter)去適配不同的板級(jí)信息,這樣板子上的基地址、中斷號(hào)等信息都在一個(gè) adapter 里去維護(hù),然后驅(qū)動(dòng)通過(guò)這個(gè) adapter 不同的 API 去獲取對(duì)應(yīng)的硬件信息。沒(méi)錯(cuò),Linux 內(nèi)核里就是運(yùn)用了這種設(shè)計(jì)思想去對(duì)設(shè)備和驅(qū)動(dòng)進(jìn)行適配隔離的,只不過(guò)在內(nèi)核里我們不叫做適配層,而取名為總線,意為通過(guò)這個(gè)總線去把驅(qū)動(dòng)和對(duì)應(yīng)的設(shè)備綁定一起,如圖:
基于這種設(shè)計(jì)思想,Linux 把設(shè)備驅(qū)動(dòng)分為了總線、設(shè)備和驅(qū)動(dòng)三個(gè)實(shí)體,這三個(gè)實(shí)體在內(nèi)核里的職責(zé)分別如下:
模型設(shè)計(jì)好后,下面來(lái)看一下具體驅(qū)動(dòng)的實(shí)踐,首先把板子的硬件信息填入設(shè)備端,然后讓設(shè)備向總線注冊(cè),這樣總線就間接的知道了設(shè)備的硬件信息。比如一個(gè)板子上有一個(gè) GITCHAT,首先向總線注冊(cè):
static struct resource gitchat_resource[] = {
{
.start = ...,
.end = ...,
.flags = IORESOURCE_MEM
}...
};
static struct platform_device gitchat_device = {
.name = "gitchat";
.id = 0;
.num_resources = ARRAY_SIZE(gitchat_resource);
.resource = gitchat_resource,
};
static struct platform_device *ip0x_device __initdata = {
&gitchat_device,
...
};
static ini __init ip0x_init(void)
{
platform_add_devices(ip0x_device, ARRAY_SIZE(ip0x_device));
}
現(xiàn)在 platform 總線上自然知道了板子上關(guān)于 GITCHAT 設(shè)備的硬件信息,一旦 GITCHAT 的驅(qū)動(dòng)也被注冊(cè)的話,總線就會(huì)把驅(qū)動(dòng)和設(shè)備綁定起來(lái),從而驅(qū)動(dòng)就獲得了基地址、中斷號(hào)等板級(jí)信息??偩€存在的目的就是把設(shè)備和對(duì)應(yīng)的驅(qū)動(dòng)綁定起來(lái),讓內(nèi)核成為該是誰(shuí)的就是誰(shuí)的和諧世界,有點(diǎn)像我們生活中紅娘的角色,把有緣人通過(guò)紅線牽在一起。設(shè)備注冊(cè)總線的代碼例子看完了,下面看下驅(qū)動(dòng)注冊(cè)總線的代碼示例:
static int gitchat_probe(struct platform_device *pdev)
{
...
db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
db->irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 2);
...
}
從代碼中看到驅(qū)動(dòng)是通過(guò)總線 API 接口 platform_get_resource 取得板級(jí)信息,這樣驅(qū)動(dòng)和設(shè)備之間就實(shí)現(xiàn)了高內(nèi)聚、低耦合的設(shè)計(jì),無(wú)論你設(shè)備怎么換,我驅(qū)動(dòng)就可以巋然不動(dòng)。
看到這里,可能有些喜歡探究本質(zhì)的讀者又要問(wèn)了,設(shè)備向總線注冊(cè)了板級(jí)信息,驅(qū)動(dòng)也向總線注冊(cè)了驅(qū)動(dòng)模塊,但總線是怎么做到驅(qū)動(dòng)和設(shè)備匹配的呢?接下來(lái)就講下設(shè)備和驅(qū)動(dòng)是怎么通過(guò)總線進(jìn)行“聯(lián)姻”的。
總線里有很多匹配方式,比如:
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
從上面可知 platform 總線下的設(shè)備和驅(qū)動(dòng)是通過(guò)名字進(jìn)行匹配的,先去匹配 platform_driver 中的 id_table 表中的各個(gè)名字與 platform_device->name 名字是否相同,如果相同則匹配。
設(shè)備驅(qū)動(dòng)模型的改善
相信通過(guò)上面的學(xué)習(xí),相信對(duì)于設(shè)備、驅(qū)動(dòng)通過(guò)總線來(lái)匹配的模型已經(jīng)有所了解。如果寫(xiě)代碼的話應(yīng)該是下面結(jié)構(gòu)圖所示:
最底層是不同板子的板級(jí)文件代碼,中間層是內(nèi)核的總線舞臺(tái)設(shè)備模型,最上層是對(duì)應(yīng)的驅(qū)動(dòng),現(xiàn)在描述板級(jí)的代碼已經(jīng)和驅(qū)動(dòng)解耦了,這也是 Linux 設(shè)備驅(qū)動(dòng)模型最早的實(shí)現(xiàn)機(jī)制,但隨著時(shí)代的發(fā)展,就像是人類(lèi)的貪婪促進(jìn)了社會(huì)的進(jìn)步一樣,開(kāi)發(fā)人員對(duì)這種模型有了更高的要求,雖然驅(qū)動(dòng)和設(shè)備解耦了,但是天下設(shè)備千千萬(wàn),每次設(shè)備的需求改動(dòng)都要去修改 board-xxx.c 設(shè)備文件的話,這樣下去,有太多的板級(jí)文件需要維護(hù)。完美的 Linux 怎么會(huì)允許這樣的事情存在,于是乎,設(shè)備樹(shù)(DTS)就登向了歷史舞臺(tái),下一篇內(nèi)容將探討設(shè)備樹(shù)的實(shí)現(xiàn)原理和用法。
【部分內(nèi)容整理于宋寶華老師課程】