一、字符设备驱动框架
1、Linux之下一切皆文件,驱动设备的表现形式也是文件,具体存放在/dev/下,例如某lcd驱动设备文件=> /dev/lcd.
2、驱动根据不同的驱动框架有不一样的编写方式,需要根据驱动框架来编写驱动程序,比如在编写字符设备驱动的时候,就需要重点编写出应用程序相对应的打开、关闭、读、写等函数。编写的时候还要考虑应用程序的开发便利性。
3、字符设备驱动的编写主要就是驱动对应的open、close、read等函数,具体是实现file_operstions这个结构体里面是成员变量的实现。
二、驱动模块的加载与卸载
1、设备驱动程序既可以编译到内核中,也可以编译成模块。前者的优势是编译进内核后每次系统启动就会运行改驱动程序,但缺点是遇到需要修改驱动的情况就得编译整个内核。后者的优势是我们需要用到哪个模块就编译哪个模块驱动程序,便于开发。但是没有哪种最好,还是看应用场景。
2、编写驱动时候的注意事项:
1) 编译驱动需要用到内核源码,将源码解压缩并编译,从而得到zImage和.dtb文件,因为需要用到这两个文件来启动系统。
2) 驱动程序编译生成的.ko文件要放到根文件系统中。加载驱动命令有insmod与modprobe,移除则使用命令rmmod来卸载。如果使用modprobe命令来加载一个新模块要先调用depmod命令。驱动模块加载成功后可以使用命令lsmod来查看。
3) 在编写驱动程序如果需要输出信息要用printk函数,printf是运行在用户态的,printk是运行在内核态。printk可以根据日志级别对输出信息进行分类,一共有8个等级,第0级是最高优先级,一般是紧急情况如内核崩溃,第7级是最低优先级,如调试信息。输出格式如下:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
三、驱动模块的注册与注销
1、当需要向系统注册一个字符设备可使用函数register_chrdev,而当我们需要卸载驱动的时候就需要注销掉先前注册过的字符设备,这时候采用函数unregister_chrdev注销字符设备。
2、register_chrdev会将主设备号下的所有次设备号都使用了,不够智能。
3、注册注销函数返回值是个负数就是出错了,linux内核函数大部分如此。
四、设备号
1、Linux内核使用32位的数据类型dev_t来表示,其中高12位表示主设备号,低20位表示次设备号,所以主设备号的范围在0~4095之间。每一个设备的设备号是唯一的!
2、从dev_t获取主设备号和次设备号,可以通过MAJOR(dev_t)函数获取主设备号,MINOR(dev_t)函数获取次设备号。也可以通过函数MKDEV(major,minor)同时构成dev_t从而获取设备号整体信息。
五、file_operations的实现
1、需要哪些函数就实现哪些函数不需要全部实现。
2、实现如open,close,read,write等基础函数可能需要的头文件有:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <uapi/linux/types.h>
#include <linux/fs.h>
3、结构组织
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
};
六、字符设备驱动框架的构建
1、
七、应用程序编写
1、linux之下一切皆文件,涉及文件就有open,read,write等函数,编写应用程序需要使用到这些函数。可以通过在ubuntu中输入man + 函数名 查看函数使用方法。
八、应用程序测试
1、加载驱动
modprobe chrdevbase.ko
2、查看加载情况
/lib/modules/4.1.15 # lsmod
Module Size Used by Tainted: G
chrdevbase 695 0
3、创建设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。“c”表示这是个字符设备,“ 200”是设备的主设备号,“ 0”是设备的次设备号
/dev # mknod /dev/chrdevbase c 200 0
/dev # ls chrdev*
chrdevbase
/ # ls /dev/chrdevbase -l
crw-r--r-- 1 0 0 200, 0 Jan 1 05:04 /dev/chrdevbase
4、运行测试
/lib/modules/4.1.15 # ./chrdevbaseAPP /dev/chrdevbase
chrdevbase_open
chrdevbase_read
chrdevbase_write
chrdevbase_release
对应驱动程序内字符设备我们实现的函数集。
/*
* 字符设备的操作集合
*/
static struct file_operations chrdevbase_fops={
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_release,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
九、完善虚拟设备驱动
1、完善驱动使应用程序能够对驱动读写操作。如应用程序读取驱动程序内的字符串,或者应用程序驱动程序写字符串。
2、驱动给应用程序传递数据要用函数cope_to_user,驱动程序接收应用程序传递来的数据要用函数copy_from_user。
3、通过这两个函数我们可以实现应用程序和驱动程序的交互,比如我们在应用程序调用read函数:
ret = read(fd, read_buf, 50);
就会跳转到驱动程序里面我们编写的chrdevbase_read函数:
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
然后我们在函数内通过函数cope_to_user将我们需要发送的数据发送给应用程序,将read_buf通过buf发送出去,一共cnt字节
memcpy(read_buff, kernel_data, sizeof(kernel_data));
ret = copy_to_user(buf, read_buff, cnt);
应用程序收到驱动程序发来的数据就会存放到read_buf中:
ret = read(fd, read_buf, 50);
if (ret < 0)
{
printf("read file %s failed\r\n, filename");
}
else
{
printf("APP read data:%s\r\n", read_buf);
}
写数据原理也类似,但是是通过函数copy_from_user来实现。