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.
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.
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.
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.
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.
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.