Sizeof struct with bit fields

In a project I would like to save some data to EEPROM of a classic Nano.

While developing I encountered that the size of the below TIMEOUT struct (23 bits) is 3 bytes (which suites me well so I possibly can squeeze a bit more out) but I would have expected it to be four bytes due to the uint32_t.

Was my expectation wrong? Any explanation (I know that compilers are quite clever)?

Below a simple demo code

#include <EEPROM.h>

struct TIMEOUT
{
  uint32_t quarter:    3;
  uint32_t startTime: 10;
  uint32_t duration:  10;
};

struct TIMEOUTS
{
  uint8_t numTimeouts;
  TIMEOUT timeouts[100];
};

TIMEOUTS timeouts;

void setup()
{
  Serial.begin(115200);

  Serial.println(sizeof(timeouts));
  Serial.println(sizeof(TIMEOUT));

  //eepromClear();
  //return;

  //saveTimeouts();
  //return;

  EEPROM.get(2, timeouts);


  Serial.println("=========");
  EEPROM.get(2, timeouts);

  for (uint8_t cnt = 0; cnt < timeouts.numTimeouts; cnt++)
  {
    Serial.print(timeouts.timeouts[cnt].quarter);
    Serial.print(" > ");
    Serial.print(timeouts.timeouts[cnt].startTime);
    Serial.print(" > ");
    Serial.println(timeouts.timeouts[cnt].duration);
  }

  // proof that only 301 bytes were written
  uint8_t b;
  EEPROM.get(2 + sizeof(timeouts), b);
  Serial.println(b, HEX);
}

void loop()
{
}


void eepromClear()
{
  uint8_t b = 0xFF;

  // clear eeprom
  for (uint16_t addr = 0; addr < 1024; addr++)
  {
    EEPROM.put(addr, b);
  }

  // proof that it was cleared
  for (uint16_t addr = 0; addr < 1024; addr++)
  {
    EEPROM.get(addr, b);
    Serial.println(b, HEX);
  }
}

void saveTimeouts()
{
  for (uint8_t cnt = 0; cnt < 100; cnt++)
  {
    timeouts.timeouts[cnt].quarter = (cnt / 25) + 1;
    timeouts.timeouts[cnt].startTime += cnt * 10;

    Serial.print(timeouts.timeouts[cnt].quarter);
    Serial.print(" > ");
    Serial.print(timeouts.timeouts[cnt].startTime);
    Serial.print(" > ");
    Serial.println(timeouts.timeouts[cnt].duration);
  }
  timeouts.numTimeouts = 100;
  EEPROM.put(2, timeouts);

}

There don't seem to be issues with this code; it works as expected.

I'm going to guess that, since an 8-bit processor can efficiently access byte addresses, that the compiler is allowed to assign the smallest integral number of bytes needed to hold the structure.

Bet it would have used 4 bytes on an ARM or ESP since they likely access 32-bit addresses more efficiently. I'll check and let you know.

1 Like

Confirmed on an ESP32:

struct TIMEOUT {
  uint32_t quarter:    3;
  uint32_t startTime: 10;
  uint32_t duration:  10;
};

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.printf("Size of TIMEOUT = %lu bytes\n", sizeof(TIMEOUT));
}

void loop() {
}

Serial Output:

Size of TIMEOUT = 4 bytes
2 Likes

The datatype (uint32_t) just has to be large enough to accommodate the number of bits. For example, startTime cannot be a uint8_t.

I suspect the compiler promotes to the datatype specified (I cannot find a reference). In other words, the compiler treats quarter as a uint32_t but stores the value in 3 bits. If that's true you can save some space / code by choosing the smallest datatype that can store the number of bits.

No. But for a different reason. The TIMEOUT struct is subject to packing / padding like any other struct. Without telling the compiler you want a packed struct the compiler can pad. In the extreme case your struct could become 12 bytes in size. (At least that's my reading of the documentation.)

1 Like

Thanks everybody

I think that you are right; I continued with my project and got a warning on the sprintf line in below code

void printTimeout(uint8_t idx)
{
  char buffer[17];
  TIMEOUT to = timer.getTimeoutInfo(idx);
  sprintf(buffer, "%d>%02d:%02d    %02d:%02d", to.quarter, to.startTime / 60, to.startTime % 60, to.duration / 60, to.duration % 60);
  lcd.printText(1, buffer);
}
/home/wim/Downloads/arduino-1.8.19/portable/sketchbook/Netball/NetballTimer.0.9/fsmMain.cpp:442:132: warning: format '%d' expects argument of type 'int', but argument 3 has type 'long unsigned int' [-Wformat=]
   sprintf(buffer, "%d>%02d:%02d    %02d:%02d", to.quarter, to.startTime / 60, to.startTime % 60, to.duration / 60, to.duration % 60);

A little strange (for me) that it only complains about the quarter; after I fixed that using a cast to uint8_t no further warnings showed.

I'm aware that packing/padding exists. Just for fun, is there a flag (attribute) to tell the compiler not to pack.

1 Like

Microsoft's compiler has a lot of options. I'm not certain about GCC. Normally, a compiler packs / aligns to what works best for the target platform. In the case of an AVR processor that would essentially be "not applicable" for both (byte-aligned, byte-packing).

Let's find out...

This appears to be the GCC way. The options are packed and aligned. There is an option for "natural" alignment; do not include the argument...

struct S { short f[3]; } __attribute__ ((aligned));

It's described in the fourth paragraph.

There are Microsoft compatible pragmas that allow changing are restoring. As far as I can tell they overlap with the GCC stuff.

I suspect there are also separate rules for bitfields.

1 Like

Thanks for that. I will play with it when the need comes.

if you wanted to have the same compressed structure on an ESP32 than on an 8 bit processor, then you can play with GCC attributes packed and aligned

struct __attribute__((packed, aligned(1))) TIMEOUT {
  uint32_t quarter:    3;
  uint32_t startTime: 10;
  uint32_t duration:  10;
};

that should give you back the 3 bytes on an ESP32

Yes, I've used that attribute to get struct compatibility between ESP and AVR.