RK3568设备树驱动开发实战:从语法解析到OF函数应用
1. 设备树是什么?为什么RK3568开发必须掌握它?
大家好,我是老张,一个在嵌入式Linux和智能硬件领域摸爬滚打了十多年的老码农。今天咱们不聊虚的,直接上干货,聊聊在RK3568平台上做驱动开发时,那个你绝对绕不开、又让很多新手头疼的东西——Linux设备树。
我刚开始接触设备树那会儿,也是一头雾水。心想,以前在单片机或者老版本Linux上,硬件信息不都是直接写在驱动代码里的吗?怎么现在又搞出个“.dts”文件,还得学一套新语法?后来踩过几次坑才明白,设备树这玩意儿,简直是现代Linux驱动开发的“基石”,尤其是对于像RK3568这样功能复杂的多核应用处理器。
简单来说,设备树就是一个描述硬件板级信息的配置文件。你可以把它想象成你电脑的“硬件清单”或者“配置说明书”。这份清单用结构化的文本(.dts文件)写成,告诉Linux内核:“咱们这块RK3568开发板上,CPU是四核Cortex-A55,内存从这里开始,I2C0控制器在这里,上面挂了个触摸屏,I2C1上挂了个EEPROM,GPIO0的第三个引脚连着个用户LED……”
那么,为什么非要引入设备树呢?这得从Linux内核的“黑历史”说起。在设备树出现之前,ARM架构的板级信息(比如上面说的那些)都硬编码在内核源码的arch/arm/mach-xxx目录下。每出一款新板子,就要在内核里加一堆描述它硬件的C文件。结果就是内核源码急剧膨胀,充斥着大量冗余、板卡特定的代码。Linux之父Linus Torvalds对此非常不满,甚至发出了著名的“吐槽”。于是,社区借鉴了PowerPC架构的经验,引入了设备树,成功地将硬件描述从内核中剥离出来。
对于使用RK3568的我们来说,好处是显而易见的:
- 内核通用化:同一份Linux内核,搭配不同的设备树文件(.dtb),就能启动在不同的RK3568开发板上(比如野火的LubanCat、正点原子的ATK-DLRK3568等)。
- 配置灵活:改硬件?不用重新编译内核,改改设备树就行。比如把I2C1上的传感器从MPU6050换成BMP280,只需要在设备树里改几行。
- 结构清晰:设备树采用树形结构,硬件拓扑一目了然,比散落在大量C文件里的
#define和结构体好维护多了。
所以,如果你想在RK3568上玩转Linux驱动,无论是点个灯、读个传感器,还是驱动屏幕、网卡,第一关就是理解和修改设备树。别怕,它的语法其实比C语言简单,接下来咱们就一步步拆解。
2. 庖丁解牛:设备树文件(.dts/.dtsi)语法全解析
刚开始看RK3568的SDK,arch/arm64/boot/dts/rockchip/目录下那一堆.dts、.dtsi文件可能让人发怵。别慌,我们像读文章一样,先掌握它的基本“字词句”。
2.1 节点与属性:设备树的“骨骼”与“血肉”
设备树的核心就两个东西:节点(Node) 和 属性(Property)。节点代表一个设备或总线,属性则是这个设备的详细参数。
一个最简单的设备树长这样:
/dts-v1/;
/ {
compatible = "rockchip,rk3568";
};
/dts-v1/;:声明版本,必须放在文件开头。/ { ... };:这是根节点。所有设备都“挂”在这棵树的根上。compatible = "rockchip,rk3568";:这是根节点的一个属性。它是一个“键值对”,键是compatible,值是字符串"rockchip,rk3568",用来告诉内核这块板子兼容的硬件平台。
节点可以嵌套,形成父子关系。比如描述RK3568的CPU:
/ {
cpus {
#address-cells = <2>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a55";
reg = <0x0 0x0>;
};
cpu1: cpu@100 {
device_type = "cpu";
compatible = "arm,cortex-a55";
reg = <0x0 0x100>;
};
};
};
cpus是根节点/的子节点,它下面又有cpu0和cpu1两个子节点。cpu0: cpu@0:这里引入了两个概念。cpu0是标签(label)。后面我们可以用&cpu0来引用这个节点,非常方便。cpu@0是节点名。格式通常是<name>[@<unit-address>],name是设备类型(如cpu, i2c, spi),unit-address通常是该设备的首地址或ID,这里0表示第一个CPU核心。
#address-cells和#size-cells:这是数据格式说明属性。它们定义了子节点reg属性中“地址”和“大小”字段各占用几个32位数(称为cell)。这里cpus节点的设置表示,它的子节点的reg属性中,地址占2个cell(64位),大小占0个cell(因为CPU核心没有长度概念)。reg = <0x0 0x0>:这是寄存器区域属性。根据父节点的#address-cells=<2>,这里两个0x0共同组成了CPU0的ID或地址。对于cpu1,它的reg是<0x0 0x100>。
2.2 你必须掌握的六大核心属性
设备树属性很多,但掌握下面这几个,你就能解决80%的问题。
1. compatible:驱动的“身份证” 这是最重要的属性,没有之一。内核靠它来为设备匹配正确的驱动程序。
i2c1: i2c@fe5a0000 {
compatible = "rockchip,rk3568-i2c", "rockchip,rk3399-i2c";
...
};
值可以是一个或多个字符串。内核会按顺序尝试匹配。首先找rockchip,rk3568-i2c的驱动,如果没找到,就找兼容的rockchip,rk3399-i2c的驱动。这保证了驱动的向后兼容性。
2. reg:设备的“住址”和“地盘大小” 定义设备寄存器空间的起始地址和长度。
uart2: serial@fe660000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe660000 0x0 0x100>;
...
};
reg = <地址1 长度1 [地址2 长度2 ...]>。这里的地址0xfe660000和长度0x100(256字节)就是UART2外设在内存映射中的位置和寄存器范围。驱动通过这个信息来访问硬件。
3. status:设备的“开关”
控制设备使能与否,最常用的就是okay和disabled。
&i2c1 {
status = "okay"; // 启用I2C1控制器
clock-frequency = <400000>; // 设置I2C时钟为400kHz
sensor@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
status = "disabled"; // 暂时禁用这个传感器
};
};
在调试时,你可以通过status = "disabled"来临时关闭某个设备,非常方便。
4. pinctrl:管脚复用的“指挥官”
RK3568的引脚功能可以复用(比如一个引脚可以是GPIO,也可以是UART的TX)。pinctrl属性用来指定一组特定的引脚功能配置。
&uart2 {
pinctrl-names = "default";
pinctrl-0 = <&uart2m0_xfer>;
status = "okay";
};
这里表示UART2使用名为uart2m0_xfer的那组引脚配置。这组配置通常在pinctrl节点中定义,由原厂提供,我们一般不需要修改,知道怎么引用就行。
5. interrupts:设备的“呼叫号码” 定义设备使用的中断号。
&uart2 {
interrupts = <GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;
...
};
这表示UART2使用SPI类型中断,编号121,高电平触发。这些宏(GIC_SPI, IRQ_TYPE_LEVEL_HIGH)一般在.dtsi头文件中定义。
6. clocks 和 clock-names:设备的“心跳” 现代SoC时钟系统复杂,设备需要知道自己的时钟来源。
&i2c1 {
clocks = <&cru CLK_I2C1>, <&cru PCLK_I2C1>;
clock-names = "i2c", "pclk";
};
这里I2C1需要两个时钟:i2c(功能时钟)和pclk(总线时钟)。&cru是时钟控制器的标签。
2.3 模块化设计:.dts、.dtsi与头文件
你可能会问,为什么RK3568的设备树被拆成了那么多文件?这是为了代码复用和分层管理,和C语言用.c和.h文件一个道理。
| 文件类型 | 作用 | 类比 | 示例 |
|---|---|---|---|
| .dts | 板级设备树 | 主程序(.c) | rk3568-lubancat2.dts,描述具体开发板的硬件差异,比如用了哪个型号的屏幕、网卡,用户按键接在哪个GPIO上。 |
| .dtsi | SoC级设备树 | 头文件/通用模块(.h) | rk3568.dtsi,描述RK3568芯片本身的硬件资源,比如CPU、内存控制器、所有I2C/SPI/UART控制器的基地址等。所有基于RK3568的板子都会包含它。 |
| .h | 宏定义头文件 | 宏定义头文件(.h) | dt-bindings/pinctrl/rockchip.h,里面定义了诸如GPIO_ACTIVE_HIGH、RK_PB0等常量,方便编写和阅读。 |
一个典型的板级.dts文件开头是这样的:
// 板级设备树:rk3568-my-board.dts
/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include "rk3568.dtsi" // 包含SoC通用定义
/ {
model = "My Custom RK3568 Board";
compatible = "my-company,my-rk3568-board", "rockchip,rk3568";
// 这里开始定义或覆盖板级特有的硬件
leds {
compatible = "gpio-leds";
user_led: led-0 {
label = "heartbeat";
gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
// 追加或修改从rk3568.dtsi继承来的节点
&i2c1 {
status = "okay";
// 添加板子上的I2C设备
temperature_sensor: lm75@48 {
compatible = "national,lm75";
reg = <0x48>;
};
};
这种结构清晰明了:芯片原厂维护rk3568.dtsi,板卡厂商或开发者基于它编写自己的.dts,只关注板级差异。切记:不要直接修改rk3568.dtsi这类通用文件! 你的所有修改都应该在自己的板级.dts文件中,通过&标签的方式对原有节点进行追加或覆盖。
3. 实战演练:为RK3568添加一个I2C设备节点
光说不练假把式。假设我们要在自己的RK3568板子上,给I2C1总线添加一个温湿度传感器SHT30。以下是完整步骤。
3.1 第一步:查阅硬件连接与芯片数据手册
- 硬件连接:确认SHT30的I2C地址。SHT30的地址引脚通常可配置,假设我们接地,地址是
0x44。 - 原理图:确认SHT30连接在RK3568的哪个I2C控制器上。假设是I2C1。
- 驱动支持:检查内核是否支持SHT30。可以
grep -r "sht3" drivers/,或者去Documentation/devicetree/bindings/iio/humidity/目录下找绑定文档。我们假设内核已有驱动drivers/iio/humidity/sht3x.c,其兼容性字符串是"sensirion,sht3x"。
3.2 第二步:修改设备树文件
找到你的板级设备树文件(例如rk3568-my-board.dts),在末尾添加以下内容:
/* 在文件末尾,对I2C1控制器节点进行追加 */
&i2c1 {
status = "okay"; // 确保I2C1控制器使能
clock-frequency = <100000>; // 设置I2C总线速度为100kHz,SHT30支持
/* 添加SHT30子节点 */
sht3x: humidity-sensor@44 {
compatible = "sensirion,sht3x";
reg = <0x44>; // I2C设备地址
// 以下是一些可选属性,根据传感器和驱动文档添加
// vdd-supply = <&vcc_3v3>; // 如果供电可控,可关联稳压器
// interrupt-parent = <&gpio0>;
// interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>; // 如果使用中断引脚
};
};
代码解读:
&i2c1:引用在rk3568.dtsi中定义的i2c1节点标签。status = "okay":确保控制器启用。有时原厂配置可能是disabled。clock-frequency:设置总线速率。不设置则用默认值(通常100kHz)。sht3x::为这个节点定义一个本地标签,方便其他地方引用(虽然这里可能用不到)。humidity-sensor@44:节点名。@后面最好跟设备地址,提高可读性。compatible:必须与驱动中的of_device_id表匹配。reg:I2C设备地址,0x44。
3.3 第三步:编译与更新设备树
修改保存后,在Linux内核源码根目录下编译设备树:
# 假设你的编译环境已配置好(如aarch64-linux-gnu-工具链)
# 方式1:编译所有设备树
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
# 方式2:只编译你修改的板子对应的设备树
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- rockchip/rk3568-my-board.dtb
编译成功后,会在arch/arm64/boot/dts/rockchip/目录下生成rk3568-my-board.dtb文件。
将这个.dtb文件替换到开发板/boot目录下(具体路径因系统而异,可能是/boot/dtb/或/boot/),然后重启开发板。
3.4 第四步:验证节点是否生效
系统启动后,可以通过以下方式验证:
- 查看系统日志:
dmesg | grep -i sht3x或dmesg | grep -i i2c1,看是否有驱动加载和设备发现的日志。 - 查看sysfs:如果驱动加载成功,在
/sys/bus/iio/devices/目录下应该会出现新的设备目录,里面会有in_humidity_raw、in_temp_raw等文件,可以通过cat读取数据。 - 查看设备树展开:
ls /proc/device-tree/i2c@fe5a0000/,应该能看到sht3x@44目录,进去cat compatible能看到sensirion,sht3x。
如果一切顺利,恭喜你,你已经成功通过设备树添加了一个硬件设备!这个过程是RK3568驱动开发的常态:查阅硬件手册 -> 修改设备树 -> 编译更新 -> 验证。
4. 驱动如何与设备树对话:OF函数实战指南
设备树描述好了硬件,那驱动代码怎么读取这些信息呢?这就是OF(Open Firmware)函数的用武之地。内核提供了一整套以of_、of_property_为前缀的函数,专门用来解析设备树。
4.1 在驱动中获取设备树信息的标准流程
我们以一个虚拟的“多功能传感器”驱动为例,演示如何读取设备树中定义的资源。
假设设备树节点如下:
&i2c2 {
my_sensor: sensor@5a {
compatible = "my-company,multi-sensor-v1";
reg = <0x5a>;
// 自定义属性
sample-rate-hz = <100>; // 采样率
channel-names = "temperature", "pressure", "humidity";
// 中断
interrupt-parent = <&gpio0>;
interrupts = <RK_PC1 IRQ_TYPE_EDGE_RISING>;
// 复位GPIO
reset-gpios = <&gpio0 RK_PB2 GPIO_ACTIVE_LOW>;
// 复杂属性:一个16位的数据数组
calibration-data = <0x1234 0x5678 0x9abc>;
};
};
对应的驱动探测(probe)函数中,可以这样操作:
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
static int my_sensor_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct device_node *np = dev->of_node; // 关键!获取本设备对应的设备树节点
int ret, sample_rate;
u32 cal_data[3];
const char *channel_name;
int irq, reset_gpio;
// 1. 检查兼容性 (通常框架已做,这里演示)
if (!of_device_is_compatible(np, "my-company,multi-sensor-v1")) {
dev_err(dev, "Device tree node not compatible!\n");
return -EINVAL;
}
// 2. 读取整数属性
ret = of_property_read_u32(np, "sample-rate-hz", &sample_rate);
if (ret) {
dev_warn(dev, "No sample-rate-hz property, using default 10Hz\n");
sample_rate = 10;
} else {
dev_info(dev, "Sample rate set to %d Hz from DT\n", sample_rate);
}
// 3. 读取字符串属性(获取第一个)
ret = of_property_read_string_index(np, "channel-names", 0, &channel_name);
if (!ret) {
dev_info(dev, "First channel name: %s\n", channel_name);
}
// 4. 读取数组属性
ret = of_property_read_u32_array(np, "calibration-data", cal_data, 3);
if (!ret) {
dev_info(dev, "Calibration data: 0x%04x, 0x%04x, 0x%04x\n",
cal_data[0], cal_data[1], cal_data[2]);
}
// 5. 获取中断号(标准方法)
irq = of_irq_get(np, 0); // 获取第一个中断
if (irq <= 0) {
dev_err(dev, "Failed to get IRQ\n");
// 可能是不使用中断的设备
} else {
dev_info(dev, "IRQ number: %d\n", irq);
// 后续可以 request_irq(irq, ...);
}
// 6. 获取GPIO描述符(标准方法)
reset_gpio = of_get_named_gpio(np, "reset-gpios", 0);
if (reset_gpio < 0) {
dev_err(dev, "Failed to get reset GPIO\n");
} else {
dev_info(dev, "Reset GPIO: %d\n", reset_gpio);
// 后续可以 gpio_request(reset_gpio, ...); gpio_direction_output(...);
}
// 7. 直接内存映射(对于寄存器空间)
// 对于I2C设备,我们通常用client->addr,而不是直接映射。
// 但对于platform设备,如果有reg属性,可以:
// struct resource *res;
// void __iomem *base;
// res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// base = devm_ioremap_resource(dev, res);
// 或者直接用 of_iomap(np, 0);
return 0; // 探测成功
}
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "my-company,multi-sensor-v1" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
关键点总结:
dev->of_node是通往设备树节点的桥梁。of_property_read_*系列函数是读取属性的主力,安全且带错误处理。- 对于中断、GPIO、时钟、DMA等标准资源,强烈建议使用
of_irq_get、of_get_named_gpio、of_clk_get等专用函数,而不是手动解析interrupts、gpios属性。这些函数处理了底层细节,更可靠。 of_device_is_compatible可以用来做额外的兼容性检查。of_find_node_by_*系列函数用于在驱动中主动查找节点,但在标准的probe函数中,通常不需要,因为框架已经通过of_match_table把匹配好的节点传给了你。
4.2 进阶:处理reg属性与地址转换
对于有内存映射寄存器(如SPI控制器、内部硬件加速器)的设备,reg属性的解析至关重要。
// 假设设备树中:reg = <0x0 0xfe610000 0x0 0x1000>;
struct resource res;
void __iomem *base;
// 方法1:使用platform设备框架提供的API(推荐)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第0个内存资源
base = devm_ioremap_resource(&pdev->dev, &res); // 映射并管理资源
if (IS_ERR(base)) {
return PTR_ERR(base);
}
// 方法2:直接使用OF函数
base = of_iomap(np, 0); // 映射节点第0个reg区域
if (!base) {
dev_err(dev, "Failed to iomap\n");
return -ENOMEM;
}
// 记得在remove或error路径用 iounmap(base);
devm_ioremap_resource是首选,因为它自动管理资源申请和释放,并且会进行必要的安全检查(比如资源是否已被占用)。
5. 避坑指南:RK3568设备树开发常见问题
在我多年的开发经验里,新手在设备树上踩的坑大同小异。这里列几个高频问题,帮你提前排雷。
问题一:修改了.dts,但编译后系统没变化。
- 检查点:
- 编译命令是否正确?确认输出了新的
.dtb文件,且文件时间戳已更新。 - 是否将正确的
.dtb文件拷贝到了开发板的正确位置?不同启动加载器(U-Boot)的加载路径可能不同,常见的是/boot/或/boot/dtb/。有时需要重命名为dtb或dtb.img。 - U-Boot是否真的加载了你更新的文件?查看U-Boot启动日志,看它加载的是哪个dtb文件。
- 最容易被忽略的一点:内核是否缓存了设备树?确保开发板是冷启动(彻底断电再上电),而不是热重启。有些系统热重启可能不会重新从存储设备加载dtb。
- 编译命令是否正确?确认输出了新的
问题二:驱动probe函数没被调用。
- 检查点:
- compatible属性是否拼写正确? 大小写、逗号、引号。必须与驱动中的
of_device_id表里的字符串完全一致。用dmesg | grep -i compatible看看内核解析出来的值是什么。 - 父总线控制器是否使能? 如果你的设备挂在I2C1上,确保
&i2c1节点里status = "okay"。我见过太多人只加了子设备节点,忘了打开父控制器。 - 驱动本身是否编译进内核或作为模块加载?
lsmod查看一下。 - 检查内核日志是否有OF相关的错误,比如
of_i2c: probe of sensor@44 failed with error -6,这能提供线索。
- compatible属性是否拼写正确? 大小写、逗号、引号。必须与驱动中的
问题三:GPIO或中断申请失败。
- 检查点:
- GPIO编号是否正确?RK3568的GPIO编号是分组的(如GPIO0, GPIO1)。在设备树中,应使用
<&gpio0 RK_PA0 GPIO_ACTIVE_HIGH>这样的形式,而不是一个全局编号。驱动中应使用of_get_named_gpio来获取,它会正确转换。 - GPIO是否被其他驱动占用?
cat /sys/kernel/debug/gpio可以查看GPIO使用情况。 - 中断号是否正确?使用
of_irq_get获取。检查设备树中interrupt-parent指向的中断控制器节点是否正确。 - 引脚复用冲突:这个引脚是否被
pinctrl配置成了其他功能(比如UART的TX)?检查设备树中该设备的pinctrl属性,以及系统中其他节点是否配置了相同的引脚。RK3568的引脚功能是唯一的。
- GPIO编号是否正确?RK3568的GPIO编号是分组的(如GPIO0, GPIO1)。在设备树中,应使用
问题四:设备树语法错误导致编译失败或内核启动卡住。
- 检查点:
- 使用
dtc工具进行语法检查:dtc -I dts -O dtb -o /dev/null your.dts。这会输出具体的语法错误行。 - 确保节点标签唯一。同一个
.dtsi文件可能被包含多次,如果标签重复,合并时会出错。 - 属性值格式要匹配。比如
reg = <0x1000>是整数,reg = "value"是字符串,不能混用。 - 内核启动卡在“Loading Device Tree...”或类似地方,通常是设备树二进制(.dtb)本身损坏或格式错误。回退到上一个能启动的版本进行对比。
- 使用
问题五:如何调试设备树?
- 三板斧:
/proc/device-tree/:这是内核中设备树结构的直观反映。它是一个目录树,直接对应设备树节点和属性。cat属性文件可以查看值,ls可以看子节点。这是最直接的调试手段。- 内核启动参数:在U-Boot中修改内核启动参数,添加
devicetree相关调试选项,如earlycon(虽然主要用于串口,但有时能看出问题)。 - 内核配置:确保内核配置了
CONFIG_PROC_DEVICETREE=y和CONFIG_OF_DEBUG=y(后者可能需要手动在make menuconfig中开启,位于Device Drivers -> Device Tree and Open Firmware support),可以开启更详细的OF调试信息。
最后,也是最重要的建议:多看官方示例和绑定文档。Rockchip SDK中arch/arm64/boot/dts/rockchip/目录下有大量参考设计。内核源码Documentation/devicetree/bindings/目录下的txt文件,是设备树属性的“权威字典”,描述了每个兼容设备所支持的标准属性及其含义。养成查阅这些文档的习惯,能让你事半功倍。
设备树是连接硬件和Linux驱动的桥梁,初学时有门槛,但一旦掌握,你就会发现它带来的灵活性和可维护性是无与伦比的。在RK3568这样强大的平台上,从点灯到驱动复杂的多媒体外设,都离不开对设备树的熟练运用。希望这篇长文能帮你打通任督二脉,在RK3568的驱动开发之路上走得更顺。
更多推荐



所有评论(0)