;==============================================================
;              HEX2ENT by Martin Eberhard
; Converts an Intel hex file to a Solos-compatible ENT file
;
; Usage:
;
; HEX2ENT <source>[.HEX] [<dest>[.ENT]]
;
;
; if no .ASM on the source file, then it is assumed to be .ASM
; If no <dest> then the detination will be named <source>.ENT
; If no <ext> on the destination file, then it is assumed to
; be '.ENT'
;
;==============================================================
;Revision History
;  9 March 2015  M. Eberhard
;     Created
;==============================================================
;**************
; CP/M Equates
;**************

;---------------
;Program Equates
;---------------
RBSIZE	EQU	8	;128-Byte records in the read buffer
WBSIZE	EQU	8	;128-Byte records in the write buffer
MAXBPL	equ	16	;max bytes per ENT file line

;------------------------------------------
;BDOS Entry Points and low-memory locations
;------------------------------------------
WBOOT	equ	0000H		;Jump to BIOS warm boot
WBOOTA	equ	WBOOT+1		;Address of Warm Boot
IOBYTE	equ	WBOOT+3
;CDISK	equ	WBOOT+4		;Login drive
BDOS	equ	WBOOT+5		;BDOS Entry Point
BDOSA	equ	WBOOT+6		; First address of BDOS
				;(can overlay up to here)

;File Control Block
FCB1	equ	WBOOT+5CH	;CP/M file control blk
FCBDR	equ	0		;Offset to drive Descriptor
FCBFN	equ	1		;Offset to file name (8 chrs)
FCBFT	equ	9		;Offset to file Type (3 chrs)
FCBEX	equ	12		;Offset to file extent

FNAME2	equ	WBOOT+6Ch	;CP/M puts 2nd file name here
FCPLEN	equ	16		;length of name copy

COMBUF	equ	WBOOT+80H	;disk & cmd line buffer
USAREA	equ	WBOOT+100H	;User program area

;------------------------------------------
;BDOS Function Codes, passed in register C
;------------------------------------------
;BRESET	equ	0	;System Reset
;BCONIN	equ	1	;Read Console Chr
BCONOT	equ	2	;Type Chr on Console
;BRDRIN	equ	3	;Read Reader Chr
;BPUNOT	equ	4	;Write Punch Chr
;BPRINT	equ	9	;Print $-terminated String
;BRDCON	equ	10	;Get Line from Console
;BCONST	equ	11	;Console Status (<>0 IF CHR)
;BDRST	equ	13	;Reset Disk
;BSDISK	equ	14	;select disk
BOPEN	equ	15	;Disk File Open
BCLOSE	equ	16	;Close disk file, FCB at de
;BSERCH	equ	17	;Search dir for file, FCB at de
;BDELET	equ	19	;delete file, FCB at (de)
BREAD	equ	20	;Read from Disk, 0=OK, <>0=EOF
BWRITE	equ	21	;Write next record, 0=OK, <>0=ERR
BMAKE	equ	22	;Make new file, 0FFH=BAD
;BCDISK	equ	25	;get current disk
BSTDMA	equ	26	;Set disk buffer to (de)

;----------------
;ASCII Characters
;----------------
CTRLC	equ	3		;Control-C
LF	equ	0AH		;Linefeed
CR	equ	0DH		;Carriage return
QUOTE	equ	27H		;single quote

;*********************
;* Beginning of Code *
;*********************
	org	USAREA		;normal place for CP/M programs

	lxi	SP,STACK	;local stack

; Check for a valid file name in FCB1

	lda	FCB1+FCBFN
	cpi	' '
	jz	HLPEXT		;no name: just help

;Copy the second given file name, extension, etc. to FCB2.

	lxi	h,FNAME2
	lxi	d,FCB2
	mvi	b,FCPLEN
	call	MOVBLK

;Write 0's into FCB1, where CP/M had put NAME2.

	xchg			;use 0's in FCB2
	lxi	d,FNAME2
	mvi	b,FCPLEN
	call	MOVBLK


;The destination name is in FCB2, and the source name is in FCB1.

;Did the source file have an extension? If not, make it .HEX

	lxi	h,HEXSTR
	lxi	d,FCB1+FCBFT
	ldax	d
	cpi	' '
	cz	MOVTYP

;Check that the source file is type .HEX

	lxi	d,HEXSTR	;'HEX'
	lxi	h,FCB1+FCBFT
	call	CHKEXT		;match?
	jnz	STPERR	

;If no destination file name was given, then it will be the same
;as the source.

	lxi	h,FCB1+FCBFN
	lxi	d,FCB2+FCBFN
	mvi	b,8

	ldax	d		;any name?
	cpi	' '
	cz	MOVBLK

;If the destination file has no file type, then make it 'ENT'

	lxi	h,ENTSTR
	lxi	d,FCB2+FCBFT
	ldax	d
	cpi	' '
	cz	MOVTYP

;---------------------------------------------------
;The complete source file FCB is set up in FCB1
;The complete destination file FCB is set up in FCB2
;---------------------------------------------------

;Open the source file

	lxi	d,FCB1		;Input file control block

	mvi	c,BOPEN		;Open file function
	call	BDOS
	inr	a		;test for ff=file not found
	jz	FNFERR

;Create the destination file

	lxi	d,FCB2
	mvi	c,BMAKE		;Make file function
	call	BDOS
	inr	a		;test for FF=error
	jz	MAKERR		;Make error?

;Write initial 'EN 0' to get things going

	call	DPRINT
	db	'EN 0',CR+80h

;Loop to read and validate each source file hex record,
;and convert it to an ENT record in the destination file

CVTLUP:	call	RCVHEX		;Get & convert a hex record
	jz	RCVDON		;0-byte record?

	call	HEX16		;send the address
	call	DPRINT
	db	':'+80h

	lxi	h,RECBUF

;Loop to convert the data from one hex record to an ENT record
;c = byte count
;hl = data address

	mvi	b,MAXBPL+1	;max bytes/line

WRTLUP:	call	DPRINT
	db	' '+80h

	dcr	b
	cz	NEWLIN		;new line if necessary

	mov	a,m
	call	HEX8
	inx	h

	dcr	c
	jnz	WRTLUP

	call	DPRINT
	db	CR+80h

	jmp	CVTLUP	

;Conversion complete

RCVDON:	call	DPRINT
	db	' /',CR+80h

;flush the write buffer

	call	FLSHWR

;Fall into CL2EXT

;***Exit*********************************
;Close both disk files and return to CP/M
;****************************************
CL2EXT:	lxi	d,FCB2		;close the destination
	mvi	c,BCLOSE	;CP/M CLOSE FILE funct.
	call	BDOS
	inr	a		;-1 meant close error
	jz	DCERR		;destination close error

;Fall into CL1EXT

;***Exit**************************************
;Close the source disk file and return to CP/M
;*********************************************
CL1EXT:	lxi	d,FCB1		;close the source
	mvi	c,BCLOSE	;CP/M CLOSE FILE funct.
	call	BDOS
	inr	a		;-1 meant close error
	jz	SCERR		;source close error

;Fall into EXIT

;************
;Exit to CP/M
;************
EXIT:	jmp	WBOOT

;***Subroutine***********************
;Insert a new line in the output file
;On Exit:
;  b = MAXBPL+1, max bytes per line
;************************************
NEWLIN:	call	DPRINT
	db	CR+80h

	mvi	b,MAXBPL+1
	ret

;***Subroutine***************************************
;Read one Intel hex record into RXBUF. Report errors
;on console.
;
;On Entry:
;On Exit:
;   c = byte count
;  hl = this record's address
;   Z set if c=0
;
;All registers trashed
;
;Register usage during hex load:
;  b: scratch
;  c: record byte count
;  d: record checksum
;  e: loop count
; hl: memory address
;***************************************************
RCVHEX:

;Wait for colon to start record

GETCO:	call	RDDISK		;get one source file chr
	jz	EOFERR		;unexpected eof?
	cpi	':'
	jnz	GETCO

;Loop to shift the following into registers:
; c: record byte count
; h: address high byte
; l: address low byte
; b: record type (gets chucked)
; d: checksum of the above 4 bytes

	lxi	d,4		;d = reset cksm, e = loop 4 times

CATLP:	mov	c,h		;shift through regs so that
	mov	h,l		;...all is in place after
	mov	l,b		;...4 passes through loop
	call	GETBYT		;data is in b, cksm in a & d
	jnz	CATLP

	push	h		;remember record address
	
;Loop to get hex data into buffer, checking for buffer
;under/overflow. Get checksum in a when loop ends.

	lxi	h,RECBUF

	mov	e,c		;loop count = record length

BYTLP:	call	GETBYT		;data in b, checksum in a&d
	jm	CHKSM		;done once we have checksum
	mov	m,b		;park data in RXBUF

	inx	h
	jmp	BYTLP

CHKSM:	ora	a		;Check record checksum in a
	jnz	CSERR		;abort w/ checksum error if <>0

;All done W/ record. check for 0-byte record, which is the EOF

	pop	h		;hl = this record's address

	ora	c		;was 0 bytes?
	ret			;Y: Z means done receiving hex

;***Subroutine***********************************
;Get 2 hex digits from the source file, combine
;them into a byte, and add the result to checksum
;On Entry:
;  d = checksum so far
;  e = loop count
;On Exit:
;  b = data byte
;  a = d = updated checksum
;  e = e-1
;  Z flag set if e = 0, M flag set if e<0
;  all OtheR regs preserved
;************************************************
GETBYT:	call	GETHEX		;get high nibble
	rlc			;shift high nibble down
	rlc
	rlc
	rlc
	mov	b,a
	call	GETHEX		;get low nibble

	add	b		;combine nibbles, catch errors
	mov	b,a		;result in b for ret
	adc	d		;compute checksum, catch errors
	mov	d,a

	dcr	e		;bump & test loop count
	ret

;***Subroutine**************************************
;Get and validate one hex digit from the Source file
;On Exit:
;  new nibble is in a<3:0>
;  all Other regs preserved
;Rude exit on bogus hex character
;***************************************************
GETHEX:	call	RDDISK		;get a source file byte
	jz	EOFERR		;unexpected eof?
	sui	'0'		;remove aSCII bias
	cpi	10		;0-9?
	rc			;Y: done, Z cleared

	sui	'A'-'0'		;test for gap chr
	cpi	6		;..or too large
	jnc	HEXERR		;bogus hex

	adi	10		;make 'A'-'F' into 0Ah-0Fh
	ret


;***Subroutine*************
;Move a file type
;On Entry:
;  hl = source address
;  de = destination address
;**************************
MOVTYP:	mvi	b,3

;Fall into MOVBLK

;***Subroutine*************
;Move a block of memory
;On Entry:
;  hl = source address
;  de = destination address
;   b = byte count
;**************************
MOVBLK:	mov	a,m
	stax	d
	inx	h
	inx	d
	dcr	b
	jnz	MOVBLK

;***Subroutine***********************
; Compare a 3-byte file extension
;On Entry:
;  hl = address of one extension
;  de = address of other extension
;On Exit:
;   Z set if the extensions are equal
;trashes psw,b,de,hl
;***************************************
CHKEXT:	mvi	b,3	;3-byte extension

CELOOP:	ldax	d
	cmp	m	;match?
	rnz		;no: exit with error
	inx	h
	inx	d
	dcr	b
	jnz	CELOOP

	ret		;with Z set

;***Subroutine*****************************************
; Send inline string to disk
;  The call to DPRINT is followed by a message string.
;  The last message chr has its msb set.
; On Exit:
;  Z cleared
; Trashes psw
;******************************************************
DPRINT:	xthl			;Save hl, get msg addr

DPLOOP:	mov	a,m
	ani	7Fh		;strip end marker

	call	WRDISK		;print byte
	mov	a,m		;end?
	inx	h		;Next byte
	ora	a		;msb set?
	jp	DPLOOP		;Do all bytes of msg

	xthl			;Restore hl,
				;..get return address
	ret

;***Subroutine******************************************
;Write hl as 4 hex digits to disk
;On Entry:
;  hl = value to print
;On Exit:
;  trashes psw
;*******************************************************
HEX16:	mov	a,h		;high byte 1st
	call	HEX8
	mov	a,l

;Fall into HEX8

;***Subroutine******************************
;Write b as 2 hex digits to disk
;On Entry:
;   a = value to print
;On Exit:
;  trashes psw
;*******************************************
HEX8:	push	psw		;save a = chr low nibble
	rrc			;movE the high 4 down
	rrc
	rrc
	rrc
	call	HEX4		;send one hex digit
	pop	psw		;recover chr in a

;Fall into HEX4

;---Local Subroutine------------------------
;Write a<3:0> as a hex digit to disk
;On Entry:
;   a<3:0> = value to print
;On Exit:
;  trashes psw
;-------------------------------------------
HEX4:	ani	0Fh		;Four on the floor
	adi	48		;we WORK with aSCII here
	cpi	58		;0-9?
	jc	OUTNIB		;yup
	adi	7		;make it a letter
OUTNIB:

;Fall into WRDISK

;***Subroutine*******************************************
; Write byte to destination file
; On Entry:
;   a = chr to write
;   WRBBC = number of free bytes in the write buffer >0
;   WRBPTR = next free location in the write buffer
;
; On Exit:
;   WRBPTR and WRBBC updated
;   Write buffer flushed if it became full
;
; trashes psw
; RUDE EXIT (IN FLSHWR) ON DISK ERROR
;********************************************************
WRDISK:	push	h
	lhld	WRBPTR
	mov	m,a
	inx	h
	shld	WRBPTR

	lhld	WRBBC		;free space counter
	dcx	h
	shld	WRBBC

	mov	a,h		;full?
	ora	l

	pop	h
	rnz			;n: done

;Fall into FLSHWR to flush the buffer

;***Subroutine*******************************************
; Write contents of write buffer to disk
; On entry:
;   WRBPTR = next free location in the write buffer
;   WRBBC = number of free bytes in the write buffer >0
;
; On Exit:
;  WRBPTR = WRBUF
;  WRBBC = WBSIZ*128
;
;trashes psw
;********************************************************
FLSHWR:	push	h
	push	d
	push	b

	lhld	WRBPTR		;Pointer into buffer
	xchg			;..into de

	lhld	WRBBC		;Free byte count

	mov	a,l		;Need to round up?
	ani	7Fh
	jz	FLWRB2		;Jump if on boundary

;Fill up to 128-byte boundary with nulls

	mov	b,a		;number of bytes to fill
	xra	a		;Clears carry too

FLWRB1:	stax	d
	inx	d
	dcr	l		;Adjust free-byte count
	dcr	b
	jnz	FLWRB1

;Compute b = number of free 128-byte records

FLWRB2:	mov	a,l
	ral
	mov	a,h		;High byte of free records
	ral			;a= # of free 128-byte records

	mov	b,a

;Compute number of 128-byte records to write

	mvi	a,WBSIZE
	sub	b		;a= # of 128-byte records to write
	jz	FWDONE		;Quit if none	

	mov	b,a		;Write b 128-byte records

	lxi	h,WRBUF		;Start at buffer beginning
	shld	WRBPTR		;fix for return
	xchg			;..BDOS wants DMA address in de

	mvi	c,BSTDMA	;CP/M SET DMA function

;Loop to write b 128-byte records to disk

FLWRB3:	push	b		;count and BSTDMA
	push	d		;DMA address=buffer address
	call	BDOS

	lxi	d,FCB2		;DEST FILE COTROL BLOCK
	mvi	c,BWRITE	;Write sequential
	call	BDOS
	pop	d		;buffer address

	ora	a		;Disk full error?
	jnz	ERRWRT

	lxi	b,128		;Bump de = next address
	xchg
	dad	b
	xchg
		
	pop	b		;count and BSTDMA
	dcr	b		;next record
	jnz	FLWRB3

	lxi	h,128*WBSIZE	;All bytes are free
	shld	WRBBC

FWDONE:	pop	b
	pop	d
	pop	h
	ret

;****Subroutine*****************************************
;Read one source file byte
;
; On Entry:
;   RDBBC = available bytes in the read buffer
;   RDBPTR = address of next buffer item
;
; On Exit:
;   RDBBC and RDBPTR updated
;   a = new chr uness EOF
;   Z set means EOF - no more data
;
;********************************************************
RDDISK:	push	h

	lhld	RDBBC		;Buffer byte count
	mov	a,l		;Anything there?
	ora	h
	cz	FILRDB		;n: go fill the buffer
				;Z set if EOF, else hl=byte count

	dcx	h
	shld	RDBBC

	lhld	RDBPTR
	mov	a,m		;Actually get data
	inx	h
	shld	RDBPTR

	pop	h
	ret			;Z still valid from FILRDB
				;or from 'ora h' above

;***Subroutine************************************************
;Read as many 128-byte disk sectors as will fit in the read
;bufffer, or until EOF

; ON EXIT:
;   RDBPTR = RDBUF
;   HL = NUMBER OF BYTES IN RDBUF
;   Z SET IF NO MORE DATA (EOF AND RDBBC=0)
;   A TRASHED
;   ALL OTHER REGS PRESERVED
;**************************************************************
FILRDB:	push	d
	push	b

	mvi	b,WBSIZE	;WBSIZE 128-byte records
	lxi	h,RDBUF		;Reset file pointer
	shld	RDBPTR

	xchg			;de=buffer address
	mvi	c,BSTDMA	;CP/M SET DMA function

	lxi	h,0		;hl counts bytes

;Loop to read up to WBSIZE 128-byte records from disk into RBUFF

FLRDB1:	push	b		;record count and BSTDMA
	push	h		;byte count
	push	d		;buffer address=DMA Address
	call	BDOS		;Set DMA

	lxi	d,FCB1	;SOURCE FILE COTROL BLOCK
	mvi	c,BREAD	;Read sequential
	call	BDOS
	pop	d		;Buffer address
	pop	h		;Byte count

	ora	a
	jnz	FLRDB2		;EOF found?

	lxi	b,128		;bump de=buffer address
	xchg
	dad	b
	xchg

	dad	b		;hl has new byte count
		
	pop	b		;Count and BSTDMA
	dcr	b		;next 128-byte record
	jnz	FLRDB1

	push	b		;compensate for following pop

;Deterine of we got any data, and return

FLRDB2:	pop	b		;b=record count
	mvi	a,WBSIZE
	sub	b		;Set Z if b=WBSIZE

	pop	b
	pop	d

	ret

;***Subroutine*****************************************
; Print CR, LF, then In-line Message
;  The call to ILPRNT is followed by a message string.
;  The last message chr has its msb set.
; Trashes psw
;******************************************************
CILPRT:	call	ILPRNT
	db	CR,LF+80h

; fall into ILPRNT

;***Subroutine*****************************************
; Print In-line Message
;  The call to ILPRNT is followed by a message string.
;  The last message chr has its msb set.
; On Exit:
;  Z cleared
; Trashes psw
;******************************************************
ILPRNT:	xthl			;Save hl, get msg addr

IPLOOP:	mov	a,m
	ani	7Fh		;strip end marker
	call	PRINTA		;print byte
	mov	a,m		;end?
	inx	h		;Next byte
	ora	a		;msb set?
	jp	IPLOOP		;Do all bytes of msg

	xthl			;Restore hl,
				;..get return address
	ret

;***Subroutine*********************************
;Print chr in a on the console
;**********************************************
PRINTA:	push	h
	push	d
	push	b
	mvi	c,BCONOT
	mov	e,a
	call	BDOS
	pop	b
	pop	d
	pop	h
	ret

;***Error Exit*****************
;Write error
;******************************
ERRWRT:	call	CILPRT
	db	'Write erro','r'+80h

	jmp	CL2EXT		;close both files and exit

;***Error Exit*****************
;Bad hex digit in source file
;******************************
HEXERR:	call	ERROR
	db	' Bad hex digi','t'+80h

	jmp	CL2EXT		;close both files and exit

;***Error Exit*****************
;Checksum error in source file
;******************************
CSERR:	call	ERROR
	db	' Bad checksu','m'+80h

	jmp	CL2EXT		;close both files and exit

;***Error Exit*****************
;Unexpected EOF in source file
;******************************
EOFERR:	call	ERROR
	db	' Unexpected EO','F'+80h

	jmp	CL2EXT		;close both files and exit

;***Error Exit*****************
;Error closing source file
;******************************
SCERR:	call	ERROR
	db	' Can',QUOTE,'t close source fil','e'+80h

	jmp	EXIT

;***Error Exit*****************
;Error closing destination file
;******************************
DCERR:	call	ERROR
	db	' Can',QUOTE,'t close destination fil','e'+80h

	jmp	CL1EXT		;close the source and exit

;***Error Exit**************
;File create error
;***************************
MAKERR:	call	ERROR
	db	' Can',QUOTE,'t create fil','e'+80h

	jmp	CL1EXT		;close the source and exit

;***Error Exit**************
;File not found error
;***************************
FNFERR:	call	ERROR
	db	'File not foun','d'+80h

	jmp	EXIT

;***Error Exit**************
;Source type error
;***************************
STPERR:	call	ERROR
	db	'Source must be .HE','X'+80h

	jmp	EXIT

;***Error Exit**************
;general error error
;***************************
ERROR:	call	CILPRT
	db	'Error:',' '+80h
	jmp	ILPRNT		;print the rest of the error

;*****************
;File name strings
;*****************
HEXSTR:	db	'HEX'
ENTSTR:	db	'ENT'

;*********************
;Initialized Variables
;*********************
RDBBC:	dw	0		;read buffer available byte count
RDBPTR:	dw	RDBUF		;read buffer pointer

WRBBC:	dw	128*WBSIZE	;write buffer free byte count
WRBPTR:	dw	WRBUF		;write buffer pointer


;*********
;Help Exit
;*********
HLPEXT:	call	CILPRT

;    1234567890123456789012345678901234567890123456789012345678901234
 db '          HEX2ENT 1.00 by M. Eberhard'
 db CR,LF,LF
 db 'Converts an Intel hex file to a Solos-style ENT file'
 db CR,LF,LF
 db 'Usage:',CR,LF
 db ' HEX2ENT <source>[.HEX] [<dest>[.<typ>]]'
 db CR,LF,LF
 db 'The source file must be .HEX, which will be assumed if'
 db CR,LF
 db 'ommitted. The destination will be <source>.ENT if ommitted.'
 db CR,LF+80h

	jmp	EXIT

;*************
; Buffers
;*************
;Initialized destination file control block

FCB2:	db	0		;(dr) use default drive
	db	'        '	;(f1-f8)
	db	'   '		;(t1-t3)
	db	0,0,0,0		;(ex,s1,s2,rc)
	dw	0,0,0,0,0,0,0,0	;(d0-d15)
	db	0,0,0,0		;(cr,r0,r1,r2)

;Record data buffer

RECBUF:	ds	256		;allow for big records

;Local stack

	ds	32
STACK:

;Disk read data buffer

RDBUF:	ds	RBSIZE*128

;Disk write buffer

WRBUF:	ds	WBSIZE*128

	END

