esp-emac 0.3.0

ESP32 EMAC bare-metal Ethernet MAC driver with DMA, RMII, and MDIO
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
// SPDX-License-Identifier: GPL-2.0-or-later OR Apache-2.0
// Copyright (c) Viacheslav Bocharov <v@baodeep.com> and JetHome (r)

//! APLL 50 MHz clock configuration and GPIO clock output/input setup.
//!
//! The ESP32 EMAC RMII interface requires a 50 MHz reference clock.
//! It can be generated internally by the Audio PLL (APLL) or supplied
//! externally from the PHY crystal oscillator.
//!
//! ## Internal APLL mode
//!
//! 1. [`configure_apll_50mhz`] powers up APLL and programs its coefficients
//!    via ROM I2C to produce 50 MHz from the 40 MHz XTAL.
//! 2. [`configure_emac_clk_out`] sets up a GPIO (0, 16, or 17) as clock
//!    output via IO_MUX function 5 so the PHY receives 50 MHz.
//!
//! The EMAC_EXT clock path registers (int_en, clk_sel, clk_en) are
//! configured separately by [`Emac::init`](crate::emac::Emac::init)
//! via `configure_phy_interface()` and `enable_ext_clocks()`.
//!
//! ## External clock mode
//!
//! [`configure_emac_clk_in`] sets up a GPIO as clock input via IO_MUX.
//! The EMAC_EXT registers for external mode are handled by `Emac::init`.
//!
//! ## APLL/WiFi conflict
//!
//! APLL cannot coexist with WiFi/BT (ESP32 errata CLK-3.22).
//! Use external clock when Ethernet + WiFi is needed.
//!
//! ## ROM I2C details
//!
//! esp-hal does not yet expose APLL configuration (its `soc/esp32/clocks.rs`
//! has `todo!()`). We use the ROM I2C functions directly:
//! - APLL I2C block ID: `0x6D`, host ID: **3** (verified on hardware).
//! - ANA_CONF register (`0x3FF4_8030`): bit 24 = PU, bit 23 = PD.

use crate::config::{ClkGpio, XtalFreq};

// =============================================================================
// APLL ROM I2C constants
// =============================================================================

/// APLL I2C block identifier for ROM I2C functions.
const I2C_APLL: u8 = 0x6D;

/// APLL I2C host identifier (ESP32-specific, verified on hardware).
///
/// ESP-IDF headers suggest 0 or 4, but hardware testing confirmed
/// host ID 3 is correct for ESP32 APLL access.
const I2C_APLL_HOSTID: u8 = 3;

/// RTC analog configuration register address.
///
/// Contains APLL power-up (bit 24) and power-down (bit 23) controls.
/// From ESP32 SVD: `RTC_CNTL_ANA_CONF_REG`.
const ANA_CONF_REG: usize = 0x3FF4_8030;

/// APLL force power-up bit in ANA_CONF (bit 24).
const ANA_CONF_PLLA_FORCE_PU: u32 = 1 << 24;

/// APLL force power-down bit in ANA_CONF (bit 23).
const ANA_CONF_PLLA_FORCE_PD: u32 = 1 << 23;

// =============================================================================
// GPIO/IO_MUX constants
// =============================================================================

/// IO_MUX base address (ESP32).
const IO_MUX_BASE: usize = 0x3FF4_9000;

/// GPIO peripheral base address.
const GPIO_BASE: usize = 0x3FF4_4000;

/// GPIO output function select register base offset.
/// For GPIO N: `GPIO_BASE + 0x530 + N*4`.
const GPIO_FUNC_OUT_SEL_BASE: usize = GPIO_BASE + 0x530;

/// GPIO output enable set (write-1-to-set) register.
const GPIO_ENABLE_W1TS: usize = GPIO_BASE + 0x024;

/// IO_MUX MCU_SEL field mask (bits 14:12).
const MCU_SEL_MASK: u32 = 0x7 << 12;

/// IO_MUX FUN_DRV (drive strength) field mask (bits 11:10).
const FUN_DRV_MASK: u32 = 0x3 << 10;

/// IO_MUX FUN_IE (input enable) bit 9.
const FUN_IE: u32 = 1 << 9;

/// Number of spin-loop iterations to wait after APLL power-up.
///
/// Matches the firmware reference. Provides ~10-20 us settling time
/// at typical ESP32 CPU frequencies (160-240 MHz).
const APLL_POWER_UP_SPIN: u32 = 10_000;

// =============================================================================
// ROM I2C FFI
// =============================================================================

unsafe extern "C" {
    fn rom_i2c_writeReg(block: u8, block_hostid: u8, reg_add: u8, indata: u8);
    fn rom_i2c_readReg(block: u8, block_hostid: u8, reg_add: u8) -> u8;
}

/// Read an APLL register via ROM I2C.
#[inline(always)]
fn regi2c_read(reg: u8) -> u8 {
    // SAFETY: ROM I2C functions are always available on ESP32.
    unsafe { rom_i2c_readReg(I2C_APLL, I2C_APLL_HOSTID, reg) }
}

/// Write an APLL register via ROM I2C.
#[inline(always)]
fn regi2c_write(reg: u8, data: u8) {
    // SAFETY: ROM I2C functions are always available on ESP32.
    unsafe { rom_i2c_writeReg(I2C_APLL, I2C_APLL_HOSTID, reg, data) }
}

/// Masked write to an APLL register: modify bits `[msb:lsb]` to `val`.
fn apll_write_mask(reg: u8, msb: u8, lsb: u8, val: u8) {
    let old = regi2c_read(reg);
    let mask = ((1u16 << (msb - lsb + 1)) - 1) as u8;
    let new = (old & !(mask << lsb)) | ((val & mask) << lsb);
    regi2c_write(reg, new);
}

// =============================================================================
// Public API
// =============================================================================

/// SDM coefficients for the ESP32 APLL.
///
/// Output frequency formula:
///
/// ```text
/// fout = fxtal * (sdm2 + sdm1/256 + sdm0/65536 + 4) / (2 * (o_div + 2))
/// ```
///
/// For each supported on-board crystal, [`ApllCoefficients::for_xtal`]
/// returns the coefficients that land on **50 MHz** (the RMII reference
/// clock).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ApllCoefficients {
    /// Fine fractional multiplier (×1/65536). 8-bit field.
    pub sdm0: u8,
    /// Mid fractional multiplier (×1/256). 8-bit field.
    pub sdm1: u8,
    /// Integer-part multiplier (added to fixed +4). 6-bit field
    /// (`apll_write_mask(7, 5, 0, sdm2)`).
    pub sdm2: u8,
    /// Output divider field. Final divisor is `2 * (o_div + 2)`. 5-bit
    /// field (`apll_write_mask(4, 4, 0, o_div)`).
    pub o_div: u8,
}

impl ApllCoefficients {
    /// Look up the coefficients that produce a 50 MHz APLL output for
    /// the given on-board crystal.
    ///
    /// Total: infallible — the input is constrained by [`XtalFreq`],
    /// which only enumerates crystals the crate has verified
    /// coefficients for (`Mhz26` / `Mhz32` / `Mhz40`). Adding support
    /// for another crystal therefore takes two concrete edits — extend
    /// `XtalFreq` with the new variant, and add a matching arm here —
    /// followed by a host-side unit test asserting the new arm lands
    /// on 50 MHz.
    ///
    /// Verified results (target 50.000 MHz):
    ///
    /// | XTAL  | sdm2 | sdm1 | sdm0 | o_div | Computed fout |
    /// |-------|------|------|------|-------|---------------|
    /// | 26 MHz| 11   | 98   | 118  | 2     | 50.0000 MHz   |
    /// | 32 MHz| 8    | 128  | 0    | 2     | 50.0000 MHz   |
    /// | 40 MHz| 6    | 0    | 0    | 2     | 50.0000 MHz   |
    pub const fn for_xtal(xtal: XtalFreq) -> Self {
        match xtal {
            // 50 MHz = 26 MHz * (11 + 98/256 + 118/65536 + 4) / 8
            XtalFreq::Mhz26 => Self {
                sdm0: 118,
                sdm1: 98,
                sdm2: 11,
                o_div: 2,
            },
            // 50 MHz = 32 MHz * (8 + 128/256 + 0/65536 + 4) / 8
            XtalFreq::Mhz32 => Self {
                sdm0: 0,
                sdm1: 128,
                sdm2: 8,
                o_div: 2,
            },
            // 50 MHz = 40 MHz * (6 + 0 + 0 + 4) / 8
            XtalFreq::Mhz40 => Self {
                sdm0: 0,
                sdm1: 0,
                sdm2: 6,
                o_div: 2,
            },
        }
    }
}

/// Configure ESP32 APLL to output 50 MHz for EMAC RMII clock,
/// using SDM coefficients chosen for the on-board crystal.
///
/// APLL formula: `fout = fxtal * (sdm2 + sdm1/256 + sdm0/65536 + 4) / (2 * (o_div + 2))`.
/// See [`ApllCoefficients::for_xtal`] for the per-crystal table.
///
/// This function:
/// 1. Powers up APLL via ANA_CONF register
/// 2. Programmes SDM coefficients (`sdm2`/`sdm1`/`sdm0`/`o_div`) for the
///    requested crystal
/// 3. Runs the calibration sequence (from ESP-IDF `clk_ll_apll_set_config`)
///
/// The EMAC_EXT clock path registers (RMII mode, int_en, clk_sel) are
/// configured separately by [`Emac::init`](crate::emac::Emac::init).
///
/// # Ordering
///
/// Independent of the EMAC peripheral clock — the routine only writes
/// RTC analog registers (`ANA_CONF`) and APLL coefficients via the ROM
/// I2C controller, both of which sit on the always-on APB clock from
/// XTAL/main PLL. May be called before or after
/// `ext::enable_peripheral_clock`. Only required when the MCU is the
/// RMII clock master (i.e. `RmiiClockConfig::InternalApll`); skip it
/// entirely for `RmiiClockConfig::External`.
///
/// # Safety
///
/// Writes to RTC analog registers and APLL coefficients via ROM I2C.
/// Don't call concurrently with other RTC analog reconfiguration.
pub fn configure_apll_50mhz(xtal: XtalFreq) {
    let c = ApllCoefficients::for_xtal(xtal);

    // Step 1: Power up APLL
    // ANA_CONF: clear PD (bit 23), set PU (bit 24)
    unsafe {
        let ana = core::ptr::read_volatile(ANA_CONF_REG as *const u32);
        core::ptr::write_volatile(
            ANA_CONF_REG as *mut u32,
            (ana & !ANA_CONF_PLLA_FORCE_PD) | ANA_CONF_PLLA_FORCE_PU,
        );
    }
    // Wait for APLL to stabilize.
    for _ in 0..APLL_POWER_UP_SPIN {
        core::hint::spin_loop();
    }

    // Step 2: APLL coefficients — chosen by `for_xtal`.
    apll_write_mask(7, 5, 0, c.sdm2);
    apll_write_mask(9, 7, 0, c.sdm0);
    apll_write_mask(8, 7, 0, c.sdm1);

    // Step 3: Calibration sequence (from ESP-IDF clk_ll_apll_set_config)
    regi2c_write(5, 0x09);
    regi2c_write(5, 0x49);
    apll_write_mask(4, 4, 0, c.o_div);
    regi2c_write(0, 0x0F);
    regi2c_write(0, 0x3F);
    regi2c_write(0, 0x1F);
}

/// Configure a GPIO as EMAC 50 MHz RMII clock output via IO_MUX function 5.
///
/// On ESP32, only GPIO0, GPIO16, and GPIO17 support EMAC clock output:
/// - GPIO0:  `EMAC_TX_CLK` (also boot strapping -- use with caution)
/// - GPIO16: `EMAC_CLK_OUT` (0 degree phase)
/// - GPIO17: `EMAC_CLK_OUT_180` (180 degree phase, most common for LAN8720A)
///
/// Sets IO_MUX to function 5 with maximum drive strength, disconnects
/// the GPIO Matrix (IO_MUX direct), and enables the output driver.
///
/// # Safety
///
/// Writes to IO_MUX and GPIO registers. Must be called before DMA reset.
pub fn configure_emac_clk_out(gpio: ClkGpio) {
    let io_mux_addr = io_mux_addr_for_clk_gpio(gpio);
    let gpio_num = gpio.gpio_num() as usize;

    unsafe {
        // Set IO_MUX function 5 (EMAC clock) + maximum drive strength (3).
        let val = core::ptr::read_volatile(io_mux_addr as *const u32);
        core::ptr::write_volatile(
            io_mux_addr as *mut u32,
            (val & !MCU_SEL_MASK & !FUN_DRV_MASK) | (5 << 12) | (3 << 10),
        );

        // Disconnect GPIO Matrix -- use IO_MUX directly.
        // Writing 256 (SIG_GPIO_OUT_IDX) disconnects the Matrix output.
        core::ptr::write_volatile((GPIO_FUNC_OUT_SEL_BASE + gpio_num * 4) as *mut u32, 256);

        // Enable output driver.
        core::ptr::write_volatile(GPIO_ENABLE_W1TS as *mut u32, 1u32 << gpio_num);
    }
}

/// Configure a GPIO as EMAC external 50 MHz clock input via IO_MUX.
///
/// Sets IO_MUX to function 5 with input enabled. Disconnects GPIO Matrix
/// to ensure IO_MUX is used directly.
///
/// Typically GPIO0 (`EMAC_TX_CLK` / RMII ref clock input).
///
/// # Safety
///
/// Writes to IO_MUX and GPIO registers. Must be called before DMA reset.
pub fn configure_emac_clk_in(gpio: ClkGpio) {
    let io_mux_addr = io_mux_addr_for_clk_gpio(gpio);
    let gpio_num = gpio.gpio_num() as usize;

    unsafe {
        // Set IO_MUX function 5 (EMAC clock) + input enable.
        let val = core::ptr::read_volatile(io_mux_addr as *const u32);
        core::ptr::write_volatile(
            io_mux_addr as *mut u32,
            (val & !MCU_SEL_MASK) | (5 << 12) | FUN_IE,
        );

        // Disconnect GPIO Matrix output -- use IO_MUX directly.
        core::ptr::write_volatile((GPIO_FUNC_OUT_SEL_BASE + gpio_num * 4) as *mut u32, 256);
    }
}

// =============================================================================
// Helpers
// =============================================================================

/// Return the IO_MUX register address for a clock-capable GPIO.
///
/// Based on ESP32 TRM Table 4-3:
/// - GPIO0:  offset 0x44
/// - GPIO16: offset 0x4C
/// - GPIO17: offset 0x50
const fn io_mux_addr_for_clk_gpio(gpio: ClkGpio) -> usize {
    let offset = match gpio {
        ClkGpio::Gpio0 => 0x44,
        ClkGpio::Gpio16 => 0x4C,
        ClkGpio::Gpio17 => 0x50,
    };
    IO_MUX_BASE + offset
}

// =============================================================================
// Tests
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn clk_gpio_io_mux_addresses() {
        // Verify IO_MUX offsets match the ESP32 TRM pad list.
        assert_eq!(
            io_mux_addr_for_clk_gpio(ClkGpio::Gpio0),
            0x3FF4_9044,
            "GPIO0 IO_MUX address mismatch"
        );
        assert_eq!(
            io_mux_addr_for_clk_gpio(ClkGpio::Gpio16),
            0x3FF4_904C,
            "GPIO16 IO_MUX address mismatch"
        );
        assert_eq!(
            io_mux_addr_for_clk_gpio(ClkGpio::Gpio17),
            0x3FF4_9050,
            "GPIO17 IO_MUX address mismatch"
        );
    }

    #[test]
    fn clk_gpio_numbers_match_enum() {
        assert_eq!(ClkGpio::Gpio0.gpio_num(), 0);
        assert_eq!(ClkGpio::Gpio16.gpio_num(), 16);
        assert_eq!(ClkGpio::Gpio17.gpio_num(), 17);
    }

    #[test]
    fn ana_conf_bits_no_overlap() {
        assert_eq!(
            ANA_CONF_PLLA_FORCE_PU & ANA_CONF_PLLA_FORCE_PD,
            0,
            "PU and PD bits must not overlap"
        );
    }

    #[test]
    fn ana_conf_bit_positions() {
        // PD = bit 23, PU = bit 24
        assert_eq!(ANA_CONF_PLLA_FORCE_PD, 1 << 23);
        assert_eq!(ANA_CONF_PLLA_FORCE_PU, 1 << 24);
    }

    #[test]
    fn ana_conf_register_address() {
        assert_eq!(ANA_CONF_REG, 0x3FF4_8030);
    }

    #[test]
    fn apll_constants() {
        assert_eq!(I2C_APLL, 0x6D);
        assert_eq!(I2C_APLL_HOSTID, 3);
    }

    #[test]
    fn io_mux_base_consistent_with_ext_regs() {
        assert_eq!(IO_MUX_BASE, crate::regs::ext::IO_MUX_BASE);
    }

    #[test]
    fn gpio_register_layout() {
        // GPIO_FUNC_OUT_SEL for GPIO0 should be at GPIO_BASE + 0x530
        assert_eq!(GPIO_FUNC_OUT_SEL_BASE, 0x3FF4_4530);
        // GPIO_ENABLE_W1TS should be at GPIO_BASE + 0x024
        assert_eq!(GPIO_ENABLE_W1TS, 0x3FF4_4024);
    }

    #[test]
    fn mcu_sel_mask_covers_function_5() {
        // Function 5 = 0b101, fits in 3-bit MCU_SEL field at bits 14:12
        let func5_shifted = 5u32 << 12;
        assert_eq!(func5_shifted & MCU_SEL_MASK, func5_shifted);
    }

    #[test]
    fn fun_drv_max_strength() {
        // Max drive strength = 3, shifted to bits 11:10
        let max_drv = 3u32 << 10;
        assert_eq!(max_drv & FUN_DRV_MASK, max_drv);
    }

    // ── APLL coefficients ────────────────────────────────────────────────

    /// Compute output frequency in MHz·Q16 fixed-point from APLL
    /// coefficients, for a host-side sanity check that the table really
    /// lands on 50 MHz. Matches the silicon formula:
    ///   fout = fxtal * (sdm2 + sdm1/256 + sdm0/65536 + 4) / (2 * (o_div + 2))
    fn fout_mhz_q16(c: ApllCoefficients, xtal_mhz: u32) -> u64 {
        let num = (xtal_mhz as u64)
            * (((c.sdm2 as u64 + 4) << 16) + (c.sdm1 as u64 * 256) + c.sdm0 as u64);
        let denom = 2 * (c.o_div as u64 + 2);
        num / denom
    }

    fn assert_50mhz(c: ApllCoefficients, xtal_mhz: u32) {
        let q16 = fout_mhz_q16(c, xtal_mhz);
        // 50 MHz in Q16: 50 << 16 = 3_276_800.
        let target_q16 = 50u64 << 16;
        // Allow ±0.001 MHz drift.
        let drift = q16.abs_diff(target_q16);
        assert!(
            drift < 100,
            "fout for {} MHz XTAL is {} (Q16) — drift {} from 50 MHz target",
            xtal_mhz,
            q16,
            drift
        );
    }

    #[test]
    fn apll_coefficients_xtal_40_lands_on_50mhz() {
        assert_50mhz(ApllCoefficients::for_xtal(XtalFreq::Mhz40), 40);
    }

    #[test]
    fn apll_coefficients_xtal_32_lands_on_50mhz() {
        assert_50mhz(ApllCoefficients::for_xtal(XtalFreq::Mhz32), 32);
    }

    #[test]
    fn apll_coefficients_xtal_26_lands_on_50mhz() {
        assert_50mhz(ApllCoefficients::for_xtal(XtalFreq::Mhz26), 26);
    }

    #[test]
    fn apll_coefficients_register_field_widths() {
        // o_div is a 5-bit field, sdm2 is 6-bit.
        for xtal in [XtalFreq::Mhz26, XtalFreq::Mhz32, XtalFreq::Mhz40] {
            let c = ApllCoefficients::for_xtal(xtal);
            assert!(
                c.o_div < 32,
                "o_div for {:?} = {} doesn't fit 5 bits",
                xtal,
                c.o_div
            );
            assert!(
                c.sdm2 < 64,
                "sdm2 for {:?} = {} doesn't fit 6 bits",
                xtal,
                c.sdm2
            );
        }
    }
}