C++ Kurs

Bitfelder

Die Themen:

Einleitung
Datenkomprimierung und Syntax
Zugriff auf Bitfeld-Elemente
Zugriff auf Peripherie über Bitfelder
Besonderheiten von Bitfeldern

Einleitung

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:

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

Datenkomprimierung und Syntax

Datenkomprimierung

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.

Syntax

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;

Die in der Lektion über den Datentyp string erwähnte Standard-Bibliothek enthält schon eine hierauf 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 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;

Zugriff auf Peripherie über Bitfelder

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
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. 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;

Beachten wie im Beispiel die nicht belegten Bits des Peripherie-Bausteins 'übersprungen' werden. Um Füllbits zu definieren, wird nur der Datentyp und die Anzahl der Bits definiert, ein Elementname muss hier nicht vergeben werden.

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.

Besonderheiten von Bitfeldern

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