esp-p4-eth/CHANGELOG.md
2026-10-05 01:16:55 +08:00

378 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.