Programming strategies for SPI

A very common use of SPI interfaces is to communicate with a device that is designed to act as SPI slave. The Arduino acts as master. Both families of Arduino support this over the ICSP connector using Arduino libraries.

But if your goal is to implement both sides of the SPI interface, things can get interesting. The slave needs to be ready to be receive data.

Registers and memory-mapped I/O

Some memory addresses on an AVR processor aren't associate with memory. Instead, they are associated with specific hardware inside the chip.

For example, to send data to an output pin, a program sets a value to a special address in memory. For example, a program on AVR 328p can send data to pins D0 through D7 by writing to location 0x0B, represented by the constant PortD. Always use the symbolic constant instead of the number to keep code legible.

But the program doesn't write data directly to I/O pins for SPI communications. Rather, it writes to a device that lives inside the chip. For example, a program could write to location 0x2E, represented by the constant SPDR. When the program on the master writes to that location, the SPI hardware starts sending a sequence clock pulses and data to transfer one byte.

These magic addresses are called registers. They are described in the datasheet for the processor. The symbols for the register addresses are in the #include files that are inside the Arduino application.

The symbols used for an AVR processor are not the same as those used by the SAMD21. The Arduino IDE compiles with the appropriate #include files according to which Arduino model has been selected in the menu. For full details on registers, refer to the datasheet for the AVR processor or the datasheet for the D21 processor.

Libraries

Normally you don't write code to address registers. You use digitalWrite() to write to a pin. You use the SPIClass instead of writing to SPI registers. This is the best way to work most of the time. The libraries that come with Arduino are built with a lot of thought. No reason to reinvent.

But there are times when you need to go outside the limitations of the Arduino libraries. For example there is no library for AVR slave mode. The SPI on AVR processors is fairly simple so you can live without a library more easily. The SPI_On_Uno.ino example does just this.

There is a downside to writing to hardware registers. Your sketch will run only on the processor for which it is written.

Polling vs. Interrupts

There are different ways you can structure your program to communicate over SPI.

Polling

The loop() function checks each cycle to see if there is a byte available in the SPI. If so, it exchanges a byte. If your loop is not complicated and you don't need to be parsimonious with processor time, this method works great.

Interrupts

Suppose that you want to do something that occupies the processor and is messy to interleave with code to check the SPI. For example, the program could have a lot of calculating to do or be sending a long message over the console.

The use of an interrupt may solve these problems. A program doesn't need to check the SPI port for every byte. The main loop can check less frequently to see if a buffer is full. This is possible if you write an interrupt service routine. Every time a byte is available, the processor automatically calls the interrupt service routine.

In that routine, you can just read one byte into the input buffer and write one byte from the output buffer. When you done, the processor takes control back to the loop. The loop continues to do its intense processing and only needs check the SPI in the time it takes to fill a buffer, not in the time it takes to receive a byte.

Direct Memory Access (DMA)

On D21 processors, there is another level of hardware control that isn't available on the AVR models.

Instead of writing an incoming byte to a location in memory, the SPI hardware can be configured to write it to a DMA channel. The DMA controller hardware completes the transfer into memory.

To start, a program sets up the channel with a starting address and size of a buffer. The hardware will move through the buffer as it transfers data.

When an input buffer is full or output buffer empty, the DMA controller triggers an interrupt. It is possible to provide a second input or output buffer to take over immediately without waiting for the interrupt to finish.

DMA on the SAMD21 can be used to transfer data from memory to peripheral, peripheral to memory, memory to memory and peripheral to peripheral. Intriguing possibilities.

At this moment, we have no sample code to offer for DMA on the D21. Stay tuned.