Art: This TOTALLY REPLACES the disk I mailed 2 days ago. Bridger. 12/29/88 TCJ #37 _words in italics_ .h1 main headings .h2 secondary headings Advanced CP/M Raw and Cooked Console I/O Bridger Mitchell {usual sidebar on Bridger} .h1 ZSDOS News The brand-new CP/M disk operating system -- ZSDOS -- that I announced in this column last fall is meeting an enthusiastic reception. At that time I wrote that the quality of the design and testing that have gone into this project means we are unlikely to see a long series of revision numbers to fix bugs. But I didn't mean to imply that a major upgrade, ZSDOS 2.0, might never appear! Indeed, I should have gone on to say that we may expect further contributions from the design team. In the first of a multi-part article in this issue of TCJ, two of the ZSDOS authors -- Hal Bower and Cameron Cotrill -- take you behind the scenes of the many innovations in the new DOS. And several of you have asked about porting ZSDOS to banked-memory (HD64180, Z80) systems. Well, early discussions are afoot to set specifications for a banked-memory version with well-defined memory-management services. Meanwhile Carson Wilson, the third member of the team, has been avidly turning out system utilities with nifty new features and is at work on a Z-System version of a popular public-domain memory-based editor. If you haven't already ordered it, ZSDOS is available from Plu*Perfect Systems and Sage Microsystems East. .h1 Feedback Loop With the demise of yet another magazine (Profiles) that provided some coverage of CP/M topics, TCJ takes on greater prominence as a continuing source of high-quality CP/M information. Art Carlson and we regular columnists need your feedback and suggestions to keep expanding and broadening TCJ's material. I've appreciated the cards and BBS messages several of you have sent. They indicate that you find these columns worthwhile, although not always fully digestible in one sitting! I will continue to focus on more advanced technical topics relating to the CP/M operating system, aiming to get the core concepts and details into print. I fully expect readers to extend, expand, revise and critique these pieces -- that's how our hobby progresses! I'd especially like to receive suggestions for topics for future columns. One early candidate is methods to make the Z-System external environment address available to applications that have not been coded as a Z-System tool, including compiler-generated .COM files. I'd also welcome information about adding 3.5" and high-density 1.2MB 5.25" drives to CP/M systems. I've recently customized DosDisk for an OEM to handle the AT-style 1.2MB floppy disk format. The DosDisk software could be similary extended to handle the 3.5" MS-DOS format, but in order for this to be usable the BIOS must talk to the drive. Some of you have doubtless done this, or at least thought it through. It would make a nice TCJ article! .h1 Unit-Record Input/Output A processor is isolated and quite useless until it can talk to the "outside world". Input and output are essential -- they supply the processor with data and enable it to report results. Memory chips provide the fastest i/o. After that come hard disks, floppy disks, magnetic tape, and serial channels at decreasing data rates. In TCJ #35 we covered file systems. Input and output to files is done in blocks (physical sectors) of many bytes, and file storage devices (floppy disks, hard disks, tape drives, ram disks) are sometimes called block-devices. Our interest in this column is input/output to/from _character_ devices -- devices that normally supply or accept one byte at a time, such as a terminal, printer or modem. Single-byte devices are sometimes called unit-record devices; they can be thought of as special block devices with a record length of one. For many purposes it's useful to think of the computer's software environment as a series of rings. At the outer ring are the application programs. Just inside are the high-level languages, and inside that is the operating system. Its outermost layer is the BDOS, providing a standardized, hardware-independent set of high-level services for access to the file structure and bundled input/output services to various devices. The next ring is the BIOS, providing all of the primitive input/output services needed by the BDOS. It is the border land between a standard system and the specific computer. At the BIOS jump table the interface is completely standardized, but within the BIOS the system designer must program down to bare metal, coding routines that know the precise conditions of the hardware -- disk drive, video display, printer handshaking conditions. In general, applications will be more portable and easier to write if they confine their operating system access to the outer ring -- BDOS calls. Yet there are good reasons for using BIOS calls in some applications, those that require highest performance or services unavailable from the BDOS. And in a few cases, an application must forego portability and itself directly access the hardware, because no BIOS service is available; for example, to read a video terminal's screen or use a modem port. The CP/M 2.2 BIOS provides character-device services for the basic device needed to command the system (the console), an auxiliary device, and a printer. These services are: CONSTAT Console Input Status CONIN Console Input CONOUT Console Output READER Auxiliary Input PUNCH Auxiliary Output LISTSTAT Printer Output Status LIST Printer Output Each input or output service returns or sends a single byte. For input, the routine waits until a byte is ready before it returns; for output, it waits until the device can accept the byte. Strangely, only one BIOS input device and one output device has a status call function available to an application. The BDOS or an application can determine, by calling CONSTAT, whether a character is waiting in the input (a key has been pressed). Similarly, it can call LISTSTAT to see whether the printer is idle and can accept a character. But there is no portable way, in CP/M 2.2, to determine whether the console device is ready to _accept_ a character. All you can do is call CONOUT to send the character and wait, hoping that the device will eventually be ready. This might seem all right (how would you run a CP/M system if you couldn't see its console output?). But, consider an application that wants to keep the processor running at full efficiency (perhaps a video game, or just a smart display utility). It would like to send a character to the console only when it knows that it will be processed immediately. Internally, however, the BIOS must have a routine to determine the input and output status of every device. In order to obtain a valid byte of input it must not access the physical device (a parallel port, an asynchronous receiver chip) until the device signals, by some type of status report, that a byte is ready. And similarly, the BIOS must not output a byte to a physical device (video ram, serial tranmsitter chip, parallel port) until the device signals that its buffer is empty and ready to recieve a byte. .h1 Cooked Input The BDOS provides standardized services to applications, hiding some of the tedious details of communicating with the input and output devices. For the console device the BDOS provides _cooked_ (processed) input and output services, sparing the programmer the overhead of including this code in almost every application. For applications needing raw console input and output, the BDOS also provides a raw (uncooked) function #6. In CP/M 2.2, console single-character input function (#1) provides: . echo to console output . flow control . abort control . tab expansion In addition, the console line-input function (#10) provides limited line-editing and printer controls: . delete last-character (backspace) . cancel line (^X or ^U) . retype line (^R) . list device output control (Other function #10 editing controls -- delete and echo, and end physical line -- existed to serve paper-output teletypes. BDOS patches and replacements such as ZSDOS have eliminated them.) Both the console single-character output function (#2) and the string output function (#9) provide: . flow control .h2 Flow Control and Lookahead Flow control is the process of starting and stopping the flow of bytes over an input/output channel. Our concern here is the control of bytes to the console device. The BDOS is designed so that the user can "freeze" a screen of messages by typing a Control-S -- the standard XOFF character. Output will resume by typing Control-Q -- the standard XON character. Actually, output resumes when any other character (except Control-C -- the abort character) is typed, but it's a good habit to use Control-Q to keep your fingers conditioned for systems, such as unix, that use the standard control characters. In order for flow control to work, the application must print its messages using BDOS functions #2 and #9. Flow control requires something that many -- including compiler authors and BDOS hackers -- have found astonishing: the BDOS console-output functions must call the BIOS console _input_ functions in order to perform a _lookahead_ function. After all, how else could the BDOS know that the user had typed a Control-S to suspend output? It works like this. Before the BDOS sends a character to the console, it checks the console input status. If no key has been pressed, the character is sent. But suppose a key has been typed. In this case the BDOS calls the BIOS console input function to get the character. From this moment on, the character is no longer in the BIOS. The BDOS then tests whether the character is a Control-S. If it is, the BDOS waits for the _next_ keypress and only then sends the output character. If it is _not_ Control-S (or Control-C, discussed below) the BDOS saves the character (say 'A') in a one-character buffer and sends the output character. If the next operation is to print another character on the console, the BDOS test for flow control will become ineffective. The BDOS has only the one-character buffer, which is now full (it's holding the 'A'), so it cannot check the next keypress for Control-S; if it did, and the character were anything else, it would have to throw away one of the input characters. The lookahead function might perhaps have been better implemented by providing a "peek" subfunction to the BIOS CONIN -- return but retain the pending next character. The key result is that the next console input character is moved from the BIOS into the BDOS one-character buffer as a result of any BDOS function #2 or #9 output. As a consequence, any application that uses the _BIOS_ to obtain console input will sometimes "lose" a typed character, only to have it emerge when the BDOS is next used for input (function #1 or #10)! .h2 Coping with missing characters The simplest rule I can give you for coping with missing characters is to keep all console input/output at _one_ level of the operating system -- all BDOS or all BIOS -- within a single application. For example, don't mix BDOS line input (function #10) and BIOS CONIN. It's fairly common for applications to use the BIOS functions for console i/o, in order to speed up output and to get every possible keyboard character. The Z3LIB and VLIB routines used in many Z-System utilities do so. At the start of such an application you may need to check the BDOS, using function #11, to see if a fast keypress has already been sucked into the BDOS one-character buffer. If it returns non-zero, you can get the character with function #1 (but that will echo). In order to use function #6 successfully to get the character without echo, you need to have installed the Plu*Perfect Systems patch (described later). Jay Sage has used the following method of obtaining input (a named-directory password) from BDOS function #10 with echoing shut off. First, save the first byte of the BIOS CONOUT jump vector (it should be the JP opcode) and replace it with a RET opcode. Next, call BDOS function #10. When the BDOS calls the BIOS CONOUT to echo the character, the BIOS will return at once. Then, immediately following the BDOS call, restore the first byte of the BIOS CONOUT jump. (Note that he follows the sound principle of saving and restoring the environment, by saving and restoring the byte in the BIOS "jump vector". he doesn't simply assume it is a JP. It's possible that other code -- perhaps in an RSX -- has already patched this location.) This trick is handy, but should be used only where no other solution is available. In Jay's case, there was insufficient room in the Z34 command processor to collect a password with function #6. The difficulty with this approach is that during the time that the BIOS CONOUT is patched out it is possible that other processes would be generating console output. What other processes could there be in CP/M? If BackGrounder ii is loaded, a press of the key would temporarily suspend the current task and prompt for user input, but the prompt would be invisible! Or an interrupt-driven task could generate a screen message that would be lost. .h2 Abort Control A Control-C will cause the BDOS to abort the current application, jumping directly to 0000, when it is: . the _first_ character typed after a Control-S has halted output from function #2 or #9. . the _first_ character typed to line-input (function #10) The abort control feature of the CP/M 2.2 BDOS is a mixed blessing at best. It gives the user a handy way to kill a job that is scrolling unwanted output to the screen. But it limits the use of edited BDOS line input to applications that can tolerate abrupt termination if the user happens to hit Control-C. As a result, most well-written applications must incorporate their own line editor in order to retain control to avoid being cancelled with unclosed files, open modem connections, or whatever. .h1 Cooked Output In addition to flow control, which is a feature of cooked console input that controls the flow of output, the BDOS alters the raw output to the console by special processing of tabs and by creating a parallel stream of output for the printer. .h2 Tab expansion The BDOS expands the horizontal tab character (09h) to the number of spaces required to reach the next logical tab stop (every eight characters). To do this it keeps a current-column count for all output to functions #2 and #10, resetting it to 0 on each carriage return. This is a handy cooked-output service. But to work successfully, all output on the line must go through these BDOS functions. Avoid mixing BDOS and BIOS console output on the same line. .h2 Echoing to the Printer The BDOS maintains a flag that, when set, causes function #2 and #9 output to be echoed to the BIOS list device as well as the console output. The flag is toggled when a Control-P is typed to function #10 -- the line-input function. Control-P is very handy for getting a quick, selective printed record of some console output. It can also mysteriously freeze your system when the printer is not ready. When your computer locks up, make a habit of checking the attached external devices (printer, modem) before you resign yourself to pressing the reset button! .h1 The Case of the Missing Character If you've used a number of CP/M systems, you've probably had the puzzling and quite annoying experience of occasionally "losing" one character you have typed when running a program, only to have it pop up unexpectedly much later, perhaps at the next command prompt. It's a difficult bug to reproduce, and occurs only on some systems. This spooky gremlin is so perplexing that a user can begin to believe his computer is truly haunted! Has CP/M been visited by the supernatural? Probably not. We've already seen how mixing BDOS and BIOS console functions can cause an input character to become stuck in the BDOS one-character buffer when subsequent input is obtained by BIOS calls. Several years ago, Derek McKay, my partner at Plu*Perfect Systems, spotted another cause of missing characters -- a bug in Digital Research's original design of the CP/M 2.2 BDOS that used faulty logic in the handling of "raw" console input with BDOS function #6. Moreover, Derek developed a Z80 patch that corrects the problem and fits in the original BDOS space. This is an important improvement, because without it there is no totally reliable way to mix cooked BDOS console i/o with any type of raw i/o, either BIOS or BDOS. We included the patch in the CP/M Enhancements that Plu*Perfect originally published for Kaypro systems. More recently, the authors of ZSDOS have incorporated the same logic into their excellent new DOS. So, on these systems, the missing character doesn't manifest itself. .h2 Raw Console Input To understand how a character can disappear, and then reappear, we first need to examine the BDOS's raw console input function. BDOS function #6 was intended to provide absolutely raw console input and output functions accessible by a BDOS call, with no input flow control and no output processing. An application would use this function, for example, when it wanted to get a character without necessarily echoing it to the terminal. DRI attempted to squeeze input, input status, and output into a single BDOS function (probably to save 8080 code space) and in doing so somewhat limited the usefulness of this service. To use function #6, set C=6 and E= 0FFh to get a character, if ready E= 0FEh to get console input status (E=0FDh to wait for a character) E= 0...0FCh to output the value in E to the console When used for input, function #6 returns a 1-byte value in A. If A is 0, no character is waiting; a non-zero value is the input character. Thus it is impossible to enter a nul character (Control-@ on most keyboards) when function #6 is used. (This defect is significant for editors, which must therefore use BIOS functions for console i/o.) When used for output, function #6 is limited to values 0h to 0FCh. Usually ok, this restriction makes some 8-bit coded graphics characters unprintable on a few terminals. (The subfunction code 0FDh is used by CP/M Plus and ZSDOS to wait for the next character and return it.) But the real bug in function #6 is its internal check for input status. The original BDOS code (figure 1) calls the BIOS CONSTAT routine to determine if a character is waiting. This is fine, except that another BDOS function may have called the lookahead routine to test for flow control and left the tested character in the lookahead buffer. When that situation exists, function #6 will return A=0 (no character waiting) until a key is typed, and then return the next typed character, not the one last typed and still in the buffer! Meanwhile, the tested character continues to sit in the buffer. Eventually, someone -- either the application program or the command processor -- will call a BDOS function that does check the lookahead buffer before returning a character. It will find the character still there, and return the missing character! .h2 Code Figure 2 contains the replacement routine. It calls a new "ckstat" routine to determine input status. The new routine just fits into the space made available by rewriting the "lkahead" routine just above it in z80 code. Note the exact logic of the ckstat routine. By clever coding it returns two flag values -- nonzero and carry not set when the next character should be obtained from the BDOS and nonzero and carry set when the next character should be obtained from the BIOS. With this new routine, the lkahead routine can determine from calling ckstat whether to call the BIOS CONIN. .h2 Patching your BDOS If you are running the original CP/M 2.2 BDOS you can upgrade it with the function #6 patch. Using a debugger, first check that the original 8080 code is exactly as shown in the figures. Then assemble just the patch code with the BDOS equate set to the base value for your system, and output a hex file. The hardest part is getting the patch installed in your system. You can load it with a debugger, and then check memory to see that it is installed. But if your system reloads the BDOS on a warm boot, the patch will be gone when the next program runs. If that's the case, you will need get the patch into the SYSGEN.COM image of the BDOS. Load SYSGEN.COM with a debugger. On most systems, the BDOS image begins at 1200h. Compare the bytes there and in the running BDOS in high memory and then compare the bytes at the patch locations in the image (by adding 1200h to the addresses in the figures here) and in high memory. If all matches up, load the hex patch into the high BDOS, compare again, and then move just the patched bytes of the two upgraded routines to their corresponding location in the overlay: MBDOS+0123,BDOS+0141,1200+0123 MBDOS+02D4,BDOS+02EC,1200+02D4 Then save the appropriage number of pages of the modified XSYSGEN.COM. Run XSYSGEN and place the system on the boot tracks of a scratch disk. Boot the disk, test the system for normal operation, and then with a debugger check the high BDOS to see that the patches are indeed in place. Note that this patch will not work with ZRDOS or other replacement BDOSes. It may be possible to write a functionally equivalent ZRDOS patch, if you can find enough free space in a BDOS that is already in Z80 code. Figure 1. Corrected CP/M 2.2 BDOS Function #6 Routine ------------------------------------------------------ Authors: Derek McKay, Bridger Mitchell (Plu*Perfect Systems) 0000 bdos equ 0000h ; base of CP/M 2.2 BDOS 00B7 abort equ bdos+00B7h ; "jp 0000" 00FB getchar equ bdos+00FBh ; get next console input char. 0301 setretval equ bdos+0301h ; set return value in A 030A charbuf equ bdos+030Ah ; 1-character input buffer 0D91 exit equ bdos+0D91h ; BDOS exit routine 0E00 bios equ bdos+0e00h ; base of BIOS 0E06 constat equ bios+6 ; console status 0E09 conin equ bios+9 ; console input 0E0C conout equ bios+0Ch ; console output ; -- original (8080) Direct Console I/O Routine -- ; malfuncting code marked with "***" 02D4 org bdos + 2D4h 02D4 79 fn6: ld a,c 02D5 3C inc a 02D6 CA 02E0 jp z,fn6in 02D9 3C inc a 02DA CA 0E06 fn6s: jp z,constat ; *** 02DD C3 0E0C jp conout 02E0 CD 0E06 fn6in: call constat ; *** 02E3 B7 or a,a 02E4 CA 0D91 jp z,exit 02E7 CD 0E09 call conin ; *** 02EA C3 0301 jp setretval ; -- corrected (z80) routine -- 02D4 org bdos + 2D4h ; 02D4 79 fn6: ld a,c ; if c == FF 02D5 3C inc a 02D6 28 08 jr z,fn6in ; ..get character 02D8 3C inc a ; if c == FE 02D9 CA 0137 jp z,ckstat ; ..get input status ; of buffer & bios 02DC C3 0E0C jp conout ; ..else output char fn6in: call ckstat ; check both buffer ; and bios 02DF CA 0D91 jp z,exit ; ..no char waiting, ; return 0 status 02E2 CD 00FB call getchar ; get char from buffer ; or bios 02E5 18 1A jr setretval ; and return it ; Figure 2. Console Look-Ahead Routines -------------------------------------- ; -- original (8080) console input look-ahead routine -- 0123 org bdos + 0123h 0123 3A 030A lkahead:ld a,(charbuf) 0126 B7 or a,a 0127 C2 0145 jp nz,return1 012A CD 0E06 call constat 012D E6 01 and 1b 012F C8 ret z 0130 CD 0E09 call conin 0133 FE 13 cp 'S'-'@' 0135 C2 0142 jp nz,savechar 0138 CD 0E09 call conin 013B FE 03 cp 'C'-'@' 013D CA 0000 jp z,0000 0140 AF xor a,a 0141 C9 ret 0142 32 030A savechar:ld (charbuf),a ; save input char ; in buffer 0145 3E 01 return1:ld a,1 ; return a non-zero 0147 C9 ret ; character ; -- shorter replacement (z80) routine -- 0123 org bdos + 0123h 0123 CD 0137 lkahead:call ckstat ; if no char waiting 0126 C8 ret z ; ..return 0127 DC 0E09 call c,conin ; if no char in buffer, ; call bios 012A FE 13 cp 'S'-'@' ; if not ^S 012C 20 14 jr nz,savechar ; ..return the char 012E CD 0E09 call conin ; ^S, so get next char 0131 FE 03 cp 'C'-'@' ; if ^C 0133 28 82 jr z,abort ; ..abort 0135 AF xor a,a ; else return false 0136 C9 ret ; status ; new check-console-status (z80) routine 0137 3A 030A ckstat: ld a,(charbuf) ; if buffered char waiting 013A B7 or a,a ; ..clear CY and return NZ 013B C0 ret nz 013C CD 0E06 call constat ; else check bios for a char 013F B7 or a,a ; if char waiting there 0140 0F rrca ; ..set CY and set NZ 0141 C9 ret ; resume original (8080) code at 0142h