本文引用自《精通Linux设备驱动程序开发》Sreekrishnan Venkateswaran 宋宝华[等]译 北京:人民邮电出版社,2010.06
为了学习parport提供的服务,让我们编写一个简单的驱动程序。考虑一个有8个发光二极管(LED),提供和标准25针并行端口接口的电路板。因为PC上的8位并行端口数据寄存器直接映射到并行端口的2~9针,所以这些针脚和电路板上的LED连通。向并行端口数据寄存器写数据可以控制这些针脚的电平,进而控制LED的开关。如下代码为一个字符设备驱动程序,它通过系统并行端口和此电路板通信。代码内的注释解释了其中所使用的parport服务例程。
并行端口LED电路板驱动程序(led.c)
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/parport.h>
#include<asm/uaccess.h>
#include<linux/platform_device.h>
#define DEVICE_NAME "led"
static dev_t dev_number; /*Allotted device number */
static struct class *led_class; /* Class to which this device belongs */
struct cdev led_cdev; /* Associated cdev */
struct pardevice *pdev /* Parallel port device */
/*LED open */
int led_open(struct inode *inode, struct file *file)
{
return 0;
}
/* Write to the LED */
ssize_t led_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
char kbuf;
if (copy_from_user(&kbuf, buf, 1)) return -EFAULT;
/* Write to the device */
parport_write_data(pdev->port, kbuf);
/* Release the port */
parport_release(pdev);
return count;
}
/* Release the device */
int led_release(struct inode *inode, struct file *file)
{
return 0;
}
/* file Operations */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.release = led_release,
};
static int led_preempt(void *handle)
{
return 1;
}
/* Parport attach method */
static void led_attach(struct parport *port)
{
/* Register the paralled LED device with parport */
pdev = parport_register_device(port, DEVICE_NAME, led_preempt, NULL, NULL, 0, NULL);
if (pdev == NULL) printk ("Bad register\n");
}
/* Parport detach method */
static void led_detach(struct parport *port)
{
/* Do nothing */
}
/* Parport driver operations */
static struct parport_driver led_driver = {
.name = "led",
.attach = led_attach,
.detach = led_detach,
};
/* Driver Initialization */
/*
新的设备模型将驱动程序和设备区分开来。led_init()通过parport_register_driver()调用项parport注册LED驱动程序。当内核在led_attach()中查找到LED板时,它调用parport_register_device()注册设备
*/
int __init led_init(void)
{
/* Request dynamic allocatino of a device major number */
if (alloc_chrdev_region(&dev_number, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_DEUG "Can't register device\n");
return -1;
}
/* create the led class */
led_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(led_class)) printk("Bad class create\n");
/* Connect the file operation with the cdev */
cdev_init(&led_cdev, &led_fops);
led_cdev.owner = THIS_MODULE;
/* Connect the major/minor number to the cdev */
if (cdev_add(&led_cdev, dev_number, 1)) {
printk("Bad cdev add\n");
return 1;
}
class_device_create(led_class, NULL, dev_number, NULL, DEVICE_NAME);
/*
该函数创建设备节点/dev/led,可以用此设备节点控制每个LED的状态
编译并将驱动程序模块加入到内核中:
bash> make -C /path/to/kerneltree/ M=$PWD modules
PS:也许你不一定能成功,因为Linux Kernel 2.6要求编译模块之前,必须先在内核源代码目录下执行make,换言之,必须先配置过内核,执行过make,然后才能make自己的内核。(仔细想想你没有配置过内核,内核怎么知道该部分是编译成模块还是编译进内核的呢)
bash> insmod ./led.ko
LED Driver Initialized
PS:早期的版本使用class_create()和class_device_create()这两个函数创建设备节点,到了
2.6.29内核以后,使用的函数则变成了class_create()和device_create(),并且要在声明中加入#include <linux/device.h>,不过使用这两个函数的前提是用户空间已经移植了udev。
内核中定义了struct class结构体,一个struct class结构体类型变量对应一个类(有待商榷), 内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于/sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应 device_create(…)函数,去/sysfs下寻找对应的类从而创建设备节点。
*/
/* Register this driver with parport */
if (parport_register_driver(&led_driver)) {
printk(KERN_ERR "Bad Parport Register\n");
return -EIO;
}
printk("LED Driver Initialized. \n");
return 0;
}
/*Driver Exit */
void __exit led_cleanup(void)
{
unregister_chrdev_region(dev_number, 1);
class_device_destroy(led_class, dev_number);
class_destroy(led_class);
return ;
}
module_init(led_init);
module_exit(led_cleanup);
MODULE_LICENSE("GPL");
为了有选择地驱动一些并行端口针脚,点亮相应的LED,将相应的值赋给/dev/led:
bash> echo 1 > /dev/led
因为1的ASCII值是31(00110001),第1,5和6个LED将会发亮。
前述命令触发led_write()调用。此驱动程序方法首先通过copy_from_user()将用户内存数据(在本例中为31)复制到内核缓冲区。然后占用并行端口,写入数据,释放端口,所有这些都使用parport接口。
相比于/dev,sysfs是更好的控制设备状态的地方。因此将LED控制委托给sysfs效果更佳。下面代码为此种驱动程序的实现代码,其中哦功能sysfs操作代码也可以作为模版用到其它的设备控制中去
使用sysfs控制并行端口LED电路板:
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/parport.h>
#include<asm/uaccess.h>
#include<linux/pci.h>
static dev_t dev_number; /* Allotted Device Number */
static struct class *led_class; /* Class Device Model */
struct cdev led_cdev; /* Character dev struct*/
struct pardevice *pdev; /*Parallel Port device */
struct kobject kobj; /* Sysfs directory object */
/* sysfs attribute of the leds */
struct led_attr {
struct attribute attr;
ssize_t (*show)(char *);
ssize_t (*store)(const char *, size_t cont);
};
#define glow_show_led(number) \
static ssize_t glow_led_##number(const char *buffer, size_t count) \
{ \
unsigned char buf; \
int value; \
\
sscanf(buffer, "%d", &value); \
\
parport_claim_or_block(pdev); \
buf = parport_read_data(pdev->port); \
if (value) { \
parport_write_data(pdev->port, buf | (1<<number)); \
} else { \
parport_write_data(pdev->port, buf & ~(1<<number)); \
} \
parport_release(pdev); \
return count; \
} \
\
static ssize_t \
show_led_##number(char *buffer) \
{ \
unsigned char buf; \
\
parport_claim_or_block(pdev); \
\
buf = parport_read_data(pdev->port); \
parport_release(pdev); \
\
if (buf & (1 << number)) { \
return sprintf(buffer, "ON\n"); \
} else { \
return sprintf(buffer, "OFF\n"); \
} \
} \
static struct led_attr led##number = \
__ATTR(led##number, 0644, show_led_##number, glow_led_##number);
glow_show_led(0); glow_show_led(1); glow_show_led(2);
glow_show_led(3); glow_show_led(4); glow_show_led(5);
glow_show_led(6); glow_show_led(7);
/*
glow_show_led()使用了内核源代码中经常使用的技术,以便简洁地定义几个类似的函数。定义的read()和write()方法(在sysfs中用术语show()和store()表示)同8个/sys文件相关,电路板上每个LED对应一个文件。因此glow_show_led(0)将glow_led_0()和show_led_0()与第一个LED对应一个文件。这些函数分别负责点亮/熄灭第一个LED,并读取其状态。##在宏定义中用于把字符串连接在一起,因此当编译器处理语句glow_show_led(0)时,glow_led_##number就变成glow_led_0()。
*/
#define DEVICE_NAME "led"
static int led_preempt (void * handle)
{
return 1;
}
/* Parport attach method */
static void led_attach (struct parport *port)
{
pdev = parport_register_device(port, DEVICE_NAME, led_preempt, NULL, NULL, 0, NULL);
}
/* Parent sysfs show() method. Calls the show() method corresponding to the individual sysfs file */
static ssize_t l_show(struct kobject *kobj, struct attribute *a, char *buf)
{
int ret;
struct led_attr *lattr = container_of(a, struct led_attr, attr);
ret = lattr->show ? lattr->show(buf) : -EIO;
return ret;
}
/* Sysfs store() method. Calls the store()method corresponding to the individual sysfs file */
static ssize_t l_store(struct kobject *kobj, struct attribute 8a, const char *buf, size_t count)
{
int ret;
struct led_attr *lattr = container_of(a, struct led_attr, attr);
ret = lattr->store ? lattr->store(buf, count) : -EIO;
return ret;
}
/* Sysfs operations structure */
static struct sysfs_ops sysfs_ops = {
.show = l_show,
.store = l_store,
};
/* Attributes of the /sys/class/pardevice/led/control/ kobject.
Each file in this directory corresponds to one LED. Control
each LED by writing or reading the associated sysfs file */
static struct attribute *led_atrrs[] = {
&led0.attr,
&led1.attr,
&led2.attr,
&led3.attr,
&led4.attr,
&led5.attr,
&led6.attr,
&led7.attr,
NULL
};
/* This describes the kobject. The kobject has 8 files, one corresponding to each LED. This representation is called the ktype of the kobject */
static struct kobj_type ktype_led = {
.sysfs_ops = &sysfs_ops,
.default_attrs = led_attrs,
};
/* Parport methods. We don't have a detach method */
static struct parport_driver led_driver = {
.name = "led",
.attach = led_atach,
};
/* Driver Initialization */
int __init led_init(void)
{
struct class_device *c_d;
if calloc_chrdev_region(&dev_number, 01, DEVICE_NAME <0) {
printk(KERN_DEBUG "can't register device \n");
return -1;
}
/* Create the pardevice class - /sys/class/pardevice */
led_class = class_create(THIS_MODULE, "pardevice");
if (IS_ERR(led_class)) printk("Bad class create\n");
/* Create the led class device - /sys/class/pardevice/led/ */
c_d = class_device_create(led_class, NULL, dev_number, NULL, DEVICE_NAME);
/* Register this driver with parport */
if (parport_register_driver(&led_driver)) {
printk(KERN_ERR "Bad parport Register \n");
return -EIO;
}
/* Instantiate a kobject to control each LED on the board */
/* Parent is /sys/classpardevice/led */
kobj.parent = &c_d->kobj;
/* the sysfs file corresponding to kboj is /sys/class/pardevice/led/control/ */
strlcpy(kobj.name, "control", KOBJ_NAME_LED);
/* Description of the kobject. Specifies the list of attribute files in /sys/class/pardevice/led/control/ */
kobj.ktype = &ktype_led;
/*
ktype描述了kobject。ktype_led结构描述了“控制”kobject,它包含指向属性数组的指针led_attrs[]。led_attrs[]数组包含每个LED的设备属性的地址。每个LED的属性通过下列语句连接在一起:
static struct led_attr led##number =
__ATTR(led##number, 0644, show_led_##number, glow_led_##number);
其结果是为每个LED产生一个控制文件/sys/class/pardevice/led/control/ledX,其中X是LED序号。为了改变ledX的状态,将1(或者0)回送给相应的控制文件。如为了点亮第一个LED,可做如下操作:
bash> echo 1 > /sys/class/pardevice/led/control/led0
在模块退出期间,驱动程序使用kobject_unregister(),class_device_destroy()和class_destroy()移除kobject和class。
*/
/* Register the kobject */
kobject_register(&kobj);
/*
基于sysfs版本的驱动程序使用了kobject用于代表“控制”抽象,它模拟了一个软件按钮控制LED。sysfs下的每个目录名代表一个kobject,因此代码清单中的kobject_register()创建了/sys/class/pardevice/led/control/目录
*/
printk("LED Driver Initialized. \n");
return 0;
}
/* Driver Exit */
void led_cleanup(void)
{
/* unregister kobject corresponding to /sys/class/pardevice/led/control */
kobject_unregister(&kobj);
/* Destroy class device corresponding to /sys/class/pardevice/led/ */
class_device_destroy(led_class, dev_number);
/* Destroy /sys/class/pardevice */
class_destroy(led_class);
return;
}
module_init(led_init);
module_exit(led_cleanup);
MODULE_LICENSE("GPL");
编写字符驱动程序不再像2.4内核中那样简单了。在上下文中,为了开发简单的LED驱动彻骨女婿,我们使用了6个数据抽象:cdev,sysfs,kobject,class,class device 和parport。当然,这些数据抽象也有优点,如编译模块无bug,代码可重用,设计流程严谨。