i.MX6 Device Tree customization

Published on December 6, 2014

Archived Notice

This article has been archived and may contain broken links, photos and out-of-date information. If you have any questions, please Contact Us.

Starting with the release of Linux 3.5.7, a new feature known as Device Trees has fundamentally changed the way that boards are represented by the kernel. The "C" modules used in earlier kernels has been replaced with Device Tree Blobs (DTBs) as a way of representing the structure and connections for a particular board.

There's a great deal of documentation on-line, including this set at elinux.org, so we won't sweat the details.Instead, this post will explain how you can create device trees that change our standard boards to over-ride their normal functionality. An example will be presented that re-purposes the parallel LCD port as a set of SPI, GPIO, I2C, and PWM ports.

For the impatient

  • We've created three new device trees that re-purposes the parallel LCD port (J15) on our Nitrogen6x, BD-SL-i.MX6 and Nitrogen6_Max boards.
  • If you're not using this port, you can configure U-Boot to load the alternate DTB and gain additional I/O.
  • We've created a break-out board to make it easier to connect to these pins.
  • You can use the same approach to build your own Device Trees and customize the kernel for your own purposes.
  • The easiest functions to re-map are the Parallel LCD port (J15), the Parallel Camera port (J5), the Android Button connector (J14) and the Boot mode/EIM connector (J12).

Background

There's a great description about the rationale for the usage of Device Trees here on the Ubuntu Wiki.

To quote:

As the number of chip-sets increase, so too does the software needed to support them at the kernel level.

To elaborate on Ubuntu's comment:

As the number of boards increases, so too does the software...

When you add in the specific requirements for even standard boards as discussed in our earlier post, things get out of control quickly.

The Flattened Device Tree specification helps to bring some order to this.

Immediate (and visible) impact

As a practical matter, the first thing you'll need to know about device trees is that we're packaging them separately from the kernel.

In earlier kernels, we've booted by simply loading a kernel (generally uImage), setting bootargs to point at a root filesystem and launching with a single-argument bootm command.

With device-tree enabled kernels, we also need to load a .dtb file and pass an additional argument to bootm or bootz.

We've updated our boot scripts to do this, and even to compute the file name based on the CPU and board type but haven't commented much about it in the blog.

Note that if you set the dtbname environment variable, the boot script will bypass the test of board and cpu type.

U-Boot > setenv dtbname "my-custom.dtb" U-Boot > saveenv

Division of labor

The key point in the separation of Device Tree and kernel is that:

  • The kernel and any loadable modules defines the potential set of drivers supported, and
  • The Device Tree defines the specific bindings for things that aren't on enumerable busses like PCIe or USB

Note that the kernel and DTB can be bound together as described here, but this sacrifices much of the feature of device trees. This type of binding can be useful in speeding the boot, and securing an image, but it's something that should happen late in the development process.

Our specifics

If you compare the file arch/arm/boot/dts/Makefile in our the boundary-imx_3.10.17_1.0.2_ga branch of our kernel repository against the Freescale release, you'll see that we've added 19 different targets to represent our standard boards and custom designs using 3.10.17.

~/linux-imx6$ git diff --stat origin/imx_3.10.17_1.0.2_ga arch/arm/boot/dts/Makefile arch/arm/boot/dts/Makefile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+)

If you look closely at the details, you'll see that some boards have multiple variants for the Quad/Dual (q) and Dual-Lite/Solo processor variants to support the entire processor lineup:

~/linux-imx6$ git diff origin/imx_3.10.17_1.0.2_ga arch/arm/boot/dts/Makefile \ | grep nitrogen6x + imx6dl-nitrogen6x.dtb \ + imx6q-nitrogen6x.dtb \

This is done because the device tree bindings contain precise memory addresses for various peripherals and registers, and many of those addresses differ between the two processor variants.

To avoid duplication of the common parts, we've split the definition of our device trees into a common include file with the prefix imx6qdl- and a file extension of .dtsi and variant-specific files with the prefix of imx6q- and imx6dl- that #include the .dtsi file.

For example, take a look at the Device Tree Source (dts) files for the BD-SL-i.MX6:

~/linux-imx6$ ls arch/arm/boot/dts/imx6*sabrelite*.dts* | grep -v nolcd arch/arm/boot/dts/imx6qdl-sabrelite.dtsi arch/arm/boot/dts/imx6q-sabrelite.dts ~/linux-imx6$ head -n 20 arch/arm/boot/dts/imx6q-sabrelite.dts ... #include #include "imx6q.dtsi" #include "imx6qdl-sabrelite.dtsi" / { model = "Freescale i.MX6 Quad SABRE Lite Board";

Notice that the file imx6q-sabrelite.dts uses the #include directive to pull in both the imx6q.dtsi and imx6qdl-sabrelite.dtsi headers. Also note that the imx6q.dtsi and imx6dl.dtsi files in turn #include the file imx6qdl.dtsi, which is a Freescale-maintained header defining common parts of the entire family. Most of the components in the common header are disabled by default, so the board-level files need to enable them explicitly.

This pattern is repeated for all of our boards, and if we're offering options for other processors, you'll see a imx6dl- and imx6q- version
as with the Nitrogen6X:

~/linux-imx6$ ls arch/arm/boot/dts/imx6*nitrogen6x*.dts* | grep -v nolcd arch/arm/boot/dts/imx6dl-nitrogen6x.dts arch/arm/boot/dts/imx6qdl-nitrogen6x.dtsi arch/arm/boot/dts/imx6q-nitrogen6x.dts

Structure of DTS

The structure of a Device tree file (after the preprocessor runs) is

/dts-v1/; / { #address-cells = ; #size-cells = ; chosen { }; aliases { }; memory { device_type = "memory"; reg = ; }; }; / { additional declarations }; &alias { override or augmentation of declaraion };

The declarations section will define the CPU, RAM, busses, and peripherals connected to the CPU, and the references that follow may augment or override components declared previously through the use of aliases.

Okay or disabled

Components default to enabled (status="ok" or status="okay", but may be disabled explicitly by setting the status property to "disabled". For instance, if a particular board has a SATA bus defined, it might be disabled through a fragment like this:

&sata { status = "disabled"; };

Drivers and Pin Control blocks

One of the coolest features of i.MX processors is the ability to re-map the functionality of individual pins through a technique called pin muxing. Through this feature, a huge number of the 624 balls on the bottom of an i.MX6 can be re-purposed to as many as five different functions.

Refer to Chapter 36 of the i.MX6DQ Reference Manual for details.

This pin-muxing is common enough in ARM SOCs that it has direct support in the device tree implementation (for most drivers) and is generally implemented by
having a driver block like the LCD driver refer to a pinctrl block.

In the Nitrogen6x DTS file, you'll find this:

lcd0: lcd@0 { compatible = "fsl,lcd"; ... pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ipu1_4>; status = "okay"; };

The declaration of pinctrl-0 is in the general

pinctrl_ipu1_4: ipu1grp-4 { fsl,pins = < MX6QDL_PAD_DI0_DISP_CLK__IPU1_DI0_DISP_CLK 0x10 MX6QDL_PAD_DI0_PIN15__IPU1_DI0_PIN15 0x10 MX6QDL_PAD_DI0_PIN2__IPU1_DI0_PIN02 0x10 MX6QDL_PAD_DI0_PIN3__IPU1_DI0_PIN03 0x10 MX6QDL_PAD_DISP0_DAT0__IPU1_DISP0_DATA00 0x10 ...

Some of the details are omitted, but the key concept is that the LCD driver refers to the pinctrl block, which defines the configuration of the balls or pads which carry the signals.

Note that the references to the constants MX6QDL_PAD_... above have two parts, separated by double-underscores. The left-hand side is a reference to the pad name (the ball on the BGA). The right hand side is a reference to the chosen function. In the lines shown above, the mapping of the DI0 pads is made to their primary function as IPU_DI functions (short-hand for Image Processing Unit Display Interface, or parallel display).

If the lcd0 driver is instantiated, the pins will be configured, but not otherwise.

You may now be figuring out that we can prevent this mapping by setting the status of the lcd0 element to "disabled":

&lcd0 { status="disabled"; };

This is exactly what we've done on our way to re-mapping the functionality of J15 (the parallel display connector). You can see this in the device tree include file imx6qdl-boundary-nolcd.dtsi.

Looking further into that file, you'll also see that we've disabled the backlight driver:

&backlight_lcd { status = "disabled"; };

And the following bits disable or define some new functions of the pins on the display connector.

The new function(s) of J15

At this point, it's time to start describing the precise functionality for the new device tree configuration on ourNitrogen6x, BD-SL-i.MX6 and Nitrogen6_Max boards.

The following table shows the Voltage, pin numbers (for the breakout board and i.MX6 board), pad name, and function of each pin:

Voltage breakout board
pin number
J15 pin number Pad name Function
5V 1 40 +5V  
5V 2 39 +5V  
5V 3 38 +5V  
3.3V 4 37 SD1_DAT3 PWM1_OUT
3.3V 5 36 GPIO16 I2C3_SDA
3.3V 6 35 GPIO5 I2C3_SCL
3.3V 11 30 DISP0_DISP_CLK GP4:16
3.3V 7 34 DISP0_PIN15 (DE) GP4:17
3.3V 9 32 DISP0_PIN2 (HSYNC) GP4:18
3.3V 8 33 DISP0_PIN3 (VSYNC) GP4:19
3.3V 37 4 DISP0_PIN4 GP4:20
3.3V 20 21 DISP0_DAT0 ECSPI3_SCLK
3.3V 19 22 DISP0_DAT1 ECSPI3_MOSI
3.3V 18 23 DISP0_DAT2 ECSPI3_MISO
3.3V 17 24 DISP0_DAT3 ECSPI3_SS0
3.3V 16 25 DISP0_DAT4 ECSPI3_SS1
3.3V 15 26 DISP0_DAT5 ECSPI3_SS2
3.3V 14 27 DISP0_DAT6 ECSPI3_SS3
3.3V 13 28 DISP0_DAT7 GP4:28
3.3V 28 13 DISP0_DAT8 GP4:29
3.3V 27 14 DISP0_DAT9 PWM2_OUT
3.3V 26 15 DISP0_DAT10 GP4:31
3.3V 25 16 DISP0_DAT11 GP5:05
3.3V 24 17 DISP0_DAT12 GP5:06
3.3V 23 18 DISP0_DAT13 GP5:07
3.3V 22 19 DISP0_DAT14 GP5:08
3.3V 21 20 DISP0_DAT15 GP5:09
3.3V 36 5 DISP0_DAT16 ECSPI2_MOSI
3.3V 35 6 DISP0_DAT17 ECSPI2_MISO
3.3V 34 7 DISP0_DAT18 ECSPI2_SS0
3.3V 33 8 DISP0_DAT19 ECSPI2_SCLK
3.3V 32 9 DISP0_DAT20 ECSPI1_SCLK
3.3V 31 10 DISP0_DAT21 ECSPI1_MOSI
3.3V 30 11 DISP0_DAT22 ECSPI1_MISO
3.3V 29 12 DISP0_DAT23 ECSPI1_SS0
GND 38 3 GND  
GND 39 2 GND  
GND 40 1 GND  

To summarize this, we've added:

  • Two PWM outputs
  • Three SPI ports. Two of these have a single chip-select. The third (SPI3) has four chip-selects, and
  • Thirteen bi-directional GPIO ports, and
  • We also retained the I2C3 functions on pins 35 and 36. These are used for a touch-screen controller in our normal use with our Nit6X_800x480 display

This is not bad for the addition of a single .dtsi file. Note that we've #include'd that file in three distinct device trees, and that these will be included in future images based on kernel 3.10.17+:

imx6q-sabrelite.dtb
imx6q-nitrogen6x.dtb
imx6q-nitrogen6_max.dtb

To re-iterate what was said above, you can use these functions by simply setting the dtbname variable in U-Boot. For example if you're running on a Nitrogen6_Max board, you'd enter these commands:

U-Boot > setenv dtbname imx6q-nitrogen6_max.dtb U-Boot > saveeenv U-Boot > boot

Note that neither the I2C nor SPI drivers have any kernel drivers associated with them, so you'll need to use the /dev/i2c or spidev interfaces to access them.

You can access the GPIO pins through /sys/class/gpio. For example, to toggle pin 16 (GP5:05), you can do this:

root@nitrogen6x:~# echo 133 > /sys/class/gpio/export root@nitrogen6x:~# echo out > /sys/class/gpio/gpio133/direction root@nitrogen6x:~# echo 1 > /sys/class/gpio/gpio133/value root@nitrogen6x:~# echo 0 > /sys/class/gpio/gpio133/value

The GPIO numbers are calculated as ((bank-1)*32)+pin.

Other re-mapping possibilities

Note that you can make other choices for the use of the pins on J15. If you simply want GPIOs, you can remove the other functions in imx6qdl-boundary-nolcd.dtsi and add new entries to the pinctrl_lcd_gpios_1 block.

If you have other drivers to attach to the I2C bus, you can add them in this file, or even over-ride the file with your own overlay.

The primary reason we chose to build a break-out board for the LCD connector is that it contains a lot of pins. As mentioned in the For the impatient section above, there are other high pin count connectors that aren't often used.

  • The parallel camera connector (J5). This connector contains 33 pins, and 20 un-buffered signals. It also contains 2.5V power supplies and ground.
  • The Android button connector (J14). This connector contains 6 usable, un-buffered signals, plus a ground.
  • The Boot mode/EIM connector (J12). This 40-pin connector contains 32 un-buffered signals, plus 3.3V and ground.

If you're designing a SOM carrier and need some additional I/O, you'll also find that the pins on these connectors are the easiest to re-map in software, since their primary functionality can be removed in large sets with a minimum of code change.

Summary

This has been a long post and covered a lot of ground, but it is by no means a reference. Some of the links above are likely required reading before you can be completely comfortable with the use of device trees.

We do hope that this post gives you some encouragement and a better understanding of how device trees can help you either use our standard boards, or define your own variants and deliver better product faster.

As always, let us know if this is helpful, and where (not if) there are confusing bits.