Customizing the Atari Operating System Device Handlers: Part II
The final half of this series is for experienced MAC/65 programmers. it describes an interactive handler that saves machine language programs as boot files. This program requires MAC/65 and OS/A+, and works on all 8-bit Atari computers of any memory size, with disk drive.
In the previous month's Antic, the first half of this series introduced device handlers and described how they work by creating two simple handlers. This final installment describes the creation and operation of MAKEBOOT, a more sophisticated device handler.
The MAKEBOOT handler lets you save object code as a boot file and convert binary load files to boot files. Before the MAKEBOOT handler can do this, however, it does something quite unique-it asks you questions.
Although almost every useful computer program prompts you for information, handlers do not. Since the CIO (Central Input/Output) uses device handlers whenever it operates, the device handlers cannot easily use CIO to prompt you for information- the CIO is busy.
If we try to use the CIO while it's busy, your Atari usually-but not always-becomes confused and acts strange. This is why handlers should not use CIO for I/O to the screen or keyboard.
The catch is that we often want to interact with a program while a handler is in use. Therefore we must use the screen or keyboard handlers directly, without going through the CIO.
MAKEBOOT is an example of such a handler. The MAKEBOOT program requires you to direct the handler operation and make some decisions while it's operating. Your Atari operating system has a built-in mechanism for accomplishing this.
Uses for this program include loading an alternate program into the same area occupied by DOS, or initializing your Atari before DOS is loaded. For example, you could load in the modification to the printer handler. You could use this program to produce bootable games or programs that produce a disk menu.
A boot file is a machine language program which resides on the outermost sectors of a disk. It is automatically loaded whenever you boot with that disk. On the disk, the boot file is a continuous, uninterrupted file which begins in the first sector and occupies successive sectors until the end of the file. On disk, there are no breaks between the end of one sector and the beginning of the next- and no directory.
Binary load files are machine language programs which may reside anywhere else on the disk. On the disk, a binary load file may be broken into sector-sized pieces and scattered throughout the disk. The last three bytes of each sector direct your Atari to the next sector of the file. Your Atari treats these sector links as "Continued On Sector xxx" messages.
Every time you boot a disk, your Atari checks the first six bytes of the first sector to determine what action to take next.
Byte 0, the first of these six bytes, is used as a flag. (A zero in this location denotes a boot file.) This value is stored to DFLAGS, memory location 576 ($0240).
Byte 1 contains the number of sectors to load, bytes 2 and 3 tell your
Atari where to load the boot data (this is the "load address"), and the
next two bytes tell your Atari where to go after the program starting at
byte 6 is executed (this is the "initialization address").
The program starting at byte 6 is an initialization program and usually ends with an RTS (ReTurn from Subroutine) instruction. If there is no initialization routine, then byte 6 must be an RTS instruction, which is represented by a 96 ($60).
If the initialization routine doesn't start at byte 6, then byte 6 must
be a JUMP instruction, directing your Atari to the start of the initializing
When the initialization program ends, the operating system jumps to the memory location given in bytes 4 and 5.
The program in Listing 1, MAKEBOOT.M65, treats the first nine bytes of sector 1 as if it was structured as shown in Figure 1. This structure requires nine bytes of data on sector 1.
When a series of sectors is loaded as part of the initial boot, the sectors are loaded sequentially in memory. For example, if the initial load address is 1000, then sector 1 loads its data starting at 1000 (including the first six bytes), sector 2 loads its data starting at 1128 (there are 128 bytes per sector), etc.
This initial boot sector load is called the first-stage load. For a DOS format disk, three sectors are loaded in the first-stage load, then your Atari loads DOS.SYS, a second-stage load.
Since the boot sectors are loaded in memory sequentially, the specified load address (bytes 2 and 3) is treated as the memory location of byte 0 of sector 1, and data on the disk is calculated relative to that address. In the example, if the load address was 1000, then byte 0 of sector 1 corresponds to memory address 1000, byte 0 of sector 2 corresponds to address 1128, etc. Note that the initialization routine of the boot sectors starts at 1006, since all of sector 1 (including the first six bytes) is loaded.
THE LOAD FILE
Now that we've determined a way to put data on the boot disk, we need to know how data will be received from the CIO. Load files are loaded by DOS as a series of data blocks. A block can be any length, but they're typically 251 bytes long (at least in MAC/65) and preceded by two two-byte numbers. The first number is the starting address, where the first byte is stored. The second number is the ending address, where the last byte is stored.
If DOS was loading this file in memory, each byte of the block would be stored sequentially until the ending address was reached. Then it would repeat the process until all the data had been loaded.
This varies only at the start of a file and when appending files. The start of a file has two bytes of the value 255 that identify it as a load file. When one load file is appended to another, these bytes are carried over to the load data. This means that a data block is preceded by either four or six bytes, where the first two are 255, 255.
Since each block has its own load address, data can be loaded in widely separated memory locations even for a short load file. Thus the load file doesn't necessarily have the same number of sectors as the resulting boot sector count used by the MAKEBOOT handler.
Finally, two more addresses are used by DOS as vectors for load file execution-the initialization address loaded to INITAD, memory location 738 ($02E2); and the run address loaded to RUNAD, memory location 736 ($02E0). The latter is executed after the file is completely loaded and the former is executed as soon as a new address is loaded to INITAD.
Generally these addresses should correspond to the boot sector run address of bytes 4 and 5 and the initialization routine starting at byte 6. Both addresses are loaded as any other data from a load file (e.g. as a two-byte data block).
HOW IT WORKS
In Part 1 of this series, we discussed three steps of adding a new handler:
1. Write the program for the handler.
2. Set up the Handler Table.
3. Make an entry in the Handler Address Table.
In step 1, the routines that comprise the handler are on lines 5000-8600.
The open routine (BOPEN, lines 5185-5480) sets the initial values of the variables used in the program and checks to make sure you still want to proceed. It also writes zeros into as many sectors as you want, starting with sector 1.
The close routine, line 7130-7495, writes the last sector to the boot disk. Then it takes the actual sector count, the run address and the initialization address, and asks you if you want to add these to the boot disk (the first nine bytes of sector 1).
The PUT BYTE routine (BPUT), line 6925-7085, receives all the data from the load file. Most subroutines in this program support the PUT BYTE handler. This routine first stores the byte from the CIO and then checks to see if it's part of the first six bytes of the load file. If so, subroutine FSTSIX checks for a load file and lets you set the sector count, load address, run address and initialization address.
Data after those first six bytes is either program data stored in a 128-byte buffer before being written to the boot disk, or load information extracted by the subroutine LDINFO. This subroutine compares the starting address of the load file with the corresponding boot sector load address and calculates the location of the next block on the boot disk. If a load file address is lower than the specified boot disk load address, an error message is issued and the CIO returns control to you.
The data in memory locations 736-739 ($02E0-$02E3) are stored in the variables RUNADR and INTADR. In the CLOSE routine, you can add these values to their respective positions in sector 1.
The handler for the GET status routine is also used as the general exit
routine for all handler routines. This large program needs an internal
status variable. Error codes are stored in STATS and loaded into the accumulator
and Y register when the handler returns to the CIO. The CIO returns control
to you when an error code greater than 127 occurs.
|Read a Sector|
|Write a Sector|
The GET BYTE and special functions are not implemented here and are represented by NOFNT (line 6785). This is simply an RTS which passes error code 146 back to the CIO.
The handler in lines 5035-5065 is not very complicated. Each address is represented by the address-minus-one of each routine and is in the order given in FIgure 1 in part one of this series from last month's Antic.
Step 3 (lines 440-630) makes an entry into the Handler Address Table, finding an empty spot in the Handler Address Table and adding the ASCII code for "B" followed by the address of the Handler Table. That's the same routine used in the NULL handler.
I/O WITHOUT CIO
The I/O subroutines for the MAKE-BOOT handler run from line 7505 to the end of the program. The first one reads and writes sectors to the boot disk. It doesn't use the resident disk handler (DSKINV) but instead uses the serial bus I/O utility vector (SIOV) and lets you write without write-verification, greatly speeding the process of writing to disk.
To use SIOV, we must fill in all the values of the Device Control Block (DCB) from memory locations 768- 779 ($0300-$030B). But for this application only four bytes of data are variable. To read a sector, set the following memory locations:
The command for writing without verification is $0050, and with verification it's $0057. The only other variable is the sector number in bytes $030A and $030B (low byte, high byte) taken from the variable SECNUM. All other values are supplied by the routine DISKIO.
The second I/O subroutine in lines 7840-7930 accepts either Y or N from the keyboard buffer and loads the accumulator with either a one for Y or a zero for N. Upon returning from this subroutine, a REQ or BNE tests for the key pressed. The only drawback to this is that the character for the key pressed is not displayed.
These two routines perform I/O consistently between Atari operating systems. SIOV is a vector that always points to the serial bus I/O utility, and the keyboard buffer is always at $02FC. To get or display a string of bytes from the keyboard, we need a different approach.
SCREEN EDITOR HANDLER
Both writing to the screen and receiving a string from the keyboard
can be done via the screen editor handler. Printing to the screen is done
by loading the accumulator with the ASCII value of the character to be
displayed and doing a JSR to the screen editor's PUT BYTE routine. To get
a byte from the screen, do a JSR to the screen editor's GET BYTE routine.
Upon return, the ASCII value of the next key pressed will be in the accumulator.
program to produce your
own bootable games or programs that
produce a disk menu.
For most Atari operating systems, the screen editor's PUT BYTE routine starts at $F6A4 and the GET BYTE routine starts at $F63E. Your program might use these addresses to read and write to the screen. The problem is that these locations aren't guaranteed and may be at different locations in different operating systems. We have to find these handler routines independently of the operating system.
The method for this is included in the initialization routine for the MAKEBOOT handler. Lines 690-1070 first locate the screen editor's Handler Table by searching the Handler Address Table (starting at $031A) for the E: device.
The two bytes following the ASCII E are the address of the Handler Table, in which bytes 4 and 5 are the address-minus-one of the GET BYTE routine and bytes 6 and 7 are the address-minus-one of the PUT BYTE routine. These addresses are stored in a three-byte jump instruction on lines 8295 and 8320. One is added to each address, so we're ready to do I/O to and from the screen.
Instead of doing a JSR to a location in the operating system, we do a JSR to either EPUT or EGET. The program is vectored to the true address of the PUT BYTE and GET BYTE routine.
Now that we've established a legal way of using the screen editor to read and write to the screen, we can finish discussing the I/O routines.
To use the subroutine in lines 8140-8270 that displays characters, load the low byte of the address of the first character of the string into the accumulator and the high byte into the Y register. Then JSR to PRINT.
This continues to display characters until it finds one with the most significant bit set (values greater than 127). If the last character equals 128, then the cursor will remain at the end of that line of text. All values greater than 128 will make the text end with a carriage return. The only other control character is a carriage return, represented by zero. Lines 8260-8515 give examples of how this routine is used.
The routine called PNUM (lines 7950-7975) displays a two-byte integer as a base 10 number. To use it, put the low byte of the number in $00D4 (FRO) and the high byte in $00D5. Then do a JSR to PNUM. The routine uses the floating-point routines found at $D800 to $DFFE IFP converts the integer to a floating-point number in FRO. FASC converts a floating-point number in FRO to a string in a buffer called INBUFF at $0580. PRINTE displays the resulting string.
Finally, GETNUM inputs a user-generated number and converts it to an integer in FRO. This routine also uses the floating-point routines, but it starts with an ASCII string in INBUFE The string is input from the keyboard by doing a JSR to EGET until a carriage return is reached. To avoid most errors, the ASCII value for a zero ($0030) is put in the first byte in the INBUFF buffer. This means that any character other than a number will return a zero.
USING THE PROGRAM
MAKEBOOT is written for OS/A + DOS, and will not work with Atari DOS 2. Compile the source code (Listing 1, MAKEBOOT.M65) using MAC/65 or your Atari Assembler/Editor cartridge. If you have the Antic monthly disk, you will find both the source code and the executable file (MAKEBOOT.EXE) already on the disk. (This executable file will NOT run with DOS 2.)
Load the resulting file from DOS. To use the new handler, simply use the COPY command to copy the desired load file to the B: handler. You need a disk to hold the new boot sectors. (It's a good idea to use a freshly formatted disk, and always a good idea to work with backup copies of your programs, just in case.)
The first six bytes of information can be added in several ways. The easiest is to put them in your program before compiling it, as shown in the example below.
100 ;Start of your program
120 *= (your load address)
130 START .BYTE 0
140 SECCNT .BYTE [LAST-START]/128+ 1
150 LOADAD .WORD START
160 RUNADR .WORD (your run address)
170 INITAD JMP (your program init)
LAST is a label that is added to the end of your program.
If this is impossible or inconvenient, they can be added while the MAKEBOOT handler is running. The first opportunity is before the load file is written to the boot disk. At this point the program asks you for the sector count, load address, run address and initialization address.
If you use this method, you must leave at least six bytes between your
load address and the beginning of the boot program for the boot information.
If you specify an initialization address, you must leave nine bytes because
the initialization address is added as a jump to the address you specify
and it starts at byte 6.
FIGURE 1 Boot Sector Data Byte # Bytes Purpose 0 1 Flag stored at $0240 1 1 Boot sector count 2 2 Load address 4 2 Run address 6 3 Jump to initialization addressThe initialization address can start at byte 6 but for the purpose of this program, a jump instruction is placed here.
Finally, after the boot sectors are written, the MAKEBOOT handler gives you the actual sector count, load file run address and load file initialization address. Then it asks you if you want to add them. If you respond with a [Y] to these prompts, the corresponding data will be added to sector 1.