;------------------------------------------------------------------------------ ; ; 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