Bitmanipulation

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Bitmaske

Im Folgenden ist häufiger von dem Begriff Bitmaske die Rede. Damit wird eine Folge von einzelnen Bit bezeichnet, die z.B. den Zustand Null ('0') oder Eins ('1') darstellen können.

Bitmasken werden im allgemeinen dazu verwendet, um unter Anwendung eines Operators (z.B. UND, ODER, XOR), eine Eingabe zu manipulieren. Das Ergebnis ist dann die Anwendung des Operators auf die Eingabe und der Bitmaske.

Wenn ein Operator eine Funktion mit zwei Argumenten ist, dann lässt sich dessen Anwendung wie folgt schreiben:

<c> Ergebnis = Operator( Eingabe, Bitmaske ) </c>

Die Bitmaske ist häufig eine Konstante, da diese z.B. die Information über die Position einer Information in einem Register darstellt. Das kann z.B. ein Überlaufflag in einem Timer Statusregister sein.

Bits setzen

Wenn in einem Byte mehrere Bits auf Eins gesetzt werden sollen, wird dies durch eine ODER-Verknüpfung erreicht. Alle Bits, welche in der Bitmaske '1' sind, werden auf '1' gesetzt. Alle Bits, die in der Maske auf '0' gesetzt sind, bleiben unverändert.

AVR-Assembler

<avrasm> sbr r16, 0b11110000  ; setzt Bits 4-7 in r16, ist ein Pseudobefehl

                       ; funktioniert nur für die Arbeitsregister r16-r31

ori r16, 0b11110000  ; setzt Bits 4-7 in r16, ori ist identisch mit sbr

                       ; funktioniert nur für die Arbeitsregister r16-r31

sbi PORTB, 5  ; setzt Bit 5 in PortB sbi PORTB, PB5  ; identisch, besser lesbar

                       ; funktioniert nur für die IO-Register 0..0x1F
                       ; für I/O Register mit I/O Adresse 0x20..0x3F muss
                       ; in/out verwendet werden

in r16, TIMSK  ; setzt Bit TOIE1 in TIMSK sbr r16, (1<<TOIE1) out UCSR0B, r16

                       ; für I/O Register oberhalb der I/O Adresse 0x3F muss
                       ; lds/sts verwednet werden
                       ; setzt Bit RXCIE0 in UCSR0B

lds r16, UCSR0B sbr r16, (1<<RXCIE0) sts UCSR0B, r16 </avrasm>

Man beachte den Unterschied! Eine "5" würde von sbr als "setze Bit 2 und 0" gedeutet (=0b00000101), während sbi sie als "setzte Bit 5" versteht. Der Befehl sbr erwartet ein Bitmuster für eine ODER-Verknüpfung, während der Befehlt sbi die Bitnummer benötigt. Darauf sind auch die Includefiles von Atmel im AVR-Studio (Assembler) als auch WinAVR (C) ausgelegt. Die Namen der Bits sind als Bitnummer definiert. Das ist wichtig, wenn man Register von grossen AVRs manipuliert, z.B. ATmega48. Hier muss aus der Bitnummer über eine Schiebeoperation erst das Bitmuster gemacht werden.

Standard C

<c> PORTB |= 0xF0; // Kurzschreibweise, entspricht PORTB = PORTB | 0xF0; bitweises ODER

/* übersichtlicher mittels Bit-Definitionen */

  1. define MEINBIT0 0
  2. define MEINBIT1 1
  3. define MEINBIT2 2

PORTB |= ((1 << MEINBIT0) | (1 << MEINBIT2)); // setzt Bit 0 und 2 in PORTB auf "1" </c>

Die letzte Zeile "entschlüsselt":

  1. (1 << n) : Zuerst wird durch die '<<'-Ausdrücke eine "1" n-mal nach links geschoben. Dies ergibt somit (in Binärschreibweise) 0b00000001 für (1 << MEINBIT0) und 0b00000100 für (1 << MEINBIT2).
  2. | : Das Ergebnis wird bitweise ODER-verknüpft, also 0b00000001 or 0b00000100 wird zu 0b00000101.
  3. |= : Diese Maske wird mit dem aktuellen Inhalt von PORTB bitweise ODER-verknüpft und das Ergebnis PORTB wieder zugewiesen.

<c> PORTB |= variable; // Kurzschreibweise PORTB = PORTB | variable; // lange Schreibweise </c>

Ist PORTB vorher z.B. 0b01111010, dann ist der Inhalt nach der Operation 0b01111010 or 0b00000101 = 0b01111111, die gewünschten Bits sind somit gesetzt!

Anmerkung: Will man das gezeigte Beispiel der Bitmanipulation auf größere Datentypen anwenden, ist zu beachten, dass der Compiler in der Operation (1 << MEINBIT1) stillschweigend die 1 als Integer Type ansieht. Beim AVR-GCC bedeutet das 16-Bit und die folgende Operation bringt ggf. nicht das gewünschte Ergebnis. (Stichwort: "Integer Promotion").

Angenommen Bit 15 soll in einer 32-Bit weiten Variable gesetzt werden.

<c>

  1. define MEINBIT15 15
  2. define MEINBIT42 42

uint32_t reg_32; uint64_t reg_64;

reg_32 |= (1 << MEINBIT15); /* FEHLER: Setzt die Bits 31 - 15, da ((int)1 << 15) == 0xFFFF8000 */

reg_32 |= ((uint32_t)1 << MEINBIT15); /* Hier wird nur Bit 15 gesetzt. */ reg_32 |= (1L << MEINBIT15); /* andere Schreibweise. */ reg_64 |= (1LL << MEINBIT42); /* Hier wird nur Bit 42 gesetzt,

                                           andere Schreibweise für 64 Bit (long long). */

</c>

Bits löschen

Wenn in einem Byte mehrere Bits auf Null gesetzt werden sollen, wird dies durch eine UND-Verknüpfung erreicht. Alle Bits, welche in der Bitmaske '0' sind, werden auf '0' gesetzt. Alle Bits, die in der Maske auf '1' gesetzt sind, bleiben unverändert.

AVR-Assembler

<avrasm> cbr r16, 0b11110000  ; löscht Bits 0-3 in r16, ist ein Pseudobefehl

                       ; funktioniert nur für die Arbeitsregister r16-r31

andi r16, 0b11110000  ; löscht Bits 0-3 in r16, andi ist identisch mit cbr

                       ; funktioniert nur für die Arbeitsregister r16-r31

andi r16, ~0b00001111  ; andere Schreibweise, hier wird die Bitmaske durch ~ invertiert

                       ; dadurch kann man einfach alle zu löschenden Bit als '1' angeben
                       ; so wie bei den Bitmasken für das setzen von Bits (positive Logik)

cbi PORTB, 5  ; löscht Bit 5 in PortB cbi PORTB, PB5  ; identisch, besser lesbar

                       ; funktioniert nur für die IO-Register 0..31
                       ; für I/O Register mit I/O Adresse 0x20..0x3F muss
                       ; in/out verwendet werden

in r16, TIMSK  ; löscht Bit TOIE1 in TIMSK cbr r16, ~(1<<TOIE1) out UCSR0B, r16

                       ; für I/O Register oberhalb der I/O Adresse 0x3F muss
                       ; lds/sts verwednet werden
                       ; löscht Bit RXCIE0 in UCSR0B

lds r16, UCSR0B cbr r16, ~(1<<RXCIE0) sts UCSR0B, r16

</avrasm>

Auch hier gilt. Man beachte den Unterschied! Eine "5" würde von cbr als "lösche Bit 7,6,5,4,3 und 1" gedeutet, während cbi sie als "lösche Bit 5" versteht. Der Befehl cbr erwartet ein Bitmuster für eine UND-Verknüpfung, während der Befehlt cbi die Bitnummer benötigt. Darauf sind auch die Includefiles von Atmel im AVR-Studio (Assembler) als auch WinAVR (C) ausgelegt. Die Namen der Bits sind als Bitnummer definiert. Das ist wichtig, wenn man Register von grossen AVRs manipuliert, z.B. ATmega48. Hier muss aus der Bitnummer über eine Schiebeoperation erst das Bitmuster gemacht werden.

Standard C

<c> PORTB &= 0xF0; // entspricht PORTB = PORTB & 0xF0; bitweises UND

                // Bits 0-3 (das "niederwertige" Nibble) werden geloescht 

/* uebersichtlicher mittels Bit-Definitionen */

  1. define MEINBIT0 0
  2. define MEINBIT1 1
  3. define MEINBIT2 2

PORTB &= ~((1 << MEINBIT0) | (1 << MEINBIT2)); // löscht Bit 0 und 2 in PORTB </c>

Die letzte Zeile entschlüsselt:

  1. (1 << n) : Zuerst wird durch die '<<'-Ausdrücke eine "1" n-mal nach links geschoben. Dies ergibt somit (in Binärschreibweise) 0b00000001 für (1 << MEINBIT0) und 0b00000100 für (1 << MEINBIT2).
  2. | : Das Ergebnis wird bitweise ODER-verknüpft also 0b00000001 or 0b00000100 wird zu 0b00000101.
  3. ~ : Der Wert in der Klammer wird bitweise invertiert, aus 0b00000101 wird 0b11111010.
  4. &= : PORTB wird mit der berechneten Maske UND-verknüpft und das Ergebnis wieder PORTB zugewiesen.

<c> PORTB &= variable; // Kurzschreibweise PORTB = PORTB & variable; // lange Schreibweise </c>

Ist PORTB vorher z.B. 0b01111111, dann ist der Inhalt nach der Operation 0b011111111 and 0b11111010 = 0b01111010, die gewünschten Bits 0 und 2 sind somit gelöscht.

Wichtiger Hinweis! Die ODER-Verknüpfung und die anschliessende Invertierung kann man nicht vertauschen! (Theorem von DeMorgan) Folgendes Beispiel soll die Richtigkeit der Aussage zeigen:

<c>

~(0b0001 | 0b0010) == 0b1100
 ~0b0001 | ~0b0010 == 0b1111

</c>

Die C-Ausdrücke sehen auf den ersten Blick etwas "erschreckend" aus und sind mehr "Tipparbeit", funktionieren aber universell und sind nach einiger Gewöhnung deutlicher und nachvollziehbarer als "handoptimierte" Konstanten.

Niederwertigstes gesetztes Bit löschen (Standard C)

Folgender Code löscht von allen 1-Bits in einer Integer-Variable das niederwertigste, unabhängig von der Position desselben.

Beispiel: 01101000 -> 01100000

<c> uint8_t byte;

byte = irgendwas();

byte = byte & (byte - 1); /* Diese seltsame Operation löscht das

                            niederwertigste 1-Bit */

</c> Beispiel: <c> Byte  : 01101000 Byte-1: 01100111 Ergebnis:01100000

</c> Das funktioniert also mit jeder beliebigen Zahl.

Dies kann bspw. zur schnellen Paritätsgenerierung eingesetzt werden:

<c> uint8_t pareven(uint8_t byte) {

 uint8_t par = 0;
 while(byte) {
   byte = byte & (byte - 1);
   par = ~par;
 }
 return par;

} </c>

Das genannte gilt natürlich nicht nur für 8-Bit-Integers, sondern für beliebige, vom Compiler unterstützte Wortlängen.

Bits invertieren

Im allgemeinen Sprachgebrauch oft Toggeln genannt (aus dem Englischen). Wenn in einem Byte mehrere Bits invertiert (getoggelt) werden sollen, wird die durch eine XOR-Verknüpfung erreicht. Alle Bits, welche in der Bitmaske '1' sind, werden invertiert. Alle Bits, die in der Maske auf '0' gesetzt sind, bleiben unverändert.

AVR Assembler

Bei AVRs erlaubt dies folgender Assemblercode. Hier wird ein Ausgangspin invertiert.

<avrasm>

 sbic  PortB, 0    ; Überspringe den nächsten Befehl, wenn das Bit 0 im Port gelöscht ist
 rjmp  ClrBitNow   ; Springe zu ClrBitNow   
 sbi   PortB, 0    ; Setze Bit 0 in PortB
 rjmp  BitReady    ; Springe BitReady

ClrBitNow:

  cbi  PortB, 0    ; Lösche Bit 0 in PortB

BitReady: </avrasm>

Kürzer gehts mit folgender Variante.

<avrasm>

sbis   PinB,   0    ; Überspringe den nächsten Befehl, wenn das Bit 0 im Port gesetzt ist
sbi    PortB,  0    ; Setze Bit 0 in PortB
sbic   PinB,   0    ; Überspringe den nächsten Befehl, wenn das Bit 0 im Port gelöscht ist
cbi    PortB,  0    ; Lösche Bit 0 in PortB

</avrasm>

Noch kürzer gehts so:
Die zweite Zeile mit dem Befehl ldi lädt die Bitmaske, in welcher die zu toggelnden Bits auf '1' gesetzt sind. In diesem Beispiel wird das dritte Bit invertiert. Der Vorteil dieser Methode ist neben der Kürze und Übersichtlichkeit auch die Möglichkeit, bis zu 8 Bit gleichzeitig zu toggeln. Diese Methode ist natürlich auch auf normale Daten anwendbar, nicht nur auf IO-Ports.

<avrasm>

in     R24, PORTE   ; Daten lesen
ldi    R25, 0x04    ; Bitmaske laden, hier Bit #2
eor    R24, R25     ; Exklusiv ODER
out    PORTE, R24   ; Daten zurückschreiben

</avrasm>

Eine andere Möglichkeit gibt es, wenn man nur das 8. Bit kippen will:

<avrasm>

in      r16, PORTB
subi    r16, 0x80
out     PORTB, r16

</avrasm>

Standard C

<c>

PORTB ^= (1<<PB0);    /* XOR, Kurzschreibweise, PORTB = PORTB ^ (1<<PB0) */

</c>

Neuere ATmegas

Bei den neueren ATmegas (z.B. ATmega48) kann man IO-Pins direkt ohne den Umweg über Register togglen, indem man das entsprechende Bit im PINx-Register setzt:

<avrasm> sbi PIND, 2  ; Bit 2 von Port D togglen </avrasm>

8051er

<avrasm> cpl bitadresse </avrasm>

Bits prüfen

Will man prüfen ob ein oder mehrere Bits in einer Variable gesetzt oder gelöscht sind, muss man sie mit einer Bitmaske UND verknüpfen. Die Bitmaske muss an den Stellen der zu prüfenden Bits eine '1' haben, an allen anderen einen '0'.

  • Ist das Ergebnis gleich Null, sind alle geprüften Bits gelöscht.
  • Ist das Ergebnis ungleich Null, ist mindestens ein geprüftes Bit gesetzt.
  • Ist das Ergebnis gleich der Bitmaske, sind alle geprüften Bits gesetzt.

AVR Assembler

Der AVR hat spezielle Befehle um direkt einzelne Bits in den CPU-Registern r0..r31 sowie den IO-Registern 0..0x1F zu prüfen.

<avrasm>

Befehle zur Prüfung von einzelnen Bits
   sbrs    r16,3       ; überspringe den nächsten Befehl, wenn in r16 Bit #3 gesetzt ist
   rjmp    bit_ist_nicht_gesetzt
   sbrc    r16,5       ; überspringe den nächsten Befehl, wenn in r16 Bit #5 gelöscht ist
   rjmp    bit_ist_nicht_geloescht
   sbis    timsk,3     ; überspringe den nächsten Befehl, wenn in timsk Bit #3 gesetzt ist
   rjmp    bit_ist_nicht_gesetzt
   sbic    timsk,5     ; überspringe den nächsten Befehl, wenn in timsk Bit #5 gelöscht ist
   rjmp    bit_ist_nicht_geloescht
Befehle zur Prüfung von mehreren Bits
   andi    r16,0b1010  ; prüfe Bit #1 und #3 in r16
   breq    alle_bits_sind_geloescht
   andi    r16,0b1010  ; prüfe Bit #1 und #3 in r16
   brne    mind_ein_bit_ist_gesetzt
   andi    r16,0b1010  ; prüfe Bit #1 und #3 in r16
   cpi     r16,0b1010
   breq    alle_bits_sind_gesetzt

</avrasm>

Standard C

<c>

   // prüfe ob Bit 4 in der Variable tmp gelöscht ist
   // die Klammer ist wichtig 
   if (!(tmp & 0x10)) {        
      // hier die Anweisungen, wenn das Bit gelöscht ist
   }
   // prüfe ob Bit 0 und Bit 4 in der Variable tmp gelöscht sind
   // die Klammer ist wichtig! 
   if ((~tmp & 0x11) == 0x11) {        
      // hier die Anweisungen, wenn beide Bits gelöscht sind
   }
   // prüfe ob Bit 0 oder Bit 4 in der Variable tmp gesetzt ist
   if (tmp & 0x11) {        
      // hier die Anweisungen, wenn mindestens ein Bit gesetzt ist
   }
   // prüfe ob Bit 0 oder Bit 4 in der Variable tmp gelöscht sind
   if (~tmp & 0x11) {        
      // hier die Anweisungen, wenn mindestens ein Bit gelöscht ist
   }
   // prüfe ob Bit 4 in der Variable tmp gesetzt ist 
   if (tmp & 0x10) {        
      // hier die Anweisungen, wenn das Bit gesetzt ist
   }
   // prüfe ob Bit 0 und Bit 4 in der Variable tmp gesetzt sind
   // die Klammer ist wichtig! 
   if ((tmp & 0x11) == 0x11) {        
      // hier die Anweisungen, wenn beide Bits gesetzt sind
   }

</c>