*****************************************************************************
*
*  PC2Flop - Write floppy with image received through a serial port
*
*	This program writes a Percom 5.25" SSSD floppy disk with a FLEX
*	SSSD disk image received through an ACIA serial port in any of
*	the SWTPC 6800 I/O ports. The XMODEM protocol is used for the
*	serial transfer.
*
*	The disk is written with no meta-data, just the 256 byte of data
*	per sector, ten sectors per track, 35 tracks. In the disk image,
*	sectors are in order with no interleaving. On the disk, however,
*	the sectors have an interleave of five as shown below.
*	
*	   Logical sectors:  0,1,2,3,4,5,6,7,8,9
*	   Physical sectors: 0,5,1,6,2,7,3,8,4,9
*
*	The SWTBUG (or MIKBUG) PROM and the Percom MiniDOS PROM must be 
*	present in computer.
*
*	If using the console port for the XMODEM transfer, error messages
*	and prompts once the XMODEM transfer is started on the terminal
*	emulator won't be seen. This can make it difficult to notice and
*	determine disk I/O problems.
*
*	Written by Mike Douglas
*
*	Rev	 Date	    Desc
*	1.0	03/08/20    Original
*	1.1	03/28/20    Fix pointer bug when XMODEM retries occur.
*
*****************************************************************************

* SWTBUG (MIKBUG) and MiniDOS equates

OUTEEE	equ	$E1D1		;SWTBUG display character
INEEE	equ	$E1AC		;SWTBUG input character
PDATA1	equ	$E07E		;SWTBUG display message
dispMsg	equ	PDATA1
STACK	equ	$A07F		;stack in SWTBUG RAM
CONTRL	equ	$E0E3		;re-enter SWTBUG

RDSEC	equ	$C00C		;read a sector
WTSEC	equ	$C00F		;write a sector
INITRK	equ	$C027		;init track registers

* Page 0 data maintained by MiniDOS

	org	0
drvNum	rmb	1		;drive number in MS 2 bits
trk	rmb	1		;desired track
sctr	rmb	1		;desired sector

bakLnk	rmb	2		;file backward link
fwdLnk	rmb	2		;file forward link
bytCnt	rmb	1		;sector byte count
addres	rmb	2		;data address vector
filTyp	rmb	1		;file type code	
ckSum	rmb	2		;sector checksum
postAm	rmb	2		;post amble

crnTrk	rmb	1		;current track
trkTbl	rmb	4		;track each drive is on
wrtAddr	rmb	2		;write buffer address
rdAddr	rmb	2		;read buffer address

* Disk information equates

NUMTRK	equ	35		;number of tracks
NUMSEC	equ	10		;number of sectors per track
SECLEN	equ	256		;sector length as transferred
MINDRV	equ	1		;first drive number
MAXDRV	equ	3		;max drive number 
TRKLEN	equ	NUMSEC*SECLEN	;length of track in bytes

* XMODEM equates

PKTLEN	equ	128		;128 byte xmodem packet length
SOH	equ	$01
EOT	equ	$04
ACK	equ	$06
NAK	equ	$15
EOF	equ	$1A		;ctrl-z character

* MP-S serial board equates

SIORDRF	equ	$01		;receive data register full
SIOTDRE	equ	$02		;transmit data register empty

* Misc equates

CR	equ	$0D		;ASCII for carriage return
LF	equ	$0A		;ASCII for line feed
DEL	equ	$7F		;ASCII DEL
BS	equ	$08		;ASCII backspace
CTRLC	equ	$03		;ASCII for control-c

	org	$0100		;load address
*-----------------------------------------------------------------------------
*   Initialize for transfer
*-----------------------------------------------------------------------------
init	bra	start
version	fcb	11		;current version number

start	lds	#STACK		;initialize stack pointer
	jsr	sizeRam		;determine amount of RAM available
	jsr	INITRK		;MiniDOS, initialize drives

* Display welcome message, then get the drive number

	ldx	#mWelcom	;display welcome message
	jsr	dispMsg

getDrv	ldx	#mDrive		;display drive number prompt
	jsr	dispMsg

	jsr	rcvCon		;get byte from the console
	oraa	#$20		;upper case letters to lower case
	cmpa	#'x'		;exit requested?
	bne	chkDrv		;no, check the drive number entered

	jmp	pgmExit		;else, exit as requested

chkDrv	suba	#MINDRV+'0'	;ASCII 1st drive goes to binary 0
	cmpa	#MAXDRV-MINDRV+1   ;validate <= max drive
	bcc	getDrv		;invalid drive number, prompt again
	
	adda	#MINDRV		;restore offset of 1st drive (if any) 
	rora			;put drive num into two MSbits
	rora
	rora
	anda	#$C0		;be sure drive bits are alone
	staa	drvNum		;save the drive number to use

* Get the serial port to use for the transfer

getPort	ldx	#mPort		;display port number prompt
	jsr	dispMsg

	jsr	rcvCon		;get byte from the console
	oraa	#$20		;upper case letters to lower case
	cmpa	#'x'		;exit requested?
	bne	chkPort		;no, check the port number entered
	jmp	pgmExit		;else, exit as requested

chkPort	suba	#'0'		;subtract ACII offset
	cmpa	#8		;must be 0-7
	bcc	getPort		;invalid port, re-enter

	asla			;convert to port address
	asla
	staa	portAdr+1	;LSB of port address
	ldab	#$80		;MSB of port address
	stab	portAdr

	ldx	portAdr		;initialize transfer port
	cmpa	#$04		;using console?
	beq	skipRst		;yes, skip port reset
	
	ldaa	#3		;reset serial port
	staa	0,x

skipRst	ldaa	#$15		;8N1
	staa	0,x

* Prompt the user to insert the disk to write, initialize, then start
*    sending NAK to start the XMODEM reception.
		
	ldaa	#1		;init xmodem block number to one
	staa	blkNum
	
	ldx	#trkBuf		;trkBuf is initial starting point for
	stx	rcvPtr		;   XMODEM receive

	clr	trkNum		;starting on track 0
	clr	eotFlag		;no EOT yet

	ldx	#0		;init header
	stx	bakLnk
	stx	fwdLnk

	clra
	staa	bytCnt		;256 byte sectors
	staa	filTyp		;file type

*  To allow booting by using the MiniDOS load command, the boot sector
*  must look like a valid MiniDOS file. The boot loader in sector
*  zero loads at $2400 and executes at $2400. These MiniDOS fields
*  are set here and will be in every sector written by FLEX, but only
*  matter when MiniDOS is used to boot (e.g. L 1000 FFFF followed by G).

	ldx	#$2400		;for boot loader
	stx	addres		;load address of $2400
	stx	postAm		;execution address of $2400
	
	ldx	#mStart		;display start file transfer prompt
	jsr	dispMsg

	ldaa	#ACK		;init exit status to good (ACK)
	staa	ackEot

	ldaa	#NAK		;send starting NAK after purging input data
	jsr	purgSio

*-----------------------------------------------------------------------------
*  mainLp - Receive as many packets as will fit into RAM and then write
*      tracks to disk. Repeat until entire disk is written. 
*-----------------------------------------------------------------------------
mainLp	bsr	bufPkts		;buffer as many packets as possible

	ldaa	rcvPtr+1	;compare rcvPtr-trkBuf
	suba	#trkBuf&$FF
	ldaa	rcvPtr
	sbca	#trkBuf/256
	bcs	allDone		;nothing received but an EOT

	ldx	#trkBuf		;writing will start from here
	stx	trkPtr

* trkLoop - write tracks until all disk tracks have been written or all
*    the tracks in the buffer have been written.
	
trkLoop	jsr	wrtTrk		;write a track 

	inc	trkNum		;increment track number
	ldaa	trkNum		;done all tracks on the disk?
	cmpa	#NUMTRK		;done all tracks on the disk?
	beq	allDone		;yes, we're all done

	ldaa	rcvPtr+1	;compare rcvPtr-start of next track
	suba	trkPtr+1
	tab			;save LSB in B for later

	ldaa	rcvPtr		;MSB
	sbca	trkPtr
	bcc	trkLoop		;trkPtr < rcvPtr, keep writing

* 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.

* At this point A:B = rcvPtr-trkPtr which is (left over byte count)-PKTLEN

	ldaa	eotFlag		;did EOT occur in last packet?
	bne	allDone		;yes, transfer is done

	ldx	#trkBuf		;X->start of track buffer
	stx	rcvPtr		;assume receive will here next time

	addb	#PKTLEN		;compute B=left over byte count
	beq	mvDone		;nothing to move, exit

moveLp	ldx	trkPtr		;X->leftover byte to move
	ldaa	0,x
	inx
	stx	trkPtr

	ldx	rcvPtr		;X->moving to start of trkBuf
	staa	0,x
	inx	
	stx	rcvPtr

	decb			;decrement bytes to move
	bne	moveLp

mvDone	ldaa	#ACK		;ACK the last packet
	jsr	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	ldaa	ackEot		;ACK or EOT is here
	jsr	sndByte

	ldx	#mDone		;assume no error
	cmpa	#ACK		;ACK means no error
	beq	noErr

	ldx	#mWrtErr	;else, write error message

noErr	jsr	dispMsg
	jmp	getDrv		;start over asking for a drive
	
*-----------------------------------------------------------------------------
* 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 point where to begin receiving new packets.
*-----------------------------------------------------------------------------
bufPkts	bsr	rcvPkt		;receive a packet
	cmpa	#EOT		;exit due to EOT?
	beq	bufDone		;yes, buffering is done

	ldaa	rcvPtr+1	;compute rcvPtr-bufEnd
	suba	bufEnd+1
	ldaa	rcvPtr
	sbca	bufEnd
	bcc	bufDone		;rcvPtr > bufEnd, no more room

	ldaa	#ACK		;ACK the packet
	jsr	sndByte

	jmp	bufPkts		;buffer more packets

* 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	ldaa	rcvPtr+1	;subtract PKTLEN from rcvPtr
	suba	#PKTLEN
	staa	rcvPtr+1

	ldaa	rcvPtr		;MSB
	sbca	#0
	staa	rcvPtr

	rts

*-----------------------------------------------------------------------------
* rcvPkt - receive an xmodem format 128 byte packet. rcvPtr points to
*     where the packet should be placed. rcvPtr is updated after each
*     packet is received.
*-----------------------------------------------------------------------------
nakPkt	ldx	pktStrt		;reset pointer to start of packet
	stx	rcvPtr
	
	ldaa	#NAK		;NAK the packet
	jsr	purgSio		;purge receive for 1s then xmit NAK

* Wait for SOH or EOT

rcvPkt	ldx	rcvPtr		;save the start of packet address
	stx	pktStrt

	ldaa	#3		;3 second timeout for SOH
	bsr	rcvByte		
	beq	nakPkt		;timeout

	cmpa	#SOH		;SOH received?
	beq	haveSoh

	cmpa	#EOT		;EOT to say we're done?
	bne	rcvPkt		;no, keep loooking

	staa	eotFlag		;EOT flag is true
	rts			;exit

* Have SOH. Now receive block number and not block number

haveSoh	bsr	rcvByt1		;get the block number (1s timeout)
	beq	nakPkt		;timeout, NAK and start over
	
	staa	rcvBlk		;save the received block number
	
	bsr	rcvByt1		;get not block number
	beq	nakPkt		;timeout, NAK and start over

	staa	rcvNBlk		;save not block number

* Receive the 128 byte data portion of the packet

	ldaa	#PKTLEN		;set transfer length
	staa	byteCnt
	clr	chkSum		;init checksum

pktLoop	bsr	rcvByt1		;get data byte
	beq	nakPkt		;timeout

	ldx	rcvPtr		;X->where to put next byte
	staa	0,x		;save the byte
	inx			;increment rcvPtr
	stx	rcvPtr

	adda	chkSum		;update checksum
	staa	chkSum
	
	dec	byteCnt		;decrement byte counter
	bne	pktLoop		;and repeat

* Verify the checksum

	bsr	rcvByt1		;get data byte
	beq	nakPkt		;timeout

	cmpa	chkSum		;checksum match
	bne	nakPkt		;no, packet error

* Checksum good, verify block numbers

	ldaa	rcvNBlk		;verify rcvBlk = not(rcvNBlk)
	coma
	cmpa	rcvBlk		;match?
	bne	nakPkt		;no, packet error

	cmpa	blkNum		;rcvBlk = expected bklNum?
	bne	nakPkt		;no, packet error
	
	inca			;increment expected block number
	staa	blkNum

	clra			;return zero status
	rts

*-----------------------------------------------------------------------------
* 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	psha			;save character to send

purge	bsr	rcvByt1		;wait for 1 second without activity
	bne	purge

	pula			;get back the character to send
				;fall through to sndByte

*-----------------------------------------------------------------------------
* sndByte - send the byte in A through the serial port. Clobbers B.
*-----------------------------------------------------------------------------
sndByte	stx	ioSaveX		;preserve X
	ldx	portAdr		;X->serial port
		
sbLoop	ldab	0,x		;wait for OK to transmit
	andb	#SIOTDRE
	beq	sbLoop	

	staa	1,x		;send the character
	ldx	ioSaveX		;restore X
	rts

*-----------------------------------------------------------------------------
* rcvByte - Receive a byte from the serial port with a timeout specified
*     in seconds in A. If a timeout occurs, zero is returned in A and the
*     zero flag is true. Otherwise, the character is returned in A and zero 
*     flag is false. Clobbers A, B, X
*-----------------------------------------------------------------------------
rcvByt1	ldaa	#1		;one second timeout
				;fall into rcvByte
rcvByte	ldx	#29412		;one second, 34 cycles per loop

rbLoop	stx	ioSaveX		;(6) preserve counter
	ldx	portAdr		;(5) X->serial port
	
	ldab	0,x		;(4) see if a new character is present
	andb	#SIORDRF	;(2)
	bne	rbRead		;(4) new character present

	ldx	ioSaveX		;(5) restore counter
	dex			;(4) decrement timeout
	bne	rbLoop		;(4)

	deca			;decrement seconds count
	bne	rcvByte

	rts			;timeout, exit with Z true
	
rbRead	ldaa	1,x		;A=byte received
	inx			;ensure Z is false
	rts

*-----------------------------------------------------------------------------
* wrtTrk - Write NUMSEC sectors to the current track from a 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. 
*-----------------------------------------------------------------------------
wrtTrk	clr	secNum		;start with physical sector 0

	ldaa	trkNum		;set track number in header
	staa	trk

wrtLoop	bsr	sec2Adr		;set buffer address based on sector
	jsr	WTSEC		;call write sector in MiniDOS PROM
	bcs	wtError		;branch if error occurred

	bsr	nextSec		;move to next sector
	bne	wrtLoop		;repeat for all 10 sectors
	
	ldaa	trkPtr+1	;increment tkrPtr by TRKLEN
	adda	#TRKLEN&$FF
	staa	trkPtr+1
	
	ldaa	trkPtr		;MSB
	adca	#TRKLEN/256
	staa	trkPtr

	rts	
	
* wtError - Write error

wtError	ldaa	#EOT
	staa	ackEot
	
	ldaa	#NUMTRK-1
	staa	trkNum
	
	rts

*-----------------------------------------------------------------------------
* nextSec - Update secNum to next sector to read using interleave of 2.
*   Exits with zero status when sector 0 is reached (full track done)
*-----------------------------------------------------------------------------
nextSec	ldaa	secNum		;move to next sector, interleave=2
	adda	#2
	staa	secNum

	cmpa	#NUMSEC		;past last sector?
	bcs	nsExit		;no, exit with non-zero status
	
	suba	#NUMSEC+1	;10->$FF, 11->0
	anda	#1		;10->1, 11->0
	staa	secNum

nsExit	rts

*-----------------------------------------------------------------------------
* sec2Adr - Set the physical sector number in the header and determine
*    the location to read the sector from the track buffer by converting
*    the physical sector number to a logical sector number.
*-----------------------------------------------------------------------------
sec2Adr	ldaa	secNum		;set physical sector in header
	staa	sctr

	ldx	#phy2Log	;X->physical sec to logical sec table
	staa	*+4		;modify the following indexed offset
	ldaa	0,x		;A=logical sector

	adda	trkPtr		;A=MSB of sector in RAM
	staa	wrtAddr
	ldaa	trkPtr+1	;set LSB of sector
	staa	wrtAddr+1

	rts	

*-----------------------------------------------------------------------------
* 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	jsr	INEEE		;use SWTBUG to get character from console
	cmpa	#CTRLC		;abort requested?
	beq	pgmExit		;yes, exit
	
	cmpa	#CR		;return pressed?
	beq	rcExit		;yes, exit
	
	tab			;save character in B

; Wait for CR, then return 1st character typed

rcWtCr	jsr	INEEE		;use SWTBUG to get character from console
	cmpa	#CTRLC		;abort requested?
	beq	pgmExit		;yes
	
	cmpa	#DEL		;delete?
	beq	rcExit		;yes, return DEL character
	
	cmpa	#BS		;backspace?
	beq	rcExit		;yes, return the backspace
	
	cmpa	#CR		;return pressed?
	bne	rcWtCr		;no, keep waiting
	
	tba			;return 1st character typed in A
rcExit	rts

*------------------------------------------------------------------
* 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	ldx	#trkBuf+$100	;look for end of RAM starting
	stx	saveX		;    at trkBuf+$100

ramLoop	ldaa	0,x		;A=current RAM content
	inc	0,x		;change value in RAM
	ldab	0,x		;B=new value from RAM
	staa	0,x		;restore original value
	
	cba			;did RAM change?
	beq	ramEnd		;no, so end of RAM found
	
	inc	saveX		;increment to next page
	ldx	saveX		;X->next page in RAM	
	bpl	ramLoop		;loop until $80xx reached

* 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	ldab	saveX		;B=1st page not RAM
	decb			;subtract 256 bytes 
	ldx	#trkBuf		;start saveX at trkBuf
	stx	saveX

bfEndLp	ldaa	saveX+1		;increment saveX by TRKLEN
	staa	bufEnd+1	;bufEnd has previous value of saveX
	adda	#TRKLEN&$FF
	staa	saveX+1

	ldaa	saveX		;MSB
	staa	bufEnd		;bufEnd has previous value of saveX
	adca	#TRKLEN/256
	staa	saveX

	cba			;saveX MSB < RAM end MSB?
	bcs	bfEndLp		;yes, still more room,

	rts			;exit with bufEnd properly set

*--------------------------------------------------------------
* pgmExit - Exit to the monitor
*--------------------------------------------------------------
pgmExit	ldx	$A048		;FLEX warm start address in $A048?
	cpx	#$AD03
	bne	notFlex		;no, not running under FLEX
	jmp	0,x		;else jump back into FLEX

notFlex	jmp	CONTRL		;exit to SWTBUG

*---------------------------------------------------------------------
* Message constants
*---------------------------------------------------------------------
mWelcom	fcb	CR,LF,LF
	fcc	'===== PC to Floppy Image Transfer, Version 1.1 ====='
	fcb	CR,LF,LF
	fcc	'Writes a Percom 5.25" disk with a FLEX SSSD image received'
	fcb	CR,LF
	fcc	'through an MP-S serial port using the XMODEM protocol.'
	fcb	EOT

mDrive	fcb	CR,LF,LF
	fcc	'Insert and specify destination drive (1-3) or X to exit: '
	fcb	EOT

mPort	fcb	CR,LF,LF
	fcc	'Specify the port to use for file transfer'
	fcb	CR,LF
	fcc	'  0) MP-S at $8000'
	fcb	CR,LF
	fcc	'  1) MP-S at $8004 (console port)'
	fcb	CR,LF
	fcc	'  2) MP-S at $8008'
	fcb	CR,LF
	fcc	'  3) MP-S at $800C'
	fcb	CR,LF
	fcc	'  4) MP-S at $8010'
	fcb	CR,LF
	fcc	'  5) MP-S at $8014'
	fcb	CR,LF
	fcc	'  6) MP-S at $8018'
	fcb	CR,LF
	fcc	'  7) MP-S at $801C'
	fcb	CR,LF
	fcc	'Enter port or X to exit: '
	fcb	EOT

mStart	fcb	CR,LF,LF
	fcc	'Start XMODEM send operation from the PC now...'
	fcb	EOT

mDone	fcb	CR,LF,LF
	fcc	'Transfer complete'
	fcb	CR,LF,EOT

mWrtErr	fcb	CR,LF,LF
	fcc	'Drive not ready or disk write protected'
	fcb	EOT

**************************************************************************
* 
*  Data area
*
**************************************************************************

* Sector skew table for skew of 5

*log2Phy fcb	0,5,1,6,2,7,3,8,4,9
phy2Log	fcb	0,2,4,6,8,1,3,5,7,9

* disk variables

trkNum	rmb	1		;track number to read
secNum	rmb	1		;current physical sector number
trkPtr	rmb	2		;pointer into trkBuf at track boundaries
bufEnd	rmb	2		;end address + 1 of last track spot in RAM

* xmodem variables

portAdr	rmb	2		;serial port address
blkNum	rmb	1		;current xmodem block number
rcvBlk	rmb	1		;block number received
rcvNBlk	rmb	1		;NOT block number received
rcvPtr	rmb	2		;pointer for start of XMODEM send
pktStrt	rmb	2		;address of start of packet
chkSum	rmb	1		;packet checksum
byteCnt	rmb	1		;count of bytes to send
ackEot	rmb	1
eotFlag	rmb	1		;non-zero if EOT received

* misc variables

saveX	rmb	2		;generic save location for X
ioSaveX	rmb	2		;save X in I/O subroutines

* track buffer runs from here to the end of memory

	rmb	PKTLEN		;space for pre-pended unsent data
trkBuf	equ	*

	org	$A048		;start address for "G" command
	fdb	init

	end



