C++ Kurs

Bitfelder

Die Themen:

Wurden bisher nur Daten verarbeitet die 1 Byte oder ein Vielfaches davon belegten, so werden wir uns nun ansehen, wie auf die einzelnen Bits eines Datums zugegriffen werden kann. Weiter vorne haben wir uns zwar schon die Bitoperationen angesehen, die es ebenfalls erlauben auch einzelne Bits zu manipulieren. Doch es geht auch eleganter.

Um auf einzelne Bits zuzugreifen stehen sogenannte Bitfelder zur Verfügung. Bitfelder werden hauptsächlich in folgenden Fällen eingesetzt:

  1. Der verfügbare Speicherplatz soll so effektiv wie möglich genutzt werden. Dieser Punkt verliert aber im Zeitalter der Gigabytes zunehmend an Bedeutung. Lediglich auf Embedded Systemen (Steuerungen) ist er noch relevant, da dort RAM einen nicht unerheblichen Kostenfaktor darstellen kann.
  2. Es soll auf die Peripherie eines Controllers/Systems zugegriffen werden, z.B. auf einen Baustein für die serielle Datenübertragung. In solchen Peripheriebausteinen werden dessen Funktionen in der Regel über entsprechende Bits in Registern gesteuert.

Datenkomprimierung und Syntax

Datenkomprimierung

Sehen wir uns zunächst den ersten Fall an, der Einsparung von Speicherplatz. Zum Abspeichern eines bestimmten Zeitpunkts, bestehend aus Datum und Uhrzeit, werden 6 Variablen benötigt. Diese Variablen benötigt bei den nachfolgend 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 enthält den Wertebereich pro Element sowie 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

Somit werden für die Speicherung der Informationen 31 Bits, gleich 4 Bytes benötigt.

Syntax

Um diese Daten jetzt komprimiert im Speicher abzulegen, wird ein Bitfeld eingesetzt. Die Syntax einer Bitfeldanweisung ist folgende:

struct [NAME]
{
   DTYP1 ELEMENT1: BITS1;
   DTYP2 ELEMENT2: BITS2;
   ...
} [bitVar1,...];

Eingeleitet wird ein Bitfeld durch das Schlüsselwort struct. Die einzelnen Elemente des Bitfeldes werden dann innerhalb einer geschweiften Klammer aufgelistet.

Jedes Element des Bitfeldes besteht aus einem Datentyp, dem Elementnamen, einem Doppelpunkt und der Anzahl der Bits, die für das Element reserviert werden sollen. Als Datentypen für die Elemente sind nur Ganzzahl-Datentypen und der Datentyp bool zugelassen. Fast selbstverständlich ist, dass die Anzahl der Bits eines Elements 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.

PgmHeader
struct Time
{
   unsigned char minute: 6;
   unsigned char hour  : 5;
   unsigned char day   : 5;
   unsigned char month : 4;
   unsigned short year : 11;
};

Das nachfolgende Bild zeigt wie die einzelnen Elemente damit im Speicher liegen könnten:

Bitfeld

Das Element minute belegt hiebei die Bits 0…5 auf der Adresse 0x0100, das Element hour die Bits 6+7 auf der Adresse 0x0100 sowie die Bits 0..2 auf der Adresse 0x0101 usw. Beachten Sie, dass das erste Bit die Bitnummer '0' besitzt.

Ein anderer (vom Speicherverbrauch sehr effizienter) Anwendungsfall ist, sogenannte Flags (Flaggen, Zustandsmarker) ü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 aber 1 Byte (herauszufinden mit der sizeof(bool) Anweisung). Werden nun z.B. 8 solche Flags benötigt, so belegen diese dann auch 8 Bytes. Wenn nun diese Flags, wie nachfolgend angegeben, in einem Bitfeld zusammengefasst werden, 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 eine bezüglich der Ablaufgeschwindigkeit des Programms.

PgmHeader
struct Flags
{
   bool flag1: 1;
   ...
   bool flag8: 1;
} anyFlags;
HinweisDie im Kapitel über den Datentyp string erwähnte Standard-Bibliothek enthält schon eine auf solche Flags spezialisierte Klasse vector<bool>. Sie sehen einmal wieder, es lohnt sich unbedingt die Standard-Bibliothek später einmal anzuschauen.

Zugriff auf Bitfeld-Elemente

Der Zugriff auf die Elemente in einem Bitfeld erfolgt in der Weise, dass zuerst der Name der Bitfeldvariable, 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 Zielvariable abgelegt. Beim Schreiben eines Werts in ein Bitfeldelement muss beachtet werden, dass überzählige Bits einfach abgeschnitten werden. Wird z.B. einem Bitfeldelement mit der Länge 4 Bits der Wert 0x1F (5 Bits!) zugewiesen, so wird im Element nur der Wert 0x0F (die niederwertigen 4 Bits) abgelegt.

PgmHeader
// 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;

Zugriff auf Peripherie über Bitfelder

Sehen wir uns jetzt an, wie Bitfelder für den Zugriff auf Peripherie-Bausteine 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 gesteuert. Sehen wir uns 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 abzubilden.

Aufbau eines fiktiven Peripherie-Bausteins:

Register Bit-Nr. Funktion
0 0 0 = Baustein gesperrt 1 = Baustein freigegeben
  1 0 = Interrupt gesperrt 1 = Interrupt freigegeben
  2 0 = einmaliger Interrupt 1 = periodischer Interrupt
  3 nicht belegt
  4..7 Vorteiler
1 0..7 Zähler
2 0 0 = kein Interrupt anstehend 1 = Interrupt anstehend
  1..6 nicht belegt
  7 Überlauf

Als erstes werden die einzelnen Bits der Register durch einsprechende Bitfelder reg0 und reg2 abgebildet. Lediglich das Register 1 bildet hierbei eine Ausnahme, da es 8 Bit belegt und deswegen direkt als unsigned char Element reg1 angelegt werden kann.

Anschließend werden die zwei Bitfelder reg0 und reg2 sowie das unsigned short Datum 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. Und da Peripherie-Bausteine sich in der Regel auf fixen Adressen befinden, wird noch ein entsprechender Strukturzeiger definiert und diesem die Adresse des ersten Registers des Bausteins zugewiesen.

PgmHeader
// 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;
HinweisBeachten Sie, wie im Beispiel die nicht belegten Bits des Peripherie-Bausteins übersprungen werden. Um Füllbits zu definieren, werden nur der Datentyp und die Anzahl der Bits angegeben. Ein Elementname muss hier nicht vergeben werden.

Im obigen Beispiel sind nach der Definition des Bitfeldes noch zwei Zugriffe auf den Peripherie-Baustein angegeben. Beachten Sie, dass der Zeigeroperator -> nur für den Zugriff auf die Struktur verwendet wird. Der Zugriff auf die Elemente reg0 und reg2 erfolgt direkt über den Punktoperator. Mehr über den Zugriff auf Strukturelemente später noch.

Besonderheiten von Bitfeldern

Auf einige Besonderheiten bei Bitfelder soll zum Schluss noch hingewiesen werden: