Linux驱动开发一.字符设备框架——2.框架完善
在上一章节我们测试了模块的加载,这个就是驱动的根基,下面我们在这个根基上面加上枝干,也就是完善dev_init()函数,但是顾名思义,这个函数只是实现设备的初始化,如果我们需要操作设备(对于linux来说就是个文件)进行操作,在初始化完成后还需要对其进行读写操作,所以还需要新的open(),read(),write()以及close()操作。
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 (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); 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 (*mremap)(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 **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif };
上面就是文件操作结构体的所有内容。但是我们不需要都写出来,只写我们需要的几个就可以了,也就是open、release、read和write。具体的函数体先不写,先把整个函数架构建立出来
/** * @brief 打开设备文件 * * @return int */ static int dev_open() { return 0; } /** * @brief 关闭设备文件 * * @return int */ static int dev_release() { return 0; } /** * @brief 设备文件写入数据 * * @return int */ static int dev_write() { return 0; } /** * @brief 设备文件读取数据 * * @return int */ static int dev_read() { return 0; } /** * @brief 自定义文件操作结构体 * */ static struct file_operations testDev_fops= { .owner = THIS_MODULE, .open = dev_open, .release = dev_release, .read = dev_read, .write = dev_write, };
因为这4个函数我们都还没有考虑传参什么的,所以后面还需要修改,包括定义的方法都要修改。修改的方法可以参考内核里提供的其他驱动的定义方法。
字符设备的注册和注销
在前面章节中我们通过注册函数实现了模块的加载和卸载,实际使用过程中加载模块主要是为了注册我们需要使用的字符设备。这里引入一个新的函数:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { return __register_chrdev(major, 0, 256, name, fops); }
这个register_chrdev就是用来注册字符设备的。在Linux中好多函数都是成对出现的,有一个注册肯定就会有个注销,所以还有个注销函数
static inline void unregister_chrdev(unsigned int major, const char *name) { __unregister_chrdev(major, 0, 256, name); }
不论是注册还是注销的函数比较简单,可以看出来参数就是一个设备号和一个设备名称。但是注册还多了个我们前面定义的文件操作结构体。注册和注销函数就放在模块加载和卸载中。我们在文件开始定义两个宏,分别是主设备号还有设备名称(设置前在用cat /proc/devices打印一下看看有哪个设备号是空闲的,别冲突了!)。因为要用到一些刚才的两个函数(路径为/include/inux/fs.h),头文件也要添加对应库
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #define DEV_MAJOR 200 #define DEV_NAME "DEVICE_TEST"
如上面代码所示,主设备号我定了个200,设备名我就随便整了一个。
把前面模块加载和卸载的函数修改一下,增加注册模块的函数
static int __init dev_init(void) { int ret = 0; printk("device init!\r\n"); //字符设备注册 ret = register_chrdev(DEV_MAJOR, DEV_NAME, &testDev_fops); if(ret < 0 ){ printk("device init failed\r\n"); } return 0; } static int __exit dev_exit(void) { //字符设备注销 unregister_chrdev(DEV_MAJOR,DEV_NAME); printk("device exit\r\n"); return 0; }
这个时候make检查一下,会发现报了一堆错误!
这是因为我们定义读写开关函数的时候没有严格按照文件操作结构体里要求的参数格式写函数。按照给定的格式要求修改一下,把每个函数内添加打印信息,下面就是整个dev的驱动文件
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #define DEV_MAJOR 200 //设备号 #define DEV_NAME "DEVICE_TEST" //设备名称 // /** // * @brief 打开设备文件 // * // * @return int // */ static int dev_open(struct inode *inode, struct file *filp) { printk("dev open!\r\n"); return 0; } /** * @brief 关闭设备文件 * * @return int */ static int dev_release(struct inode *inode, struct file *filp) { printk("dev release!\r\n"); return 0; } /** * @brief 读设备文件数据 * * @param filp * @param buf * @param count * @param ppos * @return ssize_t */ static ssize_t dev_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos) { printk("dev read data!\r\n"); return 0; } /** * @brief 设备文件数据写入 * * @param file * @param buf * @param count * @param ppos * @return ssize_t */ static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { printk("dev write data!\r\n"); return 0; } /** * @brief 文件操作结构体 * */ static struct file_operations testDev_fops= { .owner = THIS_MODULE, .open = dev_open, .release = dev_release, .read = dev_read, .write = dev_write, }; /** * @brief 初始化 * * @return int */ static int __init dev_init(void) { int ret = 0; printk("device init!\r\n"); //字符设备注册 ret = register_chrdev(DEV_MAJOR, DEV_NAME, &testDev_fops); if(ret < 0 ){ printk("device init failed\r\n"); } return 0; } /** * @brief 卸载 * */ static void __exit dev_exit(void) { //字符设备注销 unregister_chrdev(DEV_MAJOR,DEV_NAME); printk("device exit\r\n"); } module_init(dev_init); //模块加载 module_exit(dev_exit); //模块卸载 MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
重新make一下,在开发板里加载模块试一下,能挂载成功,就不再截图了(一定注意printk函数里的引号是双引号!在下一章节调试的时候编译没问题,但是读写一直报错,查了半天发现用的是单引号,估计以前写python的时候养成的习惯),但是因为没有调用文件操作都函数,那4个函数还没法演示。在下一章节我们写一个应用程序来演示如何使用。
创建字符设备文件
在挂载模块后,通过ls /dev发现在目录文件夹下还查不到我们添加的模块对应的文件,这时候还是没法使用这个模块的。需要通过mknod命令创建块设备或者字符设备文件
mknod /dev/testdev c 200 0
mknod后面第一个参数是我们新建设备文件的名称和路径,c表示是字符设备,200和0分别表示主从设备号。在添加了这个文件后就可以在/dev路径下查到这个设备文件了(换了个串口调试设备,截的图不太好看),这个文件的名称可以和驱动里定义的宏里的设备名称不同。