linux驱动编写(虚拟字符设备编写)
昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备的编写内容,今天我们就可以了解一下相关方面的内容,并且用一个实例来说明在linux上面设备是如何编写的。
因为我们是在pc linux上学习驱动的,因此暂时没有真实的外接设备可以使用,但是这丝毫不影响我们学习的热情。通过定时器、进程,我们可以仿真出真实设备的各种需求,所以对于系统来说,它是无所谓真设备、假设备的,基本的处理流程对它来说都是一样的。只要大家一步一步做下去,肯定可以了解linux驱动设备的开发工程的。
下面,为了说明问题,我们可以编写一段简单的char设备驱动代码,文件名为char.c,
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static struct cdev chr_dev;
static dev_t ndev;
static int chr_open(struct inode* nd, struct file* filp)
{
int major ;
int minor;
major = MAJOR(nd->i_rdev);
minor = MINOR(nd->i_rdev);
printk("chr_open, major = %d, minor = %d\n", major, minor);
return 0;
}
static ssize_t chr_read(struct file* filp, char __user* u, size_t sz, loff_t* off)
{
printk("chr_read process!\n");
return 0;
}
struct file_operations chr_ops = {
.owner = THIS_MODULE,
.open = chr_open,
.read = chr_read
};
static int demo_init(void)
{
int ret;
cdev_init(&chr_dev, &chr_ops);
ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");
if(ret < 0 )
{
return ret;
}
printk("demo_init(): major = %d, minor = %d\n", MAJOR(ndev), MINOR(ndev));
ret = cdev_add(&chr_dev, ndev, 1);
if(ret < 0)
{
return ret;
}
return 0;
}
static void demo_exit(void)
{
printk("demo_exit process!\n");
cdev_del(&chr_dev);
unregister_chrdev_region(ndev, 1);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("
[email protected]");
MODULE_DESCRIPTION("A simple device example!");
在module_init中的函数是模块加载时处理的函数,而模块卸载的函数则是在module_exit中。每一个设备都要对应一个基本的设备数据,当然为了使得这个设备注册在整个系统当中,我们还需要分配一个设备节点,alloc_chrdev_region就完成这样一个功能。等到cdev_add的时候,整个设备注册的过程就全部完成了,就是这么简单。当然为了编写这个文件,我们还需要编写一个Makefile文件,
[cpp] view plain copy
ifneq ($(KERNELRELEASE),)
obj-m := char.o
else
PWD := $(shell pwd)
KVER := $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions modules.* Module.*
endif
这个Makefile文件和我们之前编写的makefile基本上没有区别,唯一的区别就是文件名称改成了char.o,仅此而已。为了编写模块,我们直接输入make即可。这时候,char.ko文件就可以生成了。然后,模块需要被注册在系统当中,insmod char.ko是少不了的。如果此时,我们还不确信是否模块已经加入到系统当中,完全可以通过输入lsmod | grep char进行查找和验证。为了创建设备节点,我们需要知道设备为我们创建的major、minor数值是多少,所以dmesg | tail 查找一下数值。在我hp的机器上,这两个数值分别是249和0,所以下面可以利用它们直接创建设备节点了,输入mknod /dev/chr_dev c 249 0即可,此时可以输入ls /dev/chr_dev验证一下。那么,按照这种方法,真的可以访问这个虚拟设备了吗,我们可以编写一段简单的代码验证一下,
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define CHAR_DEV_NAME "/dev/chr_dev"
int main()
{
int ret;
int fd;
char buf[32];
fd = open(CHAR_DEV_NAME, O_RDONLY | O_NDELAY);
if(fd < 0)
{
printf("open failed!\n");
return -1;
}
read(fd, buf, 32);
close(fd);
return 0;
}
代码的内容非常简单,就是利用CHAR_DEV_NAME直接打开设备,读写设备。当然。首先还是需要对这个文件进行编译,文件名为test.c,输入gcc test.c -o test,其次就是运行这个文件,直接输入./test即可。如果没有问题的话,那么说明我们的代码是ok的,但是我们还是没有看到任何内容。没关系,我们还是通过dmesg这个命令查看内核中是否存在相关的打印内容,直接输入dmesg | tail即可。此时如果没有意外的话,我们就可以看到之前在chr_open和chr_read中留下的printk打印,这说明我们的代码完全是ok的。
上面的代码只是一段小例子,真实的内容要比这复杂一下。不过既然我们都已经入门了,那么后面的内容其实也没有什么好怕的了。最后有两个事情补充一下:(1)如果大家在创建节点后想删除设备节点,直接rm -rf /dev/chr_dev即可;(2)上面这段代码的原型来自于《深入linux设备驱动程序内核机制》这本书,稍作修改,如果大家对内核机制的内容感兴趣,可以参考这本书的内容。
========
linux驱动编写(字符设备编写框架)
上次我们编写了一个简单的字符设备,但是涉及的内容比较少,只有open和read两个函数。今天,我们打算在此基础上扩充一下内容。基本的思路是这样的:(1)编写字符设备下需要处理的各个函数,包括open、release、read、write、ioctl、lseek函数;(2)编写一个用户侧的程序来验证我们编写的驱动函数是否正确。当然,我们编写的代码部分参考了宋宝华先生的《linux设备驱动开发详解》一书,在此说明一下。
在开始今天的内容之前,其实有一些题外话可以和大家分享一下。自从工作以来,我个人一直都有一个观点。那就怎么样利用简单的代码来说明开发中的问题,或者是解释软件中的原理,这是一个很高的学问。有些道理看上去云里雾里说不清楚,其实都可以通过编写代码来验证的。os可以、cpu可以、cache可以、编译器可以、网络协议也可以,很多很多的内容完全可以通过几行代码就可以表达得非常清楚,但是事实上我们并没有这么做。我想原因无非是这么几条,一来授业者对相关知识的学习也是停留在概念上而已,二来我们的学习过于死板和教条、太关注知识、不求实践,三就是学习者自身缺少思考的能力、缺少自我反省的能力、对很多东西不求甚解。对于简单的linux设备,我们完全可以通过这几行代码说清楚问题,免得大家还要苦苦追寻,百思而不得入门。
好了,说了这么多,我们看看现在的驱动代码是怎么修改的把。
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define CHRMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
static int chr_major;
struct chr_dev
{
struct cdev cdev;
unsigned char mem[CHRMEM_SIZE];
};
struct chr_dev* char_devp;
int chr_open(struct inode* inode, struct file* filp)
{
filp->private_data = char_devp;
return 0;
}
int chr_release(struct inode* inode, struct file* filp)
{
return 0;
}
static int chr_ioctl(struct inode* inode, struct file* filp, unsigned int cmd, unsigned long arg)
{
struct chr_dev* dev = filp->private_data;
switch(cmd)
{
case MEM_CLEAR:
memset(dev->mem, 0, CHRMEM_SIZE);
break;
default:
return -EINVAL;
}
return 0;
}
static ssize_t chr_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct chr_dev* dev = filp->private_data;
if(p >= CHRMEM_SIZE)
{
return 0;
}
if(count > CHRMEM_SIZE - p)
{
return 0;
}
if(copy_to_user(buf, (void*)(dev->mem + p), count))
{
return -EINVAL;
}
else
{
*ppos += count;
ret = count;
}
return ret;
}
static ssize_t chr_write(struct file* filp, const char __user* buf, ssize_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct chr_dev* dev = filp->private_data;
if(p >= CHRMEM_SIZE)
{
return 0;
}
if(count > CHRMEM_SIZE - p)
{
count = CHRMEM_SIZE - p;
}
if(copy_from_user(dev->mem + p, buf, count))
{
ret = -EINVAL;
}
else
{
*ppos += count;
ret = count;
}
return ret;
}
static loff_t chr_llseek(struct file* filp, loff_t offset, int orig)
{
loff_t ret = 0;
/* orig can be SEEK_SET, SEEK_CUR, SEEK_END */
switch(orig)
{
case 0:
if(offset < 0)
{
ret = -EINVAL;
break;
}
if((unsigned int) offset > CHRMEM_SIZE)
{
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int) offset;
ret = filp->f_pos;
break;
case 1:
if((filp->f_pos + offset) > CHRMEM_SIZE)
{
ret = -EINVAL;
break;
}
if((filp->f_pos + offset) < 0)
{
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
break;
}
return ret;
}
static const struct file_operations chr_ops =
{
.owner = THIS_MODULE,
.llseek = chr_llseek,
.read = chr_read,
.write = chr_write,
.ioctl = chr_ioctl,
.open = chr_open,
.release = chr_release
};
static void chr_setup_cdev(struct chr_dev* dev, int index)
{
int err;
int devno = MKDEV(chr_major, index);
cdev_init(&dev->cdev, &chr_ops);
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if(err)
{
printk(KERN_NOTICE "Error happend!\n");
}
}
int chr_init(void)
{
int result;
dev_t ndev;
result = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");
if(result < 0 )
{
return result;
}
printk("chr_init(): major = %d, minor = %d\n", MAJOR(ndev), MINOR(ndev));
chr_major = MAJOR(ndev);
char_devp = kmalloc(sizeof(struct chr_dev), GFP_KERNEL);
if(!char_devp)
{
result = -ENOMEM;
goto final;
}
memset(char_devp, 0, sizeof(struct chr_dev));
chr_setup_cdev(char_devp, 0);
return 0;
final:
unregister_chrdev_region(ndev, 1);
return 0;
}
void chr_exit()
{
cdev_del(&char_devp->cdev);
kfree(char_devp);
unregister_chrdev_region(MKDEV(chr_major, 0), 1);
}
module_init(chr_init);
module_exit(chr_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("feixiaoxing!163.com");
MODULE_DESCRIPTION("A simple device example!");
不可否认,我们的代码出现了更多的内容,但是基本框架还是一致的。要是说区别,无非就是我们在原来的基础上添加了新的处理函数而已。说起来,我们对于设备的主要操作也就是这么几种,大家如果对此的概念已经非常成熟了,那么后面的学习就会轻松很多。当然和之前的驱动一样,我们也需要make & insmod char.ko & mknod /dev/chr_dev c 249 0。接下来,为了验证上述的内容是否正确,编写一段简单的测试代码是必不可少的。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define MEM_CLEAR 0x01
#define CHAR_DEV_NAME "/dev/chr_dev"
int main()
{
int ret;
int fd;
int index;
char buf[32];
/* open device */
fd = open(CHAR_DEV_NAME, O_RDWR | O_NONBLOCK);
if(fd < 0)
{
printf("open failed!\n");
return -1;
}
/* set buffer data, which will be stored into device */
for(index = 0; index < 32; index ++)
{
buf[index] = index;
}
/* write data */
write(fd, buf, 32);
memset(buf, 0, 32);
/* read data */
lseek(fd, 0, SEEK_SET);
read(fd, buf, 32);
for(index = 0; index < 32; index ++)
{
printf("data[%d] = %d\n", index, buf[index]);
}
/* reset all data to zero, read it and check whether it is ok */
ioctl(fd, MEM_CLEAR, NULL);
lseek(fd, 0, SEEK_SET);
read(fd, buf, 32);
for(index = 0; index < 32; index ++)
{
printf("data[%d] = %d\n", index, buf[index]);
}
close(fd);
return 0;
}
细心的朋友可能发现了,我们在用户侧代码中使用了很多的处理函数,基本上从open、release、read、write、lseek、ioctl全部包括了。测试代码处理的流程也非常简单,首先打开设备,接着写数据,后面就是读取数据,最后利用ioctl清除数据,程序返回。因为代码中包含了注释的内容,在此我们就不过多赘述了。大家慢慢看代码,应该都会了解和明白的。注意,用户测的代码也要在sudo模式下执行。
========
Linux字符设备驱动入门
先亮一下装备:
平台:VMware 7.0 + Linux ubuntu 3.0.0-12-generic
编译器:gcc
参考资料:LDD 3
功能:实现简单的字符操作(从用户空间向内核空间写入一串字符;从内核空间读一个字符到内核空间)
众所周知,字符设备是linux下最基本,也是最常用到的设备,它是学习Linux驱动入门最好的选择,计算机的东西很多都是相通的,掌握了其中一块,其他就可以触类旁通了。在写驱动前,必须先搞清楚字符设备的框架大概是怎样的,弄清楚了流程,才开始动手,不要一开始就动手写代码!
这里所说的框架是参考LLD3上介绍的,内核是基于Linux 2.6,3.0以上的有些地方会不一样(主要是file_operations中的ioctl修改了),但基本上适用,因为我就是在3.0的内核上实现的!字符设备驱动的初始化流程大概如下所示:
定义相关的设备文件结构体(如file_operation()中的相关成员函数的定义)->向内核申请主设备号(建议采用动态方式) ->申请成功后,调用MAJOR()获取主设备号 ->初始化cdev的结构体,调用cdev_init() ->调用cdev_add(),注册cdev到kernel ->注册设备模块:module_init()、module_exit()。
======================================================================================================
编写代码
======================================================================================================
首先定义两个全局变量(主设备号和字符设备hellow):
static int hello_major = 0; /* major device number */
static struct cdev hellow; /* hello device structure */
然后来看看file_operations(),它的定义可以在../include/linux/fs.h下找到,这里只用到了其中的几个成员函数:
/* file operations for hello device */
static struct file_operations hello_ops = {
.owner = THIS_MODULE, /*owner为所有者字段,防止在使用时模块被卸载。一边都设为THIS_MODULE*/
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
};
不同于windows驱动程序,Linux设备驱动程序在与硬件设备之间建立了标准的抽象接口。通过这个接口,用户可以像处理普通文件一样,通过open,close,read,write等系统调用对设备进行操作,如此一来也大大简化了linux驱动程序的开发。通过file_operations这个结构体(实际上是一个函数指针的集合),把驱动的操作和设备号联系起来,程序员所要做的工作只是通过file_operations挂接自己的系统调用函数。
接下来就是实现open,close,read,write操作了,这个驱动什么都没干,所以很好理解,用户请求read系统调用时,这个虚拟设备反回相应长度的“A”字符串,用户write时,将内容显示到日志中。这里要注意的是,内核空间中不能使用用户态的malloc,而是使用kmalloc/kfree。而且,用户read/write提供的buf地址也是用户态的,内核自然不能直接访问,需要通过copy_to_user/copy_from_user 进行数据拷贝,具体如下:
/* Open the device */
static int hello_open( struct inode *inode, struct file *filp ){
printk( KERN_NOTICE"Hello device open!\n" );
return 0;
}
/* Close hello_device */
static int hello_release( struct inode *inode, struct file *filp ){
printk( KERN_NOTICE"Hello device close!\n" );
return 0;
}
/* user read from hello device*/
ssize_t hello_read( struct file *flip, char __user *buf, size_t count,loff_t
*f_pos){
ssize_t retval = 0;
char *bank;
bank = kmalloc(count+1, GFP_KERNEL );
if( bank == NULL )
return -1;
memset( bank, 'A',count );
if( copy_to_user( buf, bank, count ) ){
retval = -EFAULT;
goto out;
}
retval += count;
*(bank+count)=0;
printk( KERN_NOTICE"hello: user read %d bytes from me. %s\n",count,bank );
out:
kfree(bank);
return retval;
}
/* write to hello device */
ssize_t hello_write( struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos ){
ssize_t retval = 0;
char *bank = kmalloc( count ,GFP_KERNEL );
if( bank == NULL )
return retval;
if( copy_from_user(bank, buf, count ) ){
retval = -EFAULT;
printk( KERN_NOTICE"hello: write error\n" );
goto out;
}
retval += count;
printk( KERN_NOTICE"hello: user has written %d bytes to me: %s\n",count,
bank );
out:
kfree(bank );
return retval;
}
你可能会注意到open和release函数头中的file和inode结构体,inode是内核内部文件的表示,当其指向一个字符设备时,其中的i_cdev成员既包含了指向cdev结构的指针。而file表示打开的文件描述符,对一个文件,若打开多次,则会有多个file结构,但只有一个inode与之对应。
因为驱动工作在内核空间,不能使用用户空间的libc函数,所以程序中打印语句为内核提供的printk,而非printf,KERN_NOTICE宏其实标记的是日志级别(共有八个)不同级别的消息会记录到不同的地方。如果你运行本模块,可能会发现printk语句并没有输出到控制台,这是正常的,控制台只显示一定级别的消息。当日志级别小于console_loglevel时,消息才能显示出来。你可以通过dmsg命令看到这些信息,也可以通过修改日志级别使之输出到你的虚拟终端。
作好以上准备工作后,接下来就可以开始进行向内核申请主设备号了。设备号是干什么吃的?据LDD记载,对字符设备的访问是通过文件系统内的设备名称进行的。那些被称为特殊文件、设备文件的节点,通常位于/dev目录,如果ls -l 查看该目录,第一列中带有c标志的即为字符设备,有b标志的为块设备。而第5、6列所示的两个数字分别为设备的主、次设备号。通常,主设备号标识设备所用的驱动程序(现在大多设备仍然采用“一个主设备号对应一个驱动程序”的规则),次设备号用于确定设备,比如你有两块网卡,使用同一驱动,主设备号相同,那么他们将由次设备号区分。
/* Module housekeeping */
static int hello_init(void){
int result;
dev_t dev = MKDEV( hello_major, 0 );/*to transfer major as dev_t type*/
/* alloc the major device number dynamicly */
result = alloc_chrdev_region(&dev, 0 ,1, "hello" );
if( result < 0 ){
printk( KERN_NOTICE"Hello: unable to get major %d\n",hello_major );
return result;
}
hello_major = MAJOR(dev);
/* set up devices, in this case, there is only one device */
printk( KERN_NOTICE"hello init. major:%d, minor:%d\n",hello_major,0 );
//printk( KERN_ALERT"hello init: %d, %d\n",hello_major,0 );
hello_setup_cdev(&hellow, 0 , &hello_ops );
return 0;
}
/* Exit routine */
static void hello_exit(void){
/* remove the cdev from kernel */
cdev_del(&hellow );
/* release the device numble alloced earlier */
unregister_chrdev_region( MKDEV( hello_major, 0 ), 1 );
printk( KERN_NOTICE"hello exit. major:%d,minor %d\n",hello_major,0 );
}
这里主设备号的分配由alloc_chrdev_region(第一个参数为dev_t 指针,用来存放设备编号,第二个参数为要使用的第一个次设备号,通常为0,第三个参数为请求的连续设备编号个数)动态分配,当然也可以静态指定一个未被使用的主设备号,相应函数为register_chrdev_region,但不推荐这样做。在模块被卸载时(hello_exit),通过unregister_chrdev_region释放设备号。MKDEV宏将给出的主、次设备号转换成dev_t类型,MAJOR,MINOR分别从dev_t中析取主次设备号。
这里几个函数的原型为:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
void unregister_chrdev_region(dev_t first, unsigned int count);
然后进入hello_setup_cdev函数,对设备进行初始化这里cdev结构体是内核内部使用来表示字符设备的。在内核调用设备操作之前,必须分配并注册一个或多个这样的结构。为了方便,没有动态使用cdev_alloc函数分配空间,而是定义了一个全局静态cdev变量。通常你可以将你的cdev嵌入到自定义的结构体中(这个驱动很naive,没有这么做),通过cdev_init 函数初始化。最后调用cdev_add(),注册cdev到内核。
/* set up the cdev stucture for a device */
static void hello_setup_cdev( struct cdev *dev, int minor, struct
file_operations *fops ){
int err;
int devno = MKDEV( hello_major, minor );
/* initialize the cdev struct */
cdev_init( dev,fops );
dev->owner = THIS_MODULE;
err = cdev_add( dev, devno, 1 ); /* register the cdev in the kernel */
if( err )
printk( KERN_NOTICE"Error %d adding hello%d\n",err ,minor );
}
最后module_init( hello_init ); module_exit( hello_exit );指定了模块初始化和关闭函数。MODULE_LICENSE( "Dual BSD/GPL" ); 指定模块使用的许可证能被内核识别的许可证有GPL、GPL v2、 Dual BSD/GPL、 Dual MPL/GPL、Proprietary(专有)等,如果模块没有显式标记许可证,则会被认定为“专有”,内核加载这样的模块会被“污染”。
/* register the init and exit routine of the module */
module_init( hello_init );
module_exit( hello_exit );
MODULE_AUTHOR( "jabenwang" );
MODULE_LICENSE( "Dual BSD/GPL" );
到这里,这个字符设备驱动已经完成,接下来就是编译它。
======================================================================================================
编译代码
======================================================================================================
这个是我写的makefile文件,在我台机上我没把这个模块加入到内核源码的字符设备目录下,而是放在了用户目录下面。但这个makefile文件对以上两种情况都支持:
#wjb add 2011-10-21
ifneq ($(KERNELRELEASE), )
obj-m := hellow.o
else
KERNELDIR =/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
.PHONY: modules modules_install clean
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif
======================================================================================================
模块加载&设备文件节点构造:
======================================================================================================
1. 编译成功后,会得到hellow.ko, 这时你就可以通过insmod命令加载模块
# insmod hellow.ko
这时你的日志控制台中会出现hello_init中的打印信息,如果你使用lsmod列出当前已加载模块,会发现hellow模块赫然在目:
root@ubuntu:~/share/hellow# ls
a.out hellow.ko hellow.o Makefile Module.symvers
hellow.c hellow.mod.c main.c Makefile~
hellow.c~ hellow.mod.o main.c~ modules.order
root@ubuntu:~/share/hellow# insmod hellow.ko
root@ubuntu:~/share/hellow# dmesg | tail
[ 3711.851658] hello init. major:251, minor:0
2.要想使用驱动,你需要在/dev 目录下建立设备文件节点,语法是
mknod [options]name {bc} major minor
这里需要知道设备的主、次设备号,何以知之?使用cat /proc/devices | grep hello 你就会得到其主设备号
比如我这里得知hellow的主设备号为251
那么就用下面的命令:
root@ubuntu:~/share/hellow# mknod /dev/hellow c 251 0
c表示字符设备,这样就可以通过该设备文件操作设备了。
======================================================================================================
测试程序:
======================================================================================================
现在就可以通过系统调用操作设备了,我写了一个测试程序来调用:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
int fd, ret;
char *buf = " Hello world !";
char temp[10] = "0";
fd = open ( "/dev/hellow" , O_RDWR);
if ( fd == -1 )
{
perror("open");
exit(0);
}
ret = write( fd, buf,strlen(buf));
if ( ret == -1 )
{
perror("write");
exit(0);
}
ret = read ( fd ,temp, strlen(temp) );
if ( ret == -1)
{
perror("read");
exit(0);
}
close(fd);
return 0;
}
编译之:
root@ubuntu:~/share/hellow# gcc main.c
生成的目标文件为a.out
运行之:
root@ubuntu:~/share/hellow# ./a.outroot@ubuntu:~/share/hellow# dmesg | tail
结果:
[ 4082.930492] Hello device open!
[ 4082.930520] hello: user has written 14 bytes to me: Hello world !
[ 4082.930524] hello: user read 1 bytes from me. A
[ 4082.930829] Hello device close!
当然,如果你想移除这个字符设备,可以输入如下命令:
root@ubuntu:~/share/hellow# rmmod hellow
root@ubuntu:~/share/hellow# dmesg | tail
结果显示如下信息,说明已经移除:
[ 4344.602407] hello exit. major:251,minor 0
========
linux设备驱动程序之简单字符设备驱动
一、linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序:
1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
2、块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。
二、字符设备驱动程序基础:
1、主设备号和次设备号(二者一起为设备号):
一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述,2.6.28中定义如下:
typedef u_long dev_t;
在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
可以使用下列宏从dev_t中获得主次设备号: 也可以使用下列宏通过主次设备号生成dev_t:
MAJOR(dev_t dev); MKDEV(int major,int minor);
MINOR(dev_t dev);
//宏定义:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
2、分配设备号(两种方法):
(1)静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
(2)动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count);
创建设备文件:
利用cat /proc/devices查看申请到的设备名,设备号。
(1)使用mknod手工创建:mknod filename type major minor
(2)自动创建;
利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。
3、字符设备驱动程序重要的数据结构:
(1)struct file:代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。
//重要成员:
const struct file_operations *f_op; //该操作是定义文件关联的操作的。内核在执行open时对这个指针赋值。
off_t f_pos; //该文件读写位置。
void *private_data;//该成员是系统调用时保存状态信息非常有用的资源。
(2)struct inode:用来记录文件的物理信息。它和代表打开的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。inode一般作为file_operations结构中函数的参数传递过来。
inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。
dev_t i_rdev; //对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev; //是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
//我们也可以使用下边两个宏从inode中获得主设备号和此设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
(3)struct file_operations
本部分来源于:http://blog.chinaunix.net/space.php?uid=20729583&do=blog&id=1884550
struct file_operations ***_ops={
.owner = THIS_MODULE,
.llseek = ***_llseek,
.read = ***_read,
.write = ***_write,
.ioctl = ***_ioctl,
.open = ***_open,
.release = ***_release,
。。。 。。。
};
struct module *owner;
/*第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针.
这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为
THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/
loff_t (*llseek) (struct file * filp , loff_t p, int orig);
/*(指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位
的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示.
如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).*/
ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p);
/*(指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址),
参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)
这个函数用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败.
一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).*/
ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t p);
/*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同 的,
异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。
异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);
初始化一个异步读 -- 可能在函数返回前不结束的读操作.如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).
(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/
ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos);
/*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,
ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
(注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/
ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos);
/*初始化设备上的一个异步写.参数类型同aio_read()函数;*/
int (*readdir) (struct file * filp, void *, filldir_t);
/*对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)
这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。
(poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞.
poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能.
如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
(这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
/*(inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数.
cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针.
如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的.
因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.)
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表.
如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.*/
int (*mmap) (struct file *, struct vm_area_struct *);
/*mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
(如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/
int (*open) (struct inode * inode , struct file * filp ) ;
/*(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
与open()函数对应的是release()函数。*/
int (*flush) (struct file *);
/*flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作.
这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用;
SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.*/
int (*release) (struct inode *, struct file *);
/*release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.*/
int(*synch)(struct file *,struct dentry *,int datasync);
//刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。
int (*aio_fsync)(struct kiocb *, int);
/*这是 fsync 方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync
把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束,
这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/
int (*fasync) (int, struct file *, int);
//这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:
static int ***_fasync(int fd,struct file *filp,int mode)
{
struct ***_dev * dev=filp->private_data;
return fasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为 fasync_struct结构体指针的指针。
//这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。
}
/*此操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述.
这个成员可以是NULL 如果驱动不支持异步通知.*/
int (*lock) (struct file *, int, struct file_lock *);
//lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
/*这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作;
这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).*/
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
/*这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个.
例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.*/
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
/*sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.*/
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
/*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.
这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10]*/
int (*check_flags)(int)
//这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.
int (*dir_notify)(struct file *, unsigned long);
//这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify.
三、字符设备驱动程序设计:
1.设备注册:
在linux2.6内核中,字符设备使用struct cdev来描述;
struct cdev
{
struct kobject kobj;//内嵌的kobject对象
struct module *owner;//所属模块
struct file_operations *ops;//文件操作结构体
struct list_head list;
dev_t dev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号
unsigned int count;
};
字符设备的注册分为三个步骤:
(1)分配cdev: struct cdev *cdev_alloc(void);
(2)初始化cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops);
(3)添加cdev: int cdev_add(struct cdev *p, dev_t dev, unsigned count)
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
2.设备操作的实现:file_operations函数集的实现(要明确某个函数什么时候被调用?调用来做什么操作?)
特别注意:驱动程序应用程序的数据交换:
驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
put_user(local,user);
get_user(local,user);
3.设备注销:void cdev_del(struct cdev *p);
四、字符设备驱动小结:
字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。
五:字符设备驱动程序分析:
(1)memdev.h
#ifndef _MEMDEV_H_
#define _MEMDEV_H_
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/
#endif
#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*设备数*/
#endif
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif
/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size;
};
#endif /* _MEMDEV_H_ */
(2)memdev.c
static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*设备结构体指针*/
struct cdev cdev;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;
return 0;
}
/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos; /*记录文件指针偏移位置*/
unsigned int count = size; /*记录需要读取的字节数*/
int ret = 0; /*返回值*/
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE) /*要读取的偏移大于设备的内存空间*/
return 0;
if (count > MEMDEV_SIZE - p) /*要读取的字节大于设备的内存空间*/
count = MEMDEV_SIZE - p;
/*读数据到用户空间:内核空间->用户空间交换数据*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
return ret;
}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p) /*要写入的字节大于设备的内存空间*/
count = MEMDEV_SIZE - p;
/*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count; /*增加偏移位置*/
ret = count; /*返回实际的写入字节数*/
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;
}
/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case 0: /* SEEK_SET */ /*相对文件开始位置偏移*/
newpos = offset; /*更新文件指针位置*/
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;
case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;
default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
/* 静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno); /*获得申请的主设备号*/
}
if (result < 0)
return result;
/*初始化cdev结构,并传递file_operations结构指针*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE; /*指定所属模块*/
cdev.ops = &mem_fops;
/* 注册字符设备 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 为设备描述结构分配内存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
kfree(mem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
(3)应用程序(测试文件):app-mem.c
#include <stdio.h>
int main()
{
FILE *fp0 = NULL;
char Buf[4096];
/*初始化Buf*/
strcpy(Buf,"Mem is char dev!");
printf("BUF: %s\n",Buf);
/*打开设备文件*/
fp0 = fopen("/dev/memdev0","r+");
if (fp0 == NULL)
{
printf("Open Memdev0 Error!\n");
return -1;
}
/*写入设备*/
fwrite(Buf, sizeof(Buf), 1, fp0);
/*重新定位文件位置(思考没有该指令,会有何后果)*/
fseek(fp0,0,SEEK_SET);
/*清除Buf*/
strcpy(Buf,"Buf is NULL!");
printf("BUF: %s\n",Buf);
/*读出设备*/
fread(Buf, sizeof(Buf), 1, fp0);
/*检测结果*/
printf("BUF: %s\n",Buf);
return 0;
}
测试步骤:
1)cat /proc/devices看看有哪些编号已经被使用,我们选一个没有使用的XXX。
2)insmod memdev.ko
3)通过"mknod /dev/memdev0 c XXX 0"命令创建"/dev/memdev0"设备节点。
4)交叉编译app-mem.c文件,下载并执行:
#./app-mem,显示:
Mem is char dev!
========
Linux 字符驱动开发心得
Linux字符驱动框架相比初学还是比较难记的,在学了一阵子字符驱动的开发后对于框架的搭建总结出了几个字 。
对于框架来讲主要要完成两步。
申请设备号,注册字符驱动
其关键代码就两句
~
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);//动态申请设备号
int cdev_add(struct cdev *, dev_t, unsigned); //注册字符驱动
~
执行完次就可以将我们的驱动程序加载到内核里了
首先我们搭建主程序,字符驱动的名字就叫做"main"
首先先写下将要用到的头文件,以及一个宏定义,指明了我们驱动的名称,当然名称可以任意这里就取"main" 作为名字
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/coda.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define MUDULE_NAME "main"
驱动由于需要加载到内核里,所以我们需要声明一下我们驱动所遵循的协议,如果没有申明,那么加载内核的时候系统会提示一段信息。我们按照内核的风格来,就加一个GPL协议吧
MODULE_LICENSE("GPL");
我们要想将我们的驱动注册到内核里,就必须将我们的驱动本身作为一个抽象,抽象成一个struct cdev的结构体。因为我们系统内部有许多中字符驱动,为了将这些不同种类的驱动都能使用同一个函数进行注册,内核声明了一个结构体,不同的驱动通过这个结构体--变成了一个抽象的驱动供系统调用。这段有点罗嗦,我们来看一下cdev这个结构体吧。
//这段不属于主程序
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
这个结构体就包含了一个驱动所应有的东西其中 kobj 不需要管它,我也没有仔细研究,owner指向模块的所有者,常常使用THIS_MODULE这个宏来赋值,ops是我们主要做的工作,其中定义了各种操作的接口。
下面我们定义了我们程序的抽象体mydev,以及他所需要的接口
struct cdev mydev;
struct file_operations ops;
struct file_operations这个结构有点庞大。
//不属于本程序
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
上面看到,这个结构内部都是一些函数指针,相当与这个结构本身就是一个接口,在c语言中没有接口这个概念,使用这种方式来定义也是一种巧妙的用法。不过有所不同的是我们可以不完全实现其中的接口。
应用程序在使用驱动的时候常常需要open,write,read,close这几种操作,也就对应了file_operations结构中的open,write,read,release这几个函数指针。下面我们开始实现我们自己的函数体。注意:我们自己实现的函数必须满足接口函数所定义的形式。
static int main_open(struct inode* inode,struct file* filp)
{
return 0;
}
这个教程里面的程序,我们就让驱动只能往里面写一个字符为例,读取也是只能读取一个字符。
我们定义一个静态的字符类型的变量来当作我们的存储空间,通过copy_from_user来将用户空间的数据拷贝到我们驱动 所在的内核空间。原型是:
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
类似地,我们使用copy_to_user来完成内核空间到用户空间的数据拷贝。
static inline long copy_to_user(void __user *to,const void *from, unsigned long n)
static char main_buffer;
static ssize_t main_write(struct file* filp,const char __user* buffer,size_t length,loff_t * l)
{
if(length!=1) return -1;
copy_from_user(&main_buffer,buffer,length);
return 1;
}
下面是读的实现
static ssize_t main_read(struct file* filp,char __user * buffer,size_t length,loff_t*l)
{
if(length!=1) return -1;
copy_to_user(buffer,&main_buffer,length);
return 1;
}
再稍稍实现一下close
static int main_close(struct inode* inode,struct file* filp)
{
return 0;
}
我们所需要的内容都已经填写完毕,我们在驱动初始化的时候调用cdev_add驱动注册到系统就行了,不过在注册之前我们要申请设备号。
static dev_t dev;
static int __init main_init(void)
{
首先我们使用动态申请的方式申请设备号
int result=alloc_chrdev_region(&dev,0,1,MODULE_NAME);
dev就是我们申请的设备号,其中dev_t其实就是一个无符号的long型,通过调用alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *) 将申请到的设备号写入到dev中,第二个参数是子设备号从几开始,第三个参数是申请几个设备号,因为申请多个设备号是连续的,所以我们只需要知道第一个就行了。第四个参数代表我们驱动的名称。
返回值如果小于0则表示我们申请失败,通过printk打印错误信息。我测试的在动态加载的时候printk都不能打印其信息,如果在Ubuntu下可以查看/var/log/kern.log,如果是CentOS下可以查看/var/log/mssages来查看printk打印的信息,一般查看后10条就能足够了。
if(result<0)
{
printk(KERN_ALERT"device load error");
return -1;
}
然后我们再构造一下我们的接口结构体
ops.owner=THIS_MODULE;
ops.open=main_open;
ops.release=main_close;
ops.write=main_write;
ops.read=main_read;
构造完之后,我们就只剩下我们最重要的一步了,就是向系统注册我们的驱动。
不过,先别急,我们注册前得先把我们的抽象驱动mydev给构造了,mydev的定义在最上面。
cdev_init(&mydev,&ops);
mydev.owner=THIS_MODULE;
这样,我们就使用了我们的ops构造了我们的抽象驱动,当我们把这个驱动添加到我们的内核里面的时候,假如内核想对这个驱动进行写的操作,就会从mydev->ops->main_write这样找到我们自己实现的写函数了。
接下来就注册我们的驱动。
cdev_add(&mydev,dev,1);
printk(KERN_ALERT"device load success\n");
return 0;
}
至此,我们的驱动就算完成了,不过有一点,就是我们的驱动有了初始化函数了就一定还有一个清理的函数了,该函数主要在我们卸载驱动的时候会调用,module_init及module_exit主要用于声明这个驱动的入口与出口,是必须要做的一步。
static void __exit main_exit(void)
{
unregister_chrdev_region(dev,1);
cdev_del(&mydev);
}
module_init(main_init);
module_exit(main_exit);
我的Makefile是下面这样
ifeq ($(KERNELRELEASE),)
KERNELDIR?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules -Wall
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install -Wall
clean:
rm -rf *.0 *~ core .depend .*.cmd
sudo rmmod main
sudo rm /dev/main
install:
sudo insmod main.ko
sudo mknod /dev/main c 250 0
message:
tail -n 5 /var/log/kern.log
.PHONY: modules modules_install clean
else
obj-m :=main.o
endif
因为我的机器是使用ubuntu,所以在message标签下是tail -n 5 /var/log/kern.log 如果的/var/log目录下没有kern.log,那么就替换成/var/log/messages
编译
$make
因为这个Makefile的install 都是针对我自己电脑而写的,所以并不能在你电脑上保证执行make install 的正确性。还是在命令行中敲吧
sudo insmod main.ko
安装模块,然后查看/proc/devices里面main这个模块对应的设备号是多少,我的是250,所以创建设备节点时这样创建
sudo mknod /dev/main c 250 0
这样就成功把我们的驱动安装到内核里了
执行make message可以看到如下信息
tail -n 5 /var/log/kern.log
Sep 17 20:05:57 quanweiC kernel: [23536.688371] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=2558 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43
Sep 17 20:06:02 quanweiC kernel: [23541.691748] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=3335 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43
Sep 17 20:06:51 quanweiC kernel: [23590.610275] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=10708 PROTO=UDP SPT=11818 DPT=10948 LEN=43
Sep 17 20:07:04 quanweiC kernel: [23603.815562] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=12523 PROTO=UDP SPT=11818 DPT=10104 LEN=43
Sep 17 20:07:04 quanweiC kernel: [23603.930248] device load success
最后一行显示我们加载驱动成功了。这样一个简单的字符驱动就写成功了。
Linux字符驱动中动态分配设备号与动态生成设备节点 http://www.linuxidc.com/Linux/2014-03/97438.htm
字符驱动设计----mini2440 LED驱动设计之路 http://www.linuxidc.com/Linux/2012-08/68706.htm
Linux 设备驱动 ====> 字符驱动 http://www.linuxidc.com/Linux/2012-03/57581.htm
如何编写Ubuntu字符驱动 http://www.linuxidc.com/Linux/2010-05/25887.htm
2.4下内核linux字符驱动模板 http://www.linuxidc.com/Linux/2007-06/5338.htm
========
linux字符设备驱动开发基础知识
Linux设备分类
Linux下的设备通常分为三类,字符设备,块设备和网络设 备。
字符设备
一个字符设 备是一种字节流设备,对设备的存取只能按顺序按字节的存取而不能随机访问,字符设备没有请求缓冲区,所有的访问请求都是按顺序执行的。Linux下的大多设备都是字符设备。应用程序是通过字符设备节点来访问 字符设备的。设备节点一般都由mknod命令都创 建在/dev目录下,下 面的例子显示了串口设备的设备节点。字符设备文件的第一个标志是前面的“c”标志。
root#ls -l /dev/ttyS[0-3]
crw-rw---- 1 root root 4, 64 Feb 18 23:34 /dev/ttyS0
crw-r----- 1 root root 4, 65 Nov 17 10:26 /dev/ttyS1
crw-rw---- 1 root root 4, 66 Jul 5 2000 /dev/ttyS2
crw-rw---- 1 root root 4, 67 Jul 5 2000 /dev/ttyS3
字符设备是指那些只能按顺序一个字节一个字节读取的设备,但事实上现在一些高级 字符设备也可以从指定位置一次读取一块数据。字符设备是面向数据流的设备,每个字符设备都有一个设备号,设备号由主设备号和次设备号组成。同时Linux使用管理文件相同的方法来管理字符设备,所以每个字符设备在/dev/目录下都有一个对应的设备文件,即设备节点,它们包含了设备的 类型、主/次设备号以 及设备的访问权限控制等,系统通过设备文件来对字符设备进行操作,每个字符设备文件都有自己的与普通文件相同的文件操作函数组结构(struct file_operations)。字符设 备驱动通常至少需要实现文件操作函数组中的open、release、read和write四种操作方法。常见的字符设备有鼠标、键盘、串口、控制台等。
块设备
存储设备一 般属于块设备,块设备有请求缓冲区,并且支持随机访问而不必按照顺序去存取数据,比如你可以 先存取后面的数据,然后在存取前面的数据,这对字符设备来说是不可能的。Linux下的磁盘 设备都是块设备,尽管在Linux下有块设 备节点,但应用程序一般是通过文件系统及其高速缓存来访问块设备的,而不是直 接通过设备节点来读写块设备上的数据。块设备文件的第一个标志是前面的“b”标志。
root# ls -l /dev/hda[1-3]
brw-rw---- 1 root root 3, 1 Jul 5 2000 /dev/hda1
brw-rw---- 1 root root 3, 2 Jul 5 2000 /dev/hda2
brw-rw---- 1 root root 3, 3 Jul 5 2000 /dev/hda3
块设备是指那些可以从设备的任意位置读取任意长度数据的设备。每个块设备同样有 一个设备号,设备号由主设备号和次设备号组成。同时Linux也使用管 理文件相同的方法来管理块设备,每个块设备在/dev/目录下都 有一个对应的设备文件,即设备节点,它们包含了设备的类型、主/次设备号 以及设备的访问权限控制等,系统通过设备文件来对块设备进行操作,每个块设备文件都有自己的与普通文件相同的文件操作函数组结构(struct file_operations)。但块设 备需要实现的操作方法远比字符设备的操作方法多得多,也难得多。块设备既可以作为普通的裸设备用来存放任意数据,也可以将块设备按某种文件系统类型的格式 进行格式化,然后按照该文件系统类型的格式来读取块设备上的数据,但不管哪种方式,最后访问设备上的数据都必须通过调用设备本身的操作方法实现,区别在于 前者直接调用块设备的操作方法,而后者则间接调用块设备的操作方法。常见的块设备有各种硬盘、flash磁盘、RAM磁盘等。
网络设备
网络设备不 同于字符设备和块设备,它是面向报文的而不是面向流的,它不支持随机访问,也没有请求缓冲区。在Linux里一个网络设备也可以叫做一个网络接口,如eth0,应用程序是通过Socket而不是设备节点来访问网络设备,在系统里根本就不存在网络设备节点。
网络接口用来与其他设备交换数据,它可以是硬件设备,也可以是纯软件设备,如loopback接口就是一个纯软件设备。网络接口由内核中的网络 子系统驱动,负责发送和接收数据包,但它不需要了解每项事务如何映射到实际传送的数据包,许多网络连接(尤其是使用TCP协议的连接)是面向流的,但网络设备围绕数据包的传输和接收设 计。网络驱动程序不需要知道各个连接的相关信息,它只需处理数据包。网络接口没有像字符设备和块设备一样的设备号,只有一个唯一的名字,如eth0、eth1等,而这个名字也不需要与设备文件节点对应。内核使用一套与数据 包传输相关的函数来与网络设备驱动程序通信,它们不同于字符设备和块设备的read()和write()方法。
设备节点、设备驱动及设备的关联
当我们访问 一个设备节点是,系统是如果知道使用哪个设备驱动及访问哪个设备的呢?这个是通过设备号来实现的。当我们创建一个设备节点时需要指定主设备号和次设备号。 对于设备节点来说,名字不是重要的,设备号才是最重要的,它实际指定了对应的驱动程序和对应的设备。
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存 放在/dev目录下,称 为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备 号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。
Linux为所有的 设备文件都提供了统一的操作函数接口,方法是使用数据结构struct file_operations。这个数据结构中包括许多操作函数的指针,如open()、close()、read()和write()等,但由于外设的种类较多,操作方式各不相同。Struct file_operations结构体中的 成员为一系列的接口函数,如用于读/写的read/write函数和用于控制的ioctl等。打开一个文件就是调用这个文件file_operations中的open操作。不同类型的文件有不同的file_operations成员函数, 如普通的磁盘数据文件,接口函数完成磁盘数据块读写操作;而对于各种设备文件,则最终调用各自驱动程序中的I/O函数进行具体设备的操作。这样,应用程序根本不必考虑操作的是设 备还是普通文件,可一律当作文件处理,具有非常清晰统一的I/O接口。所 以file_operations是文件层 次的I/O接口。
主设备号
驱动程序在 初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。你可以通过/proc/devices文件来驱动 系统设备的主设备号。
次设备号
驱动程序遍 历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次 设备号知道它说访问的设备了。
系统中的每一个字符设备和块设备(网络接口没有设备号)都有一个设备号,传统的UNIX以及早期版本Linux中的设备号是16位的,主次设备号都是8位的,低8位为次设备号,高8位为主设备号,因此系统最多分别支持65536个字符设备和65536个块设备,这个限制已经不能满足当 前层出不穷的各种新设备的需要,所以Linux2.6中对设备号已经进行了扩展,一个设备 号为32位,主设备号为12位,次设备号为20位,但是这32位设备号的编码方式有新旧两种,旧的 设备编号格式为:最高12位为主设备号,最低20位为次设备号;新的设备编号格式为:bit[19:8]是主设备号,bit[31:20]是次设备号的高12位,bit[7:0]是次设备号的低8位。如果知道了一个设备的主设备号major和次设备号minor,那么用MKDEV(major,minor)生成是该设备的旧格式的设备号,用new_encode_dev(MKDEV(major,minor))生成的则是新格式的设备号。Linux支持的各种设备的主设备号定义在include/linux/major.h文件中,而已经在官方注册的主设备 号和次设备号在Documentation/devices.txt文件中可以找到。
老式16位设备 号、32位旧格式设 备号以及32位新格式设 备号的转换操作函数如下:
new_encode_dev(dev_t dev)函数
将32位旧格式 设备号dev转换成32位新格式设备号。
new_decode_dev(u32 dev)函数
将32位新格式 设备号转换成32位旧格式设 备号。
old_encode_dev(dev_t dev)函数
将32位旧格式 设备号转换成老式16位设备号。
dev_t old_decode_dev(u16 val)函数
将老式16位设备号转换成32位旧格式设备号。
Linux中设备节点是通过“mknod”命令来创建 的。一个设备节点其实就是一个文件,Linux中称为设 备文件。有一点必要说明的是,在Linux中,所有 的设备访问都是通过文件的方式,一般的数据文件程序普通文件,设备节点称为设备文件。在Linux内核中网络设备也是通过文件操作的,称为网络设备文件,在用户空间是通过socket接口来访问的。socket号就是网络设备文件描述符。
如:mknod /dev/mydevice c 254 0
(c代表子都设备,254为主设备号,0为次设备号)
Open,close等操作/dev/下设备文件,内核根据文件的主设备号找到对应的设备驱动
主设备号可以分为动态和静态申请。
设备文件
Linux使用对文 件一样管理方式来管理设备,所以对于系统中的每个字符设备或者块设备都必须为其创建一个设备文件,这个设备文件就是放在/dev/目录下的设备节点,它包含了该设备的设备类型(块设备或字符设 备)、设备号(主设备号和次设备号)以及设备访问控制属性等。设备文件可以通过手工用mknod命令生成也可以由udev用户工具 软件在系统启动后根据/sys目录下每个 设备的实际信息创建,使用后一种方式可以为每个设备动态分配设备号,而不必分配固定的设备号,如果系统中的设备不多,而且设备类型又是常见的,可以使用手 工方式生成设备文件,为常用设备创建一个已经分配号的设备号对应的设备文件,这样比较方便。如果系统很大,系统中的设备太多,那么最好动态分配设备号,由udev在系统启动之后根据设备实际信息自动创建设备文件。
Linux下的大部分驱动程序都是字符设备驱动程序,通过下面的学习我们将 会了解到字符设备是如何注册到系统中的,应用程序是如何访问驱动程序的数据的,及字符驱动程序是如何工作的。
设备号
通过前面的 学习我们知道应用程序是通过设备节点来访问驱动程序及设备的,其根本是通过设备节点的设备号(主设备号及从设备号)来关联驱动程序及设备的,字符设备也不 例外(其实字符设备只能这样访问)。这里我们详细讨论Linux内部如何管 理设备号的。
设备号类型
Linux内核里用“dev_t”来表示设备 号,它是一个32位的无符号 数,其高12位用来表示主 设备号,低20位用来表示从 设备号。它被定义在<linux/types.h>头文件里。 内核里提供了操作“dev_t”的函数,驱动 程序中通过这些函数(其实是宏,定义在<linux/kdev_t.h>文件中)来 操作设备号。
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MAJOR(dev)用于获取主设备号,MINOR(dev)用于获取从设备号,而MKDEV(ma,mi)用于通过主设备号和从设备号构造"dev_t"数据。
另一点需要 说明的是,dev_t数据类型支持2^12个主设备号,每个主设备号(通常是一个设备驱动)可以支持2^20个设备,目前来说这已经足够大了,但谁又能说将来还能满足要求 呢?一个良好的编程习惯是不要依赖dev_t这个数据类 型,切记必须使用内核提供的操作设备号的函数。
字符设备号注册
内核提供了字符设备号管理的函数接口,作为一个良好的编程习惯,字符设备驱动程 序应该通过这些函数向系统注册或注销字符设备号。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)
register_chrdev_region用于向内核 注册已知可用的设备号(次设备号通常是0)范围。由 于历史的原因一些设备的设备号是固定的,你可以在内核源代码树的Documentation/devices.txt文件中找到 这些静态分配的设备号。
alloc_chrdev_region用于动态分 配的设备号并注册到内核中,分配的设备号通过dev参数返回。 作为一个良好的内核开发习惯,我们推荐你使用动态分配的方式来生成设备号。
unregister_chrdev_region用于注销一 个不用的设备号区域,通常这个函数在驱动程序卸载时被调用。
字符设备
Linux2.6内核使用“struct cdev”来记录字符设 备的信息,内核也提供了相关的函数来操作“struct cdev”对象,他们 定义在<linux/cdev.h>头文件中。 可见字符设备及其操作函数接口定义的很简单。
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
对于Linux 2.6内核来说,struct cdev是内核字符设备的基础结构,用来表示一个字符设备, 包含了字符设备需要的全部信息。
kobj:struct kobject对象数据,用 来描述设备的引用计数,是Linux设备模型的 基础结构。我们在后面的“Linux设备模型”在做详细的介绍。
owner:struct module对象数据,描 述了模块的属主,指向拥有这个结构的模块的指针,显然它只有对编译为模块方式的驱动才由意义。一般赋值位“THIS_MODULE”。
ops:struct file_operations对象数据,描 述了字符设备的操作函数指针。对于设备驱动来说,这是一个很重要的数据成员,几乎所有的驱动都要用到这个对象,我们会在下面做详细介绍。
dev:dev_t对象数据,描述了字符设备的设备号。
内核提供了操作字符设备对象“struct cdev”的函数,我们 只能通过这些函数来操作字符设备,例如:初始化、注册、添加、移除字符设备。
cdev_alloc:用于动态 分配一个新的字符设备 cdev 对象,并对其 进行初始化。采用cdev_alloc分配的cdev对象需要显示的初始化owner和ops对象。
// 参考drivers/scsi/st.c:st_probe 函数
struct cdev *cdev = NULL;
cdev = cdev_alloc();
// Error Processing
cdev->owner = THIS_MODULE;
cdev->ops = &st_fops;
cdev_init:用于初始 化一个静态分配的cdev对象,一般这 个对象会嵌入到其他的对象中。cdev_init会自动初始 化ops数据,因此应 用程序只需要显示的给owner对象赋值。cdev_init的功能与cdev_alloc基本相同,唯 一的区别是cdev_init初始化一个 已经存在的cdev对象,并且这 个初始化会影响到字符设备删除函数(cdev_del)的行为, 请参考cdev_del函数。
cdev_add:向内核系 统中添加一个新的字符设备cdev,并且使它立 即可用。
cdev_del:从内核系 统中移除cdev字符设备。如 果字符设备是由cdev_alloc动态分配的, 则会释放分配的内存。
cdev_put:减少模块 的引用计数,一般很少会有驱动程序直接调用这个函数。
文件操作对象
Linux中的所有设备都是文件,内核中用“struct file”结构来表示一 个文件。尽管我们的驱动不会直接使用这个结构中的大部分对象,其中的一些数据成员还是很重要的,我们有必要在这里做一些介绍,具体的内容请参考内核源代码 树<linux/fs.h>头文件。
// struct file 中的一些重要数据成员
const struct file_operations *f_op;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct address_space *f_mapping;
这里我们不对struct file做过多的介绍,另一篇struct file将做详细介绍。这个结构中的f_ops成员是我们的驱动所关心的,它是一个struct file_operations结构。Linux里的struct file_operations结构描述了一 个文件操作需要的所有函数,它定义在<linux/fs.h>头文件中。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
这是一个很大的结构,包含了所有的设备操作函数指针。当然,对于 一个驱动,不是所有的接口都需要来实现的。对于一个字符设备来说,一般实现open、release、read、write、mmap、ioctl这几个函数就足够了。
这里需要指 出的是,open和release函数的第一个参数是一个struct inode对象。这是一个内核文件系统索引节点对象,它包含 了内核在操作文件或目录是需要的全部信息。对于字符设备驱动来说,我们关心的是从struct inode对象中获取设备号(inode的i_rdev成员)内核提供了两个函数来做这件事。
static inline unsigned iminor(const struct inode *inode)
{
return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
return MAJOR(inode->i_rdev);
}
尽管我们可 以直接从inode->i_rdev获取设备 号,但是尽量不要这样做。我们推荐你调用内核提供的函数来获取设备号,这样即使将来inode->i_rdev有所变化, 我们的程序也会工作的很好。
字符设备驱 动可以参考Linux 设备驱动程序 第三版和linux设备驱动开发 详解,其中linux设备驱动程序 第三版中讲的:
主次编号
一些重要数据结构
字符设备注册
Open和release
读和写
一些头文件和结构体;
都非常经典, 都理解字符驱动设备很重要,很值得参考!
http://blog.csdn.net/shanzhizi
========
LINUX字符设备驱动程序实例(scull)
该驱动程序在UBUNTU10.04LTS编译通过,系统内核为linux-2.6.32-24(可使用uname -r 命令来查看当前内核的版本号)
由于安装UBUNTU10.04LTS时,没有安装LINUX内核源码,因此需要在www.kernel.org下载LINUX源码,下载linux-2.6.32.22.tar.bz2(与系统运行的LINUX内核版本尽量保持一致),使用如下命令安装内核:
1.解压内核
cd /us/src
tar jxvf linux-2.6.32.22.tar.bz2
2.为系统的include创建链接文件
cd /usr/include
rm -rf asm linux scsi
ln -s /usr/src/linux-2.6.32.22/include/asm-generic asm
ln -s /usr/src/linux-2.6.32.22/include/linux linux
ln -s /usr/src/linux-2.6.32.22/include/scsi scsi
LINUX内核源码安装完毕
【2.驱动程序代码】
/******************************************************************************
*Name: memdev.c
*Desc: 字符设备驱动程序的框架结构,该字符设备并不是一个真实的物理设备,
* 而是使用内存来模拟一个字符设备
*Parameter:
*Return:
*Author: yoyoba(
[email protected])
*Date: 2010-9-26
*Modify: 2010-9-26
********************************************************************************/
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include "memdev.h"
static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*设备结构体指针*/
struct cdev cdev;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;
return 0;
}
/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
/*读数据到用户空间*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
return ret;
}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
/*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;
}
/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case 0: /* SEEK_SET */
newpos = offset;
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;
case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;
default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno);
}
if (result < 0)
return result;
/*初始化cdev结构*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;
/* 注册字符设备 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 为设备描述结构分配内存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
kfree(mem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
/************************
*memdev.h
************************/
#ifndef _MEMDEV_H_
#define _MEMDEV_H_
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 260 /*预设的mem的主设备号*/
#endif
#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*设备数*/
#endif
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif
/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size;
};
#endif /* _MEMDEV_H_ */
【3.编译驱动程序模块】
Makefile文件的内容如下:
ifneq ($(KERNELRELEASE),)
obj-m:=memdev.o
else
KERNELDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *.mod.c *.mod.o *.ko
endif
切换到root下,执行make时,如果UBUNTU是使用虚拟机安装的,那么执行make时,不要在ubuntu和windows的共享目录下,否则会出错。
root@VMUBUNTU:~# make
make -C /lib/modules/2.6.32-24-generic/build M=/root modules
make[1]: Entering directory `/usr/src/linux-headers-2.6.32-24-generic'
CC [M] /root/memdev.o
/root/memdev.c:15: warning: type defaults to ‘int’ in declaration of ‘mem_major’
/root/memdev.c: In function ‘mem_read’:
/root/memdev.c:71: warning: format ‘%d’ expects type ‘int’, but argument 3 has type ‘long unsigned int’
/root/memdev.c: In function ‘mem_write’:
/root/memdev.c:99: warning: format ‘%d’ expects type ‘int’, but argument 3 has type ‘long unsigned int’
Building modules, stage 2.
MODPOST 1 modules
CC /root/memdev.mod.o
LD [M] /root/memdev.ko
make[1]: Leaving directory `/usr/src/linux-headers-2.6.32-24-generic'
ls查看当前目录的内容
root@VMUBUNTU:~# ls
Makefile memdev.h memdev.mod.c memdev.o Module.symvers
memdev.c memdev.ko memdev.mod.o modules.order
这里的memdev.ko就是生成的驱动程序模块。
通过insmod命令把该模块插入到内核
root@VMUBUNTU:~# insmod memdev.ko
查看插入的memdev.ko驱动
root@VMUBUNTU:~# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
260 memdev
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
226 drm
251 hidraw
252 usbmon
253 bsg
254 rtc
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 pktcdvd
254 mdp
可以看到memdev驱动程序被正确的插入到内核当中,主设备号为260,该设备号为memdev.h中定义的#define MEMDEV_MAJOR 260。
如果这里定义的主设备号与系统正在使用的主设备号冲突,比如主设备号定义如下:#define MEMDEV_MAJOR 254,那么在执行insmod命令时,就会出现如下的错误:
root@VMUBUNTU:~# insmod memdev.ko
insmod: error inserting 'memdev.ko': -1 Device or resource busy
查看当前设备使用的主设备号
root@VMUBUNTU:~# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
226 drm
251 hidraw
252 usbmon
253 bsg
254 rtc
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 pktcdvd
254 mdp
发现字符设备的254主设备号为rtc所使用,因此会出现上述错误,解决方法只需要在memdev.h中修改主设备号的定义即可。
【4.编写应用程序,测试该驱动程序】
首先应该在/dev/目录下创建与该驱动程序相对应的文件节点,使用如下命令创建:
root@VMUBUNTU:/dev# mknod memdev c 260 0
使用ls查看创建好的驱动程序节点文件
root@VMUBUNTU:/dev# ls -al memdev
crw-r--r-- 1 root root 260, 0 2010-09-26 17:28 memdev
编写如下应用程序,来对驱动程序进行测试。
/******************************************************************************
*Name: memdevapp.c
*Desc: memdev字符设备驱动程序的测试程序。先往memedev设备写入内容,然后再
* 从该设备中把内容读出来。
*Parameter:
*Return:
*Author: yoyoba(
[email protected])
*Date: 2010-9-26
*Modify: 2010-9-26
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <linux/i2c.h>
#include <linux/fcntl.h>
int main()
{
int fd;
char buf[]="this is a example for character devices driver by yoyoba!";//写入memdev设备的内容
char buf_read[4096]; //memdev设备的内容读入到该buf中
if((fd=open("/dev/memdev",O_RDWR))==-1) //打开memdev设备
printf("open memdev WRONG!\n");
else
printf("open memdev SUCCESS!\n");
printf("buf is %s\n",buf);
write(fd,buf,sizeof(buf)); //把buf中的内容写入memdev设备
lseek(fd,0,SEEK_SET); //把文件指针重新定位到文件开始的位置
read(fd,buf_read,sizeof(buf)); //把memdev设备中的内容读入到buf_read中
printf("buf_read is %s\n",buf_read);
return 0;
}
编译并执行该程序
root@VMUBUNTU:/mnt/xlshare# gcc -o mem memdevapp.c
root@VMUBUNTU:/mnt/xlshare# ./mem
open memdev SUCCESS!
buf is this is a example for character devices driver by yoyoba!
buf_read is this is a example for character devices driver by yoyoba!
表明驱动程序工作正常。。。
【5.LINUX是如何make驱动程序模块的】
Linux内核是一种单体内核,但是通过动态加载模块的方式,使它的开发非常灵活 方便。那么,它是如何编译内核的呢?我们可以通过分析它的Makefile入手。以下是 一个简单的hello内核模块的Makefile.
ifneq ($(KERNELRELEASE),)
obj-m:=hello.o
else
KERNELDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *.mod.c *.mod.o *.ko
endif
当我们写完一个hello模块,只要使用以上的makefile。然后make一下就行。 假设我们把hello模块的源代码放在/home/study/prog/mod/hello/下。 当我们在这个目录运行make时,make是怎么执行的呢? LDD3第二章第四节“编译和装载”中只是简略地说到该Makefile被执行了两次, 但是具体过程是如何的呢?
首先,由于make 后面没有目标,所以make会在Makefile中的第一个不是以.开头 的目标作为默认的目标执行。于是default成为make的目标。make会执行 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules shell是make内部的函数,假设当前内核版本是2.6.13-study,所以$(shell uname -r)的结果是 2.6.13-study 这里,实际运行的是
make -C /lib/modules/2.6.13-study/build M=/home/study/prog/mod/hello/ modules
/lib/modules/2.6.13-study/build是一个指向内核源代码/usr/src/linux的符号链接。 可见,make执行了两次。第一次执行时是读hello模块的源代码所在目录/home/s tudy/prog/mod/hello/下的Makefile。第二次执行时是执行/usr/src/linux/下的Makefile时.
但是还是有不少令人困惑的问题: 1.这个KERNELRELEASE也很令人困惑,它是什么呢?在/home/study/prog/mod/he llo/Makefile中是没有定义这个变量的,所以起作用的是else…endif这一段。不 过,如果把hello模块移动到内核源代码中。例如放到/usr/src/linux/driver/中, KERNELRELEASE就有定义了。 在/usr/src/linux/Makefile中有 162 KERNELRELEASE=$(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)$(LOCALVERSION) 这时候,hello模块也不再是单独用make编译,而是在内核中用make modules进行 编译。 用这种方式,该Makefile在单独编译和作为内核一部分编译时都能正常工作。
2.这个obj-m := hello.o什么时候会执行到呢? 在执行:
make -C /lib/modules/2.6.13-study/build M=/home/study/prog/mod/hello/ modules
时,make 去/usr/src/linux/Makefile中寻找目标modules: 862 .PHONY: modules 863 modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) 864 @echo ' Building modules, stage 2.'; 865 $(Q)$(MAKE) -rR -f $(srctree)/scripts/Makefile.modpost
可以看出,分两个stage: 1.编译出hello.o文件。 2.生成hello.mod.o hello.ko 在这过程中,会调用 make -f scripts/Makefile.build obj=/home/study/prog/mod/hello 而在 scripts/Makefile.build会包含很多文件: 011 -include .config 012 013 include $(if $(wildcard $(obj)/Kbuild), $(obj)/Kbuild, $(obj)/Makefile) 其中就有/home/study/prog/mod/hello/Makefile 这时 KERNELRELEASE已经存在。 所以执行的是: obj-m:=hello.o
关于make modules的更详细的过程可以在scripts/Makefile.modpost文件的注释 中找到。如果想查看make的整个执行过程,可以运行make -n。
========
Linux实现字符设备驱动的基础步骤
Linux应用层想要操作kernel层的API,比如想操作相关GPIO或寄存器,可以通过写一个字符设备驱动来实现。
1、先在rootfs中的 /dev/ 下生成一个字符设备。注意主设备号 和 从设备号。可用如下shell脚本生成:
[python] view plain copy
if [ ! -e audioIN ];then
sudo mknod audioIN c 240 0
fi
生成的设备为 /dev/audioIN ,主设备号240,从设备号0。
2、写audioINdriver.ko ,audioINdriver.c 基本代码框架如下:代码中定义了设备名audioIN,设备号240, 0 ,与之前创建的设备一致。
[cpp] view plain copy
/**************************************************************************\
* audioINdriver.c
*
* kang_liu <
[email protected]>
* 2014-07-15
\**************************************************************************/
#include <asm/uaccess.h>
#include <asm/errno.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <mach/gpio.h>
//#include <mach/at91_rstc.h> /* debug */
//#include <mach/at91_pmc.h>
//#include <mach/at91_rstc.h>
//#include <mach/at91_shdwc.h>
#include <mach/irqs.h>
//#include "generic.h"
//#include "clock.h"
#include <mach/w55fa92_reg.h>
#include <asm/io.h>
#define DEV_MAJOR 240
#define DEV_MINOR 0
#define NUM_MINORS 1
#define DEVICE_NAME "audioIN"
#define ERR(fmt, args...) printk(KERN_ALERT __FILE__ ": " fmt, ##args)
#define MSG(fmt, args...) printk(KERN_INFO __FILE__ ": " fmt, ##args)
#define DBG(fmt, args...) printk(KERN_DEBUG __FILE__ ": " fmt, ##args)
static ssize_t user_gpio_read(struct file *fp, char __user *buff,
size_t count, loff_t *offp)
{
char str[32] = {0};
char out[32] = {0};
int n, err;
// printk("lk~~~~~~~read buff = %s\n",buff);
err = copy_from_user(str, buff, count);
// printk("lk~~~~~~~read str = %s\n",str);
if (err)
return -EFAULT;
sprintf(out,"return values");
memset(buff, 0, count);
err = copy_to_user(buff, out, sizeof(out));
if (err)
return -EFAULT;
return n;
}
static ssize_t user_gpio_write(struct file *fp, const char __user *buff,
size_t count, loff_t *offp)
{
int err;
char tmp[32];
// printk("lk~~~~~~~write buff = %s\n",buff);
err = copy_from_user(tmp, buff, count);
// printk("lk~~~~~~~write tmp = %s\n",tmp);
if (err)
return -EFAULT;
if('1' == tmp[0])
{
//LINE IN
printk("line in\n");
}
else if('0' == tmp[0])
{
//MIC IN
printk("mic in\n");
}
return count;
}
static ssize_t user_gpio_open(struct inode *inode,struct file *fp)
{
// printk("open gpio devices\n");
return 0;
}
static struct file_operations user_gpio_file_ops =
{
.owner = THIS_MODULE,
.write = user_gpio_write,
.read = user_gpio_read,
.open = user_gpio_open,
};
static struct cdev *dev;
static void __exit user_audioIN_exit(void)
{
printk("exit audioIN\n");
dev_t devno = MKDEV(DEV_MAJOR, DEV_MINOR);
unregister_chrdev_region(devno, NUM_MINORS);
cdev_del(dev);
return;
}
static int __init user_audioIN_init(void)
{
printk("init audioIN\n");
int err = 0;
int i;
dev_t devno = MKDEV(DEV_MAJOR, DEV_MINOR);
err = register_chrdev_region(devno, NUM_MINORS, DEVICE_NAME);
if (err)
goto fail_devno;
dev = cdev_alloc();
dev->ops = &user_gpio_file_ops;
dev->owner = THIS_MODULE;
err = cdev_add(dev, devno, NUM_MINORS);
if (err)
goto fail_cdev;
return err;
fail_cdev:
fail_devno:
unregister_chrdev_region(devno, NUM_MINORS);
fail_gpio:
return err;
}
module_init(user_audioIN_init);
module_exit(user_audioIN_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kang_liu <
[email protected]>");
MODULE_DESCRIPTION("Access GSEIO from userspace.");
这里就可以调用kernel层的一些API进行底层的操作。
Makefile:生成audioINdriver.ko
[plain] view plain copy
# Comment/uncomment the following line to disable/enable debugging
#DEBUG = y
BUILD_TOOLS_PRE = arm-linux-
CC=$(BUILD_TOOLS_PRE)gcc
LD=$(BUILD_TOOLS_PRE)ld
# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
DEBFLAGS = -O2
endif
KERNEL_DIR = ../../../linux-2.6.35.4
EXTRA_CFLAGS += $(DEBFLAGS)
EXTRA_CFLAGS += -I$(LDDINC)
EXTRA_CFLAGS +=-I$(KERNEL_DIR)/arch/arm/mach-w55fa92/include
EXTRA_CFLAGS +=-I$(KERNEL_DIR)/arch/arm
EXTRA_CFLAGS +=-I$(KERNEL_DIR)/arch/arm/include
EXTRA_CFLAGS +=-I$(KERNEL_DIR)/arch/arm/include/linux
ifneq ($(KERNELRELEASE),)
# call from kernel build system
audioIN-objs := audioINdriver.o
obj-m := audioINdriver.o
else
KERNELDIR ?= $(KERNEL_DIR)
#KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) ARCH=arm CROSS_COMPILE=$(BUILD_TOOLS_PRE) -C $(KERNELDIR) M=$(PWD) LDDINC=$(PWD)/../include modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers
depend .depend dep:
$(CC) $(CFLAGS) -M *.c > .depend
ifeq (.depend,$(wildcard .depend))
include .depend
endif
3. 生成好 .ko 以后,就可以在ARM板上,加载驱动。
insmod audioINdriver.ko
4、加载驱动成功后,就可以在应用层直接操作设备 /dev/audioIN,来实现相关功能,将一些参数传到驱动层,执行相关kernel层的代码。
应用层测试程序如下:
[cpp] view plain copy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#define BUF_LEN 32
int s_audioInFd = 0;
int InitAudioInDevice()
{
s_audioInFd = open("/dev/audioIN",O_RDWR);
if (s_audioInFd > 0)
{
return 1;
}
else
{
printf("Can't open the GSE IO device\n");
return 0;
}
}
void UninitAudioInDevice()
{
if (s_audioInFd > 0)
close(s_audioInFd);
}
int getAudioIn()
{
char buffer[BUF_LEN] = {0};
if (s_audioInFd > 0)
{
memcpy(&buffer[0], "lk_test", 7);
// printf("get buffer = %s\n", buffer);
int len = read(s_audioInFd, buffer, 7);
// printf("get buffer = %s, len = %d\n", buffer, len);
return len;
}
return -1;
}
int setAudioIn(int micLine)
{
char buffer[BUF_LEN] = {0};
if (s_audioInFd > 0)
{
sprintf(buffer, "%d", micLine);
int len = write(s_audioInFd, buffer, sizeof(buffer));
if (len > 0)
return 1;
}
return 0;
}
其中的read 和 write函数,可从驱动中获取一些返回值,也可将字符串传到驱动中。
驱动的入口为:
module_init(user_audioIN_init);
module_exit(user_audioIN_exit);
========
一个简单的Linux字符型设备驱动和应用程序的例子
Linux内核支持可插入式模块,驱动程序可以编译在内核中,也可以编译为内核的模块文件。这点和应用程序差不多。常见的驱动程序是作为模块动态加载的,比如声卡,网卡等。而Linux最基本的驱动,如CPU,PCI总线,TCP/IP,APM,VFS等则直接编译到内核文件中。
下面以一个简单的字符型设备驱动为例:
./a.c
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/errno.h>
#include <asm/segment.h>
#include <asm/uaccess.h>
#define DEVICE_NAME "qq_char"
unsigned int fs_major = 0;
static ssize_t test_read(struct file *file, char *buf, size_t count, loff_t *f_pos);
static int test_open(struct inode *node, struct file *file);
static int test_release(struct inode *inode, struct file *file);
static struct file_operations char_fops =
{
read: test_read,
open: test_open,
release: test_release
};
static ssize_t test_read(struct file *file, char *buf, size_t count, loff_t *f_pos)
{
int len;
for(len = count; len > 0; len--){
put_user(1, buf); // 将数据放入用户空间的buf中
buf++;
}
return count;
}
// 打开设备
static int test_open(struct inode *node, struct file *file)
{
return 0;
}
// 释放设备
static int test_release(struct inode *inode, struct file *file)
{
return 0;
}
// 注册设备
int init_test(void)
{
int res;
res = register_chrdev(0, DEVICE_NAME, &char_fops);
if(res < 0){
printk("can't get major name!\n");
return res;
}
if(fs_major == 0)
fs_major = res;
printk("now, i am in kernel mode!\n");
return 0;
}
// 卸载设备
void cleanup_test(void)
{
printk("now, i am out of kernel\n");
unregister_chrdev(fs_major, DEVICE_NAME);
}
module_init(init_test);
module_exit(cleanup_test);
编译后,如果是2.6版本,则会生成a.ko
执行insmod a.ko后,内核调用module_init(),从而调用register_chrdev()函数注册该设备。
如果注册成功,则会在/proc/devices下面看到DEVICE_NAME,也就是qq_char,以及主设备号和次设备号,这就是注册了的设备。
如果想卸载设备,则rmmod a,内核调用module_exit(),从而调用unregister_chrdev(),卸载掉该设备。
注册完设备之后,因为/proc是个伪文件系统,/proc/devices下面的qq_char并不是真正的设备文件,所以还要将该设备映射为设备文件。
mknod /dev/qq_char c 253 0
生成设备文件后,可以在/dev下面看到qq_char。好了,到现在为止,我们的设备驱动已经做好了。
下面我们用一个简单的应用程序来测试这个驱动。
./aa.c
#include <stdio.h>
#include <sys/types.h> // pid_t
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main(void)
{
int testdev;
int i;
char buf[10];
testdev = open("/dev/qq_char", O_RDWR); // 打开设备qq_char
if(testdev == -1){
printf("cann't open file \n");
exit(0);
}
printf("now read \n");
read(testdev, buf, 10); // 从qq_char读数据到buf中
printf("read end \n");
for(i = 0; i < 10; i++){
printf("%d\n", buf);
}
close(testdev);
return 0;
}
编译,运行该程序,产生如下结果
now read
read end
1
1
1
1
1
1
1
1
1
1
========