When you’re wiring together fast embedded systems (like an ESP32 feeding data into a Teensy 4.1) it’s easy to assume USB will “just work.” After all, USB is fast. Way faster than UART, right?
That assumption is exactly what led to a subtle (and very common) bug: perfectly timed data turning into unreadable garbage.
Hello SparkFans! Paul here from PJRC. I spend a lot of time on the PJRC forum helping people solve all kinds of tricky problems. Some of those threads turn into deeper dives on how things really work.
In Paul’s Deep Dives, we'll revisit some of my favorite forum discussions. Along the way, we fill in the gaps, add context, and connect the dots so you not only see what worked — but understand why it worked. Enjoy!
Let’s walk through what happened, why it happens, and the one-line fix that makes everything click.
The goal was pretty straightforward:
The ESP32 test code was a simple counter printed once per second at 460800 baud.
On the PC? Everything looked perfect.
On the Teensy? Total nonsense:
�U��M�E��e��E��E��%�A��������...
Yet the Teensy was receiving data at the correct timing intervals. That’s the key clue.
At first glance, the reasoning makes sense:
But that’s not how USB serial adapters work.
Unlike most microcontrollers, Teensy 4.1 includes both USB device and host ports—making it possible to directly control USB-to-serial adapters like the CP2102.
The ESP32 dev board doesn’t expose raw UART over USB. Instead, it uses a USB-to-serial bridge chip (like the CP2102).
That chip sits between two worlds:
That baud rate must be set by the USB host.
When you use the Arduino Serial Monitor, selecting “460800 baud” isn’t just cosmetic—it sends a USB control message telling the CP2102:
“Talk to the ESP32 at 460800 baud.”
So when you unplug from your PC and plug into the Teensy, the Teensy becomes the host that has to configure that chip.
Back in the early days of PCs, serial ports were real hardware—9-pin or 25-pin connectors driven by UART chips. When software set a baud rate, it directly configured that hardware.
With USB-to-serial adapters like the CP2102, that idea still mostly holds—but with an extra layer.
When you select a baud rate in something like the Arduino Serial Monitor, your computer sends a USB control message to the adapter chip telling it:
“Use this baud rate on your UART pins.”
Even though the USB link itself runs at a fixed high speed (typically 12 Mbps or higher), the baud rate still matters—because it controls the UART side of the bridge.
So far, so good.
But then things get more interesting with boards that use native USB, like the Teensy.
On these boards, there is no CP2102. No hardware bridge at all.
Instead, your program itself implements the USB behavior in software.
And this leads to a subtle but important difference:
When you call Serial.begin(460800) on a Teensy, that baud rate doesn’t configure any hardware—and it doesn’t affect communication speed at all.
It’s simply stored in a variable.
If the PC sets a baud rate, that value is also just stored. Nothing about the actual USB data transfer changes—because USB always runs at its native speed (480 Mbps on Teensy 4.x).
That’s why:
Serial.begin()The baud rate is effectively ignored for communication.
From the original code:
USBSerial userial(myusb);
void setup() {
Serial.begin(460800); // Debug output
myusb.begin();
}
The mistake is subtle:
Serial.begin(460800) sets the Teensy’s USB device port (to your PC)userial (the USB host connection)So the CP2102 defaults to 115200 baud, while the ESP32 is sending at 460800 baud.
That mismatch = corrupted data.
Add this:
userial.begin(460800);
That’s it.
Now both sides agree on the UART speed, and the garbage disappears.
This issue trips people up because there are three layers involved:
The important takeaway:
USB speed and UART baud rate are completely independent.
Even though data travels over USB at megabits per second, the bridge chip still needs to know how fast to talk to the microcontroller.
Think of the CP2102 like a translator:
Setting the baud rate is like telling the translator how fast to speak on the UART side.
If you don’t tell it? It guesses (usually 115200). And that guess is often wrong.
This distinction also explains something subtle:
SerialThat’s why your Teensy debug output worked fine regardless of settings while the ESP32 link didn’t.
After fixing the baud rate on the host side, the system behaved exactly as expected:
A: 302 D: 9365
A: 312 D: 2652
A: 312 D: 2844
...
Clean, structured data from the LiDAR without any corruption.
Serial.begin() and userial.begin() control completely different interfacesThis is one of those bugs that feels like signal integrity or timing trouble—but turns out to be configuration.
Once you understand where the baud rate actually lives (hint: not just on the transmitting device), a whole class of “mystery corruption” problems becomes easy to diagnose.
This edition of Paul's Deep Dives is based off of this PJRC forum discussion