8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - Adressierung und Ausgabe auf LEDs

Bisherige Artikel dieser Serie - hier könnt ihr nochmal alle Grundlagen nachlesen, falls ihr jetzt erst einsteigt: Im Artikel Erstes Programm in Maschinensprache der Serie zum 8-Bit-Breadboard-Computer hatten wir ja das RAM getestet; also zuerst die Werte 1, 2, 4 ... 128 in die Speicherzellen 0x1000 bis 0x1007 hineingeschrieben und danach per LDA wieder ausgelesen. Da war die Position der leuchtenden LED bei jedem LDA nach links gewandert.

Dazwischen allerdings haben die Datenbyte-LEDs munter geflackert beim Ausführen des Programmes. Zum Ausgeben taugt der Datenbus also nicht, da geht zuviel drüber, was uns ausgabetechnisch interessiert.

Was man bräuchte, wäre etwas, in das man die Daten in einem bestimmten Taktzyklus (nämlich dem STA) schreiben könnte und dass dann den Zustand halten würde bis zum nächsten Schreibzugriff. Und das den stabilen, gespeicherten Zustand natürlich auch ausgeben könnte. So könnten wir die einfachste Ausgabe, eine mit 8 LEDs realisieren.

Das Speichern eines Bits macht man mit einem sogenannten D-Latch, oder D-FlipFlop. Das RS-FlipFlop hatten wir ja bereits im Artikel Verwendung des 555-Timer als Taktgeber / Clock kennengelernt, denn der 555-Timer enthält ein solches.


Das D-Latch kann zudem den Zustand (0 oder 1, Low oder High) halten. Zu Veränderung wird ein Eingang gepulst, und der dann aktuell am Eingang anliegende Zustand wird zum neuen Ausgangszustand.

Freundlicheweise müssen wir uns kein D-Latch aus Logik-Gattern (siehe auch Artikel Digitale Logik und Logikgatter einfach erklärt zusammenlöten und diese vielleicht noch aus Transistoren zusammenlöten, sondern bekommen sie als 74HC374 im Achterpack als IC geliefert.

Für unsere 8 LEDs reicht also ein einziger Chip.

Der 74HC374 Achtfach D-Latch Chip

Über den 74HC374 ist folgendes zu sagen:
Falls ihr andere LEDs als gelbe 3mm LEDs benutzt, braucht ihr vielleicht andere Vorwiderstandswerte. Benutzt dazu diesen Rechner:



74HC374 Funktionstest

Um mich mit dem Funktionsprinzip vertraut zu machen, schließe ich einmal 8 LEDs an die Ausgänge und setze die Eingänge manuell per Jumperkabel. Ich habe für euch ein Video davon aufgenommen:



OE kann ich also mit GND verbinden und auf immer Low lassen. Die Eingänge D0 bis D7 werde ich mit den Datenbits D'0 bis D7 des 6502-Datenbuses verbinden. Damit kriegt der 74HC374 alles mit, was auf dem Datenbus los ist und ändert ständig seine internen Zustände. Solange CK auf Low ist, ist aber die alte Ausgabe aktiv und der Betrachter bekommt davon nichts mit. Erst, wenn ich CK kurz auf High setze, werden die gerade anliegenden Daten übernommen und bleiben dann bis zum nächsten Update so, weil CK gleich wieder auf Low gesetzt wird.

Die Aufgabe ist also, im richtigen Moment (wenn die richtigen Daten anliegen) CK kurz auf High zu setzen und ansonsten auf Low zu lassen. Über das R/W-Signal des 6502 geht das nicht, denn dann würde bei jedem schreibenen Zugriff aufs RAM auch die LED-Ausgabe verändert werden.

Integration der LED-Ausgabe in meine Breadboard-Computer-Architektur

Bleibt nur noch der Adressbus und die Adresse, die ich manipulieren kann. Ich muss es so steuern, dass, wenn eine bestimmten Adresse gesetzt ist - nämlich die, die ich dem LED-Ausgabe-Update zuschreibe - ein High am Ausgang einer Logik-Schaltung ist und ansonsten ein Low.

Wir haben ja schon beim letzten mal gesehen, dass die Adresse bei Verwendung des STA Befehls kurzzeitig auf den im Programm angegeben gesetzt wird. Damit könnten wir das kurzfristige Schalten auf High also steuern.

Das heißt aber auch, ich muss Adress- und damit Speicherbereiche opfern. Nun muss ich einen Kompromiss finden. Natürlich könnte es so gestalten, dass nur die Adresse 0x7000 auf CK weitergeleitet ist. Dazu müsste ich aber alle 16 Adressleitungen per Logik verknüpfen. Da bräuchte ich reichlich Gatter.

Was aber, wenn ich nur A14 (Wertigkeit 0x4000), A13 (Wertigkeit 0x2000), A12 (Wertigkeit 0x1000) abfragen würde und zwar, ob diese 1 (also High) sind? Dann würde die Logik auf alles, was 0x7*** ist reagieren, also auf alles von 0x7000 bis 07FFF.

Außerdem würde es auch auf 0xF000 bis 0xFFFF reagieren. Das liegt im ROM. Ins ROM hineinschreiben geht nicht, weil wir dort WE auf nur lesend gesetzt haben. Schaden kann das also nicht anrichten. Allerdings würde die Ausgabe auch beim Lesen gesetzt neu gesetzt werden. Und das passiert bei der Reset-Routine. Hier wird ja aus 0xFFFC und 0xFFFD die Programmstartadresse gelesen. Die Ausgabe würde bei 0x8000 als Startadresse also zuerst auf 0x00 und dann auf 0x80 gesetzt werden. Aber das soll mich nicht groß stören. Ich kann ja dann beim Programmstart die LEDs gleich wieder alle auf 0 setzen. Wenn es mich doch irgendwann mal stören sollte, dann müsste ich entweder die RW-Leitung oder das Adressbit 15 mit ins Boot holen. Außer der Rest- und den IRQ-Vektoren werde ich wohl kaum bis in diese hoehn Speicherbereiche im ROM vordringen. So lang wird wohl kein Programm werden. Trotzdem werde ich im Hinterkopf behalten, dass ich der ROM-Bereich von 0xF000 bis 0xFFFF nicht benutzen sollte, weil das auch Auswirkungen auf die IO haben kann.

Ein weiterer Punkt bei dieser Konfiguration ist, dass wenn ich später noch mehrere IO-Speicherbereiche definiere, auch immer die LEDs mitflackern werden, wenn ich diese anspreche. Denn diese Adressen werden auch in 0x7000 bis 0x7FFF liegen. Aber das sehe ich nicht kritisch. Ich glaube nicht, dass ich LED-Ausgabe und andere Ausgabe gemeinsam verwenden werde.

Ich sehe das sogar mehr als Vorteil, dass ich an den LEDs das letzten Datenbyte ablesen werden kann, dass in den IO Bereich geschrieben bzw. daraus gelesen wurde. Das könnte sich zum Debugging noch als nützlich erweisen.

Weiterer Voteil: ich kann das Signal, dass ich für die LED-Ausgabe benutze, weiterverwenden. Denn wenn das High ist, ist schon einmal klar, dass A12 bis A14 auch High sind.

Zu beachten wäre eventuell noch, dass das RAM immer noch auf die Adressen 0x7000 bis 0x7FFF reagiert und alles mitspeichert, aber auch liest, was hier über den Adressbus geht. Bei der Ausgabe ist das kein Problem. Hier schreibt das RAM einfach den Wert, den der Datenbus hat ins RAM. Bei der Eingabe könnte das aber ein Problem werden, wenn Eingabeeinheit und RAM beide für die Adresse das Datenbyte ausgeben wollen. Hier könnte man in die Chip Enable Leitung des RAM die Ausgabe der A12-A14-Logik einfließen lassen, damit das RAM sich dabei nicht angesprochen fühlt. Aber das ist ein Problem, dass ich löse, wenn es soweit ist.

Neue Speicheraufteilung

Ich halte es für einen guten Kompromiss, 4KB RAM und 4KB ROM für die IO zu opfern. Zeit für ein Update unserer Speichertabelle: Adr.(hex) Beschreibung 0000 RAM Zero-Page: schneller Speicherzugriff mit zp-OpCodes ... RAM Zero-Page: schneller Speicherzugriff mit zp-OpCodes 00FF RAM Zero-Page: schneller Speicherzugriff mit zp-OpCodes 0100 RAM Stack: Hinterlegung von Rücksprungadressen ... RAM Stack: Hinterlegung von Rücksprungadressen 01FF RAM Stack: Hinterlegung von Rücksprungadressen 0200 RAM frei für User-Variablen etc. ... RAM frei für User-Variablen etc. 6FFF RAM frei für User-Variablen etc. 7000 Hardware IO / LED Ausgabe (Spiegelung im RAM) .... Hardware IO / LED Ausgabe 7FFF Hardware IO / LED Ausgabe 8000 ROM (per DIP-Switch eingeblendete Programmbank von 0 bis 15) ... ROM Programm und Konstanten EFFF ROM Programm und Konstanten F000 ROM gesperrter Bereich wegen Hardware IO Überlagerung ... ROM gesperrter Bereich wegen Hardware IO Überlagerung FFF9 ROM gesperrter Bereich wegen Hardware IO Überlagerung FFFA ROM Sprungvektor für NMI Low-Byte FFFB ROM Sprungvektor für NMI High-Byte FFFC ROM Sprungvektor für RES Low-Byte = Programmstartadresse FFFD ROM Sprungvektor für RES High-Byte = Programmstartadresse FFFE ROM Sprungvektor für BRK/IRQ Low-Byte FFFF ROM Sprungvektor für BRK/IRQ High-Byte

Eine Logik für den Adressbus

Die logische Aussage bezüglich der CK-Leitung am 74HC374 für unsere LED-Ausgabe lautet:
CK soll high sein, wenn A14 high ist und A13 high ist und A12 high ist.
Oder in Logilschreibweise ausgedrückt: CK = A14 ∧ A13 ∧ A12 Wer nur NAND-Gates (74HC00) und keine AND-Gates (74HC08) eingekauft hat, der stellt die Formel um zu: CK = ¬(((a ⊼ b) ⊼ (a ⊼ b)) ⊼ c) und kommt mit vier NAND-Gattern und einem IC aus:




Die Adressleitung A12, A13 und A14 entsprechend durch den 7400er geschickt und an CK angeklemmt, wird CK jetzt immer automatisch High, wenn eine Adresse zwischen 0x7000 und 0x7FFF angesprochen wird, egal, ob lesend oder schreibend. Wir müssen also nur etwas aus diesem Speicherbereich lesen oder schreiben, damit das Datenbyte in den 74HC374 geschrieben und die Ausgabe auf das aktuelle Datenbyte aktualisiert wird.

Ein neues Maschinensprache-Programm

Schreiben wir uns ein neues Programm, und steuern damit die LEDs an. Ich dachte an den Lauflicht-Effekt von K.I.T.T., dem Auto aus der Serie Knight Rider.

Dabei läuft die LED-Position erst ganz nach links und dort angekommen wieder ganz nach rechts und danach geht das Spiel von vorne los. Mit der Taktfrequenz können wir später die Geschwindigkeit des Lauflichtes einstellen.

Adr. Assembler Bytecode (hex) Bedeutung (hex) 8000 LDA# 01 A9 01 lade 01 (rechte LED) in den Akku 8002 STA 7000 8D 00 70 speichere den Akku-Inhalt an RAM-Adresse 7000, die rechte LED sollte nun leuchten 8005 LDA# 07 A9 07 wir müssen 7 mal nach links shiften, bis die LED ganz links angekommen ist 8007 ROL 7000 <-. 2E 00 70 alle Bits in 7000 werden eine Stelle nach links verschoben, | LED wandert nach links 800A DECA | 3A erniedrigt den Akku um eins 800B BNEr --' D0 FA nach 7 Durchläufen ist der Akku auf Null und BNE (Branch If Not Equal Zero) springt nicht mehr zurück. BNE ist ein 2-Byte-Befehl, der 1-Byte Parameter gibt den Offset zum aktuellen PC an. Sprünge zurück haben einen negativen Offset, der als Zweier- komplement angegeben werden muss. 0xFF ist -1, 0xFA ist -6 800D LDA# 07 A9 07 wir müssen die LED-Position 7 mal nach rechts verschieben, bis die LED wieder ganz rechts angekommen ist 800F ROR 7000 <-. 6E 00 70 alle Bits in 7000 werden eine Stelle nach rechts verschoben, | LED wandert nach rechts 8012 DECA | 3A erniedrigt den Akku um eins 8013 BNEr --' D0 FA nach 7 Durchläufen ist der Akku auf Null und BNE springt nicht mehr zurück 8015 JMP 8005 4C 05 80 und wieder zum Scrollanfang zurück, dann geht es wieder nach links usw. usf. Unser neues Maschinenspracheprogramm lautet also A9 01 8D 00 70 A9 07 2E 00 70 3A D0 FA A9 07 6E 00 70 3A D0 FA 4C 05 80 welches wir an Programmbank des SST39F040 flashen - von unserem RAM-Test nehmen wir damit Abschied, wir lassen nur die Startadresse von 0x8000 bei 0xFFFC/D stehen.

Testen der LED-Ausgabe

Nachdem wir den Datenbus mit dem 74HC374 verbunden haben und die Adressleitungen A12 bis A14 hoffentlich korrekt mit dem NAND-IC 74HC00 und den Ausgang des letzten Gatters mit CK verbunden haben, wird es Zeit für einen Test, den ich in folgendem Video dokumentiere:



Debugging und Erkennen des Denkfehlers

Oops, leider funktioniert unsere Schaltung wohl nicht so, wie wir uns das gedacht haben. Die LED zeigen immer nur 0x70 an. Woran liegt das wohl?

Schauen wir einmal an, was der Sniffer so ausgegeben hat und was die LED-Ausgabe anzeigt: Sniffer-Code LED-Ausgabe 01FB DC r FFFC 00 r DC FFFD 80 r DC 8000 A9 r LDA# DC 8001 01 r DC 8002 8D r STA DC 8003 00 r DC 8004 70 r DC 7000 00 W 70 7000 01 W 70 8005 A9 r LDA# 70 8006 07 r 70 8007 2E r ROL 70 8008 00 r 70 8009 70 r 70 7000 01 r 70 7000 00 W 70 7000 02 W 70 800A 3A r DECA 70 800B D0 r 70 800B D0 r BNEr 70 ... Was passiert jetzt genau? Immer wenn CK auf high geht (den orangen Stellen) wird das Datenbyte genommen und in den 74HC374-Speicher geschrieben und dort gehalten. Nur scheint es so, dass der Adressbus vor dem Datenbus aktualisiert wird und die Werte genommen werden, die vom letzten Datenbyte stammen (gelb markiert).

So geschieht es das erste mal bei Adresse FFFC: statt des neuen 00 wird das alte DC genommen. Da CK vorher low war und jetzt high ist, liegt eine Veränderung vor und die Ausgabe zeigt DC an. Danach bleiben wir in dem Speicherbereich bei FFFD, es ändert sich nichts, 0xDC bleibt in der Ausgabe.

Bei 8000 verlassen wir den IO-Speicherbereich und CK ändert sich wieder auf low, das heißt die Ausgabe wird gesperrt und bleibt auf DC. Erst beim nächsten Wechsel auf high wird wieder aktualisiert. Und der findet bei 7000 statt. Nur wird hier wieder das Datenbyte vor der Adressänderung genommen, und das ist 70. So geht die ganzen Schleifen weiter. Es wird immer die 70 angezeigt, weil wir ja die 70 als Highbyte der Adresse als letztes auf den Datenbus gepackt haben.

Korrektur der Schaltung

Was ist, wenn wir die Ausgabe aktualisieren, wenn der IO-Speicherbereich verlassen wird, anstatt beim Eintritt? Oder in anderen Worten, das CK-Signal wie wir es jetzt haben, negieren? FFFC 00 r FFFD 80 r 8000 A9 r LDA# 8001 01 r 8002 8D r STA 8003 00 r 8004 70 r 7000 00 W 7000 01 W 8005 A9 r LDA# 8006 07 r 8007 2E r ROL 8008 00 r 8009 70 r 7000 01 r 7000 00 W 7000 02 W 800A 3A r DECA 800B D0 r 800B D0 r BNEr 800C FA r 800D A9 r 8007 2E r ROL 8008 00 r 8009 70 r 7000 02 r 7000 00 W 7000 04 W 800A 3A r DECA ... Dann würde der interne Speicher des 74HC374 immer geschrieben, solange wir uns im IO-Speicherbereich aufhalten und sobald wir ihn verlassen, geht CK auf low und es würde die Ausgabe aktualisiert und gehalten, solange low anliegt.

Ich habe die Stellen, an denen der IO-Bereich verlassen wird, oben orange markiert. Wie wir jetzt ja erfahren haben, wird der Adresbus vor dem Datenbus aktualisiert. Also steht bei 8000 noch das 80 vom vorherigen Taktzyklus im Datenbyte (und intern im 74HC374, aber noch nicht ausgegeben). Mit dem Verlassen geschieht das Ausgabe-Update und wir haben eine 80 in der Ausgabe. Diese erste Ausgabe ist ungewollt, aber zeigt, dass dieses Prinzip funktioniert.

Beim nächsten Verlassen des IO-Bereichs bei 8005 steht jetzt die 01 im 374er-Register und wird ausgegeben. Das vorher schon die 00 drinsteht, wie die 6502 das wohl immer bei einem STA macht, ist irrelevant, denn das wird ja nicht angezeigt, solange wir uns im hohen IO-Speicherbereich befinden.

Das neue Prinzip funktioniert. Es steht zuerst eine 1 in der Ausgabe (und in Speicherstelle des RAM), dann eine 2, eine 8 usw. usf. Das Lauflicht bewegt sich wie gewollt.

Und auch die Spiegelung ins RAM funktioniert. Denn der LED-Wert steht nicht nur im 374er,Register, sondern auch im SRAM an Adresse 7000. Darum können wir auch die ROL und ROR-Befehle benutzen, die sich den LED-Wert aus dem RAM besorgen (read), verändern (ALU) und wieder zurückschreiben (write). Dadurch kommt unser Maschinenspracheprogramm mit nur 24 Bytes aus.

Also brauchen wir ein negiertes CK Signal, wie wir es bisher hatten. Dazu nehmen diejenige, die 7408 AND Gatter verwendet hatten, einen 7404 Inverter.

Diejenigen, die die Logik mit 7400 NAND-Gattern haben Glück: es fällt einfach nur die letzte Invertierung weg. Damit braucht es nur noch 3 NAND-Gatter und ein IC gegenüber 2 ICs (1 AND und 1 NOT-Gatter) bei der AND-Lösung. Schon wieder hat sich die Verwendung von NAND ausgezahlt.




Mit der neuen Logik funktioniert die Schaltung auch wie gewollt, wie man in folgendem Video sieht:



Im nächsten Teil der Reihe erklimmen wir dann die nächste Sprosse auf der Evolutionsleiter der Programmiersprachen und wechseln von Maschinensprache zu Assembler.