[description taken from the prlink 0.9.6 README file]

2.5.3 fastload.a65


This routine has actually nothing to do with prlink, except that it uses a
similar protocol as prlink.  This is a synchronous fastloader routine to be
used with a 1541 or 1571 disk drive, also known as "IRQ loader".  Because the
transfer routines are synchronous, the loader works on both PAL-G and NTSC-M
systems, also in fast processor modes.

The loader uses the synchronous IEC bus protocol that is described in Section
6.5.  Everything is fully synchronous, so the transfer won't be disturbed by
any amount of interrupts on the computer.

The loader was modified from a disassembly listing that I got a long time ago.
I assume that it was the loader of the G.I.Joe game, because it only compared
the first two characters of the filenames.  I practically rewrote everything,
only some label names remained.  I also left the two-character filename
comparison there.

Using the loader is very simple.  Select the target system and compile the
loader with DASM (e.g. "make TARGET=c64 fastload.prg").  You will need to
initialize the disk drive with a single call to "init".  Note that the FA
system variable, which holds the current (or last used) device number, must be
properly set before calling the routine.  After the initializations, the drive
will sit in a loop, waiting for the fastload command.  If the ATN signal gets
asserted, the fastloader will quit to normal operation.  So, the drive does
not prevent any normal bus operations; you just have to reinstall the routine
if you want to use it again.  If there aren't any other activities on the bus,
you can load several files with the fastloader without reinstalling it.

The constant LEDFLASH controls the drive LED flashing.  There are four
options.  If LEDFLASH=0, the drive LED acts normally (e.g. it is lit
constantly while a file is being loaded).  If LEDFLASH=1, the drive LED will
glow on and off while the drive is waiting for a request.  From this glow
you will recognize if the fast loader is active.  If LEDFLASH=2, the drive
LED will be on while reading a sector, and off otherwise.  Setting LEDFLASH=3
combines these two advanced options.

When the fastloader is installed, you can load program files by loading the
first two filename characters to the X and Y registers and by calling the
"irqload" routine.  The Carry flag will be set in return, if the file could
not be loaded.

There is only one possible flaw in the loader.  If the computer gets
interrupted right after it has sent the second character of the filename but
before it has completed the handshake, and if the interruption lasts so long
that the drive starts sending the first program byte (or an error code), the
computer will remain in the loop, waiting for the handshake that occurred a
long time ago.  This could be solved with a watchdog timer or something.  But
actually this is not much of a problem, since the drive will read the disk
directory before sending the status byte, which should last longer than any
interruption on the computer side.

Interrupts are disabled on the drive while sending data.  This is mainly for
improving the performance.  However, interrupts must be disabled when sending
the last byte, right before returning to the main loop.  This prevents the
deadlock situation on the drive side.  Note that the interrupts must be
enabled while waiting for the file name, because the disk motor is controlled
by the interrupt routine.  If interrupts are disabled, the disk will not stop
spinning.

These deadlocks could have been avoided by using the protocol described in
Section 6.5.2, but it would have made the loader bigger and slightly slower.

There is a further flaw in the routine.  If the drive is turned off, the
routine may read phantom data from the serial bus, i.e. the signals read from
the bus will vary, although they shouldn't.  I experienced this on my VIC-20
and Oceanic OC-118N drive (1541 clone).  There were no other serial devices on
the bus.  So, the protocol is not completely fault-tolerant, and you might
want to include some sort of a checksum in it.

[the code]
;----------------------------------------------------------------------
; Synchronous 1540/1541/1570/1571 fastloader
; Copyright © 1996 Marko Mäkelä <Marko.Makela@HUT.FI>
;
; This program is free software; you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation; either version 2 of the License, or
; (at your option) any later version.
;----------------------------------------------------------------------
; Init: jsr init
; Load: ldx #"A" ;first char of name
;       ldy #"B" ;second char of name
;       jsr irqload
;       ; C=status: 0=ok, 1=error
;----------------------------------------------------------------------

target eqm c128	; choose the target system
vic20 = 1	; Commodore VIC-20
c64   = 2	; Commodore 64
c128  = 4	; Commodore 128
c16   = 8	; The Commodore 264 Series (C16, C116, plus/4)

START = $2000	; and the start address
RETRIES = 5	; amount of retries when reading a sector

LEDFLASH = 3	; LED flashing level:
		; 0 = normal (LED constantly on while loading a file)
		; 1 = LED glows on and off while waiting for a command
		; 2 = LED on only while reading sectors
		; 3 = 1 + 2

;---------------------------------------
; source code begins
; no user-modifiable routines inside :-)

#processor 6502

; include the ROM definitions

#if target & c16
#include "include/c16rom.lib"
#else
#include "include/cbmrom.lib"
#endif

AMOUNT = $20	; amount of data bytes to transfer with one M-W command
ESCBYTE = $ac	; the escape char used in the transfers

; set the I/O constants and the variables

#if target & vic20
iecport1 = $912c
dato = 32
clko = 2
iecport2 = $911f
atno = 128
clki = 1
dati = 2
#endif

#if target & (c64 | c128)
iecport1 = $dd00
iecport2 = iecport1
atno = 8
clko = 16
dato = 32
clki = 64
dati = 128
#endif

#if target & c16
iecport1 = 1
iecport2 = iecport1
dato = 1
clko = 2
atno = 4
clki = 64
dati = 128
#endif

; declare the zeropage variables

store  = sal	; temporary byte storage

#org START

init:
; initialize the self-modifying parts of the code
  lda #<drvcode
  sta mwbyte$ + 1
  lda #>drvcode
  sta mwbyte$ + 2
  lda #<drive
  sta mwcmd$ + 2
  lda #>drive
  sta mwcmd$ + 1
; send the m-w command to write the data
mwloop$:
  jsr inidev$
  ldx #lmwcmd$ - 1
smwcmd$:
  lda mwcmd$,x
  jsr ciout
  dex
  bpl smwcmd$

; send the actual data bytes  
  ldx #0
mwbyte$:
  lda drvcode,x
  jsr ciout
  inx
  cpx #AMOUNT
  bne mwbyte$

; complete the command
  jsr unlsn

; update the addresses
  clc
  lda #AMOUNT
  adc mwbyte$ + 1
  sta mwbyte$ + 1
  bcc noupdhi1$
  clc
  inc mwbyte$ + 2
noupdhi1$:

  lda #AMOUNT
  adc mwcmd$ + 2
  sta mwcmd$ + 2
  tax
  lda #0
  adc mwcmd$ + 1
  sta mwcmd$ + 1
  cpx #<edrvcode
  sbc #>edrvcode
  bcc mwloop$

; send m-e to start the routine
  jsr inidev$
  ldx #lmecmd$ - 1
sendcmd$:
  lda mecmd$,x
  jsr ciout
  dex
  bpl sendcmd$
; perform the command
  jmp unlsn

; subroutine: make the current drive listen

inidev$:
  lda fa
  jsr listn
  lda #$6f
  jmp secnd

; the m-w command backwards

mwcmd$:
  dc.b AMOUNT,>drive,<drive,"W-M"
lmwcmd$ = . - mwcmd$

; the m-e command backwards

mecmd$:
  dc.b >drive,<drive,"E-M"
lmecmd$ = . - mecmd$


;---------------------------------------
; the actual irq loader

irqload:
  lda iecport1
  and #255 - dato - clko
  sta iec1d1a$	; CLK=1, DATA=1
  sta iec1d1b$
  eor #clko
  sta iec0d1a$	; CLK=0, DATA=1
  sta iec0d1b$

  txa		; send the file name's first character
  jsr putbyt$
  tya		; send the file name's second character
  jsr putbyt$

  jsr getbyt$	; get the start address
  tay
  jsr getbyt$
  sta adrhi$

loadloop$:
  jsr getbyt$	; get next file byte, exit on completion
adrhi$ = . + 2
  sta $100,y	; store it
  iny
  bne loadloop$
  inc adrhi$
  jmp loadloop$

;---------------------------------------
; getbyt$: get a byte, interpret the escape codes

getbyt$:
  jsr getbits$
  cmp #ESCBYTE
  bne getdone$
  jsr getbits$	; escape char fetched, get another byte
  cmp #ESCBYTE	; another escape char: it is a literal
  beq getdone$
  cmp #1	; Transfer finished. 0=ok, nonzero=error.
  pla		; Set the C flag accordingly.
  pla		; discard the return address
getdone$:
  rts

; getbits$: get a byte

getbits$:
  ldx #8	; counter: get 8 bits
getbit$:
  lda iecport2
  and #dati | clki
  eor #dati | clki
  beq getbit$	; wait for CLK==low || DATA==low

#if dati == 128
  asl		; Carry = DATA==low
#else
#if dati < clki
  and #dati
#endif
  cmp #dati
#endif

iec0d1a$ = . + 1
  lda #255 - dato
  bcs gskip$
  eor #dato | clko
gskip$:
  sta iecport1	; acknowledge the bit
  ror store	; store the data

  lda #dati | clki
wgetack$:
  bit iecport2
  beq wgetack$	; wait for CLK==high || DATA==high

iec1d1a$ = . + 1
  lda #255 - clko - dato
  sta iecport1	; raise CLK and DATA
  dex
  bne getbit$	; loop until all bits are received
  lda store
  rts

; putbyt$ puts a byte

putbyt$:
  sta store
  ldx #8	; counter: send all 8 bits
putbit$:
  lsr store	; read a bit
iec0d1b$ = . + 1
  lda #255 - dato
  bcc pskip$
  eor #dato | clko
pskip$:
  sta iecport1	; send the data

  lda #dati | clki
wputack1$:
  bit iecport2
  bne wputack1$	; wait for CLK==DATA==low

iec1d1b$ = . + 1
  lda #255 - clko - dato
  sta iecport1	; set DATA=CLK=high
wputack2$:
  lda iecport2
  and #dati | clki
  eor #dati | clki
  bne wputack2$	; wait for CLK==DATA==high
  dex
  bne putbit$	; loop until all bits are sent
  rts

;---------------------------------------
; the drive code

drvcode:

#rorg $500

acsbf	= $01 ;access to buffer 1
trkbf	= $08 ;track of buffer 1
sctbf	= $09 ;sector of buffer 1
iddrv0	= $12 ;id of drive 0
id	= $16 ;id
datbf	= $14 ;databuffer

buf	= $0400 ;sector buffer

via1pb	= $1800
via2pb	= $1c00

;---------------------------------------

drive:
  cld
driveloop:
  cli		; interrupts enabled until first sector read
#if !(LEDFLASH & 1)
  lda via2pb
  and #$f7	; led off
  sta via2pb
#endif

  jsr recv$
  sta temp1$	; get the file name, first char
  jsr recv$
  sta temp2$	; second char
#if !(LEDFLASH & 2)
  lda #8
  ora via2pb
  sta via2pb	; led on
#endif

  ldx #18
  ldy #1	; read the disk directory (track 18, sector 1)
dirloop$:
  stx trkbf
  sty sctbf
  jsr readsect$	; read the sector
  bcs errquit$	; quit if it could not be read

  ldy #$02
nextfile$:
  lda buf,y	; check file type
  and #$83
  cmp #$82	; must be PRG
  bne notfound$

  lda buf+3,y	; check the first two characters
  cmp temp1$
  bne notfound$

  lda buf+4,y
  cmp temp2$
  beq found$

notfound$:
  tya
  clc
  adc #$20
  tay
  bcc nextfile$

  ldy buf+1	; get next sector
  ldx buf	; and track
  bne dirloop$	; keep trying until the last directory block has been searched

  ; file not found: fall through

errquit$:
  ldx #ESCBYTE	; send the escape byte followed by 1 to notify the computer
  jsr send$
  ldx #1
  jsr send$
  jmp driveloop

found$:
  lda buf+1,y	; get the track and sector numbers
  sta trkbf
  lda buf+2,y
  sta sctbf

nextsect$:
  jsr readsect$
  bcs errquit$	; quit if the sector could not be read
  ldy #$00
  lda buf+1	; store the track and sector of next block
  sta sctbf
  lda buf
  sta trkbf
  bne notlast$	; if the track is nonzero, this wasn't the last sector

  ldy buf+1	; last sector: get sector length
  iny
notlast$:
  sty temp1$

  ldy #2	; skip the track and sector when sending the buffer

sendbuf$:	; send the buffer contents to the computer
  ldx buf,y
  cpx #ESCBYTE
  bne noesc$

  jsr send$	; escape the escape character
  ldx #ESCBYTE

noesc$:
  jsr send$
  iny
  cpy temp1$	; were all bytes of the block sent?
  bne sendbuf$

  lda buf
  bne nextsect$	; loop until all sectors are loaded

finish$:
  ldx #ESCBYTE	; send the escape byte followed by 0 to notify the computer
  jsr send$
  ldx #0
  jsr send$
  jmp driveloop

;---------------------------------------
; readsect$: read a sector

readsect$:
#if LEDFLASH & 2
  lda #8
  ora via2pb
  sta via2pb	; turn the LED on
#endif
  ldy #RETRIES	; load the retry count
  cli		; enable interrupts, so that the command will be executed
retry$:
  lda #$80
  sta acsbf	; code for reading the sector
poll1$:
  lda acsbf	; wait for the command to complete
  bmi poll1$

  cmp #1
  bne noexit$
#if LEDFLASH & 2
  lda #$f7
  and via2pb
  sta via2pb	; turn the LED off
#endif
  clc
  sei		; disable interrupts again to make the program faster
  rts		; success: exit the loop

noexit$:
  dey		; decrement the retry count
  bmi error$	; quit if there were too many retries

  cpy #RETRIES / 2
  bne skipcode$

  lda #$c0
  sta acsbf	; half the retries left: knock the head (seek track 1)

skipcode$:
  lda id	; tolerate disk id changes
  sta iddrv0
  lda id+1
  sta iddrv0+1

poll2$:
  lda acsbf	; wait for the command to complete
  bmi poll2$
  bpl retry$	; branch always

error$:
#if LEDFLASH & 2
  lda #$f7
  and via2pb
  sta via2pb	; turn the LED off
#endif
  sec
  sei
  rts

; send$ sends the X register contents. datbf is used as temporary storage.

send$:
  stx datbf
  ldx #8	; send 8 bits
; sendbit$ sends a bit
sendbit$:
  lsr datbf	; read next bit
  lda #2	; prepare for CLK=high, DATA=low
  bcs sskip$
  lda #8	; prepare for CLK=low, DATA=high
sskip$:
  sta via1pb	; send the data

sgetack$:
  lda via1pb	; wait for CLK==DATA==low
  and #5
  eor #5
  bne sgetack$
  sta via1pb	; set CLK=DATA=high

  lda #5
swait$:
  bit via1pb
  bne swait$	; wait for CLK==DATA==high

  dex
  bne sendbit$	; loop until all bits have been sent
  rts

;---------------------------------------
; recv$ receives a byte to A. datbf is used as temporary storage.

recv$:
#if LEDFLASH & 1
  ldy #0	; LED brightness (0=dim, 255=lit)
  tsx
fincr$:
  jsr doflash$
  ldy datbf
  iny
  bne fincr$
fdecr$:
  dey
  jsr doflash$
  ldy datbf
  bne fdecr$
  beq fincr$

doflash$:
  sty datbf	; store the counter for LED flashing
  lda #$f7
  and via2pb
  sta via2pb	; turn the LED off
  jsr fdelay$	; perform the delay
  lda #8
  ora via2pb
  sta via2pb	; turn the LED on
  lda datbf
  eor #$ff
  tay		; fall through

fdelay$:
  lda #$85
  and via1pb	; wait for any signal from the bus
  bne flashdone$
  iny
  bne fdelay$
  rts

flashdone$:
  lda #$f7
  and via2pb
  sta via2pb	; turn the LED off
  txs		; discard the return address
#endif

  ldx #8	; counter: receive 8 bits
recvbit$:
  lda #$85
  and via1pb	; wait for CLK==low || DATA==low
  bmi gotatn$	; quit if ATN was asserted
  beq recvbit$
  lsr		; read the data bit
  lda #2	; prepare for CLK=high, DATA=low
  bcc rskip$
  lda #8	; prepare for CLK=low, DATA=high
rskip$:
  sta via1pb	; acknowledge the bit received
  ror datbf	; and store it
rwait$:
  lda via1pb	; wait for CLK==high || DATA==high
  and #5
  eor #5
  beq rwait$
  lda #0
  sta via1pb	; set CLK=DATA=high

  dex
  bne recvbit$	; loop until all bits have been received
  lda datbf	; read the data to A
  rts

gotatn$:
  pla		; If ATN gets asserted, exit to the operating system.
  pla		; Discard the return address.
  cli		; Enable the interrupts.
  rts

temp1$ = .
temp2$ = . + 1

edrvcode:
#rend

