Floats and structs sent from Arduino to Arduino over SPI

Hello, I play around SPI and trying to send floats from Mega (slave) to Nano (master). I use union, it looks like elegant and readable way for me. Everything works, however, I noticed one issue. When slave increases the number that is sent, there is wrong received number showed on the side of master, and only in case of 8, 16, 128. Not for 32, 64, nor 256. I didn't check higher values. I cannot figure out what could cause this problem. I went through many discussions and didn't find this. Any experience, idea? Thank you.
Master code:

#include <SPI.h> 
union myUserData {
  float x;
  byte myBytes[4];
};
myUserData myUD;

byte myData[] = {0x00, 0x00, 0x00, 0x00};

void setup(){
  Serial.begin(19200);
  SPI.begin();
	delay(100);
	SPI.setClockDivider(SPI_CLOCK_DIV16); 
	digitalWrite(SS,LOW); 
}

void loop(){
	for(int i = 0; i < 4; i++){
		myData[i] = SPI.transfer(0);
		delayMicroseconds(100); // allow Slave to process the received data byte
		Serial.print(myData[i], HEX); // S0>M3, S1>M0, S2>M1, S3>M2
		Serial.print(" ");
	}
	myUD.myBytes[3] = myData[0];
	myUD.myBytes[0] = myData[1];
	myUD.myBytes[1] = myData[2];
	myUD.myBytes[2] = myData[3];
	
	Serial.print(" float: ");
	Serial.print(myUD.x, 2);
	Serial.println();

 	delay(3000); //test interval
}

Slave code:

#include <SPI.h>
union myUserData {
  float x;
  byte myBytes[4];
};
myUserData myUD;

volatile bool flag = false;
volatile int i = 0;
 
void setup(){
  Serial.begin(19200);
	pinMode(SS, INPUT_PULLUP);// ensure SS stays high for now
	pinMode(MISO,OUTPUT);
	SPCR |= _BV(SPE);
	SPCR |= !(_BV(MSTR)); // defining Slave
	//bitSet(SPCR, SPE); //enable SPI Port, nechat?
	SPI.attachInterrupt();
	myUD.x = 1.11;
}

void loop(){
	if (flag == true){
		flag = false;
		myUD.x++;
		Serial.print (myUD.myBytes[0], HEX);
		Serial.print (" ");
		Serial.print (myUD.myBytes[1], HEX);
		Serial.print (" ");
		Serial.print (myUD.myBytes[2], HEX);
		Serial.print (" ");
		Serial.print (myUD.myBytes[3], HEX);
		Serial.print (" float: ");
		Serial.println (myUD.x, 2);
	}
}

ISR (SPI_STC_vect){
  SPDR = myUD.myBytes[i];
	i++;
	if (i == 4) {
		i = 0; // 4-byte data are sent
		flag = true;
	}
}

Mega Slave sent data:
19:24:22.888 -> 1F 85 C3 40 float: 6.11
19:24:27.879 -> 1F 85 E3 40 float: 7.11
19:24:32.901 -> 90 C2 1 41 float: 8.11
19:24:37.924 -> 90 C2 11 41 float: 9.11
19:24:42.916 -> 90 C2 21 41 float: 10.11
Nano Master received data:
19:24:27.880 -> 40 1F 85 C3 float: 6.11
19:24:32.903 -> 40 1F 85 E3 float: 7.11
19:24:37.925 -> 40 90 C2 1 float: 2.03
19:24:42.918 -> 41 90 C2 11 float: 9.11
19:24:47.939 -> 41 90 C2 21 float: 10.11

Mega Slave sent data:
19:26:23.054 -> 48 E1 F0 41 float: 30.11
19:26:28.063 -> 48 E1 F8 41 float: 31.11
19:26:33.052 -> A4 70 0 42 float: 32.11
19:26:38.075 -> A4 70 4 42 float: 33.11
19:26:43.098 -> A4 70 8 42 float: 34.11
Nano Master received data:
19:26:28.063 -> 41 48 E1 F0 float: 30.11
19:26:33.052 -> 41 48 E1 F8 float: 31.11
19:26:38.075 -> 41 A4 70 0 float: 8.03
19:26:43.098 -> 42 A4 70 4 float: 33.11
19:26:48.073 -> 42 A4 70 8 float: 34.11

Mega Slave sent data:
19:34:23.723 -> 52 38 FC 42 float: 126.11
19:34:28.713 -> 52 38 FE 42 float: 127.11
19:34:33.743 -> 29 1C 0 43 float: 128.11
19:34:38.760 -> 29 1C 1 43 float: 129.11
19:34:43.743 -> 29 1C 2 43 float: 130.11
Nano Master received data:
19:34:28.712 -> 42 52 38 FC float: 126.11
19:34:33.742 -> 42 52 38 FE float: 127.11
19:34:38.759 -> 42 29 1C 0 float: 32.03
19:34:43.743 -> 43 29 1C 1 float: 129.11
19:34:48.765 -> 43 29 1C 2 float: 130.11

i completely agree..
now, don't do it and never think of it again..
type pruning using a union is bad..
i've been slapped already..

good luck.. ~q

Stripping out everything except the numbers and shifting the master's over a bit, do you see a pattern?

   1F 85 C3 40 1F 85 E3 40 90 C2 1 41 90 C2 11 41 90 C2 21 41
40 1F 85 C3 40 1F 85 E3 40 90 C2 1 41 90 C2 11 41 90 C2 21

The slave's response is always a byte behind the master's send. Think about that for a bit.

Also, when the slave receive's the master's 0 and puts its response in the SPDR, when you you think the master will see it?

The union is a red herring. The return from SPI.transfer(0) is the value the slave put into the SPDR for the previous SPI.transfter(0), not the current one.

1 Like

well it's an UB issue but indeed not the one creating the faulty behavior seen in the code

Thanks for the answer. So how did you finally solve it? By bit shifting? Is it a robust solution?

You welcome, sorry to crush it for you..
I tend to just use memcpy into a buff..
Modifying shared multi-byte vars outside of the isr, you should typically turn off interrupts during the operation, volatile alone doesn't cut it with them..
And then, think if you be changing x in the loop while it's still in the process of sending x, you could potentially send partial combination of the old and new..
So synchronization is needed between loop and isr, maybe just as simple as another flag and let the isr load the buffer when it's finished sending what it had started..

with the above in mind and sorry never worked with spi before, i came up with something like this for the slave code..

#include <SPI.h>

volatile bool flag = false;
volatile int i = 0;
volatile float x = 1.11;
volatile bool flagLoad = false;
volatile byte spiBuff[4];
 
void setup(){
  Serial.begin(19200);
	pinMode(SS, INPUT_PULLUP);// ensure SS stays high for now
	pinMode(MISO,OUTPUT);

    //pre-load the spi buffer..
    //the only time we touch this
    //outside of the isr..
    memcpy(spiBuff,(uint8_t*)&x,4);

	SPCR |= _BV(SPE);
	SPCR |= !(_BV(MSTR)); // defining Slave
	//bitSet(SPCR, SPE); //enable SPI Port, nechat?
	SPI.attachInterrupt();
    SPDR = myUD.myBytes[i++]; // <=== ADD THIS
}

void loop(){
	if (flag == true){
		flag = false;
		Serial.print ("sent float: ");
		Serial.println (x, 2);
    if (!flagLoad){
      x = x+1;
      flagLoad = true;
    }
 }
}

ISR (SPI_STC_vect){
  SPDR = spiBuff[i];
	i++;
	if (i == 4) {
		i = 0; // 4-byte data are sent
		flag = true;
   if (flagLoad){
      //load x into spi buffer..
      memcpy(spiBuff,(uint8_t*)&x,4);
      //done loading..
      flagLoad = false;      
   } 
 }
}

hoping that using flagLoad is enough else may still have to turn off interrupts while modifying x..

good luck.. ~q

Just change it to struct?

One line change to the slave code. Look at the end of setup().

#include <SPI.h>
union myUserData {
  float x;
  byte myBytes[4];
};
myUserData myUD;

volatile bool flag = false;
volatile int i = 0;
 
void setup(){
  Serial.begin(19200);
	pinMode(SS, INPUT_PULLUP);// ensure SS stays high for now
	pinMode(MISO,OUTPUT);
	SPCR |= _BV(SPE);
	SPCR |= !(_BV(MSTR)); // defining Slave
	//bitSet(SPCR, SPE); //enable SPI Port, nechat?
	SPI.attachInterrupt();
	myUD.x = 1.11;
	SPDR = myUD.myBytes[i++]; // <=== ADD THIS
}

void loop(){
	if (flag == true){
		flag = false;
		myUD.x++;
		Serial.print (myUD.myBytes[0], HEX);
		Serial.print (" ");
		Serial.print (myUD.myBytes[1], HEX);
		Serial.print (" ");
		Serial.print (myUD.myBytes[2], HEX);
		Serial.print (" ");
		Serial.print (myUD.myBytes[3], HEX);
		Serial.print (" float: ");
		Serial.println (myUD.x, 2);
	}
}

ISR (SPI_STC_vect){
  SPDR = myUD.myBytes[i];
	i++;
	if (i == 4) {
		i = 0; // 4-byte data are sent
		flag = true;
	}
}```
1 Like

@van_der_decken It gives nonsense output:
7B 14 8E 3F float: ovf
3E A 7 40 float: 0.19
3E A 47 40 float: 0.19
1F 85 83 40 float: 0.00
1F 85 A3 40 float: 0.00
1F 85 C3 40 float: 0.00
1F 85 E3 40 float: 0.00
90 C2 1 41 float: -0.00
90 C2 11 41 float: -0.00
90 C2 21 41 float: -0.00

When I made a correction to

SPDR = myUD.myBytes[4];

and added to setup of master trashByte

byte trashByte = SPI.transfer(0x00);

than it works ok. But the wrong behavior appears again.

@jimLee Thank you for the comment, I am not sure I understand what is the idea beind that. I know struct, but I think it is good to use it when dealing with different data types, whilst here is just one data type. Do you have a tested design used with SPI?

That turns out not to be the case if done correctly.

Output from the master with the one line change to the slave and the obvious errors in the master corrected:

Monitor port settings:
baudrate=115200
Connected to /dev/ttyUSB1! Press CTRL-C to exit.
7B 14 8E 3F  float: 1.11
3E 0A 07 40  float: 2.11
3E 0A 47 40  float: 3.11
1F 85 83 40  float: 4.11
1F 85 A3 40  float: 5.11

@qubits-us Hi, thanks, it works, however the strange behavior persists, at 2, 8, etc. I will now try to get rid of the union on the master side and see.

@van_der_decken Hi, I tried it again and found out that bytes are transferred correctly, just float is wrong. It is another solution that leads me to get rid of the union and use another option. Thanks for your contribution.

@J-M-L I like structs, thanks, it's worth exploring.

@qubits-us I used memcpy, got rid of unions and added noInterrupts(), interrupts(). Unfortunately same output with the strange behaviour - I get correct HEX, and correct floats besides float 2, 8, 32, 128, etc. Any idea? Thank you.
Current code is following:
Master NANO

#include <SPI.h> 

uint8_t myData[] = {0x00, 0x00, 0x00, 0x00};
uint8_t myData2[4];
float myFloat;

void setup(){
  Serial.begin(19200);
  SPI.begin();
	delay(100);
	SPI.setClockDivider(SPI_CLOCK_DIV16); 
	digitalWrite(SS,LOW); 
	//byte trashByte = SPI.transfer(0x00);
}

void loop(){
	for(int i = 0; i < 4; i++){
		myData[i] = SPI.transfer(myData[i]);
		delayMicroseconds(100); // allow Slave to process the received data byte
		Serial.print(myData[i], HEX); 
		Serial.print(" ");
	}
	myData2[3] = myData[0];
	myData2[0] = myData[1];
	myData2[1] = myData[2];
	myData2[2] = myData[3];

	memcpy(&myFloat, myData2, 4);
	Serial.println(myFloat, 2);
 	delay(3000); //test interval
}

Slave MEGA

#include <SPI.h>

volatile bool flag = false;
volatile int i = 0;
volatile float x = 1;
volatile bool flagLoad = false;
volatile byte spiBuff[4];
 
void setup(){
  Serial.begin(19200);
	pinMode(SS, INPUT_PULLUP);// ensure SS stays high for now
	pinMode(MISO,OUTPUT);
	memcpy(spiBuff,(uint8_t*)&x,4);
	SPCR |= _BV(SPE);
	SPCR |= !(_BV(MSTR)); // defining Slave
	//bitSet(SPCR, SPE); //enable SPI Port
	SPI.attachInterrupt();
}

void loop(){
	if (flag == true){
		noInterrupts();
		flag = false;
		Serial.print (" float: ");
		Serial.println (x, 2);
    if (!flagLoad){
      x = x+1;
      flagLoad = true;
    }
		interrupts();
  }
}

ISR (SPI_STC_vect){
  SPDR = spiBuff[i];
	i++;
	if (i == 4) {
		i = 0; // 4-byte data are sent
		flag = true;
    if (flagLoad){
      memcpy(spiBuff,(uint8_t*)&x,4);  //load x into spi buffer..
      flagLoad = false; //done loading..
    } 
  }
}

can you elaborate on the above??
i don't understand why it's not 0-0,1-1,2-2,3-3..
why you moving the bytes around inside the float??

~q

Of course the float is wrong.

The master sketch receives the 4 bytes and puts them into a temporary buffer.

That's unnecessary. Just put them into the 4 byte buffer in the union.

Had it done that, the complete nonsense of transferring the temporary buffer into the union's 4 byte buffer in completely the wrong order could have been avoided.

Understand the problem. Then fix it. Don't just randomly change things hoping that it'll suddenly and magically work.

#include <SPI.h> 

union myUserData {
   float x;
   byte myBytes[4];
};
myUserData myUD;

void setup() {
   Serial.begin(115200);
   digitalWrite(SS, HIGH);
   pinMode(SS, OUTPUT);
   SPI.begin();
   delay(100);
}

void loop() {
   digitalWrite(SS, LOW);
   for( int i = 0; i < 4; i++ ) {
      myUD.myBytes[i] = SPI.transfer(0);
      delayMicroseconds(100); // allow Slave to process the received data byte
   }
   digitalWrite(SS, HIGH);
   
   for( int i = 0; i < 4; i++ ){
      if( myUD.myBytes[i] < 0x10 ) {
         Serial.print('0');
      }
      Serial.print(myUD.myBytes[i], HEX);
      Serial.print(" ");
   }
   Serial.print(" float: ");
   Serial.println(myUD.x, 2);
   delay(3000); //test interval
}

Why would you use a union in this way which is UB?

I wouldn't do it this way at all. I'm just showing that the OP's idea does work if done properly.

When I used 0-0, 1-1,..., it gave me nonsense numbers. I noticed that the order of bytes is shifted, so I corrected it and it works. There are good float outputs now, just for couple of numbers (2, 8, 32, 128, etc.) are mistaken. Same behavior as on the beginning.