Mittwoch, 19. Dezember 2012

SDCC: crt0 ersetzen

Wenn man mit SDCC eine Datei kompiliert, nach dem Motto
sdcc -mz80 test.c
so wird die vom Compiler mitgelieferte Datei crt0.rel mit reingelinkt. Diese bestimmt die Link-Reihenfolge, setzt Standardinterruptvektoren, sorgt für die Initialisierung globaler Variablen und so weiter.
Die so erzeugte ausführbare Datei beginnt daher an Adresse 0x0. Der selbstgeschriebene Code, der in der .c-Datei abgelegt ist, beginnt ab 0x100.
Nun möchte man nicht unbedingt immer die Interruptvektoren neu definieren, oder man möchte die Startadresse des eigenen Codes ändern.

Mit
sdcc -mz80 --code-loc 0x2010 test.c
verschiebt man den selbstgeschriebenen Code auf die Adresse 0x2010. Aber man hat immer noch die Interruptvektoren usw. ab Adresse 0x0. Dann lässt er ca. 0x2000 Byte Leerraum und dann kommt der eigene Code.
Man muss die crt0.rel mit einer eigenen ersetzen. Die von SDCC mitgelieferte crt0.s sieht (abgekürzt) so aus:


   .module crt0
 .globl _main
.area _HEADER (ABS)
;; Reset vector
.org 0
jp init
.org 0x08
reti
.org 0x10
reti
.org 0x18
reti
.org 0x20
reti
.org 0x28
reti
.org 0x30
reti
.org 0x38
reti
.org 0x100
init:
;; Stack at the top of memory.
ld sp,#0xffff
 ;; Initialise global variables
 call    gsinit
call _main
jp _exit
Die .org 0x8 bis .org 0x38 Einträge sind die Interruptvektoren. Ich möchte, dass jeglicher Code ab Adresse 0x2000 beginnt. Im Bereich von 0x0 bis 0x1FFF befindet sich nämlich mein Monitorprogramm, welches Programme, die an höheren Speicheradressen abgelegt wurden, ausführen kann.
Das .org 0 bestimmt, dass die crt0.s ab Adresse 0 beginnt. Das gilt es zu ändern.

Ich verwende daher eine modifizierte crt0.s:   

.module crt0
.globl _main
.area _HEADER (ABS)
;; Reset vector
.org 0x2000
jp init
init:
;; Initialise global variables
call gsinit
call _main
ret

;; Ordering of segments for the linker.
.area _HOME
.area _CODE
.area _GSINIT
.area _GSFINAL
.area _GSINIT
gsinit::
.area _GSFINAL
ret
.area _DATA
.area _BSEG
.area _BSS
.area _HEAP
Diese assembliere ich so:
sdasz80 -o crt0_$2000.rel crt0_$2000.s
Meine .c-Datei kompiliere ich nun so:
sdcc.exe -mz80 --code-loc 0x2010 --data-loc 0 --no-std-crt0 crt0_$2000.rel test.c
Die Angabe von --data-loc 0 weist den Linker an, das Datensegment direkt hinter das Codesegment zu platzieren. So weit, so gut.

So sieht die test.c aus:
void main(void) {
}
SDCC erzeugt daraus test.ihx. Dies ist die ausführbare Datei im Intel Hex Format. Die wandle ich mit hex2bin in eine Binärdatei um:
hex2bin.exe test.ihx
hex2bin v1.0.8, Copyright (C) 1998 Jacques Pelletier
checksum extensions Copyright (C) 2004 Rockwell Automation
improved P.G. 2007
Lowest address = 00002000
Highest address = 00002011
Pad Byte = FF
8-bit Checksum = 36
Die niedrigste Adresse ist 0x2000, die höchste 0x2011. In den ersten paar Bytes liegt die crt0.s, ab 0x2010 liegt die test.c. In diesem simpelsten Beispiel belegt test.c nur ein Byte (nämlich das return aus der main()).

Nun füge ich ein großes globales Array hinzu: 
char huge_array[4096];
void main(void) {
}
Und kompiliere neu und erzeuge wieder eine Binärdatei.

hex2bin.exe test.ihx
hex2bin v1.0.8, Copyright (C) 1998 Jacques Pelletier
checksum extensions Copyright (C) 2004 Rockwell Automation
improved P.G. 2007
Lowest address  = 00002000
Highest address = 00003011
Pad Byte        = FF
8-bit Checksum = 46
Die höchste Adresse ist nun 0x3011. Und das, obwohl der Array im RAM liegen sollte und nicht im Codesegment! Dies hat zufolge, dass alle globalen Variablen die mit SDCC kompilierten Dateien unnötig aufblähen.
Bei näherer Betrachtung scheint SDCC die _GSFINAL Sektion hinter das Datensegment zu hängen, obwohl es anders in crt0.s angegeben ist. Das wird zu Problemen führen, wenn man das Programm in einem EEPROM ausführen will.

Diesen SDCC-Fehler kann man umgehen. Man kompiliert test.c, linkt aber erst im zweiten Schritt:
sdcc -mz80 test.c -c
sdcc -mz80 --code-loc 0x2010 --data-loc 0 --no-std-crt0 crt0_$2000.rel test.rel -o test.ihx
hex2bin sagt nun:
hex2bin.exe test.ihx
hex2bin v1.0.8, Copyright (C) 1998 Jacques Pelletier
checksum extensions Copyright (C) 2004 Rockwell Automation
improved P.G. 2007
Lowest address = 00002000
Highest address = 00002011
Pad Byte = FF
8-bit Checksum = 36

Die höchste Adresse liegt nun wie gewünscht bei 0x2011.

Sonntag, 20. Mai 2012

CP/M 2.2

In der letzten Zeit habe ich mich wieder etwas mit CP/M beschäftigt. Vor einem Jahr ungefähr hatte ich auf meiner anderen Selbstbauplatine ja auch ein rudimentäres CP/M laufen. Nun habe ich die Arbeit wieder ausgegraben, das BIOS auf mein neues System angepasst und einen anständigen Bootloader programmiert. CP/M startet nun von der Compact Flash-Disk und kann nur-lesend auf ein 8 MB Dateisystem zugreifen.
CP/M 2.2 startet auf meinem Z80-System
Schreibzugriffe habe ich noch nicht implementiert. Das ist nicht ganz so einfach zu handeln, da CP/M 2.2 mit 128 Byte-Sektoren arbeitet, auf der Compact Flash-Disk die Sektoren aber 512 Byte groß sind.

CP/M besteht im wesentlichen aus drei Teilen: Das BIOS, BDOS und CCP. Um CP/M auf einen Rechner (mit kompatiblen Prozessor) zu portieren, muss man die BIOS-Routinen entsprechend anpassen. Das Betriebssystem benutzt dann die BIOS-Routinen und muss nicht wissen, wie die Hardware des Computers funktioniert. Das BIOS ist also eine Sammlung einfacher Treiber für Ein-/Ausgabe, Diskettenlaufwerkszugriff usw. Hier ist eine Übersicht der Funktionen.
BDOS ist das Betriebssystem an sich, welches u.A. das CP/M-Dateisystem versteht. Und CCP ist der Kommandozeileninterpreter, eine Art rudimentäres COMMAND.COM bzw. CMD.EXE.

Als erstes braucht man also eine BDOS- und CCP-Version, von der man auch weiß, an welche Stelle im Speicher es kopiert werden muss. Ich habe dieses hier verwendet. Es gibt dort sogar Assembler-Quellcode zu CP/M.
Als zweites nimmt man ein BIOS-Grundgerüst (die findet man zu Hauf im Netz) und passt die wichtigsten Routinen an das eigene System an. Funktionen für Lochkarten kann man glücklicherweise weglassen :-)
Neben den I/O-Funktionen für Bildschirm und Massenspeicher ist die BOOT-Funktion zu implementieren. Sie kopiert die Systemspuren von Diskette in den Speicher. In den Systemspuren stecken BDOS und CCP. Danach wird der CCP gestartet, der sich mit dem A0> Prompt meldet. Die CP/M 2.2 Meldung zuvor ist im BIOS implementiert und kann beliebig geändert werden.

Der Prompt A0> steht übrigens dafür, dass aktuell auf Laufwerk A mit Benutzer 0 gearbeitet wird. CP/M kennt keine Verzeichnisse, aber die Dateien auf einer Diskette können in Benutzerbereiche aufgeteilt werden.
Wie bei MS-DOS bzw. Windows kann mit der Eingabe von B: auf Laufwerk B gewechselt werden. Tatsächlich ist CP/M hier das Vorbild für MS-DOS und somit auch für Windows gewesen. Durch die Eingabe von user 1 kann auf Benutzer 1 gewechselt werden. Lässt man die Dateien auf Diskette auflisten (DIR), werden nur die Dateien des aktiven Users angezeigt.

Nun hat man also ein einfaches BIOS mit BOOT-Routine geschrieben. Jetzt stellt sich nur noch die Frage, wie das BIOS beim Einschalten in den Speicher gebracht wird. Dazu muss ein Bootloader-Programm geschrieben werden, welches zusammen mit dem BIOS üblicherweise im ROM liegt. Der Bootloader kopiert nun das BIOS an das Ende des RAM-Speicherbereichs. Bei mir ist das ab Adresse 0xFA00. Dann wird das ROM abgeschaltet, an die Stelle RAM gesetzt und der BIOS gestartet. Ab jetzt hat CP/M die Kontrolle über das System.

In den Screenshots stammt die Meldung "Loading CP/M..." vom Bootloader, der das BIOS in den RAM kopiert. Die Meldungen "Z80 Homebrew Computer" und "CP/M 2.2 Copyright (c) by Digital Research" stammen vom BIOS. Der Prompt A0> wird von CCP (mit Hilfe der BIOS-Funktion CONOUT) getätigt.

Da das System noch keine VGA-Schnittstelle oder sonstiges Display hat, findet Bildschirmein-/ausgabe über ein serielles Terminal statt. Im Folgenden ein paar Screenshots vom System in Aktion. Die CP/M-Software stammt von der Seite http://www.retroarchive.org/.

Microsoft BASIC 5.21 vom 28.07.1981 :-)

Noch ein Microsoft BASIC, von 1977!

Tiny-C Compiler

Turbo Pascal 1.0 von Borland
Ich habe ja damals Programmieren in Turbo Pascal unter MS-DOS gelernt, deswegen muss ich mir Turbo Pascal 1.0 mal genauer anschauen!

Ein CP/M-Dateisystem-Image kann man mit cpmtools erstellen. Es lassen sich damit Dateien rein- und rauskopieren. So wird ein einfacher Dateiaustausch zwischen CP/M und der modernen Computerwelt auf Dateiebene möglich. Um dieses Image auf die CF-Karte zu bringen, habe ich (unabhängig von CP/M) ein kleines Programm geschrieben, welches Daten im Intel-Hex-Format empfängt und als Binärdaten auf CF-Karte schreibt. Da aus 16 Byte Binärdaten im Hex-Format ungefähr 40 Byte werden, wurde aus dem erzeugten (nicht vollen) Dateisystem von 800 kB ca. 2,2 MB. Um das über die serielle Schnittstelle mit 19200 Baud zu übertragen, braucht es schon eine Weile. Da muss ich mir noch etwas Besseres überlegen.

Damit CP/M den Diskzugriff korrekt durchführt, muss im BIOS der Disk Parameter Header und Disk Parameter Block entsprechend der Disk-Geometrie konfiguriert werden. Auf dieser Seite steht, wie man die BIOS-Konfiguration bestimmt.
Da CP/M 2.2 nur mit 128-Byte Sektoren arbeitet, wie es damals bei 8"-Disketten üblich war, mein System aber eine CF-Karte über IDE anspricht, welche mit 512-Byte Sektoren arbeitet, muss hier eine Übersetzung stattfinden. Digital Research hat dafür ein Grundgerüst für das sogenanntes Blocking/Deblocking vorbereitet, das genau dieses "Sektormapping" durchführt. Diese Dokumentation findet man hier. Es funktioniert bei meinem System allerdings noch nicht.

Sonntag, 15. April 2012

Z80 Computer spielt Musik

Ich habe wieder etwas Muße gefunden und an meinem Z80-Projekt weitergearbeitet.

Der dsPIC auf dem Ext-Board ist ja unter anderem für die Tastatur zuständig. Er hat nen FIFO, wo Tastendrücke zwischengespeichert werden, bis der Z80 diese Daten abruft. Das PS/2-Protokoll ist ein wenig dämlich (siehe hier). Wird eine Taste gedrückt, so schickt die Tastatur sogenannte MAKE-Codes. Wird diese Taste wieder losgelassen, wird ein BREAK-Code geschickt. Diese Codes sind historisch gewachsen und es gibt deswegen einige Ausnahmen zu programmieren.
Diesen Teil habe ich im dsPIC implementiert: Er liest den MAKE-Code und weiß dadurch, welche Taste gedrückt wurde. Jeder Taste habe ich eine 7-Bit-Zahl zugewiesen. Das 8. Bit (das höchstwertige) zeigt an, ob die Taste gedrückt (1) oder losgelassen wurde (0). Auf diese Weise kann ich Drücken und Loslassen einer Taste der Tastatur in einem Byte darstellen, anstatt dieser Mehr-Byte-Codes, die die Tastatur sendet. Einzelne Bytes kann ich im Z80 auch viel einfacher verarbeiten.

Anderes Thema: Der Soundchip AY-3-8912. Im Netz hatte ich bereits einige Soundfiles gefunden, die auf diesem Chip wiedergegeben werden können. Das sind meistens Songs aus alten Spielen der 8-Bit-Ära. Mir fehlte nur bisher eine Beschreibung über das Format dieser Dateien. Gefunden habe ich die Beschreibung des YM-Formats hier: http://leonard.oxg.free.fr/ymformat.html#ymchip. Die Soundfiles sind i.A. 300 Byte bis 3 kB groß und mit LHA komprimiert, einem alten Packformat, welches ich noch aus den guten alten MS-DOS-Zeiten kenne. Wenn man sie entpackt, vergrößert sich der Bedarf häufig leider auf über 60 kByte, zu viel für meinen Z80 (ok, ich könnte mit Bankswitching arbeiten und hätte so 512 kB RAM, aber das ist mir erstmal zu kompliziert). Einen LHA-Entpacker zu schreiben ist mir auch zu kompliziert :-) Deswegen habe ich mir ein einfaches Komprimierungsverfahren für diese Soundfiles überlegt, welches die Größe (immerhin) mindestens viertelt:
Der Soundchip hat 16 8-Bit-Register, mit denen man den Sound steuern kann. Die Soundfiles wurden erzeugt, in dem mit einer 50Hz-"Frame-Rate" die Werte der 16 Register in die Datei geschrieben wurden. Wenn man sich diese Daten näher anschaut fällt auf, dass sich die Werte eines Registers über mehrere Zeiteinheiten häufig nicht ändert. Deswegen hat man sich das "Interleaved-Format" überlegt, in dem alle Werte des ersten Registers hintereinander geschrieben wurden, danach alle Werte des zweiten Registers usw. Das lässt sich dann sehr gut durch LHA komprimieren.
Mein Format ist leider nicht so gut, aber sehr einfach zu implementieren und macht das Datenvolumen für den Z80 handelbar:
Ein "Frame" besteht aus 2 Byte, in denen jedes Bit anzeigt, ob sich bezogen auf den vorherigen Takt ein Register geändert hat. Dahinter stehen dann nur die Werte der Register, die sich geändert haben.

Der Code, der diese Daten wiedergibt, sieht so aus:


const void* const songdata=0x8002;              
const char *framedata=songdata;                 
volatile uint __at 0x8000 frame_count;          
unsigned int frame=0;                           
char ay_regs[16];                               
                                                
void isr(void) __interrupt {                    
int reg;                                       
unsigned int regchange;                        
DI;                                            
regchange=*((unsigned int*)framedata);         
framedata+=2;                                  
for(reg=0;reg<16;++reg) {                      
if( (regchange&(1<<reg))==0)                  
continue;                                    
ay_regs[reg]=*framedata;                      
++framedata;                                  
AY_ADDR=reg;                                  
AY_REG=ay_regs[reg];                          
}                                              
++frame;                                       
if(frame>=frame_count) {                       
frame=0;                                      
framedata=(char*)songdata;                    
}                                              
EI;                                            
}                                               

Die Interruptroutine wird mit 50 Hz vom dsPIC ausgelöst.

Ich habe dazu ein kleines Video aufgezeichnet:

Der gespielte Song stammt aus dem Spiel Midnight Resistance (das ich selber nicht kenne). Die C64-Version hört sich allerdings deutlich besser an. Der SID-Chip des C64 ist wohl zurecht so beliebt!

Sonntag, 19. Februar 2012

Arbeiten am Ext-Board

Das Platine des Ext-Board ist endlich da und ich habe begonnen, es zu bestücken. Die IDE-Schnittstelle funktioniert bereits und der AY-3-8912 Soundchip auch. Es fehlt noch die Audio-Verstärkerschaltung für den Lautsprecher, da fehlen mir Bauteile.

Ich habe einen 64-Pin dsPIC30F6011 im Schaltplan, jedoch einen dsPIC30F6011A aufgelötet. Ich war so naiv anzunehmen, dass die Pinkompatibel sind. Es unterscheiden sich jedoch zwei Pins: Die Programmierpins PGC und PGD wurden von Pin 15/16 auf Pin 17/18 verschoben. Deswegen hat der PICkit 3 den µC anfangs nicht erkannt. Glücklicherweise habe ich Pin 17/18 auf einem Pinheader herausgeführt, so dass ich keine Änderungen an der Platine vornehmen muss, sondern nur am Programmierkabel.
Der dsPIC ist in einem 64-Pin TQFP-Gehäuse untergebracht. Es war das erste Mal, das ich so kleine Pinabstände gelötet habe. Mit reichlich Flussmittel und Entlötlitze war das Auflöten aber relativ problemlos.
dsPIC6011A im 64-Pin TQFP-Gehäuse
Noch ist nicht alles bestückt: Der Ethernetcontroller fehlt, die Ethernetbuchse und ein 16-Bit Portexpander sind noch nicht aufgelötet.
Das Z80-System

Sonntag, 5. Februar 2012

Extboard und VGA-Versuche

Gestern habe ich meine Z80-Erweiterungsplatine in Auftrag gegeben. Wie schon geschrieben, wird darauf ein AY-3-8912 zur Sounderzeugung mit Verstärker für direkten Lautsprecheranschluss sein und eine IDE-Schnittstelle (wie bereits für das alte Z80-Board implementiert).
Ein dsPIC30F6011 wird für die Anbindung von PS/2-Maus und Tastatur sorgen. Da noch so viel Platz auf der Platine zur Verfügung stand, habe ich mich entschlossen, mich mal an Ethernet zu probieren. Microchip hat den ENC28J60 im Programm, den angeblich weltweit kleinsten Ethernet-Controller. Über SPI wird der an den dsPIC angebunden. Microchip bietet auch gleich einen kompletten TCP/IP-Stack für deren µCs an. So ein Stack ist ziemlich groß, daher habe ich einen der größten dsPICs gewählt, damit neben dem TCP/IP-Stack noch ausreichend Platz für weiteren Code bleibt.
Das Extboard
Sollte alles wie geplant klappen, implementiert mein System dann Maus, Tastatur, IDE, Sound, RS232 und Ethernet. Natürlich ist dann erst einmal viel Software zu schreiben, aber dann fängt der Spaß ja erst richtig an :-)

Nun fehlt nur noch eine "Grafikkarte". Dazu hatte ich vor ein paar Monaten mal eine kleine Testplatine mit einem XC9572 CPLD erstellt, diese lag aber nur unbestückt im Karton herum. Nun habe ich sie bestückt und den CPLD mit Anregungen von http://www.fpga4fun.com/PongGame.html programmiert.

VGA mit CPLD

Die beiden Wannensteckerbuchsen sind zur Anbindung meines alten Z80-Boards. Der unbestückte IC-Sockel kann einen RAM-Baustein (gedacht als Grafikspeicher) aufnehmen. So sieht das Testbild aus, dass der CPLD erzeugt:

VGA-Output
Die Auflösung ist 640x480 bei 8 Farben. Ein paar Verilog-Code-Anpassung sind noch nötig, da das Bild sehr weit nach links verschoben ist. Das lässt sich aber einfach durch das V-Sync-Signal beeinflussen.

Schwieriger wird es, wenn der CPLD die Grafikdaten aus dem RAM holen muss, und den RAM-Zugriff des Z80 koordinieren muss. Ob der CPLD das alles aufnehmen kann?

Montag, 30. Januar 2012

Automatische variable Arrays


Ich habe kürzlich etwas Neues über die Programmiersprache C gelernt. Bisher war ich immer der Ansicht, dass Arrays, deren Größe zur Kompilierzeit nicht feststehen, auf dem Heap allokiert (->malloc) werden müssen. Seit ISO C99 bietet der Sprachumfang variable automatische Arrays, die wie alle automatischen Variablen auf dem Stack liegen.

Folgende Funktion

void func(uint a) { 
uint size=a+123;   
int array[size];   
}                   

ist gültig. Natürlich kann es hier zum Stacküberlauf kommen, es sollte daher eher mit etwas kleineren Feldern gearbeitet werden. Auf jeden Fall handelt man sich so kein Speicherleck ein, da das Array nach Funktionsende wieder freigegeben wird. Auch die Allokierung auf dem Stack ist schneller als auf dem Heap, da der Stackpointer dazu nur um die Arraygröße erhöht werden muss.

Weitere Infos findet man in den GCC-Docs. In diesem Zusammenhang möchte ich auch noch auf alloca() hinweisen. Diese Funktion allokiert ebenfalls ein variables Array auf dem Stack, ist aber scheinbar nicht ISO-C sondern eine BSD-Extension.

Der Unterschied zwischen alloca() und einem automatischen variablem Array liegt in der Gültigkeit des Speichers:
Der mit alloca() reservierte Speicher wird für die Laufzeit seiner Funktion belegt, das variable Array ist gültig innerhalb seiner Klammerebene.

Donnerstag, 5. Januar 2012

AY-3-8912

Ich habe es nun endlich geschafft, dem AY-3-8912 ein paar Töne zu entlocken. Der Baustein ist offensichtlich sehr Timing-empfindlich, z.B. müssen innerhalb von 50ns die Bus-Steuerleitungen ge-setup't sein. Da ich die Busleitungen mit nem Mikrocontroller gesteuert habe, muss man schon überlegt verdrahten und programmieren  :-)

Der Chip wird mit einem externen Takt im Bereich 1-2 MHz getaktet. DA0-DA7 sind Datenbus und Adressbus, die sind gemultiplext. BC1 und BDIR sind die Bussteuerleitungen, die das kritische Timing erfordern. A8 und BC2 sind eine Art Chipselect und können fest auf +5V gelegt werden. CH_A-CH_C sind die drei Audiokanäle, die man einfach stumpf verbinden und in einen Kopfhörer einspeisen kann. IOA0-IOA7 ist ein digitaler "User-Port".

Der AY-3-8912 besitzt 16 interne Register. Um diese Register zu beschreiben/zu lesen, gibt es diesen prinzipiellen Ablauf:
Zum Schreiben versetzt man den Bus in den Inaktiven Zustand, dann wird die Adresse des zu selektierenden Registers auf den Bus DA0-DA7 gelegt und der Bus in den Latch-Zustand und zurück in den Inaktiven Zustand versetzt. Beim Übergang in den Inaktiven Zustand wird die Registeradresse gelatcht. Nun wird der in das Register zu schreibende Wert auf den Datenbus gelegt, in den Write-Zustand und zurück in den Inaktiven Zustand gewechselt. Nun wird der Wert in des Register geschrieben.

Den Buszustand legt man mit BDIR und BC1 fest. Der Inaktive Zustand wird durch BDIR=low und BC1=low gesetzt. Zuerst hatte ich BDIR an Portpin RB5 und BC1 an Portpin RE3 des dsPICs angebunden. Wenn man dann mit

_LATB5=0; 
_LATE3=0; 

den Bus steuern will, geht das schief, da jede Anweisung deutlich länger als 50ns benötigt, zumal der dsPIC auch nur mit 8MHz internen Takt ohne PLL läuft. Der Pegelwechsel selber von 0V auf 5V dauert ca. 7ns, das ist also kein Problem. Es müssen nur beide Pins gleichzeitig wechseln, um die 50ns nicht zu überschreiten. Also müssen beide Pins auf dem selben Port liegen. Um dann beide Pins gleichzeitig zu schalten, kann man dann das gesamte Portregister schreiben:

LATE = LATE & 0xFFFC;

Hier werden beim Port E die ersten beiden Bits gleichzeitig genullt. Möchte man ein Bit nullen, aber das andere "einsen", muss man einen Zwischenschritt einlegen:

LATE = (LATE & 0xFFFC) | 0x01;

So wird nur das zweite Bit genullt, das erste Bit gesetzt.

Den AY-3-8912 kann man auf diese Weise an den Z80 anbinden:
/AY_CS ist das Chipselect des Z80. Mit der untersten Adressleitung A0 des Z80 kann man so bestimmen, ob in das Adresslatch des AY-3-8912 geschrieben werden soll oder in eines der dadurch adressierten Register.

Versuchsaufbau