Skip to content

Zephyr: Kconfig controlled devicetree instances

Embeint Blog PicturesIf you develop with Zephyr, then you're likely familiar with devicetree nodes and the DT_INST_FOREACH_STATUS_OKAY macro. The purpose of the macro is to iterate over a list of devicetree nodes with status = "okay" and create a Zephyr device for each. This allows you to instantiate an arbitrary number of nodes from devicetree without modifying any C code (happy times). 

Devicetree nodes can refer to other nodes in the devicetree, with the C code obtaining compile-time pointers to those devices to use in future API calls. However, if the device you have a reference to is not compiled into the final application, then you will end up with a linker error instead of a pointer (sad times).

It is also important to understand that the Zephyr build system has a strict processing order:

  1. Devicetree is completely parsed
  2. Kconfig is evaluated
  3. C code compiles

There is no way to modify the content of the devicetree based on the Kconfig options that the application selects.

One potential challenge that can arise when writing generic libraries is that the various backends for an common API often have different dependencies, resulting in different combinations of devices compiled into the final binary depending on the Kconfig setup.

Take the following devicetree snippet as an example:

/* CONFIG_VND_TX_UDP, depends on CONFIG_NETWORKING */
tx_udp {
compatible = "vnd,tx";
status = "okay";
};
/* CONFIG_VND_TX_SERIAL, depends on CONFIG_SERIAL */
tx_serial {
compatible = "vnd,tx";
status = "okay";
};
/* CONFIG_VND_LOGGER, Generic logging abstraction */
logger_udp {
compatible = "vnd,logger";
status = "okay";
backend = < &tx_udp >;
};
logger_serial {
compatible = "vnd,logger";
status = "okay";
backend = < &tx_serial >;
};

We have a generic logging abstraction vnd,logger with each node referring to a specific transmission backend, as well as two transmission instances that transmit payloads over communications stacks. If an application enables CONFIG_NETWORKING then CONFIG_VND_TX_UDP is enabled and tx_udp exists in our build. The same applies to CONFIG_SERIAL with CONFIG_VND_TX_SERIAL and tx_serial.

Unfortunately, a problem arises when CONFIG_VND_LOGGER is enabled. Each logger will attempt to get a compile-time reference to the vnd,tx devices, but this will only successfully link if both CONFIG_NETWORKING and CONFIG_SERIAL are enabled. Not every application may want the networking stack compiled in (even if the hardware supports it).

The typical "solution" is to ensure that tx_udp and logger_udp don’t exist in the devicetree if CONFIG_NETWORKING is not enabled by the application. The downside of this approach is that it requires the application to have an overlay file for every board that it could compile with, to indicate whether to add required nodes or delete unnecessary ones. This does not scale well with supporting new or out-of-tree hardware platforms (trust me, sad times).

A more scalable approach I employ for solving this problem is enabling the vnd,logger nodes (not the application) to determine whether their backend dependency will be compiled into the build and skipping the instantiation if not. This requires adding a piece of Kconfig information into the devicetree node that can then be queried within DT_INST_FOREACH_STATUS_OKAY. For the UDP backend example above, the approach will look like the following:

tx_udp {
compatible = "vnd,tx";
status = "okay";
depends-on = "CONFIG_VND_TX_UDP";
};
logger_udp {
compatible = "vnd,logger";
status = "okay";
backend = < &tx_udp >;
};

The vnd,tx API can now expose this information as a 0 or 1 through a macro such as:

#define VND_TX_IS_COMPILED(tx_node) IS_ENABLED(DT_STRING_TOKEN(tx_node, depends_on))

And the vnd,logger node can consume this information at compile-time with the following pattern:

#define LOGGER_DEFINE(inst) \
static const struct config = {...};
...

#define LOGGER_DEFINE_WRAPPER(inst) \
IF_ENABLED(VND_TX_IS_COMPILED(DT_INST_PROP(inst, backend))), (LOGGER_DEFINE(inst)))

DT_INST_FOREACH_STATUS_OKAY(LOGGER_DEFINE_WRAPPER)

This results in our desired outcome of each logger node only existing in the build if the backend is compiled in. So now we have a way to control features at the Kconfig level (e.g. CONFIG_NETWORKING, CONFIG_SERIAL) and have our devices pop into existence when their dependencies are met - without needing board specific application overlays! (happy times ahead)

 

Additional suggestion when you have a per-interface compatible...

If each of your vnd,tx devices have an additional per-interface "compatible" then dependency information can be moved out to the devicetree binding like so:

tx_udp {
compatible = "vnd,tx-udp", "vnd,tx";
status = "okay";
};

vnd,tx-udp.yaml:

compatible: "vnd,tx-udp"
properties:
depends-on:
type: string
default: "CONFIG_VND_TX_UDP"

This means the dependency information can be defined in a single location, instead of on each board.