;==============================================================
;       CP/M-Memon/80 XMODEM Receiver by Martin Eberhard 
;==============================================================
;A simple xmodem receiver for loading files from MEMON/80
;onto a (possibly virgin) CP/M disk in drive A
;
;Requires Memon/80 Version 2.1 or later
;
; 1. Load CP/M from disk in drive A
; 2. Reset computer to MEMON/80 (front panel or jump-start)
; 3. Use HL command to load this RXMODEM into RAM
; 4. Initiate XMODEM file-send on PC (with e.g. Hyperterm)
; 5. use CE <filename> to execute this program
;
; Progress markers (+) will be printed for each successful
; block received. "-" will be printed for each block retry.
;
; The file will be named <filename> on the CP/M disk. Minimal
; error checking is done on the file name, so don't use a
; stupid name. 
;
; Assemble with Digital Research's ASM Assembler
;==============================================================
;Revision History:
;
; 1.0 30 May 2018  M. Eberhard
;   Created from XMODEM.com
;
;==============================================================
;---------------------------
;This code's revision number
;---------------------------
VERSION	equ	0100h	;high byte = major revision number
			;low byte = minor version

CPUMHZ	equ	2	;CPU speed in MHz (for timeout loop)

;-----------------------------
;Serial Port Definitions
;(These are calls to Memon/80)
;-----------------------------
MBASE	equ	0F800h		;Memon/80 base address
METPSI	equ	MBASE+0Fh	;Console Status
METPDO	equ	MBASE+12h	;Console output
METPDI	equ	MBASE+15h	;Console Input

FALSE	equ	0
TRUE	equ	not FALSE

ERRLIM	equ	10	;Max error-retries. 10 is standard.

;Timeout values in seconds. Values in parenthesis are
;XMODEM standard values.

SOHTO	equ	10	;(10)sender to send SOH 

BLKSIZ	equ	128	;bytes per XMODEM block
			;DO NOT CHANGE. BLKSIZ must be 128

;Progress pacifiers printed on the console

PACACK	equ	'+'	;Sent/Received a good block
PACNAK	equ	'-'	;Sent/Received a NAK

;**************
; CP/M Equates
;**************
;------------------------------------------
;BDOS Entry Points and low-memory locations
;------------------------------------------
WBOOT	equ	0000H		;Jump to BIOS warm boot
WBOOTA	equ	WBOOT+1		;Address of Warm Boot
BDOS	equ	WBOOT+5		;BDOS Entry Point

COMBUF	equ	WBOOT+80H	;disk & cmd line buffer
USAREA	equ	WBOOT+100H	;User program area

;------------------------------------------
;BDOS Function Codes, passed in register C
;Note: CON, RDR, and PUN I/O is done via
;direct BIOS calls, not BDOS calls.
;------------------------------------------
BCONOT	equ	2	;Type Chr on Console
BPRINT	equ	9	;Print $-terminated String
BCLOSE	equ	16	;Close disk file, FCB at de
BSERCH	equ	17	;Search dir for file, FCB at de
BWRITE	equ	21	;Write next record, 0=OK, <>0=ERR
BMAKE	equ	22	;Make new file, 0FFH=BAD
BSTDMA	equ	26	;Set disk buffer to (de)

;----------------
;ASCII Characters
;----------------
SOH	equ	1		;Start of 128-byte block
CTRLC	equ	3		;Control-C for user-abort
EOT	equ	4		;End XMODEM session
ACK	equ	6		;XMODEM block acknowledge
TAB	equ	9		;horizontal tab
LF	equ	0AH		;Linefeed
CR	equ	0DH		;Carriage return
NAK	equ	15H		;XMODEM block negative ACK
EOF	equ	1Ah		;^Z end of XMODEM.CFG file

;*********************
;* Beginning of Code *
;*********************
	org	USAREA		;normal place for CP/M programs

;*******************************
;Run-time stack is in the COMBUF
;(which is just below USAREA)
;*******************************
RSTACK:	lxi	SP,RSTACK	;initialization stack

;-------------------------------------------------------------
;Clear the File Control Block (FCB)
;The first byte is the drive, and is set to 0 by the assembler
;The next 11 are the name and file type, here set to spaces
;The rest are set to 0 here.
;-------------------------------------------------------------
	lxi	d,11*256+24	;load 2 counters
	lxi	h,FCBFN+36	;load backwards

FCBC1:	dcx	h
	mvi	m,0
	dcr	e
	jnz	FCBC1

FCBC2:	dcx	h
	mvi	m,' '
	dcr	d
	jnz	FCBC2

;--------------------------
;Print brief signon message
;--------------------------
	lxi	d,SIGNON
	call	PRINTF

;---------------------------------------
;Get the file name from the command line
;and put it in the FCB
; hl=FCBFN
; de=0
;---------------------------------------
	xchg			;de=FCBFN

	mvi	l,COMBUF	;h=0 already
	mov	b,m		;Number of bytes

SSPACE:	inx	h		;Skip leading spaces
	mov	a,m
	dcr	b
	jm	CMDERR

	cpi	' '
	jz	SSPACE

	mvi	c,8		;max name chrs
	call	GETNAM

	mvi	c,3		;max file type chrs
	inx	h		;point past '.'
	lxi	d,FCBFN
	dcr	b

	cp	GETNAM		;get file type if given

;-------------------------------------------
;See if file already exists, and abort if so
;-------------------------------------------
	lxi	d,FCB

	mvi	c,BSERCH	;Search directory for file
	call	GOBDOS

	lxi	d,FEMSG
	inr	a		;-1 means not there
	jnz	PEXIT

;---------------------------------------------
;Create the file, using the FCB we just built.
;Pre-existing file will cause an error.
;---------------------------------------------
	lxi	d,FCB
	mvi	c,BMAKE		;CP/M CREATE FILE func
	call	GOBDOS

	inr	a		;-1 means create error
	lxi	d,CFEMSG
	jz	PEXIT

;-------------------------------------
;Send initial NAK to initiate transfer
;-------------------------------------
	mvi	a,NAK		;send the initial ACK
	call	TXBYTE

	mvi	d,0		;init previous block #

;-------------------------------------------------------
;Receive & validate a block, and see if we got an EOT
;  XBLOCK=16-bit block number of the last block received
;-------------------------------------------------------
RXLOOP:	xra	a
	sta	ERRCNT		;Clear error count

	mov	e,d		;e=previous block number

;---------------------------------------
;Wait for SOH from sender to start
;reception, go investigate anything else
;---------------------------------------

;Bad block retry re-entry point

RXRTRY:	mvi	a,SOHTO*2	;Timeout for SOH
	call	RXBYTE

	cpi	SOH		;Did we get an SOH?
	jnz	NOTSOH		;If not, see what we got

;------------------------------------------------
;Got an SOH at beginning of the block. Now get
;the rest of the block header: 8-bit Block number
;followed by the complemented 8-bit block number
;------------------------------------------------
	call	RXBYT1		;Get block number
	mov	d,a		;Save block number

	call	RXBYT1		;complimented block number
	cma			;(4)compliment to compare

	cmp	d		;(4)
	jnz	PURGE		;(10)No match: error

;-------------------------------------------------
;Loop to receive BLKSIZ bytes and store them in
;the buffer, computing the checksum along the way. 
;Throughout the RXCHR loop:
;  b is the byte counter
;  c accumulates the checksum
;  d = this block's number
;  e = the previous block's number
;  hl is the buffer memory pointer
;------------------------------------------------
	lxi	h,BUFFER	;block buffer
	lxi	b,BLKSIZ*256+0	;(10)b=bytes, c=0 checksum

RXCHR:	call	RXBYT1		;(17+118)Get one byte of data

	mov	m,a		;(7)Store byte in buffer
	add	c		;(4)update checksum too
	mov	c,a		;(5)

;--------------------------------------------------------------
;Next byte, unless we have received all the data for this block
;--------------------------------------------------------------
	inx	h		;(5)next byte
	dcr	b		;(5)
	jnz	RXCHR		;(10)

;------------------------------------------
;We've received all the block's data bytes.
;Now verify the checksum in c.
;  hl=next buffer address
;------------------------------------------
	call	RXBYT1		;Get cksum

	cmp	c		;Does it match?
	jnz	RXSERR		;No: error

;---------------------------------------------------
;Got an error-free block. See if is the block number
;we expected, based on the prior block's number.
;  hl=next buffer address
;   d=this block's number
;   e = previous block's number
;---------------------------------------------------
	mov	a,d
	sub	e		;calculate difference
	dcr	a
	jnz	BLKORD		;Not sequential?

;----------------------------
;Write 128-byte block to disk
;----------------------------
	lxi	d,BUFFER	;reset BLKPTR
	mvi	c,BSTDMA	;CP/M SET DMA function
	call	GOBDOS		;de = DMA address

	lxi	d,FCB
	mvi	c,BWRITE	;Write from buf to disk
	call	GOBDOS

	lxi	d,EWFMSG
	ora	a		;BDOS returns 0 if okay
	jnz	PEXIT		;Oops, write error

;Print good-block pacifier on the console, if enabled

	call	PACOK

;-------------------------------------------
;Send ACK and loop back to get another block
;-------------------------------------------
ACKBLK:	call	TXACK		;Send XMODEM ACK
	jmp	RXLOOP		;LOOP until EOF

;========================
;Receive Error Processing
;========================
;-------------------------------------------------------
;Non-sequential block received without a checksum
;error. a=FFh if this block has the same block number as
;the previous block (and should be ignored). Otherwise
;abort because a block has been irretrievably lost.
;-------------------------------------------------------
BLKORD:	inr	a		;was it FFh?
	jz	ACKBLK		;y: ignore repeated block

	lxi	d,LBMSG		;Lost block
	jmp	PEXIT

;---------------------------------------
;Got something besides an SOH. If it
;was an EOT then quit directly to RXEOT.
;---------------------------------------
NOTSOH:	cpi	EOT
	jz	RXEOT		;done with reception
				;carry is clear

;-------------------------------------------------------
;No SOH or EOT - this was an invalid header. Eat the
;rest of this block (up to 256 received bytes) until
;the line is quiet for 1 second. (RXBYT1 timeout will
;jump to RXSERR.)
;-------------------------------------------------------
PURGE:	mvi	b,0		;allow 256 babbling chrs

PRGLUP:	call	RXBYT1		;Receive w/ 1-sec timeout
	dcr	b
	jnz	PRGLUP		;RXBYT1 times out to RXSERR

;The transmitter is babbling, unable to synchronize
;with the incoming data stream. Abort with message.

	lxi	d,SYNMSG	;Sync error message
	jmp	PEXIT		;too many bogus bytes

;------------------------------------
;Send a NAK to indicate receive error
;------------------------------------
RXSERR:	call	PACERR		;Print pacifier
				;trashes hl
	mvi	a,NAK
	call	TXBYTE

;Bump error count, and abort if too many errors.
;otherwise, retry the block.

	lxi	h,ERRCNT	;Clear error count
	inr	m		;bump error count

	mov	a,m		;Too many errors?
	cpi	ERRLIM
	jc	RXRTRY		;No: try again

	lxi	d,ELEMSG	;error limit exceeded
	jmp	PEXIT

;***Subroutine************************
;Receive a byte, with 1-second timeout
;On Exit:
;  exit to RXSERR if timeout
;  a = received byte if no timeout
;*************************************
RXBYT1:	mvi	a,2		;1-second timeout

;Fall into RXBYTE

;***Subroutine***************************************
;Receive a byte from the Transfer Port
;On Entry:
;  a = timeout value in half-seconds
;On Exit:
;  exit to RXSERR if timeout
;  a = received byte if no timeout
;(We assume the call to METPDI takes 59 cycles)
;****************************************************
RXBYTE:	sta	TIMRH
	push	h
	lxi	h,4348*CPUMHZ	;500,000/115

WATCRX:	dcx	h		;(5)
	mov	a,l		;(5)
	ora	h		;(4)
	cz	RXTIMR		;(11) Timeout?

	call	METPSI		;(17+59)default causes error
	ora	a		;(4)
	jz	WATCRX		;(10)

	pop	h
	jmp	METPDI		;Get the received data byte

;---Local Subroutine-------------------------
;Bump timer
;On Entry:
;  hl = 0
;  TIMRH = remaining timeout in 0.5 sec units
;  top-of-stack = our return address
;  next-on-stack = hl save value
;  next-on-stack = RXBYTE return address
;On Exit:
;  hl reloaded
;On Timeout:
;   repair stack for call to RXBYTE
;   jump to address in RXSERR
;Trashes psw
;--------------------------------------------
RXTIMR:	lxi	h,TIMRH
	dcr	m		;bump timer high byte
	lhld	TIMRLD		;reload timer
	rnz			;return unless timeout

;Timeout: fix stack, bail out to RXSERR

	pop	h		;chuck RXTIMR return address
	pop	h		;original hl
	jmp	RXSERR

***Subroutine********************************
;Send ACK
;On Exit:
;  a trashed
;  All flags and all other registers preserved
;*********************************************
TXACK:	mvi	a,ACK

;Fall into TXBYTE

;***Subroutine*******************************
;Send a to the transfer port
;On Entry:
;  a = byte to send
;On Exit:
;  All registers preserved
;********************************************
TXBYTE:	push	b
	mov	c,a		;chr to c
	call	METPDO
	pop	b
	ret

;***Subroutine********************************
;Print error pacifier on the console
;Trashes psw,c,hl
;*********************************************
PACERR:	mvi	a,PACNAK
	db	21h		;'LXI H' opcode skips 2 bytes

;Hop into PRINTA

;***Subroutine*************************************
;Print good pacifier on the console unless disabled
;Trashes psw,c
;**************************************************
PACOK:	mvi	a,PACACK

;Fall into PRINTA

;***Subroutine************************************
;Print a on the console
;On Entry:
;  a=character to print
;Trashes psw,c
;*************************************************
PRINTA:	push	d
	mov	e,a		;where BDOS wants chr
	mvi	c,BCONOT	;print single chr
	call	GOBDOS
	pop	d
	ret

;***Exit***************************************
;Received EOT during file reception. Send
;termination message, close the file,and return
;to CP/M.
;On Entry:
;  Carry set if NOT ok
;  EOF received, but not yet ACKed
;**********************************************
RXEOT:	call	TXACK		;ACK EOF

	lxi	d,FCB		;FCB describes the file
	mvi	c,BCLOSE	;CP/M CLOSE FILE function
	call	GOBDOS

	lxi	d,OKMSG
	inr	a		;-1 meant close error

	jnz	PEXIT
	lxi	d,FCEMSG	;file close error

;Fall into PEXIT

;***Exit******************************************
;Print $-terminated string at de, return to CP/M
;*************************************************
PEXIT:	call	PRINTF

;Fall into EXIT

;***Exit***********************************
;Return to CP/M. All exits go through here.
;******************************************
EXIT:	jmp	WBOOT		;go to CP/M

;***Subroutine*******************
;Print string at de on a new line
;********************************
	push	d		;CR,LF to be pretty
	lxi	d,CRLF
	mvi	c,BPRINT
	call	GOBDOS

	pop	d		;recover string

;Fall into GOBDOS

;***Subroutine*********************************
;Call BDOS while preserving all regs except psw
;**********************************************
GOBDOS:	push	h
	push	d
	push	b
	call	BDOS
	pop	b
	pop	d
	pop	h
	ret

;***Subroutine******************************
;Get a file name element and put in the FCB
;On Entry:
;  hl points to name element
;  de points to correct place in FCB
;   b = remaining character count - 1
;   c = max allowed chrs
;On Exit, terminated with '.':
;  Z set
;  hl points to '.'
;On Exit, end of string
;  Z clear
;*******************************************
GETNAM:	mov	a,b
	dcr	a		;any chrs?
	rm

NAMLUP:	mov	a,m
	cpi	'.'		;file type?
	rz

	dcr	c
	jm	TRUNC		;Too long? truncate

	ani	'a'-'A'		;uppercase
	stax	d		;save a name chr
	inx	d

TRUNC:	inx	h
	dcr	b
	jp	NAMLUP

	ret

;***Exit***********
;Bogus command line
;******************
CMDERR:	lxi	d,FNEMSG
	jmp	PEXIT

;***************************
;$-terminated abort messages
;***************************

SIGNON:	db 'RXMODEM '
 db ((VERSION AND 0F00h)/256)+'0','.',(VERSION AND 0Fh) +'0'

;Fall into CRLF

CRLF:	db	CR,LF,'$'

FNEMSG:	db	'No file name$'
FEMSG:	db	'File already exists$
CFEMSG:	db	'File create fail$'
EWFMSG:	db	'Disk write fail$'		;CP/M error
FCEMSG:	db	'FILE CLOSE FAIL!$'

ELEMSG:	db	(ERRLIM/10)+'0'		;too many block retries
	db	(ERRLIM-((ERRLIM/10)*10))+'0'
	db	' block errors$'
SYNMSG:	db	'Sync fail$'		;can't find SOH
LBMSG:	db	'Lost blocks$'		;out of sequence
OKMSG:	db	'OK$'

;******************************************************
;RAM Variables and Storage, all initialized during load
;******************************************************
;******************************************
;File Control Block, first byte pre-cleared
;******************************************
FCB:
FCBDR	db	0		;Initialized for Drive A
FCBFN	ds	8		;File name (8 chrs)
FCBFT	ds	3		;File Type (3 chrs)
FCBEXT	ds	24		;rest of file control block

;***********************
;Uninitialized variables
************************
ERRCNT:	ds	1	;Error count
TIMRLD:	ds	2	;receive timeout value
TIMRH:	ds	1	;high byte of timer

;************************************************
;Buffer for XMODEM blocks. This buffer overwrites
;the following initialization code, as well as
;CP/M's CCP. The buffer is forced to an even
;block boundary for faster compares.
;************************************************
BPAGER	equ	$
BUFFER:	equ	(BPAGER+BLKSIZ-1) and (0-BLKSIZ)

	END
