![]() |
MikrocomputertechnikProf. Jürgen Plate |


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
| AH | AL |
| ea | ea+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.

LDAA #20 - Lade Akku A mit dem Wert 20

LDAA $A174 - Lade Akku A mit dem Inhalt der Speicherzelle $A174

LDAA <$80 - Lade Akku A mit dem Inhalt der Speicherzelle $80


CLR 5,X (ergibt als Code: 6F 05) CLR -5,X (ergibt als Code: 6F FB)

Befehlsaufbau: offset = Zieladresse - Befehlsadresse - Befehlslänge
Befehlsausführung: Zieladresse = Befehlszählerstand + offset


Maschinensprache:
| Marke | OP-Code | Operand/Adresse | Kommentar |
Beispiel:
START LDAA DAT1 ; Zähler initialisieren
BOTTOM EQU TOP + 10
Zusätzlich sind auch reine Kommentarzeilen möglich, die mit einem "*" in Spalte 1 beginnen.
| 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 |
| 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 |
| 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.
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,$03Neben 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:
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,$FFA0Bei 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 23Der 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. |
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.
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 100Wichtig 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.
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
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:

Load Register from Memory or Constant
CLR Clear Akkumulator: 0 → Akkumulator-Register

Store Register into Memory (8 Bit/16 Bit)
Clear Memory

Kopie des Inhalts eines Registers in ein anderes (transfer)
(A) → B
(A) → Statusregister
(B) → A
(Statusregister) → A
(S) → X
(S) → Y
(X) → S
(Y) → S
Austausch von Registerinhalten (exchange)
D
X
D
Y
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.

(A) + (adr.) → A
(B) + (adr.) → B
(D) + (adr., adr+1) → D (16-Bit-Addition)

(A) + (B) → A
(X) + (B) → X (Adreßrechnung),
B als vorzeichenlose Zahl aufgefaßt
(Y) + (B) → Y (Adreßrechnung),
B als vorzeichenlose Zahl aufgefaßt
Addieren mit Übertrag (add with carry)
(A) + (adr.) + C → A
(B) + (adr.) + C → B
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)
(A) + 1 → A
(B) + 1 → B
(S) + 1 → S
(kein Flag beeinflußt)
(X) + 1 → X
(nur Z-Flag beeinflußt)
(Y) + 1 → Y
(nur Z-Flag beeinflußt)
(adr.) + 1 → adr.
(Increment eines Speicherbytes)
(A) - (adr.) → A
(B) - (adr.) → B
Subtraktion mit Übertrag (subtract with carry)
(A) - (adr.) + C → A
(B) - (adr.) + C → B
Erniedrigen (decrement)
(A) - 1 → A
(B) - 1 → B
(S) - 1 → S
(kein Flag beeinflußt)
(X) - 1 → X
(nur Z-Flag beeinflußt)
(Y) - 1 → Y
(nur Z-Flag beeinflußt)
(adr.) - 1 → adr.
(Decrement eines Speicherbytes)
Vorzeichenwechsel (negate)
A komplementieren
B komplementieren
adr. komplementieren
Vorzeichenlose Multiplikation (A) * (B) → D
Dividieren (divide)
Vorzeichenlose Division, ganzzahliger Anteil (integer divide)
für D > X: (D) / (X) → X, Rest in D
Vorzeichenlose Division, Quotient echter Bruch (fractional divide)
für D < X: (D) / (X) → X, Rest in D
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
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
.
.

(A) and (adr.) → A
(B) and (adr.) → B
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
(A) or (adr.) → A
(B) or (adr.) → B
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
(A) exor (adr.) → A
(B) exor (adr.) → B
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
(A) and (adr.) → Nur Flags neu setzen, A bleibt unverändert
(B) and (adr.) → Nur Flags neu setzen, B bleibt unverändert
not (A) → A
not (B) → B
not (adr.) → adr.
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.

Akku A nach rechts schieben
Akku B nach rechts schieben
Akku D nach rechts schieben
Speicherwort adr. nach rechts schieben
LSLx Logical Shift Left:
von rechts wird 0 nachgeschoben: 0 → LSB
MSB → C-Flag
Akku A nach links schieben
Akku B nach links schieben
Akku D nach links schieben
Speicherwort adr. nach links schieben
Akku A nach rechts schieben
Akku B nach rechts schieben
Speicherwort adr. nach rechts schieben
ASLx Arithmetic Shift Left:
von rechts wird 0 nachgeschoben: 0 → LSB
MSB → C-Flag
Identisch mit LSL!
Akku A nach links schieben
Akku B nach links schieben
Akku D nach links schieben
Speicherwort adr. nach links schieben
ROLx Rotate Left
C-Flag → LSB
MSB → C-Flag
Akku A linksrotieren
Akku B linksrotieren
Speicherwort adr. linksrotieren
RORx Rotate Right
C-Flag → MSB
LSB → C-Flag
Akku A rechtsrotieren
Akku B rechtsrotieren
Speicherwort adr. rechtsrotieren
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
Akku A = 0 ?
Akku B = 0 ?
(adr.) = 0 ?
| 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).
(A) - (adr.) → Flags
(B) - (adr.) → Flags
(D) - (adr., adr.+1) → Flags
(X) - (adr., adr.+1) → Flags
(Y) - (adr., adr.+1) → Flags
(A) - (B) → Flags
| • 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 |
LDAA #mask
ORAA adr.
STAA adr.
LDAA #mask
COMA
ANDA adr.
STAA adr.
XIRQ & X=1: next instruction
XIRQ & X=0: service interrupt
IRQ & I=1: no activity
IRQ & I=0: service interrupt
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.

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 8009Der 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
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).
| Befehl | Bedeutung | Sprungbedingung |
|---|---|---|
| einfach (simple) | ||
| BCC dest. | carry clear | C=0 |
| BCS dest. | carry set | C=1 |
| BVC dest. | overflow clear | V=0 |
| BVS dest. | overflow set | V=1 |
| BNE dest. | result not equal zero | Z=0 (BZC) |
| BEQ dest. | result equal zero | Z=1 (BZS) |
| BPL dest. | result plus | N=0 (BNC) |
| BMI dest. | result minus | N=1 (BNS) |
| natürlich (unsigned) | ||
| BHI dest. | higher | Op1 > Op2 |
| BLS dest. | lower or same | Op1 <= Op2 |
| BLO dest. | lower | OP1 < Op2 |
| BHS dest. | higher or same | Op1 >= Op2 |
| konegativ (signed) | ||
| BGT dest. | greater than | Op1 > Op2 |
| BLE dest. | less or equal | Op1 <= Op2 |
| BLT dest. | less than | Op1 < Op2 |
| BGE dest. | greater or equal | Op1 >= Op2 |
| bitabhängig | ||
| BRCLR | adr. 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. |
| BRSET | adr. 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 #$12Beim "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);
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.
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).


Typische Anfängerfehler:
No Operation
Branch Never

Am Programmanfang muß der Stack mit einem sinnvollen Wert besetzt werden.
Die Vorteile des SW-Stacks mit Stackpointer-Adressierung sind unter anderem:

Schreiben in den Stack:
Register A → Stack
Register B → Stack
Register X → Stack
Register Y → Stack
Lesen aus dem Stack:
Stack → Register A
Stack → Register B
Stack → Register X
Stack → Register Y
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!
Wichtig ist der sorgfältige und überlegte Umgang mit Stack- und SP-Operationen. Damit kein Stack-Überlauf(-Unterlauf) auftritt:
PSHA
PSHB
.
.
.
PULB
PULA
Anmerkungen zur Dokumentation:
Aufgaben des aufrufenden Programms:
Sprung in das UP: JSR, BSR
Unterscheidung in der Adressierungsart. Wirkung:
Rücksprung aus dem UP: RTS
Letzter Befehl des UP, Wirkung:
Vorteil der Speicherung der Rücksprungadresse auf dem Stack:
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

Anwendungen:
Aufgaben der CPU bei einen Interrupt:

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).
| Signal | RESET | Unbedinger Abbruch und Neustart |
| Signal | XIRQ & (X-Bit = 0) | Unbedingte Unterbrechung (non maskable interrupt) |
| Signal | IRQ & (I-Bit = 0) | Bedingte Unterbrechung
(interrupt request) (kann auch interne Ursachen haben) I-Bit beeinflußbar durch die Befehle SEI/CLI |
| Befehl | SWI | Befehlsgesteuerte Unterbrechung (software interrupt) |
| Befehl | Illegal Opcode | Nicht 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:
Zum vorhergehenden Abschnitt |
Zum Inhaltsverzeichnis |
Zum nächsten Abschnitt |