; this is a tentative reconstruction of the boot sector from my SOL
; CP/M boot disk.  this second-level boot program is read into memory
; at $9C00 to $9DFF by the PROM on-board the disk controller.
;
; this boot program reads in these sectors to the following addresses:
;     trk 0  sec 5  $9E00 to $9FFF
;     trk 0  sec 6  $A000 to $A1FF
;     trk 0  sec 7  $A200 to $A3FF
;     trk 0  sec 8  $A400 to $A5FF
;     trk 0  sec 9  $A600 to $A7FF
;     trk 1  sec 0  $A800 to $A9FF
;     trk 1  sec 1  $AA00 to $ABFF
;     trk 1  sec 2  $AC00 to $ADFF
;     trk 1  sec 3  $AE00 to $AFFF
;     trk 1  sec 4  $B000 to $B1FF
;     trk 1  sec 5  $B200 to $B3FF
;     trk 1  sec 6  $B400 to $B5FF
;     trk 1  sec 7  $B600 to $B7FF
;     trk 1  sec 8  $B800 to $B9FF
;     trk 1  sec 9  $BA00 to $BBFF
;
; assuming all programs are read in successfully, it then
; relocates the code if it needs to be, then jumps to some
; magic entry address.


; disk controller memory mapped registers:
DISKDATA	EQU	0EB40H	; unconfirmed but strong guess

; CP/M services:
BDOS		EQU	05H	; CP/M bdos entry point
CONOUT		EQU	02H	; console output of E
RESETDISK	EQU	0DH	; default: drive=A, DMA is $0080, R/W access
SELECTDISK	EQU	0EH	; select default disk for subsequent ops
OPENFILE	EQU	0FH	; open disk file:             A=$FF on error
CLOSEFILE	EQU	10H	; close file (de):            A=$FF on error
SRCHFIRST	EQU	11H	; find file that matches FCB: A=$FF on error
SRCHNEXT	EQU	12H	; next file that matches FCB: A=$FF on error
DELETEFILE	EQU	13H	; delete file FCB:            A=$FF on error
READSEQ		EQU	14H	; read sequential next 128B:  A=0 means OK
STACKTOP	EQU	9C00H

	ORG	9C00H

; the next four bytes, $59,$59,$59,$01 were jammed here by the boot
; ROM on the disk controller and aren't what is on the disk itself.
; The disk I have actually contains the following bytes:
;     $9C, $AF, $21, $F7
;   00: 9C:       specifies which page to use for the boot loader.
;   01: AF:       XRA A
;   02: 21 F7 B9: LXI H,$B9F7
;   05: 36 44:    MVI M,$44
; I don't know if this is actually how they are used.  I did
; a DU B9F7 and it contains $43. hmm.

	MOV	E,C		; $59 \
	MOV	E,C		; $59  >-- put here by bootrom.  why?
	MOV	E,C		; $59 /
	LXI	B,36B9H		; even the LD BC opcode, 01, came from bootrom
	MOV	B,H
	JMP	DORELOC

; on entry, HL is the same as the address of "entry" (since we came
; here via a "jp (hl)" instruction).  de is $EB40.
ENTRY:
	DI			; why didn't boot rom do this?
	LXI	SP,STACKTOP	; just below the start of this code
	XCHG			; HL <-- $EB40, the disk read data address
	SHLD	DDVEC		; save address of read data port
	MOV	A,H
	STA	DDPAGE		; top 8b of DDVEC contents

	; read track 0 sectors 5-9 into $9E00 to $A7FF.
	; (actually, this code assumes we're already on track 0.)
	LXI	H,9E00H		; just after this 512B boot loader
	LXI	B,0505H		; B is counter, C is sector pointer
RDTRK0:	PUSH	H
	PUSH	B
	CALL	RDSECTOR	; read 512B sector C to memory addr HL
	JNZ	FAIL
	POP	B
	POP	H
	INR	H		; bump up ...
	INR	H		; ... 512 bytes
	INR	C		; point to next sector
	DCR	B
	JNZ	RDTRK0

	; step the read head to track 1
	PUSH	H		; $A800
	LHLD	DDVEC		; $EB40
	DCR	H
	MVI	L,21H		; $EA21, prepare step in drive 1 (A)
	MOV	A,M
	MVI	L,31H		; $EA31, actually step in drive 1 (A)
	MOV	A,M
	MVI	L,21H		; $EA21, prepare step in drive 1 (A)
	MOV	A,M
	MVI	D,02H
	CALL	SECD		; wait for 2 sectors to go by

	; read track 1, sectors 0-9 into $A800 to $BBFF.
	POP	H		; $A800
	LXI	B,0A00H		; B is counter, C is sector pointer
RDTRK1:	PUSH	H
	PUSH	B
	CALL	RDSECTOR	; read 512B sector C to memory addr HL
	JNZ	FAIL
	POP	B
	POP	H
	INR	H		; bump up ...
	INR	H		; ...512 bytes
	INR	C		; point to next sector
	DCR	B
	JNZ	RDTRK1

	MVI	A,01H		; make sure flag is set for unknown reason
DORELOC:
	ORA	A
	CALL	RELOCATE
	LDA	DDPAGE		; $EB
	SUI	03H		; $E8
	STA	0B9F5H		; base address of disk controller board
	NOP
	JMP	0B300H		; on to bios cold boot


; read sector C of current track into memory HL.
RDSECTOR:
	PUSH	H		; save address we want to save sector to
W84SEC:	CALL	NXTSEC
	MVI	L,35H		; $EB35
	MOV	A,M		; read C-status, turn on drive motors
	ANI	0FH		; isolate sector counter field
	CMP	C
	JNZ	W84SEC		; we haven't found the sector we want yet

	LHLD	DDVEC
	MVI	L,10H		; $EB10
W84RE:	MOV	A,M		; read A-status, disk 1
	ANI	04H		; test RE (read enable) bit
	JZ	W84RE		; no phase-lock yet

	; kill a few microseconds
	MVI	A,09H		; [ 7]
DLAY:	DCR	A		; [ 4]
	JNZ	DLAY		; [10]

	MVI	B,8CH		; 140 tries
	LHLD	DDVEC
	MVI	L,10H		; $EB10
W84BD:	MOV	A,M		; read A-status, disk 1
	RRC			; get BD (body) bit into carry flag
	JC	READIT		; jump if we've seen sector sync byte(s)
	DCR	B
	JNZ	W84BD		; try again

; we come here when the read has failed, eg bad sector checksum.
; it might make a hint of sense if we instead jumped to $E800
; instead of $EB00.
FAIL:
	LDA	DDPAGE		; $EB
	MOV	H,A
	MVI	L,00H		; $EB00
	PCHL			; jump to $EB00.  nonsensical.

; read in the 512 bytes of the sector
READIT:	LHLD	DDVEC		; $EB40
	XCHG			; DE=$EB40
	POP	H		; HL=address to store sector to
	MVI	B,00H		; init sector checksum
	MVI	L,00H		; forced to be page aligned

; read in first half of 512B sector
PAGE1:	LDAX	D		; get next data byte from disk
	MOV	M,A		; save it
	XRA	B		; \
	RLC			;  >-- checksum
	MOV	B,A		; /
	INR	L
	JNZ	PAGE1

; read in second half of 512B sector
	INR	H		; save to next page
PAGE2:	LDAX	D
	MOV	M,A
	XRA	B		; \
	RLC			;  >-- checksum
	MOV	B,A		; /
	INR	L
	JNZ	PAGE2

	LDAX	D		; get stored sector checksum
	XRA	B		; compare to computed checksum
	JNZ	FAIL		; death!
	RET

; wait for next sector
NXTSEC:
	MVI	D,01H

; wait for D'th sector
SECD:
	LHLD	DDVEC
	MVI	L,11H		; $EB11
	MOV	A,M		; read status-A, reset sector flag
	LHLD	DDVEC
	MVI	L,10H		; $EB10
W84SF:	MOV	A,M
	ORA	A
	JP	W84SF		; poll until SF (sector flag) is seen
	DCR	D
	LHLD	DDVEC
	MVI	L,11H		; $EB11
	MOV	A,M		; read status-A, reset sector flag
	JNZ	SECD		; loop until D=00
	RET


; This routine makes the driver independent of the controller base address.
; The BIOS contains a table of address that reference the disk controller
; absolutely.  By scanning the table and adding an offset to all indicated
; bytes, the hardcoded address references are fixed up.
;
; The code as written assumes a base address of $EB00 for the disk control
; registers.  If it isn't $EB00, the relocation processes occurs.  The
; table consists of a list of (lsb,msb) words.  Each entry supplies a
; byte in the program that needs a relative offset added to it to fix
; the code.  The table is terminated by an entry of $0000.
;
; The boot address of the board is actually E800, but EB00 just happens
; to be the value pointed to by the DE pair on entry to the boot program.

RELOCATE:
	LDA	DDPAGE		; $EB
	SUI	0EBH		; build-time assumption is EBxxH
	RZ			; code is OK as-is
	MOV	C,A		; C=relative page offset from $EBxx
	LXI	H,0A811H	; relocation table embedded in code
RELOC:	MOV	E,M
	INX	H
	MOV	D,M
	MOV	A,M		; MOV A,D would have been faster
	ORA	E
	RZ			; return if de=0000H
	LDAX	D		; pick up absolute byte
	ADD	C		; add relative page offset
	STAX	D		; write it back with new address
	INX	H
	JMP	RELOC


DDVEC:	DW	DISKDATA	; (DS 2) pointer to disk controller address
	DB	0,0,0,0,0,0,0,0,0,0,0		; 11 bytes unused
DDPAGE:	DB	0EBH		; (DS 1) ms 8b of ddvec contents
	; I'm not sure why the program doesn't just access
	; like this: "ld a,(ddvec+1)", but it doesn't
;
	JMP	0A05CH		; 9D00 : C3 5C A0    " \ "    JMP LA05C
	JMP	0A058H		; 9D03 : C3 58 A0    " X "    JMP LA058
;
	MOV	A,A		; 9D06: 7F
	INR	B		; 9D07: 04

; interestingly, 4+1 + 49+1 + 73 = 128
AUTOSTR:			; unused by this code
	DB	'AUTO'
	DB	0		; string terminator

COPYRIGHTSTR:			; unused by this code
	DB	'           COPYRIGHT (C) 1979, DIGITAL RESEARCH  '
	DB	0		; string terminator

	DS	73		; unused

	DW	AUTOSTR		; unused by this code
	DW	0


; send the contents of A out on the console BC may be affected  (unused)
OUTA:	MOV	E,A
	MVI	C,CONOUT
	JMP	BDOS

; like outA, except BC is preserved (unused)
PRINTA:	PUSH	B
	CALL	OUTA
	POP	B
	RET

; print carriage return and line feed characters (unused)
OUTCRLF:
	MVI	A,0DH		; carriage return
	CALL	PRINTA
	MVI	A,0AH		; line feed
	JMP	PRINTA

; print a space character on output (unused)
PRTPC:
	MVI	A,' '		; space
	JMP	PRINTA

; print CR/LF and then a string pointed at by BC (unused)
PRTNSTR:
	PUSH	B
	CALL	OUTCRLF
	POP	H

; send the null-terminated string at (hl) to the console output (unused)
PRSTRZ:	MOV	A,M
	ORA	A		; test for null char
	RZ			; done
	INX	H
	PUSH	H
	CALL	OUTA		; print next char
	POP	H
	JMP	PRSTRZ

; unused routine
	MVI	C,RESETDISK
	JMP	BDOS

; unused routine
	MOV	E,A
	MVI	C,SELECTDISK
	JMP	BDOS

; perform bdos call and save return code
DOBDOS:
	CALL	BDOS
	STA	0A4EEH
	INR	A
	RET
;
DOOPEN:
	MVI	C,OPENFILE
	JMP	DOBDOS

; unused routine
	XRA	A
	STA	0A4EDH
	LXI	D,0A4CDH
	JMP	DOOPEN
;
	MVI	C,CLOSEFILE
	JMP	DOBDOS
;
DOSRCH:
	MVI	C,SRCHFIRST
	JMP	DOBDOS

; unused routine
	MVI	C,SRCHNEXT
	JMP	DOBDOS

; unused routine
	LXI	D,0A4CDH
	JMP	DOSRCH

; unused routine
	MVI	C,DELETEFILE
	JMP	BDOS
;
BDSTAT:
	CALL	BDOS
	ORA	A		; set flags on return code
	RET

; unused routine
	MVI	C,READSEQ	; read next 128B of file
	JMP	BDSTAT		; return with zflag=1 if OK, else EOF
;
	DB	11H		; also "ld de,$xxCD"
	DB	0CDH

