****************************************************************************
;
; CP/M 2.2b BIOS for the Altair FDC+ 1.5Mb floppy drive option (#5)
;
;    Drive option #5 on the FDC+ uses 5.25" HD floppy drives or 8" DSDD
;    drives to provide 1.5Mb formatted capacity per disk. This is about
;    5 times the capacity of the original Altair 8" floppy drive (308Kb).
;    The additional capacity was achieved by using both sides of the disk,
;    using double-density MFM encoding, and increasing the number of bytes
;    per track from 4096 to 5120 (before going double-density and getting
;    10240 bytes). The latter was was accomplished by using a single sector
;    for the entire track instead of 32. 
;
;    This BIOS implements full track buffering on both reads and writes to
;    improve performance. Compared to non-buffered versions, performance
;    of programs with substantial disk I/O improves by 25%-75%.
;
;    This version provides full IOBYTE support to allow use of a variety
;    of standard Altair I/O boards. Search for "IOBYTE" in this source
;    file for details.
;
;    This version looks for a Teletype on the console port during cold boot
;    and if detected (based on baud rate), subsequently follows any CR to
;    the console device with a NULL to give the Teletype carriage time to
;    reach the left margin. The send null flag (sndNull) is located imme-
;    diately following the MODE and IOBYTE in memory so it can be over-
;    ridden if needed.
;
;    Unlike the 8" drives, the FDC can't determine if a disk is inserted
;    during the drive select process with the 5.25" drives, so this BIOS
;    includes a timeout in the sector hunt loop which will generate an
;    error for the BDOS when a disk is not present.
;
;    Extended function support is provided via the select disk (selDsk)
;    entry point. This BIOS supports "Flush Track Buffer" (function 0)
;    and "Write without Read" (function 1). See selDsk for details.
;
;		Item	  Offset from start of BIOS
;		------	  ----------------------------------
;		MODE	  33h (1FB3 in memory after MOVCPM)
;		IOBYTE	  34h (1FB4 in memory after MOVCPM)
;		SNDNULL	  35h (1FB5 in memory after MOVCPM)
;
;    As of version 1.2, the disk layout has been changed for better relia-
;    bility with aging media. The last 8 tracks on the top side of the disk
;    are not used (the top head is offset inward by 8 tracks versus the 
;    bottom head). Track 76 on the bottom is the last track used for compat-
;    ibility with 8" drives. The track layout is a follows:
;
;    Cylinder    0   1   2   3       70  71  72  73  74  75  76  77  78  79
;               --- --- --- --- ... --- --- --- --- --- --- --- --- --- ---
;    Track Top   1   3   5   7      141 143  -   -   -   -   -   -   -   -
;          Bot   0   2   4   6      140 142 144 145 146 147 148  -   -   -
;
;
;    Version History
;
;    Ver     Date	  Description
;    ---   ---------	---------------------
;    1.0    06/17/20	Beta test version, Mike Douglas
;			Based on CP/M 2.2b for the Altair 8" floppy
;
;    1.1    06/24/20	Mike Douglas
;			Eliminate USER patch, drive type parameter at
;			offset 36h specifies 77 or 80 track drives.
;
;    1.2    07/04/20	Mike Douglas
;			Add support for write protect status from the
;			drive. Fix dVerify which previously looked at
;			only 1/2 the track and never failed. Use 10,240
;			bytes per track instead of 9,984. Change to track
;			layout shown above. Eliminate drive type param
;			added in v1.1.
;
;--------------------------------------------------------------------------
;
;    To patch changes made to this BIOS into a CP/M image saved from
;    MOVCPM (e.g., CPMxx.COM), use following commands:
;
;	A>DDT CPMxx.COM
;	-IBIOS.HEX
;	-Rxxxx      (where xxxx = BIAS computed below)
;	-G0	    (Go zero, not "oh")
;	A>SYSGEN
;
;****************************************************************************
MEMSIZE	equ	56			;set memory size in K bytes
BIOSLEN	equ	3000h			;length of this BIOS

;-----------------------------
; CP/M size & locations
;-----------------------------
CCPLEN	equ	0800h			;CPM 2.2 fixed length
BDOSLEN	equ	0E00h			;CPM 2.2 fixed length

BIOSBAS	equ	MEMSIZE*1024-BIOSLEN	;BIOS base address
BDOSBAS	equ	BIOSBAS-BDOSLEN		;BDOS base address
CCPBASE	equ	BDOSBAS-CCPLEN		;CCP base address
BDOSENT	equ	BDOSBAS+6		;BDOS entry address
SYSGEN	equ	0900h			;SYSGEN image base address
BOOTLEN	equ	1*128		        ;length of boot loader
BIAS	equ	SYSGEN+BOOTLEN-CCPBASE	;bias in DDT to patch in this BIOS

;-----------------------------
; CP/M page zero equates
;-----------------------------
WBOOTV	equ	00h		;warm boot vector location
BDOSV	equ	05h		;bdos entry vector location
CDISK	equ	04h		;CPM current disk
DEFDMA	equ	80h		;default dma address

IOBYTE	equ	03h		;CPM IOBYTE address
SHFTCON	equ	1		;left rotate count to align CON in bits 2,1
SHFTRDR	equ	7		;left rotate count to align RDR in bits 2,1
SHFTPUN	equ	5		;left rotate count to align PUN in bits 2,1
SHFTLST	equ	3		;left rotate count to align LST in bits 2,1

;-----------------------------
; Disk equates
;-----------------------------
NUMDISK	equ	4		;four drives supported
NUMTRK	equ	149		;number of tracks
ENDTOP	equ	72*2		;no top tracks from cylinder 72 and up
NUMSEC	equ	80		;CP/M sectors per track
SECLEN	equ	128		;CP/M sector length
TRKLEN	equ	SECLEN*NUMSEC	;length of track

; Disk parameter table values give 4K blocks and 256 directory entries

BSH	equ	5		;allocation block shift factor (4K)
BLM	equ	1Fh		;allocation block mask (4K)
EXM	equ	1		;extent mask
DSM	equ	369		;max block number 
DRM	equ	255		;max directory entry number (256 entries) 
AL0	equ	0C0h		;directory allocation block bits byte 0
AL1	equ	0		;directory allocation block bits byte 1
RESTRK	equ	1		;reserved tracks for boot image
CKS	equ	(DRM+1)/4	;directory check space

WBSSEC	equ	1		;starting sector of CP/M image
WBSECS	equ	(CCPLEN+BDOSLEN)/SECLEN   ;# warm boot sectors to read

;-----------------------------
; Altair disk controller 
;-----------------------------
DRVSLCT	equ	08h		;drive select register (out)
cDSLCT	equ	80h		;deselect drive

DRVSTAT	equ	08h		;drive status register (in)
sENWD	equ	01h		;enter new write data flag
sMOVEOK	equ	02h		;OK to move head
sHDSTAT	equ	04h		;head status flag
sDSKEN	equ	08h		;disk is selected and enabled
sWRTPRO	equ	10h		;disk is write protected
sINTEN	equ	20h		;processor interrupts enabled
sTRACK0	equ	40h		;on track zero flag
sNRDA	equ	80h		;new read data available

DRVCMD	equ	09h		;drive command register (out)
cSTEPI	equ	01h		;step in
cSTEPO	equ	02h		;step out
cHDLOAD	equ	04h		;head load
cRESTMR	equ	04h		;restart motor-off timer (MINIDSK)
cHDUNLD	equ	08h		;head unload
cREADEN	equ	10h		;read request enable
cHCSON	equ	40h		;reduce head current switch
cWRTEN	equ	80h		;write enable

DRVSEC	equ	09h		;drive sector position (in)
sNEWSEC	equ	01h		;new sector flag (sector true)

DRVDATA	equ	0Ah		;drive read/write data (in/out)

DRVTRK	equ	0Bh		;OUT to provide track number to FDC+

DRVIOS	equ	0Bh		;IN for I/O operation status
sCRCERR	equ	01h		;1=checksum error
sTRKERR	equ	02h		;1=track number error
sEOSERR	equ	04h		;1=unexpected end of sector
sDONE	equ	80h		;1f=read/write to disk complete
sERRMSK	equ	(NOT sDONE) AND 0FFh

;-----------------------------
; 2SIO Serial Board 
;-----------------------------
SIO1CTL	equ	10h		;1st port on 2SIO board - control register
SIO1DAT	equ	11h		;1st port on 2SIO board - data register
SIO2CTL	equ	12h		;2nd port on 2SIO board - control register
SIO2DAT	equ	13h		;2nd port on 2SIO board - data register
SIORDRF	equ	01h		;read data register full flag
SIOTDRE	equ	02h		;transmit data register empty flag
	
;-----------------------------
; SIO Serial Board 
;-----------------------------
SIOCTL	equ	00h		;SIO board at 0 - control register
SIODAT	equ	01h		;SIO board at 0 - data register
ACRCTL	equ	06h		;SIO board at 6 - control register (cassette)
ACRDAT	equ	07h		;SIO board at 6 - data register (cassette)
SIORCV	equ	01h		;data received bit (inverted)
SIOXMT	equ	80h		;read to transmit (inverted)

;-----------------------------
; LPC Line Printer Controller
;-----------------------------
LPCCTL	equ	02h		;line printer control port
LPCDAT	equ	03h		;line printer data port
LPCBSY	equ	02h		;mask to test printer busy flag

;-----------------------------
; Misc equates
;-----------------------------
RDTRIES	equ	6		;read tries
WRTRIES	equ	3		;track writes to try
UNDEF	equ	0FFh		;undefined value
CR	equ	13		;ASCII for carriage return
LF	equ	10		;ASCII for line feed

	org	BIOSBAS
;-----------------------------------------------------------------------------
;  BIOS Entry Jump Table
;-----------------------------------------------------------------------------
	jmp	boot		;cold start
wbJump	jmp	wBoot		;warm start
	jmp	conSt		;console status
	jmp	conIn		;console character in (and disk flush)
	jmp	conOut		;console character out
	jmp	list		;list character out
	jmp	punch		;punch character out
	jmp	reader		;reader character in
	jmp	home		;move head to home position
	jmp	selDsk		;select disk
	jmp	setTrk		;set track number
	jmp	setSec		;set sector number
	jmp	setDma		;set dma address
	jmp	read		;read disk
	jmp	write		;write disk
	jmp	listSt		;return list status
	jmp	secTran		;sector translate

;---------------------------------------------------------------------------
; MODE byte at offset 33h
;---------------------------------------------------------------------------
mode	db	40h		;default mode byte

fCLDCMD	equ	01h		;true = CCP process cmd on cold start
fWRMCMD	equ	02h		;true = CCP process cmd on warm start
fWRTVFY	equ	40h		;true = verify writes

;---------------------------------------------------------------------------
;  IOBYTE definition - default IOBYTE at offset 34h
;
;    CON device (bits 1,0):
;	00 - TTY on SIO at I/O address 0
;	01 - CRT on 2SIO port 1
;	10 - BAT indirect through RDR logical device
;	11 - UC1 on 2SIO port 2
;
;    RDR device (bits 3,2):
;	00 - TTY on SIO at I/O address 0
;	01 - PTR on 2SIO port 1
;	10 - UR1 on cassette port (SIO 6/7)
;	11 - UR2 on 2SIO port 2
;
;    PUN device (bits 5,4):
;	00 - TTY on SIO at I/O address 0
;	01 - PTP on 2SIO port 1
;	10 - UP1 on cassette port (SIO 6/7)
;	11 - UP2 on 2SIO port 2
; 
;    LST device bits (7,6):
;	00 - TTY on SIO at I/O address 0
;	01 - CRT on 2SIO port 1
;	10 - LPT on LPC board
;	11 - UL1 on 2SIO port 2
;---------------------------------------------------------------------------
dIoByte	db	055h		;CON = CRT (2SIO serial port #1)
				;RDR = PTR (2SIO serial port #1)
				;PUN = PTP (2SIO serial port #1)
				;LST = UL1 (2SIO serial port #1)
				
; sndNull flag at offset 35h
;    Determines if a NULL should be transmitted following a CR when
;    writing to the console device. This is automatically set during
;    cold start based on the baud rate of the console device. A user
;    can change this flag if needed after each cold start.

sndNull	db	0		;true if null should be sent after CR

;----------------------------------------------------------------------------
; boot - Cold boot BIOS entry. CPM is already loaded in memory. Initialize
;    serial ports, display welcome banner and enter the CCP.
;----------------------------------------------------------------------------
boot	lxi	sp,0100h	;init stack pointer
	call	serInit		;initialize serial ports and IOBYTE

	call	dispMsg		;display welcome message
	db	CR,LF,LF
	db	MEMSIZE/10 + '0',MEMSIZE mod 10 + '0'
	db	'K CP/M 2.2b v1.2',CR,LF
	db	'For Altair 1.5Mb Floppy',CR,LF,0

				;dispMsg returns with A=0
	sta	CDISK		;current CPM disk is drive zero
	sta	coldSt		;no longer in cold start

	call	initCpm		;common cold/warm boot CPM initialization
	lda	mode		;see if cold start command line requested
	ani	fCLDCMD
	jz	CCPBASE+3	;enter CCP and don't run command line
	jmp	CCPBASE		;else, enter CCP with possible cmd line

;----------------------------------------------------------------------------
; wBoot - Warm boot BIOS entry. Reload CPM from disk up to, but
;    not including the BIOS. Re-enter CPM after loading.
;----------------------------------------------------------------------------
wbRtry	sta	bfDrive		;A<>0, forces re-read of disk

wBoot	lxi	sp,0100h	;init stack pointer
	xra	a		;boot from drive zero
	sta	drvNum
	lxi	b,WBSECS*256+WBSSEC    ;B=secs to read, C=starting sec
	lxi	h,CCPBASE	;HL->CP/M load address

wbTrk	sta	trkNum		;save track number to use

; wbLoop - read sector loop for warm boot

wbLoop	shld	dmaAddr		;set read destination
	mov	a,c		;set sector number from C
	sta	secNum

	push	b		;save sector count and number
	call	read		;read next sector
	pop	b
	jnz	wbRtry		;failed read, start over
	
	dcr	b		;decrement total sector count
	jz	warmCpm		;warm boot is done, warm enter CP/M
	
	inr	c		;bump to next sector number
	mvi	a,NUMSEC	;A=number of sectors in a track
	sub	c		;all sectors in track done?
	jnz	wbLoop		;no, read next sector
	
	mov	c,a		;start with C=sector 0 on text track
	lda	trkNum		;increment track number
	inr	a

	jmp	wbTrk		;continue

warmCpm	call	initCpm		;common warm/cold boot CPM initialization

	lda	mode		;see if warm start command line requested
	ani	fWRMCMD
	jz	CCPBASE+3	;enter CCP and don't run command line
	jmp	CCPBASE		;else, enter CCP with possible cmd line

;----------------------------------------------------------------------------
; initCpm - Initialization for CPM that is common to both warm
;    boot and cold boot.
;----------------------------------------------------------------------------
initCpm	lxi	h,DEFDMA	;set the default dma address (80h)
	shld	dmaAddr

	mvi	a,JMP		;8080 "jump" opcode
	sta	WBOOTV		;store in 1st byte of warm boot vector
	sta	BDOSV		;and 1st byte of bdos entry vector

	lxi	h,wbJump	;get the warm boot jump address
	shld	WBOOTV+1	;and put it after the jump
	lxi	h,BDOSENT	;BDOS entry address
	shld	BDOSV+1		;put it after the jump opcode

	lda	CDISK		;get current disk number
	mov	c,a		;pass it to CPM in register C
	ret

;----------------------------------------------------------------------------
; selDsk - Select Disk BIOS entry. C contains the disk number to select.
;    Validate the disk number and return a pointer to the disk parameter
;    header in HL. Zero is returned in HL for invalid drive number. The
;    disk number is stored in drvNum. No drive activity takes place.
;
;    selDsk is also used as the "Extended Function" entry point. See
;    extFunc below.
;
; On Entry
;    C = disk number to select (or FFh, see Extended Functions below)
;
; On Exit
;    drvNum = disk number to select
;    Success: HL->DPH for this drive. Error: HL=0
;    Clobbers A,D,E,H,L
;----------------------------------------------------------------------------
selDsk	lxi	h,0		;init HL=0 for invalid drive request
	inr	c		;test for drive number of 0FFh
	jz	extFunc		;drive is FF, do extended function

	mov	a,c		;A=drive number + 1
	dcr	a		;A=drive number
	cpi	NUMDISK		;verify drive number less than number of disks
	rnc			;invalid drive number, return with HL=0=error

	sta	drvNum		;save new drive number
	rlc			;compute disk parameter header address
	rlc			;   as (16*drvNum) + dpHead
	rlc
	rlc			;A=16*drvNum

	lxi	h,dpHead  	;HL->start of disk parameter headers
	mov	e,a
	mvi	d,0
	dad	d		;HL->DPH for the passed drive number
	ret

;----------------------------------------------------------------------------
; extFunc
;    selDsk is also used as the "Extended Function" entry point. If a 
;    drive number of FFh is passed in C, then this call is an extended
;    function request and not a disk select request. In this case:
;
; On Entry
;    C = FFh (extended function request)
;    B = extended function number (see list below)
;
; On Exit
;    H = 0, L = number of functions supported. This return value is neither
;	the "invalid drive" response (HL=0) or a valid DPH pointer when a
;    	drive is selected. This allows the caller to detect whether a
;	BIOS supports extended functions or not.
;    Z flag set true if the extended function executed without error, Z flag
;	false otherwise.
;    Clobbers All
;
;    Extended Functions supported by this BIOS:
;	00h - Invalidate and flush track buffer.
;	01h - Write sector without track pre-read. Generally used to
;	      write a full track for formatting and for fast disk to
;	      disk copy operations.
;----------------------------------------------------------------------------
; Check for track buffer flush function 0

extfunc	mov	a,b		;A=extended function code
	ora	a		;code 0 = flush track buffer?
	jnz	efWrite		;no, go see if write w/o read request
		
	call	invFlsh		;invalidate and flush track buffer
	lxi	h,0002h		;H=0, L=number of functions
	ret

; efWrite - Check for write w/o read function 1

efWrite	dcr	a		;function 1 - write w/o read?
	jnz	efExit		;no, exit
	
	dcr	a		;set write w/o read flag non-zero
	sta	wrtNoRd
	call	wrtNr		;do write without track read
	
efExit	lxi	h,0002h		;H=0, L=number of functions
	ret

;----------------------------------------------------------------------------
; home - Home BIOS entry. Set trkNum to zero. No drive activity takes place.
;
; On Entry
;
; On Exit
;    trkNum set to zero
;    Clobbers A
;----------------------------------------------------------------------------
home	mvi	c,0		;set trkNum to zero
				;fall into setTrk

;----------------------------------------------------------------------------
; setTrk - Set Track BIOS entry. C contains the desired track number.
;    The track number is saved in trkNum for later use.
;
; On Entry
;    C = track number to set
;
; On Exit
;    trkNum = track specified in C
;    Clobbers A
;----------------------------------------------------------------------------
setTrk	mov	a,c		;save track number passed in C
	sta	trkNum
	ret

;----------------------------------------------------------------------------
; setSec - Set Sector BIOS entry. C contains the 1-indexed CPM physical 
;   sector. Saved in secNum as a zero-indexed value.  
;
; On Entry
;    C = 1-indexed sector number to set
;
; On Exit
;    secNum = zero-indexed sector number
;    Clobbers A
;----------------------------------------------------------------------------
setSec	mov	a,c		;A=1-indexed sector number
	dcr	a		;convert to zero indexed
	sta	secNum
	ret

;----------------------------------------------------------------------------
; setDma - Set DMA BIOS entry. BC contains the address for reading or
;    writing sector data for subsequent I/O operations. The address is
;    stored in dmaAddr.
;
; On Entry
;    BC = I/O buffer address
;
; On Exit
;    dmaAddr = address specified in BC
;    Clobbers H,L
;----------------------------------------------------------------------------
setDma	mov	h,b		;save buffer address passed in BC
	mov	l,c
	shld	dmaAddr
	ret
	
;----------------------------------------------------------------------------
; secTran - Sector translation BIOS entry. The translate table is 
;    ignored. Instead, the zero indexed logical sector is simply
;    converted to 1-indexed.
;
; On Entry
;    BC = logical sector number
;    DE->sector translate table for selected drive
;
; On Exit
;    HL = logical sector number + 1
;    Clobbers H,L
;----------------------------------------------------------------------------
secTran	mov	l,c		;L=lsb of sector
	mvi	h,0		;H=0=msb of sector
	inx	h		;convert to 1-indexed
	ret

;----------------------------------------------------------------------------
; read - Read sector BIOS entry. Read one sector using the drvNum, 
;    trkNum, secNum, and dmaAddr previously specified.
;
; On Entry
;    drvNum = drive to read from
;    trkNum = track to read from
;    secNum = sector number to read (0-39)
;    dmaAddr = address of buffer to read into
;
; On Exit
;    If read successful
;	sector read into (dmaAddr)
;	HL = (dmaAddr) + SECLEN
;	A=0, Z flag set true
;    Else
;	A=1, Z flag set false
;    Interrupts enabled
;    Clobbers all
;----------------------------------------------------------------------------
read	xra	a		;ensure write w/o read is flag is false
	sta	wrtNoRd

	call	ckFlush		;flush track buffer if needed
	ei			;re-enable interrupts

	call	readTrk		;fill track buffer if needed
	ei			;re-enable interrupts
	jnz	exitDio		;track read error, exit

	call	sec2Adr		;HL->sector in trkBuf
	xchg			;DE->sector in trkBuf
	lhld	dmaAddr		;HL->destination for data
	
moveSec	mvi	b,SECLEN	;B=number of bytes to move
	
movLoop	ldax	d		;move sector from (DE) to (HL)
	mov	m,a
	inx	h		;increment pointers
	inx	d
	dcr	b		;loop count
	jnz	movLoop 

exitDio	mvi	a,0		;if zero is true, return zero
	rz

exitErr	inr	a		;else return A<>0, Z false
	ret
	
;----------------------------------------------------------------------------
; write - Write sector BIOS entry. Write one sector using the drvNum,
;    trkNum, secNum and dmaAddr specified. 
;
; On Entry
;    drvNum = drive to write
;    trkNum = track to write
;    secNum = sector number to write (0-39)
;    dmaAddr = address of buffer to write from
;
; On Exit
;    If successful
;	sector written to trkBuf from (dmaAddr)
;	A=0, Z flag set true
;    Else
;	A=1, Z flag set false
;    Interrupts enabled
;    Clobbers all
;----------------------------------------------------------------------------
write	xra	a		;ensure write w/o read is flag is false
	sta	wrtNoRd

wrtNr	call	ckFlush		;flush track buffer if needed
	ei			;re-enable interrupts

	call	readTrk		;fill track buffer if needed
	ei			;re-enable interrupts
	jnz	exitDio		;track read error, exit
	
	in	DRVSTAT		;check write protect status
	ani	sWRTPRO
	jz	exitErr		;exit with forced error status

	mvi	a,0FFh		;set dirty flag true
	sta	bfDirty
	
	lhld	dmaAddr		;HL=dmaAddr
	xchg			;DE=dmaAddr
	call	sec2Adr		;HL->sector in trkBuf
	jmp	moveSec		;move (dmaAddr) to sector and exit

;--------------------------------------------------------------
; sec2Adr - Return HL with address of the sector buffer within
;    trkBuf for secNum. This routine is hard coded for a sector
;    length of 128, the SECLEN equate is ignored.
; On Entry:
;    secNum = requested sector number (0-39)
; On Exit:
;    HL->sector within trkBuf
; Clobbers B,C,H,L
;--------------------------------------------------------------
sec2Adr	lda	secNum		;A=sector number
	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
	
	lxi	h,trkBuf	;compute HL=trkBuf+BC
	dad	b
	ret

;----------------------------------------------------------------------------
; readTrk- read full track into track buffer if the requested 
;    drive (drvNum) or track (trkNum) does not match the buffered
;    drive (bfDrive) or buffered track (bfTrack). 
;
; On Entry
;    drvNum = drive to read
;    trkNum = track to read
;    bfDrive = drive from which trkBuf was filled
;    bfTrack = track from which trkBuf was filled
;
; On Exit
;    trkNum on drvNum read into trkBuf
;    Zero true if track read or already there, zero false otherwise
;    Clobbers all
;----------------------------------------------------------------------------
readTrk	lhld	bfDrive		;L=buffered drive, H=buffered track

	lda	drvNum		;A=requested drive
	cmp	l		;same drive buffered?
	jnz	rtNew		;drive doesn't match, need a new buffer

	lda	trkNum		;A=requested track
	cmp	h		;same track buffered?
	rz			;yes, already have this buffer

rtNew	mvi	a,UNDEF		;invalidate buffered data
	sta	bfDrive	
	call	dSelDrv		;select drive, load head
	mvi	e,1		;E=re-seek counter
	
rtRtry	call	dSeek		;seek to trkNum (disables interrupts)
	rnz			;a restore was required and failed

	lda	wrtNoRd		;writing without pre-read of track?
	ora	a
	jnz	rtForce		;force good status and exit without read

	mvi	d,RDTRIES	;D=count of read tries
	
rtLoop	call	dNxtSec		;wait for index pulse, HL->trkBuf
	jnz	rtRead		;have index (inverted error status)
	
	inr	a		;force non-zero status
	ret			;return with error asserted

rtRead	call	dRead		;read the track
	jnz	rtError		;read error

rtForce	xra	a		;force good status
	lhld	drvNum		;L=drvNum, H=trkNum
	shld	bfDrive		;set the buffered drive and track values
	ret			;exit with Z flag true
	
; rtError - A read error occured. Retry number of reads specified in D. Reseek
;    to track zero once when D gets to zero, then try RDTRIES time one more time.

rtError	dcr	d		;decrement retry count
	jnz	rtLoop		;not zero, try again

	dcr	e		;decrement seek retry counter
	rnz			;re-seeks done, we're finished

	call	dRestor		;restore to track zero
	jz	rtRtry		;start over after the re-seek

	ret			;exit with error

;----------------------------------------------------------------------------
; ckFlush - 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 or track number are different than the
;    buffered track.
;
; On Entry
;    Drive still selected and on same track as in trkBuf
;    drvNum, trkNum updated for the new I/O call
;
; On Exit
;    Zero true for no error, zero false if write error occured
;    Clobbers all
;----------------------------------------------------------------------------
ckFlush	lda	bfDirty		;see if track buffer is dirty
	ora	a
	rz			;no, exit with Z set

	lhld	bfDrive		;L=buffered drive, H=buffered track
	lda	drvNum		;A=requested drive number
	cmp	l		;same drive?
	jnz	wrtTrk		;no, flush

	lda	trkNum		;A=requested track number
	cmp	h		;same track?
	rz			;yes, no need to flush

wrtTrk	di			;disable interrupts
	xra	a		;clear buffer dirty flag
	sta	bfDirty
	mvi	a,WRTRIES	;initialize write retry count
	sta	wrRetry

	lda	trkNum		;A = track number
	ora	a		;clear carry
	rar			;A=cylinder number
	cpi	43		;0-42 or 43-76?
	mvi	e,cWRTEN	;assume HCS not asserted (0-42)
	jc	wtLoop		;0-42 normal head current
	mvi	e,cWRTEN+cHCSON	;43-76 set low head current

wtLoop	call	dNxtSec		;wait for next sector, HL->trkBuf
	call	dWrite		;write the track

; If the verify flag is set in the mode byte, verify the track
;    just written.

	lda	mode		;verifying writes?
	ani	fWRTVFY
	rz			;no, exit

	mvi	d,RDTRIES	;d=read retry count
	
vfyLoop	call	dNxtSec		;wait for index pulse
	call	dVerify		;verify the sector
	rz			;success, exit

	dcr	d		;decrement verify retry counter
	jnz	vfyLoop
	
	lxi	h,wrRetry	;HL->write retry counter
	dcr	m		;decrement write retry counter
	jnz	wtloop
	
	call	dispMsg
	db	CR,LF,'Delayed Write Error', CR, LF, 0

	inr	a		;dispMsg returns zero
	ret			;exit with non-zero

;-------------------------------------------------------------------
; invFlush - 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, so the conIn function calls this
;    subroutine. Even if not flushed, the track buffer is invalidated
;    to force a track read on the next disk I/O request. This helps
;    catch a disk swap.
;
; On Entry:
;    Same drive is on the same track as buffered in trkBuf
;
; On Exit:
;    Track buffer written if required and invalidated.
;    Head unloaded
;    Z set for no error, Z cleared if write error occured.
; Clobbers all except HL
;-------------------------------------------------------------------
invFlsh	mvi	a,UNDEF		;invalidate track buffer
	sta	bfDrive

	push	h		;save HL in case conIn caller expects it
	call	ckFlush		;flush the track if needed
	pop	h

	ei			;restore interrupts
	mvi	a,cHDUNLD	;unload head
	out	DRVCMD
	ret
	
;------------------------------------------------------------------------------
; dispMsg - Display the null-terminated string following the dispMsg call
;
; On Entry SP->message
;
; On Exit
;    Clobbers C,H,L
;------------------------------------------------------------------------------
dispMsg	pop	h		;HL->string

dmLoop	mov	a,m		;A=next character
	inx	h		;bump string pointer
	ora	a		;null terminator?
	jz	dmExit		;yes, exit

	mov	c,a		;conOut needs character in C
	call	conOut		;send the character
	jmp	dmLoop

dmExit	pchl			;return past message string

;---------------------------------------------------------------------------
;  CON device I/O
;---------------------------------------------------------------------------

; conSt - Console status BIOS entry point. Return FFh if character ready,
;    return zero otherwise. 

conSt	mvi	b,SHFTCON	;B=console IOBYTE alignment shift count
	call	doIo		;call the proper I/O routine below

	dw	sioSt		;TTY use SIO at I/O address 0
	dw	sio1St		;CRT use 2SIO port 1
	dw	batchSt		;BAT indirect through RDR logical device
	dw	sio2St		;UC1 use 2SIO port 2

; conIn - Console input BIOS entry point. Wait for console input and
;    return character in A. Console input is used as an idle indicator
;    and as a good time to flush a dirty track buffer, unload the head, 
;    and invalidate the track buffer.

conIn	call	invFlsh		;invalidate and flush track buffer
	
	mvi	b,SHFTCON	;B=console IOBYTE alignment shift count
	mvi	c,07Fh		;C=AND mask for input byte
	call	doIo		;call the proper I/O routine below

	dw	sioIn		;TTY use SIO at I/O address 0
	dw	sio1In		;CRT use 2SIO port 1
	dw	reader		;BAT indirect through RDR logical device
	dw	sio2In		;UC1 use 2SIO port 2

; conOut - Console output BIOS entry point. Output the character in C.
;   If the sndNull flags is true (i.e., it was determined during cold start
;   that the console is a Teletype), then follow CR with a null.

conOut	call	doConO		;send the character passed in C

	lda	sndNull		;sending null for CR?
	ora	a
	rz			;no, exit
	
	mov	a,c		;was character a CR?
	sui	CR
	rnz			;no, exit
	
	mov	c,a		;put a zero in C and send it

doConO	mvi	b,SHFTCON	;B=console IOBYTE alignment shift count
	call	doIo		;call the proper I/O routine below

	dw	sioOut		;TTY use SIO at I/O address 0
	dw	sio1Out		;CRT use 2SIO port 1
	dw	list		;BAT indirect through LST logical device
	dw	sio2Out		;UC1 use 2SIO port 2

;---------------------------------------------------------------------------
; RDR device - Reader input character BIOS entry point. Return in A
;---------------------------------------------------------------------------
reader	mvi	b,SHFTRDR	;B=reader IOBYTE alignment shift count
	mvi	c,0FFh		;C=AND mask for input byte
	call	doIo		;call the proper I/O routine below

	dw	sioIn		;TTY SIO at I/O address 0
	dw	sio1In		;PTR use 2SIO port 1
	dw	acrIn		;UR1 use cassette port (SIO 6/7)
	dw	sio2In		;UR2 use 2SIO port 2 

;---------------------------------------------------------------------------
; PUN device - Punch output character BIOS entry point. Byte to send in C.
;---------------------------------------------------------------------------
punch	mvi	b,SHFTPUN	;B=punch IOBYTE alignment shift count
	call	doIo		;call the proper I/O routine below

	dw	sioOut		;TTY SIO at I/O address 0
	dw	sio1Out		;PTP use 2SIO port 1
	dw	acrOut		;UP1 use cassette port (SIO 6/7)
	dw	sio2Out		;UP2 use 2SIO port 2

;---------------------------------------------------------------------------
;  LST device I/O
;---------------------------------------------------------------------------

; listSt - List output test BIOS entry point. Return 0FFh if list device
;    is ready, 0 if not ready

listSt	mvi	b,SHFTLST	;B=list IOBYTE alignment shift count
	call	doIo		;call the proper I/O routine below

	dw	sioOSt		;TTY use SIO at I/O address 0
	dw	sio1OSt		;CRT use 2SIO port 1
	dw	lpcOSt		;LPT use LPC board
	dw	sio2OSt		;UL1 use 2SIO port 2

; list - List output character BIOS entry point. Send the character
;    passed in C out the list port

list	mvi	b,SHFTLST	;B=list IOBYTE alignment shift count
	call	doIo		;call the proper I/O routine below

	dw	sioOut		;TTY use SIO at I/O address 0
	dw	sio1Out		;CRT use 2SIO port 1
	dw	lpcOut		;LPT use LPC board
	dw	sio2Out		;UL1 use 2SIO port 2

;---------------------------------------------------------------------------
;  BAT device I/O - The BAT device is assigned as a physical device
;     for CON. However, BAT is actually a logical device that uses
;     whatever RDR is assigned to for input and whatever LST is assigned
;     to for output. The only jump table that does not already exist
;     is the input test routine for the reader devices
;---------------------------------------------------------------------------

; batchSt - BAT device input status

batchSt	mvi	b,SHFTRDR	;BAT in uses RDR device
	call	doIo		;call the proper I/O routine below

	dw	siost		;TTY test SIO at I/O address 0	
	dw	sio1St		;RDR test 2SIO port 1
	dw	acrSt		;UR1 test cassette port (SIO 6/7)
	dw	sio2St		;UR2 test 2SIO port 2

;---------------------------------------------------------------------------
; doIo - Perform physical I/O device based on the IOBYTE. The value passed
;    in B contains the left-shift count to align the calling device's IOBYTE
;    field into bits 2 and 1 (not 1 and 0). Preserves H,L
;--------------------------------------------------------------------------- 
doIo	lda	IOBYTE		;A=CP/M IO BYTE

ioAlign	rlc			;rotate into bits 2 and 1
	dcr	b
	jnz	ioAlign

	ani	6		;get the 2 bit field alone

; 2 bit field aligned in bits 2 and 1. Index into jump table of caller.

	xthl			;hl=table address from stack, save hl

	mov	e,a		;form DE=table offset
	mvi	d,0
	dad	d		;HL->address in table based on IOBYTE

	mov	e,m		;E=lsb of where to jump
	inx	h
	mov	d,m		;D=msb of where to jump
	xchg			;HL->where to jump

	xthl			;jump address on stack, restore HL
	ret			;jump to address specified in table

;---------------------------------------------------------------------------
; Input status routines
;---------------------------------------------------------------------------

; sio1St - Console status routine for 2SIO port 1. Return FFh 
;    if character ready, return zero otherwise. 

sio1St	in	SIO1CTL		;read 2SIO #1 status/control register
	ani	SIORDRF		;data present?
	rz			;no, return zero

	mvi	a,0FFh		;else, return FF
	ret

; sio2St - Console status routine for 2SIO port 2. Return 0ff 
;    if character ready, return zero otherwise. 

sio2St	in	SIO2CTL		;read 2SIO #2 status/control register
	ani	SIORDRF		;data present?
	rz			;no, return zero

	mvi	a,0FFh		;else, return FF
	ret

; sioSt - Console status routine for SIO at I/O address 0. Return FFh 
;    if character ready, return zero otherwise. 

sioSt	in	SIOCTL		;read SIO status/control register
	xri	0FFh		;convert to positive logic
	ani	SIORCV		;data present?
	rz			;no, return zero

	mvi	a,0FFh		;else, return FF
	ret

; acrSt - Console status routine for ACR status (could get called through BAT)

acrSt	in	ACRCTL		;read ACR (SIO) control register
	xri	0FFh		;convert to positive logic
	ani	SIORCV		;data present?
	rz			;no, return zero

	mvi	a,0FFh		;else, return FF
	ret

;---------------------------------------------------------------------------
; Character input routines
;---------------------------------------------------------------------------

; sio1In - input character from 1st port on a 2SIO board and return in A.
;    C contains AND mask.

sio1In	call	sio1St		;wait for character
	jz	sio1In		;no, loop until data available

	in	SIO1DAT		;read the character
	ana	c		;possibly clear msbit
	ret

; sio2In - input character from 2nd port on a 2SIO board and return in A
;    C contains AND mask.

sio2In	call	sio2St		;wait for character
	jz	sio2In		;no, loop until data available
	
	in	SIO2DAT		;read the character
	ana	c		;possibly clear msbit
	ret

; sioIn - input character from SIO board at address 0/1
;    C contains AND mask.

sioIn	call	sioSt		;wait for character
	jz	sioIn		;no, loop until data available
	
	in	SIODAT		;read the character
	ana	c		;possibly clear msbit
	ret

; acrIn - input character from ACR SIO board at address 6/7

acrIn	call	acrSt		;wait for character
	jz	acrIn		;no, loop until data available
	
	in	ACRDAT		;read the character
	ret

;----------------------------------------------------------------------------
; Output status routines
;----------------------------------------------------------------------------

; sio1OSt - Return FFh if 2SIO Port #1 is ready, 0 if not ready

sio1OSt	in	SIO1CTL		;read 2SIO #1 status/control register
	ani	SIOTDRE		;0=busy
	rz			;not ready, return 0

	mvi	a,0FFh		;else, return FF
	ret

; sio2OSt - Return FFh if 2SIO Port #1 is ready, 0 if not ready

sio2OSt	in	SIO2CTL		;read 2SIO #2 status/control register
	ani	SIOTDRE		;0=busy
	rz			;not ready, return 0

	mvi	a,0FFh		;else, return FF
	ret

; lpcOst - Return FFh if 88-LPC is ready, 0 if not ready

lpcOSt	in	LPCCTL		;88-LPC board at port 2,3
	ani	LPCBSY		;0=busy
	rz			;not ready, return 0

	mvi	a,0FFh		;else, return FF
	ret

; sioOSt - Return FFh if SIO at 0/1 is ready, 0 if not ready
 
sioOSt	in	SIOCTL		;read SIO status/control register
	xri	0FFh		;convert to positive logic
	ani	SIOXMT		;0=busy
	rz			;not ready, return 0

	mvi	a,0FFh		;else, return FF
	ret
	
;---------------------------------------------------------------------------
; Character output routines
;---------------------------------------------------------------------------

; sio1Out - Transmit byte through 2SIO port 1. Byte to send in C. Detect
;   110 baud teletype during cold start and set sndNull flag true.

sio1Out	lxi	d,0		;init inter-character timer

sio1Wt	in	SIO1CTL		;(10) read 2SIO #1 status/control register
	ani	SIOTDRE		;(7) transmit data register empty?
	jnz	sio1Rdy		;(10) yes, ready for next character

	inx	d		;(5) count 21us tics
	jmp	sio1Wt		;(10) keep waiting for ready

; If still in cold start, then see if the inter-character timer indicates
;    we're connected to a 110 baud Teletype. If so, set the sndNull
;    flag to true

sio1Rdy	lda	coldSt		;A=FF during cold start, 0 otherwise
	ana	d		;A=D during cold start, 0 otherwise
	cpi	11		;if D (MSB of counter) < 11, not 110 baud
	jc	sio1Xmt		;not 110 baud

	sta	sndNull		;otherwise, set send null flag true

; sio1Xmt - send the character in C. 

sio1Xmt	mov	a,c		;get character to xmit
	out	SIO1DAT		;send it
	ret

;---------------------------------------------------------------------------
; sio2Out - Transmit byte through 2SIO port 2. Byte to send in C. Detect
;   110 baud teletype during cold start and set sndNull flag true.

sio2Out	lxi	d,0		;init inter-character timer

sio2Wt	in	SIO2CTL		;read 2SIO #2 status/control register
	ani	SIOTDRE		;transmit data register empty?
	jnz	sio2Rdy		;yes, ready for next character

	inx	d		;count 21us tics
	jmp	sio2Wt		;keep waiting for ready

; if still in cold start, then see if the inter-character timer indicates
;    we're connected to a 110 baud Teletype. If so, set the sndNull
;    flag to true

sio2Rdy	lda	coldSt		;A=FF during cold start, 0 otherwise
	ana	d		;A=D during cold start, 0 otherwise
	cpi	11		;if D (MSB of counter) < 11, not 110 baud
	jc	sio2Xmt		;not 110 baud

	sta	sndNull		;otherwise, set send null flag true

; sio2Xmt - send the character in C. 

sio2Xmt	mov	a,c		;get character to xmit
	out	SIO2DAT		;send it
	ret

;---------------------------------------------------------------------------
; sioOut - Transmit byte through SIO at address 0/1. Byte to send in C. 
;   Detect 110 baud teletype during cold start.

sioOut	lxi	d,0		;init inter-character timer

sioWt	in	SIOCTL		;read SIO control register
	ani	SIOXMT		;transmit data register empty?
	jz	sioRdy		;yes, ready for next character

	inx	d		;count 21us tics
	jmp	sioWt		;keep waiting for ready

; if still in cold start, then see if the inter-character timer indicates
;    we're connected to a 110 baud Teletype. If so, set the sndNull
;    flag to true

sioRdy	lda	coldSt		;A=FF during cold start, 0 otherwise
	ana	d		;A=D during cold start, 0 otherwise
	cpi	11		;if D (MSB of counter) < 11, not 110 baud
	jc	sioSnd		;not 110 baud

	sta	sndNull		;otherwise, set send null flag true

; sioSnd - send the character in C. 

sioSnd	mov	a,c		;get character to xmit
	out	SIODAT		;send it
	ret

;---------------------------------------------------------------------------
; acrOut - Transmit byte through ACR SIO at address 6/7. Byte to send in C.

acrOut	in	ACRCTL		;read ACR (SIO) control register
	ani	SIOXMT		;transmit data register empty?
	jnz	acrOut		;not yet, keep waiting

	mov	a,c		;get character to xmit
	out	ACRDAT		;send it
	ret

;---------------------------------------------------------------------------
; lpcOut - Transmit byte through 88-LPC board.

lpcOut	call	lpcOSt		;wait for printer		
	jz	lpcOut

	mov	a,c
	ani	07Fh		;force bit 7 to zero
	out	LPCDAT		;output the character
	ret

;-----------------------------------------------------------------------------
; serInit - Initialize serial I/O ports and IOBYTE
;-----------------------------------------------------------------------------
serInit	mvi	a,03h		;reset 2SIO ports
	out	SIO1CTL
	out	SIO2CTL

	mvi	a,11h		;select 8N2
	out	SIO1CTL
	out	SIO2CTL

	lda	dIoByte		;initialize IOBYTE with default
	sta	IOBYTE

	ret

;****************************************************************************
;
; Altair disk I/O routines
;
;****************************************************************************

;---------------------------------------------------------------------------
; dSelDrv - Select the drive specified in drvNum and load the head.
;    Save the track (curTrk) the current drive is on into the track table,
;    load curTrk for the new drive from the track table.
;
; On Entry
;    drvNum = drive to be selected
;    curDrv = drive currently selected
;    curTrk = track the current drive is on
;
; On Exit
;    Drive selected and head loaded
;    curDrv = drvNum
;    curTrk = track the newly selected drive is on
;    Zero true for success, zero false for error
;    Clobbers all
;---------------------------------------------------------------------------
dSelDrv	lxi	h,curDrv	;HL->currently selected drive
	mov	e,m		;E=currently selected drive
	
	lda	drvNum		;A=desired drive
	mov	c,a		;C=desired drive
	
	cmp	e		;same or new drive requested?
	jnz	dNewDrv		;different drive requested

	in	DRVSTAT		;drive still selected and enabled?
	ani	sDSKEN
	jz	dSelExt		;yes, load head and exit

	jmp	dReSel		;otherwise, re-select the drive
	
; Changing drives. Save the current track (curTrk) for the current
;    drive to the track table, then load curTrk for the new drive
;    from the track table

dNewDrv	mov	m,c		;save new drive in curDrv
	
	mvi	d,0		;form 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

	lxi	h,trkTbl	;HL->track table
	mov	e,c		;DE=offset in trkTbl for new drive
	dad	d		;HL->trkTbl for new drive
	mov	a,m		;A=track new drive is on
	sta	curTrk		;update current track

	mvi	a,cDSLCT	;deselect a possibly attached drive
	out	DRVSLCT

dReSel	mov	a,c		;A=drive to select
	out	DRVSLCT		;select it

dSelExt	mvi	a,cHDLOAD	;issue head load command and exit
	out	DRVCMD
	ret

;---------------------------------------------------------------------------
; dSeek - Seek to track in trkNum. The cylinder to step to is 1/2 trkNum
;    with even tracks on the bottom and odd tracks on the top. This is
;    true until track 144 is reached (cylinder 72). For tracks 144-148,
;    only the bottom side is written and correspond to cylinders 72-76.
;
; On Entry
;    Drive selected and ready
;    trkNum = desired track
;    curTrk = current track, UNDEF means we don't know
;
; On Exit
;    curTrk = trkNum
;    trkNum given to the FDC
;    Interrupts disabled
;    Zero true for success, zero false for failure
;	(failure is a failed restore to track zero)
;    Clobbers A,B,C,H,L
;---------------------------------------------------------------------------
dSeek	di			;disable interrupts
	lda	curTrk		;A=current track
	cpi	UNDEF		;valid track number?
	jnz	dTrkVld		;yes, track is valid

	call	dRestor		;otherwise, restore to track zero
	rnz			;exit if restore failed

dTrkVld	mov	c,a		;C=current track
	lda	trkNum		;A=requested track
	sta	curTrk		;save as new current track
	out	DRVTRK		;give track number to FDC+
	cmp	c		;requested=current track?
	rz			;yes, exit
	
; Put proper cylinder for the new track (trkNum) in B

	cpi	ENDTOP		;reached topside track limit?
	jc	dsDiv1		;no, go compute cylinder=track/2
	sui	ENDTOP/2	;convert track to cylinder
	mov	b,a		;B=requested cylinder
	jmp	dsDoCur		;go do curTrk
	
dsDiv1	ora	a		;clear carry for rotate
	rar			;A=requested cylinder
	mov	b,a		;B=requested cylinder

; Get proper cylinder for previous track (curTrk)

dsDoCur	mov	a,c		;A=current track
	cpi	ENDTOP		;reached topside track limit?
	jc	dsDiv2		;no, go compute cylinder=track/2
	sui	ENDTOP/2	;convert track to cylinder
	jmp	dSubCyl		;go subtract cylinders

dsDiv2	ora	a		;clear carry for rotate
	rar			;A=current cylinder
	
; Subtract previous cylinder from new cylinder and compute step count
	
dSubCyl	sub	b		;A=current-requested
	jz	dsSide		;same cylinder, different side

	mvi	b,cSTEPO	;B=step out (assume current>requested)
	jnc	dDoSeek		;it is
	
	mvi	b,cSTEPI	;B=step in (current<requested)
	cma			;compute A=-A
	inr	a

dDoSeek	mov	c,a		;C=number of steps
	call	dStep		;do the seek
	xra	a		;return zero
	ret

; dsSide- Already on the correct cylinder but different track. This
;    means the side select will change. Delay 100us for the side
;    select to settle

dsSide	mvi	a,1		;delay 1x100us
	jmp	delay		;delay and exit with zero status

;---------------------------------------------------------------------------
; dRestor - Restore to track 0 and then delay to ensure any subsequent
;    seek meets the minimum direction change period.
;
; On Entry
;    Drive selected and ready
;
; On Exit
;    If successful
;       curTrk = 0
;	A=0
;	Zero true
;    else
;       curTrk = UNDEF
;       Zero false
;    Clobbers A,B,C
;---------------------------------------------------------------------------
dRestor	xra	a
	sta	curTrk		;set current drive track to zero

	in	DRVSTAT		;test for track 0
	ani	sTRACK0
	rz			;at track 0, exit

; Step in three tracks, then step out to track 0

	mvi	c,3		;C=step in 3 tracks
	mvi	b,cSTEPI	;B=step in command
	call	dStep		;do 3 steps in

	mvi	a,200		;20ms direction change delay
	call	delay

; Seek back out until track 0 detected

	mvi	c,84		;C=maximum number of step outs to try
	mvi	b,cSTEPO	;B=step out command
	call	dStep		;step until track 0 detected
	jnz	drFail		;restore failed

	mvi	a,200		;20ms direction change delay
	call	delay
	ret			;return with zero

drFail	mvi	a,UNDEF		;restore failed, track is still undefined
	sta	curTrk
	ret			;return with non-zero

;---------------------------------------------------------------------------
; dStep - Step head number of steps in C, direction command in B.
;   If track zero is detected during a step-out operation, curTrk
;   is forced to zero and zero is returned in A.
;
; On Entry
;    C = number of steps > 0
;    B = step command (cSTEPO or cSTEPI)
;
; On Exit
;    If stepping out and track zero hit
;	curTrk = 0
;	A = 0
;	Zero true
;    else
;	Zero false
;    Clobbers A,B,C
;---------------------------------------------------------------------------
dStep	in	DRVSTAT		;loop until OK to move the head 
	ani	sMOVEOK
	jnz	dStep

	in	DRVSTAT		;see if we are at track 0
	ani	sTRACK0		;at track zero?
	jnz	dStep1		;no, go on

	mov	a,b		;stepping out?
	sbi	cSTEPO
	jz	dStep0		;yes, we've hit track 0

dStep1	mov	a,b		;A=step command and direction
	out	DRVCMD		;issue the step	

	dcr	c		;decrement step counter
	jnz	dStep		;loop until count reaches zero

	inr	c		;force non-zero
	ret

dStep0	sta	curTrk		;curTrk=0
	ret			;return with zero true and A=0

;---------------------------------------------------------------------------
; dRead - Read a track (one sector per track)
;
; On Entry
;    Drive is selected, sector true just detected
;    HL->track buffer
;
; On Exit
;    Track read to (HL)
;    A=I/O status byte from FDC+
;    Zero true if no error, false otherwise
;    Clobbers B,C,H,L
;---------------------------------------------------------------------------
dRead	lxi	b,TRKLEN/2	;BC=transfer counter
	mvi	a,cREADEN	;issue read request
	out	DRVCMD

drWait	in	DRVSTAT		;wait for ok to read data
	ora	a
	jm	drWait

	in	DRVDATA		;dummy read to start the transfer

; drFifo - Read data without handshake. Data should be read slower than
;    the physical transfer rate of 16us/byte. This loop averages 17us/byte.

drFifo	in	DRVDATA		;(10) get and store a byte
	mov	m,a		;(7)
	inx	h		;(5)

	in	DRVDATA		;(10) get and store abyte
	mov	m,a		;(7)
	inx	h		;(5)

	dcx	b		;(5) decrement transfer counter
	mov	a,b		;(5)
	ora	c		;(4)
	jnz	drFifo		;(10)

	in	DRVIOS		;get the I/O status
	ani	sERRMSK		;test the error bits
	ret

;---------------------------------------------------------------------------
; dWrite - Write a track (one sector per track)
;
; On Entry
;    E=Write command for FDC
;    Drive is selected, sector true just detected
;    HL->sector buffer to write
;
; On Exit
;   Zero status true
;   Clobbers B,C,H,L
;---------------------------------------------------------------------------
dWrite	mov	a,e		;A=proper write command
	out	DRVCMD		;give to FDC
	lxi	b,TRKLEN/4	;BC=transfer counter

dwWait	in	DRVSTAT		;wait for ok to send data
	rrc
	jc	dwWait

; dwFifo - Write data without handshake. Data should be written faster than
;    the physical transfer rate of 16us/byte. This loop averages 14us/byte.
	
dwFifo	mov	a,m		;(7) get next byte
	out	DRVDATA		;(10) give to controller
	inx	h		;(5) point to next byte

	mov	a,m		;(7) get next byte
	out	DRVDATA		;(10) give to controller
	inx	h		;(5) point to next byte

	mov	a,m		;(7) get next byte
	out	DRVDATA		;(10) give to controller
	inx	h		;(5) point to next byte

	mov	a,m		;(7) get next byte
	out	DRVDATA		;(10) give to controller
	inx	h		;(5) point to next byte

	dcx	b		;(5) decrement transfer counter
	mov	a,b		;(5)
	ora	c		;(4)
	jnz	dwFifo		;(10)
	
; Wait for the write to complete, then wait out the 600us trim erase period
;    for tunnel erase heads.

dwStat	in	DRVIOS		;wait for the write to complete
	ani	sDONE		;test for transfer done bit (no hurry)
	jz	dwStat

	mvi	a,6		;delay 600us for tunnel trim erase
	jmp	delay		;do delay and return

;---------------------------------------------------------------------------
; dVerify - Verify track by confirming checksum
;
; On Entry
;    Drive is selected, sector true just detected
;
; On Exit
;    A=I/O status byte from FDC+
;    Zero true if no error, false otherwise
;    Clobbers B,C
;---------------------------------------------------------------------------
dVerify	lxi	b,TRKLEN	;BC=transfer counter
	mvi	a,cREADEN	;issue read request
	out	DRVCMD

dvWait	in	DRVSTAT		;wait for ok to read data
	ora	a
	jm	dvWait
	
	in	DRVDATA		;dummy read to start the transfer

; dvFifo - Read data without handshake. Data should be read slower than
;    the physical transfer rate of 16us/byte. This loop averages 17us/byte.

dvFifo	in	DRVDATA		;(10) get and store a byte
	dcx	b		;(5) decrement transfer counter
	mov	a,b		;(5)
	ora	c		;(4)
	jnz	dvFifo		;(10)

	in	DRVIOS		;get the I/O status
	ani	sERRMSK		;test the error bits
	ret

;---------------------------------------------------------------------------
; dNxtSec - Wait for the index pulse for up to 1.4s, point HL to trkBuf
;
; On Entry
;    Drive is selected, head loaded
;
; On Exit
;    HL->track buffer
;    Returns zero true if sector hunt failed
;    Else, returns zero false when sector true has just occurred
;    Clobbers B,C
;
;---------------------------------------------------------------------------
dNxtSec	lxi	b,0		;1.4s timeout

dnLoop	dcx	b		;decrement timeout
	mov	a,b		;reach zero?
	ora	c
	rz			;yes, exit with error (z true)
	
	in	DRVSEC		;read sector position register
	rar			;wait for sector true (0=true)
	jc	dnLoop
	
	lxi	h,trkBuf	;HL->track buffer
	ret			;z is false here

;----------------------------------------------------------------------------
; delay - Delay for A x 100us
;
; On Entry
;    A = 100us increments to delay
;
; On Exit
;    A=0
;    Zero true
;    Clobbers A,B
;----------------------------------------------------------------------------
delay	mvi	b,(200/20)	;20 cycles in the loop below

delayLp	mov	b,b		;(5) delay 5 cycles
	dcr	b		;(5)
	jnz	delayLp		;(10)

	dcr	a		;decrement 100us counter
	jnz	delay
	ret


;******************************************************************************
;
;   BIOS Data Area
;
;******************************************************************************
	
;-----------------------------------------------------------------------------
; dpHead - disk parameter header for each drive
;-----------------------------------------------------------------------------
dpHead	dw	0,0,0,0,dirBuf,dpb,csv0,alv0
	dw	0,0,0,0,dirBuf,dpb,csv1,alv1
	dw	0,0,0,0,dirBuf,dpb,csv2,alv2
	dw	0,0,0,0,dirBuf,dpb,csv3,alv3

;-----------------------------------------------------------------------------
; dpb - disk parameter block. This table gives a block size of 4096 bytes
;   and 128 directory entries. 
;-----------------------------------------------------------------------------
dpb	dw	NUMSEC		;sectors per track
	db	BSH		;allocation block shift factor
	db	BLM		;data location block mask 
	db	EXM		;extent mask
	dw	DSM		;maximum block number
	dw	DRM		;maximum directory entry number 
	db	AL0,AL1		;directory allocation bit map
	dw	CKS		;directory checksum space
	dw	RESTRK		;reserved tracks for CPM and bootloader

;----------------------------------------------------------------------------
; Initialized data
;----------------------------------------------------------------------------
coldSt	db	0FFh		;FF during cold start, 0 otherwise

curDrv	db	0		;drive number currently selected
curTrk	db	RESTRK-1	;track the selected drive is on
trkTbl	db	UNDEF,UNDEF,UNDEF,UNDEF		;track each drive is on

bfDirty	db	0		;non-zero if buffer has been written to
bfDrive	db	UNDEF		;drive currently in track buffer
bfTrack	db	UNDEF		;currently buffered track (MUST follow bfDrive)

;----------------------------------------------------------------------------
; Non-initialized data
;----------------------------------------------------------------------------
drvNum	ds	1		;drive number from CP/M call
trkNum	ds	1		;track number from CP/M call (MUST follow drvNum)
secNum	ds	1		;sector number from CP/M call
dmaAddr	ds	2		;dma address for disk operations from CP/M call
wrtNoRd	ds	1		;non-zero if writing without track read
wrRetry	ds	1		;write retry counter

;-----------------------------------------------------------------------------
;  Disk scratchpad areas defined in the DPH table
;-----------------------------------------------------------------------------
dirBuf	ds	128		;bdos directory scratchpad
alv0	ds	(DSM/8 + 1)	;allocation vector storage
csv0	ds	CKS		;change disk scratchpad
alv1	ds	(DSM/8 + 1)
csv1	ds	CKS
alv2	ds	(DSM/8 + 1)
csv2	ds	CKS
alv3	ds	(DSM/8 + 1)
csv3	ds	CKS

;-----------------------------------------------------------------------------
; Track buffer. One extra byte is allocated at the start of each sector
;    to flag a bad sector (read failure). This byte is zero if the sector
;    is good, non-zero if the sector is bad.
;-----------------------------------------------------------------------------
trkBuf	ds	TRKLEN

ACTLEN	equ	$-BIOSBAS	;actual length of the BIOS

	end
