Skip to main content

esp_emac/
mdio.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//! MDIO (Management Data Input/Output) controller.
5//!
6//! Communicates with Ethernet PHY chips via the ESP32 EMAC's
7//! built-in SMI (Station Management Interface).
8
9use crate::error::EmacError;
10
11/// MDC clock divider based on system clock frequency.
12///
13/// The MDC clock must not exceed 2.5 MHz per IEEE 802.3.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[cfg_attr(feature = "defmt", derive(defmt::Format))]
16pub enum MdcClockDivider {
17    /// Clock/16 (20-35 MHz system clock)
18    Div16 = 2,
19    /// Clock/26 (35-60 MHz system clock)
20    Div26 = 3,
21    /// Clock/42 (60-100 MHz system clock) — ESP32 @ 80 MHz
22    Div42 = 0,
23    /// Clock/62 (100-150 MHz system clock)
24    Div62 = 1,
25    /// Clock/102 (150-250 MHz system clock)
26    Div102 = 4,
27    /// Clock/124 (250-300 MHz system clock)
28    Div124 = 5,
29}
30
31impl MdcClockDivider {
32    /// Select the appropriate divider for a given system clock frequency.
33    pub const fn from_sys_clock_hz(hz: u32) -> Self {
34        if hz < 35_000_000 {
35            Self::Div16
36        } else if hz < 60_000_000 {
37            Self::Div26
38        } else if hz < 100_000_000 {
39            Self::Div42
40        } else if hz < 150_000_000 {
41            Self::Div62
42        } else if hz < 250_000_000 {
43            Self::Div102
44        } else {
45            Self::Div124
46        }
47    }
48
49    /// Get the register value for this divider.
50    pub const fn reg_value(self) -> u32 {
51        self as u32
52    }
53}
54
55impl Default for MdcClockDivider {
56    /// Default: Div42 (correct for ESP32 @ 80 MHz).
57    fn default() -> Self {
58        Self::Div42
59    }
60}
61
62/// Maximum valid PHY address (5-bit field).
63pub const MAX_PHY_ADDR: u8 = 31;
64
65/// Maximum valid register address (5-bit field).
66pub const MAX_REG_ADDR: u8 = 31;
67
68/// MDIO timeout in polling iterations.
69const MDIO_TIMEOUT_ITERS: u32 = 1000;
70
71// ── EMAC MAC register offsets for MDIO ─────────────────────────────────────
72
73/// ESP32 MAC register base address.
74const MAC_BASE: usize = 0x3FF6_A000;
75
76/// GMACMIIADDR register offset from MAC_BASE.
77const GMACMIIADDR_OFFSET: usize = 0x10;
78/// GMACMIIDATA register offset from MAC_BASE.
79const GMACMIIDATA_OFFSET: usize = 0x14;
80
81// GMACMIIADDR bit fields
82const GMACMIIADDR_PA_SHIFT: u32 = 11;
83const GMACMIIADDR_PA_MASK: u32 = 0x1F << 11;
84const GMACMIIADDR_GR_SHIFT: u32 = 6;
85const GMACMIIADDR_GR_MASK: u32 = 0x1F << 6;
86const GMACMIIADDR_CR_SHIFT: u32 = 2;
87const GMACMIIADDR_CR_MASK: u32 = 0x0F << 2;
88const GMACMIIADDR_GW: u32 = 1 << 1;
89const GMACMIIADDR_GB: u32 = 1 << 0;
90
91/// ESP32 MDIO controller.
92///
93/// Provides read/write access to PHY registers via the EMAC's
94/// built-in SMI (Station Management Interface).
95///
96/// # Safety
97///
98/// This struct accesses memory-mapped EMAC registers directly.
99/// It must only be used on ESP32 with the EMAC peripheral clock enabled.
100pub struct EspMdio {
101    clock_divider: MdcClockDivider,
102}
103
104impl EspMdio {
105    /// Create a new MDIO controller with default clock divider (Div42 for 80 MHz).
106    pub fn new() -> Self {
107        Self {
108            clock_divider: MdcClockDivider::default(),
109        }
110    }
111
112    /// Create with a specific clock divider.
113    pub fn with_clock_divider(divider: MdcClockDivider) -> Self {
114        Self {
115            clock_divider: divider,
116        }
117    }
118
119    /// Read a PHY register via MDIO/SMI.
120    pub fn read(&mut self, phy_addr: u8, reg_addr: u8) -> Result<u16, EmacError> {
121        if phy_addr > MAX_PHY_ADDR {
122            return Err(EmacError::InvalidPhyAddress);
123        }
124        if reg_addr > MAX_REG_ADDR {
125            return Err(EmacError::InvalidConfig);
126        }
127
128        self.wait_not_busy()?;
129
130        let addr = self.build_mii_addr(phy_addr, reg_addr, false);
131        self.write_mii_addr(addr);
132
133        self.wait_not_busy()?;
134
135        Ok((self.read_mii_data() & 0xFFFF) as u16)
136    }
137
138    /// Write a PHY register via MDIO/SMI.
139    pub fn write(&mut self, phy_addr: u8, reg_addr: u8, value: u16) -> Result<(), EmacError> {
140        if phy_addr > MAX_PHY_ADDR {
141            return Err(EmacError::InvalidPhyAddress);
142        }
143        if reg_addr > MAX_REG_ADDR {
144            return Err(EmacError::InvalidConfig);
145        }
146
147        self.wait_not_busy()?;
148
149        self.write_mii_data(value as u32);
150
151        let addr = self.build_mii_addr(phy_addr, reg_addr, true);
152        self.write_mii_addr(addr);
153
154        self.wait_not_busy()
155    }
156
157    /// Build GMACMIIADDR register value.
158    ///
159    /// Public within crate for testing.
160    pub(crate) fn build_mii_addr(&self, phy_addr: u8, reg_addr: u8, is_write: bool) -> u32 {
161        let mut addr = 0u32;
162        addr |= ((phy_addr as u32) << GMACMIIADDR_PA_SHIFT) & GMACMIIADDR_PA_MASK;
163        addr |= ((reg_addr as u32) << GMACMIIADDR_GR_SHIFT) & GMACMIIADDR_GR_MASK;
164        addr |= (self.clock_divider.reg_value() << GMACMIIADDR_CR_SHIFT) & GMACMIIADDR_CR_MASK;
165        if is_write {
166            addr |= GMACMIIADDR_GW;
167        }
168        addr |= GMACMIIADDR_GB;
169        addr
170    }
171
172    /// Wait for MDIO operation to complete (busy bit cleared).
173    fn wait_not_busy(&self) -> Result<(), EmacError> {
174        for _ in 0..MDIO_TIMEOUT_ITERS {
175            if self.read_mii_addr() & GMACMIIADDR_GB == 0 {
176                return Ok(());
177            }
178        }
179        Err(EmacError::Timeout)
180    }
181
182    // ── Hardware register access (ESP32 only) ────────────────────────────
183
184    #[inline(always)]
185    fn read_mii_addr(&self) -> u32 {
186        unsafe { core::ptr::read_volatile((MAC_BASE + GMACMIIADDR_OFFSET) as *const u32) }
187    }
188
189    #[inline(always)]
190    fn write_mii_addr(&self, val: u32) {
191        unsafe { core::ptr::write_volatile((MAC_BASE + GMACMIIADDR_OFFSET) as *mut u32, val) }
192    }
193
194    #[inline(always)]
195    fn read_mii_data(&self) -> u32 {
196        unsafe { core::ptr::read_volatile((MAC_BASE + GMACMIIDATA_OFFSET) as *const u32) }
197    }
198
199    #[inline(always)]
200    fn write_mii_data(&self, val: u32) {
201        unsafe { core::ptr::write_volatile((MAC_BASE + GMACMIIDATA_OFFSET) as *mut u32, val) }
202    }
203}
204
205impl Default for EspMdio {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211/// MdioBus trait implementation — only with "mdio-phy" feature.
212/// Delegates to the standalone read()/write() methods.
213#[cfg(feature = "mdio-phy")]
214impl eth_mdio_phy::MdioBus for EspMdio {
215    type Error = EmacError;
216
217    fn read(&mut self, phy_addr: u8, reg_addr: u8) -> Result<u16, EmacError> {
218        EspMdio::read(self, phy_addr, reg_addr)
219    }
220
221    fn write(&mut self, phy_addr: u8, reg_addr: u8, value: u16) -> Result<(), EmacError> {
222        EspMdio::write(self, phy_addr, reg_addr, value)
223    }
224}
225
226// ── Tests (pure-logic only, no hardware register access) ─────────────────
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    // ── MdcClockDivider ──────────────────────────────────────────────────
233
234    #[test]
235    fn mdc_divider_from_sys_clock() {
236        assert_eq!(
237            MdcClockDivider::from_sys_clock_hz(20_000_000),
238            MdcClockDivider::Div16
239        );
240        assert_eq!(
241            MdcClockDivider::from_sys_clock_hz(40_000_000),
242            MdcClockDivider::Div26
243        );
244        assert_eq!(
245            MdcClockDivider::from_sys_clock_hz(80_000_000),
246            MdcClockDivider::Div42
247        );
248        assert_eq!(
249            MdcClockDivider::from_sys_clock_hz(120_000_000),
250            MdcClockDivider::Div62
251        );
252        assert_eq!(
253            MdcClockDivider::from_sys_clock_hz(160_000_000),
254            MdcClockDivider::Div102
255        );
256        assert_eq!(
257            MdcClockDivider::from_sys_clock_hz(280_000_000),
258            MdcClockDivider::Div124
259        );
260    }
261
262    #[test]
263    fn mdc_divider_default_is_div42() {
264        assert_eq!(MdcClockDivider::default(), MdcClockDivider::Div42);
265    }
266
267    #[test]
268    fn mdc_divider_reg_values() {
269        assert_eq!(MdcClockDivider::Div42.reg_value(), 0);
270        assert_eq!(MdcClockDivider::Div62.reg_value(), 1);
271        assert_eq!(MdcClockDivider::Div16.reg_value(), 2);
272        assert_eq!(MdcClockDivider::Div26.reg_value(), 3);
273        assert_eq!(MdcClockDivider::Div102.reg_value(), 4);
274        assert_eq!(MdcClockDivider::Div124.reg_value(), 5);
275    }
276
277    #[test]
278    fn mdc_divider_boundary_35mhz() {
279        assert_eq!(
280            MdcClockDivider::from_sys_clock_hz(34_999_999),
281            MdcClockDivider::Div16
282        );
283        assert_eq!(
284            MdcClockDivider::from_sys_clock_hz(35_000_000),
285            MdcClockDivider::Div26
286        );
287    }
288
289    #[test]
290    fn mdc_divider_boundary_60mhz() {
291        assert_eq!(
292            MdcClockDivider::from_sys_clock_hz(59_999_999),
293            MdcClockDivider::Div26
294        );
295        assert_eq!(
296            MdcClockDivider::from_sys_clock_hz(60_000_000),
297            MdcClockDivider::Div42
298        );
299    }
300
301    #[test]
302    fn mdc_divider_boundary_100mhz() {
303        assert_eq!(
304            MdcClockDivider::from_sys_clock_hz(99_999_999),
305            MdcClockDivider::Div42
306        );
307        assert_eq!(
308            MdcClockDivider::from_sys_clock_hz(100_000_000),
309            MdcClockDivider::Div62
310        );
311    }
312
313    #[test]
314    fn mdc_divider_boundary_150mhz() {
315        assert_eq!(
316            MdcClockDivider::from_sys_clock_hz(149_999_999),
317            MdcClockDivider::Div62
318        );
319        assert_eq!(
320            MdcClockDivider::from_sys_clock_hz(150_000_000),
321            MdcClockDivider::Div102
322        );
323    }
324
325    #[test]
326    fn mdc_divider_boundary_250mhz() {
327        assert_eq!(
328            MdcClockDivider::from_sys_clock_hz(249_999_999),
329            MdcClockDivider::Div102
330        );
331        assert_eq!(
332            MdcClockDivider::from_sys_clock_hz(250_000_000),
333            MdcClockDivider::Div124
334        );
335    }
336
337    // ── build_mii_addr ───────────────────────────────────────────────────
338
339    #[test]
340    fn build_mii_addr_read() {
341        let mdio = EspMdio::new(); // Div42 = 0
342        let addr = mdio.build_mii_addr(1, 0, false);
343        // PHY addr 1 → bits[15:11] = 1<<11 = 0x0800
344        // Reg addr 0 → bits[10:6] = 0
345        // CR = 0 (Div42) → bits[5:2] = 0
346        // GW = 0 (read)
347        // GB = 1 (trigger)
348        assert_eq!(addr, 0x0800 | GMACMIIADDR_GB);
349    }
350
351    #[test]
352    fn build_mii_addr_write() {
353        let mdio = EspMdio::new();
354        let addr = mdio.build_mii_addr(1, 0, true);
355        // Same as read + GW bit
356        assert_eq!(addr, 0x0800 | GMACMIIADDR_GW | GMACMIIADDR_GB);
357    }
358
359    #[test]
360    fn build_mii_addr_phy_and_reg() {
361        let mdio = EspMdio::new();
362        let addr = mdio.build_mii_addr(31, 31, false);
363        // PHY addr 31 → 31<<11 = 0xF800
364        // Reg addr 31 → 31<<6 = 0x07C0
365        assert_eq!(addr & GMACMIIADDR_PA_MASK, 31 << 11);
366        assert_eq!(addr & GMACMIIADDR_GR_MASK, 31 << 6);
367    }
368
369    #[test]
370    fn build_mii_addr_clock_divider() {
371        let mdio = EspMdio::with_clock_divider(MdcClockDivider::Div102);
372        let addr = mdio.build_mii_addr(0, 0, false);
373        // Div102 reg_value = 4 → bits[5:2] = 4<<2 = 0x10
374        let cr_field = (addr & GMACMIIADDR_CR_MASK) >> GMACMIIADDR_CR_SHIFT;
375        assert_eq!(cr_field, 4);
376    }
377
378    // ── Validation ───────────────────────────────────────────────────────
379    // NOTE: read()/write() can't be tested on host (hardware registers).
380    // We test validation logic that runs before hardware access.
381
382    #[test]
383    fn max_phy_addr_constant() {
384        assert_eq!(MAX_PHY_ADDR, 31);
385    }
386
387    #[test]
388    fn max_reg_addr_constant() {
389        assert_eq!(MAX_REG_ADDR, 31);
390    }
391}