Connecting a standard 16x2 parallel LCD to a microcontroller traditionally eats up six GPIO pins and requires a messy web of jumper wires. By adding a PCF8574 I2C backpack (usually pre-soldered or a $2 add-on), you reduce the wiring to just four lines: VCC, GND, SDA, and SCL. This guide covers the exact hardware specs, wiring, and Python code to get an lcd display to raspberry pi working reliably, specifically targeting the Raspberry Pi 5 architecture and the latest Raspberry Pi OS (Bookworm) environment.
Hardware Specs and Module Variants
Not all I2C backpacks are identical. The most common point of failure in LCD projects is buying a backpack with an unexpected I2C address or mismatched voltage logic. The Raspberry Pi 5 GPIO operates at 3.3V, but the HD44780 LCD controller and its backlight require 5V for proper brightness and contrast. Fortunately, the PCF8574 I/O expander accepts 3.3V logic HIGH signals from the Pi while being powered by the 5V rail.
| Component Variant | Default I2C Address | Operating Voltage | Key Characteristics & Gotchas |
|---|---|---|---|
| PCF8574T Backpack | 0x27 (39 decimal) |
5V (Logic tolerates 3.3V) | Most common. Address pins A0-A2 pulled HIGH. If you bridge them to GND, address shifts down to 0x20. |
| PCF8574AT Backpack | 0x3F (63 decimal) |
5V (Logic tolerates 3.3V) | Less common. Base address is higher. Often sold in multi-packs to allow multiple LCDs on one bus. |
| 1602A 5V LCD (Standard) | N/A (Parallel) | 5V | Requires 5V for backlight. Contrast pin (V0) needs ~0.5V. The backpack's blue trimpot handles this. |
| 1602A 3.3V LCD (Rare) | N/A (Parallel) | 3.3V | Specifically designed for 3.3V logic. Backlight is dimmer. Do not mix with standard 5V backpacks without level shifters. |
gpiozero/lgpio libraries rather than legacy Broadcom (BCM) hardcoding if you are writing custom pin-mapping scripts.
Pin Mapping and Wiring Steps
We will power the backpack from the Pi 5's 5V rail to ensure the LCD backlight turns on, while using the 3.3V I2C data lines for communication. The Pi 5 has onboard 1.8kΩ pull-up resistors on the I2C lines to 3.3V, which is perfectly within the PCF8574's logic HIGH threshold (VCC * 0.7, which is 3.5V, but in practice, the chip reliably reads 3.3V as HIGH on the Pi's I2C bus).
| Pi 5 Physical Pin | Pi 5 GPIO (BCM) | Function | PCF8574 Backpack Pin | Wire Color (Standard) |
|---|---|---|---|---|
| Pin 2 | N/A (5V Power) | 5V Power | VCC | Red |
| Pin 6 | N/A (Ground) | Ground | GND | Black |
| Pin 3 | GPIO 2 | I2C1 SDA | SDA | Blue |
| Pin 5 | GPIO 3 | I2C1 SCL | SCL | Yellow |
Wiring Procedure
- De-energize the Pi: Disconnect the USB-C power supply from the Raspberry Pi 5 before connecting jumper wires to the GPIO header.
- Connect Power: Plug the red jumper into Physical Pin 2 (5V) and the black jumper into Physical Pin 6 (GND). Connect the other ends to the VCC and GND pins on the I2C backpack.
- Connect Data Lines: Plug the blue jumper into Physical Pin 3 (SDA) and the yellow jumper into Physical Pin 5 (SCL). Connect them to the corresponding SDA and SCL pins on the backpack. Note: If your backpack labels them backwards, swap them. The Pi's I2C1 pins are fixed, but some cheap backpacks mislabel the silk screen.
- Adjust Contrast: Locate the small blue potentiometer (trimpot) on the back of the I2C backpack. Use a small Phillips screwdriver to turn it fully counter-clockwise. You will adjust this later once the code is running.
- Power Up: Reconnect the USB-C power supply and boot into Raspberry Pi OS.
Python Code for Raspberry Pi 5 (Bookworm OS)
Target Board Variant: Raspberry Pi 5 (8GB) running Raspberry Pi OS (Bookworm 64-bit).
Bookworm strictly enforces PEP 668, meaning you cannot use pip install globally without breaking system packages. Furthermore, the legacy smbus library is deprecated. We will use smbus2 inside a Python virtual environment. This code includes a custom, lightweight HD44780 driver class so you aren't reliant on abandoned third-party LCD libraries.
Step 1: Enable I2C and Setup Environment
Open the terminal and run sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot, then verify the bus with sudo i2cdetect -y 1. You should see 27 or 3f in the grid.
Next, set up the virtual environment:
mkdir ~/lcd_project && cd ~/lcd_project
python3 -m venv venv
source venv/bin/activate
pip install smbus2
Step 2: The Python Script
Save the following code as lcd_demo.py. It includes robust error handling for I2C bus failures.
import time
import sys
from smbus2 import SMBus
# --- CONFIGURATION ---
I2C_BUS = 1
# Change to 0x3F if you have a PCF8574AT backpack
I2C_ADDRESS = 0x27
# HD44780 Commands
LCD_CLEARDISPLAY = 0x01
LCD_RETURNHOME = 0x02
LCD_ENTRYMODESET = 0x04
LCD_DISPLAYCONTROL = 0x08
LCD_FUNCTIONSET = 0x20
LCD_SETDDRAMADDR = 0x80
# Flags for display entry mode
LCD_ENTRYLEFT = 0x02
LCD_ENTRYSHIFTDECREMENT = 0x00
# Flags for display on/off control
LCD_DISPLAYON = 0x04
LCD_CURSOROFF = 0x00
LCD_BLINKOFF = 0x00
# Flags for function set
LCD_4BITMODE = 0x00
LCD_2LINE = 0x08
LCD_5x8DOTS = 0x00
# Backpack Pin Mapping (Standard PCF8574 to HD44780)
RS = 0x01
RW = 0x02
EN = 0x04
BL = 0x08 # Backlight pin
D4 = 0x10
D5 = 0x20
D6 = 0x40
D7 = 0x80
class I2C_LCD:
def __init__(self, bus_num, addr):
self.bus = SMBus(bus_num)
self.addr = addr
self.backlight = BL
self.initialize()
def write_i2c_block(self, data):
try:
self.bus.write_byte(self.addr, data)
except OSError as e:
print(f"I2C Write Failed: {e}")
sys.exit(1)
def pulse_enable(self, data):
self.write_i2c_block(data | EN | self.backlight)
time.sleep(0.0005)
self.write_i2c_block((data & ~EN) | self.backlight)
time.sleep(0.0001)
def send_nibble(self, data, mode):
high = (data & 0xF0) | mode | self.backlight
low = ((data << 4) & 0xF0) | mode | self.backlight
self.write_i2c_block(high)
self.pulse_enable(high)
self.write_i2c_block(low)
self.pulse_enable(low)
def send_byte(self, data, mode):
self.send_nibble(data, mode)
self.send_nibble(data, mode)
def initialize(self):
# Wake up sequence for 4-bit mode
time.sleep(0.05)
self.write_i2c_block(0x30 | self.backlight)
time.sleep(0.005)
self.write_i2c_block(0x30 | self.backlight)
time.sleep(0.005)
self.write_i2c_block(0x30 | self.backlight)
time.sleep(0.001)
self.write_i2c_block(0x20 | self.backlight) # Set 4-bit mode
self.pulse_enable(0x20 | self.backlight)
# Function set: 2 lines, 5x8 dots
self.send_byte(LCD_FUNCTIONSET | LCD_4BITMODE | LCD_2LINE | LCD_5x8DOTS, 0)
# Display on, cursor off
self.send_byte(LCD_DISPLAYCONTROL | LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF, 0)
# Clear display
self.clear()
# Entry mode: left to right
self.send_byte(LCD_ENTRYMODESET | LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT, 0)
def clear(self):
self.send_byte(LCD_CLEARDISPLAY, 0)
time.sleep(0.002)
def set_cursor(self, col, row):
row_offsets = [0x00, 0x40, 0x14, 0x54]
if row > 1:
row = 1
self.send_byte(LCD_SETDDRAMADDR | (col + row_offsets[row]), 0)
def write_string(self, text):
for char in text:
self.send_byte(ord(char), RS)
if __name__ == "__main__":
try:
lcd = I2C_LCD(I2C_BUS, I2C_ADDRESS)
lcd.set_cursor(0, 0)
lcd.write_string("ElectricalFlux")
lcd.set_cursor(0, 1)
lcd.write_string("Pi 5 I2C Ready!")
print("Message sent to LCD. Press Ctrl+C to exit.")
while True:
time.sleep(1)
except FileNotFoundError as e:
print(f"Error: I2C bus not found. Did you enable I2C in raspi-config?\nDetails: {e}")
except OSError as e:
if e.errno == 121:
print("OSError: [Errno 121] Remote I/O error. Check wiring and I2C address.")
else:
print(f"Unexpected I2C Error: {e}")
except KeyboardInterrupt:
print("\nExiting...")
sys.exit(0)
Debugging: Fixing "OSError: [Errno 121] Remote I/O error"
When working with I2C on the Raspberry Pi, the most notorious failure is the OSError: [Errno 121] Remote I/O error. This error means the Pi's I2C controller attempted to communicate with the target address, but the device did not acknowledge (ACK) the request on the bus.
The First Three Things to Check
- Verify the I2C Address: Run
i2cdetect -y 1in the terminal. If the grid is empty, your wiring is wrong or the backpack is dead. If you see3finstead of27, change theI2C_ADDRESSvariable in the Python script to0x3F. - Check SDA/SCL Swap: Many cheap PCF8574 backpacks from overseas marketplaces have the SDA and SCL silk-screen labels swapped. Swap the blue and yellow wires on the Pi GPIO header and run
i2cdetectagain. - Verify Common Ground: If you are powering the LCD from a separate 5V bench supply (instead of the Pi's 5V pin), you must connect the ground of the bench supply to the ground of the Raspberry Pi. Without a common ground reference, the 3.3V logic signals will float, causing Errno 121.
Other Ranked Causes for Errno 121
- Cable Length/Capacitance: Standard Dupont wires longer than 30cm introduce too much capacitance for the I2C bus at 100kHz. Keep I2C runs under 20cm, or lower the bus speed to 10kHz in
/boot/firmware/config.txtby addingdtparam=i2c_baudrate=10000. - Missing Pull-ups: The Pi 5 has onboard pull-ups, but if you are using a custom HAT or a Pi Zero 2 W, you may need external 4.7kΩ pull-up resistors between SDA/SCL and 3.3V.
- Dead PCF8574 Chip: If the backlight turns on but
i2cdetectshows nothing, the I/O expander chip may have been fried by a previous 5V-to-GPIO short. Replace the backpack.
Extending and Simplifying the Build
How to Extend the Project
Once the LCD is rendering text, the natural next step is displaying live sensor data. Because I2C is a bus, you can wire a BME280 temperature/humidity sensor to the exact same SDA and SCL pins. The BME280 typically uses address 0x76 or 0x77, which won't conflict with the LCD's 0x27. You can update the Python script to read the sensor via the smbus2 library and push the formatted string to lcd.write_string() inside a while loop.
For networked projects, integrate the paho-mqtt library. Subscribe to an MQTT topic (e.g., home/lab/status) and update the LCD whenever a new payload arrives, turning your Pi into a dedicated smart-home status dashboard.
How to Simplify the Build
If soldering header pins to a raw 1602 LCD or dealing with Dupont wire spaghetti is a barrier, switch to a Grove I2C LCD module (like the Seeed Studio Grove - LCD RGB Backlight). These modules use a keyed 4-pin connector that prevents reversed wiring, feature an integrated I2C chip (often an STM32 or custom ASIC instead of the PCF8574), and allow software-controlled RGB backlight colors. They cost roughly $8-$12 (compared to $4 for the raw components) but eliminate the contrast trimpot adjustment and address-guessing entirely.
Alternatively, if you only need to display system stats (CPU temp, IP address) and don't want to write Python, install oled-stats or use a pre-configured Docker container like lcdproc, which handles the I2C daemonization and screen rendering natively in the background.






