*-------------------------------------------------------------------------
*
* Track buffered disk driver for 6800 FLEX 2/3 with DC-x controller
*
*    This track buffered disk driver provides a 40% to 70% speed
*    improvement compared to the standard disk driver. It uses the
*    same disk layout as the standard driver and requires no additional
*    RAM space from the FLEX memory map. This is achieved by placing 
*    the driver and track buffer on a 4K RAM board at $C000 or $D000
*    which is outside of FLEX's memory space.
*
*    The driver supports SSSD or DSSD operation on drives with 35 or
*    40 tracks. Ideally, a 4K RAM board is used in high memory to
*    support the driver and track buffer (e.g., at $C000 or $D000).
*    Set the DRVRLOC equate to point to this RAM. Optionally, for
*    FLEX 3, RAM below $8000 can be used by setting DRVRLOC to $7200
*    and setting MEMEND in FLEX3CON.ASM to $71FF.
*
*    For FLEX 2 vs FLEX 3, see the END directive note at the end of
*    this file. For FLEX 3, the proper INCH I/O vector in FLEX3CON.ASM 
*    must be uncommented (search for INCH in FLEX3CON).
*
*    The step rate for seeking (as defined for the 1771) is stored at
*    $BEB3. The default value is $03 which gives a 40ms step interval.
*    A value of $00 can be placed here for a 12ms/6ms step interval.
*    The utility STEPFAST does this and can be placed in the STARTUP
*    file if desired.
*
*    Written by Mike Douglas
*
*    Rev      Date	    Desc
*    1.0    11/02/2022    Original
*
*    1.1    12/15/2022    When a drive change occurs, unload the head
*			  to ensure the head load timer restarts
*
*-------------------------------------------------------------------------
DRVRLOC	equ	$D000		;place in high RAM

NUMDRV	equ	4		;number of drives supported
MAXTRK	equ	39		;maximum track number
NUMSEC	equ	10		;sectors per side

* FLEX equates

PSTRNG	equ	$AD1E		;print string
PRCNT	equ	$AC34		;non-zero if spool task executing
SPACT	equ	$ACFC		;non-zero if spooling (task active or not)

* Hardware registers

DRVREG	equ	$8014 		;drive select register
CMDREG	equ	$8018 		;1771 command register
TRKREG	equ	$8019 		;1771 track register
SECREG	equ	$801A 		;1771 sector register
DATAREG	equ	$801B 		;1771 data register

* 1771 Commands

cREAD	equ	$88		;read, head assumed settled
cWRITE	equ	$A8		;write, head assumed settled
cRDADDR	equ	$C4		;read address mark, wait head
cRESTOR	equ	$08		;restore to track 0, load head
cSEEK	equ	$18		;seek to track, load head
cHDUNLD	equ	$10		;seek, unload head

* 1771 Status Masks

sNOTRDY	equ	$80		;1=drive not ready (motor off)
sWRPROT	equ	$40		;1=write protected
sHEADLD	equ	$20		;1=head is loaded
sSKERR	equ	$10		;1=seek error
sCRCERR	equ	$08		;1=CRC error
sBUSY	equ	$01		;1=1771 busy

sSECERR	equ	$10		;1=sector not found
sWRTFLT	equ	$20		;1=write fault
sDLOST	equ	$04		;1=lost data
sDRQ	equ	$02		;1=data request

* Error masks for status after read, write

eSEEK	equ	sNOTRDY+sWRPROT+sSKERR
eREAD	equ	sNOTRDY+sSECERR+sCRCERR+sDLOST
eWRITE	equ	eREAD+sWRPROT+sWRTFLT

*-------------------------------------------------------------------------
* Disk driver jump table
*-------------------------------------------------------------------------
	org 	$BE80
	jmp	read		;read a sector
	jmp	write		;write a sector
	jmp	verify		;verify sector just written
	jmp	restore		;restore to track zero
	jmp	select		;select drive
	jmp	chkRdy		;check if drive ready
	jmp	chkRdy		;quick check if ready 
	jmp	exit0		;cold start init (does nothing)
	jmp	exit0		;warm start init (does nothing)
	jmp	exit0		;seek to track (does nothing)
	jmp	inFlush		;$BE9E flush buffer then do console input

*-------------------------------------------------------------------------
* FLEX 2 I/O Vector Table. For FLEX 3, see FLEX3CON.ASM instead. 
*-------------------------------------------------------------------------
	org	$BEA3
INVEC	fdb	inFlush		;input character (in this file)
OUTVEC	fdb	$E1D1 		;output character
ACIA	fdb	$8004 		;ACIA address
TIMER	fdb	$8012 		;timer address
IRQ	fdb	$A000		;IRQ vector
SWI	fdb	$A012		;SWI vector
MON	fdb	$E0D0		;monitor entry address
PCV	fdb	$A048		;FLEX re-entry vector in monitor

*-------------------------------------------------------------------------
* Data area
*-------------------------------------------------------------------------
stepR8	fcb	$03		;$BEB3 don't move! Step rate bits can
*				    be patched by external programs
reqDrv	fcb	0		;requested drive
reqTrk	fcb	0		;requested track (must follow reqDrv)
reqSec	fcb	0		;requested sector
reqSide	fcb	0		;requested side
reqBuf	fdb	0		;pointer to read/write buffer
pSrc	fdb	0		;pointer to source for moveBuf

bfDrive	fcb	NUMDRV		;buffered drive
bfTrack	fcb	$FF		;buffered track (must follow bfDrive)
bfSide	fdb	0		;buffered side
bfDirty fcb	0		;non-zero if track buffer needs writing

secCnt	fcb	0		;sector counter for track I/O
secIdx	fcb	0		;ordinal position of sector
secNum	fcb	0		;sector number of secIdx
secPtr	fdb	0		;pointer to sector in trkBuf	

rtryCnt	fcb	0		;retry counter
saveX	fdb	0		;save location for X
stkSave	fdb	0		;save location for stack pointer

fMotor	fcb	0		;0=motor was on, else motor was off
fWrtErr	fcb	0		;0=no write error, else probably write protect

trkTbl	fcb	$FF,$FF,$FF,$FF,$FF	;track each drive is on

* Address mark buffer

amBuf	equ	*
amTrk	fcb	0		;LSB of track number
	fcb	0
amSec	fcb	0		;sector number
amSize	fcb	0		;sector length code
	rmb	4		;CRC and some spare
	
	org	DRVRLOC
*-------------------------------------------------------------------------
* read - Read sector entry point
*    Read sector B of track A into (X). Return 1771 status in B.
*-------------------------------------------------------------------------
read	bsr	initIo		;init for I/O
	bne	rdExit		;error
	
	stx	pSrc		;set the source buffer
	ldx	reqBuf		;X->destination buffer
	bsr	moveBuf
	clrb			;exit with zero status

rdExit	cli
	rts

*-------------------------------------------------------------------------
* write - Write sector entry point
*    Write sector B of track A from (X). Return 1771 status in B.
*-------------------------------------------------------------------------
write	bsr	initIo		;init for I/O
	bne	wrExit		;error

	bsr	moveBuf		;move data to track buffer
	ldaa	#$FF		;track buffer now dirty
	staa	bfDirty
	clrb			;exit with zero status

wrExit	cli
	rts

*-------------------------------------------------------------------------
* initIo - Save the requested sector, track, and buffer pointer.
*    See if the track buffer needs to flushed and/or loaded.
*-------------------------------------------------------------------------
initIo	tst	PRCNT		;here from spooler task?
	beq	initGo		;no, continue
	swi			;else, suspend spooler
	nop			;allow time for interrupts

initGo	sei			;disable interrupts
	stx	reqBuf		;save the buffer pointer
	stx	pSrc
	staa	reqTrk		;save requested track
	stab	reqSec		;save requested sector
	
	clra			;assume bottom side
	cmpb	#NUMSEC
	bls	iniSide		;it is bottom
	ldaa	#$40		;else, select the top side
iniSide	staa	reqSide

	bsr	wrtTrk		;write track buffer if needed
	jsr	readTrk		;read new track if needed
	bne	iniExit		;read error
	
	ldab	reqSec		;return pointer to sector in trkBuf
	jsr	sec2Idx
iniExit	rts
	
*-------------------------------------------------------------------------
* moveBuf - Move 256 bytes from (pSrc) to (X)
*    The stack pointer is used to speed up the move. Interrupts MUST
*    be disabled while this move runs.
*-------------------------------------------------------------------------
moveBuf	clrb			;moving 256 bytes
	sts	stkSave		;preserve current stack
	lds	pSrc		;SP->source data
	des			;pull does pre-increment
	
mbLoop	pula			;4 get source byte
	staa	0,x		;6 save in destination buffer
	inx			;4
	decb			;2
	bne	mbLoop		;4 (20us)
	
	lds	stkSave		;restore stack pointer
	rts

*-------------------------------------------------------------------------
* verify - Verify sector entry point
*    Always returns "success" unless the last write failed (probably
*    write protect).
*-------------------------------------------------------------------------
verify	ldab	fWrtErr		;return last write status
	rts

*-------------------------------------------------------------------------
* restore - Restore to track zero entry point
*    Used by FMS after read errors to resync the drive track. Used by
*    NEWDISK to select the drive and start on track zero.
*-------------------------------------------------------------------------
restore	jsr	invFlsh		;invalidate and flush buffer if needed
	ldaa	3,x		;get drive to select
	jsr	selDrv		;select the drive

	ldaa	#cRESTOR	;restore to track 0 command
	oraa	stepR8		;incorporate the step rate
	jsr	cmdWait		;issue and wait for completion
	andb	#eSEEK		;test seek error bits
	clc			;2.0 NEWDISK compatibility
	rts

*-------------------------------------------------------------------------
* select - Select drive entry point
*    Sets the requested drive to the passed parameter. No actual drive
*    activity takes place.
*-------------------------------------------------------------------------
select	ldab	$0F		;code for invalid drive 
	ldaa	3,x		;get drive number
	cmpa	#NUMDRV-1	;validate
	bhi	badDrv
	staa	reqDrv		;else, save requested drive number
	
exit0	clrb			;indicate no error (carry clear, zero)
	rts

*-------------------------------------------------------------------------
* chkRdy - Check and quick-check entry points
*    Return "drive ready" if a valid drive number provided.
*-------------------------------------------------------------------------
chkRdy	ldab	#$80		;code for not ready
	ldaa	3,x		;get drive number
	cmpa	#NUMDRV-1	;validate
	bls	exit0

badDrv	sec			;indicate error
	rts

*-------------------------------------------------------------------------
* inFlush - Flush the track buffer then input a character
* invFlsh - Invalidate and flush the track buffer
*    Console input is used as an idle indicator and as a good time
*    to flush a dirty track buffer. Even if not flushed, the track
*    buffer is invalidated to force a track read on the next disk
*    I/O. This helps catch a disk swap.
*-------------------------------------------------------------------------
inFlush	bsr	invFlsh		;invalidate and flush track buffer
	jmp	$E1AC		;use SWTBUG for input

invFlsh	sei			;disable interrupts
	ldaa	bfDirty		;track buffer dirty?
	beq	invBuf		;no, go invalidate the track buffer

	stx	saveX		;flush track buffer
	pshb			;preserve X and B
	bsr	wtFlush		;write the track
	pulb			;restore B and X
	ldx	saveX

invBuf	ldaa	SPACT		;spooling active?
	bne	invExit		;yes, don't invalidate the track buf
	ldaa	#$FF		;else, invalidate the track buffer
	staa	bfTrack

invExit	cli
	rts

*-------------------------------------------------------------------------
* wrtTrk - Check if track buffer should be flushed
*    This function must be called before any drive selection or seek
*    operation. The buffer is flushed if the bfDirty flag is set and
*    the drive, track, or side are different than the buffered track.
*-------------------------------------------------------------------------
wrtTrk	ldaa	bfDirty		;see if track buffer is dirty
	beq	wtExit		;no

	ldx	reqDrv		;buffered drive and track the same
	cpx	bfDrive		;  as requested?
	bne	wtFlush		;no, buffer must be written

	ldaa	reqSide		;buffered side same as requested?
	cmpa	bfSide
	bne	wtFlush		;no, buffer must be written

wtExit	rts			;exit without flush

* Track buffer needs to be written

wtFlush	clr	bfDirty		;no longer dirty
	ldaa	bfDrive		;select the buffered drive
	jsr	selDrv
	ldaa	fMotor		;was motor already running?
	beq	wtAddr		;yes
	ldx	#62500	 	;else, wait 1 sec for motor start
	jsr	delay

wtAddr	ldaa	#NUMSEC		;init sector counter
	staa	secCnt
	jsr	readAm		;get the sector under the head
	bne	wtErr		;failed

wtLoop	jsr	incSec		;move to next sector index
	jsr	idx2Sec		;secNum=sector, secPtr->sector buffer
	jsr	wrtSec		;write the sector
	bne	wtErr		;hard write error	
	dec	secCnt		;decrement sector count
	bne	wtLoop

	ldx	#1000/16	;delay 1ms for trim erase
	bra	delay		;do the delay and exit

wtErr	ldx	#mWrtErr
	jmp	PSTRNG		;report delayed write error and exit

*-------------------------------------------------------------------------
* readTrk - Read a new track if the requested drive/track does not
*   match the buffered drive/track.
*-------------------------------------------------------------------------
readTrk	ldaa	reqSide		;same side as buffered?
	cmpa	bfSide
	bne	newBuf		;no

	ldx	reqDrv		;same drive, track as buffered?
	cpx	bfDrive
	beq	rtExit		;yes - just exit

* Need to read a new track

newBuf	staa	bfSide		;save requested side as bufferd 
	ldaa	#$FF		;invalidate buffer
	staa	bfTrack
	ldaa	#NUMSEC		;init sector counter
	staa	secCnt

	ldaa	reqDrv		;select the requested drive
	jsr	selDrv
	jsr	seek		;seek and secIdx=sector under head
	bne	rtExit		;error

rdLoop	bsr	incSec		;move to next sector index
	jsr	idx2Sec		;secNum=sector, secPtr->sector buffer
	jsr	readSec		;read the sector
	bne	rtExit		;error
	dec	secCnt		;else, decrement sector count
	bne	rdLoop		;read all sectors

	ldaa	reqTrk		;set buffered track = requested
	staa	bfTrack
	clrb			;no errors
rtExit	rts

*-------------------------------------------------------------------------
* incSec - Increment and wrap the sector index secIdx
*-------------------------------------------------------------------------
incSec	ldaa	secIdx
	inca
	cmpa	#NUMSEC
	bne	isExit
	clra
isExit	staa	secIdx
	rts

*-------------------------------------------------------------------------
* delay - Delay 16*X microseconds
*-------------------------------------------------------------------------
delay	inx
	dex
	dex
	bne	delay
	rts

*-------------------------------------------------------------------------
* selDrv - Select the drive in A and the side in bfSide
*    The motor status flag fMotor is set to zero if motor is already
*    running, else, non-zero. If a drive change occurs, the track table
*    is used to save and load the drives' track numbers and the head
*    is unloaded to ensure the head load timer restarts.
*-------------------------------------------------------------------------
selDrv	oraa	bfSide		;combine drive# and side select
	staa	DRVREG		;select the drive

	ldab	CMDREG		;start motor and see if already on
	andb	#sNOTRDY	;not ready set if motor was off
	stab	fMotor		;save as motor status flag
	
	anda	#$03		;get drive number alone
	cmpa	bfDrive		;different drive selected?
	beq	sdExit		;no, exit

	ldx	#trkTbl		;save track for current drive
	ldab	bfDrive		;B=current drive
	stab	sdOff1+1	;modify offset in store instruction
	ldab	TRKREG		;B=current track for current drive
sdOff1	stab	0,x		;save in track table
	
	staa	bfDrive		;new drive is now the current drive
	staa	sdOff2+1	;index by new drive number
sdOff2	ldaa	0,x		;A=track new drive is on
	staa	TRKREG		;set the track in the 1771
	bpl	sdHead		;track is valid

	ldaa	#cRESTOR	;else restore to track 0
	oraa	stepR8		;incorporate the step rate
	jsr	cmdWait		;issue command and wait to finish

sdHead	ldab	TRKREG		;drive changed, so unload head to ensure
	stab	DATAREG		;   the head load timer restarts
	ldab	#cHDUNLD	;seek with head unload
	stab	CMDREG
	jsr	cmdWait		;issue command and wait to finish
sdExit	rts

*-------------------------------------------------------------------------
* seek - Seek to track in reqTrk
*    Loads the head and reads an address mark after seek to find
*    the sector under the head (returned in secNum). Returns with
*    zero true if successful, else seek error returned in B.
*-------------------------------------------------------------------------
seek	ldaa	reqTrk		;A=requested track number
	cmpa	#MAXTRK		;validate
	bhi	skError
	
	staa	DATAREG		;give track to the 1771
	ldaa	#cSEEK		;A=seek to track and load head
	oraa	stepR8		;incorporate the step rate
	jsr	cmdWait		;issue and wait for seek
	
	bsr	readAm		;get the sector under the head
	bne	skError		;failed 
	ldaa	amTrk		;on right track?
	cmpa	reqTrk
	beq	skExit		;yes

skError	ldab	#sSKERR		;return seek error
skExit	rts

*-------------------------------------------------------------------------
* readSec - Read sector specified by secNum into buffer at (X)
*    1771 status returned in B. Zero false if an error occurs.
*-------------------------------------------------------------------------
readSec	ldaa	secNum		;give sector number to the 1771
	staa	SECREG
	ldaa	#cREAD		;1771 read command
	
doRead	staa	CMDREG		;issue command to 1771
	jsr	cmdDly		;ensure command processing time

rsLoop	ldab	CMDREG		;A=1771 status
	bitb	#sBUSY		;read still in progress>?
	beq	rsDone		;no, read has finished
	bitb	#sDRQ		;data ready flag set?
	beq	rsLoop		;no, keep waiting
	
	ldaa	DATAREG		;get the next byte
	staa	0,x		;save in buffer
	inx
	bra	rsLoop

rsDone	andb	#eREAD		;test and return read error bits
	rts

*-------------------------------------------------------------------------
* readAm - Read next address mark into amBuf and convert the sector
*    number found into an ordinal index saved in secIdx. 
*-------------------------------------------------------------------------
readAm	ldaa	#3		;set up retry counter
	staa	rtryCnt

raRetry	ldx	#amBuf		;read into the address mark buffer
	ldaa	#cRDADDR	;1771 read address command
	bsr	doRead		;read an address mark
	bne	raFail		;error

	ldab	amSec		;see if reasonable data present
	cmpb	#NUMSEC*2
	bls	sec2Idx		;convert sec num to ordinal index and exit

raFail	dec	rtryCnt		;any more retries?
	bpl	raRetry		;yes, try again
	rts			;exit with non-zero status

*-------------------------------------------------------------------------
* wrtSec - Write sector specified by secNum from (X)
*    1771 status returned in B. Zero false if an error occurs.
*-------------------------------------------------------------------------
wrtSec	ldaa	secNum		;give sector number to the 1771
	staa	SECREG
	ldaa	#cWRITE		;1771 write command
	staa	CMDREG		;issue command to 1771
	jsr	cmdDly		;ensure command processing time

wsLoop	ldab	CMDREG		;A=1771 status
	bitb	#sBUSY		;write still in progress>?
	beq	wsDone		;no, write has finished
	bitb	#sDRQ		;ready for data?
	beq	wsLoop		;no, keep waiting
	
	ldaa	0,x		;get next byte
	staa	DATAREG		;give it to the 1771
	inx
	bra	wsLoop

wsDone	andb	#eWRITE		;test and return write error bits
	stab	fWrtErr		;save most recent write status
	rts

*-------------------------------------------------------------------------
* sec2Idx - Convert sector in B to sector index in secIdx and a pointer
*    to the sector within the track buffer in secPtr and X. This is done
*    by searching for the sector number in the skew table. Zero false
*    and sector error in B on return if sector not found, else zero true
*    and in B for success. 
*-------------------------------------------------------------------------
sec2Idx	tstb			;sector zero?
	bne	siNot0		;no
	incb			;force sector 0 to be 1

siNot0	clra			;A=index into skew table
	ldx	#skewTbl	;assume bottom skew table
	cmpb	#NUMSEC
	bls	siLoop		;bottom
	ldx	#skewTop	;use top skew table

siLoop	cmpb	0,x		;sector found?
	beq	siMatch		;yes
	inx			;move to next entry
	inca
	cmpa	#NUMSEC		;past end of table?
	bne	siLoop		;no, keep looking

	ldab	#sSECERR	;sector not found error
	rts
	
siMatch	staa	secIdx		;set the sector index

	adda	#trkBuf/256	;compute MSB of sector in trkBuf
	staa	secPtr
	ldaa	#trkBuf		;LSB is the same (256 byte sectors)
	staa	secPtr+1
	ldx	secPtr		;return pointer in X

	clrb			;zero status for success
	rts

*-------------------------------------------------------------------------
* idx2Sec - Convert the sector index (secIdx) into the sector number
*    on the disk (secNum) and a pointer into the track buffer in X
*    and secPtr. Top or bottom sector number determined by bfSide.
*------------------------------------------------------------------------- 
idx2Sec	ldaa	secIdx		;A=ordinal index of the sector
	staa	isOff+1		;modify ldaa offset below
	 
	adda	#trkBuf/256	;compute address of sector in trkBuf
	staa	secPtr		;MSB
	ldaa	#trkBuf		;LSB is the same (256 byte sectors)
	staa	secPtr+1

	ldx	#skewTbl	;X->bottom translate table
	ldaa	bfSide		;top or bottom?
	beq	isOff		;bottom
	ldx	#skewTop	;else use top

isOff	ldaa	0,x		;A=sector to look for on disk
	staa	secNum		;save in secNum
	ldx	secPtr		;return sector pointer in X

	deca			;see if 1st sector in track 0
	bne	isNot1		;not 1st sector
	ldaa	TRKREG		;on track 0?
	bne	isNot1		;no
	dec	secNum		;else 1st sec on trk 0 is 0, not 1
isNot1	rts

*--------------------------------------------------------------
* cmdWait - Write command from A and wait for it to complete
*    Status from the 1771 returned in B.
*--------------------------------------------------------------
cmdWait	staa	CMDREG		;issue command from A
	bsr	cmdDly		;ensure command processing time
	
cwLoop	ldab	CMDREG		;poll for busy bit to go false
	bitb	#sBUSY
	bne	cwLoop	
	rts

*-------------------------------------------------------------------------
* cmdDly - Wait 58us (including the calling JSR) to ensure a 1771
*    command is accepted and the status register can be read.
*
*    Note: This delay bumped to 76us total (60us in the loop) to
*    prevent occasional errors in readAm with an FD-2 controller
*    (WD2797 vs FD1771).
*-------------------------------------------------------------------------
cmdDly	ldaa	#10		;10*6=60us in loop

dlyLoop	deca
	bne	dlyLoop
	rts

*-------------------------------------------------------------------------
* skewTbl - returns the formatted sector number in physical order on
*    the disk. This allows us to read the track in one revolution.
*    skewTop is for reading the top side of a double-sided disk.
*-------------------------------------------------------------------------
skewTbl	fcb	1,3,5,7,9,2,4,6,8,10
skewTop	fcb	11,13,15,17,19,12,14,16,18,20

* Messages

mWrtErr	fcc	'Delayed write error'
	fcb	$04

* Track buffer starts here

trkBuf	equ	*
	
* Use "end $AD00" when assembling for FLEX 2 and append BUFDRVR.BIN
* last when making FLEX2-TB.SYS
*
* Use "end" alone (not even a trailing comment) for FLEX 3 and append
* FLEX3CON.BIN last in the list of files when making FLEX3-TB.SYS
*
* Note that this entry address syntax only works if assembling under
* FLEX using ASMB. If assembled on a PC and uploaded as an S19 file,
* the $AD00 entry address must manually be specified when saving the
* last .BIN file you'll specify in the APPEND statement.

	end	$AD00		;for FLEX 2
*	end
