--------- CP/M Documentation as of 1-July-80 --------- Although CP/M is not the ultimate in operating systems it is an extremely useful software development tool which is implementable on a wide variety of 8080 and Z80 based computers. As a result it has become, by default if not by design, the standard of users of these microprocessors. CP/M's implementability lays mainly in the portability afforded to it through the use of the BIOS (Basic I/O System). This concept is a simple but very effective solution to the problem of hardware variability inherent in microprocessor-based computer systems. The S-100 bus not withstanding, there seems to be as many hardware configurations as there are designers (as an example, my own computer has a 56-pin backplane and uses memory-mapped I/O). It is a surprising and welcome gesture for a software manufacturer to supply the exact procedures necessary to bring up its software. By the way of contrast, I know a very compulsive programmer who spent the better part of a month bringing up ISIS on a non-MDS hardware, whereas it took less than a week for this same programmer to bring up CP/M on the same hardware. The other major factor in CP/M's success must be the configurability. How many microcomputer users start with 64K (which ISIS requires to do anything useful)? With the ability to turn a 16K toy into a "real" computer, CP/M has ensured a very large following of devotees. As mentioned in the users manual, CP/M is divided into four logically distinct but interacting parts: BDOS (Basic Disk Operating System) BIOS (Basic Input/Output System) CCP (Console Command Processor) TPA (Transient Program Area) As there isn't a whole lot to say about the TPA other than that is where the transient programs are executed, and the users manual is very explicit on the use of the CCP, I will deal mainly with the BDOS and BIOS, which I consider to be the heart of the system. THE BASIC DISK OPERATING SYSTEM (BDOS) The BDOS is the file manager of CP/M. Though it an application program can: o Open a file o Close a file o Search for the first and subsequent entries in the directory for a file o Erase a file from the directory o Read and Write logical records o Create entries in the directory o Rename entries in the directory as well as various support functions. These functions mainly add a level of abstraction to the disk hardware as implemented in the BIOS allowing the application program(mer) to deal with the stored data without having to know where it is physically located. The BDOS also acts as a conduit between the application program and the character I/O entry points in the BIOS as well as providing some macro-functions for string I/O to and from the console. Thus through the use of the BDOS, CP/M is able to transform a computer and its peripherals into a generalized system with no particular hardware characteristics machine language itself. (and through the use of one of the many "hi-level" languages which have been implemented to run under CP/M, it is possibe to solve problems with out even knowing or caring what kind of CPU is running the show). To my way of thinking, this is a rather significant development in micro-computing. The BDOS functions are listed and explained quite throughly in the programmer's guide so I will not bore the reader (and make myself type anyone than I have to) by reiterating them here. However, I would like to address myself to a couple of points which are associated with the BDOS. First, while monkeying around one day, I discovered that there are some strange locations at the beginning of the BDOS which can be (and most definitely are) used by transient programs even though they are supposedly secure inside the operating system. Here is an outline of these locations: ORG BDOS SERNUM: DS 6 ENTRY: JMP COMMAND DW BADIO DW SELERROR DW ROERROR MAP: LXI H,MAPTABLE ; POINT HL AT LOG TO PHYS MAPPING MVI B,0 ; SET BC TO LOGICAL SECTOR (IN C) DAD B ; INDEX INTO MAPPING TABLE MOV C,M ; CONVERT LOGICAL TO PHYSICAL SECTOR JMP SETSECTOR ; DO BIOS SET SECTOR FUNCTION NOP MAPTABLE: DB 1,7,13,19 DB 25,5,11,7 ; THIS TABLE MAPS LOGICAL ONTO DB 23,3,9,15 ; PHYSICAL DISK SECTORS DB 21,2,8,14 DB 20,26,6,12 DB 18,24,4,10 DB 16,22,0,0 DB 0,0,0,0 ; THE NULLS ARE ADDRESS ALINGMENT CONFIG: DB SECPT ; SECTORS PER TRACK(26 ON IBM DISKS) DB LASTDIR ; NUMBERS OF LAST DIRECTORY ENTRY (63) DB RPB ; 2**RPB=RECORDS PER BLOCK (3) DB LASTSEC ; LAST SECTOR IN BLOCK (7) DB LASTBLOCK ; LAST BLOCK ON THE DISK (242) DB DIRALLOC ; DIRECTORY ALLOCATION MASK (C0 HEX) DB DIRTACK ; TRACK THAT DIRECTORY BEGINS ON (2) The first 6 bytes of the BDOS contain the serial numbers which supposedly prevent copyright infringments. The next 3 contain the actual entry point to the BDOS command decoder. The next 9 bytes are the addresses of routines inside the BDOS which are executed when one of the three fatal errors occur. Following the error addresses is the routine which the BDOS uses to stagger its data on a track followed by the mapping table. After the mapping table are some constants which define the particular implementation of CP/M on specific size disks. The error address can be modified by an application program to recover somewhat more gracefully from a fatal error (I ran across this in a screen-oriented text editor which made possible to save the memory image of the program after an I/O error). The configuration table data can be used (and is used by STAT in version 1.4) to find out what's going on with disk space. As far as I can tell, it is not serendipitious that this information exists and is located where it is. It seems that the people at Digital Research left room for certain easy modifications and enhancements to CP/M. The second point I would like to address is the technique CP/M uses to do housekeeping on its files. All of the file management functions in the BDOS use the address of an FCB (file control block) as their parameter. Like the concept of the I/O byte the FCB is a rather elegant solution to the housekeeping problem in a file management system. It accomplishes two things for the operations system. First it "decentralizes" the process of maintaining and second, it gives application programs access to the same information about a file that the BDOS uses. These two factors together allow an application to get as close as it requires to the file management process. There are some problems with the FCB, as implemented. First there is the question of whether or not an application OUGHT to have access to housekeeping information as it is possible to louse things up pretty badly if things are not done correctly. This I think is a matter of taste. Since CP/M, as implemented, is a single-user system there isn't the problem of messing other people's files up and it is a foolish programmer indeed who messes with something that is not understood. The other problem is a bit more serious (rumor has it that Digital Research is dealing with it). Since the largest number that an allocated block can have (see below) is 255(D) and with a block size of an even 2k, the maximum number of bytes of bytes of storage CP/M can address on one disk is approximately 510 kilobytes. This presents serious impediments (due to program standardization) to implementing the operating system on the larger disk systems becoming available. Be that as it may, within the scope of single (or perhaps double) density floppy disks, the FCB is in my opinion, a stroke of genius. Because an application, through the use of the FCB, has such flexibility it behooves the assembly language programmer to understand as best he or she can how to use it. The programmer's guide describes the format and I'll elaborate a bit on it. The FCB consists of seven fields of information each having a mnemonic associated with it. They are: FIELD FCB POSITIONS ET 0 FN 1-8 FT 9-11 EX 12 NOT USED 13-14 RC 15 DM 16-31 NR 32 The FN and FT fields are only logically distinct. The BDOS uses all eleven bytes as a fundamental unit of information when opening, closing, creating, erasing, searching and renaming files. (The EX field is included during OPEN, CLOSE, CREATE and SEARCH operations). It is the CCP and transient programs which make a distinction between file name and file type. Since a file can be of any length up to the capacity of the disk and since a single FCB describes only 16k bytes of a file, there must be a way to link multiple sections of a file each described by its own FCB. This is done via the EX field. The first extent of a file has an EX value of 0, the second a value of 1 and so on. The ET field is an interesting mixture of usefulness and ambiguity. When the FCB is stored in the directory the ET filed may contain a 0, indicating that the entry is used, or, an E5(H) indicating that the entry has been deleted (or never used). However, when the FCB is used as a parmeter for one of the file management functions, the ET field serves an entirely different purpose. If ET=0 then then the BDOS will assume that the command pertains to the currently selected disk. If the ET field is not zero then the BDOS assumes that it contains the disk number+1 to which the function pertains. In this case the BDOS will temporarily select disk number ET-1 and then clear ET to zero before proceeding with the requested function. When the file operation is complete, the BDOS will restore ET to its original value. Thus, an application program need never concern itself with remembering or selecting specific disks as these values are retained throughout processing from the time that the CCP sets them up in the default FCBs. The RC field is essentially an "end of extent" pointer. It is "pushed" along by the NR field when writing is used to limit the NR field during reading to prevent the reading unwritten data. The DM field is an array of 16 bytes, each representing a logical block of data within the extent. The value of each of these bytes represents the physical area of disk space allocated to the logical block unless the value is 0, in which case the block has not been allocated any disk space. Together, the RC and DM fields form a "current" description of the locations on the disk used by the data contained with the extent. The NR field is used to specify which record, relative to the beginning of the extent, is to be read or written. The BDOS will automatically increment this number during read and write operations, making sequential file access virtually automatic. The BASIC I/O SYSTEM (BIOS) =========================== This section of the system concerns itself with the hardware dependent aspects of I/O. There are two types: 1) Disk I/O, which is block oriented 2) Character I/O, which is byte oriented It is convenient to consider these two aspects seperately as they do not interact directly. Looking at the BIOS jump table (as described in the CP/M documentation), the first two entries are paths to system initialization routines. The next six are entry points to the character I/O routines and the rest are entry points to disk I/O and disk support routines. The section called "BIOS Entry Points" in the System Alteration Guide describes the function of each of these 15 entry points better than I could. However, what the guide does not do (as it is only a manual) is point out the importance of the I/O byte. I consider this to be deserving of special attention. Experience (mostly my own) has shown that until one makes concrete use of the I/O byte concept, it is difficult to appreciate the elegance of this technique. It does have its limits, but it is very simple and effective solution to CP/M's character I/O device standardization problems. I first ran across this concept on the MDS MOD-80 development system which did not have disk drives and used paper tape for its off-line storage. The Intel Monitor used a standard jump table which allowed programs to do character I/O without necessarily having to worry about the actual hardware devices. Perhaps you've seen it, but in case you haven't here it is. ORG MOINTOR JMP MAINLINE JMP CI ; CONSOLE INPUT JMP RI ; READER INPUT JMP CO ; CONSOLE OUTPUT JMP PO ; PUNCH OUTPUT JMP LO ; LIST OUTPUT JMP CSTS ; RETURN CONSOLE STATUS JMP IOCHK ; RETURN I/O BYTE JMP IOSET ; CHANGE I/O BYTE (NEW VALUE IN C) JMP MEMCK ; RETURN TOP OF USER MEMORY IN A AND B As you can see, this does basically the same that the BIOS jump table does. IOCHK, IOSET and MEMCHK are not needed in the BIOS since the information returned by these routines are located in the zero page of CP/M's memory. As the alteration guide is not explicit on the subject of implementing an I/O byte, I'll outline in assembly language code the techniques I've found useful for a generalized implementation. But first notice that each of the six character I/O routines must decode out the path to the specific I/O device "currently assigned". The way this is done (in English) is as follows. The I/O byte contains four fields, each as consisting of low bits. Each field is associated with one of the four "logical" I/O devices (List, Punch, Reader and Console) and may take on the value of (in binary) 00, 01,10, or 11. Thus, up to four different physical I/O devices may be associated with each of the four "logical" devices. For example, the logical device "List." By manipulating the value of these 2 bits (presumably) without affecting the rest of the byte) one may "assign" a specific hardware driver (and the device itself) to the list device. In PASCALese this is the decoder: VAR IOBYTE(4): PACKED ARRAY OF (0..3) DO CASE IOBYTE(4) 0: TTYOUT; 1: LPTOUT; 2: CRTOUT; 3: USERLIST; END The alteration guide and documentation on PIP and STAT describe what these physical device might be. The command: STAT VAL: produces essentially a menu of the nominal physical devices assignable in the CP/M system. Here is some software: IOBYTE EQU 3 ; LOCATION OF IOBYTE CMASK EQU 03H ; CONSOLE MASK RMASK EQU 0CH ; READER MASK PMASK EQU 30H ; PUNCH MASK LMASK EQU 0C0H ; LIST MASK RD2 EQU 08H ; READER DEVICE 2 MASK PD2 EQU 40H ; PUNCH DEVICE 2 MASK CONTIN: LDA IOBYTE ; GET IOBYTE ANI CMASK ; LOOK AT CONSOLE FIELD JZ TTYIN ; CONSOLE 0: JPE UCIN1 ; CONSOLE 3: RAR ; CONSOLE1: JC KBDIN ; CONSOLE 1: JMP BACHIN ; CONSOLE 2 ONOUT: LDA IOBYTE ; GET IO BYTE ANI CMASK ; LOOK AT CONSOLE FIELD JZ TTYOUT ; CONSOLE 0: JPE UCOUT1 ; CONSOLE 3: RAR ; LOOK FOR CONSOLE 1 JC CRTOUT ; CONSOLE 1: JMP BACHOUT ; CONSOLE 2 CONSTAT: LDA IOBYTE ANI CMASK JZ TTYSTAT ; CONSOLE STATUS 0 JPE UCSTAT1 ; CONSOLE STATUS 3 RAR JC KBDSTAT ; CONSOLE STATUS 3 RET ; DON'T KNOW WHAT STATUS 2 IS READER: LDA IOBYTE ANI RMASK ; LOOK AT READER BITS ONLY JZ TTYIN ; READER 0 JPE UR2 ; READER 3: ANI RD2 ; SEE IF EITHER READER 1 OR 2 JZ PT ; READER 1 IF ZERO JMP UR1 ; READER 2 IF NOT PUNCH: LDA IOBYTE ANI PMASK JZ TTYOUT ; PUNCH 0 JPE UP2 ; PUNCH 3 ANI PD2 ; SEE IF EITHER READER 1 OR 2 JZ PTP ; PUNCH 1 IF ZERO JMP UP1 ; PUNCH 2 IF NOT LIST: LDA IOBYTE ANI LMASK JZ TTYOUT ; LIST 0: JPE UL1 ; LIST 3: JM LPTOUT ; LIST 2: JMP CRT ; LIST 1: The logical device "BATCH" is a hangover from the Intel I/O byte definition. It was originally intended to allow console input to be some continuous input device, such as papertape, tape, and the console output be a hard copy device such as a line printer. This was to make it possible to create "jobs" offline and then run them unattended. CP/M's SUBMIT command is the counterpart of this function in floppy disk land, though it seems to have lost a little in translation. What all this buys us as CP/M users is this: If you can figure out how to make a piece of hardware accept or produce data, one byte at a time, then it can be assigned to one or more of these logical devices. You are then able to treat it in application software as you would any of the standard character I/O devices such as a CRT or keyboard. As for the disk I/O, things are not quite as elegant but are standarized so as not to ruin CP/M's implementability. It looks as though Digital Research asked themselves, "What does all floppy disk hardware need from the processor to perform disk I/O?" Again the alteration guide is a better place than here to answer that question. However, I'd like to discuss something that was discovered during the implementation of CP/M on my hardware. By the way of background, the disk hardware on my computer is totally bizarre. It uses a 256 byte, hard sectors. 16 of them on a track, and writes the bytes bitwise, backwards with respect to literally everyone else. I did everything short of making a pact with Methistopheles (the devil's agent. Ed.) in order to make my equipment look like IBM compatible hardware. In experimenting with various techniques, both my own and from suggestions of others, I finally implemented a full-track (26 IBM sectors) buffering concept which works very well for me. I can provide listings of this if anyone cares to see it. The important thing that I discovered was that most implementations of the BIOS do not have provisions for read after write, or similar way of verifing that the data was written correctly. Obviously, with the low error rate floppy disks supposedly have (something 1 in 6 million bytes) the overhead of doing a verify after write may not be justified. Since I was writing software which, if a bit were dropped, could unpleasantly affect all future development (including modifications to CP/M itself) I felt that I could not afford even that small chance of error or at least not knowing about the error if it should occur. But here's the catch: In order to verify one sector after writing you have absolutely no choice but to wait for the disk to bring it around again. This means that in order to write one whole track this way, it will take 26 revolutions of the disk! At a sixth of a second per revolution (on 8 inch floppy drives) that amounts to 4 1/3 seconds which is obviously unacceptable and totally unjustifiable. Using a full track buffer, I was able to solve that problem very neatly. It took one revolution to write the data and one revolution to verify that it was written correctly. This is approximately 300 milliseconds or a savings of over 400 percent. The following is a detailed explanation of disk space allocation and what the BDOS does with the FCB during file I/O. Disk Space Allocation ===================== First number all of the sectors on the disk from 1 to 2002 starting at sector 1 track zero. I'll refer to this number as the "INTEGER SECTOR NUMBER" or ISECTOR. CP/M reserves the first two tracks (ISECTOR 1-52) for holding image of the system as well as the bootstrap program (see the Alteration Guide section of the CP/M documentation). Thus there are 1950 sectors available on a single density dsk starting at ISECTOR 53. For reasons which should become apparent, space is not allocated on a sector-by-sector basis. Instead, space is allocated in 8 sector blocks (I'm told that the double density version allocates in 16 sector blocks). That means there are 1950/8 or 243 (remainder of 6) allocatable blocks of disk space. Blocks zero and 1 are pre-allocated (when a disk is logged on) for the directory and the 6 sectors in the partial block are not used. Thus there are 241 blocks (241*1024 bytes) of disk space available on the disk for data, starting at block 2. In order to keep track of which blocks have been allocated and which have not, the BDOS maintains an "allocation VECTOR" for each logged-on disk. This data structure is an array of 256 bits (packed into 32 bytes), where each bit is associated by position with the corresponding block of disk space. For example the first two bits in the array are always turned on indicating that the first two blocks are allocated to the directory. The allocation vector is created when a disk is selected for the first time (logged on) after re-booting (booting), by examining the DM (Disk Map) field of all non-empty (ET=0) FCBs (File Control Blocks) in the directory. Since the DM field of an extent contains the numbers of all the blocks belonging (allocated) to the extent, the collection of all DM fields in the directory describe all space that is currently used on the disk. It is a relatively simple task to look at a byte in a DM field and turn on its associated bit in the allocation vector, which is exactly what the BDOS does. After the allocation vector is created, it is updated whenever a new block is allocated, as are the DM byes for the extent. It should be noted, I think that this technique for space management does away with specific file and free space linking as well as having to explicitly store a bit map on the disk. It is an easy task to translate block numbers into their corresponding physical track and sector numbers on the disk. By including in the process, a relative record number (from the beginning of an extent) it is not much more work to map the logical records of a file onto exact physical disk locations. This is, I believe, the virtue of CP/M's file structure. 11.5 Disk Mapping Process There are three steps to the mapping process: let: ISECTOR = integer sector number TRACK = track containing sector LSECTOR = logical sector (before staggering) PSECTOR = physical sector (after staggering) BLOCK = block containing logical record 1. The first step is to calculate the integer sector number. This is done using: ISECTOR = (BLOCK*8) + (NR MOD 8) +52 We multiply BLOCK by 8 because there are 8 sectors per block. NR MOD 8 produces the displacement into the block, and 52 must be added because block zero begins at ISECTOR 52 (remember that the System image is stored in the first 52 disk sectors). Note: if you're unfamiliar with the term "MOD", it means "Use the remainder produced by the division". E.G.: 25 MOD 5 = 0 (5*5=25 remainder 0) 25 MOD 7 = 4 (3*7=21 remainder 4) 25 MOD 27 = 25 (27*0=0 remainder 25) 2. Next we calculate the physical track and logical sector. TRACK = ISECTOR/26 LSECTOR = ISECTOR MOD 26 (26 because there are 26 sectors on a track.) 3. Finally we map the logical sector using the routine "MAP" at the beginning of the BDOS: PSECTOR = MAPTABLE(LSECTOR) There is an algorithm which will produce "MAPTABLE": DIMENSION MAPTABLE(26) MAP(1)=1 FOR N=2 TO 26 IF N > 13 THEN J=1 ELSE J=0 MAP(N)=((MAP(N-1)+6 MOD 26) +J NEXT N END What this really does is stagger the logical sectors around the track so that they are actually 5 physical sectors apart. This allows CP/M five sectors or about 35 milliseconds of processing time between disk accesses in order to keep up with disk latency. 11.6 FILE CONTROL BLOCK USAGE DURING I/O In general the following explains what happens when a read or write operation is requested by an application. There are a series of exceptions which the BDOS checks for, including "READ PAST EOF" and "DISK FULL".. Foor the sake of this discussion let us assume that a write operation is being requested. Reading is very similar. The BDOS will first increment the NR field and determine whether or not the next record is within a previously allocated block. NR := NR+1 IF DM(NR/8) = 0 THEN BLOCK NOT ALLOCATED ELSE BLOCK IS ALLOCATED "NR/8" determines which of the DM bytes (logical blocks) contains the logical record in question. If a physical block needs to be allocated, the BDOS will search through the allocation vector to find an unallocated block (which can be anywhere on the disk). If none exists (i.e. no zero bits in map) then a "DISK FULL" condition is returned to the application (calling program). If a block is found its bit is turned on and its number is deposited at location DM(NR/8). After this, the absolute sector is determined and is converted into the physical track and sector numbers. Then the data is written. The read operation is similar except that no space allocation is performed. Instead, if the record being requested to read falls within an unallocated block a "READ PAST EOF" condition is returned to the application (Calling program). For both the read and write operations, if the NR field goes to 128 decimal when incremented, the BDOS will close the current extent, increment the EX field and attempt to open the next extent of the file. If the operation was a write and the next extent doesn't exist, the BDOS will create it and then open it. If the operation was a read and the next extent doesn't exist, a "READ PAST EOF" condition is generated as it will in the case that either NR is greater that 128 or NR is greater than RC is true. Here is an example: LET: NR = 12 RC = 17 DM(0) = 50 DM(1) = 51 DM(2)-DM(15) = ANYTHING. 1. NR := NR+1 (NR=13) 2. LBLOCK = NR/8 = 12/8 = 1 3. BLOCK = DM(LBLOCK) = DM(1) = 51 4. ISECTOR = (BLOCK*8)+(NR MOD 8) + 52 = (51*8)+(13 MOD 8) + 52 = 408+4+52 = 464 5. TRACK = ISECTOR/26 = 464/26 = 17 6. LSECTOR = ISECTOR MOD 26 = 464 MOD 26 = 22 7. PSECTOR = MAPTABLE(LSECTOR) = MAPTABLE(22) = 24 Thus the sector being accessed is 24 on track 17. Miscellaneous Stuff To conclude, there are some interesting tid-bits which the reader may (or may not for that matter) find useful. The SUBMIT processor in the CCP (Console Command Processor) uses a very interesting programming technique which I found worthwhile understanding. Recall that the transient program "SUBMIT" uses as its input a text file of CP/M commands and produces a file called $$$.SUB which the CCP will use as a command file. For example consider the following submit file: A:PIP B:PROG.ASM=B:PO.SRC,B:P1.SRC(CR)(LF) A:ASM PROG.BBB(CR)(LF) A:LOAD B:PROG(CR)(LF) A:PIP LST:=B:PROG,PRN(T8P50)(CR)(LF) The Submit program turns the file into a series of 128 byte records arranged such that the first line of the original file is the last record of the new file, the second line the second to the last record and so on. Each record has the following form: byte 1: length bytes 2-length+1: command string bytes length+2-128: undefined The above file would look like this when converted to $$$.SUB (the numbers in decimal and brackets are included here just for clarity): record 1: (28)A:PIP LST:=B:PROG,PRN(T8P50) record 2: (13)A:LOAD B:PROG record 3: (14)A:ASM PROG.BBB record 4: (33)A:PIP B:PROG.ASM=B:PO.SRC,B:P1.SRC Why, you ask, is $$$.SUB backwards? Well that's part of the trick. Remember that the RC field in the FCB is an end of extent pointer. If the $$$.SUB file is on the disk the CCP will open it and set the NR field of the FCB to RC-1 and read the file. What this does is read in the last record of the file (as determined by the RC field). After the read the BDOS will decrement the RC field and close the file, which will cause the FCB and specifically the RC field to be updated. The CCP will use the data just read as a command as if it had been typed in from the console. The next time around, the CCP will do the same thing except that the RC field is now pointing at the record whose number is one less than that of the previous operation. In other words, the RC field is used as an implicit record pointer. Very neat and it works too! Sometimes it is desirable to bypass the BDOS and communicated directly with certain BIOS functions. For example MICROSOFT's BASIC interpreter does not use the BDOS character I/O functions as it does its own line editing and the USCD Pascal system completely overlays the BDOS. There is a technique for accessing the BIOS that is general enough so as not to be considered a kludge. An application can always find the page boundary on which the BIOS begins by examining the high order address byte of the warm boot entry point. Using that as the high order byte of the address the low order byte is set to an offset into that pags determined by: OFFSET=FUNCTION * 3 as each entry is three bytes. For example here is a short routine which causes data to be written to the list device: LOFF EQU 0FH ; BIOS+LOFF=LIST ENTRY LIST: PUSH H ; SAVE HL LHLD 1 ; GET ADDRESS OF WARM BOOT MVI L,LOFF ; SET LOW ORDER BYTE TO LIST OFFSET XTHL ; RESTORE HL, LIST ADDRESS ON STACK RET ; EXECUTE LIST ROUTINE IN BIOS This technique works as long as the BIOS begins on a page boundary. The more general technique would be: LOFF EQU 0FH-3 ; OFFSET FROM WARM BOOT ENTRY LIST: PUSH H ; SAVE HL LHLD 1 ; GET ADDRESS OF WARM BOOT PUSH D ; SAVE DE LXI D,LOFF DAD D ; GET TO LIST ENTRY POINT POP D ; RESTORE DE XTHL ; RESTORE HL RET ; EXECUTE LIST ROUTINE There is one more thing and then I'll quit. If you remember from above, I mentioned that while poking around inside a screen-oriented text editor, I found that it modified the error address fields at the beginning of the BDOS. It also does another clever thing. In the editor there is a command to save the rest of the file, exit the editor and automatically process it with an entirely different program, such as an assembler or text formatter. There is an interesting technique here which could be generally useful. What happens is this: First, modify the address of the console input routine in the BIOS jump table to cause a routine inside the application program to supply data to the CCP. This is done as follows: LHLD 1 ; GET THE BIOS PAGE ADDRESS IN H MVI L,CI+1 ; HL IS THE ADDRESS OF THE ADDRESS ; OF CONSOLE INPUT ROUTINE MOV E,M ; GET THE DEVICE ADDRESS IN DE INX H MOV D,M XCHG SHLD SAVE ; SAVE IT OR LATER LXI D,ALT ; DE IS ADDRESS OF ALTERNATE ROUTINE MOV M,D ; POKE JUMP ADDRESS IN BIOS JUMP TABLE DCX H MOV M,E JMP 0 ; AND RE-BOOT SAVE: DS 2 ; LOCATION USED TO SAVE ; CONSOLE INPUT DEVICE ADDRESS After doing this, everytime the CCP requests a character from what it thinks is the console input device, it will be handed a character from inside the original application program. Just before handing the CCP a carriage return, the application will restore the original address of the console input routine: LHLD SAVE ; GET ORIGINAL DEVICE ADDRESS XCHG ; PUT IN DE LHLD 1 ; GET ADDRESS OF ADDRESS FIELD OF ; CONSOLE INPUT ENTRY MVI L,CI+1 MOV M,D ; RESTORE ORIGINAL ADDRESS INX H MOV M,E ; ... AND CONTINUE The routine that does character handling is essentially this: LHLD POINTER ; GET ADDRESS OF NEXT CHARACTER MOV A,M ; GET NEXT CHARACTER INX H ; ADVANCE POINTER SHLD POINTER ; SAVE POINTER CPI CR ; END OF DATA? RNZ ; IF NOT THEN JUST RETURN IT ; ELSE RESTORE CONSOLE ; INPUT ADDRESSES This works for two reasons: obviously the BIOS jump table can be considered data as well as code (hail to John Von Neumann) and since the original program will remain intact until the next program is actually loaded on top of it, the routine simulating the console will function normally. This technique suggests a viable method for chaining a series of programs together without having to specifically build a submit file for each chain. CP/M QUICK REFERENCE; compiled by Steve Stolen =========================================================================== I Func. I Func. I Function I Entry Parameter I Return Value I I Code I Code I (C) I (DE) (E) I (A) I I (Dec) I (Hex) I I I I ========================================================================== I 1 I 1 I Read Console I - I ASCII Char. I --------------------------------------------------------------------- I 2 I 2 I Write Console I ASCII Char. I - I ---------------------------------------------------------------------------- I 3 I 3 I Read Console I - I ASCII Char. I ---------------------------------------------------------------------------- I 4 I 4 I Write Punch I ASCII Char. I - I ---------------------------------------------------------------------------- I 5 I 5 I Write List I ASCII Char. I - I --------------------------------------------------------------------------- I 6 I 6 I - I - I - I ---------------------------------------------------------------------------- I 7 I 7 I Get I/O Status I - I I/O Status Byte I ---------------------------------------------------------------------------- I 8 I 8 I Set I/O Status I I/O Status Byte I - I ---------------------------------------------------------------------------- I 9 I 9 I Print Buffer I Buffer Address I - I ---------------------------------------------------------------------------- I 10 I A I Read Buffer I Buffer Address I - I ---------------------------------------------------------------------------- I 11 I B I Check Console Rdy I - I - I ---------------------------------------------------------------------------- I 12 I C I Lift Head I - I - I ---------------------------------------------------------------------------- I 13 I D I Initialize BDOS I - I - I ---------------------------------------------------------------------------- I 14 I E I Log in Drive I 0...N I - I ---------------------------------------------------------------------------- I 15 I F I Open File I FCB I 255=not present I ---------------------------------------------------------------------------- I 16 I 10 I Close File I FCB I 255=not present I -------------------------------------------------------------------------- I 17 I 11 I File Search I FCB I 255=no match I ---------------------------------------------------------------------------- I 18 I 12 I File Search Next I FCB I Adr.next entry I ---------------------------------------------------------------------------- I 19 I 13 I Delete File I FCB I - I ---------------------------------------------------------------------------- I 20 I 14 I Read Next Record I FCB + I 0=OK 1=EOF 2=ND I ---------------------------------------------------------------------------- I 21 I 15 I Write Next Record I FCB + I See Notes I ---------------------------------------------------------------------------- I 22 I 16 I Make File I FCB I 255=DIR Full I ---------------------------------------------------------------------------- I 25 I 19 I Drive Number ? I - I Drive Number I ---------------------------------------------------------------------------- I 26 I 1A I Set DMA Address I DMA Address I - I ---------------------------------------------------------------------------- ============================================================================ HOW TO PATCH CP/M TO BACKSPACE ON RUBOUT ---------------------------------------- The patches listed below will allow CP/M to echo the delete function as a backspace. Since one of the patches takes advantage of the jump relative capability of the Z80, THE PATCHES WILL NOT WORK AS DESCRIBED ON AN 8080 SYSTEM. However with the info given it will be simple for an 8080 user to make the appropriate patch that will function. CAUTION: I am currently using this patch and to date it has proven to work well. It does not work within the 'Insert' mode of the Editor, but it does work at command level. I'll leave it to someone else to figure out why. Anyway use it at your own risk. PATCH ONE: The console input routine (12F3H) gets a char and, after testing for carriage return, tests for 'delete'. If the test for 7FH is successful, CP/M gets the counter from Reg B and tests it for zero. If true (i.e. we're at start of buffer) a jump is executed back to the console input routine. If we are not at the start of the buffer, CP/M loads the previous char (pointed to by HL) into the Acc and jumps to the CONOUT rout. Since we want to echo a backspace instead of the previous char, replace the MOV A,M with MVI A,08 (or whichever character it is that your terminal treats as a backspace). The extra memory location required by the MVI instruction is recovered by replacing the JZ XX73 with JR Z,E9. More precisely: LOC IN TPA AFTER ORIGINAL MACHINE CODE REPLACED BY SYSGEN ---------------- --------------------- ----------- 1308 CA XX 73 7E 28 E9 3E 08 PATCH TWO: CP/M will convert any character less than 20 hex to its Ascii equivalent preceeded by '^'. All except CR, LF, and that is. To enable our backspace character to be echoed unchanged, we insert a patch at the start of the conversion routine to test for '08' and exit if true. Again in the TPA: LOC IN TPA AFTER ORIGINAL MACHINE CODE REPLACED BY SYSGEN ---------------- --------------------- ----------- 12B0 F5 3E 5E C3 AND: XX F5 3E 5E C3 XX 33 NOTE: The 'XX's above must be replaced by the appropriate page boundaries if your operating CP/M system. Also since we are only echoing one backspace, the control characters which CP/M echoed as two characters will not be completely removed (we leave the '^'). Again I will leave it to you to come up with a more glamorous fix. --- EOF ---