Skip to main content
added 122 characters in body
Source Link

As has been pointed out above, theThe nano (uno) SPI library does not have an 'SPI slave' mode. But that's not a big deal; we'll just set and read control and data registers manually.

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.

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 nano (uno) SPI library does not have an 'SPI slave' mode. But that's not a big deal; we'll just set and read control and data registers manually.

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.

deleted 15 characters in body
Source Link
#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.ss;
}

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

}
#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 

}
#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 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 

}
Source Link

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 

}