Apple Disk Controller ROM
The disk hardware is controlled through 16 latched toggles mapped to
addresses $C0Z0 through $C0ZF
where Z is the slot number + 8.
This is achieved using an index addressing mode with SLOT16 (16 times the
slot number) in X: e.g. LDA $C089,X.
The first eight of these turn off or on different stepper phases.
$C0Z0 | Phase 0 OFF |
$C0Z1 | Phase 0 ON |
$C0Z2 | Phase 1 OFF |
$C0Z3 | Phase 1 ON |
$C0Z4 | Phase 2 OFF |
$C0Z5 | Phase 2 ON |
$C0Z6 | Phase 3 OFF |
$C0Z7 | Phase 3 ON |
To move the head inward, the phases are turned on and off in ascending order at a particular timing. To move the head outward, the same is done in descending order.
There are 70 positions the head can take but data written from adjacent positions can interfere with each other and so the usable positions must be separated. In practice, Apple DOS used the even positions (although one copy-protection tactic was to use odd positions or some other pattern that avoided adjacent positions).
The next eight toggles were as follows:
$C0Z8 | Motor OFF |
$C0Z9 | Motor ON |
$C0ZA | Select Drive 1 |
$C0ZB | Select Drive 2 |
$C0ZC | Strobe Data Latch |
$C0ZD | Load Data Latch |
$C0ZE | Ready Latch for Read |
$C0ZF | Ready Latch for Write |
We will cover these in more detail as we encounter them.
Seeking Track Zero
Rather than precisely move the head to track zero on boot, the disk controller just moves the head out by 80 positions. This guarantees it is in the outermost position regardless of its starting point. This is also what causes the distinctive thunk-thunk-thunk sound on disk boot as the head repeatedly hits a rubber stopper designed to prevent it moving too far.
At this point in the code, we’ve just calculated SLOT16 and it is still in
the accumulator. We transfer it to X:
Cs2E: AA TAX
We ready the latch for reading and strobe it.
Cs2F: BD 8E C0 LDA $C08E,X
Cs32: BD 8C C0 LDA $C08C,X
Then we select drive 1 and spin up the motor.
Cs35: BD 8A C0 LDA $C08A,X
Cs38: BD 89 C0 LDA $C089,X
We want to loop 80 times, so we put that in Y:
Cs3B: A0 50 LDY #80
We turn off the Phase 0 stepper motor. Remember that initially X is SLOT16.
Cs3D: BD 80 C0 LDA $C080,X
Note that on subsequent steps of the loop, X will be SLOT16 plus 2 × PHASE.
We’re now at the part of the code that will calculate this.
We take the loop index (in Y) modulo 4 (to get the phase) and
double it (to get the low four bits of the address to toggle).
Cs40: 98 TYA
Cs41: 29 03 AND #$03
Cs43: 0A ASL
When Y is 80, we get 0; when Y is 79, we get 6; when Y is 78,
we get 4; then 2; then back to 0 and so on. In other words, 2 × PHASE.
And because Y is decrementing, we are cycling through the phases in descending
order which means the head will move outwards.
SLOT16 is also in zero-page $2B so lets combine that with the 2 × PHASE.
Cs44: 05 2B ORA $2B
Cs46: AA TAX
Now X is SLOT16 + 2 × PHASE, which is exactly what we need to turn on
that stepper motor phase.
Cs47: BD 81 C0 LDA $C081,X
Now the timing is important. We need to wait about 20 milliseconds. There is
a MON_WAIT routine in the monitor ($FCA8) that consumes cycles as a quadratic
function of the accumulator (the number of cycles is 2.5A² + 13.5A + 7).
We set A to #$56 and call MON_WAIT which will consume a total of 19,664
cycles including the JSR.
Cs4A: A9 56 LDA #$56
Cs4C: 20 A8 FC JSR MON_WAIT
We now decrement the index and loop.
Cs4F: 88 DEY
Cs50: 10 EB BPL $Cs3D
Once we’ve moved the head out, we set up some more zero-page values.
Note that A is now #$00 because that’s what it ends up as after a MON_WAIT.
Cs52: 85 26 STA $26
Cs54: 85 3D STA $3D
Cs56: 85 41 STA $41
Cs58: A9 08 LDA #$08
Cs5A: 85 27 STA $27
So this puts #$0800 into $26/$27 (the location of the data better to load
the first sector into) and #$00 into $3D (the sector to load) and $41 (the
track to load).
Looking for Headers
With our drive head on the outermost track, we’re now going to read bytes until we see what’s known as the address header. We’ll then check we’ve got track 0 and sector 0 and, then look for the data header and load in the data.
Whether we’re looking for the address header or the subsequent data header is captured in the carry flag.
Because we want to find the address header first, we clear the carry and store the flags on the stack.
Cs5C: 18 CLC
Cs5D: 08 PHP
Next we strobe the latch until the high bit is on (which indicates data has been read).
Cs5E: BD 8C C0 LDA $C08C,X
Cs61: 10 FB BPL $Cs5E
Then we test whether it is #$D5 and, if not, loop back and read again.
Cs63: 49 D5 EOR #$D5
Cs65: D0 F7 BNE $Cs5E
It is important to note that the actual bytes read off disk into the latch are not the actual bytes we’re ultimately going to load into memory. The data is encoded to get around certain physical limitations in the hardware. We’ll shortly get into two types of encoding used.
Both the address header and data header start with $D5 so that’s what we
initially look for. It is not actually possible to get a $D5 in either of
the encodings used and so a $D5 can only mean the start of a header.
The next byte, whether an address header or data header, is always $AA.
We loop until we get the high bit set on the latch.
Cs67: BD 8C C0 LDA $C08C,X
Cs6A: 10 FB BPL $Cs67
Then we compare what we get with #$AA. Notice this time we use CMP not EOR.
That is because we want to keep the value in A (EOR is destructive).
If we didn’t get an $AA we jump back to see if we actually got another $D5.
Cs6C: C9 AA CMP #$AA
Cs6E: D0 F3 BNE $Cs63
Cs70: EA NOP
If we did get an $AA, we continue. It’s not clear why there is an NOP here.
We read the third byte
Cs71: BD 8C C0 LDA $C08C,X
Cs74: 10 FB BPL $C6s71
and compare it to #$96.
Cs76: C9 96 CMP #$96
Cs78: F0 09 BEQ $Cs83
If so, we have an address header D5 AA 96 and jump to $Cs83 to handle it.
Note that we do this even if we were looking for a data header because it means we’ve hit a new sector and need to check if it’s the right one.
But if we got a D5 AA that isn’t followed by a 96, what we do next does
depend on whether we’re looking for data and so we pull the status and if we
were actually looking for an address header (i.e. the carry flag is clear),
we'll start the whole process again.
Cs7A: 28 PLP
Cs7B: 90 DF BCC $Cs5C
But if we are looking for data, we check if we got a #$AD (because a data
header starts with D5 AA AD). If so, we branch to $CsA6 to handle the data.
Otherwise we go back and keep looking.
Cs7D: 49 AD EOR #$AD
Cs7F: F0 25 BEQ $CsA6
Cs81: D0 D9 BNE $Cs5C
It is worth noting here what each of D5, AA, 96, and AD look like in
binary:
D5 | 11010101 |
AA | 10101010 |
96 | 10010110 |
AD | 10101101 |
Reading An Address Header
If D5 AA 96 is read, we have an address header.
The payload of the address header is made up of three bytes: the volume, track, and sector, followed by a checksum byte.
But these aren’t stored as bytes. Instead they are each encoded using two bytes:
one containing the odd bits and one containing the even bits, with a 1 in
between.
In other words,
| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
is stored as:
1 |
D7 | 1 |
D5 | 1 |
D3 | 1 |
D1 | 1 |
D6 | 1 |
D4 | 1 |
D2 | 1 |
D0 |
To read the three components of the address header payload, we’re going to use
the Y register as our decrementing index. So we initially set it to 3.
Cs83: A0 03 LDY #$03
This is the start of our loop. We store whatever we have in the accumulator in
zero-page $40. This will initially be the 96 from the address header prologue.
But on subsequent steps will be the volume then track.
Cs85: 85 40 STA $40
We read the latch, looping until we get the high bit set.
Cs87: BD 8C C0 LDA $C08C,X
Cs8A: 10 FB BPL $Cs87
Remember this is our first byte. We rotate it to the left so
1 |
D7 | 1 |
D5 | 1 |
D3 | 1 |
D1 |
becomes
| D7 | 1 |
D5 | 1 |
D3 | 1 |
D1 | 1 |
and this is then stored in zero-page $3C.
Cs8C: 2A ROL
Cs8D: 85 3C STA $3C
We then read the second byte:
Cs8F: BD 8C C0 LDA $C08C,X
Cs92: 10 FB BPL $Cs8F
We then AND
| D7 | 1 |
D5 | 1 |
D3 | 1 |
D1 | 1 |
1 |
D6 | 1 |
D4 | 1 |
D2 | 1 |
D0 |
to get back
| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
Cs94: 25 3C AND $3C
Now we loop back until Y is zero:
Cs96: 88 DEY
Cs97: D0 EC BNE $Cs85
Once this loop exits, the accumulator will contain the sector from the header
and zero-page $40 will contain the track from the header (written at the
start of the final loop pass).
At this point, we still have the status register on the stack, so we pull it.
Cs99: 28 PLP
Going in to this whole routine, the zero-page $3D contains the desired sector
number and the zero-page $41 contains the desired track number.
So we first of all check if the header sector (in A) matches the desired sector.
If it doesn’t, we’ll start the whole read sector routine again and look for
another address header.
Cs9A: C5 3D CMP $3D
Cs9C: D0 BE BNE $Cs5C
But if we have the right sector, we next need to check we have the right track
by loading in the header track we got (in $40). If we don’t, we start over
looking for another address header.
Cs9E: A5 40 LDA $40
CsA0: C5 41 CMP $41
CsA2: D0 B8 BNE $Cs5C
After a successful CMP, the carry bit will be set and so BCS is a branch
always, in this case back to $Cs5D. And because the carry bit is set, this
will look next for the data header.
CsA4: B0 B7 BCS $Cs5D
Note that the volume, checksum, and the epilogue of the address header are all ignored.
Reading A Data Header
TO BE CONTINUED