tools/mpremote: Workaround ESP DTR/RTS reset quirk at disconnect time.

The problem with ESP board spurious reset happens at disconnect time on
Windows (clearing DTR before RTS triggers a reset).

Previous workarounds tried to detect possible ESP boards and apply the
correct DTR and RTS settings when opening the port.

Instead, we can manually clear RTS before closing the port and thereby
avoid the reset issue. Opening the port can keep the default behaviour
(RTS & DTR both set).

close() is called from a finally block in the mpremote main module
(via do_disconnect()) - so this should always happen provided the Python
process isn't terminated by the OS.

One additional workaround is needed to prevent a spurious reset first time
a Silicon Labs CP210x-based ESP board is opened by mpremote after
enumeration.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
This commit is contained in:
Angus Gratton
2025-08-29 08:06:27 +10:00
committed by Damien George
parent a563592b11
commit 3dd8073c29

View File

@@ -42,31 +42,8 @@ from errno import EPERM
from .console import VT_ENABLED
from .transport import TransportError, TransportExecError, Transport
VID_ESPRESSIF = 0x303A # Espressif Incorporated
PID_ESPRESSIF_SERIAL_JTAG = 0x1001 # Serial/JTAG peripheral of ESP32-S3,C3,C6
def has_espressif_dtr_quirk(devicename):
"""ESP8266 and ESP32 dev boards use the DTR and RTS lines to trigger reset &
reset into bootloader mode. This can causes spurious reset issues on Windows.
Apply the quirk to any USB/Serial chip on Windows that isn't using the
Microsoft CDC-ACM driver, or to the integrated Espressif Serial/JTAG device.
Don't apply it to Espressif boards running TinyUSB, as TinyUSB uses DTR
to determine if the CDC port is open (and there's no spurious reset issue).
"""
portinfo = list(serial.tools.list_ports.grep(devicename)) # type: ignore
if not portinfo:
return False
def port_attr(name):
return getattr(portinfo[0], name, None)
return (port_attr("vid"), port_attr("pid")) == (
VID_ESPRESSIF,
PID_ESPRESSIF_SERIAL_JTAG,
) or port_attr("manufacturer") != "Microsoft"
VID_SILICON_LABS = 0x10C4
class SerialTransport(Transport):
@@ -90,19 +67,27 @@ class SerialTransport(Transport):
delayed = False
for attempt in range(wait + 1):
try:
if device.startswith("rfc2217://"):
self.serial = serial.serial_for_url(device, **serial_kwargs)
elif os.name == "nt":
self.serial = serial.Serial(**serial_kwargs)
self.serial.port = device
if has_espressif_dtr_quirk(device):
# DTR False: to avoid using the reset button will hang the MCU in bootloader mode
# RTS False: to prevent pulses on rts on serial.close() that would POWERON_RESET an ESPxx
self.serial.dtr = False # DTR False = gpio0 High = Normal boot
self.serial.rts = False # RTS False = EN High = MCU enabled
self.serial = serial.serial_for_url(device, do_not_open=True, **serial_kwargs)
if os.name == "nt":
portinfo = list(serial.tools.list_ports.grep(device)) # type: ignore
if portinfo and getattr(portinfo[0], "vid", None) == VID_SILICON_LABS:
# Silicon Labs CP210x driver on Windows has a quirk
# where after a power on reset it will set DTR and RTS
# at different times when the port is opened (it doesn't
# happen on subsequent openings).
#
# To avoid issues with spurious reset on Espressif boards we clear DTR and RTS,
# open the port, and then set them in an order which prevents triggering a reset.
self.serial.dtr = False
self.serial.rts = False
self.serial.open()
self.serial.dtr = True
self.serial.rts = True
# On all other host/driver combinations we keep the default
# behaviour (pyserial will set DTR and RTS automatically on open)
if not self.serial.isOpen():
self.serial.open()
else:
self.serial = serial.Serial(device, **serial_kwargs)
break
except OSError:
if wait == 0:
@@ -121,6 +106,9 @@ class SerialTransport(Transport):
print("")
def close(self):
# ESP Windows quirk: Prevent target from resetting when Windows clears DTR before RTS
self.serial.rts = False
self.serial.dtr = False
self.serial.close()
def read_until(