Zephyr Dt Rust 1
Today’s exercise is going to be an exploration of two lines from the Zephyr “blinky” sample to understand better how devicetree support works in Zephyr, and explore how we might do something equivalent in Rust.
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
There is quite a bit of complexity going on behind this fairly simple seeming piece of code. As we start to look at how this might work, the first thing we discover is that there are a lot of macros involved, and many of these macros stitch names together to form the names for other macros.
To hope to make any sense of this, we’re going to have to get the C preprocessor involved to help us out. Fortunately, the Zephyr devicetree docs anticipate this need, and have a troubleshooting section.
By making sure that CONFIG_COMPILER_SAVE_TEMPS is set, cmake will leave the
preprocessed output. In this case, main.c’s preprocessed output will go into
build/CMakeFiles/app.dir/src/main.c.i.
Let’s start with the DT_ALIAS macro first. By doing something like:
int foo = LED0_NODE;
Just building will give us a nice compiler error about DT_N_S_leds_S_led_0
being undeclared. The definition of
DT_ALIAS,
is:
#define DT_ALIAS(alias) DT_CAT(DT_N_ALIAS_, alias)
To understand this, we need to dive a bit into some of these other aliases. In
this case, DT_CAT, which is just some macro indirection to cause the C
preprocessor to assemble a new symbol by contatenating two existing symbols.
#define DT_CAT(a1, a2) a1 ## a2
In our case, This results in DT_N_ALIAS_led0. This is defined by the
generated DT headerfile in
build/zephyr/include/generated/devicetree_generated.h:
#define DT_N_ALIAS_led0 DT_N_S_leds_S_led_0
In addition, there are a set of macros defined with this prefix, and numerous
additional names, e.g. _PATH, gives the path within the device tree
“/leds/led_0”. Note that the path in the devicetree is in the macro name, with
the ‘S’ used for a slash.
This node name was not intended to just be used directly as a symbol, and this is why we get an error if we try to use it. So, the next macro to decode is where this symbol is used:
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
This
macro
is the primary entry for accessing devicetree nodes for GPIOs, and is defined in
include/zephyr/drivers/gpio.h, and merely calls GPIO_DT_SPEC_GET_BY_IDX with an
additional argument of zero.
This definition is:
#define GPIO_DT_SPEC_GET_BY_IDX(node_id, prop, idx) \
{ \
.port = DEVICE_DT_GET(DT_GPIO_CTLR_BY_IDX(node_id, prop, idx)),\
.pin = DT_GPIO_PIN_BY_IDX(node_id, prop, idx), \
.dt_flags = DT_GPIO_FLAGS_BY_IDX(node_id, prop, idx), \
}
which will take some additional decoding. The macro DT_GPIO_CTLR_BY_IDX is
defined in include/zephyr/devicetree/gpio.h:
#define DT_GPIO_CTLR_BY_IDX(node_id, gpio_pha, idx) \
DT_PHANDLE_BY_IDX(node_id, gpio_pha, idx)
#define DT_PHANDLE_BY_IDX(node_id, prop, idx) \
DT_CAT6(node_id, _P_, prop, _IDX_, idx, _PH)
Which should now be sufficient to assemble a name from this:
DT_N_S_leds_S_led_0_P_gpios_IDX_0_PH, which is defined in the generated
devicetree file as: DT_N_S_soc_S_gpio_40014000, referring to the node where
the gpio node is defined.
Next, we have the macro DEVICE_DT_GET, which is defined in
include/zephyr/device.h:
#define DEVICE_DT_GET(node_id) (&DEVICE_DT_NAME_GET(node_id))
#define DEVICE_DT_NAME_GET(node_id) DEVICE_NAME_GET(Z_DEVICE_DT_DEV_ID(node_id))
#define Z_DEVICE_DT_DEV_ID(node_id) _CONCAT(dts_ord_, DT_DEP_ORD(node_id))
#define DEVICE_NAME_GET(dev_id) _CONCAT(__device_, dev_id)
...
#define DT_DEP_ORD(node_id) DT_CAT(node_id, _ORD)
For this node, the _ORD properpty in my build is 14. So, in this case the
.port field resolves to &__device_dts_ord_14. Similar dereferencing will
get us a pin of 13, and flags of 0.
Ord struct
The next question is where does this ord structure come from? The externs for
them comes from a line near the end of include/zephyr/device.h:
DT_FOREACH_STATUS_OKAY_NODE(Z_MAYBE_DEVICE_DECLARE_INTERNAL)
The definition itself will take a bit more investigation and will be the subject of a subsequent post.
What about Rust
The main mechanism used in Zephyr’s C device tree support is to expand all of
the properties of the device tree into #defines, where the path is flattened
into a C name.
Rust could support this, it is very much not a rust-like approach.
Instead, it seems like a much better approach would be to, instead of flattening
the names, represent the tree directly as a module tree. So instead of
DT_N_S_soc_S_gpio_40014000, this could be represented at some level in a crate
as just dt::soc::gpio::x40014000. The use of numbers would require some
simple rules. Each property would be a const defined within that module.
Ideally, the end result would be possibly some similar macros in Rust that would look up a node in the device tree, and return a structure. This structure could be of a type more specific than just ‘device’ allowing methods specific to the device to be used. This, again, will be for future discussion.