Search code examples
assembly6502apple-ii

Apple IIe 6502 Assembly Accessing Disk


I'm currently writing a program for the Apple IIe that requires reading/writing files from disk. In reading the books I've found archived online about assembly language for the Apple II I've come across the $C060 subroutine which is for accessing the cassette port, but I can't seem to find a subroutine that will access the disk drive. Is there such a monitor command? If not, what would I do to read/write a file to/from disk during the program?


Solution

  • It's possible to read and write a floppy disk without loading in DOS. DOS is useful if you want to read/write disks that are usable by other programs, and making things work reliably using DOS is apt to be easier than using raw I/O, but raw I/O can be faster than DOS and allow more information to be stored on a disk, especially if you never need to read or write less than a track at a time.

    The overall concept

    When using track-at-a-time I/O, writing and reading a disk is conceptually simple:

    1. To write a disk track, build a buffer holding about 6K of suitably-formatted data, turn on the motor, move the head to the desired track, turn on the write signal, write the pattern $92 $A4 about 500 times [if the buffer is much smaller than 4,000 bytes, it may be necessary to increase that count, so as to write a total of at least ~5,000 bytes], followed by $9F then output the contents of the buffer and turn off the write signal. Bytes must be sent to the drive controller precisely once every 32 clock cycles. Slipping by even one cycle will cause the controller to output garbage.

    2. To read a disk track, turn on the motor, move the head to the desired track, and read bytes of data from the disk until one sees a the byte sequence $92 $A4 $9F, and then read the rest of the data. Data will arrive at a rate of about 32 cycles/byte, and each byte must be read within a 7 cycle window.

    Strict requirements

    The data read back should precisely match the data written provided every byte in the buffer upholds three restrictions:

    1. Every byte must have the most significant bit set.
    2. No byte may contain more than two consecutive 0 bits.
    3. Every byte must contain at least one pair of consecutive 1 bits.

    There are 64 possible byte values that meet those criteria. Encoding arbitrary data to fit that limitation before storing it, and decoding information that is written in that fashion can be a nuisance, but that's part of the "fun" of writing one's own disk routines. Many disk routines read data into a buffer without decoding it, and then decode it later, but if one chooses a suitable encoding it's possible to decode information in real time as it's received from the disk.

    Details on reading from a controller in slot 6

    Code should generally use a two-instruction loop when waiting for data from a disk which rules out trying to incorporate a timeout in a read loop. But such a loop would have no way of exiting if a drive doesn't exist. The remedy is to check for a drive's existence before attempting to read data.

    1. To see if a drive is attached, read $C0EC a few times and see if the value changes. If not, no drive is attached.

    2. To read data from a drive that is known to exist, use a two-instruction loop to read $C0EC until the high bit becomes set.

      1. It's recommended that a four-cycle instruction be used for the read, and a two-cycle branch-not-taken once high bit became set (e.g. wait293: LDX $C0EC / BPL wait293).
      2. Note: We know an LDX/BPL loop will always terminate because, if a drive exists, it can go no more than about 60 cycles without yielding a byte of data no matter what patterns are stored on the disk. Timing may be imprecise, but the fact that some data patterns may result in code taking longer to timeout isn't really a problem.
    3. To ensure that one reads every byte, the CPU must execute at least 12 and at most 24 cycles before the next read. Taking less than 12 cycles may yield duplicate reads. Taking more than 24 may cause bytes to be skipped.

    4. If one is e.g. looking for a sector header, one can include up to 24 cycles of logic to check after each byte to see if too many bytes have been read without seeing a start-of-header byte.

    Details on writing data to slot 6

    1. To start writing data, write any value to $C0EF.
    2. Then write the first byte value to $C0ED and immediately read $C0EC (ignore the value read).
    3. One must then execute exactly 24 cycles of other code.
    4. Loop back to writing the next byte to $C0ED and reading $C0EC, etc..
    5. When done, read $C0EE.

    Selecting the drive, starting the motor, moving the head

    Of course, we can't neglect the ability to turn on the drive, select drive 1 or 2, and move the head.

    1. To turn on the drive motor, access $C0E9. To turn it off, access $C0E8. The effect of turning off the drive will be delayed by about a second.

    2. To switch to drive 2, access $C0EB. To switch to drive 1, access $C0EA.

    3. To move the head, think of it as being attached to a wheel which is attached to a hand on a clock face. The hand will point at 12:00 when the head is at any even numbered, track, and at 6:00 when it is on any odd numbered track. Reading $C0E1, $C0E3, $C0E5, or $C0E7 will turn on a coil that pulls the hand toward 12:00, 3:00, 6:00, or 9:00. Accessing the next lower address will turn off the coil. Move the head by turning on a coil 90 degrees from the wheel's current position, waiting awhile, turning that coil off and turning on the next one, etc.