2

I am trying to get a union together to map out some bit fields in a register map. The code I have is the following:

typedef union __attribute__((packed)) {
    struct {
    uint8_t     MODE:3;
    uint8_t     VSHCT:3;
    uint8_t     VBUSCT:3;
    uint8_t     AVG:3;
    uint8_t     RSVD:3;
    uint8_t     RST:1;
    };
    
    struct {
    uint8_t     lsbyte:8;
    uint8_t     msbyte:8;
    };
    uint16_t    w;
    
} CON_MAP_t;

I am initializing the fields with:

CON_MAP_t map = {
    .RST =      0,
    .RSVD =     4,
    .AVG =      0,
    .VBUSCT =   4,
    .VSHCT =    4,
    .MODE =     7
}

So far this is all fine, no compiler issues or warnings.

I expect the binary/hex representation to be 01000001_00100111 / 0x4127.

However, in the debugger I end up with a value for 'w' of: 00000100_00100111 The least significant byte is correct, but the msb(yte) is not.

I am not sure if I'm missing something fundamental here and I've just been staring at it too long, but any insight would be highly appreciated!

I am using: MPLABX v6.05 Latest XC32 Compiler

Device is a PIC32MX130F064D debugging with a PICKIT4.

awb
  • 185
  • 1
  • 7

4 Answers4

2

As noted in comments and in other posts such as Why bit endianness is an issue in bitfields?, bit-fields are so poorly defined by the C standard that it's not even funny. Put a nasty reputation compiler like MPLAB on top of that mess and you'll have a recipe for disaster.

My advise is to forget that you ever heard of bit-fields and write the code using 100% portable, standardized integer constants through macros. In this specific case there's no obvious need to type pun between word and byte - why would you ever need to byte access this since the bit-fields are all over the place?

Assuming your hardware register is named CON_MAP and the CPU is little endian (endianess matters for your bit-field code but not for my solution below) then:

#define CON_MAP (*(volatile uint16_t*)0x12345678u) // physical address

#define CON_MAP_MODE_MASK   0x3u
#define CON_MAP_MODE_BIT    0u
#define CON_MAP_MODE(val)   ( ((val) & CON_MAP_MODE_MASK) << CON_MAP_MODE_BIT )
#define CON_MAP_VSHCT_MASK  0x3u
#define CON_MAP_VSHCT_BIT   2u
#define CON_MAP_VSHCT(val)  ( ((val) & CON_MAP_VSHCT_MASK) << CON_MAP_VSHCT_BIT )
...

Usage:

CON_MAP = CON_MAP_MODE(2u)   | 
          CON_MAP_VSHCT(3u)  |
          CON_MAP_VBUSCT(0u) |
          ...                ;

The values 2u, 3u etc should ideally be replaced by named constants in case you have some meaningful names for them. Like: CON_MAP_MODE(FANCY_MODE | SPECIAL_MODE).

The above is one of several common industry standard ways to implement hardware registers in embedded systems. More info here: How to access a hardware register from firmware?

Lundin
  • 195,001
  • 40
  • 254
  • 396
1

It's implementation-defined whether bit-fields can span across the boundary of the datatype on which they're defined. There are various other details too. See following excerpts from the reference:

The following properties of bit-fields are implementation-defined:

  • Whether types other than int, signed int, unsigned int, and _Bool (since C99) are permitted
  • Whether a bit-field can straddle an allocation unit boundary
  • The order of bit-fields within an allocation unit (on some platforms, bit-fields are packed left-to-right, on others right-to-left)

In this case, you have an 8-bit datatype into which you're trying to pack 3 groups of 3 bits. The third group straddles a boundary and it seems your compiler implementation does not support that. It looks like the value has been moved to the next byte, which probably means sizeof(CON_MAP_t) is greater than 2.

So at the very least, change your types used in the first struct to uint16_t, but be aware that support for this is also implementation-defined (as per excerpts shown earlier).

As a side-note, there's no need to define bit-fields in that second struct (where you've specified 8 bits for each uint8_t).

typedef union __attribute__((packed)) {
    struct {
        uint16_t    MODE    : 3;
        uint16_t    VSHCT   : 3;
        uint16_t    VBUSCT  : 3;
        uint16_t    AVG     : 3;
        uint16_t    RSVD    : 3;
        uint16_t    RST     : 1;
    };
    
    struct {
        uint8_t     lsbyte;
        uint8_t     msbyte;
    };

    uint16_t    w;
} CON_MAP_t;

It should be noted that using bit-fields for this kind of thing is heavily platform- and compiler-dependent. And using them for type-punning violates strict aliasing.

For any kind of portability, you should look to other solutions. And even targeting one specific platform, it's wise to at least create plenty of unit tests for this structure to ensure sanity.

paddy
  • 60,864
  • 6
  • 61
  • 103
  • Err... these are hardware registers. They do not magically change the size just because you change the variable type. "And using them for type-punning violates strict aliasing." Not really, type punning with unions is ok in C. – Lundin May 11 '23 at 06:32
  • To clarify, what C means with "straddling a storage unit" does not necessarily refer to the variable type `uint8_t`. C rather defines an abstract term "addressable storage unit" which is some type with platform-dependent size that the CPU is able to address, most likely corresponding to CPU alignment - in this case a 32 bit chunk. – Lundin May 11 '23 at 06:53
  • The actual C standard says (6.7.2.1), emphasis mine: "An implementation may allocate any addressable storage unit large enough to hold a bitfield. **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.**" So this is very likely not the problem here, as an addressable storage unit on the OP's system is likely 32 bits and within that, bit-fields of the same type are guaranteed to be adjacent. – Lundin May 11 '23 at 06:54
1

When it comes to using compilers that supports some additional features over standard C compilers, like XC32 Compiler, it always is better to refer to its guides and manuals for special cases like this one.

The XC32 does fully support bit fields in structures and also guarantee the order as the first defined bit as to be the Least Significant bit. Here is how it described in section 8.6.2 Bit Fields in Structures of XC32 C Compiler User's Guide:

MPLAB XC32 C/C++ Compiler fully supports bit fields in structures. Bit fields are always allocated within 8-bit storage units, even though it is usual to use the type unsigned int in the definition. Storage units are aligned on a 32-bit boundary, although this can be changed using the packed attribute.

The first bit defined will be the Least Significant bit of the word in which it will be stored. When a bit field is declared, it is allocated within the current 8-bit unit if it will fit; otherwise, a new byte is allocated within the structure. Bit fields can never cross the boundary between 8-bit allocation units.

For example, the declaration:

struct {
     unsigned lo    : 1;
     unsigned dummy : 6;
     unsigned hi    : 1;
} foo;

will produce a structure occupying 1 byte.

So according to the guide description you should see the same order as per your bit definition.

Also note that packed attribute is meant to use only if you want to alter the 32-bit boundary. But it is not necessary since yours is only 16-bits.

Here is a demo showing the expected result following with the code:

enter image description here

Demo code shown in the screenshot:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

typedef union  {
    struct {
        unsigned     MODE:3;
        unsigned     VSHCT:3;
        unsigned     VBUSCT:3;
        unsigned     AVG:3;
        unsigned     RSVD:3;
        unsigned     RST:1;
    };
    
    struct {
        uint8_t     lsbyte:8;
        uint8_t     msbyte:8;
    };
    uint16_t    w;
    
} CON_MAP_t;

int main(int argc, char** argv) {
    
    CON_MAP_t map = {
        .RST =      0,
        .RSVD =     4,
        .AVG =      0,
        .VBUSCT =   4,
        .VSHCT =    4,
        .MODE =     7
    };
    
    if(map.lsbyte == 0x27 && map.msbyte == 0x41)
        return (EXIT_SUCCESS);
    else
        return (EXIT_FAILURE);
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Kozmotronik
  • 2,080
  • 3
  • 10
  • 25
  • 1
    _Bit fields can never cross the boundary between 8-bit allocation units._ This let me wonder about the problem of his shifted bits. – kesselhaus May 11 '23 at 22:47
0

I think I may have figured this out by doing the last thing I could think of. I changed from the macro type definitions to standard "int" and also added a bit count to the "w" field:

typedef union __attribute__((packed)) {
    struct {
    int     MODE:3;
    int     VSHCT:3;
    int     VBUSCT:3;
    int     AVG:3;
    int     RSVD:3;
    int     RST:1;
    };
    
    struct {
    int     lsbyte:8;
    int     msbyte:8;
    };
    int w:16;
    
} CON_MAP_t;

This seems to have fixed the problem in the hardware implementation.

Let me know if there's any other fundamental understanding here that I may have missed.

Thanks!

awb
  • 185
  • 1
  • 7
  • 3
    This is overkill, and also breaks the unsigned condition. It is true that the size of the integer type is controlling the packing, but you have made two unnecessary changes here: (1) You changed the field types from unsigned to signed, which you clearly don't want, and (2) You needlessly forced the size all the way up to `int`. All you need for this is `uint16_t` for all of the bit field integer types. That will cause all of the bit fields to be packed into 16-bit integers, and to be unsigned. – Tom Karzes May 11 '23 at 04:51
  • `int RST:1;` It is not defined whether a signed bitfield of length 1 can hold value `0,1` or `0,-1`. – Gerhardh May 11 '23 at 06:29
  • It is however true that C makes no guarantee for how/if bit fields of types `uint8_t/unsigned char` or `uint16_t/unsigned short` will work. Nor does it guarantee the bit order. Nor does it guarantee anything about endianess. Just don't use bit-fields. – Lundin May 11 '23 at 06:30
  • I'm afraid you're wrong when it comes to the XC compilers @Lundin. XC compilers do guarantee the bit order. – Kozmotronik May 11 '23 at 18:35
  • @Kozmotronik if you need to specify a specific compiler, that is basically what it means when "the standard does not guarantee" something. That detail is implementation defined and not portable. – Gerhardh May 12 '23 at 06:13
  • @Gerhardh The portability is not a concern for this matter. This why this post has also platform and architecture specific tags like `pic32` and `mplab` etc., doesn't it? When the OP starts having some portability problems he would ask for it apart. Writing a portable code is a good practice ofcourse, however when it comes to different architectures that has different control registers, it becomes a non-portable inherently. This is what happens in low level C programming. – Kozmotronik May 13 '23 at 07:27
  • @Gerhardh Just have a look at [FreeRTOS](https://github.com/FreeRTOS/FreeRTOS-Kernel/tree/main/portable)'s software architecture; how they handle architectural differences. They have created a port for each different platform and architecture. Because even a base concept that is crucial for an RTOS like context-switching, has to be implemented depending on the architecture hence context-switching IS NOT PORTABLE as you see. – Kozmotronik May 13 '23 at 07:32
  • @Kozmotronik I have no idea what point you want to make. All you write exactly supports the statement that this is not guaranteed by the standard. It may be defined that way by the specific compiler in use but that is not a contradiction. – Gerhardh May 13 '23 at 08:47