Why level matters
An ESP32 GPIO maxes out at 3.3V. Feed it 5V from an Arduino UNO output → over time the input pin breaks. The reverse problem: an ESP32 outputs 3.3V "HIGH" to a 5V device — which may not recognize it as HIGH at all.
Solutions cheat-sheet
- Direct connect — when both sides operate at same voltage, or when device B is 5V tolerant on inputs (rare — check datasheet).
- Voltage divider — drop 5V → 3.3V for single-direction input only. 1.8kΩ + 3.3kΩ resistors. Doesn't work for output / bidirectional / fast signals.
- MOSFET level shifter — bidirectional, works for I²C/SPI/UART. BSS138 + 2 pull-ups. Cheap, slow (~1 MHz max).
- Dedicated IC — TXB0108 (8-channel bi-directional), TXS0108E. Fast, fits 100 MHz signals.
- Opto-isolator — overkill for level matching but needed if grounds are separate (motor controllers, mains).
I²C special case
I²C is open-drain — pulled to whichever rail your pull-up resistors connect to. Connect SDA/SCL pull-ups to the LOWER voltage (3.3V) and a 3.3V MCU + 5V device can share the bus directly. No shifter needed if the 5V device's HIGH threshold is below 3.3V (check VIH spec).