1. Linux 驱动开发环境搭建

在开始编写字符设备驱动之前,必须搭建一套完整的交叉编译与内核模块开发环境。本章节将依次讲解交叉编译工具链的安装、内核源码树的配置以及模块编译 Makefile 的编写规范,为后续的中断处理与 ioctl 接口实现打下坚实基础。

环境概览:本课程基于 Ubuntu 22.04 LTS 主机,目标平台为 ARM Cortex-A7(树莓派 2B+),内核版本 Linux 5.10 LTS。所有操作均在此环境下验证。

1.1 交叉编译工具链安装

交叉编译工具链允许我们在 x86 主机上编译出 ARM 架构可执行的二进制文件。推荐使用 Linaro 提供的预编译工具链,稳定且社区支持广泛。

1.1.1 下载与解压

# 创建工作目录
mkdir -p ~/arm_toolchain && cd ~/arm_toolchain

# 下载 Linaro GCC 7.5.0 ARM 硬浮点工具链
wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 查看工具链内容
ls gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/

1.1.2 环境变量配置

将工具链路径加入 PATH,并设置交叉编译前缀变量,方便后续 Makefile 引用。

# 编辑 ~/.bashrc 或 ~/.zshrc,追加以下内容
export PATH=$PATH:~/arm_toolchain/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm

# 使配置生效
source ~/.bashrc

# 验证安装
arm-linux-gnueabihf-gcc --version
验证要点:执行 arm-linux-gnueabihf-gcc --version 应显示版本号 7.5.0。若提示“未找到命令”,请检查路径是否正确或重新加载 shell 配置。

1.1.3 工具链组件说明

组件名称 功能描述 典型用法
arm-linux-gnueabihf-gcc C 编译器,将源码编译为 ARM 目标文件 $(CC) -c file.c
arm-linux-gnueabihf-ld 链接器,合并目标文件生成可执行文件或模块 $(LD) -r -o module.o a.o b.o
arm-linux-gnueabihf-objcopy 目标文件格式转换(如 ELF → 二进制) objcopy -O binary vmlinux vmlinux.bin
arm-linux-gnueabihf-objdump 反汇编与符号表查看 objdump -D module.ko
arm-linux-gnueabihf-strip 去除调试符号,减小文件体积 strip --strip-debug module.ko

1.2 内核源码树配置

驱动模块的编译必须依赖内核源码树(Kernel Source Tree)提供的头文件、Makefile 规则和编译脚本。我们使用官方 LTS 内核并配置为 ARM 架构。

1.2.1 获取内核源码

# 下载 Linux 5.10 LTS 内核
cd ~
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.210.tar.xz
tar -xvf linux-5.10.210.tar.xz
cd linux-5.10.210

1.2.2 配置内核

使用默认的 ARM 多平台配置(multi_v7_defconfig),该配置支持大多数 ARMv7 开发板。

# 设置架构与交叉编译器
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 加载默认配置
make multi_v7_defconfig

# 可选:使用 menuconfig 进行微调(如开启调试符号)
make menuconfig
常见错误:若 make menuconfig 提示缺少 ncurses 库,请执行 sudo apt install libncurses5-dev。若跳过此步,后续模块编译可能因缺少配置头文件而失败。

1.2.3 编译内核(可选但推荐)

编译一次内核可生成必要的 Module.symvers 和 include/generated/ 下的自动头文件,确保模块编译时符号解析正确。

# 编译内核(使用 4 核加速,根据主机 CPU 调整)
make -j4 zImage

# 编译设备树
make -j4 dtbs

# 编译模块(可选,用于验证)
make -j4 modules
编译产物:arch/arm/boot/zImage 为内核镜像;arch/arm/boot/dts/ 下生成 .dtb 设备树文件。模块编译依赖 Module.symvers,该文件位于源码树根目录。

1.2.4 内核源码树结构速览

Linux 内核源码树关键目录结构 linux-5.10.210/ arch/ drivers/ include/ scripts/ arch/arm/ drivers/char/ include/linux/ scripts/mod/ 关键文件说明 arch/arm/boot/dts/ → 设备树源文件 (.dts/.dtsi) | drivers/char/ → 字符设备驱动目录 模块编译依赖 Module.symvers + include/generated/ + .config

1.3 模块编译 Makefile 编写

内核模块的编译使用内核提供的 kbuild 系统。我们只需编写一个简洁的 Makefile,通过 make -C 指向内核源码树即可完成交叉编译。

1.3.1 基础 Makefile 模板

# Makefile —— 字符设备驱动模块编译
# 目标:编译 mychardev.ko

# 模块名称(不带 .ko 后缀)
obj-m := mychardev.o

# 若模块由多个源文件组成,使用如下格式:
# mychardev-objs := file1.o file2.o

# 内核源码树路径(根据实际位置修改)
KDIR := ~/linux-5.10.210

# 当前目录
PWD := $(shell pwd)

# 默认目标:编译模块
all:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

# 清理目标
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

# 安装模块到临时目录(可选)
install:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=./modules_install

1.3.2 Makefile 关键变量解析

变量/语法 含义 示例值
obj-m 指定要编译为模块的目标文件名(不含 .ko) obj-m := mychardev.o
KDIR 内核源码树绝对路径 ~/linux-5.10.210
M=$(PWD) 告诉 kbuild 在指定目录查找源码并输出模块 M=/home/user/driver
ARCH 目标架构,必须与内核配置一致 arm
CROSS_COMPILE 交叉编译工具链前缀 arm-linux-gnueabihf-
modules kbuild 目标,编译所有 obj-m 指定的模块 —
modules_install 将编译好的 .ko 安装到指定目录 —

1.3.3 编译与验证

# 进入驱动源码目录
cd ~/driver_project

# 编译模块
make

# 预期输出示例:
# make -C ~/linux-5.10.210 M=/home/user/driver_project ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
#   CC [M]  mychardev.o
#   MODPOST mychardev.mod.c
#   CC [M]  mychardev.mod.o
#   LD [M]  mychardev.ko

# 查看生成的模块信息
arm-linux-gnueabihf-objdump -h mychardev.ko | head -20
arm-linux-gnueabihf-modinfo mychardev.ko
常见问题排查:
• 若提示 *** No rule to make target 'modules',请检查 KDIR 路径是否正确,且内核已执行过 make 或至少 make modules_prepare。
• 若提示 *** missing MODULE_LICENSE(),请在驱动源码中添加 MODULE_LICENSE("GPL");。
• 若编译后 .ko 文件大小为 0,通常是因为源码中存在编译错误,请查看终端输出的错误信息。

1.3.4 多文件模块 Makefile 示例

当驱动由多个源文件组成时,使用 <模块名>-objs 语法指定目标文件列表。

# Makefile —— 多文件模块示例
obj-m := mychardev.o
mychardev-objs := main.o ioctl.o interrupt.o

KDIR := ~/linux-5.10.210
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

1.4 环境验证与测试

完成上述步骤后,编写一个最简单的“空模块”来验证整个工具链是否正常工作。

1.4.1 最小驱动源码

// test_dummy.c
#include <linux/module.h>
#include <linux/kernel.h>

static int __init dummy_init(void)
{
    pr_info("Dummy module loaded\n");
    return 0;
}

static void __exit dummy_exit(void)
{
    pr_info("Dummy module unloaded\n");
}

module_init(dummy_init);
module_exit(dummy_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
MODULE_DESCRIPTION("A minimal dummy module for environment verification");

1.4.2 编译与检查

# 编译
make

# 检查模块架构
file test_dummy.ko
# 输出应包含:ELF 32-bit LSB relocatable, ARM, version 1 (ARM)

# 查看模块符号
arm-linux-gnueabihf-nm test_dummy.ko | grep -E "init|exit"
验证通过标志:file 命令显示 ARM 架构,nm 能正确列出 init_module 和 cleanup_module 符号,且无编译错误。至此,交叉编译环境、内核源码树和 Makefile 全部就绪,可以进入下一章的字符设备驱动框架编写。

1.5 本章小结

  • 交叉编译工具链:使用 Linaro GCC 7.5 ARM 硬浮点版本,配置 PATH、CROSS_COMPILE 和 ARCH 环境变量。
  • 内核源码树:下载 Linux 5.10 LTS,执行 multi_v7_defconfig 并至少编译一次内核以生成依赖文件。
  • Makefile 编写:利用 kbuild 系统,通过 obj-m 指定模块,KDIR 指向内核源码树,ARCH 和 CROSS_COMPILE 确保交叉编译。
  • 验证方法:编译最小驱动并用 file、nm 确认架构与符号正确。