;****************************************************************************
;
;  CP/M cold boot loader for Altair CP/M using virtual Altair 8" floppy
;    drives served through a SOL-20 or 3P+S serial port.
;
;    This code is loaded from sectors 0 and 2 into RAM by the disk boot
;    loader PROM. This loader, in turn, loads the CP/M image into RAM.
;
;    Version History
;
;    Ver     Date	  Description
;    ---   ---------	---------------------
;    1.0   11/10/24	Original, Mike Douglas
;
;--------------------------------------------------------------------------
;
;    To patch changes made to this loader into a CP/M image saved from
;    MOVCPM2S (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
;
;****************************************************************************
TBHIRAM	equ	1		;1=track buffer in high RAM (D000-E200)
MEMSIZE	equ	48		;set memory size in K bytes

; 8" floppy equates

NUMSEC	equ	32		;sectors per track
PSECLEN	equ	137		;physical sector length
CSECLEN	equ	128		;CP/M sector length
TRKLEN	equ	NUMSEC*PSECLEN
SYSDATA	equ	3		;offset of 128 byte data payload in sector
STRTSEC	equ	3		;starting sector of CP/M image on track 0

; CP/M size & locations

CCPLEN	equ	0800h		;CPM 2.2 fixed length
BDOSLEN	equ	0E00h		;CPM 2.2 fixed length

 if TBHIRAM
BIOSLEN	equ	600h		;BIOS length with track buffer at D000
 endif

 if not TBHIRAM
BIOSLEN	equ	1800h		;length of this BIOS
 endif

CCPBASE	equ	MEMSIZE*1024 - BIOSLEN - BDOSLEN - CCPLEN
BIOSBAS	equ	(CCPBASE+CCPLEN+BDOSLEN) ;base address of this BIOS

; Server serial port

SRVRCTL	equ	00h		;3P+S serial port for disk server
SRVRDAT	equ	01h
SOLSER	equ	0F8h		;serial port address in Sol-20

SRVRRDA	equ	40h		;receive data available bit
SRVRTBE	equ	80h		;transmit buffer empty bit

;--------------------------------------------------------------------------
;  Entry point
;--------------------------------------------------------------------------
	org	0

start	di			;disable interrupts
	lxi	sp,CCPBASE	;stack grows down from lowest CPM addres
	
	lda	0C000h		;SOLOS or CUTER?
	ora	a
	jnz	load		;CUTER, no changes needed

	mvi	a,SOLSER	;use Sol-20 serial port instead
	sta	waitSnd+1	;update control register access
	sta	srbLoop+1
	inr	a		;update data register access
	sta	rtOut+1
	sta	rtGarb+1
	sta	rtGarb+3
	sta	srbIn+1

; Load and move 1st track

load	call	srb3ms		;discard received bytes until 3ms passes
	jz	load		;   with no data received

	xra	a		;track 0
	sta	trkNum
	mvi	a,CSUMVAL AND 0FFh  ;re-init checksum
	sta	chkSum
	call	readTrk		;read the track
	jnz	load		;error
	
	lxi	h,trkBuf+(STRTSEC*PSECLEN)+SYSDATA   ;source pointer
	lxi	d,CCPBASE	;DE->where to put CP/M
	mvi	c,NUMSEC-3	;sector count to move

secLp1	call	movSec		;move 128 bytes to run location
	dcr	c		;decrement sector count		
	jnz	secLp1

; Load and move second track

	lxi	h,trkNum	;move to track 1
	inr	m
	lxi	h,chkSum	;update checksum
	inr	m
	call	readTrk		;read second track
	jnz	load		;error
	
	lxi	h,trkBuf+SYSDATA   ;source pointer
	lxi	d,CCPBASE+CSECLEN*(NUMSEC-3)	;dest pointer
	mvi	c,NUMSEC	;sector count to move

secLp2	call	movSec		;move 128 bytes to run location
	dcr	c		;decrement sector count		
	jnz	secLp2

	jmp	BIOSBAS		;jump to BIOS cold start

;--------------------------------------------------------------------------
; movSec - Move 128 byte sector from trkBuf to RAM
;
; On Entry
;    HL->data portion of sector in trkBuf
;    DE->where to put sector
;
; On Exit
;    HL and DE point to next sector 
; Clobbers B,D,E,H,L
;----------------------------------------------------------------------------
movSec	mvi	b,CSECLEN	;B=count of bytes to move

msLoop	mov	a,m		;move sector from trkBuf to run location
	stax	d
	inx	h		;increment pointers
	inx	d
	dcr	b		;loop count
	jnz	msLoop
 	
	push	b		;increment source pointer
	lxi	b,PSECLEN-CSECLEN
	dad	b
	pop	b
	ret

;----------------------------------------------------------------------------
; readTrk- read full track into track buffer
;
; On Entry
;    trkNum = track to read
;
; On Exit
;    track read into trkBuf
;    Clobbers all
;----------------------------------------------------------------------------
readTrk	lxi	h,readCmd	;send read command to server
	mvi	b,CMDLEN	;B=count of bytes to send

waitSnd	in	SRVRCTL		;loop until OK to send
	ani	SRVRTBE	
	jz	waitSnd

	mov	a,m		;A=byte to send
rtOut	out	SRVRDAT		;send it

	inx	h		;point to next byte
	dcr	b		;decrement byte count
	jnz	waitSnd		;loop until command is sent

rtGarb	in	SRVRDAT		;clear out any possible garbage bytes
	in	SRVRDAT

	lxi	h,trkBuf	;receive track data into trkBuf
	lxi	d,TRKLEN	;DE=length to read
	lxi	b,0		;BC=checksum

rtLoop	call	srByte		;get a byte
	rnz			;timeout error
	mov	m,a		;save byte in the buffer

	add	c		;update checksum
	mov	c,a
	mvi	a,0
	adc	b
	mov	b,a

	inx	h		;increment buffer pointer
	dcx	d		;decrement byte counter
	mov	a,d
	ora	e
	jnz	rtLoop		;loop until zero

; Receive and compare 16 bit checksum

	call	srByte		;get LSB of checksum
	rnz			;timeout
	cmp	c		;match?
	rnz			;no
		
	call	srByte		;get MSB of checksum
	rnz			;timeout
	cmp	b		;match?
	ret			;return with status

;-------------------------------------------------------------------
; srByte - Receive a byte from the server with 1 second timeout.
;    Returns zero false for timeout, else true.
;
; srb3ms does the same thing but with a 3ms timeout
;-------------------------------------------------------------------
srb3ms	push	b		;popped later, so must push
	lxi	B,118		;3ms timeout
	jmp	srbLoop

srByte	push	b		;preserve BC
	lxi	b,39216		;1 second (48 cycles, 24us per loop)
	
srbLoop	in	SRVRCTL		;(10)
	ani	SRVRRDA		;(7)
	jnz	srbNew		;(10)

	dcx	b		;(5)
	mov	a,b		;(5)
	ora	c		;(4)
	jnz	srbLoop		;(10)

	pop	b
	inr	a		;zero false for timeout
	ret

srbNew	pop	b		;restore BC
	xra	a		;zero true
srbIn	in	SRVRDAT		;get and return the byte
	ret

;-------------------------------------------------------------------
; srvrClr - Pull and discard bytes received from server until no
;    more bytes are received. A 3ms timeout between characters is 
;    used to detect end of data. This gives plenty off headroom 
;    down to 9600 baud. 
;-------------------------------------------------------------------
srvrClr	push	b		;srbLoop pops B
	lxi	b,118		;3ms timeout
	call	srbLoop		;discard received bytes
	jz	srvrClr		;  until timeout
	ret

;---------------------------------------------------------------------------
; Data Area
;---------------------------------------------------------------------------
CSUMVAL	equ	'R'+'E'+'A'+'D'+(TRKLEN/256)+(TRKLEN AND 0FFh)

readCmd	db	'READ'		;read command to server
trkNum	db	0		;track number to read
drvNum	db	0
readLen	dw	TRKLEN
chkSum	dw	CSUMVAL
CMDLEN	equ	$-readCmd

trkBuf	equ	$		;track buffer

	end
