C-Programmierung mit AVR-GCC/ IO-Ports
Was sind Ports und wie kann man sie benutzen?
BearbeitenEin Port bezeichnet beim AVR die Zusammenfassung einzelner Pins. Ein Pin wiederum ist ein Anschlussbeinchen am Mikrocontroller. Ein Port hat für gewöhnlich 8 Anschlüsse, also auch 8 Pins. Nur vereinzelt kann es sein, dass ein Port aus Platzmangel nicht die vollen 8 Anschlüsse hat.
Am Beispiel eines ATmega8 kannst du erkennen, dass die einzelnen Pins doppelt oder sogar dreifach Belegt werden. Das macht man, um Platz zu sparen und trotzdem mehrere Funktionen auf dem Controller zu vereinen.
IO-Ports sind Ein- und Ausgänge des Mikrocontrollers. Diese kannst du digital und einige sogar analog benutzen. Ein einzelner Port kann sowohl als Eingang als auch als Ausgang konfiguriert werden.
Genauer gesagt, kannst du
- digitale Signale aus einem Ausgang ausgeben
- digitale Signale in einen Eingang geben
- analoge Signale in einen Eingang geben
- keine echten analogen Signale aus einem Ausgang ausgeben – aber dafür PWM-Signale, welche zu analogen Signalen gewandelt werden können
Trotz der Mehrfachbelegung einzelner Pins, kann ein Pin natürlich nur eine Aufgabe erledigen. Die Entscheidung welche Aufgabe er übernimmt, kannst du über Konfigurationsregister einstellen. Dazu aber später mehr.
Erforderliche Register
BearbeitenDamit du einen Port benutzen kannst, musst du ihn vorher noch konfigurieren.
Als erstes stellst du das Datenrichtungsregister (Data-Direction-Register) kurz DDRx ein. Dabei gibt das kleine x an, welchen Port (PORTA, PORTB, ...) du konfigurieren willst.
Eine logische Null an der Stelle eines Pins ergibt für diesen Pin einen Eingang. Eine logische Eins hingegen einen Ausgang. Wichtig beim Konfigurieren ist, dass du nur die Pins setzt, die du auch nutzen möchtest. Damit vermeidest du Seiteneffekte, die den Programmablauf stören können. Um das zu erreichen solltest du Bitmanipulation anwenden. Bitmanipulation habe ich schon im Kapitel Register erklärt.
Um zum Beispiel den Pin PB0 als Eingang und PB1 als Ausgang zu nutzen stellst du in deinem Code folgendes ein.
#include <avr/io.h>
int main() {
DDRB &= ~(1 << PB0);
DDRB |= (1 << PB1);
}
Das Datenrichtungsregister brauchst du auch nur einmal einzustellen. Danach kannst du den Pin weiter so benutzen, wie du ihn konfiguriert hast. Nur wenn sich die Konfiguration ändern sollte, musst du DDRx neu beschreiben.
Ist ein Pin als Ausgang konfiguriert, kannst du über das Register PORTx den Pin auf High oder Low schalten und so Einsen und Nullen ausgeben. Auch hier gibt das kleine x wieder an, welchen Port du konfigurierst.
#include <avr/io.h>
int main() {
DDRB &= ~(1 << PB0);
DDRB |= (1 << PB1);
PORTB |= (1 << PB1); //PB1 High
PORTB &= ~(1 << PB1); //PB1 Low
}
Die Konfiguration für einen Eingang ist etwas komplizierter und wird unten im eigenen Abschnitt erklärt.
Digitale Ausgänge benutzen
BearbeitenDie wohl meist genutzte Funktion digitaler Ports ist der digitale Ausgang. Ist ein Pin als Ausgang konfiguriert, kannst du beispielsweise eine LED oder ein Display anhängen und damit schalten.
Datenrichtung setzen
BearbeitenWie oben schon erwähnt füllst du das Datenrichtungsregister mit Einsen an den Stellen der Pins, welche als Ausgang genutzt werden sollen.
Wenn PB0 (also Anschlusspin 14 des ATmega8) ein Ausgang sein soll, dann muss im Datenrichtungsregister DDRB das Bit an der Stelle PB0 gesetzt sein.
PB7 | PB6 | PB5 | PB4 | PB3 | PB2 | PB1 | PB0 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
DDRB |= (1 << PB0);
Pins setzen
BearbeitenWir haben den Port nun richtig konfiguriert. Jetzt möchten wir aber eine angeschlossene LED einmal blinken lassen. Dazu geben wir eine logische Eins auf dem Pin aus. Damit wird die LED eingeschaltet. Danach werden wir eine logische Null ausgeben, um die LED wieder aus zu schalten.
Um den Ausgang zu setzen beschreiben wir das Register PORTB.
#include <avr/io.h>
int main(void){
DDRB |= (1 << PB0); // damit ist dann PB0 ein Ausgang
while(1){
PORTB |= (1 << PB0); //PB0 im PORTB setzen
PORTB &= ~(1 << PB0); //PB0 im PORTB löschen
}
return 0;
}
Technisch ist unser erste Programm total in Ordnung. Wenn wir es auf den Mikrocontroller laden, würden wir aber keinen Wechsel des LED-Status sehen. Das liegt daran, dass die Ausführung unseres Programmes für das menschliche Auge viel zu schnell ist. Daher müssen wir zwischen den einzelnen Anweisungen eine gewisse Zeit warten. Darum enthält das nun folgende komplette Programm Verzögerungen.
#define F_CPU 1000000UL //Taktfrequenz 1MHz
#include <avr/io.h>
#include <util/delay.h>
int main(void){
DDRB |= (1 << PB0); // damit ist dann PB0 ein Ausgang
while(1){
PORTB |= (1 << PB0); //PB0 im PORTB setzen
_delay_ms(250); //250ms warten
PORTB &= ~(1 << PB0); //PB0 im PORTB löschen
_delay_ms(250); //250ms warten
}
return 0;
}
Wir haben nun das Beispiel aus dem Anfang des Buches nachprogrammiert und die Hintergründe der einzelnen Anweisungen verstanden.
Digitale Eingänge benutzen
BearbeitenJeder Port lässt sich auch als digitaler Eingang verwenden. Ein Eingang ermöglicht es dir, Ereignisse von außen zu erkennen und darauf zu reagieren.
Datenrichtung setzen
BearbeitenEinen Eingang kannst du im Datenrichtungsregister mit einer logischen Null konfigurieren. Möchtest du beispielsweise PB1 als Eingang benutzen, sollte im Datenrichtungsregister an der Stelle PB1 eine Null stehen.
PB7 | PB6 | PB5 | PB4 | PB3 | PB2 | PB1 | PB0 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
DDRB &= ~(1 << PB1);
Im nächsten Schritt ist es wichtig deine Hardware zu kennen, denn du solltest noch den internen Pull-Up Widerstand konfigurieren. Der interne Pull-Up Widerstand zieht den Pin auf einen definierten Zustand, in diesem Fall logisch Eins, wenn dieser nicht beschaltet ist. Wichtig wird das unter anderem bei Schaltern bzw. bei Tastern, die offen oder geschlossen sein können. Nehmen wir an du hast den Schalter so angeschlossen dass er den Pin im geschlossenen Zustand (0 Ohm) gegen Masse zieht (ergibt logisch Null am Eingang). Dann stellt der Pull-Up Widerstand sicher dass der Pin im offenen Zustand des Schalters (oder Tasters) durch den Pull-Up sicher auf logisch Eins gezogen wird. Hast du allerdings einen externen Pull-Up Widerstand verbaut, kannst du den internen Pull-Up abschalten. Das erreichst du, indem du im PORTx-Register ebenfalls an die Stelle PB1 eine logische Null schreibst. Möchtest du den internen Pull-Up benutzen schreibst du an die Stelle eine Eins.
PB7 | PB6 | PB5 | PB4 | PB3 | PB2 | PB1 | PB0 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
PORTB &= ~(1 << PB1);
Damit ist der Pin nun im sogenannten Tri-State. Das ist ein spezieller dritter Zustand, nämlich hochohmig, den ein Pin annehmen kann. Aber Vorsicht: Setzt du den Eingang als Tri-State und setzt keinen externen Pull-Up Widerstand ein, hat der Pin im Beispiel bei geöffnetem Schalter/Taster einen undefinierten hochohmigen Zustand.
Es ist auch möglich den Eingang durch den Schalter gegen Vcc zu ziehen, den Eingang auf Tri-State zu setzen und mit einem Pull Down Widerstand sicherzustellen dass bei geöffnetem Schalter der Eingang durch den Pull Down sicher auf logisch Null gezogen wird. In dieser Variante hast du bei geschlossenem Schalter eine logische Eins und bei offenem Schalter eine logische Null am Eingang.
Am einfachsten ist es jedoch den internen Pull-Up Widerstand zu nutzen und mit dem Schalter/Taster gegen Masse zu ziehen.
Pinzustand lesen
BearbeitenDen Pinzustand kannst du im Register PINx abrufen. Dieses Register hat 8 Bit und symbolisiert den Zustand des gesamten Ports. Wenn du eine Maske über den Registerwert legst, kannst du auf einzelne Pinzustände prüfen.
Nehmen wir einmal an, dich interessiert der Zustand von Pin PB1, den wir vorhergehend als Eingang konfiguriert haben. Dazu legst du einfach eine Maske auf den Wert in PINB und prüfst, ob dieser ungleich Null ist.
#include <avr/io.h>
int main(void){
DDRB &= ~(1 << PB1); // damit ist dann PB1 ein Eingang
PORTB &= ~(1 << PB1); // PB1 als Tri-State
if(PINB & (1 << PB1)) {
//PB1 ist gesetzt
}
return 0;
}