Using SPI at full speed (which is 4 Mhz for a nano or uno)
Below is a tested and working example of a nano / uno working as SPI master and a nano / uno working as SPI slave, each transmitting 10 bytes.
The master uses an SPI clock frequency of 4 MHz.
Working principle
As has been pointed out above, the SPI slave does not have a multi-byte buffer. So, it needs time to fetch each individual byte received from the SPI data register and to write a byte to be transmitted into the same register. The only reliable way to do this is using interrupts. But the master must insert a small delay after each transmission (10 microseconds in the example below) to allow the slave to do just that.
The SPI clock frequency has little impact on this, and because the SPI.transfer() method is blocking, we have interest in choosing the highest possible value: 4 Mhz, resulting in 2 microseconds to transmit one byte. In total, including the 10 microseconds delay between bytes, this gives 12 microseconds per byte. An Arduino nano (uno) as slave is simply not fast enough to decrease the 10 microsecond wait-time.
SPI slave interrupts
We'll use two separate interrupts:
- ISR(PCINT0_vect): after the falling edge of input pin SS (Slave Select): load the first byte in the SPI data register.
This is a 'pin change' interrupt, it fires for falling and rising edges, so we must single out the falling edge by reading the pin state in the ISR first.
- ISR(SPI_STC_vect): at the end of each transmission: retrieve a byte and load the next byte to send.
Working SPI master and slave code for Arduino nano(uno)
SPI master code:
#include <SPI.h>
// nano / uno as SPI master: pins (MOSI, MISO, CLK: pin 11 , 12, 13) are fixed
// any free pin can be selected as output pin SS (pin 10 is often used as a default in libraries using SPI internally)
// slave buffer size can not be greater than the master buffer
#define MASTER_BUFFER_SIZE 10
#define SLAVE_BUFFER_SIZE 6
byte txBuffer[MASTER_BUFFER_SIZE];
byte rxBuffer[SLAVE_BUFFER_SIZE];
void setup() {
Serial.begin(115200);
delay(2000);
pinMode(SS, OUTPUT); // initialize SS output pin (use default = pin 10)
digitalWrite(SS, HIGH); // no slave selected
SPI.begin();
// Fill master transmit buffer with example values
for (uint8_t i = 0; i < MASTER_BUFFER_SIZE; i++) {txBuffer[i] = 0x80 + i;}
Serial.println("SPI Master ready");
}
void loop() {
digitalWrite(SS, LOW); // select the Arduino SPI slave
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); // 400 KHz: maximum speed for reliable data transmission
// Interleaved transfer: send master bytes, read slave bytes (slave byte count <= master byte count)
for (uint8_t i = 0; i < MASTER_BUFFER_SIZE; i) {
// A SMALL delay helps the SPI slave (e.g., an Arduino UNO) to fill up its transmit buffer before A NEXT transmission starts.
// The slave uses interrupts to do that, so a small delay (10 microseconds) is enough.
delayMicroseconds(10); // **** IMPORTANT ****
byte received = SPI.transfer(txBuffer[i]); // full-duplex transfer of 1 byte
if (i < SLAVE_BUFFER_SIZE) {rxBuffer[i] = received;} // read received byte from SPI slave
i++;
}
digitalWrite(SS, HIGH); // deselect the Arduino slave
SPI.endTransaction();
// Print received bytes
Serial.print("Received bytes: ");
for (uint8_t i = 0; i < SLAVE_BUFFER_SIZE; i++) {
Serial.print(rxBuffer[i], HEX);
Serial.print(' ');
}
Serial.println();
}
and the SPI slave code:
#include <SPI.h>
// nano / uno as SPI slave: pins (SS, MOSI, MISO, CLK: pin 10, 11 , 12, 13) are fixed
// input pin SS (10) controls MISO pin state (tri-state if SS input = HIGH, MISO output if SS input = LOW)
// slave buffer size can not be greater than the master buffer
#define MASTER_BUFFER_SIZE 10
#define SLAVE_BUFFER_SIZE 6
volatile byte rxBuffer[MASTER_BUFFER_SIZE];
volatile byte txBuffer[SLAVE_BUFFER_SIZE];
volatile byte rxIndex = 0;
volatile byte txIndex = 0;
volatile byte toSend = 0; // lookahead
void setup() {
Serial.begin(115200);
pinMode(MISO, OUTPUT); // but SPI hardware will tri-state MISO output as long as SS input is high
// Initialize TX buffer (example)
for (uint8_t i = 0; i < MASTER_BUFFER_SIZE; i++) { // technically, same size as master buffer
txBuffer[i] = 0x20 + i;
}
// SS input: enable pin change interrupt
PCICR |= 1 << PCIE0; // pin change interrupt control register: enable PORT B
PCMSK0 |= 1 << PCINT2; // pin change mask register 0: enable pin 10 (PB2)
// SPI control register: enable SPI in slave mode, enable interrupt
SPCR = _BV(SPE) | _BV(SPIE); // enable SPI, enable SPI interrupts
SPCR &= ~_BV(MSTR); // slave mode
Serial.println("SPI Slave ready");
}
void loop() {
static uint8_t lastSSstate = HIGH; // detects Slave Select pin state change
uint8_t ss = digitalRead(SS); // slave select state ?
// Detect end of transaction (time to print results now)
if (ss == HIGH && lastSSstate == LOW) {
Serial.print("Received bytes: ");
for (uint8_t i = 0; i < MASTER_BUFFER_SIZE; i++) {
Serial.print(rxBuffer[i], HEX);
Serial.print(' ');
}
Serial.println();
}
lastSSstate = ss;SPI.
}
// SPI interrupt: fires when SS (Slave Select) input goes low
ISR(PCINT0_vect) {
static byte count{ 0 }; // optional: replace first byte to transmit with a 1-byte message counter
// SS pin 10 (PB2) is the only PORTB pin with Pin Change interrupts enabled: no need to identify the pin
if (!(PINB & 0b100)) { // falling edge of SS input pin
rxIndex = 0;
txIndex = 0;
SPDR = count++; /* or txBuffer[txIndex]; */ // load SPI data register with byte #0 (first byte)
toSend = txBuffer[++txIndex]; // byte #1: prepare for speedy load in interrupt routine(look ahead)
}
}
// SPI interrupt: fires after each byte is transferred
ISR(SPI_STC_vect) {
// reading and loading SPI data register must be done ASAP, master must delay next transmission for a few microseconds to allow this R/W operation
byte received = SPDR; // recover byte #n that was received, load byte #n+1 to be transmitted
SPDR = toSend;
if (txIndex + 1 < SLAVE_BUFFER_SIZE) { toSend = txBuffer[++txIndex]; } // byte #n+2: prepare for speedy load in interrupt routine(look ahead)
else { toSend = 0xff; }
if (rxIndex < MASTER_BUFFER_SIZE) rxBuffer[rxIndex++] = received; // store received byte #n in buffer
}