13

The task is to send data by I2C from Arduino to STM32.

So I got Struct and Enums defined in Arduino using C++:

enum PhaseCommands {
    PHASE_COMMAND_TIMESYNC  = 0x01,
    PHASE_COMMAND_SETPOWER  = 0x02,
    PHASE_COMMAND_CALIBRATE = 0x03
};

enum PhaseTargets {
    PHASE_CONTROLLER = 0x01,
    // RESERVED = 0x02,
    PHASE_LOAD1 = 0x03,
    PHASE_LOAD2 = 0x04
};

struct saatProtoExec {
    PhaseTargets   target;
    PhaseCommands  commandName;
    uint32_t       commandBody;
} phaseCommand;

uint8_t phaseCommandBufferSize = sizeof(phaseCommand);

phaseCommand.target = PHASE_LOAD1;
phaseCommand.commandName = PHASE_COMMAND_SETPOWER;
phaseCommand.commandBody = (uint32_t)50;

On the other side I got the same defined using C:

typedef enum {
    COMMAND_TIMESYNC  = 0x01,
    COMMAND_SETPOWER  = 0x02,
    COMMAND_CALIBRATE = 0x03
} MasterCommands;

typedef enum {
    CONTROLLER = 0x01,
    // RESERVED = 0x02,
    LOAD1 = 0x03,
    LOAD2 = 0x04
} Targets;

struct saatProtoExec {
    Targets         target;
    MasterCommands  commandName;
    uint32_t        commandBody;
} execCommand;

uint8_t execBufferSize = sizeof(execCommand);

execCommand.target = LOAD1;
execCommand.commandName = COMMAND_SETPOWER;
execCommand.commandBody = 50;

And then I compare this Structs byte-by-byte:

=====================
BYTE    | C++   |  C
=====================
Byte 0 -> 0x3  -> 0x3
Byte 1 -> 0x0  -> 0x2
Byte 2 -> 0x2  -> 0x0
Byte 3 -> 0x0  -> 0x0
Byte 4 -> 0x32 -> 0x32
Byte 5 -> 0x0  -> 0x0
Byte 6 -> 0x0  -> 0x0
Byte 7 -> 0x0  -> 0x0

So why bytes 1 and 2 are different?

Bulkin
  • 1,020
  • 12
  • 27
  • 3
    I'd say enumerate size + padding... – Jean-François Fabre Nov 07 '16 at 12:45
  • 4
    Is the same compiler used for both, same flags? both standards say nothing about the size if an enum, so its allowed that they differ. Still should be the same on one compiler – Norbert Lange Nov 07 '16 at 12:46
  • @NorbertLange No, compilers are different. One is Arduino native, the second is KEIL MDK-ARM – Bulkin Nov 07 '16 at 12:48
  • 18
    You're going between two different CPUs. You're lucky that endianness at least seems to match. It's not a C vs. C++ problem, it's a Arduino vs. ARM problem. – Art Nov 07 '16 at 12:50
  • @Art You mean between 8-bit vs 32-bit? – Bulkin Nov 07 '16 at 12:59
  • 3
    @Bulkin I mean between CPU family X and CPU family Y. Regardless of word size. You can not assume that two CPU families will have an ABI that matches. – Art Nov 07 '16 at 13:04
  • @Art Ok, I understand. I mean the same. – Bulkin Nov 07 '16 at 13:08
  • http://stackoverflow.com/questions/119123/why-isnt-sizeof-for-a-struct-equal-to-the-sum-of-sizeof-of-each-member – Lundin Nov 07 '16 at 13:32
  • 2
    In C, implementation is free to pick any type that will hold the specified enum values. In your case it seems a single byte integer was chosen. – 2501 Nov 07 '16 at 18:40
  • @Bulkin: What if you compile the C snippet on the C++ host, what do you observe then? – Matthieu M. Nov 07 '16 at 20:31
  • @MatthieuM. STM32CubeMX generates code for C. Also Keil MDK ARM supports mostly features of C++ compiler, but not the Dynamic Syntax Checking feature of µVision's Editor as written in [Tech support](http://www.keil.com/support/docs/3696.htm). So full compability I write C snippets for STM32. – Bulkin Nov 08 '16 at 06:17

3 Answers3

24

This is a really bad idea.

You should never rely on the binary representation of structures being the same beween two implementations of C, not to mention going from C to C++!

You should do some proper serialization/deserialization code, to take control at the byte level of the structure's external representation.

That said, it could be due to padding. That you end up sending padding (which is just something added by a compiler to keep its host CPU happy) over an external link is another sign of how broken this approach is.

unwind
  • 391,730
  • 64
  • 469
  • 606
9

In the C version it is clear that sizeof(Targets) == 1. And it looks like the second field of the struct is 2-byte aligned, so you have a padding byte, with undefined contents.

Now, in C++, sizeof(PhaseTargets) may be 1 or 2. If it is 1 (likely) all is well and you have the same padding space, just happened to have different garbage value. If it is 2... well, you'll have a wrong enum value!

The easy way to initialize the struct would be in the definition of the variable. If you don't have the values yet, just add a 0 and all the struct will be 0 initialized.

struct saatProtoExec execCommand = {0};

If that cannot be done, you can memset() it to zero before using.

A portable alternative would be to declare the fields of the struct as integers of the proper size, and use the enum types just as collections of constants.

struct saatProtoExec {
    uint8_t         target;
    uint8_t         commandName;
    uint8_t         padding[2];
    uint32_t        commandBody;
} execCommand;
rodrigo
  • 94,151
  • 12
  • 143
  • 190
  • Well, it is a good idea. I will use it some other way. But looks like it would be better to send data without padding and parse them on reciever. – Bulkin Nov 07 '16 at 13:01
  • 2
    This alternative is only portable for architectures with the same endianess and with 32-bit aligment or lesser (which seems to be the case for this specific question). However, using serialization as pointed ou in [unwind's answer](http://stackoverflow.com/a/40465574/3951057) is the actually portable solution and can also be more bandwidth-efficient. – André Sassi Nov 07 '16 at 13:01
  • 1
    That struct gives you a fighting chance that it might be portable, but no guarantee. It might easily be implemented as four 64-bit words. – gnasher729 Nov 07 '16 at 17:20
2

Well, as already said, you should not rely on a structure having a specific format. However, it is sometime convenient to use a structure instead of serialization as it can be more efficient if no conversion are required and the representation is compatible and efficient at run-time on a given architecture.

Thus, here are some advices when using structures:

  • Uses appropriate pragmas or attributes to ensure that the layout does not depends on project options.
  • Add checks to validate that the final size is the expected one. In C++, you can use static_assert.
  • Also add check that the endianness is the expected one.
  • Having unit test to confirm the format would also be a good idea.

In any case, I would also recommend to avoid using the same struct in your general code and for serialization. Sometime, it is useful to have additional members that are not serialized or do some adjustments.

Also another important thing is to plan for the fact that at some point, you might need to add additional fields and convert old data or some bugs might be found and require data to be corrected.

You might also want to consider what should happen if a new file is opened in an old software. In many cases, it would probably be rejected.

Phil1970
  • 2,605
  • 2
  • 14
  • 15
  • Well, yes. For now I am leaving idea to transmit Struct. I transmit data in two parts of 4 bytes each as reciever expects. This give me a goal to use the same STM32 as sender in future so I could send Struct which is created by the same platform and compiler. – Bulkin Nov 08 '16 at 08:07