	title "EPROM Programmer Kernel Rev 1.00"
	subtitle "Copyright (C) 2013 Martin Eberhard"
	list b=4, c=132, n=80

;********************************************************************************
;* EPROM Programmer Loader Kernel												*
;* Written for both the ME1702/A and the ME5204 EPROM programmers				*
;* designed to work with the PIC16F1517 and the PIC16F1519						*
;*																				*
;*Revision    Date        Author         Comments								*
;*	1.00   	05JAN2013	M.Eberhard	Created	& Tested with ME1702/A and ME5204	*
;*																				*
;********************************************************************************
; Major revision must change if the firmware is not fully compatable with		*
; previous revisions.															*
;********************************************************************************
REV_MAJOR		equ	0x01		;major revision number
REV_MINOR		equ	0x00		;minor revision number

TRUE	equ		0xff
FALSE	equ		0

;********************************************************************************
; Assembly options for various EPROM programmers								*
; NOTE: Exactly one of the following must be true								*
;********************************************************************************
ME1702A	equ		TRUE
ME5204	equ		FALSE

;********************************************************************************
; Print a message in the listing that indicates for which EPROM programmer this 
; loader was assembled.
;********************************************************************************
	if	ME1702A && ME5204
	error	"You must select exactly one programmer!"
	endif

	if	ME1702A
PIC17	equ	TRUE
	messg	"Assembled for ME1702A"
	subtitle "Assembled for ME1702A.  Copyright (C) 2012 Martin Eberhard"
	endif

	if	ME5204
PIC17 equ	FALSE
	messg	"Assembled for ME5204"
	subtitle "Assembled for ME5204.  Copyright (C) 2012 Martin Eberhard"
	endif

	ifndef	PIC17
	error "No Programmer Selected!"
	endif

	errorlevel 1		;eliminate all the 'not in bank 0' messages

;********************************************************************************
;*								GENERAL NOTES									*
;*																				*
;* This Loader Kernel allows EPROM programmer firmware to be loaded via the		*
;* serial port, and written to the PIC's FLASH memory, without any development	*
;* hardware (such as a PICkit 3). It also provides some useful subroutines for	*
;* the EPROM programmer, reusing code needed for the loader. All of these		*
;* available subroutines can be accessed via a fixed-location jump table.		*
;*																				*
;* Firmware load is via Intel Hex file, as produced by the Microchip's MPASM	*
;* (version 8.83). Unfortunately, the RAM in the PIC microcontroller is too		*
;* small to hold the entire firmware, so downloaded firmware is written into	*
;* FLASH as it is received. Any Intel Hex errors, as well as read-back verify	*
;* errors, will be reported - but these errors will likely result in corrupted	*
;* firmware. However, this entire kernel is protected (by 'boot' protection in	*
;* configuration register CONFFIG2) and cannot be overwritten. So you can try	*
;* reloading the firmware if it fails to load the first time.					*
;*																				*
;* This program must be no more than 512 words long - the amount of FLASH that	*
;* is protected by the 'boot' protection in config2. If the code is larger than	*
;* 512 bytes, then an error will be generated when assembled. Keeping the code	*
;* small, but also fully functional, required some tricky programming...		*
;* 																				*
;* The EPROM programmer firmware should load at address 0x200 (LOADED_CODE).	*
;* This program installs a dummy program there, which will be over-written when	*
;* programmer code is loaded via the serial port.								*
;*																				*
;* To invoke the loader, lean on the 'L' key (so that it auto-repeats), and		*
;* then turn on the EPROM programmer. You should see the Loader message instead	*
;* of the EPROM programmer's sign-on message.									*
;*																				*
;* To leave the loader and run the loaded firmware, type 3 or more ESCs at the	*
;* loader prompt. (ESCs will be ignored while a file is loading.)				*
;*																				*
;* Note that the PIC gets partially initialized by this loader. The programmer	*
;* firmware needs to initialize everything else.								*
;*																				*
;* This program assumes the PIC is clocked at 16 MHz, meaning a 4 MHz CPU       *
;* operating speed, or 0.25 uS instruction cycle time. The internal oscilator	*
;* has an accuracy of +/- 10%, which must be considered in all timing.			*
;*																				*
;* Page references in the comments refer to the appropriate pages in Microchip	*
;* document DS41452B "PIC16(L)F1516/7/8/9 Data Sheet"							*
;********************************************************************************
	page
;********************************************************************************
;* CALL Vectors for External Code Subroutine Access								*
;* Address	Vector		     Notes												*
;*  0000	RESET			Cold Reset (not meant to be called)					*
;*	0002	K_KERN_REV		get Loader Kernel firmware revision level in W		*
;*  0003	K_PROG_ID		returns W = 00 for ME1702A, W = 01 for ME5204		*
;*  0004    INT_VECTOR		Interrupt Vector (not meant to be called)			*
;*  0005	K_CONIN			get one Rx Queue chr into W & R0. Z set if empty	*
;*  0006	K_CONOUT		Send W to console (returns chr in R0)				*
;*	0007	K_PRINTF		print 0-terminated string at PMADRL & PMADRH		* 
;*							(must set BSR to 0x03 first)						*
;*							trashes W, R0, BSR, PMDATL, PMDATH, PMADRL, PMADRH	*
;*	0008	K_PRINTHEX1		print low nibble of W as 1 hex chr. trashes R0		*
;*	0009	K_PRINTHEX2		print W as 2 hex chrs. Trashes R0 & R1				*
;*  000A	K_HEX2BIN		Convert hex value in R0 to binary, combine it with	*
;*							nibble already in R1. On return, R0=new binary		*
;*							nibble; R1=W=combined nibbles						*
;*	000B	K_STALL25M		stall for W * 25 mSec (trashes R0 & R1)				*
;*	000C	K_STALL250U		stall for W * 250 uSec (trashes R0)					*
;*	000D	K_STALL1U		stall for W + 1 uSec								*
;*																				*
;*  0200	LOADED_CODE		beginning of serial port-loaded programmer code		*
;********************************************************************************
;* RAM assignments as used by Kernel subroutines								*
;* (The code loader will trash much of the available RAM.)						*
;*																				*
;* Common RAM																	*
;*    Address     Name          Function										*
;*		70		R0			General purpose register							*
;*		71		R1			General purpose register							*
;*	  72-7a		available	available for use by programmer firmware			*
;*		7b		RQ_IPTR		Receive queue in-pointer (written only by Rx		*
;*							interrupt service routine!)							*
;*		7c		RQ_OPTR		Receive queue out-pointer							*
;*		7d		TQ_IPTR		Transmit queue in-pointer							*
;*		7e		TQ_OPTR		Transmit queue out-pointer (written only by Tx		*
;*							interrupt service routine!)							*
;*		7f		INT_FLAGS	Flags used with the UART interrupts					*
;*		  Bit     Name          Function										*
;*		   0	XOFF_STATE	Transmitter stopped due to received XOFF			*
;*		   1	TQ_EMPTY	Tx queue is empty (set by Tx interrupt service)		*
;*		   2	PREV_CR		previous chr was CR (used internally by Rx int)		*
;*		   3	PREV_LF		previous chr was LF (used internally by Rx int)		*
;*		   4	RQ_FULL		Rx queue is full (set by Tx interrupt service)		*
;*		  5-7	reserved	reserved for Kernel	future use						*
;*																				*
;* Linear RAM																	*
;*    PIC16F1517  PIC16F1519													*
;*     Address     Address     Name          Function							*
;*		2000		2000	RAM_BUF		enough space for EPROM image buffer		*
;*		2100		2200	RX_QUEUE	UART receive circular queue (128 bytes)	*
;*		2180		2280	TX_QUEUE	UART transmit circular queue (16 bytes)	*
;*		2190		2290	available	start of available RAM					*
;*		21EF		23EF	end			last RAM address in processor			*
;********************************************************************************
;* Kernel Initialization														*
;*																				*
;*  # The PIC configuration registers are initialized. Note that this loader	*
;*    will not allow you to change the configuration registers.					*
;*  # The oscillator is initialized.											*
;*  # Ports C and E are initialized with the UART enabled and all programmer	*
;*    power supplies turned off. The other ports are not initialized.			*
;*	# The transmit and receive queues are initialized.							*
;*  # The transmit and receive interrupts are active.							*
;*  # Everything else is initialized to the PIC's power-on default value.		*
;********************************************************************************
	page
;********************************************************************************
;* Intel Hex Format	for Firmware Download										*
;*																				*
;*  # Three record types are accepted; all other records are ignored.			*
;*     1) Type 00 records are data records. Data records are written to FLASH	*
;*        only if a Type 04 record has already been received, with extended		*
;*        address = 0000. If no Type 04 record has been received yet, or if the	*
;*        last Type 04 record set the extended address to something other than	*
;*        0000, then the data record will be ignored. This means (for example)	*
;*        that you cannot write to the Config registers of the PIC.				*
;*        																		*
;*     2) A Type 01 record (with 0 bytes of data) is an End-Of-File	record, and	*
;*        is required at the end of the file to force a write to FLASH of the	*
;*        last RAM buffer full of data.	NOTE: a data record with 0 bytes of		*
;*        data is NOT treated as an End-of-file record.							*
;*     3) Type 04 records set the extended address for the subsequent records.	*
;*        The "address" field of the Type 04 record (bytes 2 and 3) is ignored.	*
;*        The first 2 bytes of the "data" field (bytes 5 and 6) set the			*
;*        extended address. MPASM sets the extended address to 0000 for FLASH	*
;*        data, and 0001 for the PIC Config registers.							*
;*  # The PIC's FLASH memory is organized in 32-word "rows", where a word is	*
;*    14 bits wide. MPASM splits each word into two sequential bytes in the		*
;*    hex file, with the low 8 bits in the first byte and the high 6 bits in	*
;*    the second byte. This means that the address in each Intel hex record is	*
;*    the FLASH address times 2.												*
;*  # This loader assumes that each Intel hex record fits completely within one	*
;*    32-word (64-byte) FLASH row. However, one FLASH row may be (probably will	*
;*    be) made up of several hex records.										*
;*  # If the hex file has data for only part of a FLASH row, then only the data	*
;*    supplied in the Hex file will be changed - the other bytes in that FLASH	*
;*    row will remain unchaged, because this program first reads the old FLASH	*
;*    data from each row into its RAM buffer, filling in the missing data for	*
;*    that row.																	*
;*  # After each row is written, it is read back and verified.					*
;*  # The checksum is checked for all records, including ignored records.		*
;*  # The following conditions will generate an error message, and increment 	*
;*    error count that is reported after the end of the file:					*
;*     1) Checksum Error														*
;*     2) Bad Hex Error (something besides 0-9 or A-F when expecting hex)		*
;*     3) Verify Error (FLASH readback did not match the RAM buffer data)		*
;*  # A FLASH write occurs when a new hex record addresses FLASH memory in a	*
;*    different FLASH row than the previous record, or when a type 01 record	*
;*    is received. This means that FLASH write verification occurs after the	*
;*    address and record-type fields of a hex data record have been received, 	*
;*    and before the data bytes are received. And this means that if a verify	*
;*    fails, the verify error message will be printed in the middle of a hex	*
;*    record, and will refer to the previous record(s).							*
;*  # All characters are ignored until a colon (marking the start of an Intel	*
;*    hex record) has been received. This means that comment lines in the Intel	*
;*    hex file will be ignored. This also means that any record where the		*
;*    initial colon has been corrupted will be ignored without being caught as	*
;*    an error.																	*
;*  # Receipt of a type 01 record (End-Of-File) causes the following actions:	*
;*     1) The total number of records loaded into the FLASH is printed. This	*
;*        can be checked manually, to make sure no records were dropped, and	*
;*        the load was in fact successful.										*
;*     2) The total number of errors detected is printed. Anything other than	*
;*        0000 means that the load was unsuccesful, and the loaded code should	*
;*        not be trusted.														*
;*     3) control returns to the loader, reprinting the loader's brief sign-	*
;*        on message. (To run the new code, hit the ESC key 3 times in a row)	*
;********************************************************************************
	page
;********************************************************************************
;* Include standard headers, programmer header, and macros						*
;********************************************************************************
	if PIC17
	#include	P16F1517.INC
	else
	#include	P16F1519.INC
	endif

	#include	ASCII.inc
	#include	MartinsMacros.inc
	#include	Kernel.inc

	page
;################################################################################
;# Dummy Firmware - gets overwritten by programmer firmware						#
;################################################################################
	org		LOADED_CODE

				movlb	0x03				;for PMADRL, PMADRH
				movlw	low(nocode_msg)		;'No Code'
				movwf	PMADRL
				movlw	high(nocode_msg)
				movwf	PMADRH
				call	K_PRINTF
				goto	code_loader

nocode_msg:		da	"\rNo programmer firmware loaded."
	if ME1702A
				da	"\rSend ME1702A.hex now.\r",0
	endif
	if	ME5204
				da	"\rSend ME5204.hex now.\r",0
	endif

	page
;################################################################################
;# Reset Vector - hardware-fixed location										#
;# Maximum of 2 words long - otherwise it will collide with K_KERN_REV.			#
;################################################################################
	org 	RESET_VECTOR				;routine must be exactly 2 words long
				clrf	PCLATH			;all code is in the first 2K of code space
				goto	init

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Fixed-location subroutine to return the version for this loader.
; This subroutine can only be 1 word long here!
; On Exit:
;     W bits 7:4 = major revision number
;     W bits 3:0 = minor revision number
;*==============================================================================*
	org		K_KERN_REV
				retlw	(REV_MAJOR << 4) | REV_MINOR

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Fixed-location subroutine to return the ID for this programmer.
; This subroutine can only be 1 word long here!
; On Exit:
;     W=0 for ME1702A, 1 for ME5204
;*==============================================================================*
	org		K_PROG_ID

	if ME1702A
				retlw	0x00
	endif
	if	ME5204
				retlw	0x01
	endif

;################################################################################
;# Interrupt Vector - hardware-fixed location									#
;################################################################################
	org		INT_VECTOR
				goto	int_service	

;################################################################################
;# Kernel call vectors (fixed locations for access by external code)			#
;# INT_VECTOR = 0x04. This table begins at 0x05, immediately following the		#
;# interrupt vector.															#
;################################################################################
	org		CALL_TABLE
K_CONIN:		goto	get_rxchr		;get one chr from Rx Q into W & R0. Z set if empty
K_CONOUT:		goto	printw			;send W to console
K_PRINTF:		goto	printf			;print 0-terminated string at PMADRL/H (set BSR to 0x03 too!)
K_PRINTHEX1:	goto	print_hex1		;print low nibble of W as 1 hex chr
K_PRINTHEX2:	goto	print_hex2		;print W as 2 hex chrs

K_HEX2BIN:		goto	hex2bin			;convert hex value in R0 to binary, combine with R1

K_STALL25M:		goto	stall_25ms		;stall for W * 25 mS
K_STALL250U:	goto	stall_250us		;stall for W * 250 uS

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to stall for W + 1 uS (including the call)
;
; This routine is right at the end of the jump vector table to eliminate the
; overhead of an additional goto instruction, whose 0.50 uS time is significant.
; On Exit:
;     W=0
;     Z set
;*==============================================================================*
K_STALL1U:							;Call Vector right here to make it fast
stall_1us:		addlw	0xff		;(.25)subtract 1 from W				
				btfss	STATUS,Z	;(.25)
				bra		stall_1us	;(.50)
			return					;(0.50)
	page
;################################################################################
;# Null-terminated strings used by kernel										#
;# located at the beginning of code to make sure they are all in the same page	#
;################################################################################
loader_msg:		da	"\x11\rME Loader "
				da	((REV_MAJOR + '0') << 7) + '.'
				da	((REV_MINOR + '0') << 7) + CR
				da	0
hexerror_msg:	da	" ?Hex",0
cksumerror_msg:	da	" ?Csm",0
verifyerr_msg:	da	" ?Ver ",0			;this error will print in the middle of a record
reccount_msg:	da	"\rLoaded Recs: ",0
errcount_msg:	da	"\rErrors: ",0

	page
;################################################################################
;# Initialization Routine														#
;#   Initialize only the registers where the Reset value is not right for us,	#
;#   and only initialize RAM as needed by Kernel.								#
;################################################################################
init:
;Bank 1 initialization
		movlb	0x01

		;internal oscillator block, 16 MHz (Page 64)
			movlw	(1 << SCS1) | (1 << IRCF0) | (1 << IRCF1) | (1 << IRCF2) | (1 << IRCF3)
			movwf	OSCCON

		;Set up UART interrupts
			movlw	(1 << RCIE)				;enable USART Rx int (Page 81)
			movwf	PIE1					;note: Tx int gets enabled when enqueing data

;initialize two of the five I/O ports per the example on page 113.
;Intersperse UART initialization as we are in bank 3
		movlb	0x0
			clrf	PORTC			;port C has some power supplies & the UART
			clrf	PORTE			;port E has some power supply controls

		movlb	0x2
			movlf	LATC_INIT,LATC
			movlf	LATE_INIT,LATE

		movlb	0x03
			movlw	0x38			;bits 3-5 are analog inputs (Page 124)
			movwf	ANSELC
			clrf	ANSELE

		;Initialize UART (page 237)
			movlw	(1 << TXEN)		;async mode, 8-bits,
			movwf	TXSTA			; low-speed baud rate (Page 246)

			movlw	(1 << SPEN) | (1 << CREN)
			movwf	RCSTA			;enable Rx and Tx, 8-bits, receive enabled (Page 247)

			clrf	SPBRGH
			movlw	0x19 			;9600 baud, w/ 8-bit baud rate generator
			movwf	SPBRGL			; & 16MHz clock (Page 249)

		movlb	0x01
			movlf	TRISC_INIT,TRISC
			movlf	TRISE_INIT,TRISE
			
;initialize the receive queue
	if	low(RX_QUEUE)
			movlw	low(RX_QUEUE)
			movwf	RQ_IPTR
			movwf	RQ_OPTR
	else
			clrf	RQ_IPTR
			clrf	RQ_OPTR
	endif

;initialize the transmit queue
	if	low(TX_QUEUE)
			movlw	low(TX_QUEUE)
			movwf	TQ_IPTR
			movwf	TQ_OPTR
	else
			clrf	TQ_IPTR
			clrf	TQ_OPTR
	endif

;initialize interrupt flags: queues are empty
			movlw	(1 << TQ_EMPTY)
			movwf	INT_FLAGS

			movlb	0x03
			movfw	RCREG			;chuck any junk in the UART receiver
			movfw	RCREG			;it's a double-buffer

;Enable interrupts - we're ready to go (Page 80)
			movlw	(1 << PEIE) | (1 << GIE)
			movwf	INTCON			;peripheral ints, global enable only

;################################################################################
;# CHOOSE LOADER OR LOADED FIRMWARE												#
;# Test for user typing 'L's to invoke the loader. If not, go to loaded code.	#
;# auto-repeat is faster than 500 mS per character - we should see one within	#
;# 500 mS. The first character in the queue might be trashed because it was		#
;# only partially received.														#
;################################################################################
			movlw	.40				;40 * 25 mS = wait for 1000 mS
			call	stall_25ms

			call	get_rxchr		;get Rx chr. W will not equal 'L' if nothing type
			xorlw	'L'				;L takes us to the loader
			btfss	STATUS,Z
			goto	LOADED_CODE		;no L typed: go to programming firmware

;fall into code_loader

	page
;################################################################################
;# Intel Hex Firmware Loader													#
;# Receive an entire file of Intel Hex records, and write them to FLASH memory	#
;# as appropriate.																#
;#																				#
;# Entry here is only from initialization (above), when the user has typed		#
;# several 'L's (that must be flushed), or from the dumme firmware code.		#
;################################################################################
; Print sign-on message, and initialize variables at beginning of file
code_loader:	call	start_loader		;clear counters, print sign-on message

;eat any following 'L's without echoing them
flush_l:			call	get_rxchr			;Z set if no chr. This does not echo
					btfsc	STATUS,Z
					bra		flush_l				;no characters

					xorlw	'L'
					btfsc	STATUS,Z
					bra		flush_l				;spin until non-'L' character

;echo the first non='L' character, and begin looking for hex records
				call	printr0				;manually echo the first non-'L' chr

				bra		load_start			;we already have the first chr in R0

;--------------------------------------------------------------------------------------
; (1)Wait for colon to start an Intel Hex record. If the user types three sequential
; ESCs, *and* we are not in the middle of a load (i.e. FLASH_MEM is not set), then
; test_esc will go to main. Ignore everything else.
;--------------------------------------------------------------------------------------
wait_recstart:	call	test_esc				;check for 3 ESCs, and get chr into R0

load_start:		cjne	R0,':',wait_recstart	;beginning of Intel Hex record?

				clrf	HEX_CKSUM				;initialize checksum
				bcf		INT_FLAGS,HERR_FLAG		;good hex so far

;--------------------------------------------------------------------------------------
; (2)Get hex record byte count and save it in BYTE_COUNT
;--------------------------------------------------------------------------------------
				call	get_hex2			;get byte count
				movwf	BYTE_COUNT			;save record byte count

;--------------------------------------------------------------------------------------
; (3,4)Get high & low address bytes from hex record, save them in ADDR_HIGH, ADDR_LOW
;--------------------------------------------------------------------------------------
				call	get_hex2			;get address high byte into W & R1
				movwf	ADDR_HIGH			;save high byte in ADDR_HIGH
				call	get_hex2			;get address low byte into W & R1
				movwf	ADDR_LOW			;save low byte in ADDR_LOW

;--------------------------------------------------------------------------------------
; (5)Get record-type byte and save it in REC_TYPE. Set msb of ADDR_HIGH if not type 0
; record. (msb of ADDR_HIGH set means this record has no FLASH data.) Also set msb
; of ADDR_HIGH if FLASH memory has not yet been selected by a type 4 record
;--------------------------------------------------------------------------------------
				call	get_hex2			;get Hex record type byte into W & R1
				movwf	REC_TYPE			;save record type in REC_TYPE

				movf	REC_TYPE,F			;test for type 0 record
				btfsc	STATUS,Z			;if no, remember no data should be loaded
				btfss	INT_FLAGS,FLASH_MEM	;type 0. FLASH memory already selected?
				bsf		ADDR_HIGH,7			;set msb so data won't load

;--------------------------------------------------------------------------------------
; Is this in a different FLASH row as the previously-received record? Is this record
; not a data record at all? If so, we must first flush the RAM buffer by writing
; it to FLASH, before we can receive the data from this new record.
; If msb of ADDR_HIGH is set, then this record should not cause a write to FLASH.
; Reasons for not loading data (msb of ADDR_HIGH is set):
; 1) This is not a data record (not type 0)
; 2) FLASH memory has not yet been selected (haven't received type 4 record with 0000)
;--------------------------------------------------------------------------------------
				movfw	ROWA_LOW			;compare record's address low byte
				xorwf	ADDR_LOW,W			;low byte row number match?
				andlw	0xC0				;test for 64-byte row
				btfss	STATUS,Z
				bra		new_row

				movfw	ROWA_HIGH			;high byte match?
				andlw	0x7f				;so that we trap msb set in ADDR_HIGH
				xorwf	ADDR_HIGH,W
				btfsc	STATUS,Z
				bra		same_row

;--------------------------------------------------------------------------------------
; This record is not in the same FLASH row as the previous record. Was the previous
; record one that should be written to FLASH? If so, we must flush the RAM buffer into
; FLASH before we can receive data from this next record.
;
;  FLASH_MEM set for program data, cleared otherwise (set by extended address record)
;  ADDR_LOW = low byte of new record address
;  ADDR_HIGH = high byte of new record address - msb set means none
;  REC_TYPE = record type for new record
;  BYTE_COUNT = record byte count
;  RECCNT_HIGH,RECCNT_LOW = total record count
;  ROWA_HIGH, ROWA_LOW = address of previous completed record 
;  ROWA_HIGH bit 7 set means the previous record was not FLASH data
;  FSR1H = high(RAM_BUF)
;--------------------------------------------------------------------------------------
new_row:		btfss	ROWA_HIGH,7		;was previous row a FLASH data record (msb cleared)?
				call	write_flash		;yes: write it to FLASH now

;--------------------------------------------------------------------------------------
; Now that the RAM buffer is available, deal with the new record that we have already
; begun to receive. We have already received the byte count, address, record type. 
;
; Set up to read hex record data into the RAM buffer
;
;  FLASH_MEM set for program data, cleared otherwise (extended address stuff)
;  ADDR_LOW = low byte of new record address
;  ADDR_HIGH = high byte of new record address - msb set means none
;  REC_TYPE = record type for new record
;  BYTE_COUNT = record byte count
;  RECCNT_HIGH,RECCNT_LOW = total record count
;  ROWA_HIGH, ROWA_LOW = address of previous completed record (msb set means none)
;  FSR1H = high(RAM_BUF)
;--------------------------------------------------------------------------------------
;compute and save new FLASH row address in ROWA_HIGH, ROWA_LOW
				movfw	ADDR_HIGH				;get record's address
				movwf	ROWA_HIGH				;...including msb flag
				movfw	ADDR_LOW
				andlw	0xC0					;make it a row address
				movwf	ROWA_LOW				;...saved in PREVA

;--------------------------------------------------------------------------------------
; See if this data is actually for FLASH memory (msb of high addres byte cleared).
; If so, read the existing FLASH data into the buffer before receiving the hex record
; data, so that any FLASH data in this row that is not part of the hex file will remain
; unchanged.
;
;  FLASH_MEM set for program data, cleared otherwise (extended address stuff)
;  ADDR_LOW = low byte of new record address
;  ADDR_HIGH = high byte of new record address - msb set means none
;  REC_TYPE = record type for new record
;  BYTE_COUNT = record byte count
;  RECCNT_HIGH,RECCNT_LOW = total record count
;  ROWA_HIGH, ROWA_LOW = ADDR_HIGH, ADDR_LOW
;  FSR1H = high(RAM_BUF)
;--------------------------------------------------------------------------------------
				btfss	ROWA_HIGH,7				;is this a FLASH data record (msb cleared)?
				call	read_ver_flash			;yes: read the flash data into RAM

;--------------------------------------------------------------------------------------
; The FLASH row is already set up, and FSR1H already points to the RAM buffer. Now set
; up FSR1L as RAM buffer pointer for this hex record. The buffer address for this hex
; record is the address within the FLASH row - the low 6 bits of the record address.
;
;  FLASH_MEM set for program data, cleared otherwise (extended address stuff)
;  ADDR_LOW = low byte of new record address
;  ADDR_HIGH = high byte of new record address - msb set means none
;  REC_TYPE = record type for new record
;  BYTE_COUNT = record byte count
;  RECCNT_HIGH,RECCNT_LOW = total record count
;  ROWA_HIGH, ROWA_LOW = address of previous completed record (msb set means none)
;  FSR1H = high(RAM_BUF)
;--------------------------------------------------------------------------------------
same_row:		movf	BYTE_COUNT,F			;0-data-byte record?
				btfsc	STATUS,Z
				bra		dr_ckcum				;yes: just check the checksum

				movfw	ADDR_LOW				;low byte of FLASH address		
				andlw	0x3F					;make it a RAM buffer address
				movwf	FSR1L 

;--------------------------------------------------------------------------------------
; Loop to get all BYTE_COUNT bytes of data in this record. We assume that all bytes
; within this record will fit within the same FLASH row.
;--------------------------------------------------------------------------------------
dr_data_loop:		call	get_hex2			;get a hex data byte into W & R1
					movwi	INDF1++				;write byte to buffer, bump pointer
					decfsz	BYTE_COUNT,F		;bump & test loop pointer
					bra		dr_data_loop

;--------------------------------------------------------------------------------------
; Get and test the record checksum. Report checksum error if wrong.
;--------------------------------------------------------------------------------------
dr_ckcum:		call	get_hex2				;get checksum into Z
				movlw	low(cksumerror_msg)		;checksum error message
				btfss	STATUS,Z				;checksum should be zero
				call	report_error

;--------------------------------------------------------------------------------------
; Report any hex errors accumulated during this record
;--------------------------------------------------------------------------------------
				btfsc	INT_FLAGS,HERR_FLAG
				call	hex_error

;--------------------------------------------------------------------------------------
; If this record has data to be loaded into FLASH, then bump RECCNT_HIGH,RECCNT_LOW
;--------------------------------------------------------------------------------------
				btfsc	ADDR_HIGH,7				;flash data record to load?
				bra		not_written				;no: don't bump count

				incf	RECCNT_LOW,F			;bump FLASH data record count
				btfsc	STATUS,Z				;need to carry?
				incf	RECCNT_HIGH,F
not_written:
;--------------------------------------------------------------------------------------
; Deal with the record type.
;--------------------------------------------------------------------------------------
;was this record type 1? Done loading hex file if so. 
				decf	REC_TYPE,W			;test for REC_TYPE = 1
				btfsc	STATUS,Z
				bra		load_done

;was this record type 4? if not, we are done with this record
				xorlw	0x03				;we already decremented REC_TYPE into W
				btfss	STATUS,Z
				bra		wait_recstart		;not type 4: done with this record

;This was a type 4 record. Set FLASH_MEM if record data = 0000 (FLASH memory space)
				bcf		INT_FLAGS,FLASH_MEM

				clrf	FSR1L				;look at first data byte
				moviw	FSR1++				;and the second data byte
				iorwf	INDF1,W				;both must be zero to select FLASH memory
				btfsc	STATUS,Z
				bsf		INT_FLAGS,FLASH_MEM

				bra		wait_recstart		;with REC_TYPE = 1								

;--------------------------------------------------------------------------------------
; The code load is complete. Print results, and start over
;--------------------------------------------------------------------------------------
load_done:		movlw	low(reccount_msg)	;Loaded recs:
				call	loader_printf

				movfw	RECCNT_HIGH			;RECCNT_HIGH has record count high byte
				call	print_hex2
				movfw	RECCNT_LOW			;RECCNT_LOW has record count low byte
				call	print_hex2

				movlw	low(errcount_msg)	;errors:
				call	loader_printf		;leaves BSR=3

;				movlb	0x03
				movfw	ERRCNT_HIGH			;RECCNT_HIGH has record count high byte
				call	print_hex2			;trashes BSR

				movlb	0x03				;ERRCNT_LOW is in bank 3
				movfw	ERRCNT_LOW			;RECCNT_LOW has record count low byte
				call	print_hex2

				call	start_loader		;clear counters, reprint sign-on message
				goto 	wait_recstart		;back to the beginning

	page
;################################################################################
;# Interrrupt Handlers															#
;#   Note: interrupts cause the PIC to save W, FSR's, STATUS automatically in 	#
;#   shadow registers, and the retfie instruction restores them. (Page 79)		#
;#   Note: Only the UART interrrupts are enabled.								#
;#   This services both the transmit and receive UART interrupts once.			#
;#   The FSR0 shadow register pair is used as a pointer into the com queues.	#
;#   The FSR1 shadow register is available for scratch registers.				#
;################################################################################
;Service the UART receive interrupt, if needed. The receive interrupt
;is always enabled, though the global interrupt mask may be set.
int_service:	movlw	high(RX_QUEUE) 
				movwf	FSR0H

				movlb	0x0					;PIR1 is in bank 0
				btfsc	PIR1,RCIF			;test for pending UART rx interrupt
				call	rxi_service

;Service the UART transmit interrupt, if needed
				movlb	0x01				;PIE1 is in bank 1
				btfss	PIE1,TXIE			;is the transmit interrupt enabled?
				retfie						;done if not

				movlb	0x00				;PIR1 is in bank 0
				btfss	PIR1,TXIF			;test for pending UART tx interrupt
				retfie						;all done if no tx interrupt

;fall into txi_service, retfie from there

;********************************************************************************
;* Transmit Interrupt Routine
;* Transmit UART character from transmit queue
;* If this empties the transmit queue, then turn TX interrupt off 
;* On Entry:
;*     BSR = 0
;*     FSR0H = high(RX_QUEUE) (see 'if' below)
;*     The queue is NOT empty
;*     Transmit is NOT blocked by XOFF
;*     The next chr to transmit is at TQ_OPTR
;* On Exit:
;*     if Tx queue is empty, then the transmit int is disabled via TXIE (in PIE1)
;*     and TQ_EMPTY is set
;********************************************************************************
; point FSR0 to transmit queue
txi_service:	movfw	TQ_OPTR
				movwf	FSR0L

	if	high(RX_QUEUE) ^ high(TX_QUEUE)		;in case somebody changed the queue location!
				movlw	high(TX_QUEUE)
				movwf	FSR0H
	endif

				movfw	INDF0				;get queue chr

				movlb	0x03				;TXREG is in in bank 3
				movwf	TXREG				;transmit now

;circular-increment queue out-pointer, assuming aligned queue
				inc_tq	TQ_OPTR

;If out-pointer now equals in-pointer, then the queue has become empty.
;If so, turn off transmit interrupt
				movfw	TQ_IPTR
				xorwf	TQ_OPTR,W
				btfss	STATUS,Z
				retfie

				bsf		INT_FLAGS,TQ_EMPTY	;flag that the queue is empty
				call	mask_txint			;mask the transmit interrupt
				retfie

;********************************************************************************
;* Receive Interrupt Subroutine
;*  Note: This is a subroutine that gets called only by the interrupt handler
;*  Note: we ignore framing and overrun errors.
;* On Entry:
;*     BSR = 0
;*     FSR0H = high(RX_QUEUE)
;*     Character is waiting in the UART receiver
;*     RQ_FULL is set if the Rx queue is already full.
;*     The next available slot in the queue is at RQ_IPTR
;*     XOFF_STATE is set if transmission has already been stopped by a previously
;*      received XOFF character.
;*     PREV_CR is set if the previously received character was a CR
;*     PREV_LF is set if the previously received character was a LF
;*
;* On Exit:
;*     BSR trashed
;*     FSR0H = high(TX_QUEUE) = high(RX_QUEUE)
;*     Chr received from the UART, parity stripped, nulls ignored
;*     Chr is put into rx queue, if there is room (RQ_FULL = 0).
;*     If the queue is already full (RQ_FULL = 1), then the chr is discarded.
;*     RQ_FULL gets set if the receive queue has just become full.
;*     A LF that immediately follows a CR gets ignored. Other LFs get translated
;*      into CRs.
;*     A CR that immediately follows a LF gets ignored.
;*     PREV_CR and PREV_LF are used to convert CR-LF, LF-CR, and LF into just CR 
;*     XOFF_STATE set & transmit interrupt disabled if TX should stop due to XOFF
;*     XOFF_STATE cleared if it had been set and we received any non-null chr
;********************************************************************************
;point FSR0 to next empty queue space
rxi_service:	movfw	RQ_IPTR
				movwf	FSR0L

;Get received chr into W, strip parity, skip nulls
				movlb	0x3					;RCREG is in in bank 3
				movfw	RCREG				;get received data from UART
				andlw	0x7f				;strip parity, test for null
				btfsc	STATUS,Z			;test for null
				return						;just ignore nulls

;see if transmit is blocked due to XOFF. If so, any received chr (except a null)
;will XON us, and we eat that character
				btfsc	INT_FLAGS,XOFF_STATE
				bra		rxi_do_xon

;put received chr in queue. If it shouldn't go in the queue, we just won't
;bump the queue pointer later.
				movwf	INDF0				;save received chr in queue

;If this is an XOFF character, then disable transmit without bumping RQ_IPTR
				xorlw	XOFF				;is this an xoff?
				btfsc	STATUS,Z
				bra		rxi_do_xoff			;if so, don't bump queue pointer

;Test for CR  which gets special treatment below
				xorlw	XOFF ^ CR			;Z set if chr = CR
				btfsc	STATUS,Z
				bra		rxi_cr				;deal with CR

;Test for LF  which gets special treatment below
				xorlw	CR ^ LF				;Did we just get a LF?
				btfsc	STATUS,Z
				bra		rxi_lf				;deal with LF

;Neither CR or LF -  cancel any CRLF sequence progress
				bcf		INT_FLAGS,PREV_CR
				bcf		INT_FLAGS,PREV_LF

;The character is already in place in the queue. Actually enqueue the character
;now by bumping the in-pointer - unless the queue is full
rxi_enqueue		btfsc	INT_FLAGS,RQ_FULL	;full queue?
				return						;full: drop chr on floor

				inc_rq	RQ_IPTR				;curcular-increment queue pointer

;Set RQ_FULL if we just became full
				movfw	RQ_IPTR
				xorwf	RQ_OPTR,W			;does RQ_IPTR = RQ_OPTR?
				btfsc	STATUS,Z
				bsf		INT_FLAGS,RQ_FULL	;note full queue
				return

;------------------------------------------------------------------------------
; Translate CR-LF into CR. This requires looking at the state from the previous
; chr, and remembering the state of this chr for next time.
;------------------------------------------------------------------------------
rxi_cr:			btfsc	INT_FLAGS,PREV_LF	;LF following a CR?
				bra		rxi_crlfdone		;yes: end of sequence

				bsf		INT_FLAGS,PREV_CR	;remember that we got a fresh CR
				bra		rxi_enqueue			;go bump queue pointer to enqueue

;------------------------------------------------------------------------------
; Translate LF-CR into CR. This requires looking at the state from the previous
; chr, and remembering the state of this chr for next time.
;------------------------------------------------------------------------------
rxi_lf:			btfsc	INT_FLAGS,PREV_CR	;LF following a CR?
				bra		rxi_crlfdone		;yes:deal with sequence

				movlw	CR					;translate LF into CR
				movwf	INDF0

				bsf		INT_FLAGS,PREV_LF	;remember that we got a LF
				bra		rxi_enqueue			;go bump queue pointer to enqueue

;------------------------------------------------------------------------------
; We've just completed a CR-LF or LF-CR sequence, deliberately not enqueing
; the second chr of one of these sequences. Now, reset state.
;------------------------------------------------------------------------------
rxi_crlfdone:	bcf		INT_FLAGS,PREV_CR	;end of sequence
				bcf		INT_FLAGS,PREV_LF
				return						;don't bump queue pointer
				
;------------------------------------------------------------------------------
; Disable transmit because of XOFF received
;------------------------------------------------------------------------------
rxi_do_xoff:	bsf		INT_FLAGS,XOFF_STATE	;set XOFF flag

;fall into mask_txint

;------------------------------------------------------------------------------
; Local subroutine to mask the transmit interrupt
; sets BSR to 1 too
;------------------------------------------------------------------------------
mask_txint:		movlb	0x01					;PIE1 is in bank 1
				bcf		PIE1,TXIE				;mask tx interrupt
			
				return

;------------------------------------------------------------------------------
; Clear XOFF state, and re-enable transmit interrupt unless queue is empty
;------------------------------------------------------------------------------
rxi_do_xon:		bcf		INT_FLAGS,XOFF_STATE	;clear XOFF flag

;if the queue is not empty, return with transmit interrupt enabled
				movlb	0x01					;PIE1 is in bank 1
				btfss	INT_FLAGS,TQ_EMPTY
				bsf		PIE1,TXIE				;not empty: unmask tx interrupt

				return

	page
;################################################################################
;# Subroutines
;################################################################################
;*==============================================================================*
; Subroutines to start the loader
; On Exit:
;     ERRCNT_HIGH, ERRCNT_LOW = 0000
;     RECCNT_HIGH, RECCNT_LOW = 0000
;     FLASH_MEM = 0
;     msb of ROWA_HIGH = 1, indicating the previous row wasn't FLASH data
;     FAR1H = high(RAM_BUF)
;     BSR = 0x03
;     R0 = last printed character of loader_msg
;     R1, FSR0, PMDATL, PMDATH, PMADRL, PMADRH trashed
;*==============================================================================*
start_loader:	movlb	0x03				;bank 3 has the error counter			
				clrf	ERRCNT_LOW			;no errors yet
				clrf	ERRCNT_HIGH

				clrf	RECCNT_LOW			;RECCNT_LOW,HIGH counts hex records
				clrf	RECCNT_HIGH			;...actually loaded into FLASH

				bcf		INT_FLAGS,FLASH_MEM	;not in FLASH space yet

				bsf		ROWA_HIGH,7			;previous record wasn't a FLASH data record

				movlw	high(RAM_BUF)		;set up FSR1 as data pointer (never changes)
				movwf	FSR1H				;data gets stored in RAM buffer

				movlw	low(loader_msg)		; print sign-on message
				goto	loader_printf		;leaves BSR=3, return from there

;*==============================================================================*
; Subroutines to report load errors
; On entry at report_error:
;     W = low byte of message address
; On Exit:
;     W = 0
;     BSR = 0x03
;     R0, R1, FSR0, PMDATL, PMDATH, PMADRL, PMADRH trashed
;*==============================================================================*
hex_error:		movlw	low(hexerror_msg)	;hex character error

report_error:	movlb	0x03				;bank 3 has the error counter
				incf	ERRCNT_LOW,F
				btfsc	STATUS,Z
				incf	ERRCNT_HIGH,F

; fall into loader_printf, with message address in W

;*==============================================================================*
; Subroutine to print brief kernel message at W
; On Entry:
;     W = low byte of string address
; On Exit:
;     W = 0
;     R0 = last printed character
;     BSR = 0x03
;     R1, FSR0, PMDATL, PMDATH, PMADRL, PMADRH trashed
;*==============================================================================*
loader_printf:	movlb	0x03				;memory control registers are in bank 3

				movwf	PMADRL
				movlw	high(loader_msg)	;same for all messages
				movwf	PMADRH
					
;	fall into printf

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to print null-terminated strings
;
; String chrs are packed 2 to a 14-bit word, as the PIC assembler likes to do.
; (One of the bytes might be 0 because the assembler pads that way. These 0's
; are ignored. The terminating null will have both bytes of the 14-bit word 0.)
; All CR's get translated to CR-LF by printw. 
; The receive interrupt is masked while the string is enqueued, so it goes in
; without echos interspersed. This makes error messages during hex loading
; look pretty. Note that long strings may cause us to miss rx characters.
; On Entry
;     PMADRH, PMADRL = address of string
;     BSR = 0x03
; On Exit:
;     W = 0
;     R0 = last printed character
;     BSR = 0x03
;     R1, FSR0, PMDATL, PMDATH, PMADRL, PMADRH trashed
;     Receive interrupt (RCIE) is enabled
;*==============================================================================*
printf:		bcf		PMCON1,CFGS			;select program memory

;print loop - terminates on null word
;get 2 characters that are packed into a 14-bit program-memory word
printf_loop:	bsf		PMCON1,RD			;initiate 14-bit word read
				nop							;(page 98) mandatory 2 nops
				nop

;Check for terminating double-null
				movfw	PMDATH
				iorwf	PMDATL,W
				btfsc	STATUS,Z			;2 bytes of null termination?
			return							;yes:done, with BSR=3

;unpack result into two characters, and print them.
				lslf	PMDATL,W			;put msb into carry
				movwf	R1					;shifted result into R1
				rlf		PMDATH,W			;carry goes with first byte
				call	printw				;print first chr from R0
				lsrf	R1,W				;finish unpacking second byte
				call	printw				;(trashes BSR)

;bump pointer to next string character pair
				movlb	0x03				;memory control registers are in bank 3
				incf	PMADRL,F			;bump string pointer
				btfsc	STATUS,Z			;roll into next page?			
				incf	PMADRH,F
				bra		printf_loop

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to print W as a 2-character hex value
; On Entry:
;     byte to send is in W
; On Exit:
;     W, R0, R1, FSR0, BSR trashed
;*==============================================================================*
print_hex2:		movwf	R1					;temp save low nibble
				swapf	R1,W				;high byte in low nibble of W
				call	print_hex1			;send the high nibble

				movfw	R1					;now the low nibble

;fall into print_hex1

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to print one hex digit from the low nibble of W
; On Entry:
;     W low nibble = value to transmit
; On Exit:
;     W, FSR0, BSR trashed
;     R0 = chr
;*==============================================================================*
;tricky binary to ascii hex conversion
print_hex1:		andlw	0x0f					;W=just the low nibble
				sublw	0x09					;W=9-nib. is value greater than 9?
				btfsc	STATUS,C				;C set means <=9			
				addlw	'A'-'9'	- 1				;W=gap+9-nib
				sublw	'0' + 'A'-'9' + 0x08	;W='0'+gap+9(-gap)-9+nib
												;W='0'(+gap)+nib
;fall into printw to enqueue character in W

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to print W
;    Nulls get eaten (the assembler may pack some nulls into strings)
;    Carriage returns (CR) get translated into CR-LF
;    If W = XOFF or W = XON, then cancel any XOFF state on our transmitter side
; On Entry at printW:
;     W = chr to transmit
; On Entry at printr0 (not externally accessable):
;     R0 = chr to transmit
; On Exit:
;     W, FSR0, BSR trashed
;     R0 = printed chr, except for CR - which returns with LF
;*==============================================================================*
printw:			movwf	R0					;save chr in R0

printr0:		movlw	high(TX_QUEUE)		;point FSR0 to transmit queue
				movwf	FSR0H

; See if tx queue is full, and stall if it is. Also stall of XOFF'd
; Queue is full if TQ_IPTR = TQ_OPTR and TXIE (in PIE1) is set (enabled)			
pw_wait_txq:		movfw	TQ_IPTR				;now check for full queue
					movwf	FSR0L				;and set up queue pointer low byte
					xorwf	TQ_OPTR,W			;Z set for empty or completely full

					btfss	STATUS,Z			;TQ_IPTR = TQ_OPTR?
					bra		pw_room				;no - then not full!

					btfss	INT_FLAGS,TQ_EMPTY	;in-pointer = out-pointer. Empty?
					bra		pw_wait_txq			;no: the queue is full
pw_room:

; put the character into the queue, unless the character's a null
				movfw	R0						;recover chr to print
				btfsc	STATUS,Z				;eat nulls
				return

				movwf	INDF0					;put chr in the queue

;mask all interrupts while we mess with the queue pointer and flags
				bcf		INTCON,GIE

;if we are sending an XOFF or XON character, and if we are XOFF'd from transmitting,
;then undo XOFF state so that this XOFF or XON will actually get sent.
				xorlw	XOFF					;set Z if XOFF
				btfss	STATUS,Z
				xorlw	XOFF ^ XON				;set Z if XON
				btfsc	STATUS,Z
				bcf		INT_FLAGS,XOFF_STATE	;undo any transmit-side XOFF

;circular-increment the queue pointer
				inc_tq	TQ_IPTR

				bcf		INT_FLAGS,TQ_EMPTY		;tell txint the queue isn't empty

;Enable the transmit interrupt unless we are XOFF'd
				movlb	0x01					;PIE1 is in bank 1
				btfss	INT_FLAGS,XOFF_STATE	;don't enable ints if XOFF'd
				bsf		PIE1,TXIE				;enable transmit interrupt

;re-enable global interrupts
				bsf		INTCON,GIE

;If this chr was a CR, then send a LF too.
				movfw	R0					;recover the printed character
				xorlw	CR
				btfss	STATUS,Z
				return						;no CR: don't need an LF

				movlw	LF			
				bra		printw				;go enqueue LF

;*==============================================================================*
; Subroutine to get an exactly 2-character hex value from the Rx queue
; This is as fast as possible so we can keep up with hex record loading
; This routine *IS* cases sensitive - lowercase will cause an error.
; This routine also does not skip over white space.
;
; Note: this is not an externally-acessible subroutine because the programmer
; firmware must load received data through a line buffer, so that line editing
; (at least deleting characters) can be supported.
; On Exit:
;     Result in W and R1
;     Result added to HEX_CKSUM
;     Z set if checksum = 0
;     HERR_FLAG set if any hex character errors. (Never cleared though)
;     R0, FSR0 trashed
;*==============================================================================*
get_hex2:		call	get_hex1			;get high nibble
				btfsc	STATUS,C			;C set means error
				bsf		INT_FLAGS,HERR_FLAG

				call	get_hex1			;get low nibble
				btfsc	STATUS,C			;C cleared means success
				bsf		INT_FLAGS,HERR_FLAG

				addwf	HEX_CKSUM,F			;add to checksum, set Z appropriately
				return

;*==============================================================================*
; Subroutine to get one character from the Rx queue, convert it to a binary
; nibble and test for good hex. This routine *IS* case-sensitive.
;
; Note: this is not an externally-acessible subroutine because the programmer
; firmware must load received data through a line buffer, so that line editing
; (at least deleting characters) can be supported.
; On Entry:
;     R1 = previous nibble(s)
; On Exit:
;     R0 = binary value of nibble
;     W = R1 = combined nibbles (upper nibble of R1 entry value is lost.)
;     C cleared if no error, set if bad hex character
;     FSR0 trashed
;*==============================================================================*
get_hex1:		call	wait_rxchr		;get chr into R0, case sensitive

				;fall into hex2bin

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to convert hex chr in  R0 to a binary, testing for a good
; hex digit. Combine result into previous digit in R1.
; This routine *IS* case-sensitive.
; On Entry: 
;     R0 is ASCII chr for this nibble
;     R1 = previous nibble(s)
; On Exit:
;     R0 = binary value of nibble
;     W = R1 = combined nibbles (upper nibble of R1 entry value is lost.)
;     C cleared if no error, set if bad hex character
;     both nibbles will be trashed if a bad hex chr is received.
; (5 uS, worst case)
;*==============================================================================*
hex2bin:		movlw	'0'				;(0.25)remove ASCII bias
				subwf	R0,F			;(0.25)result into R0

;test for chr between '0' and '9'
				movlw	0x0a			;(0.25)
				subwf	R0,W			;(0.25)
				btfss	STATUS,C		;(0.50)C cleared means '0'-'9'
				bra		good_nibble		;binary value in R0

;simultaneously check for gap chr between '9' and 'A' and for chr above 'F'
				movlw	'A'-'0'			;(0.25)shift down to zero base
				subwf	R0,F			;(0.25)legal values are now 0x00 through 0x05
				movlw	0x06			;(0.25)
				subwf	R0,W			;(0.25)C set means value is 0x06 or more

				btfsc	STATUS,C		;(0.50)C set means gap chr or above F, an error
				return					;Bad hex chr: Return with carry set
				
				movlw	0x0a			;(0.25)convert 0x00 - 0x05 into 0x0A - 0x0F
				addwf	R0,F			;(0.25)binary value in R0 (leaves C cleared)

;install new R0 value as the low nibble of R1, shifting old value to the high nibble
good_nibble:	swapf	R1,W			;(0.25)push previous low nibble into high position
				andlw	0xf0			;(0.25)strip off any previous high nibble
				iorwf	R0,W			;(0.25)combine with new low nibble
				movwf	R1				;(0.25)result back into R1 for return

				return					;(0.50)Success: value in R0 and carry cleared		

;*==============================================================================*
; Subroutine to wait for and get get one character from the Receive Queue
; echo the character upon receipt (except nulls)
; On Exit:
;     The character is in R0. If chr was CR, then R0 = LF
;     FSR0, W trashed
;     GEI is set, enabling global interrupts
;*==============================================================================*
wait_rxchr:			call	get_rxchr
					btfsc	STATUS,Z
					bra		wait_rxchr

				goto	printr0				;echo and return

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to get one character from the Receive Queue
; (does not echo)
; On Exit:
;     Z set, W & R0 =0 if the queue is empty
;     otherwise, the character is in R0 and W
;     FSR0 trashed
;     GEI is set, enabling global interrupts
;*==============================================================================*
get_rxchr:		movfw	RQ_OPTR				;(.25) check for empty queue
				movwf	FSR0L				;(0.25) FSR0 will point to Rx queue
				xorwf	RQ_IPTR,W			;(.25)
				btfss	STATUS,Z			;(.50)
				bra		gr_notempty			;queue isn't empty if INPTR <> OPTR

				btfss	INT_FLAGS,RQ_FULL	;(.50)queue is not empty if it is full
				bra		gr_done				;(.25)with Z set, W=0: nothing in the queue
gr_notempty:

;Finish pointing FSR0 at the next queue character
				movlw	high(RX_QUEUE)		;finish pointing FSR0 to receive queue 
				movwf	FSR0H

;circular-increment the Rx queue out-pointer, mask interrupts while we do
				bcf		INTCON,GIE			;mask all interrupts

				inc_rq	RQ_OPTR				;circular-increment out pointer (not FSR1...)
				bcf		INT_FLAGS,RQ_FULL	;queue is no longer full

;Get next chr from the Rx queue at FSR0
				movfw	INDF0				;get the queue chr, clear Z
											;(There are no nulls in the queue)

				bsf		INTCON,GIE			;unmask interrupts

gr_done:		movwf	R0					;return chr or 0 in R0 and W, Z cleared/set
				return

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to stall for about W * 25 mS
; On Exit:
;      R0,R1,W cleared
;*==============================================================================*
stall_25ms:		movwf	R1
s25_loop:			movlw	.100
					call	stall_250us
					decfsz	R1,F
					bra		s25_loop
				return

;************************************===========================================*
;* Externally Accessible Subroutine *
;************************************
; Subroutine to stall for about 250 * W uS
; On Exit:
;      R0,R1,W cleared
;*==============================================================================*
stall_250us:	movwf	R0				;(0.25)

s250_loop:			movlw	.248		;(0.25) (250.25 inner loop)
					call	stall_1us	;(249.00)
					decfsz	R0,F		;(0.25)
					bra		s250_loop	;0.50)
				return

;*==============================================================================*
; Subroutine to read and compare one row of FLASH data into the RAM buffer
; (Pages 98-99, 107)
; This is a dual-purpose subroutine, to save code space:
;   1) Used to copy FLASH into the buffer - in which case, Z is ignored on return
;   2) Used to verify that FLASH matches the buffer, in which case, the buffer
;      gets over-written, precluding any possibility of retrying the write.
; On Entry:
;    ROWA_HIGH, ROWA_LOW contain the FLASH address * 2.
;    FSR1H contains the high byte of the RAM buffer address
; On Exit:
;    Data from FLASH has been written to the RAM buffer
;    W = low(verifyerr_msg)
;    Z set if the entire buffer matched the data before it was copied
;    R0, FSR1L, PMADRH, PMADRL, PMDATH, PMDATL all trashed
;*==============================================================================*
read_ver_flash:	call	flash_setup		;set up PMADRH, PMADRL, FSR1L, BSR, CFGS
				clrf	R0				;no mismatches yet

rv_loop:			bsf		PMCON1,RD		;initiate 14-bit word read
					nop						;(page 98) mandatory 2 nops
					nop

;compare the flash to the buffer, and make R0 non-zero if not the same
;then put the flash data in the buffer
					movfw	PMDATL			;test low byte of data first
					xorwf	INDF1,W			;compare to buffer memory
					iorwf	R0,F			;result accumulates in R0
					movfw	PMDATL			;now copy the low byte
					movwi	FSR1++			;save to buffer, bump pointer

					movfw	PMDATH			;test high byte
					xorwf	INDF1,W			;compare to buffer memory
					iorwf	R0,F			;result accumulates in R0
					movfw	PMDATH			;now copy the high byte
					movwi	FSR1++			;save to buffer, bump pointer

					incf	PMADRL,F		;next address in this FLASH row

					btfss	FSR1L,6			;done after 64 bytes read from buffer
					bra		rv_loop

				movf	R0,F				;set Z if the buffer matched the data
				retlw	low(verifyerr_msg)	;point to verify error message, just in case

;*==============================================================================*
; Subroutine to write buffer data to one FLASH row (Pages 100-106)
; On Entry:
;    Data is in RAM buffer
;    ROWA_HIGH, ROWA_LOW = FLASH address * 2
;    FSR1H contains the high byte of the data buffer address
; On Exit:
;    Flash has been written
;    Error message printed and HERR_FLAG set if verify fails
;    W, R0, R1, FSR1L trashed
;*==============================================================================*
write_flash:

;--------------------------------------------------------------------------------------
; Writing to FLASH is slow, and interrupts must be masked during the process - so we
; must tell the sender to pause (via XOFF) while we write to FLASH. The sender may not
; respond to the XOFF immediately, so we will stall here until we don't see any more
; characters coming into the receive queue from the UART. Once the UART receiver is
; quiet, we mask all interrupts. Note that we won't actually pull any characters from
; the receive queue here: we just look at the queue pointer to see if it is changing.
;--------------------------------------------------------------------------------------
				movlw	XOFF				;pause receiver
				call	printw				;trashes W, R0, FSR0

;wait until we get no received characters for at least 3 character times = 3.1 mS
wait_xoff:			movfw	RQ_IPTR			;snapshot of Rx queue in-pointer
					movwf	R1				;temp storage

					movlw	.13				;13 X 250 uS = 3.25 mS stall
					call	stall_250us		;trashes R0

					movfw	R1				;any new characters?
					xorwf	RQ_IPTR,W		;RX_IPTR changes with each received chr
					btfss	STATUS,Z
					bra		wait_xoff

				bcf		INTCON,GIE			;Disable ints so required sequences will execute properly

;--------------------------------------------------------------------------------------
; First, erase the FLASH row at address ROWA_HIGH, ROWA_LOW. This takes about 2 mS
; (page 102)
;--------------------------------------------------------------------------------------
				call	flash_setup			;set up PMADRH, PMADRL, FSR1L, BSR, CFGS

				bsf		PMCON1,FREE			;specify erase operation
				bsf		PMCON1,WREN			;enable writes

				movlw	0x55				;unlock sequence
				movwf	PMCON2
				movlw	0xAA
				movwf	PMCON2
				bsf		PMCON1,WR			;Set WR bit to begin erase
				nop							;NOP instructions are forced as processor starts
				nop							;row erase of program memory.
											;The processor stalls until the erase process is complete
											;after erase processor continues with 3rd instruction

				bcf		PMCON1,WREN			;disable writes

;--------------------------------------------------------------------------------------
; Next, write the FLASH row with data from RAM_BUF. This takes about 2 mS (page 106)
; BSR & PMADR are already set up; interrupts are already masked
;--------------------------------------------------------------------------------------
				bcf		PMCON1,CFGS			;Not configuration space
				bsf		PMCON1,WREN			;enable writes
				bsf		PMCON1,LWLO			;Only Load Write Latches

fwrite_loop:		moviw	FSR1++			;Load first data byte into lower
					movwf	PMDATL
					moviw	FSR1++			;Load second data byte into upper
					movwf	PMDATH

					btfsc	FSR1L,6			;all but the last byte written?
					bra		start_fwrite

					movlw	0x55			;unlock sequence
					movwf	PMCON2
					movlw	0xAA
					movwf	PMCON2
					bsf		PMCON1,WR		;Set WR bit to begin write
					nop						;NOP instructions are forced as processor
					nop						;loads program memory write latches

					incf	PMADRL,F		;Still loading latches. Increment address
					bra		fwrite_loop

start_fwrite:	bcf	PMCON1,LWLO				;No more loading latches. Actually start FLASH program

				movlw	0x55				;unlock sequence
				movwf	PMCON2
				movlw	0xAA
				movwf	PMCON2
				bsf		PMCON1,WR			;Set WR bit to begin erase
				nop							;NOP instructions are forced as processor writes
				nop							;all the program memory write latches simultaneously
											;to program memory. After NOPs, the processor
											;stalls until the self-write process in complete.

				bcf		PMCON1,WREN			;disable writes

;--------------------------------------------------------------------------------------
; Finally, verify written data by comparing FLASH to the RAM buffer
; (This trashes the RAM buffer data.) 
;--------------------------------------------------------------------------------------
				call	read_ver_flash		;verify the data, trash the buffer
											;returns with Z set if match, W = low(verifyerr_msg)

				bsf		INTCON,GIE			;re-enable interrupts here, in case we report error

				btfss	STATUS,Z
				call	report_error		;W = low(verifyerr_msg)

;--------------------------------------------------------------------------------------
; Critical timing is done. send XON to the sender, and return.
;--------------------------------------------------------------------------------------
				movlw	XON					;un-pause receiver
				goto	printw				;return from there

;*==============================================================================*
; Subroutine to set up FSRL, PMADRH & PMADRL for reads and writes to FLASH
; On Entry:
;    ROWA_HIGH, ROWA_LOW = flash row address * 2
; On Exit:
;    FSR1L = 0
;    BSR = 0x03
;    CFGS bit cleared
;    PMADRH,PMADRL = (ROWA_HIGH, ROWA_LOW) / 2
;*==============================================================================*
flash_setup:	clrf	FSR1L				;beginning of RAM buffer

				movlb	0x03				;memory control registers are in bank 3
				bcf		PMCON1,CFGS			;Not configuration space

				lsrf	ROWA_HIGH,W			;high byte of address, divided by 2
				movwf	PMADRH
				rrf		ROWA_LOW,W			;low byte of address, divided by 2
				movwf	PMADRL

				return

;*==============================================================================*
; Subroutine to test for the user typing three sequential ESCAPEs to go to
; LOADED_CODE. If FLASH_MEM is set, meaning that we are actively loading a file,
; then ESCAPEs will be ignored.
; On Entry:
;     FLASH_MEM is set if we are actively loading data into FLASH.
; On Exit (not to LOADED_CODE):
;     R0 = chr received. If chr was CR, then R0 = LF
;     W, R1, FSR0 trashed
;*==============================================================================*
test_esc:	movlw	3					;loop counter: count ESCs
			movwf	R1

wait_escapes:	call	wait_rxchr			;get user input into R0

				btfsc	INT_FLAGS,FLASH_MEM	;active write to flash memory?
			return						;yes: ESC sequences will be ignored

				movfw	R0
				xorlw	ESC					;another ESC?
				btfss	STATUS,Z
			return						;any non-ESC end the hunt

				decfsz	R1,F
				bra		wait_escapes

			goto	LOADED_CODE			;complete ESC sequence: go to LOADED_CODE


;*==============================================================================*
; Print an error message in the listing if this is code larger than LOADED_CODE
;*==============================================================================*
loader_end:

	if	loader_end > LOADED_CODE
	error	"Code is too large!"
	endif

	end
