Re-lock the mailbox window on lost phase instead of anchoring blind: one loop goroutine per module owns all transport, windows edge-locked from first touch, bringup verifies both modules before either AN restart; corrected charges and link-down reads print raw registers; harness -fuzz repeats lifecycles judging by exit status (30/30 module-clean)

This commit is contained in:
flamingcow
2026-08-13 16:32:14 -07:00
parent fef6b52b74
commit 9301348850
5 changed files with 280 additions and 166 deletions
+2 -2
View File
@@ -43,7 +43,7 @@ Protocol and full verified catalog: [bcm84891l-mdio-commands.md](bcm84891l-mdio-
| Item | Command | State |
|---|---|---|
| EEE / AutogrEEEn | 0x8008/0x8009 | The famous 0x0047 GET reading was the die temperature in DATA1 (firmware scratch — see reliability notes), not an AutogrEEEn mode; GET 0x8008 is unusable. cabletest forces all-off every boot (SET with explicit params `(0, 0, 0x7A12, 0x480, 0)` + AN restart, proven) and verifies 7.60 reads 0 after relink (`phy.go`) |
| EEE / AutogrEEEn | 0x8008/0x8009 | The famous 0x0047 GET reading was the die temperature in DATA1 (firmware scratch — see reliability notes), not an AutogrEEEn mode; GET 0x8008 is unusable. cabletest forces all-off every boot (SET with explicit params `(0, 0, 0x7A12, 0x480, 0)` + AN restart, proven). The SET programs the advert register directly, so 7.60 reads 0 immediately — bringup verifies it before any AN restart, both µCs quiet; the modules link to each other, so one restart puts both into training, and bringup configures and verifies both before either restart fires (`phy.go`) |
| EEE wire-truth | arm 0x801A after link-up, read 0x801B | Zero LPI events/duration on idle link; repeat under traffic |
| Fast retrain | 0x800A (datasheet titles it EMI_MODE; description is fast retrain) | Enabled 10G/5G/2.5G; IEEE 1.147 = 0x0019, count bits zero. Keep enabled; read the 1.147 count per run — a marginal cable that fast-retrains still gets counted |
| Pair map | 0x8000 | DATA2 = 0x00E4 = identity (A/B/C/D straight through) — MDI wiring verification works |
@@ -61,7 +61,7 @@ Protocol and full verified catalog: [bcm84891l-mdio-commands.md](bcm84891l-mdio-
**The internal poll's clock, measured on hardware.** One poll every 3.494.22 s, regime-dependent but rock-stable within a regime (±15 ms): ≈3.50 s warm and idle, ≈3.70 s, ≈4.01 s cool and idle, ≈4.21 s under continuous host mailbox load — host traffic *defers* the poll, never advances it, and nothing observed brings two polls closer than 3.49 s. The poll is link-independent: it runs straight through an AN restart and the whole retrain without missing a beat. Around each poll's service the bridge serves stale data for ~50 ms: every read returns the last value successfully fetched before the µC went busy (stuck-at-last-fetch, not one-behind), which is exactly how 3.33 inherits 1.147's 0x0011. The window closes *before* the poll is detectable — a CMD read returning the fresh resident 0x0031 is itself proof the µC serviced the fetch, so flip detection doubles as the all-clear.
**The windowed protocol (`phy.go` `admit`) — contention resolved by time-division.** Since the poll cannot be silenced, the host schedules around it: after bringup, every operation is admitted only inside a fixed 3.4 s window following each observed poll (CMD flipping to resident 0x0031, polled at 10 ms); at the cutoff the host goes quiet until the next flip. 3.4 s sits below every observed cadence, so a regime switch mid-run lands harmlessly in vacated time — no prediction, no period tracking, just the last observed flip plus two constants. The host's own commands re-arm detection (they leave CMD ≠ 0x0031); a missing heartbeat free-runs one window rather than stall (never observed — the heartbeat survives retrains); bringup runs before windowing is enabled and never waits. Validated on hardware in `~/work/phydiag-work/bcm_phaselock_bench.py`: aiming read bursts at the poll reproduces the poisoning on 100% of polls (the "+17" manufactured on demand); windowed operation ran 2,881 back-to-back batches — 16× the production rate, riding the cutoff — with zero stale values, and the production 1 Hz stream is unperturbed (blackout hides inside natural gaps, worst sample gap ~1.6 s). Corrected bursts since windowing no longer decode as neighbor registers (historically every one did) and correlate with amber SNR margin under full-rate traffic — real line corrections; the graded-noise correlation run is the confirmation path.
**The windowed protocol (`phy.go`) — contention resolved by time-division.** Since the poll cannot be silenced, the host schedules around it. One goroutine per module owns every transport touch (requests execute one at a time on the loop — nothing else can reach the wire, by construction), and each request is admitted only inside a fixed 3.4 s window following an observed poll (CMD flipping to resident 0x0031, polled at 10 ms); at the cutoff the host goes quiet until the next flip. 3.4 s sits below every observed cadence, so a regime switch mid-run lands harmlessly in vacated time — no prediction, no period tracking, just the last observed flip plus two constants. **Re-lock — no window at assumed phase**: edge detection needs CMD ≠ 0x0031, and normal work leaves it armed (every handler command rewrites CMD), but after a gap with no commands — a diag plus its relink wait, an idle stretch, a poll that skipped its GET because the link bit read down — the resident is 0x0031 and the phase is unknown. Admission then arms first (a bare GET_PAIR_SWAP, resident 0x0000) and anchors only on the true edge that follows, so every window is edge-locked from first contact at boot; bringup pays one phase acquisition per module (~24 s). A missing heartbeat free-runs one window rather than stall (never observed — the heartbeat survives retrains). Before re-lock the post-diag window anchored blind on the stale resident, and every ghost observed under windowing sat in exactly that window; with re-lock, ghost-free across all bench diags to date. A poisoned batch is self-consistent and forensically visible: the stuck value clears the link bit (SNR skipped, panel shows "-", the ghost-SNR panic sidestepped) while 3.33 charges the same value as errored blocks (0x0011 → the "+17"); every corrected charge and link-down reading prints its raw 1.1/3.33/1.147 on the console, so events self-attribute — a ghost names its neighbor register, real correction decodes as nothing but itself. Validated on hardware in `~/work/phydiag-work/bcm_phaselock_bench.py`: aiming read bursts at the poll reproduces the poisoning on 100% of polls (the "+17" manufactured on demand); windowed operation ran 2,881 back-to-back batches — 16× the production rate, riding the cutoff — with zero stale values, and the production 1 Hz stream is unperturbed (blackout hides inside natural gaps, worst sample gap ~1.6 s).
## ECD — recovered from the OpenBCM SDK, proven on hardware
+1 -1
View File
@@ -2,7 +2,7 @@
## Committed tree
AF_PACKET raw sockets everywhere (`sock.go`); flow-director steering; per-packet-MAC-rx-stamped rate buckets (`SO_TIMESTAMPING` cmsg, `rx_filter=ALL` as a hard host check — nics/README.md for what that demands of the NIC; **temporarily bypassed** in `ts.go` so BCM work can run on the X520, which cannot stamp — the check reports yellow and the panel rates read zero there; restore to fatal for the product NIC); read-time-stamped NIC-counter rates; test interfaces pinned to MTU 9000 with a 9018-byte jumbo in the size mix (the modules' jumbo path is exercised, not assumed); BCM module diagnostics (`phy.go`, over the patched-ixgbe `sff_i2c` debugfs, compound-op framing; one per-module lock around every whole operation, and steady-state work time-division-multiplexed with the firmware's own mailbox client — every operation admitted only inside a 3.4 s window after each observed internal temp poll, which ends the stale-read poisoning of the corrected channel — modules/fs/ for the full trap list and validation): bringup identifies both modules and forces EEE off, jumbo on and the master/slave roles (A master / B slave — left to AN the role is a per-training lottery) every boot — no trustworthy readback exists and no cable is guaranteed to probe through; the ECD — per-pair verdicts, lengths and pair maps are the length/wiring path — runs through one async path at startup and on every reset, never blocking the UI, with counters re-baselining only after the diag's own link blip so it is never charged to the run; a 1 Hz poller feeds per-pair SNR margin (vs the ≈26.5 dB operating point; green ≥ 3 dB, amber ≥ 1 dB — provisional until the graded-noise run) and the corrected-error set (PCS 3.33 errored blocks/BER, PMA 1.147 fast-retrain count) to the panel and console; framebuffer UI; harness.
AF_PACKET raw sockets everywhere (`sock.go`); flow-director steering; per-packet-MAC-rx-stamped rate buckets (`SO_TIMESTAMPING` cmsg, `rx_filter=ALL` as a hard host check — nics/README.md for what that demands of the NIC; **temporarily bypassed** in `ts.go` so BCM work can run on the X520, which cannot stamp — the check reports yellow and the panel rates read zero there; restore to fatal for the product NIC); read-time-stamped NIC-counter rates; test interfaces pinned to MTU 9000 with a 9018-byte jumbo in the size mix (the modules' jumbo path is exercised, not assumed); BCM module diagnostics (`phy.go`, over the patched-ixgbe `sff_i2c` debugfs, compound-op framing; one loop goroutine per module owns every transport touch, and all work is time-division-multiplexed with the firmware's own mailbox client — each request admitted only inside a 3.4 s edge-locked window after an observed internal temp poll, re-acquiring phase whenever it was lost — which ends the stale-read poisoning of the corrected channel; corrected charges and link-down reads print their raw registers so events self-attribute — modules/fs/ for the full trap list and validation): bringup identifies both modules and forces EEE off, jumbo on and the master/slave roles (A master / B slave — left to AN the role is a per-training lottery) every boot — no trustworthy readback exists and no cable is guaranteed to probe through; the ECD — per-pair verdicts, lengths and pair maps are the length/wiring path — runs through one async path at startup and on every reset, never blocking the UI, with counters re-baselining only after the diag's own link blip so it is never charged to the run; a 1 Hz poller feeds per-pair SNR margin (vs the ≈26.5 dB operating point; green ≥ 3 dB, amber ≥ 1 dB — provisional until the graded-noise run) and the corrected-error set (PCS 3.33 errored blocks/BER, PMA 1.147 fast-retrain count) to the panel and console; framebuffer UI; harness.
## Stashes
+44 -3
View File
@@ -5,9 +5,11 @@
package main
import (
"bytes"
"flag"
"fmt"
"image/png"
"io"
"os"
"os/exec"
"path/filepath"
@@ -20,19 +22,58 @@ import (
const (
shotsDir = "shots"
grace = 3 * time.Second
// The framebuffer's DRM master can outlive the process group by a moment;
// an immediate next run finds the device busy.
fuzzGap = 2 * time.Second
)
func main() {
runFor := flag.Duration("for", 15*time.Second, "how long to let cabletest run")
at := flag.String("at", "", "offsets to capture the panel at, comma separated, e.g. 3s,10s")
fuzzN := flag.Int("fuzz", 0, "repeat the whole lifecycle this many times; full output only for runs that fail or log module events")
flag.Parse()
if err := run(*runFor, *at, flag.Args()); err != nil {
var err error
if *fuzzN > 0 {
err = fuzz(*fuzzN, *runFor, flag.Args())
} else {
err = run(*runFor, *at, os.Stdout, flag.Args())
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func fuzz(n int, runFor time.Duration, args []string) error {
failed := 0
for i := 1; i <= n; i++ {
if i > 1 {
time.Sleep(fuzzGap)
}
var buf bytes.Buffer
start := time.Now()
err := run(runFor, "", &buf, args)
dur := time.Since(start).Round(100 * time.Millisecond)
if err != nil {
failed++
name := fmt.Sprintf("fuzz-run%02d.log", i)
if werr := os.WriteFile(name, buf.Bytes(), 0o644); werr != nil {
name = fmt.Sprintf("unsaved: %v", werr)
}
fmt.Printf("run %02d/%02d: FAILED (%s): %v [%s]\n", i, n, dur, err, name)
os.Stdout.Write(buf.Bytes())
continue
}
fmt.Printf("run %02d/%02d: ok (%s)\n", i, n, dur)
}
fmt.Printf("fuzz: %d/%d ok, %d failed\n", n-failed, n, failed)
if failed > 0 {
return fmt.Errorf("%d/%d runs failed", failed, n)
}
return nil
}
func parseShots(s string, runFor time.Duration) ([]time.Duration, error) {
if s == "" {
return nil, nil
@@ -52,7 +93,7 @@ func parseShots(s string, runFor time.Duration) ([]time.Duration, error) {
return out, nil
}
func run(runFor time.Duration, at string, args []string) error {
func run(runFor time.Duration, at string, out io.Writer, args []string) error {
if os.Geteuid() != 0 {
return fmt.Errorf("needs root for the raw sockets and the framebuffer: sudo go run ./harness")
}
@@ -67,7 +108,7 @@ func run(runFor time.Duration, at string, args []string) error {
}
cmd := exec.Command("go", append([]string{"run", "."}, args...)...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
cmd.Stdout, cmd.Stderr = out, out
// Its own process group: go run's compiled child is reparented rather than
// killed when go run dies, and an orphan holding the wire poisons every
// measurement after it.
+5
View File
@@ -793,6 +793,11 @@ func run(aName, bName string) (err error) {
rows[i] = d.displayView()
}
v, phy := measureView(diag, modules, totalView(rows))
for _, m := range modules {
for _, n := range m.takeNotes() {
fmt.Println(stats.rule(n))
}
}
for _, line := range stats.emit(totalRow(elapsed, v, target, phy,
noise.view())) {
fmt.Println(line)
+228 -160
View File
@@ -73,26 +73,52 @@ type bcm struct {
ifname string
path string
// Every method holds it for its whole logical operation: exactly one
// host-side conversation with the module at a time, by construction.
mu sync.Mutex
// Every transport touch happens on the loop goroutine: requests execute
// one at a time, each admitted into the quiet window first.
reqs chan func()
windowEnd time.Time
windowed atomic.Bool
}
// The firmware's internal temp poll (every 3.54.2 s, never under 3.49) serves
// stale bridge reads for ~50 ms around it; work stays inside 3.4 s post-poll.
func (b *bcm) loop() {
defer holdPanic()
for fn := range b.reqs {
b.window()
fn()
}
}
func (b *bcm) exec(fn func()) {
done := make(chan struct{})
b.reqs <- func() { fn(); close(done) }
<-done
}
// The firmware's internal temp poll serves stale bridge reads for ~50 ms
// around it; work stays inside 3.4 s of an observed poll. A resident 0x0031 at
// expiry means the phase is unknown, so re-lock: arm, then take the true edge.
func (b *bcm) window() {
if !b.windowed.Load() || time.Now().Add(bcmWindowFit).Before(b.windowEnd) {
if time.Now().Add(bcmWindowFit).Before(b.windowEnd) {
return
}
armed := false
deadline := time.Now().Add(bcmFlipWait)
for {
v, err := b.mdioRead(bcmMMDVendor, bcmRegCmd)
if err != nil {
panic(fmt.Sprintf("%s: heartbeat poll: %v", b.ifname, err))
}
if v == bcmCmdResidentTemp || time.Now().After(deadline) {
if v == bcmCmdResidentTemp {
if armed {
b.windowEnd = time.Now().Add(bcmWindow)
return
}
b.rearm()
armed = true
deadline = time.Now().Add(bcmFlipWait)
continue
}
armed = true
if time.Now().After(deadline) {
b.windowEnd = time.Now().Add(bcmWindow)
return
}
@@ -100,10 +126,20 @@ func (b *bcm) window() {
}
}
func (b *bcm) acquire() func() {
b.mu.Lock()
b.window()
return b.mu.Unlock
func (b *bcm) rearm() {
if _, err := b.waitStatus(func(st uint16) bool {
return st != bcmStInProgress && st != bcmStBusy
}); err != nil {
panic(fmt.Sprintf("%s: rearm: %v", b.ifname, err))
}
if err := b.mdioWrite(bcmMMDVendor, bcmRegCmd, bcmCmdGetPairSwap); err != nil {
panic(fmt.Sprintf("%s: rearm: %v", b.ifname, err))
}
if _, err := b.waitStatus(func(st uint16) bool {
return st == bcmStPass || st == bcmStError
}); err != nil {
panic(fmt.Sprintf("%s: rearm: %v", b.ifname, err))
}
}
func openBCM(ifname string) (*bcm, error) {
@@ -121,10 +157,12 @@ func openBCM(ifname string) (*bcm, error) {
b := &bcm{
ifname: ifname,
path: "/sys/kernel/debug/ixgbe/" + filepath.Base(devLink) + "/sff_i2c",
reqs: make(chan func()),
}
if _, err := os.Stat(b.path); err != nil {
return nil, fmt.Errorf("%s: %w (patched ixgbe?)", ifname, err)
}
go b.loop()
return b, nil
}
@@ -231,76 +269,79 @@ func (b *bcm) waitStatus(want func(uint16) bool) (uint16, error) {
// 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).
func (b *bcm) command(code uint16, params ...uint16) ([5]uint16, error) {
defer b.acquire()()
var data [5]uint16
if _, err := b.waitStatus(func(st uint16) bool {
return st != bcmStInProgress && st != bcmStBusy
}); err != nil {
return data, err
}
for i, p := range params {
if err := b.mdioWrite(bcmMMDVendor, bcmRegData1+uint16(i), p); err != nil {
return data, err
func (b *bcm) command(code uint16, params ...uint16) (data [5]uint16, err error) {
b.exec(func() {
if _, err = b.waitStatus(func(st uint16) bool {
return st != bcmStInProgress && st != bcmStBusy
}); err != nil {
return
}
for i, p := range params {
if err = b.mdioWrite(bcmMMDVendor, bcmRegData1+uint16(i), p); err != nil {
return
}
}
if err = b.mdioWrite(bcmMMDVendor, bcmRegCmd, code); err != nil {
return
}
var st uint16
if st, err = b.waitStatus(func(st uint16) bool {
return st == bcmStPass || st == bcmStError
}); err != nil {
return
}
if st == bcmStError {
err = fmt.Errorf("%s: command %#04x returned ERROR", b.ifname, code)
return
}
if len(params) > 0 {
return
}
for i := range data {
if data[i], err = b.mdioRead(bcmMMDVendor, bcmRegData1+uint16(i)); err != nil {
return
}
}
}
if err := b.mdioWrite(bcmMMDVendor, bcmRegCmd, code); err != nil {
return data, err
}
st, err := b.waitStatus(func(st uint16) bool {
return st == bcmStPass || st == bcmStError
})
if err != nil {
return data, err
}
if st == bcmStError {
return data, fmt.Errorf("%s: command %#04x returned ERROR", b.ifname, code)
}
if len(params) > 0 {
return data, nil
}
for i := range data {
if data[i], err = b.mdioRead(bcmMMDVendor, bcmRegData1+uint16(i)); err != nil {
return data, err
}
}
return data, nil
return
}
func (b *bcm) identify() (string, error) {
defer b.acquire()()
hi, err := b.mdioRead(1, 2)
if err != nil {
return "", err
}
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
func (b *bcm) identify() (ident string, err error) {
b.exec(func() {
var hi, lo uint16
if hi, err = b.mdioRead(1, 2); err != nil {
return
}
if lo, err = b.mdioRead(1, 3); err != nil {
return
}
if hi != bcmPHYIDHi || lo != bcmPHYIDLo {
err = fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
b.ifname, hi, lo, bcmPHYIDHi, bcmPHYIDLo)
return
}
var sn []byte
if sn, err = b.eeprom(68, 16); err != nil {
return
}
ident = "BCM84891L sn " + strings.TrimSpace(string(sn))
})
return
}
// PMA 1.1 latches low, so the first read reports any drop since it was last
// read and the second reports the wire as it is now.
func (b *bcm) linkUp() (bool, error) {
defer b.acquire()()
if _, err := b.mdioRead(1, 1); err != nil {
return false, err
}
v, err := b.mdioRead(1, 1)
if err != nil {
return false, err
}
return v&0x0004 != 0, nil
func (b *bcm) linkUp() (up bool, raw uint16, err error) {
b.exec(func() {
if _, err = b.mdioRead(1, 1); err != nil {
return
}
if raw, err = b.mdioRead(1, 1); err != nil {
return
}
up = raw&0x0004 != 0
})
return
}
func (b *bcm) forceEEEOff() error {
@@ -316,33 +357,37 @@ func (b *bcm) forceJumbo() error {
// Left to AN, master/slave is a per-training lottery and each training's DSP
// convergence moves per-pair SNR by up to ~3.6 dB; pinned roles at least keep
// every session measured under identical conditions.
func (b *bcm) forceRole(master bool) error {
defer b.acquire()()
v, err := b.mdioRead(7, 32)
if err != nil {
return err
}
v |= 0x8000
if master {
v |= 0x4000
} else {
v &^= 0x4000
}
return b.mdioWrite(7, 32, v)
func (b *bcm) forceRole(master bool) (err error) {
b.exec(func() {
var v uint16
if v, err = b.mdioRead(7, 32); err != nil {
return
}
v |= 0x8000
if master {
v |= 0x4000
} else {
v &^= 0x4000
}
err = b.mdioWrite(7, 32, v)
})
return
}
func (b *bcm) restartAN() error {
defer b.acquire()()
v, err := b.mdioRead(7, 0)
if err != nil {
return err
}
return b.mdioWrite(7, 0, v|0x0200)
func (b *bcm) restartAN() (err error) {
b.exec(func() {
var v uint16
if v, err = b.mdioRead(7, 0); err != nil {
return
}
err = b.mdioWrite(7, 0, v|0x0200)
})
return
}
func (b *bcm) eeeAdvert() (uint16, error) {
defer b.acquire()()
return b.mdioRead(7, 60)
func (b *bcm) eeeAdvert() (v uint16, err error) {
b.exec(func() { v, err = b.mdioRead(7, 60) })
return
}
func (b *bcm) pairMap() (byte, error) {
@@ -365,22 +410,24 @@ func (b *bcm) snr() ([4]float64, error) {
return out, nil
}
func (b *bcm) pcsLatch() (blocks, ber uint64, err error) {
defer b.acquire()()
v, err := b.mdioRead(3, 33)
if err != nil {
return 0, 0, err
}
return uint64(v & 0xFF), uint64((v >> 8) & 0x3F), nil
func (b *bcm) pcsLatch() (blocks, ber uint64, raw uint16, err error) {
b.exec(func() {
if raw, err = b.mdioRead(3, 33); err != nil {
return
}
blocks, ber = uint64(raw&0xFF), uint64((raw>>8)&0x3F)
})
return
}
func (b *bcm) fastRetrainCount() (uint16, error) {
defer b.acquire()()
v, err := b.mdioRead(1, 147)
if err != nil {
return 0, err
}
return v >> 11, nil
func (b *bcm) fastRetrainCount() (count, raw uint16, err error) {
b.exec(func() {
if raw, err = b.mdioRead(1, 147); err != nil {
return
}
count = raw >> 11
})
return
}
type ecdResult struct {
@@ -388,48 +435,47 @@ type ecdResult struct {
metres [4]int
}
func (b *bcm) cableDiag() (ecdResult, error) {
defer b.acquire()()
var res ecdResult
ctrl, err := b.mdioRead(bcmMMDVendor, bcmRegECDCtrl)
if err != nil {
return res, err
}
if err := b.mdioWrite(bcmMMDVendor, bcmRegECDCtrl, ctrl&^0xF400|0x8400); err != nil {
return res, err
}
deadline := time.Now().Add(ecdDeadline)
for {
ctrl, err = b.mdioRead(bcmMMDVendor, bcmRegECDCtrl)
if err != nil {
return res, err
func (b *bcm) cableDiag() (res ecdResult, err error) {
b.exec(func() {
var ctrl uint16
if ctrl, err = b.mdioRead(bcmMMDVendor, bcmRegECDCtrl); err != nil {
return
}
if ctrl&0x0800 == 0 {
break
if err = b.mdioWrite(bcmMMDVendor, bcmRegECDCtrl, ctrl&^0xF400|0x8400); err != nil {
return
}
if time.Now().After(deadline) {
return res, fmt.Errorf("%s: cable diag still busy after %s", b.ifname, ecdDeadline)
deadline := time.Now().Add(ecdDeadline)
for {
if ctrl, err = b.mdioRead(bcmMMDVendor, bcmRegECDCtrl); err != nil {
return
}
if ctrl&0x0800 == 0 {
break
}
if time.Now().After(deadline) {
err = fmt.Errorf("%s: cable diag still busy after %s", b.ifname, ecdDeadline)
return
}
time.Sleep(ecdPoll)
}
time.Sleep(ecdPoll)
}
b.window()
v, err := b.mdioRead(1, bcmRegECDResult)
if err != nil {
return res, err
}
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))
b.window()
var v uint16
if v, err = b.mdioRead(1, bcmRegECDResult); err != nil {
return
}
m, err := b.mdioRead(1, bcmRegECDLen+uint16(i))
if err != nil {
return res, err
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))
}
var m uint16
if m, err = b.mdioRead(1, bcmRegECDLen+uint16(i)); err != nil {
return
}
res.metres[i] = int(m)
}
res.metres[i] = int(m)
}
return res, nil
})
return
}
const (
@@ -465,6 +511,7 @@ type phyModule struct {
recentDelta uint64
primed bool
retrainCount uint16
notes []string
}
// Silent while a measure owns the module.
@@ -472,7 +519,7 @@ func (m *phyModule) poll() error {
if m.busy.Load() {
return nil
}
link, err := m.bcm.linkUp()
link, linkRaw, err := m.bcm.linkUp()
if err != nil {
return err
}
@@ -487,16 +534,20 @@ func (m *phyModule) poll() error {
}
}
}
blocks, ber, err := m.bcm.pcsLatch()
blocks, ber, pcsRaw, err := m.bcm.pcsLatch()
if err != nil {
return err
}
count, err := m.bcm.fastRetrainCount()
count, frRaw, err := m.bcm.fastRetrainCount()
if err != nil {
return err
}
m.mu.Lock()
if m.link && !link {
m.notes = append(m.notes,
fmt.Sprintf("%s link read down: 1.1=0x%04x", m.bcm.ifname, linkRaw))
}
m.sampled = true
m.lastOK = time.Now()
m.link = link
@@ -505,10 +556,16 @@ func (m *phyModule) poll() error {
// The first poll after a baseline drains latches from the bringup/diag
// retrain era, so it only sets the origin; the retrain counter is 5 bits.
if m.primed {
delta := blocks + ber + uint64((count-m.retrainCount)&0x1F)
rt := uint64((count - m.retrainCount) & 0x1F)
delta := blocks + ber + rt
if delta > 0 {
m.notes = append(m.notes, fmt.Sprintf(
"%s corrected +%d raw: 3.33=0x%04x (blocks %d ber %d) 1.147=0x%04x (retrain +%d) 1.1=0x%04x",
m.bcm.ifname, delta, pcsRaw, blocks, ber, frRaw, rt, linkRaw))
}
m.blocks += blocks
m.ber += ber
m.retrains += uint64((count - m.retrainCount) & 0x1F)
m.retrains += rt
m.recentDelta = delta
} else {
m.recentDelta = 0
@@ -519,6 +576,14 @@ func (m *phyModule) poll() error {
return nil
}
func (m *phyModule) takeNotes() []string {
m.mu.Lock()
defer m.mu.Unlock()
n := m.notes
m.notes = nil
return n
}
func (m *phyModule) run(done *atomic.Bool) {
tick := time.NewTicker(phyInterval)
defer tick.Stop()
@@ -848,12 +913,11 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
return fail(res.item, err)
}
out = append(out, res)
if err := m.bcm.restartAN(); err != nil {
return fail(names[i]+" retrain", err)
}
}
// Both modules configured and verified before either AN restart: the
// modules link to each other, so one restart puts both µCs into training,
// and no read should race that. The restarts fire last, nothing after.
res := checkResult{item: "eee advert"}
var adv [2]string
for i, m := range mods {
@@ -867,10 +931,14 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
}
}
res.state = adv[0] + "/" + adv[1]
for _, m := range mods {
m.bcm.windowed.Store(true)
out = append(out, res)
for i, m := range mods {
if err := m.bcm.restartAN(); err != nil {
return fail(names[i]+" retrain", err)
}
}
return append(out, res)
return out
}
func cableLine(c cableInfo) string {