文章目录

  • 字符设备驱动基础
  • 申请设备号
  • 创建设备节点
  • 在驱动中实现操作方法
  • 文件IO调用驱动中的操作
  • 应用程序与驱动的数据交互
  • 内核驱动如何控制外设
  • 控制LED的简单驱动实例
  • 驱动程序的改进
    • 框架复盘
    • 面向对象思想
    • 出错处理
    • 读写硬件寄存器的改进
    • 代码展示

字符设备驱动基础

参考: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_tvoid 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)gccifeq ($(KERNELRELEASE), )  #默认为空KERNEL_DIR = /home/gq/linux-3.14.24
#内核路径:/home/gq/linux-3.14.24CUR_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) cleaninstall:cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)elseobj-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: //释放devicedevice_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));
err_2: //释放classclass_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);//释放devicedevice_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));//释放classclass_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 8static 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: //释放devicedevice_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));
err_2: //释放classclass_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);//释放devicedevice_destroy(led_dev->devcls, MKDEV(led_dev->dev_major, 0));//释放classclass_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)gccifeq ($(KERNELRELEASE), )  #默认为空KERNEL_DIR = /home/gq/linux-3.14.24
#内核路径:/home/gq/linux-3.14.24CUR_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) cleaninstall:cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)elseobj-m += dev.o
#指定要编译的文件,并且要编译成modules
#再增加文件的时候,只需要修改这里即可endif

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

  1. 字符设备驱动开发流程详解

    字符驱动相关概念解析 一.驱动初始化 1.1分配设备描述结构 1.2初始化设备描述结构 1.3.注册设备描述结构 1.4.硬件初始化 二.实现设备操作 2.1open 2.2read 2.3.writ ...

  2. ()shi linux字符设备,Linux字符设备驱动基础(三)

    Linux字符设备驱动基础(三) 6 创建设备节点 6.1 手动创建设备节点 查看申请的设备名及主设备号: cat /proc/devices # cat /proc/devices Characte ...

  3. linux设备模型 字符设备,Linux 字符设备驱动模型之框架解说

    一.软件操作硬件设备模型 在进行嵌入式开发的过程中,在常做的事情就是驱动配置硬件设 备,然后根据功能需求使用硬件设备,实现功能的逻辑.如下图为其 相互之间的关系. 如上图所示: 驱动程序:主要作为操作 ...

  4. linux字符设备文件的打开操作,Linux字符设备驱动模型之字符设备初始化

    因为Linux字符设备驱动主要依赖于struct cdev结构,原型为: 所以我们需要对所使用到的结构成员进行配置,驱动开发所使用到的结构成员分别为:[unsigned int count;].[de ...

  5. linux生成驱动编译的头文件,嵌入式Linux字符设备驱动——5生成字符设备节点

    嵌入式Linux字符设备驱动开发流程--以LED为例 前言 留空 头文件 #include 查看系统设备类 ls /sys/class 设备类结构体 文件(路径):include/linux/devi ...

  6. linux字符设备驱动的 ioctl 幻数

    在Linux字符设备驱动入门(一)中,我们实现了字符设备的简单读写字符功能,接下来我们要在这个基础上加入ioctl功能.首先,我们先来看看3.0内核下../include/linux/fs.h中fil ...

  7. Linux 字符设备驱动结构(四)—— file_operations 结构体知识解析

    前面在 Linux 字符设备驱动开发基础 (三)-- 字符设备驱动结构(中) ,我们已经介绍了两种重要的数据结构 struct inode{...}与 struct file{...} ,下面来介绍另 ...

  8. linux字符设备驱动在哪里设置,从点一个灯开始学写Linux字符设备驱动!

    原标题:从点一个灯开始学写Linux字符设备驱动! [导读] 前一篇文章,介绍了如何将一个hello word模块编译进内核或者编译为动态加载内核模块,本篇来介绍一下如何利用Linux驱动模型来完成一 ...

  9. Linux字符设备驱动

    /*Linux字符设备驱动源代码scdd.c*/ #include <linux/init.h>   /*模块头文件*/ #include <linux/module.h> # ...

最新文章

  1. asp.net性能优化
  2. gettype获取类名_delphi – 获取属于任何类型的单元名称(TRttiType)
  3. 自动判断浏览器的中英文版本自动跳转网站中英文页面代码
  4. 百度2012校招笔试题之全排列与组合
  5. 计算机学院考勤管理办法,计科学院进一步加强课堂考勤实施意见(试行)
  6. 在Visual Studio中利用NTVS创建Pomelo项目
  7. cpu 被挂起和阻塞_同步异步阻塞非阻塞并发并行讲解
  8. linux命令-locale字符显示
  9. 网上支付失败了我该怎么办
  10. html 隐藏广告代码大全,JS广告代码_JS广告代码大全_js特效代码_js特效代码大全 - 懒人建站...
  11. 2021年北京人大附中高考成绩查询,2021北京市地区高考成绩排名查询,北京市高考各高中成绩喜报榜单...
  12. Suspending MMON slave action kewrmapsa_ for 82800 seconds
  13. 计算机专业的优秀学长寄语大一新生,学长对大一新生的寄语
  14. app抓包工具_抓包助手app下载安装_抓包助手软件最新版免费下载
  15. Java统计List中每个元素出现的次数、用java实现生成或显示文件的一些数字、微信小程序开发回顾
  16. 关于数据库系统的学习
  17. 两个实打实干活的同事离职了,老板连谈都没谈,一句挽留都没有,你怎么看
  18. 如何让百度搜索收录自己的Hexo博客文章
  19. MATLAB 高级数据类型 table
  20. 深度学习入门-ANN神经网络参数最优化问题(SGD,Momentum,AdaGrad,RMSprop,Adam)

热门文章

  1. 基于SSM的水果商城
  2. 扩展Redis的JSON处理模块,非常强调性能的RedisJson!速学
  3. 数据安全能力持续获得认可-天空卫士同日上榜两大榜单
  4. codeforces 1089A.lice the Fan(记忆化搜索dp)
  5. 折线迷你图怎么设置_Office小技巧-在EXCEL单元格中也可以有迷你折线图-迷你office...
  6. python如何添加行号_Tkinter向文本widg添加行号
  7. 【Youtobe trydjango】Django2.2教程和React实战系列一【项目简介 | 搭建 | 工具】
  8. 如何计算方阵的特征值和特征向量np.linalg.eig()
  9. oracle发布会,浪潮K-DB自由起航 发布会花絮盘点
  10. 偶尔逛下书店.居然多少有那末一点收获. 知道网上的[威客].[赚客]吗?