嵌入式Linux之设备驱动程序
本文档以一个简单的字符设备——LED驱动设备为例,阐述Linux系统下设备驱动程序的基本原理以及设备驱动程序的编程方法。
1. 设备驱动简介
1.1 linux设备驱动分类
在Linux操作系统中,一个核心的思想就是“一切皆文件”。对于驱动设备来讲,也是将其作为文件,通过open/close/read/write/cioctl(字符设备相关操作除外)进行相应的控制。在Linux系统中,设备驱动程序可以分为三类:
字符设备:
字符设备是能够像字节流一样被访问的设备,对字符设备发出的读写请求,相应的IO操作立即发生。Linux系统中很多设备都是字符设备,例如串口,键盘,鼠标等。
块设备:
在Linux系统中进行IO以块为单位的设备被称作块设备,块设备能够安装文件系统。块设备能够安装文件系统。块设备会利用一块系统内存作为缓冲区,因此对块设备的访问并不一定会立即产生IO操作,Linux环境下常见的块设备有硬盘,软驱等。
网络设备:
网络设备既可以是网卡这样的硬件设备,也可是纯软件的设备如回环设备。网络设备由Linux的网络子系统驱动,负责数据包的发送和接收,而不是面向流设备,因此在Linux系统下网络设备并没有节点。对网络设备的访问是通过socket产生的,而不是通过文件操作如read/write产生。
1.2 设备号和设备节点
Linux系统中的设备都被当做文件来处理,被称作设备文件或者设备节点,所有的设备文件都放在系统/dev目录下,如;
#ls /dev
consol null tty ttyS0 zero ……
在看一下设备文件的详细信息:
ls –al /dev/ttyS0
crw-rw—- 1 root root 4.64 2009-03-27 09:27 /dev/ttyS0
块设备与普通文件不同,信息的第一个字符c表示这个设备是字符型设备,后面的编号4表示该设备的主设备号,64表示该设备的从设备号。主设备号用于标示设备的驱动程序,从设备号表示该设备文件所指定的设备。
1.3 file_operations结构
Linux操作系统采用文件操作的方式对设备进行管理。在用户级层面上,这些调用函数通常是open/close/write/read之类。而在设备驱动程序中,驱动对底层硬件的操作也是通过类似的读写操作实现的,这些函数通常命名为xxx_open,xxx_close,xxx_write等等,xxx一般表示设备名。这样的一组函数通常组织在结构体file_operations中,通过它把系统调用和驱动程序关联起来。注意,file_operations结构体成员函数属于内核空间。用户程序空间并不能直接调用这些函数。整个系统驱动程序调用关系如下图所示:
2. Linux驱动框架
linux2.6引入了platfrom_device的概念,在驱动的注册和管理上带来了变化,使用platform_device描述设备,通过platform_driver描述设备,注册和销毁驱动都有一系列接口函数。
platform_device是在系统中以独立实体出现的设备,包括传统的基于端口的设备,主机到外设的总线以及大部分片内集成的控制器等。这些设备的共同点是CPU可以通过总线直接对它们进行访问。
描述platform_device的结构体定义如下,是对传统设备的device的封装:
struct flatfrom_device{
const char *name;
u32 id;
struct device dev;
u32 num_resource;
struct resource *resource;
}
可以看出该结构体内包含的信息,包括:设备名,设备id等等。
platform_driver是对device_driver的封装,提供了驱动probe和remove的方法,也提供了与电源管理相关的shutdown和suspend的方法。
struct platform_driver{
int (*probe) (struct flatfrom_device *);
int (*remove)( struct flatfrom_device *);
void (*shutdown)( struct flatfrom_device *);
int (*suspend)( struct flatfrom_device*);
……
}
在定义过上述struct flatfrom_device之后,platform_driver结构体主要完成对device的各种操作。
3. LED驱动编程实例
在明白了Linux驱动程序的工作原理及主要数据结构之后,以一个简单的字符设备,LED驱动设备为例,阐述Linux环境下设备编程的思路和流程。
整体上来讲,驱动程序编程分为两大步:
第一步,主要是透过上面提到的flatfrom_device和platform_driver数据结构来实现的,
主要分为以下四部:定义flatfrom_device,注册flatfrom_device,定义platform_driver,注册platform_driver。
第二步,对设备相关操作的定义,就需要根据设备类型以及需要对设备进行的操作,编写相对应的操作函数。
3.1 设备的申请和注册
根据上面的思路,首先需要进行的是设备的申请和注册,主要是借助上面提到的两个结构体进行的。
static struct miscdevice led_miscdev =
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEV_NAME,
.fops = &led_fops,
};
struct platform_device *led_device;
static struct device_driver led_driver = {
.name = DEV_NAME,
.owner = THIS_MODULE,
.bus = &platform_bus_type,
.probe = led_probe,
.remove = led_remove,
};
3.2 设备操作函数的实现
对于一个LED驱动程序来讲,我们对其需要的功能应该包括打开,关闭,控制,因此需要有open,close,write,以及ioctl函数,至于read函数则不需要。因而file_operations中的函数应该包括:
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
.ioctl = led_ioctl,
};
填充了file_operations结构体之后,接下来要进行的就是针对该结构体内的函数进行相应的编程,这里仅仅led_open和led_write函数列写如下。
static int led_open(struct inode *inode, struct file *filp)
{
__raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE));/
try_module_get(THIS_MODULE);
printk( KERN_INFO DEV_NAME ” opened!/n”);
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buff, size_t count, loff_t *ppos)
{
int i;
unsigned char ctrl=0;
if (count > 1) {
return -EFBIG;
}
if (down_interruptible(&led_sem))
return -ERESTARTSYS;
get_user(ctrl, (u8 *)buff);
i = (ctrl-0x30)&0x03;
if(i==0) {
__raw_writel(_BIT(5), GPIO_P3_OUTP_CLR(GPIO_IOBASE));
} else {
__raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE));
}
up(&led_sem);
return count;
}
完成上面的4个函数之后,我们就可以填充led_fops这个结构体了,下面就是为了应和platform结构体系,所以我们还要编写另外的两个函数probe和remove,实现如下:
static int led_probe(struct device *dev)
{
int ret;
printk(KERN_INFO DEV_NAME ” probing…/n”);
ret = misc_register(&led_miscdev);
if (ret)
printk(KERN_ERR “Failed to register miscdev./n”);
return ret;
}
static int led_remove(struct device *dev)
{
misc_deregister(&led_miscdev);
printk(KERN_INFO DEV_NAME ” removed!/n”);
return 0;
}
驱动都是以模块的形式来实现的,所以就要有模块的入口和出口,就是init和exit。
static int __init led_init(void)
{
int rc = 0;
printk(KERN_INFO DEV_NAME ” init…/n”);
__raw_writel(_BIT(5), GPIO_P3_MUX_CLR(GPIO_IOBASE));
__raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE));
led_device = platform_device_register_simple(DEV_NAME, -1, NULL, 0);
if(IS_ERR(led_device)) {
goto out;
}
rc = driver_register(&led_driver);
if (rc < 0) {
platform_device_unregister(led_device);
}
sema_init(&led_sem, 1);
out:
return rc;
}
编译之后,在相应的文件夹中生成leddrv.ko文件,即为LED驱动模块。
4. 验证
驱动模块加载如内核之后,需要用户层的应用程序调用才能得到验证。应用层程序如下所示:
#include
#include
#include
#include
#include
#include
#include “../leddrv.h”
#define DEV_NAME “/dev/led”
int main(int argc, char *argv[])
{
int fd;
int dat=0;
int i;
fd=open(DEV_NAME, O_RDWR);
if(fd<0) {
perror(“can not open device”);
exit(1);
}
printf(“Test write method……/n”);
for (i=0; i<3; i++) {
dat = 0;
write(fd, &dat, 1);
usleep(300000);
dat = 1;
write(fd, &dat, 1);
usleep(300000);
}
printf(“/nTest write method OK!/n”);
printf(“Test ioctl method start……/n”);
for (i=0; i<3; i++) {
ioctl(fd, SET_LED_ON, NULL);
usleep(300000);
ioctl(fd, SET_LED_OFF, NULL);
usleep(300000);
}
printf(“/n”);
printf(“/nTest ioctl method OK!/n”);
close(fd);
return 0;
}
编译之后,生成可执行文件main,在验证时,首先要动态加载LED驱动模块,然后运行该用户程序。
insmod leddrv.ko
./main
可以看到开发板上LED灯连续闪烁六次,证明LED驱动程序正确且加载成功。