Samstag, 8. Februar 2014

VGA Karte mit CPLD

Ich wollte schon seit geraumer Zeit eine kleine VGA-Karte bauen. Nun habe ich einen ersten funktionierenden Prototypen. Herzstück ist ein XC95144XL - CPLD von Xilinx. Dieser kommt im TQFP-Gehäuse mit mindestens 100 Pins und fine-pitch, eine neue Herausforderung für meine Lötfähigkeiten :-)

Glücklicherweise hat das Löten problemlos geklappt- mit viel Flußmittel und Entlötlitze.
VGA-Board

Der entlötete IC war ein AVR ATmega128, mit dem ich den CPLD ansteuern wollte. Geplant war eine Daten-/Adressbus-Schnittstelle wie beim Motorola M68008-Prozessor. Das bedeutet, der Grafik-RAM wird mit einem 17-Bit breiten Adressbus, einem 8-Bit breiten Datenbus, einem /CS und R/W-Signal angesteuert.

Aus mir unbekannten Gründen ließ sich der AVR nicht programmieren, obwohl mein AVR-Dragon den Chip über die ICSP-Schnittstelle (der Wannenstecker auf der Platine) erkannt hat.

Mein Plan, den CPLD über die 68k-Schnittstelle anzusprechen, war also gescheitert. Aus dem CPLD-Verilog-Code habe ich die Schnittstelle rausgeschmissen und stattdessen eine simple SPI-Schnittstelle implementiert, worüber sich nun Daten in den Grafik-RAM schieben lassen. Den PIC32 auf dem roten Digilent-Board verwende ich, um Daten per Software-SPI in den CPLD zu schieben (die drei dünnen Kupferlackdrähte, die an den Pads des AVR aufgelötet sind).

Das SPI-Protokoll besteht aus 17+8 = 25 Bits. Die oberen 17 Bits sind Adressbits, die unteren 8 sind die Datenbits für die Pixelfarbe. Bei low-activen Chipselect shiftet der PIC32 die Adress- und Pixel-Bits in ein internes CPLD-Datenregister. Bei der dann folgenden steigenden /CS-Flanke legt der CPLD im Schreibzyklus die Daten- und Adress-Bits auf den Grafik-RAM-Bus und schreibt so die Daten in den RAM.

Pro Pixel müssen also 25 Bits in den CPLD geschoben werden. Bei einer Auflösung von 320x200 sind 64000 Pixel für ein vollständiges Bild zu schreiben, es müssen also 64000*25 Bits geschoben werden. Das begrenzt daher die Geschwindigkeit, mit der der RAM beschrieben werden kann, recht stark. Hinzu kommt, dass der CPLD mit 25 MHz getaktet ist, und daher nur SPI-Taktraten kleiner 12 MHz schaffen kann.

Prinzipielle Beschreibung

Die Grafikkarte besteht aus diesen ICs:

  • CPLD XC95144XL
  • 128 kByte SRAM AS7C31025C-10 (10 ns schnell)
  • Oszillator 25 MHz
Für ein VGA-Signal der Auflösung 640x400 benötigt man einen Pixel-Takt von 25,175 MHz (VGA-Timings siehe hier), eine Frequenz von 25 MHz tut es aber auch. Für jeden Pixel (bei 8-Bit-Farben) muss ein Byte aus dem SRAM ausgelesen werden, wofür bei der Frequenz also 40 ns zur Verfügung stehen. Das ist schon recht sportlich, die SRAMs, die man bei Reichelt findet, schaffen 55 ns oder 70 ns. Bei TME.EU habe ich aber schnellere SRAMs gefunden.

Um die Speicher- und Taktanforderungen zu senken, habe ich die Auflösung auf 320x200 halbiert. Im Prinzip verdoppel ich die Pixel horizontal und vertikal. Dadurch habe ich die doppelte Zeit, um den RAM auszulesen und nur ein Viertel des Speicherbedarfs.

Nur den RAM auszulesen, reicht aber ja nicht aus. Der Speicher muss auch beschrieben werden können. Dazu verdoppel ich den Takt, mit dem der RAM angesprochen wird. In "geraden Takt" wird der RAM ausgelesen, um die Daten aus dem Bildschirm anzuzeigen. Im ungeraden Takt können die Daten, die vom PIC32 reingeshiftet wurden, in den RAM geschrieben werden.

Testbild

Schaltung

Schaltung VGA-Karte

VGA ist analog. Daher müssen aus einer 8-Bit-Farbe drei analoge Farbkanäle (RGB) erzeugt werden. Die 8 Bits eines Farb-Bytes werden als RRRGGGBB-Farbe interpretiert. Für den roten und grünen Kanal werden drei Bits verwendet, für den blauen zwei.

Pixeldaten und RAM-Adressierung

Die begrenzte Makrozellen- und Registerzahl des CPLDs zwingen zu einer etwas verschwenderischen RAM-Belegung. Eine Bildschirmzeile besteht aus 320 Pixeln, was 320 Byte im RAM entspricht. Die naheliegenste Adressierung eines Bytes in Abhängigkeit der x-y-Koordinate wäre ja
addr = y*320 + x
Eine Multiplikation mit 320 lässt sich in dem CPLD nicht realisieren, bzw. ich wüsste nicht wie :-)
Eine Multiplikation mit 512 hingegen ist schnell gemacht, denn das entspricht einem Bit-Shift nach links um 9 Stellen. Und das ist mit nem CPLD überhaupt kein Problem. Eine Bildschirmzeile belegt daher 512 Pixel, wovon nur die ersten 320 Pixel belegt sind. Die hinteren 192 Pixel können zwar geschrieben werden, werden aber nicht dargestellt.

Palettenkompatible Bilddaten erzeugen

Das digitale RRRGGGBB-Farbsignal mit den dazugehörigen Widerständen gibt die darstellbaren Farben auf dem Bildschirm vor. Die R-Bits sind die niederwertigen Bits, die B-Bits sind die höchstwertigen. Ein Byte mit Wert 0 entspricht Schwarz, 255 ist weiß. Mit C# lässt sich ein Bild mit einer beliebigen Farbpalette und Farbtiefe in ein kompatibles Format mit ähnlichen Farben erzeugen. Folgender C#-Code erzeugt Byte-Daten mit dem gewünschten Pixelformat:

Bitmap bild = new Bitmap("sample.bmp");                         
Byte[] picdat = new Byte[bild.Width * bild.Height];             
for (int y = 0; y < bild.Height; y++)                           
{                                                               
for (int x = 0; x < bild.Width; x++)                    
{                                                       
Color col = bild.GetPixel(x, y);                
int r = col.R / 32;                             
int g = col.G / 32;                             
int b = col.B / 64;                             
byte rgb = (byte)( r | (g << 3) | (b << 6) );   
picdat[x + y * bild.Width] = rgb;               
}                                                       
}                                                               


Verilog-Code

Die CPLD-Sourcen findet man hier.

Ausblick

Mein langfristiges Ziel ist es noch weiterhin, einen kompletten kleinen Homebrew-Computer zu bauen. Das größte Problem war immer, ein sinnvolles Display an meine Systeme anzubinden. LCD-Displays in Smartphone-Größe sind mir zu klein und etwas farbig sollte es auch sein. Diese Grafikkarte erfüllt nun meine wichtigsten Anforderungen.
Auf meinem Wunschzettel stehen Sprites, um schnelle Animationen zu ermöglichen und Hardware-Cursor für einen Mauszeiger und Tastatur-Cursor. Das lässt sich aber wohl nicht mit so einem kleinen CPLD realisieren. Der Sprung zu einem FPGA ist da nur logisch, aber auch gleich nochmal ne Runde anspruchsvoller.

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

Samstag, 17. Dezember 2011

Z80 in C programmieren

Ich bin wieder fleißig gewesen und habe am Z80-Computer gearbeitet. Ich plane die erste Erweiterungskarte für das System. Darauf wird sich eine IDE-Schnittstelle befinden, die ich ja bereits für die andere Z80-Platine erstellt hatte. Dann ein Controller zum Anschluss von Maus und Tastatur. Ich habe hier ein paar Trockenübungen auf einem Steckbrett gemacht: Tastatur auslesen und Numlock blinken lassen funktioniert, das sollte also kein Problem mehr sein. Eine PS/2-Maus hat das gleiche elektrische Interface, aber die Softwareseite habe ich mir noch nicht näher angeschaut und weiß daher nicht, wie schwierig das zu implementieren ist.
Weiterhin wird sich auf dem Erweiterungsboard der AY-3-8912 befinden. Das ist ein 8-Bit Sound-Chip mit drei "Stimmen". Er kam in einigen 8-Bit Computern und Arcadespielen zum Einsatz. Ich habe noch keinen in der Hand gehabt, aber bereits den Schaltplan für die elektrische Anbindung an den Z80 erstellt. In den nächsten Tagen sollte er eintreffen, er ist bereits bestellt.

Nun zum eigentlichen Thema dieses Posts:
Ich habe mich heute mit dem C-Compiler SDCC beschäftigt, der Code für "Small Devices" erzeugen kann, wie Z80, 8051, 68HC08 und 8-Bit PIC-Controller von Microchip. Eigentlich habe ich mit größeren Schwierigkeiten gerechnet, bis ich etwas Gescheites zum Laufen zu bringen, aber es hat relativ schnell geklappt.
Die erste Frage war, wie man in C auf die IO-Ports des Z80 zugreifen kann. Im Assembler macht man das mit IN/OUT-Befehlen:
IN A, (32)   (Lesen von Port 32)
OUT (32), A  (Schreiben auf Port 32)
Diese Befehle werden Spracherweiterungen mit dem schönen Namen Storage Class Language Extension zugeordnet, wie ich nach einigem Suchen herausfand.
Mit
__sfr __at (32) uart_rxtx;
definiert man eine Variable, die man lesen und schreiben kann. Der Code, der daraus erzeugt wird, ist dann das entsprechende IN/OUT. sfr steht übrigens für Special Function Register.
Ich schreibe mir also folgenden Code in schönstem ANSI-C:

__sfr __at (32) uart_rxtx;                
__sfr __at (33) uart_st;                  
                                          
void sendstr(const char *s) {             
while( *s ) {                            
while( uart_st&2 );                     
uart_rxtx = *s;                         
++s;                                    
}                                        
}                                         
                                          
int main(void) {                          
sendstr("Z80 Computer\r\n");             
sendstr("Programmiert in C!\r\n");       
while(1);                                
}                                         


Kompiliert wird das mit
sdcc -mz80 test.c
Heraus kommt test.ihx im Intel-Hex-Format, mein Programmer schluckt aber nur einfache Binärdaten. Das Hex-File kann man mit
makebin.exe -p test.ihx test.bin
in eine einfache Binärdatei wandeln. Und es funktioniert :-)
So ganz habe ich noch nicht durchdrungen, was da alles vor sich geht. Es wird noch gewisser Startup-Code hinzugefügt, unter Anderem in Form der Datei crt0.s, wo Interruptvektoren und der Stackpointer initialisiert werden. Jetzt als Standalone-Programm läuft das mit dem SDCC-Startup-Code. Wie das eines Tages mal aussieht, wenn ich ein kleines OS geschrieben habe, von dem aus ich solch ein Binary ausführe, kann ich nicht so wirklich einschätzen. Ich werde es sehen :-)

Dienstag, 29. November 2011

Grafisches Display am Z80

Zum Testen und als erste halbwegs sinnvolle Anwendung der neuen Platine habe ich ein grafisches LCD-Display mit 128x64 Auflösung angebunden:
Z80 mit Display
Das ist ein DEM 128064A SYH-LY mit einem 8-Bit-Interface. Das Interface besteht im Wesentlichen aus dem 8-Bit-Datenbus, einer Register Select Leitung (RS), zwei high-aktive ChipSelects (CS1,CS2), high-aktives Enable (E) und low-aktives Reset (RSTB).
Das Display ist aus zwei Segmenten mit 64x64 Pixeln aufgebaut, betrieben durch je einem Treiber-IC S6B0108. Deswegen gibt es auch die beiden Chip Selects CS1, CS2. Mit CS1 selektiert man den linken Teil des Displays, mit CS2 den rechten. Register Select bestimmt, ob das Datum auf dem Datenbus ein Befehl für einen Treiber-IC ist (RS=low), oder in den Speicher des Displays geschrieben werden soll (RS=high). Bei einer fallenden Flanke von E wird das Datum vom Datenbus vom selekierten IC gelatcht und dort entsprechend verwurstet.

Mittwoch, 16. November 2011

Neues Board

Yet another Z80-Board



Da ich mit dem Z80-Board unzufrieden war, habe ich nun ein neues designt. Hauptmotivation war die Grafikkarte, die an das System angebunden werden soll. Diese hätte nur über IO-Ports angesprochen werden können. Für Zugriffe auf den Grafikspeicher ist das sehr langsam. Nicht nur, weil IO-Zugriffe einen extra-Wait-State-Takt haben, sondern weil sich die Pixel nicht direkt adressieren lassen. Statt dessen hätte ich Register für die Pixeladresse und dem Pixelfarbwert bereitstellen müssen. Um einen beliebigen Pixel zu ändern, müsste also eine 16-bit Adresse (2 IO-Zugriffe) und ein Byte für die Pixeldaten (1 IO-Zugriff) geschrieben werden.
Im neuen Design befindet sich ein CPLD auf dem Board, der den 64kB-Adressraum in vier Viertel einteilt:

BankStartEnde
00 kB16 kB
116 kB32 kB
232 kB48 kB
348 kB64 kB

In jedes Viertel (Bank) können acht verschiedene RAM-Seiten eingeblendet werden. Zusätzlich lässt sich in jedes Viertel ein externer Bereich einblenden, wie z.B. der Grafikspeicher. Im ersten Viertel ist zuerst das EEProm eingeblendet, auf dem sich der Bootloader befindet.
Das Einblenden von Speicherbereichen wird mit Hilfe eines 16-Bit-Registers im CPLD realisiert, welches über IO-Port-Adresse 0 und 1 geschrieben werden kann.

Für jedes Viertel sind vier Bits im Register vorgesehen:

extbs2bs1bs0
3210

Das ganze mal vier für vier Viertel macht 16 Bits.

Ist das ext-Bit auf 1, wird bei Zugriff auf dieses Viertel der externe Speicher selektiert, egal wie die bs-Bits stehen. In Bank 0 wird bei bs=000 das EEProm selektiert, ansonsten der RAM-Baustein.
Möchte man nun in Bank 1 in eine andere Speicherseite einblenden, z.B. durch bs=001, schreibt man Folgendes:

ld a, 0001 0000 b
out (0), a       

Es wird 00010000 in den Akku geladen und über Port 0 in das CPLD-Register geschrieben.
Das untere Nibble ist null, denn in der Bank 0 soll der EEPRom eingeblendet bleiben. Das obere Nibble ist für Bank 1. Um den EEProm auszublenden und RAM einzublenden, schreibt man:

ld a, 0001 0001 b
out (0), a       

Das obere Nibble bleibt gleich, denn Bank 1 soll ja nicht umgeschaltet werden.
Bank 2 und 3 liegen im oberen Byte des 16-Bit-Registers und werden über Port 1 angesprochen:

out (1),a

Beim "Banking" muss man generell aufpassen, dass man nicht die Seite umschaltet, von der man gerade Code ausführt, denn das führt unweigerlich zum Absturz. Auch die Seite, in der der Stack liegt, sollte nicht umgeschaltet werden, es sei denn, man weiß genau, was man tut :-)

Das neue Z80-System kann auf diese Weise 4 * 8 * 16 kB = 512 kB RAM ansprechen (minus der EEProm-Seite). Mit den ext-Bits lässt sich noch weiterer Speicher von außen einblenden.

Die MMU kann man auch recht einfach mit TTL-Logikbausteinen aufbauen. Mein Ziel, alle Features auf eine Euro-Platine zu packen, hätte dies zunichte gemacht. Es wären nämlich doch einige Bausteine nötig gewesen, um die Logik zu implementieren. Der CPLD erschlägt diese Aufgabe alleine und kann sogar noch umprogrammiert werden, eine flexible Lösung also. Als mein erstes sinnvolles CPLD/Verilog-Projekt war dies schonmal eine gute Einführung für mein Grafikkartenvorhaben.

Hier nochmal wichtige Schnipsel aus dem Schaltplan der MMU:
Speicheranbindung
Die Leitungen MA16-MA18 legen die Werte von bs0-bs2 an den RAM an, abhängig von der Bank, die gerade angesprochen werden soll. Die Bank wiederrum wird durch A14, A15 bestimmt. A14=A15=0 entspricht Bank 0, A14=1, A15=0 entspricht Bank 1, usw.

CPLD XC9536

Die PullUps an den Ausgängen (MA16-MA18, /ROM_CS, /RAM_CS, /EXT_CS) sind nötig, weil der XC9536 es nicht auf 5V-High-Pegel schafft, sondern nur ~3,9V. Statt High-Pegel wird nun ein Ausgang hochohmig geschaltet und über den PullUp auf 5V gezogen.

Weiterhin habe ich den UART 16C550 entfernt. Der dsPIC auf dem Board programmiert jetzt nicht nur den EEPROM, sondern hängt am IO-Port als UART und Co-Prozessor für schnelle Multiplikationen und Divisionen.

Und da Blog-Lesen ohne Bilder nur halb so viel Spaß macht, noch ein paar Impressionen der letzten Debugsession:


 
Erster Einsatz meines neuen Logic Analyzers