DTSh – A Devicetree viewer for Zephyr
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 modelbuild/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:
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 byzephyr/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 precedingwest 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
andtree
); 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.
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 thedtsh
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 namesDT_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
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
)
- 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.
Start the discussion at forum.golioth.io