Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 292 additions & 0 deletions src/docs-guides/device-tree-overlays.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
---
sidebar_position: 6.5
title: 'Device-tree overlays'
copy_markdown: true
draft: true
description: 'Declare a device-tree overlay in an extension and let avocado build compile it in the SDK and deliver it to the boot medium - no manual dtc, no boot-partition surgery, no BSP fork.'
---

:::danger draft - Jetson only, and not yet reproducible by a reader

**NVIDIA Jetson (Tegra) is the one target this page documents, and the only one proven on hardware.** Other targets are not covered here: their delivery paths have not been confirmed on a board, and this page does not describe a mechanism nobody has watched work.

The guide also documents behavior that is **not in a released Avocado CLI or SDK image**. Every command below has been run, but only against from-source builds of unmerged branches. Following it with released artifacts will fail at `avocado install`.

:::

A device-tree overlay is a small patch to your board's hardware description: enable a SPI bus, wire up a GPIO, add a sensor the base device tree does not know about. Traditionally that means compiling a `.dtbo` by hand with `dtc` and finding somewhere on the boot partition to put it - a per-board manual step that has to be repeated on every image.

Avocado makes the overlay a declared part of the image. An extension names the overlays it ships, and `avocado build` compiles each one in the SDK and hands it to a per-board delivery step that knows where the blob belongs on that board's boot medium.

This guide covers:

- Declaring overlays in `avocado.yaml`
- Writing an overlay source the compiler will accept
- Building and flashing on a Jetson, the worked example throughout
- Confirming on a booted board that the overlay actually applied

## Target support

The declaration is portable; the delivery mechanism is not. Each BSP layer installs its own delivery hook, so what happens to the compiled blob differs by board - including how you ship a _change_ to an overlay later.

| Target | Status | Delivery mechanism | Updating an overlay |
| ------------------------------------------- | ---------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------- |
| `jetson-orin-nano-devkit` (and other Tegra) | **Working** - verified on hardware | Merged into the base DTB at build time with `fdtoverlay` | **Reflash** - see [Jetson](#jetson-tegra) |
| `qemux86-64` | Not applicable | None - x86 boots via ACPI and has no device tree | n/a |
| Any other target | Not documented yet | Varies by BSP | Build fails by design - see [below](#unsupported-targets) |

The Jetson row has been confirmed by reading the device tree of a running kernel on the board. Other boards will be documented as each one is verified the same way - see [Confirming the overlay applied](#confirming-the-overlay-applied) for why a green build is not evidence that an overlay reached a kernel.

:::info the declaration is the portable part
The same `device_tree_overlays` block and the same `.dtso` move between boards unchanged. Only the hook underneath differs, which is the point of the hook being per-BSP.
:::

## Declaring an overlay

Overlays are declared on the **extension** that ships them, not on the runtime and not on a package:

```yaml
runtimes:
dev:
target: jetson-orin-nano-devkit
extensions:
- my-board
packages:
avocado-runtime: '*' # required - see the note below

extensions:
my-board:
types:
- sysext
device_tree_overlays:
- name: hello-overlay
src: overlays/hello-overlay.dtso
```

| Key | Required | Meaning |
| -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | yes | Authoritative. It is the compiled blob's basename (`<name>.dtbo`) and, on the targets that select overlays at boot, the selection argument. Must be a safe basename: no `/`, no whitespace, not `.` or `..`. Names must be unique across the whole runtime - a duplicate is a hard error, not last-one-wins. |
| `src` | yes | Path to the overlay source, relative to the project root. |
| `params` | no | A mapping of per-overlay parameters. Accepted by the schema, but no hook on a documented target consumes it - the Jetson hook ignores it silently. Encode the values in the `.dtso` instead. |

:::warning declare `avocado-runtime`
The runtime must pull `avocado-runtime`, which is what installs `avocado-img-bootfiles` - the package shipping `u-boot.bin` and the `bootfiles/*` entries the bundler needs to build a boot image. A project that omits it fails late, at bundling, with `File 'u-boot.bin' not found in any input directory for FAT image`, which reads like a broken BSP rather than a missing declaration. It cost us two weeks of chasing a product bug that was a project-config omission.
:::

The feature is inert unless an overlay is declared. A project with no `device_tree_overlays` anywhere builds exactly as before.

## Writing the overlay source

The source must be a real overlay, not a full device tree. Two rules the compiler enforces:

**It must declare `/plugin/;`.** Without it, `dtc` happily compiles a complete device tree instead and you get a blob nothing will apply. The build rejects this up front.

**It must materialize at least one fragment** - a top-level node with an `__overlay__` child. Both the `&label { }` shorthand and the explicit `fragment@N` form produce this.

A path-targeted overlay, which works on any board:

```dts
/dts-v1/;
/plugin/;

/ {
fragment@0 {
target-path = "/";
__overlay__ {
hello-overlay {
compatible = "acme,widget";
status = "okay";
};
};
};
};
```

A label-targeted overlay, which is what you usually want on real hardware:

```dts
/dts-v1/;
/plugin/;

&spi0 {
status = "okay";

sensor@0 {
compatible = "acme,sensor";
reg = <0>;
spi-max-frequency = <12000000>;
};
};
```

:::caution label targets need `__symbols__` in the base tree
`&spi0` resolves through the base device tree's `__symbols__` node, which only exists if the BSP compiled its DTB with `dtc -@`. Most vendor BSPs do. If the label cannot resolve, a Jetson **fails the build** - `fdtoverlay` errors and the hook treats that as fatal rather than flashing an unmerged tree. On a board whose overlays are selected at boot instead, the same unresolved label is silent, and you get a board that boots without the overlay. Use `target-path` when you are unsure.
:::

### Using `dt-bindings` macros

If your source `#include`s kernel headers, the wrapper preprocesses it with `cpp` exactly as the kernel build does (`-nostdinc -undef -x assembler-with-cpp -D__DTS__`) against the kernel's `dt-bindings` tree:

```dts
#include <dt-bindings/gpio/gpio.h>

/dts-v1/;
/plugin/;

&gpio {
my-pin {
gpios = <&gpio 17 GPIO_ACTIVE_HIGH>;
};
};
```

This requires `kernel-devsrc` in the target sysroot. It is installed automatically when the SDK is provisioned for a project declaring overlays. An overlay with no `#include` skips the preprocessor entirely and needs no kernel sources.

A `/include/ "shared.dtsi"` (the native DTS include, not a cpp one) resolves relative to your source file and does **not** pull in the preprocessor.

## Building

```bash
avocado install
avocado build --target jetson-orin-nano-devkit
```

The build logs three ordered markers. All three must appear:

```text
avocado-dtc-overlay: built .../device-tree-overlays/hello-overlay.dtbo from hello-overlay
device-tree-overlay-deliver: delivered 1 overlay(s) merged into tegra234-p3768-0000+p3767-0005-nv-super.dtb for avocado-jetson-orin-nano-devkit: hello-overlay
[SUCCESS] OS bundle created: .../os-bundle.aos
```

The middle line is the one that differs per target: on Jetson it names the base DTB the overlays were merged into, and on the loose-`.dtbo` targets it names the boot directory they were copied to instead.

To inspect what was produced, `avocado save` exports the build state; the archive holds both the compiled `device-tree-overlays/<name>.dtbo` and the finalized `os-bundle.aos`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avocado save requires --output: Usage: avocado save [OPTIONS] --output <OUTPUT> (docs-guides/avocado-cli/commands.md:334). Run as written it exits with a usage error.

Suggest avocado save -o build-state.tar.zst, matching the form already used in docs-changelog/april-2026/0.34.0.md:14.


Then flash the board. **Do this host-side, not through `avocado runtime provision`** - container-mode provisioning cannot currently reach a Jetson in recovery mode, which is a known gap. Every result on this page was obtained from a host-side flash.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts getting-started/jetson.md:92, which prescribes avocado provision -r dev --profile tegraflash for this same board and states that the tegraflash tooling "runs inside the Avocado SDK container." As of this PR the two guides give opposite provisioning advice for the Orin Nano.

Two separate problems:

  1. If container-mode provisioning genuinely cannot reach a Jetson in recovery mode, then the getting-started guide documents a path that does not work, and that is the more urgent fix — it is the page a new reader lands on first.
  2. This sentence tells the reader not to use the documented path but names no replacement. Flashing is the one action required between avocado build and every verification step below, and there is no command and no link for it here.

Suggest naming the host-side procedure explicitly (or linking it), and reconciling with getting-started/jetson.md in the same change so the two pages do not disagree on main.


### On a brand-new SDK

The SDK does not yet ship the image-build toolchain, so a clean one needs it declared explicitly:

```yaml
sdk:
packages:
nativesdk-erofs-utils: '*'
nativesdk-btrfs-tools: '*'
nativesdk-zstd: '*'
nativesdk-stone: '*'
avocado-sdk-target: '*'
```

## Confirming the overlay applied

**A green build is not evidence that an overlay is applied.** This is the single most important thing on this page. Every build-path marker above can pass - the source compiles, the right blob is delivered, the bundle finalizes, the flash reports success - while the running kernel uses a device tree your overlay never touched. That exact failure happened on Jetson and went undetected for two full build rounds.

The only evidence that counts is the tree the kernel is actually running. On the booted board:

```console
# ls /proc/device-tree/
# tr -d '\0' < /proc/device-tree/hello-overlay/compatible
acme,widget
```

`/proc/device-tree` is the live tree after every stage the bootloader applied, so a node present there is a node in effect.

:::caution you need a shell first
A raw image leaves root's password field in `/etc/shadow` as `*`, which no password can match, and this check is only readable from a shell on the board.

`avocado init` already solves it. The project it generates carries a `permissions` profile that gives root an empty password, and points the rootfs at it:

```yaml
rootfs:
permissions: dev

permissions:
dev:
users:
root:
password: ''
```

That is enough to log in on the serial console. If you started from `avocado init` you have it already; if you hand-wrote your `avocado.yaml`, this is the piece to add. **Verified on a Jetson Orin Nano**: with this profile the flashed rootfs carries `root::` and the console gives a root prompt.

For a shell over the network instead, add the SSH extension and your own public key. The extension configures sshd to read `/var/lib/ssh/authorized_keys`; `var_files` is what puts a key there, and neither half is useful alone:

```yaml
runtimes:
dev:
extensions:
- avocado-ext-sshd-dev
- avocado-bsp-{{ avocado.target.board }} # NIC driver lives here
var_files:
- source: 'files/authorized_keys'
dest: 'lib/ssh/'
```

Do not drop the BSP extension to slim a test project. On a Jetson Orin Nano it carries `kernel-module-realtek`, and without it the board boots with no ethernet interface at all - so there is nothing to SSH to, and the cause looks nothing like a missing extension.

**Verified on a Jetson Orin Nano**: the key lands root-owned at `/var/lib/ssh/authorized_keys` mode `0644`, which is what `StrictModes` requires, and `ssh -o BatchMode=yes root@<board>` connects. `BatchMode` refuses password and keyboard-interactive auth, so a connection under it is proof the key itself was accepted rather than a password prompt quietly succeeding behind it.

Expect the host key to change on every reflash. Host keys are generated on first boot rather than shipped, so the entry your client recorded for that address is stale the moment you reflash, and SSH reports it as a possible man-in-the-middle rather than as a new board. Replace the recorded entry instead of adding to it - an append leaves the old key in place and the failure persists.

The permissions profile, the SSH extension and the key are all **dev-only and must never ship**. That means the image you verify is not byte-for-byte the image you ship. Acceptable here, because none of them touches the device tree - but it is the reason to keep the difference to exactly these declarations and rebuild without them before shipping.
:::

### Run the negative control

One positive reading is not enough on its own: a node that was in the base tree all along looks identical to one an overlay added. Build the same project a second time with the `device_tree_overlays` block removed and nothing else changed, boot it on the same board, and confirm the node is **absent**:

```console
# tr -d '\0' < /proc/device-tree/hello-overlay/compatible
sh: can't open /proc/device-tree/hello-overlay/compatible: no such file
```

With both halves, the declaration is the only variable that can explain the difference. Do this once per platform you ship on - the delivery mechanism differs per board, so a pass on one proves nothing about another.

## Per-target details

### Jetson (Tegra)

Tegra has nowhere to put a loose `.dtbo`. The flashed partition set has no DTBO partition, the ESP carries only the bootloader, and the overlay lists that do exist in the Tegra flash variables are read by the classic `flash.sh` path - not by the `initrd-flash` flow Avocado uses, which consumes binaries already signed at build time. Anything written to those lists is inert.

So the hook merges the declared overlays into the board's base DTB at build time with `fdtoverlay` and publishes the merged tree as the stone manifest's `storage_devices.rootdisk.images.dtb`. That merged DTB is what gets flashed.

Two consequences:

**Changing an overlay requires a reflash, not an image update.** The overlay is baked into a signed DTB living in a flash partition, so it only moves through the provisioning path. This is the one place the platforms genuinely diverge for a user, and it belongs in your own deployment planning.

**Declaration order is load-bearing.** `fdtoverlay` applies overlays in the order declared, so an overlay targeting a node that an earlier overlay created must come after it.

The base DTB is selected by the `DTBFILE` the BSP records, because a real Tegra BSP ships one base DTB per module SKU - five on an Orin Nano dev kit. You do not choose it; the board's own flash configuration does.

## Unsupported targets

Declaring an overlay on a target whose BSP ships no delivery hook is a **hard error**, not a silent skip:

```text
[ERROR] device-tree overlays are declared but this target provides no delivery hook.
Expected an executable at .../usr/libexec/avocado/device-tree-overlay-deliver
```

The same applies one level deeper: if the hook runs but does not claim an overlay, the build fails naming it. A declared overlay either reaches the boot medium or fails the build - it never quietly disappears into an image that looks fine.

## Troubleshooting

| Symptom | Cause | Fix |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `source is not a device-tree overlay (missing '/plugin/;')` | Source is a full device tree | Add `/plugin/;` after `/dts-v1/;` |
| `device-tree overlay name '<n>' is not a valid basename` | `name` has a `/`, whitespace, or is `.`/`..` | Use a plain basename; it becomes a filename and a boot-loader argument |
| `device-tree overlay name '<n>' is declared by both '<a>' and '<b>'` | Two extensions on one runtime used the same name | Rename one; names are runtime-global |
| `this target provides no delivery hook` | Target's BSP has no hook (see the support matrix) | Remove the declaration for that target, or add a hook to the BSP layer |
| `device-tree overlays staged but not delivered by the BSP hook` | Hook ran but did not claim the overlay | A BSP bug - report it with the build log; the build correctly refused to ship a silently-dropped overlay |
| `no dt-bindings under <dir>` | Source `#include`s kernel headers, `kernel-devsrc` missing or incomplete | Confirm the SDK was provisioned for a project declaring overlays; a no-`#include` overlay needs neither |
| `fdtoverlay failed to merge` (Jetson) | Overlay targets a label or path absent from the base DTB | Check the target resolves in that board's base tree; prefer `target-path` when unsure |
| `File 'u-boot.bin' not found in any input directory for FAT image` | Runtime does not declare `avocado-runtime` | Add `packages: avocado-runtime: '*'` to the runtime |
| Build fully green, node absent from `/proc/device-tree` | Overlay was delivered but not applied at boot | Run the negative control and report it |

## What's next

- [Custom kernel](./custom-kernel) - bring your own kernel tree, including the vendor trees that ship board device trees
- [Provisioning](./provisioning) - writing a built image to a board
- [Hardware-in-the-loop development](./hardware-in-the-loop) - iterating against a live device
5 changes: 5 additions & 0 deletions src/sidebars-guides.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const sidebars = {
items: [
'cross-compilation',
'custom-kernel',
// 'device-tree-overlays' belongs here, after custom-kernel, but stays
// unlisted while that page carries `draft: true`. Drafts are dropped
// from the production build, and a sidebar item naming a dropped doc
// fails the build. Add it in the same commit that flips the draft flag
// (tracked as item 7 of that page's "Release status" table).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cites "item 7 of that page's 'Release status' table," but no such table exists. It was moved into the PR description, where it is a 6-item list and the sidebar entry is item 6. The cross-reference points at nothing the moment this merges.

The larger issue is that this comment is the only durable in-repo trace of the release gates. Everything that has to happen before the draft flag flips — avocado-cli#183, meta-avocado#292, meta-avocado#291, publishing avocadolinux/sdk:2024-edge, publishing the 2024/edge feed with avocado-dtc-overlay-deliver, cutting a CLI release — lives in a PR body, which is not discoverable from the repo afterward and is not something anyone will re-read. Whoever picks this up in three months has a code comment pointing at a missing table and no list.

Suggest filing a tracking issue with those gates and pointing this comment at the issue instead. Also worth noting here that publishing device_tree_overlays in schemas/avocado-config.json is a gate too and is absent from the current list — the guide documents a key the site's own schema page rejects.

'seeding-var',
'customizing-rootfs-initramfs',
'lockfiles-and-build-stamps',
Expand Down
Loading