; This is a reverse-engineering of the boot rom for the N* disk controller. ; Overall, this is what is going on: ; ; 1) The 256B boot prom, which contains this here code, ; does not know what address the board containing has ; been mapped to in the system. It copies itself ; down to the first page of memory, from 00030H to ; 00FFH, and begins execution. ; ; 2) It spins up the disk, steps the read head to track 0, ; sector 4. Sector 4 is conventionally where the boot ; sector is on northstar disks. ; ; 3) The first byte of sector 4 contains the page address ; of where the boot process should occur. The remaining ; 511 bytes of the sector are copied to that address. ; ; 4) We jump to byte 10 of the just-loaded boot sector, ; where a second-level bootstrap occurs. ; ; ; Just about all of it is understood except for a few things: ; ; Down just after the W870 section a test is made to see ; if the 0'th track has been recorded in double density. ; If not, we step in one track and wait for sector 8 instead ; of sector 4. Huh? ; ; If we really are in single density mode, the PAGE1/PAGE2 ; code blindly reads 512 bytes, plus a checksum byte. But ; single density mode only allows 256B plus 1 check byte ; per sector, so this code violate two things: reading ; across a sector boundary (OK, maybe the uP just is put ; in a wait state for the inter sector gap), and what about ; the other checksum byte? ; ; The code surrounding "FILL59:" writes four bytes to the start ; of the code we are loading. Why? It looks like we don't ; execute that code anyway, as we begin execution at start+10. ; ; What is the "RDT" gibberish? Is is really just copyright ; protection or programmer in-joke? ; ------------------------------------------------------------ ; IO port mapping from north star disk controller ; ; The following docs are lifted more or less verbatim from ; NorthStar document "Micro-disk system MDS-A-D Double Density" ; Case 0: PROM addressing (typically E800H) ; ; +----+----+----+----+----+----+----+----+ ; | PROM Address | ; +----+----+----+----+----+----+----+----+ ; ; Read byte from the 256 bytes of PROM. ; ; ; Case 1: Write byte of data (typically E900H) ; ; +----+----+----+----+----+----+----+----+ ; | Data | ; +----+----+----+----+----+----+----+----+ ; ; Write a byte of data to the disk. Wait if the write shift ; register is not empty. The low order 8 bits specify the ; byte to be written. ; ; ; Case 2: Controller Orders (typically EA00H) ; ; +----+----+----+----+----+----+----+----+ ; | DD | SS | DP | ST | DS | ; +----+----+----+----+----+----+----+----+ ; ; Load 8-bit order register from low order 8 address bits. ; ; DD Controls density on write. ; DD=1 for double density. ; DD=0 for single density. ; ; SS specifies the side of a double-sided diskette. The ; bottom side (and only side of a single-sided diskette) ; is selecte when SS=0. The second (top) side is ; selected when SS=1. ; ; DP has shared use. During stepping operations, DP=0 ; specifies a step out and DP=1 specifies a step in. ; During write operations, write precompensation is ; invoked if and only if DP=1. ; ; ST controls the level of the head step signal to the disk ; drives. ; ; DS is the drive select field, encoded as follows ; ; 0=no drive selected ; 1=drive 1 selected ; 2=drive 2 selected ; 4=drive 3 selected ; 8=drive 4 selected ; ; ; Case 3: Controller Command (typically EB00H) ; ; +----+----+----+----+----+----+----+----+ ; | DM | CC | ; +----+----+----+----+----+----+----+----+ ; ; Perform a disk controller command. The commands are ; specified by the 8 low order address bits. ; ; DM The DM field controls what gets multiplexed onto the ; DI bus during the command. ; ; 1=A-status ; 2=B-status ; 3=C-status ; 4=Read data (may enter wait state) ; ; CC Command code. ; ; 0=no operation ; 1=reset sector flag ; 2=disarm interrupt ; 3=arm interrupt ; 4=set body (diagnostic) ; 5=turn on drive motors ; 6=begin write ; 7=reset controller, deselect drives, stop motors ; ; DISK CONTROLLER STATUS BYTES ; ; There are three status bytes that can be read on the Data Input ; Bus. ; ; A-Status ; +----+----+----+----+----+----+----+----+ ; | SF | IX | DD | MO | WI | RE | SP | BD | ; +----+----+----+----+----+----+----+----+ ; ; SF Sector Flag: set when sector hole detected, reset by ; software. ; ; IX Index Detect: true if index hole detected during previous ; sector. ; ; DD Double Density Indicator: true if data being read is encoded ; in double density. ; ; MO Motor On: true while motor(s) are on. ; ; WI Window: true during 96-microsecond window at beginning of ; sector. ; ; RE Read Enable: true while phase-locked loop is enabled. ; ; BD Body: set when sync character(s) is detected. ; ; SP Spare: reserved for future use. ; ; ; B-Status ; +----+----+----+----+----+----+----+----+ ; | SF | IX | DD | MO | WR | SP | WP | T0 | ; +----+----+----+----+----+----+----+----+ ; ; SF, IX, DD, MO, SP: same as A-Status ; ; WR Write: true during valid write operation. ; ; WP Write Protect: true while the diskette installed in the ; selected drive is write protected. ; ; T0 Track 0: true if selected drive is at sector 0. ; [ NOTE: I think this is in error: it is true if the ; head for the selected drive is at track 0, ; not sector 0. This isn't based on any ; experiment, just the name and reason. ] ; ; C-Status ; +----+----+----+----+----+----+----+----+ ; | SF | IX | DD | MO | SC | ; +----+----+----+----+----+----+----+----+ ; ; SF, IX, DD, MO: same as A-Status ; ; SC Sector Counter: indicates the current sector position. ; DISK CONTROLLER DATA FORMAT ; ; Each diskette has 35 tracks of data. Each track is divided into ; 10 sectors. The rotational position of the beginning of the ; sectors is marked by sector holes in the diskette. Each sector ; is recorded using the following format. This information is ; recorded starting about 96 microseconds after the sector hole is ; detected. ; ; Single Density Double Density ; Zeros 16 bytes 32 bytes ; Sync Char(FB) 1 byte 2 bytes ; Data 256 bytes 512 bytes ; Check Char 1 byte 1 byte ; ; [ NOTE: a bit further on the docs claim 15/31 leading 00H ; bytes for each sector, not 16/32. ] ; ; The check character is computed iteratively by setting it to zero ; and then exclusive ORing each successive data byte value with the ; current value of the check character and left cycling the result. ; This is a tricky boot prom. Because the disk controller can ; be configured to appear at a number of locations in the 8080 ; memory space, the PROM starts off by copying itself in a ; position independent way down to location 0000H in the system. ; it them jumps to the new copy and subsequent code is then free ; to use absolute jumps. ; ; this code is assembled at 0000H, but keep in mind that it really ; is initially executing at a higher address. ORG 0000H ; I must admit, I don't completely understand what is going on here. ; There are a lot of dead variables here. Perhaps there it is some ; type of copyright tomfoolery, but it doesn't seem like it. ; vvvv actual address BOOT: MOV B,H ; ??00: huh? appears to be dead LXI B,0101H ; ??01: BC <= 0101H ADD D ; ??04: huh? appears to be dead ADD H ; ??05: huh? appears to be dead MOV A,B ; ??06: A <= 01H ANI 07H ; ??07: A <= 01H (huh?) MOV C,A ; ??09: C <= 01H (still) NOP ; ??0A ; write a piece of code into page 00H of RAM. ; 0028: C9 RET ; 0029: 1A LDAX D ; 002A: 77 MOV M,A ; 002B: 13 INX D ; 002C: 2C INR L ; 002D: C2 29 00 JNZ $0029 LXI SP,0030H ; ??0B LXI H,0029H ; ??0E PUSH H ; ??11 LXI H,0C22CH ; ??12 PUSH H ; ??15 LXI H,1377H ; ??16 PUSH H ; ??19 LXI H,1AC9H ; ??1A PUSH H ; ??1D ; at this point, SP=0028H CALL 0028H ; ??1E ; this just called the RET instruction, but the real point of this is ; to leave the address of the caller on the stack (assuming a boot ; address of E800H): ; 0026: 21 ; 0027: E8 ; (0027H) is read out later. ; again, what the heck is going on? LXI H,0030H ; ??21: HL <= 0030H MOV E,E ; ??24: DE <= ????H "[" MOV D,D ; ??25: DE <= ????H "R" MOV B,H ; ??26: BC <= 0001H "D" MOV D,H ; ??27: DE <= 00??H "T" MOV E,L ; ??28: DE <= 0030H "]" L0029: ; although this is a label, it really is ??29. when we jump ; to L0029 below, it is the relocated code we are jumping to. LDA 0027H ; ??29: read page of caller (assuming no interrupts) MOV D,A ; ??2C: DE <= E830H JMP L0029 ; ??2D: jump to code we just wrote to RAM: ; 0029: 1A LDAX D ; 002A: 77 MOV M,A ; 002B: 13 INX D ; 002C: 2C INR L ; 002D: C2 29 00 JNZ 0029H ; ; So this section of code copies [ E830H .. E8FFH ] to [ 0030H .. 00FFH ], ; and then falls through to the code just loaded into that range. All ; the code in the boot rom from 0030H to 00FFH is effectively relocated ; to the first page of RAM. ; ; At this point, the known register values are: ; ; HL = 0000H (we only did "INR L", and it just wrapped around) ; DE = E900H (we just did "INX D" and the LSB wrapped) ; BC = 0001H ; A = 54H (last byte transferred) INR D ; DE <= EA00H, the order register addr INR D ; DE <= EB00H, the command register addr ; after this point DE is always EBxxH, the disk command register. ; D is not modified again, and E is twiddled in various ways to ; point to various memory mapped locations when (de) is read. ; (de) is never written. MVI E,15H ; DE <= EB15H LDAX D ; turn on drive motors MVI H,30H ; HL <= 3000H CALL HSEC ; wait for spin up: 48 sectors MOV B,D ; BC <= EB01H DCR B ; BC <= EA01H LDAX B ; single density, bottom side, drive 1 CALL TWOSEC ; wait for two sector holes to go past ; seek index hole. hang if 12 sectors go by without one. MVI L,13 ; L=attempt counter SKTRY: DCR L SKHANG: JZ SKHANG ; hang if we don't succeed CALL NXTSEC ; DE <= EB10H LDAX D ; read A-status ANI 40H ; IX: index hole detected? JZ SKTRY ; if it no index hole, try again ; ----------------------------------------- ; big outer wrapper -- ; attempt to load the boot program from disk, starting at retry. ; If something goes wrong, we start over again from that point. ; If fail after ten attempts, we simply hang. MVI A,10 ; # of attempts to try to load boot program PUSH PSW ; the attempt counter is kept on the stack CALL STEPIN ; step in; BC = EA01H before and after call ; step the read head in until we find track 0 SEEK0: MVI E,20H ; DE <= EB20H LDAX D ; get B-status ANI 01H ; check for track 0 (hmm, why not RCA/JC?) JNZ W8SEC4 ; jump if we're at track 0 CALL STEP ; step out; BC = EA01H before and after call JMP SEEK0 ; assume we'll eventually get there ; we've found track 0; wait for sector 4 and subsequent phase lock W8SEC4: MVI L,04H CALL W84SEC ; wait for sector counter to equal 4 W84RE: MVI E,10H ; DE <= EB10H LDAX D ; get A-status ANI 04H ; wait for RE (read enable) flag JZ W84RE ; try again ; kill 7+9*14, or 133 cycles, or ~70 uS @ 2MHz MVI A,09H ; [ 7 cycles ] W870: DCR A ; [ 4 cycles ] JNZ W870 ; [ 10 cycles ] LDAX D ; get A-status ANI 20H ; check DD (double density) indicator JNZ W84BD ; branch if double density; BC=EA01 here CALL STEPIN ; step in; return with BC=EA01 MVI L,08H CALL W84SEC ; wait for sector counter to equal 8 ; wait for sync flag at start of sector, indicating start of data ; 7 + 35*n cycles. @ 2MHz & n=162, this is 2.84 ms. it takes ; about (60000ms/min)/(300 RPM)/(10 sectors/revolution), or about ; 20 ms per sector. so 2.84 ms is about 14% of a sector. W84BD: MVI B,0A3H ; countdown timer: 162 attempts MVI E,10H ; [ 7] DE <= EB10H BDLOOP: DCR B ; [ 4] JZ FAIL ; [10] timed out LDAX D ; [ 7] get A-status RRC ; [ 4] put BD (BODY) flag into carry JNC BDLOOP ; [10] wait for sync char(s) ; we've received the sync char(s) MVI E,40H ; DE <= EB40H LDAX D ; read the first data byte MOV H,A ; 256B page to load boot program to MVI L,00H ; hl = (boot page) << 8 MVI M,59H ; strange -- done again later RLC ; A=H rotatated left 1 MOV B,A ; 1st byte of CRC checksum INX H ; start transfer at second byte ; transfer 511 more bytes from (de) to (hl). HL points to memory, ; but DE is reading from a memory mapped register. As the bytes ; are transferred, each byte is CRC checksummed into B. PAGE1: LDAX D ; get next data byte MOV M,A XRA B ; \ RLC ; >-- checksum A into B MOV B,A ; / INR L JNZ PAGE1 INR H ; done with this page: go to next page PAGE2: LDAX D ; get next data byte MOV M,A XRA B ; \ RLC ; >-- checksum A into B MOV B,A ; / INR L JNZ PAGE2 ; test checksum LDAX D ; read checksum byte XRA B ; compare it to our computed checksum JNZ FAIL ; mismatch ; write 59H,59H,59H,01H to first four bytes of first page. DCR H ; go back to first page MVI L,03H ; first page, fourth byte MOV M,C ; C=01H as far as I know FILL59: DCR L ; first page, bytes 2, 1, 0 MVI M,59H JNZ FILL59 MVI L,0AH PCHL ; start running boot pgm, first page byte 10 ; like next entry point, but BC is bumped up by 20H first. ; on exit, BC is the same for either case, though. ; It would appear that both callers of this routine come in with ; BC=EA01H, so I don't know why this code can't simply load C with 21H. STEPIN: MVI A,20H ; \ ADD C ; >-- add 20H to C, w/no carry into B MOV C,A ; / ; step the read head in or out one track: ; if BC is EA01, we will step out. ; if BC is EA21, we will step in. ; after giving the step command, we wait for two sector holes to be ; detected to give us some reassurance that it is on the new track. ; ; return with H of 00H. ; return with DE of EB10H. ; return with BC of EA01. STEP: LDAX B ; BC=EA21H or EA01H; read but ignored MVI A,10H ; \ ADD C ; >-- add 10H to C MOV C,A ; / LDAX B ; step bit is high MVI A,0F0H ; \ ADD C ; >-- subtract 10H from C MOV C,A ; / LDAX B ; step bit is low MOV A,C ANI 0FH MOV C,A ; BC=EA01H ; wait for the detection of two sector holes. ; return with H of 00H. ; return with DE of EB10H. TWOSEC: CALL NXTSEC ; ... then fall through to do it again ; wait for the detection of the next sector hole. ; return with H of 00H. ; return with DE of EB10H. NXTSEC: MVI H,01H ; one pass through polling routine ; wait for the detection of a sector hole 'H' times. ; return with H of 00H. ; return with DE of EB10H. HSEC: MVI E,11H ; DE <= EB11H LDAX D ; reset sector flag DCR E ; DE <= EB10H W84SF: LDAX D ; read A-status ORA A JP W84SF ; wait until sector flag is 1 DCR H JNZ HSEC RET ; wait until the sector counter matches what is in L. ; return with H of 00H. ; return with DE of EB35H. W84SEC: CALL NXTSEC MVI E,35H ; DE <= EB35H LDAX D ; get C-status and kick drive motor ANI 0FH ; inspect bits [3:0] CMP L ; ... vs L JNZ W84SEC ; try again if they don't match RET ; they matched ; we failed to boot. decrement the boot attempt counter which ; is kept on the stack and try again if appropriate. FAIL: POP PSW ; recover attempt countdown DCR A PUSH PSW JNZ SEEK0 ; try again if we haven't reached 0 ; we've attempted to boot 10 times and failed every time. ; call it quits. HANG: JMP HANG ; spin wheels forever ; Deadwood, and the second time we've seen this sequence. ; The initials of the programmer, perhaps? MOV D,D ; "R" \ MOV B,H ; "D" >-- someone's signature? MOV D,H ; "T" /