Bidirectional SPI Communication with Structs between 2 controllers

Hi all, i`m starting to make a personal domotic project using ESP8266 (Wemos D1 mini) and Blynk application and i just found that it is very difficult to manage ESP-NOW protocol and Blynk communication on the same device.
Some one has managed to do so disconnecting Blynk and ESP-now alternatively when needed, but i want something more fast, and reliable.
So the only option look like to use 2 ESP8266 modules one as gateway for ESP-Now and one for Blynk communication as in the picture below.


My choice to use SPI communication is that because :

  • Is the fastest communication
  • I need to connect only 2 devices
  • Full Duplex communication
  • Can be used for 8-bit, 12-bit, 16-bit data transmissions.

Now my question is: I'm completely new to this kind of communication and I didn't found any good example of bidirectional SPI communication able to send and receive structs like this:

struct StructRelayModule {
    int mac_peer01[6];     
    int mac_peer02[6];     
    int amPBas01;               
    int RTM01;
    int LightThreshold;
    int amPBasJL;
    int JLStartH;
    int JLEndH;
    bool PirInd01; 
    bool RelayInd01;
    int CountDown01;  
    float LightLevel;     // this can be also an integer
    bool JapLampInd;       
};
StructRelayModule TXrelay;
StructRelayModule RXrelay;

Can someone point me to a good example to follow?

Hello,

The safest way is to use memcpy to copy the struct into an array, send this array with SPI, and receive the array then use memcpy again to convert the array into the struct

Here is an example : UsJSXv - Online C++ Compiler & Debugging Tool - Ideone.com

Here is an example of combining a struct with a byte array using union. This does not require any copy operation. It overlays the struct with a byte array.

typedef struct __attribute__( ( packed ) )
{
  uint8_t a;
  int8_t b;
  uint16_t c;
} data_t;


union data_u
{
  struct __attribute__( ( packed ) )
  {
    data_t values;
  };
  uint8_t bytes[ sizeof( data_t ) ];
};

data_u data;

You can access the variable with: data.values.a and the bytes with data.bytes[i].

There are a few points to note:

  • using packed ensures the compiler does not insert padding bytes
  • this may work different on different compilers
  • depending on the architecture this may cause a small performance penalty because of unaligned memory accesses
  • aligning the variables inside the struct may reduce the penalties
  • using the size specific types (int16_t instead of int) should ensure the structures are the same on two devices with different architectures
  • avoid float data types for interfaces because there are multiple standards
  • check Endianess (byte order) for data types larger than 8-bit inside the struct when using different architectures

Thank you very much to both, actually I'm not an advanced programmer, i have already some difficulties with a simple Struct, using the union is hurting my brain especially if i have to make multiple communication with many boards. :sweat:
So i made a combination of your solution and i ended up to this:

const int MACsize = 6;
uint8_t mac_dev02[MACsize] = {0x8C, 0xAA, 0xB5, 0x77, 0xD2, 0x4B};   // Wemos D1 mini*2 RELAY
uint8_t mac_dev06[MACsize] = {0x8C, 0xCE, 0x4E, 0xE3, 0x3F, 0x2D};   // Wemos D1 mini*6 Long Range Gateway 

struct StructRelayModule {
  uint8_t mac_peer00[MACsize];
  uint8_t mac_peer01[MACsize];     
};
StructRelayModule TXrelay;
StructRelayModule RXrelay;

void setup() {
  Serial.begin(115200);    // Initialize serial communications
    for (int i=0; i<MACsize; i++) TXrelay.mac_peer00[i] = mac_dev06[i];   // Wemos D1 mini*6 Long Range Gateway 
    for (int i=0; i<MACsize; i++) TXrelay.mac_peer01[i] = mac_dev02[i];   // Relay
}
    
void loop(){
  
  size_t size = sizeof(StructRelayModule);
  uint8_t array[size];
     
  memcpy(array, &TXrelay, size);
  
  Serial.print("Array: ["+String(size)+"] {");
    for (int i = 0; i < size; i++) {
      if (array[i] < 10 ) Serial.print(" ");
      if (array[i] < 100) Serial.print(" ");
      Serial.print(String(array[i],HEX));
      if ((i+1)<size) Serial.print("|");
  }
  Serial.print("}\n");
  
  delay(3*1000);
}

And look like it is working good, at least for now.

Now about the transmission part:
I have found some tutorial like https://microcontrollerslab.com/spi-communication-between-two-arduino-boards/ but no one even compile, they all stops at the line:

ISR (SPI_STC_vect)                        //Inerrrput routine function 

The cause can be because i`m using a ESP8266 instead of an Uno?

Many of the points I mentioned still apply to your code. Try extending your struct with a 8-bit type and a 32-bit type and then look at the result.

Array: [20] {8c|ce| 4e|e3| 3f| 2d|8c|aa|b5|77|d2| 4b| 55|  0|  0|  0|aa| 34| 12|aa}

The 0 values are padding bytes.

Try again tomorrow, your brain will have adapted to some part of the pain. :slight_smile: Here is your example using a union.

const int MACsize = 6;

uint8_t mac_dev02[MACsize] = {0x8C, 0xAA, 0xB5, 0x77, 0xD2, 0x4B};   // Wemos D1 mini*2 RELAY
uint8_t mac_dev06[MACsize] = {0x8C, 0xCE, 0x4E, 0xE3, 0x3F, 0x2D};   // Wemos D1 mini*6 Long Range Gateway

typedef struct __attribute__( ( packed ) )
{
  uint8_t mac_peer00[MACsize];
  uint8_t mac_peer01[MACsize];
  uint8_t x;
  uint32_t y;
} structRelayModule_t;

union structRelayModule_u
{
  struct __attribute__( ( packed ) )
  {
    structRelayModule_t values;
  };
  uint8_t bytes[ sizeof( structRelayModule_t ) ];
};

structRelayModule_u TXrelay;
structRelayModule_u RXrelay;

void setup()
{
  Serial.begin( 115200 );  // Initialize serial communications
  for ( uint32_t i = 0; i < MACsize; i++ ) TXrelay.values.mac_peer00[i] = mac_dev06[i]; // Wemos D1 mini*6 Long Range Gateway
  for ( uint32_t i = 0; i < MACsize; i++ ) TXrelay.values.mac_peer01[i] = mac_dev02[i]; // Relay
  TXrelay.values.x = 0x55;
  TXrelay.values.y = 0xAA1234AA;
}

void loop()
{
  size_t size = sizeof TXrelay.bytes;
  
  Serial.print( "Array: [" + String( size ) + "] {" );
  for ( uint32_t i = 0; i < size; i++ )
  {
    if ( TXrelay.bytes[i] < 10 ) Serial.print( " " );
    if ( TXrelay.bytes[i] < 100 ) Serial.print( " " );
    Serial.print( String( TXrelay.bytes[i], HEX ) );
    if ( ( i + 1 ) < size ) Serial.print( "|" );
  }
  Serial.print( "}\n" );

  delay( 3 * 1000 );
}
1 Like

Ok, I have done some homework (I think I understand the principle, but I'm still far from having 100% control over these unions), and i still have some question.

Since it will be a long work to make a correct size of the Struct each time for each sensor/module, I tried to figure the maximum size of the structure that i will need to send/receive in the actual project and in future, so i can just copy paste and go on.

So i came up with this:

const int MACsize = 6;

uint8_t mac_dev02[MACsize] = {0x8C, 0xAA, 0xB5, 0x77, 0xD2, 0x4B};   // Wemos D1 mini*2 RELAY
uint8_t mac_dev06[MACsize] = {0x8C, 0xCE, 0x4E, 0xE3, 0x3F, 0x2D};   // Wemos D1 mini*6 Long Range Gateway

typedef struct __attribute__( ( packed ) ){
  uint8_t mac_peer00[MACsize];
  uint8_t mac_peer01[MACsize];
  uint8_t mac_peer02[MACsize];
  uint8_t mac_peer03[MACsize];
  uint8_t Data[8];
  uint16_t Interlock[3];
  uint16_t Sensor[3];
  uint32_t timer24h;
} structRelayModule_t;

union structRelayModule_u{
  struct __attribute__( ( packed ) ){
    structRelayModule_t values;
  };
  uint8_t bytes[ sizeof( structRelayModule_t ) ];
};
structRelayModule_u TXrelay;
structRelayModule_u RXrelay;

void setup(){
  Serial.begin( 115200 );  // Initialize serial communications
  for ( uint32_t i = 0; i < MACsize; i++ ) TXrelay.values.mac_peer00[i] = mac_dev06[i]; // Wemos D1 mini*6 Long Range Gateway
  for ( uint32_t i = 0; i < MACsize; i++ ) TXrelay.values.mac_peer01[i] = mac_dev02[i]; // Relay
  for ( uint32_t i = 0; i < MACsize; i++ ) TXrelay.values.mac_peer02[i] = mac_dev02[i]; // Test
  for ( uint32_t i = 0; i < MACsize; i++ ) TXrelay.values.mac_peer03[i] = mac_dev02[i]; // Test
  TXrelay.values.Data[0] = 55;
  TXrelay.values.Data[7] = 255;
  TXrelay.values.Interlock[0] = 3000; // 30.00*C
  TXrelay.values.Sensor[0]    = 2550; // 25.50*C
  TXrelay.values.timer24h     = 1*60*60*24; // 24h = 86400s
}

void loop(){
  size_t size = sizeof TXrelay.bytes;
  //Serial.println ("Total Size = " + String(size));
    
  //Print MAC Address
/*
  int sizeOfMACs = (1*6)*4;
  Serial.print( "MAC: [" + String( sizeOfMACs ) + "] {" );
  for ( uint32_t i = 0; i < 24; i++ )  {
    if (i<24) Serial.print( String( TXrelay.bytes[i], HEX ) );
    if ( ( i + 1 ) < size ) Serial.print( "|" );
  }
  Serial.print( "}\n" );
*/
  //Print Values
  int sizeOfData = (1*8)+(2*3)+(2*3)+4;
  Serial.print( "Data: [" + String( sizeOfData ) + "] {" );
  for ( uint32_t i = 24; i < 24 + (sizeOfData); i++ )  {
    if ( TXrelay.bytes[i] < 10 ) Serial.print( " " );
    if ( TXrelay.bytes[i] < 100 ) Serial.print( " " );
    Serial.print( String( TXrelay.bytes[i], DEC ) );
    if ( ( i + 1 ) < size ) Serial.print( "|" );
  }
  Serial.print( "}\n" );

  delay( 3 * 1000 );
}

For mac address everything is ok, but when i go to print the data part, what i see is different from what i have assigned in setup.

3000 --> 184| 11
2550 --> 246| 9
86400 --> 128|81 |1

It look like that if i exceed 255 i print strange values.
I understand that even if it is declared uint16_t (or 32) it sore the data in packets of 8 starting from less significant ( in HEX ABCD -> CD AB).
How i can restore the values to the correct reading for decimals?

PS. sorry if can`t reply fast, but in those days i have just few moments for this Hobby.

It looks correct to me, 3000 is 0xBB8, seen as two bytes it's 0xB8 (184) and 0xB (11)

But, you should avoid using an union in this case, it's considered bad practice and may not work on another platform

You need to use the values for printing. The byte array is just for sending the entire structure to another device.

@guix I have mixed feelings about the type punning with unions. I like the code and the simplicity but I understand your concern about coding practice.

Could one use memcpy to copy the struct into the byte array inside the union? This would make the intention known to the compiler and if the compiler is smart enough would not create additional code on platforms that do not need it.

In SPI Protocol, data transmission/reception ie being handled byte-by-byte (8-bit, Fig-1).


Figure-1:

First learn how to exchaneg 1-byte data between Master/Slave; then learn the exchnage of 2-byte; then learn the exchange of 4-byte, and then learn the exchange of complex data like struture.

This following article which contains examples (near the end) could be helpful for your.
Ch7-SPIOnlineLecLatest.pdf (425.6 KB)

@Klaus_K ok, i didn`t see that easy solution XD, thank you.

@GolamMostafa Thank you very much for the PDF it is a very good course.

The problem that i'm facing is with the slave device that does not compile the Sketch.
I don't know why, can be the fact that i'm using a Wemos D1 mini instead of an Uno/Nano? or I'm missing some library?
The sketch with which i have problem:

//Slave Sketch
#include<SPI.h>

volatile bool x;//always place volatile before variables that are used in ISR
volatile bool flag = false;

void setup(){
  Serial.begin(9600);
  setBit(SPCR, MSTR); //NANO Slave
  setBit(SPCR, SPE); //SPI Port is active
  //bitSet(SPCR, SPIE) //Interrupt enable low level SWL
  //Sei(); //SWG by cefault it is active
  pinMode(SCK, INPUT);
  pinMode(MISO, OUTPUT);
  pinMode(MOSI, INPUT);
  pinMode(SS, INPUT_PULLUP);
  SPI.attachInterrupt(); //enable interrupt logic high level SWL = SPIE
}

void loop(){
  if(flag == true){
    Serial.println(x, HEX); //shows:
    flag = false;
  }
}

ISR(SPI_STC_vect){
  flag = true;
  z = SPDR; //z = 12
}

The error i get:

SpiCommF:27:5: error: expected constructor, destructor, or type conversion before '(' token
   27 | ISR(SPI_STC_vect){
      |     ^
C:\Users\Sandro Demani\Google Drive\Documenti\Arduino\ESP8266\SpiCommF\SpiCommF.ino: In function 'void setup()':
SpiCommF:9:10: error: 'SPCR' was not declared in this scope
    9 |   setBit(SPCR, MSTR); //NANO Slave
      |          ^~~~
SpiCommF:9:16: error: 'MSTR' was not declared in this scope; did you mean 'PSTR'?
    9 |   setBit(SPCR, MSTR); //NANO Slave
      |                ^~~~
      |                PSTR
SpiCommF:9:3: error: 'setBit' was not declared in this scope
    9 |   setBit(SPCR, MSTR); //NANO Slave
      |   ^~~~~~
SpiCommF:10:16: error: 'SPE' was not declared in this scope; did you mean 'SPI'?
   10 |   setBit(SPCR, SPE); //SPI Port is active
      |                ^~~
      |                SPI
SpiCommF:17:7: error: 'class SPIClass' has no member named 'attachInterrupt'
   17 |   SPI.attachInterrupt(); //enable interrupt logic high level SWL = SPIE
      |       ^~~~~~~~~~~~~~~
C:\Users\Sandro Demani\Google Drive\Documenti\Arduino\ESP8266\SpiCommF\SpiCommF.ino: At global scope:
SpiCommF:27:4: error: expected constructor, destructor, or type conversion before '(' token
   27 | ISR(SPI_STC_vect){
      |    ^
exit status 1
expected constructor, destructor, or type conversion before '(' token

Also the master i should can choose to use SPDR = 0x12; or byte y = SPI.transfer(0x12);.
But with the first one i receive SPDR was not decalred in this scope.
What do you think?

You are compiling AVR sketch using different processor. It is also not compiled in my ESP8266 NodeMCU.

Yes, this looks like some low level code accessing peripheral registers. You need to figure out how this is done on your device architecture.

Ok, thank you, but this was the main point, it is possible to use SPI communication with the ESP8266? Anyone has some experience or an example for that?
If not, which communication do you suggest me to use between 2 ESP8266?
(without using the ESP-Now since look like Blynk is not compatible with this communication).