嵌入式Linux基础:交叉编译工具链、内核源码结构、设备树基本概念与语法
在瑞芯微RK3588平台上进行外设驱动开发,首先需要建立三个核心认知:交叉编译工具链解决“在哪里编译”的问题,内核源码结构解决“驱动代码放在哪里、如何组织”的问题,而设备树(Device Tree)则解决“硬件资源如何描述、驱动如何匹配硬件”的问题。这三者构成了嵌入式Linux驱动开发的基石。本章将围绕这三个主题,从原理到实践,结合RK3588的具体情况展开深入讲解。
2.1 交叉编译工具链:原理与配置
核心原理
交叉编译(Cross Compilation)是指在一种架构的主机(Host)上,编译生成另一种架构目标机(Target)可执行代码的过程。在RK3588开发中,主机通常是x86_64架构的Ubuntu工作站,目标机则是ARMv8.2-A架构的RK3588 SoC。由于RK3588的算力有限且通常无完整的编译环境,我们必须在高性能主机上完成内核、驱动模块和应用程序的编译。
交叉编译工具链的核心组件包括:
- 交叉编译器:如
aarch64-linux-gnu-gcc,用于将C/C++源码编译为ARM64指令集的目标文件。 - 汇编器与链接器:
aarch64-linux-gnu-as和aarch64-linux-gnu-ld,负责汇编和链接。 - 库与头文件(sysroot):目标机运行所需的C库(如glibc或uClibc)和内核头文件,确保编译时能找到正确的API声明。
- 二进制工具:
objcopy、objdump、strip等,用于格式转换、反汇编和裁剪。
对于RK3588,Rockchip官方推荐使用 aarch64-linux-gnu- 前缀的工具链,通常基于GCC 10.3或更高版本。在SDK中,工具链路径通常位于 prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin。
判断方法与操作流程
要确认工具链是否可用,执行以下命令:
# 查看工具链版本
export PATH=/path/to/toolchain/bin:$PATH
aarch64-linux-gnu-gcc --version
# 查看默认目标架构
aarch64-linux-gnu-gcc -v 2>&1 | grep Target
在编译内核或驱动时,需要设置两个关键环境变量:
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
其中 ARCH 告诉内核构建系统目标架构为arm64,CROSS_COMPILE 指定交叉编译工具链前缀。编译内核时,make 命令会自动调用 aarch64-linux-gnu-gcc 等工具。
案例拆解:编译一个外部驱动模块
假设我们为RK3588编写了一个简单的字符设备驱动 hello_rk3588.c,其Makefile如下:
obj-m := hello_rk3588.o
KERNEL_DIR := /path/to/kernel-source
PWD := $(shell pwd)
all:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
执行 make 后,会生成 hello_rk3588.ko 文件。使用 file 命令验证其架构:
file hello_rk3588.ko
# 输出: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), BuildID[sha1]=..., not stripped
确认是ARM aarch64架构后,即可通过 scp 或 adb push 将其部署到RK3588开发板上,使用 insmod 加载。
gcc 编译驱动模块,否则会生成x86_64架构的 .ko 文件,加载时内核会报 Invalid module format 错误。另外,内核源码版本必须与开发板上运行的内核版本完全一致,否则模块加载会因版本魔术(vermagic)不匹配而失败。
2.2 内核源码结构:从全局到驱动目录
核心原理
Linux内核源码是一个庞大而有序的工程。对于驱动开发者而言,理解其目录结构有助于快速定位代码、编写Kconfig和Makefile。RK3588的SDK通常基于Linux 5.10或6.1内核,其顶层目录结构如下:
| 目录 | 作用 | 驱动开发关注点 |
|---|---|---|
| arch/ | 体系结构相关代码 | arch/arm64/boot/dts/rockchip/ 存放RK3588设备树源文件 |
| drivers/ | 设备驱动子目录 | drivers/gpio, drivers/i2c, drivers/spi, drivers/mfd 等 |
| include/ | 内核头文件 | include/linux/ 下为驱动API,include/dt-bindings/ 为设备树绑定宏 |
| Documentation/ | 内核文档 | Documentation/devicetree/bindings/ 为设备树绑定文档 |
| scripts/ | 构建脚本 | checkpatch.pl 用于代码风格检查 |
| tools/ | 用户态工具 | 有时包含调试和测试工具 |
驱动开发的核心区域是 drivers/ 目录,其内部按子系统分类,如 drivers/i2c、drivers/spi、drivers/pinctrl 等。每个子系统目录下通常包含:
- 核心框架代码:如
i2c-core.c,实现总线协议和驱动模型。 - 控制器驱动:如
i2c-rockchip.c,实现RK3588的I2C控制器适配器。 - 客户端驱动:如
eeprom.c,挂载在I2C总线上的具体设备驱动。
判断方法与操作流程
当需要添加一个新的外设驱动时,应遵循以下流程:
- 判断外设属于哪种总线类型(I2C、SPI、PCIe、USB等),进入对应的
drivers/子目录。 - 查看该子目录下的
Kconfig文件,了解已有的配置选项,添加新的配置项。 - 查看
Makefile,将新驱动源文件与配置项关联。 - 编写驱动源码,并参考同目录下类似驱动的实现。
例如,若要添加一个I2C接口的温湿度传感器,应在 drivers/i2c 下新建 htu21d.c,并在 Kconfig 中添加 config SENSORS_HTU21D,在 Makefile 中添加 obj-$(CONFIG_SENSORS_HTU21D) += htu21d.o。
案例拆解:定位RK3588的GPIO驱动
RK3588的GPIO驱动位于 drivers/pinctrl/pinctrl-rockchip.c。该文件实现了 pinctrl 子系统,负责引脚复用、上拉/下拉、驱动强度等配置。设备树中的 pinctrl-0 属性引用了该驱动提供的引脚配置函数。当我们在设备树中为某个外设配置引脚时,内核会通过 pinctrl 框架调用此驱动完成硬件初始化。
通过阅读该驱动,可以了解到RK3588的GPIO bank分为4组(GPIO0-GPIO4),每组32个引脚,支持复用功能(如I2C、UART、SPI)。驱动中通过 rockchip_pinctrl_set_mux 函数设置引脚复用,通过 rockchip_pinconf_set 设置电气属性。
make menuconfig 进行图形化配置。对于RK3588 SDK,通常使用 make rockchip_defconfig 生成默认配置,然后在此基础上调整。配置完成后,务必保存为新的 defconfig,以便后续复用。
2.3 设备树:硬件描述语言与驱动匹配机制
核心原理
设备树是一种描述硬件资源的数据结构,它通过节点(Node)和属性(Property)来描述SoC内部外设、外部总线设备、中断、时钟、GPIO、DMA等资源。设备树源文件(.dts)经过设备树编译器(dtc)编译为二进制设备树(.dtb),在系统启动时由Bootloader加载并传递给内核。
设备树的核心价值在于将硬件配置信息从内核源码中剥离出来,使得同一份内核镜像可以支持不同的硬件平台。驱动通过 compatible 属性与设备树节点匹配,从而获取硬件资源。
一个典型的设备树节点如下:
&i2c2 {
status = "okay";
clock-frequency = <400000>;
htu21d@40 {
compatible = "meas,htu21d";
reg = <0x40>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>;
vdd-supply = <&vcc_3v3>;
};
};
其中 &i2c2 表示引用I2C2控制器节点,htu21d@40 是子节点,compatible 用于匹配驱动,reg 表示设备地址,interrupts 描述中断资源。
判断方法与操作流程
编写设备树节点的基本流程:
- 查阅RK3588的TRM(技术参考手册)或SDK中的
rk3588.dtsi,确认外设挂接的总线控制器节点名称(如&i2c2)。 - 在板级
.dts文件中,通过&节点名引用并添加子节点。 - 设置
status = "okay"使能控制器,并配置必要属性(如时钟频率)。 - 为外设添加子节点,设置
compatible、reg、interrupts等属性。 - 使用
make dtbs编译设备树,生成.dtb文件。
验证设备树是否正确,可以在开发板上执行:
# 查看设备树中I2C2节点信息
ls /proc/device-tree/i2c@feac0000/
cat /proc/device-tree/i2c@feac0000/htu21d@40/compatible
案例拆解:为RK3588添加SPI设备节点
假设我们需要在RK3588的SPI1总线上挂载一个MCP2515 CAN控制器。首先在 rk3588.dtsi 中确认SPI1节点存在,然后在板级 .dts 中添加:
&spi1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi1m2_cs0 &spi1m2_pins>;
mcp2515@0 {
compatible = "microchip,mcp2515";
reg = <0>;
spi-max-frequency = <10000000>;
clocks = <&cru CLK_REF_MCP2515>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
vdd-supply = <&vcc_3v3>;
xceiver-supply = <&vcc_3v3>;
};
};
这里的关键属性包括:reg = <0> 表示片选0,spi-max-frequency 设置SPI时钟频率,interrupts 指定中断引脚。编译后,内核的 mcp251x 驱动会通过 compatible 匹配该节点,并调用 spi_setup 初始化设备。
interrupts 属性中的 RK_PB2 宏定义在 include/dt-bindings/pinctrl/rockchip.h 中,表示GPIO1_B2引脚。如果宏定义缺失,编译会报错。建议在修改设备树后,使用 dtc -I fs -O dts /proc/device-tree 反编译当前设备树,确认实际生效的配置。
常见误区与总结
- 误区1:设备树中的
reg属性值必须与硬件实际地址一致,否则驱动无法正确访问寄存器。 - 误区2:忘记设置
status = "okay",导致控制器被禁用,外设无法工作。 - 误区3:中断号使用错误。RK3588的中断号是SPI(Shared Peripheral Interrupt)编号,需要在
include/dt-bindings/interrupt-controller/arm-gic.h中查找正确的宏。 - 误区4:编译设备树时,未包含正确的
#include路径,导致找不到rk3588.dtsi或宏定义。
总结而言,交叉编译工具链、内核源码结构和设备树是嵌入式Linux驱动开发的“铁三角”。工具链决定了代码如何被编译,内核源码结构决定了驱动代码的组织方式,而设备树则提供了硬件与驱动之间的桥梁。掌握这三者,是后续编写RK3588外设驱动的基础。在实际开发中,建议先搭建好交叉编译环境,再深入阅读内核源码中与目标外设相关的子系统代码,最后通过设备树将硬件资源描述清楚,形成“硬件-设备树-驱动”的完整链路。