谈谈Linux字符设备驱动的实现

@

(PS:搬家来的~~~ 原CSDN博客地址https://blog.csdn.net/qq_43743762)

字符设备驱动基础

参考: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——>操作方法

谈谈Linux字符设备驱动的实现

所以!!!对于字符驱动设备来讲,设备号、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

执行效果如下:

谈谈Linux字符设备驱动的实现

注意!!!这里我只是实现了通过文件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;
}

执行结果如下,实现了从驱动读取数据,自增之后再写入驱动:

谈谈Linux字符设备驱动的实现

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

内核驱动如何控制外设

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

内核驱动可以直接操作物理地址吗?不可以,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);

谈谈Linux字符设备驱动的实现

驱动代码:

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;
}

谈谈Linux字符设备驱动的实现

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

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

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

驱动程序的改进

框架复盘

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

	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
上一篇:【转】关于Adapter的The content of the adapter has changed问题分析 关于Adapter的The content of the adapter has changed问题分析


下一篇:3.字符设备驱动------Poll机制