Aus RN-Wissen.de
Wechseln zu: Navigation, Suche


AVR Assembler Einführung

Es gibt mehrere Gründe, sich mit dem AVR-Assembler zu beschäftigen:

  • reines Interesse
  • Man hat eben keinen anderen Compiler, Bascom kostet ja was und GCC-C kann man möglicherweise genausowenig, also warum nicht gleich.
  • Endlich Programmieren ohne Sprach-Restriktionen
  • Programmierung des Tiny12 (ohne SRAM, daher weder BASCOM noch GCC-C sinnvoll)
  • (theoretisch) schnellerer und kurzer Code
  • man möchte genaue (auf den Taktzyklus) Verzögerungen / Ausführungszeiten

Dadurch ergeben sich aber auch verschiedene Haupt-Zielgruppen für eine Einführung. Die eine ist in einer- oder mehreren anderen Sprachen durchaus versiert, und möchte endlich auch mal richtig in die Tiefen der Hardware steigen. Die andere ist ein totaler Neueinsteiger und hat gehört, dass man Micro-Controller am besten mit Assembler programmiert.

Es soll hier nur eine Starthilfe zum Einstieg gegeben werden. Viele Details und Varianten werden hier bei weitem nicht erschöpfend behandelt.

Unterschiede zu anderen Sprachen

Man kann Assembler nicht direkt mit anderen Hochsprachen vergleichen. Es ist falsch gesagt Assembler ist viel besser als C, oder C ist viel besser als Assembler. Hochsprachen haben generell ihren Vorteil in kürzerer (Programmier-) Zeit viel mehr erreichen zu können. Während man in Assembler jedes Detail programmieren muss schreibt man in der Hochsprache was man genau möchte und der Compiler erledigt das dann für einen, indem er den Code dann in Assembler übersetzt. Der Vorteil liegt also klar auf der Hand, man spart sich eine Menge (unnötiger) Arbeit in dem man schneller sagen kann was man eigentlich machen möchte. Wie das jetzt genau funktioniert und was man dabei beachten muss übernimmt der Compiler. Da gibt es dann aber auch schon den ersten Nachteil. Fängt man mit einer Hochsprache an, braucht man sich hardwaremäßig kaum mit dem µC auseinander zu setzen und weiß deshalb auch nicht genau drüber Bescheid. Funktioniert dann etwas nicht sitzt man auf dem Schlauch. Die Compiler werden aber immer besser, so kommen immer seltener Fehler zustande.

Anders als bei Hochsprachen ist es für die Programmierung in ASM nötig dass man die Architektur der CPU kennt. Ein kurze Beschreibung findet sich hier. Ob dies ein Nachteil ist, ist eine Glaubensfrage. Wenn man sich kaum mehr mit der Hardware auseinander setzt und nicht wissen muss wie ein µC eigentlich funktioniert geht die "Kultur" verloren. Man lernt gar nicht zu schätzen was das Ding jetzt eigentlich leistet. Die Ansprüche werden immer höher gesetzt und jeder Aussetzer ist umso schlimmer.

Ein extremes Beispiel: Ein eiserner Geschäftsmann geht nach Spanien und braucht Arbeiter, er schickt einen Gehilfen auf die Suche nach Arbeiter und schickt diese dann aufs Feld arbeiten. Ohne auch nur irgend etwas über deren Kultur zu wissen verlangt er das hart gearbeitet wird. Doch schon in der ersten Woche gibt es Probleme, die Arbeiter möchten Mittags eine Pause. "Was, eine 3 Stündige Siesta? Seit ihr verrückt? Die in Japan arbeiten die Mittagszeit ja auch durch! Wie kommt ihr auf die Idee 3 Stunden Pause zu machen?"

Vorteile von Assembler sind also dass man weiß, was der µC genau macht (bzw. für Anfänger "was er tun sollte"). Man hat die volle Kontrolle und fast jeder Befehl wird bei einem AVR mit einem Takt ausgeführt. Man kann immer noch auf eine andere Hochsprache umsteigen, diese beherrscht man dann um so besser.

Einziger Nachteil, man muss alles genau definieren, so wie es der µC haben möchte und kann nicht direkt auf fertige Funktionen zurück greifen. Man schafft in der selben Zeit zwar genau so viel Code, der Code macht dann aber weniger als in C.

Bildhaft ist der Unterschied der: Beim Assembler hat man ein weißes Stück Papier (ohne Linien) und ein Alphabet von A-Z. Aus diesen Buchstaben soll man nun erstmal Worte suchen und dann damit einen sinnvollen Text verfassen.

Bei "höheren" Sprachen ist das Papier zumindest mal liniert, und dazu kriegt man auch ein Wörterbuch und die Grammatik. Teile des Textes sind schon vorgeschrieben und ich muß nurmehr bei den ..... Punkten was einsetzen.

Anders kann man auch sagen, Hochsprachen sind wie ein Baukasten, man muss nur nach seinen Bedürfnissen sich die Teile zusammen "stecken", ab und zu fertigt man sich auch eigene Teile. Aber das meiste ist vorgefertigt und nach eigenen Bedürfnissen schnell angepasst. In Assembler muss man sich alles selber anfertigen, fertige Module gibt es nicht, es sei denn man hat es schon einmal gemacht und zufällig da.

  • Daten

Höhere Sprachen kennen verschiedene Datentypen in einigen möglichen Längen, integer, floating point, Strings. Dadurch sind aber auch gleich die möglichen und zulässigen Operationen schon eingeschränkt.

Beim (AVR) Assembler gibt auch einige Datentypen. Sie unterscheiden sich aber nur durch die Größe, d.h. die Anzahl der Bytes, die sie belegen. Es gibt aber keine Einschränkungen bezüglich der Befehle, die man darauf anwenden kann.

  • Vor- und Nachteile

Wenn man den Assembler mit höheren Sprachen vergleicht, um vielleicht Vor- und Nachteile herausarbeiten zu können, kommt man schnell drauf, dass in jedem Vorteil für eine der Seiten auch schon der Nachteil drinnen steckt. Was ich beim Assembler an Performance heraushole, weil ich z.B. "Goodies" eines Controllers ausnutze, bezahle ich, wenn ich mir einen anderen kaufe und dann das ganze Programm umbauen muss.

Installation AVR-Studio

Die Installation des ATMEL AVR Studios ist herzergreifend einfach.

  • Man bekommt von Atmel das AStudio4b01.exe (oder was halt gerade aktuell ist) , das man einfach startet und schon ist es passiert.
  • Wenn man nun das Käferchen doppelklickt und startet,
    • erzeugt man am besten gleich mal ein neues Project.
    • Man wählt den Pfad des Projects, und
    • Wenn man seinen Brennadapter im Menü nicht findet, wählt man eben den Simulator und verwendet dann halt das PonyProg, oder was man halt hat.
    • avrstudio erzeugt ein leeres Projekt und eine leere Assemblersource, die so heißt wie das Projekt. Das Gewurstel mit der Make-file nimmt uns AvrStudio ab.

Struktur eines AVR-Maschinenprogrammes

Bei höheren Sprachen wird diese Struktur vom Compiler/Linker erzeugt, beim Assembler muß man sich selbst darum kümmern.

Asm Flow.jpg

Der Controller beginnt mit der Abarbeitung links oben bei der Programmspeicheradresse 0000. Wenn man Interrupts verwenden will, muß man gleich das nächste Bereich (ISR-Vectoren) überspringen (mit "JMP"). Wenn nicht, kann man sich diese Vektoren aber auch wegdenken.

Man landet so oder so bei den Befehlen, die der Initialisierung dienen (Setzen der Startbedingung). Das ist zumindest die Festlegung des Stack-Pointers, sonst kann man keine "CALLS" oder Interrupts durchführen.

Danach kommt man von oben in die Haupt-Programm-Schleife, die (üblicherweise) immer wieder ohne Ende durchlaufen wird.

Das "End" ist bei Microcontrollern daher auch kaum wirklich notwendig. Wenn es da ist, ist das eine ewige Wiederholung eines einzigen Befehls.

Source-Code Muster

Das folgende Codebeispiel ist eine reine Basis-Initialisierung ohne irgendeine erkennbare Funktion. Für den Einstieg ist es am besten, das einfach abzuschreiben, wie es ist, und dann an den bezeichnenten Stellen mit eigenen Befehlen nach und nach zu erweitern.

 .NOLIST                    ; List-Output unterdrücken
 .INCLUDE <m8def.inc>       ; das gibt es für jeden Controllertyp
 .LIST                      ; List-Output wieder aufdrehen
 .CSEG                      ; was nun folgt, gehört in den FLASH-Speicher


 ;------------------------------------------------------
 ;     Start Adresse 0000
 ;------------------------------------------------------
 RESET:
     rjmp INIT           ; springen nach "INIT"
                         ; bei mehr als 16 kBytes Flash ggf. auch jmp INIT
   
 ;------------------------------------------------------
 ;     ISR VECTORS
 ;------------------------------------------------------
 ;    .....    hier kommen dann die Sprungadressen für die Interrupts rein
 ;             dazu kommen wir noch


 .ORG INT_VECTORS_SIZE    ; dadurch haben wir für die Vektoren Platz gelassen
 INIT:  
 ;------------------------------------------------------
 ;     INITIALIZE
 ;------------------------------------------------------
     ldi r24,high(RAMEND)     ;Stack Pointer setzen 
     out SPH,r24              ; "RAMEND" ist in m8def.inc (s.o.) festgelegt
     ldi r24,low(RAMEND)      ; 
     out SPL,r24              ;

 ;------------------------------------------------------
 ;   eigene Initialisierungen
 ;------------------------------------------------------
 ;....
 ;....
 ;....

 ;------------------------------------------------------
 ;   HAUPTSCHLEIFE
 ;------------------------------------------------------
 Hauptschleife: 
 ;....   eigene befehle
 ;....   eigene befehle
 ;....   eigene befehle
        rjmp Hauptschleife         ; immer wiederholen 

 ;------------------------------------------------------
 ;   ENDE
 ;------------------------------------------------------
 Ende:  
        rjmp Ende


Der Ablauf der Übersetzung

Der Assembler ist ja selbst auch nur ein Programm und zu Beginn recht spartanisch ausgestattet: Er hat einen Daten-Buffer zur Verfügung, in den er den Maschinencode hineinschreiben soll, einen Pointer (Zeiger) dazu, damit er immer weiß, wo er grad ist, und eine allgemeine Tabelle, wo er Texte mit Zahlen verknüpfen kann. Alles ist zu Beginn leer bzw. auf NULL. Mit diesen Voraussetzungen fängt er an, unseren Source-Text zeilenweise zu lesen. Und in jeder Zeile (die nicht leer ist) erwartet er eine Anweisung, was zu tun ist

Assembler Direktiven/Präprozessor

Das sind Anweisungen an den Assembler selbst und erzeugen keine Maschinenbefehle. Im obigen Source-Beispiel sind einige enthalten.

.NOLIST                    

Normalerweise schreibt der Assembler ja eine Übersetzungsliste. Nach dieser Anweisung läßt er das bleiben

.INCLUDE <m8def.inc>       ; das gibt es für jeden Controllertyp

sagt ihm, er soll jetzt erstmal die (text) datei "m8def.inc" lesen und genauso behandeln, als wäre diese Datei mit dem Editor hier reinkopiert worden. In dieser Datei werden u.A. die Namen zu den IO-Registern definiert.

.LIST

Ab nun wird wieder alles in der Übersetzungsliste protokolliert.

.CSEG

was danach folgt, bezieht sich auf den FLASH-Programm-Speicher

Gleich ein Hinweis: Der "AVR Assembler 2" geht dazu über, die C-Syntax für 
Präprozessoranweisungen zu verwenden. Diese haben dann als Kennzeichen ein "#" vorangestellt.

Bemerkungen / Kommentare

;------------------------------------------------------

Alles, was NACH einem Semikolon [=Strichpunkt] steht, schreibt er einfach nur in die Übersetzungsliste, sonst macht er nix. Gerade in ASM sind Kommentare wichtig für die Verständlichkeit. Anders als es hier für die Einführung geschieht, sind später die großen Zusammenhänge wichtig, nicht die Wirkung jedes einzelnen Befehls - das erklärt der Befehl dann schon selber.

Label (Sprungmarke)

RESET:

Was gleich ganz links in der Zeile beginnt (und mit einem Doppelpunkt abgeschlossen ist) schreibt er einfach in die genannte Tabelle und den momentanen Wert des Instruction Pointers (IP) dazu. Sonst hat das im Moment keine weitere Bedeutung. Natürlich, wenn er den gleichen Text schon mal hatte, gibt es eine Fehlermeldung, denn ein Label muss eindeutig sein

Commands

    jmp INIT           ; springen nach "INIT"

Alles, was nicht ganz links steht und auch nix anderes ist, betrachtet der Assembler als "Command". Normalerweise ist das eben eine AVR-Instruktion aus dem "Instruction Set". Hier ist das "JMP", also im Programm einfach woanders weitermachen. Gut, aber wo ? Der Rechner braucht nun unbedingt eine Zahl, denn "INIT" sagt ihm ja nichts. Also sieht er nun in der Tabelle nach, ob er den Text "INIT" schon mal hatte. Hatte er nicht, denn zu der Stelle weiter hinten, wo "INIT:" als Sprungmarke steht (s.o) ist er ja noch garnicht gekommen.

Ich kann leider nicht genau sagen, welche Strategie der AVR-Assembler nun anwendet, dazu gibt es keine Doku. Manche Assembler lesen erstmal überhaupt nur den Sourcetext von vorn bis hinten durch, um alle "Sprungmarken" zu sammeln (Pass I) und fangen dann nochmal von vorne an (Pass II), diesmal mit schon gefüllter Tabelle. Andere machen das etwas gefinkelter. Ist uns aber eigentlich egal.

Jedenfalls findet er letztlich den Text "INIT" und daneben steht die Nummer der Speicherstelle, wo wir eben "INIT:" hingeschrieben haben. Und nun schreibt der Assembler also den Maschinencode für "JMP" in den Buffer und dazu die Zahl aus der Tabelle. Das war mal ein Befehl, also addiert er auf seinen Zeiger eins drauf und ist mit diesem Befehl fertig.

Den Befehl "JMP" findet man nur bei AVRs mit mehr als 8 kBytes Flash. Bei den kleineren µCs gibt es nur den Befehl "RJMP", der das gleiche macht, aber nur eine begrenzte Reichweite für den Sprung zuläßt. Bis 8 kBytes ist diese Begrenzung aber nicht relevant. Entsprechendes gilt für "CALL" und "RCALL".

ORG / JMP

Das, was für den Controller beim Ablauf dann ein "JMP" ist, ist das ".ORG" für den Assembler. Wenn also bislang der Assembler den FLASH-Speicher von 0 - nn mit Befehlen befüllt hat, macht er durch

.ORG INT_VECTORS_SIZE    

mit der Stelle weiter, die er über die gleiche Tabelle unter "INT_VECTORS_SIZE" gefunden hat. Dieser Tabelleneintrag, genauso wie "RAMEND" an anderer Stelle, entsteht aus der Datei "m8def.inc". Wie im vorliegenen Fall macht man diese "Assembler-Jumps", um Speicherstellen freizuhalten.

Merkwürdiges

Befehlsadressen / Byteadressen

Ein Maschinenbefehl besteht aus maximal drei, bit-mäßig verschachtelten Teilen. Die meisten Befehle brauchen dafür genau 16 Bit (also zwei Byte), manchmal reicht das nicht, da gehören dann nochmal 16 Bit dazu. Kleiner als 16 Bit ist aber keiner. Aus diesem Grund wäre es natürlich sinnlos, Adressen für Befehle in Bytes auszudrücken, wenn es eh immer Vielfache von 2 sein müssen, das kleinste Bit einer solchen Adresse wäre ja immer null.

Also adressiert man Commands in "Worten" von 16 - Bit bzw. 2 Bytes

Beim Ablegen eines Labels (Sprungmarke) werden also Wortadressen in der Tabelle angelegt. Solange der Wert auch als Befehls-Adresse verwendet wird (z.B. "JMP Label"), braucht das nicht zu kümmern, denn genauso will es der AVR ja dann auch haben. Wenn dort aber Daten stehen und wir wollen diese adressieren, müssen wird den Wert mal 2 nehmen (oder einmal nach links schieben, was dasselbe ist), sonst landen wir im Nirwana.

3 ist nicht gleich 3

Eine Zahl bedeutet also bei der Maschinensprache unter Umständen ganz was anderes, je nachdem, in welchem Zusammenhang sie verwendet wird. Bei allen Befehlen, die nur für die Register 16 - 31 gehen, steht im Maschinencode für das Register 16 nicht 16 drin, sondern 0. die "16" addiert er dann bei der Befehlsausführung drauf (für den Techniker: er braucht ja nur das Bit 24 hochzuziehen)

GPR ist nicht gleich GPR

Was auch beim Einstieg leicht irritieren kann ist der Umstand, daß die 32 sogenannten "General Purpose Register" irgendwie so gar nicht generell und gleich sind. Manche Befehle kann ich mit allen 32 Registern machen, einige nur mit R16-R31, andere von R24 aufwärts und dann sind da noch R26 bis R31, die heissen dann manchmal auch noch X, Y und Z

SFR ist gar nicht gleich GPR

Special Function Register sind überhaupt seltsam, die meisten GPR-Befehle gehen da gleich garnicht, dafür gibt es andererseits ein paar Bit-Manipulationen, die gehen wiederum nur mit diesen. Dann gibt es auch Bit-Abfragen, die gehen da wie dort und bewirken auch das Gleiche, aber dafür heissen sie dann anders.

Assembler-Befehle

Viele dieser Probleme nimmt uns der Assembler ab, viele aber nicht. Dort erscheinen die Instruktionen immer so

CMD ARG1 ARG2

wobei die Argumente 1 u. 2 optionell, d.h. bei Bedarf erscheinen. Command ist natürlich immer da. ARG1 gibt in der Regel an wo das Ergebnis hin geschrieben wird. Trotz der Hilfe des Assemblers durch "Warnings" und "Errors" ist bei den Argumenten aber immer Vorsicht geboten. Wir haben gehört, daß Arg 1 und Arg 2 im Grunde einfach nur zwei Zahlen sind, die dann zusammen mit dem Command-Code zum Maschinenbefehl werden. Wenn es dem Assembler also auch nur irgendwie gelingt, aus dem, was wir da hinschreiben, Zahlen zu machen die er umwandeln kann, nimmt er sie auch. Neuere Versionen des Assemblers unterscheiden zwischen Registerbezeichnungen und Zahlen und können so so machen Fehler erkennen.

Wirklich merken muss man sich die ganzen Befehle nicht gleich. Dafür gibt es die ein oder andere Übersicht über die möglichen Befehle, z.B. im Datenblatt des µC als "Instruction Set Summary" (2-3 Seiten). Die Namen der Befehle ergeben sich meist aus der englischen Erklärung der Funktion. Leider hat Atmel da selbst im Datenblatt teils noch ein paar Fehler (vor allem beim S- Flag im SREG) eingebaut.

Zwischenbilanz

Einige dieser sonderbaren Dinge lassen sich leicht erklären, manche schwer, manche mangels Unterlagen darüber gar nicht. Deswegen versuch ich es hier auch gleich gar nicht.

So oder so sind es Fakten, an die man sich am besten einfach gewöhnt. Das scheint und ist am Anfang sicher schwer, solange man noch mit den einzelnen Befehlen kämpft. RISC-Befehle werden aber erst in bestimmten Gruppierungen überhaupt wirklich sinnvoll, und durch diese sich immer wieder wiederholenden Gruppen wird es sehr schnell leichter.

Beim Briefe schreiben denkt man ja auch nicht in Buchstaben, das wäre mühsam, sondern in Worten oder in ganzen Sätzen.

Load & Store

Die meisten Befehle für den AVR laufen nach dem Load & Store (Laden und Speichern) Prinzip ab. Das heißt, die Daten werden zuerst aus dem SRAM-Speicher (oder den I/O bzw. SFR Registern) in die allgemeinen Register (GPR) geladen, dort verarbeitet, und danach wieder in den SRAM oder die SFR zurückgestellt.

Asm cmd.jpg

Das ist natürlich eine recht allgemeine Darstellung, aber es hilft, das Instruction Set des AVR schon ein wenig besser zu verstehen.

Es bedeutet, um z.B. ein Byte aus der einen Ecke des SRAM in eine andere zu kopieren, brauchen wir wenigstens 2 Maschineninstruktionen, einmal Load und einmal Store. Und wenn wir was an dem Byte ändern wollen, kommt dieser Befehl natürlich dazu.

Es gibt zu (fast) jeder Art "Load"-Befehl ein Äquivalent als "Store", und zum Finden genügt es meist, statt "LD" an der selben Stelle ein "ST" zu nehmen.

  LDS    GPR, adresse  ; lade das Byte von der Adresse in ein GPR  
  STS    adresse, GPR  ; speichere den Inhalt von GPR in der Adresse

Wenn es um die I/O Register (SFR) geht:

  IN     GPR, I/O-Register    ; GPR <-----I/O Register 
  OUT    I/O-Register, GPR    ; I/O Register <---- GPR

Für feste Werte (Literals) gibt es natürlich keine Store-Variante, denn diese Werte stehen ja im FLASH (Programm) Speicher, und da kann man nicht einfach einzelne Bytes verändern.

  LDI    GPR, wert

Als eine Einschränkung gibt es diese Form nur für die oberen 16 Register, also R16 bis R31.

Beispiel

An PORTC haben wir (mit Vorwiderständen) ein paar LEDs angeschlossen. Und die sollen einfach nur zeigen, welche 0-er und 1-er am PORTB anliegen.

 ; Eigene Initialisierungen
  LDI R24, 0            ; der wert "0" ins allgemeine Register R24
  OUT DDRB, R24         ; das kommt ins Controll-register f. PortB
                        ; dadurch sind diese PINs als INPUT festgelegt
  LDI R24, 255          ; der wert "255" = FF ins allgemeine Register R24
  OUT DDRC, R24         ; das kommt ins Controll-register f. PortC
                        ; dadurch ist das Port auf OUTPUT gestellt
 ; Eigene Befehle (in der Hauptschleife)
  IN  R24, PINB         ; die Bits vom PortB ins allgemeine Register R24
  OUT PORTC, r24        ; schreiben wir nach PortC
                        ; wo ein 1-er ist, leuchtet nun die LED

Wenn wir diese Befehle in das Programm-Schema oben einfügen, haben wir also das erste Assemblerprogramm schon geschrieben.

Programm-Map

Eine interessanter Output, wenn wir dieses Programm mit F7 übersetzen lassen, ist die Map-File. Sie ist ein Abbild der oben genannten "Text <--> Adresse"-Tabelle

(gekürzt)

AVRASM ver. 2.0.28  D:\avr_c\avrstudio\Dokutest\Dokutest.asm Sat Dec 10 11:23:45 2005
....
EQU  sram_start   00000060
EQU  sram_size    00000800
EQU  ramend       0000085f
......
EQU  sph          0000003e
EQU  spl          0000003d
EQU  portb        00000018
EQU  ddrb         00000017
EQU  pinb         00000016
EQU  portc        00000015
EQU  ddrc         00000014
EQU  pinc         00000013
EQU  int_vectors_size 0000002a
CSEG reset        00000000
CSEG init         0000002a
CSEG hauptschleife 00000032
CSEG ende         00000035

Wir finden darin alle Sprungadressen und alle sonstigen Werte, die wir verwendet haben. Tatsächlich in den Maschinencode eingesetzt werden vom Assembler immer die Zahlen, die daneben stehen

Pointer-Register

In vielen Fällen ist es nicht sinnvoll, für Load & Store explizit jedesmal die Speicheradresse dazuschreiben zu müssen. Es können auch die Pointer Register X, Y und Z verwendet werden. Das sind die Register

R26:R27 = X 
R28:R29 = Y 
R30:R31 = Z 

Da wir ja oft mehr Speicher haben als 256 Byte, werden immer zwei Register zusammengefaßt und können dadurch die Bereiche 0 - 65535 abdecken (64k). Gut, aber wie kriegen wir so große Zahlen da rein ? Wir haben ja nur einen 8-Bit Processor und nicht 16.

Wir müssen also die Adresse 8-Bit-weise übergeben, aber der Assembler hilft uns ja dabei.

 LDI   R26, LOW(Adresse)        ; R28 für "Y" u. R30 für "Z"
 LDI   R27, HIGH(Adresse)       ; R29 für "Y" u. R31 für "Z"

Mit diesem "LOW" und "HIGH" teilt uns der Assembler die Adresse genau in die richtigen 8-Bit Portionen, die wir also so in den "X" Pointer laden können.

Noch eine Hilfe gibt es: Wir können, besser zu merken, auch schreiben:

 LDI   XL, LOW(Adresse)        ; YL für "Y" u. ZL für "Z"
 LDI   XH, HIGH(Adresse)       ; YH für "Y" u. ZH für "Z"

Also "LOW" für "L" und "HIGH" für "H"


Indirekte Adressierung

Mit den Pointer-Register kann man also Indirekte Adressieren(=einmal um die Ecke). Statt:

  LDS    GPR, adresse  ; lade das Byte von der Adresse in ein GPR  

Schreiben wir

  LDI   XL, LOW(Adresse)
  LDI   XH, HIGH(Adresse)
  LD    GPR, X        ; '''Indirekt''': lade das Byte, wo der Pointer X hinzeigt

beziehungsweise

  LDI   XL, LOW(Adresse)
  LDI   XH, HIGH(Adresse)
  ST    X, GPR        ; '''Indirekt''': speichern, wo der Pointer X hinzeigt

Warum sollte man sowas kompliziertes tun ? Nun, zum Beispiel dann,

  • wenn man an einer Stelle im Programm die Adresse festlegt, aber ganz woanders braucht (Subroutinen)
  • wenn man für mehrere Folge-Befehle die gleiche Adresse wieder braucht
  • wenn man mit der Adresse (als Zahl) rechnen will

Post-Increment/Pre-Decrement

Wenn ich zum Beispiel 4 Byte hintereinander im Speicher auf "0" löschen will

  LDI   XL, LOW(Adresse)
  LDI   XH, HIGH(Adresse)
  LDI   GPR, 0
  ST    X+, GPR   ;speichern, wo X hinzeigt, und dann 1 auf X addieren
  ST    X+, GPR   ;das 2.Byte dahinter
  ST    X+, GPR   ;das 3.Byte dahinter
  ST    X+, GPR   ;das 4.Byte dahinter

Durch das automatische Addieren brauche ich nichts weiter zu tun. Als Gegenstück dazu kann man auch jedesmal 1 abziehen lassen, das aber immer vorher

  ST    -X, GPR   ;1 von X abziehen und dann speichern, wo X hinzeigt

All das gibt's für X, Y und Z. Es geht aber immer nur

  • Load & Store und dann addieren
  • erst subtrahieren und dann Load & Store

Diese Kombinationen werden einfach am häufigsten gebraucht.

Displacement

Für die beiden Pointer Y und Z kann man auch mit Displacement arbeiten

  LDD   GPR, Y+4   ; wo Y hinzeigt, aber 4 Byte dahinter

Welches Register ?

Bei den Load & Store Befehlen könnten als GPR alle 32 Allgemeinen Register beliebig verwendet werden, um das entsprechende Byte aufzunehmen oder abzugeben. Aber als Zwischenspeicher oder Arbeitsregister ist es am besten, Registern wie r26->r31, die möglicherweise als Pointer gebraucht werden, eher auszuweichen.

Wird das Assemblerprogramm durch inline oder libraries in eine Hochsprache 
eingebunden, muß man bei sich natürlich an die Register-Konventionen dieser
Umgebung halten. 
Schreibt man ein "stand-alone" Programm, kann (und sollte) man sich seinen eigenen
Konventionen und Regeln schaffen. Das ist das, was der Assembler-Fan so liebt.

Benutzerdaten

Daten wie Variable oder Felder werden üblicherweise im SRAM abgelegt und (s.o) von dort auch geholt. Bei allen Compilern und natürlich auch beim Assembler können zum Programmierzeitpunkt nur die Adressen davon festgelegt werden, aber keine Inhalte. Erst zur Laufzeit des Programmes kommen hier Werte rein. Manche Compiler gaukeln es zwar vor, aber (ein C-Beispiel):

 char  MeinName[] = "Sepp Meier" 

bewirkt in Wirklichkeit drei Dinge: Im SRAM wird die nächste, noch nicht benutzte Adresse dem Text "MeinName" in der oben genannten Tabelle abgelegt, der String "Sepp Meier" wird im FLASH Programmspeicher bereitgestellt, und es wird eine Transfer-Routine generiert, die beim tatsächlichen Start des fertigen Programmes diesen String in das Feld "MeinName" transferiert.

Im Assembler ist es eben genauso: Wir können durch die Anfangsadressen und einen Namen dazu Felder festlegen, die dann zur Laufzeit benützt werden. Mehr eigentlich nicht. Und erst, wenn wir Befehle schreiben, die diese Daten verwenden, ergibt sich daraus ein Datentyp. Vorher sind das einfach nur Bytes.

Das ist bei Hochsprachen anders: Schon in der Source legen wir durch verschiedene Angaben den Datentyp (char, integer, etc.) fest und daraus entscheidet dann der Compiler, welche Maschinen-Instruktionen er dafür verwenden wird. Wir können das nurmehr eingeschränkt beeinflussen.

Daten im SRAM

.DSEG           ; Das ist eine Datensegment, d.h. es werden SRAM-Adressen festgelegt
CharBuf: 	.byte 24
OtherData: 	.byte 4 
....     

Anmerkung: Standardgemäß beginnt der Assembler mit der kleinsten SRAM Adresse. Bei den meisten AVRs ist das die tatsächliche Byte-Adresse 0x0060 (d.i. 96 dezimal). Siehe Adress-Mapping

Dadurch, daß wir geschrieben haben ".byte 24" bestimmen wir, daß als nächste Datenadresse automatisch dann 96 + 24 = 120 gilt. Also ist mit "OtherData" dann 0x0078 (dez. 120) verknüpft.

Noch eins: Wir haben ja gesagt, für den Assembler ist alles letztlich eine Zahl. Wir können daher auch damit rechnen. Wenn wir also die "24" nicht selbst wo hinschreiben wollen, verwenden wir einfach den Ausdruck

LDI R10 , Otherdata - CharBuf 

und laden damit das Register 10 mit der Zahl 24

Daten im FLASH (Literale)

Literale sind Werte, die schon in der Source festgelegt werden können. Das geht im Prinzip ähnlich, nur müssen wir nun auch Datentypen festlegen

 
OneByte: 	.db   1             ; ein byte mit dem inhalt 0x01   
MeinName:       .db   "Sepp Meier"  ; mehrere Bytes mit Text
OneWord: 	.dw   1,2           ; zwei WORDs (16 Bit) mit dem inhalt 0x0001 u.  0x0002
....     

Diese Definitionen können wir auch direkt in den Programmcode reinmischen. Wir müssen aber natürlich schauen, daß der AVR zum Laufzeitpunkt nicht versucht, diese Zeichen als Befehl auszuführen.

Daten im EEPROM (ERAM)

genauso geht es, wenn man für den EEPROM feste Werte vorgeben will:

.ESEG           ; Das ist ein EEPROM-Segment
OneByte: 	.db   1             ; ein byte mit dem inhalt 0x01   
MeinName:       .db   "Sepp Meier"  ; mehrere Bytes mit Text
OneWord: 	.dw   1,2           ; zwei WORDs (16 Bit) mit dem inhalt 0x0001 u.  0x0002
....     

Verzweigungen

Es gibt zwei Arten, bedingte Programm-Sprünge zu formulieren:

  • Durch irgendwelche Befehle, meistens durch Vergleichsoperationen, erst die Bits im Statusregister (SREG) zu setzen, und diese Status-Bits abzufragen und je nachdem einen Programmsprung durchzuführen oder nicht.
  • Man kann aber auch abhängig von Datenbits in GPRs und SFRs den Folgebefehl überspringen (Skippen) lassen. Dieser Folgebefehl kann, muß aber nicht, ein Sprung- oder Callbefehl sein.

Zu jedem der 8 Bits im SREG gibt es je 2 bedingte Sprünge. Die Flags Zero,Carry und Signed geben Auskunft über das Ergebnis eines Vergleiches z.B. mit den Befehl

CP R1,R2  ; Vergleiche den Inhalt der Register R1 und R2

Da die selben 8 Bits für Zahlen von 0 bis 255 oder von -128 bis 127 stehen können, ist eine Unterscheidung in Zahlen mit und ohne Vorzeichen nötig. Anders als man vielleicht vermuten könnte ist der Vergleichsbefehl noch der gleiche, nur das Ergebnis des Vergleichs steht in anderen Bits des SREG.

Flag Sprung bei Falg=1 ohne Vorzeichen mit Vorzeichen Gegenstück
Zero BREQ R1 = R2 R1 = R2 BRNE
Carry BRLO R1 < R2 BRSH
Signed BRLT R1 < R2 BRGE

Für die Befehle BRLO udn BRSH gibt es mit BRCS udn BRCC noch je einen zweiten gleichwertigen Namen, wegen Bedeutung des Carry-Flags bei arithmetischen Operationen. Das sind also einfach zusätzliche Namen für den selben Befehl.

Zusammenfassung

Da dieser Artikel kein Assembler Tutorial sein kann und soll, soll hier auf die "eigentlichen" Instruktionen nicht eingegangen werden. Ich wollte nur versuchen, die "Denkweise" des Assembler und der AVR-Micros ein wenig näherzubringen und einige ermuntern, doch die ersten Operationen direkt am offenen Herzen des Kontrollers zu wagen.

In manchen Fällen habe ich aber sicher einige Leser endgültig verschreckt.

Strategie für BASCOM-Programmierer

Soll nur ein bestimmter Programmteil mit Assembler beschleunigt werden kann man diesen auch mit ein paar "NOP" umschliessen. Danach kann man die kompilierte Datei im AVR-Studio öffnen und nach diesen NOP suchen. Wenn dann die betroffene Stelle ausgemacht ist, hat man schon den Teil den man braucht in Assembler. Jetzt muss dieser nur noch optimiert werden. BASCOM übersetzt immer so, daß eine Variable aus dem Speicher in ein Register geladen wird, dann wird etwas damit gemacht, und dann kommt das ganze wieder in den Speicher. Oftmals wird die gleiche Variable danach wieder aus dem Speicher ins gleiche Register geladen um was anderes damit zu machen. Genau an der Stelle kann jetzt optimiert werden. Alle unnötigen Speicherzugriffe können entfallen. Die nächste Stufe währe, daß man auch andere Register für die Aktionen verwewendet. BASCOM übersetzt nähmlich immer gleich. Es verwendet für einen Befehl immer die gleichen Register, was dann auch manchmal solche unnötigen Speicherzugriffe notwendig macht. Also kann auch hier optimiert werden. Man muss dazu Assembler nicht bis in die letzten Tiefen verstanden haben. Es reicht wenn man erkennt wo BASCOM schlecht übersetzt. Lustiger Nebeneffekt ist, daß man Assembler auf diese Weise nebenbei lernt.

Noch ein Tip

Gut abgeschrieben ist am Anfang besser als schlecht ausgedacht. Gerade die verschiedenen Geräte-Initialisierungen (SFR) in den zahlreichen GCC Codeschnipsel können fast direkt abgeschrieben werden (Baudrate, Input/Output, etc), da der C-Compiler das ja auch nicht anders machen kann.

Autor

PicNick

Weblinks

Siehe auch