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
| Path | Purpose |
|---|---|
src/sys/dts/arm64/overlays/*.dtso | Overlay 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:
- A
compatiblestring in the overlay’s new node, e.g.pine64,bcm-hostwake. - The same
compatiblestring in the driver’sprobemethod (ofw_bus_is_compatible(dev, "pine64,bcm-hostwake")). - A parent the new node attaches to. Root-level (
&{/}) means the node attaches tosimplebus. That’s the simplest path and whatbcm_hostwakeuses. - GPIO/IRQ/clock/regulator references that resolve against
labels in the base DTB. The overlay can only refer to labels that
already exist, like
&gpio0or&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
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:
/dts-v1/; /plugin/;— declares this is an overlay, not a complete DTB. The compiler emits a fragment with patches against the base./ { compatible = "rockchip,rk3399"; };— the overlay only applies to base DTBs whose root is compatible with this string. Both the PinePhone Pro and Pinebook Pro DTBs match.&{/}— patch the root node. Addingbcm-hostwake { ... }here means the new node hangs off the simplebus root and gets enumerated byofw_bus.host-wakeup-gpios = <&gpio0 4 0>— phandle to the GPIO controller, pin number, flags. The driver reads this withgpio_pin_get_by_ofw_property().pinctrl-0 = <&bt_host_wake_l &bt_wake_l>— references pinctrl groups defined in the base DTB. These set pin mux and pull at boot. The labels exist in the upstreamrk3399-pinephone-pro.dts.
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
# 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
- Referencing a label that doesn’t exist in the base DTB. The
base must export the label in its
__symbols__node. Check withdtc -I dtb /boot/dtb/your.dtb | grep __symbols__ -A300 | grep <label>. - Wrong
compatibleon the root. The overlay’s rootcompatibleis matched against the base DTB’s root; mismatch → silent skip. - Forgetting
status = "okay". Some base DTBs have nodes declareddisabled; an overlay re-enabling them needs to set the status explicitly. - GPIO bank-offset math. Rockchip pins encode as
bank * 32 + group * 8 + pin.RK_PA4on bank 0 = pin 4;RK_PD2on bank 2 =2*32 + 3*8 + 2if you’re using global numbering, or3*8 + 2 = 26if you’re using per-bank numbering with a&gpio2phandle. The driver decides which based on whatgpio_pin_get_by_ofw_propertyis given.
See also
- Essay 10 — The HOST_WAKE saga — the story behind why this overlay exists and why it doesn’t try to drive DEV_WAKE dynamically.
- Essay 6 — Goodix and the PIC methods — what happens when GPIO IRQ infrastructure is missing from the base SoC driver, and how to add the five PIC methods.