;*****************************************************************************
;
;  Flop2PC - Transfer floppy image to PC over a serial port
;		(for Mostek Development System AID-80F, etc.)
;
;	The Mostek development systems supported include the AID-80F and
;	SYS-80FT systems. The primary criteria is presence of the SDB-80
;       (or SDB-80E) CPU board and the FLP-80 (or FLP-80E) floppy controller.
;       The monitor PROMs on the SDB-80 CPU board provide disk I/O support
;       routines that are called by this program to do disk I/O.
;
;	This program transmits an image of a SSSD soft-sectored 8" floppy
;	to a PC. The image is transmitted through the SDB-80 serial port
;	using the XMODEM protocol.
;
;	The program calls the disk driver PROMs on the SDB-80 CPU board for
;       disk I/O and does not require CP/M or an OS to function.
;
;	For this hardware, the console port and transfer port are the same,
;	so error messages can't be seen since the file transfer process
;       of the terminal emulator trashes the messages. To get around this,
;	this version of the the program defers output of error messages
;	until after the transfer completes.
;
;	The program runs standalone at 0x100 or under CP/M. The program
;	is exited with a CTRL-C and either warm boots CP/M or just starts
;       over when running stand alone.
;
;	Written by Mike Douglas
;
;	Rev	 Date	     Desc
;	1.0	03/08/14   Original (uses MOS-80 or CP/M for disk I/O)
;
;	1.1	10/05/14   M. Douglas
;		   Use sector skew of 2 to reduce track write time from
;		   over four seconds to less than 1/2 second.
;
;	1.2	03/08/16   M. Douglas
;		   Total re-write to use the generic FLOP2PC model. Calls
;		   the Mostek disk support PROM for disk I/O so this
;		   version can run stand-alone without an OS. This direct
;		   disk I/O eliminates the skew requirement of ver 1.1.
;
;*****************************************************************************

	maclib	z80		;load Z80 opcodes

; Disk information equates

NUMTRK	equ	77		;number of tracks
NUMSEC	equ	26		;number of sectors per track
SECLEN	equ	128		;sector length (as transmitted)
MINDRV	equ	0		;first drive number
MAXDRV	equ	3		;max drive number 

TRKLEN	equ	(NUMSEC*SECLEN)	;length of track in bytes
RDTRIES	equ	6		;disk retry counter

; Monitor, CP/M boot entry points

MONITOR	set	init		;no monitor, just start over
CPMBOOT	set	0		;CP/M warm boot jump vector

; XMODEM equates

PKTLEN	equ	128		;128 byte xmodem packet length
SOH	equ	001h
EOT	equ	004h
ACK	equ	006h
NAK	equ	015h
EOF	equ	01ah		;ctrl-z character
XMTRIES	equ	10		;number of xmodem retries

; CP/M Equates

WBOOT	equ	0		;warm boot jump vector

; Misc equates

CR	equ	13		;ascii for carriage return
LF	equ	10		;ascii for line feed
DEL	equ	7fh		;ascii DEL
BS	equ	08h		;ascii backspace
CTRLC	equ	03		;ascii for control-c
JMPINST	equ	0c3h		;jump instruction opcode

	org	0100h		;load address
;-----------------------------------------------------------------------------
;   Initialize for transfer
;-----------------------------------------------------------------------------
init	lxi	sp,ourStk	;initialize stack pointer
	call	chkCpm		;set flag for CP/M or not
	call	sizeRam		;determine amount of RAM available
	mvi	a,MINDRV	;a=default drive
	sta	dcbDrv		;need for pgmExit

;  Display welcome message, then get the drive number and port number
;    to use for the transfer from the operator.

	lxi	h,mWelcom	;display welcome message
	call	dispMsg

; getDrv - get drive number or letter from the user.

getDrv	lxi	h,mDrive	;display drive number prompt
	call	dispMsg
	call	rcvCon		;get byte from the console
	ori	20h		;upper case letters to lower case
	cpi	'x'		;exit requested?
	jz	pgmExit		;yes

	lxi	h,baseDrv	;ascii 1st drive - baseDrv = 0
	sub	m
	cpi	MAXDRV-MINDRV+1	;validate >=min drive and <= max drive
	jnc	getDrv		;invalid, prompt again

	adi	MINDRV		;restore offset of 1st drive (if any)
	sta	dcbDrv		;save the drive number to use

; Prompt the user to start the XMODEM receive operation on the PC. 
;    Wait for a clear receive line for a least one second, then start
;    looking for NAK or 'C' (crc protocol) from the PC

	lxi	h,mStart	;display start file transfer prompt
	call	dispMsg

clrRcv1	call	rcvByte		;loop until input clear for 1 second
	jnz	clrRcv1

	xra	a		;set CRC flag to false (checksum mode)
	sta	crcFlag

waitNak	call	chkQuit		;give user chance to abort
	call	rcvByte		;wait for a character
	cpi	NAK
	jz	haveNak		;have a NAK, use checksum protocol

	cpi	'C'		;CRC protocol requested?
	jnz	waitNak		;no

	sta	crcFlag		;set crc flag non-zero = CRC mode

;  NAK or 'C' received meaning the XMODEM receive has started. Initialize
;     for the transfer.

haveNak	xra	a		;init track we want to zero
	sta	dcbTrk

	inr	a		;init xmodem block number to one
	sta	blkNum
	sta	dcbSec		;make sure sector is a valid value

	lxi	h,trkBuf	;trkBuf is initial starting point for
	shld	sndPtr		;   XMODEM send

;-----------------------------------------------------------------------------
;  mainLp - Loop through all tracks on the disk buffering as many tracks
;    as will fit in RAM before sending via xmodem, then repeat.
;-----------------------------------------------------------------------------
mainLp	lxi	h,trkBuf	;disk buffering always start at trkBuf
	shld	trkPtr

; Read and buffer bufTrks tracks unless all tracks on drive reached first

bufLoop	call	readTrk		;read into trkBuf

	lda	dcbTrk		;increment track number (preserve HL)
	inr	a
	sta	dcbTrk	
	cpi	NUMTRK		;done all tracks on the disk?
	jz	sendBuf		;yes, go send the buffered tracks

	lda	trkErr		;read failure on the track?
	ora	a		;if so, send buffered tracks now
	jnz	sendBuf		;so XMODEM won't time out

	xchg			;de=current track pointer
	lhld	bufEnd		;hl=end of buffering space
	call	cmpDeHl		;compare current-end
	jc	bufLoop		;still room, keep going	

; Track buffer is full or all tracks have been read. Send the buffered
;    tracks via xmodem.

sendBuf	call	sndTrks		;send the buffered tracks via xmodem
	lda	dcbTrk
	cpi	NUMTRK		;done all tracks?
	jnz	mainLp		;no, not done yet

; The entire disk has been read and (mostly) transmitted. See if there are
;    left over bytes to send. If so, send them.

	mov	a,l		;hl->next packet to send, a=lsb of ptr
	cpi	trkBuf AND 0FFh	;pointing to lsb of address of trkBuf?
	jz	noExtra		;same, no extra bytes to send

	call	sndPkt		;otherwise, send one last packet

; Send EOT and wait for response. Then display the success message and
;    start the program over.

noExtra	call	sndEot		;send and get response for EOT

	lxi	h,mDone		;print the all done message
	call	dispMsg

	call	dspErrs		;display any track errors

	jmp	getDrv		;start over asking for a drive
	
;-----------------------------------------------------------------------------
; readTrk - read NUMSEC sectors from the current track into a trkBuf as
;   pointed to by trkPtr. After the track is read, trkPtr is updated
;   by the length of a track to point to the next track buffer. This
;   saved value is also returned in HL.
;-----------------------------------------------------------------------------
readTrk	mvi	a,RDTRIES	;init retry counter
	sta	rdRetry	

	xra	a		;no error for this track yet
	sta	trkErr

	call	dSet1st		;set 1st sector, buffer in the DCB

	mvi	a,NUMSEC	;init sector counter
	sta	secCnt

secLoop	call	dRead		;read the sector specified in the DCB
	jz	rdGood		;good read

	call	chkQuit		;check for ctrl-c
	
	lxi	h,rdRetry	;decrement read retry counter
	dcr	m
	jnz	secLoop		;try again until zero

; Not able to the read sector. Increment dcbDma by SECLEN since dNxtSec
;     assumes this has been done by a valid read.

	lxi	h,trkErr	;set track error flag
	mvi	m,0FFh

	mvi	a,RDTRIES	;re-start retry counter
	sta	rdRetry

	lhld	dcbDma		;increment dcbDma by sector length
	lxi	b,SECLEN
	dad	b
	shld	dcbDma

; rdGood - Decrement sector count. If full track is done, increment trkPtr
;   to point to the next track buffer in trkBuf

rdGood	call	dNxtSec		;increment and wrap sector
	lxi	h,secCnt	;decrement sector count
	dcr	m
	jnz	secLoop

; Track read complete. Save track error flag in the track error table.

	lxi	h,dcbTrk	;form BC=current track
	mov	c,m
	mvi	b,0

	lxi	h,errTbl	;compute HL=address of track in errTbl
	dad	b

	lda	trkErr		;save trkErr in errTbl for this track
	mov	m,a

; Increment trkPtr to memory location for the next track and exit.

	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
	ret

;-----------------------------------------------------------------------------
; sndTrks - send the tracks buffered in trkBuf via xmodem. trkPtr points
;    to the end+1 of the data to send
;-----------------------------------------------------------------------------
sndTrks	lhld	trkPtr		;hl=end of buffered data + 1
	xchg			;de=end of buffered data + 1
	lhld	sndPtr		;hl=start tranmission address

sndLoop	push	d		;save end pointer
	call	sndPkt		;send a packet
	pop	d		;de=end pointer

; At this point, hl->start of next packet and de->last byte read from disk+1.
;    If the next XMODEM packet will go past the end of the disk data,
;    we don't want to send it yet.

	lxi	b,PKTLEN	;bc=length of XMODEM packet
	dad	b		;hl=address at end of NEXT packet + 1
	call	cmpDeHl		;compare disk end - end of next packet
	jc	sndDone		;next packet will pass the end, stop

	lxi	b,-PKTLEN	;restore hl
	dad	b
	jmp	sndLoop

; sndDone - all the packets we can send have been sent. Move any bytes
;    left over to just before the start of trkBuf. The next group of
;    xmodem packets will be transmitted starting there.

sndDone	call	subDeHl		;hl=de-hl = left over byte count - PKTLEN
	mov	a,l		;a=left over count - PKTLEN
	adi	PKTLEN		;a=left over byte count
	mov	b,a		;b=count of bytes to move
	lxi	h,trkBuf	;hl->start of trkBuf
	jz	mvDone		;no bytes to move, we're done

; Copy the leftover data backwards to just before the track buffer. The
;   next XMODEM send sequence will start at the beginning of this copied
;   data instead of at the start of trkBuf.

moveLp	dcx	h		;hl->memory just before trkBuf
	dcx	d		;de->unsent bytes from end of trkBuf
	ldax	d		;move from end of trkBuf to before trkBuf
	mov	m,a	
	dcr	b
	jnz	moveLp

mvDone	shld	sndPtr		;save address from which to send next time
	ret

;-----------------------------------------------------------------------------
; sndPkt - send an xmodem format 128 byte packet. HL points to the 128 byte
;    buffer to send. On exit, HL points to the next 128 byte boundary.
;-----------------------------------------------------------------------------
sndPkt	mvi	a,XMTRIES	;init retry counter
	sta	xmRetry

; First, send header bytes

reSend	call	chkQuit		;check for ctrl-c
	push	h		;save hl for possible re-send
	mvi	a,SOH		;1st byte is SOH
	call	sndByte
	lda	blkNum		;2nd byte is the block number
	call	sndByte
	cma			;2nd complement of block number
	call	sndByte

; Init checksum and CRC and packet length for transmission of data portion

	xra	a		;init crc
	sta	crc16
	sta	crc16+1
	lxi	d,PKTLEN*256	;d=byte counter, e=0 (checksum)

; Loop sending the data bytes and updating checksum and CRC

pktLoop	mov	a,m
	call	sndByte		;send and update checksum in e
	call	calCrc		;update the CRC
	inx	h		;point to next byte
	dcr	d		;decrement bytes remaining
	jnz	pktLoop

; Send checksum or CRC based on crcFlag

	lda	crcFlag		;crc or checksum?
	ora	a
	jz	sndCsum		;flag clear = checksum

	lda	crc16+1		;a=high byte of CRC
	call	sndByte		;send it
	lda	crc16		;a=low byte of crc
	jmp	sndSkip		;skip next instruction	

sndCsum	mov	a,e		;send the checksum byte

sndSkip	call	sndByte

;  All bytes sent. Wait for the response.

	call	rcvByte		;get the response character
	jz	sndFail		;timeout on response

	cpi	ACK		;ack received?
	jnz	sndFail		;no, send failed

	lda	blkNum		;increment block number
	inr	a
	sta	blkNum

	pop	b		;remove pushed hl, but don't clobber hl
	ret

; sndFail - ACK not received, decrement retry and try again.

sndFail	lxi	h,xmRetry	;point to retry counter
	dcr	m
	jz	xmFail		;retries used up, failed xmodem transfer

; If we've had 3 NAKs on the 1st packet and CRC is selected, assume
;   we took so long to send the 1st packet due to disk retries that
;   the receiver has since timed out and switched to checksum

	lda	crcFlag		;are we in CRC mode
	jz	clrRcv2		;no, ignore the rest of this

	lda	blkNum		;on block 1?
	dcr	a
	jnz	clrRcv2		;no, go on

	lda	xmRetry		;failed three times in a row on block 1?
	sui	XMTRIES-3
	jnz	clrRcv2		;no

	sta	crcFlag		;clear crcFlag to force checksum

;  clrRcv2 - wait for one second of line clear time and send packet again.

clrRcv2	call	rcvByte		;wait for 1 second of clear line
	jnz	clrRcv2

	pop	h		;restore pointer to the packet
	jmp	reSend		;re-send the packet

;  xmFail - Display failure message then restart program

xmFail	lxi	h,mXmdm		;xmodem failure message
	call	dispMsg

	lxi	sp,ourStk	;initialize stack pointer
	jmp	getDrv		;start over asking for drive

;-----------------------------------------------------------------------------
; calCrc - update the 16-bit CRC with one more byte. 
;    (Copied from M. Eberhard)
; On Entry:
;   a has the new byte
;   crc16 is current except this byte
; On Exit:
;   crc16 has been updated
;   Trashes a,bc
;-----------------------------------------------------------------------------
calCrc	push	d
	push	h
	lhld	crc16		;get CRC so far
	xra	h		;XOR into CRC top byte
	mov	h,a
	lxi	b,1021h		;bc=CRC16 polynomial
	mvi	d,8		;prepare to rotate 8 bits

; do 8 bit shift/divide by CRC polynomial

cRotLp	dad	h		;16-bit shift
	jnc	cClr		;skip if bit 15 was 0
	mov	a,h		;CRC=CRC xor 1021H
	xra	b
	mov	h,a
	mov	a,l
	xra	c
	mov	l,a
cClr	dcr	d
	jnz	cRotLp		;rotate 8 times

; save the updated CRC and exit

	shld	crc16		;save updated CRC
	pop	h
	pop	d
	ret

;-----------------------------------------------------------------------------
;  sndEot - send EOT character and wait for ACK response
;-----------------------------------------------------------------------------
sndEot	mvi	a,XMTRIES	;init retry counter
	sta	xmRetry

reEot	mvi	a,EOT		;send the EOT
	call	sndByte
	call	rcvByte		;wait for response
	jz	eotFail		;timeout

	cpi	ACK		;ack received?
	jnz	eotFail		;no, eot send failed

	ret			;otherwise, we're done.

; timeout waiting for ACK to EOT. Decrement retry counter and try again

eotFail	lxi	h,xmRetry	;point to retry counter
	dcr	m
	rz			;just give up - xfer was probably good

clrRcv3	call	rcvByte		;wait for 1 second of clear line
	jnz	clrRcv3
	jmp	reEot		;try again

;-----------------------------------------------------------------------------
; dispMsg - display the null-terminated message passed in hl on the
;    console device. Clobbers b, hl
;-----------------------------------------------------------------------------
dispMsg	mov	a,m		;get the next message byte
	ora	a		;null terminates
	rz

	mov	b,a		;conOut wants character in b
	call	conOut
	inx	h		;move to next byte
	jmp	dispMsg

;-----------------------------------------------------------------------------
; rcvCon - Receive a character from the console device, echo it, then
;   wait for a CR. Exits program if Ctrl-c typed. Returns with invalid
;   character (null) if BS or DEL pressed after 1st character
;   Returns character in a, clobbers b.
;-----------------------------------------------------------------------------
rcvCon	call	conIn		;check for input
	jz	rcvCon		;nothing

	ani	7fh
	cpi	CTRLC		;abort requested?
	jz	pgmExit		;yes

	cpi	CR		;return pressed?
	rz			;yes, don't echo it

	mov	b,a		;conOut needs character in b
	call	conOut		;echo it

; Wait for CR, then return 1st character typed

rcvCr	call	conIn		;check for input
	jz	rcvCr		;nothing

	ani	7fh
	cpi	CTRLC		;abort requested?
	jz	pgmExit		;yes

	cpi	DEL		;delete
	rz			;yes, return DEL character

	cpi	BS		;backspace?
	rz			;yes, return BS character

	cpi	CR		;return pressed?
	jnz	rcvCr		;no, keep waiting

	mov	a,b		;return 1st character typed
	ret

;-----------------------------------------------------------------------------
; sndByte - send the byte in a through the specified transfer port. 
;     Adds the byte to register e for checksum. Clobbers b.
;     XMODEM send routine assumes serOut returns with a containing
;     the character sent.
;-----------------------------------------------------------------------------
sndByte	mov	b,a		;b=byte to transmit
	add	e		;update checksum
	mov	e,a		;e=updated checksum
	jmp	serOut		;send the character

;-----------------------------------------------------------------------------
; rcvByte - Receive a byte from specified transfer port with a one second
;     timeout. If a timeout occurs, zero is returned in A and the zero 
;     flag is true. Otherwise, the character is returned in A (could be
;     zero) and zero flag is false. ONESEC must be set based on processor
;     speed and the number of cycles in the serIn call + 46. 
;     Clobbers a, b and c.
;-----------------------------------------------------------------------------
rcvByte	lxi	b,ONESEC	;bc=cycles through this loop for 1s

rcvWait	call	serIn		;(17+cycles in serIn)look for a byte
	rnz			;(5)byte received

	dcx	b		;(5)otherwise, decrement timer
	mov	a,b		;(5)one second expire?
	ora	c		;(4)
	jnz	rcvWait		;(10)no, keep waiting

	ret			;return with timeout (zero true and in a)

;-----------------------------------------------------------------------------
; dspErrs - Loop through the track errTbl and print the track number
;    of each track which had an error.
;-----------------------------------------------------------------------------
dspErrs	lxi	h,errTbl	;HL->error table
	mvi	d,0		;D=current track number in table
	mvi	e,NUMTRK	;D=decrementing track counter
	
errLoop	mov	a,m		;get error byte for track
	ora	a		;non-zero?
	jz	nxtErr		;no error, check next track

; Error flag set for the track. Display the track number.

	mov	a,d		;A=current track in table
	call	dspDec		;display as decimal value

nxtErr	inx	h		;increment error table pointer
	inr	d		;increment track number
	dcr	e		;decrement track counter
	jnz	errLoop
	ret

;--------------------------------------------------------------
; dspDec - Display byte in A to 2 ASCII digits. Clobbers B.
;--------------------------------------------------------------
dspDec	mvi	b,' '		;assume zero supression
	sui	10		;value less than 10?
	jc	do1s		;yes, leading blank
	mvi	b,'1'		;have one ten already

loop10	sui	10		;count 10s
	jc	do1s		;done with 10s, do 1s
	inr	b
	jmp	loop10

do1s	adi	'0'+10		;form ASCII 1s digit
	mov	c,a		;save 1's digit in C
	call	conOut		;display 10's digit from B

	mov	b,c		;display 1's digit from B
	call	conOut

	mvi	b,' '		;display a space
	call	conOut
	ret

;--------------------------------------------------------------
; 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 A
;--------------------------------------------------------------
chkQuit	call	conIn		;check for console input
	rz

	ani	7fh
	cpi	CTRLC		;abort requested?
	rnz			;no

; Ctrl-C typed while program is running. Return to drive prompt.

	lxi	sp,ourStk	;initialize stack pointer
	jmp	getDrv		;start over asking for drive num

;--------------------------------------------------------------
; pgmExit - Exit to CP/M or to the monitor ROM based on the
;    CP/M flag
;--------------------------------------------------------------
pgmExit	lda	cpmFlag		;running under CP/M?
	ora	a
	jnz	cpmExit		;yes

; Exit to ROM monitor

	lxi	h,mExit		;display "exiting" message
	call	dispMsg
	jmp	MONITOR		;jump to ROM monitor entry point

; CP/M exit. If boot drive was used, prompt user to insert CP/M disk

cpmExit	lda	dcbDrv		;boot drive used?
	sui	MINDRV
	jnz	noDisk		;not 1, disk prompt not needed

	lxi	h,mCpm		;display "insert cp/m disk"	
	call	dispMsg
	call	rcvCon		;wait for a character

noDisk	lxi	h,mExit		;display "exiting" message
	call	dispMsg
	jmp	CPMBOOT		;reboot CP/M

;--------------------------------------------------------------
; chkCpm - check if running under CP/M. CP/M flag is set true
;   (non-zero) if yes, cleared otherwise.
;--------------------------------------------------------------
; First, initialize entries for stand-alone

chkCpm	xra	a
	sta	cpmFlag		;clear CP/M flag
	mvi	a,MINDRV+'0'	;ascii for minimum drive number
	sta	mDrvMin		;store in the drive prompt message
	sta	baseDrv		;ascii 1st drive - baseDrv = 0
	mvi	a,MAXDRV+'0'	;ascii for max drive number
	sta	mDrvMax

; Determine if we're under CP/M or standalone. CP/M is assumed if
;   a jump instruction is present at the CP/M warm start location (0)
;   and five more jumps (e.g., a jump table) is present at the
;   jump-to destination.
		
	lda	WBOOT		;see if jump instruction present for CP/M
	cpi	JMPINST
	rnz			;no, not CP/M

; A jump instruction is present at the CP/M warm boot location (0),
;   now see if that jump points to five more jumps. If so, assume CP/M

	lxi	h,WBOOT+1	;point to lsb of jump address
	mov	e,m		;e=low byte of jump
	inx	h
	mov	d,m		;de=destination of jump
	mvi	b,5		;look for 5 more jumps (a jump table)

jmpTest	ldax	d		;a=opcode at jump destination
	sui	JMPINST		;another jump present?
	rnz			;no, not CP/M
	inx	d		;move to next jump
	inx	d
	inx	d
	dcr	b
	jnz	jmpTest

	dcr	a		;a=0ffh
	sta	cpmFlag		;CP/M flag to non-zero = true

; We're running under CP/M. Change drive prompt message to show drive
;    letters instead of drive numbers and change baseDrv to convert
;    an 'A' to the base drive number (MINDRV).

	mvi	a,'A'		;'A' in drive message instead of number
	sta	mDrvMin
	adi	MAXDRV-MINDRV	;max drive letter
	sta	mDrvMax
	mvi	a,'a'		;ascii 1st drive - baseDrv = 0
	sta	baseDrv
	ret

;------------------------------------------------------------------
; sizeRam - determine how much RAM we have for buffering tracks.
;   Sets the bufEnd variable which points to end address of 
;   the last possible track buffer + 1
;------------------------------------------------------------------
; If running under CP/M, use the BIOS pointer to determine the top
;    of available RAM. Otherwise, size RAM manually.

sizeRam	lhld	WBOOT+1		;get address of BIOS jump table+3
	dcx	h		
	dcx	h
	dcx	h		;hl->top of usable ram+1
	lda	cpmFlag		;running under CP/M?
	ora	a
	jnz	ramEnd		;yes, we have the end of RAM

;  Not under CP/M. Find the end of RAM manually.

	lxi	h,(trkBuf+0ffh) AND 0ff00h   ;next 256 byte boundary

ramLoop	mov	a,m		;a=current RAM content
	inr	m		;change RAM
	cmp	m		;did RAM change?
	mov	m,a		;restore RAM
	jz	ramEnd		;end of RAM found

	inr	h		;next page
	jnz	ramLoop

; ramEnd - end of RAM found. Now determine the end address + 1 of the
;   last track buffer that will fit in RAM. Store in bufEnd

ramEnd	xchg			;de=end of RAM + 1
	mvi	a,-((TRKLEN SHR 8) + 1) AND 0ffh
	cmp	d		;force de < (10000h - TRKLEN)
	jnc	topOk

	mov	d,a		;limit max address

topOk	lxi	h,trkBuf	;hl=start of track buffer
	lxi	b,TRKLEN	;bc=length of track in bytes

; Loop increasing hl by TRKLEN until hl > end of RAM.

bfEndLp	dad	b		;hl=hl+track length
	call	cmpHlDe		;compare hl-de
	jc	bfEndLp		;still more room, keep going

; Subtract one track length from hl, this will be the end address + 1 of
;   the the last track buffer that will fit in RAM

	lxi	b,-TRKLEN	;subtract one track length
	dad	b		;hl = end address of last track + 1
	shld	bufEnd		;save as bufEnd
	ret

;--------------------------------------------------------------------
; cmHlDe - compare HL-DE. Carry set if HL<DE, carry clear if HL>=DE
;--------------------------------------------------------------------
cmpHlDe	mov	a,l		;compare HL-DE, do lsbs first
	sub	e
	mov	a,h		;do msbs
	sbb	d
	ret			;carry set HL<DE, clear HL>=DE

;--------------------------------------------------------------------
; cmDeHl - compare DE-HL. Carry set if DE<HL, carry clear if DE>=HL
;--------------------------------------------------------------------
cmpDeHl	mov	a,e		;compare DE-HL, do lsbs first
	sub	l
	mov	a,d		;do msbs
	sbb	h
	ret			;carry set DE<HL, clear DE>=HL

;--------------------------------------------------------------------
; subDeHl - HL=DE-HL
;--------------------------------------------------------------------
subDeHl	mov	a,e		;subtract DE-HL, do lsbs first
	sub	l
	mov	l,a		;lsb result in l
	mov	a,d		;do msbs
	sbb	h
	mov	h,a		;msb result in h	
	ret

;---------------------------------------------------------------------
; Message constants
;---------------------------------------------------------------------
mWelcom	db	cr,lf,lf
	db	'===== Floppy to PC Disk Image Transfer =====',cr,lf
	db	'       (Mostek AID-80F Computer, ver 1.2)',cr,lf,lf
	db	'Sends an image of a SSSD 8" floppy to a PC via the'
	db	cr,lf
	db	'console serial port using the XMODEM protocol.',0

mDrive	db	cr,lf,lf,'Insert and specify source drive ('
mDrvMin	db	'x-'
mDrvMax	db	'x) or X to exit: ',0

mPort	db	cr,lf,lf
	db	'Specify the port to use for file transfer',cr,lf
	db	'  1) 88-2SIO port A (which is also the console)',cr,lf
	db	'  2) 88-2SIO port B',cr,lf
	db	'  3) 88-SIO',cr,lf
	db	'Enter port or X to exit: ',0

mStart	db	cr,lf,lf,'Start XMODEM receive operation on the PC now...',0

mDone	db	cr,lf,lf,'Transfer complete',cr,lf,lf
	db	'A list of tracks with errors (if any) follows:'
	db	cr,lf,'  ',0

mXmdm	db	cr,lf,lf,'XMODEM communication failure',cr,lf,0

mExit	db	cr,lf,lf,'Exiting...',cr,lf,0

mCpm	db	cr,lf,lf
	db	'Insert CP/M disk into drive A, then press Return...',0


;****************************************************************************
;
;  Hardware specific console and serial I/O routines. 
;     The following four routines must be written to provide a common
;     interface to the hardware on which this program is running. The
;     port number specified for serIn and serOut matches the port number
;     input from the operator via the port menu.
;
;****************************************************************************

; Mostek serial I/O port equates

SIOCR	equ	0DDh		;serial port control register
SIODR	equ	0DCh		;serial port data register

SIOTDRE	equ	080h		;transmit register empty bit
SIORDRF	equ	040h		;receive register full bit

; The rcvByte subroutine above times a one second timeout with a code
;    loop that calls the hardware specific serIn routine below. ONESEC
;    must be set based on processor speed and the number of cycles in 
;    the serIn call + 46 cycles for the rcvByte code. Mostek runs
;    at 2.4678 mhz.

ONESEC	equ	32768		;rcvByte loop count for 1 second

;----------------------------------------------------------------------------
; conIn, serIn - input character from console (29 cycles)
;    inputs:
;    outputs: z true if no character present
;	      z false if character returned in a
;    clobbers none
;----------------------------------------------------------------------------
conIn	equ	$
serIn	in	SIOCR		;(11) see if a new character is present
	ani	SIORDRF		;(7)
	rz			;(11) no character, return zero status

	in	SIODR		;return character and non-zero status
	ret	

;----------------------------------------------------------------------------
; conOut - output character to console
;    inputs: b = character to send
;    clobbers a
;----------------------------------------------------------------------------
conOut	equ	$
serOut	in	SIOCR		;wait for OK to transmit
	ani	SIOTDRE
	jz	conOut

	mov	a,b		;a=character to transmit
	out	SIODR		;send it
	ret

;****************************************************************************
;
; Mostek disk I/O routines
;
;****************************************************************************

; PROM Disk Driver Equates

DSKDRV	equ	0EC00h		;disk driver entry point

dcSTAT	equ	10h		;return 1771 status
dcREAD	equ	11h		;read sector
dcWRITE	equ	12h		;write sector
dcSEEK	equ	13h		;seek track with verify
dcRESTR	equ	14h		;restore to track 0
dcGETRK	equ	15h		;read track # from disk, put in 1771
dcDEL	equ	16h		;write deleted sector data mark
dcFMT	equ	17h		;seek track, issue write track for fmt

dsPARAM	equ	80h		;bad parameter passsed in DCB
dsNRDY	equ	40h		;disk not ready
dsSKERR	equ	20h		;seek error
ds1771	equ	1Ch		;1771 status bits masked by 1C

;----------------------------------------------------------------------------
; dRead - Read one sector based on the track, sector, and buffer address
;     already present in the DCB
;
; On Exit: Z true if read was successful
;    	   Z false if read failed
;	   dcbDma->next 128 byte boundary
; Clobbers all
;----------------------------------------------------------------------------
dRead	mvi	a,dcREAD	;set read command in DCB
	sta	dcbCmd

	lxiy	dcb		;IY->DCB
	call	DSKDRV		;call disk driver in firmware
	rnz

; Read was successful. The driver puts the last four bytes
;     of the sector in the DCB instead of the destination buffer.
;     Move these last four bytes to the buffer. Update dcbDma
;     to point to the next 128 byte boundary.

	lxi	b,4		;BC=counter of bytes to move
	lhld	dcbDma		;HL->end of initial 124 bytes
	xchg			;DE->end of initial 124 bytes
	lxi	h,dcbLst4	;HL->last 4 data bytes in DCB
	ldir			;copy from DCB to user buffer

	xchg			;HL=original dcbDma+128
	shld	dcbDma		;update buffer address in the DCB
	ret			;Z flag still set from driver call

;----------------------------------------------------------------------------
; dWrite - Write one sector based on the track, sector, and buffer address
;     already present in the DCB
;
; On Exit: Z true if read was successful
;    	   Z false if read failed
;	   dcbDma->next 128 byte boundary
; Clobbers all
;----------------------------------------------------------------------------
;  The driver expects the last four data bytes of the sector to be in
;     the DCB, not the user's buffer. Copy the last four bytes from
;     the dmaAddr+124 to the DCB.

dWrite	lxi	b,4		;BC=counter of bytes to move
	lhld	dcbDma		;HL->sector buffer
	lxi	d,SECLEN-4	;compute offset of bytes 125-128
	dad	d		;HL->user buffer + 124
	lxi	d,dcbLst4	;DE->last 4 data bytes in DCB
	ldir			;copy from user buffer to DCB

	push	h		;save HL = dcbDma+SECLEN

; Issue the write call to the device driver. Increment dcbDma by
;    128 bytes if the write was successful.

	mvi	a,dcWRITE	;set write command in DCB
	sta	dcbCmd

	lxiy	dcb		;IY->DCB
	call	DSKDRV		;call disk driver in firmware
	pop	h		;restore HL = dcbDma+SECLEN
	rnz			;exit with write failure

	shld	dcbDma		;save dcbDma incremented by SECLEN
	ret			;Z flag still set from driver call

;--------------------------------------------------------------
; dSet1st - Determine the 1st sector number to read on a new
;    track, then set the sector and DMA address in the DCB.
;    The starting sector is set to be 45ms from the end of
;    the last sector read to give  time for the track step
;    and verify to complete and then be quickly coming upon
;    the computed sector number.
; On Entry:
;    dcbSec = last sector accessed
;    trkPtr = address of track buffer
; On Exit:
;    dcbSec = 1st sector to read
;    dcbDma = address of sector in track buffer
; Clobbers B,C,H,L
;--------------------------------------------------------------
dSet1st	lxi	h,dcbSec	;compute 1st sector to read
	mov	a,m		;a=last sector read
	adi	8		;8 sectors gives 7*6.4ms=45ms
	cpi	NUMSEC+1	;past 26?
	jc	dSet1		;no

	sui	NUMSEC		;wrap to sector 1-26

dSet1	mov	m,a		;save updated sector back to DCB

; compute and set buffer address based on start sector

	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
	shld	dcbDma		;save as buffer pointer in DCB
	ret

;--------------------------------------------------------------
; dNxtSec - Increment and wrap the sector number and buffer
;    pointer in the DCB.
; On Entry:
;    dcbSec = last sector accessed
;    dcbDma = most recent transfer buffer+128
;    trkPtr = address of track buffer
; On Exit:
;    dcbSec = next sector to read
;    dcbDma = address of next sector in track buffer
; Clobbers H,L
;--------------------------------------------------------------
dNxtSec	lxi	h,dcbSec	;increment and wrap sector
	inr	m
	mov	a,m		;a=last sector read
	cpi	NUMSEC+1	;past the last sector?
	rc			;no, exit

	mvi	a,1		;wrap to sector one
	mov	m,a		

	lhld	trkPtr		;set dcbDma to sector 1 in track
	shld	dcbDma

	ret

;**************************************************************************
; 
;  Data area
;
;**************************************************************************

;-------------------------------------------------------------------------
; dcb - disk control block. This structure is used to pass parameters
;    to the firmware disk driver.
;-------------------------------------------------------------------------
dcb	equ	$
	db	0,0,0		;0-2 not used
dcbDrv	db	0		;drive number (0-3) at offset 3
dcbFn	db	0,0,0,0,0,0,0,0,0,0,0	;file name (not used) 04-0E
dcbCmd	db	0		;command at offset 0F
	db	0,0,0,0,0,0,0
dcbStat	db	0		;status of command at 17
	db	0
dcbDma	db	0,0		;transfer buffer address at 19,1A
	db	0,0
dcbWSec	db	1		;number of sectors for dump (not used)
dcbSec	db	1		;sector to read/write at offset 1E
dcbTrk	db	0		;track to read/write at offset 1F
dcbLst4	db	0,0,0,0		;last four bytes of track are placed here

; disk variables

trkPtr	ds	2		;pointer into trkBuf at track boundaries
bufEnd	ds	2		;end address + 1 of last track spot in RAM
secCnt	ds	1		;sector counter

; retry and error storage

rdRetry	ds	1		;disk read retry counter
trkErr	ds	1		;non-zero if current track had an error
errTbl	ds	NUMTRK		;tracks with errors indexed by track	

; xmodem variables

xmRetry	ds	1		;xmodem retry counter
blkNum	ds	1		;current xmodem block number
crcFlag	ds	1		;non zero if using CRC instead of checksum
crc16	ds	2		;crc-16 result
sndPtr	ds	2		;pointer for start of XMODEM send

; misc variables

cpmFlag	ds	1		;non-zero if running under CP/M
baseDrv	ds	1		;ascii 1st drive - baseDrv = 0
	ds	64		;stack space
ourStk	equ	$

; track buffer runs from here to the end of memory

	ds	PKTLEN		;space for pre-pended unsent data
trkBuf	equ	$

	end
