Browse Month: Januar 2021

Bit-Manipulations Mechanismen

Das Dualsystem spielt in der Welt der Steuerungstechnik eine fundamentale Rolle. Daher ist es wichtig zu wissen, wie man mit Binären Zahlen und Logischen Operationen umgehen kann. Hier sollen die wesentlichen  Bit-Operationen vorgestellt werden. Im Folgenden werden die Bit-Operatoren der Sprache C vorgestellt.

Logische Operationen

UND Verknüpfung

Die UND-Verknüpfung wird in C mit dem Operator & durchgeführt. Bei der bitweisen UND-Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen beide Vergleichswerte eine 1 besitzen. 

ODER Verknüpfung

Bei der bitweisen ODER-Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen mindestens einer der beiden Vergleichswerte eine 1 besitzt. Das Operatorzeichen dafür ist das einfache Pipe-Zeichen |.

Exklusiv-Oder Verknüpfung

Bei der bitweisen XOR Verknüpfung hat das Ergebnis an den Stellen eine 1, an denen entweder der eine oder der andere Vergleichswert eine 1 besitzt. Das Operatorzeichen dafür ist das Dach-Zeichen ^.

NICHT Verknüpfung

Bei der bitweisen Negation wird jedes Bit umgekehrt: aus 0 wird 1 und aus 1 wird 0. Das Operator-Zeichen dafür ist die Tilde ~.

Beispiel-Programm

Im folgenden sollen die oben vorgestellten Verknüpfungen innerhalb eines Beispielprogramms umgesetzt werden:

    int x = 5;       // binary: 101
    int y = x & 1;   // now y == 1
    x = 4;           // binary: 100
    y = x & 1;       // now y == 0
    y = x | 2;       // now y == 110

    int x = 12;     // binary: 1100
    int y = 10;     // binary: 1010
    int z = x ^ y;  // binary: 0110, or decimal 6

    int a = 103;    // binary:  0000000001100111
    int b = ~a;     // binary:  1111111110011000 = -104

Schiebe-Operationen

Durch Verschieben, nach links mit << und nach rechts mit >>, wird ein Binärwert um eine bestimmte Anzahl von Bits nach links oder rechts verschoben. Die durch die Verschiebung freiwerdenden Stellen werden mit 0 gefüllt.

01101011 << 2 ergibt: 10101100
01101011 >> 4 ergibt: 00000110

Bit-Manipulation

In den folgenden Abschnitten wird gezeigt, wie man in einer Bitfolge gezielt einzelne Bits setzt, löscht oder umkehrt.

Setzen eines Bits

Um ein bestimmtes Bit innerhalb eines Bytes zu setzen kann man entweder eine Kombination aus einer Shift- und einer ODER-Operation verwenden oder den Maskierungsansatz verwenden.

Um das n-te Bit einer Variable A zu setzen wird folgende Formel verwendet:

Setzen von Bit 0:
0100 |= (1 << 0) ergibt: 0100 | 0001 ergibt: 01001
Setzen von Bit 3:
0100 |= (1 << 2) ergibt: 0100 | 0100 ergibt: 0100 (keine Veränderung)

Alternativ lässt sich ein Bit auch direkt über eine Maske setzen. Dazu verknüpft man den zu prüfenden Wert mit einer sogenannten Maske, in der genau das Bit (und nur das Bit) gesetzt ist, nach dem man setzen bzw. das man prüfen möchte. Der einzige Unterschied zur vorherigen Methode ist, dass die Maske manuell erstellt wird und nicht durch eine shift-Operation generiert wird.

Überprüfung ob ein BIT gesetzt ist
// Bit Operationen - Beispiel

  int Wert  = 0b01001111;
  int Maske = 0b00001000;
  int Pin = 9;
  
void setup() {
  Serial.begin(9600);
  pinMode(Pin,OUTPUT);

  // wenn Bit gesetzt leuchtet LED
  digitalWrite(Pin, (Wert & Maske) == Maske);
  
   if ( (Wert & Maske) == Maske) {
       Serial.println("Bit ist gesetzt");
    }
}

void loop(){
}

Löschen eines Bits

Das Löschen eines Bits kann durch die Kombination aus einem Shift, dem Komplement und der UND-Verknüpfung realisiert werden. Auch hier besteht die Möglichkeit anstelle des Verschiebens wieder Manuell eine Bit-Maske zu erzeugen und einzusetzen. Um das n-te Bit in der Variable A zu löschen (von 0 an gezählt, also vom niederwertigsten Bit) wird folgender Ausdruck verwendet:

Beispiel wie das Bit 0 der Variable A gelöscht (auf LOW) gesetzt werden kann:

A =  65  ==> 1000001
A &= ~ (1 << 0)   ==> 1001000 // lösche Bit 0

Togeln eines Bits

Das Wechseln eines Bits geschieht durch die Kombination der Shift-Funktion und der Exklusiv-ODER-Verknüpfung. Dabei wird das entsprechende Bit von 0 auf 1 oder von 1 auf 0 gesetzt. Um den Zustand des n-ten Bits in der Variable A zu wechseln (von 0 an gezählt) wird folgender Ausdruck verwendet:

byte A =  65;       //  ==> 1000001 
Serial.println(A);  // A=65
// toggle Bit 0
A = (A^=(1 << 0));  // ==> 1001000 
Serial.println(A);  // A=64

Bestimmung des Low-Bytes und High-Bytes

In verschiedenen Fällen benötigt man von einer 16-Bit-Variable die unteren 8-Bit und die oberen 8 Bit separiert. Hierzu können die oben beschriebenen Mechanismen verwendet werden:

 word wert = 27543;
  byte high_byte = wert >> 8; // oder (wert & 0xff00) >> 8
  byte low_byte  = wert & 0x00ff;

Arduino Funktionen

Da die oben beschriebenen Bit-Manipulationen relativ häufig vorkommen, und nicht immer jederzeit von weniger geübten programmieren abgerufen werden können, haben sich die Entwickler entschlossen, die häufig benötigten Funktionen in den Sprach-Umfang zu integrieren:

FunktionErklärung
bit(n)Berechnet den Wert des angegebenen Bits (Bit 0 ist 1, Bit 1 ist 2, Bit 2 ist 4, etc.).
bitClear(var,n)var: Die Zahlenvariable, deren Wert manipuliert werden soll.
n: Bit gelöscht werden soll; Startet bei 0 für das least-significant (rechteste) Bit.
bitRead(var,n)Liest n-tes Bit aus der Variable var
bitSet(var,n)Setze Bit n in der Variable var
highByte(var)Liest das most-significant (linkeste) Bit eines Wortes (oder das zweitkleinste Bit eines größeren Datentypes).
lowByte(var)Liest das least-significant (rechteste) Bit eines Wortes oder größeren Datentypes.
Vordefinierte Bit-Manipulations-Funktionen beim Arduino

Weitere Details finden sich in der Arduino-Referenz Dokumentation.

Arduino Grundlagen

Arduino ist eine offene Mikrocontroller-Plattform die aus einer Programmierumgebung und dem Mikrocontroller-Board. Aufgrund der einfachen Bedienbarkeit ist eine weltweite Community an Arduino-Freunden entstanden, die alle verschiedenste Projekte realisieren und die Ergebnisse mit allen Interessierten teilen.

Die Basis für Projekte ist meist der Arduino Uno für gut 20 Euro. Das Board verfügt über einen ATmega328P-Microcontroller, läuft mit den typischen 5 Volt, hat 14 Input/Output-Anschlüsse (I/O-Pins), 32 Kilobyte Flash-Speicher, USB-Anschluss und läuft auf moderaten 16 Megahertz.

Neben dem Uno gibt es aber noch viele weitere Boards in unterschiedlichen Größen und Leistungsklassen, die sich aber alle über die gleiche Entwicklungsumgebung (Arduino-IDE genannt) programmieren lassen. Als Programmiersprache wir C++ verwendet, die um einige spezifische Funktionen erweitert wurde.

Im folgenden sollen einige grundlegende Mechanismen vorgestellt werden, die das Fundament in verschiedenen Blog-Beiträgen bilden.

Ein- und Ausgabe von Daten

Wie oben erwähnt stellt jedes Arduino Board eine bestimmte Menge von Anschlüssen (PINs) bereit über die Daten eingelesen oder ausgelesen werden können. Dazu müssen die Anschlüsse-Pins über die Funktion pinMode entsprechen konfiguriert werden:

Nach dem die Konfiguration der Pins erfolgt ist, kann man Daten ein- bzw. ausgeben. Dazu stehen ebenfalls Funktionen mit den Namen digitalRead und digitalWrite zur Verfügung. Am sogenannten LED-Blinken-Beispiel lässt sich die Funktion sehr anschaulich darstellen:

void setup() {
  pinMode(13, OUTPUT); // Setzt den Digitalpin 13 als Outputpin
}

void loop() {
  digitalWrite(13, HIGH); // Setzt den Digitalpin 13 auf HIGH = "Ein"
  delay(1000);            // Wartet eine Sekunde
  digitalWrite(13, LOW);  // Setzt den Digitalpin 13 auf LOW = "Aus"
  delay(1000);            // Wartet eine Sekunde
}

Alle Funktionen sind in den Referenz-Seiten sehr gut beschrieben, so dass hier für weitere Details darauf referenziert werden soll.

Pull-up oder Pull-Down Widerstände

Ein Pullup- oder Pulldown-Widerstand wird dazu verwendet, einen Eingang auf einen definierten Wert zu “ziehen”. Normalerweise befindet sich der Eingang im Zustand “schwebend/hochohmig”, welcher sich irgendwo zwischen High und Low befindet.

Bei Arbeiten im Bereich der Mikrocontroller haben sich Werte von 4,7 kOhm für Pullup- bzw. 10 kOhm für Pulldown-Widerstände in den meisten Fällen bewährt.

Pull-Up und Pull-Down-Widerstands Schaltbilder

Es gibt zwei Möglichkeiten einen Schalter oder Taster mit einem logischen Eingangs-Pin zu verbinden. Will man dafür sorgen, dass der Eingangspin logisch LOW erhält, wenn die Taste gedrückt wird, so muss das Pull-Up-Prinzip verwendet werden. Der Pullup-Widerstand liegt zwischen dem Eingang und +Vcc. Beim Öffnen des Tasters zieht der Pullup-Widerstand die Spannung am Eingang hoch bis zum Betriebsspannungswert +Vcc, was logisch HIGH entspricht.

Will man dafür sorgen, dass der Eingang logisch HIGH erhält, wenn die Taste gedrückt wird, so muss das Pull-Down-Prinzip verwendet werden. Der Kontakt liegt zwischen dem Eingang und +Vcc. Der Pulldown-Widerstand liegt zwischen dem Eingang und GND. Beim Öffnen des Kontaktes zieht der Pulldown-Widerstand die Spannung am Eingang hinunter auf GND, was logisch LOW entspricht.

Interne Pull-x Widerstände

Um den zusätzlichen Bauteil- und Verdrahtungsaufwand beispielsweise beim ersten Prototypenaufbau zu vermeiden, sind im Microcontroller des Arduino-Boards bereits interne Pull-Up-Widerstände integriert. Sie lassen sich sehr einfach in der Pindeklaration hinzuschalten.

pinMode(8, INPUT_PULLUP);

Entstellen von Schaltern

Jeder mechanische Schalter schaltet nicht perfekt. Beim Schließen des Kontakts “hüpft” dieser einige Male hin und her, d.h. er schließt und öffnet, bis er ganz geschlossen ist.

Prell-Verhalten eines Schalters

Dieses Schalter-Prellen kann sich je nach Schaltertyp sehr negativ auf das Verhalten des Programms auswirken. Daher sollte man Schlater entstellen. Dazu gibt es Hardware- und Software-technische Lösungsansätze.

Ein sehr sicheres Mittel zum Entprellen von Schaltern ist die Zeit nach dem schliessen quasi auszublenden in dem man ein RC-Glied einführt. Dieses sorgt dann für eine “Glättung” der Spannungsspitzen und führt zu einem stabilen Eingangspegel.

Hardware-Tasten Entprellung

Weitere Möglichkeiten bieten Flipflops, was aber wiederum einen größeren Hardware-Aufwand zur Folge hat.

Software-technische Lösungen

Ohne Lib

gggg

const int buttonPin = 2;    
const int ledPin =  13;     
int ledState = LOW;
boolean buttonState = LOW; 

int pressed=0;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
}

void loop() {
  if(debounceButton(buttonState) == HIGH && buttonState == LOW){
    pressed++;
    buttonState = HIGH;
  }
  else if(debounceButton(buttonState) == LOW && buttonState == HIGH{
       buttonState = LOW;
  }
  if(pressed == 10){
    digitalWrite(ledPin,HIGH);
  }
}

boolean debounceButton(boolean state){
  boolean stateNow = digitalRead(buttonPin);
  if(state!=stateNow){
    delay(10);
    stateNow = digitalRead(buttonPin);
  }
  return stateNow; 
}

Dazu gibt es eine Bibliothek mit dem Namen “Bounce2”, die entsprechende Funktionen bereit stellt, um das mehrfache unkontrollierte senden von Signalen durch das Prellen zu verhindern.

Zunächst einmal muss für jeden Button eine Instanz von “Bounce” erzeugt werden. Im Konstruktor des Bounce Objektes wird der Pin an welchem der Taster angeschlossen ist und zusätzlich ein Wert für eine Pause (in Millisekunden)  übergeben.

#include <Bounce2.h>
#define BTN 2
int index = 0;

Bounce btnBouncer = Bounce(BTN, 50);

void setup() {
  Serial.begin(9600);
  pinMode(BTN, INPUT);
  digitalWrite(BTN, HIGH);  
}

void loop() {
  btnBouncer.update();

  if(btnBouncer.fell()){
    index = index+1;
    Serial.println(index);
  }
  
}

Port-Manipulation

Die Pins des Mikrocontroller sind in sogenannten Ports organisiert. Ein Port ist kein Pin sondern bezeichnet eine “Pin-Gruppe”. Die 14 Pins des Arduino Uno sind in 3 Ports gegliedert: Port B, C und D. Über die Port Manipulation schaltet man also die Pins eines Ports beziehungsweise einen ganzen Port auf einmal.

Port und Port-Register des Arduino UNO

Die Ports werden von jeweils 3 Registern gesteuert, die mit DDRx, PORTx und PINx bezeichnet werden. Wobei das x für die Ports Namen B,C und D stehen. Dh. für jeden Port gibt es 3 Steuer-Register Port, also zum Beispiel existiert für Port B ein DDRB-, ein PORTB- und ein PINB-Register. Analog gilt das auch für Port C und Port D.

Über das DDRx Register legt man fest, ob die einzelnen Pins eines Port OUTPUTs oder INPUTs sein sollen. Beispielsweise würde die Zeile DDRC = B00000111 die analog Eingänge A0 bis A2 als Input definieren und A3 bis A7 als Output definieren. 0 steht für Input und 1 für Output.

Über das PORTx Register legt man fest, ob die Pins auf LOW oder HIGH geschaltet werden sollen. Dabei steht das x wieder für den jeweiligen Port. Wenn wir also Pin 8, 10 und 12 auf HIGH und Pin 9, 11 und 13 auf LOW setzen wollen, schreiben wir PORTB = B101010.

Das PINx Register ist dazu da, um den aktuellen Status der Input Pins, also LOW oder HIGH, festzustellen. Dazu wählt man für x wieder den aktuellen Port und vergleicht dann das Register mit einer Abfolge. Beispielsweise überprüft if(PINB == 0b000000), ob alle Pins, des Ports B, auf LOW geschaltet sind. 

Ein etwas erweitertes Blink-Beispiel soll die praktische Anwendung illustrieren:

// Port-Manipulations Beispiele
char my_var =0;

void setup(){
  DDRB  = 0B11111111;   // alle Bits als Ausgang
  DDRC  = 0B11111111;   // Alle PINs im Port C sind Output
  DDRD  = 0B00000000;   // Alle PINs im Port D sind Input
  PORTD = 0B11111111;   // Alle PINs im Port D sind HIGH
  my_var = PIND     // Read Port D, put value in my_var
 }
void loop() {
  PORTB = B10101010; // Wechselblinker mit allen Ausgaengen
  delay(300);
  PORTB = B01010101;
  delay(300);
}

Damit stehen im Prinzip die identischen Mechanismen für die Konfiguration und die Ein- und Ausgabe von Daten wie Eingangs beschrieben bereit. Der Vorteil der Port-Manipulation liegt in der kompakten Formulierung, der Möglichkeit mehrere Pins gleichzeitig zu steuern und der sehr hohen Ausführungsgeschwindigkeit.  

Für jeden digitalWrite()-Befehl gibt es eine direkte Entsprechung: digitalWrite(16,HIGH) kann z.B. durch PORTC |= (1‹‹PC2) ersetzt werden, und digitalWrite(16,LOW) entsprechend durch PORTC &= ~(1‹‹PC2). Diese Schreibweise ist weniger einprägsam, dafür geht die Ausführung schneller. Die dafür zusätzlich erforderlichen Kenntnisse der Bit-Manipulation werden in einem separaten Beitrag erklärt. Der Nachteil gegenüber den leicht eingängigen Funktionsnamen (pinMode, digitalWrite, digitalRead) liegt sicher in der deutlich komplizierteren Handhabbarkeit.

Das gleiche Prinzip gibt es natürlich auch bei den leistungsfähigeren Boards wie dem Arduino Mega. Nur mit dem Unterschied, dass es hier natürlich wesentlich mehr Pins gibt. Die nachfolgende Graphik veranschaulicht die Pin-Zuordnung zu den Ports des Arduino Mega.

Arduino Mega Port-Belegung

Die einzelnen Register müssen nicht deklariert werden, sie sind in der Arduino-Entwicklungsumgebung bereits als Namen vordefiniert, und können direkt im Programmcode verwendet werden.