 Herzlich willkommen zum Talk hier im Open Hub zum Z80-Immulator von Jürgen Reuter. Jürgen ist ein Karlsruher und ist nicht nur ein Software-Entwickler, sondern beschäftigt sich auch mit Kunst, Musik und hat ein Fable für den Retro-Computer, den er von seinen Eltern zu Weihnachten bekommen hat. Und irgendwie bringt das uns auch zusammen. Für mich war der Z80 der erste Rechner, auf dem ich jemals programmiert habe, zwar nicht genau auf diesem hier, aber Retro-Computing hat von uns allen irgendwann mal auch die Kindheit beeindruckt und außerdem interessierst du dir auch für Musik, hast Lidipon programmiert, aber heute geht es um mitprogrammiert, aber heute geht es um den Z80. Heißt Jürgen, herzlich willkommen. Ja, hallo. Also diesen Rechner bekam ich, als ich noch ein Kind war, von meinen Eltern geschenkt und habe da auch sehr viel drauf gemacht und irgendwann ist er dann im Schrank verstaubt und irgendwann war dann auch die Folientastatur, also habe ich noch das Original so hinüber, dass man den quasi nicht mehr benutzen konnte. Statt jetzt die Tastatur zu restaurieren, habe ich mir gedacht, auch komm, schreibe ich auch ein Emulator und dann habe ich das Ding auch gleich auf meinem aktuellen Rechner drauf. So, das Ganze auch funktioniert, wollte ich nur kurz zeigen. Hier haben wir quasi den Bildschirm dieses Emulators. Ein Fullscreen-Mode existiert im Moment noch nicht, wird irgendwann später noch mal implementiert und auf dem Rechner Leaf Basic, ich habe hier auch mal ein kleines Programm geschrieben, was einfach nur hier ein paar Linien malt. Also man sieht auch, der hat auch ein Graphic-Modus mit insgesamt vier Farben sieht man jetzt hier nicht in dem Beispielprogramm. Ja, auf jeden Fall, so ein kleiner Rechner halt, wie er in den 80er Jahren üblich war. So, wie sieht jetzt das Ganze aus? Also mich hat auch die Technik hinter Emulatoren interessiert und diesen Emulator habe ich eigentlich schon vor 20 Jahren angefangen zu programmieren, aber irgendwie bin ich dann hängen geblieben. Insbesondere waren mir auch damals ein paar Details über die Hardware nicht bekannt, insbesondere was auch die Bildschirmsynchronisationen angehen, sodass dann irgendwann stecken geblieben bin, Mangels technischer Doku. Das blieb dann viele Jahre liegen und jetzt habe ich es mal wieder letztes Jahr noch mal rausgekramt und habe gedacht auch, komm, jetzt mittlerweile gibt es auch Doku im Netz, tatsächlich über die Hardware und habe das dann als Anlass genommen, den Emulator mal endlich fertig zu schreiben. Und wie wir eben gesehen haben, funktioniert er auch. Der Kern des Emulators ist quasi ein Z80-Prozessor, der in diesem Rechner drin steckt, mit 3,5 MHz. Die Maschine hatte damals 16 Kilobyte ROM, wo dann quasi das gesamte Betriebssystem plus ein Basic Interpreter drin war. Und 4 Kilobyte RAM, manche Quellen im Internet sagten, es müssten eigentlich 6 sein, aber ich habe immer nur 4 Kilobyte auf dem Ding gefunden. Wovon nochmal 2 Kilobyte fürs Videospaiche abgehen und noch ein halbes Kilobyte für den Systemspeicher abgehen, sodass dann letztendlich nur noch 1,5 Kilobyte nutzbar übrig blieben. Aber auch da war es tatsächlich noch möglich mit vielen Programmiertricks kleinere Adventure Games zu programmieren. Das ging schon. Genau, wir haben eben schon gesehen, es gibt ein Textmodus mit 32 mal 16 Zeichen, es gibt eine Blockgrafik in diesem Textmodus, das haben wir jetzt gerade nicht gesehen, aber es gibt auch noch ein Blockgrafikmodus mit 128 mal 64 Pixel, 4 Farben. Ja, das war auch damals dann der Standard in der Zeit. Und es gibt Sound, das habe ich mir als Schwerpunktthema jetzt für diesen Vortrag ein bisschen herausgegriffen. Damals war es halt nicht so, dass man eine Soundcard hat und schreibt irgendwelche Daten in den Puffer rein, sondern man hat wirklich den direkten Draht zur Lautspeicher Membran und kann quasi die Membran per Befehl in eine Richtung lenken und wieder in die ruhe Lage zurücklenken. Aber das sehen wir gleich dann noch ein bisschen näher. Um jetzt eine schnelle Emulation hinzubekommen, sind einige Tricks nötig, zwar kann man sagen, die heutigen Prozessoren im Gigahertzbereich, ja das ist doch ein Klack, so einen Emulator zu schreiben. Ja, ja, wenn man mal nachrechnet, wie gesagt 1 bis 5 Milliarden Operationen pro Sekunde heute, pro Kern, damals bei 3,5 MHz etwa und etwa im Schnitt drei Taktzyklen pro Instruktion, also pi mal da um eine Million Operationen pro Sekunde, dann kommen wir auf ungefähr 1.000 bis 500 Maschinenbefehle bei einem heutigen Rechner, die man zur Verfügung hat, um quasi eine Instruktion des alten Rechners berechnen zu können, um das Ganze noch in Echtzeit hinzubekommen. Deswegen verwenden die meisten Autoren von solchen Emulatoren einen Ansatz, einen Opcode über eine Tabelle zu Dispatchen. Gucken wir uns erst mal an, wie so ein Opcode definiert ist. Wenn man so in Datenplattformen Z80 beispielsweise reinschaut, sieht man Angaben, wie man sie hier zusammengefasst sieht. Das heißt, wir haben einen sogenannten Mnemonic, dieses JP, was für Jump steht. Wir haben dann ein CC,NN als Parameter. CC ist eine Sprungbedingung, das könnte zum Beispiel sein, Springe, wenn das Ergebnis der letzten, der vorhergehenden Operation null war oder wenn es bei der vorhergehenden Operationenübertrag bei einer Addition gab oder solche Sachen. Und NN steht hier für eine Sprungadresse, das heißt, wenn diese Bedingung erfüllt ist, dann springe zu der entsprechenden Adresse hin. Was dann auch hier bei der Operation kurz erklärt ist, wenn, also if CC true, also wenn die Sprungbedingung wahr ist, dann wird der Program Counter auf den Wert NN gesetzt, das heißt, auf gut Deutsch, der die CPU springt an die entsprechende angegebene Adresse. Also quasi wie ein Go-to Befehl. Und meistens sind dann auch noch solche Bitmasken angegeben, die halt auch quasi den Aufbau des Befehlens dann darstellen. Also hier hätten wir zum Beispiel 1.1, dann kommt 3 Bits für diese Sprungbedingung und dann noch 0.1.0. Und daraus kann man sich dann auch den Opcode zusammenrechnen. Dann gibt es speziell beim Z80 auch noch ein Multidis-Patch. Es gibt spezielle Codes, zum Beispiel den Code DD. Da war ursprünglich wohl als reserviert für spätere Erweiterung gedacht und wurde dann wohl auch erweitert. Und das heißt hinter diesem Code stecken dann nochmal eine ganze Reihe von speziellen Befehlen. Das heißt, hier reicht es nicht. Hier kann ich nicht am ersten Byte schon sofort sagen, um was für ein Maschinenbefehl es handelt. Sondern ich muss tatsächlich auch noch auf das zweite Byte hier in dem Falle 6e schauen, um wirklich zu wissen, was jetzt der Befehl überhaupt tun soll. Ja, klassischerweise programmiert man diesen Dispatch aus mit einer großen Switch Anweisung, in welcher Sprache auch immer. Das wollte ich nicht tun, weil der dadurch entstehende Code wird nämlich relativ unübersichtlich, gerade bei diesem Double Dispatch da. Deswegen war die Idee, dass ich eher eine sprechende Implementierung mache. Das ganze Ding ist in Java programmiert und sieht dann beispielsweise so aus, dass ich quasi eine Array habe, indem die ganzen Ops drinstehen, mehr oder weniger so, wie sie auch im Datenblatt drinstehen, zumindest daran angelehnt. Ich habe dann hier quasi die Bitmaske, die den Aufbau des Opcodes angibt. Ich habe hier das Mnemonic, was ich eigentlich für den Emulator als solchen gar nicht brauche. Aber wenn ich quasi ein Disassemble Listing machen will, ist es ganz gut, diesen Mnemonic zu haben. Dann gibt es noch die Operationszyklen, im Falle beispielsweise beim Sprungbefehl. Wenn der Sprungbefehl funktioniert hat, ist die Dauer eine andere, als wenn der Sprungbefehl nicht funktioniert hat, wenn er funktioniert muss, nämlich dass der Program Counter auch noch neu geladen werden. Das heißt, wobei in dem Falle wäre es sogar die identische Anzahl von Zyklen. Dann haben wir die eigentlich Execute Methode. In dem Falle bedingter Sprungbefehl hätte ich tatsächlich eine Auswertung der Bedingungen C, also hier durch die Variable C quasi repräsentiert und würde, wenn diese Bedingung dann zutrifft, dann auch das Rack PC, also den Program Counter entsprechend setzen oder im anderen Falle halt nichts tun und im anderen Falle dann aber auch die gegebenenfalls dann auch die alternative Zyklenzahl verwenden. So, das Ganze, um das jetzt zu implementieren und trotzdem noch diesen schnellen Dispatch zu haben, wird quasi zu Beginn meines Emulators diese ganzen Operationen durch instanziert sozusagen. Das heißt, es wird intern auch letztendlich so eine Switch-Stabelle aufgebaut im Hintergrund und indem nämlich einfach in diese Operation reingeschaut wird, welche Bytes quasi mit welcher symbolischen Instruktion matchen und dementsprechend auch eine Tabelle aufgebaut. Und wir sehen hier quasi dann das Ergebnis zu einer solchen Tabelle, wo dann zum Schluss rauskommt, zum Beispiel unser Sprungbefehl, den wir eben hatten für den Code FA beispielsweise, der würde quasi dieses Bit-Pattern matchen und dann würde dann dem Sprungbefehl Jump Minus entsprechen mit gefolgt noch von der Angabe der Adresse. Und für den Fall, wie gesagt, es gibt spezielle Befehle beim Z80, zum Beispiel das DD, was wir vorhin hatten oder hier das FD, da kommt man mit dem ersten weit nicht weiter. Das heißt, hier muss quasi dir noch eine weitere innere Tabelle an diesem Wert noch dran gehängt werden und so kommt man aber dann relativ zu einem schnellen Dispatch. Wie gesagt, ich wollte den Sound als Schwerpunkt noch nehmen. Damals Sound-Ansteuerung quasi über einzelne Befehle, direkt auf die Lautsprechermembran. Heute gehen wir über eine Soundkarte, haben einen Puffer, wo wir quasi dann fertig gerendertes Sampling ausgeben. Genau, so und dann haben wir auch noch eine Herausforderung, weil ich habe gerade die Emulation beschrieben, wie sie funktioniert. Jetzt haben wir beim heutigen Betriebssystem immer den Fall Tasks, K-Tulage, Kreuzmal dazwischen oder ein Interrupt, Kreuz dazwischen. Das heißt, mein Befehl wird nicht wirklich in Echtzeit exakt ausgeführt, sondern es kann durchaus mal passieren, dass eine Operation mit einer Verzögerung von 1, 2, 3 Millisekunden verzögert ausgegeben wird. Das heißt, wenn man jetzt den Sound quasi das Anpluggen dieser Membran 1 zu 1 matchen würde, käme kein gescheiter Sound bei raus. Ist aber gar kein Problem, weil uns interessiert eigentlich auch nicht quasi der Zeitpunkt, wo tatsächlich die Instruktion ausgeführt wird, sondern uns interessiert, also wir führen quasi eine virtuelle Uhr ein, die einfach zählt. Zu welchem Zeitpunkt jetzt diese Z80 Instruktion ausgeführt wird, indem ich einfach die Befehlzyklin hintereinander zähle und multipliziere mit der Taktfrequenz des Prozessors. So kann ich dann genau sagen, zu welcher Zeitpunkt diese Lautsprecher beim Pran tatsächlich ausgeführt worden wäre. Genau, die Z80-Zeiten nehme ich dann als Basis. Genau, das sieht dann in etwa so aus. Wir haben hier ein kurzes Sampler-Stück als Ausschnitt und dort, wo dann steht, LD iobase.a, also hier da ist dann quasi die eigentliche, das eigentliche Anzupfen der Membran mit XOR 21 wird das erste quasi in die eine Richtung, die Membran gezogen und dann haben wir hier noch mal den anderen Wert, der es in die andere Richtung zieht und dazwischen haben wir noch, das habe ich jetzt hier nicht dargestellt, noch ein bisschen Code, der quasi eine Schleife darstellt, der die entsprechende Verzögerung dann noch implementiert. Im Ergebnis haben wir dann quasi Zeitpunkt T1, wo hier oben die Lautsprecher Membran in die eine Richtung gezogen wird. Dann haben wir wieder ein bisschen warten und also diese Pause, dann haben wir den zweiten Teil hier unten, wo dann hier mit dem Befehl quasi die Lautsprecher Membran wieder zurückgezogen wird und dann haben wir anschließend noch mal eine Pause, weil das Ganze soll jetzt züglich dann auch tatsächlich eine Wellenform ergeben. Im Ergebnis haben wir dann quasi eine Ereignisliste in der drin steht, okay, zum Zeitpunkt, also zum Z80, Zykluszeitpunkt so und so, wurde die Membran in die eine Richtung gezogen und als das Next Event ist dann quasi ein anderer Zeitpunkt, wo dann dran steht und jetzt wurde die Membran wieder losgelassen. Jetzt haben wir so eine Liste, so jetzt kommen wir zur Soundkarte und wollen quasi aus dieser Liste quasi wieder den Sound befüllen. Das heißt, wir machen einen Rendering auf Basis dieser Event Liste und das sieht dann so aus, dass ich quasi mein, zum Beispiel meinen 44,1 Kilohertz Soundkarte habe, die dann quasi in entsprechenden Abständen jeweils ein Sample erwartet und da ich ja die Event Liste kenne, kann ich jetzt quasi auch die einzelnen Samples zu bestimmten Zeitpunkt berechnen und kriege dann quasi diese Menge an Samples raus und schieke die zur Soundkarte und dann habe ich quasi wirklich einen exakt stimmigen Sound, der auch quasi Sample genau auch korrekt rauskommt, egal ob jetzt die Instruktion tatsächlich ein bisschen zu früh oder zu spät ausgeführt wurde. Weil der Audioausgang von dem Computer auch noch ein bisschen so ein Tiefpassfilter haben hatte, deswegen baue ich quasi auch Software-Technik hier noch einen Tiefpassfilter ein, das heißt, die Samples werden dann noch ein bisschen gerundet quasi beim Übergang, um den plötzlichen Übergang ein bisschen abzumildern. So, dann wollte ich, hatte ich angekündigt, noch ein bisschen was zu Stack-Trickery erzählen. Es gibt beim Z80 den Befehl RST, steht für Restart, ist sowas ähnlich wie ein Unterprogrammaufruf so ähnlich wie Call, nur dass beim Call muss man halt auch Adresse angeben, also der 3-byte Befehl kostet auch ein bisschen mehr. Wer und für häufig benutzte Unterprogrammaufrufe es sich dann anbietet, quasi solch einen fixen Befehl zu nehmen, ist also eigentlich nur eine Optimierung, ist jetzt hier aber für die Diskussion eigentlich auch nicht so wichtig. Was hier spannend ist, ist, dass im Code tatsächlich, wenn man mal ins BIOS reinschaut, tatsächlich sowas hier drin steht wie der Aufruf, dieses Befehl und dann steht einfach als nächster Befehl quasi ein Parameter, der eigentlich hier dran übergeben wird. Wie funktioniert das, weil normalerweise ein Rücksprung würde eigentlich hier sofort direkt in die nächste Zeile gehen und nicht quasi den Parameter überspringen. Der Trick dabei ist, dass sich quasi die Implementierung auf den Stack drauf schaut, holt sich quasi die Rücksprung Adresse, benutzt die dann quasi im Moment steht die Rücksprung Adresse auf dem Komma, das heißt dann wird dieser Wert tatsächlich auch gelesen, der wird hier dann für einen Vergleich verwendet, dann habe ich hier ein Increment Befehl, der quasi dann quasi die Rücksprung Adresse um eins erhöht und das ganze speichere ich wieder ab. So und wenn dann irgendwann später ein Return hier kommt, dann springt er tatsächlich, weil wir ja hier quasi um eins erhöht haben, springt er dann tatsächlich wieder zurück an die richtige Stelle, nämlich hinter den Parameter. Genau, da haben wir den Lesenzugriff und nach dem Increment zeigt er dann auf 2102 genau und springt dann dorthin zurück. So, weitere Fälle für Stack Trickery sind zum Beispiel, wenn in einem Unterprogramm festgestellt wird, ups, da ist ein Fehler aufgetreten, ich muss jetzt eine Fehler-Routine anspringen, dass man dann sagt, ich springe nicht zurück von da, wo ich gekommen bin, sondern ich leite den Kontrollschluss ganz woanders hin um, würde man bei heutigen hochsprachlichen Implementierungen so nicht mehr machen, heute würde man Exceptions oder sonst was verwenden, aber damals hat man das so gemacht. Das heißt, da modifiziert man extra die Rücksprung Adresse, ganz im Bewusstsein, da ist irgendwas passiert und man will jetzt eigentlich gar nicht zurück springen von woher man gekommen ist. Oder anderes, zum Beispiel Sprung Tabellen, wenn ich quasi irgendwo mir berechne, wo ich hinspringe, dann kann es auch manchmal einfacher sein, einfach dieses Ergebnis auf den Stack einfach drauf zu legen und dann mit einem Return Befehl quasi auf die berechnete Sprung Adresse hinzuspringen. So, genau, das wäre jetzt quasi der Teil meiner Folie, das Ganze ist in Unterknoop Public License veröffentlicht, ist auf GitHub zu finden, ja, genau. Und ein kleines Demo Video auf YouTube habe ich auch gemacht, was aber hauptsächlich die im Basic eingebauten Features zeigt. Allerdings auch kurz den Monitor, genau. Das würde ich nämlich jetzt für den Rest der Zeit nutzen. Wir haben ja eben gesehen, also die Simulation läuft noch, ich kann hier auch ganz normal arbeiten. So, und kann jetzt hier quasi abbrechen. Jetzt ist die Simulation gestoppt, das heißt, der Kase, es blinkt jetzt nicht mehr und kann jetzt gerade gucken, wo ich jetzt gerade bin. Jetzt hänge ich hier gerade in der Schleife drin, okay, das ist jetzt eine ungünstige Stelle. Es gibt aber eine Stelle, zum Beispiel, wo der Sound Befehl, den der Rechner hat, abgefragt wird. Das müsste die Stelle sein, wenn ich es jetzt richtig im Kopf hab. So, ich kann jetzt hier unter irgendwas machen, aber habe mich jetzt hier reingeklingt quasi an die Stelle, wo er in den Pasa Code dieses Befehl rein springt, das heißt, er müsste jetzt hier stoppen, tut er auch, okay. So, und bin jetzt quasi hier an der Stelle ankommen. Ja, man wundert sich vielleicht, warum hier plötzlich Anotation und Labels dranstehen. Ich habe hier so ein bisschen retrofritzigen gemacht. Ich habe die Möglichkeit geschaffen, über ein XML quasi im Nachhinein den Bioscode zu anotieren. Und beim Laden, beim Hochfahren des Emulators kann ich quasi diese externen in XML gemachten Anotationen mit reinladen und sehe dann quasi dann auch quasi ein kommentiertes Sampler-Listing und kann hier mich dann auch durchsteppen, kann hier quasi dann Schritt für Schritt durchgehen, sehen hier auch dann immer den Registerstatus und kann wie wir eben auch gesehen haben, auch ja, zu bestimmten Stellen, also Breakpoints quasi setzen im Moment nur einen einzigen Breakpoint, das will ich aber bei Gelegenheit auch noch erweitern und generell vielleicht auch noch die Bugging-Möglichkeiten ein bisschen verbreitern. Ja, das war es eigentlich im Wesentlichen. Danke für den tollen Vortrag, gibt es Fragen? Da vorne? Ja, auch danke von mir und war sehr schön der Vortrag. Ich wollte das mal fragen jetzt mit diesem ganzen Rom und dem ganzen der Software. Hast du die dann darunter geladen von dem Rechen oder gab es die woanders dann schon? Jetzt muss ich aufpassen, dass ich diplomatisch korrekt bleibe. Also ich habe ja das Original hier liegen. Leider ist das IC nicht gesockelt, das heißt ich kann es nicht rausnehmen und den eProm-Programm-Gerät legen. Es gibt aber andere Möglichkeiten wie zum Beispiel, das Ding hat ja auch Schnittstellen nach außen beziehungsweise ich kann ja auch quasi Daten einlegen und auf Kassette speichern und kann dann die auf Kassette gespeicherten Daten wieder analysieren und kommen so an die Daten dran. Es gibt aber auch schon Leute, die das gemacht haben, also wenn man lang genug im Netz sucht. Also es gibt auch andere Leute, die schon das gemacht haben. Und dann hast du aber auch, das wollte ich noch fragen, diese ganzen Zeilen analysiert und weißt eigentlich, wie das Ganze funktioniert, die Spios oder was da jetzt in dem Fall drin ist? Ungefähr die Hälfte habe ich dis-asampliert, wobei es auch da, wenn man im Netz schaut, es so ist, dass jetzt kommen wir an interessante Kopielberechtliche Fragen, weil das Ganze wurde verkauft von einer taiwanischen Firma oder ja aus Hongkong, Entschuldigung aus Hongkong und in dem Handbuch steht nirgendwo ein der Begriff Microsoft drin. Wenn man aber sich den Code anschaut, es gibt mittlerweile auch von Microsoft abgesegnet, eine Version 1.0 des damaligen Microsoft Basics aus dem frühen 70er Jahren, die tatsächlich mittlerweile auch legal im Netz verfüg-zugreifbar ist, verfügbar ist. Und wenn man mal vergleicht, sieht man nämlich, dass quasi das Bios von diesem Rechner mehr oder weniger nur ein Clone einer offensichtlich späteren Fassung des Microsoft Basics ist, ohne dass aber in dem Handbuch von dem Gerät tatsächlich der Name Microsoft erwähnt wird. Das ist jetzt eine spannende Frage, weiß man nicht so genau. Du hattest vorhin gesagt, dass du als eines deiner Ziele hattest alte Software von Kassetten oder so zu retten. Wie hast du das gelöst in dem Emulator? Noch nicht ganz. Da bin ich tatsächlich noch dran. Also was tatsächlich funktioniert? Ich weiß nicht, ob man es hier per Audio hört. Also es gibt auch hier einen Befehl zum Speichern. Mal schauen, ob man es hört. Nee, man hört es nicht. Also dann, okay. Auf jeden Fall, im Prinzip kann ich quasi die Ausgabe, die normalerweise auf Kassette läuft, dann umleiten auf Lautsprecher beziehungsweise dann auf Mikrofon und entsprechend ein- und auslesen. Allerdings ist da irgendwo noch ein kleiner Bug im wahrscheinlich, stimmt der Pegel nicht ganz oder die Polarität muss ich noch reinschauen. Ja. Was für ein Format hatten der auf Kassette? Kennen Sie City Standard oder irgendwas Eigenes? Ich nehme an, es ist was proprietäres. Also es gibt mittlerweile auch im Netz Formate, die das ein bisschen beschreiben. Also im Wesentlichen ist es so, dass es eine Modulation ist. Also ein Einserbit und Nullerbit. Also ein Einserbit erzeugt längere, langwelligere Zyklen. Also zwei unterschiedliche Frequenzen für Null und Eins. Gibt es noch weitere Fragen? Wenn dem nicht so ist, dann sollten wir uns nochmal ganz herzlich bei Jürgen für den tollen Vortrag und für den Emulator bedanken.