eth-phy-lan867x 0.1.0

LAN8670/8671/8672 10BASE-T1S Ethernet PHY driver for no_std over MDIO
Documentation

eth-phy-lan867x

License: GPL-2.0-or-later OR Apache-2.0 Crates.io Documentation

#![no_std] MDIO driver for the Microchip LAN867x family of 10BASE-T1S Ethernet PHYs (IEEE 802.3cg-2019 Clause 147):

  • LAN8670 — 32-VQFN, MII or RMII
  • LAN8671 — 24-VQFN, RMII only
  • LAN8672 — 36-VQFN, MII only

10BASE-T1S is single-pair, half-duplex, multidrop Ethernet — quite different from the point-to-point 10/100BASE-T flavours covered by eth-phy-lan87xx. If you're plugging into a switch, you want lan87xx; if you're building a sensor / actuator backbone with up to 8 nodes on a shared single twisted pair, you want this crate.

Implements eth_mdio_phy::PhyDriver so any MAC that exposes eth_mdio_phy::MdioBus can drive the chip. On JetHome boards the MAC is the ESP32 built-in EMAC SMI controller via esp_emac::mdio::EspMdio.


Installation

[dependencies]
eth-mdio-phy    = "0.2"
eth-phy-lan867x = "0.1"
Feature Default Pulls in
defmt off defmt::Format derives via eth-mdio-phy/defmt

MSRV: 1.75. Pure #![no_std], no allocations. Works on any target.

Pre-1.0 SemVer note. Cargo's caret on ^0.1 will not pick up 0.2.x, and vice versa — both digits behave as the major axis below 1.0. Bump explicitly when a new release lands. This crate's first release is 0.1, but it depends on eth-mdio-phy 0.2.

Compatibility

Crate Version
eth-mdio-phy 0.2.x
For ESP32: esp-emac 0.2.x

Quick start

CSMA/CD multidrop bus (no PLCA), PHY at MDIO addr 0:

use eth_phy_lan867x::PhyLan867x;
use eth_mdio_phy::{MdioBus, PhyDriver};

# fn example<M: MdioBus>(mdio: &mut M)
# -> Result<(), eth_mdio_phy::PhyError<M::Error>>
# {
let mut phy = PhyLan867x::new(0);

// Probe + soft reset + RESETC handshake + PHY-ID + MIDVER + MDE=1.
phy.init(mdio)?;

// On a CSMA/CD bus there is no per-link-partner signal — the bus is
// "always there". poll_link returns Some(LinkStatus { Mbps10, Half })
// once init has succeeded.
let status = phy.poll_link(mdio)?.expect("CSMA/CD always reports linked");
assert_eq!(status.speed, eth_mdio_phy::Speed::Mbps10);
assert_eq!(status.duplex, eth_mdio_phy::Duplex::Half);
# Ok(())
# }

With PLCA (recommended for > 2-node buses)

PLCA (IEEE 802.3 Clause 148) gives the bus collision-free TDMA-style arbitration on top of CSMA/CD. One node is the coordinator (node_id = 0); the rest are followers (1..=0xFE).

use eth_phy_lan867x::{PhyLan867x, PlcaConfig};
use eth_mdio_phy::{MdioBus, PhyDriver};

# fn coordinator<M: MdioBus>(mdio: &mut M)
# -> Result<(), eth_phy_lan867x::PlcaError<M::Error>>
# {
let mut phy = PhyLan867x::new(0);
phy.init(mdio).map_err(|_| /* convert to PlcaError */ todo!())?;

phy.configure_plca(mdio, &PlcaConfig {
    node_id:    0,    // coordinator
    node_count: 8,    // up to 8 nodes on the segment
    burst_count: 0,   // single-frame TXOPs
    burst_timer: 0,
})?;
// poll_link will now consult PLCA_STS.PST and report linked when
// BEACONs are flowing.
# Ok(())
# }

For a follower:

# use eth_phy_lan867x::{PhyLan867x, PlcaConfig};
# use eth_mdio_phy::MdioBus;
# fn follower<M: MdioBus>(phy: &mut PhyLan867x, mdio: &mut M)
# -> Result<(), eth_phy_lan867x::PlcaError<M::Error>>
# {
phy.configure_plca(mdio, &PlcaConfig {
    node_id:    3,    // unique on the segment
    node_count: 8,    // must match the coordinator's NCNT
    burst_count: 0,
    burst_timer: 0,
})?;
# Ok(())
# }

Boards with a PHY reset pin

If your board exposes a GPIO-driven PHY RESET_N line — and JetHome JXD-CPU-E1T1S does, on ESP32 GPIO17 — use PhyLan867xWithReset<P>:

use embedded_hal::{delay::DelayNs, digital::OutputPin};
use eth_mdio_phy::{MdioBus, PhyDriver, PhyError};
use eth_phy_lan867x::PhyLan867xWithReset;

# fn example<P, D, M>(reset: P, delay: &mut D, mdio: &mut M)
#     -> Result<(), MyError<P::Error, M::Error>>
# where
#     P: OutputPin,
#     D: DelayNs,
#     M: MdioBus,
# {
let mut phy = PhyLan867xWithReset::new(/* MDIO addr */ 0, reset);
phy.hardware_reset(delay).map_err(MyError::Pin)?;
phy.init(mdio).map_err(MyError::Phy)?;
# Ok(())
# }
# enum MyError<P, M> { Pin(P), Phy(PhyError<M>) }

hardware_reset drives RESET_N low for 10 ms then waits 25 ms after release before MDIO is allowed — conservative timings that also match PhyLan87xxWithReset.


What init does

  1. Soft reset via BMCR.SW_RESET (self-clearing, bounded poll).
  2. Reset-complete handshake: poll STS2.RESETC in MMD-31. The chip holds IRQ_N low after every reset until the host reads STS2; reading it clears RESETC and releases the line. Without this step, subsequent register writes may not take effect.
  3. PHY identity check via PHY_ID0/1. Mask 0xFFFF_FFF0 against 0x0000_C560 (Microchip OUI 00800Fh + MODEL 010110b); silicon revision is intentionally allowed to vary.
  4. Package discrimination from STRAP_CTRL0.PKGTYPChip::Lan8670 / 8671 / 8672. Available via chip().
  5. MIDVER sanity probe: MMD-31 0xCA00 must read 0x0A10 (OPEN Alliance register-map identifier, version 1.0). Confirms the MMD indirection is functional and the silicon implements the standard T1S register layout.
  6. Multidrop enable: RMW T1SPMACTL.MDE = 1 in MMD-1. Required for any > 2-node bus; chip default is point-to-point.

What poll_link does

  • PLCA off (CSMA/CD): no MDIO traffic. Returns Some(LinkStatus { Mbps10, Half }) — the bus is "always there".
  • PLCA on: reads MMD-31 PLCA_STS.PST. Returns linked when set (BEACONs are being TX'd as coordinator or RX'd as follower); returns None while the bus is still synchronising.

The branch is selected by the driver's internal flag, which configure_plca sets and disable_plca / init clear. The driver assumes a single-owner contract: it is the sole writer to the PHY's registers. If a different host or task flips PLCA_CTRL0.EN directly via MDIO between calls, this driver will not notice — call init to resync.

What BMSR.LINK_STATUS does NOT do

It is hard-wired 1 on this chip. Calling eth_mdio_phy::ieee802_3::is_link_up will always return true, regardless of bus state. Don't. Use poll_link instead.


Troubleshooting

PhyError::ResetTimeout on init

Two possible sources:

  • BMCR.SW_RESET never self-clears (~500-attempt window). Likely the MDIO bus is silently failing the write. Verify MDIO / MDC wiring and pull-ups.
  • STS2.RESETC never asserts. The chip never finished its internal POR sequence — usually means the 50 MHz REFCLKIN (RMII) or 25 MHz crystal (MII) is not actually clocking. On JXD-CPU-E1T1S the LAN8671 has its own oscillator and exports the 50 MHz to ESP32 via GPIO0; if ESP32's EMAC is configured wrong (e.g. expecting internal APLL output instead of external clock-in) the PHY runs but the MAC can't talk to it.

PhyError::UnsupportedChip { id: 0 } on init

The PHY is on a different MDIO address than the one passed to new(), OR the MDIO clock isn't running. LAN8671 latches its SMI address from PHYAD[3:0] at hardware reset; the JetHome JXD-CPU- E1T1S pulls all four pins low ⇒ addr = 0. Other boards will differ; check the schematic and/or read STRAP_CTRL0.SMIADR.

PhyError::UnsupportedChip { id: 0xFFFF_FFFF } on init

MDIO bus reads are floating high — typical signs:

  • No external pull-up on MDIO (Microchip recommends 10 kΩ).
  • The PHY is held in RESET_N. Use PhyLan867xWithReset and call hardware_reset first.

PLCA configured but poll_link always returns None

Most common cause: every node thinks it is the coordinator (node_id = 0). Datasheet sec 4.9.2 covers the diagnostic flags (STS1.UNEXPB is set on coordinator collisions). Other causes:

  • node_count < actual node count: some followers' TXOPs never come up, but the coordinator still BEACONs and PST will oscillate.
  • node_id >= node_count on a follower: this is rejected by configure_plca with PlcaError::InvalidConfig.

Need PLCA diagnostic counters

STS1 decoding, TOCNT / BCNCNT readers — deferred to v0.2. For v0.1.x, read MMD-31 registers 0x18 / 0x24-0x27 directly through your MdioBus if you need them in the meantime.


Hardware verified on

The driver compiles, runs unit tests against a MockMdio, and matches the datasheet register-by-register against DS60001573C (silicon revision 2 = product revision B1). Hardware bring-up on JXD-R6-E1T1S is in progress and will land in a follow-up release.

License

Licensed under either of:

  • GNU General Public License, Version 2.0 or later
  • Apache License, Version 2.0

at your option.