Library for SPI on AVR

I wrote a library to handle SPI communications on AVR processors. It's different from SPIClass in these ways:

  • The new class, SPIAVRClass uses the interrupt for the SPI peripheral.
  • There is a begin function to exchange a buffer for a buffer as master.
  • There is another begin function to act as SPI slave and wait for a message from the master.
  • There is no protection if these functions are called from an interrupt handler. Don't do that.

A program using this library checks for completion of the transaction. The sketch doesn't need to handle every byte. It acts only when the transaction is complete.

It isn't yet added to the library manager. But it's at:

There are several drawbacks in your implementation:

  • you store the pins of MOSI, MISO and SCKL although this is just wasted memory
  • you set pin modes of MOSI, MISO and SCKL although the hardware is responsible for the pins in SPI mode
  • your implementation is not able to handle multiple devices on the SPI bus as a master

I cannot find a simple advantage compared to SPIClass but above drawbacks in addition to the one you mentioned yourself (not callable inside interrupt handlers). It's far from being a general use SPI implementation so you might have to add the special cases where this library might help. The only I currently see is to misuse the SPI interface as a replacement of an UART connection. Correct me if I'm wrong.

pylon:
The only I currently see is to misuse the SPI interface as a replacement of an UART connection.

Maybe because of its non-blocking nature.

As the UART (Serial) implementation, he makes SPI buffered; thus an asynchronous I/O stream (i.e. transfer() does nothing until you call a "commit" function, maybe flush() or endTransaction(); who knows). Or maybe not and transmission begins as soon as some data is placed (as Serial does).

To make it interrupt-driven, are you sure there is a flag for SPI transfer complition? I recall one for SPI controller ready, but not for transfer complition.

To all this... are you trying to use the SPI peripheral like you do on UART serial? Why?
I'm not asking like it is a bad thing, I'm asking the purpose (or motivation) behind it. I mean, having print() on SPI might be quite handy for certain applications; but which ones?
This is what pylon is asking for.

Pylon makes some interesting points. SPIAVRClass isn't a substitute for SPI for all purposes.

It has these drawbacks:

  • It is AVR only.

Arduino libraries for SPI allow some consistency across processors.

  • A data transfer cannot be initiated from an interrupt handler.

I don't know if this is done in other libraries. Is it a vital feature? Why is it needed?

  • It hasn't yet stood the test of time.

The other cited drawbacks:

  • you store the pins of MOSI, MISO and SCKL although this is just wasted memory

You could say the same about the register addresses for SPCR, SPDR and SPSR as well as SS. Seven wasted bytes. I guess I should confess a hidden motivation. I'm planning on expanding the library to the Atmega 328PB which has two SPI ports.

  • you set pin modes of MOSI, MISO and SCK although the hardware is responsible for the pins in SPI mode

The SPI hardware "overrides" SPI inputs: MISO in master mode; MOSI, SCK and SS in slave mode. But the outputs are "user defined". See the datasheet, 23.2 SPI Overview and Table 23-1. The sample code sets the DDRs accordingly.

I'll paraphrase the other comments in pylon's message as: "Why would anyone want to use SPIAVRClass instead SPIClass?"

The first answer is simple. It handles slave mode.

The other answer is a little more complex. Consider this:

Serial.print ( ... );
next line of code

The call to Serial.print() puts something into a buffer and exits. The next line of code is executed immediately. Whereas SPI.transfer() has a wait loop. The next line isn't executed until the transfer is complete.

The SPI peripheral has some distinct idiosyncrasies. The two data streams are simultaneous. But only one end of the wire determines when transfers are to occur. SPIAVR expects both master and slave to be prepared with data to send.

Thank you for your questions. They are excellent guidance to me as I prepare to add documentation to the library.

Lucario448:
Maybe because of its non-blocking nature.

As the UART (Serial) implementation, he makes SPI buffered; thus an asynchronous I/O stream (i.e. transfer() does nothing until you call a "commit" function, maybe flush() or endTransaction(); who knows). Or maybe not and transmission begins as soon as some data is placed (as Serial does).

To make it interrupt-driven, are you sure there is a flag for SPI transfer complition? I recall one for SPI controller ready, but not for transfer complition.

To all this... are you trying to use the SPI peripheral like you do on UART serial? Why?
I'm not asking like it is a bad thing, I'm asking the purpose (or motivation) behind it. I mean, having print() on SPI might be quite handy for certain applications; but which ones?
This is what pylon is asking for.

There is a dataIsReady() function called when the buffer is full.

SPIAVR uses interrupts in a fashion similar to that used in Serial. But SPI ports don't fit into Stream classes so comfortably.

I suppose there are a myriad of applications for which SPI is more suitable than UART. SPI is in common use for chips that act as servers in slave mode.

Consider this:

loop() {
if ( SPIAVR.dataIsReady() )
processCommand();
runTheMachine();
}

In this case almost all the processor time is spent in runTheMachine(). The command is processed only when the entire command has been received.

Sketches that don't require efficient use of the processor should use SPIClass. Arduino libraries aren't written to maximize the use of the processor. Most of the time, you don't care. But if you do and you are willing to be a little more careful, there are great rewards to the more venturesome in the power of these processors.

The ISR.

hmm.

https://www.nongnu.org/avr-libc//user-manual/group__avr__interrupts.html

When an ISR spins up it saves the registers that may be in use (e.g. program counter which points to the next instruction, various working registers...). Once the ISR code is running it is a walled garden, only global (static) volatile variables should be used to interact with the outside.

It is standard practice to make ISR's fully self-contained functions. The code running in an ISR walled garden should probably not try to reference variables in a C++ class (e.g. _inputBufferPosition) instance.

ron_sutherland:
The ISR.

SPIAVR/SlaveAndMaster.ino at 587d96e58474c8a7767b320ae0e36f94f27f6ffc · panchodanny/SPIAVR · GitHub

hmm.

avr-libc: <avr/interrupt.h>: Interrupts

When an ISR spins up it saves the registers that may be in use (e.g. program counter which points to the next instruction, various working registers...). Once the ISR code is running it is a walled garden, only global (static) volatile variables should be used to interact with the outside.

It is standard practice to make ISR's fully self-contained functions. The code running in an ISR walled garden should probably not try to reference variables in a C++ class (e.g. _inputBufferPosition) instance.

It's ok. SPIAVR is a global. So _inputBufferPosition is in global memory.

DannySwarzman:

  • A data transfer cannot be initiated from an interrupt handler.

I don't know if this is done in other libraries. Is it a vital feature? Why is it needed?

Actually, very few libraries do that to work properly. The only example I can give you is TMRpcm; this library reads data from a SD card inside an ISR, that reading eventually ends up in doing SPI transactions... INSIDE AN INTERRUPT!

In fact, is not a good practice doing I/O operations inside an interrupt routine (ISR); specially the blocking ones.

pylon:

  • you store the pins of MOSI, MISO and SCKL although this is just wasted memory

DannySwarzman:
You could say the same about the register addresses for SPCR, SPDR and SPSR as well as SS. Seven wasted bytes. I guess I should confess a hidden motivation. I'm planning on expanding the library to the Atmega 328PB which has two SPI ports.

In that case, you still don't need that many variables because hardware peripherals are hard-wired to specfic pins; you can't change them by software unless it's a bitbanged SPI port.
At most you need one variable, to indicate which SPI controller the instance (object) should manipulate.

DannySwarzman:
handles slave mode.

How?

Remember that SPI is designed to always transfer data in both directions and at the same time (at least as the master is concerned); so the slave Arduino somehow has to always have data to send as soon as the master begins to pulse the clock line. Only in I2C isn't that time-critical.

DannySwarzman:
The other answer is a little more complex. Consider this:

Serial.print ( ... );
next line of code

The call to Serial.print() puts something into a buffer and exits. The next line of code is executed immediately. Whereas SPI.transfer() has a wait loop. The next line isn't executed until the transfer is complete.

Yup, that's the difference between blocking (synchronous) I/O and non-blocking (asynchronous) I/O.

DannySwarzman:
The SPI peripheral has some distinct idiosyncrasies. The two data streams are simultaneous. But only one end of the wire determines when transfers are to occur. SPIAVR expects both master and slave to be prepared with data to send.

So I guess your library deals with that, because that's the caveat of the SPI slave I've mentioned before.

DannySwarzman:
There is a dataIsReady() function called when the buffer is full.

Callback or it has to be checked in the main program?

Since libraries are supposed to be coded for general usage; how you determine when "data is ready" without being used in a specific application?

DannySwarzman:
But SPI ports don't fit into Stream classes so comfortably.

Why not? Any data stream with some sort of buffer works perfectly with the Stream class (or Print if it's only for output).

DannySwarzman:
I suppose there are a myriad of applications for which SPI is more suitable than UART. SPI is in common use for chips that act as servers in slave mode.

Consider this:

loop() {
if ( SPIAVR.dataIsReady() )
processCommand();
runTheMachine();
}

In this case almost all the processor time is spent in runTheMachine(). The command is processed only when the entire command has been received.

Pretty much like Serial works; being dataIsReady() somewhat a counterpart of available().

So there's no doubt you have created an interrupt-driven version of the SPI library.

I would like to see something similar with analogRead(); since this is another "wait until it's done" function.
AVR ADCs work by successive approximation, a process that takes a while to complete. This is why an interrupt flag for this purpose exists; and analogRead() sets some registers to initiate a conversion, but it stays there until that interrupt triggers to finally give the analog reading. analogRead() enables the required interrupt; so you'll never see it getting stuck when called by another ISR.

As you can see, analogRead() has the exact same issue of SPI.transfer(); and both can be addressed in a similar way indeed.

DannySwarzman:
Sketches that don't require efficient use of the processor should use SPIClass. Arduino libraries aren't written to maximize the use of the processor. Most of the time, you don't care. But if you do and you are willing to be a little more careful, there are great rewards to the more venturesome in the power of these processors.

On low-resources environments, this is a well-known trade-off: you have either convenience or performance, but never both. A very famous example is digitalWrite() vs PORTx.

PD1: ironically, successive approximation ADCs require a DAC, which is what supposedly AVRs lack (excluding PWM).

PD2: maybe the reason why transfer() is blocking it's because of how long it takes to shift out a byte in the "full speed". At such speed, 8 bits takes up to 16 clock cycles (1 microsecond at 16 MHz); an asynchronous data retrieval will obviously delay the transfer of multiple bytes because there's no way the program would attend the completion at the exact moment.

Making transfer() non-blocking when the SPI controller operates at full speed may be actually worse, 16 clock cycles hardly makes up any meaningful processing. If the SPI operates at half speed or less, then it's a big deal; because even 32 clock cycles are enough for some meaningful processing.