Arduino to Arduino SPI with Custom Clock Rate
Introduction
In the previous post I built a demo of SPI between two Arduino UNOs. I used the Arduino’s old SPI library and the SPI internals were abstracted away. I was still curious about how SPI can be setup between two heterogeneous systems.
I decided to continue with the current arduino setup and started to look into various parameters that affect SPI.
Refer my previous post for breadboard layout and schematic.
SPI Configuration Parameters
Systems that are talking through SPI should align on three configuration parameters:
- Clock Rate
- Bit Order i.e. LSB First or MSB First
- Data Mode
In the new Arduino SPI API, all these parameters could be configured through SPISettings object.
In my previous experiement, I relied on the default values for Data Order, and
Data Mode, and I used SPI_CLOCK_DIV8
on master Arduino, and left default clock
configuration on slave. I wanted to verify these default values to understand
the configuration of my working setup.
I looked into the SPI.h code from ArduinoCore-avr repository.
This is the same file that Arduino IDE loads while compiling my sketch. I also
found out that Arduino UNO’s microcontroller (ATmega328P) stores SPI
configuration in SPCR
(SPI Control Register) and SPSR
(SPI Status Register),
and I can print the values of these registers and verify the configuration
through Serial monitor. One can refer this article
to understand the definition of each bit in SPCR
and SPSR
if you are too lazy
to search the datasheet.
So, based on SPI.h and register values, I found out that
- The default clock rate is 4 MHz,
- Bit Order is set to
MSBFIRST
, and - Data Mode is set to
SPI_MODE0
, which CPOL=0, and CPHA=0.
Read more about Data Mode in this wikipedia page.
Also, in the previous setup, when I setup clock divider to SPI_CLOCK_DIV8
, it got
the Clock Rate Down to 1 MHz (i.e 16 Mhz / 8, as 16 MHz is default clock
rate for Arduino UNO).
Custom Clock Rate
I tried out various clock rates from 125 KHz (fosc / 128) to 8 MHz (fosc / 2) and I found out that the setup doesn’t work when the both the systems are configured to use the same SPI clock rate i.e. same values in SPI2X, SPR1, SPR0 registers.
Later, I looked into ATmega328P datasheet (page 167) and found out that the recommended slave clock rate needs to be longer than 4x of master clock rate.
To ensure correct sampling of the clock signal, the minimum low and high periods should be:
Low periods: Longer than 2 CPU clock cycles.
High periods: Longer than 2 CPU clock cycles.
With this bit of information, I decided to have slave SPI rate to be 4x faster than master, and at low and high clock speeds, this configuration worked fine.
Note that 4x is not ideal because the data sheet mentions that it should be longer than 4x, so I would recommend 8x difference for production systems.
Working Low Speed Setup: commit
- SPI Master Uno: 125 KHz
- SPI Slave Uno: 500 KHz
Working High Speed Setup: commit
- SPI Master Uno: 2 MHz
- SPI Slave Uno: 8 MHz
Programming
I had to make a few changes in the message format and optimise the ISR callback on slave because I was noticing that there are some ASCII 7 (BELL) characters being sent.
Code on Master Arduino (with Push Buttons)
#include <SPI.h>
#include "pins_arduino.h"
struct Button {
int pin = 0;
int currentState = HIGH;
unsigned long stateSince = 0;
int debounce = 100;
int toggleValue = 0;
char label = '-';
};
struct Button buttonA;
struct Button buttonB;
SPISettings spiSettings(2000000, MSBFIRST, SPI_MODE0);
void setup (void) {
// Serial for Debugging
Serial.begin (9600);
// Put SCK, MOSI, SS pins into output mode
// also put SCK, MOSI into LOW state, and SS into HIGH state.
// Then put SPI hardware into Master mode and turn SPI on
SPI.begin ();
// Setup button A
buttonA.pin = 6;
buttonA.label = 'A';
pinMode(buttonA.pin, INPUT_PULLUP);
// Setup button B
buttonB.pin = 7;
buttonB.label = 'B';
pinMode(buttonB.pin, INPUT_PULLUP);
}
void loop (void) {
bool buttonA_changed = buttonToggle(&buttonA);
bool buttonB_changed = buttonToggle(&buttonB);
if (buttonA_changed) {
xferData(buttonA);
}
if (buttonB_changed) {
xferData(buttonB);
}
}
void xferData(struct Button button) {
char c;
SPI.beginTransaction(spiSettings);
// Enable Slave Select
// SS is pin 10
digitalWrite(SS, LOW);
// Create payload
char data[4] = {button.label, ('0' + button.toggleValue), 'Z', '\n'};
Serial.println(data);
// Send data
for (const char * p = data; c = *p; p++) {
SPI.transfer(c);
}
// Disable Slave Select
digitalWrite(SS, HIGH);
SPI.endTransaction();
}
bool buttonToggle(struct Button* button) {
bool toggleState = false;
int reading = digitalRead(button->pin);
if (reading == button->currentState) {
} else {
unsigned long int currentTime = millis();
if (((currentTime - button->stateSince) > button->debounce)
&& button->currentState == LOW) {
toggleState = true;
if (button->toggleValue == 0) {
button->toggleValue = 1;
} else {
button->toggleValue = 0;
}
}
button->currentState = reading;
button->stateSince = currentTime;
}
return toggleState;
}
Code on Slave Arduino (with LEDs)
#include "pins_arduino.h"
char buf [100];
volatile byte pos;
volatile boolean process_it;
int pinA = 6;
int pinB = 7;
void setup (void) {
// Serial for debugging
Serial.begin (9600);
// Act as slave, MISO should be OUTPUT
pinMode(MISO, OUTPUT);
// Turn on SPI in slave mode
SPCR |= _BV(SPE);
// Turn on interrupts
SPCR |= _BV(SPIE);
// CLK = 8 MHz
//SPCR |= _BV(SPR1);
SPSR |= _BV(SPI2X);
pos = 0;
process_it = false;
// LED Output
pinMode(pinA, OUTPUT);
pinMode(pinB, OUTPUT);
}
// SPI ISR
ISR (SPI_STC_vect) {
byte c = SPDR;
if (c > 47 && c < 91) {
// add to buffer if room
if (pos < sizeof buf) {
buf[pos++] = c;
// Z (ASCII 90) means process the buffer
if (c == 90) {
process_it = true;
}
}
} else {
pos = 0;
}
}
void loop (void) {
// wait for flag set in ISR
if (process_it) {
changeLed(buf[0], buf[1]);
process_it = false;
}
}
// update LED output
void changeLed(char pinSel, char valSel) {
int pin, value;
if (pinSel == 'A') {
pin = pinA;
} else if (pinSel == 'B') {
pin = pinB;
} else {
return;
}
if (valSel == '1') {
value = HIGH;
} else if (valSel == '0') {
value = LOW;
} else {
return;
}
digitalWrite(pin, value);
}
Next Steps
Now I am quite confident about dealing with different SPI configuration parameters. I want to bring in a different arduino board in the mix. I would like setup SPI between an Arduino UNO (16 MHz) and Arduino Due (84 Mhz).
Stay tuned and feel free to tweet me if you like/hate my content.