Skip to content

Instantly share code, notes, and snippets.

@feilipu
Created March 19, 2022 09:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save feilipu/73f84745d9019a20ee1ef15d1fee948c to your computer and use it in GitHub Desktop.
Save feilipu/73f84745d9019a20ee1ef15d1fee948c to your computer and use it in GitHub Desktop.
CP/M Xmodem V2.9 by Martin Eberhard - Modified for CP/M-IDE
;==============================================================
; CP/M Xmodem by Martin Eberhard
;==============================================================
;A command line and configuration file-driven program for
;transferring files to and from a CP/M system using the
;Xmodem protocol, supporting both the original checksum error
;checking, and also the newer Xmodem-CRC error checking
;
;This program has been tested at 115.2 K Baud, running on a
;4 MHz Z80 with 0-wait state memory, and performing direct I/O
;via a CCS 2719A serial board (which is based on the Z80-DART).
;Calculations show that a 2 MHz 8080 ought to be able to send
;and receive up to 76.8K baud (with for example a Cromemco
;TU-ART), so long as there are not too many wait states.
;
; To use Xmodem, type:
; XMODEM <filename> {option list}
;
; A file name is mandatory, and can be any legal CP/M file
; name. If you are receiving with Xmodem and the file already
; exists, then you will be asked if it should be overwritten.
; If you are sending, then the file must exist, obviously.
;
; Xmodem first looks for a file called XMODEM.CFG on CP/M's
; default drive. If found, then this file is parsed for
; options, the same as the command line. XMODEM.CFG is parsed
; first, so that options that are set on the command line will
; override those set in XMODEM.CFG.
;
; XMODEM.CFG and the command line both support the following
; options (though some are less useful on the command line.)
;
; /R Specifies Receive mode
;
; /S Specifies Send mode
;
; If neither /R nor /S is specified then you will be asked.
;
; /C Selects checksum error checking when receiving;
; otherwise receiving uses CRC error checking. When
; sending, the error-checking mode is set by the receiver.
;
; /E Specifies an enhanced RDR routine that returns with the
; Z flag set if no character is waiting. Note that this
; option does not actually select the RDR device as the
; transfer port. (/X2 does.)
;
; /In h0 h1 h2... (max h11) Defines assembly code for the
; custom I/O port (used by the /X3 option), using Intel
; 8080 machine language.
;
; n = 0 specifies initialization code, to be run when
; command line and config file parsing are done.
; All registers may be trashed. This is useful
; for setting the baud rate, etc.
;
; n = 1 installs a transmit byte routine.
; on entry to this routine, the character to send
; is in c. do not trash de or hl. Sample custom
; transmit routine (for SOLOS):
; 48 mov b,c ;SOLOS wants chr in b
; 3e 01 mvi a,1 ;serial pseudoport
; cd 1c c0 call AOUT ;output character
; Encode as follows:
; /I1 48 3E 01 CD 1C C0
;
; n = 2 installs a receive status subroutine, which
; should end with the Z flag set if no character
; is waiting, and optionally the chr in a. Do not
; trash any registers except psw. Sample routine:
; 3e 01 mvi a,1 ;serial pseudoport
; cd 22 c0 call AINP ;input character,
; ;Z set if none
; Encode as follows:
; /I2 3E 01 CD 22 C0
;
; n = 3 installs a receive character routine, assuming a
; character is waiting. Ends with the character in
; a. Trashes no registers except psw. If the status
; routine returns the character, (e.g. for SOLOS),
; then no /I3 option is required.
;
; /Knn sets the maximum buffer size in k-bytes. If no /K
; option, or if the specified size is larger than the
; free RAM, then all free RAM will be used.
;
; /M Print message on console. This lets you tell the user
; e.g. what port is set up for direct I/O in XMODEM.CFG
;
; /O Specifies an output sequence for an I/O port, intended to
; initialize the direct I/O port. The form of this
; option is:
; /O pp h1 h2 ... hn
; where pp is the port address, and all the subsequent
; bytes are sent to that port. You can have more than
; one /O option, allowing you e.g. to initialize the
; UART and also to set the baud rate.
;
; /P Defines the direct I/O transfer port (used by the /X2
; option). The form of this command is:
; /P ss dd qq rr tt
; where:
; ss is the status port (for Rx and Tx)
; dd is the data port (for both Rx and tx)
; qq is 00 if the ready bits are true when low
; and 01 if the ready bits are true when high
; rr is the receive-ready bit mask
; tt is the transmit-ready bit mask
;
; Xmodem assumes that the receive port works like this:
; RXWAIT: in <status port>
; ani <Rx Mask>
; jz/jnz RXWAIT
; nop
; in <data port>
;
; ..and the transmit port works like this:
; mov c,a
; TXWAIT: in <status port>
; ani <Tx Mask>
; jz/jnz TXWAIT
; mov a,c
; out <data port>
;
; Any port that can work with these templates will work
; with Xmodem's /X2 option.
;
; All variables for the /O and /P commands are in hexidecimal,
; and must be exactly two characters long. Legal characters
; are: {0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F} (No lowercase.)
;
; /Q Specifies quiet mode, preventing messages and pacifiers
; from being printed on the console during transfer. This
; is particularly useful if the port you are using is also
; the port used for CP/M's console. Otherwise, a '+' will
; be printed on CON for every succesful block, and a '-'
; will be printed for every block retry.
;
; /Xn Specifies the transfer port:
; n=0 uses CP/M's CON device. Contrary to CP/M specs,
; the CON input port must not strip parity.
;
; n=1 uses CP/M'd RDR and PUN devices. Contrary to CP/M
; specs, the RDR input port must not strip parity.
; Also use /E if the RDR driver has been enhanced
; to return with the Z flag set when no character is
; waiting. (Otherwise, no timeout is possible when
; waiting for a RDR character.)
;
; n=2 uses direct I/O, which can be set up using the /P
; option. If no /P option is entered, then /X2 will
; select a MITS 88-SIO port.
;
; n=3 uses custom-patched I/O routines, set with the
; /I option. If no /I option is entered then
; transfers with /X3 will just cause errors.
;
; /Zn Specifies CPU speed in MHz - n is between 1 and 8.
; The CPU speed used for timeouts while sending or
; receiving. The default is 2 MHz for an 8080, and 4 MHz
; for a Z80. (Xmodem can tell the difference.)
;
; A semicolon begins a comment on a new line. All characters
; from the ';' until the end of the line will be ignored.
;
; Here is a sample XMODEM.CFG file:
;
; /MDirect I/O is configured for 88-2SIO port B
; /P 12 13 01 01 02 ;set up for an 88-2SIO Port B
; /O 12 03 15 ;8 bits, 1 stop, no parity
; /X2 ;use direct I/O
;
; You can usually abort a transfer with ^C on the console.
;
;==============================================================
;PROGRAM NOTES
;
;Reducing the buffer size with the /K option can also reduce
;the amount of time consumed by disk accesses.
;
;The /P option modifies code in the direct I/O transmit and
;receive routines. This is because the 8080 has no command to
;IN or OUT with a port that is specified by a register value -
;so self-modifying code is the only way.
;
;Code that is only used during initialization is at the end,
;and gets overwritten by the block buffer. Xmodem uses all
;available RAM (past its 2K runtime code, up through CP/M's
;CCP) for buffering received and transmitted data, to speed up
;transfers by minimizing disk accesses. This allows Xmodem to
;run comfortably in (for example) a 16K-byte CP/M system
;(with 10K of user memory), and still have a decent 8K data
;buffer and reasonably robust options and messages.
;
;The XModem spec calls for a sender to time out after 10
;seconds if it does not receive the ACK after sending a
;complete block. XMODEM.COM holds off the sender while it
;writes to disk by waiting to send the ACK for the most recent
;block. Depending on the BIOS, CP/M can be pretty slow when
;writing to double-density floppy disks (due for example to
;its bloxking/deblocking algorithm, or verified writes). To
;prevent the sender from timing out while Xmodem writes to
;disk, XMODEM.COM receives an occasional Xmodem block while it
;is performing disk writes. To stay within the 10-second
;timeout spec with a slow deblocking algorithm in the BIOS,
;WBPERX should be no larger than about 40. (Hyperterminal is
;happy with a value even higher than 64.) CP/M is usually much
;faster at reading from disk so an equivalent feature is not
;implemented for sending files.
;
;The position of the CRC calculation table (CRCTAB) is forced
;to be on a 256-byte page boundary. Subroutines have been
;shoved around to minimize the number of wasted bytes before
;this table.
;
;This code implicitly assumes that Xmodem blocks are the same
;size as CP/M disk records - 128 bytes each.
;
;This program's help screens will display correctly on displays
;that are 16X64 characters or larger.
;
; Assemble with Digital Research's ASM Assembler
;
;==============================================================
; Thanks to Ward Christensen for inventing Xmodem and keeping
; it simple.
; Thanks to John Byrns for the Xmodem-CRC extension, which was
; adopted in Windows Hyperterm.
; Thanks to Keith Petersen, W8SDZ, for a few ideas I borrowed
; from his XMODEM.ASM V3.2
;==============================================================
;Revision History:
;
; 2.9-ide 13 March 2022 feilipu
; Increased WBPERX to write 252 128-byte disk records
; Set default port to /X0 for CON:
;
; 2.9 14 November 2021 M. Eberhard
; Eat received characters until the line is idle for 1 second,
; before receiving a file. Squeeze code a bit to keep it under
; 4 Kbytes.
;
; 2.8 9 April 2019 M. Eberhard
; Receive blocks occasionally while flushing the buffer to
; disk, to prevent sender timeout. Recognize CAN chr. Send CAN
; when aborting reception. Send EOT when aborting during
; sending. Add /K option. Better abort algorithm. Some
; improved messages. Squeeze code to keep it under 4K-bytes.
; Increase the number of bytes for the /I routines from 8 to
; 12 bytes. Eliminate seperate stack for use during INIT.
; When receiving, chuck the first received chr if it's bogus.
;
; 2.7 9 October 2017 M. Eberhard
; Fix stack bug when switching stacks
;
; 2.6 10 September 2017
; Significant speed up by using table-driven CRC algorithm.
; Make the initialization code modify the jump addresses in
; RXBYTE and TXBYTE based on the type of transfer port
; selected, eliminated some subroutine calls, and a few other
; speed optimizations. Ask user for direction if no /R or /S
; was specified. Use all RAM (even more than 32K) for the
; buffer. perform initial buffer fill on Sends (before
; determining error checking mode), while the disk is still
; spinning. Flush a byte from the receiver port at the
; beginning, if possible. Squeeze a bit to keep disk file
; smaller than 4K-bytes. Tidy up comments and labels.
;
; 2.5 9 May 2017 M. Eberhard
; Clean up file name in FCB if a command option was crammed
; against the file name (no spaces) on the command line.
;
; 2.4 4 August 2016 M. Eberhard
; Fix bug in TXLOOP that would cause sending with checksums
; to fail. Fix bug causing a spurious block after the 1st
; buffer-full of received data. (Thanks to Bob Bell for
; helping find and fix these bugs.) Add /I cmd, and add /X3
; option for custom port routine patching. (This makes it
; possible to call external I/O routines, such as SOLOS or
; POLEX.) Also cleaned up comments.
;
; 2.3 9 Oct 2014 M. Eberhard
; Eliminate intermediate data buffer. Support CP/M 1.4
;
; 2.2 7 Oct 2014 M. Eberhard
; fix error-reporting bug, increase stack size for BDOS
;
; 2.1 3 Oct 2014 M. Eberhard
; Fix bug in reporting the source of an error
; Speed up RDR/PUN
; require CR after "Y" on overwrite question
;
; 2.0 1 OCT 2014 M. Eberhard
; New major release:
; + Supports a configuration file (XMODEM.CFG), with same
; options as on the command line
; + combine features of all 1.0x assembly options
; + Define direct I/O port in XMODEM.CFG (or on command line)
; + User-set CPU speed (/Z), overrides 8080/Z80 defaults
; + Option to delete file on /R, if it already exists
; + Include which port we are using in announcement prior to
; Xmodem file transfer
; + A few new timeouts, to prevent hanging in odd situations
; + Several other minor enhancements
;
; 1.0x 06 APR 2013 through 27 SEP 2014 M. Eberhard
; Command-line driven versions Based on Xmodem for CDOS
; (Z-80), version 1.03 by M. Eberhard
;==============================================================
;---------------------------
;This code's revision number
;---------------------------
VERSION equ 0209h ;high byte = major revision number
;low byte = minor version
FALSE equ 0
TRUE equ not FALSE
ERRLIM equ 10 ;Max error-retries. 10 is standard.
DBSIZ equ 63 ;default buffer size (kbytes)
;(Uses the smaller of this
;and "all available RAM")
CRSTAL equ 10 ;delay (in seconds) before
;..receiving when the console
;..is the transfer port
;The following parameter exists to prevent the sender on
;the other end of the serial port from timing out while
;we do disk writes. See PROGRAM NOTES above.
WBPERX equ 252 ;# of 128-byte disk records to
;write before requesting
;another Xmodem block (max=256)
;Timeout values in seconds. Values in parenthesis are
;Xmodem standard values.
SOHTO equ 10 ;(10)sender to send SOH
NAKTO equ 90 ;(90)receiver to send init NAK
ACKTO equ 60 ;(60)receiver to ACK (or NAK)
;(time to write to disk)
BLKSIZ equ 128 ;bytes per Xmodem block
;DO NOT CHANGE. BLKSIZ must be 128
SECSIZ equ 128 ;CP/M logical-sector size must be 128
;Progress pacifiers printed on the console
PACACK equ '+' ;Sent/Received a good block
PACNAK equ '-' ;Sent/Received a NAK
PACLIN equ 60 ;pacifiers per line
;The following cycle values are used in the timing loops for
;timeouts when transferring via the CON or the RDR and PUN.
;It is ok if they are imperfect - the Xmodem protocol is quite
;tolerant of timing variations. The example BIOS Code below was
;used to estimate these cycle counts for CRTIME.
CRTIME equ 144 ;number of cpu cycles that BIOS uses to
;return with no chr ready for custom
;RDR driver
EXTIME equ 135 ;Number of cycles an external receive
;routine (e.g. SOLOS) will use for testing
;status, when a chr is not ready.
;===Example BIOS Code==========================================
;Timing est. for getting reader status via custom RDR driver.
;Assume the IOBYTE is implemented, and assume RDR=UR1
;(the desired RDR port)
;This takes 95 8080 cycles.
; jmp RDRIN ;(10) BIOS jump vector
; ...
;RDRIN: lda IOBYTE ;(13) which reader port?
; ani 0Ch ;(7)
; jz <not taken> ;(10) not RDR=TTY
; cpi 8 ;(7)
; jc <not taken> ;(10) not RDR=HSR
; jz UR1ST ;(10) RDR=UR1
; ...
;UR1ST: in <port> ;(10) get reader stat
; ani <mask> ;(7) test, set Z
; rz ;(11) return from BIOS
;===Example BIOS Code==========================================
;Timing estimate for getting console status.
;Assume the IOBYTE is implemented, and assume CON=CRT
;This takes 85 8080 cycles.
; jmp CONST ;(10) BIOS jump vector
; ...
;RDRIN: lda IOBYTE ;(13) which CON port?
; ani 03h ;(7)
; jz <not taken> ;(10) not CON=TTY
; cpi 2 ;(7)
; jc CRTST ;(10) CON=CRT
; ...
;CRTST: in <port> ;(10) get reader stat
; ani <mask> ;(7) test, set Z
; rz ;(11) return from BIOS
;==============================================================
;**************
; 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
IOBYTE equ WBOOT+3
;CDISK equ WBOOT+4 ;Login drive
BDOS equ WBOOT+5 ;BDOS Entry Point
BDOSA equ WBOOT+6 ;First address of BDOS
;(can overlay up to here)
FCB equ WBOOT+5CH ;CP/M file control blk
FCBDR equ FCB ;Drive Descriptor
FCBFN equ FCB+1 ;File name (8 chrs)
FCBFT equ FCB+9 ;File Type (3 chrs)
FCBNL equ 11 ;File name length
FCBEXT equ FCB+12 ;File extent within FCB
FCBCLR equ 24 ;# of bytes to clear,
;starting at FCBEXT
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 are done via
;direct BIOS calls, not BDOS calls.
;------------------------------------------
BRESET equ 0 ;System Reset
;BCONIN equ 1 ;Read Console Chr
;BCONOT equ 2 ;Type Chr on Console
;BRDRIN equ 3 ;Read Reader Chr
;BPUNOT equ 4 ;Write Punch Chr
;BPRINT equ 9 ;Print $-terminated String
BRDCON equ 10 ;Get Line from Console
;BCONST equ 11 ;Console Status (<>0 IF CHR)
;BDRST equ 13 ;Reset Disk
;BSDISK equ 14 ;select disk
BOPEN equ 15 ;Disk File Open
BCLOSE equ 16 ;Close disk file, FCB at de
BSERCH equ 17 ;Search dir for file, FCB at de
BDELET equ 19 ;delete file, FCB at (de)
BREAD equ 20 ;Read from Disk, 0=OK, <>0=EOF
BWRITE equ 21 ;Write next record, 0=OK, <>0=ERR
BMAKE equ 22 ;Make new file, 0FFH=BAD
;BCDISK equ 25 ;get current disk
BSTDMA equ 26 ;Set disk buffer to (de)
;--------------------------------------------
;BIOS Entry Points, relative to the base
;address in WBOOT. BIOS calls are used
;(instead of BDOS calls) where speed matters.
;--------------------------------------------
CONST equ 06h ;Console Status
CONIN equ 09h ;Console Input
CONOUT equ 0Ch ;Console output
PUNCH equ 12h ;punch output
READER equ 15h ;reader input
;-----------------------------------
;Altair 88-SIO Registers and Equates
;(Used as a default direct port.)
;-----------------------------------
SIOSTA equ 00h ;status port
SIODAT equ 01h ;data port
SIORDF equ 00000001b ;-RX Data register full
SIOTDE equ 10000000b ;-TX Data register empty
;----------------
;ASCII Characters
;----------------
SOH equ 1 ;Start of 128-byte block
;STX equ 2 ;Start of 1K-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
CAN equ 18h ;Xmodem Cancel
EOF equ 1Ah ;^Z end of XMODEM.CFG file
QUOTE equ 2Ch ;single quote
SELCRC equ 'C' ;selects CRC mode at initiation
;*********************
;* Beginning of Code *
;*********************
org USAREA ;normal place for CP/M programs
;*******************************
;Run-time stack is in the COMBUF
;(which is just below USAREA)
;*******************************
LSTACK:
;-------------------------------------------------------
;Set the top address of the buffer, in case we don't get
;a /K option. This is actually INIT code, and is placed
;here so that the stack can overwrite it harmlessly.
;-------------------------------------------------------
lda BDOSA+1 ;msb of start of BDOS
dcr a ;last allowed page
sta BUFMAX
;--------------------------------------------------------
;Initialize, using code that gets wiped out by the BUFFER
;--------------------------------------------------------
jmp INIT
;***Function*********************************
;Send a CP/M file in Xmodem format
;On Entry:
; Disk file is open for reading
; Initial NAK or SELRC has already been sent
; FCB is valid
; BUFCNT = 0
; XBLOCK = 0
; BUFMAX = Max address in BUFFER/256
; PACCNT = 0FFh if quiet mode, 0 otherwise
; RTORET is set to WAITNK
;********************************************
TXFILE:
;-------------------------------------------------------
;Do initial buffer fill while the disk is still spinning
;-------------------------------------------------------
call FILBUF
;------------------------------------------------------
;Get the transfer error checking mode from the receiver
;------------------------------------------------------
call GTMODE ;wait for NAK or SELCRC to
;..determine cksum or CRC mode
lxi h,ACKERR ;Timeout return address
shld RTORET ;..for RXBYTE
;---------------------------------------------------------
;Transmit the entire file in 128-byte blocks. Whenever the
;buffer is empty, refill it from disk and test for EOF.
;---------------------------------------------------------
TXLOOP: lhld BUFCNT ;block count in buffer
mov a,h ;16-bit test for 0
ora l
cz FILBUF ;Empty? go read disk
;Returns BLKPTR=BUFFER
;..and hl=BUFCNT=blocks in buf
;Exit directly if no more data
dcx h ;one fewer block in
shld BUFCNT ;..the buffer
lhld XBLOCK ;inc 16-bit Xmodem block #
inx h
shld XBLOCK
;---------------------------------------
;Send the block header: SOH, 8-bit Block
;number, Complimented 8-bit block number
;(Block send retry re-entry point)
;On Entry:
; XBLOCK=16-bit Xmodem block number
;---------------------------------------
TXRTRY: mvi a,SOH ;SOH first
call TXBYTE
lda XBLOCK ;8-bit block number
call TXBYTE ;(preserves a)
cma ;complimented block
call TXBYTE
;-------------------------------------------
;Send the next BLKSIZ-byte block from BUFFER
;On Entry:
; BLKPTR=DMA address
;On Exit:
; Data checksum is in c
; 16-bit data CRC is in de
; hl=BLKPTR+128
;-------------------------------------------
lxi b,BLKSIZ*256+0 ;b=bytes/block,
;...clear checksum in c
mov d,c ;clear CRC in de
mov e,c
lhld BLKPTR ;(hl) = data in BUFFER
TXBLUP: mov a,m ;Get a data byte
call TXBYTE ;Send it
;------------------------------------------------------
;(Inline for speed)
;Update the 16-bit CRC and 8-bit checksum with one more
;data byte. Speed matters here. For speed, this code
;assumes that the CRC table is on a page boundary, and
;that the table is split, with the high bytes in the
;first half and the low bytes in the second half.
; a has the byte just transmitted
; c has checksum so far
; de has the CRC so far
;------------------------------------------------------
push h ;(12)
mvi h,CRCTAB/256 ;(7)CRC table addr high byte
xra d ;(4)compute lookup address
mov l,a ;(4)low byte of lookup
xra d ;(4)recover original byte
add c ;(4)update checksum too
mov c,a ;(4)
mov a,m ;(7)compute new CRC high byte
xra e ;(4)using the table
mov d,a ;(4)
inr h ;(4)low bytes are in the
mov e,m ;(7)..other half of the table
pop h ;(10)
;-----------------------------------------------------------
;CRC is done. Next byte, unless we have transmitted them all
;-----------------------------------------------------------
inx h ;Next byte
dcr b
jnz TXBLUP ;loop through block
;--------------------------------------------
;Send checksum or 16-bit CRC, based on CRCFLG
; c= 8-bit checksum
; de = CRC16
; CRCFLG <>0 if CRC mode enabled
; hl=BLKPTR+128
;--------------------------------------------
lda CRCFLG ;Checksum or CRC?
ora a ;clear Z if CRCFLG
jz TXCKSM ;jump to send checksum
mov a,d
call TXBYTE ;send byte in a
mov c,e ;now the 2nd CRC byte
TXCKSM: mov a,c ;a=cksum or CRC 2nd byte
call TXBYTE ;send byte in a
;-------------------------------------------
;Wait for the ACK. If none arrives by the
;end of the timeout, or if a NAK is received
;instead of an ACK, then resend the block.
; hl=BLKPTR+128
;-------------------------------------------
call GETACK ;Wait for the ACK
;a=0, Z set if ACK
jnz TXRTRY ;NZ: timeout or NAK
sta ERRCNT ;a=0
shld BLKPTR ;next block in the buffer
;----------------------------------------------------
;Ack received. Print pacifier, and go send next block
;----------------------------------------------------
call PACOK
jmp TXLOOP
;***Function*******************************
;Receive Xmodem file & save it to disk
;-->Entry is at RXFILE<--
;On Entry:
; Carry is clear
; a=(NAKCHR) (which is NAK or SELCRC),
; to start the transfer
; FCB is valid
; Disk file has been created and is open
; XBLOCK=0
; EOFLAG = 00h
; FCB is valid
; BUFPTR = BUFFER
; BUFMAX = Max address in BUFFER/256
; XBLOCK = 0
; PACCNT = 0FFh if quiet mode, 0 otherwise
; RTORET is set to RXSERR
;******************************************
;-------------------------------------------------------
;Loop to receive Xmodem blocks until we get an EOT.
;Flush the buffer whenever it becomes full.
; XBLOCK=16-bit block number
;EOFLAG = 00h if no EOT yet
; = FFh if EOT received and it has been ACKed
;-------------------------------------------------------
RXLOOP: lda EOFLAG ;msb means EOT's been ACKed
add a ;carry means EOT's been ACKed
mvi a,ACK
;=====================================
;Routine Entry
;(a=NAKCHR and Carry cleared on entry)
;=====================================
RXFILE: push psw ;remember carry
cnc TXBYTE ;ack a block
pop psw ;carry means EOT's been ACKed
cnc RXBLK ;go receive 1 block,
;unless we got the EOT
;On return, carry means BUFFER
;..is full or EOT or CAN
cc WFLUSH ;Flush if needed
;Carry set if none flushed
jnc RXLOOP
;WFLUSH returned with carry set: success
call CILPRT ;Success message
db 'O','K'+80h
;Fall into RXEND to close the file and report
;***Exit*****************************************
;Close the file. If no blocks were written, then
;erase the empty file; otherwise, print the
;number of blocks received. Then return to CP/M.
;On Entry:
; XBLOCK = block count
;************************************************
RXEND: mvi c,BCLOSE ;CP/M CLOSE FILE function
call FCBDOS ;sets de=FCB
jm FCLERR ;-1 meant close error
lda XMODE ;Did we ever write?
add a ;msb means yes
jc RRXCNT
call FERASE ;n: erase empty file
jm EFEXIT ;BDOS returns FFh if failed
RRXCNT: call CILPRT
db 'Received',' '+80h
;Fall into REPCNT
;***Exit*******************************************
;Report 16-bit block count, and then return to CP/M
;On Entry:
; XBLOCK = block count
; either 'sent ' or 'received ' already printed
;**************************************************
REPCNT: lhld XBLOCK
call PDEC16 ;print block count in decimal
call MSGXIT ;print & return to CP/M
db ' block','s'+80h
;***Subroutine*****************************************
;Get an ACK from the receiver. If we get a NAK, then
;print the NAK pacifier on the console. Opportunity for
;user to abourt (with ^C) if timeout waiting for ACK.
;On Entry:
; RTORET = ACKERR (RXBYTE eror return address)
;On Exit:
; Z set and a=0 if ACK received
; Abort if CAN received
; Z clear if NAK received, bogus, or timeout
; If too many errors, abort
;Good ack: all other registers preserved
;Bad ack: trashes hl
;******************************************************
;-----------------------------------------
;Get a received byte, or timeout. Return
;with Z set and carry clear if it's an ACK
;-----------------------------------------
GETACK: mvi a,ACKTO*2 ;ACK-wait timeout value
call RXBYTE ;go get a character
;timeout will "return"
;..to ACKERR
xri ACK ;Did we get an ACK?
rz ;Y: Z set, a=0
;---------------------------------------------
;Is the receiver trying to cancel the session?
;---------------------------------------------
xri CAN xor ACK ;Xmodem Cancel chr
jz RXCAN ;cancel with a=0
;Fall into ACKERR
;***Subroutine***********************************
;If anything else (including a NAK, or an RXBYTE
;timeout, which jumps to ACKERR), print an error-
;pacifier and return with Z cleared, unless the
;user aborts. If too many errors, then abort.
;On Exit:
; Z clear
;Trashes a,bc,hl
;************************************************
ACKERR: call PACERR ;opportunity to abort,
;pacifier if allowed
lxi h,ERRCNT ;bump error count
inr m
mov a,m ;too many errors?
cpi ERRLIM
rc ;N: Z cleared for fail
;----Exit------------------------------
;Abort waiting for ACK: Too many errors
;--------------------------------------
call CANABT ;Cancel and abort
db (ERRLIM/10)+'0' ;too many bad ACKs
db (ERRLIM-((ERRLIM/10)*10))+'0'
db ' ACK error','s'+80h
;***Subroutine***********************************
;Print Message at hl
;Follow all CR's with LF's
;On Entry:
; hl points to the message
; The last message chr has its msb set.
;On Exit:
; hl points to the next address past the string
; Z cleared
;Trashes psw,c
;************************************************
HLPRNT: mov a,m ;get a character
ani 7Fh ;strip end marker
HLPRT1: call PRINTA ;returns chr in c
mvi a,CR ;just print a CR?
cmp c ;c has printed chr
mvi a,LF
jz HLPRT1 ;y: follow with LF
mov a,m ;was that the last chr?
ora a ;msb was set if so
inx h ;Next byte
jp HLPRNT ;Do all bytes of msg
ret
;***Table*******************************
;CRC Calculation Table
;strategically placed on a page boundary
;***************************************
TPAGER equ $ ;Force page alignment
org (TPAGER+255) and 0FF00h
CRCTAB:
;high bytes
db 000h,010h,020h,030h,040h,050h,060h,070h
db 081h,091h,0A1h,0B1h,0C1h,0D1h,0E1h,0F1h
db 012h,002h,032h,022h,052h,042h,072h,062h
db 093h,083h,0B3h,0A3h,0D3h,0C3h,0F3h,0E3h
db 024h,034h,004h,014h,064h,074h,044h,054h
db 0A5h,0B5h,085h,095h,0E5h,0F5h,0C5h,0D5h
db 036h,026h,016h,006h,076h,066h,056h,046h
db 0B7h,0A7h,097h,087h,0F7h,0E7h,0D7h,0C7h
db 048h,058h,068h,078h,008h,018h,028h,038h
db 0C9h,0D9h,0E9h,0F9h,089h,099h,0A9h,0B9h
db 05Ah,04Ah,07Ah,06Ah,01Ah,00Ah,03Ah,02Ah
db 0DBh,0CBh,0FBh,0EBh,09Bh,08Bh,0BBh,0ABh
db 06Ch,07Ch,04Ch,05Ch,02Ch,03Ch,00Ch,01Ch
db 0EDh,0FDh,0CDh,0DDh,0ADh,0BDh,08Dh,09Dh
db 07Eh,06Eh,05Eh,04Eh,03Eh,02Eh,01Eh,00Eh
db 0FFh,0EFh,0DFh,0CFh,0BFh,0AFh,09Fh,08Fh
db 091h,081h,0B1h,0A1h,0D1h,0C1h,0F1h,0E1h
db 010h,000h,030h,020h,050h,040h,070h,060h
db 083h,093h,0A3h,0B3h,0C3h,0D3h,0E3h,0F3h
db 002h,012h,022h,032h,042h,052h,062h,072h
db 0B5h,0A5h,095h,085h,0F5h,0E5h,0D5h,0C5h
db 034h,024h,014h,004h,074h,064h,054h,044h
db 0A7h,0B7h,087h,097h,0E7h,0F7h,0C7h,0D7h
db 026h,036h,006h,016h,066h,076h,046h,056h
db 0D9h,0C9h,0F9h,0E9h,099h,089h,0B9h,0A9h
db 058h,048h,078h,068h,018h,008h,038h,028h
db 0CBh,0DBh,0EBh,0FBh,08Bh,09Bh,0ABh,0BBh
db 04Ah,05Ah,06Ah,07Ah,00Ah,01Ah,02Ah,03Ah
db 0FDh,0EDh,0DDh,0CDh,0BDh,0ADh,09Dh,08Dh
db 07Ch,06Ch,05Ch,04Ch,03Ch,02Ch,01Ch,00Ch
db 0EFh,0FFh,0CFh,0DFh,0AFh,0BFh,08Fh,09Fh
db 06Eh,07Eh,04Eh,05Eh,02Eh,03Eh,00Eh,01Eh
;Low Bytes
db 000h,021h,042h,063h,084h,0A5h,0C6h,0E7h
db 008h,029h,04Ah,06Bh,08Ch,0ADh,0CEh,0EFh
db 031h,010h,073h,052h,0B5h,094h,0F7h,0D6h
db 039h,018h,07Bh,05Ah,0BDh,09Ch,0FFh,0DEh
db 062h,043h,020h,001h,0E6h,0C7h,0A4h,085h
db 06Ah,04Bh,028h,009h,0EEh,0CFh,0ACh,08Dh
db 053h,072h,011h,030h,0D7h,0F6h,095h,0B4h
db 05Bh,07Ah,019h,038h,0DFh,0FEh,09Dh,0BCh
db 0C4h,0E5h,086h,0A7h,040h,061h,002h,023h
db 0CCh,0EDh,08Eh,0AFh,048h,069h,00Ah,02Bh
db 0F5h,0D4h,0B7h,096h,071h,050h,033h,012h
db 0FDh,0DCh,0BFh,09Eh,079h,058h,03Bh,01Ah
db 0A6h,087h,0E4h,0C5h,022h,003h,060h,041h
db 0AEh,08Fh,0ECh,0CDh,02Ah,00Bh,068h,049h
db 097h,0B6h,0D5h,0F4h,013h,032h,051h,070h
db 09Fh,0BEh,0DDh,0FCh,01Bh,03Ah,059h,078h
db 088h,0A9h,0CAh,0EBh,00Ch,02Dh,04Eh,06Fh
db 080h,0A1h,0C2h,0E3h,004h,025h,046h,067h
db 0B9h,098h,0FBh,0DAh,03Dh,01Ch,07Fh,05Eh
db 0B1h,090h,0F3h,0D2h,035h,014h,077h,056h
db 0EAh,0CBh,0A8h,089h,06Eh,04Fh,02Ch,00Dh
db 0E2h,0C3h,0A0h,081h,066h,047h,024h,005h
db 0DBh,0FAh,099h,0B8h,05Fh,07Eh,01Dh,03Ch
db 0D3h,0F2h,091h,0B0h,057h,076h,015h,034h
db 04Ch,06Dh,00Eh,02Fh,0C8h,0E9h,08Ah,0ABh
db 044h,065h,006h,027h,0C0h,0E1h,082h,0A3h
db 07Dh,05Ch,03Fh,01Eh,0F9h,0D8h,0BBh,09Ah
db 075h,054h,037h,016h,0F1h,0D0h,0B3h,092h
db 02Eh,00Fh,06Ch,04Dh,0AAh,08Bh,0E8h,0C9h
db 026h,007h,064h,045h,0A2h,083h,0E0h,0C1h
db 01Fh,03Eh,05Dh,07Ch,09Bh,0BAh,0D9h,0F8h
db 017h,036h,055h,074h,093h,0B2h,0D1h,0F0h
;***Subroutine******************************************
;Receive & validate a block, and see if we got an EOT
; XBLOCK=16-bit block number of the last block received
;On Entry:
; BLKPTR = address for next block
; BUFCNT = number of blocks in the buffer
; XMODE<1> = 1 iff called from within FLUSH
; (meaning CCTRLC should not try to flush)
; BUFMAX = Max address in BUFFER/256
;On Exit (valid block):
; Carry set if buffer full
; BLKPTR points to next space
; BUFCNT incremented
;On Exit, EOF received
; Carry set
; EOFLAG = FFh
; ACK has NOT been sent
;Trashes all registers
;*******************************************************
RXBLK: xra a
sta ERRCNT ;Clear error count
;---------------------------------------
;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
;------------------------------------------------
mvi a,NAK ;we have received
sta NAKCHR ;..at least one SOH
sta RX1ST ;we've received now
call RXBYT1 ;Get block number
mov d,a ;Save block number
lda XBLOCK ;8-bit previous block number
mov e,a ;..for later
call RXBYT1 ;complimented block number
cma ;(4)compliment to compare
cmp d ;(4)
jnz PURGE ;(10)No match: error
;---------------------------------------------------
;Calculate and remember the difference between this
;block number and the previous block's block number.
;(We calculate this here because we have the time.)
;---------------------------------------------------
sub e ;(4)calc the difference
;0 means same block
;1 means next block
sta RXBDIF ;(7)Save block number diff
;-----------------------------------------------------
;Loop to receive BLKSIZ bytes and store them in the
;next slot in the buffer, computing both the checksum
;and the CRC along the way. Throughout the RXCHR loop:
; b is the byte counter
; c accumulates the checksum
; de accumulates the CRC
; hl is the buffer memory pointer
;----------------------------------------------------
lxi b,BLKSIZ*256+0 ;(10)b=bytes, c=0 checksum
mov d,c ;(4)Clear CRC too
mov e,c ;(4)
lhld BLKPTR ;(16)next block in the buffer
RXCHR: call RXBYT1 ;(17+118)Get one byte of data
mov m,a ;(7)Store byte in buffer
;-------------------------------------------------------------
;(Inline for speed: this is the critical path when receiving.)
;Update the 16-bit CRC and 8-bit checksum with one more data
;byte. For speed, this code assumes that the CRC table is on
;a page boundary, and that the table is split, with the high
;bytes in the first half and the low bytes in the second half.
; a has the newly received byte
; c has checksum so far
; de has the CRC so far
;(This loop uses 238 8080 cycles IN-to-IN for direct I/O. with
;a 4 MHz Z80, this will require about 60 uS per byte. 115.2K
;baud sends a byte every 86.8 uS, so we have enough headroom.)
;-------------------------------------------------------------
push h ;(12)
mvi h,CRCTAB/256 ;(7)CRC table addr high byte
xra d ;(4)compute lookup address
mov l,a ;(4)low byte of lookup
xra d ;(4)recover original byte
add c ;(4)update checksum too
mov c,a ;(4)
mov a,m ;(7)compute new CRC high byte
xra e ;(4)..using the table
mov d,a ;(4)
inr h ;(4)low bytes are in the
mov e,m ;(7)..other half of the table
pop h ;(10)
;--------------------------------------------------------------
;Next byte, unless we have received all the data for this block
;--------------------------------------------------------------
inx h ;(6)next byte
dcr b ;(4)
jnz RXCHR ;(10)
;-------------------------------------------------------
;We've received all the block's data bytes. Now verify
;either the checksum in c or CRC in de, based on CRCFLG.
; hl=next buffer address
;-------------------------------------------------------
lda CRCFLG ;CRC mode?
ora a ;0 means cksum
jz RXCKSM
call RXBYT1 ;Get 1st byte of CRC
cmp d ;test the 1st CRC byte
jnz RXCERR ;fail: try again, but
;..first, purge 2nd CRC
mov c,e ;put 2nd CRC byte in c
RXCKSM: call RXBYT1 ;2nd CRC byte or cksum
cmp c ;Does it match?
jnz RXSERR ;No: error
;------------------------------------------------------
;Got an error-free block. See if it has the block
;number we expected, based on the prior block's number.
; hl=next buffer address
; RXBDIF = this block's block number minus the
; previous block's block number.
; Carry is clear here.
;------------------------------------------------------
lda RXBDIF ;difference between this
;block's number & the
;prior block's number
dcr a
jnz BLKORD ;Nonsequential? (Carry clear)
;-----------------------------------------
;Correct block received. Bump pointers and
;counters, and see if the buffer is full.
; hl=next buffer address
;-----------------------------------------
shld BLKPTR ;next slot in BUFFER
mov d,h ;remember for full-test
lhld BUFCNT ;bump buffer block count
inx h
shld BUFCNT
lhld XBLOCK ;inc 16-bit Xmodem block #
inx h
shld XBLOCK
;Print good-block pacifier on the console, if enabled
call PACOK
;Test for enough room in the buffer for another
;block, and return with carry set if not.
;d = (BLKPTR)/256, the page address for the next block
lda BUFMAX
cmp d ;carry: not room for another
ret
;-------------------------------------------------------
;Non-sequential block received without a checksum or CRC
;error. a=FFh if this block has the same block number as
;the previous block (and should be ignored). Otherwise
;abort because blocks hves been irretrievably lost.
;Carry is clear on entry
;-------------------------------------------------------
BLKORD: inr a ;was it FFh?
rz ;y: ignore repeated block
;..return with carry clear
;---Exit----------------
;Blocks are out of order
;-----------------------
call CANABT ;cancel and abort
db 'Lost block','s'+80h ;out of sequence
;-----------------------------------------------------------
;Received something besides an SOH. If it was an EOT or a
;CAN then send an ACK, set EOFLAG=FFh, and return with carry
;set. If it was a CAN then say so and abort. Ignore a bogus
;chr on the very 1st byte ever received, in case the UART
;receiver had a garbage chr when we started.
;-----------------------------------------------------------
NOTSOH: lxi h,RX1ST ;First byte received?
inr m
dcr m
jz PURGE ;y: just ignore bogus chrs
cpi CAN ;Cancel?
jnz CHKEOT
call CILPRT
db 'Sender cancele','d'+80h
mvi a,EOT ;act like we got an EOT
;..so we will FLUSH
CHKEOT: cpi EOT ;End of Xmodem file?
jnz PURGE ;n: bogus
call TXACK ;returns a=ACK
sui ACK+1 ;a=FF, carry set
sta EOFLAG ;Remember EOT
ret
;***Subroutine****************************************
;Eat incoming bytes (up to 256 received bytes) until
;the line is quiet for 1 second. (RXBYT1 timeout will
;jump to the address in RTORET, which is set up during
;initialization.)
;Trashes b
;*****************************************************
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
;---Exit-------------------------------------------
;The transmitter is babbling, unable to synchronize
;with the incoming data stream. Abort with message.
;--------------------------------------------------
call CANABT ;Cancel and abort
db 'Can',QUOTE,',t syn','c'+80h ;can't find SOH
;--------------------------------
;Error on 1st CRC byte. Flush 2nd
;CRC byte, and indicate an error.
;--------------------------------
RXCERR: call RXBYT1 ;get and chuck 2nd CRC byte
;---------------------------------------------------------
;Send a NAK to indicate receive error. NAKCHR (below) gets
;modified. If we are waiting to start and we are in CRC
;mode (NAKCHR=SELCRC), then send SELCRC instead of NAK.
;---------------------------------------------------------
RXSERR: call PACERR ;opportunity to abort,
;pacifier if allowed
;trashes bc,hl
mvi a,NAK ;This NAK gets modified
NAKCHR equ $-1 ;..if CRC's
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
;---Exit------------------------
;Too many errors. Abort cleanly.
;-------------------------------
call CANABT ;Cancel and abort
db (ERRLIM/10)+'0' ;too many block retries
db (ERRLIM-((ERRLIM/10)*10))+'0'
db ' bad block','s'+80h
;***Subroutine**********************************************
;Write all data in BUFFER to disk
;Every WBPERX disk writes, stop to receive one Xmodem block,
;often enough that the sender does not time out. Put out.
;(Stop doing this once we receive an Xmodem EOT.) the new
;blocks in the beginning of the BUFFER, where it's already
;been cleared it out by writing to disk. This code assumes
;that disk records are the same size as Xmodem blocks, 12
;bytes each.
;On Entry:
; BUFCNT = count of blocks currently in BUFFER
; FCB describes the open file
; XMODE = 01 if no disk writes yet
; = 81 if disk writes have ever occured
;On Exit:
; Carry set if no blocks written
; BUFCNT=number of new blocks in buffer
; BLKPTR points to next free space in the buffer
; XMODE = 81h if disk writes have occured
;Trashes all registers
;***********************************************************
WFLUSH: lhld BUFCNT
mov a,h
ora l
stc
rz
mvi a,82h ;remember we are flushing
sta XMODE ;and that we wrote
xchg ;de has block count
lxi h,0 ;reset block count
shld BUFCNT
mvi h,BUFFER/256 ;reset BLKPTR
shld BLKPTR ;(BUFFER is page-aligned)
;-----------------------------------------------------
;Loop to write all blocks in the BUFFER to disk.
; b = WBPERX count-down of received xmodem blocks
; (to prevent timeout)
; de = number of BUFFER records to write to disk
; hl = pointer for data to write to disk
; BLKPTR = DMA address for new received Xmodem blocks
; BUFCNT counts new Xmodem received blocks
; XMODE = 82h (we will write, currently flushing)
;-----------------------------------------------------
WFLP0: mvi b,WBPERX and 0FFh ;interleave counter
;Check for ^C, with XMODE=82, indicating that the ^C
;occured during flush (meaning that the abort routine
;should not attempt to flush the buffer).
call CCTRLC ;User abort? trashes c
WFLOOP: mov a,d ;de=write record cnt
ora e ;any more to send?
jnz WFLP1 ;y:keep flushing
mvi a,81h ;no longer flushing
sta XMODE
ret ;done, carry clear
WFLP1:
;Write a disk record at hl to disk
push d ;record count
mov d,h ;de gets DMA address
mov e,l
mvi c,BSTDMA ;CP/M SET DMA function
call GOBDOS ;de = DMA address
mvi c,BWRITE ;Write from buf to disk
call FCBDOS ;returns Z for success
lxi d,BLKSIZ ;de=block size
dad d ;(hl)=next block data
pop d ;record count
jnz WFAIL ;Oops, write error
dcx d ;next block
;----------------------------------------------------
;See if we need to do a block read. Read a block
;if b has counted down to 0.
;Within RXBLK
; BUFPTR = DMA address for new received Xmodem blocks
; BUFCNT counts new received Xmodem blocks
; EOFLAG <> 0 if we've received the Xmodem EOT
; XMODE = 82h (writes occured, currently flushing)
;----------------------------------------------------
dcr b
jnz WFLOOP
lda EOFLAG ;has sender finished?
ora a
jnz WFLP0
call TXACK ;ACK previous block
push h
push d
push b
call RXBLK ;get 1 more block
;(will never be full)
pop b
pop d
pop h
jmp WFLP0 ;until all blocks sent
;***Subroutine**************************************
;Read more blocks from the disk and put them in the
;buffer until it is full or there are no more blocks
;On Entry:
; BUFCNT = 0
; BUFMAX = Max address in BUFFER/256
; Buffer start address = BUFFER
;On Exit:
; BLKPTR = buffer start address
; BUFCNT=number of 128-byte blocks in the buffer
; hl=(BUFCNT)
; EOFLAG set if EOF encountered
; direct exit to TXEOF if no more data
;Trashes all registers
;***************************************************
;--------------------------------------------------------
;BUFFER is empty: read 128-byte logical disk sectors into
;the buffer until EOF or the buffer is full (up to BDOS)
;--------------------------------------------------------
FILBUF: lda EOFLAG ;already seen the EOF?
ora a
jnz TXEOF ;y: no more data.
lxi h,BUFFER
shld BLKPTR ;reset pointer for exit
xchg ;de=address in BUFFER
FBLOOP: mvi c,BSTDMA ;de=CP/M DMA address
call GOBDOS ;Trashes no registers
xchg ;pointer to hl, free de
mvi c,BREAD ;Logical sector into BUFFER
call FCBDOS ;sets de=FCB, Z for success
jnz FBEOF ;EOF from CP/M?: no more data
lxi d,SECSIZ ;logical sector size
dad d ;next logical sector's address
xchg ;..into de
lhld BUFCNT ;count blocks in buffer. Note:
inx h ;this assumes blocks are the
shld BUFCNT ;same size as logical sectors.
;Test to see if there's enough room in the buffer for
;another logical disk sector, and return if not.
lda BUFMAX ;Page of last allowed address
cmp d ;carry set if not room
;..for another block
jnc FBLOOP ;go until all space used
ret ;with hl=BUFCNT
;-------------------------------------------
;We got an EOF from CP/M. If we read 0
;logical sectors, then send the EOF and end.
;On Entry:
; a<>0
; count-down on stack
; BUFCNT = # of blocks read from disk
;On Exit:
; hl=(BUFCNT)
; EOFLAG <> 0
;-------------------------------------------
FBEOF: sta EOFLAG ;Set EOF flag
lhld BUFCNT ;zero blocks?
mov a,h
ora l
rnz ;n: ret with hl=BUFCNT
;Fall into TXEOF to end transmission
;***Exit************************************************
;File send completed. Send EOT'S until we get an ACK
;Then print happy message, report block count anbd exit.
;On Entry:
; XBLOCK=16-bit block number of the last block sent
;*******************************************************
TXEOF: mvi a,EOT ;Send an EOT
call TXBYTE
call GETACK ;Wait for an ACK
jnz TXEOF ;Loop until we get an ACK
call CILPRT ;report success
db 'OK',CR
db 'Sent',' '+80h
jmp REPCNT ;print block count, goto CP/M
;***Subroutine********************************
;Send ACK
; returns a=ACK
; flags trashed
; All other registers preserved
;*********************************************
TXACK: mvi a,ACK
;Fall into TXBYTE
;***Subroutine********************************
;Send a to the transfer port, based on XPORT
;and the assembly options.
;This routine gets modified during INIT to
;connect the correct transmit routine.
;On Entry:
; a = byte to send
; TXROUT has been modified by initialization,
; based on the default or a /X option
;On Exit:
; All registers except flags preserved
;*********************************************
TXBYTE: push b
mov c,a ;chr to c
TXROUT: jmp EXIT ;This gets modified with
;..the routine address
;***Subroutine************************
;Receive a byte, with 1-second timeout
;On Entry:
; RTORET = error return address
;On Exit:
; exit to RTORET 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 - from the CON or
;RDR device or a direct I/O port.The RDR port may or may
;not be enhanced to return with Z set if no chr is waiting.
;
;On Entry:
; RXROUT has been patched by the initialization code,
; subject to the default port or as set by the /X option
; a = timeout value in half-seconds
; RTORET = error return address
;On Exit:
; exit to RTORET if timeout
; a = received byte if no timeout
;(118 8080 cycles for direct I/O)
;**********************************************************
RXBYTE: sta TIMRH ;(13)Timer high byte
push h ;(12)
lhld TIMRLD ;(16)start timeout timer
RXROUT: jmp EXIT ;(10)This gets modified with
;..the routine address
;***Subroutine*********************
;Print hl in decimal on the console
;with leading zeros suppressed
;Trashes all registers
;**********************************
PDEC16: lxi d,00FFh ;d: Suppress leading 0's
;e: a handy -1
lxi b,-10000
call DECDIG
lxi b,-1000
call DECDIG
lxi b,-100 ;sets b to FF
call DECDIG
mvi c,(-10) and 0FFh ;b=FF already
call DECDIG
mov c,e ;bc=-1
mov d,e ;always print final 0
;Fall into DECDIG to print the 1's digit
;---Local Subroutine------------------------------
;Divide hl by power of 10 in bc and print
;result, unless it's a leading 0.
;On Entry:
; hl=Dividend
; bc=divisor (a negative power of 10)
; d=0 if all prior digits were 0, 0FFh otherwise
; e=-1
;On Exit:
; Quotent is printed, unless it's a leading 0
; hl=remainder
; d=0 iff this and all prior digits are 0
; d=-1 if this digit was not 0
;Trashes psw,c
;-------------------------------------------------
DECDIG: mov a,e ;will go 1 too many times
push d ;leading 0 state & -1
DIGLP: mov d,h ;de gets prev value
mov e,l
inr a
dad b ;subtract power of 10
jc DIGLP
xchg ;hl has remainder
pop d ;leading 0 state & -1
cmp d ;Leading 0 to suppress?
rz ;y: done
mov d,e ;FF: no more leading 0's
adi '0' ;make digit ASCII
;Fall into PRINTA ;(trashes c)
;***Subroutine******************
;Print character in a on console
;On Exit:
; c = printed character
;Trashes psw
;*******************************
PRINTA: mov c,a ;value to c for PRINTC
;Fall into PRINTC
;***Subroutine******************
;Print character in c on console
;Trashes psw
;*******************************
PRINTC: mvi a,CONOUT
jmp GOBIOS
;***Subroutine********************************
;Print error pacifier on the console unless
;disabled, giving the user a chance to abort
;On Entry:
; PACCNT =FFh to disable pacifier printing
; otherwise, PACCNT = column count-down
;On Exit:
; PACCNT decremented, and reset to PACCNT,
; unless it was FFh
;Trashes psw,bc,hl
;*********************************************
PACERR: mvi b,PACNAK
db 21h ;'LXI H' opcode skips 2 bytes
;Hop into PACIFY
;***Subroutine************************************
;Print success pacifier on the console unless
;disabled, giving the user a chance to abort
;On Entry:
; PACCNT =FFh to disable pacifier printing
; otherwise, PACCNT = column count-down
;On Exit:
; PACCNT decremented, and reset to PACCNT,
; unless it was FFh
;Trashes psw,bc,h
;*************************************************
PACOK: mvi b,PACACK
;Fall into PACIFY
;***Subroutine************************************
;Print pacifier on the console unless disabled,
;giving the user a chance to abort
;Print a CR/LF at the end of every PACLIN columns.
;On Entry:
; PACCNT =FFh to disable pacifier printing
; otherwise, PACCNT = column count-down
; b=pacify character
;On Exit:
; PACCNT decremented, and reset to PACCNT,
; unless it was FFh
;Trashes psw,c,hl
;*************************************************
PACIFY: lxi h,PACCNT
mov a,m ;FF means quiet mode
inr a
rz
dcr m
jp PCFY1
call ILPRNT ;new line, trashes c
db CR+80h ;ILPRNT adds the LF
mvi m,PACLIN
PCFY1: mov c,b ;recover pacifier
call PRINTC ;..and print it
;Fall into CCTRLC for a user-abort opportunity
;***Subroutine*************************************
;Check for Control-C on the console, and quit if so
;On Exit:
; Z set if no chr was waiting
; Z clear if anything but ^C was waiting
; XMODE<1>=1 iff called from within FLUSH
; (meaning CANABT should not flush)
;Trashes a
;**************************************************
CCTRLC: mvi a,CONST ;anything on console?
call GOBIOS ;(about 200 cycles)
ora a ;Z means no chr waiting
rz
;Chr waiting: fall into GETCON to take a look
;***Subroutine*********************************
;Get console character, abort if it's control-C
;On Exit:
; chr in a
; Z cleared
; XMODE<1>=1 iff called from within FLUSH
; (meaning CANABT should not flush)
;Trashes a
;**********************************************
GETCON: mvi a,CONIN ;read the typed chr
call GOBIOS
cpi CTRLC
rnz ;ignore everything else
;---Exit---
;User abort
;----------
call CANABT
db '^','C'+80h ;User typed ^C
;***Subroutine*******************************
;Erase file at FCB
;On Exit:
; de=FCB
; Flags set according to how BDOS returned a
; (M flag set if error)
;Trashes psw,c
;********************************************
FERASE: mvi c,BDELET
;Fall into FCBDOS
;***Subroutine*******************************
;Call BDOS with de=FCB
;On Exit:
; de=FCB
; Flags set according to how BDOS returned a
;********************************************
FCBDOS: lxi d,FCB
;Fall into GOBDOS
;***Subroutine*********************************
;Call BDOS while preserving all regs except psw
;Flags set according to how BDOS returned a
;Generally, the result is negative if there
;was an error. Often, Z set means OK too.
;**********************************************
GOBDOS: push h
push d
push b
call BDOS
pop b
pop d
pop h
ora a ;test response
ret
;***Subroutine*********************************************
;Get the error-checking mode: Wait for the initial NAK or
;SELCRC from the receiver. (NAK means we use checksums, and
;SELCRC means we use CRC-16.) Ignore all other characters,
;with a long timeout. Abort if user types Control-C.
;
;This subroutine is not in INIT because it gets called
;after FILBUF has filled the BUFFER and wiped out INIT.
;
;On Entry:
; RTORET is set to WAITNK
; PACCNT=FFh if quiet mode or using console for transfers
; (so don't print messages on console)
;On Succesful Exit:
; CRCFLG = 0 if NAK received
; CRCFLG <> 0 if SELCRC received
; Message printed if not quiet mode
;Trashes psw,bc
;**********************************************************
GTMODE: mvi b,NAKTO ;Long timeout
lda PACCNT ;for quiet mode test
mov c,a
WAITNK: dcr b ;Timeout?
jz NTOERR ;Y: abort
call RXBYT1 ;1-second timeout
xri NAK ;NAK for checksum?
sta CRCFLG ;0 for cksum, NZ otherwise
jz PCSNT ;yes:message, done
xri SELCRC xor NAK ;'C' for CRC?
jnz WAITNK ;No: Keep looking
inr c ;Quiet mode?
rz ;y: no message
;Fall into PCRC
;***Subroutine******************
;Print 'with CRCs'
;On Entry:
; a = initial value for NAKCHR
; (only used when receiving)
;On Exit:
; Z flag cleared
;Trashes a,c
;*******************************
PCRC: sta NAKCHR ;set CRC initial ACK
;used only by RXFILE
call ILPRNT
db ' with CRC','s'+80h
ret
;***Subroutine**********************
;Print 'with checksums' unless quiet
;On Entry:
; c= FFh if quiet mode
;Trashes a,c
;***********************************
PCSNT: inr c ;quiet mode?
rz ;y: no message
;Fall into PCKSUM
;***Subroutine*********
;Print 'with checksums'
;On Exit:
; Z flag cleared
;Trashes a,c
;**********************
PCKSUM: call ILPRNT
db ' with checksum','s'+80h
ret
;***Subroutine**************************************
;Print CR, LF, then In-line Message
;The call to ILPRNT is followed by a message string.
;Follow CR's with LF's
;The last message chr has its msb set.
;Trashes psw,c
;***************************************************
CILPRT: call ILPRNT ;trashes c
db CR+80h ;ILPRNT adds the LF
;Fall into ILPRNT
;***Subroutine**************************************
;Print In-line Message
;The call to ILPRNT is followed by a message string.
;Follow CR's with LF's
;The last message chr has its msb set.
;On Exit:
; Z cleared
;Trashes psw,c
;***************************************************
ILPRNT: xthl ;Save hl, get msg addr
call HLPRNT
xthl ;Restore hl,
;..get return address
ret
;***Exit****************
;Timeout waiting for NAK
;***********************
NTOERR: call CANABT
db 'Init timeou','t'+80h
;***Exit*************
;Write fail from CP/M
;********************
WFAIL: call CANABT ;Cancel and abort
db 'Disk write fai','l'+80h ;CP/M error
;***Exit************************
;Transmitter timeout: the UART's
;CTS signal is probably not true.
;On Entry:
; a=0
;********************************
TXBTO: mov b,a ;b=0: don't close file
call ABORT
db 'UART Tx fai','l'+80h
;***Exit**************************************
;Receiver sent us a CAN, so abort with message
;a=0
;*********************************************
RXCAN: mov b,a ;0 for no flush
call ABORT
db 'by receive','r'+80h
;***Exit****************************************
;Cancel and Abort - if receiving, send a CAN.
;If sending, send an EOT. Then tidy up and quit.
;On Entry:
; top-of-stack = address of MSB-terminated
; XMODE = 00h if sending
; = X1h if receiving and not flushing
; = X2h if flushing when ^C came
; = 8Xh if we have ever written to disk
;***********************************************
CANABT: lda XMODE ;Sending or receiving?
mov b,a ;remember for ABORT
ora a ;0 means sending
mvi a,CAN
jnz CAB1
mvi a,EOT
CAB1: call TXBYTE
;Fall into ABORT to close file and report
;***Exit*****************************************
;Abort - close file if writing, erase it if no
;records were written to disk
;On Entry:
; b = XMODE (NZ means we need to close the file)
; top-of-stack = address of MSB-terminated
; XMODE = 0 for sending, <>0 for receiving
;************************************************
ABORT: call CILPRT
db 'ABORT:',' '+80h
pop h ;message to print
call HLPRNT ;print string at hl
mov a,b ;need to close the file?
ora a ;0 means sending
jz EXIT
;We were receiving an Xmodem file. Flush the
;BUFFER unless we were already flushing when
;we got the ^C from the user
; a = XMODE value <> 0
sta EOFLAG ;NZ to prevent interleaved Rx
;XMODE msb set if we were
;..writing when ^C came
ani 02h ;were we already flushing?
cz WFLUSH ;n: then flush the buffer
jmp RXEND
;***Exit***********
;Error closing file
;******************
FCLERR: call CMSGXT
db 'FILE CLOSE FAIL! May be corrup','t'+80h
;***Exit************************
;Report that the empty file has
;been erased, and return to CP/M
;*******************************
EFEXIT: call CMSGXT ;successful erase
db 'Empty file erase','d'+80h
;***Exit*******************************************
;Print CRLF, then $-terminated string following the
;call. Fix everything for CP/M, and return to CP/M
;**************************************************
CMSGXT: call ILPRNT ;trashes c
db CR+80h ;ILPRNT adds the LF
;Fall into MSGXIT
;***Exit******************************************
;Print $-terminated string following the call, fix
;everything for CP/M, and return to CP/M
;*************************************************
MSGXIT: pop h ;Get message address
call HLPRNT
;Fall into EXIT
;***Exit******************************
;Jump to CP/M's BDOS BRESET function.
; All exits go through here.
;*************************************
EXIT: mvi c,BRESET ;BDOS function 0
jmp BDOS ;go to CP/M
;---RX Byte Routine--------------------------
;Receive a transfer byte from CON
;On Entry:
; hl = timer low word
; TIMRH = timer high byte
; prior hl is on the stack
;RXBCON loop: 182+CRTIME cycles, and round up
;-->Entry is at RXBCON <---
;--------------------------------------------
CONTO: equ 50000/((182+CRTIME+9)/10)
RXCLUP:
dcx h ;(6)
mov a,l ;(4)
ora h ;(4)
cz RXTIMR ;(9) Timeout?
;-----
;Entry
;-----
RXBCON:
mvi a,CONST ;(7)get console status
call GOBIOS ;(18+120+CRTIME)
ora a ;(4)nz means chr ready
jz RXCLUP ;(10)Go get the chr
mvi a,CONIN ;get console chr
jmp CRDONE
;---RX Byte Routine--------------
;Receive a transfer byte from RDR
;On Entry:
; hl = timer low word
; TIMRH = timer high byte
; prior hl is on the stack
;--------------------------------
RXRDR: mvi a,READER ;BIOS routine offset
;Fall into CRDONE
;----------------------------------
;Get character from BIOS and return
;On Entry:
; a = BIOS routine offset
; prior hl is on the stack
;----------------------------------
CRDONE: pop h ;chuck timer
;Fall into GOBIOS
;***Subroutine*********************
;Go call a BIOS driver directly
;On Entry:
; c=value for BIOS routine, if any
; a = BIOS call address offset
;On Return:
; psw as BIOS left it
; all other regs preserved
;(120 cycles + BIOS time)
;**********************************
GOBIOS:
push h ;(12)
push d ;(12)
push b ;(12)
call DOBIOS ;(18+26+BIOS time)
pop b ;(10)
pop d ;(10)
pop h ;(10)
ret ;(10)
;---Local Subroutine---------------
;Go call a BIOS driver directly
;On Entry:
; c=value for BIOS routine, if any
; a = BIOS call address offset
;On Return:
; all regs as BIOS left them
;(26 cycles + BIOS time)
;----------------------------------
DOBIOS: lhld WBOOTA ;(16)get BIOS base address
mov l,a ;(4)a has jump vector
pchl ;(6) 'call' BIOS routine
;---RX Byte Routine----------------------------
;Receiver from enhanced BIOS RDR routine, which
;returns with Z set if no character is waiting,
;which allow us to have a timeout here.
;On Entry:
; hl = timer low word
; TIMRH = timer high byte
; prior hl is on the stack
;RXERDR loop: 178+CRTIME cycles, and round up
;--> Entry is at RXERDR <--
;----------------------------------------------
RDRTO: equ 50000/((178+CRTIME+9)/10)
RXERLP:
dcx h ;(6)
mov a,l ;(4)
ora h ;(4)
cz RXTIMR ;(9) Timeout?
RXERDR:
mvi a,READER ;(7)BIOS routine offset
call GOBIOS ;(18+129+BIOS time)
jz RXERLP ;(10)nz means chr ready
pop h ;(10)
ret ;(10)
;---RX Byte Routine-------------------------------
;Generic direct transfer port Input Routine - gets
;modified by INIT based on selected transfer port
;On Entry:
; hl = timer low word
; TIMRH = timer high byte
; prior hl is on the stack
;--> Entry is it RXDRCT <--
; WAITRX loop = 53 cycles. 0.5S / 53 uS = 9434
;-------------------------------------------------
DIRTO equ 9434
WAITRX:
dcx h ;(6)
mov a,l ;(4)
ora h ;(4)
cz RXTIMR ;(9) Timeout?
RXDRCT:
;The relative position of the following instructions must not
;change because MODIO assumes the positions of the bytes it
;modifies.
IMODFY: in SIOSTA ;(10+1)status port (modified)
ani SIORDF ;(7)test ready (clear carry) (modified)
jnz WAITRX ;(10)low when chr ready (modified)
pop h ;(10)here to match OMODFY
in SIODAT ;(10)data port (modified)
ret ;(10)
;---TX Byte Routine----------------------
;Transmit via direct I/O, with timeout
;the timeout value doesn't really matter:
;we just shouldn't hang forever here
;----------------------------------------
TXDRCT: push h
lxi h,0 ;about 1.7 second timeout
;..at 2 MHz
TXWAIT: dcx h ;(6)timeout?
mov a,h ;(4)
ora l ;(4)
jz TXBTO ;(10)y: abort
;The relative position of the following instructions must not
;change because MODIO assumes the positions of the bytes it
;modifies.
OMODFY: in SIOSTA ;(10+1)status port (modified)
ani SIOTDE ;(7)mask (modified)
jnz TXWAIT ;(10)may become jnz (modified)
;52 cycles = 26 uS per pass at 2 MHz
mov a,c ;recover chr
out SIODAT ;data port (modified)
pop h
pop b
ret
;---TX Byte Routine-------
;Transmit via CP/M PUN
;-------------------------
TXPUN: mvi a,PUNCH ;1:BIOS send c to punch
jmp TXCP
;---TX Byte Routine-------
;Transmit via CP/M CON
;-------------------------
TXCON: mvi a,CONOUT ;0:BIOS send c to console
;Fall into TXCP
;----------------------------
;Transmit via CP/M CON or PUN
;----------------------------
TXCP: call GOBIOS ;chr in c, routine in a
mov a,c ;restore character
pop b
ret
;---Local Subroutine-------------------------------
;Bump timer, test for abort every 1/2 sec
;On Entry:
; TIMRH = remaining timeout in 0.5 sec units
; RTORET = (modified) error return address
; 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 RTORET (which gets modified)
;Trashes psw
;--------------------------------------------------
RXTIMR: call CCTRLC ;user abort?
lxi h,TIMRH
dcr m ;bump timer high byte
lhld TIMRLD ;reload timer
rnz ;return unless timeout
;Timeout: fix stack, "return" from RXBYTE to address in RTORET
pop h ;chuck RXTIMR return address
pop h ;chuck original hl
pop h ;chuck RXBYTE return address
jmp PPTIMO ;This address gets modified, and is
RTORET equ $-2 ;..initialized for UART flush in INIT
;****************************************************
;Custom I/O Routines
;These all get modified in INT by the /I options. The
;Modifying code overwrites the error messages that
;are here as defaults. These routines are located
;just after CTABLE.The last one (CINIT, which is in
;the INIT code) must be within 256 bytes of CTABLE.
;****************************************************
;---Table----------------------
;Offsets to custom I/O routines
;Used only when INIT modifies
;the following routines.
;------------------------------
CTABLE: db CINIT-CTABLE
db CWDAT-(CTABLE+1)
Db CRSTAT-(CTABLE+2)
Db CRDAT-(CTABLE+3)
;---TX Byte Routine------------------------------
;Custom Output Subroutine
;On Entry:
; c=chr to send
;User routine should not trash c
;(Up to 12 bytes will be written at CWDAT by the
;/I1 option, overwriting the error message here.)
;------------------------------------------------
TXCUST:
CWDAT:
mvi b,0 ;Don't flush
call ABORT
db 'No /I','1'+80h
db nop
mov a,c ;restore registers
pop b
ret
;---RX Byte Routine--------------------------------
;Custom Receive Subroutine with timeout
;On Entry:
; hl = timer low word
; TIMRH = timer high byte
; prior hl is on the stack
;--> Entry is at RXCUST <---
;Assume WATCRX loop time is 80 cycles, and round up
;--------------------------------------------------
CUSTO: equ 50000/((80+EXTIME+9)/10)
WATCRX:
dcx h ;(6)
mov a,l ;(4)
ora h ;(4)
cz RXTIMR ;(11) Timeout?
RXCUST:
;Wait for data to be ready
;(Up to 12 bytes will be written at CRSTAT by the
;/I2 option, overwriting the error message here.)
CRSTAT:
mvi b,0 ;Don't flush
call ABORT
db 'No /I','2'+80h
db nop
jz WATCRX
pop h
;Get the received data byte
;(Up to 12 bytes will be written at CRDAT by /I3 here)
CRDAT: db nop
db nop
db nop
db nop
db nop
db nop
db nop
db nop
db nop
db nop
db nop
db nop
ret
;******************************************************
;RAM Variables and Storage, all initialized during load
;******************************************************
;------------------------------
;Xmodem file transfer variables
;------------------------------
RXBDIF: db 0 ;Received block number minus
;..previous block's block number
XBLOCK: dw 0 ;16-bit Current block number
ERRCNT: db 0 ;Error count
RX1ST: db 0 ;0 means 1st chr ever received
TIMRLD: dw 0001h ;receive timeout value
;initialized for receiver flush in INIT
TIMRH: db 0 ;high byte of timer
;------------------------
;Disk buffering variables
;------------------------
BLKPTR: dw BUFFER ;Points to next block in BUFFER
BUFCNT: dw 0 ;Count of 128-byte blocks in BUFFER
BUFMAX: db 0 ;Max address in BUFFER/256
EOFLAG: db 0 ;EOF flag (0 means no EOF yet)
;---------------------------
;Other initialized variables
;---------------------------
XMODE: db 0FFh ;00: send
;X1: Rx, not currently flushing
;X2: Rx, currently flushing
;01 or 02: no disk writes yet
;8x: disk writes have occured
;FFh: uninitialized
CRCFLG: db SELCRC ;0 for checksum, NZ for CRC
;init to SELCRC for receiving
PACCNT: db 0 ;Count-down for pacifiers. Init to
;start new line
;FFh disables pacifiers.
;*****************************************************
;Buffer for Xmodem blocks. This buffer overwrites the
;following initialization code, as well as CP/M's CCP.
;*****************************************************
;Force the BUFFER to be page-aligned for faster compares
BPAGER equ $
BUFFER: equ (BPAGER+255) and 0FF00h
;===========================================================
;= The following subroutines and variables are used only =
;= during the initial command line processing, and get =
;= wiped out by the BUFFER, once we start transfering data.=
;===========================================================
;-----------------------------------------------------
;Defaulted variables needed only during initialization
;-----------------------------------------------------
OPMODE: db 0 ;0 means reading from command line
;01h means reading from XMODEM.CFG
;80h means end of file forXMODEM.CFG
;..0 means command line
BYTCNT: db 0 ;command buffer bytes
CPUMHZ: db 2 ;CPU speed in MHz (for timeouts)
XPORT: db 0 ;Transfer port defaults to CON
ENHRDR: db 0 ;01 for RDR that returns with
;..Z set if chr not ready
;***INIT-Only Routine************************
;Initialization: parse XMODEM.CFG and command
;line, set up for transmit or receive
;********************************************
INIT: lxi SP,LSTACK ;use local stack
;------------------------------------------------------
;Set default CPU speed to 4MHZ and fix the help message
;if a Z80 is detected. Othwerwise, leave it at 2MHz.
;(The user can later change this with /Z option)
;------------------------------------------------------
sub a ;test for 8080 or Z80
jpe IS8080
mvi a,4 ;Assume Z80s run 4 MHz
sta CPUMHZ
adi '0' ;fix default in help
sta DMHZ ;..message too
IS8080:
;----------------------------------------------------
;Initialize CP/M's default File Control Block for
;disk transfers: Check for an option crammed against
;the filename in the FCB, and clean it up (by padding
;it with spaces) if necessary. Fill the FCB Extent
;bytes with zeros, as required. Test for options,
;but no filename provided.
;----------------------------------------------------
lxi d,FCBNL*256+FCBCLR ;2 counters
lxi h,FCBFN ;file name in FCB
mvi a,'/' ;option marker
cmp m ;options without filename?
jz NOFNER ;Y: error exit
FFCB1: cmp m
jnz FFCB2
mvi a,' '
mov m,a
inx h ;force all future tests
mov m,a ;..at FFCB1 to pass
dcx h
FFCB2: inx h
dcr d
jnz FFCB1
;Clear the FCB Extent bytes to 0's as required by CP/M
; d=0
; e=FCBCLR
; hl=FCBEXT
FFCB3: mov m,d
inx h
dcr e
jnz FFCB3
;----------------------------------------------------
;Skip over the filename in the command line tail to
;find the beginning of command line options. If there
;is no filename, then just print the help screens.
;(The filename can only come from the command line)
; OPMODE=0
;----------------------------------------------------
lxi d,COMBUF ;CP/M cmd line tail
ldax d ;1st byte is the byte count
sta BYTCNT
cpi 100 ;max input line length
jnc BADINP ;..else it clobbers the stack
inx d ;point to 1st chr
call WSKIP ;skip initial whitespace
jc HLPEXT ;Just XMODEM: help
;Scan past the file name, which
;either ends with a space or a '/'
SKPFIL: call CMDCEI ;get input, find end of item
jnz SKPFIL
;-------------------------------------------------------
;look for a configuration file and parse it for options,
;if it exists. Set the DMA address first, since since
;CP/M 1.4 will clobber the DMA address in BOPEN.
; de=address of 1st option on the command line
; hl=BYTCNT
;-------------------------------------------------------
xra a
mov b,m
mov m,a ;BYTCNT=0
push h ;BYTCNT
push d ;command line pointer
push b ;BYTCNT value
inr a ;a=1
sta OPMODE ;read XMODEM.CFG
lxi d,CFGBUF
mvi c,BSTDMA ;Set CP/M DMA address
call GOBDOS
lxi d,CFGFCB ;FCB describes file to open
mvi c,BOPEN ;CP/M FILE OPEN function
call GOBDOS ;M set if a's msb is set
;-1 means open failure
;..meaning no XMODEM.CFG
cp PARSE ;CMDCHR will initialize de
pop b ;BYTCNT value
pop d ;command line pointer
pop h ;BYTCNT
mov m,b ;restore BYTCNT
;--------------------------
;Parse command line options
;--------------------------
xra a ;parse command line next
sta OPMODE
call PARSE ;finally, find options
;-----------------------------------------------------
;Run any user-defined initialization code
;(Up to 12 bytes get filled in here by the /I0 option)
;-----------------------------------------------------
CINIT: nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
;--------------------------------------------------------
;Patch RXBYTE and TXBYTE for the transfer port specified
;by the /X option, or by the default transfer port.
;Also set TIMRLD to the correct value for 1/2 second
;receive timouts, based on XPORT and CPU speed. Flush the
;receiver unless it's RDR that's not modified to return
;with Z set when no chr is waiting.
;On Entry
; XPORT ENHRDR Port
; 0 x Console
; 1 0 PUN/Standard RDR
; 1 1 PUN/Enhanced RDR
; 2 x Direct I/O
; 3 x Custom I/O
;-------------------------------------------------------
lxi h,PORTAB
lda XPORT
add a ;*2
mov e,a
add a ;*4
add e ;*6
mov e,a
mvi d,0
dad d
mov c,m
inx h
mov b,m ;bc=timeout value
inx h
mov e,m
inx h
mov d,m ;de=RX byte routine addr
xchg
;Check for enhanced RDR if RDR is selected, and
;flush the reader unless it's the unmodified RDR port
;(which would hang forever waiting for input)
cpi 6 ;RDR port?
jnz PP1 ;n: hl has correct address
lda ENHRDR ;Enhanced RDR?
ora a
jz PP1 ;n: hl has correct address
lxi h,RXERDR ;y: update patch
PP1: shld RXROUT+1
cnz RXBYT1 ;flush receiver unless it's
;..unmodified RDR
PPTIMO: ;(timeout goes to PPTIMO)
xchg
inx h
mov e,m
inx h
mov d,m ;de=TX byte routine addr
xchg
shld TXROUT+1
;Set TIMRLD based on CPU speed and port cycles in bc
;
; bc = CPU cycles for 0.5 sec loop assuming 1 MHz CPU
; CPUMHZ = CPU speed, in MHz
lda CPUMHZ
lxi h,0
ADJMHZ: dad b
dcr a
jnz ADJMHZ
shld TIMRLD ;timer reload value
;--------------------------------------
;If neither /R nor /S then ask the user
;--------------------------------------
lda XMODE ;did /R or /S get set?
ora a ;-1 meant uninitialized
jp GOTDIR ;with XMODEnin a
ASKRS: call CILPRT
db 'Send or receive (S/R)?',' '+80h
call GETANS
sui 'R' ;'R' and 'S' are adjacent
sui 2 ;'R' or 'S'?
jnc ASKRS ;n: try again
cma ;0 for send, 1 for receive
sta XMODE
GOTDIR:
;----------------------------------------------
;Branch to do transmit- or receive-specific
;initialization, based on XMODE, which is in a
;----------------------------------------------
call CILPRT ;Begin message
db 'File',' '+80h
lda XMODE
ora a ;Z means receive
jnz GORX
;==========================================
;Set up for TXFILE
; FCB has the file name
; 'File ' has already been printed
; CP/M's DMA address is still CFGBUF
; FCB is set up with the transfer file name
;==========================================
lxi h,WAITNK ;timeout return address
shld RTORET ;..for RXBYTE
mvi c,BOPEN ;CP/M FILE OPEN function
call FCBDOS ;set de=FCB, -1 means fail
jm FOFAIL
;Continue announcing
call ILPRNT
db 'open',CR
db 'Sen','d'+80h
call ANNCTP ;announce transfer port
;--------------------------------------------------
;Setup is done - go transmit the file
; FCB is valid
; File is open
; File-open message has been printed on the console
;--------------------------------------------------
jmp TXFILE
;====================================
;Set up for RXFILE
; FCB has the file name
; 'File ' has already been printed
; CP/M's DMA address is set to CFGBUF
;====================================
GORX:
;--------------------------------------------
;If the file already exists then ask to
;overwrite it. (BSERCH will write a directory
;block at the current DMA address = CFGBUF)
;--------------------------------------------
mvi c,BSERCH ;Search directory for file
call FCBDOS ;sets de=FCB
jm FILNEX ;-1 means not there (ok)
call ILPRNT ;continue message
db 'exists. Overwrite (Y/N)','?'+80h
call GETANS ;Get 1-chr response
cpi 'Y'
jnz EXIT
call FERASE ;erase existing file
;returns de=FCB
call CILPRT
db 'File',' '+80h
FILNEX:
;-------------------------------------------------
;Create file on disk (for writing) and report.
; FCB has file name
; 'File ' has already been printed on the console
;-------------------------------------------------
call ILPRNT ;either 'File created'
;or 'File create error'
db 'creat','e'+80h
mvi c,BMAKE ;CP/M CREATE FILE func
call FCBDOS ;sets de=FCB
jm FCERR ;-1 means create error
;--------------------------------------
;Tell user that we are ready
;'File create' has already been printed
;--------------------------------------
call ILPRNT ;finish message
db 'd' ;end of 'File created'
db CR,'Recei','v'+80h
call ANNCTP ;announce port setup
;---------------------------------------------
;Eat up to 256 characters until the line is
;idle for 1 second. Error exit (in PURGE) if
;no timeout before 256 characters are received.
;----------------------------------------------
lxi h,RPRGDN ;Timeout return address
shld RTORET ;..for RXBYT1 and RXBYTE
call PURGE ;go eat chrs
RPRGDN: ;timeout "returns" here
;-------------------------------------------
;Install timeout return address for transfer
;-------------------------------------------
lxi h,RXSERR ;Timeout return address
shld RTORET ;..for RXBYT1 and RXBYTE
;--------------------------------------------
;Set initial character to NAK or SELCRC, and
;report error checking mode (checksum or CRC)
;--------------------------------------------
lda CRCFLG ;CRC or checksum?
ora a ;0 means checksum
;NZ means CRC
;a=SELCRC here
cnz PCRC ;saves a at NAKCHR
;..prints ' with CRCs'
;..returns with Z cleared
cz PCKSUM ;print ' with checksums'
;-----------------------------------------------
;If the console port is the transfer port then
;stall for a few seconds to give the user time
;to start the Xmodem send on the other end.
;-----------------------------------------------
lda XPORT ;console?
ora a ;clears carry
jnz RXIND
lda CPUMHZ ;adjust for CPU speed
mov b,a
RIDEL: lxi h,8929*CRSTAL ;delay loop time
RIDEL1: call CCTRLC ;(about 200 cycles)
dcx h ;(6)timeout timer?
mov a,l ;(4)Test for 16-bit 0
ora h ;(4)clears carry
jnz RIDEL1 ;(10)
;224 cycles/pass
;1 second = 1,000,000 uS = 2,000,000 cycles
;2,000,000/224 = 8929
dcr b
jnz RIDEL
RXIND:
;-----------------------------------------
;Prepare to send the initial NAK or SELCRC
;to initiate transfer, and go receive.
; Carry is clear
;-----------------------------------------
lda NAKCHR ;Initial ACK
jmp RXFILE
;***INIT-Only Subroutine**************
;Announce transfer port. Disable
;pacifiers if transfer port is CON
;On Entry:
; XPORT is valid
; 'send' or 'receiv' has been printed
;Trashes psw,c
;*************************************
ANNCTP: call ILPRNT
db 'ing via',' '+80h
lda XPORT
dcr a
jm TVC
dcr a
jm TVR
jz TVD
call ILPRNT
db 'custom cod','e'+80h
ret
TVD: call ILPRNT
db 'direct I/','O'+80h
ret
TVR: call ILPRNT
db 'RDR/PU','N'+80h
ret
;CON: turn off pacifiers
TVC: sta PACCNT ;a=FFh
call ILPRNT
db 'CO','N'+80h
ret
;***INIT-Only Subroutine**************************
;Get a 1-character response (with editing, CR, and
;potential control-C) from the user
;On Exit:
; a=uppercase response, if 1 chr typed
; a=ff for no characters typed
; a=1-9 for 2-10 characters typed (definitely not
; 'Y','N','R', or 'S')
;Trashes c,de
;*************************************************
GETANS: lxi d,COMBUF
mvi a,BRDCON ;max chrs (10)
stax d ;in place for BDOS
mov c,a ;BRDCON
call GOBDOS ;returns chr count
inx d ;COMBUF+1
ldax d ;..has the byte count
dcr a ;just 1 chr?
rnz ;n: error exit
inx d ;1st and only chr
ldax d
ani ('a'-'A')xor 0FFh ;uppercase
ret
;***INIT-Only Subroutine*********************************
;Parse command line or CFG file options
;On Entry:
; OPMODE = 00h if reading the tail of the command line
; 01h if reading from XMODEM.CFG
; 80h for end-of-file for XMODEM.CFG
; BYTCNT has the number of bytes in the buffer or line
; the command line or from XMODEM.CFG
; de points to the next input chr, either in the command
; line or in the current block from XMODEM.CFG. de
; may be uninitialized if OPMODE<>0 and BYTCNT=0.
;********************************************************
PARSE:
;-----------------------------------------------------------
;Parse all command line options & set variables accordingly.
;Each option must be preceeded by a '/' Options may be
;preceeded by any reasonable number of spaces, tabs,
;carriage returns and/or line feeds.
;-----------------------------------------------------------
OPTLUP: call WSKIP ;skip whitespace
rc ;end of input input?
lxi b,OPTLUP ;create return address
push b
cpi ';' ;comment?
jz COMENT ;y: ignore until CR or LF
cpi '/' ;all options start with /
jnz BADINP ;error: no /
call CMDCHR ;Get an option chr
jz BADINP ;Error: nothing after /
;-----------------------------------------------------
;Got an option chr in a. Loop through table of options
;looking for a match. Error exit if not in table.
; a = option character
;Trashes c,hl
;-----------------------------------------------------
sta PAR1 ;put option in error msgs
sta PAR2 ;..in case of errors
lxi h,OPTTAB
CHKLUP: cmp m ;Match? (alpha order)
inx h
mov c,m ;get routine address offset
inx h
;Options are in ASCII order
jc OPTERR ;illegal option
jnz CHKLUP ;No match: keep looking
;-----------------------------------------
;Option match. Go execute option routine
;On Entry:
; c = option routine address offset
; de points to next cmd byte
; hl points to next option table entry
; top-of-stack = return address to OPTLUP
;Command routines preserve/advance de
;-----------------------------------------
xra a ;a=b=0 for options
mov b,a ;high byte for dad too
dad b ;hl=address of routine
pchl ;go to routine
;***INIT-Only Table**********************************
;Command Line Options Table
;Table entries must be in alphabetical order, and the
;table is terminated with 0FFh. This table must be
;located before the 1st option routine, and within
;256 bytes of the start of the last option routine.
;
;2 bytes per entry:
; Byte 0 = Uppercase legal option letter
; Byte 1 = offset to address of parse routine
;****************************************************
OPTTAB: db 'C',CCKSUM-(OPTTAB+2) ;Select checksum mode
db 'E',CMODR-(OPTTAB+4) ;Enhanced RDR port
db 'I',CCIO-(OPTTAB+6) ;Custom I/O definition
db 'K',BUFKB-(OPTTAB+8) ;Max buffer size
db 'M',CMESSG-(OPTTAB+10) ;console message
db 'O',COUTP-(OPTTAB+12) ;output to port
db 'P',CPORT-(OPTTAB+14) ;define transfer port
db 'Q',CQUIET-(OPTTAB+16) ;quiet mode
db 'R',CSETR-(OPTTAB+18) ;select receive mode
db 'S',CSETS-(OPTTAB+20) ;select receive mode
db 'X',CSETX-(OPTTAB+22) ;select transfer port
db 'Z',CMHZ-(OPTTAB+24) ;specify CPU MHz
db 0FFh ;end of table
;****************
; Option Routines
;****************
;******----------------------
;* /C * Set Rx Checksum Mode
;******
;On Entry:
; a=b=0
; (de)=next command line chr
;On Exit:
; CRCFLG = 0
;----------------------------
CCKSUM: sta CRCFLG
ret
;******----------------------------
;* /E * Specify Enhanced Reader
;****** (RDR returns Z when no chr)
;On Entry:
; a=b=0
; (de)=next command line chr
;On Exit:
; ENHRDR = 1
;----------------------------------
CMODR: inr a
sta ENHRDR
ret
;*****-------------------------------------------------
; /I * Patch Custom I/O Routine
;*****
; a=b=0
; /I0 hh hh hh... defines init code
; /I1 hh hh hh... defines transmit port routine
; /I2 hh hh hh... defines receive status routine
; /I3 hh hh hh... defines receive data routine
;Max 12 hh digits. (The original intention is to use
;these patches to call some ROM I/O routines, perhaps
;with a couple of registers set up prior to the calls.)
;On Entry:
; b=0
; (de)=next command line chr
;On Exit:
; A Custom Transfer port routine has been written
; de incremented past /I data
;------------------------------------------------------
CCIO: call CMDCHR ;get next command line chr
sui '0' ;un-ASCII the chr
cpi 4 ;Valid code at all?
jnc BADVAL
;Get the address of the routine to define, based on a. The
;4-byte table is also the reference for address offsets,
;and is located just before the first modified routine.
lxi h,CTABLE
mov c,a ;0<c<4, b=0 already
;bc is table offset
dad b ;hl points to table entry
mov c,m ;lookup offset in table
dad b ;get final address
;Get & install all routine bytes, padding with nops at the end.
;The number of bytes here must match the available space in
;the custom code routines above (which expects 12 bytes).
; hl=address of beginning of routine to be modified.
CIOGET: mvi c,12 ;max bytes for a routine
CIOG0: call GETHEX
jnc GIOG1 ;any character?
xra a ;n: install nop
GIOG1: mov m,a
inx h
dcr c
jnz CIOG0
ret ;note: any more hex values
;will cause an error in PARSE
;******-------------------------------------
;* /M * print message on console
;******
;On Entry:
; de points to a string that is terminated
; by either a CR/LF or the end of the input
; subsequent bytes are init sequence
;On Exit:
; de incremented past the end of the line
;-------------------------------------------
CMESSG: call ILPRNT ;trashes c
db CR+80h ;ILPRNT adds the LF
CMSGLP: call CMDCHR ;get next chr
rz ;end of message string?
call PRINTA ;to console
jmp CMSGLP
;******-----------------------------------------
;* /O * Output to Port
;******
;On Entry:
; (de)=next command line chr
; subsequent bytes are init sequence
;On Exit:
; Data sequence has been sent to specified port
; de incremented past /O data
;-----------------------------------------------
COUTP: call GTHEXM ;get port number
sta IPORT+1
CILOOP: call GETHEX ;get an init value
rc ;done?
IPORT: out 0 ;port address gets modified
jmp CILOOP
;******-------------------------------------
;* /P * Define Transfer Port
;******
;On Entry:
; (de)=next command line chr
;On Exit:
; Transfer port routines have been modified
; de incremented past /P data
;-------------------------------------------
CPORT: mvi b,4 ;shift 4 bytes in
CPLOOP: mov l,h ;l gets status port
mov h,c ;h gets data port
mov c,a ;c gets jz/jnz flag
call GTHEXM ;a gets Rx Ready mask
dcr b
jnz CPLOOP
mov b,a ;b=Rx ready mask
call GTHEXM ;get a=Tx ready mask
push d ;command pointer
xchg ;port address to de
lxi h,OMODFY+1
call MODIO ;modify input routine
mov a,b ;Rx ready mask
lxi h,IMODFY+1
call MODIO ;modify output routine
pop d ;command pointer
ret
;******-----------------------------------
;* /Q * Enables quiet mode
;****** (no pacifiers etc. on the console)
;On Entry:
; a=b=0
; (de)=next command line chr
;On Exit:
; PACCNT=FFh
;-----------------------------------------
CQUIET: dcr a ;a=FFh
sta PACCNT
ret
;******----------------------
;* /R * Select receive mode
;******
;On Entry:
; a=b=0
; (de)=next command line chr
;On Exit:
; XMODE = 1
;----------------------------
CSETR: inr a ;a=1
;Fall into CSETS to save XMODE
;******----------------------
;* /S * Select send mode
;******
;On Entry:
; a=b=0
; (de)=next command line chr
;On Exit:
; XMODE = 0
;----------------------------
CSETS: sta XMODE
ret
;******----------------------
;* /X * Select transfer port
;******
;On Entry:
; (de)=next command line chr
;On Exit:
; XPORT set as specified
;----------------------------
CSETX: call CMDCHR
sui '0' ;un-ASCII
cpi 4 ;0-3 allowed
jnc BADVAL
sta XPORT
ret
;******--------------------------------
;* /Z * Specify CPU speed, in MHz (1-8)
;******
;On Entry:
; (de)=next command line chr
;On Exit:
; CPUMHZ updated
;-------------------------------------
CMHZ: call CMDCHR
sui '1' ;un-ASCII
cpi 8 ;1-8 allowed
jnc BADVAL
inr a ;make it 1-8
sta CPUMHZ
ret
;**********------------------------------------
;* /Kn[n] * Set max buffer k-bytes
;**********
;Use this value to set the upper limit for the
;buffer. If the given value results in an upper
;limit above the beginning of BDOS, then use
;the beginning of BDOS as the limit instead.
;1K is the minimum.
;On Entry:
; (de)=next command line chr
;On Exit:
; de advanced past /K value
; BYTCNT decremented accordingly
;trashes psw,b,hl
;----------------------------------------------
BUFKB: call CMDCHR
call D2BDIG ;error-exit if not ASCII digit
;We don't yet know whether this is the 1's digit or
;the 10's digit, so assume it could be either.
mov b,a ;temp save 1st digit
add a ;*2
add a ;*4
add b ;*5
add a ;*10 in case of another digit
mov c,a ;temp save 10x digit
;look for 2nd digit
call CMDCEI ;Z means whitespace, / or EOF
jZ BFBK1 ;no more digits?
;Convert and incorporate the new low digit
call D2BDIG ;1's digit to binary
add c ;combine with high digit
mov b,a ;both decimal digits
;b=given k-bytes in binary. Check for a reasonable value
BFBK1: mov a,b
dcr a ;min 1K buffer
cpi 64 ;max value
jnc BADVAL
;compute number of pages from number of k-bytes high byte
mov a,b
add a ;*512
add a ;*1024
;compute last allowed page's address
adi BUFFER/256
mov b,a ;result in b
;b=potential end page address
lda BDOSA+1 ;page beginning of BDOS
;Choose the smaller of b and a
cmp b
jc BFK4 ;carry means b is bigger
mov a,b
BFK4: dcr a ;last allowed page
sta BUFMAX
ret
;---Local Subroutine--------------
;Convert digit from ASCII and test
;---------------------------------
D2BDIG: sui '0' ;convert from ASCII
cpi 9+1
rc ;Valid decimal?
jmp BADVAL ;Valid?
;***INIT-Only Subroutine***************
;Ignore a comment, terminated either
;by the end of the XMODEM.CFG line
;or the end of file/end of command line
;On Entry:
; de=next command line chr
;**************************************
COMENT: call CMDCHR
jnz COMENT ;Z means EOF, CR or LF
ret
;***INIT-Only Subroutine*******************************
;Get next character from the command line or XMODEM.CFG
;-->Entry is at CMDCHR<--
;
;On Entry:
; OPMODE = 00h if reading the tail of the command line
; 01h if reading from XMODEM.CFG
; 80h for end-of-file for XMODEM.CFG
; BYTCNT has remaining buffer byte count, <=128
; de points to the next chr of the input
;On Exit, not end of input stream:
; Carry clear
; Z set iff CR or LF found
; a = chr from COMBUF or XMODEM.CFG, parity stripped
; de has been advanced and BYTCNT decremented
; unless at end
; hl points to BYTCNT
;On Exit, end of command line:
; (End of line for the command line is when BYTCNT=0.)
; Carry set
; Z set
; a=0
; de invalid
; BYTCNT is 0
;On exit, end of file found in XMODEM.CFG
; (End of file for XMODEM.CFG is either an EOF chr
; or the end-of-file response from the BDOS.)
; OPMODE=80h
; Carry set
; Z set
; a=0
; de invalid
; BYTCNT is 0
;******************************************************
RDCMD: inr m ;BYTCNT=0
lda OPMODE ;reading command line?
add a ;00 and 80 will both become 0
stc
rz ;y: done, Z and Carry set
;Try to read another block of XMODEM.CFG data
push b
lxi d,CFGBUF
push d
mvi c,BSTDMA ;Set CP/M DMA address
call GOBDOS
lxi d,CFGFCB
mvi c,BREAD ;read another sector
call GOBDOS ;a=0, Z set if not file end
pop d ;CFGBUF
pop b
jnz CCDONE ;end: go set Z and C
;No more blocks?
mvi m,BLKSIZ ;another XMODEM.CFG block
;================
;Subroutine Entry
;================
CMDCHR: lxi h,BYTCNT
dcr m ;dec BYTCNT (max was 128)
jm RDCMD ;empty: try to get more
ldax d ;get buffer chr
inx d ;bump buffer pointer
ani 7Fh ;Strip parity
cpi CR
rz
cpi LF
rz
cpi EOF ;file end chr?
stc
cmc ;n:clear carry
rnz
;Exit CMDCHR subroutine: end of file found in XMODEM.CFG
CCDONE: mvi a,80h ;remember EOF
sta OPMODE
add a ;clear a, Set Z and C
ret
;***INIT-Only Subroutine**************************
;Skip over whitespace (spaces, tabs, CRs, and LFs)
;in the command line buffer or XMODEM.CFG file
;On Entry:
; BYTCNT has remaining byte count
; de points to the next chr in buffer
;On Exit:
; a = chr from buffer
; BYTCNT has been decremented
; de has been advanced
; hl points to BYTCNT
; Carry means end of input (and a is not valid)
;*************************************************
WSKIP: call CMDTWS
rc ;end of input?
jz WSKIP ;Whitespace?
ret
;***INIT-Only Subroutine*************************************
;Get next command line or XMODEM.CFG chr, and test it for end
;of item. End of item is end of input, whitespace, or '/'.
;Whitespace is space, tab, CR, or LF. End of input for
;XMODEM.CFG is either an EOF chr or the EOF response from
;BDOS. End of input for the command line is when BYTCNT=0.
;On Entry:
; BYTCNT has remaining byte count
; de points to the next chr in buffer
;On Exit, not end of input stream:
; Carry clear
; Z set if EOF, space, tab, CR, LF, or /
; Z clear: a has valid chr
; a = chr from buffer unless whitespace or EOF
; BYTCNT has been decremented unless /
; de has been advanced unless /
; hl points to BYTCNT
;On Exit, end of command line:
; (End of line for the command line is when BYTCNT=0.)
; Carry set
; Z set
; a=0
; de invalid
; BYTCNT is 0
;On exit, end of file found in XMODEM.CFG
; (End of file for XMODEM.CFG is either an EOF chr
; or the end-of-file response from the BDOS.)
; OPMODE=80h
; Carry set
; Z set
; a=0
; de invalid
; BYTCNT is 0
;************************************************************
CMDCEI: call CMDTWS
rz ;end or whitespace?
cpi '/' ;option crammed?
stc
cmc ;clear carry
rnz
;slash (meaning crammed option), so
;back up, set Z for end of item
dcx d ;y: back up
inr m ;hl=BYTCNT from CMDCHR
xra a ;Clear carry, set Z
ret
;***INIT-Only Subroutine******************************
;Get next command line or XMODEM.CFG chr and test it
;for end of input or whitespace. Whitespace is space,
;tab, CR, or LF. End of input for XMODEM.CFG is either
;an EOF chr or the end of file response from the BDOS.
;End of input for the command line is when BYTCNT=0.
;On Entry:
; BYTCNT has remaining byte count
; de points to the next chr in buffer
;On Exit:
; a = chr from buffer
; BYTCNT has been decremented
; de has been advanced
; hl points to BYTCNT
; Carry set means EOF (and a is 0)
; Z set if EOF, space, tab, CR, or LF
; Z clear: a has valid chr
;On Exit, not end of input stream:
; Carry clear
; Z set if EOF, space, tab, CR, or LF
; Z clear: a has valid chr
; a = chr from buffer unless whitespace or EOF
; BYTCNT has been decremented
; de has been advanced
; hl points to BYTCNT
;On Exit, end of command line:
; (End of line for the command line is when BYTCNT=0.)
; Carry set
; Z set
; a=0
; de invalid
; BYTCNT is 0
;On exit, end of file found in XMODEM.CFG
; (End of file for XMODEM.CFG is either an EOF chr
; or the end-of-file response from the BDOS.)
; OPMODE=80h
; Carry set
; Z set
; a=0
; de invalid
; BYTCNT is 0
;*****************************************************
CMDTWS: call CMDCHR ;EOF, CR, LF?
rz ;Y: done, carry for EOF
cpi ' '
rz
cpi TAB
stc ;Clear carry
cmc ;painfully
ret ;Z set if TAB
;***INIT-Only Subroutine************************
;Modify either the transfer input port routine
;or output port routine. This assumes that both
;routines look like this:
;
; WAIT: ...
; IMODFY or OMODFY:
; in <status port>
; ani <port ready mask>
; jnz WAIT (may get converted to jz)
;
; pop psw (or other 1-byte cmd)
; in/out <data port>
; ...
; ret
;
;On Entry:
; a = port-ready mask byte
; c <> 0 if jz needs to be installed
; d = data port address
; e = status port address
; hl = IMODFY+1 or OMODFY+1
;Trashes a,hl
;*********************************************
MODIO: mov m,e ;install status port adr
inx h ;point to mask location
inx h
mov m,a ;install status mask
inx h ;point to jnz location
mov a,c
ora a
;Code already has a JNZ
jz MODIO1 ;need a jz instead?
mvi m,JZ ;y: install jz opcode
MODIO1:
inx h ;point to data port loc
inx h
inx h
inx h
inx h
mov m,d ;install data port adr
ret
;***INIT-Only Subroutine***************************
;Get an exactly 2-digit hex value from LINBUF
;On Entry:
; de points to first hex digit
;On Exit:
; carry set if no value found, either due to
; end of input or non-hex chr found on 1st digit
; a=value
; de advanced past hex if hex found
; de pointing to non-hex chr if found on 1st digit
; BYTCNT decremented accordingly
;Rude jump to BADVAL if bogus hex found on 2nd digit
;***************************************************
GETHEX: push h
call WSKIP ;skip whitespace, get a chr
;also sets hl=BYTCNT
;EOF will be bogus hex
call HEX2BN ;convert a=1st digit
jnc GHBACK ;bogus digit?
add a ;shift high digit
add a
add a
add a ;..into high nibble
push b
mov b,a ;save high digit
call CMDCHR ;get low digit
;a=0 if end, will fail
call HEX2BN ;convert to binary
jnc BADVAL ;no digit found or bogus?
add b ;combine w/ high digit
;(clears carry)
pop b
pop h
ret ;carry is clear for ret
;non-hex 1st chr found, so backup,
;and return with carry set
GHBACK: dcx d ;back up
inr m ;BYTCNT
pop h
stc
ret ;with carry set
;***INIT-Only Subroutine**************
;convert a to binary
;On Entry:
; a=ASCII hex digit
;On Exit:
; a=chr
; Carry set if OK, clear if bogus chr
;**************************************
HEX2BN: sui '0' ;remove ASCII bias
cpi 10
rc ;if 0-9 then we're done
sui 9+('A'-'9') ;should be 0-5 now
cpi 6 ;gap chr or too high?
rnc ;error: return W/O carry
sui 0F6h ;add 0Ah, set Carry
ret
;***INIT-Only Subroutine****************************
;Get a mandatory 2-digit hex value from LINBUF
;On Entry:
; de points to first hex digit
;On Exit:
; a=value
; de advanced 2
; Rude exit via BADVAL if no chr or bogus hex found
;***************************************************
GTHEXM: call GETHEX
rnc
;Fall into BADVAL
;***INIT-Only Exit*********************************
;Bad Value, bad hex character
;Fix everything for CP/M, and return to CP/M
;The character following the / gets pasted in here.
;**************************************************
BADVAL: call CILPRT
db '/'
PAR1: db '&' ;parameter goes here
db ' bad valu','e'+80h
jmp ERRSRC ;command line or .CFG file
;***INIT-Only Exit*********************************
;Illegal option. Print message and return to CP/M
;The character following the / gets pasted in here.
;**************************************************
OPTERR: call CILPRT ;Exit with this message
db '/'
PAR2: db '&' ;parameter goes here
db ' unknow','n'+80h
jmp ERRSRC ;command line or .CFG file
;***INIT-Only Exit**********************************
;Input error exits. Print message and return to CP/M
;***************************************************
BADINP: call CILPRT
db 'Jun','k'+80h
;Fall into ERRSRC
;***INIT-Only Exit****************
;Bad input of some sort. Print the
;source of error and quit to CP/M
;On Entry:
; OPMODE <>0 if reading .CFG file
; =0 if command line
;*********************************
ERRSRC: call ILPRNT
db ' in',' '+80h
lda OPMODE ;command line or XMODEM.CFG?
ora a
jz BADCLN
call MSGXIT
db 'XMODEM.CF','G'+80h
BADCLN: call MSGXIT
db 'command lin','e'+80h
;***INIT-Only Exit*********************
;Error opening file: Abort with message
; 'File ' has already been printed
;**************************************
FOFAIL: call MSGXIT ;Exit w/ this message
db 'not foun','d'+80h
;***INIT-Only Exit**********************
;Error: File create failed
; 'File create' has already been printed
;***************************************
FCERR: call MSGXIT
db ' fail. Write protect? Dir full','?'+80h
;***INIT-Only Exit****
;No file name provided
;*********************
NOFNER: call CMSGXT
db 'No filenam','e'+80h
;***INIT-Only Exit*********************************
;Print help screen, and then exit. Break up the
;help screen so that it even fits on a 16x64 screen
;**************************************************
HLPEXT: call CILPRT ;print this message
; 123456789012345678901234567890123456789012345678901234567890123
db '========================',CR
db 'Xmodem '
db ((VERSION AND 0F00h)/256)+'0','.',(VERSION AND 0Fh) +'0'
db ' By M.Eberhard',CR
db '========================',CR
db 'Usage: XMODEM <file> <option list>',CR
db '^C aborts',CR,LF
db 'Command line and XMODEM.CFG options:',CR
db ' /R Receive, /S Send',CR
db ' /C Receive with checksums, else CRCs',CR
db ' (Receiver always sets error check mode)',CR
db ' /E if CP/M RDR returns with Z set when not ready',CR
db ' /Knn sets buffer max k-bytes (default: all free RAM)',CR
db ' nn is decimal, 0<nn<64',CR,LF
db '--More-','-'+80h
call GETCON ;wait for user input
call CILPRT
db ' /In 8080 code patches for /X3 I/O routines:',CR
db ' /I0 h0 h1 ...h11: initialize',CR
db ' /I1 h0 h1 ...h11: Tx data, chr in c',CR
db ' /I2 h0 h1 ...h11: Rx status, Z set if no chr',CR
db ' /I3 h0 h1 ...h11: Rx data, chr in a',CR
db ' /M Console message',CR
db ' /O pp h0 h1...hn sends hex h1-hn to port pp',CR
db ' /P ss dd qq rr tt defines direct I/O port:',CR
db ' ss: status port',CR
db ' dd: data port',CR
db ' qq: 00/01 for active low/high ready',CR
db ' rr: Rx ready bit mask',CR
db ' tt: Tx ready bit mask',CR
db '/I, /O and /P values are 2-digit hex',CR,LF
db '--More-','-'+80h
call GETCON ;wait for user input
call CMSGXT ;print message and exit to CP/M
db ' /Q for Quiet; else + means ok block, - means retry',CR
db ' /X sets the transfer port:',CR
db ' /X0 CP/M CON * (default)',CR
db ' /X1 CP/M RDR/PUN *',CR
db ' /X2 Direct I/O, defined by /P option',CR
db ' /X3 Custom code from /I patches',CR
db ' * Must not strip parity',CR
db ' /Zm for m MHz CPU. 0<m<9, default m='
;This '2' gets changed to '4' if a Z80 is detected.
DMHZ: db '2',CR+80h
;***INIT-Onl* Table************************
;Port configuration table
;Three words per entry:
; Word 0 = timer constant
; Word 1 = address of receive byte routine
; Word 3 = address of transmit byte routine
;******************************************
PORTAB: dw CONTO,RXBCON,TXCON ;X0 CP/M console
dw RDRTO,RXRDR,TXPUN ;X1 CP/M RDR/PUN
dw DIRTO,RXDRCT,TXDRCT ;X2 Direct, setup with /P
dw CUSTO,RXCUST,TXCUST ;X3 Custom, setup with /I
;***INIT-Only Table**************
;Configuration File Control Block
;********************************
CFGFCB: db 0 ;(dr) use default drive
db 'XMODEM ' ;(f1-f8)
db 'CFG' ;(t1-t3)
db 0,0,0,0 ;(ex,s1,s2,rc)
dw 0,0,0,0,0,0,0,0 ;(d0-d15)
db 0,0,0,0 ;(cr,r0,r1,r2)
;***INIT-Only Buffer******
;Configuration file buffer
;*************************
CFGBUF: ds BLKSIZ
END
@feilipu
Copy link
Author

feilipu commented Mar 19, 2022

  • Increased WBPERX to write 252 128-byte disk records
  • Set default port to /X0 for CON:
  • Adjusted ticks for 8085/Z80

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment