Bitmanipulation
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 << 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)
.|
: Das Ergebnis wird bitweise ODER-verknüpft, also 0b00000001 oder 0b00000100 wird zu 0b00000101.|=
: 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. B. in stdint.h */
uint64_t reg_64; /* uint64_t definiert per typedef z. 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 << 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)
.|
: Das Ergebnis wird bitweise ODER-verknüpft, also 0b00000001 oder 0b00000100 wird zu 0b00000101.~
: Der Wert in der Klammer wird bitweise invertiert, aus 0b00000101 wird 0b11111010.&=
: 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
- Forumsbeitrag: Bits aus einem Array extrahieren
- Forumsbeitrag: Bits für ein Schieberegister zusammenstellen, TLC5941
- Forumsbeitrag: TLC5947 und ATmega16 Bitmanipulation
- Forumsbeitrag: Bitreihenfolge ändern
- Forumsbeitrag: Übersichtliche Abfrage von vielen Pins
- Forumsbeitrag: Verschiedene Möglichkeiten der Bitmanipulation bei IO-Registern
Weblinks
- AVR Built-in Functions – spezielle Funktion im avr-gcc zur schnellen Bitvertauschung (englisch)
- Sean Eron Anderson: Bit Twiddling Hacks – Sammlung von komplexeren Bitmanipulationen (englisch)
- Günther Jena: Bitmanipulation mit C – mit Übungen