diff --git a/README.md b/README.md index 95a445b..9b59e4f 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,67 @@ http://www.pjrc.com/teensy/td_libs_SPI.html ![](http://www.pjrc.com/teensy/td_libs_SPI_1.jpg) +SPI Async Extension Proposal +===================== + +This branch contains an API to allow async SPI requests on Arduinos. It is just a proof-of-concept and still requires work. + +This extension uses interrupts to transfer an entire buffer via SPI asynchronously while the main Arduino loop runs. It interacts cleanly with beginTransaction()/endTransaction() and interrupt handlers can safely queue additional transfers. It is not currently safe to use transfer() outside of a transaction block, but protections can be added for that case. + +Usage +-------- + +Here's an example program that kicks off three SPI transactions: + +```C++ +#include "SPI.h" +uint8_t buf1[] = { 0x01, 0x02 }, + buf2[] = { 0x03, 0x04 }, + buf3[] = { 0x05, 0x06 }; + +SPISettings theSettings = SPISettings(100000, MSBFIRST, SPI_MODE0); + +SPIQueuedTransfer t1(buf1, 2, theSettings, 0, 0), + t2(buf2, 2, theSettings, 0, 0), + t3(buf3, 2, theSettings, 0, 0); + +void setup() { + pinMode(13, OUTPUT); //High while transfers are queued + pinMode(14, OUTPUT); //High while SPI ISR is running + pinMode(15, OUTPUT); //High while workTransferQueue + + digitalWrite(13, 0); + SPI.begin(); + + SPI.queueTransfer(&t1); + SPI.queueTransfer(&t2); + SPI.queueTransfer(&t3); + digitalWrite(13, 1); + + //Do some things... + + //Hold up execution until all transfers are complete + while(!t1.is_completed && !t2.is_completed && !t3.is_completed); +} +``` + +Here's a trace of the output from the above program running on a Teensy 2.0 at 16Mhz with an SPI bus speed of 125kBps: + +![screen shot 2014-12-28 at 10 46 07 pm](https://cloud.githubusercontent.com/assets/17287/5566620/60398f4c-8ee3-11e4-9b1b-b143adb34447.png) + +Implementation Notes +------------------------- +* No ordering is guaranteed across queued transfers. You can see in the trace that they are processed in a LIFO order if the SPI module is busy +* The inTransaction flag becomes mandatory in order to interoperate gracefully with begin/endTransaction() +* Memory overhead is approximately 5-bytes + 8-bytes per queued transfer +* Interrupt latency is ~2.5µS per byte + ~10µS per transfer + +Performance +---------------- + +Using the ISR consumes more cycles than polling when the SPI bus is running very fast. In the trace above, the per-queued-transfer cost is about 19µS, implying that async transfers are advantageous for transfers when the SPI bus is running slower than about 2Mbps. This may not seem very useful, but the gains should be quite marked on the Teensy 3.x and other ARM-based Arduinos where the CPU runs at 48+Mhz and the FIFO queues allow transfers up to 8 bytes between interrupts. + +TODO +------- + +I have only implemented a proof-of-concept for AVR; there is no ARM implementation yet. The naming can probably also use a pass. endTransaction() still needs a few lines of code to kick off any queued transfers. \ No newline at end of file diff --git a/SPI.cpp b/SPI.cpp index d0460a9..699fa73 100644 --- a/SPI.cpp +++ b/SPI.cpp @@ -19,13 +19,21 @@ #if defined(__AVR__) +#include + SPIClass SPI; uint8_t SPIClass::interruptMode = 0; uint8_t SPIClass::interruptMask = 0; uint8_t SPIClass::interruptSave = 0; -#ifdef SPI_TRANSACTION_MISMATCH_LED uint8_t SPIClass::inTransactionFlag = 0; + +#ifdef SPI_QUEUED_TRANSFERS + +SPIQueuedTransfer *SPIClass::nextTransfer = NULL; //Stores the next transfer, or NULL if there aren't any +SPIQueuedTransfer *SPIClass::curTransfer = NULL; //Currently in-progress transfer, or NULL if there isn't any +uint8_t SPIClass::curTransferPos = 0; //Position in the current byte being read/written + #endif void SPIClass::begin() @@ -138,6 +146,42 @@ void SPIClass::usingInterrupt(uint8_t interruptNumber) SREG = stmp; } +#ifdef SPI_QUEUED_TRANSFERS + +inline void spi_isr(void) __attribute__((always_inline)); +/* + * ISR to keep pumping queued transfers out the SPI interface + */ +ISR(SPI_STC_vect) { + spi_isr(); +} + +inline void spi_isr(void) { + digitalWrite(14, 1); + uint8_t *buf = SPI.curTransfer->buffer; + uint8_t pos = SPI.curTransferPos; + uint8_t len = SPI.curTransfer->len; + + buf[pos++] = SPDR; + if(pos < len) { + //More bytes to transfer + SPDR = buf[pos]; + SPI.curTransferPos = pos; + } else { + //Complete this transfer, move on if there's another + + //Deassert Chip + digitalWrite(SPI.curTransfer->cs_pin, !SPI.curTransfer->assert_state); + + //2. Look for next queued transfer + SPI.inTransactionFlag = 0; + SPI.workTransferQueue(); + } + digitalWrite(14,0); +} + + +#endif /**********************************************************/ /* 32 bit Teensy 3.0 and 3.1 */ diff --git a/SPI.h b/SPI.h index 9f0a661..578e3c5 100644 --- a/SPI.h +++ b/SPI.h @@ -57,6 +57,47 @@ /* 8 bit AVR-based boards */ /**********************************************************/ +#ifdef SPI_QUEUED_TRANSFERS + +class SPISettings; + +/* + * Allows an SPI transfer to occurr asynchronously while the main loop + * continues to run. The is_completed flag is set to true once the transfer is + * complete. The CS pin is automatically asserted before the transfer starts, + * and deasserted at the completion of the transfer. New transfers may be + * safely queued from an ISR. + * + * buffer - pointer to the buffer for the transfer. The contents will be replaced with received values from the chip + * len - number of bytes to transfer, stored as a 7 bit quantity, 127 bytes is the max transfer size + * settings - a reference to an SPISettings structure + * cs_pin - the number value of the Chip-select pin for this transfer + * assert_state - 0 or 1 (low or high) representing the state of the CS pin that activates the device + * + */ +class SPIQueuedTransfer { +public: + SPIQueuedTransfer(uint8_t *buffer, uint8_t len, SPISettings &settings, uint8_t cs_pin, uint8_t assert_state) : + buffer(buffer), len(len), cs_pin(cs_pin), assert_state(assert_state), is_completed(0), settings(settings), next(NULL) {} + + +private: + uint8_t *buffer; + uint8_t len : 7; + + uint8_t cs_pin : 6; + uint8_t assert_state : 1; +public: + uint8_t is_completed : 1; +private: + SPISettings &settings; + SPIQueuedTransfer *next; + + friend class SPIClass; + friend void spi_isr(void); //So that the ISR can acess all the private variables +}; +#endif + #if defined(__AVR__) // define SPI_AVR_EIMSK for AVR boards with external interrupt pins @@ -187,8 +228,8 @@ class SPIClass { pinMode(SPI_TRANSACTION_MISMATCH_LED, OUTPUT); digitalWrite(SPI_TRANSACTION_MISMATCH_LED, HIGH); } - inTransactionFlag = 1; #endif + inTransactionFlag = 1; SPCR = settings.spcr; SPSR = settings.spsr; } @@ -247,8 +288,8 @@ class SPIClass { pinMode(SPI_TRANSACTION_MISMATCH_LED, OUTPUT); digitalWrite(SPI_TRANSACTION_MISMATCH_LED, HIGH); } - inTransactionFlag = 0; #endif + inTransactionFlag = 0; if (interruptMode > 0) { #ifdef SPI_AVR_EIMSK if (interruptMode == 1) { @@ -259,7 +300,49 @@ class SPIClass { SREG = interruptSave; } } + //TODO: Kick off a queued transfer immediately if there is one + } + +#ifdef SPI_QUEUED_TRANSFERS + inline static void queueTransfer(SPIQueuedTransfer *transfer) { + uint8_t tmp = SREG; + cli(); + transfer->next = nextTransfer; + nextTransfer = transfer; + + if(!inTransactionFlag) + workTransferQueue(); + SREG = tmp; + } + + //NOTE: Interrupts MUST be disabled when calling this method! + inline static void workTransferQueue() { + digitalWrite(15, 1); + inTransactionFlag = 1; + + SPIQueuedTransfer *t = nextTransfer; + if(!t) { + inTransactionFlag = 0; + digitalWrite(15, 0); + return; + } + + nextTransfer = t->next; + + //1. Configure SPI as requested + SPCR = t->settings.spcr | _BV(SPIE); //Config + interrupts + SPSR = t->settings.spsr; + + //2. Assert Chip-Select + digitalWrite(t->cs_pin, t->assert_state); + + //3. Begin transfer of first byte + curTransfer = t; + curTransferPos = 0; //This is the index of the byte we're sending + SPDR = t->buffer[0]; + digitalWrite(15, 0); } +#endif // Disable the SPI bus static void end(); @@ -291,9 +374,14 @@ class SPIClass { static uint8_t interruptMode; // 0=none, 1=mask, 2=global static uint8_t interruptMask; // which interrupts to mask static uint8_t interruptSave; // temp storage, to restore state - #ifdef SPI_TRANSACTION_MISMATCH_LED +#ifdef SPI_QUEUED_TRANSFERS + static SPIQueuedTransfer *nextTransfer; //Stores the next transfer, or NULL if there aren't any + static SPIQueuedTransfer *curTransfer; //Currently in-progress transfer, or NULL if there isn't any + static uint8_t curTransferPos; //Position in the current byte being read/written + + friend void spi_isr(void); //So that the ISR can acess all the private variables +#endif static uint8_t inTransactionFlag; - #endif };