1. Linux内核驱动概述:驱动的作用、内核模块基础、驱动开发环境搭建
1.1 驱动的作用:连接硬件与操作系统的桥梁
在嵌入式Linux系统中,设备驱动是操作系统内核与硬件设备之间的中间层。它负责将上层应用的通用I/O请求(如读、写、控制)转换为硬件设备能够理解的特定操作序列。没有驱动,操作系统无法识别或控制任何外部硬件。
核心职责: 驱动封装了硬件寄存器的操作细节,向上提供统一的文件操作接口(file_operations),使应用程序可以像操作普通文件一样操作硬件设备。
1.2 内核模块基础:动态加载与卸载
Linux内核支持可加载内核模块(Loadable Kernel Module, LKM)机制。模块是可以在运行时动态插入内核或从内核移除的代码片段,无需重新编译整个内核或重启系统。绝大多数设备驱动都以模块形式发布。
1.2.1 模块的生命周期
| 阶段 | 命令 | 内核行为 |
|---|---|---|
| 加载 | insmod / modprobe |
调用模块的 module_init() 函数,注册驱动到内核 |
| 运行 | — | 驱动响应来自VFS的系统调用,操作硬件 |
| 卸载 | rmmod |
调用模块的 module_exit() 函数,释放资源并注销驱动 |
1.2.2 最小内核模块代码示例
// hello_drv.c — 最小内核模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, driver world!\n");
return 0; // 0 表示加载成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, driver world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("A minimal kernel module example");
关键点:
__init 和 __exit 宏将函数放入特殊内存段,加载/卸载完成后内核会释放这些内存以节省空间。MODULE_LICENSE 必须声明,否则内核会标记模块为“污染”。
1.3 驱动开发环境搭建
嵌入式驱动开发通常采用交叉编译模式:在性能强大的宿主机(x86 PC)上编写和编译代码,生成的目标文件在目标板(ARM/RISC-V/MIPS)上运行。搭建环境需要准备以下三个核心组件。
1.3.1 交叉编译工具链
工具链包含交叉编译器(如 arm-linux-gnueabihf-gcc)、链接器、汇编器以及目标板所需的C库(glibc或uClibc)。
| 组件 | 典型名称 | 作用 |
|---|---|---|
| C编译器 | arm-linux-gnueabihf-gcc |
将C源码编译为目标板机器码 |
| 汇编器 | arm-linux-gnueabihf-as |
将汇编代码转为目标文件 |
| 链接器 | arm-linux-gnueabihf-ld |
链接多个目标文件与库 |
| 二进制工具 | objdump / readelf |
分析目标文件的段、符号表 |
1.3.2 内核源码树
编译内核模块必须依赖目标板对应的内核源码树(Kernel Source Tree)。源码树需要预先配置(make menuconfig)并至少编译一次,以生成 Module.symvers 和 include/generated/ 等头文件。
# 典型的内核源码树目录结构(精简)
linux-6.1.y/
├── arch/ # 体系结构相关代码 (arm64, riscv, x86 ...)
├── drivers/ # 设备驱动存放目录 (char, net, i2c, spi ...)
├── include/ # 内核头文件 (linux/, asm-generic/, uapi/ ...)
├── scripts/ # 编译脚本 (module.lds, Makefile.build ...)
├── Makefile # 顶层 Makefile
├── Module.symvers # 符号版本信息(编译模块时自动引用)
└── .config # 内核配置选项
1.3.3 模块编译的 Makefile 模板
# Makefile — 用于编译 hello_drv.ko
obj-m := hello_drv.o
# KDIR 指向目标板的内核源码树路径
KDIR := /home/user/linux-6.1.y
# CROSS_COMPILE 指定交叉编译工具链前缀
CROSS_COMPILE := arm-linux-gnueabihf-
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm clean
常见陷阱: 宿主机内核版本与目标板内核版本必须一致或兼容,否则加载模块时会报
Invalid module format 或 version magic 不匹配错误。始终使用目标板运行的内核源码树进行编译。
1.4 驱动开发流程总览
1.5 本章小结
- 驱动的作用:封装硬件操作,向上提供标准文件接口,是内核与硬件的中间层。
- 内核模块:支持动态加载/卸载,使用
module_init和module_exit管理生命周期。 - 交叉编译工具链:包含编译器、链接器、二进制工具,前缀标识目标架构(如
arm-linux-gnueabihf-)。 - 内核源码树:编译模块必须依赖目标板对应的源码树,版本必须严格匹配。
- 开发流程:环境搭建 → 编写 → 编译 → 部署 → 调试 → 合入,形成闭环迭代。
下一阶段准备: 在第二章中,我们将深入设备ID匹配机制,学习如何让驱动正确识别硬件设备,并处理多设备兼容性问题。