9

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 ?

1
  • 1
    By 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
    Commented Jun 25, 2021 at 6:35

5 Answers 5

12

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.

2
  • 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? Commented 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. Commented Jun 25, 2021 at 23:18
12

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 :))

5
  • 3
    "bool" is fine in general; we're discussimg C++. The issue with "bool" is that its size is implementation-dependent. Commented Jun 25, 2021 at 11:29
  • @DaveNewton I did not know we were talking C++. I'll leave it in my answer though
    – Swedgin
    Commented 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
    Commented Jun 25, 2021 at 11:54
  • 3
    @Swedgin That's what the Arduino ecosystem uses. Commented 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
    Commented Jun 25, 2021 at 12:20
9

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.

0
2

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.

5
  • 1
    How can I do that ? Should I format the source data to be properly understood when received by ESP8266 ?
    – Vincent
    Commented Jun 25, 2021 at 7:00
  • Declaring signed short doorOpened instead of bool doorOpened fixed the issue (as both have 2 byte size), but if you have time I'm still curious about properly handling a bool between the 2 MCU.
    – Vincent
    Commented Jun 25, 2021 at 7:08
  • 2
    Use types with explicit sizes (uint8_t, uint16_t etc).
    – Mat
    Commented Jun 25, 2021 at 7:54
  • 1
    Note particularly that int is 16 bits on Arduino Uno, but 32 bits on ESP8266.
    – PMF
    Commented Jun 25, 2021 at 8:35
  • @PMF I noticed that, which is why I changed int to short (16 bits on both MCU).
    – Vincent
    Commented Jun 25, 2021 at 11:32
2

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.