// Bootloader for Microchip PIC High-Range Microcontrollers written in CCS PIC-C.
// Copyright Gary Smithson theByteFactory.com - Visit again soon!
// Distribute freely with copyright notice intact.

// Software Prototyping Board set-up:
//	1) Install a jumper from prototype strip position 7 to 12.
//	2) Install a jumper from prototype strip position 8 to 11.
//	4) Plug the RS232 serial cable into jack A.
//	5) Install a PICmicro programmed with CodeLoader into the appropriate ZIF socket.

// Instructions:
//	CodeLoader works with your favorite terminal emulation program. In addition to
// the usual settings of 19200-8-N-1, there are two other very important settings;
// XON/XOFF handshaking and Line Delay, which should be set to 10 milliseconds.
//	When the Software Prototyping Board powers-up or is reset, it checks for a
// valid downloaded program. If a previous download was successful then the user
// program is executed. If there is no valid user program then CodeLoader prompts
// for download. The download prompt may also be invoked at any time by pressing
// and holding the "=" key on the PC during a Software Prototyping Board reset.
//	At the download prompt, simply transmit your programs HEX output file to the
// Software Prototyping Board as an ASCII text file (HyperTerminal menu Transfer |
// Send Text File...). The user program will begin automatically if the download
// is successful.
//
// Complete details and updates can be found at theByteFactory.com

//	Other than it's obvious purpose CodeLoader is also a "Business Card" for
// theByteFactory programming services. It demonstrates our innovation through
// experience, techniques of achieving functionality, comprehensiveness,
// thoroughness, and readable coding style.

/////////////// Chip and Configuration ////////////////
#include <18F452.h>				// Select the development device
#define	XTAL_HZ		20000000	// Select the crystal frequency in Hertz
#define	TRIGGER_KEY	'='			// NOTE: Do not change this to any character that may naturally occur in a HEX output file
// User may need to adjust certain fuse settings but should leave NOPROTECT,NOCPB,NOCPD,NOWRT,NOWRTB,NOWRTD,NOEBTR,NOEBTRB as they are!
#fuses HS,NOWDT,PUT,NOBROWNOUT,NOLVP,NOOSCSEN,WRTC,NOPROTECT,NOCPB,NOCPD,NOWRT,NOWRTB,NOWRTD,NOEBTR,NOEBTRB
// Fuses are effectively just suggested programmer settings

///////////////////////////////////////////////////////
#id checksum					// The default part ID will be the program memory checksum
#case							// Make the compiler case sensitive (as most are)
typedef unsigned int8	U8BIT;	// Unsigned 8 bit variables
typedef signed int8		S8BIT;	// Signed 8 bit variables
typedef unsigned int16	U16BIT;	// Unsigned 16 bit variables
typedef signed int16	S16BIT;	// Signed 16 bit variables
typedef unsigned int32	U32BIT;	// Unsigned 32 bit variables
typedef signed int32	S32BIT;	// Signed 32 bit variables

// The loader will protect itself from firmware being downloaded on top of it
// The MAX_LOADABLE values below are set loosely so that a wide variety of compiler versions will
// compile this source code without "Out of ROM" errors. To maximize the available program space for
// user applications, MAX_LOADABLE should be adjusted to as large as your compiler version will allow.
// The MAX_LOADABLE value should be an even address.
#if getenv("FUSE_SET:WDT")==TRUE
#define MAX_LOADABLE	0x6800	// The watch-dog is enabled and will be reported to (requires more program memory)
#else
#define MAX_LOADABLE	0x6820	// The watch-dog is disabled allowing for smaller loader size
#endif

#org 0x0008, MAX_LOADABLE {}	// Consume program memory to force the loader to reside in the highest space
								// CodeLoader will reside from MAX_LOADABLE + 2 to the end of program memory

/////////////////////// Options ///////////////////////
// The loader will require less program memory if the watch-dog is disabled
// If the user program requires the watch-dog timer then the loader must also report to it
#if getenv("FUSE_SET:WDT")==TRUE
#use delay (clock=XTAL_HZ, RESTART_WDT)							// Define crystal in use/Report to the watch-dog
#use rs232 (baud=19200, xmit=PIN_C6, rcv=PIN_C7, RESTART_WDT)	// Use the hardware USART/Report to the watch-dog
#else
#use delay (clock=XTAL_HZ)										// Define crystal in use
#use rs232 (baud=19200, xmit=PIN_C6, rcv=PIN_C7)				// Use the hardware USART
#endif

////////////////////// Constants //////////////////////
#define VECTOR_VALID	4		// Offset to the valid/invalid vector flag stored with the reset vector
#define BUFFER_LENGTH	64		// Size of ASCII character buffer for holding each line of the downloaded file
#define XON				0x11	// ASCII DC1
#define XOFF			0x13	// ASCII DC3
#define CR				0x0D	// ASCII Carriage Return

////////////////////// Prototypes /////////////////////
void	main (void);
#inline		// The next function is called from only one place and will be inline to save program memory
BOOLEAN	load_firmware (void);	// This routine will receive the ASCII line,parse,error check, and write program memory
#separate	// The next function is called from many places and will be a function to save program memory
U8BIT	ascii_to_u8bit (char *string);
#separate	// The next function is called from many places and will be a function to save program memory
BOOLEAN	kind_write_verify_program_eeprom (U32BIT address, U16BIT data);	// Kindly does not write data that is already there
#inline		// The next function is called from only one place and will be inline to save program memory
BOOLEAN	kind_write_verify_data_eeprom (U8BIT address, U8BIT data);	// Kindly does not write data that is already there

////////////////// Global Variables ///////////////////
U8BIT	buffer_index;			// Used with ASCII character buffer
char	buffer[BUFFER_LENGTH];	// ASCII character buffer for holding each line of the downloaded file
U16BIT	temp_reset_vector[2];	// The reset vector is held here until the download successfully completes
U32BIT	reset_vector_location;	// Self-modifying code support - Downloaded reset vector is written at this address
BOOLEAN	overall_success;		// The overall firmware download status
U8BIT	main_temp_U8;			// This would normally be a local variable in main (See comment in main)
char	main_key;				// If the trigger key is received during reset then the download prompt will be presented
U16BIT	bytes_written = 0;		// Counts the number of program bytes written for space used/remaining reporting

////////////////////// Functions //////////////////////
void main (void)
{
	// The declaration of local variables in main is being avoided in an effort to keep the stack clear
	// PIC-C does not build a traditional stack but the comment is made to demonstrate programming style

	// Self-modifying code support - Get the address where the downloaded reset vector will be written
	reset_vector_location = label_address (Downloaded_Reset_Vector);

	main_key = 0;				// Assume that a download is not being requested
	// 50 * 2mS of delay = 100mS window for the trigger key to be received
	for (main_temp_U8 = 0; ((main_temp_U8 < 50) && (main_key != TRIGGER_KEY)); main_temp_U8++) {
		if (kbhit ()) {			// There is a character available
			main_key = getc ();	// Get the character
		}
		delay_ms (2);			// Allow time for more streaming characters to enter the buffer
	}							// Either the trigger window has closed or the trigger key has been received

	// The loader will be invoked if the "force loader" trigger was active during reset or...
	// ...the last firmware download failed. Otherwise, the downloaded application will start.
	if ((main_key != TRIGGER_KEY) && (read_program_eeprom (reset_vector_location + VECTOR_VALID) == TRUE)) {
		// Start the latest downloaded firmware by processing it's reset vector
		// Doing so from main () will insure that the call stack is empty
Downloaded_Reset_Vector:
		#asm
		NOP				// Self-modifying code - The downloaded reset vector will be written here on success
		NOP				// End of reset vector
		NOP				// Valid/Invalid vector flag (NOTE: Will never execute because the vector is a jump)
		#endasm
	}

	// Did not jump to the application - Start the loader
	// Previously failed downloads are possibly still streaming data - It will be consumed here
//	printf ("\r\n\nWaiting for idle...");
	printf ("\r\n\nWait");
	// 500mS of idle time with no incoming characters advances to the "Ready" state
	for (main_temp_U8 = 0; main_temp_U8 < 250; main_temp_U8++) {	// 250 * 2mS of delay = 500mS
		while (kbhit ()) {		// The download buffer still contains characters
			getc ();			// Get and discard them
			main_temp_U8 = 0;	// When a character is found, the 500mS idle timer starts over
		}
		delay_ms (2);			// Allow time for more streaming characters to enter the buffer
	}							// Characters are no longer streaming

//	printf ("\rReady for download...\r\n");
	printf ("\rLoad\r\n");

	overall_success = load_firmware ();

	// Download is complete - Modify the loader with the application reset vector
	// NOTE: If overall_success is FALSE from the firmware download then the vector write will not occur
	for (main_temp_U8 = 0; ((main_temp_U8 <= 2) && (overall_success == TRUE)); main_temp_U8 += 2) {
		overall_success = kind_write_verify_program_eeprom ((reset_vector_location + main_temp_U8), temp_reset_vector[(main_temp_U8 >> 1)]);
	}

	// Record the overall_success flag in preperation for chip reset
	kind_write_verify_program_eeprom ((reset_vector_location + VECTOR_VALID), overall_success);

	delay_ms (2);	// Allow time for possible error messages to be printed
	reset_cpu ();	// Either start the application or re-start the loader
}

#inline
BOOLEAN load_firmware (void)
{
	BOOLEAN	done;
	BOOLEAN	success;
	U8BIT	temp_U8;		// Multi-purpose temporary variable
	U8BIT	line_type;
	U8BIT	buffer_csum;	// The checksum of the buffer for the current line
	U8BIT	file_csum;		// The checksum from the file for the current line
	U16BIT	data;			// Program memory data
	U16BIT	address_low;	// The 16 low bits of the program memory address for the current data
	U16BIT	address_high;	// The 16 high bits of the program memory address for the current data
	U32BIT	address;		// The complete program memory address for the current data

	done = FALSE;
	success = TRUE;			// Assume that all will go well
	address_high = 0x0000;

	while ((done == FALSE) && (success == TRUE)) {	// While not done and still no errors
		buffer_index = 0;
		do {				// Gather characters until carriage return received or buffer is full
			buffer[buffer_index++] = getc();
		} while ((buffer[buffer_index - 1] != CR) && (buffer_index < BUFFER_LENGTH));	// A carriage return marks line end

		putc (XOFF);		// Ask the sender to stop while the line is being processed

		if (buffer[0] != ':') {		// The line should begin with a colon
//			printf ("\r\nSynchronizing \":\" not found");
			printf ("\r\nNo:");
			success = FALSE;
		} else {					// Leading ":" was in position
			buffer_csum = 0;		// Calculate the checksum of the buffer
			for (temp_U8 = 1; temp_U8 < (buffer_index - 3); temp_U8 += 2) {
				buffer_csum += ascii_to_u8bit (&buffer[temp_U8]);
			}
			buffer_csum = 0xFF - buffer_csum + 1;

			file_csum = ascii_to_u8bit (&buffer[buffer_index - 3]);	// Get the checksum of the line in the file

			if (buffer_csum != file_csum) {			// The checksums do not match
//				printf ("\r\nChecksum error");
				printf ("\r\nChksmErr");
				success = FALSE;
			} else {								// Good checksum
				line_type = ascii_to_u8bit (&buffer[7]);	// and 8

				if (line_type == 1) {				// Done with no errors
//					printf ("\r\nSuccess\r\n");
//					printf ("\r\nOk\r\n");
					// Bytes remaining = (bytes available to user - 4) - bytes written
					printf ("\r\nOk (%lu remaining)\r\n", ((MAX_LOADABLE - 1) << 1) - bytes_written);
					// % used = ((bytes written + vector bytes yet to be written) / bytes available to user) * 100
//					printf ("\r\nOk (%01.2f%% used)\r\n", ((float)(bytes_written + 4) / (float)((MAX_LOADABLE + 1) << 1)) * 100);
//					printf ("\r\nOk (%01.2f%% remaining)\r\n", 100 - (((float)(bytes_written + 4) / (float)((MAX_LOADABLE + 1) << 1)) * 100));
					done = TRUE;
				} else if (line_type == 4) {		// Establish address extension
					address_high = make16 (ascii_to_u8bit (&buffer[9]), ascii_to_u8bit (&buffer[11]));	// 10 and 12
				} else {							// Ending record not yet found
					address_low = make16 (ascii_to_u8bit (&buffer[3]), ascii_to_u8bit (&buffer[5]));	// 4 and 6

					address = make32 (address_high, address_low);
					printf ("\r%8LX", address);		// Print the base address (prints on top of the previous address)

					if ((address >= 0x00F00000) && (address <= 0x00F000FF)) {	// Address is in the Data EEPROM area
						// 2 characters represent one unit of data and the address increments by 1
						// The data starts at the 10th character (9 zero based)
						for (temp_U8 = 9; (temp_U8 < (buffer_index - 3)) && (success == TRUE); temp_U8 += 2) {
							// Build the 8 bit data from one 8 bit field
							data = ascii_to_u8bit (&buffer[temp_U8]);	// Initially 9 and 10

							// Write the data to it's designated EEPROM memory address (internal address range 00 to FF)
							// Use only the LSB of both the address and data
							success = kind_write_verify_data_eeprom ((U8BIT) address, (U8BIT) data);

							address++;	// Next address
						}
					} else {	// Address is in the Program Memory or Configuration Register area
						// 4 characters represent one unit of data and the address increments by 2
						// The data starts at the 10th character (9 zero based)
						for (temp_U8 = 9; (temp_U8 < (buffer_index - 3)) && (success == TRUE); temp_U8 += 4) {
							// Build the 16 bit data from two 8 bit fields
							data = make16 (ascii_to_u8bit (&buffer[temp_U8 + 2]), ascii_to_u8bit (&buffer[temp_U8]));	// Initially 11, 12, 9 and 10

							if (address <= 0x00000002) {			// Capture the downloaded reset vector
								temp_reset_vector[(address >> 1)] = data;
							} else if (address <= MAX_LOADABLE) {	// Address is in the valid firmware area
								// Write the data to it's designated program memory address
								success = kind_write_verify_program_eeprom (address, data);
							} else if (address <= 0x00007FFF) {		// Address is in the reserved loader area
//								printf (" Firmware overlaps Loader space!");
								printf (" Overlap");
								success = FALSE;	// The application could not be fully downloaded and is incomplete
							} else {
								// Configuration registers (0x00200000 to 0x003FFFFF) are not writeable - Ignore them
								// Addresses that are not within a valid category will also be ignored
								// Ignoring registers or addresses does not cause a failure
								// NOTE: Doing the two prints separately actually requires less code space
								printf ("\r%8LX", address);		// Report the exact address being skipped
//								printf (" Loader dose not modify Fuses or ID\r\n");
								printf (" Skip\r\n");
							}

							address = address + 2;	// Next address
						}
					}
				}
			}
		}

		putc (XON);		// Allow the sender to resume with the next line
	}

	return (success);	// TRUE = All except the vectors have been written successfully
}

#separate
U8BIT ascii_to_u8bit (char *string)		// Converts string "A1" to unsigned 8 bit A1 (basically atoi but more compact)
{										// NOTE: This function processes both uppercase and lowercase strings
	U8BIT	MSN;						// Most Significant Nibble of the result
	U8BIT	LSN;						// Least Significant Nibble of the result

	MSN = (string[0] - '0');			// Assume numeric and subtract text 0 which is ASCII 30
	if (string[0] >= 'A') {				// The character is actually alphabetic,...
		MSN = MSN - 7;					// ...adjust for the 7 character gap in the ASCII table
	}

	LSN = (string[1] - '0');			// Assume numeric and subtract text 0 which is ASCII 30
	if (string[1] >= 'A') {				// The character is actually alphabetic,...
		LSN = LSN - 7;					// ...adjust for the 7 character gap in the ASCII table
	}
										// Shift the MSN into position (zero fill), strip lowercase bit and...
	return ((MSN << 4) | (LSN & 0x0F));	// ...merge it with the LSN after the lowercase bit is stripped
}

#separate
BOOLEAN kind_write_verify_program_eeprom (U32BIT address, U16BIT data)
{			// It's called FLASH but it has all the properties of EEPROM, even write cycle delay
	U16BIT	current_data;
	U16BIT	ee_block[32];								// Program EEPROM must be modified in blocks of 64 bytes
	U32BIT	ee_block_address;							// Contains the address of the first byte of the block
	U8BIT	ee_block_index;								// Contains the offset to the data to be modified

	bytes_written = bytes_written + 2;					// Ignore the optimization below for accurate usage reporting

	current_data = read_program_eeprom (address);		// Get the data that is currently at the target address
	if (current_data != data) {							// Do not write the eeprom if the data is already there
														// This will reduce wear on the memory and improve execution speed
		ee_block_address = address & 0xFFFFFFC0;		// 0b11111111111111111111111111000000
		ee_block_index = (address &  0x0000003F) >> 1;	// 0b00000000000000000000000000111111
														// The index is divided by 2 because the data is 2 bytes wide
		read_program_memory (ee_block_address, ee_block, 64);	// Read the entire EEPROM block (64 bytes)
		ee_block[ee_block_index] = data;						// Modify the block with the new data
		write_program_memory (ee_block_address, ee_block, 64);	// Write the entire EEPROM block
		// NOTE: Program EEPROM is automatically erased when full blocks are written, otherwise it is not

		current_data = read_program_eeprom (address);	// Get the data that is NOW at the target address
		if (current_data != data) {						// The write to program memory has failed
			// NOTE: Doing the two prints separately actually requires less code space
			printf ("\r%8LX", address);					// Report the exact address where the failure occurred
//			printf (" Write failure!");
			printf (" WriteFail");
			return (FALSE);								// Return failure flag
		}
	}													// The new data was successfully written
	return (TRUE);										// Return success flag
}

#inline
BOOLEAN	kind_write_verify_data_eeprom (U8BIT address, U8BIT data)
{
	U8BIT	current_data;

	current_data = read_eeprom (address);				// Get the data that is currently at the target address
	if (current_data != data) {							// Do not write the eeprom if the data is already there
		write_eeprom (address, data);					// This will reduce wear on the memory and improve execution speed

		current_data = read_eeprom (address);			// Get the data that is NOW at the target address
		if (current_data != data) {						// The write to data memory has failed
			// NOTE: Doing the two prints separately actually requires less code space
			printf ("\r00F000%2X", address);			// Report the exact address where the failure occurred (00F000xx)
//			printf (" Write failure!");
			printf (" WriteFail");
			return (FALSE);								// Return failure flag
		}
	}													// The new data was successfully written
	return (TRUE);										// Return success flag
}

// End of file
