嵌入式Linux驱动教程PART1-设备与驱动模型

Published on
315 18~24 min

想要开发Linux驱动,首先需要理解的就是Linux设备与驱动模型,它描述了设备与驱动的组织方式。这其中有三个重要的术语先要了解,它们是设备,驱动总线

  • 设备(device):是连接到总线上的物理或者虚拟对象。

  • 驱动(driver):是负责探测或者绑定设备的一段代码,可以处理特定总线上的特定设备。

  • 总线(bus):挂载设备的节点,是一种特殊的设备

下面将从这些术语出发,对整个Linux设备与驱动模型做一个大致的讲解。以6.6内核版本为例。

1 总线

在计算机系统中,总线一般分有内部总线,用于连接CPU内部各器件;系统总线,用于连接系统内的各个部件;外部总线,用于连接不同计算机系统。在Linux设备与驱动模型中所说的总线一般指的是系统总线,例如I2CSPIUSB总线等。除此之外,由于嵌入式系统中存在着许多不依附于总线的设备(例如使用GPIO的设备),所以Linux设备与驱动模型中也提供了一个叫“平台(platform)”的虚拟总线用于管理这些设备。

每个内核支持的总线都会有一个对应的总线核心驱动。总线核心驱动会定义并配置一个bus_type类型的结构体,并将这个结构体注册到内核的总线类型链表中。bus_type 类型定义在Linux内核源码下的include/linux/device/bus.h:80 中,用来描述一类总线。将总线(bus_type结构体)注册到内核是通过调用bus_register()函数来完成的。下面先来看看bus.h头文件中的相关定义:

struct bus_type {
	const char		*name;
	const char		*dev_name;
	const struct attribute_group **bus_groups;
	const struct attribute_group **dev_groups;
	const struct attribute_group **drv_groups;

	int (*match)(struct device *dev, struct device_driver *drv);
	int (*uevent)(const struct device *dev, struct kobj_uevent_env *env);
	int (*probe)(struct device *dev);
	void (*sync_state)(struct device *dev);
	void (*remove)(struct device *dev);
	void (*shutdown)(struct device *dev);

	int (*online)(struct device *dev);
	int (*offline)(struct device *dev);

	int (*suspend)(struct device *dev, pm_message_t state);
	int (*resume)(struct device *dev);

	int (*num_vf)(struct device *dev);

	int (*dma_configure)(struct device *dev);
	void (*dma_cleanup)(struct device *dev);

	const struct dev_pm_ops *pm;

	const struct iommu_ops *iommu_ops;

	bool need_parent_lock;
};

int __must_check bus_register(const struct bus_type *bus);

上述代码包含了bus_type类型和bus_register()函数的定义。先简要说明一下bus_type类型中的重要成员,你可能会看见一些不太认识的概念,但别着急,这会在后面进行解释。

  • name:总线的名称,会显示在/sys/bus目录下。

  • bus_groups、dev_groups、drv_groups:总线、总线上的设备和驱动的默认属性。

  • match:用于匹配设备和驱动的函数,每有设备或驱动注册在总线上时调用。

  • uevent:设备注册在总线上时用于触发uevents的函数。

  • probe:设备和驱动注册在总线上时调用,会回调驱动的probe()函数对设备进行配置。

  • remove:对应probe,设备和驱动注销时调用。

下面以平台(platform)总线核心驱动的一段代码作为例子:

struct device platform_bus = {
	.init_name	= "platform",
};
EXPORT_SYMBOL_GPL(platform_bus);

struct bus_type platform_bus_type = {
	.name		= "platform",
	.dev_groups	= platform_dev_groups,
	.match		= platform_match,
	.uevent		= platform_uevent,
	.probe		= platform_probe,
	.remove		= platform_remove,
	.shutdown	= platform_shutdown,
	.dma_configure	= platform_dma_configure,
	.dma_cleanup	= platform_dma_cleanup,
	.pm		= &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);

int __init platform_bus_init(void)
{
	int error;

	early_platform_cleanup();

	error = device_register(&platform_bus);
	if (error) {
		put_device(&platform_bus);
		return error;
	}
	error =  bus_register(&platform_bus_type);
	if (error)
		device_unregister(&platform_bus);

	return error;
}

上面的代码中,可以看到首先创建了devicebus_type结构体,并填充了相关信息,最后在platform_bus_init()函数中使用device_register()bus_register()函数注册设备和总线。

那么bus_register()做了什么工作呢?本人简略了bus_register() 的代码(在drivers/base/bus.c:853)并加以注释,内容如下:

int bus_register(const struct bus_type *bus)
{
	int retval;
	struct subsys_private *priv;    //总线的私有数据
	struct kobject *bus_kobj;    //总线对应的内核对象
	
	...

	//分配私有数据的空间
	priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
    

	priv->bus = bus;    //指明该私有数据是总线的

	...

	//这里有一段将总线的私有数据与总线对应的内核对象联系起来的代码,以后可以通过内核对象找到总线对应的私有数据

	...

	//初始化总线的私有数据里的设备和驱动列表
	klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);
	klist_init(&priv->klist_drivers, NULL, NULL);

	...

	pr_debug("bus: '%s': registered\n", bus->name);
	return 0;

...
}

可以看出,bus_register()函数分配了总线的私有数据的空间,并将私有数据与总线对应的内核对象联系起来,同时,还做了初始化总线的私有数据里设备和驱动列表的工作。

在较早的Linux内核版本中[1]bus_type类型里就有subsys_private类型的结构体成员*p,该成员用来存储总线私有数据。而在上述较新版本的Linux内核中,总线的私有数据是在bus_register()函数中分配空间,然后与总线对应的内核对象联系,并不直接作为结构体成员。当然,目前你并不太需要了解这其中的细节,重要的是,总线的私有数据维护了设备和驱动的列表。

2 设备与总线控制器

总线的私有数据维护了设备和驱动的列表,那这些链表在什么时候会发生变化(更新)呢?对于设备列表,这有两种情况:

  1. 系统初始化阶段时总线控制器驱动会扫描已插入的设备,调用device_register()函数注册设备,更新列表。

  2. 新设备插入系统(热插拔)被总线控制器驱动侦测到,调用device_register()函数注册设备,更新列表。

当总线控制器驱动注册设备时,设备的device类型的结构体成员*parent 会被设置成总线控制器总线控制器是什么呢?对于一个特定的总线类型,系统中可能存在着不同供应商提供的多个控制器,每个控制器相当于对应着一类设备。注意,总线控制器同样是设备,需要被注册到总线上。由于总线控制器一般是系统中的固有设备,所以是在内核初始化阶段进行注册的。不同的总线控制器都需要各自对应的总线控制器驱动,总线控制器驱动的主要作用就是发现设备和配置资源。

3 驱动与绑定

对于驱动列表,其更新一般在调用driver_register()时。编写驱动时需要实例化和注册一个device_driver类型的结构体,该类型定义在include/linux/device/driver.h:96下:

struct device_driver {
	const char		*name;
	const 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);
	void (*sync_state)(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 attribute_group **dev_groups;

	const struct dev_pm_ops *pm;
	void (*coredump) (struct device *dev);

	struct driver_private *p;
};

简要说明一下device_driver类型中的重要成员:

  • bus:用来标识驱动注册到哪个总线上。

  • probe:设备和驱动绑定时调用的函数,用于配置设备。

  • remove:设备移除,驱动卸载或者系统关闭时调用的函数,用来与设备解绑。

设备和驱动什么时候绑定呢?第一,一个驱动被注册到总线时,与总线关联的设备的列表会被依次遍历,总线调用其match()函数来确定是否有设备能被该驱动支持。如果找到匹配的设备,就会执行绑定。总线调用其probe()函数,而驱动的probe()函数也会被随之调用。第二,一个设备被注册到总线时,与总线关联的驱动的列表会被依次遍历,总线调用其match()函数来确定是否有支持该设备的驱动。如果找到匹配的驱动,就会执行绑定。总线调用其probe()函数,而驱动的probe()函数也会被随之调用。

参考文献:
1.《Linux Driver Development for Embedded Processors》- Alberto Liberal de los Ríos

2. The Linux Kernel Documentation

更新日志:
2025.2.8 - 文章更新


Prev Post imx6ull开发板移植新版uboot、kernel和文件系统
Next Post C语言-如何读懂数组,指针,函数三者混合的派生类型