设备树是一种描述硬件的数据结构。它像一棵树一样,从根节点开始,一层一层描述板子上的所有设备。
有以下三种文件类型:
.dts (Device Tree Source):源码文件。
驱动对应的代码。
.dtsi (Device Tree Source Include):头文件。
描述芯片内部通用的硬件(比如 STM32MP157 芯片本身有的 CPU、GPIO、I2C 控制器)。
.dtb (Device Tree Blob):二进制文件。
.dts 编译后的产物,二进制给内核看。
学习总览
设备树框架 (The Syntax)
目标:学习 .dts 文件的语法规则
相关内容 :节点基本格式、标签、路径、属性、追加内容、特殊节点。
具体目的:1.定义一个硬件(写节点)2.描述它的参数(写属性 reg, compatible)
3.在图纸上进行修改(引用标签 &label)
获取设备树节点信息 (The APIs)
目标:学习 Linux 内核提供的 C 语言函数接口。这些函数通常以 of_ 开头(Open Firmware 的缩写)。
相关内容:查找节点函数、提取属性值的 of 函数、内存映射相关 of 函数。
具体目的:1.查找节点函数 (of_find_...)在设备树节点中,找到需要的那个节点。
2.提取属性值的 of 函数 (of_property_read_...)找到节点后,读取里面的具体数据。
3.内存映射相关 of 函数 (of_iomap / of_address_to_resource)处理 reg 属性(物理地址)把设备树里的物理地址,转换成驱动能用的虚拟地址或者 resource 结构体。
编写设备树 (.dts) 文件
节点基本格式 (Node Format)
设备树中,每一个 { … } 包裹起来的代码块就是一个节点。
语法:
[标签:] 节点名称[@单元地址] {
[属性定义]
[子节点定义]
};
节点名称 (Node Name):必填。描述这个设备是什么(例如 uart, i2c, cpu)。
单元地址 (@Unit Address):选填。描述这个设备在哪里(通常是寄存器首地址)。
注意:如果节点内部有 reg 属性,那么 @ 后面的数字必须和 reg 里的起始地址一致。
示例:
/* 名字叫 gpio,地址在 50002000 */
gpio@50002000 {
/* ... 属性 ... */
};
节点标签 (Node Labels)
节点名称可能很长(比如 gpio@50002000),而且嵌套很深。如果我想在文件的外面或者另一个文件里修改它,写全路径太累了。因此在节点名称前面加一个名字和冒号。
示例:
/* "gpioa" 就是标签 */
gpioa: gpio@50002000 {
/* ... */
};
以后只需要写 &gpioa 就能找到这个节点,不用写 gpio@50002000。
节点路径 (Node Paths)
和 Linux 的文件路径(/home/user/...)一样。设备树是一个树状结构,根节点是 /。
根节点 / 下面有个 ahb4 总线,总线下面有个 gpioa。 那么 gpioa 的绝对路径就是: /ahb4/gpio@50002000
在代码中很少手写绝对路径,通常都是用标签引用。但内核在查找节点时,内部使用的是这种路径逻辑。
节点属性 (Node Properties)
属性是键值对 key = value。
根据数据类型的不同,写法有严格规定:
A. 无值属性 (Empty/Boolean)
属性名; (没有值,只有分号)只要写了,就代表“真” (True)。
broken-cd;
B. 字符串属性 (String)
属性名 = "字符串"; (用双引号)
status = "okay";
C. 32位整数属性 (U32)
属性名 = <数值>;
clock-frequency = <24000000>; (十进制)
reg = <0x50002000 0x400>; (十六进制,多个数字用空格隔开)
D. 字符串列表 (String List)
属性名 = "字符串1", "字符串2"; (用逗号隔开)
compatible = "st,stm32mp157c", "st,stm32mp157";
追加/修改节点内容 (Appending/Modifying)
芯片厂商会提供一个基础文件(如 stm32mp157.dtsi),里面定义好了所有硬件。而您在自己的板级文件(myboard.dts)里,只是想*“开启”或“修改”某个设备。
语法:使用 &标签 引用。
示例:
/* 原始文件定义了标签 usart1 */
usart1: serial@5c000000 {
status = "disabled"; /* 默认关闭 */
};
//自己进行追加修改
/* 引用标签,直接修改内容 */
&usart1 {
status = "okay"; /* 改为开启 */
pinctrl-0 = <&usart1_pins_a>; /* 追加引脚配置 */
};
编译器会把这两部分合并。您的修改会覆盖默认值。这就避免了把整个节点重抄一遍。
特殊节点 (Special Nodes)
用于给内核传递配置信息,这两个节点不代表物理硬件。
A. aliases (名节点)
引用的方式给一个节点起别名用于排号
aliases {
serial0 = &usart1; /* 让 usart1 变成系统里的 ttySTM0 */
serial1 = &usart2; /* 让 usart2 变成系统里的 ttySTM1 */
};
B. chosen (选择节点)
传递运行时的参数,告诉内核启动参数是什么,以及 printk 的打印信息应该从哪个串口吐出来。
chosen {
bootargs = "root=/dev/mmcblk0p2 rw console=ttySTM0,115200";
stdout-path = "serial0:115200n8";
};
使用示例:
/arch/arm/boot/dts/下面 nano stm32mp157c-basic.dts
根目录执行make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_image/build -j4 dtbs
生成stm32mp157c-basic.dtb
替换板子下面的相应dtb /boot/下面
重启查看节点ls /proc/device-tree/test_demo_node
# 查看字符串
cat /proc/device-tree/test_demo_node/test_str
# 输出:Hello Device Tree
# 查看兼容性暗号
cat /proc/device-tree/test_demo_node/compatible
# 输出:fire,test_demo
/ {
/* ... 这里可能有 model = "..."; 等代码 ... */
/* =========== 我们的实验代码开始 =========== */
test_demo_node {
/* 1. 匹配兼容名 (Driver 靠这个查找到) */
compatible = "fire,test_demo";
/* 2. 状态:开启 */
status = "okay";
/* 3. 随便写个整数属性 */
test_val = <123456>;
/* 4. 随便写个字符串属性 */
test_str = "Hello Device Tree";
/* 5. 假装有个寄存器 (为了以后实验 platform_get_resource) */
reg = <0x50000000 0x400>;
};
/* ... 下面是原有的其他节点 ... */
};
在驱动代码里读设备树
流程如下:
查找节点(先找到文件柜里的文件夹)。
读取通用属性(读取文件夹里的普通文档,如整数、字符串)。
读取硬件资源(读取文件夹里的特殊机密,如寄存器地址、GPIO)。
查找节点 (Find Node)
of_find_node_by_path (按路径查找)
这个方法类似在 Windows 里按路径找文件。
struct device_node *of_find_node_by_path(const char *path);
//设备树中的绝对路径,如 "/ahb4/my_led"。
//问题:简单,但不灵活。如果设备树层级变了,路径就错了。
of_find_compatible_node (按兼容性查找)(推荐)
根据compatible (兼容的)属性查找。
struct device_node *of_find_compatible_node(struct device_node *from,//from开始查找的节点(填 NULL 表示从根节点开始)。
const char *type,//type设备类型(一般填 NULL)。
const char *compat);//compat填设备树里的 compatible 字符串,如 "fire,stm32mp1-led"。
//非常灵活。不管节点在树的哪个位置,只要compatible对,就能找到。
读取通用属性 (Read Properties)
拿到节点指针 (node) 后,就可以读取里面的属性
of_property_read_u32 (读一个整数)
像读取如 id = <10086>;一个整数
int of_property_read_u32(const struct device_node *np,//np: 节点指针。
const char *propname,//propname: 属性名字(如 "id")。
u32 *out_value);//out_value: 结果存这里(传入变量地址)。
of_property_read_string (读字符串)
像读取如 status = “okay”;一个字符属性
int of_property_read_string(const struct device_node *np,
const char *propname,
const char **out_string);
of_property_read_u32_array (读数组)
像读取如 data = <0x11 0x22 0x33>;
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz);//您想读取多少个数字。
读取硬件资源 (Special Hardware Resources)
获取寄存器地址 (reg) 和 GPIO 引脚。
内核提供了专用函数来处理它们,因为它们通常需要进行映射 (Map) 才能使用。
of_iomap (获取地址并直接映射)
它自动提取 reg 属性中的物理地址,并自动执行 ioremap
设备树写了 reg = <0x50002000 0x400>;
void __iomem *of_iomap(struct device_node *np, int index);
//index: 取第几段地址(通常填 0)。
//返回值直接返回虚拟地址指针,可以直接拿来 iowrite32
//省去了先获取 resource 结构体再手动 ioremap 的麻烦。
of_get_named_gpio (获取 GPIO 编号)
设备树写了 led-gpios = <&gpioa 13 0>;
int of_get_named_gpio(struct device_node *np,
const char *propname,//propname: 属性名( "led-gpios")。
int index);//index: 第几个 GPIO(通常填 0)。
//返回一个 GPIO 编号(整数)。拿到这个编号后,您就可以调用 gpio_request 和 gpio_set_value 来控制引脚了。
应用示例:
设备树根节点添加如下:
/ {
/* ... 其他原有节点 ... */
/* --- 这是我们添加的测试节点 --- */
test_node {
/* 1. 匹配暗号 (必须有) */
compatible = "fire,test-prop";
/* 2. 状态 (必须有) */
status = "okay";
/* 3. 整数属性 (u32) */
test-id = <10086>;
/* 4. 字符串属性 (string) */
test-name = "Hello Linux DT";
/* 5. 数组属性 (array) */
test-array = <0x11 0x22 0x33 0x44>;
/* 6. 这里的 reg 是虚构的,仅用于演示读取 */
reg = <0x50000000 0x1000>;
};
};
/*
执行 make dtbs。
将生成的 .dtb 文件替换板子 /boot/ 下的文件并重启。
重启后,在板子上输入 ls /proc/device-tree/test_node/。如果看到 test-id, test-name 等文件,说明设备树修改成功。
*/
编写一个简单的 Platform Driver,在 probe 函数中读取上面定义的所有属性。:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/of.h> // 包含 of_ 开头的函数
/* * probe 函数
* 当驱动和设备树节点匹配成功时,内核调用此函数
*/
static int test_probe(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node; // 获取节点指针
int ret;
// 变量用于存放读取到的数据
u32 id_val;
const char *str_val;
u32 array_val[4];
struct resource *res;
printk("=== 驱动匹配成功,开始读取属性 ===\n");
// -------------------------------------------------
// 1. 读取整数 (u32)
// 对应 dts: test-id = <10086>;
// -------------------------------------------------
ret = of_property_read_u32(node, "test-id", &id_val);
if (ret == 0) {
printk("读取 test-id 成功: %d\n", id_val);
} else {
printk("读取 test-id 失败\n");
}
// -------------------------------------------------
// 2. 读取字符串 (string)
// 对应 dts: test-name = "Hello Linux DT";
// -------------------------------------------------
ret = of_property_read_string(node, "test-name", &str_val);
if (ret == 0) {
printk("读取 test-name 成功: %s\n", str_val);
} else {
printk("读取 test-name 失败\n");
}
// -------------------------------------------------
// 3. 读取数组 (array)
// 对应 dts: test-array = <0x11 0x22 0x33 0x44>;
// -------------------------------------------------
// 先读取数组长度 (可选操作,防止越界)
// 这里的 4 是元素个数
ret = of_property_read_u32_array(node, "test-array", array_val, 4);
if (ret == 0) {
printk("读取 test-array 成功: 0x%x, 0x%x, 0x%x, 0x%x\n",
array_val[0], array_val[1], array_val[2], array_val[3]);
} else {
printk("读取 test-array 失败\n");
}
// -------------------------------------------------
// 4. 读取标准资源 (reg)
// 对应 dts: reg = <0x50000000 0x1000>;
// -------------------------------------------------
// 注意:读取 reg 属性通常不直接用 of_property_read,
//代替of_iomap,因为他还要回收机制
// 而是用 platform_get_resource,因为它会自动处理地址转换。
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
printk("读取 reg 成功: 起始地址=0x%x, 长度=0x%x\n",
(u32)res->start, (u32)resource_size(res));
} else {
printk("读取 reg 失败\n");
}
return 0;
}
/* remove 函数 */
static int test_remove(struct platform_device *pdev)
{
printk("=== 驱动卸载 ===\n");
return 0;
}
/* 匹配表 */
static const struct of_device_id test_match_table[] = {
{ .compatible = "fire,test-prop" }, // 必须和 dts 里的一模一样
{ /* sentinel */ }
};
// 别忘了声明设备表
MODULE_DEVICE_TABLE(of, test_match_table);
/* 平台驱动结构体 */
//由于平台总线(Platform Bus)已经在调用 probe 函数之前匹配成功了,所以没用到手动调用 of_find_compatible_node
//没有平台总线的 时候手动调用 of_find_compatible_node
static struct platform_driver test_driver = {
.probe = test_probe,//匹配成功执行这个函数
.remove = test_remove,//同理probe
.driver = {
.name = "test_prop_driver",
.of_match_table = test_match_table, // 挂载匹配表
},
};
/* 一键注册驱动宏 */
module_platform_driver(test_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbedFire");
上述代码注意说明:
以上使用了设备总线方式进行匹配采用内核驱动的of_match_table进行匹配,在probe函数之前,因此没用到of_find_compatible_node。
如果没有设备总线,就要自己用自己的设备树of_find_compatible_node去查找然后读取设备属性。
编译好 dt_get_prop.ko 并在板子上执行 insmod dt_get_prop.ko 后,立刻执行 dmesg | tail可以看到
[ 123.456789] === 驱动匹配成功,开始读取属性 ===
[ 123.456790] 读取 test-id 成功: 10086
[ 123.456791] 读取 test-name 成功: Hello Linux DT
[ 123.456792] 读取 test-array 成功: 0x11, 0x22, 0x33, 0x44
[ 123.456793] 读取 reg 成功: 起始地址=0x50000000, 长度=0x1000

被折叠的 条评论
为什么被折叠?



