Intel® SoC FPGA Embedded Development Suite
Support for SoC FPGA Software Development, SoC FPGA HPS Architecture, HPS SoC Boot and Configuration, Operating Systems

Investigating Device Trees

symmt_Intel
Employee
89 Views

 

Note: This article is an English translation of this Japanese article. Please refer to the original article for updates.

 

1. Introduction

While operating an FPGA with an embedded ARM CPU on Linux, I encountered the need to modify the device tree. Therefore, I will write about what I have researched (and understood in my own way) to roughly understand the content of the device tree source (.dts) included in the Kernel source. I hope that those who see the .dts files in the Kernel source and think "what is this?" can somewhat understand and feel capable of making additional modifications.

2. What is a Device Tree?

2.1 Overview

A device tree can be described as a data structure (file format) that describes hardware components accessible from the OS (CPU) from the perspective of software (device drivers). This format was established by a group called Open Firmware (different from the Linux development group).

2.2 Usage in Linux

Linux also uses this format, separating the Kernel code from board-specific information (i.e., the device tree). This allows the same Kernel binary to operate on different boards, as long as the appropriate device tree for the board is provided. The Kernel source contains device tree source (.dts) files for major supported boards, and device tree binaries can be generated using the Makefile system (e.g., `make dtbs`) similarly to building the Kernel binary (e.g., `make zImage`). As of May 2020, the Linux kernel source tree on Kernel.org had over 1200 .dts files for 32-bit ARM CPUs. During initialization, each device driver in the Kernel code searches for the information it needs from the device tree and completes device registration and initialization accordingly. Therefore, if the information required by the Kernel code is not described in the device tree, the Kernel may not boot. As a principle, the Kernel binary and device tree should be created from the same version of the Kernel source.

2.3 Types of Device Tree Files

Device tree sources written in human-readable text format have the .dts extension. The binary form of these files is called a device tree blob, with the .dtb extension. The Kernel reads binary device tree blobs (.dtb). Conversion from device tree source to device tree blob is done using the device tree compiler (dtc command). The device tree compiler also supports reverse conversion from .dtb to .dts, which can be useful when you want to view the content of a binary .dtb.

3. Understanding Device Trees

To understand device trees in Linux, it is helpful to divide the understanding into two parts:

  • Device tree format (how to write it)
  • What information needs to be included in the device tree (what information Linux device drivers read from the device tree)

Here, we will focus on "how to write a device tree (format)." The necessary information in a device tree depends on what information the Linux device drivers need. At the end of this article, we will briefly introduce this aspect.

3.1 Overview of the dts File Format

To put it simply, a dts file is:

  • A list of hardware (device nodes) to be accessed by the CPU, described as a tree-structured database
  • Comprised of only two types of components: node and property
  • Nodes can have various attribute information properties and child nodes
  • Properties are expressed as combinations of attribute names and values, used to describe hardware information such as address and interrupt numbers

For more detailed information, please refer to the following link: https://elinux.org/Device_Tree_Usage

3.2 Nodes

    [label:] node-name[@unit-address] {
        [properties definitions]
        [child nodes]
    }
    
  • Devices are represented as nodes
  • Nodes can have properties and child nodes
  • Nodes are defined with the format node-name @ unit-address, and their content is defined within curly braces { }
  • The unit-address part is written in hexadecimal and specified by the reg property. If there are multiple devices with the same node name, they can be distinguished by unit-address. Nodes without a reg property do not need a unit-address. Linux's dtc allows compilation even without a unit-address (a warning will be issued)
  • Node names are simple ASCII strings, with a maximum length of 31 characters. Names representing the function of the device (e.g., ethernet, serial, gpio) are recommended
  • The top-level (root) node has no name, only "/". `/ { ... };`
  • The tree structure of nodes (parent node - child node relationship) should represent the connection state of devices as seen from the CPU (e.g., define Bus bridge as the parent and SlaveDevice connected to the Bus bridge as child nodes)
  • If a label is attached, the node's phandle (like a pointer to another node) can be specified as `&label`. If compiled with the -@ option, it can be referenced from another file (overlay file). (See the explanation of the phandle property below)

3.3 Properties

  • Properties are combinations of property-name and value
  • Properties can also be defined without values
    [label:] property-name = value;
    [label:] property-name;
    

3.3.1 Types of Values Used in Properties

  • Text String: Enclosed in double quotes. Example: compatible = "arm,cortex-a9";
  • Array/Cell-list: A set of 32-bit unsigned integers enclosed in angle brackets <>. Example: reg = <0xffd04000 0x1000>;
  • Binary Data: Enclosed in square brackets []. Example: mac-address = [12 34 56 ab cd ef];
  • Multiple Data: Can be concatenated using commas. Examples:
    • reg = <0xff900000 0x100000>, <0xffb80000 0x10000>;
    • mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;

3.3.2 Standard Properties Commonly Used in Many Nodes

  • compatible
    • The most important property used to associate with the device driver. Devices requiring a device driver need this property
    • The initialization routine of the device driver that has a matching compatible string with this value is called, and the device is registered
    • A list of text strings in the format "manufacturer,model". Example: compatible = "altr,socfpga-stmmac", "snps,dwmac-3.70a", "snps,dwmac";
  • status
    • Indicates whether the device is enabled or not
    • Value is "okay" or "disabled"
  • phandle
    • Sets the ID number indicating the node as a Cell-list
    • Usually omitted in human-written .dts
    • If omitted, the dtc compiler automatically generates a unique ID and adds the phandle to the .dtb
    • The phandle of a node with label "XXX" can be specified as <&XXX>. Often used in the description of the interrupt property
  • reg
    • Address information for the device. Specified as a pair of (address-cells, size-cells)
    • Address-cells specifies the base address, and size-cells specifies the memory size in bytes
    • The number of address-cells and size-cells is determined by the values of #address-cells and #size-cells in the parent node
  • #address-cells, #size-cells
    • Defined in nodes with child nodes. Specifies how the child nodes are addressed. Determines the addressing method in the ranges and reg properties of the child hierarchy
    • #address-cells specifies the number of address cells in the reg property of the child node, usually 1. If there are multiple bus masters, it may be 2 (specified by bus number and addr). 64-bit address is 2
    • #size-cells specifies the number of size cells in the reg property of the child node, usually 1. If not memory mapped, 0
  • ranges
    • Defined in nodes representing a bus, specifies how address translation is performed between the node and its child nodes
    • Specified as a combination of
    • If no value is provided, it means the parent and child nodes are in the same address space

3.4 Example of #address-cells and ranges Usage

/ {
    ...
    sopc0: sopc@0 {
        ranges;
/*
The relationship between sopc@0 (parent) and bridge@0xc0000000 (child)
sopc@0 and bridge@0xc0000000 share the same memory space (no address translation).
*/
        #address-cells = <1>;
        #size-cells = <1>;
        ...
        hps_0_bridges: bridge@0xc0000000 {
            reg = <0xc0000000 0x20000000>,
                 <0xff200000 0x00200000>;
/*
bridge@0xc0000000 has two memory areas: 
0xc0000000 with size 0x20000000 and 
0xff200000 with size 0x00200000
*/
            #address-cells = <2>;
            #size-cells = <1>;
/*
The child nodes of bridge@0xc0000000 are expressed with 2 address cells and 1 size cell.
*/
            ranges = <0x00000000 0x00000000 0xc0000000 0x00010000>,
                     <0x00000001 0x00020000 0xff220000 0x00000008>,
                     <0x00000001 0x00010040 0xff210040 0x00000020>;
/*
Memory area from 0xc0000000 with size 0x00010000 is assigned from child node address <0x00000000 0x00000000> with size 0x00010000,
Memory area from 0xff220000 with size 0x00000008 is assigned from child node address <0x00000001 0x00020000> with size 0x00000008,
Memory area from 0xff210040 with size 0x00000020 is assigned from child node address <0x00000001 0x00010040> with size 0x00000020
(No other areas are assigned to child nodes).
*/
            ...
            led_pio: gpio@0x100010040 {
                compatible = "altr,pio-16.0", "altr,pio-1.0";
                reg = <0x00000001 0x00010040 0x00000020>;
/*
gpio@0x10001004 has a memory area from <0x00000001 0x00010040> with size 0x00000020,
which maps to the memory area from 0xff210040 with size 0x00000020 in the parent node.
(CPU can access it within the range 0xff210040-60).
*/
                ...
            }; // end gpio@0x100010040 (led_pio)
            ...
        }; // end bridge@0xc0000000 (hps_0_bridges)
    };
};
    
  • interrupt-controller
    • Has no value. Indicates the device is an interrupt controller
  • #interrupt-cells
    • A property of the interrupt controller node. Specifies the number of cells in the interrupt property. Defined by the device driver of the interrupt controller (e.g., interrupt number, active-high/active-low/posedge/negedge). Refer to the controller's documentation. More explanation in the section "4. What information to write in the DeviceTree"
  • interrupt-parent
    • A property of nodes that output interrupts. Sets the phandle of the connected interrupt controller node. If not present, the value of the parent node is used
  • interrupts
    • A property of nodes that output interrupts. Specifies the attributes of the output interrupt signal. Set the values defined by the #interrupt-cells of the connected interrupt-controller

3.5 Additional Notes

  • The first line is /dts-v1/;
  • Comments can be in C style (/* ... */) or C++ style (// ...)
  • If the same node name appears at the same level, it is considered the same node and additional definitions are made to that node. Redefinition of properties is also possible (the later definition is used)
  • When compiling with the Kernel Makefile, it is first processed by the gcc preprocessor, so #include and #define can be used

4. What information to write in the DeviceTree

To understand this point, it is best to read the documentation of the Kernel Source. As an example, let's look at the description of a node with interrupts.

intc: intc@fffed000 {
    compatible = "arm,cortex-a9-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    ...
};
soc {
    interrupt-parent = <&intc>;
    ...
    i2c0: i2c@ffc04000 {
        ...
        interrupts = <0 158 0x4>;
        ...
    };
};
    

In the code above, the interrupts property of i2c@ffc04000 consists of three cells. To understand what these three values should be, search for the interrupt controller's compatible string "arm,cortex-a9-gic" in the Kernel Source's Documentation/devicetree/bindings directory (e.g., `find Documentation/devicetree/bindings -name "*.txt" | xargs grep "arm,cortex-a9-gic"` or if you have the Kernel Source with git, `git grep "arm,cortex-a9-gic" -- Documentation/devicetree/bindings`). You will find a file named Documentation/devicetree/bindings/interrupt-controller/arm,gic.txt. According to this file, #interrupt-cells is 3, and the content of each cell is as follows:

  • The first cell is the interrupt type: 0 for Shared Peripheral Interrupt (SPI), 1 for Private Peripheral Interrupt (PPI)
  • The second cell is the interrupt number: SPI ranges from [0-987], PPI ranges from [0-5]
  • The third cell is a flag with the following meanings:
    • 1 = low-to-high edge triggered
    • 2 = high-to-low edge triggered (invalid for SPIs)
    • 4 = active high level-sensitive
    • 8 = active low level-sensitive (invalid for SPIs)

The value <0 158 4> means:

  • Shared Peripheral Interrupt
  • SPI interrupt number 158
  • Active high level-sensitive interrupt

By the way, according to the specification of the SoC described by this .dts, the Shared Peripheral Interrupt numbers start from 32 in the GIC interrupt controller, so the value written in the .dts subtracts 32 from the interrupt number in the SoC specification (the SoC specification states that i2c@ffc04000 is interrupt number 190). Probably, for SoCs using ARM GIC, the interrupt number in the .dts will be commonly written with 32 subtracted.

For those who want to know more about how to write .dts overall, reading the Documentation/devicetree/booting-without-of.txt in the Linux Source's Documentation folder is recommended. It describes essential nodes such as:

  • root node
  • /cpus node
  • /cpus/* nodes
  • /memory node(s)
  • /chosen node
  • /soc<SOCname> node

and more, in detail.

That's all for now.

5. References

0 Kudos
0 Replies
Reply