;----------------------------------------------------------------------
;
;   CP/M boot loader for Burcon CP/M 2.2 for the MITS/Altair 8 inch
;	disk controller.
;
;   This loader is read in from track zero, sectors 0 and 2 by the
;   MITS DBL ROM. The loader then reads all of CPM into the proper
;   location in memory and jumps to the cold boot entry point.
;
;   Disassembled from data provided by W. Tom Sanderson
;
;   Source code regeneration by Mike Douglas, January 2013
;
;----------------------------------------------------------------------
;
;    To patch changes made to this loader into a CP/M image saved
;    from MOVCPM (e.g., CPMxx.COM), use the following commands:
;
;	A>DDT CPMxx.COM
;	-IBOOT.HEX
;	-R900
;       -M980,9FF,A00	(splits code onto sectors 0 and 2)
;	-G0	    	(Go zero, not "oh")
;	A>SYSGEN
;
;----------------------------------------------------------------------

; Set the following equates based on BIOS settings

MSIZE	equ	56		;memory size in K bytes (0 for relocatable)
BIOSLEN	equ	0700h		;BIOS length in bytes (Lifeboat Altair)
FLAGS	equ	0559h		;offset to flags (MODE) byte in the BIOS

; CP/M size and location equates

CCPLEN	equ	0800h		;CPM 2.2 fixed length
BDOSLEN	equ	0E00h		;CPM 2.2 fixed length

BIOSBAS	equ	MSIZE*1024-BIOSLEN	;base address of the BIOS
BDOSBAS	equ	BIOSBAS-BDOSLEN		;base address of the BDOS
CCPBASE	equ	BDOSBAS-CCPLEN		;base address of the CCP

BOOTLEN	equ	3*128			;3 sectors for boot code
LOADTK0	equ	CCPBASE-BOOTLEN	 	;load address for track 0
LOADTK1	equ	LOADTK0 + 01000h	;load address for track 1

; MITS Disk Drive Controller Equates

DRVSEL	equ	8		;drive select port (out)
DRVSTAT	equ	8		;drive status port (in)
DRVCTL	equ	9		;drive control port(out)
DRVSEC	equ	9		;drive sector position (in)
DRVDATA	equ	10		;drive read/write data

ENWDMSK	equ	01h		;enter new write data bit mask
HEADMSK	equ	04h		;mask to get head load bit alone
MOVEMSK	equ	02h		;mask to get head move bit alone
TRK0MSK	equ	40h		;mask to get track zero bit alone
SELMASK	equ	08h		;"not used" bit is zero when selected

DCSTEPI	equ	01h		;drisve command - step in
DCSTEPO	equ	02h		;drive command - step out
DCLOAD	equ	04h		;drive command - load head
DCULOAD	equ	08h		;drive command - unload head
DCWRITE	equ	80h		;drive command - start write sequence

; Disk information equates

NUMSECT	equ	32		;number of sectors per track
ALTLEN	equ	137		;length of Altair sector
SECLEN	equ	128		;length of CPM sector


; ALTBUF - Altair buffer contains the 137 bytes read straight from
;   the Altair drive. It is located 0100h bytes below the where
;   the CCP is loaded

ALTBUF	equ	CCPBASE-0100h	;altair disk buffer

; Tracks 0-5 of Altair disks have this format

T0TRK	equ	0		;offset of track number
T0DATA	equ	3		;offset of 128 byte data payload
T0STOP	equ	131		;offset of stop byte (0ffh)
T0CSUM	equ	132		;offset of checksum


;-----------------------------------------------------------------------------
;  Start of boot loader. Seek to track 0, load track 0, then load track 1,
;     then jump to the cold start BIOS entry.
;-----------------------------------------------------------------------------
	org	0
start	lxi	sp,CCPBASE	;stack grows down from lowest cpm address
	di
	xra	a		;select drive zero
	out	DRVSEL

selDrv	in	DRVSTAT		;verify it was selected
	ani	SELMASK
	jnz	selDrv

	mvi	a,DCLOAD	;load the head
	out	DRVCTL
	jmp	chkTrk0		;go see if we're already on track 0

; seek0 - seek to track zero

seek0	in	DRVSTAT		;loop until it's OK to move the head
	ani	MOVEMSK
	jnz	seek0

	mvi	a,DCSTEPO	;step out one track
	out	DRVCTL

chkTrk0	in	DRVSTAT		;loop until we get to track 0
	ani	TRK0MSK
	jnz	seek0

; Load track 0 content into memory. Odd sectors are read 1st and stored
;    in every other 128 byte block. Then even sectors are read to fill
;    in the other 128 byte blocks.

	lxi	b,0100h		;B=sector 1, C=track 0 
	lxi	h,loadTk0	;HL->track 0 load address
	call	loadTrk		;load all track 0 data

; Load track 1 content in the same manner.

chkTrk1	in	DRVSTAT		;wait until OK to move the head
	ani	MOVEMSK
	jnz	chkTrk1

	mvi	a,DCSTEPI	;step in one track to track 1
	out	DRVCTL

	lxi	b,0101h		;B=sector 1, C=track 1
	lxi	h,loadTk1	;HL->track 1 load address
	call	loadTrk		;load off track 1 data

; Jump into the BIOS with interrupts enabled or disabled based on the
;    flabs byte in the BIOS.

	lda	BIOSBAS+FLAGS	;A=flags (MODE) byte from BIOS
	ani	010H		;enable interrupt flag true?
	jz	BIOSBAS		;no, leave them disabled

	ei			;else, enable interrupts
	jmp	BIOSBAS

;------------------------------------------------------------------------------
;  loadTrk - load a track into memory at it's final address
;------------------------------------------------------------------------------
loadTrk	push	b		;save sector and track number we're on
	push	h		;save destination address
	mov	a,h
	cpi	(CCPBASE shr 8) ;current address < cpp start addr?
	jc	nxtSec		;yes, skip the next sector
		
	call	read		;read a sector
	jnz	start		;fatal read error, try again

nxtSec	pop	h		;get the destination pointer back
	lxi	d,0100h		;increment destination by 256 bytes
	dad	d

	pop	b		;get sector number back
	mov	a,b
	adi	2		;jump 2 setors each read
	mov	b,a

	cpi	NUMSECT+1	;past 32 sectors?
	jc	loadTrk		;not yet, keep reading

	sui	NUMSECT-1	;compute starting even sector number
	lxi	d,-0F80h	;compute load address for 1st even sector
	dad	d

	cpi	3		;done both odd and even sectors?
	mov	b,a
	jnz	loadTrk		;no, go to even sectors

	ret

;----------------------------------------------------------------------------
;  read - read a sector from the disk into ALTBUF, then move it to
;      the destination address after error checking. The sector number
;      is passed in B, the destination address is in HL.
;---------------------------------------------------------------------------
read	push	b		;save sector number
	push	h		;save address we're writing to next
	call	rdPSec		;read the sector specified in b into ALTBUF

	lxi	d,-6		;-6 bytes from the end is 0FFh stop byte
	dad	d
	inr	m		;this should increment 0FFh to zero
	pop	b		;BC->destination address
	pop	d		;D=sector number, E=track number
	rnz			;exit if stop byte wasn't 0FFh

	lxi	h,ALTBUF+T0TRK	;HL->track byte in ALTBUF
	mov	a,m		;get 1st byte with track number
	ani	07Fh		;get track number alone
	cmp	e		;verify track number is correct
	rnz			;exit if not right track

	inx	h		;move to 128 data payload at offset 3
	inx	h
	inx	h
	call	moveBuf		;move ALTBUF+T0DATA to memory at (HL)

	lxi	h,ALTBUF+T0CSUM	;verify checksum matches
	cmp	m
	ret

;----------------------------------------------------------------------------
; rdPSec - read physical sector. Read the sector specified by B into
;    ALTBUF. Physical sector length is ALTLEN (137) bytes.
;----------------------------------------------------------------------------
rdPSec	dcr	b		;convert 1 indexed sector to 0 indexed
	call	secSync		;sync to start of sector specified in B

	mvi	c,ALTLEN	;C=length of Altair sector (137 bytes)
	lxi	h,ALTBUF	;HL->ALTBUF

rdLoop	in	DRVSTAT		;get drive status byte
	ora	a		;wait for NRDA flag true (zero)
	jm	rdLoop

	in	DRVDATA		;read the byte 
	mov	m,a		;store in the read buffer
	inx	h		;increment buffer pointer
	dcr	c		;decrement characters remaining counter
	jz	rdDone		;count is zero - read is done

	dcr	c		;decrement count for 2nd byte about to be read
	nop			;timing
	in	DRVDATA		;get the 2nd byte
	mov	m,a		;store in the read buffer
	inx	h		;increment buffer pointer
	jnz	rdLoop		;loop until all bytes read

rdDone	xra	a		;return status of zero = good read
	ret

; secSync - sync to start of sector specified in B.

secSync	call	ldHead		;load the drive head

wtSecTr	in	DRVSEC
	rar
	jc	wtSecTr

	ani	01Fh		;get sector number alone
	cmp	b		;does sector number match?
	jnz	wtSecTr		;loop until it does

	ret

;----------------------------------------------------------------------------
; moveBuf - move sector buffer (128 bytes) from (HL) to (BC). Compute
;   checksum on all bytes and return in A.
;----------------------------------------------------------------------------
moveBuf	mvi	d,0		;init checksum to zero
	mvi	e,SECLEN	;128 byte data sector length

movLoop	mov	a,m		;move from (HL) to (BC)
	stax	b

	add	d		;add moved byte to checksum
	mov	d,a		;checksum saved in D

	inx	h		;increment both pointers
	inx	b
	dcr	e		;loop until all characters moved
	jnz	movLoop

	ret

;----------------------------------------------------------------------------
;  ldHead - If head already loaded, return. Otherwise, issue head load 
;     command.
;----------------------------------------------------------------------------
ldHead	in	DRVSTAT
	ani	HEADMSK		;get head loaded bit alone
	rz			;zero = true, head already loaded

	mvi	a,DCLOAD	;issue the load head command
	out	DRVCTL
	ret

	end

