Das Thema Port-Manipulation benötigt als Grundlagen die Mechanismen der Binären Logik und der Bit-Manipulation. Um beim Studium des Beitrags die Übersicht nicht zu verlieren, habe ich ein Inhaltsverzeichnis erzeugt, so dass schnell zu den Beiträgen navigiert werden kann.
In der Hardware-nahen Programmierung kommt es häufig vor, dass aus einer Bitfolge ein einzel- nes Bit gezielt gesetzt, gelöscht oder gewechselt (getoggelt) werden muß, um den Programmfluß entsprechend zu steuern. Wie bestimmte Bits in einer Variable manipuliert werden können wird im folgenden vorgestellt.
Um ein bestimmtes Bit innerhalb eines Bytes zu setzen, gibt es grundsätzlich zwei Möglichkeiten: Die erste Methode besteht in der Kombination aus einer Shift und einer ODER-Operation.
Bitweise Schiebeoperationen, sind Operationen, bei denen die Bits einer Binärzahl um eine bestimmte Anzahl von Positionen nach links oder rechts verschoben werden. Dabei werden die "herausgeschobenen" Bits verworfen und die freiwerdenden Positionen mit Nullen aufgefüllt. Die Verschiebung wird mit den größer und kleiner Zeichen eingeleitet. In der Grafik ist dieser Prozess symbolisch dargestellt.
Um das n-te Bit einer Variable A zu setzen wird das bitweise Schieben mit der logischen ODER Funktion kombiniert. Dazu wird die Variable A oder verknüpft mit der durch die Schiebeoperation entstehenden Bit-Kombination, die eine logische 1 um genau die Anzahl von n-Stellen verschiebt an der das Bit gesetzt werden soll und das Ergebnis gleich A setzt. Als Formel lässt sich das wie folgt beschreiben: A | = (1 << n).
A | = (1 << n)
Setzen von Bit 0: 0100 | = (1 << 0) ergibt: 0100 | 00001 ergibt: 001001
Setzen von Bit 2: 0100 | = (1 << 2) ergibt: 0100 | 00100 ergibt: 000100
Setzen von Bit 4: 0100 | = (1 << 4) ergibt: 0100 | 01000 ergibt: 011000
Erklärung im Detail:
A ist eine 8-Bit Variable, die bereits einen Wert enthält
(1 << n) bedeutet: Nimm die Zahl 1 und verschiebe sie um n-Stellen nach links
A | = B ist eine sogenanntes Bit-weises OR mit Zuweisung. Es bedeutet: A = A | B
D.h. A | = (1 << n) setzt das n-te Bit auf 1, ohne die restlichen Bits der Variable zu verändern
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, das 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. Ein weiterer Unterschied entsteht durch die Verwendung der UND Operation, die bei entsprechender Maske alle übrigen Bits verändert.
int A = 0b01010000;
int Maske = 0b00000010;
Setzen von Bit 1: (Wert & Maske) == Maske ergibt: 0b00000010;
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: A &= ~(1 << 0).
A = 65 ==> 1000001
A &= ~ (1 << 0) ==> 1001000 // lösche Bit 0
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;
Unter Port-Manipulation versteht man den direkten Zugriff auf die Ein- und Augabe-Pins eines Microprozessors. Dabei handelt es sich um eine hardwarenahe Art der Programmierung. Bei der es darum geht, direkt auf die Prozessor Register zuzugreifen, um das Ein- oder Ausgeben von Daten über die Pins zu steuern. Daher ist es hilfreich zu wissen, wie Logik-Operatoren eingesetzt und wie Bits gesetzt, gelöscht und verschoben werden können.
Die Ein- und Augabe-Pins des Arduino UNO mit seinem ATmega 32 Prozessor sind in drei Ports organisiert. Sie haben die Bezeichnung Port B, Port C und Port D. Die folgende Aufzählung zeigt, welche Pins zu welchem Port zugeordnet sind:
Die Ports werden von jeweils 3 Registern gesteuert, die mit DDRx, PORTx und PINx bezeichnet werden. Wobei das x für die Ports B, C und D stehen. Diese drei Register existieren für jeden 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.
Dieses Zusammenwirken ist in der nachfolgenden Grafik schematisch zur Veranschaulichung dargestellt.
Für die Ausgabe von Daten wird das entsprechende Bit im DDRx-Register auf 1 (= Ausgabe) gesetzt. Somit kann nun die Steuerung dadurch erfolgen, dass das korrespondierende Bit im zugeordneten PORTx- Register auf den gewünschten Wert gesetzt wird.
Für die Daten-Eingabe wird im DDRx-Register das jeweilige Bit auf 0 (= Eingabe) gesetzt. Die an den Eingangs-Pins anliegenden Daten werden an die korrespondierenden Bits im PIN-Register übertragen und können von dort aufgerufen werden.
Nach dem festgelegt wurde, welcher Pin als Eingang oder Ausgang definiert ist, können Daten über das zugehörige PORTx Register ausgegeben werden. Dazu muß nur das entsprechende Bit im Port-Register auf LOW oder HIGH geschaltet werden.
Das Einlesen von Daten erfolgt analog über das PINx Register. Der an den zugehörigen Pins anliegende Wert wird in korrespondierenden Bits im Register übertragen und kann von dort ausgelesen werden.
Die einzelnen Register sind in der Arduino-Entwicklungsumgebung bereits als Namen vor definiert, man kann diese somit mit den entsprechenden Bezeichnungen wie beispielsweise PORTB, PINC usw. direkt ansprechen. Das vereinfacht den Umgang erheblich. Im nachfolgenden Code-Schnipsel wird gezeigt, wie man die Port-Manipulations-Mechanismen praktisch anwenden kann.
// Wechselblinker
void setup(){
DDRB = B11111111; // alle Bits als Ausgang
}
void loop (){
PORTB = B10101010; // setzt Bits 1,3,5,7 auf High
delay(300);
PORTB = B01010101; // setzt Bits 0,2,4,6 auf High
delay(300);
}
//Weitere Beispiele:
DDRD = 0B11111111; // Alle PINs im Port D sind Output
PORTD = 0B11111111; // Alle PINs werden auf HIGH gesetzt
DDRD = 0B00000000; // Alle Pins im Port D werden als Eingang definiert
int data = PIND; // die an den Pins des Port D anliegenden Werte werden gesetzt.
Weiteres Beispiel:
void setup() {
DDRD &= ~(1 << PD2); // Setzt Pin D2 als Eingang
PORTD |= (1 << PD2); // Aktiviert internen Pull-up
Serial.begin(9600);
}
void loop() {
if (!(PIND & (1 << PD2))) {
Serial.println("Taster gedrückt");
} else {
Serial.println("Taster nicht gedrückt");
}
delay(500);
}
Der Arduino Mega arbeitet mit dem ATmega2560-Chipsatz und 16 MHz Taktfrequenz. Er hat 256 KB Flashspeicher, 8 KB SRAM und 4 KB EEPROM-Speicher. Hinzu kommen 55 digitale I/O-Pins, davon 15 mit 8-bit-PWM, 16 analoge Inputs mit 10 bit Auflösung, 6 externe Interrupt-Pins, 2 Hardware-Serial-Verbindungen, sowie wie ein SPI- und ein IIC-Bus.
Auch hier sind die I/O-Pins wieder bestimmten Ports zugeordnet und deren Nutzung erfolgt völlig analog zum Arduino Uno. Einziger Unterschied es gibt natürlich eine größere Anzahl an Ports und zugehörigen Port-Register. In der folgenden Übersicht sind die Namen der Ports und die zugeordneten Pins dargestellt. Als Gedankenstütze ist beispielhaft dargestellt, dass zu jedem Port die oben beschriebene "3 Steuer-Register" vorhanden sind.
Auslesen eines EPROMs bzw. eines EEPROMS ist eine zentrale Aufgabe, wenn man mit Mikroprozessoren wie dem 6502 oder dem Z80 experimentiert. Hierzu ist ein entsprechendes Programmiergerät natürlich sehr hilfreich, wenn man den Baustein nicht gänzlich von „Hand“ mit Daten füllen möchte. Im vorliegenden Beispiel gehen wir davon aus, dass ein programmiertes EEPROM vom Typ AT28C256 vorliegt, dessen Daten mit dem Arduino Mega ausgelesen werden sollen.
Auf den AT28C256 Baustein kann wie auf ein statisches RAM zugegriffen werden. Daten Ein-/Ausgabe wird über die können ausgelesen Anschlüsse ¬CE, ¬OE und ¬WE gesteuert. Dabei ist zu berücksichtigen, dass die Anschlüssen negiert angesteuert werden (¬). D.h. um sie zu aktivieren, muss ein Low-Signal gesetzt werden. Wird das gemäß der Spezifikation im Datenblatt durchgeführt, können Daten (die an den Pins IO0-IO7 anliegen) in die durch die Adresspins bestimmte Speicherstelle eingegeben oder an den Ausgängen IO0-IO7 ausgegeben werden. Zum Auslesen der Daten aus dem EEPROM sind folgende Schritte erforderlich:
Da die Port-Pin Zuordnung beim Arduino nicht linear durchgängig ist, muß man genau überlegen welchen Port man verwendet, um die Verdrahtung möglichst unkompliziert durchführen zu können. In unserem Beispiel verwenden wir für die Daten-Pins den Port L des Arduino Mega und die Adress-Pins werden über die Ports A und Port C angesteuert. Als Beispiel soll eine Hex-Dump Routine zum Auslesen von Speicherinhalten gezeigt werden:
// =====================================================
// EEPROM Monitor
// HexDump Anzeige von Speichstellen
// SetAdress SetData setzen von Adressen und Daten
// PHOF Nov 2020
//-------------------------------------
// Verwendete Arduino MEGA Portbelegung
// Bit7| Bit6| Bit5| Bit4| Bit3| Bit2| Bit1| Bit0
//PortA: P29 | P28 | P27 | P26 | P25 | P24 | P23 | P22
//PortL: P42 | P43 | P44 | P45 | P46 | P47 | P48 | P49
// ======================================================
#define WRITE_EN 12
#define Output_EN 10
void setup() {
DDRA = B11111111; // alle Ports output
DDRC = B11111111;
DDRL = B11111111;
DDRF = B11111111;
DDRK = B11111111;
Serial.begin(9600);
digitalWrite(WRITE_EN, HIGH); pinMode(WRITE_EN, OUTPUT);
digitalWrite(Output_EN, HIGH);pinMode(Output_EN, OUTPUT);
}// end setup
void Monitor (int Startadresse, word Endadresse) {
// Ausgabe der Speicherstellen von Startadresse bis Endadresse
Serial.println("==============================");
Serial.println("** HEX Monitor-Programm **");
Serial.println("==============================");
for (int adr=Startadresse;adr< Endadresse; adr++){
if ((adr == 0)||(adr % 8 == 0)) // zeige 8 Stellen in einer Reihe
Serial.print(Int2Hex(adr,4)+": ");
if ((adr+1)%8 == 0) // stelle fest ob Zeilenumbruch erforderlich
Serial.println();
}// end for adr
}// end Monitor
String Int2Hex(int wert, int stellen){
String temp = String(wert,HEX);
String prae = "";
int len = temp.length();
int diff = stellen-len;
for (int i=0;i< diff;i++)
prae = prae +"0";
return prae+temp;
}// end Int2Hex
void setAdress(word adresse){
// Setze 16Bit Adresse über Port-Register F und K
// Bit7|Bit6|Bit5|Bit4|Bit3|Bit2|Bit1|Bit0
// PortF: A7 | A6 | A5 | A4 | A3 | A2 | A1 | A0
// PortK: A15| A14| A13| A12| A11| A10| A9 | A8
// setze Port-Bits auf Ausgang
DDRF = B11111111;
DDRK = B11111111;
// Bestimmung Low- und High-Bytes
byte high_byte = adresse >>8; // oder (adresse & 0xff00) >> 8
byte low_byte = adresse & 0x00ff;
// Übertragung der beiden Bytes auf die PortRegister
PORTF = low_byte;
PORTK = high_byte;
}// end setAdress
byte readEEPROM(int address) {
// setze Output_Enable auf LOW = ermöglicht das Lesen von Daten
digitalWrite(Output_EN, HIGH);
delayMicroseconds(1);
digitalWrite(Output_EN, LOW);
// Setze Portregister C auf Input
DDRC = B00000000;
setAdress(address);
byte data = 0;
data=PINC;
return data;
}//end readEEPROM
// Ebenso lassen sich natürlich auch Daten in das EEPROM schreiben...
void writeEEPROM(int address, byte data) {
// setze Output_Enable auf HIGH = ermöglicht das Schreiben von Daten
digitalWrite(Output_EN, LOW);
delayMicroseconds(1);
digitalWrite(Output_EN, HIGH);
setAdress(address);
// PortC: P30 | P31 | P32 | P33 | P34 | P35 | P36 | P37
DDRC = B11111111; // setze Port-Bits C auf Ausgang
PORTC = data; // damit stehen die Daten an den Pins von Port C
// set write Enable um die Daten ins EEPROM zu schrieben
digitalWrite(WRITE_EN, LOW);
delayMicroseconds(1);
digitalWrite(WRITE_EN, HIGH);
delay(10);
}//end writeEEPROM
void loop() { }