# eth-phy-lan87xx
[](#license)
[](https://crates.io/crates/eth-phy-lan87xx)
[](https://docs.rs/eth-phy-lan87xx)
`#![no_std]` MDIO driver for the Microchip LAN87xx family of 10/100
Ethernet PHYs:
* LAN8710A
* LAN8720A
* LAN8740A
* LAN8741A
* LAN8742A
Implements [`eth_mdio_phy::PhyDriver`](https://docs.rs/eth-mdio-phy),
so any MAC that exposes `eth_mdio_phy::MdioBus` can drive the chip —
typical case is the ESP32 built-in EMAC SMI controller via
[`esp_emac::mdio::EspMdio`](https://docs.rs/esp-emac).
---
## Installation
```toml
[dependencies]
eth-mdio-phy = "0.2"
eth-phy-lan87xx = "0.2"
```
| `defmt` | off | `defmt::Format` derives via `eth-mdio-phy/defmt` |
**MSRV: 1.75.** Pure `#![no_std]`. Works on any target — picking the
target is the MAC layer's problem, not this crate's.
> **Pre-1.0 SemVer note.** Cargo's caret on `^0.1` will *not* pick up
> `0.2.x` — both digits behave as the major axis below 1.0. Bump the
> minor in your manifest explicitly when a new release lands.
## Compatibility
| [`eth-mdio-phy`](https://crates.io/crates/eth-mdio-phy) | 0.2.x |
| For ESP32: [`esp-emac`](https://crates.io/crates/esp-emac) | 0.2.x |
---
## Quick start
Driving a LAN8720A on an ESP32 board (PHY at MDIO addr 1):
```rust no_run
use esp_emac::mdio::EspMdio;
use eth_phy_lan87xx::PhyLan87xx;
use eth_mdio_phy::{MdioBus, PhyDriver};
# fn example<E>() -> Result<(), eth_mdio_phy::PhyError<E>>
# where EspMdio: MdioBus<Error = E> {
let mut mdio = EspMdio::new();
let mut phy = PhyLan87xx::new(1);
// Probe + soft reset + ANAR + kick auto-neg.
phy.init(&mut mdio)?;
// Poll until link is up. Returns `Some(LinkStatus)` when the link
// comes up; `None` while still negotiating.
loop {
if let Some(status) = phy.poll_link(&mut mdio)? {
// status.speed: Mbps10 / Mbps100
// status.duplex: Half / Full
break;
}
}
# Ok(())
# }
```
For the full embassy-net + DHCP example see
[`esp-emac/examples/embassy_net_lan8720a.rs`](https://github.com/jethub-iot/esp-emac-rs/blob/main/examples/embassy_net_lan8720a.rs).
## Boards with a PHY reset pin
If your board exposes a GPIO-driven PHY `nRST` line, use
`PhyLan87xxWithReset<P>` instead of `PhyLan87xx`. It wraps the same
driver and adds a `hardware_reset()` method that drives `nRST` low
for 2 ms, then deasserts and waits 25 ms before MDIO becomes
accessible (LAN8720A datasheet Table 4-2). Most JXD modules do
**not** route `nRST` to the MCU; for those use plain `PhyLan87xx`.
```rust no_run
use embedded_hal::{delay::DelayNs, digital::OutputPin};
use eth_mdio_phy::{MdioBus, PhyDriver, PhyError};
use eth_phy_lan87xx::PhyLan87xxWithReset;
# 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 = PhyLan87xxWithReset::new(/* MDIO addr */ 1, 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>) }
```
## Bypassing auto-negotiation
Auto-neg covers the common case. If a board needs a forced link (e.g.
a fixed-speed back-to-back connection) call
[`eth_mdio_phy::ieee802_3::force_link`](https://docs.rs/eth-mdio-phy/latest/eth_mdio_phy/ieee802_3/fn.force_link.html)
directly with the chosen `Speed` / `Duplex` after `PhyLan87xx::init`
returns — that helper clears `AN_ENABLE` and sets the `SPEED_100` /
`DUPLEX_FULL` bits in BMCR for you.
`poll_link` automatically detects the BMCR mode (auto-neg vs forced)
and decodes the link state appropriately.
---
## What `init` does
1. Issues `BMCR.RESET` (soft reset) and waits for the bit to
self-clear.
2. Reads `PHYIDR1/2` and rejects anything that doesn't decode to a
known LAN87xx OUI / model.
3. Disables Energy-Detect Power-Down by clearing `MCSR.EDPD_EN`.
With EDPD on, the LAN87xx silently drops 10 Mbps frames during
the wake-up window after auto-neg — turning it off keeps the RX
path active at all times.
4. **Writes `ANAR = 0x01E1`** explicitly — both the
10BASE-T / 10BASE-T-FD / 100BASE-TX / 100BASE-TX-FD ability bits
and the IEEE 802.3 selector field. This step is crucial; see the
troubleshooting note below.
5. Sets `BMCR.AN_ENABLE | BMCR.AN_RESTART` to kick auto-negotiation.
## What `poll_link` does
* Reads `BMSR` for the link bit.
* If the PHY is in auto-neg mode (`BMCR.AN_ENABLE = 1`), waits for
`PSCSR.AUTODONE` then decodes the negotiated speed / duplex from
the LAN87xx-specific PSCSR register (faster and more reliable than
reading `ANLPAR` because it reflects the actual result rather than
the partner's advertisement).
* If auto-neg is disabled (forced mode), decodes speed / duplex
directly from `BMCR.SPEED_100` / `BMCR.DUPLEX_FULL`.
---
## Troubleshooting
### Link comes up but unicast RX is dead (cold boot only)
Symptoms: ARP requests get replies, but ICMP/TCP times out. Reproduces
on cold boot; goes away after a re-flash without power-cycle.
Root cause: on a **cold boot** of the LAN8720A (and confirmed on its
siblings), issuing `BMCR.RESET` does NOT restore `ANAR` to the default
`0x01E1`. Whatever the PHY has in non-volatile state survives, and
that's typically a subset of the full 10/100 + half/full advertisement.
Auto-neg then converges on the partial subset and the link comes up
at the lowest common denominator — or, worse, succeeds on a speed
that the MAC isn't ready for, so unicast RX wedges and only
broadcast / multicast survive.
This driver handles the case by writing `ANAR = 0x01E1` explicitly
between the soft reset and `AN_RESTART`. **If you reimplement this
PHY init elsewhere, do the same** — it is the single most common cold-
boot Ethernet failure on LAN87xx.
### `PhyError::UnsupportedChip { id: 0 }` on `init`
The PHY is on a different MDIO address than the one passed to `new()`.
Typical strap-pin variants put LAN8720A at addr 0 or 1. Try both. On
ESP32 modules, the strap-pin assignment depends on PCB-level pull-up
configuration — check the schematic.
### `PhyError::UnsupportedChip { id: 0xFFFF_FFFF }` on `init`
MDIO bus reads are floating high — typical signs:
* MDIO line not pulled up (LAN87xx datasheet requires 1.5 kΩ pull-up
on MDIO).
* RMII reference clock is not running. The MDIO state machine inside
the PHY needs the 25 MHz clock to be alive even though MDIO is
electrically asynchronous; on LAN8720A, no REF_CLK = no MDIO ACK.
Verify `Emac::init` (or your equivalent) brings up the clock before
the first MDIO transaction.
### Link reports 10 Mbps despite a 100 Mbps switch
ANAR didn't get programmed correctly — see the cold-boot section
above. Other possibilities:
* MDIO writes aren't actually reaching the PHY (verify with a
scope or a software MDIO trace).
* The PHY's `BMCR.SPEED_100` strap pin is tied low and overrides the
software value at reset (LAN8720A datasheet table 7-1).
---
## Hardware verified on
* JXD-PM3-80-E1ETH and JXD-R6-E1ETH-LCD (LAN8720A on RMII; ESP32 APLL
drives the 50 MHz reference into the PHY through GPIO17). Cold boot,
soft reset and USB power-cycle all converge on link up
100 Mbps full duplex within a few hundred milliseconds.
## License
Licensed under either of:
* GNU General Public License, Version 2.0 or later
* Apache License, Version 2.0
at your option.