Bevor der Floppykurs so richtig in die Vollen geht, drehen wir noch eine theoretische Ehrenrunde. Es geht um das Drumherum des Floppycontrollers, dessen Programmierung sich bis jetzt kaum jemand zutraut. Aber vielleicht wird dank dieser Serie bald der Kopierschutz und die Datenpackerei auf der Diskette zum Volkssport - wenn Sie ein bißchen durchhalten.
Zur Unterstützung Ihrer eigenen Experimente gibt es eine Erweiterung für den Diskmonitor MINIMON, den ich in der letzten ST vorgestellt habe. Sie wird aus Platzgründen leider erst im nächsten Heft abgedruckt. Die Schwierigkeit dabei ist, daß Floppyprogrammierung nur in Assembler denkbar ist - aber keine Angst, Sie müssen sich nicht monatelang mit moveq, lea und Konsorten herumschlagen. Hauptsache ist, daß Sie die Assemblerroutinen, die in das BASIC-Programm eingebunden werden, anwenden können. Und dazu muß ich wohl oder übel auch ein wenig trockene Theorie erklären.
Das mögen sich einige Leser fragen, die bis jetzt in diesem Kurs noch nicht dabei waren oder die nicht so recht aufgepaßt haben. Der Floppycontroller ist ein Chip, der nur für den Datenaustausch mit dem Laufwerk zuständig ist. Er entlastet den Prozessor, indem er ihm die lästige Arbeit abnimmt, die Diskettendaten zu ver- und entschlüsseln sowie sie zu lesen und zu schreiben.
Zunächst einige Worte über die Aufzeichnung. Daß eine Diskette in Spuren und Sektoren aufgeteilt ist, wissen Sie. Sehen wir uns so eine Spur an (siehe Bild 1).
Sie sehen, daß Sie nichts sehen. Das ist auch ganz in Ordnung so, denn schließlich haben wir der Spur noch keine Informationen für ihr künftiges Leben mitgegeben. Nun könnten Sie sich vorstellen, die Bits würden einfach nacheinander auf die Spur gekleistert: Magnetisierter Fleck für „1“, nicht magnetisierter für „0“. Nicht schlecht für den Anfang, aber unbrauchbar. Denn so eine Spur ist rund, und jetzt erzählen Sie mir mal, wo ein Kreis beginnt. Eine Markierung anbringen, nach jeder Umdrehung des Laufwerksmotors einen Indeximpuls erzeugen? Gute Idee, wird auch tatsächlich praktiziert, nur reicht das noch nicht. Ein Laufwerk kann mit vernünftigem technischen Aufwand nicht dazu gebracht werden, immer gleich schnell zu drehen.
Irgendwie muß man also den Lesevorgang mit der Geschwindigkeit des Laufwerks synchronisieren. Ein Verfahren dazu heißt MFM, es wird beim ST und bei den meisten anderen Rechnern angewendet und codiert den Taktimpuls des Controllers mit in die Datenbits ein. Schreiben wir einmal einen solchen Track (siehe Bild 2).
Jetzt können wir also einwandfrei Bits lesen, ohne taktlos zu werden. Geschafft? Denkste. Wie erkenne ich denn jetzt, wo ein bestimmtes Byte anfängt? Natürlich, Indeximpuls abwar-ten, Bits lesen und dabei immer bis 8 Bits (=1 Byte) zählen, bis man beim gewünschten Byte ankommt... Nein, das ist es immer noch nicht. Man will ja nicht immer den ganzen Track lesen, sondern vielleicht nur einen Sektor, und außerdem könnte es ja sein, daß die Synchronisation mit dem Indeximpuls nicht ganz so glatt läuft; dann fängt der Controller beim falschen (zum Beispiel beim zweiten) Bit zu lesen und zu zählen an. Ergebnis: Lesefehler, Bombenanschläge, intergalaktische Kollisionen.
In der Praxis schreibt man spezielle Bytes auf die Spur, die Synchronisationsbytes. Diese Bytes erkennt der Controller daran, daß sie ohne Taktinformationen geschrieben werden. Meistens schreibt man mehrere Sync-Bytes hintereinander, und der Controller verschlingt solange Bits, bis er mal einen gültigen Wert für ein Sync-bytes ($A1) erkannt hat. Dann weiß er: Hoppla, das nächste Bit ist der Anfang eines Bytes. Hier also unser Track, wie er wirklich aussieht (das heißt, immer noch etwas verbogen und nicht so schön rund wie in Wirklichkeit) (siehe Bild 3 ).
Die Markierungen der einzelnen Bits habe ich der Übersicht wegen weggelassen. „Gap“ bezeichnet einen Abschnitt von Lückenbytes, die der Controller braucht, um sich zwischen den Daten zu erholen und klar Schiff zu machen. Dabei ist er allerdings fixer, als Atari und sogar sein Hersteller ihm Zutrauen; darauf beruht meine Idee, die Lückenbytes soweit zu kürzen, bis ein elfter Sektor auf die Spur paßt HYPERFORMAT).
Nach den Syncbytes folgen die sogenannten Adreßmarken, mit denen bestimmte Datenstrukturen angekündigt werden (Datenheader, also Vorspann oder Daten). Auch sie werden ohne Taktimpulse geschrieben. Nach den Daten- bzw. Vorspannblöcken folgt jeweils eine 2-Byte-Checksumme, mit der man die Gültigkeit der Informationen nachprüfen, aber auch jede Menge Unsinn treiben kann.
Die ganze Lese-/Schreibarbeit und die Codierungsvorgänge werden dem Abt des Klosters Atari ST, Prozessorus Maximus, von seinem Schreiberling Flopius Controllus abgenommen. Nun ist aber Prozessorus auch noch zu bequem, um Controllus die Daten selbst zu bringen oder sie abzuholen: Er setzt einen Boten ein, D.M.A. Controllus genannt, der speziell für Datenschaufeleien dieser Art angeheuert wurde. Ernst beiseite: Der DMA-Controller (Direct Memory Access) bekommt beim Schreiben vom Prozessor die Instruktion, was der Floppycontroller tun soll. Hier dient er als Relaisstation; außerdem bekommt er eine Speicheradresse, wo die Informationen stehen, die er dem Controller Häppchen für Häppchen selbständig übermitteln soll. Der Prozessor zieht sich solange würdevoll zurück, um das Ende der Übertragung abzuwarten, das er entweder im Erreichen einer bestimmten DMA-Adresse oder an einer Unterbrechungsanforderung des Controllers merkt. Das Lesen funktioniert analog.
Warum so ein Aufwand? Während der DMA-Chip sich um die Speicherschaufelei kümmert, kann der Prozessor sich seiner Freizeit oder wichtigeren Aktivitäten widmen. Vor allem für Multitasking-Betriebssysteme ist das überlebenswichtig. Nur leider nützt TOS diese Möglichkeiten nicht aus und verbrät die kostbare Prozessorzeit in Warteschleifen, solange gelesen und geschrieben wird. In RTOS dagegen kann man sogar während des Formatierens noch andere Dinge erledigen! Ein Schema soll die folgenden, recht kniffligen Ausführungen erhellen:
( siehe Bild 4 ).
Es zeigt alle Chips, die bei der Ein-/ Ausgabe auf Diskette zusammenspielen. Nicht nur Prozessor, DMA-Controller und Floppycontroller sind -wie man sieht - am Lesen/Schreiben beteiligt. Die Unterbrechungsanforderung des Floppycontrollers, mit der er vehement auf das Ende seiner Arbeit hinweist, gelangt zum MFP68901, einem Baustein im ST, der unter den Chips des ST nur „der Störenfried“ heißt, weil er ständig andere bei der Arbeit unterbricht: Er verwaltet alle Interruptanforderungen der Peripherie.
Dazu kommt noch, daß der Controller eigentlich nur ein Laufwerk ansprechen kann - und sogar nur eine Seite! Aber hilfsbereit, wie er ist, springt da der Soundchip ein und leiht dem Controller drei seiner Portbits und schaltet mit ihnen je nach Bedarf zwischen den Seiten und den Laufwerken (maximal zwei) um.
Wie diese Chips Zusammenwirken und wie man sie dazu bringt, sich gegenseitig zu unterstützen, wird uns nun beschäftigen. Die Dreh- und Angelpunkte beim Diskzugriff sind Floppycontroller (auch FDC für Floppy Disk Controller) und DMA-Controller.
Die Aufgaben des DMA-Chips wurden ja schon kurz erwähnt: Er hievt Daten vom Speicher zum Floppycontroller (oder auch zum Controller der Festplatte) und von dort aus wieder in den Speicher, ohne daß sich der Prozessor darum kümmern muß. Der Prozessor gibt dem DMA-Chip seine Anweisungen, was er gerne da und dort hätte, und beschäftigt sich dann lieber damit, Invaders oder StripPoker zu spielen.
Die Anweisungen des Prozessors landen in ein paar Adressen, die ich jetzt in ihre Bits zerlegen werde. Auch die Ausgabe des DMA-Chips (Statusmeldungen und ähnliches) läuft über diese Adressen (siehe Tabelle 1).
Adresse Bedeutung Tabelle 1 | |||||||||||||||||||
$FF8604 | REGISTERZUGRIFF Nur die unteren 8 Bits sind belegt. Aus dieser Adresse kann man den Inhalt von DMA-Chip- und Floppycontroller-Registern lesen oder man kann sie durch diese Adresse verändern. Welches Register ausgewählt ist, hängt von der folgenden Adresse ab. | ||||||||||||||||||
$FF8606 | DMA-Modus, DMA-Status Für den Schreibzugriff gilt folgende Belegung:
| ||||||||||||||||||
$FF8609 | DMA-Zähler, Highbyte. Hier liegen die obersten 8 Bit eines Zählers, der angibt, welche Adresse vom DMA-Chip gerade bearbeitet wird. | ||||||||||||||||||
$FF860B | DMA-Zähler, Midbyte. Die mittleren 8 Bit. | ||||||||||||||||||
$FF860D | DMA-Zähler, Lowbyte. Die unteren 8 Bit. |
Wie geht man mit diesem Wust nun um? Einzelne Register des Floppycontrollers anzusprechen, ist ja noch relativ einfach: Man schreibt zum Beispiel den Wert $80 in das Modusregister des DMA-Chips. Damit hat man das Statusregister des Floppycontrollers ausgewählt, das man dann im Zugriffsregister $FF8604 auslesen kann. Anderes Beispiel: $184 ins Modusregister, und schon kann man in $FF8604 das Sektorregister beschreiben (halten Sie sich immer die Tabelle 1 vor Augen!). Und so weiter. Nehmen wir an, wir wollten jetzt den Speicherbereich von JETZT__GEHTS__ABER—LOS bis DA__HÖRTS__ABER—AUF auf die Diskette schreiben, beispielsweise als Sektor. Kompliziert, denken Sie? Irrtum. SEHR kompliziert. Das Kochrezept dafür.
Natürlich war’s das noch lange nicht. Jetzt müßte man noch das Ende der Übertragung abwarten. Das macht man entweder, indem man die Unterbrechungsanforderung des Floppycon-trollers abwartet, mit der er Fehler oder das Ende seiner Mühsal anzeigt, oder indem man den DMA-Zähler beäugt, abwartet, bis die Übertragung bei einer bestimmten Adresse angelangt ist (in unserem Fall bei DA__HÖRTS__ABER__AUF) und dann den Controller brutal abwürgt (dazu gibt es einen speziellen Strangulationsbefehl). Und wer ganz sauber arbeitet, sollte sich danach noch den Status von DMA-Chip und Floppycontroller anschauen, um eventuelle Fehler abzufangen...
Damit Sie sich schnell zurechtfinden und nicht immer in die einzelnen Bits gehen müssen, habe ich in einer Tabelle die Werte zusammengefaßt, die man in die DMA-Modus-Adresse schreiben muß, um bestimmte Register auszuwählen. Hier die Tabelle für das Lesen von Registern:
Wert | angesprochenes Register |
---|---|
$80 | Statusregister des Floppycontrollers |
$82 | Spurregister des FDC (Floppycontroller) |
$84 | Sektorregister des FDC |
$86 | Datenregister des FDC |
$90 | Sektorzähler des DMA-Chips |
Und die Tabelle für den Schreibzugriff auf Register:
$180 | Kommandoregister des FDC |
$182 | Spurregister des FDC |
$184 | Sektorregister des FDC |
$186 | Datenregister des FDC |
$190 | Sektorzähler des DMA-Chips |
Eine kleine humoristische Einlage des DMA-Chips sollte noch Erwähnung finden: Liest man weniger als 16 Bytes ein, tut sich gar nichts. Woran liegt’s? Der DMA-Chip puffert intern 16 Bytes, bevor er sie in den Speicher schreibt. Wenn wir also 167 Bytes zu lesen wünschen, liefert der DMA-Chip 160 (zehnmal 16) und behält 7. Dieser Puffer wird übrigens gelöscht, wenn Sie den Status per Schreib-/ Leseleitung-Klappern löschen...
Beim Schreiben von Datenmengen (also nicht von Registern) benimmt sich der DMA-Chip noch eigenwilliger. Man ist nicht etwa dann mit dem Schreiben eines Sektors fertig, wenn die aktuelle DMA-Adresse 512 Bytes höher ist als die Startadresse, sondern erst bei 512 + 32 Bytes! Zum Glück spinnt nur der DMA-Zähler und nicht die Datenübertragung selbst, so daß man diesen Fehler leicht umgehen kann: Man wartet einfach ein bißchen länger (eben bis laut Zähler 512 + 32 Bytes übertragen sind).
Wenn Sie das alles auf Anhieb verstanden haben, sind Sie wahrscheinlich an der Entwicklung des DMA-Chips beteiligt gewesen. Schauen Sie sich mal im Listing 1 (LOCKSLEY.S) die Routine wrsec an, mit der man Sektoren schreibt. Dort werden Sie wiederfinden, was ich Ihnen erklärt habe. Noch eine Anmerkung: Bilden Sie sich nicht ein, Sie könnten diese direkte Programmierung des DMA-Chips effektiv und effizient in einer Hochsprache erledigen - wenn es eine Hochburg der Assemblerprogrammierung gibt, dann liegt sie in der Floppyprogrammierung, wo es zeitlich ziemlich knapp wird.
Aber geben Sie jetzt nicht auf, weil ihre einzigen Fremdsprachen Englisch und BASIC sind. Für Sie stelle ich im nächsten Teil des Floppykurses eine Erweiterung für den MINIMON vor, mit dem Sie all diese Dinge von BASIC aus erledigen können. Bevor ich dazu komme, bringen wir noch schnell die Funktionen von Soundchip und MFP beim Diskzugriff hinter uns.
Wenn der Controller fix und ferne mit der Welt ist, also sein Kommando abgearbeitet hat, meldet er das, indem er eine Unterbrechungsanforderung auslöst. Die meldet er dem MFP (Sie wissen schon, der ungemütliche Zeitgenosse von vorhin). Wie fragt man das ab? Ganz einfach:
btst #5,mfp
beq fertig
Dieser Assemblerbrocken testet Bit 5 des I/O-Port im MFP (Adresse $FFF-A01). Wenn das erschöpfte ’Fertig’-Keuchen des Controllers durch die Chiplandschaft hallt, meldet das der MFP, indem er dieses Bit auf 0 setzt. Wie das im Programm aussieht, sehen Sie in der Routine WARTEN_AUF__GODOT von LOCKSLEY.S (siehe Listing 1).
Beim Soundchip sind im Grunde nur zwei Adressen interessant. Die eine heißt schlicht SND (zumindest in meinem Select-Programm), liegt bei $FF-8800 und ist das Auswahlregister des Chips. Das heißt, man schreibt die Nummer des gewünschten Soundchipregisters in diese Adresse hinein und kann dann entweder deren Inhalt aus SND lesen oder das angewählte Register verändern, indem man in $FF8802 (SNDWRT für „Soundchip-Write-Eingang“) einen Wert schreibt. Das Register des Soundchips, das uns interessiert, ist das vierzehnte. Darin liegt der Port A des Chips:
Belegung von Port A des Soundchips
Bit | Bedeutung |
---|---|
0 | Seitenauswahl bei der Floppy |
1 | Auswahlsignal für Laufwerk A |
2 | Auswahlsignal für Laufwerk B |
Die restlichen Bits interessieren uns nicht. Bevor wir also den Floppycontroller selbst ansprechen (bzw. den DMA-Controller), müssen wir erst im Soundchip Seite und Laufwerk einstellen. Auch das finden Sie in einem Programm des Floppykurses, in SELECT.S.
So, jetzt reicht’s aber wirklich mit Adressen und Bits und Kommandos und derlei Verwirrendem mehr. Im Grunde habe ich das alles nur für diejenigen Verwegenen herausgearbeitet, die sich selbst an die Programmierung machen wollen, obwohl ich doch in dieser Ausgabe der ST eine Sammlung von Routinen vorstelle, die jeder abkupfern und zum Vorbild nehmen kann. Natürlich können Sie sich auch noch selbst bemühen; dazu gibt es im nächsten Teil des Floppykurses relativ komplette Informationen über den Floppycontroller (die hätten diesmal alle Heftgrenzen gesprengt).
Seit der ersten Folge des Floppykurses rennt man mir telefonisch und brieflich die Bude ein. Das ist auch ganz in Ordnung so, denn dabei kann ich ja auch noch lernen. Nur häufen sich gewisse Fragen, und ich nehme an, daß sie von allgemeinem Interesse sind:
Frage 1: Kann man mit HYPERFORMAT auch Festplatten formatieren? Die Antwort: Nein, nein, nein. Erstens nimmt HYPERFORMAT den Floppycontroller in die Mangel und nicht den Festplattencontroller. Zudem ist der bei den verschiedenen Festplatten, die für den ST angeboten werden, auch noch unterschiedlich. Und drittens wäre mir auf der Festplatte Datensicherheit wirklich wichtiger als 20 oder 30 Prozent mehr Kapazität, finden Sie nicht?
Frage 2: Gibt es neue Versionen von HYPERFORMAT und wo bekomme ich sie? Ich arbeite immer noch recht häufig an HYPERFORMAT, weil mir immer wieder etwas dazu einfällt. Die aktuelle Version ist jetzt (Ende Juni) 2.2 mit Tendenz zur 2.3. Sie hat eine zusätzliche Verifyoption und ein paar kleinere Verbesserungen mitbekommen. Zu bekommen ist die jeweils neueste Version mit einigen Extras gegen frankierten Rückumschlag und Diskette sowie 10 DM unter meiner Adresse, die Sie in den Fistings finden.
Frage 3: Wie kompatibel sind HYPERFORMAT-Disketten zu anderen Rechnern? Knifflig. Ich könnte mir gut vorstellen, daß Sie HYPERFORMAT auf vielen anderen Laufwerken physikalisch lesen können. Das hängt sehr stark vom Floppycontroller ab, aber da die meisten nach dem MFM-Standard arbeiten, habe ich da eine gewisse Hoffnung. Das logische Format ist natürlich je nach Rechner völlig verschieden. Am ähnlichsten sind Disketten, die unter MS-DOS formatiert wurden. Insofern besteht Hoffnung, falls Atari doch einmal in ferner Zukunft den MS-DOS-Emulator bringt. Vielleicht vertiefe ich mich dann auch noch einmal in die Materie und produziere ein HYPERFORMAT für MS-DOS-Rechner... Wo wir gerade bei Kompatibilitäten sind: Für das Blitter-TOS werde ich eine neue Version von HYPERFORMAT herausbringen. Wie sich HYPERFORMAT mit dem AMIGA verträgt, konnten Sie ja schon in der letzten Folge erfahren.
Frage 4: HYPERFORMAT produziert bei mir Disketten, auf die ich nicht mehr schreiben kann. Tja, das kann viele Ursachen haben. Erstens sollten Sie lieber zweimal nachprüfen, ob Sie auch eine lauffähige Version haben, das heißt, ob Sie nicht vielleicht doch einem Tippfehler erlegen sind oder etwas beim Kopieren schiefgegangen ist. Dann sollten Sie sicher sein, daß Sie HYPERFORMAT auch richtig bedienen (siehe ST 6/87). Und läuft es dann noch nicht, gibt es immer noch mehrere Möglichkeiten: Verwenden Sie schlechte Disketten? Prüfen Sie mal nach, ob mit hochwertigen Disketten nicht was zu machen ist. Dreht Ihr Laufwerk zu schnell? Normal ist eine Umdrehungszahl zwischen 300 und 304 Umdrehungen pro Minute (herauszufinden mit dem Public-domain-Programm SPEED.TOS oder auch mit dem neuen COPYSTAR).
Trifft das alles nicht zu, dann gehören Sie wahrscheinlich zu den vom Schicksal benachteiligten ST-Besitzern, deren Controller HYPERFORMAT (noch) nicht verträgt. Aber selbst dann ist noch nicht aller Tage Abend. Oft hilft es, wenn Sie nicht alle Tracks formatieren, die Sie erreichen können, sondern nur bis Spur 80. Wenn Sie ein Sourcelisting haben, vorzugsweise das der Version 2.1, versuchen Sie die Lückenbytes zu ändern (in der Version 2.1 ganz am Anfang des Listings als gap1 bis gap4 durchparametriert), das Programm zu assemblieren und damit zu formatieren. Wenn Sie das nicht können, fragen Sie einen Freund oder schreiben Sie mir. Wenn Sie selbst Probleme mit HYPERFORMAT hatten und sie irgendwie gelöst haben, lassen Sie es doch die Welt wissen: Schreiben Sie mir oder der „ST-Computer“ über Ihre Erfahrungen - geben Sie dabei bitte genau Ihre Systemkonfiguration an (und auch bitte das Kaufdatum Ihres ST, ich vermute, daß es verschiedene Baureihen gibt). Leider kann ich nicht ausschließen, daß trotz aller Bemühungen einige ST-Besitzer auf HYPERFORMAT verzichten müssen - das Programm geht nun mal bis an physikalische Grenzen heran.
Frage 5: Was kommt als nächstes? Während des Kurses kommen mir immer mehr Ideen zum Thema. Darunter ist aas bereits angekündigte Kopierprogramm für HYPERFORMAT-Disketten, ein wirklich superschnelles Formatierprogramm für „normale“ 9- und 10-Sektordisketten, eine ganz besondere RAM-Disk, der Brückenschlag zwischen .AMIGA und ST und und und...
Bleiben Sie also dran. In der nächsten Folge werden wir uns noch einmal dem Controller und dessen Programmierung widmen. Der MINIMON wird wohl noch ein wenig wachsen (irgendwann werden wir ihn umtaufen müssen), und schließlich gibt es noch ein paar Anwendungen für den Trackmonitor. Und den Rest behalte ich vorerst für mich. Bis bald!
Die Floppyroutinen finden Sie in Listing 1 und 2. Listing 1, LOCKSLEY.S, ist ein Assemblerprogramm, dem der Befehlscode des Controllerbefehls und einige Parameter in einem Feld übergeben werden und das daraufhin selbsttätig die richtige Routine auswählt und sich mit DMA, MFP und FDC herumschlägt. Listing 2, SELECT.S, dient bislang nur dazu, das richtige Laufwerk und die richtige Seite anzuwählen, wird aber noch erweitert werden.
Ein paar Anregungen: Lesen Sie mal Spuren von verschiedenen Disketten ein (normal formatierte und HYPER-FORMATierte). Sehen Sie den Unterschied? Bei HYPERFORMAT liegen die Sektoren viel dichter beieinander. Versuchen Sie doch mal, ein eigenes Trackformat zu erzeugen. Wie wäre es mit 16 Sektoren zu 256 Bytes (damit ist das erzeugte Format kompatibel zu bestimmten HP-Rechnern)? Oder mit 1024-Byte-Sektoren?
Schauen Sie sich auch mal - sofern Sie so etwas besitzen - eine kopiergeschützte Diskette an und versuchen Sie herauszufinden, worin der Schutz besteht (1024-Byte-Sektoren, wild durcheinandergewürfelte Sektorgrößen, Checksummen- und andere Fehler...); das sollte übrigens keine Aufforderung zum Raubkopieren sein, deswegen halte ich mich mit konkreten Tips zu bestimmten Programmen auch lieber zurück. Lesen Sie zur Analyse nicht nur den Track - der Read-Track-Befehl des Controllers ist nicht besonders zuverlässig. Verwenden Sie auch Read-Address. Gehen Sie am besten so vor: Ermitteln Sie mit Read-Address, wieviele Sektoren überhaupt vorhanden sind und wie groß diese sind. Dann lesen Sie die ganze Spur ein und vergleichen die Ergebnisse (dazu kann man in MINIMON den Drucker verwenden). Und zuguterletzt schauen Sie sich die einzelnen Sektoren an. Danach müßten Sie ziemlich genau wissen, wie die Spur aussieht, auch wenn der Read-Track-Befehl nicht so funktioniert, wie er das anständigerweise tun müßte.
Wie sein Namensgeber (Robin of Locksley, im Volksmund Robin Hood) kämpft LOCKSLEY. S für die Freiheit von Unterdrückten, in diesem Fall von repressiv programmierten Leseköpfen unzähliger Laufwerke (Schrittmotoren aller Länder, vereinigt Euch!). LOCKSLEY.S ist einerseits eine Sammlung von Routinen, die Sie immer wieder brauchen werden, wenn Sie sich mit der direkten Floppyprogrammierung, entschlackt von der Last des Betriebssystems, befassen, und andererseits ein Modul, das wie SELECT.S vollständig relokatibel ist. SELECT.S wie LOCKSLEY.S sind mit dem AS68 assembliert worden. Wie man das macht? Dazu ruft man den AS68 so auf: ’AS68 — 1 -u filename.s’. Der Linker produziert dann nach 'linker filename.prg = filename.o’ ein lauffähiges Programm.
Auch bei LOCKSLEY.S finden Sie am Anfang des Programms eine Tabelle von Ein-/Ausgabeparametern, die sich auch bei eventuellen Erweiterungen des Programms nicht verschieben wird. Wenn Sie LOCKSLEY.S benutzen, erwartet das Programm mindestens den Opcode desjenigen Befehls, den der Controller ausführen soll, im Parameter 'opcode'. LOCKSLEY analysiert diesen Opcode und springt dann die richtige Routine für jeden Befehl an. Zu den einzelnen Befehlen können noch weitere Parameter benötigt werden. Eine Übersicht der möglichen Befehle des Controllers und der Ein/ Ausgabeparameter finden Sie in der Tabelle auf der nächsten Seite. Was die einzelnen Befehle tun, finden Sie kurz in der Anleitung zum EXTEN -DED MINIMON erläutert (genaues in der nächsten Folge).
SELECT.S ist ein Programm, das sich (bis jetzt) ausschließlich damit beschäftigt, die richtige Seite und das richtige Laufwerk auszuwählen. Man ruft es auf, bevor man einen Controllerbefehl abschickt, damit man sicher ist, daß der Formatierbefehl, den man im Sinne hat, auch tatsächlich die freie Disk in Laufwerk B in die Mangel nimmt und nicht die Systemdiskette in Laufwerk A. Versäumt man das, erzeugt das graue Haare, aber wem unter ihnen erzähle ich damit etwas Neues...
Im Quelltext erkennen Sie, daß ganz am Anfang des Programms bestimmte Speicherstellen für die Parameterübergabe durch BASIC (oder andere Sprachen) vorgesehen sind. In ’laufwerk’ schreibt man die Bitkombination für aktuelle Seite und gewünschtes Laufwerk (dazu schauen Sie sich am besten die Belegung des Port A im Soundchip an, die im Artikel erwähnt ist). Laufwerk A, Vorderseite wählt man an, indem man „2“ in ’laufwerk’ schreibt und dann in die Routine springt.
Wenn Sie sich den Quelltext anschauen, werden Sie sich wundern, daß während des Programms alle Interrupts ausgeschaltet werden. Das hat einen einfachen Grund. In einer Interruptroutine prüft das Betriebssystem ständig, ob auf einem Laufwerk eine Diskette gewechselt wurde. Dazu muß es die Laufwerke natürlich selektieren und benutzt dazu den Port A. Damit es da keine Kollisionen gibt, habe ich den Interrupt für diese kurze Zeit einfach verboten.
Ein Wort noch zu einer weiteren Besonderheit. Nach einem Befehl deselektiert man sinnvollerweise das Laufwerk, indem man „0“ in ’laufwerk’ schreibt und SELECT aufruft. Allerdings kam es dabei vor, daß man das Laufwerk abwählte und trotzdem (oder gerade deswegen) der Laufwerksmotor sich wie das Auto einer großen Marke benahm: Er lief und lief und lief...
Woran das liegt, weiß ich noch nicht genau, ich hoffe da auf die Unterstützung eines kundigen Lesers. Was ich weiß: Wenn man abwartet, bis das Laufwerk seinen Motor von alleine abschaltet, und dann erst die Floppy de-selektiert, funktioniert es. Dabei muß man aber in Kauf nehmen, daß nach jedem Befehl eine Sekunde für das Auslaufen des Motors verlorengeht. Diese Warteschleife finden Sie im Programm ab dem Label ’motor’.
In der nächsten Ausgabe werde ich SELECT so erweitern, daß man mit diesem Programm auch die Register des Floppycontrollers direkt von BASIC aus lesen und beschreiben kann. Deswegen sind in das Listing einige Definitionen und „open ends“ eingebaut, über die Sie sich vielleicht wundern mögen.
(C.B.)
Die Liste der Routinen:
main Initialisierung
motor Warteschleife, bis der Motor ausgelaufen ist
mach_mal Laufwerk und Seite selektieren
super Supervisor-/Usermodus einschalten
time Warteschleife
Befehl | Parameter | |
---|---|---|
irq | Eingabe: nur Opcode in ’opcode’ Ausgabe: keine! | |
Read Sektor | Eingabe: Opcode in ’opcode’, Pufferadresse in ’bufferl’, Sektor in ’sector’, Anzahl der zu lesenden Bytes in ’count’ Ausgabe: gelesene Bytes in ’count’, Status des Floppycontrollers in ’fst’, DMA-Status in ’dst’, Ende der DMA-Übertragung in ’end’, Timeoutflag in ’timeout’ | |
Read Track | Eingabe: Opcode in ’opcode’, Pufferadresse in ’bufferl’, Zahl der zu lesenden Bytes in ’count’ Ausgabe: wie Read Sector | |
Write Track | Eingabe: Opcode in ’opcode’, Adresse der Trackdaten in ’bufferl’, Zahl der zu schreibenden Bytes in ’count’ Ausgabe: geschriebene Bytes in ’count’, sonst wie bei Read Sector | |
Write Sector | Eingabe: Opcode in ’opcode’, Adresse der Sektordaten in ’bufferl’, Zahl der zu schreibenden Bytes in ’count’, Sektornummer in ’sector’ Ausgabe: wie bei Write Track | |
Step, Step-in Step-out, Restore | Eingabe: Opcode in ’opcode’ Ausgabe: Controllerstatus in ’fst’ | |
Seek | Eingabe: Opcode in ’opcode’, Zielspur in ’track’ Ausgabe: wie bei Step etc. | |
Read Address | Eingabe: Opcode in ’opcode’, Zahl der zu lesenden Adreßfelder in ’count’, Pufferadresse für die Adreßfelder in ’bufferl’, Pufferadresse für die Statusmeldungen in ’buffer2’ Ausgabe: wie bei Read Sector | |
Die Liste der Routinen: main Initialisiert, verzweigt und macht Schluß wrfdc Ein Byte in $ff8604 schreiben (Zugriff auf FDC- oder DMA-Register )• as_time_goes_by Warteschleife warten_auf_godot Wartet auf Ende des FDC-Kommandos poll Testet auf Unterbrechungsanforderung game_over Zeitüberlauf passiert fix_und_fertig DMA-Übertragung am Ende Status Status des DMA-Chips lesen dma Startadresse für DMÄ setzen super Supervisor/Usermodus umschalten analyze Analysiert den Opcode und verzweigt irq Routine für den FORCE-IRQ-Befehl rdsec Routine für den Read-Sector-Befehl exe Einsprung für den Zyklus „Kommando schreiben“-„warten“-„Status lesen“ rdtrk Routine für den Read-Track-Befehl wrtrk Routine für den Write-Track-Befehl wrsec Routine für den Write-Sector-Befehl step_dance Routine für Step, Step-In, Step-Out und Restore such_hasso Routine für den Seek-Befehl rdadr Routine für den Read-Address-Befehl nochnid Schleifenmarke für 'noch ’ne ED lesen’ |
LOCKSLEY.S (C) 1907 Claus Brod
*********************************************
* LOCKSLEY - Der Befreier des Lesekopfes *
* Direkter Zugriff auf den Floppycontrol1er *
* --- Test auf eigene Gefahr ---- *
* Mit Schnittstelle nach außen für den *
* Anschluß an MINIMON *
* Written 1987 by *
* Claus Brod *
* Am Felsenkeller 2 *
* 8772 Marktheidenfeld *
* *
* Version 1.0 *
* 6.6.87 (starting all over again) *
* 7.6.87 (main work) *
* 8.-11.6.87 (debugging) *
* 12.6.87 (adding some sub's) *
* 13.6.87 (fiddling with #@!?* Read Adr.) *
* 14.-16.6.87 (cleaning up) *
* Assembliert mit AS68 *
*********************************************
*************************
* Ein paar Definitionen für den langen Weg
*************************
mfp - Sfffa01 * Adresse des MFP68901 fürs Polling
daccess= $ff8604 * DMA-Contro1ler, FDC-Zugriff oder Sektorzähler
dmodus = $ff8606 * DMA-Contro1ler, Modus einstellen
dlow = $ff860d * DMA-Contro1ler, Übertragungsstart Lowbyte
dmid = $ff860b * DMA-Contro1ler, übertragungsstart Midbyte
dhigh = $ff8609 * DMA-Contro1ler, übertragungsstart Highbyte
time = $40000 * Timeout-Konstante
*************************
* Der Sprung ins Ungewisse
*************************
bra main * Sprung zum Programmanfang
*************************
* Ein/Ausgabefeld zur Parameterübergabe
*************************
opcode: .dc.w 0 * Hier kommt der Opcode
* des Controllerbefehls hinein
track: .dc.w 0 * Tracknummer
*
sector: .dc.w 0 * Sektornummer
*
count: .dc.w 0 * in:zu übertragende Bytes/ID-fields etc.
* out:übertragene Bytes
buffer1: .dc.l 0 * Adresse des ersten Puffers für
* Track-, Sektor-, ID-Daten
buffer2: .dc.l 0 * Adresse des Reservepuffers
*
fst: .dc.w 0 * Status des Controllers
*
dst: .dc.w 0 * Status der DMA
*
dstart: .dc.l 0 * Start der DMA—Übertragung
*
dend: .dc.l 0 * Ende der DMA—Übertragung
*
timeout: .dc.w 0 * Timeoutflag
*
stk: .dc.l 0 * Puffer für Stackpointer
*
dflag: .dc.w 0 * DMA-Flag
*
***************************
* Hauptverteilerroutine
***************************
main:
movem.l d0-d7/a0-a6,-(sp) * Register verschwinden lassen
clr.l d0 * Userstack wird Supervisorstack
bsr super * Supervisormodus an
lea stk(pc),a2 * Adresse des Puffers für den Stackpointer
move.l d0,(a2) * Stackpointer retten
move.w opcode(pc),d6 * FDC-Befehl holen
lea dflag(pc),a2 * Adresse des DMA-Flags
move.w #0,(a2) * DMA-Flag initialisieren
lea timeout(pc),a2 * Adresse des Timeoutflags
move.w #0,(a2) * Timeout—Flag initialisieren
st $43e * Floppy-Vertical—B1ank—Interrupt sperren
bsr analyze * Befehl analysieren und ausführen
sf $43e * Floppy-VBL wieder erlauben
lea stk(pc),a2 * Adresse des Stackpointerpuffers
move.l (a2),d0 * Stackpointer holen
bsr super * Supervisormodus aus (schaaade...)
movem.l (sp)+,d0—d7/a0—a6 * Register wieder holen
rts * und raus (ade!)
******************************
* wrfdc: Byte in d7 an den Controller
* schicken
******************************
wrfdc:
move.w #30,d1 * Zähler auf 30
bsr as_time_goes_by * Warteschleife
move.w d7,daccess * d7 ins Access-Register des DMA-Chips
move.w #30,d1 * Zähler auf 30
******************************
* as_time_goes_by: Warteschleife
* mit d1 Durchläufen
******************************
as_time_goes_by:
dbf d1,as_time_goes_by * Looping (huiii...)
rts * back to the future
******************************
* warten_auf_godot : Wartet auf das IRQ-Signal
* des FDC
******************************
warten_auf_godot:
move.l #time,d7 * Konstante für Timeout
poll:
btst #5,mfp * IRQ am MFP7
beq fix_und_fertig * jawoll, Kommando ist ausgeführt
subq.l #1,d7 * Timeoutzähler magert ab
beq game_over * Sorry, zu lange gefackelt
lea dflag(pc),a2 * Adresse des DMA-Flags
tst.w (a2) * DMA aktiv?
beq poll * nein, zum Polling
lea buffer2(pc),a2 * Adresse der Adresse (!) des Reservepuffer!
move.b dhigh,1(a2) * Highbyte der DMA-Adresse
move.b dmid,2(a2) * Midbyte der DMA-Adresse
move.b dlow,3(a2) * Lowbyte der DMA-Adresse
move.l buffer2(pc),d0 * DMA-Adresse nach dO
cmp.l dend(pc),d0 * Mit Endadresse vergleichen
blt poll * Noch nicht erreicht, weiter testen
move.b #$d0,d7 * Force IRQ
bsr irq * FDC unterbrechen
lea dflag(pc),a2 * Adresse des DMA-Flags
move.w #0,(a2) * DMA beendet
bra fix_und_fertig * Feierabend
***************************
* Timeout-Konstante ist abgelaufen
***************************
game_over:
bsr fix_und_fertig * Status lesen und in fst ablegen
move.b #$d0,d7 * Force IRQ
bsr irq * FDC unterbrechen
lea timeout(pc),a2 * Adresse des Timeout-F1ags
move.w #1,(a2) * Timeout passiert
lea dflag(pc).a2 * Adresse des DMA-Flags
move.w #0,(a2) * DMA völlig am Ende
rts * und fertig
***************************
* DMA-Ubertragung oder FDC fertig
***************************
fix_und_fertig:
move.w daccess,d0 * Status lesen
lea fst(pc),a2 * Adresse des FDC-Status-Flags
move.w d0,(a2) * Status ablegen
rts * und raus
***************************
* status: Status und Bytezahl lesen
***************************
status:
move.w dmodus,d0 * Status lesen
lea dst(pc),a2 * Adresse des DMA-Status-Flags
move.w d0,(a2) * Status schreiben
clr.w d1 * d1 löschen
move.b dhigh,d1 * DMA-High
lsl.l #8,d1 * um ein Byte schieben
move.b dmid,d1 * DMA-Mid
lsl.l #8,d1 * um ein Byte schieben
move.b dlow,d1 * DMA-Low
lea dend(pc),a2 * Adresse der Endadresse
move.l d1,(a2) * Endadresse
sub.l dstart(pc),d1 * minus Start
lea count(pc),a2 * Adresse des Bytezählers
****************************
* dma: dma setzen (Spiegelbild zu status)
****************************
dma:
lea dstart(pc),a2 * Adresse der Startadresse
move.l d7,(a2) * Startadresse ablegen
move.b d7,dlow * Lowbyte
lsr.l #8,d7 * um ein Byte schieben
move.b d7,dmid * Midbyte
lsr.l #8,d7 * um ein Byte schieben
move.b d7,dhigh * Highbyte
move.l dstart(pc),d7 * Startadresse holen
clr.l d0 * d0 löschen
move.w count(pc),d0 * Bytecounter nach dO
add.l d0,d7 * Addieren
lea dend(pc),a2 * Adresse der Endadresse
move.l d7,(a2) * Endadresse ablegen
rts
*****************************
* super: Schaltet vom Usermode
* in der Supervisormode und umgekehrt
* Übergabe des Stackpointers in d0
*****************************
super:
move.l d0,-(sp) * Stackpointer auf Stack
move.w #$20.-(sp) * SUPER
trap #1 * im GEMDOS
addq.l #6,sp * Stack korrigieren
rts
*****************************
* analyze: Analysiert grob, welche Art von
* Befehl vorliegt
* und verteilt entsprechend
* in: d6.w Opcode
*****************************
analyze:
move.w d6,d7 * Opcode retten
move.w d6,d5 * gleich nochmal
btst #7,d6 * Bit 7 testen
bne typii * kein Typ-I-Befehl
and.b #$f0,d6 * obere 4 Bits ausmaskieren
cmp.b #16,d6 * SEEK-Befehl?
beq such_hasso * jawoll
bne step_dance * kein Seek. aber Typ-I
typii:
btst #6,d6 * Bit 6 testen
bne typiii * kein Typ-II-Befehl
btst #5,d6 * Bit 5 testen
beq rdsector * gelöscht, also Read-Sector-Befehl
bne wrsector * Write-Sector-Befehl
typiii :
and.b #$f0,d6 * obere 4 Bits ausmaskieren
cmp.b #$c0,d6 * Read Adress?
beq rdadr * jawoll
cmp.b #$e0,d6 * Read Track?
beq rdtrk * jawoll
* * alles andere ist Force Interrupt
**************************
* irq: Unterbricht den Controller
**************************
irq:
bsr wrfdc * d7 an den Controller
move.w #250,d1 * 250 Schleifendurchläufe
bra as_time_goes_by * kurz warten
*****************************
* rdsector: Liest einen Sektor oder
* gleich einen ganzen Haufen davon
*****************************
rdsector:
move.l buffer1(pc),d7 * Pufferadresse
bsr dma * als DMA-Adresse
lea dflag(pc),a2 * Adresse des DMA-Flags
move.w #1,(a2) * DMA-Flag setzen
move.w #$90,dmodus * mit der Sehreib/Leseleitung
move.w #$190,dmodus * kippeln, löscht den DMA-Status
move.w #$90,dmodus * Sektorzahler des DMA-Chips
move.w #14,d7 * maximal 14 Sektoren (utopisch)
bsr wrfdc * d7 an FDC
move.w #$84,dmodus * Sektorregister
move.w sector(pc),d7 * Sektornummer
bsr wrfdc * d7 an FDC
move.w #$80,dmodus * Kommandoregister
exe:
move.w d5,d7 * Kommando von d5 nach d7
bsr wrfdc * d7 an FDC
bsr warten_auf_godot * Ende des Kommandos abwarten
bra status * Status lesen
*****************************
* rdtrk: Track lesen
*****************************
rdtrk:
move.l buffer1(pc),d7 * Adresse des Spurpuffers
bsr dma * DMA initialisieren
lea dflag(pc),a2 * Adresse des DMA-Flags
move.w #1,(a2) * DMA in Arbeit!
move.w #$90,dmodus * Schreib/Leseleitung
move.w #$190,dmodus * umschalten, siehe oben
move.w #$90,dmodus * Sektorzähler des DMA-Chips
move.w #14.d7 * 14 Sektoren
bsr wrfdc * d7 an FDC
move.w #$80,dmodus * Kommandoregister
bra exe * Kommando schreiben und beenden
*****************************
* wrtrk: Track schreiben
*****************************
wrtrk:
move.l buffer1(pc),d7 * Spurpufferadresse nach d7
bsr dma * DMA initialisieren
lea dflag(pc),a2 * Adresse des DMA-Flags
move.w #1,(a2) * DMA in Arbeit!
move.w #$190,dmodus * Schreib/Leseleitung umschalten
move.w #$90,dmodus * (bewirkt was? Raten Sie mal!)
bsr wrfdc * d7 an FDC
move.w #$100,dmodus * Kommandoregister wählen
bra exe * Kommando schicken und beenden
*****************************
* wrsector: Sektor schreiben
***************************.1*
wrsector:
move.l buffer1(pc),d7 * Adresse des Puffers
bsr dma * DMA initialisieren
lea dflag(pc).a2 * Adresse des DMA-Flags
move.w #1,(a2) * DMA in Arbeit
move.w #$190.dmodus * mit der Schreib/Leseleitung
move.w #$90,dmodus * klappern
move.w #$190,dmodus * Sektorzähler des DMA-Chips
move.w #14,d7 * Maximal 14 Sektoren (utopisch)
bsr wrfdc * d7 an FDC
move.w #$184,dmodus * Sektorregister des FDC
move.w sector(pc),d7 * Startsektor holen
bsr wrfdc * d7 an FDC
move.w #$180,dmodus * Kommandoregister des FDC
bra exe * Kommando schicken und beenden
*****************************
* step_dance: einheitliche Routine für Step,
* step-in, step-out, restore
*****************************
step_dance:
move.w #$80,dmodus * Command-Register auswählen
bsr wrfdc * Byte in d7 an FDC
bra warten_auf_godot * Auf FDC warten...
*****************************
* such_hasso: Seek-Befehl
*****************************
such_hasso:
move.w #$86,dmodus * Datenregister auswählen
move.w track(pc).d7 * Tracknummer holen
bsr wrfdc * Byte in d7 an FDC
move.w #$80,dmodus * Commandregister
move.w d5,d7 * Kommando
bsr wrfdc * Byte an FDC
bra warten_auf_godot * Auf FDC warten
*****************************
* rdadr: Adreßfeld lesen
*****************************
rdadr:
move.l buffer1(pc),d7 * Adresse des Puffers holen
move.l buffer2(pc),a3 * Adresse des Reservepuffers
bsr dma * DMA initialisieren
move.w #$90,dmodus * Schreib/Leseleitung
move.w #$190.dmodus * umschalten
move.w #$90,dmodus * und auf Sektorzähler
move.w #1.d7 * Maximal 1 Sektor
bsr wrfdc * d7 an FDC (DMA)
move.w #$80,dmodus * Kommandoregister
move.w count(pc),d2 * Wieviele ID-Felder?
nochnid:
move.w d5,d7 * Kommando holen
bsr wrfdc * und schreiben
bsr warten_auf_godot * auf FDC warten
move.w fst(pc),d1 * FDC-Status holen
move.w d1,(a3)+ * Status in den zweiten Puffer
dbra d2,nochnid * Noch eine ID
bra status * und zum Status
SELECT.S
****************************
* SELECT.S
* Selektiert Seite und Laufwerk
* Mit Schnittstelle für BASIC
* (C) 1987 and for all eternity by
* Claus Brod
* Am Felsenkeller 2
* 8772 Marktheidenfeld
* Version 1.0
* Last update 16.6.87
* Assembliert mit AS68
*****************************
*****************************
* Definitionen
*****************************
snd = $ff8800 * Adresse des Soundchips
sndwrt = $ff8802 * Ein/Ausgabe des Soundchips
dmodus = $ff8606 * DMA-Modus-Register
daccess = $ff8604 * DMA-Access
*****************************
* Sprung ins Hauptprogramm
*****************************
bra main
*****************************
* Eingabefeld für BASIC
*****************************
laufwerk: .dc.w 0 * Eingabeparameter Laufwerk und Seite
parm: .dc.l 0 * Reserveein/ausgabefeld für Erweiterungen
temp: .dc.l 0 * Reservefeld
stk: .dc.l 0 * Puffer für Stackpointer
*****************************
* Und jetzt das Hauptprogramm
*****************************
main:
movem.l d0—d7/a0—a6,—(sp) * Register retten
clr.l d0 * Userstack wird Supervisorstack
bsr super * Supervisormode an
lea stk(pc),a2 * Stackpointer
move.l d0,(a2) * retten
st $43e * Floppy—VBL sperren
move.w laufwerk(pc),d7 * Laufwerksnummer holen
bne mach_mal * Wenn nicht gerade 0, dann mach los
move.w #$80,dmodus * Statusregister
motor:
move.w daccess,d1 * auslesen
btst #7,d1 * Motor noch an?
bne motor * jawoll
mach_ma1:
cmpi.b #8,d7 * Laufwerkscode größer als 7?
bge extend * ja, extended mode
eor.b #7,d7 * Bits invertieren
and.b #7,d7 * und ausmaskieren
move.w sr,-(sp) * Status retten
or.w #$700,sr * Interrupts aus
move.b #14,snd * Port A wählen
move.b snd,d0 * Port A lesen
and.b #$f8,d0 * ausmaskieren
or.b d0,d7 * neue Seite/neues Laufwerk setzen
move.b d7,sndwrt * in Port A
sf $43e * Floppy-VBL erlauben
move.w (sp)+,sr * Status wieder holen
raushier:
lea stk(pc),a2 * Stackpointer
move.l (a2),d0 * zurückholen
bsr super * wieder in den Usermode
movem.l (sp)+,d0-d7/a0-a6 * Register zurückholen
rts * und raus
*************************
* super: Schaltet vom Usermode
* in den Supervisormode und umgekehrt
* in: d0 Stackpointer
*************************
super:
move.l d0,-(sp) * Stackpointer auf Stack
move.w #$20,-(sp) * SUPER
trap #1 * im GEMDOS
addq.l #6,sp * Stack reparieren
rts * und raus
**************************
* time: Warteschleife
**************************
time:
dbra d1,time * Zähler magert ab
rts * und raus
**************************
* msg: Gibt String ab a2 aus
**************************
msg:
move.l a2,-(sp) * Adresse des Strings auf den Stack
move.w #9,-(sp) * PRINT LINE
trap #1 * im GEMDOS
addq.l #6,sp * Stack korrigieren
rts * und raus
**************************
* extend: Wird in der nächsten ST ergänzt
**************************
extend:
lea nochnit(pc),a2 * extend noch nicht
bsr msg * implementiert
bra raushier * und raus
nochnit:
.dc.b 'Routine noch nicht implementiert.',13,10,0