Skip to main content

esp_p4_eth/eth/
mod.rs

1//! Ethernet implementation modules staged under a future `esp-hal/src/eth/` layout.
2//!
3//! The standalone crate still re-exports these modules from the crate root so
4//! existing examples keep working, but grouping them here makes the eventual
5//! upstream move much more mechanical.
6
7#[cfg(target_arch = "riscv32")]
8use core::cell::RefCell;
9#[cfg(target_arch = "riscv32")]
10use core::future::poll_fn;
11#[cfg(target_arch = "riscv32")]
12use core::task::Poll;
13
14use static_cell::ConstStaticCell;
15
16#[cfg(target_arch = "riscv32")]
17use embassy_futures::{join::join3, yield_now};
18// AtomicWaker is needed unconditionally so the RX/TX wakers and their
19// host-side wake_rx_task / wake_tx_task tests can exercise the plumbing.
20// `Mutex` is only used inside the riscv32-only `ethernet_task`.
21use embassy_sync::waitqueue::AtomicWaker;
22#[cfg(target_arch = "riscv32")]
23use embassy_sync::blocking_mutex::{raw::NoopRawMutex, Mutex};
24
25use crate::ch;
26
27#[path = "../clock.rs"]
28pub mod clock;
29#[path = "../descriptors.rs"]
30pub mod descriptors;
31#[path = "../dma.rs"]
32pub mod dma;
33#[path = "../phy.rs"]
34pub mod phy;
35#[path = "../pins.rs"]
36pub mod pins;
37#[path = "../regs.rs"]
38pub mod regs;
39
40use self::{
41    clock::{configure_clock_ext_in, configure_speed_divider, disable_emac_clock_tree},
42    descriptors::{
43        DescriptorError, RDes, RDesRing, RxRingStats, StaticDmaResources, TDes, TDesRing,
44        TxRingStats, BUF_SIZE, RX_DESC_COUNT, TX_DESC_COUNT,
45    },
46    dma::{Dma, DmaInterruptStatus},
47    phy::{Ip101, LinkState, Phy, PhyError, Speed},
48    pins::{configure_mdio_pin_set, configure_rmii_pin_set},
49};
50
51// After `--ram --no-stub` boot the ESP32-P4 ROM leaves the HP CPU/APB at the default 40 MHz that
52// the boot ROM itself runs on (no second-stage bootloader has bumped the PLLs yet). With APB at
53// 40 MHz the IDF table picks div=3 → MDC = APB / 26 ≈ 1.54 MHz, well within the 2.5 MHz MDIO
54// limit. If/when the example installs a custom clock setup that raises APB, this constant must
55// be updated to match — too-fast MDC will make the PHY ignore us.
56const DEFAULT_AHB_CLOCK_HZ: u32 = 40_000_000;
57const MAC_STOP_TIMEOUT_POLLS: usize = 1_000;
58const MAC_DEFAULT_IFG_96_BIT_TIMES: u32 = 0;
59// IDF v5.3 baseline post-link reads MAC_FRAME_FILTER = 0x10 (PM = pass all
60// multicast). HPF (1<<10) only changes hash-vs-perfect filtering semantics
61// for the address filter; it does NOT make multicast pass. Without PM the
62// MAC drops mDNS, IGMP, IPv6 NS, etc. We follow IDF and set PM by default.
63const MACFFILT_DEFAULT: u32 = regs::bits::macffilt::PM;
64
65/// Lowest address of the ESP32-P4 L2 cache backing region. Any descriptor or
66/// packet buffer at or above this address is invisible to bus masters: the
67/// EMAC DMA cannot read or write the cache backing SRAM.
68const DMA_CACHE_BOUNDARY: u32 = 0x4FF8_0000;
69
70// Linker region `RAM_DMA` (memory.x) starts at 0x4FF40000 with LENGTH 0x10000.
71// If the boundary is ever bumped or the region is widened, the linker check
72// here will fail at compile time before the runtime guard ever runs.
73const _: () = assert!(0x4FF4_0000 + 0x1_0000 <= DMA_CACHE_BOUNDARY);
74/// PHY link-state poll period. With this set to a real `Timer::after` delay
75/// (rather than a `yield_now` loop) the executor parks in `wfi` between
76/// link checks, freeing the cycles that would otherwise be burned re-polling
77/// `rx_fut.has_packet()` / `tx_fut.has_capacity()` — each of which does an
78/// L1+L2 cache invalidate ROM call. 100 ms is responsive enough to catch
79/// cable insert/remove without measurable user latency.
80const LINK_POLL_INTERVAL_MS: u64 = 100;
81
82// Compile-time guard: keep `link_poll_delay` in the sane range. Catches a
83// future contributor reverting to `yield_now × N` (which would either
84// remove this const entirely, breaking the compile, or set the interval
85// to 0/1 ms which we lower-bound here) or picking an interval so long
86// that link-state changes go unnoticed for seconds.
87// See `feedback_p4_yield_now_antipattern.md`.
88const _: () = {
89    assert!(
90        LINK_POLL_INTERVAL_MS >= 50 && LINK_POLL_INTERVAL_MS <= 500,
91        "LINK_POLL_INTERVAL_MS must stay in [50, 500] ms",
92    );
93};
94
95/// Duplex mode configured in the MAC and negotiated with the PHY.
96#[repr(u8)]
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum Duplex {
99    /// Half duplex operation.
100    Half,
101    /// Full duplex operation.
102    Full,
103}
104
105/// Default MTU exposed through the embassy channel device.
106pub const MTU: usize = BUF_SIZE;
107/// Number of receive slots in the embassy driver channel.
108pub const CHANNEL_RX_COUNT: usize = 8;
109/// Number of transmit slots in the embassy driver channel.
110pub const CHANNEL_TX_COUNT: usize = 8;
111
112// `ch::State<MTU, RX, TX>` is ~12 KB on this configuration. The earlier
113// `StaticCell::init_with(ch::State::new)` formulation built the value through
114// a stack-resident temporary which overflowed the riscv-rt default stack and
115// hung the chain `new_from_static_resources → new → init_with → tuple return`
116// somewhere around the tuple build. `ch::State::new()` is `const fn`, so we
117// can place the storage directly in `.bss` via `ConstStaticCell` and never
118// allocate it on the stack. `take()` then hands out `&'static mut` on the
119// in-place value, so the construction is zero-cost at startup.
120static STATE: ConstStaticCell<ch::State<MTU, CHANNEL_RX_COUNT, CHANNEL_TX_COUNT>> =
121    ConstStaticCell::new(ch::State::new());
122static RX_WAKER: AtomicWaker = AtomicWaker::new();
123static TX_WAKER: AtomicWaker = AtomicWaker::new();
124
125/// Base address of the RX descriptor ring, captured by `ethernet_task::start()`
126/// for live debug peeking from outside the eth `Mutex`. Reads as 0 before init.
127pub static RX_DESC_BASE: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
128
129/// Stride (bytes) between consecutive RDes objects. Matches the descriptor
130/// type's `align(128)` (one L2 cache line each).
131pub const RX_DESC_STRIDE: usize = 128;
132
133/// Bring-up counters incremented inside [`ethernet_task`]. Reset to 0 at boot.
134pub static RX_FRAMES: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
135pub static TX_FRAMES: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
136pub static RX_BUF_REQUESTED: core::sync::atomic::AtomicU32 =
137    core::sync::atomic::AtomicU32::new(0);
138pub static TX_BUF_REQUESTED: core::sync::atomic::AtomicU32 =
139    core::sync::atomic::AtomicU32::new(0);
140/// Per-ethertype counters (RX classification by EtherType field).
141pub static RX_ARP: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
142pub static RX_IPV4: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
143pub static RX_ICMP: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
144/// Pending counters set by the rx_fut just before delivering a frame to the
145/// embassy channel. Use for RX content snooping from outside ethernet_task.
146pub static RX_LAST_ETHERTYPE: core::sync::atomic::AtomicU32 =
147    core::sync::atomic::AtomicU32::new(0);
148pub static RX_LAST_DST_MAC_HI: core::sync::atomic::AtomicU32 =
149    core::sync::atomic::AtomicU32::new(0);
150/// First 16 u32 words (= 64 bytes) of the most recent IPv4-with-UDP RX frame
151/// where source or destination port falls in the DHCP range (67/68). Used
152/// to confirm whether the router's DHCP OFFER actually reaches our DMA.
153pub static RX_LAST_DHCP_FRAME: [core::sync::atomic::AtomicU32; 16] = [
154    core::sync::atomic::AtomicU32::new(0),
155    core::sync::atomic::AtomicU32::new(0),
156    core::sync::atomic::AtomicU32::new(0),
157    core::sync::atomic::AtomicU32::new(0),
158    core::sync::atomic::AtomicU32::new(0),
159    core::sync::atomic::AtomicU32::new(0),
160    core::sync::atomic::AtomicU32::new(0),
161    core::sync::atomic::AtomicU32::new(0),
162    core::sync::atomic::AtomicU32::new(0),
163    core::sync::atomic::AtomicU32::new(0),
164    core::sync::atomic::AtomicU32::new(0),
165    core::sync::atomic::AtomicU32::new(0),
166    core::sync::atomic::AtomicU32::new(0),
167    core::sync::atomic::AtomicU32::new(0),
168    core::sync::atomic::AtomicU32::new(0),
169    core::sync::atomic::AtomicU32::new(0),
170];
171pub static RX_DHCP_FRAMES: core::sync::atomic::AtomicU32 =
172    core::sync::atomic::AtomicU32::new(0);
173
174/// Diagnostic: first 64 bytes (16 u32 words) of the most recent RX frame whose
175/// length is >= 200 bytes. Used to investigate the 200/240 byte cutoff bug.
176pub static RX_LAST_LARGE_FRAME: [core::sync::atomic::AtomicU32; 16] = [
177    core::sync::atomic::AtomicU32::new(0),
178    core::sync::atomic::AtomicU32::new(0),
179    core::sync::atomic::AtomicU32::new(0),
180    core::sync::atomic::AtomicU32::new(0),
181    core::sync::atomic::AtomicU32::new(0),
182    core::sync::atomic::AtomicU32::new(0),
183    core::sync::atomic::AtomicU32::new(0),
184    core::sync::atomic::AtomicU32::new(0),
185    core::sync::atomic::AtomicU32::new(0),
186    core::sync::atomic::AtomicU32::new(0),
187    core::sync::atomic::AtomicU32::new(0),
188    core::sync::atomic::AtomicU32::new(0),
189    core::sync::atomic::AtomicU32::new(0),
190    core::sync::atomic::AtomicU32::new(0),
191    core::sync::atomic::AtomicU32::new(0),
192    core::sync::atomic::AtomicU32::new(0),
193];
194/// Length of the most recent RX frame stored in [`RX_LAST_LARGE_FRAME`].
195pub static RX_LAST_LARGE_FRAME_LEN: core::sync::atomic::AtomicU32 =
196    core::sync::atomic::AtomicU32::new(0);
197/// TX frame content snooping — set by tx_fut just before handing the frame
198/// to the EMAC. Diagnoses smoltcp/embassy-net producing malformed frames.
199pub static TX_LAST_LEN: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
200pub static TX_LAST_ETHERTYPE: core::sync::atomic::AtomicU32 =
201    core::sync::atomic::AtomicU32::new(0);
202pub static TX_LAST_DST_MAC_HI: core::sync::atomic::AtomicU32 =
203    core::sync::atomic::AtomicU32::new(0);
204pub static TX_LAST_SRC_MAC_HI: core::sync::atomic::AtomicU32 =
205    core::sync::atomic::AtomicU32::new(0);
206
207/// `embassy-net-driver-channel` device handle returned by [`new`].
208pub type Device<'d> = ch::Device<'d, MTU>;
209/// `embassy-net-driver-channel` runner handle returned by [`new`].
210pub type Runner<'d> = ch::Runner<'d, MTU>;
211
212/// Wakes the async RX path when new RX work becomes available.
213pub fn wake_rx_task() {
214    RX_WAKER.wake();
215}
216
217/// Wakes the async TX path when new TX capacity becomes available.
218pub fn wake_tx_task() {
219    TX_WAKER.wake();
220}
221
222/// Errors surfaced by the top-level MAC/device lifecycle.
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub enum MacError {
225    /// PHY initialization or negotiation failed.
226    Phy(PhyError),
227    /// DMA reset did not complete in time.
228    ResetTimeout,
229    /// DMA stop sequence did not quiesce in time.
230    StopTimeout,
231    /// One of the descriptor or buffer arrays handed to the constructor lives
232    /// at or above the L2-cache backing region (`0x4FF80000`). Bus masters
233    /// cannot read or write that range, so any DMA transfer would silently
234    /// fail. Place [`StaticDmaResources`](crate::StaticDmaResources) under
235    /// `#[link_section = ".dma_bss"]` (the linker script's `RAM_DMA` region
236    /// keeps it below the cache boundary).
237    DmaBufferAboveCacheBoundary {
238        /// Address that fell outside the DMA-safe range.
239        addr: u32,
240    },
241}
242
243/// Verifies that a descriptor or packet-buffer address lies below the L2
244/// cache backing region. Folds to a no-op on host targets where pointers
245/// are 64-bit and the chip-specific boundary is irrelevant.
246#[inline]
247fn check_dma_buffer_address(addr: usize) -> Result<(), MacError> {
248    #[cfg(target_arch = "riscv32")]
249    {
250        if addr >= DMA_CACHE_BOUNDARY as usize {
251            return Err(MacError::DmaBufferAboveCacheBoundary { addr: addr as u32 });
252        }
253    }
254    #[cfg(not(target_arch = "riscv32"))]
255    {
256        let _ = addr;
257    }
258    Ok(())
259}
260
261/// Builds the embassy driver device/runner pair plus an initialized [`Ethernet`].
262///
263/// This entry point expects the caller to provide `'static` DMA descriptor and
264/// packet buffer storage. The constructor configures the default RMII pin bank,
265/// selects the default external `REF_CLK` input pin, initializes the MAC, and
266/// resets both DMA rings before returning.
267pub fn new(
268    mac_addr: [u8; 6],
269    tx_descriptors: &'static mut [TDes; TX_DESC_COUNT],
270    rx_descriptors: &'static mut [RDes; RX_DESC_COUNT],
271    tx_buffers: &'static mut [[u8; BUF_SIZE]; TX_DESC_COUNT],
272    rx_buffers: &'static mut [[u8; BUF_SIZE]; RX_DESC_COUNT],
273) -> (Device<'static>, Runner<'static>, Ethernet<'static>) {
274    new_with_board(
275        mac_addr,
276        tx_descriptors,
277        rx_descriptors,
278        tx_buffers,
279        rx_buffers,
280        &crate::board::BoardConfig::WAVESHARE_P4_ETH,
281    )
282}
283
284/// Same as [`new`] but accepts an explicit [`BoardConfig`].
285pub fn new_with_board(
286    mac_addr: [u8; 6],
287    tx_descriptors: &'static mut [TDes; TX_DESC_COUNT],
288    rx_descriptors: &'static mut [RDes; RX_DESC_COUNT],
289    tx_buffers: &'static mut [[u8; BUF_SIZE]; TX_DESC_COUNT],
290    rx_buffers: &'static mut [[u8; BUF_SIZE]; RX_DESC_COUNT],
291    board: &crate::board::BoardConfig,
292) -> (Device<'static>, Runner<'static>, Ethernet<'static>) {
293    let state = STATE.take();
294    let (runner, device) = ch::new(state, ch::driver::HardwareAddress::Ethernet(mac_addr));
295    let eth = Ethernet::try_new_with_board(
296        mac_addr,
297        tx_descriptors,
298        rx_descriptors,
299        tx_buffers,
300        rx_buffers,
301        board,
302    )
303    .expect("EMAC bootstrap failed");
304
305    (device, runner, eth)
306}
307
308/// Builds the embassy driver device/runner pair from a single static resource block.
309///
310/// This is the preferred top-level constructor for examples and board support
311/// code because it keeps all DMA-capable memory in one dedicated owner type.
312pub fn new_from_static_resources(
313    mac_addr: [u8; 6],
314    resources: &'static mut StaticDmaResources,
315) -> (Device<'static>, Runner<'static>, Ethernet<'static>) {
316    new_from_static_resources_with_board(
317        mac_addr,
318        resources,
319        &crate::board::BoardConfig::WAVESHARE_P4_ETH,
320    )
321}
322
323/// Same as [`new_from_static_resources`] but accepts an explicit [`BoardConfig`].
324pub fn new_from_static_resources_with_board(
325    mac_addr: [u8; 6],
326    resources: &'static mut StaticDmaResources,
327    board: &crate::board::BoardConfig,
328) -> (Device<'static>, Runner<'static>, Ethernet<'static>) {
329    let (tx_descriptors, rx_descriptors, tx_buffers, rx_buffers) = resources.split();
330    new_with_board(
331        mac_addr,
332        tx_descriptors,
333        rx_descriptors,
334        tx_buffers,
335        rx_buffers,
336        board,
337    )
338}
339
340/// Background task that moves packets between the DMA rings and the embassy channel.
341///
342/// The task starts the MAC/DMA datapath, forwards RX frames into the embassy
343/// channel, drains TX frames from the channel into the descriptor ring, and
344/// continuously reflects PHY link state changes back into the network stack.
345#[embassy_executor::task]
346#[cfg(target_arch = "riscv32")]
347pub async fn ethernet_task(runner: Runner<'static>, mut eth: Ethernet<'static>) {
348    eth.start().expect("EMAC start failed");
349    // With the IRQ-driven time driver wired up, route the EMAC main IRQ
350    // through CLIC now that DMA_INTEN is programmed. Done here (not in main)
351    // so the routing happens AFTER `start()` regardless of how the task
352    // is launched.
353    #[cfg(feature = "p4-time-driver-irq")]
354    crate::time_driver_irq::enable_emac_irq();
355    // Capture the RX descriptor list base after start() so external debug code
356    // can peek at descriptor state without reaching into the eth Mutex.
357    RX_DESC_BASE.store(
358        regs::read(regs::dma::RX_DESC_LIST),
359        core::sync::atomic::Ordering::Relaxed,
360    );
361
362    let (state_runner, mut rx_runner, mut tx_runner) = runner.split();
363    let eth = Mutex::<NoopRawMutex, RefCell<Ethernet<'static>>>::new(RefCell::new(eth));
364
365    state_runner.set_link_state(ch::driver::LinkState::Down);
366
367    let rx_fut = async {
368        loop {
369            let buf = rx_runner.rx_buf().await;
370            RX_BUF_REQUESTED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
371            // Kick the RX DMA engine. After it drains the ring and the OWN bits
372            // flip to CPU it parks in RX_BUFFER_UNAVAILABLE and stops scanning
373            // until a poll-demand write occurs. mdio_test does this every
374            // ~100ms; here we do it on every buffer hand-off.
375            Dma::demand_rx_poll();
376
377            poll_fn(|cx| {
378                RX_WAKER.register(cx.waker());
379
380                let ready = eth.lock(|eth| eth.borrow().rx_ring.has_packet());
381                if ready {
382                    Poll::Ready(())
383                } else {
384                    // Without the EMAC IRQ wired up, the only thing that can
385                    // wake us is `dma_recovery_task` polling the ring. Force
386                    // a re-poll so we don't deadlock waiting on a wake that
387                    // never comes. Under `p4-time-driver-irq` the SBD ISR
388                    // calls `wake_rx_task()` directly, so this self-wake is
389                    // skipped and the executor sleeps on `wfi`.
390                    #[cfg(not(feature = "p4-time-driver-irq"))]
391                    cx.waker().wake_by_ref();
392                    Poll::Pending
393                }
394            })
395            .await;
396
397            let len = eth.lock(|eth| {
398                let mut eth = eth.borrow_mut();
399                match eth.receive() {
400                    Some((len, data)) => {
401                        buf[..len].copy_from_slice(&data[..len]);
402                        eth.pop_rx();
403                        len
404                    }
405                    None => 0,
406                }
407            });
408
409            if len != 0 {
410                classify_rx_frame(buf, len);
411                rx_runner.rx_done(len);
412                RX_FRAMES.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
413                wake_rx_task();
414            } else {
415                yield_now().await;
416            }
417        }
418    };
419
420    let tx_fut = async {
421        loop {
422            let buf = tx_runner.tx_buf().await;
423            TX_BUF_REQUESTED.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
424
425            loop {
426                poll_fn(|cx| {
427                    TX_WAKER.register(cx.waker());
428
429                    let ready = eth.lock(|eth| eth.borrow().tx_ring.has_capacity());
430                    if ready {
431                        Poll::Ready(())
432                    } else {
433                        // See rx_fut comment — same rationale.
434                        #[cfg(not(feature = "p4-time-driver-irq"))]
435                        cx.waker().wake_by_ref();
436                        Poll::Pending
437                    }
438                })
439                .await;
440
441                record_tx_frame_metadata(buf);
442                let result = eth.lock(|eth| eth.borrow_mut().transmit(buf));
443
444                match result {
445                    Ok(()) => {
446                        tx_runner.tx_done();
447                        TX_FRAMES.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
448                        wake_tx_task();
449                        break;
450                    }
451                    Err(DescriptorError::RingFull) => {
452                        yield_now().await;
453                    }
454                    Err(_) => {
455                        tx_runner.tx_done();
456                        break;
457                    }
458                }
459            }
460        }
461    };
462
463    let link_fut = async {
464        let mut link_up = false;
465
466        loop {
467            let is_up = eth.lock(|eth| eth.borrow_mut().phy_link_status());
468
469            match link_state_action(link_up, is_up) {
470                LinkAction::NoChange => {}
471                LinkAction::AttemptNegotiate => {
472                    if eth.lock(|eth| eth.borrow_mut().phy_negotiate()).is_ok() {
473                        state_runner.set_link_state(ch::driver::LinkState::Up);
474                        link_up = true;
475                    }
476                }
477                LinkAction::GoDown => {
478                    state_runner.set_link_state(ch::driver::LinkState::Down);
479                    link_up = false;
480                }
481            }
482
483            link_poll_delay().await;
484        }
485    };
486
487    let _ = join3(rx_fut, tx_fut, link_fut).await;
488}
489
490/// Fully-initialized Ethernet MAC + DMA + PHY instance.
491pub struct Ethernet<'a> {
492    tx_ring: TDesRing<'a>,
493    rx_ring: RDesRing<'a>,
494    phy: Ip101,
495    mac_addr: [u8; 6],
496}
497
498impl<'a> Ethernet<'a> {
499    /// Creates and initializes the driver from caller-provided DMA memory.
500    ///
501    /// This infallible convenience constructor uses [`Ethernet::try_new`] and
502    /// panics if the low-level EMAC bootstrap fails.
503    pub fn new(
504        mac_addr: [u8; 6],
505        tx_descriptors: &'a mut [TDes; TX_DESC_COUNT],
506        rx_descriptors: &'a mut [RDes; RX_DESC_COUNT],
507        tx_buffers: &'a mut [[u8; BUF_SIZE]; TX_DESC_COUNT],
508        rx_buffers: &'a mut [[u8; BUF_SIZE]; RX_DESC_COUNT],
509    ) -> Self {
510        Self::try_new(
511            mac_addr,
512            tx_descriptors,
513            rx_descriptors,
514            tx_buffers,
515            rx_buffers,
516        )
517        .expect("EMAC bootstrap failed")
518    }
519
520    /// Creates and initializes the driver from a [`StaticDmaResources`] block.
521    ///
522    /// This is the preferred constructor when the DMA backing memory is owned
523    /// by a single static resource block.
524    pub fn new_from_static_resources(
525        mac_addr: [u8; 6],
526        resources: &'a mut StaticDmaResources,
527    ) -> Self {
528        let (tx_descriptors, rx_descriptors, tx_buffers, rx_buffers) = resources.split();
529        Self::new(
530            mac_addr,
531            tx_descriptors,
532            rx_descriptors,
533            tx_buffers,
534            rx_buffers,
535        )
536    }
537
538    /// Fallible constructor variant for the **Waveshare ESP32-P4-ETH** default
539    /// pin map. Equivalent to [`Ethernet::try_new_with_board`] called with
540    /// [`BoardConfig::WAVESHARE_P4_ETH`].
541    pub fn try_new(
542        mac_addr: [u8; 6],
543        tx_descriptors: &'a mut [TDes; TX_DESC_COUNT],
544        rx_descriptors: &'a mut [RDes; RX_DESC_COUNT],
545        tx_buffers: &'a mut [[u8; BUF_SIZE]; TX_DESC_COUNT],
546        rx_buffers: &'a mut [[u8; BUF_SIZE]; RX_DESC_COUNT],
547    ) -> Result<Self, MacError> {
548        Self::try_new_with_board(
549            mac_addr,
550            tx_descriptors,
551            rx_descriptors,
552            tx_buffers,
553            rx_buffers,
554            &crate::board::BoardConfig::WAVESHARE_P4_ETH,
555        )
556    }
557
558    /// Fallible constructor that takes an explicit [`BoardConfig`].
559    ///
560    /// Use this for any hardware that isn't the Waveshare ESP32-P4-ETH dev
561    /// board: provide your own `BoardConfig` literal with the matching pin
562    /// map, reference-clock pin, and PHY MDIO address.
563    pub fn try_new_with_board(
564        mac_addr: [u8; 6],
565        tx_descriptors: &'a mut [TDes; TX_DESC_COUNT],
566        rx_descriptors: &'a mut [RDes; RX_DESC_COUNT],
567        tx_buffers: &'a mut [[u8; BUF_SIZE]; TX_DESC_COUNT],
568        rx_buffers: &'a mut [[u8; BUF_SIZE]; RX_DESC_COUNT],
569        board: &crate::board::BoardConfig,
570    ) -> Result<Self, MacError> {
571        check_dma_buffer_address(tx_descriptors.as_ptr() as usize)?;
572        check_dma_buffer_address(rx_descriptors.as_ptr() as usize)?;
573        check_dma_buffer_address(tx_buffers.as_ptr() as usize)?;
574        check_dma_buffer_address(rx_buffers.as_ptr() as usize)?;
575
576        crate::phy::diag_log("[try_new] enter, init_l2_cache_mode ...");
577        // ESP32-P4 L2 cache controller carries `mode/ways/line_size` shadow
578        // registers across CPU resets. IDF's `cache_hal_init` re-programs
579        // them in the second-stage bootloader; we bypass that via
580        // `--ram --no-stub` and the ROM `Cache_WriteBack_*` helpers
581        // intermittently hang on warm reboot if the shadow state is stale
582        // (root-caused 2026-04-28). Run the same one-shot init here so any
583        // subsequent cache writeback / invalidate has a valid configuration
584        // to walk.
585        Dma::init_l2_cache_mode();
586        crate::phy::diag_log("[try_new] L2 cache mode set, configure_rmii_pin_set ...");
587        configure_rmii_pin_set(board.rmii_pins);
588        crate::phy::diag_log("[try_new] rmii ok, configure_mdio_pin_set ...");
589        configure_mdio_pin_set(board.mdio_pins);
590        crate::phy::diag_log("[try_new] mdio pins ok, configure_clock_ext_in ...");
591        configure_clock_ext_in(board.ref_clock);
592        crate::phy::diag_log("[try_new] clock ext in ok, Dma::dma_reset ...");
593        // Reset the DMA engine BEFORE any descriptor cache flush. The
594        // EMAC may carry over an active DMA state from a previous binary
595        // (or from a partial init that the previous CPU reset interrupted),
596        // and in that state the L1/L2 cache writeback ROM helpers race
597        // with the DMA engine on the same descriptor lines and intermittently
598        // hang the AHB bus. Calling `dma_reset` here puts the DMA into a
599        // known idle state and removes the ~10 % cold-boot hang rate that
600        // showed up at `Dma::flush_descriptor` inside `TDesRing::new`.
601        Dma::dma_reset().map_err(|_| MacError::ResetTimeout)?;
602        crate::phy::diag_log("[try_new] dma_reset ok, TDesRing::new ...");
603        let tx_ring = TDesRing::new(tx_descriptors, tx_buffers);
604        crate::phy::diag_log("[try_new] tx_ring ok, RDesRing::new ...");
605        let rx_ring = RDesRing::new(rx_descriptors, rx_buffers);
606        crate::phy::diag_log("[try_new] rx_ring ok, assembling struct ...");
607        let phy = Ip101::new(board.phy_addr);
608        let mut ethernet = Self { tx_ring, rx_ring, phy, mac_addr };
609        crate::phy::diag_log("[try_new] struct built, calling mac_init ...");
610        ethernet.mac_init()?;
611        crate::phy::diag_log("[try_new] mac_init ok, set_mac_address ...");
612        ethernet.set_mac_address(mac_addr);
613        crate::phy::diag_log("[try_new] set_mac_address ok, calling phy.init ...");
614        ethernet
615            .with_phy(|phy, eth| phy.init(eth))
616            .map_err(MacError::Phy)?;
617        crate::phy::diag_log("[try_new] phy.init ok, reset_dma ...");
618        ethernet.reset_dma();
619        crate::phy::diag_log("[try_new] DONE");
620
621        Ok(ethernet)
622    }
623
624    /// Returns the currently programmed MAC address.
625    pub fn mac_addr(&self) -> [u8; 6] {
626        self.mac_addr
627    }
628
629    /// Programs the primary station MAC address into hardware.
630    pub fn set_mac_address(&mut self, mac_addr: [u8; 6]) {
631        self.mac_addr = mac_addr;
632        regs::write_mac_address(mac_addr);
633    }
634
635    /// Compatibility alias for [`Ethernet::set_mac_address`].
636    pub fn set_mac_addr(&mut self, mac_addr: [u8; 6]) {
637        self.set_mac_address(mac_addr);
638    }
639
640    /// Applies baseline MAC defaults and programs the MDIO CSR clock range.
641    ///
642    /// This does not start frame traffic yet; it only prepares the MAC core for
643    /// later DMA/ring activation.
644    pub fn mac_init(&mut self) -> Result<(), MacError> {
645        crate::phy::diag_log("[mac_init] enter, Dma::dma_reset() ...");
646        Dma::dma_reset().map_err(|_| MacError::ResetTimeout)?;
647        crate::phy::diag_log("[mac_init] dma_reset ok, program_mdio_clock_range ...");
648        self.program_mdio_clock_range(DEFAULT_AHB_CLOCK_HZ);
649        crate::phy::diag_log("[mac_init] mdio clock ok, write MAC CONFIG ...");
650        regs::write(regs::mac::CONFIG, mac_default_config());
651        crate::phy::diag_log("[mac_init] MAC CONFIG ok, write FRAME_FILTER ...");
652        regs::write(regs::mac::FRAME_FILTER, MACFFILT_DEFAULT);
653        crate::phy::diag_log("[mac_init] FRAME_FILTER ok, set_mac_address ...");
654        self.set_mac_address(self.mac_addr);
655        crate::phy::diag_log("[mac_init] DONE");
656        Ok(())
657    }
658
659    /// Reinitializes the TX and RX descriptor rings, flushes their CPU-side
660    /// state out to DMA-visible memory, and programs the DMA peripheral with
661    /// their base addresses. Must be called only after [`Dma::dma_reset`]
662    /// (e.g. via [`Self::mac_init`]) — writing the descriptor-list registers
663    /// or running the L1/L2 cache writeback ROM helper while the EMAC has
664    /// not finished reset hangs the AHB bus (intermittent ~10 % rate on
665    /// warm reboot, characterised 2026-04-27).
666    pub fn reset_dma(&mut self) {
667        self.reset_descriptor_state();
668        self.flush_dma_visibility();
669        self.program_dma_descriptor_lists();
670    }
671
672    /// Resets descriptor ring software state (links, ownership, indices) and
673    /// programs the DMA descriptor-list peripheral registers — but does
674    /// **not** flush caches. Safe to call from `try_new` before the DMA
675    /// engine has been started; the cache flush is deferred to `start()`
676    /// where the chip has had more time to settle out of warm-reboot state.
677    pub fn reset_descriptor_state(&mut self) {
678        crate::phy::diag_log("[reset_dma] tx_ring.reset ...");
679        self.tx_ring.reset();
680        crate::phy::diag_log("[reset_dma] rx_ring.reset ...");
681        self.rx_ring.reset();
682    }
683
684    /// Pushes ring CPU-side state out to DMA-visible memory using
685    /// per-descriptor `Cache_WriteBack_Addr(CACHE_MAP_L2_CACHE, ...)` —
686    /// this matches IDF's `cache_ll_l2_writeback_cache_addr` path and
687    /// requires `Dma::init_l2_cache_mode` to have been called once at
688    /// driver init so the ROM helper has a valid mode/ways/line-size to
689    /// work with.
690    pub fn flush_dma_visibility(&self) {
691        crate::phy::diag_log("[reset_dma] flush descriptors (per-desc L2) ...");
692        self.tx_ring.flush_all();
693        self.rx_ring.flush_all();
694    }
695
696    /// Programs the DMA `TX_DESC_LIST` / `RX_DESC_LIST` peripheral registers
697    /// with the current ring base addresses. Must be called only after
698    /// `Dma::dma_reset` (e.g. via `mac_init`).
699    pub fn program_dma_descriptor_lists(&self) {
700        crate::phy::diag_log("[reset_dma] write TX_DESC_LIST ...");
701        regs::write(
702            regs::dma::TX_DESC_LIST,
703            self.tx_ring.descriptors_ptr() as usize as u32,
704        );
705        crate::phy::diag_log("[reset_dma] write RX_DESC_LIST ...");
706        regs::write(
707            regs::dma::RX_DESC_LIST,
708            self.rx_ring.descriptors_ptr() as usize as u32,
709        );
710        crate::phy::diag_log("[reset_dma] DONE");
711    }
712
713    /// Returns `true` when the PHY currently reports an active link.
714    pub fn phy_link_status(&mut self) -> bool {
715        matches!(
716            self.with_phy(|phy, eth| phy.poll_link(eth)),
717            LinkState::Up { .. }
718        )
719    }
720
721    /// Runs PHY negotiation and applies the resulting speed/duplex to the MAC.
722    pub fn phy_negotiate(&mut self) -> Result<(Speed, Duplex), PhyError> {
723        let (speed, duplex) = self.with_phy(|phy, eth| phy.negotiate(eth))?;
724        self.set_speed(speed);
725        self.set_duplex(duplex);
726        Ok((speed, duplex))
727    }
728
729    /// Programs the MAC and RMII divider for the selected line speed.
730    pub fn set_speed(&mut self, speed: Speed) {
731        let mut config = regs::read(regs::mac::CONFIG);
732        match speed {
733            Speed::Mbps100 => {
734                config |= regs::bits::maccfg::FES | regs::bits::maccfg::PS;
735            }
736            Speed::Mbps10 => {
737                config = (config & !regs::bits::maccfg::FES) | regs::bits::maccfg::PS;
738            }
739        }
740
741        regs::write(regs::mac::CONFIG, config);
742        configure_speed_divider(speed);
743    }
744
745    /// Programs the MAC duplex mode bit.
746    pub fn set_duplex(&mut self, duplex: Duplex) {
747        let mut config = regs::read(regs::mac::CONFIG);
748        match duplex {
749            Duplex::Full => config |= regs::bits::maccfg::DM,
750            Duplex::Half => config &= !regs::bits::maccfg::DM,
751        }
752
753        regs::write(regs::mac::CONFIG, config);
754    }
755
756    /// Starts MAC TX/RX and the DMA engines.
757    ///
758    /// The startup sequence reapplies MAC defaults, initializes DMA operating
759    /// modes, rebuilds both descriptor rings, then enables TX/RX in both MAC
760    /// and DMA.
761    pub fn start(&mut self) -> Result<(), MacError> {
762        self.mac_init()?;
763        Dma::dma_init();
764        self.reset_dma();
765
766        let config =
767            regs::read(regs::mac::CONFIG) | regs::bits::maccfg::TE | regs::bits::maccfg::RE;
768        regs::write(regs::mac::CONFIG, config);
769
770        let op_mode =
771            regs::read(regs::dma::OP_MODE) | regs::bits::dmaopmode::ST | regs::bits::dmaopmode::SR;
772        regs::write(regs::dma::OP_MODE, op_mode);
773        Ok(())
774    }
775
776    /// Stops DMA first and then disables MAC TX/RX.
777    ///
778    /// The call waits until the DMA state machine reports stopped TX/RX paths
779    /// or returns [`MacError::StopTimeout`] if the hardware does not quiesce.
780    pub fn stop(&mut self) -> Result<(), MacError> {
781        let op_mode = regs::read(regs::dma::OP_MODE)
782            & !(regs::bits::dmaopmode::ST | regs::bits::dmaopmode::SR);
783        regs::write(regs::dma::OP_MODE, op_mode);
784
785        let config =
786            regs::read(regs::mac::CONFIG) & !(regs::bits::maccfg::TE | regs::bits::maccfg::RE);
787        regs::write(regs::mac::CONFIG, config);
788
789        wait_for_dma_stop(|| regs::read(regs::dma::STATUS))
790    }
791
792    /// Enqueues a frame for transmission through the TX descriptor ring.
793    pub fn transmit(&mut self, frame: &[u8]) -> Result<(), DescriptorError> {
794        self.tx_ring.transmit(frame)
795    }
796
797    /// Returns the next received frame, if one is ready in the RX ring.
798    pub fn receive(&mut self) -> Option<(usize, &[u8])> {
799        self.rx_ring.receive()
800    }
801
802    /// Returns the current RX descriptor back to DMA ownership.
803    pub fn pop_rx(&mut self) {
804        self.rx_ring.pop();
805    }
806
807    /// Stops the device and powers down the EMAC clock tree.
808    ///
809    /// This also disables DMA and MAC interrupt sources so the peripheral can be
810    /// cleanly reinitialized later.
811    pub fn shutdown(&mut self) {
812        let _ = self.stop();
813        Dma::disable_interrupts();
814        regs::write(regs::mac::INTMASK, 0);
815        Dma::clear_interrupt_status(Dma::read_interrupt_status());
816        disable_emac_clock_tree();
817    }
818
819    /// Returns cumulative TX ring statistics.
820    pub fn tx_stats(&self) -> TxRingStats {
821        self.tx_ring.stats()
822    }
823
824    /// Returns cumulative RX ring statistics.
825    pub fn rx_stats(&self) -> RxRingStats {
826        self.rx_ring.stats()
827    }
828
829    /// Handles one DMA interrupt snapshot and performs lightweight recovery.
830    ///
831    /// The returned status value is the decoded snapshot that was processed, so
832    /// callers can log or inspect the reason for the recovery path.
833    pub fn handle_dma_interrupt(&mut self) -> DmaInterruptStatus {
834        let status = Dma::read_interrupt_status();
835        self.apply_dma_interrupt_status(status);
836        Dma::clear_interrupt_status(status);
837        status
838    }
839
840    fn program_mdio_clock_range(&mut self, ahb_clock_hz: u32) {
841        let mut miiaddr = regs::read(regs::mac::MII_ADDR);
842        miiaddr &= !regs::bits::miiaddr::CSR_CLOCK_RANGE_MASK;
843        miiaddr |= csr_clock_range_bits(ahb_clock_hz) << regs::bits::miiaddr::CSR_CLOCK_RANGE_SHIFT;
844        regs::write(regs::mac::MII_ADDR, miiaddr);
845    }
846
847    fn with_phy<R>(&mut self, f: impl FnOnce(&mut Ip101, &mut Self) -> R) -> R {
848        let mut phy = self.phy;
849        let result = f(&mut phy, self);
850        self.phy = phy;
851        result
852    }
853
854    fn apply_dma_interrupt_status(&mut self, status: DmaInterruptStatus) {
855        if status.has_fatal_bus_error() {
856            Dma::dma_init();
857            self.reset_dma();
858            let op_mode = regs::read(regs::dma::OP_MODE)
859                | regs::bits::dmaopmode::ST
860                | regs::bits::dmaopmode::SR;
861            regs::write(regs::dma::OP_MODE, op_mode);
862        } else if status.has_rx_overflow()
863            || status.has_rx_buffer_unavailable()
864            || status.has_rx_process_stopped()
865        {
866            self.rx_ring.handle_overflow();
867        }
868
869        if status.has_tx_buffer_unavailable() || status.has_tx_underflow() {
870            Dma::demand_tx_poll();
871        }
872
873        if status.has_rx_interrupt() || status.has_early_receive_interrupt() {
874            Dma::demand_rx_poll();
875        }
876
877        #[cfg(target_arch = "riscv32")]
878        {
879            if status.should_kick_rx() {
880                wake_rx_task();
881            }
882
883            if status.should_kick_tx() {
884                wake_tx_task();
885            }
886        }
887    }
888}
889
890impl Drop for Ethernet<'_> {
891    fn drop(&mut self) {
892        self.shutdown();
893    }
894}
895
896fn mac_default_config() -> u32 {
897    regs::bits::maccfg::ACS
898        | regs::bits::maccfg::DM
899        | regs::bits::maccfg::PS
900        | regs::bits::maccfg::FES
901        | regs::bits::maccfg::CST
902        | (MAC_DEFAULT_IFG_96_BIT_TIMES << regs::bits::maccfg::IFG_SHIFT)
903}
904
905fn csr_clock_range_bits(ahb_clock_hz: u32) -> u32 {
906    match ahb_clock_hz {
907        20_000_000..=34_999_999 => 0b0010,
908        35_000_000..=59_999_999 => 0b0011,
909        60_000_000..=99_999_999 => 0b0000,
910        100_000_000..=149_999_999 => 0b0001,
911        150_000_000..=249_999_999 => 0b0100,
912        250_000_000..=300_000_000 => 0b0101,
913        _ => 0b0001,
914    }
915}
916
917fn wait_for_dma_stop<F>(mut read_status: F) -> Result<(), MacError>
918where
919    F: FnMut() -> u32,
920{
921    for _ in 0..MAC_STOP_TIMEOUT_POLLS {
922        let status = read_status();
923        let rx_state = (status & regs::bits::dmastatus::RS_MASK) >> regs::bits::dmastatus::RS_SHIFT;
924        let tx_state = (status & regs::bits::dmastatus::TS_MASK) >> regs::bits::dmastatus::TS_SHIFT;
925
926        if rx_state == regs::bits::dmastatus::PROCESS_STOPPED
927            && tx_state == regs::bits::dmastatus::PROCESS_STOPPED
928        {
929            return Ok(());
930        }
931    }
932
933    Err(MacError::StopTimeout)
934}
935
936#[cfg(target_arch = "riscv32")]
937async fn link_poll_delay() {
938    embassy_time::Timer::after(embassy_time::Duration::from_millis(LINK_POLL_INTERVAL_MS)).await;
939}
940
941#[cfg(test)]
942fn link_poll_interval_ms() -> u64 {
943    LINK_POLL_INTERVAL_MS
944}
945
946// ---------------------------------------------------------------------------
947// Pure-data helpers extracted from `ethernet_task` futures so they can be
948// host-tested without standing up an embassy executor + Runner. Each function
949// is side-effecting on the global diagnostic atomics defined above.
950// ---------------------------------------------------------------------------
951
952/// Read a 4-byte big-endian word at `buf[off..off+4]`. Caller must guarantee
953/// `off + 4 <= buf.len()`.
954#[inline]
955fn read_be_u32(buf: &[u8], off: usize) -> u32 {
956    (u32::from(buf[off]) << 24)
957        | (u32::from(buf[off + 1]) << 16)
958        | (u32::from(buf[off + 2]) << 8)
959        | u32::from(buf[off + 3])
960}
961
962/// Snapshot up to 16 × 4 bytes of `buf` into `slot`. Used for hex-dumping
963/// large or DHCP frames into static storage that diagnostic readers can
964/// poll from outside the rx_fut.
965#[inline]
966fn hex_dump_into(slot: &[core::sync::atomic::AtomicU32; 16], buf: &[u8], len: usize) {
967    use core::sync::atomic::Ordering::Relaxed;
968    let n = (len / 4).min(16);
969    for (i, cell) in slot.iter().take(n).enumerate() {
970        cell.store(read_be_u32(buf, i * 4), Relaxed);
971    }
972}
973
974/// Bumps the per-ethertype RX classifiers and last-* diagnostic atomics
975/// based on the contents of a received frame. Pure side-effecting helper —
976/// no MMIO, no waker. The 4-byte big-endian destination MAC prefix and the
977/// 2-byte ethertype are extracted only when `len >= 14`. ICMP / DHCP
978/// detection layers on top of IPv4.
979///
980/// The caller guarantees `len > 0` (an empty descriptor short-circuits in
981/// `rx_fut` before reaching here) and `len <= buf.len()` (the descriptor
982/// payload is always the full frame).
983pub(crate) fn classify_rx_frame(buf: &[u8], len: usize) {
984    use core::sync::atomic::Ordering::Relaxed;
985
986    if len >= 200 {
987        RX_LAST_LARGE_FRAME_LEN.store(len as u32, Relaxed);
988        hex_dump_into(&RX_LAST_LARGE_FRAME, buf, len);
989    }
990
991    if len < 14 {
992        return;
993    }
994
995    let ethertype = (u16::from(buf[12]) << 8) | u16::from(buf[13]);
996    RX_LAST_ETHERTYPE.store(u32::from(ethertype), Relaxed);
997    RX_LAST_DST_MAC_HI.store(read_be_u32(buf, 0), Relaxed);
998
999    match ethertype {
1000        0x0806 => {
1001            RX_ARP.fetch_add(1, Relaxed);
1002        }
1003        0x0800 => {
1004            RX_IPV4.fetch_add(1, Relaxed);
1005            // IPv4 header starts at byte 14. Protocol byte at offset 14+9 = 23.
1006            if len >= 24 && buf[23] == 0x01 {
1007                RX_ICMP.fetch_add(1, Relaxed);
1008            }
1009            // UDP src/dst port at IP+20 + 0/2. Capture DHCP (67/68) frames
1010            // to a static for diagnosis.
1011            if len >= 14 + 20 + 4 && buf[23] == 0x11 {
1012                let src_port = (u16::from(buf[14 + 20]) << 8) | u16::from(buf[14 + 20 + 1]);
1013                let dst_port = (u16::from(buf[14 + 20 + 2]) << 8) | u16::from(buf[14 + 20 + 3]);
1014                if src_port == 67 || src_port == 68 || dst_port == 67 || dst_port == 68 {
1015                    RX_DHCP_FRAMES.fetch_add(1, Relaxed);
1016                    hex_dump_into(&RX_LAST_DHCP_FRAME, buf, len);
1017                }
1018            }
1019        }
1020        _ => {}
1021    }
1022}
1023
1024/// Records the headers of an outbound TX frame into the diagnostic atomics
1025/// (length, dst MAC prefix, src MAC prefix, ethertype). Mirrors the
1026/// `record_tx_frame_metadata` block from `tx_fut`.
1027pub(crate) fn record_tx_frame_metadata(buf: &[u8]) {
1028    use core::sync::atomic::Ordering::Relaxed;
1029
1030    TX_LAST_LEN.store(buf.len() as u32, Relaxed);
1031    if buf.len() >= 14 {
1032        let dst_hi = read_be_u32(buf, 0);
1033        let src_hi = read_be_u32(buf, 6);
1034        let etype = (u32::from(buf[12]) << 8) | u32::from(buf[13]);
1035        TX_LAST_DST_MAC_HI.store(dst_hi, Relaxed);
1036        TX_LAST_SRC_MAC_HI.store(src_hi, Relaxed);
1037        TX_LAST_ETHERTYPE.store(etype, Relaxed);
1038    }
1039}
1040
1041/// Action returned by `link_state_action` — captures the three observable
1042/// transitions of the link state machine in `link_fut`.
1043#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1044pub enum LinkAction {
1045    /// Link state unchanged — no observable side-effect.
1046    NoChange,
1047    /// PHY just came up. The caller must run `phy_negotiate()` and only
1048    /// flip the channel state on success.
1049    AttemptNegotiate,
1050    /// PHY just went down. The caller must report `LinkState::Down` to
1051    /// embassy-net.
1052    GoDown,
1053}
1054
1055/// Pure state machine for `link_fut`: given the previous tracked link state
1056/// (`prev_link_up`) and the current PHY readout (`is_up_now`), returns the
1057/// action to take. Side-effects on the channel runner happen in the caller.
1058#[inline]
1059pub(crate) const fn link_state_action(prev_link_up: bool, is_up_now: bool) -> LinkAction {
1060    match (prev_link_up, is_up_now) {
1061        (false, true) => LinkAction::AttemptNegotiate,
1062        (true, false) => LinkAction::GoDown,
1063        _ => LinkAction::NoChange,
1064    }
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069    use super::{
1070        classify_rx_frame,
1071        clock::{emac_clock_tree_enabled, emac_reset_asserted},
1072        csr_clock_range_bits,
1073        descriptors::{OwnedBy, RDES0_FL_SHIFT, RDES0_FS, RDES0_LS},
1074        link_poll_interval_ms, link_state_action, mac_default_config, record_tx_frame_metadata,
1075        wait_for_dma_stop, DmaInterruptStatus, Duplex, Ethernet, LinkAction, MacError, RX_ARP,
1076        RX_DHCP_FRAMES, RX_ICMP, RX_IPV4, RX_LAST_DHCP_FRAME, RX_LAST_ETHERTYPE,
1077        RX_LAST_LARGE_FRAME, RX_LAST_LARGE_FRAME_LEN, TX_LAST_DST_MAC_HI, TX_LAST_ETHERTYPE,
1078        TX_LAST_LEN, TX_LAST_SRC_MAC_HI,
1079    };
1080    use crate::{
1081        regs, zeroed_rx_descriptors, zeroed_tx_descriptors, Speed, StaticDmaResources, BUF_SIZE,
1082        RX_DESC_COUNT, TX_DESC_COUNT,
1083    };
1084
1085    const IRQ_FUZZ_ITERATIONS: usize = 2_048;
1086    const DOMAIN_MAC: [u8; 6] = [0x02, 0x44, 0x52, 0x49, 0x56, 0x01];
1087
1088    fn advance_lcg(state: &mut u32) -> u32 {
1089        *state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1090        *state
1091    }
1092
1093    fn with_bootstrapped_ethernet<R>(scenario: impl FnOnce(&mut Ethernet<'_>) -> R) -> R {
1094        regs::reset_test_registers();
1095        let mut tx_desc = zeroed_tx_descriptors();
1096        let mut rx_desc = zeroed_rx_descriptors();
1097        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1098        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1099        let mut ethernet = Ethernet::new(
1100            DOMAIN_MAC,
1101            &mut tx_desc,
1102            &mut rx_desc,
1103            &mut tx_buf,
1104            &mut rx_buf,
1105        );
1106
1107        scenario(&mut ethernet)
1108    }
1109
1110    #[test]
1111    fn domain_bootstrap_declares_station_identity_and_empty_rings() {
1112        with_bootstrapped_ethernet(|ethernet| {
1113            assert_eq!(ethernet.mac_addr(), DOMAIN_MAC);
1114            assert_eq!(regs::read(regs::mac::ADDR0_LOW), 0x4952_4402);
1115            assert_eq!(
1116                regs::read(regs::mac::ADDR0_HIGH),
1117                regs::bits::macaddr::AE0 | 0x0000_0156
1118            );
1119            assert_eq!(ethernet.tx_stats(), Default::default());
1120            assert_eq!(ethernet.rx_stats(), Default::default());
1121            assert_eq!(ethernet.tx_ring.fuzz_current_index(), 0);
1122            assert_eq!(ethernet.rx_ring.fuzz_current_index(), 0);
1123        });
1124    }
1125
1126    #[test]
1127    fn domain_transmit_frame_queues_dma_work_and_advances_tx_cursor() {
1128        with_bootstrapped_ethernet(|ethernet| {
1129            let frame = [0xA5; 64];
1130
1131            ethernet.transmit(&frame).unwrap();
1132
1133            assert_eq!(ethernet.tx_stats().transmitted_frames, 1);
1134            assert_eq!(ethernet.tx_stats().ring_full_events, 0);
1135            assert_eq!(ethernet.tx_ring.fuzz_current_index(), 1);
1136            assert_eq!(regs::read(regs::dma::TX_POLL_DEMAND), 1);
1137        });
1138    }
1139
1140    #[test]
1141    fn domain_receive_frame_delivers_payload_until_driver_acknowledges_it() {
1142        with_bootstrapped_ethernet(|ethernet| {
1143            let payload = [0xC3; 96];
1144            let status = RDES0_FS | RDES0_LS | ((payload.len() as u32) << RDES0_FL_SHIFT);
1145            ethernet
1146                .rx_ring
1147                .fuzz_seed_current(status, OwnedBy::Cpu, &payload);
1148
1149            {
1150                let (len, frame) = ethernet.receive().unwrap();
1151                assert_eq!(len, payload.len());
1152                assert_eq!(frame, &payload);
1153            }
1154
1155            assert_eq!(ethernet.rx_stats().received_frames, 1);
1156            assert_eq!(ethernet.rx_ring.fuzz_current_index(), 0);
1157
1158            ethernet.pop_rx();
1159
1160            assert_eq!(ethernet.rx_ring.fuzz_current_index(), 1);
1161        });
1162    }
1163
1164    #[test]
1165    fn domain_rx_overflow_interrupt_rebuilds_receive_path() {
1166        with_bootstrapped_ethernet(|ethernet| {
1167            ethernet.apply_dma_interrupt_status(DmaInterruptStatus::from_raw(
1168                regs::bits::dmastatus::OVF | regs::bits::dmastatus::AIS,
1169            ));
1170
1171            assert_eq!(ethernet.rx_stats().overflow_resets, 1);
1172            assert_eq!(ethernet.rx_ring.fuzz_current_index(), 0);
1173            assert_eq!(regs::read(regs::dma::RX_POLL_DEMAND), 1);
1174        });
1175    }
1176
1177    #[test]
1178    fn domain_fatal_dma_error_restores_transmit_path() {
1179        with_bootstrapped_ethernet(|ethernet| {
1180            ethernet.apply_dma_interrupt_status(DmaInterruptStatus::from_raw(
1181                regs::bits::dmastatus::FBI | regs::bits::dmastatus::AIS,
1182            ));
1183
1184            assert_ne!(
1185                regs::read(regs::dma::OP_MODE) & regs::bits::dmaopmode::ST,
1186                0
1187            );
1188            assert_ne!(
1189                regs::read(regs::dma::OP_MODE) & regs::bits::dmaopmode::SR,
1190                0
1191            );
1192            assert!(ethernet.transmit(&[0x5A; 64]).is_ok());
1193            assert_eq!(ethernet.tx_stats().transmitted_frames, 1);
1194        });
1195    }
1196
1197    #[test]
1198    fn bdd_given_bootstrapped_driver_when_station_mac_changes_then_hardware_identity_changes() {
1199        with_bootstrapped_ethernet(|ethernet| {
1200            let new_mac = [0x02, 0x10, 0x20, 0x30, 0x40, 0x50];
1201
1202            ethernet.set_mac_address(new_mac);
1203
1204            assert_eq!(ethernet.mac_addr(), new_mac);
1205            assert_eq!(regs::read(regs::mac::ADDR0_LOW), 0x3020_1002);
1206            assert_eq!(
1207                regs::read(regs::mac::ADDR0_HIGH),
1208                regs::bits::macaddr::AE0 | 0x0000_5040
1209            );
1210        });
1211    }
1212
1213    #[test]
1214    fn bdd_given_tx_ring_is_full_when_stack_transmits_then_backpressure_is_reported() {
1215        with_bootstrapped_ethernet(|ethernet| {
1216            for slot in 0..TX_DESC_COUNT {
1217                assert_eq!(ethernet.tx_ring.fuzz_current_index(), slot);
1218                ethernet.transmit(&[slot as u8; 64]).unwrap();
1219            }
1220
1221            let result = ethernet.transmit(&[0xEE; 64]);
1222
1223            assert_eq!(result, Err(super::DescriptorError::RingFull));
1224            assert_eq!(ethernet.tx_stats().transmitted_frames, TX_DESC_COUNT as u32);
1225            assert_eq!(ethernet.tx_stats().ring_full_events, 1);
1226            assert_eq!(ethernet.tx_ring.fuzz_current_index(), 0);
1227        });
1228    }
1229
1230    #[test]
1231    fn bdd_given_runt_frame_when_driver_receives_then_frame_is_dropped_and_dma_rearmed() {
1232        with_bootstrapped_ethernet(|ethernet| {
1233            let payload = [0x11; 32];
1234            let status = RDES0_FS | RDES0_LS | ((payload.len() as u32) << RDES0_FL_SHIFT);
1235            ethernet
1236                .rx_ring
1237                .fuzz_seed_current(status, OwnedBy::Cpu, &payload);
1238
1239            let received = ethernet.receive();
1240
1241            assert!(received.is_none());
1242            assert_eq!(ethernet.rx_stats().received_frames, 0);
1243            assert_eq!(ethernet.rx_stats().runt_frames, 1);
1244            assert_eq!(ethernet.rx_ring.fuzz_current_index(), 1);
1245        });
1246    }
1247
1248    #[test]
1249    fn bdd_given_rx_and_tx_recovery_bits_when_interrupt_is_handled_then_dma_paths_are_polled() {
1250        with_bootstrapped_ethernet(|ethernet| {
1251            regs::write(
1252                regs::dma::STATUS,
1253                regs::bits::dmastatus::RI
1254                    | regs::bits::dmastatus::ERI
1255                    | regs::bits::dmastatus::TU
1256                    | regs::bits::dmastatus::UNF
1257                    | regs::bits::dmastatus::AIS
1258                    | regs::bits::dmastatus::NIS,
1259            );
1260
1261            let status = ethernet.handle_dma_interrupt();
1262
1263            assert!(status.should_kick_rx());
1264            assert!(status.should_kick_tx());
1265            assert_eq!(regs::read(regs::dma::RX_POLL_DEMAND), 1);
1266            assert_eq!(regs::read(regs::dma::TX_POLL_DEMAND), 1);
1267            assert_eq!(regs::read(regs::dma::STATUS), status.clear_mask());
1268        });
1269    }
1270
1271    #[test]
1272    fn bdd_given_running_driver_when_shutdown_is_requested_then_peripheral_is_quiesced() {
1273        with_bootstrapped_ethernet(|ethernet| {
1274            ethernet.start().unwrap();
1275
1276            ethernet.shutdown();
1277
1278            assert_eq!(regs::read(regs::dma::INT_EN), 0);
1279            assert_eq!(regs::read(regs::mac::INTMASK), 0);
1280            assert!(!emac_clock_tree_enabled());
1281            assert!(emac_reset_asserted());
1282        });
1283    }
1284
1285    #[test]
1286    fn mac_default_config_matches_expected_defaults() {
1287        let config = mac_default_config();
1288
1289        assert_ne!(config & regs::bits::maccfg::ACS, 0);
1290        assert_ne!(config & regs::bits::maccfg::DM, 0);
1291        assert_ne!(config & regs::bits::maccfg::FES, 0);
1292        assert_ne!(config & regs::bits::maccfg::PS, 0);
1293        assert_ne!(config & regs::bits::maccfg::CST, 0);
1294        assert_eq!(
1295            (config & regs::bits::maccfg::IFG_MASK) >> regs::bits::maccfg::IFG_SHIFT,
1296            0
1297        );
1298    }
1299
1300    #[test]
1301    fn csr_clock_range_matches_expected_bucket() {
1302        assert_eq!(csr_clock_range_bits(30_000_000), 0b0010);
1303        assert_eq!(csr_clock_range_bits(50_000_000), 0b0011);
1304        assert_eq!(csr_clock_range_bits(80_000_000), 0b0000);
1305        assert_eq!(csr_clock_range_bits(120_000_000), 0b0001);
1306        assert_eq!(csr_clock_range_bits(200_000_000), 0b0100);
1307        assert_eq!(csr_clock_range_bits(300_000_000), 0b0101);
1308        assert_eq!(csr_clock_range_bits(10_000_000), 0b0001);
1309    }
1310
1311    /// Regression guard for the `link_poll_delay` performance bug
1312    /// (`yield_now × 500` re-polled every join3 sibling and burned ~50 % of
1313    /// wall time on cache ROM calls). Mirrors the const-level assert next
1314    /// to the constant itself so the failure mode is visible from the
1315    /// test report, not just the compile error.
1316    #[test]
1317    fn link_poll_interval_stays_in_sane_range() {
1318        let ms = link_poll_interval_ms();
1319        assert!(
1320            ms >= 50,
1321            "LINK_POLL_INTERVAL_MS = {} ms is too aggressive — \
1322             see feedback_p4_yield_now_antipattern in project memory",
1323            ms,
1324        );
1325        assert!(
1326            ms <= 500,
1327            "LINK_POLL_INTERVAL_MS = {} ms is too long — \
1328             link state changes will go unnoticed for over half a second",
1329            ms,
1330        );
1331    }
1332
1333    #[test]
1334    fn wait_for_dma_stop_succeeds_when_states_reach_stopped() {
1335        let mut polls = 0;
1336        let result = wait_for_dma_stop(|| {
1337            polls += 1;
1338            if polls < 4 {
1339                (0b011 << regs::bits::dmastatus::RS_SHIFT)
1340                    | (0b011 << regs::bits::dmastatus::TS_SHIFT)
1341            } else {
1342                0
1343            }
1344        });
1345
1346        assert_eq!(result, Ok(()));
1347        assert_eq!(polls, 4);
1348    }
1349
1350    #[test]
1351    fn wait_for_dma_stop_times_out_when_dma_keeps_running() {
1352        let result = wait_for_dma_stop(|| {
1353            (0b111 << regs::bits::dmastatus::RS_SHIFT) | (0b111 << regs::bits::dmastatus::TS_SHIFT)
1354        });
1355
1356        assert_eq!(result, Err(MacError::StopTimeout));
1357    }
1358
1359    #[test]
1360    fn enums_stay_stable() {
1361        assert_eq!(Speed::Mbps10 as u8, 0);
1362        assert_eq!(Speed::Mbps100 as u8, 1);
1363        assert_eq!(Duplex::Half as u8, 0);
1364        assert_eq!(Duplex::Full as u8, 1);
1365    }
1366
1367    #[test]
1368    fn ethernet_new_from_static_resources_uses_single_dma_resource_owner() {
1369        regs::reset_test_registers();
1370        let mut resources = StaticDmaResources::new();
1371
1372        let ethernet = Ethernet::new_from_static_resources(DOMAIN_MAC, &mut resources);
1373
1374        assert_eq!(ethernet.mac_addr(), DOMAIN_MAC);
1375        assert_eq!(ethernet.tx_stats(), Default::default());
1376        assert_eq!(ethernet.rx_stats(), Default::default());
1377    }
1378
1379    #[test]
1380    fn mac_init_programs_mdio_filter_config_and_station_address() {
1381        with_bootstrapped_ethernet(|ethernet| {
1382            regs::write(regs::mac::CONFIG, 0);
1383            regs::write(regs::mac::FRAME_FILTER, 0);
1384            regs::write(regs::mac::MII_ADDR, 0);
1385
1386            ethernet.mac_init().unwrap();
1387
1388            assert_eq!(regs::read(regs::mac::CONFIG), mac_default_config());
1389            assert_eq!(regs::read(regs::mac::FRAME_FILTER), super::MACFFILT_DEFAULT);
1390            assert_eq!(
1391                (regs::read(regs::mac::MII_ADDR) & regs::bits::miiaddr::CSR_CLOCK_RANGE_MASK)
1392                    >> regs::bits::miiaddr::CSR_CLOCK_RANGE_SHIFT,
1393                csr_clock_range_bits(super::DEFAULT_AHB_CLOCK_HZ)
1394            );
1395            assert_eq!(ethernet.mac_addr(), DOMAIN_MAC);
1396        });
1397    }
1398
1399    #[test]
1400    fn start_enables_mac_and_dma_datapaths() {
1401        with_bootstrapped_ethernet(|ethernet| {
1402            ethernet.start().unwrap();
1403
1404            let mac_config = regs::read(regs::mac::CONFIG);
1405            let op_mode = regs::read(regs::dma::OP_MODE);
1406            assert_ne!(mac_config & regs::bits::maccfg::TE, 0);
1407            assert_ne!(mac_config & regs::bits::maccfg::RE, 0);
1408            assert_ne!(op_mode & regs::bits::dmaopmode::ST, 0);
1409            assert_ne!(op_mode & regs::bits::dmaopmode::SR, 0);
1410            let interrupt_mask = regs::read(regs::dma::INT_EN);
1411            assert_ne!(interrupt_mask & regs::bits::dmainten::TIE, 0);
1412            assert_ne!(interrupt_mask & regs::bits::dmainten::RIE, 0);
1413            assert_ne!(interrupt_mask & regs::bits::dmainten::AIE, 0);
1414            assert_ne!(interrupt_mask & regs::bits::dmainten::NIE, 0);
1415        });
1416    }
1417
1418    #[test]
1419    fn stop_disables_mac_and_dma_datapaths_when_hardware_reports_stopped() {
1420        with_bootstrapped_ethernet(|ethernet| {
1421            regs::write(
1422                regs::dma::OP_MODE,
1423                regs::bits::dmaopmode::ST | regs::bits::dmaopmode::SR,
1424            );
1425            regs::write(
1426                regs::mac::CONFIG,
1427                regs::bits::maccfg::TE | regs::bits::maccfg::RE | regs::bits::maccfg::DM,
1428            );
1429            regs::write(regs::dma::STATUS, 0);
1430
1431            assert_eq!(ethernet.stop(), Ok(()));
1432
1433            assert_eq!(
1434                regs::read(regs::dma::OP_MODE)
1435                    & (regs::bits::dmaopmode::ST | regs::bits::dmaopmode::SR),
1436                0
1437            );
1438            assert_eq!(
1439                regs::read(regs::mac::CONFIG) & (regs::bits::maccfg::TE | regs::bits::maccfg::RE),
1440                0
1441            );
1442            assert_ne!(regs::read(regs::mac::CONFIG) & regs::bits::maccfg::DM, 0);
1443        });
1444    }
1445
1446    #[test]
1447    fn set_speed_updates_speed_bits_and_preserves_unrelated_config() {
1448        regs::reset_test_registers();
1449        let mut tx_desc = zeroed_tx_descriptors();
1450        let mut rx_desc = zeroed_rx_descriptors();
1451        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1452        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1453        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1454        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1455
1456        regs::write(
1457            regs::mac::CONFIG,
1458            regs::bits::maccfg::JD | regs::bits::maccfg::DM | regs::bits::maccfg::FES,
1459        );
1460
1461        ethernet.set_speed(Speed::Mbps10);
1462        let config_10 = regs::read(regs::mac::CONFIG);
1463        assert_eq!(config_10 & regs::bits::maccfg::FES, 0);
1464        assert_ne!(config_10 & regs::bits::maccfg::PS, 0);
1465        assert_ne!(config_10 & regs::bits::maccfg::JD, 0);
1466        assert_ne!(config_10 & regs::bits::maccfg::DM, 0);
1467
1468        ethernet.set_speed(Speed::Mbps100);
1469        let config_100 = regs::read(regs::mac::CONFIG);
1470        assert_ne!(config_100 & regs::bits::maccfg::FES, 0);
1471        assert_ne!(config_100 & regs::bits::maccfg::PS, 0);
1472        assert_ne!(config_100 & regs::bits::maccfg::JD, 0);
1473        assert_ne!(config_100 & regs::bits::maccfg::DM, 0);
1474    }
1475
1476    #[test]
1477    fn set_duplex_only_toggles_duplex_bit() {
1478        regs::reset_test_registers();
1479        let mut tx_desc = zeroed_tx_descriptors();
1480        let mut rx_desc = zeroed_rx_descriptors();
1481        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1482        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1483        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1484        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1485
1486        let base = regs::bits::maccfg::JD | regs::bits::maccfg::PS | regs::bits::maccfg::FES;
1487        regs::write(regs::mac::CONFIG, base | regs::bits::maccfg::DM);
1488
1489        ethernet.set_duplex(Duplex::Half);
1490        assert_eq!(regs::read(regs::mac::CONFIG), base);
1491
1492        ethernet.set_duplex(Duplex::Full);
1493        assert_eq!(regs::read(regs::mac::CONFIG), base | regs::bits::maccfg::DM);
1494    }
1495
1496    #[test]
1497    fn ethernet_new_keeps_mac_and_resets_rings() {
1498        regs::reset_test_registers();
1499        let mut tx_desc = zeroed_tx_descriptors();
1500        let mut rx_desc = zeroed_rx_descriptors();
1501        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1502        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1503        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1504
1505        let ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1506
1507        assert_eq!(ethernet.mac_addr(), mac);
1508    }
1509
1510    #[test]
1511    fn phy_link_status_is_down_on_host_stub() {
1512        regs::reset_test_registers();
1513        let mut tx_desc = zeroed_tx_descriptors();
1514        let mut rx_desc = zeroed_rx_descriptors();
1515        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1516        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1517        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1518
1519        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1520
1521        assert!(!ethernet.phy_link_status());
1522    }
1523
1524    #[test]
1525    fn phy_negotiate_returns_fallback_mode_on_host_stub() {
1526        regs::reset_test_registers();
1527        let mut tx_desc = zeroed_tx_descriptors();
1528        let mut rx_desc = zeroed_rx_descriptors();
1529        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1530        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1531        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1532
1533        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1534
1535        assert_eq!(ethernet.phy_negotiate(), Ok((Speed::Mbps10, Duplex::Half)));
1536    }
1537
1538    #[test]
1539    fn dma_interrupt_status_resets_rx_ring_on_overflow() {
1540        regs::reset_test_registers();
1541        let mut tx_desc = zeroed_tx_descriptors();
1542        let mut rx_desc = zeroed_rx_descriptors();
1543        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1544        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1545        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1546
1547        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1548
1549        ethernet.apply_dma_interrupt_status(DmaInterruptStatus::from_raw(
1550            regs::bits::dmastatus::OVF | regs::bits::dmastatus::AIS,
1551        ));
1552
1553        assert_eq!(ethernet.rx_stats().overflow_resets, 1);
1554        assert_eq!(ethernet.receive(), None);
1555    }
1556
1557    #[test]
1558    fn dma_interrupt_status_restarts_dma_on_fatal_bus_error() {
1559        regs::reset_test_registers();
1560        let mut tx_desc = zeroed_tx_descriptors();
1561        let mut rx_desc = zeroed_rx_descriptors();
1562        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1563        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1564        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1565
1566        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1567        ethernet.apply_dma_interrupt_status(DmaInterruptStatus::from_raw(
1568            regs::bits::dmastatus::FBI | regs::bits::dmastatus::AIS,
1569        ));
1570
1571        assert!(ethernet.transmit(&[0xAB; 64]).is_ok());
1572        assert_eq!(ethernet.tx_stats().transmitted_frames, 1);
1573    }
1574
1575    #[test]
1576    fn handle_dma_interrupt_reads_and_clears_status_register() {
1577        regs::reset_test_registers();
1578        let mut tx_desc = zeroed_tx_descriptors();
1579        let mut rx_desc = zeroed_rx_descriptors();
1580        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1581        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1582        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1583
1584        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1585        let raw =
1586            regs::bits::dmastatus::OVF | regs::bits::dmastatus::RI | regs::bits::dmastatus::AIS;
1587        regs::write(regs::dma::STATUS, raw);
1588
1589        let status = ethernet.handle_dma_interrupt();
1590
1591        assert_eq!(status.raw(), raw);
1592        assert_eq!(regs::read(regs::dma::STATUS), status.clear_mask());
1593        assert_eq!(regs::read(regs::dma::RX_POLL_DEMAND), 1);
1594        assert_eq!(ethernet.rx_stats().overflow_resets, 1);
1595    }
1596
1597    #[test]
1598    fn handle_dma_interrupt_kicks_tx_poll_on_tx_recovery_bits() {
1599        regs::reset_test_registers();
1600        let mut tx_desc = zeroed_tx_descriptors();
1601        let mut rx_desc = zeroed_rx_descriptors();
1602        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1603        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1604        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1605
1606        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1607        regs::write(
1608            regs::dma::STATUS,
1609            regs::bits::dmastatus::TU | regs::bits::dmastatus::UNF | regs::bits::dmastatus::AIS,
1610        );
1611
1612        ethernet.handle_dma_interrupt();
1613
1614        assert_eq!(regs::read(regs::dma::TX_POLL_DEMAND), 1);
1615    }
1616
1617    #[test]
1618    fn handle_dma_interrupt_property_fuzz_preserves_driver_invariants() {
1619        regs::reset_test_registers();
1620        let mut tx_desc = zeroed_tx_descriptors();
1621        let mut rx_desc = zeroed_rx_descriptors();
1622        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1623        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1624        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1625
1626        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1627        let mut rng = 0xA17C_E55Du32;
1628
1629        for _ in 0..IRQ_FUZZ_ITERATIONS {
1630            let random = advance_lcg(&mut rng);
1631            let mut raw = 0u32;
1632
1633            if random & 0x01 != 0 {
1634                raw |= regs::bits::dmastatus::OVF;
1635            }
1636            if random & 0x02 != 0 {
1637                raw |= regs::bits::dmastatus::RU;
1638            }
1639            if random & 0x04 != 0 {
1640                raw |= regs::bits::dmastatus::RPS;
1641            }
1642            if random & 0x08 != 0 {
1643                raw |= regs::bits::dmastatus::TU;
1644            }
1645            if random & 0x10 != 0 {
1646                raw |= regs::bits::dmastatus::UNF;
1647            }
1648            if random & 0x20 != 0 {
1649                raw |= regs::bits::dmastatus::RI;
1650            }
1651            if random & 0x40 != 0 {
1652                raw |= regs::bits::dmastatus::ERI;
1653            }
1654            if random & 0x80 != 0 {
1655                raw |= regs::bits::dmastatus::FBI;
1656            }
1657            if raw
1658                & (regs::bits::dmastatus::OVF
1659                    | regs::bits::dmastatus::RU
1660                    | regs::bits::dmastatus::RPS
1661                    | regs::bits::dmastatus::TU
1662                    | regs::bits::dmastatus::UNF
1663                    | regs::bits::dmastatus::FBI)
1664                != 0
1665            {
1666                raw |= regs::bits::dmastatus::AIS;
1667            }
1668            if raw & (regs::bits::dmastatus::RI | regs::bits::dmastatus::TI) != 0 {
1669                raw |= regs::bits::dmastatus::NIS;
1670            }
1671
1672            regs::write(regs::dma::STATUS, raw);
1673            let previous_overflows = ethernet.rx_stats().overflow_resets;
1674            let status = ethernet.handle_dma_interrupt();
1675
1676            assert_eq!(status.raw(), raw);
1677            assert_eq!(regs::read(regs::dma::STATUS), status.clear_mask());
1678            assert_eq!(ethernet.mac_addr(), mac);
1679            assert!(ethernet.tx_ring.fuzz_current_index() < TX_DESC_COUNT);
1680            assert!(ethernet.rx_ring.fuzz_current_index() < RX_DESC_COUNT);
1681
1682            if status.has_rx_overflow()
1683                || status.has_rx_buffer_unavailable()
1684                || status.has_rx_process_stopped()
1685                || status.has_fatal_bus_error()
1686            {
1687                assert!(ethernet.rx_stats().overflow_resets >= previous_overflows);
1688            }
1689        }
1690    }
1691
1692    #[test]
1693    fn drop_disables_interrupts_and_powers_down_emac() {
1694        regs::reset_test_registers();
1695        {
1696            let mut tx_desc = zeroed_tx_descriptors();
1697            let mut rx_desc = zeroed_rx_descriptors();
1698            let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1699            let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1700            let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1701
1702            let mut ethernet =
1703                Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1704            ethernet.start().unwrap();
1705        }
1706
1707        assert_eq!(regs::read(regs::dma::INT_EN), 0);
1708        assert_eq!(regs::read(regs::mac::INTMASK), 0);
1709        assert!(!emac_clock_tree_enabled());
1710        assert!(emac_reset_asserted());
1711    }
1712
1713    #[test]
1714    fn ethernet_can_be_reinitialized_after_drop() {
1715        regs::reset_test_registers();
1716        let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
1717
1718        {
1719            let mut tx_desc = zeroed_tx_descriptors();
1720            let mut rx_desc = zeroed_rx_descriptors();
1721            let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1722            let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1723
1724            let mut ethernet =
1725                Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1726            ethernet.start().unwrap();
1727        }
1728
1729        let mut tx_desc = zeroed_tx_descriptors();
1730        let mut rx_desc = zeroed_rx_descriptors();
1731        let mut tx_buf = [[0u8; BUF_SIZE]; TX_DESC_COUNT];
1732        let mut rx_buf = [[0u8; BUF_SIZE]; RX_DESC_COUNT];
1733
1734        let mut ethernet = Ethernet::new(mac, &mut tx_desc, &mut rx_desc, &mut tx_buf, &mut rx_buf);
1735        assert_eq!(ethernet.mac_addr(), mac);
1736        assert!(ethernet.start().is_ok());
1737        assert!(emac_clock_tree_enabled());
1738        assert!(!emac_reset_asserted());
1739    }
1740
1741    // -------------------------------------------------------------------
1742    // Phase 3: rx_fut / tx_fut / link_fut pure-logic helpers.
1743    //
1744    // The atomics touched by these tests are global, so a `Mutex` keeps
1745    // parallel test runs from racing. Each test snapshots the relevant
1746    // counters before the call and asserts on the delta, so absolute
1747    // values from earlier (or concurrent) tests are not observed.
1748    // -------------------------------------------------------------------
1749
1750    use core::sync::atomic::Ordering::Relaxed;
1751    use std::sync::Mutex;
1752    use std::vec;
1753    use std::vec::Vec;
1754
1755    static CLASSIFIER_LOCK: Mutex<()> = Mutex::new(());
1756
1757    /// Build a minimal Ethernet header: 6-byte dst MAC, 6-byte src MAC, 2-byte ethertype.
1758    fn eth_header(dst: [u8; 6], src: [u8; 6], ethertype: u16) -> Vec<u8> {
1759        let mut v = Vec::with_capacity(14);
1760        v.extend_from_slice(&dst);
1761        v.extend_from_slice(&src);
1762        v.push((ethertype >> 8) as u8);
1763        v.push(ethertype as u8);
1764        v
1765    }
1766
1767    /// Build an IPv4 header positioned right after a 14-byte Ethernet header.
1768    /// Only the protocol byte and total-length are realistic; the rest is
1769    /// zero-padded — enough to drive `classify_rx_frame`'s 23-byte protocol
1770    /// check.
1771    fn ipv4_payload(proto: u8, ip_payload_len: usize) -> Vec<u8> {
1772        let mut v = vec![0u8; 20 + ip_payload_len];
1773        v[0] = 0x45; // version=4, IHL=5 (20 bytes)
1774        v[9] = proto;
1775        v
1776    }
1777
1778    fn snapshot_arp() -> u32 {
1779        RX_ARP.load(Relaxed)
1780    }
1781    fn snapshot_ipv4() -> u32 {
1782        RX_IPV4.load(Relaxed)
1783    }
1784    fn snapshot_icmp() -> u32 {
1785        RX_ICMP.load(Relaxed)
1786    }
1787    fn snapshot_dhcp() -> u32 {
1788        RX_DHCP_FRAMES.load(Relaxed)
1789    }
1790
1791    #[test]
1792    fn classify_rx_frame_runt_below_14_bytes_skips_classification() {
1793        let _g = CLASSIFIER_LOCK.lock().unwrap();
1794        let arp0 = snapshot_arp();
1795        let ipv4_0 = snapshot_ipv4();
1796
1797        // 13 bytes — one byte short of an Ethernet header.
1798        let buf = [0u8; 13];
1799        classify_rx_frame(&buf, 13);
1800
1801        assert_eq!(snapshot_arp(), arp0, "runt must not bump RX_ARP");
1802        assert_eq!(snapshot_ipv4(), ipv4_0, "runt must not bump RX_IPV4");
1803    }
1804
1805    #[test]
1806    fn classify_rx_frame_arp_bumps_arp_counter() {
1807        let _g = CLASSIFIER_LOCK.lock().unwrap();
1808        let before = snapshot_arp();
1809
1810        // ARP request, ethertype 0x0806, padded to 60 bytes (Ethernet min frame).
1811        let mut buf = eth_header(
1812            [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
1813            [0x02, 0x44, 0x52, 0x49, 0x56, 0x01],
1814            0x0806,
1815        );
1816        buf.resize(60, 0);
1817        classify_rx_frame(&buf, buf.len());
1818
1819        assert_eq!(snapshot_arp(), before + 1);
1820        assert_eq!(RX_LAST_ETHERTYPE.load(Relaxed), 0x0806);
1821    }
1822
1823    #[test]
1824    fn classify_rx_frame_ipv4_with_icmp_proto_bumps_both_counters() {
1825        let _g = CLASSIFIER_LOCK.lock().unwrap();
1826        let ipv4_before = snapshot_ipv4();
1827        let icmp_before = snapshot_icmp();
1828        let dhcp_before = snapshot_dhcp();
1829
1830        let mut buf = eth_header([0; 6], [0; 6], 0x0800);
1831        buf.extend_from_slice(&ipv4_payload(0x01, 8)); // ICMP proto
1832        classify_rx_frame(&buf, buf.len());
1833
1834        assert_eq!(snapshot_ipv4(), ipv4_before + 1);
1835        assert_eq!(snapshot_icmp(), icmp_before + 1);
1836        assert_eq!(
1837            snapshot_dhcp(),
1838            dhcp_before,
1839            "ICMP must not be misclassified as DHCP"
1840        );
1841    }
1842
1843    #[test]
1844    fn classify_rx_frame_ipv4_with_unknown_proto_bumps_only_ipv4_counter() {
1845        let _g = CLASSIFIER_LOCK.lock().unwrap();
1846        let ipv4_before = snapshot_ipv4();
1847        let icmp_before = snapshot_icmp();
1848
1849        let mut buf = eth_header([0; 6], [0; 6], 0x0800);
1850        buf.extend_from_slice(&ipv4_payload(0x06, 16)); // TCP proto, not ICMP
1851        classify_rx_frame(&buf, buf.len());
1852
1853        assert_eq!(snapshot_ipv4(), ipv4_before + 1);
1854        assert_eq!(snapshot_icmp(), icmp_before, "TCP must not bump ICMP");
1855    }
1856
1857    #[test]
1858    fn classify_rx_frame_dhcp_bootp_server_port_67_is_detected() {
1859        let _g = CLASSIFIER_LOCK.lock().unwrap();
1860        let before = snapshot_dhcp();
1861
1862        // Eth (14) + IPv4 (20) + UDP header (8): src=68, dst=67 (DHCP client → server).
1863        let mut buf = eth_header([0; 6], [0; 6], 0x0800);
1864        let mut ip = ipv4_payload(0x11, 240); // UDP proto, 8 UDP header + ≥232 payload
1865        // UDP src/dst ports at IP+20 (= eth_header_len + IP_header_len + 20):
1866        // Actually offsets are relative to the IP payload's start at index 20.
1867        ip[20] = 0; // src port hi
1868        ip[21] = 68; // src port lo (DHCP client)
1869        ip[22] = 0; // dst port hi
1870        ip[23] = 67; // dst port lo (DHCP server)
1871        buf.extend_from_slice(&ip);
1872        classify_rx_frame(&buf, buf.len());
1873
1874        assert_eq!(snapshot_dhcp(), before + 1);
1875    }
1876
1877    #[test]
1878    fn classify_rx_frame_dhcp_offer_dst_port_68_is_detected() {
1879        let _g = CLASSIFIER_LOCK.lock().unwrap();
1880        let before = snapshot_dhcp();
1881
1882        let mut buf = eth_header([0; 6], [0; 6], 0x0800);
1883        let mut ip = ipv4_payload(0x11, 240);
1884        // DHCP server → client direction: src=67, dst=68.
1885        ip[21] = 67;
1886        ip[23] = 68;
1887        buf.extend_from_slice(&ip);
1888        classify_rx_frame(&buf, buf.len());
1889
1890        assert_eq!(snapshot_dhcp(), before + 1);
1891    }
1892
1893    #[test]
1894    fn classify_rx_frame_non_dhcp_udp_is_not_dhcp_classified() {
1895        let _g = CLASSIFIER_LOCK.lock().unwrap();
1896        let before = snapshot_dhcp();
1897
1898        let mut buf = eth_header([0; 6], [0; 6], 0x0800);
1899        let mut ip = ipv4_payload(0x11, 240);
1900        // Random UDP ports — neither end is 67/68.
1901        ip[20] = 0x12;
1902        ip[21] = 0x34;
1903        ip[22] = 0x12;
1904        ip[23] = 0x35;
1905        buf.extend_from_slice(&ip);
1906        classify_rx_frame(&buf, buf.len());
1907
1908        assert_eq!(snapshot_dhcp(), before);
1909    }
1910
1911    #[test]
1912    fn classify_rx_frame_ipv6_skips_classification() {
1913        let _g = CLASSIFIER_LOCK.lock().unwrap();
1914        let arp_before = snapshot_arp();
1915        let ipv4_before = snapshot_ipv4();
1916
1917        let mut buf = eth_header([0x33, 0x33, 0, 0, 0, 1], [0; 6], 0x86DD);
1918        buf.resize(80, 0);
1919        classify_rx_frame(&buf, buf.len());
1920
1921        assert_eq!(snapshot_arp(), arp_before);
1922        assert_eq!(snapshot_ipv4(), ipv4_before);
1923        assert_eq!(RX_LAST_ETHERTYPE.load(Relaxed), 0x86DD);
1924    }
1925
1926    #[test]
1927    fn classify_rx_frame_large_frame_writes_hex_dump_into_static_slot() {
1928        let _g = CLASSIFIER_LOCK.lock().unwrap();
1929        // Wipe the slot so we can detect what was written.
1930        for cell in RX_LAST_LARGE_FRAME.iter() {
1931            cell.store(0, Relaxed);
1932        }
1933        RX_LAST_LARGE_FRAME_LEN.store(0, Relaxed);
1934
1935        // Construct a 250-byte frame; first 4 bytes form a recognisable u32.
1936        let mut buf = vec![0u8; 250];
1937        buf[0] = 0xDE;
1938        buf[1] = 0xAD;
1939        buf[2] = 0xBE;
1940        buf[3] = 0xEF;
1941        // Use ethertype 0x0800 to avoid touching the DHCP path.
1942        buf[12] = 0x08;
1943        classify_rx_frame(&buf, buf.len());
1944
1945        assert_eq!(RX_LAST_LARGE_FRAME_LEN.load(Relaxed), 250);
1946        assert_eq!(RX_LAST_LARGE_FRAME[0].load(Relaxed), 0xDEAD_BEEF);
1947    }
1948
1949    #[test]
1950    fn classify_rx_frame_dhcp_writes_dump_into_static_slot() {
1951        let _g = CLASSIFIER_LOCK.lock().unwrap();
1952        for cell in RX_LAST_DHCP_FRAME.iter() {
1953            cell.store(0, Relaxed);
1954        }
1955
1956        let mut buf = eth_header([0; 6], [0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x01], 0x0800);
1957        buf[0] = 0xAA; // recognisable dst MAC prefix
1958        buf[1] = 0xBB;
1959        buf[2] = 0xCC;
1960        buf[3] = 0xDD;
1961        let mut ip = ipv4_payload(0x11, 240);
1962        ip[21] = 67; // DHCP server src
1963        ip[23] = 68; // DHCP client dst
1964        buf.extend_from_slice(&ip);
1965        classify_rx_frame(&buf, buf.len());
1966
1967        // First 4 bytes of the frame == dst MAC prefix.
1968        assert_eq!(RX_LAST_DHCP_FRAME[0].load(Relaxed), 0xAABB_CCDD);
1969    }
1970
1971    #[test]
1972    fn record_tx_frame_metadata_short_buf_records_only_length() {
1973        let _g = CLASSIFIER_LOCK.lock().unwrap();
1974        TX_LAST_LEN.store(0, Relaxed);
1975        TX_LAST_DST_MAC_HI.store(0xDEAD_BEEF, Relaxed);
1976        TX_LAST_SRC_MAC_HI.store(0xDEAD_BEEF, Relaxed);
1977        TX_LAST_ETHERTYPE.store(0xDEAD_BEEF, Relaxed);
1978
1979        let buf = [0u8; 10];
1980        record_tx_frame_metadata(&buf);
1981
1982        assert_eq!(TX_LAST_LEN.load(Relaxed), 10);
1983        // Header fields must remain untouched on short frames.
1984        assert_eq!(TX_LAST_DST_MAC_HI.load(Relaxed), 0xDEAD_BEEF);
1985        assert_eq!(TX_LAST_SRC_MAC_HI.load(Relaxed), 0xDEAD_BEEF);
1986        assert_eq!(TX_LAST_ETHERTYPE.load(Relaxed), 0xDEAD_BEEF);
1987    }
1988
1989    #[test]
1990    fn record_tx_frame_metadata_full_frame_captures_all_header_fields() {
1991        let _g = CLASSIFIER_LOCK.lock().unwrap();
1992        TX_LAST_LEN.store(0, Relaxed);
1993
1994        let buf: [u8; 14] = [
1995            0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // dst MAC
1996            0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // src MAC
1997            0x08, 0x06, // ethertype = ARP
1998        ];
1999        record_tx_frame_metadata(&buf);
2000
2001        assert_eq!(TX_LAST_LEN.load(Relaxed), 14);
2002        // dst_hi captures the first 4 bytes of dst MAC big-endian.
2003        assert_eq!(TX_LAST_DST_MAC_HI.load(Relaxed), 0xAABB_CCDD);
2004        // src_hi captures the first 4 bytes of src MAC big-endian.
2005        assert_eq!(TX_LAST_SRC_MAC_HI.load(Relaxed), 0x1122_3344);
2006        // Ethertype is the 2-byte big-endian field at offsets 12..14.
2007        assert_eq!(TX_LAST_ETHERTYPE.load(Relaxed), 0x0806);
2008    }
2009
2010    #[test]
2011    fn record_tx_frame_metadata_handles_oversize_frame() {
2012        let _g = CLASSIFIER_LOCK.lock().unwrap();
2013
2014        let mut buf = vec![0u8; 1500];
2015        buf[0] = 0x12;
2016        buf[12] = 0x86;
2017        buf[13] = 0xDD;
2018        record_tx_frame_metadata(&buf);
2019
2020        assert_eq!(TX_LAST_LEN.load(Relaxed), 1500);
2021        assert_eq!(TX_LAST_ETHERTYPE.load(Relaxed), 0x86DD);
2022    }
2023
2024    #[test]
2025    fn link_state_action_down_to_down_is_no_change() {
2026        assert_eq!(link_state_action(false, false), LinkAction::NoChange);
2027    }
2028
2029    #[test]
2030    fn link_state_action_down_to_up_demands_negotiate() {
2031        // When the PHY first reports up, the caller must negotiate before
2032        // committing the channel state. Catches the regression where someone
2033        // skips negotiate and the MAC ends up in the wrong speed/duplex.
2034        assert_eq!(
2035            link_state_action(false, true),
2036            LinkAction::AttemptNegotiate
2037        );
2038    }
2039
2040    #[test]
2041    fn link_state_action_up_to_up_is_no_change() {
2042        assert_eq!(link_state_action(true, true), LinkAction::NoChange);
2043    }
2044
2045    #[test]
2046    fn link_state_action_up_to_down_signals_go_down() {
2047        assert_eq!(link_state_action(true, false), LinkAction::GoDown);
2048    }
2049
2050    // NOTE: tests for `wake_rx_task` / `wake_tx_task` are intentionally
2051    // omitted. Both functions are one-liners (`RX_WAKER.wake()` /
2052    // `TX_WAKER.wake()`) over the embassy-sync `AtomicWaker`, whose
2053    // semantics are owned upstream and already tested there. A direct
2054    // host test would need to coordinate with the existing
2055    // `handle_dma_interrupt_*` tests which also fire those wakers
2056    // through the global statics — the synchronisation cost outweighs
2057    // the value of the assertion.
2058}