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}