rp2040: use DMA for send-only SPI transfers

This improves slightly. It also is some groundwork for better DMA
support in TinyGo in the future.

I'm not entirely sure why it improves performance (in theory the old
code should already saturate the SPI bus) but it does, so 🤷
Этот коммит содержится в:
Ayke van Laethem 2023-04-01 22:08:48 +02:00 коммит произвёл Ron Evans
родитель ad3e9e1a77
коммит 3b4e543f4e
2 изменённых файлов: 66 добавлений и 7 удалений

Просмотреть файл

@ -4,6 +4,7 @@ package machine
import ( import (
"device/rp" "device/rp"
"runtime/volatile"
"unsafe" "unsafe"
) )
@ -114,3 +115,23 @@ func ChipVersion() uint8 {
version := (chipID & SYSINFO_CHIP_ID_REVISION_BITS) >> SYSINFO_CHIP_ID_REVISION_LSB version := (chipID & SYSINFO_CHIP_ID_REVISION_BITS) >> SYSINFO_CHIP_ID_REVISION_LSB
return uint8(version) return uint8(version)
} }
// Single DMA channel. See rp.DMA_Type.
type dmaChannel struct {
READ_ADDR volatile.Register32
WRITE_ADDR volatile.Register32
TRANS_COUNT volatile.Register32
CTRL_TRIG volatile.Register32
_ [12]volatile.Register32 // aliases
}
// Static assignment of DMA channels to peripherals.
// Allocating them statically is good enough for now. If lots of peripherals use
// DMA, these might need to be assigned at runtime.
const (
spi0DMAChannel = iota
spi1DMAChannel
)
// DMA channels usable on the RP2040.
var dmaChannels = (*[12]dmaChannel)(unsafe.Pointer(rp.DMA))

Просмотреть файл

@ -5,6 +5,7 @@ package machine
import ( import (
"device/rp" "device/rp"
"errors" "errors"
"unsafe"
) )
// SPI on the RP2040 // SPI on the RP2040
@ -288,14 +289,51 @@ func (spi SPI) isBusy() bool {
// tx writes buffer to SPI ignoring Rx. // tx writes buffer to SPI ignoring Rx.
func (spi SPI) tx(tx []byte) error { func (spi SPI) tx(tx []byte) error {
// Write to TX FIFO whilst ignoring RX, then clean up afterward. When RX if len(tx) == 0 {
// is full, PL022 inhibits RX pushes, and sets a sticky flag on // We don't have to do anything.
// push-on-full, but continues shifting. Safe if SSPIMSC_RORIM is not set. // This avoids a panic in &tx[0] when len(tx) == 0.
for i := range tx { return nil
for !spi.isWritable() {
}
spi.Bus.SSPDR.Set(uint32(tx[i]))
} }
// Pick the DMA channel reserved for this SPI peripheral.
var ch *dmaChannel
var dreq uint32
if spi.Bus == rp.SPI0 {
ch = &dmaChannels[spi0DMAChannel]
dreq = 16 // DREQ_SPI0_TX
} else { // SPI1
ch = &dmaChannels[spi1DMAChannel]
dreq = 18 // DREQ_SPI1_TX
}
// Configure the DMA peripheral as follows:
// - set read address, write address, and number of transfer units (bytes)
// - increment read address (in memory), don't increment write address (SSPDR)
// - set data size to single bytes
// - set the DREQ so that the DMA will fill the SPI FIFO as needed
// - start the transfer
ch.READ_ADDR.Set(uint32(uintptr(unsafe.Pointer(&tx[0]))))
ch.WRITE_ADDR.Set(uint32(uintptr(unsafe.Pointer(&spi.Bus.SSPDR))))
ch.TRANS_COUNT.Set(uint32(len(tx)))
ch.CTRL_TRIG.Set(rp.DMA_CH0_CTRL_TRIG_INCR_READ |
rp.DMA_CH0_CTRL_TRIG_DATA_SIZE_SIZE_BYTE<<rp.DMA_CH0_CTRL_TRIG_DATA_SIZE_Pos |
dreq<<rp.DMA_CH0_CTRL_TRIG_TREQ_SEL_Pos |
rp.DMA_CH0_CTRL_TRIG_EN)
// Wait until the transfer is complete.
// TODO: do this more efficiently:
// - Add a new API to start the transfer, without waiting for it to
// complete. This way, the CPU can do something useful while the
// transfer is in progress.
// - If we have to wait, do so by waiting for an interrupt and blocking
// this goroutine until finished (so that other goroutines can run or
// the CPU can go to sleep).
for ch.CTRL_TRIG.Get()&rp.DMA_CH0_CTRL_TRIG_BUSY != 0 {
}
// We didn't read any result values, which means the RX FIFO has likely
// overflown. We have to clean up this mess now.
// Drain RX FIFO, then wait for shifting to finish (which may be *after* // Drain RX FIFO, then wait for shifting to finish (which may be *after*
// TX FIFO drains), then drain RX FIFO again // TX FIFO drains), then drain RX FIFO again
for spi.isReadable() { for spi.isReadable() {