Search code examples
c++structcpu-registersunionsbit-fields

problem representing a register with unions and bit fields


I'm writing a NES emulator in C++, and I bumped into an issue using bit fields to represent a register, which caused a very nasty bug. I'm representing an internal address register as:

union
    {
        struct
        {
            uint16_t coarseX : 5;            // bit field type is uint16_t, same as reg type
            uint16_t coarseY : 5;
            uint16_t baseNametableAddressX : 1;
            uint16_t baseNametableAddressY : 1;
            uint16_t fineY : 3;
            uint16_t unused : 1;
        } bits;
        uint16_t reg;
    } addressT, addressV;   // temporary VRAM adddress register and VRAM address register

so I can access the single bit-fields, and the register as a whole.

Initially I wrote the register as:

union
    {
        struct
        {
            uint8_t coarseX : 5;             // bit field type is uint8_t, reg type is uint16_t
            uint8_t coarseY : 5;
            uint8_t baseNametableAddressX : 1;
            uint8_t baseNametableAddressY : 1;
            uint8_t fineY : 3;
            uint8_t unused : 1;
        } bits;
        uint16_t reg;
    } addressT, addressV;   // temporary VRAM adddress register and VRAM address register

The bug was caused by the bit field behaviour when the type of the bit field (for example coarseX) is different from that of the register (reg). In this case, when I increment a field (i.e. coarseX++), the reg member was updated "incorrectly", meaning that the bit pattern inside reg didn't reflect the pattern represented by the bit fields (or by the bit fields as I layed out them inside the struct). I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, but why the behaviour changes when I change the type of the bit field?

Can someone please explain why?


Solution

  • You said it yourself:

    I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, ...

    That is exactly what is happening.

    uint8_t has 8 bits in it. The 1st two fields in your struct, coarseX and coarseY, having 5 bits each, can't fit consecutively within a single byte in memory. The compiler stores coarseX in the 1st byte, and then has to push coarseY to a 2nd byte in memory, leaving 3 unused bits in memory between coarseX and coarseY that offset your values in the register.

    The next 3 fields, coarseY, baseNametableAddressX and baseNametableAddressY, total 7 bits, so they fit within that 2nd byte.

    But that byte can't hold the fineY and unused fields, so they get pushed to a 3rd byte in memory, leaving 1 unused bit in memory between baseNametableAddressY and fineY that offset your values in the register. And the register can't access that 3rd byte!

    So, effectively, your struct ends up acting as if you had declared it like this instead:

        union
        {
            struct
            {
                // byte 1
                uint8_t coarseX : 5;
                uint8_t padding1 : 3;
    
                // byte 2
                uint8_t coarseY : 5;
                uint8_t baseNametableAddressX : 1;
                uint8_t baseNametableAddressY : 1;
                uint8_t padding2 : 1;
    
                // byte 3!
                uint8_t fineY : 3;
                uint8_t unused : 1;
                uint8_t padding3 : 4;
            } bits;
            struct {
                uint16_t reg; // <-- 2 bytes!
                uint8_t padding4; // <-- ! 
            }
        } addressT, addressV;   // temporary 
    

    Using uint16_t instead of uint8_t, you don't run into that problem with extra paying being added, since there are enough bits allocated for the register to hold all of the bits you are defining.