/ src / xmodem.s
xmodem.s
  1  ; CCGMS Terminal
  2  ;
  3  ; Copyright (c) 2016,2020, Craig Smith, alwyz, Michael Steil. All rights reserved.
  4  ; This project is licensed under the BSD 3-Clause License.
  5  ;
  6  ; XMODEM, XMODEM-CRC and XMODEM-1K Send and Receive
  7  ;
  8  
  9  MAX_RETRIES		= 10
 10  PAYLOAD_SIZE_128	= 128
 11  PAYLOAD_SIZE_1K		= 1024
 12  
 13  ; KERNAL
 14  STATUS	= $90	; channel I/O error/EOF indicator
 15  RIDBE = $029b
 16  RIDBS = $029c
 17  RODBS = $029d
 18  RODBE = $029e
 19  
 20  ; protocol constants
 21  SOH	= $01	; Start of Heading
 22  STX_	= $02	; Start of Heading (1K blocks)
 23  EOT	= $04	; End of Transmission
 24  ACK	= $06	; Acknowledge
 25  NAK	= $15	; Negative Acknowledge
 26  CAN	= $18	; Cancel
 27  CRC	= 'C'	; sent by receiver as first char instead of NAK to indicate CRC instead of checksum
 28  CPMEOF	= $1a	; CP/M EOF character
 29  
 30  ; final status of the transfer
 31  STAT_OK			= 0 ; transfer OK
 32  STAT_CANCELLED		= 1 ; peer sent CAN or didn't respond in time
 33  STAT_NO_EOT_ACK		= 2 ; during send, receiver did not ack the EOT signal
 34  STAT_MAX_RETRIES	= 3 ; too many errors retrying receiving a block
 35  STAT_SYNC_LOST		= 4 ; sender sent the wrong block
 36  STAT_USER_ABORTED	= 5 ; the user aborted the transfer
 37  
 38  ; memory
 39  xmobuf	= $fd	; zero page pointer to access the buffer
 40  
 41  ; uses the following KERNAL calls:
 42  ;  chkin
 43  ;  chkout
 44  ;  close
 45  ;  clrchn
 46  ;  getin
 47  
 48  ; calls from outside code:
 49  ;  xmodem_download	download, will jump back to main
 50  ;  xmodem_upload	upload, will jump back to main
 51  ;  xmmrtc		increment counter, generic code
 52  ; symbols used from outside code
 53  ;  xmodel		receive timeout, reused var
 54  ;  rtca0		counter, see xmmrtc
 55  ;  rtca2		 "
 56  ;  rtca1		 "
 57  
 58  ; uses the following CCGMSTERM symbols:
 59  ;  ui_abort	jumped to after a transfer has failed or was user-aborted
 60  ;  gong		play sound when printing error
 61  ;  xfrdun	jumped to after a transfer has succeeded
 62  ;  outstr	print error message
 63  ;  clear232	clear buffer
 64  ;  modget	receive byte
 65  ;  goobad	show transmission status "key" to user
 66  ;  crclo	pre-computed crc table
 67  ;  crchi	pre-computed crc table
 68  ;  enablexfer	enable serial driver
 69  ;  disablexfer	disable serial driver
 70  ;  reset	same as "enablexfer" (no punter dep)
 71  ;  protoc	protocol (XMODEM, -CRC, -1K)
 72  ;  buffer	contains 3 XMODEM buffers
 73  
 74  _enablexfer = reset
 75  
 76  xmstat	.byte 0		; final error code
 77  xmoblk	.byte 0		; current block index
 78  xmochk	.byte 0		; checksum
 79  xmobad	.byte 0		; error counter
 80  xmodel	.byte 0		; receive timeout
 81  xmoend	.byte 0		; send: EOT flag (and EOT send counter), receive: protocol error counter
 82  xmostk	.byte $ff	; stack pointer
 83  
 84  
 85  ;----------------------------------------------------------------------
 86  ; SEND
 87  ; * The sender picks the block size - we use the "protoc" setting.
 88  ; * The receiver picks checksum vs. CRC, we support both.
 89  ;----------------------------------------------------------------------
 90  ;
 91  ;                     | Receiver: checksum | Receiver: CRC  |
 92  ;---------------------|--------------------|----------------|
 93  ; Setting: XMODEM     | 128 checksum       | 128 CRC16      |\sa
 94  ; Setting: XMODEM-CRC | 128 checksum       | 128 CRC16      |/me
 95  ; Setting: XMODEM-1K  | 1K checksum        | 1K CRC16       |
 96  ;
 97  ; If the receiver does not support 1K, we *could* fall back to 128B, but
 98  ; this would be tricky:
 99  ; * A receiver that understands the "STX" code for 1K doesn't ACK it; it
100  ;   just keeps receiving the block and ACKs at the end.
101  ; * A receiver that does not understand the "STX" code just ignores it,
102  ;   receiving more bytes and hoping for a SOH, EOT or CAN.
103  ; Therefore, it's tricky to detect whether a receiver supports 1K blocks.
104  ; The common way of doing a fallback is to do a full retry with a 128B
105  ; after sending the 1K block, but:
106  ; * this is slow
107  ; * would require lots of extra logic to send the 1 KB worth of buffer
108  ;   contents as eight 128B blocks, since we can't rewind the source file.
109  ;----------------------------------------------------------------------
110  xmodem_send:
111  	; save stack pointer
112  	tsx
113  	stx xmostk
114  
115  	jsr init_transfer
116  	jsr _enablexfer
117  
118  	lda protoc
119  	cmp #PROTOCOL_XMODEM_1K
120  	bne @b128
121  	lda #0
122  	ldx #>PAYLOAD_SIZE_1K
123  	bne @contsz
124  @b128:	lda #PAYLOAD_SIZE_128
125  	ldx #1
126  @contsz:
127  	sta firstpagebytes
128  	stx pagectr
129  
130  ; expect NAK (chksum) or 'C' (CRC)
131  @loop:
132  	lda #6		; 60 secs
133  	jsr modem_get
134  	beq :+
135  @abort:	jmp xmabrt	; timeout -> cancelled
136  :	cmp #CAN
137  	beq @abort	; CAN -> cancelled
138  	cmp #NAK
139  	beq @nakok
140  	cmp #CRC
141  	bne @loop
142  @nakok:	sta xprotoc
143  
144  @block_loop:
145  	jsr setup_buffer
146  	ldy #0
147  	sty xmoend	; reset EOT flag
148  	sty xmobad	; init error counter
149  
150  	jsr disablexfer
151  
152  ; read block into buffer
153  	ldx #LFN_FILE
154  	jsr chkin
155  
156  	lda pagectr
157  	sta tmppagectr
158  	jsr crcinit
159  
160  	ldy #0
161  @snd2:	jsr getin	; read from file
162  	ldx STATUS
163  	stx xmoend	; set EOT flag if end of file (or error)
164  @snd3:	jsr store_byte
165  	bne :+
166  	inc xmobuf+1
167  	dec tmppagectr
168  	beq @snd5
169  :	ldx xmoend	; end of file?
170  	beq @snd2	; no, next byte
171  
172  	lda #CPMEOF	; EOF (or error)
173  	bne @snd3	; -> fill with end of file code
174  
175  @snd5:
176  	jsr clrchn
177  
178  @send_again:
179  	jsr clear_buffers
180  	jsr enablexfer
181  
182  ; send block header
183  	lda #SOH
184  	ldx protoc
185  	cpx #PROTOCOL_XMODEM_1K
186  	bne :+
187  	lda #STX_
188  :	jsr modput	; 0: SOH/STX (128/1K)
189  	lda xmoblk
190  	jsr modput	; 1: block index
191  	eor #$ff
192  	jsr modput	; 2: block index ^ $FF
193  
194  	jsr setup_buffer
195  
196  	ldx pagectr
197  	ldy #0
198  :	lda (xmobuf),y
199  	jsr modput
200  	iny
201  	cpy firstpagebytes
202  	bne :-
203  	inc xmobuf+1
204  	dex
205  	bne :-
206  
207  	lda xprotoc
208  	cmp #CRC
209  	bne @ncrc
210  
211  ; send CRC
212  	lda crcz+1
213  	jsr modput
214  	lda crcz
215  	jmp @send_crc_cont
216  @ncrc:
217  	lda xmochk
218  @send_crc_cont:
219  	jsr modput
220  
221  	jsr clrchn
222  	jsr clear_input_buffer
223  
224  ; expect CAN
225  	lda #3		; timeout
226  	jsr modem_get
227  	bne @snbd	; error
228  	cmp #CAN
229  	bne @snd8
230  	jmp xmabrt	; CAN -> cancelled
231  
232  @snd8:	cmp #NAK
233  	bne @snd9
234  
235  @snbd:
236  	jsr chrout	; ??? send to screen?
237  	jmp @send_again	; NAK -> send again
238  
239  @snd9:	cmp #ACK
240  	bne @snbd	; error
241  
242  ; receiver ACK'ed the block
243  	lda #'-'
244  	jsr goobad
245  
246  	ldx xmoend	; was the end of the file reached?
247  	bne :+		; yes
248  
249  	inc xmoblk	; next block index
250  	jmp @block_loop
251  
252  :	lda #0
253  	sta xmoend	; reset EOT flag
254  
255  ; send EOT
256  @sne1:
257  	jsr enablexfer
258  	lda #EOT
259  	jsr modput	; send EOT
260  	lda #3		; timeout
261  	jsr modem_get
262  	bne :+
263  	cmp #ACK	; ACK received!
264  	bne :+
265  	jmp xmfnok	; set status: OK
266  
267  :	inc xmoend
268  	lda xmoend
269  	cmp #MAX_RETRIES
270  	bcc @sne1	; retries...
271  	jmp xmneot	; set status: no EOT ack
272  
273  
274  ;----------------------------------------------------------------------
275  crcinit:
276  	; init checksum
277  	lda #0
278  	sta xmochk
279  	; init crc
280  	sta crcz
281  	sta crcz+1
282  	rts
283  
284  ;----------------------------------------------------------------------
285  init_effect:
286  	; init screen effect
287  	lda #<$0400
288  	sta scrptr
289  	lda #>$0400
290  	sta scrptr+1
291  	rts
292  
293  ; store and checksum/CRC
294  store_byte:
295  	; store
296  	sta (xmobuf),y
297  	; screen effect
298  scrptr=*+1
299  	sta $ffff
300  	; calc checksum
301  	pha
302  	clc
303  	adc xmochk
304  	sta xmochk
305  	pla
306  	; quick crc computation with lookup tables
307  	eor crcz+1
308  	tax
309  	lda crcz
310  	eor crchi,x
311  	sta crcz+1
312  	lda crclo,x
313  	sta crcz
314  	; update screen effect
315  	inc scrptr
316  	bne :+
317  	inc scrptr+1
318  :	lda scrptr
319  	cmp #<($0400+14*40)
320  	bne :+
321  	lda scrptr+1
322  	cmp #>($0400+14*40)
323  	bne :+
324  	jsr init_effect
325  :	; increment and compare
326  	iny
327  firstpagebytes=*+1
328  	cpy #$00
329  	rts
330  
331  ;----------------------------------------------------------------------
332  init_transfer:
333  	jsr init_effect
334  	lda #1
335  	sta xmoblk	; start block index
336  	lda #0
337  	sta xmobad
338  ;----------------------------------------------------------------------
339  setup_buffer:
340  	lda #<xmodem_buffer
341  	sta xmobuf
342  	lda #>xmodem_buffer
343  	sta xmobuf+1
344  	rts
345  
346  ;----------------------------------------------------------------------
347  clear_buffers:
348  	lda RODBS	; clear rs232 output buffer
349  	sta RODBE	; [XXX no driver has one, SwiftLink uses RODBS for something else!]
350  ;----------------------------------------------------------------------
351  clear_input_buffer:
352  	lda rtail	; clear rs232 input buffer
353  	sta rhead
354  	rts
355  
356  ;----------------------------------------------------------------------
357  modem_get_1:
358  	lda #1
359  ; get modem byte with timeout
360  modem_get:
361  	sta xmodel
362  	lda #0
363  	sta rtca1
364  	sta rtca2
365  	sta rtca0
366  
367  @1:	jsr modget	; receive byte
368  	bcs :+
369  	ldx #0		; ok
370  	rts
371  :	jsr xchkcm	; check for keyboard abort
372  	jsr xmmrtc
373  	lda rtca0
374  	cmp xmodel
375  	bcc @1
376  
377  	; timeout
378  	jsr clrchn
379  	and #0
380  	ldx #1
381  	rts
382  
383  ;----------------------------------------------------------------------
384  ; timeout counter
385  RTCMOD = 72271		; ???
386  xmmrtc
387  	ldx #0
388  	inc rtca2
389  	bne @1
390  	inc rtca1
391  	bne @1
392  	inc rtca0
393  @1:	sec
394  	lda rtca2
395  	sbc #RTCMOD >> 16
396  	lda rtca1
397  	sbc #>RTCMOD
398  	lda rtca0
399  	sbc #<RTCMOD
400  	bcc @2
401  	stx rtca0
402  	stx rtca1
403  	stx rtca2
404  @2:	rts
405  
406  rtca1	.byte 0
407  rtca2	.byte 0
408  rtca0	.byte 0
409  
410  ;----------------------------------------------------------------------
411  ; increment number of retries, and abort if maximum reached
412  count_bad:
413  	lda #':' ; BAD
414  	jsr goobad
415  	inc xmobad
416  	lda xmobad
417  	cmp #MAX_RETRIES
418  	bcs xmtrys	; max retries reached
419  	rts
420  
421  ;----------------------------------------------------------------------
422  ; test for CBM key pressed (abort transfer)
423  xchkcm
424  	ldx SHFLAG
425  	cpx #SHFLAG_CBM
426  	beq xmcmab
427  	rts
428  
429  ;----------------------------------------------------------------------
430  ; set final status and clean up
431  xmfnok	lda #'*' ; GOOD
432  	jsr goobad
433  	lda #STAT_OK
434  	.byte $2c
435  xmabrt	lda #STAT_CANCELLED
436  	.byte $2c
437  xmneot	lda #STAT_NO_EOT_ACK
438  	.byte $2c
439  xmtrys	lda #STAT_MAX_RETRIES
440  	.byte $2c
441  xmsync	lda #STAT_SYNC_LOST
442  	.byte $2c
443  xmcmab	lda #STAT_USER_ABORTED
444  	sta xmstat
445  
446  ; clear garbage off stack
447  	ldx xmostk
448  	txs
449  
450  	jsr clear_buffers
451  
452  	lda xmstat
453  	cmp #STAT_SYNC_LOST
454  	bcc @ex4
455  
456  	; send CAN if STAT_SYNC_LOST and STAT_USER_ABORTED
457  	jsr _enablexfer
458  
459  	ldy #8
460  	lda #CAN
461  :	jsr modput
462  	dey
463  	bpl :-		; 9x CAN
464  
465  @ex4:	jsr clrchn
466  	jsr disablexfer
467  	lda #LFN_FILE
468  	jmp close	; close file on disk
469  
470  
471  ;----------------------------------------------------------------------
472  ; RECEIVE
473  ;----------------------------------------------------------------------
474  ; * The sender picks the block size, we support 128 and 1K.
475  ; * The receiver picks checksum vs. CRC.
476  ;
477  ;                     | Sender: 128  | Sender: 1K  |
478  ;---------------------|--------------|-------------|
479  ; Setting: XMODEM     | 128 checksum | 1K checksum |
480  ; Setting: XMODEM-CRC | 128 CRC16    | 1K CRC16    |\sa
481  ; Setting: XMODEM-1K  | 128 CRC16    | 1K CRC16    |/me
482  ;
483  ;----------------------------------------------------------------------
484  xmodem_receive:
485  	tsx
486  	stx xmostk
487  
488  	; set block size & checksum based on protocol
489  	lda #NAK	; XMODEM: send NAK before first block
490  	ldx protoc
491  	cpx #PROTOCOL_XMODEM
492  	beq :+
493  	lda #CRC	; XMODEM-CRC and -1K: send 'C' before first block
494  :	sta @receive_nak_code
495  
496  	jsr _enablexfer
497  	jsr init_transfer
498  	jmp :+
499  @retry1:	; receive error
500  	jsr count_bad
501  :	lda #0
502  	sta xmoend	; reset EOT counter
503  
504  @retry2:
505  	jsr clear232
506  	jsr enablexfer
507  
508  @receive_nak_code = *+1
509  	lda #NAK
510  	jsr modput
511  	jsr clrchn
512  
513  ; block loop
514  @bloop:	jsr modem_get_1
515  	beq @3		; ok
516  
517  @error:	inc xmoend	; count protocol errors
518  	lda xmoend
519  	cmp #MAX_RETRIES
520  	bcc @retry2	; loop
521  @abort:	jmp xmabrt	; cancelled
522  
523  @3:	cmp #CAN	; cancel?
524  	beq @abort
525  	cmp #EOT	; end of transmission
526  	bne @neot
527  
528  	lda #1
529  	sta xmoend	; EOT!
530  	jmp @ackblk	; send ACK, end
531  
532  @neot:	cmp #SOH
533  	bne @nsoh
534  
535  	lda #PAYLOAD_SIZE_128
536  	ldx #1
537  	bne @contsz
538  
539  @nsoh:	cmp #STX_
540  	bne @error	; no -> protocol error
541  
542  	lda #0
543  	ldx #>PAYLOAD_SIZE_1K
544  
545  @contsz:
546  	sta firstpagebytes
547  	stx pagectr
548  	stx tmppagectr
549  
550  ; get block index and duplicate
551  	jsr modem_get_1
552  	bne @retry1
553  	sta block_index
554  	jsr modem_get_1
555  	bne @retry1
556  	sta nblock_index
557  
558  	jsr crcinit
559  	jsr setup_buffer
560  	ldy #0
561  	sty xmoend	; reset EOT flag
562  
563  @rloop:	jsr modem_get_1
564  	jne @retry1	; error
565  	jsr store_byte
566  	bne @rloop
567  
568  	inc xmobuf+1
569  	dec tmppagectr
570  	bne @rloop
571  
572  ; validate block index from duplicate
573  	lda block_index
574  	eor nblock_index; block index ^ $FF
575  	cmp #$ff
576  	jne @retry1	; incorrect
577  
578  	jsr modem_get_1	; checksum byte or first CRC byte
579  	jne @retry1
580  
581  	ldx protoc
582  	cpx #PROTOCOL_XMODEM
583  	beq @old_chksum
584  
585  ; CRC check for XMODEM-CRC or -1K receive
586  	pha
587  	jsr modem_get_1	; second CRC byte
588  	jne @retry1
589  
590  	cmp crcz
591  	bne @bad
592  	pla
593  	cmp crcz+1
594  	beq @chksum_cont
595  @bad:	jmp @retry1
596  
597  ; compare old XMODEM checksum
598  @old_chksum:
599  	cmp xmochk	; compare with transmitted checksum
600  	jne @retry1	; incorrect
601  
602  ; check whether it's the expected block, a re-send, or the wrong block
603  @chksum_cont:
604  	jsr disablexfer
605  
606  	lda block_index
607  	cmp xmoblk	; expected block?
608  	beq @okblk	; yes
609  
610  	ldx xmoblk
611  	dex
612  	txa
613  	cmp block_index	; is it the preceding block again?
614  	bne @xmorsa	; no, sync error
615  
616  	; sender misunderstood our ACK, did a re-send;
617  	; we can just ignore the block
618  	lda #'/' ; duplicate block
619  	jsr goobad
620  	jmp @next
621  
622  @xmorsa	jmp xmsync	; sync error
623  
624  ; we just received the next block intact
625  @okblk:	jsr clrchn
626  	jsr disablexfer
627  
628  ; write payload to disk
629  	jsr setup_buffer
630  	ldx #LFN_FILE
631  	jsr chkout
632  	ldx pagectr
633  	ldy #0
634  :	lda (xmobuf),y
635  	jsr chrout
636  	iny
637  	cpy firstpagebytes
638  	bne :-
639  	inc xmobuf+1
640  	dex
641  	bne :-
642  
643  @next:	lda #0
644  	sta xmoend	; reset error counter
645  	inc xmoblk	; expect next block
646  	jsr clrchn
647  	lda #'-'	; good block
648  	jsr goobad
649  
650  @ackblk:
651  	jsr clear232
652  
653  ; send ACK
654  	jsr enablexfer
655  	lda #ACK
656  	jsr modput
657  	jsr clrchn
658  
659  	lda #0
660  	sta xmobad	; reset bad block counter
661  	lda xmoend
662  	bne :+		; EOT!
663  	jmp @bloop	; next block
664  
665  :	jmp xmfnok	; end of file, send * key
666  
667  ; does not belong to XMODEM source
668  .include "filetype.s"
669  
670  ;----------------------------------------------------------------------
671  ; error messages
672  SET_PETSCII
673  msg_cancelled:
674  	.byte CR,"Transfer Cancelled.",0
675  msg_no_eto_ack:
676  	.byte CR,"EOT Not Acknowleged.",0
677  msg_max_retries:
678  	.byte CR,"Too Many Bad Blocks!",0
679  msg_sync_lost:
680  	.byte CR,"Sync Lost!",0
681  SET_ASCII
682  
683  ;----------------------------------------------------------------------
684  ; *** upload: send, handle status
685  xmodem_upload
686  	jsr xmodem_send	; send
687  	jmp xmodon
688  
689  ;----------------------------------------------------------------------
690  ; *** download: receive, handle status
691  xmodem_download:
692  	jsr xmodem_receive	; receive
693  xmodon
694  	lda #CR
695  	jsr chrout
696  	lda xmstat
697  	bne :+		; success
698  	jmp xfrdun
699  
700  :	cmp #STAT_USER_ABORTED
701  	beq xmodna	; error-return with no message
702  	cmp #STAT_CANCELLED
703  	bne xmodn3
704  	lda #<msg_cancelled
705  	ldy #>msg_cancelled
706  	bne xmodnp
707  xmodn3
708  	cmp #STAT_NO_EOT_ACK
709  	bne xmodn4
710  	lda #<msg_no_eto_ack
711  	ldy #>msg_no_eto_ack
712  	bne xmodnp
713  xmodn4	cmp #STAT_MAX_RETRIES
714  	bne xmodn5
715  	lda #<msg_max_retries
716  	ldy #>msg_max_retries
717  	bne xmodnp
718  xmodn5
719  	lda #<msg_sync_lost
720  	ldy #>msg_sync_lost
721  xmodnp
722  	jsr outstr
723  	jsr gong
724  	lda #CR
725  	jsr chrout
726  xmodna
727  	jmp ui_abort
728  
729  xprotoc:
730  	.res 1
731  pagectr:
732  	.res 1
733  tmppagectr:
734  	.res 1
735  block_index:
736  	.res 1
737  nblock_index:
738  	.res 1
739  ; used for temprarily storing the two CRC bytes
740  crcz:
741  	.res 2