Im Kapitel 5. Das serielle Programmierinterface wurde bereits ein Programm vorgestellt, das einen Treppenhaus-Licht-Automaten modelliert. Schauen wir das zugehörige Quellprogramm in Assembler genauer an.
Jedes Programm besteht aus wenigstens drei Abteilungen
Am Beginn sollte, auch wenn es am Anfang vor lauter Tatendrang nervt, eine Beschreibung des Programms. Sie sollte kurz auf die Aufgabe eingehen, die das Programm erledigen soll. Eine Aufzählung der Rahmenbedingungen, soweit nützlich und im Sinne des Programms sinnvoll, kann man auch ergänzen. In jedem Fall nützlich ist der Name des Projekts, evtl. inclusive Laufwerk und Pfad.
Es folgen Definitionen für die Aliasnamen von Ports, Registern und Bits durch die Assembler-Steueranweisung ".equ". Es folgt nach einem Leerzeichen der Aliasname und nach einem "="-Zeichen ein bereitsbekannter Wert als Hexadecimal- oder Dezimaladresse oder als bereits definierte Aliasvariable.
Diese Aliasnamen sollten auf die Verwendung der Prozessoradressen für die aktuelle Anwendung hinweisen. Der Assembler-Steuerbefehl ".include" bindet eine Definitionsdatei ein, die auf der Festplatte im Installationsverzeichnis von AVR-Studio 4.0 liegt. Die Datei "tn2313def.inc" definiert Aliasnamen, die anstelle der nichtssagenden Port- und Register Hexadressen im Programm verwendet werden können. Die eigenen Definitionen ergänzen diejenigen in der Datei "tn2313def.inc". Das Programm ist eben leichter lesbar, wenn statt portd die Bezeichnung led_port verwendet wird und anstelle der Bitnummer 4 die Bezeichnung LED. Die Lesbarkeit und der Überblick sind wichtig, vor allem wenn man ein fremdes Programm lesen oder gar pflegen (anpassen, ändern...) soll. Daher sollte man nicht Kommentaren sparen. Alles was nach einem Semikolon ";" kommt wird vom Assembler als Kommentar aufgefasst und nicht übersetzt.
Im Assemblerquellcode ist es übrigens völlig egal ob Groß- oder Kleinschreibung angewandt wird. Auch Symbole (Aliasnamen), Befehle etc. kann man beliebig scheiben, nur die Syntax ("Rechtschrebung") muss stimmen. Danach ist led das gleiche wie LED oder PortD das gleiche wie PORTD. LED_PORT wird nach unten verwendeter Definition im Programm erkannt, nicht jedoch led-port.
Im Vorbereitungteil findet man noch die Pseudoanweisungen ".org". Sie legen den Wert fest, der ab dieser Position im Programmzähler stehen soll. ".org 0x0000" weist den Assembler demnach an, den 2-Byte-Code, der dem Assemblerbefehl "rjmp start" entspricht, als Speicherwort an die Stelle 0 im Programmspeicher (Flash) abzulegen. Damit die Speicheradressen 0x0001 bis 0x0019 frei bleiben, sorgt der Pseudobefehl ".org 0x001A" dafür, dass die Assemblierung des weiteren Programmtextes ab dieser Speicheradresse erfolgt.
Hier der Speicherauszug aus dem AVR (ATTiny 2313), so wie er als Speicherworte von AVR-Studio dargestellt wird. Nicht programmierte Speicherstellen werden als FFFF dargestellt. Beim Brennen (Programmieren) werden an diesen Stellen nur die 0 Bits auf Null gebracht. Wird der Programmspeicher vor dem erneuten Programmieren gelöscht, werden alle Bits wieder auf 1 gekippt.
Das eigentliche Programm beginnt mit der Festlegung eines Namens für diese Stelle im Programm (-speicher), einem sogenannten Label. Labels sind beliebige Zeichenfolgen - ohne Sonderzeichen, die durch einen Doppelpunkt abgeschlossen werden. Labels werden überwiegend als Sprungadressen im Programm verwendet : "rjmp start" führe einen relativen Sprung zur Adresse mit dem Label "start:" durch.
Die erste wichtige Aufgabe eines Programms ist es, den Stackzeiger zu initialisieren. Das machen die ersten beiden Befehle. Die Kommentare machen es überflüssig, dass ich hier im Text noch weiter darauf eingehe.
Der Befehl "sbi" (Set Bit in IO-Register) tut genau das, was der Klartext-Name des Befehls sagt. Indem im Datenrichtungsregister an der Position LED = 4 eine 1 gesetzt wird, legt man diese Leitung als Ausgabeleitung fest. Als nächstes wird diese Portleitung auf +5V am Ausgabepin gelegt. Die LED liegt mit ihren beiden Anschlüssen jetzt auf +5V. Weil keine Potentialdifferenz vorliegt, fließt kein Strom, die LED ist aus. Legt man die Portleitung auf 0V, indem man das Portbit löscht, dann liegt über den Widerstand von 150 Ohm an der LED eine Spannung von ca. 2,2 V, die LED leuchtet. Das kommt später im Programm.
Der Befehl "cbi" setzt das Bit in einem IO-Register auf 0. Hier wird die Leitung taste = 5 auf Eingang geschaltet, indem das korrespondierende Bit im DDRD auf Null gesetzt wird.
Nachdem die nächste Programmstelle später wieder angesprungen werden soll, erhält sie das Label "loop:". Der "in"-Befehl holt den Inhalt des Eingaberegisters von PortD in das Register r16. Dann prüft der Befehl "sbrc r16, taste" ob das Bit taste (=5) im Register r16 gelöscht ist. Dann wäre die Taste gedrückt, weil ihr Kontakt den Eingang auf 0-Potential (GND) legt. Die Folge wäre, dass die LED eingeschalet werden müsste. Im Programm geschieht das dadurch, dass der Befehl "rjmp aus", der das Ausschalten der LED zur Folge hätte, übersprungen wird. "sbrc" bedeutet nämlich "Skip if Bit in Register Clear", also "überspringe den nächsten Befehl, falls das Bit im Register gelöscht ist". Wenn der "aus"-Teil übersprungen wird, wird der angespungene "an"-Teil ausgeführt.
Die damit angestoßene Logik mit dem Überspringen eines anderen Sprungbefehls wird häufig angewand. Sie realisiert die zweiseitige Alternative aus dem Programmflussdiagramm: wenn, dann, sonst. Ein Assembler kennt diese Struktur nicht, sie muss mit Hilfe von Sprung- und Verzweigungsbefehlen modelliert werden.
Ist der "an"-Teil abgearbeitet, folgt ein Sprungbefehl der den "aus"-Teil überspringt, damit der nicht auch noch ausgeführt wird - "rjmp weiter". Dann geht es zurück zur Tastenabfrage. Das Hauptprogramm (main) wird also durch den Bereich der Endlosschleife von "loop:" bis zum Sprungbefehl "rjmp loop" realisiert. Der Bereich von der Marke "start:" bis "Loop:" dient der Initialisierung des Controllers. Diese Struktur findet man in allen Mikrocontroller-Anwendungen.
Zum Vergleich hier noch einmal die zugehörigen Flussdiagramme für eine Hochsprache und die Assemblerentsprechung.
Die Leistung, die ein Assemblerprogrammierer stets selbst zu erbringen hat, nimmt einem Programmierer, der Hochsprachen (Delphi, C, Fortran, Basic ...) benutzt, der Compiler ab. Letzten Endes kommt in jedem Fall ein Maschinenprogramm heraus, das nur lineare Strukturen kennt und zweiseitige Alternativen daher grundsätzlich in Assemblerart codiert. Der Vorteil von Assembler ist eindeutig, dass der Programmierer straff organisierte Strukturen umsetzen kann, während Compiler zwar die Codierung in Maschinencode übernehmen aber nie ein derart kompaktes Programm erzeugen können wie das ein Assember tut. Ich habe das anhand eines Messgeräts mit einem ATTiny 2313 gesehen, das in Basic (Bascom) programmiert, eine Anwendung mit Mühe aufnehmen konnte. In Assembler geschrieben passten zu der Anwendung weitere 2 und es war immer noch reichlich Platz, trotz aufwendiger Strukturen wie die oben beschriebene.
Die Kapitelüberschrift verspricht einen Treppenhauslichtautomaten. Auch dafür hatten wir bereits ein Modell. Ein Blick ins Listing zeigt, dass sich bis zur Marke "loop:" nichts geändert hat.
Ab hier haben wir eine andere Struktur. Die Endlosschleife steht jetzt am Anfang des Hauptprogramms und wird nur durch das Drücken der Taste unterbrochen. Falls die Taste gedrückt ist, wird der Rücksprung zum Anfang der Hauptschleife übersprungen und die LED wird in Folge für eine bestimmte Zeit eingeschaltet und vor dem Rücksprung zur Hauptschleife wieder aus. Eine dreifache Zeitschleife im Programmteil "gedrueckt:" sorgt für eine Verzögerung von ca. 6 Sekunden.
Zum Abschluss die Lösung zum "Blinker"
Der Vorbereitungsteil ist immer noch der selbe, natürlich mit verändertem Titel.
Die Hauptschleife ist ähnlich wie im Modell zum Treppenhauslichtautomaten ausgebildet. Danach wird das 10-malige Blinken vorbereitet. Die Schleife "blinken:" ist ein weiterer Teil des Hauptprogramms. Ab der Marke "weiter:" wird dafür gesorgt, dass in jedem Fall die LED nach dem Verlassen der Blinkerschleife ausgeschaltet wird. In der Blinkerschleife wird zweimal die Procedur "warten" aufgerufen, die für eine Verzögerung von 1 Sekunde sorgt.
Aufgabe:
Wer kann die beiden Befehle nach dem Label (Marke) "loop:" in einem Befehl schreiben?