Eine Binärzahl ausgeben
Dieses Mal 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 Hex-Zahl $c9 binär aufzuschreiben. Seit unseren Übungen bei den Zahlensystemen wissen wir, dass $c dezimal 12 ist (A=10, B=11, C=12,…). Binär wäre es %1100 (1*2^3 + 1*2^2 + 0*2^1 + 0*2^0) und $9 = %1001 ist, 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.
Pseudo-Code: $c9 binär ausgeben ------------------------------- nimm die 1. Ziffer: $c wenn 0, dann sta $0400,'0' sta $0401,'0' sta $0402,'0' sta $0403,'0' jmp ziffer2 wenn 1, dann sta $0400,'0' sta $0401,'0' sta $0402,'0' sta $0403,'1' jmp ziffer2 ... wenn $e, dann sta $0400,'1' sta $0401,'1' sta $0402,'1' sta $0403,'0' jmp ziffer2 wenn $f, dann sta $0400,'1' sta $0401,'1' sta $0402,'1' sta $0403,'1' jmp ziffer2 ziffer2: das Gleiche noch mal für die 2. Ziffer <ENDE>
Dies 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 dennoch 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 😉 :
Ausgabe von 10 Zeichen auf dem BS: ---------------------------------- lda #$01 ;2 Byte / 2 TZ ldx #$0a ;2 Byte / 2 TZ loop sta $03ff,x ;3 Byte / 5 TZ * 10 dex ;1 Byte / 2 TZ * 10 bne loop ;2 Byte / 3 TZ * 9 | für die Sprünge ; + 2 TZ | beim letzten Mal KEIN Sprung ----------------------------------- 10 Byte /103 TZ
Beim BNE gehen wir davon aus, dass dieser nicht über die Page-Grenze springen muss.
Jetzt OHNE Schleife: ------------------------- lda #$01 ;2 Byte / 2 TZ sta $0400 ;3 Byte / 4 TZ sta $0401 ;3 Byte / 4 TZ sta $0402 ;3 Byte / 4 TZ sta $0403 ;3 Byte / 4 TZ sta $0404 ;3 Byte / 4 TZ sta $0405 ;3 Byte / 4 TZ sta $0406 ;3 Byte / 4 TZ sta $0407 ;3 Byte / 4 TZ sta $0408 ;3 Byte / 4 TZ sta $0409 ;3 Byte / 4 TZ ------------------------- 32 Byte /42 TZ
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 ♠-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:
lda #$41 sta $0400 sta $0401 ... sta $07fe sta $07ff rts
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 lauter ♠-Zeichen 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 verbraucht).
Da ist unsere bisherige Version doch schon schicker, oder?
Aber noch besser finde ich folgenden Ansatz:
;*** Variablen SCREENRAM = $0400 ;Start des Bildschirmspeichers CHAR = $41 ;Pik-Zeichen für die Ausgabe COLORRAM = $d800 ;Start des Farb-RAMs COLORNO = $00 ;Schwarz ($00) als Zeichenfarbe ;*** Startadresse & BASIC-Zeile *=$0801 !byte $0c,$08,$e2,$07,$9e,$20,$32,$30,$36,$32,$00,$00,$00 ;*** Start des Assemblerprogrammes ldy #$00 ;mit 0 beginnen, für 256 Durchläufe ldx #COLORNO ;Farbe ins X-Register loop lda #CHAR ;Zeichen in den Akku sta SCREENRAM,y ;Akku in die 1. Page des BS-Speichers sta SCREENRAM+$0100,y ;Akku in die 2. Page des BS-Speichers sta SCREENRAM+$0200,y ;Akku in die 3. Page des BS-Speichers sta SCREENRAM+$0300,y ;Akku in die 4. Page des BS-Speichers txa ;X-Reg. in den Akku (ist schneller als lda) sta COLORRAM,y ;Farbe in 1. Page des Farb-RAMs sta COLORRAM+$0100,y ;Farbe in 2. Page des Farb-RAMs sta COLORRAM+$0200,y ;Farbe in 3. Page des Farb-RAMs sta COLORRAM+$0300,y ;Farbe in 4. Page des Farb-RAMs dey ;Y-Register verringern bne loop ;solange nicht 0 wieder zum loop: springen rts
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) eigentlich die beste Lösung darstellt. Sie lässt sich gut pflegen, ist überschaubar und bietet ein vernünftiges Verhältnis zwischen Speicherbedarf und Ausführungszeit.
Machen wir nun aber lieber 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.
Pseudo-Code: $c9 binär ausgeben ------------------------------- nimm die 1. Ziffer: $c suche den Text für: $c unter 'bintexte:' gib den gefundenen Text aus nimm die 2. Ziffer: $9 suche den Text für: $9 'bintexte:' gib den gefundenen Text aus <ENDE> bintexte "0000" "0001" "0010" ... "1110" "1111"
Schon besser, aber wir benötigen allein 64 Byte für die Texte zu den jeweiligen 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.
Pseudocode: $c9 binär ausgeben ------------------------------ n = 0 start prüfe ob Bit-n in $c9 auf 0 oder 1 gesetzt ist wenn 1 gehe zum Label eins sonst Zeichen '0' laden und zum Label ausgabe springen eins zeichen '1' laden ausgabe geladenes Zeichen ausgeben n=n+1 wenn kleiner 8 zu start springen <ENDE>
Ich denke diese Idee ist es wert, einmal umgesetzt zu werden.
SCREEN = $0400 ;Start des Bildschirmspeichers OUTPUTVAL = $c9 ;Diese Zahl wird ausgegeben ;*** Startadresse BASIC-Zeile *=$0801 !byte $0c,$08,$e2,$07,$9e,$20,$32,$30,$36,$32,$00,$00,$00 ;*** Start des Assemblerprogrammes lda #%10000000 ;Bit das geprüft wird ldy #$00 ;Position auf dem BS binoutloop
Kurz gefasst, in OUTPUTVAL hinterlegen wir unsere Zahl, die ausgegeben werden soll. 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 möchten, steht hier übrigens, wie wir gleich sehen, beim Label number am Ende des Programms.
bit number ;bitweises UND von Akku & Adresse
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 Vergleich 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.
Akku......: %10000000 $80 number....: %11001001 $c9 --------- BIT number: %10000000 | N=1, V=1, da Bit-7 & 6 bei $c9 = 1 sind | und Z=0, da das Ergbnis <> 0 ist
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.
bne one ;Wenn es eine 1 ist, dann zum Label one springen ldx #"0" ;sonst, das Zeichen "0" nach X laden jmp out ;und zur Ausgabe springen one ;Ziel Label, falls es eine 1 ist ldx #"1" ;das Zeichen "1" ins X-Register laden out ;BS-Ausgabe
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 eine Eins ins X-Register zu laden und anschließend direkt zur Ausgabe „weiter zulaufen“. Ist Z=1, dann haben wir eine Null gefunden und springen nicht! Wir laden dann das Zeichen für die Null ins X-Register und springen anschließend über den Abschnitt one zum Label out, um mit der Ausgabe zu beginnen.
pha ;Akku auf dem Stack merken
PHA: PusH Akku on stack (Akku auf den Stack schieben)
PHA implizit ($48, 1B, 3T, <keine>)
Mit PHA legen wir den Akku-Inhalt auf dem Stack ab. 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.
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.
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 für den Stack.
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.
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:
txa ;X (Zeichen "0" / "1") in den Akku kopieren sta SCREEN,Y ;Akku auf dem BS ausgeben
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 Kopieren erreichen wir bekanntlich durch TXA. Direkt danach, geben wir das Zeichen wie gewohnt auf dem BS aus.
pla ;gemerkten Akku vom Stack holen
PLA: PuLl Akku from stack (Akku vom Stack holen)
PLA implizit ($68, 1B, 4T, NZ)
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 und zeigt somit also auf $ee. Danach wird das Byte von dieser Speicherstelle (unsere $80) in den Akku kopiert. Im Gegensatz zum PHA beeinflusst der PLA auch die N– und Z-Flaggen.
Da nur kopiert wird, bleibt die $80 natürlich auch auf dem Stack liegen.
iny ;Y für nächste BS-Position (nächstes Bit) erhöhen
Jetzt das Y-Register um eins erhöhen, damit wir zur nächsten BS-Position kommen.
lsr ;Bit im Akku nach rechts verschieben
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.
Nur zur Info:
Einige Assembler nutzen die Schreibweise LSR A. Das C64 Studio versteht beides, der Turbo Assembler wandelt bei der Eingabe LSR automatisch in LSR A um und ACME kennt nur LSR.
Schauen wir uns nun mal an, was während der acht Durchläufe in unserem Programm beim LSR passiert.
Akku %10000000 C LSR -> (0) -> %01000000 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00100000 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00010000 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00001000 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00000100 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00000010 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00000001 (0) -> C=0; N=0; Z=0 LSR -> (0) -> %00000000 (1) -> C=1; N=0; Z=1
Uns interessiert hier das Zero-Flag. Wir werden unsere Schleife solange laufen lassen, bis im Akku eine Null steht.
bne binoutloop ;solange Akku <> 0, nächstes Bit prüfen rts ;sonst, zurück zum BASIC number !byte OUTPUTVAL ;hier steht die Zahl, die der BIT-Befehl überprüft
Mit dem BNE springen wir, wie eben erwähnt, solange zum Label binoutloop, bis der Inhalt des Akkus Null ist.
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 RTS 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 $a7e9 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:
SCREEN = $0400 ;Start des Bildschirmspeichers OUTPUTVAL = $c9 ;Diese Zahl wird ausgegeben ;*** Startadresse BASIC-Zeile *=$0801 !byte $0c,$08,$e2,$07,$9e,$20,$32,$30,$36,$32,$00,$00,$00 ;*** Start des Assemblerprogrammes lda #%10000000 ;Bit das geprüft wird ldy #$00 ;Position auf dem BS binoutloop bit number ;bitweises UND von Akku & Adresse bne one ;Wenn es eine 1 ist, ;dann zum Label one: springen ldx #"0" ;sonst, das Zeichen "0" nach X laden jmp out ;und zur Ausgabe springen one ;Ziel Label, falls es eine 1 ist ldx #"1" ;Zeichen "1" ins X-Register out ;BS-Ausgabe pha ;Akku auf dem Stack merken txa ;X (Zeichen "0" oder "1") in den Akku sta SCREEN,y ;Akku auf dem BS ausgeben pla ;gemerkten Akku vom Stack holen iny ;Y für nächste BS-Position erhöhen lsr ;Bit im Akku nach rechts verschieben bne binoutloop ;solange Akku <> 0, nächstes Bit prüfen rts ;sonst, zurück zum Aufrufer (BASIC) number !byte OUTPUTVAL ;hier steht die Zahl, die bit überprüft
Oben links hat sich nun unsere binäre Ausgabe von $c9 versteckt.
Gebt ruhig mal andere Zahlen hinter OUTPUTVAL ein, um die Ausgabe zu testen.
Weitere Adressierungsarten
Kommen wir wieder zu den weiteren Adressierungsarten und Befehlsvarianten.
bit $fb
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 Befehle, um das Statusregister auf dem Stack zu speichern und zurückzuholen.
php
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
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 an Hand des Bytes auf dem Stack setzt.
Der LSR-Befehl kennt noch die folgenden Adressierungsarten:
lsr $0821
LSR absolut ($4E, 3B, 6T, NZC)
Verschiebt die Bits des Bytes an der absoluten Adresse nach rechts.
lsr $0821,x
LSR absolut X-indiziert ($5E, 3B, 7T, NZC)
An der angegebenen Adresse + Inhalt des X-Registers, die Bits des dort zu findenden Bytes nach rechts verschieben.
lsr $fb
LSR Zero-Page ($46, 2B, 5T, NZC)
Verschiebt an der angegebenen Zero-Page-Adresse die Bits des dortigen Bytes nach rechts.
lsr $fb,x
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
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.
Mir ist aufgefallen, daß ja das LSB (Bit hier) beim LSR immer im Carry landed. Dementsprechend kann man als abbruchbedingung für die Schleife auch nehmen, wenn das Carry Gesetzt ist. Oder besser ausgedrückt, so lange Das Carry Bit Clear ist (BCC) wird geloopt.
Das würde dann so aussehen:
lsr ;Bit im Akku nach rechts verschieben
bcc binoutloop ;solange Akku 0, nächstes Bit prüfen
Es ist immer interessant zu sehen, wie die Befehle auf die verschiedenen Register verwenden. Hier macht das keinen Unterschied, ein Weg, der geht ist ja genug. Aber man kann eben hier verschiedene Wege erkennen, wie man ohne einen Zähler zum Ziel kommt. (Nein, so clever war ich nicht, ich habe einen Zähler verwendet)…
Das ist ja echt der Hammer:
inc out:+1
Wir incrementieren das LSB der ScreenAddresse die im assemblierten Code ja als echte Addresse da steht. (8D 00 40 im ersten run, und dann wird die 00 zu 01 usw.) Funktioniert hier gut.
Schöner Code 🙂
Hallo Jörn,
mir geht es ähnlich wie dir und Alexander Reyer. Auch ich hatte zwar zwischen Grundschule und Mittelstufe mind. eines oder mehr C64 Assembler Bücher und wollte es immer lernen, aber irgendwie war mir das damals zu kompliziert und ich hab nie kapiert, für was man das alles braucht. (Dann kam der Amiga, ich hab mir DevPack Assembler gekauft und es war noch komplizierter und ich hab nur noch gezockt 😀 )
Irgendwann vor Kurzem hat es mich dann wieder gepackt und ich will jetzt endlich meinen Kindheitstraum verwirklichen – ein C64 Spiel erstellen 🙂
Dein Kurs ist klasse, weil du das meiste sofort an einem Beispiel erklärst und nicht erst alle Befehle durch nudelst wie die Bücher früher.
Allerdings versteh ich bei diesem Beispiel nicht, warum du den Umweg über das number Label gehst. Ich habe einfach bit %10000000 geschrieben und es funktioniert anscheinend genauso?!
Und dann noch was anderes, da mir am bisherigen Kurs völlig entgangen ist, dass man X auch mit Zeichen laden kann. Das erwähnst du hier beiläufig zum ersten mal glaube ich.
Ich habe ein Hello World PRG geschrieben und mir alle Zeichen einzeln aus der PETSCII Tabelle raus gesucht, in den Akku geladen und von dort aus auf den BS Speicher kopiert.
Jetzt hab ich es mit der “Lade ein Zeichen in X” – Methode probiert, es kommt aber ein ganz anderes raus.
Also das geht:
lda #$08 ; H
sta $05ed
aber wenn ich das versuche, kommt ein Grafikzeichen raus:
ldx #”H”
txa
sta $05ed
Woran könnte das liegen?
Danke und viele Grüße, Gerrit
Hallo Gerrit,
spielst du eigentlich Lotto? 😉
Dein BIT %10000000 funktioniert nur, weil du damit auf die Speicherstelle 128 bzw. $80 zugreifst (BIT kennt nur absolute und Zero-Page Adressierung!). Dort steht nach einem Reset $C9! Also genau der Wert, den ich hier als Beispiel verwende.
Du erwähnst leider nicht, welchen Assembler du nutzt, ich gehe mal vom C64 Studio aus.
Du mischt hier zwei Welten.
Wenn du direkt in den Bildschirmspeicher schreibst, dann brauchst du die Codes aus dem Char-ROM. Daher geht lda #$08, denn im Char-ROM steht hinter der acht, das H. Mit ldx #”H” lädt der Assembler aber die Nummer aus der PETSCII-Tabelle. Nach dem assemblieren steht da dann ldx #$48 bzw. ldx #72. Schaust du jetzt wieder ins Char-ROM findest du dort, hinter der Nummer 72, den senkrechten Strich. Mit 0 und 1 klappt das hier nur, weil die im PETSCII und Char-ROM an den gleichen Stellen stehen.
Es gibt andere Assembler, die z. B. zwischen “” und ” unterscheiden. Im ersten Fall würde das Zeichen aus dem PETSCII, im zweiten aus dem Char-ROM geladen. Beim C64 Studio ist dies aber nicht der Fall. PETSCII-Zeichen kannst du über jsr $ffd2 ausgeben, das wird später auch mal beschrieben.
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
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
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<—————————–
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
Sehr cool! Danke fuer die Antwort.
Arthur hat das von Seb gemeldete Problem
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:
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.
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.
Super, viele Dank für die schnelle Antwort. Mit lda #’a’ funktioniert es einwandfrei. Jetzt kann ich mit den Tutorial weiterfahren. 🙂
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.