;******************************************************
; North Star Disk Exerciser for CP/M, Version 1.02
;    (Single Density Controller)
;
; Commands:
;
;     BO         Boots from disk
;     LH         Load head and keep it loaded, with
;                option to nudge the head either way
;     MS         Measure spindle revolution time
;     RE         Restore to track 0
;     RS S AAAA  Read sector S into memory at
;                address AAAA
;     SK [TT]    Seek track TT
;     SU [U]     Set disk unit (1-3)
;     WS S AAAA  Write sector S from memory at
;                address AAAA
;     ?          brief help screen
;
;******************************************************
; Notes
;
; Nudging:
; The SA400 drive exhibits 'gear lash' in its head
; positioner - the head will be off-track slightly
; toward the outside diameter when a track is
; approached from the outside (stepping toward a higher
; track), and will be off-track slightly toward the
; inside diameter when a track is approached from the
; inside (stepping toward a lower track). The nudge
; sub-commands of the LH function (+ and -) allow you
; to approach the alignment track from either
; direction to observe this difference.
; 
; Use the LH function to keep the head loaded during
; radial alignment (Section 2.4.18 in the SA400
; Service Manual). Nudge the head in both directions
; while performing the alignment, and balance the
; alignment between the two directions.
;
; Spindle Rotation measurement:
; The MS command first measures the CPU speed by
; measuring the time for the Northstar controller
; to generate 125 fake sector pulses while no drive
; is selected. It then uses this measured CPU speed
; to measure the time for 50 actual sector pulses
; (5 disk revolutions). This assumes NODEXER is running
; in zero waitstate RAM.Use this function to adjust
; the drive's spindle rotation speed to be
; 200 mS/revolution. Allow the spindle motordrive to
; warm up for a few minutes before adjusting, as it
; will speed up by about 1% as it wartms up.
;******************************************************
; Revision History
; 1.00   19 November 2014  M. Eberhard
;   Created
;
; 1.01   10 October 2015 M. Douglas
;   - Fix LH (Load Head command) which previously unloaded
;     immediately after loading the head.
;
; 1.02   05 November 2015 M. Douglas
;   - Fix problem that caused one character commands (e.g.,"?")
;     to not always work. The command failed to execute if
;     the third byte in the input buffer didn't happen to
;     be zero or a valid ASCII decimal value was present.
;
;   - Verify both a sector and address are specified for the read
;     and write sector commands, otherwise the program crashed.
;
;   - If no parameter is specified for seek track (SK) or set
;     disk unit (SU), the commands now display the current
;     track or unit number.
;
;   - Update the LH command to use "H" to toggle head load
;     (drive select) in addition to space bar. Add the "M"
;     command to toggle motor on/off (on does a drive select,
;     off does de-select & motor off). Display a message on
;     each change of state to indicate what was done.
;
;   - Accept upper or lower case letters.
;
;   - Display only the banner and help instructions at
;     power up to avoid scrolling off a 16 line screen.
;
;   - Change error message to "huh?"
;
; To Do (maybe)
;  initialize disk command
;  fast-copy disk command with aggressive recovery
;******************************************************

FALSE	equ	0
TRUE	equ	not FALSE

;*************
; CP/M Equates
;*************
;------------------------------------------
;BDOS Entry Points and low-memory locations
;------------------------------------------
WBOOT	equ	0000H		;Jump to BIOS warm boot
WBOOTA	equ	WBOOT+1		;Address of Warm Boot
BDOS	equ	WBOOT+5		;BDOS Entry Point
COMBUF	equ	WBOOT+80H	;disk & cmd line buffer
USAREA	equ	WBOOT+100H	;User program area

LINBUF	equ	COMBUF		;use CP/M's buffer for input
LBSIZE	equ	64		;max input line (huge)

;------------------------------------------
;BDOS Function Codes, passed in register C
;------------------------------------------
;BRESET	equ	0	;System Reset
;BCONIN	equ	1	;Read Console Chr
;BCONOT	equ	2	;Type Chr on Console
;BPRINT	equ	9	;Print $-terminated String
BRDCON	equ	10	;Get Line from Console
;BCONST	equ	11	;Console Status (<>0 IF CHR)
;BDRST	equ	13	;Reset Disk
;BSDISK	equ	14	;select disk
;BOPEN	equ	15	;Disk File Open
;BCLOSE	equ	16	;Close disk file, FCB at de
;BSERCH	equ	17	;Search dir for file, FCB at de
;BDELET	equ	19	;delete file, FCB at (de)
;BREAD	equ	20	;Read from Disk, 0=OK, <>0=EOF
;BWRITE	equ	21	;Write next record, 0=OK, <>0=ERR
;BMAKE	equ	22	;Make new file, 0FFH=BAD
;BCDISK	equ	25	;get current disk
;BSTDMA	equ	26	;Set disk buffer to (de)

;--------------------------------------------------------
;BIOS Entry Points, relative to the base address in WBOOT
;--------------------------------------------------------
CONST	equ	06h	;Console Status
CONIN	equ	09h	;Console Input
CONOUT	equ	0Ch	;Console output

;************************************************
; Northstar disk Controller Addresses & Commands
; (Read or write from address to execute command)
;************************************************
NSCTRL	equ	0E800h		;Northstar disk controller

NSBOOT	equ	NSCTRL+100h	;disk boot PROM
NWRITE	equ	NSCTRL+200h	;write prefix (data in low byte)
NCOMND	equ	NSCTRL+300h	;commands

NASTAT	equ	NCOMND+10h	;read A status
NBSTAT	equ	NCOMND+30h	;read B status
NSTRST	equ	NCOMND+09h	;set track step flip flop
NCTRST	equ	NCOMND+08h	;clear track step flip flop
NSTOUT	equ	NCOMND+1Ch	;set step direction to 'out'
NSTIN	equ	NCOMND+1Dh	;set step direction to 'in'
NMOTRA	equ	NCOMND+90h	;motor on, read A stat
NRBYTE	equ	NCOMND+50h	;read a byte of data
NWRCRD	equ	NCOMND+04h	;start write record sequence
NRSEFL	equ	NCOMND+14h	;reset sector flag
NRESET	equ	NCOMND+18h	;reset controller

;****************************
; Disk Controller Status Bits
;****************************
NSTR0	equ	1		;NASTAT: track 0
NSWP	equ	2		;NASTAT: write protect
NSBDY	equ	4		;NASTAT: body (sync chr found)
NSWRT	equ	8		;NASTAT: write ready
NSSP	equ	0Fh		;NBSTAT: sector position
NSMO	equ	10h		;NASTAT & NBSTAT: motor on
NSWN	equ	40h		;NASTAT & NBSTAT: window flag
NSSF	equ	80h		;NASTAT & NBSTAT: sector flag

;**************************
; Disk Controller constants
;**************************
SYNC	equ	0FBh		;header sync charactor
WRMUP	equ	32h		;warm up time in sectors
SETL	equ	0Dh		;head settling time in sectors
SWAIT	equ	08Ch		;max wait for body before error
MAXUNT	equ	3		;max disk drive unit (0 not allowed)
MAXTRK	equ	39		;max track
MAXSEC	equ	9		;max sector (SA400 and SA400L)

;**************
; ASCII equates
;**************
ASCII	equ	30h		;ASCII number bias
CTRLC	equ	03h		;^C
CR	equ	0Dh		;carriage return
LF	equ	0Ah		;line feed
ESC	equ	1Bh		;"ESCAPE" KEY
QUOTE	equ	27h		;Single quote
BS	equ	5Fh		;Backspace???? for Sol-20???
DEL	equ	7Fh		;delete

;******************
; Program Constants
;******************
DPRMPT	equ	'+'		;Disk test prompt
MAXINP	equ	64		;longest allowed user input

;=============================================
; Initialization
;=============================================
	org	USAREA

	di
	lxi	sp,STACK	;local stack

	lxi	h,01FFh		;initialize CRTRAK & CRUNIT
	shld	CRTRAK

	call	CILPRT
 db 'North Star Single Density Disk Exerciser Vers. 1.02',CR,LF
 db '      by Martin Eberhard',CR,LF,LF
 db 'Type ? for help',CR,LF+80h

	jmp	CPROC		;start main loop


;**********************************
; Disk Test Subsystem Command Table
;**********************************
NSTAB:	db	'BO'		;Boot
	dw	ASKBOT
	db	'LH'		;load head
	dw	LHCMD
	db	'MS'		;measure spindle speed
	dw	NSMSPD
	db	'RE'		;restore
	dw	NSRSTR
	db	'RS'		;read sector
	dw	NSREAD
	db	'SK'		;seek track
	dw	NSSEEK
	db	'SU'		;select unit
	dw	NSUNIT
	db	'WS'		;write sector
	dw	NSWRIT
	db	'?',0		;help
	dw	NSHELP
	db	0		;table end


;******============================
;* ?  *   Help
;****** 
;==================================
NSHELP:	call	CILPRT
 db 'BO         Boot from A:',CR,LF
 db 'LH         Load/unload head (drive select/deselect)',CR,LF
 db 'MS         Measure spindle rotation',CR,LF
 db 'RE         Restore',CR,LF
 db 'SK [tt]    Seek track tt (0-39)',CR,LF
 db 'SU [u]     Select unit u (1-3)',CR,LF
 db 'RS s aaaa  Read sector s (0-9) into RAM at address aaaa',CR,LF
 db 'WS s aaaa  Write sector s (0-9) from RAM at address aaaa',CR,LF
 db LF,'Addresses are hex, all others are decimal.',CR,LF+80h
;    123456789012345678901234567890123456789012345678901234567890

	ret
;================================
;Command Processor for Subsystems
;================================

;*******************
;bogus command input
;*******************
BADINP:	call	CILPRT
	db	'huh','?'+80h

; Fall into CPROC

;*************************************************
; Command Processor Main Entry
; gets and process commands
; On Entry:
;  COMTAB = address of command table,
;           which preceeds the execution routines
;  first table byte = current prompt character
;*************************************************
CPROC:	lxi	sp,STACK	;repair stack

	call	CILPRT		;print prompt
	db	DPRMPT+80h

	lxi	d,CPROC		;create return address
	push	d		;..on the stack

;get a line of input from the user

	call	GETLIN
	rz			;No command? no error.

;Look for and execute the command if found

	lxi	h,NSTAB-1	;command table
	xchg			;command to hl
				;..table to de

;search through table at de for a 2-chr match of (hl)

NXTCOM:	inx	d		;skip over address high byte

	ldax	d
	ora	a		;table end?
	jz	BADINP		;y: unknown command

	xra	m		;test first chr
	mov	b,a		;temp save result
	inx	d		;2nd table chr
	ldax	d
	inx	h		;2nd input chr
	xra	m		;test 2nd chr

	inx	d		;point to address low byte
	inx	d		;point to address high byte

	dcx	h		;back to 1st input chr
	ora	b		;both chrs match?
	jnz	NXTCOM		;NO match: keep looking

;Got a match. Get routine address and put it on stack

	xchg			;(hl)=routine address high byte
				;de= input pointer

	mov	b,m		;get routine address from table
	dcx	h
	mov	c,m		;bc=routine address
	push	b		;command address on stack

	inx	d		;skip past 2-letter command
	inx	d

;get the following parameter (if any) and put it in hl.
;set the Z flag if no parameter is present.
;leave de pointing to the 1st chr after the 1st parameter.
;'return' to the command routine on the stack.

; fall into GETDEC

;***Subroutine**************************************
; Skip spaces, then convert ASCII decimal digits in
; input buffer to binary. Stop when a space or the
; end is encountered. Bad decimal aborts via BADINP.
; On Entry:
;    Input string starts at (de)
; On Exit:
;    Z set if no value found
;    abort to BADINP for bogus decimal or value>99
;    b=value, default = 0
;    de advanced past chr
; trashes c
;***************************************************
GETDEC:	mvi	b,0		;initial value
	call	SKIPB		;skip blanks
	rz			;no input: Z set

GDLOOP:	ldax	d		;get character
	ora	a		;end of line?
	jz	GH1		;Y: done

	cpi	' '		;end of parameter?
	jz	GH1

	sui	'0'		;un-ascii
	cpi	10		;valid?
	jnc	BADINP

	mov	c,a		;temp save new digit

	mov	a,b		;multiply by 10
	cpi	10		;will next digit overflow?
	jnc	BADINP		;too big?

	add	a		;x2
	add	a		;x4
	add	b		;x5
	add	a		;x10
	add	c		;combine digits
	mov	b,a		;save partial result

	inx	d		;bump the pointer
	jmp	GDLOOP		;get more digits

;***Subroutine**************************************
; Skip spaces, then convert ASCII hex digits in
; input buffer to binary. Stop when a space or the
; end is encountered. Bad hex aborts through BADINP.
; On Entry:
;    Input string starts at (de)
; On Exit:
;    Z set if no value found
;    abort to BADINP for bogus hex
;    hl=value, default = 0
;    de advanced past chr
; trashes a,c
;***************************************************
GETHEX:	lxi	h,0		;default value

	call	SKIPB		;skip blanks
	rz			;no input: Z set

GHEXLP:	ldax	d		;get character
	ora	a		;end of line?
	jz	GH1		;Y: done

	cpi	' '		;value separator?
	jz	GH1		;Y: done

	dad	h		;make room for the new one
	dad	h
	dad	h
	dad	h
	call	HEXCON		;do the conversion
	jc	BADINP		;bogus hex: error

	add	l
	mov	l,a		;move it in

	inx	d		;bump the pointer
	jmp	GHEXLP

GH1:	inr	a		;good value: clear Z flag
	ret

;***Subroutine*************************
;  Convert ASCII hex digit to binary
; On Entry:
;   a=chr to convert
; On Exit:
;   a=binary
;   carry clear if OK, set if bogus chr
;**************************************
HEXCON:	sui	'0'		;remove ASCII bias
	cpi	10
	cmc			;so carry means error
	rnc			;IF 0-9 then we're done

	sui	'A'-'9'-1	;subtract gap to ASCII A
	cpi	10		;netween ASCII 9 and A?
	rc			;error:return w/ carry
	cpi	10h		;above ASCII F?
	cmc			;so carry means error
	ret			;return with or w/o carry

;***Subroutine***********************************
;Get a line of input from the user
;Scan past blank positions looking for the first
;non-blank character. This assumes that LINBUF
;fits fully within one page of RAM.
; On Exit:
;   Z set, carry clear if none found
;   a=1st character value if found
;   carry set if chr is below ' '
;   de = address of 1st non-blank chr
;***********************************************
GETLIN:	mvi	c,BRDCON	;get user input
	lxi	d,LINBUF	;give CP/M a buffer
	push	d

	mvi	a,LBSIZE	;buffer size
	stax	d
	call	BDOS		;get input line

;convert lower case to upper case

	pop	d		;de->LINBUF
	inx	d		;de->byte count (from BDOS call)
	ldax	d		;a=byte count
	mov	c,a		;c=byte count

	inx	d		;de->start of input characters
	push	d		;stack has pointer to start of data

GLLOOP:	ldax	d		;a=next character from buffer
	cpi	'A'		;alpha?		
	jc	GLSKIP		;no, skip it

	ani	5fh		;convert to upper case
	stax	d
		
GLSKIP:	inx	d		;de->next character position
	dcr	c		;decrement buffer length count
	jnz	GLLOOP

;store two trailing null characters

	xra	a
	stax	d		;paste in terminating null
	inx	d		
	stax	d		;2nd null in case one char command

;skip leading blanks, and set Z for blank lines

	pop	d		;Beginning of user input

; fall into SKIPB

;***Subroutine***********************************
;Scan past blank positions looking for the first
;non-blank character.
; On Entry:
;   de = address of next input buffer chr
; On Exit:
;   Z set, carry clear if none found
;   a=1st character value if found
;   carry set if chr is below ' '
;***********************************************
SKIPB:	ldax	d		;get next character
	ora	a		;end of line?
	rz

	cpi	' '
	rnz			;we are past blanks

	inx	d		;next scan address
	jmp	SKIPB		;keep skipping

;***Subroutine*******
; Print CR, LF
; Trashes psw
;********************
PCRLF:	call	ILPRNT
	db	CR,LF+80H
	ret
		
;***Subroutine*****************************************
; Print CR, LF, then In-line Message
;  The call to ILPRNT is followed by a message string.
;  The last message chr has its msb set.
; Trashes psw
;******************************************************
CILPRT:	call	PCRLF

; fall into ILPRNT

;***Subroutine*****************************************
; Print In-line Message
;  The call to ILPRNT is followed by a message string.
;  The last message chr has its msb set.
; On Exit:
;  Z cleared
; Trashes psw
;******************************************************
ILPRNT:	xthl			;Save hl, get msg addr
	push	b

IPLOOP:	mov	a,m
	ani	7Fh		;strip end marker
	mov	c,a
	call	PRINTC		;print byte
	mov	a,m		;end?
	inx	h		;Next byte
	ora	a		;msb set?
	jp	IPLOOP		;Do all bytes of msg

	pop	b
	xthl			;Restore hl,
				;..get return address
	ret

;******==================================
;* BO *  Boot from Northstar Disk after
;******  Asking the user to insert a disk
;========================================
ASKBOT:	call	CILPRT
 db 'Boot drive A (Y/N)','?'+80h

	call	GETLIN

	cpi	'N'		;abort?
	jz	CPROC		;y: go to command

	cpi	'Y'
	jz	NSBOOT		;boot from disk

	jmp	ASKBOT

;******================================
;* LH *   Load Head and Keep it Loaded.
;******    'H' or space toggles head load (drive select)
;	   'M' toggles motor on/off 
;          '+' nudges in then out
;          '-' nudges out then in 
;          'Q' quits
;=====================================
LHCMD:	call	CILPRT
	db	'H toggles head load (drive select)',CR,LF
	db	'M toggles motor on/off (drive select & motor)',CR,LF
	db	'+ nudges in then out',CR,LF
	db	'- nudges out then in',CR,LF
	db	'Q quits',CR,LF+80h

LHSLCT:	mvi	d,0		;start with drive selected
	call	CILPRT
	db	'SELECTED (head loaded, motor started)',' '+80h

LHLOOP:	mov	a,d
	rrc
	cc	UNSLCT
	cnc	NSELCT

	call	TESTQ		;Anything from kbd?

	push	d		;save de
	lxi	d,NSTIN
	cpi	'+'		;approach from the OD?
	cz	HNUDGE

	dcr	e		;de=NSTOUT
	cpi	'-'		;approach from the ID?
	cz	HNUDGE
	pop	d		;restore de

	cpi	'M'		;motor off?
	jnz	LHCHKH		;no, go check for H command

	mov	a,d		;test current select state
	rrc
	jc	LHSLCT		;presently de-selected, go select

	inr	d		;change to de-selected state
	lda	NRESET		;reset deselects and turns off motor
	call	CILPRT
	db	'DE-SELECTED (head unloaded, motor off)',' '+80h

	jmp	LHLOOP

LHCHKH:	cpi	'H'		;toggle head state?
	jz	LHTGLH		;yes

	cpi	' '		;accept space too
	jnz	LHLOOP

LHTGLH:	mov	a,d		;test current select state
	rrc
	jc	LHSLCT		;presently de-selected, go select

	inr	d		;change to de-selected state
	call	CILPRT
	db	'DE-SELECTED (head unloaded)',' '+80h

	jmp	LHLOOP

;---Local Subroutine--------------
; Nudge the head one track
; On Entry:
;   de = NSTIN or NSTOUT
; Returns with a=0 or 2
; trashes psw,de,h
;---------------------------------
HNUDGE:	mvi	h,1		;1 step
	call	NSSTEP

	mvi	h,1		;1 step again
	mov	a,e
	xra	h		;switch direction
	mov	e,a
	jmp	NSSTEP

;---Local Subroutine--------------
; unselect drive
; trashes a, does not change carry
;---------------------------------
UNSLCT:	lda	NCOMND		;Y: un-select drive to
	ret			;..unload its head

;******=============================
;* MS *   Measure and report spindle
;******   revolution time in mSec
;===================================
NSMSPD:

;----------------------------------------------------
;Measure CPU speed by comparing to the Northstar disk
;controller's clock. (The N* contoller will generate
;32.768 mS "sector" pulses when no drive is selected. 
; (1000 * 2^16/2,000,000hz = 32.768 mS)
;----------------------------------------------------
	call	CILPRT
	db	'Measuring CPU Speed:',' '+80h

	lda	NCOMND		;select no drive

	mvi	a,125		;time 125 "sectors"
	call	TIMSEC

;divide result in (b,hl) by 8 to get CPU speed in 102.4 Hz units

	mvi	c,3

DIV8LP:	xra	a		;clear carry
	mov	a,b
	rar
	mov	b,a
	mov	a,h
	rar
	mov	h,a
	mov	a,l
	rar
	mov	l,a

	dcr	c
	jnz	DIV8LP

	mov	a,b		;result too big?
	ora	a
	jnz	TOOBIG		;y: error, exit

	lxi	d,-10240	;print CPU speed and divide
				;result by 1024 while doing
				;..decimal conversion
	mvi	a,4		;4 digits after the decimal
	call	DIVIDE		;result lands in QOTENT

	call	ILPRNT
	db	' MHz',' '+80h

;detect and report type of CPU

	sub	a		;test for 8080 or Z80
	cpe	IS8080
	cpo	ISZ80

	call	ILPRNT
	db	'80 CPU detecte','d'+80h

;compute hl= -(cpu speed) for rev-timing math

	lhld	QOTENT		;binary CPU speed
	mov	a,h		;compute -(CPU speed)
	cma
	mov	h,a
	mov	a,l
	cma
	mov	l,a
	inx	h		;complement and add 1

;------------------------------------------------
;Measure the spindle speed by counting cycles and
;dividing by the measured CPU speed. Do this over
;and over until the user types MODE Select key.
; hl = -(CPU speed)
;------------------------------------------------
	push	h		;-(cpu speed) onto stack

	call	NSELCT		;load the head

	call	CILPRT
	db	'Measuring spindle revolutions. Q to quit.',CR,LF+80h

MSLOOP:	mvi	a,50		;50 sectors to time
	call	TIMSEC		;time 50 sectors
				;..result in (b,hl)

;(b,hl) = cycle count for 50 sectors

	mov	a,b
	ani	0FEh		;17 bits max
	jnz	TOOSLO

;time (uS/rev) = ((b,hl)*10) / (CPU speed)

	pop	d		;de= -(CPU speed)
	push	d		;save for next pass

	mvi	a,2		;2 digits after decimal pt
	call	DIVIDE		;divide (b,hl) by CPU speed
				;..print result in mS

	call	ILPRNT
	db	' mSec/rev',CR,LF+80h

;Keep measuring until the user types Mode Select

MSLP2:	call	TESTQ
	jmp	MSLOOP	

; Error: CPU speed is too big

TOOBIG:	call	ILPRNT
	db	'out of rang','e'+80h
	ret

; Error: spindle is too slow

TOOSLO:	call	CILPRT
	db	'Spinde too slo','w'+80h
	jmp	MSLP2

;---Local Subroutine-----------
;Print 1st part of 8080 message
; On Exit:
;  Parity is even
;------------------------------
IS8080:	call	ILPRNT
	db	'8','0'+80h
	sub	a		;set parity even again
	ret

;---Local Subroutine-----------
;Print 1st part of Z80 message
;------------------------------
ISZ80:	call	ILPRNT
	db	'Z'+80h
	ret
	
;******============================
;* RE *   Restore Disk to Track 0
;****** 
; On Exit:
;   a=track=0
;==================================
NSRSTR:	call	NSELCT		;select drive, spin up

;step in once, in case the head is lost

	lxi	d,NSTIN		;step in once
	mvi	h,1
	call	NSSTEP		;step in now
	jz	STUCK0		;z: Stuck on track 0

;step out enough times that we should find track 0 even
;if the SA400's actuator is out of its spiral groove

	mvi	h,MAXTRK+16	;more than enough
	dcr	e		;e=NSTOUT to step out

	call	NSSTEP		;steppin' out to track 0
	rz			;z set: found track 0

;Error: track 0 not detected despite many steps outward

	call	CILPRT		;can't find track 0
	db	'No track ','0'+80h
	jmp	CPROC		;repair stack & quit

;******==============================
;* RS *   Read Sector (0-9)
;****** 
; On Entry:
;    b  = desired sector number
;   (de) = Memory address
;====================================
NSREAD:	jz	BADINP		;no parameter specified
	call	NSETUP		;set up for read/write

;Wait for sector body

	mvi	b,SWAIT		;max time until body

NSRD1:	dcr	b		;(5)
	jz	NOSYNC		;(10)no sync character
	lda	NASTAT		;(13)look for sync chr
	ani	NSBDY		;(7)
	jz	NSRD1		;(10)keep looking

	lxi	b,0		;initial CRC, 256 bytes

;read sector data

NSRD2:	lda	NRBYTE		;read a byte
	mov	m,a		;save it
	xra	b		;compute checksum
	rlc
	mov	b,a
	inx	h		;bump pointers
	dcr	c		;256 byteS
	jnz	NSRD2

;check CRC

	lda	NRBYTE		;get disk CRC
	xra	b		;compare to computed CRC
	rz			;Z flag set if OK, else error

	call	CILPRT
	db	'CRC erro','r'+80h
	ret

;******===============================
;* SU *   Set Unit (1-3)
;****** 
; On Entry:
;    b = desired unit
; On Exit:
;    CRUNIT = desired unit
;    CRTRAK < 0, since we don't Know
;            Where we are anymore
;=====================================
NSUNIT:	jnz	SUSET		;unit parameter is present

	call	CILPRT
	db	'Current unit is',' '+80h

	lda	CRUNIT		;display current unit number
	mov	b,a
	jmp	DSPDEC

SUSET:	mov	a,b
	dcr	a		;only 1-3 allowed
	cpi	MAXUNT
	jnc	BADINP

	inr	a		;1-3
	sta	CRUNIT		;remember new unit

	cma			;sets msb
	sta	CRTRAK		;don't know where we are

	ret			;ret WITH Z clear

;******==============================
;* WS *   Write Sector (0-9)
;****** 
; On Entry:
;    b = desired sector number
;   (de) = Memory address
;====================================
NSWRIT:	jz	BADINP		;no parameter specified
	lda	NASTAT		;check for write protect
	ani	NSWP
	jnz	WPERR

	call	NSETUP		;set up for read/write

;wait for controller to be ready to write the sector

	lda	NWRCRD		;prepare to write

NSWR1:	lda	NASTAT		;wait for controller
	ani	NSWRT		;..to get ready to write.
	jz	NSWR1

;write sector header

	lxi	b,NWRITE	;C=0,b=write prefix
	lxi	d,000FH		;number of header zeros,
				;init CRC calculation

NSWR2:	ldax	b		;write now.
	dcr	e		;count header zeros
	jnz	NSWR2

;write sector sync character

	mvi	c,SYNC		;write sync chr
	ldax	b

;write  sector data

NSWR3:	mov	c,m		;get chr from RAM
	mov	a,c		;Calculate CRC
	xra	d		;keep CRC in d
	rlc
	mov	d,a
	ldax	b		;write chr to disk
	inx	h		;bump pointers
	dcr	e		;write 256 bytes
	jnz	NSWR3

;write sector CRC

	mov	c,d		;Write CRC to disk
	ldax	b

	ret			;don't wait for end of sector
				;..since user is slow

;******============================
;* SK *   Seek Track (0-39 decimal)
;******   (Supports also SA400L)
; On Entry:
;    b=  desired track
;==================================
NSSEEK:	jnz	SKTRAK		;track param present, do seek

	call	CILPRT
	db	'On track',' '+80h

	lda	CRTRAK		;a=current track number
	ora	a
	jm	SKNOTK		;msb set=track undefined

	mov	b,a		;otherwise, display track number
	jmp	DSPDEC

SKNOTK:	call	ILPRNT
	db	'unknow','n'+80h
	ret

SKTRAK:	mov	a,b
	cpi	MAXTRK+1
	jnc	BADINP		;bogus?

	mov	l,a		;save track number

	call	NSELCT		;select drive, spin up

	lda	CRTRAK		;where are we?
	ora	a		;0FFh means we don't know
	cm	NSRSTR		;lost: restore

;compute the required number of steps and the direction

	sub	l		;which way to step?
	rz			;already there?

	lxi	d,NSTOUT	;desired<current
	jp	NSSK1

	cma
	inr	a		;a:=-a
	inr	e		;de=NSTIN: desired>current
NSSK1:

;remember target track, and step there

	mov	h,a		;number of steps

	mov	a,l		;remember target track
	sta	CRTRAK		;..is new track, when done
;Fall into NSSTEP

;***Subroutine*******************************
; Step Head
; On Entry:
;   h = number of steps, >0
;  de = step command: NSTIN or NSTOUT
; On Exit:
;   Z set, a=0, and CRTRAK=0 if track 0 found
;   a = 2 otherwise
; trashes psw,bc, h
;********************************************
NSSTEP:	ldax	d		;set step direction

NSSTP1:	lda	NSTRST		;Reset step flipflop
	xthl			;(18)must be here for delay.
	xthl			;(18)at least 10 uSec

	mvi	b,2		;(7)step timer
	lda	NCTRST		;set step flipflop

	call	NSWAIT		;wait 2 sector times

	lda	NASTAT		;at track 0?
	rar			;NSTR0 is bit 0
	jc	NSSTP2		;y: stop stepping

	dcr	h		;n: step again
	jnz	NSSTP1		;stepped enough?

	inr	h		;clear Z: not track 0
	ret

;on track 0. clear CRTRAK, and return with Z set

NSSTP2:	xra	a		;set Z: track 0
	sta	CRTRAK
	ret

;***Subroutine******************************
; Set up read or write, then wait for sector
; On Entry:
;    b = desired sector number
;   (de) = target memory address
; On Exit:
;     hl = memory address
;   Z cleared if an error occured
; trashes psw,bc,de
;*******************************************
NSETUP:	mvi	a,MAXSEC	;legal sector?
	cmp	b		;n: set carry
	jc	BADINP		;bad sector number

	push	b		;save b=sector	

	call	GETHEX		;get hl=memory address
	jz	BADINP		;address not present

	call	NSELCT		;select drive, spin up

	pop	d		;recover d=desired sector

;Wait for desired sector, following a sector pulse

NSRWS1:	mvi	b,1
	call	NSWAIT		;wait for next sector pulse

	lda	NBSTAT		;get current sector number
	ani	NSSP		;just the sector number bits
	cmp	d		;is this the sector we want?
	rz			;y: return quickly

	jmp	NSRWS1		;n: wait for next sector

;***Subroutine********************************
; Turn on the motor if necessary, Select drive
; On Entry:
;   CRUNIT = desired drive (1-3)
; trashes psw,bc
;*********************************************
NSELCT:	mvi	b,WRMUP-SETL	;motor warmup time

;long delay if motors are off

	lda	NMOTRA		;turn on drive motors
	ani	NSMO		;were they already on?
	cz	NSWAIT		;n: spin them up

;select drive, load head

	mvi	b,NCOMND/256	;select drive
	lda	CRUNIT
	mov	c,a
	ldax	b		;Load head on drive c

;wait for settling time after head load

	mvi	b,SETL		;head settling time

;fall into NSWAIT to stall

;***Subroutine***********************
; Wait for b sectors
; On Entry:
;   b = number of sectors to wait for
; trashes psw,b
;************************************
NSWAIT:	lda	NRSEFL		;reset sector flag

NSWAT1:	lda	NMOTRA		;get a status, bump motor
	ora	a		;test NSSF: sector hole?
	jp	NSWAT1		;nope.

	dcr	b		;wait for b sectors to pass
	rz

	jmp	NSWAIT

;***Subroutine**************************************
; Time sectors
; On Entry:
;   a = number of sectors to time
; On Exit:
;    b,hl = total number of passes
;           where each pass is 50 CPU cycles
;
;Timing in this subroutine: (8080 cycles)[Z80 cycles]
; Trashes psw,c,de
;****************************************************
TIMSEC:	mov	c,a		;save no. of sectors

;Wait for initial sector
	lda	NRSEFL+80h	;reset sector flag
				;..and keep motor on

NSTLP0:	xra	a		;(4)[4](stall)
	dad	d		;(10)[11]bump timer (stall)
	adc	b		;(4)[4]catch carry bit(stall)
	mov	b,a		;(5)[4]b=high byte(stall)

	lda	NBSTAT		;(13)[13]sector number and flag
	ora	a		;(4)[4]wait for valid sector
	jp	NSTLP0		;(10)[10]NSSP is bit 7

;set up timer and timer incrementer. Match time outside loop below.

	mvi	b,0		;(7)[7]clear MSB of timer
	lxi	h,0		;(10)[10]clear rest of timer
	lxi	d,1		;(10)for incrementing
	lxi	d,1		;(10)stall

;time b sectors using c,hl as the timer

NSTLP2:	lda	NRSEFL+80h	;(13)[13]reset sector flag
				;..and keep motor on

NSTLP1:	xra	a		;(4)[4]
	dad	d		;(10)[11]bump timer (de=1)
	adc	b		;(4)[4]catch carry bit
	mov	b,a		;(5)[4]b=high byte

	lda	NBSTAT		;(13)[13]sector number and flag
	ora	a		;(4)[4]wait for valid sector
	jp	NSTLP1		;(10)[10]NSSF is msb

;inner loop: 50 cycles per spin

	nop			;(4)[4]stall
	xra	a		;(4)[4]
	dad	d		;(5)[6]make up for time outside loop
	adc	b		;(4)[4]catch carry bit
	mov	b,a		;(5)[4]b=high byte

	dcr	c		;(5)[4]next sector
	jnz	NSTLP2		;(10)[10]

;exactly 50 cycles outside the inner loop [49 for Z80]

;b,hl = total cycles
;time/revolution (uS) = ((b,hl*50)/5)/CPU speed (MHz)
;time/revolution (uS) = (b,hl*10)/CPU speed (MHz)

	ret

;***Subroutine***********************************
; Print b,hl/de in decimal on the console, and
; compute result in binary in QOTENT
; On Entry:
;   (b,hl)=Dividend
;   de=divisor
;   a = number of digits after decimal point
; On Exit:
;   QOTENT = result in binary
; Trashes all registers
;************************************************
DIVIDE:	mvi	c,5		;digit counter

	push	h		;initialize quotent
	lxi	h,0
	shld	QOTENT
	pop	h

DDLOOP:	push	psw		;save decimal point place
	push	b		;save digit count

;Print decimal point if it's the right place

	cmp	c		
	mvi	c,'.'
	cz	PRINTC

;compute & print one ASCII digit: c=((b,hl)/de)+'0'
;and compute b,hl = remainder X 10

	call	DECDIG		;compute/print digit

;compute binary partial result in QOTENT

	push	h
	push	d

	lhld	QOTENT		;multiply result so far by 10
	mov	d,h
	mov	e,l
	dad	h		;X2
	dad	h		;X4
	dad	d		;X5
	dad	h		;X10

	mov	a,c		;de-ASCII digit
	sui	'0'
	mov	e,a
	mvi	d,0		;de=new digit in binary
	dad	d		;add in new digit

	shld	QOTENT		;binary result so far

	pop	d
	pop	h

	mov	a,b		;remainder high byte
	pop	b		;recover c=digit count
	mov	b,a		;but keep new high byte

	pop	psw		;decimal point place

;see if we've done enough digits

	dcr	c
	jnz	DDLOOP

	ret

;---Local Subroutine--------------------------------
; Divide hl by bc and print 1-digit result
; On Entry:
;   (b,hl)=Dividend
;   de=-(divisor)
; On Exit:
;   Quotent has been printed in decimal
;   (b,hl)=remainder X 10
;   c=current digit of quotent in ASCII
; trashes psw
;---------------------------------------------------
DECDIG:	mov	a,b		;a=dividend high byte
	lxi	b,0FF00h+'0'-1	;init c=ASCII quotient
				;..(will go 1 too many)
				;create b=MSB of divisor

;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

DIGLP:	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	DIGLP

	pop	h		;hl=remainder

; c = quotent in ASCII, hl = remainder in binary
; Compute (b,hl) := hl X 10 (could be 19 bits)

	push	d
	mov	d,h		;temp save X1
	mov	e,l

	xra	a
	mov	b,a

	dad	h		;X2
	adc	b		;b=0: catch carry

	dad	h		;x4
	adc	a		;MSB
		
	dad	d		;x5
	adc	b		;b=0: catch carry

	dad	h		;X10
	adc	a		;MSB

	mov	b,a		;(b,hl) = remainder X 10

	pop	d
	
;fall into PRINTC to print quotent digit in c

;***Subroutine*******************
; Print character in c on console
; trashes psw
;********************************
PRINTC:	mvi	a,CONOUT

; fall into GOBIOS

;***Subroutine**********************
; Go call a BIOS driver directly
; On Entry:
;   c=value for BIOS routine, if any
;   a = BIOS call address offset
; On Return:
;   psw as BIOS left it
;   all other regs preserved
;***********************************
GOBIOS:	push	h
	push	d
	push	b

	call	DOBIOS

	pop	b
	pop	d
	pop	h
	ret

;***Subroutine**********************
; Go call a BIOS driver directly
; On Entry:
;   c=value for BIOS routine, if any
;   a = BIOS call address offset
; On Return:
;   all regs as BIOS left them
;***********************************
DOBIOS:	lhld	WBOOTA		;get BIOS base address
	mov	l,a		;a has jump vector

	pchl			;'call' BIOS routine

;*********************************
; Disk Error Reporting Subroutines
;*********************************
STUCK0:	call	CILPRT
	db	'Track 0 stuc','k'+80h
	jmp	CPROC		;repair stack & quit

NOSYNC:	call	CILPRT
	db	'No syn','c'+80h
	ret

WPERR:	call	CILPRT
	db	'Write protec','t'+80h
	ret

;***Subroutine******************************
;DSPDEC - Display value in b as one or two
;   decimal digits (0-99 only)
; On Exit:
;   Trashes psw,b,c
;*******************************************
DSPDEC:	mvi	c,'0'-1		;c contains 10's result in ASCII
	mov	a,b		;a=value to convert

DD10LP:	inr	c		;count 10's
	sbi	10
	jnc	DD10LP

	adi	10+'0'		;compute 1's digit in ASCII
	mov	b,a		;save in b
	
	mov	a,c		;leading zero in 10's digit?
	cpi	'0'
	cnz	PRINTC		;no, print 10's digit
	
	mov	c,b		;print ones digit
	jmp	PRINTC

;***Subroutine******************************
;Test for Quit abort from user
; On Exit:
;   a=0 and Z set if no chr
;   Z clear and a=input chr if there was one
;*******************************************
TESTQ:	mvi	a,CONST
	call	GOBIOS
	ora	a		;any chr waiting?
	rz			;N: ret with z set

	mvi	a,CONIN
	call	GOBIOS		;get the chr

	cpi	'A'		;letter?
	rc			;n: done

	ani	5fh		;strip parity, make upper case

	cpi	'Q'

	rnz			;z cleared

;fall into ABORT

;*********************
;Abort EPROM operation
;*********************
ABORT:	jmp	CPROC

;============================
; RAM Variables
;============================
CRTRAK:	ds	1	;current track, <0 means lost
CRUNIT:	ds	1	;current unit (must follow CRTRAK)
QOTENT:	ds	2	;Binary quotent result
	ds	80	;40-deep stack
STACK:

	END
