;------------------------------------------------------------------------------
; Altair floppy head movement tests
; 
; Valid commands are:
;   D [x]    Select drive x, If x omitted, current drive # is displayed.
;   R        Restore to track zero
;   H L|U    Head load or unload
;   S x [y]  Step to track x. If optional track y is specified, then the 
;	     head is stepped between x and y on each subsequent press of 
;            the space bar. Pressing Q exits the test.
;   T	     If 1st character typed, the head load status is immediately
;	     toggled.
;
; Written by Mike Douglas, March 2013
;
;    Version	Date	Description
;	1.0    3/16/13	Initial Version
;   	1.1    3/20/13  Auto-select drive before each operation
;  	1.2    3/20/13  Step in 3 tracks before restore to track 0
;
;------------------------------------------------------------------------------

; 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	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

; 2SIO Serial Interface Equates

sio1Ctl	equ	010h		;1st port on 2SIO board
sio1Dat	equ	011h
sioTdre	equ	002h		;mask to test for xmit ready
sioRdrf	equ	001h		;mask to test for rcv read

;  Misc Equates

cmdLen	equ	16		;length of command buffer
numDrvs	equ	16		;number of drives supported
cr	equ	13		;ascii carriage return
lf	equ	10		;ascii line feed
bs	equ	8		;ascii backspace
notSet	equ	0ffh		;track or drive number not set
numTrks	equ	77		;number of tracks on a disk
numDrvs	equ	16		;number of drives on a controller

	org	0		;code base address
;------------------------------------------------------------------------------
; Initialize stack pointer and data structures
;------------------------------------------------------------------------------
	lxi	sp,stack	;init stack pointer
	mvi	a,3		;reset ACIA
	out	sio1Ctl
	mvi	a,015h		;RTS on, 8N1
	out	sio1Ctl
	xra	a		;default to drive zero
	sta	newDrv		;...for first drive selection
	lxi	h,mVer		;display version message
	call	dispMsg
	lxi	h,trkTbl	;point to track location table
	mvi	b,numDrvs
	mvi	a,notSet	;set invalid track all drives
	sta	curDrv		;current drive is undefined
initTrk	mov	m,a
	inx	h
	dcr	b
	jnz	initTrk

;------------------------------------------------------------------------------
; cmdLoop - main command loop
;------------------------------------------------------------------------------
cmdLoop	lxi	h,mPrompt	;display the prompt
	call	dispMsg
	call	readCmd		;get the command
	lxi	h,cmdBuf+1	;point hl to params past command character
	lda	cmdBuf		;assume first character is command
	cpi	'D'		;select drive N
	jz	cmdDrvN
	cpi	'R'		;Restore to track zero
	jz	cmdTrk0
	cpi	'S'		;step to track n
	jz	cmdTrkN
	cpi	'H'		;head load/unload
	jz	cmdHead
	cpi	'T'		;toggle head load/unload
	jz	cmdTgl
	lxi	h,mHelp		;invalid command entered, display help
	call	dispMsg
	jmp	cmdLoop
	
;------------------------------------------------------------------------------
; cmdDrvN - Select drive N
;------------------------------------------------------------------------------
cmdDrvN	call	getTokn		;get the drive number
	call	dec2bin		;convert ascii token to binary in A
	jnz	noDrv		;no drive specified
	cpi	numDrvs		;valid drive number?
	jnc	badDrv
	sta	newDrv		;newDrv = the new drive to select	
	call	selDrv		;select the drive in newDrv
	jnz	noDrv

;  New drive selected. Display the completion message

	lxi	h,mDrive	;display 'Selected Drive n'
	call	dispMsg
	lda	curDrv		;display selected drive number
	call	dispDec
	lxi	h,mDrvTrk	;display the track the drive is on
	call	dispMsg
	lda	curTrk		;get the current track to display
	cpi	notSet		;is a valid track set?
	jz	noTrack
	call	dispDec
	jmp	cmdLoop

; noTrack - current track is undefined. Display "not set"

noTrack	lxi	h,mNoTrk
	call	dispMsg
	jmp	cmdLoop

; noDrv - no drive specified, or drive selection failed. Display 
;    the current drive number

noDrv	lxi	h,mCurDrv	;display current drive prompt
	call	dispMsg
	lda	curDrv		;display current drive number
	sta	newDrv		;back to curDrv, newDrv failed
	call	dispDec
	jmp	cmdLoop

; badDrv - bad drive number specified. Display message.
	
badDrv	lxi	h,mBadDrv	;bad drive number specified	
	call	dispMsg
	jmp	cmdLoop

;------------------------------------------------------------------------------
; cmdTrk0 - Seek to track zero
;------------------------------------------------------------------------------
cmdTrk0	call	selDrv		;verify a drive is selected
	jnz	cmdLoop		;selection failed	
	call	seek0
	lxi	h,mTrack0	;display track 0 message
	call	dispMsg
	jmp	cmdLoop

;------------------------------------------------------------------------------
; cmdTrkN - Seek to track N or toggle seek between two tracks each time
;	the space bar is pressed. Any other key exits.
;------------------------------------------------------------------------------
cmdTrkN	call	getTokn		;get 1st track number
	call	dec2bin		;convert ascii token to binary in A
	jnz	badTrk		;1st track specification invalid
	cpi	numTrks		;valid track number?
	jnc	badTrk		
	sta	track1		;save 1st track number value
	call	getTokn		;get 2nd track number (if any)
	call	dec2bin
	jnz	noTrk2		;2nd track not specified
	cpi	numTrks		;valid track number?
	jnc	badTrk		

; Two track numbers provided. Loop here toggling back and forth when
;    space bar pressed. Exit to command loop when any other key pressed.

	sta	track2		;save 2nd track number
trkLoop	lda	track1		;move to 1st track number
	call	movTrk
	call	tglTrk		;prompt to toggle tracks
	jnz	cmdLoop
	lda	track2
	call	movTrk
	call	tglTrk
	jz	trkLoop
	jmp	cmdLoop
	
; One track number provided. Seek to that track then exit

noTrk2	lda	track1		;move to 1st track number
	call	movTrk
	jmp	cmdLoop

; No valid track number provided.

badTrk	lxi	h,mBadTrk	;bad track message
	call	dispMsg
	jmp	cmdLoop

;------------------------------------------------------------------------------
; tglTrk - put up message to toggle between track 1 and track 2 and then
;    wait for the user's response. Zero status true to continue. Zero
;    status false to exit. 
;------------------------------------------------------------------------------
tglTrk	lxi	h,mSpace	;press space bar message
	call	dispMsg
	call	rcvChar
	cpi	' '		;space toggles tracks
	rnz			;no space - exit
	lxi	h,mCrLf
	call	dispMsg
	xra	a
	ret			;zero status means continue test

;------------------------------------------------------------------------------
; cmdHead - Load/unload head
;------------------------------------------------------------------------------
cmdHead	call	getTokn		;get next token
	lda	token		;H or L?
	cpi	'L'		;load head
	jz	headLd
	cpi	'U'		;unload head
	jz	headUld
	lxi	h,mNoAct
	call	dispMsg
	jmp	cmdLoop

headLd	call	ldHead
	jmp	cmdLoop

headUld	call	uldHead
	jmp	cmdLoop

;------------------------------------------------------------------------------
; cmdTgl - Toggle head load/unload to opposite state
;------------------------------------------------------------------------------
cmdTgl	in	drvStat		;see if head loaded or unloaded now
	ani	headMsk		;get head loaded bit alone
	jz	tglUld		;zero = head loaded, go unload
	call	ldHead		;otherwise, load the head
	jmp	cmdLoop

tglUld	call	uldHead
	jmp	cmdLoop

;-----------------------------------------------------------------------------
;  selDrv - select the drive specified in newDrv. If drive selection fails,
;	then wait for drive to be ready and prompt the user to abort
;	while waiting. Returns zero status true if selected, zero status
;	false if not selected. Loads curTrk from trkTbl if new selection
;	is required.
;-----------------------------------------------------------------------------
selDrv	lxi	h,curDrv	;point to currently selected drive number
	lda	newDrv		;a = the new drive to selecte
	cmp	m		;same drive as already selected?
	jnz	selNew		;not the same drive, select a new drive
	in	drvStat		;make sure the drive is still selected
	ani	selMask
	rz			;drive still selected, return zero = true

;  selNew - select a new drive (or wait for same drive to be ready again).
;    Deselect the current drive and select the new drive. Wait for the
;    new drive to be ready. Allow the operator to abort the wait by
;    pressing any key.

selNew	mvi	a,dSelect	;deselect the attached drive
	out	drvSel
	lda	newDrv		;drive number to select
	out	drvSel
	in	drvStat		;see if the drive is selected
	ani	selMask
	jz	loadTrk		;selected, go load it's current track
	lxi	h,mWtDrv	;"waiting for drive, any key to abort"
	call	dispMsg
waitSel	in	sio1Ctl		;wait for a character
	rrc
	jc	abrtSel		;character received - abort select
	mvi	a,dSelect	;deselect the attached drive
	out	drvSel
	lda	newDrv		;drive number to select
	out	drvSel
	in	drvStat		;see if the drive is selected
	ani	selMask
	jnz	waitSel		;keep waiting
	lxi	h,curDrv	;point to currently selected drive number
	
; Load curTrk from trkTbl for the newly selected drive

loadTrk	lda	newDrv		;set curDrv = newDrv
	mov	m,a
	lxi	h,trkTbl	;compute address in trkTbl for curDrv
	mov	e,a		;form de = offset into trkTbl for curDrv
	mvi	d,0
	dad	d
	mov	a,m		;get track number for new drive
	sta	curTrk
	xra	a		;return zero status = success
	ret

; abrtSel - Abort drive select operation

abrtSel	in	sio1Dat		;get rid of the character that aborted
	lxi	h,mCrLf		
	lxi	h,mNoDrv	;no drive selected message
	call	dispMsg
	mvi	a,1		;force non-zero status
	ora	a
	ret

;-----------------------------------------------------------------------------
; seek0 - seek to track zero. Steps in 3 tracks before seeking back out
;    to track zero in case the zero stop is incorrect.
;-----------------------------------------------------------------------------
seek0	mvi	b,3		;step in three times
stepIn3	in	drvStat		;get drive status register
	ani	moveMsk		;loop until OK to move the head
	jnz	stepIn3		
	mvi	a,dcStepI	;issue step in command
	out	drvCtl
	dcr	b
	jnz	stepIn3

; seek out until track zero is true

stepOut	in	drvStat		;get drive status register
	ani	moveMsk		;loop until OK to move the head
	jnz	stepOut
	mvi	a,dcStepO	;issue step out command
	out	drvCtl
	in	drvStat
	ani	trk0Msk		;loop until we get to track zero
	jnz	stepOut

;  Set curTrk and the track number in trkTbl for this drive to zero

	lxi	h,trkTbl	;compute address in trkTbl for curDrv
	lda	curDrv
	mov	e,a		;form de = offset into trkTbl for curDrv
	mvi	d,0
	dad	d
	xra	a		;set track number to zero
	mov	m,a
	sta	curTrk
	ret

;------------------------------------------------------------------------------
;  movTrk - moves to the track specified in A. Assumes A is already
;     validated.
;------------------------------------------------------------------------------
movTrk	mov	c,a		;c = target track
	call	selDrv		;verify a drive is selected
	rnz			;selection failed
	lda	curTrk		;a = current track this drive is on
	cpi	notSet		;if unknown track number, seek to track zero
	cz	seek0
	sub	c		;a = current track - target track
	jz	movExit		;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

; movExit - store the new track number in curTrk and in trkTbl for the
;    current drive number. Display moved to track message

movExit	lxi	h,trkTbl	;update current track for this drive
	lda	curDrv		;form drive offset in de
	mov	e,a
	mvi	d,0
	dad	d
	mov	m,c		;store current track for this drive
	mov	a,c
	sta	curTrk
	lxi	h,mTrackN	;'Moved to track '
	call	dispMsg
	lda	curTrk		;display track number from binary value
	call	dispDec
	ret

;------------------------------------------------------------------------------
;  ldHead - If not already loaded, issue the head load command and wait for
;    the head to load. 
;------------------------------------------------------------------------------
ldHead	call	selDrv		;verify a drive is selected
	rnz			;selection failed
	in	drvStat		;get drive status
	ani	headMsk		;get head loaded bit alone
	jz	ldExit		;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
ldExit	lxi	h,mHeadL
	call	dispMsg
	ret

;------------------------------------------------------------------------------
;  uldHead - Issued the unload head command.
;------------------------------------------------------------------------------
uldHead	call	selDrv		;verify a drive is selected
	rnz			;selection failed
	mvi	a,dcUload	;issue head unload command
	out	drvCtl
	lxi	h,mHeadU
	call	dispMsg
	ret

;------------------------------------------------------------------------------
; readCmd - Read a command line from the console into cmdBuf. Handles
;   backspace, terminates on C/R. Converts lower case to upper case.
;   Clobbers a, b, c, h, l.
;------------------------------------------------------------------------------
readCmd	mvi	b,0		;b = stored character count
	lxi	h,cmdBuf	;hl = pointer to cmdBuf
nxtChar	call	rcvChar		;get character from serial port

; Look for special characters (CR, BS, control characters)

	cpi	cr		;C/R?
	jz	cmdDone
	cpi	bs		;back space?
	jz	backSpc
	cpi	020h		;ignore control characters 
	jc	nxtChar
	cpi	'a'		;convert lower to upper case (garbage past 'z')
	jc	upper
	sui	020h
upper	mov	c,a		;save the character in c
	mov	a,b		;any more room left?
	cpi	cmdLen-1	
	jz	nxtChar		;out of room for more characters
	mov	m,c		;put the new character in the buffer
	inx	h		;increment buffer pointer
	inr	b		;increment stored character counter
	call	sndChar		;echo character in c to the serial port
	mov	a,c		;see if character is a "T"
	cpi	'T'
	jnz	nxtChar		;no, loop for more data
	mov	a,b		;see if character count = 1 
	dcr	a
	jz	cmdDone		;T at 1st position, exit for Toggle cmd
	jmp	nxtChar		;otherwise, keep looping

;  backSpc - backspace pressed. Backup up in the buffer and echo a backspace,
;     space, backspace to visually delete the character.

backSpc	mov	a,b		;see if already at zero characters
	ora	a
	jz	nxtChar		;nothing to delete
	dcr	b		;decrement the character count
	dcx	h		;and the the buffer pointer
	mvi	c,bs		;echo BS, space, BS to do a delete
	call	sndChar	
	mvi	c,' '
	call	sndChar
	mvi	c,bs
	call	sndChar
	jmp	nxtChar

;  cmdDone - Carriage return received. Zero terminate the string. Echo
;     the carriage return and add a line feed.

cmdDone	xra	a		;store null terminator
	mov	m,a
	lxi	h,mCrLf		;echo carriage return, line-feed
	call	dispMsg
	ret

;------------------------------------------------------------------------------
; rcvChar - Return a character from the serial port in A. MSB is cleared.
;------------------------------------------------------------------------------
rcvChar	in	sio1Ctl		;wait for a character
	rrc
	jnc	rcvChar
	in	sio1Dat		;a = received character
	ani	07fh		;get rid of MSB
	ret

;------------------------------------------------------------------------------
; getTokn - moves the next token as pointed to by HL to the token buffer.
;    Leading spaces or commas are skipped. Trailing space, comma or 
;    terminating null in the input buffer terminates the token. The token
;    is null terminated. Clobbers a, d, e. hl updated to allow subsequent
;    calls for the next token
;------------------------------------------------------------------------------
getTokn	lxi	d,token		;de points to token string
skpLead	mov	a,m		;move from cmdBuf to token
	stax	d				
	ora	a		;end of string?
	rz			;yes, all done
	inx	h		;move to next input character
	cpi	' '		;skip leading spaces
	jz	skpLead
	cpi	','		;tread commas as spaces
	jz	skpLead
	inx	d		;move to next token spot

;  Leading spaces skipped. Move characters until trailing space, comma
;      or null is reached.

tokLoop	mov	a,m		;get next character from cmdBuf
	stax	d				
	ora	a		;end of string?
	rz			;yes, all done
	inx	h		;move to next input character
	cpi	' '		;trailing space terminates token
	jz	tokDone
	cpi	','
	jz	tokDone
	inx	d		;move to next token spot
	jmp	tokLoop

; Insert null terminator at end of token

tokDone	xra	a		;store terminating null
	stax	d
	ret

;------------------------------------------------------------------------------
; dec2bin - Convert ascii-decimal token to binary value in A. Return zero
;   status if valid value found. Return non-zero status for error.
;   Clobbers a-e.
;------------------------------------------------------------------------------
dec2bin	lxi	d,token		;de is token string pointer
	ldax	d		;test for null string
	ora	a
	jz	decBad
	mvi	b,0		;b contains result
	
decLoop	ldax	d		;next ascii digit
	ora	a		;end of string?
	jz	decDone		;yes, we're done
	sui	'0'		;subtract ASCII offset
	jc	decBad		;before '0' - invalid digit
	cpi	10		;past 9?
	jnc	decBad	
	mov	c,a		;save digit in c for  now
	mov	a,b		;multiply value so far by 10
	add	a		;*2
	add	a		;*4
	add	a		;*8
	add	b		;*9
	add	b		;*10
	add	c		;add in new digit to ones position
	mov	b,a		;leave result in b
	inx	d		;move to next digit
	jmp	decLoop

decDone	mov	a,b		;put result in a
	ret			;zero flag was true upon entry to decDonei

decBad	mvi	a,1		;force zero flag false
	ora	a
	ret

;------------------------------------------------------------------------------
; dispDec - display value in A as a two digit ascii-decimal value. Won't
;   work for values over 99. Clobbers a,b,c.
;------------------------------------------------------------------------------
dispDec	mvi	c,'0'		;c accumulates the 10's digit in ascii
tenCnt	sui	10		;divide A by 10 to get the 10's digit
	jm	tenDone
	inr	c		;count the number of 10's in ascii
	jmp	tenCnt

tenDone adi	03ah		;compute final 1's digit in ascii
	mov	b,a		;save 1's digit in b
	mov	a,c		;zero suppress 10's digit
	cpi	'0'
	jz	sndOnes
	call	sndChar		;transmit the 10's digit
sndOnes	mov	c,b		;transmits the 1's digit
	call	sndChar
	ret

;------------------------------------------------------------------------------
; dispMsg - Display null terminated string. Clobbers a,c,h,l
;------------------------------------------------------------------------------
dispMsg	mov	a,m		;next character to send
	ora	a		;exit on null
	rz
	mov	c,a		;send the character
	call	sndChar
	inx	h
	jmp	dispMsg

;------------------------------------------------------------------------------
; sndChar - Send the character in C out the serial port. Clobbers a.
;------------------------------------------------------------------------------
sndChar	in	sio1Ctl		;wait until OK to xmit
	ani	sioTdre
	jz	sndChar
	mov	a,c		
	out	sio1Dat		;send the character
	ret

;------------------------------------------------------------------------------
; message strings
;------------------------------------------------------------------------------
mVer	db	cr,lf,'Altair Floppy Head Exerciser, Ver 1.2', cr,lf,0
mPrompt	db	cr,lf,'CMD>',0

mHelp	db	cr,lf,'Valid commands are:',cr,lf
	db	'  D [x]    Select drive x',cr,lf
	db	'  R        Restore to track zero',cr,lf
	db	'  S x [y]  Step to track x',cr,lf
	db	'  H L|U    Head load or unload',cr,lf
	db	'  T        Toggle head load/unload',cr,lf,0

mNoAct	db	'No action taken',0
mHeadL	db	'Head loaded',0
mHeadU	db	'Head unloaded',0
mTrack0	db	'Moved to track zero',0
mTrackN	db	'Moved to track ',0
mDrive	db	'Selected drive ',0
mCurDrv	db	'Current drive is ',0
mNoDrv	db	cr,lf,'Drive not present or not loaded',cr,lf,0
mDrvTrk	db	cr,lf,'Current track is ',0
mNoTrk	db	'not set',0
mSpace	db	cr,lf,'   Press Spacebar to toggle tracks: ',0
mBadDrv	db	'Invalid drive number specified',0
mBadTrk	db	'Invalid track number specified',0
mWtDrv	db	'Waiting for drive to be ready. Press any key to abort...',0
mCrLf	db	cr,lf,0

;	org	xx		;must be in RAM
;------------------------------------------------------------------------------
; cmdBuf - command buffer, variables and stack space.
;------------------------------------------------------------------------------
cmdBuf	ds	16		;command input buffer
token	ds	16		;token buffer
track1	ds	1		;1st track # for toggling tracks
track2	ds	1		;2nd track # for toggling tracks
newDrv	ds	1		;new drive # to select
curDrv	ds	1		;currently selected drive #
curTrk	ds	1		;current track for current drive
trkTbl	ds	16		;current track for each drive (0-15)
	ds	64
stack	equ	$
	end
