进行驱动学习之前要先了解一下我们应用程序在使用硬件设备时的一个工作流程。应用程序通过系统提供的API(open、read、write、close等接口)去调用硬件驱动,再由驱动去操作硬件。整理的形成了操作系统的不同层次:应用层 → API → 设备驱动 → 硬件。在驱动源码中我们需要去提供操作系统提供的API的函数实体。这些函数实体要封装成一个fiel_opreation
结构体。这个结构体变量的元素主要是函数指针。用来挂接实体函数的地址。每一个驱动程序都需要一个该结构体类型的变量用以向内核注册时提供该变量。
字符设备驱动的注册
register_chrdev() -#include
作用:向内核注册自己的file_operations
,内核中由一个数组来存储注册的字符设备驱动,在register_chrdev()函数内部就是将我们要注册的驱动的信息(主要是)存储在数组中相应的位置。
函数原型:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
首先可以看到函数修饰符使用了 static、inline等关键字,这些都是在内核中常用的,static的作用主要是将函数的作用于限定在本文件内,inline函数内部只有一个函数,这样封装一层函数看上去内有什么必要,直接调用结果也是一样的,但是在封装一层函数的作用在于内核内部函数的分层。使用inline原地展开也减小了调用的开销。
@ major:主设备号,赋值为0则是让系统自动分配主设备号。如果@major > 0,则使用自定的major作为主设备号。该函数返回值使用major (主设备号)变量接收。主设备号的范围是0~255.
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
可以使用cat /proc/devices
命令查看已经被使用的主设备号。
@ name:这一系列设备的名称。
@ fops:与此设备相关联的文件操作。file_operations结构体。
字符设备驱动的卸载
unregister_chrdev() -#include
函数原型:
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
@ major:主设备号.
@ name::这一系列设备的名称。
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 (*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
};
这个就是file_operations结构体的实体。里面大部分是函数指针用以挂接驱动中定义的函数实体。可以看到我们应用层常见的read、write、open等函数。
核心工作:file_operations及其元素填充、注册驱动
要点:
代码实例:
/************************************************/
/* file name:module_test.c */
/* author: */
/************************************************/
#include // module_init module_exit
#include // __init __exit
#include
#define MYMAJOR 200
#define MYNAME "testchar"
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
printk(KERN_INFO "test_chrdev_open\n");
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, // 就是这个.open对应的函数
};
// 模块安装函数
static int __init chrdev_init(void)
{
int ret = -1;
printk(KERN_INFO "chrdev_init helloworld init\n");
// 在module_init宏调用的函数中去注册字符设备驱动
ret = register_chrdev(MYMAJOR, MYNAME, &test_fops);
if (ret)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success...\n");
return 0;
}
// 模块下载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(MYMAJOR, MYNAME);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
Makefile
# 开发板的linux内核的源码树目录
KERN_DIR = /root/driver/kernel
obj-m += module_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
arm-linux-gcc app.c -o app
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
编译: make
安装模块:insmod module_test.ko
创建设备文件:mknod /dev/test c 主设备号 次设备号 (mknod filename 设备类型 major minor)
测试程序测试
/************************************************/
/* file name:app.c */
/* author: */
/************************************************/
#include
#include
#include
#include
#define FILE "/dev/test" // 刚才mknod创建的设备文件名
int main(void)
{
int fd = -1;
fd = open(FILE, O_RDWR);
if (fd < 0)
{
printf("open %s error.\n", FILE);
return -1;
}
printf("open %s success..\n", FILE);
// 读写文件
// 关闭文件
close(fd);
return 0;
}
以上的代码就可以进行简单的驱动装载,打开和关闭等操作,后续可继续添加读(read)写(write)接口。以及硬件操作等代码。
应用和驱动之间的数据交换
copy_from_user,用来将数据从用户空间复制到内核空间
copy_to_user