For detailed reference on each protocol, see our Protocol Reference guides.
Your ESP32 is only as useful as the things it can talk to — sensors, displays, SD cards, GPS modules, and other microcontrollers. These devices communicate using standardized protocols: agreed-upon rules for sending and receiving data. In this article, we will cover the four protocols you will encounter most often in ESP32 projects: I2C, SPI, UART, and OneWire.
🔗Protocol Comparison at a Glance
| Feature | I2C | SPI | UART | OneWire |
|---|---|---|---|---|
| Wires needed | 2 (SDA, SCL) | 4+ (MOSI, MISO, SCK, CS) | 2 (TX, RX) | 1 (Data) |
| Speed | Up to 400kHz (standard) | Up to 80MHz on ESP32 | Typically 9600--115200 baud | ~16kbps |
| Devices on bus | Up to 127 (by address) | One per CS line | Point-to-point (1 to 1) | Multiple (by address) |
| Best for | Sensors, small displays | Fast displays, SD cards | GPS, other MCUs, debug | DS18B20 temperature sensors |
| Complexity | Low | Medium | Low | Low |
Tip: When shopping for a sensor or module, check which protocol it uses. If you are building a project with many sensors, I2C is usually the most convenient because multiple devices share just two wires.
🔗I2C (Inter-Integrated Circuit)
I2C (pronounced "eye-squared-see" or "eye-two-see") is the most common protocol for connecting sensors to an ESP32. It uses just two wires:
- SDA (Serial Data) -- carries the data
- SCL (Serial Clock) -- carries the clock signal
On most ESP32-WROOM-32 DevKit boards, the default I2C pins are:
| Signal | Default GPIO |
|---|---|
| SDA | GPIO 21 |
| SCL | GPIO 22 |
Note: The ESP32 has two I2C bus controllers (I2C0 and I2C1), and you can assign them to almost any GPIO pin. The defaults above are just conventions -- your board's pinout may differ. Always check your specific board's documentation.
🔗How I2C Works
I2C is a master-slave protocol. The ESP32 is the master that controls the clock and initiates communication. Each device on the bus has a unique 7-bit address (like a house number on a street). The master sends the address of the device it wants to talk to, and only that device responds.
This means you can connect many devices to the same two wires, as long as each has a different address. Common sensor addresses look like 0x76 (BME280), 0x44 (SHT31), 0x23 (BH1750), and 0x3C (SSD1306 OLED).
🔗I2C Address Scanner
When working with a new I2C device, the first thing to do is verify that the ESP32 can see it on the bus. This scanner sketch checks every possible address and reports which ones respond:
#include <Wire.h>
void setup() {
Serial.begin(115200);
delay(1000);
Wire.begin(); // Uses default SDA=21, SCL=22
Serial.println("Scanning for I2C devices...\n");
int devicesFound = 0;
for (byte address = 1; address < 127; address++) {
Wire.beginTransmission(address);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.printf("Device found at address 0x%02X\n", address);
devicesFound++;
}
}
if (devicesFound == 0) {
Serial.println("No I2C devices found. Check wiring!");
} else {
Serial.printf("\n%d device(s) found.\n", devicesFound);
}
}
void loop() {
// Nothing to do here
}Tip: If the scanner finds nothing, double-check your wiring. The most common issues are swapped SDA/SCL lines, missing pull-up resistors (most breakout boards include them), or a loose connection.
🔗Using Custom I2C Pins
If the default pins are not convenient, you can use any available GPIO pins for I2C:
#include <Wire.h>
#define CUSTOM_SDA 16
#define CUSTOM_SCL 17
void setup() {
Wire.begin(CUSTOM_SDA, CUSTOM_SCL);
// Now I2C uses GPIO 16 for SDA and GPIO 17 for SCL
}You can even use the second I2C bus if you need two separate buses (for example, to use two devices that have the same address):
TwoWire I2C_bus1 = TwoWire(0); // First I2C bus
TwoWire I2C_bus2 = TwoWire(1); // Second I2C bus
void setup() {
I2C_bus1.begin(21, 22); // Bus 0 on default pins
I2C_bus2.begin(16, 17); // Bus 1 on different pins
}🔗SPI (Serial Peripheral Interface)
SPI is faster than I2C and is used for devices that need to transfer a lot of data quickly -- displays (TFT LCDs), SD cards, and some high-speed sensors.
SPI uses four wires:
| Signal | Also Called | Default GPIO on ESP32 (VSPI) | Purpose |
|---|---|---|---|
| MOSI | SDI, DIN | GPIO 23 | Master Out, Slave In (data to device) |
| MISO | SDO, DOUT | GPIO 19 | Master In, Slave Out (data from device) |
| SCK | SCLK, CLK | GPIO 18 | Clock signal |
| CS | SS, CE | GPIO 5 (default) | Chip Select (choose which device to talk to) |
Note: The ESP32 has two usable SPI buses: VSPI (the default) and HSPI. The pins listed above are for VSPI. Like I2C, you can reassign SPI to different GPIO pins.
🔗How SPI Works
SPI is also a master-slave protocol, but instead of addresses, it uses a CS (Chip Select) line for each device. To talk to a device, the master pulls its CS line LOW. All other devices ignore the bus because their CS lines are HIGH.
This means if you have three SPI devices, you need three CS pins (one per device), but they all share the same MOSI, MISO, and SCK lines.
🔗SPI Example: Reading a Device
Most SPI devices have Arduino libraries that handle the protocol details. Here is the general pattern:
#include <SPI.h>
#define CS_PIN 5
void setup() {
Serial.begin(115200);
SPI.begin(); // Initialize SPI with default VSPI pins
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect the device
}
void readFromDevice() {
digitalWrite(CS_PIN, LOW); // Select the device
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
byte result = SPI.transfer(0x00); // Send a byte, receive a byte
SPI.endTransaction();
digitalWrite(CS_PIN, HIGH); // Deselect the device
Serial.printf("Received: 0x%02X\n", result);
}In practice, you will rarely write raw SPI code. Libraries like TFT_eSPI (for displays), SD.h (for SD cards), and sensor-specific libraries handle the SPI communication for you.
🔗UART (Universal Asynchronous Receiver-Transmitter)
UART is the simplest protocol and the one you have already been using: it is the same serial communication that connects your ESP32 to your computer via USB. UART uses two wires:
- TX (Transmit) -- sends data
- RX (Receive) -- receives data
The key rule for UART wiring is: TX connects to RX, and RX connects to TX. You cross the wires because one device's output (TX) needs to go to the other device's input (RX).
🔗ESP32 UART Ports
The ESP32 has three UART ports:
| UART Port | Default TX | Default RX | Notes |
|---|---|---|---|
| UART0 | GPIO 1 | GPIO 3 | Used for USB serial (Serial Monitor) -- do not use for other devices |
| UART1 | GPIO 10 | GPIO 9 | Pins often connected to internal flash -- reassign before using |
| UART2 | GPIO 17 | GPIO 16 | Free to use for peripherals |
Warning: UART0 (GPIO 1 and GPIO 3) is used by the USB serial connection. If you connect a device to these pins, you will lose your Serial Monitor output and may have trouble uploading code.
🔗UART Example: Reading a GPS Module
A common use of UART is connecting a GPS module. Here we use Serial2 (UART2) to read GPS data while keeping Serial (UART0) free for debugging:
#define GPS_TX 17 // ESP32 TX2 -> GPS RX
#define GPS_RX 16 // ESP32 RX2 -> GPS TX
void setup() {
Serial.begin(115200); // USB serial for debug output
Serial2.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX); // GPS at 9600 baud
delay(1000);
Serial.println("Waiting for GPS data...");
}
void loop() {
while (Serial2.available()) {
char c = Serial2.read();
Serial.print(c); // Forward GPS data to Serial Monitor
}
}The SERIAL_8N1 parameter means 8 data bits, no parity, 1 stop bit -- the most common UART configuration.
Tip: Like I2C and SPI, you can reassign UART pins to other GPIOs. This is one of the ESP32's great strengths -- pin flexibility. Just remember that GPIOs 34--39 are input-only and cannot be used as TX pins.
🔗OneWire
OneWire is a protocol that uses a single data wire (plus ground). It was designed by Dallas Semiconductor and is most commonly associated with the DS18B20 temperature sensor.
| Signal | Connection |
|---|---|
| Data | Any GPIO + 4.7k ohm pull-up resistor to 3.3V |
| GND | Ground |
| VCC | 3.3V (or parasitic power via data line) |
🔗How OneWire Works
Each OneWire device has a unique 64-bit address burned in at the factory. The master (ESP32) can discover all devices on the bus and talk to each one individually. This means you can connect multiple DS18B20 sensors to a single GPIO pin -- very handy for projects that need temperature readings at several points.
🔗OneWire Example: DS18B20 Temperature
#include <OneWire.h>
#include <DallasTemperature.h>
#define ONE_WIRE_PIN 4 // Data pin for OneWire bus
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature sensors(&oneWire);
void setup() {
Serial.begin(115200);
delay(1000);
sensors.begin();
Serial.printf("Found %d sensor(s) on the bus.\n", sensors.getDeviceCount());
}
void loop() {
sensors.requestTemperatures();
float tempC = sensors.getTempCByIndex(0); // Read first sensor
if (tempC == DEVICE_DISCONNECTED_C) {
Serial.println("Error: sensor disconnected!");
} else {
Serial.printf("Temperature: %.1f C\n", tempC);
}
delay(1000);
}You will need to install two libraries via the Arduino Library Manager: OneWire by Jim Studt and DallasTemperature by Miles Burton.
Tip: The 4.7k ohm pull-up resistor on the data line is essential. Without it, communication will be unreliable or fail entirely. If you are connecting only one sensor over a short wire (under 1 meter), some people get away without it, but it is always best practice to include it.
🔗Choosing the Right Protocol
Here is a practical decision guide:
| Situation | Use |
|---|---|
| Connecting a sensor (temperature, humidity, pressure, light) | I2C -- simplest wiring, most sensor breakout boards support it |
| Driving a TFT display or reading an SD card | SPI -- the speed is necessary |
| Connecting a GPS module, another microcontroller, or any device with TX/RX pins | UART |
| Connecting one or more DS18B20 temperature sensors | OneWire |
| You need many sensors and are running out of pins | I2C (many devices on 2 wires) or OneWire (many sensors on 1 wire) |
🔗Pin Flexibility on the ESP32
One of the ESP32's best features is that I2C, SPI, and UART can be assigned to almost any GPIO pin through a hardware feature called the GPIO matrix. This gives you a lot of flexibility when designing your wiring.
There are a few restrictions to keep in mind:
- GPIOs 34--39 are input-only. They cannot be used for output signals like SDA, SCL, MOSI, SCK, or TX.
- GPIOs 6--11 are connected to the onboard SPI flash memory on most boards. Do not use them.
- GPIO 0, 2, 5, 12, 15 have special boot functions. They work fine during normal operation, but pulling them to unexpected levels during boot can prevent the ESP32 from starting.
- Not all boards expose all pins. Always check your specific board's pinout diagram.
Tip: When in doubt, GPIOs 4, 13, 14, 16, 17, 18, 19, 21, 22, 23, 25, 26, 27, 32, and 33 are generally safe to use for any protocol.
🔗What's Next?
Now that you understand how the ESP32 communicates with peripherals over wires, it is time to go wireless. In the next article, we will connect the ESP32 to a WiFi network and make our first HTTP request.