diff --git a/phy/mdio-bitbang.go b/phy/mdio-bitbang.go new file mode 100644 index 0000000..29a1b41 --- /dev/null +++ b/phy/mdio-bitbang.go @@ -0,0 +1,187 @@ +package phy + +import ( + "errors" +) + +var _ MDIOBus = (*MDIOBitBang)(nil) // compile time guarantee of interface implementation. + +const ( + mdioRead = 0b10 + mdioWrite = 0b01 + miaddrc45 = 1 << 30 + c45bit = 1 << 15 + c45Addr = c45bit | 0b00 + c45Read = c45bit | 0b11 + c45Write = c45bit | 0b01 +) + +// MDIOBitBang provides a software defined(bitbang) MDIO/MDC management interface for PHY register access +// as the STA (Management station, this implementation) which communicates to the PHY (Physical layer device). +// Inspired by linux/v3.13.1/source/drivers/net/phy/mdio-bitbang.c +// Below is a TinyGo oriented HAL needed to use MDIOBitBang. MDC is clock line, MDIO is data line. +// +// const mdioDelay = 340 * time.Nanosecond // MDIO spec max turnaround time +// pinMDIO.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) +// pinMDC.Configure(machine.PinConfig{Mode: machine.PinOutput}) +// pinMDC.Low() +// var mdio2 phy.MDIOBitBang +// mdio2.Configure(func(outBit bool) { +// // sendBit: set data, clock high, clock low +// if outBit { +// pinMDIO.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) +// } else { +// pinMDIO.Low() +// pinMDIO.Configure(machine.PinConfig{Mode: machine.PinOutput}) +// } +// time.Sleep(mdioDelay) +// pinMDC.High() +// time.Sleep(mdioDelay) +// pinMDC.Low() +// }, func() (inBit bool) { +// // getBit: clock high, read, clock low +// time.Sleep(mdioDelay) +// pinMDC.High() +// time.Sleep(mdioDelay) +// pinMDC.Low() +// return pinMDIO.Get() +// }, func(setOut bool) { +// // setDir: configure pin direction +// if setOut { +// pinMDIO.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) +// } else { +// pinMDIO.Configure(machine.PinConfig{Mode: machine.PinInput}) +// } +// }) +type MDIOBitBang struct { + _sendBit func(bit bool) + _getBit func() (inputBit bool) + _setDir func(output bool) +} + +// Configure initializes the MDIO bit-bang interface with the given pin control callbacks. +func (m *MDIOBitBang) Configure(sendBit func(bit bool), getBit func() bool, setDir func(setOut bool)) { + if sendBit == nil || getBit == nil || setDir == nil { + panic("nil callback") + } + m._getBit = getBit + m._sendBit = sendBit + m._setDir = setDir + m.reset() +} + +func (m *MDIOBitBang) reset() { + // setting direction to output releases the bus. + m.setDir(true) +} + +// Read reads a PHY register. Uses Clause 45 framing if devAddr is non-zero. +func (m *MDIOBitBang) Read(phyAddr, devAddr uint8, regAddr uint16) (uint16, error) { + isC45 := devAddr != 0 + if isC45 { + m.cmdAddr2(phyAddr, devAddr, regAddr) + m.cmd(c45Read, phyAddr, devAddr) + } else { + m.cmd(mdioRead, phyAddr, uint8(regAddr)) + } + m.setDir(false) + // Check turnaround bit, PHY should drive it to zero. + if m.getBit() { + // PHY did not drive low, as would be expected. + // Ensure flush: + for range 32 { + m.getBit() + } + return 0xffff, errors.New("PHY did not drive turnaround low") + } + ret := m.getNum(16) + m.getBit() + return ret, nil +} + +// Write writes a value to a PHY register. Uses Clause 45 framing if devAddr is non-zero. +func (m *MDIOBitBang) Write(phyAddr, devAddr uint8, regAddr, value uint16) error { + isC45 := devAddr != 0 + if isC45 { + m.cmdAddr2(phyAddr, devAddr, regAddr) + m.cmd(c45Write, phyAddr, devAddr) + } else { + m.cmd(mdioWrite, phyAddr, uint8(regAddr)) + } + // send turnaround (10) + m.sendBit(true) + m.sendBit(false) + + m.sendNum(value, 16) + m.setDir(false) + m.getBit() + return nil +} + +func (m *MDIOBitBang) cmdAddr2(phy, dev uint8, reg uint16) { + m.cmd(c45Addr, phy, dev) + // turnaround 10. + m.sendBit(true) + m.sendBit(false) + + m.sendNum(reg, 16) + m.setDir(false) + m.getBit() +} + +func (m *MDIOBitBang) cmd(op uint16, phy uint8, reg uint8) { + const writeDir = true + m.setDir(writeDir) + // Preamble, 32 bits of 1. + for range 32 { + m.sendBit(true) + } + // Start of frame: 01 + // Clause 45 op uses 00=start, 11=read, 10=write + m.sendBit(false) + m.sendBit(op&c45bit == 0) + + m.sendBit((op>>1)&1 != 0) + m.sendBit((op>>0)&1 != 0) + m.sendNum(uint16(phy), 5) + m.sendNum(uint16(reg), 5) +} + +func (m *MDIOBitBang) sendNum(val uint16, bits int) { + for i := bits - 1; i >= 0; i-- { + m.sendBit((val>>i)&1 != 0) + } +} + +func (m *MDIOBitBang) getNum(bits int) (ret uint16) { + for i := bits - 1; i >= 0; i-- { + ret <<= 1 + ret |= uint16(b2u8(m.getBit())) + } + return ret +} + +// MDIO low-level clock operations +// Reference: https://github.com/sandeepmistry/pico-rmii-ethernet/blob/main/examples/httpd/main.c +// Reference: netif_rmii_ethernet_mdio_clock_out() and netif_rmii_ethernet_mdio_clock_in() +// from rmii_ethernet.c + +// setDir configures pins preparing for write/read operations. +func (m *MDIOBitBang) setDir(outWrite bool) { + m._setDir(outWrite) +} + +func (m *MDIOBitBang) sendBit(b bool) { + m._sendBit(b) +} + +func (m *MDIOBitBang) getBit() bool { + return m._getBit() +} + +func b2u8(b bool) uint8 { + if b { + return 1 + } + return 0 +} diff --git a/phy/rmii.md b/phy/rmii.md index 5072d0a..d792062 100644 --- a/phy/rmii.md +++ b/phy/rmii.md @@ -40,6 +40,36 @@ All RMII signals are synchronous to a continuous 50 MHz reference clock. Two mod ## Tx0/Tx1/TxEN and CLKREF/RETCLK interface + +### Frame Transmission Sequence +First 8 bytes in transmission are preamble and start of frame delimiter (SFD). +The preamble is composed of 7 bytes, all dibits valued 0b01, so TX0=1, TX1=0. +The SFD is composed of 3 0b01 dibits and a 0b11 dibit where both TX0 and TX1 are high for a single CLKREF cycle. After the final SFD(0b11) dibit the frame data is presented of the wire. + +TX_EN is asserted synchronously with the first dibit of preamble and remains HIGH throughout the entire frame (preamble, SFD, payload, CRC). + +Example at 100M link mode: +``` +REF_CLK: _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_ ... +TX_EN: __|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ... (HIGH until end of frame) +TX0: __|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾...‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|.D ... +TX1: _______________________________...____________|‾‾‾|.A ... +TXD[1:0]: 00|01 |01 |01 |01 |01 |01 |01 |...|01 |01 |01 |11 |DA|TA|... + └───────────── Preamble (28 dibits) ─────────┘└SFD┘└─ Frame data ─... +``` + + +**Inter-Packet Gap (IPG)**: After TX_EN deasserts, TXD[1:0] must be held at 00 for a minimum of 96 bit times (48 dibits = 12 bytes at 100M). This is the minimum gap required between consecutive frame transmissions. + +``` +End of frame with IPG at 100M link mode: + +REF_CLK: _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_ ... +TX_EN: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|_____________________________ ... +TXD[1:0]: ...data...|CR|C |00|00|00|00|00|00|00|00|... + └CRC┘ └──── IPG (≥48 dibits) ────... +``` + ### Practical Tx example: To illustrate programatically we'll suppose we have a hardware which requires a byte for every clock. Each byte contains 3 bits to be sent out: Tx0,Tx1,TxEn bits. This is not a contrived example, it is how Sandeep Mistry's and Rob Scott's LAN8720 drivers work.