Appendix · recipe

Writing a device-tree overlay

From .dtso to a driver bound at boot, walked end-to-end with bcm-hostwake.

We use device-tree overlays — not patches to the base DTB — for every new node we add to a board. Overlays are layered at boot via fdt_overlays= in loader.conf, so we can ship one base DTB (usually unmodified from upstream Linux) and add functionality without forking it. This page is the runbook: where overlays live, how to write one, and how the kernel module that consumes the overlay finds its node.

The worked example throughout is bcm-hostwake — a tiny overlay that declares a root-level node so a custom driver can grab two GPIOs (BT HOST_WAKE and DEV_WAKE) on the PinePhone Pro. For why this overlay exists at all, see essay 10, “The HOST_WAKE saga”.

Where things live

PathPurpose
src/sys/dts/arm64/overlays/*.dtsoOverlay sources. Compiled to .dtbo by the kernel build, installed to /boot/dtb/overlays/.
src/sys/modules/<name>/Driver source for the module that binds to the overlay’s node.
sys/contrib/device-tree/src/arm64/rockchip/Linux-imported base DTBs; we don’t edit these directly.
/boot/loader.conf (on phone)fdt_overlays="bcm-hostwake.dtbo,..." to enable an overlay at boot.

The mise run patch step copies src/sys/dts/arm64/overlays/* into freebsd-src/sys/dts/arm64/overlays/. From there the kernel build picks them up automatically — there is no separate “build the overlay” step. Anything ending in .dtso under that directory becomes a .dtbo in the install image.

The four-part contract

A working overlay-plus-driver pair has four matched pieces:

  1. A compatible string in the overlay’s new node, e.g. pine64,bcm-hostwake.
  2. The same compatible string in the driver’s probe method (ofw_bus_is_compatible(dev, "pine64,bcm-hostwake")).
  3. A parent the new node attaches to. Root-level (&{/}) means the node attaches to simplebus. That’s the simplest path and what bcm_hostwake uses.
  4. GPIO/IRQ/clock/regulator references that resolve against labels in the base DTB. The overlay can only refer to labels that already exist, like &gpio0 or &bt_host_wake_l.

If any of these is missing, the symptom is the same: the driver loads, no device_t ever attaches, and devinfo -v | grep bcm-hostwake shows nothing.

The example overlay

src/sys/dts/arm64/overlays/bcm-hostwake.dtso label: bcm-hostwake
	bcm-hostwake {
		compatible = "pine64,bcm-hostwake";
		host-wakeup-gpios   = <&gpio0 4  0>;
		device-wakeup-gpios = <&gpio2 26 0>;
		pinctrl-names = "default";
		pinctrl-0 = <&bt_host_wake_l &bt_wake_l>;
		status = "okay";
	};

What each part is doing:

The driver side

src/sys/modules/bcm_hostwake/bcm_hostwake.c is a stock simplebus driver. The shape:

static int
bhw_probe(device_t dev)
{
    if (!ofw_bus_status_okay(dev))
        return (ENXIO);
    if (!ofw_bus_is_compatible(dev, "pine64,bcm-hostwake"))
        return (ENXIO);
    device_set_desc(dev, "BCM HOST_WAKE / DEV_WAKE GPIO handler");
    return (BUS_PROBE_DEFAULT);
}

The compatible string here must match the overlay byte-for-byte. ofw_bus_status_okay honors the status = "okay" line in the overlay — if you change that to "disabled", probe fails cleanly without having to remove the overlay from loader.conf.

The module’s Makefile is the standard FreeBSD kmod template:

KMOD=   bcm_hostwake
SRCS=   bcm_hostwake.c \
        device_if.h bus_if.h ofw_bus_if.h gpio_if.h gpiobus_if.h

.include <bsd.kmod.mk>

The _if.h headers are auto-generated by the build from device_if.m, bus_if.m, etc. — they let your driver call OFW_BUS_* and GPIO_* methods on its parent.

Building and deploying

╞═ add a new overlay end-to-end ═╡
# 1. Write the .dtso (in this repo, on your laptop)
$EDITOR src/sys/dts/arm64/overlays/my-thing.dtso

# 2. (optionally) write the consumer driver
mkdir -p src/sys/modules/my_thing
$EDITOR src/sys/modules/my_thing/{my_thing.c,Makefile}

# 3. Add the module to the kernel's MODULES_OVERRIDE or build standalone
ssh honor 'cd ~/pine64-freebsd/honeyguide/freebsd-src && \
  MODULES_OVERRIDE=my_thing make ...'
# Or just include it in the kernel via src/sys/conf/files.arm64

# 4. Push, pull, patch, build
git add . && git commit -m "my-thing: bring up the doohickey"
git push
ssh honor 'cd ~/pine64-freebsd && git pull'
mise run patch
mise run build-kernel

# 5. Deploy the .dtbo + the kernel
ssh honor 'cat .../my-thing.dtbo' \
  | ssh pinephone 'sudo tee /boot/dtb/overlays/my-thing.dtbo > /dev/null'
ssh honor 'cat .../kernel' \
  | ssh pinephone 'sudo tee /boot/kernel/kernel > /dev/null'

# 6. Enable the overlay in loader.conf
ssh pinephone 'sudo sysrc -f /boot/loader.conf \
  fdt_overlays+=my-thing.dtbo'

# 7. Reboot and verify
ssh pinephone 'sudo reboot'
# After it comes back:
ssh pinephone 'sysctl hw.fdt.dtb | dtc -I dtb 2>/dev/null | grep my-thing'
ssh pinephone 'devinfo -v | grep my-thing'

Verifying the overlay applied

Two sysctls are your friends:

# Dump the runtime device tree as the kernel sees it post-merge:
sysctl -b hw.fdt.dtb | dtc -I dtb 2>/dev/null

# Check that the node bound to a driver:
devinfo -v | grep -A2 my-thing

If hw.fdt.dtb shows your node but devinfo doesn’t, the overlay applied but the driver didn’t probe — check the compatible string. If neither shows it, the overlay didn’t apply — usually a typo in loader.conf or the .dtbo isn’t where loader expects it.

Common pitfalls

See also