flashesp.pl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. #!/usr/bin/perl
  2. use strict;
  3. use integer;
  4. use PerlIO::gzip;
  5. use File::Temp;
  6. use File::Spec;
  7. use Time::HiRes qw(usleep tv_interval);
  8. use Digest::CRC qw(crc32);
  9. use v5.10; # For "state"
  10. my $esptool = ($^O eq 'MSWin32') ? 'esptool.exe' : 'esptool.py';
  11. $esptool = $ENV{'ESPTOOL'} || $esptool;
  12. my $FW_MAGIC = 0x7a07fbd6;
  13. my %datatypes = (
  14. 'end' => 0, # End of data
  15. 'data' => 1, # FPGA flash data
  16. 'target' => 2, # Firmware target string
  17. 'note' => 3, # Informative string
  18. 'espota' => 4, # ESP32 OTA image
  19. 'fpgainit' => 5, # FPGA bypass (transient) image during update
  20. 'esppart' => 6, # ESP32 partition table
  21. 'espsys' => 7, # ESP32 boot loader, OTA control partition...
  22. 'esptool' => 8 # esptool.py options for flashing
  23. );
  24. my @type;
  25. foreach my $t (keys(%datatypes)) {
  26. $type[$datatypes{$t}] = $t;
  27. }
  28. my $FDF_OPTIONAL = 0x0001;
  29. my $STRING_MAX_LEN = 4095;
  30. # For debug; DC1-4 replaced with functional names
  31. my @ascii = qw(NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
  32. DLE XON WRST XOFF WGO NAK SYN ETB CAN EM SUB ESC FS GS RS US);
  33. foreach my $i (9, 10, 13, 32..126) {
  34. $ascii[$i] = chr($i);
  35. }
  36. $ascii[127] = 'DEL';
  37. for (my $i = 128; $i < 256; $i++) {
  38. $ascii[$i] = sprintf("%02X", $i);
  39. }
  40. my @a = map { length($_) == 1 ? $_ : '<'.$_.'>' } @ascii;
  41. # Simple base64 encode using 3F-7E, bytewise bigendian
  42. sub mybaseencode {
  43. my $nbits = 0;
  44. my $bitbuf = 0;
  45. my $out = '';
  46. foreach my $s (@_) {
  47. foreach my $c (unpack('C*', $s)) {
  48. $nbits += 8;
  49. $bitbuf = ($bitbuf << 8) + $c;
  50. while ($nbits >= 6) {
  51. $nbits -= 6;
  52. $out .= pack('C', 63 + (($bitbuf >> $nbits) & 63));
  53. }
  54. }
  55. }
  56. if ($nbits) {
  57. $out .= pack('C', 63 + ($bitbuf & ((1 << $nbits) - 1)));
  58. }
  59. return $out;
  60. }
  61. sub getint($) {
  62. my($s) = @_;
  63. return undef
  64. unless ($s =~ /^(([1-9][0-9]+)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
  65. my $o = oct($3) + $2;
  66. my $p = lc($5);
  67. if ($p eq 'k') {
  68. $o <<= 10;
  69. } elsif ($p eq 'm') {
  70. $o <<= 20;
  71. } elsif ($p eq 'g') {
  72. $o <<= 30;
  73. } elsif ($p eq 't') {
  74. $o <<= 40;
  75. } elsif ($p eq 'p') {
  76. $o <<= 50;
  77. } elsif ($p eq 'e') {
  78. $o <<= 60;
  79. }
  80. return $o;
  81. }
  82. sub filelen($) {
  83. my($f) = @_;
  84. my @s = stat($f);
  85. return $s[7];
  86. }
  87. sub unquote_cmd($) {
  88. my($s) = @_;
  89. my @a;
  90. $s =~ s/[\r\n]+/ /g;
  91. while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|(\S+))(\s.*)?$/) {
  92. push(@a, $1.$2);
  93. $s = $3;
  94. }
  95. return @a;
  96. }
  97. my @args = @ARGV;
  98. my $esponly = 0;
  99. my $file;
  100. while (1) {
  101. $file = shift(@args);
  102. last if ($file !~ /^\-/);
  103. if ($file eq '--esponly') {
  104. $esponly = 1;
  105. } elsif ($file eq '--') {
  106. $file = shift(@args);
  107. last;
  108. } else {
  109. undef $file; # Invalid argument, print usage
  110. last;
  111. }
  112. }
  113. my $port = shift(@args);
  114. if (!defined($port)) {
  115. die "Usage: $0 file.fw port [esptool options...]\n";
  116. }
  117. if (!File::Spec->file_name_is_absolute($port)) {
  118. if (-c "/dev/$port") {
  119. $port = "/dev/$port";
  120. } elsif (-c "/dev/tty$port") {
  121. $port = "/dev/tty$port";
  122. } elsif ($^O eq 'MSWin32') {
  123. $port = "\\\\.\\$port";
  124. } else {
  125. die "$0: no such serial port: $port\n";
  126. }
  127. }
  128. print STDERR "Using serial port device $port\n";
  129. open(my $fw, '<:gzip', $file)
  130. or die "$0: $file: $!\n";
  131. my @chunks = ();
  132. my $err = 0;
  133. my $hdr;
  134. while (read($fw, $hdr, 16) == 16) {
  135. # magic type flags data_len addr
  136. my @h = unpack('VvvVV', $hdr);
  137. my $c = { 'hdr' => $hdr, 'magic' => $h[0], 'type' => $h[1],
  138. 'flags' => $h[2], 'len' => $h[3], 'addr' => $h[4] };
  139. if ($c->{'magic'} != $FW_MAGIC) {
  140. print STDERR "$0: $file: bad chunk magic\n";
  141. $err = 1;
  142. last;
  143. }
  144. my $t = $type[$c->{'type'}];
  145. my $d;
  146. if (read($fw, $d, $c->{'len'}) != $c->{'len'}) {
  147. print STDERR "$0: $file: short chunk read\n";
  148. $err = 1;
  149. last;
  150. }
  151. $c->{'data'} = $d;
  152. push(@chunks, $c);
  153. last if ($t eq 'end'); # End of stream
  154. }
  155. close($fw);
  156. exit $err if ($err);
  157. my $td = File::Temp->newdir(CLEANUP => 1);
  158. my @espfiles = ();
  159. my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
  160. 'baud' => 115200, 'port' => undef, 'chip' => undef,
  161. 'flash_mode' => undef, 'flash_freq' => undef,
  162. 'flash_size' => undef);
  163. # Create a compressed data buffer without the ESP32 chunks
  164. my $fpgadata;
  165. open(my $fpgafh, '>:gzip', \$fpgadata) or die;
  166. my $nc = 0;
  167. foreach my $c ( @chunks ) {
  168. my $t = $type[$c->{'type'}];
  169. if ($t eq 'esptool') {
  170. my $s = $c->{'data'};
  171. $s =~ s/[\r\n]+/ /g;
  172. while ($s =~ /^\s*(\S+)\s+(\S+)(.*)/) {
  173. $espopt{$1} = $2;
  174. $s = $3;
  175. }
  176. } elsif ($t =~ /^esp/ && $c->{'addr'}) {
  177. my $addr = sprintf('%x', $c->{'addr'});
  178. my $tff = File::Spec->catfile($td, $t.$nc.'_'.$addr.'.bin');
  179. open(my $tf, '>', $tff) or die "$0: $tff: $!\n";
  180. print $tf $c->{'data'};
  181. close($tf);
  182. push(@espfiles, '0x'.$addr, $tff);
  183. } else {
  184. print $fpgafh $c->{'hdr'};
  185. print $fpgafh $c->{'data'};
  186. }
  187. $nc++;
  188. }
  189. close($fpgafh);
  190. foreach my $e (keys %espopt) {
  191. my $ev = $ENV{"ESP\U$e"};
  192. if (defined($ev)) {
  193. $espopt{$e} = $ev;
  194. }
  195. }
  196. $espopt{'port'} = $port;
  197. my @espcmd = unquote_cmd($esptool);
  198. foreach my $o (sort grep (!/^flash_/, keys %espopt)) {
  199. if (defined($espopt{$o})) {
  200. push(@espcmd, "--$o", $espopt{$o});
  201. }
  202. }
  203. push(@espcmd, 'write_flash', '-z');
  204. foreach my $o (sort grep (/^flash_/, keys %espopt)) {
  205. if (defined($espopt{$o})) {
  206. push(@espcmd, "--$o", $espopt{$o});
  207. }
  208. }
  209. push(@espcmd, @espfiles);
  210. print STDERR join(' ', @espcmd), "\n";
  211. my $retries = 4;
  212. my $err = 0;
  213. while ($retries--) {
  214. # Sometimes esptool tanks out for no reason, with error 256
  215. $err = system(@espcmd);
  216. last if ($err != 256);
  217. usleep(1000000);
  218. }
  219. if ($err == -1) {
  220. print STDERR "$0: $espcmd[0]: $!\n";
  221. } elsif ($err) {
  222. print STDERR "$0: $espcmd[0]: exit $err\n";
  223. }
  224. exit $err if ($err || $esponly);
  225. my $SerialPort = eval {
  226. require Device::SerialPort;
  227. Device::SerialPort->import(qw(:PARAM));
  228. 'Device::SerialPort';
  229. } || eval {
  230. require Win32::SerialPort;
  231. Win32::SerialPort->import(qw(:STAT));
  232. 'Win32::SerialPort';
  233. } || die "$0: need Device::SerialPort or Win32::SerialPort\n";
  234. print STDERR "Waiting for reinit...\n";
  235. usleep(4000000);
  236. my $tty = $SerialPort->new($port);
  237. die "$0: $port: $!\n" if (!$tty);
  238. $tty->buffers($tty->buffer_max);
  239. $tty->reset_error;
  240. $tty->user_msg(1);
  241. $tty->error_msg(1);
  242. $tty->handshake('none');
  243. $tty->databits(8);
  244. $tty->baudrate(115200);
  245. $tty->stopbits(1);
  246. $tty->parity('none');
  247. $tty->stty_istrip(0);
  248. $tty->stty_inpck(0);
  249. $tty->datatype('raw');
  250. $tty->stty_icanon(0);
  251. $tty->stty_opost(0);
  252. $tty->stty_ignbrk(0);
  253. $tty->stty_inlcr(0);
  254. $tty->stty_igncr(0);
  255. $tty->stty_icrnl(0);
  256. $tty->stty_echo(0);
  257. $tty->stty_echonl(0);
  258. $tty->stty_isig(0);
  259. $tty->read_char_time(0);
  260. $tty->write_settings;
  261. # In case DTR and/or RTS was asserted on the physical serial port.
  262. # Note that DTR needs to be deasserted before RTS!
  263. # However, deasserting DTR on the ACM port prevents it from working,
  264. # so only do this if we don't see CTS (which is always enabled on ACM)...
  265. if ($tty->can_modemlines && !($tty->modemlines & $tty->MS_CTS_ON)) {
  266. usleep(100000);
  267. $tty->dtr_active(0);
  268. usleep(100000);
  269. $tty->rts_active(0);
  270. usleep(100000);
  271. }
  272. sub tty_read {
  273. state %old_timeout;
  274. my($tty,$bufref,$timeout) = @_;
  275. my $d = $$bufref;
  276. if ($d eq '') {
  277. my $c;
  278. if (!defined($old_timeout{$tty}) || $timeout != $old_timeout{$tty}) {
  279. $tty->read_const_time($timeout);
  280. $old_timeout{$tty} = $timeout;
  281. }
  282. ($c,$d) = $tty->read(256);
  283. return '' if (!$c);
  284. }
  285. my $r = substr($d,0,1);
  286. $$bufref = substr($d,1);
  287. return $r;
  288. }
  289. my $ttybuf = '';
  290. sub tty_write($$) {
  291. my($tty,$data) = @_;
  292. my $bytes = length($data);
  293. my $offs = 0;
  294. while ($bytes) {
  295. my $cnt = $tty->write(substr($data,$offs,$bytes));
  296. usleep(10000) unless ($cnt);
  297. $offs += $cnt;
  298. $bytes -= $cnt;
  299. }
  300. return $offs;
  301. }
  302. my $found = 0;
  303. my $tt = time();
  304. my $start_enq = $tt;
  305. tty_write($tty, "\005"); # ENQ
  306. while (($tt = time()) - $start_enq < 30) {
  307. my $d = tty_read($tty, \$ttybuf, 1000);
  308. if ($d eq '') {
  309. tty_write($tty, "\005"); # ENQ
  310. } else {
  311. my $ix = index("\026\004\027", $d);
  312. if ($ix < 0) {
  313. print STDERR $d;
  314. next;
  315. }
  316. $found = $found == $ix ? $found+1 : 0;
  317. last if ($found == 3);
  318. }
  319. }
  320. if ($found < 3) {
  321. die "$0: $port: no MAX80 ESP32 detected\n";
  322. }
  323. $start_enq = $tt;
  324. my $winspc;
  325. while (!defined($winspc)) {
  326. $tt = time();
  327. if ($tt - $start_enq >= 10) {
  328. die "$0: $port: failed to start FPGA firmware upload\n";
  329. }
  330. tty_write($tty, "\034\001: /// MAX80 FW UPLOAD \~\@\~ \$\r\n\035");
  331. my $d;
  332. while (1) {
  333. $d = tty_read($tty, \$ttybuf, 1000);
  334. last if ($d eq '');
  335. my $dc = unpack('C', $d);
  336. if ($dc == 036) {
  337. $winspc = 0;
  338. last;
  339. } else {
  340. print STDERR $a[$dc];
  341. }
  342. }
  343. }
  344. my $bytes = length($fpgadata);
  345. my $offset = 0;
  346. my $maxchunk = 64;
  347. my $maxahead = 256;
  348. my $last_ack = 0;
  349. my @pktends = ();
  350. my $last_enq = 0;
  351. print STDERR "\nStarting packet transmit...\n";
  352. while ($last_ack < $bytes) {
  353. my $chunk;
  354. while (1) {
  355. $chunk = $bytes - $offset;
  356. $chunk = $winspc if ($chunk > $winspc);
  357. if ($offset + $chunk > $last_ack + $maxahead) {
  358. $chunk = $last_ack + $maxahead - $offset;
  359. }
  360. $chunk = 0 if ($chunk <= 0);
  361. $chunk = $maxchunk if ($chunk > $maxchunk);
  362. my $d = tty_read($tty, \$ttybuf, $chunk ? 0 : $maxchunk/10);
  363. last if ($d eq '');
  364. my $dc = unpack('C', $d);
  365. if ($dc == 022) {
  366. $winspc = 0;
  367. print STDERR "WRST, window: $winspc\n";
  368. } elsif ($dc == 024) {
  369. if (defined($winspc)) {
  370. $winspc += 256;
  371. print STDERR "WGO, window: $winspc\n";
  372. }
  373. } elsif ($dc == 006) {
  374. $last_ack = shift(@pktends) || $last_ack;
  375. print STDERR "ACK to $last_ack\n";
  376. } else {
  377. print STDERR $a[$dc];
  378. if ($dc == 025 || $dc == 031 || $dc == 037) {
  379. print STDERR "\n$0: $port: resetting upload to $last_ack\n";
  380. $offset = $last_ack;
  381. @pktends = ();
  382. undef $winspc; # Wait for WRST before resuming
  383. } elsif ($dc == 030) {
  384. print STDERR "\n";
  385. die "$0: $port: upload aborted by target system\n";
  386. }
  387. }
  388. }
  389. print STDERR "Send offset: $offset, last ack: $last_ack, window: $winspc, chunk: $chunk\n";
  390. if (!$chunk) {
  391. if ($bytes > $offset) {
  392. my $now = time();
  393. if ($now != $last_enq) {
  394. tty_write($tty, "\026"); # SYN: request window resync
  395. print STDERR "Trying to resynchronize window\n";
  396. $last_enq = $now;
  397. }
  398. }
  399. next;
  400. }
  401. my $data = substr($fpgadata, $offset, $chunk);
  402. my $hdr = pack("CvVV", $chunk-1, 0, $offset, crc32($data));
  403. tty_write($tty, "\002".mybaseencode($hdr, $data));
  404. push(@pktends, $offset + $chunk);
  405. $offset += $chunk;
  406. $winspc -= $chunk;
  407. }
  408. tty_write($tty, "\004"); # EOT
  409. # Final messages out
  410. while (1) {
  411. my $d = tty_read($tty, \$ttybuf, 1000);
  412. last if ($d eq '');
  413. print STDERR $d;
  414. }
  415. $tty->close;
  416. print STDERR "\n$0: $port: firmware upload complete\n";
  417. exit 0;