Grundsätzliches zum Aufbau eines Programms
Bisher haben wir uns ja in erster Linie mit ein paar Codezeilen beschäftigt und bisher hätte kein Compiler ein Programm daraus compilieren können, auch wenn die Aufgaben richtig gelöst waren.
Es gibt Dinge, die müssen enthalten sein, andere wiederum erleichtern uns das Programmieren, bzw. die Übersicht zu behalten.
Definitionsfile
Für den Compiler des ARV-Studio ist es z.B. unentbehrlich zu wissen, auf welchen Prozessor der Code compiliert werden muss, bzw. wie z.B. dessen Ports ect. definiert sind. Diese Information teilen wir dem Compiler mit folgender Zeile mit:
m8def.inc ist eine Datei, welche in folgendem Verzeichnis enthalten sein muss:
C:\Program Files
\Atmel\AVR Tools\AvrAssembler\Appnotes
Im Prinzip handelt es sich dabei um ein Unterprogramm, welches in unser Projekt mit eingebunden wird und welches die Spezifikationen des jeweiligen Prozessors für den Compiler definiert. Bitte achtet darauf, dass Ihr immer das passende Definitionsfile verwendet, für den ATM8 ist es eben die Datei "m8def.inc", wenn wir einen ATM64 programmieren, dann ist das "m64def.inc".
Man kann diese Dateien auch mit einem Editor öffnen (bitte nicht darin herumeditieren) und da steht ganz oben, wozu die jeweilige Datei einzubinden ist:
m88def.inc hat 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 : "m88def.inc"
;* Title : Register/Bit Definitions for the ATMEGA88
;* Date : 14.01.2004
;* Version :
;* Support E-mail :
avr@atmel.com
;* Target MCU : ATMEGA88
Hier sehen wir auch gleich eine Eigenschaft des AVR-Studio, die wir schon ein paarmal angesprochen haben, nämlich das Kommentieren:
Alles, was innerhalb einer Zeile nach einem Semikolon kommt ist ein Kommentar und wird nicht compiliert. Ein Kommentar kann zu Beginn einer Zeile stehen oder einem Befehl folgen.
Am Anfang macht es durchaus Sinn, ein paar Informationen hinter die Strichpunkte zu schreiben, das darf gerne ein wenig ausführlicher sein.
Hier z.B. der Header des Quellcodes für den RC-Link:
Code: Alles auswählen
;Projekt: RC-Link
;offizieller Name: RC Link
;Version: 1.3
;Status: freigegeben
;Autor: Thomas Wyschkony
;Kurzbeschreibung: redundante Daten von RS485-Bus einlesen, uebersetzen und bei Aenderung an RS232 ausgeben
;Zusatz: Erkennung der Aufgleisrichtung
;System: Atmel ATM162
;Zusatz: Unterstuetzung von Matrixdisplay EADOG162
Code: Alles auswählen
ldi r16, 0xFF ; Register 16 mit 0xFF für PortB als Ausgang laden
Seid nicht geizig mit Kommentaren und selbst wenn es beim Programmieren selbst immer klar ist was die jeweilige Zeite wirklich macht, spätestens eine Woche später habt Ihr keine Ahnung mehr, warum da 0xFF ins Register r16 geladen wird... versprochen!
Kommen wir dann zu einem etwas sperrigen, fürchterlich klingenden Begriff, der
Interuptvektortabelle. Ein Interupt ist ein Ereignis, welches entweder von aussen (Hardwareinterupt) oder von innen (Softwareinterupt) eintritt. Dieses Ereignis bewirkt, dass der aktuelle Befehl abgearbeitet wird und dann die zum jeweiligen Interupt vordefinierte Adresse in der Interuptvektortabelle in den Programmzähler geladen wird.
Lasst Euch bitte nicht von dieser gruseligen Beschreibung verunsichern, wir werden auf dieses Thema noch detailierter eingehen.
Der wichtigste und bekannteste Interupt, ein Hardware-Interupt, ist der RESET. Der Reset wird ausgelöst, wenn an entsprechendem Pin des Bausteins der entspechende Pegel anliegt, beim ATM8 wird dazu eine 0 an Pin 1 erwartet.
Tritt dieses Reset-Ereignis ein, wird also der Reset-Interupt ausgelöst, dann springt der Programmzähler sofort auf die Adresse 0x0000 im Flash.
(im Gegensatz zu anderen Interupts wird beim Reset kein Befehl mehr fertig abgearbeitet!)
Sinnigerweise steht genau an dieser Adresse 0x0000 ein unbedingter Sprungbefehl zum eigentlichen Programmanfang.
Die Interupt-Vektortabelle für den ATM8 findet Ihr übrigens im
Datenblatt auf Seite 46. Da steht übrigens auch, dass der Reset auch über die Software ausgelöst werden kann, auf den Watchdog werden wir aber nicht weiter eingehen.
Um dafür zu sorgen, dass nach einem Reset zum Programmanfang gesprungen wird müssen wir folgendes zu Beginn schreiben:
Code: Alles auswählen
.org 0x0000
rjmp Init ; Springe nach einem Reset zum Label "Init"
Wörtlich übersetzt steht hier, dass wir an die Speicheradresse 0x0000 den Sprungbefehl zum Label "Init" schreiben.
In der Regel gibt es bei Mikrocontrollern kein wirkliches
Programmende. Meist soll ja ständig etwas eingelesen, ausgewertet und ausgegeben werden, oder das Programm wartet auf ein Ereignis, eine Situation und reagiert darauf. Aus diesem Grund wird das eigentliche Programm meist als Endlosschleife ausgeführt sein, am Ende wird also wieder zum Anfang des Hauptprogramms (nicht zum Initialisieren) gesprungen.
Wie bei der Interuptvektortabelle geschieht dies durch einen unbedingten Sprung und auch wenn wir die Sprungbefehle erst nächste Woche beleuchten werden möchte ich hier mit dem einfachsten Sprungbefehl vorgreifen:
springt ohne weitere Bedingung zum Label "Hauptprogramm". In Wirklichkeit ermittelt der Compiler die Einsprungadresse des Labels "Hauptprogramm" und schreibt diese beim Aufruf des rjmp-Befehls in den Programmcounter.
So könnte z.B. ein Hauptprogramm aussehen, welches die Signale, welche an einem Port anliegen einfach am anderen Port ausgeben:
Code: Alles auswählen
;Initialisierung ist erledigt, ab hier startet das Hauptprogramm
Hauptprogramm:
in r16, pinb ;Einlesen von PortB nach Register 16
out portc, r16 ;Ausgeben von r16 nach Port C
rjmp Hauptprogramm; springe zum Anfang
Soll ein Programm terminiert, also wirklich beendet werden, dann machen wir das auch mit einer Endlosschleife. Soll z.B. der Eingabeport nur einmal eingelesen und an den Ausgabeport ausgegeben werden, dann sieht das so aus:
Code: Alles auswählen
;Initialisierung haben wir schon gemacht wir sind ja fleissig
Hauptprogramm:
in r16, pinb ;Einlesen von PortB nach Register 16
out portc, r16 ;Ausgeben von r16 nach Port C
Programmende:
rjmp Programmende
Der Sprungbefehl lädt also immer seine eigene Adresse in den Programmzähler, damit läuft das Programm nicht in den undefinierten Bereich des Flash.
Etwas, was wir zwar erstmal nicht brauchen aber dennoch nicht ausser Acht lassen sollten, ist die
Definition des Stapelspeichers. Den Stack und seinen Stackpointer haben wir ja schonmal angesprochen, wir definieren ihn möglichst zum Anfang des Programms, und zwar in dem Bereich, den wir als "Initialisieren" bezeichnen.
Der Stackpointer ist ein 16 Bit grosses Register, welches wir nicht direkt beschreiben können, wir müssen also wieder ein Arbeitsregister hinzuziehen.
Zunächst ermitteln wir das untere Byte der höchstmöglichen SD-RAM-Adresse und schreiben diese in das Arbeitsregister. Anschliessen schreiben wir dessen Inhalt in das Lower Byte des Stackpointers:
Code: Alles auswählen
ldi r16, LOW(RAMEND) ; unteres Byte der hächstmöglichen Adresse holen
out SPL, r16 ; Unteres Byte des SP beschreiben
Anschliessend machen wir das mit dem höheren Byte der höchstmöglichen SD-RAM-Adresse:
Code: Alles auswählen
ldi r16, HIGH(RAMEND) ; oberes Byte der höchstmnöglichen Adresse holen
out SPH, r16 ; oberes Byte des SP beschreiben
Den Stackpointer brauchen wir spätestens dann, wenn wir mit Unterprogrammen und Interupts arbeiten, die Definition des Stack sollten wir aber von Anfang an machen.
Nun wissen wir in etwa, wie ein Assemblerprogramm aufgebaut ist und so sollten wir für die ersten Praxisanwendungen gerüstet sein. Die erste Praxisaufgabe steht ja an, und als nächstes packen wir uns Logik und Arithmetik, sowie einige Bit-Operationen.