flashmax.pl 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. #!/usr/bin/perl
  2. use strict;
  3. use integer;
  4. use IO::Handle;
  5. use PerlIO::gzip;
  6. use File::Temp;
  7. use File::Spec;
  8. use File::HomeDir;
  9. use Time::HiRes qw(usleep tv_interval);
  10. use Digest::CRC qw(crc32);
  11. use v5.10; # For "state"
  12. my $esp_retries = 10;
  13. my @FW_MAGIC = (undef, 0x7a07fbd6, 0xa924ed0b);
  14. my %datatypes = (
  15. 'end' => 0, # End of data
  16. 'data' => 1, # FPGA flash data
  17. 'target' => 2, # Firmware target string
  18. 'note' => 3, # Informative string
  19. 'espota' => 4, # ESP32 OTA image
  20. 'fpgainit' => 5, # FPGA bypass (transient) image during update
  21. 'esppart' => 6, # ESP32 partition table
  22. 'espsys' => 7, # ESP32 boot loader, OTA control partition...
  23. 'esptool' => 8, # esptool options for flashing
  24. 'boardinfo' => 9 # board_info base address
  25. );
  26. my @type;
  27. foreach my $t (keys(%datatypes)) {
  28. $type[$datatypes{$t}] = $t;
  29. }
  30. my $FDF_OPTIONAL = 0x0001;
  31. my $FDF_PRETARGET = 0x0002;
  32. my $STRING_MAX_LEN = 4095;
  33. my $boardinfo_addr = 0;
  34. my $boardinfo_len = 4096;
  35. # For debug; DC1-4 replaced with functional names
  36. my @ascii = qw(NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
  37. DLE XON WRST XOFF WGO NAK SYN ETB CAN EM SUB ESC FS GS RS US);
  38. foreach my $i (9, 10, 13, 32..126) {
  39. $ascii[$i] = chr($i);
  40. }
  41. $ascii[127] = 'DEL';
  42. for (my $i = 128; $i < 256; $i++) {
  43. $ascii[$i] = sprintf("%02X", $i);
  44. }
  45. my @a = map { length($_) == 1 ? $_ : '<'.$_.'>' } @ascii;
  46. # Simple base64 encode using 3F-7E, bytewise bigendian
  47. sub mybaseencode {
  48. my $nbits = 0;
  49. my $bitbuf = 0;
  50. my $out = '';
  51. foreach my $s (@_) {
  52. foreach my $c (unpack('C*', $s)) {
  53. $nbits += 8;
  54. $bitbuf = ($bitbuf << 8) + $c;
  55. while ($nbits >= 6) {
  56. $nbits -= 6;
  57. $out .= pack('C', 63 + (($bitbuf >> $nbits) & 63));
  58. }
  59. }
  60. }
  61. if ($nbits) {
  62. $out .= pack('C', 63 + ($bitbuf & ((1 << $nbits) - 1)));
  63. }
  64. return $out;
  65. }
  66. sub getint($) {
  67. my($s) = @_;
  68. return undef
  69. unless ($s =~ /^(([1-9][0-9]+)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
  70. my $o = oct($3) + $2;
  71. my $p = lc($5);
  72. if ($p eq 'k') {
  73. $o <<= 10;
  74. } elsif ($p eq 'm') {
  75. $o <<= 20;
  76. } elsif ($p eq 'g') {
  77. $o <<= 30;
  78. } elsif ($p eq 't') {
  79. $o <<= 40;
  80. } elsif ($p eq 'p') {
  81. $o <<= 50;
  82. } elsif ($p eq 'e') {
  83. $o <<= 60;
  84. }
  85. return $o;
  86. }
  87. sub match_version($$) {
  88. my($ver,$pattern) = @_;
  89. return 1; # FIX THIS
  90. }
  91. sub filelen($) {
  92. my($f) = @_;
  93. my @s = stat($f);
  94. return $s[7];
  95. }
  96. sub unquote_cmd($) {
  97. my($s) = @_;
  98. my @a;
  99. $s =~ s/[\r\n]+/ /g;
  100. while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|([^\"]\S*))(\s.*)?$/) {
  101. my $a = $1;
  102. $a =~ s/\"\"/\"/g;
  103. push(@a, $a.$2);
  104. $s = $3;
  105. }
  106. return @a;
  107. }
  108. # Similar to grep, but for a hash; also filters out
  109. sub hgrep(&%) {
  110. my($mfunc, %hash) = @_;
  111. return map { $_ => $hash{$_} } grep(&$mfunc, keys %hash);
  112. }
  113. # Wrapper for running esptool, returns a hash with info output
  114. # or dies on failure
  115. sub hash2opt($)
  116. {
  117. my($h) = @_;
  118. return () unless (defined($h));
  119. return map { $h->{$_} ne '' ? ('--'.$_, $h->{$_}) : () } sort keys %{$h};
  120. }
  121. my $esptool;
  122. # Try running esptool --help and look for a version string
  123. sub try_esptool($) {
  124. my($cmd) = @_;
  125. return undef if ($cmd eq '');
  126. my @espcmd = unquote_cmd($cmd);
  127. # Make sure stderr is unbuffered
  128. STDERR->autoflush(1);
  129. open(my $old_stderr, '>&', \*STDERR) or die;
  130. $old_stderr->autoflush(1);
  131. open(STDERR, '>', File::Spec->devnull()) or return undef;
  132. STDERR->autoflush(1);
  133. my $ver;
  134. my $espok = open(my $esp, '-|', @espcmd, '--help');
  135. if ($espok) {
  136. while (defined(my $l = <$esp>)) {
  137. if (!defined($ver) &&
  138. $l =~ /\besptool\S*\s+(v\S+)/) {
  139. $ver = $1;
  140. }
  141. }
  142. close($esp);
  143. }
  144. open(STDERR, '>&', $old_stderr) or die;
  145. close($old_stderr);
  146. if ($ver) {
  147. $esptool = $cmd;
  148. }
  149. return $ver;
  150. }
  151. sub updir($) {
  152. my($dir) = @_;
  153. return File::Spec->catdir($dir, File::Spec->updir());
  154. }
  155. sub find_esptool() {
  156. my $ver;
  157. my $python = '"' . ($ENV{'PYTHON'} || 'python') . '"';
  158. # The easy variants
  159. foreach my $et ($ENV{'ESPTOOL'}, 'esptool', 'esptool.py') {
  160. next unless ($et ne '');
  161. $ver = try_esptool($et);
  162. return $ver if ($ver);
  163. }
  164. # More complicated
  165. foreach my $p ($ENV{'ESPTOOL'}, File::Spec->path()) {
  166. next unless ($p ne '');
  167. next unless ( -d $p );
  168. my $et = File::Spec->catfile($p, 'esptool.py');
  169. if ( -f $et ) {
  170. $ver = try_esptool("${python} \"${et}\"");
  171. return $ver if ($ver);
  172. }
  173. }
  174. # Try to find it in an Arduino directory
  175. foreach my $dp (File::HomeDir->my_data,
  176. File::HomeDir->my_documents,
  177. updir(File::HomeDir->my_data),
  178. File::HomeDir->my_home) {
  179. next unless ( -d $dp );
  180. foreach my $dn ('.arduino15', 'Arduino15', 'ArduinoData') {
  181. my $etr = File::Spec->catdir($dp, $dn,
  182. qw(packages esp32 tools esptool_py));
  183. opendir(my $dh, $etr) or next;
  184. while (defined(my $sd = readdir($dh))) {
  185. next if ($sd eq File::Spec->curdir);
  186. next if ($sd eq File::Spec->updir);
  187. my $etd = File::Spec->catdir($etr, $sd);
  188. next unless ( -d $etd );
  189. my $esppath = File::Spec->catfile($etd, 'esptool');
  190. if ( ! -d $esppath ) {
  191. $ver = try_esptool("\"$esppath\"");
  192. last if ($ver);
  193. }
  194. $esppath .= '.py';
  195. if ( -f $esppath ) {
  196. $ver = try_esptool("${python} \"$esppath\"");
  197. last if ($ver);
  198. }
  199. }
  200. closedir($dh);
  201. return $ver if ($ver);
  202. }
  203. }
  204. return $ver;
  205. }
  206. sub run_esptool($$$$@)
  207. {
  208. my($port,$common_options,$cmd,$cmd_options,@args) = @_;
  209. my @espcmd = unquote_cmd($esptool);
  210. push(@espcmd, '--port', $port);
  211. push(@espcmd, hash2opt($common_options));
  212. push(@espcmd, unquote_cmd($cmd));
  213. push(@espcmd, hash2opt($cmd_options));
  214. push(@espcmd, @args);
  215. my $retries = $esp_retries;
  216. my $ok = 0;
  217. my %outinfo;
  218. my @output;
  219. while (!$ok && $retries--) {
  220. %outinfo = ();
  221. @output = ();
  222. print STDERR 'Command: ', join(' ', @espcmd), "\n";
  223. print STDERR "Running $espcmd[0] $cmd... ";
  224. my $esp;
  225. if (!open($esp, '-|', @espcmd)) {
  226. print STDERR $!, "\n";
  227. exit 1 if (defined($port));
  228. return undef;
  229. }
  230. while (defined(my $l = <$esp>)) {
  231. if ($l =~ /^Chip is (\S+)/) {
  232. $outinfo{'chip'} = $1;
  233. } elsif ($l =~ /^MAC: ([0-9a-f:]+)/) {
  234. $outinfo{'mac'} = $1;
  235. }
  236. push(@output, $l);
  237. }
  238. $ok = close($esp);
  239. if ($ok) {
  240. print STDERR "ok\n";
  241. last;
  242. } elsif ($retries) {
  243. @output = ();
  244. print STDERR "failed, retrying\n";
  245. usleep(1000000);
  246. } else {
  247. print STDERR "failed, giving up\n";
  248. }
  249. }
  250. print STDERR @output;
  251. if (!$ok) {
  252. if (defined($port)) {
  253. die "$0: $espcmd[0] $cmd failed\n";
  254. }
  255. return undef;
  256. }
  257. return %outinfo;
  258. }
  259. sub target_string_valid($)
  260. {
  261. my($v) = @_;
  262. return $v =~ /^(\S+) v((?:0|[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*)*)(?: ([a-zA-Z0-9]*))?$/;
  263. }
  264. sub match_version($$)
  265. {
  266. my($version,$pattern) = @_;
  267. my @vv = target_string_valid($version);
  268. return 0 unless (defined($vv[0]));
  269. my $v_board = $vv[0];
  270. my @v_ver = split(/\./, $vv[1]);
  271. my $v_flags = $vv[2];
  272. return 1 if ($pattern eq $v_board); # Board only matchall pattern
  273. if ($pattern !~ /^(\S+) v((?:0|[1-9][0-9]*)(?:\.\.0|\.[1-9][0-9]*)*)(?:(\-)((?:0|[1-9][0-9]*)(?:\.\.0|\.[1-9][0-9]*)*))?(?: ([\+\-a-zA-Z0-9]*))?$/) {
  274. return 0;
  275. }
  276. return 0 if ($1 ne $v_board);
  277. my @p_min = split(/\./, $2);
  278. my @p_max = split(/\./, $3 eq '' ? $2 : $4);
  279. my $p_flags = $5;
  280. while (scalar(@p_min) || scalar(@p_max)) {
  281. my $mi = shift(@p_min);
  282. my $ma = shift(@p_max);
  283. my $ve = shift(@v_ver) || 0;
  284. return 0 if (defined($mi) && $ve < $mi);
  285. return 0 if (defined($ma) && $ve > $ma);
  286. }
  287. my $flag_pol = 1;
  288. foreach my $c (split(//, $p_flags)) {
  289. if ($c eq '-') {
  290. $flag_pol = 0;
  291. } elsif ($c eq '+') {
  292. $flag_pol = 1;
  293. } else {
  294. return 0 if ((index($v_flags, $c) != -1) != $flag_pol);
  295. }
  296. }
  297. return 1;
  298. }
  299. my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
  300. 'connect-attempts' => 8);
  301. my $tmpdir;
  302. # Get a filename in $tmpdir
  303. sub tmpfilename($) {
  304. my($filename) = @_;
  305. if (!defined($tmpdir)) {
  306. die "$0: tmpfilename() called before tmpdir exists\n";
  307. }
  308. return File::Spec->catfile($tmpdir, $filename);
  309. }
  310. sub get_target_board($)
  311. {
  312. my($port) = @_;
  313. my $boardinfo = tmpfilename('boardinfo.bin');
  314. my %myespopt = (%espopt);
  315. $myespopt{'after'} = 'no_reset';
  316. run_esptool($port, { hgrep {!/^flash_/} \%myespopt },
  317. 'read_flash', { },
  318. sprintf('0x%x', $boardinfo_addr),
  319. $boardinfo_len, $boardinfo);
  320. open(my $bi, '<:raw', $boardinfo) or return undef;
  321. my $bid;
  322. read($bi, $bid, $boardinfo_len);
  323. close($bi);
  324. unlink($boardinfo);
  325. return undef if (length($bid) != $boardinfo_len);
  326. my @bh = unpack('VVVVZ[256]', $bid);
  327. if ($bh[0] == 0x6682df97 && $bh[1] == 0xe2a0d506) {
  328. # Standard format board_info structure
  329. substr($bid, 12, 4) = "\0\0\0\0"; # Clear the CRC field
  330. if ($bh[2] >= 16 && $bh[2] <= $boardinfo_len &&
  331. crc32(substr($bid, 0, $bh[2])) == $bh[3]) {
  332. return $bh[4];
  333. }
  334. } elsif ($bid =~ /^([[:print:]]+)\0/) {
  335. # Preliminary board_info (only version string)
  336. return $1;
  337. }
  338. return undef;
  339. }
  340. sub read_fwfile($$) {
  341. my($file, $target_board) = @_;
  342. return undef if (!defined($file));
  343. open(my $fw, '<:gzip', $file)
  344. or die "$0: $file: $!\n";
  345. my @chunks = ();
  346. my $version_match = undef;
  347. my $err = 0;
  348. my $hdr;
  349. while (read($fw, $hdr, 16) == 16) {
  350. # magic type flags data_len addr
  351. my @h = unpack('VvvVV', $hdr);
  352. my $c = { 'hdr' => $hdr, 'magic' => $h[0], 'type' => $h[1],
  353. 'flags' => $h[2], 'len' => $h[3], 'addr' => $h[4],
  354. 'vmatch' => 0, 'vmask' => 0, 'vmin' => 0, 'vmax' => 0xffff };
  355. if ($c->{'magic'} == $FW_MAGIC[2]) {
  356. if (read($fw, $hdr, 16) != 16) {
  357. print STDERR "$0: $file: short header read\n";
  358. $err = 1;
  359. last;
  360. }
  361. my @h = unpack('VVvvV', $hdr);
  362. $c->{'hdr'} .= $hdr;
  363. $c->{'vmatch'} = $h[0];
  364. $c->{'vmask'} = $h[1];
  365. $c->{'vmin'} = $h[2];
  366. $c->{'vmax'} = $h[3];
  367. } elsif ($c->{'magic'} != $FW_MAGIC[1]) {
  368. print STDERR "$0: $file: bad chunk magic\n";
  369. $err = 1;
  370. last;
  371. }
  372. my $t = $type[$c->{'type'}];
  373. my $d;
  374. if (read($fw, $d, $c->{'len'}) != $c->{'len'}) {
  375. print STDERR "$0: $file: short chunk read\n";
  376. $err = 1;
  377. last;
  378. }
  379. $c->{'data'} = $d;
  380. if ($t eq 'target') {
  381. my $is_match = match_version($target_board, $d);
  382. if ($c->{'magic'} == $FW_MAGIC[1] || $is_match) {
  383. $version_match = $c;
  384. }
  385. print STDERR "$0: $file: supports hardware: $d",
  386. ($is_match ? ' (match)' : ''), "\n";
  387. } elsif ($t eq 'end' || $t eq 'note' || ($c->{'flags'} & $FDF_PRETARGET)) {
  388. # Not filtered
  389. } else {
  390. if (!defined($version_match)) {
  391. print STDERR "$0: $file: hardware version $target_board not supported\n";
  392. $err = 1;
  393. last;
  394. }
  395. if ($c->{'vmin'} > $version_match->{'vmax'} ||
  396. $c->{'vmax'} < $version_match->{'vmin'} ||
  397. (($c->{'vmatch'} ^ $version_match->{'vmatch'}) & $c->{'vmask'})) {
  398. # Not applicable to this board
  399. next;
  400. }
  401. }
  402. push(@chunks, $c);
  403. last if ($t eq 'end'); # End of stream
  404. }
  405. close($fw);
  406. exit 1 if ($err);
  407. # Sort out the ESP chunks as files for esptool versus the ones
  408. # that should be uploaded once the ESP software is running
  409. my @espfiles;
  410. my $raw_fpgadata;
  411. my $nc = 0;
  412. foreach my $c ( @chunks ) {
  413. my $t = $type[$c->{'type'}];
  414. if ($t eq 'esptool') {
  415. my $s = $c->{'data'};
  416. $s =~ s/[\r\n]+/ /g;
  417. while ($s =~ /^\s*(\S+)\s+(\S+)(.*)/) {
  418. $espopt{$1} = $2;
  419. $s = $3;
  420. }
  421. } elsif ($t =~ /^esp/ && $c->{'addr'}) {
  422. my $addr = sprintf('%x', $c->{'addr'});
  423. my $tff = tmpfilename($t.$nc.'_'.$addr.'.bin');
  424. open(my $tf, '>', $tff) or die "$0: $tff: $!\n";
  425. print $tf $c->{'data'};
  426. close($tf);
  427. push(@espfiles, [$c->{'addr'}, $tff]);
  428. } elsif ($t eq 'note' || ($t eq 'target' && $c != $version_match)) {
  429. # Skip
  430. } else {
  431. $raw_fpgadata .= $c->{'hdr'};
  432. $raw_fpgadata .= $c->{'data'};
  433. }
  434. $nc++;
  435. }
  436. my $fpgadata;
  437. if (defined($raw_fpgadata)) {
  438. open(my $fpgafh, '>:gzip', \$fpgadata) or die;
  439. print $fpgafh $raw_fpgadata;
  440. close($fpgafh);
  441. }
  442. return ([@espfiles], $fpgadata);
  443. }
  444. sub tty_read($$$;$) {
  445. state %old_timeout;
  446. my($tty,$bufref,$timeout,$count) = @_;
  447. $count = 1 unless (defined($count));
  448. if ($$bufref eq '' || !$count) {
  449. if (!defined($old_timeout{$tty}) || $timeout != $old_timeout{$tty}) {
  450. $tty->read_const_time($timeout);
  451. $old_timeout{$tty} = $timeout;
  452. }
  453. my($c,$d) = $tty->read(256);
  454. $$bufref .= $d;
  455. }
  456. my $r = substr($$bufref,0,$count);
  457. $$bufref = substr($$bufref,$count);
  458. return $r;
  459. }
  460. sub tty_write($$$) {
  461. my($tty,$bufref,$data) = @_;
  462. my $bytes = length($data);
  463. my $offs = 0;
  464. while ($bytes) {
  465. my $cnt = $tty->write(substr($data,$offs,$bytes));
  466. tty_read($tty, $bufref, 100, 0) unless ($cnt);
  467. $offs += $cnt;
  468. $bytes -= $cnt;
  469. }
  470. return $offs;
  471. }
  472. sub upload_fpgadata($$) {
  473. use bytes;
  474. my($port, $fpgadata) = @_;
  475. my $SOH = 001;
  476. my $STX = 002;
  477. my $ETX = 003;
  478. my $EOT = 004;
  479. my $ENQ = 005;
  480. my $ACK = 006;
  481. my $XON = 021; # A.k.a. DC1
  482. my $WRST = 022; # DC2: window = 0
  483. my $XOFF = 023; # A.k.a. DC3
  484. my $WGO = 024; # DC4: window += 256 bytes
  485. my $NAK = 025;
  486. my $SYN = 026;
  487. my $ETB = 027;
  488. my $CAN = 030;
  489. my $EM = 031; # Packet offset too high
  490. my $FS = 034;
  491. my $GS = 035;
  492. my $RS = 036;
  493. my $US = 037;
  494. my $WGO_CHUNK = 256; # Each WGO = 256 bytes
  495. my $SerialPort = eval {
  496. require Device::SerialPort;
  497. Device::SerialPort->import(qw(:PARAM));
  498. 'Device::SerialPort';
  499. } || eval {
  500. require Win32::SerialPort;
  501. Win32::SerialPort->import(qw(:STAT));
  502. 'Win32::SerialPort';
  503. } || die "$0: need Device::SerialPort (Unix/MacOS) or Win32::SerialPort (Win32)\n";
  504. print STDERR "Waiting for reinit...\n";
  505. usleep(4000000);
  506. my $tty = $SerialPort->new($port);
  507. die "$0: $port: $!\n" if (!$tty);
  508. $tty->buffers($tty->buffer_max);
  509. $tty->reset_error;
  510. $tty->user_msg(1);
  511. $tty->error_msg(1);
  512. $tty->handshake('none');
  513. $tty->databits(8);
  514. $tty->baudrate(115200);
  515. $tty->stopbits(1);
  516. $tty->parity('none');
  517. $tty->stty_istrip(0);
  518. $tty->stty_inpck(0);
  519. $tty->datatype('raw');
  520. $tty->stty_icanon(0);
  521. $tty->stty_opost(0);
  522. $tty->stty_ignbrk(0);
  523. $tty->stty_inlcr(0);
  524. $tty->stty_igncr(0);
  525. $tty->stty_icrnl(0);
  526. $tty->stty_echo(0);
  527. $tty->stty_echonl(0);
  528. $tty->stty_isig(0);
  529. $tty->read_char_time(0);
  530. $tty->write_settings;
  531. # In case DTR and/or RTS was asserted on the physical serial port.
  532. # Note that DTR needs to be deasserted before RTS!
  533. # However, deasserting DTR on the ACM port prevents it from working,
  534. # so only do this if we don't see CTS (which is always enabled on ACM)...
  535. if ($tty->can_modemlines && !($tty->modemlines & $tty->MS_CTS_ON)) {
  536. usleep(100000);
  537. $tty->dtr_active(0);
  538. usleep(100000);
  539. $tty->rts_active(0);
  540. usleep(100000);
  541. }
  542. my $ttybuf = '';
  543. my $found = 0;
  544. my $tt = time();
  545. my $start_enq = $tt;
  546. tty_write($tty, \$ttybuf, "\005"); # ENQ
  547. while (($tt = time()) - $start_enq < 30) {
  548. my $d = tty_read($tty, \$ttybuf, 1000);
  549. if ($d eq '') {
  550. tty_write($tty, \$ttybuf, chr($ENQ));
  551. } else {
  552. my $ix = index("\026\004\027", $d); # SYN EOT ETB
  553. if ($ix < 0) {
  554. print STDERR $d;
  555. next;
  556. }
  557. $found = $found == $ix ? $found+1 : 0;
  558. last if ($found == 3);
  559. }
  560. }
  561. if ($found < 3) {
  562. die "$0: $port: no MAX80 ESP32 detected\n";
  563. }
  564. $start_enq = $tt;
  565. my $last_req;
  566. my $winspc;
  567. while (!defined($winspc)) {
  568. $tt = time();
  569. if ($tt - $start_enq >= 10) {
  570. die "$0: $port: failed to start FPGA firmware upload\n";
  571. }
  572. if ($tt != $last_req) {
  573. # FS SOH <string> GS
  574. tty_write($tty, \$ttybuf,
  575. "\034\001: /// MAX80 FW UPLOAD \~\@\~ \$\r\n\035");
  576. $last_req = $tt;
  577. }
  578. my $d;
  579. while (1) {
  580. $d = tty_read($tty, \$ttybuf, 100);
  581. last if ($d eq '');
  582. my $dc = unpack('C', $d);
  583. print STDERR $a[$dc];
  584. if ($dc == $RS) {
  585. $winspc = 0;
  586. last;
  587. }
  588. }
  589. }
  590. my $bytes = length($fpgadata);
  591. my $offset = 0;
  592. my $maxchunk = 64;
  593. my $maxahead = 256;
  594. my $last_ack = 0;
  595. my @pktends = ();
  596. my $last_enq = 0;
  597. my $last_ack_time = 0;
  598. print STDERR "\nStarting packet transmit...\n";
  599. while ($last_ack < $bytes) {
  600. my $chunk;
  601. my $now;
  602. while (1) {
  603. $chunk = $bytes - $offset;
  604. $chunk = $winspc if ($chunk > $winspc);
  605. if ($offset + $chunk > $last_ack + $maxahead) {
  606. $chunk = $last_ack + $maxahead - $offset;
  607. }
  608. $chunk = 0 if ($chunk <= 0);
  609. $chunk = $maxchunk if ($chunk > $maxchunk);
  610. my $d = tty_read($tty, \$ttybuf, $chunk ? 0 : $maxchunk/10);
  611. $now = time();
  612. last if ($d eq '');
  613. my $dc = unpack('C', $d);
  614. if ($dc == $WRST) {
  615. $winspc = 0;
  616. } elsif ($dc == $WGO) {
  617. if (defined($winspc)) {
  618. $winspc += $WGO_CHUNK;
  619. }
  620. } elsif ($dc == $ACK) {
  621. $last_ack = shift(@pktends) || $last_ack;
  622. if ($now != $last_ack_time) {
  623. printf STDERR "%s: %s: %d/%d (%d%%) uploaded\n",
  624. $0, $port, $last_ack, $bytes, $last_ack*100/$bytes;
  625. $last_ack_time = $now;
  626. }
  627. } else {
  628. print STDERR $a[$dc];
  629. if ($dc == $NAK || $dc == $EM || $dc == $US) {
  630. $offset = $last_ack;
  631. @pktends = ();
  632. undef $winspc; # Wait for WRST before resuming
  633. } elsif ($dc == $CAN) {
  634. print STDERR "\n";
  635. die "$0: $port: upload aborted by target system\n";
  636. }
  637. }
  638. }
  639. if (!$chunk) {
  640. if ($bytes > $offset) {
  641. if ($now != $last_enq) {
  642. # SYN: request window resync
  643. tty_write($tty, \$ttybuf, chr($SYN));
  644. $last_enq = $now;
  645. }
  646. }
  647. next;
  648. }
  649. my $data = substr($fpgadata, $offset, $chunk);
  650. my $hdr = pack("CvVV", $chunk-1, 0, $offset, crc32($data));
  651. tty_write($tty, \$ttybuf, chr($STX).mybaseencode($hdr, $data));
  652. push(@pktends, $offset + $chunk);
  653. $offset += $chunk;
  654. $winspc -= $chunk;
  655. }
  656. tty_write($tty, \$ttybuf, chr($EOT));
  657. # Final messages out
  658. while (1) {
  659. my $d = tty_read($tty, \$ttybuf, 1000);
  660. last if ($d eq '');
  661. print STDERR $d;
  662. }
  663. $tty->close;
  664. return 0;
  665. }
  666. my @args = @ARGV;
  667. my $esponly = 0;
  668. my $fpgaonly = 0;
  669. my $file;
  670. my $target_board = undef;
  671. my $setver = 0;
  672. my $getver = 0;
  673. my $port = undef;
  674. my $which = 0;
  675. while (1) {
  676. $file = shift(@args);
  677. last if ($file !~ /^\-/);
  678. if ($file eq '--esponly') {
  679. $esponly = 1;
  680. } elsif ($file eq '--fpgaonly') {
  681. $fpgaonly = 1;
  682. } elsif ($file eq '--which') {
  683. $which = 1;
  684. } elsif ($file eq '--setver') {
  685. $target_board = shift(@args);
  686. $setver = defined($target_board);
  687. } elsif ($file eq '--getver') {
  688. $getver = 1;
  689. } elsif ($file eq '--port') {
  690. $port = shift(@args);
  691. } elsif ($file eq '--') {
  692. $file = shift(@args);
  693. last;
  694. } else {
  695. undef $file; # Invalid argument, print usage
  696. last;
  697. }
  698. }
  699. %espopt = (%espopt,
  700. 'baud' => 115200, 'port' => undef, 'chip' => undef,
  701. 'flash_mode' => undef, 'flash_freq' => undef,
  702. 'flash_size' => undef);
  703. foreach my $e (keys %espopt) {
  704. my $ev = $ENV{"ESP\U$e"};
  705. if (defined($ev)) {
  706. $espopt{$e} = $ev;
  707. }
  708. }
  709. while (defined(my $eo = shift(@args))) {
  710. if ($eo =~ /^([^=]+)=(.*)$/) {
  711. $espopt{$1} = $2;
  712. } elsif ($args[0] !~ /^-/) {
  713. $espopt{$eo} = shift(@args);
  714. } else {
  715. $espopt{$eo} = '';
  716. }
  717. }
  718. if (defined($espopt{'port'})) {
  719. $port = $espopt{'port'} unless (defined($port));
  720. delete $espopt{'port'};
  721. }
  722. # Legacy command line support: if no --port, specify port after firmware file
  723. if (defined($file) && !defined($port) && $args[0] ne '--esptool') {
  724. $port = shift(@args);
  725. }
  726. $tmpdir = File::Temp->newdir(CLEANUP => 1);
  727. if (!defined($tmpdir)) {
  728. die "$0: failed to create temporary directory: $!\n"
  729. }
  730. my $espver = find_esptool();
  731. if (!$espver) {
  732. die "$0: cannot find esptool, please set ESPTOOL in the environment\n";
  733. }
  734. if ($which) {
  735. print "esptool ${espver}\n";
  736. print $esptool, "\n";
  737. exit 0;
  738. }
  739. print STDERR "esptool ${espver} found\n\n";
  740. if (!defined($port)) {
  741. die "Usage: $0 [options] [file.fw] [--esptool esptool_options...]\n".
  742. " Options:\n".
  743. " --which print the esptool command discovered, if any\n".
  744. " --setver <version> set the board version string\n".
  745. " --getver get the board version string, then exit\n".
  746. " --esponly flash ESP32 only\n".
  747. " --fpgaonly flash the FPGA only\n".
  748. " --port serial port to use\n";
  749. }
  750. if (! -c $port && !File::Spec->file_name_is_absolute($port)) {
  751. if (-c "/dev/$port") {
  752. $port = "/dev/$port";
  753. } elsif (-c "/dev/tty$port") {
  754. $port = "/dev/tty$port";
  755. } elsif ($^O eq 'MSWin32') {
  756. $port = "\\\\.\\$port";
  757. } else {
  758. die "$0: no such serial port: $port\n";
  759. }
  760. }
  761. print STDERR "$0: using serial port device $port\n";
  762. my @espfiles = ();
  763. if (defined($target_board)) {
  764. # Forcibly write target board version
  765. if (!target_string_valid($target_board)) {
  766. die "$0: $port: invalid firmware target string: $target_board\n";
  767. }
  768. my $boardinfo = tmpfilename('boardinfo.bin');
  769. open(my $bi, '>:raw', $boardinfo)
  770. or die "$0: $port: $boardinfo: $!\n";
  771. my $bid = $target_board . "\0";
  772. $bid .= "\xff" x ($boardinfo_len - length($bid));
  773. print $bi $bid;
  774. close($bi);
  775. push(@espfiles, [$boardinfo_addr, $boardinfo]);
  776. } else {
  777. # Get the board version from target flash
  778. $target_board = get_target_board($port);
  779. if (!defined($target_board) || !target_string_valid($target_board)) {
  780. die "$0: $port: board version not programmed, specify with --setver\n";
  781. }
  782. }
  783. if ($getver) {
  784. print "$0: $port: board version: $target_board\n";
  785. exit 0;
  786. }
  787. my $fpgadata;
  788. if (defined($file)) {
  789. my $fwfiles;
  790. ($fwfiles, $fpgadata) = read_fwfile($file, $target_board);
  791. push(@espfiles, @$fwfiles) if (defined($fwfiles));
  792. }
  793. if (!$fpgaonly && scalar(@espfiles)) {
  794. run_esptool($port, { hgrep {!/^flash_/} %espopt },
  795. 'write_flash', { hgrep {/^flash_/} %espopt },
  796. '-z', map { (sprintf('0x%x', $_->[0]), $_->[1]) } @espfiles);
  797. }
  798. if (!$esponly && defined($fpgadata)) {
  799. upload_fpgadata($port, $fpgadata);
  800. }
  801. print STDERR "\n$0: $port: update complete\n";
  802. exit 0;