Entwickeln mit Z88dk
Wenn man „mal eben“ ein Programm erstellen will ohne in die Tiefen von Assembler einsteigen zu wollen so greift man normalerweise auf das mitgelieferte BASIC im CPC zurück. Soll es dann doch mal etwas schneller sein, so bleibt meist nur noch der Assembler übrig und wenn man dann merkt, dass die eigenen Assembler Kenntnisse mit den Jahren dann doch etwas eingerostet sind wünscht man sich eine andere Programmiersprache, eventuell sogar eine, mit der man sowieso schon tagtäglich arbeiten muss. Warum also nicht den CPC in C programmieren, schliesslich soll C doch eine relativ hardwarenahe Sprache sein. Jetzt am besten noch eine einfache Entwicklungsplattform für den PC und schon könnte man einfach und schnell einsteigen.
Bei der Suche nach einem C-Compiler für Z80 Systeme stösst man relativ schnell auf diverse kommerzielle Compiler und zwei Compiler aus dem OpenSource Bereich: SDCC und Z88dk. An dieser Stelle soll es nur um letzteren gehen.
Was ist Z88dk?
Z88dk ist ein Cross-Compiler für die Sprache Small-C, der neben dem eigentlichen C-Compiler auch eine C Standard-Bibliothek für viele Z80 Rechner mitbringt. Z88dk erweitert Small-C um viele Funktionen von ANSI-C, wobei die Funktionen weggelassen wurden, die sich auf einem Z80 System nur ineffizient realisieren lassen. Der Name Z88dk deutet auf den Ursprung des Compilers hin. Er wurde für den Cambridge Z88 entwickelt und unterstützt mittlerweile mehr als 20 unterschiedliche Z80 Systeme.
Installation
Für die Installation unter Windows sollte am besten der Installer verwendet werden. Er kopiert die notwendigen Dateien an die richtige Stelle und setzt ein paar Umgebungsvariablen unter Windows. Leider wird nicht automatisch das Programmverzeichnis von Z88dk in die Umgebungsvariablen eingetragen, sodass man hier selbst Hand anlegen muss. Man muss also unter „Start-Einstellungen-Systemsteuerung-System“ im Reiter „Erweitert“ den Button „Umgebungsvariablen“ anklicken und bei den Benutzervariablen die Variable PATH um den Pfad zu dem Programmverzeichnis von Z88dk erweitern. Bei einer Standard-Installation ist das „C:\Programme\z88dk\bin“. An dieser Stelle sollte man daran denken, dass die neu gesetzten Umgebungsvariablen erst dann wirksam werden, wenn man eine neue Eingabeaufforderung öffnet.
Alle Anwender, die Z88dk unter Linux verwenden wollen, sollten sich das tar.gz Paket laden und den Compiler entsprechend selbst kompilieren. Solange man nicht die aktuelle CVS Version verwendet, sollte es dabei keinerlei Probleme geben.
Alle weiteren Programmierbeispiele werden sich allerdings auf Windows und Flynn’s WinCPC beziehen, da so die Binärdateien einfacher zu handhaben sind. Eine kleine Anmerkung am Rand: Unter Linux kann man WinCPC mit WINE verwenden.
Hallo Welt!
Eine Einführung in die C-Programmierung kann natürlich an dieser Stelle nicht gegeben werden, dennoch soll anhand einiger Beispiele die Arbeit mit Z88dk beschrieben werden und wie fängt man üblicherweise bei einer neuen Programmiersprache an? Genau, mit „Hello World!“. Listing 1 zeigt den Quellcode des Programms.
#include <stdio.h>
void main(){
printf(„Hello World!“);
}
Listing 1: Hello World! – hello.c
zcc +cpc -lcpcfs -O3 -create-app hello.c
Listing 2: Kompilieren von hello.c
Das Programm erklärt sich eigentlich von selbst. Es wird die Standard-Ein-/Ausgabebibliothek stdio geladen und danach mit dem Befehl printf der Text ausgegeben.
Als nächstes möchte man sein Programm natürlich kompilieren und auf dem CPC ausprobieren. Dafür verwendet man den in Listing 2 angegebenen Befehl, wobei die folgenden Werte übergeben werden müssen:
- +cpc – Als Zielplattform soll der CPC verwendet werden
- -lcpcfs – Standardbibliothek für Ein- und Ausgabe mit einbinden
- -O3 – Maximale Optimierung einschalten
- -create-app – Es wird zusätzlich zur eigentlichen Binärdatei eine Datei mit AMSDOS Header generiert
Nachdem das Kompilieren erfolgreich durchgeführt worden ist, finden sich in dem Verzeichnis die Dateien a.bin und a.cpc. Die Datei mit der Dateiendung „.cpc“ ist die Datei, die den AMSDOS Header beinhaltet. Sie könnte z.B. mit einem Programm wie CPCFS direkt in ein DSK-Image geschrieben werden oder auf den CPC übertragen werden. Wir werden an dieser Stelle aber den Emulator WinCPC von Flynn verwenden, da er die Möglichkeit bietet, Binärdateien direkt in den Speicher zu laden.
Z88dk erzeugt immer Programme, die an der Adresse &6000 beginnen. Um das Programm also in WinCPC auszuführen muss man mit „File – load binary file“ die Datei a.bin auswählen. Nach dem Bestätigen wird dann noch die Ladeadresse eingegeben, also &6000. Die Datei liegt jetzt im Speicher von WinCPC und kann mit CALL &6000 gestartet werden. Wenn nichts falsch gemacht worden ist, sollte der CPC „Hello World!“ ausgeben.
5kb? Was für ein Biest!
Wie man leicht erkennen kann, hat Z88dk eine 5kb grosse Binärdatei erzeugt. Warum ist das so? Auch wenn printf relativ mächtig ist und dementsprechend viel Platz verwendet, ist der Compiler in der Lage zu erkennen, ob die volle Funktionalität von printf überhaupt verwendet wird. Wird nicht die volle Funktionalität verwendet, ersetzt der Compiler automatisch printf durch eine einfachere Ausgabefunktion. Daran kann es also nicht liegen, da wir eine sehr einfache Ausgabe verwendet haben.
Schaut man sich einmal die CPCFS Bibliothek von Z88dk an, so fällt sofort auf, dass hier ein ganzer Block mit Null-Bytes belegt ist. Ein Blick in den Quellcode verrät, dass die Bibliothek sowohl für die Dateieingabe als auch für die Dateiausgabe 2kb reserviert (AMSDOS). Abhilfe schafft an dieser Stelle die Bibliothek ndos, die nur eine Dummy Datei Ein-/Ausgabe unterstützt. Also ersetzt man in dem Befehl aus Listing 2 -lcpcfs durch -lndos und kompiliert nochmal die Datei hello.c und siehe da: die Datei ist nun nur noch 331 Byte gross.
Darf’s noch etwas schneller sein?
Wie bei den meisten C-Compilern ist es auch bei Z88dk möglich Assembler im Quelltext einzugeben. Dadurch ist es möglich, wichtige Funktionen, die häufig angesprungen werden oder zeitkritisch sind, zu optimieren, während man die unkritischen Routinen weiterhin in C schreiben kann.
An dieser Stelle muss man wissen, wie in C Variablen an Funktionen übergeben werden und wie die einzelnen Datentypen definiert sind. Tabelle 1 enthält die verschiedenen Datentypen und deren Grösse in Bytes. Natürlich werden auch Pointer unterstützt, welche jeweils 2 Bytes Speicher belegen.
Typ | Größe | Min | Max |
char | 1 Byte | -128 | 127 |
unsigned char | 1 Byte | 0 | 255 |
int | 2 Byte | -32767 | 32767 |
unsigned int | 2 Byte | 0 | 65535 |
long | 4 Byte | -2147483647 | 2147483647 |
unsigned long | 4 Byte | 0 | 4294967296 |
Tabelle 1: Datentypen in Z88dk |
Die Assembleranweisungen werden bei Z88dk in einem Block abgelegt, der durch die Anweisung #asm und #endasm geklammert ist.
Listing 3 ist ein relativ unnützes, kleines Programm, das die Übergabe von Parametern an C Funktionen demonstrieren soll. Es werden nacheinander die Buchstaben ‘A’-‘E’ auf dem Bildschirm ausgegeben und die Zahl 42 an den aufrufenden Programmteil zurückgegeben.
Wie man an dem Programm in Listing 3 sieht, werden die Parameter über den Stack an die Funktion weitergegeben. Tabelle 2 zeigt den Aufbau des Stacks.
void printchar( char code, int codeint, long codeint2 ){
#asm
ld hl,6 //code (SP+6)
add hl,sp //'A'
ld a,(hl)
call 0xbb5a
dec hl //highbyte codeint (SP+5)
ld a,(hl) //'B'
call 0xbb5a
dec hl //lowbyte codeint (SP+4)
ld a,(hl) //'C'
call 0xbb5a
dec hl //SP+3
ld a,(hl) //'D'
call 0xbb5a
dec hl //SP+2
ld a,(hl) //'E'
call 0xbb5a
ld hl,42 //Rueckgabewert
ret
#endasm
}
void main(){
int a = printchar('A', 'B'*256+'C', 'D'*256+'E');
}
Listing 3: Beispiel für Variablenübergaben an Funktionen
Adresse | Parameter |
SP | Stackpointer für den Rücksprung |
SP+2 | 2 Byte int Wert (codeint2) |
SP+4 | 2 Byte int Wert (codeint) |
SP+6 | 1 Byte char Wert (code) |
Tabelle 2: Aufbau des Stacks in Listing 3 |
Typ | Register |
long | dehl |
int | hl |
char | h=0,l |
Tabelle 3: Rückgabewerte nach Datentyp |
Um die Werte zu laden wird HL als Pointer auf den Speicherbereich mit den übergebenen Parametern verwendet. Als Startwert bekommt HL den höchsten Parameter, in diesem Fall ‘code’ zugewiesen, was laut Tabelle 2 einen Wert von SP+6 entspricht. HL wird deshalb mit dem Wert 6 geladen und der Stackpointer dazuaddiert. Der Wert wird jetzt ausgelesen und in A gespeichert, damit die ROM Routine BB5A diesen Wert auf dem Bildschirm ausgeben kann. Als nächstes wird der Wert von HL dekrementiert, sodass HL jetzt auf das Highbyte von ‘codeint’ zeigt. Auch hier wird der Wert wieder nach A geladen und ausgegeben. Die weiteren Befehle lesen die Bytes der entsprechenden Übergabeparameter analog aus.
Kompiliert man das Programm, so erhält man eine gerade einmal 107 Byte grosse Binärdatei.
Wie geht’s weiter?
Wer sich gerne weiter mit Z88dk befassen möchte, sollte auf jeden Fall die Dokumentation lesen und sich eingehender mit den mitgelieferten Beispielprogrammen beschäftigen. Aus Platz- und Zeitgründen konnte hier auch nicht auf die Erstellung von Softwarebibliotheken eingegangen werden.
Möchte man mehr Informationen zu den mitgelieferten Systembibliotheken, sollte man in das Include Verzeichnis von Z88dk schauen. An dieser Stelle liegen die Header-Dateien der Bibliotheken.
Ausblick
Mit Z88dk ist es möglich komplexe Programme relativ einfach zu erstellen und das ohne allzugrossen Geschwindigkeitsverlust wie bei BASIC. Die Integration von Z80 Assembler in den Quellcode bietet ausserdem die Möglichkeit spezifische Optimierungen vorzunehmen.
Schaut man sich ein wenig bei den Ports für die anderen Z80 Systeme um, so stellt man fest, dass es dort einige Bibliotheken gibt, die sicherlich für die Programmierung des CPCs auch recht interessant wären, z.B. die sehr umfangreiche Sprite-Bibliothek für den ZX Spectrum.
Spezielle Bibliotheken für Hardware könnten zudem die Programmierung von Anwendungsprogrammen erleichtern, bei denen es nicht nur auf die reine Geschwindigkeit ankommt. Auch eine Portierung des Z88 TCP/IP Stacks könnte durchaus interessant sein und mittels Far-Pointern kann man die 64kb Barriere knacken.
Durch die einfache Portierbarkeit von Z88dk wäre es ausserdem ohne hohen Aufwand möglich den Compiler an andere Betriebssysteme wie FutureOS oder SymbOS anzupassen.
Schlussendlich wird Z88dk natürlich keinen Assembler ersetzen können, aber im Vergleich Code, Programmieraufwand und Wartbarkeit schneidet die C Lösung sicherlich nicht schlecht ab.