# esp-p4-eth `#![no_std]` async Ethernet MAC driver for **ESP32-P4** RMII designs, plug-in compatible with [`embassy-net`](https://crates.io/crates/embassy-net). > **Status:** ready for **0.1.0** crates.io release as of 2026-04-29. > End-to-end ping, TCP, and UDP all work on the Waveshare ESP32-P4-ETH dev > board (IP101GRI PHY) at 100 Mbps full duplex. Both `embassy-time` > (SYSTIMER) and EMAC RX/TX wakers can run IRQ-driven via the on-chip CLIC — > the executor genuinely sleeps on `wfi` when there's no work to do. > Cold-boot and warm-reboot reliability validated 100 % across stress runs > (30/30 warm reboots on the `embassy_tcp_soak` harness, 5/5 power-cycle > on the canonical examples). 180 host-side unit tests pass on > `x86_64-unknown-linux-gnu`. The public API surface (`Ethernet`, `Device`, > `Runner`, `BoardConfig`, the `pub mod diag` observability atomics) > is what we expect to commit to for 0.x SemVer. ## Why this exists As of late April 2026, the official [`esp-hal`](https://github.com/esp-rs/esp-hal) does not include `soc/esp32p4/` EMAC support — the P4 simply isn't covered by the upstream HAL crates yet. This repository is a self-contained, hand-rolled Synopsys DesignWare GMAC driver targeting the P4 silicon directly: clock tree, IO_MUX + GPIO matrix routing, DMA descriptor rings with cache coherency, MDIO + IP101 PHY, and an `embassy-net-driver-channel` adaptor that drops into any `embassy-net` stack. ## Features - 100 Mbps full duplex RMII, `embassy-net` `Driver` via `embassy-net-driver-channel` - Two `embassy-time` driver options on the P4 SYSTIMER: - **IRQ-driven** via SYSTIMER alarm 0 → CLIC entry 17, gated by the `p4-time-driver-irq` cargo feature. Executor drops to `wfi` between deadlines. Drift ~0.002 % over 1 s on the Waveshare board. - **Polling** via `time_polling_task`, gated by `p4-time-driver`. Simpler but burns one core; kept for comparison and as a `wfi`-free fallback. - **IRQ-driven EMAC RX/TX wakers** under `p4-time-driver-irq` — DMA completion interrupts (Synopsys SBD source → CLIC entry 18) wake the embassy-net rx/tx futures. No `wake_by_ref` busy-spin under that feature. - IPv4 / ARP / ICMP / TCP / UDP all verified end-to-end against a Windows host through a consumer router - All RX frame sizes from 60 up to MTU 1500 bytes round-trip cleanly (single 1472-byte UDP datagrams included) - `BoardConfig` abstraction — bring your own pin map, ref-clock pad, and PHY MDIO address - 180 host-side unit tests; the host build target is `x86_64-unknown-linux-gnu` ## Hardware support | Board | Status | `BoardConfig` | |---------------------------------------------------|----------|-------------------------------------| | Waveshare ESP32-P4-ETH (IP101GRI PHY, 25 MHz XO) | ✅ tested | `BoardConfig::WAVESHARE_P4_ETH` | | Other P4 + RMII PHY designs | should work | construct your own `BoardConfig` | ### Waveshare ESP32-P4-ETH default pin map | Signal | GPIO | |-------------|--------| | TXD0 | 34 | | TXD1 | 35 | | TX_EN | 49 | | RXD0 | 30 | | RXD1 | 29 | | CRS_DV | 28 | | MDC | 31 | | MDIO | 52 | | PHY RESET | 51 (active low) | | REF_CLK in | 50 | | PHY MDIO addr | 1 | ## Quick start (Waveshare ESP32-P4-ETH) Build a minimal "ping me" example: ```bash cargo build \ --no-default-features \ --features p4-example,p4-time-driver,embassy-net-icmp \ --example embassy_static_ping ``` Flash via [`espflash`](https://github.com/esp-rs/espflash) into RAM: ```bash espflash flash --ram --no-stub --chip esp32p4 \ --ignore_app_descriptor --monitor \ target/riscv32imafc-unknown-none-elf/debug/examples/embassy_static_ping ``` The example brings the link up at 100 Mbps full duplex (~2.5 s after reset), configures itself as `192.168.0.50/24`, and replies to ICMP echo requests. Adjust `SELF_IP` and `GATEWAY` constants in the example for your subnet. For the **IRQ-driven** path (recommended for real workloads — no polling task burning a core, executor sleeps on `wfi`): ```bash cargo build \ --no-default-features \ --features p4-example,p4-time-driver-irq,embassy-net-tcp \ --example embassy_tcp_echo_irq ``` This routes SYSTIMER alarms through CLIC entry 17 and EMAC RX/TX completion interrupts through CLIC entry 18, both dispatched by a single trap entry (`_p4_eth_trap_entry`) defined in `src/time_driver_irq.rs`. ## Mandatory P4-specific build configuration ESP32-P4 has two hardware constraints that the calling crate **must** honour: 1. **DMA-shared statics must live below `0x4FF80000`.** The upper 256 KB of HP SRAM is the L2 cache backing region; bus masters (the EMAC DMA) cannot read it. Any static that the DMA touches (descriptors, packet buffers, `StaticDmaResources`) must be placed in a linker section that resolves to the safe range. See `memory.x` and the `#[link_section = ".dma_bss"]` annotation in the bundled examples. 2. **Use a workspace `[profile.dev]` with `opt-level = 1`.** With `opt-level = 0` the debug `.text` overflows the 192 KB safe-DRAM slab; with `opt-level = "s"` inlining gets aggressive enough that the naked-counter MDIO BUSY-poll loop completes before the PHY answers and the bus times out. The ready-made `embassy_static_ping`, `embassy_tcp_echo`, and `embassy_udp_echo` examples already embed both invariants and serve as templates. ## Cargo features | Feature | Default | Purpose | |----------------------|---------|-------------------------------------------------------------------------------| | `mock-time` | yes | embassy-time mock driver — required for host tests; mutually exclusive with `p4-time-driver*` | | `embassy-net-tcp` | yes | enables the `embassy-net/tcp` socket layer | | `embassy-net-udp` | no | enables the `embassy-net/udp` socket layer | | `embassy-net-icmp` | yes | enables `embassy-net/auto-icmp-echo-reply` | | `p4-time-driver` | no | SYSTIMER-backed polling `embassy-time` driver for `riscv32imafc` targets | | `p4-time-driver-irq` | no | SYSTIMER+CLIC IRQ-driven `embassy-time` driver. Also routes EMAC RX/TX completion IRQs and removes the `wake_by_ref` paths in `eth/mod.rs`. Mutually exclusive with `p4-time-driver` | | `p4-example` | no | gates `[[example]]` blocks; required by every P4 example | For a target build, `--no-default-features` then add what you need, e.g.: ```bash --features p4-example,p4-time-driver,embassy-net-icmp,embassy-net-udp ``` ## Examples The crate ships with three canonical examples that build with their required features by default. Bring-up scratch examples (mdio_test, clk_dump, systimer_probe, etc.) live under `examples/dev/` and are gated behind the `dev-examples` feature so they don't pollute downstream builds. ### Canonical (`examples/`) | Example | What it shows | |--------------------------|-----------------------------------------------------------------------------------------------------| | `embassy_static_ping` | full embassy-net stack with static IP, ICMP echo reply via the driver (polling time driver) | | `embassy_dhcp` | embassy-net DHCP client + diagnostic atomics dump (polling time driver) | | `embassy_tcp_echo_irq` | TCP listener on :7777 echoing bytes back, byte-exact for any size 1..1500, IRQ-driven path | | `embassy_tcp_soak` | 4 parallel TCP echo listeners on :7780–:7783 with 60-second `stat_task` snapshots and hourly summary; pair with `examples/dev/soak_driver.py` from the host for byte-exact verification | Build, e.g.: ```bash cargo build --no-default-features \ --features p4-example,p4-time-driver-irq,embassy-net-tcp \ --example embassy_tcp_echo_irq ``` ### Dev / bring-up (`examples/dev/`, requires `dev-examples` feature) `mdio_test`, `phy_probe`, `clk_dump`, `systimer_probe`, `clic_irq_smoke`, `embassy_smoke`, `embassy_irq_smoke`, `embassy_time_smoke`, polling `embassy_tcp_echo` / `embassy_udp_echo`, `embassy_tcp_stress_irq`, `phy_init_diag` (cold-boot diagnostic, requires `phy-init-debug` feature), plus `soak_driver.py` host-side companion for `embassy_tcp_soak`. Build any of them by adding `dev-examples` to the feature list, e.g.: ```bash cargo build --no-default-features \ --features p4-example,dev-examples \ --example mdio_test ``` ## P4 CLIC quirks (relevant if you fork the trap entry) Three non-obvious facts learned the hard way during IRQ bring-up. They're already encoded in `src/clic.rs` and `src/time_driver_irq.rs`, but worth flagging if you're poking the trap path or routing additional IRQs: 1. **`mtvec.MODE` is forced to `11` (CLIC mode) in hardware.** Direct / Vectored RISC-V modes are not available — writing `addr | 0` reads back as `addr | 3`. 2. **Trap-entry base = `mtvec & ~0xFF`.** The low 8 bits are MODE/reserved and get clamped. The asm trap entry must be `.balign 256`. 3. **`INTERRUPT_CORE0__INT_MAP_REG` accepts the CLIC index** (`cpu_int_line + 16`), not just the CPU INT line. Writing `1` to map a peripheral to "CPU line 1" silently no-ops; you must write `17` for it to land in `CLIC_INT_CTRL_REG[17]`. 4. **Don't enable `AIE` in `DMA_INTEN` under IRQ-driven mode.** The `RU` bit (RX Buffer Unavailable, sticky) goes high immediately after `Ethernet::start()` because the descriptor ring is empty; with AIE enabled the abnormal-summary line storms the trap. The driver programs `DMA_INTEN = TIE | RIE | NIE` only and leaves abnormal recovery to a polling `dma_recovery_task`. ## Performance & footprint Numbers below are for `embassy_tcp_echo_irq` built in release mode against crate version 0.1.0 on the Waveshare ESP32-P4-ETH dev board. | Metric | Value | Notes | |------------------------------|--------------------------------------|------------------------------------------| | `.text` (code) | 71.5 KB | release, debuginfo stripped | | `.rodata` | 12.6 KB | | | `.bss` (CPU-side) | 31.3 KB | embassy + smoltcp + sockets + diagnostics| | `.dma_bss` (DMA buffers + descriptors) | 26.1 KB | 8 RX + 8 TX × 1536-byte buffers | | `.stack` budget | 76.0 KB | embassy task pool reservation | | **Total runtime RAM** | **~134 KB** | static + stack budget | | Combined RAM-loaded image | ~218 KB | the whole `--ram --no-stub` payload | | Idle CPU | **0.024 %** @ 360 MHz | executor sleeps on `wfi` | | Cold-boot to first link-up | ~3 s | includes 5 M-cycle PHY oscillator wait | | Sustained TCP echo validated | 4 Mbps × 50 s, byte-clean, RBU=0 | window-limited (2 KB sockets), not driver| | Cold-boot reliability | 100 % (5/5 power-cycle) | | | Warm-reboot reliability | 100 % (30/30 stress on soak harness) | after the L2 cache mode init fix | For an apples-to-apples comparison with the IDF `esp_eth` driver on the same chip, `MIGRATION_PLAN/ESP32_P4.md` has the canonical IDF baseline recipe; runtime numbers vs IDF are an open follow-up tracked for 0.2.0. ## Known limitations - **Throughput ceiling not characterised.** Sustained TCP RX of ~4 Mbps and TX of ~3 Mbps over 25 s (Waveshare board, 2 KB socket buffers, host through WSL NAT) round-trip cleanly with `RBU = 0`, 1:1 IRQ-to-frame ratio, and no descriptor errors. Higher rates and multi-connection saturation have not been measured yet — the stress example (`embassy_tcp_stress_irq`) is window-limited by 2 KB sockets, and `embassy_tcp_soak` has 4 listeners × 4 KB each. A multi-MB/s characterisation with 32 KB sockets and a direct cable is a planned follow-up. - **DMA buffer footprint vs IDF.** The driver currently allocates one 1536-byte buffer per descriptor (8 + 8 = ~26 KB total). IDF defaults to 20 RX + 10 TX × 512-byte buffers with descriptor chaining for jumbo frames (~16 KB). Switching to chained 512-byte buffers would shave ~10 KB of static RAM at the cost of more bookkeeping in `descriptors.rs`. - **Cache writeback uses `_All` instead of `_Addr`.** The chip ROM `Cache_WriteBack_Addr` variant returns success but does not actually flush data to RAM on the `--ram --no-stub` boot path even after `Cache_Set_L2_Cache_Mode` has been re-applied. The driver therefore uses `Cache_WriteBack_All` per descriptor — correct, but ~100 µs/frame more expensive than IDF's per-address writeback. Identifying the remaining init step IDF runs that makes `_Addr` actually flush is a planned investigation. - **DHCP not validated end-to-end.** The driver sends DHCP DISCOVER frames correctly but the lab consumer router silently drops them. Static IP works. - **Half-IRQ EMAC error recovery (cosmetic).** Under `p4-time-driver-irq`, normal RX/TX completions are IRQ-driven but `RU` / abnormal recovery still lives in the polling `dma_recovery_task`. The stress example shows recovery never fires at 3–4 Mbps sustained (rbu+=0 over 50 s of continuous traffic), so this is not a correctness issue at SmartBox-scale rates — but re-enabling `AIE` with proper debouncing and removing the polling task is a future cleanup. ## Contributing Issues and pull requests welcome. The driver is structured for side-by-side comparison with ESP-IDF baseline register dumps — when adding a fix, please include a brief explanation of which register/bit changes and what wire-level test (ping, UDP echo size, TCP echo size) catches a regression. ## License Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](../LICENSE-APACHE)) - MIT license ([LICENSE-MIT](../LICENSE-MIT)) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.