4、设备树基础语法(三):reg属性的含义与用法,ranges属性实现地址映射,中断控制器与interrupt-parent
好,咱们接着聊设备树语法。前两章我们把基础骨架搭起来了,这一章要啃几个硬骨头——reg、ranges,还有中断相关的属性。这几个东西,说白了就是设备树里描述「硬件资源怎么分配、地址怎么转换、中断怎么连」的核心手段。
我刚开始学设备树时,最头疼的就是reg和ranges。明明都是地址,为什么有的写两个数,有的写四个?为什么父节点和子节点的地址范围不一样?嗯,别急,咱们一个一个拆开看。
4.1 reg属性:描述设备占用的地址空间
reg属性,全称是registers,但它描述的不只是寄存器。它描述的是设备在总线上占用的地址空间——包括寄存器、内存、IO端口等。
它的格式很简单:
reg = <address1 length1 address2 length2 ...>;
每个地址-长度对,描述一段连续的地址空间。地址和长度的位数,由父节点的#address-cells和#size-cells决定。
举个例子:
uart@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000>;
};
这里#address-cells = <1>,#size-cells = <1>。所以0x101f0000是起始地址,0x1000是长度。这个UART设备占用了从0x101f0000开始的4KB空间。
关键点:reg中的地址是相对于父节点的地址空间。如果父节点是根节点,那就是物理地址。如果父节点是桥或总线,那就是总线地址。
我在项目中遇到过一个问题:一个网卡设备,reg里写了三个地址段——控制寄存器、DMA描述符区、数据缓冲区。当时调试时发现DMA总是访问错误,后来才发现是#size-cells写错了,导致长度解析不对。嗯,这种错误很难查,因为编译不报错,运行才出问题。
4.2 多地址段的情况
有些设备有多个地址空间,比如一个LCD控制器,可能有控制寄存器区和帧缓冲区区:
lcdc@f8000000 {
compatible = "vendor,lcdc";
reg = <0xf8000000 0x1000 /* 控制寄存器 */
0xf8001000 0x800000>; /* 帧缓冲区 */
};
驱动里通过of_iomap()或platform_get_resource()按索引获取。索引0是控制寄存器,索引1是帧缓冲区。
个人习惯:我写设备树时,喜欢在reg后面加注释,标明每个段是什么用途。这样半年后自己回来看,不用再翻手册。
4.3 ranges属性:实现地址映射
ranges属性,是设备树里最容易让人懵圈的东西之一。它的作用,说白了就是「地址翻译」——把子节点的地址空间,映射到父节点的地址空间。
格式如下:
ranges = <child-bus-address parent-bus-address length>;
每个三元组表示:子总线上的child-bus-address,对应父总线上的parent-bus-address,映射长度为length。
举个例子,一个PCIe桥:
pcie@40000000 {
#address-cells = <3>;
#size-cells = <2>;
ranges = <0x02000000 0x00 0x40000000
0x00 0x40000000
0x00 0x20000000>;
};
这里子地址是PCIe的配置空间地址(0x02000000类型),父地址是CPU物理地址0x40000000,映射长度0x20000000(512MB)。
如果ranges属性为空(ranges;),表示子地址和父地址是1:1映射。如果ranges不存在,表示子地址空间和父地址空间没有直接映射关系——这种情况常见于非内存映射的设备,比如I2C总线上的设备。
我曾经踩过的坑:有一次写一个FPGA桥接设备,ranges写错了映射关系,导致驱动里读到的寄存器地址全是错的。查了两天才发现,是#address-cells和#size-cells的位数没对齐。记住:ranges里每个字段的位数,由父节点和子节点的#address-cells共同决定,千万别搞混。
4.4 中断控制器与interrupt-parent
中断是嵌入式系统里最常用的异步机制。设备树里描述中断,需要三个要素:中断控制器、中断连接关系、中断触发类型。
中断控制器:就是能接收中断信号并转发给CPU的硬件模块。比如GIC(通用中断控制器)、GPIO控制器等。它在设备树里用interrupt-controller属性标记:
gic: interrupt-controller@f0001000 {
compatible = "arm,gic-400";
interrupt-controller;
#interrupt-cells = <3>;
reg = <0xf0001000 0x1000>;
};
#interrupt-cells表示每个中断描述需要几个32位整数。GIC-400需要3个:中断类型(0=SPI,1=PPI)、中断号、触发类型(1=上升沿,4=高电平)。
interrupt-parent:指定设备连接到哪个中断控制器。如果整个系统只有一个中断控制器,可以在根节点设置interrupt-parent,所有子节点默认继承。
/ {
interrupt-parent = <&gic>;
uart@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000>;
interrupts = <0 33 4>; /* SPI 33, 高电平触发 */
};
};
如果设备连接到多个中断控制器,或者需要覆盖父节点的设置,就在设备节点里显式指定interrupt-parent。
核心逻辑:interrupts属性里的数值含义,完全由interrupt-parent指向的中断控制器的#interrupt-cells决定。不同中断控制器,同样的数值可能代表完全不同的中断。
4.5 中断级联与多级中断
实际系统中,经常有多个中断控制器级联。比如一个GPIO控制器作为二级中断控制器,挂在GIC上:
gpio2: gpio@f0002000 {
compatible = "vendor,gpio-controller";
reg = <0xf0002000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
interrupt-parent = <&gic>;
interrupts = <0 45 4>; /* GPIO控制器本身的中断 */
};
然后一个外设挂在GPIO2上:
keypad {
compatible = "vendor,keypad";
interrupt-parent = <&gpio2>;
interrupts = <5 2>; /* GPIO2的第5号引脚,下降沿触发 */
};
这里keypad的中断先到GPIO2,GPIO2再通过自己的中断线通知GIC。驱动里处理时,需要先处理GPIO2的中断,再找到具体是哪个引脚触发的。
我记得有一次调试一个触摸屏,中断一直不触发。查了半天,发现是interrupt-parent写错了,指向了GIC而不是GPIO控制器。内核在解析中断时,用GIC的#interrupt-cells去解析GPIO的中断描述,结果当然对不上。
4.6 本章知识体系
下面这张图,把本章的核心逻辑串起来了:
4.7 小结
这一章的内容,说实话是设备树里最绕的部分。我自己的经验是:先理解地址空间的概念,再理解中断的层级关系。不要试图一次性记住所有格式,而是记住「每个属性的含义由谁决定」这个核心原则。
你想想看,reg里的地址是几位,由父节点的#address-cells决定;interrupts里的数值怎么解释,由interrupt-parent指向的节点的#interrupt-cells决定。这个「子节点属性含义由父节点决定」的规则,贯穿了整个设备树的设计思想。
嗯,下一章我们会把这些属性组合起来,写一个完整的设备树实例。到时候你会发现,理解了这些基础语法,写设备树其实就是搭积木。
公众号:蓝海资料掘金营,微信deep3321