XMODEM

hayes_300_baud_smartmodem_02XMODEM is a protocol to transfer files from one machine to another. It originally was published by Ward Christensen on October 10, 1977.

This program is sometimes called MODEM or MODEM2.

The program is written in Z80 Assembler. The code runs as a module on CP/M based machines. This code was revised after a few years to add CRC in order to reduce errors in copying files.

The code was released widely as public domain so that anyone who wanted to use it could.

Z80 SOURCE CODE

;MODEM ROUTINE - SEND, RECV, COMPUTER, TERMINAL
; * * * * * * * * * * * * * * * * * * * *
;SENSE SWITCH CONTROLS:			*
;					*
;	A12 UP TO DISPLAY INCOMING DATA *
;	A13 UP TO DISPLAY OUTGOING DATA *
; * * * * * * * * * * * * * * * * * * * *
;
;09/23/77 FIRST WRITTEN BY WARD CHRISTENSEN
;09/25/77 FIRST TESTING COMPLETE
;09/26/77 ADD ERROR$LIMIT EQU
;10/01/77 CHANGE EXIT$CHAR FROM CTL-C TO
;	  CTL-E FOR USE W/TIMESHARING COMPUTERS
;10/10/77 CORRECT TO SEND ANY LENGTH FILE
;
MODEM$CTL$PORT	EQU	4
MODEM$SEND$MASK	EQU	2
SEND$READY	EQU	2	;VALUE WHEN READY
MODEM$RECV$MASK	EQU	1
RECV$READY	EQU	1	;BIT ON WHEN READY
MODEM$DATA$PORT	EQU	5
KEY$CTL$PORT	EQU	0	;KEYBOARD STATUS
KEY$READY$MASK	EQU	2
KEY$READY	EQU	2	;VALUE WHEN KEYBOARD READY
KEY$DATA$PORT	EQU	1
INIT$REQD	EQU	1	;MODEM INIT. REQ'D?
INIT$CHAR$1	EQU	3	;FIRST INIT CHAR TO CTL PORT
INIT$CHAR$2	EQU	15H	;2ND INIT CHAR TO CTL PORT
ERROR$LIMIT	EQU	10	;MAX ALLOWABLE ERRORS
EXIT$CHAR	EQU	'E'-40H ;CHAR TO EXIT FROM T OR C
	ORG	100H
	CALL	START	;GO PRINT ID
	DB	'MODEM PROGRAM AS OF '
	DB	'10/10/77',13,10,'$'
;FLAG FOR GENERATING TEST CODE TO USE KEYBOARD
;TO ECHO ACK/NAK WHILE TESTING:
TEST	EQU	0	;GENERATE TEST CODE
;
;DEFINE ASCII CHARACTERS USED
SOH	EQU	1
EOT	EQU	4
ACK	EQU	6
NAK	EQU	15H
LF	EQU	10
CR	EQU	13
;
START	POP	D	;GET ID MESSAGE
	MVI	C,PRINT
	CALL	BDOS	;PRINT ID MESSAGE
;INIT PRIVATE STACK
	LXI	H,0	;HL=0
	DAD	SP	;HL=STACK FROM CP/M
	SHLD	STACK	;..SAVE IT
	LXI	SP,STACK	;SP=MY STACK
;
	CALL	INIT$PORT
;GOBBLE UP GARBAGE CHARS FROM THE LINE
	MVI	B,1	;TIMEOUT DELAY
	CALL	RECV
	MVI	B,1
	CALL	RECV
;
	LDA	FCB+1	;GET OPTION (S R C T)
	PUSH	PSW	;SAVE OPTION
	CALL	MOVE$FCB ;MOVE FROM 6C TO 5C
	POP	PSW	;GET OPTION
	CPI	'S'
	JZ	SEND$FILE
	CPI	'R'
	JZ	RECV$FILE
	CPI	'C'
	JZ	COMPUTER
	CPI	'T'
	JZ	TERMINAL
;INVALID OPTION
	CALL	ERXIT	;EXIT W/ERROR
	DB	'++INVALID OPTION ON MODEM COMMAND - ',CR,LF
	DB	'MUST BE ONE OF THE FOLLOWING:',CR,LF
	DB	'MODEM SEND FILENAME',CR,LF
	DB	'MODEM RECV FILENAME',CR,LF
	DB	'MODEM COMPUTER',CR,LF
	DB	'MODEM TERMINAL',CR,LF
	DB	'  ABBREV. ALLOWED: S R C T$'
;
;****************COMPUTER****************
;
;TERMINAL-TERMINAL W/ECHO SENT BY THIS PROGRAM
;'EXIT$CHAR' TYPED TO RE-BOOT
;IF ONE COMPUTER IS IN COMPUTER MODE,
;THE OTHER SHOULD BE IN TERMINAL MODE.
;AT NO TIME SHOULD BOTH BE IN COMPUTER
;MODE BECAUSE LINE ERRORS WILL BE PING-PONGED
;BACK AND FORTH AD INFINITUM.
COMPUTER:
	IN	MODEM$CTL$PORT
	ANI	MODEM$RECV$MASK
	CPI	RECV$READY
	JZ	LINE$CHAR
;NOTHING FROM LINE, CHECK KEYBOARD
	MVI	C,CONST	;CHECK STATUS
	CALL	BDOS
	ORA	A	;READY?
	JZ	COMPUTER ;..NO
	MVI	C,RDCON
	CALL	BDOS	;GET CHAR
	CPI	EXIT$CHAR ;END?
	JZ	EXIT	;YES, EXIT
	OUT	MODEM$DATA$PORT ;SEND CHAR
	JMP	COMPUTER
;GOT CHAR FROM LINE
LINE$CHAR:
	IN	MODEM$DATA$PORT
	OUT	MODEM$DATA$PORT ;ECHO
	CALL	TYPE	;TYPE IT
	JMP	COMPUTER
;
;**************TERMINAL****************
;
;SEE NOTES UNDER 'COMPUTER'
;
TERMINAL:
	IN	KEY$CTL$PORT
	ANI	KEY$READY$MASK
	CPI	KEY$READY
	JNZ	TRECV	;NOTHING FROM KEYBOARD
	IN	KEY$DATA$PORT
	ANI	7FH
	CPI	EXIT$CHAR ;TIME TO END?
	JZ	EXIT
	OUT	MODEM$DATA$PORT
TRECV	IN	MODEM$CTL$PORT
	ANI	MODEM$RECV$MASK
	CPI	RECV$READY
	JNZ	TERMINAL
	IN	MODEM$DATA$PORT
	CALL	TYPE
	JMP	TERMINAL
;INIT SERIAL PORT
INIT$PORT:
	LXI	D,MSG$BAUD
	CALL	PRINT$MESSAGE
	IF	INIT$REQD
	MVI	A,INIT$CHAR$1
	OUT	MODEM$CTL$PORT
	MVI	A,INIT$CHAR$2
	OUT	MODEM$CTL$PORT
	ENDIF
;GET THE SPEED
	XRA	A	;GET A ZERO
	CALL	SEND	;SEND A CHAR
	XRA	A	;GET ZERO
	CALL	SEND	;SEND AGAIN
;WAIT, TIMING TO DETERMINE BAUD RATE
	LXI	B,0	;INIT COUNT
INIT$WAIT:
	IN	MODEM$CTL$PORT
	ANI	MODEM$SEND$MASK
	CPI	SEND$READY
	JZ	INIT$WAIT$END
	DCR	C
	JNZ	INIT$WAIT
	DCR	B
	JNZ	INIT$WAIT
	CALL	ERXIT
	DB	'++TIME OUT DETERMINING BAUD RATE$'
INIT$WAIT$END:
	MOV	A,B	;GET COUNT
	CPI	0F5H	;110 BAUD = F0,
	JC	BAUD$110 ;300 BAUD = FA
;BAUD RATE 300
	LXI	D,MSG$300
INIT$PRINT:
	CALL	PRINT$MESSAGE
	RET		;FROM INIT$PORT
BAUD$110:
	LXI	D,MSG$110
	JMP	INIT$PRINT
MSG$BAUD DB 'BAUD RATE IS $'
MSG$110	DB	'110',CR,LF,'$'
MSG$300	DB	'300',CR,LF,'$'
;MOVE FCB (SECOND OPERAND ON COMMAND)
; TO NORMAL FCB LOCATION
MOVE$FCB:
	LXI	H,FCB
	LXI	D,FCB+16
	MVI	B,16
MOVE$LOOP:
	LDAX	D
	MOV	M,A
	INX	D
	INX	H
	DCR	B
	JNZ	MOVE$LOOP
	XRA	A	;GET 0
	STA	FCB+32	;ZERO RECORD #
	RET
;
;*****************SEND FILE***************
;
SEND$FILE:
	CALL	OPEN$FILE	;OPEN THE FILE
	LXI	D,OPENM
	CALL	PRINT$MESSAGE
SENDB	XRA	A	;GET A ZERO
	STA	ERRCT	;ZERO ERROR COUNT
;READ SECTOR, SEND IT
	CALL	READ$SECTOR
	LDA	SECTNO	;INCR SECT NO.
	INR	A
	STA	SECTNO
;SEND OR REPEAT SECTOR
REPTB	LXI	D,SECTMSG
	CALL	PRINT$MESSAGE
	LDA	SECTNO
	CALL	HEXO
	CALL	CRLF
	MVI	A,SOH
	CALL	SEND
	LDA	SECTNO
	CALL	SEND
	LDA	SECTNO
	CMA
	CALL	SEND
	MVI	C,0	;INIT CKSUM
	LXI	H,80H
SENDC	MOV	A,M
	CALL	SEND
	INX	H
	MOV	A,H
	CPI	1	;DONE WITH SECTOR?
	JNZ	SENDC
;SECTOR SENT, SEND CKSUM
	MOV	A,C	;GET CKSUM
	CALL	SEND
;GET ACK ON SECTOR
	MVI	B,4	;WAIT 4 SECONDS MAX
	CALL	RECV
	JNC	SNTO	;NO TIMEOUT
;TIMED OUT WAITING FOR ACK
	CALL	TOUT	;PRINT 'TIMEOUT', ERRCT
DATERR	LDA	ERRCT
	INR	A
	STA	ERRCT
	CPI	ERROR$LIMIT
	JC	REPTB	;REPEAT SECTOR
;SECTOR SEND NO GOOD AFTER 10 TRIES
	CALL	ERXIT
	DB	'CAN''T SEND SECTOR '
	DB	'- ABORTING',13,10,'$'
SECTMSG	DB	'SENDING SECTOR $'
;NO TIMEOUT SENDING SECTOR
SNTO	CPI	ACK	;ACK RECIEVED?
	JZ	SENDB	;..YES, SEND NEXT SECT
;ACK NOT RECIEVED
	CALL	HEXO	;TYPE CHR IN HEX
	LXI	D,ERR1
	CALL	PRINT$MESSAGE
	JMP	DATERR	;GO TO DATA ERROR
ERR1	DB	'H RECEIVED, NOT ACK',13,10,'$'
OPENM	DB	'FILE OPEN',13,10,'$'
;
;**************RECEIVE FILE****************
;
RECV$FILE:
	CALL	ERASE$OLD$FILE
	CALL	MAKE$NEW$FILE
RECV$LOOP:
	XRA	A	;GET 0
	STA	ERRCT	;INIT ERROR COUNT
RECV$HDR:
	LXI	D,RMSG
	CALL	PRINT$MESSAGE
	LDA	SECTNO
	INR	A
	CALL	HEXO
	CALL	CRLF
	MVI	B,5	;5 SEC TIMEOUT
	CALL	RECV
	JNC	RHNTO	;NO TIMEOUT
RECV$HDR$TIMEOUT:
	CALL	TOUT	;PRINT TIMEOUT
RECV$SECT$ERR:
;PURGE THE LINE OF INPUT CHARS
	MVI	B,1	;1 SEC W/NO CHARS
	CALL	RECV
	JNC	RECV$SECT$ERR ;LOOP UNTIL SENDER DONE
	MVI	A,NAK
	CALL	SEND	;SEND NAK
	LDA	ERRCT
	INR	A
	STA	ERRCT
	CPI	ERROR$LIMIT
	JC	RECV$HDR
	CALL	ERXIT
	DB	'++UNABLE TO GET VALID HEADER',0DH,0AH,'$'
RMSG	DB	'WAITING FOR SECTOR #$'
;GOT CHAR - MUST BE SOH
RHNTO	CPI	SOH
	JZ	GOT$SOH
	ORA	A	;00 FROM SPEED CHECK?
	JZ	RECV$HDR
	CPI	EOT
	JZ	GOT$EOT
;DIDN'T GET SOH - 
	CALL	HEXO
	LXI	D,ERRSOH
	CALL	PRINT$MESSAGE
	JMP	RECV$SECT$ERR
ERRSOH	DB	'H RECEIVED, NOT SOH',0DH,0AH,'$'
GOT$SOH:
	MVI	B,1
	CALL	RECV
	JC	RECV$HDR$TIMEOUT
	MOV	D,A	;D=BLK #
	MVI	B,1
	CALL	RECV	;GET CMA'D SECT #
	JC	RECV$HDR$TIMEOUT
	CMA
	CMP	D	;GOOD SECTOR #?
	IF	TEST
	JMP	RECV$SECTOR
	ENDIF
	JZ	RECV$SECTOR
;GOT BAD SECTOR #
	LXI	D,ERR2
	CALL	PRINT$MESSAGE
	JMP	RECV$SECT$ERR
ERR2	DB	'++BAD SECTOR # IN HDR',0DH,0AH,'$'
;
RECV$SECTOR:
	MOV	A,D	;GET SECTOR #
	STA	RECVD$SECT$NO
	MVI	C,0	;INIT CKSUM
	LXI	H,80H	;POINT TO BUFFER
RECV$CHAR:
	MVI	B,1	;1 SEC TIMEOUT
	CALL	RECV	;GET CHAR
	JC	RECV$HDR$TIMEOUT
	MOV	M,A	;STORE CHAR
	INR	L	;DONE?
	JNZ	RECV$CHAR
;VERIFY CHECKSUM
	MOV	D,C	;SAVE CHECKSUM
	MVI	B,1	;TIMEOUT
	CALL	RECV	;GET CHECKSUM
	JC	RECV$HDR$TIMEOUT
	CMP	D	;CHECK
	JNZ	RECV$CKSUM$ERR
;
;GOT A SECTOR, WRITE IF = 1+PREV SECTOR
;
	LDA	RECVD$SECT$NO
	MOV	B,A	;SAVE IT
	LDA	SECTNO	;GET PREV
	INR	A	;CALC NEXT SECTOR #
	CMP	B	;MATCH?
	JNZ	DO$ACK
;GOT NEW SECTOR - WRITE IT
	LXI	D,FCB
	MVI	C,WRITE
	CALL	BDOS
	ORA	A
	JNZ	WRITE$ERROR
	LDA	RECVD$SECT$NO
	STA	SECTNO	;UPDATE SECTOR #
DO$ACK	MVI	A,ACK
	CALL	SEND
	JMP	RECV$LOOP
;
WRITE$ERROR:
	CALL	ERXIT
	DB	'++ERROR WRITING FILE',0DH,0AH,'$'
;
RECV$CKSUM$ERR:
	LXI	D,ERR3
	CALL	PRINT$MESSAGE
	JMP	RECV$SECT$ERR
ERR3	DB	'++BAD CKSUM ON SECTOR'
	DB	0DH,0AH,'$'
;
GOT$EOT:
	MVI	A,ACK	;ACK THE EOT
	CALL	SEND
	LXI	D,FCB
	MVI	C,CLOSE
	CALL	BDOS
	INR	A
	JNZ	XFER$CPLT
	CALL	ERXIT
	DB	'++ERROR CLOSING FILE$'
;
ERASE$OLD$FILE:
	LXI	D,FCB
	MVI	C,SRCHF	;SEE IF IT EXISTS
	CALL	BDOS
	INR	A	;FOUND?
	RZ		;NO, RETURN
	LXI	D,EXIST
	CALL	PRINT$MESSAGE
	MVI	C,RDCON
	CALL	BDOS
	CPI	'Y'
	JNZ	0	;REBOOT IF NOT ERASE
	CALL	CRLF
;ERASE OLD FILE
	LXI	D,FCB
	MVI	C,ERASE
	CALL	BDOS
	RET
EXIST	DB	'++FILE EXISTS, TYPE Y TO ERASE:$'
;
MAKE$NEW$FILE:
	LXI	D,FCB
	MVI	C,MAKE
	CALL	BDOS
	INR	A	;FF=BAD
	RNZ		;OPEN OK
;DIRECTORY FULL - CAN'T MAKE FILE
	CALL	ERXIT
	DB	'++ERROR - CAN''T MAKE FILE',0DH,0AH
	DB	'++DIRECTORY MUST BE FULL',0DH,0AH,'$'
;
; S U B R O U T I N E S
;
;OPEN FILE
OPEN$FILE	LXI	D,FCB
	MVI	C,OPEN
	CALL	BDOS
	INR	A	;OPEN OK?
	RNZ		;GOOD OPEN
	CALL	ERXIT
	DB	'CAN''T OPEN FILE$'
; - - - - - - - - - - - - - - -
PRINT$MESSAGE:
	MVI	C,PRINT
	JMP	BDOS	;PRINT MESSAGE, RETURN
; - - - - - - - - - - - - - - -
;EXIT PRINTING MESSAGE FOLLOWING 'CALL ERXIT'
ERXIT	POP	D	;GET MESSAGE
	CALL	PRINT$MESSAGE	;PRINT IT
EXIT	LHLD	STACK	;GET ORIGINAL STACK
	SPHL		;RESTORE IT
	RET		;--EXIT-- TO CP/M
; - - - - - - - - - - - - - - -
;MODEM RECV
RECV	PUSH	D	;SAVE
MSEC	LXI	D,0BBBBH	;1 SEC DCR COUNT
	IF	NOT TEST
MWTI	IN	MODEM$CTL$PORT
	ANI	MODEM$RECV$MASK
	CPI	RECV$READY
	JZ	MCHAR	;GOT CHAR
	ENDIF
	IF	TEST
MWTI	IN	KEY$CTL$PORT	;READ KEYBOARD
	ANI	KEY$READY$MASK
	CPI	KEY$READY
	JZ	MCHAR
	ENDIF
	DCR	E	;COUNT DOWN
	JNZ	MWTI	;FOR TIMEOUT
	DCR	D
	JNZ	MWTI
	DCR	B	;DCR # OF SECONDS
	JNZ	MSEC
;MODEM TIMED OUT RECEIVING
	POP	D	;RESTORE D,E
	STC		;CARRY SHOWS TIMEOUT
	RET
;GOT MODEM CHAR
	IF	NOT TEST
MCHAR	IN	MODEM$DATA$PORT
	ENDIF
	IF	TEST
MCHAR	IN	KEY$DATA$PORT
	ANI	7FH	;DEL PARITY FROM KEYBOAREAD
	ENDIF
	POP	D	;RESTORE DE
;CALC CHECKSUM
	PUSH	PSW
	ADD	C
	MOV	C,A
;CHECK IF MONITORING INPUT
	IN	0FFH
	ANI	10H
	JZ	NO$MON$INPUT
	POP	PSW
	PUSH	PSW
	CALL	SHOW	;CHAR RECEIVED
NO$MON$INPUT:
	POP	PSW
;TURN OFF CARRY TO SHOW NO TIMEOUT
	ORA	A
	RET
; - - - - - - - - - - - - - - -
;MODEM SEND CHAR ROUTINE
SEND	PUSH	PSW
;CHECK IF MONITORING OUTPUT
	IN	0FFH
	ANI	20H
	JZ	NO$MON$OUTPUT
	POP	PSW
	PUSH	PSW
	CALL	SHOW
NO$MON$OUTPUT:
	POP	PSW
	PUSH	PSW
	ADD	C	;CALC CKSUM
	MOV	C,A
SENDW	IN	MODEM$CTL$PORT
	ANI	MODEM$SEND$MASK
	CPI	SEND$READY
	JNZ	SENDW
	POP	PSW	;GET CHAR
	OUT	MODEM$DATA$PORT
	RET
; - - - - - - - - - - - - - - -
;SHOW CHAR RECEIVED OR SENT
SHOW	CPI	0AH	;LF?
	JZ	TYPE
	CPI	0DH
	JZ	TYPE
	CPI	09	;TAB
	JZ	TYPE
	CPI	' '
	JC	SHOWHEX
	CPI	7FH
	JC	TYPE
SHOWHEX	PUSH	PSW
	MVI	A,'('
	CALL	TYPE
	POP	PSW
	CALL	HEXO
	MVI	A,')'
	JMP	TYPE
; - - - - - - - - - - - - - - -
;PRINT TIMEOUT MESSAGE
TOUTM	DB	'TIMEOUT $'
TOUT	LXI	D,TOUTM
	CALL	PRINT$MESSAGE
PRINT$ERRCT:
	LDA	ERRCT
	CALL	HEXO	;FALL INTO CR/LF
; - - - - - - - - - - - - - - -
CRLF	MVI	A,13
	CALL	TYPE
	MVI	A,10
; - - - - - - - - - - - - - - -
TYPE	PUSH	PSW
	PUSH	B
	PUSH	D
	PUSH	H
	MOV	E,A
	MVI	C,WRCON
	CALL	BDOS
	POP	H
	POP	D
	POP	B
	POP	PSW
	RET
; - - - - - - - - - - - - - - -
;HEX OUTPUT
HEXO	PUSH	PSW
	RAR
	RAR
	RAR
	RAR
	CALL	NIBBL
	POP	PSW
NIBBL	ANI	0FH
	CPI	10
	JC	ISNUM
	ADI	7
ISNUM	ADI	'0'
	JMP	TYPE
; - - - - - - - - - - - - - - -
;FILE READ ROUTINE
READ$SECTOR:
	LXI	D,FCB
	MVI	C,READ
	CALL	BDOS
	ORA	A
	RZ
	DCR	A	;EOF?
	JNZ	RDERR
;EOF
	XRA	A
	STA	ERRCT
	LXI	D,FSENTM ;FILE SENT MESSAGE
	CALL	PRINT$MESSAGE
SEOT	MVI	A,EOT
	CALL	SEND
	MVI	B,5	;WAIT 5 SEC FOR TIMEOUT
	CALL	RECV
	JC	EOTTOT	;EOT TIMEOUT
	CPI	ACK
	JZ	XFER$CPLT
;ACK NOT RECIEVED
	CALL	HEXO
	LXI	D,ERR1
	CALL	PRINT$MESSAGE
EOTERR	LDA	ERRCT
	INR	A
	STA	ERRCT
	CPI	ERROR$LIMIT
	JC	SEOT
	CALL	ERXIT
	DB	'NO ACK RECIEVED ON EOT$',10,13
FSENTM	DB	13,10,'FILE SENT, SENDING EOT''S',10,13,'$'
;TIMEOUT ON EOT
EOTTOT	CALL	TOUT
	JMP	EOTERR
;READ ERROR
RDERR	CALL	ERXIT
	DB	'++FILE READ ERROR$'
; - - - - - - - - - - - - - - -
;DONE - CLOSE UP SHOP
XFER$CPLT:
	CALL	ERXIT
	DB	13,10,'TRANSFER COMPLETE$'
	DS	40	;STACK AREA
STACK	DS	2	;STACK POINTER
RECVD$SECT$NO	DB	0
SECTNO	DB	0	;CURRENT SECTOR NUMBER 
ERRCT	DB	0	;ERROR COUNT
;
; BDOS EQUATES (VERSION 2)
;
RDCON	EQU	1
WRCON	EQU	2
PRINT	EQU	9
CONST	EQU	11	;CONSOLE STAT
OPEN	EQU	15	;0FFH=NOT FOUND
CLOSE	EQU	16	;   "	"
SRCHF	EQU	17	;   "	"
SRCHN	EQU	18	;   "	"
ERASE	EQU	19	;NO RET CODE
READ	EQU	20	;0=OK, 1=EOF
WRITE	EQU	21	;0=OK, 1=ERR, 2=?, 0FFH=NO DIR SPC
MAKE	EQU	22	;0FFH=BAD
REN	EQU	23	;0FFH=BAD
STDMA	EQU	26
BDOS	EQU	5
REIPL	EQU	0
FCB	EQU	5CH	;SYSTEM FCB