;--------------------------------------------------------------------------
;
;	CP/M 2.2 BIOS for North Star Single Density Controller
;	Console I/O uses the Lifeboat "User Area" concept at
;	the end of the BIOS.
;
;	This BIOS uses full track buffering on both reads and writes to
;	improve performance. Verify after write is always performed.
;	Even with full write verify, this BIOS performs 20%-40% faster
;	than the non-buffered CP/M 1.4 BIOS
;
;	MODE byte (BIOS flags)
;		0036h from start of BIOS
;		1636h from start of CP/M
;		2036h in memory after loading or running MOVCPM
;		1736h offset in the disk image file
;
;	USER area for console, reader, punch, list I/O
;		0500h from start of BIOS
;		1B00h from start of CP/M
;		2500h in memory after loading or running MOVCPM
;		1C00h offset in the disk image file
;			
;	IOBYTE 	001Dh from start of USER area
;
;	Version History
;
;	Ver	  Date		     Description
;	---	----------	---------------------
;	1.0 	11/09/2015	Original, Mike Douglas
;
;	1.1	11/12/2015	Move console I/O to the end of the BIOS
;				at a 100h boundary and implement the 
;				jump table at that location like done
;				with the user area in Lifeboat CP/M.
;
; 	1.2	11/14/2015	Add delays in write sector (dWrite) to
;				work with 4mhz processors
;
;	1.3	11/16/2015	Update track table from curDrv (drive
;				actually selected) instead of from drvNum
;				(CP/M drive) to support raw I/O.
;
;	1.4	12/16/2015	Use 1-indexed sector numbers in the BIOS
;				setsec entry instead of 0-indexed.
;
;--------------------------------------------------------------------------

;  CPM location equates

	 maclib	cpmsize		;bring in memory and BIOS size
BDOSENT	 equ	(CCPBASE+CCPLEN+6)	 ;entry address of BDOS
BIOSBASE equ	(CCPBASE+CCPLEN+BDOSLEN) ;base address of this BIOS

; CPM page zero equates

WBOOTV	equ	000h		;warm boot vector location
BDOSV	equ	005h		;bdos entry vector location
CDISK	equ	004h		;CPM current disk
DEFDMA	equ	080h		;default dma address
IOBYTE	equ	003h		;CPM IOBYTE

; Disk information equates

NUMDISK	equ	3		;up to three drives in use
NUMTRK	equ	35		;number of tracks
PNUMSEC	equ	10		;physical number of sectors
NUMSEC	equ	20		;number of logical (128 byte) sectors per track
PSECLEN	equ	256		;physical sector length
SECLEN	equ	128		;CP/M sector length
DSM	equ	79		;max block number (80 blocks of 1024 bytes)
DRM	equ	63		;max directory entry number (64 entries) 
CKS	equ	(DRM+1)/4	;directory check space
UNTRACK	equ	59h		;unknown track number (59h is N* convention)
RDTRIES	equ	5		;number of read (verify) attempts
WRTRIES	equ	4		;number of write attempts

; misc equates

CR	equ	13		;ascii for carriage return
LF	equ	10		;ascii for line feed

	org	BIOSBASE
;-----------------------------------------------------------------------------
;
;  BIOS Entry Jump Table
;
;-----------------------------------------------------------------------------
	jmp	boot		;cold start
wboote	jmp	wboot		;warm start
	jmp	uconst		;console status (user)
	jmp	conin		;console character in (here, then user)
	jmp	uconout		;console character out (user)
	jmp	ulist		;list character out (user)
	jmp	upunch		;punch character out (user)
	jmp	ureader		;reader character in (user)
	jmp	home		;move head to home position
	jmp	seldsk		;select disk
	jmp	settrk		;set track number
	jmp	setsec		;set sector number
	jmp	setdma		;set dma address
	jmp	read		;read disk
	jmp	write		;write disk
	jmp	ulistst		;return list status (user)
	jmp	sectran		;sector translate
	jmp	rawIo		;non-CP/M entry to do raw disk I/O

;---------------------------------------------------------------------------
;  mode (MODE byte) - BIOS flags. 
;     Must remain at offset 36h from start of BIOS
;---------------------------------------------------------------------------
mode	db	010h		;enable interrupts after disk i/o is done

fCldCmd	equ	001h		;true = CCP process cmd on cold start
fWrmCmd	equ	002h		;true = CCP process cmd on warm start
fEnaInt	equ	010h		;enable interrupts after disk I/O

;  CPM Welcome Message

cpmStr	db	CR,LF,LF
cpmSize db	'59K CP/M 2.2b Ver 1.4',CR,LF
	db	'Single Density North Star FDC',CR,LF
	ds	64-($-cpmStr)	;adjust to make 64 byte stack
LCLSTK	equ	$		;local stack

;----------------------------------------------------------------------------
; boot - Cold boot BIOS entry. CPM is already loaded in memory. Hand
;    control to the CCP.
;----------------------------------------------------------------------------
boot	lxi	sp,0100h	;init stack pointer

	xra	a
	sta	CDISK		;current CPM disk is drive zero
	sta	cldDone		;clear cold start done flag
	sta	bfDirty		;clear track buffer dirty flag

	lxi	h,UNTRACK*256+UNTRACK	;invalidate track buffer
	shld	bfDrive		;bfDrive and bfTrack written

	shld	trkTbl		;initalize track table 
	shld	trkTbl+2	;and curDrv to an invalid drive

	call	ucoldst		;user area cold start initialization

; form the two digit CPM size in ascii (e.g., "60") for "60K")

	lxi	h,BIOSEND	;hl=end of BIOS address
	mov	a,h		;convert MSB of CPM marker to 1K count
	ani	0fch		;get bits for 1K inrcrements
	rar
	rar
	mvi	l,'0'		;start with ascii "0" for the 10's digit

tenCnt	sui	10		;divide by 10 to get the 10's digit
	jm	tenDone
	inr	l		;count the number of 10's in ascii
	jmp	tenCnt

tenDone adi	03ah		;compute final 1's digit in ascii
	mov	h,a		;h = 1's digit in ascii
	shld	cpmSize		;store two digit ascii size in welcome msg

;  Output the CPM welcome message

	lxi	h,cpmStr	;hl->cpm welcome message
	call	dispMsg		;display it

	jmp	entCpm		;enter CP/M

;----------------------------------------------------------------
; wboot - Warm boot BIOS entry. Reload CPM from disk up to, but
;    not including the BIOS. Re-enter CPM after loading.
;----------------------------------------------------------------
wboot	lxi	sp,0100h	;init stack pointer

	call	uwarmst		;user area warm start initialization

	xra	a		;disk zero is boot disk
	sta	drvNum

loadCpm	mvi	a,4		;load four sectors
	lxi	b,0001h		;track 0, drive 1 (CP/M drive 0)
	lxi	d,0001h		;sector 0, read command
	lxi	h,CCPBASE	;start of CP/M
	call	rawIo
	jnz	loadCpm	

	mvi	a,5		;load five sectors (skip sector 4)
	lxi	b,0001h		;track 0, drive 1
	lxi	d,0501h		;sector 5, read command
	lxi	h,CCPBASE+0400h
	call	rawIo
	jnz	loadCpm

	mvi	a,10		;load ten sectors
	lxi	b,0101h		;track 1, drive 1
	lxi	d,0001h		;sector 0, read command
	lxi	h,CCPBASE+0900h
	call	rawIo
	jnz	loadCpm

	mvi	a,3		;load three sectors (up to BIOS)
	lxi	b,0201h		;track 2, drive 1
	lxi	d,0001h		;sector 0, read command
	lxi	h,CCPBASE+1300h
	call	rawIo
	jnz	loadCpm
				;fall through to entCpm

; entCpm - enter CPM. Set page zero variables, enter CPM with or without
;   command line based on MODE flags.

entCpm	lxi	b,DEFDMA	;set the default dma address (080h)
	call	setdma

	mvi	a,0c3h		;8080 "jump" opcode
	sta	WBOOTV		;store in 1st byte of warm boot vecctor
	sta	BDOSV		;and 1st byte of bdos entry vecctor
	lxi	h,wboote	;get the warm boot address
	shld	WBOOTV+1	;and put it after the jump
	lxi	h,BDOSENT	;BDOS entry address
	shld	BDOSV+1		;put it after the jump opcode

	lda	CDISK		;get current disk number
	mov	c,a		;pass it to CPM in register c

	lxi	h,cldDone	;test then set the cold start flag
	mov	a,m
	ora	a
	mvi	m,1		;set the flag true - cold start has been done

	lda	mode		;a=mode byte
	jz	coldSt		;branch for cold vs warm start (tested above)

	ani	fWRMCMD		;warm start - run default command line?
	jmp	cmdChk

coldSt	ani	fCLDCMD		;cold start - run default command line?

cmdChk	jz	CCPBASE+3	;enter CCP and don't run command line
	jmp	CCPBASE		;enter CCP with possible cmd line

;----------------------------------------------------------------------------
; conin - BIOS console input call
;   This call for console input is used as an "idle" indicator and
;   is therefore a good time to flush a dirty track buffer. Even if
;   not flushed, the track buffer is invalidated to force a track
;   read on the next disk I/O request. This helps catch a disk swap.
;
;   After handling the disk flush and buffer invalidation, the user
;   console input routine is called.
;----------------------------------------------------------------------------
conin	mvi	a,0ffh		;invalidate track buffer
	sta	bfTrack

	lda	bfDirty		;is disk buffer dirty?
	ora	a
	jz	uconin		;no, jump to user console input routine

; flush the dirty track buffer. 

	push	b		;preserve registers
	push	d
	push	h

	call	wrtTrk		;flush the track

	pop	h		;restore registers
	pop	d
	pop	b
	
	lda	mode		;should we re-enable interrupts?		
	ani	fEnaInt
	jz	uconin		;no, 
	ei			;yes, re-enable inerrupts
	jmp	uconin

;----------------------------------------------------------------------------
; seldsk - Select Disk BIOS entry. C contains the disk number to select.
;    Validate the disk number and return a pointer to the disk parameter
;    header in HL. Zero is returned in HL for invalid drive number. The
;    disk number is stored in drvNum. No drive activity takes place.
;----------------------------------------------------------------------------
seldsk	lxi	h,0		;hl of zero is error flag
	mov	a,c		;a = drive number
	cpi	NUMDISK		;verify drive number less than number of disks
	rnc			;invalid drive number, return with hl=0=error

	mov	a,c		;save the selected disk number in drvNum
	sta	drvNum
	
	rlc			;compute disk parameter header address
	rlc			;   as (16*drvNum) + dpHead
	rlc
	rlc
	lxi	h,dpHead  	;start of disk parameter headers.
	mov	e,a
	mvi	d,0
	dad	d		;hl points to the DPH for the passed drive

	ret

;----------------------------------------------------------------------------
; home - Home BIOS entry. Set trkNum to zero. No drive activity takes place.
;----------------------------------------------------------------------------
home	xra	a		;set trkNum to zero
	sta	trkNum
	rts

;----------------------------------------------------------------------------
; settrk - Set Track BIOS entry. C contains the desired track number.
;    The track number is saved in trkNum for later use. No actual
;    drive activity takes place.
;----------------------------------------------------------------------------
settrk	mov	a,c
	sta	trkNum
	ret

;----------------------------------------------------------------------------
; setsec - Set Sector BIOS entry. C contains the desired sector number.
;    The sector number is saved in secNum for later use. No actual
;    drive activity takes place.
;----------------------------------------------------------------------------
setsec	mov	a,c
	dcr	a		;convert 1-indexed to 0-indexed
	sta	secNum
	ret

;----------------------------------------------------------------------------
; setdma - Set DMA BIOS entry. BC contains the address for reading or
;    writing sector data for subsequent I/O operations. The address is
;    stored in dmaAddr.
;----------------------------------------------------------------------------
setdma	mov	h,b
	mov	l,c
	shld	dmaAddr
	ret

;----------------------------------------------------------------------------
; sectran - Sector translation BIOS entry. Convert zero-indexed logical 
;    sector number in BC to a 1-indexed sector number in HL. No interleave
;    is required since this BIOS uses track buffering.
;----------------------------------------------------------------------------
sectran	mov	h,b		;copy bc to hl
	mov	l,c
	inr	l		;convert to 1-indexed
	ret

;----------------------------------------------------------------------------
; read - Read sector BIOS entry. Read one sector using the trkNum, secNum
;    and dmaAddr specified for drvNum. Returns 0 in A if successful, 1 in A
;    if a non-recoverable error has occured.
;----------------------------------------------------------------------------
read	call	swapStk		;swap to local stack
	call	ckFlush		;see if track buffer should be flushed
	jnz	exitDio		;track flush error

	call	trkRead		;fill track buffer if needed
	jnz	exitDio		;track read error
	
; move sector from proper offset in trkBuf to (dmaAddr)

	lhld	dmaAddr		;hl=(dmaAddr)
	xchg			;de=(dmaAddr)
	call	sec2Adr		;hl=address of sector within trkBuf
	call	moveH2D		;move sector to (dmaAddr)
	jmp	exitDio		;disk i/o exit routine

;----------------------------------------------------------------------------
; write - Write sector BIOS entry. Write one sector using the trkNum, secNum
;    and dmaAddr specified for drvNum. Returns 0 in A if successful, 1 in A
;    if a non-recoverable error has occured.
;----------------------------------------------------------------------------
write	call	swapStk
	call	ckFlush		;see if track buffer should be flushed
	jnz	exitDio		;error

	call	trkRead		;fill track buffer if needed
	jnz	exitDio		;track read error, exit

; move sector from (dmaAddr) to proper offset in trkBuf

	mvi	a,0ffh		;set buffer dirty flag true
	sta	bfDirty

	call	sec2Adr		;hl=address of sector within trkBuf
	xchg			;de=address of sector within trkBuf
	lhld	dmaAddr		;hl=(dmaAddr)
	call	moveH2D		;move (dmaAddr) to proper spot in trkbuf

;  exitDio - exit disk i/o. Restore interrupts if enable interrupt flag is
;     set in the MODE byte. 

exitDio	push	psw		;save result of read or write
	lda	mode		;should we re-enable interrupts?		
	ani	fEnaInt
	jz	noInts		;no
	ei			;yes, re-enable inerrupts

noInts	pop	psw		;restore result of read or write
	mvi	a,0		;if zero status is true, return zero
	rz

	mvi	a,1		;return fail code of one
	ora	a		;set non-zero status
	ret

;------------------------------------------------------------------------
; trkRead - read full track into track buffer if the requested 
;    drive (drvNum) or track (trkNum) do not match the buffered
;    drive (bfDrive) or buffered track (bfTrack). 
; On Entry:
;    drvNum, trkNum hold drive and track for the requested read
; On Exit:
;    trkNum on drvNum read into trkBuf
;    Z set if track read, Z cleared if error
;    clobbers all
;------------------------------------------------------------------------
trkRead	lxi	h,bfDrive	;hl->buffered drive number
	lda	drvNum		;a=requested drive
	cmp	m		;same drive buffered?

	inx	h		;hl->buffered track number
	jnz	newBuf		;drive doesn't match, need a new buffer

	lda	trkNum		;a=requested track
	cmp	m		;same track buffered?
	rz			;yes, already have this buffer

; newBuf - new buffer (full track) needed

newBuf	mvi	a,0ffh		;invalidate buffered track and drive
	mov	m,a		;bfTrack
	dcx	h
	mov	m,a		;bfDrive

	di			;protect from interrupts
	call	dSelect		;select drvNum
	call	dSeek		;seek to trkNum
	rnz			;seek error

	xra	a
	sta	rtStep		;current retry step 
	mvi	d,PNUMSEC	;d=sector counter

; secLoop - Read an entire track. Start at whatever sector comes up next

secLoop	call	dNxtSec		;wait for next (any) sector sync
	lxi	h,trkBuf	;hl->track buffer
	dad	b		;hl->sector within trkBuf
	call	dRead		;read the sector
	jnz	secRtry		;error, go to retry logic

nxtSec	dcr	d		;decrement sector count
	jnz	secLoop

; Track read successful. Update bfDrive and bfTrack with the drive and
;    track number present in trkBuf and exit.

trkGood	lxi	h,bfDrive	;hl-> buffered drive number
	lda	drvNum		;a=drive number just read
	mov	m,a		;store in bfDrive

	inx	h		;hl->buffered track number
	lda	trkNum		;a=track just read
	mov	m,a		;store in bfTrack

	xra	a		;return success code
	ret

;--------------------------------------------------------------------------
; secRtry - read error retry logic. Three retry steps are tried in
;   the following order:
;
;   Step    Action
;    0	Re-read RDTRIES times
;    1	Seek in one track and back, then re-read RDTRIES times
;    2	Seek out to track 0 and back, then re-read RDTRIES times
;--------------------------------------------------------------------------	
secRtry	mvi	a,RDTRIES 	;init disk retry counter
	sta	rdRetry

retryLp	call	dWtSec		;wait for sector specified in e
	lxi	h,trkBuf	;hl->track buffer
	dad	b		;hl->sector within trkBuf
	call	dRead		;read the sector
	jnz	nextTry		;read failed, try again

	xra	a		;retry worked, restart retry state
	sta	rtStep
	jmp	nxtSec		;back to main sector loop

; nextTry - Decrement retry count and try again if not zero. Once
;   retry counter expires, increment the step counter to try the
;   next retry stage.

nextTry	lxi	h,rdRetry	;decrement retry counter
	dcr	m
	jnz	retryLp		;try again

; read re-tries expired, try next retry step

	lxi	h,rtStep	;move to next retry step
	inr	m

	mov	a,m		;a=new retry step
	dcr	a		;step 1, seek past track and come back
	jz	skPast

	dcr	a		;step 2 restore to track zero and back
	jz	restor0
	
	ret			;all retry steps done, abort with Z false

;-----------------------------------------------------------------------------
; restor0 - retry step to restore to track zero, then seek back to
;    the current track.
;
; skPast - retry step to seek past the current track, then back to
;    the current track.
;-----------------------------------------------------------------------------
restor0	push	d		;save sector count and number
	call	dRestor		;restore to track zero

reseek	call	dSeek		;seek back to trkNum
	pop	d		;restore sector in e
	jmp	secRtry		;try again

skPast	lda	trkNum		;a=current track
	cpi	NUMTRK-1	;at max track?
	jz	restor0		;yes, just try restore again

	push	d		;save sector count and number
	inr	a		;step in a track
	call	dSeekA		;seek track in A
	jmp	reseek		;then back out


;-------------------------------------------------------------------
; ckFlush - Check if track buffer should be flushed. This
;    function must be called before any drive selection or seek
;    operation. The buffer is flushed if the bfDirty flag is
;    set and the drive or track number are different than the
;    buffered track.
; On Entry:
;    drvNum, trkNum updated for the new I/O call
; On Exit:
;    Z set for no error, Z cleared if write error occured.
;    Clobbers all
;-------------------------------------------------------------------
ckFlush	lda	bfDirty		;see if track buffer is dirty
	ora	a
	rz			;no, exit with Z set

	lxi	h,bfDrive	;hl->buffered drive number
	lda	drvNum		;a=requested drive number
	cmp	m		;same?
	jnz	wrtTrk		;drive doesn't match, flush

	inx	h		;hl->buffered track number
	lda	trkNum		;a=requested track number
	cmp	m		;same?
	rz			;yes, no need to flush

; wrtTrk - track buffer needs to be written

wrtTrk	xra	a		;clear buffer dirty flag
	sta	bfDirty

	mvi	a,WRTRIES	;init write retry counter
	sta	wrRetry

	di			;in case flush called from conin
	lda	bfDrive		;select drive that is buffered
	call	dSlctA

rtryWrt	mvi	d,PNUMSEC	;d=count of sectors to write

; wrtLoop - write a full track starting with any sector

wrtLoop	call	dNxtSec		;wait for next (any) sector sync
	lxi	h,trkBuf	;hl->start of track buffer
	dad	b		;bc has offset of sector within track
	call	dWrite		;write the sector
	dcr	d		;decrement sector count
	jnz	wrtLoop		;loop until all sectors written

; Verify the track just written

	call	dNxtSec		;force a 1 sector delay for trim erase
	mvi	d,PNUMSEC	;d=count of sectors to verify

vfyLoop	call	dNxtSec		;wait for next (any) sector sync
	lxi	h,trkBuf	;hl->start of track buffer
	dad	b		;bc has offset of sector within track
	call	dVerify		;verify the sector
	jnz	vfyRtry		;error, go to retry logic

vfyNext	dcr	d		;decrement sector count
	jnz	vfyLoop		;loop until all sectors verified
	ret			;track write successful

; vfyRtry - verify failed, retry reads followed by a re-write
;   of the track in needed.

vfyRtry	mvi	a,RDTRIES	;init retry counter
	sta	rdRetry

vRtryLp	call	dWtSec		;wait for sector specified in e
	lxi	h,trkBuf	;hl->start of track buffer
	dad	b		;bc has offset of sector within track
	call	dVerify		;verify the sector
	jz	vfyNext		;success, go verify next sector

; Re-verify failed. Decrement retry count and try again if not zero. Once
;   retry counter expires, do another write.

	lxi	h,rdRetry	;decrement the read retry counter
	dcr	m
	jnz	vRtryLp		;try read for verify again

	lxi	h,wrRetry	;decrement the write retry counter
	dcr	m
	jnz	rtryWrt		;retry starting with the write

; All retries failed. Display error message since CP/M won't in all
;    situations

	lxi	h,mWrtErr	;hl->"Buffered Write Failure"
	call	dispMsg		;a is zero on return

	dcr	a		;force a non-zero status
	ret	

mWrtErr	db	CR,LF,'Buffered write error', CR, LF, 0

;----------------------------------------------------------------------------
; rawIo - Perform direct I/O with disk, bypassing normal CP/M disk I/O.
;    The disk track buffer is flushed (if required) and invalidated prior
;    to performing the raw I/O request.
;
; On Entry:
;    A=number of sectors
;    B=track
;    C=drive (North Star 1 indexed, not CP/M 0 indexed)
;    D=starting sector
;    E=command (0=write, 1=read, 2=verify)
;    HL=buffer address
; On Exit:
;    Z set on success, clear on failure
;    clobbers all
;----------------------------------------------------------------------------
; First, save passed parameters

rawIo	shld	rawAddr		;rawAddr=hl
	lxi	h,rawCnt	;hl->rawCnt
	mov	m,a		;rawCnt=a
	inx	h
	mov	m,b		;rawTrk=b
	inx	h
	dcr	c		;1-indexed drive to 0-indexed drive
	mov	m,c		;rawDrv=c
	inx	h
	mov	m,d		;rawSec=d
	inx	h
	mov	m,e		;rawCmd=e

; flush track buffer from normal CP/M disk I/O

	call	swapStk		;swap to local stack

	mvi	a,0ffh		;invalidate track buffer
	sta	bfTrack

	call	ckFlush		;see if track buffer should be flushed
	jnz	exitDio		;track flush error

; select drive and seek to specified track
	
	lda	rawDrv		;a=drive
	di
	call	dSlctA		;select drive in A

	lda	rawTrk		;a=track
	call	dSeekA		;seek track in A
	call	dNxtSec		;skip one sector in case write to read

; perform the requested disk I/O

	lda	rawCnt
	mov	d,a		;d=number of sectors to process
	lhld	rawAddr		;hl->buffer specified
	lda	rawSec
	mov	e,a		;e=starting sector

doRaw	call	dWtSec		;wait for sector e
	lda	rawCmd		;a=command
	dcr	a		;write=-1, read=0, verify=1
	jm	rawWrt		;write
	jnz	rawVfy		;verify
	
; raw read

	call	dRead		;read a sector
	jnz	exitDio		;error
	jmp	nxtRaw

; raw write

rawWrt	call	dWrite		;write a sector
	jmp	nxtRaw

; raw verify

rawVfy	call	dVerify		;verify a sector
	jnz	exitDio		;error

nxtRaw	inr	e		;increment sector number
	dcr	d		;decrement sector count
	jnz	doRaw		;loop until done

	jmp	exitDio		

;--------------------------------------------------------------------------
; swapStk - switch to local stack. When the caller of this routine
;   does a ret, the return jumps to "resStk" below which restores
;   the original stack pointer.
;--------------------------------------------------------------------------
swapStk	pop	d		;de = return address. Return address popped

	lxi	h,0
	dad	sp		;hl = stack pointer after return addr popped

	lxi	sp,LCLSTK	;use LCLSTK as the new stack 

	push	h		;save original stack pointer

	lxi	h,resStk	;"ret" to this level will go to resStk, below
	push	h

	xchg			;put original return address in HL
	pchl			;return to caller

resStk	pop	h		;HL = original stack pointer from CP/M
	sphl			;put original stack pointer back
	ret			;return to caller of swapStk caller

;------------------------------------------------------------------------------
; savTNum - save track number in track table
; On Entry:
;    a=track number to save
;    drvNum=current drive number
; On Exit:
;    trkTbl[drvNum]=a
;    clobbers none
;------------------------------------------------------------------------------
savTNum	push	h		;preserve hl, bc
	push	b
	push	a		;save track number

	lxi	h,trkTbl	;hl->track table
	mvi	b,0		;form bc=drive number
	lda	curDrv
	mov	c,a
	dad	b		;hl->track number spot for drvNum
	pop	a		;a=track number to save
	mov	m,a		;store track number in trkTbl

	pop	b
	pop	h
	ret

;------------------------------------------------------------------------------
; getTNum - read track number from track table
; On Entry:
;    drvNum=current drive number
; On Exit:
;    a=trkTbl[drvNum]
;    clobbers none
;------------------------------------------------------------------------------
getTNum	push	h		;preserve hl, bc
	push	b

	lxi	h,trkTbl	;hl->trkTble
	mvi	b,0		;form bc=drive number
	lda	curDrv
	mov	c,a
	dad	b		;hl->track number spot for drvNum
	mov	a,m		;a=current track for drvNum

	pop	b
	pop	h
	ret

;------------------------------------------------------------------------------
; moveH2D - move 128 bytes from (hl) to (de).
; On Entry:
;    hl->source buffer
;    de->destination buffer
; On Exit:
;   Z flag set
;   Clobbers b. de and hl updated as buffer moved.
;------------------------------------------------------------------------------
moveH2D	mvi	b,SECLEN	;b = buffer length (128 bytes)

movLoop	mov	a,m		;move from (hl) to (de)
	stax	d

	inx	d		;increment both pointers
	inx	h

	dcr	b		;decrement character count
	jnz	movLoop		;loop until count = 0

	ret

;-------------------------------------------------------------------------
; sec2Adr - Sector number to buffer address
; On Entry:
;    secNum = CP/M sector number (0-19)
; On Exit:
;    hl->sector within trkBuf
;    clobbers b,c,h,l
;-------------------------------------------------------------------------
sec2Adr	xra	a		;clear carry
	lda	secNum		;a=requested sector
	rar			;offset = 128*sector = (256*sector) >> 1
	mov	b,a		;msb of offset
	rar
	ani	80h
	mov	c,a		;lsb of offset

	lxi	h,trkBuf	;hl->start of track buffer
	dad	b		;hl->requested sector in track buffer

	ret

;-----------------------------------------------------------------------
; dispMsg - display null terminated string on console device
; On Entry: hl->message
; On Exit:
;   clobbers c,hl
;-----------------------------------------------------------------------
dispMsg	mov	a,m		;next character
	ora	a		;test for null terminator
	rz			;done

	mov	c,a		;character to send in c
	push	h		;save hl in case BIOS doesn't
	call	uconout
	pop	h

	inx	h		;point to next character
	jmp	dispMsg


;************************************************************************
;
;  North Star disk driver routines (single density controller)
;
;************************************************************************

; Disk Controller Addresses & Commands
;   (Read from address to execute command)

NSCTRL	equ	0E800h		;Northstar disk controller
NSBOOT	equ	NSCTRL+100h	;disk boot PROM entry point
NWRITE	equ	NSCTRL+200h	;write prefix (data in low byte)
NCOMND	equ	NSCTRL+300h	;commands base address

NASTAT	equ	NCOMND+10h	;read A status
NBSTAT	equ	NCOMND+30h	;read B status
NSTRST	equ	NCOMND+09h	;set track step flip flop
NCTRST	equ	NCOMND+08h	;clear track step flip flop
NSTOUT	equ	NCOMND+1Ch	;set step direction to 'out'
NSTIN	equ	NCOMND+1Dh	;set step direction to 'in'
NMOTRA	equ	NCOMND+90h	;motor on, read A status
NRBYTE	equ	NCOMND+50h	;read a byte of disk data
NWRCRD	equ	NCOMND+04h	;start write-sector sequence
NRSEFL	equ	NCOMND+94h	;reset sector flag, restart motor timeout

; Disk Controller Status Bits

NSTR0	equ	1		;NASTAT: track 0
NSWP	equ	2		;NASTAT: write protect
NSBDY	equ	4		;NASTAT: body (sync chr found)
NSWRT	equ	8		;NASTAT: write ready
NSMO	equ	10h		;NASTAT & NBSTAT: motor on
NSWN	equ	40h		;NASTAT & NBSTAT: window flag
NSSF	equ	80h		;NASTAT & NBSTAT: sector flag
NSSP	equ	0Fh		;NBSTAT: sector position

; Disk Controller constants

SYNC	equ	0FBh		;header sync charactor
SPINUP	equ	50		;motor spin-up time in sectors
SETL	equ	13		;index sync time in sectors
SWAIT	equ	08Ch		;max wait for body before error


;--------------------------------------------------------------------
; dRead - Read sector on current track.
; On Entry:
;    Drive is selected, motor is running and sector
;        sync was just previously detected
;    hl->sector buffer
; On Exit:
;    z flag set on good read, cleared for error
;    clobbers b,c,h,l
;--------------------------------------------------------------------
;Wait for sector body

dRead	mvi	b,SWAIT		;max time until body

dRead1	dcr	b
	jz	dNoSync		;sync not found

	lda	NASTAT		;LOOK for sync chr
	ani	NSBDY
	jz	dRead1		;keep looking

	lxi	b,0		;initial CRC and sector length

;read sector data

dRead2	lda	NRBYTE		;read a byte
	mov	m,a		;save it

	xra	b		;compute checksum
	rlc
	mov	b,a

	inx	h		;bump pointers
	dcr	c		;256 bytes
	jnz	dRead2

;check CRC

	lda	NRBYTE		;get disk CRC
	xra	b		;compare to computed CRC
	ret			;Z flag set if OK, else error

dNoSync	dcr	b		;b=0ffh
	ret			;return with zero false

;--------------------------------------------------------------------
; dVerify - Verify sector on current track
; On Entry:
;    Drive is selected, motor is running and sector
;        sync was just previously detected
;    hl->buffer to compare against
; On Exit:
;    z flag set on good verify, cleared for error
;    clobbers b,c,h,l
;--------------------------------------------------------------------
;Wait for sector body

dVerify	mvi	b,SWAIT		;max time until body

dVfy1	dcr	b
	jz	dNoSync		;sync not found

	lda	NASTAT		;LOOK for sync chr
	ani	NSBDY
	jz	dVfy1		;keep looking

	lxi	b,0		;initial CRC and sector length

;read and verify sector data

dVfy2	lda	NRBYTE		;read a byte
	cmp	m		;compare to validate data
	rnz			;match fail

	xra	b		;compute checksum
	rlc
	mov	b,a

	inx	h		;bump pointer
	dcr	c		;256 bytes
	jnz	dVfy2

;check CRC

	lda	NRBYTE		;get disk CRC
	xra	b		;compare to computed CRC
	ret			;Z flag set if OK, else error

;--------------------------------------------------------------------
; dWrite - Write a sector on current track
; On Entry:
;    Drive is selected, motor is running and sector
;        sync was just previously detected
;    hl->buffer address
; On Exit:
;   clobbers b,c,h,l
;--------------------------------------------------------------------
dWrite	push	d		;save de 
	lda	NWRCRD		;prepare to write

dWrite1	lda	NASTAT		;wait for controller
	ani	NSWRT		;..to get ready to write.
	jz	dWrite1

;write sector header

	lxi	b,NWRITE	;C=0,b=write prefix
	lxi	d,000Fh		;number of header zeros in e,
				;  init CRC in d

dWrite2	ldax	b		;write header zeros
	push	psw		;waste 21 cycles, need >8us between
	pop	psw		;two ldax b's even at 4mhz
	dcr	e
	jnz	dWrite2

;write sector sync character

	mvi	c,SYNC		;write sync chr
	ldax	b	
	nop			;ensure >8us at 4mhz to next ldax b 
	nop

;write sector data

dWrite3	mov	c,m		;c=next byte to write

	mov	a,c		;compute CRC in d
	xra	d
	rlc
	mov	d,a

	ldax	b		;write byte to disk

	inx	h		;bump pointers
	dcr	e		;write 256 bytes
	jnz	dWrite3

;write sector CRC

	mov	c,d		;write CRC
	nop			;ensure >8us at 4mz between ldax b
	nop
	ldax	b
	
	pop	d		;restore de
	ret

;--------------------------------------------------------------------
; dSeek,dSeekA - Seek Track. The motor timeout is restarted 
;    as a result of this call.
; On Entry:
;    drive selected
;    trkTbl[drvNum] = current track, UNTRACK means we don't know
;    trkNum = desired track (dSeekA expects track in A)
; On Exit:
;    Z set for successful seek and trkTbl[drvNum] updated
;    Z cleared otherwise
;    clobbers all registers
;--------------------------------------------------------------------
dSeek	lda	trkNum		;put track number in l
dSeekA	mov	l,a

	call	getTNum		;a=current track number of drvNum
	cpi	UNTRACK
	jnz	dSeek1

	call	dRestor		;lost: restore
	rnz			;failed, exit with error

;compute the required number of steps and the direction

dSeek1	sub	l		;which way to step?
	rz			;already there?

	lxi	d,NSTOUT	;desired<current
	jp	dSeek2

	cma
	inr	a		;a:=-a
	lxi	d,NSTIN		;desired>current

;save target track in trkTbl, then step there

dSeek2	mov	h,a		;h=number of steps

	mov	a,l		;save target track in trkTbl
	call	savTNum

	call	dStep		;go step h tracks
	
	xra	a		;return with Z set
	ret

;--------------------------------------------------------------------
; dRestor - Restore to track 0
; On Entry: drive selected
; On Exit:
;   Z set and trkTbl[drvNum]=0 if track 0 found
;   Z cleared for error (track 0 not found or stuck there)
;   clobbers b,c,h
;--------------------------------------------------------------------
dRestor lxi	d,NSTIN		;step in once in case head lost
	mvi	h,1
	call	dStep		;step in now
	jz	dStuck0		;z: Stuck on track 0

;step out enough times that we should find track 0 even
;if the SA400's actuator is out of its spiral groove

	mvi	h,NUMTRK+16	;more than enough
	lxi	d,NSTOUT	;prepare to step out
	jmp	dStep		;try to step out to track 0

;Error: stuck on track zero

dStuck0	inr	a		;a was zero, force non-zero
	ret			;return with z cleared (error)

;--------------------------------------------------------------------
; dStep - Step head
; On Entry:
;   h = number of steps, >0
;  de = step command - NSTIN or NSTOUT
; On Exit:
;   Z set and trkTbl[drvNum]=0 if track 0 hit
;   Z cleared otherwise
;   clobbers bc,h
;--------------------------------------------------------------------
dStep	ldax	d		;set step direction

dStep1	lda	NSTRST		;Reset step flipflop
	xthl			;must be here for delay
	xthl			;at least 10 uSec

	mvi	b,2		;step timer
	lda	NCTRST		;set step flipflop

	call	dWait		;wait 2 sector times

	lda	NASTAT		;at track 0?
	ani	NSTR0
	jnz	dStep2		;yes, stop stepping

	dcr	h		;step again
	jnz	dStep1		;stepped enough?

	inr	a		;clear Z: not track 0
	ret

;on track 0. Save in trkTbl[drvNum], return with Z set.

dStep2	xra	a		;track 0
	call	savTNum		;save track number in trkTbl[drvNum]
	xra	a		;set Z, return zero
	ret

;--------------------------------------------------------------------
; dSelect,dSlctA - Select drive. Turn on the motor if necessary.
;    If motor started or different drive selected, wait for 13 
;    sectors to pass to ensure index sync has occured and the 
;    head is settled.
; On Entry:
;   curDrv = drive selected on the controller
;   drvNum = desired drive (0-2 corresponds to North Star 1-3)
;   or A = drive number (0-2) for entry dSlctA
; On Exit:
;   curDrv = drvNum
;   clobbers bc
;--------------------------------------------------------------------
dSelect	lda	drvNum
dSlctA	mov	c,a		;c=requested drive number
	
;if motor not on, start it and wait SPINUP sectors

	lda	NMOTRA		;turn on drive motors
	ani	NSMO		;already on?
	jnz	dsMotOn		;yes, motor already on

	mvi	b,SPINUP	;wait for motor spin-up time
	mov	a,b		;force curDrv to not match
	sta	curDrv
	call	dWait

;if a different drive is requested then currently selected, select
;   the new drive and wait SETL sectors to ensure index sync

dsMotOn	lda	curDrv		;a=current drive on FDC
	cmp	c		;compare to requested drive

	mov	a,c		;save curDrv=requested drive
	sta	curDrv

	rz			;exit if same drive already selected

	inr	c		;CP/M drive #0-2 -> 1-3 
	mvi	b,NCOMND/256	;b=MSB of command address
	ldax	b		;select drive in c, get A status

	mvi	b,SETL		;13 sector wait
				;fall into dWait

;--------------------------------------------------------------------
; dWait - Wait for b sectors
; On Entry:
;   b = number of sectors to wait for
; clobbers b
;--------------------------------------------------------------------
dWait	lda	NRSEFL		;reset sector flag, restart motor timeout

dWait1	lda	NASTAT		;get status a
	rlc			;test NSSF, sector flag set?
	jnc	dWait1		;no

	dcr	b		;wait for b sectors to pass
	rz

	jmp	dWait

;--------------------------------------------------------------------
; dWtSec - Wait for specified sector.
; On Entry:
;    Drive is selected, motor is running
;    e = desired sector number
; On Exit:
;    drive is at proper sector
;    e = sector number
;    bc = byte offset of this sector in a track
; clobbers b,c
;--------------------------------------------------------------------
dWtSec	lda	NRSEFL		;reset sector flag, restart motor timeout

dWtSec1	lda	NBSTAT		;a=sector flag and sector number
	rlc			;test NSSF, sector flag set?
	jnc	dWtSec1

; Sector flag set, get sector number alone and see if this
;   is the right sector

	lda	NBSTAT		;get sector again to be safe
	ani	NSSP		;get sector bits alone
	cmp	e		;the sector we want?
	jnz	dWtSec		;no, keep looking

; Compute the offset of this sector within the track and return in bc

	mov	b,a		;256 bytes per sector
	mvi	c,0
	ret 

;--------------------------------------------------------------------
; dNxtSec - Wait for next (any) sector
; On Entry:
;    Drive is selected, motor is running
; On Exit:
;    drive is at proper sector
;    e = sector number
;    bc = byte offset of this sector in a track
; clobbers b,c,e
;--------------------------------------------------------------------
dNxtSec	lda	NRSEFL		;reset sector flag, restart motor timeout

dNext1	lda	NBSTAT		;a=sector flag and sector number
	rlc			;test NSSF, sector flag set?
	jnc	dNext1

; Sector flag set, return the sector number in e and compute the offset
;    of this sector within the track.
	
	lda	NBSTAT		;get sector again to be safe
	ani	NSSP		;get sector bits alone
	mov	e,a		;return sector in e

	mov	b,a		;bc=offset of sector within track
	mvi	c,0
	ret


;******************************************************************************
;
; User I/O area. This area is patched by the end user to insert console,
;    reader, punch, and listing I/O for their hardware. This is very
;    similar to the user are as defined by Lifeboat. 
;
;    The code below is in the user area when an unconfigured disk is
;    booted or MOVCPM is run. The I/O performed by this code accesses
;    an Altair 88-2SIO board, but can easily be patched to get a
;    console up and running on another I/O board.
;
;    Note that the cold start init routine stalls in an endless loop
;    so that the computer can then be stopped and console I/O code
;    patched in memory. After patching, jump to the starting address
;    of the BIOS (cold start entry point). Do NOT reboot or patches
;    will be lost. MOVCPM, DDT, and SYSGEN can then be used to save
;    the patches to disk.
;
;******************************************************************************
	org	($ AND 0ff00h) + 0100h		;start on page boundary

USRAREA	equ	$		;start of user area

; 88-2SIO equates

CSRA	equ	10h		;port a control/status register
DRA	equ	11h		;port a data register
CSRB	equ	12h		;port b control/status register
DRB	equ	13h		;port b data register

RDRF	equ	1		;receive data bit mask
TDRE	equ	2		;transmit ready bit mask

; Misc equates

IOBYTE	equ	3		;location of CP/M IOBYTE

;----------------------------------------------------------------
; Jump Table
;    The BIOS accesses the user I/O routines through this jump
;    table. The table must remain at the start of the user area
;    and in the same order.
;----------------------------------------------------------------
	org	USRAREA

ucoldst	jmp	coldIni		;cold start init
uwarmst	jmp	warmIni		;warm start init
uconst	jmp	conStat		;BIOS console status call
uconin	jmp	conInp		;BIOS console input call
uconout	jmp	conOut		;BIOS console output call
ulist	jmp	conOut		;BIOS list output call
upunch	jmp	conOut		;BIOS punch output call
ureader	jmp	conInp		;BIOS reader input call
ulistst	jmp	lstStat		;BIOS list status call

; The next 8 bytes may be accessed externally and must remain in the
;     same order and immediately follow the jump table

usrLen	dw	USREND-USRAREA	;Length of USER AREA
defIob	db	00h		;LST = TTY (2SIO port A)
				;PUN = TTY (2SIO port A)
				;RDR = TTY (2SIO port A)
				;CON = TTY (2SIO port A)
	db	0,0,0,0,0	;reserved

;-----------------------------------------------------------------------
; coldIni - cold start initialization
;   This entry is called during cold boot.
;-----------------------------------------------------------------------
coldIni	jmp	coldIni		;stall here for initial I/O patching
	
	mvi	a,3		;reset ACIA
	out	CSRA		;port a
	out	CSRB		;port b

	mvi	a,11h		;8N1, /16
	out	CSRA
	out	CSRB

	lda	defIob		;store default IOBYTE for CP/M
	sta	IOBYTE

	ret

	db	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0		;patch space

;-----------------------------------------------------------------------
; warmIni - warm start initialization
;   This entry is called during each warm boot.
;-----------------------------------------------------------------------
warmIni	ret
	db	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0		;patch space


;-----------------------------------------------------------------------
; conStat - console status check
;-----------------------------------------------------------------------
conStat	lda	IOBYTE		;select device based on IOBYTE 1:0
	call	devSel

	dw	statusA		;TTY
	dw	statusA		;CRT
	dw	statusA		;BAT
	dw	statusA		;UC1

;-----------------------------------------------------------------------
; conInp - console input
;-----------------------------------------------------------------------
conInp	lda	IOBYTE		;select device based on IOBYTE 1:0
	call	devSel

	dw	inputA		;TTY
	dw	inputA		;CRT
	dw	inputA		;BAT
	dw	inputA		;UC1

;-----------------------------------------------------------------------
; conOut - console output
;-----------------------------------------------------------------------
conOut	lda	IOBYTE		;select device based on IOBYTE 1:0
	call	devSel

	dw	outputA		;TTY
	dw	outputA		;CRT
	dw	outputA		;BAT
	dw	outputA		;UC1

;-----------------------------------------------------------------------
; lstStat - list status not supported. Return zero per CP/M spec.
;-----------------------------------------------------------------------
lstStat	xra	a
	ret

;-----------------------------------------------------------------------
; devSel - jump to proper I/O routine based on device in A
;-----------------------------------------------------------------------
devSel	xthl			;hl->table, old hl saved on stack
	push	d		;de and hl saved on stack

	ani	3		;get 2 bit device selection alone
	rlc			;two bytes per table entry
	mov	e,a		;de=offset into table
	mvi	d,0
	dad	d		;hl->table[device]

	mov	e,m		;load routine address into de
	inx	h
	mov	d,m		;de->address of I/O routine 

	xchg			;hl->address of I/O routine
	pchl			;jump to I/O routine

;-----------------------------------------------------------------------
; statusA - return status of 2SIO port A.
;    Return 0FFh if character ready, zero if not
;-----------------------------------------------------------------------
statusA	pop	d		;restore de, hl
	pop	h

	in	CSRA		;a=port a status
	ani	RDRF		;read data present?
	rz			;no, exit with zero status

	mvi	a,0ffh		;character is ready
	ret

;-----------------------------------------------------------------------
; inputA - input character from 2SIO port A.
;-----------------------------------------------------------------------
inputA	pop	d		;restore de, hl
	pop	h

inALoop	in	CSRA		;a=port a status
	ani	RDRF		;read data present?
	jz	inALoop		;no

	in	DRA		;a=data from serial port
	ani	07fh		;strip parity
	ret


;-----------------------------------------------------------------------
; outputA - output character in C through 2SIO port A.
;-----------------------------------------------------------------------
outputA	pop	d		;restore de, hl
	pop	h

outALp	in	CSRA		;a=port a status
	ani	TDRE		;transmit ready?
	jz	outALp		;no

	mov	a,c		;a=byte to send
	out	DRA		;send character

	ret


USREND	equ	$		;end of user area


;******************************************************************************
;
;   BIOS Data Area
;
;******************************************************************************
	ds	384-($-USRAREA)		;reserve 384 bytes for user area

;-----------------------------------------------------------------------------
; dpHead - disk parameter header for each drive
;-----------------------------------------------------------------------------
dpHead	dw	0,0,0,0,dirBuf,dpb,csv0,alv0
	dw	0,0,0,0,dirBuf,dpb,csv1,alv1
	dw	0,0,0,0,dirBuf,dpb,csv2,alv2

;-----------------------------------------------------------------------------
; dpb - disk parameter block. This table gives a block size of 1024 bytes
;   and 64 directory entries. This matches North Star CP/M 1.4
;-----------------------------------------------------------------------------
dpb	dw	NUMSEC		;sectors per track
	db	3		;allocation block shift factor (BSH)
	db	07h		;data location block mask (BLM)
	db	0		;extent mask (EXM)
	dw	DSM		;maximum block number (DSM 79)	
	dw	DRM		;maximum directory entry number (DRM 63)
	db	0c0h,0		;AL0, AL1 
	dw	CKS		;CKS=(DRM+1)/4
	dw	3		;reserved tracks for CPM and bootloader

;-----------------------------------------------------------------------------
; uninitialized data
;-----------------------------------------------------------------------------
cldDone	ds	1		;set true (non-zero) after cold start

trkTbl	ds	3		;current track for each drive
curDrv	ds	1		;currently selected drive (must follow trkTbl)

drvNum	ds	1		;drive number from CP/M call
trkNum	ds	1		;track number from CP/M call
secNum	ds	1		;sector number from CP/M call
dmaAddr	ds	2		;dma address for disk operations from CP/M call

rdRetry	ds	1		;read retry counter
wrRetry	ds	1		;write retry counter
rtStep	ds	1		;retry step

; raw I/O data area. ITEMS MUST REMAIN IN THIS ORDER

rawCnt	ds	1		;count of sectors to transfer
rawTrk	ds	1		;track
rawDrv	ds	1		;drive
rawSec	ds	1		;starting sector
rawCmd	ds	1		;command (0=write, 1=read, 2=verify)
rawAddr	ds	2		;buffer address

;-----------------------------------------------------------------------------
; track buffering 
;-----------------------------------------------------------------------------
bfDrive	ds	1		;drive currently in track buffer
bfTrack	ds	1		;currently buffered track (must follow bfDrive)
bfDirty	ds	1		;track buffer needs to be written
trkBuf	ds	PNUMSEC*PSECLEN

;-----------------------------------------------------------------------------
;  Disk scratchpad areas defined in the DPH table
;-----------------------------------------------------------------------------
dirBuf	ds	128		;bdos directory scratchpad
alv0	ds	(DSM/8 + 1)	;allocation vector storage
csv0	ds	CKS		;change disk scratchpad
alv1	ds	(DSM/8 + 1)
csv1	ds	CKS
alv2	ds	(DSM/8 + 1)
csv2	ds	CKS

BIOSEND	equ	$+255		;round up to next 256 byte boundary 

	end

