Skip to content

Conversation

@projectgus
Copy link
Contributor

@projectgus projectgus commented Aug 28, 2025

Summary

This is a possible alternative fix for #9659 which uses a different approach to #17776 and #17800.

Background

There are two conflicting problems with use of DTR and RTS in mpremote (and other serial host programs):

  1. USB-CDC implementations tend to use "DTR is set" as a signal for "host is connected" (a reasonable thing to do), and therefore don't send any data to the host if DTR is cleared.

  2. ESP8266 and ESP32 boards use a convention where setting (DTR&&!RTS) means "put IO0 low for bootloader mode" and (!DTR&&RTS) means "trigger reset", allowing the host to reset the chip to bootloader mode. Traditionally this is implemented in a small circuit - the circuit mostly exists so that default behaviour of an open serial port (DTR&&RTS) doesn't trigger a reset.

    With the Espressif integrated "USB Serial & JTAG" peripheral and Espressif TinyUSB native USB stack the same reset logic is implemented by looking for a sequence of line state transition packets (implemented in hardware and software, respectively).

#9659 describes a problem where Windows (and/or pyserial on Windows) clears DTR before RTS when closing the port, and therefore triggers a hard reset each time mpremote exits.

@Josverl has done a lot of hard work and testing to find a DTR & RTS setting which works for (2) without causing problems due to (1), and has come up with a heuristic for detecting possible Espressif boards. The challenge is that pretty much any USB/Serial chip on the market has been connected to an ESP8266 or ESP32 at some point, and there's no way to tell what MCU is connected from the USB side.

There also cases like #17999 where an Espressif chip with a native USB implementation needs (1) in order to function correctly.

Approach in this PR

Instead, we can manually clear RTS before DTR when closing the port, to avoid the reset issue. Opening the port can mostly use the default behaviour (RTS & DTR both set). So far have only found one exception: on Windows the Silicon Labs CP210x driver toggles DTR and RTS with a long delay in between the first time the port is opened after a reset, which triggers a hard reset the first time mpremote tries to connect - have worked around this specific case on connection.

In mpremote, the transport 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.

Testing

On Linux & WSL my test process is:

mpremote a0 exec "a=53"; mpremote a0 resume exec "print(a)" 

If the resume works correctly then this should print 53. If a reset occurs when mpremote exits then it will print NameError: name 'a' isn't defined.

Windows allocates a new COM port for each device, so made a batch file to only type the port name once:

python -m mpremote connect %1 exec "a=52"
python -m mpremote connect %1 resume exec "print(a)"

Results:

Board & Port USB Device Linux Windows 11 WSL MacOS
ESP32-S3-DevKitC (USB port) MP TinyUSB OK OK OK
ESP32-S3-DevKitC (UART port) SiLabs CP2102N OK OK OK
ESP32-C6-DevKitM (USB port) Espressif Serial/JTAG Peripheral OK OK OK
ESP32-C6-DevKitM (UART port) SiLabs CP2102N OK OK OK
ESP32-C2-DevKitM SiLabs CP2102N OK OK OK
ESP32 Pico Core Board V3 SiLabs CP2102 OK OK OK
ESP32 ECO1 Core Board SiLabs CP2102 OK OK
Seeed XIAO (ESP32-C3) Espressif Serial/JTAG Peripheral OK OK OK
ESP32-WROVER-KIT FTDI FT2232H OK OK
ESP8266 NodeMCU Amica SiLabs CP2102 OK OK
ESP8266 WeMos D1 Mini WCH CH340 OK OK OK
ESP8266 WeMos D1 WCH CH340 OK OK
RPI_PICO MP TinyUSB OK OK OK
Seeed WIO Terminal (SAMD D51) MP TinyUSB OK OK
PyBoard SF-2W MP stm32 USB-CDC OK OK
NUCLEO-H723G CN1 st-link embedded USB-CDC adapter OK OK
  • Windows tests are using a VM, but the USB host controller PCI device is passed through to the VM with VFIO so the USB device only interacts with Windows, should be equivalent to a native host. (Passthrough of the USB controller is necessary to reproduce the quirk with CP210x driver after power-on, if the USB device is first enumerated by Linux then the problem goes away.)
  • I haven't tested every board on WSL (using usbipd-win) but tested one of each chip at least.
  • If anyone has ESP32 boards with WCH USB/serial chips onboard then I'd appreciate any testing you could do.
  • I don't have access to a Mac to verify there's no regression there, but I don't expect one (the port opening sequence hasn't changed for macOS, and clearing RTS early shouldn't break anything).
  • Also did some manual tests of pressing the Reset button while mpremote REPL was connected to double check the board doesn't reset into bootloader mode.

Trade-offs and Alternatives

  • If the mpremote process is killed by Windows then Windows may clear DTR before RTS during cleanup and trigger a hard reset (not verified), but this seems like a reasonable limitation. The close() path is in a finally block otherwise, so any "normal" mpremote exit should hit it.
  • We could keep adding quirks for specific USB IDs, but I think this risks getting into a game of "whack a mole" with vendor USB/serial chip choices.

Follow Up Work

  • We should add command line options to manually set DTR and RTS on the serial port. This will allow using Espressif devices where DTR and RTS are wired directly to the control pins, and provide a way to work around any other unusual serial configs. Can be done in a follow-up PR.

@projectgus projectgus marked this pull request as draft August 28, 2025 22:34
@projectgus projectgus added port-esp32 tools Relates to tools/ directory in source, or other tooling labels Aug 28, 2025
@projectgus
Copy link
Contributor Author

projectgus commented Aug 28, 2025

@Josverl you've looked into this a lot more recently than I have, so I'm very curious what you think.

I noticed that in your results table here you didn't get a REPL on non-ESP devices when setting DTR=True and RTS=True. I wasn't able to reproduce this (my understanding is that as long as DTR is set, RTS should be ignored) but it did give me some concern that I've missed something (or behaviour is different in some other Windows configuration).

@codecov
Copy link

codecov bot commented Aug 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.39%. Comparing base (a563592) to head (3dd8073).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #18001   +/-   ##
=======================================
  Coverage   98.39%   98.39%           
=======================================
  Files         171      171           
  Lines       22289    22289           
=======================================
  Hits        21931    21931           
  Misses        358      358           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link

github-actions bot commented Aug 28, 2025

Code size report:

  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@robert-hh
Copy link
Contributor

Some board vendors (ESP32 and others) wanted to save a few pennies and omitted the two transistor logic for RTS and DTR. Instead RTS and DTR are directly connected to the respective pins, e.g. Reset and IO0. Then it matters whether RTS and DTR are both low or both high. Such board required having control over RTS and DTR. That is possible e.g. with picocom. I faintly recall that there were similar discussion for mpremote.

@Josverl
Copy link
Contributor

Josverl commented Aug 29, 2025

My tests are all passing through the USB device to a Windows 11 VM on a Linux host. I expect native Windows behaviour will be the same., but it'd be good if someone could verify this.

I do expect differences, mainly as I also see differences in when running Ubuntu/ WSL2 in Windows. Could be it's just timing, but we know it only needs a spike to cause a reset.
I'll see what I can repo with the boards I have with me now. ( Not at home)

@Josverl
Copy link
Contributor

Josverl commented Aug 29, 2025

Then it matters whether RTS and DTR are both low or both high. Such board required having control over RTS and DTR. That is possible e.g. with picocom. I faintly recall that there were similar discussion for mpremote.

So would it be better to set RTS/DTR defaults (for ESPxxx) that can be changed by cmdline/config, rather than hard coding ?

Also, how can I determine on a board if the dual transistor logic you mention is in place? I do not want to pollute the tests, and I have both cheap and non-cheap boards

(And my electronics skills are basic)

@robert-hh
Copy link
Contributor

So would it be better to set RTS/DTR defaults (for ESPxxx) that can be changed by cmdline/config, rather than hard coding ?

Yes. The default can be that both DTR and RTS are set (levels at the board low), which is the actual state.

Also, how can I determine on a board if the dual transistor logic you mention is in place? I do not want to pollute the tests, and I have both cheap and non-cheap boards

You can try the various settings, whether the board responds, or just assume that the majority of boards behave well with RTS and DTR both set and not use untypical boards for firmware testing. The special cases are then left as support for users with untypical boards.

P.S.: Not all cheap boards omit the reset/firmware_load logic.

@Josverl
Copy link
Contributor

Josverl commented Aug 29, 2025

@projectgus
wrt to the table above ; Several of the boards have multiple USB ports,
Usually they are labeled USB or UART/COM/Serial .

Can you clarify that in the table please ?
As these issues show these details 🙄matters in testing , and in issue reports as well.
I only have a few boards with me currently ( S3 - dual port , and C3-Dual port )

@projectgus
Copy link
Contributor Author

Some board vendors (ESP32 and others) wanted to save a few pennies and omitted the two transistor logic for RTS and DTR. Instead RTS and DTR are directly connected to the respective pins, e.g. Reset and IO0. Then it matters whether RTS and DTR are both low or both high. Such board required having control over RTS and DTR. That is possible e.g. with picocom. I faintly recall that there were similar discussion for mpremote.

Indeed! In fact, this is the wiring convention (and problem) that inspired the first reset circuit for ESP8266, on the original NodeMCU board circa 2014/15!

I agree it'd be useful to have this configurable for mpremote. However, that's somewhat separate to this problem (already mpremote will assert DTR and RTS from a MacOS or Linux host, Windows is the only one that doesn't due to the existing workaround).

FWIW, over the years I was the esptool maintainer I don't remember any complete dev boards with USB but no transistor logic, although I'm sure you're right that there are some out there (I do remember some with crappy implementations of the transistor logic). I've seen the "no reset circuit" pattern crop up a lot in consumer products or module breakouts where there might be a header that exposes RX, TX, IO0 and RST pins directly.

wrt to the table above ; Several of the boards have multiple USB ports,
Usually they are labeled USB or UART/COM/Serial .

Can you clarify that in the table please ?

Sorry about that, I've edited the table to explain better. A bunch of the "todos" are a bit vague because they're prompts for me to go rummage through my embarassingly large stock of ESP boards and find examples with all the possible interface chips! Once I find one (or someone else reports a result) then I'll update the table with the actual board details.

I'll see what I can repo with the boards I have with me now. ( Not at home)

If/when you have time then that'd be great, thank you! I'll do some testing with other boards (and maybe other host configs) next week, as well.

@robert-hh
Copy link
Contributor

already mpremote will assert DTR and RTS from a MacOS or Linux host

assert DTR and RTS means that the level at the output of the USB/UART bridge is low. So these "non-conforming" board do not work with mpremote and Linux.

@projectgus
Copy link
Contributor Author

already mpremote will assert DTR and RTS from a MacOS or Linux host

assert DTR and RTS means that the level at the output of the USB/UART bridge is low. So these "non-conforming" board do not work with mpremote and Linux.

Yes indeed, that's why I'm saying that it's a separate concern to the problem that this PR is addressing. (To be clear: If this PR is merged then those configurations may stop working on Windows, but they already don't work from Linux or MacOS. This PR would at least make the behaviour consistent across different hosts, and then if necessary we can look at adding a config setting or an option that allows those particular boards to work from all hosts.)

@kdschlosser
Copy link

You have to be careful messing around with setting the states of DTR and RTS as they control the ESP32's boot mode and restarting. I know that this is hard wired into the hardware USB-CDC on the ESP32's that support native USB. I do not know how this plays into the software stack that MicroPython is using but my guess is going to be that the software stack needs to watch those signals and to perform reboots and or setting the boot mode as needed depending on the state of DTR and RTS.

at the very bottom.
https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/get-started/establish-serial-connection.html#example-output

and then if you read here it will shed some more insight on those lines.

https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/boot-mode-selection.html#automatic-bootloader

Now I know the above link is outlined for using a UART to USB bridge IC but the same applies when using the native USB.

I also think that this might be a tripping up point...

When developing esptool, keep in mind DTR and RTS are active low signals, i.e., True = pin @ 0V, False = pin @ VCC.

@projectgus
Copy link
Contributor Author

projectgus commented Sep 1, 2025

You have to be careful messing around with setting the states of DTR and RTS as they control the ESP32's boot mode and restarting

I appreciate your concern, but I'm well aware. Without putting too fine a point on this, I was the maintainer of esptool from 2015 to 2021 (including five years employed by Espressif). I wrote the sentence from the esptool documentation that you've quoted from (although that was a wiki page, back then).

I'm not saying this PR is infallible (hence marking as draft and asking for testers), but I am saying that I'm across the issue of DTR and RTS on Espressif boards.

@projectgus
Copy link
Contributor Author

projectgus commented Sep 4, 2025

I improved my Windows VM test setup by passing through the USB controller PCIe device to the VM - this should give equivalent USB behaviour to a native host. This revealed a quirk in the Silicon Labs driver that needs a workaround to avoid triggering a hard reset the first time the port is opened after power on (but the port was OK if opened additional times after that, very odd).

Was able to do a bunch more testing with various boards, have updated the table at the top. So far so good.

If anyone has unusual boards, additional USB/Serial chips, or unusual Windows setups then I'd greatly appreciate any test results you can provide. 🙏

@Josverl
Copy link
Contributor

Josverl commented Sep 4, 2025

Sharing my test results
All tests using :

  • mpremote 1.27.0rc0.post75+g2416c6fee.d20250907 ( corrected on Windows )
  • MicroPython v1.26.0
Board & Port USB Device Linux Windows 11 WSL MacOS
Wemos ESP32-S2 mini USB-CDC . OK OK .
PYBB v1.1 clone USB-CDC . OK OK .
PICO USB-CDC . OK OK .
RP2040 nano connect USB-CDC . OK OK .
NodeMCU Devkit 0.9 esp8266 CH340 . OK OK .
Clone D1 esp8266 CH340 . OK OK .
PICO2 USB-CDC . OK OK .
NodeMCU Amica esp8266 cp2102 / CP210x . OK OK .
TTGO v1.8 ESP32 CP210x . OK OK .
ESP32-S3-DevKitC (USB port) USB-CDC . OK OK .
ESP32-S3-DevKitC (UART port) CP210x . OK OK .
Lolin C3 mini v2.1.0 USB-CDC . OK, FAIL-1 OK .
clone AI-ESP32-C3: nact ESP32-C6 v1.0 (UART) CH340 . OK OK .
clone AI-ESP32-C3: nact ESP32-C6 v1.0 (USB) USB-CDC . OK FAIL-2 OK .
clone ESP32-C6: nact ESP32-C6 v1.0 (UART) CH340 . OK OK .
clone ESP32-C6: nact ESP32-C6 v1.0 (USB) USB-CDC . ok FAIL-3 OK .

Updated with WLS2 tests

No longer relevant

FAIL-1:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' isn't defined```

FAIL-2:

mpremote.transport.TransportError: could not enter raw repl

FAIL-3:

works on UART interface
fails on USB CDC interface

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' isn't defined```

@projectgus
Copy link
Contributor Author

Thanks @Josverl, I really appreciate your help with this. That's interesting that all three failures you have are connecting to the Espressif USB/JTAG hardware peripheral. I'll do some more testing with different MP versions on my board, try to reproduce.

  • mpremote 1.26.0rc0.post174+g5a7200217b.d20250619

Just to check: I don't have a commit 5a7200217b in my repo anywhere - did you perhaps rebase this PR branch? (I don't expect it's the difference, but worth ruling it out.)

@Josverl
Copy link
Contributor

Josverl commented Sep 7, 2025

Thanks for catching my mistake.
I assumed uv pip install tools/mpremote would update mpremote in the active environment.
It only does that after forcing it with --reinstall 🤦

I repeated the Windows tests with the mpremote version from this PR

@projectgus
Copy link
Contributor Author

I repeated the Windows tests with the mpremote version from this PR

Well, that's good news at least, I was really not looking forward to trying to figure out what was different between my Windows test setup and yours! Thanks for testing twice. 😁

Do you have any outstanding concerns with this approach (aside from the desirability of adding a manual override option, which I fully agree we should do)?

(For my own 2c: I'm not super happy that we turned out to need a SiLabs CP210x driver workaround on Windows hosts, although on the plus side the workaround should lead to consistent CP210x behaviour on all host OSes - and the behaviour is valid whether or not the CP210x is connected to an Espressif SoC or something else that might expect DTR or RTS to be used in a conventional way.)

@Josverl
Copy link
Contributor

Josverl commented Sep 9, 2025

Do you have any outstanding concerns with this approach

LGTM

and then we can add the option to apply rts/dtr tweaks from options/environment/config files separately.

( I do have one board ( ESP32-PICO-KIT-V4 - 4517) that needs a hard reset before mpremote will connect - but that was also with mpremote 1.25.0 - so I think that is a board defect and should not hold this back

@projectgus projectgus marked this pull request as ready for review September 10, 2025 00:37
@dpgeorge
Copy link
Member

This can now be rebased.

@projectgus projectgus force-pushed the bugfix/mpremote_esp_win_quirk branch from 2416c6f to 868c5f9 Compare September 10, 2025 23:06
@projectgus
Copy link
Contributor Author

projectgus commented Sep 10, 2025

Rebased, did a couple of quick re-tests (although the only mpremote change after rebase is moving the import block to the top of the file).

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>
@dpgeorge dpgeorge force-pushed the bugfix/mpremote_esp_win_quirk branch from 868c5f9 to 3dd8073 Compare September 30, 2025 23:11
@dpgeorge dpgeorge merged commit 3dd8073 into micropython:master Sep 30, 2025
72 of 73 checks passed
@projectgus projectgus deleted the bugfix/mpremote_esp_win_quirk branch October 1, 2025 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

port-esp32 tools Relates to tools/ directory in source, or other tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants