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 use the default behaviour (RTS & DTR both set).

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

Device Linux Windows 11 VM MacOS
ESP32-S3 TinyUSB OK OK
ESP32 + SiLabs CP2102 OK OK
ESP32-C3 Serial/JTAG Peripheral OK OK
ESP32 + CH33x
ESP8266 + SiLabs CP2102
ESP32 + FTDI
RPI_PICO TinyUSB OK OK
D51 TinyUSB OK OK
stm32 native (PyBoard) OK OK
stm32 + st-link OK OK
nrf5x TinyUSB
  • A few targets in this table are still untested (mostly, testing all the different USB chipset drivers), but I think I have most of these in my bucket of ESP boards!
  • 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 don't have access to a Mac to verify there's no regression there, but I don't expect one (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.

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

Signed-off-by: Angus Gratton <angus@redyak.com.au>
Signed-off-by: Angus Gratton <angus@redyak.com.au>
@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).

Copy link

codecov bot commented Aug 28, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.38%. Comparing base (8c47e44) to head (132731a).
⚠️ Report is 10 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #18001   +/-   ##
=======================================
  Coverage   98.38%   98.38%           
=======================================
  Files         171      171           
  Lines       22296    22296           
=======================================
  Hits        21937    21937           
  Misses        359      359           

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

Copy link

Code size report:

   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 )

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.

3 participants