讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設備、驅動模型
公元1951年5月15日的國會聽證上,美國陸軍五星上將麥克阿瑟建議把朝鮮戰爭擴大至中國,布萊德利隨後發言:“如果我們把戰爭擴大到共產黨中國,那麽我們會被卷入到一場錯誤的時間,錯誤的地點同錯誤的對手打的一場錯誤的戰爭中。”
寫代碼,適用於同樣的原則,那就是把正確的代碼放到正確的位置而不是相反。同樣的一個代碼,可以出現在多個可能的位置,它究竟應該出現在哪裏,是軟件架構設計的結果,說白了一切都是為了高內核和低耦合。
1. 陷入絕境
下面我們設想一個名字叫做ABC的簡單的網卡,它需要接在一個CPU(假設CPU為X)的內存總線上,需要地址、數據和控制總線(以及中斷pin腳等)。
那麽在ABC的網卡驅動裏面,我們需要定義ABC的基地址、中斷號等信息。假設在CPU X的電路板上面,ABC的地址為0x100000,中斷號為10。假設我們是這樣定義的宏:
[cpp] view plain copy
#define ABC_BASE 0x100000
#define ABC_INTERRUPT 10
並且這樣寫代碼完成發送報文和初始化申請中斷:
[cpp] view plain copy
#define ABC_BASE 0x100000
#define ABC_IRQ 10
int abc_send(...)
{
writel(ABC_BASE + REG_X, 1);
writel(ABC_BASE + REG_Y, 0x3);
...
}
int abc_init(...)
{
request_irq(ABC_IRQ,...);
}
這個代碼的問題在於,一旦重新換板子,ABC_BASE和ABC_IRQ就不再一樣,代碼也需要隨之變更。
有的程序員說我可以這麽幹:
[cpp] view plain copy
#ifdef BOARD_A
#define ABC_BASE 0x100000
#define ABC_IRQ 10
#elif defined(BOARD_B)
#define ABC_BASE 0x110000
#define ABC_IRQ 20
#elif defined(BOARD_C)
#define ABC_BASE 0x120000
#define ABC_IRQ 10
...
#endif
這麽幹固然是可以,但是如果你有1萬個不同的板子,你就要ifdef一萬次,這樣寫代碼,找到了一種明顯的砌墻的感覺(你感覺寫代碼,就跟砌墻似的,一塊塊磚頭一樣放進去的時候,簡單重復機械,這個時候,就很危險了,可能代碼裏面就已經出現了不好的“味道”)。考慮到Linux向全世界各個產品適配,各種硬件適配的特點,究竟有多少個板子用ABC,還真的誰也說不清楚。
那麽,是不是真的#ifdef走一萬次,就一定能解決問題呢?還真的是不能。假設有一個電路板有2個ABC網卡,就徹底傻眼了。難道這樣定義?
[cpp] view plain copy
#ifdef BOARD_A
#define ABC1_BASE 0x100000
#define ABC1_IRQ 10
#define ABC2_BASE 0x101000
#define ABC2_IRQ 11
#elif defined(BOARD_B)
#define ABC1_BASE 0x110000
#define ABC1_IRQ 20
...
#endif
如果這樣做,abc_send()和abc_init()又該如何改?難道這樣:
[cpp] view plain copy
int abc1_send(...)
{
writel(ABC1_BASE + REG_X, 1);
writel(ABC1_BASE + REG_Y, 0x3);
...
}
int abc1_init(...)
{
request_irq(ABC1_IRQ,...);
}
int abc2_send(...)
{
writel(ABC2_BASE + REG_X, 1);
writel(ABC2_BASE + REG_Y, 0x3);
...
}
int abc2_init(...)
{
request_irq(ABC2_IRQ,...);
}
…
還是這樣?
[cpp] view plain copy
int abc_send(int id, ...)
{
if (id == 0) {
writel(ABC1_BASE + REG_X, 1);
writel(ABC1_BASE + REG_Y, 0x3);
<span style="white-space:pre"> </span>} else if (id == 1) {
writel(ABC2_BASE + REG_X, 1);
writel(ABC2_BASE + REG_Y, 0x3);
}
...
}
無論你怎麽改,這個代碼實在都已經是慘不忍睹了,連自己都看不下去了。我們為什麽會陷入這樣的困境,是因為我們犯了未能“把正確的代碼,放入正確的位置的錯誤”,這樣引入了極大的耦合。
2. 迷途反思
我們犯的致命的錯誤,在於把板級互連信息,耦合進了驅動的代碼,導致驅動無法跨平臺。
我們轉念想一想,ABC的驅動的真正職責是完成ABC網卡的收發流程,試問,這個流程,真的與它接在什麽CPU(TI、三星、Broad、Allwinner等)有半毛錢關系嗎?又和接在哪個板子上有半毛錢關系嗎?
答案是真的沒有什麽關系!ABC網卡,不會因為你是TI的ARM,你是龍芯,還是你是Blackfin有什麽不同。任你外面什麽板子排山倒海,狗急跳墻,ABC自己都是巋然不動。
既然沒有什麽關系,那麽這些板子級別的互連信息,又為什麽要放在驅動的代碼裏面呢?基本上,我們可以認為,ABC不會因誰而變,所以它的代碼應該是天然跨平臺的。故此,我們認為“#defineABC_BASE 0x100000, #define ABC_IRQ 10”這樣的代碼,出現在驅動裏面,屬於“在錯誤的地點,和錯誤的敵人,打一場錯誤的戰爭”。它沒有被放在正確的位置上,而我們寫代碼,一定“讓天堂的歸天堂, 讓塵土的歸塵土”。我們真實的期待,恐怕是這個樣子:
軟件工程強調高內聚、低耦合。若一個模塊內各元素聯系的越緊密,則它的內聚性就越高;模塊之間聯系越不緊密,其耦合性就越低。所以高內聚、低耦合強調,內部的要緊緊抱團,外面的給我滾蛋。對於驅動而言,板級互連信息,顯然屬於應該滾蛋的。每個軟件模塊最好是一個宅男,不談戀愛,不看電影,不吃大餐,不踢足夠,和外界唯一的聯系就是“餓了嗎”,這樣的軟件,顯然是又高內聚、又低耦合。
有一次我在一個德國外企,問到工程師們“高內聚和低耦合是什麽關系”,有一個工程師非常積極地回答,“高內聚和低耦合是一對矛盾”。我覺得他的腦子好亂,如果一定要用一個關系來描述高內聚和低耦合的關系,我認為他們符合馬列主義,毛澤東思想強調的“高內聚和低耦合,相互依存,缺一不可,相輔相成,共同促進”,它其實反映了同一個事物兩個不同的側面,總之,把政治課本背一遍就對了。你寫個串口的代碼,裏面從頭到尾都是串口相關的東西,聚地緊,它也自然不會滿世界亂跑到SPI裏面去耦合。SPI要和串口低耦合,它也勢必要求UART內部代碼把串口的東東全部聚一起,不要亂竄,沒有SPI的戶口,居住證也不發給你,就給我滾回老家去。
3. 柳岸花明
現在板級互連信息已經和驅動分離開來了,讓它們彼此出現在不同的軟件模塊。但是,最終它們仍然有一定的聯系,因為,驅動最終還是要取出基地址、中斷號等板級信息的。怎麽取,這是個大問題。
一種方法是ABC的驅動滿世界詢問各個板子,“請問你的基地址,中斷號是幾?”,“你媽貴姓?”這仍然是一個嚴重的耦合。因為,驅動還是得知道板子上有沒有ABC,哪個板子有,怎麽個有法。它還是在和板子直接耦合。
可不可以有另外一種方法,我們維護一個共同的類似數據庫的東西,板子上有什麽網卡,基地址中斷號是什麽,都統一在一個地方維護。然後,驅動問一個統一的地方,通過一個統一的API來獲取即好?
基於這樣的想法,linux把設備驅動分為了總線、設備和驅動三個實體,總線是上圖中的統一紐帶,設備是上圖中的板級互連信息,這三個實體完成的職責分別如下:
實體 | 功能 | 代碼 |
設備 | 描述基地址、中斷號、時鐘、DMA、復位等信息 | arch/arm arch/blackfin arch/xxx 等目錄 |
驅動 | 完成外設的功能,如網卡收發包,聲卡錄放,SD卡讀寫… | drivers/net sound drivers/mmc 等目錄 |
總線 | 完成設備和驅動的關聯 | drivers/base/platform.c drivers/pci/pci-driver.c … |
我們把所有的板子互連信息填入設備端,然後讓設備端向總線註冊告知總線自己的存在,總線上面自然關聯了這些設備,並進一步間接關聯了設備的板級連接信息。比如arch/blackfin/mach-bf533/boards/ip0x.c這塊板子有2個DM9000的網卡,它是這樣註冊的:
[cpp] view plain copy
static struct resource dm9000_resource1[] = {
{
.start = 0x20100000,
.end = 0x20100000 + 1,
.flags = IORESOURCE_MEM
},{
.start = 0x20100000 + 2,
.end = 0x20100000 + 3,
.flags = IORESOURCE_MEM
},{
.start = IRQ_PF15,
.end = IRQ_PF15,
.flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE
}
};
static struct resource dm9000_resource2[] = {
{
.start = 0x20200000,
.end = 0x20200000 + 1,
.flags = IORESOURCE_MEM
}…
};
…
static struct platform_device dm9000_device1 = {
.name = "dm9000",
.id = 0,
.num_resources = ARRAY_SIZE(dm9000_resource1),
.resource = dm9000_resource1,
};
…
static struct platform_device dm9000_device2 = {
.name = "dm9000",
.id = 1,
.num_resources = ARRAY_SIZE(dm9000_resource2),
.resource = dm9000_resource2,
};
static struct platform_device *ip0x_devices[] __initdata = {
&dm9000_device1,
&dm9000_device2,
…
};
static int __init ip0x_init(void)
{
platform_add_devices(ip0x_devices, ARRAY_SIZE(ip0x_devices));
…
}
這樣platform的總線這個統一紐帶上,自然就知道板子上面有2個DM9000的網卡。一旦DM9000的驅動也被註冊,由於platform總線已經關聯了設備,驅動自然可以根據已經存在的DM9000設備信息,獲知如下的內存基地址、中斷等信息了:
[cpp] view plain copy
static struct resource dm9000_resource1[] = {
{
.start = 0x20100000,
.end = 0x20100000 + 1,
.flags = IORESOURCE_MEM
},{
.start = 0x20100000 + 2,
.end = 0x20100000 + 3,
.flags = IORESOURCE_MEM
},{
.start = IRQ_PF15,
.end = IRQ_PF15,
.flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE
}
};
總線存在的目的,則是把這些驅動和這些設備,一一配對的匹配在一起。如下圖,某個電路板子上有2個ABC,1個DEF,1個HIJ設備,以及分別1個的ABC、DEF、HIJ驅動,那麽總線,就是讓2個ABC設備和1個ABC驅動匹配,DEF設備和驅動一對一匹配,HIJ設備和驅動一對一匹配。
驅動本身,則可以用最簡單的API取出設備端填入的互連信息,看一下drivers/net/ethernet/davicom/dm9000.c的dm9000_probe()代碼:
[cpp] view plain copy
static int dm9000_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, 0);
…
}
這樣,板級互連信息,再也不會闖入驅動,而驅動,看起來也沒有和設備之間直接耦合,因為它調用的都是總線級別的標準API:platform_get_resource()。總線裏面有個match()函數,來完成哪個設備由哪個驅動來服務的職責,比如對於掛在內存上的platform總線而言,它的匹配類似(最簡單的匹配方法就是設備和驅動的name字段一樣):
[cpp] view plain copy
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);
}
VxBus是風河公司新的設備驅動程序架構,它是在VxWorks 6.2及以後版本被增加到VxWorks中的,直至VxWorks 6.9,基本都已經VxBus化了。但是,這個VxBus,可以說和Linux的總線、設備、驅動模型是極大地雷同的。但是,請問,你為什麽要叫VxBus呢,它非常地Vx嗎?
所以,這個時候我們看到的代碼會是這樣,無論是哪個板子的ABC設備,都統一使用了一個不變的drivers/net/ethernet/abc.c驅動,而arch/arm/mach-yyy/board-a.c這樣的代碼,則有很多很多份。
4. 更上層樓
我們仍然看到大量的arch/arm/mach-yyy/board-a.c這樣的代碼,沖刺著描述板級信息的細節代碼,盡管它本身已經和驅動解耦了。這些代碼的存在,簡直是對Linux內核的汙染和對Linus Torvalds的無情藐視,因為,太木有技術含量了!
我們有理由,把這些設備端的信息,用一個非C的腳本語言來描述,這個腳本文件,就是傳說中的Device Tree(設備樹)。
設備樹,是一種dts文件,它用最簡單的語法描述每個板子上的所有設備,以及這些設備的連接信息。比如arch/arm/boot/dts/ imx1-apf9328.dts下面的DM9000就是這樣的腳本,基地址、中斷號都成為了DM9000設備節點的一個屬性:
[plain] view plain copy
eth: [email protected],c00000 {
compatible = "davicom,dm9000";
reg = <
4 0x00c00000 0x2
4 0x00c00002 0x2
>;
interrupt-parent = <&gpio2>;
interrupts = <14 IRQ_TYPE_LEVEL_LOW>;
…
};
之後,C代碼被剔除,arch/arm/mach-xxx/board-a.c這樣的文件永遠地進入了歷史的故紙堆,代碼就變成這樣的架構,換個板子,只要換個Device Tree就好。“讓天堂的歸天堂, 讓塵土的歸塵土”,讓驅動的歸驅動C代碼,讓設備的歸設備樹腳本。
我們很高興也很悲痛地看到,VxWorks 7的新版,也采用Device Tree了。我們高興的是,它終於來了;我們悲痛的是,它終於又來晚了。Linux的車輪滾滾向前,無情碾壓一切。人類的千年軌跡,滄海桑田,鬥轉星移,重復地進行著歷史的歸於歷史,未來還是歸於歷史的過程。這是現實的悲愴,也是歷史的豪邁。
《孫子兵法》曰:“水因地而制流,兵因敵而制勝。故兵無常勢,水無常形;能因敵變化而取勝者,謂之神。”一切不過是順勢而為,把正確的代碼,安放到正確的位置。
為了更進一步深入地探討這個話題,CSDN學院聯合博主組織了2017年7月5日8PM~9PM的關於《探究Linux的總線、設備、驅動模型》直播活動,有314人參與了在線直播,活動已經結束,想觀看錄播視頻的讀者可以進入:
http://edu.csdn.net/huiyiCourse/detail/426?ref=0
本文出自 “宋寶華的博客” 博客,請務必保留此出處http://21cnbao.blog.51cto.com/109393/1946351
讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設備、驅動模型