;----------------------------------------------------------------------
;
;   BIOS for Lifeboat Associates CP/M2 Vers 2.20 for the MITS/Altair
;   	8 inch disk controller.
;
;   Disassembled by Patrick Linstruth and Mike Douglas, May 2020
;
;----------------------------------------------------------------------
;
;   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
;
;-----------------------------------------------------------------------
	
; BIOS size equates

MSIZE	equ	56		;Distribution size
BIOSLEN	equ	0900h		;BIOS length

; CP/M size equates

CCPLEN	equ	0800h		;CP/M 2.2 fixed
BDOSLEN	equ	0E00h		;CP/M 2.2 fixed
SYSGEN	equ	0900h		;SYSGEN image location
USER	equ	0500h		;offset to the user area
BOOTLEN	equ	3*128		;boot loader length

; These equates are automatically changed by MSIZE.

BIOS	equ	(MSIZE*1024)-BIOSLEN	;BIOS base address
BDOS	equ	BIOS-BDOSLEN		;BDOS base address
BDOSENT	equ	BDOS+6			;BDOS entry address
CCP	equ	BDOS-CCPLEN		;CCP base address
BIAS	equ	SYSGEN+BOOTLEN-CCP	;puts CCP,BDOS,BIOS in SYSGEN image
BIOSGEN	equ	BIOS+BIAS		;BIOS ends up here in SYSGEN image

	org	BIOS

; CCP equates

CMDLINE	equ	CCP+7		;location of command line in the CCP

; 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

; MITS 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	01h		;enter new write data bit mask
HEADMSK	equ	04h		;mask to get head load bit alone
MOVEMSK	equ	02h		;mask to get head move bit alone
TRK0MSK	equ	40h		;mask to get track zero bit alone
DSELECT	equ	0FFh		;deselects drive when written to DRVSEL
SELMASK	equ	08h		;"not used" bit is zero when drive selected

DCSTEPI	equ	01h		;drive command - step in
DCSTEPO	equ	02h		;drive command - step out
DCLOAD	equ	04h		;drive command - load head
DCULOAD	equ	08h		;drive command - unload head
DCWRITE	equ	80h		;drive command - start write sequence

; Disk information equates

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
DSM	equ	149		;max block number (150 blocks of 2048 bytes)
DRM	equ	63		;max directory entry number (64 entries) 
CKS	equ	(DRM+1)/4	;directory check space
MAXTRY	equ	5		;maximum number of disk I/O tries
UNTRACK	equ	7Fh		;unknown track number

; Misc equates

CR	equ	13		;ascii for carriage return
LF	equ	10		;ascii for line feed

;-----------------------------------------------------------------------------
;
;  BIOS Entry Jump Table
;
;-----------------------------------------------------------------------------
	jmp	boot		;cold boot entry
wboote	jmp	wboot		;warm boot entry
	jmp	const		;console input status
;	jmp	conin		;console input
	jmp	coninUl		;unload head before console input
	jmp	conout		;console output
	jmp	list		;printer output
;	jmp	listUl		;unload head before printer output
	jmp	punch		;punch output
	jmp	reader		;reader input
	jmp	home		;home disk
	jmp	seldsk		;select specified drive
	jmp	settrk		;set specified track
	jmp	setsec		;set specified sector
	jmp	setdma		;set sector transfer address
	jmp	read		;read sector
	jmp	write		;write sector
	jmp	listst		;printer status
	jmp	sectran		;sector translate

mBanner	db	CR,LF
	db	'CP/M2 on Altair',CR,LF
	db	'0'+(MSIZE/10),'0'+(MSIZE mod 10)
	db	'K Vers 2.20  ',CR,LF
	db	'(c) 1981 Lifeboat Associates$'

; Diaplay message at HL until '$' reached

dispMsg	mov	a,m		;A=character to display
	cpi	'$'		;$ means end of string
	rz

	mov	c,m		;C=character to display
	inx	h
	push	h
	call	conout		;display the character
	pop	h
	jmp	dispMsg

;----------------------------------------------------------------------------
; boot - Cold boot BIOS entry. CPM is already loaded in memory. Hand
;    control to the CCP.
;----------------------------------------------------------------------------
boot	xra	a
	sta	CDISK		;set initially to drive A
	call	cinit		;cold initialize user area
	jmp	entCpm		;go to cp/m

;----------------------------------------------------------------
; 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
	call	clrTrks		;clear current track and track table
	xra	a
	sta	diskNum		;disk zero is boot disk
	sta	trkNum		;set track number to zero

	lxi	h,CCP-BOOTLEN	;HL->where 1st sector would go if allowed
	inr	a		;A=1=first sector (1-indexed)
	call	loadTrk		;load track 0

	mvi	a,1		;move to track 1
	sta	trkNum
	lxi	h,CCP+0E80h	;HL->where track 1 goes
	call	loadTrk

	call	winit		;warm initialize user area

; entCpm - enter CPM. Set page zero variables, enter CPM with or without
;   command line based on the flags variable

entCpm	mvi	a,JMP		;8080 "jmp" opcode
	sta	WBOOTV		;store in 1st byte of warm boot vector
	sta	BDOSV		;and 1st byte of BDOS entry vector

	lxi	h,wboote	;HL=warm start address in BIOS
	shld	WBOOTV+1	;put it after the jmp
	lxi	h,BDOSENT	;HL=BDOS entry address
	shld	BDOSV+1		;put it after the jump

	lxi	h,cldDone	;get the "cold start done" flag
	mov	a,m
	ora	a		;Z false if cold start already done
	mvi	m,1		;indicate cold start has been done
	push	psw		;save the test result

	lxi	h,mBanner	;HL->cold start message
	cz	dispMsg		;display only during cold start

	pop	psw		;recover cold start status 
	lxi	h,CDISK		;HL->CP/Mcurrent disk
	mov	c,m		;C=current disk number
	lda	flags		;check the "process command line" flags
	jz	coldSt		;jump if we're in cold start

	ani	fWRMCMD 	;in warm start, execute command line?
	jmp	cmdChk

coldSt	ani	fCLDCMD		;in cold start, execute command line?

cmdChk	jz	CCP+3		;no, enter CCP and clear cmd line
	jmp	CCP		;yes, enter CCP with possible cmd line

; loadTrk - load one track into memory. Read odd sectors into every other 
;    128 bytes of memory until the BIOS base address is reached or all
;    32 sectors in the track have been read. Then read even sectors into
;    the opposite 128 byte sections of memory until the BIOS base address
;    is reached or all 32 sectors in the track have been read.

loadTrk	sta	secNum		;save the sector number we're on
	shld	dmaAddr		;save the destination address
	
	mov	a,h		;A=MSB of address to check
	cpi	(BIOS shr 8)	;current address >= BIOS start address?
	jnc	nxtSec		;yes, skip read (don't overwrite BIOS)
	
	cpi	(CCP shr 8)	;current address < CCP start address?
	jc	nxtSec		;yes, skip read (not to valid data yet)
	
	call	read		;read a sector
	jnz	wboot		;fatal read error, restart
	
nxtSec	lhld	dmaAddr		;HL->destination address
	lxi	d,0100h		;increment address by 256 bytes
	dad	d
	
	lda	secNum		;A=sector number
	adi	2		;jump 2 sectors each read
	cpi	NUMSECT+1	;past the last sector in the track?
	jc	loadTrk		;not yet, keep reading
	
	sui	NUMSECT-1	;compute starting even sector number
	lxi	d,-0F80h	;compute load address for 1st even sector
	dad	d
	
	cpi	3		;done both odd and even sectors in a track?
	jnz	loadTrk		;no, go do even sectors
	
	ret

;-----------------------------------------------------------------------------
; dpHead - disk parameter header for each drive
;-----------------------------------------------------------------------------
dpHead	dw	xlate,0,0,0,dirBuf,mitsDrv,csv0,alv0
	dw	xlate,0,0,0,dirBuf,mitsDrv,csv1,alv1
	dw	xlate,0,0,0,dirBuf,mitsDrv,csv2,alv2
	dw	xlate,0,0,0,dirBuf,mitsDrv,csv3,alv3

;-----------------------------------------------------------------------------
; mitsdrv - disk parameter block with block size of 2048 bytes.
;   Per CPM docs, EXM should be 1 and AL0,AL1 should be 80h, 00h.
;   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 done to
;   maintain compatibility with CPM 1.4 disks.
;-----------------------------------------------------------------------------
mitsDrv	dw	NUMSECT		;sectors per track
	db	4		;allocation block shift factor (BSH)
	db	0Fh		;data location block mask (BLM)
	db	0		;extent mask (EXM), see note above
	dw	DSM		;maximum block number (DSM)
	dw	DRM		;maximum directory entry number (DRM)
	db	80h,0		;AL0, AL1, see note above
	dw	CKS		;CKS (DRM+1)/4
	dw	2		;reserved tracks for CPM and bootloader

;----------------------------------------------------------------------------
; 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	mov	a,c		;A=drive number
	ani	07Fh
	lxi	h,numDisk	;verify drive number less than number of disks
	cmp	m
	jnc	selerr		;drive number error
	
	sta	diskNum		;save the selected disk number
	
	mvi	h,0		;compute disk parameter header address
	mov	l,a		;(16*drvNum) + dpHead
	dad	h
	dad	h
	dad	h
	dad	h
	lxi	d,dpHead
	dad	d		;HK->DPH for the passed drive
	ret
	
selerr	lxi	h,0		;error, set HL=0, cdisk=0, A=1
	xra	a
	sta	CDISK
	inr	a
	ret

;----------------------------------------------------------------------------
; home - Home BIOS entry. Set track to zero. No drive activity takes
;    place.
;----------------------------------------------------------------------------
home:	mvi	c,0		;C=track=zero

;----------------------------------------------------------------------------
; settrk - Set Track BIOS entry. C contains the desired track number.
;    The track number is saved in trkNum for later use. No actual
;    drive activity takes place.
;----------------------------------------------------------------------------
settrk:	mov	a,c		;save track number from C in trkNum
	sta	trkNum
	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		;save sector number from C in secNum
	sta	secNum
	ret

;----------------------------------------------------------------------------
; setdma - Set DMA BIOS entry. BC contains the address for reading or
;    writing sector data for subsequent I/O operations. The address is
;    stored in dmaAddr.
;----------------------------------------------------------------------------
setdma:	mov	h,b		;save transfer address from BC in dmaAddr
	mov	l,c
	shld	dmaAddr
	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	lxi	h,xlate		;HL->translate table
	mvi	b,0		;form BC=input sector number
	dad	b		;HL->physical sector number
	
	mov	l,m		;get physical sector in L
	mvi	h,0		;form HL=physical sector number
	ret

;---------------------------------------------------------------------------
; xlate - sector translation table for skew
;---------------------------------------------------------------------------
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

; coninUl - A program calling conin to do console input is used as an
;   indicator that this is a good time to unload the head.

coninUl	call	uldHead
	jmp	conin

; listUl - A program calling list to do printer output unloads the disk
;   head. This entry point is presently NOT used.

listUl	push	b
	call	uldHead
	pop	b
	jmp	list

;----------------------------------------------------------------------------
; 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	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

;----------------------------------------------------------------------------
; 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	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		;verifying writes?
	ani	fWRTVFY
	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

;  exitDio - exit disk i/o. Restore interrupts if enable interrupt flag
;     is set.

exitDio	push	psw		;save status from I/O call
	lda	flags		;should we re-enable interrupts?
	ani	fENAINT
	jz	noInts		;no
	ei			;else, enable them
	
noInts	pop	psw		;restore I/O status
	mvi	a,0		;if status zero, return zero
	rz	
	inr	a		;else return A=1 for error
	ret

; rTrkSec - return track in C, physical sector in B

rTrkSec	sta	trkVrfy		;save the passed track verify flag
	lda	diskNum
	mov	c,a
	call	chkMnt		;manually mount another disk?
	mov	a,c
	sta	diskNum		;update disk number
	
	lhld	trkNum		;L=track number, H=sector number
	mov	c,l		;return C=track number
	dcr	h		;convert 1-32 to 0-31 for Altair FDC
	mov	b,h		;return B=physical sector
	ret

; chkMnt - check if we need to mount another disk in this drive

chkMnt	lda	flags		;single disk mode enabled?
	ani	fMNTDSK
	rz			;no
	
	lxi	h,mntdrv	;HL->mounted drive number
	mov	a,c
	adi	'A'		;are we mounting a different drive?
	cmp	m
	jz	mounted		;no, drive is already mounted
	
	mov	m,a		;else, save new mounted drive
	lxi	h,mMount	;prompt to change disks
	call	dispMsg
	call	conin		;wait for character input
	
mounted	mvi	c,0		;physical drive is always drive 0
	ret

mMount	db	CR,LF,'Mount disk '
mntdrv	db	'A'
	db	', then <cr>',CR,LF,'$'

;-----------------------------------------------------------------------------
; 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			;return if no more retries
	
	push	b		;save sector number
	mov	a,b		;A=sector number
	call	altSkew		;do 17 sector skew like Altair Basic does
	
	lxi	h,altBuf	;HK->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 based on track#
	cpi	6		;tracks 0-5 processed directly below
	jnc	rTrk676		;jmp for tracks 6-76

; validate and move data for sectors in tracks 0 - 5

	lxi	h,altBuf+T0STOP	;should have 0FFh at byte 131 in altBuf
	inr	m
	jnz	reRead		;wasn't 255, re-try the sector read
	
	lxi	h,altBuf+T0TRK	;track number + 80h at byte 0 in altBuf
	mov	a,m
	ani	07Fh		;get track number alone
	pop	b		;sector number off the stack
	cmp	c		;track number in C match track # in data?
	jnz	rdTrkEr		;no, have track number error
	
	push	b		;sector back on stack
	lhld	dmaAddr		;HL->data destination 
	mov	b,h		;BC->data destination
	mov	c,l
	
	lxi	h,altBuf+T0DATA	;data starts at byte 3 of altBuf
	call	moveBuf		;move cpm sector to (dmaAddr), return checksum
	
	lxi	h,altBuf+T0CSUM	;sector checksum is in altBuf + 132
	cmp	m
	jnz	reRead		;checksum fail, re-try the sector read
	
	pop	b		;sector number off stack
	xra	a		;return success code
	ret

; rTrk676 - validate and move data for sectors in tracks 6-76

rTrk676	dcx	h		;move back to last by read (offset 136)
	mov	a,m
	ora	a		;verify it is zero
	jnz	reRead		;not zero, re-try the sector read
	
	dcx	h		;move back to offset 135
	inr	m		;should have 0FFh here
	jnz	reRead		;0FFh not preset, re-try the sector read
	
	lxi	h,altBuf+T6TRK	;verify 1st byte of altBuf matches track #
	mov	a,m
	ani	07Fh		;get track number alone
	pop	b		;sector off stack
	cmp	c		;track number in c match track # in data?
	jnz	rdTrkEr		;no, have track number error
	
	inx	h		;move to offset 1, should have sector num here
	mov	a,m
	cmp	b		;verify it matches requested sector number
	jnz	reReadP		;sector match fail, retry the sector read
	
	push	b		;sector back on stack
	lhld	dmaAddr		;HL=>data destination
	mov	b,h		;BC->data destination
	mov	c,l
	
	lxi	h,altBuf+T6DATA	;data starts at byte 7 in altBuf
	call	moveBuf		;move cpm sector to (dmaAddr), return 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		;sector off stack
	xra	a		;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	setSync		;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 in counter
	jz	rDone		;count is zero - read is done
	
	dcr	c		;decrement count for 2nd byte
	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
	
rDone	xra	a		;return success 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).

setSync	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 failed
	
	lda	flags		;see if raw I/O flag is set
	ani	fRAWIO
	jnz	setHCS		;raw I.O, write altBuf, go get 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->altair sector buffer
	stax	b		;put track number at offset 0
	
	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->data buffer
	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		;sector off stack
	jmp	setHCS		;got 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->data buffer
	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		;sector off stack
	lxi	h,altBuf	;HL->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			;80h if track >= 43
	stc
	rar			;C0h if track >= 43, else 80h
	mov	d,a		;D=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->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,80h		;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 (bytes 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 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		;E=desired drive
	lda	selNum		;A=currently selected drive
	cmp	e		;same drive as already selected?
	mov	a,e
	sta	selNum		;set selected drive = diskNum
	jnz	selNew		;not the same drive, select 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
	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 the
;    deive number in E.

rTrkTbl	lda	numDisk		;verify valid drive number in E
;	dcr	a		;this statement is missing
	cmp	e
	jc	retErr		;requested drive > numDisk
	
	lxi	h,trkTbl	;index by drive number (E) into 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

;------------------------------------------------------------------------------
;  clrTrks - Initialize current track and the track table entry for each
;     drive to "unknown." This entry point is called by the boot loader
;     as well.
;------------------------------------------------------------------------------
clrTrks	lxi	h,flags		;HL->flags byte
	mov	a,m		;A=flags byte
	ani	0F7h
	mov	m,a		;clear force track verification
	mvi	a,UNTRACK	;A=undefined track	
	sta	selNum		;no drive selected
	lxi	h,256*UNTRACK+UNTRACK	;HL=7F7F
	shld	trkTbl
	shld	trkTbl+2
	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		;on track zero?
	ani	TRK0MSK
	jnz	seek0		;not yet
	
	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	lxi	h,rtryCnt	;HL->retry counter
	dcr	m		;decrement the count
	jz	retErr		;error if it reaches zero
	
	xra	a		;return zero - still ok
	ret

; retErr - Return error code with 1 in accumulator and non-zero status flag

retErr	mvi	a,1
	ora	a
	ret


;------------------------------------------------------------------------------
; USER AREA for CP/M2 on Altair
;
; Copyright (C) 1981 Lifeboat Associates
;
; User area is at offset 500h from start of the BIOS
;------------------------------------------------------------------------------

; The number of disks and the flags (MODE) byte are stored just before the
;     USER area.

	org	BIOS+USER-5

numDisk	db	4		;number of disks
	dw	altBuf		;not referenced anywhere
	db	10h		;not referenced anywhere
flags	db	0		;flags defined below 

fCLDCMD	equ	01h		;true = CCP process cmd on cold start
fWRMCMD	equ	02h		;true = CCP process cmd on warm start
fMNTDSK	equ	04h		;true = single disk mounting
fRAWIO	equ	08h		;r/w directly from altBuf
fENAINT	equ	10h		;enable interrupts after disk I/O
fWRTVFY	equ	40h		;write verify flag (true = verify)
fTRKVFY	equ	80h		;force track number verification

; JUMP TABLE - Jumps MUST remain here in same order.

cinit	jmp	cinitr		;Cold boot init
winit	jmp	nulUser		;Warm boot init
const	jmp	nulUser		;Console status
conin	jmp	nulUser		;Console input
conout	jmp	nulUser		;Console output
list	jmp	nulUser		;Printer output
punch	jmp	nulUser		;Punch output
reader	jmp	nulUser		;Reader input
listst	jmp	nulUser		;Printer status

; Null User Function

nulUser	xra	a		;return zero status 
	ret

; Set the CCP input buffer to run CONFIG.COM on startup

cinitr	lxi	d,CMDLINE	;DE->command line destination in CCP
	lxi	h,cmdStr	;HL->command line string 
	mvi	b,CMDLEN	;B=length of command string 
	call	movStr		;move command into CCP input buffer 

	lxi	h,BDOS+0AFh	;HL->bdos wait$err
	shld	const 		;console status jumps to wait$err
	shld	conin		;console input jumps to wait$err
	shld	conout		;console output jumps to wait$err

	mvi	a,jmp		;initialize the warm boot and
	sta	WBOOTV		;   BDOS jump vectors on page 0 
	sta	BDOSV
	lxi	h,wboote	;HL=BIOS warm start entry address
	shld	WBOOTV+1
	lxi	h,BDOSENT	;HL=BDOS entry address
	shld	BDOSV+1

	xra	a		;drive 0 is current disk
	sta	CDISK
	mov	c,a		;also pass to CCP in C
	jmp	CCP		;enter the CCP and execute CONFIG.COM

cmdStr:	db	6,'CONFIG',0
CMDLEN	equ	$-cmdStr

movStr:	mov	a,m 		;move (HL) to (DE) for B bytes
	stax	d 
	inx	h
	inx	d 
	dcr	b
	jnz	movStr
	ret 


	org	BIOS+USER+200h	;reserve 200h bytes for the user area
;------------------------------------------------------------------------------
; 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	ALTLEN		;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.

;-----------------------------------------------------------------------------
;  Disk scratchpad areas defined in the DPH table
;-----------------------------------------------------------------------------
dirBuf	ds	128		;BDOS directory scratchpad
alv0	ds	(DSM/8 + 1)	;disk allocation bitmap
csv0	ds	CKS		;change disk checksum area
alv1	ds	(DSM/8 + 1)
csv1	ds	CKS
alv2	ds	(DSM/8 + 1)
csv2	ds	CKS
alv3	ds	(DSM/8 + 1)
csv3	ds	CKS

;-----------------------------------------------------------------------------
; 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 number if <> 0
selNum	ds	1		;disk number currently selected on controller
rtryCnt	ds	1		;retry counter
cldDone	ds	1		;true after cold start has completed
trkTbl	ds	4		;current track for each drive

;-----------------------------------------------------------------------------
; Computed size and location equates
;-----------------------------------------------------------------------------
CLRTOFF	equ	clrTrks-BIOS	;offset to clrTrks (needed by boot.asm)
COLDOFF	equ	cldDone-BIOS	;offset to cldDone flag (needed for boot.asm)

BIOSCNT	equ	$-BIOS		;count of bytes in the BIOS

	end

