驱动开发-设备驱动模块
LINUX设备驱动模块
设备驱动程序,就是驱使设备按照用户的预期进行工作的软件,它是应用程序与设备沟通的桥梁。设备驱动程序主要负责硬件设备的参数配置、数据读写与中断处理。
Linux中的大部分驱动程序是以内核模块的形式编写的。内核模块是Linux 内核向外部提供的一个接口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM)。
驱动程序的加载方式
内核模式
将新驱动程序编译进内核,需要修改内核代码和编译选项,这种方式将驱动程序代码直接编译到内核镜像(zImage
或uImage
等)中,成为内核的一部分。
特点:
- 集成度高:驱动随内核启动自动加载,无法卸载。
- 性能稍好:省去了模块加载和解析的开销。
- 安全性/稳定性高:常用于系统最核心、必须的驱动(如根文件系统驱动)。
- 内核体积大:所有驱动都会被编译进去,导致内核镜像文件变大。
配置方法:通过内核的配置菜单(make menuconfig
等)将对应的驱动选项配置为 y
。
实验一
本次实验是通过静态添加自己编写的驱动如:beep.c/ap3216c.c/icm20608.c等驱动。
步骤一:
找到驱动放置的路径,将自己编写好的驱动放在这个路径下,一般是linux-kernel/drivers/xxx
路径下:
- 蜂鸣器(beep)这种简单的字符设备,通常可以放在
linux-kernel/drivers/char/
(老习惯)或linux-kernel/drivers/misc/
(现在更常见)目录下。misc
是“混杂设备”的意思,专门放置无法简单归类的简单字符设备驱动。 - I2C 客户端设备(如传感器)的驱动都放在这个目录或其子目录下。你可以直接放在
drivers/i2c/
下,或者为其新建一个子目录,比如drivers/i2c/sensors/
(如果有很多传感器驱动,这样管理更清晰)。 - SPI 设备驱动应放在
linux-kernel/drivers/spi/
目录下。

步骤二:
修改对应目录的Kconfig和Makefile
Makefile文件
1
obj-$(CONFIG_CHAR_BEEP) += beep.o
解释:
- 如果在使用
make menuconfig
的时候配置选项CONFIG_MISC_BEEP
被设置为y
,则将beep.o
编译进内核。 - 如果被设置为
m
,则将beep.o
编译成模块 (beep.ko
)。 - 如果未设置,则不编译。
- 如果在使用
kconfig文件
找到我们放置路径对应的文件夹下面的kconfig文件
1
2
3
4
5
6config CHAR_BEEP
tristate "CHAR BEEP Device Support"
default n
help
This is a simple beep driver for testing.
Say Y here if you want to support the beep.解释:
config MISC_BEEP
:定义了一个配置符号,它必须与 Makefile 中的CONFIG_
前缀后的名字完全一致。tristate
:表示该配置有三种状态:y
(内置),m
(模块),n
(不编译)。"MISC BEEP Device Support"
:这是在make menuconfig
中显示的菜单文本。default n
:默认状态为不编译。help
:后面的文字是帮助信息。
步骤三:
配置内核并编译
进入内核源码根目录
启动配置菜单:
1
make menuconfig
在菜单中找到你的驱动:
根据你在
Kconfig
中设置的路径,你的驱动会出现在相应的子菜单中。例如,beep
驱动会在:Device Drivers -> [*] Character devices -> <*> CHAR BEEP Device Support
使用空格键将选项设置为
<*>
(编译进内核) 或<M>
(编译为模块)。(Pressingincludes, excludes, modularizes features)
保存配置并退出
编译内核
1
make -j$(nproc) # 使用所有CPU核心进行编译,加快速度
步骤四:
更新你的开发板系统:将新编译的内核镜像烧写或拷贝到你的开发板上,并重启。cp zImage /tftpboot/
对其他驱动的操作:对 ap3216c.c
和 icm20608.c
进行完全相同的操作,只不过是在它们对应的 drivers/i2c/
和 drivers/spi/
目录下的 Kconfig
和 Makefile
中进行修改。配置符号的名字可以自己定,但要清晰易懂,例如 CONFIG_I2C_AP3216C
和 CONFIG_SPI_ICM20608
。
动态加载与卸载
这是驱动开发调试阶段最常用、最灵活的方式。驱动程序被编译成 .ko (Kernel Object) 文件,而不是直接链接进内核镜像。
特点:
- 灵活性高:可以在系统运行时动态地加载和卸载,无需重启系统。
- 节省内存:不需要的驱动可以不加载,减少内核占用内存。
- 易于调试:开发调试周期短,修改代码后重新编译并加载即可。
常用命令:
insmod
:最基础的加载命令,sudo insmod beep.ko
。但它不解决模块依赖。modprobe
:更智能的加载命令,它会自动从lib/modules/$(uname -r)
目录查找并加载模块所依赖的其他模块。需要先运行depmod
生成模块依赖关系。rmmod
:卸载模块,sudo rmmod beep
。lsmod
:查看当前已加载的所有模块。
模块的编写
模块的加载与卸载
1 | module_init(XXX_init);//注册模块加载函数 |
**module_exit()**函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用“insmod”、”modprobe”命令加载驱动的时候,xxx_init 这个函数就会被调用;
**module_exit()**函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit 函数就会被调用。
1 | static int __init xxx_init(void) |
一个典型的字符串驱动模块:
1 |
|
编写它对应的Makefile文件:
1 | KERNELDIR := /home/arm-linux/MX6U/linux-core |
带参数的可加载模块
向动态加载的Linux内核模块传递参数是一个非常常用且强大的功能,特别是在调试和配置驱动时。它允许你在不重新编译模块的情况下改变其行为。
内核提供了一组宏(定义在 linux/moduleparam.h
头文件中),让你在模块代码中声明参数。加载模块时,insmod
或 modprobe
命令就可以接收这些参数的值。
在模块代码中声明参数
最基本的宏是 module_param
,其语法如下:
1 | module_param(name, type, perm); |
name
:参数的名称(也是模块内部的变量名)。type
:参数的数据类型。常见的有:bool
:布尔值(true
/false
)int
:整型uint
:无符号整型long
:长整型ulong
:无符号长整型charp
:字符指针(即字符串),内核会为它动态分配内存。array
:数组(需要配合module_param_array
使用)
perm
:指定在/sys/module/<module_name>/parameters/
目录下对应参数文件的权限。这用于在模块加载后,通过sysfs
文件系统查看或修改参数。S_IRUGO
:只读(用户、组、其他都可读)S_IWUSR
: root用户可写(通常与S_IRUGO
用|
组合,如S_IRUGO | S_IWUSR
)0
:完全不在sysfs
中创建该参数的入口,参数只能在加载时设置。
加载模块时传递参数
使用 insmod
或 modprobe
命令时,使用 参数名=值
的格式来传递。
insmod
(需指定完整路径):1
sudo insmod /path/to/my_module.ko param1=value1 param2=value2
modprobe
(在模块搜索路径中查找,更常用):1
sudo modprobe my_module param1=value1 param2=value2
使用
modprobe
前,通常需要先运行sudo depmod -a
来更新模块依赖信息。
具体示例
步骤 1:
在驱动代码中 (beep.c
) 声明参数
1 |
|
步骤 2
编译模块并查看参数信息
编译后,你可以使用 modinfo
命令查看模块的信息,其中就包括我们定义的参数及其描述:
1 | modinfo beep.ko |
输出会类似这样:
1 | filename: /.../beep.ko |
步骤 3:
加载模块并传递参数
示例 1:使用默认参数加载
1 | insmod beep.ko |
示例 2:在加载时指定自定义参数
1 | insmod beep.ko debug_level=2 device_name="my_cool_buzzer" |
步骤 4:
在 Sysfs 中查看和修改参数
模块加载后,参数会出现在 /sys/module/
目录下:
1 | ls /sys/module/beep/parameters/ |
由于我们在声明 debug_level
时设置了 S_IWUSR
权限,root 用户甚至可以在模块运行时动态修改它(如果驱动代码设计为能响应这种实时变化):
1 | sudo su |
传递数组
module_param_array
允许你传递一个数组。
1 | static int my_array[5]; |
加载时这样传递:
1 | sudo insmod my_module.ko my_array=10,20,30,40,50 |
内核会自动解析逗号分隔的值,并将实际元素数量填入 array_size
变量中。
模块的依赖
Linux内核模块之间可以相互引用一些符号,这些符号包括函数与变量。
特点:
- 一个模块引用其他模块的符号,称为模块依赖关系。
- 被引用的模块必须先安装,引用模块才能安装。
内核使用宏定义EXPORT_SYMBOL
导出变量与函数
EXPORT_SYMBOL(symbol_name)
仅将符号导出给那些**使用 GPL 兼容许可证**(如 `GPL`, `Dual MIT/GPL` 等)的模块。这是一种强制性的“开源策略”,如果你希望你的模块只被开源社区使用,而不希望闭源模块使用你的代码,就应该用这个宏。1
2
3
4
5
将符号 `symbol_name` 导出到全局符号表。**所有其他模块**(无论其许可证是什么)都**可以看到并使用**这个符号。
- ```c
EXPORT_SYMBOL_GPL(symbol_name)EXPORT_SYMBOL_NS(symbol_name, namespace_name)
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
将符号导出到一个特定的命名空间(namespace)。这提供了更好的符号封装和避免命名冲突的能力。只有导入了相应命名空间的模块才能看到和使用这个符号。
```c
/****************** 符号提供者 *************************/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
// 1. 定义一个将要被导出的函数
void exported_function(void)
{
pr_info("Hello from exported_function in Module A!\n");
}
EXPORT_SYMBOL(exported_function); // 导出函数
// 2. 定义一个将要被导出的变量
int exported_variable = 1024;
EXPORT_SYMBOL(exported_variable); // 导出变量
static int __init module_a_init(void)
{
pr_info("Module A initialized\n");
return 0;
}
static void __exit module_a_exit(void)
{
pr_info("Module A exited\n");
}
module_init(module_a_init);
module_exit(module_a_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("KEVIN");
MODULE_DESCRIPTION("A module that exports symbols");
/**************** 符号消费者 **********************/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/printk.h>
// 声明外部符号(编译器,这个符号在其他地方定义)
extern void exported_function(void);
extern int exported_variable;
static int __init module_b_init(void)
{
pr_info("Module B initialized\n");
// 使用从 Module A 导入的符号
pr_info("I'm going to call exported_function and use exported_variable\n");
exported_function(); // 调用外部函数
pr_info("The value of exported_variable is: %d\n", exported_variable); // 使用外部变量
return 0;
}
static void __exit module_b_exit(void)
{
pr_info("Module B exited\n");
}
module_init(module_b_init);
module_exit(module_b_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("KEVIN");
MODULE_DESCRIPTION("A module that uses symbols from Module A");
实验步骤:
错误的加载顺序(先加载依赖者)
1
insmod module_b.ko
结果:
使用
dmesg
查看内核日志,你会看到明确的错误信息:分析:加载
module_b
时,内核找不到exported_function
和exported_variable
这两个符号的定义,因为它们的提供者module_a
还没有被加载。加载失败。正确的加载顺序(先加载提供者,再加载依赖者)
1
2
3
4
5
6
7
8
9
10
11# 1. 先加载提供符号的模块
sudo insmod module_a.ko
# 检查 dmesg: Module A initialized
# 2. 再加载依赖它的模块
sudo insmod module_b.ko
# 检查 dmesg:
# Module B initialized
# I'm going to call exported_function and use exported_variable
# Hello from exported_function in Module A!
# The value of exported_variable is: 1024成功!
module_b
找到了它所需的符号,并成功执行。查看模块依赖关系(
lsmod
)1
2
3
4/lib/modules/4.1.15 # lsmod
Module Size Used by Tainted: G
module_b 786 0
module_a 1309 1 module_bUsed by
列清晰地显示了依赖关系:module_a
被module_b
使用。卸载的时候要先卸载使用引用功能的模块
使用
modprobe
自动处理依赖1
2
3
4
5
6
7
8depmod
# 现在,使用 modprobe 加载 module_b
sudo modprobe module_b
# 检查 dmesg,你会看到:
# Module A initialized
# Module B initialized ... (所有日志)