Mikrocomputertechnik


Prof. Jürgen Plate

3. Programmierung des Mikrocontrollers 68HC11

3.1 Überblick

Programmiermodell

Da man wohl kaum einen Schaltplan des CPU-Herstellers bekommt und da dieser auch viel zu komplex ist, wird für die Programmierung mit dem sogenannten "Programmiermodell" gearbeitet. Dies umfaßt: Der Prozessor 68HC11 ist eine Weiterentwicklung des 6800 (weitgehend befehlskompatibel, erweiterter Befehlssatz, zusätzliche Register). Der 68HC11 besitzt vier 8-Bit-Register und fünf 16-Bit-Register:

Einteilung der Befehle

Der 68HC11 kennt 59 verschiedene Grundbefehle → überschaubarer Befehlssatz. Anwendung der Befehle mit unterschiedlichen Registern und Adressierungsarten führt zu 1464 verschiedenen OP-Codes. Der OP-Code besteht (einschließlich Angabe über Adressierungart) aus einem, zwei oder drei Bytes: Der Adreßteil (falls vorhanden) besteht aus einem oder zwei Bytes; es gibt also 1-, 2-, 3-, 4- und 5-Byte-Befehle. Das Befehlsformat kann also sein:

Das "prebyte" erweitert den Befehlsumfang, so daß mehr als 256 Befehle möglich sind. Der Nachteil dabei ist, daß die Abarbeitung des Befehls länger dauert und der Befehl natürlich auch ein Byte mehr an Speicher braucht.

Adressen werden in zwei aufeinanderfolgenden Speicherbytes abgelegt und zwar in der Form

AHAL
eaea+1

Mit "ea" wird die effektive Adresse = echte Adresse = Adresse des Operanden im Speicher bezeichnet. Je nach Adressierungsart (siehe unten) ist eine Adreßberechnung notwendig. Das Ergebnis der Adreßrechnung landet im Adreßregister und wird dann zur Adressierung des Speichers verwendet.

Befehlsklassen:

Angaben über gültige Adressierungsarten, Länge, CC-Beeinflussung, Dauer (Anzahl der Taktzyklen) stehen in der Befehlstabelle.

3.2 Adressierungsarten

Die meisten Befehle gestatten mehrere Adressierungsarten, aber nicht alle Adressierungarten lassen sich auf jeden Befehl anwenden. Jede zulässige Adressierungsart bei einem Befehl führt zu einen eigenen OP-Code. Die zulässigen Adressierungsarten und der jeweilige OP-Code lassen sich der Befehlsliste entnehmen. Die tatsächliche Länge und Ausführungszeit eines Befehls hängen von der Adressierungsart ab. Die Zahl der Bytes eines Befehls und seine Ausführungszeit lassen sich aus der Befehlsliste entnehmen.

Konstanten-Adressierung (Immediate Addressing)

Die Daten folgen unmittelbar auf den OP-Code. Je nach Befehl/Register sind es 8 Bit (1 Byte) oder 16 Bit (2 Byte). Im Assembler wird dies durch Voranstellen von "#" kenntlich gemacht, zum Beispiel:
   
LDAA #20 - Lade Akku A mit dem Wert 20 

Absolute Adressierung

Absolut vollständige Adressierung (Extended Addressing)

Der Adreßteil des Befehls enthält eine vollständige 16-Bit-Adresse. Es ist eine Adressierung des gesamten Adreßraums möglich. Zum Beispiel:
 
LDAA $A174 - Lade Akku A mit dem Inhalt der Speicherzelle $A174 

Absolut unvollständige Adressierung (Zero Page / Direct Addressing)

Der Adreßteil enthält nur den niederwertigen Teil (LSB) der Adresse. Der höherwertige Teil wird auf Null gesetzt. Damit lassen sich die Adressen zwischen 0 und 255 ($0 - $FF) erreichen. Der Befehl ist ein Byte kürzer als bei der absoluten Adressierung und deshalb auch schneller in der Ausführung. Im Assembler wird dies durch Voranstellen von "<" gekennzeichnet. Zum Beispiel:
 
LDAA <$80 - Lade Akku A mit dem Inhalt der Speicherzelle $80 

Implizite Adressierung (Inherent Addressing)

Alle Adreßangaben sind im OP-Code des Befehls enthalten, es gibt keinen Adreßteil.

Indizierte Adressierung (Indexed Addressing)

Die effektive Adresse ergibt sich hier aus dem Inhalt eines Adreßregisters (z. B. X oder Y) und einer dazu addierten Distanz (Offset), die auch Null sein kann. Der Offset wird grundsätzlich konegativ bewertet (-128 .. +127). Die Adreßrechnung erfolgt modulo 64 K (bleibt also immer im Adreßraum des 68HC11). Zum Beispiel:
CLR  5,X        (ergibt als Code: 6F 05)
CLR -5,X        (ergibt als Code: 6F FB)

Anmerkungen zur Progam Counter-relativen Adressierung:

Es wird der Stand des Program Counterszum Zeitpunkt der Befehlsausführung verwendet (Dies ist nicht die Adresse, an der der Befehl beginnt, denn der Program Counter enthält ja immer die Adresse des nächsten, auszuführenden Befehls. Der Program Counter ist gleich der Adresse des Befehls + Länge des Befehls). Die Program Counter-relative Adressierung wird oft bei Sprungbefehlen verwendet. Zweck: lageunabhängige Programme (Ablauf an jeder Position im Speicher).

Befehlsaufbau: offset = Zieladresse - Befehlsadresse - Befehlslänge

Befehlsausführung: Zieladresse = Befehlszählerstand + offset

Alle Adressierungsarten im Überblick

zeigt die folgende Grafik:

3.3 Assembler-Befehlsaufbau

Der Assembler ist ein Programm zur Übersetzung mnemotechnischer Befehlsbezeichnungen in die Maschinensprache. Es erfolgt 1:1-Abbildung der Befehle in die Maschinensprache, d. h. eine Assemblerzeile entspricht einem Befehl.

Maschinensprache:

Assemblersprache: Allgemeiner Aufbau eines Assemblerbefehls:

Marke OP-Code Operand/Adresse Kommentar

Beispiel:

  START        LDAA         DAT1          ; Zähler initialisieren 

Marken-Feld: (optional)
Das Feld beginnt in Spalte 1 (also ganz vorne). Die Marke besteht aus Buchstaben, Ziffern und Sonderzeichen (".", "_", "@") und beginnt mit einem Buchstaben. Die Länge ist auf 31 Zeichen begrenzt. Die Marke kann durch einen ":" abgeschlossen werden, dieser wird vom Assembler ignoriert. Es wird bei manchen Assemblern - wie beispielsweise auch in C - zwischen Groß- und Kleinschreibung unterschieden. Registernamen (A, B, C, D, X, Y, S) sind reserviert und dürfen nicht verwendet werden. Marken bezeichnen Konstante oder Speicheradressen.

OP-Code-Feld: (muß vorhanden sein)
Falls keine Marke vorhanden, muß wenigstens ein Leerzeichen davor stehen. Sinnvoller ist es jedoch, die Opcodes in einer Spalte untereindander zu schreiben. Die Befehle werden durch Mnemonics aus zwei bis vier Buchstaben dargestellt. Bei einigen Befehlen ist eine Registerangabe direkt an das Mnemonic anzuhängen (z. B. ADDA, ADDB, ADDD). In diesem Feld kann auch eine Assembler-Direktive (Pseudobefehl) stehen → Anweisung für den Assembler, Genaueres später.

Operanden/Adreß-Feld: (abhängig vom OP-Code)
Dieses Feld enthält den Operanden des Befehls, meist eine Adresse oder eine Konstante. Aufbau hängt von der Adressierungsart ab. Falls eine Adressierungsart mehrere Operanden verlangt (z. B. Register + Offset) erfolgt Trennung der Operanden mit Komma.
Bei vielen Assemblern sind arithmetische und logische Ausdrücke aus Konstanten möglich. Die Adreßangabe (Offset) kann erfolgen als: Im Operandenfeld kann auch mit den vier Grundrechenarten (+ - * /) und Klammern gearbeitet werden. Beachten Sie, daß die Ausdrücke zur Assemblierungszeit ausgewertet werden - nicht zur Laufzeit des Programms! Es sollte sich also immer um "konstante" Ausdrücke handeln, z. B.
BOTTOM	EQU	TOP + 10

Kommentar-Feld: (optional)
Das Kommentarfeld wird durch den Strichpunkt oder zwei Schrägstriche eingeleitet. Es enthält beliebige Zeichenfolgen und wird vom Assembler ignoriert.

Zusätzlich sind auch reine Kommentarzeilen möglich, die mit einem "*" in Spalte 1 beginnen.

Der Assembler hat zwei Durchläufe:
  1. Durchlauf (pass one): Syntaxprüfung und Aufbau des Adreßbuchs.
  2. Durchlauf (pass two): Vollständigkeitsprüfung und Aufbau des Zielprogramms.
Beispiel:

1. Quellcode

Die Felder für Adresse und Code sind hier noch leer, sie werden vom Assembler gefüllt. Es gibt zwei Pseudobefehle, die weiter unten genauer behandelt werden: ORG legt die Startadresse des erzeugten Codes fest und END beendet den Quellcode.

Adresse Inhalt Marke Befehl Operand Kommentar
                   ORG $8000 In den Speicher ab $8000
    MAIN BRA MARKE relativer Vorwärtssprung
      JMP MARKE absoluter Vorwärtssprung
      BRA MAIN relativer Rückwärtssprung
    MARKE JMP MAIN absoluter Rückwärtssprung
      END    

2. Erster Assemblerlauf: Adresspegel und Code eintragen

Im ersten Durchlauf werden die Befehle übersetzt. Damit liegt die Adresse eines jeden Befehls fest (weil die Länge eines Befehls ja bekannt ist). Jedoch können die Sprungadressen noch nicht eingetragen werden, weil bei Vorwärtssprüngen diese beim Übersetzen des jeweiligen Befehls noch nicht bekannt sind.

Adresse Inhalt Marke Befehl Operand Kommentar
      ORG $8000 In den Speicher ab $8000
8000 20 ?? MAIN BRA MARKE relativer Vorwärtssprung
8002 7E ?? ??   JMP MARKE absoluter Vorwärtssprung
8005 20 ??   BRA MAIN relativer Rückwärtssprung
8007 7E ?? ?? MARKE JMP MAIN absoluter Rückwärtssprung
      END    

3. Zweiter Assemblerlauf: Adressen absättigen

Beim ersten Durchlauf hat der Assembler parallel zur Übersetzung noch eine Symboltabelle angelegt, welche die Zuordnung zwischen Marke (Name) und Adresse enthält. Nun können mit Hilfe dieser Tabelle im zweiten Durchlauf die Adressen eingetragen werden.

Adresse Inhalt Marke Befehl Operand Kommentar
      ORG $8000 In den Speicher ab $8000
8000 20 05 MAIN BRA MARKE relativer Vorwärtssprung
8002 7E 80 07   JMP MARKE absoluter Vorwärtssprung
8005 20 F9   BRA MAIN relativer Rückwärtssprung
8007 7E 80 00 MARKE JMP MAIN absoluter Rückwärtssprung
      END    

Die Symboltabelle nimm neben den Sprungadressen auch alle Namen von Konstanten und Variablen auf. Im zweiten Lauf werden dann auch diese Namen durch die ermittelten Werte ersetzt.

3.4 Pseudobefehle (Assembler-Direktiven)

Pseudobefehle erzeugen keine Maschinenbefehle sondern

Der Assembler führt einen Zähler, der Auskunft über den aktuellen Adreßpegel gibt. Dieser 'Location Counter' dient beispielsweise zum Berechnen von Sprungadressen. Er entspricht im Prinzip dem 'Program Counter' im realen Prozessor. Die Pseudobefehle werden im OP-Code-Feld einer Assemblerzeile angegeben. Unser Assembler kennt folgende Direktiven:

ORG Origin
Der Location Counter wird auf die im Operandenfeld angegebene Adresse gesetzt. Er legt damit die Adresse fest, an der der Code später im Speicher des Controllers abgelegt wird, z. B.:
     ORG  $8000
EQU Equate
Definition eines Namens (Konstante, Marke). Der im Operandenfeld angegebene Wert wird dem im Markenfeld angegebenen Namen gleichgesetzt, z. B.:
ETX  EQU  3
CR   EQU  $D
LF   EQU  $A
PUFL EQU  $100
DC.B Define Constant Byte
Reservierung von Speicherplatz und Belegung mit den im Operandenfeld angegebenen konstanten Byte-Werten. Mehrere Operanden durch Komma trennen! Z. B.:
TAB  DC.B  $0D,$0A,$03
Neben einfachen Konstanten sind in (einfache oder doppelte) Hochkomma eingeschlossene Zeichenketten möglich. Die Hochkommas gehören dabei nicht zur Zeichenkette. In den Zeichenketten sind folgende Sonderzeichen erlaubt:
  • \a Bell ($07)
  • \f Formfeed ($0C)
  • \n Newline ($0A)
  • \r Carriage Return ($0D)
  • \t Tabulator ($09)
  • \\ \
Z. B.:
TEXT  DC.B "Hello World!\n\r"
Bei manchen Assemblern wird anstelle von DC.B das Kürzel FCB (Form Constant Byte) verwendet (bei unserem geht beides).
DC.W Define Constant Word
Reservierung von Speicherplatz und Belegung mit den im Operandenfeld angegebenen konstanten 16-Bit-Werten. Mehrere Operanden durch Komma trennen, z. B.:
ATAB  DC.W  $FF30,$FF45,$FFA0
Bei manchen Assemblern (auch unserem) wird anstelle von DC.W das Kürzel FDB (Form Double Byte) verwendet.
DC.L Define Constant Longword
Reservierung von Speicherplatz und Belegung mit den im Operandenfeld angegebenen konstanten 32-Bit-Werten. Mehrere Operanden durch Komma trennen, z. B.:
XTAB  DC.L  $FF30FF45,$F000FFA0
DS.B
DS.W
DS.L
Define Storage Bytes/Words/Longwords
Reservieren von Speicherbytes, Speicherworten (16 Bit) oder Langworten (32 Bit) ohne Vorbelegung. Der Operand gibt die Anzahl der zu reservierenden Elemente (Bytes, Worte oder Langworte) an, z. B.:
PUFFER  DS.B 100
ADRTAB  DS.W 23
Der Reservierungsvorgang hat eher informatorischen Charakter. Es findet seitens des Assemblers keinerlei Adress- oder Bereichsüberprüfung statt! Bei manchen Assembler wird stattdessen nur eine Reservierung in Bytes mit dem Befehl RMB (Reserve Memory Byte) vorgenommen.
INCLUDE Einfügen einer Quelldatei an der entsprechenden Stelle. Der Dateiname wird entweder in < ... > (voreingestelltes Include-Verzeichnis) oder " ... " (beliebiger Dateipfad) eingeschlossen, z. B.:
     INCLUDE "definitionen.a"
     INCLUDE <HC11.h>
NOLIST Unterdrücken der Erzeugung eines Programmlistings bis zum nächsten LIST-Befehl.
LIST Fortsetzen des Listings nach einem vorher platzierten NOLIST-Befehl.
END Ende des Assemblerprogramms. Optionaler Befehl am Programmende. Wird aus Gründen der Kompatibilität vom Assembler akzeptiert, kann aber weggelassen werden. Text nach diesem Befehl wird vom Assembler ignoriert.

Speicher-Organisation

Mit Hilfe der ORG-Anweisung kann der Programmierer verschiedene Programmteile oder Datenbereiche an beliebigen Stellen im gesamten Adreßraum ablegen. Eine sinnvolle Speicheraufteilung hat jedoch verschiedene Randbedingungen zu berücksichtigen.

Von der Hardwareseite her sind z.B. RAM- und ROM-Bereiche verschiedener Größe gegeben, denn Programme sollen häufig im ROM liegen während variable Datenbereiche natürlich nur im RAM sein können. Eine effiziente Nutzung der physikalisch vorhandenen Speicher verlangt auch die Definition von lückenlos zusammenhängenden Teilbereichen sowohl von Programmteilen (Hauptprogramm, Unterprogramme, Interrupt-Service-Routinen) als auch von Datenbereichen (Datenpuffer, Ausgabetexte, Zustandsvariablen, Stacks, Peripherie-Baustein-Register etc).

Als Grundregel gilt: Je eine ORG-Anweisung für den Daten- und den Programmbereich genügt, denn dann sind beide Bereiche jeweils zusammenhängend. Es ist nicht nötig, z.B. für jedes Unterprogramm oder jede Interrupt-Service-Routine ein neues ORG zu setzen. Jede weitere ORG-Anweisung steigert nur die Gefahr von Fehlern (z.B. überlappende Bereiche) bzw. Speicherverschwendung durch unnötige Lücken. Bedenken Sie, dass Sie vom Assembler nicht gewarnt werden, wenn sich Speicherbereiche überlappen oder Sie Bereiche wählen, an denen kein realer Speicher vorhanden ist.

Ganz auf die ORG-Anweisung zu verzichten ist zwar möglich (Programm- und Datenbereich hängen dann zusammen und beginnen bei Null), aber nicht zu empfehlen. Durch kommentierte ORG-Anweisungen zeigt man, das diese wohlüberlegt gewählt wurden; also besser ORG $0000 angeben als einfach weglassen.

Anmerkung: Da bei einem Mikrocontroller ja zwischen ROM- bzw. EEPROM-Speicher und RAM unterschieden werden muß, ist auch die Reservierung von Speicher von dieser Unterscheidung betroffen. Mit DC.B, DC.W oder DC.L vordefinierte Konstante liegen im ROM/EEPROM-Bereich, mit DS.B, DS.W oder DS.L reservierter Speicher im RAM-Bereich.

Merkregeln für die Verwendung von EQU, DS.B/DS.W/DS.L und DC.B/DC.W/DC.L

Grundsätzlicher Aufbau eines 68HC11-Assemblerprogramms

1. Konstantendefinitionen

Hier werden alle Definitionen für Werte und Adressen (EQU-Anweisung) vorgenommen. Diese "Namensdefinitionen" belegen keinen Speicherplatz, sondern dienen nur der besseren Lesbarkeit und Wartbarkeit des Programms. Zum Beispiel:
   STACK    EQU  $7FFF  Stackbereich
   PROG     EQU  $8000  Programmbereich
   DATA     EQU  $2000  Datenbereich (auch Adr. 0 möglich)
   RVECT    EQU  $FFFE  Reset-Vektor

   SCCR1    EQU  $102C
   SCCR2    EQU  $102D
   CR       EQU  13     Carriage-Return-Zeichen 
   LF       EQU  10     Linefeed-Zeichen 
   ETX      EQU  3      Textende 
   MAXDATA  EQU  100 
Wichtig sind die ersten fünf Zeilen, die bei keinem Programm fehlen dürfen. Sie legen die Adressbereiche für RAM und EEPROM fest und definieren die Adressen der beiden Vektoren, die immer gesetzt werden müssen.

2. Variablendefinitionen

Hier werden alle Daten definiert, die Speicherplatz belegen. Es ist ratsam, den Datenbereich durch eine eigene ORG-Anweisung einzuleiten, um die Daten an definierter Stelle im Speicher abzulegen. Bei einem Steuerungsrechner muß auf jeden Fall in Datenbereich (RAM) und Programmbereich (PROM) unterschieden werden. Man kann die Variablen auch nach dem Programmcode ablegen. Zum Beispiel:
            ORG  DATA           Datenbereich z.B. ab Adresse $2000 
   COUNT    DC.B 0              Wert mit Null vorbelegen 
   PUFFER   DS.B 80             80 Byte Pufferbereich reservieren 
   TEXT     DC.B 'Hello World'  Text, abgeschlossen durch Zeilen- 
            DC.B CR, LF, ETX    wechsel und Nullbyte 

3. Programmbereich

Nun folgt das eigentliche Programm. Es wird immer mit einer ORG-Anweisung an eine definierte Adresse gelegt. Diese Adresse ist auch Startadresse des Programms.

Achtung: Der Assembler kümmert sich nicht um die Adreßlage des Programms, auch Überlappungen mit dem Datenbereich werden nicht als Fehler gemeldet → aufpassen.

Es gibt zwei Möglichkeiten der Anordnung von Haupt- und Unterprogrammen. Üblich ist es, das Hauptprogramm am Anfang zu postieren und dann die Unterprogramme folgen zu lassen. Wer lieber die UP zuerst stehen hat, muß das HP mit einer Marke versehen (z. B. "MAIN") und nach der ORG-Anweisung als erste Anweisung einen Sprung zum HP eintragen, z. B.:

            ....... Definitionen (1. und 2.) 

            ORG  PROG           Programmbereich, z. B. $8000
            JMP  MAIN 

            ....... Unterprogramme 

   MAIN     LDS  #STACK         Stackpointer setzen, sonst geht nichts
             .
             .
             .
             .

            ORG  RVECT
            DC.W MAIN           Reset-Vektor auf Programmanfang setzen
            END 

Häufiger ist jedoch die Variante mit vorangestelltem Hauptprogramm anzutreffen. Beim folgenden Rumpfprogramm sind auch noch einige weitere Besonderheiten berücksichtigt:

     stak   equ    $7FFF    ; Stackbereich ab $7FFF (RAM, abwaerts)
     prog   equ    $8000    ; Programmbereich ab $8000 (EEPROM)
     data   equ    $2000    ; Datenbereich ab $2000 (RAM)

     rvec   equ    $FFFE    ; Reset-Vektor
             .              ; ggf. weitere Vektoren

            org    data     ; Beginn Datenbereich
             .              ; Variablen-Definitionen
             .

            org    prog     ; Beginn Programmbereich
     main   lds    #stak    ; Stackpointer setzen
             .
             .              ; alle Initialisierungen
             .
     loop    .              ; Befehle des Programms
             .     
             .     
            jmp    loop     
             .              ; ggf. fixe Tabellen dahinter
             .     

             .              ; ggf. Setzen der Interrupt-Vektoren
             .              ; (Reihenfolge der Adressen beachten!)

            org    rvec     ; Setzen des Reset-Vektors
            fdb    main 
            end  
Beim Programmieren einer Controller-Anwendung ist u. a. auch zu berücksichtigen, dass RAM-Inhalte nach dem Ausschalten und späteren Wiedereinschalten verloren gehen. Das bedeutet, dass feste Werte wie Ausgabestrings, Anfangswerte von Variablen usw. nicht im RAM (Datenspeicher), sondern im EEPROM (Programmspeicher) abgelegt werden müssen. Das hat aber Konsequenzen: Hinweis: Die im Datenbereich abgelegten konstanten Werte (mittels DC.B etc.) werden nur einmal beim Herunterladen des Programms ins Zielsystem im Speicher eingetragen, danach nie wieder. Schon das Drücken der Reset-Taste kann zu "seltsamen" Verhaltenformen des Programms führen.

3.5 CPU-Befehle

Bei der folgenden Beschreibung der Befehle werden einige Standardnamen verwendet:

Transport-Befehle

Ladebefehle:

Load Register from Memory or Constant

CLR Clear Akkumulator: 0 → Akkumulator-Register

Speicherbefehle:

Store Register into Memory (8 Bit/16 Bit)

  • STAA, STAB (8 Bit): (Reg) → adr.
  • STD, STS, STX, STY (16 Bit): (Reg) → adr., adr+1

    Clear Memory

    Transport zwischen Registern:

    Kopie des Inhalts eines Registers in ein anderes (transfer)

    Bit X im Statusregister kann mittels TAP nicht gesetzt werden.

    Austausch von Registerinhalten (exchange)

    Beispiel: Tausch A B
    Benötigt wird eine Hilfsvariable im Daten-Speicherbereich

        STAA   HILF
        TBA
        LDAB   HILF
    
    
    Beispiel: Tausch X Y
    *             Beispielbelegung  X  Y  D
    *                               1  2  3
        XGDX                        3  2  1
        XGDY                        3  1  2
        XGDX                        2  1  3
    

    Beispiel: Unterprogramm zum Kopieren eines Speicherbereichs (nullterminierte Zeichenkette). Die Adresse des Quellstrings steht im X-Register, die Adresse des Zielbereichs im Y-Register:

    strcpy ldaa   0,x       ; lade ein Zeichen aus der Quelle
           staa   0,y       ; ablegen im Zielbereich
           beq    strc_e    ; wenn Nullbyte, dann fertig
           inx              ; beide Zeiger inkementieren
           iny
           bra    strcpy    ; und naechstes Zeichen
    strc_e rts
    
    Soll ein beliebiger Speicherbereich kopiert werden, müsste noch ein Blocklängenzähler implementiert werden.

    Arithmetische Befehle

    Dies sind Befehle, die arithmetische Operationen bewirken. Im allgemeinen steht ein Operand in einem Akku und das Ergebnis wieder im gleichen Akku.

    Additionsbefehle:

    Addieren (add)

    Addieren mit Übertrag (add with carry)

    Beispiel: Addition zweier 4-Byte-Zahlen

    1. Operand: Adresse $41 - $44 
    2. Operand: Adresse $45 - $48 
    Ergebnis:   Adresse $49 - $4C 
    
          LDAA $44      Niederwertiges Byte addieren 
          ADDA $48 
          STAA $4C 
          LDAA $43      Zweites Byte addieren 
          ADCA $47 
          STAA $4B 
          LDAA $42      Drittes Byte addieren 
          ADCA $46 
          STAA $4A 
          LDAA $41      Höchstwertiges Byte addieren 
          ADCA $45 
          STAA $49 
    

    Erhöhen (increment)

    Subtraktionsbefehle:

    Subtraktion (subtract) = Addition des Zweierkomplements

    Subtraktion mit Übertrag (subtract with carry)

    Erniedrigen (decrement)

    Vorzeichenwechsel (negate)

    Multiplikation und Division:

    Multiplizieren (multiply)

    Dividieren (divide)

    Beispiele:

    IDIV:                             FDIV:
    
    $8421 : $0004 = $2108  R $0001    $1000 : $2000 = $.8000  R $0000
    $0001 : $FFFF = $0000  R $0001    $0001 : $FFFF = $.0001  R $0001
    $FFFF : $0001 = $FFFF  R $0000    $FFFF : $0001 = unzulässig, V=1
    

    Weitere arithmetische Befehle:

    DAA Decimal Adjust A

    Beispiel: Addition zweier 4-stelligen BCD-Zahlen

    1. Zahl:  Adr. $31-$32 
    2. Zahl:  Adr. $33-$34  
    Ergebnis: Adr. $35-$36 
    
        LDAA $32   niederwertige 2 Ziffern 
        ADDA $34   binär addieren 
        DAA        BCD-Justage 
        STAA $36 
        LDAA $31   höherwertige 2 Ziffern 
        ADCA $34   binär addieren mit Carry! 
        DAA        BCD-Justage 
        STAA $35 
    
    Erzwingen kann man die Dezimalkorrektur nach dem Laden eines Weites oder bei der Subtraktion durch Addition vom 0:
           .
           .
         LDAA     WERT
         ADDA     #0
         DAA
         STAA     PORTC
           .
           .
    

    Logische Befehle

    Bei logischen Befehlen erfolgt die bitweise logische Verknüpfung eines Akku-Inhalts mit einem Operanden (Speicherinhalt oder Konstante). Logische Befehle spielen vor allem beim Zugriff auf E/A-Komponenten eine Rolle, weil bei E/A-Operationen häufig einzelne Bits eines Datenworts gesetzt, gelöscht oder invertiert werden müssen.

    Und-Verknüpfung (and)

    Beispiele:

    1) Löschen der 4 höherwertigen Bits von Akku A 
    
       ANDA #$0F
    
    2) Rücksetzen des 5. Bits
    
       ANDA #%11101111
    
    3) Rücksetzen des 4. Bits einer Variablen namens FLAG
      
       LDAA FLAG
       ANDA #%11110111
       STAA FLAG
    

    Oder-Verknüpfung (or)

    Beispiele:

    1) Setzen der 4 höherwertigen Bits von Akku A 
    
         ORA #$F0
    
    2) Setzen des 5. Bits 
    
         ORAA #%00010000 
    
    3) Setzen des 4. Bits einer Variablen namens FLAG
      
       LDAA FLAG
       ORAA #%00001000
       STAA FLAG
    

    Exklusiv Oder (exor)

    Beispiele:

    1) Invertieren der 4 höherwertigen Bits von Akku A 
    
        EORA  #%11110000 
    
    2) Invertieren der Bits 0 und 3 einer Variablen namens FLAG
      
       LDAA FLAG
       EORA #%00000101
       STAA FLAG
    

    Bit prüfen (bit test)

    Diese Befehle haben Bedeutung in Zusammenhang mit den weiter unten behandelten Sprungbefehlen. Sie erlauben den Test eines Akkumulators auf Null bzw. Vorzeichen, ohne dessen Inhalt zu ändern.

    Invertieren (complement)

    1-Komplement → Invertieren aller Bits.
    Auch auf Speicherbytes anwendbar.

    Beispiel: Verwendung des Speicher-Bytes $41 als Software-Flip-Flop. Zu Beginn des Programms muß der Inhalt von $41 gelöscht werden!

      COM $41       Inhalt wird $FF 
       . 
       . 
      COM $41       Inhalt wird wieder 0 
       . 
       . 
      usw. 
    

    Schiebe- und Rotationsbefehle

    Die Schiebebefehle bewirken das Verschieben eines Akku- oder Speicherinhalts um eine Stelle nach rechts oder links. Bei den Rotationsbefehlen erfolgt die Ringverschiebung eines Akku- oder Speicherinhalts unter Einbeziehung des C-Flags nach links oder rechts. Die Befehle sind auf A, B, D oder einen Speicherinhalt anwendbar. Die Realisierung auf Speicherinhalt erfolgt durch: Laden in Rechenregister, Schiebeoperation im Rechenregister, Zurückspeichern des Register-Inhalts).

    Logische Verschiebung:

    LSRx Logical Shift Right:
    von links wird 0 nachgeschoben: 0 → MSB
    LSB → C-Flag

    LSLx Logical Shift Left:
    von rechts wird 0 nachgeschoben: 0 → LSB
    MSB → C-Flag

    Arithmetische Verschiebung:

    ASRx Arithmetic Shift Right:
    Entspricht Division durch 2
    MSB MSB (Vorzeichen bleibt erhalten), LSB → C-Flag

    ASLx Arithmetic Shift Left:
    von rechts wird 0 nachgeschoben: 0 → LSB
    MSB → C-Flag
    Identisch mit LSL!

    Rotationsbefehle:

    Hin- und Herschieben des Akku- oder Speicherinhalts ohne ihn zu zerstören (anschließend Test des C-Flags). Vorzeichenlose duale Multiplikation und Division.

    ROLx Rotate Left
    C-Flag → LSB
    MSB → C-Flag

    RORx Rotate Right
    C-Flag → MSB
    LSB → C-Flag

    Beispiele:

    1) Multiplikation von Akku A mit 10: (A) * 10 → A 
       ASLA        (A)*2 → A 
       STAA $2040  Erg. zwischenspeichern 
       ASLA        (A)*4 → A 
       ASLA        (A)*8 → A 
       ADDA $204   (A)*8 + (A)*2 → A 
    
    2) Akku D nach links schieben: 
                                           A         C         B
       ASLB   MSB von B --> Carry     +----------+__+-+__+----------+
       ROLA   Carry --> LSB von A     +----------+  +-+  +----------+
    
    3) Zerlegung eines Bytes in zwei Halbbytes: 
    Die Halbbytes sind in getrennten Speicherzellen als niederwertiges Halbbyte 
    zu speichern (höherwertiges Halbbyte ist auf 0  zu setzen). 
    
       $2070: zu zerlegendes Byte 
       $2071: höherwertiges Nibble 
       $2072: niederwetiges Nibble 
    
       LDAA $2070   Byte holen   (binär: xxxxyyyy) 
       ANDA #$0F    niederwertiges Nibble maskieren 
       STAA $2072   speichern     (binär: 0000yyyy)  
       LDAA $2070   Byte nochmal holen 
       LSRA         4 x nach rechts schieben  (0xxxxyyy) 
       LSRA                                   (00xxxxyy) 
       LSRA                                   (000xxxxy) 
       LSRA                                   (0000xxxx) 
       STAA $2071   höherwertiges Nibble speichern 
    

    Test- und Statusregisterbefehle

    Sie dienen der Überprüfung von Datenwerten auf bestimmte Eigenschaften. Die Überprüfung erfolgt durch arithmetische und logische Operationen. Als Ergebnis werden bestimmte Flags des CC-Register beeinflusst, der Inhalt der Akkus bleibt unverändert. Die Flags können zur Steuerung bestimmter Sprungbefehle verwendet werden → Programmfortsetzung abhängig von Dateneigenschaften → Steuerbefehle. Beispiele bei den Sprungbefehlen.

    Test

    Die Befehle sind anwendbar auf A, B und den Speicher. Test auf Null und negativ.

    Vergleich (compare)

    ("Gedachte Subtraktion")
    Die Befehle sind anwendbar auf A, B, D, X und Y. Es wird jeweils ein Registerinhalt mit einem Speicherinhalt oder einer Konstanten verglichen (Ausnahme: CBA). Der Vergleich erfolgt durch eine Subtraktion ohne Anfallen des Ergebnisses. Der Vergleich auf Null und "negativ" ermöglicht alle Vergleiche von Werten. Mathematisch gesehen werden die links stehenden Ungleichungen in die rechts stehenden umgesetzt:

    X1 = X2 X1 - X2 = 0
    X1 < X2 X1 - X2 < 0
    X1 > X2 X1 - X2 > 0

    Durch bedingte Sprungbefehle kann dann abhängig vom Ergebnis im Programm verzweigt werden. Es gibt 8-Bit- und 16-Bit-Vergleiche (je nach Wortbreite des Registers).

    Bitorientierte Befehle

    Bitbefehle Statusregister

    Diese Befehle erlauben das komfortable Setzen und Rücksetzen einiger Bits des Statusregisters.
      • CLC clear carry flag X X X X X X X 0
      • CLI clear interrupt flag X X X 0 X X X X
      • CLV clear overflow flag X X X X X X 0 X
      • SEC set carry flag X X X X X X X 1
      • SEI set interrupt flag X X X 1 X X X X
      • SEV set overflow flag X X X X X X 1 X

    Bitbefehle Speicher

    Diese Befehle erlauben das Setzen oder Löschen einzelner Bits in Speicherworten. Als einzige Befehle haben sie zwei Operanden, die Adresse des zu ändernden Speicherbytes und eine konstante Maske. Da die Maske immer konstant sein muß, entfällt hier auch das "#"-Zeichen. Für jedes 1-Bit in der Maske wird das entsprechende Speicherbit gesetzt (BSET) oder gelöscht (BCLR).

    Beeinflussung des Systemzustands

    Dies sind Befehle, welche die Programmausführung anhalten. Sie dienen der Synchronisation des Programmablaufs mit externen Ereignissen.

    Warte auf Interrupt (wait for interrupt)

    Ablage aller Register auf dem Stack, dann warten auf Interrupt.

    Stop

    Reaktion hängt ab von S-Flag im Statusregister: Freigabe des STOP-Befehls:
            TPA               Statusreg. nach Akku A
            ANDA   #$7F       Freigabe (S-Flag auf 0)
            TAP               Akku A nach Statusreg.
    
    Für den Fall S=0 und X=1 wird beim Aktivieren des XIRQ-Eingangs der Prozessor nach einem STOP-Befehl "aufgeweckt" und er macht im Programm ganz normal weiter.

    Sprungbefehle

    Alle Sprungbefehle verändern den PC-Inhalt → Programmausführung wird an anderer Stelle fortgesetzt. Das Statusregister wird nicht beeinflußt. Einige Befehle speichern Registerinhalte auf dem Stack oder holen Registerinhalte von dort wieder zurück.

    Unbedingte Sprungbefehle (jump, branch)

    JMP adr. adr. → PC

    Bei JMP erfolgt die Zielangabe mit vollständiger absoluter Adressierung (extended direct) oder indizierter Adressierung (mit X oder Y). Es ist ein Sprung im gesamten Adressraum möglich, denn der Operand ist ein 16-Bit-Wert, der die Zieladresse im Speicher angibt.

    Der Sprungbefehl hat im Assemblerprogramm normalerweise eine so genannte Marke als Operand. Diese Marke steht am Sprungziel, wie Sie da schon eingangs des Kapitels bei der Erklärung der beiden Assemblerdurchläufe sehen konnten. Der Assembler entnimmt die Adresse dieser Marke aus der Symboltabelle und fügt sie dem JMP-Befehl als Operand hinzu. Der Programmierer kann daher immer mit symbolischen Adressen arbeiten.

    BRA dest.

    Hier wird keine absolute Adresse, sondern ein Offset relativ zum Program Counter angegeben (-128 ... +127). Hier berechnet der Assembler die Distanz zwischen BRA-Befehl und Sprungmarke und trägt als Offset eine 8-Bit-Zahl in Komplementdarstellung ein. Dazu ein Beispiel:

    Adresse Code  Quelltext
                           .
                           .
    8000    01    back    nop                           
    8001    01            nop                           
    8002    01            nop                           
    8003    2004          bra  forward ; Vorwärtssprung
    8005    01            nop                           
    8006    01            nop                           
    8007    20f7          bra  back    ; Rückwärtssprung
    8009    01    forward nop                           
    800a    01            nop                           
                           .
                           .
    
    Der Vorwärtssprung nach 8009 berechnet sich folgendermaßen (beachten Sie, dass der Program Counter bei Ausführung des Sprungbefehls bereits auf den nächsten Befehl weist):
       8005    Adresse nach "bra forward"
      +  04    Offset im Sprungbefehl
       8009
    
    Der Rückwärtssprung nach 8000 berechnet sich folgendermaßen (f7 ist die Zweierkomplementdarstellung, der Betrag ergibt sich zu 9):
       8009    Adresse nach "bra back"
      -  09    F7 = Offset im Sprungbefehl
       8000
    

    Bedingte Sprungbefehle ("Verzeigungsbefehle")

    Hier gibt es nur PC-relative Adressierung (branch conditionally). Es gibt acht Paare von zueinander komplementären Befehlen. Die Unterscheidung erfolgt in der jeweiligen Verzweigungsbedingung. Als Verzweigungsbedingung dienen (ein oder mehr) Bits des Statusregisters → Verzweigung abhängig vom Ergebnis der vorhergehenden Operation → ist die Bedingung erfüllt, erfolgt ein Sprung; im anderen Fall wird die Programmausführung beim nächsten Befehl fortgesetzt. Der Assembler übernimmt die Offset-Berechnung.

    Vergleich von Hochsprache und Assembler-Programmierung:

    Die folgende Tabelle fasst alle bedingten Sprünge zusammen. Die erste Gruppe springt abhängig vom Wert bestimmter Flags des Condition Code Registers. Weitere Gruppen bilden die Sprungbefehle, die von arithmetischen Vergleichen abhängen.

    Achtung: Die Flags werden nicht nur bei arithmetischen Operationen oder Vergleichen (Compare-Befehl) gesetzt, sondern bei fast jedem Befehl (z. B. Ladebefehle, logische Operationen).

    BefehlBedeutungSprungbedingung
    einfach (simple)
    BCC dest.carry clearC=0
    BCS dest.carry setC=1
    BVC dest.overflow clearV=0
    BVS dest.overflow setV=1
    BNE dest.result not equal zeroZ=0 (BZC)
    BEQ dest.result equal zeroZ=1 (BZS)
    BPL dest.result plusN=0 (BNC)
    BMI dest.result minusN=1 (BNS)
    natürlich (unsigned)
    BHI dest.higherOp1 > Op2
    BLS dest.lower or sameOp1 <= Op2
    BLO dest.lowerOP1 < Op2
    BHS dest.higher or sameOp1 >= Op2
    konegativ (signed)
    BGT dest.greater thanOp1 > Op2
    BLE dest.less or equalOp1 <= Op2
    BLT dest.less thanOp1 < Op2
    BGE dest.greater or equalOp1 >= Op2
    bitabhängig
    BRCLRadr. mask dest springe, wenn die in der Maske mit "1" markierten Bits alle "0" sind →alle Bits des Speicherbytes adr. werden mit der Maske UND-verknüpft und wenn das Ergebnis 0 ist, wird gesprungen.
    BRSETadr. mask dest springe, wenn die in der Maske mit "1" markierten Bits alle "1" sind → alle Bits des Speicherbytes adr. werden mit der Maske UND-verknüpft und wenn das Ergebnis gleich der Maske ist, wird gesprungen.

    Der Vergleich bei natürlichen Zahlen und konegativen Zahlen kann zu unterschiedlichen Ergebnissen führen. Betrachten Sie dazu die beiden folgenden Befehle:

       LDAA     #$F8
       CMPA     #$12
    
    Beim "unsigned"-Vergleich ist $F8 > $12, der Sprungbefehl BHI würde also ausgeführt. Betrachtet man die Zahlen jedoch als ganze (konegative) Zahlen, handelt es sich um die Zahl -8dez., die natürlich kleiner als $12 (18dez.) ist. Der Befehl BGT würde daher nicht ausgeführt.

    Betrachten Sie folgende Beispiele für die Anwendung von Sprungbefehlen, die Berechnen der Summe über ein Feld von Messwerten:

    FLENG  EQU 100         ;Symbolische Konstante Feldlaenge vereinbaren
    FELD   RMB FLENG       ; Symbolischer Name und Speicherplatz
                           ; fuer ein Feld von 100 Bytes reservieren
    SUMME  RMB 1           ; Symbolischer Name und Speicherplatz
                           ; fuer 1 Byte reservieren
                           ; das Indexregister X dient als Pointer!
           LDX   #FELD     ; symbolische Adresse des Feldanfangs ' X
           LDAB  #FLENG    ; Zaehler für die Feldlaenge initialisieren
           CLRA            ; Summe in Akku A bilden, -> erst loeschen
           LOOP  ADDA 0,X  ; Feldelemente indiziert adressieren
           DECB            ; Zaehler um eins vermindern
                           ; Feld abgearbeitet (bei 0 angekommen)?
           BEQ   FERTIG    ; wenn ja (Zaehler = 0): fertig
           INX             ; sonst: Indexregister (Zeiger!) erhoehen
           BRA   LOOP      ; und weitermachen in der Schleife
    FERTIG STAA  SUMME     ; Ergebnis abspeichern
    
    In der Sprache C könnte das etwa folgendermaßen aussehen, wobei feld als Array der Größe fleng vereinbart ist.
    feldptr = &feld[0]; 
    summe = 0;
    i = fleng;
    do
      {
      summe = summe + *feldptr;
      feldptr++;
      i--;
      } while(i>0);
    

    Unterprogrammsprünge (jump/branch to subroutine)

    Die Unterprogrammtechnik ist ein wertvolles Hilfsmittel zur modularen Programmierung in Assembler. Darüber hinaus wird bei Verwendung von Unterprogrammen (UP) Speicherplatz für Programme gespart, wenn sie in einem Programm mehrfach verwendet (aufgerufen) werden. Gleiche oder sehr ähnliche Befehlssequenzen in einem Programm sollten deshalb immer zu Unterprogrammen zusammengefasst werden, wie eine Funktion in C. Ein weiterer Nutzen: Die Programme werden dadurch leichter zu lesen. Voraussetzung sind natürlich aussagekräftige Namen für die Unterprogramme und eine möglichst gute Kommentierung, auch im Programmkopf. Unterprogramme können außerdem geschachtelt, das heißt, von Unterprogrammen aufgerufen werden.

    UP können innerhalb der Programme, die diese verwenden, oder außerhalb derselben in UP-Bibliotheken definiert sein. Sie sollen möglichst universell programmiert und damit für wechselnde Datenwerte geeignet sein. Folglich müssen Eingabe- und Ausgabeparameter übergeben werden. Unterprogramme müssen auch an beliebiger Stelle des Hauptprogramms aufgerufen werden können (und damit auch an mehreren Stellen), sie sollten daher den Inhalt derjenigen Register sichern (z. B. auf dem Stack), die innerhalb des UP verändert werden. Das aufrufende Programm muss die Initialisierung des Stackpointers vornehmen, beim Aufruf des UP die Eingabeparameter an vereinbarter Stelle bereitstellen und eventuell Platz für Ausgabeparameter reservieren.

    Es wird allerdings ein Mechanismus notwendig, der die Rückkehr aus dem Unterprogramm zum Befehl nach dem Unterprogramm-Aufruf sicherstellt. Der Einsprung in ein Unterprogramm erfolgt deshalb mit einem speziellen Sprungbefehl, der automatisch den aktuellen Befehlszählerstand (Rücksprungadresse) auf den Systemstack rettet, bevor er mit der Sprungziel-Adresse überschrieben wird. Der Rücksprung nach Beendigung des Unterprogramms zurück ins aufrufende Programm besteht dann nur aus einem Zurückschreiben der beiden obersten Stack-Bytes in den Befehlszähler.

    Der "Jump to Subroutine" dient zum Sprung zu einem Unterprogramm (Function, Procedure in höheren Programmiersprachen). Der Unterschied zum "normalen" Sprung besteht darin, dass am Ende des Unterprogramms mit einem "Return from Subroutine" (RTS) an die Stelle des "Aufrufs" (Absprungs) zurückgekehrt werden muss.

    JSR adr.

    Beim JSR wird also vor dem Überschreiben des PC der aktuelle PC-Stand auf den Stack gerettet, d.h. der Stack wächst dabei um 2 Bytes! So kann der RTS-Befehl auf dem Stack den ursprünglichen PC-Stand wiederfinden (Rücksprung aus dem Unterprogramm zurück in das aufrufende Programm).

    BSR dest.

    RTS (return from subroutine)

    Der RTS-Befehl dient dazu, aus einem Unterprogramm in das "aufrufende" Programm zurückzukehren. Notwendige Voraussetzung ist, dass vom aufrufenden Programm per JSR- oder BSRBefehl in das Unterprogramm gesprungen wurde, denn nur diese Befehle retten vor dem Sprung den Inhalt des PC auf den Stack.

    Unterprogramme lassen sich beliebig schachteln.

    Typische Anfängerfehler:

    Programmunterbrechungsbefehle

    SWI (software interrupt)

    RTI (return from interrupt)

    Reine Verzögerungsbefehle

    Anwendungen:

    3.6 Stapelspeicher (Stack)

    Zum Zwischenspeichern von Information ist ein Register(-satz) prinzipiell sehr gut geeignet. Das Problem ist nur seine sehr begrenzte Kapazität. Legt man temporäre Zwischenspeicher im Arbeitsspeicher an, so muss man Geschwindigkeitseinbußen in Kauf nehmen aber vor allem ist die Verwaltung derartiger "Hilfszellen" umständlich und zeitraubend. Eine Lösung dieses Problems bietet ein Stapelspeicher (Stack). Dies ist ein wortorganisierter Speicher mit implizitem Zugriff → LIFO-Speicher (Last In First Out). Die Realisierung beim 68HC11 erfolgt als externer Stack (Software-Stack). Er ist Teil des Arbeitsspeichers, Die automatische Adressierung erfolgt über den Stapelzeiger (Stackpointer, S). Der Stackpointer ist ein 16-Bit-Register und zeigt immer auf die nächste freie Zelle. Der Stack wächst zu kleiner werdenden Adressen ("nach unten").

    Am Programmanfang muß der Stack mit einem sinnvollen Wert besetzt werden.

    Die Vorteile des SW-Stacks mit Stackpointer-Adressierung sind unter anderem:

    Befehle für den Stack-Zugriff

    Man kann normalerweise nur immer das "oberste" Stackelement lesen. Die Stack-"Verwaltung" muss sehr sorgfältig geschehen, damit jederzeit klar ist, welche Information gerade "oben" liegt oder - besser ausgedrückt - wo der Stackpointer gerade hinzeigt. Unsauberer Umgang mit dem Stack führt fast unweigerlich zu unverständlichen Programmabstürzen!

    Anwendung des Stack

    Der Stack dient dem Zwischenspeichern von Registerinhalten um:

    Wichtig ist der sorgfältige und überlegte Umgang mit Stack- und SP-Operationen. Damit kein Stack-Überlauf(-Unterlauf) auftritt:

    3.7 Unterprogramme (Subroutines)

    Unterprogramme sind Programmteile (=Befehlsfolgen), die mit einem Namen versehen sind und unter diesem Namen aufgerufen werden. UP können in den Programmen, die diese verwenden, oder außerhalb derselben definiert sein → UP-Bibliotheken. Sie sollen möglichst universell programmiert (und damit für wechselnde Datenwerte) sein → Parameter müssen übergeben werden (Eingabe- und Ausgabeparameter). UP müssen an beliebiger Stelle des Hauptprogramms aufgerufen werden können (und damit auch an mehreren Stellen), sie sollten daher den Inhalt derjenigen Register sichern (z. B. auf dem Stack), die innerhalb des UP verändert werden. Typische Anwendungen der UP-Technik sind: Programme mit UP dauern etwas länger als ohne (meist überwiegen jedoch die Vorteile der UP).

    Anmerkungen zur Dokumentation:

    Aufgaben des aufrufenden Programms:

    Unterprogramm-Sprungbefehle

    sind spezielle Sprungbefehle, die vor dem Sprung die Rücksprungadresse auf dem Stack sichern. Beim Rücksprung wird diese Adresse automatisch geladen.

    Sprung in das UP: JSR, BSR

    Unterscheidung in der Adressierungsart. Wirkung:

    Rücksprung aus dem UP: RTS

    Letzter Befehl des UP, Wirkung:

    Achtung: Vor dem Rücksprung muß der Stackpointer i. a. auf die gleiche Adresse zeigen, wie nach dem Ansprung des UP → Sorgfalt bei Stackoperationen notwendig! Beim UP-Sprung (und Rücksprung) wird nur der PC-Inhalt geändert, der Inhalt aller anderen Register bleibt erhalten.
    → Register können zur Parameterübergabe verwendet werden
    → Im UP verwendete Register müssen gegebenenfalls gesichert werden

    Vorteil der Speicherung der Rücksprungadresse auf dem Stack:

    Parameter-Übergabe

    Eingabeparameter: Werte → UP
    Ausgabeparameter: Werte UP

    Möglichkeiten der Parameterübergabe:

    Beispiel: Verzögerungsroutine

    Es soll eine Verzögerung von 1 ms erreicht werden. Das Prinzip der Routine ist recht einfach: Ein Akkumulator wird mit einem Anfangswert besetzt und dann in einer Schleife solange dekrementiert, bis sein Wert Null geworden ist.

    
           ORG   PROG     Programm soll bei Adresse PROG beginnen 
    MAIN   BSR   VERZ     Aufruf Unterprogramm 
    STOP   BRA   STOP     Endlosschleife, stoppt Programm 
                          ("Wiederbeleben" des Computers mit Reset) 
    
    * Verzögerungs-Unterprogramm 
    * Verzögerungszeit: 1 ms 
    * Keine Parameter 
    *
    VERZ   LDAA  #221     Anfangswert (Konstante) laden (siehe unten) 
    VZ     NOP            No Operation (Zeitverzoegerung)
           NOP            No Operation (Zeitverzoegerung)
           DECA           A runterzählen bis 0 
           BNE   VZ       solange A != 0, weiterer Durchlauf 
           RTS            Verzögerung erreicht (A = 0) 
    
           END 
    
    Berechnung der Verzögerungszeit: Aus der Befehlstabelle kann man die Anzahl der Taktzyklen für jeden Befehl entnehmen. Wenn man nun noch die Taktfrequenz weiß, kann man die Ablaufzeit eines Programms ausrechnen. Wir müssen vier Zeiten wissen:
         tl   Ausführungszeit LDAA         2 Taktzyklen 
         td           -"-          DECA + 2 NOP 6    -"- 
         tb           -"-          BNE          3    -"- 
         tr           -"-          RTS          5    -"- 
    
    Bei unserem Praktikumsrecher dauert ein Taktzyklus 0,5 Mikrosekunden. Die Gesamtzeit tg ergibt sich zu:
         tg = 0,5 * n * (td + tb) + tl + tr 
            = 0,5 * n * (6  + 3 ) + 2  + 5 
            = 0,5 * (n * 9 + 7) 
    
    Da tg = 1 ms = 1000 Mikrosekunden sein soll, berechnet sich n zu
         n = (1000 - 3,5) / 4,5 
           = 996,5 / 4,5 
           = 221,44  
    
      → n = 221 
    

    Es ergibt sich also ein kleiner Fehler; unsere Schleife ist einen knappen Taktzyklus zu langsam. Will man wirklich genaue Zeitintervalle, greift man auf den integrierten Timer zurück (siehe später).

    Im gezeigten Beispiel "Verzögerungsroutine" ist die maximale Verzögerungszeit durch die Wortbreite des Akku A (8 Bit → 0..255) auf etwas mehr als 1 ms begrenzt.

    Für größere Verzögerungen:

    Beispiel: Gegeben ist folgende Verzögerungsroutine mit 16-Bit-Register:

    * Verzögerungs-Unterprogramm 
    * Verzögerungszeit: 500 ms 
    * Keine Parameter 
    *
    
    ; delay dauert 3 + 5 + 4 + 5 + $f000*16 cycl
    ;              = 8,5 + $f000*8 us = 491528,5 us 
    delay   pshx	         ; X retten, 4 cycl
    	      ldx   #$f000   ; Anfangswert laden, 3 cycl
    delay1  nop            ; Zeit verschwenden, 5 x nop = 10 cycl
            nop
            nop
            nop
            nop
    				dex							; X runterzaehlen bis 0, 3 cycl
    				bne		delay1		; solange X != 0 weiter, 3 cycl
    				pulx						; X restor, 5 cycl
    				rts							; und weg, 5 cycl
    

    3.8 Interrupts (Programmunterbrechungen)

    HAlle modernen Mikroprozessoren besitzen deshalb mehr oder weniger ausgefeilte Hardware-Mechanismen zur Programmunterbrechung durch solche Ereignisse. Ein Hardware-Ereignis erzwingt einen "Unterprogrammsprung" zu einem vorbereiteten Bearbeitungs-Programm, der so genannten Interrupt-Service-Routine (ISR). Die Ähn-lichkeit des Interrupt mit einem Unterprogrammsprung (JSR) ist tatsächlich groß. Das Auftreten des Interrupt-Request-Signals auf dem Bus wird vom Mikroprogramm am Ende jedes Befehlszyklus geprüft und führt zu einem Sprung zum Beginn der ISR. Ein JSR zu einem bestimmten Unterprogramm kann an beliebigen Stellen im Programm vorkommen, der Rücksprung RTS sorgt für die Rückkehr an die richtige Stelle im aufrufenden Programm. Ein Interrupt dagegen kann prinzipiell nach jedem beliebigen Befehl des Programms wirksam werden, dementsprechend muss, wie beim JSR, der PC-Stand gerettet werden; auch das erledigt die Ablaufsteuerung bei der Annahme des Interrupt Request, genau wie beim JSR. Die Interrupt-Service-Routine wird mit einem ähnlichen Befehl wie ein Unterprogramm verlassen (RTI statt RTS), denn auch hier muss der Rücksprung wieder zur Unterbrechungsstelle zurückführen. Man unterscheidet beim 68HC11 Interrupts, die von Hardwaresignalen ausgelöst werden (XIRQ, IRQ und RESET) und den internen Interruptquellen (interne Peripherie-bausteine bzw. "illegal Operation"). Außerdem gibt es einem Befehl (SWI), der ebenfalls einen Interrupt auslöst. Ein weiteres Unterscheidungskriterium ist die Maskierbarkeit. Der "normale" Interrupt ist nur dann wirksam, wenn das entsprechende Maskenbit im CC-Register gelöscht ist (gleich 0). Die Signale RESET und XIRQ sind nicht maskierbar, sie werden deshalb auch als "nicht maskierbare Interrupts" (NMI) bezeichnet. Der XIRQ weist dabei noch eine Besonderheit auf: er ist nach einem RESET so lange gesperrt (maskiert), bis er durch Nullsetzen von Bit 6 im CC-Register freigegeben wird. Danach ist er nicht mehr sperrbar!

    Anwendungen:

    Aufgaben der CPU bei einen Interrupt:

    1. Abspeichern von Registerinhalten (bei 68HC11 im Stack)

    2. Setzen der Interruptmaske zum Sperren weiterer Interrupts
      (Unter Berücksichtigung der Priorität)

    3. Laden der Startadresse (= Interrupt-Vektor) der ISR in den PC
      • Der Interruptvektor steht an einer hardwaremäßig vorgegebenen Adresse, die von der Art des Interrupts abhängt.
      • Je nach Interrupt-Art wird die entsprechende Interrupt-Vektor-Adresse auf den Bus gegeben.
      • In den so adressierten Speicherzellen steht dann die Startadresse der ISR (= Interrupt-Vektor), die in den PC gebracht wird → Start der ISR

    4. Fortsetzung des unterbrochenen Programms nach der Interrupt-Bearbeitung: RTI-Befehl
      • weggespeicherte Register werden vom Stack geholt
      • PC erhält Adresse des nächsten Befehls des unterbrochenen Programms

    Im allgemeinen wird ein Hardware-IRQ erst am Ende eine Befehlszyklus bedient. Außer dem PC und dem Stackpointer werden keine Register beeinflußt. Interrupts können geschachtelt werden (3 Ebenen).

    Interruptsystem des 68HC11:

    SignalRESETUnbedinger Abbruch und Neustart
    SignalXIRQ & (X-Bit = 0)Unbedingte Unterbrechung (non maskable interrupt)
    SignalIRQ & (I-Bit = 0)Bedingte Unterbrechung (interrupt request)
    (kann auch interne Ursachen haben)
    I-Bit beeinflußbar durch die Befehle SEI/CLI
    BefehlSWIBefehlsgesteuerte Unterbrechung (software interrupt)
    BefehlIllegal OpcodeNicht implementierter Befehl

    Beim Auftreten eines Interrupts wird das I-Bit auf 1 gesetzt (Sperren IRQ), bei Reset und XIRQ auch das X-Bit. Beim RTI wird durch das Zurückschreiben des Statusregisters diese Sperre automatisch wieder aufgehoben. Das X-Bit kann nicht per Programm beeinflußt werden.

    Jedem dieser Interrupts ist eine eindeutige Interrupt-Vektor-Adresse (= Adresse, unter welcher der Interrupt-Vektor, also die Adresse der Interrupt-Serviceroutine, zu finden ist) zugeordnet:

    Zu jeder Interrupt-Art ist eine eigene ISR möglich. Zumindest der RESET-Vektor muß bei Einschalten vorhanden sein → ROM im höchsten Adreßbereich nötig.

    Beispiel: Starten der Interrupt-Service-Routine für XIRQ

    Grundsätzlich gilt für Programme mit Interrupt-Serviceroutinen, daß vier Schritte für die Initialisierung (zu Beginn des Hauptprogramms) nötig sind:

    1. Setzen des System-Stackpointers
      Solange er nicht gesetzt ist, werden keine Interrupts entgegengenommen.

    2. Initialisierung der Hardware
      In der Regel werden Interrupts von E/A-Bausteinen ausgelöst. Diese müssen entsprechend programmiert werden.

    3. Setzen des Interruptvektors
      Siehe oben.

    4. Freigeben des Interrupts durch Setzen des entsprechenden I-Flags im CC-Register auf 0. Bei einer ISR für XIRQ entfällt dieser Punkt natürlich.

    Zum vorhergehenden Abschnitt Zum Inhaltsverzeichnis Zum nächsten Abschnitt


    Copyright © FH München, FB 04, Prof. Jürgen Plate
    Letzte Aktualisierung: 02. Jul 2012