Unverified Commit e6c7ee78 authored by Earle F. Philhower, III's avatar Earle F. Philhower, III Committed by GitHub

Add asynchronous SPI transactions (#2168)

Fixes #1192

Uses DMA operations to avoid the need to bit-bang or busy wait for SPI
operations that might be very slow. Optional, adds new API calls to enable.
Simple example included.
parent dc856dbb
...@@ -39,7 +39,39 @@ in order to consunme the received data and provide data to transmit. ...@@ -39,7 +39,39 @@ in order to consunme the received data and provide data to transmit.
* The callbacks operate at IRQ time and may be called very frequently at high SPI frequencies. So, make then small, fast, and with no memory allocations or locking. * The callbacks operate at IRQ time and may be called very frequently at high SPI frequencies. So, make then small, fast, and with no memory allocations or locking.
Asynchronous Operation
======================
Applications can use asynchronous SPI calls to allow for processing while long-running SPI transfers are
being performed. For example, a game could send a full screen update out over SPI and immediately start
processing the next frame without waiting for the first one to be sent. DMA is used to handle
the transfer to/from the hardware freeing the CPU from bit-banging or busy waiting.
Note that asynchronous operations can not be intersped with normal, synchronous ones. ``transferAsync``
should still occur after a ``beginTransaction()`` and when ``finishedAsync()`` returns ``true`` then
``endTransaction()`` should also be called.
All buffers need to be valid throughout the entire operation. Read data cannot be accessed until
the transaction is completed and can't be "peeked" at while the operation is ongoing.
bool transferAsync(const void \*send, void \*recv, size_t bytes)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Begins an SPI asynchronous transaction. Either ``send`` or ``recv`` can be ``nullptr`` if data only needs
to be transferred in one direction.
Check ``finishedAsync()`` to determine when the operation completes and conclude the transaction.
This operation needs to allocate a buffer from heap equal to ``bytes`` in size if ``LSBMODE`` is used.
bool finishedAsync()
~~~~~~~~~~~~~~~~~~~~
Call to check if the asynchronous operations is completed and the buffer passed in can be either read or
reused. Frees the allocated memory and completes the asynchronous transaction.
void abortAsync()
~~~~~~~~~~~~~~~~~
Cancels the outstanding asynchronous transaction and frees any allocated memory.
Examples Examples
~~~~~~~~ ========
See the SPItoMyself example for a complete Master and Slave application. See the SPItoMyself and SPItoMyselfAsync examples for a complete Master and Slave application.
// Shows how to use SPISlave on a single device in asynchronous mode
// Core0 runs as an SPI master and initiates a transmission to the slave
// Core1 runs the SPI Slave mode and provides a unique reply to messages from the master
//
// Released to the public domain 2024 by Earle F. Philhower, III <earlephilhower@yahoo.com>
#include <SPI.h>
#include <SPISlave.h>
// Wiring:
// Master RX GP0 <-> GP11 Slave TX
// Master CS GP1 <-> GP9 Slave CS
// Master CK GP2 <-> GP10 Slave CK
// Master TX GP3 <-> GP8 Slave RX
SPISettings spisettings(1000000, MSBFIRST, SPI_MODE0);
// Core 0 will be SPI master
void setup() {
SPI.setRX(0);
SPI.setCS(1);
SPI.setSCK(2);
SPI.setTX(3);
SPI.begin(true);
delay(5000);
}
int transmits = 0;
void loop() {
char msg[42];
int loops = 0;
memset(msg, 0, sizeof(msg));
sprintf(msg, "What's up? This is transmission %d", transmits);
Serial.printf("\n\nM-SEND: '%s'\n", msg);
SPI.beginTransaction(spisettings);
SPI.transferAsync(msg, msg, sizeof(msg));
while (!SPI.finishedAsync()) {
loops++;
}
SPI.endTransaction();
Serial.printf("M-RECV: '%s', idle loops %d\n", msg, loops);
transmits++;
delay(5000);
}
// Core 1 will be SPI slave
volatile bool recvBuffReady = false;
char recvBuff[42] = "";
int recvIdx = 0;
void recvCallback(uint8_t *data, size_t len) {
memcpy(recvBuff + recvIdx, data, len);
recvIdx += len;
if (recvIdx == sizeof(recvBuff)) {
recvBuffReady = true;
recvIdx = 0;
}
}
int sendcbs = 0;
// Note that the buffer needs to be long lived, the SPISlave doesn't copy it. So no local stack variables, only globals or heap(malloc/new) allocations.
char sendBuff[42];
void sentCallback() {
memset(sendBuff, 0, sizeof(sendBuff));
sprintf(sendBuff, "Slave to Master Xmission %d", sendcbs++);
SPISlave1.setData((uint8_t*)sendBuff, sizeof(sendBuff));
}
// Note that we use SPISlave1 here **not** because we're running on
// Core 1, but because SPI0 is being used already. You can use
// SPISlave or SPISlave1 on any core.
void setup1() {
SPISlave1.setRX(8);
SPISlave1.setCS(9);
SPISlave1.setSCK(10);
SPISlave1.setTX(11);
// Ensure we start with something to send...
sentCallback();
// Hook our callbacks into the slave
SPISlave1.onDataRecv(recvCallback);
SPISlave1.onDataSent(sentCallback);
SPISlave1.begin(spisettings);
delay(3000);
Serial.println("S-INFO: SPISlave started");
}
void loop1() {
if (recvBuffReady) {
Serial.printf("S-RECV: '%s'\n", recvBuff);
recvBuffReady = false;
}
}
...@@ -26,6 +26,9 @@ setRX KEYWORD2 ...@@ -26,6 +26,9 @@ setRX KEYWORD2
setTX KEYWORD2 setTX KEYWORD2
setSCK KEYWORD2 setSCK KEYWORD2
setCS KEYWORD2 setCS KEYWORD2
transferAsync KEYWORD2
finishedAsync KEYWORD2
abortAsync KEYWORD2
####################################### #######################################
# Constants (LITERAL1) # Constants (LITERAL1)
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
*/ */
#include "SPI.h" #include "SPI.h"
#include <hardware/dma.h>
#include <hardware/spi.h> #include <hardware/spi.h>
#include <hardware/gpio.h> #include <hardware/gpio.h>
#include <hardware/structs/iobank0.h> #include <hardware/structs/iobank0.h>
...@@ -218,7 +219,7 @@ void SPIClassRP2040::beginTransaction(SPISettings settings) { ...@@ -218,7 +219,7 @@ void SPIClassRP2040::beginTransaction(SPISettings settings) {
void SPIClassRP2040::endTransaction(void) { void SPIClassRP2040::endTransaction(void) {
noInterrupts(); // Avoid race condition so the GPIO IRQs won't come back until all state is restored noInterrupts(); // Avoid race condition so the GPIO IRQs won't come back until all state is restored
DEBUGSPI("SPI::endTransaction()\n"); DEBUGSPI("SPI::endTransaction()\n");
// Re-enablke IRQs // Re-enable IRQs
for (auto entry : _usingIRQs) { for (auto entry : _usingIRQs) {
int gpio = entry.first; int gpio = entry.first;
int mode = entry.second; int mode = entry.second;
...@@ -230,6 +231,103 @@ void SPIClassRP2040::endTransaction(void) { ...@@ -230,6 +231,103 @@ void SPIClassRP2040::endTransaction(void) {
interrupts(); interrupts();
} }
bool SPIClassRP2040::transferAsync(const void *send, void *recv, size_t bytes) {
DEBUGSPI("SPI::transferAsync(%p, %p, %d)\n", send, recv, bytes);
const uint8_t *txbuff = reinterpret_cast<const uint8_t *>(send);
uint8_t *rxbuff = reinterpret_cast<uint8_t *>(recv);
_dummy = 0xffffffff;
if (!_initted || (!send && !recv)) {
return false;
}
_channelDMA = dma_claim_unused_channel(false);
if (_channelDMA == -1) {
return false;
}
_channelSendDMA = dma_claim_unused_channel(false);
if (_channelSendDMA == -1) {
dma_channel_unclaim(_channelDMA);
return false;
}
if (send && (_spis.getBitOrder() != MSBFIRST)) {
_dmaBuffer = (uint8_t *)malloc(bytes);
if (!_dmaBuffer) {
dma_channel_unclaim(_channelDMA);
dma_channel_unclaim(_channelSendDMA);
return false;
}
for (size_t i = 0; i < bytes; i++) {
_dmaBuffer[i] = reverseByte(txbuff[i]);
}
}
_dmaBytes = bytes;
_rxFinalBuffer = rxbuff;
hw_write_masked(&spi_get_hw(_spi)->cr0, (8 - 1) << SPI_SSPCR0_DSS_LSB, SPI_SSPCR0_DSS_BITS); // Fast set to 8-bits
dma_channel_config c = dma_channel_get_default_config(_channelSendDMA);
channel_config_set_transfer_data_size(&c, DMA_SIZE_8); // 8b transfers into SPI FIFO
channel_config_set_read_increment(&c, send ? true : false); // Reading incrementing addresses
channel_config_set_write_increment(&c, false); // Writing to the same FIFO address
channel_config_set_dreq(&c, spi_get_dreq(_spi, true)); // Wait for the TX FIFO specified
channel_config_set_chain_to(&c, _channelSendDMA); // No chaining
channel_config_set_irq_quiet(&c, true); // No need for IRQ
dma_channel_configure(_channelSendDMA, &c, &spi_get_hw(_spi)->dr, !send ? (uint8_t *)&_dummy : (_spis.getBitOrder() != MSBFIRST ? _dmaBuffer : txbuff), bytes, false);
c = dma_channel_get_default_config(_channelDMA);
channel_config_set_transfer_data_size(&c, DMA_SIZE_8); // 8b transfers into SPI FIFO
channel_config_set_read_increment(&c, false); // Reading same FIFO address
channel_config_set_write_increment(&c, recv ? true : false); // Writing to the buffer
channel_config_set_dreq(&c, spi_get_dreq(_spi, false)); // Wait for the RX FIFO specified
channel_config_set_chain_to(&c, _channelDMA); // No chaining
channel_config_set_irq_quiet(&c, true); // No need for IRQ
dma_channel_configure(_channelDMA, &c, !recv ? (uint8_t *)&_dummy : rxbuff, &spi_get_hw(_spi)->dr, bytes, false);
spi_get_hw(_spi)->dmacr = 1 | (1 << 1); // TDMAE | RDMAE
dma_channel_start(_channelDMA);
dma_channel_start(_channelSendDMA);
return true;
}
bool SPIClassRP2040::finishedAsync() {
if (!_initted) {
return true;
}
if (dma_channel_is_busy(_channelDMA) || (spi_get_hw(_spi)->sr & SPI_SSPSR_BSY_BITS)) {
return false;
}
dma_channel_cleanup(_channelDMA);
dma_channel_unclaim(_channelDMA);
dma_channel_cleanup(_channelSendDMA);
dma_channel_unclaim(_channelSendDMA);
spi_get_hw(_spi)->dmacr = 0;
if (_spis.getBitOrder() != MSBFIRST) {
for (int i = 0; i < _dmaBytes; i++) {
_rxFinalBuffer[i] = reverseByte(_rxFinalBuffer[i]);
}
free(_dmaBuffer);
_dmaBuffer = nullptr;
}
return true;
}
void SPIClassRP2040::abortAsync() {
if (!_initted) {
return;
}
dma_channel_cleanup(_channelDMA);
dma_channel_unclaim(_channelDMA);
dma_channel_cleanup(_channelSendDMA);
dma_channel_unclaim(_channelSendDMA);
spi_get_hw(_spi)->dmacr = 0;
free(_dmaBuffer);
_dmaBuffer = nullptr;
}
bool SPIClassRP2040::setRX(pin_size_t pin) { bool SPIClassRP2040::setRX(pin_size_t pin) {
constexpr uint32_t valid[2] = { __bitset({0, 4, 16, 20}) /* SPI0 */, constexpr uint32_t valid[2] = { __bitset({0, 4, 16, 20}) /* SPI0 */,
__bitset({8, 12, 24, 28}) /* SPI1 */ __bitset({8, 12, 24, 28}) /* SPI1 */
......
...@@ -39,6 +39,13 @@ public: ...@@ -39,6 +39,13 @@ public:
// Sends one buffer and receives into another, much faster! can set rx or txbuf to nullptr // Sends one buffer and receives into another, much faster! can set rx or txbuf to nullptr
void transfer(const void *txbuf, void *rxbuf, size_t count) override; void transfer(const void *txbuf, void *rxbuf, size_t count) override;
// DMA/asynchronous transfers. Do not combime with synchronous runs or bad stuff will happen
// All buffers must be valid for entire DMA and not touched until `finished()` returns true.
bool transferAsync(const void *send, void *recv, size_t bytes);
bool finishedAsync(); // Call to check if the async operations is completed and the buffer can be reused/read
void abortAsync(); // Cancel an outstanding async operation
// Call before/after every complete transaction // Call before/after every complete transaction
void beginTransaction(SPISettings settings) override; void beginTransaction(SPISettings settings) override;
void endTransaction(void) override; void endTransaction(void) override;
...@@ -51,7 +58,7 @@ public: ...@@ -51,7 +58,7 @@ public:
bool setCS(pin_size_t pin); bool setCS(pin_size_t pin);
bool setSCK(pin_size_t pin); bool setSCK(pin_size_t pin);
bool setTX(pin_size_t pin); bool setTX(pin_size_t pin);
bool setMOSI(pin_size_t pin) { inline bool setMOSI(pin_size_t pin) {
return setTX(pin); return setTX(pin);
} }
...@@ -92,6 +99,14 @@ private: ...@@ -92,6 +99,14 @@ private:
bool _initted; // Transaction begun bool _initted; // Transaction begun
std::map<int, int> _usingIRQs; std::map<int, int> _usingIRQs;
// DMA
int _channelDMA;
int _channelSendDMA;
uint8_t *_dmaBuffer = nullptr;
int _dmaBytes;
uint8_t *_rxFinalBuffer;
uint32_t _dummy;
}; };
extern SPIClassRP2040 SPI; extern SPIClassRP2040 SPI;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment