  EMULATING THE APPLE HIGH SPEED SCSI CARD: AN EXERCISE IN DIGITAL ARCHAEOLOGY

                                by James Hammons

    ~~==< Brought to you in Glorious 80-Column Monospace-o-Vision(TM) >==~~


Motivations
-----------

While reading 4am's Twitter feed one day, he talked about his "Pitch Dark" hard 
drive image, which looked incredibly cool and like something that I would very 
much be interested in.  But in reading about it, I came across a seemingly 
throwaway line about how all decent emulators can run them, which, sadly, 
Apple2 could not at the time.  And so, in order to save Apple2 from indecency 
(and because I wanted to see if I could get 4am's "Pitch Dark" to work because 
it looked cool and interesting), I set about for finding some documentation on 
how hard drives interfaced to Apple IIs--and ran into a complete dearth of 
information.  There were little things sprinkled around here and there, but 
nothing of any deep, satisfying, technical significance.


In Order To Run A Hard Drive Image, You Must First Create The Universe
----------------------------------------------------------------------

While it's a nice bit of hyperbole, it's not exactly true that you have to 
first create the Universe, as fortunately, that part has largely been taken 
care of.  However, you still have to figure out how to emulate it if you are 
keen on running a hard drive image on your emulator of choice.  And in so 
doing, you have to figure what the requirements are; what the minimal pieces 
are that are required to have a functioning hard drive system; you also have to 
figure out how that system talks to the emulated computer.  And that all 
requires information.  I wasn't asking for much, but something along the lines 
of Jim Sather's "Understanding The Apple IIe" for hard drives would have been a 
nice thing to have.


The Next Part, In Which Nice Things To Have Are Not Forthcoming
---------------------------------------------------------------

Unfortunately, Jim Sather, and nobody else as far as I can tell, ever wrote 
such a document, and so I did what any lazy programmer would do: I took a look 
at some other project's source--in this case, AppleWin's source.  I didn't 
really *want* to look at it, having looked at it before and recoiled in horror 
at the sight, but, my search-fu apparently being not up to the task of finding 
relevant information drove me to it.  And looking at it didn't really provide 
any illumination; to me it looked like some kind of hacky thing and I wasn't 
interested in that kind of approach at all--so I abandoned the idea.  As I dug 
a little deeper into the minute literature that existed as such on the subject, 
I learned that pretty much any time you wanted to hook up a hard drive to your 
Apple II, you had to use an interface card, and typically that meant some kind 
of SCSI card.  And looking here, there was no shortage of SCSI cards that you 
could use to hook up your hard drive therewith.

So, that being a promising looking path to pursue on the road to this 
particular perdition, the question then became, which one should I choose?  At 
first I thought the RAMFast card would fit the bill as it seemed to be very 
popular, but there was literally no technical infomation on the thing.  The 
Apple SCSI card looked promising, but then I saw that it "ghosted" a slot, 
meaning that it would have to occupy two consecutive slots in order to work and 
I didn't much care for that.  And so, after looking at, and rejecting, card 
after card for pretty much the same reason, I settled on the Apple High Speed 
SCSI card for a few reasons--one, it was purportedly fast; two, it worked on 
the Apple IIe (as well as the IIgs, but I didn't really care that much about 
that to be honest); three, it had a users manual that wasn't completely devoid 
of technical information; four, it had a schematic; and five, it had a firmware 
image.  This looked like a promising start--how hard could it be to make this 
work?


Things Aren't Exactly Hard, But They Aren't Exactly Soft Either
---------------------------------------------------------------

One of the necessary things that I didn't have out of all of that was good 
information on how the thing worked.  I knew that it was a SCSI card, and I 
knew that it talked to the SCSI bus using an NCR 53C80 chip, but I had no idea 
exactly how.  But I did have something that *did* know how to talk to it: the 
firmware for the card.

Now when you take a look at the firmware, the first thing you notice is that 
it's 32K in size--which is *much* larger than the typical 256 bytes that you 
encounter when looking at Apple II card drivers.  It also happens to be quite a 
bit larger than the 2K "bonus" space that Apple II cards have available to them 
in the $C800 to $CFFF address space.  So what gives?

Fortunately for me, Apple2 has a built-in disassembler (which will probably 
stay in for all time, as it turns out to be a very useful thing to have on 
hand), and so I split that out into a stand-alone command line driven program, 
called d65c02, in order to be able to disassemble such things as device driver 
firmware blobs.  It isn't fancy, it doesn't do any analysis on what is code and 
what is data, but it gets the job done in turning incomprehensible binary 
gibberish (except to certain mad geniuses who will go heretofore unnamed) into 
human readable ASCII gibberish.  Thus I used said tool to disassemble the 
firmware blob.

Pulling up the results in my text editor, I could see that at least the front 
of the listing looked like it could plausibly be code that would go into the 
usual 256 byte card slot address space of $Cx00 to $CxFF, where x ranges from 1 
to 7 depending on the slot number.  Looking further, I could see this first 256 
bytes of code was repeated three times, meaning that this was a good candidate 
for the slot device code.  I could also see that it was written as relocatable 
code, and it contained this little tidbit:

001B: A9 60     LDA  #$60     ; Stuff an RTS into RAM somewhere
001D: 8D F8 07  STA  $07F8
0020: 20 F8 07  JSR  $07F8    ; Jump there and return in order to get evidence
                              ; of where in memory we did it from
0023: BA        TSX           ; Retrieve the stack pointer
0024: BD 00 01  LDA  $0100,X  ; Get the hi byte of the address we just pushed on
                              ; the stack in order to come back here
0027: 8D F8 07  STA  $07F8    ; & save it for later perusal

which meant that it was an excellent candidate for the slot device code.  But 
why should that be?


A Short Digression Into Why Slot Code Must Be Relocatable
---------------------------------------------------------

Slot code must be relocatable because such a card may be installed into any 
given slot in an Apple II--which means its code will show up anywhere from 
$C100 to $C700 (it always shows up on a page boundary).  By virtue of this, it 
also means that the I/O address for the card will also show up in the 
corresponding $C090 to $C0F0 address range (it always shows up on a 16-byte 
boundary).  And so, because of this, you have to write your slot code in such a 
way that it will work regardless of which slot it's installed in, which means 
the code must be relocatable--which ultimately means you can't use any JMP 
instructions to addresses in your driver, and you can't use absolute addressing 
to refer to stuff in the slot address space.

So, using the above code, a clever coder can figure out what slot their code is 
executing in and they can then use that knowledge to figure out which is the 
proper I/O range to use for the card.  All this being necessary in order to 
make a seamless experience for the end user of the card.


The Next Part, In Which 32K Is Still Larger Than 256
----------------------------------------------------

So, in looking at the code that comes after the Code Which Looks Like It 
Belongs In Slot Memory (which makes the wonderful acronym CWLLIBISM), I noticed 
that it seemed to be organized in 1K chunks.  And further persual of said 
chunks made it seem very likely that they resided in the $CC00 to $CFFF memory 
space.  However, the "extra" memory space given to cards to use starts 1K 
earlier--at $C800.  What could this mean?

Well, in looking at the schematic for the card, one not only finds the 32K ROM 
chip, but also an 8K static RAM.  Which means that it's very likely that the 
address space from $C800 to $CBFF is mapped to that 8K static RAM.  But 8K is 
larger than 1K; how does that work?

As it turns out, it's bank switched, but I didn't know it at the time--we'll 
get to that eventually.  In the meantime, with further perusal of the code (the 
code gets perused quite a bit), it seems very likely that the 1K address range 
from $C800 to $CBFF is said RAM as that range is written to by the 1K code 
chunks quite frequently.

Finding that the code in the firmware is divvied up into 1K chunks would seem 
to imply that it's bank switched into the $CC00 to $CFFF range.  And in looking 
at the CWLLIBISM, we see the following:

005C: A9 0B     LDA  #$0B     ; Get 11 in the accumulator
005E: AE 08 C8  LDX  $C808    ; Get offset to proper I/O space in X
0061: 5A        PHY           ; Save Y on the stack for later
0062: A8        TAY           ; Copy the accumulator to Y
0063: 29 1F     AND  #$1F     ; Strip off the upper three bits
0065: 9D 6E C0  STA  $C06E,X  ; & write to card I/O location $E

which implies it heavily.  Taking the number put into the accumulator and then 
masking out the lower 5 bits creates a range that goes from 0 to 31, which is 
32 distinct values, which corresponds to 32 1K chunks of code.

The above code, which is part of the initialization of the card, heavily 
implies that it's selecting a 1K chunk of code from bank 11 (counting from 
zero, naturally) to put into the $CC00 to $CFFF address range.  And so we get 
to(*) look there for a start.

(*) While changing 'have to' to 'get to' can make life awesome in many ways, 
this is far from a universal truth.  'Getting to' have one's arm amputated is 
never, ever awesome


The Next Part, In Which We Sadly Bid Adeiu To CWLLIBISM
-------------------------------------------------------

But before we do that, in order to understand what's going on in those wicked 
little 1K chunks of code, we should first take a closer look at CWLLIBISM.  So 
let's jump in:

0000: A2 20     LDX  #$20     ; The bytes after the LDX # identify this card as
0002: A2 00     LDX  #$00     ; being capable of SmartPort calls, and the $82 at
0004: A2 03     LDX  #$03     ; $FB further identifies it as a SCSI card ($2)
0006: A2 00     LDX  #$00     ; that supports extended calls ($8).

The way that I was able to find out that this seemingly useless bit of code was 
a way of identifying SmartPort capable cards was in the serendipitous find of 
the "Technical Manual for the Apple SCSI Card"(*), which, while helpful in some 
ways, was almost completely useless in trying to figure out the what the card 
I/O addresses did.

(*) No relation to the Apple High Speed SCSI Card

0008: 2C 58 FF  BIT  $FF58    ; Check byte in ROM (usually, an RTS lives here)
000B: 70 05     BVS  $0012    ; Bit 6 set?  >> $12 (which means, this branch
                              ; will be taken...)

This little tidbit checks a ROM location that usually carries an RTS (at least 
it does in the Apple IIe), which is $60.  Which means that the following BVS 
will always be taken and skip over the following:

000D: 38        SEC           ; ProDOS entry point
000E: B0 01     BCS  $0011    ; Branch over the following CLC
0010: 18        CLC           ; SmartPort DISPATCH
0011: B8        CLV           ; Signal we're doing normal I/O, not init code

So this clever little bit here, according to the "Technical Manual for the 
Apple SCSI Card", sets some flags so that later on in the firmware, it can 
discern whether it's being called from ProDOS (in which the carry flag will be 
set) or if it's a SmartPort call (in which the carry flag will be clear).  
Either way, the overflow flag is cleared to let the firmware know that this is 
a request to talk to the drive, and not initialization.  Initialization skips 
over this code and ends up here:

0012: D8        CLD           ; Clear the decimal flag, to prevent bad math
0013: 08        PHP           ; Save the carry & overflow flags for later
0014: 78        SEI           ; Turn IRQs off
0015: AD FF CF  LDA  $CFFF    ; Turn INTC8ROM off (puts card in $C800-CFFF)
0018: 8D 00 CC  STA  $CC00    ; ???

This bit of code is a bit of housekeeping; making sure the decimal flag isn't 
set so that ADC & SBC both work as expected, saving the flags register so that 
the firmware code later can determine whether it's an initialization call or a 
regular I/O call, making sure that IRQs don't happen while in the firmware 
code, and turning on the "extra" addresses in the $C800 to $CFFF range.

The store to $CC00 is mysterious, as it's a ROM location and stores to ROM 
locations are usually void and of null effect.  This likely means that it's 
some kind of soft-switch that controls something in card, but exactly what 
would require a few things that I don't have, namely: the contents of the two 
PALs on the card (which sit between the address lines of the slot and the rest 
of the card), and a description of what the ports on the Sandwich II do (the 
chip that sits between the Apple IIe proper and the NCR 53C80).  So, moving 
right along:

001B: A9 60     LDA  #$60     ; See where we're executing from
001D: 8D F8 07  STA  $07F8
0020: 20 F8 07  JSR  $07F8
0023: BA        TSX
0024: BD 00 01  LDA  $0100,X  ; Get the address we just pushed on the stack
0027: 8D F8 07  STA  $07F8    ; Save it

We've seen this already, this is the code that determines which slot it's 
sitting in.  Say, for example, that it's sitting in slot 7; the byte that it 
will retrieve from the stack will be $C7 (for the sake of completeness, the lo 
byte will be $22--as to why, this is left as an exercise for the reader).  In 
order to turn that into something that it can use to hit the proper slot I/O 
addresses, it does the following:

002A: 29 0F     AND  #$0F     ; Get the lo nybble
002C: 0A        ASL  A        ; Multiply it x16
002D: 0A        ASL  A
002E: 0A        ASL  A
002F: 0A        ASL  A
0030: 18        CLC
0031: 69 20     ADC  #$20     ; Add $20 to it for some reason
0033: AA        TAX           ; & stick in the X register

The important part of the $C7 hi byte of the address we found through 
cleverness and trickery is the slot number, which will always fall in the lower 
4 bits.  And, in order to be useful to find the correct slot I/O address range, 
that slot number needs to be multiplied by 16, as each of the slot I/O address 
ranges cover exactly sixteen bytes.  Note that masking off the bottom 4 bits, 
as is done with the AND #$0F instruction, is unnecessary as the four ASL A 
instructions after it will necessarily shift the top four bits out of the 
picture.

The one thing that stands out as not typical of this kind of device driver code 
is the adding of $20 to the index.  Typically, writers of this kind of I/O code 
will use $C080 to $C08F (plus the contents of the X register to reach the 
correct slot I/O range) as the base address for slot I/O, but, for some reason, 
the writers of this card's firmware chose to use $C060 to $C06F, thus 
necessitating the addition of $20 to the value in the X register to reach the 
correct range for slot I/O.

0034: A9 00     LDA  #$00     ;
0036: 9D 6E C0  STA  $C06E,X  ; Select bank #0 (register $E, lower 5 bits)
0039: A9 0F     LDA  #$0F
003B: 9D 6F C0  STA  $C06F,X  ; Store a $F in register $F
003E: 8E 08 C8  STX  $C808    ; Put slot # at $C808 (banked RAM in $C800-CBFF)
0041: 9C 09 C8  STZ  $C809    ; Put zero at $C809
0044: 9C F2 C8  STZ  $C8F2    ; & $C8F2

One thing I forgot to mention is that the Apple High Speed SCSI card is only 
usable by enhanced Apple IIe and IIgs machines, and that's because it relies on 
instructions only found in the 65C02 like STZ and PHY; a regular 6502 will not 
even remotely do the same things that those instructions do on the 65C02--so 
they're right out.

At any rate, the above code does some writing to the slot I/O address range and 
sets up some values in the card's static RAM, including saving the contents of 
the X register for later.

0047: A2 22     LDX  #$22     ; Transfer 35 bytes from ZP ($40) to $C82D
0049: B5 40     LDA  $40,X
004B: 9D 2D C8  STA  $C82D,X
004E: CA        DEX
004F: 10 F8     BPL  $0049

This bit of code transfers 35 bytes in page zero RAM to the card's static RAM, 
presumably to restore them later.

0051: AD F8 07  LDA  $07F8    ; Get original $Cx byte again
0054: 8D 01 C8  STA  $C801    ; Put it in $C801
0057: A9 61     LDA  #$61     ;
0059: 8D 00 C8  STA  $C800    ; Put $61 in $C800 (= $Cx61)
005C: A9 0B     LDA  #$0B
005E: AE 08 C8  LDX  $C808    ; Get X from $C808

This little bit of code sets up for the code that comes below; it sets up 
locations $C800-1 as a location for an indirect jump that seems to happen a lot 
in the 1K chunks that come later.  The address it sets up as the jump target is 
the code that comes next:

0061: 5A        PHY           ; Save Y (follow on bank, passed in by caller)
0062: A8        TAY           ; Save A register
0063: 29 1F     AND  #$1F     ; Mask off the lower 5 bits
0065: 9D 6E C0  STA  $C06E,X  ; First time, select bank 11:0 (I/O register $E)
0068: 98        TYA           ; Restore the A register
0069: 29 E0     AND  #$E0     ; Mask off the upper 3 bits
006B: 4A        LSR  A        ; & shift them down
006C: 4A        LSR  A
006D: 4A        LSR  A
006E: 4A        LSR  A
006F: A8        TAY           ; Use as an index into a table (Y x 2)

What this does is save the Y register on the stack, then separates the 
accumulator into a upper 3-bit part and a lower 5-bit part.  The lower 5 bits 
go into I/O slot register $E, which presumably selects which 1K chunk of code 
will appear in the $CC00 to $CFFF address range while the upper 3 bits are used 
as an index into a table that appears near the end of each 1K chunk:

0070: B9 F0 CF  LDA  $CFF0,Y  ; Get address of current 1K bank
0073: 85 54     STA  $54      ; & stuff it into $54/55
0075: B9 F1 CF  LDA  $CFF1,Y
0078: 85 55     STA  $55

So it uses the Y register as index into the current selected bank's $CFF0 
address range and stuffs them into $54 and $55, so that it can jump to the 
address at some point.

007A: AD F8 07  LDA  $07F8    ; Get original $Cx byte again
007D: A8        TAY           ; Put it in Y
007E: 48        PHA           ; Put it to the stack
007F: A9 86     LDA  #$86
0081: 48        PHA           ; Push $86: return address is now $Cx87

What this does is set up the stack for what I'm going to name (for lack of a 
better term, or any at all to be honest) an "RTS call".  This takes advantage 
of how the CPU uses the stack to return execution to the instruction after a 
JSR instruction: when the CPU encounters a JSR opcode, it pushes the the 
location of the program counter, plus two, onto the stack before loading the 
program counter with the address that comes after the JSR.  When an RTS opcode 
is then encountered, it restores the program counter from the stack and adds 
one to it before resuming execution.

The upshot of this is that you can transfer execution of a program from one 
place to the next, without using JMP, JSR or branch instructions by simulating 
this behavior--which also turns out to be a necessity when you're writing 
relocatable code.  So what the above code does is set up the stack so that it 
will jump to location $Cx87 when it encounters an RTS.

0082: 5A        PHY           ; Push $Cx
0083: A9 8B     LDA  #$8B     ; Push $8B: return address is now $Cx8C
0085: 48        PHA

Similarly, this code sets up the stack so it will jump to $Cx8C when it 
encounters an RTS as well.  So it will go there first, then to $Cx87 second 
when the routine first called via RTS call, er, uh, returns.

0086: 60        RTS           ; First time, will "return" to $Cx8C

Thus, this first RTS transfers control to the JMP ($0054) down below, which was 
set up above as an address somewhere in a 1K code chunk.  Since the code that 
goes into the 1K code chunk is a JMP instruction, once that code returns, it 
will then find the address that was pushed on the stack earlier, and execute 
the following code:

0087: 68        PLA           ; After the $CCxx block is done, it comes here
0088: 9D 6E C0  STA  $C06E,X  ; Restore last block (one passed in Y reg)
008B: 60        RTS           ; & return to calling code in that block

This code pops the Y register that was saved way back up at location $Cx61 and 
uses it to set the I/O register at $E, which, presumably, is the bank switch 
I/O address for the card.  This will turn out to be of vital importance later, 
but we'll leave it for now.  The RTS, finally, returns from initialization and 
back from whence it came.

008C: 6C 54 00  JMP  ($0054)  ; Jump to the $CCxx block code

This indirect JMP instruction, called up above via RTS call, kicks things off.

008F-00FA: 00                 ; $6B worth of zeroes
00FB: 82 00 00 BF 0D          ; ID/offset bytes

So these bytes that look like a bit of detritus actually do serve a useful 
function in ProDOS.  The $0D at the very end serves as an offset from the 
beginning of the code to the ProDOS entry point, which in this case works out 
to $Cx0D.  It also serves as the entry point for SmartPort calls (by adding 3 
to it), which works out to $Cx10.

Further, the "Technical Manual for the Apple SCSI Card" says the following 
about the byte at $FB: "An additional byte, at $CnFB, should contain $82, 
indicating that the device is the SCSI card ($2) and that it supports extended 
calls ($8)."  This just happens to be one of a small handful of those 
aforementioned tiny bits of useful information that I was able to glean from 
that source.

And so, at last, we come to the realization that this is definitely the slot 
ROM code, and thus CWLLIBISM becomes CWSISM (Code Which Sits In Slot Memory).


And Now For Something Not Quite So Completely Different
-------------------------------------------------------

And with that digression into CWSISM, we turn our attention back to the 1K 
chunk of initialization code that sits in bank 11.  In looking at the table 
that we discovered sits at $CFF0, we find the following in the 11th (counting 
from zero) 1K chunk:

CFF0: 00 CC
CFF2: 91 CE
CFF4: 9A CD
CFF6: 00 00 00 00 00 00 00 00 00 00

This tells us that there are only three valid addresses in the table (as the 
zeroes will take you nowhere), and that further, they are $CC00, $CE91 and 
$CD9A.  And since the CWSISM set up the $Cx61 dispatch call with $0B (at 
$Cx5C), it will pick the zeroeth address in that list, namely, $CC00.  So, 
looking at the code that lies there, what we see looks promising:

CC00: 68        PLA          ; Discard the 2nd return path (bank switch back)
CC01: 68        PLA
CC02: 68        PLA          ; Discard the follow on bank #, as there is none

Since this is initialization code, we can discard the RTS call from the stack 
since we aren't calling this code from another bank.  Which also means that we 
can discard that parameter which tells the RTS call what bank to select before 
returning.

CC03: 86 5E     STX  $5E     ; Save slot # (+$20) in $5E
CC05: 9C 93 C8  STZ  $C893   ; Zero out $C893 & $5D
CC08: 64 5D     STZ  $5D
CC0A: 20 C1 CC  JSR  $CCC1   ; Test for GS hardware + DMA switch

This is basically housekeeping, and the routine called at $CCC1 tests if the 
card is running on an Apple IIgs and sets bit 6 of zero page location $5D if it 
detects that.  It also checks the physical DMA on/off switch on the card as 
well; if it's set, it sets bit 5 of $5D.  The following bit of code checks $5D 
to see if bit 6 is clear and skips the instructions at $CC11 to $CC19 if 
so--and since I'm emulating an Enhanced Apple IIe, it *will* skip those 
instructions:

CC0D: 24 5D     BIT  $5D     ; Check if bit 6 of $5D is set (means it's a GS)
CC0F: 50 0B     BVC  $CC1C   ; Skip over if not set (it's not a IIgs)
CC11: AD 36 C0  LDA  $C036   ; IIgs Speed Reg.
CC14: 8D 96 C8  STA  $C896   ; Save it for later...
CC17: 09 80     ORA  #$80    ; Set speed to 2.8 MHz
CC19: 8D 36 C0  STA  $C036   ; & modify

Luckily there exists a very good techinical reference manual for the Apple 
IIgs; unluckily, it's a bit hard to track down.  But once you do, the 
information in it is quite good.  The above bit of code shows that the card 
firmware shifts the IIgs into high gear while running on the card.  However, we 
don't really care about that bit of code; which is why we spent so much time 
explaining what it does.

CC1C: 68        PLA          ; Get flags from slot init

Way back in CWSISM, at slot location $Cx13, there was an innocuous looking PHP 
instuction; here is where we finally take a look at the contents of it.

CC1D: A8        TAY          ; Save them in Y
CC1E: 29 04     AND  #$04    ; Check if I flag is set
CC20: F0 05     BEQ  $CC27   ; Skip if I is not set
CC22: A9 80     LDA  #$80    ; Else, signal I flag is set ($80 -> $C893)
CC24: 8D 93 C8  STA  $C893

Here we look at the interrupt disable bit in the processor flags that we saved 
earlier; if it's not set we skip on over to the next bit of code below.  
Otherwise, the code sets $80 into memory location $C983 to signal that 
initialization code was called with the I flag set.

CC27: 98        TYA          ; Restore flags from Y
CC28: 09 04     ORA  #$04    ; Set I flag
CC2A: 48        PHA          ; Push them to the stack
CC2B: 28        PLP          ; & restore flags for real

Since we need to get the values of the overflow and carry flags back, which 
were set way back in CWSISM at addresses $Cx0D through $Cx11, we have to 
retrieve them from the Y register, then push them onto the stack and then use a 
PLP to get them back into the flags register proper.  Along the way, we set the 
interrupt disable flag at $CC28 (the ORA #$04 instruction).

And in looking at code as we're doing here, it's hard not to look at it with a 
critical eye and notice that the coder could have saved a byte by deleting the 
ORA #$04 (which takes two bytes) and putting an SEI after the PLP (which takes 
one byte).  And, since we don't have any source code to look at, we may never 
know what the intention was; though it's quite likely that this was just a 
simple oversight.

CC2C: 50 09     BVC  $CC37   ; If SmartPort call, skip over

Here we see that if the card firmware was called via the SmartPort vector at 
$Cx10, the overflow flag would be clear and we would skip over the following.  
But, since the flag was definitely set, we know that we will execute what 
follows:

CC2E: BA        TSX          ; Slot init & regular ProDOS dispatch get here
CC2F: 8E 07 C8  STX  $C807   ; Save stack pointer in $C807
CC32: A9 0F     LDA  #$0F
CC34: 4C 5F CF  JMP  $CF5F   ; Jump to bank 15:0 for rest of init

This saves the stack pointer and sets up to jump to a new bank, which means we 
won't be coming back here.  Onward:

CF5F: A6 5E     LDX  $5E     ; Restore slot # (+$20) in X
CF61: A0 0B     LDY  #$0B    ; Y gets loaded with bank to return to on RTS
CF63: 6C 00 C8  JMP  ($C800) ; & go!

There are variants of this piece of code throughout every 1K bank of firmware 
code.  And since we took a good long look at CWSISM, we know that CWSISM set up 
location $C800 and $C801 to point to the card slot I/O location of $Cx61, and 
suddenly it becomes clear what that bit of code does.

Since the firmware code bounces around a lot in different banks (as we will 
discover shortly), it needs a mechanism to get back to the place that called it 
in the first place.  The problem is this: once a new 1K bank of code is 
switched into the $CC00 to $CFFF address space, there's no way for the 65C02 to 
get back to the caller with a simple RTS; any code that attempted to do so 
would end up executing the wrong code as the 65C02 knows nothing about bank 
switching and has no built-in mechanism to handle such things.

And so, by virtue of this, the code needs a way to do this manually.  Which is 
why the $Cx61 code in CWSISM saves the bank number on stack, and then sets up a 
pair of RTS calls which first, sets the correct bank and calls the correct 
function number in that bank and second, sets the bank to the bank that made 
the call in the first place before executing a final RTS which then goes back 
to the correct address.

And since we saw up above that it passed $0F into the calling routine (well, 
actually, it jumped there), we know that it's going to call function #0 in bank 
15.  As it turns out, the function table for bank 15 looks like this:

CFF0: 00 CC
CFF2: 00 00 00 00 00 00 00 00 00 00 00 00 00 00

which means bank 15 only contains one function, and it starts at $CC00.


The Next Part, In Which We Peruse Bank 15
-----------------------------------------

The story so far: we started in slot ROM, set up a bunch of variables, then 
bounced to bank 11, and just now bounced to bank 15.

CC00: A9 40     LDA  #$40
CC02: 8D 09 C8  STA  $C809   ; Put $40 into $C809
CC05: 8D 32 BF  STA  $BF32   ; & $BF32(!)
CC08: 9C 0A C8  STZ  $C80A   ; Zero out $C80A

So far this is all normal housekeeping boilerplate, though putting the value 
$40 into RAM at address $BF32 makes me raise an eyebrow (to this day, I still 
have no idea what that's supposed to do).  So then we come to the heart of the 
matter:

CC0B: A9 03     LDA  #$03
CC0D: 20 AF CF  JSR  $CFAF   ; Call bank 3:0 (enumerate all connected drives)

Here is the first proper JSR into bank switched code, and in taking a cursory 
glance at the code there, well...  It's a bit of a Gordian knot.  So we'll 
ignore the stones in the field for now, and keep on plowing ahead:

CC10: AE 08 C8  LDX  $C808   ; Restore slot # (+$20) to X
CC13: A5 4F     LDA  $4F
CC15: F0 03     BEQ  $CC1A   ; Skip over if call was successful ($4F == 0)
CC17: 4C F0 CC  JMP  $CCF0   ; Else, do a LDA #2B, JMP $CFAF to bank 11:1

So here the code retrieves the slot I/O offset in X from the location set way 
back in CWSISM, then checks what looks like some kind of error condition.  If 
it fails, it skips on over to function 1 in bank 11; otherwise, it keeps going 
here:

CC1A: 24 5D     BIT  $5D     ; Are we running on a IIgs?
CC1C: 70 05     BVS  $CC23   ; If so, skip over & keep going

Since we're not running on a IIgs, this branch is not taken and thus it can be 
safely ignored.  Continuing on:

CC1E: A9 4B     LDA  #$4B    ; Else, jump to bank 11:2 (normal success path)
CC20: 4C AF CF  JMP  $CFAF
;
CFAF: A6 5E     LDX  $5E     ; Restore slot (+$20) in X
CFB1: A0 0F     LDY  #$0F    ; Make sure we come back here...
CFB3: 6C 00 C8  JMP  ($C800) ; & go!!

So what this means is that if the function call to bank 3:0 succeeded, the code 
will then bounce to function 2 in bank 11.  And, as we saw above, function 2 
starts at $CD9A in bank 11.


The Next Part, In Which Be Bounce Back To Bank 11 And Find Something Familiar
-----------------------------------------------------------------------------

So far, this little expedition is proving to be circuituitous, but not 
impenetrable.  And it makes sense that we would come back to bank 11, as that's 
where the initialization code sent us in the first place.  And so, pressing on, 
we find:

CD9A: 86 5E     STX  $5E     ; Save X in $5E
CD9C: A9 01     LDA  #$01    ; Put 1 in $43, $44
CD9E: 85 43     STA  $43
CDA0: 85 44     STA  $44
CDA2: 64 46     STZ  $46     ; Zero out $46, $47, $48, $49
CDA4: 64 47     STZ  $47
CDA6: 64 48     STZ  $48
CDA8: 64 49     STZ  $49
CDAA: A9 08     LDA  #$08    ; Put $08 in $41
CDAC: 85 41     STA  $41
CDAE: 64 40     STZ  $40     ; Zero out $40, $42
CDB0: 64 42     STZ  $42

This is again more housekeeping boilerplate, initializing a bunch of zero page 
locations.  Then we find this:

CDB2: A9 09     LDA  #$09
CDB4: 20 5F CF  JSR  $CF5F   ; Call bank 9:0 (directly)

So this calls function 0 in bank 9, which lives at $CC00.  And looking through 
that code, well, let's just put that aside for now as it's long and involved 
and will require a fair amount of study.  Continuing:

CDB7: A5 4F     LDA  $4F
CDB9: D0 0C     BNE  $CDC7   ; Fail if $4F is non-zero

This looks at the error flag we saw up above in bank 15, and jumps to function 
1 in this bank if the error flag is non-zero.

CDBB: AD 01 08  LDA  $0801   ; Get byte @ $801 (!)
CDBE: F0 07     BEQ  $CDC7   ; Fail if it's zero

Now here is something interesting!  Why this is interesting is because when 
booting from a floppy disk, the disk driver typically loads at least one sector 
(256 bytes of data) into location $800.  So we can deduce that the above call 
into function 0 in bank 9 is loading something similar from the hard drive into 
memory at a similar address.  With this bit of knowledge, we can see up above 
where it puts address $800 into zero page locations $40 and $41 that those 
locations must be a loading address.

CDC0: AD 00 08  LDA  $0800   ; Get byte @ $800 (!)
CDC3: C9 01     CMP  #$01
CDC5: F0 03     BEQ  $CDCA   ; Keep going if it's equal to 1
CDC7: 4C 91 CE  JMP  $CE91   ; Else, jump to function 1 (failure point)

Again, this interesting because with floppy disks, the first byte of the first 
sector loaded into memory at $800 contains the number of sectors that the 
floppy driver should load into memory; this looks eerily similar--only in this 
case, it will jump to the failure path if it sees it wanting more than one 
block.  Assuming all is well, we then have this:

CDCA: 8D 09 C8  STA  $C809   ; Put a 1 into $C809
CDCD: AD F8 07  LDA  $07F8   ; Get $7F8
CDD0: 0A        ASL  A       ; x16
CDD1: 0A        ASL  A
CDD2: 0A        ASL  A
CDD3: 0A        ASL  A
CDD4: AA        TAX          ; Store it in X
CDD5: A9 00     LDA  #$00    ; Stuff 0 in $C035 (GS location?)
CDD7: 8D 35 C0  STA  $C035
CDDA: 8D 01 CC  STA  $CC01   ; What does this do?
CDDD: 4C 01 08  JMP  $0801   ; Run the code from block 0

And here we see it hand off execution to data that it pulled from the hard 
drive by jumping to $801, and thus we see that this must be the end of the hard 
drive boot logic.  As far as the firmware is concerned, its initialization job 
of bootstrapping the hard drive is concluded.

However, we still really don't know anything that tells us what the slot I/O 
addresses do (aside from location $E) and we still have no idea how the card 
talks to the hard drive.  At least we have a pretty good idea of where to look.


What Are All These Eels, And What Are They Doing In My Hovercraft
-----------------------------------------------------------------

So at last we get to take a look at function 0 in bank 3.  And, much like a 
hovercraft full of eels, it's a twisty mass of slippery, squirming code.  And, 
looking at it more closely, it does a bunch of things which don't make much 
sense until you understand other code, which bounces around to lots of other 
banks.  And a lot of it is opaque unless you somewhat understand what the ports 
on the NCR 53C80 do and how the SCSI protocol works.

So while we have an excellent start on understanding, for the most part, the 
broad outlines of how the card works, we are still stuck with a profound lack 
of critical knowledge on how the thing talks to the the hard drive and, 
conversely, how the hard drive talks to the card.  And without that knowledge, 
we perish.


The Next Part, In Which We Are Not Ready To Perish
--------------------------------------------------

Fortunately, the NCR 5380 and, by extension, the 53C80 is well documented and 
said documentation is readily available, and so I availed myself of it.  I took 
another look at the schematic for the card and noticed that the 53C80 had three 
address lines on it, which implied that it had eight ports for controlling it.  
Unfortunately, there's an error on the schematic in which they have the address 
lines hooked up in reverse, and this caused me no small amount of consternation.

It seemed obvious that those eight ports were hooked up to the slot I/O 
addresses, and also seemed very plausible, after having looked at and analyzed 
a lot of code heretofore unmentioned, that it was connected to the lower half 
of that address space.  So, in order to confirm my suspicions, I started 
writing the hard drive emulator.

This started out, simply, as a bunch of statements that output human readable 
words to a log file whenever the slot I/O addresses were accessed by the card 
firmware; I used the firmware's access to the slot I/O to tell me what it said 
and what it was listening for.  Well, that, and some code to properly handle 
the bank selection of the ROM space as well.  In this way, I was able to 
enlarge my understanding of what the card expected to see as well as what the 
ports that weren't connected to the 53C80 (which were likely connected to the 
Sandwich II) might be up to.

So in fits and starts, I used the code that writes to the Mode Register of the 
53C80 to get the code to successfully... do something.  It was at that point I 
could see that it was getting through the initialization phase of the card's 
firmware as Apple2 would be able to boot a floppy image inserted into a drive 
in slot 6 at that point.  But in tracing the reads and writes to the slot I/O 
address space in the log I could see that it was getting through the card's 
firmware in a failure mode.  It was progress, of a sort.  Even failure tells 
you something.

And what it told me was that I needed to dig into the SCSI specification to 
figure out how the protocol worked.  Looking back I can see that I was getting 
through to the MESSAGE phase and, because of the way I was responding to that 
message, that the firmware would then send an ABORT message, but that's all 
pretty much meaningless as I haven't explained anything about the SCSI protocol 
and how it works.

And here, while there is a lot of information about the latter day iterations 
of the SCSI protocol, there wasn't much pertaining to the kind of SCSI that the 
Apple High Speed SCSI card spoke, which in its case, has been retroactively 
labeled SCSI-1.

And when looking at the SCSI protocol, the first thing that hits you is that 
it's a very well designed, robust protocol and it's nothing short of a minor 
miracle that it survived and still survives to this day.  However, the 
documentation on how it *really* works is a bit lacking.  Yes, you can discover 
that there are nine phases, and the first three are fairly easy to understand; 
it's what comes after that where things get murky.


Talk SCSI To Me
---------------

So here is a crash course in the SCSI-1 protocol.  The SCSI bus is engineered 
such that it allows for eight devices to connect to said bus; devices connected 
to the bus can have Initiator and/or Target roles.  Devices can talk to each 
other by passing messages over this bus, however only one pair of devices can 
use the bus at any one time.  In order to prevent deadlock from happening when 
more than one device attempts to take control of the bus, there is an enforced 
hierarchy of devices wherein they all have a unique ID; a device that contends 
for use of the bus at the same time as another device wins this contention if 
and only if its device ID is higher than the other device's ID (1 in this case 
being the highest, and 128 being the lowest).  The bus is an 8-bit parallel 
data bus that is controlled by a variety of signals (and these are typically 
called "lines").

In contending for and utilizing the bus, there are nine phases that all SCSI 
devices must understand and negotiate.  They are as follows:

 -  Bus Free
 -  Arbitration
 -  Selection
 -  Message In
 -  Message Out
 -  Data In
 -  Data Out
 -  Command
 -  Status

In the Bus Free phase, as one might expect, no devices are using the bus.  This 
is the ground state of the SCSI protocol, the phase from whence all 
communication starts and where it all ends.  Any device that wishes to talk to 
another device on the bus must start here.

Once a device sees that the bus is free, it can enter the Arbitration phase as 
an Initiator; it does so by first setting the bit that corresponds to its 
device ID on the data bus.  If another device tries to do this at the same 
time, the device with the lower ID will remove its bit from the data bus and 
try again when it detects that the bus is free again.  When the Initiator has 
waited a certain amount of time with no other contention, it then asserts the 
SEL line and goes into the Selection phase.

In the Selection phase, the Initiator sets the bit that corresponds to the 
device ID it wants to talk to (the Target) on the data bus.  Every other device 
on the bus, by virtue of the asserted SEL line, knows it's in the Selection 
phase and can see the device ID bits being asserted on the data bus; if none of 
the bits match its own ID, it will stay silent.  If the Target device doesn't 
respond in a timely manner, the device that tried "calling" it drops the bits 
it asserted on the data bus and drops the SEL line.  Otherwise, if the Target 
device sees its ID on the data bus, it responds by asserting the BSY (BuSY) 
line.

The device that started all of this (the Initiator) then drops the SEL line and 
the Initiator and Target devices then enter the next phase.  What phase that is 
took some teasing out of lots of different papers, datasheets and manuals--as 
well as much trial and error in the emulation code.  And what I found was this: 
once the devices are in the Selection phase, they typically(*) dance through 
the following set of phases, in order, before being done with their 
transaction: Message Out(**), Command, Data In/Out, Status, Message In.

(*) One exception to this is the TEST UNIT READY command, which will skip the 
Data In/Out phase

(**) Note that the qualifiers "In" and "Out" come strictly from the perspective 
of the Initiator

Once the devices have successfully negotiated the Message In phase at the end 
of their phase dance, the Target device drops the BSY line and the bus is then 
free again for another transaction.

One thing I forgot to mention is that each phase transition, once the devices 
are in the Selection phase, is punctuated by a REQ/ACK handshake.  Typically, 
the Target asserts and drops the REQ line while the Initiator asserts and drops 
the ACK line.  Basically, when the Target is ready to move to a different 
phase, it will assert the REQ line; the Initiator will see this and then assert 
the ACK line.  Once the Target sees the ACK line asserted, it will drop the REQ 
line; the Initiator, seeing this, will then drop the ACK line.  And thus hands 
are shaken, and all are in agreement as to where they are and what they are 
doing.

One interesting consequence of this kind of handshaking is that it means that 
every phase past Arbitration is driven by the Target device.


By Your Command
---------------

And so having deciphered the proper steps in the post-Selection phase dance, we 
come as last to the heart of the matter: the Command phase.  Commands come in a 
few different flavors: the six byte, the ten byte and the twelve byte.  The 
flavor is given by the top three bits of first byte while the command itself is 
given by the bottom five bits.  Treating those top three bits as a number from 
zero to seven, the flavors fall into the following groups:

six byte: 0
ten byte: 1, 2
twelve byte: 5

Yes, 3, 4, 6 and 7 are all missing, and, for the purposes of this crash course, 
can be safely ignored(*).

(*) For the terminally curious, 3 and 4 are (were?) "reserved", and 6 and 7 are 
for "vendor specific" commands

Having now discerned their form, the question arises: just what do these 
commands do?  Basically, they tell the Target what the Initiator wants from it.
For example, let's say that the Initiator wants to know if a device on the bus 
is ready to receive commands.  It would send out, during the Command phase, a 
TEST UNIT READY command which has the following form:

00 00 00 00 00 00

Assuming the device receiving this command actually is ready to receive 
commands, it would then send back a status message (in the Message In phase 
following the Status phase) saying "Good" (which, in this case, is coded as 
$00).

Other commands follow basically the same form; only instead of going directly 
to the Status phase, as the TEST UNIT READY command does, it will go into 
either the Data In or Data Out phase before going to the Status 
phase--depending on what the command does.  For example, a READ command will go 
to the Data In phase, because the Initiator is requesting data from the Target; 
likewise, a WRITE command will go to the Data Out phase because the Initiator 
wants to send data to the Target.


Back To Our Regularly Scheduled Analysis
----------------------------------------

So, before we diverged into a crash course of the SCSI-1 protocol, we were 
looking at where I had been able to have the card's firmware return back to the 
Apple IIe's Autostart program, but in a failure mode.  Which, while ultimately 
unsatisfying, *was* a step in the right direction.

So I could see that with my hard-coded responses to the firmware's inquiries, I 
was getting an IDENTIFY message ($80) followed by an ABORT message ($06).  It 
was a this point I could also see that I was going to have to start writing the 
actual hard drive device emulator code as well, as trying to keep track of all 
the phase changes in the slot I/O register code was turning into an 
impenetrable mess and wasn't going to be fruitful in the long run.

This also necessitated a closer look at the code for function 0 in bank 3.  I 
took copious notes on where the code went and what it did, and eventually found 
that almost everything, at some point, seemed to end up calling function 0 in 
bank 16.


All Roads Lead To Bank 16:0
---------------------------

The one thing I was trying to figure out from this code was: what was the 
failure mode that would get you out cleanly?  Because in order for the code 
that called here to work properly, it would have to have some kind of clean 
failure mode to indicate that there was no drive present at this device ID; 
also in my first attempts to get the firmware code to successfully run (for 
some value of "successfully" > 0), it would hang up somewhere in this code.  
And that meant, since I didn't understand the SCSI chip, that I would have to 
understand the SCSI chip and how it worked to have any hope of untangling the 
tangled mass of code here.

So before we take a quick look at that, let's take a look at the top level code 
that lives at function 0, bank 16.  At first glance, it doesn't look all that 
bad:

CC00: 8D 00 CD  STA  $CD00   ; Write to $CD00 (what does it do?)
CC03: 20 D0 CD  JSR  $CDD0   ; Clear DMA bit (1) from reg. $2, init some stuff
CC06: 20 CE CE  JSR  $CECE   ; Check if reg. $4 has 0, 2 (/SEL) or 4 (/I/O)
CC09: B0 16     BCS  $CC21   ; If failure, skip over

This is pretty straightforward stuff; the routine at $CECE will set the carry 
flag if slot I/O register $4 is not exactly one of: 0, 2, or 4.  If the carry 
is set, it bypasses the following sections of code:

CC0B: 20 42 CF  JSR  $CF42   ; Check if bit 7 in $C893 is set (success == yes)
CC0E: 20 24 CC  JSR  $CC24   ; Do Arbitration phase
CC11: B0 03     BCS  $CC16   ; If Arbitration timed out, jump over Selection

It wasn't obvious when I first encountered this code, but, once I delved into 
the SCSI protocol I was able to figure out that the code at $CC24 was 
negotiating the Arbitration phase.

CC13: 20 7A CC  JSR  $CC7A   ; Do Selection phase

Likewise, it was not obvious that the code at $CC7A was negotiating the 
Selection phase--but I was able to figure out that the code could cleanly exit 
this bank (in a failure mode, naturally) if the BSY line was not asserted.

CC16: 20 58 CF  JSR  $CF58   ; Check if bit 7 in $C893 is set (success = yes)
CC19: B0 06     BCS  $CC21   ; Skip over if it failed

Since the address at $C893 got loaded with $80 way back in function 0 in bank 
11, the carry flag will be clear and we will execute the following:

CC1B: 20 E4 CC  JSR  $CCE4   ; Do SCSI communication with target
CC1E: 20 A0 CD  JSR  $CDA0   ; Do nothing if $C88F is nonzero, else check on
                             ; $C8EC

The code at $CCE4 was quite mystifying for some time, even after I had educated 
myself on the intricacies of the SCSI protocol and the ins and outs of the NCR 
53C80's ports.  I wasn't able to make sense of this until I was able to 
understand the phases after Selection and how they were expected to be 
negotiated.

CC21: 4C 18 CE  JMP  $CE18   ; Do some post cleanup before returning

The code at $CE18 basically does some error checking and cleanup before 
returning back to whence it came; it's fairly easy to digest.  But before we 
dig into subroutines of bank 16:0, we need to take a short digression into how 
the ports of the 53C80 work.


A Somewhat Brief Digression Into The 53C80's Ports
--------------------------------------------------

And so, having avoided looking into the 53C80 and how it works up until this 
point, we find we can no longer avoid it and thus, finally bite the bullet.  
The 53C80 has eight ports (also called registers) with which the Apple IIe's 
CPU can communicate.  They are:

$0 - Data on the SCSI bus
$1 - Initiator Command
$2 - Mode
$3 - Target Command
$4 - Current SCSI Bus Status (R), Select Enable (W)
$5 - Bus and Status (R), Start DMA Send (W)
$6 - Input Data (R), Start DMA Target Receive (W)
$7 - Reset Parity/Interrupt (R), Start DMA Initiator Receive (W)

Note too that there is a one-to-one correspondence with the port numbers as 
they appear on the 53C80 and their location in the slot I/O address range.  
What follows is an explanation of what the registers do:

Register $0 is pretty much what it says it is; data on the SCSI bus will appear 
here barring this caveat: it only works when bit 0 of register $1 (ASSERT DATA 
BUS) is set.  Which bring us to...

Register $1 is used to monitor and assert signals on the SCSI bus.  The bits 
are:

7    6              5             4    3    2    1    0
RST  AIP/TEST MODE  LA/DIFF ENBL  ACK  BSY  SEL  ATN  DATA BUS

RST (ReSeT) sets the RST signal on the SCSI bus and resets the internal state 
of the 53C80; it stays in the reset state until this bit is cleared.  AIP/TEST 
MODE (Arbitration In Progress) is a bit that is split between two functions: 
when read, it signals whether or not the Arbitration phase is in progress; when 
a one is written to it, it disables all output from the chip (zero restores 
output).  LA/DIFF ENABL (Lost Arbitration) is another split signal: when read, 
it signals whether or not Arbitration was lost; writing has no effect.  ACK 
(ACKnowledge) sets or clears the ACK line, BSY (BuSY), SEL (SELect), ATN 
(ATteNtion) and DATA BUS all do the same.

The important thing to note here is that by setting the ATN line on the SCSI 
bus, the initiator signals to the Target that it wants to send a message and 
so, at the appropriate time, the Target will then assert the MSG and C/D lines 
in response.

Register $2 controls various modes of the 53C80, as well as whether or not 
certain interrupts will be triggered.  The bits are:

7      6       5         4          3           2        1     0
BLOCK  TARGET  ENABLE    ENABLE     ENABLE EOP  MONITOR  DMA   ARBITRATE
MODE   MODE    PARITY    PARITY     INTERRUPT   BUSY     MODE
DMA            CHECKING  INTERRUPT

The only two of real interest are bits 1 (DMA MODE) and 0 (ARBITRATE); the 
former sets the chip into DMA mode, readying it for a DMA transfer while the 
latter tells the chip to start the Arbitration phase.

Register $3 is used mainly if the chip is operating in Target mode, as all the 
lines controlled by it are typically only controllable by the Target device.  
The only exception is when the Initiator is sending data to the Target; in that 
case, bits 0, 1 and 2 must match the lines being asserted by the Target.  The 
bits are (where X means unused):

7               6  5  4  3    2    1    0
LAST BYTE SENT  X  X  X  REQ  MSG  C/D  I/O

Register $4 is another split register.  When read, it returns the state of the 
following lines on the SCSI bus:

7    6    5    4    3    2    1    0
RST  BSY  REQ  MSG  C/D  I/O  SEL  DBP

When written to, it enables an interrupt to occur if the device ID written to 
the SCSI bus is present, BSY is clear and SEL is set.

The important thing about this register is that it allows monitoring of the 
MSG, C/D and I/O lines of the SCSI bus.  These three bits are what the Target 
uses to signal moves from phase to phase; without these three bits it would be 
impossible, as an initiator, to figure out what to do once in the Selection 
phase.

And with three bits, you would expect there to be eight phases controlled here, 
but only six are controlled from these signals--having MSG set to 1 while C/D 
is set to 0 is an illegal combination, and that knocks two of the combinations 
right out of contention.  Each legal combination corresponds to a phase, and 
this is, as it turns out, vital information:

Data Out:  MSG = 0, C/D = 0, I/O = 0 (0)
Data In: MSG = 0, C/D = 0, I/O = 1 (1)
Command: MSG = 0, C/D = 1, I/O = 0 (2)
Status: MSG = 0, C/D = 1, I/O = 1 (3)
Message Out: MSG = 1, C/D = 1, I/O = 0 (6)
Message In: MSG = 1, C/D = 1, I/O = 1 (7)

Note that there's nothing magical about the order of these three lines; they 
could be in any order whatsoever and they would still work the same way.  The 
only reason that they are presented this way is one, this is how they are laid 
out in the NCR 53C80 chip (in this register in particular) and two, this is 
order that they are used in the firmware.

Register $5 is--you guessed it--another split register.  When read, it returns 
some internal state registers as well as a couple more SCSI bus lines:

7       6        5        4       3      2      1    0
END OF  DMA      PARITY   IRQ     PHASE  BUSY   ATN  ACK
DMA     REQUEST  ERROR    ACTIVE  MATCH  ERROR

When written to, it initiates a DMA send transfer from memory to the SCSI bus.

Register $6, another split register, when read, holds data coming from the SCSI 
bus during a DMA transfer.  When written to, it initiates a DMA receive 
transfer from the SCSI bus (the Target) to memory.

And finally, register $7 is yet another split register, that when read, resets 
the internal PARITY ERROR, IRQ ACTIVE and BUSY ERROR bits in register $5; when 
written to in initiates a DMA receive transfer from the SCSI bus (the 
Initiator) to memory.


Back To Bank 16
---------------

So, with that info-dump out of the way, let's return back to the first 
subroutine of the initial code of bank 16:0.  We start with the routine at 
$CC24:

CC24: 9E 63 C0  STZ  $C063,X ; Zero reg $3 (Target Command)
CC27: 20 2F CF  JSR  $CF2F   ; Toggle bit 7 of reg. $E (ON-off-ON)
CC2A: AD DA C8  LDA  $C8DA   ; Get SCSI ID of initiator device
CC2D: 9D 60 C0  STA  $C060,X ; & put it in reg. $0 (Output Data)
;
CC30: 9E 62 C0  STZ  $C062,X ; Zero out reg. $2 (Mode)
CC33: A9 01     LDA  #$01
CC35: 9D 62 C0  STA  $C062,X ; Set bit 0 (ARBITRATE) of reg. $2

This code zeroes out the Target Command register, then toggles bit 7 of 
register $E on, then off, then back on.  It then puts the SCSI ID of the 
initiator device into the SCSI Data Bus register, then clears and sets the 
ARBITRATE bit of the Mode register.  This is the start of the Arbitrate phase.

CC38: BD 6C C0  LDA  $C06C,X ; Get reg. $C
CC3B: 89 10     BIT  #$10    ; Check bit 4
CC3D: D0 05     BNE  $CC44   ; Skip over this if it's set
CC3F: 20 0C CF  JSR  $CF0C   ; Toggle bit 7 of register $E ON-off-ON
                             ; # of times before C is set is in $C817/8
CC42: B0 2E     BCS  $CC72   ; Signal failure is C is set

There is a lot of this code and variants thereof sprinkled liberally throughout 
the firmware code.  I'm still not sure what bit 4 of register $C is a signal 
for, but it seems clear that it indicates some kind of error condition because 
whenever it's not set, it toggles bit 7 of register $E and will eventually, 
when this has happened enough times, signal an error and exit.

CC44: 3C 61 C0  BIT  $C061,X ; Check bit 6 (AIP) of reg. $1
CC47: 50 E7     BVC  $CC30   ; Try again if it's not set

This little bit of code checks the AIP (Arbitration In Progress) bit, and loops 
back to try again if it's not set.

CC49: EA        NOP          ; Do a small delay
CC4A: EA        NOP
CC4B: A9 20     LDA  #$20
CC4D: 3D 61 C0  AND  $C061,X ; Check if bit 5 (LA) of reg. $1 is set
CC50: D0 DE     BNE  $CC30   ; Try again if it's set

After checking to see if the AIP bit is set, it then waits a short amount of 
time before checking to see if the LA (Lost Arbitration) bit is set; if it's 
set, it loops back to try again.

CC52: BD 60 C0  LDA  $C060,X ; Get reg. $0
CC55: 4D DA C8  EOR  $C8DA   ; EOR it with what we put there to begin with
CC58: F0 05     BEQ  $CC5F   ; If it's the same, bypass (we won arbitration)
CC5A: CD DA C8  CMP  $C8DA   ; Otherwise, see if the EORed value is >= orig
CC5D: B0 D1     BCS  $CC30   ; Try again if so

Here we look at the data on the SCSI bus and see if there were any other 
devices attempting to arbitrate at the same time.  If there were, and their 
SCSI ID was higher than ours, then loop back and try again; otherwise, we won 
arbitration and continue on:

CC5F: A9 20     LDA  #$20
CC61: 3D 61 C0  AND  $C061,X ; Check if bit 5 (LA) of reg. $1 is set
CC64: D0 CA     BNE  $CC30   ; Try again if so

We check the LA bit one more time to ensure it's not set; if it is, then loop 
back and try again.

CC66: A9 06     LDA  #$06    ; Set bits 1-2 (ASSERT /ATN, /SEL) of reg. $1
CC68: 1D 61 C0  ORA  $C061,X
CC6B: 29 9F     AND  #$9F    ; And clear bits 5-6 (TEST MODE, DIFF ENBL) of $1
CC6D: 9D 61 C0  STA  $C061,X
CC70: 18        CLC          ; Signal success
CC71: 60        RTS          ; & return

Now that we've won the Arbitration phase, we assert the ATN and SEL lines and 
make sure that the TEST MODE and DIFF ENBL lines are dropped.  By setting the 
ATN line, we signal to the Target that we want to go to the Message Out phase 
after the Selection phase is done.  Once that's done, we signal success and 
return.

CC72: A9 80     LDA  #$80
CC74: 8D 8F C8  STA  $C88F
CC77: 4C 91 CD  JMP  $CD91   ; Signal failure

This bit is called if the code that checks register $C fails; this is the only 
failure path for the Arbitration phase code.


A Fine SELECTion Of Devices
---------------------------

Now that the Initiator (us) has won the Arbitration phase, it's time to see if 
the device we want to talk to exists, and is ready and able to talk.

CC7A: 9E 64 C0  STZ  $C064,X ; Zero out reg. $4 (Select Enable)
CC7D: AD DA C8  LDA  $C8DA   ; Host ID
CC80: 0D DB C8  ORA  $C8DB   ; Target ID
CC83: 9D 60 C0  STA  $C060,X ; Store $C8DA & DB (ORed) into reg. $0 (Data Bus)
CC86: A9 41     LDA  #$41    ; Set bits 0 (DATA BUS) & 6 (TEST MODE) in reg. $1
CC88: 1D 61 C0  ORA  $C061,X ; Then clear bits 5-6 (DIFF ENBL, TEST MODE) in $1
CC8B: 29 9F     AND  #$9F
CC8D: 9D 61 C0  STA  $C061,X

The code here clears the Select Enable register to ensure no IRQs are generated 
during the Select phase, then puts both the Initiator's SCSI ID and the 
Target's SCSI ID into the 53C80's data register.  It then does something that 
doesn't seem to make any sense, as it sets the DATA BUS ENABLE and TEST MODE 
bits.  The former puts the 53C80's data register onto the SCSI data bus, while 
the latter disables all outputs of the 53C80.  Maybe this was necessary because 
of the Sandwich II chip and the way it was hooked up to the slot I/O bus and 
the 53C80, but there's no way to know for sure without access to actual 
hardware.

After this, it disables the TEST MODE bit, which then enables the outputs of 
the 53C80, and thus the Target's SCSI ID is then visible to all the devices 
connected to the SCSI bus.

CC90: A9 FE     LDA  #$FE    ; Clear bit 0 (ARBITRATE) in reg. $2
CC92: 3D 62 C0  AND  $C062,X
CC95: 9D 62 C0  STA  $C062,X
CC98: A9 02     LDA  #$02    ; Set bit 1 (DMA MODE) in reg. $2
CC9A: 1D 61 C0  ORA  $C061,X
CC9D: 9D 61 C0  STA  $C061,X
CCA0: AD DC C8  LDA  $C8DC   ; Get $C8DC, set hi bit, save in $C821
CCA3: 09 80     ORA  #$80
CCA5: 8D 21 C8  STA  $C821
CCA8: A9 F7     LDA  #$F7    ; Clear bit 3 (ASSERT /BSY) in reg. $1
CCAA: 3D 61 C0  AND  $C061,X
CCAD: 9D 61 C0  STA  $C061,X

This is all pretty straightforward stuff.  It clears the ARBITRATE bit, sets 
the DMA MODE bit, and clears BSY (if it was set before; more likely than not, 
it will have been cleared already).  It also sets bit 7 of $C8DC and saves it 
in $C821, but it's not clear just why yet.

CCB0: 20 51 CD  JSR  $CD51   ; Wait for bit 6 (/BSY) of reg. $4 to be set
CCB3: 90 03     BCC  $CCB8   ; Skip over JSR if success
CCB5: 20 75 CD  JSR  $CD75   ; Shorter wait for bit 6 in reg. $4 to be set

This bit of code waits for the Target to assert the BSY line; if it fails after 
the first attempt, it will try again with a shorter wait time.

CCB8: A9 FB     LDA  #$FB    ; Clear bit 2 (ASSERT /SEL) in reg. $1
CCBA: 3D 61 C0  AND  $C061,X
CCBD: 9D 61 C0  STA  $C061,X
CCC0: 90 10     BCC  $CCD2   ; Skip over if the JSR was successful

This code drops the SEL line, and depending on whether or not the Target 
asserted the BSY line, will either drop through to the failure path or skip 
over to the success path.

CCC2: A9 FE     LDA  #$FE    ; Clear bit 0 (DATA BUS) in reg. $1
CCC4: 3D 61 C0  AND  $C061,X
CCC7: 9D 61 C0  STA  $C061,X
CCCA: A9 81     LDA  #$81    ; Put $81 in $C88F
CCCC: 8D 8F C8  STA  $C88F
CCCF: 4C 91 CD  JMP  $CD91   ; Signal failure

This is the only failure path in the Selection phase code, but, unlike the 
Arbitration phase code, this code path will *not* lock up waiting for signals.  
It will wait only so long for the Target to assert the BSY line before giving 
up and signalling failure.  It will also bail out of this bank completely, so 
it will not try any further communication--for now.

CCD2: A9 9D     LDA  #$9D    ; Clear bits 1, 5-6 (TEST, DIFF E., DMA) in $1
CCD4: 3D 61 C0  AND  $C061,X
CCD7: 9D 61 C0  STA  $C061,X
CCDA: A9 FE     LDA  #$FE    ; Then clear bit 0 (DATA BUS) in $1
CCDC: 3D 61 C0  AND  $C061,X
CCDF: 9D 61 C0  STA  $C061,X
CCE2: 18        CLC          ; Signal success
CCE3: 60        RTS          ; & return

Otherwise, the code clears TEST MODE, DIFF ENBL and DMA MODE before clearing 
DATA BUS, signalling success and returning.


The Next Part, In Which We Find Ourselves In A Maze Of Twisty Code
------------------------------------------------------------------

Now that we've successfully navigated the Selection phase, it's time to talk 
SCSI.  For the sake of brevity, we will refer to this code as The Code That 
Comes After Selection, or TCTCAS for short.  This bit of code calls a bunch of 
other code which in turns calls even more code; keeping it all straight was 
quite the challenge.

CCE4: BD 6C C0  LDA  $C06C,X ; Get $C
CCE7: 89 10     BIT  #$10    ; Is bit 4 set?
CCE9: D0 05     BNE  $CCF0   ; Skip ahead if so
CCEB: 20 0C CF  JSR  $CF0C   ; Else, toggle bit 7 of $E (ON-off-ON) w/countdown
CCEE: B0 40     BCS  $CD30   ; Exit if countdown hit zero

Here again we see the boilerplate checking of bit 4 of register $C.

CCF0: BD 64 C0  LDA  $C064,X ; Get reg. $4
CCF3: 29 42     AND  #$42    ; Are bits 1 (/SEL) & 6 (/BSY) clear?
CCF5: F0 3A     BEQ  $CD31   ; If so, we're done (jump down, signal error)

Here we're checking the BSY and SEL lines; if both have been dropped after the 
last phase, we jump down to $CD31 and do some final checking before exiting.

CCF7: C9 40     CMP  #$40    ; Is only bit 6 (/BSY) set?
CCF9: D0 E9     BNE  $CCE4   ; Loop back if not...

The second check looks to see if only BSY is set; if not it loops back to the 
start of this subroutine, otherwise it continues on:

CCFB: BD 62 C0  LDA  $C062,X ; Clear bit 1 (DMA MODE) of reg. $2
CCFE: A8        TAY
CCFF: 29 FD     AND  #$FD
CD01: 9D 62 C0  STA  $C062,X
CD04: 98        TYA          ; Then restore its previous state
CD05: 1D 62 C0  ORA  $C062,X
CD08: 9D 62 C0  STA  $C062,X

This little bit of code toggles DMA MODE line off then on if it was set to 
begin with, otherwise it does nothing.  Well, it doesn't *do* nothing, but the 
effect is null and void.

CD0B: BD 64 C0  LDA  $C064,X ; Is bit 5 (/REQ) of reg. $4 clear?
CD0E: A8        TAY
CD0F: 29 20     AND  #$20
CD11: F0 D1     BEQ  $CCE4   ; Loop back if so...

This checks to see if the REQ line has been asserted by the target yet, and if 
not, loop back to the beginning of the subroutine.

CD13: AD 1F C8  LDA  $C81F   ; Save $C81F in $C820 (last 3-bit pattern we saw)
CD16: 8D 20 C8  STA  $C820

Here we save the last phase that was seen in $C820.

CD19: 98        TYA          ; Restore reg. $4 from Y
CD1A: 29 1C     AND  #$1C    ; Keep only bits 2-4 (/I/O, /C/D, /MSG)
CD1C: 8D 1F C8  STA  $C81F   ; & save in $C81F

Earlier we saved the contents of register $4 (which holds the MSG, C/D and I/O 
bits) in the Y register, now we retrieve them and mask off the MSG, C/D and I/O 
bits and save them for later.  By virtue of this, every time we get here the 
previous value that was in $C81F must be different than the last value we saw 
here.

As to why: when I first encountered this code, I approached it the way I 
usually approach unknown code: by feeding it zeroes.  However, when I did that, 
these lines of code caused a failure mode later on.  And so I had to dig a 
little deeper into all things SCSI and 53C80 to figure out why--we'll see why 
that caused a failure later on.

CD1F: 4A        LSR  A
CD20: 8D 2B C8  STA  $C82B   ; & put /2 in $C82B

Here we shift it right one bit and stuff it into $C82B; this is also a clever 
way of making it into an index for a jump table.

CD23: A8        TAY          ; & use as index into jump table
CD24: 4A        LSR  A       ; & /2 again
CD25: 9D 63 C0  STA  $C063,X ; Write it to reg. $3 (Target Command)

Here we put it into the Y register and then shift it to the right one more time 
to set the bits in the Target Command register properly.  The Initiator needs 
to set this register properly at each phase change, otherwise the 53C80 will 
signal a phase match error.

CD28: 20 48 CD  JSR  $CD48   ; Use Y as idx to jump table and go there

So here the code uses the three phase bits (MSG, C/D and I/O) as an index into 
a jump table to handle the six phases after the Selection phase (Data Out, Data 
In, Command, Status, Message Out, Message In).  We'll have more to say about 
this shortly.

CD2B: 2C 06 C8  BIT  $C806   ; Is bit 7 of $C806 clear?
CD2E: 10 B4     BPL  $CCE4   ; Loop back if so...
CD30: 60        RTS

This simply checks bit 7 of $C806, which only gets set under very specific 
circumstances; those being that MSG, C/D and I/O are all asserted (Message In 
phase), and that the value returned from the Target is a "Good" message, and 
that the prior phase was either Message In, Message Out, or Status.

CD31: AD 8F C8  LDA  $C88F   ; Get $C88F
CD34: D0 08     BNE  $CD3E   ; If $C88F is != 0, just return
CD36: A9 82     LDA  #$82    ; Stuff $82 into $C88F
CD38: 8D 8F C8  STA  $C88F
CD3B: 4C 91 CD  JMP  $CD91   ; Signal failure (?) & return
CD3E: 80 F0     BRA  $CD30

This is the code path taken if the BSY and SEL lines are dropped.  It signals 
that something went wrong before returning.


The Next Part, In Which Things Start To Make Sense
--------------------------------------------------

So TCTCAS is, as it turns out, where the Target drives the Initiator; which in 
this case is the hard drive driving the card.  As I mentioned up above, when I 
first started poking around at this code, I was feeding it zeroes at first as a 
place to start seeing if I could get it to do something meaningful.  However, 
when you try that, you run into the following bit of code which says, "No, 
fuggetaboutit."

CEE5: AD 1F C8  LDA  $C81F   ; Get the current MSG, C/D, I/O values
CEE8: CD 20 C8  CMP  $C820   ; Compare it to the previous values
CEEB: D0 05     BNE  $CEF2   ; If they're different, skip over
CEED: A9 27     LDA  #$27    ; (This is ignored by the jump target)
CEEF: 4C 6C CE  JMP  $CE6C   ; Else, do a soft, then a hard reset of the card
CEF2: ...

And so, after looking over the SCSI documentation for the umpteenth time, I 
realized that what it was saying is that you can't do a Data Out phase directly 
after the Selection phase; it has to be Something Else. And this is because 
$C81F gets initialized with zero (which corresponds to the Data Out 
phase)--which means starting with zero Won't Work.

As luck would have it, however, we know that in the Selection phase, it 
asserted the ATN line, which in turn tells the Target to assert the MSG and C/D 
lines (but not I/O).  Which means that we *know* that the Target will first go 
to the Message Out phase, every time.

And so, by writing the hard drive emulator to properly respond to the MSG, C/D 
and I/O lines I got it to handshake the Message Out phase properly.  But I 
could see that after that, it wasn't exiting; it was running through another 
round of seeing what was in MSG, C/D and I/O and running the appropriate 
handler.

Now I was a bit stuck here, as there was *no* documentation on how a Target 
device, such as a hard drive, would drive the handshaking for the Initiator 
device.  And it wasn't clear what phase the firmware was expecting to come 
next, so guessing wasn't likely to yield positive results.

So, by the serendipitous luck of the Search Engine gods, I stumbled upon a page 
which looked like a scan of a book mixed with some bespoke images made by 
someone whose primary language was not English.  One of the images, which had 
misaligned text set next to it, was, however, suggestive.  It showed a sequence 
of phases that went from Bus Free to Arbitration to Selection to Message Out to 
Command to Data In to Status to Message In to Bus Free.  This was the first 
time I had seen anything like this; in all of the SCSI literature that I had 
surveyed, there was nothing beyond the vaguest hints that there was a typical 
order to the phases.  Sure, they would say that one *could* go from one phase 
to another, and how the handshaking worked, but there was *nothing* saying that 
there was a definite order to the phases that should be observed.

So, as I said, this image was highly suggestive.  Could this be the key to the 
whole thing that I was missing?

I had set things up in the hard drive emulation to go to the Message Out phase 
after the Selection phase, and so I added code to go to the Command phase after 
that.  I could see that the firmware was sending something in the Command phase 
at this point, which was the following six bytes: 00 00 00 00 00 00.  And 
looking that up in the SCSI literature showed that to be the TEST UNIT READY 
command.  But the firmware was still looking for more.

From what I saw in the logs, it didn't look like it was going for a Data In 
phase next, so I set it up to go to the Status phase, and that got things going 
a little bit further.  To me, this looked like it should be the end of the 
dance, but the firmware was *still* looking for more.

But even though a byte was sent from the Target to the Initiator during the 
Status phase, it seemed that the Status reponse was actually sent in the 
Message In phase.  Once I had coded this into the hard drive emulation, I could 
see the TEST UNIT READY command going into TCTCAS and coming out of it in a 
non-failure mode.

The dance has steps, and they must be followed in order.


Dancing In The Dark
-------------------

However, something is still not quite right; my assumption--that all the 
firmware needed to do to see if there was a drive on the bus was to probe 
through to the Selection phase and then, if anything responded, to see if it 
successfully responded to the TEST UNIT READY command--turned out to be wrong.  
How wrong?  Let's take a look back at the code in bank 3:0 which attempts to 
enumerate all devices it can see on the SCSI bus:

CC55: A0 07     LDY  #$07
CC57: 8C 73 C8  STY  $C873   ; Save Y in $C873
CC5A: 9C DC C8  STZ  $C8DC   ; Zero out $C8DC
CC5D: B9 F4 CF  LDA  $CFF4,Y ; Get SCSI ID from table into A
CC60: CD DA C8  CMP  $C8DA   ; Compare it to our SCSI ID (default is $01)
CC63: F0 1F     BEQ  $CC84   ; Skip over if it's equal (don't query our SCSI ID)

So here it's looping through all eight SCSI IDs, starting with the lowest 
priority and working its way up to the highest (for reference, the table at 
$CFF4 has the following values: $01, $02, $04, $08, $10, $20, $40, $80).  It 
compares the SCSI ID from the table to the SCSI ID of the card, and skips over 
the following code (down to $CC84) if it's the same.

CC65: 8D DB C8  STA  $C8DB   ; Else, put SCSI ID to look at in $C8DB
CC68: 64 4F     STZ  $4F     ; Zero out $4F (error flag)
CC6A: 20 5F CF  JSR  $CF5F   ; Do TEST UNIT READY (calls bank 16:0)

This is the code that I was now able to successfully navigate with my hard 
drive emulation.  It emulated exactly one SCSI ID, and that one ID returned 
here successfully (every other ID, obviously with nothing connected to the bus, 
returned failure).  However, I could see from the log file that it was trying 
to issue some more commands--which was puzzling, but told me that I needed to 
dig even deeper into the code.

CC6D: A5 4F     LDA  $4F     ; Get error code
CC6F: D0 0F     BNE  $CC80   ; Skip over if error occurred

This is fairly straightforward; it checks the error code returned from the call 
we made to bank 16:0, and if it's anything but zero, skip over the following 
code:

CC71: EE 0D C8  INC  $C80D   ; Success means add one to $C80D (# of devices)
CC74: 20 9F CC  JSR  $CC9F   ; & call Function 1 in this bank (INQUIRY + MORE)
CC77: 90 0B     BCC  $CC84   ; Check next ID if C == 0

So here we increment a counter, which we suppose to be a count of the number of 
valid devices we have found on the SCSI bus.  And here, we come to the 
realization that it isn't just hard drives that can talk to the Apple High 
Speed SCSI card, it's also printers, scanners, tape drives and whatnot.  And 
so, it makes perfect sense that TEST UNIT READY is only the first step in 
discovering if a device is a hard drive or not because here, it calls function 
1 of bank 3 (the bank we're currently in) which is what issues more commands to 
figure out what the device it's talking to actually *is*.

CC79: A9 99     LDA  #$99    ; Else, stuff $99 into $C887
CC7B: 8D 87 C8  STA  $C887
CC7E: 80 17     BRA  $CC97   ; & signal success

So if the call to $CC9F (INQUIRY + MORE) returned with the carry flag set, it 
stuffs a magic number into $C887, signals success and returns.

CC80: C9 80     CMP  #$80    ; Was error $80?
CC82: F0 16     BEQ  $CC9A   ; Signal NoDrive error if so

This is where it lands if the TEST UNIT READY call returned a non-zero result 
in the "error code" memory location. if it equals $80, it puts the ProDOS error 
code for a "NoDrive" error into the error code and returns.

CC84: AC 73 C8  LDY  $C873   ; Restore Y
CC87: 88        DEY          ; Done looking at all IDs?
CC88: 10 CD     BPL  $CC57   ; Go back if not.

Here we decrement the counter and loop back if we haven't looked at all eight 
(except for the card's) SCSI IDs.  Otherwise, we've finished, and fall through 
to the following:

CC8A: A9 77     LDA  #$77    ; Else, stuff $77 into $C80A & $C887
CC8C: 8D 0A C8  STA  $C80A
CC8F: 8D 87 C8  STA  $C887
CC92: AD 0D C8  LDA  $C80D   ; Did we find any devices?
CC95: F0 03     BEQ  $CC9A   ; Signal NoDrive if not
CC97: 64 4F     STZ  $4F     ; Else, signal success
CC99: 60        RTS          ; & return

So here it stuffs the magic number $77 into $C887 and $C80A; it also checks the 
"number of devices found" memory location, and signals a "NoDrive" error if the 
count is equal to zero.

CC9A: A9 28     LDA  #$28    ; Return $28 (NoDrive) in $4F
CC9C: 85 4F     STA  $4F
CC9E: 60        RTS

This is the landing location for the various failure modes seen up above; it 
simply puts the ProDOS "NoDrive" error into the error flag and returns.

So now I get to figure out what the commands are in that call to 3:1 that are 
causing the card to return in a failure mode.


The Test Is Easy, When You Have The Answer Key
----------------------------------------------

At this point, even though I had the hard drive emulation doing a proper dance 
through the TEST UNIT COMMAND, it was in a very crude state and couldn't really 
do anything else.  And so I had to take a closer look at the seemingly 
impenetrable code that set up a bunch of memory locations before calling bank 
16:0 to see if I could make sense of it.

Rather than go through every last one, I will go through part of the first such 
piece of code, as it's instructive:

CD0E: 20 A4 CF  JSR  $CFA4   ; Set $60/1 to $C923, $56/7 to $C92F
CD11: 20 B9 CF  JSR  $CFB9   ; Put $C9C3 into $C92F/30, zero $C931
CD14: A9 12     LDA  #$12    ; Put $12 into $C923
CD16: 8D 23 C9  STA  $C923
CD19: 9C 24 C9  STZ  $C924   ; Zero out $C924-6, $C928
CD1C: 9C 25 C9  STZ  $C925
CD1F: 9C 26 C9  STZ  $C926
CD22: 9C 28 C9  STZ  $C928
CD25: A9 1E     LDA  #$1E    ; Put $1E in $C927, $C933 (length of reply, 30)
CD27: 8D 27 C9  STA  $C927
CD2A: 8D 33 C9  STA  $C933

So we can see right off the bat that it's setting up zero page locations $60 
and $61 to point to memory at $C923, and that it sets up six bytes at that 
location with the following:

C923: 12 00 00 00 1E 00

Reaching back to our crash course on SCSI commands, we can see by the first 
byte, since the top three bits are all zero, that this must be a six-byte 
command.  And after that, uh, well, we don't really know much of anything.  So 
after digging around some more for something even remotely relevant, I found a 
document dealing with SCSI-2 and SCSI-3 hard disk interfacing--which told me, 
first of all, that $12 was the INQUIRY command, and second, that the fifth byte 
in the command was the length of the message that the Initiator was expecting 
back from the target in response to this command.  Progress!

CD2D: 20 CB CF  JSR  $CFCB   ; Call bank 16:0 (Do INQUIRY command)
CD30: A5 4F     LDA  $4F
CD32: F0 05     BEQ  $CD39   ; Skip over if no error

And this, as we now know, does the phase to phase dance from start to finish, 
and checks the resulting error code to do any necessary error handling.  But 
what of the response?  How do we know what to say from our emulated hard disk 
back to the firmware?  The hard disk interface document had something that 
looked plausible, if overlong (it seems that latter day SCSI drives are 
expected to return 148 bytes instead of 30).  So I expected that I could adapt 
that to suit the purposes of the emulation.

It was obvious that I had to write code to handle more than just the TEST UNIT 
READY command, and that it had to be able to send and receive data over the 
SCSI bus, which it, in its current state, couldn't do.  Eventually I was able 
to get that working and I could see that the firmware was successfully 
negotiating the INQUIRY command *and* coming to the conclusion that it was 
talking to a hard disk.  More progress!

And, as it turns out, this first call in bank 3:1 is what determines what the 
device we're talking to actually is, and it sets up appropriate memory 
locations to signal that to other parts of the firmware.  This is another one 
of those places where the "Technical Manual for the Apple SCSI Card" had a 
useful tidbit, namely a small table that looked something like this:

Code  Device Type
------------------------------
$03   Nonspecific SCSI
$05   CD-ROM
$06   Direct-access tape drive
$07   Hard disk
$08   Scanner
$09   Printer

These device codes are different from the device codes that the INQUIRY command 
returns, and this bit of code also does the translation from one to the other.


The Next Part, In Which More Progress Is Made
---------------------------------------------

And so, in using similar analysis in the other parts of the code called by bank 
3:1, I was able to discern that after the INQUIRY command, it was calling the 
MODE SENSE, MODE SELECT, READ CAPACITY and READ commands afterward.  And since 
I didn't know exactly what these commands returned, I used the time honored 
method of returning messages consisting of all zeroes.

And, in fixing up the hard drive emulation to respond to these commands, I 
could see the firmware was making it all the way through the bank 3:1 code 
successfully, and not in a failure mode.  It didn't boot anything yet, as I 
hadn't written the code to load a hard disk image much less dole it out over 
the SCSI bus, but it was a good result and I could finally see the end of this 
Herculean task coming into view.

However, I could see from the log file that something still wasn't quite right.


The Next Part, In Which Things Start Getting LUN-ey
---------------------------------------------------

The problem was one of too much success.  It wasn't going through the set of 
INQUIRY, MODE SENSE, MODE SELECT, READ CAPACITY and READ commands just once, it 
was doing it *eight* times.  And in looking for the culprit, I found the 
following tidbit:

CCE5: EE DC C8  INC  $C8DC   ; Increment a counter
CCE8: AD DC C8  LDA  $C8DC
CCEB: C9 08     CMP  #$08
CCED: D0 B0     BNE  $CC9F   ; Loop back if we haven't checked 8 times yet

It wasn't obvious on first examination, but I eventually figured out that 
location $C8DC was being put into byte one of every command being sent over the 
SCSI bus--as I could see the INQUIRY command was changing every time it was 
called like so:

12 00 00 00 1E 00
12 20 00 00 1E 00
12 40 00 00 1E 00
12 60 00 00 1E 00
12 80 00 00 1E 00
12 A0 00 00 1E 00
12 C0 00 00 1E 00
12 E0 00 00 1E 00

And so, after more digging into the hard disk interface document, I could see 
that the field being modified was called the Logical Unit Number, or LUN for 
short.  Further, hard disks conforming to the SCSI-2 and SCSI-3 had a 
commandment, that being as follows:

The LUN Shall Be Zero, And Zero Shall The LUN Be.  It Shall Be No Other Number 
Save For Zero, For Any Other Number Shall Be An Abomination Before The Drive.

Well, going by simple logic, it would appear that the SCSI-1 protocol was not 
bound by such a rule, and so you could have eight Logical Units for each SCSI 
device on the bus.  But this presents an interesting challenge.  We need to 
tell the firmware to pound sand for all but one LUN.


Failure Is An Option
--------------------

And so I found myself in the position of needing to have the hard drive 
emulation fail in a meaningful way; which sounds like an oxymoron but really 
isn't.  I needed to code the hard drive emulation to respond with a CHECK SENSE 
message, which is how, I eventually discovered, that you signal an error 
condition in the SCSI protocol.  When I did this, the firmware then sent a 
REQUEST SENSE command, which I wasn't sure how to craft a response that would 
signal failure for an invalid LUN.  Responding with all zeroes didn't signal 
failure as I hoped it would, so it was back to the hard disk interface document 
to find the missing information.

There I found out that byte two of the response is a four-bit "Sense Key", and 
that zero corresponds to "No Sense", which means the command was successful.  
Which, as it turns out, is no way to signal failure.  The one that fit the bill 
was five, which corresponds to "Illegal Request".

And so it seems that 16 Sense Keys was not enough for the designers of the SCSI 
protocol, so those Sense Keys correspond to broad categories.  To give even 
more fine-grained responses to what went wrong, there are at least two more 
eight-bit bytes called the "Additional Sense Code" and "Additional Sense Code 
Qualifier", which, taken together, provide for 65,536 different combinations.  
And, in the interface document, I found $08 $00 which corresponds to "Logical 
Unit Communication Failure" which seemed like a reasonable message for this 
failure mode.

Coding up the meaningful failure path and running the emulation showed that 
this mostly satisfied the firmware; it would almost get all the way to the 
point where it attempted to read block zero from the hard drive in a 
non-failure mode, but there was still a small problem.


Every Problem Is Small, From A Certain Point Of View
----------------------------------------------------

There is a call in the bank 3:1 code that calls bank 4:0 to read a block from 
the disk and do some analysis on what it finds.  The logs also showed that this 
code was also doing a lot of writing to slot I/O register $F.  Much of it being 
calls to the following brief routine:

CFB4: AD 86 C8  LDA  $C886   ; Get the value in $C886
CFB7: 4A        LSR  A       ; Shift the hi nybble to the lo nybble
CFB8: 4A        LSR  A
CFB9: 4A        LSR  A
CFBA: 4A        LSR  A
CFBB: 09 08     ORA  #$08    ; Set the high bit of the lo nybble
CFBD: 9D 6F C0  STA  $C06F,X ; & store it in slot I/O register $F
CFC0: 60        RTS

This was some highly suggestive code, and what it suggested was that it was 
using three bits of a value set up elsewhere which made for eight combinations.
The only significant loose end, as far as the hardware was concerned, was the 
8K static RAM; in all of the analysis I had done up to this point, it *seemed* 
that only 1K of it was ever used.  But this code suggested otherwise.

It was suggesting that slot I/O register $F was a bank select soft switch for 
the 8K static RAM; once I coded it up as such, the firmware was then completely 
satisfied and would get all the way to where it attempted to read block zero 
from the hard drive in a non-failure mode.


The End Is Nigh
---------------

And so, having studiously and painstakingly laid the foundation for the actual 
purpose of the hard drive emulation--that being the transfer of data to and 
from the thing--I came at last to the part where I had to actually write code 
to have real data flowing to and from the emulated hard disk.  And this, as it 
turns out, was the least interesting part of the whole thing; getting the 
contents of files into memory and parsing them is a really trivial thing and 
usually quite boring.

So in writing this bit of code, I used 4am's "Pitch Dark" hard drive image, and 
added the necessary code to serve up appropriate slices of it in response to 
the firmware's READ command.  And, of course, after running the new emulation 
it failed to load anything.

It was then that I remembered that I sent back messages of all zeroes to 
requests from commands, for the most part, with a few exceptions.  One of these 
that was sure to cause problems without a proper response was the READ CAPACITY 
command.  When the firmware inquired about the size of the hard drive, the 
emulator would happily tell it that it had zero capacity--which meant that any 
attempted reads by the firmware would be out of range.

So I coded up a proper response for the size of the hard drive image I was 
using and fired up the emulator and...  It still didn't work.  The logs told me 
that it was sending a ten-byte command, and one I hadn't seen before, which was 
basically the ten-byte variant of the READ command.  Once I had *that* coded up 
properly, I fired up the emulator and after a few seconds, found myself in the 
monitor.

What?  Why?  How does this even--

To quell the questions that were pooling up in my head I wrote some hooks into 
the emulator to trigger a code trace at the appropriate time; that being where 
the code transfered control to memory address $801, the ostensible location 
where the firmware allegedly read from block zero and placed it in memory at 
$800.  And I knew that it was getting to that point successfully because the 
firmware doesn't get there unless everything is working on the SCSI bus as it 
should, and the trace in the log file confirmed this.

There are worse things than being dumped into the Apple II monitor; at least I 
could poke around memory and disassemble things to try to figure out what was 
going wrong.  And I could see that the block that was loaded into memory was 
looking at the slot ROM for a certain value that caused it to take a branch 
that landed it in a crash zone.  This made no sense whatsoever.

Fortunately for me though, I have the ability to disassemble a snapshot of any 
memory range that I desire--so I disassembled the entire block from $800 to 
$9FF.  And what I saw there was still strange; near the end of the block it 
just kind of ran out of instructions, like something was missing.  And looking 
near the middle of the block, I saw something eerily similar to what I saw at 
the end.

Then I realized it wasn't similar, it was *identical*.  Looking through the 
hard drive emulator code, I was not surprised to find this:

static uint8_t * buf;
static uint8_t bufPtr;

Yes, I had made a rookie mistake of using too small of a value for my buffer 
pointer; it was loading the correct block, but, because the buffer pointer was 
only eight bits wide, it only copied the first 256 bytes out of the hard drive 
image *twice*.

As embarrassing as this was, it was also good news, as it meant that firmware 
bootstrap code was working; it was reading real data from the hard drive 
emulation and running correctly.  Which meant that once I fixed the size of my 
buffer pointer, the emulated hard drive should boot up correctly.

And once I coded up the fix and started up the emulator once more, after six or 
so seconds, "Pitch Dark" came up on the screen and it was glorious...


Sic Transit Gloria Mundi
------------------------

I was able to navigate forward and back through the various games on the hard 
drive image; I could even view the artwork that came with each one.  And lo and 
behold: the games worked!

I was playing through a bit Wishbringer when I got to a point where I wanted to 
save my game.  And, even though there was no WRITE command hooked up yet, I 
tried it anyway and got a nice hard lockup on the emulator.  This would never 
do--to have a hard disk that was read-only--so I coded up the WRITE command 
handler.

And upon booting up the hard drive, it looked like it was OK, only there were 
problems; namely, while you could navigate through the various games, you could 
not launch them.  As a matter of fact, the only game that *could* be launched 
was Zork I, which was the first game to pop up on the menu.  So after looking 
the code, I noticed that there was an asymmetry in the ports used for reading 
and writing to the SCSI bus.  Which requires a brief digression into data 
transference.


To DMA, Or Not To DMA, That Is The Question
-------------------------------------------

As it turns out, I was finally able to figure out that the physical DMA on/off 
switch on the card was wired to bit 6 of slot I/O register $C.  I further found 
out that, since I was defaulting to zero for any unknown bit in the slot I/O 
registers, that it was treating the DMA switch as if it were in the off 
position.  However, even so, the firmware was still treating this as a DMA 
transfer.

And, looking at the 53C80 manual, I could see that it supported three distinct 
kinds of bus I/O: Programmed I/O (or PIO for short), Direct Memory Access (or 
DMA for short) and Pseudo DMA.  Of these three, PIO is the slowest, as it 
relies 100% on handshaking on the SCSI bus for data transfer, while DMA is the 
fastest, as all you need to do is set some registers and tell the 53C80 to go 
and it handles the transfer all in the background without the need for any 
intervention from the CPU whatsoever. But what the firmware was doing, in this 
DMA switch in the off position mode, was Pseudo DMA.

How it works for reading data from the SCSI bus is that the CPU monitors bit 6 
(DMA REQ) in the slot I/O register $5, then reads the data that shows up in 
slot I/O register $6 when the DMA REQ bit is asserted.  For this kind of 
transfer to work, however, there must be some kind of address decoding that 
will assert the DACK (Dma ACKnowledge) line once the data is read.  Because 
this code works, we can logically deduce that the read to slot I/O register $6 
is wired to produce this signal, even if we can't prove it conclusively through 
the schematic of the card.

Writing works in a similar manner by monitoring the DMA REQ line, but instead 
of writing to slot I/O register $6 (which is a trigger for starting a DMA 
transfer) it writes to slot I/O register $0.  And, as we inferred through logic 
about the setting of the DACK line in the reading case, we can similarly infer 
that the DACK line is being set in a similar manner in the writing case.

The upshot is, even though Pseudo DMA transfers are still CPU intensive, they 
are faster than PIO transfers.  And when it comes to relatively slow CPUs like 
the 65C02, faster is better.


And They All Lived Happily Ever After-ish
-----------------------------------------

So in looking at the code for the WRITE command, I could see that I had it 
using register $6 for the data transfer, which, as we can see from the short 
digression above, won't work.  Fixing this to look at the correct register ($0) 
brought things into alignment, and a thorough test of "Pitch Dark" confirmed 
that I had indeed solved the problem.

So, in the final analysis, I was finally able to restore decency to Apple2 and 
play "Pitch Dark" on it to boot.  But was it worth it?  In my opinion the 
answer is an unequivocal "yes", and not just because it enables the use of hard 
drive images in emulators.

The reason this little exercise in digital archaeology was worth the effort 
expended is that it underscores a problem that seems to have gone largely 
underappreciated: the early microcomputers, in some respects, are very well 
documented; however, in many others, they are not--and the knowledge of exactly 
how they worked is in danger of disappearing.  The fact that the documentation 
for the Apple High Speed SCSI card is of a consumer oriented nature with very 
little technical content was of little use in figuring out how it really 
worked, and shows a marked contrast to the early days of Apple where they 
published very detailed information about their computers and how they worked, 
including schematics and source code.

All that is to say that unless those of us who still remember these artifacts 
and have the ability to analyze them to tease out their inner workings actually 
*do* so, these things *will* disappear, and they will pass out of human memory 
forever.


--------------
v1.0: 6/3/2019
v1.1: 1/10/2020
