Bringup waits for nothing (no sleeps, no link waits, SETs fire cable-or-not with advert readback), errors held off the display while a measure is in flight, status poll is a deadline not a retry count, ghost signals (implausible SNR, undefined ECD verdict) panic outright; persistent stale-pipeline bridge fact documented

This commit is contained in:
flamingcow
2026-08-13 10:49:01 -07:00
parent f2891886f3
commit f9140ff8d9
4 changed files with 56 additions and 64 deletions
+36 -55
View File
@@ -41,10 +41,9 @@ const (
bcmReadDelayUs = 3000
bcmRetryDelayUs = 10000
bcmStatusPoll = 100 * time.Millisecond
bcmStatusTries = 30
bcmSetSettle = time.Second
bcmQuiet = 10 * time.Second
bcmStatusPoll = 100 * time.Millisecond
// Covers the handler's documented 2 s freeze during 10GBASE-T training.
bcmStatusTimeout = 3 * time.Second
ecdPoll = 200 * time.Millisecond
ecdDeadline = 50 * time.Second
@@ -175,25 +174,28 @@ func (b *bcm) eeprom(off byte, n int) ([]byte, error) {
return b.compound(0xA0, 0xA1, 500, n, []byte{off})
}
// The datasheet's completion handshake: poll STATUS on its 100 ms cadence
// until the wanted state, bounded by a deadline.
func (b *bcm) waitStatus(want func(uint16) bool) (uint16, error) {
var st uint16
for i := 0; i < bcmStatusTries; i++ {
var err error
st, err = b.mdioRead(bcmMMDVendor, bcmRegStatus)
deadline := time.Now().Add(bcmStatusTimeout)
for {
st, err := b.mdioRead(bcmMMDVendor, bcmRegStatus)
if err != nil {
return 0, err
}
if want(st) {
return st, nil
}
if time.Now().After(deadline) {
return 0, fmt.Errorf("%s: command handler stuck, status %#04x", b.ifname, st)
}
time.Sleep(bcmStatusPoll)
}
return 0, fmt.Errorf("%s: command handler stuck, status %#04x", b.ifname, st)
}
// GETs must be invoked bare (pre-writing any DATA register leaves the handler
// executing as a no-op); SETs must pass their full parameter set (the handler
// executes stale DATA) and get settle time in place of unprovable completion.
// executes stale DATA).
func (b *bcm) command(code uint16, params ...uint16) ([5]uint16, error) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -212,9 +214,6 @@ func (b *bcm) command(code uint16, params ...uint16) ([5]uint16, error) {
if err := b.mdioWrite(bcmMMDVendor, bcmRegCmd, code); err != nil {
return data, err
}
if len(params) > 0 {
time.Sleep(bcmSetSettle)
}
st, err := b.waitStatus(func(st uint16) bool {
return st == bcmStPass || st == bcmStError
})
@@ -375,6 +374,9 @@ func (b *bcm) cableDiag() (ecdResult, error) {
}
for i := range res.verdicts {
res.verdicts[i] = int(v>>(4*i)) & 0xF
if res.verdicts[i] > pairXtalk {
panic(fmt.Sprintf("%s: ghost ECD verdict %#04x", b.ifname, v))
}
m, err := b.mdioRead(1, bcmRegECDLen+uint16(i))
if err != nil {
return res, err
@@ -394,12 +396,16 @@ const (
snrOperatingPoint = 26.5
snrGoodMargin = 3.0
snrWarnMargin = 1.0
// No trained link produces SNR outside this; readings there are another
// register's data (die temp ≈ 8, handler status ≈ 0.4) and the run dies.
snrGhostLow = 15.0
snrGhostHigh = 50.0
)
type phyModule struct {
bcm *bcm
busy atomic.Bool
upSince time.Time
bcm *bcm
busy atomic.Bool
mu sync.Mutex
sampled bool
@@ -415,32 +421,25 @@ type phyModule struct {
retrainCount uint16
}
// Silent while a measure owns the module, and no handler command outside a
// quiet window (carrier up and stable): writes landing on a µC busy with
// post-AN work have wedged its SMI service permanently. Reads are always safe.
// Silent while a measure owns the module.
func (m *phyModule) poll() error {
if m.busy.Load() {
return nil
}
if carrierUp(m.bcm.ifname) {
if m.upSince.IsZero() {
m.upSince = time.Now()
}
} else {
m.upSince = time.Time{}
}
quiet := !m.upSince.IsZero() && time.Since(m.upSince) >= bcmQuiet
link, err := m.bcm.linkUp()
if err != nil {
return err
}
link = link && quiet
var snr [4]float64
if link {
if snr, err = m.bcm.snr(); err != nil {
return err
}
for _, s := range snr {
if s < snrGhostLow || s > snrGhostHigh {
panic(fmt.Sprintf("%s: ghost SNR %.1f dB", m.bcm.ifname, s))
}
}
}
blocks, ber, err := m.bcm.pcsLatch()
if err != nil {
@@ -629,9 +628,8 @@ func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay
return d
}
// Every handler write inside runs in a quiet window: the ECD trigger only
// after the carrier has settled, the pair-map commands only after the run's
// own blip has settled too. The pollers are held silent throughout.
// The pollers are held silent throughout; pair maps are read after the
// relink, so the MDI resolution is the fresh one.
func measureCable(mods []*phyModule, waitRelink bool, done *atomic.Bool) (cableInfo, bool, error) {
for _, m := range mods {
m.busy.Store(true)
@@ -641,11 +639,6 @@ func measureCable(mods []*phyModule, waitRelink bool, done *atomic.Bool) (cableI
m.busy.Store(false)
}
}()
names := [2]string{mods[0].bcm.ifname, mods[1].bcm.ifname}
time.Sleep(2 * phyInterval)
if _, up := waitCarrier(names, done); up {
time.Sleep(bcmQuiet)
}
var c cableInfo
var err error
@@ -655,9 +648,9 @@ func measureCable(mods []*phyModule, waitRelink bool, done *atomic.Bool) (cableI
}
relinked := false
if waitRelink {
names := [2]string{mods[0].bcm.ifname, mods[1].bcm.ifname}
_, relinked = waitCarrier(names, done)
}
time.Sleep(bcmQuiet)
for i, m := range mods {
if c.maps[i], err = m.bcm.pairMap(); err != nil {
return c, false, err
@@ -780,17 +773,14 @@ func waitCarrier(names [2]string, done *atomic.Bool) (time.Duration, bool) {
// cable is guaranteed at bringup, so both settings are forced every boot: the
// one deterministic assurance. The handler freezes during training, so the
// carrier settles — the host checks just reset the links — before any command.
// Never waits for a link: there may be no cable, and forcing config needs
// none — the AN restart applies it whenever training next happens.
func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
var out []checkResult
fail := func(item string, err error) []checkResult {
return append(out, checkResult{item: item, err: err})
}
_, hadLink := waitCarrier(names, nil)
if hadLink {
time.Sleep(bcmQuiet)
}
for i, m := range mods {
res := checkResult{item: names[i] + " eee", state: "forced off"}
if err := m.bcm.forceEEEOff(); err != nil {
@@ -809,12 +799,7 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
}
}
res := checkResult{item: "link retrain"}
var took time.Duration
up := false
if hadLink {
took, up = waitCarrier(names, nil)
}
res := checkResult{item: "eee advert"}
var adv [2]string
for i, m := range mods {
v, err := m.bcm.eeeAdvert()
@@ -822,15 +807,11 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
return fail(res.item, err)
}
adv[i] = fmt.Sprintf("%#04x", v)
if up && v != 0 {
if v != 0 {
res.err = fmt.Errorf("%s still advertises EEE %#04x", names[i], v)
}
}
if up {
res.state = fmt.Sprintf("up in %.1fs, eee advert %s/%s", took.Seconds(), adv[0], adv[1])
} else {
res.state = fmt.Sprintf("no link (cable unplugged?), eee advert %s/%s", adv[0], adv[1])
}
res.state = adv[0] + "/" + adv[1]
return append(out, res)
}