Sending struct via SPI between Arduino Nano and Arduino Mega 2560

In this experiment, Arduino Nano is SPI Master, sending structured data (struct customMessage) via SPI link to Arduino Mega 2560, which is SPI Slave.

The struct is wrapped into union allowing byte level access, used both to read and to write data used in SPI transfer.

typedef struct customMessage {
  unsigned int parameterA;
  unsigned int parameterB;
  byte parameterC;
  byte parameterD;
} b_s;

typedef union customMessageUnion {
  b_s message;
  unsigned char bytes[sizeof(b_s)];
} b_u;

At Master, the most important points are:

  • to set SS pin to HIGH before callingh SPI.begin()
  • to set suitable clock divider after SPI.begin(). Smaller divider: greater speed, smaller reliability.
void setup() {
...
  digitalWrite(pinSS, HIGH);
  SPI.begin();  
  SPI.setClockDivider(SPI_CLOCK_DIV8);
...
}

and to property transfer each byte, using for loop and SS signalling

void send(customMessage table)
{
  customMessageUnion msgUnion;
  msgUnion.message = table;

  digitalWrite(pinSS, LOW);

  for (size_t i = 0; i < sizeof(b_s); i++)
  {
    SPI.transfer(msgUnion.bytes[i]);
  }
  delay(10);
  digitalWrite(pinSS, HIGH);

}

Initially, following an online example, delay before pinSS is set back to HIGH, was 60ms. I-ve reduced it to 10ms and noticed no changes in behavior/reliability.

The complete source for Master (Arduino Nano):

void log(String msg) {
  Serial.println(msg);
}

int pinSS = 10;  

void log(customMessage msg) {

  Serial.print("A ");
  Serial.println(msg.parameterA);

  Serial.print("B ");
  Serial.println(msg.parameterB);

  Serial.print("C ");
  Serial.println(msg.parameterC);

  Serial.print("D ");
  Serial.println(msg.parameterD);
  
}

unsigned int i = 5;

void setup() {

  Serial.begin(38400);
  digitalWrite(pinSS, HIGH);

  SPI.begin();
  
  SPI.setClockDivider(SPI_CLOCK_DIV8);

}

void send(customMessage table)
{
  customMessageUnion msgUnion;
  msgUnion.message = table;

  digitalWrite(pinSS, LOW);

  for (size_t i = 0; i < sizeof(b_s); i++)
  {
    SPI.transfer(msgUnion.bytes[i]);
  }
  delay(10);
  digitalWrite(pinSS, HIGH);

}

customMessage msg;

void loop() {

  msg.parameterA = i * 100;
  msg.parameterB = i * 200;
  msg.parameterC = i * 5;
  msg.parameterD = i * 1;

  i++;

  if (i > 10) i = 0;

  send(msg);

  log(msg);

  delay(1000);
}

At Slave, the most important points are:

  • to set MISO pin as OUTPUT
  • to set registers for SPI slave mode and attach the interrrupt
void setup() {
...
  pinMode(MISO, OUTPUT);
...
   // turn on SPI in slave mode
  SPCR |= _BV(SPE);
...
  SPI.attachInterrupt();
...
}

The interrupt stores byte from SPI input register into buffer byte array, until we collect all bytes for our struct:

ISR(SPI_STC_vect)
{
  byte c = SPDR;  

  buf[pos] = c;
  pos++;
  if (pos >= sizeof(b_s)) {
    process_it = true;
  }
}

Which is updated from loop() function, when ready:

loop() {
...
 if (process_it)
  {
    buf[pos] = 0;
    customMessageUnion msgUnion;

    for (size_t i = 0; i < sizeof(b_s); i++)
    {
      msgUnion.bytes[i] = buf[i];
    }

    message = msgUnion.message;
    newMessage = true;
    pos = 0;
    process_it = false;
  }  
...
}

 

Source for the Slave Arduino (Mega 2560)

ACEMegaHostTFTClass Display;

int pinSS = 53;

customMessage message;

char buf[100];
volatile byte pos;
volatile boolean process_it;

bool newMessage = false;

// the setup function runs once when you press reset or power the board
void setup() {

  Serial.begin(38400);
  
  pinMode(MISO, OUTPUT);

  message.parameterA = 0;
  message.parameterB = 0;
  message.parameterC = 0;
  message.parameterD = 0;
  
   // turn on SPI in slave mode
  SPCR |= _BV(SPE);

  // now turn on interrupts
  SPI.attachInterrupt();

  // get ready for an interrupt 
  pos = 0;   // buffer empty
  process_it = false;

  
  Display.setTextSize(2);
  Display.begin();
  Display.setRotation(1);
  Display.fillScreen(GLCD_CL_BLACK);

  delay(5000);
}
unsigned int di = 0;
void log(String msg) {
  if (di > 14) {
    Display.fillScreen(GLCD_CL_BLACK);
    Display.setCursor(0, 0);
    di = 0;
  }

  Serial.println(msg);
  Display.println(msg);
  di++;
}

void log(customMessage msg) {

  
  Display.fillScreen(GLCD_CL_BLACK);
  Display.setCursor(0, 0);
  di = 0;
  
  log("A " + String(msg.parameterA));
  log("B " + String(msg.parameterB));
  log("C " + String(msg.parameterC));
  log("D " + String(msg.parameterD));
}

ISR(SPI_STC_vect)
{
  byte c = SPDR;  

  buf[pos] = c;
  pos++;
  if (pos >= sizeof(b_s)) {
    process_it = true;
  }
}


void loop(void)
{
  if (newMessage) {

    log(message);
    log(pos);
    newMessage = false;
  }

  if (process_it)
  {
    buf[pos] = 0;
    customMessageUnion msgUnion;

    for (size_t i = 0; i < sizeof(b_s); i++)
    {
      msgUnion.bytes[i] = buf[i];
    }

    message = msgUnion.message;
    newMessage = true;
    pos = 0;
    process_it = false;
  }  
}

 

SPI struct transfer between Nano and Mega

Wiring:

On the Slave side, chip-select pin (SS) is “somewhat” predefined to pin 10 (Arduino Nano, Uno…) and pin 53 (Mega). There seems to be a way to use another digital pin for SS interrupt (see).

In this case, we are transferring data only in one direction (from Master to Slave), therefore we don’t need MISO connection between pin 53 (Mega) and 12 (Nano).

 

Critical findings:

  • SPI.transfer(*byte, n) — has issues with overwriting output buffer, thus corrupting communication, it is more advisable to use: for { } loop with SPI.transfer(byte);
  • Selecting smallest yet stable SPI clock divider played important in achieving stable communication. In this case (Arduino Nano as Master) SPI_CLOCK_DIV8 was found suitable, while SPI_CLOCK_DIV4 caused total mess.

 

Main sources consulted:

On converting struct to byte array and vice versa:

On SPI communication between two Arduinos:

Arduino SPI Communication Example

Spread the love