Files
cabletest/phy.go
T

1217 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/sys/unix"
)
const (
bcmI2CWrite = 0xAC
bcmI2CRead = 0xAD
bcmMMDVendor uint16 = 0x1E
bcmRegCmd uint16 = 0x4005
bcmRegStatus uint16 = 0x4037
bcmRegData1 uint16 = 0x4038
bcmStInProgress uint16 = 0x0002
bcmStPass uint16 = 0x0004
bcmStError uint16 = 0x0008
bcmStBusy uint16 = 0xBBBB
bcmCmdGetPairSwap uint16 = 0x8000
bcmCmdSetEEEMode uint16 = 0x8009
bcmCmdSetJumbo uint16 = 0x801C
bcmRegECDCtrl uint16 = 0x4006
bcmRegECDResult uint16 = 0xA896
bcmRegECDLen uint16 = 0xA897
bcmPHYIDHi = 0x3590
bcmPHYIDLo = 0x5081
bcmCmdResidentTemp uint16 = 0x0031
bcmReadDelayUs = 3000
bcmRetryDelayUs = 10000
bcmWindow = 3400 * time.Millisecond
bcmWindowFit = 100 * time.Millisecond
bcmFlipPoll = 10 * time.Millisecond
bcmFlipWait = 5 * 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
pairIdentityMap = 0xE4
fsVendorPN = "SFP-10G-T-100"
wiitekVendorPN = "UF-RJ45-10G-100"
)
const (
pairOK = 1
pairOpen = 2
pairShort = 3
pairXtalk = 4
)
var pairVerdicts = map[int]string{
pairOK: "ok", pairOpen: "OPEN", pairShort: "SHORT", pairXtalk: "XTALK",
}
type sff struct {
ifname string
path string
// Every transport touch happens on the loop goroutine: requests execute
// one at a time, each admitted by the module type's own gate first.
reqs chan func()
admit func()
}
func openSFF(ifname string) (*sff, error) {
devLink, err := os.Readlink("/sys/class/net/" + ifname + "/device")
if err != nil {
return nil, fmt.Errorf("%s: %w", ifname, err)
}
drv, err := ifDriver(ifname)
if err != nil {
return nil, fmt.Errorf("%s: %w", ifname, err)
}
if drv != "ixgbe" {
return nil, fmt.Errorf("%s: no module I2C transport for driver %s", ifname, drv)
}
s := &sff{
ifname: ifname,
path: "/sys/kernel/debug/ixgbe/" + filepath.Base(devLink) + "/sff_i2c",
reqs: make(chan func()),
admit: func() {},
}
if _, err := os.Stat(s.path); err != nil {
return nil, fmt.Errorf("%s: %w (patched ixgbe?)", ifname, err)
}
go s.loop()
return s, nil
}
func (s *sff) loop() {
defer holdPanic()
for fn := range s.reqs {
s.admit()
fn()
}
}
func (s *sff) exec(fn func()) {
done := make(chan struct{})
s.reqs <- func() { fn(); close(done) }
<-done
}
func (s *sff) name() string { return s.ifname }
func (s *sff) op(cmd string) (string, error) {
fd, err := unix.Open(s.path, unix.O_RDWR, 0)
if err != nil {
return "", fmt.Errorf("%s: %w", s.path, err)
}
defer unix.Close(fd)
if _, err := unix.Write(fd, []byte(cmd)); err != nil {
return "", fmt.Errorf("%s %q: %w", s.ifname, cmd, err)
}
buf := make([]byte, 256)
n, err := unix.Read(fd, buf)
if err != nil {
return "", fmt.Errorf("%s %q: %w", s.ifname, cmd, err)
}
resp := strings.TrimSpace(string(buf[:n]))
if !strings.HasPrefix(resp, "ok") {
return "", fmt.Errorf("%s %q: %s", s.ifname, cmd, resp)
}
return strings.TrimSpace(resp[2:]), nil
}
func parseHexBytes(s string, n int) ([]byte, error) {
fields := strings.Fields(s)
if len(fields) != n {
return nil, fmt.Errorf("want %d bytes, got %q", n, s)
}
out := make([]byte, n)
for i, f := range fields {
var v byte
if _, err := fmt.Sscanf(f, "%x", &v); err != nil {
return nil, fmt.Errorf("byte %q in %q", f, s)
}
out[i] = v
}
return out, nil
}
// A single bus hold; split write/read ops would let the driver's own SFP
// traffic consume the bridge's pending read.
func (s *sff) compound(waddr, raddr byte, delayUs, n int, wdata []byte) ([]byte, error) {
var sb strings.Builder
fmt.Fprintf(&sb, "x %02x %02x %d %x", waddr, raddr, delayUs, n)
for _, v := range wdata {
fmt.Fprintf(&sb, " %02x", v)
}
resp, err := s.op(sb.String())
if err != nil {
return nil, err
}
return parseHexBytes(resp, n)
}
func (s *sff) eeprom(off byte, n int) ([]byte, error) {
return s.compound(0xA0, 0xA1, 500, n, []byte{off})
}
func (s *sff) vendorPN() (string, error) {
pn, err := s.eeprom(40, 16)
if err != nil {
return "", err
}
return strings.TrimSpace(string(pn)), nil
}
type mdioDev interface {
name() string
exec(func())
mdioRead(devad, reg uint16) (uint16, error)
mdioWrite(devad, reg, val uint16) error
}
type phyDev interface {
mdioDev
identify() (string, error)
}
type snrSource interface {
snrMargins() ([4]float64, error)
}
// 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 devLinkUp(d mdioDev) (up bool, raw uint16, err error) {
d.exec(func() {
if _, err = d.mdioRead(1, 1); err != nil {
return
}
if raw, err = d.mdioRead(1, 1); err != nil {
return
}
up = raw&0x0004 != 0
})
return
}
func devPCSLatch(d mdioDev) (blocks, ber uint64, raw uint16, err error) {
d.exec(func() {
if raw, err = d.mdioRead(3, 33); err != nil {
return
}
blocks, ber = uint64(raw&0xFF), uint64((raw>>8)&0x3F)
})
return
}
func devFastRetrain(d mdioDev) (count, raw uint16, err error) {
d.exec(func() {
if raw, err = d.mdioRead(1, 147); err != nil {
return
}
count = raw >> 11
})
return
}
// AN enable is forced alongside the restart: the ECD can leave the BCM with
// 7.0.12 cleared (proven live — no AN pulses, both ends deaf, link down until
// power cycle), and a bare restart bit preserves the cleared enable.
func devRestartAN(d mdioDev) (err error) {
d.exec(func() {
var v uint16
if v, err = d.mdioRead(7, 0); err != nil {
return
}
err = d.mdioWrite(7, 0, v|0x1200)
})
return
}
func devEEEAdvert(d mdioDev) (v uint16, err error) {
d.exec(func() { v, err = d.mdioRead(7, 60) })
return
}
type bcm struct {
*sff
windowEnd time.Time
}
func newBCM(t *sff) *bcm {
b := &bcm{sff: t}
t.exec(func() { t.admit = b.window })
return b
}
// 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.
// Each taken edge immediately re-arms — the one CMD write per window lands at
// the start of the quiet period, maximally far from the next poll (writes near
// the poll are the µC-wedge risk), and every later expiry reads the phase
// without writing.
func (b *bcm) window() {
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 {
if armed {
b.windowEnd = time.Now().Add(bcmWindow)
b.rearm()
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
}
time.Sleep(bcmFlipPoll)
}
}
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 (b *bcm) mdioReadDelay(devad, reg uint16, delayUs int) (uint16, error) {
d, err := b.compound(bcmI2CWrite, bcmI2CRead, delayUs, 2,
[]byte{0x20 | byte(devad), byte(reg >> 8), byte(reg)})
if err != nil {
return 0, err
}
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
}
return b.mdioReadDelay(devad, reg, bcmRetryDelayUs)
}
func (b *bcm) mdioWrite(devad, reg, val uint16) error {
_, err := b.op(fmt.Sprintf("w %02x %02x %02x %02x %02x %02x",
bcmI2CWrite, byte(devad), byte(reg>>8), byte(reg), byte(val>>8), byte(val)))
return err
}
// 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) {
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)
}
}
// 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) (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
}
}
})
return
}
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
}
func (b *bcm) forceEEEOff() error {
_, err := b.command(bcmCmdSetEEEMode, 0x0000, 0x0000, 0x7A12, 0x0480, 0x0000)
return err
}
func (b *bcm) forceJumbo() error {
_, err := b.command(bcmCmdSetJumbo, 1, 0, 0, 0, 0)
return err
}
// 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) (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) pairMap() (byte, error) {
d, err := b.command(bcmCmdGetPairSwap)
if err != nil {
return 0, err
}
return byte(d[1]), nil
}
const (
rbI2CWrite = 0xA2
rbI2CRead = 0xA3
rbOffPassword byte = 0x7B
rbOffPage byte = 0x7F
rbOffCmd byte = 0x80
rbOffDevad byte = 0x81
rbOffValHi byte = 0x84
rbOffPartNum byte = 0xFA
rbPageMailbox byte = 3
rbCmdWrite byte = 0x01
rbCmdRead byte = 0x02
rbCmdDone byte = 0x04
rbReadDelayUs = 500
rbCmdPoll = 20 * time.Millisecond
// Matches the BCM allowance for a handler frozen by 10GBASE-T training.
rbCmdTimeout = 3 * time.Second
rbPHYIDHi uint16 = 0x002B
rbPHYIDLo uint16 = 0x0BF4
// IEEE margins land near 7-9 dB on a healthy short cable; far outside is
// another register's data.
rbGhostLow = -10.0
rbGhostHigh = 25.0
)
// Only the registers proven safe on this PHY (docs/modules/wiitek/): single
// reads in the vendor windows brick the µC permanently, so everything else
// refuses before touching hardware.
var rbReadSafe = map[uint16]map[uint16]bool{
1: {1: true, 2: true, 3: true, 133: true, 134: true, 135: true, 136: true, 147: true},
3: {32: true, 33: true},
7: {0: true, 33: true, 60: true},
}
var rbWriteSafe = map[uint16]map[uint16]bool{
7: {0: true},
}
type rollball struct {
*sff
}
func (r *rollball) i2cWrite(off byte, data ...byte) error {
var sb strings.Builder
fmt.Fprintf(&sb, "w %02x %02x", rbI2CWrite, off)
for _, v := range data {
fmt.Fprintf(&sb, " %02x", v)
}
_, err := r.op(sb.String())
return err
}
func (r *rollball) i2cRead(off byte, n int) ([]byte, error) {
return r.compound(rbI2CWrite, rbI2CRead, rbReadDelayUs, n, []byte{off})
}
func (r *rollball) unlock() error {
if err := r.i2cWrite(rbOffPage, rbPageMailbox); err != nil {
return err
}
return r.i2cWrite(rbOffPassword, 0xFF, 0xFF, 0xFF, 0xFF)
}
func (r *rollball) mbox(cmd byte, devad, reg, val uint16) error {
if err := r.unlock(); err != nil {
return err
}
if err := r.i2cWrite(rbOffDevad, byte(devad), byte(reg>>8), byte(reg)); err != nil {
return err
}
if cmd == rbCmdWrite {
if err := r.i2cWrite(rbOffValHi, byte(val>>8), byte(val)); err != nil {
return err
}
}
if err := r.i2cWrite(rbOffCmd, cmd); err != nil {
return err
}
deadline := time.Now().Add(rbCmdTimeout)
for {
d, err := r.i2cRead(rbOffCmd, 1)
if err != nil {
return err
}
if d[0] == rbCmdDone {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("%s: mailbox %d.%#04x stuck at %#02x", r.ifname, devad, reg, d[0])
}
time.Sleep(rbCmdPoll)
}
}
func rbGuard(safe map[uint16]map[uint16]bool, ifname, what string, devad, reg uint16) {
if !safe[devad][reg] {
panic(fmt.Sprintf("%s: refusing MDIO %s %d.%#04x: outside the proven-safe set",
ifname, what, devad, reg))
}
}
func (r *rollball) mdioRead(devad, reg uint16) (uint16, error) {
rbGuard(rbReadSafe, r.ifname, "read", devad, reg)
if err := r.mbox(rbCmdRead, devad, reg, 0); err != nil {
return 0, err
}
d, err := r.i2cRead(rbOffValHi, 2)
if err != nil {
return 0, err
}
return uint16(d[0])<<8 | uint16(d[1]), nil
}
func (r *rollball) mdioWrite(devad, reg, val uint16) error {
rbGuard(rbWriteSafe, r.ifname, "write", devad, reg)
return r.mbox(rbCmdWrite, devad, reg, val)
}
func (r *rollball) identify() (ident string, err error) {
r.exec(func() {
var hi, lo uint16
if hi, err = r.mdioRead(1, 2); err != nil {
return
}
if lo, err = r.mdioRead(1, 3); err != nil {
return
}
if hi != rbPHYIDHi || lo != rbPHYIDLo {
err = fmt.Errorf("%s: PHY ID %#04x:%#04x, want %#04x:%#04x",
r.ifname, hi, lo, rbPHYIDHi, rbPHYIDLo)
return
}
if err = r.unlock(); err != nil {
return
}
var part []byte
if part, err = r.i2cRead(rbOffPartNum, 1); err != nil {
return
}
var sn []byte
if sn, err = r.eeprom(68, 16); err != nil {
return
}
ident = fmt.Sprintf("CUX3610 sn %s (A2.250=%d)", strings.TrimSpace(string(sn)), part[0])
})
return
}
func (r *rollball) snrMargins() (out [4]float64, err error) {
r.exec(func() {
for i := range out {
var v uint16
if v, err = r.mdioRead(1, uint16(133+i)); err != nil {
return
}
m := (float64(v) - 0x8000) / 10
if m < rbGhostLow || m > rbGhostHigh {
panic(fmt.Sprintf("%s: ghost SNR margin %.1f dB (1.%d=%#04x)", r.ifname, m, 133+i, v))
}
out[i] = m
}
})
return
}
const (
phyInterval = time.Second
phyStale = 5 * time.Second
phyMaxDark = 30
linkWaitSpan = 25 * time.Second
linkWaitPoll = time.Second
snrGoodMargin = 3.0
snrWarnMargin = 1.0
)
type phyModule struct {
dev phyDev
busy atomic.Bool
mu sync.Mutex
sampled bool
lastOK time.Time
link bool
haveSNR bool
margins [4]float64
blocks uint64
ber uint64
retrains uint64
recentDelta uint64
primed bool
retrainCount uint16
notes []string
}
// Silent while a measure owns the module.
func (m *phyModule) poll() error {
if m.busy.Load() {
return nil
}
link, linkRaw, err := devLinkUp(m.dev)
if err != nil {
return err
}
var margins [4]float64
haveSNR := false
if src, ok := m.dev.(snrSource); ok && link {
if margins, err = src.snrMargins(); err != nil {
return err
}
haveSNR = true
}
blocks, ber, pcsRaw, err := devPCSLatch(m.dev)
if err != nil {
return err
}
count, frRaw, err := devFastRetrain(m.dev)
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.dev.name(), linkRaw))
}
m.sampled = true
m.lastOK = time.Now()
m.link = link
m.haveSNR = haveSNR
m.margins = margins
// 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 {
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.dev.name(), delta, pcsRaw, blocks, ber, frRaw, rt, linkRaw))
}
m.blocks += blocks
m.ber += ber
m.retrains += rt
m.recentDelta = delta
} else {
m.recentDelta = 0
m.primed = true
}
m.retrainCount = count
m.mu.Unlock()
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()
dark := 0
var lastErr error
for !done.Load() {
<-tick.C
if err := m.poll(); err != nil {
dark++
lastErr = err
if dark >= phyMaxDark {
panic(fmt.Sprintf("module diagnostics dark for %d polls: %v", dark, lastErr))
}
continue
}
dark = 0
}
}
func (m *phyModule) reset() {
m.mu.Lock()
m.blocks, m.ber, m.retrains, m.recentDelta = 0, 0, 0, 0
m.primed = false
m.mu.Unlock()
}
type phyModView struct {
fresh bool
link bool
haveSNR bool
margins [4]float64
blocks uint64
ber uint64
retrain uint64
recent uint64
}
func (m *phyModule) view() phyModView {
m.mu.Lock()
defer m.mu.Unlock()
v := phyModView{
fresh: m.sampled && time.Since(m.lastOK) < phyStale,
link: m.link,
haveSNR: m.haveSNR,
margins: m.margins,
blocks: m.blocks,
ber: m.ber,
retrain: m.retrains,
}
if v.fresh {
v.recent = m.recentDelta
}
return v
}
type cableInfo struct {
ecd ecdResult
maps [2]byte
haveMaps [2]bool
}
func (c cableInfo) metresString() string {
sum, n := 0, 0
for i, v := range c.ecd.verdicts {
if v == pairOK {
sum += c.ecd.metres[i]
n++
}
}
if n == 0 {
return "-"
}
return fmt.Sprintf("%d", (sum+n/2)/n)
}
const (
clsNone = iota
clsGood
clsWarn
clsBad
)
func snrClass(margin float64) int {
switch {
case margin >= snrGoodMargin:
return clsGood
case margin >= snrWarnMargin:
return clsWarn
default:
return clsBad
}
}
type phyDisplay struct {
haveSNR bool
worstMargin float64
corrected uint64
recent uint64
metres string
metresClass int
}
func pairLetter(i int) string { return string(rune('A' + i)) }
// Each end resolves MDI on its own, so a swap at either known end counts; an
// end with no readable map abstains.
func pairSwapped(i int, c cableInfo) bool {
for e := range c.maps {
if c.haveMaps[e] && int(c.maps[e]>>(2*i))&3 != i {
return true
}
}
return false
}
func cableSummary(cable cableInfo, measuring bool) (string, int) {
if measuring {
return "...", clsNone
}
anyData, anyFault, anySwap := false, false, false
for i, v := range cable.ecd.verdicts {
if v != 0 {
anyData = true
}
if v != 0 && v != pairOK {
anyFault = true
}
if pairSwapped(i, cable) {
anySwap = true
}
}
s := cable.metresString()
switch {
case !anyData:
return "-", clsNone
case anyFault:
return s, clsBad
case anySwap:
return s, clsWarn
}
return s, clsGood
}
// The margin is the worst pair across the ends that measure SNR (the Wiitek's
// IEEE 1.133136), gated on the whole pair being fresh and linked.
func phyDisplayFrom(cable cableInfo, measuring bool, a, b phyModView) phyDisplay {
d := phyDisplay{
haveSNR: a.fresh && b.fresh && a.link && b.link && (a.haveSNR || b.haveSNR),
corrected: a.blocks + a.ber + a.retrain + b.blocks + b.ber + b.retrain,
recent: a.recent + b.recent,
}
if d.haveSNR {
first := true
for _, v := range []phyModView{a, b} {
if !v.haveSNR {
continue
}
for _, m := range v.margins {
if first || m < d.worstMargin {
d.worstMargin = m
first = false
}
}
}
}
d.metres, d.metresClass = cableSummary(cable, measuring)
return d
}
type ecdResult struct {
verdicts [4]int
metres [4]int
}
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 err = b.mdioWrite(bcmMMDVendor, bcmRegECDCtrl, ctrl&^0xF400|0x8400); err != nil {
return
}
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)
}
b.window()
var v uint16
if v, err = b.mdioRead(1, bcmRegECDResult); err != nil {
return
}
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)
}
})
return
}
func bcmEnd(mods []*phyModule) *bcm {
for _, m := range mods {
if b, ok := m.dev.(*bcm); ok {
return b
}
}
panic("no BCM module in the pair: the ECD is the only length path")
}
// 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)
}
defer func() {
for _, m := range mods {
m.busy.Store(false)
}
}()
var c cableInfo
var err error
end := bcmEnd(mods)
c.ecd, err = end.cableDiag()
if err != nil {
return c, false, err
}
if err = devRestartAN(end); err != nil {
return c, false, err
}
relinked := false
if waitRelink {
names := [2]string{mods[0].dev.name(), mods[1].dev.name()}
_, relinked = waitCarrier(names, done)
}
for i, m := range mods {
b, ok := m.dev.(*bcm)
if !ok {
continue
}
if c.maps[i], err = b.pairMap(); err != nil {
return c, false, err
}
c.haveMaps[i] = true
}
return c, relinked, nil
}
type cableDiag struct {
mods []*phyModule
completed chan error
mu sync.Mutex
info cableInfo
running bool
}
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
return &cableDiag{mods: mods, completed: make(chan error, 1), info: info}
}
func (c *cableDiag) snapshot() (cableInfo, bool) {
c.mu.Lock()
defer c.mu.Unlock()
return c.info, c.running
}
func (c *cableDiag) kick(done *atomic.Bool) bool {
c.mu.Lock()
if c.running {
c.mu.Unlock()
return false
}
c.running = true
c.mu.Unlock()
go func() {
defer holdPanic()
info, _, err := measureCable(c.mods, true, done)
if err != nil {
info = cableInfo{}
}
c.mu.Lock()
c.info = info
c.running = false
c.mu.Unlock()
select {
case c.completed <- err:
default:
}
}()
return true
}
func mapString(m byte, have bool) string {
if !have {
return "unread"
}
if m == pairIdentityMap {
return "straight"
}
out := make([]string, 4)
for i := range out {
out[i] = pairLetter(int(m>>(2*i)) & 3)
}
return "swapped to " + strings.Join(out, "")
}
func verdictString(r ecdResult) string {
bad := []string{}
for i, v := range r.verdicts {
if v != pairOK {
s, ok := pairVerdicts[v]
if !ok {
s = fmt.Sprintf("%d", v)
}
bad = append(bad, fmt.Sprintf("%s %s at %dm", pairLetter(i), s, r.metres[i]))
}
}
if len(bad) > 0 {
return strings.Join(bad, ", ")
}
return fmt.Sprintf("all pairs ok, %d/%d/%d/%d m",
r.metres[0], r.metres[1], r.metres[2], r.metres[3])
}
func openModules(names [2]string) ([]*phyModule, [2]string, error) {
mods := make([]*phyModule, 0, 2)
var idents [2]string
for i, name := range names {
t, err := openSFF(name)
if err != nil {
return nil, idents, err
}
pn, err := t.vendorPN()
if err != nil {
return nil, idents, err
}
var dev phyDev
switch pn {
case fsVendorPN:
dev = newBCM(t)
case wiitekVendorPN:
dev = &rollball{sff: t}
default:
return nil, idents, fmt.Errorf("%s: unknown module PN %q", name, pn)
}
idents[i], err = dev.identify()
if err != nil {
return nil, idents, err
}
m := &phyModule{dev: dev}
// Born busy: the pollers stay silent through bringup's SETs and
// retrains until the first measure completes and lifts the gate.
m.busy.Store(true)
mods = append(mods, m)
}
return mods, idents, nil
}
func waitCarrier(names [2]string, done *atomic.Bool) (time.Duration, bool) {
start := time.Now()
deadline := start.Add(linkWaitSpan)
for {
if carrierUp(names[0]) && carrierUp(names[1]) {
return time.Since(start), true
}
if time.Now().After(deadline) || (done != nil && done.Load()) {
return time.Since(start), false
}
time.Sleep(linkWaitPoll)
}
}
// No trustworthy config readback exists (DATA1 is firmware scratch) and no
// 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})
}
// With a non-BCM partner the BCM is forced slave: the partner's manual
// config is unreachable, and auto-resolves-master against manual-slave is
// the combination proven to link.
mixed := false
for _, m := range mods {
if _, ok := m.dev.(*bcm); !ok {
mixed = true
}
}
master := !mixed
for i, m := range mods {
b, ok := m.dev.(*bcm)
if !ok {
out = append(out, checkResult{item: names[i] + " role", state: "auto"})
continue
}
res := checkResult{item: names[i] + " eee", state: "forced off"}
if err := b.forceEEEOff(); err != nil {
return fail(res.item, err)
}
out = append(out, res)
res = checkResult{item: names[i] + " jumbo", state: "forced on"}
if err := b.forceJumbo(); err != nil {
return fail(res.item, err)
}
out = append(out, res)
res = checkResult{item: names[i] + " role", state: "forced master"}
if !master {
res.state = "forced slave"
}
if err := b.forceRole(master); err != nil {
return fail(res.item, err)
}
master = false
out = append(out, res)
}
// 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]uint16
var advs [2]string
for i, m := range mods {
v, err := devEEEAdvert(m.dev)
if err != nil {
return fail(res.item, err)
}
adv[i] = v
advs[i] = fmt.Sprintf("%#04x", v)
if _, ok := m.dev.(*bcm); ok && v != 0 {
res.err = fmt.Errorf("%s still advertises EEE %#04x", names[i], v)
}
}
if res.err == nil && adv[0]&adv[1] != 0 {
res.err = fmt.Errorf("EEE would negotiate: common ability %#04x", adv[0]&adv[1])
}
res.state = advs[0] + "/" + advs[1]
out = append(out, res)
for i, m := range mods {
if err := devRestartAN(m.dev); err != nil {
return fail(names[i]+" retrain", err)
}
}
return out
}
func cableLine(c cableInfo) string {
return fmt.Sprintf("%s; map %s / %s",
verdictString(c.ecd), mapString(c.maps[0], c.haveMaps[0]), mapString(c.maps[1], c.haveMaps[1]))
}