Arduino LCD Programming Complete Guide

Master LCD displays with Arduino - from basic connections to advanced projects including I2C, custom characters, and practical applications

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

16x2 LCD 2 rows, 16 characters each
20x4 LCD 4 rows, 20 characters each
OLED Display Higher contrast, no backlight needed

LCD Pin Configuration

Standard HD44780-based LCDs have 16 pins:

Pin Name Function Arduino Connection
1VSSGroundGND
2VDD+5V Power5V
3V0ContrastPotentiometer
4RSRegister SelectDigital Pin 12
5EnableEnableDigital Pin 11
6D0Data Bit 0Not Used
7D1Data Bit 1Not Used
8D2Data Bit 2Not Used
9D3Data Bit 3Not Used
10D4Data Bit 4Digital Pin 5
11D5Data Bit 5Digital Pin 4
12D6Data Bit 6Digital Pin 3
13D7Data Bit 7Digital Pin 2
14ABacklight Anode5V
15KBacklight CathodeGND

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