;*****************************************************************************
;
;  Custom version for the Altair/Tarbell combo BIOS for which Tarbell
;      drives 0 & 1 are CP/M drives C & D. 11/30/19, M Douglas
;
;  COPY.ASM
;	Disk copy, verify, and format utility for CP/M on a Tarbell single
;	density controller with 8 inch drives. Only the format option is
;	Tarbell specific, but even that is easily adaptable to other 
;	controllers using the WD1771 or WD179x chips.
;
;	Rev	  Date	    Desc
;	1.0	2/09/16   M. Douglas, Original
;
;  	1.1	3/07/16   M. Douglas
;		   To prevent drive select errors on exit, always exit
;		   with a CP/M warm boot instead of returning directly
;		   to the CCP when the CCP wasn't clobbered.
;
;  	1.2	3/27/16   M. Douglas
;		   Display track and sector number for unrecoverable read
;		   errors. Do not abort with a read error. Write verify
;		   failures still abort. Disable interrupts during format.
;
;  	1.3	12/15/18   M. Douglas
;		   Add "GENERIC" assembly option which disables any feature
;		   that directly accesses the disk controller (e.g., write
;		   protect checking outside the BIOS and the FORMAT option).
;		   2-1 interleave is used in GENERIC mode since most BIOS's
;		   can't keep up with consecutive sector read/write.
;
;*****************************************************************************

GENERIC	equ	0		;1=disable controller-specific options

; Drive equates

NUMDRVS	equ	4		;number of drives supported
NUMALT	equ	2		;number of Altair drives
NUMTRKS	equ	77		;35 tracks
NUMSECS	equ	26		;ten sectors per track
SECLEN	equ	128		;sector length
TRKLEN	equ	(NUMSECS*SECLEN)	;length of track
SYSTRKS	equ	2		;number of system tracks
DATATRK	equ	2		;starting data track

; CP/M equates

PRINT	equ	9		;BDOS write string to console
RDLINE	equ	10		;BDOS read a line from the console
BDOS	equ	5		;BDOS entry point
IBUFLEN	equ	80		;length of input buffer
WBOOT	equ	0		;warm boot vector

; Misc equates

WRTRYS	equ	4		;write attempts
CR	equ	13		;ascii carriage return
LF	equ	10		;ascii line feed
CTRLC	equ	3		;ascii control-c

;  BIOS Jump Table Offsets. This table is used to compute BIOS entry
;     points by adding the symbols below to the warm boot entry address
;     stored by CP/M at location 1 on page zero.
	
	org	0
bWboot	ds	3		;warm start
bConSt	ds	3		;console status
bConIn	ds	3		;console character in 
bConOut	ds	3		;console character out 
bList	ds	3		;list character out 
bPunch	ds	3		;punch character out 
bReader	ds	3		;reader character out
bHome	ds	3		;move head to home position
bSelDsk	ds	3		;select disk
bSetTrk	ds	3		;set track number
bSetSec	ds	3		;set sector number
bSetDma	ds	3		;set dma address
bRead	ds	3		;read disk
bWrite	ds	3		;write disk

	org	0100h		;CP/M load and entry address
;-----------------------------------------------------------------------------
;  Program entry and initialization
;-----------------------------------------------------------------------------
	lxi	h,0		;save stack pointer by HL=SP+0
	dad	sp		;HL=current stack pointer from CP/M
	shld	stack		;save for restore later
	lxi	sp,ourStk	;setup our own stack

	xra	a		
	sta	secNum		;init current sector number

	lxi	d,mBanner	;display startup message
	call	dispMsg

; determine the number of tracks we can buffer based without clobbering
;    the BDOS. Save in maxBuf.

	lhld	BDOS+1		;hl=address of BDOS entry
	mvi	l,0		;l=number of tracks that fit
	mvi	a,trkBuf/256	;a=msb of track buffer

sizeLp	inr	l		;count a track
	adi	TRKLEN/256	;msb of track length (ok because LSB is zero)
	cmp	h		;reached or passed BDOS yet?
	jc	sizeLp		;no, keep counting

	dcr	l		;back up one track
	mov	a,l		;maxBuf = maximum tracks in trkBuf	
	sta	maxBuf

;-----------------------------------------------------------------------------
; dspMenu - Main menu loop
;-----------------------------------------------------------------------------
dspMenu	lxi	d,mMenu		;display menu
	call	dispMsg

	call	readChr		;read response
	ani	5fh		;clear bit 5 ('a' to 'A')

	cpi	'A'		;copy all tracks
	cz	cmdAll

	cpi	'S'		;copy system tracks only
	cz	cmdSys

	cpi	'D'		;copy data tracks only
	cz	cmdData

 if not GENERIC
	cpi	'F'		;format disk
	cz	cmdFmt
 endif
	cpi	'V'		;verify disk
	cz	cmdVfy	

	cpi	'X'		;exit
	jz	WBOOT		;warm boot CP/M to exit		

	jmp	dspMenu

;-----------------------------------------------------------------------------
; cmdAll - Copy all tracks
;-----------------------------------------------------------------------------
cmdAll	lxi	d,mCmdAll	;display command name
	call	dispmsg

	mvi	a,NUMTRKS	;copying all tracks on the disk
	sta	remTrks		;remaining tracks = all tracks
	xra	a		;current track = track zero
	sta	srcTrk
	sta	dstTrk

; cpyTrks - copy the number of tracks in remTrks, starting at src/dstTrk

cpyTrks	call	getSrc		;get source and destination from operator
	rnz			;user aborted
	call	getDst
	rnz

; if not single drive copy, display prompt to insert disks

	lxi	h,dstDrv	;hl->destination drive
	lda	srcDrv		;source = destination?
	cmp	m
	jz	chkWp		;single drive copy, skip next message

	lxi	d,mRdyCpy	;display insert disks prompt
	call	getRdy		;get ready confirmation from operator
	rnz			;user aborted

chkWp	equ	$
 if not GENERIC
	lda	dstDrv		;see if destination write protected
	sui	NUMALT		;A=Tarbell drive number

	call	dSelDrv		;select Tarbell drive
	call	dChkWp		;write protected?
 endif

; cpyLoop - filling trkBuf from source, then writing trkBuf to destination

cpyLoop	call	ldSrc		;prompt for source if single drive
	rnz			;user aborted

	call	fillBuf		;read as many tracks as possible

	call	ldDst		;prompt for destination if single drive
	rnz			;user aborted

	call	wrtBuf		;write the full track buffer
	jnz	wrFail		;write error

	lda	remTrks		;any tracks left?
	dcr	a		;negative or zero -> negative
	jp	cpyLoop

; copy complete, display message and exit

	lxi	d,mCpyDon	;copy done message
	call	dispMsg
	xra	a		;clear command character
	ret

;-----------------------------------------------------------------------------
; cmdSys - Copy system tracks only
;-----------------------------------------------------------------------------
cmdSys	lxi	d,mCmdSys	;display command name
	call	dispmsg

	mvi	a,SYSTRKS	;copying just system tracks
	sta	remTrks		;remaining tracks = just system tracks
	xra	a		;current track = track zero
	sta	srcTrk
	sta	dstTrk

	jmp	cpyTrks		;do the copy (in cmdAll)

;-----------------------------------------------------------------------------
; cmdData - Copy data tracks only
;-----------------------------------------------------------------------------
cmdData	lxi	d,mCmdDat	;display command name
	call	dispmsg

	mvi	a,NUMTRKS-SYSTRKS	;copying just data tracks
	sta	remTrks		;remaining tracks = just data tracks
	mvi	a,DATATRK	;current track = first data track
	sta	srcTrk
	sta	dstTrk

	jmp	cpyTrks		;do the copy (in cmdAll)

;-----------------------------------------------------------------------------
; cmdFmt - Format the disk
;-----------------------------------------------------------------------------
cmdFmt	lxi	d,mCmdFmt	;display command name
	call	dispmsg

	mvi	a,NUMTRKS	;formatting all tracks on the disk
	sta	trkCnt
	xra	a		;current track = track zero
	sta	dstTrk

	call	getDst		;get destination disk from operator
	rnz			;user aborted

	lxi	d,mRdyFmt	;display insert disk prompt
	call	getRdy		;get ready confirmation from operator
	rnz			;user aborted
	
	di			;disable interrupts during format
	call	format		;format the disk
	ei			;restore interrupts
	jnz	fmtFail		;error, format failed

; format complete, display message and exit

	lxi	d,mFmtDon	;formatting done message
	call	dispMsg
	xra	a		;clear command character
	ret

;-----------------------------------------------------------------------------
; cmdVfy - Verify all tracks
;-----------------------------------------------------------------------------
cmdVfy	lxi	d,mCmdVfy	;display command name
	call	dispmsg

	mvi	a,NUMTRKS	;verifying all tracks on the disk
	sta	trkCnt
	xra	a		;current track = track zero
	sta	srcTrk

	call	getSrc		;get source disk from operator
	rnz			;user aborted

	lxi	d,mRdyVfy	;display insert disk prompt
	call	getRdy		;get ready confirmation from operator
	rnz			;user aborted

	lda	srcDrv		;use BIOS to select drive
	call	selDsk

; vfyLoop - read all tracks which verifies checksum

vfyLoop	lxi	h,trkBuf	;restart at beginning of trkBuf each time
	shld	trkPtr

	call	readTrk		;read a track

	lxi	h,srcTrk	;increment current track
	inr	m
	lxi	h,trkCnt	;decrement track counter
	dcr	m
	jnz	vfyLoop

; verify complete, display message and exit

	lxi	d,mVfyDon	;de->verify done message
	call	dispMsg
	xra	a		;clear command character
	ret

;-----------------------------------------------------------------------------
; fillBuf - fill trkBuf with as many tracks that will fit, or the
;    remaining number of tracks to process, whichever is fewer.
; On Entry:
;    srcDrv = drive to read
;    srcTrk = track to start from
;    remTrks = number of tracks left to read
; On Exit:
;    srcTrk, remTrks updated (remTrks may go negative)
;    fillCnt = number of tracks read
;    Clobbers all
;-----------------------------------------------------------------------------
; compute fillCnt, the number of tracks to read into trkBuf

fillBuf	lda	srcDrv		;use BIOS to select drive
	call	selDsk

	lxi	h,maxBuf	;set b=max tracks that fit in trkBuf
	mov	b,m

	lxi	h,remTrks	;set a&c=tracks remaining to process
	mov	a,m
	mov	c,a

	sub	b		;a=remaining-max in buffer
	mov	m,a		;save remaining tracks (may be negative)
	mov	a,b		;a=max tracks in buffer	
	jnc	doMax		;remaining >= max in buffer, do max in buffer

	mov	a,c		;otherwise, fill count=remaining tracks

doMax	sta	fillCnt		;fillCnt=number of tracks to read (saved)
	sta	trkCnt		;trkCnt=counter of tracks to read (decrements)

; fillLp - read fillCnt tracks.
	
	lxi	h,trkBuf	;hl->beginning of track buffer
	shld	trkPtr

fillLp	call	readTrk		;read a track into (trkPtr)
	
	lxi	h,srcTrk	;increment current track
	inr	m
	lxi	h,trkCnt	;decrement track counter
	dcr	m
	jnz	fillLp

	ret			;fill done, zero status

;-----------------------------------------------------------------------------
; wrtBuf - write and verify fillCnt tracks from trkbuf
; On Entry:
;    dstDrv = drive to read
;    dstTrk = track to start from
;    fillCnt = number of tracks to write
; On Exit:
;    dstTrk updated 
;    Z set if OK, cleared for error
;    Clobbers all
;-----------------------------------------------------------------------------
wrtBuf	lda	dstDrv		;use BIOS to select drive
	call	selDsk

	lda	fillCnt		
	sta	trkCnt		;trkCnt=counter of tracks to write

	lxi	h,trkBuf	;hl->beginning of track buffer
	shld	trkPtr

; wrtLoop - write loop. Write a track and verify. Repeat trkCnt times.

wrtLoop	call	wrtTrk		;write a track from (trkPtr)
	rnz			;exit if error

	lxi	h,dstTrk	;increment current track
	inr	m
	lxi	h,trkCnt	;decrement track counter
	dcr	m
	jnz	wrtLoop

	ret

;-----------------------------------------------------------------------------
; readTrk - read a track into memory at (trkPtr). Sector errors are displayed
;    and reading continues with the next sector.
; On Entry:
;    srcDrv = drive to read
;    srcTrk = track to read
;    trkPtr->location within track buffer for this track
; On Exit: 
;    trkPtr->last byte of track+1
;    Clobbers all
;-----------------------------------------------------------------------------
readTrk	lda	srcTrk		;use BIOS to specify track
	call	setTrk

	call	get1st		;get 1st sector number for new track
	mvi	a,NUMSECS	;secCnt = sector counter
	sta	secCnt

	xra	a		;no error on the track yet
	sta	trkErr

readSec	call	chkQuit		;check for ctrl-c
	call	nxtSec		;set next sector and DMA address
	call	read		;call BIOS read entry point
	ora	a		;test result (to be safe)
	cnz	dispErr		;display the error	

	lxi	h,secCnt	;decrement sector count
	dcr	m
	jnz	readSec

	mvi	a,'.'		;display '.' pacifier per track
	call	CONOUT

; updtTrk - track read complete, update trkPtr by the length of a
;    full track and return with zero status

updtTrk	lhld	trkPtr		;hl=current track pointer
	lxi	d,TRKLEN	;de=bytes in a track
	dad	d		;hl=start of next track in trkBuf
	shld	trkPtr

	xra	a		;return with good status
	ret

;-----------------------------------------------------------------------------
; dispErr - display the current track and sector number which just had
;     a read error. If this is the first error on a track, the track
;     number is displayed first.
;-----------------------------------------------------------------------------
dispErr	lda	trkErr		;test track error flag
	ora	a
	jnz	dispSec		;track already displayed, go display sector

	inr	a		;set track flag non-zero
	sta	trkErr

; First error on this track. Display "Track xx errors: "

	lxi	h,mErrTrk	;hl->where to put ascii decimal
	lda	srcTrk		;a=track with error on it
	call	bin2dec		;track to ascii
	lxi	d,mTrkErr	;display the track error message
	call	dispMsg

; dispSec - display the sector number with an error

dispSec	lxi	h,mErrSec	;hl->where to put ascii sector
	lda	secNum
	call	bin2dec

	lxi	d,mErrSec
	call	dispMsg		;display the error
	ret

;--------------------------------------------------------------
; bin2dec - Binary byte in A to 2 ASCII digits at (HL)
;   HL is preserved
;--------------------------------------------------------------
bin2dec	mvi	m,' '		;assume zero supression
	sui	10		;value less than 10?
	jc	do1s		;yes, leading blank
	mvi	m,'1'		;have one ten already

loop10	sui	10		;count 10s
	jc	do1s		;done with 10s, do 1s
	inr	m
	jmp	loop10

do1s	adi	'0'+10		;form ASCII 1s digit
	inx	h		;move to 1s position
	mov	m,a
	dcx	h		;restore hl
	ret

;-----------------------------------------------------------------------------
; wrtTrk - write a track with retries from (hl)
; On Entry:
;    dstDrv = drive to write
;    dstTrk = track to write
;    hl->track buffer
; On Exit: 
;    hl->last byte of track+1 if successful, start of bad track otherwise
;    Z set if OK, cleared for error
;    Clobbers all
;-----------------------------------------------------------------------------
wrtTrk	mvi	a,WRTRYS	;init write retry counter
	sta	rtryCnt

	lda	dstTrk		;use BIOS to specify track
	call	setTrk

	call	get1st		;get 1st sector number for new track

wrtRtry	mvi	a,NUMSECS	;secCnt = sector counter
	sta	secCnt

wrtSec	call	chkQuit		;check for ctrl-c
	call	nxtSec		;set next sector and DMA address
	call	write		;call BIOS write entry point
	ora	a		;test result (force Z flag)
	rnz			;write failure
				
	lxi	h,secCnt	;decrement sector count
	dcr	m
	jnz	wrtSec

; track write complete, verify by reading and getting a good CRC

	mvi	a,NUMSECS	;secCnt = sector counter
	sta	secCnt

vfySec	call	chkQuit		;check for ctrl-c
	call	nxtSec		;set next sector
	lxi	h,secBuf	;read into unused secBuf
	call	setDma		;set buffer address using BIOS call
	call	read		;call BIOS read entry point
	ora	a		;test result (force Z flag)
	jnz	vfyErr		;error				

	lxi	h,secCnt	;decrement sector count
	dcr	m		;decrement sector count
	jnz	vfySec

; verify successful, display "*" pacifier, then update hl and exit

	mvi	a,'*'		;display '*' pacifier
	call	CONOUT
	jmp	updtTrk		;update trkPtr and exit (in readTrk)

; vfyErr - verify error, retry the write WRTRYS times.

vfyErr	lxi	h,rtryCnt	;decrement retry counter
	dcr	m
	jnz	wrtRtry		;try again

	inr	m		;force non-zero status
	ret			;and exit

;--------------------------------------------------------------
; get1st - Get first sector number for a new track. A track
;    step will occur before the first sector read on a new
;    track. The sector is set to give 32ms from the end
;    of the last sector read to give time for the track step
;    to complete and then be quickly coming upon the computed
;    sector number.
; On Entry:
;    secNum = last sector accessed
; On Exit:
;    secNum = last sector + 5 wrapped
; Clobbers h,l
;--------------------------------------------------------------
get1st	lxi	h,secNum	;get last sector accessed

 if GENERIC
	mvi	a,0FFh		;will increment to 1
 endif

 if not GENERIC
	mov	a,m		;a=last sector accessed
	adi	5		;bump by five sector times
	cpi	NUMSECS+1	;past 26?
	jc	noWrap

	sui	NUMSECS		;wrap to sector 1-26
 endif

noWrap	mov	m,a		;secNum updated for nxtSec call
	ret

;--------------------------------------------------------------
; nxtSec - Increment and wrap secNum, then call BIOS setSec
;    to set the sector for the next read. Compute the offset
;    of the new sector number from trkPtr and call BIOS
;    setDma to give the BIOS the buffer address.
; On Entry:
;    secNum = last sector accessed
;    trkPtr->address within trkBuf for current track
; On Exit:
;    secNum = new sector number, passed to BIOS
;    address within trkBuf for this sector passed to BIOS
; Clobbers all
;--------------------------------------------------------------
nxtSec	lxi	h,secNum	;increment sector number
	inr	m

 if GENERIC
	inr	m		;increment by two
 endif
	jz	wrap1		;in case uninitialized rolled to 0

	mov	a,m		;verify between 1 and 26
	cpi	NUMSECS+1
	jc	haveSec		;valid sector

 if GENERIC
	mvi	a,2		;if at 27, wrap to sector 2
	jz	wrapSav
 endif

wrap1	mvi	a,1		;restart at sector 1
wrapSav	mov	m,a

haveSec	call	setSec		;call BIOS setSec entry

	lda	secNum		;compute bc=128*(secNum-1)
	sui	1		;zero index and clear carry
	rar
	mov	b,a		;b=sector >> 1

	rar			;put lsbit into msbit	
	ani	80h
	mov	c,a		;bc=128*secNum

	lhld	trkPtr		;compute hl=trkPtr+sector offset
	dad	b
	call	setDma		;call BIOS setDma entry
	ret

;----------------------------------------------------------------------
; dispMsg - use the BDOS print message command to display the string
;    pointed to by DE.
; On Entry:
;    de->message to display
; On Exit
;    clobbers all
;----------------------------------------------------------------------
dispMsg	mvi	c,PRINT		;BDOS command to display a string
	jmp	BDOS		;call BDOS

;----------------------------------------------------------------------
; readChr - read and return a single character but use the BDOS input
;   line command for "normal" look and feel. Return zero if no input
;   entered (e.g., just return pressed).
; On Entry:
;    de->message to display
; On Exit
;    a = first character typed, zero if nothing typed
;    clobbers all
;----------------------------------------------------------------------
readChr	lxi	d,inBuf		;DE=pointer to input buffer
	mvi	a,IBUFLEN	;store max length to read
	stax	d
	mvi	c,RDLINE	;BDOS command, read line
	call	BDOS

	lxi	d,mCRLF
	call	dispMsg

	lda	inBuf+1		;get length read
	ora	a		;see if nothing entered
	rz			;if so, return zero

	lda	inBuf+2		;otherwise, return first character
	ret

;-----------------------------------------------------------------------------
; getSrc - get source drive from user, return in srcDrv, update drive
;    letter in message prompts.
; On Entry:
; On Exit: 
;    srcDrv = 0-3 if specified
;    Z set if valid drive entered, cleared otherwise
;    Clobbers all
;-----------------------------------------------------------------------------
getSrc	lxi	d,mSrc		;enter source drive prompt 
	call	dispMsg

	call	readChr		;read drive response
	ani	5fh		;clear bit 5 ('a' to 'A')
	sta	mSrcDrv		;update drive in messages
	sta	mVfyDrv
	sta	mSglSrc

	sui	'A'		;subtract ascii offset
	cpi	NUMALT		;Tarbell drive?
	rc			;exit if Altair drive A or B

	cpi	NUMDRVS		;valid drive number?
	jnc	getErr		;above 'D', exit

	sta	srcDrv		;save it

	xra	a		;return zero status
	ret

;-----------------------------------------------------------------------------
; getDst - get destination drive from user, return in dstDrv, update
;    drive letter in message prompts.
; On Entry:
; On Exit: 
;    dstDrv = 0-3 if specified
;    Z set if valid drive entered, cleared otherwise
;    Clobbers all
;-----------------------------------------------------------------------------
getDst	lxi	d,mDst		;enter source drive prompt 
	call	dispMsg

	call	readChr		;read drive response
	ani	5fh		;clear bit 5 ('a' to 'A')
	sta	mDstDrv		;update drive in messages
	sta	mFmtDrv
	sta	mSglDst

	sui	'A'		;subtract ascii offset
	cpi	NUMALT		;Tarbell drive?
	rc			;exit if Altair drive A or B

	cpi	NUMDRVS		;valid drive number?
	jnc	getErr		;above 'D', exit

	sta	dstDrv		;save it

	xra	a		;return zero status
	ret

; getErr - invalid response, return non-zero status

getErr	ora	a		;force non-zero staus
	ret

;-----------------------------------------------------------------------------
; getRdy - prompt user for confirmation of ready. 
; On Entry:
;    de->message prompt to display
; On Exit: 
;    Z set if user confirmed, cleared if user aborted.
;    Clobbers all
;-----------------------------------------------------------------------------
getRdy	call	dispMsg		;display passed prompt
	lxi	d,mCont		;continue? prompt
	call	dispMsg

	call	readChr		;read response
	ani	5fh		;clear bit 5 ('a' to 'A')
	cpi	'Y'		;confirmed?
	ret

;-----------------------------------------------------------------------------
; ldSrc, ldDst - prompt user to load the source or destination disk if
;    this is a single drive copy.
; On Entry:
;    srcDrv = source drive
;    dstDrv = destination drive
; On Exit: 
;    Z set if ok to continue, cleared if user aborted.
;    Clobbers all
;-----------------------------------------------------------------------------
ldSrc	lxi	d,mInsSrc	;de->prompt user to insert source disk
	jmp	chkSngl		;go see if single drive copy

ldDst	lxi	d,mInsDst	;de->prompt user to insert destination disk

chkSngl	lxi	h,dstDrv	;hl->destination drive
	lda	srcDrv		;source = destination?
	cmp	m
	jz	getRdy		;single drive copy, get ready from user

	xra	a		;not single drive, return zero status
	ret

;-------------------------------------------------------------------
; wrFail, fmtFail - display fail message and return to main loop
;-------------------------------------------------------------------
wrFail	lxi	d,mWrtErr
	jmp	dispMsg

fmtFail	lxi	d,mFmtErr
	jmp	dispMsg

;--------------------------------------------------------------
; chkQuit - check for the user to request abort (ctrl-c). If
;    a character is present on the console port, read it and
;    see if ctrl-c. Clobbers all.
;--------------------------------------------------------------
chkQuit	call	conSt		;check for console input
	ora	a		;set z flag
	rz			;nothing present

	call	conIn		;read the character
	ani	7fh
	cpi	CTRLC		;abort requested?
	rnz			;no

; Ctrl-C typed while program is running. Return to drive prompt.

	lxi	sp,ourStk	;restore stack pointer
	jmp	dspMenu		;start over

;-------------------------------------------------------------------
;  BIOS function calls. Single byte parameters all come in A and
;     are moved to C as required. Address passed in hl for setdma
;     moved to B, C as required.
;-------------------------------------------------------------------
conSt	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bConSt	;DE=offset of console status function
	dad	d
	pchl			;jump to console status function

conIn	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bConIn	;DE=offset of console input function
	dad	d
	pchl			;jump to console input function

conOut	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bConOut	;DE=offset of console input function
	dad	d
	mov	c,a		;character to send in c
	pchl			;jump to console outpput function

selDsk	mov	c,a		;selDsk wants param in c
	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bSelDsk	;DE=offset of select disk function
	dad	d
	pchl			;jump to select disk function

setTrk	mov	c,a		;setTrk wants param in c
	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bSetTrk	;DE=offset of set track function
	dad	d
	pchl			;jump to set track function

setSec	mov	c,a		;setSec wants param in c
	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bSetSec	;DE=offset of set sector function
	dad	d
	pchl			;jump to set sector function

setDma	mov	b,h		;BC=HL
	mov	c,l
	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bSetDma	;DE=offset of set dma function
	dad	d
	pchl			;jump to set dma function (dma addr in bc)

read	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bRead		;DE=offset of disk read function
	dad	d
	pchl			;jump to disk read function

write	lhld	WBOOT+1		;HL->warm start entry in BIOS
	lxi	d,bWrite	;DE=offset of disk write function
	dad	d
	pchl			;jump to disk read function


;**************************************************************
;
; format - format disk routine taken from format.asm
;
;**************************************************************
format	lda	dstDrv		;a=drive to format
	sui	NUMALT		;a=Tarbell drive number
	call	dRestor		;select and restore to track 0
	rnz			;exit if restore failed

	call	dChkWP		;check for write protect

	mvi	l,0		;set track number to 0
	mvi	h,77		;set total tracks to 77

; nxtTrk - do the next track

nxtTrk	mvi	d,1		;sector cnt to 0
	mvi	e,26		;set max # sectors -1 
	mvi	b,40		;gap 4 preindex 40 bytes of ff
	mvi	a,cWRTRK	;load track write command
	out	FDCCMD		;issue track write

; write preindex fill

preInd	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0FFh		;load preindex fill
	out	FDCDATA		;write it on disk
	dcr	b		;count = count - 1
	jnz	preInd		;go back till b = 0

	mvi	b,6

preIn1	in	FDCWAIT
	ora	a
	jp	fmtFail

	xra	a
	out	FDCDATA
	dcr	b
	jnz	preIn1

; write address mark on track

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0FCh		;load address mark
	out	FDCDATA		;write it on disk

; post index gap

postGap	mvi	b,26		;set # of bytes

postId	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0FFh		;load fill data
	out	FDCDATA		;write it on disk
	dcr	b		;count = count - 1
	jnz	postId		;if not 0 go back

; pre id section

	mvi	b,6		;get # of bytes

sector	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	xra	a		;make a = 0
	out	FDCDATA		;write it on track
	dcr	b		;count = count - 1
	jnz	sector		;jmp back if not done

; write id address mark

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;if error jmp out

	mvi	a,0FEh		;get address mark
	out	FDCDATA		;write it on disk

; write track number on disk

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mov	a,l		;get track number
	out	FDCDATA		;write it on disk

; write one byte of 00

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	xra	a		;set a to 0
	out	FDCDATA		;write it on disk

; write sector # on disk

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mov	a,d		;get sector #
	out	FDCDATA		;write it on disk

; one more byte 0

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	xra	a		;set a to 00
	out	FDCDATA		;write it on disk
	inr	d		;bump sect. #

; write 2 crc's on this sector

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0F7h		;get crc pattern
	out	FDCDATA		;write it on disk

; pre data 11 bytes FF, 6 bytes 00

	mvi	b,11		;set count
preDat	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0FFh		;set a to FF
	out	FDCDATA		;write it on disk
	dcr	b		;reduce count by 1
	jnz	preDat		;go back if not done

	mvi	b,6

preDa1	in	FDCWAIT
	ora	a
	jp	fmtFail

	xra	a 
	out	FDCDATA
	dcr	b
	jnz	preDa1

; data address mark

	in	FDCWAIT		;wait for drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0FBh		;get data address mark
	out	FDCDATA		;write it on disk

; fill data field with E5

	mvi	b,128		;set field length
dFill	in	FDCWAIT		;wait for drq
	ora	a		;you know what
	jp	fmtFail		;happens here by now

	mvi	a,0E5h		;get fill byte
	out	FDCDATA		;write it on disk
	dcr	b		;drop 1 from count
	jnz	dFill		;do till 00

; write CRC's

	in	FDCWAIT		;wait till drq
	ora	a		;set flags
	jp	fmtFail		;jmp out if error

	mvi	a,0F7h		;get crc byte
	out	FDCDATA		;write it on disk

; end of sector fill

	dcr	e		;reduce sector count
	jz	endTrk		;if 0 do end of track rtn

datGap	in	FDCWAIT		;wait for drq
	ora	a		;set flags	
	jp	fmtFail		;jmp out if error

	mvi	a,0FFh		;get fill character
	out	FDCDATA		;write it on disk
	jmp	postGap		;go back for more

; endTrk - fill with FFh until command ends at index hole

endTrk	in	FDCWAIT		;wait for drq or intrq
	ora	a		;set flags
	jp	done		;jmp out if error

	mvi	a,0FFh		;load a with FFh
	out	FDCDATA		;write it on disk
	jmp	endTrk		;do until intrq

; done - check for error from the write track command. Wait for trim
;     erase delay in case the drive has tunnel erase heads, then
;     move to the next track.

done	in	FDCSTAT		;read status of write track command
	ani	0ffh		;test for any flag
	rnz			;exit if error

	mvi	b,87		;delay 500us min (1ms at 2mhz)

trimDly	dcr	b		;(5)
	nop			;(4)
	nop			;(4)
	jnz	trimDly		;(10)

; decrement track counter, check for finish

	inr	l		;bump track #
	dcr	h		;trk count =count -1
	rz			;all done, exit

; issue step to next track

	mvi	a,cSTEPIN	;step in to next track
	out	FDCCMD
	in	FDCWAIT		;wait til done

; display '.' pacifier, check for operator abort, then jump to
;     nxtTrk to repeat
		
	push	h		;save for BIOS call
	mvi	a,'.'		;display '.' pacifier per track
	call	CONOUT		;display pacifier
	call	chkQuit		;see if abort requested
	pop	h
	jmp	nxtTrk


;****************************************************************************
;
; Tarbell disk I/O routines (single density controller)
;
;****************************************************************************

; Tarbell Disk Drive Controller Equates

FDCBASE	equ	0F8H		;controller base address
FDCCMD	equ	FDCBASE		;1771 command register
FDCSTAT	equ	FDCBASE		;1771 status register
FDCTRK	equ	FDCBASE+1	;1771 track register
FDCSEC	equ	FDCBASE+2	;1771 sector register
FDCDATA	equ	FDCBASE+3	;1771 data in/out register
FDCWAIT	equ	FDCBASE+4	;FDC stall/wait port
FDCSEL	equ	FDCBASE+4	;FDC drive select port

STEP10	equ	2		;10ms step rate

; Commands for 1771.

cRESTOR	equ	000h+STEP10	;restore, no head load, no verify
cSTEPIN	equ	058h+STEP10	;step in, update track reg, head loaded
cRESET	equ	0D0h		;reset 1771 to idle
cWRTRK	equ	0F4h		;write track command

; Status bits from 1771

sNOTRDY	equ	80h		;1=not ready
sWRPROT	equ	40h		;1=write protected
sHDLOAD	equ	20h		;1=head is loaded
sSKERR	equ	10h		;1=seek error
sCRCERR	equ	08h		;1=CRC error
sTRACK0	equ	04h		;1=on track zero
sINDEX	equ	02h		;1=index detected
sBUSY	equ	01h		;1=1771 busy

; Combined status flags for status of a seek

fSEEK	equ	sSKERR+sCRCERR+sNOTRDY+sBUSY

;--------------------------------------------------------------
; dRestor - Selects drive, loads heads and restores to track 0.
;    If restore fails, message display and program restarted
;    at getDrv.
; On Entry:
;    a = drive to be selected and restored to track zero
; On Exit:
;    drive selected and on track zero
; Clobbers none 
;--------------------------------------------------------------
dRestor	call	dSelDrv		;make sure drive is selected

	mvi	a,cRESTOR	;issue restore to track zero
	out	FDCCMD
	in	FDCWAIT		;and wait for restore to complete

	in	FDCSTAT		;verify restore worked
	ani	fSEEK		;isolate seek bits
	ret			;return with status

;--------------------------------------------------------------
; dSelDrv - Select and wait for drive specified in drvNum 
;    to be ready.
; On Entry:
;    a = drive to be selected
; On Exit:
;    drive selected and ready, 1771 not busy
; Clobbers none
;--------------------------------------------------------------
dSelDrv	cma			;the selection decoder inverts
	add	a		;shift left 4 with zero fill
	add	a
	add	a
	add	a
	ori	2		;set the latch bit
	out	FDCSEL		;select the drive

; reset the 1771 and wait for not busy

	mvi	a,cRESET	;ensure 1771 is idle
	out	FDCCMD

dWtIdle	in	FDCSTAT		;wait for 1771 to be idle
	rrc			;busy bit into carry
	jc	dWtIdle

; drive selected, 1771 idle, wait for drive ready

dWtRdy	in	FDCSTAT
	rlc			;not ready bit in carry
	jc	dWtRdy		;wait for ready
	ret

;--------------------------------------------------------------
; dChkWP - check if disk is write protected. 
; On Entry:
;    disk already selected
; On Exit:
;    returns if disk is not protected. Otherwise, an error
;    message is displayed and the program started over.
; Clobbers none
;--------------------------------------------------------------
dChkWP	in	FDCSTAT
	ani	sWRPROT		;get write protect bit alone
	rz			;return if not protected

; Disk write protected. Display error and restart program.

	lxi	d,mWrtPrt	;write protect message
	call	dispMsg

	lxi	sp,ourStk	;reset stack pointer
	jmp	dspMenu		;start over


;-------------------------------------------------------------------
;  String Constants
;-------------------------------------------------------------------
mBanner
 db CR,LF,'COPY Version 1.3 by M. Douglas',CR,LF,LF

 if GENERIC
 db 'Disk utility for 8in SSSD drives',CR,LF,'$'
 endif

 if not GENERIC
 db 'Disk utility for the Altair/Tarbell combo BIOS',CR,LF
 db '  (Tarbell drives C & D only)',CR,LF,'$'
 endif

mMenu
 db CR,LF,LF,'======  O P T I O N   T A B L E  ======',CR,LF,LF
 db '  A = Copy all tracks',CR,LF
 db '  S = Copy only system tracks (0-1)',CR,LF
 db '  D = Copy only data tracks (2-76)',CR,LF

 if not GENERIC
 db '  F = Format disk',CR,LF
 endif

 db '  V = Verify disk',CR,LF
 db '  X = Exit and return to CP/M',CR,LF,LF
 db 'Enter Selection: $'

mCmdAll
 db CR,LF,'  Copy All Tracks',CR,LF,'$'

mCmdSys
 db CR,LF,'  Copy Only System Tracks',CR,LF,'$'

mCmdDat
 db CR,LF,'  Copy Only Data Tracks',CR,LF,'$'

mCmdFmt
 db CR,LF,'  Format Disk',CR,LF,'$'

mCmdVfy
 db CR,LF,'  Verify Disk',CR,LF,'$'

mSrc
 db CR,LF,'Enter source drive (C, D or X to abort): $' 

mDst
 db CR,LF,'Enter destination drive (C, D or X to abort): $' 

mRdyCpy	db	CR,LF,'Source on drive '
mSrcDrv	db	'A, destination on drive '
mDstDrv	db	'B$'

mRdyFmt	db	CR,LF,'Disk to format in drive '
mFmtDrv	db	'A$'

mRdyVfy	db	CR,LF,'Disk to verify in drive '
mVfyDrv	db	'A$'

mInsSrc	db	CR,LF,'Insert source disk in drive '
mSglSrc	db	'A$'

mInsDst	db	CR,LF,'Insert destination disk in drive '
mSglDst	db	'A$'

mCont	db	CR,LF,'  Continue(Y/N)? $'

mSkErr	
 db CR,LF,'*** Restore/seek failure ***',CR,LF,'$'

mWrtErr	
 db CR,LF,LF,'** Unrecoverable write error **',CR,LF,'$'

mCpyDon
 db CR,LF,LF,'Copy complete. Note any displayed errors.',CR,LF,'$'

mVfyDon
 db CR,LF,LF,'Verify complete. Note any displayed errors.',CR,LF,'$'

mWrtPrt	
 db CR,LF,'** Disk is write protected **',CR,LF,'$'

mFmtDon
 db CR,LF,LF,'Formatting complete',CR,LF,'$'

mFmtErr
 db CR,LF,LF,'** Formatting failed **'

mCRLF
 db CR,LF,'$'


mTrkErr
 db CR,LF,'Track '
mErrTrk
 db 'xx failed sectors: $'
mErrSec
 db 'xx $'

;-------------------------------------------------------------------
;  Data Area
;-------------------------------------------------------------------	
stack	ds	2		;save area for CP/M stack pointer
inBuf	ds	IBUFLEN		;storage for command string

reBoot	ds	1		;1=reboot required on exit

srcDrv	ds	1		;source drive for copy
dstDrv	ds	1		;destination drive for copy

srcTrk	ds	1		;track source disk is on
dstTrk	ds	1		;track destination disk is on

maxBuf	ds	1		;number of tracks that can be buffered
fillCnt	ds	1		;number of tracks to buffer this time
trkCnt	ds	1		;counter of tracks while processing
remTrks	ds	1		;number of tracks remaining to process
trkPtr	ds	2		;pointer into trkBuf

secNum	ds	1		;current sector number
secCnt	ds	1		;count of sectors left to process
secBuf	ds	128		;sector read buffer when verifying writes

rtryCnt	ds	1		;write retry counter
trkErr	ds	1		;non-zero = read error on the track

	ds	64		;our stack
ourStk	equ	$
trkBuf	equ	$

	end

