Device tree addons: describing non-discoverable addon boards and connectors

Lots of embedded devices exist that have a connector for addon boards: BeagleBone Black Capes and Raspberry Pi Hats are just some common de facto standards, each with a whole ecosystem of base boards and addon boards that can be mixed and matched in countless ways. Most of them are based on embedded System-on-Chips, and thus use device tree to describe their hardware. Yet there is no good way to describe the connectors and addons in an efficient, modular and effective way.

At Bootlin we have been working for a few years to implement a proper way of describing such addon boards and their connectors using device tree. We have found a huge interest in the embedded systems community, but the road to get there is steep.

Our initial effort based on device tree overlays was rejected due to fundamental shortcomings in the device tree overlay design at its root. After discussing with the community we are now proposing a totally new concept: device tree addons. It’s a huge work, a lot has been done but there is still a lot to be done.

This post introduces the rationale and use cases, shows a practical example, describes how to test our current proposal and, last but not least, suggests how to contribute to the mailing list discussion to help this work land upstream for everyone’s benefit.

Goal

Besides the well-known boards such as the BeagleBone Black and the Raspberry Pi there are others, including several custom devices from the embedded industry. Some of these even allow hot-plugging and hot-unplugging the addons while the system is running, dynamically adding and removing devices at runtime. The majority of use cases we are aware of however involve only coldplug: addons can be connected and disconnected only when the system is not powered. Even though we are working to support both coldplug and hotplug, the core of this post is valid for both, being related to how the addons and connectors are described, not how they are implemented.

That said, there are various cases that need to be supported.

  • Case 1: for a given base board with a connector, any addon having a compatible connector can be connected

  • Case 2: a given addon board can be connected to any base board with a compatible connector

  • Case 3: A base board can have several connectors, of the same of different types, and to each connector an addon can be connected independently of what happens on other connectors; this includes connecting two identical addons to two compatible connectors on the same base board

The central point to support all these cases is the connector. The connector is the contract between the two sides:

  • the base board provides some resources on the connector: GPIOs, I²C and SPI busses, etc
  • the addon uses some or all those resources.

To cleanly and efficiently describe all this we need a modular approach, where:

  • a single device tree file describes a base board model and any compatible addon can be applied to it (case 1)
  • one single device tree file describes an addon model and can be used unmodified on any base board with a compatible connector (case 2)
  • an addon device tree file is usable unmodified on any connector of a multi-connector board (case 3).

So we need to decouple the base board and the addon descriptions:

  • the base board describes what it provides on the connector (not how it implements it)
  • the addon describes which resources on the connector it uses and how (not referring to how it is implemented by the base board).

What about Device Tree Overlays?

Before going further, readers could think about device tree overlays to describe the extension board. That’s exactly what we tried initially, but it didn’t work out.

The main reason overlays are not suitable for this task is that an overlay refers directly to symbols exported via labels from the base tree. Consider for example a base board with a connector that offers a GPIO line: the GPIO controller controlling the line is on the main board (e.g. gpio1) and the GPIO is used by a chip on the addon as a reset line. Using the board overlay dtso would refer to the GPIO controller  (e.g. reset-gpios = <&gpio1 ...>). This does not scale: a different base board, or a different connector on the same board, could provide the same GPIO line using a different controller (e.g. gpio2) and thus the overlay would not work.

So we need to decouple the base board and the addon, and that’s what the connector contract does.

Another problem with that approach is that the base device tree exposes all the labels to overlays in a global namespace, and overlays refer to those labels. This is  risky and error prone as it exposes a lot of information from the base tree that has nothing to do with the connector, and so it should not be accessible to addon boards and their overlay files.

Decoupling requires putting more information in the base device tree about the symbols that are meant to be exported to the addon board, and information in the overlay in order to resolve the symbols when the overlay is applied. Overlays already put similar information in special nodes (__symbols__, __fixups_ and __local_fixups__), so we proposed to add new nodes: export-symbols.

This was rejected because adding such information in nodes was a bad design even in the original device tree overlays: the device tree data (nodes and properties) indeed is meant to describe the hardware. Information to resolve symbols is something else, so we were asked to store it a new place: metadata. Metadata is stored in dtb files but not as part of the regular device tree nodes and properties. This was a fundamental addition to the device tree binary format.

Device tree addons

Our current proposal went past old device tree overlays and their limitations, and introduces a new concept: device tree addons. Addons are similar to overlay in principle, but very different in the technical details. Let’s understand them by looking at a small but realistic example.

Example hardware

Let’s assume a board vendor has designed a board with two connectors on which extension boards can be connected.

The board has two connectors, and the board vendor gives the following pinout for both — this is the connector contract:

Pin Description
Pin 1 3V3 supply
Pin 2,3 I2C SCL, SDA
Pin 4 GPIO #0 / IRQ #0
Pin 5 GPIO #1 / IRQ #1
Pin 6 GND

Here’s the base board schematics, showing how the mainboard implements the contract for each connector:

And here’s a simple addon board schematics, showing how this addon uses the features provided based on the contract. It has an EEPROM and an IO expander.

Base board description

The device tree source (.dts file) for the base board is mostly standard, except some new syntax, described below.

001  /* 3V3 Power Supply 1 */
002  regul1_3v3: regul1 {
003  	 compatible = "fixed-regulator";
004  	 ...
005  };
006  
007  /* 3V3 Power Supply 2 */
008  regul2_3v3: regul2 {
009  	 compatible = "fixed-regulator";
010  	 ...
011  };
012  
013  soc {
014  	 /* I2C 1 Controller */
015  	 i2c1: i2c@1000 {
016  	     compatible = "soc-vendor,i2c-ctrl";
017  	     ...
018  	 };
019  
020  	 /* I2C 2 Controller */
021  	 i2c2: i2c@2000 {
022  	     compatible = "soc-vendor,i2c-ctrl";
023  	     ...
024  	 };
025  
026  	 /* GPIO 1 Controller, also interrupt controller */
027  	 gpio1: gpio@4000 {
028  	     compatible = "soc-vendor,gpio-ctrl";
029  	     ...
030  	     gpio-controller;
031  	     #gpio-cells = <0x02>;
032  	     ...
033  	     interrupt-controller;
034  	     #interrupt-cells = <0x02>;
035  	     #address-cells = <0x00>;
036  	     ...
037  	 };
038  
039  	 /* GPIO 2 Controller, also interrupt controller */
040  	 gpio2: gpio@5000 {
041  	     compatible = "soc-vendor,gpio-ctrl";
042  	     ...
043  	     gpio-controller;
044  	     #gpio-cells = <0x02>;
045  	     ...
046  	     interrupt-controller;
047  	     #interrupt-cells = <0x02>;
048  	     #address-cells = <0x00>;
049  	     ...
050  	 };
051  };
052  
053  conn_a: conn-a {
054  	 compatible = "myvendor,myconnector";
055  
056  	 /*
057  	  * Nexus node definition for GPIOs
058  	  *
059  	  * Map:
060  	  *  - gpio 0 at connector to gpio1 10
061  	  *  - gpio 1 at connector to gpio2 3
062  	  */
063  	 #gpio-cells = <2>;
064  	 gpio-map-mask = <0xf 0x0>;
065  	 gpio-map-pass-thru = <0x0 0xf>;
066  	 gpio-map = <0 0 &gpio1 10 0>,
067  		    <1 0 &gpio2 3 0>;
068  
069  	 /*
070  	  * Nexus node definition for interrupts
071  	  *
072  	  * Map:
073  	  *  - irq 0 at connector to the gpio1 irq 10
074  	  *  - irq 1 at connector to the gpio2 irq 3
075  	  */
076  	 #address-cells = <0>;
077  	 #interrupt-cells = <2>;
078  	 interrupt-map =
079  	     <0 IRQ_TYPE_LEVEL_LOW &gpio1 10 IRQ_TYPE_LEVEL_LOW>,
080  	     <0 IRQ_TYPE_LEVEL_HIGH &gpio1 10 IRQ_TYPE_LEVEL_HIGH>,
081  	     <1 IRQ_TYPE_LEVEL_LOW &gpio2 3 IRQ_TYPE_LEVEL_LOW>,
082  	     <1 IRQ_TYPE_LEVEL_HIGH &gpio2 3 IRQ_TYPE_LEVEL_HIGH>;
083  
084  	 /* The connector itself */
085  	 /export/ conn: &conn_a;
086  
087  	 /* The 3v3 at Connector A: 3V3 Power Supply 1 */
088  	 /export/ supply_3v3: &regul1_3v3;
089  
090  	 /* The I2C at Connector A: I2C 1 Controller */
091  	 /export/ i2c:	&i2c1;
092  
093  	 /*
094  	  * Even if the exported 'conn' symbol is enough,
095  	  * make things clear and export a specific symbol
096  	  * for GPIOs pointing to the nexus node (i.e. the
097  	  * connector itself).
098  	  */
099  	 /export/ gpios: &conn_a;
100  
101  	 /*
102  	  * As for GPIOs, make things clear and export a
103  	  * specific symbol for interrupts.
104  	  */
105  	 /export/ irq: &conn_a;
106  };
107  
108  conn_b: conn-b {
109  	 compatible = "myvendor,myconnector";
110  
111  	 /*
112  	  * Nexus node definition for GPIOs
113  	  *
114  	  * Map:
115  	  *  - gpio 0 at connector to gpio2 5
116  	  *  - gpio 1 at connector to gpio2 6
117  	  */
118  	 #gpio-cells = <2>;
119  	 gpio-map-mask = <0xf 0x0>;
120  	 gpio-map-pass-thru = <0x0 0xf>;
121  	 gpio-map = <0 0 &gpio2 5 0>,
122  		    <1 0 &gpio2 6 0>;
123  
124  	 /*
125  	  * Nexus node definition for interrupts
126  	  *
127  	  * Map:
128  	  *  - irq 0 at connector to the gpio2 irq 5
129  	  *  - irq 1 at connector to the gpio2 irq 6
130  	  */
131  	 #address-cells = <0>;
132  	 #interrupt-cells = <2>;
133  	 interrupt-map =
134  	     <0 IRQ_TYPE_LEVEL_LOW &gpio2 5 IRQ_TYPE_LEVEL_LOW>,
135  	     <0 IRQ_TYPE_LEVEL_HIGH &gpio2 5 IRQ_TYPE_LEVEL_HIGH>,
136  	     <1 IRQ_TYPE_LEVEL_LOW &gpio2 6 IRQ_TYPE_LEVEL_LOW>,
137  	     <1 IRQ_TYPE_LEVEL_HIGH &gpio2 6 IRQ_TYPE_LEVEL_HIGH>;
138  
139  	 /* The connector itself */
140  	 /export/ conn: &conn_b;
141  
142  	 /* The 3v3 at Connector B: 3V3 Power Supply 2 */
143  	 /export/ supply_3v3: &regul2_3v3;
144  
145  	 /* The I2C at Connector B: I2C 2 Controller */
146  	 /export/ i2c:	&i2c2;
147  
148  	 /* The GPIO nexus node */
149  	 /export/ gpios: &conn_b;
150  
151  	 /* The interrupt nexus node */
152  	 /export/ irq: &conn_b;
153  };

The first part of the device tree is nothing special or new, it’s classic device tree code:

  • Lines 1-11 describe the power supplies on the base board
  • Lines 13-51 describe the SoC components used to provide the I²C busses and GPIO/interrupt lines to the connectors

Things get interesting with the following two nodes, each describing one of the connectors. Let’s look at the conn-a node.

Lines 56-67 define a nexus node for GPIOs. This is not a new concept, it is already well-defined and used. The GPIOs at the connector are part of the contract, and as such they need a well-defined GPIO number for all instances of the connector. In our case we have GPIO #0 / IRQ #0 on pin 4 and GPIO #1 / IRQ #1 on pin 5, for both connectors. But GPIO #0 is provided by GPIO 10 of the gpio1 controller for connector A, while for connector B GPIO #0 is provided by GPIO 5 of the gpio2 controller. The nexus node allows the addon board to reference GPIO #0 and GPIO #1 of the connector, without needing to know the implementation details of each base board the addon can be connected to, nor of each connector of the same board. The “translation” will be done later.
So this is really describing how the base board is providing a feature to satisfy the connector contract.

Lines 69-82 define a nexus node for using the same GPIO pins as interrupts, so it is similar to the GPIO nexus node.

With the following lines it is time to introduce a proposed new DTS keyword, /export/!

Line 85 exports the connector node using the new /export/ keyword. This means the base board offers some features at the connector (the GPIOs and interrupts, via nexus nodes) via this node.
The syntax is /export/ <name>: <ref>; where:

    • <name> (conn in the example) is the symbol name exposed to the addon. It is part of the contract between the base board and the addon board.
    • <ref> (&conn_a in the example) is the reference to the node linked to the symbol. It is base-board-specific and not usable by the addon.

Line 88 uses again the /export/ keyword to export the regulator providing the power supply to the addon. This lets chips on the addon reference the regulator that provides voltage to them, for example to turn on and off the correct power supply when the addon chips need it.

Line 91 similarly exports the I²C bus that the base board connects to connector A, which is i2c1.

Lines 94-105 are not strictly needed, but allow using the GPIOs and interrupts on the addon with a clearer description. Indeed, referencing the connector with the conn symbol exported at line 136 is sufficient to reference the Nexus node from the addon. However having dedicated gpios and irq symbols make things clearer when such GPIOs or IRQs are referenced from the addon.

That’s all for connector A. As you can see it only exports the few symbols needed to expose to the addon the features provided on the connector pins. Other resources on the base board, not wired to the connector, won’t be accessible to the addon.

Lines 108-153 describe connector B in node conn_b. You can easily see it has the exact same structure as conn_a:

  • The exported names (the connector contract) are the same: the number of GPIOs and interrupts, and the conn, supply_3v3, i2c, gpios and irq names. This allows an addon device tree to be attached wither to conn_a or conn_b.
  • They way those exported symbols are provided however are different. For example, i2c is provided by i2c2 for connector B, not by i2c1.

With the base board described, and most notably the connector contract defined thanks to the new /export/ keyword, we can now describe the addon board.

Addon board description

Similarly to device tree overlays .dtso files, we are using a new device tree file format for addons, having the .dtsa extension and some new keywords and syntax.

Unlike device tree overlays, a device tree addon is always applied to a single node of the underlying device tree. That’s the connector node. This is a natural description of the hardware: an addon board is physically connected to a connector and can only access resources wired to that connector. The device tree addon description maps the physical reality.

Here’s how our simple addon can be described in a dtsa file:

001  /*
002   * Import only the connector and use namespace
003   * symbol references
004   */
005  /import/ conn: "";
006  
007  &conn.i2c {
008  	 io-expander@20 {
009  	     compatible = "foo,io-expander";
010  	     reg = <0x20>;
011  	     vcc1-supply = <&conn.supply_3v3>;
012  	     ...
013  	     reset-gpios = <&conn.gpios 1 GPIO_ACTIVE_LOW>;
014  	     interrupt-parent = <&conn.irq>;
015  	     interrupts = <0 IRQ_TYPE_LEVEL_LOW>;
016  	 };
017  
018  	 eeprom@52 {
019  	     compatible = "foo,eeprom";
020  	     reg = <0x52>;
021  	     ...
022  	 };
023  };

Line 5 is using the new /import/ keyword to import a symbol from the connector. This is the counterpart of the /export/ keyword.

The syntax is: /import/ <name>: <compatible>; where:

  • <name> is the symbol name used by the addon. It is part of the contract between the base board and the addon board.
  • <compatible> is a compatible string that can be used during symbol resolution to identify the kind of node pointed to by the symbol name. It can be set as an empty string for the common cases (see below).

Imported symbols have to match symbols exported by the node to which the addon will be applied (the connector node).

Using the same name on both sides is the simple way to perform this match, but some use cases have been identified where names are not sufficient. Attaching a compatible string to imported symbols helps with the matching criteria. For instance, we can imagine /import/ foo_bus: "bus,i2c"; to state that foo_bus has to be an I2C bus.

Furthermore, this compatible string could also be used in device tree checks against bindings. Based on the compatible strings of imported symbols, the use of the symbol by the addon could then be checked.

Line 7 starts the decription of what is on the I²C bus in the addon, and it does so introducing a new syntax: namespaced symbols. The &conn.i2c string intuitively reads: a phandle referring to the I²C bus exposed by the connector node, and that’s correct in the practice. It works thanks to the /export/ i2c: &...; line in the connector description, which exports the i2csymbol within the connector symbol namespace, because it is inside the connector node.

This mechanism allows specific parts of what the connector contract exposes, like specific busses and pins, to be imported by the addon with a concise and readable syntax.

Lines 8-16 define the IO expander on the addon board. It is a standard device tree description for an I²C device. What’s relevant here is that it accesses resources provided by the base board over the connector, again using namespaced symbols: &conn.supply_3v3 and &conn.irq to access exported nodes, and &conn.gpios 1 GPIO_ACTIVE_LOW to access a GPIO exposed by the GPIO nexus node.

Lines 18-22 finally describe the EEPROM on the I²C node. There is nothing new here compared to regular device tree syntax.

Try it yourself

Our current work is currently focused on the new file formats and the libfdt/dtc code, and is visible on the devicetree-compiler mailing list, in these two patch series:

To allow you easily test our current work we created a fork of the dtc git repository with the two patch series applied. Follow these instructions to try it out.

Fetch the code and compile

All the needed modifications in dtc/libfdt are available on the Bootlin fork of the dtc repository. Here’s how to get the code and test it.

# Get the source
$ git clone https://github.com/bootlin/dtc.git
$ cd dtc
# Use the code corresponding to the latest series sent upstream
# (i.e. the bootlin/support-for-addon-rfc-v2 branch)
$ git checkout -b support-for-addon-rfc-v2 \
                  origin/bootlin/support-for-addon-rfc-v2
# Setup the build directory
$ meson setup builddir/
# Compile
$ meson compile -C builddir/
# Run internal tests (if you want to)
$ meson test -C builddir/

The compiled binaries are available in the builddir directory. To be sure everything is correct, you can check the dtc version:

$ ./builddir/dtc --version
Version: DTC v1.8.1-108-gc68038e

Apply an addon dtsa file

Several base dts and addon dtsa files are available in the tests directory.
They are used for the internal tests, but fdtaddon_realistic_base.dts (base board description) and fdtaddon_realistic_addon.dtsa (extension board description) could be interesting as a starting point.

Compile dts and dtsa files (the syntax is exactly the same for both):

$ ./builddir/dtc -I dts -O dtb -o /tmp/base.dtb \
                 ./tests/fdtaddon_realistic_base.dts
$ ./builddir/dtc -I dts -O dtb -o /tmp/addon.dtba \
                 ./tests/fdtaddon_realistic_addon.dtsa

Benefits of the new metadata

These device tree blobs can be converted back to dts to understand what was done… with a little surprise! Let’s have a look at the output of deciding the main device tree blob:

$ ./builddir/dtc -I dtb -O dts /tmp/base.dtb

In  the output you may notice:

/dts-v1/;

/ {
    regulator {
        compatible = "regulator-fixed";
        gpios = <&{/soc/gpio@2000} 0x05 0x00>;
        ...

Here properties using a phandle value is converted back using a reference by path (&{/soc/gpio@2000}) instead of a number as you would see with current device tree code. This is a benefit of the proposed addition of metadata to  the dtb file.

You can also notice the /export/ed symbols are clearly visible in the decoded device tree, again thanks to the new metadata:

    connector-a { 
       /export/ connector: &{/connector-a}; 
       /export/ conn_i2c: &{/connector-a/conn-i2c}; 
       /export/ conn_5v: &{/regulator}; 
       ...
    };

Now let’s decode the addon dtba file:

$ ./builddir/dtc -I dtb -O dts /tmp/addon.dtba

In the decoded output you can see both the imported symbols and the readable phandles as before, and namespaced symbols are decoded in a readable manner as well:

/import/ connector: "abc,foo-connector"; 
/import/ conn_5v: ""; 

&connector { 
   conn-i2c { 
       gpio@23 { 
           vcc1-supply = <&conn_5v>; 
           vcc2-supply = <&connector.conn_5v>; 
           reset-gpios = <&connector 0x01 0x00>; 
           ...
       }; 
   }; 
};

Even more info from fdtdump

To get even more information decoded from the dtb and dtba files, the fdtdump debug tool has been extended as well to show the metadata present in the new binary format. For the curious:

$ ./builddir/fdtdump /tmp/base.dtb
$ ./builddir/fdtdump /tmp/addon.dtba

The new fdtaddon tool

To apply an addon device tree blob (dtba) we propose a new tool: fdtaddon

$ ./builddir/fdtaddon --help
Usage: apply an addon to a base blob
    fdtaddon

Options: -[i:o:t:e:vhV]
  -i, --input   Input base DT blob
  -o, --output  Output DT blob
  -t, --target  Target node
  -e, --export  Custom export symbol
    Multiple --export or -e can be present in order to export
    multiple symbols.
    The following syntaxes are supported:
      - Defines an export symbol pointing to a node using its
        symbol
          --export '=&'
      - Defines an export symbol pointing to a node by path
          --export '=&{path_to_node}'
  -v, --verbose      Verbose messages
  -h, --help         Print this help and exit
  -V, --version      Print version and exit
$

Let’s apply the generated addon.dtba to base.dtb. Remember an addon is applied to a specific node in the base tree, which is the connector node. A base board could have more than one, so we need to point to the desired one.

In the example base device tree the connector is available at the /connector-a node, so we can apply the addon to that connector node:

$ ./builddir/fdtaddon -i /tmp/base.dtb -o /tmp/merged.dtb \
                      -t "/connector-a" /tmp/addon.dtba

The resulting dtb is the merged.dtb file. This file can be converted back to dts to be reviewed:

$ ./builddir/dtc -I dtb -O dts /tmp/merged.dtb

The resulting merged.dtb file describes the base board with the addon connector to Connector A.

When can I use it for my project? (aka: roadmap and current status)

The new features we proposed, including a new .dts syntax and a new binary format version, touch a foundational component so many embedded systems rely on in the industry. Should you suspect this is not a short and easy path, you’d be right.

We presented our initial proposal at Linux Plumbers Conference 2024, an improved prototype on mailing lists in May 2025, which got discussed with the community during ELCE 2025 and a follow-up mailing discussion. That old approach has been rejected, because it was based on device tree overlays. The discussion led to the current idea.

Right now we are working to the foundation of the new idea in the libfdt/dtc code, which we are presenting here.

The two series mentioned above implement all of what is mentioned in this blog post: the additions to the dts syntax, the extension to the binary blob format and the new dtsa file format. Now discussion is in progress to agree on the new source and binary formats as well as the implementation details.

Once the source and binary format will have been agreed on, the future steps in the roadmap include:

  • Update the Device Tree Specification with the new dts  keywords and new dtb tags.
  • Minimal support in U-Boot to parse the new v18 DTB format: this is quite simple, U-Boot just needs to know about the new binary format and ignore the new optional tags;
  • Addon support in the Linux kernel: this will allow the kernel to apply an addon file at runtime, supporting both coldplug and hotplug use cases; this is more complex than the U-Boot work but we can leverage the experience achieved while working on our previous prototypes;
  • Other bootloaders and operating systems are not on our roadmap, but are doable after the previous ones establish the base practice.

Clearly the approval on the new syntax and dtb format is a blocking step before having any chance of Linux or any other software to use the new format. That’s where your  🫵 feedback can help our work move forward!

Follow the above instructions to test the new features using the fdtaddon tool, then reply to the main series and let the community know; you can give your Tested-by: to both series if it works or your use case. Writing real addon dts and dtsa files for your board and checking the merged dtb works on the hardware would be awesome!

Remember, this is a community work and community feedback is fundamental, especially for changes at the foundational bricks of the ecosystem.

Upcoming discussion opportunities: conferences next October in Prague

Community discussion does not stop at mailing lists. Experience tells complex topics are better discussed face to face.

For this reason I will be participating to both the Linux Plumbers Conference 2026 and the Embedded Linux Conference Europe 2026 next October in Prague. These are two great opportunities for discussion with the community.

And because this topic is very complex and needs discussion on various aspects, I will be leading a discussion session during Linux Plumbers Conference: Device Tree Addons: Describe hot-pluggable extension boards.

If you are going at one of these conferences and you have any comments, questions or use cases to discuss, but cannot attend the discussion session, don’t hesitate to contact me in advance or grab me in the hallway.

Conclusion

Concluding, the use case of extension boards connected to connectors available on different base boards is a common one in the embedded world, and the industry and communities have long been waiting for a solution that is not yet available.

The device tree addon work presented here offers a well-structured solution covering even complex use cases in a clean and effective way. The road is still long but with community involvement progress can happen in the near future, to hopefully provide a fully upstream solution in the near future.

Again, you can help by providing your feedback: don’t hesitate to report your feedback and use cases in reply to the main patch series.

Leave a Reply