DTSh – A Devicetree viewer for Zephyr

Chris Duf is a chip-to-cloud software architect, working on embedded systems and IoT applications. He is the author of the DTSh tool.

Devicetree and the Devicetree Source Format (DTS) are major parts of Zephyr RTOS. But understanding the DTS output of an entire system—not to mention troubleshooting devicetree errors—can prove to be quite a challenge. This is especially true for beginners.

In this article, I’ll first discuss why the content of these DTS files is interesting, then introduce the Devicetree Shell (DTSh), a simple command line tool for visualizing the devicetree in a DTS, with versatile export capabilities.

Let’s do a quick demo before diving into the details. At build-time, during the configuration phase, Zephyr generates a devicetree that will represent the system hardware during the actual build phase. Two files are generated:

  • build/zephyr/include/generated/devicetree_generated.h: the devicetree in C, that describes the hardware to the device driver model
  • build/zephyr/zephyr.dts: the same devicetree in Devicetree Source Format (DTS), generated for debugging

DTSh can be used to present a much more human-readable view of these results:

The Devicetree Shell

find --with-bus * --OR --on-bus * -T --format NYcd

You can try this yourself, with the following in mind:

  • DTSh should install and run fine on GNU Linux (including WSL) and macOS with Python 3.8 to 3.11
  • on Windows, the readline API, on which DTSh relies for auto-completion and command history, is no longer distributed with the Python Standard Library: as a consequence, the user experience will be significantly degraded on this platform

Devicetree and Zephyr’s Driver Model

Devicetree is first a specification for a standardized data structure that can describe a system hardware and its initial configuration. Beside the DTS format we’ve already mentioned, a binary blob format is also defined (Flattened Devicetree, DTB).

Devicetree is primarily designed as a complete interface between a boot program like a bootloader and a client program like an operating system. When using Devicetree, Linux applies this approach (see Linux and the Devicetree):

  • the DTS for the system hardware is compiled to DTB, and linked into the vmlinux image
  • the boot program eventually passes a pointer to the devicetree in DTB format to the operating system client
  • the Linux kernel then walks through the devicetree to populate its device model, allocating device structures dynamically for the enabled DT nodes

While the hardware that the Linux kernel targets can benefit from this design, Zephyr targets embedded systems, which are more constrained, especially regarding persistent memory:

  • the binary blob is then wasted .rodata (typically Flash memory)
  • the kernel API needed to walk through the devicetree is wasted .text (also typically Flash memory)
  • the run-time overhead is worthless, most of the time device structures will be statically allocated

Zephyr has therefore chosen a different approach:

  • the devicetree is not passed to the Zephyr client program (everything happens at build-time)
  • only the device structures for enabled DT nodes are statically allocated into the .rodata section
  • the API to access these devices structures is entirely macro based, and does not end up in the .text section

Compared to Linux, we could say that the devicetree is passed to Zephyr in C format at build-time.

And, as a Zephyr application developer:

  • You use macros like DT_PATH or DT_NODELABEL to get DT node identifiers (this relies on the generated devicetree_generated.h, which is included by zephyr/devicetree.h)
  • Others like DEVICE_DT_GET to get pointers to the corresponding device structures

All this macrobatics is well explained in a presentation Marti Bolivar gave at a Zephyr Developer Summit in 2022: a few things have changed but still really worth watching.

So, why should I be interested in the DTS files content ?

A one sentence answer (well, two) could be:

  • Because the generated DTS will always describe the whole system hardware from the board files (the .dts and .dtsi files, plus possible overlays), including disabled devices and buses
  • Since the DTS file is generated early during the configuration phase, chances are that it will exist even when the build eventually fails: here, checking the devicetree content will prove helpful in debugging common issues (you know, the DT_N_S_ and __device_dts_ord_ things)

Well, but then what application can I use to open this DTS file, other than a text editor?

DTSh is just that: a simple tool to navigate, visualize and search a devicetree described in a DTS file, along with the bindings that specify and document its content.

In the remainder of this article, we’ll install DTSh before we reconsider this question with a new tool on hand.

Install DTSh

For simplicity, the examples in this article will assume DTSh is installed alongside your Zephyr development environment (please refer to DTSh’s Getting Started Guide for details about installation methods): just enter pip install dtsh from the same prompt where you usually run west commands.

You should now be able to open DTS files in the Devicetree Shell, e.g.:

$ cd zephyr/samples/sensor/bme680
$ west build
$ dtsh
dtsh (0.2rc1): Shell-like interface with Devicetree
How to exit: q, or quit, or exit, or press Ctrl-D

/
> ls -l
 Name              Labels          Binding
 ───────────────────────────────────────────────────────
 chosen
 aliases
 soc
 pin-controller    pinctrl         nordic,nrf-pinctrl
 entropy_bt_hci    rng_hci         zephyr,bt-hci-entropy
 sw-pwm            sw_pwm          nordic,nrf-sw-pwm
 cpus
 leds                              gpio-leds
 pwmleds                           pwm-leds
 buttons                           gpio-keys
 connector         arduino_header  arduino-header-r3
 analog-connector  arduino_adc     arduino,uno-adc

In the above example:

  • no need to pass the DTS file path to DTSh: by default it will try to open the devicetree at build/zephyr/zephyr.dts, which the preceding west build command has just generated
  • no need to tell where to search for the YAML binding files: DTSh first tries to retrieve this information from a CMake cache at build/CMakeCache.txt, which should also exist

As you can see, it really presents itself like a shell: a prompt where you enter commands like cd, ls or find, but these operate here on device paths rather than on file system paths. And, as with a shell, you can redirect commands output to files.

Only a few principles are necessary to start using DTSh:

  • You can navigate the devicetree (change the current working branch) with the cd command like a hierarchical file system
  • Device paths can be absolute path names or relative paths from the current working branch, may start with DTS labels and support some globbing
  • By default, commands will output paths to represent nodes, just like their Unix homonyms represent files or directories (ls, find and tree); the --format FMT option will instead format the output to show the information you’re looking for (-l is a shortcut for a configurable default format)
  • Command output redirection also uses the POSIX syntax, e.g. tree -lR > devicetree.html (the file extension determines the format)
  • For a better default user experience, prefer a dark Terminal theme and run the shell full-screen or in a maximized window
  • Last but not least, don’t be afraid to press the TAB key twice: it will trigger contextual auto-completion from nearly everywhere

A word of advice: if a command line tool looks scary, don’t worry. The Devicetree Shell syntax is simple and consistent, and command line auto-completion and history will save you from both memorizing and typing most things.
The DTSh handbook provides detailed documentation and various examples.

Back to DTS files

Let’s go back to the use cases we put forward:

  • Quickly learn about a new hardware configuration
  • Debug mysterious Devicetree issues

Learn about Hardware Configurations

When starting a new project, or porting an existing application to another board, it’s nice to get some visual representation of what’s available to your program.

Let’s say you’re interested in the nRF52840 DK (nrf52840dk_nrf52840), which you haven’t yet used with Zephyr.

First, generate the corresponding devicetree, then open it in dtsh:

# Any Zephyr sample that supports the board would do just fine.
cd zephyr/samples/sensor/bme680

# Remember, the configuration phase is enough.
cmake -B build -DBOARD=nrf52840dk_nrf52840

# No need to pass arguments, dtsh will open build/zephyr/zephyr.dts
dtsh

Try the command tree --format NKYC: it will dump the whole devicetree as a detailed tree view, with all device labels and alias (Also Known As), buses and bindings, ending with something like in the screenshot below.

Devicetree as tree view

tree --format NKYC

You have probably noticed:

  • Some nodes appear dim: these are the disabled buses and devices
  • Most of the time, the content of the Binding column appears with a (possibly dashed) underline: these are hyperlinks to the corresponding binding files, hovering over the text will reveal their paths, and clicking while holding the Ctrl key will open them in with your default application for handling YAML files
  • The content of the Binding column may also be anchored to its parent: these are child-bindings specified in the same YAML file

But the most blindingly obvious was probably that the command literally dumped its output, making impossible to understand the tree view calmly before it disappears. This can be addressed in two ways:

  • Try tree --format NKYC --pager: this will pass the command output to the system pager, where you can take your time (press h to display the pagers’ help screen, q to exit the pager and return to the dtsh prompt)
  • You can also export the tree view to an HTML file, e.g. tree --format NKYC > nrf52840dk_nrf52840.html

Another similar use case could consist of finding some information without having to resort to the SoC’s datasheet. You may not have the datasheet readily available, nor do you want to have it littering your desk.

The size and base addresses of the different kind of available memory is typically something you can forget, especially if you work with a handful of boards.

/
> find -E --also-known-as (image|storage).* --format NLrd > memory.txt

/
> find --also-known-as ram -i --format NLrd >> memory.txt

You now have a simple and nice memory.txt file:

Name             Labels             Registers         Description
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
partition@c000   slot0_partition    0xc000 (472 kB)   Each child node of the fixed-partitions node represents…
partition@82000  slot1_partition    0x82000 (472 kB)  Each child node of the fixed-partitions node represents…
partition@f8000  storage_partition  0xf8000 (32 kB)   Each child node of the fixed-partitions node represents…

Name             Labels  Registers            Description
──────────────────────────────────────────────────────────────────────────────
memory@20000000  sram0   0x20000000 (256 kB)  Generic on-chip SRAM description

Tip: find supports a handful of criteria to match devicetree nodes with. To get the full list, start a command line with find --, then press TAB twice. The auto-completion will enumerate all the options that have a long name (short option names begin with a single -): the criteria are those starting with --with unless another term really seems more natural, e.g. --also-known-as or --on-bus.

/
> find --[TAB][TAB]
-h --help                      print command help
--also-known-as PATTERN        match labels or aliases
--chosen-for PATTERN           match chosen nodes
--count                        print matches count
--enabled-only                 filter out disabled nodes or branches
--format FMT                   node output format
--NOT                          negate the criterion chain
--on-bus PATTERN               match bus of appearance
--OR                           match any criterion instead of all
--order-by KEY                 sort nodes or branches
--pager                        page command output
--with-alias PATTERN           match aliases
--with-binding PATTERN         match binding's compatible or headline
--with-binding-depth EXPR      match child-binding depth
--with-bus PATTERN             match supported bus protocols
--with-compatible PATTERN      match compatible strings
--with-description PATTERN     grep binding's description
--with-device-label PATTERN    match device label
--with-dts-ord EXPR            match dependency ordinal
--with-irq-number EXPR         match IRQ numbers
--with-irq-priority EXPR       match IRQ priorities
--with-label PATTERN           match node labels
--with-name PATTERN            match node name
--with-path PATTERN            match path name
--with-reg-addr EXPR           match register addresses
--with-reg-size EXPR           match register sizes
--with-status PATTERN          match status string
--with-unit-addr EXPR          match unit address
--with-unit-name PATTERN       match unit name
--with-vendor PATTERN          match vendor prefix or name

DTS labels often match names in the SoC’s documentation:

/
> ls &[TAB][TAB]
acl                  Nordic nRF family ACL (Access Control List)
adc                  Nordic Semiconductor nRF family SAADC node
arduino_adc          ADC channels exposed on Arduino Uno (R3) headers…
arduino_header       GPIO pins exposed on Arduino Uno (R3) headers…
arduino_i2c          Nordic nRF family TWI (TWI master)…
arduino_serial       Nordic nRF family UARTE (UART with EasyDMA)
arduino_spi          Nordic nRF family SPIM (SPI master with EasyDMA)
boot_partition       Each child node of the fixed-partitions node represents…
button0              GPIO KEYS child node
... stripped ...

/
> ls &button0 -ld
Name      Labels   Binding
───────────────────────────────────────
button_0  button0  GPIO KEYS child node

/
> ls &timer0 -ld
Name            Labels  Binding
────────────────────────────────────────
timer@40008000  timer0  nordic,nrf-timer

/
> ls &i2c0 -ld
Name          Labels             Binding
───────────────────────────────────────────────
i2c@40003000  i2c0, arduino_i2c  nordic,nrf-twi

Debug Devicetree Issues

We are now equipped to look again at these mysterious DT_N_S_ and __device_dts_ord_ things.

Let’s start by disabling the I2C bus in boards/nrf52840dk_nrf52840.overlay to intentionally introduce a build error:

&i2c0 {
  status = "disabled";
  bme680@76 {
    compatible = "bosch,bme680";
    reg = <0x76>;
  };
};

west build will fail with one of these mysterious errors:

$ west build

... stripped ...

 from zephyr/drivers/sensor/bme680/bme680.c:14: error: '__device_dts_ord_124'
 undeclared here (not in a function); did you mean '__device_dts_ord_14'?
 89 | #define DEVICE_NAME_GET(dev_id) _CONCAT(__device_, dev_id)
    |                                         ^~~~~~~~~

... stripped ...

FATAL ERROR: command exited with status 2: /usr/bin/cmake --build zephyr/samples/sensor/bme680/build

First, you need to know that __device_dts_ord_124 reads “the device with dependency ordinal 14”, where the dependency ordinal is a non-negative integer value such that the value for a node is less than the value for all nodes that depend on it.

Knowing how to read C tokens like DT_N_S_soc_S_i2c_40003000 is also useful:

  • DT_N reads “devicetree node” (DT node)
  • _S_ represents the / that separates node names
  • DT_N_S_soc_S_i2c_40003000 is then the C token for the node with path name /soc/i2c@40003000

And indeed, if your search devicetree_generated.h for DT_N_S_soc_S_i2c_40003000, you’ll eventually come across the lines below (scattered among more than ten thousand):

#define DT_N_S_soc_S_i2c_40003000_ORD 124
#define DT_N_S_soc_S_i2c_40003000_STATUS_disabled 1
#define DT_N_S_soc_S_i2c_40003000_S_bme680_76_ORD 125
#define DT_N_S_soc_S_i2c_40003000_S_bme680_76_REQUIRES_ORDS 124

Which should help you conclude that the I2C bus the BME680 sensor should connect to is actually disabled.

Compare this to what DTSh can tell you with a single command line:

/
> find --with-dts-ord 124 --format psTd
Mysterious build error

A not so mysterious issue.

Epilogue

You should now be able to:

  • Open DTS files that represent your hardware configurations (hint: cd myproject && cmake -B build -DBOARD=myboard && dtsh)
  • Navigate (cd) and visualize (ls, tree) the devicetree
  • Experiment with find --[TAB][TAB] to search for buses, bindings, interrupts, and more
  • Experiment with the --format --[TAB][TAB] option to see the information you’re interested in
  • Experiment with command output redirection to text, HTML and SVG (hint: find --with-bus * --OR --on-bus * -T --format NYcd > buses.svg)
This brief introduction to DTSh misses interesting topics, don’t hesitate to skim through the documentation to find:
  • More usage examples
  • How to configure DTSh behavior and appearance with preferences and theme files
  • Learn more about the command line interface, auto-completion and history
  • A summary of useful key bindings (hint: Up and Down move backward and forward through the command history, respectively)
  • A detailed description of the built-in commands, their options and parameters

And remember, auto-completion (pressing TAB twice) is your friend, and can even list the available commands:

/
> [TAB][TAB]
alias     list aliased nodes
cd        change the current working branch
chosen    list chosen nodes
find      search branches for nodes
ls        list branch contents
pwd       print path of current working branch
tree      list branch contents in tree-like format

RFC: DTSh: DTS file viewer with a shell-like command line interface is a proposal to upstream the Devicetree Shell as a new Zephyr extension to West, such that you would eventually be able to just run with:

$ west build
$ west dtsh

This is the appropriate place to comment on: the potential usefulness of such a tool, the acceptability of the command line approach for beginners, the features you found interesting and those you would like to see added (accessing arbitrary node properties is definitely a miss), or the technical reasons why you think it is a good idea, or not, to upstream DTSh as a new West command. This is not the appropriate place to report bugs, though. Please open issues on the DTSh project instead.

Talk with an Expert

Implementing an IoT project takes a team of people, and we want to help out as part of your team. If you want to troubleshoot a current problem or talk through a new project idea, we're here for you.

Start the discussion at forum.golioth.io