
by Val J. Golding
Call-A.P.P.L.E. Magazine March 1983 PP81
CP/MUFFIN was a rush job. We had a story submitted to us on Wordstar on a CP/M formatted diskette. Fortunately for us, we had just published a story in the December Call -A.P.PL.E. by Art Messler, which pretty well explained the structure of a CP/M directory. As it turned out, the clues in that story were sufficient to start us on the right path. We were working in the dark, not having a Z-80 system ourselves, thus writing a program to convert CP/M files to standard DOS presented an intriguing challenge.
At the same time, we had just finished editing a story which was published in “All About DOS” by Bill Parker, which outlined some simple techniques to call RWTS from BASIC. The basic premise of Bill’s routine was to repoint two Applesoft strings at pre-existing data in the RWTS data buffer and write them to disk. With Bill’s short assembly program as a core, we set about getting the data from the CP/M disk into a form we could interpret. In brief, this meant reading the CP/M directory into an array, checking for and bypassing deleted and dummy files, then presenting the user with the option of copying the entire disk or single files. Later, after having done some test conversions, we determined it would be helpful to have a choice of removing control characters from the file or removing [CTRL-J] only. (CP/M uses a [CTRL-J] after each carriage return.)
CP/M data is stored on a diskette in manner different than standard DOS, with respect to the sequence in which sectors are read, thus based on Messler’s article, we had to establish an array to be used as a look-up table, then base our read on the four block patterns in the table. This was the essence of the whole program, since DOS RWTS can read any sector on a CP/M or Pascal diskette.
We put this together over a weekend and ended up with a working program, albeit slow. (45 minutes to convert a single diskette is not exactly the world’s land speed record.) At this point, having gotten our needed results, we called in David Sparks, who waved his magic wand, added an additional assembly language section which laid out a buffer to handle an entire CP/M file and then write it to disk.
The BASIC program is well modularized and with the REMs should be easy to follow. It will now convert a full CP/M disk in about seven or eight minutes. This speed could be improved on by deleting line 1030 which puts a MONO into effect. CP/M does not use a track! sector list to keep track of its files; this is done via directory entries and dynamic block allocation, therefore it is possible although unlikely to encounter a DISK FULL ERROR while converting a very full CP/M diskette. The BASIC program is complete in itself, since the machine code (which is fully re-locatable) is loaded using the S. H. Lam routine. The assembly listing, which is well commented, is for the use of those who wish to further expand on the program. It should be fairly simple to write a program to convert DOS files to CP/M.
Name : CP/MUFFIN
Length : $1279 (4729)
Load at : $0801 (2049)
10 REM ^J^M [CP/M]UFFIN BY VAL J GOLDING^J^MWITH DAVE SPARKS AND
BILL PARKER^M CALL -A.P.P.L.E. : MARCH 1983^J^M CONVERTS
CP/M TEXT FILES TO DOS^J
100 GOTO 9000: REM ^J
500 REM ^J^MGET TRACK NBR & PATTERN FOR BLOCK
510 TRACK% = INT (BLOCK%(I,K + 1) / MOD) + 3
520 PTN = INT ( ABS(BLOCK%(I,K + 1)) - INT ( ABS(BLOCK%(I,K + 1) / MOD)) * ABS(MOD) + .05) * SGN(BLOCK%(I,K + 1) / MOD)
530 RETURN: REM ^J
1000 REM ^J^MWRITE ONE FILE^J
1010 VTAB 7:
PRINT "WRITING ";NUMOFBLOCKS * 4;" SECTORS..."
1020 POKE 34,11:
HOME
1030 PRINT CHR$(4);"MONO"
1040 PRINT CHR$(4);"OPENT.";FILNAME$(I);",D2"
1050 PRINT CHR$(4);"WRITET.";FILNAME$(I)
1060 & OUTPUT > BFPTR(1),BFPTR(2) - 1
1070 PRINT CHR$(4);"NOMONO"
1080 PRINT CHR$(4);"CLOSE"
1090 POKE 34,5:
HOME:
RETURN
1100 END: REM ^J
2000 REM ^J^MCALL RWTS TO READ CPM SECTOR^J
2010 POKE TRACK,TRACK%:
POKE SECTR,SECTR%:
POKE DRIVE,DRIVE%
2020 VTAB 4:
HTAB 1:
PRINT "READING BLOCK: ";:
INVERSE:
PRINT BLOCK%(I,K + 1);" ";:
NORMAL:
PRINT " OF: ";:
INVERSE:
PRINT FILNAME$(I):
NORMAL
2030 PRINT "TRACK: ";:
:
INVERSE:
PRINT TR%;" ";:
NORMAL:
PRINT " SECTOR: ";:
INVERSE:
PRINT SECTR%;" ":
NORMAL
2040 POKE RWCODE,1:
POKE PREGISTER,0
2050 CALL USERRWTS
2060 POKE RWCODE,2:
POKE PREGISTER,0
2070 IF CTRLCHR THEN
& CTRLKILL
2080 IF JCTRL THEN
& LINEFEEDKILL
2090 & R > BUFFR$(1),BUFFR$(2)
2100 RETURN: REM ^J
3000 REM ^J^MREAD AND WRITE A BLOCK^J
3010 FOR I = 1 TO LSTFILE
3020 RNUM = 0:
IF SNGL THEN
I = SNGL: REM ^J
3200 FOR J = 0 TO 15:
IF BLOCK%(I,J + 1) = 0 THEN
NUMOFBLOCKS = J:
J = 15:
NEXT J:
GOTO 3220
3210 NEXT J:
NUMOFBLOCKS = 16
3220 IF NOT LEN(FILNAME$(I)) THEN
3710: REM ^J
3300 BFPTR(2) = BFPTR(1)
3310 HOME
3320 VTAB 10:
PRINT "DATA LOCATED FROM ";BFPTR(1);" TO ";: REM ^J
3400 FOR K = 0 TO NUMOFBLOCKS - 1
3410 GOSUB 500: REM ^J
3500 FOR J = 1 TO 4
3510 SECTR% = STBL(PTN,J)
3520 GOSUB 2000
3530 & MOVE > BFPTR(2):
BFPTR(2) = BFPTR(2) + 256:
VTAB 10:
HTAB 28:
PRINT BFPTR(2) - 1
3540 NEXT J: REM ^J
3600 NEXT K: REM ^J
3700 GOSUB 1000
3710 IF SNGL THEN
POKE 34,3:
HOME:
GOTO 6100
3720 NEXT I
3730 END: REM ^J
4000 REM ^J^MREAD CPM DIRECTORY^J
4010 TRACK% = 3:
SECTR% = 0:
ENTNBR = 0
4020 FOR I = 1 TO 6:
GOSUB 2000: REM ^J
4100 NDX = 1:
FOR J = 1 TO 4:
ENTNBR = ENTNBR + 1
4110 DIR$(ENTNBR) = MID$(BUFFR$(1),NDX,32)
4120 NDX = NDX + 32:
:
NEXT J: REM ^J
4200 NDX = 1:
FOR J = 1 TO 4:
ENTNBR = ENTNBR + 1
4210 DIR$(ENTNBR) = MID$(BUFFR$(2),NDX,32)
4220 NDX = NDX + 32:
NEXT J: REM ^J
4300 SECTR% = SECTR% + 6
4310 IF SECTR% > 15 THEN
SECTR% = SECTR% - 15
4330 NEXT I: REM ^J
5000 REM ^J^MINTREPRET CPM DIRECTORY^J
5010 K = 0:
FOR I = 1 TO 48
5020 IF MID$(DIR$(I),2,1) = E5$ THEN
I = 48:
NEXT I:
GOTO 5420: REM
5030 IF LEFT$(DIR$(I),1) = E5$ THEN
5410: REM ^MCHECK IF DELETED FILE^J
5040 K = K + 1:
FILNAME$(K) = MID$(DIR$(I),2,11)
5050 TEST$ = ( MID$(DIR$(I),13,1)):
IF TEST$ < > CHR$(0) THEN
FILNAME$(K) = FILNAME$(K) + "." + CHR$( ASC(TEST$) + 48): REM ^MCHECK FOR FILENAME SUFFIX^J
5060 FOR J = 1 TO LEN(FILNAME$(K)):
IF MID$(FILNAME$(K),J,1) < > " " THEN
X$ = X$ + MID$(FILNAME$(K),J,1): REM REMOVE SPACES^J
5070 NEXT :
FILNAME$(K) = X$:
X$ = ""
5080 NREC%(K) = ASC( MID$(DIR$(I),16,1)): REM CHECK FOR DUMMY FILE^J
5090 IF NOT NR%(K) THEN
FILNAME$(K) = "":
GOTO 5410: REM ^J
5200 FOR J = 1 TO 16
5210 BLOCK%(K,J) = ASC( MID$(DIR$(I),J + 16,1))
5220 IF BLOCK%(K,J) = 0 THEN
J = 16:
NEXT J:
GOTO 5410: REM
5400 NEXT J
5410 NEXT I
5420 LSTFILE = K: REM FALL THRU TO 6000^J
6000 REM ^J^MCOPY OPTIONS MENU^J
6010 POKE 35,24:
HOME:
VTAB 9:
PRINT "COPY ENTIRE DISK (Y/N) ";:
GET CHOICE$:
PRINT CHOICE$:
6020 IF CHOICE$ = "Y" THEN
GOSUB 6400:
GOTO 3000: REM ^J
6100 HOME:
FOR DIR = 1 TO LSTFILE
6110 IF NOT LEN(FILNAME$(DIR)) THEN
6130
6120 PRINT " [";DIR;"] ";FILNAME$(DIR)
6130 NEXT DIR: REM ^J
6200 PRINT :
INPUT "WHICH FILE NBR ? [PRESS RETURN TO QUIT]";CHOICE$:
SNGL = VAL(CHOICE$):
IF NOT SNGL THEN
IF LEN(CHOICE$) THEN
6200
6210 IF NOT SNGL THEN
POKE 34,0:
END
6220 GOSUB 6400:
GOTO 3020: REM ^J
6400 PRINT :
PRINT "REMOVE ALL CONTROL CHARACTERS FROM TEXT
(Y/N) ? ";:
GET CHOICE$:
PRINT CHOICE$
6410 IF CHOICE$ = "Y" THEN
CTRLCHR = 1:
RETURN: REM ^J
6500 PRINT :
PRINT "REMOVE CONTROL J'S FROM TEXT (Y/N) ? ";:
GET CHOICE$:
PRINT CHOICE$
6510 IF CHOICE$ = "Y" THEN
JCTRLCHR = 1
6520 RETURN: REM ^J
9000 REM ^J^MINITIALIZATION^J
9010 HOME:
VTAB 12:
HTAB 11:
FLASH:
PRINT " LOADING HEX DATA ":
NORMAL
9020 HX$ = "2E0:A9 4C 8D F5 03 A9 F0 8D F6 03 A9 02 8D F7 03 60 A2 CF 86 0D 86 0E C9 52 D0 48 20 AA D9 C8 20 98 D9 20 E3 DF A9 80 20 52 E4 A0 00 91 83 C8 A5 71":
GOSUB 9900
9030 HX$ = "310:91 83 C8 A5 72 91 83 A0 B4 A2 BB A9 80 20 E2 E5 20 BE DE 20 E3 DF A9 80 20 52 E4 A0 00 91 83 C8 A5 71 91 83 C8 A5 72 91 83 A0 B5 A2 3B A9 80 4C":
GOSUB 9900
9040 HX$ = "340:E2 E5 C9 43 D0 1D A2 00 BD BB B4 9D BB B4 C9 0D F0 04 C9 20 90 06 E8 D0 EF 4C 95 D9 A9 20 9D BB B4 D0 F3 C9 4C D0 19 A2 00 BD BB B4 9D BB B4 C9":
GOSUB 9900
9050 HX$ = "370:0A F0 06 E8 D0 F3 4C 95 D9 A9 20 9D BB B4 D0 F3 C9 4D D0 18 20 AA D9 C8 20 98 D9 20 67 DD 20 52 E7 A0 00 B9 BB B4 91 50 C8 D0 F8 60 C9 4F D0 2E":
GOSUB 9900
9060 HX$ = "3A0:20 AA D9 C8 20 98 D9 20 67 DD 20 52 E7 20 BE DE 20 67 DD 20 F2 EB B1 50 20 5C DB E6 50 D0 02 E6 51 A5 A1 C5 50 A5 A0 E5 51 B0 EB 4C FB DA 00":
GOSUB 9900
9070 CALL 736: REM ^J
9080 REM ^J^MRELOCATABLE MACHINE CODE^MMODIFIED BY DAVID SPARKS^J
9200 BFPTR(1) = 22016:
HIMEM:22016
9210 RWCODE = 45121:
SLOT = 46583:
DRIVE = 46584:
9220 TRACK = 45975:
SECTR = 45976:
MOD = 4:
E5$ = CHR$(229)
9230 USERRW = 45111:
PREGISTER = 72
9240 DIM DIR$(49),FILNAME$(49),NR%(49),BLOCK%(49,17): REM ^J
9400 TEXT:
HOME:
VTAB 2:
PRINT "[CP/MUFFIN] CP/M->DOS FILE CONVERTER":
PRINT :
POKE 34,3:
INPUT "SOURCE SLOT, DRIVE: ";SLOT%,DRIVE%
9410 POKE SLOT,SLOT% * 16:
POKE DRIVE,DRIVE%
9420 POKE 35,16:
VTAB 20:
HTAB 11:
PRINT :
INVERSE:
PRINT " READING CP/M DIRECTORY ":
:
:
NORMAL: REM ^J
9600 REM ^J^MSET UP 4 SECTOR READ SEQUENCE^MPATTERNS^J
9610 STBL(0,1) = 0:
STBL(0,2) = 6:
STBL(0,3) = 12:
STBL(0,4) = 3
9620 STBL(1,1) = 9:
STBL(1,2) = 15:
STBL(1,3) = 14:
STBL(1,4) = 5
9630 STBL(2,1) = 11:
STBL(2,2) = 2:
STBL(2,3) = 8:
STBL(2,4) = 7
9640 STBL(3,1) = 13:
STBL(3,2) = 4:
STBL(3,3) = 10:
STBL(3,4) = 1
9650 GOTO 4000: REM ^J
9900 REM ^J^MPOKE RWTS STRING MACHINE CODE^MINTO MEMORY WITH S H LAM
ROUTINE^J
9910 HX$ = HX$ + " N D9C6G":
FOR I = 1 TO LEN(HX$):
POKE 511 + I, ASC( MID$(HX$,I,1)) + 128:
NEXT :
POKE 72,0:
CALL - 144:
RETURN














