aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/machine/machine_rp2_spi.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/machine/machine_rp2_spi.go')
-rw-r--r--src/machine/machine_rp2_spi.go406
1 files changed, 406 insertions, 0 deletions
diff --git a/src/machine/machine_rp2_spi.go b/src/machine/machine_rp2_spi.go
new file mode 100644
index 000000000..faab9839a
--- /dev/null
+++ b/src/machine/machine_rp2_spi.go
@@ -0,0 +1,406 @@
+//go:build rp2040 || rp2350
+
+package machine
+
+import (
+ "device/rp"
+ "errors"
+ "unsafe"
+)
+
+// SPI on the RP2040
+var (
+ SPI0 = &_SPI0
+ _SPI0 = SPI{
+ Bus: rp.SPI0,
+ }
+ SPI1 = &_SPI1
+ _SPI1 = SPI{
+ Bus: rp.SPI1,
+ }
+)
+
+// SPIConfig is used to store config info for SPI.
+type SPIConfig struct {
+ Frequency uint32
+ // LSB not supported on rp2040.
+ LSBFirst bool
+ // Mode's two most LSB are CPOL and CPHA. i.e. Mode==2 (0b10) is CPOL=1, CPHA=0
+ Mode uint8
+ // Serial clock pin
+ SCK Pin
+ // TX or Serial Data Out (MOSI if rp2040 is master)
+ SDO Pin
+ // RX or Serial Data In (MISO if rp2040 is master)
+ SDI Pin
+}
+
+var (
+ ErrLSBNotSupported = errors.New("SPI LSB unsupported on PL022")
+ ErrSPITimeout = errors.New("SPI timeout")
+ ErrSPIBaud = errors.New("SPI baud too low or above 66.5Mhz")
+ errSPIInvalidSDI = errors.New("invalid SPI SDI pin")
+ errSPIInvalidSDO = errors.New("invalid SPI SDO pin")
+ errSPIInvalidSCK = errors.New("invalid SPI SCK pin")
+)
+
+type SPI struct {
+ Bus *rp.SPI0_Type
+}
+
+// Tx handles read/write operation for SPI interface. Since SPI is a synchronous write/read
+// interface, there must always be the same number of bytes written as bytes read.
+// The Tx method knows about this, and offers a few different ways of calling it.
+//
+// This form sends the bytes in tx buffer, putting the resulting bytes read into the rx buffer.
+// Note that the tx and rx buffers must be the same size:
+//
+// spi.Tx(tx, rx)
+//
+// This form sends the tx buffer, ignoring the result. Useful for sending "commands" that return zeros
+// until all the bytes in the command packet have been received:
+//
+// spi.Tx(tx, nil)
+//
+// This form sends zeros, putting the result into the rx buffer. Good for reading a "result packet":
+//
+// spi.Tx(nil, rx)
+//
+// Remark: This implementation (RP2040) allows reading into buffer with a custom repeated
+// value on tx.
+//
+// spi.Tx([]byte{0xff}, rx) // may cause unwanted heap allocations.
+//
+// This form sends 0xff and puts the result into rx buffer. Useful for reading from SD cards
+// which require 0xff input on SI.
+func (spi SPI) Tx(w, r []byte) (err error) {
+ switch {
+ case w == nil:
+ // read only, so write zero and read a result.
+ err = spi.rx(r, 0)
+ case r == nil:
+ // write only
+ err = spi.tx(w)
+ case len(w) == 1 && len(r) > 1:
+ // Read with custom repeated value.
+ err = spi.rx(r, w[0])
+ default:
+ // write/read
+ err = spi.txrx(w, r)
+ }
+ return err
+}
+
+// Write a single byte and read a single byte from TX/RX FIFO.
+func (spi SPI) Transfer(w byte) (byte, error) {
+ for !spi.isWritable() {
+ }
+
+ spi.Bus.SSPDR.Set(uint32(w))
+
+ for !spi.isReadable() {
+ }
+ return uint8(spi.Bus.SSPDR.Get()), nil
+}
+
+func (spi SPI) SetBaudRate(br uint32) error {
+ const freqin uint32 = 125 * MHz
+ const maxBaud uint32 = 66.5 * MHz // max output frequency is 66.5MHz on rp2040. see Note page 527.
+ // Find smallest prescale value which puts output frequency in range of
+ // post-divide. Prescale is an even number from 2 to 254 inclusive.
+ var prescale, postdiv uint32
+ for prescale = 2; prescale < 255; prescale += 2 {
+ if freqin < (prescale+2)*256*br {
+ break
+ }
+ }
+ if prescale > 254 || br > maxBaud {
+ return ErrSPIBaud
+ }
+ // Find largest post-divide which makes output <= baudrate. Post-divide is
+ // an integer in the range 1 to 256 inclusive.
+ for postdiv = 256; postdiv > 1; postdiv-- {
+ if freqin/(prescale*(postdiv-1)) > br {
+ break
+ }
+ }
+ spi.Bus.SSPCPSR.Set(prescale)
+ spi.Bus.SSPCR0.ReplaceBits((postdiv-1)<<rp.SPI0_SSPCR0_SCR_Pos, rp.SPI0_SSPCR0_SCR_Msk, 0)
+ return nil
+}
+
+func (spi SPI) GetBaudRate() uint32 {
+ const freqin uint32 = 125 * MHz
+ prescale := spi.Bus.SSPCPSR.Get()
+ postdiv := ((spi.Bus.SSPCR0.Get() & rp.SPI0_SSPCR0_SCR_Msk) >> rp.SPI0_SSPCR0_SCR_Pos) + 1
+ return freqin / (prescale * postdiv)
+}
+
+// Configure is intended to setup/initialize the SPI interface.
+// Default baudrate of 4MHz is used if Frequency == 0. Default
+// word length (data bits) is 8.
+// Below is a list of GPIO pins corresponding to SPI0 bus on the rp2040:
+//
+// SI : 0, 4, 17 a.k.a RX and MISO (if rp2040 is master)
+// SO : 3, 7, 19 a.k.a TX and MOSI (if rp2040 is master)
+// SCK: 2, 6, 18
+//
+// SPI1 bus GPIO pins:
+//
+// SI : 8, 12
+// SO : 11, 15
+// SCK: 10, 14
+//
+// No pin configuration is needed of SCK, SDO and SDI needed after calling Configure.
+func (spi SPI) Configure(config SPIConfig) error {
+ const defaultBaud uint32 = 4 * MHz
+ if config.SCK == 0 && config.SDO == 0 && config.SDI == 0 {
+ // set default pins if config zero valued or invalid clock pin supplied.
+ switch spi.Bus {
+ case rp.SPI0:
+ config.SCK = SPI0_SCK_PIN
+ config.SDO = SPI0_SDO_PIN
+ config.SDI = SPI0_SDI_PIN
+ case rp.SPI1:
+ config.SCK = SPI1_SCK_PIN
+ config.SDO = SPI1_SDO_PIN
+ config.SDI = SPI1_SDI_PIN
+ }
+ }
+ var okSDI, okSDO, okSCK bool
+ switch spi.Bus {
+ case rp.SPI0:
+ okSDI = config.SDI == 0 || config.SDI == 4 || config.SDI == 16 || config.SDI == 20
+ okSDO = config.SDO == 3 || config.SDO == 7 || config.SDO == 19 || config.SDO == 23
+ okSCK = config.SCK == 2 || config.SCK == 6 || config.SCK == 18 || config.SCK == 22
+ case rp.SPI1:
+ okSDI = config.SDI == 8 || config.SDI == 12 || config.SDI == 24 || config.SDI == 28
+ okSDO = config.SDO == 11 || config.SDO == 15 || config.SDO == 27
+ okSCK = config.SCK == 10 || config.SCK == 14 || config.SCK == 26
+ }
+
+ switch {
+ case !okSDI:
+ return errSPIInvalidSDI
+ case !okSDO:
+ return errSPIInvalidSDO
+ case !okSCK:
+ return errSPIInvalidSCK
+ }
+
+ if config.Frequency == 0 {
+ config.Frequency = defaultBaud
+ }
+ // SPI pin configuration
+ config.SCK.setFunc(fnSPI)
+ config.SDO.setFunc(fnSPI)
+ config.SDI.setFunc(fnSPI)
+
+ return spi.initSPI(config)
+}
+
+func (spi SPI) initSPI(config SPIConfig) (err error) {
+ spi.reset()
+ // LSB-first not supported on PL022:
+ if config.LSBFirst {
+ return ErrLSBNotSupported
+ }
+ err = spi.SetBaudRate(config.Frequency)
+ // Set SPI Format (CPHA and CPOL) and frame format (default is Motorola)
+ spi.setFormat(config.Mode)
+
+ // Always enable DREQ signals -- harmless if DMA is not listening
+ spi.Bus.SSPDMACR.SetBits(rp.SPI0_SSPDMACR_TXDMAE | rp.SPI0_SSPDMACR_RXDMAE)
+ // Finally enable the SPI
+ spi.Bus.SSPCR1.SetBits(rp.SPI0_SSPCR1_SSE)
+ return err
+}
+
+//go:inline
+func (spi SPI) setFormat(mode uint8) {
+ cpha := uint32(mode) & 1
+ cpol := uint32(mode>>1) & 1
+ spi.Bus.SSPCR0.ReplaceBits(
+ (cpha<<rp.SPI0_SSPCR0_SPH_Pos)|
+ (cpol<<rp.SPI0_SSPCR0_SPO_Pos)|
+ (uint32(7)<<rp.SPI0_SSPCR0_DSS_Pos), // Set databits (SPI word length) to 8 bits.
+ rp.SPI0_SSPCR0_SPH_Msk|rp.SPI0_SSPCR0_SPO_Msk|rp.SPI0_SSPCR0_DSS_Msk|rp.SPI0_SSPCR0_FRF_Msk, 0)
+}
+
+// reset resets SPI and waits until reset is done.
+//
+//go:inline
+func (spi SPI) reset() {
+ resetVal := spi.deinit()
+ rp.RESETS.RESET.ClearBits(resetVal)
+ // Wait until reset is done.
+ for !rp.RESETS.RESET_DONE.HasBits(resetVal) {
+ }
+}
+
+//go:inline
+func (spi SPI) deinit() (resetVal uint32) {
+ switch spi.Bus {
+ case rp.SPI0:
+ resetVal = rp.RESETS_RESET_SPI0
+ case rp.SPI1:
+ resetVal = rp.RESETS_RESET_SPI1
+ }
+ // Perform SPI reset.
+ rp.RESETS.RESET.SetBits(resetVal)
+ return resetVal
+}
+
+// isWritable returns false if no space is available to write. True if a write is possible
+//
+//go:inline
+func (spi SPI) isWritable() bool {
+ return spi.Bus.SSPSR.HasBits(rp.SPI0_SSPSR_TNF)
+}
+
+// isReadable returns true if a read is possible i.e. data is present
+//
+//go:inline
+func (spi SPI) isReadable() bool {
+ return spi.Bus.SSPSR.HasBits(rp.SPI0_SSPSR_RNE)
+}
+
+// PrintRegs prints SPI's peripheral common registries current values
+func (spi SPI) PrintRegs() {
+ cr0 := spi.Bus.SSPCR0.Get()
+ cr1 := spi.Bus.SSPCR1.Get()
+ dmacr := spi.Bus.SSPDMACR.Get()
+ cpsr := spi.Bus.SSPCPSR.Get()
+ dr := spi.Bus.SSPDR.Get()
+ ris := spi.Bus.SSPRIS.Get()
+ println("CR0:", cr0)
+ println("CR1:", cr1)
+ println("DMACR:", dmacr)
+ println("CPSR:", cpsr)
+ println("DR:", dr)
+ println("RIS:", ris)
+}
+
+//go:inline
+func (spi SPI) isBusy() bool {
+ return spi.Bus.SSPSR.HasBits(rp.SPI0_SSPSR_BSY)
+}
+
+// tx writes buffer to SPI ignoring Rx.
+func (spi SPI) tx(tx []byte) error {
+ if len(tx) == 0 {
+ // We don't have to do anything.
+ // This avoids a panic in &tx[0] when len(tx) == 0.
+ return nil
+ }
+
+ // 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*
+ // TX FIFO drains), then drain RX FIFO again
+ for spi.isReadable() {
+ spi.Bus.SSPDR.Get()
+ }
+ for spi.isBusy() {
+ }
+ for spi.isReadable() {
+ spi.Bus.SSPDR.Get()
+ }
+ // Don't leave overrun flag set
+ spi.Bus.SSPICR.Set(rp.SPI0_SSPICR_RORIC)
+ return nil
+}
+
+// rx reads buffer to SPI ignoring x.
+// txrepeat is output repeatedly on SO as data is read in from SI.
+// Generally this can be 0, but some devices require a specific value here,
+// e.g. SD cards expect 0xff
+func (spi SPI) rx(rx []byte, txrepeat byte) error {
+ plen := len(rx)
+ const fifoDepth = 8 // see txrx
+ var rxleft, txleft = plen, plen
+ for txleft != 0 || rxleft != 0 {
+ if txleft != 0 && spi.isWritable() && rxleft < txleft+fifoDepth {
+ spi.Bus.SSPDR.Set(uint32(txrepeat))
+ txleft--
+ }
+ if rxleft != 0 && spi.isReadable() {
+ rx[plen-rxleft] = uint8(spi.Bus.SSPDR.Get())
+ rxleft--
+ continue
+ }
+ }
+ for spi.isBusy() {
+ gosched()
+ }
+ return nil
+}
+
+// Write len bytes from src to SPI. Simultaneously read len bytes from SPI to dst.
+// Note this function is guaranteed to exit in a known amount of time (bits sent * time per bit)
+func (spi SPI) txrx(tx, rx []byte) error {
+ plen := len(tx)
+ if plen != len(rx) {
+ return ErrTxInvalidSliceSize
+ }
+ // Never have more transfers in flight than will fit into the RX FIFO,
+ // else FIFO will overflow if this code is heavily interrupted.
+ const fifoDepth = 8
+ var rxleft, txleft = plen, plen
+ for txleft != 0 || rxleft != 0 {
+ if txleft != 0 && spi.isWritable() && rxleft < txleft+fifoDepth {
+ spi.Bus.SSPDR.Set(uint32(tx[plen-txleft]))
+ txleft--
+ }
+ if rxleft != 0 && spi.isReadable() {
+ rx[plen-rxleft] = uint8(spi.Bus.SSPDR.Get())
+ rxleft--
+ }
+ }
+
+ if txleft != 0 || rxleft != 0 {
+ // Transaction ended early due to timeout
+ return ErrSPITimeout
+ }
+ for spi.isBusy() {
+ gosched()
+ }
+ return nil
+}