1. Linux 设备驱动概述

设备驱动是操作系统与硬件设备之间的“翻译官”。在 Linux 系统中,驱动以内核模块的形式存在,负责管理设备的初始化、数据传输、中断处理以及电源管理等核心任务。本章将建立驱动开发的基本认知框架,包括驱动的作用、内核空间与用户空间的划分,以及交叉编译环境的搭建方法。

1.1 驱动的作用与分类

Linux 设备驱动的主要职责可以概括为以下四点:

  • 设备初始化与配置:在系统启动或模块加载时,完成寄存器设置、中断注册、内存映射等硬件初始化工作。
  • 数据收发:实现 read/write/ioctl 等文件操作接口,在用户态与硬件之间搬运数据。
  • 中断处理:响应硬件中断,在中断上下文中完成紧急处理,并将耗时任务推送到下半部(bottom half)。
  • 电源管理与热插拔:配合内核 PM 框架实现设备的休眠/唤醒,以及设备的热移除/热插入。

根据设备类型和通信方式,Linux 驱动通常分为三大类:

驱动类型 典型设备 核心框架 用户接口
字符设备 串口、GPIO、I2C 传感器 cdev / file_operations /dev/xxx 节点
块设备 eMMC、SD 卡、NVMe SSD block_device / gendisk /dev/sda 等
网络设备 以太网 PHY、WiFi 模块 net_device / sk_buff socket 接口
课程聚焦:本课程以字符设备为主线,结合 platform 框架讲解驱动与设备树的匹配机制。字符设备结构简单、调试方便,是入门 Linux 驱动开发的最佳起点。

1.2 内核空间与用户空间

Linux 系统将虚拟内存划分为两个特权级区域:用户空间(User Space)和内核空间(Kernel Space)。驱动代码运行在内核空间,拥有最高权限;应用程序运行在用户空间,受 MMU 保护,无法直接访问硬件。

内核空间与用户空间分层架构 内核空间与用户空间分层架构 用户空间 (User Space) 应用程序 (App) — 通过系统调用访问驱动 C 库 (glibc / musl) — 封装 open/read/write/ioctl 虚拟地址空间 — 受 MMU 保护,不可直接访问硬件 系统调用接口 (syscall) — copy_from_user / copy_to_user 内核空间 (Kernel Space) VFS / 设备驱动框架 — 字符设备 / 块设备 / 网络协议栈 驱动模块 — 硬件初始化、中断处理、数据收发 硬件抽象层 — GPIO / I2C / SPI / DMA 控制器 物理地址空间 — 直接访问寄存器与内存 硬件设备 (Hardware) — 寄存器 / 中断控制器 / 外设总线
关键区别:用户空间通过系统调用陷入内核,驱动在内核空间完成硬件操作后,将结果拷贝回用户缓冲区。这一过程涉及地址空间切换和数据拷贝,是驱动性能优化的核心关注点。

1.3 驱动开发环境搭建

嵌入式 Linux 驱动开发通常采用交叉编译模式:在性能强大的宿主机(x86 PC)上编写和编译代码,生成目标架构(ARM / RISC-V / MIPS)的可执行文件,再下载到目标板运行。

1.3.1 交叉编译工具链

工具链包含编译器、链接器、库和调试器。以 ARM Cortex-A7 为例,常用工具链如下:

组件 示例名称 说明
C 编译器 arm-linux-gnueabihf-gcc 生成 ARM 硬浮点指令
链接器 arm-linux-gnueabihf-ld 链接目标文件与库
反汇编器 arm-linux-gnueabihf-objdump 查看二进制指令与符号表
调试器 arm-linux-gnueabihf-gdb 远程调试目标板内核
避坑提示:工具链的 glibc 版本必须与目标板根文件系统匹配,否则编译出的驱动模块可能因符号版本不兼容而加载失败。建议使用板卡厂商提供的 SDK 工具链,或从 Linaro / Bootlin 下载预编译版本。

1.3.2 内核源码树

驱动模块的编译依赖内核源码树(Kernel Source Tree),包括头文件、Kconfig 和 Makefile 结构。搭建步骤:

  1. 下载内核源码:从 kernel.org 或板卡厂商仓库获取,版本需与目标板运行的内核一致。
  2. 配置内核:使用 make ARCH=arm menuconfig 加载默认配置或厂商 defconfig。
  3. 编译内核:执行 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 生成 zImage 和 dtb。
  4. 编译模块:进入驱动源码目录,使用内核树提供的 Makefile 编译外部模块。

典型的外部模块 Makefile 模板:

# Makefile — 外部驱动模块
obj-m := hello_driver.o

KERNEL_DIR ?= /home/user/linux-5.10
CROSS_COMPILE ?= arm-linux-gnueabihf-

all:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean
交叉编译流程 交叉编译流程 宿主机 (x86) 编写驱动源码 交叉编译工具链 内核源码树 头文件 / Kconfig Makefile 框架 目标板 (ARM) 加载 .ko 模块 运行测试 编译产物:hello_driver.ko(内核模块) 通过 scp / nfs / tftp 传输到目标板

1.3.3 环境验证

搭建完成后,执行以下命令验证环境是否就绪:

# 检查工具链
arm-linux-gnueabihf-gcc --version

# 检查内核树是否已编译
ls /home/user/linux-5.10/include/linux/device.h

# 编译一个空模块测试
make -C /home/user/linux-5.10 M=$(pwd) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
推荐实践:在虚拟机或 Docker 容器中搭建统一的开发环境,避免因宿主机库版本冲突导致编译失败。使用 export ARCH=arm 和 export CROSS_COMPILE=arm-linux-gnueabihf- 可减少每次编译时的参数输入。

1.4 本章小结

  • 驱动是内核与硬件之间的桥梁,字符设备驱动是入门首选。
  • 用户空间与内核空间通过系统调用隔离,驱动运行在内核态,拥有硬件访问权限。
  • 交叉编译环境包括工具链和内核源码树,两者版本必须与目标板严格对齐。
  • 使用外部模块 Makefile 模板可以快速编译独立驱动,无需每次重新编译整个内核。