8-Bit-Breadboard-Computer auf Basis einer 6502-CPU - 4x4 Keypad als Tastatur mit 65C22 abfragen
Bisherige Artikel dieser Serie - hier könnt ihr nochmal alle Grundlagen nachlesen, falls ihr jetzt erst einsteigt:- Digitale Logik und Logikgatter einfach erklärt
- Verwendung des 555-Timer als Taktgeber / Clock
- Das Clock-Modul: Taktgeber für unseren Breadboard-Computer
- Speichertypen und Zugriff auf Speicher
- Erste Schritte mit der CPU
- Eine echte WDC W65C02-CPU
- Das Speicher-Modul: Anbindung von RAM und ROM
- Erstes Programm in Maschinensprache: RAM-Test
- Das Sniffer-Modul: Ein Arduino/STM32 zeigt an, was auf dem Bus los ist.
- Erstes Ausgabegerät: Adressierung und Ausgabe auf 8 LEDs
- Programmiersprache-Evolution: von Maschinensprache zu Assembler
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 1: Taktungsprobleme
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 2: 20 Nanosekunden, die nicht sein sollten
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 3: Assembler-Programme
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 4: neue Adressierungsarchitektur mit 74HC688 und 74HC138
- 3-fach 7-Segment-Anzeige als dezimale Ausgabe, Teil 5: Programmierung in Assembler
- 3x16 Zeichen LCD DOGM163 als Textausgabe-Gerät, Teil 1: Breadboard-Aufbau und erste Programmversion
- 3x16 Zeichen LCD DOGM163 als Textausgabe-Gerät, Teil 2: Debugging
- Clock-Modul upgraden: höherer Takt (2 KHz) und Frequenzgenerator (bis 160 KHz)
Das Keypad habe ich bereits mit einem STM32 ausgetestet. Das Funktionsprinzip ist ganz einfach: Es gibt vier horizontale Leitungen und 4 vertikale Leitungen, die einmal waagerecht und einmal senkrecht unter den Tasten durchführen. Beim Drücken auf eine Taste wird an dem entsprechenden Punkt wird nun die dort durchlaufenden waagerechte mit der dort durchlaufenden senkrechten Leitung verbunden.

Zur Abfrage wird durch die ersten vier Leitungen (A bis D) nacheinander Strom geschickt und dann geschaut, ob und wo bei 1 bis 4 dieser wieder anliegt. Das Hineinschicken des Stromimpulses ist kein Problem, das kann man ganz einfach mit ein bisschen Bitbanging der unteren 4 Bits machen. Auch das Auslesen sollte kein Problem sein, wenn man während einer Schleife auf eine Taste wartet und abfragt, ob eines der oberen 4 Bits auf High gegangen ist.
Das Problem bei der Sache ist allerdings, dass ein Tastendruck den Datenbus kurzschließt. Wenn ich eine Taste drücke, wird das eine (untere) Bit auf ein anderes (oberes) Bit kopiert. Das ist natürlich fatal, wenn man was aus ROM oder RAM lesen will und der Datenbus ist auf einmal durcheinander. Man müsste deswegen strikte Disziplin walten lassen und wirklich immer nur etwas eingeben, wenn das Programm dazu auffordert, und in der Länge, dass das Programm vorgibt. Soll man etwa zwei Ziffern eingeben und gibt aus Versehen drei ein, dann befindet sich das Programm eventuell schon wieder im Verarbeitsmodus und der Tastendruck schießt dazwischen, ändert was am Datenbus und die Ergebnisse sind falsch oder das Programm stürzt ab.
Mit den verwendeten 74HC374 bzw. 74HC574 Latches kommen wir also diesmal nicht weit. Wir brauchen etwas, dass den Tastendruck entgegennimmt, ihn speichert und erst auf Befehl kurz den Datenbus auf die Eingabe setzt und danach den Datenbus wieder "frei" lässt.
Der 65C22 Versatile Interface Adapter (VIA)
- D0...D7 wird an den Datenbus des 6502 angeschlossen
- Es gibt zwei Ports je 8 Bit: PA0...PA7 und PB0...PB7. Hier kann man bis zu 2 IO-Geräte dranhängen, die mit ...
- ... Register-Befehlen gesteuert werden, die durch RS0...RS3 definiert werden. Hier werden 4 Leitungen des Adressbus' (untere Adressbits) angeschlossen. Je nach Adresse weiß der 65C22 dann, welcher Befehl ausgeführt werden soll. Die Parameter dazu werden sozusagen über den Datenbus übergeben und hängen zudem vom Status des R/W Pins ab.
- Ob der Chip überhaupt auf Anfragen reagiert, wird über die beiden Chip Select-Leitungen CS1 (aktiv, wenn 1) und CS2 (aktiv, wenn 0) gesteuert. Diese sollte mit dem Ergebnis einer Logikschaltung über die oberen Adressbits angesteuert werden.
- CA1 und CA2 bzw. CB1 und CB2 dienen als Eingabe für Interrupts oder Ausgabe von Handshakes. Außerdem können sie als serieller Datenport verwendet werden (zusammen mit den Shiftregistern).
- IRQ ist eine Output-Leitung und kann Interrupt Anforderungen senden. Dies geschieht in Zusammenarbeit mit den internen Timern und Countern. Damit kann man in bestimmten Intervallen Interrupts an die CPU senden.
- PHI2 wird mit der Clock des 65C02 verbunden. Der 65C22 ist auf den 65C02 abgestimmt und weiß die Clock-Flanken richtig zu timen.
- RES auf Low gesetzt, setzt den 6522er zurück. Wir verbinden ihn mit dem Reset-Signal unseres Reset-Tasters.
- Ebenso verbinden wir die R/W-Leitung mit der CPU
- VDD (+5V) und VSS (GND) sollten selbsterklärend sein
65C22 Register Befehle
Register Number | RS Coding | Register Designation | Description | ||||
---|---|---|---|---|---|---|---|
RS3 | RS2 | RS1 | RS0 | Write | Read | ||
0 | 0 | 0 | 0 | 0 | ORB/IRB | Output Register "B" | Input Register "B" |
1 | 0 | 0 | 0 | 1 | ORA/IRA | Output Register "A" | Input Register "A" |
2 | 0 | 0 | 1 | 0 | DDRB | Data Direction Register "B" | |
3 | 0 | 0 | 1 | 1 | DDRA | Data Direction Register "A" | |
4 | 0 | 1 | 0 | 0 | T1C-L | T1 Low-Order Latches | T1 Low-Order Counter |
5 | 0 | 1 | 0 | 1 | T1C-H | T1 High-Order Counter | |
6 | 0 | 1 | 1 | 0 | T1L-L | T1 Low-Order Latches | |
7 | 0 | 1 | 1 | 1 | T1L-H | T1 High-Order Latches | |
8 | 1 | 0 | 0 | 0 | T2C-L | T2 Low-Order Latches | T2 Low-Order Counter |
9 | 1 | 0 | 0 | 1 | T2C-H | T2 High-Order Counter | |
A | 1 | 0 | 1 | 0 | SR | Shift Register | |
B | 1 | 0 | 1 | 1 | ACR | Auxiliary Control Register | |
C | 1 | 1 | 0 | 0 | PCR | Peripheral Control Register | |
D | 1 | 1 | 0 | 1 | IFR | Interrupt Flag Register | |
E | 1 | 1 | 1 | 0 | IER | Interrupt Enable Register | |
F | 1 | 1 | 1 | 1 | ORA/IRA | Same as Reg 1 except no "Handshake" |
Ansprache des 6522
Um unsere KeyPad-Tastatur auszulesen , soll durch A...D (Datenbits 4...7) Strom geschickt werden und dann in 1...4 (Datenbits 0...3) geschaut werden, ob der Strom dort wieder auftaucht. D4...D7 werden also als Output-Leitungen und D0...D3 als Input-Leitungen definiert. Mit diesem Mix brauchen wir nur einen Datenport (A) und haben Datenport B für ein weiteres Gerät frei:D0 IN 1
D1 IN 2
D2 IN 3
D3 IN 4
D4 OUT A
D4 OUT B
D4 OUT C
D4 OUT D
Anpassung der Adressierungsarchitektur
Doch zuerst müssen wir ein Problem lösen: Bisher hört unser RAM auf alles zwischen $0000 und $7FFF. Das hat nichts bei den bisherigen Nur-Schreibbefehlen ausgemacht. Sollte der in ein IO-Output-Gerät geschriebene Wert doch ruhig auch im RAM stehen. Doch beim Lesen würde sich das RAM und ein IO-Input-Gerät ins Gehege kommen. Wer soll jetzt liefern? Das RAM oder das Eingabegerät? Was ankommt, wäre ein Glücksspiel. Wir müssen dem RAM also beibringen, nicht zu reagieren, wenn der Adressbereich ab $7000 angesprochen wird. Das RAM soll sich einfach raushalten und keine Antwort geben. So kommt die Antwort des Eingabegerätes ungehindert durch.Um die Adressierungs-Architektur möglichst einfach zu halten - meine Erfahrung mit zu vielen kaskadierten Logikgattern habe ich ja bereits gemacht - habe ich mir überlegt, ob mir nicht auch nur 16 KB RAM ausreichen würde. Mit dem Gedanken, dass wohl anzuzeigende, vorgefertigte Texte den meisten Speicherplatz verbrauchen werden und die im ROM liegen, kann ich mir momentan gar nicht vorstellen, was ich alles in 16 KB RAM unterkriegen sollte. Da stehen doch eigentlich nur veränderliche Werte drin. Ich habe darum beschlossen, folgende, simplere Adressierung zu verwenden:
A15 ($8000+) | A14 ($4000+) | A13 ($2000+) | A12 ($1000+) | Speicherbereich | Verwendung |
---|---|---|---|---|---|
H | * | * | * | $8000...$FFFF | ROM (EPROM/Flash) |
L | L | * | * | $0000...$3FFF | RAM (Variablen) |
L | H | L | L | $4000... | IO Output (LED, 7Seg, LCD, ...) |
L | H | L | H | $5000... | IO Output (noch frei) |
L | H | H | L | $6000... | IO Input 6522 #1 (Keypad) |
L | H | H | H | $7000... | IO Input z. B. 6522 #2 (noch frei) |
L | H | H | H + {A11...A0} | $7800... | Coprozessor (Arduino oder STM32), der den Adressbus selbst auf bestimmte IO-Adressen überwacht und dann entsprechend nach Funktion reagiert (RND, RTC, etc.) |
Das sollte für den Breadboard Computer an Adressen eigentlich ausreichen. Und wenn nicht, muss ich später auch noch die Adressleitung A11 mit einbeziehen.
Natürlich muss ich damit auch die Chip Select Leitungen für RAM, 74HC138 und den neuen 6522 anpassen:
neuer 74HC688 (rechts neben 7-Segment-Anzeige): RAM Chip Select: bis $3FFF
Paar Signal P / Q Bemerkung
0. A15 H/L L \ Adressen ab $0000 bis $3FFF
1. A14 H/L L / lesen und schreiben
2 L L unbenutzt
3 L L unbenutzt
4 L L unbenutzt
5 L L unbenutzt
6. L L unbenutzt
7. L L unbenutzt
G L G wird fest auf Low gesetzt
74HC688 (Breadboard oben rechts) Änderung für Output-Geräte: ab $4000
Paar Signal P / Q Bemerkung
0. PHI2 H/L H Der Adressbus ist valide, wenn PHI2 High ist
1. R/W H/L L Der Datenbus ist valide, wenn R/W Low (=write) ist
2. A15 H/L L nur für den Bereich $0*** bis $7*** nicht $8**** bis $F*** (ROM)
3. A14 H/L H \
4. A13 H/L L } Adressen ab $4000
5. A12 H/L L /
6. L L unbenutzt
7. L L unbenutzt
G L G wird fest auf Low gesetzt
neuer 74HC688 (rechts neben 6522 Breadboard unten rechts): Input-Geräte: ab $6000
Paar Signal P / Q Bemerkung
0. A15 H/L L nur für den Bereich $0*** bis $7*** nicht $8**** bis $F*** (ROM)
1. A14 H/L H \
2. A13 H/L H } Adressen ab $6000
3. A12 H/L L /
4 L L unbenutzt
5 L L unbenutzt
6. L L unbenutzt
7. L L unbenutzt
G L G wird fest auf Low gesetzt
Unsere Speicheraufteilung bekommt damit auch neue Grenzen. Ich habe die sonstigen fest verwendeten Adressen auch gleich mit eingetragen:
Adr.(hex) Beschreibung
0000 RAM Zero-Page: schneller Speicherzugriff mit zp-OpCodes, Muss für BIT-Befehle
0 lcdhb: High Byte/Nibble für LCD-Ansteuerung
1 lcdlb: Low Byte/Nibble für LCD-Ansteuerung
2 lcdrs: RS-Status für LCD: 1=char, 0=cmd; (vor jsr lcdByte setzen)
...
00FF
0100 RAM Stack: Hinterlegung von Rücksprungadressen
...
01FF
0200 RAM für in mehreren Programmen verwendete Variablen
...
1000 RAM für programmbezogene Variablen
...
3FFF
4000 Hardware IO Output #1
0 LED Ausgabe
1 linke 7-Segment-Anzeige
2 mittlere 7-Segment-Anzeige
3 rechte 7-Segment-Anzeige
4 DOGM163 LCD mit 3*16 Zeichen
5 frei für Output IO-Gerät, Anschluss an 74HC138 Breadboard oben rechts
6 frei für Output IO-Gerät, Anschluss an 74HC138 Breadboard oben rechts
7 frei für Output IO-Gerät, Anschluss an 74HC138 Breadboard oben rechts
8 frei für Output IO-Gerät, Anschluss an 74HC138 Breadboard oben rechts
5000 Hardware IO Output #2 (zur zukünftigen Verwendung)
6000 Hardware IO Input #1
0 Write: Output Register "B"
Read: Input Register "B"
1 Write: Output Register "A" 4x4 KeyPad
Read: Input Register "A" 4x4 KeyPad
2 Data Direction Register "B"
3 Data Direction Register "A" 4x4 KeyPad
4 Write: T1 Low-Order Latches
Read: T1 Low-Order Counter
5 T1 High-Order Counter
6 T1 Low-Order Latches
7 T1 High-Order Latches
8 Write: T2 Low-Order Latches
Read: T2 Low-Order Counter
9 T2 High-Order Counter
A Shift Register
B Auxiliary Control Register
C Peripheral Control Register
D Interrupt Flag Register
E Interrupt Enable Register
F Same as Reg 1 except no "Handshake" 4x4 KeyPad
7000 Hardware IO Input #2 (zur zukünftigen Verwendung)
7800 Coprozessor (zur zukünftigen Verwendung)
8000 ROM (per DIP-Switch eingeblendete Programmbank von 0 bis 15)
...
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
Anpassungen der Programme und Test
Da die IO-Geräte nun hardwaretechnisch neue Adressen haben, müssen natürlich auch die Programme daran angepasst werden. Ich habe das so gelöst, dass ich eine Include-Datei erstellt habe:io-defs.inc
; IO-Geräte-Adressen
; last edit: Oliver Kuhlemann (www.cool-web.de), 2020-08-31
; --- Konstanten: IO-Adressen ---
LED: equ $4000 ; 8-LED-Ausgabe
SEGL: equ $4001 ; linke 7-Segment-Anzeige
SEGM: equ $4002 ; mittlere 7-Segment-Anzeige
SEGR: equ $4003 ; rechte 7-Segment-Anzeige
LCD: equ $4004 ; DOGM163 LCD mit 3*16 ZeichenlcdInit: ; LCD initialisieren
und diese mit INCLUDE io-defs.inc
in die einzelnen Programme einbinde.Die Programme für die LED und 7 Segment-Anzeige muss ich natürlich auch noch in der Hinsicht anpassen, dass es nun keine RAM-Spiegelung mehr gibt. Da das RAM jetzt nur noch auf Adresse unter $4000 horcht, wird bei STA LED oder STA SEGL nichts mehr ins RAM geschrieben, sondern nur noch in das IO-Gerät. Die verwendeten ROL-Befehle muss ich deshalb anpassen.
Auf die Änderungen gehe ich in folgendem Video ein. Dort seht ihr auch, ob die Hardware / die Programme mit den Adress-Anpassungen noch laufen:

Mit dem 6522 die Tastatur abfragen
Zu allererst müssen wir dem 6522 mitteilen, welchen Port (A oder B) wir verwenden wollen, und welche der 8 Bits Input und welche Output sind. Dabei dürfen wir freudig mixen, auf einem Port dürfen sowohl Input- als auch Output-Bits sein.Wenn wir die oberen 4 Bits von Port A als Output haben wollen und die unteren als Input, um dann zu sehen, wo das Output beim Input wieder rauskommt bei einem Tastendruck (der die beiden ja kurz schließt), dann schreiben wir in das 3. Befehls-Register %1111000. Eine 1 bedeutet Output, eine 0 Input.
Das dritte Befehlsregister ist für die Datenrichtung (Data Direction) von Port A zuständig. Wir laden hier also %1111000 in unseren Akku und speichern ihn dann an Adresse $6003, womit die Funktion vom 6522er ausgeführt wird.
Idealerweise schreibt man sich eine Include-Datei. Meine heißt io-defs.inc:
; --- 6522er ---
VIA: equ $6000 ; 6522 #1: 4x4 Keypad
VIA_PORTB: equ VIA+$0 ; Port B
VIA_PORTA: equ VIA+$1 ; Port A
VIA_DDRB: equ VIA+$2 ; Data Direction Port B
VIA_DDRA: equ VIA+$3 ; Data Direction Port A
VIA_T1CL: equ VIA+$4 ; Timer 1 Low Order Latches (W) / Counter (R)
VIA_T1CH: equ VIA+$5 ; T1 High-Order Counter
VIA_T1LL: equ VIA+$6 ; T1 Low-Order Latches
VIA_T1LH: equ VIA+$7 ; T1 High-Order Latches
VIA_T2CL: equ VIA+$8 ; Timer 2 Low Order Latches (W) / Counter (R)
VIA_T2CH: equ VIA+$9 ; T2 High-Order Counter
VIA_SR: equ VIA+$A ; Shift Register
VIA_ACR: equ VIA+$B ; Auxiliary Control Register
VIA_PCR: equ VIA+$C ; Peripheral Control Register
VIA_IFR: equ VIA+$D ; Interrupt Flag Register
VIA_IER: equ VIA+$E ; Interrupt Enable Register
VIA_PA_No_SHAKE: equ VIA+$F ; Same as Reg 1 except no "Handshake"
VIA2: equ $5000 ; 6522 #2: für zukünfitge Verwendung
Dann kann man die Adressen mit VIA_DDRB ansprechen und nicht mit $6003. Zum Einen ist das besser merk- und lesbar. Und zum Anderen ist der Anpassungsauswand denkbar gering, sollte sich die Adresse durch Architekturänderung doch noch einmal ändern (was ich allerdings derzeit nicht glaube, aber man weiß ja nie): lediglich die $6000 muss angepasst werden. Die anderen Adressen sind davon abhängig.Mit
getKey2:
lda #%11110000
sta VIA_DDRA
geben wir dem 6522er also über unsere Datenrichtungen Bescheid. Und der verhält sich dann so, wie man es sich wünscht und erwartet:
WDC 65C22 Datasheet, Seite 8Das heißt, wenn ein Datenbit als Input definiert ist, ist egal, wenn wir versuchen, etwas hereinzuschreiben. Es wird ignoriert.
Should data be written into bit positions corresponding to pins which have been programmed as input, the output pins will be unaffected.
Meine ersten Messungen des 6522 am Oszilloskop haben übrigens ergeben, dass die Datenleitungen auf High gezogen sind, wenn sie keinen definieren Wert haben (Stichwort Pullup-Widerstände).
Darum macht es mehr Sinn, statt durch jeweils eine der vier Output-Leitungen Strom zu schicken (wäre High, wäre Normalzustand), ein Low zu schicken und zu schauen, ob das Low beim Input durch Tastendruck ankommt (Input ist Normalzustand auch alles High).
Programmmiertechnisch ist das kaum mehr Aufwand
ldx #4 ; 4 Output-Leitungen nacheinander mit LOW versorgen...
nextKeyOut:
lda keyOut
sta VIA_PORTA
...
rol keyOut ; bits eins nach links schieben = nächste Leitung
dex
beq getKey2 ; wenn 4x durch, dann nochmal von vorne
bra nextKeyOut
sollte den Job erledigen. Jetzt müssen wir nur noch innerhalb der Schleife schauen, ob eine der Leitungen Low geworden ist, weil eine Taste gedrückt wurde und das Low aus dem Output-Pin durchschlägt (weil ja kurz geschlossen):
; und schauen, ob das bei einer der 4 Input-Leitungen ankommt...
lda VIA_PORTA
sta keyIn
; ein unteres Bit auf 0? Dann Taste gedrückt
and #%00001111
cmp #$0F ; $0F = Standardwert = nichts gedrückt
bne getKeyDone ; es wurde etwas gedrückt
Wie wir sehen, können wir VIA_PORTA sowohl mit STA, also auch mit LDA ansprechen, sprich etwas hineinschreiben oder etwas herauslesen. Was davon geschieht, entscheidet die R/W Leitung, die wir ja auch angeschlossen haben und die die 6502 CPU für uns automatisch setzt.Lassen wir die Schleife nun laufen, dann wandert das Low-Bit vom Output von rechts nach links durch (macht das ROL). Drücken wir dann eine Keypad-Taste, leitet der Kurzschluss das Low weiter zu einem Input-Bit, was wir nachfolgend auslesen. In diesem Augenblick speichern wir den Zustand in keyIn und verlassen die Schleife. Dort können wir noch warten, bis die Taste wieder losgelassen wurde und haben dann in keyIn das Bitmuster der gedrückten Taste.
Tastendrucke analysieren ...
Wie genau das Keypad jetzt angeschlossen ist, kann uns eigentlich egal sein, Hauptsache ist, für jede Taste kommt ein anderes Bitmuster heraus. Das lasse ich übrigens auf die LED-Ausgabe ausgeben.Linken wir unser Assembler-Programm einmal, flashen es auf unser EPROM und schauen, was unser Breadboard-Computer an Bitmustern ausspuckt:

Taste Bitmuster Hex
1 0111 0111 77
2 1011 0111 B7
3 1101 0111 D7
A 1110 0111 E7
4 0111 1011 7B
5 1011 1011 BB
6 1101 1011 DB
B 1110 1011 EB
7 0111 1101 7D
8 1011 1101 BD
9 1101 1101 DD
C 1110 1101 ED
E 0111 1110 7E
0 1011 1110 BE
F 1101 1110 DE
D 1110 1110 EE
Ich habe die "wandernde" Null einmal gelb markiert, damit klar wird, wie das Muster zustande kommt.Nun kann ich wieder - ähnlich der Definition für die 7-Segment-Anzeigen - eine Tabelle mit 256 Werte im ROM anlegen, um der Taste auch eine Wertigkeit zuzuweisen. Dabei möchte ich natürlich die Reihenfolge 0123456789ABCDEF für die Werte von Null bis Fünfzehn bzw. den Wert für ein Halbbyte / Nibble.
0 1 2 3 4 5 6 7 8 9 A B C D E F
BE 77 B7 D7 7B BB DB 7D BD DD E7 EB ED EE 7E DE
... und brauchbar machen
Nun brauche ich nur noch eine 256 Byte-Tabelle, in die ich überall $FF schreiben, bis auf an die gültigen Positionen. Das sind die, die wir gerade herausgefunden haben. Dort schreibe ich dann den Wert von $01 bis $0F. Durch diese Hilfstabelle muss ich dann einfach nur noch das Bitmuster auf die Startadresse der Hilfstabelle KEY_VAL addieren (indexierte Adressierung) und bekommen den Wert für das eingegebene Halbbyte.KEY_VAL (klicken, um diesen Abschnitt aufzuklappen)
KEY_VAL:
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $01, $FF, $FF, $FF, $04, $FF, $07, $0E, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $02, $FF, $FF, $FF, $05, $FF, $08, $00, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $03, $FF, $FF, $FF, $06, $FF, $09, $0F, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $0A, $FF, $FF, $FF, $0B, $FF, $0C, $0D, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
Statt der Zahlenwerte kann man natürlich auch die ASCII-Codes hinterlegen, will man nicht damit rechnen, sondern diese auf dem LCD ausgeben:
KEY_ASC (klicken, um diesen Abschnitt aufzuklappen)
KEY_ASC:
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $31, $FF, $FF, $FF, $34, $FF, $37, $45, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $32, $FF, $FF, $FF, $35, $FF, $38, $30, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $33, $FF, $FF, $FF, $36, $FF, $39, $46, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $41, $FF, $FF, $FF, $42, $FF, $43, $44, $FF
BYTE $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF, $FF
Die beiden Tabellen packe ich in das Include-File key-defs.inc.
Damit habe ich eigentlich auch schon alles zusammen, um den Tastendruck auch anzeigen zu lassen, sei es als Wert, z. B. auf die 7-Segment-Anzeige oder als ASCII-Zeichen für das DOGM163-LCD.
Source-Code Assembler
Die Funktion getKey kommt in das Include-File key.inc. Außerdem schreibe ich noch die Funktionen keyToLCD, keyToSeg und keyToVal, um die Ausgabe einfacher zu machen:key.inc (klicken, um diesen Abschnitt aufzuklappen)
; Tastendrücke vom 4x4 Keypad entgegennehmen
; last edit: Oliver Kuhlemann (www.cool-web.de), 2020-09-02
; -----------
; Funktionen:
; -----------
; getKey: Wartet auf Tastendruck (+loslassen) und schreibt Tasten-Muster nach keyIn
; in: -
; out: Key-Bitmuster in Var. keyIn
; keyToLCD: gibt die gedrückte Taste als ASCII auf dem LCD aus
; in: - (keyIn)
; out: -
; keyToSeg: gibt die gedrückte Taste auf der 7-Segment-Anzeige aus
; in: - (keyIn)
; out: -
; keyToVal: gibt die gedrückte Taste als Wert im Akku zurück
; in: - (keyIn)
; out: TastenWert ($0...$F) in Akku
; ------------------------------------------------------------------------------------------
keyToLCD: ; --- gibt die gedrückte Taste als ASCII auf dem LCD aus
phx ; Register X auf Stack retten
lda #1 ; ist ein zeichen
sta lcdrs
; ASCII-Zeichen für Key-Muster holen ("0"..."9" und "A"..."F")
ldx keyIn
lda KEY_ASC, x
jsr lcdByte
plx ; Register X wieder herstellen
rts
keyToSeg: ; --- gibt die gedrückte Taste auf der 7-Segment-Anzeige aus
phx ; Register X auf Stack retten
lda #1 ; ist ein zeichen
sta lcdrs
; Wert für Key-Muster holen ($0...$F bzw. 0 bis 15)
ldx keyIn
lda KEY_VAL, x ; erst das Key-Bitmuster zuordnen
tax
lda SEG, x ; dann dafür das Segment-Muster holen
sta SEGR
plx ; Register X wieder herstellen
rts
keyToVal: ; --- gibt die gedrückte Taste als Wert im Akku zurück
phx ; Register X auf Stack retten
lda #1 ; ist ein zeichen
sta lcdrs
; Wert für Key-Muster holen ($0...$F bzw. 0 bis 15)
ldx keyIn
lda KEY_VAL, x ; erst das Key-Bitmuster zuordnen
plx ; Register X wieder herstellen
rts
getKey: ; --- Wartet auf Tastendruck (+loslassen) und schreibt Tasten-Muster nach keyIn
phx ; Register X auf Stack retten
; oberen 4 Bits Port A Output, unteren 4 Bits Port Input
; standardmäßig sind die Leitungen auf High, wir müssen also auf low prüfen
lda #%11110000
sta VIA_DDRA
getKey2:
lda #%11101111
sta keyOut
ldx #4 ; 4 Output-Leitungen nacheinander mit LOW versorgen...
nextKeyOut:
lda keyOut
sta VIA_PORTA
; und schauen, ob das bei einer der 4 Input-Leitungen ankommt...
lda VIA_PORTA
sta keyIn
; ein unteres Bit auf 0? Dann Taste gedrückt
and #%00001111
cmp #$0F ; $0F = Standardwert = nichts gedrückt
bne getKeyDone ; es wurde etwas gedrückt
rol keyOut ; bits eins nach links schieben = nächste Leitung
dex
beq getKey2 ; wenn 4x durch, dann nochmal von vorne
bra nextKeyOut
getKeyDone: ; Ergebnis steht in keyIn
; warten, bis die Taste wieder losgelassen wurde
loslassen:
NOP
NOP
NOP
NOP
NOP
lda VIA_PORTA
and #%00001111
cmp #$0F ; $0F = Standardwert = nichts gedrückt
bne loslassen
plx ; Register X wieder herstellen
rts ; gedrückte Taste steht in keyIn
Das Alles kann ich jetzt komfortabel in meinem Hauptprogramm nutzen, um Tastendrücke entgegenzunehmen und auszugeben.
7-Keypad-Test.asm (klicken, um diesen Abschnitt aufzuklappen)
; Meldung auf DOGM163-LCD ausgeben und Tastendrücke entgegennehmen und anzeigen
; last edit: Oliver Kuhlemann (www.cool-web.de), 2020-09-02
CHIP W65C02S ; (default)
ORG $8000 ; Programmstartadresse (von der CPU aus gesehen)
; --- Konstanten: Timing ---
; 200 nF Kondensator von 7 Hz bis 2 KHz
; Ein Takt sind 0.5 ms, ein NOP sind zwei Takte sind 1 ms
NopsPerMs: equ 1 ; wieviele NOPs braucht es, um eine Millisek. voll zu machen?
INCLUDE io-defs.inc
INCLUDE zeropage-defs.inc
INCLUDE ram-defs.inc
init:
; LED-Ausgabe löschen
lda #0
sta LED
; Segment-Ausgabe löschen
lda #$FF
sta SEGL
sta SEGM
sta SEGR
init_LCD:
; Reset an LCD (Bit 0 auf Low für ein paar ms)
lda #0
sta LCD
lda #5
jsr delay
; wieder auf high und min 50 ms warten
lda #1
sta LCD
lda #10
jsr delay
jsr lcdInit
start:
ldx #0
eingabeaufforderung:
print:
ldy MSG1,x ; X. Buchstaben aus Message in Y-Register laden
beq eingabe
lda #1 ; ist ein zeichen
sta lcdrs
tya
jsr lcdByte
inx
bra print
eingabe:
; Cursor blinken lassen
lda #0 ; ist ein Befehl
sta lcdrs
lda #opLCD_CursorOn
jsr lcdByte
keypadEingabe:
jsr getKey
jsr keyToLCD ; auf LCD ausgeben
jsr keyToSeg ; auf 7-Segment-Anzeige ausgeben
; Wert holen und auf LED diesen auf LED ausgeben
jsr keyToVal ; liefert in Akku zurück
sta LED
bra keypadEingabe ; und das Spielchen wieder von vorne
stop:
STP ; Hält die CPU an
; --- Unterprogramme ---
delay: ; 1 delay ca. 5 ms
NOP ; Zeit verschwenden, NOP = 2 Taktzyklen
NOP
dec A
bne delay
rts
; für LCD
INCLUDE lcd.inc
; für 4x4 Keypad
INCLUDE key.inc
; --- im ROM abgelegte Konstanten
MSG1:
; 1234567890123456 Spaces am Ende gelten auch, auch wenn man sie nicht sieht
ASCII Dr
BYTE lcd_ue
ASCII cke eine
ASCII Taste:
BYTE 0
; Standard-Definitionen für IO-Geräte
INCLUDE seg-defs.inc
INCLUDE lcd-defs.inc
INCLUDE key-defs.inc
; --- Sprungvektoren
ORG $FFFA
word $8000 ; BRK/IRQ bei $FFFA/B
word $8000 ; Programmstartadresse bei $FFFC/D
word $8000 ; NMI bei $FFFE/F
Im an- und abschließenden Video gehe ich noch einmal auf die neuen Assembler-Programmteile ein und teste das Ganze, ob die Ausgabe der eingegebenen Zeichen auch gut funktioniert:

Ausblick
Der nächste Teil wird wohl wieder ein Software-only-Part. Ich will mit den jetzt gegebenen Möglichkeiten (Eingabe von der Tastatur, Ausgabe auf mehrere IO-Geräte) etwas halbwegs sinnvolles programmieren, vielleicht ein kleines Spielchen.Ich habe bei der Entwicklung des Spielchens bemerkt, dass man doch mehr Fehler beim Assembler Programmieren macht, als einem lieb ist. Zu schnell schleicht sich ein Fehler ein. Darum habe ich zuerst ein weiteres Debug-Instrument eingebaut.