Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
*.bin
*.hex

# Local files.
**/local*

# Test binary, built with `go test -c`
*.test

Expand Down
94 changes: 56 additions & 38 deletions rp2-pio/piolib/rmii-rx-extclk.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ type RMIIRxConfig struct {
IRQ uint8
// IRQSource is the triggering source for state machine. Varies between 0..3 on RP2040 and extends to 0..7 on RP2350.
IRQSourceIndex uint8
// RefClk is the pin to REFCLK a.k.a RETCLK or EXTCLK. This is the external clock we synchronize with.
RefClk machine.Pin
}

// RMIIRx is a PIO-based RMII receiver. It samples RX0, RX1 and CRS_DV at
// RMIIRxExtClk is a PIO-based RMII receiver. It samples RX0, RX1 and CRS_DV at
// the RMII clock rate and delivers complete frames via an IRQ-driven callback.
type RMIIRx struct {
type RMIIRxExtClk struct {
sm pio.StateMachine
rxOff uint8
dma dmaChannel
Expand All @@ -37,40 +39,55 @@ type RMIIRx struct {
}

// Configure sets up the PIO state machine and DMA for RMII reception.
func (r *RMIIRx) Configure(PIO *pio.PIO, cfg RMIIRxConfig) error {
// The Baud field in cfg is ignored; PIO runs at the CPU clock frequency.
func (r *RMIIRxExtClk) Configure(PIO *pio.PIO, cfg RMIIRxConfig) error {
if cfg.IRQSourceIndex > 3 {
return errors.New("IRQSource index out of range (0-7)")
} else if cfg.RefClk == 0 || cfg.RefClk == machine.NoPin {
return errors.New("need a clock that is not GP0")
}

whole, frac, err := pio.ClkDivFromFrequency(cfg.Baud, machine.CPUFrequency())
if err != nil {
return err
// PIO runs at full CPU clock. Compute timing from CPU frequency and 50 MHz RMII clock.
const rmiiClk = 50_000_000 // 50 MHz RMII dibit clock.
cpuFreq := machine.CPUFrequency()
if cpuFreq%rmiiClk != 0 {
return errors.New("CPU frequency must be a multiple of 50 MHz")
}
txPhase := cpuFreq / rmiiClk // PIO cycles per RMII dibit period.
if txPhase < 2 || txPhase > 6 {
return errors.New("CPU frequency out of range (need 100-300 MHz)")
}

rxBit := txPhase - 2 // Inter-sample delay.
rxD0 := txPhase - 1 // SOF alignment delay.

const (
idxRX0 = iota
idxRX1
idxCRSDV

polRising = true
labelLoop = 2
polRising = true
labelSample = 4
labelS2 = 6
)

/*
Copyright (c) 2026 Patricio Whittingslow, with portions copyrighted as below
Copyright (c) 2025 Rob Scott
Copyright (c) 2021 Sandeep Mistry
*/
asm := pio.AssemblerV0{SidesetBits: 0}
var rxprog = [...]uint16{
/*
Copyright (c) 2026 Patricio Whittingslow, with portions copyrighted as below
Copyright (c) 2025 Rob Scott
Copyright (c) 2021 Sandeep Mistry
*/
asm.WaitPin(polRising, idxCRSDV).Encode(),
asm.WaitPin(polRising, idxRX1).Delay(1).Encode(), // Delay modified from rscott version, yields better results.
labelLoop:// main read loop while CRSDV is high at byte boundary.
asm.In(pio.InSrcPins, 2).Encode(),
asm.Jmp(pio.JmpPinInput, labelLoop).Encode(),
// Pull in another dibit just in case we desynced by a tidbit. If no desync happened is itty bitty harmless.
// CRSDV Toggling in 10M mode may require more logic here to check DV status. See https://github.com/soypat/lneto/blob/main/phy/rmii.md
asm.In(pio.InSrcPins, 2).Encode(),
asm.IRQSet(false, cfg.IRQSourceIndex).Encode(),
asm.WaitPin(polRising, idxCRSDV).Encode(), // Wait for CRS_DV assertion.
asm.Set(pio.SetDestX, 0).Encode(), // Init dibit counter.
asm.WaitPin(polRising, idxRX1).Delay(uint8(rxD0)).Encode(), // Wait for SOF + alignment delay.
asm.WaitGPIO(false, uint8(cfg.RefClk)).Encode(), // Sync to RMII clock falling edge.
labelSample: // Sample loop (2 dibits per iteration).
asm.In(pio.InSrcPins, 2).Delay(uint8(rxBit)).Encode(), // Sample dibit.
asm.Jmp(pio.JmpXNZeroDec, labelS2).Encode(), // Timing spacer + counter decrement.
labelS2: asm.In(pio.InSrcPins, 2).Delay(uint8(rxBit)).Encode(), // Sample dibit.
asm.Jmp(pio.JmpPinInput, labelSample).Encode(), // Loop while CRS_DV high.
asm.IRQSet(false, cfg.IRQSourceIndex).Encode(), // Signal end of frame.
}
rxSM, err := PIO.ClaimStateMachine()
if err != nil {
Expand All @@ -82,28 +99,29 @@ func (r *RMIIRx) Configure(PIO *pio.PIO, cfg RMIIRxConfig) error {
return err
}

// Configure RX pins as inputs
// Configure RX pins as inputs.
rxPin := cfg.RxBase
pinCfg := machine.PinConfig{Mode: PIO.PinMode()}
for i := machine.Pin(0); i < 3; i++ {
pin := rxPin + i
pin.Configure(pinCfg)
}
// Create state machine configuration
// Create state machine configuration.
rxcfg := asm.DefaultStateMachineConfig(rxoff, rxprog[:])
rxcfg.SetInPins(rxPin, 2) // IN pins: RX0, RX1 at rxPin
rxcfg.SetJmpPin(rxPin + idxCRSDV) // JMP pin: CRS_DV at rxPin+2
rxcfg.SetInShift(true, true, 8) // In shift: right shift, autopush enabled, threshold 8 bits (1 byte)
rxcfg.SetInPins(rxPin, 2) // IN pins: RX0, RX1 at rxPin.
rxcfg.SetJmpPin(rxPin + idxCRSDV) // JMP pin: CRS_DV at rxPin+2.
rxcfg.SetInShift(true, true, 8) // Right shift, autopush at 8 bits (1 byte).
rxcfg.SetFIFOJoin(pio.FifoJoinRx)
rxcfg.SetClkDivIntFrac(whole, frac)
// Initialize SM at start of program
rxcfg.SetClkDivIntFrac(1, 0) // Run at CPU clock.
// Initialize SM at start of program.
rxSM.Init(rxoff, rxcfg)
// Set RX pins as inputs (pindirs = 0 for input)
// Set RX pins as inputs (pindirs = 0 for input).
var rxPinMsk uint32 = 0b111 << rxPin
rxSM.SetPindirsMasked(0, rxPinMsk)

// Optional: bypass input synchronizers for lower latency
PIO.SetInputSyncBypassMasked(rxPinMsk, rxPinMsk)
// Bypass input synchronizers for lower latency on RX pins and RefClk.
syncBypassMsk := rxPinMsk | (1 << cfg.RefClk)
PIO.SetInputSyncBypassMasked(syncBypassMsk, syncBypassMsk)
r.dma.helperEnableDMA(true)
r.sm = rxSM
r.rxOff = rxoff
Expand All @@ -120,7 +138,7 @@ func (r *RMIIRx) Configure(PIO *pio.PIO, cfg RMIIRxConfig) error {
return nil
}

func (r *RMIIRx) irqhandler(pioblock, irqLine uint8, source pio.IRQSource) {
func (r *RMIIRxExtClk) irqhandler(pioblock, irqLine uint8, source pio.IRQSource) {
// Halt ongoing rx async logic to wait for a future StartRx call.
r.sm.SetEnabled(false)
r.block = pioblock
Expand All @@ -134,7 +152,7 @@ func (r *RMIIRx) irqhandler(pioblock, irqLine uint8, source pio.IRQSource) {
}

// StopRx halts reception, aborts any in-flight DMA, and resets the state machine.
func (r *RMIIRx) StopRx() error {
func (r *RMIIRxExtClk) StopRx() error {
if !r.sm.IsEnabled() {
return errors.New("already stopped")
}
Expand All @@ -149,7 +167,7 @@ func (r *RMIIRx) StopRx() error {
}

// StartRx begins frame reception. SetRxIRQHandler must be called first.
func (r *RMIIRx) StartRx() error {
func (r *RMIIRxExtClk) StartRx() error {
if len(r.rxbuf) == 0 {
return errors.New("no rxbuf configured")
} else if r.sm.IsEnabled() {
Expand Down Expand Up @@ -186,7 +204,7 @@ func (r *RMIIRx) StartRx() error {

// SetRxIRQHandler sets the receive buffer and callback invoked from the IRQ
// handler when a frame ends (CRS_DV deasserts). Stops any ongoing reception.
func (r *RMIIRx) SetRxIRQHandler(rxbuf []byte, callback func(buf []byte)) error {
func (r *RMIIRxExtClk) SetRxIRQHandler(rxbuf []byte, callback func(buf []byte)) error {
// Terminate ongoing transaction since we are tinkering with DMA target.
r.StopRx()
r.rxbuf = rxbuf
Expand All @@ -195,11 +213,11 @@ func (r *RMIIRx) SetRxIRQHandler(rxbuf []byte, callback func(buf []byte)) error
}

// ReceivedSinceStartRx returns true if a frame has been received since the last StartRx call.
func (r *RMIIRx) ReceivedSinceStartRx() bool {
func (r *RMIIRxExtClk) ReceivedSinceStartRx() bool {
return r.block != 0xff || r.irq != 0xff || r.irqsource != 0xffff_ffff
}

// InRx returns true if the receiver state machine is currently enabled.
func (r *RMIIRx) InRx() bool {
func (r *RMIIRxExtClk) InRx() bool {
return r.sm.IsEnabled()
}
4 changes: 3 additions & 1 deletion rp2-pio/piolib/rmii-tx-extclk.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type RMIITxConfig struct {
TxBuffer []byte
// TxBase is the first pin of the consecutive, ordered set [TX0,TX1,TXEN]
TxBase machine.Pin
// RefClk is the pin to REFCLK a.k.a RETCLK or EXTCLK. This is the external clock we synchronize with.
RefClk machine.Pin
}

Expand Down Expand Up @@ -134,7 +135,8 @@ func (r *RMIITxExtClk) Configure(PIO *pio.PIO, cfg RMIITxConfig) error {
// Idle bus and Inter-Packet Gap (12 byte times total).
asm.Set(pio.SetDestPins, 0b000).Delay(uint8(txByte - 1)).Encode(), // 1 byte.
asm.Set(pio.SetDestX, 9).Encode(), // 10-iteration loop.
labelIPG: asm.Jmp(pio.JmpXNZeroDec, labelIPG).Delay(uint8(txByte)).Encode(), // 10 bytes.
labelIPG:// 10 bytes.
asm.Jmp(pio.JmpXNZeroDec, labelIPG).Delay(uint8(txByte)).Encode(),
// .wrap → back to pull block (1 byte).
}

Expand Down