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
46 changes: 46 additions & 0 deletions rp2-pio/piolib/all_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ var (
errBusy = errors.New("piolib:busy")

errDMAUnavail = errors.New("piolib:DMA channel unavailable")

// errAsyncRequiresDMA is returned by asynchronous Tx helpers when DMA is
// not enabled, since there is no non-blocking way to feed the TX FIFO
// from software alone.
errAsyncRequiresDMA = errors.New("piolib:async transfer requires DMA to be enabled")
)

//go:generate pioasm -o go parallel8.pio parallel8_pio.go
Expand Down Expand Up @@ -97,3 +102,44 @@ func helperPushUntilStall[T uint8 | uint16 | uint32](sm pio.StateMachine, dma dm
}
return nil
}

// helperPushStart begins an asynchronous, DMA-backed push of buf into the
// state machine's TX FIFO and returns immediately, without waiting for the
// transfer to complete. DMA must already be enabled on dma (see
// [dmaChannel.helperEnableDMA]); errAsyncRequiresDMA is returned otherwise.
// It is the caller's responsibility to ensure no other transfer is already
// in flight on sm/dma before calling helperPushStart; see [helperPushBusy].
//
// buf must not be modified, reused for another transfer, or allowed to go out
// of scope until the transfer completes (see [helperPushBusy] and
// [helperPushWait]), since the DMA engine reads directly from its backing
// array in the background.
func helperPushStart[T uint8 | uint16 | uint32](sm pio.StateMachine, dma dmaChannel, buf []T) error {
if !dma.helperIsEnabled() {
return errAsyncRequiresDMA
}
if len(buf) == 0 {
return nil // Nothing to do; TX-stalled flag already reads true.
}
sm.ClearTxStalled()
dreq := dmaPIO_TxDREQ(sm)
return dmaPushStart(dma, (*T)(unsafe.Pointer(sm.TxReg())), buf, dreq)
}

// helperPushBusy reports whether an asynchronous transfer started by
// [helperPushStart] is still in progress. Completion requires both the DMA
// channel to have finished moving data and the state machine to have drained
// its TX FIFO out to the pins (TX-stall), since the two can complete a few
// PIO cycles apart.
func helperPushBusy(sm pio.StateMachine, dma dmaChannel) bool {
return dma.busy() || !sm.HasTxStalled()
}

// helperPushWait blocks until an asynchronous transfer started by
// [helperPushStart] has fully completed, i.e. [helperPushBusy] returns false.
// It is safe to call even if no asynchronous transfer is pending.
func helperPushWait(sm pio.StateMachine, dma dmaChannel) {
for helperPushBusy(sm, dma) {
gosched()
}
}
33 changes: 23 additions & 10 deletions rp2-pio/piolib/dma.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,29 @@ func (ch dmaChannel) Push8(dst *byte, src []byte, dreq uint32) error {

// Push32 writes each element of src slice into the memory location at dst.
func dmaPush[T uint8 | uint16 | uint32](ch dmaChannel, dst *T, src []T, dreq uint32) error {
if err := dmaPushStart(ch, dst, src, dreq); err != nil {
return err
}
deadline := ch.dl.newDeadline()
for ch.busy() {
if deadline.expired() {
ch.abort()
return errTimeout
}
gosched()
}
ch.HW().CTRL_TRIG.ClearBits(rp.DMA_CH0_CTRL_TRIG_EN_Msk)
return nil
}

// dmaPushStart configures and triggers a DMA transfer of src into dst without
// waiting for it to complete. It returns once the transfer has been started
// (or with an error if the channel could not be safely reconfigured).
//
// The caller must not modify, reuse, or let src go out of scope until the
// transfer completes, since the DMA engine reads directly from its backing
// array. Use ch.busy to poll for, or block until, completion.
func dmaPushStart[T uint8 | uint16 | uint32](ch dmaChannel, dst *T, src []T, dreq uint32) error {
// If currently busy we wait until safe to edit hardware registers.
deadline := ch.dl.newDeadline()
for ch.busy() {
Expand Down Expand Up @@ -237,16 +260,6 @@ func dmaPush[T uint8 | uint16 | uint32](ch dmaChannel, dst *T, src []T, dreq uin

// We begin our DMA transfer here!
hw.CTRL_TRIG.Set(cc.CTRL)

deadline = ch.dl.newDeadline()
for ch.busy() {
if deadline.expired() {
ch.abort()
return errTimeout
}
gosched()
}
hw.CTRL_TRIG.ClearBits(rp.DMA_CH0_CTRL_TRIG_EN_Msk)
return nil
}

Expand Down
74 changes: 74 additions & 0 deletions rp2-pio/piolib/parallel.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ type Parallel struct {
sm pio.StateMachine
progOff uint8
dma dmaChannel
// asyncPending is true while a transfer started by Tx8Async/Tx16Async/
// Tx32Async has not yet been observed as complete by IsTxAsyncBusy or
// WaitTxAsync.
asyncPending bool
}

type ParallelConfig struct {
Expand Down Expand Up @@ -133,3 +137,73 @@ func (p6 *Parallel) IsDMAEnabled() bool {
func (p6 *Parallel) EnableDMA(enabled bool) error {
return p6.dma.helperEnableDMA(enabled)
}

// Tx32Async starts an asynchronous, DMA-backed transfer of the uint32 buffer
// to the PIO TX register and returns immediately, without waiting for the
// transfer to complete.
//
// EnableDMA(true) must have been called beforehand; Tx32Async returns an
// error if DMA is not enabled. It also returns an error, without starting a
// new transfer, if a previously started async transfer has not yet completed
// (see IsTxAsyncBusy and WaitTxAsync).
//
// data must not be modified, reused for another transfer, or allowed to be
// garbage collected until the transfer completes: the DMA engine reads
// directly from its backing array in the background. Use IsTxAsyncBusy to
// poll for completion, or WaitTxAsync to block until it is done, before
// touching data again or starting another transfer.
func (p6 *Parallel) Tx32Async(data []uint32) error { return parallelTxAsync(p6, data) }

// Tx16Async is the uint16 equivalent of Tx32Async. See Tx32Async for the full
// semantics and buffer-lifetime requirements.
func (p6 *Parallel) Tx16Async(data []uint16) error { return parallelTxAsync(p6, data) }

// Tx8Async is the uint8 equivalent of Tx32Async. See Tx32Async for the full
// semantics and buffer-lifetime requirements.
func (p6 *Parallel) Tx8Async(data []uint8) error { return parallelTxAsync(p6, data) }

// parallelTxAsync implements Tx8Async/Tx16Async/Tx32Async for any supported
// element type.
func parallelTxAsync[T uint8 | uint16 | uint32](p6 *Parallel, data []T) error {
if p6.asyncPending {
if helperPushBusy(p6.sm, p6.dma) {
return errBusy
}
p6.asyncPending = false
}
if err := helperPushStart(p6.sm, p6.dma, data); err != nil {
return err
}
p6.asyncPending = len(data) != 0
return nil
}

// IsTxAsyncBusy reports whether a transfer started by Tx8Async, Tx16Async, or
// Tx32Async is still in progress. Completion requires both the DMA channel to
// have finished moving data into the PIO TX FIFO and the state machine to
// have finished shifting that data out to the pins, since the two complete a
// few PIO cycles apart; IsTxAsyncBusy accounts for both.
//
// It is safe to call at any time, including when no async transfer was ever
// started, in which case it returns false.
func (p6 *Parallel) IsTxAsyncBusy() bool {
if !p6.asyncPending {
return false
}
if helperPushBusy(p6.sm, p6.dma) {
return true
}
p6.asyncPending = false
return false
}

// WaitTxAsync blocks until a transfer started by Tx8Async, Tx16Async, or
// Tx32Async has fully completed, i.e. until IsTxAsyncBusy would return false.
// It is safe to call even if no async transfer is currently pending.
func (p6 *Parallel) WaitTxAsync() {
if !p6.asyncPending {
return
}
helperPushWait(p6.sm, p6.dma)
p6.asyncPending = false
}
Loading