Skip to main content

eth_phy_lan867x/
lib.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//! `#![no_std]` MDIO driver for the Microchip LAN867x family of
5//! 10BASE-T1S Ethernet PHYs (IEEE 802.3cg-2019 Clause 147):
6//!
7//! - LAN8670 (32-VQFN, MII or RMII)
8//! - LAN8671 (24-VQFN, RMII only) — JetHome hardware target
9//! - LAN8672 (36-VQFN, MII only)
10//!
11//! Implements [`eth_mdio_phy::PhyDriver`], so any MAC that exposes
12//! [`eth_mdio_phy::MdioBus`] can drive the chip.
13//!
14//! 10BASE-T1S is single-pair, half-duplex, multidrop Ethernet — quite
15//! different from the point-to-point 10/100BASE-T flavours covered by
16//! [`eth-phy-lan87xx`](https://docs.rs/eth-phy-lan87xx). Notably:
17//!
18//! - There is no auto-negotiation (`BMCR.AUTO_NEG_EN` is hard-wired 0).
19//! - `BMSR.LINK_STATUS` is hard-wired 1 — useless for link detection.
20//!   Use [`PhyLan867x::poll_link`], which reads `PLCA_STS.PST` when PLCA
21//!   is enabled.
22//! - Most operational state lives in MMD-31 (Vendor Specific 2),
23//!   accessed via the IEEE Annex 22D MMDCTRL/MMDAD indirection.
24//! - The chip ships in a multidrop-disabled state — driver `init()`
25//!   sets `T1SPMACTL.MDE = 1`.
26//!
27//! Reference datasheet: Microchip DS60001573C (silicon revision 2,
28//! product revision B1).
29
30#![no_std]
31
32mod mmd;
33mod regs;
34
35pub mod plca;
36
37pub use plca::{PlcaConfig, PlcaError, PlcaStatus};
38
39use eth_mdio_phy::{
40    ieee802_3, Duplex, LinkStatus, MdioBus, PhyCapabilities, PhyDriver, PhyError, Speed,
41};
42
43/// Concrete LAN867x family member, identified at [`PhyLan867x::init`]
44/// time from `STRAP_CTRL0.PKGTYP`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[cfg_attr(feature = "defmt", derive(defmt::Format))]
47pub enum Chip {
48    /// LAN8670 — 32-VQFN, supports both MII and RMII.
49    Lan8670,
50    /// LAN8671 — 24-VQFN, RMII only. JetHome hardware target.
51    Lan8671,
52    /// LAN8672 — 36-VQFN, MII only.
53    Lan8672,
54}
55
56/// LAN867x PHY driver (software-only, no reset pin).
57///
58/// For a variant with a hardware reset pin, see [`PhyLan867xWithReset`].
59pub struct PhyLan867x {
60    addr: u8,
61    chip: Option<Chip>,
62    /// `Some(id)` when [`PhyLan867x::configure_plca`] has succeeded;
63    /// `None` otherwise. `poll_link` uses this to decide whether to gate
64    /// link status on `PLCA_STS.PST` or always report linked.
65    pub(crate) plca_id: Option<u8>,
66}
67
68impl PhyLan867x {
69    /// Create a new driver bound to the given MDIO/SMI address.
70    ///
71    /// Address discovery: `STRAP_CTRL0.SMIADR` reflects the strap-pin
72    /// state; on JXD-CPU-E1T1S all PHYAD pins are pulled low ⇒ addr = 0.
73    pub fn new(addr: u8) -> Self {
74        Self {
75            addr,
76            chip: None,
77            plca_id: None,
78        }
79    }
80
81    /// Concrete chip discovered at `init()`. `None` until `init()` runs.
82    pub fn chip(&self) -> Option<Chip> {
83        self.chip
84    }
85}
86
87impl PhyDriver for PhyLan867x {
88    fn phy_addr(&self) -> u8 {
89        self.addr
90    }
91
92    fn init<M: MdioBus>(&mut self, mdio: &mut M) -> Result<(), PhyError<M::Error>> {
93        // 0. Drop cached driver state. `poll_link` uses `chip.is_some()`
94        //    as the "init has succeeded" gate, so we must not leave a
95        //    stale `Some(_)` from a previous successful init in case
96        //    THIS init fails partway through. Soft reset (step 1) also
97        //    wipes the chip's PLCA_CTRL0/CTRL1 to defaults, so the
98        //    driver's PLCA cache must follow. Single-owner contract:
99        //    this driver is the sole writer to the chip's registers, so
100        //    we don't need to read PLCA state back from silicon.
101        self.plca_id = None;
102        self.chip = None;
103
104        // 1. Software reset (BMCR.SW_RESET, self-clearing). Bounded poll —
105        //    matches the lan87xx driver's allowance.
106        let cleared = ieee802_3::soft_reset(mdio, self.addr, 500).map_err(PhyError::Mdio)?;
107        if !cleared {
108            return Err(PhyError::ResetTimeout);
109        }
110
111        // 2. Reset-complete handshake (DS60001573C sec 4.7).
112        //
113        //    After any reset (including the soft reset above), the chip
114        //    holds IRQ_N low until the host reads STS2 in MMD-31. Until
115        //    that read, register writes from this point onward are NOT
116        //    guaranteed to take effect — the device is still completing
117        //    its internal initialisation. Poll STS2.RESETC; the read is
118        //    what clears the bit and releases IRQ_N.
119        let mut got_resetc = false;
120        for _ in 0..500 {
121            let sts2 = mmd::mmd_read(mdio, self.addr, regs::MMD_VS2, regs::MMD_REG_STS2)
122                .map_err(PhyError::Mdio)?;
123            if sts2 & regs::STS2_RESETC != 0 {
124                got_resetc = true;
125                break;
126            }
127        }
128        if !got_resetc {
129            return Err(PhyError::ResetTimeout);
130        }
131
132        // 3. Verify family identity from PHY_ID0 / PHY_ID1.
133        //
134        //    Mask out the silicon-revision nibble — the driver supports
135        //    every revision Microchip has shipped to date (Rev 0 and
136        //    Rev 2). If a future revision changes register semantics,
137        //    add silicon-rev branching here.
138        let id = ieee802_3::read_phy_id(mdio, self.addr).map_err(PhyError::Mdio)?;
139        if id & regs::PHY_OUI_MODEL_MASK != regs::PHY_OUI_MODEL_LAN867X {
140            return Err(PhyError::UnsupportedChip { id });
141        }
142
143        // 4. Discriminate the concrete package from STRAP_CTRL0.PKGTYP.
144        //    The strap is latched at hardware reset and survives soft
145        //    reset (NASR), so reading it after step 1 is safe.
146        //
147        //    Hold the discovered chip in a local — `self.chip` is only
148        //    written at the very end so that any later step's failure
149        //    leaves the driver in the "uninitialised" state and the
150        //    `poll_link` gate stays honest.
151        let strap = mdio
152            .read(self.addr, regs::REG_STRAP_CTRL0)
153            .map_err(PhyError::Mdio)?;
154        let chip = match strap & regs::STRAP_CTRL0_PKGTYP_MASK {
155            regs::STRAP_CTRL0_PKGTYP_LAN8670 => Chip::Lan8670,
156            regs::STRAP_CTRL0_PKGTYP_LAN8671 => Chip::Lan8671,
157            regs::STRAP_CTRL0_PKGTYP_LAN8672 => Chip::Lan8672,
158            _ => {
159                // PHY ID matched LAN867x family but PKGTYP is not one
160                // of the three documented packages. Surface the strap
161                // value rather than the PHY ID so the caller doesn't
162                // mistakenly conclude the chip is unsupported.
163                return Err(PhyError::UnsupportedPackage {
164                    strap: u32::from(strap),
165                });
166            }
167        };
168
169        // 5. Sanity-probe the OPEN Alliance map identifier in MMD-31 —
170        //    confirms the indirection sequence is functional and that
171        //    we're looking at an OPEN Alliance T1S PHY (and not, say,
172        //    the wrong vendor of LAN867x clone).
173        let midver = mmd::mmd_read(mdio, self.addr, regs::MMD_VS2, regs::MMD_REG_MIDVER)
174            .map_err(PhyError::Mdio)?;
175        if midver != regs::MIDVER_EXPECTED {
176            return Err(PhyError::UnsupportedChip { id });
177        }
178
179        // 6. Multidrop enable — required for any > 2-node bus, which is
180        //    the topology JetHome boards are designed for. Use RMW to
181        //    preserve any other bits the silicon may have come up with.
182        mmd::mmd_rmw(
183            mdio,
184            self.addr,
185            regs::MMD_PMA_PMD,
186            regs::MMD_REG_T1SPMACTL,
187            0,
188            regs::T1SPMACTL_MDE,
189        )
190        .map_err(PhyError::Mdio)?;
191
192        // 7. Commit. Every fallible step has succeeded; `self.chip =
193        //    Some(_)` from this point onward truthfully signals to
194        //    `poll_link` that the chip is in the post-init multidrop-
195        //    ready state.
196        self.chip = Some(chip);
197
198        Ok(())
199    }
200
201    fn poll_link<M: MdioBus>(
202        &mut self,
203        mdio: &mut M,
204    ) -> Result<Option<LinkStatus>, PhyError<M::Error>> {
205        // On a 10BASE-T1S multidrop bus there is no autonegotiation and
206        // no per-link-partner signal. Three distinct cases:
207        //
208        // - Driver not yet `init`-ed: report None. The chip has neither
209        //   completed its RESETC handshake nor had `T1SPMACTL.MDE` set,
210        //   so we have no business claiming a working link. Used as the
211        //   gate: `self.chip` is populated only by `init()` and never
212        //   reset, so `chip.is_some()` ≡ "init has succeeded".
213        //
214        // - PLCA off (CSMA/CD), post-init: the bus is "always there".
215        //   Report linked — the caller can attempt to send and the chip
216        //   will handle (possibly colliding) transmissions.
217        //
218        // - PLCA on: PLCA_STS.PST tracks whether BEACONs are being TX'd
219        //   (coordinator) or RX'd (follower). It is the only meaningful
220        //   "are we participating in the network" indicator. Report
221        //   linked when set; report None until the bus stabilises.
222        //
223        // PLCA mode is selected by `self.plca_id`, which `configure_plca`
224        // sets and `disable_plca` / `init` clear. Single-owner contract:
225        // this driver is assumed to be the sole writer to the chip's
226        // registers, so the driver-side flag is authoritative. If
227        // someone else flips `PLCA_CTRL0.EN` directly via MDIO between
228        // our `configure_plca` and a `poll_link`, we will not notice.
229        match (self.chip, self.plca_id) {
230            (None, _) => Ok(None),
231            (Some(_), None) => Ok(Some(LinkStatus::new(Speed::Mbps10, Duplex::Half))),
232            (Some(_), Some(_)) => {
233                let sts = mmd::mmd_read(mdio, self.addr, regs::MMD_VS2, regs::MMD_REG_PLCA_STS)
234                    .map_err(PhyError::Mdio)?;
235                if sts & regs::PLCA_STS_PST != 0 {
236                    Ok(Some(LinkStatus::new(Speed::Mbps10, Duplex::Half)))
237                } else {
238                    Ok(None)
239                }
240            }
241        }
242    }
243
244    fn capabilities<M: MdioBus>(
245        &self,
246        mdio: &mut M,
247    ) -> Result<PhyCapabilities, PhyError<M::Error>> {
248        // BASIC_STATUS reports only the 10BASE-T half-duplex bit on this
249        // chip — `read_capabilities` decodes it correctly. The other
250        // ability bits are hard-wired 0, which `PhyCapabilities`
251        // already reflects via its boolean fields.
252        ieee802_3::read_capabilities(mdio, self.addr).map_err(PhyError::Mdio)
253    }
254
255    fn phy_id<M: MdioBus>(&self, mdio: &mut M) -> Result<u32, PhyError<M::Error>> {
256        ieee802_3::read_phy_id(mdio, self.addr).map_err(PhyError::Mdio)
257    }
258}
259
260// ── PLCA configuration / introspection (chip-specific, not in PhyDriver) ─
261
262impl PhyLan867x {
263    /// Configure and enable PLCA on this node.
264    ///
265    /// Must be called after [`PhyLan867x::init`] — `init` only puts the
266    /// chip in CSMA/CD multidrop mode, with PLCA off.
267    ///
268    /// Validation:
269    ///
270    /// - `node_id == 0xFF` is rejected (silicon sentinel for "disabled");
271    ///   use [`PhyLan867x::disable_plca`] instead.
272    /// - For followers (`node_id != 0`) with a non-zero `node_count`,
273    ///   `node_id < node_count` must hold; otherwise this node would
274    ///   never be granted a transmit opportunity.
275    ///
276    /// # Failure semantics — NOT transactional
277    ///
278    /// The wire protocol is three sequential MDIO writes — `PLCA_CTRL1`
279    /// (NCNT/ID), `PLCA_BURST` (MAXBC/BTMR), `PLCA_CTRL0.EN` (RMW). An
280    /// MDIO bus error after one of those writes succeeds leaves the
281    /// chip in a partially configured state and the driver-side
282    /// `plca_id` cache out of sync with the silicon:
283    ///
284    /// - **Step 1 (CTRL1) fails:** nothing was written. Silicon
285    ///   keeps its previous `node_id`/`node_count` and prior
286    ///   `CTRL0.EN`; if PLCA was running, it keeps running with
287    ///   the prior parameters. `plca_id` is unchanged.
288    /// - **Step 2 (BURST) fails:** silicon's CTRL1 already holds
289    ///   the *new* `node_id`/`node_count`, but BURST and EN are
290    ///   prior values. If `CTRL0.EN` was already 1 from a prior
291    ///   successful `configure_plca`, PLCA now runs with the **new**
292    ///   `node_id` and **old** burst settings; `plca_id` is still the
293    ///   driver-side cache of the *old* configure call. `plca_status`
294    ///   will report the silicon-truth `node_id` (new), which won't
295    ///   match `plca_id` if the caller has stashed it.
296    /// - **Step 3 (CTRL0.EN RMW) fails:** silicon holds the new CTRL1
297    ///   and BURST; EN still carries the prior state. If EN was 1,
298    ///   PLCA now runs with the **new** CTRL1/BURST while `plca_id`
299    ///   is still the old cache. If EN was 0, PLCA stays disabled.
300    ///   In both cases `poll_link` reads `PLCA_STS.PST` correctly
301    ///   from silicon, but `plca_id` and the chip's actual `node_id`
302    ///   diverge.
303    ///
304    /// Recovery: retry `configure_plca` with the same parameters, or
305    /// call [`PhyLan867x::init`] to reset the chip and the driver
306    /// state together.
307    ///
308    /// A future release may add transactional semantics (write-then-
309    /// readback or rollback-on-error) once the broader 10BASE-T1S +
310    /// PLCA architectural plan in `docs/plans/eth-phy-lan867x-plca.md`
311    /// (in the parent repository) settles the runtime-toggling story.
312    pub fn configure_plca<M: MdioBus>(
313        &mut self,
314        mdio: &mut M,
315        config: &PlcaConfig,
316    ) -> Result<(), PlcaError<M::Error>> {
317        if config.node_id == regs::PLCA_ID_DISABLED {
318            return Err(PlcaError::InvalidConfig);
319        }
320        if config.node_count != 0 && config.node_id >= config.node_count {
321            return Err(PlcaError::InvalidConfig);
322        }
323
324        // PLCA_CTRL1 = NCNT[15:8] | ID[7:0]
325        let ctrl1 = (u16::from(config.node_count) << regs::PLCA_CTRL1_NCNT_SHIFT)
326            | u16::from(config.node_id);
327        mmd::mmd_write(
328            mdio,
329            self.addr,
330            regs::MMD_VS2,
331            regs::MMD_REG_PLCA_CTRL1,
332            ctrl1,
333        )
334        .map_err(PlcaError::Mdio)?;
335
336        // Always program PLCA_BURST so a re-configuration with
337        // `burst_count = 0` reliably clears MAXBC, undoing any prior
338        // configure_plca that enabled bursting. Per datasheet sec
339        // 5.4.18, MAXBC = 0 is the explicit "burst disabled" encoding.
340        // burst_timer = 0 in PlcaConfig is a sentinel that means "leave
341        // chip default of 0x80" — interpret it here so users don't
342        // accidentally write BTMR = 0 (which would make burst mode
343        // non-functional even when MAXBC > 0).
344        let btmr = if config.burst_timer == 0 {
345            0x80_u16
346        } else {
347            u16::from(config.burst_timer)
348        };
349        let burst = (u16::from(config.burst_count) << 8) | btmr;
350        mmd::mmd_write(
351            mdio,
352            self.addr,
353            regs::MMD_VS2,
354            regs::MMD_REG_PLCA_BURST,
355            burst,
356        )
357        .map_err(PlcaError::Mdio)?;
358
359        // Flip the enable bit last, after CTRL1 is in place. RMW preserves
360        // any other bits that future silicon revisions might define.
361        mmd::mmd_rmw(
362            mdio,
363            self.addr,
364            regs::MMD_VS2,
365            regs::MMD_REG_PLCA_CTRL0,
366            0,
367            regs::PLCA_CTRL0_EN,
368        )
369        .map_err(PlcaError::Mdio)?;
370
371        self.plca_id = Some(config.node_id);
372        Ok(())
373    }
374
375    /// Disable PLCA — chip falls back to CSMA/CD on the segment.
376    pub fn disable_plca<M: MdioBus>(&mut self, mdio: &mut M) -> Result<(), PlcaError<M::Error>> {
377        mmd::mmd_rmw(
378            mdio,
379            self.addr,
380            regs::MMD_VS2,
381            regs::MMD_REG_PLCA_CTRL0,
382            regs::PLCA_CTRL0_EN,
383            0,
384        )
385        .map_err(PlcaError::Mdio)?;
386        self.plca_id = None;
387        Ok(())
388    }
389
390    /// Snapshot the chip's PLCA registers.
391    ///
392    /// Returns a chip-truth view: even if `configure_plca` was never
393    /// called on this driver instance (e.g. a different host configured
394    /// the chip earlier), the result reflects what the silicon currently
395    /// reports.
396    pub fn plca_status<M: MdioBus>(&self, mdio: &mut M) -> Result<PlcaStatus, PlcaError<M::Error>> {
397        let ctrl0 = mmd::mmd_read(mdio, self.addr, regs::MMD_VS2, regs::MMD_REG_PLCA_CTRL0)
398            .map_err(PlcaError::Mdio)?;
399        let ctrl1 = mmd::mmd_read(mdio, self.addr, regs::MMD_VS2, regs::MMD_REG_PLCA_CTRL1)
400            .map_err(PlcaError::Mdio)?;
401        let sts = mmd::mmd_read(mdio, self.addr, regs::MMD_VS2, regs::MMD_REG_PLCA_STS)
402            .map_err(PlcaError::Mdio)?;
403
404        let id = (ctrl1 & regs::PLCA_CTRL1_ID_MASK) as u8;
405        Ok(PlcaStatus {
406            enabled: ctrl0 & regs::PLCA_CTRL0_EN != 0,
407            node_id: id,
408            is_coordinator: id == regs::PLCA_ID_COORDINATOR,
409            stable: sts & regs::PLCA_STS_PST != 0,
410        })
411    }
412}
413
414/// LAN867x PHY driver with a hardware reset pin.
415///
416/// Wraps [`PhyLan867x`] and adds [`hardware_reset`](Self::hardware_reset)
417/// for toggling the PHY `RESET_N` line before `init()`. On JetHome
418/// JXD-CPU-E1T1S the reset pin is wired to ESP32 GPIO17.
419pub struct PhyLan867xWithReset<P: embedded_hal::digital::OutputPin> {
420    inner: PhyLan867x,
421    reset_pin: P,
422}
423
424impl<P: embedded_hal::digital::OutputPin> PhyLan867xWithReset<P> {
425    /// Create a new driver with the given MDIO address and reset pin.
426    pub fn new(addr: u8, pin: P) -> Self {
427        Self {
428            inner: PhyLan867x::new(addr),
429            reset_pin: pin,
430        }
431    }
432
433    /// Drive `RESET_N` low for 10 ms, then wait 25 ms after release
434    /// before MDIO is touched.
435    ///
436    /// The 10 ms low-pulse is conservative — datasheet sec 7.6.4 allows
437    /// shorter — and is intentionally longer than the 2 ms used by
438    /// `eth-phy-lan87xx`'s wrapper, since LAN867x boards (e.g. JXD-CPU-
439    /// E1T1S) drive `RESET_N` from a slow GPIO that may have noticeable
440    /// rise/fall times. The 25 ms post-release delay matches the
441    /// lan87xx wrapper.
442    pub fn hardware_reset<D: embedded_hal::delay::DelayNs>(
443        &mut self,
444        delay: &mut D,
445    ) -> Result<(), P::Error> {
446        self.reset_pin.set_low()?;
447        delay.delay_ms(10);
448        self.reset_pin.set_high()?;
449        delay.delay_ms(25);
450        Ok(())
451    }
452
453    /// Borrow the inner [`PhyLan867x`] for chip-specific operations
454    /// (e.g. PLCA configuration) that aren't part of [`PhyDriver`].
455    pub fn inner_mut(&mut self) -> &mut PhyLan867x {
456        &mut self.inner
457    }
458}
459
460impl<P: embedded_hal::digital::OutputPin> PhyDriver for PhyLan867xWithReset<P> {
461    fn phy_addr(&self) -> u8 {
462        self.inner.phy_addr()
463    }
464
465    fn init<M: MdioBus>(&mut self, mdio: &mut M) -> Result<(), PhyError<M::Error>> {
466        self.inner.init(mdio)
467    }
468
469    fn poll_link<M: MdioBus>(
470        &mut self,
471        mdio: &mut M,
472    ) -> Result<Option<LinkStatus>, PhyError<M::Error>> {
473        self.inner.poll_link(mdio)
474    }
475
476    fn capabilities<M: MdioBus>(
477        &self,
478        mdio: &mut M,
479    ) -> Result<PhyCapabilities, PhyError<M::Error>> {
480        self.inner.capabilities(mdio)
481    }
482
483    fn phy_id<M: MdioBus>(&self, mdio: &mut M) -> Result<u32, PhyError<M::Error>> {
484        self.inner.phy_id(mdio)
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    extern crate alloc;
491
492    use super::*;
493    use alloc::vec;
494    use alloc::vec::Vec;
495    use eth_mdio_phy::ieee802_3::bmcr;
496
497    // ── Mock MDIO bus ──────────────────────────────────────────────────
498
499    #[derive(Debug, PartialEq)]
500    struct MockError;
501
502    struct MockMdio {
503        reads: Vec<u16>,
504        read_idx: usize,
505        writes: Vec<(u8, u8, u16)>,
506        fail_at: Option<usize>,
507        call_count: usize,
508    }
509
510    impl MockMdio {
511        fn new(reads: Vec<u16>) -> Self {
512            Self {
513                reads,
514                read_idx: 0,
515                writes: Vec::new(),
516                fail_at: None,
517                call_count: 0,
518            }
519        }
520
521        fn with_failure(reads: Vec<u16>, fail_at: usize) -> Self {
522            Self {
523                reads,
524                read_idx: 0,
525                writes: Vec::new(),
526                fail_at: Some(fail_at),
527                call_count: 0,
528            }
529        }
530    }
531
532    impl MdioBus for MockMdio {
533        type Error = MockError;
534
535        fn read(&mut self, _phy_addr: u8, _reg_addr: u8) -> Result<u16, Self::Error> {
536            if self.fail_at == Some(self.call_count) {
537                self.call_count += 1;
538                return Err(MockError);
539            }
540            self.call_count += 1;
541            let v = *self
542                .reads
543                .get(self.read_idx)
544                .expect("MockMdio: reads vector exhausted");
545            self.read_idx += 1;
546            Ok(v)
547        }
548
549        fn write(&mut self, phy: u8, reg: u8, value: u16) -> Result<(), Self::Error> {
550            if self.fail_at == Some(self.call_count) {
551                self.call_count += 1;
552                return Err(MockError);
553            }
554            self.call_count += 1;
555            self.writes.push((phy, reg, value));
556            Ok(())
557        }
558    }
559
560    // PHY_ID readout helpers — silicon Rev 2 (B1) for each package variant.
561    const PHY_ID0_LAN867X: u16 = 0x0000;
562    const PHY_ID1_LAN867X_REV2: u16 = 0xC562;
563
564    /// Successful-init read sequence shared across happy-path tests.
565    fn reads_for_successful_init(strap_pkgtyp: u16) -> Vec<u16> {
566        vec![
567            // (1) BMCR poll inside soft_reset — SW_RESET cleared.
568            0x0000,
569            // (2) MMDAD read → STS2 with RESETC asserted.
570            regs::STS2_RESETC,
571            // (3) PHY_ID0 → 0x0000
572            PHY_ID0_LAN867X,
573            // (4) PHY_ID1 → silicon-rev-2 LAN867x family.
574            PHY_ID1_LAN867X_REV2,
575            // (5) STRAP_CTRL0 → caller-supplied PKGTYP encoding.
576            strap_pkgtyp,
577            // (6) MMDAD read → MIDVER, must be 0x0A10.
578            regs::MIDVER_EXPECTED,
579            // (7) MMDAD read → T1SPMACTL pre-RMW: chip default 0.
580            0x0000,
581        ]
582    }
583
584    // ── Constructor tests ──────────────────────────────────────────────
585
586    #[test]
587    fn new_sets_address_and_no_chip() {
588        let phy = PhyLan867x::new(7);
589        assert_eq!(phy.phy_addr(), 7);
590        assert_eq!(phy.chip(), None);
591    }
592
593    // ── init() tests ───────────────────────────────────────────────────
594
595    #[test]
596    fn init_success_lan8671_jethome_target() {
597        let mut mdio = MockMdio::new(reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8671));
598        let mut phy = PhyLan867x::new(0);
599        phy.init(&mut mdio).unwrap();
600        assert_eq!(phy.chip(), Some(Chip::Lan8671));
601    }
602
603    #[test]
604    fn init_success_lan8670() {
605        let mut mdio = MockMdio::new(reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8670));
606        let mut phy = PhyLan867x::new(0);
607        phy.init(&mut mdio).unwrap();
608        assert_eq!(phy.chip(), Some(Chip::Lan8670));
609    }
610
611    #[test]
612    fn init_success_lan8672() {
613        let mut mdio = MockMdio::new(reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8672));
614        let mut phy = PhyLan867x::new(0);
615        phy.init(&mut mdio).unwrap();
616        assert_eq!(phy.chip(), Some(Chip::Lan8672));
617    }
618
619    #[test]
620    fn init_rejects_invalid_pkgtyp() {
621        // PKGTYP = 00b is "Undefined" per datasheet sec 5.1.7.
622        let mut mdio = MockMdio::new(vec![
623            0x0000,
624            regs::STS2_RESETC,
625            PHY_ID0_LAN867X,
626            PHY_ID1_LAN867X_REV2,
627            0x0000, // STRAP_CTRL0 with PKGTYP = 00b
628        ]);
629        let mut phy = PhyLan867x::new(0);
630        let err = phy.init(&mut mdio).unwrap_err();
631        // Distinct from UnsupportedChip: the PHY ID matched correctly,
632        // only the package strap is unrecognised.
633        match err {
634            PhyError::UnsupportedPackage { strap } => assert_eq!(strap, 0x0000),
635            other => panic!("expected UnsupportedPackage, got {other:?}"),
636        }
637    }
638
639    #[test]
640    fn init_writes_t1spmactl_mde_bit() {
641        let mut mdio = MockMdio::new(reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8671));
642        let mut phy = PhyLan867x::new(0);
643        phy.init(&mut mdio).unwrap();
644
645        // Find the data-write to MMDAD at the end of the T1SPMACTL RMW.
646        // The pre-RMW read returned 0x0000 → final write must be exactly
647        // T1SPMACTL_MDE with no other bits set.
648        let last_mmdad_data_write = mdio
649            .writes
650            .iter()
651            .rev()
652            .find(|&&(_, reg, _)| reg == regs::REG_MMDAD)
653            .expect("expected an MMDAD data write");
654        assert_eq!(
655            last_mmdad_data_write.2,
656            regs::T1SPMACTL_MDE,
657            "init must set T1SPMACTL.MDE = 1 (multidrop enable)"
658        );
659    }
660
661    #[test]
662    fn init_t1spmactl_rmw_preserves_other_bits() {
663        // Same flow but the chip's pre-RMW T1SPMACTL has TXD set.
664        // After init, the data-write must keep TXD AND set MDE.
665        let mut reads = reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8671);
666        // Replace position 6 (the T1SPMACTL pre-RMW read) with TXD-set.
667        reads[6] = regs::T1SPMACTL_TXD;
668        let mut mdio = MockMdio::new(reads);
669        let mut phy = PhyLan867x::new(0);
670        phy.init(&mut mdio).unwrap();
671
672        let last_mmdad_data_write = mdio
673            .writes
674            .iter()
675            .rev()
676            .find(|&&(_, reg, _)| reg == regs::REG_MMDAD)
677            .unwrap();
678        assert_eq!(
679            last_mmdad_data_write.2,
680            regs::T1SPMACTL_TXD | regs::T1SPMACTL_MDE,
681            "RMW must preserve pre-existing T1SPMACTL bits"
682        );
683    }
684
685    #[test]
686    fn init_reset_timeout_when_bmcr_never_clears() {
687        // 1000 reads of BMCR all returning RESET set → soft_reset returns
688        // false → ResetTimeout. Buffer larger than the 500-attempt limit
689        // to avoid coupling the test to the precise loop count.
690        let mut mdio = MockMdio::new(vec![bmcr::RESET; 1000]);
691        let mut phy = PhyLan867x::new(0);
692        let err = phy.init(&mut mdio).unwrap_err();
693        assert!(matches!(err, PhyError::ResetTimeout));
694    }
695
696    #[test]
697    fn init_reset_timeout_when_resetc_never_asserts() {
698        // BMCR clears immediately, but STS2.RESETC never goes high — the
699        // chip never reports reset-complete. We must time out, not block.
700        let mut reads = vec![0x0000_u16; 1001]; // [0]=BMCR cleared, rest=STS2 with RESETC never set
701        reads[0] = 0x0000;
702        let mut mdio = MockMdio::new(reads);
703        let mut phy = PhyLan867x::new(0);
704        let err = phy.init(&mut mdio).unwrap_err();
705        assert!(matches!(err, PhyError::ResetTimeout));
706    }
707
708    #[test]
709    fn init_rejects_wrong_phy_id() {
710        let mut mdio = MockMdio::new(vec![
711            0x0000,
712            regs::STS2_RESETC,
713            0x0007, // PHY_ID0 — not LAN867x
714            0xC0F0, // PHY_ID1 — looks like LAN8720A
715        ]);
716        let mut phy = PhyLan867x::new(0);
717        let err = phy.init(&mut mdio).unwrap_err();
718        match err {
719            PhyError::UnsupportedChip { id } => assert_eq!(id, 0x0007_C0F0),
720            e => panic!("expected UnsupportedChip, got {e:?}"),
721        }
722    }
723
724    #[test]
725    fn init_rejects_wrong_midver() {
726        // Right PHY_ID, right STRAP, but MMD-31 MIDVER returns garbage —
727        // either the chip is mis-clocked or the MMD indirection broke.
728        let mut mdio = MockMdio::new(vec![
729            0x0000,
730            regs::STS2_RESETC,
731            PHY_ID0_LAN867X,
732            PHY_ID1_LAN867X_REV2,
733            regs::STRAP_CTRL0_PKGTYP_LAN8671,
734            0xDEAD, // MIDVER — wrong
735        ]);
736        let mut phy = PhyLan867x::new(0);
737        let err = phy.init(&mut mdio).unwrap_err();
738        assert!(matches!(err, PhyError::UnsupportedChip { .. }));
739    }
740
741    #[test]
742    fn init_mdio_error_propagates() {
743        // Fail on call 0 (the BMCR write inside soft_reset).
744        let mut mdio = MockMdio::with_failure(vec![], 0);
745        let mut phy = PhyLan867x::new(0);
746        let err = phy.init(&mut mdio).unwrap_err();
747        assert!(matches!(err, PhyError::Mdio(MockError)));
748    }
749
750    #[test]
751    fn init_partial_failure_at_midver_leaves_chip_none() {
752        // The PKGTYP read succeeded (so we know which Chip variant the
753        // package is), but the MIDVER probe returned garbage and init
754        // bailed with UnsupportedChip. The driver MUST NOT leave
755        // `self.chip = Some(...)` from the PKGTYP step — otherwise the
756        // poll_link gate becomes a lie. Pre-seed `chip = Some(...)` to
757        // simulate a prior successful init and confirm the failed
758        // re-init resets it back to None.
759        let mut mdio = MockMdio::new(vec![
760            0x0000,                           // BMCR poll cleared
761            regs::STS2_RESETC,                // STS2 with RESETC
762            PHY_ID0_LAN867X,                  // PHY_ID0
763            PHY_ID1_LAN867X_REV2,             // PHY_ID1
764            regs::STRAP_CTRL0_PKGTYP_LAN8671, // STRAP_CTRL0 → would map to Lan8671
765            0xDEAD,                           // MIDVER — wrong
766        ]);
767        let mut phy = PhyLan867x::new(0);
768        phy.chip = Some(Chip::Lan8670); // pretend a previous init landed
769        phy.plca_id = Some(5); // and configured PLCA
770        let err = phy.init(&mut mdio).unwrap_err();
771        assert!(matches!(err, PhyError::UnsupportedChip { .. }));
772        assert_eq!(phy.chip, None, "chip must be None after MIDVER failure");
773        assert_eq!(phy.plca_id, None);
774    }
775
776    #[test]
777    fn init_partial_failure_at_mde_rmw_leaves_chip_none() {
778        // Every MDIO call up through MIDVER succeeds, but the very
779        // first write of the T1SPMACTL RMW fails — meaning MDE never
780        // made it to the chip, so the driver isn't in the documented
781        // post-init state. chip must stay None.
782        //
783        // Call layout (read+write count interleaved):
784        //   0  BMCR write (soft_reset begin)
785        //   1  BMCR read  (poll)
786        //   2-5 STS2 indirection: 3 writes + 1 read
787        //   6  PHY_ID0 read
788        //   7  PHY_ID1 read
789        //   8  STRAP_CTRL0 read
790        //   9-12 MIDVER indirection
791        //   13  ← first MMDCTRL write of T1SPMACTL RMW
792        let mut mdio = MockMdio::with_failure(
793            vec![
794                0x0000,                           // BMCR poll
795                regs::STS2_RESETC,                // STS2.RESETC
796                PHY_ID0_LAN867X,                  // PHY_ID0
797                PHY_ID1_LAN867X_REV2,             // PHY_ID1
798                regs::STRAP_CTRL0_PKGTYP_LAN8671, // STRAP_CTRL0
799                regs::MIDVER_EXPECTED,            // MIDVER OK
800            ],
801            13,
802        );
803        let mut phy = PhyLan867x::new(0);
804        phy.chip = Some(Chip::Lan8672); // simulate prior init state
805        let err = phy.init(&mut mdio).unwrap_err();
806        assert!(matches!(err, PhyError::Mdio(MockError)));
807        assert_eq!(phy.chip, None, "chip must be None after MDE step failure");
808    }
809
810    #[test]
811    fn init_failure_makes_poll_link_report_none() {
812        // End-to-end behavioural invariant: a failed init means
813        // poll_link reports None, not the "always linked" shortcut.
814        // This is the user-visible payoff of the atomic-init
815        // contract.
816        let mut mdio = MockMdio::new(vec![
817            0x0000,
818            regs::STS2_RESETC,
819            PHY_ID0_LAN867X,
820            PHY_ID1_LAN867X_REV2,
821            regs::STRAP_CTRL0_PKGTYP_LAN8671,
822            0xDEAD, // MIDVER bad → init fails
823        ]);
824        let mut phy = PhyLan867x::new(0);
825        phy.chip = Some(Chip::Lan8671); // prior good init
826        let _ = phy.init(&mut mdio); // expected to fail
827                                     // After the failed re-init: poll_link must NOT claim a link.
828        let mut empty_mdio = MockMdio::new(vec![]);
829        let result = phy.poll_link(&mut empty_mdio).unwrap();
830        assert!(result.is_none());
831    }
832
833    #[test]
834    fn init_writes_resetc_handshake_indirection_before_phy_id_read() {
835        // Behavioural ordering: the MMDCTRL/MMDAD writes that drive the
836        // STS2 read MUST be issued BEFORE the PHY_ID0/1 reads. Without
837        // the handshake first, the chip might still be holding its
838        // configuration registers in reset.
839        let mut mdio = MockMdio::new(reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8671));
840        let mut phy = PhyLan867x::new(0);
841        phy.init(&mut mdio).unwrap();
842
843        // Find the position of the first MMDAD write addressing STS2,
844        // and confirm that no PHY_ID read happens before it. The MMDAD
845        // *value-write* is what carries the STS2 register address (it's
846        // the second write in any read sequence).
847        let sts2_addr_write_idx = mdio
848            .writes
849            .iter()
850            .position(|&(_, reg, val)| reg == regs::REG_MMDAD && val == regs::MMD_REG_STS2)
851            .expect("expected an MMDAD address-write targeting STS2");
852
853        // PHY_ID reads come from `read_phy_id` — those are *reads*, not
854        // writes, so we have to look at the call-count timing through
855        // the read_idx instead. Equivalent invariant: the index in the
856        // reads vector at which PHY_ID0 sits is index 2, and STS2's
857        // value sits at index 1. The fact that mdio.read_idx hit 2
858        // means STS2 was already consumed.
859        assert!(mdio.read_idx >= 2);
860        // And the writes log starts with: BMCR.RESET write, then the
861        // MMDCTRL ADDR write, then MMDAD STS2 addr write — i.e. the
862        // STS2 indirection precedes everything that comes after.
863        assert!(
864            sts2_addr_write_idx <= 2,
865            "MMDAD STS2 write must be amongst the first three writes"
866        );
867    }
868
869    // ── poll_link tests ────────────────────────────────────────────────
870
871    #[test]
872    fn poll_link_before_init_returns_none() {
873        // Pre-init: chip = None ⇒ poll_link must NOT claim a working
874        // link. The reset handshake hasn't run, T1SPMACTL.MDE is at the
875        // chip's power-on default (zero), so there is no link to report.
876        let mut mdio = MockMdio::new(vec![]);
877        let mut phy = PhyLan867x::new(0);
878        assert!(phy.poll_link(&mut mdio).unwrap().is_none());
879        // No MDIO traffic on the un-init path either.
880        assert!(mdio.writes.is_empty());
881        assert_eq!(mdio.read_idx, 0);
882    }
883
884    #[test]
885    fn poll_link_plca_disabled_reports_linked() {
886        // No PLCA configured → "always linked" once init done. Simulate
887        // the post-init state by seeding `chip` directly so the test
888        // stays focused on the poll_link branch logic without dragging
889        // in the seven-read init sequence.
890        let mut mdio = MockMdio::new(vec![]);
891        let mut phy = PhyLan867x::new(0);
892        phy.chip = Some(Chip::Lan8671);
893        let result = phy.poll_link(&mut mdio).unwrap();
894        assert_eq!(result, Some(LinkStatus::new(Speed::Mbps10, Duplex::Half)));
895        // Crucially: no MDIO traffic in the PLCA-off branch.
896        assert!(mdio.writes.is_empty());
897        assert_eq!(mdio.read_idx, 0);
898    }
899
900    #[test]
901    fn poll_link_plca_enabled_pst_set_reports_linked() {
902        let mut mdio = MockMdio::new(vec![regs::PLCA_STS_PST]);
903        let mut phy = PhyLan867x::new(0);
904        phy.chip = Some(Chip::Lan8671); // simulate post-init state
905        phy.plca_id = Some(0); // simulate post-configure_plca state
906        let result = phy.poll_link(&mut mdio).unwrap();
907        assert_eq!(result, Some(LinkStatus::new(Speed::Mbps10, Duplex::Half)));
908    }
909
910    #[test]
911    fn poll_link_plca_enabled_pst_clear_reports_none() {
912        let mut mdio = MockMdio::new(vec![0x0000]); // PST clear
913        let mut phy = PhyLan867x::new(0);
914        phy.chip = Some(Chip::Lan8671);
915        phy.plca_id = Some(1); // follower waiting for BEACONs
916        let result = phy.poll_link(&mut mdio).unwrap();
917        assert!(result.is_none());
918    }
919
920    #[test]
921    fn poll_link_propagates_mdio_error() {
922        let mut mdio = MockMdio::with_failure(vec![], 0);
923        let mut phy = PhyLan867x::new(0);
924        phy.chip = Some(Chip::Lan8671);
925        phy.plca_id = Some(0); // forces the MDIO path
926        let err = phy.poll_link(&mut mdio).unwrap_err();
927        assert!(matches!(err, PhyError::Mdio(MockError)));
928    }
929
930    // ── capabilities / phy_id passthroughs ─────────────────────────────
931
932    #[test]
933    fn capabilities_passes_through_to_helper() {
934        // BASIC_STATUS reset value on this chip: bit 11 (10BASE-T HD) = 1,
935        // bit 0 (EXT_CAP) = 1. read_capabilities decodes the relevant bit.
936        let bmsr = 1 << 11;
937        let mut mdio = MockMdio::new(vec![bmsr]);
938        let phy = PhyLan867x::new(0);
939        let caps = phy.capabilities(&mut mdio).unwrap();
940        assert!(caps.speed_10_hd);
941    }
942
943    #[test]
944    fn phy_id_passes_through_to_helper() {
945        let mut mdio = MockMdio::new(vec![PHY_ID0_LAN867X, PHY_ID1_LAN867X_REV2]);
946        let phy = PhyLan867x::new(0);
947        let id = phy.phy_id(&mut mdio).unwrap();
948        assert_eq!(id, 0x0000_C562);
949    }
950
951    // ── PhyLan867xWithReset tests ──────────────────────────────────────
952
953    #[derive(Default)]
954    struct MockPin {
955        history: Vec<bool>, // true = high, false = low
956    }
957
958    impl embedded_hal::digital::ErrorType for MockPin {
959        type Error = core::convert::Infallible;
960    }
961
962    impl embedded_hal::digital::OutputPin for MockPin {
963        fn set_low(&mut self) -> Result<(), core::convert::Infallible> {
964            self.history.push(false);
965            Ok(())
966        }
967        fn set_high(&mut self) -> Result<(), core::convert::Infallible> {
968            self.history.push(true);
969            Ok(())
970        }
971    }
972
973    #[derive(Default)]
974    struct MockDelay {
975        delays_ms: Vec<u32>,
976    }
977
978    impl embedded_hal::delay::DelayNs for MockDelay {
979        fn delay_ns(&mut self, ns: u32) {
980            // Record in millisecond resolution — that's what
981            // hardware_reset uses.
982            self.delays_ms.push(ns / 1_000_000);
983        }
984        fn delay_ms(&mut self, ms: u32) {
985            self.delays_ms.push(ms);
986        }
987    }
988
989    #[test]
990    fn with_reset_hardware_reset_drives_pin_low_then_high_with_delays() {
991        let mut phy = PhyLan867xWithReset::new(0, MockPin::default());
992        let mut delay = MockDelay::default();
993        phy.hardware_reset(&mut delay).unwrap();
994        assert_eq!(phy.reset_pin.history, vec![false, true]);
995        // 10 ms low + 25 ms post-release.
996        assert_eq!(delay.delays_ms, vec![10, 25]);
997    }
998
999    #[test]
1000    fn with_reset_phy_addr_passes_through() {
1001        let phy = PhyLan867xWithReset::new(5, MockPin::default());
1002        assert_eq!(phy.phy_addr(), 5);
1003    }
1004
1005    // ── PLCA configure / disable / status tests ────────────────────────
1006
1007    /// Find the last `MMDAD` data-write — i.e. the value the driver
1008    /// pushed to the most recent MMD register access. Used heavily in
1009    /// the PLCA tests since each `mmd_write` ends with an MMDAD write
1010    /// carrying the data.
1011    fn last_mmdad_data_write(mdio: &MockMdio) -> u16 {
1012        mdio.writes
1013            .iter()
1014            .rev()
1015            .find(|&&(_, reg, _)| reg == regs::REG_MMDAD)
1016            .map(|&(_, _, v)| v)
1017            .expect("expected at least one MMDAD data write")
1018    }
1019
1020    /// Find the n-th MMDAD data-write (0-indexed). MMD writes alternate
1021    /// "address-write" (whose value is the MMD register address) and
1022    /// "data-write" (whose value is the register payload). To
1023    /// distinguish, we use the fact that the second write to MMDAD in
1024    /// any sequence carries the actual payload.
1025    fn mmdad_data_writes(mdio: &MockMdio) -> Vec<u16> {
1026        // Sequence per mmd_write call: [MMDCTRL_ADDR, MMDAD_addr,
1027        // MMDCTRL_DATA, MMDAD_data]. So every fourth write is a data
1028        // write — but only when each MMDCTRL/MMDAD pair belongs to a
1029        // single mmd_write. For mmd_rmw we have a read in the middle
1030        // (read-then-write), changing the pattern.
1031        //
1032        // Pragmatic approach: collect every MMDAD write, then keep the
1033        // ones whose preceding MMDCTRL.FNCTN = DATA. Since MMDCTRL is
1034        // always the one immediately before any MMDAD touch, the test
1035        // becomes: walk the writes, track the latest FNCTN, classify
1036        // each MMDAD write accordingly.
1037        let mut current_fnctn: u16 = 0;
1038        let mut data_values = Vec::new();
1039        for &(_, reg, val) in &mdio.writes {
1040            if reg == regs::REG_MMDCTRL {
1041                current_fnctn = val & 0xC000; // top two bits
1042            } else if reg == regs::REG_MMDAD && current_fnctn == regs::MMDCTRL_FNCTN_DATA {
1043                data_values.push(val);
1044            }
1045        }
1046        data_values
1047    }
1048
1049    #[test]
1050    fn configure_plca_coordinator_writes_ctrl1_then_enables() {
1051        // Pre-RMW PLCA_CTRL0 read returns 0 (chip default), so the EN
1052        // write is exactly PLCA_CTRL0_EN.
1053        let mut mdio = MockMdio::new(vec![0x0000]);
1054        let mut phy = PhyLan867x::new(0);
1055        phy.configure_plca(
1056            &mut mdio,
1057            &PlcaConfig {
1058                node_id: 0,
1059                node_count: 8,
1060                burst_count: 0,
1061                burst_timer: 0,
1062            },
1063        )
1064        .unwrap();
1065
1066        let writes = mmdad_data_writes(&mdio);
1067        // CTRL1 = (8 << 8) | 0 = 0x0800
1068        assert_eq!(writes[0], 0x0800, "CTRL1 = NCNT(8) << 8 | ID(0)");
1069        // PLCA_CTRL0 RMW final value = EN. Whatever else gets programmed
1070        // in between (PLCA_BURST), EN must be the last write — the
1071        // ordering invariant is what the dedicated test below enforces.
1072        assert_eq!(*writes.last().unwrap(), regs::PLCA_CTRL0_EN);
1073        assert_eq!(phy.plca_id, Some(0));
1074    }
1075
1076    #[test]
1077    fn configure_plca_follower_with_burst() {
1078        let mut mdio = MockMdio::new(vec![0x0000]); // pre-RMW PLCA_CTRL0
1079        let mut phy = PhyLan867x::new(0);
1080        phy.configure_plca(
1081            &mut mdio,
1082            &PlcaConfig {
1083                node_id: 3,
1084                node_count: 8,
1085                burst_count: 2,
1086                burst_timer: 0x40,
1087            },
1088        )
1089        .unwrap();
1090
1091        let writes = mmdad_data_writes(&mdio);
1092        // CTRL1 = 0x0803
1093        assert_eq!(writes[0], 0x0803);
1094        // BURST = (MAXBC << 8) | BTMR = 0x0240
1095        assert_eq!(writes[1], 0x0240);
1096        // CTRL0 EN.
1097        assert_eq!(writes[2], regs::PLCA_CTRL0_EN);
1098        assert_eq!(phy.plca_id, Some(3));
1099    }
1100
1101    #[test]
1102    fn configure_plca_always_writes_burst_register_with_maxbc_zero() {
1103        // Re-configuring a previously-bursting node with `burst_count = 0`
1104        // must clear MAXBC on the chip. configure_plca therefore writes
1105        // PLCA_BURST unconditionally — MAXBC = 0 is the explicit
1106        // "burst disabled" encoding per datasheet sec 5.4.18.
1107        let mut mdio = MockMdio::new(vec![0x0000]);
1108        let mut phy = PhyLan867x::new(0);
1109        phy.configure_plca(
1110            &mut mdio,
1111            &PlcaConfig {
1112                node_id: 1,
1113                node_count: 8,
1114                burst_count: 0,
1115                burst_timer: 0, // sentinel
1116            },
1117        )
1118        .unwrap();
1119
1120        // Find the PLCA_BURST write — its MMDAD address-write carries
1121        // 0xCA05; the immediately-following data-write is the value.
1122        let burst_data_idx = mdio
1123            .writes
1124            .iter()
1125            .position(|&(_, reg, val)| reg == regs::REG_MMDAD && val == regs::MMD_REG_PLCA_BURST)
1126            .expect("expected an MMDAD address-write to PLCA_BURST")
1127            + 2; // skip MMDCTRL_DATA write that follows
1128        let burst_value = mdio.writes[burst_data_idx].2;
1129        // MAXBC = 0 (burst disabled), BTMR = 0x80 (sentinel default).
1130        assert_eq!(burst_value, 0x0080);
1131    }
1132
1133    #[test]
1134    fn configure_plca_burst_timer_zero_uses_sentinel_default() {
1135        // burst_count > 0 with burst_timer = 0 — the sentinel MUST be
1136        // applied so BTMR lands at the chip default (0x80) rather than
1137        // literally 0 (which would make burst mode non-functional).
1138        let mut mdio = MockMdio::new(vec![0x0000]);
1139        let mut phy = PhyLan867x::new(0);
1140        phy.configure_plca(
1141            &mut mdio,
1142            &PlcaConfig {
1143                node_id: 0,
1144                node_count: 8,
1145                burst_count: 4,
1146                burst_timer: 0, // sentinel ⇒ BTMR = 0x80
1147            },
1148        )
1149        .unwrap();
1150
1151        let burst_data_idx = mdio
1152            .writes
1153            .iter()
1154            .position(|&(_, reg, val)| reg == regs::REG_MMDAD && val == regs::MMD_REG_PLCA_BURST)
1155            .unwrap()
1156            + 2;
1157        let burst_value = mdio.writes[burst_data_idx].2;
1158        // MAXBC = 4, BTMR = 0x80.
1159        assert_eq!(burst_value, 0x0480);
1160    }
1161
1162    #[test]
1163    fn configure_plca_burst_timer_nonzero_passes_through() {
1164        // Non-zero burst_timer must NOT be touched by the sentinel.
1165        let mut mdio = MockMdio::new(vec![0x0000]);
1166        let mut phy = PhyLan867x::new(0);
1167        phy.configure_plca(
1168            &mut mdio,
1169            &PlcaConfig {
1170                node_id: 0,
1171                node_count: 8,
1172                burst_count: 4,
1173                burst_timer: 0x40,
1174            },
1175        )
1176        .unwrap();
1177
1178        let burst_data_idx = mdio
1179            .writes
1180            .iter()
1181            .position(|&(_, reg, val)| reg == regs::REG_MMDAD && val == regs::MMD_REG_PLCA_BURST)
1182            .unwrap()
1183            + 2;
1184        let burst_value = mdio.writes[burst_data_idx].2;
1185        assert_eq!(burst_value, 0x0440);
1186    }
1187
1188    #[test]
1189    fn configure_plca_rejects_id_0xff() {
1190        let mut mdio = MockMdio::new(vec![]);
1191        let mut phy = PhyLan867x::new(0);
1192        let err = phy
1193            .configure_plca(
1194                &mut mdio,
1195                &PlcaConfig {
1196                    node_id: 0xFF,
1197                    node_count: 8,
1198                    burst_count: 0,
1199                    burst_timer: 0,
1200                },
1201            )
1202            .unwrap_err();
1203        assert!(matches!(err, PlcaError::InvalidConfig));
1204        // Driver state must not have been touched on rejection.
1205        assert_eq!(phy.plca_id, None);
1206        // No MDIO traffic on early-rejection path.
1207        assert!(mdio.writes.is_empty());
1208    }
1209
1210    #[test]
1211    fn configure_plca_rejects_follower_id_at_or_above_count() {
1212        // id=8 with count=8 means slot 8 doesn't exist (slots are 0..7).
1213        let mut mdio = MockMdio::new(vec![]);
1214        let mut phy = PhyLan867x::new(0);
1215        let err = phy
1216            .configure_plca(
1217                &mut mdio,
1218                &PlcaConfig {
1219                    node_id: 8,
1220                    node_count: 8,
1221                    burst_count: 0,
1222                    burst_timer: 0,
1223                },
1224            )
1225            .unwrap_err();
1226        assert!(matches!(err, PlcaError::InvalidConfig));
1227    }
1228
1229    #[test]
1230    fn configure_plca_allows_follower_with_count_zero() {
1231        // Followers may legitimately set NCNT=0 (only the coordinator
1232        // strictly cares about that value). configure_plca must not
1233        // reject this combination.
1234        let mut mdio = MockMdio::new(vec![0x0000]);
1235        let mut phy = PhyLan867x::new(0);
1236        phy.configure_plca(
1237            &mut mdio,
1238            &PlcaConfig {
1239                node_id: 5,
1240                node_count: 0,
1241                burst_count: 0,
1242                burst_timer: 0,
1243            },
1244        )
1245        .unwrap();
1246    }
1247
1248    #[test]
1249    fn configure_plca_writes_ctrl1_before_enable() {
1250        // Ordering invariant: PLCA_CTRL1 (NCNT/ID) must be programmed
1251        // *before* PLCA_CTRL0.EN flips on. Otherwise the chip would
1252        // start operating with stale ID/NCNT for one or more PLCA
1253        // cycles.
1254        let mut mdio = MockMdio::new(vec![0x0000]);
1255        let mut phy = PhyLan867x::new(0);
1256        phy.configure_plca(&mut mdio, &PlcaConfig::default())
1257            .unwrap();
1258
1259        let writes = mmdad_data_writes(&mdio);
1260        // PLCA_CTRL0_EN must be the LAST write.
1261        assert_eq!(writes.last().copied().unwrap(), regs::PLCA_CTRL0_EN);
1262    }
1263
1264    #[test]
1265    fn disable_plca_clears_en_and_internal_state() {
1266        // Pre-RMW returns EN set; expect the post-RMW write to clear it.
1267        let mut mdio = MockMdio::new(vec![regs::PLCA_CTRL0_EN]);
1268        let mut phy = PhyLan867x::new(0);
1269        phy.plca_id = Some(2);
1270        phy.disable_plca(&mut mdio).unwrap();
1271        assert_eq!(last_mmdad_data_write(&mdio), 0);
1272        assert_eq!(phy.plca_id, None);
1273    }
1274
1275    #[test]
1276    fn plca_status_decodes_chip_state() {
1277        // Sequence of three MMD reads: CTRL0 (EN=1), CTRL1 (NCNT=8 ID=3),
1278        // STS (PST=1).
1279        let mut mdio = MockMdio::new(vec![regs::PLCA_CTRL0_EN, 0x0803, regs::PLCA_STS_PST]);
1280        let phy = PhyLan867x::new(0);
1281        let s = phy.plca_status(&mut mdio).unwrap();
1282        assert_eq!(
1283            s,
1284            PlcaStatus {
1285                enabled: true,
1286                node_id: 3,
1287                is_coordinator: false,
1288                stable: true,
1289            }
1290        );
1291    }
1292
1293    #[test]
1294    fn plca_status_recognises_coordinator() {
1295        let mut mdio = MockMdio::new(vec![regs::PLCA_CTRL0_EN, 0x0800, regs::PLCA_STS_PST]);
1296        let phy = PhyLan867x::new(0);
1297        let s = phy.plca_status(&mut mdio).unwrap();
1298        assert!(s.is_coordinator);
1299        assert_eq!(s.node_id, 0);
1300    }
1301
1302    #[test]
1303    fn plca_status_when_disabled() {
1304        // Chip default after init: EN=0, ID=0xFF (silicon power-up
1305        // value), PST=0.
1306        let mut mdio = MockMdio::new(vec![0x0000, 0x00FF, 0x0000]);
1307        let phy = PhyLan867x::new(0);
1308        let s = phy.plca_status(&mut mdio).unwrap();
1309        assert_eq!(
1310            s,
1311            PlcaStatus {
1312                enabled: false,
1313                node_id: 0xFF,
1314                is_coordinator: false,
1315                stable: false,
1316            }
1317        );
1318    }
1319
1320    #[test]
1321    fn poll_link_after_configure_plca_uses_pst_branch() {
1322        // End-to-end: configure_plca records plca_id, then poll_link
1323        // must consult PLCA_STS instead of returning the "always linked"
1324        // shortcut. PST=0 ⇒ None.
1325        let mut mdio = MockMdio::new(vec![
1326            0x0000, // pre-RMW PLCA_CTRL0 read
1327            0x0000, // PLCA_STS read in poll_link
1328        ]);
1329        let mut phy = PhyLan867x::new(0);
1330        phy.chip = Some(Chip::Lan8671); // simulate post-init state
1331        phy.configure_plca(&mut mdio, &PlcaConfig::default())
1332            .unwrap();
1333        let result = phy.poll_link(&mut mdio).unwrap();
1334        assert!(result.is_none(), "PST=0 ⇒ link not yet up");
1335    }
1336
1337    #[test]
1338    fn init_clears_cached_plca_id() {
1339        // After a soft reset the chip's PLCA_CTRL0/CTRL1 are back to
1340        // defaults (PLCA off). Driver-side `plca_id` must follow, or
1341        // `poll_link` would keep reading PLCA_STS and reporting None
1342        // forever even though the chip is actually CSMA/CD-ready.
1343        // Single-owner contract justifies clearing rather than reading
1344        // chip state back.
1345        let mut mdio = MockMdio::new(reads_for_successful_init(regs::STRAP_CTRL0_PKGTYP_LAN8671));
1346        let mut phy = PhyLan867x::new(0);
1347        phy.plca_id = Some(3); // simulate prior configure_plca
1348        phy.init(&mut mdio).unwrap();
1349        assert_eq!(phy.plca_id, None);
1350    }
1351
1352    #[test]
1353    fn configure_plca_reconfigure_with_burst_zero_clears_chip_burst() {
1354        // Regression: before this fix, configure_plca skipped the
1355        // PLCA_BURST write whenever burst_count = 0, leaving prior
1356        // burst settings on the chip. Now the BURST register is
1357        // always programmed.
1358        let mut mdio = MockMdio::new(vec![
1359            0x0000, // pre-RMW PLCA_CTRL0 for first configure
1360            0x0000, // pre-RMW PLCA_CTRL0 for second configure
1361        ]);
1362        let mut phy = PhyLan867x::new(0);
1363
1364        // First call: enable burst.
1365        phy.configure_plca(
1366            &mut mdio,
1367            &PlcaConfig {
1368                node_id: 0,
1369                node_count: 8,
1370                burst_count: 4,
1371                burst_timer: 0x40,
1372            },
1373        )
1374        .unwrap();
1375
1376        // Second call: disable burst by passing burst_count = 0.
1377        phy.configure_plca(
1378            &mut mdio,
1379            &PlcaConfig {
1380                node_id: 0,
1381                node_count: 8,
1382                burst_count: 0,
1383                burst_timer: 0,
1384            },
1385        )
1386        .unwrap();
1387
1388        // Walk the writes vector picking out every data-write to
1389        // PLCA_BURST. The data-write follows two writes after the
1390        // address-write that targets MMD_REG_PLCA_BURST: [MMDCTRL_ADDR,
1391        // MMDAD<addr>, MMDCTRL_DATA, MMDAD<data>].
1392        let mut burst_data_writes = Vec::new();
1393        for (i, &(_, reg, val)) in mdio.writes.iter().enumerate() {
1394            if reg == regs::REG_MMDAD && val == regs::MMD_REG_PLCA_BURST {
1395                // Next MMDAD write after this one is the data write.
1396                if let Some(data) = mdio.writes[i + 1..]
1397                    .iter()
1398                    .find(|w| w.1 == regs::REG_MMDAD)
1399                    .map(|w| w.2)
1400                {
1401                    burst_data_writes.push(data);
1402                }
1403            }
1404        }
1405        assert_eq!(
1406            burst_data_writes.len(),
1407            2,
1408            "expected one PLCA_BURST write per configure_plca call"
1409        );
1410        // First call enabled burst.
1411        assert_eq!(
1412            burst_data_writes[0], 0x0440,
1413            "first call: MAXBC=4, BTMR=0x40"
1414        );
1415        // Second call MUST land MAXBC=0 on the chip.
1416        assert_eq!(
1417            burst_data_writes[1] & 0xFF00,
1418            0x0000,
1419            "second call MUST clear MAXBC to 0"
1420        );
1421    }
1422
1423    #[test]
1424    fn poll_link_after_disable_plca_returns_to_always_linked() {
1425        // configure → disable → poll_link must short-circuit to "linked"
1426        // again, with no MDIO traffic.
1427        let mut mdio = MockMdio::new(vec![
1428            0x0000,              // pre-RMW PLCA_CTRL0 for configure
1429            regs::PLCA_CTRL0_EN, // pre-RMW PLCA_CTRL0 for disable
1430        ]);
1431        let mut phy = PhyLan867x::new(0);
1432        phy.chip = Some(Chip::Lan8671); // simulate post-init state
1433        phy.configure_plca(&mut mdio, &PlcaConfig::default())
1434            .unwrap();
1435        phy.disable_plca(&mut mdio).unwrap();
1436
1437        let writes_before = mdio.writes.len();
1438        let reads_before = mdio.read_idx;
1439        let result = phy.poll_link(&mut mdio).unwrap();
1440        assert_eq!(result, Some(LinkStatus::new(Speed::Mbps10, Duplex::Half)));
1441        // No additional MDIO traffic from poll_link.
1442        assert_eq!(mdio.writes.len(), writes_before);
1443        assert_eq!(mdio.read_idx, reads_before);
1444    }
1445}