Standard 16×2 LCD displays require up to 12 Arduino pins for a full parallel connection — a nightmare on pin-limited boards like the Nano or Pro Mini. The solution is Arduino LCD I2C wiring using the I2C protocol and a PCF8574 backpack adapter. With just 2 data pins (SDA and SCL), you get a fully functional display while freeing the remaining 10+ pins for sensors, motors, and other peripherals. This guide covers everything: the hardware adapter, I2C address discovery, library installation, complete working code, and fixes for every common problem beginners encounter.
Table of Contents
- 1. How I2C LCD Works: The PCF8574 Backpack
- 2. Wiring I2C LCD to Arduino (All Boards)
- 3. Finding the I2C Address: Address Scanner Sketch
- 4. Installing LiquidCrystal_I2C Library
- 5. Complete Code Examples
- 6. Advanced Features: Custom Characters and Scrolling
- 7. Running Multiple LCDs on One Bus
- FAQ
1. How I2C LCD Works: The PCF8574 Backpack
A standard HD44780-based 16×2 LCD has 16 pins and requires 6–12 GPIO lines from the Arduino for parallel communication. The I2C backpack module — built around the PCF8574 I/O expander IC — converts this to a 2-wire serial bus.
The PCF8574 is an 8-bit I/O port expander that communicates via I2C. Its 8 output pins are wired to the LCD’s data lines (D4–D7), RS, EN, and backlight control. From the Arduino’s perspective, you write a single byte to the PCF8574 over I2C, and the chip immediately puts those bits on its 8 output pins — exactly replicating what a direct parallel connection would do, just far more slowly (but fast enough for display updates).
Key specifications of the backpack module:
- I2C address: 0x27 (most common) or 0x3F (alternative PCF8574A variant)
- Three address pins (A0, A1, A2) allow 8 unique addresses on the same bus (0x20–0x27 or 0x38–0x3F)
- Built-in 5V backlight transistor with potentiometer for contrast
- Pull-up resistors already on most backpack boards (4.7kΩ on SDA and SCL)
The I2C bus runs at 100 kHz (standard mode) or 400 kHz (fast mode). For LCD updates, even 100 kHz is more than sufficient — a full 16×2 screen refresh takes less than 5 ms.
2. Wiring I2C LCD to Arduino (All Boards)
The beauty of I2C is that the wiring is identical regardless of which Arduino you use — only the physical pin numbers differ.
I2C LCD Backpack Pins → Arduino Connection:
- GND → Arduino GND
- VCC → Arduino 5V (most LCDs; check if yours is 3.3V)
- SDA → Arduino SDA pin
- SCL → Arduino SCL pin
SDA and SCL pin locations by board:
- Arduino Uno R3: SDA = A4 (pin 18), SCL = A5 (pin 19). Also exposed on the dedicated SDA/SCL headers near the USB port.
- Arduino Nano: SDA = A4, SCL = A5 (same as Uno, analog pin area)
- Arduino Mega 2560: SDA = Pin 20, SCL = Pin 21 (dedicated I2C pins near reset)
- Arduino Nano Every: SDA = A4, SCL = A5
- Arduino Nano 33 IoT: SDA = A4, SCL = A5 (3.3V logic — use 3.3V LCD or level shifter)
- Arduino Pro Mini 3.3V: SDA = A4, SCL = A5 (3.3V logic)
Important notes:
- The I2C bus requires pull-up resistors on SDA and SCL (typically 4.7kΩ to VCC). Most I2C LCD backpacks include these, but if you are connecting other I2C devices on the same bus, only one set of pull-ups is needed.
- For 3.3V Arduino boards (Nano 33 IoT, Pro Mini 3.3V), use a 3.3V-compatible LCD or an I2C level shifter. Most PCF8574 backpacks are 5V only.
- Common ground between Arduino and LCD is mandatory even if powered from separate supplies.
3. Finding the I2C Address: Address Scanner Sketch
Before writing any display code, you must find the I2C address of your specific backpack. Different manufacturers set different factory defaults. The I2C scanner sketch is your first tool:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("I2C Scanner — scanning addresses 0x01 to 0x7F...");
byte count = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.print("Device found at 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
count++;
}
}
if (count == 0) {
Serial.println("No I2C devices found. Check wiring.");
} else {
Serial.print(count);
Serial.println(" device(s) found.");
}
}
void loop() {}
Upload this, open Serial Monitor at 9600 baud. You will see output like:
Device found at 0x27
Note this address — you will use it in every display sketch. Common results:
0x27: PCF8574 with A0/A1/A2 all HIGH (most common default)0x3F: PCF8574A variant0x20: PCF8574 with A0/A1/A2 all LOW (jumpers shorted)- No device found: check wiring, check VCC (5V not 3.3V?), check that pull-ups exist
4. Installing LiquidCrystal_I2C Library
The standard library for I2C LCDs is LiquidCrystal_I2C by Frank de Brabander (updated by Marco Schwartz). Install via Arduino IDE Library Manager:
- In Arduino IDE: Sketch → Include Library → Manage Libraries
- Search for:
LiquidCrystal I2C - Select the entry by “Frank de Brabander” — install the latest version
- Click Install and wait for confirmation
If you are on Arduino IDE 2.x, the Library Manager is accessible from the left sidebar (book icon). The library installs to your sketchbook’s libraries folder.
Alternative: hd44780 library (John Rickman) — more robust, auto-detects I2C address, handles multiple bus speeds, and is more actively maintained. Install via Library Manager: search hd44780. Usage is slightly different but more powerful.
5. Complete Code Examples
Basic Hello World:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Parameters: I2C address, columns, rows
LiquidCrystal_I2C lcd(0x27, 16, 2);
void setup() {
lcd.init(); // Initialize LCD
lcd.backlight(); // Turn on backlight
lcd.setCursor(0, 0); // Column 0, Row 0 (top row)
lcd.print("Hello, World!");
lcd.setCursor(0, 1); // Column 0, Row 1 (bottom row)
lcd.print("Zbotic.in");
}
void loop() {}
Displaying sensor data with DHT11:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
#define DHTPIN 2
#define DHTTYPE DHT11
LiquidCrystal_I2C lcd(0x27, 16, 2);
DHT dht(DHTPIN, DHTTYPE);
void setup() {
lcd.init();
lcd.backlight();
dht.begin();
lcd.setCursor(0, 0);
lcd.print("Temp: Hum:");
}
void loop() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (!isnan(temp) && !isnan(hum)) {
lcd.setCursor(6, 0);
lcd.print(temp, 1);
lcd.print("C");
lcd.setCursor(12, 0);
lcd.print(hum, 0);
lcd.print("%");
}
delay(2000);
}
Stopwatch with millis():
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
unsigned long startTime;
bool running = false;
void setup() {
lcd.init();
lcd.backlight();
pinMode(7, INPUT_PULLUP); // Start/stop button
lcd.print("Press to start");
}
void loop() {
if (digitalRead(7) == LOW) {
if (!running) {
startTime = millis();
running = true;
lcd.clear();
lcd.print("Running...");
} else {
running = false;
lcd.setCursor(0, 1);
lcd.print("Stopped");
}
delay(300); // Debounce
}
if (running) {
unsigned long elapsed = millis() - startTime;
int secs = elapsed / 1000;
int ms = (elapsed % 1000) / 10;
lcd.setCursor(0, 0);
if (secs < 10) lcd.print("0");
lcd.print(secs);
lcd.print(".");
if (ms < 10) lcd.print("0");
lcd.print(ms);
lcd.print(" sec");
}
}
6. Advanced Features: Custom Characters and Scrolling
The HD44780 LCD controller can store up to 8 custom characters (5×8 pixel bitmaps) in CGRAM. This lets you display degree symbols, battery level bars, arrows, and other graphics not in the standard ASCII set.
// Define a custom degree symbol (°)
byte degreeChar[8] = {
B00110,
B01001,
B01001,
B00110,
B00000,
B00000,
B00000,
B00000
};
void setup() {
lcd.init();
lcd.backlight();
lcd.createChar(0, degreeChar); // Store at position 0
lcd.setCursor(0, 0);
lcd.print("Temp: 28");
lcd.write(0); // Print custom char at position 0
lcd.print("C");
}
Scrolling text for messages longer than 16 characters:
String message = "Welcome to Zbotic Electronics Store!";
void loop() {
for (int pos = 0; pos < message.length() - 15; pos++) {
lcd.setCursor(0, 0);
lcd.print(message.substring(pos, pos + 16));
delay(400);
}
delay(1000);
}
For a more polished scroll, use the built-in hardware scroll: lcd.scrollDisplayLeft() shifts all displayed content one position left. But beware — hardware scroll is global (affects all rows), while the software substring approach lets you scroll only specific rows.
7. Running Multiple LCDs on One Bus
The PCF8574 address pins (A0, A1, A2) let you connect up to 8 I2C LCDs to a single Arduino I2C bus. On the backpack PCB, look for three solder jumpers or through-hole pins labelled A0, A1, A2. By default, they are HIGH (open) giving address 0x27. Solder a bridge to pull any pin LOW, changing the address.
| A2 | A1 | A0 | Address |
|---|---|---|---|
| 0 | 0 | 0 | 0x20 |
| 0 | 0 | 1 | 0x21 |
| 1 | 1 | 1 | 0x27 |
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd1(0x27, 16, 2); // First display
LiquidCrystal_I2C lcd2(0x26, 16, 2); // Second display (A0 LOW)
void setup() {
lcd1.init(); lcd1.backlight();
lcd2.init(); lcd2.backlight();
lcd1.print("Display 1");
lcd2.print("Display 2");
}
void loop() {}
Frequently Asked Questions
My LCD shows blocks or garbled characters. What is wrong?
The contrast potentiometer on the backpack controls the visibility of characters. Turn it slowly with a small screwdriver until text appears clearly. Too high = invisible characters (white squares), too low = dark blocks with no text. This is the most common issue with new I2C LCD setups.
The I2C scanner finds no device even though the LCD is wired correctly.
Check that VCC is 5V (not 3.3V for a 5V module), SDA goes to A4 and SCL goes to A5 (not swapped), and GND is common. If pull-ups are missing, add 4.7kΩ resistors from SDA to 5V and SCL to 5V. On some cheap backpacks, the pull-ups are missing from the PCB.
Can I use I2C LCD with a 3.3V Arduino like the Nano 33 IoT?
Standard 5V PCF8574 backpacks are not 3.3V-compatible without a level shifter. The logic levels and VCC voltage must match. Use a BSS138-based bidirectional level shifter between the 3.3V Arduino I2C pins and the 5V backpack, or find a 3.3V-specific I2C LCD module.
What is the maximum I2C bus length?
For standard 100 kHz I2C with 4.7kΩ pull-ups and 5V, practical maximum is about 1 metre before capacitance causes signal degradation. For longer runs, use lower pull-up values (1kΩ), lower bus speed, or an I2C bus extender like the PCA9600.
How do I make the LCD display only update changed characters instead of clearing and reprinting?
Call lcd.clear() only when necessary — it takes 1.52 ms and causes a visible flicker. Instead, use lcd.setCursor(col, row) and print only the changed portion. For numeric values, print trailing spaces to overwrite old digits (e.g., lcd.print(" ") then reposition and print the new value).
Need display components for your Arduino project? Browse the complete range of Arduino boards, display modules, and sensors at Zbotic — fast shipping across India.
Add comment