From 985e087842279f9ea9ef2e4d5c0847cede61796b Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Mon, 19 Jan 2026 10:29:15 -0300 Subject: [PATCH] refactor phy package --- internet/pcap/stringers.go | 5 +- phy/mdio.go | 14 --- phy/phy.go | 205 ++++++++++++++++++++++++++++++++----- phy/phy_stringers.go | 36 +++++++ phy/reg.go | 205 ++++++++++++++++++++++++++++++++++++- stringers.go | 6 +- 6 files changed, 428 insertions(+), 43 deletions(-) delete mode 100644 phy/mdio.go create mode 100644 phy/phy_stringers.go diff --git a/internet/pcap/stringers.go b/internet/pcap/stringers.go index b195f53..d81f419 100644 --- a/internet/pcap/stringers.go +++ b/internet/pcap/stringers.go @@ -24,11 +24,12 @@ func _() { _ = x[FieldClassAddress-13] _ = x[FieldClassBinaryText-14] _ = x[FieldClassOperation-15] + _ = x[FieldClassTimestamp-16] } -const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypesizeflagsidentificationchecksumoptionspayloadtextaddressbinary-textop" +const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypesizeflagsidentificationchecksumoptionspayloadtextaddressbinary-textoptimestamp" -var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 49, 54, 68, 76, 83, 90, 94, 101, 112, 114} +var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 49, 54, 68, 76, 83, 90, 94, 101, 112, 114, 123} func (i FieldClass) String() string { if i >= FieldClass(len(_FieldClass_index)-1) { diff --git a/phy/mdio.go b/phy/mdio.go deleted file mode 100644 index cc5e63c..0000000 --- a/phy/mdio.go +++ /dev/null @@ -1,14 +0,0 @@ -package phy - -// MDIOBus is a HAL for MDIO bus access supporting both Clause 22 and Clause 45 devices. -// Implementations should use devaddr to select the framing: -// - devaddr=0: Clause 22 framing (devaddr ignored in transaction) -// - devaddr>=1: Clause 45 framing (PMA/PMD=1, WIS=2, PCS=3, PHY XS=4, DTE XS=5, AN=7) -// -// Register address range: Clause 22 uses 0-31, Clause 45 uses 0-65535. -type MDIOBus interface { - // Read reads a 16-bit register from the PHY. - Read(phyAddr, devaddr uint8, regAddr uint16) (value uint16, err error) - // Write writes a 16-bit value to a PHY register. - Write(phyAddr, devaddr uint8, regAddr, value uint16) error -} diff --git a/phy/phy.go b/phy/phy.go index 61c8ce5..18c9950 100644 --- a/phy/phy.go +++ b/phy/phy.go @@ -3,13 +3,33 @@ // for configuring and monitoring physical layer transceivers. package phy +// Add more stringers in linecomment mode by adding them to type flag (comma separated). +//go:generate stringer -type=LinkMode -linecomment -output=phy_stringers.go + import ( "errors" "time" ) +// MDIOBus is a HAL for MDIO bus access supporting both Clause 22 and Clause 45 devices. +// Implementations should use devaddr to select the framing: +// - devaddr=0: Clause 22 framing (devaddr ignored in transaction) +// - devaddr>=1: Clause 45 framing (PMA/PMD=1, WIS=2, PCS=3, PHY XS=4, DTE XS=5, AN=7) +// +// Register address range: Clause 22 uses 0-31, Clause 45 uses 0-65535. +// Invalid combinations of devaddr and regAddr may or may not return an error +// depending on the implementation or result in undefined behavior. +// To avoid this wrap your MDIOBus interfaces with a wrapper type that checks validity of ranges. +type MDIOBus interface { + // Read reads a 16-bit register from the PHY. + Read(phyAddr, devAddr uint8, regAddr uint16) (value uint16, err error) + // Write writes a 16-bit value to a PHY register. + Write(phyAddr, devAddr uint8, regAddr, value uint16) error +} + // FindPHYs finds all regular non-clause45 PHYs on the MDIO bus and writes them to dst. -func FindPHYs(mdio MDIOBus, dst []uint8) (n int, err error) { +// FindClause22PHYs returns error only if unable to find no PHYs. +func FindClause22PHYs(mdio MDIOBus, dst []uint8) (n int, err error) { const maxAddr = 31 const regBasicStatus = 0x01 if len(dst) < 32 { @@ -19,9 +39,7 @@ func FindPHYs(mdio MDIOBus, dst []uint8) (n int, err error) { for addr := uint8(0); addr <= maxAddr; addr++ { // Future proofing for supported clause 45. // Check PMA/PMD device (DEVAD 1), register 0 (control) - const devAddr = 1 - var val uint16 - val, err = mdio.Read(addr, devAddr, BMCRAddr) + val, err := mdio.Read(addr, 0, AddrBMSR) if err != nil { continue } @@ -32,36 +50,87 @@ func FindPHYs(mdio MDIOBus, dst []uint8) (n int, err error) { } time.Sleep(150 * time.Microsecond) } + if n <= 0 { + err = errors.New("no phy found") + } return n, err } type Device struct { mdio MDIOBus phyaddr uint8 - // Is 1 for Clause 45 devices and 0 for clause 22 devices. - clause45 uint8 + // isClause45 is 0 for clause 22 devices and 1 for clause45 devices. + isClause45 uint8 } -func (d *Device) BasicControl() (BMCR, error) { - ctl, err := d.rread(BMCRAddr) +// ConfigureAs22 resets all state of device to be used as a Clause22 device. Does not do a software reset. +func (phy *Device) ConfigureAs22(mdio MDIOBus, phyAddr uint8) { + if phyAddr > 31 || mdio == nil { + panic("invalid argument to phy.Device.Reset") + } + phy.mdio = mdio + phy.phyaddr = phyAddr + phy.isClause45 = 0 +} + +// IsClause45 returns true if the device uses Clause 45 MDIO addressing (extended register access). +func (phy *Device) IsClause45() bool { + return phy.isClause45 == 1 +} + +// PHYAddr returns the PHY address on the MDIO bus (0-31). +func (phy *Device) PHYAddr() uint8 { + return phy.phyaddr +} + +// BasicControl reads the Basic Mode Control Register (BMCR, register 0). +func (phy *Device) BasicControl() (BMCR, error) { + ctl, err := phy.rread(AddrBMCR) return BMCR(ctl), err } -func (d *Device) BasicStatus(phyaddr uint8) (BMSR, error) { - stat, err := d.rread(BMSRAddr) +// BasicStatus reads the Basic Mode Status Register (BMSR, register 1). +func (phy *Device) BasicStatus() (BMSR, error) { + stat, err := phy.rread(AddrBMSR) return BMSR(stat), err } -func (d *Device) ID1() (uint16, error) { - return d.rread(regPhyId1) +// EnableAutoNegotiation enables or disables PHY auto-negotiation and verifies the change took effect. +func (phy *Device) EnableAutoNegotiation(b bool) error { + ctl, err := phy.BasicControl() + if err != nil { + return err + } + if b { + ctl |= BMCRANEnable + } else { + ctl &^= BMCRANEnable + } + err = phy.rwrite(AddrBMCR, uint16(ctl)) + if err != nil { + return err + } + ctl, err = phy.BasicControl() + if (ctl&BMCRANEnable != 0) != b { + return errors.New("unable to set control enable bit") + } + return nil } -func (d *Device) ID2() (uint16, error) { - return d.rread(regPhyId2) +// ID1 reads the PHY Identifier 1 register (register 2), containing bits 3-18 of the OUI. +func (phy *Device) ID1() (uint16, error) { + return phy.rread(regPhyId1) } -func (d *Device) Reset(phyaddr uint8) error { - err := d.rwrite(BMCRAddr, uint16(BMCRReset)) +// ID2 reads the PHY Identifier 2 register (register 3), containing bits 19-24 of the OUI and model/revision. +func (phy *Device) ID2() (uint16, error) { + return phy.rread(regPhyId2) +} + +// ResetPHY performs a software reset and waits for completion. +// Returns an error on IO error on MDIO bus or on timeout during wait for register reset. +func (phy *Device) ResetPHY() (err error) { + err = phy.rwrite(AddrBMCR, uint16(BMCRReset)) if err != nil { return err } @@ -69,9 +138,10 @@ func (d *Device) Reset(phyaddr uint8) error { // IEEE 802.3 allows up to 500ms. const maxPolls = 50 const resetTimeout = 500 * time.Millisecond // As per standard. + var ctl BMCR for i := 0; i < maxPolls; i++ { time.Sleep(resetTimeout / maxPolls) - ctl, err := d.BasicControl() + ctl, err = phy.BasicControl() if err != nil { continue } @@ -79,15 +149,104 @@ func (d *Device) Reset(phyaddr uint8) error { return nil } } + if err != nil { + return err + } return errors.New("PHY reset timeout") } -// rwrite mdio register write. -func (d *Device) rwrite(addr uint16, value uint16) error { - return d.mdio.Write(d.phyaddr, d.clause45, addr, value) +// SetupForced disables auto-negotiation and forces a specific link mode. +// +// Inspired by drivers/net/phy/phy_device.c +func (phy *Device) SetupForced(mode LinkMode) error { + var ctl BMCR + switch mode.SpeedMbps() { + case 1000: + ctl |= BMCRSpeed1000 + case 100: + ctl |= BMCRSpeed100 + case 10: + // No speed bits = 10Mbps + default: + return errors.New("unsupported forced link mode") + } + if mode.IsFullDuplex() { + ctl |= BMCRFullDuplex + } + // Note: BMCRANEnable is NOT set, disabling auto-negotiation + return phy.rwrite(AddrBMCR, uint16(ctl)) } -// rread mdio register read. -func (d *Device) rread(addr uint16) (value uint16, _ error) { - return d.mdio.Read(d.phyaddr, d.clause45, addr) +// Advertisement reads the current Auto-Negotiation Advertisement Register. +func (phy *Device) Advertisement() (ANAR, error) { + val, err := phy.rread(AddrANAR) + return ANAR(val), err +} + +// SetAdvertisement writes to the Auto-Negotiation Advertisement Register. +// Does NOT restart auto-negotiation; call RestartAutoNeg() after if needed. +func (phy *Device) SetAdvertisement(ad ANAR) error { + return phy.rwrite(AddrANAR, uint16(ad)) +} + +// LinkPartnerAdvertisement reads what the link partner is advertising (ANLPAR). +func (phy *Device) LinkPartnerAdvertisement() (ANAR, error) { + val, err := phy.rread(AddrANLPAR) + return ANAR(val), err +} + +// RestartAutoNeg enables auto-negotiation and restarts it. +func (phy *Device) RestartAutoNeg() error { + ctl, err := phy.BasicControl() + if err != nil { + return err + } + ctl |= BMCRANEnable | BMCRANRestart + return phy.rwrite(AddrBMCR, uint16(ctl)) +} + +// IsLinkUp returns true if link is established. +func (phy *Device) IsLinkUp() (bool, error) { + status, err := phy.BasicStatus() + if err != nil { + return false, err + } + return status&BMSRLinkStatus != 0, nil +} + +// NegotiatedLink returns the auto-negotiated link mode using standard MII registers. +// Returns LinkMode based on ANAR (our advertisement) AND ANLPAR (link partner ability). +// Priority order per IEEE 802.3 Annex 28B.3. +func (phy *Device) NegotiatedLink() (LinkMode, error) { + // First check if auto-negotiation is complete + status, err := phy.BasicStatus() + if err != nil { + return LinkDown, err + } + if status&BMSRANComplete == 0 { + return LinkDown, errors.New("auto-negotiation not complete") + } + + // Read our advertisement + anar, err := phy.Advertisement() + if err != nil { + return LinkDown, err + } + + // Read link partner's advertisement + anlpar, err := phy.LinkPartnerAdvertisement() + if err != nil { + return LinkDown, err + } + + // Common capabilities = what both sides support + common := anar & anlpar + return common.LinkMode(), nil +} + +func (phy *Device) rread(regaddr uint16) (uint16, error) { + return phy.mdio.Read(phy.phyaddr, phy.isClause45, regaddr) +} +func (phy *Device) rwrite(regaddr, value uint16) error { + return phy.mdio.Write(phy.phyaddr, phy.isClause45, regaddr, value) } diff --git a/phy/phy_stringers.go b/phy/phy_stringers.go new file mode 100644 index 0000000..772c694 --- /dev/null +++ b/phy/phy_stringers.go @@ -0,0 +1,36 @@ +// Code generated by "stringer -type=LinkMode -linecomment -output=phy_stringers.go"; DO NOT EDIT. + +package phy + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[LinkDown-0] + _ = x[Link10HDX-1] + _ = x[Link10FDX-2] + _ = x[Link100HDX-3] + _ = x[Link100FDX-4] + _ = x[Link100T4-5] + _ = x[Link1000HDX-6] + _ = x[Link1000FDX-7] + _ = x[Link2500FDX-8] + _ = x[Link5GFDX-9] + _ = x[Link10GFDX-10] + _ = x[Link25GFDX-11] + _ = x[Link40GFDX-12] + _ = x[Link100GFDX-13] +} + +const _LinkMode_name = "down10M-H10M-F100M-H100M-F100M-T41000M-H1000M-F2.5G-F5G-F10G-F25G-F40G-F100G-F" + +var _LinkMode_index = [...]uint8{0, 4, 9, 14, 20, 26, 33, 40, 47, 53, 57, 62, 67, 72, 78} + +func (i LinkMode) String() string { + if i >= LinkMode(len(_LinkMode_index)-1) { + return "LinkMode(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _LinkMode_name[_LinkMode_index[i]:_LinkMode_index[i+1]] +} diff --git a/phy/reg.go b/phy/reg.go index 3267649..3fc0e9a 100644 --- a/phy/reg.go +++ b/phy/reg.go @@ -1,5 +1,7 @@ package phy +// See https://github.com/PieVo/mdio-tool/blob/master/mii.h + // Registers 0..15 as defined by 802.3. const ( // First two registers are BMCR and BMSR. See below. @@ -24,7 +26,7 @@ const ( type BMCR uint16 const ( - BMCRAddr = 0x00 + AddrBMCR = 0x00 // Address of Basic Mode Control Register. BMCRSpeed1000 BMCR = 0x0040 // MSB of Speed (1000Mbps) BMCRCollision BMCR = 0x0080 // Collision test @@ -43,7 +45,7 @@ const ( type BMSR uint16 const ( - BMSRAddr = 0x01 + AddrBMSR = 0x01 // Address of Basic Mode Status Register. BMSRExtCap BMSR = 0x0001 // Extended register capability BMSRJabber BMSR = 0x0002 // Jabber detected @@ -61,3 +63,202 @@ const ( BMSR100Full BMSR = 0x4000 // 100Mbps full-duplex capable BMSR100Base4 BMSR = 0x8000 // 100BASE-T4 capable ) + +// ANAR represents the Auto-Negotiation Advertisement Register value at address 0x04. +// ANLPAR (Link Partner Ability Register at 0x05) shares the same bit layout. +// Reference: IEEE 802.3 Clause 28.2.4.1 +type ANAR uint16 + +const ( + AddrANAR = 0x04 // Address of Auto-Negotiation Advertisement Register. + AddrANLPAR = 0x05 // Address of Auto-Negotiation Link Partner Advertisement Register. + AddrANER = 0x06 // Address of Auto-Negotiation Error Register. + + ANARSelector ANAR = 0x001f // Protocol selector mask + ANARSelector8023 ANAR = 0x0001 // IEEE 802.3 selector value (required) + ANAR10Half ANAR = 0x0020 // 10BASE-T half-duplex + ANAR10Full ANAR = 0x0040 // 10BASE-T full-duplex + ANAR100Half ANAR = 0x0080 // 100BASE-TX half-duplex + ANAR100Full ANAR = 0x0100 // 100BASE-TX full-duplex + ANAR100BaseT4 ANAR = 0x0200 // 100BASE-T4 + ANARPause ANAR = 0x0400 // Pause capability + ANARPauseAsym ANAR = 0x0800 // Asymmetric pause + ANARRemoteFault ANAR = 0x2000 // Remote fault + ANARAck ANAR = 0x4000 // Acknowledge (ANLPAR only) + ANARNextPage ANAR = 0x8000 // Next page capable + + // Convenience masks + ANARSpeedMask ANAR = ANAR10Half | ANAR10Full | ANAR100Half | ANAR100Full | ANAR100BaseT4 + ANARPauseMask ANAR = ANARPause | ANARPauseAsym +) + +func (l LinkMode) ANAR() (a ANAR) { + switch l { + case Link10HDX: + a = ANAR10Half + case Link10FDX: + a = ANAR10Full + case Link100HDX: + a = ANAR100Half + case Link100FDX: + a = ANAR100Full + case Link100T4: + a = ANAR100BaseT4 + } + return a +} + +// WithPause returns ANAR with pause bits set according to parameters. +// +// Flow control allows a receiver to signal the sender to pause transmission. +// Common combinations: +// - (true, false): Symmetric pause - both ends can pause each other +// - (true, true): Full flow control with asymmetric fallback +// - (false, true): Rx-only pause - we can be paused, won't pause partner +// - (false, false): No flow control +func (a ANAR) WithPause(symmetric, asymmetric bool) ANAR { + a &^= ANARPauseMask + if symmetric { + a |= ANARPause + } + if asymmetric { + a |= ANARPauseAsym + } + return a +} + +// WithMaxSpeed returns ANAR with only speeds at or below maxMbps enabled. +// Preserves non-speed bits (pause, selector, etc). +func (a ANAR) WithMaxSpeed(maxMbps int) ANAR { + a &^= ANARSpeedMask + switch { + case maxMbps >= 100: + a |= ANAR100Half | ANAR100Full + fallthrough + case maxMbps >= 10: + a |= ANAR10Half | ANAR10Full + } + return a +} + +// FullDuplexOnly returns ANAR with half-duplex modes cleared. +func (a ANAR) FullDuplexOnly() ANAR { + return a &^ (ANAR10Half | ANAR100Half) +} + +// HalfDuplexOnly returns ANAR with full-duplex modes cleared. +func (a ANAR) HalfDuplexOnly() ANAR { + return a &^ (ANAR10Full | ANAR100Full) +} + +// NewANAR returns an ANAR with the IEEE 802.3 selector set. +// Always start with this when building an advertisement value. +func NewANAR() ANAR { + return ANARSelector8023 +} + +// With10M returns ANAR with 10Mbps modes (half and full) enabled. +func (a ANAR) With10M() ANAR { + return a | ANAR10Half | ANAR10Full +} + +// With100M returns ANAR with 100Mbps modes (half and full) enabled. +func (a ANAR) With100M() ANAR { + return a | ANAR100Half | ANAR100Full +} + +// Without10M returns ANAR with 10Mbps modes cleared. +func (a ANAR) Without10M() ANAR { + return a &^ (ANAR10Half | ANAR10Full) +} + +// Without100M returns ANAR with 100Mbps modes cleared. +func (a ANAR) Without100M() ANAR { + return a &^ (ANAR100Half | ANAR100Full | ANAR100BaseT4) +} + +// LinkMode returns the highest priority LinkMode from the ANAR speed bits. +// Priority order per IEEE 802.3 Annex 28B.3. +// Returns LinkDown if no speed bits are set. +func (a ANAR) LinkMode() LinkMode { + switch { + case a&ANAR100Full != 0: + return Link100FDX + case a&ANAR100BaseT4 != 0: + return Link100T4 + case a&ANAR100Half != 0: + return Link100HDX + case a&ANAR10Full != 0: + return Link10FDX + case a&ANAR10Half != 0: + return Link10HDX + default: + return LinkDown + } +} + +// LinkMode represents the negotiated/force-set Ethernet link speed and duplex mode. +// +// Naming convention: +// - H/HDX: Half-duplex (one direction at a time) +// - F/FDX: Full-duplex (simultaneous bidirectional) +// - T4: 100BASE-T4 (100Mbps over 4 twisted pairs, legacy) +// - G: Gigabit, implies number is multiplied by 1000 (1G=1000M) +type LinkMode uint8 + +const ( + LinkDown LinkMode = iota // down + Link10HDX // 10M-H + Link10FDX // 10M-F + Link100HDX // 100M-H + Link100FDX // 100M-F + Link100T4 // 100M-T4 + Link1000HDX // 1000M-H + Link1000FDX // 1000M-F + + // Clause 45 speeds (10Gbps+, full-duplex only): + + Link2500FDX // 2.5G-F + Link5GFDX // 5G-F + Link10GFDX // 10G-F + Link25GFDX // 25G-F + Link40GFDX // 40G-F + Link100GFDX // 100G-F +) + +// SpeedMbps returns the link speed in megabits per second. +func (lm LinkMode) SpeedMbps() int { + switch lm { + case Link10HDX, Link10FDX: + return 10 + case Link100HDX, Link100FDX, Link100T4: + return 100 + case Link1000HDX, Link1000FDX: + return 1000 + case Link2500FDX: + return 2500 + case Link5GFDX: + return 5000 + case Link10GFDX: + return 10_000 + case Link25GFDX: + return 25_000 + case Link40GFDX: + return 40_000 + case Link100GFDX: + return 100_000 + default: + return 0 + } +} + +// IsFullDuplex returns true if the link mode is full duplex. +func (lm LinkMode) IsFullDuplex() bool { + switch lm { + case Link10FDX, Link100FDX, Link1000FDX, + Link2500FDX, Link5GFDX, Link10GFDX, Link25GFDX, Link40GFDX, Link100GFDX: + return true + default: + return false + } +} diff --git a/stringers.go b/stringers.go index cc4d002..c6e4db1 100644 --- a/stringers.go +++ b/stringers.go @@ -196,11 +196,13 @@ func _() { var x [1]struct{} _ = x[ErrPacketDrop-1] _ = x[ErrBadCRC-2] + _ = x[ErrZeroSource-3] + _ = x[ErrZeroDestination-4] } -const _errGeneric_name = "packet droppedincorrect checksum" +const _errGeneric_name = "packet droppedincorrect checksumzero source(port/addr)zero destination(port/addr)" -var _errGeneric_index = [...]uint8{0, 14, 32} +var _errGeneric_index = [...]uint8{0, 14, 32, 54, 81} func (i errGeneric) String() string { i -= 1