8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - 7-Segment-Anzeigen - Assembler-Programme

Bisherige Artikel dieser Serie - hier könnt ihr nochmal alle Grundlagen nachlesen, falls ihr jetzt erst einsteigt: Aufbauend auf dem Assembler-Programm für die LED-Ausgabe entwerfen wir zuerst drei Programme, um statt einer LED die Segmente der 7-Segment-Anzeige durchlaufen zu lassen, denn im Grunde ist eine 7-Segment-Anzeige nichts anderes als 8 in einem Muster angeordnete LEDs.

Durchlauftest: Test der Adressierung

Aus ; K.I.T.T. Lauflicht auf die 8-LED-Ausgabe oben rechts auf dem Breadboard ; last edit: OK, 2020-06-20 org $8000 ; Programmstartadresse (von der CPU aus gesehen) ; --- Konstanten: IO-Adressen --- LED: equ $7000 ; 8-LED-Ausgabe, hört aber auf alles von $7000 bis $7fff start: lda #$01 ; rechts anfangen sta LED scroll: lda #$07 ; 7x nach links left: rol LED dec A bne left lda #$07 ; 7x nach rechts right: ror LED dec A bne right jmp scroll; und wieder nach links wird ; 3-fach Sieben-Segment-Anzeige Testprogramm ; last edit: OK, 2020-06-21 org $8000 ; Programmstartadresse (von der CPU aus gesehen) ; --- Konstanten: IO-Adressen --- LED: equ $7000 ; 8-LED-Ausgabe, hört aber auf alles von $7000 bis $7fff SEGL: equ $7001 ; linke 7-Segment-Anzeige SEGM: equ $7002 ; mittlere 7-Segment-Anzeige SEGR: equ $7004 ; rechte 7-Segment-Anzeige start: lda #$01 ; rechts anfangen sta SEGL scroll: lda #$07 ; 7x nach links left: rol SEGL dec A bne left jmp start ; wieder von vorne Dabei habe nur nur die drei neuen Ausgabegerät SEGL (für linke 7-Segment-Anzeige), SEGM (mittig) und SEGR (rechts) mit ihren Adressen definiert und spreche diese ganz ähnlich wie schon bei der LED-Ausgabe an. Nur lasse ich die LED nicht hin und herlaufen, sondern beginne immer wieder ganz rechts und lasse sie nur nach links durchlaufen.

Das ergibt im Ergebnis jeweils ein ausgeschaltetes Segment, denn ich benutze ja hier die B-Version (gemeinsame Anode) der Segmentanzeige. Bei der A-Version würde genau ein Segment angeschaltet sein und der Rest aus.

Der Programmplatz für das linke Segment ist 1 (0 bleibt für unsere LED-Ausgabe). Auf Programmplatz 2 folgt das mittlere Segment und auf Platz 3 das rechte. Hier steht anstatt SEGL einfach nur die jeweilige Adresse, der Rest des Programmes bleibt gleich.

Ein Test sieht vielversprechend aus. Es läuft jeweils nur das angesprochene Segment und die beiden anderen halten sich brav raus, wenn sie nicht gemeint sind:



Mustertest: einmal durchzählen bitte

Als nächstes möchte ich die Muster für die Ziffern Null bis Neun (und vielleicht noch ein paar Buchstaben) auf allen drei Anzeigen testen. Ob diese auch korrekt angezeigt werden. Ich gebe auf allen drei Segmenten jeweils die gleiche Ziffer aus, so werden Ausreißer z. B. Falschverkabelung am schnellsten deutlich.

Dazu muss ich zuerst einmal definieren, welche der Segmente ausgeschaltet (also bei meinem B-Modell auf high) sein sollen. Das habe ich zuerst für die ersten 10 Ziffern per Hand gemacht, aber das wurde mir dann doch irgendwie zu mühselig - auch weil ich schlussendlich auch alle Buchstaben von A bis Z und ein paar Sonderzeichen auf den 7-Segment-Displays anzeigen lassen will.

Darum habe ich mir ein kleines Hilfsprogramm geschrieben:



Ich hatte die Segmente ja so angeschlossen, dass die Kabel möglichst kurz und die Anschlüsse auf dem Breadboard möglichst logisch sind, also den linkesten Q-Ausgang des 1. 374er an den linkesten Eingang der Segmentanzeige, der zweitlinkeste Q an den zweitlinkesten Seg-Pin in der oberen Reihe und so weiter und so fort.

Demnach ist die Beschaltung nicht Datenbit 0 an A, DB 1 an 1 usw. sondern Datenbit 1 an B, Datenbit 2 an A Datenbit 3 an F etc. Um dem Rechnung zu tragen, kann ich Programm die Wertigkeiten (in dezimal) und damit die Verkabelung eintragen. Dann klicke ich ein Segment an, um es an bzw. wieder auszuschalten und berechne den Wert für gemeinsame Anoder bzw. gemeinsame Kathode, je nachdem was ausgewählt ist, in dezimal und hexadezimal. Mit add kann ich den aktuellen Wert der Definitionsliste hinzufügen und die dann ins Assembler-Programm eintragen. Jeder diese Einträge repräsentiert das Segment-Muster für die jeweilige Ziffer.

Bei $88 für 0 z. B. sind also die Segment $80 (128) und $8 ausgeschaltet, was eine 0 ergibt. Bei der Ziffer 1 bleibt alles außer 1 und 64 stehen. Macht 255 - 64 = 190 = $BE.

Dann brauchen wir nur noch ein Programm, diese Musterdefinitionen anzuzeigen. Das soll erstmal von 0 bis 9 zählen und die jeweiligen Ziffern auf alle drei Segmentanzeigen ausgeben.

; 3-fach Sieben-Segment-Anzeige Testprogramm ; last edit: OK, 2020-07-03 CHIP W65C02S ; (default) ORG $8000 ; Programmstartadresse (von der CPU aus gesehen) ; --- Konstanten: IO-Adressen --- LED: equ $7000 ; 8-LED-Ausgabe, hört aber auf alles von $7000 bis $7fff SEGL: equ $7001 ; linke 7-Segment-Anzeige SEGM: equ $7002 ; mittlere 7-Segment-Anzeige SEGR: equ $7004 ; rechte 7-Segment-Anzeige ; 000 ... JJJ zählen start: ldx #0 ; for x = 0 ... for(x=0;x<16;x++) {... ; mit der ersten Ziffer (0) anfangen next: lda SEG, x ; Ziffer aus SEG+x laden sta SEGL ; und in allen 3 Anzeigen anzeigen sta SEGM sta SEGR inx ; nächste Ziffer cpx #21 ; ... to 20 ; z. Zt. haben wir nur 16 Ziffern definiert, danach ist Schluss bne next bra start ; und nochmal das Ganze ;RADIX H ; ab hier alles Hex, zurück mit RADIX D - ; Befehl DB geht dann allerdings nicht mehr, darum Byte ; 2 ; __ ; 4 | | 1 ; -- 8 ; 10 |__| 40 .=80 ; ; 20 SEG: BYTE $88, $BE, $C4, $94, $B2, $91, $81, $BC, $80, $90 ; 0 bis 9 = Ziffern 0 bis 9 BYTE $A0, $83, $C9, $86, $C1, $E1, $89, $A2, $BE, $9E ; 11 bis 20 = Ziffern A bis J ; außerdem möglich: WORD (2 Bytes), LONG (4 Bytes), FLOAT (4 Bytes), DOUBLE (8 Bytes) Unsere Segmentdefinitionen finden wir ganz am Ende hinter dem Code wieder. Ich habe noch ein bisschen weiter gemacht und auch die erste 10 Buchstaben definiert. Mein Zeichensatz besteht also aus den Plätzen 0 bis 9 für die Ziffern 0 bis 9 und dann folgt das Alphabet ab 11. A wird also 11 werden und Z 26 + 10 = 36.

Und auch im Code wird diese Byte-Definition hinter den Code gesetzt. Ich habe das mal blau markiert:



Das Programm geht bis $80 (BRA) $EB ("start"). Direkt darauf folgt unsere Definition mit 20 Bytes an Segment-Mustern. Durch die Marke SEG: merkt sich der Assembler unter SEG diese Muster-Startadresse und wir können sie einfach mit etwa SEG+0 für die Null oder SEG+9 für die Neun abrufen.

Das machen wir uns oben zunutze, indem wir die indexierte Adressierung bei lda SEG, x ; Ziffer aus SEG+x laden benutzen. Indexierte Adressierung hieß ja: "hole den Wert, der in der Adresse SEG plus X gespeichert ist und lade ihn in den Akkumulator. Vielleicht möchte jemand an dieser Stelle noch einmal die Adressierungsarten nachlesen. Wenn wir X jetzt von 0 bis 20 hochzählen, ist es ein Klacks, die ganzen Definitionen der Reihe nach abzuklappern.

Und genau das tun wir. mit ldx #0 wird bei 0 begonnen und wenn X dann 21 wird, sorgt der Befehl cpx #21 für ein gesetze Zero-Bit und der bne next wird nicht mehr ausgeführt. Es geht also bei 21 mit dem nächsten Assembler-Befehl weiter, dem bra start. Und der startet die Zählerei wieder von Neuem bei Null.

Mit der LDA addr, X-Anweisung kann man also wunderbar For...Next-Schleifen realisieren, die in vielen höheren Programmiersprachen gängig sind.

Wer sich die ganze Dollarzeichen vor den Definitionswerten sparen will, der kann die Assembler-Anweisung RADIX H benutzen. Ab der Zeile, in der er angegeben ist, werden dadurch alle Zahlen als hexadezimal angesehen. Das $ ist dann nicht mehr nötig. Will man wieder zurück zur Dezimalinterpretation, benutzt man RADIX D in der Zeile, ab der wieder die Norm gelten soll. Da die Liste aber jetzt aus meinem 7-Segment-Rechner kommt und die die Dollarzeichen automatisch einfügt, kann ich auf das RADIX H nun verzichten.

Ganz am Anfang des Sourcecodes steht noch eine neue Direktive: CHIP W65C02S. Die besagt, dass das Programm für den WDC W65C02S Chip geschrieben ist, den wir ja benutzen und schaltet dann auch den erweiterten Befehlssatz scharf, damit wir z. B. auch den BRA-Befehl benutzen können. Allerdings ist das Standardeinstellung, wenn wir den WDC02AS.exe-Assembler benutzen und müsste gar nicht angegeben werden.

Ich habe außerdem ein Video gemacht, in dem ich die Bedienung der Programmier-Assistenten zeige und ein paar zusätzlichen Worte zur Programmierung verliere:



Mustertest am "lebenden" Objekt

Den nächste Schritt ist natürlich, das Mustertestprogramm am "lebenden" Objekt zu testen, also auf unserem Breadboard-Computer laufen zu lassen. Was wäre besser als ein Video geeignet, das Ergebnis zu zeigen?



So gut sieht das nicht aus. Da habe ich mich wohl zu früh gefreut und angenommen, wenn die Anzeigen einzeln die Segmente korrekt durchschalten, dass dann auch alles funktionieren wird. Mit der korrekten Anzeige der Ziffern klappt es nämlich nicht jedesmal.

Vielleicht liegen die Gatterlaufzeiten bei 2-fachen Invertierungsvorgang (gleich 2 Gatterdurchläufe) mit den Chinesen-Chips (30 ns Laufzeit pro Einzelgatter) immer noch zuweit auseinander? Also messe ich mit dem Oszilloskop nach und sehe, wie lahm die Chinesen-Chips sind und dass sie es nicht besser, sondern schlechter machen, die Diskrepanz der Gatterlaufzeiten also erhöht statt erniedrigt.

Also versuche ich es dann wieder mit den TI-Chips (5 ns Laufzeit pro Einzelgatter) und 2 Durchläufen, was im Einzelsegmenttest ja gut aussah, aber auch hier versagt.

Was es eigentlich garantiert tun müsste, wären 4 Gatterdurchläufe durch TI-Chips, dann mit zusammen 20 ns gegenüber den 20 ns für die A12....A14-Durchläufe. Also probiere ich auch das für erstmal eine Adressleitung, nämlich A1 aus und merke völlig überrascht, dass es auch das nicht tut.

Hierzu habe ich auch schon eine Idee, nämlich eine andere Hardware-Adressierung mit 8-bit-Komparatoren (74HC688) (evtl. zusammen mit einem 3-to-8-Line-Decoder (74HC138)), die flexibler ist und mit der ich auch Clock und RW-Leitung der CPU mit einbeziehen kann.

Im nächsten Teil wird es also um die Verbesserung der Hardware-Adressierung gehen.