1. 程式人生 > >讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設備、驅動模型

讓天堂的歸天堂,讓塵土的歸塵土——談Linux的總線、設備、驅動模型

linux 總線 設備 驅動

公元1951年5月15日的國會聽證上,美國陸軍五星上將麥克阿瑟建議把朝鮮戰爭擴大至中國,布萊德利隨後發言:“如果我們把戰爭擴大到共產黨中國,那麽我們會被卷入到一場錯誤的時間,錯誤的地點同錯誤的對手打的一場錯誤的戰爭中。”

寫代碼,適用於同樣的原則,那就是把正確的代碼放到正確的位置而不是相反。同樣的一個代碼,可以出現在多個可能的位置,它究竟應該出現在哪裏,是軟件架構設計的結果,說白了一切都是為了高內核和低耦合。

1. 陷入絕境

下面我們設想一個名字叫做ABC的簡單的網卡,它需要接在一個CPU(假設CPU為X)的內存總線上,需要地址、數據和控制總線(以及中斷pin腳等)。


那麽在ABC的網卡驅動裏面,我們需要定義ABC的基地址、中斷號等信息。假設在CPU X的電路板上面,ABC的地址為0x100000,中斷號為10。假設我們是這樣定義的宏:

[cpp] view plain copy

  1. #define ABC_BASE 0x100000

  2. #define ABC_INTERRUPT 10

並且這樣寫代碼完成發送報文和初始化申請中斷:

[cpp] view plain copy

  1. #define ABC_BASE 0x100000

  2. #define ABC_IRQ 10

  3. int abc_send(...)

  4. {

  5. writel(ABC_BASE + REG_X, 1);

  6. writel(ABC_BASE + REG_Y, 0x3);

  7. ...

  8. }

  9. int abc_init(...)

  10. {

  11. request_irq(ABC_IRQ,...);

  12. }

這個代碼的問題在於,一旦重新換板子,ABC_BASE和ABC_IRQ就不再一樣,代碼也需要隨之變更。

有的程序員說我可以這麽幹:

[cpp] view plain copy

  1. #ifdef BOARD_A

  2. #define ABC_BASE 0x100000

  3. #define ABC_IRQ 10

  4. #elif defined(BOARD_B)

  5. #define ABC_BASE 0x110000

  6. #define ABC_IRQ 20

  7. #elif defined(BOARD_C)

  8. #define ABC_BASE 0x120000

  9. #define ABC_IRQ 10

  10. ...

  11. #endif

這麽幹固然是可以,但是如果你有1萬個不同的板子,你就要ifdef一萬次,這樣寫代碼,找到了一種明顯的砌墻的感覺(你感覺寫代碼,就跟砌墻似的,一塊塊磚頭一樣放進去的時候,簡單重復機械,這個時候,就很危險了,可能代碼裏面就已經出現了不好的“味道”)。考慮到Linux向全世界各個產品適配,各種硬件適配的特點,究竟有多少個板子用ABC,還真的誰也說不清楚。

那麽,是不是真的#ifdef走一萬次,就一定能解決問題呢?還真的是不能。假設有一個電路板有2個ABC網卡,就徹底傻眼了。難道這樣定義?

[cpp] view plain copy

  1. #ifdef BOARD_A

  2. #define ABC1_BASE 0x100000

  3. #define ABC1_IRQ 10

  4. #define ABC2_BASE 0x101000

  5. #define ABC2_IRQ 11

  6. #elif defined(BOARD_B)

  7. #define ABC1_BASE 0x110000

  8. #define ABC1_IRQ 20

  9. ...

  10. #endif

如果這樣做,abc_send()和abc_init()又該如何改?難道這樣:

[cpp] view plain copy

  1. int abc1_send(...)

  2. {

  3. writel(ABC1_BASE + REG_X, 1);

  4. writel(ABC1_BASE + REG_Y, 0x3);

  5. ...

  6. }

  7. int abc1_init(...)

  8. {

  9. request_irq(ABC1_IRQ,...);

  10. }

  11. int abc2_send(...)

  12. {

  13. writel(ABC2_BASE + REG_X, 1);

  14. writel(ABC2_BASE + REG_Y, 0x3);

  15. ...

  16. }

  17. int abc2_init(...)

  18. {

  19. request_irq(ABC2_IRQ,...);

  20. }

還是這樣?

[cpp] view plain copy

  1. int abc_send(int id, ...)

  2. {

  3. if (id == 0) {

  4. writel(ABC1_BASE + REG_X, 1);

  5. writel(ABC1_BASE + REG_Y, 0x3);

  6. <span style="white-space:pre"> </span>} else if (id == 1) {

  7. writel(ABC2_BASE + REG_X, 1);

  8. writel(ABC2_BASE + REG_Y, 0x3);

  9. }

  10. ...

  11. }

無論你怎麽改,這個代碼實在都已經是慘不忍睹了,連自己都看不下去了。我們為什麽會陷入這樣的困境,是因為我們犯了未能“把正確的代碼,放入正確的位置的錯誤”,這樣引入了極大的耦合。

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

  1. static struct resource dm9000_resource1[] = {

  2. {

  3. .start = 0x20100000,

  4. .end = 0x20100000 + 1,

  5. .flags = IORESOURCE_MEM

  6. },{

  7. .start = 0x20100000 + 2,

  8. .end = 0x20100000 + 3,

  9. .flags = IORESOURCE_MEM

  10. },{

  11. .start = IRQ_PF15,

  12. .end = IRQ_PF15,

  13. .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE

  14. }

  15. };

  16. static struct resource dm9000_resource2[] = {

  17. {

  18. .start = 0x20200000,

  19. .end = 0x20200000 + 1,

  20. .flags = IORESOURCE_MEM

  21. }…

  22. };

  23. static struct platform_device dm9000_device1 = {

  24. .name = "dm9000",

  25. .id = 0,

  26. .num_resources = ARRAY_SIZE(dm9000_resource1),

  27. .resource = dm9000_resource1,

  28. };

  29. static struct platform_device dm9000_device2 = {

  30. .name = "dm9000",

  31. .id = 1,

  32. .num_resources = ARRAY_SIZE(dm9000_resource2),

  33. .resource = dm9000_resource2,

  34. };

  35. static struct platform_device *ip0x_devices[] __initdata = {

  36. &dm9000_device1,

  37. &dm9000_device2,

  38. };

  39. static int __init ip0x_init(void)

  40. {

  41. platform_add_devices(ip0x_devices, ARRAY_SIZE(ip0x_devices));

  42. }

這樣platform的總線這個統一紐帶上,自然就知道板子上面有2個DM9000的網卡。一旦DM9000的驅動也被註冊,由於platform總線已經關聯了設備,驅動自然可以根據已經存在的DM9000設備信息,獲知如下的內存基地址、中斷等信息了:

[cpp] view plain copy

  1. static struct resource dm9000_resource1[] = {

  2. {

  3. .start = 0x20100000,

  4. .end = 0x20100000 + 1,

  5. .flags = IORESOURCE_MEM

  6. },{

  7. .start = 0x20100000 + 2,

  8. .end = 0x20100000 + 3,

  9. .flags = IORESOURCE_MEM

  10. },{

  11. .start = IRQ_PF15,

  12. .end = IRQ_PF15,

  13. .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_HIGHEDGE

  14. }

  15. };

總線存在的目的,則是把這些驅動和這些設備,一一配對的匹配在一起。如下圖,某個電路板子上有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

  1. static int dm9000_probe(struct platform_device *pdev)

  2. {

  3. db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

  4. db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);

  5. db->irq_res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);

  6. }

這樣,板級互連信息,再也不會闖入驅動,而驅動,看起來也沒有和設備之間直接耦合,因為它調用的都是總線級別的標準API:platform_get_resource()。總線裏面有個match()函數,來完成哪個設備由哪個驅動來服務的職責,比如對於掛在內存上的platform總線而言,它的匹配類似(最簡單的匹配方法就是設備和驅動的name字段一樣):

[cpp] view plain copy

  1. static int platform_match(struct device *dev, struct device_driver *drv)

  2. {

  3. struct platform_device *pdev = to_platform_device(dev);

  4. struct platform_driver *pdrv = to_platform_driver(drv);

  5. /* When driver_override is set, only bind to the matching driver */

  6. if (pdev->driver_override)

  7. return !strcmp(pdev->driver_override, drv->name);

  8. /* Attempt an OF style match first */

  9. if (of_driver_match_device(dev, drv))

  10. return 1;

  11. /* Then try ACPI style match */

  12. if (acpi_driver_match_device(dev, drv))

  13. return 1;

  14. /* Then try to match against the id table */

  15. if (pdrv->id_table)

  16. return platform_match_id(pdrv->id_table, pdev) != NULL;

  17. /* fall-back to driver name match */

  18. return (strcmp(pdev->name, drv->name) == 0);

  19. }

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

  1. eth: [email protected],c00000 {

  2. compatible = "davicom,dm9000";

  3. reg = <

  4. 4 0x00c00000 0x2

  5. 4 0x00c00002 0x2

  6. >;

  7. interrupt-parent = <&gpio2>;

  8. interrupts = <14 IRQ_TYPE_LEVEL_LOW>;

  9. };

之後,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的總線、設備、驅動模型