284 lines
14 KiB
Markdown
284 lines
14 KiB
Markdown
|
|
# 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_<peripheral>_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.
|