Skip to content

rp2-pio/piolib: add nonblocking DMA transfer API to Parallel - #61

Draft
davecheney wants to merge 1 commit into
tinygo-org:mainfrom
davecheney:davecheney-async-parallel-dma
Draft

davecheney wants to merge 1 commit into
tinygo-org:mainfrom
davecheney:davecheney-async-parallel-dma

Conversation

@davecheney

@davecheney davecheney commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a nonblocking DMA transfer API to rp2-pio/piolib.Parallel so callers can start a DMA-backed parallel bus transfer and later poll or wait for completion, instead of blocking for the whole transfer as Tx8/Tx16/Tx32 do today.

This allows display and frame drivers (such as parallel ST7789 displays on RP2040 boards like the Tufty 2040) to start a frame's parallel DMA transfer and continue CPU computation while the previous frame is still being clocked out over the 8-bit bus.

New API

func (p6 *Parallel) Tx8Async(data []uint8) error
func (p6 *Parallel) Tx16Async(data []uint16) error
func (p6 *Parallel) Tx32Async(data []uint32) error
func (p6 *Parallel) IsTxAsyncBusy() bool
func (p6 *Parallel) WaitTxAsync()

Semantics

  • Requires EnableDMA(true) beforehand; otherwise returns errAsyncRequiresDMA.
  • Starts the DMA transfer and returns immediately (no PIO TX-stall wait).
  • If a previously started async transfer hasn't completed yet, returns errBusy rather than reconfiguring DMA/PIO registers out from under the in-flight transfer.
  • "Complete" requires both the DMA channel to be idle and the PIO TX FIFO to have stalled (fully drained out to the pins), since these finish a few PIO cycles apart. IsTxAsyncBusy/WaitTxAsync account for both, so callers can safely release chip-select or reuse/free the buffer once either reports done.
  • data must not be modified, reused for another transfer, or garbage collected until completion is observed, since the DMA engine reads directly from its backing array in the background.
  • Existing Tx8/Tx16/Tx32 behavior and timing are unchanged: dmaPush is refactored into a new nonblocking dmaPushStart (configure + trigger) plus a wait loop, and the blocking methods just call the same helper as before.

Validation

  • tinygo build -target=tufty2040 ./rp2-pio/examples/parallel/tufty — passes
  • tinygo build -target=pico2 ./rp2-pio/examples/parallel/tufty — passes (RP2350)
  • go test ./rp2-pio/... — passes for the host-buildable core package
  • gofmt -l — clean on touched files
  • Validated on real Tufty 2040 hardware with an 8-bit parallel ST7789 display: async overlap reduced draw-path wait from ~10.36ms (synchronous) to ~27µs wait + ~123µs to start the next transfer, while CPU processing continues on a single core (no -scheduler=cores needed).

Add Tx8Async/Tx16Async/Tx32Async plus IsTxAsyncBusy/WaitTxAsync to
piolib.Parallel so callers can start a DMA-backed parallel bus
transfer and poll or wait for completion later, instead of blocking
for the whole transfer as Tx8/Tx16/Tx32 do.

- dmaPush is split into dmaPushStart (configure + trigger, no wait)
  and the existing blocking dmaPush (dmaPushStart + wait), so Tx8/
  Tx16/Tx32 behavior and timing are unchanged.
- helperPushStart/helperPushBusy/helperPushWait in all_generate.go
  provide the async push/poll/wait primitives shared by the *Async
  methods.
- Parallel tracks asyncPending so a new async transfer started while
  one is still in flight returns errBusy instead of corrupting DMA/PIO
  state; IsTxAsyncBusy/WaitTxAsync clear it once the DMA channel is
  idle and the PIO TX FIFO has stalled (fully drained to the pins).
- Starting an async transfer without DMA enabled returns
  errAsyncRequiresDMA.
@davecheney
davecheney marked this pull request as draft September 11, 2026 03:15
@soypat

soypat commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

What's the outlook on this? At a glance I feel like I would have approached this with a boolean flag on dmaChannel so the code can be more efficently reused and also simplified greatly

func dmaPushHelper[]( ... , async bool) {
 ...
 dma.async = async
 if !async {
     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)
 }
}

func (dma) AsyncPending() bool {
   if !dma.async {
       return false
   }
   dma.async = dma.isTxBusy()
   return dma.async
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants