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 }