;****************************************************************************** ; ; PC2Flop - Transfer disk image from PC to floppy over serial port. ; ; This program writes a Micropolis floppy disk from a disk image ; transmitted from a PC. The image is transmitted through a Sol-20 ; port using the XMODEM checksum protocol. The disk is written in ; raw format (270 bytes per sector, 16 sectors per track, ; 77 tracks). ; ; This program assumes the console port and the transfer port ; are not the same device. ; ; This program runs standalone at 0x100 or under CP/M. The program ; is exited with a CTRL-C and either warm boots CP/M or exits to ; the SOLOS/CUTER ROM. ; ; The disk image format matches the Vector Graphic Image (VGI) format ; defined by Howard Harte for his VG work on the Simh emulator. Each ; sector in the PC file is 275 bytes in length, organized as follows: ; ; Length Content ; 1 Sync byte (always 0xff) ; 1 Track number (0-76) ; 1 Sector number (0-15) ; 266 Sector data payload (CP/M uses last 256 bytes of this) ; 1 Checksum ; 4 ECC for HD-FD controller (not used by floppy, set to zero) ; 1 ECC valid flag (not used by floppy, set to zero) ; ; Written by Mike Douglas ; ; Rev Date Desc ; 1.0 05/27/14 Original ; ; 1.1 11/19/14 Use generic algorithms for buffering disk ; and XMODEM I/O to handle sector sizes that ; aren't multiples of the 128 byte XMODEM ; packet. Not really needed for the Northstar, ; but it makes this code more portable. Change ; drive validation, console I/O, serial I/O, ; etc. to make as common an interface as ; possible for different platforms. ; ; 1.2 12/02/14 Ctrl-C during transfer returns to main menu ; instead of exiting. CR required after answering ; drive and port prompts. Check for 5 jumps of ; a jump table in chkCpm. ; ; 1.3 08/13/15 Increase time from drive selection until ; I/O begins from the VG and Micropolis default ; of 250ms up to 750ms. This code can end up ; writing very quickly without reading track/sector ; ID's and takes the first sector that comes around, ; so this delay ensures a good quality write. ; ; 1.4 08/05/20 Support Mod-I (35 track) drives as well as the ; 77 track Mod-II drives by ending the reception ; if the XMODEM sender sends a shorter file. ; ;***************************************************************************** ; CUTER/SOLOS ROM/RAM locations CUTER equ 0C000h ;CUTER RETRN equ CUTER+004h ;Warm boot, jumps to COMND ; Disk information equates NUMTRK equ 77 ;number of tracks NUMSEC equ 16 ;number of sectors per track SECLEN equ 275 ;sector length (as transmitted) MINDRV equ 0 ;first drive number MAXDRV equ 3 ;max drive number TRKLEN equ (NUMSEC*SECLEN) ;length of track in bytes RDTRIES equ 5 ;number of read (verify) attempts WRTRIES equ 4 ;number of write attempts ; Monitor, CP/M boot entry points MONITOR equ RETRN ;ROM monitor entry point (stand alone mode) CPMBOOT equ 0f800h ;cold boot ROM entry point ; XMODEM equates PKTLEN equ 128 ;128 byte xmodem packet length SOH equ 001h EOT equ 004h ACK equ 006h NAK equ 015h EOF equ 01ah ;ctrl-z character ; CP/M Equates WBOOT equ 0 ;warm boot jump location ; Misc equates CR equ 13 ;ascii for carriage return LF equ 10 ;ascii for line feed DEL equ 7fh ;ascii DEL BS equ 08h ;ascii backspace CTRLC equ 03 ;ascii for control-c JMPINST equ 0c3h ;jump instruction opcode org 0100h ;----------------------------------------------------------------------------- ; Initialize for transfer ;----------------------------------------------------------------------------- init lxi sp,ourStk ;initialize stack pointer call chkCpm ;set flag for CP/M or not call sizeRam ;determine amount of RAM available mvi a,MINDRV ;a=default drive sta drvNum ;need for pgmExit ; Display welcome message, then get the drive number and port number ; to use for the transfer from the operator. lxi h,mWelcom ;display welcome message call dispMsg ; getDrv - get drive number or letter from the user. getDrv lxi h,mDrive ;display drive number prompt call dispMsg call rcvCon ;get byte from the console ori 20h ;upper case letters to lower case cpi 'x' ;exit requested? jz pgmExit ;yes lxi h,baseDrv ;ascii 1st drive - baseDrv = 0 sub m jm getDrv ;error, entry was < first drive cpi MAXDRV-MINDRV+1 ;validate <= max drive jnc getDrv ;invalid, prompt again adi MINDRV ;restore offset of 1st drive (if any) sta drvNum ;save the drive number to use ; Verify drive is not write protected call dRestor ;restore to track 0 (selects drive) call dChkWP ;check write protect (aborts to getDrv) ; getPort - get pseudo port number 1-3 from the user. getPort lxi h,mPort ;display transfer port prompt call dispMsg call rcvCon ;get byte from the console ori 20h ;upper to lower case, nums not affected cpi 'x' ;exit requested? jz pgmExit ;yes sui '1' ;validate >= '1' jm getPort ;invalid, prompt again cpi 3 ;validate <= '3' jnc getPort ;invalid, prompt again inr a ;binary 1-3 sta xfrPort ;save the pseudo port to use ; Prompt the user to insert the disk to write, initialize, then start ; sending NAK to start the XMODEM reception. lxi h,mStart ;display start file transfer prompt call dispMsg mvi a,0ffh ;init disk driver track number sta curTrk ;to "invalid" xra a ;init track we're reading to zero sta trkNum inr a ;init xmodem block number to one sta blkNum lxi h,trkBuf ;trkBuf is initial starting point for shld rcvPtr ; XMODEM receive mvi a,NAK ;send starting nak after purging input data call purgSio ;----------------------------------------------------------------------------- ; mainLp - Receive as many packets as will fit into RAM and then write ; tracks to disk. Repeat until entire disk is written. ;----------------------------------------------------------------------------- mainLp call bufPkts ;buffer as many packets as possible xchg ;de=start of last packet received lxi h,trkBuf ;hl=start of track buffer shld trkPtr ;writing will start from here call cmpDeHl ;compare last packet start - trkBuf jc allDone ;nothing received but an EOT ; trkLoop - write tracks until all disk tracks have been written or all ; the tracks in the buffer have been written. trkLoop call wrtTrk ;write a track (hl returns->next track) lda trkNum ;increment track number (preserve HL) inr a sta trkNum cpi NUMTRK ;done all tracks? jz allDone ;yes xchg ;de=next track pointer lhld rcvPtr ;hl=start of last packet received call cmpHlDe ;compare rcvPtr-start of next track jnc trkLoop ;another track is present ; All the buffered tracks have been written. Move any bytes left over ; from the xmodem receive to the start of trkbuf. Adjust rcvPtr ; to the end of that data which is where reception of the next ; packet will begin. call subHlDe ;hl=hl-de = left over byte count - PKTLEN mov a,l ;a=left over count - PKTLEN adi PKTLEN ;a=left over byte count mov b,a ;b=left over byte count (bytes to move) lxi h,trkBuf ;hl->start of trkBuf jz mvDone ;no bytes to move, we're done ; Copy the leftover data to the start of trkBuf. The next XMODEM receive ; sequence will start at the end of this copied data instead of at the ; start of trkBuf. moveLp ldax d ;move from end of trkBuf to start of trkBuf mov m,a inx h ;hl->starting bytes of trackbuf inx d ;de->unwritten bytes from end of trkBuf dcr b jnz moveLp mvDone shld rcvPtr ;save address where to receive next packet lda eotFlag ;see if EOT terminated the reception cpi EOT jz allDone ;if so, probably a single side disk mvi a,ACK ;ACK the last packet or EOT call sndByte jmp mainLp ;go receive more packets ; allDone - The disk is completely written. ACK the final packet (or EOT), ; display the "transfer complete" message and start the program over. allDone mvi a,ACK ;ACK the final packet or EOT call sndByte lxi h,mDone ;hl->done message call dispMsg jmp getDrv ;start over asking for drive num ;----------------------------------------------------------------------------- ; bufPkts - Receive XMODEM packets until we fill all the track buffer ; space available. Reception starts at the address pointed to by ; rcvPtr. Reception is stopped when the last packet exactly ; fills or "crosses" the end of the last track spot in RAM. rcvPtr is ; updated on exit to point to the start of the last packet received. ; The disk write routines use this pointer to know where data ends and ; then update rcvPtr to start to where to begin receiving new packets. ;----------------------------------------------------------------------------- bufPkts call rcvPkt ;receive a packet xchg ;de=current xmodem receive pointer sta eotFlag ;save status returned (may be EOT) cpi EOT ;EOT received? jz bufDone ;yes, buffering is done lhld bufEnd ;hl=end of buffering space call cmpDeHl ;compare current-end jnc bufDone ;past or at end, buffering is done mvi a,ACK ;otherwise, ACK the packet call sndByte xchg ;put xmodem rcv pointer back in HL jmp bufPkts ;buffer some more ; bufDone - no more room for packets. Update rcvPtr to point to the ; start of the last packet received so the disk write routines know ; where data ends. bufDone xchg ;hl=current xmodem receive pointer lxi d,-PKTLEN dad d ;hl=hl-PKTLEN = start of last rcv'd packet shld rcvPtr ;save use by write tracks ret ;----------------------------------------------------------------------------- ; rcvPkt - receive an xmodem format 128 byte packet. HL points to the 128 byte ; buffer to receive. HL is updated by 128 bytes after each succuessful ; reception of a new 128 byte packet. ;----------------------------------------------------------------------------- nakPkt pop h ;get HL back mvi a,NAK call purgSio ;purge receive for 1s then transmit NAK ;wait for SOH or EOT rcvPkt push h ;save HL for retries waitSoh call chkQuit ;check for user abort mvi a,3 ;3 second timeout for soh call rcvByte ;receive character with timeout jz nakPkt ;timeout cpi SOH ;SOH received? jz haveSoh cpi EOT ;EOT to say we're done? jnz waitSoh ;no, keep looking for SOH pop h ;undo the entry push ret ;return with EOT ; Have SOH, receive block number and not block number haveSoh mvi a,1 ;one second timeout once inside a packet call rcvByte ;get the block number jz nakPkt ;timeout, NAK and start over sta rcvBlk ;save the received block number mvi a,1 ;one second timeout call rcvByte ;get not block number jz nakPkt ;timeout, NAK and start over sta rcvNBlk ;save not block number ; Receive the 128 byte block mvi e,0 ;init checksum mvi d,pktLen ;d is byte counter pktLoop mvi a,1 ;one second timeout call rcvByte ;get next data byte jz nakPkt ;timeout mov m,a ;store the character inx h ;point to next byte add e ;update the checksum mov e,a dcr d ;decrement bytes remaining jnz pktLoop ; Verify the checksum mvi a,1 ;one second timeout call rcvByte ;get the checksum jz nakPkt ;timeout cmp e ;checksum match? jnz nakPkt ;no, packet error ; Checksum good. Verify block numbers lda rcvNBlk ;verify rcvBlk = not (rcvNBlk) cma ;A should = block number now mov b,a ;save in b lda rcvBlk cmp b ;compare rcvBlk = not(rcvNBlk)? jnz nakPkt ;no, error lda blkNum ;compare rcvBlk = expected blkNum cmp b jnz nakPkt ;nak if not the same (also nak's re-send) inr a ;increment expected block number sta blkNum pop b ;get HL off stack, but don't clobber HL xra a ;return a zero ret ;----------------------------------------------------------------------------- ; wrtTrk - write and verify NUMSEC sectors to the current track in trkBuf ; as pointed to by trkPtr. After the track is written, trkPtr is updated ; by the length of a track to point to the next track buffer. This ; saved value is also returned in HL. ;----------------------------------------------------------------------------- wrtTrk lxi h,trkNum ;get desired track from trkNum mov l,m ;and put into l call dSeek ;seek to the track call dChkWP ;check write protect mvi a,WRTRIES ;init write retry counter sta wrRetry rtryWrt mvi d,NUMSEC ;d=count of sectors to write ; wrtLoop - write a full track starting with any sector wrtLoop call dNxtSec ;wait for next (any) sector sync lhld trkPtr ;hl->start of current track buffer dad b ;bc has offset of sector within track call dWrite ;write the sector dcr d ;decrement sector count jnz wrtLoop ;loop until all sectors written ; Verify the track just written call dNxtSec ;force a 1 sector delay for trim erase mvi d,NUMSEC ;d=count of sectors to write vfyLoop call chkQuit ;check for ctrl-c call dNxtSec ;wait for next (any) sector sync lhld trkPtr ;hl->start of current track buffer dad b ;bc has offset of sector within track call dVerify ;verify the sector jnz vfyRtry ;error, go to retry logic vfyNext dcr d ;decrement sector count jnz vfyLoop ;loop until all sectors verified ; Track verified, increment trkPtr to memory location for next track ; and exit. lhld trkPtr ;hl=current track pointer lxi d,TRKLEN ;de=bytes in a track dad d ;hl=start of next track in trkBuf shld trkPtr ret ; vfyRtry - verify failed, retry reads followed by a re-write ; of the track in needed. vfyRtry mvi a,RDTRIES ;init retry counter sta rdRetry retryLp call chkQuit ;check for ctrl-c call dWtSec ;wait for sector specified in e lhld trkPtr ;hl->start of current track buffer dad b ;bc has offset of sector within track call dVerify ;verify the sector jz vfyNext ;success, go verify next sector ; Re-verify failed. Decrement retry count and try again if not zero. Once ; retry counter expires, do another write. lxi h,rdRetry ;decrement the read retry counter dcr m jnz retryLp ;try again ; read re-tries expired, decrement the write retry count and re-write ; if not expired lxi h,wrRetry ;decrement the write retry counter dcr m jnz rtryWrt ;retry starting with the write ;fall through to disk fail ;----------------------------------------------------------------------------- ; Disk write failure. Display the track and sector with the error. Restart ; the program. ;----------------------------------------------------------------------------- lxi h,errTrk ;hl->where to put ascii decimal lda trkNum ;a=track with error on it call bin2dec ;track to ascii lxi h,errSec ;hl->where to put ascii sector mov a,e ;a=sector where error occured call bin2dec lxi h,mDskErr ;display the error message call dispMsg lxi sp,ourStk ;initialize stack pointer jmp getDrv ;start over asking for drive num ;----------------------------------------------------------------------------- ; dispMsg - display the null-terminated message passed in hl on the ; console device. Clobbers b, hl ;----------------------------------------------------------------------------- dispMsg mov a,m ;get the next message byte ora a ;null terminates rz mov b,a ;conOut wants character in b call conOut inx h ;move to next byte jmp dispMsg ;----------------------------------------------------------------------------- ; rcvCon - Receive a character from the console device, echo it, then ; wait for a CR. Exits program if Ctrl-c typed. Returns with invalid ; character (null) if BS or DEL pressed after 1st character ; Returns character in a, clobbers b. ;----------------------------------------------------------------------------- rcvCon call conIn ;check for input jz rcvCon ;nothing ani 7fh cpi CTRLC ;abort requested? jz pgmExit ;yes cpi CR ;return pressed? rz ;yes, don't echo it mov b,a ;conOut needs character in b call conOut ;echo it ; Wait for CR, then return 1st character typed rcvCr call conIn ;check for input jz rcvCr ;nothing ani 7fh cpi CTRLC ;abort requested? jz pgmExit ;yes cpi DEL ;delete rz ;yes, return DEL character cpi BS ;backspace? rz ;yes, return BS character cpi CR ;return pressed? jnz rcvCr ;no, keep waiting mov a,b ;return 1st character typed ret ;----------------------------------------------------------------------------- ; purgSio - Wait for 1 second of idle time on receive line, then transmit the ; character passed in A (usually ACK or NAK). Clobbers b ;----------------------------------------------------------------------------- purgSio push a purge mvi a,1 ;1 second timeout call rcvByte ;wait for 1 second without activity jnz purge pop a ;get back the character to send ;fall through to sndByte ;----------------------------------------------------------------------------- ; sndByte - send the byte in a through the specified transfer port. ; Clobbers b. (assuming serOut clobbers only a). ;----------------------------------------------------------------------------- sndByte mov b,a ;b=byte to transmit lda xfrPort ;a=port to use for transfer jmp serOut ;send the character ;----------------------------------------------------------------------------- ; rcvByte - Receive a byte from the specified transfer port. A wait timeout ; in seconds is specified in a. If a timeout occurs, zero is returned in ; a and the zero flag is true. Otherwise, the character is returned in a ; (could be zero) and the zero flag is false. ONESEC must be set based on ; processor speed and the number of cycles in the serIn call + 59. serIn ; on the Sol-20 is the AINP call and takes about 136 cycles. ; Clobbers a, b and c. ;----------------------------------------------------------------------------- rcvByte push d ;save d, e mov d,a ;save timeout in d initSec lxi b,ONESEC ;bc=cycles through this loop for 1s rcvWait lda xfrPort ;(13) a=port to use for transfer call serIn ;(17+136) look for a byte jnz haveChr ;(10) byte received dcx b ;(5) otherwise, decrement timer mov a,b ;(5) one second expire? ora c ;(4) jnz rcvWait ;(10) no, keep waiting ; one second timeout has occured. Decrement the seconds counter. dcr d ;decrement seconds counter jnz initSec ;initialize for another 1 second count haveChr pop d ;restore d, e ret ;-------------------------------------------------------------- ; bin2dec - Binary byte in A to 2 ASCII digits at (HL) ; HL is preserved ;-------------------------------------------------------------- bin2dec mvi m,' ' ;assume zero supression sui 10 ;value less than 10? jc do1s ;yes, leading blank mvi m,'1' ;have one ten already loop10 sui 10 ;count 10s jc do1s ;done with 10s, do 1s inr m jmp loop10 do1s adi '0'+10 ;form ASCII 1s digit inx h ;move to 1s position mov m,a dcx h ;restore hl ret ;-------------------------------------------------------------- ; chkQuit - check for the user to request abort (ctrl-c). If ; a character is present on the console port, read it and ; see if ctrl-c. Clobbers A ;-------------------------------------------------------------- chkQuit call conIn ;check for console input rz ani 7fh cpi CTRLC ;abort requested? rnz ;no ; Ctrl-C typed while program is running. Return to drive prompt. lxi sp,ourStk ;initialize stack pointer jmp getDrv ;start over asking for drive num ;-------------------------------------------------------------- ; pgmExit - Exit to CP/M or to the monitor ROM based on the ; CP/M flag ;-------------------------------------------------------------- pgmExit lda cpmFlag ;running under CP/M? ora a jnz cpmExit ;yes ; Exit to ROM monitor lxi h,mExit ;display "exiting" message call dispMsg jmp MONITOR ;jump to ROM monitor entry point ; CP/M exit. If boot drive was used, prompt user to insert CP/M disk cpmExit lda drvNum ;boot drive used? sui MINDRV jnz noDisk ;not 1, disk prompt not needed lxi h,mCpm ;display "insert cp/m disk" call dispMsg call rcvCon ;wait for a character noDisk lxi h,mExit ;display "exiting" message call dispMsg jmp CPMBOOT ;reboot CP/M ;-------------------------------------------------------------- ; 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 mvi a,MINDRV+'0' ;ascii for minimum drive number sta mDrvMin ;store in the drive prompt message sta baseDrv ;ascii 1st drive - baseDrv = 0 mvi a,MAXDRV+'0' ;ascii for max drive number sta mDrvMax ; 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 dcr a ;a=0ffh sta cpmFlag ;CP/M flag to non-zero = true ; We're running under CP/M. Change drive prompt message to show drive ; letters instead of drive numbers and change baseDrv to convert ; an 'A' to the base drive number (MINDRV). mvi a,'A' ;'A' in drive message instead of number sta mDrvMin adi MAXDRV-MINDRV ;max drive letter sta mDrvMax mvi a,'a' ;ascii 1st drive - baseDrv = 0 sta baseDrv ret ;------------------------------------------------------------------ ; sizeRam - determine how much RAM we have for buffering tracks. ; Sets the bufEnd variable which points to end address of ; the last possible track buffer + 1 ;------------------------------------------------------------------ sizeRam lxi h,(trkBuf+0ffh) AND 0ff00h ;next 256 byte boundary ramLoop mov a,m ;a=current RAM content inr m ;change RAM cmp m ;did RAM change? mov m,a ;restore RAM jz ramEnd ;end of RAM found inr h ;next page jnz ramLoop ; ramEnd - end of RAM found. Subtrack 256 bytes from this to make room ; for xmodem packet overflow at the end. Determine the end address + 1 ; of the last track buffer that will fit in RAM. Store in bufEnd ramEnd xchg ;de=end of RAM + 1 mvi a,-((TRKLEN SHR 8) + 1) AND 0ffh cmp d ;force de < (10000h - TRKLEN) jnc topOk mov d,a ;limit max address topOk dcr d ;subtract 256 bytes from end of RAM lxi h,trkBuf ;hl=start of track buffer lxi b,TRKLEN ;bc=length of track in bytes ; Loop increasing hl by TRKLEN until hl > end of RAM. bfEndLp dad b ;hl=hl+track length call cmpHlDe ;compare hl-de jc bfEndLp ;still more room, keep going ; Subtrack one track length from hl, this will be the end address + 1 of ; the the last track buffer that will fit in RAM lxi b,-TRKLEN ;subtract one track length dad b ;hl = end address of last track + 1 shld bufEnd ;save as bufEnd ret ;-------------------------------------------------------------------- ; cmHlDe - compare HL-DE. Carry set if HL=DE ;-------------------------------------------------------------------- cmpHlDe mov a,l ;compare HL-DE, do lsbs first sub e mov a,h ;do msbs sbb d ret ;carry set HL=DE ;-------------------------------------------------------------------- ; cmDeHl - compare DE-HL. Carry set if DE=HL ;-------------------------------------------------------------------- cmpDeHl mov a,e ;compare DE-HL, do lsbs first sub l mov a,d ;do msbs sbb h ret ;carry set DE=HL ;-------------------------------------------------------------------- ; subHlDe - HL=HL-DE ;-------------------------------------------------------------------- subHlDe mov a,l ;subtract HL-DE, do lsbs first sub e mov l,a ;lsb result in l mov a,h ;do msbs sbb d mov h,a ;msb result in h ret ;----------------------------------------------------------------------------- ; Data Area ;----------------------------------------------------------------------------- mWelcom db cr,lf,lf db '===== PC to Floppy Image Transfer, Version 1.4 =====' db cr,lf,lf db 'This program writes a Micropolis floppy from a disk image' db cr,lf db 'received through a specified Sol-20 port using XMODEM.',0 mDrive db cr,lf,lf,'Insert and specify destination drive (' mDrvMin db 'x-' mDrvMax db 'x) or X to exit: ',0 mPort db cr,lf,lf db 'Specify the pseudo port to use for file transfer',cr,lf db ' 1) Default serial port',cr,lf db ' 2) Default parallel port',cr,lf db ' 3) User-defined pseudo port',cr,lf db 'Enter port or X to exit: ',0 mStart db cr,lf,lf db 'Start XMODEM send now...',0 mDone db cr,lf,lf,'Creation of new disk successful!',cr,lf,0 mNoTrk0 db cr,lf,lf,'Seek to track 0 failed',cr,lf,0 mStkTk0 db cr,lf,lf,'Drive stuck on track 0',cr,lf,0 mNotRdy db cr,lf,lf,'Drive not ready',cr,lf,0 mDskErr db cr,lf,lf,'Write failure on track ' errTrk db 'xx, sector' errSec db 'xx. Disk creation aborted.',cr,lf,0 mWrtPrt db cr,lf,lf,'Disk is write protected',cr,lf,0 mExit db cr,lf,lf,'Exiting...',cr,lf,0 mCpm db cr,lf,lf db 'Insert CP/M disk into drive A, then press Return...',0 ;**************************************************************************** ; ; Hardware specific console and serial I/O routines. ; The following four routines must be written to provide a common ; interface to the hardware on which this program is running. The ; port number specified for serIn and serOut matches the port number ; input from the operator via the port menu. ; ;**************************************************************************** ; The rcvByte subroutine above times a one second timeout with a code ; loop that calls the hardware specific serIn routine below. ONESEC ; must be set based on processor speed and the number of cycles in ; the serIn call + 59. serIn on the Sol-20 is the AINP call and takes ; about 136 cycles. ONESEC equ 10256 ;rcvByte loop count for 1 second ;---------------------------------------------------------------------------- ; conIn - input character from console ; inputs: ; outputs: z true if no character present ; z false if character returned in a ; clobbers none ;---------------------------------------------------------------------------- conIn equ CUTER+01Fh ;SOLOS/CUTER conIn routine ;---------------------------------------------------------------------------- ; conOut - output character to console ; inputs: b = character to send ; clobbers none ;---------------------------------------------------------------------------- conOut equ CUTER+019h ;SOLOS/CUTER conOut routine ;---------------------------------------------------------------------------- ; serIn - input character from port specified in a ; inputs: a = port to read from ; outputs: z true if no character present ; z false if character returned in a ; clobbers none ;----------------------------------------------------------------------------- serIn equ CUTER+022h ;SOLOS/CUTER READ from pseudo port in A ;----------------------------------------------------------------------------- ; Hardware specific I/O ; serOut - output character to port specified in a ; inputs: a = port to transmit through ; b = character to send ; clobbers none ;----------------------------------------------------------------------------- serOut equ CUTER+01Ch ;SOLOS/CUTER WRITE B to pseudo port in A ;**************************************************************************** ; ; DISK DRIVER FOR MICROPOLIS FLEXIBLE DISK SUBSYSTEM ; ; Created for PC2Flop and Flop2PC by Mike Douglas 11/2014 from ; the Micropolis driver code (COPYRIGHT MICROPOLIS CORPORATION 8 JUNE 1977) ; ; Original code is in all caps. New code is in lower case. ; ;**************************************************************************** ;*************************************** ; REGISTER DEFINITIONS AND ; FLAG EQUATES FOR MICROPOLIS ; FLEXIBLE DISK CONTROLLER B ;*************************************** BPROM EQU 0F800H FDCBASE EQU BPROM+0200H SCLEN EQU 270 ;sector length including sync and cksum ; FDC STATUS REGISTERS ; Sector register is at FDCBASE ; 0-3 SECTOR COUNT ; 4 SPARE ; 5 SPARE ; 6 SCTR INTERRUPT FLAG ; 7 SECTOR FLAG ; ; FLAG BITS SIFLG EQU 040H SFLG EQU 080H DTMR EQU 020H ; Status register is at FDCBASE+1 ; 0-1 UNIT ADDRESS ; 2 UNIT SELECTED (LOW TRUE) ; 3 TRACK 0 ; 4 WRITE PROTECT ; 5 DISK READY ; 6 PINTE ; 7 TRANSFER FLAG ; ; FLAG BITS TFLG EQU 80H INTE EQU 40H RDY EQU 20H WPT EQU 10H TK0 EQU 08H USLT EQU 04H ; Command register at FDCBASE (and again at FDCBASE+1) ; 0-1 COMMAND MODIFIER ; 5-7 COMMAND ; ; COMMANDS SLUN EQU 020H ;SELECT UNIT ; MODIFIER CONTAINS UNIT ADDRESS SINT EQU 040H ;SET INTERRUPT ; MODIFIER = l ENABLE INTERRUPT ; 0 DISABLE INTERRUPT STEP EQU 060H ;STEP CARRIAGE ; MODIFIER = 00 STEP OUT ; 01 STEP IN WTCMD EQU 080H ;ENABLE WRITE ; NO MODIFIER USED RESET EQU 0A0H ;RESET CONTROLLER ; NO MODIFIER USED ;------------------------------------------------------------- ; dWrite - Write a sector on current track ; On Entry: ; Drive is selected, motor is running and sector ; sync was just previously detected ; hl->buffer address ; On Exit: ; clobbers b,c,h,l ;------------------------------------------------------------- dWrite push d ;save de xchg ;de->buffer lxi h,FDCBASE+1 ;hl->FDC status register ; ENABLE WRITE AND WAIT FOR TRANSFER FLAG MVI M,WTCMD ;command register at offset of 0 and 1 dWrWait ORA M JP dWrWait ; Write 270 bytes from memory including the sync byte and checksum. ; If wrong in the memory buffer, they'll be wrong on disk. INX H ;hl->FDC data register XCHG ;de->FDC data register, hl->buffer mvi c,SCLEN/2 ;transferring 270 bytes ; Write loop dWrLoop MOV A,M ;GET BYTE FROM MEM STAX D ;WRITE TO DISK INX H ;NEXT BYTE MOV A,M ;-ETC- STAX D INX H DCR C JNZ dWrLoop pop d ;restore de RET ;------------------------------------------------------------- ; dChkWP - check if disk is write protected. ; On Entry: ; drive already selected ; On Exit: ; returns if OK to write, otherwise, restarts the program ; asking for the disk prompt ; clobbers h,l ;------------------------------------------------------------- dChkWP lxi h,FDCBASE+1 ;hl->FDC status register mov a,m ;check for write protect ani WPT rz ;return if not protected ; Protected, display message and restore state to start ; of program lxi h,mWrtPrt ;otherwise, display error message call dispMsg ;display the error lxi sp,ourStk ;re-init stack pointer jmp getDrv ;go back to drive prompt ;------------------------------------------------------------- ; dRead - Read sector on current track. Copies sync byte, ; 266 byte data section, and checksum byte. ; On Entry: ; Drive is selected, motor is running and sector ; sync was just previously detected ; hl->sector buffer ; e = sector number (used for compare) ; On Exit: ; Success = zero true (valid checksum, track, sector) ; clobbers b,c,h,l ;------------------------------------------------------------- ; First, wait for transfer ready flag (sector data available) dRead push d ;save de xchg ;de->buffer lxi h,FDCBASE+1 ;hl->FDC status register xra a ;a=0 dRdWait ora m ;wait for transfer ready flag in msb jp dRdWait ; sector data ready - pull off sync byte xchg ;hl->buffer, de->FDC status register inx d ;de->FDC data register ldax d ;read and save sync byte mov m,a inx h shld hdrPtr ;pointer to sector header (track #, sec #) ; dRdLoop - Read loop. Read 268 bytes of sector content two bytes ; at a time computing checksum in B. Carry started clear with ; ora instruction at dRdWait. lxi b,(SCLEN-2)/2 ;b=0 checksum, c=268 bytes/2 dRdLoop LDAX D ;READ FROM DISK MOV M,A ;MOVE TO BUFFER INX H ;NEXT LOC ADC B ;ADD TO CHECKSUM MOV B,A ;AND SAVE LDAX D ;NEXT READ MOV M,A ;-ETC- INX H ADC B MOV B,A DCR C ;END OF DATA? JNZ dRdLoop ;NO-LOOP ; END OF DATA-READ CHECKSUM and compare LDAX D ;load checksum from disk mov m,a ;store in read buffer inx h CMP B ;COMPARE WITH computed POP D ;RESTORE DE RNZ ;RETURN IF ERROR ; CHECKSUM OK - verify track and sector lhld hdrPtr ;hl->track number read (sector header) lda trkNum ;a=expected track number cmp m ;right track? rnz ;no, exit with error inx h ;hl->sector number read mov a,m ;a=sector number read cmp e ;right sector? ret ;return with status ;------------------------------------------------------------- ; dVerify - Verify sector on current track. Compares all 266 ; bytes of the sector plus sync and checksum. ; On Entry: ; Drive is selected, motor is running and sector ; sync was just previously detected ; hl->sector buffer to compare against ; On Exit: ; Success = zero true (valid compare) ; clobbers b,c,h,l ;------------------------------------------------------------- ; First, wait for transfer ready flag (sector data available) dVerify push d ;save de xchg ;de->buffer lxi h,FDCBASE+1 ;hl->FDC status register xra a ;a=0 dVfWait ora m ;wait for transfer ready flag in msb jp dVfWait ; Sector data ready, prepare to compare bytes xchg ;hl->buffer, de->FDC status register inx d ;de->FDC data register mvi c,SCLEN/2 ;c=270 bytes (270/2) ; dVfLoop - Verify loop. Read sector content two bytes at a time ; comparing to buffer. dVfLoop ldax d ;a=byte from disk cmp m ;match buffer? jnz dVfErr ;no, have an error inx h ;move to next byte ldax d ;a=byte from disk cmp m ;match buffer? jnz dVfErr ;no, have an error inx h ;move to next byte dcr c ;all bytes done? jnz dVfLoop ;no, keep going ; All bytes verified including sync and checksum bytes dVfErr pop d ;restore de ret ;return with status ;------------------------------------------------------------- ; dWtSec - Wait for specified sector. ; On Entry: ; Drive is selected, motor is running ; e = desired sector number ; On Exit: ; drive is at proper sector ; e = sector number ; bc = byte offset of this sector in a track ; clobbers hl ;------------------------------------------------------------- dWtSec lxi h,FDCBASE ;hl->sector register dWtSec1 mov a,m ;a=sector register ora a ;test for msb to be set jp dWtSec1 ; have sector pulse. See if this is the right sector. If so, ; return the byte offset of the sector within a track in bc ani 0fh ;get sector number alone cmp e ;right sector? jnz dWtSec1 ;no mvi h,(mult275 SHR 8) ;h=msb of "multiply by 275" table rlc ;sector*2 (2 bytes per table entry) mov l,a ;hl->sec*275 (table on 256 byte boundary) mov c,m ;get bc=sec*275 inx h mov b,m ;bc=byte offset of sector within a track ret ;------------------------------------------------------------- ; dNxtSec - Wait for next (any) sector. ; On Entry: ; Drive is selected, motor is running ; On Exit: ; drive is at proper sector ; e = sector number ; bc = byte offset of this sector in a track ; clobbers hl ;------------------------------------------------------------- dNxtSec lxi h,FDCBASE ;hl->sector register dWtSec2 mov a,m ;a=sector register ora a ;test for msb to be set jp dWtSec2 ; have sector pulse, return the sector number in e and the ; byte offset of the sector within a track in bc ani 0fh ;get sector number alone mov e,a ;return sector number in e mvi h,(mult275 SHR 8) ;h=msb of "multiply by 275" table rlc ;sector*2 (2 bytes per table entry) mov l,a ;hl->sec*275 (table on 256 byte boundary) mov c,m ;form bc=sec*275 inx h mov b,m ;bc=byte offset of sector within a track ret ;------------------------------------------------------------------ ; mult275 - table to multiply byte 0-15 by 275. This table is ; on a 256 byte boundary to speed up its use. ;------------------------------------------------------------------ org ($+255) and 0ff00h mult275 dw 00*275,01*275,02*275,03*275,04*275,05*275,06*275,07*275 dw 08*275,09*275,10*275,11*275,12*275,13*275,14*275,15*275 ;------------------------------------------------------------- ; dSeek - Seek Track. The motor timeout is restarted ; as a result of this call. ; On Entry: ; curTrk = current track, FFh means we don't know ; l = desired track ; On Exit: ; curTrk = current track ; trashes all registers ;------------------------------------------------------------- dSeek CALL SLCT ;ENSURE DRIVE SELECTED mov c,l ;leave track in c from here on lxi h,curTrk ;hl->current track mvi a,0ffh ;has head position been initialized? cmp m jnz SEEK1 ;yes CALL dRestor ;no, restore to track 0 SEEK1 mov a,c ;a=desired track SUB M ;ALREADY AT TRACK? rz ;yes, exit ; NOT AT TRACK — ISSUE THE ; APPROPRIATE NUMBER OF STEPS TO ; MOVE TO THE DESIRED TRACK mov m,c ;update curTrk with new position JM SEKOUT SEKIN CALL STEPIN DCR A JNZ SEKIN JMP SEEKR1 SEKOUT CALL STPOUT INR A JNZ SEKOUT SEEKR1 CALL SETTLE ;WAIT HEAD SETTLE RET ; STEP POSITIONER IN 1 TRACK STEPIN PUSH PSW PUSH H lxi h,FDCBASE ;STEP IN ONE TRK MVI M,STEP+1 LXI D,30 ;WAIT STEP TIME CALL TIMER POP H POP PSW RET ; STEP POSITIONER OUT 1 TRACK STPOUT PUSH PSW PUSH H lxi h,FDCBASE ;STEP IN ONE TRK MVI M,STEP ;STEP OUT ONE TRK LXI D,30 ;WAIT STEP TIME CALL TIMER POP H POP PSW RET ; WAIT HEAD SETTLE TIME SETTLE LXI D,10 ;10 MILLISECONDS CALL TIMER RET ;------------------------------------------------------------- ; dRestor - Restore to track 0. Selects the drive ; and starts the motor if needed. ; On Exit: ; Z set and curTrk=0 if track 0 found ; trashes psw,d,e ;------------------------------------------------------------- dRestor PUSH H ;save hl and bc PUSH B lxi h,curTrk ;init current track to "bad" MVI M,0FFH ;PRESET TO BAD TRK CALL RESTR1 ;RESTORE TO TK 0 MVI M,0 ;SET TRACK=0 POP B POP H RET ; RESTR1 - RESTORE TO TK 0 RESTR1 PUSH H CALL SLCT ;ENSURE UNIT selected and read lxi h,FDCBASE+1 ;hl->FDC status register MOV A,M ;ALREADY AT track zero? ANI TK0 JZ REST3 ;NO - PRESS ON ; ALREADY AT TRACK 0 - STEP ; IN 8 TIMES THEN RESTORE ; TO ENSURE GOOD POSITION MVI A,8 REST2 CALL STEPIN ;STEP IN 8 DCR A ;TRACKS JNZ REST2 CALL SETTLE ;WAIT SETTLE TIME ; STEP OUT UNTIL TRACK 0 SWITCH ; IS ACTUATED OR UNTIL 85 STEPS ; HAVE BEEN ISSUED SO THAT WE ; DONT BANG AGAINST THE STOP ; FOREVER IF TK0 SWITCH IS ; BROKEN REST3 MVI C,85 ;LOAD MAX STEPCNT REST3A MOV A,M ;TRACK 0? ANI TK0 JNZ REST4 ;YES- PRESS ON CALL STPOUT ;STEP OUT ONE TK DCR C ;MAX STEPS ? JNZ REST3A ;NO - TRY AGAIN ; MAXIMUM NUMBER OF STEPS HAVE ; BEEN ISSUED - ERROR ABORT lxi h,mNoTrk0 call dispMsg lxi sp,ourStk ;initialize stack pointer jmp getDrv ;start over asking for drive num ; FOUND TRACK 0 - WAIT ; SETTLE TIME THEN EXIT REST4 CALL SETTLE ;WAIT HEAD SETTLE POP H RET ;------------------------------------------------------------- ; SELECT DRIVE specified in drvNum ;------------------------------------------------------------- SLCT PUSH D PUSH B PUSH H lxi h,FDCBASE+1 ;hl->FDC status register lda drvNum ;a=disk number MOV B,A ;AND SAVE MOV A,M ;read selected unit from controller MOV C,A ;SAVE in c ANI 7 ;MASK selected device number XRA B ;DESIRED UNIT PREV ; NOTE-THIS TEST WILL FAIL IF CONTROLLER IS NOT PLUGGED IN MOV A,C ;SELECTED? JZ SL010 ;YES-CHECK RDY MOV A,B ;GET UNIT ADDRESS ORI SLUN ;BUILD COMMAND MOV M,A ;OUTPUT COMMAND ;WAIT 750 MSEC (up from 250ms) FOR LXI D,750 ;SECTOR CNTR TO CALL TIMER ;GET IN SYNC MOV A,M ;GET STATUS ANI 7 ;SELECTED NOW? XRA B MOV A,M ;GET STATUS AGAIN JNZ SL020 ;ERROR IF NOT SLTD SL010 ANI RDY ;ENSURE UNIT IS XRI RDY ;READY SL020 POP H POP B POP D RZ ;RETURN IF OK ; DRIVE NOT UP ERROR lxi h,mNotRdy ;drive not ready message call dispMsg lxi sp,ourStk ;initialize stack pointer jmp getDrv ;start over asking for drive num ;------------------------------------------------------------- ; TIMER - 1 MILLISECOND TIMER ; DE=(DELAY) TIME IN MSEC ; ; A IS DESTROYED ;------------------------------------------------------------- TIMER PUSH B PUSH H lxi h,FDCBASE ;hl->FDC sector register MOV A,M ;2 or 4 Mhz? ANI DTMR MVI A,96 ;1MS @2MHZ JNZ TI005 ;IF NOT ZERO, 2MHZ RLC ;DOUBLE COUNTER FOR 4MHZ TI005 MOV B,A ;SAVE COUNTER IN B TI010 MOV A,B ;COUNT SUI 1 ;DELAY LOOP-1.008 ORA A ;MSEC @500 NSEC JNZ TI010+1 ; 1MSEC EXPIRED - DECREMENT DELAY ; MULTIPLIER & CHECK FOR DONE DCX D MOV A,E ORA D JNZ TI010 POP H POP B RET ;------------------------------------------------------------- ; RAM STORAGE REQUIRED FOR DRIVER ;------------------------------------------------------------- hdrPtr ds 2 ;pointer to sector header (track #, sec #) ;************************************************************************** ; ; Data area ; ;************************************************************************** ; disk variables drvNum ds 1 ;drive number to use curTrk ds 1 ;current track number drive is on trkNum ds 1 ;track number to read trkPtr ds 2 ;pointer into trkBuf (start of next track) bufEnd ds 2 ;end address + 1 of last track spot in RAM ; retry logic variables rdRetry ds 1 ;disk read retry counter wrRetry ds 1 ;disk write retry counter ; xmodem variables xfrPort ds 1 ;pseudo port for file transfer blkNum ds 1 ;current xmodem block number rcvBlk ds 1 ;block number received rcvNBlk ds 1 ;"not" block number received rcvPtr ds 2 ;pointer for start of XMODEM send ; misc variables cpmFlag ds 1 ;non-zero if running under CP/M baseDrv ds 1 ;ascii 1st drive - baseDrv = 0 eotFlag ds 1 ;set to EOT if EOT terminated reception ds 64 ;stack space ourStk equ $ ; track buffer runs from here to the end of memory trkBuf equ $ end