8-Linux驱动开发-设备树

设备树是一种描述硬件的数据结构。它像一棵树一样,从根节点开始,一层一层描述板子上的所有设备。
有以下三种文件类型:
	.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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

偶像你挑的噻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值