;******************************************************************************
;
;  PC2Flop - Transfer disk image from PC to floppy over 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 writes a SSSD soft-sectored 8" floppy with a disk image
;	transmitted from 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.
;
;	Since this is a soft sectored controller, the destination disk must
;	have been formatted at some point. To allow this program to create
;	a new disk for a "cold" machine, it offers a format disk option. 
;
;	For this hardware, the console port and transfer port are the same,
;	so most error messages won'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 the error message
;	until a C/R is received from the serial port. If the program seems
;	to hang, cancel the file transfer on the PC, then press C/R to see
;	the error message.
;
;	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 PC2FLOP 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	4		;number of read (verify) attempts
WRTRIES	equ	3		;number of write attempts

; 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

; CP/M Equates

WBOOT	equ	0		;warm boot jump location

; 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	
;-----------------------------------------------------------------------------
;  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 whether or not to format the disk

	lxi	h,mFormat	;display format prompt
	call	dispMsg
	call	rcvCon		;get byte
	ori	20h		;upper to lower case, nums not affected
	cpi	'y'		;format requested?
	cz	format		;yes, go format disk

; Prompt the user to insert the disk to write, initialize, then start
;   sending NAK to start the XMODEM reception.

	lxi	h,mStart	;display start file transfer prompt
	call	dispMsg

	xra	a		;init track to zero
	sta	dcbTrk

	inr	a		;init xmodem block number to one
	sta	blkNum
	sta	dcbSec		;init DCB with a valid sector number

	lxi	h,trkBuf	;trkBuf is initial starting point for
	shld	rcvPtr		;   XMODEM receive

	mvi	a,NAK		;send starting nak after purging input data
	call	purgSio

;-----------------------------------------------------------------------------
;  mainLp - Receive as many packets as will fit into RAM and then write
;      tracks to disk. Repeat until entire disk is written. 
;-----------------------------------------------------------------------------
mainLp	call	bufPkts		;buffer as many packets as possible
	xchg			;de=start of last packet received
	lxi	h,trkBuf	;hl=start of track buffer
	shld	trkPtr		;writing will start from here
	call	cmpDeHl		;compare last packet start - trkBuf
	jc	allDone		;nothing received but an EOT

; trkLoop - write tracks until all disk tracks have been written or all
;    the tracks in the buffer have been written.
	
trkLoop	call	wrtTrk		;write a track (hl returns->next track)

	lda	dcbTrk		;increment track number (preserve HL)
	inr	a
	sta	dcbTrk	
	cpi	NUMTRK		;done all tracks?
	jz	allDone		;yes

	xchg			;de=next track pointer
	lhld	rcvPtr		;hl=start of last packet received
	call	cmpHlDe		;compare rcvPtr-start of next track
	jnc	trkLoop		;another track is present

; All the buffered tracks have been written. Move any bytes left over
;    from the xmodem receive to the start of trkbuf. Adjust rcvPtr
;    to the end of that data which is where reception of the next
;    packet will begin.

	call	subHlDe		;hl=hl-de = left over byte count - PKTLEN
	mov	a,l		;a=left over count - PKTLEN
	adi	PKTLEN		;a=left over byte count
	mov	b,a		;b=left over byte count (bytes to move)
	lxi	h,trkBuf	;hl->start of trkBuf
	jz	mvDone		;no bytes to move, we're done
	
; Copy the leftover data to the start of trkBuf. The next XMODEM receive
;   sequence will start at the end of this copied data instead of at the
;   start of trkBuf.

moveLp	ldax	d		;move from end of trkBuf to start of trkBuf
	mov	m,a
	inx	h		;hl->starting bytes of trackbuf
	inx	d		;de->unwritten bytes from end of trkBuf		
	dcr	b
	jnz	moveLp

mvDone	shld	rcvPtr		;save address where to receive next packet

	mvi	a,ACK		;ACK the last packet or EOT
	call	sndByte

	jmp	mainLp		;go receive more packets

; allDone - The disk is completely written. ACK the final packet (or EOT),
;     display the "transfer complete" message and start the program over.

allDone	mvi	a,ACK		;ACK the final packet or EOT
	call	sndByte

	lxi	h,mDone		;hl->done message
	call	dispMsg
	jmp	getDrv		;start over asking for drive num

;-----------------------------------------------------------------------------
; bufPkts - Receive XMODEM packets until we fill all the track buffer
;   space available. Reception starts at the address pointed to by
;   rcvPtr. Reception is stopped when the last packet exactly
;   fills or "crosses" the end of the last track spot in RAM. rcvPtr is
;   updated on exit to point to the start of the last packet received.
;   The disk write routines use this pointer to know where data ends and
;   then update rcvPtr to start to where to begin receiving new packets.
;-----------------------------------------------------------------------------
bufPkts	call	rcvPkt		;receive a packet
	xchg			;de=current xmodem receive pointer
	cpi	EOT		;EOT received?
	jz	bufDone		;yes, buffering is done

	lhld	bufEnd		;hl=end of buffering space
	call	cmpDeHl		;compare current-end	
	jnc	bufDone		;past or at end, buffering is done

	mvi	a,ACK		;otherwise, ACK the packet
	call	sndByte
	xchg			;put xmodem rcv pointer back in HL
	jmp	bufPkts		;buffer some more

; bufDone - no more room for packets. Update rcvPtr to point to the 
;   start of the last packet received so the disk write routines know
;   where data ends.

bufDone	xchg			;hl=current xmodem receive pointer
	lxi	d,-PKTLEN
	dad	d		;hl=hl-PKTLEN = start of last rcv'd packet
	shld	rcvPtr		;save use by write tracks
	ret

;-----------------------------------------------------------------------------
; rcvPkt - receive an xmodem format 128 byte packet. HL points to the 128 byte
;    buffer to receive. HL is updated by 128 bytes after each succuessful
;    reception of a new 128 byte packet.
;-----------------------------------------------------------------------------
nakPkt	pop	h		;get HL back
	mvi	a,NAK
	call	purgSio		;purge receive for 1s then transmit NAK

;wait for SOH or EOT

rcvPkt	push	h		;save HL for retries
waitSoh	call	chkQuit		;check for user abort
	mvi	a,3		;3 second timeout for soh
	call	rcvByte		;receive character with timeout
	jz	nakPkt		;timeout

	cpi	SOH		;SOH received?
	jz	haveSoh

	cpi	EOT		;EOT to say we're done?
	jnz	waitSoh		;no, keep looking for SOH

	pop	h		;undo the entry push
	ret			;return with EOT

;  Have SOH, receive block number and not block number

haveSoh	mvi	a,1		;one second timeout once inside a packet
	call	rcvByte		;get the block number
	jz	nakPkt		;timeout, NAK and start over

	sta	rcvBlk		;save the received block number
	mvi	a,1		;one second timeout
	call	rcvByte		;get not block number
	jz	nakPkt		;timeout, NAK and start over

	sta	rcvNBlk		;save not block number

;  Receive the 128 byte block

	mvi	e,0		;init checksum
	mvi	d,pktLen	;d is byte counter
pktLoop	mvi	a,1		;one second timeout
	call	rcvByte		;get next data byte
	jz	nakPkt		;timeout

	mov	m,a		;store the character
	inx	h		;point to next byte
	add	e		;update the checksum
	mov	e,a
	dcr	d		;decrement bytes remaining
	jnz	pktLoop

;  Verify the checksum

	mvi	a,1		;one second timeout
	call	rcvByte		;get the checksum
	jz	nakPkt		;timeout

	cmp	e		;checksum match?
	jnz	nakPkt		;no, packet error

;  Checksum good. Verify block numbers

	lda	rcvNBlk		;verify rcvBlk = not (rcvNBlk)
	cma			;A should = block number now
	mov	b,a		;save in b
	lda	rcvBlk
	cmp	b		;compare rcvBlk = not(rcvNBlk)?
	jnz	nakPkt		;no, error

	lda	blkNum		;compare rcvBlk = expected blkNum
	cmp	b
	jnz	nakPkt		;nak if not the same (also nak's re-send)

	inr	a		;increment expected block number
	sta	blkNum
	pop	b		;get HL off stack, but don't clobber HL
	xra	a		;return a zero
	ret

;-----------------------------------------------------------------------------
; wrtTrk - write and verify NUMSEC sectors to the current track in trkBuf
;   as pointed to by trkPtr. After the track is written, trkPtr is updated
;   by the length of a track to point to the next track buffer. This
;   saved value is also returned in HL.
;-----------------------------------------------------------------------------
wrtTrk	mvi	a,WRTRIES	;init write retry counter
	sta	wrRetry

	call	dSet1st		;set 1st sector, buffer in the DCB

; rtryWrt - track write and rewrite loop

rtryWrt	mvi	a,NUMSEC	;init sector counter
	sta	secCnt

wrtLoop	call	dWrite		;write the sector specified in the DCB
	jnz	wrtErr		;write failed - write protected?

	call	dNxtSec		;increment and wrap sector
	lxi	h,secCnt	;decrement sector count
	dcr	m
	jnz	wrtLoop		;loop until all sectors written

; Verify the track just written

	mvi	a,RDTRIES	;init retry counter
	sta	rdRetry

	mvi	a,NUMSEC	;init sector counter
	sta	secCnt

vfyLoop	lxi	h,secBuf	;read into ignored secBuf
	shld	dcbDma
	call	dRead	
	jz	vfyPass		;verify passed

; Verify failure. Retry reading, and if it still fails, retry writing

	call	chkQuit		;check for ctrl-c

	lxi	h,rdRetry	;decrement read retry counter
	dcr	m
	jnz	vfyLoop		;try again until zero

	lxi	h,wrRetry	;decrement write retry counter
	dcr	m
	jnz	rtryWrt		;retry with new write

	jmp	vfyErr		;give up.
	
; vfyPass - Verify passed. Increment to next sector and repeat until all
;    sectors written and verified.

vfyPass	call	dNxtSec		;increment and wrap sector
	lxi	h,secCnt	;decrement sector count
	dcr	m
	jnz	vfyLoop		;loop until all sectors verified

; Track written and verified, 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

;-----------------------------------------------------------------------------
; Disk write or verify failure. Wait for the operator to press a C/R,
;    then display the track and sector with the error and restart the
;    program.
;-----------------------------------------------------------------------------
wrtErr	lxi	h,mWrtErr	;'Write protected or format needed'
	jmp	errExCr		;disply message and exit

vfyErr	lxi	h,errTrk	;hl->where to put ascii decimal
	lda	dcbTrk		;a=track with error on it
	call	bin2dec		;track to ascii

	lxi	h,errSec	;hl->where to put ascii sector
	lda	dcbSec		;a=sector where error occured
	call	bin2dec

	lxi	h,mDskErr	;hl->error message

; errExCr - HL points to an error message to display. To be sure the
;    operator sees the message, wait for them to press a C/R before
;    displaying the message.

errExCr	call	conIn		;wait for operator to type something
	jz	errExCr

	cpi	CR		;carriage return pressed?
	jnz	errExCr		;no, keep waiting

errExit	call	dispMsg
	lxi	sp,ourStk	;initialize stack pointer
	jmp	getDrv		;start over asking for drive num

;-----------------------------------------------------------------------------
; 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

;-----------------------------------------------------------------------------
; purgSio - wait for 1 second of idle time on receive line, then transmit the
;	character passed in A (usually ACK or NAK). Clobbers b
;-----------------------------------------------------------------------------
purgSio	push	a

purge	mvi	a,1		;1 second timeout
	call	rcvByte		;wait for 1 second without activity
	jnz	purge

	pop	a		;get back the character to send
				;fall through to sndByte
;-----------------------------------------------------------------------------
; sndByte - send the byte in a through the specified transfer port. 
;     Clobbers b. (assuming serOut clobbers only a).
;-----------------------------------------------------------------------------
sndByte	mov	b,a		;b=byte to transmit
	jmp	serOut		;send the character

;-----------------------------------------------------------------------------
; rcvByte - Receive a byte from the specified transfer port. A wait timeout
;     in seconds is specified in a. 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 the zero flag is false. ONESEC must be set based
;     on processor speed and the number of cycles in the serIn call + 51.
;     Clobbers a, b and c.
;-----------------------------------------------------------------------------
rcvByte	push	d		;save d, e
	mov	d,a		;save timeout in d

initSec	lxi	b,ONESEC	;one second timeout 51+29 cycles per loop

rcvWait	call	serIn		;(17+cycles in serIn) look for a byte
	jnz	haveChr		;(10) byte received

	dcx	b		;(5) otherwise, decrement timer
	mov	a,b		;(5) one second expire?
	ora	c		;(4)
	jnz	rcvWait		;(10) no, keep waiting

; one second timeout has occured. Decrement the seconds counter.

	dcr	d		;decrement seconds counter
	jnz	initSec		;initialize for another 1 second count
haveChr	pop	d		;restore d, e
	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

;--------------------------------------------------------------
; 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. Subtract 256 bytes from this to make room
;   for xmodem packet overflow at the end. 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	dcr	d		;subtract 256 bytes from end of RAM
	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

;--------------------------------------------------------------------
; subHlDe - HL=HL-DE
;--------------------------------------------------------------------
subHlDe	mov	a,l		;subtract HL-DE, do lsbs first
	sub	e
	mov	l,a		;lsb result in l
	mov	a,h		;do msbs
	sbb	d
	mov	h,a		;msb result in h	
	ret

;-----------------------------------------------------------------------------
;   Data Area
;-----------------------------------------------------------------------------
mWelcom	db	cr,lf,lf
	db 	'===== PC to Floppy Disk Image Transfer =====',cr,lf
	db	'       (Mostek AID-80F Computer, ver 1.2)',cr,lf,lf
	db	'Writes a SSSD 8" floppy with a disk image received via the'
	db	cr,lf
	db	'console serial port using the XMODEM protocol.',cr,lf,lf
	db	'If the file transfer hangs, cancel the transfer at the PC,'
	db	cr,lf
	db	'then press RETURN on this computer to see the errors.',0

mDrive	db	cr,lf,lf,'Insert and specify destination drive ('
mDrvMin	db	'x-'
mDrvMax	db	'x) or X to exit: ',0

mStart	db	cr,lf,lf
	db	'Start XMODEM send now...',0

mDone	db	cr,lf,lf,'Creation of new disk successful!',cr,lf,0

mNoTrk0	db	cr,lf,lf,'Seek to track 0 failed',cr,lf,0

mWrtErr	db	cr,lf,lf,'Disk is write protected or requires formatting',0

mDskErr	db	cr,lf,lf,'Write failure on track '
errTrk	db	'xx, sector '
errSec	db	'xx. Disk creation aborted.',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

mFormat	db	cr,lf,lf,'Format the target disk? ',0

mFmting	db	cr,lf,'  Formatting',0

mFmtDon	db	cr,lf,'  Format complete',0

mFmtErr	db	cr,lf,lf,'Formatting failed',cr,lf,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 + 51 cycles for the rcvByte code. Mostek runs
;    at 2.4678 mhz.

ONESEC	equ	30720		;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

; Floppy controller board equates

FDBCTL	equ	0E3h		;floppy board control register
FFRES1	equ	020h		;bit mask to assert FIFO reset
FFRES0	equ	01Fh		;clear FIFO reset (and buffer bits)

;----------------------------------------------------------------------------
; 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			;exit with read failure

; 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

;**************************************************************
;
; format - The disk format routine is custom to the controller
;    and firmware disk drivers in the Mostek computer. The
;    firmware write track command is used to format the disk.
;
;**************************************************************
format	lxi	h,mFmting	;'formatting...' message
	call	dispMsg

	call	initFmt		;initialize trkBuf for format

	lxi	h,trkBuf	;put track buffer address in DCB
	shld	dcbDma

	mvi	a,dcFMT		;put format track command in DCB
	sta	dcbCmd

	xra	a		;put starting track number in DCB
	sta	dcbTrk

	inr	a		;make sure sector has a valid value
	sta	dcbSec

	lxiy	dcb		;point iY to the DCB

; nextTrk - Format the next track.

nextTrk	lxi	h,TRKID		;HL->1st track ID in trkBuf
	mvi	c,NUMSEC	;C=sector counter
	lxi	d,SECLEN+58	;DE=length of sector when formatting
	lda	dcbTrk		;A=current track number

; Put the current track number in the ID field of each sector on the track

tidLoop	mov	m,a		;store track # in sector track ID
	dad	d		;compute address of next sector
	dcr	c
	jnz	tidLoop

; Issue the write track command after issuing a FIFO reset

	in	FDBCTL		;reset FIFO buffer on FDC board
	ori	FFRES1		;set reset bit
	out	FDBCTL		;issue the reset

	ani	FFRES0		;clear reset bit, no buffering
	out	FDBCTL

	call	DSKDRV		;call the firmware disk driver
	jnz	fmtErr		;format failure
	
; display '.' pacifier, check for operator abort, then jump to
;     nextTrk to format the next track
		
	mvi	b,'.'		;display '.' pacifier per track
	call	conOut		;display pacifier
	call	chkQuit		;see if abort requested

	lxi	h,dcbTrk	;increment and check track number
	inr	m
	mov	a,m		;A=next track number
	cpi	NUMTRK		;done?
	jnz	nextTrk		;no, do next track

	lxi	h,mFmtDon	;format complete message
	jmp	dispmsg		;display message and return

; fmtErr - Display format failure message and exit

fmtErr	lxi	h,mFmtErr	;format fail message
	jmp	errExit		;display error and start over

;-------------------------------------------------------------------
;  initFmt - initialize a full track of data for formatting the
;     disk. The actual track number is filled in later at the
;     write track commands are issued.
;-------------------------------------------------------------------
initFmt	lxi	h,trkBuf	;HL->track of format data

	mvi	b,40		;gap 4 preindex 40 bytes of FF
	mvi	a,0FFh		;A=fill value
	call	fill

	mvi	b,6		;6 bytes of 00
	xra	a
	call	fill

	mvi	m,0FCh		;index mark of FC
	inx	h

	mvi	b,26		;26 bytes of FF
	mvi	a,0FFh
	call	fill

; fillSec - fill repeating sector portion of the track

	mvi	c,1		;c=sector number

fillSec	mvi	b,6		;6 bytes of 00
	xra	a
	call	fill

	mvi	m,0FEh		;ID address mark of FE
	inx	h

	mov	m,a		;track number (updated later)
	inx	h
	mov	m,a		;00 after track
	inx	h

	mov	m,c		;sector number
	inx	h
	mov	m,a		;00 after sector
	inx	h

	mvi	m,0F7h		;force CRC code F7
	inx	h

	mvi	b,11		;11 pre-data bytes of FF
	mvi	a,0FFh
	call	fill

	mvi	b,6		;6 pre-data bytes of 00
	xra	a
	call	fill

	mvi	m,0FBh		;data address mark of FB
	inx	h

	mvi	b,128		;128 sector bytes of E5
	mvi	a,0E5h
	call	fill

	mvi	m,0F7h		;force CRC code F7
	inx	h

	mvi	b,27		;27 post data bytes of FF
	mvi	a,0FFh
	call	fill

	inr	c		;move to next sector number
	mov	a,c		;done all sectors?
	cpi	NUMSEC+1
	jc	fillSec		;no, do next sector

; sectors done, fill with 256 trailing FF for rest of track and then some

	mvi	b,0		;256 bytes of FF
	mvi	a,0FFh
				;fall into fill and exit

; fill - store A (HL++) B times

fill	mov	m,a
	inx	h
	dcr	b
	jnz	fill
	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,0,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 (start of next track)
bufEnd	ds	2		;end address + 1 of last track spot in RAM
secCnt	ds	1		;sector counter
secBuf	ds	128		;sector buffer for read/verify

; Retry logic variables

rdRetry	ds	1		;disk read retry counter
wrRetry	ds	1		;disk write retry counter

; Xmodem variables

blkNum	ds	1		;current xmodem block number
rcvBlk	ds	1		;block number received
rcvNBlk	ds	1		;"not" block number received
rcvPtr	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	96		;stack space
ourStk	equ	$

; Track buffer runs from here to the end of memory

trkBuf	equ	$

TRKID	equ	trkBuf+80	;offset of 1st track ID in trkBuf

	end

