flashesp.pl 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. my $esptool = ($^O eq 'MSWin32') ? 'esptool.exe' : 'esptool.py';
  10. $esptool = $ENV{'ESPTOOL'} || $esptool;
  11. my $FW_MAGIC = 0x7a07fbd6;
  12. my %datatypes = (
  13. 'end' => 0, # End of data
  14. 'data' => 1, # FPGA flash data
  15. 'target' => 2, # Firmware target string
  16. 'note' => 3, # Informative string
  17. 'espota' => 4, # ESP32 OTA image
  18. 'fpgainit' => 5, # FPGA bypass (transient) image during update
  19. 'esppart' => 6, # ESP32 partition table
  20. 'espsys' => 7, # ESP32 boot loader, OTA control partition...
  21. 'esptool' => 8 # esptool.py options for flashing
  22. );
  23. my @type;
  24. foreach my $t (keys(%datatypes)) {
  25. $type[$datatypes{$t}] = $t;
  26. }
  27. my $FDF_OPTIONAL = 0x0001;
  28. my $STRING_MAX_LEN = 4095;
  29. sub getint($) {
  30. my($s) = @_;
  31. return undef
  32. unless ($s =~ /^(([1-9][0-9]+)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
  33. my $o = oct($3) + $2;
  34. my $p = lc($5);
  35. if ($p eq 'k') {
  36. $o <<= 10;
  37. } elsif ($p eq 'm') {
  38. $o <<= 20;
  39. } elsif ($p eq 'g') {
  40. $o <<= 30;
  41. } elsif ($p eq 't') {
  42. $o <<= 40;
  43. } elsif ($p eq 'p') {
  44. $o <<= 50;
  45. } elsif ($p eq 'e') {
  46. $o <<= 60;
  47. }
  48. return $o;
  49. }
  50. sub filelen($) {
  51. my($f) = @_;
  52. my @s = stat($f);
  53. return $s[7];
  54. }
  55. sub unquote_cmd($) {
  56. my($s) = @_;
  57. my @a;
  58. $s =~ s/[\r\n]+/ /g;
  59. while ($s =~ /^\s*(?:\"((?:[^\"]+|\"\")*)\"|(\S+))(\s.*)?$/) {
  60. push(@a, $1.$2);
  61. $s = $3;
  62. }
  63. return @a;
  64. }
  65. my @args = @ARGV;
  66. my $esponly = 0;
  67. my $file;
  68. while (1) {
  69. $file = shift(@args);
  70. last if ($file !~ /^\-/);
  71. if ($file eq '--esponly') {
  72. $esponly = 1;
  73. } elsif ($file eq '--') {
  74. $file = shift(@args);
  75. last;
  76. } else {
  77. undef $file; # Invalid argument, print usage
  78. last;
  79. }
  80. }
  81. my $port = shift(@args);
  82. if (!defined($port)) {
  83. die "Usage: $0 file.fw port [esptool options...]\n";
  84. }
  85. if (!File::Spec->file_name_is_absolute($port)) {
  86. if (-c "/dev/$port") {
  87. $port = "/dev/$port";
  88. } elsif (-c "/dev/tty$port") {
  89. $port = "/dev/tty$port";
  90. } elsif ($^O eq 'MSWin32') {
  91. $port = "\\\\.\\$port";
  92. } else {
  93. die "$0: no such serial port: $port\n";
  94. }
  95. }
  96. print STDERR "Using serial port device $port\n";
  97. open(my $fw, '<:gzip', $file)
  98. or die "$0: $file: $!\n";
  99. my @chunks = ();
  100. my $err = 0;
  101. my $hdr;
  102. while (read($fw, $hdr, 16) == 16) {
  103. # magic type flags data_len addr
  104. my @h = unpack('VvvVV', $hdr);
  105. my $c = { 'hdr' => $hdr, 'magic' => $h[0], 'type' => $h[1],
  106. 'flags' => $h[2], 'len' => $h[3], 'addr' => $h[4] };
  107. if ($c->{'magic'} != $FW_MAGIC) {
  108. print STDERR "$0: $file: bad chunk magic\n";
  109. $err = 1;
  110. last;
  111. }
  112. my $t = $type[$c->{'type'}];
  113. my $d;
  114. if (read($fw, $d, $c->{'len'}) != $c->{'len'}) {
  115. print STDERR "$0: $file: short chunk read\n";
  116. $err = 1;
  117. last;
  118. }
  119. $c->{'data'} = $d;
  120. push(@chunks, $c);
  121. last if ($t eq 'end'); # End of stream
  122. }
  123. close($fw);
  124. exit $err if ($err);
  125. my $td = File::Temp->newdir(CLEANUP => 1);
  126. my @espfiles = ();
  127. my %espopt = ('before' => 'default_reset', 'after' => 'hard_reset',
  128. 'baud' => 115200, 'port' => undef, 'chip' => undef,
  129. 'flash_mode' => undef, 'flash_freq' => undef,
  130. 'flash_size' => undef);
  131. # Create a compressed data buffer without the ESP32 chunks
  132. my $fpgadata;
  133. open(my $fpgafh, '>:gzip', \$fpgadata) or die;
  134. my $nc = 0;
  135. foreach my $c ( @chunks ) {
  136. my $t = $type[$c->{'type'}];
  137. if ($t eq 'esptool') {
  138. my $s = $c->{'data'};
  139. $s =~ s/[\r\n]+/ /g;
  140. while ($s =~ /^\s*(\S+)\s+(\S+)(.*)/) {
  141. $espopt{$1} = $2;
  142. $s = $3;
  143. }
  144. } elsif ($t =~ /^esp/ && $c->{'addr'}) {
  145. my $addr = sprintf('%x', $c->{'addr'});
  146. my $tff = File::Spec->catfile($td, $t.$nc.'_'.$addr.'.bin');
  147. open(my $tf, '>', $tff) or die "$0: $tff: $!\n";
  148. print $tf $c->{'data'};
  149. close($tf);
  150. push(@espfiles, '0x'.$addr, $tff);
  151. } else {
  152. print $fpgafh $c->{'hdr'};
  153. print $fpgafh $c->{'data'};
  154. }
  155. $nc++;
  156. }
  157. close($fpgafh);
  158. foreach my $e (keys %espopt) {
  159. my $ev = $ENV{"ESP\U$e"};
  160. if (defined($ev)) {
  161. $espopt{$e} = $ev;
  162. }
  163. }
  164. $espopt{'port'} = $port;
  165. my @espcmd = unquote_cmd($esptool);
  166. foreach my $o (sort grep (!/^flash_/, keys %espopt)) {
  167. if (defined($espopt{$o})) {
  168. push(@espcmd, "--$o", $espopt{$o});
  169. }
  170. }
  171. push(@espcmd, 'write_flash', '-z');
  172. foreach my $o (sort grep (/^flash_/, keys %espopt)) {
  173. if (defined($espopt{$o})) {
  174. push(@espcmd, "--$o", $espopt{$o});
  175. }
  176. }
  177. push(@espcmd, @espfiles);
  178. print STDERR join(' ', @espcmd), "\n";
  179. $err = system(@espcmd);
  180. if ($err == -1) {
  181. print STDERR "$0: $espcmd[0]: $!\n";
  182. } elsif ($err) {
  183. print STDERR "$0: $espcmd[0]: exit $err\n";
  184. }
  185. exit $err if ($err || $esponly);
  186. my $SerialPort = eval {
  187. require Device::SerialPort;
  188. Device::SerialPort->import(qw(:PARAM));
  189. 'Device::SerialPort';
  190. } || eval {
  191. require Win32::SerialPort;
  192. Win32::SerialPort->import(qw(:STAT));
  193. 'Win32::SerialPort';
  194. } || die "$0: need Device::SerialPort or Win32::SerialPort\n";
  195. print STDERR "Waiting for reinit...\n";
  196. usleep(4000000);
  197. my $tty = $SerialPort->new($port);
  198. die "$0: $port: $!\n" if (!$tty);
  199. $tty->buffers($tty->buffer_max);
  200. $tty->reset_error;
  201. $tty->user_msg(1);
  202. $tty->error_msg(1);
  203. $tty->handshake('none');
  204. $tty->databits(8);
  205. $tty->baudrate(115200);
  206. $tty->parity('none');
  207. $tty->stopbits(1);
  208. $tty->datatype('raw');
  209. $tty->stty_ignbrk(0);
  210. #$tty->stty_brkint(0);
  211. $tty->stty_parmrk(0);
  212. $tty->stty_istrip(0);
  213. $tty->stty_inlcr(0);
  214. $tty->stty_igncr(0);
  215. $tty->stty_icrnl(0);
  216. $tty->stty_opost(0);
  217. $tty->stty_echo(0);
  218. $tty->stty_echonl(0);
  219. $tty->stty_icanon(0);
  220. $tty->stty_isig(0);
  221. #$tty->stty_iexten(0);
  222. $tty->read_char_time(0);
  223. $tty->write_settings;
  224. # In case DTR and/or RTS was asserted on the physical serial port.
  225. # Note that DTR needs to be deasserted before RTS!
  226. # However, deasserting DTR on the ACM port prevents it from working,
  227. # so only do this if we don't see CTS (which is always enabled on ACM)...
  228. if ($tty->can_modemlines && !($tty->modemlines & $tty->MS_CTS_ON)) {
  229. usleep(100000);
  230. $tty->dtr_active(0);
  231. usleep(100000);
  232. $tty->rts_active(0);
  233. usleep(100000);
  234. }
  235. sub tty_read {
  236. my($tty,$bufref,$timeout) = @_;
  237. my $d = $$bufref;
  238. if ($d eq '') {
  239. my $c;
  240. $tty->read_const_time($timeout);
  241. ($c,$d) = $tty->read(256);
  242. return '' if (!$c);
  243. }
  244. my $r = substr($d,0,1);
  245. $$bufref = substr($d,1);
  246. return $r;
  247. }
  248. my $ttybuf = '';
  249. sub tty_write($$) {
  250. my($tty,$data) = @_;
  251. my $bytes = length($data);
  252. my $offs = 0;
  253. while ($bytes) {
  254. my $cnt = $tty->write(substr($data,$offs,$bytes));
  255. usleep(10000) unless ($cnt);
  256. $offs += $cnt;
  257. $bytes -= $cnt;
  258. }
  259. return $offs;
  260. }
  261. my $found = 0;
  262. my $tt = time();
  263. my $start_enq = $tt;
  264. while (($tt = time()) - $start_enq < 30) {
  265. my $d = tty_read($tty, \$ttybuf, 1000);
  266. if ($d eq '') {
  267. tty_write($tty, "\005"); # ENQ
  268. } else {
  269. my $ix = index("\026\004\027", $d);
  270. if ($ix < 0) {
  271. print STDERR $d;
  272. next;
  273. }
  274. $found = $found == $ix ? $found+1 : 0;
  275. last if ($found == 3);
  276. }
  277. }
  278. if ($found < 3) {
  279. die "$0: $port: no MAX80 ESP32 detected\n";
  280. }
  281. $start_enq = $tt;
  282. my $winspc;
  283. while (!defined($winspc)) {
  284. $tt = time();
  285. if ($tt - $start_enq >= 10) {
  286. die "$0: $port: failed to start FPGA firmware upload\n";
  287. }
  288. tty_write($tty, "\034\001: /// MAX80 FW UPLOAD \~\@\~ \$\r\n\035");
  289. my $d;
  290. do {
  291. $d = tty_read($tty, \$ttybuf, 1000);
  292. if ($d eq "\036") {
  293. $winspc = 0;
  294. last;
  295. } else {
  296. print $d;
  297. }
  298. } while ($d ne '');
  299. }
  300. my $bytes = length($fpgadata);
  301. my $offset = 0;
  302. while ($offset < $bytes) {
  303. my $chunk = $bytes - $offset;
  304. $chunk = 64 if ($chunk > 64);
  305. $chunk = $winspc if ($chunk > $winspc);
  306. my $d = tty_read($tty, \$ttybuf, $chunk ? undef : 1000);
  307. if ($d ne '') {
  308. if ($d eq "\022") {
  309. $winspc = 0;
  310. } elsif ($d eq "\024") {
  311. $winspc += 256;
  312. } elsif ($d eq "\027" || $d eq "\030" || $d eq "\025") {
  313. die "$0: $port: upload terminated by MAX80\n";
  314. } elsif ($d ne "\006") {
  315. print STDERR $d;
  316. }
  317. }
  318. next unless($chunk);
  319. my $data = substr($fpgadata, $offset, $chunk);
  320. my $hdr = pack("CCvVV", 2, $chunk-1, 0, $offset, crc32($data));
  321. tty_write($tty, $hdr.$data);
  322. $offset += $chunk;
  323. $winspc -= $chunk;
  324. }
  325. tty_write($tty, "\004"); # EOT
  326. $tty->close;
  327. exit 0;