Converting from bytes to float via Union not working on ESP32

I am trying to convert messages received on my CAN line into two separate floats (4 bytes) and then doing the same thing in the other direction to send. Converting from float to byte works but when I try to convert from bytes to floats the float is always 0.00. Below is the test code I am using. I am running this on the ESP32 dev kit v1

typedef union {
    float value;
    byte bytes[sizeof(float)];
} FLOAT_BYTE_UNION;

FLOAT_BYTE_UNION test1;
FLOAT_BYTE_UNION test2;

void setup() {
  test1.value = 5;
  test2.bytes[0] = 0;
  test2.bytes[1] = 1;
  test2.bytes[2] = 2;
  test2.bytes[3] = 3;
  Serial.begin(115200);
  while (!Serial);
  
}

void loop() {
  Serial.println();
  for (int i = 0; i<sizeof(float); i++){
    Serial.print(test1.bytes[i], HEX);
  }
  Serial.print(" -> ");
  Serial.println(test2.value);
}

output is:

00A040 -> 0.00

Tell us what you think is going-on there

Two separate tests. test1's float is being assigned to 5 and test2's bytes are being assigned to 0,1,2,3 respectively.

And what do you think is wrong with the results you are seeing?

(You're not really supposed to use unions for type-punning; use memcpy instead)

Byte array [0,1,2,3] should not equal 0.00 float correct?

Find yourself an IEEE754 converter online, and see what result it gives with those values.

2 Likes

Gotcha, it is a very small number. Thank you, I should have checked that before posting. I appreciate the help.

When using a union to send bytes, between separate systems, one has to be aware of "Endian" problems. For example, AVR architecture like ATMega328P is little-endian while "Freescale" (NXP formerly Motorola) are notoriously Big-endian. I have no idea what a tensilica or whatever the esp32 is.

you should not use union for that in the first place :wink:

Your example code populates bytes in test1 but prints the value of test2.

Agreed, but isn't that what the OP is doing? or do I missunderstand?

The way I understood it is that OP is receiving bytes on a CAN bus and wants to populate a structure with the data so s/he can then read multibytes data through the structure

the way to do this is to receive the data in a byte buffer and then use memcpy() to transfer the buffer into the struct (and let the compiler sort things out).

Using a union to receive multiple bytes of a larger variable type is problematic for the same reasons as sending and receiving. The pitfall is endianness. Also, unions are implementation specific how the array is ordered with the float.

For reference this is my actual code. The original code was a test I was trying to preform.

void CAN_Sender(void * parameters){
    FLOAT_BYTE_UNION translationMeasured_f;
    FLOAT_BYTE_UNION rotationMeasured_f;

    for(;;){
        double startTime = millis();
        translationMeasured_f.value = (float)Translation_Stepper.getCurrentPositionInMillimeters();
        rotationMeasured_f.value = (float)Rotation_Stepper.getCurrentPositionInRevolutions();;
        if (CAN_running()) {
            CAN.beginPacket(0b10010000 + (1 << boardID));
            for (int i = 0; i < sizeof(float); i++){
                CAN.write(translationMeasured_f.bytes[i]);
            }
            for (int i = 0; i < sizeof(float); i++){
                CAN.write(rotationMeasured_f.bytes[i]);
            }
            CAN.endPacket();
        }
        if (!RUNNING || !ENABLED) {
            CAN.beginPacket((1 << boardID) + 0b10000000);
            for (int i = 0; i < sizeof(float); i++){
                CAN.write(translationMeasured_f.bytes[i]);
            }
            for (int i = 0; i < sizeof(float); i++){
                CAN.write(rotationMeasured_f.bytes[i]);
            }
            CAN.endPacket();
        }
        double comExecutionTime = millis() - startTime;
        delay(COM_DELAY - comExecutionTime);
    }
}

//Need to decide on packet format
void CAN_Handler(int packet_size){
    FLOAT_BYTE_UNION translationDesired_f;
    FLOAT_BYTE_UNION rotationDesired_f;
    messageTime = millis();
    if (SERIAL_ON) {
        Serial.print("(packet: 0x");
        Serial.print(CAN.packetId(), HEX);
        Serial.print(" ");
    }

    //EMERGENCY STOP
    if (CAN.packetId() & 0b11110000 == 0b00000000) {
        SHUTDOWN();
        E_STOP = true;
        return;
    }

    //ZERO
    if (CAN.packetId() & 0b11000000 == 0b01000000) {
        SHUTDOWN();
        ZERO();
        START();
        return;
    }

    int rotationIndex = sizeof(float)-1;
    int translationIndex = sizeof(float)-1;
    bool dataReceived = false;
    while(CAN.available()){
        dataReceived = true;
        if (translationIndex >= 0){
            translationDesired_f.bytes[translationIndex--] = (byte)CAN.read();
        } else if (rotationIndex >= 0) {
            rotationDesired_f.bytes[rotationIndex--] = (byte)CAN.read();
        } else {
            if (SERIAL_ON) Serial.println("DATA DROPPED)");
            return;
        }
    }
    if (dataReceived){
        if (!((translationIndex < 0) && (rotationIndex < 0))) {
            if (SERIAL_ON) Serial.println("LESS THAN 8 BYTES RECEIVED)");
            return;
        }
        rotationDesired = (double)rotationDesired_f.value;
        translationDesired = (double)translationDesired_f.value;
        if (SERIAL_ON){
            Serial.print(" Rotation: ");
            Serial.print(rotationDesired);
            Serial.print(" Translation: ");
            Serial.print(translationDesired);
        }
    }

    if (SERIAL_ON) Serial.println(")");
    if (!RUNNING) START();
}

rotationDesired and translationDesired are doubles that are being using in another task to tell my stepper motors where to go. I have too many global variables here and need to do some clean up, but this shows generally what I am trying to do. I am doing everything in big endian.

The Motorola 68k architecture was big-endian, but AFAIK all of their ARM Cortex chips are little-endian. The endianness of ARM (and also PPC and MIPS (eg PIC32) is actually selectable - sometimes when the chip is designed, sometimes at boot time via some pin state, and even at runtime (but not in Cortex-M, AFAIK.)

It's been a long time since I've seen a "new" chip that defaulted to big-endian.
Tensilica (ESP8266, ESP32) and RISC-V are both little-endian.

(You're not really supposed to use unions for type-punning; use memcpy instead)

Sigh. If you don't mind the significant performance hit. :frowning:

Hey! I'm a native English speaker.
I don't even agree it should refer to word-play, so please, don't shoot the messenger.

Which performance hit?
memcpy is the correct way to do a cast like this. Compiler developers know that, and no sensible compiler will actually emit the call to memcpy. Especially if you're just inspecting the bytes of a float.

Proof: Compiler Explorer

float bytes_to_float(const unsigned char (&bytes)[4]) {
    float f;
    static_assert(sizeof f == sizeof bytes, "Unexpected float size");
    memcpy(&f, bytes, sizeof f);
    return f;
}

Is compiled into (GCC 11.2, ARM64, -O1):

bytes_to_float(unsigned char const (&) [4]):
        ldr     s0, [x0]
        ret

If you just use the resulting floats, they never even get instantiated. For example:

// Interpret the two byte operands as floats, add them as floats, and store the
// result back into the first operand as a float.
void compound_add_floats_as_bytes(unsigned char (&op1)[4], unsigned char (&op2)[4]) {
    float op1_f, op2_f;
    static_assert(sizeof op1_f == sizeof op1, "Unexpected float size");
    memcpy(&op1_f, op1, sizeof op1_f);
    memcpy(&op2_f, op2, sizeof op2_f);
    float sum = op1_f + op2_f;
    memcpy(op1, &sum, sizeof sum);
}
compound_add_floats_as_bytes(unsigned char (&) [4], unsigned char (&) [4]):
        ldr     s0, [x0]
        ldr     s1, [x1]
        fadd    s0, s0, s1
        str     s0, [x0]
        ret

Are you willing to count on that?
Gcc 10.x for Cortex-M0

(I guess Cortex-M0 doesn't do unaligned accesses, so gcc punts and takes the easy way out. clang does a little better. They both do better with CM3 or CM4.)
(Hmm. MIPS and MSP430 both generate actual calls to memcpy() PPC and Tensilica have the alignment-fixign inline code like M0 clang)

Be careful of operator priority. It is not what it should be.

2 Likes

You're probably not going to do this conversion without any context. In an artificial function like this, without knowing the alignment, performance is always going to suffer, but this is not the result of using memcpy, it's because the compilers cannot make the necessary assumptions to optimize the code effectively.

When the compiler has the necessary information, it optimizes it correctly:

float bytes_to_float(const unsigned char (&bytes)[4]) {
    float f;
    static_assert(sizeof f == sizeof bytes, "Unexpected float size");
    memcpy(&f, std::assume_aligned<alignof f>(bytes), sizeof f);
    return f;
}

bytes_to_float(unsigned char (&) [4]):
        ldr     r0, [r0]
        bx      lr

So if you compare the memcpy approach to the union approach in the same context (i.e. where alignment etc. is known), you'll see the same performance. But the former is valid, whereas the latter is not.