|
|
AVR-Tutorial: IO-Grundlagen
[Bearbeiten] HardwareFü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. 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: [Bearbeiten] ZahlensystemeBevor 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:
"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. [Bearbeiten] Ausgabe[Bearbeiten] Assembler-SourcecodeUnser erstes Assemblerprogramm, das wir auf dem Controller laufen lassen möchten, sieht so aus:
[Bearbeiten] AssemblierenDas 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. [Bearbeiten] Hinweis: Konfigurieren der Taktversorgung des ATmega8Beim ATmega8 ist standardmäßig 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. [Bearbeiten] ProgrammerklärungIn 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:
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. Die Erklärungen nach dem Semikolon sind Kommentare und werden vom Assembler nicht beachtet. Der 3. Befehl 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:
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.
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. [Bearbeiten] EingabeIm folgenden Programm wird Port B als Ausgang und Port D als Eingang verwendet:
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. [Bearbeiten] Stolperfalle bei Matrixtastaturen etc.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:
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.
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. [Bearbeiten] Pullup-WiderstandBei 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 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.
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. [Bearbeiten] Zugriff auf einzelne BitsMan 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:
Achtung: Diese Befehle können nur auf die IO-Register angewandt werden! Am besten verstehen kann man das natürlich an einem Beispiel:
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. [Bearbeiten] Zusammenfassung der PortregisterFür jeden Hardwareport gibt es im Mikroprozessor insgesamt 3 Register:
[Bearbeiten] Ausgänge benutzen, wenn mehr Strom benötigt wirdMan 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. 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.
|