Scoped SPI transactions

Status
Not open for further replies.

christoph

Well-known member
Many SPI devices require small transfers, somewhere between one and <not many> bytes. These short transfers must be enclosed in a pair of SPI.beginTransaction() and SPI.endTransaction() calls.

Sometimes it's necessary to read e.g. a single status byte from a device to decide if further transfers are necessary; something along these lines:

Code:
bool readFromDeviceIfReady(outValue& result)
{
  SPI.beginTransaction(deviceSettings);

  if (SPI.readByte(0) != device_indicates_readiness)
  {
    SPI.endTransaction();
    return false;
  }

  uint8_t buffer[numberOfBytes];
  SPI.transfer(someBuffer, numberOfBytes);
  result = convertToResult(buffer, numberOfBytes);

  SPI.endTransaction();
  return true;
}
It's not really hard to create matching pairs of those, but it's also not hard to screw that up. That's why I have created an SPI_scopedTransaction macro:

Code:
#define SPI_scopedTransaction(settings) \
for(                                    \
  struct                                \
  {                                     \
    struct Helper                       \
    {                                   \
      Helper() : done_(false)           \
      {                                 \
        SPI.beginTransaction(settings); \
      }                                 \
      ~Helper()                         \
      {                                 \
        SPI.endTransaction();           \
      }                                 \
      bool done_;                       \
    };                                  \
                                        \
    bool done() const                   \
    {                                   \
      return helper_.done_;             \
    }                                   \
    void exec()                         \
    {                                   \
      helper_.done_ = true;             \
    }                                   \
    Helper helper_;                     \
  } scope; !scope.done() ; scope.exec())
explanation below, let's first see how it makes writing SPI code easier:

Code:
bool readFromDevice(uint32_t& result)
{
  SPI_scopedTransaction(SPISettings())
  {
    if (SPI.transfer(0) != 1) // let's assume 1 indicates ready
    {
      return false;
    }

    uint32_t buffer;
    SPI.transfer(&buffer, sizeof(buffer));
    result = buffer;
  }
  return true;
}

So how does it work?

A for-loop has the following structure:
Code:
for (initialization; condition; increment) statement
What happens?
  1. The initialization expression is executed. It can declare a counter variable (or several of the same type) and can also initialize it. The variable type can basically be anything, including an anonymous struct. Such an anonymous struct is declared by the macro. Unfortunately, since it is anonymous, it cannot have non-default constructor and destructor. We'll get back to that later.
  2. If condition evaluates to true, statement is executed (that can also be a block {}). Otherwise, the loop proceeds at 4.
  3. increment is executed and the loop proceeds at 2.
  4. the loop ends.
In order to properly begin an SPI transaction, SPI.beginTransaction() must be called before statement is executed. We can do that in the initialization, when our anonymous struct is constructed - but we can't use its constructor for that, since it is anonymous. But it can have a named helper for which a constructor can be defined. This is done in the macro.

The helper also has a member done_ which is properly initialized to false in the constructor as well. We need that to track the number of executions of statement, which should be exactly one. As soon as increment is executed, done_ is set to true. The next check of condition will fail. Great - the loop is executed only once.

Now we also need to make sure that SPI.endTransaction() is called after statement was executed, no matter how and when the loop is ended (either because it finished or because return was called, there are a few ways). We cannot rely on increment for that if there is an intermediate return. What we can rely on, however, is that all destructors are called when the loop is finished - so SPI.endTransaction() is in the helper's destructor.

The ATOMIC_BLOCK macro inspired me here, but I needed this stack exchange answer for the anonymous struct thing.

A pull request for Paul is underway.
 
I'd rather suggest SPI_TRANSACTION_BLOCK, because the term ATOMIC is used for contexts in which no interrupts are allowed at all (aren't they?). But they probably won't mind because ATOMIC looks more familiar and would be no complicated new term to learn.
 
Yes, SPI_TRANSACTION_BLOCK is better.

I want to allow some time to collect feedback from Arduino, in case they have any interest to include this in their SPI.h.

Their mail list can be a pretty painful and slow process. I want to allow at least a couple weeks for their input. Since the next Teensyduino release is at least 3 months out, there still should be plenty of time.
 
Serious problem: If the macro is used inside another for loop, the expected bahavior of e.g. break and continue is borked:

Code:
for (uint8_t i = 0; i < 5; i++)
{
  SPI_TRANSACTION_BLOCK(settings)
  {
    if(something)
    {
      break; // <- this should break the outer for loop, but won't
    }
  }
}
 
An alternative would be to introduce a scope guard class:
Code:
struct SPI_transactionGuard
{
  SPI_transactionGuard(const SPISettings& settings)
  {
    SPI.beginTransaction(settings);
  }
  ~SPI_transactionGuard(const SPISettings& settings)
  {
    SPI.endTransaction();
  }
};
which can be used like
Code:
for (uint8_t i = 0; i < 5; i++)
{
  SPI_transactionGuard guard(settings);
  if(something)
  {
    break; // <- this correctly breaks the for loop
  }
}
The only downside of this is that SPI_transactionGuard can be stored, which can't be done with the anonymous struct in SPI_TRANSACTION_BLOCK. The whole point of the macro was to disallow non-local storage of the guard class.

However, we have now have at least two options to make matching pairs of beginTransaction() and endTransaction() a bit easier.
 
Last edited:
Status
Not open for further replies.
Back
Top