;----------------------------------------------------------------------------
;
;	CP/M 2.2 BIOS for MITS Altair 8800x with 88-HDSK hard drive
;	and 88-DCDD Floppy Drive. CP/M drives A and B are the two
;	platters (A-Removable, B-Fixed) in an 88-HDSK unit. Drives
;	C and D are two Altair floppy drives 0 and 1. 
;
;	Written by Mike Douglas based on disassembly of the 
;	Altair (Burcon) CP/M 2.2 BIOS for the floppy drive.
;
;	Version History
;	1.0 	4/23/2014	Original
;
;	1.1	4/27/2014	Support hard disk buffer sizes up to 2K,
;				buffer hard disk writes, hard disk sector
;				skew of 7.
;
;	1.2  	6/6/2014 	Change boot switch from A8 to A11.
;	1.3	7/17/2014	"Home" entry point only sets trkNum to zero
;				instead of actually seeking to track zero.
;				Auto-detect a Teletype on the console port
;				and force subsequent console I/O to send
;				a NULL after CR. Look for "sndNull"
;
;	1.4	7/23/2014	Verify floppy track number in sector before 
;				verifying sector data. This ensures the
;				track error logic works when the BIOS
;				and the actual drive are on different
;				track groups (e.g., 0-5 vs 6-76).
;
;	1.5	12/17/2015	Use 1-indexed sector numbers in the setsec
;				call for hard disk I/O. Was previously
;				0-indexed for the hard disk.
;
;	1.6	04/14/2020	Include patch code jumped to by the CCP to:
;				1) Display the user number in the prompt
;				   (e.g., "A3>").
;				2) Look on drive A with current user, then
;				   drive A user 0 for the typed command if
;				   it fails to open on the default drive.
;				Search for "prompt" and "user0" in the code.
;
;----------------------------------------------------------------------------
memsize	equ	48		;memory size in K bytes

;  CPM location equates

ccpLen	equ	0800h		;CPM 2.2 fixed
bdosLen	equ	0e00h		;CPM 2.2 fixed
biosLen	equ	0f00h		;BIOS length in bytes

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	0a00h-ccpBase	;bias in DDT to patch in this BIOS

; CPM 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, see shift counts below
shftCon	equ	1		;left rotate count to align CON in bits 2,1
shftRdr	equ	7		;left rotate count to align RDR
shftPun	equ	5		;left rotate count to align PUN
shftLst	equ	3		;left rotate count to align LST

; 2SIO Serial Board Equates

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 Equates

sio0Ctl	equ	00h		;SIO board at 0 - control register
sio0Dat	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 Equates

lpcCtl	equ	02h		;line printer control port
lpcData	equ	03h		;line printer data port
lpcBusy	equ	02h		;mask to test printer busy flag

; MITS Floppy Disk Drive Controller Equates

drvSel	equ	8		;drive select port (out)
drvStat	equ	8		;drive status port (in)
drvCtl	equ	9		;drive control port(out)
drvSec	equ	9		;drive sector position (in)
drvData	equ	10		;drive read/write data

enwdMsk	equ	001h		;enter new write data bit mask
headMsk	equ	004h		;mask to get head load bit alone
moveMsk	equ	002h		;mask to get head move bit alone
trk0Msk	equ	040h		;mask to get track zero bit alone
dSelect	equ	0ffh		;deselects drive when written to drvSel
selMask	equ	008h		;"not used" bit is zero when drive selected

dcStepI	equ	001h		;drive command - step in
dcStepO	equ	002h		;drive command - step out
dcLoad	equ	004h		;drive command - load head
dcUload	equ	008h		;drive command - unload head
dcWrite	equ	080h		;drive command - start write sequence

; Floppy Disk Parameters

numDisk	equ	4		;up to four drives in use
numTrk	equ	77		;number of tracks
numSect	equ	32		;number of sectors per track
altLen	equ	137		;length of Altair sector
secLen	equ	128		;length of CPM sector
dsm0	equ	149		;max block number (150 blocks of 2048 bytes)
drm0	equ	63		;max directory entry number (64 entries) 
cks	equ	(drm0+1)/4	;directory check space
maxTry	equ	5		;maximum number of disk I/O tries
unTrack	equ	7fh		;unknown track number
fDrvMsk	equ	01h		;mask to force floppy drives 2,3 to 0,1

; 88-HDSK buffering parameters. Be careful to adjust for the change in
;   BIOS size when changing HDBUFSZ as this buffer is in the BIOS.

HDBUFSZ	equ	1024		    ;256, 512, 1024, or 2048 only
HDBFMSK	equ	(HDBUFSZ-1) shr 8   ; 00,  01,   03,      07
HDBFSEC	equ	(HDBUFSZ/256)	    ;number of 256 byte sectors in hdBuf.

; 88-HDSK ports (The interface board is actually an 88-4PIO.)

CREADY	equ	0A0h		;IN: Ctlr ready for command (bit7)
CSTAT	equ	0A1h		;IN: error flags, reset CREADY
ACSTA	equ	0A2h		;IN: Command Ack (bit 7)
ACMD	equ	0A3h		;IN: reset Command Ack
				;OUT: Command high byte/initiate
CDSTA	equ	0A4h		;IN: data/stat available at CDATA
CDATA	equ	0A5h		;IN: Disk data or status from Ctlr
ADSTA	equ	0A6h		;IN: ADATA Port Available (bit 7)
ADATA	equ	0A7h		;OUT: Command low byte

; 88-HDSK ACMD:ADATA Commands

CSEEK	equ	00h		;Bits 15:12 = 0000b
				;Bits 11:10 = Unit #
				;Bits  9:0  = Cylinder #

CWRSEC	equ	20h		;same bit fields as CRDSECT
CRDSEC	equ	30h		;Bits 15:12 = 0011b
				;Bits 11:10 = Unit #
				;Bits  9:8  = Buffer #
				;Bit   7:6  = Platter #
				;Bits    5  = Side #
				;Bits  4:0  = Sector #

CSIDE	equ	020h	  	;Side select for CRDSEC
CFPLTR	equ	0C0h	 	;platter mask for CRDSEC
CUNIT	equ	00Ch	  	;Unit mask for CSEEK & CRDSEC

CWRBUF	equ	40h		;same bit fields as CRDBUF
CRDBUF	equ	50h		;Bits 15:12 = 0101b
				;Bits 11:10 = not used
				;Bits  9:8  = buffer #
				;Bits  7:0  = # bytes to transfer
				;(00 means 256)
HDSPT	equ	24		;Sectors per track
ERMASK	equ	07Fh		;all the actual error bits
SECMASK	equ	01Fh		;isolates sector bits alone	
FLOPNUM	equ	2		;first floppy drive number
BUFNULL	equ	0FFh		;hard disk buffer content undefined

; Warm boot equates.

WBSECTS	 equ	(ccpLen+bdosLen)/secLen   ;number of 128 byte sectors to load
WBSTSEC	 equ	5			  ;128 byte starting sector (1 indexed)

; misc equates

SENSESW	equ	0ffh		;front panel sense switch port
PLATMSK	equ	08h		;isoloate boot platter switch A11
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	conin		;console character in 
	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

;  CPM Welcome Message

cpmStr	db	cr, lf, lf
	db	memSize/10 + '0',memSize mod 10 + '0'
	db	'K CP/M 2.2b v1.6', cr, lf
	db	'For MITS 88-HDSK', cr, lf, 0
	ds	64-($-cpmStr)	;adjust to make 64 byte stack
lclStk	equ	$		;local stack

;----------------------------------------------------------------------------
; Drive A, User 0 Patch for the CCP
;    1) Display the user number in the prompt (e.g., "A3>").
;    2) Look on drive A with current user, then drive A user 0 for the
;       typed command if it fails to open on the default drive.
;----------------------------------------------------------------------------
cmdDrv	equ	ccpBase+07F0h	;drive field used by the CCP
cmdType	equ	ccpBase+07D6h	;file type in the FCB
usrNum	equ	ccpBase+0B41h	;user number in the BDOS

ccpCmd	equ	ccpBase+06CDh	;CCP entry, try to open COM file
ccpErr	equ	ccpBase+076Bh	;CCP entry, COM open failed
ccpOpen	equ	ccpBase+00D0h	;CCP entry, open file in the FCB
ccpGo	equ	ccpBase+06DEh	;CCP entry, execute opened COM file
ccpPrnt	equ	ccpBase+008Ch	;CCP entry, print A

; patch the CCP to call our routines

SAVLOC	equ	$		;save the location counter

	org	ccpBase+038Dh	;display prompt in the CCP
	call	prompt		;call patch below instead

	org	ccpBase+06DBh	;.COM open failure branch in the CCP
	jz	user0		;jump to patch below instead

	org	SAVLOC		;restore location counter

; user0 is jumped to when the CCP fails to open the .COM file typed
;     on the command line.

user0	lxi	h,cmdDrv	;HL->drive flag used by the CCP
	mov	a,m		;A=content of drive field
	ora	a		;was a drive specified?
	jnz	uHavDrv		;yes

	inr	m		;drive flag now = drive A
	lxi	d,cmdType	;point DE to file type field
	jmp	ccpCmd		;try again to open the command file

; uHavDrv - drive field was specified or our drive A change just failed

uHavDrv lda	usrNum		;A=user number from the BDOS
	ora	a		;user zero?
	jz	ccpErr		;yes, nothing more to do

	push	psw		;save original user number
	xra	a		;set user number to zero
	sta	usrNum

	call	ccpOpen		;try to open the command file as user 0

	pop	d		;restore original user number in D
	lxi	h,usrNum	;HL->user number in the BODS
	mov	m,d		;restore original user number

	jnz	ccpGo		;execute .COM if it opened
	jmp	ccpErr		;else show the error

; prompt is called when the CCP has loaded the drive letter for the prompt
;    into A. The drive letter is printed and then followed with the user num.

prompt	call	ccpPrnt		;display drive letter
	lda	usrNum		;A=user number
	cpi	10		;0-9?
	jc	do1s		;yes, just do the 1's digit

	mvi	a,'1'		;print the 10's digit
	call	ccpPrnt

	lda	usrNum		;A=user number
	sui	10		;get 1's digit alone

do1s	adi	'0'		;convert 1's digit to ASCII
	jmp	ccpPrnt		;display it and exit

;----------------------------------------------------------------------------
; boot - Cold boot BIOS entry. CPM is already loaded in memory. Hand
;    control to the CCP.
;----------------------------------------------------------------------------
boot	in	SENSESW		;front panel A11 determines boot device
	ani	PLATMSK		;get A11 (bit 3) alone
	rrc			;put in bit zero
	rrc	
	rrc
	sta	bootDrv		;save as the boot drive
	
	xra	a
	sta	cdisk		;current CPM disk is drive zero

	call	serinit		;initialize both serial ports on a 2SIO board

	lxi	h,cpmStr	;hl = address of cpm welcome message

welOut	mov	c,m
	push	h
	call	conout
	pop	h
	inx	h
	mov	a,m
	ora	a
	jnz	welOut		;string ends with terminating null

	jmp	entCpm

;----------------------------------------------------------------
; 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
	lda	flags		;turn the raw disk I/O flag off
	ori	fRawIO
	xri	fRawIO	
	sta	flags
	call	hdBoot		;reload CCP and BDOS from hard disk

; entCpm - enter CPM. Set page zero variables, enter CPM with or without
;   command line based on 

entCpm	lxi	b,defDma	;set the default dma address (080h)
	call	setdma
	mvi	a,0c3h		;8080 "jump" opcode
	sta	wbootv		;store in 1st byte of warm boot vecctor
	sta	bdosv		;and 1st byte of bdos entry vecctor
	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
	lxi	h,cldDone	;get the "cold start done" flag
	mov	a,m
	ora	a
	mvi	m,1		;set the flag true - cold start has been done
	lda	flags		;check the proper "process command line" flag
	jz	coldSt		;jump if this is cold start
	ani	fWrmCmd		;warm start - run default command line?
	jmp	cmdChk
coldSt	ani	fCldCmd		;cold start - run default command line?
cmdChk	jz	ccpBase+3	;no, enter ccp and clear cmd line
	jmp	ccpBase		;yes, enter ccp with possible cmd line

;----------------------------------------------------------------------------
; 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
;    selected disk number is stored in diskNum. No actual drive activity 
;    takes place.
;----------------------------------------------------------------------------
seldsk	lxi	h,0		;hl of zero is error flag
	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	diskNum		;save the selected disk number
	rlc			;compute disk parameter header address
	rlc			;(16*drvNum) + dpHead
	rlc
	rlc
	lxi	h,dpHead  	;start of disk parameter headers.
	mov	e,a
	mvi	d,0
	dad	d		;hl points to the DPH for the passed drive
	ret

;----------------------------------------------------------------------------
; home - Home BIOS entry. Just set track number to zero. This BIOS
;     does track verification on all reads (floppy). The hard disk
;     does track verification on its own.
;----------------------------------------------------------------------------
home	xra	a		;set track number to zero
	sta	trkNum
	sta	trkMsb
	ret

;----------------------------------------------------------------------------
; settrk - Set Track BIOS entry. BC 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 lsb of track number
	sta	trknum
	mov	a,b		;save msb of track number
	ani	1		;limit to one bit
	sta	trkMsb
	ret

;----------------------------------------------------------------------------
; setsec - Set Sector BIOS entry. C contains the desired sector number.
;    The sector number has already been translated through the skew table.
;    The sector number is saved in secNum for later use. No actual
;    drive activity takes place.
;----------------------------------------------------------------------------
setsec	mov	a,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
	mov	l,c
	shld	dmaAddr
	ret

;----------------------------------------------------------------------------
; read - Read sector BIOS entry. Read one sector using the trkNum, secNum
;    and dmaAddr specified for diskNum. Returns 0 in A if successful, 1 in A
;    if a non-recoverable error has occured.
;----------------------------------------------------------------------------
read	call	swapStk		;swap to local stack
	lda	diskNum		;hard or floppy disk?
	cpi	FLOPNUM		;first floppy drive number (2)	
	jc	hdRead

; Floppy read

	mvi	a,1		;set the "verify track number" flag to true
	call	rTrkSec		;retrieve track in c, physical sector in b
	di
	call	readSec		;read the sector
	jmp	exitDio		;disk i/o exit routine

; Hard disk read

hdRead	call	hdRd128		;read a 128 byte sector from hard disk
	jmp	exitDio

;----------------------------------------------------------------------------
; write - Write sector BIOS entry. Write one sector using the trkNum, secNum
;    and dmaAddr specified for diskNum. Returns 0 in A if successful, 1 in A
;    if a non-recoverable error has occured.
;----------------------------------------------------------------------------
write	call	swapStk
	lda	diskNum		;hard or floppy disk?
	cpi	FLOPNUM		;first floppy drive number (2)	
	jc	hdWrite

; Floppy write

	xra	a		;set the "verify track number" flag to false
	call	rTrkSec		;retrieve track in c, physical sector in b
	di
	call	wrtSec		;write sector
	jnz	exitDio		;write failed, exit
	lda	flags
	ani	fWrtVfy		;are we verifying writes?
	jz	exitDio		;no, exit
	mvi	a,1		;set the "verify track number" flag to true
	call	rTrkSec		;retrieve track in c, physical sector in b
	lxi	h,altBuf	; (this isn't actually used by readSec)
	call	readSec		;read the sector just written
	jmp	exitDio

; Hard disk write

hdWrite	call	hdWr128		;write a 128 byte sector to hard disk
				;fall through to exitDio

;  exitDio - exit disk i/o. Restore interrupts if enable interrupt flag is
;     set.  

exitDio	push	psw
	lda	flags
	ani	fEnaInt		;should we re-enable interrupts?
	jz	noInts		;no
	ei
noInts	pop	psw
	mvi	a,0		;return error code of zero (success)
	rz
	mvi	a,1		;return fail code of one
	ora	a
	ret

;----------------------------------------------------------------------------
; sectran - Sector translation BIOS entry. Convert logical sector number in
;    BC to physical sector number in HL using the skew table passed in DE.
;----------------------------------------------------------------------------
sectran	lda	diskNum		;hard or floppy disk?
	cpi	FLOPNUM		;first floppy drive number (2)	
	jc	hdSecTr

; Floppy disk sectran

	xchg			;put table address (DE) into HL
	dad	b		;offset by logical sector number
	mov	l,m		;get physical sector number in L
	mvi	h,0		;force H to zero
	ret

; Hard disk sectran (out=in+1). This converts the 0-indexed sector numbers
;    from CP/M into the 1-indexed numbers specified for BIOS setsec calls.

hdSecTr	inx	b		;0-indexed to 1-indexed
	mov	h,b		;return in hl
	mov	l,c
	ret

;----------------------------------------------------------------------------
;  rTrkSec - return track in c, physical sector in b
;----------------------------------------------------------------------------
rTrkSec	sta	trkVrfy		;save the passed track verify flag
	lhld	trkNum		;L = track number, H = sector number
	mov	c,l
	dcr	h		;convert 1-32 to 0-31 for altair hardware
	mov	b,h
	ret

;--------------------------------------------------------------------------
; swapStk - switch to local stack. When the caller of this
;   routine does a ret, the return jumps to "resStk" below which restores
;   the normal stack.
;--------------------------------------------------------------------------
swapStk	pop	d		;de = return address. Return address popped
	lxi	h,0
	dad	sp		;hl = stack pointer after return addr popped
	lxi	sp,lclStk	;use lclStk as the new stack area
	push	h		;save original stack pointer
	lxi	h,resStk	;next "ret" will go to resStk, below
	push	h
	xchg			;put original return address in HL
	pchl			;jump to the "return" address

resStk	pop	h		;HL = original stack pointer
	sphl			;put original stack pointer back
	ret			;return to caller of swapStk caller

;-----------------------------------------------------------------------------
; disk control data
;-----------------------------------------------------------------------------
diskNum	ds	1		;current disk number
trkNum	ds	1		;track num (sector num MUST follow in memory)
secNum	ds	1		;sector number for disk operations
dmaAddr	ds	2		;dma address for disk operations
trkVrfy	ds	1		;verify track numbers if <> 0
selNum	ds	1		;disk number currently selected on controller
rtryCnt	ds	1		;retry counter
cldDone	db	0		;true after cold start has completed

; trkTbl - track information for each drive. Init to unkown track number.

trkTbl	db	unTrack, unTrack, unTrack, unTrack

;-----------------------------------------------------------------------------
; readSec - read sector. Selects the drive in diskNum, seeks to the track
;    in C and then reads the sector specified in B into the buffer
;    pointed to by dmaAddr.
;----------------------------------------------------------------------------
readSec	call	selTrk		;select the drive, seek to the proper track
	rnz			;exit if error
reReadP	push	b		;save sector number
reRead	call	rtryChk		;decrement and check retry counter
	pop	b
	rnz			;exit if no more retries
	push	b
	mov	a,b		;a = sector number
	call	altSkew		;do 17 sector skew like Altair Disk Basic does
	lxi	h,altBuf	;hl points to altair sector buffer
	call	rdPSec		;read physical sector
	lda	flags		;raw I/O or normal move to (dmaAddr)?
	ani	fRawIO
	jnz	rdExit		;raw I/O, leave data in altBuf and exit
	lda	trkNum		;process data differently depending on track#
	cpi	6		;tracks 0-5 processed directly below
	jnc	rTrk676		;jump for tracks 6-76

;-----------------------------------------------------------------------------
; Tracks 0-5: Validate track number

	lxi	h,altBuf+t0Trk	;hl-> track number byte in altBuf
	mov	a,m
	ani	07Fh		;get track number alone
	pop	b		;retrieve sector, track in b,c
	cmp	c		;requested track = track read?
	jnz	rdTrkEr		;no, have track number error
	push	b		;put sector, track back on stack

; Tracks 0-5: Validate stop byte

	lxi	h,altBuf+t0Stop	;should have 0ffh at byte 131 in altBuf
	inr	m
	jnz	reRead		;wasn't 255, re-try the sector read

; Tracks 0-5: Move the 128 byte data sector

	lhld	dmaAddr		;hl = destination address for the data
	mov	b,h		;put hl into bc
	mov	c,l
	lxi	h,altBuf+t0Data	;data starts at byte 3 in altBuf
	call	moveBuf		;move cpm sector to (dmaAddr), return checksum

; Tracks 0-5: Validate checksum

	lxi	h,altBuf+t0CSum	;sector checksum is in altBuf + 132
	cmp	m
	jnz	reRead		;checksum fail, re-try the sector read

	pop	b
	xra	a		;return success code
	ret

;----------------------------------------------------------------------------
;  rTrk676 - validate and move data for sectors in tracks 6-76
;     First, validate track number

rTrk676	lxi	h,altBuf+t6Trk	;hl-> track number byte in altBuf
	mov	a,m
	ani	07Fh		;get track number alone
	pop	b		;retrieve sector, track in b,c
	cmp	c		;requested track = track read?
	jnz	rdTrkEr		;no, have track number error
	push	b		;put sector, track back on stack

; Tracks 6-76: Validate sector number

	inx	h		;move to offset 1, should have sector num here
	mov	a,m
	cmp	b		;verify it matches requested sector number
	jnz	reRead		;sector match fail, retry the sector read

; Tracks 6-76: Validate stop byte

	lxi	h,altBuf+t6Stop	;should have 0ffh at byte 135 in altBuf
	inr	m		;should have 0ffh here
	jnz	reRead		;0ffh not present, re-try the sector read

; Tracks 6-76: Move the 128 byte data sector

	lhld	dmaAddr		;hl = destination address for the data
	mov	b,h		;put hl into bc
	mov	c,l
	lxi	h,altBuf+t6Data	;data starts at byte 7 in altbuf
	call	moveBuf		;move cpm sector to (dmaAddr), return checksum

; Tracks 6-76: Validate checksum

	lxi	h,altBuf+6	;add bytes 2,3,5 and 6 to checksum
	mov	b,m		;b = byte 6
	dcx	h
	mov	c,m		;c = byte 5
	dcx	h
	mov	d,m		;d = byte 4 (checksum byte)
	dcx	h
	mov	e,m		;e = byte 3
	dcx	h		;m = byte 2
	add	e		;add bytes 3, 6, 5 and 2 (not 4) to checksum
	add	b
	add	c
	add	m
	cmp	d		;compare to checksum
	jnz	reRead		;checksum fail, re-try the sector read

	pop	b
	xra	a		;otherwise, return success code
	ret

; rdTrkEr - Track number error during the read operation

rdTrkEr	xra	a
	call	reTkSec		;retry the track and sector seek
	jmp	reReadP		;retry the sector read (push B entry)

;  rdExit - exit read (raw) where data is left in altBuf

rdExit	pop	b
	xra	a
	ret

; rdPSec - read physical sector. Read the physical Altair sector (0-31)
;    specified by e into the buffer specified by HL. Physical sector
;    length is altLen (137) bytes.

rdPSec	call	secSync		;sync to start of sector specified in e
	mvi	c,altLen	;c = length of Altair sector (137 bytes)
rdLoop	in	drvStat		;get drive status byte
	ora	a		;wait for NRDA flag true (zero)
	jm	rdLoop
	in	drvData		;read the byte 
	mov	m,a		;store in the read buffer
	inx	h		;increment buffer pointer
	dcr	c		;decrement characters remaining counter
	jz	rdDone		;count is zero - read is done
	dcr	c		;decrement count for 2nd byte about to be read
	nop			;timing
	in	drvData		;get the 2nd byte
	mov	m,a		;store in the read buffer
	inx	h		;increment buffer pointer
	jnz	rdLoop		;loop until all bytes read
rdDone	xra	a		;return status of zero = good read
	ret

; secSync - sync to start of sector specified in e. The sector number in e
;    is an Altair physical sector number (0-31).

secSync	call	ldHead		;load the drive head
wtSecTr	in	drvSec		;read sector position register
	rar			;wait for sector true flag (zero = true)
	jc	wtSecTr
	ani	01fh		;get sector number alone
	cmp	e		;match sector num we're looking for?
	jnz	wtSecTr
	ret

;------------------------------------------------------------------------------
; wrtSec - Write a sector. Selects the drive in diskNum, seeks to the
;    track in C and then writes the sector specified in B from the
;    buffer pointed to by dmaAddr.
;-----------------------------------------------------------------------------
wrtSec	call	selTrk		;select the drive, seek to the proper track
	rnz			;return if it failed
	lda	flags		;see if raw I/O flag is set
	ani	fRawIO
	jnz	setHCS		;raw I/O, write altBuf, go set head current
	lda	trkNum		;process data differently depending on track #
	cpi	6		;tracks 0-5 processed directly below
	jnc	wTrk676		;jump to process tracks 6-76

;  Sector write for tracks 0-5

	push	b		;save sector number
	mov	a,c		;a = track number
	lxi	b,altBuf	;bc points to altair sector buffer
	stax	b		;put track number at offset zero
	xra	a		;put 0100h (16 bit) at offset 1,2
	inx	b
	stax	b
	inr	a
	inx	b
	stax	b
	inx	b		;bc = cpm data in altBuf at offset 3
	lhld	dmaAddr		;hl = location from which to read data
	call	moveBuf		;move cpm sector from (dmaAddr) to altbuf
	mvi	a,0ffh		;offset 131 is stop byte (0ffh)
	stax	b
	inx	b		;offset 132 is checksum
	mov	a,d		;a = checksum
	stax	b		;store it at offset 132
	pop	b
	jmp	setHCS		;go set head current setting

;  wTrk676 - write sector for tracks 6-76

wTrk676	push	b		;save sector number
	lxi	b,altBuf+t6Data	;bc = cpm data in altBuf at offset 7
	lhld	dmaAddr		;hl = location from which to read data
	call	moveBuf		;move cpm sector from (dmaAddr) to altbuf
	mvi	a,0ffh		;offset 135 is stop byte (0ffh)
	stax	b
	inx	b		;offset 136 is unused (store zero)
	xra	a
	stax	b
	mov	a,d		;a = checksum
	lhld	altBuf+2	;add bytes at offset 2 and 3 to checksum
	add	h
	add	l
	lhld	altBuf+5	;add bytes at offset 5 and 6 to checksum
	add	h
	add	l
	sta	altBuf+t6CSum	;store final checksum at offset 4
	pop	b
	lxi	h,altBuf	;hl = pointer to altair sector buffer
	mov	m,c		;store track number at offset 0
	inx	h
	mov	m,b		;store sector number at offset 1

; setHCS - set the head current reduction (bit 6) for tracks 43-76.
;   Also set the write bit (bit 7)

setHCS	mov	a,c		;a = track number
	adi	(-43 and 0ffh)	;add -43 (1st track for HCS bit = 1)
	mvi	a,0		;set a=0 without affecting carry
	rar			;080h if track >= 43
	stc
	rar			;0c0 if track >= 43, else 080h
	mov	d,a		;d is the control command for the drive
	mov	a,b		;a = sector number
	call	altSkew		;do sector skew like Altair Disk Basic

; wait for sector true and the right sector number

wtWrSec	in	drvSec		;read sector position register
	rar			;wait for sector true (0=true)
	jc	wtWrSec
	ani	01fh		;get sector number alone
	cmp	e		;at the right sector yet?
	jnz	wtWrSec		;no, keep looking
	mov	a,d		;control command formed in d above (setHCS)
	out	drvCtl		;issue write command
	lxi	h,altBuf	;hl points to altair sector buffer
	lxi	b,0100h+altLen	;c=137 bytes to write, b=1 byte of 0's at end
	mvi	d,enwdMsk	;Enter New Write Data flag mask
	mvi	a,080h		;1st byte of sector is sync byte
	ora	m		;sync byte must have msb set
	mov	e,a		;e = next byte to write
	inx	h

; wrSec - write physical sector loop

wrSec	in	drvStat		;read drive status register
	ana	d		;write flag (ENWD) asserted (zero)?
	jnz	wrSec		;no, keep waiting
	add	e		;put byte to write into accumulator
	out	drvData		;write the byte
	mov	a,m		;a = next byte to write
	inx	h		;increment source buffer pointer
	mov	e,m		;e = byte to write next time through this loop
	inx	h		;increment source buffer pointer
	dcr	c		;decrement chars remaining (byte just written)
	jz	wrDone		;zero - sector data written
	dcr	c		;dec chars remaining (byte about to write)
	out	drvData		;write 2nd byte
	jnz	wrSec		;loop if count <> 0

; wrDone - write is done. Now write # of zeros specified in b (just 1)

wrDone	in	drvStat		;wait for another write flag
	ana	d
	jnz	wrDone
	out	drvData		;write zero b times
	dcr	b
	jnz	wrDone
	xra	a		;return success status
	ret

;------------------------------------------------------------------------------
; moveBuf - move sector buffer (128 bytes) from (hl) to (bc). Compute
;   checksum on all bytes and return in a.
;------------------------------------------------------------------------------
moveBuf	mvi	d,0		;d = checksum
	mvi	e,secLen	;e = buffer length (128 bytes)
movLoop	mov	a,m		;move from (hl) to (bc)
	stax	b
	add	d		;add byte to checksum
	mov	d,a
	inx	b		;increment both pointers
	inx	h
	dcr	e		;decrement character count
	jnz	movLoop		;loop until count = 0
	ret

;------------------------------------------------------------------------------
; altSkew - Do Altair sector skew like disk basic. For sectors greater than 6,
;    physical = (logical * 17) mod 32. Returns physical sector number in e.
;    This is done on top of the secTran skew table mechanism of CPM. The math
;    works out such that this call to altSkew does almost nothing.
;------------------------------------------------------------------------------
altSkew	mov	e,a		;e = input sector number
	lda	trkNum		;see if track number >= 6
	cpi	6
	rc			;track < 6, exit
	mov	a,e		;multiply by 17
	add	a
	add	a
	add	a
	add	a
	add	e
	ani	01Fh		;keep lower 5 bits (0-31)
	mov	e,a
	ret

;------------------------------------------------------------------------------
;  selTrk - select the drive, go to the proper track (specified in diskNum 
;     and C)
;------------------------------------------------------------------------------
selTrk	mvi	a,maxTry	;set retry count (5 tries)
	sta	rtryCnt
	call	selDrv		;returns with HL=trkTable entry for this drive
	rnz
	mov	a,m		;a = track this drive is presently on
	cpi	unTrack		;if unknown track number, seek to track zero
reTkSec	cz	ldSeek0		;if 7f, load head and seek to track 0
	mov	a,m		;a = track number drive is presently on
	mov	m,c		;put destination track num in the track table
	mov	e,a		;e = track number drive is presently on
	call	ldHead		;load the drive head
	mov	a,e		;a = current track, c = desired track
	sub	c		;a = current - desired
	rz			;difference is zero - on proper track already
	mvi	d,dcStepI	;step in to higher tracks if desired > current
	jc	mulStep
	mvi	d,dcStepO	;step out to lower tracks if desired < current
	cma			;make it a negative value (1's complement + 1)
	inr	a

; mulStep - issue multiple track steps in or out (controller step command
;    in d). The number of tracks to step is passed as -steps. E.g., 10
;    steps passed as -10.

mulStep	mov	e,a		;e = -tracks left to step
wMoveOk	in	drvStat		;get drive status register
	ani	moveMsk		;wait until it's OK to move the head
	jnz	wMoveOk
	mov	a,d		;issue step in or out per command in d
	out	drvCtl	
	mov	a,e		;e has -tracks to step, counting up to zero
	inr	a
	jnz	mulStep		;loop until we've stepped to the desired track
	call	rtryChk		;decrement and check retry counter
	jz	stpDone		;retry count OK
	call	selTrk0		;max tries reached. Return error.
	jmp	retErr

stpDone	lda	trkVrfy		;track number verification flag set?
	ora	a
	jnz	vrfyTrk		;yes, verify track on disk = computed track
	lda	flags		;see if the force track verify flag is set
	ani	fTrkVfy		;if so, verify the track data anyway
	rz

;  vrfyTrk - Verify we're on the right track (passed in c) by reading
;     the track number that is stored in the sync byte at the start
;     of every sector. We don't care which sector we read.

vrfyTrk	in	drvSec		;wait for sector true (any sector)
	rar
	jc	vrfyTrk
wtVrfy	in	drvStat		;wait for new read data available flag
	ora	a
	jm	wtVrfy		;wait for NRDA flag
	in	drvData		;get sync byte (contains track number)
	ani	07Fh		;get the track number alone
	cmp	c
	rz			;we're on the right track number
	xra	a
	jmp	reTkSec		;otherwise, retry track and sector seek

;-----------------------------------------------------------------------------
;  selDrv - select the drive specified in diskNum
;-----------------------------------------------------------------------------
selDrv	lda	diskNum		;diskNum is the disk that CPM wants to use
	mov	e,a
	lda	selNum		;disk currently selected on the controller
	cmp	e		;same drive as already selected?
	mov	a,e
	sta	selNum		;set selected drive = diskNum
	jnz	selNew		;not the same drive, select the new drive
	in	drvStat		;make sure the drive is still selected
	ani	selMask
	jz	rTrkTbl		;drive selected, retrieve the track table ptr

;  selNew - select a new drive, deselect the current, then select the new

selNew	mvi	a,dSelect	;deselect the attached drive
	out	drvSel
	lda	selNum		;drive number to select
	ani	fDrvMsk		;force physical drives 0 and 1 from CP/M 2 and 3
	out	drvSel
	in	drvStat		;loop until drive is selected
	ani	selMask
	jnz	selNew

; rTrkTbl - return track table pointer. Also validates the drive number 
;    passed in e. Returns with HL pointing to the trkTbl entry for drive in e.

rTrkTbl	mvi	a,4		;drive number OK? (should be 3 here?)
	cmp	e		;e has drive number
	jc	retErr		;error if drive number > 4 (should 3 be used?)
	lxi	h,trkTbl   	;index by drive number (e) into the track table
	mvi	d,0		;de = 16 bit drive number
	dad	d
	ora	m		;test for msb set in track number
	jm	retErr		;track number invalid if msbit is set
	xra	a		;else, return zero
	ret

;------------------------------------------------------------------------------
;  ldHead - If head already loaded, return. Otherwise, issue the head load
;	command and wait for it to load.
;------------------------------------------------------------------------------
ldHead	in	drvStat		;get drive status
	ani	headMsk		;get head loaded bit alone
	rz			;zero = true, head is loaded
	mvi	a,dcLoad	;issue the load head command
	out	drvCtl
wtHead	in	drvStat		;get drive status
	ani	headMsk		;wait for head loaded to be true
	jnz	wtHead
	ret

;------------------------------------------------------------------------------
;  uldHead - Issued the unload head command
;------------------------------------------------------------------------------
uldHead	mvi	a,dcUload	;issue head unload command
	out	drvCtl
	ret

;-----------------------------------------------------------------------------
; selTrk0 - Select drive in diskNum, load head and seek to track 0
;   ldSeek0 - Load head and seek to track 0
;-----------------------------------------------------------------------------
selTrk0	call	selDrv		;select drive in diskNum
ldSeek0	call	ldHead		;load the drive head
seek0	in	drvStat		;get drive status register
	ani	moveMsk		;loop until OK to move the head
	jnz	seek0		
	mvi	a,dcStepO	;issue step out command
	out	drvCtl
	in	drvStat
	ani	trk0Msk		;loop until we get to track zero
	jnz	seek0
	lda	selNum		;selNum is diskNum once drive is selected
	mov	e,a
	call	rTrkTbl		;HL = track table pointer for this drive
	mvi	m,0		;set track number for this drive to zero
	ret

;------------------------------------------------------------------------------
; rtryChk - retry counter check. Decrement retry counter. Returns zero if
;     more tries left, non-zero if retry counter reaches zero.
;------------------------------------------------------------------------------
rtryChk	lda	rtryCnt
	dcr	a
	sta	rtryCnt
	jz	retErr
	xra	a
	ret

; retErr - Return error code with 1 in accumulator and non-zero status flag

retErr 	mvi	a,1
	ora	a
	ret

;------------------------------------------------------------------------------
; altBuf - Altair buffer contains the 137 bytes read straight from
;   the Altair drive. This BIOS assumes the disk is laid out in a 
;   manner similar to the Altair Disk Basic format. Sectors in tracks 0-5
;   have a different layout than sectors in tracks 6-76.
;------------------------------------------------------------------------------
altBuf	ds	137		;altair disk buffer

; Tracks 0-5

t0Trk	equ	0		;offset of track number
t0Data	equ	3		;offset of 128 byte data payload
t0Stop	equ	131		;offset of stop byte (0ffh)
t0CSum	equ	132		;offset of checksum

; Tracks 6-76

t6Trk	equ	0		;offset of track number
t6Sec	equ	1		;offset of sector number
t6CSum	equ	4		;offset of checksum
t6Data	equ	7		;offset of 128 byte data payload
t6Stop	equ	135		;offset of stop byte (0ffh)
t6Zero	equ	136		;offset of unused, but checked for zero.

	ds	5		;not used

;-----------------------------------------------------------------------------
; dpHead - disk parameter header for each drive
;-----------------------------------------------------------------------------
dpHead	dw	0,0,0,0,dirBuf,mitHdsk,csv0,alv0
	dw	0,0,0,0,dirBuf,mitHdsk,csv1,alv1
	dw	xlate,0,0,0,dirBuf,mitsDrv,csv2,alv2
	dw	xlate,0,0,0,dirBuf,mitsDrv,csv3,alv3

;-----------------------------------------------------------------------------
; mitsdrv - disk parameter block for Atlair floppy drive. This table
;   gives a block size of 2048 bytes. Per CPM docs, EXM should be 1 
;   and AL0,AL1 should be 080h, 000h.This would give two logical extents
;   per physical extent. (32K per physical extent). The settings here
;   give one logical extent per physical extent (16K per physical extent). 
;   This was probably done to maintain compatibility with CPM 1.4 disks.
;-----------------------------------------------------------------------------
mitsDrv	dw	numSect		;sectors per track
	db	4		;allocation block shift factor (BSH)
	db	00fh		;data location block mask (BLM)
	db	0		;extent mask (EXM), see note above
	dw	dsm0		;maximum block number (DSM)	
	dw	drm0		;maximum directory entry number (DRM)
	db	0c0h, 0		;AL0, AL1, see note above
	dw	cks		;CKS (DRM+1)/4
	dw	2		;reserved tracks for CPM and bootloader

;-----------------------------------------------------------------------------
; mitHdsk - disk parameter block for the MITS hard disk (88-HDSK). This 
;   table gives a block size of 4096 bytes and 256 directory entries.
;-----------------------------------------------------------------------------
mitHdsk	dw	96		;sectors per track
	db	5		;allocation block shift factor (BSH)
	db	01fh		;data location block mask (BLM)
	db	1		;extent mask (EXM)
	dw	1214		;maximum block number (DSM)	
	dw	255		;maximum directory entry number (DRM)
	db	0c0h, 0		;AL0, AL1
	dw	0		;treat as not removable
	dw	1		;reserved tracks for CPM and bootloader

;---------------------------------------------------------------------------
; xlate - sector translation table for skew (not a consistent skew step)
;---------------------------------------------------------------------------
xlate	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

;---------------------------------------------------------------------------
;  flags (MODE byte) - bios behavior flags. Some are used in normal
;     operation. Others can be set/cleared for debug. At offset 0559h
;     (2559h in memory after MOVCPM). Note the address after MOVCPM is
;     80h less than floppy BASIC (25D9h) because the hard disk boot
;     loader takes 100h bytes in memory instead of 180h bytes.
;---------------------------------------------------------------------------
	ds	36		;to keep flags at offset 0559h
flags	db	010h		;enable interrupts after disk i/o is done

fCldCmd	equ	001h		;true = CCP process cmd on cold start
fWrmCmd	equ	002h		;true = CCP process cmd on warm start
fRawIO	equ	008h		;r/w directly from altBuf
fEnaInt	equ	010h		;enable interrupts after disk I/O
fWrtVfy	equ	040h		;write verify flag (true = verify)
fTrkVfy	equ	080h		;force track number verification 

;---------------------------------------------------------------------------
;  IOBYTE - at offset 055Ah (255Ah in memory after MOVCPM). Note the
;     address after MOVCPM is 80h less than floppy BASIC (25DAh) because
;     the hard disk boot loader takes 100h bytes in memory versus 180h bytes.
;
;    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
;---------------------------------------------------------------------------
; default I/O byte

dIoByte	db	055h		;LST = UL1 (2SIO serial port #1)
				;PUN = PTP (2SIO serial port #1)
				;RDR = PTR (2SIO serial port #1)
				;CON = CRT (2SIO serial port #1)

;---------------------------------------------------------------------------
; sndNull flag - at offset 55Bh (255Bh in memory after MOVCPM).
;    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.
;    Note the address after MOVCPM is 80h less than floppy BASIC (25DBh)
;    because the hard disk boot loader takes 100h bytes in memory instead
;    of 180h bytes.
;---------------------------------------------------------------------------
sndNull	db	0		;true if null should be sent after CR


;---------------------------------------------------------------------------
;  CON device I/O
;---------------------------------------------------------------------------

; const - Console status BIOS entry point. Return 0ff if character ready,
;    return zero otherwise. 

const	mvi	b,shftCon	;B=console IOBYTE alignmnt shift count
	call	doIo		;call the proper I/O routine below
	dw	sio0St		;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. Return character in A 

conin	call	uldHead		;if waiting for input, assume disk I/O is done
	lda	hdDirty		;hdBuf need to be written?
	ora	a
	cnz	hdFlush		;yes, flush hdBuf to disk
	mvi	b,shftCon	;B=console IOBYTE alignmnt shift count
	mvi	c,07fh		;C=AND mask for input byte
	call	doIo		;call the proper I/O routine below
	dw	sio0In		;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	lda	sndNull		;send null flag true?
	ora	a
	jz	doConO		;no, do normal console output
	mov	a,c		;sending a C/R?
	cpi	cr
	jnz	doConO		;no, send the character as normal
	call	doConO		;otherwise, send the CR
	mvi	c,0		;then send a null
	call	doConO
	mvi	c,cr		;just in case, return with original CR
	ret

; doConO - execute the actual console output operation of byte in C

doConO	mvi	b,shftCon	;B=console IOBYTE alignment shift count
	call	doIo		;call the proper I/O routine below
	dw	sio0Out		;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 alignmnt shift count
	mvi	c,0ffh		;C=AND mask for input byte
	call	doIo		;call the proper I/O routine below
	dw	sio0In		;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 alignmnt shift count
	call	doIo		;call the proper I/O routine below
	dw	sio0Out		;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 alignmnt shift count
	call	doIo		;call the proper I/O routine below
	dw	sio0OSt		;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 alignmnt shift count
	call	doIo		;call the proper I/O routine below
	dw	sio0Out		;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	sio0st		;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

;---------------------------------------------------------------------------
; Input Status routines
;---------------------------------------------------------------------------

; sio1St - Console status routine for 2SIO port 1. Return 0ff 
;    if character ready, return zero otherwise. 

sio1St	in	sio1Ctl		;read 2SIO #1 status/control register
	ani	sioRdrf		;data present?
	mvi	a,0
	rz			;no, return zero
	cma			;else return 0ffh
	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?
	mvi	a,0
	rz			;no, return zero
	cma			;else return 0ffh
	ret

; sio0St - Console status routine for SIO at I/O address 0. Return 0ff 
;    if character ready, return zero otherwise. 

sio0St	in	sio0Ctl		;read SIO status/control register
	xri	0ffh		;convert to positive logic
	ani	sioRcv		;data present?
	mvi	a,0
	rz			;no, return zero
	cma			;else return 0ffh
	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?
	mvi	a,0
	rz			;no, return zero
	cma			;else return 0ffh
	ret

;---------------------------------------------------------------------------
; Character input routines
;---------------------------------------------------------------------------

; sio1In - input character from 1st port on a 2SIO board and return in A.
;    C contains AND mask.

sio1In	in	sio1Ctl		;read 2SIO #1 status/control register
	ani	sioRdrf		;data present?
	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	in	sio2Ctl		;read 2SIO #2 status/control register
	ani	sioRdrf		;data present?
	jz	sio2In		;no, loop until data available
	in	sio2Dat		;read the character
	ana	c		;possibly clear msbit
	ret

; sio0In - input character from SIO board at address 0/1
;    C contains AND mask.

sio0In	in	sio0Ctl		;read SIO control register
	ani	sioRcv		;data present?
	jnz	sio0In		;no, loop until data available
	in	sio0Dat		;read the character
	ana	c		;possibly clear msbit
	ret

; acrIn - input character from ACR SIO board at address 6/7

acrIn	in	acrCtl		;read ACR (SIO) control register
	ani	sioRcv		;data present?
	jnz	acrIn		;no, loop until data available
	in	acrDat		;read the character
	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		;read 2SIO #1 status/control register
	ani	sioTdre		;transmit data register empty?
	jnz	sio1Rdy		;yes, ready for next character
	inx	d		;count 21us tics
	jmp	sio1Wt		;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	cldDone		;see if we're in cold start
	ora	a
	jnz	sio1Xmt		;no, go send the character
	mov	a,d		;check msb of inter-character timer
	cpi	11		;if msb < 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	cldDone		;see if we're in cold start
	ora	a
	jnz	sio2Xmt		;no, go send the character
	mov	a,d		;check msb of inter-character timer
	cpi	11		;if msb < 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

;---------------------------------------------------------------------------
; sio0Out - Transmit byte through SIO at address 0/1. Byte to send in C. 
;   Detect 110 baud teletype during cold start.
;---------------------------------------------------------------------------
sio0Out	lxi	d,0		;init inter-character timer
sio0Wt	in	sio0Ctl		;read SIO control register
	ani	sioXmt		;transmit data register empty?
	jz	sio0Rdy		;yes, ready for next character
	inx	d		;count 21us tics
	jmp	sio0Wt		;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

sio0Rdy	lda	cldDone		;see if we're in cold start
	ora	a
	jnz	sio0Xmt		;no, go send the character
	mov	a,d		;check msb of inter-character timer
	cpi	11		;if msb < 11, not 110 baud
	jc	sio0Xmt		;not 110 baud
	sta	sndNull		;otherwise, set send null flag true

; sio0Xmt - send the character in C. 

sio0Xmt	mov	a,c		;get character to xmit
	out	sio0Dat		;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	in	lpcCtl		;88-LPC board at port 2,3
	ani	lpcBusy		;wait for printer (0=busy)		
	jz	lpcOut
	mov	a,c
	ani	07fh		;force bit 7 to zero
	out	lpcData		;output the character
	ret

;----------------------------------------------------------------------
; List output test routines
;----------------------------------------------------------------------

; sio1OSt - Return 0ffh if 2SIO Port #1 is ready, 0 if not ready

sio1OSt	in	sio1Ctl		;read 2SIO #1 status/control register
	ani	sioTdre		;0=busy
	mvi	a,0
	rz			;not ready, return 0
	cma			;else return 0ffh
	ret

; sio2OSt - Return 0ffh if 2SIO Port #1 is ready, 0 if not ready

sio2OSt	in	sio2Ctl		;read 2SIO #2 status/control register
	ani	sioTdre		;0=busy
	mvi	a,0
	rz			;not ready, return 0
	cma			;else return 0ffh
	ret

; lpcOst - Return 0ffh if 88-LPC is ready, 0 if not ready

lpcOSt	in	lpcCtl		;88-LPC board at port 2,3
	ani	lpcBusy		;0=busy
	mvi	a,0
	rz		
	cma
	ret

; sio0OSt - Return 0ffh if SIO at 0/1 is ready, 0 if not ready
 
sio0OSt	in	sio0Ctl		;read SIO status/control register
	xri	0ffh		;convert to positive logic
	ani	sioXmt		;0=busy
	mvi	a,0
	rz			;not ready, return 0
	cma			;else return 0ffh
	ret

;-----------------------------------------------------------------------------
; serinit - initialize serial I/O ports and I/O Byte
;-----------------------------------------------------------------------------
serinit	lda	dIoByte		;initialize CP/M's IOBYTE
	sta	ioByte
	mvi	a,003h		;reset the 6850 ACIA
	out	sio1Ctl
	out	sio2Ctl
	mvi	a,011h		;RTS asserted, no interrupts, 8N2, /16
	out	sio1Ctl
	out	sio2Ctl
	ret

;---------------------------------------------------------------------------
; 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).
;--------------------------------------------------------------------------- 
doIo	lda	ioByte		;A=CP/M IO BYTE
ioAlign	rlc			;rotate into bits 2 and 1
	dcr	b
	jnz	ioAlign

; 2 bit field aligned in bits 2 and 1. Index into jump table of caller.

	ani	6		;get the 2 bit field alone
	mov	e,a		;form DE=table offset
	mvi	d,0
	pop	h		;get table address
	dad	d		;HL=table address
	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
	pchl			;jump to address specified in table

;===========================================================================
;
;  88-HDSK Hard Disk subroutines
;
;===========================================================================

;---------------------------------------------------------------------
; hdBoot - Load the CCP and BDOS from disk (not the BIOS). This is
;    done using the standard CP/M hard disk routines in this BIOS.
;    The BIOS setdma call is bypassed since we have to directly
;    access the dmaAddr value to increment it anyway.
;---------------------------------------------------------------------
hdBoot	mvi	c,0		;select disk zero
	call	seldsk
	call	home		;set track zero
	lxi	h,ccpBase	;set starting address of CP/M
	shld	dmaAddr		;   as the dma address
	mvi	b,WBSECTS	;B=number of sectors to load
	mvi	c,WBSTSEC	;C=sector to read

; Loop reading the CCP and BDOS back into memory.

bootLp	push	b		;save BC
	call	setsec		;set sector to read from C
	call	hdRd128		;read a CP/M 128 byte sector
	lhld	dmaAddr		;HL->128 bytes just written
	lxi	d,secLen	;bump by 128 bytes
	dad	d
	shld	dmaAddr	
	pop	b		;restore BC
	inr	c		;move to next 128 byte sector
	dcr	b		;loop for wbSects
	jnz	bootLp
	ret

;---------------------------------------------------------------------
; hdRd128 - Read a CP/M 128 byte sector from the hard disk. The
;      drive number, track and sector are taken from diskNum,
;      trkNum and secNum. Data is read into dmaAddr.
;---------------------------------------------------------------------
hdRd128	call	rdHdBuf		;fill the hard disk buffer, hdBuf
	rnz			;error

; Move the proper 128 byte CP/M sector from the hdBuf to (dmaAddr)

	call	hdScAdr		;get sector address in DE
	lhld	dmaAddr		;HL->where to store data
	
; 128 byte move loop: hdBuf to (dmaAddr)

	mvi	b,secLen	;B=bytes to move
byteLp1	ldax	d		;read from hdBuf
	mov	m,a		;store at dmaAddr buffer
	inx	d
	inx	h
	dcr	b
	jnz	byteLp1
	xra	a		;return successful status
	ret

;---------------------------------------------------------------------
; hdWr128 - Write a CP/M 128 byte sector from the hard disk. The
;      drive number, track and sector are taken from diskNum,
;      trkNum and secNum. Data is written from dmaAddr.
;---------------------------------------------------------------------
hdWr128	call	rdHdBuf		;read HD sector into hdBuf if needed
	rnz			;error

; Move 128 byte CP/M sector into hdBuf from (dmaAddr)

	call	hdScAdr		;get sector address in DE
	lhld	dmaAddr		;HL->where to get data
	
; 128 byte move loop: (dmaAddr) to hdBuf

	mvi	b,secLen	;B=bytes to move
byteLp2	mov	a,m		;read from (dmaAddr)
	stax	d		;store in hdBuf
	inx	d
	inx	h
	dcr	b
	jnz	byteLp2
	mvi	a,1		;set hdBuf dirty flag
	sta	hdDirty
	xra	a		;return zero
	ret

;---------------------------------------------------------------------
; rdHdBuf - Read HDBFSEC sectors (256 byte hard disk sectors) into
;     hdBuf. The starting hard disk sector to read is based on
;     diskNum, trkNum and secNum. If these sectors are already in 
;     hdBuf, they are not re-read.
;---------------------------------------------------------------------
rdHdBuf	call	cpm2hd		;CP/M sector to hard disk sector in A

; compare compute track, head, sector to what is buffered. This also
;   checks the disk number since the disk number determines the 
;   the 2's bit in the head number that is part of bHdSect.

	lxi	h,bHdSect	;HL->head:sector presently in memory
	cmp	m		;A has requested head:sector
	jnz	rdReqd		;not the same, must do a read
	inx	h		;move to track lsb
	lda	trkNum		;A=requested track lsb
	cmp	m
	jnz	rdReqd		;not the same, must doa read
	inx	h		;move to track msb
	lda	trkMsb		;A=requested track msb
	cmp	m		;track msb's match?
	rz			;requested data is already in memory

; rdReqd - Reads are required to fill hdBuf. First, see if the present
;    content of hdBuf is dirty and must be written to disk.

rdReqd	lda	hdDirty		;hdBuf need to be written?
	ora	a
	cnz	hdFlush		;yes, flush hdBuf to disk

; Seek to proper track for the read

	lda	trkMsb		;A=msb of track (0 or 1)
;	ori	CSEEK		;seek command is zero - this not needed
	mov	h,a		;H=msb of command
	lda	trkNum		;A=lsb of track and is lsb of command
	call	hdCmdA		;issue seek command
	rnz			;error

; Fill hdBuf with HDBFSEC hard disk sectors of 256 bytes each.

	mvi	a,BUFNULL
	sta	bHdSect		;init buffer content to "unknown"
	mvi	c,HDBFSEC	;C=number of sectors to read
	lxi	d,hdBuf		;DE->hard disk buffer
	mvi	h,CRDSEC	;H=msb of read sector command
	lda	hdSect		;A=head:sector at start of buffer
	mov	l,a		;L=lsb of command=head:sector

; Loop for each hard disk sector to be read

hdRdLp1	call	hdTran		;convert L to physical sector in A
	call	hdCmdA		;issue read command
	rnz			;error
	call	hdRdBuf		;read the buffer into (DE)
	rnz			;always passes for now
	inr	l		;move to next sector number
	dcr	c		;decrement sector count
	jnz	hdRdLp1

; Store hdSect in bHdSect and trkNum in bHdTrk to identify the 
;    data that is buffered. Return with zero status.

	lxi	h,bHdSect	;HL->bHdSect
	lda	hdSect
	mov	m,a		;bHdSect = hdSect
	inx	h		;HL->bHdTrk lsb
	lda	trkNum
	mov	m,a		;bHdTrk lsb = trkNum (lsb)
	inx	h		;HL->bHdTrk msb
	lda	trkMsb
	mov	m,a		;bHdTrk msb = trkMsb
	xra	a		;return success status
	ret

;---------------------------------------------------------------------
; hdFlush - write hdBuf to disk, then clear the dirty flag
;    Clobbers all.
;---------------------------------------------------------------------
hdFlush	mvi	c,HDBFSEC	;C=number of sectors to write
	lxi	d,hdBuf		;DE->hard disk buffer
	mvi	h,CWRSEC	;H=msb of write sector command
	lda	bHdSect		;head:sector at start of buffer
	mov	l,a		;L=LSB of command=head:secctor

; Move the next 256 bytes to the hard drive's buffer and then
;   issue the sector write. Repeat HDBFSEC times.

hdWrLp1	call	hdWrBuf		;move from (DE) to drive's buffer
	call	hdTran		;convert L to physical sector in A
	call	hdCmdA		;issue write command
	inr	l		;move to next sector number
	dcr	c		;decrement sector count
	jnz	hdWrLp1

; Clear dirty flag and exit

	xra	a		;A=0
	sta	hdDirty
	ret

;---------------------------------------------------------------------
; cpm2hd - Convert CP/M diskNum and secNum into the physical
;    hard disk head number and sector number that corresponds
;    to the start of hdBuf. This always be on a HDBUFSZ 
;    boundary. Store as a single value in hdSect. The format
;    of hdSect matches the low byte of a hard disk read/write
;    sector command.
;
;    The CP/M 128 byte sector number ranges from 0-95. This is
;    converted to a 256 byte sector number from 0-47 by dividing
;    by two. If this value is 0-23, the sector is on side zero
;    of the platter. If this value is 24-47, 24 is subtracted
;    to give a sector number 0-23 and side one of the platter
;    is selected by setting the 1's bit of the head number.
;    Finally, the sector number is forced to be at the start
;    of hdBuf by ANDing with the complement of HDBFMSK.
;
;    The drive number (0 or 1) determines the 2's bit of the
;    head number. If the boot drive was drive 1, then the
;    physical drives are swapped so that drive A is physical 
;    drive 1 and drive B is physical drive 0
;
;    Returns with hdSect in A, clobbers B, HL
;---------------------------------------------------------------------

; First, determine physical drive to use. If boot drive is drive 0,
;   then CP/M drives 0 and 1 (A and B) are physical drives 0 and 1.
;   If the boot drive is drive 1, then CP/M drives A and B
;   are physical drives 1 and 0.

cpm2hd	lda	diskNum		;CP/M disk 0 or 1 (A or B)
	lxi	h,bootDrv	;HL->boot drive 0 or 1
	xra	m		;if boot=1, then CP/M drive swapped
	rrc			;move physical drive bit to bit 6
	rrc
	mov	b,a		;B=drive to use (in bit 6)

; Convert CP/M 128 byte sector to a physical 256 byte side and
;   sector on the hard drive

	lda	secNum		;divide 0-indexed sector number by 2
	dcr	a		;1-indexed to 0-indexed
	rar			;zero comes in msb as carry was clear
	cpi	HDSPT		;past end of side 0?
	jc	sideOk		;no, sector number is good
	adi	CSIDE-HDSPT	;select bottom, sector to 0-23
sideOk	ani	(NOT HDBFMSK) AND 0ffh	;force to start of hdBuf
	ora	b		;form complete drive,side,sector in A	
	sta	hdSect
	ret	

;---------------------------------------------------------------------
; hdTran - Hard disk sector translation (skew).
;    Given low byte of read/write sector command in L (which
;    contains logical HD sector in the lower 5 bits), return A
;    as a copy of L with the physical sector inserted into the 
;    lower 5 bits. Clobbers none. 
;---------------------------------------------------------------------
hdTran	push	h		;save HL
	push	d		;and DE
	mov	a,l		;get passed head:sector from L
	ani	SECMASK		;get sector bits alone
	mov	e,a		;E=low byte of offset into skew table
	mov	a,l		;get passed head:sector from L
	ani	(NOT SECMASK) AND 0ffh	;clear out sector bits
	mvi	d,0		;DE = offset into skew table
	lxi	h,hdSkew	;HL->skew table
	dad	d		;HL->physical sector
	ora	m		;put in physical sector
	pop	d		;restore
	pop	h
	ret

;--------------------------------------------------------------------
; hdScAdr - compute the address the current sector in hdBuf and
;    return it in DE. Clobbers A and HL.
;--------------------------------------------------------------------
hdScAdr	lxi	h,hdBuf		;HL->start of hdbuf
	lda	secNum		;A=128 byte CP/M sector
	dcr	a		;convert 1-indexed to 0-indexed
	rrc			;put 1's bit into bit 7 of LSB
	mov	d,a		;temp save in D
	ani	080h		;isolate 1's bit now in bit 7 
	mov	e,a		;E=LSB of sector offset in hdBuf
	mov	a,d		;get back sector >> 1
	ani	HDBFMSK		;A=MSB of sector offset in hdBuf
	mov	d,a		;DE=offset of sector in hdBuf
	dad	d		;HL->start of sector in hdBuf
	xchg			;DE->start of sector in hdBuf
	ret

;---------------------------------------------------------------------
; hdCmd - Issue the disk command in HL to the controller and then 
;    wait for the controller to complete the command.
;
; On Entry at hdCmd
;   hl = complete command
; On entry at hdCmdA
;   h = high byte of command
;   a = low byte of command
;
; On Exit:
;   Status returned in A, zero flag matches content of A.
;   All other registers preserved. The command is completed
;   and the controller is ready.
;---------------------------------------------------------------------
hdCmd	mov	a,l		;low byte of command 1st
hdCmdA	out	ADATA
	in	CSTAT		;reset CRDY flag just in case
	in	ACMD		;clear CMDACK in ACSTA
	mov	a,h		;A=command high byte
	out	ACMD		;issue command

hdWait	in	CREADY		;is the controller done? 
	rlc			;look at msb=CRDY
	jnc	hdWait		;keep waiting

	in	CSTAT		;reset CRDY flag
	ani	ERMASK		;and get A=error code
	ret			;return - non-zero = error

;---------------------------------------------------------------------
; hdRdBuf - Read buffer zero from the hard disk into memory pointed
;    to by DE. Clobbers A,B. DE updated as data moved. 
;---------------------------------------------------------------------
hdRdBuf	in	CDATA		;reset CDA in CDSTA
	in	ACMD		;clear CMDACK in ACSTA
	xra	a		;A=0=transfer 256 bytes
	mov	b,a		;init B to zero as well
	out	ADATA
	mvi	a,CRDBUF	;issue Read Buffer zero cmd
	out	ACMD	 

; Wait for data to be ready

hdRdWt	in	CDSTA 
	rlc			;msb=CDA
	jnc	hdRdWt

; Move 256 bytes of buffer data into memory at DE

hdRdLp2	in	CDATA		;get a data byte
	stax	d		;write it to RAM
	inx	d		;next address
	dcr	b
	jnz	hdRdLp2		;until done (B=0)
	ret

;---------------------------------------------------------------------
; hdWrBuf - Write disk buffer zero from the memory pointed to by DE.
;   Clobbers A,B. DE updated as data moved.
;---------------------------------------------------------------------
hdWrBuf	in	ADATA		;reset ADPA in ADSTA
	in	ACMD		;clear CMDACK in ACSTA
	xra	a		;A=0=transfer 256 bytes
	mov	b,a		;init B to zero as well
	out	ADATA
	mvi	a,CWRBUF	;issue Write Buffer zero cmd
	out	ACMD

; Wait for data port to be ready

hdWrWt	in	ADSTA
	rlc			;msb=ADPA
	jnc	hdWrWt

; Move 256 bytes from (DE) to the drive buffer

hdWrLp2	ldax	d		;get a data byte
	out	ADATA		;send to the controller
	inx	d		;next address
	dcr	b		;decrement byte counter
	jnz	hdWrLp2
	ret

patch	ds	50h		

;-----------------------------------------------------------------------
; Hard disk skew table. Sector skewing is done independent of the
;     CP/M sectran mechanism. A skew of 7 gives 6ms between reads
;     when filling hdBuf. Actual processing time is about 4.9ms
;-----------------------------------------------------------------------
hdSkew	db	00,07,14,21,04,11,18,01,08,15,22,05
	db	12,19,02,09,16,23,06,13,20,03,10,17

;-----------------------------------------------------------------------
;  hard disk variables and disk buffer
;-----------------------------------------------------------------------
bootDrv	ds	1		;boot drive 0 or 1
trkMsb	ds	1		;msb of track number for hard disk
hdSect	ds	1		;computed hard disk head:sector 
hdDirty	db	0		;hard disk buffer dirty flag

; bdSect and bHdTrk must remain in this order

bHdSect	db	BUFNULL		;currently buffered hard disk head:sector
bHdTrk	ds	2		;currently buffered hard disk track lsb:msb

; hdBuf is the HBUFSZ buffer where data from/to the hard disk
;    is temporarily stored.

hdBuf	ds	HDBUFSZ		;hard disk I/O buffer

;-----------------------------------------------------------------------------
;  Disk scratchpad areas defined in the DPH table
;-----------------------------------------------------------------------------
dirBuf	ds	128		;bdos directory scratchpad
alv0	ds	1214/8+1	;bdos allocation scratchpad dsm/8+1
csv0	ds	0		;change disk scratchpad
alv1	ds	1214/8+1
csv1	ds	0
alv2	ds	dsm0/8+1
csv2	ds	cks
alv3	ds	dsm0/8+1
csv3	ds	cks

actLen	equ	$-biosBas	;actual length of the BIOS

	end

