Hardware Hacking/ UART: From Signal to Shell / Baudrate Identification

Baudrate Identification

practical

Oscilloscope UART signal Oscilloscope capture: idle HIGH, start bit pulls LOW, 8 data bits, stop bit.

Why Baudrate Matters

UART is a frameless protocol. There is no clock line, no synchronization signal, no handshake telling the receiver how fast to sample incoming bits. Both sides simply agree on a speed beforehand β€” the baudrate. If you connect to a device at the wrong baudrate, the receiver samples bits at the wrong intervals and reconstructs garbage. You will see random binary noise, replacement characters, or partial fragments that look like text but aren't. You cannot read anything useful until you have the correct baudrate.

This is the first problem to solve when approaching an unknown device. Before you can read a shell, read boot messages, or interact with anything β€” you need the baudrate.


The Unknown Device Problem

Most consumer IoT devices, routers, embedded systems, and industrial controllers ship with UART exposed on the PCB but with zero documentation about it. The vendor never intended you to use it. There is no label, no datasheet, no FCC filing that mentions "connect at 115200 8N1." You are working from a blank slate.

Your job is to determine the correct baudrate through one of four methods: 1. Try common values systematically 2. Measure pulse width on the TX line with an oscilloscope or logic analyzer 3. Use automated brute-force tooling 4. Use auto-detection features in your capture tool


Method 1 β€” Try Common Values

The vast majority of embedded Linux devices use one of a small set of standard baudrates. Memorize this list:

Baudrate Common Usage
9600 Legacy devices, some industrial PLCs
19200 Older embedded systems
38400 Some Huawei and ZTE devices
57600 Older Cisco equipment, some modems
115200 Default for most modern IoT/Linux devices
230400 High-speed embedded, some routers
460800 Fast boot loaders, Qualcomm SoCs
921600 Very high-speed, rare

Start with 115200. This is correct for the majority of devices running embedded Linux (OpenWrt, Buildroot, Yocto-based systems, most MediaTek and Broadcom SoCs). If it fails, work down the list.

The workflow: connect your USB-to-UART adapter, open a terminal, power cycle the device, watch for output.

# Test with picocom β€” restart device between each attempt
picocom -b 115200 /dev/ttyUSB0

# If nothing readable after full boot, try next
picocom -b 9600 /dev/ttyUSB0

# Continue down the list
picocom -b 38400 /dev/ttyUSB0
picocom -b 57600 /dev/ttyUSB0

Press Ctrl+A then Ctrl+X to exit picocom between attempts. Always power cycle the device between tries β€” you want to catch early boot messages, not join mid-stream.

Signs you have the correct baudrate: - Readable ASCII text, even partial words - Recognizable Linux boot messages ("Booting Linux", "Starting kernel", "init: ") - U-Boot banner text - Any coherent English sentence

Signs of wrong baudrate: - Streams of ?, ~, }, { characters - Block characters (β–ˆ) or replacement characters (βŒ‚) - Binary-looking noise interspersed with random printable characters - Complete silence (different problem β€” may be wrong pins or TX/RX swapped) - Text that looks almost right but has every other character wrong β€” you're close, try adjacent values


Method 2 β€” Oscilloscope / Logic Analyzer Measurement

If trial-and-error is taking too long, or you want a definitive answer before connecting, measure the TX line directly.

The principle: UART encodes each bit as a fixed-duration pulse. The baudrate defines how many bits per second are transmitted. Therefore:

baudrate = 1 / bit_duration

More precisely, measure the shortest pulse visible on the TX line during active transmission (such as during boot output). That shortest pulse represents exactly one bit period.

The formula:

baudrate = 1 / T_bit

Where:
  T_bit = duration of one bit in seconds

Worked example:

Your oscilloscope shows the shortest pulse on TX is 8.68 microseconds.

baudrate = 1 / 0.00000868
baudrate = 115,207 β‰ˆ 115,200 baud

Another example: pulse width is 104 Β΅s:

baudrate = 1 / 0.000104
baudrate = 9,615 β‰ˆ 9,600 baud

And one more: pulse width is 17.36 Β΅s:

baudrate = 1 / 0.00001736
baudrate = 57,604 β‰ˆ 57,600 baud

The result will never be exactly a standard value due to crystal oscillator tolerances. Round to the nearest standard baudrate.

With a logic analyzer (Saleae, sigrok-compatible): Trigger capture on the TX line at boot. Most logic analyzer software (PulseView, Logic 2) will auto-decode UART if you set the correct baudrate β€” you can iterate the decoder setting until text appears. Alternatively, zoom in on any transmitted byte and measure the narrowest pulse manually.

With an oscilloscope: Set trigger to falling edge on the TX line. The device will idle high (mark state). When it transmits, it pulls low for the start bit, then alternates per data. Measure the minimum pulse width across several transitions.


Method 3 β€” baudrate.py Automated Brute-Force

Craig Heffner's baudrate.py tool automates the trial-and-error process. It cycles through common baudrates, captures a burst of data at each one, and scores the result based on printable ASCII content ratio. The baudrate with the highest printable ratio wins.

# Install dependencies first
pip3 install pyserial

# Run against your serial device
python3 baudrate.py /dev/ttyUSB0

The tool will cycle through candidates and display a score for each. It requires the device to be actively transmitting β€” ideal to run at boot time. If the device is idle, force output first (hold the reset button or power cycle).

Source: https://github.com/devttys0/baudrate


Method 4 β€” Auto-Detection with espilon-monitor

The espilon-monitor tool includes an --auto-baud flag that implements a similar scoring approach to baudrate.py, integrated into the capture workflow. It cycles through the standard baudrate list, measures printable character ratio, and locks onto the best candidate before starting structured logging.

./espilon-monitor --auto-baud /dev/ttyUSB0

When auto-detection locks onto a baudrate, it prints the detected value and switches into normal capture mode. This is the fastest workflow for initial reconnaissance on a new device.


Practical Workflow

When you encounter an unknown device:

  1. Connect UART, do not power on yet. Get your terminal ready.
  2. Try 115200 first. Power cycle, watch for 5–10 seconds through full boot.
  3. If garbage: Try 9600, then 38400, then 57600.
  4. If still garbage: Use oscilloscope or logic analyzer. Measure shortest TX pulse. Apply the formula.
  5. If you have espilon-monitor: Use --auto-baud to skip the manual iteration.

Edge Cases and Non-Standard Baudrates

Not every device uses a clean standard baudrate. Known exceptions:

  • Older Cisco equipment: 57600 is the historical default, not 115200
  • Some Huawei ONT/router hardware: 38400, occasionally 19200
  • Industrial PLCs and SCADA controllers: May use 9600 or custom values derived from non-standard crystal oscillators
  • Custom SoC firmware: Some vendors implement proprietary bootloaders that start at one baudrate (e.g. 921600 for fast initial flash) and switch to another (115200) for the OS β€” you may see a clean boot only at a different baudrate than the bootloader
  • Devices with dynamic baudrate negotiation: Rare but exists in some modem firmware; the device sends an AT banner at multiple speeds

If your measured pulse width gives a non-standard result (say, 26 Β΅s β†’ ~38,461 baud), do not round to the nearest standard value blindly. Try both adjacent standards (38400 and 57600). One of them will produce readable output.

The baudrate is foundational. Get it right before moving to anything else.