1. 驱动开发概述
Linux驱动开发是嵌入式系统工程师的核心技能。本章从宏观视角梳理驱动分类、内核模块编程模型,并手把手搭建交叉编译环境与内核源码树,为后续GPIO与I2C实战打下坚实基础。
1.1 Linux驱动分类
Linux内核将设备驱动划分为三大类,每一类对应不同的硬件抽象层次和编程接口。理解分类是选择驱动框架的第一步。
| 驱动类型 | 典型设备 | 注册接口 | 文件操作接口 | 应用层访问 |
|---|---|---|---|---|
| 字符设备 | GPIO、UART、LED、按键 | register_chrdev() / cdev_add() | file_operations | /dev/xxx,open/read/write/ioctl |
| 块设备 | eMMC、NAND Flash、SD卡 | register_blkdev() | block_device_operations | 挂载文件系统,mount /dev/mmcblk0 |
| 网络设备 | 以太网、Wi-Fi、CAN | register_netdev() | net_device_ops | Socket接口,ifconfig eth0 up |
岗位提示:嵌入式驱动开发岗位中,字符设备驱动占比超过70%。GPIO和I2C均属于字符设备范畴,掌握字符设备框架是入门关键。
1.2 内核模块基础
Linux驱动以内核模块(Kernel Module)的形式存在,支持动态加载和卸载,无需重新编译整个内核。模块编程是驱动开发的最小单元。
1.2.1 模块三要素
- 模块许可证:
MODULE_LICENSE("GPL"),避免污染内核符号。 - 模块作者/描述:
MODULE_AUTHOR()、MODULE_DESCRIPTION()。 - 入口与出口:
module_init()指定初始化函数,module_exit()指定清理函数。
1.2.2 最小模块代码示例
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, GPIO Driver World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, GPIO 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");
1.2.3 模块编译与操作命令
| 操作 | 命令 | 说明 |
|---|---|---|
| 编译模块 | make -C /lib/modules/$(uname -r)/build M=$(pwd) modules | 使用内核源码树编译 |
| 加载模块 | insmod hello.ko | 调用 module_init |
| 查看模块 | lsmod | grep hello | 确认模块已加载 |
| 卸载模块 | rmmod hello | 调用 module_exit |
| 查看日志 | dmesg | tail | 查看 printk 输出 |
避坑指南:嵌入式目标板与开发主机架构不同,必须使用交叉编译链编译模块。直接在x86主机上编译的.ko文件无法在ARM目标板上加载,会报
invalid module format 错误。
1.3 驱动开发环境搭建
嵌入式驱动开发采用“宿主机编译 + 目标板运行”的交叉开发模式。环境搭建包含三大核心组件:交叉编译链、内核源码树、根文件系统。
1.3.1 交叉编译链
交叉编译链运行在x86_64宿主机上,生成ARM(或其他架构)的可执行代码。以ARM Cortex-A7为例,常用工具链为 arm-linux-gnueabihf-。
| 工具 | 用途 | 示例 |
|---|---|---|
arm-linux-gnueabihf-gcc | C编译器 | 编译应用与内核模块 |
arm-linux-gnueabihf-ld | 链接器 | 链接目标文件 |
arm-linux-gnueabihf-objdump | 反汇编 | 调试驱动二进制 |
arm-linux-gnueabihf-strip | 符号剥离 | 减小模块体积 |
1.3.2 内核源码树
编译内核模块必须依赖完整的内核源码树,包含头文件、Kconfig、Makefile和编译好的 Module.symvers。源码树路径通常通过 KERNELDIR 变量传递给模块Makefile。
# 模块Makefile示例
obj-m := gpio_driver.o
KERNELDIR ?= /home/embedded/linux-5.10.100
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) ARCH=arm clean
关键路径:内核源码树必须与目标板运行的内核版本完全一致,否则模块加载时会因版本魔术字(vermagic)不匹配而失败。建议从板卡厂商获取已配置好的内核源码树。
1.4 驱动开发全流程SVG架构图
下图展示了从宿主机编写代码到目标板加载驱动的完整链路,包含交叉编译、模块部署、动态加载与用户态交互四个阶段。
1.5 驱动分类与内核模块关系图
下图展示三大驱动类型在内核中的位置,以及内核模块与VFS、硬件层之间的调用关系。
1.6 环境搭建检查清单
在开始GPIO/I2C驱动实战之前,请逐项确认以下环境就绪状态:
- 交叉编译链:执行
arm-linux-gnueabihf-gcc --version确认输出正常。 - 内核源码树:确认
KERNELDIR路径下存在include/linux、Makefile、Module.symvers。 - 目标板内核版本:目标板执行
uname -r,与源码树版本一致。 - 模块编译测试:编译最小模块
hello.ko,传输至目标板并成功insmod。 - 设备节点访问:确认目标板
/dev目录可写,或使用mknod手动创建设备节点权限。
实战起点:完成上述环境搭建后,即可进入第2章——GPIO驱动框架与寄存器操作实战。建议将本章最小模块代码作为“驱动开发Hello World”保留,后续所有驱动均以此模板扩展。