An I2C to UART bridge (like the NXP SC16IS750 or SC16IS752) converts a synchronous, multi-drop I2C bus into an asynchronous, point-to-point UART serial port. You need this hardware when your microcontroller—such as an ESP32 or ATmega2560—runs out of native hardware UARTs for GPS modules, cellular modems, or DMX512 lighting, but still has a free I2C bus. Rather than relying on unstable software serial emulation (which drops bytes at high baud rates), an I2C-to-UART IC provides a dedicated hardware FIFO buffer, handling the timing-critical bit-banging on the silicon level while your MCU simply reads and writes bytes over I2C.
I2C vs UART Bus Mechanics and Physical Layer
Before wiring a bridge, you must understand the physical layer differences between the two protocols. I2C is an open-drain, synchronous bus that requires external pull-up resistors to function. UART is a push-pull, asynchronous protocol that drives lines actively high and low. Bridging them requires an IC that translates the open-drain I2C clock/data signals into the rigid timing windows of UART start, data, parity, and stop bits.
| Feature | I2C (Inter-Integrated Circuit) | UART (Universal Asynchronous Receiver-Transmitter) |
|---|---|---|
| Wires | 2 (SDA, SCL) + Ground | 2 (TX, RX) + Ground |
| Topology | Multi-drop bus (up to 127 devices) | Point-to-point (1 TX to 1 RX) |
| Clocking | Synchronous (shared SCL line) | Asynchronous (local baud rate generators) |
| Max Speed | 100 kHz (Standard), 400 kHz (Fast), 1 MHz (Fast+) | Typically 115,200 bps; up to 5 Mbps on modern ICs |
| Addressing | 7-bit or 10-bit hardware addressing | None (hardware wire routing only) |
| Max Distance | ~1 meter (limited by bus capacitance) | ~15 meters at 9600 bps (RS-232/TTL limits) |
| Physical Drive | Open-drain (requires pull-up resistors) | Push-pull (active high/low drivers) |
Because I2C is open-drain, the SDA and SCL lines will float if not pulled high. For a 3.3V ESP32 running at 400 kHz, use 4.7 kΩ pull-up resistors to 3.3V. If your bus capacitance exceeds 200 pF (common when adding long wires or multiple breakout boards), drop to 2.2 kΩ to sharpen the rise times. Never connect 5V pull-ups to a 3.3V MCU I2C bus; you will fry the GPIO pins.
Sizing the Bridge: Distance, Speed, and Device Count
Choosing between native UART, an I2C-to-UART bridge, or an RS-485 transceiver depends entirely on your physical constraints. Use native UART when you have the pins available and need maximum throughput with zero I2C overhead. Use an I2C-to-UART bridge when you are pin-constrained on the MCU but the target serial device is within a few centimeters (intra-board). If you need to run serial data across a room or between buildings, abandon TTL UART entirely and use an RS-485 transceiver (like the MAX485) driven by your bridge or native UART.
The NXP SC16IS750 is the industry-standard single-channel bridge. It features a 64-byte TX and 64-byte RX FIFO, meaning your ESP32 can be busy handling WiFi interrupts for milliseconds without dropping incoming serial bytes.
| Parameter | Specification | Practical Implication |
|---|---|---|
| I2C Speed | Up to 400 kHz (Fast-mode) | Max I2C bus throughput is ~400 kbps; leaves headroom for 115.2k UART. |
| UART Baud Rate | Up to 5 Mbps (with 14.7456 MHz crystal) | Easily handles standard 9600, 57600, and 115200 baud rates. |
| FIFO Depth | 64 bytes (TX and RX) | Prevents data loss during MCU context switches or WiFi stack delays. |
| I2C Address | Configurable via A0 and A1 pins | Base address 0x48; allows up to 4 SC16IS750s on one I2C bus (16 total UARTs). |
| Operating Voltage | 2.5V to 3.6V (VCC) | Native 3.3V logic; perfect for ESP32, ESP8266, and Raspberry Pi. |
Classic Failures: Pull-ups, Address Clashes, and Baud Drift
When an I2C-to-UART bridge fails to communicate, the issue is almost always at the physical layer or in the register initialization sequence. Here is how to diagnose the three most common bench failures.
1. Missing or Undersized Pull-ups (The NACK Loop)
If your I2C scanner returns no devices, or you get random NACKs (Not Acknowledged) under load, check your pull-ups. Open-drain lines rely on the resistor to pull the voltage high. If the resistor is too large (e.g., 10 kΩ on a heavily loaded bus), the rise time exceeds the I2C specification, and the SC16IS750 misinterprets the clock edges. Fix: Verify 4.7 kΩ resistors are physically present on the SDA and SCL lines. Measure the idle bus voltage with a multimeter; it must read exactly VCC (3.3V).
2. Address Clashes and Pin Strapping
The SC16IS750 uses two hardware pins (A0 and A1) to set the lower two bits of its 7-bit I2C address. If you leave these pins floating, the internal logic will drift, causing the chip to respond to ghost addresses or ignore the MCU entirely. Fix: Explicitly tie A0 and A1 to GND for the base address (0x48), or to VCC to shift the address. Never leave CMOS configuration pins floating.
3. Baud Rate Drift and FIFO Overruns
UART relies on both sides agreeing on the exact timing of a bit. The SC16IS750 generates this timing using an external crystal oscillator. If you use a cheap breakout board with a 5% tolerance ceramic resonator instead of a quartz crystal, your 115,200 baud might actually be 118,000 baud, causing framing errors on the receiving device. Furthermore, if your MCU doesn't read the bridge's Receive Holding Register (RHR) fast enough, the 64-byte FIFO overflows. Fix: Sniff the UART TX line with a logic analyzer to verify the actual bit width. In code, monitor the Line Status Register (LSR, address 0x05) for the Overrun Error (OE) bit.
Wiring and Minimal Working Exchange (ESP32 to SC16IS750)
Below is a complete, dependency-free implementation using the Arduino Wire library. This bypasses third-party libraries that often break across ESP32 core updates, teaching you exactly how to manipulate the bridge's internal registers.
| SC16IS750 Pin | ESP32 DevKit Pin | Notes |
|---|---|---|
| VCC | 3V3 | Do not use 5V. |
| GND | GND | Common ground is mandatory. |
| SDA | GPIO 21 | Requires 4.7kΩ pull-up to 3V3. |
| SCL | GPIO 22 | Requires 4.7kΩ pull-up to 3V3. |
| A0 / A1 | GND | Sets I2C address to 0x48. |
| TX (UART out) | Target Device RX | Direct connection to peripheral. |
| RX (UART in) | Target Device TX | Direct connection from peripheral. |
Minimal Working Code: Register Configuration and TX/RX
To set the baud rate on the SC16IS750, you must set the Divisor Latch Access Bit (DLAB) in the Line Control Register (LCR). This remaps registers 0x00 and 0x01 from the FIFO buffers to the baud rate divisor latches. Assuming a standard 1.8432 MHz crystal on the breakout board, a divisor of 12 yields exactly 9600 baud.
#include <Wire.h>
#define BRIDGE_ADDR 0x48
// Register Addresses
#define REG_RHR 0x00 // Receive Holding Register (Read)
#define REG_THR 0x00 // Transmit Holding Register (Write)
#define REG_LCR 0x03 // Line Control Register
#define REG_LSR 0x05 // Line Status Register
void writeRegister(uint8_t reg, uint8_t val) {
Wire.beginTransmission(BRIDGE_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
uint8_t readRegister(uint8_t reg) {
Wire.beginTransmission(BRIDGE_ADDR);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(BRIDGE_ADDR, 1);
return Wire.read();
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // ESP32 default I2C pins
Wire.setClock(400000); // 400kHz Fast-mode
// 1. Reset the chip (Optional but recommended)
writeRegister(0x0E, 0x08); // Software reset via IOControl register
delay(10);
// 2. Enable FIFO and clear RX/TX buffers
writeRegister(0x02, 0x07); // FCR: Enable FIFO, clear RX and TX
// 3. Set Baud Rate to 9600 (Assuming 1.8432 MHz Crystal)
writeRegister(REG_LCR, 0x80); // Set DLAB = 1 to access divisor latches
writeRegister(0x00, 0x0C); // DLL = 12 (1.8432M / (16 * 9600))
writeRegister(0x01, 0x00); // DLM = 0
writeRegister(REG_LCR, 0x03); // Clear DLAB, set 8 data bits, no parity, 1 stop bit
Serial.println("SC16IS750 initialized at 9600 baud.");
}
void loop() {
// Transmit a byte to the UART peripheral
writeRegister(REG_THR, 'H');
writeRegister(REG_THR, 'i');
writeRegister(REG_THR, '\n');
// Check if data is available in the RX FIFO
uint8_t lsr = readRegister(REG_LSR);
if (lsr & 0x01) { // Data Ready bit
uint8_t rxByte = readRegister(REG_RHR);
Serial.print("Received from UART: ");
Serial.println((char)rxByte);
}
delay(1000);
}
If your ESP32 randomly freezes on
Wire.endTransmission(), the I2C bus has locked up—usually because the SC16IS750 was reset mid-transaction and is holding the SDA line low. Always implement a watchdog timer in production firmware, and ensure your breakout board has a dedicated hardware reset pin (RST) tied to a GPIO so you can hard-reset the bridge without power-cycling the entire system.
By offloading serial timing to the SC16IS750, you free your ESP32's main CPU cores to handle TLS encryption, WebSocket parsing, or sensor fusion without the constant interrupt overhead of software serial. Verify your physical pull-ups, strap your address pins, and let the silicon handle the bit-banging.






