Der You-Tube Kanal von Ben Eater befasst sich mit Grundlagen der Digital- und Computertechnik. Insbesondere der Beitrag „Hello, world” from scratch on a 6502 hat mich inspiriert, in dieser Richtung meine eigenen Experimente zu machen.
Im folgenden werden einige Teil-Projekte vorgestellt, die rund um das Thema entwickelt wurden. Es handelt sich somit um eine Sammlung von Einzelteilen und nicht um ein in sich geschlossenes Projekt bzw. einer solchen Beschreibung.
Inhalt
Funktionsweise des 6502 Prozessors
Clock Baustein
EEPROM Programmierung
Arduino als Adress- und Daten-Monitor
6502 Assembler
Ein und Ausgabe von Daten auf den Daten- und Adressbus
HexDump Routine
EEPROM AT28C256 auslesen
Funktionsweise des 6502 Prozessors
Der 6502 hat Computer Geschichte geschrieben. In den Anfängen waren viele der ersten Computer ( Apple II, Commodore PET, KIM, etc)mit diesem Prozessor ausgestattet. Er ist heute noch erhältlich allerdings in einer leicht modifizierten Form. Das Western Design Center hat den Prozessor mit der Bezeichnung W65C02S im Programm. Auf der Webseite sind alle Details sowie das Datenblatt verfügbar.
Eine der wichtigsten Änderungen ist die Möglichkeit den Prozessor nun mit unterschiedlichen Taktraten weit unter 1 MHz zu betreiben. Was ihn für Maker-Anwendungen und natürlich auch für Ausbildungszwecke sehr geeignet macht.
Der Prozessor verfügt über einen aus heutiger Perspektive sehr einfachen Aufbau.
Er besteht aus 6 Register und verfügt über einen überschaubaren Befehlssatz von 69 Instruktionen. Es gibt heute noch eine Vielzahl von Dokumenten wie Beispielsweise das geniale Monitor und Assemblerprogram von Steve Wozniak, das er für den Apple 1 entwickelt hat. Ebenso sind noch Implementierungen der damaligen BASIC Versionen erhältlich, so dass man sich leicht eine Replik aéines der klassischen Computer Modellen nachbauen kann.
Funktionsweise
Die Funktionsweise des Prozessors kann wie folgt beschrieben werden: Nach einem Reset-Signal wenn eine positive Flanke am Pin 40 erkannt wird, gibt es eine Reset-Sequenz, die sieben Taktzyklen dauert. Der Programmzähler wird mit dem Rücksetz-Vektor von den Plätzen FFFC (niedriges Byte) und FFFD (hohes Byte) geladen. Im nächsten Takt, wird dann an diese Adresse gesprungen. Dies ist die Startposition für die Programmsteuerung. Der Programmzähler wird mit dieser Adresse geladen und beginnt dann mit der Programmausführung.
Ich habe dies in meinem Versuchsaufbau nachgebildet. Er besteht aus einem Arduino Mega und einer Lochrasterplatine auf der neben dem 6502 Prozessor nur ein manueller Taktgeber (Monoflop), der Rest-Taster und ein Steckverbinder mit dem Arduino-Port verbaut ist.
Der Datenbus ist mit dem Wert 0xEA = Op-Code für No Operation durch Widerstände vorgelegt, damit etwas sinnvolles anliegt.
Wie man auf der nebenstehenden Abbildung erkennt, findet man in den ersten 6 Takten nach dem Reset keine brauchbaren Daten. Ab dem 7. Takt wird aber zur Adresse 0xFFFc bzw. 0xFFFd gesprungen und die Einsprungadresse für den Programmstart bestimmt. In meinem Beispiel habe ich die Werte 0x00 und 0x80 eingegeben, und somit die Adresse 0x8000 definiert. An dieser Adresse beginnt dann die Programmausführung.
In der Praxis würde man hier an einen Bereich des adressierbaren Speichers verweisen, in dem der abzuarbeitende Programmcode abgelegt ist.
Clock-Baustein
Normalerweise arbeitet der Mikroprozessor mit Taktfrequenzen im MHz Bereich. Um aber etwas genauer verfolgen zu können, wie der Prozessor arbeitet, ist es sinnvoll eine geringere Taktzeit vorzusehen. Dazu werden auf Basis des NE555 Timer IC ein Stabiler und ein Monostabiler Multivibrator gebaut. Mit dem Monoflop Modul lassen sich dann einzelne Schritte ausführen, während der astabile Bruder für eine kontinuierliche Impulsfolge zwischen 1 und 5 Hz (je nach Auslegung des RC Glieds) sorgt.
Die Schaltung ist in folgender Graphik dargestellt:
EEPROM Programmierung
Wenn man mit einem Microcomputer aus der Frühzeit der Computertechnik arbeitet, bietet es sich an, anstelle der EPROMS heute verfügbare EEPROMS zu verwenden. Diese lassen sich bequem mit Hilfe eines entsprechenden Programmiergerätes flexibel Programmieren.
Ich verwende dazu ein USB Programmiergerät der Firma BATRONIX. Meine Version ist schon 20 Jahre alt, verrichtet seinen Dienst aber nach wie vor tadellos und ist auch mit der neusten Version der Programmierumgebung „ProExpress“ einsetzbar. Der USB Chip Programmer (und seine erhältlichen Nachfolger) ist ein besonders flexibler und einfach zu handhabender Eprom Programmer mit umfangreicher Unterstützung für Eproms, EEproms, Flash und weitere Speicherchips. Die besondere Flexibilität wird durch eine komplette Versorgung über den USB Port erreicht. Ein Netzteil oder Batterien werden nicht benötigt, alle Programmierspannungen zwischen 3 und 25 Volt werden intern aus der USB Spannung über Ladungspumpen generiert.
Generierung der EEPROM Programmier-Informationen
Natürlich kann man versuchen die EEPROMS auch von Hand zu programmieren, aber weit sinnvoller ist es, dies natürlich mit Hilfe eines Programms zu machen, das die entsprechenden Daten in eine Datei schreibt, die dann von der Programmiersoftware eingelesen und auf den Chip geschrieben wird.
Als erstes soll ein Python Programm vorgestellt werden, die diese Aufgabe übernimmt. Der Aufbau ist ziemlich übersichtlich. Zuerst wird ein Array definiert, das der Größe des EEPROM entspricht und dann mit Daten gefüllt. Einzelne Stellen können dann noch manuell eingetragen werden. Anschliessend wird das Ganze dann als Datei mit dem Namen „rom.bin“ ausgegeben.
rom = bytearray ([0xEA]*32768) rom[0x7FFC] = 0x00 rom[0x7FFC] = 0x80 with open ("rom.bin","wb") as out_file:out_file.write(rom)
Natürlich kann auch jede andere Programmiersprache verwendet werden. Ich verwende üblicherweise mein Mathematica bzw. die Wolfram Language für derartige Programmieraufgaben. In Mathematica würde das Problem wie folgt lösen:
Nach dem das File dann über die Programmer-Software auf den EEPROM übertragen wurde, sieht das im Tool dann etwa so aus:
Arduino als Adress- und Daten-Monitor
Der Arduino Mega mit seinen 54 Pins biete sich an, um damit ein Monitor-Programm für den Adress- und Datenbus und andere relevante Datenleitungen des Mikroprozessors zu verwenden. Das folgende Programm bildet die Basis. Mit Hilfe einer Interrupt Routine werden bei jedem Takt-Impuls die 16-Adress- und 8-Daten-Bits ausgelesen und dargestellt.
// ******************************************* // Monitor Programm um Daten und Adress Bits // mit Arduino Mega auszulesen // nach Ben Eater modifiziert von // ProfHof Sept 2022 // Version 1.0 // ******************************************* const char ADDR[] = {22, 24, 26, 28, 30, 32, 34, 36, 40, 42, 44, 46, 47, 49, 51, 53}; const char DATA[] = {14, 15, 16, 17, 18, 19, 20, 21}; #define CLOCK 2 #define READ_WRITE 3 void setup() { for (int n = 0; n < 16; n += 1) { pinMode(ADDR[n], INPUT); } for (int n = 0; n < 8; n += 1) { pinMode(DATA[n], INPUT); } pinMode(CLOCK, INPUT); pinMode(READ_WRITE, INPUT); // sobald clock impulse am Eingang 2 dann wird // Interrupt routinbe "Clock" aufgerufen attachInterrupt(digitalPinToInterrupt(CLOCK), onClock, RISING); Serial.begin(57600); } void onClock() { char output[15]; unsigned int address = 0; for (int n = 0; n < 16; n += 1) { int bit = digitalRead(ADDR[n]) ? 1 : 0; Serial.print(bit); // Umwandlung in ein Integerwert address = (address << 1) + bit; } Serial.print(" "); unsigned int data = 0; for (int n = 0; n < 8; n += 1) { int bit = digitalRead(DATA[n]) ? 1 : 0; Serial.print(bit); // Umwandlung in ein Integerwert data = (data << 1) + bit; } // Ausgabe hat das Format: // 16bit Adresse __ 8Bit Data ___ HexAdr __ R/W __ HexData sprintf(output, " %04x %c %02x", address, digitalRead(READ_WRITE) ? 'r' : 'W', data); Serial.println(output); } void loop() { }
Die Ausgabe sieht dann beispielsweise wie folgt aus:
6502 Assembler
Natürlich kann man kleinere Programme „von Hand“ wie oben gezeigt schreiben. Aber ein Assembler erleichtert die Arbeit schon deutlich. Im Beitrag von Ben Eater wird der Assembler „VASM“ von Volker Bartelmann und Frank Wille eingesetzt. http://sun.hasenbraten.de/vasm/
Das Besondere an diesem Assembler ist, dass man ihn für nicht kommerzielle Zwecke frei nutzen darf und es bereits lauffähige Binaries für den Mac gibt. Natürlich stehen auch Versionen für Windows und Linux Betriebssysteme zur Verfügung.
Die Benutzung erfolgt rein über die Kommandozeile und bedarf daher etwas Übung. Es gibt verschiedene Möglichkeiten den Assembler zu konfigurieren. Im folgenden wird beschrieben, wie man den „VASM“ nutzt, um aus einer Datei in der ein 6502-Assembler Programm steht, den Maschinencode für den Eprom-Programmer zu erzeugen.
Beispiel für blinkende LEDs als 6502 Assembler Programm :
.org $8000 reset: lda #$ff sta $6002 lda #$50 sta $6000 loop: ror sta $6000 jmp loop .org $fffc .word reset .word $0000
Diese Datei (mit dem Namen blink.s) wird nun dem Assembler übergeben, und diesem mitgeteilt, dass man Binärcode (option -Fbin) erwartet und mnemonic Anweisungen (wie .org, etc.) berücksichtigt haben will (-dotdir). Der Assembler generiert daraus dann das File a.out, das man mit den Befehlen „cut“ und „hexdump“ anschauen kann. In der Kommandozeile sieht das dann etwa so aus:
MBP-II-2018:mac phof$ ./vasm6502_oldstyle -Fbin -dotdir blink.s vasm 1.8f (c) in 2002-2019 Volker Barthelmann vasm 6502 cpu backend 0.8 (c) 2002,2006,2008-2012,2014-2018 Frank Wille vasm oldstyle syntax module 0.13f (c) 2002-2018 Frank Wille vasm binary output module 1.8a (c) 2002-2009,2013,2015,2017 Volker Barthelmann seg8000(acrwx1): 17 bytes segfffc(acrwx1): 4 bytes MBP-II-2018:mac phof$ cat a.out ???`?P?`j?`L ??MBP-II-2018:mac phof$ hexdump -C a.out 00000000 a9 ff 8d 02 60 a9 50 8d 00 60 6a 8d 00 60 4c 0a |....`.P..`j..`L.| 00000010 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00007ff0 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 |................| 00008000 MBP-II-2018:mac phof$
Der Inhalt dieser Datei lässt sich dann – wie oben beschrieben – in einen EEPROM programmieren und zur Steuerung des Mikroprozessors verwenden. Schematisch lässt sich das wie im nachfolgenden Schaubild darstellen:
Ein und Ausgabe von Daten auf den Daten- und Adressbus
Der Arduino Mega bietet sich mit seinen vielen Ein- und Ausgängen an, nicht nur Daten zu lesen sondern diese auch zu schrieben. Am einfachsten geht das über die Port-Adressen. Diese haben nur das Problem, dass sie nicht in einer adäquaten Reihenfolge [D0…D7] bzw. [A0…A15] an den Steckerleisten angreifbar sind.
Ein Blick auf die interne Pin-Belegung zeigt, dass sich für diese Zwecke die Ports
PORTL = Data (Pin 42…49)
PORTA = Address LSB (Pin29 … Pin 22)
PORTC = Address MSB (Pin30 … Pin37)
idealerweise einsetzen lassen.
Anbei ein kurzes Test Programm, das eine Möglichkeit vorstellt, wie die Datenverarbeitung aussehen könnte:
// test Programm Daten Handling Arduino MEGA // PORTL = Data // PORTA = Address LSB // PORTC = Address MSB /* * Schreibt bzw liest Daten über den Port L * und gibt diese zur Kontrolle am Monitor aus * Ziel: Daten Lesen und Schreiben auf den Daten * und Adressbus des 6502 zu simulieren */ unsigned int data = 255; uint16_t address = 0x0000; #define CLOCK 2 void setup() { Serial.begin(115200); // Interrupt routine "Clock" aufgerufen attachInterrupt(digitalPinToInterrupt(CLOCK), onClock, RISING); } void onClock() { char output[15]; //writeData(data); //data = readData(); writeAdr(address); /* Serial.println("--------------------------------------"); sprintf(output, " %04x %02x", address, data); Serial.println(output); Serial.println("--------------------------------------"); //data = data -1; */ address = address+1; } void loop() { // put your main code here, to run repeatedly: } void writeData(int d) { // write to Pins DDRL = 0xFF; PORTL = data; // Serial.print("Data to Port L ----------->"); Serial.println(data); }// end wD byte readData() { // read from Pins int d =0; DDRL = 0x00; d = PINL; Serial.print("Data from Port L ----------->"); Serial.print(d,HEX); Serial.print(" dec: ");Serial.print(d,DEC); return d; } //end rD void writeAdr(uint16_t adr) { // write adr to Pins DDRA = 0xFF; // Address = OUTPUT DDRC = 0xFF; // Address = OUTPUT uint16_t Adresse = adr; byte LoByte = (Adresse & 0x00FF); byte HiByte = ((Adresse & 0xFF00) >>8); PINA = LoByte; // Schreibe Adresse an PortPins PINC = HiByte; Serial.print( "High Adresse: " ); Serial.println( Adresse, HEX ); Serial.print( "Hi Byte: " ); Serial.print ( HiByte, HEX ); Serial.print(" ... B : " ); Serial.println( HiByte, BIN ); Serial.print( "Lo Byte: " ); Serial.print ( LoByte, HEX ); Serial.print(" ... B : " ); Serial.println( LoByte, BIN ); Serial.println(); }// end wD
Hexdump -Routine
Bei der Arbeit mit Mikroprozessoren ist es immer wieder hilfreich bestimmte Speicherbereiche übersichtlich auszugeben. Dazu verwendet man sogenannte Hexdumps. Im folgenden ein Beispiel wie sowas implementiert werden kann:
// HEXDUMP for memory unsigned char memory[0x0fff]; // 4K of memory mapped to 0xF000..0xFFFF void setup() { Serial.begin(115200); for (int n=0; n < 0x0fff; n++) { memory[n] = 0xEA; // NOP } // .org 0xF000 // reset: memory[0x0000] = 0xA9; // LDA memory[0x0001] = 0x00; // #$00 // loop: memory[0x0002] = 0x1A; // INC // storing and loading A to valid memory allows us to see it on the data bus in diagnostics memory[0x0003] = 0x8D; //STA memory[0x0004] = 0x00; memory[0x0005] = 0xF1; memory[0x0006] = 0xAE; //LDX memory[0x0007] = 0x00; memory[0x0008] = 0xF1; memory[0x0009] = 0x4C; // JMP loop memory[0x000A] = 0x02; memory[0x000B] = 0xF0; // .org 0xFFFC memory[0x0FFC] = 0x00; // reset memory[0x0FFD] = 0xF0; DDRL = 0x00; // Data = INPUT DDRA = 0x00; // Address = INPUT DDRC = 0x00; // Address = INPUT Serial.println(); Serial.println("----------------------------------------------------------"); Serial.println(" * HEX-Dump * " ); Serial.println("----------------------------------------------------------"); hexDump(0,0x100); } // end setup void hexDump(int startAdr, int endAdr) { for (unsigned int adr = startAdr; adr <= endAdr; adr++) { int a = memory[adr]; // lese adressSpeicher if ( (adr == 0) || (adr % 16 == 0)) Serial.print ( " " + int2hex(adr, 4) + " : "); // Zeige Speicheradresse an Serial.print (" " + int2hex(a, 2) + " "); // Zeige Daten-Byte an if( (adr + 1) % 16 == 0) Serial.println(); // Zeilenvorschub }// end for }//hexDump String int2hex(int wert, int stellen) { // int -› hex-String Konvertierung mit der Angabe der Stellen String temp = String(wert, HEX); String prae = ""; int len = temp.length(); // Die Länge der Zeichenkette ermitteln int diff = stellen - len; for(int i = 0; i < diff; i++) prae = prae + "O"; // Führende Nullen erzeugen return prae + temp; // Führende Nullen + Ergebnis zurückliefern } // end int2he void loop() { }
Die Routinen ergeben dann folgendes Ergebnis auf dem Monitor des Arduino:
EEPROM AT28C256 auslesen
Das Auslesen eines EPROMs bzw. eines EEPROMS ist eine zentrale Aufgabe, wenn man mit Mikroprozessoren wie dem 6502 oder dem Z80 experimentiert. Hierzu ist ein entsprechendes Programmiergerät natürlich sehr hilfreich, wenn man den Baustein nicht gänzlich von „Hand“ mit Daten füllen möchte.
Im vorliegenden Beispiel gehen wir davon aus, dass ein programmiertes EEPROM vom Typ AT28C256 vorliegt, dessen Daten mit dem Arduino Mega ausgelesen werden sollen. Der Versuchsaufbau sieht wie folgt aus:
Wie man erkennt ist der Anschluss relativ einfach: Daten-Pins kommen an den Port L des Arduino Mega und die Adress-Pins ebenso an die oben im Beitrag schon beschriebenen Ports A und Port C. Damit wird die Programmierung auf die Port-Manipulations-Funktionen des Arduino beschränkt, die das Ein- und Ausgeben der Daten sehr komfortabel umsetzen lassen.
Wichtig ist noch dass die Anschlüsse OE und CE auf Masse gelegt werden, d.h. aktiv Low sind. Ebenso muss der WE-Pin auf +5V gelegt werden. Das Programm kann dann so aussehen:
/*EEPROM Ansteuerung LESE Daten aus EEPROM Alternativ als hexdump oder Step by step */ #define CLOCK 2 unsigned int data = 0; uint16_t address = 0x0000; int staADR = 0x00; int endADR = 0xff; void setup() { Serial.begin(115200); pinMode(CLOCK, INPUT); DDRA = 0b11111111; // Address = Output DDRC = 0b11111111; // Address = Output DDRL = 0x00; // Data = Input // hexDump(staADR, endADR); attachInterrupt(digitalPinToInterrupt(CLOCK), onClock, RISING); } void loop() { // put your main code here, to run repeatedly: } void onClock() { char output[15]; // wandle address in 16 bit word byte LoByte = (address & 0x00FF); byte HiByte = ((address & 0xFF00) >>8); // Schreibe Adresse an PortPins PORTA = LoByte; PORTC = HiByte; // lese Daten von portPins data = PINL; Serial.println("-------------------------------------------------"); sprintf(output, " %04x %02x", address, data); Serial.println(output); //Serial.println("-----------------------------------------------"); // next address address = address+1; }// end onClock void hexDump(int startAdr, int endAdr) { Serial.println(); Serial.println("------------------------------------------------------------------------"); Serial.println(" * HEX-Dump * " ); Serial.println("------------------------------------------------------------------------"); for (unsigned int adr = startAdr; adr <= endAdr; adr++) { int a = adr; // wandle addresse adr in 16 bit word byte LoByte = (adr & 0x00FF); byte HiByte = ((adr & 0xFF00) >>8); // Schreibe Adresse an PortPins PORTA = LoByte; PORTC = HiByte; // lese Daten von portPins data = PINL; if ( (adr == 0) || (adr % 16 == 0)) Serial.print ( " " + int2hex(adr, 4) + " : "); // Zeige Speicheradresse an Serial.print (" " + int2hex(data, 2) + " "); // Zeige Daten-Byte an if( (adr + 1) % 16 == 0) Serial.println(); // Zeilenvorschub }// end for }//hexDump String int2hex(int wert, int stellen) { // int -› hex-String Konvertierung mit der Angabe der Stellen String temp = String(wert, HEX); String prae = ""; int len = temp.length(); // Die Länge der Zeichenkette ermitteln int diff = stellen - len; for(int i = 0; i < diff; i++) prae = prae + "O"; // Führende Nullen erzeugen return prae + temp; // Führende Nullen + Ergebnis zurückliefern } // end int2he
Das Programm kann entweder über einen Taster gesteuert werden…in diesem Fall wird die Interrupt-Routine verwendet und der Eingangs-Impuls an Pin2 des Arduino Mega geleitet. So kann man im Einzelschritt sehen welche Adresse an den EEPROM angelegt wird und welche Daten ausgelesen werden.
Alternativ habe ich die Möglichkeit vorgesehen einen ganzen Speicherbereich über die entwickelte Hexdump-Funktion auszugeben. Hierzu ist der Interrupt zu deaktivieren und die Start- und Endadresse anzugeben.