;------------------------------------------------------------------------------
;
; AFEXER - Altair Floppy Disk Exerciser 
;     Altair floppy adjustment and diagnostic test utility. Loads and runs
;     at 0100h either stand alone or under Altair CP/M (2SIO assumed)
; 
; Valid commands are:
;   D [x]    Select drive x, If x omitted, current drive # is displayed.
;   R        Restore to track zero
;   C	     Compare track number read from disk with expected track
;	     number. If expected track number is undefined, it is
;	     set to the track number read from disk.
;   H L|U    Head load or unload
;   M        Measure & report spinde revolution time continuously until
;            the user types anything
;   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. 
;   W hh [1|2]  Write hex value continuously to current track. Specifying
;	     1 writes to every sector, 2 writes to every other sector to
; 	     allow trim erase adjustment. Default is 2. Any key to abort.
;
; Immediate commands (action taken as soon as the key is pressed)
;   T	     Toggle head load state.
;   I	     Step in one track
;   O        Step out one track
; 
; Version    Author    Date     Description
;   1.0    M.Douglas  3/16/13	Initial Version
;
;   1.1    M.Douglas  3/20/13	Auto-select drive before each operation
;
;   1.2    M.Douglas  3/20/13	Step in 3 tracks before restore to track 0
;
;   1.3    M.Eberhard 5/28/14	Add W command for adjusting write trim
;
;   1.4    M.Eberhard 7/18/14	Limit number of steps when seeking track 0.
;				Wait for MVHEAD before sampling TRACK0.
;
;   1.5    M.Douglas  12/3/14	Update 1.4 did not wait for 3rd step-in
;				to complete before stepping out.
;
;   1.6    M.Eberhard 12/6/14	Add M command to measure spindle rev time
;
;   1.7    M.Douglas  12/14/14	Modify to run stand alone or under CP/M
;
;   1.8    M.Eberhard		Add timeout into spindle measurement loops.
;				Unload head in init, in case we came from CP/M
;
;   1.9    M.Douglas  12/18/14 	Remove automatic drive type detection and
;				prompt for drive type (8" or 5.25") at startup
;				instead. This allows AFEXER to work with soft
;				sectored disks (e.g., an alignment disk) and
;				with drives that have sector index problems.
;				Don't display "Moved to track 0" message in 
;				cmdTrk0 if the seek failed. Add 30ms delay
;				on seek direction change in seek0.
;
;   2.0    M.Douglas  01/28/15	Add option to write command to write every
;				sector (new) or every other sector (as it
;				was previously). If connected to a minidisk,
;				output timer reset command during the write 
;				to keep the drive from powering down.
;
;   2.1    M.Douglas  08/07/15	1) For the minidisk, head load/unload com-
;				   mands are treated as motor on/off commands,
;				   and once on, the motor is kept on by
;				   issuing the timer reset command in the
;				   rcvStat console input routine. 
;				2) Added the C command to compare and display
;				   the track number read from disk with the
;				   expected track number. 
;				3) Added the immediate I and O commands to
;				   step in or out one track. These commands
;				   work even if a track number has not been
;				   established in the program yet. 
;				4) For the write command, the delay to force a 
;				   sector to be skipped was increased from
;				   600us to 4ms so that the command works for
;				   the minidisk as well as the 8" drive.
;
;------------------------------------------------------------------------------

; 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 MVHEAD bit alone
trk0Msk	equ	040h		;mask to get TRACK0 bit alone
dSelect	equ	0ffh		;deselects drive when written to drvSel
selMask	equ	008h		;"not used" bit is zero when drive selected

secMask	equ	03eh		;sector-number bits in drvSec
svalMsk	equ	001h		;Sector-number bits valid when this bit is 0

dcStepI	equ	001h		;drive command - step in
dcStepO	equ	002h		;drive command - step out
dcLoad	equ	004h		;drive command - load head (8")
dcTimer	equ	004h		;drive command - reset timer (5.25")
dcUload	equ	008h		;drive command - unload head
dcWrite	equ	080h		;drive command - start write sequence

; MITS Disk subsystem parameters for 88-DCDD and 88-MDS

altLen	equ	137		;length of Altair sector
numSPT5	equ	16		;sectors/track for a minidisk
numSPT8	equ	32		;sectors/track for an 8" disk
maxTrk5	equ	34		;max track number on a minidisk
maxTrk8	equ	76		;max track number on an 8" disk
numDrvs	equ	16		;number of 8" drives on a controller

; 88-2SIO Channel A Serial Interface Equates

sioACtl	equ	010h		;port A on 88-2SIO board
sioADat	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
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
wBoot	equ	0		;CP/M warm boot vector
jmpInst	equ	0c3h		;8080 jump instruction

; Equates for spindle revolution measurement
; Notes: 
; 1. TSTCYCS is calculated from the number of cycles in the measurement loop.
; 2. TSTREVS should be as large as possible, but not so large that the 16-bit
;    counter might overflow during the test. The counter value is calculated
;    as follows (for a minidisk, with 200 mS/rev, nominal):
;        hl = TSTREVS * 200 * CPUSPD / TSTCYCS
; 3. The assembler will do a poor job of calculating DIVISOR, because it does
;    not do floating point math. Therefore this value must be hand calculated
;    using the provided formula.

CPUSPED	equ	2000		;CPU speed in KHz
TSTREVS	equ	5		;spindle revolutions per test
TSTCYCS	equ	39		;8080 cycles per test inner loop
;DIVISOR	equ	(100*TSTREVS/TSTCYCS)*CPUSPED
DIVISOR	equ	25641		;exactly 2.000MHz

;DIVISOR	equ	25544		;for 8800b at 1.99245MHz
;DIVISOR	equ	25637		;for 8800 at 1.9997MHz

	org	0100h		;load at 0100h for CP/M compatibility
;------------------------------------------------------------------------------
; Initialize stack pointer, UART and data structures
;------------------------------------------------------------------------------
	mvi	a,dSelect	;de-select a possibly selected drive on entry
	out	drvSel

	lxi	sp,stack	;init stack pointer
	call	chkCpm		;determine if running under CP/M
	call	initSio		;init 2SIO if needed
	xra	a		;default to drive zero
	sta	newDrv		;...for first drive selection
	sta	numSPT		;we don't know what type of drive yet
	lxi	h,mVer		;display version message
	call	dispMsg
	call	getType		;get drive type from user
	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
	sta	curTrk		;current track not defined	
initTrk	mov	m,a
	inx	h
	dcr	b
	jnz	initTrk

;------------------------------------------------------------------------------
; cmdLoop - main command loop
;------------------------------------------------------------------------------
cmdLoop	lxi	h,mPrompt	;display the prompt
	call	dispMsg
	lxi	h,cmdLoop	;create return address for commands
	push	h
	call	readCmd		;get the command
	lxi	h,cmdBuf	;assume 1st character is the command
	mov	a,m
	inx	h		;point hl to params past command character
	cpi	'D'		;select drive N
	jz	cmdDrvN
	cpi	'R'		;restore to track zero
	jz	cmdTrk0
	cpi	'C'		;compare track on disk to expected track
	jz	cmdCmp
	cpi	'S'		;step to track n
	jz	cmdTrkN
	cpi	'O'		;immedate step out-in
	jz	cmdOut
	cpi	'I'		;immediate step in-out
	jz	cmdIn
	cpi	'H'		;head load/unload
	jz	cmdHead
	cpi	'T'		;toggle head load/unload
	jz	cmdTgl
	cpi	'W'		;write track continuously
	jz	cmdWTrk
	cpi	'M'		;Measure spindle rev time
	jz	cmdMeas
	cpi	'X'		;exit to CP/M
	cz	exitCpm
	lxi	h,mHelp		;invalid command entered, display help
	call	dispMsg	
	lxi	h,mHelp2
	jmp	dispMsg		;display and return to cmdLoop
	
;------------------------------------------------------------------------------
; cmdMeas - measure and display spindle revolution time repeatedly until the
;     user types something.
;   Note 1: There is no way for this software to measure the CPU speed. The CPU
;     speed is therefore hard-coded in the value CPUSPD(which is set with an
;     equate above).
;   Note 2: The timing loops here assume an 8080 CPU with zero wait-state
;     memory. Because of differences in the number of cycles in certain
;     equivalent Z80 instructions, this code will give incorrect timing values
;     if a Z80 CPU is used.
;------------------------------------------------------------------------------
cmdMeas	call	selDrv		;select drive, load head
	rnz			;drive not ready: return to cmdLoop

	lxi	h,mMSpind	;"Measuring spindle revs"
	call	dispAny		;print with ". any key to abort"

;------------------------------------------------------------------------
; Measure and report spindle rev time continuously until the user types
; something, assuming a CPUSPD (KHz) CPU. Do this over and over until
; the user types something. Note: the svalMsk pulse is only 30 uS long.
; Numbers in parenthesis are 8080 cycle counts for the instructions.
;------------------------------------------------------------------------

;compute number of sectors for 4 revs/test,
; to get b=total number of sectors to time

	call	ldHead		;load head and select drive

msLoop:	lxi	h,mCrLf		;print CR,LF
	call	dispMsg

;calculate the number of sectors in TSTREVS revolutions

	lxi	h,numSPT	;total number of sectors/track
	mvi	b,TSTREVS	;number of revs to test
	xra	a

TstSecs	add	m
	dcr	b
	jnz	TstSecs
	mov	b,a		;b=number of sectors to time

	lxi	d,TimeOut	;create return address
	push	d		;..for rc instructions below
	lxi	d,1		;for dad instructions below
	mov	h,d		;timeout for initial sector

; Wait for initial sector, with timout if hl overflows.
; Exactly 39 8080 cycles (~19.5 uS) per pass

meaSec0	dad	d		;(10)bump & test timer
	rc			;(5)Timeout?

	in	drvSec		;(10)
	rrc			;(4)test svalMsk. sector pulse?
	jc	meaSec0		;(10)

;stall for exactly 3 X 39 = 117 cycles, so that the sector pulse is
;definitely gone when we fall into the "meaSec" hunt loop.

	lxi	h,0		;(10)start timer
	mov	a,b		;(5)stall

;Measure time for b sectors, using hl as a timer

MeaSecs	dad	d		;(10)account for time outside
	rc			;(5)..the sector-hunt loop
	dad	d		;(10)
	rc			;(5)Timeout?
	dad	d		;(10)
	rc			;(5)Timeout?
	
	xthl			;(18) \
	xthl			;(18)  Stall for exact timing
	push	h		;(11) /
	pop	h		;(10)/

; Hunt for the next sector, with timout if hl overflows.
; Exactly 39 8080 cycles per pass

meaSec	dad	d		;(10)bump & test timer
	rc			;(5)Timeout?

	in	drvSec		;(10)
	rrc			;(4)test svalMsk. sector pulse?
	jc	meaSec		;(10)n: keep looking

	dcr	b		;(5)next sector test 1st
	jnz	meaSecs		;(10)go find another sector pulse

	pop	d		;chuck return-to-TimeOut address

; [b,hl] = total time for TSTREVS revolutions in units of TSTCYCS CPU
; cycles. (b=0 at the end of the above loop.) Compute and report the
; result in mSec, based on CPUSPED, TSTREVS and TSTCYCS.

	mvi	d,6		;6 digits to print

DivLoop	mov	a,d		;time for the decimal point?
	cpi	3		;3 digits after decimal point
	mvi	c,'.'
	cz	sndChar		;y: print decimal point

	push	d		;remember d=digit count
	lxi	d,-DIVISOR	;divisor based on CPU speed, etc.
	call	DivDigt		;compute & print a digit, and
	pop	d		;..compute remainder X 10 in [b,hl]

	dcr	d		;next digit	
	jnz	DivLoop		;unless we're done

	lxi	h,mMsRev	;print ' mS/rev'
	call	dispMsg

;Keep measuring & reporting until the user types something

	call	rcvStat		;did the user type anything?
	jz	msLoop		;n: go do another measurement

	jmp	uldHead		;unload head and exit

;timeout waiting for sector pulses

TimeOut	call	uldHead		;unload the head
	lxi	h,mTimOut	;Timeout message
	jmp	dispMsg

;---Local Subroutine-----------------------------------------
; Divide [b,hl] by de and print 1-digit whole number quotent
; On Entry:
;   [b,hl]=dividend
;   de=negative of divisor
; On Exit:
;   Quotent is printed always
;   [b,hl]=remainder X 10
; trashes psw,c,de
;------------------------------------------------------------
DivDigt	mov	a,b		;a=dividend high byte
	lxi	b,0FF00h+('0'-1) ;create b=MSB of negative divisor
				;initialize c=ASCII quotient-1
				;..(will go 1 too many times)

;Divide [a,hl] by -[b,de]. The result will be an ASCII
;number in c. The remainder will be 16 bits on the stack.

	push	h		;set up stack

DgtLoop	inx	sp		;chuck stack value
	inx	sp		;..pop to garbage
	push	h		;remember prior result
	inr	c		;bump quotent
	dad	d		;add a negative number
	adc	b		;catch carry bit
	jc	DgtLoop

	pop	h		;hl=remainder

;c = quotent in ASCII, hl = remainder in binary.
;Compute [b,hl] := hl X 10 (could be up to 19 bits.)

	mov	d,h		;temp save X 1
	mov	e,l
	xra	a		;initialize high byte
	dad	h		;X 2
	adc	a		;a=0: catch carry
	dad	h		;x 4
	adc	a		;high byte		
	dad	d		;x 5
	aci	0		;catch carry in high byte
	dad	h		;hl = remainder X 10, 2 low bytes
	adc	a		;a = remainder X 10 high byte
	mov	b,a		;[b,hl] = remainder X 10

	jmp	sndChar		;print c, return from there

;------------------------------------------------------------------------------
; cmdWTrk - Write every sector or every other sector based on 1st parameter
;     (1 or 2), with the given value continuously until the user types something
;------------------------------------------------------------------------------
cmdWTrk	call	getTokn		;get the write data
	call	hex2bin		;convert ascii hex token to binary in a & b
	jnz	badData		;write data specification invalid

; Get the interleave parameter (1 or 2). If invalid or not specified,
;    every other sector is assumed

	call	getTokn		;every sector (1) or every other sector (2)?
	lda	token
	sui	'1'		;a=0 if every sector specified
	sta	skipSec		;skipSec non-zero for every other sector

	call	ldHead		;verify a drive is selected, and load its head
	rnz			;nz: selection failed, return to cmdLoop

	lxi	h,mWrtDat	;display Write message
	call	dispAny		;print with ". any key to abort"

;compute write command with correct write current based on current track

setHCS	lda	curTrk		;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	e,a		;e=write command with head load

	mvi	d,enwdMsk	;d=Enter New Write Data flag mask

;if skipSec is true, stall for 4ms to force a miss of the sector true
;   signal of the next sector. Writing every other sector allows trim
;   erase on the the drive to be measured.

nxtSec	lda	skipSec		;skip sector set?
	ora	a
	jz	secSync		;no, do every sector

	lxi	h,333		;stall for 4ms
sStall	dcx	h		;(5)
	mov	a,h		;(5)
	ora	l		;(4)
	jnz	sStall		;(10) 12 uS/pass

secSync	in	drvSec		;read sector position register
	rar			;wait for any sector true (0=true)
	jc	secSync

	mvi	c,altLen-1	;byte count: 137 bytes per sector

	mov	a,e		;issue write command
	out	drvCtl

;write first byte with sync bit set

wrSec0	in	drvStat		;(10)read drive status register
	ana	d		;(4)write flag (ENWD) asserted (zero)?
	jnz	wrSec0		;(10)no, keep waiting
	add	b		;(4)put byte to write into accumulator
	ori	80h		;(7)set up sync bit
	out	drvData		;(10)write the byte with sync bit

;loop to write the rest of the sector

wrSec	in	drvStat		;(10)read drive status register
	ana	d		;(4)write flag (ENWD) asserted (zero)?
	jnz	wrSec		;(10)no, keep waiting
	add	b		;(4)put byte to write into accumulator
	out	drvData		;(10)write the byte
	dcr	c		;(4)dec chars remaining
	jnz	wrSec		;(10)loop if count <> 0

;if a key has been pressed, exit.

	call	rcvStat		;any user input?
	rnz			;yes, exit to cmdLoop
	jmp	nxtSec

; badData - invalid write data value specified

badData	lxi	h,mBadDat	;bad hex value specified	
	jmp	dispMsg		;return to 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		;drive not ready: no new selection

; New drive selected. Display the selected drive

	lxi	h,mDrive	;display 'Selected Drive n'
	call	dispMsg
	lda	curDrv		;display selected drive number
	call	dispDec

; Print current track

	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
	jmp	dispDec		;display and return to cmdLoop

; noTrack - current track is undefined. Display "not set"

noTrack	lxi	h,mNoTrk
	jmp	dispMsg		;display and return to 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
	cpi	notSet		;curDrv valid yet?
	jz	noSet		;no
	sta	newDrv		;otherwise, back to curDrv, newDrv failed
	jmp	dispDec		;display and return to cmdLoop

; noSet - curDrv has not been set yet. Display "not set" as current drive number

noSet	lxi	h,mNotSet
	jmp	dispMsg		;display and return to cmdLoop

; badDrv - bad drive number specified. Display message.
	
badDrv	lxi	h,mBadDrv	;bad drive number specified	
	jmp	dispMsg		;display and return to cmdLoop

;------------------------------------------------------------------------------
; cmdTrk0 - Seek to track zero
;------------------------------------------------------------------------------
cmdTrk0	call	selDrv		;verify a drive is selected
	rnz			;selection failed: return to cmdLoop	
	call	seek0
	rnz			;exit if seek0 failed
	lxi	h,mTrack0	;display track 0 message
	jmp	dispMsg		;display and return to 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 track token		
	call	dec2bin		;convert ascii token to binary in a and b
	jnz	badTrk		;1st track specification invalid
	sta	track1		;save 1st track number value
	lda	maxTrkN
	cmp	b		;reasonable track number?
	jc	badTrk		;n: error

	call	getTokn		;get 2nd track number (if any)
	call	dec2bin
	jnz	noTrk2		;2nd track not specified
	sta	track2		;save 2nd track number
	lda	maxTrkN
	cmp	b		;reasonable track number?
	jc	badTrk		;n: error

; Two track numbers provided. Loop here toggling back and forth when
;    space bar pressed. Exit to command loop when any other key pressed.

trkLoop	lda	track1		;move to 1st track number
	call	movTrk
	call	tglTrk		;prompt to toggle tracks
	rnz			;return to cmdLoop
	lda	track2
	call	movTrk
	call	tglTrk
	jz	trkLoop
	ret			;return to cmdLoop
	
; One track number provided. Seek to that track then exit

noTrk2	lda	track1		;move to 1st track number
	jmp	movTrk		;move and return to cmdLoop

; No valid track number provided.

badTrk	lxi	h,mBadTrk	;bad track message
	jmp	dispMsg		;display and return to cmdLoop

;------------------------------------------------------------------------------
; cmdCmp - Read and display the track number from disk along with the
;    expected track number. If the expected track number is not valid
;    and a valid track number was found on the disk, set curTrk and
;    the trkTbl entry for this drive to the track number from disk.
;------------------------------------------------------------------------------
cmdCmp	call	rdTrkId		;read the track ID
	mov	b,a		;b=first track ID read
	call	rdTrkId		;read track ID again
	mov	c,a		;c=2nd track ID read
	call	rdTrkId		;read track ID again
	mov	d,a		;d=3rd track ID read

; see if any two of the three track numbers read match

	mvi	e,notSet	;set result to "not found" in e
	mov	a,c		;#1 and #2 match?
	cmp	b
	jz	idMatch		

	cmp	d		;#2 and #3 match?
	jz	idMatch		

	mov	c,b		;c=copy of #1 
	mov	a,b		;#1 and #3 match?
	cmp	d
	jnz	noTrkId		;no match found

; idMatch - verify the matched value is a valid track number

idMatch	lda	maxTrkN		;a=max track number for this drive
	cmp	c		;valid track number?
	jc	noTrkId		;no, valid track ID wasn't read

; display the track number found on disk

	mov	e,c		;e=track read from disk
	lxi	h,mTrkId	;display track ID message
	call	dispMsg
	mov	a,e		;a=track number read
	call	dispDec		;display it
	jmp	dispExp		;go display expected track

; noTrkId - valid track ID not found on disk

noTrkId	lxi	h,mNoTkId	;display no track ID message
	call	dispMsg

; dispExp - display the expected track number

dispExp	lxi	h,mExpTrk	;display expected track number
	call	dispMsg

	lda	curTrk		;a=expected track
	cpi	notSet		;track number set yet?
	jnz	dispDec		;yes, display it and exit

; noCurTk - No current track number. If the track ID read from disk is
;    valid, then set the current track to the read track

noCurTk	lxi	h,mNotSet	;"not set" message
	call	dispMsg

	lda	maxTrkN		;a=max track number for this drive
	cmp	e		;valid track number read?
	rc			;no, just exit

	mov	c,e		;c=track read from disk
	call	saveTrk		;save track in C as current track
	lxi	h,mTrkSet	;display "Current track set to"
	call	dispMsg
	lda	curTrk
	jmp	dispDec		;display track and exit

;------------------------------------------------------------------------------
;  rdTrkId - Read track number from the next sector found. Returns
;    track number in A if found. If sector hunt times out, an invalid
;    sector number is returned in A.
;    clobbers a,de,hl
;------------------------------------------------------------------------------
rdTrkId	call	ldHead		;select drive and load head
	
	lxi	d,1		;increment timeout by 1 each time
	lxi	h,-25641	;1/2 second timeout .5s/19.5us

; rtWtSect - wait for sector true with a 1/2 second timeout

rtWtSec	dad	d		;(10)increment timeout counter
	rc			;(5)return if timeout
	in	drvSec		;(10)
	rrc			;(4)sector true?
	jc	rtWtSec		;(10)

; wait for read data flag, then read first byte which has the track number

rtData	in	drvStat		;get drive status byte
	ora	a		;wait for NRDA flag true (zero)
	jm	rtData
	in	drvData		;read the sync/track byte
	ani	7fh		;get rid of sync bit
	ret

;------------------------------------------------------------------------------
; 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 with Z clear
	lxi	h,mCrLf
	jmp	dispMsg		;returns with Z set

;------------------------------------------------------------------------------
; cmdOut - Step out one track. Step is issued no matter what. 
;    If curTrk valid, update it.
;------------------------------------------------------------------------------
cmdOut	call	selDrv		;verify a drive is selected
	rnz			;selection failed

	mvi	a,dcStepO	;step out command
	out	drvCtl

	lda	curTrk		;update current track if valid
	cpi	notSet		
	jnz	decTrk		;valid, go decrement track number

	lxi	h,mStpOut	;hl->"stepped out" message
	jmp	dispMsg		;display and exit

; decTrk - decrement and display track number after a step out

decTrk	mov	c,a		;c=current track number
	ora	a		;aready at track zero?
	jz	movDisp		;yes, just display the current track
	dcr	c		;else, decrement track by one
	jmp	movExit		;save and display it

;------------------------------------------------------------------------------
; cmdIn - Step in one track. Step is issued no matter what.
;     If curTrk valid, update it.
;------------------------------------------------------------------------------
cmdIn	call	selDrv		;verify a drive is selected
	rnz			;selection failed

	mvi	a,dcStepI	;step in command
	out	drvCtl

	lda	curTrk		;update current track if valid
	cpi	notSet		
	jnz	incTrk		;valid, go increment track number

	lxi	h,mStpIn	;hl->"stepped in" message
	jmp	dispMsg		;display and exit

; incTrk - increment and display track number after a step in

incTrk	mov	c,a		;c=current track number
	lda	maxTrkN		;a=max valid track
	cmp	c
	jz	movDisp		;already at max, just display current
	inr	c		;else, increment track by one
	jmp	movExit		;save and display it

;------------------------------------------------------------------------------
; cmdHead - Load/unload head
;------------------------------------------------------------------------------
cmdHead	call	getTokn		;get next token
	lda	token		;a=token after H command
	cpi	'L'		;load head?
	jz	doLoad		;yes

	cpi	'U'		;unload head?
	jz	doUnld		;yes

	lxi	h,mNoAct	;no action take message
	jmp	dispMsg		;display and exit

;------------------------------------------------------------------------------
; 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	doUnld		;zero = head loaded, do unload

doLoad	call	ldHead		;load the head
	lxi	h,mHeadL	;display status message and exit
	jmp	dispMsg	

doUnld	call	uldHead		;unload the head
	lxi	h,mHeadU	;display status message and exit
	jmp	dispMsg		

;-----------------------------------------------------------------------------
;  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.
;       clobbers a, c, de, hl
;-----------------------------------------------------------------------------
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"
	call	dispAny		;with ". any key to abort"
waitSel	call	rcvStat		;anything from user?
	jnz	NoneSel		;y: abort
	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

;-----------------------------------------------------------------------------
; NoneSel - Prints No Drive Selected message, and returns with Z cleared
;-----------------------------------------------------------------------------
NoneSel	lxi	h,mNoDrv	;no drive selected message
	call	dispMsg
	inr	a		;force non-zero status
	ret

;-----------------------------------------------------------------------------
; seek0 - seek to track zero. Steps in 3 tracks before seeking back out
;    to track zero in case the zero stop is incorrect. The max number of
;    steps outward is the max number of tracks on the disk plus 16. These
;    extra steps will cause an SA400 minidisk to find track 0 even if the
;    actuator mechanism is out of its spiral groove.
;-----------------------------------------------------------------------------
seek0	mvi	b,3		;step in three times
stepIn	mvi	a,dcStepI	;issue step in command 
	out	drvCtl
wtStepI	in	drvStat		;get drive status register
	ani	moveMsk		;loop until OK to move the head
	jnz	wtStepI
	dcr	b		;decrement step count
	jnz	stepIn

; seek out until track zero is true, or too many steps

	mvi	a,30		;wait additional 30ms (40ms+ total)
	call	delayMs		;before direction change
	lda	maxTrkN
	adi	16		;max tries for track 0
	mov	b,a

stepOut	mvi	a,dcStepO	;issue step out command
	out	drvCtl

wtStepO	in	drvStat		;get drive status register
	ani	moveMsk		;loop until OK to move the head
	jnz	wtStepO

	in	drvStat		;check track 0 flag
	ani	trk0Msk
	jz	saveTrk		;found track 0 (c=0 from delayMs)

	dcr	b		;too many steps?
	jnz	stepOut		;no - keep stepping outward

; couldn't find track 0. Notify user.

	lxi	h,mNoTk0	;no track 0 message
	call	dispMsg
	inr	a		;force a non-zero
	ret			;return with error status

;------------------------------------------------------------------------------
;  movTrk - moves to the track specified in A. Assumes A is valid.
;     trashes a,c,de,hl
;------------------------------------------------------------------------------	
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,dcStepO	;step out to lower tracks if desired < current
	jnc	movTrk1
	mvi	d,dcStepI	;step in to higher tracks if desired > current
	cma			;make it a positive value (1's complement + 1)
	inr	a
movTrk1	mov	e,a		;e counts steps

; Issue e track steps in or out (controller step command in d)

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	
	dcr	e
	jnz	wMoveOk		;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, then display "moved to track" message
; movDisp - display "moved to track" message

movExit	call	saveTrk		;save track in curTrk and trkTbl
movDisp	lxi	h,mTrackN	;'Moved to track '
	call	dispMsg
	lda	curTrk		;display track number from binary value
	jmp	dispDec		;display and exit

;------------------------------------------------------------------------------
;  saveTrk - Save the track number passed in C to curTrk and to the
;    track table entry for the current drive.
;    clobbers a,de,hl
;------------------------------------------------------------------------------
saveTrk	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
	xra	a		;return with zero status
	ret

;------------------------------------------------------------------------------
;  ldHead - Issue the head load command and wait for
;    the head to load. Z set if successful, cleared otherwise
;    clobbers a,c,de,hl
;------------------------------------------------------------------------------
ldHead	call	selDrv		;verify a drive is selected
	rnz			;selection failed
	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. Z set if successful, cleared
;    otherwise. If minidisk, the drive is deselected instead.
;    Clobbers c,de,hl
;------------------------------------------------------------------------------
uldHead	lda	numSPT		;minidisk?
	cpi	numSPT5
	jnz	unload8		;no, 8" drive
	mvi	a,dSelect	;deselect the minidisk drive
	out	drvSel
	ret

; unload8 - unload the head of an 8" drive

unload8	mvi	a,dcUload	;issue head unload command
	out	drvCtl
	ret

;------------------------------------------------------------------------------
; getType - Prompt user to specify drive type (8" or 5.25")
;   On Exit
;      maxTrkN = 76 or 34 for 8" or 5.25"
;      numSPT = 32 or 16 for 8" or 5.25"
;------------------------------------------------------------------------------
getType	lxi	h,mDrvTyp	;display prompt to enter drive type
	call	dispMsg
	call	readCmd		;get the user's response
	lda	cmdBuf
	cpi	'8'		;8" drive?
	jz	drive8		;yes
	cpi	'5'		;5.25" drive?
	jnz	getType		;invalid response

; 5.25" drive - set number of sectors and max track number

	mvi	a,numSPT5	;set sectors per track for 5.25" drive
	sta	numSPT
	mvi	a,maxTrk5	;set max track for 5.25" drive
	sta	maxTrkN
	ret

; 8" drive - set number of sectors and max track number

drive8	mvi	a,numSPT8	;set sectors per track for 8" drive
	sta	numSPT
	mvi	a,maxTrk8	;set max track for 8" drive
	sta	maxTrkN
	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

; check for an immediate T, I, or O command

	mov	a,c		;a=current character
	cpi	'T'
	jz	test1st		;see if first character on the line
	cpi	'I'
	jz	test1st		;see if first character on the line
	cpi	'O'
	jnz	nxtChar		;not 1st on the line, continue

test1st	mov	a,b		;see if character count = 1 
	dcr	a
	jz	cmdDone		;im,ediate command at 1st position
	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	mvi	m,0		;store null terminator
	lxi	h,mCrLf		;echo carriage return, line-feed
	jmp	dispMsg

;------------------------------------------------------------------------------
; rcvChar - Return a character from the serial port in A. MSB is cleared.
; Z is cleared cleared unless received chr is a null.
;------------------------------------------------------------------------------
rcvChar	call	rcvStat		;wait for a character
	jz	rcvChar
	ret

;------------------------------------------------------------------------------
; rcvStat - Test for Serial Port A chr and get it if available. Return
;    with a=0 and Z set if no character available or if null received.
;    This routine also keeps the minidisk motor running if a minidisk
;    is selected.
;------------------------------------------------------------------------------
rcvStat	lda	numSPT		;minidisk in use?
	cpi	numSPT5
	jnz	rcvChk		;no, do the character check

	in	drvStat		;get drive status
	ani	headMsk		;head loaded (i.e., motor spinning)?
	jnz	rcvChk		;no, do the character check
	
	mvi	a,dcTimer	;issue timer reset command
	out	drvCtl		;to keep motor running

; rcvChk - do the actual
		
rcvChk	in	sioACtl		;wait for a character
	ani	sioRdrf		;set z, clear a if no chr		
	rz
	in	sioADat		;a = received character
	ani	07fh		;strip parity, clear Z unless null
	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	','		;treat 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 and b. Return zero
;   status if valid value found. Return non-zero status for error.
;   Clobbers c-e.
;------------------------------------------------------------------------------
dec2bin	mvi	b,0		;b accumulates result
	lxi	d,token		;de is token string pointer
	ldax	d		;test for null string
	ora	a
	jz	decBad
	
decLoop	ldax	d		;next ascii digit
	ora	a		;end of string?
	jz	decDone		;yes, we're done
	sui	'0'		;subtract ASCII offset
	rc			;before '0' - invalid digit (Z flag is cleared)
	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	b		;*5
	add	a		;*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 too
	ret			;zero flag was true upon entry to decDone

decBad	inr	a		;force zero flag false (a<>0FFh here.)
	ret

;------------------------------------------------------------------------------
; hex2bin - Convert ascii-hexadecimal token to binary value in a and b.
;   Return zero status if valid value found. Return non-zero status for error.
;   Clobbers c-e.
;------------------------------------------------------------------------------
hex2bin	mvi	b,0		;b accumulates result
	lxi	d,token		;de is token string pointer
	ldax	d		;test for null string
	ora	a
	jz	decBad
	
hexLoop	ldax	d		;next ascii digit
	ora	a		;end of string?
	jz	decDone		;yes, we're done
	cpi	'9'+1		;below ASCII 9?
	jc	HC1		;Yes: deal with digit
	cpi	'A'		;between 9 & A?
	rc			;y:error (Z flag is cleared.)
	sui	'A'-'9'-1	;no: subtract offset	
HC1:	sui	'0'
	cpi	10H		;above 0Fh?
	jnc	decBad		;y: error
	mov	c,a		;save digit in c for  now
	mov	a,b		;multiply value so far by 16
	add	a
	add	a
	add	a
	add	a
	add	c		;add in new digit to ones position
	mov	b,a		;leave result in b
	inx	d		;move to next digit
	jmp	hexLoop

;------------------------------------------------------------------------------
; 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'-1		;c accumulates the 10's digit in ascii

tenCnt	inr	c		;count the number of 10's in ascii
	sui	10		;divide A by 10 to get the 10's digit
	jp	tenCnt

	adi	'0'+10		;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'
	cnz	sndChar		;transmit the 10's digit
	mov	c,b		;transmits the 1's digit
	call	sndChar
	ret

;------------------------------------------------------------------------------
; dispAny - Display null terminated string, followed by '. any key to abort.
; Clobbers a,c,h,l.  Clears a, sets Z.
;------------------------------------------------------------------------------
dispAny	call	dispMsg		;print pessage
	lxi	h,mAnyKey	;followed by ". any key to abort"

; fall into dispMsg

;------------------------------------------------------------------------------
; dispMsg - Display null terminated string. Clobbers c,h,l. Clears a, sets Z.
;------------------------------------------------------------------------------
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	sioACtl		;wait until OK to xmit
	ani	sioTdre
	jz	sndChar
	mov	a,c		
	out	sioADat		;send the character
	ret

;------------------------------------------------------------------------------
; chkCpm - check if running under CP/M. CP/M flag is set true (non-zero)
;     if yes, cleared otherwise.
;------------------------------------------------------------------------------
; First, initialize entries for stand-alone

chkCpm	xra	a
	sta	cpmFlag		;clear CP/M flag
	sta	mExit		;prevent CP/M exit message from showing

; Determine if we're under CP/M or standalone. CP/M is assumed if
;   a jump instruction is present at the CP/M warm start location (0)
;   and five more jumps (e.g., a jump table) is present at the
;   jump-to destination.
		
	lda	wBoot		;see if jump instruction present for CP/M
	cpi	jmpInst
	rnz			;no, not CP/M

; A jump instruction is present at the CP/M warm boot location (0),
;   now see if that jump points to five more jumps. If so, assume CP/M

	lxi	h,wBoot+1	;point to lsb of jump address
	mov	e,m		;e=low byte of jump
	inx	h
	mov	d,m		;de=destination of jump
	mvi	b,5		;look for 5 more jumps (a jump table)
jmpTest	ldax	d		;a=opcode at jump destination
	sui	jmpInst		;another jump present?
	rnz			;no, not CP/M
	inx	d		;move to next jump
	inx	d
	inx	d
	dcr	b
	jnz	jmpTest

; Running under CP/M. Set CP/M flag, allow CP/M exit message to display

	mvi	a,' '		;replace null with a leading space to
	sta	mExit		;   allow CP/M exit message to display
	sta	cpmFlag		;set CP/M flag to non-zero value
	ret

;------------------------------------------------------------------------------
; exitCpm - if running under CP/M, prompt user to insert the CP/M
;    disk and then warm-start CP/M. Otherwise, just return.
;------------------------------------------------------------------------------
exitCpm	lda	cpmFlag		;running under CP/M?
	ora	a		;test for zero
	rz			;no, not CP/M

; Prompt user to re-insert CP/M disk, wait for response, then
;    warm boot CP/M

	mvi	a,dSelect	;de-select drive before warm boot
	out	drvSel
	lxi	h,mCpm		;display "Insert cp/m disk"	
	call	dispMsg
	call	rcvChar		;wait for a character
	jmp	wBoot		;warm boot CP/M

;------------------------------------------------------------------------------
; initSio - reset and initialize 2SIO port A if not running under CP/M
;------------------------------------------------------------------------------
initSio	lda	cpmFlag		;running under CP/M?
	ora	a
	rnz			;yes, 2SIO already initialized
	mvi	a,3		;reset ACIA
	out	sioACtl
	mvi	a,015h		;RTS on, 8N1
	out	sioACtl
	ret

;-----------------------------------------------------------------------------
; delayMs - delay for the ms specified in a
;   clobbers b,c
;-----------------------------------------------------------------------------
delayMs	mov	b,a		;b=ms to delay

delay1	mvi	c,(CPUSPED/19)	;19 is cycles in the loop below
delayLp	nop			;(4)
	dcr	c		;(5)
	jnz	delayLp		;(10)

	dcr	b		;decrement ms counter
	jnz	delay1
	ret

;------------------------------------------------------------------------------
; message strings
;------------------------------------------------------------------------------
mVer	db	cr,lf,'Altair Floppy Drive Exerciser, Ver 2.1', cr,lf,0
mDrvTyp	db	cr,lf,'Using 8" or 5.25" drives (8 or 5)? ',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	'  C        Compare the track number read from disk with',cr,lf
	db	'              the expected track number',cr,lf
	db	'  M        Measure spindle rev time',cr,lf
	db	'  S x [y]  Step to track x',cr,lf
	db	'  H L|U    Head load or unload',cr,lf
	db	'  W hh [1|2]   Write hex data to current track',cr,lf
	db	'               1=every sector, 2=every other (def)',cr,lf
mExit	db	'  X        Exit to CP/M',cr,lf,0

mHelp2	db	lf,'Immediate commands:',cr,lf
	db	'  T        Toggle head load state',cr,lf
	db	'  O        Step out one track',cr,lf
	db	'  I        Step in one track',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
mTrkId	db	'Track ID from disk: ',0
mNoTkId	db	'Track ID could not be read',0
mExpTrk	db	cr,lf,'Expected track: ',0
mTrkSet	db	cr,lf,'  ** Current track set to ',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
mNotSet	equ	$
mNoTrk	db	'not set',0
mSpace	db	cr,lf,'   Press Spacebar to toggle tracks: ',0
mStpIn	db	'Stepped in one track',0
mStpOut	db	'Stepped out one track',0
mBadDrv	db	'Invalid drive number',0
mBadTrk	db	'Invalid track number',0
mWtDrv	db	'Waiting for drive to be ready',0
mBadDat	db	'Invalid write data',0
mWrtDat	db	cr,lf,'Writing to disk',0
mNoTk0	db	'Track 0 not found',0
mMSpind	db	cr,lf,'Measuring Spindle Revs',0
mAnyKey	db	'. Press any key to abort.',0
mMsRev	db	' mS/rev',0
mCpm	db	cr,lf,'Insert CP/M disk into drive A, then press Return...',0
mTimOut	db	cr,lf,'Disk timeout',0
mCrLf	db	cr,lf,0
mComma	db	', ',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
numSPT	ds	1		;sectors/track for this type of disk
maxTrkN	ds	1		;max track number for this type of disk
trkTbl	ds	16		;current track for each drive (0-15)
cpmFlag	ds	1		;non-zero if running under CP/M
skipSec	ds	1		;non-zero if writing every other sector
	ds	64
stack	equ	$
	end
