让天堂的归天堂,让尘土的归尘土——谈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

时间: 2024-08-04 15:36:09

让天堂的归天堂,让尘土的归尘土——谈Linux的总线、设备、驱动模型的相关文章

触摸屏设备驱动程序

由于触摸屏设备简单.价格低廉,到处应用 在消费电子商品.工业控制系统.甚至航空领域都有应用 触摸屏作为一种最新的电脑输入设备,是目前最简单.方便.自然的的一种人机交互方式,具有坚固耐用.反应速度快.节省空间.易于交流等许多优点. 事实上,触摸屏是一个使多媒体信息系统改头换面的设备,它赋予多媒体系统以崭新的面貌,是极富有吸引力的全新多媒体交互设备 从技术原理来区别触摸屏,可分为5类: 1,矢量压力传感技术触摸屏 2,电阻式触摸屏 3,电容式触摸屏 4,红外线技术触摸屏 5,表面声波技术触摸屏 矢量

如何切入 Linux 内核源代码

Makefile不是Make Love 从前在学校,混了四年,没有学到任何东西,每天就是逃课,上网,玩游戏,睡觉.毕业的时候,人家跟我说Makefile我完全不知,但是一说Make Love我就来劲了,现在想来依然觉得丢人. 毫不夸张地说,Kconfig和Makefile是我们浏览内核代码时最为依仗的两个文件.基本上,Linux内核中每一个目录下边都会有一个 Kconfig文件和一个Makefile文件.对于一个希望能够在Linux内核的汪洋代码里看到一丝曙光的人来说,将它们放在怎么重要的地位都

各种音视频编解码学习详解

各种音视频编解码学习详解 媒体业务是网络的主要业务之间.尤其移动互联网业务的兴起,在运营商和应用开发商中,媒体业务份量极重,其中媒体的编解码服务涉及需求分析.应用开发.释放license收费等等.最近因为项目的关系,需要理清媒体的codec,比较搞的是,在豆丁网上看运营商的规范 标准,同一运营商同样的业务在不同文档中不同的要求,而且有些要求就我看来应当是历史的延续,也就是现在已经很少采用了.所以豆丁上看不出所以然,从 wiki上查.中文的wiki信息量有限,很短,而wiki的英文内容内多,删减版

就是这么快

一个美女一不留神来到了天堂,看到天堂上挂着无数的时钟,有的表针快有的走的慢.于是美女问天使,为啥有的快有的慢啊?天使说这个是一个人生前按不安分的.如果对丈夫或是妻子不衷心,出轨次数越多表针走的越快.美女好奇的问,那我老公的? 天使说,他的拿到办公室了.美女高兴的问是不是他的走的特别准啊 ?天使说,你快拉倒吧! 自从你来这儿以后,我们就拿到办公室当电扇去了.美女怒冲冲的来到办公室发现他丈夫的表表针没有了.再细一看地上躺着一个天使头上还插着一个表针. Welcome To Visit Our Hom

七,置换计划(上)

返回目录:http://www.cnblogs.com/wantnon/p/4649254.html yc: 这是咋回事儿?这是哪儿?你是谁? 蒙面人: 你穿越了,这是古代,我是救你逃命的人. yc:  啊?不会吧!我没坐飞机也没崴脚呀. 蒙面人:  你打游戏了!坐飞机和崴脚是女屌丝的穿越方式,像你这种屌丝男士,就是打飞机或者打游戏. 她说这话时有点笑场. yc:错,我可不是屌丝,我现在比扎特伯格还有钱,是真正的高富帅! 蒙面人:本质上还是屌丝,就知道看女孩手. yc:这怪我吗,你裹这么严实,我

Linux内核学习之道

来自:http://blog.chinaunix.net/uid-26258259-id-3783679.html 内核文档 内核代码中包含有大量的文档,这些文档对于学习理解内核有着不可估量的价值,记住,在任何时候,它们在我们心目中的地位都应该高于那些各式的内核参考书.下面是一些内核新人所应该阅读的文档. README 这个文件首先简单介绍了Linux内核的背景,然后描述了如何配置和编译内核,最后还告诉我们出现问题时应该怎么办. Documentation/Changes这个文件给出了用来编译和

学计算机的值得一看的文章,跟帖也很有水平啊

转自http://blog.csdn.net/Xviewee/article/details/1606247 回复CSDN和KAOYAN诸位网友的几点看法,(为避免吵架,郑重声明,本人不是高手,只是有感而发的一点个人陋见,欢迎指正,事先感谢): 就我自己的理解,谈谈我对读研和软件学院的看法,不妥之处一笑了之即可. 如果你有实际开发工作经验,感觉自己的水平和实力进入了一个高原期,迫切需要从理论上提高,那么计算机学院是唯一选择.因为计算机学院才能让你在理论上更上一层楼.软件学院从教学计划上就没有

Linux内核学习方法

Makefile不是Make Love 从前在学校,混了四年,没有学到任何东西,每天就是逃课,上网,玩游戏,睡觉.毕业的时候,人家跟我说Makefile我完全不知,但是一说Make Love我就来劲了,现在想来依然觉得丢人. 毫不夸张地说,Kconfig和Makefile是我们浏览内核代码时最为依仗的两个文件.基本上,Linux内核中每一个目录下边都会有一个Kconfig文件和一个Makefile文件.对于一个希望能够在Linux内核的汪洋代码里看到一丝曙光的人来说,将它们放在怎么重要的地位都不

视音频编解码基本术语及解释

摘要:          整理了一些基本视音频术语,用于入门和查询使用. H264: H264是视频的标准,是MPEG4-10,基于内容的高效编码方式. H.264/MPEG-4第10部分,或称AVC(AdvancedVideo Coding,高级视频编码),是一种视频压缩标准,一种被广泛使用的高精度视频的录制.压缩和发布格式.第一版标准的最终草案于 整理了一些基本视音频术语,用于入门和查询使用. H264: H264是视频的标准,是MPEG4-10,基于内容的高效编码方式. H.264/MPE