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驱动编译方法有两种:

  1. 直接把驱动编译到内核中

  2. 把驱动编译成模块(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
../../../_images/git-code.png

注意

教程源码中的linux_driver文件夹需要复制到 内核代码同级目录 才能运行。

学习时可以根据教程源码自行新建文件夹和文件,如下图新建文件夹,并新建helloworld.c文件和Makefile文件。

../../../_images/helloworld.png

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