esp_emac/emac.rs
1// SPDX-License-Identifier: GPL-2.0-or-later OR Apache-2.0
2// Copyright (c) Viacheslav Bocharov <v@baodeep.com> and JetHome (r)
3
4//! Native ESP32 EMAC driver.
5//!
6//! Owns the DMA engine and drives the bring-up sequence directly via
7//! the local register helper modules in `crate::regs::*` and
8//! [`crate::reset::ResetController`]. No `ph-esp32-mac` dependency.
9
10use embedded_hal::delay::DelayNs;
11
12use crate::regs::dma as dma_regs;
13use crate::regs::ext as ext_regs;
14use crate::regs::gpio as gpio_matrix;
15use crate::regs::mac as mac_regs;
16use crate::reset::ResetController;
17
18use crate::config::{ClkGpio, EmacConfig, RmiiClockConfig};
19use crate::dma::engine::DmaEngine;
20use crate::error::EmacError;
21use crate::interrupt::InterruptStatus;
22use crate::regs::dma::{bus_mode, operation};
23use crate::regs::mac::{config, frame_filter};
24
25const TX_FIFO_FLUSH_TIMEOUT_US: u32 = 100_000;
26
27// =============================================================================
28// Link parameters and driver state
29// =============================================================================
30
31// Re-export the link-parameter enums from the trait crate so a PHY
32// driver's `LinkStatus` lands directly into `set_speed` / `set_duplex`
33// without the call-site `.into()` boilerplate that was needed when
34// these were duplicate local types. Keeping the types in one place
35// (eth_mdio_phy) also means a future minor-release variant addition
36// (`Speed::Mbps1000`) propagates through both ends of the stack with
37// a single bump.
38//
39// Gated by the `mdio-phy` feature because that feature is what pulls
40// `eth_mdio_phy` in as a dependency. Users without the feature can
41// still drop down to `crate::regs::mac::set_speed_100mbps` /
42// `set_duplex_full` directly — see the module-level docs.
43#[cfg(feature = "mdio-phy")]
44pub use eth_mdio_phy::{Duplex, Speed};
45
46/// EMAC driver state.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[cfg_attr(feature = "defmt", derive(defmt::Format))]
49pub enum EmacState {
50 /// Not yet initialized.
51 Uninitialized,
52 /// `init()` succeeded but DMA/MAC are not running.
53 Initialized,
54 /// `start()` succeeded — DMA active, can transmit/receive.
55 Running,
56}
57
58// =============================================================================
59// EMAC driver
60// =============================================================================
61
62/// ESP32 EMAC driver with statically allocated DMA buffers.
63///
64/// The DMA descriptor chain is self-referential, so the driver MUST be
65/// placed in its final memory location BEFORE [`init`](Self::init) is
66/// called.
67pub struct Emac<const RX: usize = 10, const TX: usize = 10, const BUF: usize = 1600> {
68 dma: DmaEngine<RX, TX, BUF>,
69 config: EmacConfig,
70 state: EmacState,
71 mac_address: [u8; 6],
72 /// Last applied speed setting; idempotency guard for [`Self::set_speed`].
73 /// `None` means speed has not been applied yet (default before first
74 /// `set_speed` call). PHY-link state polling calls `set_speed` every
75 /// cycle — without the guard each call would issue a redundant MMIO
76 /// write regardless of whether the parameters actually changed.
77 ///
78 /// **Caveat:** this cache can become stale if a consumer bypasses the
79 /// API by calling [`crate::regs::mac::set_speed_100mbps`] /
80 /// [`crate::regs::mac::set_duplex_full`] directly. Mixing the high-level
81 /// [`Self::set_speed`] / [`Self::set_duplex`] with those low-level
82 /// helpers is **not supported** — the idempotency guard will early-return
83 /// on a stale cached value and leave the hardware configured against
84 /// expectations. Pick one API and stick with it.
85 #[cfg(feature = "mdio-phy")]
86 current_speed: Option<Speed>,
87 /// Last applied duplex setting; analogous to `current_speed` (same
88 /// caveat about mixing with `regs::mac::set_duplex_full`).
89 #[cfg(feature = "mdio-phy")]
90 current_duplex: Option<Duplex>,
91}
92
93impl<const RX: usize, const TX: usize, const BUF: usize> Emac<RX, TX, BUF> {
94 /// Create a new (uninitialized) driver.
95 pub const fn new(config: EmacConfig) -> Self {
96 Self {
97 dma: DmaEngine::new(),
98 config,
99 state: EmacState::Uninitialized,
100 mac_address: [0; 6],
101 #[cfg(feature = "mdio-phy")]
102 current_speed: None,
103 #[cfg(feature = "mdio-phy")]
104 current_duplex: None,
105 }
106 }
107
108 // ── State accessors ────────────────────────────────────────────────────
109
110 #[inline(always)]
111 pub fn state(&self) -> EmacState {
112 self.state
113 }
114
115 #[inline(always)]
116 pub fn mac_address(&self) -> [u8; 6] {
117 self.mac_address
118 }
119
120 #[inline(always)]
121 pub fn config(&self) -> &EmacConfig {
122 &self.config
123 }
124
125 /// Total static memory used by this EMAC instance.
126 pub const fn memory_usage() -> usize {
127 DmaEngine::<RX, TX, BUF>::memory_usage()
128 }
129
130 // ── Configuration ──────────────────────────────────────────────────────
131
132 /// Set the MAC address.
133 ///
134 /// If the driver has been initialized, the hardware filter registers
135 /// are updated immediately.
136 pub fn set_mac_address(&mut self, mac: [u8; 6]) {
137 self.mac_address = mac;
138 if self.state != EmacState::Uninitialized {
139 crate::regs::mac::set_mac_address(&mac);
140 }
141 }
142
143 /// Apply the link speed reported by the PHY.
144 ///
145 /// The ESP32 EMAC peripheral physically supports only 10 Mbps and
146 /// 100 Mbps. `Speed` is `#[non_exhaustive]` in the trait crate, so
147 /// future variants (e.g. a hypothetical `Mbps1000`) compile but
148 /// have no register encoding here. They are clamped to 100 Mbps —
149 /// the highest mode the EMAC actually supports — and a warning is
150 /// emitted under the `defmt` feature so the discrepancy is
151 /// visible at runtime.
152 ///
153 /// Available only with the `mdio-phy` feature, which is also what
154 /// pulls in the [`Speed`] type from `eth_mdio_phy`. Without the
155 /// feature, drop down to [`crate::regs::mac::set_speed_100mbps`].
156 #[cfg(feature = "mdio-phy")]
157 pub fn set_speed(&mut self, speed: Speed) {
158 if self.state == EmacState::Uninitialized {
159 return;
160 }
161 // Idempotency guard — avoid redundant MMIO writes when PHY reports
162 // unchanged link parameters. Without the guard, link-state polling
163 // every 500 ms would issue a write at every poll regardless of change.
164 if self.current_speed == Some(speed) {
165 return;
166 }
167 let is_100 = match speed {
168 Speed::Mbps10 => false,
169 Speed::Mbps100 => true,
170 _ => {
171 #[cfg(feature = "defmt")]
172 defmt::warn!(
173 "esp-emac: unsupported Speed variant, clamping to 100 Mbps \
174 (ESP32 EMAC is 10/100 only)"
175 );
176 true
177 }
178 };
179 mac_regs::set_speed_100mbps(is_100);
180 self.current_speed = Some(speed);
181 }
182
183 /// Apply the duplex mode reported by the PHY.
184 ///
185 /// `Duplex` is `#[non_exhaustive]` in the trait crate. ESP32 EMAC
186 /// has only the two MII-canonical modes (Half/Full); any future
187 /// variant is clamped to Full (the more permissive default) with
188 /// a `defmt::warn!` so the unexpected input doesn't pass silently.
189 ///
190 /// Available only with the `mdio-phy` feature, which is also what
191 /// pulls in the [`Duplex`] type from `eth_mdio_phy`. Without the
192 /// feature, drop down to [`crate::regs::mac::set_duplex_full`].
193 #[cfg(feature = "mdio-phy")]
194 pub fn set_duplex(&mut self, duplex: Duplex) {
195 if self.state == EmacState::Uninitialized {
196 return;
197 }
198 // Idempotency guard — see [`Self::set_speed`].
199 if self.current_duplex == Some(duplex) {
200 return;
201 }
202 let is_full = match duplex {
203 Duplex::Half => false,
204 Duplex::Full => true,
205 _ => {
206 #[cfg(feature = "defmt")]
207 defmt::warn!(
208 "esp-emac: unsupported Duplex variant, clamping to Full \
209 (ESP32 EMAC supports Half/Full only)"
210 );
211 true
212 }
213 };
214 mac_regs::set_duplex_full(is_full);
215 self.current_duplex = Some(duplex);
216 }
217
218 // ── Initialization ─────────────────────────────────────────────────────
219
220 /// Initialize the EMAC peripheral.
221 ///
222 /// Sequence (mirrors the canonical ESP32 GMAC bring-up):
223 /// 1. APLL 50 MHz programming — only when MCU is the RMII clock master
224 /// (`RmiiClockConfig::InternalApll`); skipped for `External`.
225 /// 2. RMII reference-clock pad routing (GPIO0 input for External,
226 /// GPIO16/17 output for InternalApll).
227 /// 3. SMI + RMII data-pin routing.
228 /// 4. DPORT EMAC peripheral clock enable.
229 /// 5. PHY interface mode (RMII) + clock source select.
230 /// 6. EMAC extension clocks + RAM power-up.
231 /// 7. DMA software reset.
232 /// 8. MAC config defaults (PS/FES/DM/ACS/JD/WD).
233 /// 9. DMA bus mode + operation mode defaults.
234 /// 10. DMA descriptor chains and base-address registers.
235 /// 11. MAC address program.
236 pub fn init(&mut self, delay: &mut impl DelayNs) -> Result<(), EmacError> {
237 if self.state != EmacState::Uninitialized {
238 return Err(EmacError::AlreadyInitialized);
239 }
240
241 // 0. Validate user-configurable pins before touching any
242 // registers, so a bad `EmacConfig::pins` is rejected loudly
243 // rather than silently writing to unintended MMIO.
244 if !gpio_matrix::is_valid_smi_pin(self.config.pins.mdc)
245 || !gpio_matrix::is_valid_smi_pin(self.config.pins.mdio)
246 {
247 return Err(EmacError::InvalidConfig);
248 }
249
250 // RMII reference-clock pad direction on ESP32 is fixed by the
251 // IO_MUX function:
252 //
253 // - GPIO0 function 5 = `EMAC_TX_CLK` — INPUT only
254 // - GPIO16 function 5 = `EMAC_CLK_OUT` — OUTPUT only
255 // - GPIO17 function 5 = `EMAC_CLK_OUT_180` — OUTPUT only
256 //
257 // External clock therefore requires GPIO0 (the only input pad);
258 // internal APLL output requires GPIO16 or GPIO17. Any other
259 // combination is hardware-impossible — reject it before we
260 // start writing IO_MUX bits.
261 match self.config.clock {
262 RmiiClockConfig::External { gpio } if !matches!(gpio, ClkGpio::Gpio0) => {
263 return Err(EmacError::InvalidConfig);
264 }
265 RmiiClockConfig::InternalApll {
266 gpio: ClkGpio::Gpio0,
267 ..
268 } => {
269 return Err(EmacError::InvalidConfig);
270 }
271 _ => {}
272 }
273
274 // 1. APLL — programmed only when the MCU is the RMII clock
275 // master. SDM coefficients are picked from the configured
276 // on-board crystal (`xtal`) so the same code lands on
277 // 50 MHz on 26/32/40 MHz boards alike. APLL is independent
278 // of the EMAC peripheral clock (only writes RTC analog +
279 // ROM I2C on the always-on APB), so order here doesn't
280 // matter. Skipped entirely for `External`.
281 if let RmiiClockConfig::InternalApll { xtal, .. } = self.config.clock {
282 crate::clock::configure_apll_50mhz(xtal);
283 }
284
285 // 2. Route the RMII reference-clock pad: input on GPIO0 for
286 // `External`, or output on GPIO16/17 for `InternalApll`.
287 match self.config.clock {
288 RmiiClockConfig::External { gpio } => crate::clock::configure_emac_clk_in(gpio),
289 RmiiClockConfig::InternalApll { gpio, .. } => {
290 crate::clock::configure_emac_clk_out(gpio)
291 }
292 }
293
294 // 3. Configure SMI pins (MDC/MDIO from `EmacConfig::pins`) and
295 // RMII data pins (fixed function 5 — not configurable).
296 gpio_matrix::configure_smi_pins(self.config.pins.mdc, self.config.pins.mdio);
297 gpio_matrix::configure_rmii_pins();
298
299 // 4. Enable EMAC peripheral clock through DPORT.
300 ext_regs::enable_peripheral_clock();
301
302 // 5. PHY interface — RMII with the appropriate clock source.
303 ext_regs::set_rmii_mode();
304 match self.config.clock {
305 RmiiClockConfig::External { .. } => ext_regs::set_rmii_clock_external(),
306 RmiiClockConfig::InternalApll { .. } => ext_regs::set_rmii_clock_internal(),
307 }
308
309 // 6. EMAC extension clocks + RAM power.
310 ext_regs::enable_clocks();
311 ext_regs::power_up_ram();
312
313 // 7. Software reset of the DMA controller. `ResetController::new`
314 // uses the canonical `crate::reset::SOFT_RESET_TIMEOUT_MS`
315 // default — single source of truth for the reset window.
316 // `ResetError::Timeout` converts to `EmacError::DmaResetTimeout`
317 // via the `From` impl, so callers can distinguish DMA-stuck
318 // from MDIO timeouts.
319 let mut reset_ctrl = ResetController::new(BorrowedDelay(delay));
320 reset_ctrl.soft_reset()?;
321
322 // 8. MAC configuration defaults: 100 Mbps full duplex, port select,
323 // auto pad/CRC strip, jabber + watchdog disabled.
324 //
325 // CHECKSUM_OFFLOAD (IPC, bit 10) is **disabled**. The ESP32 GMAC
326 // checksum engine on at least rev v3.1 silicon is unreliable for
327 // both directions:
328 // * TX insertion (TDES0.CIC=0b11) produced bad checksums and
329 // broke TCP throughput after the first MSS-sized segment
330 // (see `TxDescriptor::prepare`).
331 // * RX verification (IPC=1) symmetrically marks valid frames
332 // as having checksum errors, which the DMA then drops at
333 // DMAOPERATION.DT=0 before they reach the CPU. This was
334 // observed as iperf2 downlink throughput collapsing to
335 // 0 Mbps while uplink still trickled data through.
336 // Both sides therefore stay off and smoltcp computes / verifies
337 // IPv4/TCP/UDP/ICMP checksums in software (see
338 // `Driver::capabilities` advertising `ChecksumCapabilities::default()`).
339 let mac_cfg = config::PORT_SELECT
340 | config::SPEED_100
341 | config::DUPLEX_FULL
342 | config::AUTO_PAD_CRC_STRIP
343 | config::JABBER_DISABLE
344 | config::WATCHDOG_DISABLE;
345 mac_regs::set_config(mac_cfg);
346
347 // Frame filter: pass all multicast (broadcast accepted by default).
348 mac_regs::set_frame_filter(frame_filter::PASS_ALL_MULTICAST);
349 mac_regs::set_hash_table(0);
350
351 // 9. DMA bus mode and operation mode.
352 //
353 // ATDS = enhanced 8-word descriptor layout (32 bytes per
354 // descriptor). Our `dma::descriptor::{TxDescriptor,
355 // RxDescriptor}` are now 8 words to match.
356 let pbl = 32u32;
357 let bus = bus_mode::FIXED_BURST
358 | bus_mode::AAL
359 | bus_mode::USP
360 | bus_mode::ATDS
361 | ((pbl << bus_mode::PBL_SHIFT) & bus_mode::PBL_MASK);
362 dma_regs::set_bus_mode(bus);
363 dma_regs::set_operation_mode(operation::TSF | operation::RSF);
364 dma_regs::disable_all_interrupts();
365 dma_regs::clear_all_interrupts();
366
367 // 10. Descriptor chains. Returns physical base addresses suitable for
368 // DMARXBASEADDR / DMATXBASEADDR.
369 let (rx_base, tx_base) = self.dma.init();
370 dma_regs::set_rx_desc_list_addr(rx_base);
371 dma_regs::set_tx_desc_list_addr(tx_base);
372
373 // 11. Programme the MAC address into ADDR0H / ADDR0L (with AE bit).
374 // The internal filter latch on this Synopsys GMAC fires on the
375 // LOW write — `regs::mac::set_mac_address` writes HIGH first to
376 // keep the AE bit, then LOW to trigger the latch.
377 crate::regs::mac::set_mac_address(&self.mac_address);
378
379 self.state = EmacState::Initialized;
380 Ok(())
381 }
382
383 /// Start TX/RX (DMA + MAC).
384 pub fn start(&mut self) -> Result<(), EmacError> {
385 match self.state {
386 EmacState::Initialized => {}
387 EmacState::Running => return Ok(()),
388 EmacState::Uninitialized => return Err(EmacError::NotInitialized),
389 }
390
391 // Reset descriptor ownership in case of a previous run, then
392 // re-program `DMARXBASEADDR` / `DMATXBASEADDR` from the base
393 // addresses the engine returns. `dma.reset()` rebuilds chains
394 // and zeroes the software `current_index`; the hardware DMA
395 // pointer wherever it last was (middle of the ring after a
396 // `stop()`/`start()` cycle, or unset on the very first start)
397 // must be put back on the chain head, otherwise software and
398 // hardware will walk different descriptors and RX wedges.
399 let (rx_base, tx_base) = self.dma.reset();
400 dma_regs::set_rx_desc_list_addr(rx_base);
401 dma_regs::set_tx_desc_list_addr(tx_base);
402
403 dma_regs::clear_all_interrupts();
404 dma_regs::enable_default_interrupts();
405
406 // Enable MAC TX, then DMA TX, DMA RX, then MAC RX (matches the
407 // ordering from the ESP32 reference manual / IDF EMAC driver).
408 let cfg = mac_regs::config();
409 mac_regs::set_config(cfg | config::TX_ENABLE);
410
411 dma_regs::start_tx();
412 dma_regs::start_rx();
413
414 let cfg = mac_regs::config();
415 mac_regs::set_config(cfg | config::RX_ENABLE);
416
417 // Issue an RX poll demand so the DMA does not stay in Suspended
418 // state if all descriptors were already CPU-owned.
419 dma_regs::rx_poll_demand();
420
421 self.state = EmacState::Running;
422 Ok(())
423 }
424
425 /// Stop TX/RX.
426 ///
427 /// Polls the TX-FIFO flush bit (`FTF`) for up to
428 /// `TX_FIFO_FLUSH_TIMEOUT_US` microseconds, sleeping `delay` between
429 /// polls so the DMA actually has time to drain. The rest of the
430 /// teardown (MAC RX/TX disable, DMA RX stop, interrupt-status
431 /// clear, state transition to `Initialized`) runs unconditionally
432 /// — even on flush timeout the driver winds up in `Initialized`
433 /// and is safe to re-`start()`.
434 ///
435 /// Returns:
436 /// - `Ok(())` on a clean teardown (FTF self-cleared in time).
437 /// - `Err(EmacError::TxFlushTimeout)` when the FTF poll exhausted
438 /// `TX_FIFO_FLUSH_TIMEOUT_US`. Teardown still completed — at
439 /// least one in-flight TX frame may have been truncated on the
440 /// wire. `state` is `Initialized` either way, so a follow-up
441 /// `start()` is the recoverable path. There is no in-crate
442 /// "full re-init" — [`Emac::init`] is one-shot — so a terminal
443 /// recovery means a peripheral or SoC reset from the
444 /// application layer.
445 /// - `Err(EmacError::NotInitialized)` if called from `Uninitialized`.
446 ///
447 /// Idempotent on an already-stopped driver: calling `stop` while
448 /// in `Initialized` returns `Ok(())` without touching hardware.
449 pub fn stop(&mut self, delay: &mut impl DelayNs) -> Result<(), EmacError> {
450 match self.state {
451 EmacState::Running => {} // proceed with the tear-down below
452 EmacState::Initialized => return Ok(()),
453 EmacState::Uninitialized => return Err(EmacError::NotInitialized),
454 }
455
456 // Stop DMA TX, wait for in-flight data to drain (best effort).
457 dma_regs::stop_tx();
458
459 // Flush TX FIFO and wait for the bit to self-clear.
460 dma_regs::flush_tx_fifo();
461 const POLL_STEP_US: u32 = 10;
462 let mut waited_us = 0u32;
463 let mut flush_timed_out = true;
464 while waited_us < TX_FIFO_FLUSH_TIMEOUT_US {
465 if (dma_regs::operation_mode() & operation::FTF) == 0 {
466 flush_timed_out = false;
467 break;
468 }
469 delay.delay_us(POLL_STEP_US);
470 waited_us += POLL_STEP_US;
471 }
472
473 // Disable MAC TX and RX, then DMA RX.
474 let cfg = mac_regs::config();
475 mac_regs::set_config(cfg & !(config::TX_ENABLE | config::RX_ENABLE));
476
477 dma_regs::stop_rx();
478 dma_regs::disable_all_interrupts();
479 // Acknowledge any W1C bits that latched in DMASTATUS while the
480 // engine was running, so a future `start()` doesn't observe
481 // stale flags through `last_dmastat` / `interrupt_status` and
482 // a re-enable from outside the driver doesn't fire spuriously.
483 dma_regs::clear_all_interrupts();
484
485 self.state = EmacState::Initialized;
486
487 if flush_timed_out {
488 Err(EmacError::TxFlushTimeout)
489 } else {
490 Ok(())
491 }
492 }
493
494 // ── Frame I/O ─────────────────────────────────────────────────────────
495
496 /// Transmit a frame.
497 ///
498 /// Does not block on descriptor availability — caller must check
499 /// [`can_transmit`](Self::can_transmit) (or [`tx_ready`](Self::tx_ready)
500 /// for single-descriptor frames) before calling, or be ready to handle
501 /// `EmacError::NoDescriptorsAvailable` / `EmacError::DescriptorBusy`
502 /// when the TX ring is full, and `EmacError::FrameTooLarge` when the
503 /// payload exceeds the ring's combined capacity.
504 pub fn transmit(&mut self, data: &[u8]) -> Result<usize, EmacError> {
505 if self.state != EmacState::Running {
506 return Err(EmacError::NotInitialized);
507 }
508 let n = self.dma.transmit(data)?;
509 // Kick TX DMA out of suspended state if we just refilled descriptors.
510 dma_regs::tx_poll_demand();
511 Ok(n)
512 }
513
514 /// Receive a frame, if any.
515 ///
516 /// Issues an RX poll-demand whenever a descriptor was potentially
517 /// recycled by `DmaEngine::receive` — that includes the success
518 /// path (`Ok(Some(_))`) **and** the error paths (`FrameError`,
519 /// `BufferTooSmall`, …) where the engine still hands the descriptor
520 /// back to the DMA. Only `Ok(None)` skips the kick, since nothing
521 /// in the ring changed. Without this, an errored frame on a
522 /// suspended ring would leave RX wedged with the `RU` bit asserted
523 /// until the next *successful* receive — exactly the kind of
524 /// post-error hang we hit in the field.
525 pub fn receive(&mut self, buffer: &mut [u8]) -> Result<Option<usize>, EmacError> {
526 if self.state != EmacState::Running {
527 return Err(EmacError::NotInitialized);
528 }
529 let result = self.dma.receive(buffer);
530 if !matches!(result, Ok(None)) {
531 dma_regs::rx_poll_demand();
532 }
533 result
534 }
535
536 /// Whether a received frame is currently waiting in the ring.
537 #[inline(always)]
538 pub fn rx_available(&self) -> bool {
539 self.dma.rx_available()
540 }
541
542 /// Whether the TX ring has room for a frame of `len` bytes.
543 #[inline(always)]
544 pub fn can_transmit(&self, len: usize) -> bool {
545 self.dma.can_transmit(len)
546 }
547
548 /// Whether at least one TX descriptor is available for the next frame.
549 #[inline(always)]
550 pub fn tx_ready(&self) -> bool {
551 self.dma.tx_available() > 0
552 }
553
554 // ── Interrupt helpers ──────────────────────────────────────────────────
555
556 /// Bind an interrupt handler to the EMAC peripheral and enable the
557 /// interrupt at the chip level.
558 #[cfg(feature = "esp-hal")]
559 pub fn bind_interrupt(&mut self, handler: esp_hal::interrupt::InterruptHandler) {
560 use esp_hal::peripherals::Interrupt;
561
562 for core in esp_hal::system::Cpu::other() {
563 esp_hal::interrupt::disable(core, Interrupt::ETH_MAC);
564 }
565 esp_hal::interrupt::bind_handler(Interrupt::ETH_MAC, handler);
566 esp_hal::interrupt::enable(Interrupt::ETH_MAC, handler.priority());
567 }
568
569 /// Disable the EMAC interrupt at the chip level.
570 #[cfg(feature = "esp-hal")]
571 pub fn disable_interrupt(&mut self) {
572 use esp_hal::peripherals::Interrupt;
573 esp_hal::interrupt::disable(esp_hal::system::Cpu::current(), Interrupt::ETH_MAC);
574 }
575
576 /// Read and parse the DMA status register.
577 pub fn interrupt_status(&self) -> InterruptStatus {
578 // SAFETY: read from a known-valid memory-mapped register.
579 let raw = unsafe {
580 core::ptr::read_volatile(
581 (crate::regs::dma::BASE + crate::regs::dma::DMASTATUS) as *const u32,
582 )
583 };
584 InterruptStatus::from_raw(raw)
585 }
586
587 /// Clear DMA status flags via write-1-to-clear.
588 ///
589 /// Writes the raw register snapshot back into `DMASTATUS`,
590 /// masked against [`crate::regs::dma::status::ALL_INTERRUPTS`] so
591 /// only the documented W1C interrupt bits are touched. The
592 /// non-W1C fields in `DMASTATUS` — `RS`/`TS` (process state),
593 /// `EB` (error bits), `MMC`/`PMT`/`TTI` — are read-only and
594 /// silently ignored by the hardware on write, but masking them
595 /// keeps the contract explicit: every bit we send is something
596 /// we mean to acknowledge.
597 ///
598 /// Pass the raw snapshot you previously read so every W1C bit
599 /// (including ones not modeled in [`InterruptStatus`] such as
600 /// `ERI` / `ETI` / `RWT`) is acknowledged in a single write.
601 pub fn clear_interrupts_raw(&self, raw: u32) {
602 // SAFETY: write to a known-valid memory-mapped register.
603 unsafe {
604 core::ptr::write_volatile(
605 (crate::regs::dma::BASE + crate::regs::dma::DMASTATUS) as *mut u32,
606 raw & crate::regs::dma::status::ALL_INTERRUPTS,
607 );
608 }
609 }
610
611 /// Convenience: handle the ISR — read status, clear all flags
612 /// (via the raw snapshot, so unrepresented W1C bits are also
613 /// acknowledged), return the parsed copy.
614 pub fn handle_interrupt(&self) -> InterruptStatus {
615 // SAFETY: read from a known-valid memory-mapped register.
616 let raw = unsafe {
617 core::ptr::read_volatile(
618 (crate::regs::dma::BASE + crate::regs::dma::DMASTATUS) as *const u32,
619 )
620 };
621 self.clear_interrupts_raw(raw);
622 InterruptStatus::from_raw(raw)
623 }
624}
625
626// `Default for Emac` is intentionally not implemented. The clock and pin
627// configuration is hardware-specific and silently picking one (e.g.
628// internal APLL on GPIO17) would mis-drive any board that expects an
629// external PHY-driven clock or that routes MDC/MDIO to non-default
630// GPIOs. Callers must construct an explicit `EmacConfig` — see the
631// crate-level docs and `RmiiClockConfig` for the available modes.
632
633// ── Default ring sizings ──────────────────────────────────────────────
634//
635// Single source of truth for the const generics that parameterize the
636// `EmacDefault` / `EmacSmall` aliases on the MAC side and the matching
637// `EmacDefaultDriver` / `EmacSmallDriver` aliases in `embassy.rs`. Keep
638// the driver aliases pulled from these constants — retuning a value
639// here updates both alias families together.
640
641/// RX descriptor ring size for [`EmacDefault`].
642pub const DEFAULT_RX: usize = 10;
643/// TX descriptor ring size for [`EmacDefault`].
644pub const DEFAULT_TX: usize = 10;
645/// Per-buffer length (bytes) for [`EmacDefault`] / [`EmacSmall`].
646pub const DEFAULT_BUF: usize = 1600;
647
648/// RX descriptor ring size for [`EmacSmall`].
649pub const SMALL_RX: usize = 4;
650/// TX descriptor ring size for [`EmacSmall`].
651pub const SMALL_TX: usize = 4;
652
653/// Convenience alias: [`DEFAULT_RX`] RX / [`DEFAULT_TX`] TX /
654/// [`DEFAULT_BUF`]-byte buffers (10/10/1600).
655pub type EmacDefault = Emac<DEFAULT_RX, DEFAULT_TX, DEFAULT_BUF>;
656
657/// Convenience alias: [`SMALL_RX`] RX / [`SMALL_TX`] TX /
658/// [`DEFAULT_BUF`]-byte buffers (4/4/1600).
659pub type EmacSmall = Emac<SMALL_RX, SMALL_TX, DEFAULT_BUF>;
660
661/// RX descriptor ring size for [`EmacBench`].
662pub const BENCH_RX: usize = 32;
663/// TX descriptor ring size for [`EmacBench`].
664pub const BENCH_TX: usize = 16;
665
666/// Deeper-ring EMAC configuration for high-pps or bursty workloads.
667///
668/// **Ring sizing:** 32 RX × 16 TX × [`DEFAULT_BUF`]-byte buffers.
669/// **Memory footprint:** ≈ 76.5 KiB total (32 × 32B desc + 32 × 1600B
670/// buf for RX, plus 16 × 32B desc + 16 × 1600B buf for TX — the ESP32
671/// EMAC enhanced descriptor layout is 8 dwords = 32 B per descriptor).
672///
673/// The default 10/10/1600 [`EmacDefault`] sizing is tuned for steady
674/// production traffic where DMA latency budget is small and 32 KiB of
675/// internal RAM is plenty. `EmacBench` deliberately over-provisions
676/// the rings so the DMA missed-frame counter
677/// ([`crate::regs::dma::missed_frames`]) stays at zero under burstier
678/// senders (e.g. tight spin-poll loops), which makes the measured
679/// throughput a property of the EMAC pipeline itself rather than of
680/// ring depth.
681///
682/// # Memory budget — caller's responsibility
683///
684/// The 76.5 KiB sits in `.bss` (internal DRAM only — ESP32 EMAC DMA
685/// is not PSRAM-capable on this silicon). Callers must verify their
686/// linker layout has the headroom; a typical pattern is to drop a
687/// `heap_allocator!` block of comparable size, or downsize to a
688/// `Emac<16, 8, 1600>` (≈ 38 KiB) if the full depth isn't required.
689pub type EmacBench = Emac<BENCH_RX, BENCH_TX, DEFAULT_BUF>;
690
691// =============================================================================
692// Helpers
693// =============================================================================
694
695/// Wraps a `&mut DelayNs` so it can be passed by value to APIs that take
696/// an owned `DelayNs` implementor (such as
697/// [`crate::reset::ResetController::with_timeout`]).
698struct BorrowedDelay<'a, D: DelayNs + ?Sized>(&'a mut D);
699
700impl<D: DelayNs + ?Sized> DelayNs for BorrowedDelay<'_, D> {
701 fn delay_ns(&mut self, ns: u32) {
702 self.0.delay_ns(ns);
703 }
704}
705
706// =============================================================================
707// Tests
708// =============================================================================
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 fn test_config() -> EmacConfig {
715 EmacConfig {
716 clock: RmiiClockConfig::InternalApll {
717 gpio: ClkGpio::Gpio17,
718 xtal: crate::config::XtalFreq::Mhz40,
719 },
720 pins: crate::config::RmiiPins::default(),
721 }
722 }
723
724 #[test]
725 fn new_is_uninitialized() {
726 let emac: EmacDefault = Emac::new(test_config());
727 assert_eq!(emac.state(), EmacState::Uninitialized);
728 assert_eq!(emac.mac_address(), [0u8; 6]);
729 }
730
731 #[test]
732 fn set_mac_before_init_only_caches() {
733 let mut emac: EmacDefault = Emac::new(test_config());
734 let mac = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
735 emac.set_mac_address(mac);
736 assert_eq!(emac.mac_address(), mac);
737 // No register writes performed because state is Uninitialized.
738 }
739
740 #[test]
741 fn memory_usage_matches_dma() {
742 // Source the comparison from the same constants as the alias
743 // itself — retuning `DEFAULT_*` continues to match without
744 // touching this test.
745 assert_eq!(
746 EmacDefault::memory_usage(),
747 DmaEngine::<DEFAULT_RX, DEFAULT_TX, DEFAULT_BUF>::memory_usage()
748 );
749 }
750}