LINUX设备驱动模块

设备驱动程序,就是驱使设备按照用户的预期进行工作的软件,它是应用程序与设备沟通的桥梁。设备驱动程序主要负责硬件设备的参数配置、数据读写与中断处理。

Linux中的大部分驱动程序是以内核模块的形式编写的。内核模块是Linux 内核向外部提供的一个接口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM)。

驱动程序的加载方式

内核模式

将新驱动程序编译进内核,需要修改内核代码编译选项,这种方式将驱动程序代码直接编译到内核镜像(zImageuImage等)中,成为内核的一部分。

特点

  • 集成度高:驱动随内核启动自动加载,无法卸载。
  • 性能稍好:省去了模块加载和解析的开销。
  • 安全性/稳定性高:常用于系统最核心、必须的驱动(如根文件系统驱动)。
  • 内核体积大:所有驱动都会被编译进去,导致内核镜像文件变大。

配置方法:通过内核的配置菜单(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/ 目录下。
image-20250908134947864

步骤二:

修改对应目录KconfigMakefile

  • 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
    6
    config 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> (编译为模块)。(Pressing includes, excludes, modularizes features)

      image-20250908135942511 image-20250908140059903 image-20250908140128935
  • 保存配置并退出

    image-20250908140457162
  • 编译内核

    1
    make -j$(nproc) # 使用所有CPU核心进行编译,加快速度

步骤四:

更新你的开发板系统:将新编译的内核镜像烧写或拷贝到你的开发板上,并重启。cp zImage /tftpboot/


对其他驱动的操作:对 ap3216c.cicm20608.c 进行完全相同的操作,只不过是在它们对应的 drivers/i2c/drivers/spi/ 目录下的 KconfigMakefile 中进行修改。配置符号的名字可以自己定,但要清晰易懂,例如 CONFIG_I2C_AP3216CCONFIG_SPI_ICM20608

动态加载与卸载

这是驱动开发调试阶段最常用、最灵活的方式。驱动程序被编译成 .ko (Kernel Object) 文件,而不是直接链接进内核镜像。

特点

  • 灵活性高:可以在系统运行时动态地加载和卸载,无需重启系统。
  • 节省内存:不需要的驱动可以不加载,减少内核占用内存。
  • 易于调试:开发调试周期短,修改代码后重新编译并加载即可。

常用命令

  • insmod:最基础的加载命令,sudo insmod beep.ko。但它不解决模块依赖。
  • modprobe:更智能的加载命令,它会自动从lib/modules/$(uname -r)目录查找并加载模块所依赖的其他模块。需要先运行depmod生成模块依赖关系。
  • rmmod:卸载模块,sudo rmmod beep
  • lsmod:查看当前已加载的所有模块。

模块的编写

模块的加载与卸载

1
2
module_init(XXX_init);//注册模块加载函数
module_exit(xxx_exit);//注册模块卸载函数

**module_exit()**函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用“insmod”、”modprobe”命令加载驱动的时候,xxx_init 这个函数就会被调用

**module_exit()**函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit 函数就会被调用

1
2
3
4
5
6
7
8
9
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}

一个典型的字符串驱动模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <linux/module.h>
#include <linux/moduleparam.h> // 必须包含这个头文件

static int __init beep_init(void)
{
return 0;
}

static void __exit beep_exit(void)
{
}
module_init(beep_init);
module_exit(beep_exit);
MODULE_AUTHOR("KEVIN");
MODULE_LICENSE("GPL");

编写它对应的Makefile文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KERNELDIR := /home/arm-linux/MX6U/linux-core
CURRENT_PATH := $(shell pwd)

obj-m := bepp.o

build: kernel_modules

kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
install:
sudo cp *.ko /home/arm-linux/MX6U/nfs/rootfs/lib/modules/4.1.15

带参数的可加载模块

向动态加载的Linux内核模块传递参数是一个非常常用且强大的功能,特别是在调试和配置驱动时。它允许你在不重新编译模块的情况下改变其行为。

内核提供了一组宏(定义在 linux/moduleparam.h 头文件中),让你在模块代码中声明参数。加载模块时,insmodmodprobe 命令就可以接收这些参数的值。

在模块代码中声明参数

最基本的宏是 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 中创建该参数的入口,参数只能在加载时设置。

加载模块时传递参数

使用 insmodmodprobe 命令时,使用 参数名=值 的格式来传递。

  • 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
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
#include <linux/module.h>
#include <linux/moduleparam.h> // 必须包含这个头文件

// 声明模块参数对应的全局变量
static int debug_level = 1; // 默认值
static char *device_name = "default_beep"; // 默认值

// 声明参数
// 参数名debug_level,类型int,权限为可读可写(root)
module_param(debug_level, int, S_IRUGO | S_IWUSR);
// 参数名device_name,类型charp(字符串指针),权限为可读可写(root)
module_param(device_name, charp, S_IRUGO | S_IWUSR);

// 用MODULE_PARM_DESC为每个参数添加描述
MODULE_PARM_DESC(debug_level, "Debug message level (0-2). Default: 1");
MODULE_PARM_DESC(device_name, "The name of the beep device.");

static int __init beep_init(void)
{
printk(KERN_INFO "Beep module loaded!\n");
printk(KERN_INFO "Debug level is: %d\n", debug_level);
printk(KERN_INFO "Device name is: %s\n", device_name);

// ... 你的初始化代码,可以使用debug_level和device_name变量
return 0;
}

static void __exit beep_exit(void)
{
printk(KERN_INFO "Beep module unloaded!\n");
}

module_init(beep_init);
module_exit(beep_exit);
MODULE_LICENSE("GPL");

步骤 2

编译模块并查看参数信息

编译后,你可以使用 modinfo 命令查看模块的信息,其中就包括我们定义的参数及其描述:

1
modinfo beep.ko

输出会类似这样:

1
2
3
4
5
6
7
8
9
10
11
filename:       /.../beep.ko
license: GPL
description:
author:
srcversion: ...
depends:
retpoline: Y
name: beep
vermagic: ...
parm: debug_level:int
parm: device_name:charp

步骤 3:

加载模块并传递参数

示例 1:使用默认参数加载

1
2
3
4
5
insmod beep.ko
# dmesg 输出:
# Beep module loaded!
# Debug level is: 1
# Device name is: default_beep

示例 2:在加载时指定自定义参数

1
2
3
4
5
insmod beep.ko debug_level=2 device_name="my_cool_buzzer"
# dmesg 输出:
# Beep module loaded!
# Debug level is: 2
# Device name is: my_cool_buzzer

步骤 4:

在 Sysfs 中查看和修改参数

模块加载后,参数会出现在 /sys/module/ 目录下:

1
2
3
4
5
6
7
8
ls /sys/module/beep/parameters/
# debug_level device_name

cat /sys/module/beep/parameters/debug_level
# 2

cat /sys/module/beep/parameters/device_name
# my_cool_buzzer

由于我们在声明 debug_level 时设置了 S_IWUSR 权限,root 用户甚至可以在模块运行时动态修改它(如果驱动代码设计为能响应这种实时变化):

1
2
sudo su
echo 0 > /sys/module/beep/parameters/debug_level

传递数组

module_param_array 允许你传递一个数组。

1
2
3
4
static int my_array[5];
static int array_size;
module_param_array(my_array, int, &array_size, S_IRUGO);
MODULE_PARM_DESC(my_array, "An array of integers");

加载时这样传递:

1
sudo insmod my_module.ko my_array=10,20,30,40,50

内核会自动解析逗号分隔的值,并将实际元素数量填入 array_size 变量中。

模块的依赖

Linux内核模块之间可以相互引用一些符号,这些符号包括函数与变量。

特点:

  • 一个模块引用其他模块的符号,称为模块依赖关系。
  • 被引用的模块必须先安装,引用模块才能安装。

内核使用宏定义EXPORT_SYMBOL 导出变量与函数

  • EXPORT_SYMBOL(symbol_name)
    
    1
    2
    3
    4
    5

    将符号 `symbol_name` 导出到全局符号表。**所有其他模块**(无论其许可证是什么)都**可以看到并使用**这个符号。

    - ```c
    EXPORT_SYMBOL_GPL(symbol_name)
    仅将符号导出给那些**使用 GPL 兼容许可证**(如 `GPL`, `Dual MIT/GPL` 等)的模块。这是一种强制性的“开源策略”,如果你希望你的模块只被开源社区使用,而不希望闭源模块使用你的代码,就应该用这个宏。
  • 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

    结果

    image-20250908165012796

    使用 dmesg 查看内核日志,你会看到明确的错误信息:

    image-20250908165146468

    分析:加载 module_b 时,内核找不到 exported_functionexported_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

    image-20250908183933683

    成功!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_b

    Used by 列清晰地显示了依赖关系:module_amodule_b 使用。

  • 卸载的时候要先卸载使用引用功能的模块

    image-20250908184224994

  • 使用 modprobe 自动处理依赖

    1
    2
    3
    4
    5
    6
    7
    8
    depmod

    # 现在,使用 modprobe 加载 module_b
    sudo modprobe module_b

    # 检查 dmesg,你会看到:
    # Module A initialized
    # Module B initialized ... (所有日志)

    image-20250908184438338