From 1a3829389e3c547fb1b207afb70a9bb549e55ff0 Mon Sep 17 00:00:00 2001 From: "radim.karnis" Date: Thu, 9 Nov 2023 16:04:38 +0100 Subject: [PATCH] feat(spi_connection): Support --spi-connection on all chips Closes https://github.com/espressif/esptool/issues/916 --- .gitlab-ci.yml | 4 ++ docs/en/advanced-topics/serial-protocol.rst | 10 ++-- docs/en/esptool/advanced-options.rst | 52 ++++++++++------- esptool/__init__.py | 65 +++++++++++---------- esptool/targets/esp32.py | 7 ++- esptool/targets/esp32c2.py | 5 ++ esptool/targets/esp32c3.py | 9 +++ esptool/targets/esp32c6.py | 9 +++ esptool/targets/esp32h2.py | 10 ++++ esptool/targets/esp32p4.py | 3 + esptool/targets/esp32s2.py | 9 +++ esptool/targets/esp32s3.py | 11 ++++ esptool/targets/esp8266.py | 9 +-- esptool/util.py | 2 +- test/test_esptool.py | 58 ++++++++++++++++-- 15 files changed, 197 insertions(+), 66 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 22f1339be..52f347cbb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -282,6 +282,8 @@ target_esp32c3: extends: .target_esptool_test tags: - esptool_esp32c3_target + variables: + ESPTOOL_TEST_SPI_CONN: "6,2,7,4,10" script: - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_esptool.py --port /dev/serial_ports/ESP32C3 --chip esp32c3 --baud 115200 @@ -329,6 +331,8 @@ target_esp32s3_jtag_serial: extends: .target_esptool_test tags: - esptool_esp32s3_jtag_serial_target + variables: + ESPTOOL_TEST_SPI_CONN: "12,13,11,9,10" script: - coverage run --parallel-mode -m pytest ${CI_PROJECT_DIR}/test/test_esptool.py --port /dev/serial_ports/ESP32S3_JTAG_SERIAL --preload-port /dev/serial_ports/ESP32S3_PRELOAD --chip esp32s3 --baud 115200 diff --git a/docs/en/advanced-topics/serial-protocol.rst b/docs/en/advanced-topics/serial-protocol.rst index 83a7e3a6b..c2faa307b 100644 --- a/docs/en/advanced-topics/serial-protocol.rst +++ b/docs/en/advanced-topics/serial-protocol.rst @@ -384,7 +384,7 @@ The SPI _ATTACH command enables the SPI flash interface. It takes a 32-bit data .. only:: not esp8266 - On the {IDF_TARGET_NAME} stub loader, it is required to send this command before interacting with SPI flash. + On the {IDF_TARGET_NAME} stub loader sending this command before interacting with SPI flash is optional. On {IDF_TARGET_NAME} ROM loader, it is required to send this command before interacting with SPI flash. +------------------+----------------------------------------------------------------------------------------------------------------------------------+ | Value | Meaning | @@ -400,9 +400,11 @@ The SPI _ATTACH command enables the SPI flash interface. It takes a 32-bit data When writing the values of each pin as 6-bit numbers packed into the data word, each 6-bit value uses the following representation: - * Pin numbers 0 through 30 are represented as themselves. - * Pin numbers 32 & 33 are represented as values 30 & 31. - * It is not possible to represent pins 30 & 31 or pins higher than 33. This is the same 6-bit representation used by the ``SPI_PAD_CONFIG_xxx`` efuses. + .. only:: esp32 + + * Pin numbers 0 through 30 are represented as themselves. + * Pin numbers 32 & 33 are represented as values 30 & 31. + * It is not possible to represent pins 30 & 31 or pins higher than 33. This is the same 6-bit representation used by the ``SPI_PAD_CONFIG_xxx`` efuses. On {IDF_TARGET_NAME} ROM loader only, there is an additional 4 bytes in the data payload of this command. These bytes should all be set to zero. diff --git a/docs/en/esptool/advanced-options.rst b/docs/en/esptool/advanced-options.rst index d47ab515a..83ec8c73b 100644 --- a/docs/en/esptool/advanced-options.rst +++ b/docs/en/esptool/advanced-options.rst @@ -45,7 +45,7 @@ The ``--no-stub`` option disables uploading of a software "stub loader" that man Passing ``--no-stub`` will disable certain options, as not all options are implemented in every chip's ROM loader. -.. only:: esp32 +.. only:: not esp8266 Overriding SPI Flash Connections -------------------------------- @@ -61,42 +61,50 @@ Passing ``--no-stub`` will disable certain options, as not all options are imple The only exception to this is if the ``--no-stub`` option is also provided. In this case, efuse values are ignored and ``--spi-connection`` will default to ``--spi-connection SPI`` unless set to a different value. - SPI Mode - ^^^^^^^^ + .. only:: esp32 - ``--spi-connection SPI`` uses the default SPI pins: + SPI Mode + ^^^^^^^^ - * CLK = GPIO 6 - * Q = GPIO 7 - * D = GPIO 8 - * HD = GPIO 9 - * CS = GPIO 11 + ``--spi-connection SPI`` uses the default SPI pins: - During normal booting, this configuration is selected if all SPI pin efuses are unset and GPIO1 (U0TXD) is not pulled low (default). + * CLK = GPIO 6 + * Q = GPIO 7 + * D = GPIO 8 + * HD = GPIO 9 + * CS = GPIO 11 - This is the normal pin configuration for ESP32 chips that do not contain embedded flash. + During normal booting, this configuration is selected if all SPI pin efuses are unset and GPIO1 (U0TXD) is not pulled low (default). - HSPI Mode - ^^^^^^^^^ + This is the normal pin configuration for ESP32 chips that do not contain embedded flash. - ``--spi-connection HSPI`` uses the HSPI peripheral instead of the SPI peripheral for SPI flash communications, via the following HSPI pins: + HSPI Mode + ^^^^^^^^^ - * CLK = GPIO 14 - * Q = GPIO 12 - * D = GPIO 13 - * HD = GPIO 4 - * CS = GPIO 15 + ``--spi-connection HSPI`` uses the HSPI peripheral instead of the SPI peripheral for SPI flash communications, via the following HSPI pins: - During normal booting, this configuration is selected if all SPI pin efuses are unset and GPIO1 (U0TXD) is pulled low on reset. + * CLK = GPIO 14 + * Q = GPIO 12 + * D = GPIO 13 + * HD = GPIO 4 + * CS = GPIO 15 + + During normal booting, this configuration is selected if all SPI pin efuses are unset and GPIO1 (U0TXD) is pulled low on reset. Custom SPI Pin Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``--spi-connection ,,,,`` allows a custom list of pins to be configured for the SPI flash connection. This can be used to emulate the flash configuration equivalent to a particular set of SPI pin efuses being burned. The values supplied are GPIO numbers. - For example, ``--spi-connection 6,17,8,11,16`` sets an identical configuration to the factory efuse configuration for ESP32s with embedded flash. + .. only:: esp32 + + For example, ``--spi-connection 6,17,8,11,16`` sets an identical configuration to the factory efuse configuration for ESP32s with embedded flash. + + When setting a custom pin configuration, the SPI peripheral (not HSPI) will be used unless the ``CLK`` pin value is set to 14 (HSPI CLK), in which case the HSPI peripheral will be used. + + .. note:: - When setting a custom pin configuration, the SPI peripheral (not HSPI) will be used unless the ``CLK`` pin value is set to 14 (HSPI CLK), in which case the HSPI peripheral will be used. + Some GPIO pins might be shared with other peripherals. Therefore, some SPI pad pin configurations might not work reliably or at all. Use a different combination of pins if you encounter issues. Specifying Arguments via File ----------------------------- diff --git a/esptool/__init__.py b/esptool/__init__.py index 206b6fadd..1fdb56f41 100644 --- a/esptool/__init__.py +++ b/esptool/__init__.py @@ -177,9 +177,9 @@ def add_spi_connection_arg(parent): parent.add_argument( "--spi-connection", "-sc", - help="ESP32-only argument. Override default SPI Flash connection. " + help="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).", + "to use for SPI flash (CLK,Q,D,HD,CS). Not supported with ESP8266.", action=SpiConnectionAction, ) @@ -770,14 +770,22 @@ def add_spi_flash_subparsers(parent, allow_keep, auto_detect): "Keeping initial baud rate %d" % initial_baud ) - # override common SPI flash parameter stuff if configured to do so + # Override the 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) + spi_config = args.spi_connection + if args.spi_connection == "SPI": + value = 0 + elif args.spi_connection == "HSPI": + value = 1 + else: + esp.check_spi_connection(args.spi_connection) + # Encode the pin numbers as a 32-bit integer with packed 6-bit values, + # the same way the ESP ROM takes them + clk, q, d, hd, cs = args.spi_connection + spi_config = f"CLK:{clk}, Q:{q}, D:{d}, HD:{hd}, CS:{cs}" + value = (hd << 24) | (cs << 18) | (d << 12) | (q << 6) | clk + print(f"Configuring SPI flash mode ({spi_config})...") + esp.flash_spi_attach(value) elif args.no_stub: print("Enabling default SPI flash mode...") # ROM loader doesn't enable flash unless we explicitly do it @@ -846,6 +854,15 @@ def flash_xmc_startup(): "Try checking the chip connections or removing " "any other hardware connected to IOs." ) + if ( + hasattr(args, "spi_connection") + and args.spi_connection is not None + ): + print( + "Some GPIO pins might be used by other peripherals, " + "try using another --spi-connection combination." + ) + except FatalError as e: raise FatalError(f"Unable to verify flash chip connection ({e}).") @@ -1023,43 +1040,31 @@ class SpiConnectionAction(argparse.Action): """ def __call__(self, parser, namespace, value, option_string=None): - if value.upper() == "SPI": - value = 0 - elif value.upper() == "HSPI": - value = 1 + if value.upper() in ["SPI", "HSPI"]: + values = value.upper() 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, + f"{value} is not a valid list of comma-separate pin numbers. " + "Must be 5 numbers - CLK,Q,D,HD,CS.", ) 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." + f"{values} is not a valid argument. " + "All pins must be numeric values", ) - # 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, + f"{value} is not a valid spi-connection value. " + "Values are SPI, HSPI, or a sequence of 5 pin numbers - CLK,Q,D,HD,CS.", ) - setattr(namespace, self.dest, value) + setattr(namespace, self.dest, values) class AutoHex2BinAction(argparse.Action): diff --git a/esptool/targets/esp32.py b/esptool/targets/esp32.py index e9c66da52..fd8e3f518 100644 --- a/esptool/targets/esp32.py +++ b/esptool/targets/esp32.py @@ -281,7 +281,7 @@ def read_efuse(self, n): return self.read_reg(self.EFUSE_RD_REG_BASE + (4 * n)) def chip_id(self): - raise NotSupportedError(self, "chip_id") + raise NotSupportedError(self, "Function chip_id") def read_mac(self, mac_type="BASE_MAC"): """Read MAC from EFUSE region""" @@ -374,6 +374,11 @@ def change_baud(self, baud): time.sleep(0.05) # get rid of garbage sent during baud rate change self.flush_input() + def check_spi_connection(self, spi_connection): + # Pins 30, 31 do not exist + if not set(spi_connection).issubset(set(range(0, 30)) | set((32, 33))): + raise FatalError("SPI Pin numbers must be in the range 0-29, 32, or 33.") + class ESP32StubLoader(ESP32ROM): """Access class for ESP32 stub loader, runs on top of ROM.""" diff --git a/esptool/targets/esp32c2.py b/esptool/targets/esp32c2.py index ff8341869..d353a71a5 100644 --- a/esptool/targets/esp32c2.py +++ b/esptool/targets/esp32c2.py @@ -8,6 +8,7 @@ from .esp32c3 import ESP32C3ROM from ..loader import ESPLoader +from ..util import FatalError class ESP32C2ROM(ESP32C3ROM): @@ -144,6 +145,10 @@ def is_flash_encryption_key_valid(self): return True return False + def check_spi_connection(self, spi_connection): + if not set(spi_connection).issubset(set(range(0, 21))): + raise FatalError("SPI Pin numbers must be in the range 0-20.") + class ESP32C2StubLoader(ESP32C2ROM): """Access class for ESP32C2 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32c3.py b/esptool/targets/esp32c3.py index 47191b0c2..e00e51786 100644 --- a/esptool/targets/esp32c3.py +++ b/esptool/targets/esp32c3.py @@ -228,6 +228,15 @@ def _post_connect(self): if not self.sync_stub_detected: # Don't run if stub is reused self.disable_watchdogs() + def check_spi_connection(self, spi_connection): + if not set(spi_connection).issubset(set(range(0, 22))): + raise FatalError("SPI Pin numbers must be in the range 0-21.") + if any([v for v in spi_connection if v in [18, 19]]): + print( + "WARNING: GPIO pins 18 and 19 are used by USB-Serial/JTAG, " + "consider using other pins for SPI flash connection." + ) + class ESP32C3StubLoader(ESP32C3ROM): """Access class for ESP32C3 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32c6.py b/esptool/targets/esp32c6.py index c18bf9d1f..0004afeb4 100644 --- a/esptool/targets/esp32c6.py +++ b/esptool/targets/esp32c6.py @@ -180,6 +180,15 @@ def is_flash_encryption_key_valid(self): return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + def check_spi_connection(self, spi_connection): + if not set(spi_connection).issubset(set(range(0, 31))): + raise FatalError("SPI Pin numbers must be in the range 0-30.") + if any([v for v in spi_connection if v in [12, 13]]): + print( + "WARNING: GPIO pins 12 and 13 are used by USB-Serial/JTAG, " + "consider using other pins for SPI flash connection." + ) + class ESP32C6StubLoader(ESP32C6ROM): """Access class for ESP32C6 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32h2.py b/esptool/targets/esp32h2.py index 66600c7da..b042cd7a1 100644 --- a/esptool/targets/esp32h2.py +++ b/esptool/targets/esp32h2.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later from .esp32c6 import ESP32C6ROM +from ..util import FatalError class ESP32H2ROM(ESP32C6ROM): @@ -58,6 +59,15 @@ def get_crystal_freq(self): # ESP32H2 XTAL is fixed to 32MHz return 32 + def check_spi_connection(self, spi_connection): + if not set(spi_connection).issubset(set(range(0, 28))): + raise FatalError("SPI Pin numbers must be in the range 0-27.") + if any([v for v in spi_connection if v in [26, 27]]): + print( + "WARNING: GPIO pins 26 and 27 are used by USB-Serial/JTAG, " + "consider using other pins for SPI flash connection." + ) + class ESP32H2StubLoader(ESP32H2ROM): """Access class for ESP32H2 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32p4.py b/esptool/targets/esp32p4.py index 0791ab65a..7d9f9bc97 100644 --- a/esptool/targets/esp32p4.py +++ b/esptool/targets/esp32p4.py @@ -169,6 +169,9 @@ def _post_connect(self): # if not self.sync_stub_detected: # Don't run if stub is reused # self.disable_watchdogs() + def check_spi_connection(self, spi_connection): + pass # TODO: Define GPIOs for --spi-connection + class ESP32P4StubLoader(ESP32P4ROM): """Access class for ESP32P4 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32s2.py b/esptool/targets/esp32s2.py index cd361f918..19f5532c6 100644 --- a/esptool/targets/esp32s2.py +++ b/esptool/targets/esp32s2.py @@ -287,6 +287,15 @@ def hard_reset(self): def change_baud(self, baud): ESPLoader.change_baud(self, baud) + def check_spi_connection(self, spi_connection): + if not set(spi_connection).issubset(set(range(0, 22)) | set(range(26, 47))): + raise FatalError("SPI Pin numbers must be in the range 0-21, or 26-46.") + if any([v for v in spi_connection if v in [19, 20]]): + print( + "WARNING: GPIO pins 19 and 20 are used by USB-OTG, " + "consider using other pins for SPI flash connection." + ) + class ESP32S2StubLoader(ESP32S2ROM): """Access class for ESP32-S2 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32s3.py b/esptool/targets/esp32s3.py index 3dc785216..f3d814455 100644 --- a/esptool/targets/esp32s3.py +++ b/esptool/targets/esp32s3.py @@ -349,6 +349,17 @@ def hard_reset(self): def change_baud(self, baud): ESPLoader.change_baud(self, baud) + def check_spi_connection(self, spi_connection): + if not set(spi_connection).issubset(set(range(0, 22)) | set(range(26, 49))): + raise FatalError("SPI Pin numbers must be in the range 0-21, or 26-48.") + if spi_connection[3] > 46: # hd_gpio_num must be <= SPI_GPIO_NUM_LIMIT (46) + raise FatalError("SPI HD Pin number must be <= 46.") + if any([v for v in spi_connection if v in [19, 20]]): + print( + "WARNING: GPIO pins 19 and 20 are used by USB-Serial/JTAG and USB-OTG, " + "consider using other pins for SPI flash connection." + ) + class ESP32S3StubLoader(ESP32S3ROM): """Access class for ESP32S3 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp8266.py b/esptool/targets/esp8266.py index e686abf6e..58e465158 100644 --- a/esptool/targets/esp8266.py +++ b/esptool/targets/esp8266.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later from ..loader import ESPLoader -from ..util import FatalError, NotImplementedInROMError +from ..util import FatalError, NotSupportedError class ESP8266ROM(ESPLoader): @@ -170,9 +170,10 @@ def get_erase_size(self, offset, size): return (num_sectors - head_sectors) * sector_size def override_vddsdio(self, new_voltage): - raise NotImplementedInROMError( - "Overriding VDDSDIO setting only applies to ESP32" - ) + raise NotSupportedError(self, "Overriding VDDSDIO") + + def check_spi_connection(self, spi_connection): + raise NotSupportedError(self, "Setting --spi-connection") class ESP8266StubLoader(ESP8266ROM): diff --git a/esptool/util.py b/esptool/util.py index ecd41c0d9..fb4a57193 100644 --- a/esptool/util.py +++ b/esptool/util.py @@ -167,7 +167,7 @@ class NotSupportedError(FatalError): def __init__(self, esp, function_name): FatalError.__init__( self, - "Function %s is not supported for %s." % (function_name, esp.CHIP_NAME), + f"{function_name} is not supported by {esp.CHIP_NAME}.", ) diff --git a/test/test_esptool.py b/test/test_esptool.py index c625ea983..419beeb21 100755 --- a/test/test_esptool.py +++ b/test/test_esptool.py @@ -230,13 +230,16 @@ def teardown_class(self): # Restore the stored working directory os.chdir(self.stored_dir) - def readback(self, offset, length): + def readback(self, offset, length, spi_connection=None): """Read contents of flash back, return to caller.""" dump_file = tempfile.NamedTemporaryFile(delete=False) # a file we can read into try: - self.run_esptool( + cmd = ( f"--before default_reset read_flash {offset} {length} {dump_file.name}" ) + if spi_connection: + cmd += f" --spi-connection {spi_connection}" + self.run_esptool(cmd) with open(dump_file.name, "rb") as f: rb = f.read() @@ -248,8 +251,10 @@ def readback(self, offset, length): dump_file.close() os.unlink(dump_file.name) - def verify_readback(self, offset, length, compare_to, is_bootloader=False): - rb = self.readback(offset, length) + def verify_readback( + self, offset, length, compare_to, is_bootloader=False, spi_connection=None + ): + rb = self.readback(offset, length, spi_connection) with open(compare_to, "rb") as f: ct = f.read() if len(rb) != len(ct): @@ -764,6 +769,51 @@ def test_flash_id_trace(self): assert "Device:" in res +@pytest.mark.skipif( + os.getenv("ESPTOOL_TEST_SPI_CONN") is None, reason="Needs external flash" +) +class TestExternalFlash(EsptoolTestCase): + conn = os.getenv("ESPTOOL_TEST_SPI_CONN") + + def test_short_flash_to_external_stub(self): + # First flash internal flash, then external + self.run_esptool("write_flash 0x0 images/one_kb.bin") + self.run_esptool( + f"write_flash --spi-connection {self.conn} 0x0 images/sector.bin" + ) + + self.verify_readback(0, 1024, "images/one_kb.bin") + self.verify_readback(0, 1024, "images/sector.bin", spi_connection=self.conn) + + # First flash external flash, then internal + self.run_esptool( + f"write_flash --spi-connection {self.conn} 0x0 images/one_kb.bin" + ) + self.run_esptool("write_flash 0x0 images/sector.bin") + + self.verify_readback(0, 1024, "images/sector.bin") + self.verify_readback(0, 1024, "images/one_kb.bin", spi_connection=self.conn) + + def test_short_flash_to_external_ROM(self): + # First flash internal flash, then external + self.run_esptool("--no-stub write_flash 0x0 images/one_kb.bin") + self.run_esptool( + f"--no-stub write_flash --spi-connection {self.conn} 0x0 images/sector.bin" + ) + + self.verify_readback(0, 1024, "images/one_kb.bin") + self.verify_readback(0, 1024, "images/sector.bin", spi_connection=self.conn) + + # First flash external flash, then internal + self.run_esptool( + f"--no-stub write_flash --spi-connection {self.conn} 0x0 images/one_kb.bin" + ) + self.run_esptool("--no-stub write_flash 0x0 images/sector.bin") + + self.verify_readback(0, 1024, "images/sector.bin") + self.verify_readback(0, 1024, "images/one_kb.bin", spi_connection=self.conn) + + @pytest.mark.skipif( os.name == "nt", reason="Temporarily disabled on windows" ) # TODO: ESPTOOL-673