Categories
z80

XModem for Z80

I wanted to implement a XModem to incorporate into the Z80 monitor program for my CPUVille Z80. It needed to be small to fit in the 2K ROM monitor, the whole thing ends up being 264 additional bytes. Here is the code for that:

; based on 6502 version http://www.6502.org/source/io/xmodem/xmodem-receive.txt
; re-coded by Tom Lovie 2023-03-23
; sub write_string and put_chr are already in ROM

monitor_warm_start:     equ     0450h

SOH     equ 01h         ; start block
EOT     equ 04h         ; end of text marker
ACK     equ 06h         ; good block acknowledged
NAK     equ 15h         ; bad block acknowledged
CAN     equ 18h         ; cancel (not standard, not supported)
CR      equ 0dh         ; carriage return
LF      equ 0ah         ; line feed
ESC     equ 1bh         ; ESC to exit

rbuff:  equ 0c00h       ; page aligned receive buffer
rbuffp: equ 0ch         ; page of receive buffer
sbuff:  equ 1000h       ; storage buffer to place received data
sbuffp: equ 10h         ; storage buffer page

        org 0800h

                call XModem
                jp monitor_warm_start

XModem:
                ld hl,msg
                call write_string
                ld hl,sbuff
                ld (ptr),hl     ; initialize the storage pointer
                ld a,01h
                ld (blkno),a    ; start at block number 1
StartX:         ld a,NAK        ; Start in CRC mode - no fallback
                call put_chr    ; send it
                ld a,00h        ; loop counter in a
                call GetByte    ; try to get a byte
                jr c,GotByte
                jr nc,StartX    ; if not try again

StartBlk:       ld a,00h        ; loop counter in a
                call GetByte    ; try to get a byte
                jr nc,StartBlk
GotByte:        cp ESC          ; want to quit
                ret z
                cp SOH          ; start of block?
                jr z,BegBlk
                cp EOT          ; end of text
                jr nz,BadCrc
                jr Done
BegBlk:         ld hl,rbuff     ; start hl at the receive buffer
GetBlk:         ld a,00h        ; 3 second window to receive char
GetBlk1:        call GetByte    ; get next char
                jr nc,BadCrc    ; just sending NAK
GetBlk2:        ld (hl),a       ; store the character in buffer
                inc hl          ; increment the buffer
                ld a,83h
                cp l            ; <01><FE><128 bytes><CHKSUM>
                jr nz,GetBlk    ; get 131 characters (0x83)
                ld l,00h        ; start at beginning of buffer 
                ld a,(blkno)    ; actual block number
                cp (hl)         ; sent block number
                jr z,GoodBlk1   ; block number is expected
                jr ErrorOut     ; error out of the xmodem routine
GoodBlk1:       xor 0ffh        ; compliment the actual block no
                inc hl
                cp (hl)         ; compare to second byte
                jr z,GoodBlk2   ; block number compliment 
                jr ErrorOut     ; error out of the xmodem routine
GoodBlk2:       ld h,rbuffp     ; point hl at the receive buffer
                ld l,81h        ; last byte
                ld a,00h        ; initialize a
CalcCrc:        add (hl)        ; compute running total start 82h
                dec l
                jr nz,CalcCrc
                add a,(hl)      ; do the block number as well
                inc a           ; because blockno + cpl = 255.
                ld l,82h        ; (hl) is the checksum
                cp (hl)
                jr z,GoodCrc
BadCrc:         call Flush      ; flush serial buffer
                ld a,NAK        ; and send
                call put_chr    ; a NAK
                jr StartBlk     ; restart the block
GoodCrc:        ld l,02h        ; hl is now pointing data
                ld de,(ptr)     ; de is now pointing storage
                ld c,80h        ; 128 bytes
                ld b,00h        ;
                ldir            ; copy the block
                ld (ptr),de     ; store the current pos
                ld a,(blkno)    ; load the block number
                inc a
                ld (blkno),a    ; store the block number back
                ld a,ACK        ; send ACK
                call put_chr
                jp StartBlk     ; get next block
Done:           ld a,ACK
                call put_chr
                call Flush
                ld hl,good      ; load success message
                call write_string
                ret

ErrorOut:       ld hl,err       ; print error message and exit
                call write_string
                call Flush      ; discard remaining buffer
                ret             ; return after fatal error

; subroutine to wait a set amount of time to get a byte
; Byte will be in A, destroys BC (delay loop)
GetByte:        ld b,a
                ld c,a
GetByteLoop:    call get_chr
                ret c           ; return if got chr (carry set)
                dec c
                jr nz,GetByteLoop
                dec b
                jr nz,GetByteLoop       ; delay loop
                or a                    ; clear carry flag
                ret

; subroutine to flush the receive buffer
; destroys A
Flush:          ld a,80h
                call GetByte
                jr c,Flush
                ret


;Get one byte from the serial port if available.
;Returns with byte in A reg with carry flag set, if carry flag clear means no character available
get_chr:        or      a           ;clear carry flag
                in      a,(3)       ;get status
                and     002h        ;check RxRDY bit
                ret     z           ;not ready, quit
                in      a,(2)       ;get char
                scf                 ;set carry flag we got a char
                ret

ptr:    dfw 0
blkno:  dfb 0
err:    dfb "Up Err!",CR,LF,0
good:   dfb "Up Ok!",CR,LF,0
msg:    dfb CR,LF,"X/CSUM <Esc> to q",CR,LF,0