;****************************************************************************
;
;   CP/M 2.2 BIOS for Vector Graphic 8" Floppy Controller
;
;	Console I/O and reader input is through the VG monitor PROM. The
;	punch writes through the serial port at 4/5 (default ZCB serial
;	port or port 2 on a Bitstreamer II). The list device goes through
;	the same serial port (4/5) or through the VG monitor as selected
;	by the IOBYTE.
;
;	This is a SSSD BIOS and fits on the first two tracks of a SSSD
;	floppy without requiring a double-density track 1 (the Vector
;	Graphic SSSD BIOS requires track 1 to be double-density to
;	hold the full CP/M boot image).
;
;	This BIOS uses full track buffering on both reads and writes to
;	improve performance. This BIOS performs 20%-70% faster than the
;       non-buffered BIOS for typical disk based operations.
;
;	In order to fit in the boot tracks of a standard SSSD IBM format
;	soft sectored 8" disk, the code/initialized data portion of this
;	BIOS can only be 380h bytes long. This restriction substantially
;	reduces the features that can be provided by this BIOS. The source
;	file has a conditional assembly statement to detect when this size
;	limit is exceeded. Look for "BIOS IS TO BIG ****" as an error
;	statement when assembling. 
;
;	MODE byte and default IOBYTE location for patching:
;		33h, 34h from start of BIOS
;		1FB3h, 1FB4h in memory after MOVCPM
;
;       To patch an update to this BIOS into a CP/M image created and saved
;	after running MOVCPM, use the following commands:
;
;	   A>DDT CPM56.COM    (specify the saved CP/M image file)
;	   -IBIOS.HEX
;	   -Rxxxx      	      (xxxx = the value of the BIAS equate below)
;	   -G0		      (Go zero, not "oh")
;
;	Ver	  Date		     Description
;	---	----------	------------------------------------------
;	1.0 	03/24/2016	Mike Douglas, Original
;
;	1.1	11/02/2017	Mike Douglas
;				Do not write sectors marked as "bad" when
;				flushing the track buffer. Change secTran
;				to use an algorithm instead of a table to
;				save code space.
;
;	1.2	11/15/2017	Mike Douglas
;				Support IOBYTE for the LST device to allow
;				listing to the serial port or screen. Move
;				reader and punch devices to the serial port.
;				To free up code space, a single DPH entry is
;				used for all drives and the ALV and CSV
;				pointers are filled in during drive select.
;
;	1.3	02/28/2019	Mike Douglas
;				Undo DPH update of ver 1.2 and go back to
;				a DPH table per drive. CP/M puts persistent
;				data per drive in the three "scratch" words
;				in the DPH. Due to space constraints, only
;				two drives are now supported instead of
;				four. Invalidate the track buffer during
;				warm boot so that retrys actually re-read
;				the disk. Display "delayed write error" when
;				buffer flush fails due to Read Address Mark
;				error. Change dSeek retry logic to fix bug
;				that could result in a clobbered retry
;				counter in register E. 
;
;	1.4	06/27/2020	Mike Douglas
;				Add 600us delay after write for drives with
;				tunnel erase heads
;
;****************************************************************************

;  CP/M location equates

MEMSIZE	equ	48		;memory size in K bytes

BIOSLEN	equ	1200h		;length of this BIOS
CCPLEN	equ	0800h		;CPM 2.2 fixed length
BDOSLEN	equ	0E00h		;CPM 2.2 fixed length

CCPBASE	equ	MEMSIZE*1024 - BIOSLEN - BDOSLEN - CCPLEN
BDOSENT	equ	(CCPBASE+CCPLEN+6)	 ;entry address of BDOS
BIOSBAS	equ	(CCPBASE+CCPLEN+BDOSLEN) ;base address of this BIOS

BIAS	equ	0980h-CCPBASE

; 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

; CP/M (logical) disk information

NUMDISK	equ	2		;four drives supported
NUMTRK	equ	77		;number of tracks on the disk
NUMSEC	equ	26		;logical (128 byte) sectors per track
SECLEN	equ	128		;CP/M sector length
WBSECS	equ	(CCPLEN+BDOSLEN)/SECLEN   ;warm boot sectors to read

DSM	equ	242		;max block number (243 blocks of 1024 bytes)
DRM	equ	63		;max directory entry number (64 entries) 
CKS	equ	(DRM+1)/4	;directory check space

; Head settle delay, head reverse delay.
;    Based on the drive used, specify the head settle delay and the head
;    direction reversal delay (both are the additional delay after the
;    10ms delay provided by the 1793). On a 2mhz 8080, the delay function
;    is 3.07ms per lsbit. On a 4mhz Z80, 1.535ms per lsbit, 6mhz 1.03ms

HDSETL	equ	10		;>10ms settle after step even at 6Mhz
REVDIR	equ	10		;>10ms when reversing step direction

; Vector Graphic Monitor Console I/O Equates

KEYSTAT	equ	0E003h		;return keyboard status
KEYDATA	equ	0E006h		;return keypress
CRT	equ	0E009h		;display character from A

; Serial port at 4/5 on ZCB or Bitstreamer II. Initialized by VG monitor.

SIOBCR	EQU	05h		;Port B Control Register 
SIOBDR	EQU	04h		;Port B Data Register
XMTRDY	EQU	01h		;transmit ready bit mask
RCVRDY	EQU	02h		;receive data ready bit mask

; Misc equates

NMIVECT	equ	066h		;location of NMI interrupt vector
UNDEF	equ	0FFh		;undefined value
RDTRIES	equ	5		;number of read attempts
CR	equ	13		;ascii for carriage return
LF	equ	10		;ascii for line feed

	org	BIOSBAS
;-----------------------------------------------------------------------------
;  BIOS Entry Jump Table. 
;-----------------------------------------------------------------------------
	jmp	boot		;cold start
wboote	jmp	wBoot		;warm start
	jmp	conSt		;console status
	jmp	conFlsh		;console character in (check for disk flush)
	jmp	conOut		;console character out
	jmp	listOut		;list character out
	jmp	serOut		;punch character out
	jmp	serIn		;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 - BIOS flags. 
;     Must remain at offset 33h from start of BIOS
;---------------------------------------------------------------------------
fCLDCMD	equ	01h		;true = CCP process cmd on cold start
fWRMCMD	equ	02h		;true = CCP process cmd on warm start

mode	db	00h		;don't process command line

;---------------------------------------------------------------------------
;  IOBYTE - at offset 34h from start of BIOS
;
;    CON device (bits 1,0): Always VG monitor
;    RDR device (bits 3,2): Always serial port at 4/5
;    PUN device (bits 5,4): Always serial port at 4/5
;    LST device bits (7,6):
;	00 - TTY VG monitor
;	01 - CRT VG monitor
;	10 - LPT serial port at 4/5 (default)
;	11 - UL1 serial port at 4/5
;---------------------------------------------------------------------------
defIob	db	80h		;LST = LPT (serial port at 4/5)
				;PUN = TTY (serial port at 4/5)
				;RDR = TTY (serial port at 4/5)
				;CON = TTY (VG monitor)

;----------------------------------------------------------------------------
; boot - Cold boot BIOS entry. CPM is already loaded in memory. Hand
;    control to the CCP.
;----------------------------------------------------------------------------
boot	lxi	sp,0100h	;init stack pointer

	xra	a
	sta	CDISK		;current CPM disk is drive zero

	lda	defIob		;initialize IOBYTe
	sta	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.4',CR,LF
	db	'VG 8in Floppy',CR,LF+80h

; Determine whether or not execution of a cold start command line is
;    requested, then enter CP/M.

	lda	mode		;see if cold start command line requested
	ani	fCLDCMD
	push	psw		;save result

	jmp	coldCpm		;enter CP/M

;----------------------------------------------------------------
; wBoot - Warm boot BIOS entry. Reload CPM from disk up to, but
;    not including the BIOS. Re-enter CPM after loading.
;----------------------------------------------------------------
wbRetry	sta	bfDrive		;force re-read of disk (A=0FFh)

wBoot	lxi	sp,0100h	;init stack pointer

	mvi	c,0		;boot from drive zero
	call	selDsk		;select drive zero

	lxi	b,WBSECS*256+2	;B=secs to read, C=starting sec
	lxi	h,CCPBASE	;HL->CP/M load address
	xra	a		;starting track = 0

wbTrk	sta	trkNum		;save track number to use

; wbLoop - read sector loop for warm boot

wbLoop	shld	dmaAddr		;set read destination
	call	setSec		;set sector from C

	push	b		;save sector count and number
	call	read		;read next sector
	pop	b
	jnz	wbRetry		;failed read, start over
	
	dcr	b		;decrement total sector count
	jz	entCpm		;warm boot is done, enter CP/M
	
	inr	c		;bump to next sector number
	mvi	a,NUMSEC	;A=number of sectors per track
	cmp	c		;compare NUMSEC - next sector
	jnc	wbLoop		;sector is 26 or lower, go read it
	
	mvi	c,1		;resume with C=sector 1 and...
	mov	a,c		;  A=track 1
	jmp	wbTrk

; entCpm - Enter CPM. Set page zero variables, enter CPM with or without
;   command line based on MODE flags.

entCpm	lda	mode		;see if warm start command line requested
	ani	fWRMCMD
	push	psw		;save result

coldCpm	lxi	h,DEFDMA	;set the default dma address (080h)
	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,wboote	;get the warm boot 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

	pop	psw		;restore command line test
	jz	CCPBASE+3	;enter CCP and don't run command line
	jmp	CCPBASE		;enter CCP with possible cmd line

;---------------------------------------------------------------------------
; conSt - Console status BIOS entry point. Return FF if character ready,
;    return zero otherwise.
;---------------------------------------------------------------------------
conSt	call	KEYSTAT		;VG monitor: keyboard status 
	rz			;nothing there, A=0

stExit	mvi	a,0FFh		;return FF	
	ret

;---------------------------------------------------------------------------
; conIn - Console input byte BIOS entry point. Wait for character
;    and return it
;---------------------------------------------------------------------------
conIn	call	KEYSTAT		;wait for character
	jz	conIn

	jmp	KEYDATA		;VG monitor: input character and return

;---------------------------------------------------------------------------
; conOut - Console output BIOS entry point. Output the character in C
;    through the serial port.
;---------------------------------------------------------------------------
conOut	mov	a,c		;transmit the passed character
	jmp	CRT		;VG monitor: display character and return

;---------------------------------------------------------------------------
; listSt - List status BIOS entry point. Return FF if OK to transmit,
;    return zero otherwise.
;---------------------------------------------------------------------------
listSt	lda	IOBYTE		;A=IOBYTE
	ora	a		;LPT or UL1 selected?
	jp	stExit		;no, return FF = ready

serStat	in	SIOBCR		;else, check serial port status
	ani	XMTRDY		;ready to transmit?
	rz			;no, return with zero in A
	
	jmp	stExit		;exit through stExit in conSt

;---------------------------------------------------------------------------
; listOut - List output BIOS entry point. Output the character in C
;    through the printer serial port.
;---------------------------------------------------------------------------
listOut	lda	IOBYTE		;A=IOBYTE
	ani	0C0h		;get printer bits alone
	cpi	080h		;LPT selected?
	jnz	conOut		;no, list to screen

serOut	call	serStat		;wait for OK to transmit
	jz	serOut

	mov	a,c		;transmit the passed character
	out	SIOBDR		
	ret

;---------------------------------------------------------------------------
; serIn - Read character from serial port
;---------------------------------------------------------------------------
serIn	in	SIOBCR		;A=serial port status
	ani	RCVRDY		;character present?
	jz	serIn		;no, wait

	in	SIOBDR		;A=character from serial port		
	ret			;exit

;----------------------------------------------------------------------------
; 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	lxi	h,0		;init HL=0 for invalid drive request
	mov	a,c		;A=drive number
	cpi	NUMDISK		;valid drive number?
	rnc			;invalid drive number, return with HL=0=error

	lxi	h,fNewSel	;set the "new select" flag
	mov	m,h		;(H is not zero)

	sta	drvNum		;save new drive number
	add	a		;compute disk parameter header address
	add	a		;   as (16*drvNum) + dpHead
	add	a
	add	a		;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

;----------------------------------------------------------------------------
; home - Home BIOS entry. Set trkNum to zero. No drive activity takes place.
;----------------------------------------------------------------------------
home	xra	a		;set trkNum to zero
	sta	trkNum
	ret

;----------------------------------------------------------------------------
; setTrk - Set Track BIOS entry. C contains the desired track number.
;    The track number is saved in trkNum for later use. No actual
;    drive activity takes place.
;----------------------------------------------------------------------------
setTrk	mov	a,c		;save track number passed in C
	sta	trkNum
	ret

;----------------------------------------------------------------------------
; setSec - Set Sector BIOS entry. C contains the desired sector number.
;    The sector number is saved in secNum for later use. No actual
;    drive activity takes place.
;----------------------------------------------------------------------------
setSec	mov	a,c		;save sector number passed in C
	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.
;----------------------------------------------------------------------------
setDma	mov	h,b		;save buffer address passed in BC
	mov	l,c
	shld	dmaAddr
	ret

;----------------------------------------------------------------------------
; secTran - Sector translation BIOS entry. Convert zero-indexed logical 
;    sector number in BC to a sector number in HL based on the translate
;    table passed in DE.
;
;    In order to save a few bytes, this routine performs sector translation
;    with an algorithm rather than a table. The table address passed in DE
;    is ignored - all drives in this BIOS use the same translation.
;
;    The standard 8" SSSD translate table shown here is implemented:
;
;	tranTbl	db	01,07,13,19,25,05,11,17,23,03,09,15,21
;		db	02,08,14,20,26,06,12,18,24,04,10,16,22
;----------------------------------------------------------------------------
secTran	mov	a,c		;A=logical sector 0-25

	add	a		;*2 multiply logical sector by 6
	add	c		;*3
	add	a		;*6

	lxi	h,NUMSEC	;H=0, L=26

	cpi	(NUMSEC/2)*6	;is the input logical sector >= 13?
	cmc			;carry now set if A >= 13
	adc	h		;increments A by 1 if sector >= 13

stMod	sub	l		;do MOD 26 (subtract 26)
	jnc	stMod

	adc	l		;add back last 26 + 1 (carry is set)
	mov	l,a		;HL=translated sector out
	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 = physical sector number to read
;    dmaAddr = address of buffer to read into
; On Exit:
;   If read successful:
;      sector read into (dmaAddr)
;      HL->dmaAddr+SECLEN
;      Z flag = 1
;   if read fails:
;      Z flag = 0
;  Clobbers all (see HL note above)
;----------------------------------------------------------------------------
read	call	setNmi		;set up the NMI interrupt vecctor

	lxi	h,exitDio	;RET will jump to exitDio
	push	h

	call	ckFlush		;see if track buffer should be flushed
	rnz			;track flush error (jump to exitDio)

	call	readTrk		;fill track buffer if needed
	rnz			;track read error (jump to exitDio)
	
; move sector from proper offset in trkBuf to (dmaAddr) unless the sector
;   is flagged as bad.

	call	loadHD		;HL=(dmaAddr), DE->sector buffer in trkBuf

	ldax	d		;check 1st byte (sector status byte)
	ora	a
	rnz			;bad sector (jump to exitDio)

	inx	d		;DE->1st data byte in sector
	jmp	moveD2H		;move sector to (dmaAddr), exit thru exitDio

;----------------------------------------------------------------------------
; 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 = physical sector number to write
;    dmaAddr = address of buffer to write from
; On Exit:
;   If successful:
;      sector written to trkBuf from (dmaAddr)
;      DE->dmaAddr+SECLEN
;      Z flag = 1
;   if write fails:
;      Z flag = 0
;  Clobbers all (see DE note above)
;----------------------------------------------------------------------------
write	call	setNmi		;setup the NMI interrupt vector

	lxi	h,exitDio	;RET will jump to exitDio
	push	h

	call	ckFlush		;see if track buffer should be flushed
	rnz			;error, jump to exitDio

	call	readTrk		;fill track buffer if needed
	rnz			;track read error, jump to exitDio

	call	dChkWp		;write protected?
	rnz			;yes, jump to exitDio

	lxi	h,bfDirty	;set buffer dirty flag true
	mvi	m,0FFh

; move sector from (dmaAddr) to proper offset in trkBuf

	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
	jmp	moveD2H		;move data to trkBuf, exit through exitDio

; exitDio - exit disk I/O. Restore interrupts and properly set A and
;     the zero flag. Note that moveD2H exits with zero status true.

exitDio	call	resNmi		;restore content at NMI vector location

	mvi	a,0		;if zero status is true, return zero
	rz

	inr	a		;return A<>0, Z false
	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). 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:
;    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
;------------------------------------------------------------------------
readTrk	lhld	bfDrive		;L=buffered drive, H=buffered track

	lda	drvNum		;A=requested drive
	cmp	l		;same drive buffered?
	jnz	newBuf		;drive doesn't match, need a new buffer

	lda	trkNum		;A=requested track
	cmp	h		;same track buffered?
	rz			;yes, already have this buffer

; newBuf - new buffer (full track) needed

newBuf	mvi	a,UNDEF		;invalidate buffered data
	sta	bfDrive

	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

;-------------------------------------------------------------------
; 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:
;    drvNum, trkNum updated for the new I/O call
; On Exit:
;    Z set for no error, Z cleared 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 - track buffer needs to be written

wrtTrk	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)
	cz	dWrite		;write the sector if not bad
	jnz	wrtErr		;hard write error

	dcr	d		;decrement sector count
	jnz	wrtLoop

	mvi	a,1		;delay 1ms-6ms for trim erase
	jmp	delay		;do the delay and exit

; 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,'Write Err', CR, LF+80h

	ret			;exit with non-zero status	

;-------------------------------------------------------------------
; conFlsh - The BIOS console input call comes here before control
;    is passed to the conIn routine. 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 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.
;    Z set for no error, Z cleared if write error occured.
; Clobbers none
;-------------------------------------------------------------------
conFlsh	mvi	a,UNDEF		;invalidate track buffer
	sta	bfDrive

	push	h		;preserve HL in case conIn caller expects it
	call	setNmi		;setup NMI interrupt vector
	call	ckFlush		;flush the track if needed
	call	resNmi		;restore content at NMI vector location	
	pop	h		;restore original H
	
	jmp	conIn		;continue on to conIn

;-------------------------------------------------------------------
; setNmi - Set NMI interrupt vector (66h) to POP PSW, RET
; On Entry:
;    NMI vector (66h) has not been set (has CP/M FCB data). Calls to
;    setNmi-resNmi cannot be nested.
; On Exit:
;    Original content of 66h saved, 66h contains a POP PSW followed
;    by a RET
; Clobbers HL
;-------------------------------------------------------------------
setNmi	lhld	NMIVECT		;save what's presently at the NMI vector
	shld	nmiSave
	lxi	h,0C9F1h	;POP PSW and RET
	shld	NMIVECT		;install NMI interrupt routine
	ret

;-------------------------------------------------------------------
; resNmi - Restore content of 66h to original value
; On Entry:
;    NMI vector (66h) contains POP PSW, RET
; On Exit:
;    Original content of 66h restored. Interrupts are enabled (EI)
;    as this is a common place that disk I/O is exiting.
; Clobbers none
;-------------------------------------------------------------------
resNmi	push	h
	lhld	nmiSave		;restore what was at NMI vector
	shld	NMIVECT
	pop	h
	ei
	ret

;----------------------------------------------------------------------------
; loadHD - Return HL=(dmaAddr), DE->sector buffer in trkBuf. 
; On Entry:
;    secNum = sector to read or write
;    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
	call	dSecAdr		;HL->sector within trkBuf (status byte)
	xchg			;DE->sector within trkBuf

	lhld	dmaAddr		;HL=(dmaAddr)
	ret

;------------------------------------------------------------------------------
; 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 (hl)
	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
	
;-----------------------------------------------------------------------
; dispMsg - display null terminated string on console device. This
;    code assume conOut does not clobber HL in order to save two
;    bytes.
; On Entry: HL->message
; On Exit:
; Clobbers C,H,L
;-----------------------------------------------------------------------
dispMsg	pop	h

dmLoop	mov	c,m		;display next character
	call	conOut

	ora	m		;end of message? (MSBit set)
	inx	h
	jp	dmLoop		;no, repeat with next char

	pchl			;return past message string

;--------------------------------------------------------------------
; delay - delay value in A*24*256 cycles.
; On Entry: A = delay constant
; On Exit:
; Clobbers none
;--------------------------------------------------------------------
delay	push	b		;preserve B,C
	mov	b,a		;B=passed delay

dlyLoop	dcx	b		;(5,6) decrement BC
	mov	a,b		;(5,4) test for zero
	ora	c		;(4)
	jnz	dlyLoop		;(10)

	pop	b
	ret


;****************************************************************************
;
; Vector Graphic 8" FDC disk I/O routines (single density assumed)
;
;****************************************************************************

; Controller Equates

FDCBASE	equ	0E0H		;controller base address
FDCCMD	equ	FDCBASE		;1793 command register
FDCSTAT	equ	FDCBASE		;1793 status register
FDCTRK	equ	FDCBASE+1	;1793 track register
FDCSEC	equ	FDCBASE+2	;1793 sector register
FDCDATA	equ	FDCBASE+3	;1793 data in/out register
FDCSEL	equ	FDCBASE+4	;FDC drive select/status port
FDCWAIT	equ	FDCBASE+5	;FDC stall/wait port

FDMSIDE	equ	04h		;selects top side when set in FDCSEL
FDMDEN	equ	08h		;selects double density when set in FDCSEL

; Commands for 1793.

STPRATE	equ	0		;3ms step rate

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 1793 to idle

; Status bits from 1793

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=1793 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 1793 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,fNewSel	;test the "new select" flag
	mov	a,m
	ora	a
	rz			;new select not requested, exit

	mvi	m,0		;clear the new select flag

; New drive select requested. Output the new drive select to the
;    controller.

	inx	h		;HL->drvNum
	mov	a,m		;A=drvNum
	out	FDCSEL		;select the drive

; Reset the 1793 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.

	call	dChkWp		;does a 1793 reset

; Read address mark to find out what track the new drive is on.
;    Update the track register in the 1793 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 1793
	ret			;return with zero true

;--------------------------------------------------------------
; dChkWp - Reset 1793 and return status of the write protect
;    status from the 1793.
; On Entry:
;    Drive selected and ready
; On Exit:
;    Z flag set = not write protected
;    Z flag clear = write protected.
; Clobbers none
;--------------------------------------------------------------
dChkWp	mvi	a,cRESET	;ensure 1793 is idle
	out	FDCCMD
	xthl			;waste some time before checking
	xthl

	in	FDCSTAT		;read 1793 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 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 1793 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 none
;--------------------------------------------------------------
dSeekC	lda	trkNum		;give track to seek to 1793
	out	FDCDATA

	mvi	a,cSEEK		;issue seek command
	out	FDCCMD
	call	dWtComp		;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 none
;--------------------------------------------------------------
dRestor	mvi	a,cRESTOR	;issue restore to track zero
	out	FDCCMD
	call	dWtComp		;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

;--------------------------------------------------------------
; dWtComp - Wait here for NMI to return to caller. The NMI
;    routine aborts the loop and does the return.
; On Entry:
;    A command has been issued to the 1793
; On Exit:
;    1793 has completed the command
; Clobbers none
;--------------------------------------------------------------
dWtComp	in	FDCSEL		;allows NMI to occur
	nop
	jmp	dWtComp

;--------------------------------------------------------------
; 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		;A=read sector command
	out	FDCCMD		;issue command in A

	push	h		;save address of status byte
	inx	h		;HL->first data byte location
	call	dRdSec		;do the data transfer

	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
	out	FDCCMD		;issue command in A
	call	dRdSec		;do the data transfer

	in	FDCSTAT		;get status of the read
	ani	smREAD		;any of these bits is an error

	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

;--------------------------------------------------------------
; dRdSec - Do the physical transfer of data from the disk
;    into memory. When the disk controller completes the
;    command, it generates an NMI interrupt that returns
;    control to the caller of this subroutine.
; On Entry:
;    Read sector or read address command issued to controller
;    HL->sector (or address mark) buffer
; On Exit:
; Clobbers H,L
;--------------------------------------------------------------
dRdSec	in	FDCSEL		;(11) look for DRQ bit
	rrc			;(4)
	jnc	dRdSec		;(10)

	in	FDCDATA		;(11) get the next byte
	mov	m,a		;(7) store in memory
	inx	h		;(5)
	jmp	dRdSec		;(10)

;--------------------------------------------------------------
; 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
	call	dWrSec		;do the data transfer

	in	FDCSTAT		;get status of the write
	ani	smWRITE		;any of these bits is an error
	ret

;--------------------------------------------------------------
; dWrSec - Do the physical transfer of data from memory to
;    the disk. When the disk controller completes the
;    command, it generates an NMI interrupt that returns
;    control to the caller of this subroutine.
; On Entry:
;    Write sector command issued to controller
;    HL->sector (or address mark) buffer
; On Exit:
; Clobbers H,L
;--------------------------------------------------------------
dWrSec	in	FDCSEL		;(11) look for DRQ bit
	rrc			;(4)
	jnc	dWrSec		;(10)

	mov	a,m		;(7) A=next byte to write
	out	FDCDATA		;(10) give to controller
	inx	h		;(5)
	jmp	dWrSec		;(10)

;--------------------------------------------------------------
; 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		;A=zero indexed 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-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

;******************************************************************************
;
;   BIOS Data Area
;
;******************************************************************************


;-----------------------------------------------------------------------------
; dpHead - disk parameter headers for each drive
;-----------------------------------------------------------------------------
dpHead	dw	tranTbl,0,0,0,dirBuf,dpb,csv0,alv0
	dw	tranTbl,0,0,0,dirBuf,dpb,csv1,alv1

;-----------------------------------------------------------------------------
; dpb - disk parameter block. This table gives a block size of 1024 bytes
;   and 64 directory entries. 
;-----------------------------------------------------------------------------
dpb	dw	NUMSEC		;sectors per track
	db	3		;allocation block shift factor (BSH)
	db	07h		;data location block mask (BLM)
	db	0		;extent mask (EXM)
	dw	DSM		;maximum block number (DSM 242)	
	dw	DRM		;maximum directory entry number (DRM 63)
	db	0C0h,0		;AL0, AL1 
	dw	CKS		;CKS=(DRM+1)/4
	dw	2		;reserved tracks for CPM and bootloader

; In order to save a few bytes, secTran implements the sector translation
;    of this table with an algorithm.

tranTbl	equ	$
;	db	01,07,13,19,25,05,11,17,23,03,09,15,21
;	db	02,08,14,20,26,06,12,18,24,04,10,16,22

; Data initialized at cold start

bfDirty	db	0		;non-zero if buffer has been written to
bfDrive	db	UNDEF		;drive currently in track buffer 

;----------------------------------------------------------------------------
;  Driver code size check. Must be less than 380h bytes in length here.
;
;  Note that ASM and MAC actually only check bit 0 of the expression
;  for zero, not the entire value, so the expression below moves the
;  sign bit to bit zero (sign bit set if BIOS is too big).
;----------------------------------------------------------------------------
SZDIFF	equ	380h-($-BIOSBAS)
SZFLAG	equ	(SZDIFF and 8000h) shr 15

	if SZFLAG

  BIOS IS TOO BIG ****

	endif

;----------------------------------------------------------------------------
; Non-initialized data
;----------------------------------------------------------------------------
bfTrack	ds	1		;currently buffered track (MUST FOLLOW bfDrive)

fNewSel	ds	1		;new drive selected flag
drvNum	ds	1		;drive number from CP/M call (MUST FOLLOW fNewSel)
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

nmiSave	ds	2		;page zero data saved from NMI vector at 66h

; Address mark buffer

amBuf	equ	$
amTrk	ds	1		;track
	ds	1
amSec	ds	1		;sector
amSecL	ds	1		;sector length code
amCrc	ds	2		;CRC 
	ds	4		;safety space

; 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	NUMSEC*(SECLEN+1)

;-----------------------------------------------------------------------------
;  Disk scratchpad areas defined in the DPH table. ALV and CSV storage
;  MUST REMAIN in this order since address math is done on them in selDrv.
;-----------------------------------------------------------------------------
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

ACTLEN	equ	$-BIOSBAS	;actual length of the BIOS

	end

