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