Skip to main content

esp_emac/
clock.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//! APLL 50 MHz clock configuration and GPIO clock output/input setup.
5//!
6//! The ESP32 EMAC RMII interface requires a 50 MHz reference clock.
7//! It can be generated internally by the Audio PLL (APLL) or supplied
8//! externally from the PHY crystal oscillator.
9//!
10//! ## Internal APLL mode
11//!
12//! 1. [`configure_apll_50mhz`] powers up APLL and programs its coefficients
13//!    via ROM I2C to produce 50 MHz from the 40 MHz XTAL.
14//! 2. [`configure_emac_clk_out`] sets up a GPIO (0, 16, or 17) as clock
15//!    output via IO_MUX function 5 so the PHY receives 50 MHz.
16//!
17//! The EMAC_EXT clock path registers (int_en, clk_sel, clk_en) are
18//! configured separately by [`Emac::init`](crate::emac::Emac::init)
19//! via `configure_phy_interface()` and `enable_ext_clocks()`.
20//!
21//! ## External clock mode
22//!
23//! [`configure_emac_clk_in`] sets up a GPIO as clock input via IO_MUX.
24//! The EMAC_EXT registers for external mode are handled by `Emac::init`.
25//!
26//! ## APLL/WiFi conflict
27//!
28//! APLL cannot coexist with WiFi/BT (ESP32 errata CLK-3.22).
29//! Use external clock when Ethernet + WiFi is needed.
30//!
31//! ## ROM I2C details
32//!
33//! esp-hal does not yet expose APLL configuration (its `soc/esp32/clocks.rs`
34//! has `todo!()`). We use the ROM I2C functions directly:
35//! - APLL I2C block ID: `0x6D`, host ID: **3** (verified on hardware).
36//! - ANA_CONF register (`0x3FF4_8030`): bit 24 = PU, bit 23 = PD.
37
38use crate::config::{ClkGpio, XtalFreq};
39
40// =============================================================================
41// APLL ROM I2C constants
42// =============================================================================
43
44/// APLL I2C block identifier for ROM I2C functions.
45const I2C_APLL: u8 = 0x6D;
46
47/// APLL I2C host identifier (ESP32-specific, verified on hardware).
48///
49/// ESP-IDF headers suggest 0 or 4, but hardware testing confirmed
50/// host ID 3 is correct for ESP32 APLL access.
51const I2C_APLL_HOSTID: u8 = 3;
52
53/// RTC analog configuration register address.
54///
55/// Contains APLL power-up (bit 24) and power-down (bit 23) controls.
56/// From ESP32 SVD: `RTC_CNTL_ANA_CONF_REG`.
57const ANA_CONF_REG: usize = 0x3FF4_8030;
58
59/// APLL force power-up bit in ANA_CONF (bit 24).
60const ANA_CONF_PLLA_FORCE_PU: u32 = 1 << 24;
61
62/// APLL force power-down bit in ANA_CONF (bit 23).
63const ANA_CONF_PLLA_FORCE_PD: u32 = 1 << 23;
64
65// =============================================================================
66// GPIO/IO_MUX constants
67// =============================================================================
68
69/// IO_MUX base address (ESP32).
70const IO_MUX_BASE: usize = 0x3FF4_9000;
71
72/// GPIO peripheral base address.
73const GPIO_BASE: usize = 0x3FF4_4000;
74
75/// GPIO output function select register base offset.
76/// For GPIO N: `GPIO_BASE + 0x530 + N*4`.
77const GPIO_FUNC_OUT_SEL_BASE: usize = GPIO_BASE + 0x530;
78
79/// GPIO output enable set (write-1-to-set) register.
80const GPIO_ENABLE_W1TS: usize = GPIO_BASE + 0x024;
81
82/// IO_MUX MCU_SEL field mask (bits 14:12).
83const MCU_SEL_MASK: u32 = 0x7 << 12;
84
85/// IO_MUX FUN_DRV (drive strength) field mask (bits 11:10).
86const FUN_DRV_MASK: u32 = 0x3 << 10;
87
88/// IO_MUX FUN_IE (input enable) bit 9.
89const FUN_IE: u32 = 1 << 9;
90
91/// Number of spin-loop iterations to wait after APLL power-up.
92///
93/// Matches the firmware reference. Provides ~10-20 us settling time
94/// at typical ESP32 CPU frequencies (160-240 MHz).
95const APLL_POWER_UP_SPIN: u32 = 10_000;
96
97// =============================================================================
98// ROM I2C FFI
99// =============================================================================
100
101unsafe extern "C" {
102    fn rom_i2c_writeReg(block: u8, block_hostid: u8, reg_add: u8, indata: u8);
103    fn rom_i2c_readReg(block: u8, block_hostid: u8, reg_add: u8) -> u8;
104}
105
106/// Read an APLL register via ROM I2C.
107#[inline(always)]
108fn regi2c_read(reg: u8) -> u8 {
109    // SAFETY: ROM I2C functions are always available on ESP32.
110    unsafe { rom_i2c_readReg(I2C_APLL, I2C_APLL_HOSTID, reg) }
111}
112
113/// Write an APLL register via ROM I2C.
114#[inline(always)]
115fn regi2c_write(reg: u8, data: u8) {
116    // SAFETY: ROM I2C functions are always available on ESP32.
117    unsafe { rom_i2c_writeReg(I2C_APLL, I2C_APLL_HOSTID, reg, data) }
118}
119
120/// Masked write to an APLL register: modify bits `[msb:lsb]` to `val`.
121fn apll_write_mask(reg: u8, msb: u8, lsb: u8, val: u8) {
122    let old = regi2c_read(reg);
123    let mask = ((1u16 << (msb - lsb + 1)) - 1) as u8;
124    let new = (old & !(mask << lsb)) | ((val & mask) << lsb);
125    regi2c_write(reg, new);
126}
127
128// =============================================================================
129// Public API
130// =============================================================================
131
132/// SDM coefficients for the ESP32 APLL.
133///
134/// Output frequency formula:
135///
136/// ```text
137/// fout = fxtal * (sdm2 + sdm1/256 + sdm0/65536 + 4) / (2 * (o_div + 2))
138/// ```
139///
140/// For each supported on-board crystal, [`ApllCoefficients::for_xtal`]
141/// returns the coefficients that land on **50 MHz** (the RMII reference
142/// clock).
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct ApllCoefficients {
145    /// Fine fractional multiplier (×1/65536). 8-bit field.
146    pub sdm0: u8,
147    /// Mid fractional multiplier (×1/256). 8-bit field.
148    pub sdm1: u8,
149    /// Integer-part multiplier (added to fixed +4). 6-bit field
150    /// (`apll_write_mask(7, 5, 0, sdm2)`).
151    pub sdm2: u8,
152    /// Output divider field. Final divisor is `2 * (o_div + 2)`. 5-bit
153    /// field (`apll_write_mask(4, 4, 0, o_div)`).
154    pub o_div: u8,
155}
156
157impl ApllCoefficients {
158    /// Look up the coefficients that produce a 50 MHz APLL output for
159    /// the given on-board crystal.
160    ///
161    /// Total: infallible — the input is constrained by [`XtalFreq`],
162    /// which only enumerates crystals the crate has verified
163    /// coefficients for (`Mhz26` / `Mhz32` / `Mhz40`). Adding support
164    /// for another crystal therefore takes two concrete edits — extend
165    /// `XtalFreq` with the new variant, and add a matching arm here —
166    /// followed by a host-side unit test asserting the new arm lands
167    /// on 50 MHz.
168    ///
169    /// Verified results (target 50.000 MHz):
170    ///
171    /// | XTAL  | sdm2 | sdm1 | sdm0 | o_div | Computed fout |
172    /// |-------|------|------|------|-------|---------------|
173    /// | 26 MHz| 11   | 98   | 118  | 2     | 50.0000 MHz   |
174    /// | 32 MHz| 8    | 128  | 0    | 2     | 50.0000 MHz   |
175    /// | 40 MHz| 6    | 0    | 0    | 2     | 50.0000 MHz   |
176    pub const fn for_xtal(xtal: XtalFreq) -> Self {
177        match xtal {
178            // 50 MHz = 26 MHz * (11 + 98/256 + 118/65536 + 4) / 8
179            XtalFreq::Mhz26 => Self {
180                sdm0: 118,
181                sdm1: 98,
182                sdm2: 11,
183                o_div: 2,
184            },
185            // 50 MHz = 32 MHz * (8 + 128/256 + 0/65536 + 4) / 8
186            XtalFreq::Mhz32 => Self {
187                sdm0: 0,
188                sdm1: 128,
189                sdm2: 8,
190                o_div: 2,
191            },
192            // 50 MHz = 40 MHz * (6 + 0 + 0 + 4) / 8
193            XtalFreq::Mhz40 => Self {
194                sdm0: 0,
195                sdm1: 0,
196                sdm2: 6,
197                o_div: 2,
198            },
199        }
200    }
201}
202
203/// Configure ESP32 APLL to output 50 MHz for EMAC RMII clock,
204/// using SDM coefficients chosen for the on-board crystal.
205///
206/// APLL formula: `fout = fxtal * (sdm2 + sdm1/256 + sdm0/65536 + 4) / (2 * (o_div + 2))`.
207/// See [`ApllCoefficients::for_xtal`] for the per-crystal table.
208///
209/// This function:
210/// 1. Powers up APLL via ANA_CONF register
211/// 2. Programmes SDM coefficients (`sdm2`/`sdm1`/`sdm0`/`o_div`) for the
212///    requested crystal
213/// 3. Runs the calibration sequence (from ESP-IDF `clk_ll_apll_set_config`)
214///
215/// The EMAC_EXT clock path registers (RMII mode, int_en, clk_sel) are
216/// configured separately by [`Emac::init`](crate::emac::Emac::init).
217///
218/// # Ordering
219///
220/// Independent of the EMAC peripheral clock — the routine only writes
221/// RTC analog registers (`ANA_CONF`) and APLL coefficients via the ROM
222/// I2C controller, both of which sit on the always-on APB clock from
223/// XTAL/main PLL. May be called before or after
224/// `ext::enable_peripheral_clock`. Only required when the MCU is the
225/// RMII clock master (i.e. `RmiiClockConfig::InternalApll`); skip it
226/// entirely for `RmiiClockConfig::External`.
227///
228/// # Safety
229///
230/// Writes to RTC analog registers and APLL coefficients via ROM I2C.
231/// Don't call concurrently with other RTC analog reconfiguration.
232pub fn configure_apll_50mhz(xtal: XtalFreq) {
233    let c = ApllCoefficients::for_xtal(xtal);
234
235    // Step 1: Power up APLL
236    // ANA_CONF: clear PD (bit 23), set PU (bit 24)
237    unsafe {
238        let ana = core::ptr::read_volatile(ANA_CONF_REG as *const u32);
239        core::ptr::write_volatile(
240            ANA_CONF_REG as *mut u32,
241            (ana & !ANA_CONF_PLLA_FORCE_PD) | ANA_CONF_PLLA_FORCE_PU,
242        );
243    }
244    // Wait for APLL to stabilize.
245    for _ in 0..APLL_POWER_UP_SPIN {
246        core::hint::spin_loop();
247    }
248
249    // Step 2: APLL coefficients — chosen by `for_xtal`.
250    apll_write_mask(7, 5, 0, c.sdm2);
251    apll_write_mask(9, 7, 0, c.sdm0);
252    apll_write_mask(8, 7, 0, c.sdm1);
253
254    // Step 3: Calibration sequence (from ESP-IDF clk_ll_apll_set_config)
255    regi2c_write(5, 0x09);
256    regi2c_write(5, 0x49);
257    apll_write_mask(4, 4, 0, c.o_div);
258    regi2c_write(0, 0x0F);
259    regi2c_write(0, 0x3F);
260    regi2c_write(0, 0x1F);
261}
262
263/// Configure a GPIO as EMAC 50 MHz RMII clock output via IO_MUX function 5.
264///
265/// On ESP32, only GPIO0, GPIO16, and GPIO17 support EMAC clock output:
266/// - GPIO0:  `EMAC_TX_CLK` (also boot strapping -- use with caution)
267/// - GPIO16: `EMAC_CLK_OUT` (0 degree phase)
268/// - GPIO17: `EMAC_CLK_OUT_180` (180 degree phase, most common for LAN8720A)
269///
270/// Sets IO_MUX to function 5 with maximum drive strength, disconnects
271/// the GPIO Matrix (IO_MUX direct), and enables the output driver.
272///
273/// # Safety
274///
275/// Writes to IO_MUX and GPIO registers. Must be called before DMA reset.
276pub fn configure_emac_clk_out(gpio: ClkGpio) {
277    let io_mux_addr = io_mux_addr_for_clk_gpio(gpio);
278    let gpio_num = gpio.gpio_num() as usize;
279
280    unsafe {
281        // Set IO_MUX function 5 (EMAC clock) + maximum drive strength (3).
282        let val = core::ptr::read_volatile(io_mux_addr as *const u32);
283        core::ptr::write_volatile(
284            io_mux_addr as *mut u32,
285            (val & !MCU_SEL_MASK & !FUN_DRV_MASK) | (5 << 12) | (3 << 10),
286        );
287
288        // Disconnect GPIO Matrix -- use IO_MUX directly.
289        // Writing 256 (SIG_GPIO_OUT_IDX) disconnects the Matrix output.
290        core::ptr::write_volatile((GPIO_FUNC_OUT_SEL_BASE + gpio_num * 4) as *mut u32, 256);
291
292        // Enable output driver.
293        core::ptr::write_volatile(GPIO_ENABLE_W1TS as *mut u32, 1u32 << gpio_num);
294    }
295}
296
297/// Configure a GPIO as EMAC external 50 MHz clock input via IO_MUX.
298///
299/// Sets IO_MUX to function 5 with input enabled. Disconnects GPIO Matrix
300/// to ensure IO_MUX is used directly.
301///
302/// Typically GPIO0 (`EMAC_TX_CLK` / RMII ref clock input).
303///
304/// # Safety
305///
306/// Writes to IO_MUX and GPIO registers. Must be called before DMA reset.
307pub fn configure_emac_clk_in(gpio: ClkGpio) {
308    let io_mux_addr = io_mux_addr_for_clk_gpio(gpio);
309    let gpio_num = gpio.gpio_num() as usize;
310
311    unsafe {
312        // Set IO_MUX function 5 (EMAC clock) + input enable.
313        let val = core::ptr::read_volatile(io_mux_addr as *const u32);
314        core::ptr::write_volatile(
315            io_mux_addr as *mut u32,
316            (val & !MCU_SEL_MASK) | (5 << 12) | FUN_IE,
317        );
318
319        // Disconnect GPIO Matrix output -- use IO_MUX directly.
320        core::ptr::write_volatile((GPIO_FUNC_OUT_SEL_BASE + gpio_num * 4) as *mut u32, 256);
321    }
322}
323
324// =============================================================================
325// Helpers
326// =============================================================================
327
328/// Return the IO_MUX register address for a clock-capable GPIO.
329///
330/// Based on ESP32 TRM Table 4-3:
331/// - GPIO0:  offset 0x44
332/// - GPIO16: offset 0x4C
333/// - GPIO17: offset 0x50
334const fn io_mux_addr_for_clk_gpio(gpio: ClkGpio) -> usize {
335    let offset = match gpio {
336        ClkGpio::Gpio0 => 0x44,
337        ClkGpio::Gpio16 => 0x4C,
338        ClkGpio::Gpio17 => 0x50,
339    };
340    IO_MUX_BASE + offset
341}
342
343// =============================================================================
344// Tests
345// =============================================================================
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn clk_gpio_io_mux_addresses() {
353        // Verify IO_MUX offsets match the ESP32 TRM pad list.
354        assert_eq!(
355            io_mux_addr_for_clk_gpio(ClkGpio::Gpio0),
356            0x3FF4_9044,
357            "GPIO0 IO_MUX address mismatch"
358        );
359        assert_eq!(
360            io_mux_addr_for_clk_gpio(ClkGpio::Gpio16),
361            0x3FF4_904C,
362            "GPIO16 IO_MUX address mismatch"
363        );
364        assert_eq!(
365            io_mux_addr_for_clk_gpio(ClkGpio::Gpio17),
366            0x3FF4_9050,
367            "GPIO17 IO_MUX address mismatch"
368        );
369    }
370
371    #[test]
372    fn clk_gpio_numbers_match_enum() {
373        assert_eq!(ClkGpio::Gpio0.gpio_num(), 0);
374        assert_eq!(ClkGpio::Gpio16.gpio_num(), 16);
375        assert_eq!(ClkGpio::Gpio17.gpio_num(), 17);
376    }
377
378    #[test]
379    fn ana_conf_bits_no_overlap() {
380        assert_eq!(
381            ANA_CONF_PLLA_FORCE_PU & ANA_CONF_PLLA_FORCE_PD,
382            0,
383            "PU and PD bits must not overlap"
384        );
385    }
386
387    #[test]
388    fn ana_conf_bit_positions() {
389        // PD = bit 23, PU = bit 24
390        assert_eq!(ANA_CONF_PLLA_FORCE_PD, 1 << 23);
391        assert_eq!(ANA_CONF_PLLA_FORCE_PU, 1 << 24);
392    }
393
394    #[test]
395    fn ana_conf_register_address() {
396        assert_eq!(ANA_CONF_REG, 0x3FF4_8030);
397    }
398
399    #[test]
400    fn apll_constants() {
401        assert_eq!(I2C_APLL, 0x6D);
402        assert_eq!(I2C_APLL_HOSTID, 3);
403    }
404
405    #[test]
406    fn io_mux_base_consistent_with_ext_regs() {
407        assert_eq!(IO_MUX_BASE, crate::regs::ext::IO_MUX_BASE);
408    }
409
410    #[test]
411    fn gpio_register_layout() {
412        // GPIO_FUNC_OUT_SEL for GPIO0 should be at GPIO_BASE + 0x530
413        assert_eq!(GPIO_FUNC_OUT_SEL_BASE, 0x3FF4_4530);
414        // GPIO_ENABLE_W1TS should be at GPIO_BASE + 0x024
415        assert_eq!(GPIO_ENABLE_W1TS, 0x3FF4_4024);
416    }
417
418    #[test]
419    fn mcu_sel_mask_covers_function_5() {
420        // Function 5 = 0b101, fits in 3-bit MCU_SEL field at bits 14:12
421        let func5_shifted = 5u32 << 12;
422        assert_eq!(func5_shifted & MCU_SEL_MASK, func5_shifted);
423    }
424
425    #[test]
426    fn fun_drv_max_strength() {
427        // Max drive strength = 3, shifted to bits 11:10
428        let max_drv = 3u32 << 10;
429        assert_eq!(max_drv & FUN_DRV_MASK, max_drv);
430    }
431
432    // ── APLL coefficients ────────────────────────────────────────────────
433
434    /// Compute output frequency in MHz·Q16 fixed-point from APLL
435    /// coefficients, for a host-side sanity check that the table really
436    /// lands on 50 MHz. Matches the silicon formula:
437    ///   fout = fxtal * (sdm2 + sdm1/256 + sdm0/65536 + 4) / (2 * (o_div + 2))
438    fn fout_mhz_q16(c: ApllCoefficients, xtal_mhz: u32) -> u64 {
439        let num = (xtal_mhz as u64)
440            * (((c.sdm2 as u64 + 4) << 16) + (c.sdm1 as u64 * 256) + c.sdm0 as u64);
441        let denom = 2 * (c.o_div as u64 + 2);
442        num / denom
443    }
444
445    fn assert_50mhz(c: ApllCoefficients, xtal_mhz: u32) {
446        let q16 = fout_mhz_q16(c, xtal_mhz);
447        // 50 MHz in Q16: 50 << 16 = 3_276_800.
448        let target_q16 = 50u64 << 16;
449        // Allow ±0.001 MHz drift.
450        let drift = q16.abs_diff(target_q16);
451        assert!(
452            drift < 100,
453            "fout for {} MHz XTAL is {} (Q16) — drift {} from 50 MHz target",
454            xtal_mhz,
455            q16,
456            drift
457        );
458    }
459
460    #[test]
461    fn apll_coefficients_xtal_40_lands_on_50mhz() {
462        assert_50mhz(ApllCoefficients::for_xtal(XtalFreq::Mhz40), 40);
463    }
464
465    #[test]
466    fn apll_coefficients_xtal_32_lands_on_50mhz() {
467        assert_50mhz(ApllCoefficients::for_xtal(XtalFreq::Mhz32), 32);
468    }
469
470    #[test]
471    fn apll_coefficients_xtal_26_lands_on_50mhz() {
472        assert_50mhz(ApllCoefficients::for_xtal(XtalFreq::Mhz26), 26);
473    }
474
475    #[test]
476    fn apll_coefficients_register_field_widths() {
477        // o_div is a 5-bit field, sdm2 is 6-bit.
478        for xtal in [XtalFreq::Mhz26, XtalFreq::Mhz32, XtalFreq::Mhz40] {
479            let c = ApllCoefficients::for_xtal(xtal);
480            assert!(
481                c.o_div < 32,
482                "o_div for {:?} = {} doesn't fit 5 bits",
483                xtal,
484                c.o_div
485            );
486            assert!(
487                c.sdm2 < 64,
488                "sdm2 for {:?} = {} doesn't fit 6 bits",
489                xtal,
490                c.sdm2
491            );
492        }
493    }
494}