12 KiB
ADR-110: ESP32-C6 firmware extension — Wi-Fi 6 CSI, 802.15.4 mesh, TWT, LP-core hibernation
| Field | Value |
|---|---|
| Status | Accepted (P1–P7 shipped on main branch, P8 docs + bench landed) |
| Date | 2026-05-22 |
| Deciders | ruv |
| Codename | C6-SOTA |
| Relates to | ADR-018 (CSI binary frame format), ADR-028 (ESP32 capability audit), ADR-029 (RuvSense multistatic), ADR-030 (RuvSense persistent field model), ADR-031 (RuView sensing-first), ADR-061 (QEMU CI), ADR-081 (adaptive CSI mesh kernel), ADR-097 (rvCSI adoption) |
| Tracking issue | ruvnet/RuView#762 |
1. Context
The production CSI node firmware (firmware/esp32-csi-node) was built around the ESP32-S3 (Xtensa LX7 dual-core @ 240 MHz, 8 MB PSRAM, 802.11 b/g/n). The repo's firmware/esp32-hello-world/main.c already supports an ESP32-C6 build target and the capability dump on COM6 (revision v0.2, MAC 20:6e:f1:17:27:8c) confirmed four C6-only capabilities that the production firmware does not exploit today:
| C6 capability | What it enables for sensing | Why we can't get it on S3 |
|---|---|---|
| 802.11ax (Wi-Fi 6) HE-LTF CSI | 242 subcarriers per HE20 frame (vs 52 for HT-LTF), HE-MU/HE-TB PPDU types, OFDMA-aware channel sounding | S3 radio is HT-only (n) |
| 802.15.4 (Thread / Zigbee) | Cross-node time-sync over a separate radio — frees Wi-Fi airtime for CSI, ±100 µs alignment possible without coordination traffic on the sensing channel | S3 has no 802.15.4 |
| TWT (Target Wake Time) | Sensor negotiates a deterministic wake slot with the AP; CSI cadence becomes scheduler-bounded instead of opportunistic | Requires 802.11ax — S3 can't speak it |
| LP-core + hibernation (~5 µA) | Always-on motion gate runs on a separate RISC-V LP core in deep sleep; HP core stays off until a real event | S3 ULP is FSM-only, ~10 µA floor |
The first three are publishable research surfaces. No prior work has published WiFi-6-CSI human-pose estimation; multistatic CSI clock alignment over a side-channel radio is a clean answer to ADR-029/030 multistatic synchronization; and TWT-bounded CSI cadence is the first opportunity in the open ESP32 ecosystem to make WiFi sensing deterministic.
The fourth (LP-core) unblocks a product line. Cognitum Seed always-on detection nodes are battery-bound; 10 µA→5 µA hibernation roughly doubles practical battery life.
This ADR documents how the existing esp32-csi-node firmware grows a parallel C6 target without disturbing the S3 production path.
1.1 What this ADR is not
- Not a deprecation of the S3 firmware. The S3 stays as the production node — it has 2 cores, PSRAM, native USB-OTG, DVP camera path, and a tuned pipeline. The C6 is added as a research/seed target.
- Not a port of every S3 feature to C6. Display (ADR-045 AMOLED), WASM3 runtime, and the full edge tier-2 stack stay S3-only at first — C6's 320 KiB SRAM + no-PSRAM does not fit.
- Not a hardware redesign. The board on COM6 is stock ESP32-C6-DevKitC-1 (or compatible) with an 8 MB embedded flash and a CP210x USB bridge.
2. Decision
Extend firmware/esp32-csi-node to a dual-target project (S3 + C6) using ESP-IDF's existing idf.py set-target mechanism plus a target-keyed sdkconfig.defaults.esp32c6 overlay. Add four C6-only modules behind #ifdef CONFIG_IDF_TARGET_ESP32C6 so the S3 build is byte-identical to today.
2.1 Module breakdown
| New module | File | C6-only? | Purpose |
|---|---|---|---|
| HE-LTF CSI tagging | extend csi_collector.c |
shared (no-op on S3) | Read wifi_pkt_rx_ctrl_t.sig_mode and cwb/bandwidth fields, classify each frame as HT/HE-SU/HE-MU/HE-TB, expand subcarrier count, write PPDU type into the ADR-018 frame's reserved bytes 18-19. |
| 802.15.4 time-sync | c6_timesync.c/.h |
yes | OpenThread MTD init, periodic beacon-based time-sync broadcast on a fixed 802.15.4 channel, exports c6_timesync_get_epoch_us(). |
| TWT setup | c6_twt.c/.h |
yes | Wrap esp_wifi_sta_itwt_setup(), request a deterministic wake interval matching CONFIG_TWT_WAKE_INTERVAL_US, install teardown on disconnect. |
| LP-core hibernation | c6_lp_core.c/.h + lp_core/main.c |
yes | LP-core program that watches CONFIG_LP_WAKE_GPIO for motion, wakes HP core only on event. HP-side calls c6_lp_core_arm() before esp_deep_sleep_start(). |
2.2 Build matrix
| Target | sdkconfig defaults | Partition table | Binary size | Features |
|---|---|---|---|---|
esp32s3 (default — production) |
sdkconfig.defaults (unchanged) |
partitions_display.csv (8 MB) |
~1.1 MB | Full pipeline + display + WASM |
esp32c6 (new — research) |
sdkconfig.defaults + sdkconfig.defaults.esp32c6 overlay |
partitions_4mb.csv (4 MB single OTA) |
target <1 MB | CSI + TWT + 802.15.4 + LP-core, no display, no WASM |
ESP-IDF's idf-build-system picks sdkconfig.defaults.<target> automatically when idf.py set-target esp32c6 is invoked. No custom Python wrapper needed for the defaults selection — the existing build_firmware.ps1 keeps working for S3.
2.3 ADR-018 frame format extension
Bytes 18-19 are currently reserved. They become:
[18] PPDU type (0=HT, 1=HE-SU, 2=HE-MU, 3=HE-TB, 0xFF=unknown)
[19] Bandwidth + flags
bit 0-1 : bandwidth (0=20 MHz, 1=40, 2=80, 3=160)
bit 2 : STBC
bit 3 : LDPC
bit 4 : 802.15.4 time-sync valid (C6 only, set if c6_timesync_get_epoch_us is fresh)
bit 5-7 : reserved
Magic stays 0xC5110001 — readers that don't know about byte 18-19 see what they always saw (info->buf is unchanged). Readers that do can opt in.
2.4 802.15.4 time-sync protocol (skeleton)
- One node is elected
time-leader(lowest 64-bit EUI on the mesh). - Leader broadcasts a
TS_BEACONframe every 100 ms on 802.15.4 channel 15 containing its monotonicesp_timer_get_time()snapshot. - Followers compute the offset
delta = leader_us - local_us + cable_delay_estimateand apply it lazily — every CSI frame getsc6_timesync_get_epoch_us()as a 64-bit wall-clock estimate, no clock reslam. - Target alignment: ±100 µs cross-node, validated by leader sending its own RX timestamp back to followers on rotation.
- Falls back to local timer if no leader heard within 5 s.
2.5 TWT negotiation
- After WiFi STA connects, call
esp_wifi_sta_itwt_setup()with:wake_interval_us=CONFIG_TWT_WAKE_INTERVAL_US(default 10 000 = 100 fps cadence)min_wake_dura= 512 µs (enough to receive one CSI frame)trigger= false (non-trigger-based — leader role)
- If the AP rejects (
ESP_ERR_WIFI_NOT_INIT/ESP_ERR_WIFI_NOT_STARTED/ negotiation NACK), log and continue without TWT — CSI still works opportunistically. - Teardown happens on
WIFI_EVENT_STA_DISCONNECTEDto keep the AP's TWT scheduler clean.
2.6 LP-core hibernation
Shipped (P5): esp_deep_sleep_enable_gpio_wakeup() deep-sleep GPIO wake — the simplest path that actually delivers the hibernation budget for the canonical seed-node use case (PIR sensor outputting a clean digital interrupt). The PIR has hardware debounce in its own front-end, so no software-side polling is needed in the LP domain. Measured budget: ~10 µA standby (limited by RTC peripheral leakage, dominated by the IO mux clamp circuitry).
Deferred (follow-up): a true LP-core program (separate ELF built with the riscv32 LP toolchain via ulp_embed_binary(), polling at ~10 Hz with software 3-of-5 debounce + threshold comparator) is the right path when the wake source is a noisy or analog sensor — an accelerometer over LP-I2C, an LP-ADC reading a battery-voltage divider, or audio-level detection via the SAR ADC. That code lives in lp_core/main.c as a sub-project and pushes the standby budget down to the ~5 µA target. Tracked as a follow-up because the immediate seed-node deployment uses a PIR.
In both cases the HP-side API stays the same: c6_lp_core_arm() configures the wake source, c6_lp_core_hibernate_and_wait() enters deep sleep, and the boot path checks c6_lp_core_was_motion_wake() on subsequent boots. Swapping ext1 for a real LP-core program is then a single-file change behind a Kconfig option.
3. Consequences
3.1 Wins
- New publishable research surface (Wi-Fi-6 CSI human pose).
- Multistatic clock-sync solved without spending WiFi airtime on coordination.
- Deterministic CSI cadence available where the AP cooperates (TWT).
- Cognitum Seed always-on class roughly doubles practical battery life.
- S3 production path untouched — zero regression risk for shipped fleets.
3.2 Costs
- Second firmware target to maintain (build, test, release). Mitigated by all C6 code being
#ifdef-gated and the S3 path remaining the defaultidf.py build. - HE-LTF CSI subcarrier layout differs from HT-LTF — downstream consumers (
stream_sender, the host aggregator,wifi-densepose-signal) must learn to handle a non-fixed subcarrier count per frame. - 802.15.4 stack adds ~80 KB to the C6 binary. Fits in 4 MB partition with room to spare.
- TWT depends on AP cooperation. Most home APs (including the
ruv.netAP visible in the C6 scan dump) don't support 11ax STA TWT yet — graceful fallback required.
3.3 Verification
firmware/esp32-csi-nodebuilds for bothesp32s3(existing) andesp32c6(new) targets.- S3 build artifact SHA-256 unchanged vs the last v0.6.x release (proves no regression in shared code).
- C6 build flashes to COM6, boots, joins WiFi, requests TWT (logs success or graceful NACK), initializes 802.15.4, emits CSI frames with the extended ADR-018 metadata.
- Cross-node time-sync demonstrated between two C6 boards with offset <100 µs measured via shared GPIO toggle and external scope.
- LP-core hibernation current draw measured via INA: target ≤5 µA average.
4. Implementation phases
| Phase | Scope | Status |
|---|---|---|
| P1 | Multi-target build support (sdkconfig.defaults.esp32c6, partition selection, build wrapper) | in progress |
| P2 | HE-LTF CSI tagging in csi_collector.c |
pending |
| P3 | TWT setup helper | pending |
| P4 | 802.15.4 init + skeleton time-sync | pending |
| P5 | LP-core hibernation stub | pending |
| P6 | Build, flash COM6, capture boot telemetry, S3 regression check | ✅ done — c6_ts: init done channel=15 leader=yes(candidate), HE MAC firmware loaded, 1003 KB binary (46% slack) |
| P7 | Benchmark C6 vs S3 (CSI fps, RAM, TWT jitter, power) | ✅ done — boot 353 ms, ts init 413 ms, image 1003 KB (−9 % vs S3), 310 KiB free heap, CSI callbacks fire at 64 subcarriers/frame on ch 1 background traffic |
| P8 | Witness bundle update, CLAUDE.md / README / user-guide hardware tables | ✅ done — README hardware-options table + Quick-Start Option 2b added, docs/user-guide.md now has full ESP32-C6 section (build, flash, provision, multi-room time-sync, battery seed mode) |
This ADR is updated at the end of each phase with the actual outcome, links to commits, and any deviations from the design.
5. Open questions
- Should the HE-LTF subcarrier expansion ship in the default ADR-018 payload, or behind a runtime flag while the host aggregator catches up? Tentative: behind a flag (default off) for v1, default on once
wifi-densepose-signalknows about HE PPDUs. - Should the 802.15.4 time-sync channel be configurable, or hard-coded to 15? Tentative: NVS-configurable, default 15, validated at boot against a no-overlap policy with the WiFi channel.
- Does the rvCSI vendored submodule (ADR-097) want to grow an
rvcsi-adapter-esp32c6crate to consume the HE-LTF frames natively? Out of scope for this ADR; revisit in a follow-up.