Files
cabletest/phy.go
T

591 lines
13 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"
"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, 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
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()
}
// 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
}
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
}
// 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.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 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.busy.Store(true)
}
defer func() {
for _, m := range mods {
m.forgive()
m.busy.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
}
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
// While set, every failure counter is suppressed at its source: the
// measure's own link blip is never counted anywhere, rather than counted,
// hidden and reverted.
measuring atomic.Bool
mu sync.Mutex
info cableInfo
}
func newCableDiag(mods []*phyModule, info cableInfo) *cableDiag {
return &cableDiag{mods: mods, info: info}
}
func (c *cableDiag) snapshot() (cableInfo, bool) {
c.mu.Lock()
defer c.mu.Unlock()
return c.info, c.measuring.Load()
}
// 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.measuring.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
c.mu.Unlock()
c.measuring.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)
}
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) {
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, 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
}