Kampis Elektroecke

Inbetriebnahme des Prozessors

Bei dem W65C02 handelt es sich um einen Mikroprozessor, also um eine CPU ohne Speicher oder sonstigen Peripherieeinheiten, wie man sie ggf. von einem Mikrocontroller kennt. Um den Mikroprozessor in Betrieb nehmen zu können werden also mindestens ein zusätzliches RAM und ein EEPROM als Programmspeicher benötigt.

Grundbeschaltung des Prozessors:

Damit der Prozessor in Betrieb genommen werden kann, müssen die folgenden Signale beschaltet werden:

Pin Signal Beschreibung
2 RDY – Ready Ein Low-Pegel pausiert den Prozessor
4 IRQ – Interrupt Request Aktivierbarer (maskierbarer) Interrupteingang
6 NMI – Non-Maskable Interrupt Nicht maskierbarer Interrupt
8 Vcc Positiver Eingang der Spannungsversorgung
21 GND Negativer Eingang der Spannungsversorgung
36 BE – Bus Enable Ein High-Pegel aktiviert den Adress-, Daten- und RW-Buffer
36 SO – Set Overflow Wird genutzt um das Overflow-Bit im Status-Register zu setzen
37 PHI2 – Phase 2 In Eingang vom Oszillator
40 RESET Reset-Signal

Die Beschaltung des Prozessors sieht somit folgendermaßen aus, wobei die Taktfrequenz für den Prozessor dank des statischen Designs fast beliebig gewählt werden kann:

Der Prozessor kann jetzt bereits in Betrieb genommen werden. Dazu wird das Bitmuster 0xEA an die Datenleitungen des Prozessors angelegt und der Prozessor mit Spannung versorgt. Es lässt sich nun wunderbar beobachten, wie der Prozessor alle zwei Taktzyklen die Adresse um eins erhöht.


Hinweis:

Über das Signal SYNC lässt sich erkennen, wann der Prozessor eine neue Instruktion einließt. Ein Wechsel auf High während eines Taktimpulses signalisiert einen neuen OpCode-Fetch, also das Einlesen eines neuen Prozessorbefehls.


Aktuell ist der Prozessor aber noch nicht voll funktionsfähig, da noch keine Möglichkeit existiert den Prozessor mit Befehlen zu füttern oder Daten zu speichern. Hierfür besitzt der Prozessor einen 8-Bit breiten Datenbus, einen 16-Bit breiten Adressbus und ein RW-Signal, die über I/Os zur Verfügung stehen und für die Adressierung der Speicher und der Peripherie, wie ein EEPROM doer ein SRAM, genutzt werden.

Über das 74HC00-NAND Gatter wird die Adresslogik für das SRAM und das EEPROM umgesetzt um den verfügbaren 16-Bit Adressraum aufzuteilen.

Adressbereich Größe Beschreibung
0x0000 – 0xFFFF 64K Zur Verfügung stehender Speicher
0x0000 – 0x7FFF 32K SRAM
0xA000 – 0xFFFF 24K Programmspeicher

Während der Low-Phase vom Taktsignal können sich die Signale am Adressbus, am Datenbus und am RW-Pin beliebig ändern und ein Wechsel von Read auf Write würde ggf. zu fehlerhaften Daten im SRAM führen. Daher wird das NAND-Gatter zusätzlich genutzt um das SRAM mit dem Takt zu synchronisieren.

Für die Entwicklung eigener Programme wird ein 6502-kompatibler Compiler und ein EEPROM-Programmiergerät benötigt. Ich verwende in diesen Artikeln die folgenden Tools:

  • TL866A EEPROM Programmer
  • cc65 als 6502 kompatible Toolchain

Das zu erstellende Programm hat die Aufgabe, zwei Werte aus dem Programmspeicher zu laden, diese zu Addieren und das Ergebnis in das SRAM zu kopieren. Für die Erstellung eines ausführbaren Programmcodes, werden zudem einige weitere Dateien benötigt, die der Reihe nach erstellt werden:

  • Ein Linkerskript, damit der Linker weiß wie er den Programmcode verteilen soll
  • Das Programm als Assemblerdatei
  • Ein Makefile um das Kompilieren zu vereinfachen

Die Aufteilung des adressierbaren Speichers wird in einem Linkerskript erfasst und dient dem Linker als Anleitung für die Zuweisung einzelner Abschnitte des Programmcodes. Während der Erstellung der Software können bestimmte Codeteile durch die Direktive .segment <Segment> markiert werden um sie in bestimmte Speicherbereiche zu platzieren.

Bestimmte Befehle, wie z. B. Sprungadressen bei Interrupts, müssen an eine ganz spezielle Stelle im Speicher geschrieben werden, damit diese Befehle bei einem Interrupt und dem damit verbundenen Adresswechsel korrekt angesprungen werden.


Info:

Die Sprungadresse für einen Interrupt wird Interruptvektor und eine Sammlung aus Interruptvektoren wird Interruptvektortabelle (IVT) gennant. An jedem Interruptvektor befindet sich eine Sprungadresse zu einer Interrupt Interrupt-Service-Routine (ISR). Interruptvektortabellen müssen immer an einer prozessorspezifischen Position im Speicher platziert werden.


Der Mikroprozessor besitzt insgesamt drei verschiedene Interruptvektoren, die bei einem Power-On Reset, einem Non-Maskable-Interrupt oder bei einem Hardware-Interrupt am IRQ-Pin ausgegeben werden. 

Adresse Interruptquelle
0xFFFE, 0xFFFF BRK (Software), IRQ (Hardware)
0xFFFC, 0xFFFD RESET
0xFFFA, 0xFFFB NMI

Bei jedem Interrupt wechselt der Mikroprozessor innerhalb von sieben Taktzyklen in einen Interruptzustand und gibt dabei nacheinander die zwei 16-bit Adressen des jeweiligen Interruptvektors aus. Während der Prozessor die jeweilige Adresse des Interruptvektors ausgibt, ließt er den Zustand des Datenbus ein. Aus den beiden eingelesenen Datenbytes ergibt sich dann die Adresse der ISR.

Adressbus      Datenbus  R/W
0xffff :       0x00      R
0xa00b :       0x4c      R
0x02ec :       0xa1      R
0x02eb :       0x9e      R
0x02ea :       0xf3      R
0xfffc :       0x00      R
0xfffd :       0xa0      R
0xa000 :       0x00      R

Der Mikroprozessor ist mittels eines Resets zurückgesetzt worden. Nach sechs Taktzyklen gibt er die Adresse 0xFFFC am Adressbus aus und ließt mit dieser Adresse den Wert 0x00 aus dem EEPROM aus. Anschließend legt der Mikroprozessor die Adresse 0xFFFD an den Adressbus an und ließt erneut das EEPROM aus und dieses mal gibt das EEPROM 0xA0 aus. Aus diesen beiden Datenbytes konstruiert der Prozessor dann die Adresse 0xA000 für die ISR und legt die Adresse auf den Adressbus. Mit der Adresse 0xA000 wird das EEPROM ausgewählt und das an der Adresse 0x2000 gespeicherte Datenbyte auf den Datenbus gelegt.

Die verwendete Speicheraufteilung und die IVT sollen nun in einem Linkerskript festgelegt werden. Dazu wird zuerst eine neue Datei names W65C02.ld erstellt (wobei der Name und die Endung theoretisch egal sind). In dieser Datei wird nun eine neue Memory Map erstellt:

MEMORY {
	RAM:
		start = $0000,
		size = $8000;
	ROM:
		start = $A000,
		size = $6000;
}

Diese Memory Map definiert zwei nutzbare Speicherbereiche, die RAM und ROM genannt werden. Für jeden Speicherbereich wird eine Startadresse und die Größe des Speicherbereichs angegeben. Über eine Segment Map werden die einzelnen Codeabschnitte dann den entsprechenden Speicherbereichen zugeordnet.

SEGMENTS {
	CODE:
		load = ROM,
		type = ro;
	VECTORS:
		load = ROM,
		type = ro,
		start = $FFFA,
		optional = yes;
}

Die einzelnen Speichersegmente müssen auch wieder mit entsprechenden Eigenschaften beschrieben werden.

Eigenschaft Funktion
load Legt den Speicherbereich fest, in den dieses Codesegment geladen wird.
type Legt die Attribute für dieses Segment fest.

  • ro – Read only
  • rw – Read/Write
  • bss – Unititialisiertes Segment
  • zp – Zero Page
start Startadresse im zugewiesenen Speicherbereich.
optional Markiert das Segment als optional.

Für das Programm wird zuerst eine neue Datei angelegt und die IVT definiert, wobei in diesem Beispiel erst einmal nur der Reset-Vektor benötigt wird:

.segment "VECTORS"
.word 	$00
.word	__resetVec
.word	$00

Durch die .segment Direktive werden die nachfolgenden Befehle im Segment VECTORS, welches bei der Speicheradresse 0xFFFA beginnt, platziert. An der Speicheradresse 0xFFFC ist die Adresse des Labels __resetVect gespeichert. Die beiden anderen Interruptvektoren werden mit 0x00 gefüllt.

Der nachfolgende Programmcode wird im Segment CODE, welches bei der Speicheradresse 0xA000 beginnt, platziert. Das Label __resetVect markiert den Programmeinstiegspunkt da diese Adresse nach einem Reset direkt angesprungen wird.

.segment "CODE"
__resetVec:

Als nächstes die Operanden für den auszuführenden Befehl, in diesem Fall der Befehl adc für eine Addition, geladen werden. Insgesamt besitzt der Mikroprozessor zwei Register und einen Akkumulator, die für die Befehlsargumente genutzt werden können. Mit Hilfe der Opcode-Matrix, die dem Datenblatt des W65C02 entnommen ist, können die einzelnen Argumente für die verschiedenen Befehle bestimmt werden.

In diesem Fall interessiert uns der Opcode mit dem Wert 0x69. Bei dieser Implementierung ist das zweite Argument der adc Instruktion ein Immediate, also ein Wert aus dem Programmspeicher. Dieser Wert wird zum Inhalt des Akkumulators hinzuaddiert.

Der Akkumulator hingegen wird über den lda Opcode geladen, von dem auch eine Implementierung für einen Immediate vorhanden ist (Opcode 0xA9). Mit Hilfe des sta Opcodes wird der Inhalt des Akkumulators dann in das SRAM kopiert.

Bei dem sta Opcode kann entweder die Implementierung 0x85, also die Zero Page Indirect Variante, genutzt werden oder die Variante 0x8D, welche eine absolute Adresse erwartet. Der verwendete Opcode hängt i. d. R. von der angegebenen Adresse ab und wird vom Compiler entsprechend ausgewählt.

lda		#$02
adc		#$04
sta		$01

Hinweis:

Ein vorangestelltes # bezeichnet einen konstanten Wert der mit in den Programmspeicher geschrieben wird (Immediate). Wenn nichts vorangestellt wird, so handelt es sich automatisch um eine Adresse!


Das fertige Programm sieht nun folgendermaßen aus:

.segment "VECTORS"
.word 	$00
.word	__resetVec
.word	$00

.segment "CODE"
__resetVec:
	lda		#$02
	adc		#$04
	sta		$01

Jetzt fehlt nur noch das Makefile um das Programm zu kompilieren und zu linken.

TARGET = main

CPU = 65c02
INCLUDE = include
DEFINES = 
COMPILEFLAGS = 
LINKERFLAGS = -C W65C02.ld

BUILD_DIR = build
SOURCE_DIR = src
OBJECT_DIR = obj

OBJECT_FILES = $(patsubst $(SOURCE_DIR)/%.s, $(OBJECT_DIR)/%.o, $(wildcard $(SOURCE_DIR)/*.s))

.PHONY: all
all: Directories $(TARGET).bin

.PHONY: Directories
Directories: $(BUILD_DIR) $(OBJECT_DIR)

$(BUILD_DIR):
	mkdir -p $@

$(OBJECT_DIR):
	mkdir -p $@

$(TARGET).bin: $(OBJECT_FILES)
	ld65 $(LINKERFLAGS) $^ -o $(BUILD_DIR)/$@ -m $(BUILD_DIR)/$(TARGET).map -Ln $(BUILD_DIR)/symbols.txt

$(OBJECT_DIR)/%.o: $(SOURCE_DIR)/%.s
	ca65 --cpu $(CPU) $< -o $@ -I $(INCLUDE) $(COMPILEFLAGS) $(DEFINES) 

.PHONY: clean
clean:
	rm -rf $(BUILD_DIR)
	rm -rf $(OBJECT_DIR)

Wenn alle Dateien soweit vorbereit wurden, kann das Makefiles aufgerufen und das Programm kompiliert werden.

make

Am Ende erhält man eine Datei namens Addition.bin und eine Datei namens Addition.map. Die Datei Addition.bin erhält den vollständigen Programmcode und kann mit einem Hex-Editor (z. B. HxD) gelesen und kontrolliert werden.

00000000  A9 02 69 04 85 01 00 00 00 00 00 00 00 00 00 00  ©.i.…...........

Eine Zusammenfassung des Linkers ist in der Datei Addition.map gespeichert.

Modules list:
-------------
Addition.o:
    CODE              Offs=000000  Size=000006  Align=00001  Fill=0000
    VECTORS           Offs=000000  Size=000006  Align=00001  Fill=0000


Segment list:
-------------
Name                   Start     End    Size  Align
----------------------------------------------------
CODE                  00A000  00A005  000006  00001
VECTORS               00FFFA  00FFFF  000006  00001

Exports list by name:
---------------------

Exports list by value:
----------------------

Imports list:
-------------

Hier führt der Linker zum einen auf, welche Objektdateien er gelinkt hat und wie die einzelnen Segmente in den Objektdateien aufgebaut waren und zum anderen wo er welche Segmente platziert hat und wie groß die einzelnen Segmente sind.

Wenn alles korrekt ist, kann die Binärdatei in das EEPROM geschrieben werden. Hierbei ist wichtig, dass die Startadresse im EEPROM auf 0x2000 gesetzt wird, damit der Programmcode korrekt adressiert wird. Das beschriebene EEPROM wird dann eingesetzt und der Mikroprozessor mittels Reset zurückgesetzt, woraufhin der Mikroprozessor mit der Ausführung des Programms beginnt.

Adressbus OpCode  Datenbus R/W
0xffff :           0xff    R
0x3689 :           0x84    R
0x02f9 :           0x08    R
0x02f8 :           0x34    R
0x02f7 :           0xff    R
0xfffc :           0x00    R
0xfffd :           0xa0    R
0xa000 :   LDA     0xa9    R
0xa001 :           0x02    R
0xa002 :   ADC     0x69    R
0xa003 :           0x04    R
0xa004 :   STA     0x85    R
0xa005 :           0x01    R
0x0001 :           0x06    W

Damit wäre der Einstieg geschafft. Im nächsten Teil wollen wir uns dann etwas intensiver mit dem Speichermanagement des Prozessors auseinandersetzen.

Zurück

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert