Linux设备驱动的软件架构思想

Linux驱动的软件架构

Linux不是为了某一电路板而设计的操作系统,它可以支持约30种体系结构下一定数量的硬件。Linux设备驱动非常重视软件的可重用和跨平台能力。譬如,如果我们写下一个DM9000网卡的驱动,Linux的想法是这个驱动最好一行都不要改就可以在任何一个平台上跑起来。为了做到这一点(看似很难,因为每个板子连接DM9000的基地址,中断号什么的都可能不一样),驱动中势必会有类似的代码:

  1. #ifdef BOARD_XXX
  2. #define DM9000_BASE 0x10000
  3. #define DM9000_IRQ 8
  4. #elif defined(BOARD_YYY)
  5. #define DM9000_BASE 0x20000
  6. #define DM9000_IRQ 7
  7. #elif defined(BOARD_ZZZ)
  8. #define DM9000_BASE 0x20000
  9. #define DM9000_IRQ 7
  10. #else
  11. /* ... define ... */
  12. #endif

上述代码主要有如下问题:

1)如果有100个板子,就要 if/else 100次,代码进行着简单的“复制-粘贴”,不利于维护。

2)非常难做到一个驱动支持多个设备,如果某个电路板上有两个DM9000网卡,则DM9000_BASE这个宏就不够用了,此时势必要定义出来DM9000_BASE_1、DM9000_IRQ_2之类的宏。

3)依赖于make menuconfig选择的项目来编译内核,因此,在不同的硬件平台下要依赖于所选择的BOARD_XXX、BOARD_YYY选项来决定代码逻辑。这不符合ARM Linux 3.x一个映像适用于多个硬件的目标。

我们按照上面方法编写代码的时候,相信自己编着编着也会觉得奇怪。我们有没有办法把设备端的信息从驱动里面剥离出来,让驱动以某种标准方法拿到这些平台信息呢?Linux总线、设备和驱动模型实际上可以做到这一点,驱动只管驱动,设备只管设备,总线则负责匹配设备和驱动,而驱动则以标准途径拿到板级信息,这样,驱动就可以放之四海而皆准了,如下图所示。

Linux的字符设备驱动需要编写file_operations成员函数,并负责处理阻塞、非阻塞、多路复用、SIGIO等复杂事务。但是,当我们面对一个真实的硬件驱动时,假如要编写一个按键的驱动,作为一个“懒散”的程序员,你真的只想做最简单的工作,譬如,收到一个按键中断、汇报一个按键值,file_operations、I/O模型等Linux的事情不想关心。于是,这里就衍生出来一个软件分层的想法,尽管file_operations、I/O模型不可或缺,但是关于此部分的代码,全世界恐怕所有的输入设备都是一样的,为什么不提炼一个中间层出来,把这些事情搞定,也就是在底层编写驱动的时候,搞定具体的硬件操作。

将软件进行分层设计应该是软件工程最基本的一个思想,如果提炼一个input的核心层出来,把跟Linux接口以及整个一套的汇报机制都在这里面实现,如下图所示,显然是非常好的。

在Linux设备驱动框架的设计中,除了分层设计以外,还有分隔的思想。举一个简单的例子,假设我们要通过SPI总线访问某外设,假设CPU的名字叫XXX1,SPI外设叫YYY1。在访问YYY1外设的时候,要通过操作CPU XXX1上的SPI控制器的寄存器才能达到访问SPI外设YYY1的目的,最简单的代码逻辑是:

  1. void cpu_xxx1_spi_reg_write();
  2. void cpu_xxx1_spi_reg_read();
  3. void spi_client_yyy1_work1();
  4.  
  5. void cpu_xxx2_spi_reg_write();
  6. void cpu_xxx2_spi_reg_read();
  7. void spi_client_yyy1_work2();

如果按照这种方式来设计驱动,结果对于任何一个SPI外设来讲,它的驱动代码都是与CPU相关的。也就是说,当代码用在CPU XXX1上的时候,它访问XXX1的SPI主机控制寄存器,当用在XXX2上的时候,它访问XXX2的SPI主机控制寄存器。

这显然是不被接收的,因为这意味着外设YYY1用在不同的CPU1 XXX1和XXX2上的时候需要不同的驱动。同时,如果CPU XXX1除了支持YYY1以外,还要支持外设YYY2、YYY3、YYY4等,这个XXX的代码就要重复出现在YYY1、……、YYY4的驱动里面。

按照这样的逻辑,如果要让N个不同的YYY在M个不同的CPU XXX上跑起来,需要M*N份代码。这是一种典型的强耦合,不符合软件工程“高内聚、低耦合”和“信息隐蔽”的基本原则。

这种软件架构是一种典型的网状耦合,网状耦合一般不适合人类的思维方式,会把我们的思维搞乱。对于网站耦合的M:N,我们一般要提炼出一个中间“1”,让M与“1”耦合,N也与这个“1”耦合,如下图所示。

那么,我们可以用如下图所示的思想对主机控制器驱动和外设驱动进行分离。这样的结果是,外设YYY1、……、YYY4的驱动与主机控制器XXX1、……、XXX4的驱动不相关,主机驱动不关心外设,而外设也不关心主机,外设只是访问核心层的通用API进行数据传输,主机和外设之间可以进行任意组合。

platform设备驱动

platform总线、设备与驱动

在Linux2.6以后的设备驱动模型种,需关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。

一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等设备而言,这自然不是问题,但是在嵌入式系统里面,在SoC系统种集成的独立外设控制器、挂接在SoC内存空间的外设等却不依附于此类总线。基于这一背景,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动称为platform_driver。

所谓的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段,例如,我们通常把SoC内部集成的I2C、RTC、LCD、看门狗等控制器都归纳为platform_device,而它们本身就是字符设备。platform_device的结构体定义如下代码所示。

  1. struct platform_device {
  2.     const char *name;
  3.     int id;
  4.     bool id_auto;
  5.     struct device dev;
  6.     u32 num_resources;
  7.     struct resource *resource;
  8.  
  9.     const struct platform_device_id *id_entry;
  10.     char *driver_override; /* Driver name to force a match */
  11.  
  12.     /* MFD cell pointer */
  13.     struct mfd_cell *mfd_cell;
  14.  
  15.     /* arch specific additions */
  16.     struct pdev_archdata archdata;
  17. };

platform_driver这个结构体中包含probe()、remove()、一个device_driver实例、电源管理函数suspend()、resume(),如下代码所示。

  1. struct platform_driver {
  2.     int (*probe)(struct platform_device *);
  3.     int (*remove)(struct platform_device *);
  4.     void (*shutdown)(struct platform_device *);
  5.     int (*suspend)(struct platform_device *, pm_message_t state);
  6.     int (*resume)(struct platform_device *);
  7.     struct device_driver driver;
  8.     const struct platform_device_id *id_table;
  9.     bool prevent_deferred_probe;
  10. };

直接填充platform_driver的suspend()、resume()做电源管理回调的方法目前已经过时,较好的做法是实现platform_driver的device_driver中的dev_pm_ops结构体成员,以下代码给出了device_driver的定义。

  1. struct device_driver {
  2.     const char *name;
  3.     struct bus_type *bus;
  4.  
  5.     struct module *owner;
  6.     const char *mod_name; /* used for built-in modules */
  7.  
  8.     bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
  9.     enum probe_type probe_type;
  10.  
  11.     const struct of_device_id *of_match_table;
  12.     const struct acpi_device_id *acpi_match_table;
  13.  
  14.     int (*probe) (struct device *dev);
  15.     int (*remove) (struct device *dev);
  16.     void (*shutdown) (struct device *dev);
  17.     int (*suspend) (struct device *dev, pm_message_t state);
  18.     int (*resume) (struct device *dev);
  19.     const struct attribute_group **groups;
  20.  
  21.     const struct dev_pm_ops *pm;
  22.     void (*coredump) (struct device *dev);
  23.  
  24.     struct driver_private *p;
  25. };

与platform_driver地位对等的i2c_driver、spi_driver、usb_driver、pci_driver中都包含了device_driver结构体实例成员。它其实描述了各种xxx_driver(xxx是总线名)在驱动意义上的一些共性。

系统为platform总线定义一个bus_type的实例platform_bus_type,其定义位于drivers/base/platform.c下,如下代码所示。

  1. struct bus_type platform_bus_type = {
  2.     .name = "platform",
  3.     .dev_groups = platform_dev_groups,
  4.     .match = platform_match,
  5.     .uevent = platform_uevent,
  6.     .dma_configure = platform_dma_configure,
  7.     .pm = &platform_dev_pm_ops,
  8. };

这里要重点关注其match()成员函数,正是此成员函数确定了platform_device和platform_driver之间是如何进行匹配的,如下代码所示。

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

从以上代码可以看出,匹配platform_device和platform_driver有4种可能性,一是基于设备树风格的匹配;三是匹配ID表(即platform_device设备名是否出现在platform_driver的ID表内);第四种是匹配platform_device设备名和驱动的名字。

对于Linux2.6 ARM平台而言,对platform_device的定义通常在BSP的板文件中实现,在板文件中,将platform_device归纳成一个数组,最终通过platform_add_devices()函数统一注册。platform_add_devices()函数可以将平台设备添加到系统中,这个函数的原型为:

  • extern int platform_add_devices(struct platform_device **, int);

该函数的第一个参数为平台设备数组的指针,第二个参数为平台设备的数量,它内部调用了platform_device_register()函数以注册单个的平台设备。

Linux3.x之后,ARM Linux不太喜欢人们以编码的形式去填写platform_devices和注册,而倾向于根据设备树中的内容自动展开platform_device。

思考和总结

感觉越往后实验就越难做了,原理也就了解个大概。感觉后续的内容和硬件的关联度越来越高,还需要继续琢磨,之后有条件买一款开发板实验一下。