Arduinos are often used as controllers of SPI peripherals.1) Here, I want to explore using an Arduino itself as an SPI peripheral. I will only be considering the case of peripherals based on the ATmega328P.
Nick Gammon's incredibly informative thread on SPI - Serial Peripheral Interface - for Arduino published code for both a controller and a peripheral.
The controller in this example transfers the characters in the string “Hello, world!\n”
to the peripheral every second (give or take).
The peripheral accepts the characters sent from the controller, and when a newline is received it outputs the accumulated string to Serial
output. The process on the peripheral side is interrupt based. On each SPI interrupt, the ISR grabs a byte from the SPI Data Register (SPDR) and places it in a buffer. When a newline is received, the ISR sets a flag that tells the main loop that it's time to render the string to Serial
output.
The magic that makes the microcontroller act as a peripheral is a bit in the SPI Control Register (SPCR):
SPCR |= bit (SPE);
Note that byte_pos
and process_it
are declared in the code as volatile
because they are changed in the ISR.
The code works as expected, as written and when using the currently preferred way to configure and begin/end SPI transactions: SPI.beginTransaction()
and SPI.endTransaction()
. See Paul Stoffregen's Better SPI Bus Design in 3 Steps for a brief discussion about this and some other best practices.
Gammon offers another example in the same thread where the controller asks for data and the the peripheral provides it. The controller uses SPI.transfer() to simultaneously issue a request and receive a response.
The controller and peripheral work as expected, with and without using SPI.beginTransaction()
and SPI.endTransaction()
. The key to understanding what's going on is that the data that is sent back to the controller is whatever is in the SPDR at the start of the transfer. In other words, this is a “read before set” process.
For systems that use multiple SPI peripherals on the same SPI bus, it's important the the outputs of the peripherals become tristated, or go high impedance, when they aren't in use. In general, this can be done by using devices that explicitly have tristateable outputs, using open collector/open drain outputs and making sure they are turned off, and/or re-configuring the microcontroller outputs as an inputs.
Nick Gammon in the Arduino forums assures us the ATmega328 does the right thing:
From the Atmega manual, p171:
When the SPI is configured as a Slave, the Slave Select (SS) pin is always input. When SS is held low, the SPI is activated, and MISO becomes an output if configured so by the user. All other pins are inputs. When SS is driven high, all pins are inputs, and the SPI is passive, which means that it will not receive incoming data. Note that the SPI logic will be reset once the SS pin is driven high.
Paul Stoffregen's Better SPI Bus Design in 3 Steps presents an electronic method for testing whether the POCI pin of a peripheral is tristated. It involves two 10K resistors, one placed as a pullup and the other as a pulldown on POCI. With the peripheral disabled, the voltage measured at POCI should be VCC/2. If you get something close to VCC or GND, then the peripheral isn't tristating the output.
I used Gammon's SPI-with-data sketches (above) with long delays strategically inserted to make capturing the POCI voltage easy and the needed resistors, and I'm happy to report I measured VCC/2 when the peripheral was inactive. Yay.
Another way we can confirm this is actually the case is to use two Arduino peripherals and make sure they both work. TL;DR: They do.
I did this using three Unos: one as the controller and two as peripherals. One peripheral was configured as an even number counter, the other as an odd number counter. If you want to see the nitty-gritty, feel free to have a look at the code for the controller, the evens counter, and the odds counter.
In this setup, pin 10 (the default SS) was used as the chip select for the evens counter and pin 9 was used as the chip select for the evens counter.
I'm happy to report that things worked as expected, with one small exception. In the counters, I am initializing SPDR
to 0 (even) or 1 (odd) in setup()
. This seems to work as expected on a hardware reset. However, from a cold powerup, the value seems to be ignored. I'm still pondering this. The workaround is to always have the controller issue a reset command before first use.
You'll also see that I am wondering about setting both POCIs to be an output at the same time — and before SPCR |= bit(SPE);
intervenes and starts tristating outputs when required. In this case, things worked just fine. But be careful about situations where other POCIs may not appreciate being pulled low at startup. Maybe that's none of them.
In addition to protecting bus access with SPI.beginTransaction()
/ SPI.endTransaction()
and verifying that POCI tristates, both of which we have which we have discussed already, Paul Stoffregen's Better SPI Bus Design in 3 Steps also recommends you use pullup resistors on chip selects and/or a software workaround in setup()
:
void setup() { pinMode(10, OUTPUT); digitalWrite(10, HIGH); pinMode(OTHER_SPI_PERIPHERAL_CS, OUTPUT); digitalWrite(OTHER_SPI_PERIPHERAL_CS, HIGH); delay(1); // now it's safe to use SPI. }
I'm not sure what the exact issue the above is intended to remedy, but explicitly de-activating all chip selects before starting the show seems like good practice no matter what. In addition, resistors are inexpensive, so adding an additional pullup per peripheral is cheap insurance.