The 16×2 LCD with an I2C backpack is the cheapest way to give a project a readable display, and most units sold in India are 5 V parts. On a 3.3 V ESP32 that needs one extra component: a small BSS138 level shifter between the board and the backpack.
On the 38-pin DevKitC the LCD takes 5V from the bottom-left pin, and the I2C lines come from 21 (SDA) and 22 (SCL) on the right header, each through one channel of the shifter. The sketch finds the backpack at 0x27 or 0x3F on its own, shows the uptime, and draws a bar along the second row using custom characters — five steps per cell, so it moves smoothly instead of jumping a block at a time.
Wiring
| ESP32 DevKitC (38-pin) | 16×2 LCD + I2C backpack | Wire | Notes |
|---|---|---|---|
| 5V | VCC | red | Bottom-left pin, printed 5V. The LCD needs 5 V for proper contrast. |
| GND | GND | black | Also to the level shifter’s GND. |
| 21 | SDA | blue | Via level shifter — Shifter LV side to 21, HV side to the LCD. Shifter supply: LV → 3V3, HV → 5V, GND → GND. |
| 22 | SCL | yellow | Via level shifter — Second channel of the same shifter. |
5 V (3.3 V versions exist) part, 3.3 V board. Most of these LCDs need 5 V for good contrast, and the backpack pulls SDA/SCL up to its own VCC. On ESP32 or Pico, put a BSS138 level shifter between the board and the backpack instead of wiring SDA/SCL directly.
Board quirks
- Why the shifter? The PCF8574 backpack has pull-up resistors from SDA and SCL to its own VCC. At 5 V, wiring it straight to 21/22 holds the ESP32’s pins at 5 V whenever the bus is idle — above what they are rated for. The BSS138 shifter keeps the ESP32 side at 3.3 V and the LCD side at 5 V.
- On the 38-pin board the supply pins for the shifter are far apart: 3V3 is the top-left pin, 5V the bottom-left one, and 21/22 are on the right header with TX and RX between them. Wire the shifter on the breadboard next to the right header and run the two power wires across.
- The sketch tries 0x27 (PCF8574T backpacks) and 0x3F (PCF8574AT) and uses whichever answers, so you don’t need to edit the address.
- Blank screen with a lit backlight, or a row of solid boxes, is almost always contrast: turn the blue potentiometer on the backpack slowly until the text appears.
- The LiquidCrystal I2C library is marked for AVR only; on the ESP32 it compiles with a warning and works.
Code
// 16×2 I2C LCD (PCF8574 backpack) on an ESP32 DevKitC (38-pin), through a level shifter.
// LCD VCC → 5V, GND → GND, SDA → shifter → 21, SCL → shifter → 22 (shifter LV = 3V3, HV = 5V).
// Finds the backpack at 0x27 or 0x3F by itself, shows uptime, and draws a smooth bar
// with custom characters (5 steps per cell).
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C *lcd = nullptr;
// Custom characters 0–4: a cell filled 1 to 5 columns from the left.
void makeBarChars() {
for (uint8_t n = 0; n < 5; n++) {
uint8_t row = 0xFF << (4 - n) & 0x1F; // n+1 columns lit
uint8_t glyph[8];
for (auto &r : glyph) r = row;
lcd->createChar(n, glyph);
}
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA, SCL
uint8_t addr = 0;
for (uint8_t a : {0x27, 0x3F}) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) { addr = a; break; }
}
if (!addr) {
Serial.println("No LCD backpack at 0x27 or 0x3F — check the level shifter's 3V3/5V/GND and SDA=21, SCL=22");
while (true) delay(1000);
}
lcd = new LiquidCrystal_I2C(addr, 16, 2);
lcd->init();
lcd->backlight();
makeBarChars();
Serial.printf("LCD found at 0x%02X\n", addr);
}
void loop() {
static uint32_t last = 0;
if (millis() - last < 250) return;
last = millis();
uint32_t ms = millis();
uint32_t s = ms / 1000;
lcd->setCursor(0, 0);
lcd->printf("Up %02lu:%02lu:%02lu ", (unsigned long)(s / 3600), (unsigned long)(s / 60 % 60), (unsigned long)(s % 60));
// Second row: progress through the current minute, 16 cells × 5 columns = 80 steps.
uint32_t steps = (ms % 60000UL) * 80 / 60000UL;
lcd->setCursor(0, 1);
for (uint8_t cell = 0; cell < 16; cell++) {
int lit = int(steps) - cell * 5;
if (lit >= 5) lcd->write(4);
else if (lit > 0) lcd->write(lit - 1);
else lcd->write(' ');
}
if (s % 10 == 0 && ms % 1000 < 250) Serial.printf("Uptime %lus\n", (unsigned long)s);
}// 16×2 I2C LCD (PCF8574 backpack) on an ESP32 DevKitC (38-pin), through a level shifter.
// LCD VCC → 5V, GND → GND, SDA → shifter → 21, SCL → shifter → 22 (shifter LV = 3V3, HV = 5V).
// Finds the backpack at 0x27 or 0x3F by itself, shows uptime, and draws a smooth bar
// with custom characters (5 steps per cell).
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C *lcd = nullptr;
// Custom characters 0–4: a cell filled 1 to 5 columns from the left.
void makeBarChars() {
for (uint8_t n = 0; n < 5; n++) {
uint8_t row = 0xFF << (4 - n) & 0x1F; // n+1 columns lit
uint8_t glyph[8];
for (auto &r : glyph) r = row;
lcd->createChar(n, glyph);
}
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22); // SDA, SCL
uint8_t addr = 0;
for (uint8_t a : {0x27, 0x3F}) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) { addr = a; break; }
}
if (!addr) {
Serial.println("No LCD backpack at 0x27 or 0x3F — check the level shifter's 3V3/5V/GND and SDA=21, SCL=22");
while (true) delay(1000);
}
lcd = new LiquidCrystal_I2C(addr, 16, 2);
lcd->init();
lcd->backlight();
makeBarChars();
Serial.printf("LCD found at 0x%02X\n", addr);
}
void loop() {
static uint32_t last = 0;
if (millis() - last < 250) return;
last = millis();
uint32_t ms = millis();
uint32_t s = ms / 1000;
lcd->setCursor(0, 0);
lcd->printf("Up %02lu:%02lu:%02lu ", (unsigned long)(s / 3600), (unsigned long)(s / 60 % 60), (unsigned long)(s % 60));
// Second row: progress through the current minute, 16 cells × 5 columns = 80 steps.
uint32_t steps = (ms % 60000UL) * 80 / 60000UL;
lcd->setCursor(0, 1);
for (uint8_t cell = 0; cell < 16; cell++) {
int lit = int(steps) - cell * 5;
if (lit >= 5) lcd->write(4);
else if (lit > 0) lcd->write(lit - 1);
else lcd->write(' ');
}
if (s % 10 == 0 && ms % 1000 < 250) Serial.printf("Uptime %lus\n", (unsigned long)s);
}Library: LiquidCrystal I2C (Frank de Brabander) — compiled with LiquidCrystal I2C 1.1.2.
Expected serial output
LCD found at 0x27
Uptime 10s
Uptime 20s
Uptime 30sLCD found at 0x27 Uptime 10s Uptime 20s Uptime 30s
Common errors
“No LCD backpack at 0x27 or 0x3F”
Usually the shifter: it needs LV on 3V3, HV on 5V and a common GND, or it passes nothing. Then check SDA → 21 and SCL → 22 (clones may print G21/G22) and that the LCD has 5 V.
Backlight on, no text
Turn the contrast potentiometer on the back. At 3.3 V instead of 5 V the text may never appear — VCC must be on 5V.
Garbled characters after a while
Loose jumper or a long cable. Keep the I2C wires under about 30 cm; the shifter adds its own 10 kΩ pull-ups on both sides, which is enough at that length.