Skip to main content

Device Tree Customization

Introduction​

The goal of this article is to provide information and guidance to developers on how to customize Device Trees for Toradex system on modules, specifically for custom carrier boards that may require modification of the default device tree, explaining the basics of device trees, their anatomy and the steps involved in modifying and compiling them.

Why Customize Device Trees​

A Device Tree is a data structure that describes a system's hardware, including the SoC's internal memory-mapped peripherals, external hardware and the whole carrier board. The ARM Linux Kernel uses device trees as the preferred format of hardware description, which became necessary as more ARM SoC vendors and boards appeared. Toradex, therefore, creates device trees for each module and carrier board. Custom carrier boards and some peripherals may require modifications to the device tree.

Understanding Device Trees​

To make an operating system portable across different devices, a description of the layout of each supported hardware configuration is required to ensure the correct drivers and configurations are used. The ARM world is very heterogeneous, each SoC vendor and each board vendor wire their hardware a bit differently. In some other architectures (e.g. x86), there is a standard interface between the board firmware (BIOS) and the operating system to communicate the hardware layout - for instance, ACPI on x86. To overcome this lack of hardware description, the ARM Linux Kernel uses device trees as the preferred format of hardware description beginning around kernel version ~3.2. Prior to this change, all details of how the hardware was wired were part of the platform/machine layer and hard-coded in C structs. This became complicated as more and more ARM SoC vendors and boards appeared.

The device-tree is not only a data structure that describes the SoC's internal memory-mapped peripherals, but it also allows us to describe the whole board. Typically, it is defined at multiple levels and composed of multiple device tree files. Device tree files (dts and dtsi) may include other device tree files known as includable device tree files (dtsi). In this manner, a board-level device tree file (dts) generally includes a SoC level device tree file (dtsi). To support the modular approach of Toradex products, our device tree files usually have three levels of inclusion: carrier board, module, and SoC. This is also reflected in the device tree file names, which are composed of the three levels: ${soc}-${module}-${board}.dtb.

The Linux kernel needs device tree binaries (*.dtb) to boot. These binaries are generated by the device tree compiler from the device tree source files. The compiler is part of the Linux sources and is automatically built if needed. The kernel build system provides the dtbs target which compiles all device trees which are compatible with the current kernel configuration.

Structure​

A device tree is a hierarchical structure that describes the hardware configuration of a system. The structure consists of nodes that represent various hardware components. Each node contains properties that describe the configuration of the hardware component.

Each supported hardware device has a compatible string. Along with the compatible property, the device-specific properties need to be specified. These properties are specified in the device tree bindings. The most important properties are compatible, reg, clocks, interrupts, and status. A memory-mapped device (UART in this case) looks like this:

Device Tree anatomy

Nodes can be referenced using the ampersand (&) character and the label.

The device tree structure is defined using a specialized language called Device Tree Source (DTS). The DTS file is compiled into a binary format called Device Tree Blob (DTB) that is loaded by the bootloader.

Compiling the Device Tree​

After making changes to the device tree, it needs to be compiled into a DTB file that can be loaded by the bootloader. This can be done using the Device Tree Compiler (DTC) tool.

The DTC tool is included in the Toradex Linux BSP and can be used to compile the device tree. The compiled DTB file should be placed in the boot partition of the storage device.

Customizing Nodes and Properties​

Choosing the Base Device Tree​

The first step in customizing the device tree is to choose the base device tree that matches the hardware platform. Toradex provides a set of base device trees for its hardware platforms that can be found in linux-toradex.git, if you are working with a downstream-based kernel, or linux.git, if upstream-based.

  1. Find a device tree layout of the module you are using and the respective carrier board level device tree for you to base on.

  2. Create your custom .dts based on the previously selected device tree. You can do this by copying the base device tree and editing the custom one. For example:

    $ cp ./arch/arm/boot/dts/imx6ull-colibri-eval-v3.dts ./arch/arm/boot/dts/imx6ull-colibri-eval-custom.dts
  3. Edit the Makefile and insert your custom device tree under the right dtb-$(CONFIG_SOC_*). For example:

    arch/arm/boot/dts/Makefile
    dtb-$(CONFIG_SOC_IMX6UL) += \
    ...
    imx6ull-colibri-aster.dtb \
    imx6ull-colibri-emmc-aster.dtb \
    imx6ull-colibri-emmc-eval-v3.dtb \
    imx6ull-colibri-eval-custom.dtb \
    ...
  4. Run the make dtbs to compile this new device tree binary.

    tip

    The kernel build system writes the combined device tree to the drive, e.g. arch/arm/boot/dts/imx6ull-colibri-eval-custom.dtb.dts.tmp. The combined dts file can be handy to debug what the actual device tree file will look like. The combined file is ultimately compiled into the device tree binary representation (*.dtb) which is used by the kernel to boot.

Overwriting Nodes and Properties​

Typically the higher layers (e.g. carrier board device tree) overwrite the lower layers (e.g. SoC device tree) since the higher layers include the lower layers at the very beginning (the sequence order of entries is what matters, hence the include order matters). Entire nodes can be overwritten by simply redefining them. The node needs to be referenced using the ampersand & character and the label, and its properties changed under the node.

For example, to overwrite the pin configuration of Colibri iMX6ULL ADC1, overwrite the adc1grp node by simply redefining it in your device tree.

&iomuxc {
pinctrl_adc1: adc1grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 0x3000 /* SODIMM 8 */
MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0x3000 /* SODIMM 6 */
MX6UL_PAD_GPIO1_IO08__GPIO1_IO08 0x3000 /* SODIMM 4 */
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x3000 /* SODIMM 2 */
>;
};

Activating/Deactivating Devices​

An important device attribute is the status property. It allows devices to be activated/deactivated. A lot of devices are specified in the SoC level device trees but are disabled by default. By referencing the base node (using the ampersand character and the label), the device can be enabled by any of the layers overwriting the status property.

&i2c2 {
status = "okay";
};

Delete Properties or Nodes​

It is also possible to delete properties or even nodes using /delete-property/ or /delete-node/. For example:

imx6q-apalis-ixora-v1.1.dts
...
/delete-node/ &reg_can1_supply;
...
&can1 {
/delete-property/ xceiver-supply;
};

Aliases​

The device tree allows some device types to be rearranged using aliases. This is useful for RTCs, for instance, since the first RTC device is used as the primary time source for the system.

imx7-colibri.dtsi
aliases {
rtc0 = &rtc;
rtc1 = &snvs_rtc;
};

However, it can be used for other kinds of device types, such as ethernet

imx6ull.dtsi
aliases {
ethernet0 = &fec2;
ethernet1 = &fec1;
};

Referencing Nodes​

If resources of another device are required, a reference is used to connect the two devices. Typically this is used to assign resources such as interrupts, clocks, GPIOs or PWM channels to a device. Depending on the referenced device, a specific amount of parameters (cells) are necessary. The amount is defined in the -cells property of the parent device.

GPIO​

A GPIO specification needs a reference to a GPIO node and one or more cells (arguments). The amount of cells is driver specific. It can be obtained from the device tree binding documentation or by looking at the GPIO controller node (a device which exports GPIO is marked with the gpio-controller property). The #gpio-cells property defines how many cells are expected. For example:

imx6ul.dtsi
gpio1: gpio@209c000 {
...
gpio-controller;
#gpio-cells = <2>;
...
};

So, this means that the GPIO need to be referenced using two cells.

    node {
enable-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
};

This example assigns a single GPIO from the GPIO controller with the label gpio1 (referenced using the ampersand character &), and passes it the two cells with a value of 18 and GPIO_ACTIVE_HIGH. The meaning/order of the cells depends on the parent device type. The parent device's device tree binding documentation should contain more information on that. For more information, refer to Documentation/devicetree/bindings/gpio/gpio.txt.

Interrupt​

The interrupt controller also specifies the number of cells required. The SoC internal interrupts are already assigned to the peripherals in the SoC level device tree, hence those most often do not need further handling. For external devices often GPIOs are used as an interrupt source. To make GPIOs available as interrupt sources, the GPIO controllers node is also annotated with the interrupt-controller property:

imx6ul.dtsi
gpio1: gpio@209c000 {
...
interrupt-controller;
#interrupt-cells = <2>;
...
};

Interrupts can be assigned in a similar fashion, however, instead of using the linked parent as part of the interrupt specification, the interrupt-parent property needs to be used:

    interrupt-parent = <&gpio1>;
interrupts = <10 IRQ_TYPE_LEVEL_HIGH>;

This example assigns GPIO 10 of the GPIO bank represented by gpio1 as the interrupt of a device.

Pinmuxing​

Almost all peripherals need signals multiplexed to external pins in order to operate. The exact workings of pinctrl (muxing, pin configuration etc.) vary quite a bit between different SoC vendors; therefore, device tree bindings are not standardized across our modules. Refer to the module-specific sections below for how to define a pinctrl block for the module you are using.

However, assigning pins to a driver works with standardized bindings. Each pinctrl subnode needs to be assigned to a driver, otherwise, the pinctrl won't apply the settings on its own. How and how many pinctrl groups can be assigned to a device depends on the device driver used. Most drivers are documented in the kernel source under Documentation/devicetree/bindings/.

The default assignment can be made using these properties:

  • pinctrl-0, pinctrl-1, ..., pinctrl-n: List of phandles, each pointing at a pin configuration node within a pin controller. These referenced pin configuration nodes must be child nodes of the pin controller that they configure.
  • pinctrl-names: The list of names to assign states.

Here follows an example:

device {
pinctrl-names = "active", "idle";
pinctrl-0 = <&state_0_node_a>;
pinctrl-1 = <&state_1_node_a>, <&state_1_node_b>;
};

To verify that a certain pinctrl has been picked up by the driver and correctly applied, the debug information available via sysfs can be helpful:

# cat /sys/kernel/debug/pinctrl/pinctrl-handles

i.MX 6 Based Modules​

Pin configuration such as pinmux or drive strength is either set by pinctrl-imx6dl driver, for Colibri iMX6S or Colibri iMX6DL, or the pinctrl-imx6q driver, for Apalis iMX6Q or Apalis iMX6D. The SoC level device trees define the base configuration and allow to extend entries through the iomuxc label.

To configure a pin, a device tree node inside the pin controller node with the property fsl,pins is required. Cells need to be assigned to the property, each pin requires 5 cells. However, the first four are usually given by a pre-processor macro (see arch/arm/boot/dts/imx6dl-pinfunc.h or imx6q-pinfunc.h). The macros consist of three parts, a prefix, the pad (or ball) name (as used in datasheets), and the alternate function name. Since each pad has multiple alternate functions, there are multiple macros for a single pad, all ending with a different alternate function. It is crucial to select the correct macro for the intended use (e.g. the macro which contains GPIO as an alternate function if the pad is going to be used as a GPIO).

MX6QDL_PAD_EIM_A24__GPIO5_IO04

Prefix: MX6QDL_PAD
Pad/ball name: EIM_A24
Alternate function: GPIO5_IO04

The 5th and last cell of a pin muxing entry need to be provided as a number in the device tree. This last cell contains the pin settings typically in a hexadecimal notation. Additionally, the last cell's bit 30 is used to give the setting of the SION bit, bit 31 prevents the iomuxc from changing the pad control register.

pinctrl_csi_gpio_2: csigpio2grp {
fsl,pins = <
MX6QDL_PAD_EIM_A24__GPIO5_IO04 0x1b0b0
>;
};

There are preprocessor definitions for commonly used pin configurations (e.g. PAD_CTRL_HYS_PU). The bitwise definition for the last cell is given by the registers of the i.MX 6 Input/Output Multiplexer Controller. For further details see Chapter 4 of the Toradex Colibri iMX6 datasheet or Apalis iMX6 datasheet, the NXP® i.MX 6Solo/6DualLite Applications Processor Reference Manual and/or NXP® i.MX 6Dual/6Quad Applications Processor Reference Manual .

i.MX 6ULL Based Modules​

The i.MX 6ULL SoC allows multiplexing pins through its Input/Output Multiplexer Controller (IOMUXC). Besides multiplexing pins, this controller also allows setting pin configurations such as drive strength. There are two largely independent controllers: the IOMUXC and the IOMUXC LPSR (low-power pin controller). The SoC level device trees define the driver node for each of these controllers which bind to the pinctrl-imx6ul driver and defines the labels iomuxc and iomuxc_lpsr to give lower-level device tree access to the node.

To configure a pin, a device tree node inside the pin controller node with the property fsl,pins is required. Cells need to be assigned to the property, each pin requires 5 cells. However, the first four are usually given by a pre-processor macro (see arch/arm/boot/dts/imx6ull-pinfunc.h or arch/arm/boot/dts/imx6ull-pinfunc-lpsr.h), only the last cell needs to be provided. This last cell contains the pin settings in a hexadecimal notation. Additionally, the last cell's bit 30 is used to give the setting of the SION bit, bit 31 prevents the iomuxc from changing the pad control register. Since i.MX 6ULL SoC shares pin assignments with the i.MX 6UL SoC, various pin assignments for both processors are in the file arch/arm/boot/dts/imx6ul-pinfunc.h, which is included in arch/arm/boot/dts/imx6ull-pinfunc.h.

&iomuxc {
...
pinctrl_can_int: canintgrp {
fsl,pins = <
MX6UL_PAD_ENET1_TX_DATA1__GPIO2_IO04 0x13010 /* SODIMM 73 */
>;
};
...
};

The bitwise definition for the last cell is given by the PAD Control Registers of the i.MX 6ULL Input/Output Multiplexer Controller. For further details see Chapter 4 of the Colibri iMX6ULL datasheet or/and the NXP®/Freescale i.MX 6ULL application processor reference manual.

i.MX 7 Based Modules​

The i.MX 7 SoC allows multiplexing pins through its Input/Output Multiplexer Controller (IOMUXC). Besides multiplexing pins, this controller also allows setting pin configurations such as drive strength. There are two largely independent controllers: the IOMUXC and the IOMUXC LPSR (low-power pin controller). The SoC level device trees define the driver node for each of these controllers which bind to the pinctrl-imx7 driver and defines the labels iomuxc and iomuxc_lpsr to give lower-level device tree access to the node.

To configure a pin, a device tree node inside the pin controller node with the property fsl,pins is required. Cells need to be assigned to the property, each pin requires 5 cells. However, the first four are usually given by a pre-processor macro (see arch/arm/boot/dts/imx7d-pinfunc.h or arch/arm/boot/dts/imx7d-pinfunc-lpsr.h), only the last cell needs to be provided. This last cell contains the pin settings in a hexadecimal notation. Additionally, the last cell's bit 30 is used to give the setting of the SION bit, bit 31 prevents the iomuxc from changing the pad control register.

&iomuxc {
...
pinctrl_can_int: canintgrp {
fsl,pins = <
MX7D_PAD_SD1_RESET_B__GPIO5_IO2 0x14 /* SODIMM 73 */
>;
};
...
};

The bitwise definition for the last cell is given by the PAD Control Registers of the i.MX 7 Input/Output Multiplexer Controller. For further details see Chapter 4 of the Colibri iMX7 datasheet or/and the NXP®/Freescale i.MX 7 application processor reference manuals: i.MX 7Dual Applications Processor Reference Manual and i.MX 7Solo Applications Processor Reference Manual.

i.MX 8/8X Based Modules​

The i.MX 8/8X based System on Chips (SoCs) enables the multiplexing of pins through its Input/Output Multiplexer Controller (IOMUXC). The SoC level device trees define the driver node for this controller. All pin muxing and drive strength configurations are managed in the System Controller Unit, which is a dedicated M-Core that runs the System Controller Firmware (SCFW). The Linux IOMUXC driver cannot directly access the IOMUXC registers but communicates with its firmware (SCFW) through the SCU driver.

To configure a pin, a device tree node is required inside the pin controller node with the property fsl,pins. Each pin requires three integers to be assigned to the property. The first two integers are typically given by preprocessor definitions found in:

  • include/dt-bindings/pinctrl/pads-imx8qm.h
  • include/dt-bindings/pinctrl/pads-imx8qxp.h
  • include/dt-bindings/pinctrl/pads-imx8dxl.h

These #define statements specify the SoC pin and its desired function. The third integer defines the pin settings in hexadecimal notation. In the examples provided, the pin FLEXCAN0_TX is muxed to the function ADMA_FLEXCAN0_TX, for i.MX 8X, and ADC_IN0 to ADC0_IN0, for i.MX 8.

info

The meaning of these bits depends on the pin and has to be checked for every pin in the Reference Manual.

&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_cam1_gpios>, <&pinctrl_dap1_gpios>, ...

/* Apalis AN1_ADC */
pinctrl_adc0: adc0grp {
fsl,pins =
/* Apalis AN1_ADC0 */
<IMX8QM_ADC_IN0_DMA_ADC0_IN0 0xc0000060>,
...
};

For further details and a table of possible pin settings, refer to the i.MX 8QuadMax Applications Processor Reference Manual or i.MX 8DualXPlus/8QuadXPlus Applications Processor Reference Manual. The possible muxings are listed in chapter 4 of the datasheets or can be found using the Pinout Designer tool available at pinout.torizon.io.

i.MX 8M Mini/Plus Based Modules​

The Input/Output Multiplexer Controller (IOMUXC) in the i.MX 8M Mini/Plus SoCs enables the multiplexing of pins and allows for pin configurations such as drive strength. To configure a pin, a device tree node is required inside the pin controller node with the property fsl,pins. Six integers must be assigned to the property for each pin, with the first five usually given by preprocessor definitions found at imx8mm-pinfunc.h or imx8mp-pinfunc.h. These #define statements specify the desired function for a given SoC pin. For instance, in the example below, GPIO1_IO06 is muxed to GPIO1_IO06 on Verdin iMX8MM, and ECSPI2_MISO is muxed to UART4_DCE_CTS on Verdin iMX8MP.

The third integer then defines the pin settings in hexadecimal notation. For a detailed description and table of possible pin-settings, look them up in the i.MX 8M Plus Applications Processor Reference Manual or i.MX 8M Mini Applications Processor Reference Manual. The possible muxings can be found in chapter 4 of the datasheet or with our Pinout Designer pinout.torizon.io.

/arch/arm64/boot/dts/freescale/imx8mm-verdin.dtsi
&iomuxc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio1>, <&pinctrl_gpio2>, ...

pinctrl_can1_int: can1intgrp {
fsl,pins =
<MX8MM_IOMUXC_GPIO1_IO06_GPIO1_IO6 0x146>; /* CAN_1_SPI_INT#_1.8V */
};

Device Tree for NXP Based Modules​

The diagrams in this section represent the device tree and related structures of every Toradex module. All of those are divided into three levels of inclusion:

  1. SoC Level: Device tree includes that describes the SoC, cores, reserved memory and SoC peripherals.
  2. Module Level: Device tree includes that describe specific configurations at the SoM's level, such as some pinmuxing, GPIOs, peripherals, external memory and WiFi.
  3. Carrier Board Level: Device trees and related includes that describe carrier board specific configurations or peripherals.

i.MX 6 Based Modules​

The modules Colibri iMX6S/iMX6DL share the same device tree binary, so do the modules Apalis iMX6D/iMX6Q. Click on the box to see the current version of the respective device tree file.

Apalis/Colibri iMX6 device tree structure

Extra Resources​

Webinar: Demystifying Device Tree for NXP® i.MX Processors​



Send Feedback!