Sending multiple bytes quickly from SPI slave to master

I have two Arduinos communicating with SPI. The slave sends 12 bytes to the master and then goes to sleep until awakened by a pin interrupt (pin tied to SS).

I have this working fine, except that I have to add a 20 uS delay between each request from the master, as suggested by Nick Gammon on his page: http://www.gammon.com.au/spi

I'd like the transfer to happen as fast as possible, and the delay adds an overhead of 240 uS, which makes it very slow for SPI (almost as slow as I2C). My question is: Is there a way to reliably send/receive 12 bytes without adding delays? Here's my current working code:

Master (controller):


#include <SPI.h>

byte data[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

void setup(void) {

    pinMode(2, OUTPUT);
    digitalWrite(2, HIGH);  // ensure SS stays high for now
    SPI.begin();
    SPI.setClockDivider(SPI_CLOCK_DIV32);  //2 MHz for NRF52840
}


byte transferAndWait(const byte what) {
    byte a = SPI.transfer(what);
    delayMicroseconds(20);
    return a;
}

void loop(void) {


    digitalWrite(2, LOW);
    transferAndWait(0);
    for (byte i = 0; i < 12; i++) {
        data[i] = transferAndWait(i + 1);
    }
    digitalWrite(2, HIGH);


    for (byte i = 0; i < 12; i++) {
        Serial.println(data[i]);
    }
    Serial.println("");



    delay(50);
}

Slave (peripheral):



#include <SPI.h>
#include <avr/sleep.h>
#include <avr/power.h>

bool dataSent = false;

volatile byte data[] = { 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 };

void setup(void) {
    Serial.begin(115200);

    // turn on SPI in slave mode
    SPCR |= bit(SPE);
    pinMode(MISO, OUTPUT);
    SPI.attachInterrupt();
    pinMode(0, INPUT_PULLUP);
    attachInterrupt(0, pinFall, FALLING);
}

void pinFall() {  //just used to wake up MCU
}


// SPI interrupt routine
ISR(SPI_STC_vect) {
    byte c = SPDR;
    SPDR = data[c];
    if (c == 12) {
        delayMicroseconds(10);  //needed for sending the last byte before sleeping
        dataSent = true;
    }
}


void loop(void) {

    if (dataSent == true) {
        dataSent = false;

        set_sleep_mode(SLEEP_MODE_STANDBY);
        cli();                //turn off interrupts
        power_adc_disable();  //turn off the ADC to save a little more power
        sleep_enable();
        sei();  //turn on interrupts immediately before sleeping
        sleep_cpu();

        //sleeping__________________________________

        sleep_disable();  //disable sleep after waking
        power_adc_enable();
    }
}

I'd include SS into the protocol. Then the slave can wake up and prepare to transmit the first byte as soon as it is selected. When SS goes off then the slave can go to sleep again.

The smaller ATmega don't support slave transmission well, lacking a double transmit buffer. So the slave should prepare to send the next byte with the SPI interrupt or polling SPIF. Then the only requirement is a long enough gap between bytes sent by the master, so that the slave can prepare in time the next byte to be sent. The slave should send the next byte before reading the input buffer, this sequence is supported by the double buffered input.

Thank you. The SS pin on the slave is tied to pin 2 on the master as well as to the interrupt pin. Is that what you mean by including SS in the protocol? I do like the idea of being sure that SS is low before going to sleep.

Are you saying that less time is needed between transferring each byte if the slave sends each byte before reading SPDR? I don't see how that saves time, but I suspect I'm misunderstanding. Ideally I'd like to be able to send 12 bytes consecutively without even having to read what the master has sent, but when I've tried that they seem to get jumbled.

This may give an Write Collision error in the library.

So if I understand correctly, the speed of transfer using an Arduino as a slave is limited because each byte has to be put in the buffer individually by the software, is that correct? I was hoping that SPI communication between two Arduinos would be much faster than I2C, but it seems like they'll be in the same order of magnitude.

That's how things work if no DMA is available.

I2C defaults to 100 kHz while SPI goes up to 2 MHz.

But if each transfer needs an extra 20 uS delay, the actual speed of SPI is much lower, like 40 bytes per mS, correct? In reality that's not much faster than I2C at 400 kHz. I just want to make sure because I'm new to all this and I feel like I may be missing something important :slight_smile:

Why does it? Why 20 µs?

Can you measure how long it takes from an interrupt to putting a byte into the SPI output register?

I haven't measured it directly, but I start losing data if I lower the delay to much less than 20 uS. I started with that number because that's what Nick Gammon suggested, but it seems about right empirically. I am using an 8MHz AVR as the slave.

When I want to do that, I set an output HIGH while entering the ISR and set it LOW while exiting. An oscilloscope tells me how long the ISR (or any portion of code) takes (+ 2 x 125ns for the PORTB |= 0b00100000; and PORTB &= ~0b00100000; "timing" instructions).

Which makes it a bad choice for operation as an SPI Peripheral (AKA "Slave"). A better choice would be to use an Arduino Nano Every which has an ATmega4809 with double buffering.

The peripheral/slave SPI cannot load the data register fast enough before the data starts shifting out, so the controller/master needs the delay. And 20 µs might not be enough if some interrupt occurring adds additional latency to the peripheral response. Basically you need to make sure nothing else is happening in the peripheral microcontroller when the transfers are happening, such as the system clock tick interrupt.

Perhaps add another signal line from the peripheral to the controller indicating the the peripheral is ready for the shifting to begin. Then no additional delay would be necessary as the two devices are effectively handshaking.

Just so I understand-- even if the peripheral is ready for the shifting to begin, the delay would still be needed after every byte if I need to transfer 12 bytes consecutively, correct?

Correct, but IMO 1 µs should be sufficient if the slave is coded properly.

Wow! Great news-- you are absolutely right, the long delay is only needed before the first reading, to give the peripheral time to wake up from Standby. I should have tried this yesterday, but it didn't occur to me, and I misunderstood what you and DrDiettrich were saying.

I also didn't realize until you mentioned it that a lot of my data glitches were coming from the Timer0 interrupt. After disabling all the timers I now only need a five uS delay before the first transfer, and I actually don't seem to need any delay between transfers, but I'm going to add a few uS to be safe. All my data seems rock-solid now. Thanks so much all of you for your help!

Such a delay better is made after selecting the slave. IMO the entire wake up and sleep should be triggered by SS. Then the slave will not wake up on communication with another slave.

Yes, I wasn't clear-- I'm adding a 10uS delay after setting SS low, which seems to be plenty of time for the AVR to wake up. Then I'm adding a 1uS delay between each transfer. The majority of my issues were definitely stemming from the Timer0 interrupt. The wakeup is currently triggered by SS, but then the slave is reading several sensors after the transfer, so it just goes to sleep after reading the sensors and processing the data (my initial code above was just for testing the SPI timing).

It turns out that I'm having some issues again now that I've turned on Bluetooth on the controller device (NRF52840). The SoftDevice interrupts are causing problems with the SPI timing, causing bad data every once in a while. The thing that I don't understand is that the problem goes away if I add a long delay (300 uS) on the peripheral after SS goes high, before reading my sensors and going to sleep again. I would have thought that the SPI transactions would be completely done after SS goes high-- is that not the case? Here's code from the loop() on the peripheral. You'll see where I've added the 300 uS delay:



void pinFall() {}  //just used to wake up MCU. Triggered when SS changes.




// SPI interrupt routine
ISR(SPI_STC_vect) {
    byte c = SPDR;
    SPDR = toneholePacked[c];  //send a byte to the NRF52840 as requested
}



void loop() {

    //sleep after getting and preparing the readings.
    if (dataReady) {

        set_sleep_mode(SLEEP_MODE_STANDBY);
        cli();                //turn off interrupts
        power_adc_disable();  //turn off the ADC to save a little more power
        sleep_enable();
        sei();  //turn on interrupts immediately before sleeping
        sleep_cpu();

        //sleeping__________________________________

        sleep_disable();  //disable sleep after waking
        power_adc_enable();


        while (digitalRead(SS) == HIGH) {
            //digitalWrite(2, HIGH);  //timing test for scope
            //digitalWrite(2, LOW);
        }

        delayMicroseconds(300);
        dataReady = false;
        readSensors();
    }
}

Right. The master should not break an ongoing transaction by deactivating the slave. Unless the last partially received byte is of no importance.

Well, that was silly of me-- I was pausing while SS was HIGH instead of LOW. It's better now, though I do still get the occasional glitch when I have Bluetooth turned on.