# Changelog All notable changes to `esp-p4-eth` will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project will adhere to [Semantic Versioning](https://semver.org/) once the first 0.x tag ships to crates.io. ## [Unreleased] ### Release readiness — 0.1.0 pre-flight (2026-04-29) - **`cargo package` clean.** 45 files, 120 KB compressed; `cargo package --no-verify` succeeds. - **180 host-side tests pass** under `cargo test --target x86_64-unknown-linux-gnu --lib --features p4-example`. - **Canonical examples** (`embassy_static_ping`, `embassy_dhcp`, `embassy_tcp_echo_irq`, `embassy_tcp_soak`) build clean (release + debug) with **zero warnings**. - **Hardware sanity:** the production-flavour `embassy_tcp_echo_irq.elf` (no `phy-init-debug` feature) boots cleanly across a 5/5 warm-reboot stress. - **API audit:** ~130 `pub` items; every documented; `pub mod diag` carries an explicit "intentionally not part of the stable API" disclaimer. - **Static footprint vs IDF (release, embassy_tcp_echo_irq):** `.text` 71.5 KB, `.rodata` 12.6 KB, `.bss` 31.3 KB, `.dma_bss` 26.1 KB, stack budget 76.0 KB, total runtime RAM ~134 KB. Code is roughly half the size of the equivalent IDF + LWIP build; static RAM is comparable, slightly heavier on DMA buffers (1536 B per descriptor vs IDF's 512 B + chaining). See README "Performance & footprint" for the full table and known follow-ups. - **`critical-section` is now an unconditional dependency.** The cache-controller wrap in `dma::sync_cache_region` requires it regardless of which time-driver feature is enabled. - **Doc-link housekeeping:** broken intra-doc links to `new_from_static_resources_with_board` and `peek_rdes` fixed. ### Reliability - **Cold-boot / warm-reboot hang root-caused and FULLY RESOLVED** (2026-04-28). The intermittent deadlock during `Ethernet::try_new` was traced to the chip ROM `Cache_WriteBack_All` helper at `0x4FC00414`. Root cause: ESP32-P4's L2 cache controller carries `mode / ways / line_size` shadow registers across CPU reset; the ROM helpers re-read them and walk the L2 by line index. Without an explicit re-program after warm reboot the helpers walk with stale parameters and intermittently hang the AHB bus. IDF runs `Cache_Set_L2_Cache_Mode` from `cache_hal_init` in the second-stage bootloader; we bypass the bootloader via `--ram --no-stub` and skipped this step. Fix: new `Dma::init_l2_cache_mode()` helper that calls `Cache_Set_L2_Cache_Mode(CACHE_SIZE_256K=10, CACHE_8WAYS_ASSOC=2, CACHE_LINE_SIZE_64B=3)` at ROM `0x4FC003D4` followed by `Cache_Invalidate_All(CACHE_MAP_L2_CACHE)` at ROM `0x4FC00404`, invoked as the very first step of `Ethernet::try_new_with_board`. Stress on `embassy_tcp_soak`: 30/30 warm reboots OK = **100 % reliability**, up from 5/15 (~33 %) baseline and 23/30 (~77 %) with the earlier mitigation set. Cold boot (true power-cycle) was already 100 % and remains so. Earlier mitigation steps shipped in the same release that contributed reliability improvements before the L2 mode init landed: - `CLOCK_CONFIG_DELAY_SPINS` 1024 → 100 000 cycles in `clock.rs` so the EMAC peripheral has time to leave reset before the first MMIO. - DMA `TX_DESC_LIST` / `RX_DESC_LIST` register writes moved out of `TDesRing::reset` / `RDesRing::reset` into `Ethernet::reset_dma::program_dma_descriptor_lists` so the rings no longer poke the DMA engine before `Dma::dma_reset` has run. - Early `Dma::dma_reset()` inside `try_new_with_board` right after `configure_clock_ext_in` so the engine reaches a known-idle state before any descriptor work. - `critical_section::with` wrapping the ROM cache helper in `dma::sync_cache_region` so SYSTIMER / CLIC interrupts cannot land while the cache controller is mid-transaction (kept; empirically gives a small additional reliability margin). ### Public API - New `Dma::init_l2_cache_mode()` helper. Call once at driver init before any descriptor work — replicates the `cache_hal_init` step IDF runs from the second-stage bootloader. `Ethernet::try_new_with_board` now invokes it as its very first step. - New `MacError::DmaBufferAboveCacheBoundary { addr: u32 }` variant returned by `Ethernet::try_new_with_board` if any descriptor or buffer pointer lands at or above `0x4FF80000` (the L2 cache backing region, invisible to bus masters). Compile-time `const _: () = assert!(...)` in `eth/mod.rs` keeps the linker `RAM_DMA` region in sync with the runtime guard. - New `pub mod diag` re-exports diagnostic atomics (RX_ARP, RX_IPV4, TX_LAST_*, CACHE_*_CALLS / TICKS, etc.) with explicit unstable-API doc disclaimer. Crate-root re-exports for those items removed; the bundled examples now `use esp_p4_eth::diag::{...}` instead. - New `phy-init-debug` cargo feature that enables UART markers in `Ethernet::try_new_with_board`, `Ethernet::mac_init`, `Ethernet::reset_dma`, and `Ip101::init`. Compiles to no-ops when off; exists primarily to localise the cold-boot hang point during future bring-up but is left in tree for any future ISR / clock-tree regression debugging. - New helpers on `TDesRing` / `RDesRing`: `flush_all` (single bulk cache writeback) and `descriptors_ptr` (raw pointer for the DMA-list programming step). Existing `reset` no longer touches DMA peripheral registers. - New helpers on `Ethernet`: `reset_descriptor_state` (RAM-only ring reinit), `flush_dma_visibility` (single bulk writeback), and `program_dma_descriptor_lists` (DMA-list register write). The composite `reset_dma` still exists and calls all three; the split is available for advanced callers that need to reorder these phases. - New helper on `Dma`: `flush_caches_all` — one-shot L1+L2 writeback via the ROM `_All` variant. ### Build / hygiene - Removed dead-code constants (`DEFAULT_PHY_ADDR`, `DEFAULT_REF_CLOCK_PIN` in `eth/mod.rs`; `ROM_CACHE_WRITEBACK_ADDR` in `dma.rs`). Canonical builds now produce zero warnings. - 19 bring-up scratch examples moved to `examples/dev/` and gated behind the new `dev-examples` cargo feature. Three canonical examples (`embassy_dhcp`, `embassy_static_ping`, `embassy_tcp_echo_irq`) stay in `examples/` and build by default with their required features. - Added `*.log` to `.gitignore` so UART/test capture archives stay local. ### Tooling - New `examples/embassy_tcp_soak.rs` — 4-listener TCP echo on ports 7780–7783 (4 KB sockets each), per-port byte counters, 60-second `stat_task` with hourly summary line, manual short-write drain loop. Built for long-duration reliability validation. - New `examples/dev/soak_driver.py` — Python 3.12 host driver, 4 concurrent TCP streams, xorshift32 deterministic stream, byte-exact echo verification, MB/s + mismatch reporting per minute. Run: `python3 soak_driver.py --host 192.168.0.50 --duration 3600`. ### Tests - **Host-side test count: 115 → 180** (+65 tests across three refactor passes to expose previously target-only modules and async-task pure-logic to the `x86_64-unknown-linux-gnu` test harness). Phase 3 (+17) extracts pure helpers from `ethernet_task` futures so RX classification, TX header snooping, and link-state transitions become host-testable without standing up the embassy executor: - `classify_rx_frame(buf, len)` — bumps RX_ARP / RX_IPV4 / RX_ICMP / RX_DHCP_FRAMES atomics and writes hex-dumps based on frame contents. 10 tests cover runt frames (< 14 bytes), ARP, IPv4 with TCP, IPv4 with ICMP, IPv4 with UDP DHCP-67 / DHCP-68 client / server, IPv4 with non-DHCP UDP ports, IPv6 (ethertype 0x86DD passes through without misclassification), large-frame hex dump, DHCP-frame hex dump. - `record_tx_frame_metadata(buf)` — populates TX_LAST_LEN / TX_LAST_DST_MAC_HI / TX_LAST_SRC_MAC_HI / TX_LAST_ETHERTYPE. 3 tests cover short buf (only length recorded), exact 14-byte minimum Ethernet header, oversize frames. - `link_state_action(prev, is_up_now) -> LinkAction` — pure state machine for `link_fut`. 4 tests cover all four (prev, is_up_now) inputs: down→down (NoChange), down→up (AttemptNegotiate — caller must run `phy_negotiate` before committing), up→up (NoChange), up→down (GoDown). - Helper utilities: `read_be_u32`, `hex_dump_into` (factored out of the inline rx_fut body — shared by large-frame and DHCP-frame hex dumps). - The diagnostic atomics (RX_ARP, RX_IPV4, RX_LAST_LARGE_FRAME, etc.) and the wake_rx_task / wake_tx_task functions had to be ungated from `#[cfg(target_arch = "riscv32")]` so the host-test classifier can reach them. They cost ~80 bytes of `.bss` on host builds and are no-op on host beyond storing values. - `critical-section = { features = ["std"] }` added to `[target.x86_64-unknown-linux-gnu.dev-dependencies]` (was Windows-only). Required because `embassy_sync::AtomicWaker` uses `critical_section::with` internally and now compiles on host too. Phase 1 + 2 (+48 tests) was a pair of refactor passes that exposed previously target-only modules to the host test harness. Coverage broken down: - `board.rs`: 0 → 8 tests. WAVESHARE_P4_ETH constant validation (PHY addr legal range, RMII / MDIO pin map, reset polarity, ref-clock pad), GPIO uniqueness, `Copy + Clone` API guarantee. - `dma.rs`: 17 → 22 tests. Cache counter instrumentation: per-op routing, SYSTIMER tick wrap-around, multi-call accumulation, u32 overflow wrap. `record_cache_call` extracted as a host-testable helper from the riscv32-only ROM-call path. - `eth/mod.rs`: 31 → 32 tests. `link_poll_interval_stays_in_sane_range` runtime guard plus a const-level `assert!` next to `LINK_POLL_INTERVAL_MS` that fires at compile time if the constant ever drifts outside [50, 500] ms. - `clic.rs`: 0 → 13 tests. Pure-data helpers extracted: `clic_attr_byte`, `clic_ctl_byte`, `clic_idx_for_cpu_line`, `int_map_value`, `clic_threshold_value`, `clic_ctrl_offset`. INT_MAP register addresses cross-checked against IDF v5.3.5 baseline; CLIC entry layout (IP/IE/ATTR/CTL byte order) verified. - `systimer.rs`: 0 → 10 tests. `target_ticks_split` extracted for HI/LO packing. TICK_RATE_HZ = 16 MHz, ticks↔µs round-trip, 20-bit HI mask truncation, integer-floor sub-microsecond cases. - `time_driver_irq_logic.rs` (new module): 0 → 11 tests. `is_async_irq`, `mcause_code`, CPU INT line constants and CLIC index derivations. The pure-data parts of the IRQ-driven time driver are now host-testable independently of the `p4-time-driver-irq` feature flag. - All four previously riscv32-only modules (`clic`, `systimer`, `time_driver_irq_logic`) now compile on host. MMIO `read_volatile` / `write_volatile` calls remain `#[cfg(target_arch = "riscv32")]` so the host build is a pure no-op on those code paths; address arithmetic and byte-packing logic execute on both targets. ### Fixed - **`link_poll_delay` was burning ~50 % of wall-clock time on cache ROM calls.** The function used a 500-iteration `yield_now` loop to space out PHY link-state checks. Each `yield_now` re-polls all `join3` children in `ethernet_task`, so every yield triggered an `rx_ring.has_packet()` and a `tx_ring.has_capacity()` call — and each of those does an L1 + L2 cache invalidate ROM call (~20 µs per pair). Result: ~24 400 invalidate calls per second in idle, consuming ~500 ms of wall time per second. Replaced the yield loop with a single `embassy_time::Timer::after(100 ms)`, which lets the executor park in `wfi` between link checks. Measured impact on the Waveshare board (espflash `--ram` boot, IRQ time driver): | metric | before | after | |---|---|---| | idle `minstret` | 22.3 M instr/s | 0.062 M instr/s (**360× lower**) | | idle invalidate calls | 24 400/s | 15/s (**1 600× lower**) | | idle invalidate wall-clock | 500 ms/s (50 %) | 310 µs/s (0.03 %) | | CPU @ 360 MHz idle | ~6.4 % | ~0.024 % | | CPU @ 360 MHz under 4 Mbps RX | ~6.6 % | ~3.5 % retire + 12 % cache stall | RBU stays 0 and TCP byte-clean round-trip is preserved. ### Changed - `dma_recovery_task` poll interval bumped from 1 ms to 50 ms in `embassy_tcp_stress_irq` and `embassy_tcp_echo_irq` (the two examples that run under `p4-time-driver-irq`). The recovery loop is empirically dead code under the IRQ time driver (rbu = 0 over 50 s of sustained 3–4 Mbps RX + TX), and a 50 ms recovery window is well below TCP's retransmit timer (≥200 ms), so the slower interval cannot mask any real-world stall. **Caveat — measured idle CPU did not drop:** `minstret` baseline stayed at ~23 M instr/s before and after the change, suggesting the dominant idle overhead lives somewhere else (most likely the embassy-net `Runner` / smoltcp polling), not in the user-space recovery task. 50 ms is kept as the better default for a dead-code path even though the empirical CPU saving was nil; future investigation into idle-baseline cost should look at embassy-net's link-check / smoltcp poll-at scheduling. ### Added - CPU instrumentation in `embassy_tcp_stress_irq` `stat_task`: per-second `mcycle` and `minstret` deltas. **Important:** on the ESP32-P4 Andes RV32 core, `mcycle` ticks at 40 MHz wall-clock (XTAL) and does NOT halt during `wfi` — useless for CPU%. `minstret` does halt during `wfi` (no instructions retire) and is the right signal. Empirical baseline: 23 M instructions/s idle, 24.5 M under 4 Mbps RX (≈ 3 000 instructions per Ethernet frame for the full RX → smoltcp → TCP → socket → ACK chain). At assumed 360 MHz HP CPU clock that's ~6–7 % CPU under SmartBox-scale load. - `examples/embassy_tcp_stress_irq.rs` — sustained-rate smoke test on the IRQ-driven path. Two independent TCP listeners (`:7780` RX-sink, `:7781` TX-source) plus a 1 s `stat_task` printing per-second deltas of bytes, frames, EMAC IRQs, RBU recoveries, and cumulative descriptor errors. First run on the Waveshare board (host through WSL NAT, 2 KB socket buffers): **RX ~4 Mbps × 25 s, TX ~3 Mbps × 25 s, `rbu = 0` end-to-end, IRQ-to-frame ratio 1:1, zero runt / oversized / framing errors.** The polling `dma_recovery_task` did not fire once over 50 s of continuous traffic, confirming it is dead code at SmartBox-scale workloads under `p4-time-driver-irq`. Throughput is window-limited (2 KB sockets + ~5 ms WSL NAT RTT), not driver-limited. - **IRQ-driven `embassy-time` driver** (`src/time_driver_irq.rs`, `p4-time-driver-irq` cargo feature). SYSTIMER alarm 0 → `INTERRUPT_CORE0_SYSTIMER_TARGET0_INT_MAP_REG = 17` → CLIC entry 17 → `_p4_eth_trap_entry` (naked-asm, `.balign 256`) → `rust_trap_dispatch`. Tick rate 1 µs, drift ~0.002 % over 1 s on the Waveshare board (12× more accurate than the polling driver). Mutually exclusive with `p4-time-driver`. - **IRQ-driven EMAC RX/TX wakers** (under `p4-time-driver-irq`). Synopsys SBD source (peripheral ID 92, `INTERRUPT_CORE0_BASE + 0x170`) → CLIC entry 18 → same trap entry → `Dma::read_interrupt_status()` → `crate::wake_rx_task` / `crate::wake_tx_task`. The `eth/mod.rs` `rx_fut` and `tx_fut` `wake_by_ref` paths are now gated `#[cfg(not(feature = "p4-time-driver-irq"))]` — under the IRQ feature they just `Poll::Pending` and let the executor sleep until the ISR signals. - `src/clic.rs` module — CLIC + INTERRUPT_CORE0 register helpers (`route_to_clic`, `route_systimer_target0`, `route_emac_sbd`, `enable_cpu_int`, `disable_cpu_int`, `clear_pending`, `set_threshold`, `read_ctrl_word`). Constants: `CLIC_EXT_INTR_NUM_OFFSET = 16`, `INT_MAP_SYSTIMER_TARGET0`, `INT_MAP_EMAC_SBD`. - `systimer::init_alarm0`, `arm_alarm0`, `disarm_alarm0`, `clear_alarm0`, `alarm0_pending` helpers. `init_alarm0` sets `SYSTIMER_CONF.target0_work_en` (bit 24) which is **off by default** — without it `INT_RAW.target0` never asserts. - `time_driver_irq::EMAC_IRQS` / `EMAC_IRQ_RX` / `EMAC_IRQ_TX` public `AtomicU32` counters for diagnostics. - `examples/clic_irq_smoke.rs` — bare-bones CLIC IRQ probe (no embassy, no driver crate dependency on the trap entry). Useful for any future ISR debugging on P4. - `examples/embassy_irq_smoke.rs` — embassy executor + `Timer::after` driven by the IRQ time-driver. Confirms ~16_000_300 ticks per second with no polling task spawned. - `examples/embassy_tcp_echo_irq.rs` — TCP echo on the full IRQ-driven path. Confirms RX/TX completion IRQs fire one-per-event (no trap storm) and TCP round-trip works end-to-end. - `BoardConfig` struct grouping `RmiiPinConfig`, `MdioPinConfig`, `PhyResetPinConfig`, `RefClockPin`, and PHY MDIO address. Provides `BoardConfig::WAVESHARE_P4_ETH` constant for the Waveshare ESP32-P4-ETH dev board. - `Ethernet::try_new_with_board`, `eth::new_with_board`, and `eth::new_from_static_resources_with_board` constructors — accept an explicit `BoardConfig` so the driver no longer hard-codes the Waveshare pin map. - `embassy_tcp_echo` example: TCP listener on :7777 with byte-exact echo loop and `tx`/`rx` frame counters. - `embassy_udp_echo` example: UDP listener on :7778 with diagnostic counters (`RX_LARGE_FRAMES`, `RX_LAST_LARGE_FRAME[16]`, `RX_LAST_FRAME_LEN`, `RX_RUNT_FRAMES_TOTAL`, `RX_OVERSIZED_FRAMES_TOTAL`, `RX_ERROR_FRAMES_TOTAL`) and a hex-dump of the most recent ≥200-byte RX frame. - `embassy-net-udp` cargo feature gating `embassy-net/udp`. - Atomic counters in the descriptor pipeline ( `RX_RUNT_FRAMES_TOTAL`, `RX_OVERSIZED_FRAMES_TOTAL`, `RX_ERROR_FRAMES_TOTAL`, `RX_LAST_FRAME_LEN`, `RX_LAST_RDES0`, `RX_LARGE_FRAMES`) re-exported from `lib.rs` for downstream diagnostics. - `RX_LAST_LARGE_FRAME[16]` + `RX_LAST_LARGE_FRAME_LEN` for hex-dumping the latest large RX frame from outside `ethernet_task`. - README at the crate root with hardware-support matrix, pin map, quick start, mandatory P4-specific build invariants (RAM_DMA section, workspace `opt-level = 1`), Cargo features, examples, and known limitations. - `.github/workflows/ci.yml` with host unit tests (`x86_64-unknown-linux-gnu`), target build checks (`riscv32imafc-unknown-none-elf`) for `embassy_static_ping`, `embassy_tcp_echo`, and `embassy_udp_echo`, plus `cargo fmt --check` and `cargo clippy`. - Dual `LICENSE-MIT` + `LICENSE-APACHE` text files at workspace root. ### Changed - `ethernet_task` now calls `time_driver_irq::enable_emac_irq()` after `eth.start()` under `#[cfg(feature = "p4-time-driver-irq")]`. The call reprograms `DMA_INTEN = TIE | RIE | NIE` (drops `AIE`) before unmasking the CLIC entry. See *Fixed* below for why. - **Crate renamed** from `smartbox-emac` to `esp-p4-eth`. Library import path is now `use esp_p4_eth::…`. Workspace, crate `Cargo.toml`, examples, fuzz targets, and internal references all updated. - **License** changed from `MIT` to `MIT OR Apache-2.0` to match the Rust ecosystem standard. - `dma_op_mode_value()` now returns `TSF | FUF` (was `TSF | RSF`). See the *Fixed* section below for rationale. - `Ethernet::try_new` is now a thin wrapper around `Ethernet::try_new_with_board(…, &BoardConfig::WAVESHARE_P4_ETH)` — call signature unchanged for the Waveshare board. - `Cargo.toml` package metadata extended with `description`, `repository`, `homepage`, `documentation`, `readme`, `keywords`, and `categories`. `publish = false` removed. ### Fixed - **`AIE` trap-storm under IRQ-driven mode.** When `enable_emac_irq()` was first wired up with the default `DMA_INTEN` (which includes `AIE`, the abnormal-summary enable), the CPU got stuck in a level-triggered trap loop right after `Ethernet::start()` because `RU` (RX Buffer Unavailable, sticky) goes high the moment the DMA core looks at its empty descriptor ring. Symptoms: `init: all tasks spawned` printed but nothing after — no link-up message, no `stat_task` ticks. Fix: the IRQ-driven enable path narrows `DMA_INTEN` to `TIE | RIE | NIE` (normal-summary only). Abnormal recovery stays with the polling `dma_recovery_task` which already pokes `RX_POLL_DEMAND` on RBU. - **256-byte L2 frame cutoff in RX path.** With `DMA_OP_MODE.RSF = 1` (Receive Store and Forward) the GMAC waited for the entire frame in the on-chip RX FIFO before transferring it to memory; the P4 RX FIFO is too small to hold a full Ethernet frame, so any frame larger than the FIFO was silently dropped before reaching the DMA descriptor pipeline (`err = runt = oversized = 0`). Symptoms: `ping -l 200` worked, `ping -l 210` did not; `embassy_tcp_echo` hung on a single 1400-byte send and reset after ~30 s; `embassy_udp_echo` timed out on any datagram ≥240 bytes. Switching to cut-through receive (`RSF = 0`, default RTC threshold of 64 bytes) makes every size from 60 to MTU 1500 bytes round-trip byte-exact. - Renamed unit test `dma_op_mode_enables_store_and_forward` → `dma_op_mode_uses_tx_store_forward_and_cut_through_rx`; the new test asserts `RSF` stays 0. ### Removed - Repository-private `target_fresh_161820/` debug build directory. - Stale `examples/dhcp_echo.rs~8728` OneDrive backup file. ## Older history For pre-rename history (`smartbox-emac` era — bring-up of clock tree, IO_MUX + GPIO matrix routing fixes, MDIO bus, IP101 PHY, embassy-net integration, L2 cache region discovery, FUF + ACS-clear + 5M PHY wait, SYSTIMER `embassy-time` driver), see the git log on `main` and the project memos under `MIGRATION_PLAN/ESP32_P4.md` in the SmartBox repo.