让天堂的归天堂,让尘土的归尘土——谈Linux的总线设备驱动模型

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了让天堂的归天堂,让尘土的归尘土——谈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.   

  4. int abc_send(...)  

  5. {  

  6.         writel(ABC_BASE + REG_X, 1);  

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

  8.         ...  

  9. }  

  10.   

  11. int abc_init(...)  

  12. {  

  13.         request_irq(ABC_IRQ,...);  

  14. }  

这个代码的问题在于,一旦重新换板子,ABC_BASE和ABC_IRQ就不再一样,代码也需要随之变更。

有的程序员说我可以这么干:

[cpp] view plain copy

  1. #ifdef BOARD_A  

  2. #define ABC_BASE 0x100000  

  3. #define ABC_IRQ 10  

  4.   

  5. #elif defined(BOARD_B)  

  6. #define ABC_BASE 0x110000  

  7. #define ABC_IRQ 20  

  8.   

  9. #elif defined(BOARD_C)  

  10. #define ABC_BASE 0x120000  

  11. #define ABC_IRQ 10  

  12. ...  

  13. #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.   

  7. #elif defined(BOARD_B)  

  8. #define ABC1_BASE 0x110000  

  9. #define ABC1_IRQ 20  

  10. ...  

  11. #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.   

  8. int abc1_init(...)  

  9. {  

  10.         request_irq(ABC1_IRQ,...);  

  11. }  

  12.   

  13. int abc2_send(...)  

  14. {  

  15.         writel(ABC2_BASE + REG_X, 1);  

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

  17.         ...  

  18. }  

  19.   

  20. int abc2_init(...)  

  21. {  

  22.         request_irq(ABC2_IRQ,...);  

  23. }  

  24. …  

还是这样?

[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.   

  17. static struct resource dm9000_resource2[] = {  

  18.     {  

  19.         .start = 0x20200000,  

  20.         .end   = 0x20200000 + 1,  

  21.         .flags = IORESOURCE_MEM  

  22.     }…  

  23. };  

  24.   

  25. …  

  26. static struct platform_device dm9000_device1 = {  

  27.     .name           = "dm9000",  

  28.     .id             = 0,  

  29.     .num_resources  = ARRAY_SIZE(dm9000_resource1),  

  30.     .resource       = dm9000_resource1,  

  31. };  

  32.   

  33. …  

  34. static struct platform_device dm9000_device2 = {  

  35.     .name           = "dm9000",  

  36.     .id             = 1,  

  37.     .num_resources  = ARRAY_SIZE(dm9000_resource2),  

  38.     .resource       = dm9000_resource2,  

  39. };  

  40.   

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

  42.     &dm9000_device1,  

  43.     &dm9000_device2,  

  44. …  

  45. };  

  46.   

  47. static int __init ip0x_init(void)  

  48. {  

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

  50.     …  

  51. }  

这样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.     …  

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

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

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

  7. …  

  8. }  

这样,板级互连信息,再也不会闯入驱动,而驱动,看起来也没有和设备之间直接耦合,因为它调用的都是总线级别的标准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.   

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

  7.         if (pdev->driver_override)   

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

  9.   

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

  11.         if (of_driver_match_device(dev, drv))  

  12.                 return 1;  

  13.   

  14.         /* Then try ACPI style match */  

  15.         if (acpi_driver_match_device(dev, drv))  

  16.                 return 1;  

  17.   

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

  19.         if (pdrv->id_table)  

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

  21.   

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

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

  24. }  

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.         …  

  10. };  

之后,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的总线设备驱动模型的主要内容,如果未能解决你的问题,请参考以下文章

给PS4装上Linux,然后用Xbox手柄打任天堂游戏?网友:这场面我没见过

天堂与地狱

安卓丨新电影天堂VIP版

51Nod1417 天堂里的游戏

中国黑客找到了越狱任天堂交换机的方法

任天堂底座方案,支持按键切换TV模式!