;******************************************************************************
;
;  PC2Flop - Transfer disk image from PC to floppy over serial port.
;	    (for Polymorphic hard sector, single density controller)
;
;	This program writes a Polymorphic single density disk from a disk
;	image transmitted from a PC. The image is transmitted through the
;	Poly serial port (printer port) using the XMODEM checksum protocol.
;	The image is written directly to the floppy in raw format (256
;	bytes per sector, 10 sectors per track, 35 tracks). The disk image
;	is the linear sequence of 256 byte sectors from track 0, sector 0
;	through track 34, sector 9. This is the same format used by the
;	Polymorphic 8813 emulator.
;
;	This program runs standalone at 3200h or under Exec. The standard
;	Polymorphic disk PROM set is expected on the CPU board.
;
;	Rev	 Date	    Desc
;	1.0	12/17/17    M. Douglas, Original
;
;	1.1	01/24/18    M. Douglas, If a partial disk image is sent,
;			    write all remaining sectors and tracks so the
;			    entire disk is formatted and can be copied.
;			    Most disk images I've found are truncated at
;			    the end of data.		
;
;*****************************************************************************

; Disk information equates

NUMTRK	equ	35		;number of tracks
NUMSEC	equ	10		;number of sectors per track
SECLEN	equ	256		;sector length (as transmitted)
MINDRV	equ	1		;first drive number
MAXDRV	equ	3		;max drive number
TRKLEN	equ	(NUMSEC*SECLEN)	;length of track in bytes

; Poly PROM equates

USER	equ	3200h		;start of user programs
CONIN	equ	0C20h		;console input wormhole 0
CONOUT	equ	0C24h		;console output wormhole 1
SRA4	equ	0C16h		;serial port interrupt jump vector
SETUP	equ	02ADh		;setup 8251 from table	
Dio	equ	0406h		;disk I/O entry vector
R1we	equ	0427h		;prompt then read line from console
Msg	equ	040Ch		;display message
PVEC	equ	2D93h		;contains ctrl-y processing 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

; Poly serial port equates

SIODR	equ	00h		;8251 data register
SIOCR	equ	01h		;8251 control register

SIORDA	equ	02h		;8251 receive data available
SIOXCMP	equ	04h		;8251 transmit complete
SIORENA	equ	26h		;8251 receive enabled
SIOXENA	equ	27h		;8251 receive and transmit enabled

ONESEC	equ	23332		;rcvByte loop count for 1 second

; Misc equates

MAXRAM	equ	0F800h		;max RAM address to check (e.g., video) 
IBLEN	equ	20		;console input buffer length
CR	equ	13		;ascii for carriage return

;-----------------------------------------------------------------------------
;  Start of code
;-----------------------------------------------------------------------------
	org	USER		;standard Exec program load address

cold	jmp	init		;first entry jump vector
warm	jmp	getDrv		;re-entry jump vector

init	lxi	h,0		;save stack pointer in savStk
	dad	sp
	shld	savStk	

	lhld	PVEC		;save current ctrl-y processor
	shld	savPvec

	lxi	h,pgmExit	;set ctrl-y processor to pgmExit
	shld	PVEC	

	call	serInit		;init serial I/O
	call	sizeRam		;determine amount of RAM available

	lxi	h,mWelcom	;display welcome message
	call	Msg

; getDrv - get drive number or letter from the user.

getDrv	lhld	savStk		;restore stack pointer
	sphl
	
	lxi	h,inBuf		;HL->user input buffer
	lxi	d,mDrive	;DE->prompt for drive number
	lxi	b,IBLEN		;B=0=echo terminator, C=inBuf length
	call	R1we		;get input from user

	mov	a,b		;A=read length	
	ora	a		;anything typed?
	jz	getDrv		;no, ask again

	lda	inBuf		;A=first character typed
	ori	20h		;upper case letters to lower case
	cpi	'x'		;exit requested?
	jz	pgmExit		;yes

	sui	'0'+MINDRV	;1st drive is now zero
	cpi	MAXDRV-MINDRV+1	;validate <= max drive
	jnc	getDrv		;invalid, prompt again

	adi	MINDRV		;restore offset of 1st drive (if any)
	sta	drvNum		;save the drive number to use

; 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	Msg

	xra	a
	sta	trkNum		;start at track zero

	inr	a		;init xmodem block number to one
	sta	blkNum

	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	trkNum		;increment track number (preserve HL)
	inr	a
	sta	trkNum	
	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
	lda	eotFlag		;see if EOT terminated the reception
	cpi	EOT	
	jz	allDone		;yes, image was smaller than disk

	mvi	a,ACK		;ACK the last packet or EOT
	call	serOut		;send one byte
	jmp	mainLp		;go receive more packets

; allDone - The disk image has been received. ACK the final packet. If the
;     image did not fill disk, zero fill and write all remaining tracks.
;     Display the "transfer complete" message and start over.

allDone	mvi	a,ACK		;ACK the final packet or EOT
	call	serOut		;send one byte
	
	lda	trkNum		;all tracks written?
	cpi	NUMTRK
	jz	dskDone		;yes, the disk is done

	lxi	h,mFill		;display "filling disk message"
	call	Msg

	lxi	h,trkBuf	;zero fill one track in trkBuf
	lxi	b,TRKLEN	;bc=bytes in a track
	
zeroFil	mvi	m,0		;zero fill a byte
	inx	h
	dcx	b		;decrement bc and test for zero
	mov	a,b
	ora	c
	jnz	zeroFil
	
wrtLast	lxi	h,trkBuf	;hl->zero filled track we just made
	shld	trkPtr
	call	wrtTrk		;write the track
		
	lxi	h,trkNum	;increment track number
	inr	m
	mov	a,m		;A=next track number
	cpi	NUMTRK		;done all tracks?
	jnz	wrtLast		;loop until all tracks written
	
dskDone	lxi	h,mDone		;hl->done message
	call	Msg
	jmp	warm		;start over at warm entry point

;-----------------------------------------------------------------------------
; 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
	sta	eotFlag		;save status returned (may be EOT)
	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	serOut		;send one byte
	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	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	lxi	h,drvNum	;HL->drvNum
	mov	c,m		;C=drive to use
	
	lhld	trkPtr		;HL->next spot in track buffer
	xchg			;DE->next spot in track buffer
	
	mvi	b,0		;B=write command
	mov	h,b		;init H to zero

	lda	trkNum		;convert track number into disk address
	mov	l,a		;L=track number
	add	a		;A=track*2
	add	a		;A=track*4
	add	l		;A=track*5
	add	a		;A=track*10 (could have carry out)
	mov	l,a		;L=lsb of disk address
	jnc	haveSec		;disk address < 256
	
	inr	h		;disk address >= 256
	
haveSec	mvi	a,NUMSEC	;A=number of sectors to write (10)
	call	DIO		;issue write command (in PROM)
	jc	dskErr		;write failure
	
	lhld	trkPtr		;point trkPtr to next track
	lxi	d,TRKLEN	;DE=bytes in a track
	dad	d		;HL=start of next track in trkBuf
	shld	trkPtr
	
	ret

;-----------------------------------------------------------------------------
; dskErr - Disk write failure. Display the disk write error code (in E)
;    and the track number with the error. Restart the program.
;-----------------------------------------------------------------------------
dskErr	lxi	h,errCode	;HL->where to put ascii error code
	mov	a,e		;A=error code
	call	bin2dec

	lxi	h,errTrk	;HL->where to put ascii track number
	lda	trkNum		;A=track with error on it
	call	bin2dec		;track to ascii

	lxi	h,mDskErr	;display the error message
	call	Msg
	
	jmp	getDrv		;start over

;-----------------------------------------------------------------------------
; 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		;preserve character to send

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
	jmp	serOut		;send one character

;--------------------------------------------------------------
; 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

;------------------------------------------------------------------
; 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
;------------------------------------------------------------------
sizeRam	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
	mov	a,h		;reached max RAM address?
	cpi	MAXRAM/256
	jnz	ramLoop		;loop if not

; 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
	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

;--------------------------------------------------------------
; pgmExit - Exit program. Disable serial interrupts, restore
;    stack, restore ctrl-y processor
;--------------------------------------------------------------
pgmExit	xra	a		;disable serial port 
	out	SIOCR		;(prevents further interrupts)

	lhld	savPvec		;restore ctrl-y processor
	shld	PVEC

	lhld	savStk		;restore stack pointer
	sphl
	
	ei			;needed if entered via ctrl-y

	mvi	a,CR		;CR to console
	jmp	CONOUT		;display CR and exit

;--------------------------------------------------------------------
; 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

;----------------------------------------------------------------------------
; serInit - initialize serial port and serial interrupt logic
;-----------------------------------------------------------------------------
serInit	call	SETUP		;initialize 8251 from following table 
	db	01Fh		;device 1 (printer port), 9600 baud
	db	0AAh		;fake sync character
	db	040h		;reset gets us to mode level
	db	04Eh		;mode async, 8N1, 16x
	db	000h		;command: idle, also end of table

	xra	a		;A=0		
	sta	rcvFlag		;no receive byte present

	lxi	h,serIsr	;HL->local serial I/O ISR
	shld	SRA4		;store local ISR

	mvi	a,SIORENA	;A=enable receive (not xmit)
	out	SIOCR

	ret

;-----------------------------------------------------------------------------
; rcvByte - Receive a byte from the serial 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 B and C.
;-----------------------------------------------------------------------------
rcvByte	push	d		;save DE
	mov	d,a		;save timeout in D

initSec	lxi	b,ONESEC	;BC=one second timeout

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


;----------------------------------------------------------------------------
; serIn - input character from serial port and return in A
;    inputs:
;    outputs: Z true if no character present
;	      Z false if character returned in A
;    clobbers none
;    28 cycles with no character 
;-----------------------------------------------------------------------------
serIn	lda	rcvFlag		;(13) serial data available ?
	ora	a		;(4)
	rz			;(11) no

	mvi	a,0		;A=0, preserve non-zero status
	sta	rcvFlag		;clear data present flag

	lda	rcvData		;A=byte received
	ret			;return with Z false

;-----------------------------------------------------------------------------
; serOut - Transmit byte through serial port. This code assumes only single
;    byte transmissions in response to received messages
;    inputs: A = character to send
;    clobbers A
;-----------------------------------------------------------------------------
serOut	sta	xmtData		;store the byte for the ISR to pick up
	mvi	a,SIOXENA	;enable transmitter
	out	SIOCR		;ISR will immediately run
	ret

;--------------------------------------------------------------------------
; serIsr - Serial port interrupt service routine
;--------------------------------------------------------------------------
serIsr	in	SIOCR		;A=serial status register
	ani	SIORDA		;data received?
	jz	isrXmt		;no, must be a transmit interrupt

	sta	rcvFlag		;set received data flag true (non-zero)
	in	SIODR		;A=byte received
	sta	rcvData		;rcvByte = byte received

	jmp	isrExit		;restore registers and exit

; isrXmt - process a transmit interrupt

isrXmt	lda	xmtData		;get byte to transmit
	out	SIODR		;send it

isrCmp	in	SIOCR		;A=serial status register
	ani	SIOXCMP		;test for empty xmit shift register
	jz	isrCmp		;wait until character completely sent

	mvi	a,SIORENA	;disable xmit (rcv still enabled)
	out	SIOCR		;xmit off disables xmit interrupt

isrExit	pop	h		;restore registers
	pop	d
	pop	b
	pop	psw
	ei			;enable interrupts
	ret

;-----------------------------------------------------------------------------
;   Data Area
;-----------------------------------------------------------------------------
mWelcom	db	CR
	db	'===== PC to Floppy Image Transfer, Version 1.1 ====='
	db	CR
	db	'    (Poly-88xx with Poly Hard Sector Controller)'
	db	CR,CR
	db	'Writes a single density Poly disk image received via'
	db	CR
	db	'the serial port using the XMODEM protocol.',CR,0

mDrive	db	CR,'Insert and specify destination disk (1-3)',CR
	db	'  or X to exit: ',0

mStart	db	CR,'Start XMODEM send from the PC now...',0

mFill	db	CR,'Filling to end of disk...',0
mDone	db	CR,CR,'Creation of new disk successful!',CR,0

mDskErr	db	CR,CR,'Write failure '
errCode	db	'xx on track '
errTrk	db	'xx. Disk creation aborted.',CR,0

;**************************************************************************
; 
;  Data area
;
;**************************************************************************
; disk data

drvNum	ds	1		;drive number to use
trkNum	ds	1		;track number to read
trkPtr	ds	2		;pointer into trkBuf (start of next track)
bufEnd	ds	2		;end address + 1 of last track spot in RAM

; xmodem data

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

; serial I/O data

rcvFlag	ds	1		;non-zero = received byte available
rcvData	ds	1		;byte received from serial port
xmtData	ds	1		;byte to transmit

; misc variables

savStk	ds	2		;saved entry stack pointer
savPvec	ds	2		;saved entry PVEC pointer
eotFlag	ds	1		;set to EOT if EOT terminated reception

; track buffer runs from here to the end of memory

trkBuf	equ	$
inBuf	equ	$		;use for console input as well

	end




