#! /usr/bin/perl

# This script converts intel-hex data into fast flash download
# binary format, for use with the PJRC MP3 player fast flash
# firmware download.

# Typical usages:
#   hex2fdl mp3player.hex > mp3player.fdl
#   hex2fdl mp3player.hex | xmit115200



$ascii_output = 0;


$code_zero		= 0x5A;
$code_two_ff		= 0x2D;
$code_three_ff		= 0x19;
$code_escape_byte	= 0x59;
$code_address		= 0x29;
$code_115200_baud	= 0x6A;
$code_checksum		= 0x3C;
$code_end_of_data	= 0x58;
$code_undef_skip	= 0x1D;
$code_abort		= 0x1B;



$h = '[0-9A-F]';
$addr_hi = 0;

while (<>) {
	$addr_hi = 0x00000, next if /^:020000020000FC/;
	$addr_hi = 0x10000, next if /^:020000021000EC/;
	next unless /^:($h{2})/;
	#todo: this parsing is slow, can it be faster somehow?
	$len = hex($1) * 2;
	$hh = "$h\{$len\}";
	next unless /^:($h{2})($h{4})00($hh)($h{2})/;
	$addr = hex($2) + $addr_hi;
	$data_text = $3;
	$sum = hex($4);
	#todo: check the checksum
	for ($i=0; $i<$len/2; $i++) {
		$data_text =~ /^($h{2})/;
		$data[$addr + $i] = hex($1);
		$data_text = $';
	}
}

$cksum = 0;
output_range(0x00000, 0x0FFFF);
output_range(0x10000, 0x1FFFF);
emit_byte($code_end_of_data);
print "\n" if $ascii_output;


sub output_range
{
	my $begin = shift;
	my $end = shift;
	my $addr;
	my $undef_count = 100000;
	my $ff_count = 0;
	my $data_count = 0;

	for ($addr=$begin; $addr<$end+1; $addr++) {
		if (!defined($data[$addr])) {
			if ($ff_count > 0) {
				emit_ff($ff_count);
				$ff_count = 0;
			}
			$undef_count++;
		} else {
			if ($undef_count > 0) {
				if ($undef_count < 120) {
					emit_undef_skip($undef_count);
				} else {
					emit_addr($addr);
				}
				$undef_count = 0;
			}
			if ($data[$addr] == 0xFF) {
				$ff_count++;
			} else {
				if ($ff_count > 0) {
					emit_ff($ff_count);
					$ff_count = 0;
				}
				emit_data($data[$addr]);
				$data_count++;
				if ($data_count > 400) {
					emit_cksum();
					$data_count = 0;
				}
			}
		}
	}
	emit_ff($ff_count) if $ff_count > 0;
	emit_cksum() if $data_count > 0;
}



sub emit_addr
{
	my $addr = shift;
	my $byte;

	warn sprintf "Addr: %04X\n", $addr;

	emit_byte($code_address);

	$byte = ($addr & 0x7F) | 0x80;
	$cksum = ($cksum + (($byte ^ 0x6B) << 8)) & 0xFFFF;
	emit_byte($byte);

	$byte = (($addr >> 8) & 0x7F) | 0x80;
	$cksum = ($cksum + (($byte ^ 0x5A) << 8)) & 0xFFFF;
	emit_byte($byte);

	$byte = (($addr >> 7) & 1) | (($addr >> 14) & 6) | 0x80;
	$cksum = ($cksum + ($byte << 8)) & 0xFFFF;
	emit_byte($byte);
}

sub emit_cksum
{
	emit_byte($code_checksum);

	#warn("cksum: $cksum\n");

	emit_byte(($cksum & 0x7F) | 0x80);
	emit_byte((($cksum >> 8) & 0x7F) | 0x80);
	$cksum = 0;
}

sub emit_undef_skip
{
	my $num = shift;
	my $byte;

	#warn("undef: $num\n");

	emit_byte($code_undef_skip);
	$byte = $num | 0x80;
	$cksum = ($cksum + (($byte ^ 0x4E) << 8)) & 0xFFFF;
	emit_byte($byte);
}

sub emit_ff
{
	my $num = shift;

	while ($num >= 3) {
		#warn("Three FFs\n");
		$cksum = ($cksum + 0x3C00) & 0xFFFF;
		emit_byte($code_three_ff);
		$num -= 3;
	}
	if ($num == 2) {
		#warn("Two FFs\n");
		$cksum = ($cksum + 0x5D00) & 0xFFFF;
		emit_byte($code_two_ff);
	}
	if ($num == 1) {
		#warn("One FFs\n");
		emit_data(0xFF);
	}
}


sub emit_data
{
	my $byte = shift;

	$cksum = ($cksum + $byte + (($byte ^ 0x56) << 8)) & 0xFFFF;

	if ($byte == 0) {
		emit_byte($code_zero);
		return;
	}
	if ($byte == $code_zero
	 || $byte == $code_two_ff
	 || $byte == $code_three_ff
	 || $byte == $code_escape_byte
	 || $byte == $code_address
	 || $byte == $code_115200_baud
	 || $byte == $code_checksum
	 || $byte == $code_end_of_data
	 || $byte == $code_undef_skip
	 || $byte == $code_abort) {
		#warn("ESC: $byte\n");
		emit_byte($code_escape_byte);
		emit_byte($byte | 0x80);
		return;
	}
	emit_byte($byte);
}


sub emit_byte
{
	my $byte = shift;

	if ($ascii_output) {
		printf("%02X ", $byte);
		print "\n" if ++$bytecount % 16 == 0;
	} else {
		print pack('C', $byte);
	}
}


