3. SPI设备驱动开发环境搭建:交叉编译工具链、内核源码树配置、模块编译与设备树编译
搭建一个可靠、可复现的开发环境是嵌入式Linux驱动开发的基石。本章将系统讲解交叉编译工具链的安装与配置、内核源码树的准备与配置、内核模块的独立编译方法,以及设备树(Device Tree)的编译与集成。掌握这些步骤后,你便可以在主机上高效地为目标ARM平台编译SPI驱动模块。
3.1 交叉编译工具链
交叉编译是指在主机(x86_64架构)上编译生成目标平台(如ARM Cortex-A7)可执行代码的过程。工具链的选择直接影响驱动的兼容性与调试效率。
3.1.1 工具链类型与选择
| 类型 | 前缀示例 | 适用场景 |
|---|---|---|
| GNU GCC (Linaro) | arm-linux-gnueabihf- | 通用ARM 32位,支持硬浮点 |
| GCC ARM 64位 | aarch64-linux-gnu- | ARMv8 64位平台 |
| Buildroot生成 | arm-buildroot-linux-uclibcgnueabihf- | 定制化根文件系统与工具链 |
| Yocto SDK | arm-poky-linux-gnueabi- | Yocto项目集成开发 |
gcc-arm-linux-gnueabihf工具链即可满足需求。该工具链维护活跃,社区支持广泛。
3.1.2 安装与验证
以下步骤以Ubuntu 22.04主机为例,安装ARM 32位硬浮点工具链:
# 更新包索引
sudo apt update
# 安装交叉编译工具链
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf
# 验证安装
arm-linux-gnueabihf-gcc --version
# 测试编译一个简单C文件
echo 'int main(void){ return 0; }' > test.c
arm-linux-gnueabihf-gcc -o test test.c
file test
# 输出应包含:ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)
file命令显示x86-64架构,说明使用了主机gcc而非交叉编译器。请检查PATH环境变量,确保交叉编译器路径优先于系统gcc。
3.1.3 工具链组件说明
一个完整的交叉工具链包含以下核心组件:
- 编译器(gcc):将C/C++源码编译为目标架构的汇编代码。
- 汇编器(as):将汇编代码转换为目标文件(.o)。
- 链接器(ld):将多个目标文件和库链接为可执行文件或内核模块。
- C库(glibc/uclibc):提供系统调用接口与标准函数实现。
- 调试器(gdb):支持远程调试目标板上的程序。
3.2 内核源码树配置
驱动模块必须与目标板运行的内核版本完全一致。因此,需要准备与目标板相同版本的内核源码,并完成配置。
3.2.1 获取内核源码
推荐从官方内核仓库或板卡厂商(如NXP、TI、Rockchip)提供的BSP中获取:
# 方法一:从kernel.org下载主线内核
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.28.tar.xz
tar -xf linux-6.1.28.tar.xz
cd linux-6.1.28
# 方法二:从厂商BSP仓库克隆(以NXP i.MX6为例)
git clone -b imx_5.10.72_2.2.0 https://github.com/nxp-imx/linux-imx.git
cd linux-imx
3.2.2 内核配置步骤
配置内核需要生成.config文件,该文件决定了哪些功能被编译进内核或作为模块:
- 加载默认配置:使用厂商提供的defconfig文件。
- 手动调整:通过
menuconfig添加SPI驱动支持。 - 保存配置:确保
.config文件与目标板一致。
# 设置交叉编译环境变量
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 加载厂商默认配置(以imx_v7_defconfig为例)
make imx_v7_defconfig
# 进入图形化配置界面,确认SPI子系统已启用
make menuconfig
# 在menuconfig中检查以下选项:
# Device Drivers --->
# [*] SPI support --->
# <M> SPI driver for NXP i.MX SoCs
# [*] SPI device interface (spidev)
# 保存并退出
CONFIG_SPI=y或=m,以及对应SPI控制器的驱动选项(如CONFIG_SPI_IMX)已启用。如果使用spidev用户态接口,还需开启CONFIG_SPI_SPIDEV。
3.2.3 编译内核与模块
配置完成后,编译内核镜像和所有模块:
# 编译内核镜像(zImage)和设备树(dtb)
make -j$(nproc) zImage dtbs
# 编译所有内核模块
make -j$(nproc) modules
# 安装模块到临时目录(可选,用于后续打包)
make modules_install INSTALL_MOD_PATH=./modules_install
编译完成后,在arch/arm/boot/目录下生成zImage,在arch/arm/boot/dts/目录下生成对应的.dtb文件。
3.3 模块编译
在驱动开发阶段,频繁编译整个内核是不现实的。更高效的方式是使用内核源码树提供的Makefile框架,单独编译SPI驱动模块。
3.3.1 独立模块编译方法
在驱动源码目录下,使用以下命令单独编译模块:
# 设置环境变量
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 指定内核源码树路径
export KERNEL_DIR=/home/user/linux-6.1.28
# 进入驱动源码目录
cd ~/spi_driver
# 编译模块
make -C $(KERNEL_DIR) M=$(pwd) modules
其中-C指定内核源码树路径,M=$(pwd)指定驱动源码所在目录。编译成功后,会在当前目录生成.ko文件。
3.3.2 驱动Makefile模板
一个标准的SPI驱动Makefile示例如下:
# SPI设备驱动Makefile
obj-m := spi_mydevice.o
spi_mydevice-objs := spi_mydevice_main.o spi_mydevice_ops.o
# 内核源码树路径(由外部传入或硬编码)
KDIR ?= /home/user/linux-6.1.28
# 当前目录
PWD := $(shell pwd)
# 默认目标
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
# 清理
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# 安装模块到临时目录
install:
$(MAKE) -C $(KDIR) M=$(PWD) modules_install INSTALL_MOD_PATH=./output
EXTRA_CFLAGS += -DDEBUG可以启用驱动中的dev_dbg()调试输出。配合make V=1可以查看完整的编译命令,便于排查编译错误。
3.3.3 模块加载与验证
将编译好的.ko文件拷贝到目标板,使用以下命令加载和验证:
# 加载模块
insmod spi_mydevice.ko
# 查看模块加载信息
dmesg | tail -20
# 确认模块已加载
lsmod | grep spi_mydevice
# 查看SPI设备注册情况
cat /sys/kernel/debug/spi/spi_summary
# 卸载模块
rmmod spi_mydevice
3.4 设备树编译
设备树(Device Tree)是描述硬件资源的结构化数据,SPI控制器的基地址、中断号、时钟频率等信息都在设备树中定义。驱动通过设备树获取硬件参数,实现与具体硬件平台的解耦。
3.4.1 设备树源文件(.dts)结构
一个典型的SPI控制器节点定义如下:
/* arch/arm/boot/dts/imx6ul.dtsi (SoC级定义) */
spi1: spi@02008000 {
compatible = "fsl,imx6ul-spi", "fsl,imx51-spi";
reg = <0x02008000 0x4000>;
interrupts = <0 31 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_SPI1>,
<&clks IMX6UL_CLK_SPI1>;
clock-names = "ipg", "per";
dmas = <&sdma 3 7 1>,
<&sdma 4 7 2>;
dma-names = "rx", "tx";
status = "disabled";
#address-cells = <1>;
#size-cells = <0>;
};
/* 板级文件 imx6ul-14x14-evk.dts (板级定义) */
&spi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_spi1>;
cs-gpios = <&gpio4 9 GPIO_ACTIVE_LOW>;
status = "okay";
spidev@0 {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <20000000>;
};
};
3.4.2 编译设备树
设备树编译器(dtc)将.dts文件编译为二进制.dtb文件:
# 单独编译设备树
make imx6ul-14x14-evk.dtb
# 编译所有设备树
make dtbs
# 使用dtc工具手动编译(调试用)
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译dtb为dts(查看二进制内容)
dtc -I dtb -O dts -o myboard.dts myboard.dtb
3.4.3 设备树与驱动的绑定
驱动通过compatible属性与设备树节点匹配。在驱动代码中,使用of_match_table声明支持的设备:
static const struct of_device_id spi_mydevice_of_match[] = {
{ .compatible = "mycompany,spi-mydevice", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_mydevice_of_match);
static struct spi_driver spi_mydevice_driver = {
.driver = {
.name = "spi_mydevice",
.of_match_table = spi_mydevice_of_match,
},
.probe = spi_mydevice_probe,
.remove = spi_mydevice_remove,
};
- compatible不匹配:驱动和设备树中的compatible字符串必须完全一致,包括大小写。
- reg属性错误:SPI片选号从0开始,
reg = <0>表示第一个片选。 - status未设置:SoC级定义中status为"disabled",板级文件必须显式设置为"okay"。
- 时钟或引脚复用冲突:确保SPI引脚没有被其他外设占用。
3.5 环境验证清单
在开始编写SPI驱动之前,建议逐项确认以下环境要素:
| 检查项 | 验证命令/方法 | 预期结果 |
|---|---|---|
| 交叉编译器可用 | arm-linux-gnueabihf-gcc --version |
显示版本信息,无错误 |
| 内核源码已配置 | ls .config |
文件存在且非空 |
| SPI子系统已启用 | grep CONFIG_SPI .config |
显示CONFIG_SPI=y或=m |
| 设备树编译正常 | make dtbs |
无编译错误,生成.dtb文件 |
| 模块编译成功 | make modules |
生成.ko文件,无未定义符号 |
| 目标板内核版本一致 | uname -r(目标板) |
与源码树版本号相同 |
check_env.sh),每次开始开发前运行一次,确保环境一致性。这可以避免因环境问题导致的“驱动加载失败”假象,将精力集中在驱动逻辑本身。