123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- #!/usr/bin/perl
- use strict;
- use integer;
- use PerlIO::gzip;
- use File::Temp;
- use File::Spec;
- use Time::HiRes qw(usleep tv_interval);
- use Digest::CRC qw(crc32);
- my $esptool = ($^O eq 'MSWin32') ? 'esptool.exe' : 'esptool.py';
- $esptool = $ENV{'ESPTOOL'} || $esptool;
- my $FW_MAGIC = 0x7a07fbd6;
- my %datatypes = (
- 'end' => 0, # End of data
- 'data' => 1, # FPGA flash data
- 'target' => 2, # Firmware target string
- 'note' => 3, # Informative string
- 'espota' => 4, # ESP32 OTA image
- 'fpgainit' => 5, # FPGA bypass (transient) image during update
- 'esppart' => 6, # ESP32 partition table
- 'espsys' => 7, # ESP32 boot loader, OTA control partition...
- 'esptool' => 8 # esptool.py options for flashing
- );
- my @type;
- foreach my $t (keys(%datatypes)) {
- $type[$datatypes{$t}] = $t;
- }
- my $FDF_OPTIONAL = 0x0001;
- my $STRING_MAX_LEN = 4095;
- sub getint($) {
- my($s) = @_;
- return undef
- unless ($s =~ /^(([1-9][0-9]+)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
- my $o = oct($3) + $2;
- my $p = lc($5);
- if ($p eq 'k') {
- $o <<= 10;
- } elsif ($p eq 'm') {
- $o <<= 20;
- } elsif ($p eq 'g') {
- $o <<= 30;
- } elsif ($p eq 't') {
- $o <<= 40;
- } elsif ($p eq 'p') {
- $o <<= 50;
- } elsif ($p eq 'e') {
- $o <<= 60;
- }
- return $o;
- }
- sub filelen($) {
- my($f) = @_;
- my @s = stat($f);
- return $s[7];
- }
- sub unquote_cmd($) {
- my($s) = @_;
- my @a;
- $s =~ s/[\r\n]+/ /g;
- while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|(\S+))(\s.*)?$/) {
- push(@a, $1.$2);
- $s = $3;
- }
- return @a;
- }
- my @args = @ARGV;
- my $esponly = 0;
- my $file;
- while (1) {
- $file = shift(@args);
- last if ($file !~ /^\-/);
- if ($file eq '--esponly') {
- $esponly = 1;
- } elsif ($file eq '--') {
- $file = shift(@args);
- last;
- } else {
- undef $file; # Invalid argument, print usage
- last;
- }
- }
- my $port = shift(@args);
- if (!defined($port)) {
- die "Usage: $0 file.fw port [esptool options...]\n";
- }
- if (!File::Spec->file_name_is_absolute($port)) {
- if (-c "/dev/$port") {
- $port = "/dev/$port";
- } elsif (-c "/dev/tty$port") {
- $port = "/dev/tty$port";
- } elsif ($^O eq 'MSWin32') {
- $port = "\\\\.\\$port";
- } else {
- die "$0: no such serial port: $port\n";
- }
- }
- print STDERR "Using serial port device $port\n";
- open(my $fw, '<:gzip', $file)
- or die "$0: $file: $!\n";
- my @chunks = ();
- my $err = 0;
- my $hdr;
- while (read($fw, $hdr, 16) == 16) {
- # magic type flags data_len addr
- my @h = unpack('VvvVV', $hdr);
- my $c = { 'hdr' => $hdr, 'magic' => $h[0], 'type' => $h[1],
- 'flags' => $h[2], 'len' => $h[3], 'addr' => $h[4] };
- if ($c->{'magic'} != $FW_MAGIC) {
- print STDERR "$0: $file: bad chunk magic\n";
- $err = 1;
- last;
- }
- my $t = $type[$c->{'type'}];
- my $d;
- if (read($fw, $d, $c->{'len'}) != $c->{'len'}) {
- print STDERR "$0: $file: short chunk read\n";
- $err = 1;
- last;
- }
- $c->{'data'} = $d;
- push(@chunks, $c);
- last if ($t eq 'end'); # End of stream
- }
- close($fw);
- exit $err if ($err);
- my $td = File::Temp->newdir(CLEANUP => 1);
- my @espfiles = ();
- my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
- 'baud' => 115200, 'port' => undef, 'chip' => undef,
- 'flash_mode' => undef, 'flash_freq' => undef,
- 'flash_size' => undef);
- # Create a compressed data buffer without the ESP32 chunks
- my $fpgadata;
- open(my $fpgafh, '>:gzip', \$fpgadata) or die;
- my $nc = 0;
- foreach my $c ( @chunks ) {
- my $t = $type[$c->{'type'}];
- if ($t eq 'esptool') {
- my $s = $c->{'data'};
- $s =~ s/[\r\n]+/ /g;
- while ($s =~ /^\s*(\S+)\s+(\S+)(.*)/) {
- $espopt{$1} = $2;
- $s = $3;
- }
- } elsif ($t =~ /^esp/ && $c->{'addr'}) {
- my $addr = sprintf('%x', $c->{'addr'});
- my $tff = File::Spec->catfile($td, $t.$nc.'_'.$addr.'.bin');
- open(my $tf, '>', $tff) or die "$0: $tff: $!\n";
- print $tf $c->{'data'};
- close($tf);
- push(@espfiles, '0x'.$addr, $tff);
- } else {
- print $fpgafh $c->{'hdr'};
- print $fpgafh $c->{'data'};
- }
- $nc++;
- }
- close($fpgafh);
- foreach my $e (keys %espopt) {
- my $ev = $ENV{"ESP\U$e"};
- if (defined($ev)) {
- $espopt{$e} = $ev;
- }
- }
- $espopt{'port'} = $port;
- my @espcmd = unquote_cmd($esptool);
- foreach my $o (sort grep (!/^flash_/, keys %espopt)) {
- if (defined($espopt{$o})) {
- push(@espcmd, "--$o", $espopt{$o});
- }
- }
- push(@espcmd, 'write_flash', '-z');
- foreach my $o (sort grep (/^flash_/, keys %espopt)) {
- if (defined($espopt{$o})) {
- push(@espcmd, "--$o", $espopt{$o});
- }
- }
- push(@espcmd, @espfiles);
- print STDERR join(' ', @espcmd), "\n";
- $err = system(@espcmd);
- if ($err == -1) {
- print STDERR "$0: $espcmd[0]: $!\n";
- } elsif ($err) {
- print STDERR "$0: $espcmd[0]: exit $err\n";
- }
- exit $err if ($err || $esponly);
- my $SerialPort = eval {
- require Device::SerialPort;
- Device::SerialPort->import(qw(:PARAM));
- 'Device::SerialPort';
- } || eval {
- require Win32::SerialPort;
- Win32::SerialPort->import(qw(:STAT));
- 'Win32::SerialPort';
- } || die "$0: need Device::SerialPort or Win32::SerialPort\n";
- print STDERR "Waiting for reinit...\n";
- usleep(4000000);
- my $tty = $SerialPort->new($port);
- die "$0: $port: $!\n" if (!$tty);
- $tty->buffers($tty->buffer_max);
- $tty->reset_error;
- $tty->user_msg(1);
- $tty->error_msg(1);
- $tty->handshake('none');
- $tty->databits(8);
- $tty->baudrate(115200);
- $tty->parity('none');
- $tty->stopbits(1);
- $tty->datatype('raw');
- $tty->stty_ignbrk(0);
- #$tty->stty_brkint(0);
- $tty->stty_parmrk(0);
- $tty->stty_istrip(0);
- $tty->stty_inlcr(0);
- $tty->stty_igncr(0);
- $tty->stty_icrnl(0);
- $tty->stty_opost(0);
- $tty->stty_echo(0);
- $tty->stty_echonl(0);
- $tty->stty_icanon(0);
- $tty->stty_isig(0);
- #$tty->stty_iexten(0);
- $tty->read_char_time(0);
- $tty->write_settings;
- # In case DTR and/or RTS was asserted on the physical serial port.
- # Note that DTR needs to be deasserted before RTS!
- # However, deasserting DTR on the ACM port prevents it from working,
- # so only do this if we don't see CTS (which is always enabled on ACM)...
- if ($tty->can_modemlines && !($tty->modemlines & $tty->MS_CTS_ON)) {
- usleep(100000);
- $tty->dtr_active(0);
- usleep(100000);
- $tty->rts_active(0);
- usleep(100000);
- }
- sub tty_read {
- my($tty,$bufref,$timeout) = @_;
- my $d = $$bufref;
- if ($d eq '') {
- my $c;
- $tty->read_const_time($timeout);
- ($c,$d) = $tty->read(256);
- return '' if (!$c);
- }
- my $r = substr($d,0,1);
- $$bufref = substr($d,1);
- return $r;
- }
- my $ttybuf = '';
- sub tty_write($$) {
- my($tty,$data) = @_;
- my $bytes = length($data);
- my $offs = 0;
- while ($bytes) {
- my $cnt = $tty->write(substr($data,$offs,$bytes));
- usleep(10000) unless ($cnt);
- $offs += $cnt;
- $bytes -= $cnt;
- }
- return $offs;
- }
- my $found = 0;
- my $tt = time();
- my $start_enq = $tt;
- while (($tt = time()) - $start_enq < 30) {
- my $d = tty_read($tty, \$ttybuf, 1000);
- if ($d eq '') {
- tty_write($tty, "\005"); # ENQ
- } else {
- my $ix = index("\026\004\027", $d);
- if ($ix < 0) {
- print STDERR $d;
- next;
- }
- $found = $found == $ix ? $found+1 : 0;
- last if ($found == 3);
- }
- }
- if ($found < 3) {
- die "$0: $port: no MAX80 ESP32 detected\n";
- }
- $start_enq = $tt;
- my $winspc;
- while (!defined($winspc)) {
- $tt = time();
- if ($tt - $start_enq >= 10) {
- die "$0: $port: failed to start FPGA firmware upload\n";
- }
- tty_write($tty, "\034\001: /// MAX80 FW UPLOAD \~\@\~ \$\r\n\035");
- my $d;
- do {
- $d = tty_read($tty, \$ttybuf, 1000);
- if ($d eq "\036") {
- $winspc = 0;
- last;
- } else {
- print $d;
- }
- } while ($d ne '');
- }
- my $bytes = length($fpgadata);
- my $offset = 0;
- while ($offset < $bytes) {
- my $chunk = $bytes - $offset;
- $chunk = 64 if ($chunk > 64);
- $chunk = $winspc if ($chunk > $winspc);
- my $d = tty_read($tty, \$ttybuf, $chunk ? undef : 1000);
- if ($d ne '') {
- if ($d eq "\022") {
- $winspc = 0;
- } elsif ($d eq "\024") {
- $winspc += 256;
- } elsif ($d eq "\027" || $d eq "\030" || $d eq "\025") {
- die "$0: $port: upload terminated by MAX80\n";
- } elsif ($d ne "\006") {
- print STDERR $d;
- }
- }
- next unless($chunk);
- my $data = substr($fpgadata, $offset, $chunk);
- my $hdr = pack("CCvVV", 2, $chunk-1, 0, $offset, crc32($data));
- tty_write($tty, $hdr.$data);
- $offset += $chunk;
- $winspc -= $chunk;
- }
- tty_write($tty, "\004"); # EOT
- $tty->close;
- exit 0;
|