Bridged MDIO reads verify freshness: a late async fetch serves the previous response as the wrong register (observed STATUS 0x0004 as 0.4dB SNR); unchanged-or-zero results re-read bare without re-arming until stable, first read always verified, identify retries against the constant PHY ID

This commit is contained in:
flamingcow
2026-08-13 08:26:22 -07:00
parent bba1fa5e02
commit e99c7121e7
3 changed files with 74 additions and 26 deletions
+72 -25
View File
@@ -40,12 +40,14 @@ const (
bcmPHYIDHi = 0x3590
bcmPHYIDLo = 0x5081
bcmReadDelayUs = 3000
bcmRetryDelayUs = 10000
bcmReadDelayUs = 3000
bcmStatusPoll = 100 * time.Millisecond
bcmStatusTries = 30
bcmVerifyGap = 2 * time.Millisecond
bcmVerifyTries = 8
ecdPoll = 200 * time.Millisecond
ecdDeadline = 50 * time.Second
@@ -70,6 +72,10 @@ type bcm struct {
// Guards multi-op sequences only; single reads are already atomic on the
// wire through the compound op.
mu sync.Mutex
rmu sync.Mutex
lastBuf uint16
haveBuf bool
}
func openBCM(ifname string) (*bcm, error) {
@@ -155,14 +161,50 @@ func (b *bcm) mdioReadDelay(devad, reg uint16, delayUs int) (uint16, error) {
return uint16(d[0])<<8 | uint16(d[1]), nil
}
// 0x0000 is also the bridge's not-ready signature, so a zero is read again at
// a longer delay before being believed.
func (b *bcm) mdioRead(devad, reg uint16) (uint16, error) {
v, err := b.mdioReadDelay(devad, reg, bcmReadDelayUs)
if err != nil || v != 0 {
return v, err
func (b *bcm) bufRead() (uint16, error) {
resp, err := b.op(fmt.Sprintf("r %02x 2", bcmI2CRead))
if err != nil {
return 0, err
}
return b.mdioReadDelay(devad, reg, bcmRetryDelayUs)
d, err := parseHexBytes(resp, 2)
if err != nil {
return 0, err
}
return uint16(d[0])<<8 | uint16(d[1]), nil
}
// The bridge fetch is asynchronous: one that outruns the delay leaves the
// previous response in the buffer, served silently as the wrong register's
// data. A changed buffer value proves a fresh fetch; an unchanged or zero one
// is re-read bare — never re-armed, so clear-on-read registers keep their
// data — until it stabilizes.
func (b *bcm) mdioRead(devad, reg uint16) (uint16, error) {
b.rmu.Lock()
defer b.rmu.Unlock()
v, err := b.mdioReadDelay(devad, reg, bcmReadDelayUs)
if err != nil {
return 0, err
}
if b.haveBuf && v != 0 && v != b.lastBuf {
b.lastBuf = v
return v, nil
}
stable := 0
for i := 0; i < bcmVerifyTries && stable < 2; i++ {
time.Sleep(bcmVerifyGap)
r, err := b.bufRead()
if err != nil {
return 0, err
}
if r == v {
stable++
} else {
v, stable = r, 0
}
}
b.haveBuf, b.lastBuf = true, v
return v, nil
}
func (b *bcm) mdioWrite(devad, reg, val uint16) error {
@@ -229,24 +271,29 @@ func (b *bcm) command(code uint16, params ...uint16) ([5]uint16, error) {
return data, nil
}
// Retried against the known constant: the first reads after a process start
// can land while the bridge still holds a dead process's pending fetch.
func (b *bcm) identify() (string, error) {
hi, err := b.mdioRead(1, 2)
if err != nil {
return "", err
var hi, lo uint16
var err error
for i := 0; i < 5; i++ {
if hi, err = b.mdioRead(1, 2); err != nil {
return "", err
}
if lo, err = b.mdioRead(1, 3); err != nil {
return "", err
}
if hi == bcmPHYIDHi && lo == bcmPHYIDLo {
sn, err := b.eeprom(68, 16)
if err != nil {
return "", err
}
return "BCM84891L sn " + strings.TrimSpace(string(sn)), nil
}
time.Sleep(200 * time.Millisecond)
}
lo, err := b.mdioRead(1, 3)
if err != nil {
return "", err
}
if hi != bcmPHYIDHi || lo != bcmPHYIDLo {
return "", fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
b.ifname, hi, lo, bcmPHYIDHi, bcmPHYIDLo)
}
sn, err := b.eeprom(68, 16)
if err != nil {
return "", err
}
return "BCM84891L sn " + strings.TrimSpace(string(sn)), nil
return "", fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
b.ifname, hi, lo, bcmPHYIDHi, bcmPHYIDLo)
}
// PMA 1.1 latches low, so the first read reports any drop since it was last