I'm trying to transfer data between a Arduino Uno board to a NodeMCU ESP8266 board, using nRF24L01+ transceivers module and RF24 library on both side. The data I'm transferring are stored in a struct
defined this way:
// Struct declared in both code
struct sensorData {
bool doorOpened;
int light;
int temperature;
int humidity;
float voltage_battery;
};
// Variable in Uno code (source node)
sensorData dataToSend;
// Variable in ESP8266 code (dest node)
sensorData dataToReceive;
But the data are not transferred properly between the two boards and I get random values or 0 when trying to display them from ESP8266 (values are correctly displayed from Uno).
Display in Uno (using signed short
as data type, see following paragraphs):
Data displayed from Arduino UNO
Display in ESP8266 (using signed short
as data type, see following paragraphs):
Data displayed from NodeMCU ESP8266
After some testing, I found out that memory-allocation on both boards are different, as Uno takes 2 bytes to store int
while ESP8266 takes 4 bytes. Moreover, sensorData takes 11 bytes of memory on Uno (1 for bool
, 2 for int
and 4 for float
, which sums up to 11 bytes), but takes 20 bytes of memory on ESP8266 (1 for bool
, 4 for int
and 4 for float
, which does NOT sum up to 20 !).
Trying to solve this, I tried changing int
for signed int
, byte
, short
, signed short
but the transmission problem remains. When using short
or signed short
(which takes 2 bytes on both boards), my structure sensorData
still takes 11 bytes of memory on UNO while it takes 12 bytes of memory on ESP8266. For some reason, ESP8266 always takes more space than needed to store sensorData
, probably causing bits to be wrongly placed when receiving data.
I'm sure that both boards, radio module and transmission protocol work as I successfully tested the same code with a smaller struct (text
was "Hello World !" and cmp
was increased at each transmission):
struct Package {
char text[16];
long cmp;
};
Does anyone have faced this issue before ? How could I solve that ?
-
1By the way, I'm used to coding (VBA, C++ and python mostly) but I'm still new to the Arduino world, so I might be missing something obvious here.Vincent– Vincent2021年06月25日 06:35:45 +00:00Commented Jun 25, 2021 at 6:35
5 Answers 5
There are three main issues you may need to deal with when passing structures between systems that use different processors.
The first is that data types vary in size between architectures. You can avoid this for integers by avoiding the basic C types and instead using the fixed size types from stdint.h/cstdint. For floating point types this is less of a problem as most systems use the IEEE single and double precision types for float and double.
The second is padding, according to the C standard padding is implementation dependent. In practice most compilers will insert the minimum amount of padding needed to meet alignment requirements. Exactly what the alignment requirements are also depend on the implementation, but the alignment requirement of a type is always a factor of it's size.
You can thus avoid compiler-generated padding on most architectures by designing your structure to place types on "natural" alignment (that is their offset in the structure is a multiple of their size) and to have a size that is a multiple of the largest type in the structure. So you might design your structure as.
struct sensorData {
int8_t doorOpened; //size 1 offset 0
int8_t spare; //size 1 offset 1
int16_t light; //size 2 offset 2
int16_t temperature; //size 2 offset 4
int16_t humidity; // size 2 offset 6
float voltage_battery; //size 4 offset 8
};
Another option is to use packed structures, but extreme care is needed when using such, especially in C++. It is easy to create unaligned pointers or references.
The final issue is byte order (also known as endian), if your systems use different byte orders for integer or floating point values you will need to insert conversions to deal with this.
-
Interesting. Do you have an example of a non-aligned struct, please? Would it be something like a float after an int16_t, so
size 4 offset 2
?Eric Duminil– Eric Duminil2021年06月25日 22:26:43 +00:00Commented Jun 25, 2021 at 22:26 -
1@EricDuminil Anything that breaks natural alignment, uint8/uint16/uint8 breaks that because the uint16 isn't aligned to a 16-bit boundary.Dave Newton– Dave Newton2021年06月25日 23:18:30 +00:00Commented Jun 25, 2021 at 23:18
int
, long
, etc have different sizes depended on the compiler and target.
Use explicit sizes to make sure you're variables have the size you want. Eg. uint8_t
, int16_t
or int32_t
, ... (as @Mat commented)
Also, I wouldn't use bool
(if writing in C
) as it's not really standard in C
and the size is also implementation dependent: https://stackoverflow.com/questions/1608318/is-bool-a-native-c-type
Extra, there are different types of explicit sizes: https://stackoverflow.com/questions/5254051/the-difference-of-int8-t-int-least8-t-and-int-fast8-t (but don't use it here otherwise you'll have again the same problem :))
-
3"bool" is fine in general; we're discussimg C++. The issue with "bool" is that its size is implementation-dependent.Dave Newton– Dave Newton2021年06月25日 11:29:09 +00:00Commented Jun 25, 2021 at 11:29
-
@DaveNewton I did not know we were talking
C++
. I'll leave it in my answer thoughSwedgin– Swedgin2021年06月25日 11:50:50 +00:00Commented Jun 25, 2021 at 11:50 -
Thank you for the explanation and links, they are quite useful ! I never had this issue before so I didn't pay attention to explicity sizes, but I will from now on.Vincent– Vincent2021年06月25日 11:54:41 +00:00Commented Jun 25, 2021 at 11:54
-
3@Swedgin That's what the Arduino ecosystem uses.Dave Newton– Dave Newton2021年06月25日 12:06:53 +00:00Commented Jun 25, 2021 at 12:06
-
@DaveNewton My bad, it's been so long since I used the Arduino ecosystem that I forgot it was C++.Swedgin– Swedgin2021年06月25日 12:20:21 +00:00Commented Jun 25, 2021 at 12:20
In addition to the rule of always using fixed and predictable sizes across different architectures, it's also a good idea to pack your structs. This prevents the compiler from padding smaller variables with blanks to align them with the architecture's word boundary when needed. For example:
struct Package {
uint8_t doorOpened;
uint16_t light;
int16_t temperature;
uint8_t humidity;
float voltage_battery;
} __attribute__((packed));
Depending on what "bytewise access" instructions are available on your target 32-bit architecture, without the packing you may end up with (worst case) the equivalent of:
struct Package {
uint8_t doorOpened;
// 3 bytes padding
uint16_t light;
// 2 bytes padding
int16_t temperature;
// 2 bytes padding
uint8_t humidity;
// 3 bytes padding
float voltage_battery;
} __attribute__((packed));
Of course there may be a granular enough access method in the architecture which means the packing is irrelevant - but it's better to have it and it be ignored than to not have it and need it.
-
@Vincent Yes, it can cause a problem. But either it causes problems or it doesn't, so you can just try it. You can also add your own paddingStack Exchange Broke The Law– Stack Exchange Broke The Law2021年06月25日 20:43:46 +00:00Commented Jun 25, 2021 at 20:43
-
I wasn't aware that "using a pointer to a variable" was a "stupid thing"?Stack Exchange Broke The Law– Stack Exchange Broke The Law2021年06月25日 20:44:33 +00:00Commented Jun 25, 2021 at 20:44
The esp8266 has a 32 bit mcu and the uno an 8 bit mcu so their variables have different storage requirements. You need to allow for this in your code.
-
1How can I do that ? Should I format the source data to be properly understood when received by ESP8266 ?Vincent– Vincent2021年06月25日 07:00:34 +00:00Commented Jun 25, 2021 at 7:00
-
Declaring
signed short doorOpened
instead ofbool doorOpened
fixed the issue (as both have 2 byte size), but if you have time I'm still curious about properly handling abool
between the 2 MCU.Vincent– Vincent2021年06月25日 07:08:56 +00:00Commented Jun 25, 2021 at 7:08 -
2Use types with explicit sizes (
uint8_t, uint16_t
etc).Mat– Mat2021年06月25日 07:54:33 +00:00Commented Jun 25, 2021 at 7:54 -
1Note particularly that
int
is 16 bits on Arduino Uno, but 32 bits on ESP8266.PMF– PMF2021年06月25日 08:35:56 +00:00Commented Jun 25, 2021 at 8:35 -
@PMF I noticed that, which is why I changed
int
toshort
(16 bits on both MCU).Vincent– Vincent2021年06月25日 11:32:25 +00:00Commented Jun 25, 2021 at 11:32
For all the reasons mentioned in the other answers (different types sizes, alignment requirements and endianness), it's often a bad idea to use structs for data that needs to be sent/received (be it over the network or stored in files), as interoperability is often a problem.
Using a plain array of bytes (i.e. uint8_t[]) is often a much better solution, together with functions to read/write data with specific types at specific offsets.
In many higher-level languages (perl, php, python...) this is often done using pack
/unpack
or equivalents, but as far as I know there is no direct equivalent in C.
You then end up using temporary variables, htons
/ntohs
/htonl
/ntohl
and memcpy
to copy data between the temporary variables and the transmit buffer. Floats are a bit more tricky because even though their format is well defined, they endianness is not, and may not be the same as integers, but htonf
/ntohf
are rare. In your case, I would recommend converting to integers with a factor of 10 (if you need one digit) or 100 (if you need 2) for instance.
If you're using C++ you could consider using Google's Protocol buffers
Explore related questions
See similar questions with these tags.