在Linux中一切皆为文件,驱动加载成功后会在/dev目录下生成一个对应文件/dev/xxx,xxx位具体驱动的名字,对这个文件进行操作就可实现对硬件进行操作。
应用程序运行在用户空间,驱动运行在内核空间。当我们需要在用户空间实现对内核的操作,例如现在有个led设备的驱动文件/dev/led,使用open函数打开这个驱动,因为用户空间不能直接对内核进行操作,必须通过系统调用的方法实现从用户空间“陷入”到内核空间,这样实现对底层驱动的操作,如上图。其中open、close、write和read等函数由C库提供,在linux系统中,系统调用作为C库的一部分。使用open函数是的流程如下图:
应用程序调用了open函数,那么在驱动程序中也应该由一个open函数与之对应,每一个系统调用,在驱动中都有与之对应的一个驱动函数,在linux内核文件中include/linux/fs.h文件中定义了结构体file_operations,此结构体是linux内核驱动函数集合,通过这个结构体可以关联应用层和驱动层之间的相对应的系统调用函数。
字符设备驱动的开发步骤
- 驱动模块的加载和卸载
linux驱动由两种运行方式。第一种是将驱动编译到内核中,内核启动时自动加载驱动;第二种是将驱动编译成模块,在linux内核启动后使用insmod或者modprobe命令加载驱动模块。在开发过程中一般采用第二种方式,方便开发调试,驱动开发完成后再根据需要决定是否将驱动编译到内核中。
模块的加载和卸载两种操作,在写驱动的时候需要注册这两种操作函数,模块的加载和卸载函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init函数是向linux内核注册一个模块加载函数,参数xxx_init是需要注册的具体函数,当使用insmod或者modprobe命令加载驱动时,xxx_init函数就会被调用。
module_exit函数向linux内核注册一个模块卸载函数,当使用命令rmmod下载模块的时候就会调用xxx_exit函数。
- 字符设备的注册与注销
对于字符设备,在驱动模块加载完成后还需要向注册字符设备,卸载驱动模块的时候也需要注销字符设备。字符设备的注册与注销函数如下:
static inline int register_chrdev(unsigned int major, const char * name, const struct file_operations * fops);
static inline void unregister_chrdev(unsigned int major, const char *name);
major:主设备号,linux下每个设备都由设备号,设备号分为主设备号和次设备号,后面详细解释。 name:设备名称
fops:指向设备操作函数集合结构体变量
一般字符设备的注册在驱动模块的入口函数中进行,也就是上一步中的xxx_init函数中进行,相应的在出口函数xxx_exit函数中注销设备。
-
实现设备的具体操作函数
file_operations结构体中包含这设备的具体操作函数,定义该结构体变量并初始化成员变量,也就是初始化其中open、release、read和write等具体操作函数。
设备的打开和关闭操作需要实现file_operations中的open和release函数
对设备的读写进行操作需要实现read和write函数 -
添加LICENSE和作者信息
最后需要添加LICENSE和作者信息,LICENSE信息是必须添加的,否则编译报错;可使用下面方式添加:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
字符设备驱动基本框架:
/*包含需要的头文件*/
#include <linux/types.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/ide.h>
#define XXXDEVBASE_MAJOR 200
#define XXXDEVBASE_NAME "xxxdevbase"
/*打开设备*/
static int xxx_open(struct inode * inode, struct file * filp)
{
return 0;
}
/*读设备*/
static ssize_t xxx_read(struct file * filp, char __user * buf, size_t cnt, loff_t offset)
{
return 0;
}
/*写设备*/
static ssize_t xxx_write(struct file * filp, char __user * buf, size_t cnt, loff_t offset)
{
return 0;
}
/*关闭设备*/
static int xxx_release(struct inode * inode, struct file * filp)
{
return 0;
}
/*驱动各种操作函数集*/
static struct file_operations xxxdevbase_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_release,
};
/*驱动入口函数,需要用“__init”修饰,当驱动加载的时候就会被调用*/
static int __init xxx_init(void)
{
/*实现入口函数具体功能*/
/*注册字符设备驱动*/
int retvalue = 0;
retvalue = register_chrdev(XXXDEVBASE_MAJOR, XXXDEVBASE_NAME, &xxxdevbase_fops);
if(retvalue < 0)
{
/*注册失败处理*/
}
return 0;
}
/*驱动出口函数,需要用“__exit”修饰,当驱动卸载的时候会被调用*/
static void __exit xxx_exit(void)
{
/*实现出口函数具体功能*/
/*注销字符设备驱动*/
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
}
/*注册模块加载和卸载函数*/
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author");
下面是根据字符设备驱动基本框架写的虚拟字符读写设备驱动
文件名:chrdevbase.c
功能:1.读取虚拟设备中的字符串 2.向虚拟设备中写入字符串
#include <linux/types.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/ide.h>
#define CHRDEVBASE_MAJOR 200 //设备号
#define CHRDEVBASE_NAME "chrdevbase" //设备名称
//字符串读写缓冲区
static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data!"};
/*打开设备*/
static int chrdevbase_open(struct inode * inode, struct file * filp)
{
return 0;
}
/*读设备*/
static ssize_t chrdevbase_read(struct file * filp, char __user * buf, size_t cnt, loff_t offset)
{
int retvalue = 0;
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt); //将设备中的字符串读取到用户空间
if(retvalue == 0)
{
printk("kernel send data ok!\r\n");
}
else
{
printk("kernel send data failed!\r\n");
}
return 0;
}
/*写设备*/
static ssize_t chrdevbase_write(struct file * filp, char __user * buf, size_t cnt, loff_t offset)
{
int retvalue = 0;
retvalue = copy_from_user(writebuf, buf, cnt); //将用户空间的字符串写到设备
if(retvalue == 0)
{
printk("kernel recevie data : %s\r\n", writebuf);
}
else
{
printk("kernel recevie data failed!\r\n");
}
return 0;
}
/*释放设备*/
static int chrdevbase_release(struct inode * inode, struct file * filp)
{
return 0;
}
/*操作函数集*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*驱动入口函数*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops); //注册设备
if(retvalue < 0)
{
printk("chrdevbase driver register failed!\r\n");
}
printk("chrdevbase init()!\r\n");
return 0;
}
/*驱动出口函数*/
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit()!\r\n");
}
/*注册与注销驱动入口出口函数*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/*LICENSE和作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author");
Makefile文件内容:
KERNELDIR := "/home/kernel/linux-imx"
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build:kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
KERNELDIR := “/home/kernel/linux-imx” 表示:linux内核源码路径,替换为自己主机上相应路径
执行make命令后生成chrdevbase.ko驱动文件
启动嵌入式linux系统,将chrdevbase.ko上传到/lib/modules/4.1.15+路径下,如果文件系统中没有这个路径的话自行创建该路径。我的板子用的芯片是NXP的imx6q,内核版本是rel_imx_4.1.15_2.1.0_ga,内核版本不同该路径名称可能会有区别。
输入以下命令加载驱动:
modprobe chrdevbase.ko
可能会出现以下提示,缺少modules.dep文件,挂载失败
modprobe: can’t open ‘modules.dep’: No such file or directory
不能直接创建modules.dep文件,输入depmod命令即可生成modules.dep文件,如果跟文件系统中没有depmod命令,需要重新配置busybox使能此命令,重新编译生成跟文件系统即可。depmod命令执行成功后会生成以下三个文件:
modules.alias
modules.dep
modules.symbols
重新执行modprobe加载chrdevbase.ko驱动。
从上图可以看到终端打印了“chrdevbase init()!”,说明驱动加载成功。
使用lsmod命令可能查看当前系统中存在的模块,见下图:
可以在/proc/devices下查看是否存在chrdevbase设备,见下图:
但是,在/dev目录下并没有对应的设备文件,因为还没有创建设备节点。输入以下语句创建设备节点/dev/chrdevbase。
mknod /dev/chrdevbase c 200 0
其中mknod是创建节点的命令,“/dev/chrdevbase”是节点文件,“c”表示字符设备,“200”是设备号,“0”是设备的次设备号。执行成功后,即可生成节点文件,见下图:
至此,虚拟设备驱动文件完成加载。
下面对该字符设备进行读写操作。
编写测试app代码
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
static char usrdata[] = {"usr data"};
int main(int argc, char * argv[])
{
int fd, retvalue;
char * filename = NULL;
char readbuf[100], writebuf[100];
if(argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("Can't open file %s!\r\n", filename);
return -1;
}
if(atoi(argv[2]) == 1) //读操作
{
retvalue = read(fd, readbuf, 50);
if(retvalue < 0)
{
printf("read file %s failed!\r\n", filename);
}
else
{
printf("read data: %s\r\n", readbuf);
}
}
if(atoi(argv[2]) == 2) //写操作
{
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0)
{
printf("write file %s failed!\r\n", filename);
}
}
retvalue = close(fd);
if(retvalue < 0)
{
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
交叉编译:
arm-linux-gnueabihf-gcc testApp.c -o testApp
编译完成后生成testApp的可执行程序,上传到开发板跟文件系统/opt路径下,给与其权限。
输入以下命令,读去设备字符串
./testApp /dev/chrdevbase 1
输入以下命令,写字符串到设备
./testApp /dev/chrdevbase 2
读写设备测试成功!
当不在需要这个驱动模块的时候,可以使用rmmod命令卸载。见下图:
模块卸载成功!