*****************************************************************************
;
; TBIOS.ASM
;    Tarbell routines for CP/M 2.2AT. This is a CP/M 2.2 BIOS for the
;    Altair and Tarbell floppy controllers together in an Altair 8800.
;
;    See the ABIOS.ASM for detailed information.
;
;****************************************************************************
	public	tMoveRd,tMovWrt,tRdTrk,tWrtTrk,tChkWp
	
	extrn	dispMsg,delay
	extrn	curDrv,curTrk,trkTbl,bfDirty,bfDrive,bfTrack
	extrn	drvNum,trkNum,secNum,dmaAddr,trkBuf,tDrvNum
	extrn	amBuf,amTrk,amSec

; Head settle delay, head reverse delay.
;    Based on the drive used, specify the head settle delay and the head
;    direction reversal delay (both are additional delay after the final 
;    10ms step delay provided by the 1771).

NUMSEC	equ	26		;number of sectors per track
HDSETL	equ	120		;12ms additional head settle after step
REVDIR	equ	120		;12ms delay when reversing step direction

; Misc equates

SECLEN	equ	128		;sector length
RDTRIES	equ	5		;number of read attempts
CR	equ	13		;ascii for carriage return
LF	equ	10		;ascii for line feed

	cseg
;----------------------------------------------------------------------------
; tMoveRd - Move sector data from track buffer to (dmaAddr) for a disk
;    read request
;
; On Entry
;    trkNum = track to read
;    secNum = sector number to read (0-25)
;    dmaAddr = address of buffer to read into
;
; On Exit
;    If successful (good sector)
;	sector data moved to (dmaAddr), Z flag set true
;	HL = (dmaAddr) + 128
;    Else (sector flagged as bad)
;	Z flag set false
;    Clobbers all
;----------------------------------------------------------------------------
tMoveRd	call	loadHD		;HL=(dmaAddr), DE->sector buffer in trkBuf

	ldax	d		;check 1st byte (sector status byte)
	ora	a
	rnz			;bad sector

	inx	d		;DE->1st data byte in sector
	jmp	moveD2H		;move sector to (dmaAddr) and exit

;----------------------------------------------------------------------------
; tMovWrt - Move sector data from (dmaAddr) to track buffer for a disk
;    write request.
;
; On Entry
;    trkNum = track to write
;    secNum = sector number to write (0-25)
;    dmaAddr = address of buffer to write from
;
; On Exit
;    Sector data moved to trkBuf from (dmaAddr)
;    A=0, Z status true
;    Clobbers all
;----------------------------------------------------------------------------
tMovWrt	call	loadHD		;HL=(dmaAddr), DE->sector buffer in trkBuf
	xchg			;DE=(dmaAddr), HL->sector buffer in trkBuf

	mvi	m,0		;force sector to "good"
	inx	h		;HL->first data byte in sector
				;fall into moveD2H

;------------------------------------------------------------------------------
; moveD2H - move 128 bytes from (DE) to (HL).
; On Entry:
;    DE->source buffer
;    HL->destination buffer
; On Exit:
;   Z flag set
; Clobbers B. DE and HL updated as buffer moved.
;------------------------------------------------------------------------------
moveD2H	mvi	b,SECLEN	;B=buffer length (128 bytes)

movLoop	ldax	d		;move from (DE) to (J:)
	mov	m,a
	inx	d		;increment both pointers
	inx	h
	dcr	b		;decrement character count
	jnz	movLoop		;loop until count = 0

	ret			;Z is set on exit

;------------------------------------------------------------------------------
; tRdTrk - read full track into track buffer. The status byte
;    at the start of each sector is zero if the sector is good,
;    non-zero if the sector couldn't be read.
; On Entry:
;    A=Tarbell drive number (0 or 1)
;    drvNum, trkNum hold drive and track for the requested read
; On Exit:
;    trkNum on drvNum read into trkBuf
;    Z set if track read, Z cleared if error
; Clobbers all
;------------------------------------------------------------------------------
tRdTrk	sta	tDrvNum		;save Tarbell drive number

	call	dSelDrv		;select the drive
	rnz			;error, probably not ready

	call	dSeek		;seek to trkNum, E=sec under head
	rnz			;unrecoverable seek error

	mvi	d,NUMSEC	;D=sector counter

rdLoop	mvi	b,RDTRIES	;restart retry count
	call	dNxtSec		;E=sector, HL->sector buffer

rdRtry	call	dRead		;read the sector
	jz	rdNext		;good read, do next sector

	call	dSecAdr		;HL->sector buffer
	dcr	b		;decrement retry counter
	jnz	rdRtry
				;fall through to do next sector, the bad
				;sector has a non-zero status byte

rdNext	dcr	d		;decrement sector count
	jnz	rdLoop		;loop exits with zero true

; Track read successful. Update bfDrive and bfTrack with the drive and
;    track number present in the track buffer and exit.

	lhld	drvNum		;L=drvNum, H=trkNum
	shld	bfDrive		;bfDrive=drvNum, bfTrack=trkNum

	ret			;return with Z flag set

;------------------------------------------------------------------------------
; tWrtTrk - Flush track buffer to the already selected drive on the
;    current track.
; On Entry:
;    Drive to write already selected.
; On Exit:
;    Z set for no error, Z cleared if write error occured.
;    Clobbers all
;------------------------------------------------------------------------------
tWrtTrk	xra	a		;clear buffer dirty flag
	sta	bfDirty

	call	dReadAm		;get E=sector under head
	jnz	wrtErr		;couldn't read address mark
	
	mvi	d,NUMSEC	;D=sector counter

wrtLoop	call	dNxtSec		;E=sector, HL->sector buffer
	
	mov	a,m		;check sector error flag
	ora	a		;only write good sectors (flag=0)
	jnz	wrtNext		;bad sector, skip it
	
	call	dWrite		;write the sector if not bad
	jnz	wrtErr		;hard write error

wrtNext	dcr	d		;decrement sector count
	jnz	wrtLoop

	mvi	a,6		;delay 600us for trim erase
	jmp	delay		;do the delay and return

; wrtErr - Display error message since CP/M won't for a delayed write
;   called at conIn time.

wrtErr	call	dispMsg		;A is non-zero on return
	db	CR,LF,'Tarbell Delayed Write Err',CR,LF,0

	inr	a		;force non-zero return status
	ret	

;----------------------------------------------------------------------------
; loadHD - Return HL=(dmaAddr), DE->sector buffer in trkBuf. 
; On Entry:
;    secNum = sector to read or write (0-25)
;    dmaAddr = address of buffer to read or write
; On Exit:
;   HL=(dmaAddr)
;   DE->sector buffer in trkBuf (status byte)
; Clobbers none
;----------------------------------------------------------------------------
loadHD	lxi	h,secNum	;load requested sector into E
	mov	e,m
	inr	e		;convert to 1-indexed
	call	dSecAdr		;HL->sector within trkBuf (status byte)
	xchg			;DE->sector within trkBuf

	lhld	dmaAddr		;HL=(dmaAddr)
	ret

;****************************************************************************
;
; Tarbell disk I/O routines (single density controller)
;
;****************************************************************************

; Tarbell Disk Drive Controller Equates

FDCBASE	equ	0F8H		;controller base address
FDCCMD	equ	FDCBASE		;1771 command register
FDCSTAT	equ	FDCBASE		;1771 status register
FDCTRK	equ	FDCBASE+1	;1771 track register
FDCSEC	equ	FDCBASE+2	;1771 sector register
FDCDATA	equ	FDCBASE+3	;1771 data in/out register
FDCWAIT	equ	FDCBASE+4	;FDC stall/wait port
FDCSEL	equ	FDCBASE+4	;FDC drive select port

STPRATE	equ	2		;10ms step rate

; Commands for 1771

cRESTOR	equ	008h+STPRATE	;restore, head load, no verify
cSEEK	equ	018h+STPRATE	;seek, head load, no verify
cSTPIN	equ	058h+STPRATE	;step in, update track reg, head loaded
cSTPOUT	equ	078h+STPRATE	;step out, update track reg, head loaded
cREAD	equ	088h		;read, head assumed settled
cWRITE	equ	0A8h		;write, head assumed settled
cRDADDR	equ	0C4h		;read address marker, wait head settle
cRESET	equ	0D0h		;reset 1771 to idle

cSTPTGL	equ	020h		;xor'd toggles between step in and out
cWAITHD	equ	004h		;bit for read/write to wait for head settle

; Status bits from 1771

sNOTRDY	equ	80h		;1=not ready
sWRPROT	equ	40h		;1=write protected
sHDLOAD	equ	20h		;1=head is loaded
sSKERR	equ	10h		;1=seek error
sCRCERR	equ	08h		;1=CRC error
sTRACK0	equ	04h		;1=on track zero
sINDEX	equ	02h		;1=index detected
sBUSY	equ	01h		;1=1771 busy

sSECERR	equ	10h		;1=sector not found
sWRTFLT	equ	20h		;1=write fault
sDLOST	equ	04h		;1=lost data 

; Combined status flag masks for status of a seek, read, and write

smSEEK	equ	sSKERR+sCRCERR+sNOTRDY+sBUSY
smREAD	equ	sSECERR+sCRCERR+sDLOST+sNOTRDY+sBUSY
smWRITE	equ	smREAD+sWRPROT+sWRTFLT

;--------------------------------------------------------------
; dSelDrv - If a new selDrv has occured (fNewSel is true),
;    do a physical drive select and update the track register
;    in the 1771 by reading an address mark from the disk. If
;    this operation fails, a restore to track zero is requested.
; On Entry:
;    drvNum = drive to be selected
;    fNewSel = "new select" flag when not zero
; On Exit:
;    curDrv = drvNum
;    Z flag set = drive selected and ready
;    Z flag clear = error
; Clobbers H,L
;--------------------------------------------------------------
dSelDrv	lxi	h,curDrv	;HL->currently selected drive
	mov	e,m		;E=currently selected drive

	lda	drvNum		;A=desired drive
	mov	c,a		;save new drive in C
	cmp	e		;same or different drive?
	jz	dsSame		;same drive
	
; Save current track (curTrk) for the current drive to the track
;    table in case it was an Altair drive

	mov	m,c		;save new drive in curDrv
	
	mvi	d,0		;DE=currently selected drive
	lxi	h,trkTbl	;HL->track table
	dad	d		;HL->trkTbl entry for current drive

	lda	curTrk		;A=track current drive is on
	mov	m,a		;save it in the track table

; New drive select requested. Output the new drive select to the
;    Tarbell controller.

	lda	tDrvNum		;A=Tarbell drive number
	cma			;the selection decoder inverts
	add	a		;shift left 4 with zero fill
	add	a
	add	a
	add	a
	ori	2		;set the latch bit
	out	FDCSEL		;select the drive

; Reset the 1771 in case we've entered from a bad state. This is
;    done by calling the check write-protect routine which does
;    a reset as part of its function.

dsSame	call	dChkWp		;does a 1771 reset

; Read address mark to find out what track the new drive is on.
;    Update the track register in the 1771 with the proper track.

	call	dReadAm		;returns A=track from address mark
	jnz	dRestor		;failed, restore to track 0 and exit

	out	FDCTRK		;update track register in 1771
	ret			;return with zero true

;--------------------------------------------------------------
; tChkWp
; dChkWp - Reset 1771 and return status of the write protect
;    status from the 1771.
; On Entry:
;    Drive selected and ready
; On Exit:
;    Z flag set = not write protected
;    Z flag clear = write protected.
; Clobbers none
;--------------------------------------------------------------
tChkWp	equ	$
dChkWp	mvi	a,cRESET	;ensure 1771 is idle
	out	FDCCMD
	xthl			;waste some time before checking
	xthl

	in	FDCSTAT		;read 1771 status
	ani	sWRPROT		;return write protect bit alone
	ret

;--------------------------------------------------------------
; dSeek - Seek to the track specified in trkNum (set by a
;    previous call to the BIOS setTrk entry point) on the
;    currently selected drive. After the seek completes, an
;    address mark is read to verify the seek and to determine 
;    what sector is under the head. This action also ensures
;    the head is settled so subsequent reads and writes can
;    assume a loaded and settled head.
; On Entry:
;    trkNum = desired track number
; On Exit:
;    E = sector number from the address mark
;    Z flag set = seek worked and verified
;    Z flag clear = error
; Clobbers B,D,E,H,L
;--------------------------------------------------------------
dSeek	mvi	d,3		;D=retry counter

dsRetry	call	dSeekC		;issue seek command
	rnz			;hard error, probably not ready

	call	dReadAm		;returns A=track from address mark
	jnz	dsFail		;couldn't read address mark

	lxi	h,trkNum	;HL->desired track number
	cmp	m		;does actual match requested?
	rz			;yes, we're done

dsFail	dcr	d		;decrement retry counter
	rm			;went below zero, exit with <> 0

	call	dRestor		;restore to track zero
	jz	dsRetry		;try again
	
	ret			;return with error

;--------------------------------------------------------------
; dSeekC - Issue command to 1771 to seek to trkNum. After
;    the seek completes, the head settle time (HDSETL) is
;    waited before returning.
; On Entry:
;    trkNum = desired track number
; On Exit:
;    Z flag set = seek completed
;    Z flag clear = error
; Clobbers B
;--------------------------------------------------------------
dSeekC	lda	trkNum		;give track to seek to 1771
	out	FDCDATA

	mvi	a,cSEEK		;issue seek command
	out	FDCCMD
	in	FDCWAIT		;wait for seek to complete

	mvi	a,HDSETL	;wait for head settle time
	call	delay

	in	FDCSTAT		;test status of the seek
	ani	smSEEK		;any of these bits is an error
	ret

;--------------------------------------------------------------
; dRestor - Restore the currently selected drive to track 0.
;    Waits for the direction reversal delay (REVDIR) before
;    exiting in case a subsequent seek is immediately done.
; On Entry:
;    
; On Exit:
;    Z flag set = restore operation worked
;    Z flag clear = restore failed
; Clobbers B
;--------------------------------------------------------------
dRestor	mvi	a,cRESTOR	;issue restore to track zero
	out	FDCCMD
	in	FDCWAIT		;and wait for restore to complete

	mvi	a,REVDIR	;wait direction reversal delay
	call	delay

	in	FDCSTAT		;verify restore worked
	ani	smSEEK		;isolate seek bits
	ret			;return status

;--------------------------------------------------------------
; dRead - Read sector on current track. The sector is read
;    starting at HL+1. The byte at (HL) is a status byte
;    that is set to zero for a good read, non-zero for a
;    bad read.
; On Entry:
;    Drive is selected, on proper track, head loaded
;    HL->sector buffer
;    E = sector number to read
; On Exit:
;    Z flag set = success
;    Z flag clear = error
; Clobbers H,L
;--------------------------------------------------------------
dRead	mov	a,e		;give sector number to FDC
	out	FDCSEC
	mvi	a,cREAD		;output read command
dReadA	out	FDCCMD

	push	h		;save address of status byte
	inx	h		;point to 1st data byte

dRdLoop	in	FDCWAIT		;wait until byte available
	ora	a		;sector finished?
	jp	dRdDone		;yes

	in	FDCDATA		;read next byte and...
	mov	m,a		;store in buffer
	inx	h		;bump memory pointer
	jmp	dRdLoop

dRdDone	in	FDCSTAT		;get status of the read
	ani	smREAD		;any of these bits is an error
	pop	h		;hl->status byte
	mov	m,a		;save sector status in 1st byte
	ret

;--------------------------------------------------------------
; dReadAm - Read address mark from disk. Returns track
;    in A and sector in E from the first address mark found.
;    Since an address mark is read before any sector reads
;    or writes take place, this function is used as a single
;    point to initially disable interrupts prior to data I/O.
; On Entry:
;    Drive is selected
; On Exit:
;    Z flag set = success
;    Z flag clear = error
;    A = track number from disk
;    E = sector number from disk
; Clobbers H,L
;--------------------------------------------------------------
dReadAm	di			;disable interrupts
	lxi	h,amBuf		;HL->address mark buffer
	mvi	a,cRDADDR	;read address mark command
	call	dReadA		;read with command in A

	lxi	h,amSec		;HL->sector from address mark
	mov	e,m		;E=sector from address mark

	lda	amTrk		;A=track from address mark
	ret			;exit with data and status

;--------------------------------------------------------------
; dWrite - Write a sector on current track. Data is written
;    starting at HL+1 since the 1st byte is a sector status
;    byte for internal use.
; On Entry:
;    Drive is selected, on proper track, head loaded
;    HL->buffer address
;    E = sector number to write
; On Exit:
;    Z flag set = success
;    Z flag clear = error
; Clobbers H,L
;--------------------------------------------------------------
dWrite	mov	a,e		;give sector number to FDC
	out	FDCSEC
	mvi	a,cWrite	;output write command
	out	FDCCMD
	inx	h		;move to 1st data byte

dWrLoop	in	FDCWAIT		;wait until ready for byte
	ora	a		;sector finished?
	jp	dWrDone		;yes

	mov	a,m		;A=next byte to write
	out	FDCDATA
	inx	h		;bump memory pointer
	jmp	dWrLoop

dWrDone	in	FDCSTAT		;get status of the write
	ani	smWRITE		;any of these bits is an error
	ret

;--------------------------------------------------------------
; dNxtSec - Increment and wrap the sector number passed in E,
;    then fall into dSecAdr to return HL with a pointer to the
;    sector address within the track buffer.
; On Entry:
;    E = most recent sector number
; On Exit:
;    E = next sector number
;    HL->sector within trkBuf
; Clobbers none
;--------------------------------------------------------------
dNxtSec	inr	e		;bump to next sector
	jz	dSec1		;in case uninitialized rolled to 0

	mvi	a,NUMSEC	;A=number of sectors
	cmp	e		;compare NUMSEC - next sector
	jnc	dSecAdr		;next is good (1-26)

dSec1	mvi	e,1		;restart at sector 1
				;fall into dSecAdr

;--------------------------------------------------------------
; dSecAdr - Return HL with address of the sector buffer within
;    trkBuf for the sector number in E. This routine is hard
;    coded for a sector length of 128+1, the SECLEN equate is 
;    ignored. The extra byte accounts for the status byte at
;    the start of each sector.
; On Entry:
;    E = sector number (must be 1-26)
; On Exit:
;    E = sector number (not changed)
;    HL->sector within trkBuf
; Clobbers none
;--------------------------------------------------------------
dSecAdr	push	b		;save BC

	dcr	e		;zero index sector number
	mov	a,e
	ora	a		;clear carry

	rar			;form B=sector >> 1
	mov	b,a

	rar			;put sector lsbit into msbit of C	
	ani	80h
	mov	c,a		;BC=128*(sector-1)
	
	lxi	h,trkBuf	;compute HL=trkBuf+BC
	dad	b

	mov	c,e		;form BC=sector-1
	mvi	b,0
	dad	b		

	inr	e		;restore E
	pop	b		;restore BC
	ret

	end
