一个完整的驱动程序及功能测试

来自个人维基
2015年5月6日 (三) 16:19Hovercool讨论 | 贡献的版本

(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳转至: 导航搜索

目录

驱动程序的编写

一个驱动程序通常包括3个部分:.h头文件、.c文件和Makefile。其中,.h头文件一般会定义一些宏和这个设备相关的结构体;.c文件是驱动功能的实现;Makefile则是对模块生成规则的一些定义。

在这个例子中,我们定义了一个功能非常简单,结构却十分完整的字符型驱动程序。之所以说功能简单,是因为这个设备只能输出一些调试信息、存储一个int型的数据;而结构完整,则是因为它具备了在"/sys/"、"/dev/"、"/proc/"三个目录下生成设备文件的能力,能在"/dev/"下自动挂载,而且还提供了ioctl接口功能。

首先给这个虚拟设备取个名称——"vircdev",然后我们分别来说一下这三个文件:

一、头文件

//vircdev.h

#ifndef _VIRCDEV_H_
#define _VIRCDEV_H_

#define VIRCDEV_NUM (1)

#define VIRCDEV_CLASS_NAME  "vircdev_class"
#define VIRCDEV_DEVICE_NAME	"vircdev_device"
#define VIRCDEV_NODE_NAME   "vircdev_node"
#define VIRCDEV_PROC_NAME	"vircdev_proc"

#define VIRCDEV_IOC_MAGIC   'k'

#define VIRCDEV_IOHI        _IO(VIRCDEV_IOC_MAGIC, 0)
#define VIRCDEV_IOWBYTE     _IOW(VIRCDEV_IOC_MAGIC, 1, int)
#define VIRCDEV_IORBYTE     _IOR(VIRCDEV_IOC_MAGIC, 2, int)

struct vircdev_cntx {
    int r1;
    struct semaphore sem; 
	struct cdev cdev;
};

#endif

这里首先是定义了设备的个数,因为linux中设备是由一个主设备号和一个副设备号区分的,而系统只会区分主设备号,并在一些通用文件访问(如read、write)时转向设备对应的驱动程序,而副设备号系统则是完全漠视,直接传给驱动程序,由驱动进行处理的,这里的VIRCDEV_NUM则代表了设备的个数(自然也决定了副设备号的个数)。

接着,是定义了一些名称,这些名称将会在创建设备文件时使用。

而接下来这些宏,如VIRCDEV_IOHI,则是用于ioctl中的cmd命令;VIRCDEV_IOC_MAGIC是我们通常所说的幻数,这个数的定义要比较小心,一般是取系统中未使用的,以确保ioctl中cmd命令的唯一性,这样在对错误的文件使用这些命令时便不会出现不可预知的错误。

二、vircdev.c文件

1、全局变量的定义:

/*====global====*/
static int g_vircdev_major = 0;
static int g_vircdev_minor = 0;

struct vircdev_cntx *g_vircdev_ptr = NULL;

struct class *g_vircdev_class = 0;

static struct file_operations g_vircdev_fops = {
    .owner = THIS_MODULE,
    .read = vircdev_read,
    .write = vircdev_write, 
    .unlocked_ioctl = vircdev_unlocked_ioctl,
    .open = vircdev_open,
    .release = vircdev_release,
};

g_vircdev_major、g_vircdev_minor为主副设备号;g_vircdev_ptr为设备结构体变量指针;g_vircdev_class是在构建/dev/设备文件时需要保留的一个struct class中间变量,在exit时要使用这个变量进行destroy;g_vircdev_fops,这个就不用过多解释了,设备的大部分操作都在这里面。


2、定义传统设备文件的访问方法,主要是定义vircdev_read、vircdev_write、vircdev_unlocked_ioctl、vircdev_open和vircdev_release:

ssize_t vircdev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    printk(KERN_ALERT "vircdev read.\n");
    return 0;
}

ssize_t vircdev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    printk(KERN_ALERT "vircdev write.\n");
    return count;
}

long vircdev_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    int err = 0;
    long retval = 0;
    
	/*
	 * extract the type and number bitfields, and don't decode
	 * wrong cmds: return ENOTTY (inappropriate ioctl) before access_ok()
	 */
	if (_IOC_TYPE(cmd) != VIRCDEV_IOC_MAGIC) return -ENOTTY;
	//if (_IOC_NR(cmd) > VIRCDEV_IOC_MAXNR) return -ENOTTY;

	/*
	 * to verify *arg is in user space
	 */
	if (_IOC_DIR(cmd) & _IOC_READ)
		err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
	else if (_IOC_DIR(cmd) & _IOC_WRITE)
		err =  !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
	if (err) return -EFAULT;
    
    switch(cmd)
    {
        case VIRCDEV_IOHI:
            printk(KERN_ALERT "ioctl:you said hi.\n");
            break;
            
        case VIRCDEV_IOWBYTE:            
            //retval = __get_user(g_vircdev_ptr->r1, (int __user *)arg);
            g_vircdev_ptr->r1 = (int __user *)arg;
            printk(KERN_ALERT "ioctl:write r1 to %i.\n", g_vircdev_ptr->r1);
            break;

        case VIRCDEV_IORBYTE:                     
            retval = __put_user(g_vircdev_ptr->r1, (int __user *)arg);
            printk(KERN_ALERT "ioctl:read r1:%i.\n", g_vircdev_ptr->r1);
            break;

        default:
            printk(KERN_ALERT "ioctl:you got the wrong command.\n");
            break;
    }
    
    return retval;
}

int vircdev_open(struct inode *inode, struct file *filp) {
    return 0;
}

int vircdev_release(struct inode *inode, struct file *filp) {
    return 0;
}


3、动态分配设备号函数:

//alloc device major
static int vircdex_alloc_major(void) {
    dev_t devt = 0;
    int result = 0;
    
    result = alloc_chrdev_region(&devt, g_vircdev_minor, VIRCDEV_NUM, VIRCDEV_NODE_NAME);
    g_vircdev_major = MAJOR(devt);
    
    return result;
}
static int vircdev_release_major(void) {
    dev_t devt = MKDEV(g_vircdev_major, g_vircdev_minor);
    unregister_chrdev_region(devt, 1);
    return 0;
}


4、字符设备安装函数:

static int vircdev_setup_dev(struct vircdev_cntx *dev) {
    int err, devno = MKDEV(g_vircdev_major, g_vircdev_minor);
    
    cdev_init(&(dev->cdev), &g_vircdev_fops);
    dev->cdev.owner = THIS_MODULE;
    err = cdev_add(&dev->cdev, devno, 1);
    if(err){   
        return err;
    }
    
    //init_MUTEX(&(dev->sem));
    sema_init(&(dev->sem), 1);
    
    return 0;
}
static int vircdev_unsetup_dev(struct vircdev_cntx *dev) {
    cdev_del(&(dev->cdev));
    return 0;
}


5、创建设备文件函数:

static int vircdev_create_devfiles(dev_t devt) {//, const struct device_attribute *attr) {
    int err = -1;    
    struct device *dev = NULL;
                          
    g_vircdev_class = class_create(THIS_MODULE, VIRCDEV_CLASS_NAME);
    if(IS_ERR(g_vircdev_class)) {  
        err = PTR_ERR(g_vircdev_class);  
        printk(KERN_ALERT "Failed to create class.\n");  
        goto CLASS_CREATE_ERR;  
    }
    
    dev = device_create(g_vircdev_class, NULL, devt, NULL, VIRCDEV_DEVICE_NAME);
    //dev = device_create(hello_class, NULL, dev, "%s", HELLO_DEVICE_FILE_NAME);  
    //device_create( my_class, NULL, MKDEV(hello_major, 0), "hello" "%d", 0 );
    //dev = device_create(g_vircdev_class, NULL, MKDEV(MYDRIVER_Major, 0), NULL, DEVICE_NAME);
    if(IS_ERR(dev)) {  
        err = PTR_ERR(dev);  
        printk(KERN_ALERT "Failed to create device.\n");  
        goto DEVICE_CREATE_ERR;  
    }
    
    /*err = device_create_file(dev, attr);  
    if(err < 0) {  
        printk(KERN_ALERT"Failed to create attribute file.");                  
        goto DEVICE_CREATE_FILE_ERR;  
    }*/          
    printk(KERN_ALERT "seems ok.\n"); //zmk@@debug    
    
    return 0;
    
DEVICE_CREATE_FILE_ERR:
    device_destroy(g_vircdev_class, devt);
DEVICE_CREATE_ERR:
    class_destroy(g_vircdev_class);             
CLASS_CREATE_ERR:  
    return err; 
}
static int vircdev_delete_devfiles(dev_t devt) {
    device_destroy(g_vircdev_class, devt);
    class_destroy(g_vircdev_class);
    //device_remove_file(dev, attr);
    return 0;
}


6、创建/proc调试文件函数:

static int vircdev_read_proc(char *buf, char **start, off_t offset,
                            int count, int *eof, void *data)
{
    int len =sprintf(buf, "vircdev read proc.\n");
    return len;
}
static int vircdev_create_proc_file(void) {
    struct proc_dir_entry *entry = NULL;
 
    entry = create_proc_read_entry(VIRCDEV_PROC_NAME, 0,
                            NULL, vircdev_read_proc,
                            NULL);
    if(entry)
    {
        return 0;
    }
    else                            
    {                        
        return -1;                       
    }                       
}
static int vircdev_delete_proc_file(void) {
    remove_proc_entry(VIRCDEV_PROC_NAME, NULL);
    return 0;
}


7、设备驱动模块的加载和释放:

MODULE_LICENSE("GPL");

static int vircdev_init(void) {
    int err = -1;
    dev_t devt = 0;
    
    //[1] alloc node number
    err = vircdex_alloc_major();
    if(0 > err)
    {
        printk(KERN_ALERT"alloc major failed.\n");
        goto ALLOC_MAJOR_ERR;
    }
    devt = MKDEV(g_vircdev_major, g_vircdev_minor);
    
    //[2] device object init    
    g_vircdev_ptr = kmalloc(sizeof(struct vircdev_cntx), GFP_KERNEL);  
    if(!g_vircdev_ptr) {  
        err = -ENOMEM;  
        printk(KERN_ALERT"kmalloc failed.\n");  
        goto KMALLOC_ERR;  
    }
    memset(g_vircdev_ptr, 0, sizeof(struct vircdev_cntx));
    
    //[3] setup device
    err = vircdev_setup_dev(g_vircdev_ptr);
    if(0 > err)
    {
        printk(KERN_ALERT"device setup failed.\n");
        goto DEVICE_SETUP_ERR;
    }
    
    //[4] create files in directory "/dev/" and "/sys/" 
    ///err = vircdev_create_devfiles(devt, attr);
    err = vircdev_create_devfiles(devt);
    if(0 > err)
    {
        printk(KERN_ALERT"devfiles create failed.\n");
        goto DEVFILES_CREATE_ERR;
    }

    //[5] create proc file
    err = vircdev_create_proc_file();
    if(0 > err)
    {
        printk(KERN_ALERT"proc file create failed.\n");
        goto PROC_FILE_CREATE_ERR;
    }
        
    return 0;
    
PROC_FILE_CREATE_ERR:
    vircdev_delete_devfiles(devt);
DEVFILES_CREATE_ERR:
    vircdev_unsetup_dev(g_vircdev_ptr);
DEVICE_SETUP_ERR:
    kfree(g_vircdev_ptr); 
ALLOC_MAJOR_ERR:
    vircdev_release_major();
KMALLOC_ERR:
    return err;
}
static void vircdev_exit(void) {
    dev_t devt = MKDEV(g_vircdev_major, g_vircdev_minor);
    
    vircdev_delete_proc_file();
    vircdev_delete_devfiles(devt);
    vircdev_unsetup_dev(g_vircdev_ptr);
    kfree(g_vircdev_ptr); 
    vircdev_release_major();
}

module_init(vircdev_init);
module_exit(vircdev_exit);

三、Makefile

 obj-m := vircdev.o
 KERNELDIR := /lib/modules/$(shell uname -r)/build
 PWD := $(shell pwd)
 modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
 modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

关于Makefile,在《驱动模块中Makefile的自我理解》一文中有过说明,这里不再作过多解释。

四、验证

1.进入vircdev文件夹,编译:

$:cd vircdev
$:make

查看是否生成.ko文件:

$:ls

结果:

Makefile       Module.symvers  vircdev.h   vircdev.mod.c  vircdev.o
modules.order  vircdev.c       vircdev.ko  vircdev.mod.o

2.载入模块:

$:sudo insmod ./vircdev.ko

查看模块是否载入成功:

$:lsmod |grep vir

结果:

vircdev                12980  0

3.分别使用cat、echo命令验证驱动模块的open、write、read和close功能:

$:cat /dev/vircdev_device
$:echo abcd > /dev/vircdev_device
$:dmesg |grep vircdev

结果:

[34058.091485] vircdev read.
[34134.980198] vircdev write.

4.验证/proc调试功能:

$:cat /proc/vircdev_proc

结果:

vircdev read proc.


附:驱动程序源文件下载:文件:Vircdev.zip


ioctl测试程序的编写

鉴于read、write函数已经通过cat、echo验证ok了,现在这里编写的验证函数主要是为了验证ioctl函数。

//vircdev_ioctl_test.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <asm/ioctls.h>

#define DEVICE_NAME "/dev/vircdev_device"

#define VIRCDEV_IOC_MAGIC   'k'

#define VIRCDEV_IOHI        _IO(VIRCDEV_IOC_MAGIC, 0)
#define VIRCDEV_IOWBYTE     _IOW(VIRCDEV_IOC_MAGIC, 1, int)
#define VIRCDEV_IORBYTE     _IOR(VIRCDEV_IOC_MAGIC, 2, int)

int main(int argc, char *argv[])
{
    char cmd = *argv[1];
    int arg = *(argv[1]+1) - 48;
    int fd = 0;

    printf("cmd = %c, arg = %d\n", cmd, arg);

    fd = open(DEVICE_NAME, O_RDWR);
    switch(cmd)
    {
        case 'h':
            ioctl(fd, VIRCDEV_IOHI, arg);
            break;

        case 'w':
            ioctl(fd, VIRCDEV_IOWBYTE, arg);
            break;

        case 'r':
            ioctl(fd, VIRCDEV_IORBYTE, &arg);
            printf("read value:%d\n", arg);
            break;

        default:
            break;
    }

    close(fd);
    return 0;
}

函数首先是定义了之前在驱动模块vircdev.h中定义过的宏,这里只是拷贝过来,不过为规范起见,建议是直接将该头文件包含过来,另外由于使用了_IOW等宏,要包含"asm/ioctls.h"头文件。

这个函数是取得用户在命令行输入的命令,然后控制对应的ioctl:

h -- VIRCDEV_IOHI,使设备驱动打印消息"ioctl:you said hi."

w -- VIRCDEV_IOWBYTE,写vircdev设备中的r1寄存器

r -- VIRCDEV_IORBYTE,读vircdev设备中的r1寄存器

编译:

$:gcc gcc vircdev_ioctl_test.c

读寄存器(自然前提是vircdev驱动模块已载入到系统):

$:./a.out r1
cmd = r, arg = 1
read value:9

写寄存器:

$:./a.out w5
cmd = w, arg = 5

再读出当前寄存器的值:

$:./a.out r1
cmd = r, arg = 1
read value:5

与写入值相等,sucess!!!

vircdev_ioctl_test.c下载:文件:Vircdev ioctl test.c.zip