Vergleiche und Zubehör [2 neue Flags] [bedingte(!) Sprünge] ("Programmieren" lernen)

  • Da man es ja nunmal bei Rechnen öfters mit Zahlen zu tun hat, bietet es sich gelegentlich an, Vergleiche anzustellen. Man schaut sich also zwei Werte an und stellt dann fest, welcher der größere und welcher der kleinere ist, oder auch ob beide gleich sind. Vielleicht auch ob einer größer-gleich oder kleiner-gleich ist.


    Ganz wie bei normalen Zahlen macht man das, indem beide voneinander subtrahiert werden.

    Es wird also gerechnet


    A - B


    und dann das Ergebnis angeschaut; folgendes ist dabei möglich


    A und B sind gleichgroß - es ergibt sich Null


    A ist größer als B - es entsteht ein Ergebnis größer Null


    A ist kleiner als B - es entsteht ein Ergebnis kleiner Null



    Nun passiert eigentlich nicht anderes, als sonst auch, wenn Zahlen in der CPU besondere Werte annehmen: Es werden entsprechende Flags in der CPU gesetzt.


    Am Besten verständlich ist das bei der "gleichgroß" Variante. Es ergibt sich als Subtraktionsergebnis "0", wenn A = B ist.

    Folglich wird auch das Zero-Flag gesetzt ! (und das kennt man ja mittlerweile schon)

    Das bedeutet auch, wenn man nun das Zero-Flag abfragt, dann kann man Zahlenpaare die gleichgroß sind von ungleichen Zahlenpaaren unterscheiden und unterschiedlich darauf reagieren.


    Im Fall des Zero-Flags erhalten die beiden möglichen Zustände den Namen

    EQ - wie Equal - wenn A = B

    NE - wie Not-Equal - wenn A <> B


    ( da das so einfach ist, ist diese Bezeichnung auf fast allen CPUs quasi gleich )



    Daneben werden noch zwei weitere Flags beeinflußt, beim 6502 nämlich noch das Negativ-Flag und das Carry-Flag. Auf anderen CPUs evtl. auch noch andere.


    Das Carry-Flag ist ein bißchen "einfacher": Es bleibt ungesetzt, wenn die Subtraktion kleiner 0 wird, also wenn A < B ist. Es wird dagegen eingeschaltet, wenn beim Subtrahieren >= 0 herauskommt, also A >= B ist.


    Dieses kennt dann die beiden Zustände

    CS - Carry Set - wenn A >= B

    CC - Carry Clear(ed) - wenn A < B


    Das Negativ-Flag ist "komplexer": Es schaut darauf, ob ein Wert (virtuell) negativ ist. Das ist eine Konvention, die besagt, daß auf einem Rechner eine Zahl negativ ist, wenn das Bit mit der höchsten Wertigkeit gesetzt ist. In einem 8 Bit Rechner ist das also das Bit Nummer 7 (wenn man bei Bit 0 zu zählen beginnt) (bei einem 32 Bit Rechner wäre es eben das Bit 31). Die Zahl, die entsteht, wenn man bei 8 Bit das höchste Bit setzt, liegt genau in der Mitte der damit darstellbaren Zahlen; das gilt so auch für alle anderen Bitbreiten, wie 32 Bit. Bei 8 Bit ist die Mitte der Wert 128 bzw. $80. Oder anders gesagt: Man zählt links der $00 abwärts, also $FF, $FE, $FD ... bis $80 und stellt sich vor, daß das negative Zahlen sind. In die andere Richtung wird normal hochgezählt $01, $02, $03 ... bis $7F. Das bedeutet natürlich auch, daß man bei diesem Zahlenstrahl NICHT 255 natürliche Zahlen sehen, sondern im Positiven nur noch bis zur Mitte zählen kann, also bis 127 ($7F), die anderen Zahlen (über $80) sind ja als negativ definiert.

    Wird nun A - B gerechnet, dann wird das Negativ-Flag immer dann gesetzt, wenn das Ergebnis > $7F ist ( (MI) wie Minus), also größer als die Mitte des Zahlenraums. Und hier dreht man sich dann im Kreis, weil das ja einfach nur bedeutet, das beim Ergebnis der Subtraktion das höchstwertige Bit gesetzt ist. Das Flag wird gelöscht, wenn A - B <= $7F ist ( (PL) wie plus).



    Zum Testen reichen normalerweise die beiden ersten Flags gut aus.

    Allerdings muß man sie teilweise kombinieren.


    Es sind nämlich


    A=B wenn das Zero-Flag gesetzt ist (EQ)


    A<>B wenn das Zero-Flag gelöscht ist (NE)


    A<B wenn das Carry-Flag gelöscht ist (CC)


    A>=B wenn das Carry-Flag gesetzt ist (CS)


    und


    A>B wenn das Carry Flag gesetzt (CS) UND das Zero-Flag gelöscht ist (NE)


    sowie


    A<=B wenn das Carry Flag gelöscht (CC) ODER das Zero-Flag gesetzt ist (EQ)



    Das "Testen" selbst funktioniert so wie in "Flaggen und Abzweige" für das Zero-Flag schon beschrieben.

    Man benutzt einen Sprungbefehl (Branch), der entweder auf die gewünschte Aktion springt oder, wenn man auf das gegenteilige Ergebnis testet, eine solche überspringt.


    Dabei können auch mehrere solche Sprünge hintereinander kommen, also mehrere "Tests auf Flags" gemacht werden, weil nämlich diese Sprünge selbst i.a. die Flags nicht(!) zusätzlich beeinflussen. Man testet daher mit dem zweiten Sprung die gleichen Flags aus dem gleichen Vergleich.

    (Das funktioniert aber nur, wenn zwischen den beiden Sprüngen nicht noch Anweisungen liegen, die die Flags neu setzen.)


    Vergleichen lassen sich neben Registern mit Registern, auch Werte aus Adressen mit Registern, oder fix vorgegebene Absolutwerte mit Registern.



    Die Schreibweise ist oft so, daß der Befehl selbst den ersten Operanden (also das A) vorgibt und man nur noch den Vergleichswert, dann meist eine Adresse oder einen Absolutwert, angibt.

    Bei RISC Prozessoren wird oft ein Register und der Vergleichswert benötigt, manchmal auch extra noch ein Ergebnisregister. Aufgrund ihrer Struktur sind dort Direktvergleiche mit RAM Inhalten i.a. nur nach einem vorherigen Ladebefehl durchführbar.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Es gibt beim 6502 genau drei Vergleichsbefehle - für jedes Register einen.


    Dementsprechend heißen die auch


    CPX - compare with X


    CPY - compare with Y


    CMP - compare - eben einfach nur compare, für das Hauptregister, den Akku



    CPY und CPY können mit Absolutwerten (#$27), Adressen ($5500) oder Zeropage-Adressen ($D0) vergleichen.

    CMP kann da wesentlich mehr, es gehen als Vergleichswerte zusätzlich Adressen indiziert ($5500,X oder $5500,Y), Zeropage X-indiziert (also z.B. $D0,X) und in der Zeropage indirekt indiziert und indiziert indirekt.

    (Alle Adressierungen mit Klammern und komischen Namen bekommen noch ein Extrakapitel.)



    Damit der Vergleich sinnvoll wird, muß man im Register einen Wert haben (das A). Der Subtrahend - das B - wird dann einfach hinter den Befehl geschrieben, also


    LDA #$35

    CMP $5700


    vergleicht den Wert $35 mit dem Inhalt der Adresse $5700


    es geht aber auch andersherum


    LDA $5700

    CMP #$35


    was die gleichen beiden Werte vergleicht, aber andersherum, weshalb das Ergebnis in den Flags ein anderes sein wird !

    (Die Richtung der Subtraktion ist ja genau andersherum, weil A und B jetzt vertauscht sind.)


    Ebenso funktioniert das für XR und YR.



    Üblicherweise hängt man dann zur Auswertung einen Branch Befehl dahinter, der je nach gesetztem Flag verzweigt.


    LDX #$FF

    DEX

    BNE (auf DEX)


    hatten wir schon öfter, hier ist der Vergleich i.P. implizit dabei, man läßt ihn also daher einfach weg, weil das Zero-Flag sowieso gesetzt wird, sobald durch das DEX im X-Register eine $00 erreicht wird (man spart sich hier den Vergleichsbefehl (CPX #$00) und damit 2 Byte an Speicherplatz sowie ein halbe Handvoll Taktzyklen Bearbeitungszeit).

    Man könnte aber genausogut auch schreiben


    LDX #$FF

    DEX

    CPX #$80

    BNE (nach DEX)


    Das XR wird also von $FF bis $80 heruntergezählt.

    Hier wäre bei XR = $80 Schluß mit der Schleife, weil CPX #$80 mit der $80 aus dem XR verglichen "EQ" ergibt, also A=B, weshalb das BNE keine Wirkung hätte (der Sprung wird ja nur bei Ungleichheit ausgeführt (NE) - also solange XR noch größer als $80 ist) und das Programm hinter dem BNE weiterginge.


    Es ist nun möglicherweis nicht so sinnvoll von $FF auf $80 mit Vergleichsbefehl (CPX) herunterzuzählen, aber andersherum kann das überaus sinnvoll sein, vor allem wenn man irgendwo "dazwischen" zählen will


    LDX #$28

    INX

    CPX #$50

    BNE (nach INX)



    Das geht nun auch mit allem möglichen, wie z.B. Adressen


    LDX $5500

    CPX $5501

    BNE (nach WEITER)

    LDA $D0

    STA $5501

    WEITER


    Hier wird der Wert von zwei aufeinanderfolgenden Speicherstellen verglichen. Sind sie ungleich (NE) wird direkt zum weiteren Programmablauf verzweigt. Trifft das nicht zu, sind sie also gleich, wird der Inhalt von $D0 nach $5501 kopiert.



    Und noch einer, nämlich ein Verkleinern eines Wertes B bis A > B erreicht ist


    LDA #$12

    CMP $5700

    BEQ (nach DEC $5700)

    BCS (nach WEITER)

    DEC $5700

    WEITER


    Was passiert ? Der Vergleich findet statt. Wenn A <> B, also $12 <> dem Wert in $5700, ist, wird das Zero-Flag NICHT gesetzt (sie sind ja nicht gleich), deshalb wird das BEQ nicht benutzt sondern es geht bei BCS weiter. In dem Moment wo A=B erreicht wird, springt BEQ direkt auf das DEC und BCS wird einfach ausgelassen, d.h. übersprungen. Das Carry-Flag ist nun nur gesetzt, wenn A >= B ist. Da der Fall A=B bereits vom vorherigen Sprung (BEQ) bearbeitet wird, springt der Sprungbefehl BCS also immer dann über das DEC hinweg, wenn der Fall A>B eintritt. In allen anderen Fällen, also wenn A<B ist, wird der Wert in $5700 erniedrigt.


    Die letzte Routine einfach mal eingeben (z.B. bei $5000)



    Mit

    >5700 15

    einen Vergleichswert eintragen

    das Programm laufen lassen (G5000) und mit

    M 5700 5701

    nachschauen, ob sich was tut, insbesondere, wenn das Programm mehrmals hintereinander aufgerufen wird; verschiedene Vergleichswerte probieren ($FF, $00, $80, $12, $15, $07)

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Nun wieder eine Varante des Scrollers - diesmal wird von $00 bis $27 hochgezählt. Der Abbruch der Schleife erfolgt, wenn der Vergleichsbefehl CPX#$27 den Zustand EQ findet, d.h. Gleichheit von X-Register und dem Absolutwert #$27.


    Der Vorteil liegt nun darin, daß die Vorstellung des Umkopierens im Bildschirmspeicher einfacher wird (na ja, zumindest anders) . Man benötigt nun auch keine Zwischenspeicherung für jedes Zeichen. Stattdessen wird einfach der Wert von einem Zeichen in die Adresse des vorhergehenden kopiert, wodurch dieses zwar überschrieben wird, was aber egal ist, da die Routine jetzt "in die richtige Richtung" läuft (zumindest für einen Scroll nach links).

    Natürlich muß das erste Zeichen besonders behandelt werden - es würde ja sonst direkt vom 2ten überschrieben und wäre weg, weshalb es ganz zu Beginn gerettet wird, auf besonderen Wunsch hin auf dem Stack ;) wohin es mit PHA gelangt und wo es am Ende mit PLA wieder abgeholt wird (wie ein Hund, den man vor der Einkaufshalle angebunden hat).



    Die Warteschleife muß wieder "anwesend" sein.

    ( L "SUBWAIT",8,6000 )



    Wenn man die Werte in der Warteschleife hoch genug einstellt, kann man schon dem einzelnen Kopieren der Buchstaben zusehen. Diesen Effekt verliert man auch bei schnellen Einstellungen nie wirklich. Es "ruckelt" sich die ganze Reihe immer stufenartig ein Zeichen nach links.


    Wenn man das nun "in schön" haben möchte, gibt es zwei Möglichkeiten

    a.) man wirft noch mehr Code auf das Problem

    b.) man macht das Ganze anders und viel schneller


    Hier mal Variante a.)


    Double-Buffering dürfte manchem aus dem Grafikbereich als Begriff bekannt sein. Es bedeutet, daß man die komplette Grafik zunächst in einem unsichtbaren Speicherbereich (einem doppelten Screenbuffer) aufbaut und erst wenn sie komplett gezeichnet ist, schnell auf den eigentlichen Bildschirm kopiert (oder dem Bildschirm sagt, wo der zweite Screenbuffer zu finden ist).


    Das geht mit Text auch.

    Deshalb kann man hier die ruckelige Verschiebeaktion zunächst in einen unsichtbaren Bereich schreiben ($5C00,X). Bis zu Zeile $5018 und bis auf diese Adressänderung ist das das gleiche Programm wie eben.

    Da man aber nun ja nichts mehr sieht, muß anschließend die komplette neue Zeile in einem Rutsch kopiert werden, wobei wieder einfach das X-Register bis Null runtergezählt wird.


    (Wer jetzt bemerkt, daß ja die Warteschleife beim Kopierteil gar nicht dabei ist, der hat natürlich völlig recht. Es ist aber auch mit Warteschleife so, daß der Ruckeleffekt nicht in der Form sichtbar wird, wie bei der Version ganz oben.)

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    Einmal editiert, zuletzt von ThoralfAsmussen ()

  • =6502=


    Und damits nicht nur Scroller gibt (keine Angst der kommt nochmal), hier mal ein anderes "VergleichsDemo"


    Das XR zählt wieder mal eine Bildschirmzeile durch. Da von oben gezählt wird, spart man den CPX Befehl ein.

    Dafür wird nun zusätzlich jedes Zeichen der ersten Bildschirmzeile in den Akku geladen und mit dem Wert "1A" verglichen.

    Was $1A ist kann man sich anschauen mit

    >0C00 1A


    Wenn der Vergleich, also die Subtraktion ( Akku - #$1A ), nun eine positive Zahl ergibt, das Zeichen im Akku also größer als #$1A war, passiert gar nichts, denn es wird per BPL ans Ende der Schleife gesprungen und das XR als Schleifencounter eins runtergezählt.


    Wenn aber der Akku Wert ein Zeichen von $00 bis $1A war, dann wird das Ergebnis der Subtraktion negativ und somit bei BPL nicht gesprungen. Dadurch kommt dann quasi automatisch der INC Befehl zum Einsatz und der erhöht nun den Wert in der Adresse womit der Akku geladen worden war um eins.



    Bitte in die erste Zeile einen Text mit Zahlen schreiben (z.B. die Einschaltmeldung dahin schieben).

    Dann mehrmals die Rotuine starten.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Eine wichtige Sache, die sich mit Vergleichen anstellen läßt, ist eine sogenannte Mehrfachauswahl.


    Dies bedeutet, daß man eine Variable auf ihre möglichen Zustände abfragt und je nachdem was gerade aktuell ist, anders darauf reagiert. Im Prinzip entspricht das dem switch Konstrukt in C bzw. einem case-of in Pascal oder eben einfach einer mehrfachen IF Abfrage in BASIC.


    Das Prinzip dazu geht so, daß man den Wert einmal lädt und dann gegen verschiedene Vergleichswerte testet. Dabei bemüht man sich die Flags nicht zu zerstören, so daß man immer weitere Vergleiche anfügen kann.

    Wenn die Auswahl klein genug ist, kann man sich darin auch komplett mit relativen Sprüngen bewegen, ansonsten benutzt man für die Aktionen, die nach dem Test folgen am besten JMPs, oder man kombiniert beides. Die Bewegung zum nächsten Vergleich geschieht eigentlich immer mit bedingten Sprüngen. Sie sind die entscheidenden Stellen, wo die Auswahl getroffen wird.


    Das Thema ist hier schonmal kurz angesprochen worden.

    Für mich wäre interessant zu wissen, wie man nur unter einer bestimmten Bedingung einen JMP ausführen kann. Quasi wie IF x=1 then goto 10..


    Das Beispiel unten zeigt nun eigentlich folgendes


    IF x=1 THEN akku=$51 ELSE

    . IF x=2 THEN akku=$5A ELSE

    . . IF x=3 THEN akku=$D8

    OTHERWISE akku=$20


    man könnte aber natürlich statt der LDA Anweisungen z.B. Subroutinen mit JSR aufrufen oder direkt aus der Mehrfachauswahl mit JMP heraus springen (nicht nur weiter nach $5022 wie im Beispiel). Letzteres entspräche dem gewünschten IF x=? THEN GOTO , nur halt hier mit mehreren Möglichkeiten.




    ( Warteschleife bei $6000 ist erforderlich)


    Was macht es nun ?


    Ohne zusätzliche Vorbereitung, nicht viel. Es muß nämlich noch per Hand nach $5100 bis $510F eine Liste mit den Vergleichswerten geschrieben werden, z.B.


    >5100 00 01 02 03 02 03 01 02

    >510F 01 00 03 02 03 02 01 03


    Diese werden einzeln eingelesen (von hinten nach vorn) und nachgeschaut, welcher Wert gerade aktuell ist. Für $01, $02, $03 wird noch direkt in der Mehrfachauswahl der Akku mit $51,$5A bzw. $D8 geladen. Für alle anderen Werte bekommt der Akku eine $20 (SPACE) mit auf den Weg.

    Anschließend wird der jeweils ausgewählte Wert auf den Bildschirm geschrieben - ein paarmal (240), damit man's auch gut sieht.


    Wichtig! Wie man das gestaltet ist einem selbst überlassen - man könnte z.B. (s.o.) auch mit den JMP Anweisungen an eine völlig andere Stelle springen, da etwas Kompliziertes machen und später wieder in die Auswahl oder dahinter zurückkehren.


    Wichtig! Die bedingten Sprünge springen nicht sehr weit. Nur 128 Bytes zurück oder 127 Bytes nach vorn sind möglich, was aber oft ausreicht.



    So eine Struktur eigenet sich gut für alle Variablenabfragen, bei denen mehrere Werte auftreten können. Genauso paßt es aber auch, wenn man bestimmte Routinen in bestimmter Reihenfolge regelmäßig anspringen will (Polling). Prinzipiell könnte man auch Abfragen für Eingaben, etwa Keyboards oder Joysticks, so bauen.



    Was zeigt es noch?: Es benutzt im Prinzip eine erste Tabelle. Die Werte bei $5100 - $510F können quasi beliebig verändert werden, solange man im Rahmen der Abfrage ($01-$03) bleibt.

    Einfach mal damit spielen und die Anordnung ändern.


    Wer mag schreibt noch ein JMP $5000 ganz ans Ende; oder läßt das X-Register über alle 255 möglichen Werte laufen und erweitert die Tabelle auf eben diese Größe.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

    2 Mal editiert, zuletzt von ThoralfAsmussen ()

  • =6502=


    Man kann auch ganze Gruppen von Abfragen/Tests zusammenfassen. Dazu "befragt" man die Vergleichswerte öfters hinereinander, ob sie bestimmte Werte haben und verläßt die Abfrage nicht mit einzelnen JMP Anweisungen, sondern nur noch für jeden "Treffer" mittels der Branch Befehle.


    Die Struktur die nachgebildet wird ist i.P. sowas hier


    IF ( A=0 ) OR ( A=12 ) OR ( A=56 ) OR ( A=232 ) THEN ... DO something


    bzw.


    CASE A

    . OF 0,12,56,232 : DO something


    Damit das Programm was tut, gibt es diesmal eine Routine, die den ganzen sichtbaren Bildschirm als Datenquelle benutzt - und jedes Zeichen im Bildschirmspeicher einmal lädt. Dieser Wert wird dann an die Subroutine bei $5100 übergeben und dort erfolgt der Vergleich.

    Bei fünf bestimmten Werten ($01, $05, $09, $0F, $15) wird hier der Wert um $80 erhöht, alle anderen Werte bleiben unverändert.


    Warum $80 ? Bei den Commodores hat jedes Zeichen einen bestimmten Wert. Der Wert + 128 ergibt auf dem Bildschirm das gleiche Zeichen, aber invertiert dargestellt.


    Dabei nutzt man dann auch gleich mal einen "richtigen" Rechenbefehl - man beachte: alle bisherigen Beispiel kamen gut ohne sowas aus ! - und zwar den Befehl fürs Addieren: ADC


    (das CLC davor löscht das Carry-Flag, was beim 6502 nötig ist, da ein gesetztes Carry-Flag sonst ebenfalls noch dazu addiert würde, was eine "Eigenheit" des Rechenwerks ist [die auch einen gewissen Sinn hat; man kann damit in einem nachfolgenden zweiten Addierbefehl einen entstandenen Übertrag automatisch mit in die nächsthöhere Stelle nehmen] ).





    ( die Warteschleife SUBWAIT bei $6000 wird auch wieder benötigt )


    An sich ist das Programm aber aufgrund seiner immens vielen Sprünge ja schon auch selbst eine Warteschleife ! Hier wird ja für jedes Zeichen einzeln ein JSR bemüht. Sowas sollt man normalerweise tunlichst vermeiden ! Aber es erlaubt die Verlgeichsroutine schön extra zu stellen.





    Frage 1 - was müßte man tun/ändern, um alle Vokale rot einzufärben ?


    (Hinweis: bei Adresse $0800 - was bei ähnlichen Geräten auch eine leicht andere Adresse sein kann, bitte vorher irgendwo nachlesen - beginnt der "Farbspeicher". Dort liegt für jedes Zeichen auf dem Bildschirm ein Farbwert in Form einer Zahl. Die Anordnung der Zeichen ist dabei die gleiche wie im Bildschirmspeicher selbst, nur die Adresse ist eine andere, d.h. das Zeichen links-oben liegt bei $0C00 im Bildschirmspeicher (sein Zeichenwert) und bei $0800 im Farbspeicher (sein Farbwert). Dagegen das letzte Zeichen der ersten Zeile bei $0C27 (Zeichen) und $0827 (Farbe) usw. )


    Frage 2 - Warum wird das X-Register hier nur mit #$C8 geladen ?

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries