Mikrocomputertechnik


Prof. Jürgen Plate

Programmbeispiel-Sammlung

Den folgenden "Programmschnipseln" fehlt noch das "Drumrum" (Definitionen von Variablen, Hauptprogramm, usw.). Sie dienen der Illustration verschiedener Programmiertechniken in Assembler.

Die größere von zwei Zahlen finden

Vergleichen ($40) und ($41), Ergebnis in $42
          LDAA $40       ; 1. Zahl holen 
          CMPA $41       ; 2. Zahl größer? 
          BHS  WEITER    ; 1. Zahl ist größer 
          LDAA $41       ; 2. Zahl laden 
WEITER    STAA $42       ; Abspeichern 

16-Bit-Addition (Prüfsumme)

Addition eines Feldes von 8-Bit-Werten. Die Anzahl der Werte steht in Speicherbyte $42, die zu addierenden Werte folgen ab Adresse $43. Das 16-Bit-Ergebnis wird in $40 (MSB) und $41 (LSB) abgelegt.
CHECKSUM  CLRA           ; Summe in A und B, Register 
          CLRB           ; zuerst mal löschen 
          LDX  #$0043    ; Adresse erster Summand (Feldanfang) 
CSUM      ADDB ,X        ; Feldelement addieren
          ADCA #0        ; Übertrag im MSB aufaddieren 
          INX            ; X inkrementieren 
          DEC  $42       ; Elementezähler vermindern 
          BNE  CSUM      ; Wiederholen, solange Zähler > 0 ist 
          STD  $40       ; Checksumme speichern 
          RTS 

Quadrate von 0 - 15 über Tabelle ermitteln

Wert in $41, Ergebnis in $42
          LDAB $41       ; Operanden holen 
          LDX  #TABELLE  ; X-Register auf Tabellenanfang 
          ABX            ; X und B addieren
          LDAA 0,X       ; B-ten Eintrag aus Tabelle holen 
          STAA $42       ; und abspeichern 

TABELLE   DC.B           0,1,4,9,16,25,36,49,64,81,100 
          DC.B           121,144,169,196,225 

Addition von 8-stelligen BCD-Zahlen

Die 1. Zahl belegt den Speicherbereich $41 - $44 (LSB zuerst)
Die 2. Zahl -"- $51 - $54 -"-
Das Ergebnis wird auf die erste Zahl gespeichert
          LDAB #4        ; Zahlen sind 4 Byte lang = 8 Stellen 
          LDX  #$41      ; 1. Operand 
          LDY  #$51      ; 2. Operand 
          CLRA           ; Akku A (und Carry-Flag) löschen 
BCDADD    LDAA 0,X       ; zwei Ziffern des 1.Operanden holen 
          ADCA 0,Y       ; zwei Ziffern des 2.Operanden addieren 
          INY
          DAA            ; Dezimalkorrektur für BCD 
          STAA 0,X       ; Ergebnis abspeichern 
          INX
          DECB 
          BNE  BCDADD    ; solange, bis alle Stellen bearbeitet 

Zählen der negativen Elemente in einem Feld

ANZAHL    EQU  20        ; z.B. 20 Feldelemente 

FELD      DS.B 20        ; Platz für 20 Werte 
COUNT     DS.B  1        ; 1 Byte Zähler 
           . 
           . 
           . 
          CLRB           ; Zähler für neg. Elemente löschen 
          LDX  #FELD     ; X auf Feldanfang 
          LDAA ANZAHL     
          STAA COUNT     ; Zähler setzen 
LOOP      LDAA 0,X       ; nächstes Feldelement 
          BPL  WEITER    ; wenn positiv weiter 
          INCB           ; negative Elemente zählen 
WEITER    INX
          DEC  COUNT 
          BNE  LOOP      ; bis alle Elemente getestet sind 
          STAB COUNT     ; Ergebnis in COUNT speichern 

Ein- und Ausgabe auf Ports

Das folgende Programm liest die unteren 4 Bits von Port C ein und gibt sie auf Port B wieder aus, gleichzeitig blinken die oberen 4 Bits von Port C.
          .
          .
	        .
        clr   portb      ; Portb auf 0 setzen
        ldab  #%1010     ; Register B setzen
      
        ldaa  #%11110000 ; Port C Richtungsregister setzen:
        staa  ddrc       ; Bits 0-3 Eingabe, 4-7 Ausgabe
      
loop    ldaa  portc      ; Port C lesen
        anda  #$0F       ; untere 4 Bits ausmaskieren
        staa  portb      ; auf Port B ausgeben
        stab  portc      ; Register B ausgeben
        eorb  #%11110000 ; und Bits 4-7 invertieren
        bsr   delay      ; 0,5 s warten (siehe unten)
        jmp   loop       ; endlosschleife
         .
         .
         .

Auf-/Abwärtszähler Der folgende Zähler zä,hlt abhängig von der Variablen "tflag" aufwärts oder abwärts. Die Zählrichtung wird durch eine Taste am Eingang STRA umgeschaltet.
; Vereinbarungen
prog    equ     $8000
data    equ     $2000
stack   equ     $7FFF
portb   equ     $1004
portc   equ     $1003
portcl  equ     $1005
pioc    equ     $1002
ddrc    equ     $1007
reset   equ     $fffe
         .
         .

        org   data       ; Datenbereich
tflag   ds.b  1
         .
         .

        org   prog       ; Programmbereich
main    lds   #stack     ; Stackpointer laden
        clr   portb      ; Ports/Variablen init.
        ldaa  #1         ; tflag = 1
        staa  tflag

loop    bsr   count      ; Hauptschleife, nur zwei UP-Aufrufe
        bsr   taste      ; Taste abfrage
        jmp   loop       ; auf/ab zaehlen

; Zaehl-Unterprogramm
; tflag wird addiert (tflag kann 1 oder -1 sein)
count   adda  tflag      ; zaehlen
        staa  portb      ; ausgeben
        bsr   delay      ; 0,5 s warten (siehe unten)
        rts
; Tasten-Abfrage-UP
; bei jedem Tastendruck wird tflag negiert (+1  -1, -1  +1) 
taste   psha             ; Akku A retten
        ldaa  pioc       ; Taste gedrueckt
        bpl   njet       ; nein  fertig
        ldaa  portcl     ; dummy-read zum Ruecksetzend es Tastenbits in pioc
        neg   tflag      ; tflag negieren
njet    pula             ; Akku A restaurieren
        rts
         .
         .
         .

Software-Uhr

Eine Software-Uhr wird in der Regel durch einen periodischen Interrupt (z. B. vom Timer) unterstützt. Bei jedem Interrupt wird ein Zähler incrementiert. Die Zählung erfolgt in Sekunden, Minuten und Stunden. Das im Vordergrund arbeitende Programm kann auf die Zeitinformation zugreifen.

Die Bedienung des Timer-Interrupts wird hier nur angedeutet (da noch nicht behandelt). Der CLOCK-Interrupt wird im Beispiel alle 100 ms ausgelöst = 10 Tics/s.

ZEHNTEL   DS.B 1         ; 1/10 Sekunden 
SEKUNDEN  DS.B 1         ; Sekunden 
MINUTEN   DS.B 1         ; Minuten 
STUNDEN   DS.B 1         ; Stunden 
           .
           .
           .

CLOCK
*         An dieser Stelle muß die 
*         Timer-Hardware bedient werden 
           
          INC  ZEHNTEL   ; 1/10 Sekunden erhöhen 
          LDAA #10
          CMPA ZEHNTEL   ; Sekunde erreicht? 
          BNE  ENDCLOCK  ; nein - fertig 
          CLR  ZEHNTEL   ; sonst 1/10 Sekunden auf 0 setzen 
          INC  SEKUNDEN  ; und Sekunden erhöhen 
          LDAA #60        
          CMPA SEKUNDEN  ; Minute erreicht? 
          BNE  ENDCLOCK  ; nein - fertig 
          CLR  SEKUNDEN  ; sonst Sekunden auf 0 setzen  
          INC  MINUTEN   ; und Minuten erhöhen 
          CMPA MINUTEN   ; Stunden erreicht? 
          BNE  ENDCLOCK  ; nein - fertig 
          CLR  MINUTEN   ; sonst Minuten auf 0 setzen 
          INC  STUNDEN   ; und Stunden erhöhen 
          LDAA #24        
          CMPA STUNDEN   ; Tageswechsel? 
          BNE  ENDCLOCK  ; nein - fertig 
          CLR  STUNDEN   ; sonst Stunden auf 0 setzen 
ENDCLOCK  RTI            ; Rücksprung 

Die Dauer der ISR beträgt im schlechtesten Fall weniger als 150 Taktzyklen. Da die ISR nur alle 100 ms ausgelöst wird, ist die zusätzliche Belastung des Systems relativ gering (ca. 0,1%).

Nützliche Unterprogramme

Die folgenden Unterprogramme lassen sich in eigenen Programmen verwenden.

Ergänzen eines ASCII-Codewortes auf gerade Parität

EVENPAR   PSHB           ; Codewort zwischenspeichern
          CLRA           ; Zaehler = 0
ZHL       LSRB           ; naechstes Bit ins Carry holen
          ADCA #0        ; Carry zu A ddieren
          TSTB           ; Restwort = 0?
          BNE ZHL        ; nein, weiter
          PULB           ; Codewort holen
          ASLB           ; links schieben
          LSRA           ; Parity-Bit ins Carry
          RORB           ; Parity-Bit ins MSB vom Codewort
          RTS

Binärwert (0 - 15) zu ASCII konvertieren

Unterprogramm zur Konvertierung eines Binärwertes (0 - 15) in die ASCII-Darstellung (z.B. für die Ausgabe am Terminal). Der Wert wird in Register A an das UP übergeben und das entsprechende ASCII-Zeichen wird vom UP wieder in A zurückgegeben.
Der Abstand zwischen der Ziffer "9" und dem Buchstaben "A" beträgt im ASCII-Zeichensatz 7 Zeichen, weshalb zwischen den Eingangswerten 0 - 9 und 10 - 15 unterschieden werden muss. Bei Werten größer 10 muss daher 7 addiert werden.
*  
* Konvertierung Binär zu ASCII 
* Parameter in Akku A 
*
BINASC    ANDA #0F       ; auf Werte zwischen 0 und 15 begrenzen
          CMPA #9        ; Wert 0 bis 9 ? 
          BLS  ZIF       ; ja, dann direkt umwandeln 
          ADDA #7        ; "A" - "9" - 1 = $41 - $39 - 1 = 7 
ZIF       ADDA #$30      ; $30 = "0" (ASCII-Wert) 
          RTS

ASCII-Zeichen zu Binär konvertieren

Unterprogramm zur Konvertierung eines ASCII-Zeichens (0 - 9, A - F) in einen Binärwert, z.B. bei der Eingabe von Zahlen über das Terminal. Das Zeichen wird in Akku A übergeben und der Wert wieder in A zurückgegeben. Das UP arbeitet invers zum obigen UP. Bei Fehleingabe (keine Ziffer) wird Akku A auf $FF gesetzt.
*
* Unterprogramm ASCII-Binär-Konvertierung (0 - 9, A - F) 
* Parameter in A 
*
ASCBIN    SUBA #$30      ; $30 = ASCII "0" 
          BLO  FEHL      ; Erg. negativ  keine Ziffer 
          CMPA #9        ; kleiner oder gleich 9? 
          BLS  DONE      ; dann fertig 
          SUBA #7        ; "A" - "F"  10 - 15 
          CMPA #10       ; kleiner als 10? 
          BLO  FEHL      ; dann Fehler 
          CMPA #15       ; größer 15? 
          BLS  DONE      ; nein, fertig 
FEHL      LDAA #$FF      ; Fehlerflag setzen 
DONE      RTS             

Binär zu ASCII-Decodierung (1, 2 oder 4 Stellen)

Im Speicher liegen die Werte bekanntermaßen binär vor. Um einen Speicherinhalt oder eine Adresse auf der seriellen Schnittstelle (Terminal) oder einem LC-Display ausgeben zu können, müsen die Werte als ASCII-Zeichen dargestellt werden.
Die folgenden vier miteinander verbundenen Unterprogramme erweitern das Beispiel oben und wandeln einen 16-Bit-Wert, einen 8-Bit-Wert (Byte) oder einen 4-Bit-Wert (Nibble) in eine Hexadezimalzahl in ASCII-Darstellung um (4, 2, oder 1 Stelle): Diese Unterprogramme werden beim vierten Praktikumstermin benötigt.
hx4a	  psha               ; Akku A auf dem Stack ablegen
        bsr     hx2a       ; Akku B umwandeln (Stelle 3 und 4)
        pulb               ; Akku A vom Stack holen, in Akku B ablegen
                           ; Nun einfach in hx2a fallen und Akku B
                           ; umwandeln (Stelle 1 und 2)
hx2a    pshb               ; Akku B aud dem Stack sichern
        bsr     hx1a       ; Bits 0-3 von Akku B umwandeln
        pulb               ; Original-Akku B wieder holen
        lsrb               ; viermal schieben (Bits 4-7 --> 0-3)
        lsrb
        lsrb               ; und nun die obere Haelfte umwandeln
        lsrb               ; indem einfach in hx1a gefallen wird
                           ; Nun wird die eigentliche Arbeit gemacht:
hx1a    andb     #$0F      ; die oberen 4 Bita ausmaskieren
        addb     #48       ; ASCII-Wert von "0" addieren
        cmpb     #57       ; groesser als "9"?
        bls     hx1b       ; nein, dann speichern
        addb     #7        ; sonst die Distanz zwischen "9" und "a" addieren
hx1b    stab     0,y       ; Im Speicher ablegen, auf den Y zeigt
        dey                ; Y zeigt jetzt auf die Stelle davor
        rts                ; und weg

Überlesen von Leerzeichen am Anfang einer Zeichenkette

Nach Ausführung zeigt X auf das erste Zeichen != ' '. Der String muß mit einem Zeichen != ' ' abgeschlossen sein.
          LDX  #STRING   ; Beginn der Zeichenkette 
CHECK     LDAA 0,X       ; Zeichen lesen
          INX            ; X erhoehen
CHECK     CMPA #$20      ; Leerzeichen? 
          BEQ  CHECK     ; Ja, weiter testen 
          DEX            ; nein, Zeiger korrigieren 
           .
           .
           .

STRING    DC.B '    Zeichenkette mit 4 Leerzeichen am Anfang' 
          DC.B 0    Abschlußzeichen 

Beispiel: Vergleich zweier Zeichenketten (Strings)

Die Adressen der beiden Zeichenketten werden in X und Y übergeben. Als Ergebnis wird Akku A verwendet - 0: Strings sind ungleich, FF: Strings sind gleich
; Vergleich zweier Strings, Anfangsadressen in X und Y	
; Ergebnis wird in Akku A zurueckgegeben (0: ungleich, FF: gleich)
vergl   ldaa   0,X       ; Zeichen aus String 1 vergleichen 
        cmpa   0,Y       ; mit Zeichen aus String 2 
        bne    ver0      ; Strings ungleich, fertig
        cmpa   #0        ; Stringende
        beq    ver1      ; Strings sind gleich, fertig
        inx              ; naechstes Zeichen
        iny
        bra    vergl
ver1    ldaa   #$ff
        rts
ver0    clra
        rts

Delay-Routine

Das folgende Unterprogramm erlaubt längere Verzögerungszeiten als das in Kapitel 3 vorgestellte, da hier das 16-Bit-Register X verwendet wird. "delay" dauert 4 + 3 + $f422*16 + 4 + 5 + 5 Taktzyklen, was einer Wartezeit von 8,5 + $f422*8 μs = 499992,5 μs entspricht - also ungefähr 0,5 s.
delay   pshx           ; Register x retten, 4 cycl
        ldx  #$f211    ; Anfangswert laden, 3 cycl
delay1  nop            ; 5 x nop = 10 cycl
        nop
        nop
        nop
        nop
        dex            ; X runterzaehlen, 3 cycl
        bne delay1     ; 3 cycl
        pulx           ; Register restaurieren, 5 cycl
        rts            ; 5 cycl

Komplette Programmbeispiele

Die folgenden Programme sind vollständig und lassen sich direkt assemblieren, simulieren und im Zielsystem ausführen.

Ausgabe beliebiger Bitmuster

Es wird ein Array mit Bitmustern angelegt ("muster"), die nacheinander auf Port B ausgegeben werden. Ein Nullbyte markiert das Ende des Arrays. Wenn dies erreicht ist, beginnt die Ausgabe wieder beim ersten Element. Das Array ist beliebig erweiterbar.
prog    equ    $8000       ; Vereinbarungen 
data    equ    $2000
stack   equ    $7FFF
portb   equ    $1004
reset   equ    $fffe

        org    prog
main    lds    #stack
        ldx    #muster     ; X verweist auf das erste Feldelement
loop    ldaa   0,x         ; erstes Feldelement laden
        bne    ausg        ; wenn nicht 0, ausgeben
        ldx    #muster     ; sonst wieder zum ersten Element
        bra    loop        ; ohne Ausgabe
ausg    staa   portb       ; Bitmuster ausgeben
        jsr    delay       ; 0,5 s warten
        inx                ; naechstes Element
        bra    loop
        
delay   pshx               ; Delay, siehe oben
        ldx    #$f422
delay1  nop
        nop
        nop
        nop
        nop
        dex
        bne    delay1
        pulx
        rts

muster  dc.b  %11110000, %01111000, %00111100, %00011110
        dc.b  %00001111, %00011110, %00111100, %01111000
        dc.b  %0

        org   reset      ; Reset-Vektor setzen
        fdb   main
        end

Dezimal zu Siebensegment Codierung

Der Inhalt von Akkumulator B soll in die Siebensegmentdarstellung gemäß untenstehender Abbildung umcodiert werden. Der erzeugte Code wird ebenfalls in Akku B zurückgegeben. Bei Werten größer 9, soll B mit $FF besetzt werden.

prog    equ   $8000
data    equ   $2000
stack   equ   $7FFF
portb   equ   $1004
reset   equ   $fffe

        org   data
count   ds.b  1

        org   prog
main    lds   #stack
        clr   count      ; Speicher loeschen
loop    ldab  count      ; Schleifenbeginn, count mod 16 hochzaehlen
        incb      
        andb  #$0F
        stab  count
        bsr   umco       ; Count in Akku B wird umcodiert, Ergebnis in Akku B
        stab  portb      ; und ausgeben
        jsr   delay      ; 0,5 s warten
        bra   loop

; Umcodierung binaer --> 7-Segment, eingabe und Ausgabe in Akku B
umco    ldx   #umcotab   ; Tabellenadresse laden
        andb  #$0F       ; fuer B nur Werte 0 - F zulassen
        abx              ; X = X + B
        ldab  0,x        ; 7-Sement-Wert holen, Adressierung durch X-Register
        rts
      
delay   pshx               ; Delay, siehe oben
        ldx    #$f422
delay1  nop
        nop
        nop
        nop
        nop
        dex
        bne    delay1
        pulx
        rts

; Siebensegment-Tabelle (Segmentanordnung: -abcdefg)
; Die Ziffern "0" bis "9" sind decodiert, fuer "A" bis "F"
; wird nur ein "-" angezeigt --> noch zu ergaenzen
umcotab dc.b   %01111110, %00110000, %01101101, %01111001
        dc.b   %00110011, %01011011, %01011111, %01110000
        dc.b   %01111111, %01111011, %00000001, %00000001
        dc.b   %00000001, %00000001, %00000001, %00000001

        org    reset
        fdb    main
        end

Kommandoeingabe über die serielle Schnittstelle

; Vereinbarungen
prog    equ    $8000
data    equ    $2000
stack   equ    $7FFF
portb   equ    $1004
portc   equ    $1003
portcl  equ    $1005
pioc    equ    $1002
ddrc    equ    $1007

scdr    equ    $102f
scsr    equ    $102e
sccr1   equ    $102c
sccr2   equ    $102d
baud    equ    $102b

reset   equ    $fffe

; ASCII-Zeichen
cr      equ    13
lf      equ    10

        org    prog
main    lds    #stack    ; Initialisierungen
        bsr    sini

loop    ldx    #cmd      ; Prompt ausgeben
        bsr    ssend
        bsr    recv      ; Befehl (1 Zeichen) einlesen

        cmpa   #'a'      ; ist es 'a'?
        bne    next1     ; nein, weiter
        bsr    doit      ; Kommando 'a' verarbeiten
        bra    loop
next1   cmpa   #'b'      ; ist es 'b'?
        bne    next2     ; nein, weiter
        bsr    doit      ; Kommando 'b' verarbeiten
        bra    loop
next2   cmpa   #'c'      ; ist es 'c'?
        bne    next3     ; nein, weiter
        bsr    doit      ; Kommando 'c' verarbeiten
        bra    loop
next3   ldx    #oops     ; falsche Eingabe: "Oops" ausgeben
        bsr    ssend
        bra    loop

; Kommandobearbeitungsroutine, hier als Dummy
doit    psha             ; Kommandobearbeitung, erst Akku A retten
        ldx    #k1       ; Text "Eingabe war" ausgeben
        bsr    ssend
        pula             ; Akku A wiederherstellen
        suba   #$20      ; Kleinbuchstaben in Grossbuchstaben wandeln 
        bsr    send      ; ausgeben
        bsr    crlf      ; "neue Zeile" ausgeben
        rts

; Carriage Return und Line Feed ausgeben
crlf    ldaa   #cr
        bsr    send
        ldaa   #lf
        bsr    send
        rts
      
; String ausgeben (ohne cr/lf am Ende), Stringadresse (erstes Zeichen)
; muss in Register X uebergeben werden (siehe Kapitel 5)
ssend   ldaa   0,x
        beq    sfin
        bsr    send
        inx
        bra    ssend
sfin    rts

; serielle Schnittstelle initialisieren (siehe Kapitel 5)
sini    clr    sccr1
        ldaa   #$0C
        staa   sccr2
        ldaa   #$30
        staa   baud
        rts

; Zeichen seriell ausgeben (siehe Kapitel 5)
send    ldab   scsr
        bpl    send
        staa   scdr
        rts
      
; Zeichen seriell empfangen (siehe Kapitel 5)
recv    ldab   scsr
        andb   #$20
        beq    recv
        ldaa   scdr
        rts      

; String-Konstanten (Strings werden durch ein 0-Byte abgeschlossen)
cmd     dc.b   'Kommando: '
        dc.b   0

oops    dc.b   'Oops!'
        dc.b   cr,lf,0

k1      dc.b   'Eingabe war '
        dc.b   0
      
        org    reset     ; Reset-Vektor setzen
        fdb    main
        end

Demo für den Timer-Interrupt

Das Programm zählt eine Variable "count" interruptgesteuert hoch.
prog    equ    $8000
data    equ    $2000
stack   equ    $7FFF
portb   equ    $1004
portc   equ    $1003
ddrc    equ    $1007
reset   equ    $fffe
tvec    equ    $FFDE     ; interrupt vector of timer
tmsk    equ    $1024     ; timer mask
tflg    equ    $1025     ; timer flag
pacr    equ    $1026     ; pulse accu control

        org    data
count   ds.b   1
cflg    ds.b   1

        org    prog
main    lds    #stack    ; init variables
        clr    portb
        clr    count
        clr    cflg

        ldab   #$40      ; enable PA7 interrupt
        stab   pacr      ; enable pulse accu
        ldab   #$80     
        stab   tmsk
        cli              ; enable interrupts

loop    ldaa   cflg      ; get clock flag
        cmpa   #20       ; trigger point?
        bne    loop      ; no, wait
        clr    cflg      ; yes, clear clock flag
        inc    count     ; count up
        ldaa   count
        staa   portb     ; display counter
        bra    loop

; interrupt service routine
tirq    ldaa   #$80
        staa   tflg      ; clear interrupt
        inc    cflg      ; count interrupts
        rti

        org    tvec      ; set interrupt vector
        fdb    tirq      ; DE timer overflow

        org    reset     ; set reset vector
        fdb    main
        end

Zum vorhergehenden Abschnitt Zum Inhaltsverzeichnis Zum nächsten Abschnitt


Copyright © FH München, FB 04, Prof. Jürgen Plate
Letzte Aktualisierung: 09. Apr 2009