4. Linux驱动简述¶
linux驱动是linux内核驱动的全称,至于什么是内核,可以查看上一章节的《 内核概念 》内容。
4.1. Linux三大驱动¶
在讲Linux三大驱动前,先讲一下Linux设备分类。
linux是文件型系统,所有硬件都会在对应的目录(/dev)下面用相应的文件表示。在文件系统的linux下面,都有对应文件与键盘、鼠标、硬盘等实实在在硬件硬件设备关联,访问这些文件就可以访问实际硬件。
按照读写存储数据方式,我们可以把设备分为以下几种:字符设备、块设备和网络设备。 而Linux三大驱动就是指对这些设备的驱动,即字符设备、块设备驱动和网络设备驱动。
4.1.1. 字符设备¶
字符设备指能够像字节流串行顺序依次进行访问的设备,对它的读写是以字节为单位。
字符设备的特点:
一个字节一个字节读写的设备
读取数据需要按照先后数据(顺序读取)
每个字符设备在/dev目录下对应一个设备文件,linux用户程序通过设备文件(或称 设备节点)来使用驱动程序操作字符设备。
常见的字符设备有鼠标、键盘、串口、SPI、I2C等
4.1.2. 块设备¶
块设备是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
块设备的特点:
数据以固定长度进行传输,比如512K
块设备能够随机访问,而字符设备则只能顺序访问。
块设备包括硬盘、磁盘、U盘和SD卡等
每个块设备在/dev目录下对应一个设备文件,linux用户程序可以通过设备文件(或称设备节点)来使用驱动程序操作块设备。
块设备可以容纳文件系统,所以一般都通过文件系统来访问,而不是/dev设备节点。
4.1.3. 网络设备¶
网络设备驱动不同于字符设备和块设备,不在/dev下以文件节点代表,而是通过单独的网络接口来代表。
网络设备的特点:
网络接口没有像字符设备和块设备一样的设备号和/dev设备节点,只有接口名,如eth0,eth1
对网络设备的访问只能通过socket操作,而不是open、closc、read、write
4.2. Linux驱动编译方法¶
Linux驱动编译方法有两种:
直接把驱动编译到内核中
把驱动编译成模块(Module),在内核启动后由用户手动动态加载
由前面内核章节内容的介绍可知,Linux为宏内核架构,如果开启所有的功能(把所有驱动都编译到内核),内核就会变得十分臃肿。
为了解决一缺点,linux中引入了 内核模块
这一机制。内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。
有了内核模块这一机制,我们就讲实现某个功能的内核驱动代码编译成模块,在内核运行过程,可以加载这部分代码到内核中,从而动态地增加了内核的功能。
4.3. 设备驱动的基本概念¶
注意
对设备驱动最通俗的解释就是“驱使硬件设备行动”,有操作系统的存在则大大降低了应用软件与硬件平台的耦合度。
4.3.1. 驱动的作用¶
设备驱动充当了我们硬件与应用软件之间的纽带,使得应用软件只需要调用驱动程序接口API就可以让硬件去完成要求的开发,而应用软件则不需要关心硬件到底是如何工作的。这将大大提高我们应用程序的可移植性和开发效率。
设备驱动与底层硬件直接打交道,按照硬件设备的具体工作方式读写设备寄存器, 完成设备的轮询、中断处理、DMA通信,进行物理内存向虚拟内存的映射,最终使通信设备能够收发数据,使显示设备能够显示文字和画面,使存储设备能够记录文件和数据。
4.3.2. 裸机驱动开发¶
一般我们把没有操作系统的编程环境,称为裸机编程环境,比如单片机编程(假设单片机没有跑RTOS)。
无操作系统(即裸机)时的设备驱动,也就是直接操作寄存器的方式控制硬件,在这样的系统中,虽然不存在操作系统,但是设备驱动是必须存在的。
注意
一般情况下,对每一种设备驱动都会定义为一个软件模块。其他模块需要使用这个设备的时候,只需要包含设备驱动的头文件然后调用其中的外部接口函数即可。这在STM32的开发中很常见,也相对比较简单。
在系统中没有操作系统的情况下,工程师可以根据硬件设备的特点 自行定义接口 ,如对LED定义LightOn()、LightOff()等。
4.3.3. 系统驱动开发(Linux)¶
反观有操作系统时,首先,驱动硬件工作的的部分仍然是必不可少的,其次,我们还需要将设备驱动融入内核。
为了实现这种融合,必须在所有的设备驱动中设计面向操作系统内核的接口,这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。
在有操作系统的情况下,设备驱动的架构则由相应的操作系统定义,驱动工程师必须按照相应的架构设计设备驱动,如在本次实验中必须设计 file_operations的接口 。这样,设备驱动才能良好地整合到操作系统的内核中。
注意
当系统中存在操作系统的时候,设备驱动变成了连接硬件和内核的桥梁。操作系统的存在势必要求设备驱动附加更多的代码和功能,把单一的驱动变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的API。
操作系统的存在究竟带来了什么好处呢?
首先操作系统完成了多任务并发。
其次操作系统为我们提供了内存管理机制。
对于应用程序来说,应用程序将可使用统一的系统调用接口来访问各种设备。
通过write()、read()等函数读写文件就可以访问各种字符设备和块设备,而不用管设备的具体类型和工作方式。
4.4. Linux驱动——helloworld¶
在 内核源码同级目录
下开始操作,开发的IDE软件是《开发环境搭建》章节提到的VSCode,并且VSCode SSH连接到了服务器。
野火提供了驱动教程的源码,可通过以下命令获取:
# 从gitee获取
git clone https://gitee.com/LubanCat/lubancat_allwinner_code_storage.git
注意
教程源码中的linux_driver文件夹需要复制到 内核代码同级目录
才能运行。
学习时可以根据教程源码自行新建文件夹和文件,如下图新建文件夹,并新建helloworld.c文件和Makefile文件。
4.4.1. helloworld.c¶
helloworld.c文件,也就是教程的第一个驱动代码文件,先介绍一下写驱动的四个组成部分。
头文件
驱动模块的入口及出口函数
声明信息
功能实现
#include <linux/init.h> //包含宏定义的头文件
#include <linux/module.h> //包含初始化加载模块的头文件
module_init(); //驱动入口
module_exit(); //驱动出口
MODULE_LICENSE("GPL2"); //声明开源许可证
static int hello_init(void); //入口函数功能实现
static void hello_exit(void); //出口函数功能实现
下面是helloworld.c文件完整内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <linux/init.h> //包含宏定义的头文件
#include <linux/module.h> //包含初始化加载模块的头文件
//入口函数功能实现
static int hello_init(void)
{
//内核层只能使用printk,不能使用printf,因为内核层不支持C语言
printk(KERN_EMERG "[ KERN_EMERG ] Hello World Init\n"); //输出等级为0
printk("[ default ] Hello World Init\n");
return 0;
}
//出口函数功能实现
static void hello_exit(void)
{
printk(KERN_EMERG "[ KERN_EMERG ] Hello World Exit\n"); //输出等级为0
printk("[ default ] Hello World Exit\n");
}
module_init(hello_init); //驱动入口
module_exit(hello_exit); //驱动出口
MODULE_LICENSE("GPL v2"); //声明开源许可证
// "GPL" 是指明 这是GNU General Public License的任意版本
// “GPL v2” 是指明 这仅声明为GPL的第二版本
// "GPL and addtional"
// "Dual BSD/GPL"
// "Dual MPL/GPL"
// "Proprietary" 私有的
MODULE_AUTHOR("embedfire"); //声明作者信息
MODULE_DESCRIPTION("hello world"); //对这个模块作一个简单的描述
MODULE_ALIAS("hello world_test"); //这个模块的别名
|
4.4.2. Makefile¶
Makefile文件里指明了编译的架构和使用的编译器。
obj-m
指编译成模块,这里的模块指的就是内核模块,也就是下章要讲的重点内容,hello world例程的编译留到下一章再演示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | KERNEL_DIR=../../kernel/
# KERNEL_DIR的路径得是内核源码路径
#声明编译的架构为arm64,编译器前缀为aarch64-linux-gnu-
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPILE
#obj-m:编译成模块
obj-m := helloworld.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
#伪目标
.PHONE:clean
#指当make命令后紧跟clean时(即make clean),执行以下伪目标clean对应的指令
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
|