;****************************************************************************
;
; FLEXPGM - EEPROM programmer for the FLEX64K RAM/EEPROM board
;
;    This program is designed to run standalone on most any computer. Set
;    the serial I/O port parameters immediately below as required for your
;    serial board.
;
;    The first line in the .hex file must have the lowest address in the
;    file. This address is used as the offset for all remaining data in the
;    file. The maximum address range that can be programmed with a single
;    hex file is about 30K.
;
; EEPROM Software Lock
;    The programming routine unlocks the EEPROM, programs the part, then
;    re-locks the part when done. If you have a locked part, and/or you
;    want the part locked when programming completes, then the following
;    address blocks must be enabled as ROM even if they are not in the
;    range you are trying to program: 4K block at A000, 1K block at D400.
;
;    Ver      Date	  Description
;    ---    ---------	---------------------
;    1.0    2/11/2014	Original, Mike Douglas
;
;****************************************************************************

; Set serial port equates to match your I/O board

SIOSR	equ	10h		;serial status port
SIODR	equ	11h		;serial data port
RDRF	equ	01h		;receive data register full
TDRE	equ	02h		;transmit data register empty
EORMASK	equ	00h		;use 0FFh for negative true status bits

; Misc equates

CR	equ	13
LF	equ	10
BS	equ	8

	org	0100h
;-----------------------------------------------------------
; Main program loop
;-----------------------------------------------------------
main	lxi	sp,stack	;init stack pointer
	lxi	h,mMenu		;display main menu
	call	dspMsg
	call	getChar		;wait for user to continue

	call	loadHex		;load hex file into promBuf
	lda	haveBas		;successful load?
	ora	a
	jz	main		;no, restart

	call	confirm		;confirm and/or change addresses
	call	unlock		;ensure EEPROM is unlocked
	call	program		;write the EEPROM range
	call	lock		;lock against writes

	lxi	h,mDone		;"EEPROM Written"
	call	msgExit
	jmp	main		;start over

;-----------------------------------------------------------
; confirm - Display the start and end addresses found
;   and give the user the option to continue, change
;   the addresses, or abort.
;-----------------------------------------------------------
confirm	lhld	baseAdr		;make start address readable
	xchg			;DE=start address
	lxi	h,mStart	;where to put it
	call	wrd2hex

	lhld	maxAddr		;make end address readable
	xchg			;DE=end address+1
	dcx	d
	lxi	h,mEnd		;where to put it
	call	wrd2hex

	lxi	h,mLoc		;display location prompt
	call	dspMsg

	call	getLine		;get user response
	lda	inbuf		;what was typed?
	cpi	'P'		;program?
	rz			;yes

	cpi	'X'		;abort?
	jz	main		;yes

	cpi	CR		;nothing typed?
	jz	confirm		;prompt again	

	call	hex2wrd		;see if valid address entered
	jnz	badVal		;invalid value, retry
	
	xchg			;save new start address
	shld	baseAdr
	xchg			;save new max address
	lhld	length
	dad	d
	shld	maxAddr
	jmp	confirm		;confirm again

badVal	lxi	h,mBadVal
	call	dspMsg
	jmp	confirm

;-----------------------------------------------------------
; loadHex - Receive .hex file through serial port and save
;   in promBuf
;-----------------------------------------------------------
loadHex	lxi	h,promBuf	;init buffer to 0FFh
	mvi	b,0FFh
	mvi	a,80h		;stop at 8000h

initlp	mov	m,b		;0FFh to all of promBuf
	inx	h
	cmp	h
	jnz	initLp

	xra	a
	sta	haveBas		;"have base address" is false
	lxi	h,0		;init max address found
	shld	maxAddr
	lxi	h,mSend		;"Send hex file now"
	call	dspMsg

; Receive and save a hex file line

rcvLine	mvi	a,CR		;display CR/LF
	call	dspChar
	mvi	a,LF
	call	dspChar

	mvi	b,0		;clear echo character flag
	mvi	d,0		;init checksum

wtMark	call	getChar		;read next character
	sui	':'		;record marker?
	jnz	wtMark		;no, keep looking

	call	inByte		;input two hex digits (byte count)
	mov	a,e		;test for zero byte count
	ora	a
	jz	rcvDone	;count of 0 means end
	mov	c,e		;c=byte count on line

; Get line address in HL, update max address found, set base address
;   if first hex record.

	inr	b		;set echo flag for address bytes
	call	inByte		;H=MSB of address
	mov	h,e
	call	inByte		;L=LSB of address
	mov	l,e
	dcr	b		;clear the echo flag
	call	addrChk		;check for base and max address

; Subtract the base address from the current address. Exit with error if
;   this line's address is below the base address.

	push	d		;preserve checksum
	xchg			;DE=line address
	lhld	negBase		;compute HL=address in promBuf
	dad	d
	jc	addrOk		;addr >= base

	lda	baseByt		;ok if base address is 0
	ora	a
	jnz	baseErr		;record address < base address

addrOk	lxi	d,promBuf	;compute final address		
	dad	d
	pop	d		;restore checksum

	call	inByte		;ignore/discard record type
	
; Receive the data bytes of the record and move to promBuf, then
;    validate checksum

dataLp	call	inByte		;read a data byte (2 hex digits)
	mov	m,e		;store in promBuf
	inx	h
	dcr	c
	jnz	dataLp

	call	inByte		;read and validate checksum
	jz	rcvLine		;good if zero, go on to next line

csumErr	lxi	h,mChkSum	;else display checksum error message
	jmp	rcvErr

; baseErr - First record wasn't the lowest address in the file

baseErr	lxi	h,mBase		;display the error

rcvErr	xra	a		;flag the error by setting haveBas false
	sta	haveBas

; msgExit - Display message pointed to by HL, flush incoming data,
;    prompt user to press CR, wait for repsonse, then exit

msgExit	call	dspMsg		;display the error message
	call	flush		;flush incoming data
	lxi	h,mCont		;"Press return to continue:"
	call	dspMsg
	call	getChar
	ret

; rcvDone - Receive operation successful. Compute total length and save in
;    length. Flush incoming data and exit

rcvDone	lhld	negBase		;compute length
	xchg			;DE is -(start address)
	lhld	maxAddr
	dad	d
	shld	length
				;fall into flush and exit
;-----------------------------------------------------------
; flush - Remove received characters until none are received
;   for 1/4 second
;-----------------------------------------------------------
flush	in	SIODR		;clear possible received char
	lxi	d,8621		;0.25s timeout for 58 cycle loop

flshLp	in	SIOSR		;(10) look for character on console
	xri	EORMASK		;(7) account for polarity
	ani	RDRF		;(7) data present?
	jnz	flush		;(10) yes, start over	

	dcx	d		;(5) decrement timeout
	mov	a,d		;(5)
	ora	e		;(4)
	jnz	flshLp		;(10) loop until zero

	ret

;-----------------------------------------------------------
; addrChk - Update max address found, set base address
;    if this is the first hex record.
;-----------------------------------------------------------
addrChk	push	h		;preserve start of line address
	dad	b		;HL=end of line address+1
	lda	maxAddr		;higher max address?
	sub	l
	lda	maxAddr+1
	sbb	h
	jnc	chkBase		;no, not higher
	shld	maxAddr		;else, set new maximum address

; If this is the 1st record, use its address as the base address

chkBase	pop	h		;HL=line address
	lda	haveBas		;have base address yet?
	ora	a
	rnz			;yes, we're done

	inr	a		;set "have base address" flag true
	sta	haveBas
	shld	baseAdr		;use first line as base address

	mov	a,h		;byte baseByt = 0 if word baseAdr
	ora	l		;   is zero
	sta	baseByt

	xra	a		;compute -(base address)
	sub	l
	sta	negBase
	mvi	a,0
	sbb	h
	sta	negBase+1
	ret

;-----------------------------------------------------------
; program - Write EEPROM from promBuf
;-----------------------------------------------------------
program	lhld	length		;put length in BC
	mov	b,h
	mov	c,l
	lhld	baseAdr		;HL=destination
	lxi	d,promBuf	;DE=source
	
pgmLp1	mov	a,l		;on 64 byte page boundary?
	ani	3Fh
	jz	pageLp		;yes

	ldax	d		;program one byte
	mov	m,a
	inx	h
	inx	d
	call	waitPgm

	dcx	b		;decrement and test byte count
	mov	a,b
	ora	c
	jnz	pgmLp1

pageLp	mvi	a,64		;64 bytes per page
	push	psw
	
pgmLp2	ldax	d		;load a byte into page
	mov	m,a
	inx	h
	inx	d
	dcx	b		;decrement total byte counter
	mov	a,b
	ora	c
	jz	lastPgm		;all bytes moved

	pop	psw		;decrement page byte count
	dcr	a
	push	psw
	jnz	pgmLp2

	call	waitPgm		;wait for the page to program
	pop	psw		;remove page byte counter
	jmp	pageLp

lastPgm	call	waitPgm		;wait for final page to program
	pop	psw		;remove page byte counter
	ret

;-----------------------------------------------------------
; waitPgm - Waits for programming cycle to complete by
;    looping until the byte read from EEPROM matches the
;    last byte written. Aborts if this doesnt complete
;    within 100ms.
;-----------------------------------------------------------
waitPgm push	b		;preserve BC
	lxi	b,4167		;48 cycle loop, about 100ms
	dcx	h		;point to last byte written
	dcx	d

waitLp	dcx	b		;(5) timeout counter
	mov	a,b		;(5)
	ora	c		;(4)
	jz	pgmFail		;(10)
	
	ldax	d		;(7) bytes match yet?
	cmp	m		;(7) 
	jnz	waitLp		;(10) no

	inx	h		;done, restore HL and DE
	inx	d
	pop	b		;restore BC
	ret

pgmFail	lxi	h,mPgmErr	;display error message
	call	msgExit
	jmp	main		;start over

;-----------------------------------------------------------
; unlock - Issue unlock sequence to the EEPROM
;-----------------------------------------------------------
unlock	mvi	a,0AAh		;step 1
	sta	0D555h
	mvi	a,055h		;step 2
	sta	0AAAAh
	mvi	a,080h		;step 3
	sta	0D555h
	mvi	a,0AAh		;step 4
	sta	0D555h
	mvi	a,055h		;step 5
	sta	0AAAAh
	mvi	a,020h		;step 6
	sta	0D555h
	jmp	lockDly		;wait 100ms

;-----------------------------------------------------------
; lock - Issue lock sequence to the EEPROM
;-----------------------------------------------------------
lock	lda	0D555h		;get a byte to write back
	mov	b,a

	mvi	a,0AAh		;step 1
	sta	0D555h
	mvi	a,055h		;step 2
	sta	0AAAAh
	mvi	a,0A0h		;step 3
	sta	0D555h

	mov	a,b		;write copied data
	sta	0D555h

lockDly	lxi	d,8333		;100ms for 24 cycle loop

dlyLoop	dcx	d		;(5) decrement timeout
	mov	a,d		;(5)
	ora	e		;(4)
	jnz	dlyLoop		;(10) loop until zero
	ret

;-----------------------------------------------------------
; inByte - Read two ascii hex bytes and return binary
;    value in e. 
;-----------------------------------------------------------
inByte	call	getChar		;get a character
	call	asc2Bin		;ascii hex digit to binary
	add	a		;put in msn, zero lsn
	add	a
	add	a
	add	a
	mov	e,a		;save byte with MSN in E

; 2nd byte (LSN)

	call	getChar		;get a character
	call	asc2Bin		;ascii hex digit to binary
	add	e		;combine msn and lsn
	mov	e,a		;save in E
	add	d		;add character to checksum
	mov	d,a
	ret		

;-------------------------------------------------------------
; asc2Bin - ASCII hex digit to binary conversion. Digit
;    passed in a, returned in a. Carry clear for valid
;    conversion, carry set for error.
;-------------------------------------------------------------
asc2Bin	sui	'0'		;'0' to 0
	rc			;error - below zero
	cpi	10		;0-9 ?
	jc	ascExit		;yes
	sui	11h		;A-F to 0-5
	rc			;error - below A
	adi	10		;convert back to A-F
	cpi	16		;too big? (carry clear if so)
ascExit	cmc			;complement carry
	ret

;-----------------------------------------------------------
; wrd2hex - Convert word in DE into four ascii hex bytes
;    and store where pointed to by HL. HL=HL+4 on exit.
;-----------------------------------------------------------
wrd2hex	call	byt2hex		;do 1st byte
	mov	d,e		;do 2nd byte
				;fall into byt2hex	
byt2hex	mov	a,d		;A=byte to convert
	rrc			;put msn into lsn
	rrc
	rrc
	rrc
	ani	0Fh		;get nibble alone
	adi	'0'		;convert to printable ascii
	cpi	'9'+1		;see if > '9'
	jc	msnDone
	adi	7		;make A to F printable

msnDone	mov	m,a		;store msn at (HL)
	inx	h

	mov	a,d		;get original byte back
	ani	0Fh		;get lsb alone
	adi	'0'		;convert to printable ascii
	cpi	'9'+1		;see if > '9'
	jc	lsnDone		;no, lsn is done
	adi	7		;make A-F printable

lsnDone	mov	m,a		;store at (HL)
	inx	h
	ret

;-----------------------------------------------------------------------------
; hex2wrd - Convert CR terminated ASCII string in inbuf to a binary value
;    in DE. Exits with zero true for valid hex value, else zero false.
;-----------------------------------------------------------------------------
hex2wrd	lxi	h,inbuf		;HL->input buffer
	lxi	d,0		;result in D
	lxi	b,0		;BC has new nibble

hwLoop	mov	a,m		;A=next typed character
	cpi	CR		;end?
	rz			;yes

	call	asc2bin		;get value of ASCII nibble
	jc	hwErr
	mov	c,a		;BC=new nibble

	xchg			;result in HL for now
	dad	h		;shift left 4
	dad	h
	dad	h
	dad	h
	dad	b		;add in new nibble
	xchg			;result back in DE

	inx	h		;move to next byte
	jmp	hwLoop

hwErr	mvi	a,1		;force non-zero result
	ora	a
	ret

;-------------------------------------------------------------
; dspMsg - Display null terminated message pointed to by HL
;-------------------------------------------------------------
dspMsg	mov	a,m		;A=next byte to send
	inx	h
	ora	a		;null terminator?
	rz			;yes, exit
	call	dspChar
	jmp	dspMsg

;-------------------------------------------------------------
; dspChar - Display character from A. Character remains in
;    A on exit.
;-------------------------------------------------------------
dspChar	push	psw		;preserve character to send
	
dcWait	in	SIOSR		;wait for ready to send
	xri	EORMASK		;account for bit polarity
	ani	TDRE
	jz	dcWait

	pop	psw		;get back character to send
	out	SIODR
	ret

;-------------------------------------------------------------
; getLine - Read user input into inbuf until terminated
;    by CR.
;-------------------------------------------------------------
getLine	lxi	h,inbuf		;read into inbuf
	mvi	b,0		;turn off echo

glLoop	call	getChar		;read next character
	cpi	'A'		;if alpha, force upper case
	jc	glNum		;treat as number
	ani	5Fh		;lower alpha to upper
glNum	mov	m,a		;put in the buffer

	cpi	CR		;exit if CR
	rz
	cpi	BS		;process back space
	jz	backSp

	call	dspChar		;echo character
	inx	h		;next spot in buffer
	jmp	glLoop

backSp	mvi	a,inbuf and 0FFh
	cmp	l		;already at start of line?
	jz	glLoop		;yes, ignore the BS

	dcx	h		;backup in buffer
	mvi	a,BS		;output BS, space, BS
	call	dspChar
	mvi	a,' '
	call	dspChar
	mvi	a,BS
	call	dspChar

	jmp	glLoop

;-------------------------------------------------------------
; getChar - read a character from the 2SIO port. The character
;    is also echoed to the console port if the echo flag (b)
;    is set (non-zero)
;-------------------------------------------------------------
getChar	push	b		;save b,c

gcWait	in	SIOSR		;wait for character
	xri	EORMASK		;account for bit polarity
	ani	RDRF
	jz	gcWait

	in	SIODR		;get the character
	ani	7Fh		;strip parity
	mov	c,a		;save character in c
	mov	a,b		;echo flag (b) set?
	ora	a
	jz	noEcho		;no echo

	mov	a,c		;a=character to send
	pop	b		;restore b,c
	jmp	dspChar		;display character and exit

noEcho	mov	a,c		;a=byte read
	pop	b		;restore b,c
	ret

;-------------------------------------------------------------
; Messages
;-------------------------------------------------------------
mMenu	db  CR,LF,LF,LF,'=== FLEX64K EEPROM Programmer v1.0 ===',CR,LF
	db  LF
	db  '1) EEPROM must be enabled and RAM disabled for the range',CR,LF
	db  '   of EEPROM memory you want to program',CR,LF
	db  '2) EEPROM blocks A000 and D400 must be enabled and RAM',CR,LF
	db  '   disabled for the EEPROM unlock/lock to work',CR,LF
	db  '3) The EEPROM write-enable switch (SW7-3) must be ON to',CR,LF
	db  '   allow writes to EEPROM',CR,LF
	db  LF
mCont	db  'Press CR to continue: ',0

mSend	db  CR,LF,LF,'Send .hex file now...',0

mLoc	db  CR,LF,LF,'** EEPROM will be written from '
mStart	db  'xxxx to '
mEnd	db  'xxxx **',CR,LF,'P to program, X to abort, '
	db  'or key in a new start address: ',0

mDone	db  CR,LF,LF,'EEPROM written successfully',CR,LF,LF,0
 
mChkSum	db  CR,LF,LF,'Checksum error',CR,LF,LF,0

mPgmErr	db  CR,LF,LF,'Programming failed - check EEPROM write '
	db  'enable switch',CR,LF,LF,0

mBase	db  CR,LF,LF,'First line of hex file must be the lowest address'
	db  CR,LF,LF,0

mBadVal	db  CR,LF,LF,'Invalid hex address entered',0

;-------------------------------------------------------------
;  Data Area
;-------------------------------------------------------------
haveBas	ds	1		;non-zero if have base address offset
baseByt	ds	1		;will be zero if base address is zero
baseAdr	ds	2		;base address of loaded file
negBase	ds	2		;negative of baseAdr
maxAddr	ds	2		;maximum address found
length	ds	2		;maxAddr-baseAdr
inbuf	ds	64		;console input buffer
	ds	32		;stack space
stack	equ	$
promBuf	equ	$

	end
