【嵌入式小白从入门到实践】【Linux 驱动开发】:Linux 驱动基础

【嵌入式小白从入门到实践】持续更新

【Linux 驱动开发】:Linux 驱动基础前言1. Linux 驱动概述1.1 驱动的定义1.2 驱动的作用
2. Linux 驱动的分类2.1 按设备类型分类2.2 按加载方式分类2.3 按硬件连接方式分类2.4 按功能角色分类
3. Linux 驱动的基本框架3.1 模块加载函数(`module_init`)3.2 模块卸载函数(`module_exit`)3.3 模块许可证声明(Module License)3.4 模块参数(module_param)3.5 模块导出符号(EXPORT_SYMBOL)3.6 模块作者/描述信息(MODULE_AUTHOR)3.7 第一个驱动程序 helloword
4. Linux 驱动的编译方法4.1 驱动编译的核心原理1. 编译依赖2. 编译输出产物​
4.2 模块编译4.3 内置编译
5. 模块的加载与卸载5.1 模块的加载1. `insmod` 基础加载命令(不处理依赖)2. `modprobe` 智能加载命令(自动处理依赖)3. `modprobe -a` 批量加载多个模块4. 模块加载的底层机制5. 模块加载成功的验证方法
5.2 模块的卸载1. `rmmod` 基础卸载命令​2. `modprobe -r` 智能卸载命令(自动处理依赖)​3. 模块卸载的底层机制​4. 卸载成功的验证方法​
5.3 模块加载 / 卸载的注意事项​1. 模块参数的传递方式​2. 卸载失败的常见原因与解决方法​3. 开机自动加载模块的配置方法​

6. 查看模块信息6.1 modinfo 查看模块元数据与参数(最常用)6.2 lsmod 查看已加载模块列表与依赖关系6.3 dmesg 查看模块加载 / 卸载日志6.4 /sys/module/ 通过 sysfs 查看模块动态信息6.5 /proc/kallsyms 查看模块导出符号
7. 驱动模块传参7.1 模块传参概述1. 支持的数据类型2. 核心宏定义
7.2 实现步骤1. 单个参数实现步骤2. 数组参数实现步骤3. 参数权限说明
7.3 参数使用方法1. 加载时传递参数2. 加载后修改参数3. 查看参数信息
7.4 常见应用1. 参数合法性检查2. 调试参数的使用3. 避免敏感参数暴露4. 数组参数的边界处理
7.5 常见问题

【Linux 驱动开发】:Linux 驱动基础


前言

  Linux设备驱动开发作为操作系统内核编程的核心技术,是连接硬件与软件系统的关键桥梁;

1. Linux 驱动概述

1.1 驱动的定义

  驱动程序 (
Driver
)
:操作系统内核的关键组成部分,是运行在内核态的特殊软件,用于管理和控制硬件设备的专用软件模块;本质是 “硬件抽象层(
Hardware Abstraction Layer
)”,它充当硬件与操作系统及应用程序之间的桥梁:

向上:提供内核可识别的标准化接口(如文件操作接口
open
/
read
/
write
、网络数据包收发接口),让内核和应用程序无需关注硬件的具体型号、寄存器配置等细节,只需通过统一接口即可操控硬件向下:直接对接硬件的物理接口(如寄存器地址、中断线、
DMA
通道),理解硬件的底层工作逻辑;
  驱动与系统各层的关系可表示为:
应用程序 → 内核子系统(文件系统/网络子系统等) → 驱动程序 → 硬件设备

1.2 驱动的作用

  简单来说,驱动就是充当操作系统内核与硬件设备之间的 “翻译官”,即将内核的 通用指令(如 “读取数据”)翻译成硬件能听懂的 专属语言(如操作特定寄存器的指令),同时将硬件的 “状态反馈”(如 “数据读取完成”)翻译成内核能理解的信号;
Linux
驱动的核心作用可归纳为以下四点

屏蔽硬件差异,实现 “硬件无关性”​
  不同厂商的同类型硬件(如
Intel

Realtek

Broadcom
的网卡),底层寄存器布局、数据传输协议可能完全不同。驱动通过统一的接口封装这些差异,让内核用相同的逻辑操作所有同类硬件;如无论使用哪种网卡,内核都通过相同的网络子系统接口发送 / 接收数据包,无需修改代码;管理硬件资源,避免资源冲突
  硬件工作需占用系统资源(如中断号、
IO
端口地址、物理内存块、
DMA
通道),这些资源由内核统一管理;驱动需向内核 “申请” 所需资源(如通过
request_irq
申请中断号),内核验证资源未被占用后分配给驱动;驱动卸载时需 “释放” 资源,供其他硬件复用,避免资源冲突导致系统崩溃。;​实现数据传输与硬件控制​
  驱动是内核与硬件之间的 “数据搬运工” 和 “指令执行者”:​
指令传递:内核通过驱动向硬件发送控制指令(如 设置显卡分辨率为
1920×1080
、让
U
盘进入读写模式);​数据读写:负责内核与硬件间的数据流交互(如从摄像头读取图像数据、向硬盘写入文件数据);​中断处理:硬件触发中断(如 数据接收完成、
USB
设备插入”)时,驱动的中断处理函数会被内核调用,及时响应硬件事件;​ 处理硬件异常,保障系统稳定​
  驱动需监测硬件的异常状态(如磁盘坏道、
USB
设备意外断开、传感器数据超量程),并向内核反馈异常信息(如通过
printk
打印错误日志),内核可基于这些信息采取补救措施(如标记坏道、重新枚举
USB
设备),避免硬件异常扩散为系统崩溃。

2. Linux 驱动的分类

2.1 按设备类型分类

  根据 硬件设备的工作方式和功能特性,驱动可以分为:

字符设备驱动(
Character Device Driver

核心特征:
字节流
顺序读写数据
,无缓存(或极简缓存),像 “流水线” 一样处理数据,数据传输实时性强,通常不支持随机访问;操作接口:通过
/dev
目录下的
字符设备文件
(类型标识为
c
)访问,支持
open

read

write

ioctl
等系统调用;​典型设备:串口(
/dev/ttyS0
)、键盘、鼠标、
LED
灯、
ADC
转换器、触摸屏、串口打印机;​关键结构体:
struct file_operations
定义驱动支持的文件操作函数(如
read
对应驱动的
led_read
函数); 块设备驱动(
Block Device Driver

核心特征:
固定大小的块
(通常为
512
字节、
4KB

8KB
)读写数据
,有复杂的缓存机制(如页缓存),可缓存数据提升存储效率,支持随机访问;​操作接口:通过
/dev
目录下的
块设备文件
(类型标识为
b
)访问,支持
open

read

write
,且可挂载文件系统(如
mount /dev/sda1 /mnt
);​典型设备:硬盘(
/dev/sda
)、U 盘(
/dev/sdb1
)、
SD
卡(
/dev/mmcblk0p1
)、固态硬盘(
SSD
)、虚拟磁盘(
/dev/loop0
);​关键机制:依赖内核的
块 IO 子系统

Block IO Subsystem
),处理缓存、
IO
调度(如
CFQ

noop
调度算法)、异步
IO
网络设备驱动(
Network Device Driver

核心特征:无传统设备文件,通过内核
网络子系统
提供的
socket
接口访问,专注于 “数据包收发”;​操作逻辑:驱动将硬件接收的数据包交给网络子系统(如
TCP/IP
协议栈),将网络子系统的发送数据包传递给硬件;​典型示例:有线网卡(
eth0
)、无线网卡(
wlan0
)、虚拟网卡(
lO
,回环接口)、
VPN
虚拟网卡;​关键结构体:
struct net_device
定义网卡的
MAC
地址、
MTU
、数据包收发函数(如
hard_start_xmit
负责发送数据包); 杂项设备驱动(
Misc Device Driver

核心特征:简化的字符设备,主设备号固定为
10
(无需手动申请主设备号),适合功能简单的小设备;​适用场景:
GPIO
按键、小型传感器(如温湿度传感器)、自定义硬件模块(如
FPGA
扩展接口);​优势:开发门槛低,无需处理主设备号分配(避免设备号冲突),只需实现核心功能函数;​关键函数:
misc_register()
注册杂项设备;
misc_deregister()
注销杂项设备。

2.2 按加载方式分类

  根据 驱动与内核的耦合方式,驱动分为:

内置驱动(
Built-in Driver

特点:编译时直接嵌入内核镜像文件(如
zImage

vmlinuz
),系统启动时随内核一同加载,无法卸载;​适用场景:核心硬件(如
CPU
、内存控制器、主板芯片组、启动磁盘控制器),这类硬件必须在系统启动初期就绪,否则内核无法正常初始化;​优点:启动速度快,无需额外加载步骤;​缺点:占用内核镜像空间,即使硬件未使用也会占用内存; 内核模块驱动(
Kernel Module Driver

特点:独立编译为
.ko

Kernel Object
)文件,可通过命令(
insmod

modprobe
)动态加载到内核,也可通过
rmmod
卸载,不占用内核镜像空间;​适用场景:大部分外设(如
USB
设备、网卡、声卡、摄像头、
LED
灯),支持热插拔;​优点:灵活高效,调试时无需重启系统,节省内存;​核心机制:依赖内核的 “模块管理子系统”,加载时解析模块依赖(如依赖其他模块的函数),卸载时检查模块是否被占用(如无进程使用则允许卸载)。

2.3 按硬件连接方式分类

   根据 硬件的连接方式,驱动分为:

总线驱动(
Bus Driver

管理特定的总线类型的设备,如
USB

I2C

SPI

PCIe
等;负责总线上设备的枚举(识别设备)、资源分配(分配地址、中断),为设备驱动提供基础支持(如
USB
总线驱动枚举
U
盘、
U
盘设备驱动基于总线驱动工作); 平台驱动(
Platform Driver

针对芯片内部 “无标准总线的硬件”(如
SoC
上的
GPIO
、定时器、
ADC
),这些设备没有独立的物理总线,依赖
CPU
的内部总线;采用
平台设备 - 平台驱动
模型,通过设备树
Device Tree
描述硬件信息,是嵌入式
Linux
中最常用的驱动类型(如
LED
、按键驱动多基于 Platform 框架)。

2.4 按功能角色分类

   根据 设备的功能角色,驱动分为:

物理设备驱动:直接控制真实硬件(如 键盘、硬盘、网卡)的驱动,是最常见的类型;虚拟设备驱动:不对应真实硬件,而是模拟出的设备,如 虚拟终端
/dev/tty
、内存盘
/dev/ram
、环路设备(
/dev/loop
用于挂载
ISO
文件)等,用于扩展系统功能,简化对硬件的抽象访问。

3. Linux 驱动的基本框架

  
Linux
驱动框架主要由以下几个核心组件构成:模块加载函数模块卸载函数许可证声明模块参数模块导出符号作者信息,每个组件都有明确的作用和标准实现方式,接下来我们将逐一解析这些组件的作用:

3.1 模块加载函数(
module_init

基本说明:是
驱动入口函数
,当加载驱动模块时,内核会执行模块加载函数,完成模块加载函数中的初始化工作核心作用:是驱动被
insmod

modprobe
加载时第一个执行的函数
,主要完成
硬件初始化(如配置寄存器、申请
GPIO
/ 中断资源)驱动核心结构注册(如字符设备的
cdev_add

Platform
驱动的
platform_driver_register
)内存 / 资源申请(如通过
kmalloc
分配内核内存) 实现方式:通过
module_init(函数名)
宏声明加载函数
函数返回值为
int

0
表示成功,
负数
表示失败函数命名通常为xxx_init(xxx 为驱动名) 注意事项
函数前的__init宏:告诉编译器 “该函数仅在模块加载时执行,执行后可释放内存”,节省内核空间;​错误处理:若某一步初始化失败(如 GPIO 申请失败),需反向释放已申请的资源(如已申请的内存需kfree),避免资源泄漏;​日志打印:使用printk而非printf(内核态无 C 标准库),并通过KERN_ERR/KERN_INFO指定日志级别(便于调试)

3.2 模块卸载函数(
module_exit

基本说明:是
驱动出口函数
,当卸载驱动模块时,内核会执行模块卸载函数,完成模块卸载函数中的退出工作核心作用:是驱动被
rmmod
卸载时最后执行的函数
,主要完成
释放加载函数中申请的所有资源(如
GPIO
、中断、内存)注销驱动核心结构(如
cdev_del

platform_driver_unregister
)清理驱动运行时产生的临时数据 实现方式:通过
module_exit(函数名)
宏声明卸载函数,函数无返回值(
void
),函数命名通常为
xxx_exit
注意事项
函数前的
__exit
宏:仅对可卸载模块有效,告诉编译器 该函数仅在模块卸载时执行;​资源释放顺序:与加载函数的资源申请顺序相反(如先申请
GPIO
再申请内存,卸载时先释放内存再释放
GPIO
),避免依赖资源未释放导致崩溃;​卸载合法性:若驱动正在被使用(如用户进程已打开
/dev/led
设备文件),内核会阻止卸载(
rmmod
会返回 “资源忙” 错误)。

3.3 模块许可证声明(Module License)

基本说明:是
驱动的“合规标识”
,许可证声明描述了内核模块的许可权限,如果不声明模块许可,模块在加载的时候,会收到 内核被污染(
kernel tainted
的警告核心作用声明驱动的开源协议决定驱动能否与
Linux
内核(
GPL
协议)兼容
,是驱动加载的必要条件(无此声明会导致内核警告,甚至拒绝加载)常见的许可证类型

GPL

GNU
通用公共许可证
(与
Linux
内核协议一致),常用于需调用内核
GPL
导出符号(如
EXPORT_SYMBOL_GPL
)的驱动
GPL v2
:**
GLP
协议第 2 版*,是最常用的许可证类型,兼容绝大多数内核版本
MIT

MIT
许可证(宽松开源协议)
,是不依赖内核
GPL
符号的独立驱动
Proprietary
闭源协议,是商业闭源驱动(需内核支持 “闭源模块”,部分内核禁用) 实现方式:通过
MODULE_LICENSE("许可证类型")
宏声明,通常放在驱动代码末尾注意事项
若驱动使用了内核的
EXPORT_SYMBOL_GPL
导出的函数(如某些
GPIO
操作函数),必须声明为
GPL

GPL v2
协议,否则加载时内核会报错
“disagrees about version of symbol xxx”
无许可证声明的驱动加载时,内核会打印警告
“module xxx does not have a GPL compatible license”
,部分内核会直接拒绝加载

3.4 模块参数(module_param)

基本说明
驱动动态配置接口
,内核模块参数是驱动模块被加载的时候可以传递给内核的值核心作用允许用户在加载驱动时动态传递参数(如
GPIO
号、设备地址),避免驱动代码硬编码(无需修改代码即可适配不同硬件)支持的参数类型
基本类型
int

char

char *
(字符串)、
bool
(布尔值)数组类型:通过
module_param_array
声明,支持多值传递 实现方式
单个参数声明:通过
module_param(参数名, 类型, 权限)
宏声明,参数说明
参数名:内核空间变量名类型:参数数据类型(如
int

charp
表示字符串)权限:参数文件(
/sys/module/模块名/parameters/参数名
)的访问权限(如
0644
表示所有者可读可写,其他用户只读) 数组参数声明:通过
module_param_array(数组名, 类型, 数组长度指针, 权限)
宏声明,支持加载时传递多个值


#include <linux/moduleparam.h>

// 1. 定义模块参数(默认值为10)
static int led_gpio = 10;
// 声明int类型参数“gpio”,用户加载时可通过“gpio=xx”修改
module_param(led_gpio, int, 0644);
// 为参数添加描述(可选,通过“modinfo 驱动.ko”可查看)
MODULE_PARM_DESC(led_gpio, "GPIO number for LED (default: 10)");

// 2. 定义数组参数(支持多个LED GPIO)
static int led_gpios[5];  // 最多5个GPIO
static int gpio_count;    // 实际传递的GPIO数量
// 声明数组参数“gpios”,用户加载时可通过“gpios=10,11,12”传递
module_param_array(led_gpios, int, &gpio_count, 0644);
MODULE_PARM_DESC(led_gpios, "GPIO numbers for multiple LEDs (max 5)");

// 加载函数中使用参数
static int __init led_driver_init(void) {
    int i;
    // 打印单个参数值
    printk(KERN_INFO "Single LED GPIO: %d
", led_gpio);
    // 打印数组参数值
    printk(KERN_INFO "Multiple LED GPIOs count: %d
", gpio_count);
    for (i = 0; i < gpio_count; i++) {
        printk(KERN_INFO "LED GPIO %d: %d
", i, led_gpios[i]);
    }
    return 0;
}

使用方式:用户加载驱动时通过
参数名 = 值
传递参数
传递单个参数:
insmod led_drv.ko led_gpio=12 // 指定LED GPIO为12
传递数组参数:
insmod led_drv.ko led_gpios=10,11,12 // 指定3个LED GPIO为10、11、12
查看参数当前值(通过
sysfs
):
cat /sys/module/led_drv/parameters/led_gpio

3.5 模块导出符号(EXPORT_SYMBOL)

基本说明:是
驱动功能复用接口
,内核模块可以导出的符号,导出符号以后其他内核模块可以使用本模块中的变量或函数核心作用:将 驱动中的函数或变量导出到内核符号表,允许其他内核模块(驱动)调用该函数或访问该变量,实现驱动功能复用(如
A
驱动的
GPIO
控制函数导出后,
B
驱动可直接调用)实现方式
EXPORT_SYMBOL(符号名):导出符号,所有内核模块均可访问EXPORT_SYMBOL_GPL(符号名):导出符号,仅 GPL 协议的内核模块可访问(符合 Linux 内核开源合规性)


// 定义LED控制函数(要导出的符号)
void led_set_status(int gpio, int status) {
    if (status == 1) {
        gpio_set_value(gpio, 1);  // LED亮
    } else {
        gpio_set_value(gpio, 0);  // LED灭
    }
}
// 导出函数,仅GPL协议模块可调用
EXPORT_SYMBOL_GPL(led_set_status);

// 定义全局变量(要导出的符号)
int led_max_brightness = 100;
// 导出变量,所有模块可访问
EXPORT_SYMBOL(led_max_brightness);

注意事项
导出符号的声明:需在头文件中声明导出的函数 / 变量(如
extern void led_set_status(int gpio, int status);
),供其他模块包含依赖管理:若
B
模块调用
A
模块的导出符号,加载
B
模块前必须先加载
A
模块(否则
B
模块加载失败,提示
“undefined symbol”
)符号表查看:通过
cat /proc/kallsyms | grep 符号名
查看导出的符号(如
cat /proc/kallsyms | grep led_set_status

3.6 模块作者/描述信息(MODULE_AUTHOR)

基本说明:是驱动的说明文档,用于添加驱动的作者、版本、描述等元信息,便于开发者维护和调试(通过modinfo命令可查看这些信息),无功能影响但属于 “规范开发” 的必要部分实现方式:通过以下宏声明

MODULE_AUTHOR("作者信息")
:驱动作者(如姓名、邮箱)
MODULE_DESCRIPTION("驱动描述")
:驱动功能说明
MODULE_VERSION("版本号")
:驱动版本(如
“v1.0”

MODULE_ALIAS("别名")
:驱动别名(如字符设备的设备名,便于
udev
匹配


// 作者信息
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
// 驱动描述
MODULE_DESCRIPTION("Simple LED Driver for Embedded Linux");
// 驱动版本
MODULE_VERSION("v1.0");
// 驱动别名(字符设备示例,对应/dev/led)
MODULE_ALIAS("char-major-240-0");

查看方式:通过
modinfo
命令查看模块元信息,如
modinfo led_drv.ko

3.7 第一个驱动程序 helloword


#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/moduleparam.h>

// 1. 模块参数定义与声明
static int led_gpio = 10;
module_param(led_gpio, int, 0644);
MODULE_PARM_DESC(led_gpio, "GPIO number for LED (default: 10)");

// 2. 导出符号(LED控制函数)
void led_set_status(int gpio, int status) {
    if (gpio_is_valid(gpio)) {
        gpio_set_value(gpio, status);
        printk(KERN_INFO "LED GPIO %d set to %d
", gpio, status);
    }
}
EXPORT_SYMBOL_GPL(led_set_status);

// 3. 模块加载函数
static int __init led_driver_init(void) {
    int ret;
    // 申请GPIO资源
    ret = gpio_request(led_gpio, "led_gpio");
    if (ret < 0) {
        printk(KERN_ERR "Failed to request GPIO %d
", led_gpio);
        return ret;
    }
    // 配置GPIO为输出,初始灭
    gpio_direction_output(led_gpio, 0);
    printk(KERN_INFO "LED driver loaded. GPIO: %d
", led_gpio);
    return 0;
}

// 4. 模块卸载函数
static void __exit led_driver_exit(void) {
    // 释放GPIO资源
    gpio_free(led_gpio);
    printk(KERN_INFO "LED driver unloaded. GPIO: %d
", led_gpio);
}

// 5. 模块声明与元信息
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_DESCRIPTION("Simple LED Driver with Full Module Framework");
MODULE_VERSION("v1.0");

4. Linux 驱动的编译方法

   Linux 驱动的编译:核心是将驱动代码(通常是
.c
文件)与内核源码结合,生成可在目标系统运行的二进制文件可加载模块(
.ko
模块
内核镜像)的过程;常见的编译方法主要有两种 内置编译(将驱动集成到内核)和 模块编译(以独立
.ko
模块形式编译),而在实际开发中,常用模块编译的方式,因为无需重新编译整个内核,调试效率更高。

4.1 驱动编译的核心原理

1. 编译依赖

   驱动编译本质
基于内核源码的二次编译
,需依赖以下三个核心组件:

内核源码:必须与目标系统内核版本完全一致(通过
uname -r
查看,如
5.10.100
),提供驱动所需的头文件、
API
函数实现及编译规则;​编译工具链:根据目标架构选择(
x86
架构用
gcc

ARM
架构用
arm-linux-gnueabihf-gcc
),负责将源码编译为二进制指令;​内核配置文件(
.config
:存储内核编译选项(如是否支持某类硬件、是否开启模块功能),驱动编译需遵循此配置(如内核未开启
GPIO
支持,
GPIO
驱动会编译失败);

2. 编译输出产物​

模块编译:生成独立的
.ko

Kernel Object
)文件,可通过
insmod
/
modprobe
动态加载到内核;​内置编译:驱动代码嵌入内核镜像文件(如
zImage

vmlinuz
),随内核启动一同加载,无独立文件。

4.2 模块编译

   模块编译:是把驱动编译成可加载的模块,然后使用命令
insmod
/
modprobe
把驱动加载到内核里面;内核模块(.ko文件)是独立的二进制文件,可以在运行时动态加载 / 卸载,无需重新编译整个内核,仅编译驱动代码生成.ko文件,灵活高效,是驱动开发调试的首选方式;
  模块编译的具体使用过程如下

准备内核源码:下载与目标系统一致的内核源码,解压源码并生成
.config
配置文件(若已有配置文件可跳过),编译内核(生成驱动编译所需的中间文件,如
vmlinux
、头文件)编写驱动代码文件
xx.c
:如
led_drv.c
(包含模块加载 / 卸载、参数等完整框架);编写 Makefile 文件(文件名固定):通过
Makefile
指定内核源码路径、编译工具链及目标文件;


# 1. 指定目标内核源码路径(需替换为实际路径)
# 本地编译(x86架构,目标系统与编译机同内核):可设为/lib/modules/$(shell uname -r)/build
# 交叉编译(ARM架构):设为下载的内核源码路径(如/home/user/linux-5.10.100)
KERNELDIR ?= /home/user/linux-5.10.100

# 2. 当前驱动代码所在目录(自动获取)
PWD := $(shell pwd)

# 3. 指定要编译的驱动模块:obj-m += 驱动文件名(不含后缀)
obj-m += led_drv.o  # 单个文件:生成led_drv.ko
# 若驱动由多个文件组成(如led_drv.c、led_gpio.c):
# obj-m += led_drv.o
# led_drv-objs := led_drv.o led_gpio.o

# 4. 编译规则:调用内核Makefile编译驱动
all:
    # 本地编译(x86架构):
    # $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
    # 交叉编译(ARM架构):指定架构和工具链
    $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

# 5. 清理编译产物(.ko、.o、.mod.c等)
clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

编译驱动:在驱动代码目录下执行
make
命令编译驱动模块,编译成功后生成以下文件:


xxx_drv.ko        # 核心驱动模块文件
xxx_drv.mod.c     # 模块依赖关系文件(自动生成)
xxx_drv.mod.o     # 模块依赖目标文件
xxx_drv.o         # 驱动代码目标文件
modules.order     # 模块编译顺序文件
Module.symvers    # 模块导出符号文件

验证编译结果
查看
.ko
文件的目标架构(确保与目标硬件匹配)
:如
arm-linux-gnueabihf-readelf -h led_drv.ko # 查看ARM架构.ko文件信息,输出中“Machine”字段应为“ARM”
查看模块信息(验证模块框架是否完整):如
modinfo xxx_drv.ko # 应显示模块许可证、作者、参数、依赖等信息(与代码中的MODULE_*宏对应)
部署内核与驱动:将生成的
.ko
文件拷贝到目标板,通过
insmod
加载。

4.3 内置编译

  内置编译:是指将驱动直接编译到内核中,适用于核心硬件驱动(如
CPU
、内存控制器、启动磁盘控制器),驱动代码嵌入内核镜像,随内核启动加载,无法动态卸载;
  内置编译的具体使用过程如下

编写驱动代码文件
xx.c
:如
led_drv.c
(包含模块加载 / 卸载、参数等完整框架);将驱动代码加入内核源码树:内核源码中驱动文件按功能分类存放,如 字符设备驱动
linux/drivers/char/

GPIO
驱动
linux/drivers/gpio/
、平台设备驱动
linux/drivers/platform/
等,将
led_drv.c
拷贝到对应目录(如字符设备驱动目录);修改内核目录的 Kconfig 文件(添加配置选项)修改内核目录的 Makefile(添加编译规则):编辑内核驱动目录的
Makefile
(如
linux/drivers/char/Makefile
),添加驱动的编译规则 # 格式:
obj-$(CONFIG_XXX) += 驱动文件名.o
,如
obj-$(CONFIG_LED_DRIVER) += led_drv.o
配置内核(选择驱动编译方式):执行
make menuconfig
,在菜单中找到驱动选项并设置编译方式编译内核与驱动:执行
make
编译内核,驱动会随内核一起编译部署内核与驱动:将生成的
zImage
烧录到目标板的
boot
分区,重启后驱动随内核加载。

5. 模块的加载与卸载

  在
Linux
驱动开发中,模块的加载与卸载是连接编译产物(
.ko
文件)与内核运行的关键步骤;

通过 加载 操作:可将驱动模块动态注入内核并初始化硬件;通过 卸载 操作:可释放资源并移除模块,无需重启系统即可完成驱动更新或调试。

5.1 模块的加载

  模块加载:是将
.ko
文件中的驱动代码、数据结构加载到内核地址空间,执行初始化函数(
module_init
指定的函数),完成硬件资源申请与驱动注册,最终让驱动处于可使用状态;在 Linux 中提供 3 个常用的模块加载命令:

1.
insmod
基础加载命令(不处理依赖)

功能:加载指定的
.ko
模块文件,仅执行模块初始化,不自动解决模块依赖(如驱动依赖
gpio
模块,需手动先加载
gpio
);​语法
insmod [模块文件路径] [模块参数=值]
使用示例
insmod ./led_drv.ko led_gpio=10 # 加载当前目录的 led_drv.ko,传递参数 led_gpio=10(LED连接 GPIO10)
关键特点:​
需指定完整的
.ko
文件路径(如
./led_drv.ko

/lib/modules/led_drv.ko
);​若模块依赖其他模块(如
modinfo led_drv.ko
显示
depends: gpio
),直接用
insmod
加载会失败,需先手动加载依赖模块。

2.
modprobe
智能加载命令(自动处理依赖)

语法
modprobe [选项] 模块名 [模块参数=值]
;​常用选项:​

-v
:显示加载过程的详细日志(便于调试依赖问题);​
-f
:强制加载模块(仅在模块版本与内核版本轻微不匹配时使用,不推荐常规使用); 使用说明

modprobe led_drv led_gpio=10 # 加载led_drv模块(无需指定.ko路径,内核自动从/lib/modules/$(uname -r)/查找)

modprobe -v led_drv led_gpio=10 # 显示详细加载日志(查看依赖加载过程)
关键特点:​
无需指定
.ko
文件路径,内核会从默认路径(
/lib/modules/$(uname -r)/
其中
$(uname -r)
为内核版本)查找模块;​若模块未在默认路径,需先执行
depmod -a
更新模块依赖数据库,或通过
/etc/modprobe.d/
配置文件指定模块路径;​自动处理依赖:如
LED
驱动依赖
gpio
模块,
modprobe
会先加载
gpio
,再加载
led_drv

3.
modprobe -a
批量加载多个模块

功能:一次性加载多个模块,适用于需要同时加载多个依赖模块的场景;使用示例
modprobe -a gpio led_drv led_gpio=10 # 同时加载gpio和led_drv模块

4. 模块加载的底层机制

  加载命令执行后,内核会按以下流程完成模块注入:​

文件校验:检查
.ko
文件的合法性(如
ELF
格式、目标架构与内核匹配、版本魔法字
version magic
一致);​内存分配:在内核地址空间分配内存,用于存储模块的代码段、数据段及符号表;​符号解析:解析模块中引用的内核符号(如
gpio_request

printk
),将符号地址与内核中的实际地址绑定;​依赖加载(仅
modprobe
:通过
/lib/modules/$(uname -r)/modules.dep
(模块依赖数据库)分析依赖,递归加载所有未加载的依赖模块;​执行初始化函数:调用模块的
module_init
指定函数(如
led_driver_init
),完成硬件初始化(申请
GPIO
、注册字符设备等);​模块注册:将模块信息(如模块名、占用内存、导出符号)添加到内核的模块管理链表(
struct module
链表),供后续查询或卸载。

5. 模块加载成功的验证方法

查看已加载模块列表
命令:
lsmod
(本质是读取
/proc/modules
文件)输入:
lsmod | grep led_drv
输出:
led_drv 16384 0 # 模块名:led_drv,占用内存:16384字节,被引用次数:0
查看模块详细信息
命令:
modinfo 模块名
(查看模块的元信息、参数、依赖等)输入:
modinfo led_drv
输出:


filename:       /lib/modules/5.10.100/led_drv.ko
version:        v1.0
description:    Simple LED Driver
author:         shenx
license:        GPL v2
depends:        gpio  # 模块依赖gpio
parm:           led_gpio:GPIO number for LED (default: 10) (int)  # 模块参数

查看内核日志
命令:
dmesg
(查看内核环形缓冲区日志),常用
dmesg | tail -n 10
查看最新
10
条日志输入:
dmesg | tail -5
输出:


[1234.567890] Allocated dev number: major=240, minor=0  # 设备号分配成功
[1234.568901] LED driver loaded successfully! GPIO: 10  # 驱动初始化成功

5.2 模块的卸载

  模块卸载:是执行模块的清理函数(
module_exit
指定的函数),释放加载时申请的所有资源(如
GPIO
、设备号、内存),将模块从内核管理链表中移除,回收内核地址空间;

1.
rmmod
基础卸载命令​

功能卸载指定的已加载模块,需指定模块名(非
.ko
文件路径);​语法
rmmod [选项] 模块名
;​常用选项:​

-v
:显示卸载过程的详细日志;​
-f
:强制卸载模块(仅在模块无法正常卸载时临时使用,可能导致资源泄漏或系统不稳定,不推荐);​ 使用示例

rmmod led_drv​ # 卸载led_drv模块​

rmmod -v led_drv​ # 显示详细卸载日志​

2.
modprobe -r
智能卸载命令(自动处理依赖)​

功能:卸载模块时自动卸载 “仅被当前模块依赖” 的依赖模块,避免误卸载被其他模块使用的依赖;​语法
modprobe -r [选项] 模块名
;​使用示例
​modprobe -r led_drv​ # 卸载led_drv
,若
gpio
模块仅被
led_drv
依赖,则同时卸载
gpio​
关键特点:​
安全卸载:仅卸载无其他模块依赖的模块,如
gpio
模块若还被
key_drv
依赖,则仅卸载
led_drv
,保留
gpio
;​批量卸载:支持同时卸载多个模块(如
modprobe -r gpio led_drv
)。​

3. 模块卸载的底层机制​

  卸载命令执行后,内核会按以下流程完成模块移除:​

依赖检查:检查当前模块是否被其他模块引用(如
led_drv
依赖
gpio
,若
gpio被
其他模块引用,
led_drv
可正常卸载;若
led_drv
被其他模块引用,则卸载失败);​资源清理:调用模块的
module_exit
指定函数(如
led_driver_exit
),释放加载时申请的资源(如
gpio_free
释放
GPIO

unregister_chrdev_region
释放设备号);​符号注销:将模块导出的符号(如
led_set_status
)从内核符号表中移除,避免其他模块后续引用无效符号;​内存回收:回收模块占用的内核地址空间(代码段、数据段),并将模块从内核模块管理链表中删除;​日志记录:在内核日志中记录卸载成功信息,供后续查看。​

4. 卸载成功的验证方法​

确认模块已从列表中移除​
命令
lsmod | grep 模块名
若无输出则表示卸载成功;​使用示例:​
lsmod | grep led_drv # 无输出,说明led_drv已卸载​

查看内核卸载日志​
命令
dmesg | tail -n 5
查看卸载相关日志;​输出示例(表示卸载成功):​

c [1567.890123] LED driver unloaded successfully! GPIO: 10 # 驱动清理完成​ [1567.891234] Released dev number: major=240, minor=0 # 设备号释放完成​ ​

5.3 模块加载 / 卸载的注意事项​

1. 模块参数的传递方式​

加载时传递:通过
insmod

modprobe
命令在模块名后添加
参数名=值
,多个参数用空格分隔;​如 传递多个参数​
modprobe led_drv led_gpio=10 led_brightness=80​ # LED驱动传递GPIO和亮度参数​

加载后修改:若模块参数权限允许(如
0644
),可通过
/sys/module/
模块名/parameters/参数名文件动态修改参数值;​如 动态修改
LED
亮度​
查看当前亮度参数:
cat /sys/module/led_drv/parameters/led_brightness
​修改亮度为
100
(需
root
权限)​:
echo 100 > /sys/module/led_drv/parameters/led_brightness​

2. 卸载失败的常见原因与解决方法​

原因 1:模块被其他模块依赖​
错误现象:执行
rmmod led_drv
时提示
“rmmod: ERROR: Module led_drv is in use by: xxx”

xxx
为依赖该模块的其他模块);​解决方法:先卸载依赖当前模块的其他模块,再卸载当前模块;​使用示例:若
led_drv

app_drv
依赖,需先卸载
app_drv


rmmod app_drv​
rmmod led_drv​

原因 2:模块被用户进程占用​
错误现象:提示
“rmmod: ERROR: Module led_drv is in use”
,无其他模块依赖,说明驱动对应的设备文件(如
/dev/led_char_dev
)被用户进程打开;​解决方法:​
查找占用设备文件的进程:
fuser /dev/led_char_dev
(显示占用进程的
PID
);​终止占用进程:
kill -9 PID

PID
为上一步查到的进程
ID
);​重新卸载模块;​ 使用示例:​


# 查找占用/dev/led_char_dev的进程​
fuser /dev/led_char_dev​
# 输出:/dev/led_char_dev:  1234  # PID为1234的进程占用​
# 终止进程​
kill -9 1234​
# 卸载模块​
rmmod led_drv​

原因 3:模块初始化失败未清理资源​
错误现象:模块加载时初始化失败(如
GPIO
申请失败),但未正确释放已申请的资源(如设备号),导致卸载时提示
“resource busy”
;​解决方法:​
在驱动代码中完善错误处理逻辑,确保初始化失败时反向释放资源(如
goto
语句链);​若已出现此问题,可通过
reboot
重启系统强制释放资源(仅适用于调试场景)。​

3. 开机自动加载模块的配置方法​

  实际应用中,常需驱动在系统开机时自动加载,无需手动执行
modprobe
,可通过以下两种方式配置:​

通过
/etc/modules
文件(
Debian/Ubuntu
系统)​
编辑
/etc/modules
文件,在末尾添加模块名及参数(每行一个模块);​使用示例(配置
LED
驱动开机加载):​


sudo vi /etc/modules​
# 添加以下内容​
led_drv led_gpio=10​

生效方式:重启系统后自动加载,或执行
modprobe -a $(cat /etc/modules)
立即加载。​ 通过
/etc/modprobe.d/
配置文件(通用方法)​

/etc/modprobe.d/
目录下创建
.conf
后缀的配置文件,添加
options 模块名 参数=值
(设置默认参数)和
install 模块名 /sbin/modprobe --ignore-install 模块名
(确保加载);​使用示例(创建 LED 驱动配置文件):


sudo vi /etc/modprobe.d/led_drv.conf
# 添加以下内容
options led_drv led_gpio=10  # 设置默认参数led_gpio=10
install led_drv /sbin/modprobe --ignore-install led_drv  # 配置自动加载​
```​

生效方式:重启系统后自动加载,或执行
depmod -a
更新依赖后用
modprobe led_drv
加载。

6. 查看模块信息

  查看 Linux 驱动模块的信息是驱动开发和调试中的重要环节,通过工具可以获取模块的元数据、参数、依赖关系、符号表等关键信息,帮助理解模块功能、排查加载问题;

6.1 modinfo 查看模块元数据与参数(最常用)

  
modinfo
是查看模块信息的核心命令
可提取
.ko
文件或已加载模块的元数据(如作者、许可证)、模块参数、依赖关系等
,信息来源于模块代码中的
MODULE_*
宏定义(如
MODULE_AUTHOR

MODULE_PARAM
)。

基本用法
modinfo [选项] 模块名或模块文件路径
常用选项

-n
:仅显示模块文件的完整路径(已加载模块)
-k
:指定内核版本(用于查看其他内核的模块信息)
-0
:用
NULL
字符分隔输出(便于脚本解析) 使用示例:如
led_drv.ko
模块,执行
modinfo led_drv.ko
(或模块已加载时直接
modinfo led_drv
),输出如下:


filename:       /home/root/led_drv.ko  # 模块文件路径
version:        v1.0                   # 模块版本(MODULE_VERSION)
description:    Simple LED Driver for Embedded Linux  # 模块描述(MODULE_DESCRIPTION)
author:         Zhang San <zhangsan@example.com>  # 作者信息(MODULE_AUTHOR)
license:        GPL v2                 # 许可证(MODULE_LICENSE)
srcversion:     A1B2C3D4E5F6G7H8I9J0K1L2  # 源码版本哈希(自动生成)
depends:        gpio,platform_driver   # 依赖的其他模块(通过代码中引用的符号自动生成)
retpoline:      Y                      # 是否启用retpoline(内核安全机制)
name:           led_drv                # 模块名
vermagic:       5.10.100 SMP mod_unload modversions ARMv7 p2v8  # 版本魔法字(内核版本+配置)
parm:           led_gpio:GPIO number for LED (default: 10) (int)  # 模块参数(MODULE_PARAM_DESC)
parm:           led_brightness:LED brightness (0-100) (int)  # 另一模块参数

6.2 lsmod 查看已加载模块列表与依赖关系

  
lsmod
用于列出当前系统中所有已加载的内核模块,显示模块名、占用内存、被引用次数等信息
,本质是读取
/proc/modules
文件。

基本用法


lsmod  # 列出所有已加载模块
lsmod | grep 模块名  # 过滤特定模块(如lsmod | grep led_drv)

输出解析:执行
lsmod
后输出


Module                  Size  Used by
led_drv                16384  0   # 模块名:led_drv,占用内存:16KB,被引用次数:0
gpio                   28672  1 led_drv  # gpio被led_drv引用(Used by: 1 led_drv)
platform_driver        12288  1 led_drv
其中: Size:模块占用的内核内存大小(单位:字节);
	  Used by:引用该模块的其他模块数量及名称(如gpio被led_drv引用,因此Used by: 1 led_drv),若为0表示无模块引用,可安全卸载

6.3 dmesg 查看模块加载 / 卸载日志

  模块的加载(
module_init
)和卸载(
module_exit
)过程中,通过
printk
打印的日志会记录在
kernel ring buffer
中,可通过
dmesg
查看,用于验证模块初始化是否成功。

基本用法
dmesg | tail -10 # 查看最近10条日志
输出解析:以 LED 驱动为例


[1234.567890] LED driver: allocating dev number (major=240)  # 初始化日志
[1234.568901] LED driver: GPIO 10 requested successfully     # GPIO申请成功
[1234.569012] LED driver loaded successfully                 # 加载完成
[1567.890123] LED driver: releasing GPIO 10                  # 卸载时释放资源
[1567.891234] LED driver unloaded successfully               # 卸载完成

6.4 /sys/module/ 通过 sysfs 查看模块动态信息

  内核通过
sysfs
文件系统(挂载在
/sys
)提供模块的动态信息,每个已加载模块对应
/sys/module/模块名/目录
,包含参数、属性、依赖等实时数据;其核心目录与文件:


/sys/module/模块名/parameters/
:模块参数的当前值,可直接读写(需权限);如查看 / 修改
led_drv

led_gpio
参数


# 查看当前值
cat /sys/module/led_drv/parameters/led_gpio
# 修改值(需root权限)
echo 12 > /sys/module/led_drv/parameters/led_gpio


/sys/module/模块名/drivers/
:模块关联的驱动(如字符设备、平台设备驱动);
/sys/module/模块名/refcnt
:模块被引用的次数(与
lsmod

Used by
一致);
/sys/module/模块名/sections/
:模块代码段、数据段的内核地址(调试用)。

6.5 /proc/kallsyms 查看模块导出符号

  模块通过
EXPORT_SYMBOL

EXPORT_SYMBOL_GPL
导出的符号(函数、变量)会记录在
/proc/kallsyms
中,供其他模块调用,可通过该文件查看模块导出的符号及地址。

基本用法
grep led_drv /proc/kallsyms # 查看led_drv模块导出的符号
输出解析


ffffffffc0001234 T led_set_status    [led_drv]  # T表示函数,[led_drv]表示所属模块
ffffffffc0001345 D led_max_brightness [led_drv]  # D表示变量


T
:表示导出的函数(
Text
段);
D
:表示导出的变量(
Data
段);地址(如
ffffffffc0001234
)是符号在内核地址空间的位置(调试时用于定位函数)。

7. 驱动模块传参

7.1 模块传参概述

  模块参数:是
Linux
驱动与用户空间交互的基础接口之一,允许用户在加载驱动时动态配置参数(如
GPIO
号、缓冲区大小、调试开关等),无需修改驱动代码即可适配不同硬件环境或功能需求;如可以传递串口驱动的波特率、数据位数、校验位、停止位等参数进行功能的设置,从而节省编译模块的时间,提高调试速度;其核心优势:

增强驱动灵活性,同一驱动可适配多硬件场景支持动态调试(如通过参数开启日志输出)简化驱动部署流程,无需重新编译即可调整配置

1. 支持的数据类型

类型标识 对应
C
类型
说明

int

int

32
位整数

long

long

64
位整数

charp

char *
字符串(内核自动分配内存)

bool

bool
布尔值(
1/0

y/n/on/off

ushort
unsigned short 无符号短整数

uint

unsigned int
无符号整数

ulong

unsigned long
无符号长整数

2. 核心宏定义

  模块参数通过以下宏定义实现(需包含头文件
<linux/moduleparam.h>
):


module_param(name, type, perm)

功能:定义单个参数参数

name
:参数变量名(内核空间全局变量)
type
:参数类型(如
int

charp

bool

perm
:参数文件权限(
/sys/module/<模块名>/parameters/<参数名>
的权限,如
0644
示例
module_param(led_gpio, int, 0644);

module_param_array(name, type, nump, perm)

功能:定义数组参数参数

name
:数组变量名
type
:数组元素类型
nump
:指针变量,用于存储实际传入的元素数量
perm
:权限(同基本类型) 示例
module_param_array(gpios, int, &count, 0644);

MODULE_PARM_DESC(name, desc)

功能:为参数添加描述信息(
modinfo
可见)参数

description
:参数功能说明(通过
modinfo
可查看) 示例
MODULE_PARM_DESC(led_gpio, "LED GPIO number (0-127)");

7.2 实现步骤

1. 单个参数实现步骤

定义参数变量:在驱动代码中定义全局变量(或静态全局变量),用于存储参数值:


#include <linux/module.h>
#include <linux/moduleparam.h>

// 定义参数变量,设置默认值(未传参时使用)
static int led_gpio = 10;  // 默认GPIO10
static bool debug = false; // 默认关闭调试模式
static char *led_name = "user_led";  // 默认名称

声明参数:使用
module_param
宏声明参数,指定变量名、类型和权限


// 声明int类型参数“led_gpio”,权限0644(所有者可读写,其他只读)
module_param(led_gpio, int, 0644);
// 声明bool类型参数“debug”
module_param(debug, bool, 0644);
// 声明字符串参数“led_name”(类型标识为charp)
module_param(led_name, charp, 0644);

添加参数描述:使用
MODULE_PARM_DESC
宏添加描述,便于用户通过
modinfo
查看参数含义


MODULE_PARM_DESC(led_gpio, "GPIO number for LED control (range: 0-127, default: 10)");
MODULE_PARM_DESC(debug, "Enable debug log (0=disable, 1=enable, default: 0)");
MODULE_PARM_DESC(led_name, "Name of the LED (default: 'user_led')");

在驱动中使用参数:在初始化函数或其他逻辑中使用参数变量


MODULE_PARM_DESC(led_gpio, "GPIO number for LED control (range: 0-127, default: 10)");
MODULE_PARM_DESC(debug, "Enable debug log (0=disable, 1=enable, default: 0)");
MODULE_PARM_DESC(led_name, "Name of the LED (default: 'user_led')");

2. 数组参数实现步骤

定义数组变量


#define MAX_GPIO_COUNT 5  // 最大支持5个GPIO
static int gpios[MAX_GPIO_COUNT];  // 存储GPIO号的数组
static int gpio_count;  // 实际传递的参数数量(由内核自动填充)

声明数组参数:使用module_param_array宏声明


// 声明数组参数“gpios”,类型int,实际数量存入gpio_count,权限0644
module_param_array(gpios, int, &gpio_count, 0644);
MODULE_PARM_DESC(gpios, "Array of GPIO numbers (max 5, e.g., gpios=10,11,12)");

使用数组参数


static int __init multi_led_init(void) {
    int i;
    printk(KERN_INFO "Total GPIOs received: %d
", gpio_count);
    for (i = 0; i < gpio_count && i < MAX_GPIO_COUNT; i++) {
        printk(KERN_INFO "GPIO %d: %d
", i, gpios[i]);
        // 初始化每个GPIO...
    }
    return 0;
}

3. 参数权限说明

  
module_param
宏的
perm
参数指定
/sys/module/[模块名]/parameters/[参数名]
文件的访问权限,遵循
Linux
文件权限规则(如
0644
表示
-rw-r--r--
):

权限值:由
3
位八进制数组成(用户、组、其他),常用值:

0444
:只读(所有用户可读取,不可修改)
0644
:所有者可读写,其他只读(推荐用于可动态调整的参数)
0000
:无权限(参数仅加载时可配置,运行时不可查改) 注意
若权限包含写权限(如
0644
),用户可在模块加载后通过
sysfs
动态修改参数值权限值需包含
S_IRUGO

0444
)才能通过
modinfo
查看参数,否则参数描述不可见

7.3 参数使用方法

1. 加载时传递参数


insmod
命令传参
:需指定
.ko
文件路径,参数以
参数名=值
形式附加在后面


# 单个参数
insmod led_drv.ko led_gpio=12 debug=1

# 字符串参数(直接传值,无需引号)
insmod led_drv.ko led_name=status_led

# 数组参数(用逗号分隔多个值)
insmod led_drv.ko gpios=10,11,12,13


modprobe
命令传参
:无需指定路径(模块需在
/lib/modules/$(uname -r)
目录),传参方式与
insmod
一致


# 启用调试模式,设置GPIO为15
modprobe led_drv debug=1 led_gpio=15

2. 加载后修改参数

  若参数权限允许(如
0644
),可通过
sysfs
文件系统动态修改参数值


# 查看当前参数值
cat /sys/module/led_drv/parameters/led_gpio

# 修改参数值(需root权限)
echo 18 > /sys/module/led_drv/parameters/led_gpio

# 查看数组参数
cat /sys/module/led_drv/parameters/gpios

注意:动态修改参数后,需驱动内部逻辑支持实时生效(如通过
work_struct
或定时检查参数变化),否则需重新加载模块才能生效。

3. 查看参数信息

  通过
modinfo
命令查看模块参数的描述、类型和默认值:


modinfo led_drv.ko | grep -A 10 'parm:'

# 输出:
parm:           led_gpio:LED GPIO number (range: 0-127, default: 10) (int)
parm:           debug:Enable debug log (0=disable, 1=enable, default: 0) (bool)
parm:           led_name:Name of the LED (default: 'user_led') (charp)
parm:           gpios:Array of GPIO numbers (max 5, e.g., gpios=10,11,12) (int, array)

7.4 常见应用

1. 参数合法性检查

  驱动必须对接收的参数进行合法性验证,避免无效值导致系统异常


static int __init led_drv_init(void) {
    // 检查GPIO号范围(假设系统支持0-127)
    if (led_gpio < 0 || led_gpio > 127) {
        printk(KERN_ERR "Invalid led_gpio: %d (must be 0-127)
", led_gpio);
        return -EINVAL;  // 返回错误,模块加载失败
    }

    // 检查数组参数数量
    if (gpio_count > MAX_GPIO_COUNT) {
        printk(KERN_WARNING "Too many GPIOs (max %d), using first %d
", 
               MAX_GPIO_COUNT, MAX_GPIO_COUNT);
        gpio_count = MAX_GPIO_COUNT;  // 截断数组
    }
    return 0;
}

2. 调试参数的使用

  通过
debug
参数控制日志输出,避免调试信息污染正常日志


// 定义调试日志宏(仅debug=1时生效)
#define DEBUG_LOG(fmt, ...) 
    do { if (debug) printk(KERN_DEBUG "[LED_DRV] " fmt, ##__VA_ARGS__); } while (0)

// 在代码中使用
static void led_turn_on(void) {
    DEBUG_LOG("Turning on LED (GPIO=%d)
", led_gpio);  // 仅debug=1时打印
    gpio_set_value(led_gpio, 1);
}

3. 避免敏感参数暴露

  对于包含敏感信息的参数(如硬件密钥),应设置权限为
0000
,禁止通过
sysfs
查看:


static char *hw_key = "default_key";
module_param(hw_key, charp, 0000);  // 无权限,隐藏参数
MODULE_PARM_DESC(hw_key, "Hardware encryption key (hidden)");

4. 数组参数的边界处理

  数组参数需限制最大长度,避免缓冲区溢出:


static int gpios[MAX_GPIO_COUNT];
static int gpio_count;

module_param_array(gpios, int, &gpio_count, 0644);

static int __init init_gpios(void) {
    // 强制限制数量不超过数组大小
    gpio_count = min(gpio_count, MAX_GPIO_COUNT);
    // ...
}

7.5 常见问题

问题现象
modinfo
看不到参数描述
原因分析:未添加
MODULE_PARM_DESC
或权限未包含
S_IRUGO
解决方法:添加
MODULE_PARM_DESC
权限设为
0444
以上 问题现象:字符串参数传递失败
原因分析:传递含空格的字符串未处理,或内核内存不足解决方法:避免空格,或通过
sysfs
动态修改 问题现象:数组参数仅部分生效
原因分析:传递数量超过数组最大长度,未做截断处理解决方法:在驱动中用
min()
限制数量 问题现象:动态修改参数后不生效
原因分析:驱动未实现参数变化检测逻辑解决方法:增加参数检查(如定时轮询或事件触发) 问题现象:加载时参数类型错误(如给
int
传字符串)
原因分析:用户传递的参数类型与声明不符解决方法:驱动中添加类型检查,输出错误提示

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容