# SPDX-FileCopyrightText: 2018 ladyada for Adafruit Industries # # SPDX-License-Identifier: MIT """ `adafruit_miniesptool` ==================================================== ROM loader for ESP chips, works with ESP8266 or ESP32. This is a 'no-stub' loader, so you can't read MD5 or firmware back on ESP8266. See this document for protocol we're implementing: https://github.com/espressif/esptool/wiki/Serial-Protocol See this for the 'original' code we're miniaturizing: https://github.com/espressif/esptool/blob/master/esptool.py There's a very basic Arduino ROM loader here for ESP32: https://github.com/arduino-libraries/WiFiNINA/tree/master/examples/Tools/FirmwareUpdater * Author(s): ladyada Implementation Notes -------------------- **Hardware:** **Software and Dependencies:** * Adafruit CircuitPython firmware for the supported boards: https://github.com/adafruit/circuitpython/releases """ import os import struct import time from digitalio import Direction __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_miniesptool.git" SYNC_PACKET = b"\x07\x07\x12 UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU" ESP32_DATAREGVALUE = 0x15122500 ESP8266_DATAREGVALUE = 0x00062000 # Commands supported by ESP8266 ROM bootloader ESP_FLASH_BEGIN = 0x02 ESP_FLASH_DATA = 0x03 ESP_FLASH_END = 0x04 ESP_MEM_BEGIN = 0x05 ESP_MEM_END = 0x06 ESP_MEM_DATA = 0x07 ESP_SYNC = 0x08 ESP_WRITE_REG = 0x09 ESP_READ_REG = 0x0A ESP_SPI_SET_PARAMS = 0x0B ESP_SPI_ATTACH = 0x0D ESP_CHANGE_BAUDRATE = 0x0F ESP_SPI_FLASH_MD5 = 0x13 ESP_CHECKSUM_MAGIC = 0xEF ESP8266 = 0x8266 ESP32 = 0x32 FLASH_SIZES = { "512KB": 0x00, "256KB": 0x10, "1MB": 0x20, "2MB": 0x30, "4MB": 0x40, "2MB-c1": 0x50, "4MB-c1": 0x60, "8MB": 0x80, "16MB": 0x90, } class miniesptool: """A miniature version of esptool, a programming command line tool for ESP8266 and ESP32 chips. This version is minimized to work on CircuitPython boards, so you can burn ESP firmware direct from the CPy disk drive. Handy when you have an ESP module wired to a board and need to upload new AT firmware. Its slow! Expect a few minutes when programming 1 MB flash.""" FLASH_WRITE_SIZE = 0x200 FLASH_SECTOR_SIZE = 0x1000 # Flash sector size, minimum unit of erase. ESP_ROM_BAUD = 115200 def __init__( self, uart, gpio0_pin, reset_pin, *, flashsize, baudrate=ESP_ROM_BAUD, ): gpio0_pin.direction = Direction.OUTPUT reset_pin.direction = Direction.OUTPUT self._gpio0pin = gpio0_pin self._resetpin = reset_pin self._uart = uart self._uart.baudrate = baudrate self._debug = False self._efuses = [0] * 4 self._chipfamily = None self._chipname = None self._flashsize = flashsize # self._debug_led = DigitalInOut(board.D13) # self._debug_led.direction = Direction.OUTPUT @property def debug(self): """Print out all sent/received UART data plus some debugging output""" return self._debug @debug.setter def debug(self, flag): self._debug = flag @property def baudrate(self): """The baudrate of the UART connection. On ESP8266 we cannot change this once we've started syncing. On ESP32 we must start at 115200 and then manually change to higher speeds if desired""" return self._uart.baudrate @baudrate.setter def baudrate(self, baud): if self._chipfamily == ESP8266: raise NotImplementedError("Baud rate can only change on ESP32") buffer = struct.pack("> 16) & 0xFF, (mac3 >> 8) & 0xFF, mac3 & 0xFF) elif ((mac1 >> 16) & 0xFF) == 0: oui = (0x18, 0xFE, 0x34) elif ((mac1 >> 16) & 0xFF) == 1: oui = (0xAC, 0xD0, 0x74) else: raise RuntimeError("Couldnt determine OUI") mac_addr[0] = oui[0] mac_addr[1] = oui[1] mac_addr[2] = oui[2] mac_addr[3] = (mac1 >> 8) & 0xFF mac_addr[4] = mac1 & 0xFF mac_addr[5] = (mac0 >> 24) & 0xFF if self._chipfamily == ESP32: mac_addr[0] = mac2 >> 8 & 0xFF mac_addr[1] = mac2 & 0xFF mac_addr[2] = mac1 >> 24 & 0xFF mac_addr[3] = mac1 >> 16 & 0xFF mac_addr[4] = mac1 >> 8 & 0xFF mac_addr[5] = mac1 & 0xFF return mac_addr @property def chip_type(self): """ESP32 or ESP8266 based on which chip type we're talking to""" if not self._chipfamily: datareg = self.read_register(0x60000078) if datareg == ESP32_DATAREGVALUE: self._chipfamily = ESP32 elif datareg == ESP8266_DATAREGVALUE: self._chipfamily = ESP8266 else: raise RuntimeError("Unknown Chip") return self._chipfamily @property def chip_name(self): """The specific name of the chip, e.g. ESP8266EX, to the best of our ability to determine without a stub bootloader.""" self.chip_type self._read_efuses() if self.chip_type == ESP32: return "ESP32" if self.chip_type == ESP8266: if self._efuses[0] & (1 << 4) or self._efuses[2] & (1 << 16): return "ESP8285" return "ESP8266EX" return None def _read_efuses(self): """Read the OTP data for this chip and store into _efuses array""" if self._chipfamily == ESP8266: base_addr = 0x3FF00050 elif self._chipfamily == ESP32: base_addr = 0x6001A000 else: raise RuntimeError("Don't know what chip this is") for i in range(4): self._efuses[i] = self.read_register(base_addr + 4 * i) def get_erase_size(self, offset, size): """Calculate an erase size given a specific size in bytes. Provides a workaround for the bootloader erase bug on ESP8266.""" sectors_per_block = 16 sector_size = self.FLASH_SECTOR_SIZE num_sectors = (size + sector_size - 1) // sector_size start_sector = offset // sector_size head_sectors = sectors_per_block - (start_sector % sectors_per_block) if num_sectors < head_sectors: head_sectors = num_sectors if num_sectors < 2 * head_sectors: return (num_sectors + 1) // 2 * sector_size return (num_sectors - head_sectors) * sector_size def flash_begin(self, *, size=0, offset=0): """Prepare for flashing by attaching SPI chip and erasing the number of blocks requred.""" if self._chipfamily == ESP32: self.check_command(ESP_SPI_ATTACH, bytes([0] * 8)) # We are hardcoded for 4MB flash on ESP32 buffer = struct.pack(" 0: c = self._uart.read(1) if c == b"\xdb": escaped_byte = True elif escaped_byte: if c == b"\xdd": reply += b"\xdb" elif c == b"\xdc": reply += b"\xc0" else: reply += [0xDB, c] escaped_byte = False else: reply += c if reply and reply[0] != 0xC0: # packets must start with 0xC0 del reply[0] if len(reply) > 1 and reply[1] != 0x01: del reply[0] if len(reply) > 2 and reply[2] != opcode: del reply[0] if len(reply) > 4: # get the length packet_length = reply[3] + (reply[4] << 8) if len(reply) == packet_length + 10: break # Check to see if we have a complete packet. If not, we timed out. if len(reply) != packet_length + 10: if self._debug: print(f"Timed out after {timeout} seconds") return (None, None) if self._debug: print("Packet:", [hex(i) for i in reply]) print("Reading:", bytearray(reply)) value = reply[5:9] data = reply[9:-1] if self._debug: print("value:", [hex(i) for i in value], "data:", [hex(i) for i in data]) return (value, data) def read_register(self, reg): """Read a register within the ESP chip RAM, returns a 4-element list""" if self._debug: print("Reading register 0x%08x" % reg) packet = struct.pack("I", reg) register = self.check_command(ESP_READ_REG, packet)[0] return struct.unpack("I", bytearray(register))[0] def reset(self, program_mode=False): """Perform a hard-reset into ROM bootloader using gpio0 and reset""" print("Resetting") self._gpio0pin.value = not program_mode self._resetpin.value = False time.sleep(0.1) self._resetpin.value = True time.sleep(1.0) def flash_block(self, data, seq, timeout=0.1): """Send one block of data to program into SPI Flash memory""" self.check_command( ESP_FLASH_DATA, struct.pack(" 0: print( "\rWriting at 0x%08x... (%d %%)" % ( address + seq * self.FLASH_WRITE_SIZE, 100 * (seq + 1) // blocks, ), end="", ) block = file.read(self.FLASH_WRITE_SIZE) # Pad the last block block = block + b"\xff" * (self.FLASH_WRITE_SIZE - len(block)) # print(block) self.flash_block(block, seq, timeout=2) seq += 1 written += len(block) print("Took %.2fs to write %d bytes" % (time.monotonic() - stamp, filesize)) if md5: print("Verifying MD5sum ", md5) calcd = self.md5(offset, filesize) if md5 != calcd: raise RuntimeError("MD5 mismatch, calculated:", calcd) def _sync(self): """Perform a soft-sync using AT sync packets, does not perform any hardware resetting""" self.send_command(0x08, SYNC_PACKET) for _ in range(8): reply, data = self.get_response( # noqa: F841 0x08, 0.1 ) if not data: continue if len(data) > 1 and data[0] == 0 and data[1] == 0: return True return False def sync(self): """Put into ROM bootload mode & attempt to synchronize with the ESP ROM bootloader, we will retry a few times""" self.reset(True) for _ in range(5): if self._sync(): time.sleep(0.1) return True time.sleep(0.1) raise RuntimeError("Couldn't sync to ESP") @staticmethod def checksum(data, state=ESP_CHECKSUM_MAGIC): """Calculate checksum of a blob, as it is defined by the ROM""" for b in data: state ^= b return state @staticmethod def slip_encode(buffer): """Take a bytearray buffer and return back a new bytearray where 0xdb is replaced with 0xdb 0xdd and 0xc0 is replaced with 0xdb 0xdc""" encoded = [] for b in buffer: if b == 0xDB: encoded += [0xDB, 0xDD] elif b == 0xC0: encoded += [0xDB, 0xDC] else: encoded += [b] return bytearray(encoded)