Files
cabletest/phy.go
T

691 lines
16 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"
"slices"
"sync"
"sync/atomic"
"time"
)
const (
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 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, bool, 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
}
const (
phyInterval = time.Second
phyStale = 5 * time.Second
phyMaxDark = 30
linkWaitSpan = 25 * time.Second
linkWaitPoll = time.Second
// The PHY core wakes within 0.25 s of the port raise (bench-measured:
// BCM instant, Wiitek 0.07-0.24 s across cycles).
moduleUpWait = 3 * time.Second
moduleUpPoll = 100 * time.Millisecond
snrGoodMargin = 3.0
snrWarnMargin = 1.0
)
// While masked a measure owns the module: every register it would poll is the
// measure's own disturbance, so nothing is read at all.
type phyModule struct {
dev phyDev
masked 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
}
func (m *phyModule) poll(hist *history) error {
if m.masked.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, haveSNR, err = src.snrMargins(); err != nil {
return err
}
}
blocks, ber, pcsRaw, err := devPCSLatch(m.dev)
if err != nil {
return err
}
count, frRaw, err := devFastRetrain(m.dev)
if err != nil {
return err
}
// Setting masked is a measure's first act, so masked still clear here
// proves every read above finished before the measure disturbed anything;
// a measure that began mid-read makes the samples ambiguous, and forgive()
// re-baselines after it anyway.
if m.masked.Load() {
return nil
}
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
hist.addCorrected(time.Now(), 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, hist *history) {
tick := time.NewTicker(phyInterval)
defer tick.Stop()
dark := 0
var lastErr error
for !done.Load() {
<-tick.C
if err := m.poll(hist); 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()
}
// Latches drained after a measure belong to its blip: un-priming makes the
// next poll an origin only, without discarding the pre-measure totals.
func (m *phyModule) forgive() {
m.mu.Lock()
m.primed = false
m.recentDelta = 0
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
}
// The median across the pairs rather than the mean: the pairs run the same
// cable, so one pair's skewed TDR estimate is an outlier, not signal. Always
// a value some pair actually measured — the lower middle when the count is
// even, never an average.
func (c cableInfo) metresValue() (int, bool) {
var ok []int
for i, v := range c.ecd.verdicts {
if v == pairOK {
ok = append(ok, c.ecd.metres[i])
}
}
if len(ok) == 0 {
return 0, false
}
slices.Sort(ok)
return ok[(len(ok)-1)/2], true
}
func (c cableInfo) metresString() string {
m, ok := c.metresValue()
if !ok {
return "-"
}
return fmt.Sprintf("%d", m)
}
// One value per completed healthy measure; the running median steadies as
// they accumulate, and like every median here it is always a value that was
// actually measured.
type cableLengths struct {
vals []int
}
func (l *cableLengths) add(m int) {
l.vals = append(l.vals, m)
}
func (l cableLengths) median() (int, bool) {
if len(l.vals) == 0 {
return 0, false
}
s := slices.Clone(l.vals)
slices.Sort(s)
return s[(len(s)-1)/2], true
}
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
}
// 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
}
// The whole cable verdict in one cell: the length when healthy, the first
// faulted pair's verdict when not, "xover" for a pair swap.
func cableSummary(cable cableInfo, measuring bool) (string, int) {
if measuring {
return "-", clsNone
}
anyData, anySwap := false, false
for i, v := range cable.ecd.verdicts {
if v != 0 {
anyData = true
}
if v != 0 && v != pairOK {
return pairVerdicts[v], clsBad
}
if pairSwapped(i, cable) {
anySwap = true
}
}
switch {
case !anyData:
return "-", clsNone
case anySwap:
return "xover", clsWarn
}
return cable.metresString(), clsGood
}
// The margin is the worst pair across the ends that measure SNR (the Wiitek's
// IEEE 1.133–136), gated on the whole pair being fresh and linked.
func phyDisplayFrom(cable cableInfo, lens cableLengths, 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)
// The running median outlives moments without a fresh verdict — a
// running measure, or one that returned no data — and only an actual
// fault verdict replaces it.
if m, ok := lens.median(); ok && (d.metresClass == clsGood || d.metresClass == clsNone) {
d.metres, d.metresClass = fmt.Sprintf("%d", m), clsGood
}
return d
}
type ecdResult struct {
verdicts [4]int
metres [4]int
}
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, done *atomic.Bool) (cableInfo, error) {
for _, m := range mods {
m.masked.Store(true)
}
defer func() {
for _, m := range mods {
m.forgive()
m.masked.Store(false)
}
}()
var c cableInfo
var err error
end := bcmEnd(mods)
c.ecd, err = end.cableDiag()
if err != nil {
return c, err
}
if err = devRestartAN(end); err != nil {
return c, err
}
// A pair the diag just called faulted is not going to relink; waiting the
// full carrier timeout would hold the verdict hostage for 25 s on an
// unplugged cable. A verdict of no data is not a fault: the restart above
// took a working link down, and returning before carrier drops the
// suppression while it is still retraining.
faulted := false
for _, v := range c.ecd.verdicts {
if v != 0 && v != pairOK {
faulted = true
}
}
if !faulted {
waitCarrier([2]string{mods[0].dev.name(), mods[1].dev.name()}, 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, err
}
c.haveMaps[i] = true
}
return c, nil
}
type cableDiag struct {
mods []*phyModule
// The mask itself. While set, every fault delta is dropped at the mask
// gate before it can reach an aggregate: the measure's own link blip is
// never counted anywhere, rather than counted, hidden and reverted.
masked atomic.Bool
mu sync.Mutex
info cableInfo
lengths cableLengths
}
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
return &cableDiag{mods: mods, info: info}
}
func (c *cableDiag) snapshot() (cableInfo, cableLengths, bool) {
c.mu.Lock()
defer c.mu.Unlock()
return c.info, c.lengths, c.masked.Load()
}
func (c *cableDiag) resetLengths() {
c.mu.Lock()
c.lengths = cableLengths{}
c.mu.Unlock()
}
// A measure that errors means the diag transport or the µC is broken, and a
// tester that cannot run its diagnostics must not keep testing: the failure is
// fatal, and the reboot's driver reload is also the wedged-µC recovery.
func (c *cableDiag) kick(done *atomic.Bool) bool {
if !c.masked.CompareAndSwap(false, true) {
return false
}
go func() {
defer holdPanic()
info, err := measureCable(c.mods, done)
if err != nil {
panic(fmt.Sprintf("cable measure: %v", err))
}
c.mu.Lock()
c.info = info
if _, cls := cableSummary(info, false); cls == clsGood {
if m, ok := info.metresValue(); ok {
c.lengths.add(m)
}
}
c.mu.Unlock()
// The judge can charge a masked-era loss up to one deadline after the
// wire went quiet, so the mask outlives the measure by that window —
// universally, rather than each source tracking its own tail.
time.Sleep(lossDeadline)
c.masked.Store(false)
}()
return true
}
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)
}
// The port was just raised (the host checks run first): TX_DISABLE
// deasserts and the module re-powers its PHY core, which answers
// 0xffff through a working mailbox until it is back. Identify
// settles bounded while the core wakes.
deadline := time.Now().Add(moduleUpWait)
for {
idents[i], err = dev.identify()
if err == nil {
break
}
if time.Now().After(deadline) {
return nil, idents, err
}
time.Sleep(moduleUpPoll)
}
m := &phyModule{dev: dev}
// Born masked: the pollers stay silent through bringup's SETs and
// retrains until the first measure completes and lifts the gate.
m.masked.Store(true)
mods = append(mods, m)
}
return mods, idents, nil
}
// Down reads as EINVAL rather than zero, and either way the answer is the
// same: no carrier here now.
func carrierUp(name string) bool {
v, ok := readUint("/sys/class/net/" + name + "/carrier")
return ok && v == 1
}
func waitCarrier(names [2]string, done *atomic.Bool) {
deadline := time.Now().Add(linkWaitSpan)
for !(carrierUp(names[0]) && carrierUp(names[1])) {
if time.Now().After(deadline) || done.Load() {
return
}
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) []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 _, m := range mods {
b, ok := m.dev.(*bcm)
if !ok {
out = append(out, checkResult{item: m.dev.name() + " role", state: "auto"})
continue
}
res := checkResult{item: m.dev.name() + " eee", state: "forced off"}
if err := b.forceEEEOff(); err != nil {
return fail(res.item, err)
}
out = append(out, res)
res = checkResult{item: m.dev.name() + " jumbo", state: "forced on"}
if err := b.forceJumbo(); err != nil {
return fail(res.item, err)
}
out = append(out, res)
res = checkResult{item: m.dev.name() + " 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", m.dev.name(), 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 _, m := range mods {
if err := devRestartAN(m.dev); err != nil {
return fail(m.dev.name()+" retrain", err)
}
}
return out
}