Über den Beitrag
In vielen Beiträgen, so auch in meinem letzten über IR Fernbedienungen, habe ich Binärlogik und Portmanipulationen verwendet. Damit meine ich Ausdrücke wie z. B. PORTD |= (1<<PD2)
anstelle von digitalWrite(2, HIGH)
. In diesem Beitrag möchte ich nun einmal gesondert auf dieses Thema eingehen.
Zugegebenermaßen ist das ein wenig trocken. Interessant wird es eigentlich erst dann, wenn man es mit einer Anwendung verbindet. In den anwendungsbezogenen Beiträgen war aber bisher kein Platz dafür. Betrachtet diesen Beitrag also als eine Art Referenz, auf die ich verweisen werde, wenn ich mal wieder Binärlogik und Port Manipulation verwende.
Ich beginne mit der Binärlogik und gehe dann auf die Ports des ATmega328P und die Portmanipulation ein. Dann kommt eine kleine praktische Übung. Zum Schluss gibt es einen Geschwindigkeitsvergleich und ich beantworte die Frage, warum die Portmanipulation so viel schneller ist.
Warum sind Binärlogik und Portmanipulationen wichtig?
Portmanipulation, also der direkte Zugriff auf Pins des Arduino (oder andere Boards oder Microcontroller) über seine Ports ist wesentlich schneller als die gewohnten Arduino Funktionen. Meistens spielt das keine Rolle, manchmal aber schon. Und die Binärlogik mit ihren Bitoperatoren ist dafür das Rüstzeug. Die Grammatik sozusagen.
Eigentlich mag ich den Begriff „Portmanipulation“ nicht besonders. Es klingt irgendwie nach Verbotenem. Im Grunde ist es aber die natürlichere Art Mikrocontroller zu programmieren. digitalWrite
, pinMode
und Co hingegen vernebeln eigentlich den Blick darauf, was auf Hardwareebene tatsächlich passiert.
Die Binärlogik kommt auch dann ins Spiel, wenn ihr euch allgemein mit der Programmierung von Registern beschäftigt, wie z.B. in Sensoren. Dabei hat man es oft mit Aufgaben zu tun wie z.B. „schreibe eine 101“ in die Bits 3-5 des Registers XY. So etwas ist nur mit Binärlogik vernünftig abbildbar.
Binärlogik: die Bitoperatoren
Logische Bitoperatoren und Shiftoperatoren
Die logischen Bitoperatoren sind:
- & logisches UND
- | logisches ODER
- ^ exklusives ODER (XOR)
- ~ Negation (NICHT)
Ein wesentlicher Unterschied zu den gewohnten Operatoren wie z.B. Plus oder Minus ist, dass die Bitoperatoren bitweise angewendet werden.
Die Shiftoperatoren sind:
- >> Rechtsshift
- << Linksshift
Bitoperator UND (&)
Der Bitoperator UND prüft, ob die beiden Operanden 1 (true) sind. Ist das der Fall, dann ist das Ergebnis 1, sonst 0 (false).
0 & 0 = 0 1 & 0 = 0 0 & 1 = 0 1 & 1 = 1
Anwendung auf ein ganzes Byte:
0b10011100 & 0b01010111 = 0b00010100
Bitoperator ODER (|)
Der Bitoperator ODER prüft, ob mindestens einer der beiden Operanden 1 (true) ist. Wenn ja, dann ist das Ergebnis 1, sonst 0.
0 | 0 = 0 1 | 0 = 1 0 | 1 = 1 1 | 1 = 1
Anwendung auf ein ganzes Byte:
0b10011100 | 0b01010111 = 0b11011111
Bitoperator XOR (^)
Der Bitoperator XOR prüft, ob genau einer der beiden Operanden 1 ist. Wenn ja, ist das Ergebnis 1, sonst 0. Im Gegensatz zum ODER liefert dieser Operator also auch dann 0, wenn beide Operanden 1 sind.
0 ^ 0 = 0 1 ^ 0 = 1 0 ^ 1 = 1 1 ^ 1 = 0
Anwendung auf ein ganzes Byte:
0b10011100 ^ 0b01010111 = 0b11001011
Bitoperator NICHT (~)
Der Bitoperator NICHT hat nur einen Operanden. NICHT kehrt den Wert um, aus 1 (true) wird also 0 (false) und umgekehrt.
~0 = 1 ~1 = 0
Anwendung auf ein ganzes Byte:
~0b10011100 = 0b01100011
Shiftoperatoren
Shiftoperatoren verschieben einen Wert bitweise nach rechts oder links. Eine Verschiebung um x Stellen nach links entspricht dabei einer Multiplikation mit 2x. Eine Verschiebung um x Stellen nach rechts entspricht einer Division durch 2x.
Bei der Verschiebung nach rechts fallen alle Stellen weg, die hinter die erste Stelle (LSB = least significant bit) verschoben werden. Bei der Verschiebung nach links fallen alle Stellen weg, die sich außerhalb des Wertebereichs der Variable befinden (jenseits des MSB = most significant bit).
(0b1 << 1) = 10 (0b101 << 1) = 1010 (0b111 >> 1) = 11 (0b11110000 << 1) = 0b11100000 // wenn 0b11110000 als byte definiert wurde
Dabei gibt es ein paar Stolperfallen. Überlegt einmal welche Ausgabe der folgende Sketch gibt:
byte a,b,c; unsigned int d; int e; void setup() { Serial.begin(9600); a = 0b1111; b = a<<5; c = a>>1; d = a<<5; e = a<<12; Serial.print("a = "); Serial.println(a, BIN); Serial.print("b = "); Serial.println(b, BIN); Serial.print("c = "); Serial.println(c, BIN); Serial.print("d = "); Serial.println(d, BIN); Serial.print("e = "); Serial.println(e, BIN); Serial.print("e(dezimal) = "); Serial.println(e); } void loop() {}
Hier die (erwartete?) Auflösung bei Verwendung von AVR basierten Boards. Verwendet ihr ein SAMD oder ESP32 Board, dann ersetzt einmal e = a<<12
durch e = a<<28
und schaut was passiert.
Die Ports des Arduino UNO
Der Arduino UNO besitzt vierzehn digitale I/O Pins (0 – 13) und die sechs „quasi-analogen“ I/O Pins A0 bis A5. Schaut ihr ins Datenblatt des ATmega 328 P, dem Herz des Arduino, findet ihr die Pins in einer anderen Organisationsstruktur wieder.
Die I/O Pins sind in den Ports B, C und D organisiert. Die Pinbezeichnungen sind entsprechend PBx, PCx und PDx mit x = 0 bis maximal 7. Zu diesen Gruppen gibt es je drei 8 Bit Register, auf die ich gleich näher eingehen werde, nämlich DDRx, PORTx und PINx mit x = B, C oder D.
Die Pins PB6 und PB7 sind bei Verwendung des Arduino UNO nicht zugänglich, da der 16 MHz Taktgeber fest verdrahtet dranhängt. PC6 ist auf dem Arduino Board als Reset festgelegt und entsprechend nicht als I/O Pin zugänglich. PC7 existiert schlicht nicht.
Die Richtungsregister DDRx
Wollt ihr einen I/O Pin als Input oder Output nutzen, dann stellt ihr das in der „Arduino Sprache“ über die pinMode
Funktion ein. Im ATmega 328 P werden dabei die entsprechenden Bits im zuständigen Richtungsregister (DDR = Data Direction Register) gesetzt. Hier stellvertretend die Struktur des Richtungsregisters für den Port B:
Als Beispiel möchte ich den digitalen Pin 13 des Arduinos auf OUTPUT setzen. Dieser entspricht gemäß dem Pinout Schema von oben dem Pin PB5. Das bedeutet, dass das Bit Nr. 5 im Register DDRB gesetzt werden muss. Der Zugriff auf das Register ist denkbar einfach. Es reicht folgende Zuweisung:
DDRB = 0b100000
oder DDRB = 0x20
oder DDRB = 32
Dieser Zugriff ist deshalb so einfach möglich, weil „DDRB“ über eine #define
Anweisung in den AVR Bibliotheken (avr/io.h –> avr/iom328p.h) die notwendigen Anweisungen enthält:
#define DDRB _SFR_IO8(0x04)
Möchtet ihr mehrere Pins des Ports B auf OUTPUT setzen, z. B. Pin 5 und Pin 3, dann sieht die Anweisung folgendermaßen aus:
DDRB = 0b101000
, oder hexadezimal: DDRB = 0x28
, oder dezimal: DDRB = 40
Die Port Daten Register PORTx
Wollt ihr einen I/O Pin, den ihr zuvor auf OUTPUT gesetzt habt, nun in den Zustand HIGH versetzen, dann greift ihr im Rahmen der Portmanipulation auf das zuständige Port Daten Register (PORTx) zu, hier PORTB als Beispiel:
Ein Pin ist HIGH, wenn ihr das entsprechende Bit gesetzt habt. Um beim obigen Beispiel vom Arduino Pin 13 zu bleiben:
PORTB = 0b100000
entspricht also digitalWrite(13, HIGH)
Halt, stopp! Natürlich gilt die Entsprechung nur in Bezug auf den Pin 13 (PB5), denn die erste Anweisung schaltet alle anderen PORTB Pins auf LOW. Dahingegen ist digitalWrite
selektiv.
Das Port Input Pin Register PINx
Den Zustand, also LOW oder HIGH, eines Input Pins könnt ihr über das entsprechende PINx (x = B, C, D) Register abfragen. Für Arduino Pin 13 zum Beispiel:
PINB == 0b100000
anstelle von digitalRead(13)
Wieder gilt die Einschränkung, dass digitalRead selektiv ist, wohingegen die ob PINB Abfrage in dieser Form prüft, ob an PINB nur PB5 HIGH ist.
Einsatz der Binärlogik bei der Portmanipulation
Selektives Setzen von Bits
Um nun ein einzelnes oder mehrere Bits selektiv zu setzen, verwendet ihr das logische ODER. Folgendermaßen könnt ihr zum Beispiel PB5 auf HIGH zu setzen, ohne die restlichen Pins des PORTB zu beeinflussen:
PORTB = PORTB | 0b100000
bzw. PORTB |= 0b100000
oder, bevorzugt:
PORTB |= (1<<PB5)
PB5 ist über ein #define schlicht als 5 definiert. Man könnte also genauso gut PORTB |= (1<<5)
schreiben. Ihr könntet PB5 sogar durch PC5 oder PD5 ersetzen. Gut lesbar wäre das natürlich nicht.
Mehre Bits, zum Beispiel PB5 und PB3, setzt ihr folgendermaßen:
PORTB |= (1<<PB5) | (1<<PB3)
, da
(1<<PB5) | (1<<PB3) = 0b100000 | 0b1000 = 0b101000
Gelegentlich findet man auch den Ausdruck _BV(x) anstelle von (1<<x). Dabei steht „BV“ für Bitvalue. Beides ist identisch wie ein Blick in die Bibliotheksdatei sfr_defs.h verrät:
#define _BV(bit) (1 << (bit))
Selektives Löschen von Bits
Auch das selektive Löschen von Bits ist sehr einfach, es erschließt sich aber vielleicht nicht unbedingt auf den ersten Blick. Ich nehme wieder PB5 als Beispiel:
PORTB &= ~(1<<PB5)
, gleichbedeutend mit:
PORTB &= ~(0b100000)
bzw. PORTB &= 0b11011111
Ihr könnt natürlich auch mehrere Bits gleichzeitig löschen, hier zum Beispiel PB3 und PB5:
PORTB &= ~((1<<PB5)|(1<<PB3))
Selektives Invertieren von Bits
Für das Invertieren einzelner Bits eignet sich das logische XOR. Dabei macht man sich zunutze, dass ein ^1
aus einer 0 eine 1 macht und umgekehrt. Ein ^0
hingegen verhält sich neutral. So z. B. invertiert ihr PB5 selektiv:
PORTB ^= (1<<PB5)
Oder für mehrere Bits, die invertiert werden sollen:
PORTB ^= (1<<PB5)|(1<<PB3)
Wenn ihr einen ganzen Port invertieren wollt, kommen zwei Varianten infrage:
PORTB = ~PORTB
oder PORTB ^= 0b11111111
Selektives Abfragen von Pinzuständen
Das ist jetzt keine Überraschung mehr. Der Ausdruck
PINB & (1<<PB5)
liefert 0, also false, wenn PB5 LOW ist. Ist PB5 HIGH, dann ist der Ausdruck ungleich 0, also true. Der genaue Wert, hier 0b100000, also 32, ist dabei nicht von Belang.
Ein kleiner Übungssketch
Nun könnt ihr das ganze einmal praktisch ausprobieren, wenn ihr wollt. Dazu baut folgende Schaltung auf:
Dann probiert den folgenden Sketch aus und spielt ein bisschen damit herum. Passiert das, was ihr erwartet?
int dTime = 2000; //delay time void setup(){ DDRD = 0xFF; } void loop() { PORTD = 0b10101010; delay(dTime); PORTD |= (1<<PD6); delay(dTime); PORTD ^= (1<<PD6)|(1<<PD7); delay(dTime); PORTD |= (1<<PD0)|(1<<PD2)|(1<<PD4); delay(dTime); PORTD = (PORTD >> 3); delay(dTime); PORTD &= ~((1<<PD0)|(1<<PD2)); delay(dTime); PORTD = ~PORTD; delay(dTime); }
Portmanipulation an anderen Microcontrollern
Die Portmanipulation auf andere Microcontroller zu übertragen ist einfach, zumindest was AVR Vertreter wie ATmegas und ATtinys angeht. Ein Blick ins Datenblatt auf das Pinout Schema reicht meistens. Die ATtinys 25 / 45 / 85 haben beispielsweise nur einen PORTB mit den Pins PB0 bis PB5:
Bei Nicht-AVR Microcontrollern kann sich der direkte Portzugriff stark unterscheiden. Im Falle des ESP8266 ESP01 gibt es beispielsweise ein Register zum Setzen der Bits und ein anderes zum Löschen:
GPOS |= (1<<Pin)
für HIGH, bzw. GPOC |= (1<<Pin)
für LOW
Ein kleiner Geschwindigkeitstest
Portmanipulation vs. digitalWrite
Wie viel Unterschied macht nun die Portmanipulation gegenüber digitalWrite
aus? Um diese Frage zu klären, habe ich den folgenden Minisketch verwendet und mir das Ergebnis beider Methoden am Oszilloskop angeschaut.
void setup() { DDRD = 0xFF; } void loop() { //PORTD |= (1<<PD3); //PORTD &= ~(1<<PD3); digitalWrite(3, HIGH); digitalWrite(3, LOW); }
Ein HIGH/LOW Zyklus wird bei Verwendung von digitalWrite
mit einer maximalen Frequenz von 113 kHz durchlaufen, mit Portmanipulation sind es 2 MHz, also gute 17 Mal schneller. 2 MHz bedeutet, dass lediglich 8 Prozessortakte Takte pro Durchlauf benötigt werden.
Portmanipulation vs. digitalRead
Um die Geschwindigkeit von digitalRead
vs. PINx &= (1<<Pin)
zu ermitteln, habe ich beides 100.000 Mal angewendet und die dafür benötigte Zeit ermittelt.
unsigned long numberOfReads = 100000; unsigned long startTime = 0; unsigned long readTimeLength = 0; bool pinStatus; void setup() { Serial.begin(9600); pinMode(5,INPUT); } void loop() { startTime = millis(); for(unsigned long i=0; i<numberOfReads; i++){ pinStatus = digitalRead(5); } readTimeLength = millis() - startTime; Serial.print("digitalRead: "); Serial.print(readTimeLength); Serial.print(" ms "); Serial.print(" / "); delay(1000); startTime = millis(); for(unsigned long i=0; i<numberOfReads; i++){ pinStatus = (PIND & (1<<PD5)); } readTimeLength = millis() - startTime; Serial.print("PIND Abfrage: "); Serial.print(readTimeLength); Serial.println(" ms"); delay(5000); }
Das Ergebnis:
Die direkte PIND Abfrage ist also gute achtmal schneller als digitalRead
. Teilen durch 100000 ergibt, dass die digitalRead
Funktion ca. 3,5 µs benötigt, das direkte Abfragen hingegen nur ca. 0,44 µs. Ganz exakt ist das natürlich nicht, da auch die Schleifenbearbeitung selbst etwas Zeit benötigt. Es zeigt aber zumindest, dass der Unterschied erheblich ist.
Woher kommt der Unterschied?
Der Unterschied in der Geschwindigkeit liegt darin begründet, dass digitalRead
und digitalWrite
einiges mehr leisten als die einfachen Portmanipulationsfunktionen. Die zusätzlichen Maßnahmen, die digitalRead
und digitalWrite
treffen, machen die Funktionen robuster gegenüber Fehlern. Wenn ihr den Geschwindigkeitsgewinn durch Portmanipulation nicht braucht, dann bleibt deshalb auch gerne bei digitalRead
und digitalWrite
.
Hier als Beispiel die digitalWrite
Funktion, die ihr übrigens in der Datei Arduino\hardware\arduino\avr\cores\arduino\wiring_digital.c findet:
void digitalWrite(uint8_t pin, uint8_t val) { uint8_t timer = digitalPinToTimer(pin); uint8_t bit = digitalPinToBitMask(pin); uint8_t port = digitalPinToPort(pin); volatile uint8_t *out; if (port == NOT_A_PIN) return; // If the pin that support PWM output, we need to turn it off // before doing a digital write. if (timer != NOT_ON_TIMER) turnOffPWM(timer); out = portOutputRegister(port); uint8_t oldSREG = SREG; cli(); if (val == LOW) { *out &= ~bit; } else { *out |= bit; } SREG = oldSREG; }
Danksagung
Den Arduino im Beitragsbild habe ich von Daan Lenaerts auf Pixabay. Die Nullen und Einsen (die ich allerdings ausgeschnitten, eingefärbt und als Ebene eingefügt habe) habe ich Gerd Altmann zu verdanken, auch Pixabay.
Hallo Wolfgang,
deine Seiten sind sehr hilfreich, danke dafür.
Kann es sein, dass sich ein Schreibfehler eingeschlichen hat? Beim 328er ist
DDRB = 0b100000
doch Port PB7 und nicht PB5 ?
Nur falls Anfänger darüber stolpern…
Hallo Roland,
nein, das hat schon seine Richtigkeit. DDRB = 0b1 bringt PB0 auf OUTPUT, bei 0b10 wäre es PB1, usw. Du kannst es ja einfach mal ausprobieren.
Aber trotzdem vielen Dank, dass Du mich auf einen Fehler aufmerksam machen wolltest.
VG, Wolfgang
Hallo Wolfgang,
zunächst einmal ein großes Lob für die informativen Seiten!
Meine Frage ist folgende:
In dem Beispelsketch „Portmanipulation_test.ino“ wird mit der Anweisung
DDRD = 0xFF;
auch Pin 0 (RX) als Ausgang definiert.
Viele Autoren warnen dringend davor, dies zu tun, da es dann (angeblich?) keine Möglichkeit mehr gäbe, Uploads von anderen Sketches durchzuführen, da wegen der falschen Einstellung von RX keine serielle Kommunikation mehr möglich sei. Zur Untermauerung dieser These wird der Link http://www.arduino.cc/reference/en/PortManipulation angegeben. Allerdings läuft dieser Link ins Leere.
Da du keine diesbezügliche Warnung gibst, gehe ich davon aus, dass es einen Mechanismus gibt, der bei einem Reset dafür sorgt, dass Pin 0 (RX) wieder korrekt als Eingang konfiguriert wird.
Wie funktioniert das genau?
Gibt es in dieser Hinsicht eine Änderung bei neuen Arduino-Boards gegenüber früheren Versionen, sodass die Warnung mittlerweile obsolet ist?
Viele Grüße
Benno
Hallo Benno, die Sorge ist unbegründet, auch wenn ich sie prinzipiell verstehen kann. In der Tat ist der RX Pin am Upload von Sketchen beteiligt. Beim Upload Prozess wird aber auch ein Reset durchgeführt und der lässt den Atmega328P alle Einstellungen vergessen. Der Grundzustand aller Pins ist INPUT. Und mir wäre neu, dass das irgendwann mal anders gewesen wäre.
Um Restzweifel auszuräumen: Ich habe den Sketch natürlich selbst auch ausprobiert und hätte das Problem bemerkt.
Was ein Problem ist, ist den RX Pin auf GND oder HIGH zu ziehen. Du kannst ja mal den RX Pin über einen 330 Ohm Widerstand mit GND verbinden. Dann bekommst du in der Tat keinen Sketch mehr auf den Arduino. Wenn du die Verbindung wieder trennst, funktioniert es wieder. Vielleicht sind diejenigen, die die Bedenken geäußert haben damit durcheinander gekommen.
Letzte Bemerkung: wenn sich tatsächlich mal jemand ausschließen sollte, z.B. weil er sein Projekt so verdrahtet hat, das RX hoch- oder runtergezogen ist, dann kann er den Atmega328P immer noch per ISP programmieren. Das geht mit einem Programmer oder einem zweiten Arduino:
https://wolles-elektronikkiste.de/atmega328p-standalone-betreiben
VG, Wolfgang
Hallo,
ich hätte eine Frage zu einem Projekt welches ich gerade als SHK an der Uni mache und bei der ich gerade beim Thema Binärlogik und Portmanipulation scheitere.
Ich möchte mit einer PS2 Mouse interagieren, da jedoch die vorhandenen Librarys dazu alle nicht mit meiner Hardware funktionieren habe ich versucht den mit dem Logic analyzer aufgenommenen Algorithmus zwischen Host und Device zu emulieren.
Nun meine Frage:
Ich möchte die Daten, die am Dataport ankommen bitweise printen. Am besten wäre auch noch eine hexadezimale Darstellung.
dazu habe ich folgende Funktion geschrieben:
void printBinaryDigits(){
for (int i = 0; i <= 7; i++){
Serial.println(data_pin,BIN);
delayMicroseconds(ein_bit);
}
}
die leider nicht funktioniert.
Wie könnte man das besser/eleganter lösen?
Vielen Dank für jegliche Hilfe im Voraus.
Johanna Hoppe
Hallo Johanna,
ich denke, da brauche ich – oder auch andere, die deine Frage lesen – etwas mehr Informationen. Ich muss allerdings auch dazu sagen, dass ich bisher nicht mit einem Logic Analyzer gearbeitet habe.
Was deine Funktion macht, ist dass sie sieben mal hintereinander den Wert von data_pin als binären Wert ausgibt. Da sich der Wert von data_pin nicht ändert bekommst du sieben mal denselben Wert. Ist data_pin der Pin, an dem Daten bitweise ankommen? Ich vermute mal du möchtest die Daten, die bitweise am data_pin ankommen, in Bytes zusammenfassen, richtig? Wie liest du den Pin denn aus? Du kannst mir auch mal den ganzen Sketch an wolfgang.ewald@wolles-elektronikkiste.de senden.
VG, Wolfgang
Hallo,
ich bin in kurzer Zeit jetzt zum zweiten Mal durch eine Google Suche zu verschiedenen Stichworten auf dieser Seite gelandet, und ich muß sagen, ich bin von den dargebotenen Informationen beeindruckt. Alles wird in gut verdaubarer Form und mit machbarer Lernkurve, grafisch gut unterstützt, erläutert. Das hilft sehr, vor allem jemand wie mir :-), der die Grundprinzipien gut kennt; wenn’s dann mal selten ans Machen geht, muß man doch mal schnell die Details nachsehen. Das geht hier super! Danke für dieses Engagement.
Viele Grüße, Thomas Peterreins.
Vielen Dank!
Vielen Dank für Ihre Antwort, so habe ich es auch probiert, und es funktioniert auch. Ich hatte ursprünglich ein Unterprogramm der Art
void up(byte *ddr,byte *port,byte pin) { … }
im Sinn, das dann mit
up(&DDRB,&PORTB,5);
aufzurufen wäre, aber die Arduino-IDE lässt ein „&DDRB“ nicht zu (obwohl die Port-Register im SRAM gespiegelt sind, wie man liest).
Zum Beispiel Shiftoperatoren. Du schreibst:
a = 0b1111;
e = a<<12;
Ich hätte jetzt erwartet das e = 0b 1111 0000 0000 0000 ist da 1111 um 12 Stellen nach links geschoben wird. Aber warum sind es dann e = 0b 1111 1111 1111 1111 1111 0000 0000 0000 ? Wo kommen die zusätzlichen Einsen her?
e ist als integer definiert, d.h. es hat einen Wertebereich von -2^15 bis +2^15. Jedenfalls ist das auf den AVR basierten Arduino Boards so. Das 16. Bit ist das Vorzeichenbit. Wenn du in das Vorzeichenbit „hineinshiftest“, dann passieren nicht unbedingt vorhersehbare Dinge. Ein Shift von a um 11 geht gerade noch. Oder wenn du e als unsigned int definierst dann bekommst du auch das erwartete Ergebnis.
Wenn du einen ESP32 oder ein SAMD basiertes Arduino Board nimmst dann funktioniert a<<12 wieder korrekt (darauf sollte ich auch noch einmal hinweisen). Denn dort hat ein Integer eine Größe von +/-2^31 Bit, d.h. du kommst dem Vorzeichenbit nicht in die Quere. Das Problem tritt dann erst bei a<<28 auf.
Ok, das 16.Bit ist das Vorzeichen. Damit schieben wir eine 1 in das Vorzeichen, was die Zahl mit einem Vorrangestellten – wiedergibt. Richtig?
Aber warum sind dort aufeinmal so viele Einsen voran gestellt. Und warum werden auf dem Serial Monitor 26 Bits geschrieben, wenn die Zahl doch nur aus 16 Bits besteht?
Gibt es eine Regel wie negative Integer Zahlen binär aussehen? Ich hätte erwartet das 1111 0000 0000 0000 entsteht und dass das dann einfach eine hohe negative Dezimalzahl ist.
Warum bei den negativen Zahlen auf einmal 32 Stellen (nicht 26) ausgegeben werden, das weiß ich nicht. Aber bei negativen Zahlen zählen die voranstehenden Einsen nicht, so wie die Nullen bei den positiven. Das liegt an der Zweierkomplementdarstellung der negativen Zahlen. Wenn negative binäre Zahlen einfach nur eine 1 anstelle der 0 vorangestellt hätten, könnte der Rechner nicht so schön damit rechnen. Bei der Zweierkomplementdarstellung werden alle Bits invertiert und eine 1 dazu addiert. Z.B.:
4096 (dez) = 0b 0001 0000 0000 0000
-4096 (dez) => Zweierkomplement => 0b 1110 1111 1111 1111 + 1 => 0b 1111 0000 0000 0000
Hier ist eine ganz gute Erklärung:
http://www.ulthryvasse.de/negative-binaere-zahlen.html
Danke für die Erklärung und den Link
Hi,
die Beispiele: Anwendung auf ein ganzes byte sind mir unklar (Tippfehler?)
bei ODER 0b10011100 | 0b010101111 = 0b11011111
bei XOR 0b10011100 ^ 0b010101111 = 0b11010011
ist da eine 1 zuviel, oder mache ich einen Denkfehler?
gruß
Hallo, vielen Dank. Du hast absolut recht. Ein byte mit neun bits wäre ungewöhnlich! Ist noch keinem aufgefallen oder keiner hat’s geschrieben. Ich habe das verbessert. VG, Wolfgang
Danke für die Bestätigung/rasche Antwort…
Eigentlich suche ich nach einer Erklärung des „OR“-Strich bei solchen „Sachen:
ldi temp, (1<<3) | (1<<1) | (1<<2) | (1<<0)
#define emCursorIncRead LCD_EM_CursorIncrement | LCD_EM_CursorRead
sind die senkrechten Striche hier auch als OR zu interpretieren?
Fange gerade erst an, zu versuchen, Fremdcode zu verstehen und muss mich durch die Syntax jedes Befehls hangeln… Mühsam…
gruß thomas
Der Strich ist immer ein OR und die OR Operationen lassen sich auch aneinanderhängen:
(1>>3) | (1>>1) | (1>>2) | (1>>0) =
0b00001000 | 0b00000010 | 0b000000100 | 0b00000001 = 0b00001111
Oder Variable1 = 16 und Variable2 = 5, dann ist
Variable1 | Variable 2 = 16 | 5 = 0b00010000 | 0b00000101 = 0b00010101
…danke – ist im ersten Lesen erstmal verwirrend, wird mir aber weiterhelfen…
Der Fehler der Zeitmessung durch den Schleifenaufruf ist recht gross und kann doch relativ einfach eliminiert werden:
https://github.com/jschwender/Arduino-Profiler
Sehr schöne Sammlung an „Tachometern“!
Und meine erste Antwort muss ich revidieren. Pragmatische Lösung, sehr gut!
An die anderen Leser gerichtet: Um die Zeit für die Schleife herauszurechnen liegt es nahe, einfach eine Leerschleife, die man x-fach durchläuft zu messen. Allerdings wird die vom Compiler „wegoptimiert“. Nun kommt der Trick: man packt einfach eine dummy Aktion mit in die Schleife, z.B. dummy++; Das macht man bei der Leerschleifenmessung wie auch in der Schleife die die eigentlich zu vermessende Funktion enthält. Dann kann man die benötigte Zeit für die Schleife mit der dummy Aktion von der anderen Messzeit abziehen.
Vielleicht könnterst du auch was zu if sagen. Stimmt das so rum? Bei meiner Platine liegt der PIN über einen R an Vcc 5V. Mit dem Taster ziehe ich den PIN auf GND. Wir muss es den jetzt sein? Mit meiner Hardware passt das nicht zusammen.
Nächster Versuch:
Wenn bei dir PINB2 auf 5 Volt hochgezogen ist und durch Tasterdruck auf 0 geht, dann soll bei Tasterdruck der Ausdruck PINB & (1 << PB2)) unwahr sein. D.h. du müsstest schreiben if(!(PINB & (1 << PB2))) was gleichbedeutend ist mit if( (PINB & (1 << PB2)) == false);
bzw. jeweils PINB2 anstelle PB2 bei deiner bevorzugten Schreibweise.
Du verwendest das folgende:
Selektives Abfragen von Pin-Zuständen
Der Ausdruck
PINB &= (1<<PINB2)
liefert 0, also false (falsch), wenn PB2 LOW ist. Ist PB2 HIGH, dann ist der Ausdruck ungleich 0, also true (wahr).
Ich verwende zur einfachen Abfrage der Taster an den Pins das folgende:
if (PINB & (1<<PINB2)) // Taster T2
{ // Wenn T2 gedrückt…
PORTA |=(1<<led1); // LED 1 PINA0 ein
}
else
{ // wenn nicht
PORTA &=~(1<<led1); // LED 1 PINA0 aus
}
Es gibt dabei ein paar kleine Unterschiede in der Schreibweise. Wieso?
Ist die Funktion in der if.. Abfrage gleich?
Hallo Achim, das Gleichheitszeichen gehört da nicht hin. Gut bemerkt.
&= wäre eine Zuordnung. Aber ich will ja nur den Pinzustand vergleichen. Werde es gleich ändern – Danke.
Bei mir nimmt er Atmel Studio ohne define erst mal nur PINA1, mit define geht es natürlich auch anders. Liegt auch daran das ich kein arduino nehme
PA1 geht aber auch. Hat nicht so viel mit dem Arduino zu tun, sondern kommt aus den avr Headerdateien, die man normalerweise bei Programmierung mit Atmel Studio einbindet. Da muss man auch kein #definr irgendwo hinschreiben. Wenn PINA1 auch geht, dann ist das auch irgendwo über ein #define definiert. Letztlich steckt schlicht „1“ dahinter. Nur ist PA1 oder PINA1 besser lesbar. PINA1 ist sogar noch besser lesbar- da habe ich auch noch was gelernt!
Wenn ich das richtig verstanden habe, geht das mit Port A so:
PORTA &= ~(1<<PINA1), gleichbedeutend mit:
PORTA &= ~(0b00000010) bzw. PORTA &=0b11111101
Nach deinem Artikel bedeutet & logische Und und ~ negation
Genau, nur dass PINA5 glaube ich nicht definiert ist, sondern nur PA5. Die entsprechenden #define Anweisungen sind in der avr/io.h die man mit einbindet.
Selektives Löschen von Bits
Auch das selektive Löschen von Bits ist sehr einfach, es erschließt sich aber vielleicht nicht unbedingt auf den ersten Blick. Ich nehme wieder PB5 als Beispiel:
PORTB &= ~(1<<PB5), gleichbedeutend mit:
PORTB &= ~(0b100000) bzw. PORTB &= 0b11011111
Da stimmt was nicht. Mit 0b100000 veränderst du doch auch die anderen Bits, ist also nicht nur (1<<PB5). Hast du was bei &=0b11011111 vergessen?
Hallo Achim, mit „&=“ vergleicht man jedes Bit einzeln. ~(0b100000) ist 0b11011111. In der Maske 0b11011111 ist nur das Bit 5 Null, alle anderen sind 1. Wenn man die Maske nun auf ein anderes Byte anwendet, also „&=“ ausführt, dann bleiben alle Einsen Eins und alle Nullen Null. Nur Bit 5 wird wird zwingendermaßen auf Null gesetzt. Z. B. : 0b10101010 &= 0b11011111 ergibt 0b10001010.
Typ von Registern
Eine Frage in diesem Zusammenhang: wenn wir in C
PORTB&=~(1<<PB5);
schreiben, welchen Datentyp hat dann PORTB (in C müsste man ja erwarten, dass PORTB einen hat).
Ich frage deshalb, weil ich ein Unterprogramm z. B. eine LED einschalten lassen will und ihm dazu DDR- und PORT-Register und Pin-Nummer als Parameter mitgeben möchte, also etwa so:
up(DDRB,PORTB,5);
Vielen Dank!
Wolfgang Böhm
Wenn man in den Tiefen der AVR Dateien „wühlt“, die standardmäßig mit geladen werden, findet man die Definiton für PORTB (iom328pb.h für den ATmega328P):
#define PORTB _SFR_IO8(0x05)
_SFR_IO8(0x05) ist eine Funktion und kein Datentyp. Ich würde bei der Funktion, die du haben willst, deshalb anders vorgehen. Z.B.
char port = ‘b‘;
Funktionsaufruf:
up(‘b‘, 5);
Funktion:
void up(char port, byte pin){
if (port == ‘b‘){
DDRB |= (1<<pin);
PORTB |= (1<<pin);
}
if (port == ‘c‘){
……
usw.
oder so ähnlich.