Skip to content

Commit

Permalink
feat(watchdog_reset): Add a new watchdog_reset option working even in…
Browse files Browse the repository at this point in the history
… USB modes
  • Loading branch information
radimkarnis committed Jan 3, 2025
1 parent ef407ed commit d37c38a
Show file tree
Hide file tree
Showing 14 changed files with 71 additions and 57 deletions.
1 change: 1 addition & 0 deletions docs/en/esptool/advanced-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion esptool/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)

Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions esptool/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 0 additions & 3 deletions esptool/targets/esp32c2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
12 changes: 3 additions & 9 deletions esptool/targets/esp32c3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))):
Expand Down
5 changes: 5 additions & 0 deletions esptool/targets/esp32c5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions esptool/targets/esp32c6.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions esptool/targets/esp32c61.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import struct
from typing import Dict

from .esp32c3 import ESP32C3ROM
from .esp32c6 import ESP32C6ROM


Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions esptool/targets/esp32h2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))):
Expand Down
5 changes: 0 additions & 5 deletions esptool/targets/esp32h21.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


from .esp32h2 import ESP32H2ROM
from ..loader import ESPLoader
from ..util import FatalError


Expand Down Expand Up @@ -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.")
Expand Down
10 changes: 5 additions & 5 deletions esptool/targets/esp32p4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions esptool/targets/esp32s2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions esptool/targets/esp32s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
29 changes: 12 additions & 17 deletions test/test_esptool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit d37c38a

Please sign in to comment.