Linux 设备驱动的软件架构思想(一)(摘录)
在前面几章我们看到了 globalmem 、 globalfifo 这样类型的简单的字符设备驱动,但是,纵观 Linux 内核的源代码,读者几乎找不到有如此简单形式的驱动。
在实际的 Linux 驱动中, Linux 内核尽量做得更多,以便于底层的驱动可以做得更少。而且,也特别强调了驱动的跨平台特性。因此, Linux 内核势必会为不同的驱动子系统设计不同的框架。
举一个简单的例子,假设我们要通过 SPI 总线访问某外设,假设 CPU 的名字叫 XXX1 , SPI 外设叫 YYY1 。在访问 YYY1 外设的时候,要通过操作 CPU XXX1 上的 SPI 控制器的寄存器才能达到访问 SPI 外设 YYY1 的目的,这显然是不被接受的,因为这意味着外设 YYY1 用在不同的 CPU XXX1 和 XXX2 上的时候需要不同的驱动。同时,如果CPU XXX1 除了支持 YYY1 以外,还要支持外设 YYY2 、 YYY3 、 YYY4 等,这个 XXX 的代码就要重复出现在 YYY1 、YYY2 、 YYY3 、 YYY4 的驱动里面
这种软件架构是一种典型的网状耦合,网状耦合一般不太适合人类的思维逻辑,会把我们的思维搞乱。对于网状耦合的M ∶ N ,我们一般要提炼出一个中间 “1” ,让 M 与 “1” 耦合, N 也与这个 “1” 耦合
那么,我们可以用如图 12.4 所示的思想对主机控制器驱动和外设驱动进行分离。这样的结果是,外设 YYY1 、 YYY2 、YYY3 、 YYY4 的驱动与主机控制器 XXX1 、 XXX2 、 XXX3 、 XXX4 的驱动不相关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层的通用 API 进行数据传输,主机和外设之间可以进行任意组合。
如果我们不进行如图 12.4 所示的主机和外设分离,外设 YYY1 、 YYY2 、 YYY3 和主机 XXX1 、 XXX2 、 XXX3 进行组合的时候,需要 9 个不同的驱动。设想一共有 m 个主机控制器, n 个外设,分离的结果是需要 m+n 个驱动,不分离则需要 m*n个驱动。因为, m 个主机控制器, n 个外设的驱动都可以被充分地复用了。
platform 设备驱动
在 Linux 2.6 以后的设备驱动模型中,需关心总线、设备和驱动这 3 个实体,总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。
一个现实的 Linux 设备和驱动通常都需要挂接在一种总线上,对于本身依附于 PCI 、 USB 、 I 2 C 、 SPI 等的设备而言,这自然不是问题,但是在嵌入式系统里面,在 SoC 系统中集成的独立外设控制器、挂接在 SoC 内存空间的外设等却不依附于此类总线。基于这一背景, Linux 发明了一种虚拟的总线,称为 platform 总线,相应的设备称为platform_device ,而驱动成为 platform_driver 。
platform_device 结构体
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
/* MFD cell pointer */
struct mfd_cell *mfd_cell;
/* arch specific additions */
struct pdev_archdata archdata;
};
platform_driver 结构体
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
直接填充 platform_driver 的 suspend ()、 resume ()做电源管理回调的方法目前已经过时,较好的做法是实现platform_driver 的 device_driver 中的 dev_pm_ops 结构体成员(后续的 Linux 电源管理章节会对此进行更细致的介绍)
device_driver 结构体
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
enum probe_type probe_type;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};
与 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 下
platform 总线的 bus_type 实例 platform_bus_type
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
这里要重点关注其 match ()成员函数,正是此成员函数确定了 platform_device 和 platform_driver 之间是如何进行匹配
platform_bus_type 的 match ()成员函数
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* When driver_override is set, only bind to the matching driver */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI style match */
if (acpi_driver_match_device(dev, drv))
return 1;
/* Then try to match against the id table */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* fall-back to driver name match */
return (strcmp(pdev->name, drv->name) == 0);
}
从代码可以看出,匹配 platform_device 和 platform_driver 有 4 种可能性,一是基于设备树风格的匹配;二是基于 ACPI 风格的匹配;三是匹配 ID 表(即 platform_device 设备名是否出现在platform_driver 的 ID 表内);第四种是匹配 platform_device 设备名和驱动的名字。
对于 Linux 2.6ARM 平台而言,对 platform_device 的定义通常在 BSP 的板文件中实现,在板文件中,将platform_device 归纳为一个数组,最终通过 platform_add_devices ()函数统一注册。 platform_add_devices ()函数可以将平台设备添加到系统中,这个函数的原型为:
int platform_add_devices(struct platform_device **devs, int num);
该函数的第一个参数为平台设备数组的指针,第二个参数为平台设备的数量,它内部调用了platform_device_register ()函数以注册单个的平台设备。Linux 3.x 之后, ARM Linux 不太喜欢人们以编码的形式去填写 platform_device 和注册,而倾向于根据设备树中的内容自动展开 platform_device 。