Chapter 27B: Device Tree for a custom i.MX6ULL board¶
What: a custom-board Device Tree case study covering UART console, SD/eMMC, Ethernet, I2C, SPI, LEDs, keys, regulators, aliases and
/chosen.Why: Chapter 27 teaches the grammar. This chapter teaches the job: turn a schematic into a
.dtsfile that the kernel can boot.Result: a view of how schematic connections become kernel resource relationships, and why compilation, schema checks and peripheral operation establish different things.
Focus: do not start by copying the whole EVK and hoping. Start with a small bootable board description, then add one peripheral at a time.
The case uses the image-loading interface described in Chapter 26. Its fragments are illustrative, not a qualified description of the fitted MINI board, and a working port is not a reading prerequisite.
27B.1 The board DTS is not the SoC DTS¶
The i.MX6ULL SoC file already exists:
arch/arm/boot/dts/nxp/imx/imx6ull.dtsi
It describes hardware inside the chip:
CPU.
GIC.
UART controllers.
I2C controllers.
SPI controllers.
USDHC controllers.
GPIO banks.
CCM clocks.
IOMUXC pin controller.
OCRAM.
Your board file describes what is connected outside the chip and which SoC blocks are actually used:
arch/arm/boot/dts/nxp/imx/imx6ull-pa-mini.dts
Board DTS files answer questions like:
Which UART is the console?
Which pins are routed to the SD slot?
Which regulator powers the SD card?
Which PHY is connected to FEC Ethernet?
Which I2C address is the temperature sensor?
Which GPIO drives the user LED?
27B.2 Files changed¶
File |
What changes |
|---|---|
|
New board description. |
|
Add |
U-Boot environment |
Set |
No kernel driver is written in this chapter.
27B.3 Start with the smallest bootable DTS¶
Create arch/arm/boot/dts/nxp/imx/imx6ull-pa-mini.dts:
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "Point Atom MINI i.MX6ULL";
compatible = "point-atom,imx6ull-mini", "fsl,imx6ull";
chosen {
stdout-path = &uart1;
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512 MiB */
};
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
&iomuxc {
pinctrl_uart1: uart1grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
>;
};
};
This is enough for:
Kernel to identify the machine.
Kernel to know RAM size.
Kernel to find the console UART.
Kernel to print early boot logs.
It is not enough for SD rootfs, Ethernet, GPIO LEDs, or I2C. Add those later.
27B.4 Add it to the kernel build¶
Edit arch/arm/boot/dts/nxp/imx/Makefile:
dtb-$(CONFIG_SOC_IMX6ULL) += \
imx6ull-pa-mini.dtb
Keep the list sorted near other imx6ull-* boards if the file already has a long block.
Build:
$ make dtbs
$ ls arch/arm/boot/dts/nxp/imx/imx6ull-pa-mini.dtb
Boot it:
=> tftp 0x82000000 zImage
=> tftp 0x83000000 imx6ull-pa-mini.dtb
=> setenv bootargs 'console=ttymxc0,115200 earlycon root=/dev/mmcblk0p2 rw rootwait'
=> bootz 0x82000000 - 0x83000000
The first proof is the model line:
OF: fdt: Machine model: Point Atom MINI i.MX6ULL
If you see that line, the kernel consumed your DTB.
27B.5 Add fixed regulators¶
Most boards have always-on rails or GPIO-controlled rails. Describe them explicitly. Drivers use these nodes to know when power is available.
Example SD-card 3.3 V rail:
/ {
reg_sd1_vmmc: regulator-sd1-vmmc {
compatible = "regulator-fixed";
regulator-name = "VSD_3V3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
gpio = <&gpio1 9 GPIO_ACTIVE_HIGH>;
enable-active-high;
};
};
If the rail is truly always on and has no GPIO:
/ {
reg_3v3: regulator-3v3 {
compatible = "regulator-fixed";
regulator-name = "3V3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
};
};
Do not invent regulators just to silence warnings. Each regulator node should correspond to a real rail in the schematic.
27B.6 Add SD or eMMC¶
Example SD slot on USDHC1:
&usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
keep-power-in-suspend;
vmmc-supply = <®_sd1_vmmc>;
bus-width = <4>;
status = "okay";
};
Pin group:
&iomuxc {
pinctrl_usdhc1: usdhc1grp {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x17059
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x10071
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x17059
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x17059
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x17059
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x17059
>;
};
};
Use the actual pins from your schematic. The pad names must match imx6ul-pinfunc.h.
Boot proof:
mmc0: new high speed SDHC card at address 0001
mmcblk0: mmc0:0001 SD16G 14.8 GiB
If you do not see mmcblk0, check pinctrl, card-detect GPIO, regulator, and CONFIG_MMC_SDHCI_ESDHC_IMX.
27B.7 Add Ethernet¶
Ethernet needs more than a MAC node. You need MAC pins, MDIO, PHY address, reset GPIO, clocking, and sometimes a regulator.
Example:
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1>;
phy-mode = "rmii";
phy-handle = <ðphy0>;
phy-supply = <®_3v3>;
status = "okay";
mdio {
#address-cells = <1>;
#size-cells = <0>;
ethphy0: ethernet-phy@0 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <0>;
reset-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;
reset-assert-us = <10000>;
reset-deassert-us = <80000>;
};
};
};
Boot proof:
fec 2188000.ethernet eth0: registered PHC device 0
libphy: fec_enet_mii_bus: probed
Runtime proof:
target# ip link show eth0
target# udhcpc -i eth0
If the PHY is not found, check:
reg = <N>matches the PHY strap address.MDC/MDIO pins are correct.
PHY reset is deasserted.
50 MHz RMII reference clock is present.
27B.8 Add I2C devices¶
Enable the controller:
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
temp@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
Pin group:
&iomuxc {
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
};
The 0x40000000 bit selects open-drain behavior in the i.MX pin config. I2C also needs pull-ups on the board.
Boot proof:
tmp102 0-0048: initialized
Runtime proof:
target# cat /sys/class/hwmon/hwmon*/temp1_input
27B.9 Add SPI devices¶
Enable ECSPI:
&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>;
status = "okay";
spidev@0 {
compatible = "rohm,dh2228fv";
reg = <0>;
spi-max-frequency = <10000000>;
};
};
For learning, spidev is useful. For production, bind the real device to a real driver. Do not ship product logic that depends on a fake compatible string.
Runtime proof:
target# ls /dev/spidev*
27B.10 Add LEDs and keys¶
LED:
/ {
leds {
compatible = "gpio-leds";
user_led {
label = "user";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
default-state = "off";
};
};
};
Button:
/ {
gpio-keys {
compatible = "gpio-keys";
recovery {
label = "recovery";
gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
linux,code = <KEY_RESTART>;
wakeup-source;
};
};
};
Add the include for key constants:
#include <dt-bindings/input/input.h>
Runtime proof:
target# ls /sys/class/leds
target# echo 1 > /sys/class/leds/user/brightness
target# cat /proc/bus/input/devices
27B.11 Add aliases and chosen¶
Make Linux names stable:
/ {
aliases {
serial0 = &uart1;
ethernet0 = &fec1;
mmc0 = &usdhc1;
i2c0 = &i2c1;
spi0 = &ecspi1;
};
chosen {
stdout-path = &uart1;
};
};
Aliases affect numbering. If you want /dev/mmcblk0 to stay predictable, use aliases.
27B.12 Build and validate¶
Build only the DTB:
$ make dtbs
Run schema checks:
$ make dtbs_check DT_SCHEMA_FILES=imx
Reverse-compile for inspection:
$ dtc -I dtb -O dts \
arch/arm/boot/dts/nxp/imx/imx6ull-pa-mini.dtb \
> /tmp/imx6ull-pa-mini.expanded.dts
Search the expanded file:
$ grep -n 'Point Atom MINI' /tmp/imx6ull-pa-mini.expanded.dts
$ grep -n 'serial@2020000' /tmp/imx6ull-pa-mini.expanded.dts
$ grep -n 'ethernet-phy' /tmp/imx6ull-pa-mini.expanded.dts
The expanded DTS is what the kernel effectively sees after includes.
27B.13 Bring-up order¶
The dependency order for a staged board description is:
Memory and UART.
SD/eMMC.
Rootfs boot.
Ethernet.
LEDs and keys.
I2C.
SPI.
Displays, audio, sensors, and optional devices.
A log from each known state separates a new peripheral dependency from a regression in an earlier one.
27B.14 Key Notes¶
A board DTS owns external topology, not the SoC register description or driver implementation. Included SoC nodes provide the base that board-specific references refine.
Memory and console form an early observation path. Storage and filesystem support are additional dependencies before a normal rootfs can become usable.
Each enabled controller needs more than
status = "okay". Pinmux, supplies, bus width, reset signals and attached-device addresses must describe the same physical circuit.Ethernet spans MAC, MDIO, PHY, reset and reference clock. An interface name or model string alone does not establish a usable physical link or correct MINI wiring.
LEDs, keys and sensors use existing subsystem bindings where possible. A fake SPI compatible string introduces a description mismatch rather than a production device interface.
Build registration selects which board blob is produced, while bootloader selection decides which blob Linux receives. Either can be correct while the other is wrong.
The fragments describe architectural patterns, not verified carrier mappings. A fitted board requires schematic-to-pad tracing and runtime evidence beyond compilation and schema checks.
27B.15 Pitfalls¶
Copying the EVK DTS blindly. It may describe pins and regulators your board does not have.
Enabling a controller without pinctrl. The driver probes, but pins stay muxed to something else.
Wrong GPIO polarity.
GPIO_ACTIVE_LOWvsGPIO_ACTIVE_HIGHchanges behavior completely.Wrong PHY address. Ethernet will probe MDIO and find nothing.
Forgetting
vmmc-supply. MMC may work sometimes and fail under load.Using DT to fix driver bugs. DT describes hardware. Do not encode software policy unless the binding explicitly asks for it.
27B.16 Going deeper¶
Documentation/devicetree/bindings/, for each device class.arch/arm/boot/dts/nxp/imx/, for i.MX board examples.scripts/dtc/, for the compiler.drivers/of/, for how the kernel creates devices from DT.
Previous: Chapter 27A: DT bindings YAML and dt_binding_check