ESP32-C3 WiFi MAC ↔ SimNet bridge (design + RE notes)
Update (TCP/HTTP landed). The virtual AP (
peripherals/esp32c3/virtual_wifi.rs) now terminates TCP to an HTTP port in addition to DHCP/ARP/UDP: a station's SYN gets a SYN-ACK, its HTTP request is reassembled and handed to the shared L4 [HttpServer] (network/sim.rs, reusing its router + HTTP/1.1 encoder — one source of truth), and the response is segmented back followed by a FIN. It serves the/v1/public-statssnapshot (the LBC3.1 device's target). This closes the "subsequent TCP payloads relay to the existing L4 SimNet servers" item below. Covered by unit tests (tcp_http_get_roundtrip,tcp_http_unknown_path_404) that drive the exact segment exchange lwIP performs, with checksum verification. Still owed: end-to-end validation against a real booted C3 runningesp_http_client(needs a connecting-app firmware fixture; the current probe idles / only does UDP).
Status: bidirectional + associated. An unmodified C3 IDF binary boots, brings
WiFi up, and associates with a virtual AP over the real MAC (no
wifi_thunks): init → auth → assoc → run → STA CONNECTED. The bridge injects
RX frames (beacon/auth-resp/assoc-resp) and captures the MAC's TX (the STA's
probe/auth/assoc requests and a real broadcast IPv4/UDP DHCP-discover).
Remaining: an IP/DHCP responder that turns captured TX into injected RX replies
so the STA gets an IP and exchanges app traffic.
Bidirectional summary (implemented in esp32c3::wifi_mac + cli bridge)
- RX inject (
queue_rx_frame): write[48-byte rx-control header][802.11 frame]into an owner-held lldesc RX descriptor, set the lldesc (owner=0, eof=1, length=total), set RX event0x01004000, raise MAC IRQ (matrix src 0). Header must have word@0 bit28 (matched-vif), word@4 =0x08000000 | len<<8, byte44=rssi, byte45=rate. - TX capture (
take_tx_frames): on a write of0xC000_0000to per-AC PLCP00x60033D08-AC*8, follow the low-20-bit pointer → TX lldesc → word1 = frame buffer (DRAM), read the 802.11 frame, then signal TX-complete (per-queue done state0xCB0+ event bit0x80) since TX is fire-and-forget. - Virtual AP (cli
build_open_beacon/build_auth_resp/build_assoc_resp, BSSID02:00:00:00:00:01, STA00:00:00:00:00:00, OPEN auth).
The remaining DHCP/IP final mile
The captured DHCP-discover (802.11 data, to-DS, LLC/SNAP ethertype 0x0800,
IPv4/UDP) must be answered: parse it, build a DHCP-offer then DHCP-ack (assign
e.g. 192.168.4.2, gw 192.168.4.1), wrap in UDP/IPv4 (IPv4 header checksum
required; UDP checksum may be 0)/LLC-SNAP/802.11-data (from-DS: addr1=STA,
addr2=BSSID, addr3=server), and queue_rx_frame it. Then the STA reaches
IP_EVENT_STA_GOT_IP; subsequent TCP/UDP payloads relay to the existing L4
SimNet servers. (Original phase notes below.)
Why this is its own phase (the impedance mismatch)
- The real C3 MAC operates on raw 802.11 frames in hardware DMA rings. The running firmware (lmac/pp/wdev + the libnet80211 driver) does real scan → auth → assoc → data, programming the MAC registers and DMA descriptors.
- The existing
SimNet(crates/core/src/network/sim.rs) is an L4 socket simulation (TCPconnect/send/recv, HTTP/echo servers,VirtualAp.associate). The S3 bridged to it by thunking at the lwIP socket layer (esp32s3::wifi_thunks) and fakingWL_CONNECTED— i.e. it never ran esp_wifi/MAC at all. That is the thunk we are explicitly removing.
Bridging the real MAC to SimNet therefore needs a frame-level layer in
between (802.11 ↔ Ethernet ↔ the existing L4 SimNet), not a socket shim.
MAC interrupt / event anatomy (RE'd from wifi_probe.elf + ROM)
- MAC interrupt event register:
hal_mac_interrupt_get_eventreads0x6003_3C3C;hal_mac_interrupt_clr_eventwrites0x6003_3C40(W1C). - ISR
wDev_ProcessFiq(0x4038_34A4) reads the event word and dispatches: | event mask | handler | meaning | |---|---|---| |0x0100_4000|lmacProcessRxSucData(ROM 0x4000_1614) | RX frame received | |0x80|lmacPostTxComplete(ROM 0x4000_1608) | TX complete | |0x100|lmacProcessCollisions(ROM 0x4000_1610) | TX collision | |0x1E|wdev_process_tbtt| beacon timing | |0x1E0|wdev_process_tsf_timer| TSF | - RX is a descriptor linked list (
wdev_record_rx_linked_list,wdev_dump_rx_linked_list);lmacProcessRxSucDatawalks it. - MAC interrupt = interrupt-matrix source 0 (
MAC_INTR_MAP@ offset 0 ininterrupt_core0.yaml), routed to a CPU line by the C3 interrupt matrix we already model — so raising it delivers towDev_ProcessFiqvia the normal trap path.
MAC DMA registers (RE'd from the live connect run, LABWIRED_MAC_TRACE)
Captured by tracing writes to the 0x6003_3000 MAC window while the real driver
brings WiFi up and starts a scan:
- RX descriptor ring base:
0x6003_3088← a DRAM pointer (e.g.0x3fca4904). - RX descriptor format (linked list, 3 words each):
| word | meaning |
|---|---|
| 0 | flags/len —
0x8064_0640: bit31 = owner (HW may fill), low 16 = buffer size (0x640= 1600 = the "static rx buffer" size) | | 1 | buffer pointer (DRAM, the 1600-byte frame buffer) | | 2 | next-descriptor pointer (ring is a singly-linked list) | - Trigger / handshake:
0x6003_3084bit31 (written0x8000_0000to kick; the prior session's "handshake" scratch bit). - Other config seen:
0x6003_3c60/c64/c6c(a second ring/EOF pointer at0x6003_3c64←0x3fc00000, zeroed),0x6003_3d04,0x6003_3084.
RX descriptor is an ESP lldesc_t (CONFIRMED by tracing the driver's reads
of an injected descriptor): word0 = size[11:0] | length[23:12] | offset[28:24]
| sosf[29] | eof[30] | owner[31]. Empty/HW-owned reads 0x80640640
(owner=1, length=size=1600). On RX completion HW writes owner=0, eof=1,
length=actual-rx-bytes, size preserved (e.g. 0x40140640 for a 320-byte
frame). VALIDATED end-to-end: with that writeback, the real driver's RX
callback follows word1 (buffer ptr) and reads the injected frame bytes out of
the buffer, then recycles the descriptor (owner re-set to 0xc0140640). The
RX inject path (queue → DMA → lldesc → MAC IRQ → wDev_ProcessFiq →
lmacProcessRxSucData → driver reads frame) works against the real firmware.
The 802.11 frame starts at buffer offset 0 (no rx-control prefix in the buffer).
TX ring (still to RE): the scan probe-request TX path hadn't queued a TX descriptor within the traced window; needs a longer trace / break on the lmac TX path to find the TX-kick register + descriptor.
RX-inject mechanism (target design)
- Place the received 802.11 frame into the next free RX DMA descriptor's
buffer (RX ring base register: TODO — finish RE'ing where the driver
programs it in
mac_txrx_init/ppRxPkt). - Set the RX-success bits in the event register
0x6003_3C3C(0x0100_4000). - Assert MAC interrupt source 0 → matrix → CPU line → trap →
wDev_ProcessFiq→lmacProcessRxSucDataconsumes the descriptor and hands the frame up.
TX-capture mechanism (target design)
The driver fills a TX descriptor and writes a TX-kick register; the model reads
the frame out of the descriptor buffer and hands it to the frame-level AP, then
sets the TX-complete event bit (0x80) + raises the MAC interrupt so
lmacPostTxComplete runs. TODO — RE the TX-kick register + descriptor format.
Remaining build (sequence)
- MAC DMA model (
esp32c3::wifi_mac, behavioral, replacing the declarative window but preserving the bring-up register-backing + MAC-ready bit): event register + interrupt raise + RX descriptor inject + TX descriptor capture. Finish the RX-ring-base / TX-kick RE first. - Frame-level
VirtualAp: handle the 802.11 management the driver sends (probe/auth/assoc) so it associates, and relay data-frame payloads to/from the existing L4SimNet(de/encapsulate 802.11 ↔ Ethernet ↔ IP). - A connecting C3 app: the current
wifi_probefixture brings WiFi up and idles ("idling for trace") — it never scans/connects, so it generates no MAC traffic. A minimalesp_wifi_connect+ socket app (C3 IDF build) is needed to exercise and validate the bridge end-to-end.
The natural first milestone is association over the real MAC (driver TX of probe/auth/assoc via the real DMA ring → frame-level AP responds via RX inject → driver reaches connected) — the first true "real MAC, no thunks" comms.