!pr0
!lm12
!rm75
Commented Listing of ProDOS $F800-$F90B, $F996-FEBD
                       ....................Bob Sander-Cederlof

ProDOS boots its bulk into the RAM card, from $D000 thru $FFFF.  More is loaded into the alternate $D000-DFFF space, and all but 255 bytes are reserved out of the entire 16K space.

A system global page is maintained from $BF00-BFFF, for various variables and linkage routines.  All communication between machine language programs and ProDOS is supposed to be through MLI (Machine Language Interface) calls and the system global page.

One of the first things I did with ProDOS was to start dis-assembling and commenting it.  I want to know what is inside and how it works!  Apple's 4-inch thick binder tells a lot, but not all.

Right away I ran into a roadblock:  to disassemble out of the RAM card it has to be turned on.  There is no monitor in the RAM card when ProDOS is loaded.  Turning on the RAM card from the motherboard monitor causes a loud crash!

I overcame most of the problem by copying a monitor into the $F800-FFFF region of the RAM card like this:

       *C089 C089 F800<F800.FFFFM
       *C083 C083

The double C089 write-enables the RAM card, while memory reads are still from the motherboard.  The rest of the line copies a monitor up.  The two C083's get me into the RAM card monitor, ready to type things like "D000LLLLLLLLLLLL"

But what about dis-assemblies of the space between $F800 and $FFFF?  For this I had to write a little move program.  My program turned on the RAM card and copied $F800-FFFF down to $6800-6FFF.  Then I BSAVEd it, and later disassembled it.

The code from $F800-FFFF is mostly equivalent to what is in DOS 3.3 from $B800-BFFF.  First I found a read/write block subroutine, which calls an RWTS-like subroutine twice per block.  (All ProDOS works with 512-byte blocks, rather than sectors; this is like Apple Pascal, and the Apple ///.)

The listing which follows shows the RWB and RWTS subroutines, along with the READ.ADDRESS and READ.SECTOR subroutines.  Next month I plan to lay out the SEEK.TRACK and WRITE.SECTOR subroutines, as well as the interrupt and reset handling code.

The outstanding difference between ProDOS and DOS 3.3 disk I/O is speed.  ProDOS is considerably faster.  Most of the speed increase is due to handling the conversion between memory-bytes and disk-bytes on the fly.  DOS pre-converted a 256-byte block into 342 bytes in a special buffer, and then wrote the 342 bytes; ProDOS forms the first 86 bytes of the disk data in a special buffer, writes them, and then proceeds to write the rest of the data directly from the caller's buffer.  When reading, DOS read the 342 disk-bytes into a buffer for later decoding into the caller's buffer.  ProDOS reads and decodes simultaneously directly into the caller's buffer.  This is achieved by extensive use of tables and self-modifying code.

Not only is direct time saved by doing a lot less copying of buffers, but also the sector interleaving can be arranged so that only two revolutions are required to read all 8 blocks on a track.

I believe Apple Pascal uses the same technique, at least for reading.

Whoever coded ProDOS decided to hard-code some parameters which DOS used to keep in tables specified by the user.  For example, the number which tells how long to wait for a drive motor to rev up used to be kept in a Device Characteristics Table (DCT).  That value is now inside a "LDA #$E8" instruction at $F84F.  That means that if you are using a faster drive you have to figure out how to patch and unpatch ProDOS to take advantage of it.

Another hard-coded parameter is the maximum block number.  This is no longer part of the data on an initialized disk.  It is now locked into the four instructions at $F807-F80D, at a maximum of 279.  If you have a 40- or 70-track drive, you can only use 35.  Speaking of tracks, the delay tables for track seeking are still used, but they are of course buried in this same almost-unreachable area.  If you have a drive with faster track-to-track stepping, the table to change is at $FB73-FB84.

Calls to RWTS in DOS 3.3 involved setting up two tables, an IOB and a DCT.  The IOB contained all the data about slot, drive, track, sector, buffer address, etc.  The DCT was a 5-byte table with data concerning the drive.  ProDOS RWB is called in an entirely different way.  A fixed-position table located at $42-47 in page zero is set up with the command, slot, buffer address, and block number.

There are three valid commands, which I call test, read, and write.  Test (0) starts up the indicated drive.  If it is successful, a normal return occurs; if not, you get an error return (carry set, and (A) non-zero).  Read (1) and write (2) are what you expect them to be.  RWB has a very simple job:  validate the call parameters in $42-47, convert block number to track and sector, and call RWTS twice (once for each sector of the block).

ProDOS RWTS expects the sector number in the A-register, and the track in a variable at $FB56.  RWTS handles turning on the drive motor and waiting for it to come up to speed.  RWTS then calls SEEK.TRACK to find the desired track, READ.ADDRESS to find the selected sector, and branches to READ.SECTOR or WRITE.SECTOR depending on the command.

READ.ADDRESS is virtually the same in ProDOS as it was in DOS 3.3.  READ.SECTOR is entirely different.  I should point out here that ProDOS diskettes are entirely compatible with Apple /// diskettes.  The file structures are exactly the same.  Both ProDOS and Apple /// diskettes use the same basic recording techniques on the disk as DOS 3.3, so the diskettes are copyable using standard DOS 3.3 copiers such as the COPYA program on your old System Master Diskette.

READ.SECTOR begins by computing several addresses and plugging them into the code further down.  (This enables the use of faster addressing modes, saving enough cycles to leave time for complete decoding of disk data on the fly.)  First the disk slot number is merged into the instructions which read bytes from the drive.  Next the caller's buffer address is put into the store instructions.

Note that the byte from the disk is loaded into the X-register, then used to index into BYTE.TABLE, at $F996, to get the equivalent 6-bit data value.  Since a disk byte may only have certain values, there is some space within BYTE.TABLE that will never be accessed.  Most of this unused space contains $FF bytes, but some of it is used for other small tables:  BIT.PAIR.LEFT, .MIDDLE, and .RIGHT, and DATA.TRAILER.  These are used by WRITE.SECTOR, which we'll look at next month.

Your buffer is divided into three parts:  two 86-byte chunks, and one of 84 bytes.  Data coming from the disk is in four chunks:  three of 86 bytes, and one of 84.

The first chunk contains the lower two bits from every byte in the original data.  READ.SECTOR reads this chunk into TBUF, so that the bits will be available later for merging with the upper six of each byte.  ($FC53-FC68)

The second chunk contains the upper six bits from the first 86 bytes of the original data.  $FC69-FC83 reads the chunk and merges in the lower two bits from TBUF, storing the completed bytes in the first 85 bytes of the caller's buffer.  The last (86th) byte is saved on the stack (I am not sure why), and not stored in the caller's buffer until after all the rest of the data has been read.

A tricky manipulation is necessary to merge in those lower two bits.  The data in TBUF has those bits in backward order, packed together with the bits from the other chunks.  There was a good diagram of this on page 10 of the June 1981 issue of AAL.  DOS merged them with a complex time-consuming shifting process.  ProDOS does a swift table lookup, using the TBUF byte as an index to the BIT.PAIR.TABLE.

BIT.PAIR.TABLE has four bytes per row.  The first three in each row supply the bit pairs; the fourth is used by SECTOR.WRITE to encode data, and will be covered next month.

When $FC69-FC83 is reading the first chunk, the first byte in each row is used to supply the lower two data bits.  The byte in TBUF corresponding to the current position in the chunk selects a byte from BIT.PAIR.TABLE, and the two parts are merged together.

The next two chunks are handled just like the one I just described.  After all the data has been read, READ.SECTOR expects to have accumulated a checksum of 00, and expects to find a trailing $EB after the data.  Return with carry clear indicates all went well; carry set indicates a read error (bad checksum, missing header, or missing trailer).

I can't help wondering:  can this fast read technique be fit into DOS 3.3?  It takes a little more code and table space, but on the other hand it uses 256 bytes less of intermediate buffer.  If we sacrificed the INIT command, could both the fast read and write be squeezed into DOS 3.3?

For more good information on ProDOS, be sure to take a look at Tom Weishaar's DOStalk column in the current issue of Softalk.
