/ adafruit_miniesptool.py
adafruit_miniesptool.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2018 ladyada for Adafruit Industries
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_miniesptool`
 24  ====================================================
 25  
 26  ROM loader for ESP chips, works with ESP8266 or ESP32.
 27  This is a 'no-stub' loader, so you can't read MD5 or firmware back on ESP8266.
 28  
 29  See this document for protocol we're implementing:
 30  https://github.com/espressif/esptool/wiki/Serial-Protocol
 31  
 32  See this for the 'original' code we're miniaturizing:
 33  https://github.com/espressif/esptool/blob/master/esptool.py
 34  
 35  There's a very basic Arduino ROM loader here for ESP32:
 36  https://github.com/arduino-libraries/WiFiNINA/tree/master/examples/Tools/FirmwareUpdater
 37  
 38  * Author(s): ladyada
 39  
 40  Implementation Notes
 41  --------------------
 42  
 43  **Hardware:**
 44  
 45  **Software and Dependencies:**
 46  
 47  * Adafruit CircuitPython firmware for the supported boards:
 48    https://github.com/adafruit/circuitpython/releases
 49  
 50  
 51  """
 52  
 53  import os
 54  import time
 55  import struct
 56  from digitalio import Direction
 57  
 58  __version__ = "0.0.0-auto.0"
 59  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_miniesptool.git"
 60  
 61  SYNC_PACKET = b"\x07\x07\x12 UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
 62  ESP32_DATAREGVALUE = 0x15122500
 63  ESP8266_DATAREGVALUE = 0x00062000
 64  
 65  # Commands supported by ESP8266 ROM bootloader
 66  # pylint: disable=bad-whitespace
 67  ESP_FLASH_BEGIN = 0x02
 68  ESP_FLASH_DATA = 0x03
 69  ESP_FLASH_END = 0x04
 70  ESP_MEM_BEGIN = 0x05
 71  ESP_MEM_END = 0x06
 72  ESP_MEM_DATA = 0x07
 73  ESP_SYNC = 0x08
 74  ESP_WRITE_REG = 0x09
 75  ESP_READ_REG = 0x0A
 76  ESP_SPI_SET_PARAMS = 0x0B
 77  ESP_SPI_ATTACH = 0x0D
 78  ESP_CHANGE_BAUDRATE = 0x0F
 79  ESP_SPI_FLASH_MD5 = 0x13
 80  ESP_CHECKSUM_MAGIC = 0xEF
 81  
 82  ESP8266 = 0x8266
 83  ESP32 = 0x32
 84  # pylint: enable=bad-whitespace
 85  
 86  FLASH_SIZES = {
 87      "512KB": 0x00,
 88      "256KB": 0x10,
 89      "1MB": 0x20,
 90      "2MB": 0x30,
 91      "4MB": 0x40,
 92      "2MB-c1": 0x50,
 93      "4MB-c1": 0x60,
 94      "8MB": 0x80,
 95      "16MB": 0x90,
 96  }
 97  
 98  
 99  class miniesptool:  # pylint: disable=invalid-name
100      """A miniature version of esptool, a programming command line tool for
101      ESP8266 and ESP32 chips. This version is minimized to work on CircuitPython
102      boards, so you can burn ESP firmware direct from the CPy disk drive. Handy
103      when you have an ESP module wired to a board and need to upload new AT
104      firmware. Its slow! Expect a few minutes when programming 1 MB flash."""
105  
106      FLASH_WRITE_SIZE = 0x200
107      FLASH_SECTOR_SIZE = 0x1000  # Flash sector size, minimum unit of erase.
108      ESP_ROM_BAUD = 115200
109  
110      def __init__(
111          self,
112          uart,
113          gpio0_pin,  # pylint: disable=too-many-arguments
114          reset_pin,
115          *,
116          flashsize,
117          baudrate=ESP_ROM_BAUD
118      ):
119          gpio0_pin.direction = Direction.OUTPUT
120          reset_pin.direction = Direction.OUTPUT
121          self._gpio0pin = gpio0_pin
122          self._resetpin = reset_pin
123          self._uart = uart
124          self._uart.baudrate = baudrate
125          self._debug = False
126          self._efuses = [0] * 4
127          self._chipfamily = None
128          self._chipname = None
129          self._flashsize = flashsize
130          # self._debug_led = DigitalInOut(board.D13)
131          # self._debug_led.direction = Direction.OUTPUT
132  
133      @property
134      def debug(self):
135          """Print out all sent/received UART data plus some debugging output"""
136          return self._debug
137  
138      @debug.setter
139      def debug(self, flag):
140          self._debug = flag
141  
142      @property
143      def baudrate(self):
144          """The baudrate of the UART connection. On ESP8266 we cannot change
145          this once we've started syncing. On ESP32 we must start at 115200 and
146          then manually change to higher speeds if desired"""
147          return self._uart.baudrate
148  
149      @baudrate.setter
150      def baudrate(self, baud):
151          if self._chipfamily == ESP8266:
152              raise NotImplementedError("Baud rate can only change on ESP32")
153          buffer = struct.pack("<II", baud, 0)
154          self.check_command(ESP_CHANGE_BAUDRATE, buffer)
155          self._uart.baudrate = baud
156          time.sleep(0.05)
157          self._uart.reset_input_buffer()
158          self.check_command(ESP_CHANGE_BAUDRATE, buffer)
159  
160      def md5(self, offset, size):
161          """On ESP32 we can ask the ROM bootloader to calculate an MD5 on the
162          SPI flash memory, from a location over a size in bytes. Returns a
163          string with the MD5 in lowercase"""
164          if self._chipfamily == ESP8266:
165              raise NotImplementedError("MD5 only supported on ESP32")
166          self.check_command(ESP_SPI_ATTACH, bytes([0] * 8))
167          buffer = struct.pack("<IIII", offset, size, 0, 0)
168          md5 = self.check_command(ESP_SPI_FLASH_MD5, buffer, timeout=2)[1]
169          return "".join([chr(i) for i in md5])
170  
171      @property
172      def mac_addr(self):
173          """The MAC address burned into the OTP memory of the ESP chip"""
174          mac_addr = [0] * 6
175          mac0, mac1, mac2, mac3 = self._efuses
176          if self._chipfamily == ESP8266:
177              if mac3 != 0:
178                  oui = ((mac3 >> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF)
179              elif ((mac1 >> 16) & 0xFF) == 0:
180                  oui = (0x18, 0xFE, 0x34)
181              elif ((mac1 >> 16) & 0xFF) == 1:
182                  oui = (0xAC, 0xD0, 0x74)
183              else:
184                  raise RuntimeError("Couldnt determine OUI")
185  
186              mac_addr[0] = oui[0]
187              mac_addr[1] = oui[1]
188              mac_addr[2] = oui[2]
189              mac_addr[3] = (mac1 >> 8) & 0xFF
190              mac_addr[4] = mac1 & 0xFF
191              mac_addr[5] = (mac0 >> 24) & 0xFF
192          if self._chipfamily == ESP32:
193              mac_addr[0] = mac2 >> 8 & 0xFF
194              mac_addr[1] = mac2 & 0xFF
195              mac_addr[2] = mac1 >> 24 & 0xFF
196              mac_addr[3] = mac1 >> 16 & 0xFF
197              mac_addr[4] = mac1 >> 8 & 0xFF
198              mac_addr[5] = mac1 & 0xFF
199          return mac_addr
200  
201      @property
202      def chip_type(self):
203          """ESP32 or ESP8266 based on which chip type we're talking to"""
204          if not self._chipfamily:
205              datareg = self.read_register(0x60000078)
206              if datareg == ESP32_DATAREGVALUE:
207                  self._chipfamily = ESP32
208              elif datareg == ESP8266_DATAREGVALUE:
209                  self._chipfamily = ESP8266
210              else:
211                  raise RuntimeError("Unknown Chip")
212          return self._chipfamily
213  
214      @property
215      def chip_name(self):
216          """The specific name of the chip, e.g. ESP8266EX, to the best
217          of our ability to determine without a stub bootloader."""
218          self.chip_type  # pylint: disable=pointless-statement
219          self._read_efuses()
220  
221          if self.chip_type == ESP32:
222              return "ESP32"
223          if self.chip_type == ESP8266:
224              if self._efuses[0] & (1 << 4) or self._efuses[2] & (1 << 16):
225                  return "ESP8285"
226              return "ESP8266EX"
227          return None
228  
229      def _read_efuses(self):
230          """Read the OTP data for this chip and store into _efuses array"""
231          if self._chipfamily == ESP8266:
232              base_addr = 0x3FF00050
233          elif self._chipfamily == ESP32:
234              base_addr = 0x6001A000
235          else:
236              raise RuntimeError("Don't know what chip this is")
237          for i in range(4):
238              self._efuses[i] = self.read_register(base_addr + 4 * i)
239  
240      def get_erase_size(self, offset, size):
241          """Calculate an erase size given a specific size in bytes.
242          Provides a workaround for the bootloader erase bug on ESP8266."""
243  
244          sectors_per_block = 16
245          sector_size = self.FLASH_SECTOR_SIZE
246          num_sectors = (size + sector_size - 1) // sector_size
247          start_sector = offset // sector_size
248  
249          head_sectors = sectors_per_block - (start_sector % sectors_per_block)
250          if num_sectors < head_sectors:
251              head_sectors = num_sectors
252  
253          if num_sectors < 2 * head_sectors:
254              return (num_sectors + 1) // 2 * sector_size
255          return (num_sectors - head_sectors) * sector_size
256  
257      def flash_begin(self, *, size=0, offset=0):
258          """Prepare for flashing by attaching SPI chip and erasing the
259          number of blocks requred."""
260          if self._chipfamily == ESP32:
261              self.check_command(ESP_SPI_ATTACH, bytes([0] * 8))
262              # We are hardcoded for 4MB flash on ESP32
263              buffer = struct.pack(
264                  "<IIIIII", 0, self._flashsize, 0x10000, 4096, 256, 0xFFFF
265              )
266              self.check_command(ESP_SPI_SET_PARAMS, buffer)
267  
268          num_blocks = (size + self.FLASH_WRITE_SIZE - 1) // self.FLASH_WRITE_SIZE
269          if self._chipfamily == ESP8266:
270              erase_size = self.get_erase_size(offset, size)
271          else:
272              erase_size = size
273          timeout = 13
274          stamp = time.monotonic()
275          buffer = struct.pack(
276              "<IIII", erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset
277          )
278          print(
279              "Erase size %d, num_blocks %d, size %d, offset 0x%04x"
280              % (erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset)
281          )
282  
283          self.check_command(ESP_FLASH_BEGIN, buffer, timeout=timeout)
284          if size != 0:
285              print(
286                  "Took %.2fs to erase %d flash blocks"
287                  % (time.monotonic() - stamp, num_blocks)
288              )
289          return num_blocks
290  
291      def check_command(
292          self, opcode, buffer, checksum=0, timeout=0.1
293      ):  # pylint: disable=unused-argument
294          """Send a command packet, check that the command succeeded and
295          return a tuple with the value and data.
296          See the ESP Serial Protocol for more details on what value/data are"""
297          self.send_command(opcode, buffer)
298          value, data = self.get_response(opcode, timeout)
299          if self._chipfamily == ESP8266:
300              status_len = 2
301          elif self._chipfamily == ESP32:
302              status_len = 4
303          else:
304              if len(data) in (2, 4):
305                  status_len = len(data)
306          if data is None or len(data) < status_len:
307              raise RuntimeError("Didn't get enough status bytes")
308          status = data[-status_len:]
309          data = data[:-status_len]
310          # print("status", status)
311          # print("value", value)
312          # print("data", data)
313          if status[0] != 0:
314              raise RuntimeError("Command failure error code 0x%02x" % status[1])
315          return (value, data)
316  
317      def send_command(self, opcode, buffer):
318          """Send a slip-encoded, checksummed command over the UART,
319          does not check response"""
320          self._uart.reset_input_buffer()
321  
322          # self._debug_led.value = True
323          checksum = 0
324          if opcode == 0x03:
325              checksum = self.checksum(buffer[16:])
326          # self._debug_led.value = False
327  
328          packet = [0xC0, 0x00]  # direction
329          packet.append(opcode)
330          packet.extend(struct.pack("H", len(buffer)))
331          packet.extend(self.slip_encode(struct.pack("I", checksum)))
332          packet.extend(self.slip_encode(buffer))
333          packet += [0xC0]
334          if self._debug:
335              print([hex(x) for x in packet])
336              print("Writing:", bytearray(packet))
337          self._uart.write(bytearray(packet))
338  
339      def get_response(self, opcode, timeout=0.1):  # pylint: disable=too-many-branches
340          """Read response data and decodes the slip packet, then parses
341          out the value/data and returns as a tuple of (value, data) where
342          each is a list of bytes"""
343          reply = []
344  
345          stamp = time.monotonic()
346          packet_length = 0
347          escaped_byte = False
348          while (time.monotonic() - stamp) < timeout:
349              if self._uart.in_waiting > 0:
350                  c = self._uart.read(1)  # pylint: disable=invalid-name
351                  if c == b"\xDB":
352                      escaped_byte = True
353                  elif escaped_byte:
354                      if c == b"\xDD":
355                          reply += b"\xDC"
356                      elif c == b"\xDC":
357                          reply += b"\xC0"
358                      else:
359                          reply += [0xDB, c]
360                      escaped_byte = False
361                  else:
362                      reply += c
363              if reply and reply[0] != 0xC0:
364                  # packets must start with 0xC0
365                  del reply[0]
366              if len(reply) > 1 and reply[1] != 0x01:
367                  del reply[0]
368              if len(reply) > 2 and reply[2] != opcode:
369                  del reply[0]
370              if len(reply) > 4:
371                  # get the length
372                  packet_length = reply[3] + (reply[4] << 8)
373              if len(reply) == packet_length + 10:
374                  break
375          # Check to see if we have a complete packet. If not, we timed out.
376          if len(reply) != packet_length + 10:
377              if self._debug:
378                  print("Timed out after {} seconds".format(timeout))
379              return (None, None)
380          if self._debug:
381              print("Packet:", [hex(i) for i in reply])
382              print("Reading:", bytearray(reply))
383          value = reply[5:9]
384          data = reply[9:-1]
385          if self._debug:
386              print("value:", [hex(i) for i in value], "data:", [hex(i) for i in data])
387          return (value, data)
388  
389      def read_register(self, reg):
390          """Read a register within the ESP chip RAM, returns a 4-element list"""
391          if self._debug:
392              print("Reading register 0x%08x" % reg)
393          packet = struct.pack("I", reg)
394          register = self.check_command(ESP_READ_REG, packet)[0]
395          return struct.unpack("I", bytearray(register))[0]
396  
397      def reset(self, program_mode=False):
398          """Perform a hard-reset into ROM bootloader using gpio0 and reset"""
399          print("Resetting")
400          self._gpio0pin.value = not program_mode
401          self._resetpin.value = False
402          time.sleep(0.1)
403          self._resetpin.value = True
404          time.sleep(1.0)
405  
406      def flash_block(self, data, seq, timeout=0.1):
407          """Send one block of data to program into SPI Flash memory"""
408          self.check_command(
409              ESP_FLASH_DATA,
410              struct.pack("<IIII", len(data), seq, 0, 0) + data,
411              self.checksum(data),
412              timeout=timeout,
413          )
414  
415      def flash_file(self, filename, offset=0, md5=None):
416          """Program a full, uncompressed binary file into SPI Flash at
417          a given offset. If an ESP32 and md5 string is passed in, will also
418          verify memory. ESP8266 does not have checksum memory verification in
419          ROM"""
420          filesize = os.stat(filename)[6]
421          with open(filename, "rb") as file:
422              print("\nWriting", filename, "w/filesize:", filesize)
423              blocks = self.flash_begin(size=filesize, offset=offset)
424              seq = 0
425              written = 0
426              address = offset
427              stamp = time.monotonic()
428              while filesize - file.tell() > 0:
429                  print(
430                      "\rWriting at 0x%08x... (%d %%)"
431                      % (
432                          address + seq * self.FLASH_WRITE_SIZE,
433                          100 * (seq + 1) // blocks,
434                      ),
435                      end="",
436                  )
437                  block = file.read(self.FLASH_WRITE_SIZE)
438                  # Pad the last block
439                  block = block + b"\xff" * (self.FLASH_WRITE_SIZE - len(block))
440                  # print(block)
441                  self.flash_block(block, seq, timeout=2)
442                  seq += 1
443                  written += len(block)
444              print("Took %.2fs to write %d bytes" % (time.monotonic() - stamp, filesize))
445              if md5:
446                  print("Verifying MD5sum ", md5)
447                  calcd = self.md5(offset, filesize)
448                  if md5 != calcd:
449                      raise RuntimeError("MD5 mismatch, calculated:", calcd)
450  
451      def _sync(self):
452          """Perform a soft-sync using AT sync packets, does not perform
453          any hardware resetting"""
454          self.send_command(0x08, SYNC_PACKET)
455          for _ in range(8):
456              reply, data = self.get_response(  # pylint: disable=unused-variable
457                  0x08, 0.1
458              )
459              if not data:
460                  continue
461              if len(data) > 1 and data[0] == 0 and data[1] == 0:
462                  return True
463          return False
464  
465      def sync(self):
466          """Put into ROM bootload mode & attempt to synchronize with the
467          ESP ROM bootloader, we will retry a few times"""
468          self.reset(True)
469  
470          for _ in range(5):
471              if self._sync():
472                  time.sleep(0.1)
473                  return True
474              time.sleep(0.1)
475  
476          raise RuntimeError("Couldn't sync to ESP")
477  
478      @staticmethod
479      def checksum(data, state=ESP_CHECKSUM_MAGIC):
480          """ Calculate checksum of a blob, as it is defined by the ROM """
481          for b in data:
482              state ^= b
483          return state
484  
485      @staticmethod
486      def slip_encode(buffer):
487          """Take a bytearray buffer and return back a new bytearray where
488          0xdb is replaced with 0xdb 0xdd and 0xc0 is replaced with 0xdb 0xdc"""
489          encoded = []
490          for b in buffer:
491              if b == 0xDB:
492                  encoded += [0xDB, 0xDD]
493              elif b == 0xC0:
494                  encoded += [0xDB, 0xDC]
495              else:
496                  encoded += [b]
497          return bytearray(encoded)