Bitmanipulation

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

Bitoperatoren

Bitoperatoren stammen ursprünglich aus dem Bereich des Maschinen-Codes und Assembler, existieren aber auch in den meisten Hochsprachen, wie C.

>>    nach rechts schieben
<<    nach links schieben (Bsp.: a<<b ist das gleiche wie a * 2^b;
          bzw. bei 1<<3 wird die 1 um drei Stellen nach links geschoben)
|     bitweises ODER
&     bitweises UND
^     bitweises exklusives ODER
~     bitweises NICHT

Bitoperatoren in dieser Schreibweise können in Assemblercode für konstante Ausdrücke benutzt werden.

Beispiel:

 
ldi temp, (1<<3) | (1<<1) | (1<<2) | (1<<0)

entspricht:

 
ldi temp, 8 | 2 | 4 | 1

entspricht:

 
ldi temp, 15

Bitmaske

Im Folgenden ist häufiger von dem Begriff Bitmaske die Rede. Damit wird eine Folge von einzelnen Bits bezeichnet, die 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 und der Bitmaske auf die Eingabe.

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

Ergebnis = Operator( Eingabe, Bitmaske )

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 gleich „1“ sind, werden auf „1“ gesetzt. Alle Bits, die in der Maske auf „0“ gesetzt sind, bleiben unverändert.

AVR-Assembler

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 Port B;
sbi PORTB, PB5          ; identisch, besser lesbar;
                        ; funktioniert nur für die IO-Register 0..0x1F

                        ; Für IO-Register mit IO-Adresse 0x20..0x3F muss
                        ; in/out verwendet werden:
in  r16, TIMSK          ; setzt Bit TOIE1 in TIMSK
sbr r16, (1<<TOIE1)
out TIMSK, r16

                        ; Für IO-Register oberhalb der IO-Adresse 0x3F muss
                        ; lds/sts verwendet werden:
                        ; setzt Bit RXCIE0 in UCSR0B
lds r16, UCSR0B
sbr r16, (1<<RXCIE0)
sts UCSR0B, r16

Man beachte den Unterschied! Eine „5“ würde von sbr als „setze Bit 2 und 0“ gedeutet (=0b00000101), während sbi sie als „setze Bit 5“ versteht. Der Befehl sbr erwartet ein Bitmuster für eine ODER-Verknüpfung, während der Befehl 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 großen AVRs manipuliert, z. B. ATmega48. Hier muss aus der Bitnummer über eine Schiebeoperation erst das Bitmuster gemacht werden.

ARMv6-M (Cortex-M0)

Der minimalistische Instruction-Set der ARMv6M-Architektur, auf welcher der Cortex-M0 basiert, unterstützt Bit-Operationen nur mit Registern, nicht über Literals. Daher muss der Bitmasken-Wert zunächst per movs in ein beliebiges Register geladen werden, und dann dies über die orrs -Instruktion verknüpft werden:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
orrs r0, r3      /* Setze bits 4-7 in r0 */

Man beachte das s in movs bzw. orrs - dies weist den Prozessor an, die Flags im Flag-Register (Zero, Negative, Carry. etc) zu aktualisieren. Das wird hier zwar gar nicht benötigt, aber bei ARMv6-M gibt es keine Variante dieser Instruktionen die die Flags nicht setzen - damit aber der Assembler-Code "aufwärtskompatibel" zu neueren Architekturversionen ist, muss das s angehängt werden, um das Überschreiben der Flags anzudeuten.

Außerdem unterstützt die movs-Instruktion nur 8-bit immediates; möchte man also eines der bits 8-31 setzen, muss man den entsprechenden 32bit-Literalwert per ldr aus dem Literal-Pool im Flash laden:

ldr r3, =#0xF00000F0   /* Lade literal-Wert mit den bits 4-7 und 28-31 gesetzt */
orrs r0, r3            /* Setze bits 4-7 und 28-31 in r0 */
/* ... Mehr Code ... */
bx lr                  /* Funktions-Rücksprung */
.ltorg                 /* Hier alle zuvor benötigten "langen" Literal-Werte in den Flash legen. Darf nicht als Code ausgeführt werden. */

Per .ltorg wird ein separater Flash-Bereich deklariert, in welchem der Assembler alle zuvor per ldr rX, =... verwendeten "langen" Literal-Werte ablegt. Der Assembler ersetzt das ldr dann durch ein ldr rX, [PC, #off], d.h. berechnet den Abstand zwischen der Instruktion selbst und dem abgelegten Literal-Wert, und lässt die ldr-Instruktion diesen Abstand auf den Program Counter PC aufaddieren und von dort den Wert laden. Durch diese relative Adressierung spielt die absolute Platzierung dieses Codes im Flash keine Rolle. Üblicherweise platziert man eine .ltorg -Direktive ans Ende jeder Funktion, denn der genannte Abstand darf nicht zu groß werden.

Wenn die zu setzenden Bits in einem einzelnen Byte darstellbar sind, kann man sich mit der Kombination movs + lsls behelfen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
lsls r3, #24     /* Um 24bits nach links shiften - Ergebnis ist dann 0xF0000000 */
orrs r0, r3      /* Setze bits 28-31 in r0 */

Dies belegt nur 6 statt 8 Bytes wie bei der Variante mit ldr, und ist wahrscheinlich gleich schnell (ldr benötigt mindestens 2 Takte plus ggf. Flash-Latenz; die einfachen arithmetischen Instruktionen immer nur 1 Takt).

Falls sich nur der negierte Wert der Bitmaske als einzelnes Byte darstellen lässt, kann man sich mit der Kombination movs + mvns behelfen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
mvns r3, r3      /* r3 negieren - Ergebnis ist dann 0xFFFFFF0F */
orrs r0, r3      /* Setze alle bits außer 4-7 in r0 */

Dies kann genutzt werden, wenn so viele Bits auf 1 gesetzt werden sollen, dass sich diese nicht mehr in der movs-Instruktion darstellen lassen, aber die nicht zu setzenden Bits schon. Dies ist mit 6 Bytes ebenfalls effizienter als die Version mit ldr.

Dies kann dann auch noch mit lsls kombiniert werden falls die nicht zu setzenden Bits in ein Byte an einer beliebigen Position im Worts stehen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
lsls r3, #24     /* Um 24 bits nach links schieben - Ergebnis ist dann 0xF0000000 */
mvns r3, r3      /* r3 negieren - Ergebnis ist dann 0x0FFFFFFF */
orrs r0, r3      /* Setze alle bits außer 28-31 in r0 */

Allerdings ist hier der Speicherverbrauch mit 8 Bytes genau so groß wie bei der ldr-Version, während die Ausführung langsamer sein wird, weshalb die ldr-Version eher zu bevorzugen ist.

ARMv7-M (Cortex-M3/M4/M7)

Die ARMv7-M basierten Prozessoren unterstützen die gleichen oben gezeigten Möglichkeiten für Bitoperationen wie ARMv6-M, aber zusätzlich noch einige weitere, effizientere Varianten:

Die orr-Instruktion kann hier direkt die Bitmaske als Immediate-Wert enthalten:

orr r0, r3, #0xF0      /* Setze bits 4-7 in r0 */

Dies ist schnell und effizient, funktioniert aber nur, wenn die Bitmaske sich "kompakt" darstellen lässt - d.h. als 8bit-Wert, der an eine beliebige Stelle im 32bit-Wert geshiftet wird, oder sich 1-4x wiederholt. Im ARMv7-M Architecture Reference Manual ist dies detailliert spezifiziert. Auf dem ARMv7M hat man bei der orr-Instruktion zudem die Wahl, ob die Flags im Flag-Register gesetzt werden sollen, d.h. orr und orrs können beide genutzt werden, aber nur zweiteres aktualisiert die Flags.

Falls sich nur der negierte Wert der Bitmaske als Immediate darstellen lässt, kann die orn -Instruktion genutzt werden. Diese negiert den angegebenen Immediate-Wert, bevor die OR-Operation durchgeführt wird. Das ist äquivalent zur Cortex-M0-Version mit movs+mvns+orrs, aber in nur einer einzelnen Instruktion:

orn r0, r3, #0x00F00000      /* Setze alle bits außer 20-23 in r0 */

Dies kann äquivalent genutzt werden, wenn so viele Bits auf 1 gesetzt werden sollen, dass sich diese nicht mehr in der orr-Instruktion darstellen lassen, aber die nicht zu setzenden Bits schon. Der Assembler macht das aber auch automatisch, d.h. ersetzt orr durch orn (und umgekehrt) falls nötig. Somit sind diese beiden Zeilen identisch, beides wird als orn assembliert:

orn r0, r3, #0x00F00000      /* Setze alle bits außer 20-23 in r0 */
orr r0, r3, #0xFF0FFFFF      /* Setze alle bits außer 20-23 in r0 */

Wenn die gewünschte Bitmaske und auch ihr Komplement "komplexer" sind und sich nicht als Immediate-Wert in orr/orn darstellen lassen, kann wie zuvor (siehe oben) beim Cortex-M0 auch die Kombination aus ldr und orrs genutzt werden. Dabei empfiehlt es sich falls möglich immer orrs statt orr zu nutzen (wie oben), denn die orrs -Instruktion die nur auf Registern arbeitet (ohne Immediates) belegt nur 16bit Programmspeicher, aber orr benötigt immer 32bit, ohne schneller zu sein.

Zum Setzen von Bits auf dem ARMv7-M gibt es noch einen weiteren Trick - falls die zu setzenden Bits zusammenhängend und ganz "oben" bzw. "unten" im Wert sind, können diese einfach "ins Nirvana" raus geschoben werden, und der Wert dann wieder zurückgeschoben und mit 0-Bits aufgefüllt werden. Wenn dies mit der mvns -Instruktion gemacht wird, wird der Wert dabei 2x invertiert, wodurch die "frischen" 0-Bits zu 1-Bits werden, und der Rest des Werts erhalten bleibt:

mvn r0, r0, #7, lsl #7      /* r0 um 7 bits nach links schieben und invertieren - die oberen 7 bits gehen verloren */
mvn r0, r0, #7, lsr #7      /* r0 um 7 bits nach rechts schieben und invertieren - die unteren 25 bits sind wieder in Ausgangposition und unverändert, die oberen 7 bits werden gesetzt */
mvn r0, r0, #7, lsr #7      /* r0 um 7 bits nach rechts schieben und invertieren - die unteren 7 bits gehen verloren */
mvn r0, r0, #7, lsl #7      /* r0 um 7 bits nach links schieben und invertieren - die oberen 25 bits sind wieder in Ausgangposition und unverändert, die unteren 7 bits werden gesetzt */

Standard-C

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

/* übersichtlicher mittels Bit-Definitionen */
#define MEINBIT0 0
#define MEINBIT1 1
#define MEINBIT2 2

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

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 oder 0b00000100 wird zu 0b00000101.
  3. |= : Diese Maske wird mit dem aktuellen Inhalt von PORTB bitweise ODER-verknüpft und das Ergebnis PORTB wieder zugewiesen.
PORTB |= variable;         // Kurzschreibweise
PORTB  = PORTB | variable; // lange Schreibweise
Ist PORTB vorher z. B. 0b01111010, dann ist der Inhalt nach der Operation 0b01111010 oder 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, gemäß den C-Regeln, die 1 als Integer-Typ ansieht. Beim AVR-GCC bedeutet das 16-Bit/signed 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.

#define MEINBIT15 15
#define MEINBIT42 42

uint32_t reg_32; /* uint32_t definiert per typedef z.&nbsp;B. in stdint.h */
uint64_t reg_64; /* uint64_t definiert per typedef z.&nbsp;B. in stdint.h */

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 |= (1U << MEINBIT15);             /* */
reg_32 |= (1L << MEINBIT15);             /* andere Schreibweise. */
reg_64 |= (1LL << MEINBIT42);            /* Hier wird nur Bit 42 gesetzt,
                                            andere Schreibweise für 64 Bit (long long). */

Bei Compilern für 32-bit-Controller (z. B. ARM7TDMI) sind Integers per default 32-bit und Konstanten sind somit implizit ebenfalls 32-bit weit. Man sollte aber dennoch die oben gezeigte Vorgehensweise verwenden, um Probleme zu vermeiden, die entstehen könnten, wenn Code unter verschiedenen Plattformen/Compilern verwendet werden soll.

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 gleich „0“ sind, werden auf „0“ gesetzt. Alle Bits, die in der Maske auf „1“ gesetzt sind, bleiben unverändert.

AVR-Assembler

cbr r16, 0b00001111     ; 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 Port B
cbi PORTB, PB5          ; identisch, besser lesbar
                        ; funktioniert nur für die IO-Register 0..31

                        ; Für IO-Register mit IO-Adresse 0x20..0x3F muss
                        ; in/out verwendet werden, weil dieser Bereich nicht
                        ; bitadressierbar ist:
in  r16, TIMSK          ; löscht Bit TOIE1 in TIMSK
cbr r16, 1<<TOIE1
out TIMSK, r16

                        ; Für IO-Register oberhalb der IO-Adresse 0x3F muss
                        ; lds/sts verwendet werden:
                        ; löscht Bit RXCIE0 in UCSR0B
lds r16, UCSR0B 
cbr r16, 1<<RXCIE0
sts UCSR0B, r16

Auch hier gilt: Man beachte den Unterschied! Eine „5“ würde von cbr als „lösche Bit 2 und 0“ gedeutet, während cbi sie als „lösche Bit 5“ versteht. Der Befehl cbr erwartet ein Bitmuster für eine UND-NOT-Verknüpfung (nicht zu verwechseln mit NAND), während der Befehl 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 großen AVRs manipuliert, z. B. ATmega48. Hier muss aus der Bitnummer über eine Schiebeoperation „<<“ erst das Bitmuster gemacht werden.

ARMv6-M (Cortex-M0)

Das Löschen von Bits funktioniert auf der ARM-Architektur äquivalent zum Setzen, aber über bic statt orrs. Es gelten die gleichen Einschränkungen und Möglichkeiten zum Definieren der zu setzenden Bits als Bitmaske. Die zum Löschen statt Setzen der Bits angepassten Beispiele sind dann:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
bics r0, r3      /* Lösche bits 4-7 in r0 */

Äquivalent um die Bits 8-31 löschen zu können kann wieder ein Literal-Pool genutzt werden:

ldr r3, =#0xF00000F0   /* Lade literal-Wert mit den bits 4-7 und 28-31 gesetzt */
bics r0, r3            /* Lösche bits 4-7 und 28-31 in r0 */
/* ... Mehr Code ... */
bx lr                  /* Funktions-Rücksprung */
.ltorg                 /* Hier alle zuvor benötigten "langen" Literal-Werte in den Flash legen. Darf nicht als Code ausgeführt werden. */

Die Kombination movs + lsls wird dann:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
lsls r3, #24     /* Um 24bits nach links shiften - Ergebnis ist dann 0xF0000000 */
bics r0, r3      /* Lösche bits 28-31 in r0 */

Beim ARMv6-M Instruction Set gibt es eine kleine Asymmetrie; das Äquivalent zur Kombination movs+mvns+orrs zum Löschen von vielen Bits kann einfacher über die ands-Instruktion implementiert werden:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt. Kein mvns nötig. */
ands r0, r3      /* Lösche alle bits außer 4-7 in r0 */

Dies kann genutzt werden, wenn so viele Bits auf 1 gesetzt werden sollen, dass sich diese nicht mehr in der movs-Instruktion darstellen lassen, aber die nicht zu setzenden Bits schon. Dies ist mit 6 Bytes ebenfalls effizienter als die Version mit ldr.

Die Kombination mit lsls ist dann wiederum sinnvoll, falls die nicht zu setzenden Bits in ein Byte an einer beliebigen Position im Worts stehen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
lsls r3, #24     /* Um 24 bits nach links schieben - Ergebnis ist dann 0xF0000000 */
ands r0, r3      /* Lösche alle bits außer 28-31 in r0 */

Dies ist wieder besser als die ldr-Version. Zum Löschen von zusammenhängenden Bits ganz "oben" bzw. "unten" im Wert kann das Äquivalent zur ARMv7-M Version mit 2x mvn zum Setzen von Bits genutzt werden, welches zum Löschen aber auch auf dem ARMv6-M funktioniert. Die Bits werden einfach "ins Nirvana" raus geschoben und der Wert dann wieder zurückgeschoben und mit 0-Bits aufgefüllt.

Obere 7 bits löschen:

lsls r0, #7      /* r0 um 7 bits nach links schieben - die oberen 7 bits gehen verloren */
lsrs r0, #7      /* r0 um 7 bits nach rechts schieben - die unteren 25 bits sind wieder in Ausgangposition, die oberen 7 bits werden gelöscht */

Untere 7 bits löschen:

lsrs r0, #7      /* r0 um 7 bits nach rechts schieben - die unteren 7 bits gehen verloren */
lsls r0, #7      /* r0 um 7 bits nach links schieben - die oberen 25 bits sind wieder in Ausgangposition, die unteren 7 bits werden gelöscht */

ARMv7-M (Cortex-M3/M4/M7)

Bei ARMv7-M basierten Prozessoren gibt es wieder erweiterte Instruktionen welche Bitmaske und Operation kombinieren:

Die bic-Instruktion zum Löschen von Bits, äquivalent zu orr von zuvor:

bic r0, r3, #0xF0      /* Lösche bits 4-7 in r0 */

Hier besteht wieder die Wahl zwischen bic und bics zum Behalten oder Überschreiben der Status-Flags.

Das Äquivalent von orn ist hier wieder and, falls sich nur der negierte Wert der Bitmaske als Immediate darstellen lässt:

and r0, r3, #0x00F00000      /* Lösche alle bits außer 20-23 in r0 */

Der Assembler ersetzt wieder automatisch bic durch and (und umgekehrt) falls nötig. Somit sind diese beiden Zeilen identisch, beides wird als and assembliert:

bic r0, r3, #0xFF0FFFFF      /* Lösche alle bits außer 20-23 in r0 */
and r0, r3, #0x00F00000      /* Lösche alle bits außer 20-23 in r0 */

Für komplexe Bitmasken kann wie zuvor (siehe oben) beim Cortex-M0 auch die Kombination aus ldr und bics genutzt werden.

Außerdem kann mit bfc eine beliebige Folge zusammenhängender Bits gelöscht werden:

bfc r0, #4, #3      /* Lösche bits 4-6 in r0 */


Standard-C

PORTB &= 0xF0;   // entspricht PORTB = PORTB & 0xF0; bitweises UND
                 // Bits 0..3 (das "niederwertige" Nibble) werden gelöscht

/* übersichtlicher mittels Bit-Definitionen */
#define MEINBIT0 0
#define MEINBIT1 1  
#define MEINBIT2 2  

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

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 oder 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.
PORTB &= variable;          // Kurzschreibweise
PORTB  = PORTB & variable;  // lange Schreibweise
Ist PORTB vorher z. B. 0b01111111, dann ist der Inhalt nach der Operation 0b01111111 und 0b11111010 = 0b01111010, die gewünschten Bits 0 und 2 sind somit gelöscht.

Die C-Ausdrücke mittels Definitionen von Bitnummern und Schiebeoperator (<<) sehen auf den ersten Blick etwas „erschreckend“ aus und sind mehr „Tipparbeit“, funktionieren aber universell und sind deutlicher und nachvollziehbarer als „handoptimierte“ Konstanten. Bei eingeschalteter Optimierung löst der Compiler die Ausdrücke mit konstanten Werten bereits zur Compilierungszeit auf und es entsteht kein zusätzlicher Maschinencode. Bei AVR sind die Definitionen meist Teil der Entwicklungsumgebungen (bei avr-libc z. B. implizit durch #include <avr/io.h>). Sie entsprechen den Angaben und Beispielen in den Datenblättern und sind damit de facto ein Standard beim Zugriff auf Bits in Hardware-Registern.

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

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

Noch ein wichtiger Hinweis: Der Operator ~ mit einem Operanden vom Typ int negiert nur so viele Bits, wie der Typ int hat. Will man ein Bit in einer breiteren Variablen löschen, dann sollte die nach links zu shiftende „1“ den Typ dieser Variablen haben.

Ein Programm, welches das verdeutlicht:

#include <stdio.h>
#include <stdint.h>

int main(int argc, const char* argv[])
{
    int bit = 60;
    uint64_t ui64;
    printf("sizeof(int)=%d\n", (int)sizeof(int));
    
    ui64 = 0xFFFFFFFFFFFFFFFF;    
    ui64 &= ~(1<<60); /* Keine Wirkung bei sizeof(int) < 8 */
    /* gcc warnt sogar:    
     * gcc -Wall bit_clear.c -o bit_clear
     * bit_clear.c: In function ‘main’:
     * bit_clear.c:11:5: warning: left shift count >= width of type [enabled by default]
     */    
    printf("%d\n", ui64!=0xFFFFFFFFFFFFFFFF);

    ui64 = 0xFFFFFFFFFFFFFFFF;    
    ui64 &= ~((uint64_t)1<<60); /* OK */
    printf("%d\n", ui64!=0xFFFFFFFFFFFFFFFF);

    ui64 = 0xFFFFFFFFFFFFFFFF;
    ui64 &= ~(1LL<<60); /* auch OK, und kürzer */
    printf("%d\n", ui64!=0xFFFFFFFFFFFFFFFF);

    ui64 = 0xFFFFFFFFFFFFFFFF;
    ui64 &= ~(1<<bit); /* Ohne Warnung, und funktioniert manchmal, je nach Optimierung */
    printf("%d\n", ui64!=0xFFFFFFFFFFFFFFFF);

    return 0;
}
/* Ausgabe auf meinem PC ohne Optimierung:
 * sizeof(int)=4
 * 0
 * 1
 * 1
 * 1
 * Ausgabe auf meinem PC mit -O2
 * 0
 * 1
 * 1
 * 0
 */

Niederwertigstes gesetztes Bit löschen (Standard-C)

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

Beispiel: 01101000 → 01100000

uint8_t byte;

byte = irgendwas();

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

Beispiel:

Byte  :   01101000
Byte-1:   01100111
Ergebnis: 01100000

Das funktioniert also mit jeder beliebigen Zahl.

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

uint8_t pareven(uint8_t byte) {
  uint8_t par = 0;

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

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: to toggle). Wenn in einem Byte mehrere Bits invertiert („getoggelt“) werden sollen, wird dies durch eine XOR-Verknüpfung erreicht. Alle Bits, welche in der Bitmaske gleich „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.

  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 zu BitReady
ClrBitNow:
  cbi  PortB, 0    ; Lösche Bit 0 in PortB
BitReady:

Noch kürzer geht's 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 acht Bits gleichzeitig zu toggeln. Diese Methode ist natürlich auch auf normale Daten anwendbar, nicht nur auf IO-Ports.

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

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

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

ARMv6-M (Cortex-M0)

Das Invertieren von Bits geschieht über die eors-Instruktion, hier muss wieder wie zuvor eine Bitmaske mit den zu invertierenden Bits geladen werden:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
eors r0, r3      /* Invertiere bits 4-7 in r0 */

Version mit ldr für beliebige Bitmasken:

ldr r3, =#0xF00000F0   /* Lade literal-Wert mit den bits 4-7 und 28-31 gesetzt */
eors r0, r3            /* Invertiere bits 4-7 und 28-31 in r0 */
/* ... Mehr Code ... */
bx lr                  /* Funktions-Rücksprung */
.ltorg                 /* Hier alle zuvor benötigten "langen" Literal-Werte in den Flash legen. Darf nicht als Code ausgeführt werden. */

Wenn die zu invertierenden Bits in einem einzelnen Byte darstellbar sind, kann man sich mit der Kombination movs + lsls behelfen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
lsls r3, #24     /* Um 24bits nach links shiften - Ergebnis ist dann 0xF0000000 */
eors r0, r3      /* Invertiere bits 28-31 in r0 */

Falls sich nur der negierte Wert der Bitmaske als einzelnes Byte darstellen lässt, kann man sich mit der Kombination movs + mvns behelfen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
mvns r3, r3      /* r3 negieren - Ergebnis ist dann 0xFFFFFF0F */
eors r0, r3      /* Invertiere alle bits außer 4-7 in r0 */

Kombination mit lsls falls die nicht zu setzenden Bits in einem Byte an einer beliebigen Position im Worts stehen:

movs r3, #0xF0   /* Lade literal-Wert mit den bits 4-7 gesetzt */
lsls r3, #24     /* Um 24 bits nach links schieben - Ergebnis ist dann 0xF0000000 */
mvns r3, r3      /* r3 negieren - Ergebnis ist dann 0x0FFFFFFF */
eors r0, r3      /* Invertiere alle bits außer 28-31 in r0 */

ARMv7-M (Cortex-M3/M4/M7)

Bei ARMv7-M basierten Prozessoren gibt es wieder erweiterte Instruktionen welche Bitmaske und Operation kombinieren:

Die eor-Instruktion kann hier direkt die Bitmaske als Immediate-Wert enthalten:

eor r0, r3, #0xF0      /* Invertiere bits 4-7 in r0 */

Hier besteht wieder die Wahl zwischen eor und eors zum Behalten oder Überschreiben der Status-Flags.

Es gibt jedoch kein orn-Äquivalent zu eor, welches die Bitmaske invertiert. Es spielt jedoch keine Rolle, ob einer der Eingabewerte oder der Ausgabewert an eor negiert wird. Daher kann dies mit mvn kombiniert werden, wenn sich nur der negierte Wert der Bitmaske als Immediate darstellen lässt:

eor r0, r3, #0x00F00000      /* Invertiere die bits 20-23 in r0 */
mvn r0, r0                   /* Invertiere alle bits in r0; im Endeffekt werden alle bits außer 20-23 invertiert */

Für komplexe Bitmasken kann wie zuvor (siehe oben) beim Cortex-M0 auch die Kombination aus ldr und eors genutzt werden.

Standard-C

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

Neuere ATmegas

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

sbi PIND, 2       ; Bit 2 von Port D toggeln

8051er

cpl bitadresse

Bits prüfen

Will man prüfen, ob ein oder mehrere Bits in einer Variablen 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 eine „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.

; 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

ARMv6-M (Cortex-M0)

Zum Prüfen von einzelnen Bits wird auf ARM-Prozessoren im Wesentlichen die lsls -Instruktion in Kombination mit bpl/bmi genutzt. Die Idee ist, das fragliche Bit an die höchste Stelle zu schieben, und dann das N -Flag im Status-Register für einen bedingten Sprung zu nutzen. Das N-Flag wird von arithmetischen Operationen automatisch mit dem höchsten Bit des Ergebnisses überschrieben - ist das fragliche Bit gesetzt, gilt der bit-geschobene Wert als negativ, sonst positiv, weshalb die bedingten Sprünge für negativ/positiv genutzt werden können:

lsls r3, r0, #7  /* Wert um 7 bits nach links schieben, N-Flag setzen */
bmi BitGesetzt   /* Bedingter Sprung, falls bit gesetzt ist */
bpl BitNichtGesetzt /* Alternative: Bedingter Sprung, falls bit nicht gesetzt ist */

Sollen mehrere Bits auf einmal geprüft werden, kann die tst-Instruktion genutzt, welche fast identisch zur bereits gezeigten ands -Instruktion ist, aber das Ergebnis verwirft und nur die Status-Flags setzt. Hier müssen die üblichen Maßnahmen für das Laden von Literals getroffen werden:

mov r3, 0xF0     /* Wert 0xF0 nach r3 laden */
tst r0, r3       /* Bitweises und berechnen, Ergebnis verwerfen, Status-Flags setzen */
bne BitGesetzt   /* Bedingter Sprung, falls eines der Bits 4-7 gesetzt ist */
beq BitNichtGesetzt /* Alternative: Bedingter Sprung, falls keines der Bits gesetzt ist */

ARMv7-M (Cortex-M3/M4/M7)

Beim ARMv7-M wird hauptsächlich auch die Variante mit lsls genutzt. Zum prüfen mehrerer Bits gibt es eine Version von tst welche die zu prüfende Bitmaske als Immediate-Wert enthält:

tst r0, #0xF0       /* Bitweises und zwischen r0 und #0xF0 berechnen, Ergebnis verwerfen, Status-Flags setzen */
bne BitGesetzt      /* Bedingter Sprung, falls eines der Bits 4-7 gesetzt ist */
beq BitNichtGesetzt /* Alternative: Bedingter Sprung, falls keines der Bits gesetzt ist */

Standard-C

    // prüfe, ob Bit 4 in der Variablen 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 Variablen tmp gelöscht sind
    // die Klammer ist wichtig!
    if ((tmp & 0x11) == 0) {
       // hier die Anweisungen, wenn beide Bits gelöscht sind
    }

    // prüfe, ob Bit 0 oder Bit 4 in der Variablen 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 Variablen tmp gelöscht sind
    if (~tmp & 0x11) {
       // hier die Anweisungen, wenn mindestens ein Bit gelöscht ist
    }

    // prüfe, ob Bit 4 in der Variablen tmp gesetzt ist
    if (tmp & 0x10) {
       // hier die Anweisungen, wenn das Bit gesetzt ist
    }

    // prüfe, ob Bit 0 und Bit 4 in der Variablen tmp gesetzt sind
    // die Klammer ist wichtig!
    if ((tmp & 0x11) == 0x11) {
       // hier die Anweisungen, wenn beide Bits gesetzt sind
    }

Hilfsfunktionen zur Bitmanipulation in C/C++

Um "einfacher" elementare Bitmanipulationen durchzuführen bietet es sich an einige Hilfsfunktionen zu definieren. Dabei gibt es zwei verschiedene Möglichkeiten diese zu realiseren:

  • Als C-Makro C_Makros
  • Als Inline-Funktion

In beiden Fällen wird bei eingeschalteter Optimierung letztendlich vom Compiler ein sehr kompakter (und identischer!) Code erzeugt, jedoch ist dringend von der Verwendung von Makros abzuraten (siehe Makro )! Im Fehlerfall zeigt der Compiler bei der Verwendung vom Makros keine eindeutigen Fehlermeldungen an, da es sich um simple Ersetzungen handelt - bei der Verwendung von Inline-Funktionen hingegen gibt es eine "brauchbare" Fehlermeldung!

Beispiele - Inline Variante

// Achtung: Zugriffe erfolgen über Pointer
// PORTA, PB2 setzen
BIT_SET(&PORTA, PB2);

// PORTC, PB0 löschen
BIT_CLEAR(&PORTC, PB0);

// PORTA, PB2 direkt setzen
// HIGH
BIT_BOOL_SET(&PORTA, PB2, 1);

// LOW
BIT_BOOL_SET(&PORTA, PB2, 0);

Beispiele - MakroVariante

// Achtung: Zugriffe erfolgen direkt über die Variablen/Portnamen
// PORTA, PB2 setzen
BIT_SET(PORTA, PB2);

// PORTC, PB0 löschen
BIT_CLEAR(PORTC, PB0);

Um die Hilfsfunktionen verwenden zu können einfach folgenden Code in eine neue Header-Datei (z.B. BitIO.h) kopieren:

Hilfsfunktionen als Inline-Methoden

Achtung: Wenn nur ein C Compiler verwendet wird, kennt dieser den Typ "bool" nicht, dieser muss dann vorher definiert werden!

/**
 *  BitIO.h
 *	@author 	Andi Dittrich <http://andidittrich.de>
 *	@version	1.0
 *	@license	MIT Style X11 License
*/


#include <inttypes.h>

#ifndef BITIO_H_
#define BITIO_H_

// set bit
static inline void BIT_SET(volatile uint8_t *target, uint8_t bit) __attribute__((always_inline));
static inline void BIT_SET(volatile uint8_t *target, uint8_t bit){
	*target |= (1<<bit);
};

// set clear
static inline void BIT_CLEAR(volatile uint8_t *target, uint8_t bit) __attribute__((always_inline));
static inline void BIT_CLEAR(volatile uint8_t *target, uint8_t bit){
	*target &= ~(1<<bit);
};

// bit toogle
static inline void BIT_TOGGLE(volatile uint8_t *target, uint8_t bit) __attribute__((always_inline));
static inline void BIT_TOGGLE(volatile uint8_t *target, uint8_t bit){
	*target ^= (1<<bit);
};

// set bit by boolean
static inline void BIT_BOOL_SET(volatile uint8_t *target, uint8_t bit, bool enable) __attribute__((always_inline));
static inline void BIT_BOOL_SET(volatile uint8_t *target, uint8_t bit, bool enable){
	if (enable){
		BIT_SET(target, bit);
	}else{
		BIT_CLEAR(target, bit);
	}
};

#endif /* BITIO_H_ */

Hilfsfunktionen als C-Makro (nicht empfohlen)

/* Bit setzen */
#define set_bit(var, bit) ((var) |= (1 << (bit)))
 
/* Bit löschen */
#define clear_bit(var, bit) ((var) &= (unsigned)~(1 << (bit)))
 
/* Bit togglen */
#define toggle_bit(var,bit) ((var) ^= (1 << (bit)))

Bitmanipulation beim MSP430

Beim MSP430 und dessen Compilern sind die Bitnamen meist anders definiert. Und zwar nicht als Bitnummer, sondern als Bitmuster. Darum schreibt man dort die Bitzugriffe in C anders. Das kann auch bei anderen Mikrocontrollern bzw. C-Compilern so sein. Wichtig ist, dass man seine eignen Definitionen in der gleichen Weise wie der Compiler anlegt, um Verwirrung zu vermeiden, siehe Strukturierte Programmierung auf Mikrocontrollern

// Definition von Bitnamen in den Headerfiles des Compilers

#define PD4 4               // Definition im AVR GCC als Bitnummer
#define PD5 5

#define P14 (1<<4)          // Definition im MSP430 GCC als Bitmuster
#define P15 (1<<5)

// Bitmanipulation im Programm
 
   DDRD = (1<<PD5) | (1<<PD4);   // AVR GCC
   P1DIR = P15 | P14;            // MSP430 GCC

Siehe auch

Weblinks