;		    FINDBAD.ASM ver. 5.4
;		     (revised 05/21/81)
;
;	     NON-DESTRUCTIVE DISK TEST PROGRAM
;
;FINDBAD will find all bad blocks on a disk and build a file
;named [UNUSED].BAD to allocate them, thus "locking out" the
;bad blocks so CP/M will not use them.
;
;Originally written by Gene Cotton,  published in "Interface
;Age", September 1980 issue, page 80.
;
;See notes below concerning 'TEST' conditional assembly option,
;SYSTST and BADUSR directives.
;
;********************************************************
;*							*
;*			  NOTE				*
;*							*
;*   This program has been re-written to allow it to	*
;* work  with (hopefully)  all CP/M 2.x systems, and	*
;* most 1.4 CP/M systems. It has been tested on sev-	*
;* eral different disk systems, including Northstar,	*
;* Micropolis, DJ2D, and  Keith  Petersen's 10 MByte    *
;* hard disk system.  I have tested it personally on	*
;* my "modified" Northstar, under several  different	*
;* formats (including >16K per extent), and have ob-	*
;* no difficulties.					*
;*  If you have have difficulties getting this	pro-	*
;* gram  to run, AND if you  are using CP/M 2.x, AND	*
;* if  you  know  your CBIOS  to be  bug-free, leave	*
;* me a message on the CBBS mentioned below ... I am	*
;* interested in making this program  as "universal"	*
;* as possible. 					*
;*  I can't help with any version of CP/M 1.4, other    *
;* than  "standard" versions  (whatever that means),	*
;* because there are just too many heavily  modified	*
;* versions available.					*
;*  One  possible  problem you may  find is with the	*
;* system tracks of your diskettes...if they are  of	*
;* a  different density  than the data	tracks, then	*
;* see the note regarding the "SYSTST" equate.		*
;*							*
;*				Ron Fowler		*
;*				Westland, Mich		*
;*				7 April, 1981		*
;*							*
;********************************************************
;
;SYSTST and BADUSR options:
;  Many double-density disk systems have single-density system 
;tracks.  If this is true with your system, you can change the 
;program to skip the system tracks,  without re-assembling it.  
;To do this, set the byte at 103H to a 0 if you don't want the 
;system  tracks tested,  otherwise leave it 1.   This is  also 
;necessary if you have a "blocked" disk system;  that is, when 
;the same physical disk is seperated into logical disks by use 
;of the SYSTRK word in the disk parameter block.
;   If you are a CP/M 2.x user, you may assign the user number 
;where  [UNUSED.BAD] will be created by changing the  byte  at 
;104H  to  the  desired user number.   If you want it  in  the 
;default user,  then leave it 0FFH.  CP/M 1.4 users can ignore 
;this byte altogether.
;
;Note that these changes can be done with DDT as follows:
;
;		A>DDT FINDBAD.COM
;		-S103
;		103 01 0	;DON'T TEST SYSTEM TRACKS
;		104 FF F	;PUT [UNUSED.BAD] IN USER 15
;		105 31 .	;DONE WITH CHANGES
;		-^C
;		A>SAVE XX FINDBAD.COM
;
;----------------------------------------------------------------
;NOTE: If you want to  update this program, make sure you have
;the latest version first.  After adding your changes, please
;modem a copy of the new file to "TECHNICAL CBBS" in Dearborn,
;Michigan - phone 313-846-6127 (110, 300, 450 or 600 baud).
;Use the filename FINDBAD.NEW.	 (KBP)
;
;Modifications/updates: (in reverse order to minimize reading time)
;
;05/21/81 Corrected error in description of how to set SYSTST
;	  byte at 103h.  Added CRLF to block error message. (KBP)
;
;05/19/81 Corrected omission in DOLOG routine so that BADUSR
;	  will work correctly. Thanks to Art Larky. (CHS)
;
;04/10/81 Changed extent DB from -1 to 0FFH so program can be
;	  assembled by ASM.  Added BADUSR info to instructions
;	  for altering with DDT.  (KBP)
;
;04/09/81 Changed sign-on message, added control-c abort test,
;	  added '*' to console once each track	(RGF)
;
;04/07/81 Re-wrote to add the following features:
;		1) "Universal" operation
;		2) DDT-changeable "SYSTRK" boolean (see above)
;		3) Report to console when bad blocks are detected
;		4) Changed the method of printing the number of
;		   bad blocks found (at end of run)...the old
;		   method used too much code, and was too cum-
;		   bersome.
;		5) Made several cosmetic changes
;
;			Ron Fowler
;			Westland, Mich
;
;03/23/81 Set equates to standard drive and not double-sided. (KBP)
;
;03/01/81 Corrected error for a Horizon with double sided drive.
;	  This uses 32k extents, which code did not take into account.
;	  (Bob Clyne)
;
;02/05/81 Merged 2/2/81 and 1/24/81 changes, which were done
;	  independently by Clyne and Mack.  (KBP)
;
;02/02/81 Added equates for North Star Horizon - 5.25" drives,
;	  double density, single and double sided. (Bob Clyne)
;
;01/24/81 Added equates for Jade DD disk controller
;	  (Pete H. Mack)
;
;01/19/81 Added equates for Icom Microfloppy 5.25" drives.
;	  (Eddie Currie)
;
;01/05/81 Added equates for Heath H-17 5.25" drives.
;	  (Ben Goldfarb)
;
;12/08/80 Added equates for National Multiplex D3S/D4S
;	  double-density board in various formats.
;	  (David Fiedler)
;
;09/22/80 Added equates for Morrow Disk Jockey 2D/SS, 256,
;	  512 and 1024-byte sector options.  Fix 'S2' update
;	  flag for larger max number of extents. Cleaned up
;	  file. (Ben Bronson and KBP)
;
;09/14/80 Corrected DGROUP equate for MMDBL. Added new routine
;	  to correct for IMDOS group allocation.  Corrected
;	  error in instructions for using TEST routine.
;	  (CHS) (AJ) (KBP) - (a group effort)
;
;09/08/80 Fixed several errors in Al Jewer's mods.  Changed
;	  return to CP/M to warm boot so bitmap in memory will
;	  be properly updated. Added conditional assembly for
;	  testing program. (KBP)
;
;09/02/80 Added IMDOS double-density equates & modified for 
;	  more then 256 blocks per disk. (Al Jewer)
;
;09/01/80 Changed equates so that parameters are automatically
;	  set for each disk system conditional assembly (KBP)
;
;08/31/80 Add conditional assembly for Digital Microsystems FDC3
;	  controller board in double-density format and fix to
;	  do 256 blocks in one register. (Thomas V. Churbuck)
;
;08/31/80 Correct MAXB equate - MAXB must include the directory
;	  blocks as well as the data blocks.  Fix to make sure
;	  any [UNUSED].BAD file is erased before data area is
;	  checked. (KBP)
;
;08/30/80 Added conditional assembly for Micromation
;	  double-density format. (Charles H. Strom)
;
;08/27/80 Fix missing conditional assembly in FINDB routine.
;	  Put version number in sign-on message. (KBP)
;
;08/26/80 Modified by Keith Petersen, W8SDZ, to:
;	  (1) Add conditional assembly for 1k/2k groups
;	  (2) Add conditional assembly for standard drives
;	      and Micropolis MOD II
;	  (3) Make compatible with CP/M-2.x
;	  (4) Remove unneeded code to check for drive name
;	      (CP/M does it for you and returns it in the FCB)
;	  (5) Changed to open additional extents as needed for
;	      overflow, instead of additional files
;	  (6) Add conditional assembly for system tracks check
;	      (some double-density disks have single-density
;	      system tracks which cannot be read by this program)
;	  (7) Increased stack area (some systems use more than
;	      others).
;
;08/06/80 Added comments and crunched some code.
;	  KELLY SMITH.	805-527-9321 (Modem, 300 Baud)
;			805-527-0518 (Verbal)
;
;
;			Using the Program
;
; Before  using this program to "reclaim" a diskette,  it  is
;recommended that the diskette be reformatted. If this is not
;possible,  at least assure yourself that any existing	files
;on the diskette  do not contain unreadable  sectors.  If you
;have changed disks since the last warm-boot, you  must warm-
;boot again before running this program.
;
; To  use the program,	insert	both the disk containing  the
;program  FINDBAD.COM and the diskette to be checked into the
;disk drives. It is possible that the diskette containing the
;program is the one to be checked. Assume that the program is
;on drive "A" and the suspected bad disk is on drive "B".  In
;response to the CP/M prompt "A>",  type in FINDBAD B:.  This
;will  load the file FINDBAD.COM from drive "A" and test  the
;diskette  on  drive "B" for  unreadable  sectors.  The  only
;allowable  parameter  after  the  program name  is  a	drive
;specification	(of the form " N:") for up to four (A  to  D)
;disk drives.  If no drive is specified, the currently logged
;in drive is assumed to contain the diskette to check.
;
; The  program first checks the CP/M System tracks (0 and 1),
;and  any  errors here prohibit the disk from being  used  on
;drive	"A",  since all "warm  boots" occur using the  system
;tracks from the "A" drive.
;
; The  program next checks the first two data blocks  (groups
;to some of us) containing the directory of the diskette.  If
;errors  occur	here,  the  program  terminates  and  control
;returns  to  CP/M  (no other data blocks are  checked	since
;errors in the directory render the disk useless).
;
; Finally,  all  the remaining data blocks are	checked.  Any
;sectors  which  are  unreadable cause the data  block	which
;contains them to be stored temporarily as a "bad block".  At
;the end of this phase,  the message "XX bad blocks found" is
;displayed (where XX is replaced by the number of bad blocks,
;or "No" if no read errors occur).  If bad blocks occur,  the
;filname [UNUSED].BAD is created, the list of "bad blocks" is
;placed  in  the allocation map of the	directory  entry  for
;[UNUSED].BAD,	and the file is closed.  Note,	that when the
;number of "bad blocks" exceeds 16,  the  program  will  open
;additional  extents  as  required  to	hold the overflow.  I
;suggest that if the diskette has more than  32 "bad blocks",
;perhaps it should be sent to the "big disk drive in the sky"
;for the rest it deserves.
;
; The  nifty part of all this is that if any "bad blocks"  do
;occur, they are allocated to [UNUSED].BAD and no longer will
;be available to CP/M for future allocation...bad sectors are
;logically locked out on the diskette!
;
;
;	       Using the TEST conditional assembly
;
;A  conditional  assembly has been added to allow  testing  this 
;program  to  make sure it is reading all sectors on  your  disk 
;that  are accessible to CP/M.	The program reads the disk on  a 
;block by block basis, so it is necessary to first determine the 
;number of blocks present.  To start, we must know the number of 
;sectors/block (8 sectors/block for standard IBM single  density 
;format).  If  this  value  is	not  known,  it  can  easily  be 
;determined  by saving one page in a test file and interrogating 
;using the STAT command:
;
;	A>SAVE 1 TEST.SIZ
;	A>STAT TEST.SIZ
;
;For standard single-density STAT will report this file as being
;1k.  The file size reported (in bytes) is the size of a  block. 
;This  value  divided  by 128 bytes/sector  (the  standard  CP/M 
;sector  size)	will  give sectors/block.  For	our  IBM  single 
;density example, we have:
;
;  (1024 bytes/block) / (128 bytes/sector) = 8 sectors/block.
;
;We  can now calculate blocks/track (assuming we know the number 
;sectors/track). In our example:
;
;  (26 sectors/track) / (8 sectors/block) = 3.25 blocks/track
;
;Now  armed with the total number of data tracks (75 in our  IBM 
;single density example), we get total blocks accessible: 
;
;  75 (tracks/disk) x (3.25 blocks/track) = 243.75 blocks/disk 
;
;CP/M cannot access a fractional block, so we round down (to 243 
;blocks  in  our  example).  Now  multiplying  total  blocks  by 
;sectors/block	results in total sectors as should  be	reported 
;when TEST is set TRUE and a good disk is read. For our example, 
;this value is 1944 sectors. 
;
;Finally,  note that if SYSTST is set to 0,  the sectors present 
;on  the  first  two tracks must be added in  as  well.  In  the 
;previous  example,  this  results in  1944 + 52 = 1996  sectors 
;reported by the TEST conditional.
;
;Run the program on a KNOWN-GOOD disk.	It should report that it
;has read  the	correct number of sectors.  The test conditional
;assembly should then be set FALSE and the program re-assembled.
;The test routines  cannot be left in  because this program does
;not read all the sectors in a block that is found to be bad and
;thus will report an inaccurate number of sectors read.
;
;
;Define TRUE and FALSE
;
FALSE	EQU	0
TRUE	EQU	NOT FALSE
;
;******************************************************************
;
;Conditional assembly switch for testing this program
;(for initial testing phase only - see remarks above)
;
TEST	EQU	FALSE	;TRUE FOR TESTING ONLY
;
;******************************************************************
;
;System equates
;
BASE	EQU	0	;STANDARD CP/M BASE ADDRESS (4200H FOR ALTCPM)
BDOS	EQU	BASE+5	;CP/M WARM BOOT ENTRY
FCB	EQU	BASE+5CH;CP/M DEFAULT FCB LOCATION
;
;Define ASCII characters used
;
CR	EQU	0DH	;CARRIAGE RETURN CHARACTER
LF	EQU	0AH	;LINE FEED CHARACTER
TAB	EQU	09H	;TAB CHARACTER
;
DPBOFF	EQU	3AH	;CP/M 1.4 OFFSET TO DPB WITHIN BDOS
TRNOFF	EQU	15	;CP/M 1.4 OFFSET TO SECTOR XLATE ROUTINE
;
;
	ORG	BASE+100H
;
	JMP	START	;JMP AROUND OPTION BYTES
;
;If you want the system tracks tested, then
;put a 1 here, otherwise 0.
;
SYSTST: DB	1	;0 IF NO SYS TRACKS, OTHERWISE 1
;
;If you are a CP/M 2.x user, change this byte
;to the user number you want [UNUSED].BAD to
;reside in.  If you want it in the default
;user, then leave it 0FFH.  CP/M 1.4 users
;can ignore this byte altogether.
;
BADUSR: DB	0FFH	;USER # WHERE [UNUSED.BAD] GOES
			;0FFH = DEFAULT USER
;
START:	LXI	SP,NEWSTK ;MAKE NEW STACK
	CALL	START2	;GO PRINT SIGNON
	DB	CR,LF,'FINDBAD - ver 5.4'
	DB	CR,LF,'Bad sector lockout '
	DB	'program',CR,LF
	DB	'Universal version',CR,LF
	DB	CR,LF,'Type CTL-C to abort',CR,LF,'$'
;
START2: POP	D	;GET MSG ADRS
	MVI	C,9	;BDOS PRINT BUFFER FUNCTION
	CALL	BDOS	;PRINT SIGN-ON MSG
	CALL	SETUP	;SET BIOS ENTRY, AND CHECK DRIVE
	CALL	ZMEM	;ZERO ALL AVAILABLE MEMORY
	CALL	FINDB	;ESTABLISH ALL BAD BLOCKS
	JZ	NOBAD	;SAY NO BAD BLOCKS, IF SO
	CALL	SETDM	;FIX DM BYTES IN FCB
;
NOBAD:	CALL	CRLF
	MVI	A,TAB
	CALL	TYPE
	LXI	D,NOMSG ;POINT FIRST TO 'NO'
	LHLD	BADBKS	;PICK UP # BAD BLOCKS
	MOV	A,H	;CHECK FOR ZERO
	ORA	L
	JZ	PMSG1	;JUMP IF NONE
	CALL	DECOUT	;OOPS..HAD SOME BAD ONES, REPORT
	JMP	PMSG2
;
PMSG1:	MVI	C,9	;BDOS PRINT BUFFER FUNCTION
	CALL	BDOS
;
PMSG2:	LXI	D,ENDMSG ;REST OF EXIT MESSAGE
;
PMSG:	MVI	C,9
	CALL	BDOS
;
	IF	TEST
	MVI	A,TAB	;GET A TAB
	CALL	TYPE	;PRINT IT
	LHLD	SECCNT	;GET NUMBER OF SECTORS READ
	CALL	DECOUT	;PRINT IT
	LXI	D,SECMSG ;POINT TO MESSAGE
	MVI	C,9	;BDOS PRINT BUFFER FUNCTION
	CALL	BDOS	;PRINT IT
	ENDIF		;TEST
;
	JMP	BASE	;EXIT TO CP/M WARM BOOT
;
;Get actual address of BIOS routines
;
SETUP:	LHLD	BASE+1	;GET BASE ADDRESS OF BIOS VECTORS
;
;WARNING...Program modification takes place here...do not change.
;
	LXI	D,24	 ;OFFSET TO "SETDSK"
	DAD	D
	SHLD	SETDSK+1 ;FIX OUR CALL ADDRESS
	LXI	D,3	 ;OFFSET TO "SETTRK"
	DAD	D
	SHLD	SETTRK+1 ;FIX OUR CALL ADDRESS
	LXI	D,3	 ;OFFSET TO "SETSEC"
	DAD	D
	SHLD	SETSEC+1 ;FIX OUR CALL ADDRESS
	LXI	D,6	 ;OFFSET TO "DREAD"
	DAD	D
	SHLD	DREAD+1  ;FIX OUR CALL ADDRESS
	LXI	D,9	 ;OFFSET TO CP/M 2.x SECTRAN
	DAD	D
	SHLD	SECTRN+1 ;FIX OUR CALL ADDRESS
	MVI	C,12	 ;GET VERSION FUNCTION
	CALL	BDOS
	MOV	A,H	 ;SAVE AS FLAG
	ORA	L
	STA	VER2FL
	JNZ	GDRIV	 ;SKIP 1.4 STUFF IF IS 2.x
	LXI	D,TRNOFF ;CP/M 1.4 OFFSET TO SECTRAN
	LHLD	BDOS+1	 ;SET UP JUMP TO 1.4 SECTRAN
	MVI	L,0
	DAD	D
	SHLD	SECTRN+1
;
;Check for drive specification
;
GDRIV:	LDA	FCB	;GET DRIVE NAME
	MOV	C,A
	ORA	A	;ZERO?
	JNZ	GD2	;IF NOT,THEN GO SPECIFY DRIVE
	MVI	C,25	;GET LOGGED-IN DRIVE
	CALL	BDOS
	INR	A	;MAKE 1-RELATIVE
	MOV	C,A
;
GD2:	LDA	VER2FL	;IF CP/M VERSION 2.x
	ORA	A
	JNZ	GD3	;  SELDSK WILL RETURN SEL ERR
;
;Is CP/M 1.4, which doesn't return a select
;error, so we have to do it here
;
	MOV	A,C
	CPI	4+1	;CHECK FOR HIGHEST DRIVE NUMBER
	JNC	SELERR	;SELECT ERROR
;
GD3:	DCR	C	;BACK OFF FOR CP/M
	PUSH	B	;SAVE DISK SELECTION
	MOV	E,C	;ALIGN FOR BDOS
	MVI	C,14	;SELECT DISK FUNCTION
	CALL	BDOS
	POP	B	;GET BACK DISK NUMBER
;
;EXPLANATION: WHY WE DO THE SAME THING TWICE
;
;	You might notice that we are
;	doing the disk selection twice,
;	once by a BDOS call and once by
;	direct BIOS call. The reason for this:
;
;	The BIOS call is necessary in order to
;	get the necessary pointer back from CP/M
;	(2.x) to find the sector translate table.
;	The BDOS call is necessary to keep CP/M
;	in step with the  BIOS...we may later
;	have to create a [UNUSED].BAD file, and
;	CP/M must know which drive we are using.
;			     (RGF)
;
SETDSK: CALL	$-$	;DIRECT BIOS VEC FILLED IN AT INIT
	LDA	VER2FL
	ORA	A
	JZ	DOLOG	;JUMP IF CP/M 1.4
	MOV	A,H
	ORA	L	;CHECK FOR 2.x 
	JZ	SELERR	;JUMP IF SELECT ERROR
	MOV	E,M	;GET SECTOR TABLE PNTR
	INX	H
	MOV	D,M
	INX	H
	XCHG
	SHLD	SECTBL	;STORE IT AWAY
	LXI	H,8	;OFFSET TO DPB POINTER
	DAD	D
	MOV	A,M	;PICK UP DPB POINTER
	INX	H	;  TO USE
	MOV	H,M	;  AS PARAMETER
	MOV	L,A	;  TO LOGIT
;
DOLOG:	CALL	LOGIT	;LOG IN DRIVE, GET DISK PARMS
	CALL	GETDIR	;CALCULATE DIRECTORY INFORMATION
;
;Now set the required user number
;
	LDA	VER2FL
	ORA	A
	RZ		;NO USERS IN CP/M 1.4
	LDA	BADUSR	;GET THE USER NUMBER
	CPI	0FFH	;IF IT IS 0FFH, THEN RETURN
	RZ
	MOV	E,A	;BDOS CALL NEEDS USER # IN E
	MVI	C,32	;GET/SET USER CODE
	CALL	BDOS
	RET
;
;Look for bad blocks
;
FINDB:	LDA	SYSTST
	ORA	A
	JZ	DODIR	;JUMP IF NO SYS TRACKS TO BE TESTED
	CALL	CHKSYS	;CHECK FOR BAD BLOCKS ON TRACK 0 AND 1
;
DODIR:	CALL	CHKDIR	;CHECK FOR BAD BLOCKS IN DIRECTORY
	CALL	TELL1
	DB	CR,LF,'Testing data area...',CR,LF,'$'
;
TELL1:	POP	D
	MVI	C,9	;BDOS PRINT STRING FUNCTION
	CALL	BDOS
	CALL	ERAB	;ERASE ANY [UNUSED].BAD FILE
	LHLD	DIRBKS	;START AT FIRST DATA BLOCK
	MOV	B,H	;PUT INTO BC
	MOV	C,L
;
FINDBA: CALL	READB	;READ THE BLOCK
	CNZ	SETBD	;IF BAD, ADD BLOCK TO LIST
	INX	B	;BUMP TO NEXT BLOCK
	LHLD	DSM
	MOV	D,B	;SET UP FOR (MAXGRP - CURGRP)
	MOV	E,C
	CALL	SUBDE	;DO SUBTRACT: (MAXGRP - CURGRP)
	JNC	FINDBA	;UNTIL CURGRP>MAXGRP
	CALL	CRLF
	LHLD	DMCNT	;GET NUMBER OF BAD SECTORS
	MOV	A,H
	ORA	L	;SET ZERO FLAG, IF NO BAD BLOCKS
	RET		;RETURN FROM "FINDB"
;
;Check system tracks, notify user if bad, but continue
;
CHKSYS: CALL	CHSY1	;PRINT MESSAGE
	DB	CR,LF,'Testing system tracks...',CR,LF,'$'
;
CHSY1:	POP	D
	MVI	C,9	;PRINT STRING FUNCTION
	CALL	BDOS
	LXI	H,0	;SET TRACK 0, SECTOR 1
	SHLD	TRACK
	INX	H
	SHLD	SECTOR
;
CHKSY1: CALL	READS	;READ A SECTOR
	JNZ	SYSERR	;NOTIFY, IF BAD BLOCKS HERE
	LHLD	SYSTRK	;SET UP (TRACK-SYSTRK)
	XCHG
	LHLD	TRACK
	CALL	SUBDE	;DO THE SUBTRACT
	JC	CHKSY1	;LOOP WHILE TRACK < SYSTRK
	RET		;RETURN FROM "CHKSYS"
;
SYSERR: LXI	D,ERMSG5 ;SAY NO GO, AND BAIL OUT
	MVI	C,9	;BDOS PRINT BUFFER FUNCTION
	CALL	BDOS
	RET		;RETURN FROM "SYSERR"
;
;Check for bad blocks in directory area
;
CHKDIR: CALL	CHKD1
	DB	CR,LF,'Testing directory area...',CR,LF,'$'
;
CHKD1:	POP	D
	MVI	C,9	;BDOS PRINT STRING FUNCTION
	CALL	BDOS
	LXI	B,0	;START AT BLOCK 0
;
CHKDI1: CALL	READB	;READ A BLOCK
	JNZ	ERROR6	;IF BAD, INDICATE ERROR IN DIRECTORY AREA
	INX	B	;BUMP FOR NEXT BLOCK
	LHLD	DIRBKS	;SET UP (CURGRP - DIRBKS)
	DCX	H	;MAKE 0-RELATIVE
	MOV	D,B
	MOV	E,C
	CALL	SUBDE	;DO THE SUBTRACT
	JNC	CHKDI1	;LOOP UNTIL CURGRP > DIRGRP
	RET		;RETURN FROM "CHKDIR"
;
;Read all sectors in block, and return zero flag set if none bad
;
READB:	CALL	CNVRTB	;CONVERT TO TRACK/SECTOR IN H&L REGS.
	LDA	BLM
	INR	A	;NUMBER OF SECTORS/BLOCK
	MOV	D,A	;  IN D REG
;
READBA: PUSH	D
	CALL	READS	;READ SKEWED SECTOR
	POP	D
	RNZ		;ERROR IF NOT ZERO...
	DCR	D	;DEBUMP SECTOR/BLOCK
	JNZ	READBA	;DO NEXT, IF NOT FINISHED
	RET		;RETURN FROM "READBA"
;
;Convert block number to track and skewed sector number
;
CNVRTB: PUSH	B	;SAVE CURRENT GROUP
	MOV	H,B	;NEED IT IN HL
	MOV	L,C	; FOR EASY SHIFTING
	LDA	BSH	;DPB VALUE THAT TELLS HOW TO
;
SHIFT:	DAD	H	;  SHIFT GROUP NUMBER TO GET
	DCR	A	;  DISK-DATA-AREA RELATIVE
	JNZ	SHIFT	;  SECTOR NUMBER
	XCHG		;REL SECTOR # INTO DE
	LHLD	SPT	;SECTORS PER TRACK FROM DPB
	CALL	NEG	;FASTER TO DAD THAN CALL SUBDE
	XCHG
	LXI	B,0	;INITIALIZE QUOTIENT
;
;Divide by number of sectors
;	quotient = track
;	     mod = sector
;
DIVLP:	INX	B	;DIRTY DIVISION
	DAD	D
	JC	DIVLP
	DCX	B	;FIXUP LAST
	XCHG
	LHLD	SPT
	DAD	D
	INX	H
	SHLD	SECTOR	;NOW HAVE LOGICAL SECTOR
	LHLD	SYSTRK	;BUT BEFORE WE HAVE TRACK #,
	DAD	B	;  WE HAVE TO ADD SYS TRACK OFFSET
	SHLD	TRACK
	POP	B	;THIS WAS OUR GROUP NUMBER
	RET
;
;READS reads a logical sector (if it can)
;and returns zero flag set if no error.
;
READS:	PUSH	B	;SAVE THE GROUP NUMBER
	CALL	LTOP	;CONVERT LOGICAL TO PHYSICAL
	LDA	VER2FL	;NOW CHECK VERSION
	ORA	A
	JZ	NOTCP2	;SKIP THIS STUFF IF CP/M 1.4
	LHLD	PHYSEC	;GET PHYSICAL SECTOR
	MOV	B,H	;INTO BC
	MOV	C,L
;
SETSEC: CALL	$-$	;ADDRS FILLED IN AT INIT
;
;QUICK NOTE OF EXPLANATION: This code appears
;as if we skipped the SETSEC routine for 1.4
;CP/M users.  That's not true; in CP/M 1.4, the
;call within the LTOP routine to SECTRAN  ac-
;tually does the set sector, so no need to do
;it twice.      (RGF)
;
NOTCP2: LHLD	TRACK	;NOW SET THE TRACK
	MOV	B,H	;CP/M WANTS IT IN BC
	MOV	C,L
;
SETTRK: CALL	$-$	;ADDRS FILLED IN AT INIT
;
;Now do the sector read
;
DREAD:	CALL	$-$	;ADDRS FILLED IN AT INIT
	ORA	A	;SET FLAGS
	PUSH	PSW	;SAVE ERROR FLAG
;
	IF	TEST
	LHLD	SECCNT	;GET SECTOR COUNT
	INX	H	;ADD ONE
	SHLD	SECCNT	;SAVE NEW COUNT
	ENDIF		;TEST
;
	LHLD	SECTOR	;GET LOGICAL SECTOR #
	INX	H	;WE WANT TO INCREMENT TO NEXT
	XCHG		;BUT FIRST...CHECK OVERFLOW
	LHLD	SPT	;  BY DOING (SECPERTRK-SECTOR)
	CALL	SUBDE	;DO THE SUBTRACTION
	XCHG
	JNC	NOOVF	;JUMP IF NOT SECTOR>SECPERTRK
;
;Sector overflow...bump track number, reset sector
;
	LHLD	TRACK
	INX	H
	SHLD	TRACK
	MVI	A,'*'	;TELL CONSOLE ANOTHER TRACK DONE
	CALL	TYPE
	CALL	STOP	;SEE IF CONSOLE WANTS TO QUIT
	LXI	H,1	;NEW SECTOR NUMBER ON NEXT TRACK
;
NOOVF:	SHLD	SECTOR	;PUT SECTOR AWAY
	POP	PSW	;GET BACK ERROR FLAGS
	POP	B	;RESTORE GROUP NUMBER
	RET
;
;Convert logical sector # to physical
;
LTOP:	LHLD	SECTBL	;SET UP PARAMETERS
	XCHG		;  FOR CALL TO SECTRAN
	LHLD	SECTOR
	MOV	B,H
	MOV	C,L
	DCX	B	;ALWAYS CALL SECTRAN W/ZERO-REL SEC #
;
SECT1:	CALL	SECTRN	;DO THE SECTOR TRANSLATION
	LDA	SPT+1	;CHECK IF BIG TRACKS
	ORA	A	;SET FLAGS (TRACKS > 256 SECTORS)
	JNZ	LTOP1	;NO SO SKIP
	MOV	H,A	;ZERO OUT UPPER 8 BITS
;
LTOP1:	SHLD	PHYSEC	;PUT AWAY PHYSICAL SECTOR
	RET
;
;Sector translation vector
;
SECTRN: JMP	$-$	;FILLED IN AT INIT
;
;Put bad block in bad block list
;
SETBD:	PUSH	B
	CALL	SETBD1
	DB	CR,LF,'Bad block: $'
;
SETBD1: POP	D	;RETRIEVE ARG
	MVI	C,9	;PRINT STRING
	CALL	BDOS
	POP	B	;GET BACK BLOCK NUMBER
	MOV	A,B
	CALL	HEXO	;PRINT IN HEX
	MOV	A,C
	CALL	HEXO
	CALL	CRLF
	LHLD	DMCNT	;GET NUMBER OF SECTORS
	LDA	BLM	;GET BLOCK SHIFT VALUE
	INR	A	;MAKES SECTOR/GROUP VALUE
	MOV	E,A	;WE WANT 16 BITS
	MVI	D,0
	DAD	D	;BUMP BY NUMBER IN THIS BLOCK
	SHLD	DMCNT	;UPDATE NUMBER OF SECTORS
	LHLD	BADBKS	;INCREMENT NUMBER OF BAD BLOCKS
	INX	H
	SHLD	BADBKS
	LHLD	DMPTR	;GET POINTER INTO DM
	MOV	M,C	;...AND PUT BAD BLOCK NUMBER
	INX	H	;BUMP TO NEXT AVAILABLE EXTENT
	LDA	DSM+1	;CHECK IF 8 OR 16 BIT BLOCK SIZE
	ORA	A
	JZ	SMGRP	;JUMP IF 8 BIT BLOCKS
	MOV	M,B	;ELSE STORE HI BYTE OF BLOCK #
	INX	H	;AND BUMP POINTER
;
SMGRP:	SHLD	DMPTR	;SAVE DM POINTER, FOR NEXT TIME THROUGH HERE
	RET		;RETURN FROM "SETBD"
;
;Eliminate any previous [UNUSED].BAD entries
;
ERAB:	LXI	D,BFCB	;POINT TO BAD FCB
	MVI	C,19	;BDOS DELETE FILE FUNCTION
	CALL	BDOS
	RET
;
;Create [UNUSED].BAD file entry
;
OPENB:	LXI	D,BFCB	;POINT TO BAD FCB
	MVI	C,22	;BDOS MAKE FILE FUNCTION
	CALL	BDOS
	CPI	0FFH	;CHECK FOR OPEN ERROR
	RNZ		;RETURN FROM "OPENB", IF NO ERROR
	JMP	ERROR7	;BAIL OUT...CAN'T CREATE [UNUSED].BAD
;
CLOSEB: XRA	A
	LDA	BFCB+14 ;GET CP/M 2.x 'S2' BYTE
	ANI	1FH	;ZERO UPDATE FLAGS
	STA	BFCB+14 ;RESTORE IT TO OUR FCB (WON'T HURT 1.4)
	LXI	D,BFCB	;FCB FOR [UNUSED].BAD
	MVI	C,16	;BDOS CLOSE FILE FUNCTION
	CALL	BDOS
	RET		;RETURN FROM "CLOSEB"
;
;Move bad area DM to BFCB
;
SETDM:	LXI	H,DM	;GET DM
	SHLD	DMPTR	;SAVE AS NEW POINTER
	LDA	EXM	;GET THE EXTENT SHIFT FACTOR
	MVI	C,0	;INIT BIT COUNT
	CALL	COLECT	;GET SHIFT VALUE
	LXI	H,128	;STARTING EXTENT SIZE
	MOV	A,C	;FIRST SEE IF ANY SHIFTS TO DO
	ORA	A
	JZ	NOSHFT	;JUMP IF NONE
;
ESHFT:	DAD	H	;SHIFT
	DCR	A	;BUMP
	JNZ	ESHFT	;LOOP
;
NOSHFT: PUSH	H	;SAVE THIS, IT IS RECORDS PER EXTENT
	LDA	BSH	;GET BLOCK SHIFT
	MOV	B,A
;
BSHFT:	CALL	ROTRHL	;SHIFT RIGHT
	DCR	B
	JNZ	BSHFT	;TO GET BLOCKS PER EXTENT
	MOV	A,L	;IT'S IN L (CAN'T BE >16)
	STA	BLKEXT	;SETDME WILL NEED THIS LATER
	POP	H	;GET BACK REC/EXT
;
SET1:	XCHG		;NOW HAVE REC/EXTENT IN DE
	LHLD	DMCNT	;COUNT OF BAD SECTORS
;
SETDMO: PUSH	H	;SET FLAGS ON (DMCNT-BADCNT)
	CALL	SUBDE	;HAVE TO SUBTRACT FIRST
	MOV	B,H	;SAVE RESULT IN BC
	MOV	C,L
	POP	H	;THIS POP MAKES IT COMPARE ONLY
	JC	SETDME	;JUMP IF LESS THAN 1 EXTENT WORTH
	MOV	A,B
	ORA	C	;TEST IF SUBTRACT WAS 0
	JZ	EVENEX	;EXTENT IS EXACTLY FILLED (SPL CASE)
	MOV	H,B	;RESTORE RESULT TO HL
	MOV	L,C
	PUSH	H	;SAVE TOTAL
	PUSH	D	;AND SECTORS/EXTENT
	XCHG
	CALL	SETDME	;PUT AWAY ONE EXTENT
	XCHG
	SHLD	DMPTR	;PUT BACK NEW DM POINTER
	POP	D	;GET BACK SECTORS/EXTENT
	POP	H	;AND COUNT OF BAD SECTORS
	JMP	SETDMO	;AND LOOP
;
;Handle the special case of a file that ends on an extent
;boundary.  CP/M requires that such a file have a succeeding
;empty extent in order for the BDOS to properly access the file.
;
EVENEX: XCHG		;FIRST SET EXTENT W/BAD BLOCKS
	CALL	SETDME
	XCHG
	SHLD	DMPTR
	LXI	H,0	;NOW SET ONE WITH NO DATA BLOCKS
;
;Fill in an extent's worth of bad sectors/block numbers.
;Also fill in the extent number in the FCB.
;
SETDME: PUSH	H	;SAVE RECORD COUNT
	LDA	EXTNUM	;UPDATE EXTENT BYTE
	INR	A
	STA	EXTNUM	;SAVE FOR LATER
	STA	BFCB+12 ; AND PUT IN FCB
	CALL	OPENB	;OPEN THIS EXTENT
	POP	H	;RETRIEVE REC COUNT
;
;Divide record count by 128 to get the number
;of logical extents to put in the EX field
;
	MVI	B,0	;INIT QUOTIENT
	LXI	D,-128	;-DIVISOR
	MOV	A,H	;TEST FOR SPL CASE
	ORA	L	;  OF NO RECORDS
	JZ	SKIP
;
DIVLOP: DAD	D	;SUBTRACT
	INR	B	;BUMP QUOTIENT
	JC	DIVLOP
	LXI	D,128	;FIX UP OVERSHOOT
	DAD	D
	DCR	B
	MOV	A,H	;TEST FOR WRAPAROUND
	ORA	L
	JNZ	SKIP
	MVI	L,80H	;RECORD LENGTH
	DCR	B
;
SKIP:	LDA	EXTNUM	;NOW FIX UP EXTENT NUM
	ADD	B
	STA	EXTNUM
	STA	BFCB+12
	MOV	A,L	;MOD IS RECORD COUNT
	STA	BFCB+15 ;THAT GOES IN RC BYTE
;
MOVDM:	LDA	BLKEXT	;GET BLOCKS PER EXTENT
	MOV	B,A	;INTO B
;
SETD1:	LHLD	DMPTR	;POINT TO BAD ALLOCATION MAP
	XCHG
	LXI	H,BFCB+16 ;DISK ALLOC MAP IN FCB
;
SETDML: LDAX	D
	MOV	M,A
	INX	H
	INX	D
;
;Now see if 16 bit groups...if so,
;we have to move another byte
;
	LDA	DSM+1	;THIS TELLS US
	ORA	A
	JZ	BUMP1	;IF ZERO, THEN NOT
	LDAX	D	;IS 16 BITS, SO DO ANOTHER
	MOV	M,A
	INX	H
	INX	D
;
BUMP1:	DCR	B	;COUNT DOWN
	JNZ	SETDML
	PUSH	D
	CALL	CLOSEB	;CLOSE THIS EXTENT
	POP	D
	RET
;
;Error messages
;
SELERR: LXI	D,SELEMS ;SAY NO GO, AND BAIL OUT
	JMP	PMSG
;
SELEMS: DB	CR,LF,'Drive specifier out of range$'
;
ERMSG5: DB	CR,LF,'+++ Warning...System tracks'
	DB	' bad +++',CR,LF,CR,LF,'$'
;
ERROR6: LXI	D,ERMSG6 ;OOPS...CLOBBERED DIRECTORY
	JMP	PMSG
;
ERMSG6: DB	CR,LF,'Bad directory area, try reformatting$'
;
ERROR7: LXI	D,ERMSG7 ;SAY NO GO, AND BAIL OUT
	JMP	PMSG
;
ERMSG7: DB	CR,LF,'Can''t create [UNUSED].BAD$'
;
;
;==== SUBROUTINES ====
;
;Decimal output routine
;
DECOUT: PUSH	B
	PUSH	D
	PUSH	H
	LXI	B,-10
	LXI	D,-1
;
DECOU2: DAD	B
	INX	D
	JC	DECOU2
	LXI	B,10
	DAD	B
	XCHG
	MOV	A,H
	ORA	L
	CNZ	DECOUT
	MOV	A,E
	ADI	'0'
	CALL	TYPE
	POP	H
	POP	D
	POP	B
	RET
;
;Carriage-return/line-feed to console
;
CRLF:	MVI	A,CR
	CALL	TYPE
	MVI	A,LF	;FALL INTO 'TYPE'
;
TYPE:	PUSH	B
	PUSH	D
	PUSH	H
	MOV	E,A	;CHARACTER TO E FOR CP/M
	MVI	C,2	;PRINT CONSOLE FUNCTION
	CALL	BDOS	;PRINT CHARACTER
	POP	H
	POP	D
	POP	B
	RET
;
;Subroutine to test console for control-c abort
;
STOP:	LHLD	1	;FIND BIOS IN MEMORY
	MVI	L,6	;OFFSET TO CONSOLE STATUS
	CALL	GOHL	;THANKS TO BRUCE RATOFF FOR THIS TRICK
	ORA	A	;TEST FLAGS ON ZERO
	RZ		;RETURN IF NO CHAR
	LHLD	1	;NOW FIND CONSOLE INPUT
	MVI	L,9	;OFFSET FOR CONIN
	CALL	GOHL
	CPI	'C'-40H ;IS IT CONTROL-C?
	RNZ		;RETURN IF NOT
	LXI	D,ABORTM ;EXIT WITH MESSAGE
	MVI	C,9	;PRINT MESSAGE FUNCTION
	CALL	BDOS	;SAY GOODBYE
	JMP	0	;THEN LEAVE
;
ABORTM: DB	CR,LF
	DB	'Test aborted by control-C'
	DB	CR,LF,'$'
;
;A thing to allow a call to @HL
;
GOHL:	PCHL
;
;Zero all of memory to hold DM values
;
ZMEM:	LHLD	BDOS+1	;GET TOP-OF-MEM POINTER
	LXI	D,DM	;STARTING POINT
	CALL	SUBDE	;GET NUMBER OF BYTES
	MOV	B,H
	MOV	C,L
	XCHG		;BEGIN IN HL, COUNT IN BC
;
ZLOOP:	MVI	M,0	;ZERO A BYTE
	INX	H	;POINT PAST
	DCX	B	;COUNT DOWN
	MOV	A,B
	ORA	C
	JNZ	ZLOOP
	RET
;
;Subtract DE from HL
;
SUBDE:	MOV	A,L
	SUB	E
	MOV	L,A
	MOV	A,H
	SBB	D
	MOV	H,A
	RET
;
;Negate HL
;
NEG:	MOV	A,L
	CMA
	MOV	L,A
	MOV	A,H
	CMA
	MOV	H,A
	INX	H
	RET
;
;Move from (HL) to (DE)
;Count in BC
;
MOVE:	MOV	A,M
	STAX	D
	INX	H
	INX	D
	DCR	B
	JNZ	MOVE
	RET
;
;Print byte in accumulator in hex
;
HEXO:	PUSH	PSW	;SAVE FOR SECOND HALF
	RRC		;MOVE INTO POSITION
	RRC
	RRC
	RRC
	CALL	NYBBLE	;PRINT MS NYBBLE
	POP	PSW
;
NYBBLE: ANI	0FH	;LO NYBBLE ONLY
	ADI	90H 
	DAA
	ACI	40H
	DAA
	JMP	TYPE	;PRINT IN HEX
;
;Subroutine to determine the number
;of groups reserved for the directory
;
GETDIR: MVI	C,0	;INIT BIT COUNT
	LDA	AL0	;READ DIR GRP BITS
	CALL	COLECT	;COLLECT COUNT OF DIR GRPS..
	LDA	AL1	;..IN REGISTER C
	CALL	COLECT
	MOV	L,C
	MVI	H,0	;BC NOW HAS A DEFAULT START GRP #
	SHLD	DIRBKS	;SAVE FOR LATER
	RET
;
;Collect the number of '1' bits in A as a count in C
;
COLECT: MVI	B,8
;
COLOP:	RAL
	JNC	COSKIP
	INR	C
;
COSKIP: DCR	B
	JNZ	COLOP
	RET
;
;Shift HL right one place
;
ROTRHL: ORA	A	;CLEAR CARRY
	MOV	A,H	;GET HI BYTE
	RAR		;SHIFT RIGHT
	MOV	H,A	;PUT BACK
	MOV	A,L	;GET LO
	RAR		;SHIFT WITH CARRY
	MOV	L,A	;PUT BACK
	RET
;
;Routine to fill in disk parameters
;
LOGIT:	LDA	VER2FL
	ORA	A	;IF NOT CP/M 2.x THEN
	JZ	LOG14	;	DO IT AS 1.4
	LXI	D,DPB	;   THEN MOVE TO LOCAL
	MVI	B,DPBLEN ;  WORKSPACE
	CALL	MOVE
	RET
;
LOG14:	LHLD	BDOS+1	;FIRST FIND 1.4 BDOS
	MVI	L,0
	LXI	D,DPBOFF ;THEN OFFSET TO 1.4'S DPB
	DAD	D
	MVI	D,0	;SO 8 BIT PARMS WILL BE 16
	MOV	E,M	;NOW MOVE PARMS
	INX	H	; DOWN FROM BDOS DISK PARM BLOCK
	XCHG		; TO OURS
	SHLD	SPT
	XCHG
	MOV	E,M
	INX	H
	XCHG
	SHLD	DRM
	XCHG
	MOV	A,M
	INX	H
	STA	BSH
	MOV	A,M
	INX	H
	STA	BLM
	MOV	E,M
	INX	H
	XCHG
	SHLD	DSM
	XCHG
	MOV	E,M
	INX	H
	XCHG
	SHLD	AL0
	XCHG
	MOV	E,M
	XCHG
	SHLD	SYSTRK
	RET
;
;--------------------------------------------------
;The disk parameter block
;is moved here from CP/M
;
DPB	EQU	$	;DISK PARAMETER BLOCK (COPY)
;
SPT:	DS	2	;SECTORS PER TRACK
BSH:	DS	1	;BLOCK SHIFT
BLM:	DS	1	;BLOCK MASK
EXM:	DS	1	;EXTENT MASK
DSM:	DS	2	;MAXIMUM BLOCK NUMBER
DRM:	DS	2	;MAXIMUM DIRECTORY BLOCK NUMBER
AL0:	DS	1	;DIRECTORY ALLOCATION VECTOR
AL1:	DS	1	;DIRECTORY ALLOCATION VECTOR
CKS:	DS	2	;CHECKED DIRECTORY ENTRIES
SYSTRK: DS	2	;SYSTEM TRACKS
;
;End of disk parameter block
;
DPBLEN	EQU	$-DPB	;LENGTH OF DISK PARM BLOCK
;
;--------------------------------------------------
BLKEXT: DB	0	;BLOCKS PER EXTENT
DIRBKS: DW	0	;CALCULATED # OF DIR BLOCKS
VER2FL: DB	0	;VERSION 2.X FLAG
;
BFCB:	DB	0,'[UNUSED]BAD',0,0,0,0
FCBDM:	DS	17
;
NOMSG:	DB	'No$'
ENDMSG: DB	' bad blocks found',CR,LF,'$'
;
BADBKS: DW	0	;COUNT OF BAD BLOCKS
SECTOR: DW	0	;CURRENT SECTOR NUMBER
TRACK:	DW	0	;CURRENT TRACK NUMBER
PHYSEC: DW	0	;CURRENT PHYSICAL SECTOR NUMBER
SECTBL: DW	0	;SECTOR SKEW TABLE POINTER
;
EXTNUM: DB	0FFH	;USED FOR UPDATING EXTENT NUMBER
DMCNT:	DW	0	;NUMBER OF BAD SECTORS
DMPTR:	DW	DM	;POINTER TO NEXT BLOCK ID
;
SECMSG: DB	' total sectors read',CR,LF,'$'
;
SECCNT: DW	0	;NUMBER OF SECTORS READ
;
	DS	64	;ROOM FOR 32 LEVEL STACK
NEWSTK	EQU	$	;OUR STACK
DM	EQU	$	;BAD BLOCK ALLOCATION MAP
;
	END
