1. 驱动开发概述:Linux驱动分类、内核模块基础、驱动开发环境搭建

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、CANregister_netdev()net_device_opsSocket接口,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-gccC编译器编译应用与内核模块
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架构图

下图展示了从宿主机编写代码到目标板加载驱动的完整链路,包含交叉编译、模块部署、动态加载与用户态交互四个阶段。

驱动开发全流程:宿主机 → 目标板 ① 宿主机开发环境 • 编写 .c / .h 驱动源码 • 配置 KERNELDIR 路径 • 执行 make 交叉编译 • 生成 .ko 内核模块 • 工具链: arm-linux-gnueabihf- ② 模块部署 • scp / nfs / tftp 传输 • 复制到目标板 /lib/modules • 或直接放入 /tmp 测试 • 确保文件权限可读 ③ 目标板加载 • insmod gpio_driver.ko • 查看 dmesg 确认 init • lsmod 验证模块列表 • 生成 /dev/gpio_xxx 节点 ④ 用户态交互 • open /dev/gpio_xxx • ioctl 配置方向/值 • read/write 操作GPIO • close 释放设备 环境依赖清单 ✅ 交叉编译链:arm-linux-gnueabihf- ✅ 内核源码树:linux-5.10.100 ✅ 目标板内核版本与源码树一致

1.5 驱动分类与内核模块关系图

下图展示三大驱动类型在内核中的位置,以及内核模块与VFS、硬件层之间的调用关系。

Linux驱动分类与内核模块架构 用户空间应用程序(open/read/write/ioctl/socket) 虚拟文件系统(VFS) / Socket层 字符设备驱动 file_operations cdev / register_chrdev GPIO / I2C / UART / SPI 块设备驱动 block_device_operations request_queue / bio eMMC / NAND / SD 网络设备驱动 net_device_ops sk_buff / NAPI Ethernet / Wi-Fi / CAN 内核模块(.ko):module_init / module_exit / MODULE_LICENSE 硬件抽象层(寄存器 / MMIO / 中断控制器 / DMA)

1.6 环境搭建检查清单

在开始GPIO/I2C驱动实战之前,请逐项确认以下环境就绪状态:

  1. 交叉编译链:执行 arm-linux-gnueabihf-gcc --version 确认输出正常。
  2. 内核源码树:确认 KERNELDIR 路径下存在 include/linux、Makefile、Module.symvers。
  3. 目标板内核版本:目标板执行 uname -r,与源码树版本一致。
  4. 模块编译测试:编译最小模块 hello.ko,传输至目标板并成功 insmod。
  5. 设备节点访问:确认目标板 /dev 目录可写,或使用 mknod 手动创建设备节点权限。
实战起点:完成上述环境搭建后,即可进入第2章——GPIO驱动框架与寄存器操作实战。建议将本章最小模块代码作为“驱动开发Hello World”保留,后续所有驱动均以此模板扩展。