From 36926d80ad21d4b2651e60c3678573447a588a2a Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 10 Jan 2022 02:07:12 +0100 Subject: [PATCH] Updated precompiled firmware --- Makefile | 2 +- Precompiled/esptool/esptool.py | 4582 +++++++++++++++++++ Precompiled/rnode_firmware_latest_tbeam.zip | Bin 163240 -> 243648 bytes 3 files changed, 4583 insertions(+), 1 deletion(-) create mode 100755 Precompiled/esptool/esptool.py diff --git a/Makefile b/Makefile index 8e1fe2f..a361594 100644 --- a/Makefile +++ b/Makefile @@ -29,5 +29,5 @@ release-tbeam: cp build/esp32.esp32.t-beam/RNode_Firmware.ino.bin build/rnode_firmware_latest_tbeam.bin cp build/esp32.esp32.t-beam/RNode_Firmware.ino.bootloader.bin build/rnode_firmware_latest_tbeam.bootloader cp build/esp32.esp32.t-beam/RNode_Firmware.ino.partitions.bin build/rnode_firmware_latest_tbeam.partitions - zip --junk-paths ./Precompiled/rnode_firmware_latest_tbeam.zip build/rnode_firmware_latest_tbeam.boot_app0 build/rnode_firmware_latest_tbeam.bin build/rnode_firmware_latest_tbeam.bootloader build/rnode_firmware_latest_tbeam.partitions + zip --junk-paths ./Precompiled/rnode_firmware_latest_tbeam.zip ./Precompiled/esptool/esptool.py build/rnode_firmware_latest_tbeam.boot_app0 build/rnode_firmware_latest_tbeam.bin build/rnode_firmware_latest_tbeam.bootloader build/rnode_firmware_latest_tbeam.partitions rm -r build diff --git a/Precompiled/esptool/esptool.py b/Precompiled/esptool/esptool.py new file mode 100755 index 0000000..01176af --- /dev/null +++ b/Precompiled/esptool/esptool.py @@ -0,0 +1,4582 @@ +#!/usr/bin/env python +# +# ESP8266 & ESP32 family ROM Bootloader Utility +# Copyright (C) 2014-2021 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted. +# https://github.com/espressif/esptool +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +# Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from __future__ import division, print_function + +import argparse +import base64 +import binascii +import copy +import hashlib +import inspect +import io +import itertools +import os +import shlex +import string +import struct +import sys +import time +import zlib + +try: + import serial +except ImportError: + print("Pyserial is not installed for %s. Check the README for installation instructions." % (sys.executable)) + raise + +# check 'serial' is 'pyserial' and not 'serial' https://github.com/espressif/esptool/issues/269 +try: + if "serialization" in serial.__doc__ and "deserialization" in serial.__doc__: + raise ImportError(""" +esptool.py depends on pyserial, but there is a conflict with a currently installed package named 'serial'. + +You may be able to work around this by 'pip uninstall serial; pip install pyserial' \ +but this may break other installed Python software that depends on 'serial'. + +There is no good fix for this right now, apart from configuring virtualenvs. \ +See https://github.com/espressif/esptool/issues/269#issuecomment-385298196 for discussion of the underlying issue(s).""") +except TypeError: + pass # __doc__ returns None for pyserial + +try: + import serial.tools.list_ports as list_ports +except ImportError: + print("The installed version (%s) of pyserial appears to be too old for esptool.py (Python interpreter %s). " + "Check the README for installation instructions." % (sys.VERSION, sys.executable)) + raise +except Exception: + if sys.platform == "darwin": + # swallow the exception, this is a known issue in pyserial+macOS Big Sur preview ref https://github.com/espressif/esptool/issues/540 + list_ports = None + else: + raise + + +__version__ = "3.1" + +MAX_UINT32 = 0xffffffff +MAX_UINT24 = 0xffffff + +DEFAULT_TIMEOUT = 3 # timeout for most flash operations +START_FLASH_TIMEOUT = 20 # timeout for starting flash (may perform erase) +CHIP_ERASE_TIMEOUT = 120 # timeout for full chip erase +MAX_TIMEOUT = CHIP_ERASE_TIMEOUT * 2 # longest any command can run +SYNC_TIMEOUT = 0.1 # timeout for syncing with bootloader +MD5_TIMEOUT_PER_MB = 8 # timeout (per megabyte) for calculating md5sum +ERASE_REGION_TIMEOUT_PER_MB = 30 # timeout (per megabyte) for erasing a region +ERASE_WRITE_TIMEOUT_PER_MB = 40 # timeout (per megabyte) for erasing and writing data +MEM_END_ROM_TIMEOUT = 0.05 # special short timeout for ESP_MEM_END, as it may never respond +DEFAULT_SERIAL_WRITE_TIMEOUT = 10 # timeout for serial port write +DEFAULT_CONNECT_ATTEMPTS = 7 # default number of times to try connection + + +def timeout_per_mb(seconds_per_mb, size_bytes): + """ Scales timeouts which are size-specific """ + result = seconds_per_mb * (size_bytes / 1e6) + if result < DEFAULT_TIMEOUT: + return DEFAULT_TIMEOUT + return result + + +def _chip_to_rom_loader(chip): + return { + 'esp8266': ESP8266ROM, + 'esp32': ESP32ROM, + 'esp32s2': ESP32S2ROM, + 'esp32s3beta2': ESP32S3BETA2ROM, + 'esp32s3beta3': ESP32S3BETA3ROM, + 'esp32c3': ESP32C3ROM, + 'esp32c6beta': ESP32C6BETAROM, + }[chip] + + +def get_default_connected_device(serial_list, port, connect_attempts, initial_baud, chip='auto', trace=False, + before='default_reset'): + _esp = None + for each_port in reversed(serial_list): + print("Serial port %s" % each_port) + try: + if chip == 'auto': + _esp = ESPLoader.detect_chip(each_port, initial_baud, before, trace, + connect_attempts) + else: + chip_class = _chip_to_rom_loader(chip) + _esp = chip_class(each_port, initial_baud, trace) + _esp.connect(before, connect_attempts) + break + except (FatalError, OSError) as err: + if port is not None: + raise + print("%s failed to connect: %s" % (each_port, err)) + _esp = None + return _esp + + +DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', + 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', + 0x18: '16MB', 0x19: '32MB', 0x1a: '64MB'} + + +def check_supported_function(func, check_func): + """ + Decorator implementation that wraps a check around an ESPLoader + bootloader function to check if it's supported. + + This is used to capture the multidimensional differences in + functionality between the ESP8266 & ESP32/32S2/32S3/32C3 ROM loaders, and the + software stub that runs on both. Not possible to do this cleanly + via inheritance alone. + """ + def inner(*args, **kwargs): + obj = args[0] + if check_func(obj): + return func(*args, **kwargs) + else: + raise NotImplementedInROMError(obj, func) + return inner + + +def stub_function_only(func): + """ Attribute for a function only supported in the software stub loader """ + return check_supported_function(func, lambda o: o.IS_STUB) + + +def stub_and_esp32_function_only(func): + """ Attribute for a function only supported by software stubs or ESP32/32S2/32S3/32C3 ROM """ + return check_supported_function(func, lambda o: o.IS_STUB or isinstance(o, ESP32ROM)) + + +PYTHON2 = sys.version_info[0] < 3 # True if on pre-Python 3 + +# Function to return nth byte of a bitstring +# Different behaviour on Python 2 vs 3 +if PYTHON2: + def byte(bitstr, index): + return ord(bitstr[index]) +else: + def byte(bitstr, index): + return bitstr[index] + +# Provide a 'basestring' class on Python 3 +try: + basestring +except NameError: + basestring = str + + +def print_overwrite(message, last_line=False): + """ Print a message, overwriting the currently printed line. + + If last_line is False, don't append a newline at the end (expecting another subsequent call will overwrite this one.) + + After a sequence of calls with last_line=False, call once with last_line=True. + + If output is not a TTY (for example redirected a pipe), no overwriting happens and this function is the same as print(). + """ + if sys.stdout.isatty(): + print("\r%s" % message, end='\n' if last_line else '') + else: + print(message) + + +def _mask_to_shift(mask): + """ Return the index of the least significant bit in the mask """ + shift = 0 + while mask & 0x1 == 0: + shift += 1 + mask >>= 1 + return shift + + +def esp8266_function_only(func): + """ Attribute for a function only supported on ESP8266 """ + return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") + + +class ESPLoader(object): + """ Base class providing access to ESP ROM & software stub bootloaders. + Subclasses provide ESP8266 & ESP32 specific functionality. + + Don't instantiate this base class directly, either instantiate a subclass or + call ESPLoader.detect_chip() which will interrogate the chip and return the + appropriate subclass instance. + + """ + CHIP_NAME = "Espressif device" + IS_STUB = False + + DEFAULT_PORT = "/dev/ttyUSB0" + + # 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 + + # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub) + ESP_SPI_SET_PARAMS = 0x0B + ESP_SPI_ATTACH = 0x0D + ESP_READ_FLASH_SLOW = 0x0e # ROM only, much slower than the stub flash read + ESP_CHANGE_BAUDRATE = 0x0F + ESP_FLASH_DEFL_BEGIN = 0x10 + ESP_FLASH_DEFL_DATA = 0x11 + ESP_FLASH_DEFL_END = 0x12 + ESP_SPI_FLASH_MD5 = 0x13 + + # Commands supported by ESP32-S2/S3/C3/C6 ROM bootloader only + ESP_GET_SECURITY_INFO = 0x14 + + # Some commands supported by stub only + ESP_ERASE_FLASH = 0xD0 + ESP_ERASE_REGION = 0xD1 + ESP_READ_FLASH = 0xD2 + ESP_RUN_USER_CODE = 0xD3 + + # Flash encryption encrypted data command + ESP_FLASH_ENCRYPT_DATA = 0xD4 + + # Response code(s) sent by ROM + ROM_INVALID_RECV_MSG = 0x05 # response if an invalid message is received + + # Maximum block sized for RAM and Flash writes, respectively. + ESP_RAM_BLOCK = 0x1800 + + FLASH_WRITE_SIZE = 0x400 + + # Default baudrate. The ROM auto-bauds, so we can use more or less whatever we want. + ESP_ROM_BAUD = 115200 + + # First byte of the application image + ESP_IMAGE_MAGIC = 0xe9 + + # Initial state for the checksum routine + ESP_CHECKSUM_MAGIC = 0xef + + # Flash sector size, minimum unit of erase. + FLASH_SECTOR_SIZE = 0x1000 + + UART_DATE_REG_ADDR = 0x60000078 + + CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000 # This ROM address has a different value on each chip model + + UART_CLKDIV_MASK = 0xFFFFF + + # Memory addresses + IROM_MAP_START = 0x40200000 + IROM_MAP_END = 0x40300000 + + # The number of bytes in the UART response that signify command status + STATUS_BYTES_LENGTH = 2 + + # Response to ESP_SYNC might indicate that flasher stub is running instead of the ROM bootloader + sync_stub_detected = False + + # Device PIDs + USB_JTAG_SERIAL_PID = 0x1001 + + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): + """Base constructor for ESPLoader bootloader interaction + + Don't call this constructor, either instantiate ESP8266ROM + or ESP32ROM, or use ESPLoader.detect_chip(). + + This base class has all of the instance methods for bootloader + functionality supported across various chips & stub + loaders. Subclasses replace the functions they don't support + with ones which throw NotImplementedInROMError(). + + """ + self.secure_download_mode = False # flag is set to True if esptool detects the ROM is in Secure Download Mode + + if isinstance(port, basestring): + self._port = serial.serial_for_url(port) + else: + self._port = port + self._slip_reader = slip_reader(self._port, self.trace) + # setting baud rate in a separate step is a workaround for + # CH341 driver on some Linux versions (this opens at 9600 then + # sets), shouldn't matter for other platforms/drivers. See + # https://github.com/espressif/esptool/issues/44#issuecomment-107094446 + self._set_port_baudrate(baud) + self._trace_enabled = trace_enabled + # set write timeout, to prevent esptool blocked at write forever. + try: + self._port.write_timeout = DEFAULT_SERIAL_WRITE_TIMEOUT + except NotImplementedError: + # no write timeout for RFC2217 ports + # need to set the property back to None or it will continue to fail + self._port.write_timeout = None + + @property + def serial_port(self): + return self._port.port + + def _set_port_baudrate(self, baud): + try: + self._port.baudrate = baud + except IOError: + raise FatalError("Failed to set baud rate %d. The driver may not support this rate." % baud) + + @staticmethod + def detect_chip(port=DEFAULT_PORT, baud=ESP_ROM_BAUD, connect_mode='default_reset', trace_enabled=False, + connect_attempts=DEFAULT_CONNECT_ATTEMPTS): + """ Use serial access to detect the chip type. + + We use the UART's datecode register for this, it's mapped at + the same address on ESP8266 & ESP32 so we can use one + memory read and compare to the datecode register for each chip + type. + + This routine automatically performs ESPLoader.connect() (passing + connect_mode parameter) as part of querying the chip. + """ + detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) + detect_port.connect(connect_mode, connect_attempts, detecting=True) + try: + print('Detecting chip type...', end='') + sys.stdout.flush() + chip_magic_value = detect_port.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM, ESP32C6BETAROM]: + if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE: + # don't connect a second time + inst = cls(detect_port._port, baud, trace_enabled=trace_enabled) + inst._post_connect() + print(' %s' % inst.CHIP_NAME, end='') + if detect_port.sync_stub_detected: + inst = inst.STUB_CLASS(inst) + inst.sync_stub_detected = True + return inst + except UnsupportedCommandError: + raise FatalError("Unsupported Command Error received. Probably this means Secure Download Mode is enabled, " + "autodetection will not work. Need to manually specify the chip.") + finally: + print('') # end line + raise FatalError("Unexpected CHIP magic value 0x%08x. Failed to autodetect chip type." % (chip_magic_value)) + + """ Read a SLIP packet from the serial port """ + def read(self): + return next(self._slip_reader) + + """ Write bytes to the serial port while performing SLIP escaping """ + def write(self, packet): + buf = b'\xc0' \ + + (packet.replace(b'\xdb', b'\xdb\xdd').replace(b'\xc0', b'\xdb\xdc')) \ + + b'\xc0' + self.trace("Write %d bytes: %s", len(buf), HexFormatter(buf)) + self._port.write(buf) + + def trace(self, message, *format_args): + if self._trace_enabled: + now = time.time() + try: + + delta = now - self._last_trace + except AttributeError: + delta = 0.0 + self._last_trace = now + prefix = "TRACE +%.3f " % delta + print(prefix + (message % format_args)) + + """ Calculate checksum of a blob, as it is defined by the ROM """ + @staticmethod + def checksum(data, state=ESP_CHECKSUM_MAGIC): + for b in data: + if type(b) is int: # python 2/3 compat + state ^= b + else: + state ^= ord(b) + + return state + + """ Send a request and read the response """ + def command(self, op=None, data=b"", chk=0, wait_response=True, timeout=DEFAULT_TIMEOUT): + saved_timeout = self._port.timeout + new_timeout = min(timeout, MAX_TIMEOUT) + if new_timeout != saved_timeout: + self._port.timeout = new_timeout + + try: + if op is not None: + self.trace("command op=0x%02x data len=%s wait_response=%d timeout=%.3f data=%s", + op, len(data), 1 if wait_response else 0, timeout, HexFormatter(data)) + pkt = struct.pack(b' self.STATUS_BYTES_LENGTH: + return data[:-self.STATUS_BYTES_LENGTH] + else: # otherwise, just return the 'val' field which comes from the reply header (this is used by read_reg) + return val + + def flush_input(self): + self._port.flushInput() + self._slip_reader = slip_reader(self._port, self.trace) + + def sync(self): + val, _ = self.command(self.ESP_SYNC, b'\x07\x07\x12\x20' + 32 * b'\x55', + timeout=SYNC_TIMEOUT) + + # ROM bootloaders send some non-zero "val" response. The flasher stub sends 0. If we receive 0 then it + # probably indicates that the chip wasn't or couldn't be reseted properly and esptool is talking to the + # flasher stub. + self.sync_stub_detected = val == 0 + + for _ in range(7): + val, _ = self.command() + self.sync_stub_detected &= val == 0 + + def _setDTR(self, state): + self._port.setDTR(state) + + def _setRTS(self, state): + self._port.setRTS(state) + # Work-around for adapters on Windows using the usbser.sys driver: + # generate a dummy change to DTR so that the set-control-line-state + # request is sent with the updated RTS state and the same DTR state + self._port.setDTR(self._port.dtr) + + def _get_pid(self): + if list_ports is None: + print("\nListing all serial ports is currently not available. Can't get device PID.") + return + active_port = self._port.port + + # Pyserial only identifies regular ports, URL handlers are not supported + if not active_port.startswith(("COM", "/dev/")): + print("\nDevice PID identification is only supported on COM and /dev/ serial ports.") + return + # Return the real path if the active port is a symlink + if active_port.startswith("/dev/") and os.path.islink(active_port): + active_port = os.path.realpath(active_port) + + ports = list_ports.comports() + for p in ports: + if p.device == active_port: + return p.pid + print("\nFailed to get PID of a device on {}, using standard reset sequence.".format(active_port)) + + def bootloader_reset(self, esp32r0_delay=False, usb_jtag_serial=False): + """ Issue a reset-to-bootloader, with esp32r0 workaround options + and USB-JTAG-Serial custom reset sequence option + """ + # RTS = either CH_PD/EN or nRESET (both active low = chip in reset) + # DTR = GPIO0 (active low = boot to flasher) + # + # DTR & RTS are active low signals, + # ie True = pin @ 0V, False = pin @ VCC. + if usb_jtag_serial: + # Custom reset sequence, which is required when the device + # is connecting via its USB-JTAG-Serial peripheral + self._setRTS(False) + self._setDTR(False) # Idle + time.sleep(0.1) + self._setDTR(True) # Set IO0 + self._setRTS(False) + time.sleep(0.1) + self._setRTS(True) # Reset. Note dtr/rts calls inverted so we go through (1,1) instead of (0,0) + self._setDTR(False) + self._setRTS(True) # Extra RTS set for RTS as Windows only propagates DTR on RTS setting + time.sleep(0.1) + self._setDTR(False) + self._setRTS(False) + else: + self._setDTR(False) # IO0=HIGH + self._setRTS(True) # EN=LOW, chip in reset + time.sleep(0.1) + if esp32r0_delay: + # Some chips are more likely to trigger the esp32r0 + # watchdog reset silicon bug if they're held with EN=LOW + # for a longer period + time.sleep(1.2) + self._setDTR(True) # IO0=LOW + self._setRTS(False) # EN=HIGH, chip out of reset + if esp32r0_delay: + # Sleep longer after reset. + # This workaround only works on revision 0 ESP32 chips, + # it exploits a silicon bug spurious watchdog reset. + time.sleep(0.4) # allow watchdog reset to occur + time.sleep(0.05) + self._setDTR(False) # IO0=HIGH, done + + def _connect_attempt(self, mode='default_reset', esp32r0_delay=False, usb_jtag_serial=False): + """ A single connection attempt, with esp32r0 workaround options """ + # esp32r0_delay is a workaround for bugs with the most common auto reset + # circuit and Windows, if the EN pin on the dev board does not have + # enough capacitance. + # + # Newer dev boards shouldn't have this problem (higher value capacitor + # on the EN pin), and ESP32 revision 1 can't use this workaround as it + # relies on a silicon bug. + # + # Details: https://github.com/espressif/esptool/issues/136 + last_error = None + + # If we're doing no_sync, we're likely communicating as a pass through + # with an intermediate device to the ESP32 + if mode == "no_reset_no_sync": + return last_error + + if mode != 'no_reset': + self.bootloader_reset(esp32r0_delay, usb_jtag_serial) + + for _ in range(5): + try: + self.flush_input() + self._port.flushOutput() + self.sync() + return None + except FatalError as e: + if esp32r0_delay: + print('_', end='') + else: + print('.', end='') + sys.stdout.flush() + time.sleep(0.05) + last_error = e + return last_error + + def get_memory_region(self, name): + """ Returns a tuple of (start, end) for the memory map entry with the given name, or None if it doesn't exist + """ + try: + return [(start, end) for (start, end, n) in self.MEMORY_MAP if n == name][0] + except IndexError: + return None + + def connect(self, mode='default_reset', attempts=DEFAULT_CONNECT_ATTEMPTS, detecting=False): + """ Try connecting repeatedly until successful, or giving up """ + if mode in ['no_reset', 'no_reset_no_sync']: + print('WARNING: Pre-connection option "{}" was selected.'.format(mode), + 'Connection may fail if the chip is not in bootloader or flasher stub mode.') + print('Connecting...', end='') + sys.stdout.flush() + last_error = None + + usb_jtag_serial = (mode == 'usb_reset') or (self._get_pid() == self.USB_JTAG_SERIAL_PID) + + try: + for _ in range(attempts) if attempts > 0 else itertools.count(): + last_error = self._connect_attempt(mode=mode, esp32r0_delay=False, usb_jtag_serial=usb_jtag_serial) + if last_error is None: + break + last_error = self._connect_attempt(mode=mode, esp32r0_delay=True, usb_jtag_serial=usb_jtag_serial) + if last_error is None: + break + finally: + print('') # end 'Connecting...' line + + if last_error is not None: + raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error)) + + if not detecting: + try: + # check the date code registers match what we expect to see + chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE: + actually = None + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM]: + if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE: + actually = cls + break + if actually is None: + print(("WARNING: This chip doesn't appear to be a %s (chip magic value 0x%08x). " + "Probably it is unsupported by this version of esptool.") % (self.CHIP_NAME, chip_magic_value)) + else: + raise FatalError("This chip is %s not %s. Wrong --chip argument?" % (actually.CHIP_NAME, self.CHIP_NAME)) + except UnsupportedCommandError: + self.secure_download_mode = True + self._post_connect() + + def _post_connect(self): + """ + Additional initialization hook, may be overridden by the chip-specific class. + Gets called after connect, and after auto-detection. + """ + pass + + def read_reg(self, addr, timeout=DEFAULT_TIMEOUT): + """ Read memory address in target """ + # we don't call check_command here because read_reg() function is called + # when detecting chip type, and the way we check for success (STATUS_BYTES_LENGTH) is different + # for different chip types (!) + val, data = self.command(self.ESP_READ_REG, struct.pack(' 0: + # add a dummy write to a date register as an excuse to have a delay + command += struct.pack(' start: + raise FatalError(("Software loader is resident at 0x%08x-0x%08x. " + "Can't load binary at overlapping address range 0x%08x-0x%08x. " + "Either change binary loading address, or use the --no-stub " + "option to disable the software loader.") % (start, end, load_start, load_end)) + + return self.check_command("enter RAM download mode", self.ESP_MEM_BEGIN, + struct.pack(' length: + raise FatalError('Read more than expected') + + digest_frame = self.read() + if len(digest_frame) != 16: + raise FatalError('Expected digest, got: %s' % hexify(digest_frame)) + expected_digest = hexify(digest_frame).upper() + digest = hashlib.md5(data).hexdigest().upper() + if digest != expected_digest: + raise FatalError('Digest mismatch: expected %s, got %s' % (expected_digest, digest)) + return data + + def flash_spi_attach(self, hspi_arg): + """Send SPI attach command to enable the SPI flash pins + + ESP8266 ROM does this when you send flash_begin, ESP32 ROM + has it as a SPI command. + """ + # last 3 bytes in ESP_SPI_ATTACH argument are reserved values + arg = struct.pack(' 0: + self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1) + if miso_bits > 0: + self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1) + else: + + def set_data_lengths(mosi_bits, miso_bits): + SPI_DATA_LEN_REG = SPI_USR1_REG + SPI_MOSI_BITLEN_S = 17 + SPI_MISO_BITLEN_S = 8 + mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1) + miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1) + self.write_reg(SPI_DATA_LEN_REG, + (miso_mask << SPI_MISO_BITLEN_S) | ( + mosi_mask << SPI_MOSI_BITLEN_S)) + + # SPI peripheral "command" bitmasks for SPI_CMD_REG + SPI_CMD_USR = (1 << 18) + + # shift values + SPI_USR2_COMMAND_LEN_SHIFT = 28 + + if read_bits > 32: + raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") + if len(data) > 64: + raise FatalError("Writing more than 64 bytes of data with one SPI command is unsupported") + + data_bits = len(data) * 8 + old_spi_usr = self.read_reg(SPI_USR_REG) + old_spi_usr2 = self.read_reg(SPI_USR2_REG) + flags = SPI_USR_COMMAND + if read_bits > 0: + flags |= SPI_USR_MISO + if data_bits > 0: + flags |= SPI_USR_MOSI + set_data_lengths(data_bits, read_bits) + self.write_reg(SPI_USR_REG, flags) + self.write_reg(SPI_USR2_REG, + (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command) + if data_bits == 0: + self.write_reg(SPI_W0_REG, 0) # clear data register before we read it + else: + data = pad_to(data, 4, b'\00') # pad to 32-bit multiple + words = struct.unpack("I" * (len(data) // 4), data) + next_reg = SPI_W0_REG + for word in words: + self.write_reg(next_reg, word) + next_reg += 4 + self.write_reg(SPI_CMD_REG, SPI_CMD_USR) + + def wait_done(): + for _ in range(10): + if (self.read_reg(SPI_CMD_REG) & SPI_CMD_USR) == 0: + return + raise FatalError("SPI command did not complete in time") + wait_done() + + status = self.read_reg(SPI_W0_REG) + # restore some SPI controller registers + self.write_reg(SPI_USR_REG, old_spi_usr) + self.write_reg(SPI_USR2_REG, old_spi_usr2) + return status + + def read_status(self, num_bytes=2): + """Read up to 24 bits (num_bytes) of SPI flash status register contents + via RDSR, RDSR2, RDSR3 commands + + Not all SPI flash supports all three commands. The upper 1 or 2 + bytes may be 0xFF. + """ + SPIFLASH_RDSR = 0x05 + SPIFLASH_RDSR2 = 0x35 + SPIFLASH_RDSR3 = 0x15 + + status = 0 + shift = 0 + for cmd in [SPIFLASH_RDSR, SPIFLASH_RDSR2, SPIFLASH_RDSR3][0:num_bytes]: + status += self.run_spiflash_command(cmd, read_bits=8) << shift + shift += 8 + return status + + def write_status(self, new_status, num_bytes=2, set_non_volatile=False): + """Write up to 24 bits (num_bytes) of new status register + + num_bytes can be 1, 2 or 3. + + Not all flash supports the additional commands to write the + second and third byte of the status register. When writing 2 + bytes, esptool also sends a 16-byte WRSR command (as some + flash types use this instead of WRSR2.) + + If the set_non_volatile flag is set, non-volatile bits will + be set as well as volatile ones (WREN used instead of WEVSR). + + """ + SPIFLASH_WRSR = 0x01 + SPIFLASH_WRSR2 = 0x31 + SPIFLASH_WRSR3 = 0x11 + SPIFLASH_WEVSR = 0x50 + SPIFLASH_WREN = 0x06 + SPIFLASH_WRDI = 0x04 + + enable_cmd = SPIFLASH_WREN if set_non_volatile else SPIFLASH_WEVSR + + # try using a 16-bit WRSR (not supported by all chips) + # this may be redundant, but shouldn't hurt + if num_bytes == 2: + self.run_spiflash_command(enable_cmd) + self.run_spiflash_command(SPIFLASH_WRSR, struct.pack(">= 8 + + self.run_spiflash_command(SPIFLASH_WRDI) + + def get_crystal_freq(self): + # Figure out the crystal frequency from the UART clock divider + # Returns a normalized value in integer MHz (40 or 26 are the only supported values) + # + # The logic here is: + # - We know that our baud rate and the ESP UART baud rate are roughly the same, or we couldn't communicate + # - We can read the UART clock divider register to know how the ESP derives this from the APB bus frequency + # - Multiplying these two together gives us the bus frequency which is either the crystal frequency (ESP32) + # or double the crystal frequency (ESP8266). See the self.XTAL_CLK_DIVIDER parameter for this factor. + uart_div = self.read_reg(self.UART_CLKDIV_REG) & self.UART_CLKDIV_MASK + est_xtal = (self._port.baudrate * uart_div) / 1e6 / self.XTAL_CLK_DIVIDER + norm_xtal = 40 if est_xtal > 33 else 26 + if abs(norm_xtal - est_xtal) > 1: + print("WARNING: Detected crystal freq %.2fMHz is quite different to normalized freq %dMHz. Unsupported crystal in use?" % (est_xtal, norm_xtal)) + return norm_xtal + + def hard_reset(self): + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + time.sleep(0.1) + self._setRTS(False) + + def soft_reset(self, stay_in_bootloader): + if not self.IS_STUB: + if stay_in_bootloader: + return # ROM bootloader is already in bootloader! + else: + # 'run user code' is as close to a soft reset as we can do + self.flash_begin(0, 0) + self.flash_finish(False) + else: + if stay_in_bootloader: + # soft resetting from the stub loader + # will re-load the ROM bootloader + self.flash_begin(0, 0) + self.flash_finish(True) + elif self.CHIP_NAME != "ESP8266": + raise FatalError("Soft resetting is currently only supported on ESP8266") + else: + # running user code from stub loader requires some hacks + # in the stub loader + self.command(self.ESP_RUN_USER_CODE, wait_response=False) + + +class ESP8266ROM(ESPLoader): + """ Access class for ESP8266 ROM bootloader + """ + CHIP_NAME = "ESP8266" + IS_STUB = False + + CHIP_DETECT_MAGIC_VALUE = [0xfff0c101] + + # OTP ROM addresses + ESP_OTP_MAC0 = 0x3ff00050 + ESP_OTP_MAC1 = 0x3ff00054 + ESP_OTP_MAC3 = 0x3ff0005c + + SPI_REG_BASE = 0x60000200 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = None + SPI_MISO_DLEN_OFFS = None + SPI_W0_OFFS = 0x40 + + UART_CLKDIV_REG = 0x60000014 + + XTAL_CLK_DIVIDER = 2 + + FLASH_SIZES = { + '512KB': 0x00, + '256KB': 0x10, + '1MB': 0x20, + '2MB': 0x30, + '4MB': 0x40, + '2MB-c1': 0x50, + '4MB-c1': 0x60, + '8MB': 0x80, + '16MB': 0x90, + } + + BOOTLOADER_FLASH_OFFSET = 0 + + MEMORY_MAP = [[0x3FF00000, 0x3FF00010, "DPORT"], + [0x3FFE8000, 0x40000000, "DRAM"], + [0x40100000, 0x40108000, "IRAM"], + [0x40201010, 0x402E1010, "IROM"]] + + def get_efuses(self): + # Return the 128 bits of ESP8266 efuse as a single Python integer + result = self.read_reg(0x3ff0005c) << 96 + result |= self.read_reg(0x3ff00058) << 64 + result |= self.read_reg(0x3ff00054) << 32 + result |= self.read_reg(0x3ff00050) + return result + + def _get_flash_size(self, efuses): + # rX_Y = EFUSE_DATA_OUTX[Y] + r0_4 = (efuses & (1 << 4)) != 0 + r3_25 = (efuses & (1 << 121)) != 0 + r3_26 = (efuses & (1 << 122)) != 0 + r3_27 = (efuses & (1 << 123)) != 0 + + if r0_4 and not r3_25: + if not r3_27 and not r3_26: + return 1 + elif not r3_27 and r3_26: + return 2 + if not r0_4 and r3_25: + if not r3_27 and not r3_26: + return 2 + elif not r3_27 and r3_26: + return 4 + return -1 + + def get_chip_description(self): + efuses = self.get_efuses() + is_8285 = (efuses & ((1 << 4) | 1 << 80)) != 0 # One or the other efuse bit is set for ESP8285 + if is_8285: + flash_size = self._get_flash_size(efuses) + max_temp = (efuses & (1 << 5)) != 0 # This efuse bit identifies the max flash temperature + chip_name = { + 1: "ESP8285H08" if max_temp else "ESP8285N08", + 2: "ESP8285H16" if max_temp else "ESP8285N16" + }.get(flash_size, "ESP8285") + return chip_name + return "ESP8266EX" + + def get_chip_features(self): + features = ["WiFi"] + if "ESP8285" in self.get_chip_description(): + features += ["Embedded Flash"] + return features + + def flash_spi_attach(self, hspi_arg): + if self.IS_STUB: + super(ESP8266ROM, self).flash_spi_attach(hspi_arg) + else: + # ESP8266 ROM has no flash_spi_attach command in serial protocol, + # but flash_begin will do it + self.flash_begin(0, 0) + + def flash_set_parameters(self, size): + # not implemented in ROM, but OK to silently skip for ROM + if self.IS_STUB: + super(ESP8266ROM, self).flash_set_parameters(size) + + def chip_id(self): + """ Read Chip ID from efuse - the equivalent of the SDK system_get_chip_id() function """ + id0 = self.read_reg(self.ESP_OTP_MAC0) + id1 = self.read_reg(self.ESP_OTP_MAC1) + return (id0 >> 24) | ((id1 & MAX_UINT24) << 8) + + def read_mac(self): + """ Read MAC from OTP ROM """ + mac0 = self.read_reg(self.ESP_OTP_MAC0) + mac1 = self.read_reg(self.ESP_OTP_MAC1) + mac3 = self.read_reg(self.ESP_OTP_MAC3) + if (mac3 != 0): + oui = ((mac3 >> 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 FatalError("Unknown OUI") + return oui + ((mac1 >> 8) & 0xff, mac1 & 0xff, (mac0 >> 24) & 0xff) + + 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.""" + + 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 + else: + return (num_sectors - head_sectors) * sector_size + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("Overriding VDDSDIO setting only applies to ESP32") + + +class ESP8266StubLoader(ESP8266ROM): + """ Access class for ESP8266 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + def get_erase_size(self, offset, size): + return size # stub doesn't have same size bug as ROM loader + + +ESP8266ROM.STUB_CLASS = ESP8266StubLoader + + +class ESP32ROM(ESPLoader): + """Access class for ESP32 ROM bootloader + + """ + CHIP_NAME = "ESP32" + IMAGE_CHIP_ID = 0 + IS_STUB = False + + CHIP_DETECT_MAGIC_VALUE = [0x00f01d83] + + IROM_MAP_START = 0x400d0000 + IROM_MAP_END = 0x40400000 + + DROM_MAP_START = 0x3F400000 + DROM_MAP_END = 0x3F800000 + + # ESP32 uses a 4 byte status reply + STATUS_BYTES_LENGTH = 4 + + SPI_REG_BASE = 0x3ff42000 + SPI_USR_OFFS = 0x1c + SPI_USR1_OFFS = 0x20 + SPI_USR2_OFFS = 0x24 + SPI_MOSI_DLEN_OFFS = 0x28 + SPI_MISO_DLEN_OFFS = 0x2c + EFUSE_RD_REG_BASE = 0x3ff5a000 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + 0x18 + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = (1 << 7) # EFUSE_RD_DISABLE_DL_ENCRYPT + + DR_REG_SYSCON_BASE = 0x3ff66000 + + SPI_W0_OFFS = 0x80 + + UART_CLKDIV_REG = 0x3ff40014 + + XTAL_CLK_DIVIDER = 1 + + FLASH_SIZES = { + '1MB': 0x00, + '2MB': 0x10, + '4MB': 0x20, + '8MB': 0x30, + '16MB': 0x40 + } + + BOOTLOADER_FLASH_OFFSET = 0x1000 + + OVERRIDE_VDDSDIO_CHOICES = ["1.8V", "1.9V", "OFF"] + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3F400000, 0x3F800000, "DROM"], + [0x3F800000, 0x3FC00000, "EXTRAM_DATA"], + [0x3FF80000, 0x3FF82000, "RTC_DRAM"], + [0x3FF90000, 0x40000000, "BYTE_ACCESSIBLE"], + [0x3FFAE000, 0x40000000, "DRAM"], + [0x3FFE0000, 0x3FFFFFFC, "DIRAM_DRAM"], + [0x40000000, 0x40070000, "IROM"], + [0x40070000, 0x40078000, "CACHE_PRO"], + [0x40078000, 0x40080000, "CACHE_APP"], + [0x40080000, 0x400A0000, "IRAM"], + [0x400A0000, 0x400BFFFC, "DIRAM_IRAM"], + [0x400C0000, 0x400C2000, "RTC_IRAM"], + [0x400D0000, 0x40400000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + FLASH_ENCRYPTED_WRITE_ALIGN = 32 + + """ Try to read the BLOCK1 (encryption key) and check if it is valid """ + + def is_flash_encryption_key_valid(self): + + """ Bit 0 of efuse_rd_disable[3:0] is mapped to BLOCK1 + this bit is at position 16 in EFUSE_BLK0_RDATA0_REG """ + word0 = self.read_efuse(0) + rd_disable = (word0 >> 16) & 0x1 + + # reading of BLOCK1 is NOT ALLOWED so we assume valid key is programmed + if rd_disable: + return True + else: + # reading of BLOCK1 is ALLOWED so we will read and verify for non-zero. + # When ESP32 has not generated AES/encryption key in BLOCK1, the contents will be readable and 0. + # If the flash encryption is enabled it is expected to have a valid non-zero key. We break out on + # first occurance of non-zero value + key_word = [0] * 7 + for i in range(len(key_word)): + key_word[i] = self.read_efuse(14 + i) + # key is non-zero so break & return + if key_word[i] != 0: + return True + return False + + def get_flash_crypt_config(self): + """ For flash encryption related commands we need to make sure + user has programmed all the relevant efuse correctly so before + writing encrypted write_flash_encrypt esptool will verify the values + of flash_crypt_config to be non zero if they are not read + protected. If the values are zero a warning will be printed + + bit 3 in efuse_rd_disable[3:0] is mapped to flash_crypt_config + this bit is at position 19 in EFUSE_BLK0_RDATA0_REG """ + word0 = self.read_efuse(0) + rd_disable = (word0 >> 19) & 0x1 + + if rd_disable == 0: + """ we can read the flash_crypt_config efuse value + so go & read it (EFUSE_BLK0_RDATA5_REG[31:28]) """ + word5 = self.read_efuse(5) + word5 = (word5 >> 28) & 0xF + return word5 + else: + # if read of the efuse is disabled we assume it is set correctly + return 0xF + + def get_encrypted_download_disabled(self): + if self.read_reg(self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG) & self.EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT: + return True + else: + return False + + def get_pkg_version(self): + word3 = self.read_efuse(3) + pkg_version = (word3 >> 9) & 0x07 + pkg_version += ((word3 >> 2) & 0x1) << 3 + return pkg_version + + def get_chip_revision(self): + word3 = self.read_efuse(3) + word5 = self.read_efuse(5) + apb_ctl_date = self.read_reg(self.DR_REG_SYSCON_BASE + 0x7C) + + rev_bit0 = (word3 >> 15) & 0x1 + rev_bit1 = (word5 >> 20) & 0x1 + rev_bit2 = (apb_ctl_date >> 31) & 0x1 + if rev_bit0: + if rev_bit1: + if rev_bit2: + return 3 + else: + return 2 + else: + return 1 + return 0 + + def get_chip_description(self): + pkg_version = self.get_pkg_version() + chip_revision = self.get_chip_revision() + rev3 = (chip_revision == 3) + single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU + + chip_name = { + 0: "ESP32-S0WDQ6" if single_core else "ESP32-D0WDQ6", + 1: "ESP32-S0WD" if single_core else "ESP32-D0WD", + 2: "ESP32-D2WD", + 4: "ESP32-U4WDH", + 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4", + 6: "ESP32-PICO-V3-02", + }.get(pkg_version, "unknown ESP32") + + # ESP32-D0WD-V3, ESP32-D0WDQ6-V3 + if chip_name.startswith("ESP32-D0WD") and rev3: + chip_name += "-V3" + + return "%s (revision %d)" % (chip_name, chip_revision) + + def get_chip_features(self): + features = ["WiFi"] + word3 = self.read_efuse(3) + + # names of variables in this section are lowercase + # versions of EFUSE names as documented in TRM and + # ESP-IDF efuse_reg.h + + chip_ver_dis_bt = word3 & (1 << 1) + if chip_ver_dis_bt == 0: + features += ["BT"] + + chip_ver_dis_app_cpu = word3 & (1 << 0) + if chip_ver_dis_app_cpu: + features += ["Single Core"] + else: + features += ["Dual Core"] + + chip_cpu_freq_rated = word3 & (1 << 13) + if chip_cpu_freq_rated: + chip_cpu_freq_low = word3 & (1 << 12) + if chip_cpu_freq_low: + features += ["160MHz"] + else: + features += ["240MHz"] + + pkg_version = self.get_pkg_version() + if pkg_version in [2, 4, 5, 6]: + features += ["Embedded Flash"] + + if pkg_version == 6: + features += ["Embedded PSRAM"] + + word4 = self.read_efuse(4) + adc_vref = (word4 >> 8) & 0x1F + if adc_vref: + features += ["VRef calibration in efuse"] + + blk3_part_res = word3 >> 14 & 0x1 + if blk3_part_res: + features += ["BLK3 partially reserved"] + + word6 = self.read_efuse(6) + coding_scheme = word6 & 0x3 + features += ["Coding Scheme %s" % { + 0: "None", + 1: "3/4", + 2: "Repeat (UNSUPPORTED)", + 3: "Invalid"}[coding_scheme]] + + return features + + def read_efuse(self, n): + """ Read the nth word of the ESP3x EFUSE region. """ + return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n)) + + def chip_id(self): + raise NotSupportedError(self, "chip_id") + + def read_mac(self): + """ Read MAC from EFUSE region """ + words = [self.read_efuse(2), self.read_efuse(1)] + bitstring = struct.pack(">II", *words) + bitstring = bitstring[2:8] # trim the 2 byte CRC + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_erase_size(self, offset, size): + return size + + def override_vddsdio(self, new_voltage): + new_voltage = new_voltage.upper() + if new_voltage not in self.OVERRIDE_VDDSDIO_CHOICES: + raise FatalError("The only accepted VDDSDIO overrides are '1.8V', '1.9V' and 'OFF'") + RTC_CNTL_SDIO_CONF_REG = 0x3ff48074 + RTC_CNTL_XPD_SDIO_REG = (1 << 31) + RTC_CNTL_DREFH_SDIO_M = (3 << 29) + RTC_CNTL_DREFM_SDIO_M = (3 << 27) + RTC_CNTL_DREFL_SDIO_M = (3 << 25) + # RTC_CNTL_SDIO_TIEH = (1 << 23) # not used here, setting TIEH=1 would set 3.3V output, not safe for esptool.py to do + RTC_CNTL_SDIO_FORCE = (1 << 22) + RTC_CNTL_SDIO_PD_EN = (1 << 21) + + reg_val = RTC_CNTL_SDIO_FORCE # override efuse setting + reg_val |= RTC_CNTL_SDIO_PD_EN + if new_voltage != "OFF": + reg_val |= RTC_CNTL_XPD_SDIO_REG # enable internal LDO + if new_voltage == "1.9V": + reg_val |= (RTC_CNTL_DREFH_SDIO_M | RTC_CNTL_DREFM_SDIO_M | RTC_CNTL_DREFL_SDIO_M) # boost voltage + self.write_reg(RTC_CNTL_SDIO_CONF_REG, reg_val) + print("VDDSDIO regulator set to %s" % new_voltage) + + def read_flash_slow(self, offset, length, progress_fn): + BLOCK_LEN = 64 # ROM read limit per command (this limit is why it's so slow) + + data = b'' + while len(data) < length: + block_len = min(BLOCK_LEN, length - len(data)) + r = self.check_command("read flash block", self.ESP_READ_FLASH_SLOW, + struct.pack('> 21) & 0x0F + return pkg_version + + def get_chip_description(self): + chip_name = { + 0: "ESP32-S2", + 1: "ESP32-S2FH16", + 2: "ESP32-S2FH32", + }.get(self.get_pkg_version(), "unknown ESP32-S2") + + return "%s" % (chip_name) + + def get_chip_features(self): + features = ["WiFi"] + + if self.secure_download_mode: + features += ["Secure Download Mode Enabled"] + + pkg_version = self.get_pkg_version() + + if pkg_version in [1, 2]: + if pkg_version == 1: + features += ["Embedded 2MB Flash"] + elif pkg_version == 2: + features += ["Embedded 4MB Flash"] + features += ["105C temp rating"] + + num_word = 4 + block2_addr = self.EFUSE_BASE + 0x05C + word4 = self.read_reg(block2_addr + (4 * num_word)) + block2_version = (word4 >> 4) & 0x07 + + if block2_version == 1: + features += ["ADC and temperature sensor calibration in BLK2 of efuse"] + return features + + def get_crystal_freq(self): + # ESP32-S2 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("ERROR: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this error, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + self._setRTS(False) + + +class ESP32S3ROM(ESP32ROM): + CHIP_NAME = "ESP32-S3" + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x44000000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3e000000 + + UART_DATE_REG_ADDR = 0x60000080 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1c + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + # todo: use espefuse APIs to get this info + EFUSE_BASE = 0x6001A000 # BLOCK0 read base address + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES256_KEY_1 = 2 + PURPOSE_VAL_XTS_AES256_KEY_2 = 3 + PURPOSE_VAL_XTS_AES128_KEY = 4 + + UART_CLKDIV_REG = 0x60000014 + + GPIO_STRAP_REG = 0x60004038 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3D000000, "DROM"], + [0x3D000000, 0x3E000000, "EXTRAM_DATA"], + [0x600FE000, 0x60100000, "RTC_DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FC88000, 0x403E2000, "MEM_INTERNAL"], + [0x3FC88000, 0x3FD00000, "DRAM"], + [0x40000000, 0x4001A100, "IROM_MASK"], + [0x40370000, 0x403E0000, "IRAM"], + [0x600FE000, 0x60100000, "RTC_IRAM"], + [0x42000000, 0x42800000, "IROM"], + [0x50000000, 0x50002000, "RTC_DATA"]] + + def get_chip_description(self): + return "ESP32-S3" + + def get_chip_features(self): + return ["WiFi", "BLE"] + + def get_crystal_freq(self): + # ESP32S3 XTAL is fixed to 40MHz + return 40 + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-S3 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see either an AES-128 key or two AES-256 keys + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + if any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes): + return True + + return any(p == self.PURPOSE_VAL_XTS_AES256_KEY_1 for p in purposes) \ + and any(p == self.PURPOSE_VAL_XTS_AES256_KEY_2 for p in purposes) + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-S3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + +class ESP32S3BETA2ROM(ESP32S3ROM): + CHIP_NAME = "ESP32-S3(beta2)" + IMAGE_CHIP_ID = 4 + + CHIP_DETECT_MAGIC_VALUE = [0xeb004136] + + def get_chip_description(self): + return "ESP32-S3(beta2)" + + +class ESP32S3BETA3ROM(ESP32S3ROM): + CHIP_NAME = "ESP32-S3(beta3)" + IMAGE_CHIP_ID = 6 + + CHIP_DETECT_MAGIC_VALUE = [0x9] + + def get_chip_description(self): + return "ESP32-S3(beta3)" + + +class ESP32C3ROM(ESP32ROM): + CHIP_NAME = "ESP32-C3" + IMAGE_CHIP_ID = 5 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1C + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + # Magic value for ESP32C3 eco 1+2 and ESP32C3 eco3 respectivly + CHIP_DETECT_MAGIC_VALUE = [0x6921506f, 0x1b31506f] + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x60008800 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES128_KEY = 4 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], + [0x3C000000, 0x3C800000, "DROM"], + [0x3FC80000, 0x3FCE0000, "DRAM"], + [0x3FC88000, 0x3FD00000, "BYTE_ACCESSIBLE"], + [0x3FF00000, 0x3FF20000, "DROM_MASK"], + [0x40000000, 0x40060000, "IROM_MASK"], + [0x42000000, 0x42800000, "IROM"], + [0x4037C000, 0x403E0000, "IRAM"], + [0x50000000, 0x50002000, "RTC_IRAM"], + [0x50000000, 0x50002000, "RTC_DRAM"], + [0x600FE000, 0x60100000, "MEM_INTERNAL2"]] + + def get_pkg_version(self): + num_word = 3 + block1_addr = self.EFUSE_BASE + 0x044 + word3 = self.read_reg(block1_addr + (4 * num_word)) + pkg_version = (word3 >> 21) & 0x0F + return pkg_version + + def get_chip_revision(self): + # reads WAFER_VERSION field from EFUSE_RD_MAC_SPI_SYS_3_REG + block1_addr = self.EFUSE_BASE + 0x044 + num_word = 3 + pos = 18 + return (self.read_reg(block1_addr + (4 * num_word)) & (0x7 << pos)) >> pos + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C3", + }.get(self.get_pkg_version(), "unknown ESP32-C3") + chip_revision = self.get_chip_revision() + + return "%s (revision %d)" % (chip_name, chip_revision) + + def get_chip_features(self): + return ["Wi-Fi"] + + def get_crystal_freq(self): + # ESP32C3 XTAL is fixed to 40MHz + return 40 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-C3") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-C3 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see an AES-128 key + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + + +class ESP32C6BETAROM(ESP32C3ROM): + CHIP_NAME = "ESP32-C6 BETA" + IMAGE_CHIP_ID = 7 + + CHIP_DETECT_MAGIC_VALUE = [0x0da1806f] + + UART_DATE_REG_ADDR = 0x00000500 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C6", + }.get(self.get_pkg_version(), "unknown ESP32-C6") + chip_revision = self.get_chip_revision() + + return "%s (revision %d)" % (chip_name, chip_revision) + + +class ESP32StubLoader(ESP32ROM): + """ Access class for ESP32 stub loader, runs on top of ROM. + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32ROM.STUB_CLASS = ESP32StubLoader + + +class ESP32S2StubLoader(ESP32S2ROM): + """ Access class for ESP32-S2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK + + +ESP32S2ROM.STUB_CLASS = ESP32S2StubLoader + + +class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader + + +class ESP32S3BETA3StubLoader(ESP32S3BETA3ROM): + """ Access class for ESP32S3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32S3BETA3ROM.STUB_CLASS = ESP32S3BETA3StubLoader + + +class ESP32C3StubLoader(ESP32C3ROM): + """ Access class for ESP32C3 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader + + +class ESPBOOTLOADER(object): + """ These are constants related to software ESP8266 bootloader, working with 'v2' image files """ + + # First byte of the "v2" application image + IMAGE_V2_MAGIC = 0xea + + # First 'segment' value in a "v2" application image, appears to be a constant version value? + IMAGE_V2_SEGMENT = 4 + + +def LoadFirmwareImage(chip, filename): + """ Load a firmware image. Can be for any supported SoC. + + ESP8266 images will be examined to determine if they are original ROM firmware images (ESP8266ROMFirmwareImage) + or "v2" OTA bootloader images. + + Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2). + """ + chip = chip.lower().replace("-", "") + with open(filename, 'rb') as f: + if chip == 'esp32': + return ESP32FirmwareImage(f) + elif chip == "esp32s2": + return ESP32S2FirmwareImage(f) + elif chip == "esp32s3beta2": + return ESP32S3BETA2FirmwareImage(f) + elif chip == "esp32s3beta3": + return ESP32S3BETA3FirmwareImage(f) + elif chip == 'esp32c3': + return ESP32C3FirmwareImage(f) + elif chip == 'esp32c6beta': + return ESP32C6BETAFirmwareImage(f) + else: # Otherwise, ESP8266 so look at magic to determine the image type + magic = ord(f.read(1)) + f.seek(0) + if magic == ESPLoader.ESP_IMAGE_MAGIC: + return ESP8266ROMFirmwareImage(f) + elif magic == ESPBOOTLOADER.IMAGE_V2_MAGIC: + return ESP8266V2FirmwareImage(f) + else: + raise FatalError("Invalid image magic number: %d" % magic) + + +class ImageSegment(object): + """ Wrapper class for a segment in an ESP image + (very similar to a section in an ELFImage also) """ + def __init__(self, addr, data, file_offs=None): + self.addr = addr + self.data = data + self.file_offs = file_offs + self.include_in_checksum = True + if self.addr != 0: + self.pad_to_alignment(4) # pad all "real" ImageSegments 4 byte aligned length + + def copy_with_new_addr(self, new_addr): + """ Return a new ImageSegment with same data, but mapped at + a new address. """ + return ImageSegment(new_addr, self.data, 0) + + def split_image(self, split_len): + """ Return a new ImageSegment which splits "split_len" bytes + from the beginning of the data. Remaining bytes are kept in + this segment object (and the start address is adjusted to match.) """ + result = copy.copy(self) + result.data = self.data[:split_len] + self.data = self.data[split_len:] + self.addr += split_len + self.file_offs = None + result.file_offs = None + return result + + def __repr__(self): + r = "len 0x%05x load 0x%08x" % (len(self.data), self.addr) + if self.file_offs is not None: + r += " file_offs 0x%08x" % (self.file_offs) + return r + + def get_memory_type(self, image): + """ + Return a list describing the memory type(s) that is covered by this + segment's start address. + """ + return [map_range[2] for map_range in image.ROM_LOADER.MEMORY_MAP if map_range[0] <= self.addr < map_range[1]] + + def pad_to_alignment(self, alignment): + self.data = pad_to(self.data, alignment, b'\x00') + + +class ELFSection(ImageSegment): + """ Wrapper class for a section in an ELF image, has a section + name as well as the common properties of an ImageSegment. """ + def __init__(self, name, addr, data): + super(ELFSection, self).__init__(addr, data) + self.name = name.decode("utf-8") + + def __repr__(self): + return "%s %s" % (self.name, super(ELFSection, self).__repr__()) + + +class BaseFirmwareImage(object): + SEG_HEADER_LEN = 8 + SHA256_DIGEST_LEN = 32 + + """ Base class with common firmware image functions """ + def __init__(self): + self.segments = [] + self.entrypoint = 0 + self.elf_sha256 = None + self.elf_sha256_offset = 0 + + def load_common_header(self, load_file, expected_magic): + (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' 16: + raise FatalError('Invalid segment count %d (max 16). Usually this indicates a linker script problem.' % len(self.segments)) + + def load_segment(self, f, is_irom_segment=False): + """ Load the next segment from the image file """ + file_offs = f.tell() + (offset, size) = struct.unpack(' 0x40200000 or offset < 0x3ffe0000 or size > 65536: + print('WARNING: Suspicious segment 0x%x, length %d' % (offset, size)) + + def maybe_patch_segment_data(self, f, segment_data): + """If SHA256 digest of the ELF file needs to be inserted into this segment, do so. Returns segment data.""" + segment_len = len(segment_data) + file_pos = f.tell() # file_pos is position in the .bin file + if self.elf_sha256_offset >= file_pos and self.elf_sha256_offset < file_pos + segment_len: + # SHA256 digest needs to be patched into this binary segment, + # calculate offset of the digest inside the binary segment. + patch_offset = self.elf_sha256_offset - file_pos + # Sanity checks + if patch_offset < self.SEG_HEADER_LEN or patch_offset + self.SHA256_DIGEST_LEN > segment_len: + raise FatalError('Cannot place SHA256 digest on segment boundary' + '(elf_sha256_offset=%d, file_pos=%d, segment_size=%d)' % + (self.elf_sha256_offset, file_pos, segment_len)) + # offset relative to the data part + patch_offset -= self.SEG_HEADER_LEN + if segment_data[patch_offset:patch_offset + self.SHA256_DIGEST_LEN] != b'\x00' * self.SHA256_DIGEST_LEN: + raise FatalError('Contents of segment at SHA256 digest offset 0x%x are not all zero. Refusing to overwrite.' % + self.elf_sha256_offset) + assert(len(self.elf_sha256) == self.SHA256_DIGEST_LEN) + segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \ + segment_data[patch_offset + self.SHA256_DIGEST_LEN:] + return segment_data + + def save_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_data = self.maybe_patch_segment_data(f, segment.data) + f.write(struct.pack(' 0: + if len(irom_segments) != 1: + raise FatalError('Found %d segments that could be irom0. Bad ELF file?' % len(irom_segments)) + return irom_segments[0] + return None + + def get_non_irom_segments(self): + irom_segment = self.get_irom_segment() + return [s for s in self.segments if s != irom_segment] + + def merge_adjacent_segments(self): + if not self.segments: + return # nothing to merge + + segments = [] + # The easiest way to merge the sections is the browse them backward. + for i in range(len(self.segments) - 1, 0, -1): + # elem is the previous section, the one `next_elem` may need to be + # merged in + elem = self.segments[i - 1] + next_elem = self.segments[i] + if all((elem.get_memory_type(self) == next_elem.get_memory_type(self), + elem.include_in_checksum == next_elem.include_in_checksum, + next_elem.addr == elem.addr + len(elem.data))): + # Merge any segment that ends where the next one starts, without spanning memory types + # + # (don't 'pad' any gaps here as they may be excluded from the image due to 'noinit' + # or other reasons.) + elem.data += next_elem.data + else: + # The section next_elem cannot be merged into the previous one, + # which means it needs to be part of the final segments. + # As we are browsing the list backward, the elements need to be + # inserted at the beginning of the final list. + segments.insert(0, next_elem) + + # The first segment will always be here as it cannot be merged into any + # "previous" section. + segments.insert(0, self.segments[0]) + + # note: we could sort segments here as well, but the ordering of segments is sometimes + # important for other reasons (like embedded ELF SHA-256), so we assume that the linker + # script will have produced any adjacent sections in linear order in the ELF, anyhow. + self.segments = segments + + +class ESP8266ROMFirmwareImage(BaseFirmwareImage): + """ 'Version 1' firmware image, segments loaded directly by the ROM bootloader. """ + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266ROMFirmwareImage, self).__init__() + self.flash_mode = 0 + self.flash_size_freq = 0 + self.version = 1 + + if load_file is not None: + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + return input_file + '-' + + def save(self, basename): + """ Save a set of V1 images for flashing. Parameter is a base filename. """ + # IROM data goes in its own plain binary file + irom_segment = self.get_irom_segment() + if irom_segment is not None: + with open("%s0x%05x.bin" % (basename, irom_segment.addr - ESP8266ROM.IROM_MAP_START), "wb") as f: + f.write(irom_segment.data) + + # everything but IROM goes at 0x00000 in an image file + normal_segments = self.get_non_irom_segments() + with open("%s0x00000.bin" % basename, 'wb') as f: + self.write_common_header(f, normal_segments) + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + for segment in normal_segments: + checksum = self.save_segment(f, segment, checksum) + self.append_checksum(f, checksum) + + +ESP8266ROM.BOOTLOADER_IMAGE = ESP8266ROMFirmwareImage + + +class ESP8266V2FirmwareImage(BaseFirmwareImage): + """ 'Version 2' firmware image, segments loaded by software bootloader stub + (ie Espressif bootloader or rboot) + """ + + ROM_LOADER = ESP8266ROM + + def __init__(self, load_file=None): + super(ESP8266V2FirmwareImage, self).__init__() + self.version = 2 + if load_file is not None: + segments = self.load_common_header(load_file, ESPBOOTLOADER.IMAGE_V2_MAGIC) + if segments != ESPBOOTLOADER.IMAGE_V2_SEGMENT: + # segment count is not really segment count here, but we expect to see '4' + print('Warning: V2 header has unexpected "segment" count %d (usually 4)' % segments) + + # irom segment comes before the second header + # + # the file is saved in the image with a zero load address + # in the header, so we need to calculate a load address + irom_segment = self.load_segment(load_file, True) + irom_segment.addr = 0 # for actual mapped addr, add ESP8266ROM.IROM_MAP_START + flashing_addr + 8 + irom_segment.include_in_checksum = False + + first_flash_mode = self.flash_mode + first_flash_size_freq = self.flash_size_freq + first_entrypoint = self.entrypoint + # load the second header + + segments = self.load_common_header(load_file, ESPLoader.ESP_IMAGE_MAGIC) + + if first_flash_mode != self.flash_mode: + print('WARNING: Flash mode value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' + % (first_flash_mode, self.flash_mode)) + if first_flash_size_freq != self.flash_size_freq: + print('WARNING: Flash size/freq value in first header (0x%02x) disagrees with second (0x%02x). Using second value.' + % (first_flash_size_freq, self.flash_size_freq)) + if first_entrypoint != self.entrypoint: + print('WARNING: Entrypoint address in first header (0x%08x) disagrees with second header (0x%08x). Using second value.' + % (first_entrypoint, self.entrypoint)) + + # load all the usual segments + for _ in range(segments): + self.load_segment(load_file) + self.checksum = self.read_checksum(load_file) + + self.verify() + + def default_output_name(self, input_file): + """ Derive a default output name from the ELF name. """ + irom_segment = self.get_irom_segment() + if irom_segment is not None: + irom_offs = irom_segment.addr - ESP8266ROM.IROM_MAP_START + else: + irom_offs = 0 + return "%s-0x%05x.bin" % (os.path.splitext(input_file)[0], + irom_offs & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) + + def save(self, filename): + with open(filename, 'wb') as f: + # Save first header for irom0 segment + f.write(struct.pack(b' 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + def get_alignment_data_needed(segment): + # Actual alignment (in data bytes) required for a segment header: positioned so that + # after we write the next 8 byte header, file_offs % IROM_ALIGN == segment.addr % IROM_ALIGN + # + # (this is because the segment's vaddr may not be IROM_ALIGNed, more likely is aligned + # IROM_ALIGN+0x18 to account for the binary file header + align_past = (segment.addr % self.IROM_ALIGN) - self.SEG_HEADER_LEN + pad_len = (self.IROM_ALIGN - (f.tell() % self.IROM_ALIGN)) + align_past + if pad_len == 0 or pad_len == self.IROM_ALIGN: + return 0 # already aligned + + # subtract SEG_HEADER_LEN a second time, as the padding block has a header as well + pad_len -= self.SEG_HEADER_LEN + if pad_len < 0: + pad_len += self.IROM_ALIGN + return pad_len + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + pad_len = get_alignment_data_needed(segment) + if pad_len > 0: # need to pad + if len(ram_segments) > 0 and pad_len > self.SEG_HEADER_LEN: + pad_segment = ram_segments[0].split_image(pad_len) + if len(ram_segments[0].data) == 0: + ram_segments.pop(0) + else: + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + else: + # write the flash segment + assert (f.tell() + 8) % self.IROM_ALIGN == segment.addr % self.IROM_ALIGN + checksum = self.save_flash_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + if self.secure_pad: + # pad the image so that after signing it will end on a a 64KB boundary. + # This ensures all mapped flash content will be verified. + if not self.append_digest: + raise FatalError("secure_pad only applies if a SHA-256 digest is also appended to the image") + align_past = (f.tell() + self.SEG_HEADER_LEN) % self.IROM_ALIGN + # 16 byte aligned checksum (force the alignment to simplify calculations) + checksum_space = 16 + if self.secure_pad == '1': + # after checksum: SHA-256 digest + (to be added by signing process) version, signature + 12 trailing bytes due to alignment + space_after_checksum = 32 + 4 + 64 + 12 + elif self.secure_pad == '2': # Secure Boot V2 + # after checksum: SHA-256 digest + signature sector, but we place signature sector after the 64KB boundary + space_after_checksum = 32 + pad_len = (self.IROM_ALIGN - align_past - checksum_space - space_after_checksum) % self.IROM_ALIGN + pad_segment = ImageSegment(0, b'\x00' * pad_len, f.tell()) + + checksum = self.save_segment(f, pad_segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + if self.secure_pad: + assert ((image_length + space_after_checksum) % self.IROM_ALIGN) == 0 + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def save_flash_segment(self, f, segment, checksum=None): + """ Save the next segment to the image file, return next checksum value if provided """ + segment_end_pos = f.tell() + len(segment.data) + self.SEG_HEADER_LEN + segment_len_remainder = segment_end_pos % self.IROM_ALIGN + if segment_len_remainder < 0x24: + # Work around a bug in ESP-IDF 2nd stage bootloader, that it didn't map the + # last MMU page, if an IROM/DROM segment was < 0x24 bytes over the page boundary. + segment.data += b'\x00' * (0x24 - segment_len_remainder) + return self.save_segment(f, segment, checksum) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + chip_id = fields[4] + if chip_id != self.ROM_LOADER.IMAGE_CHIP_ID: + print(("Unexpected chip id in image. Expected %d but value was %d. " + "Is this image for a different chip model?") % (self.ROM_LOADER.IMAGE_CHIP_ID, chip_id)) + + # reserved fields in the middle should all be zero + if any(f for f in fields[6:-1] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + append_digest = fields[-1] # last byte is append_digest + if append_digest in [0, 1]: + self.append_digest = (append_digest == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", append_digest) + + def save_extended_header(self, save_file): + def join_byte(ln, hn): + return (ln & 0x0F) + ((hn & 0x0F) << 4) + + append_digest = 1 if self.append_digest else 0 + + fields = [self.wp_pin, + join_byte(self.clk_drv, self.q_drv), + join_byte(self.d_drv, self.cs_drv), + join_byte(self.hd_drv, self.wp_drv), + self.ROM_LOADER.IMAGE_CHIP_ID, + self.min_rev] + fields += [0] * 8 # padding + fields += [append_digest] + + packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields) + save_file.write(packed) + + +ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage + + +class ESP32S2FirmwareImage(ESP32FirmwareImage): + """ ESP32S2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S2ROM + + +ESP32S2ROM.BOOTLOADER_IMAGE = ESP32S2FirmwareImage + + +class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3BETA2ROM + + +ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage + + +class ESP32S3BETA3FirmwareImage(ESP32FirmwareImage): + """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32S3BETA3ROM + + +ESP32S3BETA3ROM.BOOTLOADER_IMAGE = ESP32S3BETA3FirmwareImage + + +class ESP32C3FirmwareImage(ESP32FirmwareImage): + """ ESP32C3 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C3ROM + + +ESP32C3ROM.BOOTLOADER_IMAGE = ESP32C3FirmwareImage + + +class ESP32C6BETAFirmwareImage(ESP32FirmwareImage): + """ ESP32C6 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C6BETAROM + + +ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage + + +class ELFFile(object): + SEC_TYPE_PROGBITS = 0x01 + SEC_TYPE_STRTAB = 0x03 + + LEN_SEC_HEADER = 0x28 + + SEG_TYPE_LOAD = 0x01 + LEN_SEG_HEADER = 0x20 + + def __init__(self, name): + # Load sections from the ELF file + self.name = name + with open(self.name, 'rb') as f: + self._read_elf_file(f) + + def get_section(self, section_name): + for s in self.sections: + if s.name == section_name: + return s + raise ValueError("No section %s in ELF file" % section_name) + + def _read_elf_file(self, f): + # read the ELF file header + LEN_FILE_HEADER = 0x34 + try: + (ident, _type, machine, _version, + self.entrypoint, _phoff, shoff, _flags, + _ehsize, _phentsize, _phnum, shentsize, + shnum, shstrndx) = struct.unpack("<16sHHLLLLLHHHHHH", f.read(LEN_FILE_HEADER)) + except struct.error as e: + raise FatalError("Failed to read a valid ELF header from %s: %s" % (self.name, e)) + + if byte(ident, 0) != 0x7f or ident[1:4] != b'ELF': + raise FatalError("%s has invalid ELF magic header" % self.name) + if machine not in [0x5e, 0xf3]: + raise FatalError("%s does not appear to be an Xtensa or an RISCV ELF file. e_machine=%04x" % (self.name, machine)) + if shentsize != self.LEN_SEC_HEADER: + raise FatalError("%s has unexpected section header entry size 0x%x (not 0x%x)" % (self.name, shentsize, self.LEN_SEC_HEADER)) + if shnum == 0: + raise FatalError("%s has 0 section headers" % (self.name)) + self._read_sections(f, shoff, shnum, shstrndx) + self._read_segments(f, _phoff, _phnum, shstrndx) + + def _read_sections(self, f, section_header_offs, section_header_count, shstrndx): + f.seek(section_header_offs) + len_bytes = section_header_count * self.LEN_SEC_HEADER + section_header = f.read(len_bytes) + if len(section_header) == 0: + raise FatalError("No section header found at offset %04x in ELF file." % section_header_offs) + if len(section_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from section header (expected 0x%x.) Truncated ELF file?" % (len(section_header), len_bytes)) + + # walk through the section header and extract all sections + section_header_offsets = range(0, len(section_header), self.LEN_SEC_HEADER) + + def read_section_header(offs): + name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from(" 0] + self.sections = prog_sections + + def _read_segments(self, f, segment_header_offs, segment_header_count, shstrndx): + f.seek(segment_header_offs) + len_bytes = segment_header_count * self.LEN_SEG_HEADER + segment_header = f.read(len_bytes) + if len(segment_header) == 0: + raise FatalError("No segment header found at offset %04x in ELF file." % segment_header_offs) + if len(segment_header) != (len_bytes): + raise FatalError("Only read 0x%x bytes from segment header (expected 0x%x.) Truncated ELF file?" % (len(segment_header), len_bytes)) + + # walk through the segment header and extract all segments + segment_header_offsets = range(0, len(segment_header), self.LEN_SEG_HEADER) + + def read_segment_header(offs): + seg_type, seg_offs, _vaddr, lma, size, _memsize, _flags, _align = struct.unpack_from(" 0] + self.segments = prog_segments + + def sha256(self): + # return SHA256 hash of the input ELF file + sha256 = hashlib.sha256() + with open(self.name, 'rb') as f: + sha256.update(f.read()) + return sha256.digest() + + +def slip_reader(port, trace_function): + """Generator to read SLIP packets from a serial port. + Yields one full SLIP packet at a time, raises exception on timeout or invalid data. + + Designed to avoid too many calls to serial.read(1), which can bog + down on slow systems. + """ + partial_packet = None + in_escape = False + while True: + waiting = port.inWaiting() + read_bytes = port.read(1 if waiting == 0 else waiting) + if read_bytes == b'': + waiting_for = "header" if partial_packet is None else "content" + trace_function("Timed out waiting for packet %s", waiting_for) + raise FatalError("Timed out waiting for packet %s" % waiting_for) + trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes)) + for b in read_bytes: + if type(b) is int: + b = bytes([b]) # python 2/3 compat + + if partial_packet is None: # waiting for packet header + if b == b'\xc0': + partial_packet = b"" + else: + trace_function("Read invalid data: %s", HexFormatter(read_bytes)) + trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) + raise FatalError('Invalid head of packet (0x%s)' % hexify(b)) + elif in_escape: # part-way through escape sequence + in_escape = False + if b == b'\xdc': + partial_packet += b'\xc0' + elif b == b'\xdd': + partial_packet += b'\xdb' + else: + trace_function("Read invalid data: %s", HexFormatter(read_bytes)) + trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting()))) + raise FatalError('Invalid SLIP escape (0xdb, 0x%s)' % (hexify(b))) + elif b == b'\xdb': # start of escape sequence + in_escape = True + elif b == b'\xc0': # end of packet + trace_function("Received full packet: %s", HexFormatter(partial_packet)) + yield partial_packet + partial_packet = None + else: # normal byte in packet + partial_packet += b + + +def arg_auto_int(x): + return int(x, 0) + + +def div_roundup(a, b): + """ Return a/b rounded up to nearest integer, + equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only + without possible floating point accuracy errors. + """ + return (int(a) + int(b) - 1) // int(b) + + +def align_file_position(f, size): + """ Align the position in the file to the next block of specified size """ + align = (size - 1) - (f.tell() % size) + f.seek(align, 1) + + +def flash_size_bytes(size): + """ Given a flash size of the type passed in args.flash_size + (ie 512KB or 1MB) then return the size in bytes. + """ + if "MB" in size: + return int(size[:size.index("MB")]) * 1024 * 1024 + elif "KB" in size: + return int(size[:size.index("KB")]) * 1024 + else: + raise FatalError("Unknown size %s" % size) + + +def hexify(s, uppercase=True): + format_str = '%02X' if uppercase else '%02x' + if not PYTHON2: + return ''.join(format_str % c for c in s) + else: + return ''.join(format_str % ord(c) for c in s) + + +class HexFormatter(object): + """ + Wrapper class which takes binary data in its constructor + and returns a hex string as it's __str__ method. + + This is intended for "lazy formatting" of trace() output + in hex format. Avoids overhead (significant on slow computers) + of generating long hex strings even if tracing is disabled. + + Note that this doesn't save any overhead if passed as an + argument to "%", only when passed to trace() + + If auto_split is set (default), any long line (> 16 bytes) will be + printed as separately indented lines, with ASCII decoding at the end + of each line. + """ + def __init__(self, binary_string, auto_split=True): + self._s = binary_string + self._auto_split = auto_split + + def __str__(self): + if self._auto_split and len(self._s) > 16: + result = "" + s = self._s + while len(s) > 0: + line = s[:16] + ascii_line = "".join(c if (c == ' ' or (c in string.printable and c not in string.whitespace)) + else '.' for c in line.decode('ascii', 'replace')) + s = s[16:] + result += "\n %-16s %-16s | %s" % (hexify(line[:8], False), hexify(line[8:], False), ascii_line) + return result + else: + return hexify(self._s, False) + + +def pad_to(data, alignment, pad_character=b'\xFF'): + """ Pad to the next alignment boundary """ + pad_mod = len(data) % alignment + if pad_mod != 0: + data += pad_character * (alignment - pad_mod) + return data + + +class FatalError(RuntimeError): + """ + Wrapper class for runtime errors that aren't caused by internal bugs, but by + ESP8266 responses or input content. + """ + def __init__(self, message): + RuntimeError.__init__(self, message) + + @staticmethod + def WithResult(message, result): + """ + Return a fatal error object that appends the hex values of + 'result' as a string formatted argument. + """ + message += " (result was %s)" % hexify(result) + return FatalError(message) + + +class NotImplementedInROMError(FatalError): + """ + Wrapper class for the error thrown when a particular ESP bootloader function + is not implemented in the ROM bootloader. + """ + def __init__(self, bootloader, func): + FatalError.__init__(self, "%s ROM does not support function %s." % (bootloader.CHIP_NAME, func.__name__)) + + +class NotSupportedError(FatalError): + def __init__(self, esp, function_name): + FatalError.__init__(self, "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME)) + +# "Operation" commands, executable at command line. One function each +# +# Each function takes either two args (, ) or a single +# argument. + + +class UnsupportedCommandError(RuntimeError): + """ + Wrapper class for when ROM loader returns an invalid command response. + + Usually this indicates the loader is running in Secure Download Mode. + """ + def __init__(self, esp, op): + if esp.secure_download_mode: + msg = "This command (0x%x) is not supported in Secure Download Mode" % op + else: + msg = "Invalid (unsupported) command 0x%x" % op + RuntimeError.__init__(self, msg) + + +def load_ram(esp, args): + image = LoadFirmwareImage(esp.CHIP_NAME, args.filename) + + print('RAM boot...') + for seg in image.segments: + size = len(seg.data) + print('Downloading %d bytes at %08x...' % (size, seg.addr), end=' ') + sys.stdout.flush() + esp.mem_begin(size, div_roundup(size, esp.ESP_RAM_BLOCK), esp.ESP_RAM_BLOCK, seg.addr) + + seq = 0 + while len(seg.data) > 0: + esp.mem_block(seg.data[0:esp.ESP_RAM_BLOCK], seq) + seg.data = seg.data[esp.ESP_RAM_BLOCK:] + seq += 1 + print('done!') + + print('All segments done, executing at %08x' % image.entrypoint) + esp.mem_finish(image.entrypoint) + + +def read_mem(esp, args): + print('0x%08x = 0x%08x' % (args.address, esp.read_reg(args.address))) + + +def write_mem(esp, args): + esp.write_reg(args.address, args.value, args.mask, 0) + print('Wrote %08x, mask %08x to %08x' % (args.value, args.mask, args.address)) + + +def dump_mem(esp, args): + with open(args.filename, 'wb') as f: + for i in range(args.size // 4): + d = esp.read_reg(args.address + (i * 4)) + f.write(struct.pack(b'> 16 + args.flash_size = DETECTED_FLASH_SIZES.get(size_id) + if args.flash_size is None: + print('Warning: Could not auto-detect Flash size (FlashID=0x%x, SizeID=0x%x), defaulting to 4MB' % (flash_id, size_id)) + args.flash_size = '4MB' + else: + print('Auto-detected Flash size:', args.flash_size) + + +def _update_image_flash_params(esp, address, args, image): + """ Modify the flash mode & size bytes if this looks like an executable bootloader image """ + if len(image) < 8: + return image # not long enough to be a bootloader image + + # unpack the (potential) image header + magic, _, flash_mode, flash_size_freq = struct.unpack("BBBB", image[:4]) + if address != esp.BOOTLOADER_FLASH_OFFSET: + return image # not flashing bootloader offset, so don't modify this + + if (args.flash_mode, args.flash_freq, args.flash_size) == ('keep',) * 3: + return image # all settings are 'keep', not modifying anything + + # easy check if this is an image: does it start with a magic byte? + if magic != esp.ESP_IMAGE_MAGIC: + print("Warning: Image file at 0x%x doesn't look like an image file, so not changing any flash settings." % address) + return image + + # make sure this really is an image, and not just data that + # starts with esp.ESP_IMAGE_MAGIC (mostly a problem for encrypted + # images that happen to start with a magic byte + try: + test_image = esp.BOOTLOADER_IMAGE(io.BytesIO(image)) + test_image.verify() + except Exception: + print("Warning: Image file at 0x%x is not a valid %s image, so not changing any flash settings." % (address, esp.CHIP_NAME)) + return image + + if args.flash_mode != 'keep': + flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + + flash_freq = flash_size_freq & 0x0F + if args.flash_freq != 'keep': + flash_freq = {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] + + flash_size = flash_size_freq & 0xF0 + if args.flash_size != 'keep': + flash_size = esp.parse_flash_size_arg(args.flash_size) + + flash_params = struct.pack(b'BB', flash_mode, flash_size + flash_freq) + if flash_params != image[2:4]: + print('Flash params set to 0x%04x' % struct.unpack(">H", flash_params)) + image = image[0:2] + flash_params + image[4:] + return image + + +def write_flash(esp, args): + # set args.compress based on default behaviour: + # -> if either --compress or --no-compress is set, honour that + # -> otherwise, set --compress unless --no-stub is set + if args.compress is None and not args.no_compress: + args.compress = not args.no_stub + + # In case we have encrypted files to write, we first do few sanity checks before actual flash + if args.encrypt or args.encrypt_files is not None: + do_write = True + + if not esp.secure_download_mode: + if esp.get_encrypted_download_disabled(): + raise FatalError("This chip has encrypt functionality in UART download mode disabled. " + "This is the Flash Encryption configuration for Production mode instead of Development mode.") + + crypt_cfg_efuse = esp.get_flash_crypt_config() + + if crypt_cfg_efuse is not None and crypt_cfg_efuse != 0xF: + print('Unexpected FLASH_CRYPT_CONFIG value: 0x%x' % (crypt_cfg_efuse)) + do_write = False + + enc_key_valid = esp.is_flash_encryption_key_valid() + + if not enc_key_valid: + print('Flash encryption key is not programmed') + do_write = False + + # Determine which files list contain the ones to encrypt + files_to_encrypt = args.addr_filename if args.encrypt else args.encrypt_files + + for address, argfile in files_to_encrypt: + if address % esp.FLASH_ENCRYPTED_WRITE_ALIGN: + print("File %s address 0x%x is not %d byte aligned, can't flash encrypted" % + (argfile.name, address, esp.FLASH_ENCRYPTED_WRITE_ALIGN)) + do_write = False + + if not do_write and not args.ignore_flash_encryption_efuse_setting: + raise FatalError("Can't perform encrypted flash write, consult Flash Encryption documentation for more information") + + # verify file sizes fit in flash + if args.flash_size != 'keep': # TODO: check this even with 'keep' + flash_end = flash_size_bytes(args.flash_size) + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + if address + argfile.tell() > flash_end: + raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " + "Use --flash-size argument, or change flashing address.") + % (argfile.name, argfile.tell(), address, flash_end)) + argfile.seek(0) + + if args.erase_all: + erase_flash(esp, args) + else: + for address, argfile in args.addr_filename: + argfile.seek(0, os.SEEK_END) + write_end = address + argfile.tell() + argfile.seek(0) + bytes_over = address % esp.FLASH_SECTOR_SIZE + if bytes_over != 0: + print("WARNING: Flash address {:#010x} is not aligned to a {:#x} byte flash sector. " + "{:#x} bytes before this address will be erased." + .format(address, esp.FLASH_SECTOR_SIZE, bytes_over)) + # Print the address range of to-be-erased flash memory region + print("Flash will be erased from {:#010x} to {:#010x}..." + .format(address - bytes_over, div_roundup(write_end, esp.FLASH_SECTOR_SIZE) * esp.FLASH_SECTOR_SIZE - 1)) + + """ Create a list describing all the files we have to flash. Each entry holds an "encrypt" flag + marking whether the file needs encryption or not. This list needs to be sorted. + + First, append to each entry of our addr_filename list the flag args.encrypt + For example, if addr_filename is [(0x1000, "partition.bin"), (0x8000, "bootloader")], + all_files will be [(0x1000, "partition.bin", args.encrypt), (0x8000, "bootloader", args.encrypt)], + where, of course, args.encrypt is either True or False + """ + all_files = [(offs, filename, args.encrypt) for (offs, filename) in args.addr_filename] + + """Now do the same with encrypt_files list, if defined. + In this case, the flag is True + """ + if args.encrypt_files is not None: + encrypted_files_flag = [(offs, filename, True) for (offs, filename) in args.encrypt_files] + + # Concatenate both lists and sort them. + # As both list are already sorted, we could simply do a merge instead, + # but for the sake of simplicity and because the lists are very small, + # let's use sorted. + all_files = sorted(all_files + encrypted_files_flag, key=lambda x: x[0]) + + for address, argfile, encrypted in all_files: + compress = args.compress + + # Check whether we can compress the current file before flashing + if compress and encrypted: + print('\nWARNING: - compress and encrypt options are mutually exclusive ') + print('Will flash %s uncompressed' % argfile.name) + compress = False + + if args.no_stub: + print('Erasing flash...') + image = pad_to(argfile.read(), esp.FLASH_ENCRYPTED_WRITE_ALIGN if encrypted else 4) + if len(image) == 0: + print('WARNING: File %s is empty' % argfile.name) + continue + image = _update_image_flash_params(esp, address, args, image) + calcmd5 = hashlib.md5(image).hexdigest() + uncsize = len(image) + if compress: + uncimage = image + image = zlib.compress(uncimage, 9) + # Decompress the compressed binary a block at a time, to dynamically calculate the + # timeout based on the real write size + decompress = zlib.decompressobj() + blocks = esp.flash_defl_begin(uncsize, len(image), address) + else: + blocks = esp.flash_begin(uncsize, address, begin_rom_encrypted=encrypted) + argfile.seek(0) # in case we need it again + seq = 0 + bytes_sent = 0 # bytes sent on wire + bytes_written = 0 # bytes written to flash + t = time.time() + + timeout = DEFAULT_TIMEOUT + + while len(image) > 0: + print_overwrite('Writing at 0x%08x... (%d %%)' % (address + bytes_written, 100 * (seq + 1) // blocks)) + sys.stdout.flush() + block = image[0:esp.FLASH_WRITE_SIZE] + if compress: + # feeding each compressed block into the decompressor lets us see block-by-block how much will be written + block_uncompressed = len(decompress.decompress(block)) + bytes_written += block_uncompressed + block_timeout = max(DEFAULT_TIMEOUT, timeout_per_mb(ERASE_WRITE_TIMEOUT_PER_MB, block_uncompressed)) + if not esp.IS_STUB: + timeout = block_timeout # ROM code writes block to flash before ACKing + esp.flash_defl_block(block, seq, timeout=timeout) + if esp.IS_STUB: + timeout = block_timeout # Stub ACKs when block is received, then writes to flash while receiving the block after it + else: + # Pad the last block + block = block + b'\xff' * (esp.FLASH_WRITE_SIZE - len(block)) + if encrypted: + esp.flash_encrypt_block(block, seq) + else: + esp.flash_block(block, seq) + bytes_written += len(block) + bytes_sent += len(block) + image = image[esp.FLASH_WRITE_SIZE:] + seq += 1 + + if esp.IS_STUB: + # Stub only writes each block to flash after 'ack'ing the receive, so do a final dummy operation which will + # not be 'ack'ed until the last block has actually been written out to flash + esp.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR, timeout=timeout) + + t = time.time() - t + speed_msg = "" + if compress: + if t > 0.0: + speed_msg = " (effective %.1f kbit/s)" % (uncsize / t * 8 / 1000) + print_overwrite('Wrote %d bytes (%d compressed) at 0x%08x in %.1f seconds%s...' % (uncsize, + bytes_sent, + address, t, speed_msg), last_line=True) + else: + if t > 0.0: + speed_msg = " (%.1f kbit/s)" % (bytes_written / t * 8 / 1000) + print_overwrite('Wrote %d bytes at 0x%08x in %.1f seconds%s...' % (bytes_written, address, t, speed_msg), last_line=True) + + if not encrypted and not esp.secure_download_mode: + try: + res = esp.flash_md5sum(address, uncsize) + if res != calcmd5: + print('File md5: %s' % calcmd5) + print('Flash md5: %s' % res) + print('MD5 of 0xFF is %s' % (hashlib.md5(b'\xFF' * uncsize).hexdigest())) + raise FatalError("MD5 of file does not match data in flash!") + else: + print('Hash of data verified.') + except NotImplementedInROMError: + pass + + print('\nLeaving...') + + if esp.IS_STUB: + # skip sending flash_finish to ROM loader here, + # as it causes the loader to exit and run user code + esp.flash_begin(0, 0) + + # Get the "encrypted" flag for the last file flashed + # Note: all_files list contains triplets like: + # (address: Integer, filename: String, encrypted: Boolean) + last_file_encrypted = all_files[-1][2] + + # Check whether the last file flashed was compressed or not + if args.compress and not last_file_encrypted: + esp.flash_defl_finish(False) + else: + esp.flash_finish(False) + + if args.verify: + print('Verifying just-written flash...') + print('(This option is deprecated, flash contents are now always read back after flashing.)') + # If some encrypted files have been flashed print a warning saying that we won't check them + if args.encrypt or args.encrypt_files is not None: + print('WARNING: - cannot verify encrypted files, they will be ignored') + # Call verify_flash function only if there at least one non-encrypted file flashed + if not args.encrypt: + verify_flash(esp, args) + + +def image_info(args): + image = LoadFirmwareImage(args.chip, args.filename) + print('Image version: %d' % image.version) + print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') + print('%d segments' % len(image.segments)) + print() + idx = 0 + for seg in image.segments: + idx += 1 + segs = seg.get_memory_type(image) + seg_name = ",".join(segs) + print('Segment %d: %r [%s]' % (idx, seg, seg_name)) + calc_checksum = image.calculate_checksum() + print('Checksum: %02x (%s)' % (image.checksum, + 'valid' if image.checksum == calc_checksum else 'invalid - calculated %02x' % calc_checksum)) + try: + digest_msg = 'Not appended' + if image.append_digest: + is_valid = image.stored_digest == image.calc_digest + digest_msg = "%s (%s)" % (hexify(image.calc_digest).lower(), + "valid" if is_valid else "invalid") + print('Validation Hash: %s' % digest_msg) + except AttributeError: + pass # ESP8266 image has no append_digest field + + +def make_image(args): + image = ESP8266ROMFirmwareImage() + if len(args.segfile) == 0: + raise FatalError('No segments specified') + if len(args.segfile) != len(args.segaddr): + raise FatalError('Number of specified files does not match number of specified addresses') + for (seg, addr) in zip(args.segfile, args.segaddr): + with open(seg, 'rb') as f: + data = f.read() + image.segments.append(ImageSegment(addr, data)) + image.entrypoint = args.entrypoint + image.save(args.output) + + +def elf2image(args): + e = ELFFile(args.input) + if args.chip == 'auto': # Default to ESP8266 for backwards compatibility + print("Creating image for ESP8266...") + args.chip = 'esp8266' + + if args.chip == 'esp32': + image = ESP32FirmwareImage() + if args.secure_pad: + image.secure_pad = '1' + elif args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s2': + image = ESP32S2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s3beta2': + image = ESP32S3BETA2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32s3beta3': + image = ESP32S3BETA3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c3': + image = ESP32C3FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c6beta': + image = ESP32C6BETAFirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.version == '1': # ESP8266 + image = ESP8266ROMFirmwareImage() + else: + image = ESP8266V2FirmwareImage() + image.entrypoint = e.entrypoint + image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] + + if args.chip != 'esp8266': + image.min_rev = int(args.min_rev) + + # ELFSection is a subclass of ImageSegment, so can use interchangeably + image.segments = e.segments if args.use_segments else e.sections + + image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] + image.flash_size_freq += {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] + + if args.elf_sha256_offset: + image.elf_sha256 = e.sha256() + image.elf_sha256_offset = args.elf_sha256_offset + + before = len(image.segments) + image.merge_adjacent_segments() + if len(image.segments) != before: + delta = before - len(image.segments) + print("Merged %d ELF section%s" % (delta, "s" if delta > 1 else "")) + + image.verify() + + if args.output is None: + args.output = image.default_output_name(args.input) + image.save(args.output) + + +def read_mac(esp, args): + mac = esp.read_mac() + + def print_mac(label, mac): + print('%s: %s' % (label, ':'.join(map(lambda x: '%02x' % x, mac)))) + print_mac("MAC", mac) + + +def chip_id(esp, args): + try: + chipid = esp.chip_id() + print('Chip ID: 0x%08x' % chipid) + except NotSupportedError: + print('Warning: %s has no Chip ID. Reading MAC instead.' % esp.CHIP_NAME) + read_mac(esp, args) + + +def erase_flash(esp, args): + print('Erasing flash (this may take a while)...') + t = time.time() + esp.erase_flash() + print('Chip erase completed successfully in %.1fs' % (time.time() - t)) + + +def erase_region(esp, args): + print('Erasing region (may be slow depending on size)...') + t = time.time() + esp.erase_region(args.address, args.size) + print('Erase completed successfully in %.1f seconds.' % (time.time() - t)) + + +def run(esp, args): + esp.run() + + +def flash_id(esp, args): + flash_id = esp.flash_id() + print('Manufacturer: %02x' % (flash_id & 0xff)) + flid_lowbyte = (flash_id >> 16) & 0xFF + print('Device: %02x%02x' % ((flash_id >> 8) & 0xff, flid_lowbyte)) + print('Detected flash size: %s' % (DETECTED_FLASH_SIZES.get(flid_lowbyte, "Unknown"))) + + +def read_flash(esp, args): + if args.no_progress: + flash_progress = None + else: + def flash_progress(progress, length): + msg = '%d (%d %%)' % (progress, progress * 100.0 / length) + padding = '\b' * len(msg) + if progress == length: + padding = '\n' + sys.stdout.write(msg + padding) + sys.stdout.flush() + t = time.time() + data = esp.read_flash(args.address, args.size, flash_progress) + t = time.time() - t + print_overwrite('Read %d bytes at 0x%x in %.1f seconds (%.1f kbit/s)...' + % (len(data), args.address, t, len(data) / t * 8 / 1000), last_line=True) + with open(args.filename, 'wb') as f: + f.write(data) + + +def verify_flash(esp, args): + differences = False + + for address, argfile in args.addr_filename: + image = pad_to(argfile.read(), 4) + argfile.seek(0) # rewind in case we need it again + + image = _update_image_flash_params(esp, address, args, image) + + image_size = len(image) + print('Verifying 0x%x (%d) bytes @ 0x%08x in flash against %s...' % (image_size, image_size, address, argfile.name)) + # Try digest first, only read if there are differences. + digest = esp.flash_md5sum(address, image_size) + expected_digest = hashlib.md5(image).hexdigest() + if digest == expected_digest: + print('-- verify OK (digest matched)') + continue + else: + differences = True + if getattr(args, 'diff', 'no') != 'yes': + print('-- verify FAILED (digest mismatch)') + continue + + flash = esp.read_flash(address, image_size) + assert flash != image + diff = [i for i in range(image_size) if flash[i] != image[i]] + print('-- verify FAILED: %d differences, first @ 0x%08x' % (len(diff), address + diff[0])) + for d in diff: + flash_byte = flash[d] + image_byte = image[d] + if PYTHON2: + flash_byte = ord(flash_byte) + image_byte = ord(image_byte) + print(' %08x %02x %02x' % (address + d, flash_byte, image_byte)) + if differences: + raise FatalError("Verify failed.") + + +def read_flash_status(esp, args): + print('Status value: 0x%04x' % esp.read_status(args.bytes)) + + +def write_flash_status(esp, args): + fmt = "0x%%0%dx" % (args.bytes * 2) + args.value = args.value & ((1 << (args.bytes * 8)) - 1) + print(('Initial flash status: ' + fmt) % esp.read_status(args.bytes)) + print(('Setting flash status: ' + fmt) % args.value) + esp.write_status(args.value, args.bytes, args.non_volatile) + print(('After flash status: ' + fmt) % esp.read_status(args.bytes)) + + +def get_security_info(esp, args): + (flags, flash_crypt_cnt, key_purposes) = esp.get_security_info() + # TODO: better display + print('Flags: 0x%08x (%s)' % (flags, bin(flags))) + print('Flash_Crypt_Cnt: 0x%x' % flash_crypt_cnt) + print('Key_Purposes: %s' % (key_purposes,)) + + +def merge_bin(args): + chip_class = _chip_to_rom_loader(args.chip) + + # sort the files by offset. The AddrFilenamePairAction has already checked for overlap + input_files = sorted(args.addr_filename, key=lambda x: x[0]) + if not input_files: + raise FatalError("No input files specified") + first_addr = input_files[0][0] + if first_addr < args.target_offset: + raise FatalError("Output file target offset is 0x%x. Input file offset 0x%x is before this." % (args.target_offset, first_addr)) + + if args.format != 'raw': + raise FatalError("This version of esptool only supports the 'raw' output format") + + with open(args.output, 'wb') as of: + def pad_to(flash_offs): + # account for output file offset if there is any + of.write(b'\xFF' * (flash_offs - args.target_offset - of.tell())) + for addr, argfile in input_files: + pad_to(addr) + image = argfile.read() + image = _update_image_flash_params(chip_class, addr, args, image) + of.write(image) + if args.fill_flash_size: + pad_to(flash_size_bytes(args.fill_flash_size)) + print("Wrote 0x%x bytes to file %s, ready to flash to offset 0x%x" % (of.tell(), args.output, args.target_offset)) + + +def version(args): + print(__version__) + +# +# End of operations functions +# + + +def main(argv=None, esp=None): + """ + Main function for esptool + + argv - Optional override for default arguments parsing (that uses sys.argv), can be a list of custom arguments + as strings. Arguments and their values need to be added as individual items to the list e.g. "-b 115200" thus + becomes ['-b', '115200']. + + esp - Optional override of the connected device previously returned by get_default_connected_device() + """ + + external_esp = esp is not None + + parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') + + parser.add_argument('--chip', '-c', + help='Target chip type', + type=lambda c: c.lower().replace('-', ''), # support ESP32-S2, etc. + choices=['auto', 'esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3beta3', 'esp32c3', 'esp32c6beta'], + default=os.environ.get('ESPTOOL_CHIP', 'auto')) + + parser.add_argument( + '--port', '-p', + help='Serial port device', + default=os.environ.get('ESPTOOL_PORT', None)) + + parser.add_argument( + '--baud', '-b', + help='Serial port baud rate used when flashing/reading', + type=arg_auto_int, + default=os.environ.get('ESPTOOL_BAUD', ESPLoader.ESP_ROM_BAUD)) + + parser.add_argument( + '--before', + help='What to do before connecting to the chip', + choices=['default_reset', 'usb_reset', 'no_reset', 'no_reset_no_sync'], + default=os.environ.get('ESPTOOL_BEFORE', 'default_reset')) + + parser.add_argument( + '--after', '-a', + help='What to do after esptool.py is finished', + choices=['hard_reset', 'soft_reset', 'no_reset', 'no_reset_stub'], + default=os.environ.get('ESPTOOL_AFTER', 'hard_reset')) + + parser.add_argument( + '--no-stub', + help="Disable launching the flasher stub, only talk to ROM bootloader. Some features will not be available.", + action='store_true') + + parser.add_argument( + '--trace', '-t', + help="Enable trace-level output of esptool.py interactions.", + action='store_true') + + parser.add_argument( + '--override-vddsdio', + help="Override ESP32 VDDSDIO internal voltage regulator (use with care)", + choices=ESP32ROM.OVERRIDE_VDDSDIO_CHOICES, + nargs='?') + + parser.add_argument( + '--connect-attempts', + help=('Number of attempts to connect, negative or 0 for infinite. ' + 'Default: %d.' % DEFAULT_CONNECT_ATTEMPTS), + type=int, + default=os.environ.get('ESPTOOL_CONNECT_ATTEMPTS', DEFAULT_CONNECT_ATTEMPTS)) + + subparsers = parser.add_subparsers( + dest='operation', + help='Run esptool {command} -h for additional help') + + def add_spi_connection_arg(parent): + parent.add_argument('--spi-connection', '-sc', help='ESP32-only argument. Override default SPI Flash connection. ' + 'Value can be SPI, HSPI or a comma-separated list of 5 I/O numbers to use for SPI flash (CLK,Q,D,HD,CS).', + action=SpiConnectionAction) + + parser_load_ram = subparsers.add_parser( + 'load_ram', + help='Download an image to RAM and execute') + parser_load_ram.add_argument('filename', help='Firmware image') + + parser_dump_mem = subparsers.add_parser( + 'dump_mem', + help='Dump arbitrary memory to disk') + parser_dump_mem.add_argument('address', help='Base address', type=arg_auto_int) + parser_dump_mem.add_argument('size', help='Size of region to dump', type=arg_auto_int) + parser_dump_mem.add_argument('filename', help='Name of binary dump') + + parser_read_mem = subparsers.add_parser( + 'read_mem', + help='Read arbitrary memory location') + parser_read_mem.add_argument('address', help='Address to read', type=arg_auto_int) + + parser_write_mem = subparsers.add_parser( + 'write_mem', + help='Read-modify-write to arbitrary memory location') + parser_write_mem.add_argument('address', help='Address to write', type=arg_auto_int) + parser_write_mem.add_argument('value', help='Value', type=arg_auto_int) + parser_write_mem.add_argument('mask', help='Mask of bits to write', type=arg_auto_int, nargs='?', default='0xFFFFFFFF') + + def add_spi_flash_subparsers(parent, allow_keep, auto_detect): + """ Add common parser arguments for SPI flash properties """ + extra_keep_args = ['keep'] if allow_keep else [] + + if auto_detect and allow_keep: + extra_fs_message = ", detect, or keep" + elif auto_detect: + extra_fs_message = ", or detect" + elif allow_keep: + extra_fs_message = ", or keep" + else: + extra_fs_message = "" + + parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', + choices=extra_keep_args + ['40m', '26m', '20m', '80m'], + default=os.environ.get('ESPTOOL_FF', 'keep' if allow_keep else '40m')) + parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', + choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], + default=os.environ.get('ESPTOOL_FM', 'keep' if allow_keep else 'qio')) + parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' + ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message, + action=FlashSizeAction, auto_detect=auto_detect, + default=os.environ.get('ESPTOOL_FS', 'keep' if allow_keep else '1MB')) + add_spi_connection_arg(parent) + + parser_write_flash = subparsers.add_parser( + 'write_flash', + help='Write a binary blob to flash') + + parser_write_flash.add_argument('addr_filename', metavar='
', help='Address followed by binary filename, separated by space', + action=AddrFilenamePairAction) + parser_write_flash.add_argument('--erase-all', '-e', + help='Erase all regions of flash (not just write areas) before programming', + action="store_true") + + add_spi_flash_subparsers(parser_write_flash, allow_keep=True, auto_detect=True) + parser_write_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") + parser_write_flash.add_argument('--verify', help='Verify just-written data on flash ' + '(mostly superfluous, data is read back during flashing)', action='store_true') + parser_write_flash.add_argument('--encrypt', help='Apply flash encryption when writing data (required correct efuse settings)', + action='store_true') + # In order to not break backward compatibility, our list of encrypted files to flash is a new parameter + parser_write_flash.add_argument('--encrypt-files', metavar='
', + help='Files to be encrypted on the flash. Address followed by binary filename, separated by space.', + action=AddrFilenamePairAction) + parser_write_flash.add_argument('--ignore-flash-encryption-efuse-setting', help='Ignore flash encryption efuse settings ', + action='store_true') + + compress_args = parser_write_flash.add_mutually_exclusive_group(required=False) + compress_args.add_argument('--compress', '-z', help='Compress data in transfer (default unless --no-stub is specified)', + action="store_true", default=None) + compress_args.add_argument('--no-compress', '-u', help='Disable data compression during transfer (default if --no-stub is specified)', + action="store_true") + + subparsers.add_parser( + 'run', + help='Run application code in flash') + + parser_image_info = subparsers.add_parser( + 'image_info', + help='Dump headers from an application image') + parser_image_info.add_argument('filename', help='Image file to parse') + + parser_make_image = subparsers.add_parser( + 'make_image', + help='Create an application image from binary files') + parser_make_image.add_argument('output', help='Output image file') + parser_make_image.add_argument('--segfile', '-f', action='append', help='Segment input file') + parser_make_image.add_argument('--segaddr', '-a', action='append', help='Segment base address', type=arg_auto_int) + parser_make_image.add_argument('--entrypoint', '-e', help='Address of entry point', type=arg_auto_int, default=0) + + parser_elf2image = subparsers.add_parser( + 'elf2image', + help='Create an application image from ELF file') + parser_elf2image.add_argument('input', help='Input ELF file') + parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2'], default='1') + parser_elf2image.add_argument('--min-rev', '-r', help='Minimum chip revision', choices=['0', '1', '2', '3'], default='0') + parser_elf2image.add_argument('--secure-pad', action='store_true', + help='Pad image so once signed it will end on a 64KB boundary. For Secure Boot v1 images only.') + parser_elf2image.add_argument('--secure-pad-v2', action='store_true', + help='Pad image to 64KB, so once signed its signature sector will start at the next 64K block. ' + 'For Secure Boot v2 images only.') + parser_elf2image.add_argument('--elf-sha256-offset', help='If set, insert SHA256 hash (32 bytes) of the input ELF file at specified offset in the binary.', + type=arg_auto_int, default=None) + parser_elf2image.add_argument('--use_segments', help='If set, ELF segments will be used instead of ELF sections to genereate the image.', + action='store_true') + + add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False) + + subparsers.add_parser( + 'read_mac', + help='Read MAC address from OTP ROM') + + subparsers.add_parser( + 'chip_id', + help='Read Chip ID from OTP ROM') + + parser_flash_id = subparsers.add_parser( + 'flash_id', + help='Read SPI flash manufacturer and device ID') + add_spi_connection_arg(parser_flash_id) + + parser_read_status = subparsers.add_parser( + 'read_flash_status', + help='Read SPI flash status register') + + add_spi_connection_arg(parser_read_status) + parser_read_status.add_argument('--bytes', help='Number of bytes to read (1-3)', type=int, choices=[1, 2, 3], default=2) + + parser_write_status = subparsers.add_parser( + 'write_flash_status', + help='Write SPI flash status register') + + add_spi_connection_arg(parser_write_status) + parser_write_status.add_argument('--non-volatile', help='Write non-volatile bits (use with caution)', action='store_true') + parser_write_status.add_argument('--bytes', help='Number of status bytes to write (1-3)', type=int, choices=[1, 2, 3], default=2) + parser_write_status.add_argument('value', help='New value', type=arg_auto_int) + + parser_read_flash = subparsers.add_parser( + 'read_flash', + help='Read SPI flash content') + add_spi_connection_arg(parser_read_flash) + parser_read_flash.add_argument('address', help='Start address', type=arg_auto_int) + parser_read_flash.add_argument('size', help='Size of region to dump', type=arg_auto_int) + parser_read_flash.add_argument('filename', help='Name of binary dump') + parser_read_flash.add_argument('--no-progress', '-p', help='Suppress progress output', action="store_true") + + parser_verify_flash = subparsers.add_parser( + 'verify_flash', + help='Verify a binary blob against flash') + parser_verify_flash.add_argument('addr_filename', help='Address and binary file to verify there, separated by space', + action=AddrFilenamePairAction) + parser_verify_flash.add_argument('--diff', '-d', help='Show differences', + choices=['no', 'yes'], default='no') + add_spi_flash_subparsers(parser_verify_flash, allow_keep=True, auto_detect=True) + + parser_erase_flash = subparsers.add_parser( + 'erase_flash', + help='Perform Chip Erase on SPI flash') + add_spi_connection_arg(parser_erase_flash) + + parser_erase_region = subparsers.add_parser( + 'erase_region', + help='Erase a region of the flash') + add_spi_connection_arg(parser_erase_region) + parser_erase_region.add_argument('address', help='Start address (must be multiple of 4096)', type=arg_auto_int) + parser_erase_region.add_argument('size', help='Size of region to erase (must be multiple of 4096)', type=arg_auto_int) + + parser_merge_bin = subparsers.add_parser( + 'merge_bin', + help='Merge multiple raw binary files into a single file for later flashing') + + parser_merge_bin.add_argument('--output', '-o', help='Output filename', type=str, required=True) + parser_merge_bin.add_argument('--format', '-f', help='Format of the output file', choices='raw', default='raw') # for future expansion + add_spi_flash_subparsers(parser_merge_bin, allow_keep=True, auto_detect=False) + + parser_merge_bin.add_argument('--target-offset', '-t', help='Target offset where the output file will be flashed', + type=arg_auto_int, default=0) + parser_merge_bin.add_argument('--fill-flash-size', help='If set, the final binary file will be padded with FF ' + 'bytes up to this flash size.', action=FlashSizeAction) + parser_merge_bin.add_argument('addr_filename', metavar='
', + help='Address followed by binary filename, separated by space', + action=AddrFilenamePairAction) + + subparsers.add_parser( + 'version', help='Print esptool version') + + subparsers.add_parser('get_security_info', help='Get some security-related data') + + # internal sanity check - every operation matches a module function of the same name + for operation in subparsers.choices.keys(): + assert operation in globals(), "%s should be a module function" % operation + + argv = expand_file_arguments(argv or sys.argv[1:]) + + args = parser.parse_args(argv) + print('esptool.py v%s' % __version__) + + # operation function can take 1 arg (args), 2 args (esp, arg) + # or be a member function of the ESPLoader class. + + if args.operation is None: + parser.print_help() + sys.exit(1) + + # Forbid the usage of both --encrypt, which means encrypt all the given files, + # and --encrypt-files, which represents the list of files to encrypt. + # The reason is that allowing both at the same time increases the chances of + # having contradictory lists (e.g. one file not available in one of list). + if args.operation == "write_flash" and args.encrypt and args.encrypt_files is not None: + raise FatalError("Options --encrypt and --encrypt-files must not be specified at the same time.") + + operation_func = globals()[args.operation] + + if PYTHON2: + # This function is depreciated in Python3 + operation_args = inspect.getargspec(operation_func).args + else: + operation_args = inspect.getfullargspec(operation_func).args + + if operation_args[0] == 'esp': # operation function takes an ESPLoader connection object + if args.before != "no_reset_no_sync": + initial_baud = min(ESPLoader.ESP_ROM_BAUD, args.baud) # don't sync faster than the default baud rate + else: + initial_baud = args.baud + + if args.port is None: + ser_list = get_port_list() + print("Found %d serial ports" % len(ser_list)) + else: + ser_list = [args.port] + esp = esp or get_default_connected_device(ser_list, port=args.port, connect_attempts=args.connect_attempts, + initial_baud=initial_baud, chip=args.chip, trace=args.trace, + before=args.before) + if esp is None: + raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list)) + + if esp.secure_download_mode: + print("Chip is %s in Secure Download Mode" % esp.CHIP_NAME) + else: + print("Chip is %s" % (esp.get_chip_description())) + print("Features: %s" % ", ".join(esp.get_chip_features())) + print("Crystal is %dMHz" % esp.get_crystal_freq()) + read_mac(esp, args) + + if not args.no_stub: + if esp.secure_download_mode: + print("WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub") + args.no_stub = True + else: + esp = esp.run_stub() + + if args.override_vddsdio: + esp.override_vddsdio(args.override_vddsdio) + + if args.baud > initial_baud: + try: + esp.change_baud(args.baud) + except NotImplementedInROMError: + print("WARNING: ROM doesn't support changing baud rate. Keeping initial baud rate %d" % initial_baud) + + # override common SPI flash parameter stuff if configured to do so + if hasattr(args, "spi_connection") and args.spi_connection is not None: + if esp.CHIP_NAME != "ESP32": + raise FatalError("Chip %s does not support --spi-connection option." % esp.CHIP_NAME) + print("Configuring SPI flash mode...") + esp.flash_spi_attach(args.spi_connection) + elif args.no_stub: + print("Enabling default SPI flash mode...") + # ROM loader doesn't enable flash unless we explicitly do it + esp.flash_spi_attach(0) + + if hasattr(args, "flash_size"): + print("Configuring flash size...") + detect_flash_size(esp, args) + if args.flash_size != 'keep': # TODO: should set this even with 'keep' + esp.flash_set_parameters(flash_size_bytes(args.flash_size)) + + try: + operation_func(esp, args) + finally: + try: # Clean up AddrFilenamePairAction files + for address, argfile in args.addr_filename: + argfile.close() + except AttributeError: + pass + + # Handle post-operation behaviour (reset or other) + if operation_func == load_ram: + # the ESP is now running the loaded image, so let it run + print('Exiting immediately.') + elif args.after == 'hard_reset': + esp.hard_reset() + elif args.after == 'soft_reset': + print('Soft resetting...') + # flash_finish will trigger a soft reset + esp.soft_reset(False) + elif args.after == 'no_reset_stub': + print('Staying in flasher stub.') + else: # args.after == 'no_reset' + print('Staying in bootloader.') + if esp.IS_STUB: + esp.soft_reset(True) # exit stub back to ROM loader + + if not external_esp: + esp._port.close() + + else: + operation_func(args) + + +def get_port_list(): + if list_ports is None: + raise FatalError("Listing all serial ports is currently not available. Please try to specify the port when " + "running esptool.py or update the pyserial package to the latest version") + return sorted(ports.device for ports in list_ports.comports()) + + +def expand_file_arguments(argv): + """ Any argument starting with "@" gets replaced with all values read from a text file. + Text file arguments can be split by newline or by space. + Values are added "as-is", as if they were specified in this order on the command line. + """ + new_args = [] + expanded = False + for arg in argv: + if arg.startswith("@"): + expanded = True + with open(arg[1:], "r") as f: + for line in f.readlines(): + new_args += shlex.split(line) + else: + new_args.append(arg) + if expanded: + print("esptool.py %s" % (" ".join(new_args[1:]))) + return new_args + return argv + + +class FlashSizeAction(argparse.Action): + """ Custom flash size parser class to support backwards compatibility with megabit size arguments. + + (At next major relase, remove deprecated sizes and this can become a 'normal' choices= argument again.) + """ + def __init__(self, option_strings, dest, nargs=1, auto_detect=False, **kwargs): + super(FlashSizeAction, self).__init__(option_strings, dest, nargs, **kwargs) + self._auto_detect = auto_detect + + def __call__(self, parser, namespace, values, option_string=None): + try: + value = { + '2m': '256KB', + '4m': '512KB', + '8m': '1MB', + '16m': '2MB', + '32m': '4MB', + '16m-c1': '2MB-c1', + '32m-c1': '4MB-c1', + }[values[0]] + print("WARNING: Flash size arguments in megabits like '%s' are deprecated." % (values[0])) + print("Please use the equivalent size '%s'." % (value)) + print("Megabit arguments may be removed in a future release.") + except KeyError: + value = values[0] + + known_sizes = dict(ESP8266ROM.FLASH_SIZES) + known_sizes.update(ESP32ROM.FLASH_SIZES) + if self._auto_detect: + known_sizes['detect'] = 'detect' + known_sizes['keep'] = 'keep' + if value not in known_sizes: + raise argparse.ArgumentError(self, '%s is not a known flash size. Known sizes: %s' % (value, ", ".join(known_sizes.keys()))) + setattr(namespace, self.dest, value) + + +class SpiConnectionAction(argparse.Action): + """ Custom action to parse 'spi connection' override. Values are SPI, HSPI, or a sequence of 5 pin numbers separated by commas. + """ + def __call__(self, parser, namespace, value, option_string=None): + if value.upper() == "SPI": + value = 0 + elif value.upper() == "HSPI": + value = 1 + elif "," in value: + values = value.split(",") + if len(values) != 5: + raise argparse.ArgumentError(self, '%s is not a valid list of comma-separate pin numbers. Must be 5 numbers - CLK,Q,D,HD,CS.' % value) + try: + values = tuple(int(v, 0) for v in values) + except ValueError: + raise argparse.ArgumentError(self, '%s is not a valid argument. All pins must be numeric values' % values) + if any([v for v in values if v > 33 or v < 0]): + raise argparse.ArgumentError(self, 'Pin numbers must be in the range 0-33.') + # encode the pin numbers as a 32-bit integer with packed 6-bit values, the same way ESP32 ROM takes them + # TODO: make this less ESP32 ROM specific somehow... + clk, q, d, hd, cs = values + value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk + else: + raise argparse.ArgumentError(self, '%s is not a valid spi-connection value. ' + 'Values are SPI, HSPI, or a sequence of 5 pin numbers CLK,Q,D,HD,CS).' % value) + setattr(namespace, self.dest, value) + + +class AddrFilenamePairAction(argparse.Action): + """ Custom parser class for the address/filename pairs passed as arguments """ + def __init__(self, option_strings, dest, nargs='+', **kwargs): + super(AddrFilenamePairAction, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + # validate pair arguments + pairs = [] + for i in range(0, len(values), 2): + try: + address = int(values[i], 0) + except ValueError: + raise argparse.ArgumentError(self, 'Address "%s" must be a number' % values[i]) + try: + argfile = open(values[i + 1], 'rb') + except IOError as e: + raise argparse.ArgumentError(self, e) + except IndexError: + raise argparse.ArgumentError(self, 'Must be pairs of an address and the binary filename to write there') + pairs.append((address, argfile)) + + # Sort the addresses and check for overlapping + end = 0 + for address, argfile in sorted(pairs, key=lambda x: x[0]): + argfile.seek(0, 2) # seek to end + size = argfile.tell() + argfile.seek(0) + sector_start = address & ~(ESPLoader.FLASH_SECTOR_SIZE - 1) + sector_end = ((address + size + ESPLoader.FLASH_SECTOR_SIZE - 1) & ~(ESPLoader.FLASH_SECTOR_SIZE - 1)) - 1 + if sector_start < end: + message = 'Detected overlap at address: 0x%x for file: %s' % (address, argfile.name) + raise argparse.ArgumentError(self, message) + end = sector_end + setattr(namespace, self.dest, pairs) + + +# Binary stub code (see flasher_stub dir for source & details) +ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +eNq9PGtD3Da2f8U2BBgCjWTP2HIezTDAJGmTbUI3NO2lLfIr3dymJRO2YbvJ/e3X5yXLngGSbbcfBvyQpaPzPkdH+vfmeX1xvnk7KDZPLqw5udDq5EKpaftHn1w0Dfzm5/Co++Xtz+Db++0DI03bG6PoJy3N2L+f\ +TuXq4R5/kI9dV/A3pyF1fHJRwr0KAhg7tu2fpH02ObmoGxivbWABtrrtIl3A22ft3Rg+h65TuNDypO1ETQCQL563fagAIPgJvpm1Q00QMkVtdbEPQMIlt5s9h7+3U/cg2se/8mU7SF3QIPBd1sIT3w/bhwICXbRA\ +1Ti523EfhGfS4mQTpkJzN2kf4fLjD0ftnw7Cb6GbOWCp1+jbXiP4JGmhqRDW2y34qmScUoP4iIcBagj+W15YABXolcU7xwTGZwLhiPr+Vw/3HhEn2ZLf2rG72WpBUNDx9HE7cYtXDAv2eyo815+4XoEMQP2qZ6bs\ +8yCNMGjYDTJ4ybMg5LmbdvxNr8fxKnAZ5N7MlfE6KXpvepJiRgPpavwOEr4BLLgb6Mn1lpcsD8196KEScTTuMRFMFwNUmG4mTsiftn9q70bHcnPXA7NQ3vjF2Lup4MbizR3vg6anNUofMuiq9lSIKvqkdy+xpcxu\ +JQOoPoQkpAyA7jNW7tMDb6xMpxR6TgmJ7sZ2Ny+QitODOf7beYz/Lh46nvqCr4rxI74qy8/4qjI5XrWtK+ka5lbjlKaPN2Rs/jYkmAD6HFQfySZ+ojXIcGTXIpRQml1sWy1VxrZVcFVsQfnEFvRabBlvNWva0uEL\ +hohZZxUJY6okOVCpj20AKf4yyqC1IaVcAJo1QwACqMr97bZHULuWDQS2g850+J4H17/zgBbV0vaOU0ftBWAoDun7Dhh8qv2nR9R9tTQjaIWqNSKMIRDUPVApCdaDvn5E+APuLhl2x8/jj33+RgSxVfF5zUaALrRc\ +FAyZ4unFDGW1gjo5MoAmTK05yA19ptO38g0jsOI3A0Wfd091HO0Bj0Wo1wGOch2fTGbP9mMbbSC3tXpBl0kGzQM2TcYXNPo6ifEfGC80jlpFQQP2VW/turG3oEVkoz5H6TKKnu0TgxCzCCONCU7QedpGhy04Bb1e\ +PWIctGjKUKtkxM064a4QkxOysK7/uoa5tg8nDMyEOEbpVqar0tcWHoqAix+QHCIi2w9ylOaTzfE+UBSeluAwsBZr0kXQcGuTvpGHM0Ak8Ca1P4bn7bgl+DoKUCe9mwyfHHU9W+4ZiFGir9CSysT/Is/HuLG6lgB2\ +7oMNLw1Or+nPxSb8Rdrxny7X3afz9zxXtIiApH63n8tr0uIl2IPhuDwEgF5kguKGSNaO8KWMCqa2G/gFY6jAz2dEw0o/AM78CUcEXahL7xPjt1TY8itGoXM55HXcg/IWOkX8Kslcj+0FQDxpLyyCvjNjEavRg0sE\ +k6+w1U12FkUzA7sZn8eVOpQnJAE5Xxt37aSBwQHx0yhEBdgU+swA2+hj5tvKwYS6RNVzlh1xJAG1CczzR54MygCyUdLxWe544ZSkQ+l/LKu/xlKHeRnRZ6bnvQU7e/BhM/zwGKixBkaJjGCDPsIONSnLWwDdP3hY\ +FaCDfkxQg9df65vUISokhBM9cATyhxVjVaC9atW9cWM12efki3pdEczfLPXTCiHOZBnmY4lI5t8B0IxJw2ze91UaZmJgp6ryzBvQK+40lkkvVn92wHhJPYdcPlR6Q9pBizPkwF2wnSAdGY1Wxx1PuhFj4ODC0f8X\ +NC/fg/42k99QLd4VelwynEmR9DEOF9KELh8ucxKP7ubWPTZLJVAKkBfT6OXkLYwD7ltG1Cdf4BF0VSU3wZ1ZA1s+ZvZNv/PsZpN439QzL1ASQFr61BnFojTzF9zAHG3twgPoAcW4FG2BBg3Gy8HCgIxp8wQ+L9i0\ +ND7dj9iNAqVWHDxtw9q/QxfZDXBKyYxNjvAf22IQIuBErSXY1E/3g2kc0OusYxe0fQqsbbABfcqcdEBw5yjl+z4bhewsOZzEV+DEcQPqdTAtromhJtqx2l3qo45nDn/w+Hanzk3cQU6kBpbresic7YFoZNLsd0YD\ +ve7JHGeggvFqgUF4nA6ntqA4wKlpKVNXrA/j98QXSCK0BFufx+IjqiVX7uFnreei6wLd92AdnO84BL5HqtbOvzkiv31fPT3AuDPqO5gpBWCgKwHnZU1efJPdJWyi7sQghSY+J/2B2gFMFsLV+MJNKROM86HzXtrA\ +snZzZH7IbhfgfeLYGkS1nt4AyVE/wN+nEYG1xArxl63vUjDn1BVj2RA/utZ5Zj7HqHHcJw1AT3JCEUI16cBR6rtt9A2/fI65jS8PxEF9SkawlZUJjanTFoOVfeQ5XjB6+pJxY5KOZeoJU4g4reOYdDl4XKWh2J1G\ +VvyR4ya8LSc/wO3hr2hzH2MA5juIDbnL+mQTZHgizhz6HUCOsbiJACVSc7wRd20akU6gae6FwuVa1vko2BtgX/yCKsZRD9lzSpwszEUJzqW/kLQdEtLcg093veiDuaYffcTysIs64DEmN4I1YpkalBHIeKXuBYck\ +c0qDWsf+S+pfWKj0Qgh0O2t2fYp02IC5ECfx4vuHT8y9iAEuJ68Y7ymb95o8twVFdC5EQG0aJIFjfKe3Dx/xvFNPMbqhj6TZkV2/PNSJiGvaXu02Mxz6JudoCN9Bp28o+oDeizhCCWWcGuG9Ft5N6jmPw214YdEk\ +8/xbfGzmHktbkCpqEUPcYiY5IzAO0zevofOjcL0It8++ZK/KHrx6Tn6jGR/ZGzjCDkcYzvqBHRu3IBfIpPPH9A7EEATQAim1OoK/k63XAKrdQkA2juzo7tegyj6APO2SDoAUQeuPbnouD8RdkI08AQjJO96iwREL\ +aIr0zFddog4ocQFGCf6bZAT2f8Tc00LArm+Nd1ue84uaRi1CCVp0cgYO8/g9WPHTfejzNaYXiu8BP4twveMoxV6wieFpS5kbIbsUEApAVq9CmTRi4S9yoimaLdbJNuax19QrwN97ctsRlvEpyEoPgDml0VTSQnLU\ +DrgFA35D4VJVHLEPoAi9Jv6WBNSoWxIEgch/F82sPoLsK/EHGJ8qAz75V6erdDIL9VE4Jq45Zp1BQUcQPAdYjykaoSReMBZeN8GE85G+jRVfVsf2s06WdDNlYbL6m7TLlqoG8+2KNAkSG2QEOTRRTQBRxbwTKLVC\ +c1ctWULWu/XOPiOQ7QPGPPygAAdF10yXWhgCKMHcIJCfrgk90F9J6BPKJXJnMOrci4Eq9tpa7iB5KirKtiPS4W2xG3V+FqnzqSg/Tbgr0fOtA4a1HgMIzRsnRGB+1kfABUG41/4vM+LQOtkjmpQY/B2TOdNkvtZP\ +Nu9uzcWXRAbmSUMC8ap5fw3rGNhjXnwAnYcx4biTCPpdjRGLGLGCEf2fYoRnQjlSvOFU55RxgDwUcRRmAg5yFQVrYtrBRcqZy0zqrzx8PDvw5zWHUjBvRkGf+rX62LnKHA17h2AAwUeqIYYqOaB39AVeAHVC9rCC\ +XKBzuRETZm/V5JjyIvWO+KQCxzJFIG35lq3heGmKQE5IFTcElKNr1Y5ZgehWsawSNTb6IhLABBGaVUSTftW5QJRrC8nANNV7XsGLSRlpyCvgRRU06hTWXMatu5c34vvBm04XIKA7OANeZyF0Bqw+mo+j9N2E2he9\ +POAnSX/+n0u/0P81AYzMrc7F2Doq93gCgXALgXf3Gbds1x1qwv91BpmXU1uagv0tfoXLQ6HXc5IWZKTsmBFhWBWWXlox8dKd8VUICm1vZOtbyOsJslOyAsr7yziXyl8FFhpSYjB6BcutpXoKWlO95MSDxo5fEjvT\ +XfOEIAIP9umNoLWehZ2IAUXfSsTRCVYRTpz9fAXkfvX89FdMCQGf53Na8sSoEtGwwc4ChiHjlRPB5bEk/NXTvRgMZAPdi+tUG87PDzqNEr0Ejb/DeRiDg8/gWSzacEJzW4Dv1E0PV0x601uECc2N5gnefMYpyVzS\ +IcrM9iFpUoCkFgmkyWjZRZ0Jk0B6FR18avOO2hAr5eMFAP0C6PVc0pcXc19gfm+/yi2GF6hRdgkTusC0LShCHb6CT/WrLv9pemtXM6eKIg6jmmyOadPwJX3TaqxOSsHW6Gru+SrKc+XYgNorDSj7vOnfB2o0RNeX\ +BizCGy9XeQv34j6tWV/AmBhq1YGXtiH1kaF1DGG0au/vbgSvnXjR2GHSKaDpV0xHIA665FrSxI4Rkp6hfYgJxV00QOsK1qNVVLLz6VT8fU/FYyYwfIB8yDaPVqY8B1Xpn4bcR7yPfO6EbN5f12Xf3NHEXEOTWjQ7\ +asaFE7octQf69JlY73f4avOTbDd6bhgigFHU8QreantX69sHoRqY9haXPUsOfh2hMyQYW/MaYWx/n5YodZlAyhHMObJNxgZDiVFEzn/sZQkoj6kPfM2GOkDp/ZWijxkpmECJ8z3lKh+II+I9cqhbuLdwhkJTDhEw\ +1xNbVLV3hp0j0cCNgZQEMQVWC13elQKqDzuBFEjZUGcQOWwjK0bEisYSK7bR6W5kP3vCbNRqMqfVWA8njBnVvOg4WFNwGkfC2IRsHYefTW9hViqm8h1avB3BuxHr0cpbZ4XoVO1TJznW1hx8QWkeXOFpvtjbHnFK\ +gscZUfCcQ4hvOeVXT6ScqIQCHsOy3KTbN0N2vsoZafBKTR9wDArDmReQTDCstYtyex3pchPBO8dc6MzmEWX79j/G19lJOmPs6cWbOOAvQl7wDtGHMBRwaYjzrbrgJeuCE/q5Zx1NtxLO+IismYXmLMDsuL3FctBM\ +o+Dszd7xj12aAEYzWXbn7IIxrd6hQXwHt2dnehaqBX6PuZM3nGdiT0YbLgSCIhCrAV/jM4I85+IVWOfSekHZBrf05NTCLLwFX0ezF92yWPv5JqkTXABOA5KqPCAhwuo3S8JkWT4L1QqVzQkyi31MBY4FMwyo5fJl\ +DAOVHygjRUmtOhjtkUOJMJYc/IxdDglJw6OrcSfKZQt+/p7oQc8Ugzbmyh1DL6xaCcw7Aka9dfk1zGuFGTtmpAiyWchLF4VpI1gaGpBQmgDpG7wDUN6jAgjzs9/V74tI4N9+B44MaJsSmuUY0HwD3c2A74CbcvS4\ +D2b25iL8jFQ5ukycLS14qa2Fa3djy1vmzHll2XhmEjP0boUKGnZR1A2QsBHntdNIrNwNzpUK7mtsPt77ALfba1NwYRTHMhSbxZg+DDSnBnF4NQIVkMa41AZOSOvYrXfr/Tru+38wGMKZHFASBYjSlO/JWfcs9ym8\ +huipRfOW2t5Af8rBlIuuKt6SLkFcpDdL9Pdekt8LJmYpqfURnpCWEpT0t3561XlEnhdURxJA6uLe9fZWaIXmD3Hh5ccr2y2fUH6O8/RscF3OjnwhLK1yw1exrBatgGItcImJWFIN4T97dLdfEd1PEcexT/ekR3dL\ +dM819GnG85PzYU3iz6yogN5jFlNdSOwwRvpCCZnGEttxcLKpUMfb6LRP4CeEpByY1hGi79k5vy+CUJOWM042IcM/CW8DHAvMRra0/4GgkpKkxUxW78Y9G7/VUJYU8tUQ6Wfk9bto1k/qeTEQLWKAUiVuTjGZl0J5\ +b62/JtR2aXchU2Q32HPnjutPZVUorUonH8Gnq6P7RbhxXYD/jrNYLW429JSQmFMMXnareGyRGvo+RxetaNzyBZALIj8MF/2Y0PSoud2EbEuMI+z2LXGuGlqNUaUsXMHP7qOfVd4euxAEg+JhLPyI2g8D4VckcuRb\ +LVUyl5zRxJLx1l2B5d8WjfAvflVJKB5z7QOGERmXbWV9i9tjla/nVDEoiYE2GoT+Q2BzVGHNCn93MpxA6xSKn03FEFoThPFtrJtUww/8wFhTSqMptohEbSw1YmJpCn2b4oJ81SrlhQMql9IFWBSnyKTyyqIrVu+x\ +o1I8HcvzNTQKT2RlTBSeZ61M+hnzPTdG27THqbYCM8mgo3o2qVhhk2g1gG3SkWeTYOD1rlRMJyuKcPvmiU1S7nkk/03z9DHJXN3LdFYflVT6k8wTBsXF0Dwt1Rua4u6lJuomd/sxtmmPE+NXkh79baiiUkx9quQK\ +RlpKncghLH14kchok4qOsLpY61GXLAs7ndhVxi5yk6DCmHJ1M+frMbmLxIUbqMZz6rif8A+cterW7Bt2p4lJp53v1I/hpViIIuke1cN/IKq8HGlDiVEOr3XxfE4bP/ruIrBUTgnwzo+Qtee14DJ2YUJ21JLQtaMW\ +hhZq2YMkkimNNKpSr5imJGroYv0yN0Hl6jYs22XMGMYm04R9FSz8QXchZuc04TRA5dmbS2jxDlD7nkuhsOIiA8jsbAkzBwj+enC5IPlemMfOZ8vYIYae+djB7qeBnj4S7MQednBJVELbdEkLjVothDsPilWe1Gte\ +Fme/vakkc+7hWhAz9RCjOPuLCXZEZjzvc2aLAOhK34SleinrbgA0M25oaaRXO4L4wvTaNqSJMF+DqAFG1a8irrrekOLoonVRXhUcH3DZEFYATMAx0ZRmX3xaXk2XUmr2P50i9ZSol1urqHRy9cpI66ZtXL8+YinT\ +WWBbP6WZ+t/4yczTQTIz7TtJ3bJ1vWRI2TPAmA80Qi/oKyno20tWq1ZgXtVcEfIp37zOeubViHkth1Hf6sgPFjqKvyTs+xSmQCDTm5da1wFj/FnWVf211tVy3ULHAqd9Frgi+jP96K9vXQ2WORd/deiH3nMh0qO3\ +E4NZJruhuf6ZVoI3ttdYuWyhGvmFl/wzLLSYf0qVhebFEqx/y7q83PUOWXWZLsk/SpfkGUdjrHp8dWK7zwpaIu00yjZFGr/0EMjljojDwm694Y1WluLgs3+S9mgB25FUrsXAjNUK2XZ1O1rY0T1SBrK3Cl2hPd5S\ +BK6BRczhRhdcJXZKIiSt0NidryFiLtPXuBwMxRxQsNzYDycLDKVLmG7683WpXin+s84dImSNWpQswp3TbWQYWuRs7BkhZ82jA1DH4vYd0vB2xNZPFb8eMz9VfjEoSyJ9XzBPsH2V51jnqJw5j+9E+CC+A1Y15WJB\ +LUrLtVPUC6yZqonAqMnevnruV61INvY9JaiNi1oW3p4tSSrWkvUDQUDTkZFicjuEMgkdsYol8JYVP06b7rDXxVtXOvG4uiJhoF7z/271zXuYjau8aflsPijXgvU0v2ahZaN1qDS0nPHnugeM3jFrDpDQxRPc62fH\ +3nq127slK5cQjLcxfBuMB1RlqF3KAZhh57K6n08gQp70y0j+7MVDKfzAoOm8jy1X9qFSWWP5uLTWDlYTgJzZvJgPgeecFlYWxl1lYQv/3imveqAl3ZIVOil6HdEDKuUxc1nk9fsOv0NNtJYXXXWfcfk+aYXpvQ2S\ +Vz35EjYylAt/P84l7GOYfcyQfcBwQvbVis8LfEQXkCe6EGaS+gdapOTE1kS23MmiIuXm2D4An8GCQY2pjHDEZIl5p3L8E1zg6iIwt1uzLsIJF3YDIbEVRA94MeH67YysAC7VTLgOW+QY9wfWmAi21EllsKb6kCYm\ +O6yxBHr8RFbQ+Df5wOgvLxAQXEW/MawO891w0auU1MKq27grEYdngARYnwQ5Q5hqT+Ul9E5+GNpb2vXq2oyveDe54l16xbus/w5gq/neFNFtmMWDHFA7XYO8GrBywSjP1WkvEIt9Cwafdp1t56RPVfwA1mUb/TdA\ +Ae6kmLWuwgqmog1UCsomAFe0e2JXdrX8RuuH2i1KT9/xBrCW//Yg422Jf2TTBeQusl0ua8VaIimRT5cPT8CVbNCoyvDolnIaQNUq7pisKo9FLTMz1bwcV3MNGi4PJkvxqpfb5XBBydIjGnesA7dH4fbWWgE11BXt\ +pYKL53wBDSve1t7Y0ZpBJ7y4dWS33/A+MhBd+3VwKyjs+rcniwCy2JOziWwYBLBnlMsxWGiMm9NoZwsXfsomJ97rSyQDqCfbbtP5ArG+TdBg9WvOWsXABjXop4hF3m5p2UkAc8CgkAmB66YFx1SNtwu0zEbfe7ui\ +lDoG7VlD32g24/kzrn+QTVZdDl0e5tAFFuq3DsduVwqg0ses9FAoeTcAam+7vGVd18vPTfqAHxoBCPMBhwKTt00D8+eD70vl2VYIaBsr+2uopOP9nLee2Ms76e3XSZYDuBI0XwMANmongLAJ4qGSkVBy3Xdj51CA\ +XqCYJbJfxEwSb2dvNxpvlzMYgowwV//hp9e012Rrt2NuxdmVJu/DXPv1HjFJSxlLUuQGB3Ee9pvJcidukLQLs8ihB8nNH35vdh/xxhvaJ5R7G8EsHzGATn4uZRSGt4e5TXsMF+6dUJD9QzOT3+tG1yW+cg27BD/A\ +01tflaRgKQBUJCMRSX/JNYtI7Djc759eUcYhnk8R4vkUIZ5PEd4nxa21f/LM8KiSrv4UbjoddOqfFHMa8npW70Ah0nreURDMz/HJOTzLeXMk6svGDI5UKNnRQIWD5bgl6YhKs92Bak+/phNPoWjGvX4gkGwD6gWe\ +bVR6G0xoP7ckorg8lj4OOQvd8Ak9HfDMAqUJ6cgHrT0JXXmOj7MLckCEw1vsYzReQi+M7SHV3wqm1PrxPgNKpzjtCUv2T56QTbzd40e94ynwZI7j8yWEuVIqKhRR0dKUzNIRMNPudB5nE8f+nD2uQMqX+gzBP9db\ +j4UPSOWjg2EOuzlpydwYaMqr5PB/AWEyri+WWzsq6mKQFnS2TgkFyxVwkMYjXGCznHAb7nBERFZyWEhdLyOyj//jt9y0ypZ5LQjWj2fowWPaIaPyKZwcb8vSaVekIRqsUmugvszjAKmTPzY722ujHeTKc1lNxIoa\ +3G8PvgS6dFCzWPUBjirRY3nveVz0jqzyZsK8rzzKIdv7XL6Kux/LqTvyEvUwiGapD5g1cc+igVg23zs5fwOEftYpcMyoJKyjOa2oWR/D/HD0kkpLII+NlCz3Vu1NOuSlgQZYxEqCZry1gyxSccEPb2wVpspjjrU0\ +Zuyae5xF67GBO1sBVIgpY3EMxqOTE/z04V022A2sgZRQrKTLR5DsNG8AjQC8ecbhZNOd+RWsVBSNXwT9bER49KTnXPaSjWeYXlGP9x7dcAoA2k5GE1wtH38Rba8FO3ujQzFQUS1HS/zPCoNotBiuSbBG1vQqpabu\ +DMiPwLldyFz2reVIjarwDHoz3JGKhF3S5/65IIVsx5Uz8NLL+uFS3M4S9OZ4naZeUlSsms61rI3dBkPa88ujzhGTkFmOGBEdBroiF80FTgXqYbyIrxYz+L3wje1Z/4w2qNbF4kCkRYkHukzH3jMMOHM5+cojwZLc\ +gpCizDpJ7U69EqmNutihlOVJcbGGh5XoLgw3Lo0iIQsupzR8olNF20EWIIF6txPDRq0Uw6acSWMPHiVRxACOSs4f4mvKGR/z1s0qpN3rDR40wl1hNbKRPrMVR0TVuttToRI5JWYimNjtvMu+okIuLYkQch4Je2Gf\ +pDA6Pu2OXTrnUxUK+x+w/s8+W/3k35z7Nxf+zfs+K5rB8YH58N4/3s2Ud1bYD+RPpiFZj8r3whvmVGDO8zfMsT6TtmRwvg9q9x1e87AJOj97HU9VPr3tgXfMijqqRJpfUy3dal60kqSQXXpYedfAYZPpS7+c9RFt\ +o1l4e+6XDoRLxAFB/tne62Cefz44a4JO89N8Lk7TDYrfsE+KnsRGd1YHQQNZ0Diwwnv32Hm1DZxhidvRawy2+Wg2a1ac9FYZPukTh3tBY0FcXMiy23j2mn25yjsyYymPsUcEwFNrxNezB3yaJsqSFiDr3e6oJtyL\ +2RwKRRBonkildhny3K6glvhENR+j1M77hay7JAIALeKjuwZzAs7Ewsfm6Y7wGK3RdWfqCDrnMhHZq6VmT34O+RiGZqBWilXHNNGBKFY+iU4WKJfgpKHlg+yA24/OCx94Wk/6Nylg0w6TAM042u+Whmo+kYPw6g52\ +zWSrxMbfRuG/WTrUvaAH8P+t0CkTLrZrwt+WtOGhlAQfigbd6ZKalhOr/XyInHgEGwaaBo89sH2JggyU+eaut/AsXm7qRW3e+uCnq1fIGMXu6CF06h/w2gdgr/QSDyrtiOG2pSgo0Kpj6kz2C2YbER/TsrQgImei\ +FcDhExwKN4tEIxasXkbGOUt4AtoEowOEBlvjWVB91S7d407fWGZS9D7rXDT3KbYeGokiXcOqLDgqpknxNIEENr8Uk9HTA+8Iu3G3fU4Qk+EGTMUMpIvI7TlRGhz64uCY0yl1lkouLfIBMFT2gk+y1EuhxKvcOPod\ +PDrujnvkVu0ktgD+2IcfcieXTYGSMJR5bIGdMZzNMVfYNX4DGaUsv74OkCG4OCYkldvuzwPeDSZrhEvfxD52u9ebOwGeMv3j23O7gLOmtcrGZpykY9O+qX85X/zLeziJ24eVPbd8KHXvDF2UxYnnk0uJsnB8wj+O\ +LEFjgZuLhwDXjXcDBeLo4uIZrdbd/MSnSGGbml1tNT13V5zszZda48E3cqQxqivrf1AMPuiuXlBZbu9ZxlVS4izXdJLxHXe1qiOa3lIDUus1uhXTWharppzNTfCGFx01uulg7i8ZYtVVQSzSe/aNN3rOHjZhqfHw\ +blM3GJ1V2eGvXIljcKV5LP2R0H36VeTg+NGDqKg9WJfOHh76EcNy6GEMOdi/6viXfv0NIIuBTA7G9o+IQretl3zseTY9j2147HXvpHG94rBtPWivB+/jwX0yuB8P7tPBvRncl4P1iuH6Ra994N/0WvonfOvTq8+M\ +/lN/+pr7+BN56Dqeuo7HhvfpNffZNffmyvvzK+5+ueKud/r3yvvyyvvFVbJz7e9T5Tb9JBydf8K8h5A312iBAeR6AMnw6Hfd62/Nv7np3/S6vePf9A6V7HkWPYK8HWiaAZx2cF8O7utkhZTov1CK/9ta4I9qiT+q\ +Rf6olvmjWui6+0/8adWlLZ0EZih5lO6TUGPsFpj5ND/J7zlJW2XjLp3pJnutvpObZLEaG/Ph/wFCW8dM\ +"""))) +ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S3BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32S3BETA3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C6BETAROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +TB3fMw==\ +"""))) + + +def _main(): + try: + main() + except FatalError as e: + print('\nA fatal error occurred: %s' % e) + sys.exit(2) + + +if __name__ == '__main__': + _main() diff --git a/Precompiled/rnode_firmware_latest_tbeam.zip b/Precompiled/rnode_firmware_latest_tbeam.zip index 860b487a8581544324b95753278a674ef5a015d0..d51bbd685e90086ffac82c0c042209b3275b61fa 100644 GIT binary patch delta 81222 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 K+!b``OcY_&yv4%rzksB3?qc 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 V92W@^1QO)szgzzr!T-DZ{{Y^?ewzRQ delta 191 zcmX@`o^Qo(PM!d7W)=|!1_lm>Al`{QHf(