;*****************************************************************************
;
; Flop2PC - Transfer floppy image to PC over a serial port
;	      (for iCOM 3812 and 3712 disk controllers)
;
;	This program transmits an image of a soft-sectored 8" floppy as
;	used in the iCOM 3812 or 3712 to a PC. The image is transmitted
;       using the XMODEM protocol through either port on an 88-2SIO or 
;       through an 88-SIO port.
;
;	The program runs standalone at 0x100 or under CP/M. The program
;	talks directly to the controller and the serial ports so it does
;	not require CP/M or any other OS to function.
;
;	Single and double density disks are supported with the 3812.
;       The 3712 supports single density only.
;
;	This program works best if the console port and the transfer port
;	are not the same device. Otherwise, most error messages won't
;	be seen as the file transfer dialog box prevents their display.
;
;	Written by Patrick Linstruth based on FLOP2PC for the Tarbell
;       controller by Mike Douglas.
;
;	Rev	 Date	    Desc
;	1.0	01/19/21    Original
;	
;	1.1     01/21/28    M. Douglas, Update to run on real hardware.
;			    Add support for the FD3712.
;
;--------------------------------------------------------------------------
;
; FLOP2PC first allocates a track buffer by searching for the top of
; memory. The track buffer starts at trkBuf and ends at bufEnd. When
; running under CP/M the top is memory is determined by the start of
; the BIOS.
;
; mainLp is the main loop that reads tracks from disk into trkBuf
; with readTrk. The FD3812 supports single or double density, so during
; initialization, the density of track 1 is determined and the disk
; flagged as single or double density based on that result.
;
; readTrk reads all the sectors of the track into trkBuf pointed
; to by trkPtr. After all sectors are read, trkPtr points to the
; end of track data + 1.
;
; When trkBuf will not fit another complete track, sndTrks
; sends trkBuf up to trkPtr in 128 byte packets using
; the Xmodem protocol. When all packets have been sent,
; mainLp reads the next tracks into trkBuf until all tracks
; have been read and sent.
;
;
;                          ************
;                          *  trkBuf  *
;                          ************
; sndPtr increased by +--> *  TRK X   * <--+ trkPtr is increased by
;    length of packet |    ************    | trkLen as tracks are
; as packets are sent +--> *  TRK X+1 * <--+ read from disk
;         over Xmodem |    ************    |
;                     |    * ........ *    |
;                     v    * ........ *    v
;                          * ........ *
;                          ************
;                          *  TRK X+n *
;                          ************
;                          *  bufEnd  * <--- Tracks are read until
;                          ************      trkPtr + trkLen > bufEnd
;
;*****************************************************************************

; Disk information equates

MINDRV	equ	0		;first drive number
MAXDRV	equ	3		;max drive number 

NUMTRK	equ	77		;number of tracks
NUMSEC	equ	26		;maximum number of sectors
SDSECL	equ	128		;single density sector length
DDSECL	equ	256		;double density sector length
SDTRKL	equ	(NUMSEC*SDSECL)	;length of single density track
DDTRKL	equ	(NUMSEC*DDSECL)	;length of double density track

RDTRIES	equ	3		;read tries

; 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	01h
EOT	equ	04h
ACK	equ	06h
NAK	equ	15h
EOF	equ	1Ah		;ctrl-z character
XMTRIES	equ	10		;number of xmodem retries

; CP/M Equates

WBOOT	equ	0		;warm boot jump vector

; Misc equates

CR	equ	0Dh		;ascii for carriage return
LF	equ	0Ah		;ascii for line feed
DEL	equ	7Fh		;ascii DEL
BS	equ	08h		;ascii backspace
CTRLC	equ	03h		;ascii for control-c

	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
	call	s2Init		;initialize the 88-2SIO ports

;  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	drvNum		;save the drive number to use
	mov	b,a		;save drive number in b

	lda	mDrvMin		;a='0' or 'A'	
	add	b		;a=ascii drive number or letter
	sta	mSrcDrv		;update message with drive designator

	call	dSelDrv		;select drive
	call	dRestor		;restore to track 0
	call	ctlType		;get controller type, 3712 or 3812
	jnz	getPort		;if 3712, skip density detection

; For the 3812, test and display the disk density

	lxi	h,mGetDen	;hl->detecting disk density...
	call	dispMsg

	call	dGetDen		;get density of the disk that is inserted
	lxi	h,mBadDsk	;hl->disk cant be read
	jnz	errMsg		;couldn't read disk at all

	lxi	h,mSrcDet	;density detected message
	call	dispMsg

	lxi	h,mSD		;hl->single density message
	lda	dskDen		;is it single density?
	ora	a
	jz	dispDen		;yes
	lxi	h,mDD		;else, show double density message

dispDen	call	dispMsg		;display density
	lxi	h,mDrvDen	;display rest of density message
	call	dispMsg

; getPort - get serial port number from the user.

getPort	lxi	h,mPort		;hl->transfer port prompt
	call	dispMsg
	call	rcvCon		;get byte from the console
	ori	20h		;upper to lower case, nums not affected
	cpi	'x'		;exit requested?
	jz	pgmExit		;yes

	sui	'1'		;'1' - '3' to binary 0-2
	cpi	3		;validate ascii 1-3
	jnc	getPort		;invalid, prompt again

	sta	xfrPort		;save the port 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	;hl->start file transfer prompt
	call	dispMsg

clrRcv1	mvi	b,1		;wait for 1s of input clear
	call	rcvByte
	jnz	clrRcv1

	xra	a		;set CRC flag to false (checksum mode)
	sta	crcFlag

waitNak	call	chkQuit		;give user chance to abort
	mvi	b,1		;1 second timeout
	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	trkNum

	inr	a		;init xmodem block number to one
	sta	blkNum

;-----------------------------------------------------------------------------
;  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	trkNum		;increment track number (preserve HL)
	inr	a
	sta	trkNum	
	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	trkLen		;fit another whole track?
	dad	d
	xchg			;de=end of next 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	trkNum
	cpi	NUMTRK		;done all tracks?
	jnz	mainLp		;no, not done yet

; Send EOT and wait for response. Then display the success message and
;    start the program over.

	call	sndEot		;send and get response for EOT
	call	dRestor		;home

	lxi	h,mDone		;print the all done message
	call	dispMsg

	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	call	chkQuit		;check for ctrl-c from operator

	mvi	a,1		;init retry step
	sta	rtStep

	xra	a		;no error on this track yet
	sta	trkErr

rtSeek	call	dSeek		;seek to the trkNum
	jz	rtInit		;seek worked, init for read

; Seek error - a seek error means the controller was not able to read any
;   address mark after 16 revolutions. Restore to track 0 and attempt
;   re-seek one time. If that fails, then report all sectors bad and
;   don't attempt to read sectors since no address marks were readable.

	call	dClrErr		;clear the seek error
	lxi	h,rtStep	;decrement retry step
	dcr	m
	jm	rtFail		;no more retries, fail all sectors

	call	dRestor		;restore to track 0
	jmp	rtSeek		;try the seek again

rtFail	mvi	e,NUMSEC	;report all sectors bad

rtAll	call	dspErr		;display error on sector 
	dcr	e
	jnz	rtAll		;loop to report all sectors
	
	jmp	rtDone		;exit

; rtInit - initalize and then read all sectors on the track
	
rtInit	mvi	e,0FFh		;e=sector number, init to -1
	mvi	d,NUMSEC	;d=sector count

rtLoop	call	dNxtSec		;increment and point to next sector
rtRtry	call	dRead		;read sector e into (hl)
	jnz	secErr		;read error (dRead tried three times)

rtNext	dcr	d		;decrement sector count
	jnz	rtLoop		;more sectors

; Track is done, compute start address for next track

rtDone	lhld	trkLen		;hl=length of track
	xchg			;de=track length
	lhld	trkPtr		;hl=current track pointer
	dad	d		;hl->start of next track in trkBuf
	shld	trkPtr		;trkPtr->start of next track in trkBuf
	ret

;--------------------------------------------------------------------------
; secErr - read error retry logic. If a restore to track 0 with re-seek
;   has not already been performed, then do it and try againg to read
;   the failed sector. Otherwise, display the error and continue with
;   the next sector.
;--------------------------------------------------------------------------	
secErr	lxi	h,rtStep	;decrement to next retry step
	dcr	m
	jp	reSeek		;ok to try restore and reseek

	call	dspErr		;else display the sector error
	jmp	rtNext		;continue with next sector

reSeek	call	dRestor		;restore to track zero
	call	dSeek		;seek back to desired track
	call	dSecPtr		;point hl to re-read same sector
	jmp	rtRtry

;-----------------------------------------------------------------------------
; 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
	lxi	h,trkBuf	;trkBuf is initial starting point for

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
	rc			;done with track buffer, return

	lxi	b,-PKTLEN	;restore hl
	dad	b
	jmp	sndLoop

; sndDone - reset sndPtr to start of track buffer

sndDone	lhld	trkBuf		;hl=trkBuf
	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.

	mvi	b,2		;5 second timeout
	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		;else, clear crcFlag to force checksum

;  clrRcv2 - wait for one second of line clear time and send packet again.

clrRcv2	mvi	b,2		;2 second timeout
	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
	call	sndByte
	mvi	b,2		;2 second timeout
	call	rcvByte		;
	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
	jnz	reEot

;just give up - xfer was probably good

	lxi	h,mNoAck
	jmp	dispMsg

;-----------------------------------------------------------------------------
; 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
	inx	h		;move to next byte
	ora	a		;null terminates
	rz

	mov	b,a		;conOut wants character in b
	call	conOut
	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
	lda	xfrPort		;a=port to use for transfer
	jmp	serOut		;send the character

;-----------------------------------------------------------------------------
; rcvByte - Receive a byte from specified transfer port with a 'b' 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 + 59. 
;     Clobbers a, b, d, and e.
;-----------------------------------------------------------------------------
rcvByte lxi     d,ONESEC        ;de=cycles through this loop for 1s

rcvWait lda     xfrPort         ;(13) a=port to use for transfer
        call    serIn           ;(17+cycles in serIn)look for a byte
        rnz                     ;(5)byte received

        dcx     d               ;(5)otherwise, decrement timer
        mov     a,d             ;(5)one second expire?
        ora     e               ;(4)
        jnz     rcvWait         ;(10)no, keep waiting
	lxi	d,ONESEC	;reinitialize de
        dcr     b               ;seconds left?
        jnz     rcvByte		;yes, start another second

	ret			;return with timeout (zero true and in a)

;-----------------------------------------------------------------------------
; dspErr - 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.
;
;  on entry:
;     e = sector number
;  on exit:
;     clobbers b,h,l
;-----------------------------------------------------------------------------
dspErr	lda	trkErr		;test track error flag
	ora	a
	jnz	dspSec		;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,errTrk	;hl->where to put ascii decimal
	lda	trkNum		;a=track with error on it
	call	bin2dec		;track to ascii
	lxi	h,mTrkErr	;display the track error message
	call	dispMsg

; dspSec - display the sector number with an error

dspSec	lxi	h,errSec	;hl->where to put ascii sector
	mov	a,e		;a=sector where error occured
	call	bin2dec
	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

;--------------------------------------------------------------
; 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	drvNum		;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	mvi	a,MINDRV	;re-select drive A for CP/M 1.4
	sta	drvNum
	call	dSelDrv

	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	JMP
	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	JMP		;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,-((DDTRKL 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,DDTRKL	;bc=length of track in bytes

; Loop increasing hl by DDTRKL 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,-DDTRKL	;subtract one track length
	dad	b		;hl = end address of last track + 1
	shld	bufEnd		;save as bufEnd
	ret

;--------------------------------------------------------------
; ctlType - determine the controller type (3712 or 3812) based
;   on the interface board type. The interface type is based on
;   the PROM on the board. Only one PROM was ever used with
;   the 3812, so unique code in the 3812 PROM is checked.
; 
; On Exit:
;   ctl3712 set true (non zero) for 3712, zero for 3812.
;   Zero status matches ctl3712 value
;--------------------------------------------------------------
ctlType	lhld	0F016h		;two bytes from PROM to check
	mvi	a,79h		;3812 is 79h here
	sub	l
	mov	l,a		;l=0 if match 3812
	
	mvi	a,32h		;3812 is 32h here
	sub	h		;a=0 if match 3812
	ora	l		;a=0 only if both bytes match
	
	sta	ctl3712		;set result
	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
	
;--------------------------------------------------------------------
; errMsg - display message pointed to by hl and then start over
;--------------------------------------------------------------------
errMsg	call	dispMsg
	lxi	sp,ourStk	;initialize stack pointer
	jmp	getDrv		;start over asking for drive num

;---------------------------------------------------------------------
; Message constants
;---------------------------------------------------------------------
mWelcom	db	cr,lf,lf
	db	'===== Floppy to PC Disk Image Transfer =====',cr,lf
	db	'       (for iCOM 3712/3812, ver 1.1)',cr,lf,lf
	db	'Sends an image of an 8" floppy to a PC via an 88-2SIO'
	db	cr,lf
	db	'or 88-SIO 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

mBadDsk	db	cr,lf,lf,'Disk not readable',cr,lf,0

mGetDen	db	cr,lf,lf,'Detecting disk density...',0
mSrcDet	db	cr,lf,'Detected a ',0
mSD	db	'single',0
mDD	db	'double',0
mDrvDen	db	' density disk in drive '
mSrcDrv	db	'0.',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 1-3 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,0

mNoAck	db	cr,lf,lf,'No ACK received on EOT.',cr,lf,0

mXmdm	db	cr,lf,lf,'XMODEM communication failure',cr,lf,0

mNoTrk0	db	cr,lf,lf,'Seek to track 0 failed',cr,lf,0

mNotRdy	db	cr,lf,lf,'Drive not ready. Insert disk or Ctrl-C...',0

mTrkErr	db	cr,lf,'Track '
errTrk	db	'xx failed sectors: ',0
errSec	db	'xx ',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
	db	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.
;****************************************************************************

; 88-2SIO Serial Board Equates

s2aCtl	equ	10h		;1st port on 88-2SIO board - control register
s2aDat	equ	11h		;1st port on 88-2SIO board - data register
s2bCtl	equ	12h		;2nd port on 88-2SIO board - control register
s2bDat	equ	13h		;2nd port on 88-2SIO board - data register
s2Rdrf	equ	01h		;read data register full flag
s2Tdre	equ	02h		;transmit data register empty flag
s2Rst	equ	03h		;reset command
s28n1	equ	15h		;8N1 selection
s28n2	equ	11h		;8N2 selection

; 88-SIO Serial board Equates

sioCtl	equ	00h		;control port
sioDat	equ	01h		;data port
sioRdrf	equ	01h		;-RX Data register full
sioTdre	equ	80h		;-TX Data register empty

; 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 + 59 cycles for the rcvByte code. 

ONESEC	equ	19231		;rcvByte loop count for 1 second

;----------------------------------------------------------------------------
; s2Init - reset and initialize 88-2SIO ports for 8N1
;----------------------------------------------------------------------------
s2Init	mvi	a,s2Rst		;reset and init 2nd 88-2SIO port
	out	s2bCtl
	mvi	a,s28n1		;transfer port as 8N1
	out	s2bCtl

; if not running under CP/M, reset and init 1st 88-2SIO port

	lda	cpmFlag		;running under CP/M?
	ora	a
	rnz			;yes, 2SIO already initialized

	mvi	a,s2Rst		;reset and init 1st 88-2SIO port
	out	s2aCtl
	mvi	a,s28n2		;configure console for 8N2 in case Teletype
	out	s2aCtl
	ret	

;----------------------------------------------------------------------------
; conIn - input character from console
;    inputs:
;    outputs: z true if no character present
;	      z false if character returned in a
;    clobbers none
;----------------------------------------------------------------------------
conIn	in	s2aCtl		;see if a new character is present
	ani	s2Rdrf
	rz			;no character, return zero status

	in	s2aDat		;return character and non-zero status
	ret	

;----------------------------------------------------------------------------
; conOut - output character to console
;    inputs: b = character to send
;    clobbers a
;----------------------------------------------------------------------------
conOut	in	s2aCtl		;wait for OK to transmit
	ani	s2Tdre
	jz	conOut
	mov	a,b		;a=character to transmit
	out	s2aDat		;send it
	ret

;----------------------------------------------------------------------------
; Hardware specific I/O
; serIn - input character from port specified in a
;       0 = 88-2SIO port A
;       1 = 88-2SIO port B
;       2 = 88-SIO
;    inputs: a = port to read from
;    outputs: z true if no character present
;	      z false if character returned in a
;    clobbers none
; 88-2SIO Port A: 53 cycles including return when no character present
; 88-2SIO Port B: 43 cycles including return when no character present
; 88-SIO: 57 cycles including return when no character present
;-----------------------------------------------------------------------------
serIn	dcr	a		;(5)3-way test
	jz	s2InB		;(10)a was 1: 88-2SIO port B
	jp	sioIn		;(10)a was 2: 88-SIO
				;    a was 0: 88-2SIO port A
; Input from 88-2SIO port A

s2InA	in	s2aCtl		;(10+1)see if a new character is present
	ani	s2Rdrf		;(7)
	rz			;(10)no character, return zero status
	in	s2aDat		;else return byte and non-zero status
	ret	
	
; Input from 88-2SIO port B

s2InB	in	s2bCtl		;(10+1)see if a new character is present
	ani	s2Rdrf		;(7)
	rz			;(10)no character, return zero status
	in	s2bDat		;else return byte and non-zero status
	ret	

; Input from 88-SIO

sioIn	in	sioCtl		;(10+1)see if a new character is present
	cma			;(4)invert Rx Data Ready flag
	ani	sioRdrf		;(7)
	rz			;(10)no character, return zero status
	in	sioDat		;else return byte and non-zero status
	ret	

;-----------------------------------------------------------------------------
; Hardware specific I/O
; serOut - output character to port specified in a
;       0 = 88-2SIO port A
;       1 = 88-2SIO port B
;       2 = 88-SIO
;    inputs: a = port to transmit through
;	     b = character to send
;    clobbers a
;-----------------------------------------------------------------------------
serOut	dcr	a		;3-way test
	jz	s2OutB		;a was 1: 88-2SIO port B
	jp	sioOut		;a was 2: 88-SIO
				;a was 0: 88-2SIO port A

; Send character through 88-2SIO port A

S2OutA	in	s2aCtl		;wait for OK to transmit
	ani	s2Tdre
	jz	S2OutA

	mov	a,b		;a=character to transmit
	out	s2aDat		;send it
	ret

; Send character through 88-2SIO port B

s2OutB	in	s2bCtl		;wait for OK to transmit
	ani	s2Tdre
	jz	s2OutB

	mov	a,b		;a=character to transmit
	out	s2bDat		;send it
	ret

; Send character through 88-SIO

sioOut	in	sioCtl		;wait for OK to transmit
	ani	sioTdre
	jnz	sioOut		;active-low flag

	mov	a,b		;a=character to transmit
	out	sioDat		;send it
	ret


;****************************************************************************
;
; iCOM 3812/3712 disk I/O routines
;
;****************************************************************************

; FD3812 controller commands

cSTATUS	equ	00h		;read status
cREAD	equ	03h		;read sector
cWRITE	equ	05h		;write sector
cRDCRC	equ	07h		;validate sector CRC
cSEEK	equ	09h		;seek to track
cCLRERR	equ	0Bh		;clear error flags
cRESTOR	equ	0Dh		;seek to track 0
cSETTRK	equ	11h		;set track to seek
cDRVSEC	equ	21h		;set unit and sector
cWRTBF8	equ	30h		;write buffer for 3812
cWRTBF7	equ	31h		;write buffer for 3712
cRDBUF	equ	40h		;read buffer
cSHIFT	equ	41h		;shift read buffer
cRESET	equ	81h		;reset the controller
cLDCFG	equ	15h		;load configuration register

; Controller status bits

sBUSY	equ	01h		;1=busy
sCRCERR	equ	08h		;1=CRC error
sWRTPRT	equ	10h		;1=write protected
sNOTRDY	equ	20h		;1=drive not ready
sERROR	equ	29h		;any error

; Controller configure register bits

rDDEN	equ	10h		;double density

; Interface board registers

CMDOUT	equ	0C0h		;command output register
DATAIN	equ	0C0h		;data input from controller
DATAOUT	equ	0C1h		;data out to controller

;--------------------------------------------------------------
; dSelDrv - selects drive in drvNum and waits for it to
;    be ready.
; On Entry:
;    drvNum = drive to be selected
; On Exit:
;    drvSel = drive select bits in position
;    clobbers b,c,h,l
;--------------------------------------------------------------
dSelDrv	lda	drvNum		;a=desired drive
	rrc			;move drive to bits 6,7
	rrc
	sta	drvSel		;save drive select bits
	ori	1		;insert a valid sector number
	
	mvi	c,cDRVSEC	;c=set drive/sector command
	call	dCmdDat		;set data from a, command from c
	ani	sNOTRDY		;test ready status
	rz			;drive is ready, return

	lxi	h,mNotRdy	;display not ready message
	call	dispMsg

dWtRdy	call	chkQuit		;check for ctrl-c
	in	DATAIN		;check drive status
	ani	sNOTRDY		;drive ready yet?
	jnz	dWtRdy		;no, check again

	ret			;drive is ready, return

;--------------------------------------------------------------
; dGetDen - detect disk density. Seeks to track 3 in double
;    density, and if that fails, then in single density to
;    determine disk density. Tries tracks 2 and 1 as well
;    before giving up.
; On Entry:
;    Drive is selected
; On Exit:
;    dskDen = 0 if single density, rDDEN if double density
;    z true if success, else z false.
;    clobbers b,c,d
;--------------------------------------------------------------
dGetDen	xra	a		;init density to single
	sta	dskDen	
	mvi	d,3		;d=max track to try
	
dgLoop	lda	dskDen		;toggle density setting
	xri	rDDEN
	sta	dskDen
	jz	dgTest		;test single density, same track		

	dcr	d		;any more tracks to try?
	rm			;no, give up with z false

dgTest	mvi	c,cLDCFG	;set density
	call	dCmdDat

	mov	a,d		;set track for the seek
	inr	a		;d contains track-1
	mvi	c,cSETTRK
	call	dCmdDat	

	mvi	c,cSEEK		;issue seek, reads sector header
	call	dCmd		;  to verify track number
	rz			;no error, density found

	call	dClrErr
	call	dRestor		;restore to track 0
	jmp	dgLoop
	
;--------------------------------------------------------------
; dRead - Read sector on current track. Retries for a total
;    of RDTRIES attempts.
; On Entry:
;    Drive is selected, on proper track
;    hl->sector buffer
;    e = sector number to read
; On Exit:
;    Z flag set = good, Z flag clear = error
;    clobbers b,c,h,l
;--------------------------------------------------------------
dRead	lda	drvSel		;set drive and sector
	ora	e
	mvi	c,cDRVSEC
	call	dCmdDat

	mvi	b,RDTRIES-1	;B=try count (exits at -1)

drLoop	mvi	c,cREAD		;issue read command to controller
	call	dCmd
	ani	sERROR		;any errors?
	jz	drMove		;no, go move the bytes

	call	dClrErr		;clear controller errors
	dcr	b		;decrement retry count
	jp	drLoop		;try again

	ret			;exit with non-zero status (error)

; drMove - move bytes from fifo to (hl)

drMove	lda	ctl3712		;3712 controller?
	ora	a
	jnz	mv3712		;yes

; 3812 move loop
	
	mvi	b,0		;assume 256 bytes (DD)
	lda	trkDen		;check track density
	ora	a
	jnz	ini3812		;double density, go start the move
	
	mvi	b,SDSECL	;else move 128 bytes (SD)

ini3812	mvi	a,cRDBUF	;issue read buffer command to the controller
	out	CMDOUT

lp3812	in	DATAIN		;get next byte from fifo
	mov	m,a		;store in RAM destination
	inx	h		
	dcr	b		;decrement byte counter
	jnz	lp3812

	xra	a		;leave controller in examine status mode
	out	CMDOUT
	ret			;exit with zero status (success)
	
; 3712 move loop
	
mv3712	mvi	b,SDSECL	;move 128 bytes

lp3712	mvi	a,cRDBUF	;enable read buffer output
	out	CMDOUT

	in	DATAIN		;get next byte from fifo
	mov	m,a		;store in RAM destination
	inx	h		;increment buffer pointer
	
	mvi	a,cSHIFT	;shift FIFO to next byte
	out	CMDOUT
			
	dcr	b		;decrement byte counter
	jnz	lp3712

	xra	a		;leave controller in examine status mode
	out	CMDOUT
	ret			;exit with zero status (success)

;--------------------------------------------------------------
; dSeek - Seek to track trkNum and set density in controller 
;    based on the destination track.
; On Entry:
;    trkNum = desired track
; On Exit:
;    trkLen = length of destination track
;    trkDen = destination density, SD or DD (zero or not zero)
;    Zero true for success, non-zero for failure.
;    clobbers c,h,l
;--------------------------------------------------------------
dSeek	lda	trkNum		;a=desired track number
	ora	a		;track 0 requested?
	jnz	dsNot0		;no

	sta	trkDen		;set trkDen=0 (single density)
	lxi	h,SDTRKL	;set single density track length
	shld	trkLen
	jmp	dRestor		;use restore for track 0, not seek

dsNot0	mvi	c,cSETTRK	;set desired track number
	call	dCmdDat

	lxi	h,DDTRKL	;assume double density track length
	lda	dskDen		;a=0 if single, rDDEN for double
	ora	a		;single or double density?
	jnz	dSetDen		;double, go set density
	
	lxi	h,SDTRKL	;else, set single density length

dSetDen	shld	trkLen		;set proper track length
	sta	trkDen		;save density flag for new track
	
	lda	ctl3712		;3712 controller?
	ora	a
	jnz	ds3712		;yes, skip load config register
	
	lda	trkDen		;a=track density
	mvi	c,cLDCFG	;set config for single or double
	call	dCmdDat

ds3712	mvi	c,cSEEK		;seek to track
	call	dCmd
	ret			;return with seek status

;--------------------------------------------------------------
; dRestor - restore drive to track 0
; On Entry:
;    drive is selected
; On Exit:
;    drive on track zero, single density selected
;    clobbers c
;--------------------------------------------------------------
dRestor lda	ctl3712		;3712 controller?
	ora	a
	jnz	dr3712		;yes, skip load config register
	
	xra	a		;track zero is single density
	mvi	c,cLDCFG	;set controller to SD
	call	dCmdDat

dr3712	mvi	c,cRESTOR	;restore to track 0
	call	dCmd
	rz			;exit if no error

	lxi	h,mNoTrk0	;restore failed
	jmp	errMsg		;display and start over

;--------------------------------------------------------------
; dNxtSec - determine next sector to read and return pointer
;    to the sector within trkBuf in hl. Must start with sector
;    e=-1 and call 26 times to read all sectors on a track.
; On Entry:
;    e = sector (1-26)
; On Exit:
;    e = sector incremented with interleave of 2
;    hl->sector in trkBuf
; Clobbers b,c,h,l
;--------------------------------------------------------------
dNxtSec	inr	e		;increment sector by 2
	inr	e
	mov	a,e		;see if time to wrap
	cpi	NUMSEC+1
	jc	dSecPtr		;no wrap needed
	
	mvi	e,2		;start over with even sectors

; dSecPtr - return sector buffer pointer in hl based on
;   sector number in e.

dSecPtr	mov	b,e		;form bc=256*(sector-1)
	dcr	b
	mvi	c,0

	lda	trkDen		;single or double density track?
	ora	a		;test for zero (also clears carry)
	jnz	haveOff		;double density, already have bc

	mov	a,b		;compute bc=128*(sector-1)
	rar			;a=MSB of (sector-1)/128
	mov	b,a		;b=MSB of (sector-1)/128

	rar			;put lsbit into msbit	
	ani	80h		;keep only msbit
	mov	c,a		;bc=128*(sector-1)

haveOff	lhld	trkPtr		;hl = start of current track buffer
	dad	b		;add sector offset from bc
	ret			;return hl->sector buffer		

;--------------------------------------------------------------
; dCmdDat - stores A in DATAOUT before COMMAND from C
; On Entry:
;    a = data
;    c = command
;
; dCmd - send COMMAND from C to controller
;
; On Entry:
;    c = command
;
; On Exit:
;    a = status
;--------------------------------------------------------------
dCmdDat	out	DATAOUT		;data passed in A to controller

dCmd	mov	a,c
	out	CMDOUT		;command from C to controller
	xra	a		;3812 cannot be left with cDRVSEC in
	out	CMDOUT		;  CMDOUT and 3712 must clear bit 0

dWtCmd	in	DATAIN		;wait for execution to complete
	ani	sBUSY
	jnz	dWtCmd

	in	DATAIN		;return status in A
	ani	sERROR		;mask error bits
	ret

;--------------------------------------------------------------------------
; dClrErr - Issue the clear errors command to the controller
;--------------------------------------------------------------------------
dClrErr	mvi	a,cCLRERR	;clear controller errors
	out	CMDOUT		;give command to the controller
	xra	a		;leave with examine status in CMDOUT
	out	CMDOUT		;  (3712 requires)
	ret

;**************************************************************************
; 
;  Data area
;
;**************************************************************************
; disk variables

drvNum	db	MINDRV		;drive number to use
dskDen	db	0		;0=SD,1=DD
drvsel	ds	1		;drive select bits
trkNum	ds	1		;track number to read
trkDen	ds	1		;track density
trkPtr	ds	2		;pointer into trkBuf at track boundaries
trkLen	ds	2		;length of current track
bufEnd	ds	2		;end address + 1 of last track spot in RAM

; retry logic variables

rdRtry	ds	1		;disk read retry counter
rtStep	ds	1		;current retry step
trkErr	ds	1		;non zero if error occured on track

; xmodem variables

xfrPort	ds	1		;pseudo port for file transfer
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
ctl3712	ds	1		;non-zero for 3712, 0=3812
	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
