From d37c38a683fae93fda5e6c9302a2e561490d95ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20Karni=C5=A1?= Date: Thu, 2 Jan 2025 15:55:55 +0100 Subject: [PATCH] feat(watchdog_reset): Add a new watchdog_reset option working even in USB modes --- docs/en/esptool/advanced-options.rst | 1 + esptool/__init__.py | 17 +++++++++++++++- esptool/loader.py | 7 +++++++ esptool/targets/esp32c2.py | 3 --- esptool/targets/esp32c3.py | 12 +++--------- esptool/targets/esp32c5.py | 5 +++++ esptool/targets/esp32c6.py | 6 +++--- esptool/targets/esp32c61.py | 5 +++++ esptool/targets/esp32h2.py | 6 +++--- esptool/targets/esp32h21.py | 5 ----- esptool/targets/esp32p4.py | 10 +++++----- esptool/targets/esp32s2.py | 10 +++++----- esptool/targets/esp32s3.py | 12 ++++++------ test/test_esptool.py | 29 ++++++++++++---------------- 14 files changed, 71 insertions(+), 57 deletions(-) diff --git a/docs/en/esptool/advanced-options.rst b/docs/en/esptool/advanced-options.rst index 89e87bc36..2e53b9f01 100644 --- a/docs/en/esptool/advanced-options.rst +++ b/docs/en/esptool/advanced-options.rst @@ -35,6 +35,7 @@ The ``--after`` argument allows you to specify whether the chip should be reset :esp8266: * ``--after soft_reset`` runs the user firmware, but any subsequent reset will return to the serial bootloader. This was the reset behaviour in esptool v1.x. * ``--after no_reset`` leaves the chip in the serial bootloader, no reset is performed. * ``--after no_reset_stub`` leaves the chip in the stub bootloader, no reset is performed. + :not esp8266 and not esp32 and not esp32h2 and not esp32c6: * ``--after watchdog_reset`` hard-resets the chip by triggering an internal watchdog reset. This is useful when the RTS control line is not available, especially in the USB-OTG and USB-Serial/JTAG modes. Use this if a chip is getting stuck in download mode when using the default reset method in USB-Serial/JTAG mode. Using this may cause the port to re-enumerate on Linux (e.g. ``/dev/ttyACM0`` -> ``/dev/ttyACM1``). Connect Loop diff --git a/esptool/__init__.py b/esptool/__init__.py index bb6c9f2bb..270c7c46f 100644 --- a/esptool/__init__.py +++ b/esptool/__init__.py @@ -151,7 +151,13 @@ def main(argv=None, esp=None): "--after", "-a", help="What to do after esptool.py is finished", - choices=["hard_reset", "soft_reset", "no_reset", "no_reset_stub"], + choices=[ + "hard_reset", + "soft_reset", + "no_reset", + "no_reset_stub", + "watchdog_reset", + ], default=os.environ.get("ESPTOOL_AFTER", "hard_reset"), ) @@ -1065,6 +1071,15 @@ def flash_xmc_startup(): esp.soft_reset(False) elif args.after == "no_reset_stub": print("Staying in flasher stub.") + elif args.after == "watchdog_reset": + if esp.secure_download_mode: + print( + "WARNING: Watchdog hard reset is not supported in Secure Download " + "Mode, attempting classic hard reset instead." + ) + esp.hard_reset() + else: + esp.watchdog_reset() else: # args.after == 'no_reset' print("Staying in bootloader.") if esp.IS_STUB: diff --git a/esptool/loader.py b/esptool/loader.py index 0c0b880b0..9b5dfe05f 100644 --- a/esptool/loader.py +++ b/esptool/loader.py @@ -1581,6 +1581,13 @@ def soft_reset(self, stay_in_bootloader): # in the stub loader self.command(self.ESP_RUN_USER_CODE, wait_response=False) + def watchdog_reset(self): + print( + f"WARNING: Watchdog hard reset is not supported on {self.CHIP_NAME}, " + "attempting classic hard reset instead." + ) + self.hard_reset() + def slip_reader(port, trace_function): """Generator to read SLIP packets from a serial port. diff --git a/esptool/targets/esp32c2.py b/esptool/targets/esp32c2.py index 08a7ad1ce..84b8b5ed4 100644 --- a/esptool/targets/esp32c2.py +++ b/esptool/targets/esp32c2.py @@ -133,9 +133,6 @@ def _post_connect(self): self.stub_is_disabled = True self.IS_STUB = False - def hard_reset(self): - ESPLoader.hard_reset(self) - """ Try to read (encryption key) and check if it is valid """ def is_flash_encryption_key_valid(self): diff --git a/esptool/targets/esp32c3.py b/esptool/targets/esp32c3.py index 0f5179e77..1da9b62e5 100644 --- a/esptool/targets/esp32c3.py +++ b/esptool/targets/esp32c3.py @@ -253,21 +253,15 @@ def _post_connect(self): if not self.sync_stub_detected: # Don't run if stub is reused self.disable_watchdogs() - def hard_reset(self): - if self.uses_usb_jtag_serial(): - self.rtc_wdt_reset() - sleep(0.5) # wait for reset to take effect - else: - ESPLoader.hard_reset(self) - - def rtc_wdt_reset(self): - print("Hard resetting with RTC WDT...") + def watchdog_reset(self): + print("Hard resetting with a watchdog...") self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY) # unlock self.write_reg(self.RTC_CNTL_WDTCONFIG1_REG, 2000) # set WDT timeout self.write_reg( self.RTC_CNTL_WDTCONFIG0_REG, (1 << 31) | (5 << 28) | (1 << 8) | 2 ) # enable WDT self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0) # lock + sleep(0.5) # wait for reset to take effect def check_spi_connection(self, spi_connection): if not set(spi_connection).issubset(set(range(0, 22))): diff --git a/esptool/targets/esp32c5.py b/esptool/targets/esp32c5.py index 79f44d086..47e645448 100644 --- a/esptool/targets/esp32c5.py +++ b/esptool/targets/esp32c5.py @@ -6,6 +6,7 @@ import time from typing import Dict +from .esp32c3 import ESP32C3ROM from .esp32c6 import ESP32C6ROM from ..loader import ESPLoader from ..util import FatalError @@ -162,6 +163,10 @@ def check_spi_connection(self, spi_connection): "consider using other pins for SPI flash connection." ) + def watchdog_reset(self): + # Watchdog reset disabled in parent (ESP32-C6) ROM, re-enable it + ESP32C3ROM.watchdog_reset(self) + class ESP32C5StubLoader(ESP32C5ROM): """Access class for ESP32C5 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32c6.py b/esptool/targets/esp32c6.py index 1de4e812f..02c78e6af 100644 --- a/esptool/targets/esp32c6.py +++ b/esptool/targets/esp32c6.py @@ -192,10 +192,10 @@ def check_spi_connection(self, spi_connection): "consider using other pins for SPI flash connection." ) - def hard_reset(self): + def watchdog_reset(self): # Bug in the USB-Serial/JTAG controller can cause the port to disappear - # if the chip is reset with RTC WDT, do a classic reset - ESPLoader.hard_reset(self) + # if watchdog reset happens, disable it on ESP32-C6 + ESPLoader.watchdog_reset(self) class ESP32C6StubLoader(ESP32C6ROM): diff --git a/esptool/targets/esp32c61.py b/esptool/targets/esp32c61.py index 066b5b861..102f0430d 100644 --- a/esptool/targets/esp32c61.py +++ b/esptool/targets/esp32c61.py @@ -5,6 +5,7 @@ import struct from typing import Dict +from .esp32c3 import ESP32C3ROM from .esp32c6 import ESP32C6ROM @@ -118,6 +119,10 @@ def read_mac(self, mac_type="BASE_MAC"): } return macs.get(mac_type, None) + def watchdog_reset(self): + # Watchdog reset disabled in parent (ESP32-C6) ROM, re-enable it + ESP32C3ROM.watchdog_reset(self) + class ESP32C61StubLoader(ESP32C61ROM): """Access class for ESP32C61 stub loader, runs on top of ROM. diff --git a/esptool/targets/esp32h2.py b/esptool/targets/esp32h2.py index 2bd7bfef2..be42a7b64 100644 --- a/esptool/targets/esp32h2.py +++ b/esptool/targets/esp32h2.py @@ -76,9 +76,9 @@ def get_crystal_freq(self): # ESP32H2 XTAL is fixed to 32MHz return 32 - def hard_reset(self): - # RTC WDT reset not available, do a classic reset - ESPLoader.hard_reset(self) + # Watchdog reset is not supported on ESP32-H2 + def watchdog_reset(self): + ESPLoader.watchdog_reset(self) def check_spi_connection(self, spi_connection): if not set(spi_connection).issubset(set(range(0, 28))): diff --git a/esptool/targets/esp32h21.py b/esptool/targets/esp32h21.py index 00c49800f..016097103 100644 --- a/esptool/targets/esp32h21.py +++ b/esptool/targets/esp32h21.py @@ -5,7 +5,6 @@ from .esp32h2 import ESP32H2ROM -from ..loader import ESPLoader from ..util import FatalError @@ -46,10 +45,6 @@ def get_crystal_freq(self): # ESP32H21 XTAL is fixed to 32MHz return 32 - def hard_reset(self): - # RTC WDT reset not available, do a classic reset - ESPLoader.hard_reset(self) - 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.") diff --git a/esptool/targets/esp32p4.py b/esptool/targets/esp32p4.py index 4d4c4ef54..ae91e3f68 100644 --- a/esptool/targets/esp32p4.py +++ b/esptool/targets/esp32p4.py @@ -262,19 +262,19 @@ def check_spi_connection(self, spi_connection): "consider using other pins for SPI flash connection." ) - def rtc_wdt_reset(self): - print("Hard resetting with RTC WDT...") + def watchdog_reset(self): + print("Hard resetting with a watchdog...") self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY) # unlock self.write_reg(self.RTC_CNTL_WDTCONFIG1_REG, 2000) # set WDT timeout self.write_reg( self.RTC_CNTL_WDTCONFIG0_REG, (1 << 31) | (5 << 28) | (1 << 8) | 2 ) # enable WDT self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0) # lock + sleep(0.5) # wait for reset to take effect def hard_reset(self): - if self.uses_usb_jtag_serial() or self.uses_usb_otg(): - self.rtc_wdt_reset() - sleep(0.5) # wait for reset to take effect + if self.uses_usb_otg(): + self.watchdog_reset() else: ESPLoader.hard_reset(self) diff --git a/esptool/targets/esp32s2.py b/esptool/targets/esp32s2.py index 039328e55..3b48e3a39 100644 --- a/esptool/targets/esp32s2.py +++ b/esptool/targets/esp32s2.py @@ -288,27 +288,27 @@ def _post_connect(self): if self.uses_usb_otg(): self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK - def rtc_wdt_reset(self): - print("Hard resetting with RTC WDT...") + def watchdog_reset(self): + print("Hard resetting with a watchdog...") self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY) # unlock self.write_reg(self.RTC_CNTL_WDTCONFIG1_REG, 2000) # set WDT timeout self.write_reg( self.RTC_CNTL_WDTCONFIG0_REG, (1 << 31) | (5 << 28) | (1 << 8) | 2 ) # enable WDT self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0) # lock + sleep(0.5) # wait for reset to take effect def hard_reset(self): uses_usb_otg = self.uses_usb_otg() if uses_usb_otg: - # Check the strapping register to see if we can perform RTC WDT reset + # Check the strapping register to see if we can perform a watchdog reset 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 # GPIO0 low and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0 ): - self.rtc_wdt_reset() - sleep(0.5) # wait for reset to take effect + self.watchdog_reset() return ESPLoader.hard_reset(self, uses_usb_otg) diff --git a/esptool/targets/esp32s3.py b/esptool/targets/esp32s3.py index 461083f20..8ad1d4e8d 100644 --- a/esptool/targets/esp32s3.py +++ b/esptool/targets/esp32s3.py @@ -352,14 +352,15 @@ def _post_connect(self): if not self.sync_stub_detected: # Don't run if stub is reused self.disable_watchdogs() - def rtc_wdt_reset(self): - print("Hard resetting with RTC WDT...") + def watchdog_reset(self): + print("Hard resetting with a watchdog...") self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, self.RTC_CNTL_WDT_WKEY) # unlock self.write_reg(self.RTC_CNTL_WDTCONFIG1_REG, 2000) # set WDT timeout self.write_reg( self.RTC_CNTL_WDTCONFIG0_REG, (1 << 31) | (5 << 28) | (1 << 8) | 2 ) # enable WDT self.write_reg(self.RTC_CNTL_WDTWPROTECT_REG, 0) # lock + sleep(0.5) # wait for reset to take effect def hard_reset(self): try: @@ -372,16 +373,15 @@ def hard_reset(self): # Skip if response was not valid and proceed to reset; e.g. when monitoring while resetting pass uses_usb_otg = self.uses_usb_otg() - if uses_usb_otg or self.uses_usb_jtag_serial(): - # Check the strapping register to see if we can perform RTC WDT reset + if uses_usb_otg: + # Check the strapping register to see if we can perform a watchdog reset 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 # GPIO0 low and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0 ): - self.rtc_wdt_reset() - sleep(0.5) # wait for reset to take effect + self.watchdog_reset() return ESPLoader.hard_reset(self, uses_usb_otg) diff --git a/test/test_esptool.py b/test/test_esptool.py index 1690a93d6..fdc9034e3 100755 --- a/test/test_esptool.py +++ b/test/test_esptool.py @@ -1534,29 +1534,24 @@ def test_make_image(self): os.remove("test0x00000.bin") -@pytest.mark.skipif( - arg_chip in ["esp8266", "esp32", "esp32h2"], reason="Not supported on this chip" -) @pytest.mark.skipif( "ESPTOOL_TEST_USB_OTG" in os.environ or arg_preload_port is not False, reason="Boot mode strapping pin pulled constantly low, can't reset out of bootloader", ) class TestReset(EsptoolTestCase): - def test_rtc_wdt_reset(self): - # Erase the bootloader to get "invalid header" output + test RTC WDT reset - res = self.run_esptool("--after no_reset erase_region 0x0 0x4000") - assert "Erase completed" in res - try: - esp = esptool.get_default_connected_device( - [arg_port], arg_port, 10, 115200, arg_chip + def test_watchdog_reset(self): + # Erase the bootloader to get "invalid header" output + test watchdog reset + res = self.run_esptool("--after watchdog_reset erase_region 0x0 0x4000") + if arg_chip in ["esp8266", "esp32", "esp32h2", "esp32c6"]: + assert "Watchdog hard reset is not supported" in res + assert "Hard resetting via RTS pin..." in res + else: + assert "Hard resetting with a watchdog..." in res + # If there is no output, the chip did not reset + # Mangled bytes are for C2 26 MHz when the baudrate doesn't match + self.verify_output( + [b"invalid header", b"\x02b\xe2n\x9e\xe0p\x12n\x9c\x0cn"] ) - esp.rtc_wdt_reset() - finally: - esp._port.close() - sleep(0.2) # Give the chip time to reset - # If there is no output, the chip did not reset - # Mangled bytes are for C2 26 MHz when the baudrate doesn't match - self.verify_output([b"invalid header", b"\x02b\xe2n\x9e\xe0p\x12n\x9c\x0cn"]) @pytest.mark.skipif(arg_chip != "esp32", reason="Don't need to test multiple times")