Tutorial zur ersten Verwendung des ATmega8 Experimentierboards (Teil 2)
Der zweite Teil des Tutorials beschreibt schrittweise die Erstellung eines kleinen Beispielprogramms (ca 30 Zeilen), welches einen Wert vom PC erhalten, und diesen über Port-Pins des Atmega8 ausgeben soll, um so die LEDs anzusteuern.
Auf Ulis Seiten wird immer auf den Editor Programmers Notepad verwiesen, weil dieser minimalistisch und daher gut zu beschreiben ist. Hinzu kommt, dass gerade Anfänger durch die vielen Möglichkeiten einer "größeren" Entwicklungsumgebung häufig eher behindert werden. Daher mache ich das in diesem Text genauso, möchte aber an dieser Stelle auch das kostenfreie AVR-Studio von Atmel empfehlen, weil hier ein Simulator enthalten ist, der beim Verständnis für die Hardware und für die Fehlersuche sehr hilfreich sein kann. (Mehr Simulation: Wer mit SPICE umgehen kann, wird von den Möglichkeiten in VM-LAB schnell begeistert sein. Damit lässt sich z.B. ein Oszilloskop gleich mit simulieren.)
Neues Projekt mit Programmers Notepad
- Programmers Notepad starten und ein neues Projekt anlegen
- File->New->Projekt
- Name: "usart_test"
- Folder: Ordner angeben
- Makefile erzeugen (Skript, welches z.B. das Kompilieren automatisiert)
- Das Programm MFile starten
- Im Menü "Makefile" folgende Einstellung auswählen
- MCUtype->ATmega->atmega8
(Der Rest kann erstmal wie vorgegeben bleiben) - Mit "Save as" das Makefile in den oben angegebenen Projektordner speichern
- Das Makefile in Programmers Notepad öffnen und bearbeiten
- Im Projektbaum mit der rechten Maustaste auf das Projekt "usart_test" klicken und "Add Files..." auswählen
- Zum gespeicherten Makefile im Projektordner navigieren und importieren
- Das Makefile durch Doppelklick zum Editieren öffnen
- Prozessortakt auf 12Mhz ändern:
F_CPU = 12000000
- Programmerbezeichnung ändern:
AVRDUDE_PROGRAMMER = usbasp - Das Makefile speichern
- Erstellen einer Datei main.c
- File->New->C / C++
- Speichern als main.c
- main.c wie das Makefile in den Projektbaum importieren
Was soll das Programm leisten?
Der im ersten Teil für die USB-Kommunikation vorbereitete Attiny2313, ist mit dem seriellen Interface (USART) des Atmega8 verbunden. Um Daten empfangen zu können, muss das Programm zunächst das serielle Interface aktivieren und für die richtige Übertragungsart konfigurieren. Weiterhin muss PORTD/PORTB als Ausgang zur Darstellung des übertragenen Wertes eingerichtet werden. Anschließend soll das Programm neu eintreffenden Werte endlos vom seriellen Interface lesen und an PORTD/PORTB ausgeben.
Das Programm abschnittweise
Benötigte Makros und Definitionen
Im Programm werden wir einige Definitionen und Makros verwenden. Beispielsweise müssen die Adressen von IO-Registern dann nicht als Zahlenwert geschrieben werden, sondern es können "sprechende Namen" dafür verwendet werden. Diese Definitionen werden von der AVR Libc in header-Dateien bereitgestellt. Diese Bibliothek wurde mit WinAVR installiert und kann wie folgt verwendet werden.
Unser Programm benötigt für die Ausgabe des Wertes an PORTD und PORTB die Adressen der Register, welche diesen Port steuern. Dazu teilen wir in der ersten Zeile des Programms dem Präprozessor mit, er solle an dieser Stelle die Definitionen für die Ein- Ausgangsports hinzufügen.
1 |
#include <avr/io.h>
|
Außerdem soll das Programm auf eintreffende Daten vom PC asynchron reagieren können. Dazu ist die USART-Peripherie im ATmega8 in der Lage, das laufende Programm zu unterbrechen. Sie löst dazu einen Interrupt aus und lässt den µC an eine bestimmte Stelle im Programm springen, an der die Anweisungen zur Verarbeitung der eingegangenen Daten stehen. (mehr unten bei ISR) Für die Programmierung mit Interrupts gibt es in der AVR Libc ebenfalls ein header-File.
2 |
#include <avr/interrupt.h>
|
Der ATtiny2313 sendet die Daten in einer Geschwindigkeit von 9600 Baud. Für diesen Wert legen wir einen Namen (BAUD) fest, den wir im Programm verwenden können, um der USART-Schnittstelle im ATmega8 mitteilen zu können, dass diese ebenfalls mit 9600 Baud arbeiten soll. Der Präprozessor wird dann vor dem Kompilieren alle Vorkommen von "BAUD" durch "9600" ersetzen.
3 |
#define BAUD 9600
|
Leider reicht für die korrekte Einstellung der USART die Angabe der Baudrate nicht aus, denn diese ergibt nur Sinn in Bezug auf die Taktfrequenz des µC. Die Taktfrequenz erhält der Präprozessor aus dem Makefile, dort haben wir ja oben den Namen F_CPU bereits erstellt. Folgende Präprozessor-Anweisung stellt den Wert BAUD über eine einfache Formel (aus dem Datenblatt des ATmega8) in Beziehung zur Taktfrequenz F_CPU und definiert einen Namen für den so errechneten Wert.
4 |
#define UBRR_VAL F_CPU/16/BAUD-1
|
Definition von Variablen
Im Programm wird nur eine Variable zum Zwischenspeichern des eingetroffenen Wertes benötigt.
-> Sie ist vom Typ char und somit 8-Bit "breit". (Genauso "groß" ist auch der Wert, der jeweils empfangen werden soll.)
-> Außerdem soll die Variable nicht Vorzeichenbehaftet sein, hat also mit unsigned einen Wertebereich von 0 bis 255.
- >Mit volatile wird auf etwas Optimierung verzichtet und Compiler sorgt dafür, dass der Wert dieser Variable nicht zeitweise nur in/aus Registern geschrieben/gelesen wird, sondern dass der Wert immer direkt in den Speicher geschrieben und von dort aus auch gelesen wird. Volatile wird immer dann benötigt, wenn Variablen innerhalb von Interrupt Service Routinen geschrieben/gelesen werden, die auch im "normalen" Programmablauf geschrieben/gelesen werden.
5 |
volatile unsigned char receive_char;
|
Initialisierung der USART-Schnittstelle
Für die Konfiguration der USART-Peripherie, sind eine ganze Reihe von Kontroll-Registern vorgesehen. Ein Blick ins Datenblatt lohnt auf jeden Fall. Für unser Beispiel braucht es aber nur Änderungen in zwei Registern, weil die Standardeinstellung gut passt.
Zunächst beschreiben wir das USART Baud Rate Register (UBRR) zur Einstellung der Baudrate mit dem an die Funktion übergebenen Wert in unsigned int ubrr. Da dieser Wert für ein Register (8-Bit) zu groß werden kann, haben die AVR-Entwickler das "Register" in UBRRH für die 8 höherwertigen Bit und in UBRRL für die niederwertigen Bit aufgeteilt.
Durch die Bitverschiebung um 8 Stellen nach rechts und anschließendes casten zu unsigned char erhalten wir in Zeile 7 genau die benötigten oberen 8 Bit und schreiben diese in das Register UBRRH. Für die unteren 8 Bit reicht der cast, denn dabei werden die oberen 8 Bit einfach abgeschnitten/vergessen.
Als nächstes wird im USART Control and Status Register B (UCSRB) das Senden und Empfangen aktiviert. Dazu wird im Register an den Stellen TXEN und RXEN eine 1 eingetragen. (Erinnerung: TXEN, RXEN sind Namen für Konstanten aus io.h)
Außerdem wollen wir ja, dass die USART-Einheit des Atmega8 einen Interrupt auslöst, wenn ein neues Zeichen (ein neuer Wert) eingegangen ist. Dazu wird auch an der Stelle RXCIE eine 1 eingetragen.
6 |
void init_uart(unsigned int ubrr) { |
Die main-Funktion
Diese Funktion wird beim Start des µC als erstes aufgerufen. Hier beginnt das Programm mit dem Aufruf der Funktion init_usart mit der oben errechneten und definierten Konstante UBRR_VAL als Parameter.
Als nächstes werden im Data Direction Register D (DDRD) alle freien Pins des PORTD als Ausgang definiert. PD0 und PD1 sind ja bereits über die Jumper mit dem Tiny2313 verbunden und stehen uns daher für die Ansteuerung der LEDs nicht mehr zur Verfügung. Daher werden zwei Pins eines anderen Ports benötigt, wenn jedes Bit unseres Wertes eine LED ansteuern soll. Wir verwenden dazu hier die ersten beiden Pins des PORTB. (Hinweis: Einstellen der Datenrichung von Portpins)
Die Anweisung sei() (set global interrupt enable) aktiviert das globale Interrupt Flag im Status Register (SREG). Bei Auftreten eines Interrupt wird also ab jetzt an die entsprechende Stelle (Interrupt Service Routine: siehe unten) im Programm gesprungen.
Als Letztes folgt noch eine endlose While-Schleife. Darin "kreist" der µC unablässig, nur ab und an durch den Interrupt der USART-Peripherie unterbrochen. Gewöhnlich steht hier das "Hauptprogramm" der Anwendung, unsere ist jedoch so einfach, dass die Schleife hier einfach leer bleibt. (Man könnte darin den µC schlafen legen, ein Interrupt würde ihn dann jeweils wieder aufwecken.) Die ganze Arbeit wird in diesem Beispiel in der ISR gemacht.
11 12 13 14 15 16 17 18 |
int main(void) { init_uart(UBRR_VAL); DDRD = DDRD | 0b11111100; DDRB = DDRB | 0b00000011; sei(); while (1) { } } |
Die Interrupt Service Routine (ISR)
Eine ISR ist ein Block von Anweisungen, der direkt nach einem Interrupt ausgeführt wird. Dazu springt der µC je nach Interruptquelle (z.B. USART) an eine, durch die Hardware festgelegte Adresse im Codesegment (Flash). Diese Adresse befindet sich in der so genannten Interrupt-Vector-Tabelle ganz am Anfang des Codesegments.
Für unser Beispiel muss also in der Interrupt-Vector-Tabelle an der Adresse, die nach dem Empfang eines Wertes von USART angesprungen wird, ein weiterer Sprungbefehl hin zu unserer ISR-Routine eingetragen werden.
Zum Glück erledigen dies alles der Präprozessor und Compiler für uns, wenn eine Interrupt-Service-Routine im Programm enthalten ist.
( ISR(x_vect) ist kein gewöhnlicher Funktionskopf sondern Makros aus interrupt.h. In den Klammern ist angegeben, welcher Interruptquelle der folgende Code zugeordnet ist. In der Dokumentation zur avr-libc findet sich eine Tabelle mit den Interrupt-Vector-Bezeichnungen für die AVR-Familie. )
Der neu empfangene Wert ist von der USART in das Register UDR eingetragen worden, von dort aus kopieren wir den Wert in unsere Hilfsvariable receive_char, um diesen direkt danach über den Port D und Port B auszugeben. Dabei verändern wir nur die Bits 2 bis 7 von PORTD und die Bits 0 und 1 von PORTB. Alle anderen Bits der beiden Register behalten ihren Wert. (-> Bitmanipulation)
19 20 21 22 23 |
ISR(USART_RXC_vect) { receive_char = (UDR); PORTD = (PORTD & 0b00000011) | (receive_char & 0b11111100); PORTB = (PORTB & 0b11111100) | (receive_char & 0b00000011); } |
Es sind also eigentlich nur drei Zeilen Code nötig, um die eigentliche Arbeit zu erledigen. (Es ist oft so, dass die Schwierigkeit beim Experimentieren mit µC eher in der korrekten Konfiguration der beteiligten Komponenten liegt, als im Programmteil zur Lösung der eigentlichen Aufgabe.)
Das Programm auf einen Blick (main.c)
Hier der fast vollständige Quelltext für unser kleines Beispiel. Dieser kann per copy n' paste von der Webseite in die Datei main.c eingefügt werden.
main.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <avr/io.h> #include <avr/interrupt.h> #define BAUD 9600 #define UBRR_VAL F_CPU/16/BAUD-1 volatile unsigned char receive_char; void init_uart(unsigned int ubrr) { UBRRH = (unsigned char)(ubrr>>8); UBRRL = (unsigned char)(ubrr); UCSRB |= (1<<TXEN | 1<<RXEN | 1<<RXCIE); } ISR(USART_RXC_vect) { receive_char = (UDR); PORTD = (PORTD & 0b00000011) | (receive_char & 0b11111100); PORTB = (PORTB & 0b11111100) | (receive_char & 0b00000011); } int main(void) { init_uart(UBRR_VAL); DDRD = DDRD | 0b11111100; DDRB = DDRB | 0b00000011; sei(); while (1) { // Mal "entkommentieren" und schauen was passiert --> // while (!(UCSRA & (1<<UDRE))); //Warten bis USART wieder frei // UDR = receive_char; } } |
Warum "fast vollständig"?
Es fehlt ein ganz wesentlicher Teil: Die Kommentare zu bestimmten Programm-Zeilen und Blöcken und zu allen Funktionen und Variablen.
// Kommentar bis zum Ende der Zeile
/* Kommentar bis zum ...
... Vorkommen der Zeichenfolge: */
Kommentare sind bestenfalls so geschrieben, dass auch eine dritte Person das Programm versteht. Darauf achtet man am besten immer, denn wer an einem Programm arbeiten muss, das er/sie vor Jahren selbst geschrieben und seither nicht mehr angesehen hat, stellt sich dabei häufig Fragen wie:
"Das soll ich geschrieben haben? - Wie bin ich bloß auf diese geniale Idee gekommen!?" oder "Mensch, wie konnte ich nur so einen Unsinn schreiben?".
Also ist man am Ende evtl. selbst jene dritte Person, für die man hoffentlich in der Vergangenheit sorgfältig Kommentare verfasst hat ;-)
Beschreiben des ATmega8 und Ausprobieren
Ab damit ins Codesegment
Wie wir das Codesegment eines AVR beschreiben, haben wir ja schon im ersten Teil des Tutorials gesehen. Ganz ähnlich verfahren wir jetzt auch mit unserem selbst geschriebenen Programm. Natürlich wird der USB-ASP jetzt an SV1 angeschlossen, denn wir wollen uns ja dieses mal mit dem ATmega8 verbinden. Ausserdem müssen die Einstellungen der sog. FUSEBIT hier etwas anders und lauten:
lfuse:0x3f / hfuse:0xdf
(Damit wird u.a. nicht mehr die interne Takt-Quelle (interner RC-Schwingkeis mit 1Mhz) genutzt, sondern der externe 12Mhz Quarz. Genau für diese Frequenz haben wir ja im Programm die USART konfiguriert. Stimmt das nicht überein, geschehen wundersame Dinge.)
LED verdrahten
Es werden die vom Programm genutzten Ausgänge PD2-PD7 und PB0-PB1 mit den Eingängen des Treiber IC2 verbunden und dabei auf die Reihenfolge geachtet, damit wir beim Testen auch wirklich den übertragenen Wert als Binärzahl ablesen können.
Ausprobieren
Wenn alles geklappt habt, können wir nun - mit einem Terminalprogramm (z.B. Hterm) - ein Zeichen an das Board senden. Schnell noch eine ASCII-Tabelle zur Hand genommen und nachgeschaut, ob das Zeichen auch richtig übertragen wird...
Voher müssen natürlich noch die Jumper JP5 gebrückt werden, denn die verbinden ja schließlich das geniale USB-Interfache im Tiny2313 mit unserm Programm im mega8.
... und gelobt werden
Du warst wirklich ein "blutiger Anfänger"? Und hast es erfolgreich bis hier her geschafft!? Dann hast du ein riesen Lob verdient und Uli und ich sagen: Das war gute Arbeit!
(Und wenn du doch irgendwo im Tutorial stecken geblieben bist, oder du Vorschläge zur Verbesserung dieses Tutorials hast, dann melde dich bitte im Forum :)
Mit dieser Grundlage, und Begeisterung für die Sache, kannst du jetzt die weiteren Möglichkeiten des ATmega8 Experimentierboards erkunden. Lass dich dabei nicht entmutigen, denn Fehler macht jeder und man hat um so mehr gelernt, je länger man einen Fehler intensiv suchen musste.
Gruß
Thomas
Vorherige Seite: Tutorial zur ersten Verwendung des ATmega8-Experimentierboards (Teil 1)
Nächste Seite: AVR -> DMX