How to wire two Arduino MCUs as master/slave via SPI and transfer useful data (e.g. sensor readings >1 Byte) from the slave to the master.

I don’t usually write overly technical how-tos, as in the majority of cases these topics are covered all around the internet and it’s usually pretty easy to find the right answer within a short amount of time. However, when it comes to this topic, namely the implementation of SPI on an MCU like Arduino, things look slighlty different: There are plenty tutorials and videos showing how one could wire up two MCUs in order to let an LED attached to the second MCU lighten up by pressing a button that’s attached on the first one. But usually that’s the top of sophistication this topic seems to receive from especially the Arduino community. Try googling about how to transmit more than a byte over SPI, from one Arduino to another. You’ll find some of the most confusing and even partially wrong answers the internet has to offer. I myself have spent way too much time on finding the right answers on such topics - hence I decided to write down this brief documentation on how to connect two Arduino MCUs via SPI and have them share more than the single byte that’s required to get a LED lighting up. Let’s start by wiring the two MCUs. For this setup I’m using an Arduino Mega2560 as master and an Arduino Nano as slave. The wiring looks like this:

Wiring

Mega2560 Nano MOSI 51 11 MISO 50 12 SCK 52 13 SS (Slave) 53 10

The pins are documented in Arduino’s SPI library documentation. So if you’d want to use different Arduino models, simply check the pins table and re-wire according to it:

SPI Pins

Next, let’s set up the code on the slave. In order to provide a more useful output than just a handful of numbers, I’ve connected HC-SR04 ultrasonic sensors to the slave and have it measure the distance for each sensor.

Slave

#include <SPI.h> #include <HCSR04.h> UltraSonicDistanceSensor distanceSensorPrimary ( 2 , 3 ); UltraSonicDistanceSensor distanceSensorSecondary ( 4 , 5 ); UltraSonicDistanceSensor distanceSensorTertiary ( 6 , 7 ); UltraSonicDistanceSensor distanceSensorQuaternary ( 8 , 9 ); uint16_t distancePrimaryCM = 0 ; uint16_t distanceSecondaryCM = 0 ; uint16_t distanceTertiaryCM = 0 ; uint16_t distanceQuaternaryCM = 0 ; volatile byte myData [ 8 ] = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 }; volatile uint8_t pos = 0 ; volatile bool active ; void setup () { Serial . begin ( 115200 ); pinMode ( MISO , OUTPUT ); pinMode ( SS , INPUT_PULLUP ); SPCR |= _BV ( SPE ); bitClear ( SPCR , 8 ); SPCR |= _BV ( SPIE ); } void loop () { distancePrimaryCM = distanceSensorPrimary . measureDistanceCm (); myData [ 0 ] = lowByte ( distancePrimaryCM ); myData [ 1 ] = highByte ( distancePrimaryCM ); distanceSecondaryCM = distanceSensorSecondary . measureDistanceCm (); myData [ 2 ] = lowByte ( distanceSecondaryCM ); myData [ 3 ] = highByte ( distanceSecondaryCM ); distanceTertiaryCM = distanceSensorTertiary . measureDistanceCm (); myData [ 4 ] = lowByte ( distanceTertiaryCM ); myData [ 5 ] = highByte ( distanceTertiaryCM ); distanceQuaternaryCM = distanceSensorQuaternary . measureDistanceCm (); myData [ 6 ] = lowByte ( distanceQuaternaryCM ); myData [ 7 ] = highByte ( distanceQuaternaryCM ); } ISR ( SPI_STC_vect ) { byte c = SPDR ; if ( c == 1 ) { active = true ; pos = 0 ; SPDR = myData [ pos ]; pos ++ ; return ; } if ( active == false ) { SPDR = 0 ; return ; } SPDR = myData [ pos ]; pos ++ ; if ( pos >= 8 ) { active = false ; } }

The ISR (Interrupt Service Routines) function is a special one on Arduino, that’s being triggered every time data is being retreived over SPI. Hence you don’t need to call that function on your own or use attachInterrupt to make it active. Inside the loop we’re now free to spend out time do some real work instead of polling for new SPI data on every iteration.

The master needs to initiate the data transfer and select the slave it requests the data from. Let’s have a look at that code.

Master

#include <SPI.h> byte myData [] = { 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 }; void setup () { Serial . begin ( 115200 ); SPI . begin (); SPI . setClockDivider ( SPI_CLOCK_DIV8 ); digitalWrite ( SS , HIGH ); delay ( 100 ); } void loop () { digitalWrite ( SS , LOW ); SPI . beginTransaction ( SPISettings ( SPI_CLOCK_DIV8 , MSBFIRST , SPI_MODE0 )); SPI . transfer ( 1 ); for ( uint8_t pos = 0 ; pos < 8 ; pos ++ ) { delayMicroseconds ( 15 ); myData [ pos ] = SPI . transfer ( 0 ); } SPI . endTransaction (); digitalWrite ( SS , HIGH ); int number1 = ( int )( myData [ 1 ] << 8 ) | ( int ) myData [ 0 ]; int number2 = ( int )( myData [ 3 ] << 8 ) | ( int ) myData [ 2 ]; int number3 = ( int )( myData [ 5 ] << 8 ) | ( int ) myData [ 4 ]; int number4 = ( int )( myData [ 7 ] << 8 ) | ( int ) myData [ 6 ]; Serial . print ( "Number-1: " ); Serial . println ( number1 ); Serial . print ( "Number-2: " ); Serial . println ( number2 ); Serial . print ( "Number-3: " ); Serial . println ( number3 ); Serial . print ( "Number-4: " ); Serial . println ( number4 ); Serial . println ( "================================" ); delay ( 100 ); }

That’s basically it. The master is now able toreliably interface with the slave via SPI in order to retrieve all four values from the four individual ultrasonic sensors. Of course, this is really just a basic example and there are couple of things that are required in order to make this a solid piece of code. However, it’s a good basis to start upon which so far I couldn’t find anywhere online.

I hope this quick write-up will help some of you implementing SPI slaves that deliver useful output.