A 20×4 character LCD shows twice as much as the common 16×2 — enough for four sensor values or a small menu. With a PCF8574 I2C backpack soldered on, it needs four wires.
On the Uno everything runs at 5 V, so the backpack connects straight to 5V, GND and the SDA/SCL header (the same signals as A4/A5). Two things trip people up: the backpack’s address (0x27 or 0x3F, depending on the chip maker) and the odd row order. The sketch handles the first by trying both addresses. For the second, it always places text with setCursor().
It labels four lines, then refreshes a live analog reading and the uptime twice a second by overwriting only the changing digits.
Wiring
| Arduino Uno R3 | 20×4 LCD + I2C backpack | Wire | Notes |
|---|---|---|---|
| GND | GND | black | |
| 5V | VCC | red | Backlight and logic together draw about 30–60 mA — fine from the Uno’s 5V pin. |
| SDA | SDA | blue | Same signal as A4 |
| SCL | SCL | yellow | Same signal as A5 |
5 V part, 5 V board. The LCD needs 5 V for proper contrast, and the backpack pulls SDA/SCL up to 5 V. On ESP32 or Pico, put a BSS138 level shifter between the board and the backpack instead of wiring SDA/SCL straight to 3.3 V pins.
Board quirks
- The 20x4 shares the 16x2’s controller, and its memory makes text that runs past column 20 on row 0 continue on row 2. Always place text with
setCursor(col, row). - Rewriting a value with padding spaces (as the sketch does) avoids the flicker of
clear(), which also takes about 2 ms. - The library and a 20x4 buffer use very little RAM, so this display is a good fit for the Uno when an OLED’s 1 KB buffer won’t fit.
- Everything is 5 V, so no level shifter. SDA/SCL and A4/A5 are the same pins — use one pair.
Code
// 20x4 LCD with a PCF8574 I2C backpack on an Arduino Uno R3.
// GND → GND, VCC → 5V, SDA → SDA (A4), SCL → SCL (A5).
// Finds the backpack at 0x27 or 0x3F by itself.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
uint8_t findLcd() {
const uint8_t addrs[] = {0x27, 0x3F};
for (uint8_t a : addrs) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) return a;
}
return 0;
}
LiquidCrystal_I2C* lcd;
void setup() {
Serial.begin(9600);
Wire.begin();
uint8_t addr = findLcd();
if (!addr) {
Serial.println(F("No LCD backpack at 0x27 or 0x3F - check SDA/SCL"));
for (;;) {}
}
static LiquidCrystal_I2C display(addr, 20, 4);
lcd = &display;
lcd->init();
lcd->backlight();
// Rows must be set with setCursor: text that runs past column 20
// continues on row 3, not row 2 (that's how the controller's memory is laid out).
lcd->setCursor(0, 0);
lcd->print(F("Arduino Uno + 20x4"));
lcd->setCursor(0, 1);
lcd->print(F("I2C at 0x"));
lcd->print(addr, HEX);
lcd->setCursor(0, 2);
lcd->print(F("A0 raw:"));
lcd->setCursor(0, 3);
lcd->print(F("Uptime:"));
Serial.print(F("LCD ready at 0x"));
Serial.println(addr, HEX);
}
void loop() {
char buf[8];
lcd->setCursor(8, 2);
snprintf(buf, sizeof(buf), "%-5d", analogRead(A0)); // pad with spaces, no flicker
lcd->print(buf);
lcd->setCursor(8, 3);
lcd->print(millis() / 1000);
lcd->print(F(" s "));
delay(500);
}// 20x4 LCD with a PCF8574 I2C backpack on an Arduino Uno R3.
// GND → GND, VCC → 5V, SDA → SDA (A4), SCL → SCL (A5).
// Finds the backpack at 0x27 or 0x3F by itself.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
uint8_t findLcd() {
const uint8_t addrs[] = {0x27, 0x3F};
for (uint8_t a : addrs) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) return a;
}
return 0;
}
LiquidCrystal_I2C* lcd;
void setup() {
Serial.begin(9600);
Wire.begin();
uint8_t addr = findLcd();
if (!addr) {
Serial.println(F("No LCD backpack at 0x27 or 0x3F - check SDA/SCL"));
for (;;) {}
}
static LiquidCrystal_I2C display(addr, 20, 4);
lcd = &display;
lcd->init();
lcd->backlight();
// Rows must be set with setCursor: text that runs past column 20
// continues on row 3, not row 2 (that's how the controller's memory is laid out).
lcd->setCursor(0, 0);
lcd->print(F("Arduino Uno + 20x4"));
lcd->setCursor(0, 1);
lcd->print(F("I2C at 0x"));
lcd->print(addr, HEX);
lcd->setCursor(0, 2);
lcd->print(F("A0 raw:"));
lcd->setCursor(0, 3);
lcd->print(F("Uptime:"));
Serial.print(F("LCD ready at 0x"));
Serial.println(addr, HEX);
}
void loop() {
char buf[8];
lcd->setCursor(8, 2);
snprintf(buf, sizeof(buf), "%-5d", analogRead(A0)); // pad with spaces, no flicker
lcd->print(buf);
lcd->setCursor(8, 3);
lcd->print(millis() / 1000);
lcd->print(F(" s "));
delay(500);
}Library: LiquidCrystal I2C (Frank de Brabander) — compiled with LiquidCrystal I2C 1.1.2.
Expected serial output
LCD ready at 0x27LCD ready at 0x27
Common errors
Backlight on, but only white blocks or nothing
Turn the blue contrast trimmer on the backpack slowly with a screwdriver. See LCD shows blocks or blank.
Only the first letter of each word appears
A library mismatch. Install “LiquidCrystal I2C” by Frank de Brabander — see this fix.
“No LCD backpack at 0x27 or 0x3F”
SDA/SCL swapped or loose. Run the I2C scanner; some backpacks use 0x20–0x26.