feat(spi_connection): Support --spi-connection on all chips

Closes https://github.com/espressif/esptool/issues/916
This commit is contained in:
radim.karnis
2023-11-09 16:04:38 +01:00
parent a91eee192e
commit 1a3829389e
15 changed files with 197 additions and 66 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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 <CLK>,<Q>,<D>,<HD>,<CS>`` 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
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.
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::
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
-----------------------------

View File

@@ -177,9 +177,9 @@ def main(argv=None, esp=None):
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 main(argv=None, esp=None):
"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 main(argv=None, esp=None):
"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,
f"{values} is not a valid argument. "
"All pins must be numeric 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,
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):

View File

@@ -281,7 +281,7 @@ class ESP32ROM(ESPLoader):
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 @@ class ESP32ROM(ESPLoader):
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."""

View File

@@ -8,6 +8,7 @@ import time
from .esp32c3 import ESP32C3ROM
from ..loader import ESPLoader
from ..util import FatalError
class ESP32C2ROM(ESP32C3ROM):
@@ -144,6 +145,10 @@ class ESP32C2ROM(ESP32C3ROM):
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.

View File

@@ -228,6 +228,15 @@ class ESP32C3ROM(ESP32ROM):
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.

View File

@@ -180,6 +180,15 @@ class ESP32C6ROM(ESP32C3ROM):
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.

View File

@@ -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 @@ class ESP32H2ROM(ESP32C6ROM):
# 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.

View File

@@ -169,6 +169,9 @@ class ESP32P4ROM(ESP32ROM):
# 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.

View File

@@ -287,6 +287,15 @@ class ESP32S2ROM(ESP32ROM):
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.

View File

@@ -349,6 +349,17 @@ class ESP32S3ROM(ESP32ROM):
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.

View File

@@ -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 @@ class ESP8266ROM(ESPLoader):
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):

View File

@@ -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}.",
)

View File

@@ -230,13 +230,16 @@ class EsptoolTestCase:
# 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 @@ class EsptoolTestCase:
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 @@ class TestFlashDetection(EsptoolTestCase):
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