1. Linux 驱动开发环境搭建:交叉编译工具链安装、内核源码树配置、模块编译与加载基础

嵌入式 Linux 驱动开发与普通 PC 开发最大的区别在于交叉编译。目标板(ARM、RISC-V 等)资源有限,无法运行完整的编译工具链,因此需要在性能强劲的宿主机(x86_64)上编译出目标架构的二进制模块,再部署到目标板运行。本章将完整搭建这一基础环境。

1.1 交叉编译工具链安装

交叉编译工具链(Cross-Compilation Toolchain)包含针对目标架构的编译器、链接器、库和头文件。常见获取方式有两种:

方式 适用场景 典型命令/包名
发行版预装 快速原型、通用 ARM 平台 gcc-arm-linux-gnueabihf
芯片厂商 SDK 特定 SoC(如 i.MX、STM32MP1) Yocto / Buildroot 输出工具链
Linaro 预编译包 ARMv7-A / ARMv8-A 通用开发 gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf

以 Ubuntu 宿主机 + ARM Cortex-A7 目标板为例,安装 Linaro 工具链:

# 下载并解压到 /opt
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
sudo tar -xf gcc-linaro-*.tar.xz -C /opt

# 添加至 PATH
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# 验证安装
arm-linux-gnueabihf-gcc --version
提示:工具链版本必须与目标板内核版本匹配。使用过高版本的 GCC 编译旧内核模块可能因内联函数 ABI 变化导致加载失败。

1.2 内核源码树配置

驱动模块必须依赖完整的内核源码树(Kernel Source Tree)进行编译。源码树需要预先配置,生成 .config 和 Module.symvers 等关键文件。

配置流程分为三步:

  1. 获取内核源码:从 kernel.org 或芯片厂商 BSP 获取,版本必须与目标板运行内核一致。
  2. 生成默认配置:使用目标板厂商提供的 defconfig,或从目标板 /proc/config.gz 导出。
  3. 编译内核与模块:先编译完整内核,再编译模块,确保 Module.symvers 生成。
# 以 i.MX6ULL EVK 为例
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
make imx_v7_defconfig
make -j4 zImage modules
make modules_install INSTALL_MOD_PATH=./modules_install
注意:如果只编译模块而不先编译内核,Module.symvers 文件缺失,将导致模块中引用的内核符号(如 input_register_device)无法解析,insmod 时报 Unknown symbol 错误。

1.3 模块编译与加载基础

驱动模块的编译依赖内核源码树提供的 Kbuild 系统。开发者只需编写 Makefile,指定模块名和源文件,Kbuild 会自动处理交叉编译参数。

一个最简单的驱动模块 Makefile 模板:

# Makefile — 外部模块编译
obj-m := hello_drv.o
hello_drv-objs := hello.o

KERNEL_DIR ?= /home/user/linux-imx
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 clean

编译后生成 hello_drv.ko,部署到目标板后使用以下命令管理:

操作 命令 说明
加载模块 insmod hello_drv.ko 直接插入,不处理依赖
卸载模块 rmmod hello_drv 按名称卸载
查看已加载模块 lsmod 显示模块列表及引用计数
查看模块信息 modinfo hello_drv.ko 显示作者、描述、依赖、参数
自动加载依赖 modprobe hello_drv 需先执行 depmod
关键概念:模块加载时内核会调用 module_init() 注册的回调函数;卸载时调用 module_exit()。触摸屏驱动正是在 init 中申请 I2C/SPI 设备、注册 input 设备。

1.4 环境搭建全流程 SVG 图

下图展示了从宿主机到目标板的完整环境搭建链路,以及模块编译、部署、加载的流程关系:

嵌入式 Linux 驱动开发环境搭建流程 宿主机 (x86_64 Ubuntu) 交叉编译工具链 arm-linux-gnueabihf-gcc 内核源码树 .config + Module.symvers make -C $(KERNEL_DIR) M=$(PWD) modules 生成 hello_drv.ko scp / nfs 部署 目标板 (ARM Cortex-A7) insmod hello_drv.ko module_init() 执行 lsmod / rmmod 调试与卸载 dmesg 查看内核日志 printk 输出定位问题 关键检查点 1. 工具链架构匹配 arm-linux-gnueabihf-gcc -dumpmachine 2. 内核版本一致性 目标板 uname -r vs 源码版本 3. Module.symvers 存在 否则 Unknown symbol 错误 4. 模块依赖处理 modprobe 前先 depmod 5. 内核配置选项 CONFIG_MODULES=y 必须开启 6. 文件系统权限 /lib/modules/ 目录可写 7. 调试手段 dmesg -w 实时跟踪 cat /proc/kmsg 备用 8. 模块签名 CONFIG_MODULE_SIG 需关闭 或使用目标板签名密钥

1.5 常见问题与避坑

  • 模块加载报错 Invalid module format:内核版本或 vermagic 不一致。用 modinfo 对比模块与内核的 vermagic 字符串。
  • Unknown symbol 错误:模块引用了未导出的内核符号,或 Module.symvers 未正确生成。重新编译内核并确认 EXPORT_SYMBOL。
  • insmod 后无任何输出:检查 printk 日志级别,使用 dmesg -n 8 提升控制台显示级别。
  • 模块卸载时系统崩溃:exit 函数中释放了仍在使用的资源,或未正确注销 input 设备。
开发效率建议:在宿主机搭建 NFS 根文件系统,目标板通过网络挂载,编译后的 .ko 文件直接同步到目标板,无需反复烧写镜像,大幅缩短调试周期。