Disk Copy
Wer hat sich als stolzer Besitzer eines Commodore 64 und einer 1541-Floppy noch nicht beim Kopieren von Programmen geärgert? Solange es sich um reine Basic-Programme handelt, geht es noch: Originaldiskette einlegen, Programm laden, Diskette wechseln, Programm »saven«, Diskette wechseln, Programm laden...
Schlimmer wird es, wenn sich um Maschinenprogramme oder gar um sequentielle Files handelt. Das Anlegen einer Sicherheitskopie wird zur zeitraubenden, umständlichen Prozedur und unterbleibt deshalb, bis man eines Tages feststellt, daß das Original aus irgendwelchen Gründen nicht mehr läuft. Mir ist das mit einer Datei passiert, die aus über 400 Einträgen bestand und die ich eines Tages einfach nicht mehr einladen konnte. Seitdem gibt es bei mir von allen wichtigen Disketten Sicherheitskopien, bei deren Erstellung mir das folgende Programm gute Dienste leistet.
Das Besondere an diesem Programm ist, daß alle Arten von Files mit Ausnahme relativer Dateien kopiert werden können und das recht komfortabel und schnell. Der Trick besteht darin, daß die Files, die man kopieren möchte, der Reihe nach in den Speicher gelesen werden bis dieser voll ist. Da das Programm auch die »versteckten« RAM-Bereiche mitbenutzt, die man normalerweise gar nicht ansprechen kann, weil Basic-Interpreter und Betriebssystem an derselben Speicheradresse liegen, kommt man in einem Durchgang auf stattliche 226 Blöcke (zirka 56 KByte). Sollte das noch nicht ausreichen, startet das Programm einen zweiten oder auch dritten Durchgang. Dann ist spätestens die gesamte Diskette kopiert (bei ganz ungünstigen Verhältnissen wird noch ein vierter Durchgang gebraucht). Damit ergeben sich Zeiten von unter 20 Minuten für eine Komplettkopie.
Wie funktioniert's?
Sehen wir uns zuerst das Basicprogramm an (Bild 1): Ich habe es absichtlich in mehrere Teile zerlegt, um es übersichtlicher zu machen. Die REM-Zeilen können beim Eintippen natürlich ebenso wegfallen wie die Zeilen, in denen nur ein Doppelpunkt steht.
Zeile 100 bis 140
setzt zuerst die Speicherobergrenze für Basic herunter (Speicherstelle 56) und berechnet, wieviel Speicher zum Kopieren zur Verfügung steht (RB). Um spätere Erweiterungen einbauen zu können (zum Beispiel Backup für relative Dateien), arbeite ich hier nicht mit festen Zahlen, sondern mit Variablen, die das Programm selbst berechnet. Das gilt ebenso für die Startadressen der Maschinenroutinen. Diese befinden sich direkt hinter dem Basic-Teil und brauchen deshalb nicht erst über DATA-Zeilen bei jedem Programmlauf »eingepoked« werden. Das spart nicht nur Zeit, sondern vor allem auch Speicherplatz (zirka 700 Bytes). Dafür müssen Sie beim Abschreiben zwei Teile zusammenfügen. Im Initialisierungsteil werden auch alle Variablen und Arrays eingerichtet (siehe Variablentabelle).
Zeile 160 bis 240
enthält das Menü. Sie können hier jederzeit weitere Funktionen einfügen.
Wird der Programmteil »Formatieren« gewählt, springt das Programm in die Zeilen 700 bis 750. Interessant ist hier die Möglichkeit, bei der Frage nach der Disk – ID keine Eingabe zu machen, sondern »RETURN« zu drücken. Dies ist bei Disketten sinnvoll, die bereits formatiert sind, aber gelöscht werden sollen. Das DOS der 1541 löscht dann nur die BAM und die erste Seite des Directory, was viel schneller geht als vollständiges Neuformatieren. Das Programm schließt diesen Teil mit Ausgabe der Fehlermeldung ab und springt wieder ins Menü.
Der Programmteil »Directory« ermöglicht ein schnelles Einlesen des Directorys, natürlich ohne Programmverlust. Man kann sich so einen kurzen Überblick über Original- und Zieldiskette verschaffen, ohne die Kopierroutine aufrufen zu müssen. Hier wird ein Maschinenteilprogramm benutzt, um Speicherplatz und Zeit zu sparen.
Zeile 270 bis 650
Kommen wir nun zum Kern der Sache, dem eigentlichen Kopierteil, Dieser befindet sich in den Zeilen 270 bis 650. Im ersten Teil (Zeile 270 bis 350) werden alle Files, die auf der Diskette sind, in ein Stringarray (NF$(I)) eingelesen, die zugehörigen Blocklängen in ein Variablenarray (BL%(I)). Das dauert etwas länger, weil der Speicherplatz begrenzt ist und der Basic-Interpreter mitunter eine Garbage-Collection (Müll-Sammlung) durchführt, um Platz zu schaffen. Die Anzahl der Files wird in der Variablen AN festgehalten. Ist die Diskette leer, weil man zum Beispiel noch die gerade formatierte Zieldiskette im Laufwerk hatte, springt das Programm ins Menü zurück.
In Zeile 370 bis 440 erfolgt die Auswahl, welche Files kopiert werden sollen. Das Programm schreibt dazu die einzelnen Namen mit der zugehörigen Blocklänge auf den Bildschirm und Sie können mit »J« oder »N« aussuchen. Die Entscheidung merkt sich das Programm wieder in einem Variablenarray (CF%(I)): Ist CF%=-1, wird kopiert, sonst nicht. Gleichzeitig wird in der Variablen BL aufaddiert, wieviele Blöcke zu kopieren sind, damit das Programm herausbekommt, ob ein Durchgang ausreicht oder nicht. Wenn Sie alle Files mit »N« kennzeichnen, springt das Programm wieder ins Menü.
Nachdem nun endlich alle Entscheidungen und Vorbereitungen abgeschlossen sind, geht es ans Kopieren (Zeile 460 bis 650). Der Reihe nach werden alle gekennzeichneten Files in den Speicher eingelesen. Dazu wird das Laufwerk mit »OPEN«-Befehlen angesprochen, weil so alle Arten von Files geladen und auch abgespeichert werden können. (Mit »LOAD« könnten nur Programmfiles geladen werden.) Erfreulicherweise enthält die Variable NF$ nicht nur den Namen des jeweiligen Files, sondern auch den Typ, also PRG, SEQ oder USR. Deshalb können alle Filetypen mit ein und derselben Routine verarbeitet werden.
Der Unterschied zwischen Lesen und Schreiben liegt lediglich darin, daß dem »OPEN«-Befehl im ersten Falle ein R (für Read), im zweiten ein W (für Write) angehängt wird. Die Datenübertragung selbst erledigt das Maschinenprogramm, dem wir uns gleich zuwenden werden. Um Variablen an diese Routinen übergeben zu können, benutzen wir die Zeropage-Speicherstellen 252 bis 255 ($FC bis $FF).
Werfen wir nun noch einen Blick auf die Maschinensprach-Teile (Bild 3 bis 5). Dabei soll vor allem erläutert werden, wie man den vollen RAM-Speicher des C 64 nutzen kann.
Dazu ist ein kurzer Blick auf die Speicheraufteilung des C 64 erforderlich, insbesondere den Teil ab $A000 bis $FFFF (dez. 40960 - 65535). Hier liegt normalerweise der Basic-Interpreter, der 8 KByte Adreßraum benötigt ($A000 - $C000). Darüber liegen in 4 KByte die Ein-Ausgabe-Einheiten ($D000 - $E000). Ganz oben im Speicher befindet sich das Betriebssystem, das genau wie der Basic-Interpreter 8 KByte belegt ($E000 - $FFFF). Zusätzlich ist aber der gesamte Bereich auch noch mit RAM bestückt. Woher weiß der Prozessor nun, was er benutzen soll? Lediglich 3 Bits in Speicherstelle 1 sind für die Auswahl zuständig: Bit 0 schaltet den Basic-Interpreter ein und aus, Bit 1 gleichzeitig Basic-Interpreter und Betriebssystem, Bit 2 ist für uns schon uninteressant. Werden beide, Bit 0 und Bit 1, auf 0 gesetzt, ist zusätzlich auch noch der Ein-Ausgabebereich abgeschaltet. Eigentlich doch ganz einfach. Der Teufel steckt wie fast immer im Detail: Wenn der Basic-Interpreter abgeschaltet ist, wie soll dann ein Basic-Programm laufen? Mehr noch, ohne sein Betriebssystem ist der Prozessor hilfloser als ein Blinder im Nebel.
Die Lösung liegt einfach darin, zu verhindern, daß der Prozessor überhaupt auf die Idee kommt, in sein Betriebssystem oder ins Basic hineinzuspringen. Letzteres ist nicht schwierig, denn wir befinden uns ja in einem Maschinenprogramm, wenn wir das Basic abschalten. Und bei der Bearbeitung eines solchen Programms kann nur dann etwas passieren, wenn der Prozessor das Programm verläßt. Das tut er allerdings jede 1/50 Sekunde, zum Beispiel um die interne Uhr weiterzustellen und nachzusehen, ob eine Taste gedrückt wurde etc. Das ist die sogenannte Interrupt-Routine. Wenn wir ihm die sperren, kann eigentlich gar nichts mehr schiefgehen. Und es funktioniert tatsächlich:
Im Leseteil des Maschinenprogramms wird zuerst der mit »OPEN« eröffnete Kanal als Eingabekanal gesetzt (FFC6). Dann wird ein Byte über diesen Kanal geholt. Jetzt sperrt der SEI (Set Interrupt) die Interruptroutine und wir können in aller Ruhe schalten und walten. Wir schieben das Byte ins X-Register, legen die beiden unteren Bits der Speicherstelle 1 auf 0, holen unser Byte aus dem X-Register und legen es im RAM ab. Jetzt setzen wir die Bits wieder auf 1, löschen die Interrupt-Sperre, und alles ist wieder in Ordnung. Nachdem unser Byte im RAM sicher untergebracht ist, fragen wir nun ab, ob vielleicht ein Fehler aufgetreten ist (FFB7) oder das Ende unseres Files erreicht ist. Wenn ja, springen wir ins Basic zurück und brechen im Fehlerfalle das Programm mit einer entsprechenden Meldung ab. Wenn nein, wenden wir uns dem nächsten Byte zu, das übertragen werden soll und behandeln es mit der gleichen Sorgfalt. Ganz zum Schluß müssen wir noch wieder die Kanäle zurücksetzen (Tastatur als Eingabe, Bildschirm als Ausgabe (FFCC)). Das alles klingt zwar umständlich und langwierig, geht aber in Wirklichkeit unglaublich schnell.
Das Schreiben auf Diskette bringt nichts grundsätzlich Neues, der ganze Vorgang läuft hier einfach andersherum ab. Unser Kanal 1 ist jetzt Ausgabekanal ($FFC9), und anstatt ein Byte von der Diskette zu holen, geben wir es aus ($FFD2).
Auch die Directory-Ausgabe folgt diesem Muster:
Kanal 3 als Eingabe setzen ($FFC6), Zeichen für Zeichen holen ($FFCF) und – jetzt auf dem Bildschirm – ausgeben ($FFD2).
Wichtige Bedienungshinweise
So, nun steht dem Eintippen des Programms nichts mehr im Wege. Noch ein paar wichtige Hinweise: Die beiden Teile des Programms müssen beim ersten Mal zusammengefügt werden. Dazu gehen wir folgendermaßen vor:
- Tippen Sie das Programm »Disk Copy« ab und speichern Sie es auf Diskette.
- Starten Sie das Programm mit »RUN« und drücken Sie die »RUN/STOP«-Taste, wenn das Menü erscheint.
- Geben sie ein: »PRINT PR« und schreiben Sie sich die angezeigte Zahl auf.
- Tippen Sie das Programm »Basic-Data-Lader« ein und starten Sie es. Auf die Frage nach der Anfangsadresse geben Sie Ihre aufgeschriebene Zahl ein.
- Folgen Sie genau den Anweisungen des Programs und geben Sie die beiden »POKE«-Befehle ein.
- Speichern Sie das vollständige Programm auf Diskette ab.
Jetzt haben Sie das Programm gebrauchsfähig auf Diskette. Sie können auch beliebige Änderungen am Programm durchführen, der Maschinensprach-Teil wird sich immer automatisch mitverschieben.
Anpassung auf VC 20:
Das Programm läuft auch auf dem VC 20, für den ich es ursprünglich geschrieben hatte. Nur Zeile 110 muß geändert werden:
110 POKE56,PEEK(46)+14:CLR:RB=PEEK(644)-PEEK (56):...
Wenn Sie mit einer 1541-Floppy arbeiten, sollten Sie noch einfügen:
118 OPEN1,8,15,'"UI-":CLOSE1.
Und nun viel Spaß beim Kopieren.
(Dietrich Weineck)100 rem *** initialisierung *** 110 poke56,peek(46)+14:clr:rb=255-peek(56):pa=1:an=0:bl=0:nf$="" 120 pe=peek(45)+256*peek(46):mr=pe-135:mw=pe-79:md=pe-24 130 dimnf$(140),cf%(140),bl%(140),p%(10),al%(90),ah%(90) 140 p%(0)=0:al%(0)=0:ah%(0)=peek(56)-1 150 : 160 rem *** menue *** 170 print"{clr}{down}"tab(9)"***** disk copy *****":printtab(10)"von d.weineck 2/84" 180 print"{down}{down}{down}{down}{rght}{rght}1. directory 190 print"{down}{rght}{rght}2. kopieren 200 print"{down}{rght}{rght}3. formatieren 210 print"{down}{rght}{rght}4. ende 220 printspc(212)"{rvon}bitte waehlen sie 230 getdc$:dc=val(dc$):ifdc<1ordc>4then230 240 ondcgoto910,270,700,670 250 : 260 rem *** kopieren *** 270 print"{clr}{down}{rvon}originaldiskette einlegen" 280 gosub990 290 rem *** files einlesen *** 300 open1,8,0,"$0" 310 gosub760:ifnf$<>""then340 320 ifst=0then310 330 goto350 340 bl%(an)=asc(bl$+chr$(0)):nf$(an)=nf$:ifst=0thenan=an+1:nf$="":goto310 350 close1:an=an-1:ifan=0thenprint"{down}{down}{rvon}leere diskette{rvof}":gosub990:run 360 rem *** kopierauswahl *** 370 print"{clr}{down}antworten sie mit j/n{down}" 380 fori=1toan:printbl%(i);tab(5)nf$(i)" ? ";:poke198,0 390 wait198,1:geta$:ifa$="j"thencf%(i)=-1:bl=bl+bl%(i):printtab(30)"{rvon} ja {rvof}":goto420 400 cf%(i)=0:ifa$<>"n"then390 410 printtab(30)"nein" 420 ifbl>rbthenp%(pa)=i-1:pa=pa+1:bl=bl%(i) 430 nexti:p%(pa)=an 440 ifbl=0then640 450 rem *** kopie *** 460 print"{clr}{rvon}kopie in arbeit{down}" 470 fori=1topa 480 forrw=0to1:nr=0:ifrw=1thenprint"{rvon}{down}zieldisk einlegen":gosub990 490 forj=p%(i-1)+1top%(i) 500 ifnotcf%(j)thennextj:goto540 510 nf$=nf$(j):printbl%(j);tab(5)nf$:gosub570:ifst=0orst=64then530 520 gosub880:run 530 nextj 540 nextrw:ifi=pathen640 550 print"{rvon}{down}originaldisk einlegen":gosub990 560 nexti:run 570 ifrw=1then610 580 open1,8,5,nf$+",r":poke252,0:poke253,ah%(nr)+1 590 sysmr:nr=nr+1:al%(nr)=peek(254):ah%(nr)=peek(255) 600 close1:return 610 open1,8,5,nf$+",w":poke252,0:poke253,ah%(nr)+1 620 poke254,al%(nr+1):poke255,ah%(nr+1):sysmw 630 nr=nr+1:close1:return 640 print"{down}{down}{down}{rvon}kopie fertig ! 650 gosub990:run 660 rem *** ende *** 670 poke56,160:end 680 : 690 rem *** formatieren *** 700 input"{clr}{down}{down}{down}diskname";fo$:id$="":input"{down}disk-id";id$:ifid$<>""thenid$=","+id$ 710 fo$=fo$+id$ 720 print"{down}{rvon}bitte zieldiskette einlegen" 730 gosub990 740 open1,8,15,"n:"+fo$:close1 750 gosub880:goto170 760 rem directory einlesen 770 get#1,a$,b$ 780 get#1,bl$,b$ 790 get#1,a$ 800 get#1,b$:ifst<>0thenreturn 810 ifb$<>chr$(34)then800 820 get#1,b$:ifb$<>chr$(34)thennf$=nf$+b$:goto820 830 get#1,b$:ifb$=chr$(32)then830 840 nf$=nf$+","+b$:fori=0to1:get#1,b$:nf$=nf$+b$:next 850 get#1,b$:ifb$<>""then850 860 return 870 rem *** fehler-ausgabe *** 880 open15,8,15:input#15,a,b$,c,d:printa;b$;c;d:close15:gosub990:return 890 : 900 rem *** directory *** 910 print"{clr}" 920 open3,8,0,"$0":get#3,a$,a$ 930 get#3,a$,a$,bl$,bh$ 940 ifa$=""thenclose3:goto980 950 bl$=bl$+chr$(0):bh$=bh$+chr$(0) 960 print256*asc(bh$)+asc(bl$); 970 sysmd:goto930 980 gosub 990:goto170 990 printspc(69)"{CBM-@}{CBM-@}{CBM-@}{CBM-@}{CBM-@}{CBM-@}{CBM-@}":printspc(29)"{rvon}*taste*{rvof}" 1000 poke198,0:wait198,1:geta$:return
100 print"{clr}{down}{down}{rght}{rght}basic - data - lader fuer 'disk copy 110 input"{down}{down}anfangsadresse";ad:ed=ad+135 120 fori=adtoed-1 130 readz:pokei,z:next 140 hb=int(ed/256):lb=edand255 150 print"{down}{down}{down}laden sie nun das programm 'disk copy'. 160 print"{down}geben sie nach dem laden ein:":print"{down}{down}{rght}{rght}poke 45,"lb":poke 46,"hb" 170 print"{down}{down}das programm befindet sich jetzt voll- 180 print"{down}staendig im speicher und kann ge'saved' {down}werden. 190 data162,1,32,198,255,160,0,32,207,255,120,170,165,1,41,252,133,1,138,145 200 data252,165,1,9,3,133,1,88,32,183,255,201,64,240,11,201,0,208,7,200,208 210 data221,230,253,208,217,32,204,255,132,254,165,253,133,255,96,162,1,32 220 data201,255,160,0,120,165,1,41,252,133,1,177,252,170,165,1,9,3,133,1,88 230 data138,32,210,255,32,183,255,201,0,208,17,200,208,2,230,253,165,255,197 240 data253,208,217,196,254,144,213,240,211,76,204,255,162,3,32,198,255,32 250 data207,255,32,210,255,208,248,169,13,32,210,255,76,204,255,0,0,0
LESEN VON DISKETTE ,114D A2 01 LDX #01 ,114F 20 C6 FF JSR FFC6 ,1152 A0 00 LDY #00 ,1154 20 CF FF JSR FFCF ,1157 78 SEI ,1158 AA TAX ,1159 A5 01 LDA 01 ,115B 29 FC AND #FC ,115D 85 01 STA 01 ,115F 8A TXA ,1160 91 FC STA (FC),Y ,1162 A5 01 LDA 01 ,1164 09 03 ORA #03 ,1166 85 01 STA 01 ,1168 58 CLI ,1169 20 B7 FF JSR FFB7 ,116C C9 40 CMP #40 ,116E F0 0B BEQ 117B ,1170 C9 00 CMP #00 ,1172 D0 07 BNE 117B ,1174 C8 INY ,1175 D0 DD BNE 1154 ,1177 E6 FD INC FD ,1179 D0 D9 BNE 1154 ,117B 20 CC FF JSR FFCC ,117E 84 FE STY FE ,1180 A5 FD LDA FD ,1182 85 FF STA FF ,1184 60 RTS
SCHREIBEN AUF DISKETTE ,1185 A2 01 LDX #01 ,1187 20 C9 FF JSR FFC9 ,118A A0 00 LDY #00 ,118C 78 SEI ,118D A5 01 LDA 01 ,118F 29 FC AND #FC ,1191 85 01 STA 01 ,1199 B1 FC LDA (FC),Y ,1195 AA TAX ,1196 A5 01 LDA 01 ,1198 09 03 ORA #03 ,119A 85 01 STA 01 ,119C 58 CLI ,119D 8A TXA ,119E 20 D2 FF JSR FFD2 ,11A1 20 B7 FF JSR FFB7 ,11A4 C9 00 CMP #00 ,11A6 D0 11 BNE 11B9 ,11A8 C8 INY ,11A8 D0 02 BNE 11AD ,11AB E6 FD INC FD ,11AD A5 FF LDA FF ,11AF C5 FD CMP FD ,11B1 D0 D9 BNE 118C ,11B9 C4 FE CPY FE ,11B5 90 D5 BCC 118C ,11B7 F0 D3 BEQ 118C ,11B9 4C CC FF JMP FFCC
DIRECTORY HOLEN ,11BC A2 03 LDX #03 ,11BE 20 C6 FF JSR FFC6 ,11C1 20 CF FF JSR FFCF ,11C4 20 D2 FF JSR FFD2 ,11C7 D0 F8 BNE 11C1 ,11C9 A9 0D LDA #0D ,11CB 20 D2 FF JSR FFD2 ,11CE 4C CC FF JMP FFCC
RB | Anzahl dar zur Verfügung stehenden RAM-Blöcke |
PA | Anzahl der Durchgänge beim Kopieren |
AN | Anzahl der Files auf der Diskette |
BL | Anzahl der zu kopierenden Blöcke |
NF$ | Name und Typ des Files |
PE | Programmende |
MR | Adresse der Maschinenroutine zum Lesen |
MW | Adresse der Maschinenroutine zum Schreiben |
MD | Adresse der Maschinenroutine für Directory-Ausgabe |
NF$(I) | Feld für Filenamen |
CF%(I) | Feld für Kopierflags |
BL%(I) | Blocklänge der einzelnen Flles |
P%(I) | Anzahl der Files in einzelnen Durchgaengen |
AL%(I) | Endadresse der einzelnen Flles im Speicher (Low-Byte) |
AH%(I) | Endadresse der einzelnen Files im Speicher (Hlgh-Byte) |
RW | Flag für Lesen oder Schreiben |
I,J | Schleifenvariable |