我的理解本质上还是复用。尤其是嵌入式领域,设备多种多样,但是很多设备接口都是标准的,或者大同小异。以前驱动开发可能每个设备商都去抄别家的现成代码,这样造成了大量的垃圾代码。后面linux内核就把这些做成公共库抽象出来,后面设备只需要传入“我是什么设备”,那么linux就可以从内核中公共库找到需要的驱动,避免了很多质量不一的冗余代码,而且也方便了管理。

在设备树中,涉及到的文件主要是DTS,DTSI,还有DTB。
基本流程是编译前编写DTS(有一个DTSi,可以视为厂商的DTS头文件,可以提取一些共性数据),之后会通过DTC编译成DTB,通过Bootloader传给kernel。
DTS基本上描述一个特定的硬件,DTSi则是包含可共享的硬件信息。DTB是编译之后的二进制文件。
下面是一个DTS文件的例子:
/dts-v1/;
/ {
compatible = "my_board";
model = "My Custom Board";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a7";
reg = <0>;
};
};
memory {
device_type = "memory";
reg = <0x80000000 0x4000000>; // Start address and size of the memory
};
uart@0 {
compatible = "ns16550a";
reg = <0x01c28000 0x20>; // Base address and size of the UART registers
interrupt-parent = <&intc>;
interrupts = <1>;
};
};
这里就描述这个设备的硬件信息,其中compatible 就是这个设备的硬件名称,用来在驱动中做匹配的。reg描述设备的寄存器地址和长度。第一个值是基地址,第二个值是寄存器空间大小。interrupts定义设备使用的中断号,interrupt-parent指定中断控制器。#address-cells 和 #size-cells 定义子节点地址和大小的单元数目。常用于总线节点,例如CPU节点。device_type 指定设备的类型,常用于CPU和内存节点。
总之,设备描述符的内容其实就是驱动中相关配置的提取。所以具体这些参数怎么定义?怎么填写,怎么用,都要看驱动里面是怎么搞的。
之后就是编译,生成二进制的dtb文件:
dtc -I dts -O dtb -o example.dtb example.dts
驱动里面怎么做的呢?
static const struct of_device_id my_uart_of_match[] = {
{ .compatible = "ns16550a" },
{ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);
static int my_uart_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// Initialize UART hardware and register the driver
return 0;
}
static struct platform_driver my_uart_driver = {
.probe = my_uart_probe,
.driver = {
.name = "my_uart",
.of_match_table = my_uart_of_match,
},
};
module_platform_driver(my_uart_driver);
可以看到,大致应该就是遍历匹配,DTS里面有compatible,驱动代码里面也有compatible。两个match上了就OK。之后继续处理其它的参数。
到这里,可以看出,DTS其实就是驱动的配置文件。。。
再说一下设备树的语法:
设备树分别属性和Node节点两种类型。
这个就是节点:
uart0: serial@40010000 {
compatible = "example,uart"; // 兼容性字符串
reg = <0x40010000 0x1000>; // 地址和大小
interrupts = <5>; // 中断号
status = "okay"; // 设备状态
};
节点的格式如下:
[lable:] node-name[@unit-address] {
[properties definitions];
[child nodes] ;
};
label是标号,用来引用的。必须全局唯一。而node-name是可以重复的。
比如这个&uart0 {status = "disable";};,就是修改了上面serial@40010000中属性的值。也可以用node-name[@unit-address] 来引用。
这个就是属性:
compatible = "example,uart";
属性中不同的类型,用的符号不同,比如“”是字符串,<>是数值。[]是字节序列。三者可以用逗号分隔。
在设备树中,还有一种机制就是overlay。这个鬼词在嵌入式真的用烂了,之前openwrt的文件系统也是overlay。
在这里是指的追加DTB。命令如下:
dtc -I dts -O dtb -o base.dtb base.dts
dtc -I dts -O dtb -o overlay.dtb overlay.dts
fdtoverlay -i base.dtb -o final.dtb overlay.dtb
最后解答一下以前写的一些疑问。
1 DTS写的驱动只是一个配置,那么真实的驱动在哪里?
代码中,device或者vendor中。内核中也有可能。
2 从上面的描述看Bootloader和Kernel是共用一个DTS,是怎么实现的?
Bootloader读取编译后的DTB,载入内存中。
之后通过命令传入这个地址给Kernel。bootz ${loadaddr} - ${fdtaddr}
3 DTS编写的规则。
这个好像没有统一的规则,就像每个程序的配置文件都不同,只有看每个驱动要什么。。。
4 增加一个常用设备,是什么流程?
基本上只用改DTS文件就可以。
5 增加一个全新设备,又是什么流程?
一般来说就是增加一套驱动,当然,可以再增加一套DTS,也可以不增加。这个取决于项目需求。
这篇真的写的比较全:
本文深入探讨设备树DTS在嵌入式领域的应用,解析DTS、DTSI及DTB的作用与生成流程,通过实例展示如何配置硬件信息,如设备寄存器地址、中断号等,以及驱动代码如何与DTS匹配,实现高效资源管理。
913

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



