#!/usr/bin/perl

# Lock bit three disables external memory access, which is very useful!

$set_lock_bit_one = 'true';   # See notes about what lock bits do.
$set_lock_bit_two = 'true';   # If set, bit one must also be set.
$set_lock_bit_three = 'true'; # If set, all bits must also be set.

# Lock bits:
#
# All lock bits are disabled when the chip is erased.  Their purpose is to
# protect the contents of the flash memory, not to prevent reuse of the chip.
#
# Setting lock bit one does three things:
#
#   1. It prevents 'MOVC' instructions ('TAB' in Orgasm) which are read from
#      external program memory from fetching bytes from the internal flash
#      memory.
#   2. It causes pin 31 (-EA) to be latched upon reset, whereas normally it
#      is sampled each time it's state is relevant.
#   3. It disables further programming of the flash memory.
#      (Until the chip is erased, that is.)
#
# Setting lock bit two does just one thing:
#
#   1. Reading the flash memory contents via a programming device is disabled.
#
# Setting lock bit three does one very useful thing:
#
#   1. Executing code from external memory is disabled.
#
# Of the three bits, I think bit three is the most useful.  It's easy for a
# buggy program to accidentally jump outside of the internal EEPROM, and that
# causes a lot of the I/O pins to be used for external memory I/O, which may
# cause all sorts of problems, particularly because they use 'strong pullups'
# (which aren't just resistors, but actual positive voltage) which might
# damage unsuspecting circuits.  Therefore, I recommend always setting lock
# bit three, and since you'll have to, the other two bits as well.

# End of notes!

# Check out my awesome failure message!
# (It sucks to debug code which isn't actually broken, only simply not tested.)

sub failure {
  disable_reset(); select undef, undef, undef, 0.1;
  print STDERR "\n";
  print STDERR "\e[1;5;33m############################################\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m                                        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ########    ####    ######  ##        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ##        ##    ##    ##    ##        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ##        ##    ##    ##    ##        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ######    ########    ##    ##        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ##        ##    ##    ##    ##        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ##        ##    ##    ##    ##        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m  ##        ##    ##  ######  ########  \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m##\e[0;1;31m                                        \e[1;5;33m##\e[0m\n";
  print STDERR "\e[1;5;33m############################################\e[0m\n";
  print STDERR "\n";
  die "\e[1;37m" . $_[0] . "\e[0m\n";
};

$| = 1; # Make writes to STDOUT occur instantly!

open_programming_device();

print "Activating the reset signal...\n";
disable_reset(); select undef, undef, undef, 0.1;
enable_reset(); select undef, undef, undef, 0.1;

if ($ARGV[0] eq '') {
  disable_reset(); failure "Please supply the name of the file you wish to write into the chip.\n";
};

print "Enabling programming interface...\n";
send_byte(0xAC); send_byte(0x53); send_byte(0x00);
failure "Failed to enable programming!\n" unless receive_byte() == 0x69;

print "Erasing the flash memory...\n";
send_byte(0xAC); send_byte(0x80); send_byte(0x00); send_byte(0x00);
select undef, undef, undef, 2/3; # It takes 0.5 seconds.

# Look into reading the data to be written to the chip...
if ($ARGV[0] eq '') {
  disable_reset(); failure "Please supply the name of the file you wish to write into the chip.\n";
} elsif (!-e $ARGV[0]) {
  disable_reset(); failure "The file '$ARGV[0]' does not exist!\n";
} elsif (!-f $ARGV[0] and !-l $ARGV[0]) {
  disable_reset(); failure "The file '$ARGV[0]' does not appear to be a normal file!\n";
};
unless (open DATA, '<', $ARGV[0]) {
  disable_reset(); failure "I cannot open the file '$ARGV[0]' for reading!\n";
};
read DATA, $data, 8192;
if (0 < read DATA, $trash, 1) {
  disable_reset(); failure "The file '$ARGV[0]' contains more than 8192 bytes of data!\n";
};
close DATA;

# Pad with 'nop' bytes to meet a block boundary.
$blocks = int((length($data) + 255) / 256);
$data .= "\x00" x (256 * $blocks - length($data));

print "Transfering data to chip...\n";
for ($block = 0; $block < $blocks; $block++) { $count = $block + 1;
  print "\rBlock $count of $blocks, writing...\e[K";
  send_byte(0x50); send_byte($block);
  send_data(substr($data, 256 * $block, 256));
  print "\rBlock $count of $blocks, verifying...\e[K";
  send_byte(0x30); send_byte($block);
  $verify = receive_data(256);
  if ($verify eq substr($data, 256 * $block, 256)) {
    # Hooray, all is well!
  } elsif ($verify eq "\xFF" x 256) {
    failure
"It appears that the chip was erased, but not programmed at all.  This seems to
usually mean that, while the chip has sufficient voltage to operate, it doesn't
have sufficient voltage to program its flash memory.  Check the power supplied
to the chip and make sure it is a full 5.0 volts, then try again.

The other possible cause is that the programmer is writing bytes too quickly,
and so the chip doesn't have sufficient time to program them into it's flash
memory.  If this is the case, the code in the programmer needs to be adjusted.\n";
  } else {
    if (0) {
      open FILE, ">.verify"; print FILE $verify; close FILE;
      open FILE, ">.data"; print FILE substr($data, 256 * $block, 256); close FILE;
      `hexdump -C .verify > .verify.txt`;
      `hexdump -C .data > .data.txt`;
      print `diff .data.txt .verify.txt`;
      unlink(".verify", ".verify.txt", ".data", ".data.txt");
    };
    failure "Data read from chip does not match data written to chip!\n";
  };
};
print "\rData transfer complete!\e[K\n";

if ($set_lock_bit_one) {
  print "Setting lock bit one...\n";
  send_byte(0xAC); send_byte(0xE1); send_byte(0x00); send_byte(0x00);
  select undef, undef, undef, 0.1;
};
if ($set_lock_bit_two) {
  print "Setting lock bit two...\n";
  send_byte(0xAC); send_byte(0xE2); send_byte(0x00); send_byte(0x00);
  select undef, undef, undef, 0.1;
};
if ($set_lock_bit_three) {
  print "Setting lock bit three...\n";
  send_byte(0xAC); send_byte(0xE3); send_byte(0x00); send_byte(0x00);
  select undef, undef, undef, 0.1;
};

print "Checking lock bits...\n";
send_byte(0x24); send_byte(0x00); send_byte(0x00); $lockbits = receive_byte();
if ($lockbits & 0x04) {
  print "Lock bit 1 is set!\n";
} elsif ($set_lock_bit_one) {
  failure "Failed to set lock bit one!\n";
};
if ($lockbits & 0x08) {
  print "Lock bit 2 is set!\n";
} elsif ($set_lock_bit_two) {
  failure "Failed to set lock bit two!\n";
};
if ($lockbits & 0x10) {
  print "Lock bit 3 is set!\n";
} elsif ($set_lock_bit_three) {
  failure "Failed to set lock bit three!\n";
};

print "Deactivating the reset signal...\n";
disable_reset();

print "\e[1;37mDevice programming completed successfully!\e[0m\n";
exit;

# The functions that communicate with the programming device:

sub open_programming_device {

  if ($ARGV[1] eq '') {
    # If a specific device wasn't specified, let's try to find one...
    @devices = split /\n/, `find /dev/FTDI/JfmTbD/ -type c`;
    #$" = "\n    "; print "Connected AT89S52 programming devices:\n    @devices\n";
    if (@devices < 1) {
      failure
        #________________________________________________________________________________#
        "The programming device was not found.  Peraps it isn't connected.  Perhaps it\n" .
        "is broken.  ...or, perhaps you haven't installed the necessary udev rule which\n" .
        "makes it possible to tell one FT245RL device from another.  If that's the case,\n" .
        "try inserting the following line into '/etc/udev/rules.d/FTDI.rules'\n" .
        "\n" .
        "ATTRS{idVendor}==\"0403\", ATTRS{idProduct}==\"6001\", NAME=\"FTDI/\$attr{serial}\", MODE=\"666\"\n" .
        "\n" .
        "If you create the file, you'll have to restart udev, but if it already exists,\n" .
        "it will probably be sufficient to unplug and replug the programming device.\n";
    } elsif (@devices > 1) {
      $choices = '';
      foreach $device (@devices) {
        $choices .= "  'AT89S52 Programmer' \"$ARGV[0]\" $device\n";
      };
      failure
        #________________________________________________________________________________#
        "You have more than one AT89S52 programmer connected to your computer.  Try one\n" .
        "of the following commands to specify which device you want to use:\n\n$choices\n" .
        "You may also resolve the ambiguity by unplugging all but one of the devices.\n";
    } else {
      $device = $devices[0];
    };
  } else {
    # If something was specified, let's see if it is a usable device...
    if (-c "/dev/FTDI/JfmTbD/$ARGV[1]") {
      $device = "/dev/FTDI/JfmTbD/$ARGV[1]";
    } elsif (-c "/dev/FTDI/$ARGV[1]") {
      $device = "/dev/FTDI/$ARGV[1]";
    } elsif (-c "/dev/$ARGV[1]") {
      $device = "/dev/$ARGV[1]";
      `stty raw -echo < $device`;
    } elsif (-c "$ARGV[1]") {
      $device = "$ARGV[1]";
      `stty raw -echo < $device`;
    } else {
      failure("I cannot find a device like '$ARGV[1]'\n");
    };
  };

  print "Using device '$device'\n";

  print "Opening the programming device...\n";
  `stty raw -echo < '$device'`;
  open LINK, '+<', $device;    # Open the device!
  select LINK; $| = 1; select STDOUT; # Disable Perl's I/O buffer.

  print "Clearing any stale data in any input buffers...\n";
  while ('bitch') {
    $read = $write = $bitch = '';
    vec($read, fileno(LINK), 1) = 1;
    select $read, $write, $whatever, 1/2;
    last unless vec($read, fileno(LINK), 1);
    $result = sysread LINK, $bullshit, 4096;
    if ($result <= 0) {
      failure "OS error reading from programming device: $!\n";
    };
  };

  print "Testing responsiveness of programming device...\n";
  $test_string =  pack('H*', '00010204081020407F7E7D7B776F5F3F');
  $test_string .= pack('C', rand(128)) while length($test_string) < 36;
  $input_buffer = ''; $output_buffer = $test_string;
  while ('bitch') {

    $read = $write = $bitch = '';
    vec($read, fileno(LINK), 1) = 1;
    vec($write, fileno(LINK), 1) = 1 if length($output_buffer);
    $result = select $read, $write, $bitch, 2/3;
    last if $result == 0;

    if (vec($read, fileno(LINK), 1)) {
      $result = sysread LINK, $input_buffer, 4096, length($input_buffer);
      if ($result <= 0) {
        failure "OS error reading from programming device: $!\n";
      };
    };

    if (vec($write, fileno(LINK), 1)) {
      $size = length($output_buffer); $size = 4096 if $size > 4096;
      $result = syswrite LINK, $output_buffer, $size;
      if ($result <= 0) {
        failure "OS error writing to programming device: $!\n";
      };
      substr($output_buffer, 0, $result) = '';
    };

  };

  if ($input_buffer eq $test_string) {
    print "The programming device passed its basic I/O test!\n";
  } elsif ($input_buffer eq '') {
    failure "The programming device has failed to respond at all.\n";
  } elsif (length($input_buffer) == length($test_string)) {
    failure "The programming device is responding incorrectly:\n\n" .
    "Sent: " . uc(unpack('H*', $test_string)) . "\n" .
    "Rcvd: " . uc(unpack('H*', $input_buffer)) . "\n";
  } else {
    failure "The programming device sent an incorrect response to test data.\n";
    "Sent: " . uc(unpack('H*', $test_string)) . "\n" .
    "Rcvd: " . uc(unpack('H*', $input_buffer)) . "\n";
  };

};

sub disable_reset {
  print LINK pack('C*', 0xA0);
};

sub enable_reset {
  print LINK pack('C*', 0xB0);
};

sub send_byte {
  my ($first, $second, $data);
  $first = 0x80 | ($_[0] & 0x0F);
  $second = 0x90 | ($_[0] & 0xF0) >> 4;
  print LINK pack('C*', $first, $second);
  $junk_bytes++;
};

sub send_data {
  my ($i);
  for ($i = 0; $i < length($_[0]); $i++) {
    send_byte(unpack('C', substr($_[0], $i, 1)));
  };
};

sub transfer_byte {
  my ($data);
  send_byte($_[0]);
  read LINK, $data, $junk_bytes;
  $junk_bytes = 0;
  return unpack('C', substr($data, -1));
};

sub receive_byte {
  return transfer_byte();
};

sub receive_data {
  my ($data);
  send_data("\x00" x $_[0]);
  read LINK, $data, $junk_bytes;
  $junk_bytes = 0;
  return substr($data, -$_[0]);
};
