****************************************************************************
;
; CP/M 2.2 BIOS for a Sol-20 or an S-100 computer with Processor 
;    Technology's Subsystem B boards. Altair 8" floppy disk images
;    are served through the Sol-20 or 3P+S serial port. The same PC  
;    based disk server that supports the FDC+ can be used as the server
;    for this version of CP/M (use server version 1.4 or newer).
;
;    Since the maximum baud rate on the Sol-20 and 3P+S is 9600 baud,
;    performance is very sluggish. The same disk layout as used with
;    the original versions of CP/M 1.4 and 2.2 is maintained for full
;    backward and  forward compatibility.
;
;    This version provides full IOBYTE support to allow use of the four
;    SOLOS/CUTER pseudo ports. Search for "IOBYTE" in this source file
;    for details. CUTER/SOLOS is required at C000h.
;
;		Item	  Offset from start of BIOS
;		------	  ----------------------------------
;		MODE	  33h (20B3h in memory after MOVCPM)
;		IOBYTE	  34h (20B4h in memory after MOVCPM)
;
;    Version History
;
;    Ver     Date	  Description
;    ---   ---------	---------------------
;    1.0   11/10/2024	Original, Mike Douglas
;
;--------------------------------------------------------------------------
;
;    To patch changes made to this BIOS into a CP/M image saved from
;    MOVCPM (e.g., CPMxx.COM), use the following commands:
;
;	A>DDT CPMxx.COM
;	-IBIOS.HEX
;	-Rxxxx      (where xxxx = BIAS computed below)
;	-G0	    (Go zero, not "oh")
;	A>SYSGEN
;
;****************************************************************************
TBHIRAM	equ	1		;1=track buffer in high RAM (D000-E200)
MEMSIZE	equ	48		;set memory size in K bytes

;-----------------------------
; CP/M size & locations
;-----------------------------
CCPLEN	equ	0800h		;CPM 2.2 fixed length
BDOSLEN	equ	0E00h		;CPM 2.2 fixed length

 if TBHIRAM
BIOSLEN	equ	600h		;BIOS length with track buffer at D000
 endif

 if not TBHIRAM
BIOSLEN	equ	1800h		;length of this BIOS
 endif

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	0A80h-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 parameters
;-----------------------------
NUMTRK	equ	77		;number of tracks on the disk
NUMSEC	equ	32		;sectors per track
DATATRK	equ	6		;1st data format track
NUMDISK	equ	3		;three drives supported
CSECLEN	equ	128		;CP/M sector length
PSECLEN	equ	137		;physical sector length
TRKLEN	equ	NUMSEC*PSECLEN
WBSSEC	equ	3		;starting sector of CP/M image
WBSECS	equ	(CCPLEN+BDOSLEN)/CSECLEN   ;# warm boot sectors to read

;-----------------------------
; CP/M Disk Parameter Block Values
;-----------------------------
BSH	equ	4		;allocation block shift factor (2K block)
BLM	equ	0Fh		;allocation block mask (2K block)
EXM	equ	0		;extent mask
DSM	equ	149		;max block number (150 blocks of 2K bytes)
DRM	equ	63		;max directory entry number (64 entries) 
AL0	equ	0C0h		;directory allocation block bits byte 0
AL1	equ	0		;directory allocation block bits byte 1
RESTRK	equ	2		;reserved tracks for boot image
CKS	equ	(DRM+1)/4	;directory check space

; System tracks (0-5) sector format

SYSTRK	equ	0		;offset of track number
SYSDATA	equ	3		;offset of 128 byte data payload
SYSSTOP	equ	131		;offset of stop byte (FFh)
SYSCSUM	equ	132		;offset of checksum

; Data tracks (6-76) sector format

DATTRK	equ	0		;offset of track number
DATSEC	equ	1		;offset of sector number
DATCSUM	equ	4		;offset of checksum
DATDATA	equ	7		;offset of 128 byte data payload
DATSTOP	equ	135		;offset of stop byte (FFh)

;-----------------------------
; Server serial port
;-----------------------------
SRVRCTL	equ	00h		;3P+S serial port for disk server
SRVRDAT	equ	01h
SOLSER	equ	0F8h		;serial port address in Sol-20

SRVRRDA	equ	40h		;receive data available bit
SRVRTBE	equ	80h		;transmit buffer empty bit

;-----------------------------
; SOLOS/CUTER entry points
;-----------------------------
AOUT	equ	0C01Ch		;send byte in b out pseudo port in a
AINP	equ	0C022h		;return byte in a from port in a
UIPRT	equ	0C800h		;user defined input routine for port 3
UOPRT	equ	0C802h		;user defined output outine for port 3

;-----------------------------
; 2SIO Serial Board 
;-----------------------------
SIOCTL	equ	10h		;1st port on 2SIO board - control register
SIODATA	equ	11h		;1st port on 2SIO board - data register
SIORDRF	equ	01h		;read data register full flag
SIOTDRE	equ	02h		;transmit data register empty flag

;-----------------------------
; Misc equates
;-----------------------------
RDTRIES	equ	3		;read tries 
WRTRIES	equ	3		;write tries

UNDEF	equ	0FFh		;undefined value
EMPTY	equ	80h		;empty byte in receive buffer
DEL	equ	7Fh		;Sol-20 DEL on keyboard
BS	equ	08h		;ASCII backspace
SOLBS	equ	81h		;makes Sol-20 screen to backspac
CR	equ	0Dh		;ASCII for carriage return
LF	equ	0Ah		;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 flags (offset 33h)
;-----------------------------------------------------------------------------
mode	db	00h		;default mode byte
fCLDCMD	equ	01h		;true = CCP process cmd on cold start
fWRMCMD	equ	02h		;true = CCP process cmd on warm start

;---------------------------------------------------------------------------
;  IOBYTE (offset 34h)
;    Values 0-3 correspond to SOLOS/CUTER pseudo-devices 0-3. The
;    user device (3) is mapped to the first port on an 88-2SIO
;    serial interface board (I/O addresses 10h/11h). Note that the
;    serial port (pseudo device 1) is used as the disk server, so
;    it should not be used for any sort of standard I/O. 
;---------------------------------------------------------------------------
defIob	db	00h		;all devices go to video console

;----------------------------------------------------------------------------
; 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		;init 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.2 v1.0',CR,LF
	db	'SERIAL Altair Floppy',CR,LF,0
				;dispMsg returns with A=0
	sta	CDISK		;current CPM disk is drive zero
	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.
;----------------------------------------------------------------------------
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	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	wBoot		;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.
;
; 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
	mov	a,c		;A=drive number
	cpi	NUMDISK		;verify drive number less than number of disks
	rnc			;invalid drive number, return with HL=0=error
	sta	drvNum		;else, 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

;----------------------------------------------------------------------------
; home - Home BIOS entry. Set trkNum to zero. No drive activity takes place.
;
; On Entry
;
; On Exit
;    trkNum set to zero
;    Clobbers A
;----------------------------------------------------------------------------
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.
;
; 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. Convert zero-indexed logical 
;    sector number in BC to a CPM one-indexed physical sector number in HL
;    based on the translate table passed in DE.
;
; On Entry
;    BC = logical sector number
;    DE->sector translate table for selected drive
;
; On Exit
;    HL = physical sector number
;    Clobbers B,D,E,H,L
;----------------------------------------------------------------------------
secTran	xchg			;HL->translate table
	mvi	b,0		;make sure msb of sector is zero
	dad	b		;HL->translated sector num
	mov	l,m		;L=lsb of sector
	mov	h,b		;H=0=msb of sector
	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-31)
;    dmaAddr = address of buffer to read into
;
; On Exit
;    If read successful
;	sector read into (dmaAddr)
;	HL = (dmaAddr) + CSECLEN
;	A=0, Z flag set true
;    Else
;	A=1, Z flag set false
;    Interrupts enabled
;    Clobbers all
;----------------------------------------------------------------------------
read	call	ckFlush		;flush track buffer if needed
	call	readTrk		;fill track buffer if needed
	jnz	exitDio		;track read error, exit
	
	call	movRead		;move sector to (dmaAddr)

exitDio	mvi	a,0		;if zero is true, return zero
	rz
	inr	a		;else return A<>0, Z false
	ret

;----------------------------------------------------------------------------
; movRead - Move sector data from track buffer to (dmaAddr) for a
;    CPM read request
;
; On Entry
;    trkNum = track to read
;    secNum = sector number to read (0-31)
;    dmaAddr = address of buffer to read into
;
; On Exit
;    sector data moved to (dmaAddr), Z flag set true
;    HL = (dmaAddr) + CSECLEN
;    Clobbers all
;----------------------------------------------------------------------------
movRead	call	altSkew		;secNum to hard sector in A
	call	sec2Adr		;HL->sector in trkBuf
	
	lxi	d,DATDATA	;DE=offset to data portion of sector
	lda	trkNum		;on a data track?
	cpi	DATATRK
	jnc	mrMove		;yes, data track (DE already correct)
	lxi	d,SYSDATA	;else, use system track offset

mrMove	dad	d		;HL->data portion of sector
	xchg			;DE->data portion of sector
	lhld	dmaAddr		;HL->destination for data
	mvi	b,CSECLEN	;B=number of bytes to move
	
mrLoop	ldax	d		;move sector from trkBuf to (dmaAddr) 
	mov	m,a
	inx	h		;increment pointers
	inx	d
	dcr	b		;loop count
	jnz	mrLoop 

	ret			;exit with zero	status

;----------------------------------------------------------------------------
; 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-31)
;    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	call	ckFlush		;flush track buffer if needed
	call	readTrk		;fill track buffer if needed
	jnz	exitDio		;track read error, exit

	call	altSkew		;secNum to hard sector in A
	call	sec2Adr		;HL->sector in trkBuf

	lda	trkNum		;A=track
	ori	80h		;set sync bit
	mov	m,a		;set track in sector
	
	inx	h		;HL->byte after track
	cpi	DATATRK+80h	;on a system or data track?
	jnc	wDatTrk		;data track 6-76

; Create Altair sector for system tracks 0-5

	xra	a		;put 0100h (16 bit) at offset 1,2
	mov	m,a
	inx	h		;HL->offset 2
	inr	a		;A=1
	mov	m,a
	
	inx	h		;HL->128 byte CPM sector in Altair sector
	call	wrtMove		;move (dmaAddr) to sector in trkBuf
	
	mvi	m,0FFh		;offset 131 is stop byte (0FFh)
	
	inx	h		;offset 132 is checksum
	mov	m,b		;store checksum at offset 132
	jmp	wExit		;exit

; wDatTrk- Create Altair sector for tracks 6-76 (mindisk 4-34)

wDatTrk	lda	secNum		;A=sector before Altair skew
	mov	m,a		;store Altair logical sector number
	
	inx	h		;HL->offset 2 in sector
	xra	a		;store zero at offsets 2-6
	mov	m,a		;offset 2
	
	inx	h
	mov	m,a		;zero at offset 3
	
	inx	h
	push	h		;save address of offset 4 = checksum
	
	inx	h
	mov	m,a		;zero at offset 5
	
	inx	h
	mov	m,a		;zero at offset 6
	
	inx	h		;HL->128 byte CPM sector in Altair sector
	call	wrtMove		;move (dmaAddr) to sector in trkBuf

	mvi	m,0FFh		;offset 135 is stop byte (0FFh)
	
	pop	h		;HL->checksum byte in Altair sector
	mov	m,b		;store the checksum
	
; wExit - set dirty flag true, return success status

wExit	mvi	a,0FFh		;set dirty flag true
	sta	bfDirty
	xra	a		;return success status
	ret

;------------------------------------------------------------------------------
; wrtMove - Move sector buffer (128 bytes) from (dmaAddr) to (HL) as part
;   of a CPM write command. Compute checksum on all bytes moved and return
;   the checksum in B.
;
; On Entry
;    HL->destination sector in trkBuf
;    dmaAddr = address of buffer to move from
;
; On Exit
;    128 bytes moved from (dmaAddr) to (HL)
;    HL = HL + 128
;    B = checksum of the 128 bytes moved
;    Clobbers all
;------------------------------------------------------------------------------
wrtMove	xchg			;DE->destination CPM sector in trkBuf
	lhld	dmaAddr		;HL->source buffer
	lxi	b,CSECLEN	;B=checksum (0), C=128 byte count
	
mwLoop	mov	a,m		;move from (HL) to (DE)
	stax	d
	add	b		;add byte to checksum
	mov	b,a
	
	inx	d		;increment both pointers
	inx	h
	dcr	c		;decrement character count
	jnz	mwLoop		;loop until count = 0
	
	xchg			;return with buffer pointer in HL	
	ret
	
;----------------------------------------------------------------------------
; altSkew - Perform Altair skew on the sector number in secNum and return
;    the result in A. The skew is based on the track as:
;
;	Tracks 0-5, secOut = secIn
;	Tracks 6-76, secOut = (secIn * 17) MOD 32
;
;    The skew computation for tracks 6-76 is implemented as:
;	secOut = secIn if secIn is even
;	secOut = secIn XOR 10h if secIn is odd
;
; On Entry
;    trkNum = current track
;    secNum = sector number (0-31)
;
; On Exit
;    A = sector number after Altair skew
;    Clobbers A,B
;----------------------------------------------------------------------------	
altSkew	lda	trkNum		;on a data track?
	cpi	DATATRK
	lda	secNum		;A=unmodified sector number
	rc			;system track, no change to sector
	
	mov	b,a		;save secNum in B
	rrc			;test for even/odd
	mov	a,b		;restore secNum in A
	rnc			;return with secNum if even	
	xri	10h		;else translate as in comments above
	ret

;---------------------------------------------------------------------------
; sec2Adr - Convert hard sector in A to address within trkBuf for
;    the specified sector
;
; On Entry
;    A = Hard sector number
;
; On Exit
;    HL->sector buffer in trkBuf
;    Clobbers A,B,C,H,L
;---------------------------------------------------------------------------
sec2Adr	lxi	h,secAddr  	;HL->sector address table
	mvi	b,0		;form BC=sector*2
	rlc			;A=sector*2 (2 bytes per table entry)
	mov	c,a		;BC=sector*2
	dad	b		;HL->address table entry for passed sector
	
	mov	a,m		;A=lsb of sector buffer address
	inx	h
	mov	h,m		;H=msb of sector buffer address
	mov	l,a		;HL->sector buffer
	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 - New track needs to be read

rtNew	mvi	a,UNDEF		;invalidate buffered data
	sta	bfDrive
	mvi	a,RDTRIES	;init retry counter
	sta	rtryCnt

rtRetry	lda	drvNum		;A=drive to read
	lxi	h,trkNum	;B=track to read
	mov	b,m
	lxi	h,readCmd	;send read command to server
	call	srvrCmd

	lxi	h,trkBuf	;receive track data into trkBuf
	lxi	d,TRKLEN	;length to read
	call	srvrRcv
	jnz	rtErr		;error	

	lhld	drvNum		;L=drvNum, H=trkNum
	shld	bfDrive		;set the buffered drive and track values
	mov	a,l		;save copy of drive number
	sta	bfDrvSv
	ret			;exit with Z flag true

rtErr	call	srb3ms		;discard received bytes until 3ms passes
	jz	rtErr		;   with no data received

	lxi	h,rtryCnt	;retry read if more retries available
	dcr	m
	jnz	rtRetry

	inr	m		;exit with non-zero error status
	ret

;----------------------------------------------------------------------------
; 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	xra	a		;clear buffer dirty flag
	sta	bfDirty
	mvi	a,WRTRIES	;init retry counter
	sta	rtryCnt

wtRetry	lda	bfDrvSv		;A=buffered data drive number
	lxi	h,bfTrack	;B=buffered data track number
	mov	b,m
	lxi	h,wrtCmd	;send write command to server
	call	srvrCmd

	lxi	h,cmdResp	;receive write response
	lxi	d,CMDLEN
	call	srvrRcv
	jnz	wtErr		;error	

	lda	cmdResp		;verify 'WRIT' received
	cpi	'W'
	jnz	wtErr
	lda	cmdResp+1
	cpi	'R'
	jnz	wtErr

	lda	crCode		;A=response code
	ora	a		;zero = 'OK'
	jnz	wtErr

	lxi	h,trkBuf	;send track buffer to write	
	lxi	d,TRKLEN
	call	srvrSnd

	lxi	h,cmdResp	;receive write response
	lxi	d,CMDLEN
	call	srvrRcv
	jnz	wtErr		;error	

	lda	cmdResp		;verify 'WSTA' received
	cpi	'W'
	jnz	wtErr
	lda	cmdResp+1
	cpi	'S'
	jnz	wtErr

	lda	crCode		;A=response code
	ora	a		;zero = 'OK'
	rz

wtErr	call	srb3ms		;discard received bytes until 3ms passes
	jz	wtErr		;   with no data received

	lxi	h,rtryCnt	;decrement the write retry counter
	dcr	m
	jnz	wtRetry		;retry the write

	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	push	h		;preserve h
	lxi	h,bfDrive
	mvi	m,UNDEF		;invalidate the track buffer
	call	ckFlush		;flush the track if needed
	pop	h
	ret

;------------------------------------------------------------------------------
; srvrCmd - Send command pointed to by HL to the serial disk server.
;    Computes and sends the checksum as well.
;
; On Entry:
;    HL->command string to send
;    A=Drive number
;    B=Track number
;
; On Exit:
;
; Clobbers all
;------------------------------------------------------------------------------
srvrCmd	push	h		;preserve command pointer
	rlc			;put drive in upper nibble
	rlc
	rlc
	rlc
	lxi	d,rcDrive-readCmd    ;DE=offset to drive number
	dad	d
	mov	m,a		;store drive number in command

	dcx	h		;move to track number
	mov	m,b		;store track number in command
	
	pop	h		;restore command pointer
	lxi	d,CMDLEN	;DE=length to send
				;fall into srvrSnd

;------------------------------------------------------------------------------
; srvrSnd - Send a data buffer to the serial disk server. Computes and
;    sends the checksum as well.
;
; On Entry:
;    HL points to the buffer to send
;    DE contains the length of the buffer
;
; On Exit:
;
; Clobbers all
;------------------------------------------------------------------------------
srvrSnd	lxi	b,0		;BC=checksum

ssLoop	mov	a,m		;A=byte to send
	add	c		;update checksum LSB
	mov	c,a		
	mvi	a,0
	adc	b		;MSB
	mov	b,a	

	mov	a,m		;A=byte to send
	call	ssByte		;server send byte
	inx	h		;point to next byte
	dcx	d		;decrement byte count
	mov	a,d		;all done?
	ora	e
	jnz	ssLoop		;loop until zero

; Send checksum (2 bytes)

	mov	a,c		;LSB of checksum
	call	ssByte
	mov	a,b		;MSB of checksum
				;fall into ssByte

;-------------------------------------------------------------------
; ssByte - Send a single byte from A to the serial disk server
;-------------------------------------------------------------------
ssByte	push	psw		;preserve char to send
	
ssbLoop	in	SRVRCTL		;loop until OK to send
	ani	SRVRTBE	
	jz	ssbLoop

	pop	psw		;A=byte to send
ssbOut	out	SRVRDAT
	ret
	
;------------------------------------------------------------------------------
; srvrRcv - Receive a data buffer from the serial disk server. Computes and
;    verifies the checksum as well.
;
; On Entry:
;    HL points to the buffer to receive
;    DE contains the length of the buffer
;
; On Exit:
;    Zero true if received without error
;    Zero false for timeout or checksum error
; Clobbers all
;
; srLoop is 181 cycles
;------------------------------------------------------------------------------
srvrRcv	lxi	b,0		;compute checksum in BC

srLoop	call	srByte		;get a byte
	rnz			;timeout error
	mov	m,a		;save byte in the buffer

	add	c		;update checksum
	mov	c,a
	mvi	a,0
	adc	b
	mov	b,a

	inx	h		;increment buffer pointer
	dcx	d		;decrement byte counter
	mov	a,d
	ora	e
	jnz	srLoop		;loop until zero

; Receive and compare 16 bit checksum

	call	srByte		;get LSB of checksum
	rnz			;timeout
	cmp	c		;match?
	rnz			;no
		
	call	srByte		;get MSB of checksum
	rnz			;timeout
	cmp	b		;match?
	ret			;return with status

;-------------------------------------------------------------------
; srByte - Receive a byte from the server with 1 second timeout.
;    Returns zero false for timeout, else true.
;
; srb3ms does the same thing but with a 3ms timeout
;-------------------------------------------------------------------
srb3ms	push	b		;popped later, so must push
	lxi	b,118		;3ms timeout
	jmp	srbLoop

srByte	push	b		;preserve BC
	lxi	b,39216		;1 second (51 cycles, 25.5us per loop)
	
srbLoop	in	SRVRCTL		;(10)
	ani	SRVRRDA		;(7)
	jnz	srbNew		;(10)

	dcx	b		;(5)
	mov	a,b		;(5)
	ora	c		;(4)
	jnz	srbLoop		;(10)

	pop	b
	inr	a		;zero false for timeout
	ret

srbNew	pop	b		;restore BC
	xra	a		;zero true
srbIn	in	SRVRDAT		;get and return the byte
	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

;-----------------------------------------------------------------------
; serInit - Initialize serial ports
;-----------------------------------------------------------------------
serInit	mvi	a,03h		;reset 2SIO port
	out	SIOCTL
	mvi	a,11h		;select 8N2
	out	SIOCTL

	lxi	h,sioIn		;88-2SIO serial port is User routine
	shld	UIPRT
	lxi	h,sioOut	;88-2SIO serial port is User routine
	shld	UOPRT

	lxi	h,EMPTY*256+EMPTY	;init buffer table to empty
	shld	bufTbl		;one spot per pseudo port number
	shld	bufTbl+2

	lda	defIob		;initialize default I/O Byte
	sta	IOBYTE

	lda	0C000h		;SOLOS or CUTER?
	ora	a
	rnz			;CUTER, no changes needed

	mvi	a,SOLSER	;use Sol-20 serial port instead
	sta	ssbLoop+1	;update control register access
	sta	srbLoop+1
	inr	a		;update data register access
	sta	ssbOut+1
	sta	srbIn+1
	ret

;-----------------------------------------------------------------------
; conSt - console status check
;   Use SOLOS AINP routine to test for character. AINP accepts a
;   pseudo port number in A to do input from one of three ports.
;   Since AINP returns a character if present, we have to buffer
;   the returned value for a later call to conIN.
;-----------------------------------------------------------------------
conSt	lda	IOBYTE		;get IOBYTE = pseudo port
	ani	3		;get console bits alone

doStat	push	h		;save HL and BC
	push	b

	mov	c,a		;c=pseudo port number
	mvi	b,0		;form bc=index by pseudo port
	lxi	h,bufTbl
	dad	b		;hl->buffer table per port
	shld	bufPtr		;pointer to buffered char

	mov	a,m		;a=buffered char for this port
	cpi	EMPTY
	jnz	haveChr		;not empty, already have byte

	mov	a,c		;pseudo port=IOBYTE
	call	AINP		;SOLOS read from port in A
	mov	m,a		;save possible char in buffer table
	jnz	haveChr		;not-zero = have a character

	mvi	m,EMPTY		;otherwise, force empty
	xra	a		;CP/M no character status
	jmp	inExit		;exit input routine

haveChr	mvi	a,0FFH		;CP/M character present status
inExit	pop	b		;restore and exit
	pop	h
	ret

;-----------------------------------------------------------------------
; conIn, reader- console and reader input.
;   If a character is already buffered, return it. Otherwise, wait
;   for a new character.
;-----------------------------------------------------------------------
conIn	call	invFlsh		;invalidate and flush track buffer
	lda	IOBYTE		;get IOBYTE = pseudo port
	jmp	doIn

reader	lda	IOBYTE		;get IOBYTE = pseudo port
	rar			;put in ls bits
	rar
	jmp	doIn		;leave jump for possible patch

; doIn - do the input operation. If a character already buffered
;   return it. Otherwise, wait for a new character

doIn	ani	3		;get pseudo port alone
	sta	savPort		;save it
inLoop	call	doStat		;do status check on port in a
	lda	savPort		;restore port
	jz	inLoop		;no character present, keep waiting

; new byte present and buffered in the buffer table. Pull it out and
;   set the buffer to empty. If the pseudo device is the console,
;   translate DEL to BS

	push	h
	lhld	bufPtr		;hl=saved I/O table pointer
	ora	a		;pseudo port 0?
	jnz	retChr		;no, go return the char

	mvi	a,DEL		;translate DEL to BS
	cmp	m
	jnz	retChr

	mvi	a,BS		;return backspace instead
	jmp	setEmp		;set buffer to empty

retChr	mov	a,m		;a=buffered character
setEmp	mvi	m,EMPTY		;set buffer spot to empty
	pop	h		;restore HL
	ani	7Fh
	ret

;-----------------------------------------------------------------------
; conOut, list, punch - console, list and punch output.
;   Output to the selected device. If sending to the Sol-20
;   screen, translate BS to 81h which is equivalent to BS.
;-----------------------------------------------------------------------
conOut	push	b		;save the character
	lda	IOBYTE		;get IOBYTE = pseudo port
	jmp	charOut

list	push	b
	lda	IOBYTE		;get IOBYTE = pseudo port
	rlc			;put in ls bits
	rlc
	jmp	charOut

punch	push	b
	lda	IOBYTE		;get IOBYTE = pseudo port
	rar			;put in ls bits
	rar
	rar
	rar
	jmp	charOut		;leave for possible patch

; charOut - send the character in c

charOut	push	h		;preserve HL
	mov	b,c		;b=char to send
	ani	3		;a=pseudo port to use
	jnz	sndChr		;go send if not Sol-20 screen

; Sol-20 screen output, don't send multiple CR's in a row, translate
;   ASCII BS to 81h which does the equivalent operation on the Sol

	lda	prevChr		;what was the previous char?
	cpi	CR		;if CR, don't send again
	jnz	notCr
	cmp	b		;is this CR too?
	jz	outExit		;yes, just ignore

notCr	mov	a,b		;character to send in a
	cpi	BS		;is it a backspace?
	jnz	notBs		;no
	mvi	b,SOLBS		;send Sol BS (81h) instead

notBs	mov	a,b
	sta	prevChr		;save previous char
	xra	a		;pseudo port is zero

sndChr	call	AOUT		;monitor AOUT, pseudo port in A

outExit	pop	h		;restore
	pop	b
	mov	a,c		;exit with char still in A
	ret

;------------------------------------------------------------------------------
; sioIn, sioOut - 88-2SIO serial port I/O routines to work as the
;    User device for the Sol-20
;------------------------------------------------------------------------------
sioIn	in	SIOCTL		;get UART status
	ani	SIORDRF		;isolate data available bit
	rz			;nothing there

	in	SIODATA		;else, return the data
	ret
	
sioOut	in	SIOCTL		;get UART status
	ani	SIOTDRE		;isolate xmit ready bit
	jz	sioOut		;wait until ready

	mov	a,b		;output char from B
	out	SIODATA
	ret

;-----------------------------------------------------------------------
; listSt - list status not supported. Return zero per CP/M spec.
;-----------------------------------------------------------------------
listSt	xra	a		;always or never ready?
	ret

;******************************************************************************
;
;   BIOS Data Area
;
;******************************************************************************

;---------------------------------------------------------------------------
; secAddr - Sector address table. Returns sector buffer address within
;    the track buffer when indexed by hard sector number. Each sector
;    requires PSECLEN bytes.
;---------------------------------------------------------------------------
secAddr	dw	trkBuf+00*PSECLEN,trkBuf+01*PSECLEN,trkBuf+02*PSECLEN
	dw	trkBuf+03*PSECLEN,trkBuf+04*PSECLEN,trkBuf+05*PSECLEN
	dw	trkBuf+06*PSECLEN,trkBuf+07*PSECLEN,trkBuf+08*PSECLEN
	dw	trkBuf+09*PSECLEN,trkBuf+10*PSECLEN,trkBuf+11*PSECLEN
	dw	trkBuf+12*PSECLEN,trkBuf+13*PSECLEN,trkBuf+14*PSECLEN
	dw	trkBuf+15*PSECLEN,trkBuf+16*PSECLEN,trkBuf+17*PSECLEN
	dw	trkBuf+18*PSECLEN,trkBuf+19*PSECLEN,trkBuf+20*PSECLEN
	dw	trkBuf+21*PSECLEN,trkBuf+22*PSECLEN,trkBuf+23*PSECLEN
	dw	trkBuf+24*PSECLEN,trkBuf+25*PSECLEN,trkBuf+26*PSECLEN
	dw	trkBuf+27*PSECLEN,trkBuf+28*PSECLEN,trkBuf+29*PSECLEN
	dw	trkBuf+30*PSECLEN,trkBuf+31*PSECLEN
	
;-----------------------------------------------------------------------------
; dpHead - disk parameter header for each drive
;-----------------------------------------------------------------------------
dpHead	dw	tranTbl,0,0,0,dirBuf,dpb,csv0,alv0
	dw	tranTbl,0,0,0,dirBuf,dpb,csv1,alv1
	dw	tranTbl,0,0,0,dirBuf,dpb,csv2,alv2
;	dw	tranTbl,0,0,0,dirBuf,dpb,csv3,alv3

;-----------------------------------------------------------------------------
; dpb - disk parameter block. This table gives a 2K block size and
;   64 directory entries. 
;-----------------------------------------------------------------------------
dpb	dw	NUMSEC		;sectors per track
	db	BSH		;allocation block shift factor (BSH)
	db	BLM		;data location block mask (BLM)
	db	EXM		;extent mask (EXM)
	dw	DSM		;maximum block number (149)	
	dw	DRM		;maximum directory entry number (63)
	db	AL0,AL1		;AL0, AL1 
	dw	CKS		;CKS=(DRM+1)/4
	dw	RESTRK		;reserved tracks for CPM and bootloader

tranTbl	db	01,09,17,25,03,11,19,27,05,13,21,29,07,15,23,31
	db	02,10,18,26,04,12,20,28,06,14,22,30,08,16,24,32

;----------------------------------------------------------------------------
; Initialized data
;----------------------------------------------------------------------------
readCmd	db	'READ'		;read command to server
rcTrack	db	0
rcDrive	db	0
rcLen	dw	TRKLEN
CMDLEN	equ	$-readCmd

wrtCmd	db	'WRIT'		;write command to server
wcTrack	db	0
wcDrive	db	0
wcLen	dw	TRKLEN

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
;----------------------------------------------------------------------------
bfDrvSv	ds	1		;saved drive number that is currently buffered
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
rtryCnt	ds	1		;disk I/O retry counter

cmdResp	ds	4		;server command response buffer
crCode	ds	2		;response code
crData	ds	2		;response data

bufPtr	ds	2		;pointer into buffer table
savPort	ds	1		;saved pseudo port number
prevChr	ds	1		;previous character sent
bufTbl	ds	4		;character buffer per pseudo-po

;-----------------------------------------------------------------------------
;  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
;-----------------------------------------------------------------------------
 if TBHIRAM
ACTLEN	equ	$-BIOSBAS	;actual length of the BIOS
trkBuf	equ	0D000h
 endif

 if not TBHIRAM
trkBuf	ds	NUMSEC*PSECLEN
ACTLEN	equ	$-BIOSBAS	;actual length of the BIOS
 endif
	end


