当前位置: 首页 > Linux

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

时间:2023-04-07 01:54:09 Linux

本文转载,版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。作者:宋宝华来源:微信公众号linux代码阅读领域(id:linuxdev)1951年5月15日,国会听证会上,美军五星上将麦克阿瑟建议将朝鲜战争扩大到中国,布拉德利然后说:“如果我们将战争扩大到共产主义中国,那么我们将在错误的时间错误的地点与错误的对手一起被卷入错误的战争。”编写代码,同样的原则适用,那就是将正确的代码放在正确的位置,而不是相反。相同的代码可以出现在多个可能的位置。它应该出现在哪里是软件架构设计的结果。说白了,一切都是为了高内聚低耦合。无奈之下我们设想一个名为ABC的简单网卡,需要连接到一个CPU的内存总线上(假设CPU是X),需要地址、数据和控制总线(以及中断引脚等)。那么在ABC的网卡驱动中,我们需要定义ABC的基址、中断号等信息。假设在CPUX的电路板上,ABC的地址是0x100000,中断号是10。假设我们这样定义宏:#defineABC_BASE0x100000#defineABC_IRQ10写代码完成报文的发送和初始化应用程序中断:#defineABC_BASE0x100000#defineABC_IRQ10intabc_send(...){write(ABC_BASE+REG_X,1);writel(ABC_BASE+REG_Y,0x3);...}intabc_init(...){request_irq(ABC_IRQ,...);}这段代码的问题是,一旦板子再换一次,ABC_BASE和ABC_IRQ就不会再换了,代码需要相应的改。一些程序员说我可以这样做:#ifdefBOARD_A#defineABC_BASE0x100000#defineABC_IRQ10#elifdefined(BOARD_B)#defineABC_BASE0x110000#defineABC_IRQ20#elifdefined(BOARD_C)#defineABC_BASE0x120000#IRQ..#endif这样做是可能的,但是如果你有10,000个不同的板,你必须ifdef10,000次。这样写代码,我发现了一种明显的砌墙的感觉(写代码的感觉,就好像在砌墙一样。当你把砖头放进去的时候,简单的重复一下机器。这时候就很危险了,还有可能是代码中的难闻“气味”)。考虑到Linux在世界范围内适配各种产品,以及各种硬件适配的特点,究竟有多少板子使用ABC,谁也不知道。那么,是不是跑10000次#ifdef就一定能解决问题呢?真的不能。假设有一块电路板有2个ABC网卡,那就彻底傻眼了。是这么定义的吗?#ifdefBOARD_A#defineABC1_BASE0x100000#defineABC1_IRQ10#defineABC2_BASE0x101000#defineABC2_IRQ11#elifdefined(BOARD_B)#defineABC1_BASE0x110000#defineABC1_IRQ20...如何更改它?是这样的:intabc1_send(...){writel(ABC1_BASE+REG_X,1);写(ABC1_BASE+REG_Y,0x3);...}intabc1_init(...){request_irq(ABC1_IRQ,...);}intabc2_send(...){writel(ABC2_BASE+REG_X,1);写(ABC2_BASE+REG_Y,0x3);...}intabc2_init(...){request_irq(ABC2_IRQ,...);}...还是这样?intabc_send(intid,...){if(id==0){writel(ABC1_BASE+REG_X,1);writel(ABC1_BASE+REG_Y,0x3);}elseif(idSEelArit==1){wBC+REG_X,1);writel(ABC2_BASE+REG_Y,0x3);}...}不管怎么改,这段代码实在是太恐怖了,连你都受不了。之所以陷入这样的困境,是因为我们犯了没有“把正确的代码放在正确的地方”的错误,引入了很大的耦合。失落反思我们犯的致命错误,就是把板级互连信息耦合到驱动代码中,导致驱动无法跨平台。我们再考虑一下。ABC驱动真正的职责是完成ABC网卡的发送和接收过程。请问一下,这个进程真的和它连接的CPU(德州仪器、三星、博大、全志等)有关吗?跟接在哪个板子上的半毛钱有关系吗?答案是真的没关系!ABC网卡,不会因为你是TI的ARM,你是龙芯,或者你是Blackfin。不管外面有什么样的板子,ABC本身是不动的。既然无所谓,为什么要把这些板级互联信息放在驱动代码中呢?基本上我们可以认为ABC不会为任何人改变,所以它的代码应该是天然跨平台的。因此,我们认为驱动中出现的“#defineABC_BASE0x100000,#defineABC_IRQ10”等代码属于“在错误的地方与错误的敌人打了一场错误的战争”。它没有放在正确的位置,我们编写的代码必须“将天堂归于天堂,将尘土归于尘土”。我们真正的期望大概是这样的:软件工程强调高内聚低耦合。一个模块中的元素连接得越近,它的内聚性就越高;模块之间的连接越不紧密,耦合度就越低。所以,高内聚低耦合强调的是内在的要紧紧相拥,外在的要滚蛋。对于驱动程序来说,板级互连信息显然是应该消失的东西。每个软件模块最好是个宅男,不谈恋爱,不看电影,不吃大餐,不玩够游戏。与外界的唯一联系就是“你饿了吗?”这样的软件显然是高内聚的,而且是低耦合的。有一次在德国外企问工程师“高内聚和低耦合是什么关系”,一位工程师很肯定的回答,“高内聚和低耦合是一对矛盾体”。我觉得他脑子有问题。如果非要用一种关系来形容高内聚和低耦合的关系,我觉得是符合马克思列宁主义的。毛泽东思想强调“高凝聚和低耦合相互依存,缺一不可,相辅相成,共同促进”,其实反映的是同一事物的两个不同方面,总之政治课本背一遍就可以了。你给串口写一段代码,从头到尾全是串口相关的东西,而且包得严严实实,自然不会满世界跑去SPI解耦。SPI需要和串口低耦合,必然需要UART的内部代码把串口的东西都凑到一起。不要乱跑。如果你没有SPI账户,你就不会获得居留许可,所以你还是回老家吧。刘安华明现在板级互连信息已经从驱动程序中分离出来,使它们出现在彼此不同的软件模块中。不过最终他们还是有一定联系的,因为驱动程序还需要取出基址、中断号等板级信息。如何得到它是个大问题。一种方法是ABC驱动向全世界的每块板子询问,“请问你的基地址和中断号是多少?”,“你妈叫什么名字?”这仍然是一个严重的耦合。因为,司机还需要知道板子上有没有ABC,哪块板子有,怎么做。它仍然直接与电路板耦合。能不能有别的方法,我们维护一个普通的类似数据库的东西,板子上有什么网卡,基地址中断号是多少,都维护在一个地方。然后,驱动问一个统一的地方,通过统一的API获取好不好?基于这种思想,Linux将设备驱动程序分为三个实体:总线、设备和驱动程序。总线就是上图中的统一链路,设备就是上图中的板级互连信息。这三个实体的职责如下:我们将所有板卡互连信息填入设备端,然后让设备端向总线注册,告知总线本身的存在。这些设备自然与总线相关联,进而间接与设备的板级连接信息相关联。例如板子arch/blackfin/mach-bf533/boards/ip0x.c有两块DM9000网卡,它是这样注册的:staticstructresourcedm9000_resource1[]={{{.start=0x20100000,+1x00.End201=,.flags=ioresource_mem},{.start=0x20100000+2,.end=0x20100000+3,.flags=ioresource_mem},{.start=irq_pf15,.end=irq_pf15,.end=irq_pf15,.flags=irq_pf15,.flags=ioreseorce_iores_iores_ioresirqIORESOURCE_IRQ_HIGHEDGE}};静态结构资源dm9000_resource2[]={{.start=0x20200000,.end=0x20200000+1,.flags=IORESOURCE_MEM}…};…staticstructplatform_devicedm9000_device1={.name="dm9000",.id=0,.num_resources=ARRAY_SIZE(dm9000_resource1),.resource=dm9000_resource1,};…staticstructplatform_devicedm9000_device2={.name="dm9000英寸,.id=1,.num_resources=array_size(dm9000_resource2),.resource=dm9000_resource2,};staticstructstructplatform_device*ip0x_devices[ip0x_devices[]__initdata=={ip0x_devices,ARRAY_SIZE(ip0x_devices));...}在平台总线的统一链接上,自然知道板子上有两块DM9000网卡,一旦DM9000驱动也注册了,由于平台总线有已经关联了设备,驱动程序自然可以将已有的DM9000设备信息获知,获知如下内存基址和中断信息:StaticStructResourceDM9000_Resource1[]={.start=0x20100000,.nd=0x20100000+1,.flags=ioresource_mem},{.start=。0x20100000+2,.end=0x20100000+3,.flags=ioresource_mem},{.start=irq_pf15,.end.end.end=end=irq_pf15IORESOURCE_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()代码:staticintdm9000_probe(structplatform_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()函数来完成哪个设备由哪个驱动程序服务的职责。比如挂在内存上的平台总线,它的匹配是类似的(最简单的匹配方式是设备和驱动的name字段相同):staticintplatform_match(structdevice*dev,structdevice_driver*drv){structplatform_device*pdev=to_platform_device(dev);structplatform_driver*pdrv=to_platform_driver(drv);moverideondriversetto/*Wrdriver*/if(pdev->driver_override)return!strcmp(pdev->driver_override,drv->name);/*首先尝试OF样式匹配*/if(of_driver_match_device(dev,drv))return1;/*然后尝试ACPI样式匹配*/if(acpi_driver_match_device(dev,drv))return1;/*然后尝试匹配id表*/if(pdrv->id_table)returnplatform_match_id(pdrv->id_table,pdev)!=NULL;/*回退到驱动程序名称匹配*/return(strcmp(pdev->name,drv->name)==0);}VxBus是WindRiver的一种新的设备驱动架构,在VxWorks6.2及之后的版本中被添加到VxWorks中,直到VxWorks6.9,它才基本被VxBusized。不过,这个VxBus可以说和Linux的总线、设备、驱动模型都大同小异。但是,请问,为什么叫VxBus,是不是很Vx?所以,这时候我们看到的代码会是这样的,不管使用哪个板子的ABC设备,统一使用一个常量drivers/net/ethernet/abc.c驱动,而arch/arm/mach-yyy/board里面有很多很多代码的副本,比如-a.c.在下一层,我们仍然看到arch/arm/mach-yyy/board-a.c等大量代码,冲刺描述板级信息的详细代码,虽然已经和驱动本身解耦了。这些代码的存在简直是对Linux内核的污染,是对LinusTorvalds的无情蔑视,因为太技术化了!我们有理由用非C脚本语言来描述这些设备上的信息。这个脚本文件就是传说中的DeviceTree(设备树)。设备树是一个dts文件,用最简单的语法描述了每块板上的所有设备以及这些设备的连接信息。比如arch/arm/boot/dts/imx1-apf9328.dts下的DM9000就是这样一个脚本。基址和中断号已经成为DM9000设备节点的一个属性:eth:eth@4,c00000{compatible="davicom,dm9000";reg=<40x00c000000x240x00c000020x2>;中断父=<&gpio2>;中断=<14IRQ_type_level_low>;...};这样的文件会永远进入历史的旧纸堆,代码也会变成这样的结构。如果换板子,只需要改一下DeviceTree就可以了。“让天归天,让尘归尘”,让驱动回归驱动C代码,让设备回归设备树脚本。看到新版VxWorks7也使用了DeviceTree,我们感到非常高兴和难过。我们很高兴它终于来了;我们很伤心,它终于又来晚了。Linux的车轮滚滚向前,无情碾压着一切。人类几千年的轨迹,沧海桑田,岁月星辰的更替,重复着历史回归历史,未来回归历史的过程。这是现实的悲情,也是历史的英雄气概。《孙子兵法》说:“水因地而治流,兵因敌而胜。故兵无常势,水无常形;能因敌变而胜者,谓之神。””代码,放在正确的位置。更多精彩更新来袭……欢迎关注微信公众号:linux代码阅读田(id:linuxdev)