Anonyme Strukturen in Unions mit C/C++

Unter dem sperrigen Begriff der Verbundvariablen stellt C/C++ struct und union zur Verfügung. Die so erzeugten Datentypen und Variablen bekommen normalerweise einen Namen. Das ist aber nicht immer erwünscht...

In C/C++ kann man Datentypen in einem neuen Datentyp zusammenfassen. Diese in Deutsch "Verbundvariablen" genannten Typen haben in der Regel einen Namen. Während dies in C++ bereits der Name des Datentyps ist, muss man im guten alten C noch explizit mit typedef einen Datentyp definieren. Soviel als Erinnerungsstütze.

// without typedef
struct sample_data { ... };
struct sample_data variable;

// and with typedef
typedef struct sample_data sample_data_t;
sample_data_t variable;

Die Sache mit dem Bit und dem Register

Im Embedded Bereich kommt es häufig vor, dass man die Register von Bausteinen in Datentypen zusammenfasst. Diese Register werden zwar byteweise gelesen, aber ihr Inhalt ist in der Regel bitweise zu interpretieren. Dies beschreibt dann auch wunderbar das Anwendungsgebiet.

Nutzt man zum Lesen eine Byte-Variable, dann hat man nachher mit Bitmasken und Schiebeoperationen zu kämpfen. Also warum also nicht diese unschöne Tipparbeit sparen und die Features des C-Compilers nutzen. Das Rezept ist eine schöne union mit dem Byte-Register, gewürzt mit einem Bitfeld in einer struct. Meist endet das in einer Konstruktion wie der folgenden.

union register_data_t
{
    uint8_t raw;
    struct register_data_structure
    {
        uint8_t bit1:1;
        uint8_t bit2:1;
        uint8_t group1:2;
        uint8_t group2:3;
    } bits;
};

Im Datentyp register_data_t wird ein Byte raw von einem Bitfeld bits überlagert. Zur Erinnerung, durch union werden alle dort zusammengefassten Elemente im gleichen Speicher abgelegt. Die Variablen "überlagern" sich quasi.

union register_data_t reg;
reg.raw = read_register(...);
if ( reg.bits.bit2 == 1 ) ...

Wie das Beispiel zeigt, kann man jetzt mittels reg.raw auf das Byte und mit reg.bits.bit1 auf die Bitstruktur zugreifen. Dem geübten Leser fällt gleich auf, dass man bei dem Zugriff auf die Bitstruktur leider einen Variablennamen mehr eingeben muss. Das ist weder schön, noch muss es sein. Die Lösung sind anonyme Strukturen.

Da man immer auf das Bitfeld und niemals auf die Verbundvariable zugreifen will, ist der Name bits für die Verbundvariable überflüssig. Und tatsächlich erlaubt uns C/C++ diesen Namen einfach wegzulassen. Da der Name der Verbundvariablen register_data_structure ebenfalls nirgendwo Verwendung findet, kann auch er eingespart werden. Wie das am Ende aussehen kann, zeigt das nachfolgende Beispiel für final_data_byte_t.

union final_data_byte_t
{
    uint8_t raw;
    struct                    // without name for the structure
    {
        uint8_t bit1:1;
        uint8_t bit2:1;
        uint8_t group1:2;
        uint8_t group2:3;
        uint8_t rest:1;
    };                        // without variable name
};

In diesem Fall haben wir den Namen der Variablen bits einfach weggelassen. Dadurch kann man die Zugriffe auf das Bitfeld genauso kurz wie die auf das Feld raw schreiben. Aus reg.bits.bit2 wird ein einfaches reg.bit2.

union register_data_t reg;
reg.raw = read_register(...);
if ( reg.bit2 == 1 ) ...

Hinweis: Um ganz genau zu sein, muss man bei der Verwendung von anonymen Verbundvariablen beide Namen, den des Datentyps und den der Variablen, weggelassen! Macht man das nicht, meldet sich der Compiler mit einem Fehler!

Was passiert wenn es nicht passt?

Bitfelder in Verbundvariablen füllen den angegebenen Basistyp. In dem hier verwendeten Beispielen ist dies bisher immer uint8_t. Man kann allerdings in der Summe auch mehr als die 8 Bit dieses Datentyps belegen. Der Compiler wird dann aber auch sang und klanglos das Ergebnis vergrößern und ein weiteres Byte allokieren.

union final_data_byte2_t
{
    uint8_t raw;
    struct
    {
        uint8_t bit1:1;
        uint8_t bit2:1;
        uint8_t group1:2;
        uint8_t group2:3;
        uint8_t rest:3;       // 2 bits too much for a byte!
    };
};

In der Deklaration von final_data_byte2_t wird durch die Bitbreite von rest das zuvor allokierte Byte um zwei Bit "überfüllt". Das bedeutet, der Compiler nimmt sich ein weiteres Byte. Meines Wissens nach ist nicht definiert, ob rest nun auf zwei Bytes verteilt wird (ein Bit im ersten Byte und die restlichen zwei Bit auf das neue Byte), oder ob der Compiler rest komplett in das zweite Byte legt.

Um diese Unklarheiten zu umschiffen, sollte man für den Basistyp unbedingt die passende Größe (in diesem Fall unit16_t) wählen. Ein Umsetzung hierfür zeigt final_data_word_t.

union final_data_word_t
{
    uint8_t raw;              // struct is larger (uint16_t)
    struct
    {
        uint16_t bit1:1;
        uint16_t bit2:1;
        uint16_t group1:2;
        uint16_t group2:3;    // less bits used, but still uint16_t
    };
};

Das folgende Code-Schnipsel zeigt den Zugriff auf die Felder:

union final_data_byte_t a_byte;
a_byte.bit1=0;
a_byte.bit2=1;
a_byte.group1=2;
a_byte.group2=3;
printf("sizeof(final_data_byte_t)=%lu raw=0x%2.2X\n",sizeof(a_byte),a_byte.raw);

Ein komplette kompilierbares Beispiel gibt es hier als C-Source. Es macht zwar nicht viel, aber es zeigt nochmal die Schnipsel als Ganzes.

Und das was es auch schon...

Comments