Stapeln und schieben

Für diesen Beitrag wurde das CBM prg Studio verwendet.
weitersagen ...
Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedIn

CBM prg StudioBinärzahl ausgeben

Diesmal wollen wir eine Binärzahl auf dem Bildschirm ausgeben. Wir werden dabei eine ganze Reihe neuer Befehle kennenlernen und auch den Stack verwenden.

Aber bevor wir mit dem Programm beginnen, sollten wir uns zunächst mal Gedanken machen, wie wir das Problem lösen.
Ich weiß, es juckt euch in den Fingern direkt drauflos zu programmieren, aber lasst uns kurz überlegen (auch ohne alle Assemblerbefehle zu kennen) wie das Problem gelöst werden kann.

Aufgabenstellung: Binärzahl ausgeben

Eine 8-Bit Zahl soll als Abfolge von Nullen und Einsen auf dem BS ausgegeben werden. Die Zahl legen wir im Programm fest in einer Variablen ab und fürs Erste geben wir die Binärzahl direkt oben links in der Ecke des BS aus, also einfach am Beginn des Bildschirmspeichers.

Überlegungen zu Lösung:

Bereits in den vergangenen Beispielen haben wir Pik-Zeichen (ihr erinnert euch?) auf dem BS ausgegeben. Die Ausgabe eines Zeichens sollte also nicht allzu kompliziert sein, aber wie erkennen wir, wann eine 0 und wann eine 1 benötigt wird?
Uns fällt es mittlerweile nicht schwer,  z. B. die Zahl $C9 binär aufzuschreiben. Seit unseren Übungen bei den Zahlensystemen wissen wir, dass $C dezimal 12 ist (A=10, B=11, C=12,…) und binär wäre es %1100 (1*2^3 + 1*2^2 + 0*2^1 + 0*2^0) und $9 = %1001 also kommen wir auf %11001001.

1. Idee (Brut-Force)
Wir schauen uns wie oben, jede Ziffer der Hex-Zahl getrennt an und geben den Text direkt für das jeweilige Nibble aus.

Das würde zwar funktionieren, aber ‘schön‚ ist was Anderes. Wir müllen uns den Speicher ziemlich zu und pflegeleicht ist so ein Source-Monstrum auch nicht gerade. Für unsere Zwecke würde das zwar reichen, aber wir wollen doch lieber mal überlegen, ob sich nicht eine schlanke und denn noch effizente Lösung finden lässt.

Verschweigen möchte ich an dieser Stelle nicht, dass eine Abfolge von Befehlen immer schneller ist, als eine Schleife. Um das zu beweisen, setzen wir unser bisheriges Wissen und die Liste der Mnemonics ein (die Anzahl der Ausgaben {10} ist bewusst gering gehalten, ihr könnt das aber gerne mal für 100 oder 200 nachrechnen  😉  ):

Beim BNE gehen wir davon aus, dass dieser nicht über die Page-Grenze springen muss.

Es gilt immer Programmier- & Pflegeaufwand sowie Speicherbedarf und Laufzeit gegeneinander abzuwägen. Wie wir sehen können, braucht die 2. Version nur 40% der Ausführungszeit der oberen, aber dafür wird auch mehr als dreimal soviel Speicher benötigt. Wäre diese Stelle super zeitkritisch und wir hätten die paar Bytes über, könnte die 2. Version durchaus interessant sein, aber wenn nun statt 10 doch 100 Ausgaben gewünscht sind, haben wir ein Problem.

Da ich zu Beginn schon wieder die Pik-Symbole erwähnt habe, möchte ich noch kurz aufzeigen, dass der Ansatz von eben (einzelne Befehle anzugeben), sinnvoll eingesetzt
zu einer insg. besseren Lösung führen kann.
Statt unserer verschachtelten Schleife (4 mal 256 Durchläufe / den erwähnten Überhang von 24 Bytes lassen wir mal außer Acht) hätten wir (analog zu eben) auch einfach schreiben können:

Diese Lösung wäre zwar die schnellste (1024 * 4 TZ + 2 TZ LDA + 6 TZ RTS = 4104 TZ), aber zu welchem Preis? Wer möchte 1024 mal STA… eingeben und damit über 3KB Speicher (1024 * 3Byte / vgl. absolute Adressierung + 2 Byte LDA + 1Byte RTS) nur fürs Füllen des BS mit Pik-Symbolen opfern? Wenn wir dann noch, wie gleich, das setzen der Farbe hinzunehmen, müssen wir die Werte verdoppeln, also über 6KB (auch die 4 Bytes auf der Zero-Page dürfen wir nicht vergessen) und 8202 TZ (nur 1x 6 TZ für RTS, das natürlich auch nur 1 Byte ‚kostet‚).

Da ist unsere bisherige Version doch schon schicker, oder?

Aber noch besser ist folgender Ansatz:

Das Programm macht exakt das Selbe, wie unsere bisherige Version (nach der 1. Korrektur auf 256 Zeichen). Es ist aber nur noch 32 Source-Zeilen, statt bisher 50, lang. Wer Speicherbedarf und Laufzeit überprüft, wird feststellen, dass hier sogar beide geringer, als beim bisherigen Beispiel sind und auf die Zero-Page verzichten wir auch noch.

Version
Bytes
%-Änderung
Taktzyklen
%-Änderung
Tausende STA-Zeilen
6153
18645%
8202
100%
Bisherige Version
39
118%
23643
288%
Neue Version: Eine Schleife mit 8x STA
33
100%
12553

153%

Ich würde unsere neue Version als sehr gelungen bezeichnen. Ein Blick auf die Tabelle verrät uns, dass sie (auf diese drei Beispiele bezogen) die beste Lösung darstellt.

Machen wir weiter mit unserer eigentlichen Aufgabe.

2. Idee
Wir schauen uns wieder jede Hex-Ziffer getrennt an und geben je Nibble die passende 0/1-Kombination aus, die wir als Text fest im Speicher ablegen.

Schon besser, aber wir benötigen allein 64 Byte für die Texte zu den Nibble. Es wäre doch schöner, direkt das entsprechende BIT auszugeben…

3. Idee
Wir prüfen jedes einzelne Bit, ob es Null oder Eins ist und geben dann die entsprechende Ziffer aus.

Ich denke diese Idee ist es wert, einmal umgesetzt zu werden.

Kurz gefasst, in OUTPUTVAL hinterlegen wir unsere Zahl, die ausgegeben wird. In den Akku laden wir die Nr. des Bits, das wir prüfen möchten und ins Y-Register kommt die Ausgabeposition für den BS.
Unsere Zahl, die wir ausgeben wollen, steht hier übrigens, wie wir gleich sehen, beim Label number: am Ende des Programms.

BIT: BIT test (Bits testen)
BIT absolut ($2C, 3B, 4T, NVZ)
Der BIT-Befehl führt im Prinzip eine bitweise UND-Verknüpfung (die wir schon vom AND kennen) durch. Es gibt aber einige Besonderheiten im Gegensatz zum AND. Wie ihr bei den Flags erkennen könnt, setzt BIT nicht nur das Negativ- und Zero-Flag, wie AND, sondern evtl. auch die oVerflow Flagge. Das liegt daran, dass der BIT-Befehl zuerst Bit-7 & 6 des Bytes an der angegebenen Adresse prüft, dadurch werden N und V beeinflusst, abschließend wird die UND-Verknüpfung durchgeführt, was wiederum Z beeinflusst. Es ist sogar möglich, dass alle drei Flags gleichzeitung auf 1 gesetzt werden, beim AND können N und Z nie gleichzeitig auf 1 stehen. Der nächste Unterschied, liegt darin, dass im Gegensatz zum AND, kein Ergebnis gespeichert wird, es werden also nur die Flags gesetzt.

Schauen wir uns mal an, was beim ersten Durchlauf passiert.

Da zuerst Bit-7 und 6 des Bytes an der angegebenen Speicherstelle geprüft werden, werden durch obiges Beispiel die N– & V-Flagge auf 1 gesetzt. Durch die anschließende AND-Verknüpfung, kommt BIT schließlich zum Ergebnis %10000000, wodurch die Z-Flagge auf 0 gesetzt wird.

Der Abschnitt erklärt sich eigentlich von selbst, wenn nach unserer BIT-Prüfung das Zero-Flag auf 0 steht, springen wir zum Label one: um das Zeichen für die Eins ins X-Register zu laden und direkt zur Ausgabe ‚weiter zulaufen‚. Ist Z=1 haben wir eine Null und springen nicht, wir laden dann das Zeichen für die Null ins X-Register und springen anschließend ‚über‚ den Abschnitt für one: zum Label out: um mit der Ausgabe zubeginnen.

PHA: PusH Akku on stack (Akku auf den Stack schieben)
PHA implizit ($48, 1B, 3T, <keine>)
Mit PHA schieben wir den Akku-Inhalt auf den Stack. Da wir den Akku gleich für etwas Anderes benötigen, merken wir uns seinen Inhalt vorläufig auf dem bereits erwähnten Stack.

Wat is‘n Stack? Da stelle mer uns ma janz dumm…
…und stellen uns den Stack einfach als einen Stapel vor, vgl. ‚Kleine Hardwarekunde‚. Dort habe ich den Stack mit einem Bücherstapel verglichen. Nun sind wir aber schon viel weiter und schauen uns den Stack mal von der technischen Seite an.
Der Stack dient also zum Zwischenspeichern von Werten, die wir später noch mal benötigen. Wie ihr beim RTS gesehen habt (und weiter unten nochmal genauer seht) kann der Stack sogar als Quelle für ein Sprungziel dienen.

 

Füllrichtung des Stacks.
Füllrichtung des Stacks.

Um unsere Werte zu speichern, benötigt der Stack natürlich Speicher. Die 1. Page ($0100 – $01FF) des Speichers ist für den Stack reserviert. Der Stack wird von $01FF absteigend bis $0100 gefüllt.

 

 

 

 

 

 

 

Beispiel des Stacks vor PHA.
Beispiel des Stacks vor PHA.

Schauen wir uns mit Hilfe der Grafik rechts mal an, was passiert, wenn unser PHA ausgeführt wird.
Wir gehen davon aus, dass der Stackpointer (SP) nach dem Programmstart auf $EE (ein Byte reicht, da der Speicher wie bereits erwähnt auf der 1. Page liegt) zeigt. Der SP verweist also auf die nächste freie Speicherstelle.
Damit der Computer weiß, wo das nächste freie Byte auf dem Stack zu finden ist, gibt es den Stackpointer (SP). Der genaue Wert des Stackpointers und der Inhalt des Stacks hängt natürlich von den vorherigen Programmen und Befehlen ab. Auch die Bytes der aktuell freien Plätze auf dem Stack sind nicht vorhersagbar, sie hängen wiederum von der letzten Benutzung ab.

 

Nach dem PHA
Nach dem PHA

Wird nun PHA ausgeführt, dann wird zunächst der Inhalt des Akkus (bei uns $80, wir testen zur Zeit ja das höchste Bit) an der im SP stehenden Adresse abgelegt. Also landet an der Speicherstelle $01EE die $80 aus dem Akku. Anschließend wird der Stackpointer um eins verringert, damit er auf die nächste freie Stelle zeigt.

Wie wir den Wert wieder vom Stack bekommen, sehen wir nach zwei bekannten Befehlen.

 

 

Als nächstes geben wir unser Zeichen auf dem BS aus:

Im X-Register liegt ja das Zeichen, dass wir ausgeben möchten. Da es keine absolute Y-indizierte Adressierung für den STX-Befehl gibt, müssen wir unser Zeichen in den Akku kopieren (jetzt kennt ihr auch denn Grund fürs PHA von eben)  um es auszugeben. Das erreichen wir bekanntlich durch TXA. Direkt danach geben wir das Zeichen wie gewohnt auf dem BS aus.

PLA: PuLl Akku from stack (Akku vom Stack holen)
PLA implizit ($68, 1B, 4T, NZ)

Auswirkung des PLA
Auswirkung des PLA

Mit PLA werden wir nun wieder unseren bisherigen Wert zurück in den Akku holen, um gleich das nächste Bit zu testen.
Schauen wir uns das nochmal exemplarisch mit der Grafik rechts an. Aktuell steht der SP ja auf Adresse $ED (s. oben). Trifft die CPU nun auf den PLA-Befehl, wird zunächst der SP um eins erhöht (zeigt also auf $EE). Danach wird das Byte von dieser Speicherstelle (unsere $80) in den Akku kopiert. Im Gegensatz um PHA beeinflusst der PLA auch die N– und Z-Flaggen.
Da nur kopiert wird, bleibt die $80 natürlich auch auf dem Stack liegen.

Jetzt das Y-Register um eins erhöhen, damit wir zur nächsten BS-Position kommen.

 

LSR: Logical Shift Right (bitweise Verschiebung nach rechts)
LSR Akku ($4A, 1B, 2T, NZC)
Wir haben uns ja vorgenommen, alle Bits nacheinander zu prüfen. Zum Glück gibt es den LSR-Befehl. Dieser verschiebt den Inhalt das Akkumulators um jeweils ein Bit nach rechts. Dabei wird von links eine 0 eingeschoben und das Bit, dass rechts herausfällt, landet im Carry-Flag. Auch die Negativ- und Zero-Flagge werden beeinflusst.
Schauen wir uns mal an, was während der acht Durchläufe in unserem Programm beim LSR passiert.

LSR
LSR

Uns interessiert hier das Zero-Flag. Wir werden unsere Schleife solange laufen lassen, bis im Akku eine Null steht.

Mit dem BNE springen wir wie eben erwähnt solange zum Label binoutloop:, bis der Inhalt des Akkus Null ist.

Rücksprung zum Basic.
Rücksprung zum Basic.

Zum Schluß springt der RTS-Befehl wieder zurück zum Basic. Das haben wir ja schon häufig gemacht. Aber diesmal betrachten wir den Stack, einmal näher. Der Befehl holt sich zwei Bytes vom Stack (LSB & MSB) um die Rücksprungadresse zu erhalten. Bei uns sind das $E9 und $A7, der SP wird dabei jeweils um eins verringert und zeigt zum Schluß in unserem Beispiel auf $F0. Die Adresse vom Stack wird noch um eins erhöht, so dass letztendlich zur Adresse $A7EA gesprungen wird (sprich der PC wird auf diese Adresse gesetzt).

Das Label number: hinter dem RTS nimmt unsere Zahl auf, die wir ausgeben wollen, damit der BIT-Befehl diese prüfen kann.

Unser Programm ist nun fertig.

Hier, wie so häufig, das komplette Listing:

Oben links hat sich nun unsere binäre Ausgabe von $C9 versteckt.

Binäre Ausgabe.
Binäre Ausgabe.

Gebt ruhig mal andere Zahlen hinter OUTPUTVAL ein, um die Ausgabe zu testen.


Kommen wir wieder zu den weiteren Adressierungsarten und Befehlsvarianten.

BIT Zero-Page ($24, 2B, 3T, NVZ)
Der BIT-Befehl verfügt nur noch über eine weitere Adressierungsart und das ist die Zero-Page-Adressierung.


Bei den Stackbefehlen gibt es noch zwei weitere, um das Statusregister auf dem Stack zu speichern und zurückzuholen.

PHP: PusH Processor Status on Stack (Statusregister auf den Stack legen)
PHP implizit ($08, 1B, 3T, <keine>)
Mit PHP könnt ihr das Statusregister (also unsere Flags) auf dem Stack sichern. Das könnte z. B. notwendig sein, wenn ihr nach einem Anweisungsblock wieder die gleichen Flags wie davor benötigt.

PLP: PuLl Processor Status from Stack (Statusregister vom Stack holen)
PLP implizit ($28, 1B, 4T, NV-BDIZC)
Wenn ihr die Flags wieder zurück ins Statusregister kopieren wollt, müsst ihr PLP verwenden. Der Befehl beinflusst als einziger aller Flags, was klar ist, da er die Flags anhand des Bytes auf dem Stack setzt.


Der LSR-Befehl kennt noch die folgenden Adressierungsarten:

LSR absolut ($4E, 3B, 6T, NZC)
Verschiebt die Bits des Bytes an der absoluten Adresse nach rechts.

 

LSR absolut X-indiziert ($5E, 3B, 7T, NZC)
An der angegebenen Adresse + Inhalt des X-Registers, die Bits des dort zu findenen Bytes nach rechts verschieben.

 

LSR Zero-Page ($46, 2B, 5T, NZC)
Verschiebt an der angegebenen Zero-Page-Adresse, die Bits des dortigen Bytes nach rechts.

 

LSR Zero-Page X-indiziert ($56, 2B, 6T, NZC)
Die Bits des Bytes, an der Zero-Page-Adresse + Inhalt des X-Registers, nach rechts verschieben.

 

Es gibt auch ein Verschieben in die andere Richtung.

ASL: Arithmetic Shift Left (bitweises verschieben nach links)
ASL Akku ($0A, 1B, 2T, NZC)
Das Gegenstück zum LSR, nur das hier die Bits nach links verschoben werden. Dabei wird von rechts mit Nullen aufgefüllt und das herausfallende Bit landet im Carry-Flag.

Da wir den Befehl schon im nächsten Beitrag verwenden werden, führe ich den hier nicht weiter aus und vertage das bis dahin.


Im nächsten Beitrag werden wir wie versprochen ein eingegebenes Zeichen (ok es wird nur vom BS gelesen) binär und hexadezimal ausgeben. Außerdem lernen wir Subroutinen kennen und benutzen zur Abwechslung mal ein oder zwei System-Routinen.


Schrott!!Naja...Geht so...Ganz gut...SUPER! (9 Bewertungen | Ø 5,00 von 5 | 100,00%)

Loading...


<<< zurück | weiter >>>

weitersagen ...
Tweet about this on TwitterShare on FacebookShare on Google+Share on LinkedIn

10 Gedanken zu „Stapeln und schieben“

  1. Ich habe das eben folgendermaßen gelöst. Dann braucht man das BIT und TXA nicht.
    Den Vorteil von BIT erkenne ich auch nicht besonders.
    Der Kurs hier ist super. Den hätte ich in der 6ten Klasse der Realschule benötigt als
    die Begeisterung für c64-Assembler noch wesentlich stärker war.
    Das ist besser als so manches Buch was ich gelesen habe.

    *=$0801
    byte $0B,$08, $01,$00, $9E, $20, $32, $30, $36, $32, $00, $00, $00

    screenadr = $0400
    nr = $c9

    lda #$25
    sta screenadr
    ldx #$08
    lda #nr

    loop:
    clc
    lsr
    pha ; accu auf den Stack
    bcs carry:
    lda #$30
    sta screenadr,X
    jmp next:
    carry:
    lda #$31
    sta screenadr,X
    next:
    pla ; accu vom Stack
    dex
    bne loop:

    rts

    1. Nabend,
      erstmal schön, dass dir der Kurs gefällt und ich finde es gut, dass du deinen eigenen Weg gehst.

      Falls sich deine Bemerkung bzgl. BIT auf das Listing bezieht, es war nur drin, da ich ein Beispiel haben wollte.
      Denk immer daran, dass die Beispielprogramme nur der Veranschaulichung dienen und keine perfekten Routinen sind.

      Weiterhin viel Spass,
      Jörn

  2. Hi Joern, dear mates !
    If I am not wrong (please check, dear mates — I am very tired at the moment) one could
    save 2 cycles in the binary printout in skipping the txa step and pushing the accumulator to stack before the char is loaded in x, see below. The memory footprint is however slightly higher (one byte).

    Thanks for the great page — I learn a lot here.
    Cheers, Stephan

    ————————–8 No txa necessary (saves 2 cycles, but needs more memory because of 2 x pha)

    sta SCREENRAM, y
    pla
    iny
    lsr
    bne loop

    rts

    numberToConvert:
    .byte $00
    ————————–8<—————————–

    1. Hello Stephan,
      you’re right. You can optimize nearly every code snippet on my page.
      It’s not my aim to show always the shortest or fastest way.

      If you really want to speed up things and shorten the code, have a look at this:

      SCREEN = $0400 ;Start des Bildschirmspeichers
      OUTPUTVAL = $C9 ;Diese Zahl wird ausgegeben

      ;*** Startadresse BASIC-Zeile
      *=$0801
      BYTE $0B,$08, $DD,$07, $9E, $20, $32, $30, $36, $32, $00,$00, $00

      ;*** Start des Assemblerprogrammes
      lda #%10000000

      binoutloop:
      ldx #'0'
      bit number:
      beq out:
      inx
      out:
      stx SCREEN
      inc out:+1
      lsr
      bne binoutloop:
      rts

      number:
      BYTE OUTPUTVAL

      Greetings,
      Jörn

  3. Arthur hat das von Seb gemeldete Problem

    lda 'x'
    lda #'x'
    
    jetzt auf seiner Liste und ich habe die entsprechenden Stellen angepasst.

    Sollte ich etwas übersehen haben, wäre ich für einen Hinweis sehr dankbar.

    Das CBM prg Studio hat diese Befehle bisher leider noch nie korrekt verarbeitet!

    Hier nochmal, wie es eigentlich funktionieren sollte und hoffentlich bald von Arthur umgesetzt wird:

    lda #'x' ;soll das BYTE für das Zeichen 'x' laden
    lda 'x'  ;soll das BYTE von der Adresse, die durch
             ;das Zeichen 'x' bestimmt wird, laden
    
  4. Hi,
    CBM Prg Studio 3.0 scheint befehle wie ldx ‚0‘ nicht mehr zuzulassen. Oder sollte man dafür was in den Options ändern? Vorher ging das einwandfrei. Danke.

    1. Ist mir bisher noch gar nicht aufgefallen, danke für den Hinweis.
      Ich habe die Frage mal an Arthur weitergegeben.

      Du kannst aber auch ldx #’a‘ nehmen, was in meinen Augen auch die richtige Schreibweise ist.

      Eigentlich war ldx ‚a‘ noch nie richtig umgesetzt im CBM prg Studio. Es hat sich wie ldx #’a‘ verhalten, hätte aber statt des Buchstaben ‚a‘ den Wert von der Adresse die durch das ‚a‘ bestimmt wird holen müssen.

      Ich werde nochmal kontrollieren, ob es bei Version 2.5.3 Probleme mit ldx #’a‘ gab. Ich kann mir sonst nicht erklären, weshalb ich ldx ‚0‘ verwendet habe.

      Sobald ich eine Antwort von Arthur habe, werde ich dann ggf. die Sourcecodes auf der Seite anpassen.

      1. Super, viele Dank für die schnelle Antwort. Mit lda #’a‘ funktioniert es einwandfrei. Jetzt kann ich mit den Tutorial weiterfahren. 🙂

        1. Gern geschehen.

          Ich habe es eben nochmal kontrolliert, bei 2.5.3 war es die ‚richtige‘ Schreibweise ldx #’0′, die nicht ging. Daher steht im Tutorial immer ldx ‚0‘. Bis zur BETA 3.0.0 vom Januar war diese falsche Schreibweise möglich. Jetzt wurde es leider ohne ‚Vorwarung‘ umgebaut.

          Ich hoffe, dass Arthur dies jetzt beibehält und zusätzlich ldx ‚0‘ richtig (als absolute Addressierung) einbaut.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Protected by WP Anti Spam