从头实现Linux字符设备驱动——2万字详解

字符设备驱动基础

参考:https://blog.csdn.net/zqixiao_09/article/details/50839042

Linux中有很多设备,主要分为三类:字符设备、块设备、网络设备。

重点学习字符设备,字符设备是以字节流的方式驱动的,典型的字符设备是LCD、键盘……

Linux中一切皆文件,如何去操作驱动呢?在应用层中通过文件IO来操作驱动,比如open()打开设备、write()向驱动写字节,即用户向驱动操作就是文件IO。用户层中调用open()、write()操作驱动,对应的在内核中实现了专门的函数提供给用户层中的操作。

问题就来了!!!

1.那么多设备,用户层中是怎么确定操作的是哪一个设备?
实际每一个设备都对应着一个设备节点,比如/dev/led就是一个LED灯的设备节点。

2.如果有多个相同类型的设备,多个设备共用一个设备节点,那要如何精确到某一个设备呢?

通过设备号来区分,每一个设备在内核中都维护一个设备号,设备号是内核区分不同设备的唯一信息,分为主设备号和次设备号,主设备号区分一类设备,次设备号区分同一类设备中的不同个设备。

3.内核是如何实现对驱动设备的操作方法?

内核通过一系列操作方法来实现对驱动设备的操作,这些操作方法封装在struct file_operations结构体中。

内核使用cdev结构体来描述一个字符设备,cdev的定义如下:

<include/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;                   //隶属于同一主设备号的次设备号的个数.
};

就是通过cdev的成员struct file_operations所关联的操作方法来实现,对驱动设备的操作。

4.如何通过打开设备节点来绑定操作方法?

mknod将设备节点文件名、文件类型(驱动设备类型)、设备号等信息保存在磁盘上。

第一次的时候使用open()文件IO打开设备节点,里面过程比较复杂,要关注的是通过do_file_open()来构造一 个file结构体,并初始化相关成员,这里会将file的f_op成员指向file_operations。

do_filp_open()会将mknod保存在磁盘上的信息读出来,填充到内存中的inode结构的相关成员中;然后根据设备号找到添加在内核中代表字符设备的cdev,用cdev关联的file_operations操作方法替代之前初始化file结构体中的操作方法,然后调用cdev中file_operations中的打开函数,真正的完成设备打开操作,到这里就标志着do_filp_open()的结束。

虽然打开设备文件的操作很繁琐,但是打开操作会返回一个文件描述符,之后再操作驱动的时候,都是以这个文件描述符为参数传递给内核,内核得到文件描述符之后可以直接索引fd_array,找到对应的file结构体,然后调用对应的操作方法

open(“/dev/led”);—>设备文件路径名——>创建file——>从磁盘中提取信息放在inode——>根据设备号找到cdev——>操作方法

在这里插入图片描述

所以!!!对于字符驱动设备来讲,设备号、cdev、操作方法集合是至关重要的,在打开一个设备的时候,内核找到设备文件路径对于的inode之后,要和驱动建立连接。首先就是根据inode中的设备号找到cdev,然后根据cdev找到关联的操作方法集合,从而调用驱动提供的操作方法来实现具体驱动的操作。可以说字符设备的驱动框架就是围绕设备号、cdev和操作方法集合来实现的。

申请设备号

设备号是向系统申请的,在模块加载的入口函数中进行设备号申请,申请设备号的函数是regsiter_chrdev(),函数原型如下:

int register_chrdev(unsigned int major, const char * name, const struct file_operations * fops);

/* 
 * major:设备号(32bit--dev_t)==主设备号(12bit) + 次设备号(20bit)
 * name:描述一个设备信息,可以自定义。在/proc/devices 下可以查看已定义的设备
 * fops:文件操作对象,提供
 * 返回值: 正确返回0,错误返回负数
 */

申请设备号分为静态申请和动态申请,major直接填0就是动态申请,系统会分配一个设备号;或者可以指定一个整数来作为设备号,注意系统中可能已经占用了一些设备号。

有申请设备号,在卸载模块的时候,就要释放设备号,如下:

void unregister_chrdev(unsigned int major, const char * name);

实例代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>

static unsigned int major = 250;  //全局,以便于申请和释放

struct file_operations chr_dev_fops = {
	
};

static int __init chr_dev_init(void)
{
	int ret;
	ret = regsiter_chrdev(major, "guquan_dev", &chr_dev_fops); //申请设备号
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
	
	return 0;
}

static void __exit chr_dev_exit(void)
{
	unregister_chrdev(major, "guquan_dev"); //释放设备号
}


module_init(chr_dev_init);
module_exit(chr_dev_exit);

MODULE_LICENSE("GPL");

这里申请设备号,只是向内核注册了cdev

可以通过查看/proc/devices来获取内核注册了哪些设备号。

创建设备节点

创建设备节点也有两种方式:

  • 手动创建,通过命令mknod创建设备节点,如mknod /dev/led c 250 0
  • 通过udev/mdev机制,自动创建

可以通过ls /dev查看已创建的设备节点。

手动创建时,需要在命令行下执行,有一个缺点,因为/dev目录下的文件存放在内存中,断点会丢失,所以板子重新启动之后就不会自动创建设备节点。

我们更希望在模块注册的入口函数中创建设备节点,这样每次加载模块,都可以自动创建设备节点,通过以下函数进行自动创建:

struct class *class_create(owner, name);
/* 
 * 创建一个类,返回一个指向类的指针
 * owner:THIS_MODULE,相当于this指针
 * name:字符串名字,用户自定义
 * 返回一个class指针
*/

struct device *device_create(struct class * class, struct device * parent, dev_t devt, 
							void * drvdata, const char * fmt,...);

/* 
 * 创建一个设备,使用了面向对象思想
 * class:通过class_create()调用之后的返回值
 * parent:表示父亲设备,这里用到了面向对象的概念,一边填NULL
 * devt:设备号类型 dev_t devt
					#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
					#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
					#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))
 * drvdata:私有数据,一般NULL
 * fmt,...:可变参数,是一个字符串,设备节点的名字
 */

是先创建一个类,然后根据类来创建一个设备节点。既然有创建,在卸载模块的时候就要销毁设备节点,用到的函数如下:

void device_destroy(devcls,  MKDEV(dev_major, 0));
//参数1: class结构体,class_create调用之后到返回值
//参数2: 设备号类型 dev_t

void class_destroy(devcls);
//参数1: class结构体,class_create调用之后到返回值

注意!!!创建设备节点的时候,先创建类,然后创建设备;销毁设备节点的时候,先销毁设备,然后销毁类。

实例代码如下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>

#define MINORBITS 20

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))


static unsigned int major = 250;  //全局,以便于申请和释放

static struct class *devcls;
static struct device *dev;

struct file_operations chr_dev_fops = {
	
};

static int __init chr_dev_init(void)
{
	int ret;
	ret = register_chrdev(major, "guquan_dev", &chr_dev_fops); //申请设备号
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
	
	//创建设备节点
	devcls = class_create(THIS_MODULE, "whocare");
	dev = device_create(devcls, NULL, MKDEV(major, 0), NULL, "guquan_dev_name");
	
	return 0;
}

static void __exit chr_dev_exit(void)
{
	unregister_chrdev(major, "guquan_dev"); //释放设备号
	
	//销毁设备节点 注意销毁顺序与创建时相反
	device_destroy(devcls,  MKDEV(major, 0));
    class_destroy(devcls);
}

module_init(chr_dev_init);
module_exit(chr_dev_exit);

MODULE_LICENSE("GPL");

这样,使用insmod加载模块之后,在入口函数中会自动创建设备号和创建设备节点。

在驱动中实现操作方法

根据上面字符设备驱动基础中所讲,用户层对设备驱动节点的IO操作,在struct file_operations中会有对应的对驱动的操作。所以我们要关注两个点:

  • 用户如何通过文件IO调用struct file_operations中的操作方法
  • struct file_operations中的操作方法是如何实现的

struct file_operations的设计思想是面向对象的,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 (*iterate) (struct file *, struct dir_context *);
    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);
    int (*show_fdinfo)(struct seq_file *m, struct file *f);
}; //函数指针的集合,其实就是接口,我们写驱动到时候需要去实现

下面说一下如何实现,首先要声明操作方法,比如实现下面几个操作:

ssize_t my_read (struct file *, char __user *, size_t, loff_t *);
ssize_t my_write (struct file *, const char __user *, size_t, loff_t *);
int my_open (struct inode *, struct file *);
int release (struct inode *, struct file *);

ssize_t my_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}
ssize_t my_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;  
}
int my_open (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}
int my_release (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}

当方法实现之后,在struct file_operations中指定方法:

struct file_operations my_fops = {
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

实例如下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>

#define MINORBITS 20

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))


static unsigned int major = 250;  //全局,以便于申请和释放

static struct class *devcls;
static struct device *dev;

ssize_t my_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}
ssize_t my_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;  
}
int my_open (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}
int my_release (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}

struct file_operations my_fops = {
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

static int __init chr_dev_init(void)
{
	int ret;
	//申请设备号,实际是注册cdev结构体,实现file_operations操作方法
	ret = register_chrdev(major, "guquan_dev", &my_fops); 
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
	
	//创建设备节点
	devcls = class_create(THIS_MODULE, "whocare");
	dev = device_create(devcls, NULL, MKDEV(major, 0), NULL, "guquan_dev_name");
	
	return 0;
}

static void __exit chr_dev_exit(void)
{
	unregister_chrdev(major, "guquan_dev"); //释放设备号
	
	//销毁设备节点 注意销毁顺序与创建时相反
	device_destroy(devcls,  MKDEV(major, 0));
    class_destroy(devcls);
}


module_init(chr_dev_init);
module_exit(chr_dev_exit);

MODULE_LICENSE("GPL");

文件IO调用驱动中的操作

上面字符设备驱动基础中说了,文件IO调用操作方法之前,要先打开设备节点,也就是通过open()打开设备节点,这个过程很复杂,其主要作用就是创建file结构体,根据创建设备节点时写入磁盘中的信息来填充inode,然后通过设备号找到内核中注册的cdev结构体,用cdev中定义的操作方法集合替代file结构体中原来初始化时的操作方法集合。

所以文件IO调用操作方法的前提就是,使用open()打开设备节点。

实例如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(int argc, const char *argv[])
{
	int buf[1024] = {0};
	int ret;
	int fd = open("/dev/guquan_dev_name", O_RDWR);
	if (fd < 0) {
		perror("open");
		exit(1);
	}
	
    //调用驱动中的操作 测试
	read(fd, buf, 4); 
	write(fd, buf, 4);
	close(fd);
	
	return 0;
}

直接在Makefile中修改,通过make管理测试代码的编译与拷贝到板子:

ROOTFS_DIR = /nfs/rootfs
#挂载根文件系统的目录
APP_NAME = test
CROSS_COMPILE = arm-none-linux-gnueabi-
CC = $(CROSS_COMPILE)gcc

ifeq ($(KERNELRELEASE), )  #默认为空

KERNEL_DIR = /home/gq/linux-3.14.24
#内核路径:/home/gq/linux-3.14.24

CUR_DIR = $(shell pwd)
#通过执行shell命令pwd获取当前路径

all:
    make -C $(KERNEL_DIR) M=$(CUR_DIR) modules 
 #-C代表进入到内核,即进入到内核路径,会读取内核源码顶层目录中Makefile,
#顶层Makefile中会给KERNELRELEASE赋版本号
#M=$(CUR_DIR) 用来指定模块的位置,内核会按照自己的规则来编译指定路径下的文件
#这里内核还不知道要将哪个文件编译为模块,所以会重新执行一次ifeq,这次就跳到else了
#所以这个Makefile会被读取两次:第一次是执行make的时候,第二次是在内核源码中的Makfeile读取
#modules 表示将文件编译为
    $(CC) $(APP_NAME).c -o $(APP_NAME)
clean:
    make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
    
install:
    cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)

else

obj-m += dev.o
#指定要编译的文件,并且要编译成modules
#再增加文件的时候,只需要修改这里即可

endif

执行效果如下:

在这里插入图片描述

注意!!!这里我只是实现了通过文件IO打开设备节点,测试了相关操作方法的执行,并没有实现与驱动的实际操作。

应用程序与驱动的数据交互

上面实现了在应用程序中通过文件IO调用驱动中的操作,实际开发中,不仅仅是简单的调用,还会涉及到数据的交互。

应用程序在用户空间,驱动在内核空间,应用程序与驱动的数据交互就是用户空间与内核空间的数据交互,我们通过如下两个函数实现数据交互:

#include <asm/uaccess.h>
int copy_to_user(void __user * to, const void * from, unsigned long n); //从内核拷贝到用户
int copy_from_user(void * to, const void __user * from, unsigned long n); //从用户拷贝到内核
//返回值>0是代表出错,此时返回值的大小就是剩余拷贝的个数
//返回值=0 表示成功

下面就使用这两个函数进行应用程序域驱动的一个简单的数据交互,在驱动中完善read和write调用:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>

#define MINORBITS 20

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

static int kernel_value = 123;

static unsigned int major = 250;  //全局设备号,以便于申请和释放

static struct class *devcls;
static struct device *dev;

ssize_t my_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
	int ret = copy_to_user(buf, &kernel_value, 4);
	if (ret > 0) {
		printk("my_read filed\n");
		return ret;
	}
	printk("my_read is called, read from kernel successful:%d\n", kernel_value);
    
    return 0;
}
ssize_t my_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
	
	int ret = copy_from_user(&kernel_value, buf, 4);
	if (ret > 0) {
		printk("my_write filed\n");
		return ret;
	}
	printk("my_write is called, wirte to kernel successful:%d\n", kernel_value);
    
    return 0;  
}
int my_open (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}
int my_release (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    
    return 0;
}

struct file_operations my_fops = {
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

static int __init chr_dev_init(void)
{
	int ret;
	//申请设备号,实际是注册cdev结构体,实现file_operations操作方法
	ret = register_chrdev(major, "guquan_dev", &my_fops); 
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
	
	//创建设备节点
	devcls = class_create(THIS_MODULE, "whocare");
	dev = device_create(devcls, NULL, MKDEV(major, 0), NULL, "guquan_dev_name");
	
	return 0;
}

static void __exit chr_dev_exit(void)
{
	unregister_chrdev(major, "guquan_dev"); //释放设备号
	
	//销毁设备节点 注意销毁顺序与创建时相反
	device_destroy(devcls,  MKDEV(major, 0));
    class_destroy(devcls);
}


module_init(chr_dev_init);
module_exit(chr_dev_exit);

MODULE_LICENSE("GPL");

测试程序如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(int argc, const char *argv[])
{
	int buf = 666;
	int ret;
	int fd = open("/dev/guquan_dev_name", O_RDWR);
	if (fd < 0) {
		perror("open");
		exit(1);
	}
	
	read(fd, &buf, 4);
	++buf;
	write(fd, &buf, 4);
	
	close(fd);
	
	return 0;
}

执行结果如下,实现了从驱动读取数据,自增之后再写入驱动:
在这里插入图片描述

这样,我们就实现了用户空间与内核空间的数据交互。

内核驱动如何控制外设

驱动如何控制外设?大多数外设的驱动都是通过读写寄存器的方式操作的,也就是读写寄存器所在的地址,即读写物理地址。

内核驱动可以直接操作物理地址吗?不可以,MMU会把物理地址映射为虚拟地址,程序可以操作的地址都是虚拟地址。

内核如何访问物理地址?通过MMU,把物理地址映射在虚拟地址中,通过访问虚拟地址来操作实际的物理地址。

所以!!!内核驱动外设实际上就是通过将外设的物理地址映射到虚拟地址上,然后在内核的驱动程序中访问虚拟地址,从而操作外设(物理地址)。

物理地址到虚拟地址的映射可以通过ioremap()来实现,我们可以在驱动入口函数中建立地址映射,在卸载模块的时候解除映射关系,用到的函数如下:

#include <asm/io.h>
void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void *ioremap(unsigned long phys_addr, unsigned long size);

/* 
 * phys_addr:要映射的物理地址,通常是外设的寄存器地址
 * size:映射的地址长度,以Byte为单位
 * flags:要映射的IO空间的和权限有关的标志
 * 返回值:映射之后的虚拟地址,通过操作虚拟地址可以实现对物理地址的操作
 */

void iounmap(void * addr);
//用来解除地址的映射关系,参数addr为映射之后的虚拟地址

控制LED的简单驱动实例

上面说了,驱动控制外设的方法就是建立地址映射,将外设寄存器地址映射在虚拟地址中,供驱动程序操作,这里尝试控制LED。

根据原理图以及芯片手册,可以查到LED的相关寄存器信息:

//led引脚:GPX2_7 高电平点亮,高电平导通三极管
GPX2CON ==0x11000C40
GPX2DAT ==0x11000C44  

volatile unsigned int *led0_con = NULL;
volatile char *led0_dat = NULL;
led0_con = ioremap(0x11000c40, 4);
led0_dat = ioremap(0x11000c44, 1);
*led0_con |= (0x1<<28);
*led0_dat |= (1<<7);
*led0_dat &= ~(1<<7);

在这里插入图片描述

驱动代码:

ssize_t my_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
	
	int ret = copy_from_user(&kernel_value, buf, 4);
	if (ret > 0) {
		printk("my_write filed\n");
		return ret;
	}
	printk("my_write is called, wirte to kernel successful:%d\n", kernel_value);
	
	if (kernel_value) *led0_dat |= (1<<7);  //亮
	else *led0_dat &= ~(1<<7); //灭
	
    
    return 0;  
}

static int __init chr_dev_init(void)
{
	int ret;
	//申请设备号,实际是注册cdev结构体,实现file_operations操作方法
	ret = register_chrdev(major, "guquan_dev", &my_fops); 
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
	
	//创建设备节点
	devcls = class_create(THIS_MODULE, "whocare");
	dev = device_create(devcls, NULL, MKDEV(major, 0), NULL, "guquan_dev_name");
	
    //LED寄存器地址映射
	led0_con = ioremap(0x11000c40, 4);
	led0_dat = ioremap(0x11000c44, 1);
	*led0_con |= (0x1<<28);
	
	return 0;
}

static void __exit chr_dev_exit(void)
{
	//解除地址映射
	iounmap(led0_con);
	iounmap(led0_dat);
	
	unregister_chrdev(major, "guquan_dev"); //释放设备号
	
	//销毁设备节点 注意销毁顺序与创建时相反
	device_destroy(devcls,  MKDEV(major, 0));
    class_destroy(devcls);
	
}

测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, const char *argv[])
{
	int buf = 666;
	int ret;
	int fd = open("/dev/guquan_dev_name", O_RDWR);
	if (fd < 0) {
		perror("open");
		exit(1);
	}
	
	read(fd, &buf, 4);
	++buf;
	write(fd, &buf, 4);
	
	while (1)
	{
		sleep(1);
		buf = 1;
		write(fd, &buf, 4);
		sleep(1);
		buf = 0;
		write(fd, &buf, 4);
	}
	
	close(fd);
	
	return 0;
}

在这里插入图片描述

虽然实现了从应用程序到驱动,再从驱动到外设,但是这样的驱动程序健壮性不好,由以下几点可以看出:

  • 驱动中用到了大量的全局变量,不好管理
  • 没有很完善地处理出错信息
  • 驱动程序的框架没有规范化

下面就针对这些点来改进驱动程序。

驱动程序的改进

框架复盘

首先来复盘一下字符设备驱动框架的流程:

	1,实现模块加载和卸载入口函数
        module_init(chr_dev_init);
        module_exit(chr_dev_exit);
	
	2,在模块加载入口函数中
		a, 申请设备号,也就是在内核中注册cdev (内核通过设备号区分设备)
				 register_chrdev(dev_major, "chr_dev_test", &my_fops);

		b,创建设备节点文件 (为用户提供一个可操作到文件接口--open())
				struct  class *class_create(THIS_MODULE, "chr_cls");
				struct  device *device_create(devcls, NULL, MKDEV(dev_major, 0), NULL, "chr2");

		c, 硬件的初始化
			   1,寄存器地址的映射
					gpx2conf = ioremap(GPX2_CON, GPX2_SIZE);
			   2,中断到申请
			   3,实现硬件的寄存器到初始化
					// 需要配置gpio功能为输出
					*gpx2conf &= ~(0xf<<28);
					*gpx2conf |= (0x1<<28);
		e,实现file_operations 操作方法
				const struct file_operations my_fops = {
						.open = chr_drv_open,
						.read = chr_drv_read,
						.write = chr_drv_write,
						.release = chr_drv_close,
				};

面向对象思想

在驱动中,经常会用到许多全局变量,这些全局变量是表示设备的相关信息的,比如设备号、外设寄存器、创建设备节点所需的class、device结构体等。我们可以根据面向对象编程的思想,将这些全局变量封装在一个结构体中,抽象为一个设备对象。

将所需要的全局变量封装在结构体中,抽象为对象,如下:

struct led_desc
{
	//设备号
	unsigned int dev_major;
	//创建设备节点所需的结构
	struct class *devcls;  
	struct device *dev;
	//映射后的寄存器基址
	void *reg_virt_base;

};

struct led_desc *led_dev = NULL; //声明一个全局设备对象 

然后在入口函数中实例化对象,也就是向堆申请空间:

//GFP_KERNEL表示如果当前内存不够用,函数会一直阻塞  头文件<linux/slab.h>
led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL); 
if (led_dev == NULL) {
    printk(KERN_ERR "malloc error\n");  //可以通过KERN_ERR筛选调试信息
    return -ENOMEM;
}

出错处理

注意!!!是先实例化的对象,然后初始化对象的,如果在初始化的时候出现错误,退出的时候记得释放实例化对象时申请的内存空间,防止内存泄漏!!!这是必须要注意到的,然后就是每执行一步,都有可能出错,那就需要将之前的每一步所申请的数据结构全都释放!!!比如申请的对象的空间、设备号、设备节点、class、device等。

利用程序执行流,逐级执行出错处理操作!!!可以通过goto语句在出错后跳转到处理代码中去执行,注意出错处理的代码放在正常执行的return之后。

在检查错误的时候,内核提供了一些宏定义来协助完成:

  • 内核提供了一个宏定义来专门判断指针,即IS_ERR()
  • 提供了指针出错的具体原因的宏定义,即PTR_ERR()
  • 在打印出错信息的时候,支持标签打印,例如printk(KERN_ERR "class_create filed\n");,程序员可以根据KERN_ERR 标签来过滤调试信息;

所以在模块注册的入口函数中,可以做如下修改:

static int __init chr_dev_init(void)
{
	int ret = 0;
	//实例化对象
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL); //GFP_KERNEL表示如果当前内存不够用,函数会一直阻塞  头文件<linux/slab.h>
	if (led_dev == NULL) {
		printk(KERN_ERR "malloc error\n");  //可以通过KERN_ERR筛选调试信息
		return -ENOMEM;
	}
	
	//申请设备号
#if 1  /* 动态申请设备号 */
	//major为0是动态申请设备号,并返回设备号
	led_dev->dev_major = register_chrdev(0, "guquan_dev", &my_fops); 
	if (led_dev->dev_major < 0) {
		printk(KERN_ERR "regsiter error\n");
		ret = -ENODEV;
		goto err_0;  //释放对象空间
	} else printk("regsiter successful\n");
#else /* 静态申请设备号 */
	//注册cdev结构体,实现file_operations操作方法
	int ret = register_chrdev(major, "guquan_dev", &my_fops); 
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
#endif /* 动态申请设备号 */

	//创建设备节点
	led_dev->devcls = class_create(THIS_MODULE, "do_not_care");
	if (IS_ERR(led_dev->devcls)) {
		printk(KERN_ERR "class_create error\n");
		ret = PTR_ERR(led_dev->devcls);  //返回指针出错的具体原因
		goto err_1;  //注销设备号、释放空间
	}
	led_dev->dev = device_create(led_dev->devcls, NULL, MKDEV(led_dev->dev_major, 0), 
                                 NULL, "guquan_dev%d_name", 0);
	if (IS_ERR(led_dev->dev)) {
		printk(KERN_ERR "device_create error\n");
		ret = PTR_ERR(led_dev->dev); 
		goto err_2;  //注销设备号、释放空间、释放class
	}
	
	//硬件初始化
	led_dev->reg_virt_base = ioremap(GPX2_CON, GPX2_SIZE);  //寄存器地址映射
	if (IS_ERR(led_dev->reg_virt_base)) {
		printk(KERN_ERR "ioremap error\n");
		ret = PTR_ERR(led_dev->reg_virt_base);
		goto err_3;  //注销设备号、释放空间、释放class、释放device
	}
	led0_con = ioremap(0x11000c40, 4);
	led0_dat = ioremap(0x11000c44, 1);
	*led0_con |= (0x1<<28);

	return 0;

err_3: //释放device
	device_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));
err_2: //释放class
	class_destroy(led_dev->devcls);
err_1: //释放设备号
	unregister_chrdev(led_dev->dev_major, "guquan_dev"); 
err_0: //释放内存
	kfree(led_dev);
	return ret;
}

static void __exit chr_dev_exit(void)
{
	//解除地址映射
	iounmap(led_dev->reg_virt_base);
	//释放device
	device_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));
	//释放class
	class_destroy(led_dev->devcls);
	//释放设备号
	unregister_chrdev(led_dev->dev_major, "guquan_dev"); 
	//释放内存
	kfree(led_dev);
}

读写硬件寄存器的改进

上面读写寄存器的值,还是先通过ioremap映射到内存空间中,然后去操作映射的地址。

可以使用readl和writel函数直接向对应地址中写入或者读取值,其函数原型如下:

unsigned int readl(const volatile void __iomem *addr);//从地址中读取地址空间到值
void writel(unsigned long value , const volatile void __iomem *add);

则对应的,LED初始化的时候的配置可以改为如下:

// gpio的输出功能的配置
u32 value = readl(led_dev->reg_virt_base);
value &= ~(0xf<<28);
value |= (0x1<<28);
writel(value, led_dev->reg_virt_bas);

对从测试程序读取操作LED也可以修改为如下:

ssize_t my_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
	
	int ret = copy_from_user(&kernel_value, buf, 4);
	if (ret > 0) {
		printk("my_write filed\n");
		return ret;
	}
	printk("my_write is called, wirte to kernel successful:%d\n", kernel_value);
	
	if (kernel_value) { //点亮
		writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );
	} else {
		writel( readl(led_dev->reg_virt_base + 4) & ~(1<<7),   led_dev->reg_virt_base + 4 );
	}
	
    return 0;  
}

代码展示

dev.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <asm/io.h>


#define MINORBITS 20

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

struct led_desc
{
	//设备号
	unsigned int dev_major;
	//创建设备节点所需的结构
	struct class *devcls;  
	struct device *dev;
	//映射后的寄存器基址
	void *reg_virt_base;

};
struct led_desc *led_dev = NULL; //声明一个全局设备对象  

#define GPX2_CON 0x11000C40  
#define GPX2_SIZE 8

static int kernel_value = 123;

ssize_t my_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
	int ret = copy_to_user(buf, &kernel_value, 4);
	if (ret > 0) {
		printk("my_read filed\n");
		return ret;
	}
	printk("my_read is called, read from kernel successful:%d\n", kernel_value);
    
    return 0;
}
ssize_t my_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("this is %s\n", __FUNCTION__);
	
	int ret = copy_from_user(&kernel_value, buf, 4);
	if (ret > 0) {
		printk("my_write filed\n");
		return ret;
	}
	printk("my_write is called, wirte to kernel successful:%d\n", kernel_value);
	
	if (kernel_value) { //点亮
		writel( readl(led_dev->reg_virt_base + 4) | (1<<7),   led_dev->reg_virt_base + 4 );
	} else {
		writel( readl(led_dev->reg_virt_base + 4) & ~(1<<7),   led_dev->reg_virt_base + 4 );
	}
	
    return 0;  
}
int my_open (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    return 0;
}
int my_release (struct inode *inode, struct file *filp)
{
    printk("this is %s\n", __FUNCTION__);
    return 0;
}

struct file_operations my_fops = {
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

static int __init chr_dev_init(void)
{
	int ret = 0;
	//实例化对象
	led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL); //GFP_KERNEL表示如果当前内存不够用,函数会一直阻塞  注意<linux/slab.h>
	if (led_dev == NULL) {
		printk(KERN_ERR "malloc error\n");  //可以通过KERN_ERR筛选调试信息
		return -ENOMEM;
	}
	
	//申请设备号
#if 1  /* 动态申请设备号 */
	//major为0是动态申请设备号,并返回设备号
	led_dev->dev_major = register_chrdev(0, "guquan_dev", &my_fops); 
	if (led_dev->dev_major < 0) {
		printk(KERN_ERR "regsiter filed\n");
		ret = -ENODEV;
		goto err_0;  //释放对象空间
	} else printk("regsiter successful\n");
#else /* 静态申请设备号 */
	//注册cdev结构体,实现file_operations操作方法
	int ret = register_chrdev(major, "guquan_dev", &my_fops); 
	if (ret < 0) {
		printk("regsiter filed\n");
		return -1;
	} else printk("regsiter successful\n");
#endif /* 动态申请设备号 */

	//创建设备节点
	led_dev->devcls = class_create(THIS_MODULE, "do_not_care");
	if (IS_ERR(led_dev->devcls)) {
		printk(KERN_ERR "class_create filed\n");
		ret = PTR_ERR(led_dev->devcls);  //返回指针出错的具体原因
		goto err_1;  //注销设备号、释放空间
	}
	led_dev->dev = device_create(led_dev->devcls, NULL, MKDEV(led_dev->dev_major, 0), NULL, "guquan_dev_name");
	if (IS_ERR(led_dev->dev)) {
		printk(KERN_ERR "device_create filed\n");
		ret = PTR_ERR(led_dev->dev); 
		goto err_2;  //注销设备号、释放空间、释放class
	}
	
	//硬件初始化
	led_dev->reg_virt_base = ioremap(GPX2_CON, GPX2_SIZE);  //寄存器地址映射
	if (IS_ERR(led_dev->reg_virt_base)) {
		printk(KERN_ERR "ioremap filed\n");
		ret = PTR_ERR(led_dev->reg_virt_base);
		goto err_3;  //注销设备号、释放空间、释放class、释放device
	}    
	// gpio的输出功能的配置
	unsigned int value = readl(led_dev->reg_virt_base);
	value &= ~(0xf<<28);
	value |= (0x1<<28);
	writel(value, led_dev->reg_virt_base);

	return 0;


err_3: //释放device
	device_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));
err_2: //释放class
	class_destroy(led_dev->devcls);
err_1: //释放设备号
	unregister_chrdev(led_dev->dev_major, "guquan_dev"); 
err_0: //释放内存
	kfree(led_dev);
	return ret;
}

static void __exit chr_dev_exit(void)
{
	//解除地址映射
	iounmap(led_dev->reg_virt_base);
	//释放device
	device_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));
	//释放class
	class_destroy(led_dev->devcls);
	//释放设备号
	unregister_chrdev(led_dev->dev_major, "guquan_dev"); 
	//释放内存
	kfree(led_dev);
}


module_init(chr_dev_init);
module_exit(chr_dev_exit);

MODULE_LICENSE("GPL");

test.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main(int argc, const char *argv[])
{
	int buf = 666;
	int ret;
	int fd = open("/dev/guquan_dev_name", O_RDWR);
	if (fd < 0) {
		perror("open");
		exit(1);
	}
	
	read(fd, &buf, 4);
	++buf;
	write(fd, &buf, 4);
	
	while (1)
	{
		sleep(1);
		buf = 1;
		write(fd, &buf, 4);
		sleep(1);
		buf = 0;
		write(fd, &buf, 4);
	}
	
	close(fd);
	
	return 0;
}

Makefile

ROOTFS_DIR = /nfs/rootfs
#挂载根文件系统的目录


APP_NAME = test
CROSS_COMPILE = arm-none-linux-gnueabi-
CC = $(CROSS_COMPILE)gcc

ifeq ($(KERNELRELEASE), )  #默认为空

KERNEL_DIR = /home/gq/linux-3.14.24
#内核路径:/home/gq/linux-3.14.24

CUR_DIR = $(shell pwd)
#通过执行shell命令pwd获取当前路径

all:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) modules #-C代表进入到内核,即进入到内核路径,会读取内核源码顶层目录中Makefile,
#顶层Makefile中会给KERNELRELEASE赋版本号
#M=$(CUR_DIR) 用来指定模块的位置,内核会按照自己的规则来编译指定路径下的文件
#这里内核还不知道要将哪个文件编译为模块,所以会重新执行一次ifeq,这次就跳到else了
#所以这个Makefile会被读取两次:第一次是执行make的时候,第二次是在内核源码中的Makfeile读取
#modules 表示将文件编译为
	$(CC) $(APP_NAME).c -o $(APP_NAME)
clean:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
	
install:
	cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)

else

obj-m += dev.o
#指定要编译的文件,并且要编译成modules
#再增加文件的时候,只需要修改这里即可

endif
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值