AVR-Tutorial: IO-Grundlagen
Hardware
Für die ersten Versuche braucht man nur ein paar Taster und LEDs an die IO-Ports des AVRs anzuschließen. An PB0–PB5 schließt man 6 LEDs über einen Vorwiderstand von je 1 kΩ gegen Vcc (5V) an. In der Praxis ist es unerheblich, ob der Widerstand vor oder nach der Diode liegt – wichtig ist nur, dass er da ist. Weitere Details zu LEDs und entsprechenden Vorwiderständen sind im Artikel über LEDs und in diesem Thread im Forum zu finden.
Dass die LEDs an den gleichen Pins wie der ISP-Programmer angeschlossen sind, stört übrigens normalerweise nicht. Falls wider Erwarten deshalb Probleme auftreten sollten, kann man versuchen, den Vorwiderstand der LEDs zu vergrößern.
An PD0–PD3 kommen 4 Taster mit je einem 10-kΩ-Pullup-Widerstand:
Zahlensysteme
Bevor es losgeht, hier noch ein paar Worte zu den verschiedenen Zahlensystemen.
Binärzahlen werden für den Assembler im Format 0b00111010
geschrieben, Hexadezimalzahlen als 0x3A
oder $3A
(0x
und $
bedeuten dasselbe[1] und werden im Laufe der nächsten Kapitel gemischt verwendet). Umrechnen kann man die Zahlendarstellungen z. B. mit dem Windows-Rechner. Hier ein paar Beispiele:
dezimal | hexadezimal | binär |
---|---|---|
0 | 0x00 | 0b00000000 |
1 | 0x01 | 0b00000001 |
2 | 0x02 | 0b00000010 |
3 | 0x03 | 0b00000011 |
4 | 0x04 | 0b00000100 |
5 | 0x05 | 0b00000101 |
6 | 0x06 | 0b00000110 |
7 | 0x07 | 0b00000111 |
8 | 0x08 | 0b00001000 |
9 | 0x09 | 0b00001001 |
10 | 0x0A | 0b00001010 |
11 | 0x0B | 0b00001011 |
12 | 0x0C | 0b00001100 |
13 | 0x0D | 0b00001101 |
14 | 0x0E | 0b00001110 |
15 | 0x0F | 0b00001111 |
100 | 0x64 | 0b01100100 |
255 | 0xFF | 0b11111111 |
0b
bzw. 0x
und $
haben für die Berechnung keine Bedeutung; sie zeigen nur an, dass es sich bei dieser Zahl um eine Binär- bzw. Hexadezimalzahl handelt.
Wichtig dabei ist, dass Hexadezimal-, Binär- und Dezimaldarstellungen nur unterschiedliche Schreibweisen für immer das Gleiche sind: eine Zahl. Welche Schreibweise bevorzugt wird, hängt auch vom Verwendungszweck ab. Je nachdem kann die eine oder die andere Schreibweise klarer sein.
Auch noch sehr wichtig: Computer und Mikrocontroller beginnen immer bei 0 zu zählen, d. h., wenn es 8 Dinge (Bits etc.) gibt, hat das erste die Nummer 0, das zweite die Nummer 1, …, und das letzte (das 8.) die Nummer 7(!).
Ausgabe
Assembler-Sourcecode
Unser erstes Assemblerprogramm, das wir auf dem Controller laufen lassen möchten, sieht so aus:
.include "m8def.inc" ; Definitionsdatei für den Prozessortyp einbinden
ldi r16, 0xFF ; lade Arbeitsregister r16 mit der Konstanten 0xFF
out DDRB, r16 ; Inhalt von r16 ins IO-Register DDRB ausgeben
ldi r16, 0b11111100 ; 0b11111100 in r16 laden
out PORTB, r16 ; r16 ins IO-Register PORTB ausgeben
ende: rjmp ende ; Sprung zur Marke „ende“ → Endlosschleife
Assemblieren
Das Programm muss mit der Endung .asm
abgespeichert werden, z. B. als leds.asm
. Diese Datei können wir aber noch nicht direkt auf den Controller programmieren. Zuerst müssen wir sie dem Assembler füttern. Bei wavrasm funktioniert das z. B., indem wir ein neues Fenster öffnen, den Programmtext hineinkopieren, speichern und auf „assemble“ klicken. Wichtig ist, dass sich die Datei m8def.inc
(wird beim Atmel-Assembler mitgeliefert) im gleichen Verzeichnis wie die Assembler-Datei befindet. Der Assembler übersetzt die Klartext-Befehle des Assemblercodes in für den Mikrocontroller verständlichen Binärcode und gibt ihn in Form einer sogenannten „Hex-Datei“ aus. Diese Datei kann man dann mit der entsprechenden Software direkt in den Controller programmieren.
Hinweis: Konfigurieren der Taktversorgung des ATmega8
Beim ATmega8 ist vom Hersteller der interne 1-MHz-Oszillator aktiviert; weil dieser für viele Anwendungen (z. B. das UART, siehe späteres Kapitel) aber nicht genau genug ist, soll der Mikrocontroller seinen Takt aus dem angeschlossenen 4-MHz-Quarzoszillator beziehen. Dazu müssen ein paar Einstellungen an den Fusebits des Controllers vorgenommen werden. Am besten und sichersten geht das mit einem GUI-Programm wie z. B. myAVR ProgTool. Wenn man das Programm gestartet hat und der ATmega8 richtig erkannt wurde, klickt man unter dem Reiter „Brennen“ im Abschnitt „Fuses brennen“ auf den Knopf „Bearbeiten“ und im sich öffnenden Fenster zunächst auf den Knopf „Hardware Auslesen“. Das Ergebnis sollte so aussehen wie in diesem Screenshot. Nun ändert man die Radiobuttons und das Häkchen so, dass das folgende Bild entsteht
und klickt auf „Jetzt Schreiben“. Vorsicht, wenn die Einstellungen nicht stimmen, kann es sein, dass z. B. die ISP-Programmierung deaktiviert wird und man den AVR somit nicht mehr programmieren kann! Die Fusebits bleiben übrigens nach dem Löschen des Controllers aktiv, müssen also nur ein einziges Mal eingestellt werden. Mehr über die Fusebits findet sich im Artikel AVR Fuses.
Nach dem Assemblieren sollte eine neue Datei mit dem Namen leds.hex
oder leds.rom
vorhanden sein, die man mit AVRDUDE, dem myAVR ProgTool, PonyProg oder AVRISP in den Flash-Speicher des Mikrocontrollers laden kann. Wenn alles geklappt hat, leuchten jetzt die ersten beiden angeschlossenen LEDs.
Programmerklärung
In der ersten Zeile wird die Datei m8def.inc
eingebunden, welche die prozessortypischen Bezeichnungen für die verschiedenen Register definiert. Wenn diese Datei fehlen würde, wüsste der Assembler nicht, was mit PORTB
, DDRD
usw. gemeint ist. Für jeden AVR-Mikrocontroller gibt es eine eigene derartige Include-Datei, da zwar die Registerbezeichnungen bei allen Controllern mehr oder weniger gleich sind, die Register aber auf unterschiedlichen Controllern unterschiedlich am Chip angeordnet sind und nicht alle Funktionsregister auf allen Prozessoren existieren. Für einen ATmega8 beispielsweise heißt die einzubindende Datei m8def.inc
. Normalerweise ist also im Namen der Datei der Name des Chips in irgendeiner Form, auch abgekürzt, enthalten. Kennt man den korrekten Namen einmal nicht, so sieht man ganz einfach nach. Die Include-Dateien sind beim Atmel-Studio 7 in mehreren Verzeichnissen gespeichert; diese lauten bei einer Standardinstallation am PC z. B.
C:\Program Files (x86)\Atmel\Studio\7.0\packs\atmel\ATmega_DFP\1.7.374\avrasm\inc
für ATmega-Controller oder
C:\Program Files (x86)\Atmel\Studio\7.0\packs\atmel\ATtiny_DFP\1.10.348\avrasm\inc
für ATtiny-Controller. Einige Include-Dateien heißen
ATmega48A: m48Adef.inc ATmega8: m8def.inc ATmega16: m16def.inc ATmega32: m32def.inc ATTiny12: tn12def.inc ATTiny2313: tn2313def.inc
Um sicher zu gehen, dass man die richtige Include-Datei hat, kann man diese mit einem Texteditor (AVR-Studio oder Notepad) öffnen. Der Name des Prozessors wurde von Atmel immer am Anfang der Datei erwähnt:
;***** THIS IS A MACHINE GENERATED FILE - DO NOT EDIT ********************
;***** Created: 2011-02-09 12:03 ******* Source: ATmega8.xml *************
;*************************************************************************
;* A P P L I C A T I O N N O T E F O R T H E A V R F A M I L Y
;*
;* Number : AVR000
;* File Name : "m8def.inc"
;* Title : Register/Bit Definitions for the ATmega8
;* Date : 2011-02-09
;* Version : 2.35
;* Support E-mail : avr@atmel.com
;* Target MCU : ATmega8
; ...
Aber jetzt weiter mit dem selbstgeschriebenen Programm.
In der 2. Zeile wird mit dem Befehl ldi r16, 0xFF
der Wert 0xFF
(entspricht 0b11111111
) in das Register r16 geladen (mehr Infos unter Adressierung). Die AVRs besitzen 32 Arbeitsregister, r0–r31, die als Zwischenspeicher zwischen den I/O-Registern (z. B. DDRB, PORTB, UDR, …) und dem RAM genutzt werden. Zu beachten ist außerdem, dass die ersten 16 Register (r0–r15) nicht von jedem Assemblerbefehl genutzt werden können. Ein Register kann man sich als eine Speicherzelle direkt im Mikrocontroller vorstellen. Natürlich besitzt der Controller noch viel mehr Speicherzellen, die werden aber ausschließlich zum Abspeichern von Daten verwendet. Um diese Daten zu manipulieren, müssen sie zuerst in eines der Register geladen werden. Nur dort ist es möglich, die Daten zu manipulieren und zu verändern. Ein Register ist also vergleichbar mit einer Arbeitsfläche, während der restliche Speicher eher einem Stauraum entspricht. Will man arbeiten, so muss das Werkstück (= die Daten) aus dem Stauraum auf die Arbeitsfläche geholt werden und kann dann dort bearbeitet werden.
Der Befehl ldi
lädt jetzt einen bestimmten konstanten Wert in so ein Arbeitsregister. In diesem Fall kommt der zu ladende Wert also nicht aus dem Stauraum, sondern der Programmierer kennt ihn bereits. Auch Assemblerbefehle sind nicht einfach willkürlich gewählte Buchstabenkombinationen, sondern sind oft Abkürzungen für eine bestimmte Aktion. ldi
bedeutet in Langform load immediate. Load ist klar – laden. Und immediate bedeutet, dass der zu ladende Wert beim Befehl selber angegeben wurde (engl. immediate – unmittelbar).
Die Erklärungen nach dem Semikolon sind Kommentare und werden vom Assembler nicht beachtet.
Der 3. Befehl, out DDRB, r16
, gibt den Inhalt von r16 (= 0xFF
) in das Datenrichtungsregister für Port B aus. Das Datenrichtungsregister legt fest, welche Portpins als Ausgang und welche als Eingang genutzt werden. Steht in diesem Register ein Bit auf 0, wird der entsprechende Pin als Eingang konfiguriert, steht es auf 1, ist der Pin ein Ausgang. In diesem Fall sind also alle 6 Pins von Port B Ausgänge. Datenrichtungsregister können ebenfalls nicht direkt beschrieben werden, daher muss man den Umweg über eines der Arbeitsregister r16–r31 gehen.
Der nächste Befehl, ldi r16, 0b11111100
, lädt den Wert 0b11111100
in das Arbeitsregister r16, der durch den darauffolgenden Befehl out PORTB, r16
in das I/O-Register PORTB (und damit an den Port, an dem die LEDs angeschlossen sind) ausgegeben wird. Eine 1 im PORTB-Register bedeutet, dass an dem entsprechenden Anschluss des Controllers die Spannung 5V anliegt, bei einer 0 sind es 0V (Masse).
Schließlich wird mit rjmp ende
ein Sprung zur Marke ende:
ausgelöst, also an die gleiche Stelle, wodurch eine Endlosschleife entsteht. Sprungmarken schreibt man gewöhnlich an den Anfang der Zeile, Befehle in die 2. und Kommentare in die 3. Spalte. Eine Marke ist einfach nur ein symbolischer Name, auf den man sich in Befehlen beziehen kann. Sie steht stellvertretend für die Speicheradresse des unmittelbar folgenden Befehls. Der Assembler, der den geschriebenen Text in eine für den µC ausführbare Form bringt, führt über die Marken Buch und ersetzt in den eigentlichen Befehlen die Referenzierungen auf die Marken mit den korrekten Speicheradressen.
Bei Kopier- und Ladebefehlen (ldi
, in
, out
, …) wird immer der 2. Operand in den ersten kopiert:
ldi r17, 15 ; das Register r17 wird mit der Konstanten 15 geladen
mov r16, r17 ; das Register r16 wird mit dem Inhalt des Registers r17 geladen
out PORTB, r16 ; das IO-Register „PORTB“ wird mit dem Inhalt des Registers r16 geladen
in r16, PIND ; das Register r16 wird mit dem Inhalt des IO-Registers „PIND“ geladen
Wer mehr über die Befehle wissen möchte, sollte das AVR® Instruction Set Manual (PDF; 1,2 MB) konsultieren oder in der Hilfe von Assembler oder AVR-Studio nachschauen. Achtung: nicht alle Befehle sind auf jedem Controller der AVR-Serie verwendbar!
Nun sollten die beiden ersten LEDs leuchten, weil die Portpins PB0 und PB1 durch die Ausgabe von 0 (low) auf Masse (0V) gelegt werden und somit ein Strom durch die gegen Vcc (5V) geschalteten LEDs fließen kann. Die 4 anderen LEDs sind aus, da die entsprechenden Pins durch die Ausgabe von 1 (high) auf 5V liegen.
Warum leuchten die beiden ersten LEDs, wo doch die beiden letzen Bits auf 0 gesetzt sind? Das liegt daran, dass man die Bitzahlen von rechts nach links schreibt. Ganz rechts steht das niedrigstwertige Bit („LSB“, Least Significant Bit), das man als Bit 0 bezeichnet, und ganz links das höchstwertige Bit („MSB“, Most Significant Bit), bzw. Bit 7. Das Präfix „0b
“ gehört nicht zur Zahl, sondern sagt dem Assembler, dass die nachfolgende Zahl in binärer Form interpretiert werden soll.
Das LSB steht für PB0, und das MSB für PB7 … aber PB7 gibt es doch z. B. beim AT90S4433 gar nicht, es geht doch nur bis PB5? Der Grund ist einfach: Am Gehäuse des AT90S4433 gibt es nicht genug Pins für den kompletten Port B, deshalb existieren die beiden obersten Bits nur intern.
Eingabe
Im folgenden Programm wird Port B als Ausgang und Port D als Eingang verwendet:
.include "m8def.inc"
ldi r16, 0xFF
out DDRB, r16 ; Alle Pins am Port B durch Ausgabe von 0xFF ins
; Richtungsregister DDRB als Ausgang konfigurieren
ldi r16, 0x00
out DDRD, r16 ; Alle Pins am Port D durch Ausgabe von 0x00 ins
; Richtungsregister DDRD als Eingang konfigurieren
loop:
in r16, PIND ; an Port D anliegende Werte (Taster) nach r16 einlesen
out PORTB, r16 ; Inhalt von r16 an Port B ausgeben
rjmp loop ; Sprung zu "loop:" -> Endlosschleife
Wenn der Port D als Eingang geschaltet ist, können die anliegenden Daten über das IO-Register PIND eingelesen werden. Dazu wird der Befehl in
verwendet, der ein IO-Register (in diesem Fall PIND) in ein Arbeitsregister (z. B. r16) kopiert. Danach wird der Inhalt von r16 mit dem Befehl out
an Port B ausgegeben. Dieser Umweg ist notwendig, da man nicht direkt von einem IO-Register in ein anderes kopieren kann.
rjmp loop
sorgt dafür, dass die Befehle in r16, PIND
und out PORTB, r16
andauernd wiederholt werden, so dass immer die zu den gedrückten Tasten passenden LEDs leuchten.
Achtung: Auch wenn es hier nicht explizit erwähnt wird: Man kann natürlich jeden Pin eines jeden Ports einzeln auf Ein- oder Ausgabe schalten. Dass hier ein kompletter Port jeweils als Eingabe bzw. Ausgabe benutzt wurde, ist reine Bequemlichkeit.
In komplexeren Situationen als der einfachen Verbindung eines Port-Pins mit einem Taster, der zuverlässig auf GND-Potential zieht, ist die Schaltschwelle des Eingangstreibers zu beachten. Diese liegt bei etwa 50 % der Versorgungsspannung. In dieser Testschaltung wird dieser Aspekt genauer untersucht.
Mögliche Zeitverzögerungen
Vorsicht! In bestimmten Situationen kann es passieren, dass scheinbar Pins nicht richtig gelesen werden.
Speziell bei der Abfrage von Matrixtastaturen kann der Effekt auftreten, dass Tasten scheinbar nicht reagieren. Typische Sequenzen sehen dann so aus:
ldi r16, 0x0F
out DDRD, r16 ; oberes Nibble Eingang, unteres Ausgang
ldi r16, 0xFE
out PORTD, r16 ; PD0 auf 0 ziehen, PD4..7 Pull ups aktiv
in r17, PIND ; Pins lesen schlägt hier fehl!
Warum ist das problematisch? Nun, der AVR ist ein RISC-Microcontroller, welcher die meisten Befehle in einem Takt ausführt. Gleichzeitig werden aber alle Eingangssignale über FlipFlops abgetastet (synchronisiert), damit sie sauber im AVR zur Verfügung stehen. Dadurch ergibt sich eine Verzögerung (Latenz) von bis zu 1,5 Takten, mit der auf externe Signale reagiert werden kann. Die Erklärung dazu findet man im Datenblatt unter der Überschrift „I/O Ports – Reading the Pin Value“.
Was tun? Wenn der Wert einer Port-Eingabe von einer unmittelbar vorangehenden Port-Ausgabe abhängt, muss man wenigstens einen weiteren Befehl zwischen beiden einfügen, im einfachsten Fall ein nop
. nop
bedeutet in Langform no operation, und genau das macht der Befehl auch: nichts. Er dient einzig und alleine dazu, dass der Prozessor einen Befehl abarbeitet, also etwas zu tun hat, aber ansonsten an den Registern oder sonstigen Internals nichts verändert.
ldi r16, 0x0F
out DDRD, r16 ; oberes Nibble Eingang, unteres Ausgang
ldi r16, 0xFE
out PORTD, r16 ; PD0 auf 0 ziehem, PD4..7 Pull ups aktiv
NOP ; Delay der Synchronisations-FlipFlops ausgleichen
in r17, PIND ; Pins lesen ist hier OK.
Ein weiteres Beispiel für dieses Verhalten bei rasch aufeinanderfolgenden out
- und in
-Anweisungen ist in einem Forenbeitrag zur Abfrage des Busyflag bei einem LCD angegeben. Dort spielen allerdings weitere, vom LCD-Controller abhängige Timings eine wesentliche Rolle für den korrekten Programmablauf.
Pullup-Widerstand
Bei der Besprechung der notwendigen Beschaltung der Ports wurde an einen Eingangspin jeweils ein Taster mit einem Widerstand nach Vcc vorgeschlagen. Diesen Widerstand nennt man einen Pullup-Widerstand. Wenn der Taster geöffnet ist, so ist es seine Aufgabe, den Eingangspegel am Pin auf Vcc zu ziehen. Daher auch der Name: „pull up“ (engl. für hochziehen). Ohne diesen Pullup-Widerstand würde ansonsten der Pin bei geöffnetem Taster in der Luft hängen, also weder mit Vcc noch mit GND verbunden sein. Dieser Zustand ist aber unbedingt zu vermeiden, da bereits elektromagnetische Einstreuungen auf Zuleitungen ausreichen, dem Pin einen Zustand vorzugaukeln, der in Wirklichkeit nicht existiert. Der Pullup-Widerstand sorgt also für einen definierten 1-Pegel bei geöffnetem Taster. Wird der Taster geschlossen, so stellt dieser eine direkte Verbindung zu GND her und der Pegel am Pin fällt auf GND. Durch den Pullup-Widerstand rinnt dann ein kleiner Strom von Vcc nach GND. Da Pullup-Widerstände in der Regel aber relativ hochohmig sind, stört dieser kleine Strom meistens nicht weiter.
Anstelle eines externen Widerstandes wäre es auch möglich, den Widerstand wegzulassen und stattdessen den in den AVR eingebauten Pullup-Widerstand zu aktivieren. Die Beschaltung eines Tasters vereinfacht sich dann zum einfachst möglichen Fall: Der Taster wird direkt an den Eingangspin des µC angeschlossen und schaltet nach Masse durch.
Das geht allerdings nur dann, wenn der entsprechende Mikroprozessor-Pin auf Eingang geschaltet wurde. Ein Pullup-Widerstand hat nun mal nur bei einem Eingangspin einen Sinn. Bei einem auf Ausgang geschalteten Pin sorgt der Mikroprozessor dafür, dass ein dem Port-Wert entsprechender Spannungspegel ausgegeben wird. Ein Pullup-Widerstand wäre in so einem Fall kontraproduktiv, da der Widerstand versucht, den Pegel am Pin auf Vcc zu ziehen, während eine 0 im Port-Register dafür sorgt, dass der Mikroprozessor versuchen würde, den Pin auf GND zu ziehen.
Ein Pullup-Widerstand an einem Eingangspin wird durch das PORT-Register gesteuert. Das PORT-Register erfüllt also zwei Aufgaben. Bei einem auf Ausgang geschalteten Port steuert es den Pegel an den Ausgangspins. Bei einem auf Eingang geschalteten Port steuert es, ob die internen Pullup-Widerstände aktiviert werden oder nicht. Ein 1-Bit aktiviert den entsprechenden Pullup-Widerstand.
DDRx | PORTx | IO-Pin-Zustand |
---|---|---|
0 | 0 | Eingang ohne Pull-Up (Resetzustand) |
0 | 1 | Eingang mit Pull-Up |
1 | 0 | Push-Pull-Ausgang auf LOW |
1 | 1 | Push-Pull-Ausgang auf HIGH |
.include "m8def.inc"
ldi r16, 0xFF
out DDRB, r16 ; Alle Pins am Port B durch Ausgabe von 0xFF ins
; Richtungsregister DDRB als Ausgang konfigurieren
ldi r16, 0x00
out DDRD, r16 ; Alle Pins am Port D durch Ausgabe von 0x00 ins
; Richtungsregister DDRD als Eingang konfigurieren
ldi r16, 0xFF ; An allen Pins vom Port D die Pullup-Widerstände
out PORTD, r16 ; aktivieren. Dies geht deshalb durch eine Ausgabe
; nach PORTD, da ja der Port auf Eingang gestellt ist.
loop:
in r16, PIND ; an Port D anliegende Werte (Taster) nach r16 einlesen
out PORTB, r16 ; Inhalt von r16 an Port B ausgeben
rjmp loop ; zu "loop:" → Endlosschleife
Werden auf diese Art und Weise die AVR-internen Pullup-Widerstände aktiviert, so sind keine externen Widerstände mehr notwendig und die Beschaltung vereinfacht sich zu einem Taster, der einfach nur den µC-Pin mit GND verbindet.
Zugriff auf einzelne Bits
Man muss nicht immer ein ganzes Register auf einmal einlesen oder mit einem neuen Wert laden. Es gibt auch Befehle, mit denen man einzelne Bits abfragen und ändern kann:
- Der Befehl
sbic
(skip if bit cleared) überspringt den darauffolgenden Befehl, wenn das angegebene Bit gleich 0 (low) ist. sbis
(skip if bit set) bewirkt das gleiche, wenn das Bit gleich 1 (high) ist.- Mit
cbi
(clear bit) wird das angegebene Bit auf 0 gesetzt. sbi
(set bit) bewirkt das Gegenteil.
Achtung: Diese Befehle können nur auf die IO-Register angewandt werden!
Der große Vorteil vor allem der cbi
- bzw. sbi
-Instruktionen ist es, dass sie tatsächlich nur ein einziges Bit am Port manipulieren. Dies ist insbesonders dann interessant, wenn an einem Port mehrere LED hängen, die unterschiedliche Dinge anzeigen. Will man dann eine bestimmte LED ein- bzw. ausschalten, dann sollen sich ja deswegen die anderen LED nicht verändern. Greift man mittels out
auf den kompletten Port zu, dann muss man dies berücksichtigen. Wird die eine LED aber mittels cbi
bzw. sbi
manipuliert, dann braucht man sich um die anderen LED an diesem Port nicht kümmern – deren Zustand verändert sich durch cbi
bzw. sbi
nicht.
Am besten verstehen kann man das natürlich an einem Beispiel:
.include "m8def.inc"
ldi r16, 0xFF
out DDRB, r16 ; Port B ist Ausgang
ldi r16, 0x00
out DDRD, r16 ; Port D ist Eingang
ldi r16, 0xFF
out PORTB, r16 ; PORTB auf 0xFF setzen → alle LEDs aus
loop: sbic PIND, 0 ; "skip if bit cleared", nächsten Befehl überspringen,
; wenn Bit 0 im IO-Register PIND =0 (Taste 0 gedrückt)
rjmp loop ; Sprung zu "loop:" → Endlosschleife
cbi PORTB, 3 ; Bit 3 im IO-Register PORTB auf 0 setzen → 4. LED an
ende: rjmp ende ; Endlosschleife
Dieses Programm wartet so lange in einer Schleife (loop:
… rjmp loop
), bis Bit 0 im Register PIND
gleich 0 wird, also die erste Taste gedrückt ist. Durch sbic
wird dann der Sprungbefehl zu loop:
übersprungen, die Schleife wird also verlassen und das Programm danach fortgesetzt. Ganz am Ende schließlich wird das Programm durch eine leere Endlosschleife praktisch „angehalten“, da es ansonsten wieder von vorne beginnen würde.
Zusammenfassung der Portregister
Für jeden Hardwareport gibt es im Mikroprozessor insgesamt 3 Register:
- Das Datenrichtungsregister DDRx. Es wird verwendet, um die Richtung jedes einzelnen Mikroprozessor-Pins festzulegen. Eine 1 an der entsprechenden Bitposition steht für Ausgang, eine 0 steht für Eingang.
- Das Einleseregister PINx.
- Ist das entsprechende Datenrichtungsbit auf Eingang geschaltet, wird es verwendet, um von einem Mikroprozessor-Pin den aktuellen, extern anliegenden Zustand einzulesen.
- Bei den neueren AVR (wie z. B. ATtiny13, ATtiny2313, ATtiny24/44/84, ATtiny25/45/85, ATmega48/88/168, usw.) kann man als Ausgang konfigurierte Pins „toggeln“ (PORTx zwischen 0 und 1 umschalten), indem man eine 1 an die entsprechende Bitposition des PINx-Registers schreibt.
- Das Ausgangsregister PORTx. Es erfüllt zwei Funktionen, je nachdem wie das zugehörige Datenrichtungsbit geschaltet ist.
- Steht es auf Ausgang, so wird bei einer Zuweisung an das PORTx-Register der entsprechende Mikroprozessor-Pin auf den angegebenen Wert gesetzt.
- Steht es auf Eingang, so beeinflusst das PORTx-Bit den internen Pullup-Widerstand an diesem Mikroprozessor-Pin. Bei einer 0 wird der Widerstand abgeschaltet, bei einer 1 wird der Widerstand an den Eingangs-Pin zugeschaltet.
Ausgänge benutzen, wenn mehr Strom benötigt wird
Man kann nicht jeden beliebigen Verbraucher nach dem LED-Vorbild von oben an einen µC anschließen. Die Ausgänge des ATMega8 können nur eine begrenzte Menge Strom liefern, so dass der Chip schnell überfordert ist, wenn eine nachgeschaltete Schaltung mehr Strom benötigt. Die Ausgangstreiber des µC würden in solchen Fällen den Dienst quittieren und durchbrennen.
Abhilfe schafft in solchen Fällen eine zusätzliche Treiberstufe, die im einfachsten Fall mit einem Transistor als Schalter aufgebaut wird.
Die LED samt zugehörigen Widerständen dienen hier lediglich als Sinnbild für den Verbraucher, der vom µC ein und ausgeschaltet werden soll. Welcher Transistor als Schalter benutzt werden kann, hängt vom Stromverbrauch des Verbrauchers ab. Die Widerstände R1 und R2 werden als Basiswiderstände der Transistoren bezeichnet. Für ihre Berechnung siehe z. B. den Artikel Basiswiderstand. Um eine sichere Störfestigkeit im Resetfall des Mikrocontrollers zu gewähren (wenn der µC daher die Ausgänge noch nicht ansteuert), sollte man noch einen Pulldown-Widerstand zwischen Basis und Emitter schalten oder einen digitalen Transistor (z. B. BCR135) mit integriertem Basis- und Basisemitterwiderstand benutzen.
Um ein Relais an einen µC-Ausgang anzuschließen, siehe den Artikel Relais mit Logik ansteuern.