Linux设备模型

x33g5p2x  于2022-06-13 转载在 Linux  
字(28.1k)|赞(0)|评价(0)|浏览(549)

本文是对这篇文章的补充:从零开始学Linux设备驱动–Linux设备模型

设备模型基础补充

关于解决
不能查看相应的设备和驱动的的信息,比如Windows中有设备管理器,我们就可以比较方便的查看相关的设备和驱动的信息。

是关于设备和驱动信息的展示。在Linux系统中有一个sysfs伪文件系统,挂载于/sys目录下,该目录罗列了设备、驱动和硬件相关的信息

在fs4412的终端上,可以使用下面的命令来查看

在/sys目录下有很多子目录
例如
block目录下是块设备
bus目录下是系统中的所有总线(如I2C、SPI和 USB等)
class目录下是一些设备类(如 input输入设备类、tty终端设备类)
devices目录下是系统中所有的设备。

再仔细查看/sys/bus/platforml/devices/5000000.ethernet/目录,它是一个挂接在一个叫platform总线下的以太网设备,其目录下的driver是一个软链接,指向了... ../bus/platform/drivers/dm9000
也就是说,该设备是被注册在platform总线下的一个名叫dm9000的驱动程序所驱动

再看对应的驱动目录/sys/bus/platform/drivers/dm9000/,会发现该驱动程序驱动了..../ .. ../devices/5000000.srom-cs1/5000000.ethernet设备,即驱动了devices目录下的以太网设备

/sys/bus/platform/devices/5000000.ethernet又指向.../.devices/5000000.srom-cs1/5000000.ethernet的软链接。
所以也可以说前面的驱动程序驱动了/sys/bus/platform/drivers/dm9000/设备

上面的内容看起来有点乱,但思路是清晰的,即在总线bus目录下有很多具体的总线,而具体的总线目录下有注册的驱动和挂接的设备,注册的驱动程序驱动对应总线目录下的某些具体设备,总线目录下的某些设备被对应总线下的某个驱动程序所驱动。

那么上面这些信息是怎么来的呢。我们知道,伪文件系统在系统运行时才会有内容,也就是说,伪文件系统的目录、文件以及软链接都是动态生成的,这些内容都是反映内核的相关信息,回顾我们之前学习的 proc接口,不难猜想得出这些信息的生成可以在驱动中来实现。

structkobject

接下来我们就来讨论要生成这些信息的一个重要内核数据结构——structkobject

了解MFC 或者Qt的人都知道那些窗口部件都是一层一层继承下来的,而在最上层有一个最基础的类,MFC的根类是CObject,而 Qt的根类则是QObject。

在这里我们将结构看成类,那么kobject就是Linux 设备驱动模型中的根类。作为驱动开发者,我们没有必要了解kobject 的详细信息,就像作为一个Qt应用程序开发者不需要了解QObject的详细信息一样。

在这里,我们只需知道它和/sys目录下的目录和文件的关系

当向内核成功添加一个kobject对象后,底层的代码会自动在/sys目录下生成一个子目录。

另外,kobject可以附加一些属性,并绑定操作这些属性的方法,当向内核成功添加一个kobject对象后,其附加的属性会被底层的代码自动实现为对象对应目录下的文件,用户访问这些文件最终就变成了调用操作属性的方法来访问其属性。最后,通过 sys的API接口可以将两个kobject对象关联起来,形成软链接

struct kset

除了struct kobject,还有一个叫struct kset的类,它是多个kobject对象的集合,也就是多个kobject对象可以通过一个kset集合在一起。kset本身也内嵌了一个kobject,它可以作为集合中的kobject对象的父对象,从而在 kobject之间形成父子关系,这种父子关系在/sys目录下体现为父目录和子目录的关系。而属于同一集合的 kobject对象形成兄弟关系,在/sys目录下体现为同级目录。

kset也可以附加属性,从而在对应的目录下产生文件。

例子

为了能更好地了解这部分内容,而又不过分深入细节,特别编写了一个非常简单的模块,为了突出主线,省略了出错处理

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/slab.h>
#include <linux/kobject.h>

static struct kset *kset;
static struct kobject *kobj1;
static struct kobject *kobj2;
static unsigned int val = 0;

static ssize_t val_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
	return snprintf(buf, PAGE_SIZE, "%d\n", val);
}

static ssize_t val_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
	char *endp;

	printk("size = %d\n", count);
	val = simple_strtoul(buf, &endp, 10);

	return count;
}

static struct kobj_attribute kobj1_val_attr = __ATTR(val, 0666, val_show, val_store);
static struct attribute *kobj1_attrs[] = {
	&kobj1_val_attr.attr,
	NULL,
};

static struct attribute_group kobj1_attr_group = { 
	        .attrs = kobj1_attrs,
};

static int __init model_init(void)
{
	int ret;

	kset = kset_create_and_add("kset", NULL, NULL);
	kobj1 = kobject_create_and_add("kobj1", &kset->kobj);
	kobj2 = kobject_create_and_add("kobj2", &kset->kobj);

	ret = sysfs_create_group(kobj1, &kobj1_attr_group);
	ret = sysfs_create_link(kobj2, kobj1, "kobj1");

	return 0;
}

static void __exit model_exit(void)
{
	sysfs_remove_link(kobj2, "kobj1");
	sysfs_remove_group(kobj1, &kobj1_attr_group);
	kobject_del(kobj2);
	kobject_del(kobj1);
	kset_unregister(kset);
}

module_init(model_init);
module_exit(model_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple module for device model");

代码第42行使用kset_create_and_add创建并向内核添加了一个名叫kset的kset对象。

kset = kset_create_and_add("kset", NULL, NULL);

代码第43行和第44行用kobject_create_and_add 分别创建并向内核添加了两个名叫kobj1
和kobj2的kobject对象。

kobj1 = kobject_create_and_add("kobj1", &kset->kobj);
	kobj2 = kobject_create_and_add("kobj2", &kset->kobj);

代码第46行为kobj1添加了一组属性kobj1_attr_group

ret = sysfs_create_group(kobj1, &kobj1_attr_group);

这组属性中只有一个属性kobj1_val_ attr, 属性的名字叫val, 所绑定的读和写的方法分别是val_show 和val_store, 对应的文件访问权限是0666。

代码第47行使用sysfs_create_ link在kobj2下创建了一个kobj1的软链接,名叫kobj1。

ret = sysfs_create_link(kobj2, kobj1, "kobj1");

代码第54行至第58行是初始化操作的反操作,用于删除软链接、属性和对象。

static void __exit model_exit(void)
{
	sysfs_remove_link(kobj2, "kobj1");
	sysfs_remove_group(kobj1, &kobj1_attr_group);
	kobject_del(kobj2);
	kobject_del(kobj1);
	kset_unregister(kset);
}

属性val的读方法将val的值以格式%d打印在buf中,那么读取相应的属性文件,则会得到val的十进制字符串

属性val的写方法是将用户写入文件的内容,即buf中的字符串通过simple_strtoul将字符串转换成十进制的数值再赋值给val

测试:

在创建kset对象时,由于没有指定其父对象,所以kset位于/sys目录下,在创建kobj1
和kobj2时,指定其父对象为kset中内嵌的kobject,所以kobj1 和kobj2位于kset目录之
下。

kobj1 附加了一个属性叫val,所以在kobj1目录下有一个val的文件,对该文件可以
进行读写,其实就是对属性val进行读写。
在kobj2下创建了一个软链接kobj1,所以在kobj2目录下有kobj1的软链接。

对象的关系如图8.1所示。其中,虚线表示kobj1、kobj2属于集合kset,kobj1和kobj2实线指向kset内嵌的kobject表示它们的父对象是kset内嵌的kobject.

总线设备和驱动补充

基本知识见最上面链接文章
为了更好的理解设备模型的影响,以一个简单的例子来说明

/* vbus.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/device.h>

static int vbus_match(struct device *dev, struct device_driver *drv)
{
	return 1;
}

static struct bus_type vbus = {
	.name = "vbus",
	.match = vbus_match,
};

EXPORT_SYMBOL(vbus);

static int __init vbus_init(void)
{
	return bus_register(&vbus);
}

static void __exit vbus_exit(void)
{
	bus_unregister(&vbus);
}

module_init(vbus_init);
module_exit(vbus_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A virtual bus");

在vbus.c文件中,代码第12行至第15行

static struct bus_type vbus = {
	.name = "vbus",
	.match = vbus_match,
};

定义了一个代表总线的vbus对象,该总线的名字是vbus,用于匹配驱动和设备的函数是vbus_ match

代码第21行向内核注册了该总线。

static int __init vbus_init(void)
{
	return bus_register(&vbus);
}

代码第26行是总线的注销。

static void __exit vbus_exit(void)
{
	bus_unregister(&vbus);
}

为了简单起见,vbus_match仅仅返回1,表示传入的设备和驱动匹配成功,而更一般的情况是考察它们的ID号是否匹配

/* vdrv.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/device.h>

extern struct bus_type vbus;

static struct device_driver vdrv = {
	.name = "vdrv",
	.bus = &vbus,
};

static int __init vdrv_init(void)
{
	return driver_register(&vdrv);
}

static void __exit vdrv_exit(void)
{
	driver_unregister(&vdrv);
}

module_init(vdrv_init);
module_exit(vdrv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A virtual device driver");

在vdrv.c文件中,代码第9行至第12行定义了一个代表驱动的vdrv对象,该驱动的名字是vdrv,所属的总线是vbus

static struct device_driver vdrv = {
	.name = "vdrv",
	.bus = &vbus,
};

这样注册这个驱动时,就会将之注册在vbus总线之下。

代码第16行和第21行分别是驱动的注册和注销操作。

static int __init vdrv_init(void)
{
	return driver_register(&vdrv);
}

static void __exit vdrv_exit(void)
{
	driver_unregister(&vdrv);
}

模块中使用了vbus模块导出的符号vbus。

/* vdev.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/device.h>

extern struct bus_type vbus;

static void vdev_release(struct device *dev)
{
}

static struct device vdev = {
	.init_name = "vdev",
	.bus = &vbus,
	.release = vdev_release,
};

static int __init vdev_init(void)
{
	return device_register(&vdev);
}

static void __exit vdev_exit(void)
{
	device_unregister(&vdev);
}

module_init(vdev_init);
module_exit(vdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A virtual device");

在vdev.c文件中,代码第13行至第17行定义了一个代表设备的vdev对象,该设备的名字是vdev,是完全用代码虚拟出来的一个设备。所属的总线是vbus,这样注册这个设备时,就会将之挂接到vbus总线之下。

static struct device vdev = {
	.init_name = "vdev",
	.bus = &vbus,
	.release = vdev_release,
};

还有一个用于释放的函数vdev_release, 为了简单起见,这个函数什么都没做。

代码第21行和第26行分别是设备的注册和注销。模块中使用了vbus模块导出的符号vbus。

static int __init vdev_init(void)
{
	return device_register(&vdev);
}

static void __exit vdev_exit(void)
{
	device_unregister(&vdev);
}

下面是测试和编译的命令结果:

在加载了vbus模块后,/sys/bus 目录下自动生成了vbus目录,并且在vbus目录下生
成了devices和drivers两个目录,分别来记录挂接在vbus总线上的设备 和注册在vbus总线上的驱动

当加载了vdrv模块后,/sys/bus/vbus/drivers 目录下自动生成了vdrv 目录,此时还没有设备与之绑定。

当加载了vdev模块后,/sys/bus/vbus/devices 目录下自动生成了vdev目录,并…bus/vbus/drivers/vdrv的驱动绑定成功,在/sys/devices 目录下也自动生成了vdev目录

其实/sys/bus/vbus/devices/vdev是指向/sys/bus/vbus/devices/vdev的软链接

最后/sys/bus/vbus/drivers/vdrv/中的 vdev 也指定了其绑定的设备…/…/…/…/devices/vdev。

这和我们在前面看到的DM9000网卡非常类似,只是DM9000网卡设备是挂接在platform总线下的,而驱动也是注册在platform总线下的。

虽然使用struct bus_type、 struct device和struct device_driver 能够实现Linux设备模型,但是它们的抽象层次还是太高,不能具体地刻画某一种特定的总线。

所以一种具体的总线会在它们的基础上派生出来,形成更具体的子类,这些子类对象能够更好地描述相应的对象。比如,针对USB总线就派生出了struct usb_bus_type、 struct usb_device 和struct usb_driver, 分别代表具体的USB总线、USB设备和USB驱动。

通常情况下,总线已经在内核中实现好,我们只需要写对应总线的驱动即可,有时候还会编写相应的设备注册代码。

平台设备补充

平台设备及其资源常存在于BSP(Board Support Package, 板级支持包)文件中,该文件通常包含和目标板相关的一些代码。

例如对于QT2410目标板,其对应的BSP文件为arch/arm/mach-s3c24xx/mach-qt2410.c
以CS8900网卡的平台设备摘录如下:

static struct resource qt2410_cs89x0_resources[]={
[0] = DEFINE_RES_MEM(0x19000000,17),
[1] = DEFINE_RES_IRQ(IRQ_EINT9),
};
static struct platform_device qt2410_cs89x0 = {
.name  ="cirrus-cs89x0",
.num resources  =ARRAY SIZE(qt2410_cs89x0_resources),
.resource   =qt2410_cs89x0_resources,
};

CS8900平台设备有两个资源,分别是IORESOURCE_MEM和IORESOURCE_IRQ两种类型的,并用宏DEFINE_RES_MEM和DEFINE_RES_IRQ来定义。

对于DEFINE_RES_MEM宏,里面的两个参数分别是内存的起始地址和大小;

对于DEFINE_RES_IRQ宏,里面的参数则是中断号

读者可以自行查看这两个宏的定义,最终是对start、end 和flags成员进行了赋值

最终定义的平台设备是qt2410_cs89x0, ARRAY_SIZE 是用于获取数组元素个数的宏。

平台总线注册和注销平台设备的API接口补充

主要是举个利用它的例子:

例如:在CS8900网卡驱动中就有如下的代码来获取资源及其大小。

mem_res = platform get_resource(pdev, IORESOURCE_MEM, 0);
dev->irq = platform get_irq(pdev,0);
lp->size = resource_size(mem_res);
virt_addr = ioremap(mem_res->start, lp->size);

先获取了IORESOURCE_ MEM资源,序号为0。
再获取了IORESOURCE_IRQ资源,序号也为0。
所以,当资源类型不同后,序号重新开始编号

lp->size = resource_size(mem_res);获取了内存资源的大小。

最后使用ioremap将内存资源进行映射,得到映射后的虚拟地址。

平台驱动补充

因为在驱动中,经常在模块初始化函数中注册一个平台驱动,在清除函数中注销一个平台驱动

所以内核定义了一个宏来简化这些代码(module_platform_driver)宏的定义如下:

#define module_platform_driver(_platform_driver)\
module_driver(__platform driver, platform driver_register,\
platform_driver_unregister)

#define module_driver(driver,_register,__unregister, ...)\
static int _init__driver##_init_(void)\
(\
return register(&(__driver),##__VA_ARGS__);\
}\
module_init(__driver##_init);\
static void__exit__driver##_exit(void)\
(\
__unregister(&(driver),## __VA_ARGS__); \
module exit(__driver##_exit);

平台驱动简单实例

在前面的基础之上,我们来编写一个简单的平台驱动,再编写一个模块来注册二个设备,代码如下:

/* pltdev.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/platform_device.h>

static void pdev_release(struct device *dev)
{
}

struct platform_device pdev0 = {
	.name = "pdev",
	.id = 0,
	.num_resources = 0,
	.resource = NULL,
	.dev = {
		.release = pdev_release,
	},
};

struct platform_device pdev1 = {
	.name = "pdev",
	.id = 1,
	.num_resources = 0,
	.resource = NULL,
	.dev = {
		.release = pdev_release,
	},
};

static int __init pltdev_init(void)
{
	platform_device_register(&pdev0);
	platform_device_register(&pdev1);

	return 0;
}

static void __exit pltdev_exit(void)
{
	platform_device_unregister(&pdev1);
	platform_device_unregister(&pdev0);
}

module_init(pltdev_init);
module_exit(pltdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("register a platfom device");

解析:
在pltdev.c文件中,代码第7行至第29行分别定义了两个平台设备,id 为0和1,以示区别,名字都为pdev,没有使用任何资源。

static void pdev_release(struct device *dev)
{
}

struct platform_device pdev0 = {
	.name = "pdev",
	.id = 0,
	.num_resources = 0,
	.resource = NULL,
	.dev = {
		.release = pdev_release,
	},
};

struct platform_device pdev1 = {
	.name = "pdev",
	.id = 1,
	.num_resources = 0,
	.resource = NULL,
	.dev = {
		.release = pdev_release,
	},
};

在(后面的init和exit)模块的初始化函数和清除函数中分别注册和注销了这两个平台设备。

/* pltdrv.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/platform_device.h>

static int pdrv_suspend(struct device *dev)
{
	printk("pdev: suspend\n");
	return 0;
}

static int pdrv_resume(struct device *dev)
{
	printk("pdev: resume\n");
	return 0;
}

static const struct dev_pm_ops pdrv_pm_ops = {
	.suspend = pdrv_suspend,
	.resume  = pdrv_resume,
};

static int pdrv_probe(struct platform_device *pdev)
{
	return 0;
}

static int pdrv_remove(struct platform_device *pdev)
{
	return 0;
}

struct platform_driver pdrv = {
	.driver = {
		.name    = "pdev",
		.owner   = THIS_MODULE,
		.pm      = &pdrv_pm_ops,
	},
	.probe   = pdrv_probe,
	.remove  = pdrv_remove,
};

module_platform_driver(pdrv);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple platform driver");
MODULE_ALIAS("platform:pdev");

解析:
在pltdrv.c文件中,代码第34行至第42行定义了一个平台驱动,名字也为pdev,这样才能和平台设备匹配。

struct platform_driver pdrv = {
	.driver = {
		.name    = "pdev",
		.owner   = THIS_MODULE,
		.pm      = &pdrv_pm_ops,
	},
	.probe   = pdrv_probe,
	.remove  = pdrv_remove,
};

.pm电源管理函数的集合,实现了挂起和恢复两个电源管理操作。因为是虚拟设备,所以并没有做任何电源管理相关的操作。
为了简单,probe和remove函数也只是返回成功而已。

代码第44行module_platform_driver(pdrv);使用module_ platform _driver这个宏来简化模块初始化函数和卸载函数的编写。
这个宏的定义见上面

编译和测试命令如下:

从上面的测试看出,平台驱动驱动了二个设备pdev.0 和 pdev.1,这是设备名字加id构成的名字

电源管理

在平台驱动里面实现了挂起和恢复两个电源管理函数,从而可以管理设备的电源状
态。

/sys/devices/platform/pdev.0/power/control/sys/devices/platform/pdev.1/power/control两个文件可以用来管理两个设备的电源控制方式,如果文件的内容为auto,那么设备的电源会根据系统的状态自动进行管理为on则表示打开

我们首先确定电源控制方式为自动,可以使用下面的命令进行确认。

cat /sys/devices/platform/pdev.0/power/control
auto
cat /sys/devices/platform/pdev.1/power/control
auto

接下来将Ubuntu系统挂起

系统挂起后,再恢复系统,使用dmesg可以看到,驱动中的suspend和resume(就是挂起和唤醒)函数先后都被调用了二次。

udev和驱动的自动加载

在上面的例子中,我们可以通过加载模块来向系统添加两个设备,也可以通过移除模块来删除这两个设备。
对于这样的操作,我们想使设备被添加到系统后,其驱动能够自动被加载,这对于实际的可支持热插拔的硬件来说更有必要。
比如,我们插入一个USB无线网卡,那么对应的驱动就应该自动加载,而不是由用户来手动加载。

要做到这一点,就必须利用到一个工具一udev,在嵌入式系统中通常使用mdev,其功能比udev要弱很多,但也可以移植udev到嵌入式系统上

使用了Linux设备模型后,任何设备的添加、删除或状态修改都会导致内核向用户空间发送相应的事件,这个事件叫uevent,和kobject密切关联。这样用户空间就可以捕获这些事件来自动完成某些操作,如自动加载驱动、自动创建和删除设备节点、修改权限、创建软链接、修改网络设备的名字等。

目前实现这个功能的工具就是udev (或mdev),这是一个用户空间的应用程序,捕获来自内核空间发来的事件,然后根据其规则文件进行操作

udev 的规则文件为/etc/udev/rules.d目录下后缀为.rules的文件。
udev规则文件用#来注释,除此之外的就是一条一条的规则。
每条规则至少包含一个键值对,键分为匹配和赋值两种类型。
如果内核发来的事件匹配了规则中的所有匹配键的值,那么这条规则就可以得到应用,并且赋值键被赋予指定的值。
一条规则包含了一个或多个键值对,这些键值对用逗号隔开每个键由操作符规定一个操作,合法的操作符如下:

  • ==和!=:判等,用于匹配键。
  • =、+=和:=:赋值,用于赋值键,=和:=的区别是前者允许用新值来覆盖原来的值,后者则不允许。+=则是追加赋值。

常见的键如下。

  • ACTION:事件动作的名字,如 add表示添加。
  • DEVPATH:事件设备的路径。
  • KERNEL:事件设备的名字。
  • NAME:节点或网络接口的名字。
  • SUBSYSTEM:事件设备子系统。
  • DRIVER:事件设备驱动的名字。
  • ENV {(key}:设备的属性。
  • OWNER、 GROUP、MODE:设备节点的权限。
  • RUN:添加一个和设备相关的命令到一个命令列表中。
  • IMPORT{type}:导入一组设备属性的变量,依赖于类型 type。

上面的键有的是匹配键,有的是赋值键,还有的既是匹配键又是赋值键。其他详细详见udev的man手册(帮助手册)

值还可以使用?、*和[]来进行通配,这和正则表达式中的含义是一样的。接下来来看一个例子:

ACTION=="add", SUBSYSTEM=="scsi_device", RUN+="/sbin/modprobe sg"

它表示当向SCSI子系统添加任意设备后都要添加一个命令“/sbin/modprobe sg”到命令列表中,这个命令就是为相应的设备加载sg驱动模块。

在Ubuntu中自动加载驱动的规则如下,请将这条规则添加到/etc/udev/rules.d/40-modprobe.rules文件中,如果没有这个文件请新建一个。

ENV{MODALIAS}=="?*",RUN+="/sbin/modprobe Senv{MODALIAS}"

它表示根据模块的别名信息,用modprobe命令加载对应的内核模块
为此,我们要给平台驱动一个别名,如 pltdrv.c文件中代码的MODULE_ALIAS("platform:pdev");

pdev要和驱动中用于匹配平台设备的名字保持一致。

MODULE_ALIAS("platform:pdev");

添加了这一条规则后,加载pltdev模块就可以自动加载平台 pltdrv驱动

使用平台设备的LED驱动

前面我们说过,之前的驱动最大的问题就是没有把设备和驱动分离开,这使得驱动的通用性很差。
只要硬件有任何改动(比如换一个管脚,增加或删除 LED 灯),都会导致驱动代码的修改。

有了Linux设备模型以及平台总线后,我们可以把设备的信息用平台设备来实现,这就大大提高了驱动的通用性。

接下来的任务就是把前面的LED驱动改造成基于平台总线的设备和驱动

首先是平台设备,代码如下:

/* fsdev.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/platform_device.h>

static void fsdev_release(struct device *dev)
{
}

static struct resource led2_resources[] = {
	[0] = DEFINE_RES_MEM(0x11000C40, 4),
};

static struct resource led3_resources[] = {
	[0] = DEFINE_RES_MEM(0x11000C20, 4),
};

static struct resource led4_resources[] = {
	[0] = DEFINE_RES_MEM(0x114001E0, 4),
};

static struct resource led5_resources[] = {
	[0] = DEFINE_RES_MEM(0x114001E0, 4),
};

unsigned int led2pin = 7;
unsigned int led3pin = 0;
unsigned int led4pin = 4;
unsigned int led5pin = 5;

struct platform_device fsled2 = {
	.name = "fsled",
	.id = 2,
	.num_resources = ARRAY_SIZE(led2_resources),
	.resource = led2_resources,
	.dev = {
		.release = fsdev_release,
		.platform_data = &led2pin,
	},
};

struct platform_device fsled3 = {
	.name = "fsled",
	.id = 3,
	.num_resources = ARRAY_SIZE(led3_resources),
	.resource = led3_resources,
	.dev = {
		.release = fsdev_release,
		.platform_data = &led3pin,
	},
};

struct platform_device fsled4 = {
	.name = "fsled",
	.id = 4,
	.num_resources = ARRAY_SIZE(led4_resources),
	.resource = led4_resources,
	.dev = {
		.release = fsdev_release,
		.platform_data = &led4pin,
	},
};

struct platform_device fsled5 = {
	.name = "fsled",
	.id = 5,
	.num_resources = ARRAY_SIZE(led5_resources),
	.resource = led5_resources,
	.dev = {
		.release = fsdev_release,
		.platform_data = &led5pin,
	},
};

static struct platform_device *fsled_devices[]  = {
	&fsled2,
	&fsled3,
	&fsled4,
	&fsled5,
};

static int __init fsdev_init(void)
{
	return platform_add_devices(fsled_devices, ARRAY_SIZE(fsled_devices));
}

static void __exit fsdev_exit(void)
{
	platform_device_unregister(&fsled5);
	platform_device_unregister(&fsled4);
	platform_device_unregister(&fsled3);
	platform_device_unregister(&fsled2);
}

module_init(fsdev_init);
module_exit(fsdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("register LED devices");

由上可知,我们分别定义了4个平台设备,每一个平台设备代表一个LED灯,之所以要这样做,是因为可以任意增加或删除一个LED灯。

4个平台设备都有一个IORESOURCE_MEM资源,用来描述2个寄存器所占用的内存空间;名字都为fsled,用来和平台驱动匹配;id分别为2、3、4、5,用来区别不同的设备。

还给每个平台设备的platform_data成员赋了值,platform_data的类型是void *,用来向驱动传递更多的信息,在这里传递的是每个LED灯使用的管脚号因为只有I/O内存是不能够控制一个具体的管脚的

这些平台设备放在fsled_devices数组中,在模块初始化函数中使用platform_add_devices一次注册到平台总线。

在模块的清除函数中,则使用 platform_device_unregister来注销。

再看看看平台驱动:

/* fsled.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>

#include <linux/slab.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>

#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/platform_device.h>

#include "fsled.h"

#define FSLED_MAJOR	256
#define FSLED_DEV_NAME	"fsled"

struct fsled_dev {
	unsigned int __iomem *con;
	unsigned int __iomem *dat;
	unsigned int pin;
	atomic_t available;
	struct cdev cdev;
};

static int fsled_open(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = container_of(inode->i_cdev, struct fsled_dev, cdev);

	filp->private_data = fsled;
	if (atomic_dec_and_test(&fsled->available))
		return 0;
	else {
		atomic_inc(&fsled->available);
		return -EBUSY;
	}
}

static int fsled_release(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = filp->private_data;

	writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);

	atomic_inc(&fsled->available);
	return 0;
}

static long fsled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct fsled_dev *fsled = filp->private_data;

	if (_IOC_TYPE(cmd) != FSLED_MAGIC)
		return -ENOTTY;

	switch (cmd) {
	case FSLED_ON:
		writel(readl(fsled->dat) | (0x1 << fsled->pin), fsled->dat);
		break;
	case FSLED_OFF:
		writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
		break;
	default:
		return -ENOTTY;
	}

	return 0;
}

static struct file_operations fsled_ops = {
	.owner = THIS_MODULE,
	.open = fsled_open,
	.release = fsled_release,
	.unlocked_ioctl = fsled_ioctl,
};

static int fsled_probe(struct platform_device *pdev)
{
	int ret;
	dev_t dev;
	struct fsled_dev *fsled;
	struct resource *res;
	unsigned int pin = *(unsigned int*)pdev->dev.platform_data;

	dev = MKDEV(FSLED_MAJOR, pdev->id);
	ret = register_chrdev_region(dev, 1, FSLED_DEV_NAME);
	if (ret)
		goto reg_err;

	fsled = kzalloc(sizeof(struct fsled_dev), GFP_KERNEL);
	if (!fsled) {
		ret = -ENOMEM;
		goto mem_err;
	}

	cdev_init(&fsled->cdev, &fsled_ops);
	fsled->cdev.owner = THIS_MODULE;
	ret = cdev_add(&fsled->cdev, dev, 1);
	if (ret)
		goto add_err;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		ret = -ENOENT;
		goto res_err;
	}

	fsled->con = ioremap(res->start, resource_size(res));
	if (!fsled->con) {
		ret = -EBUSY;
		goto map_err;
	}
	fsled->dat = fsled->con + 1;

	fsled->pin = pin;
	atomic_set(&fsled->available, 1);
	writel((readl(fsled->con) & ~(0xF  << 4 * fsled->pin)) | (0x1  << 4 * fsled->pin), fsled->con);
	writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
	platform_set_drvdata(pdev, fsled);

	return 0;

map_err:
res_err:
	cdev_del(&fsled->cdev);
add_err:
	kfree(fsled);
mem_err:
	unregister_chrdev_region(dev, 1);
reg_err:
	return ret;
}

static int fsled_remove(struct platform_device *pdev)
{
	dev_t dev;
	struct fsled_dev *fsled = platform_get_drvdata(pdev);

	dev = MKDEV(FSLED_MAJOR, pdev->id);

	iounmap(fsled->con);
	cdev_del(&fsled->cdev);
	kfree(fsled);
	unregister_chrdev_region(dev, 1);

	return 0;
}

struct platform_driver fsled_drv = { 
	.driver = { 
		.name    = "fsled",
		.owner   = THIS_MODULE,
	},  
	.probe   = fsled_probe,
	.remove  = fsled_remove,
};

module_platform_driver(fsled_drv);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver for LEDs on FS4412 board");

代码分析:
代码第152行至第159行定义了一个平台驱动fsled_drv,名字叫 fsled,和平台设备匹配。

struct platform_driver fsled_drv = { 
	.driver = { 
		.name    = "fsled",
		.owner   = THIS_MODULE,
	},  
	.probe   = fsled_probe,
	.remove  = fsled_remove,
};

代码第161行是平台驱动注册和注销的简化宏。

module_platform_driver(fsled_drv);

fsled_probe函数中,代码第86行首先通过platform_data获取了管脚号。

unsigned int pin = *(unsigned int*)pdev->dev.platform_data;

代码第88行以平台设备中的id为次设备号。

dev = MKDEV(FSLED_MAJOR, pdev->id);

代码第93行动态分配了struct fsled_dev结构对象。

fsled = kzalloc(sizeof(struct fsled_dev), GFP_KERNEL);

代码第105行使用platform_get_resource获取了I/O内存的资源,这样要操作GPIO管脚的两个信息就都获得了,一个是管脚号,一个是I/O内存地址

res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

代码第122行使用platform_set_drvdata将动态分配得到的fsled 保存到了平台设备中,便于之后的代码能从平台设备中获取 struct fsled_dev结构对象的地址,是经常会使用到的一种技巧,也是一个驱动支持多个设备的关键。

platform_set_drvdata(pdev, fsled);

函数fsled_remove中使用了platform_get_drvdata得到了对应的struct fsled_dev结构对象的地址,其他操作则是函数fsled_probe的反操作。

static int fsled_remove(struct platform_device *pdev)
{
	dev_t dev;
	struct fsled_dev *fsled = platform_get_drvdata(pdev);

	dev = MKDEV(FSLED_MAJOR, pdev->id);

	iounmap(fsled->con);
	cdev_del(&fsled->cdev);
	kfree(fsled);
	unregister_chrdev_region(dev, 1);

	return 0;
}

函数fsled_open也使用了container_of宏得到了对应的struct fsled_dev结构对象的地址。并保存在 filn->private _data中,这也是我前面读到的一个驱动支持多个设备的技巧。

static int fsled_open(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = container_of(inode->i_cdev, struct fsled_dev, cdev);

	filp->private_data = fsled;
	if (atomic_dec_and_test(&fsled->available))
		return 0;
	else {
		atomic_inc(&fsled->available);
		return -EBUSY;
	}
}

函数fsled_ioctl相比于以前则要简单一些,因为只控制一个对应的LED灯。

测试的应用代码则是分别打开了4个LED设备文件,然后再分别控制,代码比较简单,这里就不再赘述。
测试方法和前面基本一致,只是要创建4个设备文件,用到4个不同的次设备号2、3、4、5。

自动创建设备节点

前面谈到,内核中设备的添加、删除或修改都会向应用层发送热插拔事件,应用程序可以捕获这些事件来自动完成某些操作,如自动加载驱动、自动创建设备节点等。

接下来以mdev为例,来说明如何自动创建设备节点。
mdev创建设备节点有两种方法,一种是运行mdev-s命令,一种是实时捕获热插拔事件

mdev -s命令通常在根文件系统挂载完成后运行一次,它将递归扫描/sys/block目录/sys/class目录下的文件,根据文件的内容来调用make_device自动创建设备文件,这在busybox中的mdev源码中展现得非常清楚:

int mdev_main(int argc UNUSED_PARAM, char **argv)
{
.....
if(argv[1] && strcmp(argv[1],"-s")==0){
/*
* Scan: mdev -s
* /
.....
recursive_action("/sys/block",ACTION_RECURSE | ACTION_FOLLOWLINKS | ACTION_QUIET,fileAction, dirAction, temp,0);
)
recursive_action("/sys/olass",ACTION_RECURSE| ACTION_FOLLOWLINKS,fileAction, dirAction, temp, 0);

另外一种情况则是当内核发生了热插拔事件后,mdev会自动被调用,这体现在根文件系统中的/etc/init.d/rcS初始化脚本文件中。

echo/sbin/mdev > /proc/sys/kernel/hotplug

内核有一种在发生热插拔事件后调用应用程序的方式,那就是执行/proc/sys/kernel/hotplug 文件中的程序,因为这种方式比较简单,所以常用在嵌入式系统之中。而之前说的udev使用的则是netlink机制。发生热插拔事件时,调用mdev程序会将热插拔信息放在环境变量和参数当中mdev程序利用这些信息就可以自动创建设备节点,在mdev的源码中也有清晰的体现:

int mdev main(int arge UNUSED_PARAM,char **argv)
{
.....
env_devname= getenv("DEVNAME");/*can be NULL*/
G.subsystem = getenv("SUBSYSTEM");
action=getenv("ACTION");
env_devpath=getenv("DEVPATH");
......
op= index_in_strings (keywords,action);
.....
snprintf(temp, PATH_MAX, "/sys%s", env_devpath);
if (op==OP_remove){
.....
if (!fw)
make_device(env_devname, temp, op);
}
else{
make_device(env_devname, temp, op);
if(ENABLE_FEATURE_MDEV_LOAD_FIRMWARE){
if (op == OP_add && fw)
load_firmware(fw, temp);
}
}

上面的代码的总体思路是根据ACTION键的值来决定op是增加还是移除操作,最终调用make_device来自动创建或删除设备节点

了解了应用层自动创建设备节点的方式后,接下来就需要讨论在驱动中如何实现了。

既然自动设备节点的创建要依靠热插拔事件和 sysfs文件系统,那这和我们之前讨论的kobject就是分不开的,mdev扫描/sys/class目录暗示我们要创建类,并且在类下面应该有具体的设备。

为此,内核提供了相应的API:

class_create (owner, name)
void class_destroy(struet class *cls);
struct device *device_create(struct class *class,struct device *parent, dev_tdevt, void *drvdata, const char *fmt, ...);
void device_destroy(struct class *class, dev_t devt):

/* 
@ class_create:创建类,owner是所属的模块对象指针,name是类的名字,返回struct class对象指针
-返回值通过IS_ERR宏来判断是否失败,通过PTR_ERR宏来获得错误码。
@ class_destroy:销毁cls类。
@ device_create:在类class下创建设备,parent是父设备,没有则为NULL。
-devt是设备的主次设备号,drvdata是驱动数据,没有则为NULL。
-fmt是格式化字符串,使用方法类似于printk。
@ device_destroy:销毁class类下面主次设备号为devt的设备。返回值的检查方式同class_create。
*/

主要代码(例子)

添加了自动创建设备的驱动的主要代码如下:

/* fsled.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>

#include <linux/slab.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>

#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/platform_device.h>

#include "fsled.h"

#define FSLED_MAJOR	256
#define FSLED_DEV_NAME	"fsled"

struct fsled_dev {
	unsigned int __iomem *con;
	unsigned int __iomem *dat;
	unsigned int pin;
	atomic_t available;
	struct cdev cdev;
	struct device *dev;
};

struct class *fsled_cls;

static int fsled_open(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = container_of(inode->i_cdev, struct fsled_dev, cdev);

	filp->private_data = fsled;
	if (atomic_dec_and_test(&fsled->available))
		return 0;
	else {
		atomic_inc(&fsled->available);
		return -EBUSY;
	}
}

static int fsled_release(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = filp->private_data;

	writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);

	atomic_inc(&fsled->available);
	return 0;
}

static long fsled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct fsled_dev *fsled = filp->private_data;

	if (_IOC_TYPE(cmd) != FSLED_MAGIC)
		return -ENOTTY;

	switch (cmd) {
	case FSLED_ON:
		writel(readl(fsled->dat) | (0x1 << fsled->pin), fsled->dat);
		break;
	case FSLED_OFF:
		writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
		break;
	default:
		return -ENOTTY;
	}

	return 0;
}

static struct file_operations fsled_ops = {
	.owner = THIS_MODULE,
	.open = fsled_open,
	.release = fsled_release,
	.unlocked_ioctl = fsled_ioctl,
};

static int fsled_probe(struct platform_device *pdev)
{
	int ret;
	dev_t dev;
	struct fsled_dev *fsled;
	struct resource *res;
	unsigned int pin = *(unsigned int*)pdev->dev.platform_data;

	dev = MKDEV(FSLED_MAJOR, pdev->id);
	ret = register_chrdev_region(dev, 1, FSLED_DEV_NAME);
	if (ret)
		goto reg_err;

	fsled = kzalloc(sizeof(struct fsled_dev), GFP_KERNEL);
	if (!fsled) {
		ret = -ENOMEM;
		goto mem_err;
	}

	cdev_init(&fsled->cdev, &fsled_ops);
	fsled->cdev.owner = THIS_MODULE;
	ret = cdev_add(&fsled->cdev, dev, 1);
	if (ret)
		goto add_err;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		ret = -ENOENT;
		goto res_err;
	}

	fsled->con = ioremap(res->start, resource_size(res));
	if (!fsled->con) {
		ret = -EBUSY;
		goto map_err;
	}
	fsled->dat = fsled->con + 1;

	fsled->pin = pin;
	atomic_set(&fsled->available, 1);
	writel((readl(fsled->con) & ~(0xF  << 4 * fsled->pin)) | (0x1  << 4 * fsled->pin), fsled->con);
	writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
	platform_set_drvdata(pdev, fsled);

	fsled->dev = device_create(fsled_cls, NULL, dev, NULL, "led%d", pdev->id);
	if (IS_ERR(fsled->dev)) {
		ret = PTR_ERR(fsled->dev);
		goto dev_err;
	}

	return 0;

dev_err:
	iounmap(fsled->con);
map_err:
res_err:
	cdev_del(&fsled->cdev);
add_err:
	kfree(fsled);
mem_err:
	unregister_chrdev_region(dev, 1);
reg_err:
	return ret;
}

static int fsled_remove(struct platform_device *pdev)
{
	dev_t dev;
	struct fsled_dev *fsled = platform_get_drvdata(pdev);

	dev = MKDEV(FSLED_MAJOR, pdev->id);

	device_destroy(fsled_cls, dev);
	iounmap(fsled->con);
	cdev_del(&fsled->cdev);
	kfree(fsled);
	unregister_chrdev_region(dev, 1);

	return 0;
}

struct platform_driver fsled_drv = { 
	.driver = { 
		.name    = "fsled",
		.owner   = THIS_MODULE,
	},  
	.probe   = fsled_probe,
	.remove  = fsled_remove,
};

static int __init fsled_init(void)
{
	int ret;

	fsled_cls = class_create(THIS_MODULE, "fsled");
	if (IS_ERR(fsled_cls))
		return PTR_ERR(fsled_cls);

	ret = platform_driver_register(&fsled_drv);
	if (ret)
		class_destroy(fsled_cls);

	return ret;
}

static void __exit fsled_exit(void)
{
	platform_driver_unregister(&fsled_drv);
	class_destroy(fsled_cls);
}

module_init(fsled_init);
module_exit(fsled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver for LEDs on FS4412 board");

代码第177行使用class_create创建了名叫fsled的类。

fsled_cls = class_create(THIS_MODULE, "fsled");

代码第127行使用device_createfsled类下面创建了led%d的设备,%d 用平台设备的id来替代

在创建过程中,内核会发送热插拔事件给mdev,mdev利用这些信息就可以创建设备节点,因为设备的名字和设备号都传递给了device_create,而内核又会利用这些参数生成热插拔信息

static int fsled_probe(struct platform_device *pdev)
{
	fsled->dev = device_create(fsled_cls, NULL, dev, NULL, "led%d", pdev->id);
	if (IS_ERR(fsled->dev)) {
		ret = PTR_ERR(fsled->dev);
		goto dev_err;
	}

	return 0;

使用上面的驱动且驱动加载成功后,设备节点就自动被创建了,不需要再手动创建,整个测试过程和前面的例子类似,这里就不再重复了。

相关文章