驱动概念
狭义上来说,驱动程序专指操作系统中的用来操控硬件的代码。
下面的驱动框架中也会提到,我们在上层应用写好程序,要求实现某一个硬件的功能,比如点亮LED等等,这里面会通过一系列调用,在内核态找到我们所需要的驱动,通过函数操作直接与硬件交互。而在裸机开发中我们也经常会听到裸机驱动,其实严格意义上来说,没有操作系统是没有驱动概念的,如果非要说裸机驱动的话,也是指裸机中操作硬件的那部分程序。
驱动与底层硬件直接打交道,按照硬件设备的具体工作方式。读写设备的寄存器,完成设备的轮询,中断处理,DMA通信,进行物理内存向虚拟内存的映射实现各种接口函数,充当了硬件与应用软件中间的桥梁。
驱动分类
1、字符设备(Char Device)
2、块设备(Block Device)
3、网络设备(Net Device)
字符设备:字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。
对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生。
字符设备驱动程序通常至少要实现open、close、read和write系统调用。
比如我们常见的lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输。
块设备:一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。
块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。
块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。
只有一个块设备可以支持一个安装的文件系统。
比如我们常见的电脑硬盘、SD卡、U盘、光盘等。
网络接口:任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。
访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。
内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函数(socket函数)而不是read、write等。
比如我们常见的网卡设备、蓝牙设备。
linux中所有的驱动程序最终都能归到这三种设备中,当然他们之间也没有非常严格的界限,这些都是程序中对他们的划分而已,比如一个sd卡,我们也可以把它封装成字符设备去操作也是没有问题的。
以上摘:https://www.linuxidc.com/Linux/2011-10/44721p2.htm
设备驱动程序功能
1、对设备初始化和释放
2、把数据从内核传送到硬件和从硬件读取数据
3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据
4、检测和处理设备出现的错误
访问特定硬件:
访问特定硬件就是访问物理地址(如处理器内设备的寄存器、外设的地址映射)。然而由于MMU的内存映射以及对操作系统的保护,运行在用户态的应用程序一般不能直接访问硬件地址。因此需要驱动程序作为应用程序和访问硬件之间的媒介。
运行流程
上图是我们Linux系统的分层图,这里很直观的可以看到我们上层应用是通过什么流程什么设备去调用内核然后传达到硬件设备的。
当我们编写好应用代码,在用户态去调用封装C库,C库函数接着进去内核态调用系统调用函数(触发软中断),通过栈调用vfs的相关函数,然后去驱动链表里面根据设备名、设备号来找到相应的设备驱动,来调用驱动里面的相关函数,而这个函数正是来操作或者设置io口电平的函数。从我们上图来举例,应用层我们调用open函数,会触发一个软中断,软中断为了快速响应,用汇编实现的sys_call,sys_call通过栈调用到sys_open函数,sys_open函数会去内核的驱动链表里面根据设备名以及设备名找到相应的驱动函数,去调用驱动函数里面的open,这个open设置的io口电平操作,从而完成对硬件的操控。
驱动框架
入口函数
应用层编写我们的入口就是main函数,但在驱动编写时不是这样的,有两种情况。
1、缺省情况下
int __init init_module(void) 加载模块时的初始化函数,也就是驱动模块的入口函数
void __exit cleanup_module(void) 卸载模块时的函数,也就是卸载某个驱动时要执行的函数
2、
static int __init xxxx_init(void) 加载模块时的初始化函数,也就是驱动模块的入口函数
static void __exit xxxx_exit(void) 卸载模块时的函数,也就是卸载某个驱动时要执行的函数
上述两种情况相比,我们一般用第二种,因为第一种的名称是固定的,我们不能做更改,第二种我们可以改写xxxx为我们自己模块的名字。可以达到见文知义。
在用第二种模式时,我们要首先用固定格式声明一下:
module_init(xxxx_init);
module_exit(xxxx_exit);
来表明加载初始化函数和卸载函数。
c语言的标准函数库不能使用
驱动属于内核的一部分,我们此时还无法使用类似像printf这样的c库,但是内核会提供自己的c库,在内核中我们用printk代替printf函数。
添加LICENSE声明
linux是开源的系统,那就要我们遵守一定的规范,我们一般用GPL规范,所以在驱动编写时都要声明一下
MODULE_LICENSE(“GPL”);
简单的驱动框架:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
static int __init demo_init(void)
{
printk("%s,%d\n", __func__, __LINE__);
printk("val:%d\n", obj.val);
obj.func();
return 0;
}
static void __exit demo_exit(void)
{
printk("%s,%d\n", __func__, __LINE__);
}
module_init(demo_init);
module_exit(demo_exit);
下面我们来看几个驱动中常用的命令
1、加载驱动模块insmod
将生成驱动模块.ko文件加载,
insmod xxx.ko
加载后就会执行xxxx_init函数
2、卸载驱动模块rmmod
对应的卸载驱动的命令
rmmod xxxx 注意不用带.ko
3、查看内核中的模块信息
lsmod
4、查看模块的描述信息
modinfo xxxx.ko
我们可以在驱动程序添加一些辅助信息,例如作者 ,驱动描述等。
5、查看模块打印信息
dmesg
printk是内核打印函数,默认模式下在中断下无法显示(当然,可以设置成打印到终端),用dmesg可以查看一下打印到内核的信息。
以上摘:https://blog.csdn.net/u012142460/article/details/78879079