AVR-Tutorial: IO-Grundlagen

Wechseln zu: Navigation, Suche

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 findet ihr im Artikel über LEDs und in diesem Thread im Forum.

Standard Led Anschluss

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:

Standard Taster Anschluss

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 0x7F. Umrechnen kann man die Zahlen 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" und "0x" 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 es, dass Hexadezimal- bzw. Binärzahlen bzw. Dezimalzahlen 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 µCs 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 dem Programm yaap. Wenn man das Programm gestartet hat und der ATmega8 richtig erkannt wurde, wählt man aus den Menüs den Punkt "Lock Bits & Fuses" und klickt zunächst auf "Read Fuses". Das Ergebnis sollte so aussehen: Screenshot. Nun ändert man die Kreuze so, dass das folgende Bild entsteht: Screenshot und klickt auf "Write Fuses". Vorsicht, wenn die Einstellungen nicht stimmen, kann es sein, dass 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 Fuse-Bits 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 yaap, 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 würde die einzubindende Datei m8def.inc heißen. 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. Alle Include-Dateien wurden von Atmel in einem gemeinsamen Verzeichnis gespeichert. Das Verzeichnis ist bei einer Standardinstallation am PC auf C:\Programme\Atmel\AVR Tools\AvrAssembler\Appnotes\. Einige Include-Dateien heißen

 AT90s2313:  2313def.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 an den Anfang der Datei geschrieben:

 
;***************************************************************************
;* 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            :"2313def.inc"
;* Title                :Register/Bit Definitions for the AT90S2313
;* Date                 :99.01.28
;* Version              :1.30
;* Support E-Mail       :avr@atmel.com
;* Target MCU           :AT90S2313
...

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 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 bedutet, 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, 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 normalen Register 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. Ein 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 16 wird mit dem Inhalt des IO-Registers "PIND" geladen

Wer mehr über die Befehle wissen möchte, sollte sich die PDF-Datei Instruction Set (1,27 MB) runterladen (benötigt Acrobat Reader 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 Prefix "0b" gehört nicht zur Zahl, sondern sagt dem Assembler, dass die nachfolgende Zahl in binärer Form interpretiert werden soll.

bits.gif

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:

Download leds+buttons.asm

 
.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

Standard Taster Anschluss

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.

Taster bei Benutzung des interen Pullup

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 2 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 0 (low) ist.
  • sbis ("skip if bit set") bewirkt das Gleiche, wenn das Bit 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 allen Dingen 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 mittel 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:

Download bitaccess.asm

  
.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 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 Bit Position steht für Ausgang, eine 0 steht für Eingang.
  • Das Einleseregister PINx. Es wird verwendet um von einem Mikroprozessor-Pin den aktuellen, extern anliegenden Zustand einzulesen. Dazu muss das entsprechende Datenrichtungsbit auf Eingang geschaltet sein.
  • Das Ausgangsregister PORTx. Es erfüllt 2 Funktionen, je nachdem wie das zugehörige Datenrichtungsbit geschaltet ist.
    • Steht es auf Ausgang, so wird bei einer entsprechenden 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.
  • 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 Bit Position des PINx Register schreibt.

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.

Transistor Treiberstufe

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. hier. 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 hier.