Is there a difference in how bitfields behave depending on the underlying type width?

202 Views Asked by At

I am defining some registers (they're all 32-bits wide) to interface with a hardware peripheral using bitfields. At first I defined everything by mixing uint8_t, uint16_t, and uint32_t types. For example:

... 
// More definitions above
union  {
    volatile uint32_t I2C_REVNB_HI;

    volatile struct {
        uint16_t FUNC   : 12;
        uint8_t RSVD0   : 2;
        uint8_t SCHEME  : 2;
        uint16_t RSVD1  : 16;
    } I2C_REVNB_HI_bits;
}; // 0x04 - 0x08
...
// More definitions below

When a bitfield is bits <= 8 I assign it to a uint8_t, and similarly when 8 < bits <= 16 I assigned it to a uint16_t and 16 < bits <= 32 to a uint32_t . This looked a bit messy to me however and I refactored everything to be uint32_t regardless of the bit width since I assumed it would not matter:

... 
// More definitions above
union  {
    volatile uint32_t I2C_REVNB_HI;

    volatile struct {
        uint32_t FUNC    : 12;
        uint32_t RSVD0   : 2;
        uint32_t SCHEME  : 2;
        uint32_t RSVD1   : 16;
    } I2C_REVNB_HI_bits;
}; // 0x04 - 0x08
...
// More definitions below

I tried running my code with both implementations of the register definitions, but the one where everything is uint32_t seems to not work; the peripheral is not behaving as expected. So my question is, does anything happen under the hood that is different between the two implementations? I assumed there would be no difference since all bit fields are the same length as before. I could not really find anything online about this.

PS. The code I am writing is to run on one of the Programmable Real-time Units of the AM335X processor, to interface with the I2C peripheral and is compiled using the clpru compiler.

Some notes from the clpru compiler document on bit-fields (p108) that may or may not be useful:

  • "Bit fields are handled using the declared type"
  • "Bit fields declared volatile are accessed according to the bit field's declared type. A volatile bit field reference generates exactly one reference to its storage; multiple volatile bit field accesses are not merged"
  • "The size of a struct containing the bit field depends on the declared type of the bit field. For example, both of these structs have a size of 4 bytes:"
    struct st {int a:4};
    struct st {char a:4; int :22;};

And on (p69) of the compiler manual:

"In addition to _Bool, signed int, and unsigned int, the compiler allows char, signed char, unsigned char, signed short, unsigned shot, signed long, unsigned long, signed long long, unsigned long long, and enum types as bit-field types."

Update 1

I tried to check if there was any difference assigning to the fields as suggested by @IanAbbot:

i2c_refactored.I2C_REVNB_HI = 0;
i2c_refactored.I2C_REVNB_HI_bits.SCHEME = ~0;
i2c_refactored.I2C_REVNB_HI_bits.FUNC   = ~0;
DEBUG_MEMORY_0.status = i2c_refactored.I2C_REVNB_HI;

i2c_original.I2C_REVNB_HI = 0;
i2c_original.I2C_REVNB_HI_bits.SCHEME = ~0;
i2c_original.I2C_REVNB_HI_bits.FUNC   = ~0;
DEBUG_MEMORY_1.status = i2c_original.I2C_REVNB;

But the outputs are the same: 0x0000CFFF (or 0b00000000000000001100111111111111)

Update 2

After trying to set all fields to unsigned int instead of uint32_t, the faulty behaviour still occurs

Update 3

I made the following simple testcase:

int main(void){

    volatile pru_I2C_tmp i2c_refactored = {0};
    volatile pru_I2C i2c_working = {0};

    i2c_refactored.I2C_REVNB_HI = 0;
    i2c_refactored.I2C_REVNB_HI_bits.SCHEME = ~0;
    i2c_refactored.I2C_REVNB_HI_bits.FUNC   = ~0;
    DEBUG_MEM0.status = i2c_refactored.I2C_REVNB_HI;

    i2c_working.I2C_REVNB_HI = 0;
    i2c_working.I2C_REVNB_HI_bits.SCHEME = ~0;
    i2c_working.I2C_REVNB_HI_bits.FUNC   = ~0;
    DEBUG_MEM1.status = i2c_working.I2C_REVNB_HI;

    __halt();
    return 0;
}

With the corresponding ASM (manual link):

[0x0000] 0x240000c0    LDI R0.w2, 0x0000
[0x0001] 0x24080080    LDI R0.w0, 0x0800
[0x0002] 0x0504e0e2    SUB R2, R0, 0x04
[0x0003] 0x2eff818e    UNKNOWN-F2
[0x0004] 0x230007c3    JAL R3.w2, 0x0007
[0x0005] 0x240001ee    LDI R14, 0x0001
[0x0006] 0x230040c3    JAL R3.w2, 0x0040
[0x0007] 0x05ffe2e2    SUB R2, R2, 0xff
[0x0008] 0x240800ef    LDI R15, 0x0800
[0x0009] 0x2400d8f0    LDI R16, 0x00d8
[0x000a] 0xe1fd02c3    SBBO R3.b2, R2, 253, 2
[0x000b] 0x05b3e2e2    SUB R2, R2, 0xb3
[0x000c] 0x0100e2ee    ADD R14, R2, 0x00
[0x000d] 0x230033c3    JAL R3.w2, 0x0033
[0x000e] 0x2408d8ef    LDI R15, 0x08d8
[0x000f] 0x2400d8f0    LDI R16, 0x00d8
[0x0010] 0x01d8e2ee    ADD R14, R2, 0xd8
[0x0011] 0x230033c3    JAL R3.w2, 0x0033
[0x0012] 0x240000e0    LDI R0, 0x0000
[0x0013] 0x24c000e1    LDI R1, 0xc000
[0x0014] 0xe1042280    SBBO R0, R2, 4, 4
[0x0015] 0x0104e2e0    ADD R0, R2, 0x04
[0x0016] 0xf100208e    LBBO R14, R0, 0, 4
[0x0017] 0x12e1eee1    OR R1, R14, R1
[0x0018] 0xe1002081    SBBO R1, R0, 0, 4
[0x0019] 0x240fffe1    LDI R1, 0x0fff
[0x001a] 0xf100208e    LBBO R14, R0, 0, 4
[0x001b] 0x12e1eee1    OR R1, R14, R1
[0x001c] 0x2eff818e    UNKNOWN-F2
[0x001d] 0xe1002081    SBBO R1, R0, 0, 4
[0x001e] 0x240001c1    LDI R1.w2, 0x0001
[0x001f] 0x24000081    LDI R1.w0, 0x0000
[0x0020] 0xf1042280    LBBO R0, R2, 4, 4
[0x0021] 0xe1002180    SBBO R0, R1, 0, 4
[0x0022] 0x240001c1    LDI R1.w2, 0x0001
[0x0023] 0x24024881    LDI R1.w0, 0x0248
[0x0024] 0xe1dc228e    SBBO R14, R2, 220, 4
[0x0025] 0xf1dd0200    LBBO R0, R2, 221, 1
[0x0026] 0x13c00000    OR R0.b0, R0.b0, 0xc0
[0x0027] 0xe1dd0200    SBBO R0, R2, 221, 1
[0x0028] 0x240fff80    LDI R0.w0, 0x0fff
[0x0029] 0xf1dc02c0    LBBO R0.b2, R2, 220, 2
[0x002a] 0x1280c080    OR R0.w0, R0.w2, R0.w0
[0x002b] 0xe1dc0280    SBBO R0, R2, 220, 2
[0x002c] 0xf1dc2280    LBBO R0, R2, 220, 4
[0x002d] 0xe1002180    SBBO R0, R1, 0, 4
[0x002e] 0x2a000000 >> HALT 
[0x002f] 0x01b3e2e2    ADD R2, R2, 0xb3
[0x0030] 0xf1fd02c3    LBBO R3.b2, R2, 253, 2
[0x0031] 0x01ffe2e2    ADD R2, R2, 0xff
[0x0032] 0x20c30000    JMP R3.w2
[0x0033] 0x5100f00c    QBEQ 12, R16, 0
[0x0034] 0x10eeeef1    AND R17, R14, R14
[0x0035] 0x24003000    LDI R0.b0, 0x0030
[0x0036] 0x70f00002    QBGE 2, R0.b0, R16
[0x0037] 0x10f0f000    AND R0.b0, R16, R16
[0x0038] 0x0400f0f0    SUB R16, R16, R0.b0
[0x0039] 0xff00cf12    LBBO R18, R15, 0, b0
[0x003a] 0xef00d112    SBBO R18, R17, 0, b0
[0x003b] 0x5100f004    QBEQ 4, R16, 0
[0x003c] 0x0000efef    ADD R15, R15, R0.b0
[0x003d] 0x0000f1f1    ADD R17, R17, R0.b0
[0x003e] 0x21003500    JMP 0x0035
[0x003f] 0x20c30000    JMP R3.w2
[0x0040] 0x230042c3    JAL R3.w2, 0x0042
[0x0041] 0x21004100    JMP 0x0041
[0x0042] 0x10000000    AND R0.b0, R0.b0, R0.b0
[0x0043] 0x20c30000    JMP R3.w2

DEBUG_MEM0 is at address 0x10000 and DEBUG_MEM1 at address 0x10248. The statements seem logical, albeit that there are some statements I don't fully understand. (R2 is the stack pointer and R14 the return register). Does anyone see anything that could hint to why it is not functioning?

6

There are 6 best solutions below

0
Eric Postpischil On

The C standard does not specify how an implementation lays out bit-fields in storage units. C 2018 6.7.2.1 11 says (emphasis added):

An implementation may allocate any addressable storage unit large enough to hold a bit-field…

So a C compiler is free to allocate one, two, or more bytes for a 2-bit bit-field, it is free to allocate two or more bytes for a 12-bit bit-field (assuming eight-bit bytes), and so on. It may base its decision based on the nominal type of the bit-field.

About the only constraint the standard imposes on selecting storage units is, also in 6.7.2.1 11:

… If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit…

2
chux - Reinstate Monica On

does anything happen under the hood that is different between the two implementations?

Not withstanding OP's select compiler documentation: "In addition to _Bool, signed int, and unsigned int, the compiler allows char, signed char, unsigned char, signed short, ...", concerning bit fields, uint16_t, uint8_t, char, uint32_t and many others types are not always well defined per the C spec.

Portable code that does not rely on implementation defined behavior should only use bool, signed int, unsigned int.

A bit-field shall have a type that is a qualified or unqualified version of _Bool, signed int, unsigned int, or some other implementation-defined type. It is implementation-defined whether atomic types are permitted. C11dr §6.7.2.1 5

... except that for bitfields, it is implementation-defined whether the specifier int designates the same type as signed int or the same type as unsigned int. C11dr § 6.7.2 5


C2X may change things.

A bit-field shall have a type that is a qualified or unqualified bool, signed int, unsigned int, a bit-precise integer type, or other implementation-defined type. It is implementation-defined whether atomic types are permitted. C23_n3096 § 6.7.2.1 5

This does not certainly include char.

3
nielsen On

As others have commented, the behavior of bit fields in this case is not fully defined by the standard.

Without more information, it is difficult to say anything for sure, but this is a guess:

According to the OP information, in both cases, the struct is 32 bits and the bit layout seems to be identical. Suppose that e.g. the FUNC bits are written:

   bits.FUNC = 0x5;

The compiler must then generate code which reads at least 12 bits at the memory location corresponding to bits.FUNC, modifies the 12 bits corresponding to FUNC, and writes the result back. It seems that if the FUNC field is declared as uint16_t, then the compiler will do this via a 16-bit read/write, i.e. write back not only the FUNC-bits, but also the RSVD0 and SCHEME bits. If the field is declared uint32_t, then it will do it via a 32-bit read/write which will also cover the RSVD1 bits.

See this example for ARM showing exactly the described behavior:

        movs    r2, #5
  // Assignment to FUNC when declared uint16_t:
        ldrh    r3, [sp]
        bfi     r3, r2, #0, #12
        strh    r3, [sp]        @ movhi
  // Assignment to FUNC when declared uint32_t:
        ldr     r3, [sp, #4]
        bfi     r3, r2, #0, #12
        str     r3, [sp, #4]

Depending on the peripheral properties, this implicit write of the RSVD1 bits could cause the unexpected behavior.

1
Jonathan Leffler On

Given the original struct within the union:

        uint16_t FUNC   : 12;
        uint8_t RSVD0   : 2;
        uint8_t SCHEME  : 2;
        uint16_t RSVD1  : 16;

I suspect the compiler is working along the lines of:

  • The first 12 bits fit into a uint16_t — no problem.
  • The next 2 bits are in a different data type; start a new storage unit (the same size as the new type — uint8_t).
  • The next 2 bits are of the same type and fit into that storage unit.
  • The last 16 bits are in a different type again (and wouldn't fit into a storage unit the size of a uint8_t anyway); start a new storage unit (the same size as the new type — uint16_t). Further, because of the alignment requirement for uint16_t, add a padding byte after the prior storage unit.

If you use uint16_t for all the bit-fields, or if you use uint32_t for them all, you will probably get the result you want.

What I describe is, I believe, permitted by the standard. The standard barely mandates anything for bit-fields, though. Basically, all uses of bit-fields are subject to the whims of the 'implementation' — meaning, subject to the whims of the compiler. For details, see (C11) §6.7.2.1 Structure and union specifiers, especially paragraphs 4, 5, 9-12.

One more issue to worry about is whether the first bit-field is in the most significant bits or the least significant bits. Have you verified what is used on your platform? Again, the standard does not specify how the bits are laid out, whether that's most to least significant or vice versa, nor where padding bits are placed.

2
John Bollinger On

I am defining some registers (they're all 32-bits wide) to interface with a hardware peripheral using bitfields.

This is arguably a mistake. If you need to pack several fields into an integer then shifts and bitmasks are the portable way to do it. That's likely to also be how your compiler does it under the hood when you use bitfields, but if you do it yourself then you can control the layout details directly.

Is there a difference in how bitfields behave depending on the underlying type width?

The C language allows implementations considerable latitude in how bitfields are laid out within a struct, and that layout is exposed in your case via the other member of your union. It is well within the spec for an implementation to take the declared type of bitfield members into account in laying the containing structure.

However, I do not see a plausible explanation for the observed difference that is consistent with the asserted facts:

  • the union containing the integer and the bitfield structure is exactly the same size (32 bits) as its uint32_t member
  • in aggregate, the bitfields require the full size of the union
  • the specific widths of the first three structure members will require them to be stored in the same ASU (assuming a machine with 8-bit bytes) to comply with C's layout rules
  • experiment shows these to be laid out in the space corresponding to the two least-significant bytes of the integer, in the same relative positions in both cases
  • the compiler is the PRU Optimizing C/C++ Compiler v2.3

... unless something else has changed, or some other code not presented in the question is involved. In particular, I don't see any way that the compiler specified, operating as documented and observed on the code presented, is mapping bits of the bitfields to bits of the integer differently in one case than in the other.

The most likely explanation, then, is that some other code consuming the affected objects is sensitive to the types of the bitfield members. For example, it may be relevant that with the original code, if bitfield I2C_REVNB_HI_bits.SCHEME is subjected to the standard integer promotions then the resulting value has type int, whereas with the revised code, such a promotion will leave the type unchanged (uint32_t, which is unsigned int for this compiler).

0
Luis Colorado On

Storage of bitfields is some field in which compiler writers are given full freedom of implementation (this happens since K&R specification) and is the less portable feature of the C language. If you want to use bitfields and be portable, just use int or unsigned as the type in the field, to create unsidned or signed bitfields. The compiler will arrange them in an optimum way, but you will have poor and nonportable way of knowing how they are internally implemented, but if you are tied already to a compiler, there are good news. Almost every compiler documents how they do the packing of bitfields into the structure, so you can follow that and use it to develop. Just remember, that will be non-portable. If you want a portable solution, dont use bitfields.