嵌入式Linux基础:交叉编译工具链、内核源码结构与设备树
在进入RK3588外设驱动开发之前,必须夯实三个核心基础:交叉编译工具链解决“如何在x86主机上构建ARM目标代码”的问题;内核源码结构决定“驱动代码放在哪里、如何被编译”的工程组织方式;设备树(Device Tree)则承担“如何描述硬件资源并传递给内核”的接口职责。这三者构成了驱动开发的底层工作环境,任何一环缺失或理解偏差,都会导致驱动无法编译、无法加载或无法正确匹配硬件。本章将围绕这三块内容展开系统讲解,并结合RK3588平台给出可落地的操作路径。
上图展示了从开发主机到目标板的完整驱动开发链路。交叉编译工具链将源码编译为ARM指令集的可执行文件或内核模块;设备树源文件(DTS)通过dtc编译器生成二进制DTB,随内核启动时被解析;驱动模块通过compatible属性与设备树节点匹配,进而完成资源获取和硬件初始化。下面逐一深入。
2.1 交叉编译工具链:原理、配置与验证
核心原理
交叉编译是指在一种架构(如x86_64)的主机上,编译生成另一种架构(如ARM64)可执行代码的过程。RK3588采用ARMv8-A架构,支持64位指令集(AArch64),因此需要aarch64-linux-gnu-前缀的工具链。工具链包含编译器(gcc)、汇编器(as)、链接器(ld)、库(libc)以及调试工具(gdb)等。关键点在于头文件路径和库路径必须指向目标板的sysroot,否则编译出的程序可能在主机上能编译通过,但运行时因缺少ARM库而失败。
操作流程:安装与验证
- 下载工具链:从Rockchip官网或Linaro获取aarch64-linux-gnu-gcc压缩包,解压到/opt目录。
- 设置环境变量:编辑
~/.bashrc,添加export PATH=/opt/gcc-arm-10.3/bin:$PATH。 - 验证编译:编写一个简单的hello.c,执行
aarch64-linux-gnu-gcc hello.c -o hello,然后使用file hello查看输出格式。
# 验证命令
aarch64-linux-gnu-gcc -v
# 输出应包含 "Target: aarch64-linux-gnu"
file hello
# 输出应包含 "ELF 64-bit LSB executable, ARM aarch64"
案例拆解:编译并运行第一个ARM程序
假设开发环境为Ubuntu 22.04,目标板为RK3588 EVB。我们编写一个读取CPU信息的程序,交叉编译后通过NFS或scp传输到开发板运行。
// cpuinfo.c
#include <stdio.h>
#include <unistd.h>
int main() {
printf("RK3588 ARM64 测试程序\n");
sleep(1);
return 0;
}
编译命令:aarch64-linux-gnu-gcc -static cpuinfo.c -o cpuinfo(静态链接避免动态库缺失)。传输到开发板后执行./cpuinfo,若输出正常则工具链可用。若出现No such file or directory,通常是因为动态链接器路径不对,改用静态编译即可。
常见误区与复盘
- 误区1:忘记设置
ARCH=arm64和CROSS_COMPILE=环境变量,导致内核编译时使用x86配置。 - 误区2:直接使用主机gcc编译内核模块,加载时提示
invalid module format。 - 复盘:每次编译前执行
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-,并检查make menuconfig中的架构选项。
2.2 内核源码结构:RK3588相关目录与编译体系
核心原理
Linux内核采用分层目录结构,驱动开发主要关注drivers/、arch/和include/。对于RK3588,arch/arm64/boot/dts/rockchip/存放设备树源文件,drivers/下按子系统分类(如gpio、i2c、spi、pinctrl)。内核编译系统基于Kbuild,通过Kconfig和Makefile管理配置与编译。驱动开发者通常以模块(.ko)形式编译,便于调试和加载。
| 目录/文件 | 作用 | RK3588开发关注点 |
|---|---|---|
| arch/arm64/boot/dts/rockchip/ | 设备树源文件 | rk3588-evb.dts、rk3588s.dtsi |
| drivers/ | 驱动源码 | drivers/gpio、drivers/i2c、drivers/spi |
| include/dt-bindings/ | 设备树头文件 | gpio.h、pinctrl.h、clock.h |
| drivers/clk/ | 时钟框架 | RK3588时钟树定义 |
| drivers/pinctrl/ | 引脚控制 | rockchip pinctrl驱动 |
判断方法:如何定位驱动源码
当需要编写或修改某个外设驱动时,首先通过设备树中的compatible属性搜索内核源码。例如,若设备树节点包含compatible = "rockchip,rk3588-i2c",则在drivers/i2c/busses/下查找i2c-rockchip.c。使用grep -r "rk3588-i2c" drivers/可快速定位。
案例拆解:添加一个虚拟GPIO驱动模块
假设我们要在RK3588上添加一个简单的GPIO控制模块,用于点亮LED。步骤如下:
- 在
drivers/leds/下新建led-rk3588.c,实现platform_driver。 - 在
drivers/leds/Kconfig中添加配置项CONFIG_LEDS_RK3588。 - 在
drivers/leds/Makefile中添加obj-$(CONFIG_LEDS_RK3588) += led-rk3588.o。 - 编译模块:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- M=drivers/leds modules。
// led-rk3588.c 核心片段
static const struct of_device_id rk3588_led_match[] = {
{ .compatible = "rockchip,rk3588-led" },
{ }
};
MODULE_DEVICE_TABLE(of, rk3588_led_match);
static struct platform_driver rk3588_led_driver = {
.probe = rk3588_led_probe,
.driver = {
.name = "rk3588-led",
.of_match_table = rk3588_led_match,
},
};
module_platform_driver(rk3588_led_driver);
make defconfig或make rockchip_linux_defconfig),否则Kbuild无法生成头文件。
常见误区与总结
- 误区:修改设备树后不重新编译内核,仅替换dtb,导致驱动probe失败。
- 误区:在
drivers/下新增目录但忘记修改父级Makefile,模块不会被编译。 - 复盘:每次修改Kconfig/Makefile后,务必执行
make menuconfig确认新选项可见,并检查.config中对应宏是否置y或m。
2.3 设备树概念与基本语法
核心原理
设备树是一种描述硬件资源的数据结构,由节点(node)和属性(property)组成。内核启动时通过解析DTB,将硬件信息传递给驱动。每个节点有一个compatible属性,用于与驱动中的of_device_id匹配。节点还包含reg(寄存器地址)、interrupts(中断号)、clocks(时钟)等属性。RK3588的SoC级描述在rk3588s.dtsi中,板级差异在rk3588-evb.dts中通过#include和覆盖(overlay)实现。
基本语法与结构
// rk3588-evb.dts 示例
/dts-v1/;
#include "rk3588s.dtsi"
/ {
model = "Rockchip RK3588 EVB";
compatible = "rockchip,rk3588-evb", "rockchip,rk3588";
chosen {
stdout-path = &uart2;
};
leds {
compatible = "gpio-leds";
work_led {
gpios = &gpio1 RK_PB2 GPIO_ACTIVE_HIGH;
label = "work";
};
};
};
&i2c2 {
status = "okay";
touchscreen@38 {
compatible = "goodix,gt911";
reg = <0x38>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
};
};
关键语法点:&i2c2表示引用并覆盖父节点;status = "okay"启用设备;reg用于I2C从机地址;interrupt-parent指定中断控制器。设备树编译命令为make dtbs,生成rk3588-evb.dtb。
案例拆解:为RK3588添加SPI触摸屏节点
假设硬件上SPI2连接了触摸屏控制器(如GT9147),需要添加设备树节点。首先确认SPI2的基地址和中断引脚,然后在rk3588-evb.dts中追加:
&spi2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi2m2_cs0 &spi2m2_pins>;
touchscreen@0 {
compatible = "goodix,gt9147";
reg = <0>;
spi-max-frequency = <1000000>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PA0 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpio1 RK_PA1 GPIO_ACTIVE_LOW>;
};
};
编译后,内核启动时会在/sys/bus/spi/devices/spi2.0下生成设备节点。驱动通过spi_setup()和spi_sync()进行通信。
RK_PA0表示GPIO0_A0,偏移为0。错误编号会导致驱动申请GPIO失败或误操作其他引脚。
常见误区与复盘
- 误区1:忘记在
pinctrl-0中配置引脚复用,导致SPI功能不生效。 - 误区2:中断类型配置错误(如高电平触发写成低电平),导致中断风暴或无法触发。
- 复盘:调试设备树时,使用
ls /proc/device-tree/查看实际解析结果,或在内核启动参数中添加earlycon和printk日志定位解析错误。
rk3588s.dtsi中的外设节点定义,最后动手编写一个简单的platform驱动,形成完整闭环。