esp32arduino-esp32

ESP32-S3 delay before serial printing works


I need to see some feedback in my setup() code within Arduino framework, and am using a simple Serial.println(), but it never appears unless I add a delay. Sometimes even the Serial.println() in loop() misses the first few loops.

#include <Arduino.h>

int loopCount = 0;

void setup() {
  Serial.begin(460800);
//  delay(3500);
  while(!Serial) {}
  Serial.println("setup");
}

void loop() {
  Serial.println(String(++loopCount) + " " + String(millis()));
  delay(1000);
}

If I run this on a plain ESP32 (eg esp32doit-devkit-v1), and comment out the fixed delay and the while loop, I get

setup
1 28
2 1029
3 2028

which is what I need and expect - serial printing starts straight away. On the ESP32-S3 (eg esp32-s3-devkitc-1) without delay or while, I get

4 3098
5 4098
6 5098

so the setup printing is missed, as well as the first 3 times through the loop. If I use the while(!Serial) {} line, I get

5 4098
6 5098
7 6098
8 7098

which is even worse, so I mainly use the fixed delay(3500). But I really need immediate feedback within the code I am debugging, just like the plain ESP32.

I am using a Mac and PlatformIO to define the boards

[platformio]
default_envs = esp32
src_dir = src
include_dir = include

[env:esp32]
platform = espressif32
; board = esp32doit-devkit-v1
board = esp32-s3-devkitc-1
framework = arduino
upload_port = /dev/cu.usb*

; Serial Monitor options
monitor_speed = 460800
monitor_filters = colorize
monitor_echo = yes

What can I do?


Solution

  • That board has two USB interfaces. (Oddly, I just checked what I thought was a clone of that board, albeit with USB-C connectors, and the location of those two interfaces is physically swapped compared to your picture. TIL...)

    The one that's labeled UART uses a "real" USB-Serial bridge, provided by the hardware UART that's just above the buttons in that picture. That chip works across a hardware reset and will hold the connection to your Mac as it never goes away. You can write bytes right after a reset and they're dutifully delivered to the host.

    The one that's labeled as "USB" is kind of a pain for development and debugging. It uses gates inside the SOC and a dash of magic in the provided internal ROM to provide a USB implementation on the ESP32-S3 itself. This is awesome as it can let your ESP32-S3 pretend to be lots of different things, like a joystick, keyboard, or even a mass storage device to your computer and you can attach USB things to it. The downfall is that between it taking a few milliseconds after reset for the chip itself to bring up the internal logic to pretend to be a serial port AND the fact that the stupid port disappears and has to be re-discovered by the Host OS, which drops serial connections to your debugger, your monitor, and anything else you had connected, there's a second or two after even a software reset where the chip will disconnect from your computer and then re-appear, after which any attached processes (like 'monitor', tio, or cu) can choose to reconnect.

    This is why the delay is necessary.

    Another helpful technique I use is to just buffer all my output and then make that available via a telnet port (which ALSO goes away on reset) where I just buffer everything after boot until I can confirm a connection or keystroke from the monitor application before barfing up all the messages that had been collected until that point. (OK, I only collect a few thousand when booting, but PSRAM is cheap and holding data for a few seconds before freeing it isn't a big deal...)

    Honestly, it makes debugging/developing boot code kind of a pain on that port. By far the simplest thing is to just use the other one when you have that option. On designs that don't have that redundant (and usually unnecessary in production) hardware, I've taken to spin-waiting in setup() exactly as you describe - or doing that part of my development on a board with both and then moving back to the real hardware once I'm comfortably into main().

    Incidentally, f that board uses the same WCH UART that my $4 clone boards use, it's worth a note that the serial ports MOSTLY work with the default driver on MacOS, but if you press them deeply into service, such as with an upload of a few hundred KB, they will mysteriously corrupt your data and make you crazy while you chase ghosts. On those boards you have to install the driver from WCHthat gives the dev nodes names like /dev/cu.wchusbserial54E2.... and not /dev/cu.usbmodem*

    Configuring the S3 (and C3 and all the ESP32's after the 2016 era of corees) to be serial ports in this way is configured by the ARDUINO_USB_CDC_ON_BOOT flag in your build. With that set, the second endpoint on the USB pipe becomes the serial connection. That tells the Arduino wrapper to build that fake Serial object for you.

    Various doc on these facilities: https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-guides/usb-serial-jtag-console.html?highlight=usb%20serial

    https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/get-started/establish-serial-connection.html

    https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/usb_host.html?highlight=usb%20serial