#![no_std]
mod regs;
use eth_mdio_phy::ieee802_3;
use eth_mdio_phy::{Duplex, LinkState, MdioBus, PhyCapabilities, PhyDriver, PhyError, Speed};
pub struct PhyLan87xx {
addr: u8,
link_up: bool,
}
impl PhyLan87xx {
pub fn new(addr: u8) -> Self {
Self {
addr,
link_up: false,
}
}
fn parse_pscsr(pscsr_val: u16) -> Option<(Speed, Duplex)> {
match pscsr_val & regs::pscsr::SPEED_DUPLEX_MASK {
regs::pscsr::SPEED_10_HD => Some((Speed::_10M, Duplex::Half)),
regs::pscsr::SPEED_10_FD => Some((Speed::_10M, Duplex::Full)),
regs::pscsr::SPEED_100_HD => Some((Speed::_100M, Duplex::Half)),
regs::pscsr::SPEED_100_FD => Some((Speed::_100M, Duplex::Full)),
_ => None,
}
}
pub fn set_loopback<M: MdioBus>(
&mut self,
mdio: &mut M,
on: bool,
) -> Result<(), PhyError<M::Error>> {
if on {
let value = ieee802_3::bmcr::LOOPBACK
| ieee802_3::bmcr::SPEED_100
| ieee802_3::bmcr::DUPLEX_FULL;
mdio.write(self.addr, ieee802_3::regs::BMCR, value)
.map_err(PhyError::Mdio)?;
self.link_up = false;
} else {
let cur = mdio
.read(self.addr, ieee802_3::regs::BMCR)
.map_err(PhyError::Mdio)?;
mdio.write(
self.addr,
ieee802_3::regs::BMCR,
cur & !ieee802_3::bmcr::LOOPBACK,
)
.map_err(PhyError::Mdio)?;
}
Ok(())
}
}
impl PhyDriver for PhyLan87xx {
fn phy_addr(&self) -> u8 {
self.addr
}
fn init<M: MdioBus>(&mut self, mdio: &mut M) -> Result<(), PhyError<M::Error>> {
let cleared = ieee802_3::soft_reset(mdio, self.addr, 500).map_err(PhyError::Mdio)?;
if !cleared {
#[cfg(feature = "defmt")]
defmt::warn!(
"LAN87xx@{=u8}: soft reset did not clear BMCR.RESET within 500 attempts",
self.addr
);
return Err(PhyError::ResetTimeout);
}
let id = ieee802_3::read_phy_id(mdio, self.addr).map_err(PhyError::Mdio)?;
if id & regs::PHY_OUI_MASK != regs::PHY_OUI {
#[cfg(feature = "defmt")]
defmt::warn!(
"LAN87xx@{=u8}: PHY ID {=u32:08x} does not match Microchip OUI {=u32:08x} (mask {=u32:08x})",
self.addr,
id,
regs::PHY_OUI,
regs::PHY_OUI_MASK
);
return Err(PhyError::UnsupportedChip { id });
}
let mcsr = mdio
.read(self.addr, regs::mcsr::ADDR)
.map_err(PhyError::Mdio)?;
mdio.write(self.addr, regs::mcsr::ADDR, mcsr & !regs::mcsr::EDPD_EN)
.map_err(PhyError::Mdio)?;
let anar = ieee802_3::anar::TX_FD
| ieee802_3::anar::TX_HD
| ieee802_3::anar::T10_FD
| ieee802_3::anar::T10_HD
| ieee802_3::anar::SELECTOR_IEEE802_3;
mdio.write(self.addr, ieee802_3::regs::ANAR, anar)
.map_err(PhyError::Mdio)?;
ieee802_3::enable_auto_negotiation(mdio, self.addr).map_err(PhyError::Mdio)?;
self.link_up = false;
Ok(())
}
fn poll_link<M: MdioBus>(&mut self, mdio: &mut M) -> Result<LinkState, PhyError<M::Error>> {
let up = ieee802_3::is_link_up(mdio, self.addr).map_err(PhyError::Mdio)?;
if !up {
self.link_up = false;
return Ok(LinkState::down());
}
let bmcr = mdio
.read(self.addr, ieee802_3::regs::BMCR)
.map_err(PhyError::Mdio)?;
let (speed, duplex) = if bmcr & ieee802_3::bmcr::AN_ENABLE != 0 {
let pscsr = mdio
.read(self.addr, regs::pscsr::ADDR)
.map_err(PhyError::Mdio)?;
if pscsr & regs::pscsr::AUTODONE == 0 {
self.link_up = false;
return Ok(LinkState::down());
}
match Self::parse_pscsr(pscsr) {
Some(sd) => sd,
None => {
self.link_up = false;
return Ok(LinkState::down());
}
}
} else {
let speed = if bmcr & ieee802_3::bmcr::SPEED_100 != 0 {
Speed::_100M
} else {
Speed::_10M
};
let duplex = if bmcr & ieee802_3::bmcr::DUPLEX_FULL != 0 {
Duplex::Full
} else {
Duplex::Half
};
(speed, duplex)
};
self.link_up = true;
Ok(LinkState::up(speed, duplex))
}
fn capabilities<M: MdioBus>(
&self,
mdio: &mut M,
) -> Result<PhyCapabilities, PhyError<M::Error>> {
ieee802_3::read_capabilities(mdio, self.addr).map_err(PhyError::Mdio)
}
fn phy_id<M: MdioBus>(&self, mdio: &mut M) -> Result<u32, PhyError<M::Error>> {
ieee802_3::read_phy_id(mdio, self.addr).map_err(PhyError::Mdio)
}
}
pub struct PhyLan87xxWithReset<P: embedded_hal::digital::OutputPin> {
inner: PhyLan87xx,
reset_pin: P,
}
impl<P: embedded_hal::digital::OutputPin> PhyLan87xxWithReset<P> {
pub fn new(addr: u8, pin: P) -> Self {
Self {
inner: PhyLan87xx::new(addr),
reset_pin: pin,
}
}
pub fn hardware_reset<D: embedded_hal::delay::DelayNs>(
&mut self,
delay: &mut D,
) -> Result<(), P::Error> {
self.reset_pin.set_low()?;
delay.delay_ms(2);
self.reset_pin.set_high()?;
delay.delay_ms(25);
Ok(())
}
pub fn set_loopback<M: MdioBus>(
&mut self,
mdio: &mut M,
on: bool,
) -> Result<(), PhyError<M::Error>> {
self.inner.set_loopback(mdio, on)
}
}
impl<P: embedded_hal::digital::OutputPin> PhyDriver for PhyLan87xxWithReset<P> {
fn phy_addr(&self) -> u8 {
self.inner.phy_addr()
}
fn init<M: MdioBus>(&mut self, mdio: &mut M) -> Result<(), PhyError<M::Error>> {
self.inner.init(mdio)
}
fn poll_link<M: MdioBus>(&mut self, mdio: &mut M) -> Result<LinkState, PhyError<M::Error>> {
self.inner.poll_link(mdio)
}
fn capabilities<M: MdioBus>(
&self,
mdio: &mut M,
) -> Result<PhyCapabilities, PhyError<M::Error>> {
self.inner.capabilities(mdio)
}
fn phy_id<M: MdioBus>(&self, mdio: &mut M) -> Result<u32, PhyError<M::Error>> {
self.inner.phy_id(mdio)
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
use alloc::vec;
use alloc::vec::Vec;
use eth_mdio_phy::ieee802_3::{bmcr, bmsr};
#[derive(Debug, PartialEq)]
struct MockError;
struct MockMdio {
reads: Vec<u16>,
read_idx: usize,
writes: Vec<(u8, u8, u16)>,
fail_at: Option<usize>,
call_count: usize,
}
impl MockMdio {
fn new(reads: Vec<u16>) -> Self {
Self {
reads,
read_idx: 0,
writes: Vec::new(),
fail_at: None,
call_count: 0,
}
}
fn with_failure(reads: Vec<u16>, fail_at: usize) -> Self {
Self {
reads,
read_idx: 0,
writes: Vec::new(),
fail_at: Some(fail_at),
call_count: 0,
}
}
}
impl MdioBus for MockMdio {
type Error = MockError;
fn read(&mut self, _phy_addr: u8, _reg_addr: u8) -> Result<u16, Self::Error> {
if self.fail_at == Some(self.call_count) {
self.call_count += 1;
return Err(MockError);
}
self.call_count += 1;
let val = *self
.reads
.get(self.read_idx)
.expect("MockMdio: reads vector exhausted — test needs more entries");
self.read_idx += 1;
Ok(val)
}
fn write(&mut self, phy_addr: u8, reg_addr: u8, value: u16) -> Result<(), Self::Error> {
if self.fail_at == Some(self.call_count) {
self.call_count += 1;
return Err(MockError);
}
self.call_count += 1;
self.writes.push((phy_addr, reg_addr, value));
Ok(())
}
}
#[test]
fn new_sets_address() {
let phy = PhyLan87xx::new(3);
assert_eq!(phy.phy_addr(), 3);
}
#[test]
fn new_link_starts_down() {
let phy = PhyLan87xx::new(0);
assert!(!phy.link_up);
}
#[test]
fn init_success() {
let mut mdio = MockMdio::new(vec![
0x0000, 0x0007, 0xC0F0, regs::mcsr::EDPD_EN, 0x0000, ]);
let mut phy = PhyLan87xx::new(1);
phy.init(&mut mdio).unwrap();
}
#[test]
fn init_rejects_wrong_phy_id() {
let mut mdio = MockMdio::new(vec![
0x0000, 0x0022, 0x1619, ]);
let mut phy = PhyLan87xx::new(1);
let err = phy.init(&mut mdio).unwrap_err();
match err {
PhyError::UnsupportedChip { id } => assert_eq!(id, 0x0022_1619),
_ => panic!("expected UnsupportedChip, got {:?}", err),
}
}
#[test]
fn init_reset_timeout() {
let mut mdio = MockMdio::new(vec![bmcr::RESET; 1000]);
let mut phy = PhyLan87xx::new(1);
let err = phy.init(&mut mdio).unwrap_err();
match err {
PhyError::ResetTimeout => {}
_ => panic!("expected ResetTimeout, got {:?}", err),
}
}
#[test]
fn init_writes_anar_standard_advertisement() {
let mut mdio = MockMdio::new(vec![
0x0000, 0x0007, 0xC0F0, regs::mcsr::EDPD_EN, 0x0000, ]);
let mut phy = PhyLan87xx::new(1);
phy.init(&mut mdio).unwrap();
let anar_idx = mdio
.writes
.iter()
.position(|&(_, reg, _)| reg == eth_mdio_phy::ieee802_3::regs::ANAR)
.expect("expected a write to ANAR");
let expected = eth_mdio_phy::ieee802_3::anar::TX_FD
| eth_mdio_phy::ieee802_3::anar::TX_HD
| eth_mdio_phy::ieee802_3::anar::T10_FD
| eth_mdio_phy::ieee802_3::anar::T10_HD
| eth_mdio_phy::ieee802_3::anar::SELECTOR_IEEE802_3;
assert_eq!(
mdio.writes[anar_idx].2, expected,
"ANAR must advertise standard 10/100 full+half + 802.3 selector"
);
let bmcr_an_idx = mdio
.writes
.iter()
.rposition(|&(_, reg, val)| {
reg == eth_mdio_phy::ieee802_3::regs::BMCR
&& (val
& (eth_mdio_phy::ieee802_3::bmcr::AN_ENABLE
| eth_mdio_phy::ieee802_3::bmcr::AN_RESTART))
!= 0
})
.expect("expected a BMCR write that enables/restarts auto-neg");
assert!(
anar_idx < bmcr_an_idx,
"ANAR (write #{anar_idx}) must be programmed BEFORE BMCR.AN_ENABLE/AN_RESTART (write #{bmcr_an_idx})",
);
let pre_anar_an_restart = mdio.writes[..anar_idx].iter().any(|&(_, reg, val)| {
reg == eth_mdio_phy::ieee802_3::regs::BMCR
&& (val
& (eth_mdio_phy::ieee802_3::bmcr::AN_ENABLE
| eth_mdio_phy::ieee802_3::bmcr::AN_RESTART))
!= 0
});
assert!(
!pre_anar_an_restart,
"BMCR.AN_ENABLE/AN_RESTART must not be issued before the ANAR write",
);
}
#[test]
fn init_disables_edpd() {
let mcsr_initial: u16 = regs::mcsr::EDPD_EN | regs::mcsr::ENERGYON;
let mut mdio = MockMdio::new(vec![
0x0000, 0x0007, 0xC0F0, mcsr_initial, 0x0000, ]);
let mut phy = PhyLan87xx::new(1);
phy.init(&mut mdio).unwrap();
let mcsr_write = mdio
.writes
.iter()
.find(|&&(_, reg, _)| reg == regs::mcsr::ADDR)
.expect("expected a write to MCSR");
assert_eq!(
mcsr_write.2 & regs::mcsr::EDPD_EN,
0,
"EDPD_EN should be cleared"
);
assert_ne!(
mcsr_write.2 & regs::mcsr::ENERGYON,
0,
"other MCSR bits should be preserved"
);
}
#[test]
fn init_mdio_error_propagates() {
let mut mdio = MockMdio::with_failure(vec![], 0);
let mut phy = PhyLan87xx::new(1);
let err = phy.init(&mut mdio).unwrap_err();
match err {
PhyError::Mdio(MockError) => {}
_ => panic!("expected Mdio error, got {:?}", err),
}
}
#[test]
fn poll_link_down() {
let mut mdio = MockMdio::new(vec![0x0000]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert!(!result.up);
assert!(!phy.link_up);
}
#[test]
fn poll_link_100_full() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS, ieee802_3::bmcr::AN_ENABLE, regs::pscsr::AUTODONE | regs::pscsr::SPEED_100_FD, ]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_100M, Duplex::Full));
assert!(phy.link_up);
}
#[test]
fn poll_link_10_half() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::AN_ENABLE,
regs::pscsr::AUTODONE | regs::pscsr::SPEED_10_HD,
]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_10M, Duplex::Half));
}
#[test]
fn poll_link_100_half() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::AN_ENABLE,
regs::pscsr::AUTODONE | regs::pscsr::SPEED_100_HD,
]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_100M, Duplex::Half));
}
#[test]
fn poll_link_10_full() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::AN_ENABLE,
regs::pscsr::AUTODONE | regs::pscsr::SPEED_10_FD,
]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_10M, Duplex::Full));
}
#[test]
fn poll_link_unknown_speed_reports_down() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::AN_ENABLE,
regs::pscsr::AUTODONE, ]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert!(!result.up);
assert!(!phy.link_up);
}
#[test]
fn poll_link_reports_down_when_autodone_clear() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::AN_ENABLE,
regs::pscsr::SPEED_100_FD, ]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert!(!result.up, "must wait for AUTODONE before decoding speed");
assert!(!phy.link_up);
}
#[test]
fn poll_link_forced_100_full() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::SPEED_100 | ieee802_3::bmcr::DUPLEX_FULL,
]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_100M, Duplex::Full));
assert!(phy.link_up);
}
#[test]
fn poll_link_forced_10_half() {
let mut mdio = MockMdio::new(vec![bmsr::LINK_STATUS, 0x0000]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_10M, Duplex::Half));
}
#[test]
fn poll_link_forced_skips_pscsr_read() {
let mut mdio = MockMdio::new(vec![
bmsr::LINK_STATUS,
ieee802_3::bmcr::SPEED_100, ]);
let mut phy = PhyLan87xx::new(1);
let result = phy.poll_link(&mut mdio).unwrap();
assert_eq!(result, LinkState::up(Speed::_100M, Duplex::Half));
}
#[test]
fn poll_link_mdio_error() {
let mut mdio = MockMdio::with_failure(vec![], 0);
let mut phy = PhyLan87xx::new(1);
let err = phy.poll_link(&mut mdio).unwrap_err();
match err {
PhyError::Mdio(MockError) => {}
_ => panic!("expected Mdio error"),
}
}
#[test]
fn capabilities_reads_bmsr() {
let bmsr_val = bmsr::TX_FD_CAPABLE
| bmsr::TX_HD_CAPABLE
| bmsr::T10_FD_CAPABLE
| bmsr::T10_HD_CAPABLE
| bmsr::AN_ABILITY;
let mut mdio = MockMdio::new(vec![bmsr_val]);
let phy = PhyLan87xx::new(1);
let caps = phy.capabilities(&mut mdio).unwrap();
assert!(caps.speed_100_fd);
assert!(caps.speed_100_hd);
assert!(caps.speed_10_fd);
assert!(caps.speed_10_hd);
assert!(caps.auto_negotiation);
}
#[test]
fn phy_id_reads_registers() {
let mut mdio = MockMdio::new(vec![0x0007, 0xC0F0]);
let phy = PhyLan87xx::new(1);
let id = phy.phy_id(&mut mdio).unwrap();
assert_eq!(id, 0x0007_C0F0);
}
#[test]
fn set_loopback_on_writes_full_bmcr_overwrite() {
let mut mdio = MockMdio::new(vec![]);
let mut phy = PhyLan87xx::new(1);
phy.set_loopback(&mut mdio, true).unwrap();
assert_eq!(
mdio.writes.len(),
1,
"set_loopback(true) must issue exactly one BMCR write"
);
let (phy_addr, reg_addr, value) = mdio.writes[0];
assert_eq!(phy_addr, 1, "must target the configured PHY address");
assert_eq!(reg_addr, ieee802_3::regs::BMCR, "must target BMCR");
assert_eq!(
value, 0x6100,
"BMCR overwrite must equal LOOPBACK | SPEED_100 | DUPLEX_FULL (0x6100), got 0x{value:04x}"
);
assert_eq!(
value,
bmcr::LOOPBACK | bmcr::SPEED_100 | bmcr::DUPLEX_FULL,
"BMCR overwrite bit composition drifted from spec"
);
assert_eq!(
value & bmcr::AN_ENABLE,
0,
"AN_ENABLE must be cleared by the full BMCR overwrite"
);
}
#[test]
fn set_loopback_off_rmw_preserves_other_bits() {
let initial: u16 = 0x3000;
let mut mdio = MockMdio::new(vec![initial]);
let mut phy = PhyLan87xx::new(1);
phy.set_loopback(&mut mdio, false).unwrap();
assert_eq!(
mdio.writes.len(),
1,
"set_loopback(false) must issue exactly one BMCR write"
);
let (phy_addr, reg_addr, value) = mdio.writes[0];
assert_eq!(phy_addr, 1);
assert_eq!(reg_addr, ieee802_3::regs::BMCR);
assert_eq!(
value,
initial & !bmcr::LOOPBACK,
"OFF must clear only bit 14; expected 0x{:04x}, got 0x{:04x}",
initial & !bmcr::LOOPBACK,
value
);
}
#[test]
fn set_loopback_off_clears_existing_loopback_bit() {
let initial: u16 = bmcr::LOOPBACK | bmcr::SPEED_100 | bmcr::DUPLEX_FULL;
let mut mdio = MockMdio::new(vec![initial]);
let mut phy = PhyLan87xx::new(1);
phy.set_loopback(&mut mdio, false).unwrap();
let (_, _, value) = mdio.writes[0];
assert_eq!(value & bmcr::LOOPBACK, 0, "LOOPBACK must be cleared");
assert_ne!(
value & bmcr::SPEED_100,
0,
"SPEED_100 must be preserved across the RMW"
);
assert_ne!(
value & bmcr::DUPLEX_FULL,
0,
"DUPLEX_FULL must be preserved across the RMW"
);
}
#[test]
fn set_loopback_on_invalidates_cached_link() {
let mut mdio = MockMdio::new(vec![]);
let mut phy = PhyLan87xx::new(1);
phy.link_up = true;
phy.set_loopback(&mut mdio, true).unwrap();
assert!(
!phy.link_up,
"set_loopback(true) must invalidate the cached link-up flag"
);
}
#[test]
fn set_loopback_on_mdio_error_propagates() {
let mut mdio = MockMdio::with_failure(vec![], 0);
let mut phy = PhyLan87xx::new(1);
let err = phy.set_loopback(&mut mdio, true).unwrap_err();
match err {
PhyError::Mdio(MockError) => {}
_ => panic!("expected Mdio error, got {:?}", err),
}
}
#[test]
fn set_loopback_off_mdio_error_propagates_on_read() {
let mut mdio = MockMdio::with_failure(vec![], 0);
let mut phy = PhyLan87xx::new(1);
let err = phy.set_loopback(&mut mdio, false).unwrap_err();
match err {
PhyError::Mdio(MockError) => {}
_ => panic!("expected Mdio error, got {:?}", err),
}
}
#[test]
fn set_loopback_off_mdio_error_propagates_on_write() {
let mut mdio = MockMdio::with_failure(vec![0x3000], 1);
let mut phy = PhyLan87xx::new(1);
let err = phy.set_loopback(&mut mdio, false).unwrap_err();
match err {
PhyError::Mdio(MockError) => {}
_ => panic!("expected Mdio error, got {:?}", err),
}
}
#[test]
fn wrapped_set_loopback_delegates_to_inner() {
struct NoopPin;
impl embedded_hal::digital::ErrorType for NoopPin {
type Error = core::convert::Infallible;
}
impl embedded_hal::digital::OutputPin for NoopPin {
fn set_low(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn set_high(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
let mut mdio = MockMdio::new(vec![]);
let mut phy = PhyLan87xxWithReset::new(1, NoopPin);
phy.set_loopback(&mut mdio, true).unwrap();
assert_eq!(
mdio.writes.len(),
1,
"delegated set_loopback(true) must issue exactly one BMCR write"
);
let (addr, reg, value) = mdio.writes[0];
assert_eq!(addr, 1, "phy address must propagate");
assert_eq!(reg, ieee802_3::regs::BMCR, "must target BMCR");
assert_eq!(
value,
bmcr::LOOPBACK | bmcr::SPEED_100 | bmcr::DUPLEX_FULL,
"full BMCR overwrite value must match inner contract"
);
}
#[test]
fn parse_pscsr_all_modes() {
assert_eq!(
PhyLan87xx::parse_pscsr(regs::pscsr::SPEED_10_HD),
Some((Speed::_10M, Duplex::Half))
);
assert_eq!(
PhyLan87xx::parse_pscsr(regs::pscsr::SPEED_10_FD),
Some((Speed::_10M, Duplex::Full))
);
assert_eq!(
PhyLan87xx::parse_pscsr(regs::pscsr::SPEED_100_HD),
Some((Speed::_100M, Duplex::Half))
);
assert_eq!(
PhyLan87xx::parse_pscsr(regs::pscsr::SPEED_100_FD),
Some((Speed::_100M, Duplex::Full))
);
assert_eq!(PhyLan87xx::parse_pscsr(0x0000), None);
}
#[test]
fn parse_pscsr_ignores_other_bits() {
let val = regs::pscsr::SPEED_100_FD | regs::pscsr::AUTODONE | 0x0003 | 0x8000;
assert_eq!(
PhyLan87xx::parse_pscsr(val),
Some((Speed::_100M, Duplex::Full))
);
}
}