Hooking up a raspberry pi with lcd display is a foundational embedded project, but the sheer number of GPIO pins required for a raw parallel LCD makes it impractical for modern builds. The direct answer: use a 16x2 or 20x4 character LCD based on the HD44780 controller, paired with a PCF8574 I2C backpack. This reduces your wiring from 16 pins down to just 4 (VCC, GND, SDA, SCL) and frees up your Pi's GPIO for actual sensors.
This guide targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS (Bookworm or newer), though the I2C bus 1 pinout remains identical for the Pi 4 Model B and Pi 3 B+. We will cover the critical 3.3V logic trap, provide a production-ready Python script with error handling, and debug the most common I2C failures.
Hardware Specifications and Pin Mapping
Before grabbing jumper wires, you must address the most common hardware killer in Pi LCD projects: logic level mismatch. The Raspberry Pi's I2C pins (GPIO 2 and GPIO 3) operate at 3.3V. Standard PCF8574 I2C backpacks are often designed for 5V Arduino logic. Feeding 5V back into the Pi's SDA/SCL lines can degrade or destroy the Pi's GPIO bank over time. Always source a 3.3V I2C LCD module or use a bidirectional logic level shifter (like the BSS138).
I2C LCD Module Spec Sheet (3.3V Variant)
| Parameter | Specification / Value | Notes for Pi 5 Integration |
|---|---|---|
| Controller IC | HD44780 (or compatible SPLC780D) | Standard 4-bit/8-bit parallel character matrix. |
| I2C Expander | PCF8574T or PCF8574AT | Determines base I2C address (0x27 vs 0x3F). |
| Operating Voltage | 3.3V DC (Logic & Backlight) | Do NOT use 5V modules without a level shifter. |
| I2C Clock Speed | 100 kHz (Standard Mode) | Pi 5 defaults to 100kHz; safe for long wire runs. |
| Default Address | 0x27 (PCF8574T) or 0x3F (PCF8574AT) | Verify with i2cdetect -y 1 before coding. |
| Contrast Control | 10K Ohm Trimpot (on backpack) | Requires physical adjustment with a small Phillips driver. |
Raspberry Pi 5 to PCF8574 Pin Mapping
The Pi's 40-pin header places the I2C bus on the top right pins. Use the physical pin numbers, not the BCM GPIO numbers, when wiring.
| Pi 5 Physical Pin | BCM GPIO | Function | Connect to LCD Backpack |
|---|---|---|---|
| Pin 1 | 3V3 Power | 3.3V DC Output | VCC |
| Pin 6 | GND | Ground Reference | GND |
| Pin 3 | GPIO 2 | I2C SDA (Data) | SDA |
| Pin 5 | GPIO 3 | I2C SCL (Clock) | SCL |
Step-by-Step Wiring and OS Configuration
With the hardware mapped, configure the Raspberry Pi OS to enable the I2C bus and install the necessary Python libraries. We use RPLCD, the modern standard for character LCDs in Python, which wraps the smbus2 library.
- Enable I2C Interface: Open the terminal and run
sudo raspi-config. Navigate to Interface Options > I2C and select Yes to enable it. Reboot the Pi (sudo reboot). - Install I2C Tools: Run
sudo apt update && sudo apt install i2c-tools python3-smbus -y. This gives you the command-line utilities to scan the bus. - Verify Hardware Connection: Run
i2cdetect -y 1. You should see a grid output with27or3fhighlighted. If the grid is empty, check your wiring. - Install Python Library: Create a virtual environment (recommended for Pi OS Bookworm) and install the library:
python3 -m venv ~/lcd_envsource ~/lcd_env/bin/activatepip install RPLCD
Complete Python Control Code
Below is the complete, compilable Python script. It explicitly defines the I2C port and address, initializes the display, and includes robust error handling to catch the specific hardware and bus errors that plague I2C projects. For full API details, refer to the RPLCD official documentation or the RPLCD GitHub repository.
import time
import sys
from RPLCD.i2c import CharLCD
# --- PIN & HARDWARE DEFINITIONS ---
# Target: Raspberry Pi 5 / 4 (I2C Bus 1)
# Module: 16x2 HD44780 with PCF8574 I2C Backpack
I2C_PORT = 1
I2C_ADDRESS = 0x27 # Change to 0x3F if using PCF8574AT variant
def initialize_lcd():
try:
lcd = CharLCD(i2c_expander='PCF8574',
address=I2C_ADDRESS,
port=I2C_PORT,
cols=16,
rows=2,
dotsize=8,
charmap='A02',
auto_linebreaks=True,
backlight_enabled=True)
return lcd
except FileNotFoundError:
print("CRITICAL: I2C bus /dev/i2c-1 not found. Is I2C enabled in raspi-config?")
sys.exit(1)
except OSError as e:
if e.errno == 121:
print(f"CRITICAL: {e} - Remote I/O error. Check SDA/SCL wiring and pull-ups.")
else:
print(f"CRITICAL: Unexpected OS Error: {e}")
sys.exit(1)
def main():
lcd = initialize_lcd()
try:
lcd.clear()
lcd.cursor_pos = (0, 0)
lcd.write_string("Raspberry Pi 5")
lcd.cursor_pos = (1, 0)
lcd.write_string("LCD Online...")
# Main loop to keep script alive and update dynamic data
counter = 0
while True:
time.sleep(1)
counter += 1
lcd.cursor_pos = (1, 0)
lcd.write_string(f"Uptime: {counter}s ")
except KeyboardInterrupt:
print("\nExiting gracefully...")
lcd.clear()
lcd.backlight_enabled = False
sys.exit(0)
except Exception as e:
print(f"CRITICAL: Unhandled runtime exception: {e}")
lcd.clear()
sys.exit(1)
if __name__ == "__main__":
main()
Debugging: Fixing I2C Errors and Blank Screens
When working with I2C on the Raspberry Pi hardware interfaces, you will inevitably encounter bus errors. The most notorious is the Remote I/O error.
The Exact Error String
OSError: [Errno 121] Remote I/O error
This error occurs when the Pi's I2C controller attempts to send data to the target address (e.g., 0x27) but receives no acknowledgment (ACK) on the bus. The Pi assumes the device is dead or disconnected.
The First Three Things to Check When It Fails
Before rewriting code or replacing the Pi, execute this exact diagnostic sequence:
- Run the Bus Scan: Execute
i2cdetect -y 1in the terminal. If your address (27 or 3f) shows up as--or the grid is entirely empty, the Pi cannot see the hardware. This is a physical wiring or power issue, not a code issue. - Verify SDA/SCL Swap: The most common physical mistake is swapping GPIO 2 (SDA) and GPIO 3 (SCL). I2C is strictly directional regarding clock and data. Swap the wires on pins 3 and 5 and re-run the bus scan.
- Check the Address Mismatch: PCF8574 chips come in two variants. The PCF8574T defaults to
0x27. The PCF8574AT defaults to0x3F. Look at the tiny text printed on the black IC chip on the backpack. If it says "AT", changeI2C_ADDRESS = 0x27to0x3Fin the Python script.
Ranked Causes for Blank Screens (No Error Thrown)
If the script runs without throwing an OSError, but the screen remains blank or shows only solid blocks:
- Cause 1 (90%): Contrast trimpot is misadjusted. Use a small screwdriver to turn the blue potentiometer on the back of the I2C backpack until characters appear.
- Cause 2 (8%): Insufficient current on the 3.3V rail. The Pi's 3.3V pin can supply roughly 50mA. If the LCD backlight draws more, the voltage sags, resetting the HD44780 controller. Power the LCD VCC from the 5V pin (Pin 2) only if you are using a logic level shifter for the SDA/SCL lines.
- Cause 3 (2%): Dead backlight LED. The characters are actually rendering, but without illumination, they are invisible. Shine a flashlight at an angle across the screen to verify.
Extending and Simplifying the Build
Once you have the baseline I2C communication working, you can adapt the project to fit your specific enclosure or data-logging needs.
How to Simplify the Build
If jumper wires and breadboards are causing intermittent Errno 121 errors due to loose connections, abandon the wire approach entirely. Purchase an I2C GPIO HAT with an integrated 16x2 LCD (such as those made by Pimoroni or Adafruit). These plug directly into the 40-pin header, eliminate wiring faults, and often include onboard tactile buttons for menu navigation, shifting your project from a simple display to a full standalone interface.
How to Extend the Build
To turn this into a functional environmental monitor, wire a DHT22 temperature and humidity sensor to a free GPIO pin (e.g., GPIO 17). Modify the Python while True loop to poll the DHT22 using the adafruit-circuitpython-dht library, and format the output to the LCD:
# Extension snippet for DHT22 integration
import adafruit_dht
import board
dht_device = adafruit_dht.DHT22(board.D17)
# Inside your main loop:
try:
temp_c = dht_device.temperature
humidity = dht_device.humidity
lcd.cursor_pos = (1, 0)
lcd.write_string(f"{temp_c:.1f}C {humidity:.0f}%")
except RuntimeError:
# DHT22 often throws read errors due to Pi OS timing jitter
pass
By combining the robust error handling of the I2C LCD script with sensor polling, you create a resilient dashboard that won't crash your entire Python environment the moment a sensor read times out or an I2C bus glitch occurs.






