
Arduino Uno connected to 16x2 LCD display with I2C module
Introduction
LCD (Liquid Crystal Display) screens are among the most popular output devices for Arduino projects. Whether you're building a temperature monitor, digital clock, or data logger, understanding how to control LCD displays is essential for any Arduino enthusiast.
In this comprehensive guide, we'll cover:
- Basic LCD connections and wiring
- I2C LCD modules for simplified connections
- Programming fundamentals and library usage
- Creating custom characters and symbols
- Scrolling text and advanced display techniques
- Practical projects and applications
What You'll Need
Arduino Uno/Nano, 16x2 LCD display, I2C LCD backpack (optional), jumper wires, breadboard, and potentiometer for contrast adjustment.
Understanding LCD Displays
Common LCD Types
LCD Pin Configuration
Standard HD44780-based LCDs have 16 pins:
Pin | Name | Function | Arduino Connection |
---|---|---|---|
1 | VSS | Ground | GND |
2 | VDD | +5V Power | 5V |
3 | V0 | Contrast | Potentiometer |
4 | RS | Register Select | Digital Pin 12 |
5 | Enable | Enable | Digital Pin 11 |
6 | D0 | Data Bit 0 | Not Used |
7 | D1 | Data Bit 1 | Not Used |
8 | D2 | Data Bit 2 | Not Used |
9 | D3 | Data Bit 3 | Not Used |
10 | D4 | Data Bit 4 | Digital Pin 5 |
11 | D5 | Data Bit 5 | Digital Pin 4 |
12 | D6 | Data Bit 6 | Digital Pin 3 |
13 | D7 | Data Bit 7 | Digital Pin 2 |
14 | A | Backlight Anode | 5V |
15 | K | Backlight Cathode | GND |
Basic LCD Connection
Standard Parallel Connection
Connect the LCD directly to Arduino using parallel interface:
// Include the LCD library
#include <LiquidCrystal.h>
// Initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
void setup() {
// Set up the LCD's number of columns and rows:
lcd.begin(16, 2);
// Print a message to the LCD
lcd.print("Hello, World!");
}
void loop() {
// Set the cursor to column 0, line 1
// (note: line 1 is the second row, since counting begins with 0):
lcd.setCursor(0, 1);
// Print the number of seconds since reset:
lcd.print(millis() / 1000);
}
Connection Tips
Use a 10kΩ potentiometer for contrast adjustment (pin 3). Incorrect contrast will make the display appear blank or show only blocks. Test different contrast levels if text isn't visible.
I2C LCD Module
I2C LCD modules significantly simplify connections by reducing the required pins from 6 to just 2 (SDA and SCL):
I2C Wiring
/*
Arduino Uno I2C Pins:
- SDA: A4
- SCL: A5
- VCC: 5V
- GND: GND
Arduino Nano/Pro Mini:
- SDA: A4
- SCL: A5
Arduino Mega:
- SDA: Pin 20
- SCL: Pin 21
*/
I2C LCD Code
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// Set the LCD address to 0x27 for a 16 chars and 2 line display
LiquidCrystal_I2C lcd(0x27, 16, 2);
void setup() {
// Initialize the LCD
lcd.init();
// Turn on the blacklight
lcd.backlight();
// Print a message
lcd.setCursor(0, 0);
lcd.print("I2C LCD Ready!");
}
void loop() {
// Display counter
lcd.setCursor(0, 1);
lcd.print("Count: ");
lcd.print(millis() / 1000);
delay(100);
}
Finding I2C Address
If your LCD doesn't work, the I2C address might be different. Use this scanner:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("\nI2C Scanner");
}
void loop() {
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("I2C device found at address 0x");
if (address < 16)
Serial.print("0");
Serial.print(address, HEX);
Serial.println(" !");
nDevices++;
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
else
Serial.println("done\n");
delay(5000);
}
Advanced LCD Programming
Custom Characters
Create custom symbols and characters for your displays:
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
// Custom character arrays (5x8 pixels each)
byte heart[8] = {
0b00000,
0b01010,
0b11111,
0b11111,
0b11111,
0b01110,
0b00100,
0b00000
};
byte thermometer[8] = {
0b00100,
0b01010,
0b01010,
0b01110,
0b11111,
0b11111,
0b01110,
0b00000
};
byte degree[8] = {
0b00110,
0b01001,
0b01001,
0b00110,
0b00000,
0b00000,
0b00000,
0b00000
};
void setup() {
lcd.begin(16, 2);
// Create custom characters
lcd.createChar(0, heart);
lcd.createChar(1, thermometer);
lcd.createChar(2, degree);
// Display custom characters
lcd.setCursor(0, 0);
lcd.write(byte(0)); // Heart
lcd.print(" Arduino LCD");
lcd.setCursor(0, 1);
lcd.write(byte(1)); // Thermometer
lcd.print(" 25");
lcd.write(byte(2)); // Degree symbol
lcd.print("C");
}
void loop() {
// Blink heart
lcd.setCursor(0, 0);
lcd.write(byte(0));
delay(500);
lcd.setCursor(0, 0);
lcd.print(" ");
delay(500);
}
Scrolling Text
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
String message = "Welcome to itanveer.tech - Your Arduino Learning Hub! ";
int messageLength = message.length();
void setup() {
lcd.begin(16, 2);
lcd.print("Scrolling Text:");
}
void loop() {
// Horizontal scrolling
for (int position = 0; position < messageLength; position++) {
lcd.setCursor(0, 1);
// Clear the line
lcd.print(" ");
lcd.setCursor(0, 1);
// Print substring
if (position + 16 <= messageLength) {
lcd.print(message.substring(position, position + 16));
} else {
// Handle text wrapping
String displayText = message.substring(position) +
message.substring(0, 16 - (messageLength - position));
lcd.print(displayText);
}
delay(300);
}
}
Practical Projects
1. Digital Clock with LCD
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
unsigned long previousMillis = 0;
unsigned long seconds = 0;
unsigned long minutes = 0;
unsigned long hours = 0;
void setup() {
lcd.begin(16, 2);
lcd.setCursor(0, 0);
lcd.print("Digital Clock");
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 1000) {
previousMillis = currentMillis;
seconds++;
if (seconds >= 60) {
seconds = 0;
minutes++;
if (minutes >= 60) {
minutes = 0;
hours++;
if (hours >= 24) {
hours = 0;
}
}
}
// Display time
lcd.setCursor(0, 1);
// Format with leading zeros
if (hours < 10) lcd.print("0");
lcd.print(hours);
lcd.print(":");
if (minutes < 10) lcd.print("0");
lcd.print(minutes);
lcd.print(":");
if (seconds < 10) lcd.print("0");
lcd.print(seconds);
}
}
2. Temperature Monitor
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
const int tempSensorPin = A0;
// Custom degree symbol
byte degree[8] = {
0b00110,
0b01001,
0b01001,
0b00110,
0b00000,
0b00000,
0b00000,
0b00000
};
void setup() {
lcd.begin(16, 2);
lcd.createChar(0, degree);
lcd.setCursor(0, 0);
lcd.print("Temp Monitor");
}
void loop() {
// Read temperature sensor (LM35)
int sensorValue = analogRead(tempSensorPin);
float voltage = sensorValue * (5.0 / 1023.0);
float temperatureC = voltage * 100; // LM35: 10mV/°C
float temperatureF = (temperatureC * 9.0 / 5.0) + 32.0;
// Display temperature
lcd.setCursor(0, 1);
lcd.print("T: ");
lcd.print(temperatureC, 1);
lcd.write(byte(0)); // Degree symbol
lcd.print("C ");
lcd.print(temperatureF, 1);
lcd.write(byte(0)); // Degree symbol
lcd.print("F");
delay(1000);
}
3. Data Logger Display
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
const int sensorPin = A0;
float sensorReadings[10]; // Store last 10 readings
int readingIndex = 0;
bool arrayFilled = false;
void setup() {
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Data Logger");
// Initialize array
for (int i = 0; i < 10; i++) {
sensorReadings[i] = 0;
}
}
void loop() {
// Read sensor
float sensorValue = analogRead(sensorPin) * (5.0 / 1023.0);
// Store reading
sensorReadings[readingIndex] = sensorValue;
readingIndex++;
if (readingIndex >= 10) {
readingIndex = 0;
arrayFilled = true;
}
// Calculate average
float average = calculateAverage();
// Display current and average values
lcd.setCursor(0, 1);
lcd.print("Cur:");
lcd.print(sensorValue, 2);
lcd.print("V");
lcd.setCursor(8, 1);
lcd.print("Avg:");
lcd.print(average, 2);
lcd.print("V");
delay(1000);
}
float calculateAverage() {
float sum = 0;
int count = arrayFilled ? 10 : readingIndex;
for (int i = 0; i < count; i++) {
sum += sensorReadings[i];
}
return sum / count;
}
Pro Tips for LCD Projects
- Use non-blocking delays for responsive interfaces
- Clear specific areas instead of the entire display for better performance
- Create a menu system with button navigation
- Use EEPROM to store settings between power cycles
Troubleshooting Common Issues
Problem | Possible Cause | Solution |
---|---|---|
Blank display | Wrong contrast setting | Adjust contrast potentiometer |
Garbled text | Loose connections | Check all wiring connections |
No backlight | Backlight pins not connected | Connect pins 15 (A) and 16 (K) |
I2C not working | Wrong address | Use I2C scanner to find correct address |
Advanced Features and Libraries
Menu Systems
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
const int buttonUp = 7;
const int buttonDown = 8;
const int buttonSelect = 9;
int menuIndex = 0;
const int menuItems = 4;
String menu[] = {"1.Temperature", "2.Humidity", "3.Pressure", "4.Settings"};
void setup() {
lcd.begin(16, 2);
pinMode(buttonUp, INPUT_PULLUP);
pinMode(buttonDown, INPUT_PULLUP);
pinMode(buttonSelect, INPUT_PULLUP);
displayMenu();
}
void loop() {
if (digitalRead(buttonUp) == LOW) {
menuIndex = (menuIndex > 0) ? menuIndex - 1 : menuItems - 1;
displayMenu();
delay(200); // Simple debounce
}
if (digitalRead(buttonDown) == LOW) {
menuIndex = (menuIndex < menuItems - 1) ? menuIndex + 1 : 0;
displayMenu();
delay(200);
}
if (digitalRead(buttonSelect) == LOW) {
executeMenuItem(menuIndex);
delay(200);
}
}
void displayMenu() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Main Menu");
lcd.setCursor(0, 1);
lcd.print(">");
lcd.print(menu[menuIndex]);
}
void executeMenuItem(int item) {
lcd.clear();
lcd.setCursor(0, 0);
switch(item) {
case 0:
lcd.print("Temperature: 25C");
break;
case 1:
lcd.print("Humidity: 60%");
break;
case 2:
lcd.print("Pressure: 1013mb");
break;
case 3:
lcd.print("Settings Menu");
break;
}
lcd.setCursor(0, 1);
lcd.print("Press any key...");
// Wait for button press
while(digitalRead(buttonSelect) == HIGH &&
digitalRead(buttonUp) == HIGH &&
digitalRead(buttonDown) == HIGH) {
delay(50);
}
delay(200); // Debounce
displayMenu();
}
Performance Optimization
Efficient Display Updates
// Instead of clearing entire display frequently
void inefficientUpdate() {
lcd.clear(); // Slow operation
lcd.setCursor(0, 0);
lcd.print("Value: ");
lcd.print(sensorValue);
}
// Use targeted updates
void efficientUpdate() {
static float lastValue = -999; // Track previous value
if (sensorValue != lastValue) {
lcd.setCursor(7, 0); // Position after "Value: "
lcd.print(" "); // Clear only the number area
lcd.setCursor(7, 0);
lcd.print(sensorValue);
lastValue = sensorValue;
}
}
Conclusion
LCD displays are versatile components that can enhance any Arduino project. Whether you choose parallel or I2C connection depends on your pin availability and project complexity. The examples in this guide provide a solid foundation for building more advanced LCD-based projects.
Key takeaways:
- I2C modules simplify connections and save pins
- Custom characters add visual appeal to your projects
- Efficient programming prevents display flickering
- Menu systems create professional user interfaces