How to receive float data with SPI (STM32) - Page 1
Forum section: electronics / general

author: nishantnidaria
author_rank: Contributor
timestamp: March 08, 2022, 08:59:32 am
content: Hello everybody! I am trying to establish SPI communication between an Arduino nano and STM32G0 (custom board).My goal is to send sensor data (float values) from Arduino nano to STM32.Now, the stm32 is receiving but only string data:Let's say my SPI buffer is:Code:char rxbuff ;and the command to receive in blocking mode is :Code:HAL_SPI_Receive(&hspi1, (uint8_t*)rxbuff, 10, 1000);I am using uart to print what i have received as such:Code:printmsg("%d\r\n", rxbuff);printmsg("%s\r\n", rxbuff);printmsg("%c\r\n", rxbuff);printmsg("%.2f\r\n", rxbuff);When i send a string e.g., "SPI open up!" it works fine, but when i send actual data e.g., "float data = 12.12;"Can i receive float data or integer data using only char buffer? or do i need to create a function specifically to receive float data?i receive nothing or atleast it prints just weird characters or nothing. I am sure there are many things wrong in my method and code.Could anyone please help me reach my intitial goal?Thanks in advance.

author: Siwastaja
author_rank: Super Contributor
timestamp: March 08, 2022, 09:15:58 am
content: Code:typedef union{struct __attribute__((packed)){float value; // 4 bytesuint32_t something_else; // 4 bytesuint16_t more_stuff; // 2 bytes};uint8_t u8;} message_t;message_t rxbuff;void func(){HAL_SPI_Receive(&hspi1, rxbuff.u8, sizeof rxbuff, 1000);printmsg("%f", rxbuff.value);}This code relies on both sides using the same endianness, which is today realistically always the case.

author: nishantnidaria
author_rank: Contributor
timestamp: March 08, 2022, 10:47:46 am
content: Thank you for the reply and help, i tried the code and it didn't give me any error but i receive 0.0000 when i send e.g.,23.65

author: DavidAlfa
author_rank: Super Contributor
timestamp: March 08, 2022, 05:36:46 pm
content: I wouldn't use unions really.Float can be recasted to uint8 before sending, breaking it into 8-bit data transfers:Code:float data; // data can be float, double...void spi_send(uint8_t * data, uint16_t size);void my_func(){spi_send( (uint8_t *)&data, sizeof(data) );}The receiver should do the same:Code:float data; // data can be float, double...void spi_receive(uint8_t * data, uint16_t size);void my_func(){spi_receive( (uint8_t *)&data, sizeof(data) );}The worst case scenario would be a endiannes mismatch, requiring byte reordering:Code:float data; // data can be float, double...void my_func(){uint8_t temp;uint8_t * out = (uint8_t *)&data;uint8_t * last = first + (sizeof(data)-1);spi_receive( out, sizeof(data) );for (uint8_t i=0; i<(sizeof(data)/2); i++){temp = *out;*out++ = *last;*last-- = temp;}}This also allows you to change the size of data requiring minimal code tweaking (ex. Changing data type to double).

author: Siwastaja
author_rank: Super Contributor
timestamp: March 08, 2022, 06:15:44 pm
content: Quote from: DavidAlfa on March 08, 2022, 05:36:46 pmCrappy solution because works only one way (float -> uint8) as is. The other way (uint8 -> float), requires alignment attributes on the uint8 array.Union works both ways with the same syntax, and compiler knows how to handle alignment so that all types are accessed correctly.Pointer casting is not the ultimate sin, but unions are better, and also document the intent quite well: this is a type which is accessed in these different ways.

author: DiTBho
author_rank: Super Contributor
timestamp: March 08, 2022, 06:28:27 pm
content: Quote from: DavidAlfa on March 08, 2022, 05:36:46 pm(it already happens with u-boot, which is everywhere, even in my toaster, so it's norm-job nowadaysno, ok, seriously, you are right)

author: DiTBho
author_rank: Super Contributor
timestamp: March 08, 2022, 07:22:27 pm
content: Code:typedef union{float fp32;uint32_t my32;} magic_t;void float_to_uint8(float data_in,uint8_t data_out[]){magic_t magic;magic.fp32 = data_in;printf("magic = 0x%0lx\n", magic.my32);data_out = ((magic.my32 & 0x000000ff));data_out = ((magic.my32 & 0x0000ff00) >> 8);data_out = ((magic.my32 & 0x00ff0000) >> 16);data_out = ((magic.my32 & 0xff000000) >> 24);}float uint8_to_float(uint8_t data_in[]){float data_out;magic_t magic;magic.my32 = 0x00000000;magic.my32 = magic.my32 | (data_in);magic.my32 = magic.my32 | (data_in << 8);magic.my32 = magic.my32 | (data_in << 16);magic.my32 = magic.my32 | (data_in << 24);data_out = magic.fp32;return data_out;}Something like this (just an idea) should be interesting because endian-free.(magic_t is only used internally)edit: typo

author: GromBeestje
author_rank: Frequent Contributor
timestamp: March 08, 2022, 07:46:12 pm
content: Quote from: DiTBho on March 08, 2022, 07:22:27 pmWhat do you mean endian-free? The endianness of the sending and receiving machine must be equal, otherwise the result is wrong.Even though Big Endian is rare these days, one should be aware of this.

author: DiTBho
author_rank: Super Contributor
timestamp: March 08, 2022, 07:50:06 pm
content: Quote from: GromBeestje on March 08, 2022, 07:46:12 pmlook at the above codeThanks to that trick(1) the information sent from the sender to the receiver depends neither on the endianness of the receiver nor of the sender.edit:(1) I mean { shiftR, shiftL, bitwiseOr, bitwiseAnd }: all common ALU operations.

author: DavidAlfa
author_rank: Super Contributor
timestamp: March 08, 2022, 07:55:33 pm
content: Quote from: Siwastaja on March 08, 2022, 06:15:44 pmWhy? Float is basically a 32 bit integer.A 8-bit pointer should address the lower memory byte?Then just write the 4 bytes as they're received, using the float as a 4-byte array.What am I missing here?

author: GromBeestje
author_rank: Frequent Contributor
timestamp: March 08, 2022, 09:19:45 pm
content: Quote from: DiTBho on March 08, 2022, 07:50:06 pmI see, sorry I was blind.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: March 08, 2022, 10:21:32 pm
content: Why on earth would you use floats for this? There is no real-world sensor that needs the range of floats. Remember, float gives you range, not resolution.

author: JPortici
author_rank: Super Contributor
timestamp: March 08, 2022, 10:48:59 pm
content: Quote from: DavidAlfa on March 08, 2022, 07:55:33 pma four bytes array may start at address offset 0,1,2,3.Using an union between float and/or uint32_t and/or uint8_t ensures that the variable is always 32bit aligned. That will mean same effort when accessing the bytes and much less effort to access the float/uint32_t.

author: DiTBho
author_rank: Super Contributor
timestamp: March 08, 2022, 11:33:02 pm
content: Quote from: mikeselectricstuff on March 08, 2022, 10:21:32 pmYes, good point. I don't usually comment on people's needs because I don't actually know what they are doing and for what, and for example I know there are some "crazy" modbus sensors that output floating point over the serial line. Really insane, but also great because they work out of the box, so people don't usually complain. I don't know why, probably because the programmers chose it that way.Who knowsAnyway, years ago I developed a small C ++ library forfixed point numbers. Why C ++? Because it can be used to overload operators so that it looks more natural, but is irrelevant, rather aesthetic (I'm a bit of a snob at times), the fixed point can be implemented in C as well and can be a good choice if you tune it correctly to your specific case.The good isthat it's still 32bit, the above tricks still works, and the library takes less resources than every softfloat library.

author: DavidAlfa
author_rank: Super Contributor
timestamp: March 09, 2022, 12:11:49 am
content: Quote from: JPortici on March 08, 2022, 10:48:59 pmBut you're already pointing to an existing float variable, casting it as uint8_t*.The float will be aligned, so would be a pointer to it?

author: Siwastaja
author_rank: Super Contributor
timestamp: March 09, 2022, 09:58:30 am
content: Quote from: DavidAlfa on March 09, 2022, 12:11:49 amPointer to an existing float can be safely cast to char* or void*. It's just worth understanding that uint8_t buf[] cannot be cast into float* for reinterpretation, unless you manually make sure the buffer is aligned by four.Typing system in C is quite good, actually; it's a good idea to let it do the heavy lifting, so that the compiler figures out the right way to access. Union is just the right tool to describe data types which can be accessed in different ways.

author: DavidAlfa
author_rank: Super Contributor
timestamp: March 09, 2022, 11:14:28 am
content: Yes, but I didn't do that anywhereso where's the problem?

author: Doctorandus_P
author_rank: Super Contributor
timestamp: March 16, 2022, 06:39:59 pm
content: I totally agree with MikesElectricStuff.why use floats at all?It's one of the reasons I do not like MBed. It tries to use floats for everything.You want a 12us delay? Enter a float ing point number in seconds (and what sort of overhead do you then have from your 12us?)ADC peripheral library functions give a number between 0 and 1.Yuch!But if you want to go along and use floats anyway...I don't like DavidAlfa's advise. It's is very sloppy programming, and gluing pieces of sloppy code together leads to headaches and medicine abuse.Structures and unions are a pretty good way and flexible for formatting data.I would make a structure for a whole packet, including a header, and combine that with unions.Your floating point numbers do not mean anything on themselves. Especially when you start combining data from different sensors.So think about how you want to structure your data. Embed the meaning of the data into the packets.I normally also put an array of bytes into the packet union.This helps with debugging. It makes it for example easier to verify if your floating point numbers are actually witten into your packet.You can for example send a bunch of dummy data with different numbers, and then check which bytes change in your packet.If your floating point number ends up in the wrong bytes in your packet, then you have no chance at all in interpreting the data correctly.Do it in small steps, and extend each iteration to build a complete program.

author: DavidAlfa
author_rank: Super Contributor
timestamp: March 16, 2022, 11:58:46 pm
content: This is just a technical discussion right? I also like to know what I'm doing wrong and whyUnions aren't proof either due compiler padding. Ex.:Code:union{uint32_t f;struct{unsigned int a;unsigned char b;unsigned char c;}bits;}data;Unless you specify the "packed" attribute, a lot of compilers will size all "bits" members to int to provide easier access.So you might write to a, b and c, and get strange data in f.You say my way would be wrong, but a float is always 32-bit (unless using some weird datatype), so it's also always 32-bit aligned.And any 32-bit variable can be seen as a 4-byte array, so pointing to it using uint8_t* should always work.From my understanding (Which might be wrong), these 3 examples should work in the same way when passing "bits" 8-bit pointer:Code:union{float f;uint8_t bit;}data;uint8_t * bits = data.bit;Code:typedef uint8_t bits_t;float f;bits_t * bits = (bits_t *)&f;Code:float f;uint8_t * bits = (uint8_t *)&f;I understand that you shouldn't do it backwards, casting any uint8_t as float*, because it can be 32-bit missaligned and crash the system.But this is not the case and should never happen because the pointer address originally came from a float:Code:float f;uint8_t * bits = (uint8_t *)&f;*(float*)bits = 1.023456f;

author: peter-h
author_rank: Super Contributor
timestamp: March 18, 2022, 12:15:26 pm
content: The real challenge with transmitting data like this is packaging, detecting end of packet, and error recovery. Once you got this sorted, you may as well sprintf to a buffer and transmit that, and use sscanf to parse it at the other end.In performance-limited scenarios it pays to replace those two functions with something more dedicated. I was once involved with a project which was parsing HPGL, with a Z180 running at 8MHzand had to replace those functions with specialised versions. The end result was about 100x faster than the C runtimes (IAR C).

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: March 20, 2022, 11:08:11 pm
content: Convert the float to a string and send as a string. Convert back to float on the other end. This is the only way I've been able to do it reliably. Strings limit byte values to ASCII making unique start and end values available and safe.

author: Siwastaja
author_rank: Super Contributor
timestamp: March 21, 2022, 04:08:13 pm
content: Quote from: Ground_Loop on March 20, 2022, 11:08:11 pmI think this tells more about you than about the idea of using strings.Sure, use strings if that makes sense. Otherwise, it's a massive complication, if done for no reason. Also surprisingly prone to bugs (variable and nondeterministic message length, zero termination; usual catches with sprintf, scanf and the like); if you can't make a simple binary protocol work (for some reason or another), you are even less likely to be able to write a robust string solution.QuoteThere are much better encodings if you want to reserve start/end markers, than converting all to decimal strings and back.But, the OP did specify SPI, which has hardware packet delimitation. There is no need to check for magic numbers from unsynchronized data stream.This is also why ASCII over SPI is almost nonexistent. Through UART, yes, partially because of the tradition of being able to use standard terminal programs for development / debug (at the expense of having to write more complicated parsers).

author: Ground_Loop
author_rank: Frequent Contributor
timestamp: March 22, 2022, 02:43:35 pm
content: It likely does say a lot about me, or at least my programming skills. Although an EE that learned actual electronics more than 30 years ago, I have limited formal training in programming and have only been working in C as a hobbyist for a couple years. My day job is building factories. I would be interested in learning better ways though.