Einleitung
Datenkomprimierung und Syntax
Zugriff auf Bitfeld-Elemente
Zugriff auf Peripherie über Bitfelder
Besonderheiten von Bitfeldern
Wurden bisher immer nur Daten verarbeitet die mindestens 1 Byte belegten, so erfahren Sie jetzt, wie Sie auf die einzelnen Bits eines Datums zugreifen können. Zu Beginn des Kurses haben Sie zwar schon die Bitoperationen kennen gelernt, über die einzelne Bits manipulieren können. Doch es geht auch eleganter. Bevor Sie jetzt weiter machen, können Sie sich die Bitoperationen hier nochmals ansehen.
Um auf einzelne Bits zuzugreifen, stehen so genannte Bitfelder zur Verfügung. Bitfelder werden hauptsächlich bei folgenden Anforderungen eingesetzt:
Sehen wir uns zunächst den ersten Fall an, der Einsparung von Speicherplatz. Als Ausgangspunkt sollen 6 Variablen zum Abspeichern eines Zeitpunkts, bestehend aus Datum und Uhrzeit, definiert werden. Diese Variablen benötigt bei den angegebenen Definitionen auf einem PC etwa 6 Bytes (4 x 1 Byte und 1 x 2 Byte).
| unsigned char minute; unsigned char hour; unsigned char day; unsigned char month; unsigned short year; |
Und diesen benötigten Speicherplatz gilt es nun zu optimieren. Dazu wird zuerst der Wertebereich der einzelnen Elemente bestimmt. Ist der Wertebereich bekannt, so kann daraus die Anzahl der benötigten Bits pro Element berechnet werden. Die nachfolgende Tabelle zeigt den Wertebereich pro Variable so wie die dafür mindestens benötigten Bits.
| Variable | Wertebereich | Anzahl Bits |
| minute | 0..59 (<64 = 26) | 6 |
| hour | 0..23 (<32 = 25) | 5 |
| day | 1..31 (<32 = 25) | 5 |
| month | 1..12 (<16 = 24) | 4 |
| year | 0..2047 (<2048 = 211) | 11 |
Damit werden für die (nicht redundante) Speicherung der Informationen 31 Bits, gleich 4 Bytes benötigt. Die restlichen 2 Bytes der bisherigen Variablen sind 'verschenkter' Speicherplatz.
Um diese Daten jetzt komprimiert im Speicher abzulegen, wird ein Bitfeld eingesetzt. Die Syntax einer Bitfeldanweisung ist nachfolgend dargestellt.
| struct [Name] { DATEMTYP1 Element1: Bitzahl; DATENTYP2 Element2: Bitzahl; ... } [bitVar1,...]; |
Eingeleitet wird ein Bitfeld durch das Schlüsselwort struct. Die einzelnen Elemente werden dann innerhalb einer geschweiften Klammer aufgelistet.
Zusätzlich wird nach jedem Elementnamen ein Doppelpunkt angegeben und danach die Anzahl der Bits, die das Element belegt. Als Datentypen für die Elemente sind nur die Datentypen bool, char, short, int und long (sowohl signed als auch unsigned) zugelassen. Fast selbstverständlich ist, dass die Anzahl der Bits nicht die Anzahl der Bits des jeweiligen Datentyps übersteigen darf. So können in einem short-Element maximal sizeof(short)*BitsProByte (in der Regel sind dies dann 16 Bits) abgelegt werden.
Mit dem nun erworbenen Wissen kann das Bitfeld Time wie unten angegeben komprimiert im Speicher abgelegt werden. Das nachfolgende Bild zeigt wie die einzelnen Elemente im Speicher liegen könnten:
|
struct Time { unsigned char minute: 6; unsigned char hour : 5; unsigned char day : 5; unsigned char month : 4; unsigned short year : 11; }; |

Ein anderer (vom Speicherverbrauch sehr effizienter) Anwendungsfall ist, so genannte Flags (Flaggen, Zustandsmerker) über ein Bitfeld zu realisieren. Flags können nur die beiden Zustände gesetzt oder nicht gesetzt annehmen. Hierfür werden in der Regel bool Variablen verwendet, die ja die äquivalenten Zustände true und false annehmen können. Ein solches bool Datum belegt bei den meisten Compiler 1 Byte (herauszufinden mit der sizeof(bool) Anweisung). Werden nun z.B. 8 solche Flags benötigt, so belegen diese dann auch 8 Bytes. Fassen Sie aber die Flags, wie nachfolgend angegeben, in einem Bitfeld zusammen, so werden für die 8 Flags nur noch 8 Bit (in der Regel gleich 1 Byte) benötigt. Wie gesagt, dies ist eine Optimierung bezüglich des Speicherverbrauchs, nicht aber der Ablaufgeschwindigkeit des Programms.
|
struct Flags { bool flag1: 1; ... bool flag8: 1; } anyFlags; |
|
|
Der Zugriff auf die Elemente in einem Bitfeld erfolgt in der Weise, dass zuerst der Name des Bitfeldvariablen, dann der Punktoperator und zum Schluss der Name des Bitfeldelements angegeben wird. Wird der Wert eines Bitfeldelements ausgelesen, so wird er immer beginnend ab dem niederwertigsten Bit in der Zielvariablen abgelegt. Beim übertragen eines Werts in ein Bitfeldelement müssen Sie beachten, dass überzählige Bits einfach abgeschnitten werden. Weisen Sie z.B. einem Bitfeldelement mit der Länge 4 Bits den Wert 0x1F (5 Bits!) zu, so wird im Element nur der Wert 0x0F (die niederwertigen 4 Bits) abgelegt.
|
// Bitfeld-Definition struct Time { unsigned char minute: 6; unsigned char hour : 5; unsigned char day : 5; unsigned char month : 4; unsigned short year : 11; } myTime; // Zugriffe auf Bitfeldelemente mytime.minute = 23; unsigned short year = myTime.year; |
So, genug mit Speicherplatz gegeizt. Sehen wir uns jetzt an, wie Bitfelder für den Zugriff auf Peripherie-Bausteinen verwendet werden können. Peripherie-Bausteine besitzen in der Regel mehrere 8, 16 oder 32 Bit breite Register. Innerhalb dieser Register werden die Funktionen des Bausteins durch einzelne Bits kontrolliert. Sehen Sie sich dazu einmal den Aufbau eines fiktiven Timer-Bausteins an. Er enthält drei 8-Bit breite Register, deren einzelne Bits verschiedene Funktionen steuern. Diesen Baustein gilt es nun softwaremäßig nachzubilden.
Aufbau eines fiktiven
Peripherie-Bausteins
|
Als erstes werden die einzelnen Bits der Register durch einsprechende Bitfelder reg0 und reg2 abgebildet. Nur das Register 1 bildet hierbei eine Ausnahme, da es volle 8 Bit belegt und deswegen direkt als unsigned char Element reg1 angelegt werden kann.
Anschließend werden die zwei Bitfelder reg0 und reg2 so wie der unsigned short Wert reg1 in einer übergeordneten Struktur Timer zusammengefasst. Strukturen werden später noch ausführlicher behandelt und dienen zum Zusammenfassen von Daten die logisch zusammengehören. Damit ist die 'Definition' des Peripherie-Bausteins komplett. Da Peripherie-Bausteine sich in der Regel auf fixen Adressen befinden, wird ein entsprechender Strukturzeiger definiert und diesem die Adresse des ersten Registers des Bausteins zugewiesen.
|
// Abbildung des Peripherie-Bausteins #define UCHAR unsigned char // Peripherie-Baustein nachbilden struct Timer { struct { bool enable : 1; bool intEnable : 1; bool intPeriod : 1; bool : 1; UCHAR preScale : 4; } reg0; UCHAR reg1; struct { bool pending : 1; UCHAR : 6; bool overf : 1; } reg2; } *pTimer = (struct Timer*)0x0100; // Zugriff auf Register des Bausteins pTimer->reg0.enable = true; error = pTimer->reg2.overf; |
|
|
Im Beispiel sind nach der Zeigerdefinition noch zwei Zugriffe auf den Peripherie-Baustein angegeben. Beachten Sie hier bitte, dass der Zeigeroperator -> nur für den Zugriff auf die Struktur über den definierten Zeiger verwendet wird. Der Zugriff auf die Elemente reg0 und reg2 erfolgt direkt über den Punktoperator. Mehr über den Zugriff auf Strukturelemente später noch.
Auf einige Besonderheiten bei Bitfelder soll zum Schluss noch hingewiesen werden: