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}