Der Schneider CPC 464 ist ein Relikt aus den 1980er Jahren und stand als solches in meinem Elternhaus. Dazu muss man sagen, dass er zu diesem Zeitpunkt eigentlich auch schon ins Museum gehört hätte, aber er war mein erster Kontakt mit Computern und so verbrachte ich einen nicht unwesentlichen Teil meiner Kindheit und Jugend mit diesem Gerät. In erster Linie natürlich mit den verfügbaren Spielen.
Vor einiger Zeit hatte ich die Idee, auf einem dieser alten Schätzchen eine Webseite zu hosten. Warum? Spaß am experimentieren!
Dabei wurde recht schnell klar, dass es zwar eine Reihe von Informationen im Netz gibt, aber das Wenigste davon wirklich vom ersten bis zum letzten Schritt reicht. Diese Seite unternimmt den Versuch, diese Lücke zu schließen.
Der CPC ist ein 8-bit-Rechner und bietet nur relativ wenige Möglichkeiten ihn mit moderner Hardware kommunizieren zu lassen. Eine dieser Möglichkeiten ist der Druckerport, welcher aber in der ersten Version des CPC (um die es hier geht) ein paar Probleme mit sich bringt. Dazu später mehr. Immerhin gibt es aber eine Schnittstelle, welche im Prinzip und mit ein wenig Bastelei mit moderner Hardware kommunizieren kann.
Das erste Ziel dieses Projektes ist es, vom CPC bereitgestellte Texte zum PC zu übertragen und umgekehrt. Es wird eine Python-Erweiterung in C geschrieben, welche den Datentransfer vom CPC zum PC (und umgekehrt) erleichtert.
Natürlich ist alles was hier beschrieben wird nur auf eigene Gefahr nachzubauen. Es wird keinerlei Gewähr oder Haftung für Hardwareschäden, Verletzungen oder die Funktionsfähigkeit der beschrieben Hard- und Software übernommen. Generell sollte man beim Löten immer vorsichtig sein und sich der Gefahr bewusst sein, dass man den PC bzw. CPC beschädigen oder sogar zerstören kann.
Der Anschluss des Parallelports besteht aus 25 Leitungen. Wird sein Stecker von vorne betrachtet, so beginnt die Zählung in der oberen Reihe von rechts bei eins und endet links bei 13. In der unteren Reihe wird ebenfalls von rechts nach links und von 14 bis 25 gezählt.
Der Parallelport hat drei Register: Daten-, Status- und Kontrollregister. Das Datenregister hat die Speicheradresse 0x378 und ist für die Pinne 2 bis 9 zuständig. Über diese Pinne kann ein Byte parallel (also auf einmal) entweder gelesen oder geschrieben werden. Ob das Datenregister gelesen oder Daten hineingeschrieben werden sollen, entscheidet ein Bit im Kontrollregister. Dazu später mehr. Die Adresse des Kontrollregisters ist 0x37A. Das Statusregister hat die Speicheradresse 0x379.
Um die grundsätzliche Funktionsfähigkeit des Parallelports am PC zu testen und später das Verhalten der Schnittstelle zu visualisieren, bietet es sich an, für den PC eine Sub-D 25 Plugbox zu verdrahten. Mit dieser lassen sich leicht LED's an den Parallelport des PC's anschließen und ansteuern.
Die Plugbox welche ich verwendet habe ist "Male" und hat 25 Anschlusspinne sowie 25 Anschlüsse an welche leicht die Dioden und Widerstände angeschlossen werden können.
Die Datenpinne sind die Nummern 2 bis 9. Für die ersten Versuche wird die Plugbox also an den Anschlüssen 2 bis 9 mit Dioden bestückt. Bei jeder Diode gilt: Das lange Beinchen kommt an den Datenpin (also die Pinne zwei bis neun) und an das kurze wird ein Vorwiderstand (1 kOhm) angelötet welcher dann an die Anschlüsse 18 bis 25 (jeweils einer) angeklemmt wird. Die letztgenannten Anschlüsse liegen an Masse an. Sollten mehr Dioden (z.B. um auch andere Register zu visualisieren) benötigt werden, so können sich auch mehrere Dioden eine Masse teilen.
Für das ganze Projekt werden die Pinne des Datenregisters sowie Pin 1 (Kontrollregister) und Pin 10 (Statusregister) benötigt. Pin 1 ist wird als Rückkanal zum CPC benutzt werden. Da das Datenregister des CPC nur beschrieben werden, nicht aber gelesen werden kann, ist dies die einzige Möglichkeit Daten in Richtung des CPC zu senden.
Sobald alle Dioden und Widerstände angeschlossen sind, kann die Plugbox an den Parallelport des PCs angeschlossen werden. Optimalerweise sollte man ein entsprechendes Kabel zur Hand haben, damit die Plugbox besser in Sichtweite positioniert werden kann.
Nun ist es an der Zeit sich der Software zu widmen und einen ersten Versuch zu unternehmen, die Dioden nach dem eigenen Willen leuchten zu lassen. Dazu wird das folgende kleine C-Programm geschrieben und kompiliert:
#include <sys/io.h> int main(void) { /* Open data and controll port */ ioperm(0x378,1,1); ioperm(0x37A,1,1); /* Make sure to have set the data port to write mode */ outb(0, 0x37A); /* Write a full byte to the data port */ outb(255, 0x378); return 0; }
Das Kompilieren und ausführen sollte so funktionieren:
$ gcc -O example.c -o example $ ./example
Da nur root auf den Speicher des Parallelports zugreifen darf, muss das Programm mit sudo (oder eben als root) gestartet werden. Wenn alles geklappt hat, dann strahlen nun alle LED's:
Natürlich kann man auch nur einzelne LEDs an und ausschalten. Dazu ist es erforderlich, dass man versteht was hier passiert. Das Datenregister ist acht Bit -also ein Byte- lang. Ist ein Bit davon gesetzt, so leuchtet die entprechende LED. Im Beispiel wurde der Dezimalwert 255 in das Register geschrieben. Dieses entspricht im Binärsystem 11111111. Also acht Einsen. Wird eine dezimale 1 in das Register geschrieben, so entspricht dies im Binärsystem ebenfalls 1 (die anderen sieben Stellen kann man sich als mit Nullen gefüllt vorstellen).
Eine dezimale 2 entspricht also 00000010 und eine 3 wird zu 00000011. Werden diese Zahlen in das Register geschrieben, so wird bei der 2 die LED am Pin 2 leuchten und bei einer 3 werden die LED's an Pin 1 und 2 leuchten. Die LED's und der Datenport sind also eine vollständige Abbildung des Binärsystems im Bereich von 0 bis 255.
Um nun eine bestimmte einzelne LED zum Leuchten zu bringen müssen also die Binärwerte, welche jeweils nur an der entsprechenden Stelle eine 1 haben gefunden werden. Dies sind praktischerweise natürlich immer die Werte welche das Doppelte des vorherigen sind. Also im Dezimalsystem: 1, 2, 4, 8, 16, 32, 64, 128. Wenn diese Zahlen in einer Schleife an das Datenregister übergeben werden, so wird aus den LED's ein Lauflicht:
#include <stdio.h> #include <unistd.h> #include <sys/io.h> int main(void) { /* Open data and controll port */ ioperm(0x378,1,1); ioperm(0x37A,1,1); /* Make sure to have set controll port to write mode */ outb(0, 0x37A); /* We need a pause to see the effect */ int pause = 90000; int pins[8] = {1, 2, 4, 8, 16, 32, 64, 128}; /* A while loop to run forever */ int i; while(1) { for(i = 0; i <= 7; i++) { outb(pins[i], 0x378); usleep(pause); /* To slow it down */ } outb(0, 0x378); } return 0; }
Der Printer Port am CPC besteht aus 34 Ätzungen auf der Hauptplatine. Eine Besonderheit ist, dass er eigentlich aus 36 bestehen müsste, man aber auf die Anschlüsse 18 und 36 verzichtet hat. Bei der Nummerierung der Anschlüsse geht diese Beschreibung aber davon aus, dass es 36 sind.
Die datenführenden Anschlüsse sind die Nummern zwei bis acht. Es gibt also nur sieben, womit nur reines ASCII übertragen werden kann. Außerdem sind sie (im Gegensatz zu den Datenpinnen am PC) nur schreibend zu verwenden; sie können nicht ausgelesen werden.
An Anschluss elf liegt das BUSY-Signal. Dieser Anschluss kann ausgelesen werden und bildet damit also den Kanal vom PC zum CPC. Umgekehrt können die Datenanschlüsse vollständig an den PC gesendet und von diesem auch ausgelesen werden.
Die folgende Darstellung zeigt die Anschlüsse des Printer Ports in der Rückansicht des CPC. Zwischen den Anschlüssen vier und fünf bzw. 21 und 22 ist die Platine als Verpolungsschutz eingekerbt.
Damit die Verbindung hergestellt werden kann, muss der CPC vorbereitet werden. Hier kommt nun ggf. der Lötkolben zum Einsatz. Es empfiehlt sich, einen Edge Connector mit 34 Anschlüssen und ein entsprechendes Flachkabel zu verwenden. Das Kabel an diesem an zu schließen ist dank Schneid-Klemm-Technik relativ einfach. Am anderen Ende des Kabels wird ein Sub-D 25-Anschluss verlötet.
Die Anschlüsse des Statusregisters (also die Nummern 2 bis 8) werden auf ihren entsprechenden Gegenstücken am Sub-D 25-Anschluss angelötet. Hierbei ist zu beachten, dass bei einem Edge Connector die Zählweise der Anschlüsse nicht der Zählweise der Platinenanschlüsse entspricht. Die Zählung geht so vor, dass in der oberen Reihe alle geraden und in der unteren alle ungeraden Nummern liegen. Also etwa so:
Es muss also beachtet werden, dass die nebeneinander liegenden Kabel nicht benachbarte Kabel im Sinne des Platinenanschluss sind. Die folgende Tabelle zeigt wie die Anschlüsse des CPC mit dem Sub-D 25-Anschluss verbunden werden müssen:
# CPC | Funktion CPC | # Sub-D 25 | Funktion PC |
---|---|---|---|
1 | Strobe | 10 | ACK |
2 | Datenbit 0 | 2 | Datenbit 0 |
3 | Datenbit 1 | 3 | Datenbit 1 |
4 | Datenbit 2 | 4 | Datenbit 2 |
5 | Datenbit 3 | 5 | Datenbit 3 |
6 | Datenbit 4 | 6 | Datenbit 4 |
7 | Datenbit 5 | 7 | Datenbit 5 |
8 | Datenbit 6 | 8 | Datenbit 6 |
9 | GND | 9 | Datenbit 7 |
10 | N.c. | - | - |
11 | Busy <- | 1 | Strobe |
12 | N.c. | - | - |
13 | N.c. | - | - |
14 | GND | - | - |
15 | N.c. | - | - |
16 | N.c. | GND | - |
17 | N.c. | - | - |
18 | Existiert nicht | - | - |
19 | GND | 18 | GND |
20 | GND | 19 | GND |
21 | GND | 20 | GND |
22 | GND | 21 | GND |
23 | GND | 22 | GND |
24 | GND | 23 | GND |
25 | GND | 24 | GND |
26 | GND | 25 | GND |
27 | N.c. | - | - |
28 | GND | 25 | GND |
29 | N.c. | - | - |
30 | N.c. | - | - |
31 | N.c. | - | - |
32 | N.c. | - | - |
33 | N.c. | - | - |
34 | N.c. | - | - |
35 | N.c. | - | - |
36 | Existiert nicht | - | - |
Auf dem Foto oben kann man die beiden über Kreuz gelegten Kabel 1 (auf 10) und 11 (auf 1) erkennen. Somit wird das Strobe-Signal (Anschluss 1) des CPC später im PC im Statusregister ankommen, was das auslesen des Signals gegenüber einem Anschluss im Kontrollregister deutlich vereinfacht, da dieses u.a. dazu verwendet wird, das Datenregister in den Lese- bzw. Schreibmodus zu setzen.
Getested weden kann das ganze mit der Plugbox welche schon beim PC zum Einsatz kam. Sie muss hierfür nicht ggf. etwas angepasst werden, da die GND-Anschlüsse nicht alle zu denen des PC passen müssen. Dann kann auch hier ein Lauflicht programmiert werden:
Um einen einzelnen Buchstaben über den Parallelport an den PC zu senden, muss der binäre Wert des entsprechenden ASCII-Zeichens in das Register des Ports geschoben werden. Dies geschieht mit dem folgenden Aufruf:
Damit der PC etwas empfangen kann, muss der folgende C-Code in einer Datei (example.c) gespeichert, kompiliert und ausgeführt werden:
#include <stdio.h> #include <unistd.h> #include <sys/io.h> int main(void) { /* Open data and controll port */ ioperm(0x378,1,1); ioperm(0x37A,1,1); /* Setting control register to read mode */ outb(255, 0x37A); /* Reading the data port in a loop forever */ while (1){ printf("Read from data port: %c\n", inb(0x378)); /* To slow things down a bit */ usleep(100000); }; return 0; }
Nicht vergessen, dass nur root auf den Parallelport zugreifen darf. Also die Datei ggf. mit sudo oder direkt als root ausführen. Wenn alles geklappt hat, dann sollte nun folgendes zu sehen sein:
Read from data port port: h Read from data port port: h ...
Es kann nur ein Bit übertragen werden. Um auf dem CPC die Änderungen zu sehen, kann das folgende Programm laufen:
Hier wird zunächst der Eingangsport an Adresse &F500 in die Variable "a" gelegt. In Zeile 30 werden die drei Werte aus dem Wert in "a" generiert. Hier ist zu beachten, dass für den State ein bisschen getrixt wird: Es wird per MID$ nur die erste Stelle des vorher mit BIN$ auf 7 Stellen festgelegten Strings (BIN$ macht aus einer Dezimalzahl einen String welcher den binären Wert darstellt) ausgegeben. Das heisst, wir haben hier keine echte Zahl sondern ein Char, das eine 0 abbildet. Für diesen Anwendungsfall reicht das aber vollkomment aus.
Zeile 40 ist nur eine Verzögerung, damit die Routine nicht mit maximaler Geschwindigkeit läuft sondern etwas ausgebremst wird. Zeile 50 startet einfach alles wieder von Anfang.
Auf dem PC muss folgender Code kompiliert und dann ausgeführt werden:
#include <stdlib.h> #include <stdio.h> #include <sys/io.h> int main(int argc, char *argv[]) { ioperm(0x37A,1,1); int arg = atoi(argv[1]); printf("Sending %d", arg); outb(arg, 0x37A); return 0; }
Die main-Funktion nimmt den ersten Parameter aus der Kommandozeile und sendet den Wert an den Kontrollport. Dem Programm kann also beim Aufruf eine Zahl übergeben werden. Hier bieten sich 0 und 1 an, da so im CPC der gewünschte Effekt eintritt: Der ausgelesene Pin 11 ändert seinen Zustand entsprechend dem gesendeten Wert.
./example 0 ./example 1 ...
Die Ausgabe auf dem CPC sieht dann in etwa so aus:
Dec | Bin | State |
122 | 1111010 | 1 |
Dec | Bin | State |
122 | 1111010 | 1 |
Dec | Bin | State |
58 | 0111010 | 0 |
Dec | Bin | State |
58 | 0111010 | 0 |
Dem aufmerksamen Leser wird aufgefallen sein, dass der Beispielaufruf und die Beispielausgabe scheinbar in verkehrter Reihenfolge hier angezeigt werden. Schließlich wird doch zuerst eine 0 und dann eine 1 gesendet während der CPC es genau anders herum anzeigt. Dies ist allerdings kein Fehler: Der Pin 1 des Parallelports am PC ist invertiert. Wird eine 1 im Speicher gesetzt so, liegt am Pin keine Spannung an und umgekehrt. Das mag spannende technische Gründe haben, aber hier sollte man es einfach nur beachten oder zumindest zur Kenntnis nehmen.
Eine Python-Erweiterung bietet sich an um einfacher auf den Port zugreifen zu können. Die im folgenden beschriebene Erweiterung besteht aus einem Modul pyparport welches eine Klasse PyParport enthält. Der Quellcode der Erweiterung sowie der der später folgenden Beispielprogramme, kann hier heruntergeladen werden (die Erweiterung liegt im Verzeichnis "C").
Diese wiederum hat die Methoden data (Datenregister), control (Kontrollregister) und status (Statusregister). Jede dieser Methoden bringt die Methoden read() und write().write() muss der Wert der geschrieben werden soll als Dezimalzahl übergeben werden.
Der folgende C-Code stellt den hardwarenahen Teil der Python-Erweiterung dar:
#include#include #include #include static PyObject* PortError; static PyObject* readport(PyObject* self, PyObject *args) { /* Open registers */ ioperm(0x378,1,1); ioperm(0x379,1,1); ioperm(0x37A,1,1); char *reg; int addr; if (!PyArg_ParseTuple(args, "si", ®, &addr)) { return NULL; } PyArg_ParseTuple(args, "si", ®, &addr); if (!strcmp(reg, "d")) { /* Set dataport to read mode */ outb(255, addr+2); /* Read the port */ return Py_BuildValue("i", inb(addr)); } else if (!strcmp(reg, "s") | !strcmp(reg, "c")) { /* Read the port */ return Py_BuildValue("i", inb(addr)); } else { PyErr_SetString(PortError, "Please choose a valid register: d(ata), c(ontroll) or s(tatus)"); return NULL; } } static PyObject* writeport(PyObject* self, PyObject *args) { /* Open registers */ ioperm(0x378,1,1); ioperm(0x379,1,1); ioperm(0x37A,1,1); int val; char *reg; int addr; if (!PyArg_ParseTuple(args, "isi", &val, ®, &addr)) { return NULL; } PyArg_ParseTuple(args, "isi", &val, ®, &addr); if (!strcmp(reg, "d")) { /* Set dataport to write mode */ outb(0, addr+2); /* Set the port */ outb(val, addr); } else if (!strcmp(reg, "s") | !strcmp(reg, "c")) { /* Set the port */ outb(val, addr); } else { PyErr_SetString(PortError, "Please choose a valid register: d(ata), c(ontroll) or s(tatus)"); return NULL; } Py_RETURN_NONE; } static PyMethodDef pyparport_funcs[] = { {"read", (PyCFunction)readport, METH_VARARGS}, {"write", (PyCFunction)writeport, METH_VARARGS}, {NULL} }; #if PY_MAJOR_VERSION >= 3 // Python3 compatibilty static struct PyModuleDef _interface = { PyModuleDef_HEAD_INIT, "_interface", "Python parallel port object", -1, pyparport_funcs }; PyMODINIT_FUNC PyInit__interface(void) { PyObject *module; module = PyModule_Create(&_interface); PortError = PyErr_NewException("pyparport.PortError", NULL, NULL); Py_INCREF(PortError); PyModule_AddObject(module, "PortError", PortError); return module; } #else // Python2 compatibilty void init_interface(void) { PyObject *module; module = Py_InitModule3("_interface", pyparport_funcs, "Python parallel port object"); PortError = PyErr_NewException("pyparport.PortError", NULL, NULL); Py_INCREF(PortError); PyModule_AddObject(module, "PortError", PortError); } #endif
Der Code wird in einer Datei pyparport.c gespeichert, welche dann ihrerseits in ein Unterverzeichnis _interface gelegt wird. Neben diesem Verzeichnis wird ein weiteres Verzeichnig pyparport gelegt, welches eine Datei __init__.py mit folgendem Inhalt enthält:
#!/usr/bin/env python # coding: utf-8 import _interface PortError = _interface.PortError class Port(object): """ Abstraction layer for a more comfortable use """ def __init__(self, port, addr): self.port = port self.addr = addr def read(self): return _interface.read(self.port, self.addr) def write(self, value): return _interface.write(value, self.port, self.addr) class PyParport(object): """ The main class which implements the interface to the port """ def __init__(self, base_addres=0x378): self.data_address = base_addres self.status_address = base_addres + 1 self.control_address = base_addres + 2 self.data = Port("d", self.data_address) self.status = Port("s", self.status_address) self.control = Port("c", self.control_address)
Außerdem wird noch eine setup.py im Hauptverzeichnis des Projektes benötigt, mit deren Hilfe die Erweiterung kompiliert und in die Pythonumgebung installiert wird. Es bietet sich an, die Erweiterung in ein Virtualenv zu installieren, da es bei einer systemweiten Installation nur sehr umständlich möglich ist, sie wieder zu entfernen. Die Datei hat den folgenden Inhalt:
from setuptools import setup, Extension extension = Extension("_interface", sources=["_interface/pyparport.c"]) setup(name="pyparport", version="0.6", description="This module provides the possibility to connect to parallel ports from Python.", license='GPLv3', packages=['_interface', "pyparport"], author='Christian Kokoska', author_email='christian@softcreate.de', ext_modules=[extension])
Somit sollte das Projekt folgendermaßen aufgebaut sein:
$ cd pyparport $ tree . . ├── _interface │ └── pyparport.c ├── pyparport │ └── __init__.py └── setup.py
Die Datei sollte in das Virtualenv (pythonenv) kopiert werden. Mit dem folgenden Aufruf wird die Erweiterung dann in selbiges installiert:
$ bin/python setup.py install
Die Benutzung wird durch das folgendes Codebeispiel erläutert:
from pyparport import PyParport parport = PyParport() # Show the data of the data register parport.data.read() # Show the data of the control register parport.control.read() # Show the data of the status register parport.status.read() # To write 0 to the data register parport.data.write(0) # To write a 255 to the data register parport.data.write(255)
#!/bin/env python from time import sleep from pyparport import PyParport parport = PyParport() while True: end = False content = "" cur = parport.status.read() while not end: if parport.status.read() == cur: continue else: cur = parport.status.read() if not parport.data.read() == 1: content += str(unichr(parport.data.read())) else: end = True print content sleep(0.001)
#!/usr/bin/env python import os from time import sleep from flask import abort, Flask, Response from pyparport import PyParport parport = PyParport() app = Flask(__name__) app.config["PROPAGATE_EXCEPTIONS"] = True class CpcLocked(Exception): pass lockfilepath = "/tmp/cpc464.lock" class EnsureFileLock: """Creates a lock file on script initialisation and removes it at it's end.""" def __enter__(self): if os.path.isfile(lockfilepath): raise CpcLocked("The CPC464 is locked or works on another request.") open(lockfilepath, "a").close() def __exit__(self, type, value, traceback): os.remove(lockfilepath) def change_state(): if parport.control.read() == 192: parport.control.write(255) elif parport.control.read() == 255: parport.control.write(0) @app.route('/') def index(): try: with EnsureFileLock(): change_state() end = False content = "" cur = parport.status.read() while not end: if parport.status.read() == cur: continue else: cur = parport.status.read() if not parport.data.read() == 1: content += str(unichr(parport.data.read())) else: end = True return Response(content) except CpcLocked as e: return Response(e)
#! coding: utf-8 #!/bin/env python import binascii import os import re from time import sleep from pyparport import PyParport parport = PyParport() def receive(): end = False url = "" cur = parport.status.read() while not end: if parport.status.read() == cur: continue else: cur = parport.status.read() if not parport.data.read() == 1: url += str(unichr(parport.data.read())) else: end = True parport.control.write(0) send(url) def send(url): print "CPC asks for URL: {}".format(url) url = url.replace("\0", "") try: content = os.popen("lynx --dump --nolist " + url).read() except TypeError as e: print e content = "404" content = re.sub("\s+", " ", content .replace("\n", " ") .replace("\xf6", "oe") .replace("\xd6", "OE") .replace("\xe4", "ae") .replace("\xc4", "AE") .replace("\xfc", "ue") .replace("\xdc", "UE") .replace("\xdf", "ss")) content = "00000000" + bin(int(binascii.hexlify(content), 16))[2:] content += "00000100" cur = parport.status.read() while cur != 63: cur = parport.status.read() for i in content: while cur == parport.status.read(): continue if i == "0": parport.control.write(0) elif i == "1": parport.control.write(255) cur = parport.status.read() receive() receive()
Auch wenn es noch Verbesserungspotential gibt und die eine oder andere Routine noch Schwachstellen hat, so ist es doch möglich einen Computer, der mehr als 30 Jahre alt ist, mit moderner Hardware kommunizieren zu lassen und ihn sogar auf die eine oder andere Weise zu einem Teil des Internets zu machen.
Ich für meinen Teil habe gelernt, dass selbst in einem so alten Rechner sehr viel Potential steckt und es sicher noch viel mehr drin. Verbesserungsvorschläge und Anregungen sind immer herzlich willkommen!
Nicht zuletzt möchte ich mich bei meinem Vater für die Hilfe bei diesem Projekt bedanken. Ich habe die gemeinsamen Stunden mit Lötkolben, Dioden, Kabeln und Steckern sehr genossen.
Dieses Werk fällt unter diese Lizenz:
Creative Commons by-nc-sa 4.0