2. Linux驱动开发环境搭建
嵌入式 Linux 驱动开发与普通桌面开发最大的区别在于交叉编译。目标板(ARM、RISC-V 等)资源有限,无法运行完整的编译工具链,因此需要在性能强大的宿主机(x86_64)上编译出目标板可执行的二进制模块。本章将系统讲解交叉编译工具链的安装、内核源码树的配置以及模块 Makefile 的编写规范。
学习目标:完成本章后,你应能独立搭建一套可用的嵌入式驱动开发环境,并成功编译第一个“Hello World”内核模块。
2.1 交叉编译工具链
交叉编译工具链由 binutils(汇编器、链接器)、gcc(编译器)、glibc/uclibc(C 库)以及 linux kernel headers 组成。工具链的命名规则通常为 arch-vendor-os-abi,例如 arm-linux-gnueabihf-。
2.1.1 工具链获取方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 芯片厂商 SDK | NXP Yocto、TI Processor SDK、ST SDK 等 | 量产项目,与 BSP 版本严格绑定 |
| Linaro 预编译工具链 | gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf |
快速原型验证,社区广泛使用 |
| Buildroot / Yocto 自编译 | 从源码构建完整工具链 | 深度定制,需要控制 C 库版本 |
| 发行版包管理器 | apt install gcc-arm-linux-gnueabihf |
简单测试,版本可能较旧 |
2.1.2 安装与验证
以 Linaro arm-linux-gnueabihf 为例,下载后解压并添加至 PATH:
# 解压
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/
# 添加环境变量(写入 ~/.bashrc)
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 验证
arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-objdump --version
关键环境变量:
ARCH 指定目标架构,CROSS_COMPILE 指定交叉编译工具链前缀。内核构建系统通过这两个变量自动选择正确的编译器。
2.2 内核源码树配置
驱动模块本质上是内核的一部分,编译时需要引用内核源码树中的头文件和 Makefile 规则。因此,必须准备一份与目标板运行内核版本一致的、已配置好的内核源码树。
2.2.1 获取内核源码
- 从芯片厂商获取:最可靠,包含所有板级补丁和驱动。
- 从 kernel.org 主线获取:适用于通用平台学习。
- 从开发板社区获取:如 Raspberry Pi Linux、BeagleBoard Linux。
2.2.2 配置与编译内核
以 ARM 架构为例,配置内核的典型步骤:
# 清理
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
# 使用默认配置(以 vexpress 开发板为例)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig
# 菜单配置(可选,用于裁剪或添加驱动)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 编译内核与设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage dtbs -j4
注意:如果只编译外部模块,不需要完整编译内核,但必须执行
make modules_prepare 来生成 Module.symvers 和必要的目标文件。
2.2.3 内核源码树目录结构
| 目录 | 作用 |
|---|---|
arch/ |
架构相关代码,如 arch/arm、arch/arm64 |
drivers/ |
设备驱动子目录,如 char、net、i2c、spi |
include/ |
内核头文件,包括 uapi(用户空间 API) |
scripts/ |
构建脚本,如 modpost、kconfig 工具 |
Module.symvers |
导出的符号版本信息,外部模块依赖此文件 |
2.3 模块编译基础(Makefile)
Linux 内核模块的编译依赖内核构建系统(Kbuild)。开发者只需编写一个简洁的 Makefile,内核构建系统会自动处理交叉编译、依赖跟踪和模块链接。
2.3.1 最简单的模块 Makefile
# KDIR:指向已配置的内核源码树路径
KDIR ?= /home/user/linux-5.10
# 当前目录
PWD := $(shell pwd)
# 目标模块名称(不带 .ko 后缀)
obj-m := hello.o
# 如果模块由多个源文件组成,使用模块名-y
# hello-y := file1.o file2.o
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
2.3.2 带交叉编译的 Makefile
当需要交叉编译时,在 make 命令行传入 ARCH 和 CROSS_COMPILE:
# 编译命令
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
也可以在 Makefile 中直接指定:
ARCH ?= arm
CROSS_COMPILE ?= arm-linux-gnueabihf-
KDIR ?= /home/user/linux-5.10
obj-m := hello.o
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
2.3.3 Makefile 关键变量说明
| 变量 | 含义 | 示例 |
|---|---|---|
obj-m |
需要编译成模块的目标文件列表 | obj-m := mydrv.o |
obj-y |
需要编译进内核的目标文件列表 | obj-y := mydrv.o |
ccflags-y |
附加的编译标志 | ccflags-y := -DDEBUG -I$(src)/include |
ldflags-y |
附加的链接标志 | ldflags-y := --strip-debug |
KBUILD_EXTRA_SYMBOLS |
额外符号文件路径,用于模块间符号依赖 | export KBUILD_EXTRA_SYMBOLS=/path/Module.symvers |
2.3.4 编译与加载测试
# 编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# 查看生成的模块
file hello.ko
# 输出: ELF 32-bit LSB relocatable, ARM, version 1
# 将模块拷贝到目标板,加载
insmod hello.ko
# 查看模块信息
modinfo hello.ko
# 卸载
rmmod hello
常见错误:
Invalid module format— 内核版本或配置不匹配,重新编译内核或模块。Unknown symbol— 模块依赖的符号未导出,检查Module.symvers或使用KBUILD_EXTRA_SYMBOLS。Permission denied— 目标板文件系统权限问题,使用chmod +x或 root 用户。
2.4 本章小结
- 交叉编译工具链是嵌入式驱动开发的基石,
ARCH和CROSS_COMPILE是核心环境变量。 - 内核源码树必须与目标板运行内核版本一致,且至少执行过
make modules_prepare。 - 模块 Makefile 通过
obj-m指定目标,借助-C $(KDIR) M=$(PWD)调用内核构建系统。 - 编译产物
.ko文件是 ELF 格式的可重定位目标文件,通过insmod加载到内核空间。
课后练习:编写一个最简单的
hello.c 模块,使用本章的 Makefile 模板,在 QEMU vexpress 或真实 ARM 开发板上完成交叉编译、加载和卸载测试。