#! /usr/bin/perl

#-------------------------------------------------------------------------------
#	%M% %W% %G%
#
# Interface routines for AYUCR bootload.asm
#-------------------------------------------------------------------------------

use Device::SerialPort 0.12;

#-------------------------------------------------------------------------------
# Turn off I/O buffering so comm routines flush each char as it is queued up

$| = 1;

#-------------------------------------------------------------------------------
# Processor memory layout

my $PAGESIZE = 64;
my $NULLVAL = "FF";
my $MINUSER = 0x01;
my $MAXUSER = 0x37;
my $MINBOOT = 0x38;
my $MAXBOOT = 0x3F;
my $MINADDR = 0x00;
my $MAXADDR = 0x3F;

# Communications settings

my $PORT = "/dev/ttyS1";
my $BAUD = 2400;
my $DATA = 8;
my $STOP = 1;
my $PARI = "none";
my $TOMS = 1000;

#-------------------------------------------------------------------------------

my ($filename) = @ARGV;

if (!defined $filename) {
	print "usage: upfirm.pl <filename.hex>\n";
	exit;
}

# Read, parse and display image to load
read_hex ($filename);
dump_pages (\@page_list);
print "\n";

# Init
print "Initializing Communications...\n";
init_comm ();

# Read existing firmware
print "Reading Firmware";
read_firmware ();
dump_pages (\@chip_list);
write_hex ("previous.hex", \@chip_list);

# Compare protected regions to ensure they are compatible
$errors = compare_pages (\@page_list, \@chip_list, 0, 0) +
		compare_pages (\@page_list, \@chip_list, $MINBOOT, $MAXBOOT);
if ($errors) {
	print "\nWARNING!\n",
		"The reserved bootloader portions of the new and existing program images\n",
		"are different.  Attempting to load this image using the bootloader may\n",
		"result in damage to the firmware and may require reflashing the chip\n";

	<STDIN>;
}

print "Writing Firmware.";
write_firmware (\@page_list);

print "Verifying Firmware";
read_firmware ();
$errors = compare_pages (\@page_list, \@chip_list, $MINADDR, $MAXADDR);
if ($errors) {
	print "\nWARNING!\n",
		"Error verifying firmware.  Do not power down controller.  Attempt to\n",
		"reflash now.\n"
}
else {
	print "Update successful.\n";
}

# close serial port
undef $port;

#-------------------------------------------------------------------------------
# Read and parse hex file

sub read_hex
{
	my $filename = shift;
	
	open (HEX, $filename) || die "Could not open $filename\n";

	while (<HEX>) {
		my ($count, $address, $type, $data, $checksum) = m/^:(\S\S)(\S\S\S\S)(\S\S)(\S*)(\S\S)/;

		# Convert data fields from hex
		$count = hex ($count);

		my $page = int ((hex ($address) + 1) / $PAGESIZE);
		my $offset = hex ($address) % $PAGESIZE;

		# Confirm checksum of data
		$sum = $count + hex (substr ($address, 0, 2)) + hex (substr ($address, 2, 2)) +
				hex ($type) + hex_to_sum ($data);
		$sum = sprintf ("%02X", (~$sum + 1) & 0xff);

		die "Record at address ($address) has bad checksum ($checksum)  I get $sum\n" if ($sum ne $checksum);

		# Add data records to page list
		if ($type eq "00") {
			if (!defined $page_list[$page]) {
				$page_list[$page] = "  " x $PAGESIZE;
			}
			substr ($page_list[$page], $offset * 2, $count * 2) = $data;
		}

#		printf ("$type %2d $address(%04X %2d) $data $checksum $sum\n", $count, $page, $offset);
	}

	close (HEX);
}

#-------------------------------------------------------------------------------
# Write hex file

sub write_hex
{
	my $filename = shift;
	my $pages = shift;

	open (HEX, ">$filename") || die "Could not open $filename\n";

	# Loop over all pages
	for ($MINADDR .. $MAXADDR) {
		if (defined $$pages[$_]) {
			# break each page into blocks of 16 bytes
			for $block (0..3) {
				my $offset = $block * $PAGESIZE/2;
				my $addr = $_ * $PAGESIZE + $offset/2;
				my $page = substr ($$pages[$_], $offset, $PAGESIZE/2);

				# Loop over block until everything has been dumped
				for (;;) {
					($pre, $data, $rest) = ($page =~ m/(\s*)(\S+)(.*)/);
					last if $data eq "";

					# Update address to account for skipped bytes
					$addr += length ($pre)/2;

					$count = length ($data)/2;

					$sum = $count + int ($addr / 0xFF) + ($addr & 0xFF) + hex_to_sum ($data);
					$sum = (~$sum + 1) & 0xFF;

					printf HEX ":%02X%04X%02X%s%02X\r\n", $count, $addr, 0x00, $data, $sum;

					$addr += length ($data)/2;

					$page = $rest;
				}
			}
		}
	}

	print HEX ":00000001FF\r\n";

	close (HEX);
}

#-------------------------------------------------------------------------------
#

sub dump_pages
{
	my $pages = shift;

	for $num (0 .. $#$pages) {
		if (defined $$pages[$num]) {
			my $page = $$pages[$num];

			printf ("%03X-%02X : |%s|\n", $num, 0, substr ($page, 0, $PAGESIZE));
			printf ("%03X-%02X : |%s|\n", $num, $PAGESIZE, substr ($page, $PAGESIZE, $PAGESIZE));
		}
	}
}

#-------------------------------------------------------------------------------
# Check that the data is appropriate to download

sub compare_pages
{
	my $new = shift;
	my $old = shift;
	my $low = shift;
	my $high = shift;

	my $errors = 0;

	# Check interrupt vector and boot loader
	for (0x38 .. 0x3F) {
		$errors++ if ($$new[$_] ne $$old[$_])
	}

	$errors;
}

#-------------------------------------------------------------------------------
# Converts hex string to data bytes.  Empty values are set to the NULLVAL

sub hex_to_bytes
{
	my ($data) = @_;
	my $length = length ($data) / 2;
	my $bytes = "";

	for $offset (0 .. $length - 1) {
		my $byte = substr ($data, $offset * 2, 2);
		$byte = $byte eq "  " ? $NULLVAL : $byte;
		$bytes .= chr (hex ($byte));
	}

	return $bytes;
}

#-------------------------------------------------------------------------------
# Converts data bytes to a hex string.  Empty values are set to the NULLVAL

sub bytes_to_hex
{
	my ($data) = @_;
	my $length = length ($data);
	my $string = "";

	for $offset (0 .. $length - 1) {
		$string .= sprintf ("%02X", ord (substr ($data, $offset, 1)));
	}

	return $string;
}

#-------------------------------------------------------------------------------
# sum the hex bytes

sub hex_to_sum
{
	my ($data) = @_;
	my $sum = 0;

	for (0 .. length ($data) / 2 - 1) {
		$byte = substr ($data, $_ * 2, 2);
		$sum += hex ($byte);
	}

	return $sum;
}

#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------

sub init_comm
{
	$port = Device::SerialPort->new ($PORT) || die "Could not create port object\n";

	$port->baudrate ($BAUD);
	$port->databits ($DATA);
	$port->stopbits ($STOP);
	$port->read_const_time ($TOMS);

	# Look for controller
	$port->write ("/M");
	get_line ();
	$line = get_line ();
	die "Could not find controller [$line]\n" if ($line ne "APC");
	die "NO OK\n" if !get_ok ();
	print "Hardware : $line\n";

	# Get Version
	$port->write ("/V");
	get_line ();
	print "Version  : ", get_line (), "\n";
	die "NO OK\n" if !get_ok ();

	# Start bootloader and look for prompt
	for (1 .. 10) {
		$port->write ("/B");

		return if $port->read (1) eq "K";
	}

	undef $port;

	die "Could not find boot loader on $PORT\n";
}

#-------------------------------------------------------------------------------
# get line

sub get_line
{
	my $line = "";

	while (1) {
		$ch = $port->read (1);
		last if ($ch eq "");

		# Check for end of line
		if ($ch eq chr (13)) {
			$port->read (1);
			last;
		}

		$line .= $ch;
	}

	return $line;
}

#-------------------------------------------------------------------------------
# get OK

sub get_ok
{
	my $ok = $port->read (2);

	return $ok eq "OK"
}

#-------------------------------------------------------------------------------
# Retun address as a LSB + MSB 16 bit word

sub get_address
{
	my $page_num = shift;

	my $addr = $page_num * $PAGESIZE / 2;
	my $low_addr = $addr & 0xFF;
	my $high_addr = int ($addr / 0xFF);

	chr ($low_addr) . chr ($high_addr);
}

#-------------------------------------------------------------------------------
# Get the checksum

sub calc_checksum
{
	my $checksum = 0;

	foreach (@_) {
		foreach $c (split (//)) {
			$checksum += ord ($c);
		}
	}

	chr ($checksum & 0xFF);
}

#-------------------------------------------------------------------------------
# Generate a command string including a command char, address and checksum
sub get_command
{
	my $command = shift;
	my $page = shift;
	my $address = get_address ($page);
	my $checksum = calc_checksum ($address);

	$command . $address . $checksum;
}

#-------------------------------------------------------------------------------

sub read_page
{
	my ($page) = @_;

	$port->write (get_command ("R", $page));

	$port->read (64);
}

#-------------------------------------------------------------------------------

sub erase_page
{
	my ($page) = @_;

	$port->write (get_command ("E", $page));
}

#-------------------------------------------------------------------------------

sub wait_k
{
	my $data = "";
	my $timeout = 5;

	while (1) {
		$data .= $prompt = $port->read (1);
		last if $prompt eq "K";

		# Check timeout
		last if ($prompt eq "" && $timeout-- == 0)
	}

	$data;
}

#-------------------------------------------------------------------------------
# Write a single page to the chip

sub write_page
{
	my ($page_num, $page) = @_;

	my $len = length ($page);

	# Data checks
	if (! defined $page) {
		erase_page ($page_num);
		return;
	}
	if ($len != $PAGESIZE * 2) {
		print "\nInvalid data page size ($len) for page $page_num\n";
		return;
	}

	$address = get_address ($page_num);
	$data = hex_to_bytes ($page);
	$checksum = calc_checksum ($address, $data);

	$command = "W" . $address . $data . $checksum;
	$port->write ($command);
}

#-------------------------------------------------------------------------------

sub write_firmware
{
	my $pages = shift;

	# Write all non protected pages
	for ($MINUSER .. $MAXUSER) {
		$ret = write_page ($_, $$pages[$_]);

		$prompt = wait_k ();

		if ($prompt ne "K") {
			printf ("Error [$prompt] writing page %2X:%3X\n", $_, $_ * $PAGESIZE / 2);
			return;
		}

		print ".";
	}

	print "\n";
}

#-------------------------------------------------------------------------------

sub read_firmware
{
	# Read all pages and create a list
	for ($MINADDR .. $MAXADDR) {
		my $data = read_page ($_);

		# Check for an error
		if ($data eq "CK") {
			print "\nChecksum error issuing command $_\n";
			redo;
		}

		$checksum = $port->read (1);

	 	$page = bytes_to_hex ($data);
	
		wait_k ();

		# Check checksum
		if ($checksum != calc_checksum ($data)) {
			print "\nChecksum error reading page $_\n";
			redo;
		}

		print ".";

		# Remove NULL commands
		for (my $offset = 0; $offset < length ($page); $offset += 4) {
			if (substr ($page, $offset, 4) eq "FF3F") {
				substr ($page, $offset, 4) = "    ";
			}
		}

		$chip_list[$_] = $page if ($page !~ /^\s*$/);
	}

	print "\n";
}
