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
hostapdvia 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
mac802154device (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 ESP32C6driver/- Linux kernel modulecommon/- shared protocol headers, plus host-side tests incommon/tests/docs/- architecture, protocol, buildroot/device-tree, and per-mode guidesscripts/- 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 atfirmware/blobs/trellis_flash_stub.bin(see that directory'sREADME.mdfor 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 buildcd .. && ./scripts/sync-fw-blob.sh productionsync-fw-blob.shcopiesfirmware/build/<preset>/trellis_radio_fw.bintofirmware/blobs/trellis_radio_fw.bin, where the package'sTRELLIS_RADIO_INSTALL_FW_BLOBShook 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.