Files
cabletest/rollball.go
T

187 lines
4.7 KiB
Go
Raw Normal View History

package main
import (
"fmt"
"strings"
"time"
)
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)
}
// The µC can be mid-service of an earlier session's command when this one is
// issued: it completes the old one, leaving DONE and a stale value in the
// block, and a status sample taken before the new command commits reads them
// as this command's (seen live: PHY ID high word answered by an orphaned 7.60
// read). So status is never sampled before a full poll gap, the value rides
// in the same block read as the status, and a completion only counts when the
// block echoes this command's devad/reg.
func (r *rollball) mbox(cmd byte, devad, reg, val uint16) (out [2]byte, err error) {
if err = r.unlock(); err != nil {
return
}
if err = r.i2cWrite(rbOffDevad, byte(devad), byte(reg>>8), byte(reg)); err != nil {
return
}
if cmd == rbCmdWrite {
if err = r.i2cWrite(rbOffValHi, byte(val>>8), byte(val)); err != nil {
return
}
}
if err = r.i2cWrite(rbOffCmd, cmd); err != nil {
return
}
deadline := time.Now().Add(rbCmdTimeout)
for {
time.Sleep(rbCmdPoll)
var d []byte
if d, err = r.i2cRead(rbOffCmd, 6); err != nil {
return
}
if d[0] == rbCmdDone && d[1] == byte(devad) &&
d[2] == byte(reg>>8) && d[3] == byte(reg) {
out[0], out[1] = d[4], d[5]
return
}
if time.Now().After(deadline) {
err = fmt.Errorf("%s: mailbox %d.%#04x stuck at %#02x", r.ifname, devad, reg, d[0])
return
}
}
}
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)
d, err := r.mbox(rbCmdRead, devad, reg, 0)
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)
_, err := r.mbox(rbCmdWrite, devad, reg, val)
return err
}
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
}