本文是对这篇文章的补充:从零开始学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_create
在fsled类
下面创建了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;
使用上面的驱动且驱动加载成功后,设备节点就自动被创建了,不需要再手动创建,整个测试过程和前面的例子类似,这里就不再重复了。