理解Device Tree Usage(续)

时间:2021-07-13 22:02:37
4 How Interrupts work
 
与遵循树的自然结构的地址范围转换不同, 中断信号可以起源于或者终止于板卡上的任何设备。 与设备树中自然表示的设备寻址不同,中断信号的表示独立于设备树节点之间的连接。通常用下面的四个属性来描述一个中断连接:
  • interrupt-controller - 一个空属性,声明一个接收中断信号的设备节点
  • #interrupt-cells -  这是中断控制器节点的一个属性。它声明中断控制器的 interrupt specifier(中断描述符)占用多少单元格(类似于#address-cells和#size-cells)。
  • interrupt-parent - 一种包含指向中断控制器句柄指针的属性;如果没有该属性,节点也可以从其父节点继承该属性
  • interrupts - 包含一系列的interrupt specifier的属性,每一个interrupt specifier表示设备发出的一个中断信号
一个interrupt specifier包含1个或多个单元格的数据(具体多少个单元格由“#interrupt-cells”属性决定), 它指定设备连接到哪个中断输入。大多数设备只有一个中断输出,如下面的例子所示,但是在一个设备上可以有多个中断输出。 interrupt specifier的含义完全取决于中断控制器设备的绑定。每个中断控制器可以决定需要多少单元格来唯一地定义一个中断输入。
接下来的代码,在我们的  Coyote's Revenge机器上增加了中断连接:
/dts-v1/;

/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>; cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
}; serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
}; serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
interrupts = < 2 0 >;
}; gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
interrupts = < 3 0 >;
}; intc: interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;
#interrupt-cells = <2>;
}; spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = < 4 0 >;
}; external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
interrupts = < 5 2 >;
}; i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = < 6 2 >;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
}; flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
需要注意的是:
  • 本机只有一个中断控制器:  interrupt-controller@10140000
  • “intc”标签, 已添加到中断控制器节点,该标签用于为根节点中的“interrupt-parent”属性分配一个phandle。这个“interrupt-parent”属性成为系统的默认值,因为所有子节点都会继承它,除非显式地重写它。
  • 每个设备使用属性“interrupts”来指定一条不同的中断信号输入线
  • “ #interrupt-cells”属性等于2,所以每个interrupt specifier有2个cells, 本例使用了一种常见的模式,即使用第一个单元格对中断行号进行编码,使用第二个单元格一些flags进行编码。比如高有效、低有效或者边缘有效与敏感程度。对于任何给定的中断控制器,请参阅控制器的对应文档,以了解specifier是如何编码的。
5 Device Specific Data
在通用的属性之外,还可以在节点里添加任意的属性和子节点。任何操作系统所需的数据都可以加进来,只要遵守以下的规则:
首先, 新的特定于设备的属性名应该有一个制造商名字作为前缀,这样它们就不会与现有的标准属性名冲突。
其次,必须在 binding 中记录属性和子节点的含义,以便设备驱动程序的作者知道如何解释数据。 binding 记录了特定兼容值的含义、它应该具有的属性、它可能具有的子节点以及它表示的设备。每个惟一的“ compatible”属性都应该有自己的 binding (或声明与另一个“ compatible”属性相兼容)。新设备的 binding 记录在此它的wiki中。有关文档格式和审查流程的描述,请参考网页(https://elinux.org/Main_Page)。
第三,在devicetree-discuss@lists.ozlabs.org邮件列表中发布新的binding以供审查。新的binding的代码审查能发现许多常见的错误,这些错误将来会导致问题。
 
6 Special Nodes
别名(aliases node)
特殊节点的表示通常涉及完整路径,比如/external-bus/ethernet@0,0。但是当用户真正想知道的是“哪个设备是eth0?”时,还是有些繁琐。这时就可以使用“aliases node”来代替完整设备路径。例如:
aliases {
ethernet0 = &eth0;
serial0 = &serial0;
};
在为设备分配标识符时,操作系统支持使用别名。
可以发现,在这里使用了一个新的语法。“属性= &label;”这个语法将标签引用的完整节点路径指定为字符串属性。这与文章前面出现的,phandle = < &label >不同(前面是把一个phandle插入到cell中);
chosen 节点
chosen节点不代表真实的设备,而是用作在固件和操作系统之间传递数据的地方,比如boot参数。所选节点中的数据不代表硬件。通常,所选节点在.dts源文件中为空,并在启动过程中时填充。
在我们的示例系统中,固件可能会将以下内容添加到chosen节点:
    chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
7 高级主题
高级示例机器
之前,我们已经学习了一些基础的定义。现在让我们在示例机器里增加一些新的硬件,以便讨论一些更复杂的用例。高级的示例主板里增加了一个PCI主桥,其控制寄存器映射到地址空间0x10180000,BARS空间从地址0x80000000开始。 根据我们对设备树的了解,我们可以开始添加以下节点来描述PCI主机桥接。
    pci@10180000 {
compatible = "arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
};

PCI Host Bridge

本节将描述 Host/PCI bridge node.
注意,理解本节需要有一些PCI的基本知识。但这不是一个关于PCI的教程,如果你需要更深入的信息,请阅读[1]。您还可以参考ePAPR v1.1,或者kernel的Documentation\devicetree\bindings目录(在那里可以找到飞思卡尔MPC5200的完整工作示例)

PCI Bus numbering

每个PCI总线都被唯一编号,总线编号通过使用bus-range属性在PCI节点中公开,该属性包含两个单元。第一个单元给出分配给这个节点的总线号,第二个单元给出任何从属PCI总线的最大总线号。
下面的例子只有一个简单的PCI总线,所以两个单元格全是0.
        pci@0x10180000 {
compatible = "arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
bus-ranges = <0 0>;
};

PCI Address Translation

与之前描述的本地总线类似,PCI地址空间与CPU的地址空间是完全分离的,所以需要有地址转换,将PCI地址空间转换到CPU的地址空间。像之前一样,转换的过程依赖于属性“  range,“”#address-cells,“和“#size-cells”。
        pci@0x10180000 {
compatible = "arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
bus-ranges = <0 0>;
#address-cells = <3>
#size-cells = <2>;
ranges = <0x42000000 0 0x80000000 0x80000000 0 0x20000000
0x02000000 0 0xa0000000 0xa0000000 0 0x10000000
0x01000000 0 0x00000000 0xb0000000 0 0x01000000>;
}
 
红色的PCI地址占用3个单元格。PCI地址范围占用2个单元格。为什么地址占用三个单元格?这是与PCI的 phys.hi, phys.mid ,phys.low相对应。
* phys.hi cell: npt000ss bbbbbbbb dddddfff rrrrrrrr
* phys.mid cell: hhhhhhhh hhhhhhhh hhhhhhhh hhhhhhhh
* phys.low cell: llllllll llllllll llllllll llllllll
 
PCI地址是64位宽,编码成为了三段:phys.hi, phys.mid ,phys.low。需要重点注意的是phys.hi地址段:
* n: relocatable region flag (doesn't play a role here)
* p: prefetchable (cacheable) region flag
* t: aliased address flag (doesn't play a role here)
* ss: space code
    * 00: configuration space
    * 01: I/O space
    * 10: 32 bit memory space
    * 11: 64 bit memory space
* bbbbbbbb: The PCI bus number. PCI may be structured hierarchically. So we may have PCI/PCI bridges which will define sub busses.
* ddddd: The device number, typically associated with IDSEL signal connections.
* fff: The function number. Used for multifunction PCI devices.
* rrrrrrrr: Register number; used for configuration cycles.
 
在PCI地址转换中,重要的字段是p和ss,   phys.hi 中的p和ss字段决定了哪个地址PCI地址空间正在被访问。 看一下range属性,我们有三个区域:
从PCI地址0x80000000开始的32位可预取内存区域,大小为512 MByte,将映射到主机CPU上的地址0x80000000。
从PCI地址0xa0000000开始的32位不可预取内存区域,大小为256 MByte,将映射到主机CPU上的地址0xa0000000。
从PCI地址0x00000000开始的一个I/O区域,其大小为16 MByte,将映射到主机CPU上的地址0xb0000000。
比较麻烦的是,phys.hi 的存在意味着, 操作系统需要知道节点表示PCI桥接器,以便在转换时忽略不相关的字段。 操作系统将在pci总线节点中查找字符串“pci”,以确定是否需要屏蔽额外字段。

PCI DMA Address Translation

上述“ranges”属性定义了CPU如何查看PCI内存,并帮助CPU设置正确的内存窗口,并将正确的参数写入各种PCI设备寄存器。这有时称为“ outbound memory”。
地址转换的一个特殊情况涉及PCI主设备如何查看系统的核心内存。当PCI控制器充当主设备并独立访问系统的核心内存时,就会发生这种情况。由于这通常是与CPU不同的视图(与内存线如何连接有关),因此可能需要在初始化时将其编程到PCI主控制器中。 这被看作是一种DMA,因为PCI总线独立地执行直接内存访问,因此这些映射被命名为DMA -range。这种类型的内存映射有时称为“ inbound memory”,不属于PCI设备树规范的一部分。
在某些情况下,ROM (BIOS)或类似的引导程序将在引导时设置这些寄存器,但在其他情况下,PCI控制器完全未初始化,需要从设备树中读取信息,并完成这些转换。然后,PCI主机驱动程序通常将解析dma-ranges属性,并相应地在主机控制器中设置一些寄存器。
继续扩展上面的例子:
pci@0x10180000 {
compatible = "arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
bus-ranges = <0 0>; #address-cells = <3>
#size-cells = <2>;
ranges = <0x42000000 0 0x80000000 0x80000000 0 0x20000000
0x02000000 0 0xa0000000 0xa0000000 0 0x10000000
0x01000000 0 0x00000000 0xb0000000 0 0x01000000
dma-ranges = <0x02000000 0 0x00000000 0x80000000 0 0x20000000>;
};
“dma-ranges”表示,从PCI主机控制器的角度看,从PCI-E地址0x00000000开始的512MB内存将出现在CPU core地址的0x80000000.你也能看到,“ss”地址被设置为0x02来表示它是一个32位的内存空间。

Advanced Interrupt Mapping

现在我们开始更有趣的部分,PCI中断映射。一个PCI设备可以触发中断,通过信号线#INTA,#INTB,#INTC,#INTD。在中断信号名字前面的“#”表示这个中断时低有效(这是一个常见的约定)。PCI的中断信号一直是低有效。 单功能设备必须使用#INTA来中断。多功能设备必须使用#INTA如果它使用一个中断引脚,#INTA和#INTB如果它使用两个中断引脚,等等。 由于这些规则,#INTA通常比#INTB、#INTC和#INTD被更多的函数使用。 为了将负载分配到支持#INTA到#INTD的四条IRQ线路上,PCI插槽或者设备通过rotate的方式连接到中断控制器的不同信号线上,以避免所有的#INTA都连接到相同的信号线上。 这个过程称为对中断进行swizzling。 因此,设备树需要一种方法将每个PCI中断信号映射到中断控制器的输入。“ #interrupt-cells”,“ interrupt-map”,“ interrupt-map-mask”属性被用来描述中断信号的映射。
实际上,这里描述的中断映射并不局限于PCI总线,任何节点都可以指定复杂的中断映射,但是到目前为止,PCI用例是最常见的。
 pci@0x10180000 {
compatible = "arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
bus-ranges = <0 0>;
#address-cells = <3>
#size-cells = <2>;
ranges = <0x42000000 0 0x80000000 0x80000000 0 0x20000000
0x02000000 0 0xa0000000 0xa0000000 0 0x10000000
0x01000000 0 0x00000000 0xb0000000 0 0x01000000>;
#interrupt-cells = <1>;
interrupt-map-mask = <0xf800 0 0 7>;
interrupt-map = <0xc000 0 0 1 &intc 9 3 // 1st slot
0xc000 0 0 2 &intc 10 3
0xc000 0 0 3 &intc 11 3
0xc000 0 0 4 &intc 12 3
0xc800 0 0 1 &intc 10 3 // 2nd slot
0xc800 0 0 2 &intc 11 3
0xc800 0 0 3 &intc 12 3
0xc800 0 0 4 &intc 9 3>;
};
首先,你会注意到,PCI的中断号只占用1个单元格,不像系统其他的中断控制器使用2个单元格;一个对应中断信号线的序号,一个对应flag。PCI只需要一个单元格来表示中断,是因为PCI中断都是低有效。
在我们的示例板卡中,我们有2个PCI插槽,每一个有4个中断线。所以我们需要映射8个中断信号线到中断控制器中。这是通过“interrupt-map”属性来完成的。
由于中断号(例如#INTA)并不足以区分是PCI总线上哪个设备触发的中断。我们也需要表示出来是哪个PCI设备触发的中断。 幸运的是,每个PCI设备都有一个我们可以使用的唯一设备号。为了区分多个PCI设备的中断,我们需要一个由PCI设备号和PCI中断号组成的元组。 更进一步,我们构造了一个单元中断说明符,它有四个单元:
3个单元格表示地址,包含phys.hi,phys.mid,phys.low
1个单元格表示中断号,(#INTA、#INTB、#INTC、#INTD)
因为我们只需要PCI地址的设备号部分,中断映射掩码属性就发挥作用了。“ interrupt-map-mask”也是一个由四部分组成的元组,像“ unit interrupt specifier”一样。掩码的第一段表示“ unit interrupt specifier”的哪一部分是有用的。 在我们的示例中,我们可以看到只有phys的设备编号只有phys.hi是必需的,我们需要3位来区分这4个中断。 现在我们可以构造中断映射属性。此属性是一个表,该表中的每个条目由子(PCI总线)单元中断说明符、父句柄(负责服务中断的中断控制器)和父单元中断说明符组成。因此,在第一行中,我们可以读到PCI中断#INTA被映射到irq9上,这是我们中断控制器的低灵敏度级别。
目前唯一缺少的部分是PCI总线单元中断描述符中的数字。单元中断描述符的重要部分是来自“  phys.hi ”的设备编号。而设备编号是板卡相关的,不同的板卡的设备编号可能不同,它取决于每个PCI主机控制器如何激活每个设备上的IDSEL pin。在本例中,为PCI插槽1分配设备id为24 (0x18),为PCI插槽2分配设备id为25 (0x19)。每个PCI插槽的“ phys.hi”取值,是通过将设备号左移11位,得到ddddd部分来确定的,如下所示:
* phys.hi for slot 1 is 0xC000, and
* phys.hi for slot 2 is 0xC800.
 
slot1的0x18左移11位,就是0xC000;slot2的0x19左移11位,就是0xC800。
综合一起考虑,中断映射表如下:
插槽1的INTA为IRQ9,低有效
插槽1的INTB为IRQ10,低有效
插槽1的INTC为IRQ11,低有效
插槽1的INTD为IRQ12,低有效
以及
插槽2的INTA为IRQ10,低有效
插槽2的INTB为IRQ11,低有效
插槽2的INTC为IRQ12,低有效
插槽2的INTD为IRQ9,低有效
 
属性“ interrupts = <8 0>;”表示PCI中断控制器本身可能触发的中断,不要与PCI设备触发的中断相混淆  ( INTA, INTB, ...)。
 
最后要注意的一点是,就像“ interrupt-parent”属性一样,节点上存在“interrupt-map”属性将更改所有子节点和子节点的默认中断控制器。在本PCI示例中,这意味着PCI主机桥接器成为默认中断控制器。如果通过PCI总线连接的设备与另一个中断控制器有直接连接,那么它还需要指定自己的“ interrupt-parent”属性。