文章目录
前言
在简单结束应用层的开发学习后,本系列将开启驱动层的学习,本文作为该系列第一期旨在归纳前期需要准备的知识。
一、Liunx、MCU和FPGA编程的区别
FPGA的编程更多地依赖于硬件描述语言(HDL),如VHDL或Verilog,用于描述电路的结构和行为。这种编程方法将硬件电路抽象为逻辑门、寄存器等基本元素,并通过编写代码来描述它们之间的连接关系和功能。相较于其他两种编程方式,FPGA编程需要频繁抓取时钟,这是因为FPGA的并行性、硬件描述方式、精确的时序控制需求以及性能优化等方面的要求。这种时钟抓取机制使得FPGA能够高效地处理复杂的数字逻辑任务,并在各种应用中实现高性能和灵活性。
相比之下,MCU的编程通常更侧重于顺序执行和寄存器的直接操作。因为MCU的操作是顺序执行的,开发者可以通过直接读写寄存器来管理内部状态和操作。这种编程方式相对简单直观,但可能不如FPGA在并行处理和复杂逻辑实现上灵活。
至于Linux框架下的驱动编程,它与FPGA和单片机的编程方式有显著的不同。Linux驱动编程主要关注于设备与操作系统之间的交互,包括设备节点的创建、设备驱动程序的编写和与操作系统的接口实现等。驱动程序需要处理设备的硬件特性,并将其抽象为操作系统可以理解和操作的接口。这种编程方式更注重于软件的架构和接口设计,与硬件的交互通常是通过特定的接口和协议来实现的。
二、Linux内核模块
在Linux系统中,设备驱动会以内核模块的形式出现,学习Linux内核模块编程是驱动开发的先决条件。 第一次接触Linux内核模块,我们将围绕着“Linux内核模块是什么”,“Linux内核模块的工作原理”以及 “我们该怎么使用Linux内核模块”这样的思路一起走进Linux内核世界。
1. 什么是内核模块
Linux内核模块是Linux内核向外部提供的一个插口,也被称为动态可加载内核模块(Loadable Kernel Module,LKM)。它是一个具有独立功能的程序,可以被单独编译,但不能独立运行。在运行时,内核模块被链接到内核作为内核的一部分在内核空间运行。
内核模块的主要作用是扩展内核的功能,而无需重新编译整个内核。例如,内核模块通常用于添加新的设备驱动程序、文件系统或其他功能到内核中。通过内核模块,Linux内核能够实现内核功能扩展,提供新的系统调用或特性,甚至实现内核的安全增强以增加系统的安全性。
这里展示一张图片可以让大家直观地感受一下Linux的内核体系(Monolithic Kernel)。
可以看到Linux所使用的宏内核架构是将包括微内核(Microkernel)以及微内核之外的应用层IPC、文件系统功能、设备驱动模块都编译成一个整体。 其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。 Linux操作系统正是采用了宏内核结构。为了解决这一缺点,linux中引入了内核模块这一机制。
2. 内核模块的代码架构
Linux内核模块的代码框架通常由下面几个部分组成:
- 模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
- 模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
- 模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
- 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
- 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
- 模块的其他相关信息: 可以声明模块作者等信息。
参数 | 功能 |
---|---|
MODULE_LICENSE() | 表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可 |
MODULE_AUTHOR() | 描述模块的作者信息 |
MODULE_DESCRIPTION() | 对模块的简单介绍 |
MODULE_ALIAS() | 给模块设置一个别名 |
这里给出一个简单的示例(注:仅作介绍使用):
#include <linux/module.h> // 包含内核模块所需的头文件
#include <linux/kernel.h>
// 模块许可证声明,这是必须的,且必须是这种形式的宏定义
MODULE_LICENSE("Dual BSD/GPL");
// 模块初始化函数,当模块被加载时调用
static int __init my_module_init(void)
{
// 初始化代码,比如申请资源、注册设备驱动等
printk(KERN_INFO "My module has been loaded.\n");
// 返回0表示初始化成功,非0值表示失败
return 0;
}
// 模块清理函数,当模块被卸载时调用
static void __exit my_module_exit(void)
{
// 清理代码,比如释放资源、注销设备驱动等
printk(KERN_INFO "My module has been unloaded.\n");
}
// 使用module_init和module_exit宏来指定初始化函数和清理函数
module_init(my_module_init);
module_exit(my_module_exit);
3. 头文件
前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。 编写内核模块所需要的头文件,并不在上述说到的目录,而是在Linux内核源码中的include文件夹。编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。
#include <linux/module.h>//包含内核模块信息声明的相关函数
#include <linux/init.h>//包含了 module_init()和 module_exit()函数的声明
#include <linux/kernel.h>//包含内核提供的各种函数,如printk
4. 模块参数
模块参数允许用户在加载模块时通过命令行指定参数值。这些参数在模块的加载过程中被获取,并转换成相应类型的值,然后赋值给对应的变量,这个过程常常发生在函数调用之前。
module_param(name, type, perm)
/*
name: 我们定义的变量名
type: 参数的类型,目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,
invbool。其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值
也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是
charp
perm: 表示的是该文件的权限,详细参考下表
*/
用户 | 参数 | 功能 |
---|---|---|
当前用户 | S_IRUSR | 用户拥有读权限 |
S_IWUSR | 用户拥有写权限 | |
当前用户组 | S_IRGRP | 当前用户组的其他用户拥有读权限 |
S_IWUSR | 当前用户组的其他用户拥有写权限 | |
其他用户 | S_IROTH | 其他用户拥有读权限 |
S_IWOTH | 其他用户拥有写权限 |
模块参数的使用通常涉及以下步骤:
- 在模块代码中声明变量,并使用module_param()宏或module_param_named()宏来定义模块参数。这些宏接受三个参数:变量名、变量类型以及访问权限。访问权限用于定义在sysfs中对应文件的访问权限,与Linux文件访问权限的管理方式相同。
- 在加载模块时,用户可以通过命令行传递参数值给模块。这些值将被转换为相应的类型,并赋值给在模块代码中声明的变量。
5. makefile说明
对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。 为此,我们在编译时需要到内核源码目录下进行编译。 编译内核模块使用的Makefile文件,和我们前面编译C代码使用的Makefile大致相同, 这得益于编译Linux内核所采用的Kbuild系统,因此在编译内核模块时,我们也需要指定环境变量ARCH和CROSS_COMPILE的值。
KERNEL_DIR=../../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export ARCH CROSS_COMPILE
obj-m := XXX.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
以上代码中提供了一个关于编译内核模块的Makefile。
第1行:该Makefile定义了变量KE