2. 驱动开发环境搭建
嵌入式驱动开发与普通PC端开发的核心区别在于交叉编译。目标设备(ARM、RISC-V等架构)资源有限,无法直接运行本地编译工具链,因此必须在性能强大的宿主机(x86_64)上安装交叉编译工具链,为目标架构生成可执行代码。本章将系统讲解从工具链安装、内核源码树配置到模块编译Makefile编写的完整流程。
2.1 交叉编译工具链安装
交叉编译工具链是一套完整的开发工具集合,包含编译器、汇编器、链接器、库文件及调试器。安装过程分为下载、解压、配置环境变量三步。
2.1.1 下载与解压
从ARM官方或Linaro社区获取预编译工具链。推荐使用ARM官方提供的gcc-arm-10.3-2021.07版本。
# 创建工作目录
mkdir -p ~/tools
cd ~/tools
# 下载工具链(约200MB)
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.07/gcc-arm-none-eabi-10.3-2021.07-x86_64-linux.tar.bz2
# 解压到当前目录
tar -xjf gcc-arm-none-eabi-10.3-2021.07-x86_64-linux.tar.bz2
# 解压后目录结构
ls gcc-arm-none-eabi-10.3-2021.07/
# bin lib libexec share arm-none-eabi
2.1.2 配置环境变量
将工具链的bin目录添加到PATH环境变量中,并设置交叉编译前缀。
# 编辑 ~/.bashrc 或 ~/.zshrc
echo 'export PATH=$PATH:~/tools/gcc-arm-none-eabi-10.3-2021.07/bin' >> ~/.bashrc
echo 'export CROSS_COMPILE=arm-none-linux-gnueabihf-' >> ~/.bashrc
echo 'export ARCH=arm' >> ~/.bashrc
# 使配置生效
source ~/.bashrc
# 验证安装
arm-none-linux-gnueabihf-gcc --version
# 输出示例:arm-none-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture) 10.3.1 20210621
CROSS_COMPILE变量定义了交叉编译工具的前缀,Makefile通过该变量自动选择正确的编译器。例如$(CROSS_COMPILE)gcc会被展开为arm-none-linux-gnueabihf-gcc。
2.1.3 工具链组件清单
| 组件 | 可执行文件 | 功能说明 |
|---|---|---|
| 编译器 | arm-none-linux-gnueabihf-gcc |
C语言编译器,将源码编译为目标架构的汇编/机器码 |
| 汇编器 | arm-none-linux-gnueabihf-as |
将汇编代码转换为目标文件(.o) |
| 链接器 | arm-none-linux-gnueabihf-ld |
将多个目标文件和库链接为最终可执行文件或内核模块 |
| 目标文件工具 | arm-none-linux-gnueabihf-objcopy |
转换目标文件格式(如ELF转二进制) |
| 反汇编器 | arm-none-linux-gnueabihf-objdump |
反汇编目标文件,用于调试和分析 |
| 库文件 | libc.so.6等 |
ARM架构的C运行时库,位于arm-none-linux-gnueabihf/libc/ |
2.2 Linux内核源码树配置
驱动模块的编译必须依赖内核源码树(Kernel Source Tree)。内核源码树提供了模块编译所需的头文件、Makefile规则和Kconfig配置系统。配置过程包括获取源码、配置内核选项、编译内核(可选)和生成模块符号。
2.2.1 获取内核源码
从内核官网或芯片厂商(如ST、NXP)获取适配目标板的内核源码。本课程使用Linux 5.10.61版本。
# 下载内核源码
cd ~/workspace
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.61.tar.xz
tar -xJf linux-5.10.61.tar.xz
cd linux-5.10.61
2.2.2 配置内核选项
内核配置通过make menuconfig或直接使用厂商提供的默认配置文件(defconfig)完成。
# 使用ARM架构的默认配置
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- defconfig
# 或者使用厂商提供的配置文件(以STM32MP1为例)
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- stm32mp1_defconfig
# 手动调整配置(可选)
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- menuconfig
ARCH和CROSS_COMPILE,make menuconfig会使用x86_64的默认配置,导致后续模块编译时头文件不匹配。务必在每次内核操作前确认这两个变量已正确设置。
2.2.3 编译内核与模块
编译内核映像和所有模块,生成vmlinux、zImage以及modules.order等文件。
# 编译内核(使用4个并行任务)
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -j4 zImage
# 编译设备树
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- dtbs
# 编译所有模块
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- modules
# 安装模块到临时目录(用于后续打包)
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- INSTALL_MOD_PATH=./modules_install modules_install
2.2.4 内核源码树结构
以下SVG图展示了内核源码树中与驱动开发密切相关的核心目录结构。
2.3 模块编译Makefile编写
内核模块的编译不依赖传统的gcc命令行,而是通过内核构建系统(Kbuild)完成。开发者只需编写一个简洁的Makefile,声明模块名称、源文件列表和依赖的内核源码树路径。
2.3.1 基础Makefile模板
以下是一个字符设备驱动模块的标准Makefile,适用于本课程后续章节的驱动代码。
# Makefile - 字符设备驱动模块编译
# 目标:编译 mychardev.ko 内核模块
# 内核源码树路径(根据实际环境修改)
KERNEL_DIR ?= /home/user/workspace/linux-5.10.61
# 当前模块源码路径
PWD := $(shell pwd)
# 模块名称和目标文件
obj-m := mychardev.o
# 如果模块由多个源文件组成,使用如下语法:
# mychardev-objs := file1.o file2.o
all:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- clean
# 安装模块到临时目录
install:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- INSTALL_MOD_PATH=$(PWD)/modules_install modules_install
.PHONY: all clean install
2.3.2 Makefile关键变量解析
| 变量/语法 | 含义 | 示例值 |
|---|---|---|
KERNEL_DIR |
已配置的内核源码树绝对路径 | /home/user/workspace/linux-5.10.61 |
M=$(PWD) |
告诉Kbuild当前模块源码所在目录 | M=/home/user/driver/mychardev |
obj-m |
声明需要编译为模块的目标文件(不含.ko后缀) | mychardev.o |
ARCH |
目标架构,必须与内核配置一致 | arm |
CROSS_COMPILE |
交叉编译工具链前缀 | arm-none-linux-gnueabihf- |
-C |
切换到内核源码目录执行Make | -C $(KERNEL_DIR) |
2.3.3 编译与验证
在模块源码目录执行make命令,Kbuild系统会自动完成预处理、编译、链接,最终生成.ko文件。
# 进入模块源码目录
cd ~/driver/mychardev
# 执行编译
make
# 编译输出示例
make -C /home/user/workspace/linux-5.10.61 M=/home/user/driver/mychardev ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- modules
CC [M] /home/user/driver/mychardev/mychardev.o
MODPOST /home/user/driver/mychardev/Module.symvers
LD [M] /home/user/driver/mychardev/mychardev.ko
# 验证生成的模块
file mychardev.ko
# 输出:mychardev.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=..., not stripped
# 查看模块信息
modinfo mychardev.ko
# filename: mychardev.ko
# license: GPL
# description: A simple character device driver
# depends:
# vermagic: 5.10.61 SMP preempt mod_unload modversions ARMv7
make时,Kbuild系统依次执行:1.
scripts/Makefile.build 解析obj-m变量2. 调用交叉编译器编译
.c文件生成.o目标文件3. 调用
scripts/mod/modpost进行模块符号检查4. 调用链接器将
.o文件链接为.ko内核模块
2.4 常见问题与避坑指南
- 内核版本不匹配: 模块的vermagic必须与目标板运行的内核版本完全一致,否则
insmod会报“Invalid module format”错误。使用modinfo检查vermagic字段。 - ARCH变量遗漏: 如果Makefile中未传递
ARCH=arm,Kbuild会使用x86_64的头文件,导致编译时找不到asm/types.h等架构相关头文件。 - 内核源码未配置: 如果内核源码树未执行
make defconfig或make menuconfig,include/generated/autoconf.h文件缺失,编译会立即失败。 - 权限问题: 在目标板上加载模块需要root权限,使用
sudo insmod或su -c 'insmod'。
make menuconfig)后,必须重新编译内核和模块,否则模块可能因依赖的配置选项变化而无法加载。建议在开发阶段固定内核配置,避免频繁重编。
2.5 本章小结
本章完成了嵌入式驱动开发环境的三大核心搭建步骤:
- 交叉编译工具链: 下载、解压、配置
PATH和CROSS_COMPILE变量,验证gcc --version。 - 内核源码树: 获取源码、执行
make defconfig、编译内核和模块,理解drivers/、include/、arch/等关键目录的作用。 - 模块Makefile: 掌握
obj-m、KERNEL_DIR、M=$(PWD)等核心语法,能够独立编译出.ko文件并通过modinfo验证。