But the datasheet says that speeds of Fosc/4 are possible in slave mode, which would be 4MHz in this case. Would that be achievable if I removed the ISR and just used a busy loop?
Well, yes the datasheet has an example of doing a busy loop:
char SPI_SlaveReceive(void)
{
/* Wait for reception complete */
while(!(SPSR & (1<<SPIF)))
;
/* Return Data Register */
return SPDR;
}
Regardless of what they claim, what you intend has to be physically possible. So with Fosc/4, assuming a 16 MHz clock, you have 2 μs for a byte to arrive (250 ns * 8). So the hardware may well be able to receive one byte in that time, and you may just have enough clock cycles to notice it arriving and plonk it into memory somewhere (although things could be tight) but you won't have time to send a different response after each incoming byte, because you only have around 250 ns to do it in, and the loop that detects the interrupt will probably use up most or all of that.
After a bit of mucking around, I got it to work at 250 kHz where the objective was just for the slave to return some data (after all, I presume that is what the slave is supposed to do).
Master
#define MOSI 12
#define SCK 13
#include <SPI.h>
#include "pins_arduino.h"
#include <avr/wdt.h>
void setup(void) {
Serial.begin(9600);
digitalWrite(SS, HIGH);
pinMode(SS, OUTPUT);
digitalWrite(SS, HIGH);
digitalWrite(SCK, HIGH);
pinMode(SCK, OUTPUT);
pinMode(MOSI, OUTPUT);
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV64);
Serial.println("\n\nSTARTING...\n");
}
void do_transfer(byte attempt, byte payload) {
// packet size of 200
byte buffer[200];
// perform transfer
digitalWrite(SS, LOW);
delay (1); // give slave time to react
SPI.transfer(buffer, sizeof(buffer));
digitalWrite(SS, HIGH); // SS is pin 10
// tally results
byte successes = 0, failures = 0;
for (byte index = 0; index < sizeof(buffer); index++)
if (buffer[index] == index) successes++; else failures++;
// print results
Serial.print("Attempt ");
Serial.print(attempt);
Serial.print(" with payload ");
Serial.print(payload);
Serial.print(": ");
if (failures == 0)
Serial.print("SUCCESS");
else {
Serial.print(successes);
Serial.print(" correct bytes, ");
Serial.print(failures);
Serial.print(" incorrect bytes");
for (byte index = 0; index < sizeof(buffer); index++) {
if (index % 10 == 0)
Serial.print("\n\t");
if (buffer[index] == index)
Serial.print("-");
else
Serial.print(buffer[index]);
if (index < sizeof(buffer) - 1)
Serial.print("\t");
}
}
Serial.println();
Serial.println();
}
void loop(void) {
delay(800); // give the slave time to be ready
for (byte attempt = 1; attempt <= 10; attempt+=2) {
do_transfer(attempt, 0x00);
do_transfer(attempt+1, 0xFF);
wdt_reset();
}
while(true); // end of program
}
Slave
#include "pins_arduino.h"
#include <avr/power.h>
#include <avr/sleep.h>
const int TEST_SIZE = 200;
volatile byte my_data [TEST_SIZE];
volatile byte dummy;
ISR (PCINT0_vect)
{
// handle pin change interrupt for D8 to D13 here
// ignore transition to HIGH
if (digitalRead (SS) == HIGH)
return;
SPDR = 0;
for (byte i = 0; i < TEST_SIZE; i++)
{
SPDR = my_data [i];
while(!(SPSR & (1<<SPIF)))
{
// if SS went high, give up
if (PINB & (1 << 2))
return;
}
} // end of for loop
} // end of PCINT0_vect
void setup(void) {
pinMode(MISO, OUTPUT); // MISO needs to be an output
SPCR |= _BV(SPE); // enable SPI in slave mode
power_timer0_disable();
for (int i = 0; i < TEST_SIZE; i++)
my_data [i] = i;
// pin change interrupt for D10
PCMSK0 |= bit (PCINT2); // want pin 10
PCIFR |= bit (PCIF0); // clear any outstanding interrupts
PCICR |= bit (PCIE0); // enable pin change interrupts for D8 to D13
SPDR = 0;
set_sleep_mode (SLEEP_MODE_IDLE);
}
void loop (void)
{
sleep_mode ();
}
At the next clock divisor (SPI_CLOCK_DIV32) I started getting occasional errors. After all, Fclk/32 is only 2 μs per clock pulse and 16 μs per byte, and the necessary stuff in the inner loop must be taking that long — I didn't calculate the individual instruction times, you are welcome to do that. :)
I was hoping to get better performance than that, but then I realised that even with a tight sending loop in the slave, you can't afford to send before the master has acknowledged the previous byte (ie. that it has read it) so you still have the tight interval between the end of one byte and the start of the next, which is the time in which you have to prepare the next byte for sending.
It's not as bad if you are the master, because you control when the byte starts, so you can take as long as you want between bytes.
But the datasheet says that speeds of Fosc/4 are possible in slave mode, which would be 4MHz in this case. Would that be achievable if I removed the ISR and just used a busy loop?
Well, yes the datasheet has an example of doing a busy loop:
char SPI_SlaveReceive(void)
{
/* Wait for reception complete */
while(!(SPSR & (1<<SPIF)))
;
/* Return Data Register */
return SPDR;
}
Regardless of what they claim, what you intend has to be physically possible. So with Fosc/4, assuming a 16 MHz clock, you have 2 μs for a byte to arrive (250 ns * 8). So the hardware may well be able to receive one byte in that time, and you may just have enough clock cycles to notice it arriving and plonk it into memory somewhere (although things could be tight) but you won't have time to send a different response after each incoming byte, because you only have around 250 ns to do it in, and the loop that detects the interrupt will probably use up most or all of that.
After a bit of mucking around, I got it to work at 250 kHz where the objective was just for the slave to return some data (after all, I presume that is what the slave is supposed to do).
Master
#define MOSI 12
#define SCK 13
#include <SPI.h>
#include "pins_arduino.h"
#include <avr/wdt.h>
void setup(void) {
Serial.begin(9600);
digitalWrite(SS, HIGH);
pinMode(SS, OUTPUT);
digitalWrite(SS, HIGH);
digitalWrite(SCK, HIGH);
pinMode(SCK, OUTPUT);
pinMode(MOSI, OUTPUT);
SPI.begin();
SPI.setClockDivider(SPI_CLOCK_DIV64);
Serial.println("\n\nSTARTING...\n");
}
void do_transfer(byte attempt, byte payload) {
// packet size of 200
byte buffer[200];
// perform transfer
digitalWrite(SS, LOW);
delay (1); // give slave time to react
SPI.transfer(buffer, sizeof(buffer));
digitalWrite(SS, HIGH); // SS is pin 10
// tally results
byte successes = 0, failures = 0;
for (byte index = 0; index < sizeof(buffer); index++)
if (buffer[index] == index) successes++; else failures++;
// print results
Serial.print("Attempt ");
Serial.print(attempt);
Serial.print(" with payload ");
Serial.print(payload);
Serial.print(": ");
if (failures == 0)
Serial.print("SUCCESS");
else {
Serial.print(successes);
Serial.print(" correct bytes, ");
Serial.print(failures);
Serial.print(" incorrect bytes");
for (byte index = 0; index < sizeof(buffer); index++) {
if (index % 10 == 0)
Serial.print("\n\t");
if (buffer[index] == index)
Serial.print("-");
else
Serial.print(buffer[index]);
if (index < sizeof(buffer) - 1)
Serial.print("\t");
}
}
Serial.println();
Serial.println();
}
void loop(void) {
delay(800); // give the slave time to be ready
for (byte attempt = 1; attempt <= 10; attempt+=2) {
do_transfer(attempt, 0x00);
do_transfer(attempt+1, 0xFF);
wdt_reset();
}
while(true); // end of program
}
Slave
#include "pins_arduino.h"
#include <avr/power.h>
#include <avr/sleep.h>
const int TEST_SIZE = 200;
volatile byte my_data [TEST_SIZE];
volatile byte dummy;
ISR (PCINT0_vect)
{
// handle pin change interrupt for D8 to D13 here
// ignore transition to HIGH
if (digitalRead (SS) == HIGH)
return;
SPDR = 0;
for (byte i = 0; i < TEST_SIZE; i++)
{
SPDR = my_data [i];
while(!(SPSR & (1<<SPIF)))
{
// if SS went high, give up
if (PINB & (1 << 2))
return;
}
} // end of for loop
} // end of PCINT0_vect
void setup(void) {
pinMode(MISO, OUTPUT); // MISO needs to be an output
SPCR |= _BV(SPE); // enable SPI in slave mode
power_timer0_disable();
for (int i = 0; i < TEST_SIZE; i++)
my_data [i] = i;
// pin change interrupt for D10
PCMSK0 |= bit (PCINT2); // want pin 10
PCIFR |= bit (PCIF0); // clear any outstanding interrupts
PCICR |= bit (PCIE0); // enable pin change interrupts for D8 to D13
SPDR = 0;
set_sleep_mode (SLEEP_MODE_IDLE);
}
void loop (void)
{
sleep_mode ();
}
At the next clock divisor (SPI_CLOCK_DIV32) I started getting occasional errors. After all, Fclk/32 is only 2 μs per clock pulse and 16 μs per byte, and the necessary stuff in the inner loop must be taking that long — I didn't calculate the individual instruction times, you are welcome to do that. :)
I was hoping to get better performance than that, but then I realised that even with a tight sending loop in the slave, you can't afford to send before the master has acknowledged the previous byte (ie. that it has read it) so you still have the tight interval between the end of one byte and the start of the next, which is the time in which you have to prepare the next byte for sending.
It's not as bad if you are the master, because you control when the byte starts, so you can take as long as you want between bytes.
Your problem here is timing. I tried your setup, and found that I did indeed get occasional errors at 125 kHz. Disabling Timer 0 (as suggested by Chrisl) fixed that:
power_timer0_disable();
But while it now works up to 250KHz, I get many many errors at 500KHz.
OK, the time between clock pulses is now 2 μs, however as I document here the time taken to execute an ISR like SPI_STC_vect is 2.625 μs, therefore it can't keep up with having to put the data there every 2 s.
You actually have slightly more time than 2 μs because the critical time is the time between the end of the first byte (which is when the ISR is called) and the time before the start of the second byte, which is when the master will start sampling your response.
See screenshot below with the critical time boxed in yellow.
That time is 2.45 μs and the ISR takes 2.625 μs. Now some of that is the ISR epilogue which won't really matter that much, but you can see that things are tight. It's hard to know exactly at what point the processor starts calling the ISR after the last bit arrives, and the length of the instruction it is currently executing could be an influencing factor.
I got your sketches to work running at 250 kHz with these amendments to the slave:
#include "pins_arduino.h"
#include <avr/power.h>
#include <avr/sleep.h>
void setup(void) {
pinMode(MISO, OUTPUT); // MISO needs to be an output
SPCR |= _BV(SPE); // enable SPI in slave mode
SPCR |= _BV(SPIE); // enable interrupts
SPDR = 66;
power_timer0_disable();
set_sleep_mode (SLEEP_MODE_IDLE);
}
ISR (SPI_STC_vect) { SPDR = 66; } // just send 66 until the cows come home
void loop (void)
{
sleep_mode ();
}
The sleeping in idle mode makes sure that the processor is not half-way through an instruction when the interrupt is raised.
As expected (for the reasons above) it did not work at 500 kHz.
If you want to transfer data reliably you need to allow, in the master, for the time it takes the slave to do stuff. Every line of code in the slave means that the master has to wait for that to be executed.
I mention that in my page about SPI where I build in a delay in the master, if I want the slave to do calculations or something.