SPI Transactions in Arduino

By: paul

2014-07-30 11:05:50

For the last several weeks, I've been working on SPI transactions for Arduino's SPI library, to solve conflicts that sometimes occur between multiple SPI devices when using SPI from interrupts and/or different SPI settings.

To explain, a picture is worth 1000 words.  In this screenshot, loop() repetitively sends 2 bytes, where green is its chip select and red is the SPI clock.  Blue is the interrupt signal (rising edge) from a wireless module.  In this test, the interrupt happens at just the worst moment, during the first byte while loop() is using the SPI bus!

Click "Read mode" for lots more detail.....

Without transactions, the wireless lib interrupt would immediately assert (active low) the yellow chip select while the green is still active low, then begin sending its data with both devices listening!

With transactions (shown above), this interrupt is masked until the green access completes.  Not shown is the fact other unrelated interrupts remain enabled, so timing sensitive libs like Servo & SoftwareSerial aren't delayed, only interrupts using the SPI library.  SPI transactions also manage SPI settings, so each device is always accessed with its own settings, as shown here with the fast clock during green and slower clock during yellow.

Here's a more involved test case with Adafruit CC3000 and the SD library.

Hopefully soon this new SPI code will become part of Arduino's officially published SPI library and as SPI-based libs update to use the new functions, strange conflicts between SPI devices will become a thing of the past.

This new transaction support is being done in collaboration with the Arduino developers.  In fact, Matthijs Kooijman really deserves credit for the SPISettings portion of this work, as that part and the AVR implemention of it was his work.  Mikael Patel contributed many valuable insights, based on his COSA project, and Cristian Maglie (Arduino's technical lead) created benchmarks to study the performance impact these new functions might have.  Nantonos also contributed, by writing detailed documentation for these new SPI functions.

This work adds 3 new functions to the SPI library.

  SPI.beginTransaction(SPISettings);
  SPI.endTransaction();
  SPI.usingInterrupt(number);

In a netshell, SPI.beginTransaction() protects your SPI access from other interrupt-based libraries, and guarantees correct setting while you use the SPI bus.  SPI.endTransaction() tells the library when you're done using the SPI bus, and SPI.usingInterrupt() informs the SPI library if you will be using SPI from a function through attachInterrupt.

The new SPISettings is a special data type, just for describing SPI clock, data order and format.  For fixed settings, you can use beginTransaction(SPISettings(clock, order, format)), and the compiler will automatically inline your fixed settings with the most optimal code.  For user controlled settings, you can create a variable of SPISetting type and assign it based on user choice, which you don't know in advance.  This allows a very efficient beginTransaction(), because the non-const settings are converted to an efficient form ahead of time.

For example, you might use these new functions this way, for fixed settings:

int readStuff(void) {
  SPI.beginTransaction(SPISettings(12000000, MSBFIRST, SPI_MODE0));  // gain control of SPI bus
  digitalWrite(10, LOW);         // assert chip select
  SPI.transfer(0x74);            // send 16 bit command
  SPI.transfer(0xA2);
  byte b1 = SPI.transfer(0);     // read 16 bits of data
  byte b2 = SPI.transfer(0);
  digitalWrite(10, HIGH);        // deassert chip select
  SPI.endTransaction();          // release the SPI bus
  return (int16_t)((b1 << 8) | b2);
}

This approach generates the most efficient code, because the SPI settings are fixed.  SPISettings compiles to optimal code, thanks to Matthijs Kooijman's nice work!

The other awesome feature of SPISetting is hardware independence.  Any clock speed can be specified as a normal 32 bit integer.  There's no need for "dividers" that require knowing your board's clock rate.  Matthijs's code inside SPISettings efficiently converts those integers to the dividers used by the hardware.  The clock speed you give to SPISettings is the maximum speed your SPI device can use, not the actual speed your Arduino compatible board can create.  The SPISettings code automatically converts the max clock to the fastest clock your board can produce, which doesn't exceed the SPI device's capability.  As Arduino grows as a platform, onto more capable hardware, this approach is meant to allow SPI-based libraries to automatically use new faster SPI speeds.

For non-const settings, on libraries that allow the user to set the clock speed or other settings, you can create a SPISettings variable to hold the settings.  For example:

SPISettings mySettings;

void useClockSpeed(unsigned long clock) {
  mySettings = SPISettings(clock, MSBFIRST, SPI_MODE3);
};

void writeTwoBytes(byte a, byte b) {
  SPI.beginTransaction(mySettings);  // gain control of SPI bus
  digitalWrite(10, LOW);             // assert chip select
  SPI.transfer(a);                   // send 16 bit command
  SPI.transfer(b);
  digitalWrite(10, HIGH);            // deassert chip select
  SPI.endTransaction();              // release the SPI bus
}

The simplest way to use SPI transactions involves SPI.beginTransaction() right before asserting chip select, and SPI.endTransaction() right after releasing it.  But other approaches are possible.  For example, my SPI transaction patch for the Ethernet library implements transactions at the socket level.  In the SD library initialization, 80 clocks are sent with chip select high to prep the SD card, as another example where transactions to not necessarily correspond to chip selects.  This design is meant to be flexible, and easy to add to the dozens of existing SPI-based libraries.

The new SPI.h header defines a symbol SPI_HAS_TRANSACTION, to allow library authors to easily add SPI.beginTransaction() and SPI.endTransaction() inside #ifdef checks, for libraries supporting a wide range of Arduino versions.

The one other new function is SPI.usingInterrupt(), which informs the SPI library you will be using SPI functions from an interrupt.  It takes an integer input, which is the same number you use with attachInterrupt().

Newer versions of Arduino have a digitalPinToInterrupt() function, which is useful for converting Arduino pin numbers into their interrupt numbers, and it can tell you whether a pin has interrupt capability.  Here's how it would be used:

void configureInterruptPin(byte pin) {
  int intNum = digitalPinToInterrupt(pin);
  if (intNum != NOT_AN_INTERRUPT) {
    SPI.usingInterrupt(intNum);
    attachInterrupt(intNum, myInterruptFunction, RISING);
  }
}

The main idea is a call from your SPI-based library using interrupts causes OTHER libraries to temporarily mask the interrupt your library uses, so your interrupt won't conflict with their SPI activity and the SPI bus will be free when your interrupt function runs.


Developing and testing this function SPI sharing functionality has been a long and difficult path.  Since April, the API was discussed in great detail before Cristian Maglie (Arduino's technical lead and the man who ultimately decides what contributions Arduino will accept) ultimately decided on this approach, using a hybrid of my original proposal and Matthijs's SPISettings.  All contributions to a very widely used system like Arduino involve some controversy... there's *always* someone who doesn't like change or wants things done differently.  Luckily, after about 6 weeks of discussion, a clear path was chosen (by Cristian).

I should also mention how rare and insidious these SPI conflicts are.  Even with my intentional test case, rapidly re-reading a file from the SD card while the Adafruit CC3000 library is likely to interrupt, the data would often be read without error.  The SPI port is very fast and the AVR processor is quite slow, so even in a worst case, the window of opportunity for conflicts is small.  Many "normal" programs might see errors very rarely.  Sketches written in very simple ways, where you wait for all activity on one device to finish before using another, are unlikely to ever have conflicts.  The rare nature of these problems has led many people to question the need for this work, but I can assure you (or you could run the test code yourself) these conflicts are real and eventually cause data error or even complete lockup if the interrupt strikes are just the wrong moment.

Hopefully soon, Arduino will publish a new version with this updated SPI library, and over time, the many SPI-based Arduino libraries will gradually update to use these functions.  I want Arduino to be a very solid platform, not just for people using my own Teensy boards, but for all Arduino compatible products... which is why I've put so much work into this SPI library update over the last several weeks.

 

 

 

Back to archive index