Skip to content

gus/trellis-radio

Trellis Radio

Trellis Radio turns an ESP32C6 into a radio co-processor for a Linux single-board computer, connected over SDIO. The ESP32C6 does the RF work; a Linux kernel driver exposes it through standard Linux interfaces - netdevs, an HCI controller, a wpan device - instead of a custom API.

The project is two halves that grow together: firmware/ (ESP-IDF, for the ESP32C6) and driver/ (a Linux kernel module), sharing wire-format headers in common/. See docs/architecture.md for how they fit together.

Features

  • WiFi monitor mode: promiscuous 802.11 capture, delivered as radiotap over a standard monitor netdev (iw, tcpdump, Wireshark). Receive-only - no packet injection.
  • WiFi station mode: the ESP32 runs the 802.11 client stack itself (association, WPA2/WPA3); the host sees decrypted Ethernet on a normal STA netdev, driven by wpa_supplicant/NetworkManager via cfg80211.
  • WiFi access point mode: ESP32-hosted SoftAP, driven by hostapd via cfg80211, with open/WPA2/WPA3/transition profiles and per-client stats.
  • BLE: ESP32 is controller-only; BlueZ runs the full host stack on Linux. HCI is relayed verbatim over a VHCI-style hci_dev.
  • IEEE 802.15.4: raw radio exposed as a Linux mac802154 device (wpan0, iwpan/wpan-tools). No Zigbee/Thread network layer yet, but the protocol leaves room for it.

Use case

For a Linux SBC with no radios of its own (or not enough), gained through a single cheap SoC, while still looking like a normal Linux wireless system to everything above the driver - no bespoke radio API to integrate against.

Repository layout

  • firmware/ - ESP-IDF project for the ESP32C6
  • driver/ - Linux kernel module
  • common/ - shared protocol headers, plus host-side tests in common/tests/
  • docs/ - architecture, protocol, buildroot/device-tree, and per-mode guides
  • scripts/ - flash/monitor helpers and hardware test scripts

Getting started

Firmware (ESP-IDF v6.0.2):

cd firmware
idf.py --preset production build flash monitor

Kernel module, standalone out-of-tree build:

make -C driver KDIR=/path/to/linux/build ARCH=<arch> CROSS_COMPILE=<prefix>

Add HITL=1 to build in the debugfs test surface (production builds omit it entirely).

Buildroot package configuration

The intended deployment path is as a Buildroot package, built straight from this repo's git history rather than a local checkout. Add package/trellis_radio/Config.in:

config BR2_PACKAGE_TRELLIS_RADIO
bool "trellis_radio"
depends on BR2_LINUX_KERNEL
help
Linux driver for the Trellis Radio ESP32C6 SDIO co-processor.
comment "trellis_radio needs a Linux kernel to be built"
depends on !BR2_LINUX_KERNEL

and package/trellis_radio/trellis_radio.mk:

################################################################################
#
# trellis_radio
#
################################################################################
TRELLIS_RADIO_VERSION = main
TRELLIS_RADIO_SITE = git@pushin.eu:gus/trellis-radio.git
TRELLIS_RADIO_SITE_METHOD = git
TRELLIS_RADIO_LICENSE = GPL-2.0
# The kernel module lives in driver/, not the repo root - build there.
# common/ (driver/Makefile's -I$(src)/../common) comes along for free since
# SITE_METHOD=git clones the whole repo, not just driver/ - no separate
# sync step needed, unlike a local-directory SITE pointed at driver/ alone.
TRELLIS_RADIO_MODULE_SUBDIRS = driver
# firmware/blobs/trellis_flash_stub.bin is committed; trellis_radio_fw.bin is
# gitignored and only present after scripts/sync-fw-blob.sh has run.
define TRELLIS_RADIO_INSTALL_FW_BLOBS
$(INSTALL) -D -m 0644 $(@D)/firmware/blobs/trellis_flash_stub.bin \
$(TARGET_DIR)/lib/firmware/trellis_flash_stub.bin
if [ -f $(@D)/firmware/blobs/trellis_radio_fw.bin ]; then \
$(INSTALL) -D -m 0644 $(@D)/firmware/blobs/trellis_radio_fw.bin \
$(TARGET_DIR)/lib/firmware/trellis_radio_fw.bin; \
else \
echo "trellis_radio: trellis_radio_fw.bin not synced, skipping (see scripts/sync-fw-blob.sh)"; \
fi
endef
TRELLIS_RADIO_POST_INSTALL_TARGET_HOOKS += TRELLIS_RADIO_INSTALL_FW_BLOBS
$(eval $(kernel-module))
$(eval $(generic-package))

TRELLIS_RADIO_VERSION = main tracks the branch; pin it to a tag or commit hash instead for a reproducible build. The git@ remote needs SSH access configured on the build host - swap in an https:// URL if that's not available. Register the package by adding source "package/trellis_radio/Config.in" to package/Config.in, then enable it in your defconfig:

BR2_PACKAGE_TRELLIS_RADIO=y

Depending on which features you want, also enable matching userspace packages: BR2_PACKAGE_IW/_TCPDUMP (monitor), BR2_PACKAGE_HOSTAPD (_WPA3=y for SAE) and _DNSMASQ (AP), BR2_PACKAGE_BLUEZ5_UTILS (BLE), BR2_PACKAGE_WPAN_TOOLS (802.15.4). See the per-mode docs for specifics.

For local edit/rebuild iteration against a live working copy instead of a pinned git ref, Buildroot's generic <pkg>_OVERRIDE_SRCDIR mechanism still applies: set TRELLIS_RADIO_OVERRIDE_SRCDIR = /path/to/trellis-radio in your (untracked) local.mk, then make trellis_radio-rebuild.

Firmware provisioning files

The driver's auto-provisioning path (docs/architecture.md) loads two files via request_firmware(), so the package installs both into /lib/firmware:

  • trellis_flash_stub.bin - the ESP32-C6 flasher-stub blob, committed at firmware/blobs/trellis_flash_stub.bin (see that directory's README.md for provenance). Always installed.

  • trellis_radio_fw.bin - the merged firmware image (bootloader + partition table + app). This is not built by Buildroot (no ESP-IDF toolchain in this tree) - it's a prebuilt artifact, gitignored because it's 1.1MB and changes every build. Build and stage it before running Buildroot:

    cd firmware && idf.py --preset production build
    cd .. && ./scripts/sync-fw-blob.sh production

    sync-fw-blob.sh copies firmware/build/<preset>/trellis_radio_fw.bin to firmware/blobs/trellis_radio_fw.bin, where the package's TRELLIS_RADIO_INSTALL_FW_BLOBS hook picks it up. If it's absent at install time, the package skips it with a build-log warning rather than failing - a dev tree with only the driver built is a valid configuration.

board/protolux/trellis/rootfs_overlay/etc/modprobe.d/trellis_radio.conf ships options trellis_radio fw_autoupdate=0 on the current (dev/HITL test rig) image, so a hardware session isn't disrupted by an unattended reflash. A production image should drop that overlay file so fw_autoupdate defaults on.

Device tree

The ESP32 needs a devicetree node describing it as an SDIO function of the host's mmc controller, plus a power-sequence node for its reset line. The pwrseq node is new (not a reference to an existing label), so it needs its own / { ... }; block; the SDIO function node nests inside your existing mmc controller's &mmc1 { ... }; block:

/ {
esp32_pwrseq: pwrseq-esp32 {
compatible = "mmc-pwrseq-simple";
reset-gpios = <&pio 3 16 GPIO_ACTIVE_LOW>; /* board-specific pin */
power-off-delay-us = <2000000>;
post-power-on-delay-ms = <1500>;
};
};
&mmc1 {
/* ... existing controller properties (vmmc-supply, bus-width, etc.) ... */
mmc-pwrseq = <&esp32_pwrseq>;
status = "okay";
trellis-radio@1 {
compatible = "protolux,trellis-radio";
reg = <1>; /* SDIO function 1 */
boot-gpios = <&pio 3 14 GPIO_ACTIVE_LOW>; /* board-specific pin */
};
};

reset-gpios and boot-gpios are the ESP32's reset and bootstrap (download-mode) pins - adjust the &pio ... references for your board's actual wiring. The driver claims both: reset is toggled only via the kernel's own mmc_hw_reset() (through the pwrseq node above, never directly), and boot is requested directly by trellis_radio and exposed through the esp32_boot_mode sysfs attribute (see Driver interfaces below) for entering/leaving the ESP32's UART bootloader. Once these lines are described in the devicetree, they're kernel-owned - anything that used to drive them from userspace (libgpiod, gpioset, etc.) needs to go through that sysfs attribute instead.

Driver interfaces

Once bound, trellis_radio exposes each radio and its control surface as a standard Linux interface - no custom userspace API or ioctl surface.

Path Kind Feature Notes
wlan<N> (e.g. wlan0) netdev (Ethernet) WiFi STA Persistent, created at probe. Standard cfg80211 connect/scan via wpa_supplicant/NetworkManager.
wlan<N> (e.g. wlan1) netdev (Ethernet) WiFi AP Persistent, created at probe. Driven by hostapd.
user-chosen (e.g. mon0) netdev (ARPHRD_IEEE80211_RADIOTAP) WiFi monitor Created on demand (iw phy phy0 interface add mon0 type monitor), shares the one wiphy with STA/AP. Receive-only, no packet injection.
hci<N> (e.g. hci0) hci_dev BLE HCI_SDIO bus type, driven entirely by BlueZ (bluetoothctl, btmon) - no vendor commands.
wpan0 (+ phy<N>) ieee802154_hw / netdev IEEE 802.15.4 Created at probe by mac802154. Driven by iwpan/wpan-tools.
/sys/bus/sdio/drivers/trellis_radio/mmc1:*/esp32_boot_mode sysfs attribute (RW) Boot/reset control Read reports normal/bootloader/error; write bootloader/normal to sequence the ESP32's boot/reset pins and enter/leave UART download mode.
/sys/kernel/debug/trellis_radio/control_ping debugfs, HITL builds only Diagnostics Read fires a real CONTROL PING round trip and reports the result.
/sys/kernel/debug/trellis_radio/diag debugfs, HITL builds only Diagnostics Firmware-side diagnostic counters (CONTROL responses sent, SDIO TX failures, send-queue high-water mark, capture frames sent/dropped, plus STA data drops and AP data-frame sent/drop counters).
/sys/kernel/debug/trellis_radio/fuzz_control debugfs, HITL builds only Diagnostics Write raw bytes directly onto the CONTROL channel, bypassing the normal request/response framing.
/sys/kernel/debug/trellis_radio/tx_loopback / rx_loopback debugfs, HITL builds only Diagnostics Write/read the LOOPBACK channel directly, for datapath throughput and signal-integrity testing.

The /sys/kernel/debug/trellis_radio/ entries only exist in a HITL=1 build (CONFIG_TRELLIS_RADIO_HITL) - a production build compiles them out entirely, per the HITL build variants described above.

Testing

scripts/run-tests.sh tier0 # host only
scripts/run-tests.sh all --ko <path-to-trellis_radio.ko> # + hardware

Full reference: docs/testing.md.

Further reading

docs/architecture.md for how the pieces fit, docs/protocol.md for the SDIO on-wire format, docs/buildroot-install.md for building and flashing the SBC, and docs/devicetree.md for the device tree binding. Per-mode guides: docs/wifi-monitor-mode.md, docs/wifi-ap-mode.md, docs/ble-hci.md, docs/ieee802154.md. Known issues: docs/todo.md.