#!/usr/bin/perl -w

# parse_sychip_fw - convert binary firmware in a Sychip prc file to
# S-Record format

# Copyright (C) 2004,2005 Pavel Roskin <proski@gnu.org>

# This script is Free Software, and it can be copied, distributed and
# modified as defined in the GNU General Public License.  A copy of
# its license can be downloaded from http://www.gnu.org/copyleft/gpl.html

# Usage:
#      parse_sychip_fw infile

use strict;

sub swap16 ($)
{
	my $word = shift(@_);
	return (($word & 0xff) << 8) + (($word & 0xff00) >> 8);
	my $radix = 0x1000000;
	my $res = 0;
	while ($word != 0) {
		$res += $radix * ($word & 0xff);
		$word >>= 8;
		$radix >>= 8;
	}
	return $res;
}

sub swap32 ($)
{
	my $word = shift(@_);
	my $radix = 0x1000000;
	my $res = 0;
	while ($word != 0) {
		$res += $radix * ($word & 0xff);
		$word >>= 8;
		$radix >>= 8;
	}
	return $res;
}

sub readnum_ba ()
{
	my $byte_a;
	read INFILE,$byte_a,1;
	my $byte_b;
	read INFILE,$byte_b,1;
	return (ord($byte_b) << 8) + ord($byte_a);
}

sub readnum_abcd ()
{
	my $byte_a;
	read INFILE,$byte_a,1;
	my $byte_b;
	read INFILE,$byte_b,1;
	my $byte_c;
	read INFILE,$byte_c,1;
	my $byte_d;
	read INFILE,$byte_d,1;
	return (ord($byte_a) << 24) + (ord($byte_b) << 16) + (ord($byte_c) << 8) + ord($byte_d);
}

sub crc_print
{
	my $line = sprintf(shift (@_), @_);

	# Split, clean empty elements.
	my @twochars = split (/(..)/, $line);
	foreach my $index (0 .. $#twochars) {
		unless ($twochars[$index] =~ m/../) {
			splice (@twochars, $index, 1);
		}
		last if ($index >= $#twochars);
	}

	# Print Sn, find record length
	printf OUTFILE "%s", shift(@twochars);
	my $bytelen = hex $twochars[0];

	# Calculate CRC, print the line
	my $crc = 0;
	foreach my $index (0 .. $bytelen - 1) {
		$crc += hex($twochars[$index]);
		printf OUTFILE "$twochars[$index]";
	}
	printf OUTFILE "%02X\n", (0xFF - $crc) & 0xFF;
}



my @bl_offset;
my @bl_addr;
my @bl_len;
my @pdr_type;
my @pdr_addr;

sub find_offset ($)
{
	my $a = shift(@_);
	my $block = 0;
	while (defined $bl_addr[$block]) {
		if ( ($bl_addr[$block] <= $a) &&
		     ($bl_addr[$block] + $bl_len[$block] > $a) ) {
			return ($bl_offset[$block] + $a - $bl_addr[$block]);
		}
		$block++;
	}
	return 0;
}

# Print message and exit (like "die", but without raising an exception).
# Newline is added at the end.
sub error
{
	printf STDERR "ERROR: ";
	printf STDERR @_;
	printf STDERR "\n";
	exit 1;
}


my $addr;
my $len;
my $line;
my $start_addr = 0;
my $pdr6addr = 0;
my $fwidaddr = 0;

my %pdrlen = (
	0x0006 => 0x000a,
	0x0101 => 0x0006,
	0x0103 => 0x000c,
	0x0104 => 0x0002,
	0x0105 => 0x0002,
	0x0105 => 0x0002,
	0x0105 => 0x0002,
	0x0107 => 0x0002,
	0x0202 => 0x0064,
	0x0203 => 0x0080,
	0x0204 => 0x0050,
	0x0300 => 0x001c,
	0x0301 => 0x0022,
	0x0302 => 0x0002,
	0x0303 => 0x0002,
	0x0405 => 0x0004,
	0x0406 => 0x0002,
	0x0412 => 0x0006,
	0x0413 => 0x0002,
	0x0414 => 0x0024,
);

my $nic3841 = [
	[0x8002, 1, 0, 0],
	[0x8002, 1, 0, 1],
	[0x8003, 1, 0, 0],
	[0x8003, 1, 0, 1],
	[0x8004, 1, 0, 0],
	[0x8008, 1, 0, 0],
];

my $nic3842 = [
	[0x800A, 1, 0, 0],
	[0x800B, 1, 0, 0],
	[0x800C, 1, 0, 0],
	[0x800D, 1, 0, 0],
	[0x8012, 1, 0, 0],
	[0x8013, 1, 0, 0],
	[0x8014, 1, 0, 0],
	[0x8016, 1, 0, 0],
	[0x8017, 1, 0, 0],
	[0x8018, 1, 0, 0],
	[0x801A, 1, 0, 0],
	[0x801B, 1, 0, 0],
	[0x801C, 1, 0, 0],
	[0x8021, 1, 0, 0],
	[0x8022, 1, 0, 0],
	[0x8023, 1, 0, 0],
	[0x800A, 1, 0, 0],
	[0x800E, 1, 0, 0],
	[0x8015, 1, 0, 0],
	[0x8019, 1, 0, 0],
	[0x801D, 1, 0, 0],
	[0x8024, 1, 0, 0],
];

if ($#ARGV != 0) {
	error ("Usage: parse_sychip_fw infile");
}

unless (open (INFILE, "< $ARGV[0]")) {
	error ("couldn't open $ARGV[0] for reading: $!");
}

# Skip to the beginning of firmware
$/ = "_";
while (<INFILE>) {
	last if (/SYCHIP/);
}
while (<INFILE>) {
	last if (/SYCHIP/);
}
while (<INFILE>) {
	last if (/SYCHIP/);
}
while (<INFILE>) {
	last if (/SYCHIP/);
}

#
# First pass
#

# Data blocks
my $min_addr = 0x7fffffff;
my $max_addr = 0;
while (!eof(INFILE)) {
	$addr = readnum_abcd();
	$len = readnum_abcd();
	my $pdr_count = readnum_abcd();

	seek (INFILE, 12, 1);
	foreach my $index (0 .. $pdr_count - 1) {
		my $pdrtype = readnum_abcd();
		my $pdraddr = readnum_abcd();
		push @pdr_type, $pdrtype;
		push @pdr_addr, $pdraddr;
		if ($pdrtype == 0x0006) {
			$pdr6addr = $pdraddr;
		} elsif ($pdrtype == 0x0107) {
			$fwidaddr = $pdraddr + 0x30;
		}
	}

	push @bl_offset, tell(INFILE);
	push @bl_addr, $addr;
	push @bl_len, $len;

	$min_addr = $addr if ($addr < $min_addr);
	$max_addr = ($addr + $len) if ($addr + $len > $max_addr);

	seek (INFILE, $len + 8, 1);
	last if ($pdr_count == 0);
}

if ($pdr6addr == 0) {
	error "Cannot identify firmware - no PDR 6";
}

my $pdr6offset = find_offset($pdr6addr);
if ($pdr6offset == 0) {
	error "Cannot identify firmware - PDR 6 doesn't point to the image";
}

my $fwidoffset = find_offset($fwidaddr);
if ($fwidoffset == 0) {
	error "Cannot identify firmware - Firmware ID string is not in the image";
}

seek(INFILE, $fwidoffset, 0);
my $secmagic = readnum_ba();
if ($secmagic != 0x6672) {
	error "Firmware ID doesn't start with \"rf\"";
}

seek(INFILE, $pdr6offset + 14, 0);
my $hwver = readnum_ba();
seek(INFILE, $pdr6offset + 20, 0);
my $compid = readnum_ba();
my $variant = readnum_ba();
my $major = readnum_ba();
my $minor = readnum_ba();

if ($compid != 0x1f) {
	error "Component ID %04X - not secondary firmware?\n", $compid;
}

my $nictable;
my $hwchar;
my $hwtext;
if ($hwver == 1) {
	$hwtext = "hfa3841 (Prism 2)";
	$nictable = $nic3841;
	$hwchar = "1";
} elsif ($hwver == 2) {
	$hwtext = "hfa3842 (Prism 2.5 and 3)";
	$nictable = $nic3842;
	$hwchar = "f";
} else {
	error "Unknown hardware version %d\n", $hwver;
}

printf "Found secondary firmware %d.%d.%d for %s\n",
       $major, $minor, $variant, $hwtext;

my $intname;
my $varstr;
if ($major == 0) {
	$varstr = sprintf("c%01x", $variant);
} else {
	$varstr = sprintf("%02x", $variant);
}

$intname = sprintf("r%s%02x%02x%s.hex", $hwchar, $major, $minor, $varstr);
printf "Firmware internal name: %s\n", $intname;

unless (open(OUTFILE, "> $intname")) {
	error "Cannot open %s for writing: $!", $intname;
}


#
# Second pass
#

# Data blocks
my $block = 0;
while (defined $bl_addr[$block]) {
	seek(INFILE, $bl_offset[$block], 0);
	my $len = $bl_len[$block];
	my $count = 0;
	while (1) {
		my $byte1;
		my $byte2;
		if ($count % 16 == 0) {
			my $rest = 16;
			$rest = $len if ($len < 16);
			$line = sprintf("S3%02X%08X", $rest + 5,
			       $bl_addr[$block] + $count);
		}
		read INFILE, $byte1, 1;
		read INFILE, $byte2, 1;
		$line .= sprintf("%02X%02X", ord($byte1), ord($byte2));
		$count += 2;
		$len -= 2;
		last if ($len <= 0);
		if ($count % 16 == 0 && $len) {
			crc_print ($line);
		}
	}
	crc_print ($line);
	$block++;
}

# FWID PDR - firmware file name
crc_print("S311FF000000FFFFFFFF%08X0E000000", swap32($fwidaddr));

# Other PDRs
foreach my $index (0 .. $#pdr_type - 1) {
	my $pdtype = $pdr_type[$index];
	my $pdaddr = $pdr_addr[$index];
	my $pdlen = $pdrlen{$pdtype};
	if (!defined $pdlen) {
		error "Unknnown length for PDR type 0x%04X", $pdtype;
	}
	crc_print("S311FF000000%08X%08X%08X",
		  swap32($pdtype), swap32($pdaddr), swap32($pdlen));
}

# CRC record - cover 64k, program CRC
crc_print("S311FF100000%08X%08X%08X", swap32($min_addr), swap32(65536),
	  swap32(1));

crc_print "S30BFF200000020003000000D0";

# Firmware version
crc_print "S311FF20000005000100%04X%04X%04X%04X", swap16($compid),
       swap16($variant), swap16($major), swap16($minor);

# Compatibility ranges
if ($hwver == 1) {
	crc_print "S313FF2000000600020001000100010001000100";
	crc_print "S313FF2000000600020001000200010001000100";
	crc_print "S313FF2000000600020001000300010002000200";

} elsif ($hwver == 2) {
	crc_print "S313FF2000000600020000000400%04X01000F00",
		  swap16($variant);
	crc_print "S313FF2000000600020001000100010001000100";
	crc_print "S313FF2000000600020001000200020001000100";
	crc_print "S313FF2000000600020001000300010004000400";
}

# Included name
$line = "S317FF20000007000180";
my @namechars = split(//, $intname);
foreach (@namechars) {
	$line .= sprintf("%02X", ord($_));
}
$line .= "0000";
crc_print $line;

# NIC ID table
my $nic = 0;
while (defined $nictable->[$nic][0]) {
	crc_print("S311FF20000005000400%04X%04X%04X%04X",
	       swap16($nictable->[$nic][0]), swap16($nictable->[$nic][3]),
	       swap16($nictable->[$nic][1]), swap16($nictable->[$nic][2]));
	$nic++;
}

# At last, write starting address
if ($hwver == 1) {
	$start_addr = 0x0F8C01;
} else {
	$start_addr = 0x3F0C01;
}
crc_print "S705%08X", $start_addr;

close(INFILE);
close(OUTFILE);
