目录
- 1. 字符设备驱动简介
- 1.1 重要函数
- 1.2 简单框架代码流程
- 1.3 linux中关于驱动的重要命令
- 2. 字符设备驱动简单框架编写
- 2.1 添加LICENSE信息
- 2.2 驱动模块的入口与出口
- 2.3 入口和出口函数的编写
- 2.4 设备操作结构体定义
- 2.4.1 结构体函数内容填充
- 3. 应用程序简介:
- 4. 应用程序的编写思路
1. 字符设备驱动简介
目前的驱动开发一般是分为三类,第一类就是字符设备驱动、块设备驱动、和网络驱动三类,其中字符设备驱动是最多最杂的,现在对字符设备驱动进行一个简要的介绍:
字符设备驱动,是指那些以字节流进行数据传输的设备、IIC、SPI、LCD、按键、例如键盘、鼠标、打印机等,其中包含以下几个关键的部分:
设备注册和注销:
通过设备注册使设备能被系统识别;注销则相反;数据操作函数:
通常包含open,read,write,realse等;中断处理:
处理设备产生的中断,以响应特定事件;
通过字符型设备驱动,可以使系统方便统一管理不同的设备,这样就可以给上层应用提供相应的接口函数,方便应用程序与设备之间进行数据交换和通信;
本次的实验是通过简单的实验建立一个设备驱动开发的基本框架,为后续的学习打下基础
,其中会列出重要的函数以及重要的挂载指令,当然其中有一些函数是比较老的,例如要手动分配设备号,但是为了便于学习目前就以简单的为主
,因为越是抽象的越简单,但是越抽象就越难以理解;
1.1 重要函数
这里只是把本实验相关的重要函数给罗列出来了,主要的作用就是对本次实验的一个总结,如果没有做个这个实验,那么看着没啥感觉的;例如我下面罗列函数的顺序就是我们在编写驱动实验时整个流程的顺序,首先就是注册入口和出口函数,其次就是编写入口和出口函数,再次就是编写文件结构体的对应相关的函数内容;
MODULE-LICENSE("GPL")
: 表示该内核模块遵循的许可协议是通用公共许可(GPL)。指定许可协议非常重要,它明确了该模块在使用、分发等方面的权利和限制。如果不写这个的话就会导致在装载设备驱动时出现警告;module_init(*****_init)
:模块的入口函数,进行初始化,也就是加载模块时第一个就是运行这个函数注册的函数*******;对模块进行初始化设置;module_exit(*****_exit)
:模块的出口函数,对模块进行卸载时就会执行这个函数注册的函数*******;static int __init *****_init(void)
:设备加载函数,进行模块的初始化和加载的设置;这个函数要被入口函数进行注册后起作用
;register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
:对设备进行注册;static int __exit *****_exit(void)
:设备卸载函数、进行模块的卸载时这个函数内部的程序就会执行,不过要被出口函数进行注册
;unregister_chrdev(unsigned int major, const char *name)
:对模块进行卸载;static const struct file_operations **_fops
: struct file_operations是一个很重要的结构体,其中定义了很多与文件操作有关的函数指针,例如read,write,realse,open等等,这些函数可以按需进行填充,这样就方便与设备之间进行文件的操作;下面进行实验时会进行一个详细的说明;
1.2 简单框架代码流程
代码实验编写流程:
1.3 linux中关于驱动的重要命令
lsmod
:显示有哪些模块被加载了,也就是显示所有的加载模块lsmod
:显示有哪些模块被加载了,也就是显示所有的加载模块depmod
:更新模块的依赖关系,也就是新加载一个模块时,要先运行一下这个命令,不然有错误modprob ***.ko
:这个命令的作用就是加载模块***.komknod /dev/*** c 200 0
:mknod:手动创建节点的命令。/dev/*** :创建设备的名称。c:字符型设备。200:主设备号,自己指定。0:次设备号,自己指定。cat /proc/devices
:这个命令的作用是查看所有加载的模块,显示所有设备号等;rmmod ***.ko
:卸载模块***.ko;cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq
:这个命令是显示当成CPU的频率;
2. 字符设备驱动简单框架编写
上面已经介绍了驱动编写的则整体流程,以及介绍了重要的加载驱动的一些linux的命令,因此下面的代码就不做过多的介绍:
2.1 添加LICENSE信息
我们需要在代码中添加LICENSE信息,否则编译会出错,不过我们还可以添加一些其他的信息,例如作者,邮箱等等;
MODULE_LICENSE("GPL");
2.2 驱动模块的入口与出口
linux的驱动有两种加载形式,一就是编译进linux内核中,当内核启动,驱动也启动,另一种方法就是把启动编译成模块也就***.ko文件,通过insmode或者modprob的命令进行加载模块,这样做的好处就是便于调试,而且不用重启linux内核;代码如下,下面两个函数的作用就是对模块进行加载和卸载,并在加载和卸载函数中对字符设备进行注册,如我们裸机编程中系统中断函数对中断服务函数的注册一样;
/*
* 模块入口和出口函数注册
*/
module_init(chrdevbase_init);/*入口,加载模块*/
module_exit(chrdevbase_exit);/*出口,卸载模块*/
2.3 入口和出口函数的编写
注意,入口函数是通过__init
来修饰(注意是两个杠),而对于出口函数通过__exit
进行修饰,在出入口函数中对设备进行注册,CHRDEVBASE_MAJOR是设备号,CHREEVBAScE_NAME是设备名,chrdevbase_fops是文件操作结构体;出口函数是类似的;
#define CHRDEVBASE_MAJOR 200
#define CHREEVBASE_NAME "chrdevbase"
static int __init chrdevbase_init(void)
{
printk("chrdevbase_init3\r\n");
/*注册字符设备*/
register_chrdev(CHRDEVBASE_MAJOR, CHREEVBASE_NAME,&chrdevbase_fops);
return 0;
}
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
unregister_chrdev(CHRDEVBASE_MAJOR,CHREEVBASE_NAME);
}
2.4 设备操作结构体定义
对于设备的操作file_operations
结构体,也称为文件操作结构体,这也是为什么linux下一切皆文件,我们的操作大部分都是通过文件来进行管理,我们可能用不到那么多的功能,因此我们要用到什么功能就进行对应的添加和书写就行,代码如下,注意这里使用的是=:
static const struct file_operations chrdevbase_fops={
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_release,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
2.4.1 结构体函数内容填充
可以看到,我们对上面的文件操作结构体中定义了四个函数指针,分别是:chrdevbase_open、chrdevbase_release、chrdevbase_read、chrdevbase_write
,接下来就是对这四个函数进行内容填充:
static char readbuf[100];/*读缓冲*/
static char writebuf[100];
static char kerneldata[]={"kernel data!"};
/************/
static ssize_t chrdevbase_read(struct file * file, char * buf, size_t count, loff_t *off)
{
int ret=0;
memcpy(readbuf,kerneldata,sizeof(kerneldata));
ret = copy_to_user(buf,readbuf,count);
if(ret<0){
printk("Error!");
}else{
}
return 0;
}
/************/
static ssize_t chrdevbase_write(struct file * file, const char * buf, size_t count,loff_t *off)
{
int ret =0;
ret = copy_from_user(writebuf,buf,count);
printk("Kernel recevdata:%s\r\n",writebuf);
if(ret = 0){
printk("Kernel recevdata:%s\r\n",writebuf);
}
return 0;
}
static int chrdevbase_release(struct inode * inode, struct file * file)
{
printk("chrdevbase_ralease\r\n");
return 0;
}
static int chrdevbase_open(struct inode * inode, struct file * file)
{
printk("chrdevbase_open\n");
return 0;
}
3. 应用程序简介:
当驱动程序编写完毕后,要通过应用程序进行调用驱动函数的一些接口函数,这些功能的实现是在应用程序中实现的,这样就实现了应用程序与驱动程序的分离
;可以这样类比,我们写应用程序就相当于我们在windows系统上写C程序,我们写C程序时也没有关注下层,这就是一种分离
,不过这两者是可以相互进行数据交换的,不过要用特定的方法;注意驱动程序和应用程序的编译是不一样的:对于应用程序的编译是下面的指令:
arm-linux-gnueabihf-gcc ***APP.c -o ***APP
:应用程序的编译指令$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
:驱动程序的编译:Make部分核心指令
从上面的编译指令中就可以看出区别,对于驱动的编译要用到一系列的库,这些库包含板子的信息,以及一些arm中的一些库等等,最终生成一个驱动模块,属于下层的驱动文件;
而对于应用程序的编译则是用到了一个.c文件以及基础库,就像我们编译C语言一样,只不过我们写一个hello.c文件用的是gcc编译器,生成的是x86架构的可执行文件,同理,我们使用arm-linux-gnueabihf-gcc交叉编译器生成的是arm架构的可执行文件,这明显是上层的应用程序,不过我们写上层应用程序时通过传参命令的形式就可以与下层的驱动进行数据文件交换;
4. 应用程序的编写思路
编写测试APP就是编写Linux应用程序,需要用到C库和文件操作相关的一些函数,open、read、write 和 close 这四个函数;这些函数可以根据下面的命令进行找详细帮助:“man 1”通常是用户命令的手册页;“man 2”一般是系统调用的手册页;“man 3”可能是 C 库函数的手册页;
wyj@BK:~$ man 2 read
wyj@BK:~$ man 2 write
wyj@BK:~$ man 2 close
wyj@BK:~$ man 2 open
因此在应用程序中编写程序就是如何对驱动程序进行调用和使用,不过要注意的是对于驱动的编写属于内核态,而对于应用程序的编写属于应用态,应用态不能直接操作内核态,要通过一定的程序从而间接操作内核态
:
int mian(int argc,char *argv[])
{
int ret = 0;
int fd = 0;
char *filename;
char readbuf[100];
char writebuf[100];
static char usrdata[]={"Usr data!Usr data!Usr data!"};
filename=argv[1];
fd = open(filename, O_RDWR);
if(fd<0)
{
printf("Can't open file %s\r\n",filename);
}
/*read*/
if(atoi(argv[2])==1)
{
ret=read(fd, readbuf, 50);
printf("\r\nAPP read data:%s\r\n",readbuf);
}
/*write*/
if(atoi(argv[2])==2)
{
memcpy(writebuf,usrdata,sizeof(usrdata));
ret = write(fd, writebuf,50);
}
/*close*/
ret = close(fd);
if(ret<0)
{
printf("Can't close %s\r\n",filename);
}
return 0;
}