ESP32 Tutorial: Web Server for 16×2 LCD Display (4-Bit Parallel)
Abstract
This tutorial details how to interface a standard 16×2 LCD (HD44780) with the ESP32 using the 4-bit parallel mode (without an I²C adapter) and control the displayed text via a simple web server. The ESP32 hosts an HTML form where a user can input two lines of text, which are then sent to the display. This showcases direct GPIO control of a common display module combined with the ESP32’s Wi-Fi capability.
1. Introduction to LCD Display 16x2
Integrating physical displays with Internet-connected devices is a cornerstone of the Internet of Things (IoT). The 16×2 Character LCD (HD44780) remains one of the most common and robust display modules in embedded electronics due to its simplicity and low cost.
This tutorial focuses on an intermediate-level integration, demonstrating how to merge the ESP32’s powerful Wi-Fi capabilities with direct hardware control of the LCD. Instead of relying on the simpler, but often slower, I²C adapter, we utilize the 4-bit parallel mode. While this requires slightly more complex wiring (using 6 dedicated GPIO pins), it provides direct, low-level control and ensures maximum compatibility and performance using the standard LiquidCrystal library.
The final application turns the ESP32 into a Web Server, allowing remote users to instantly change the text displayed on the physical LCD. A user simply navigates to the ESP32’s IP address, enters two lines of text into an HTML form, and submits the data via HTTP POST. This showcases a practical, real-world application of the ESP32’s ability to seamlessly bridge web-based input with physical output devices.
2. Prerequisites and Wiring
2.1 Hardware and Library
- Hardware: ESP32 Dev Board, 16×2 Character LCD (HD44780), 10 kΩ Potentiometer (for contrast), Breadboard.
- Library: The standard LiquidCrystal Library is used, which is included in the Arduino IDE.
2.2 ESP32-to-LCD Wiring (4-Bit Mode)
This mode requires 6 data/control lines plus power and ground. The potentiometer controls the contrast (V0).
LCD Pin | Function | Connection | Notes |
VSS | Ground | GND | |
VDD | Power | 5V (from ESP32 or external) | The LCD often prefers 5V power. |
V0 | Contrast | Middle pin of 10 kΩ pot | Other pot pins to 5V and GND. |
RS | Register Select | GPIO 14 | Control pin: High=Data, Low=Instruction. |
RW | Read/Write | GND | We only write data, so tie to GND. |
E | Enable | GPIO 12 | Control pin: Clocks data in/out. |
D4 | Data Bit 4 | GPIO 27 | 4-bit parallel data lines. |
D5 | Data Bit 5 | GPIO 26 | |
D6 | Data Bit 6 | GPIO 25 | |
D7 | Data Bit 7 | GPIO 33 | |
A/LED+ | Backlight Anode | 5V (or 3.3V) | Often connected via a resistor (e.g., 220Ω). |
K/LED- | Backlight Cathode | GND |
3. The ESP32 Arduino Sketch
The web server handles a POST request from the HTML form to extract the two text lines and send them to the LCD using the LiquidCrystal library.
3.1 Libraries and Global Variables
#include
#include
#include
// Replace with your network credentials
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
WebServer server(80);
// Initialize the LiquidCrystal library with the pin numbers
// LiquidCrystal(rs, enable, d4, d5, d6, d7)
LiquidCrystal lcd(14, 12, 27, 26, 25, 33);
// Global variables to hold the displayed text
String line1 = "ESP32 LCD Server";
String line2 = "Status: Online";
3.2 LCD Handler Function
This function prints the stored global strings to the LCD display.
void updateLCD() {
lcd.clear();
lcd.setCursor(0, 0); // Start of line 1 (Row 0)
lcd.print(line1);
lcd.setCursor(0, 1); // Start of line 2 (Row 1)
lcd.print(line2);
Serial.print("LCD Updated: Line 1: '");
Serial.print(line1);
Serial.print("', Line 2: '");
Serial.print(line2);
Serial.println("'");
}
3.3 Web Server Handlers
The handleFormPost() is the core function that retrieves the user’s input from the HTTP request and calls updateLCD().
// Handler for the root web page (HTML form)
void handleRoot() {
server.send(200, "text/html", getHtmlPage());
}
// Handler for the POST request when the user submits the form
void handleFormPost() {
// Check if both fields exist in the POST request arguments
if (server.hasArg("line_1") && server.hasArg("line_2")) {
// Retrieve and store the new strings (limit to 16 characters for the display)
line1 = server.arg("line_1").substring(0, 16);
line2 = server.arg("line_2").substring(0, 16);
// Update the physical LCD display immediately
updateLCD();
// Redirect back to the main page to show the updated status
server.sendHeader("Location", "/");
server.send(303); // HTTP 303: See Other (redirect)
} else {
server.send(400, "text/plain", "Invalid request. Missing line arguments.");
}
}
3.4 HTML Generation
The HTML includes a form with two input fields that submit data to the /submit endpoint using the POST method.
String getHtmlPage() {
String html = R"rawliteral(
ESP32 LCD Controller
LCD Web Interface
Current Text on Display:
Line 1: LINE_1_CURRENT
Line 2: LINE_2_CURRENT
ESP32 IP: IP_ADDRESS
)rawliteral";
// Replace placeholders with current data
html.replace("LINE_1_CURRENT", line1);
html.replace("LINE_2_CURRENT", line2);
html.replace("IP_ADDRESS", WiFi.localIP().toString());
return html;
}
3.5 Setup and Loop
void setup() {
Serial.begin(115200);
// 1. Setup LCD
lcd.begin(16, 2); // Initialize the 16x2 display
updateLCD(); // Print the initial message
// 2. Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
Serial.print("Web Server IP: ");
Serial.println(WiFi.localIP());
// 3. Setup Web Server Routes
server.on("/", HTTP_GET, handleRoot);
server.on("/submit", HTTP_POST, handleFormPost);
server.begin();
}
void loop() {
// Must continuously handle incoming web requests
server.handleClient();
}
4. Source Code
The entire code for an easier copy and paste:
#include
#include
#include
// Replace with your network credentials
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
WebServer server(80);
// Initialize the LiquidCrystal library with the pin numbers
// LiquidCrystal(rs, enable, d4, d5, d6, d7)
LiquidCrystal lcd(14, 12, 27, 26, 25, 33);
// Global variables to hold the displayed text
String line1 = "ESP32 LCD Server";
String line2 = "Status: Online";
void updateLCD() {
lcd.clear();
lcd.setCursor(0, 0); // Start of line 1 (Row 0)
lcd.print(line1);
lcd.setCursor(0, 1); // Start of line 2 (Row 1)
lcd.print(line2);
Serial.print("LCD Updated: Line 1: '");
Serial.print(line1);
Serial.print("', Line 2: '");
Serial.print(line2);
Serial.println("'");
}
// Handler for the root web page (HTML form)
void handleRoot() {
server.send(200, "text/html", getHtmlPage());
}
// Handler for the POST request when the user submits the form
void handleFormPost() {
// Check if both fields exist in the POST request arguments
if (server.hasArg("line_1") && server.hasArg("line_2")) {
// Retrieve and store the new strings (limit to 16 characters for the display)
line1 = server.arg("line_1").substring(0, 16);
line2 = server.arg("line_2").substring(0, 16);
// Update the physical LCD display immediately
updateLCD();
// Redirect back to the main page to show the updated status
server.sendHeader("Location", "/");
server.send(303); // HTTP 303: See Other (redirect)
} else {
server.send(400, "text/plain", "Invalid request. Missing line arguments.");
}
}
String getHtmlPage() {
String html = R"rawliteral(
ESP32 LCD Controller
LCD Web Interface
Current Text on Display:
Line 1: LINE_1_CURRENT
Line 2: LINE_2_CURRENT
ESP32 IP: IP_ADDRESS
)rawliteral";
// Replace placeholders with current data
html.replace("LINE_1_CURRENT", line1);
html.replace("LINE_2_CURRENT", line2);
html.replace("IP_ADDRESS", WiFi.localIP().toString());
return html;
}
void setup() {
Serial.begin(115200);
// 1. Setup LCD
lcd.begin(16, 2); // Initialize the 16x2 display
updateLCD(); // Print the initial message
// 2. Connect to Wi-Fi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi...");
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
Serial.print("Web Server IP: ");
Serial.println(WiFi.localIP());
// 3. Setup Web Server Routes
server.on("/", HTTP_GET, handleRoot);
server.on("/submit", HTTP_POST, handleFormPost);
server.begin();
}
void loop() {
// Must continuously handle incoming web requests
server.handleClient();
}
4.1 Code Explanation and Logic Breakdown
The code combines three main elements: the LiquidCrystal library for hardware control, the WebServer for networking, and a POST handler to synchronize the two.
4.1.1 LCD and Library Initialization
The LiquidCrystal library manages the complex timing and data/instruction sequencing required by the HD44780 controller, simplifying the process into simple print commands.
Initialization: The
LiquidCrystal lcd(...)object is instantiated in global scope, mapping the library’s required control and data lines to the chosen ESP32 GPIO pins:
// LiquidCrystal(rs, enable, d4, d5, d6, d7)
LiquidCrystal lcd(14, 12, 27, 26, 25, 33);
Setup: In
setup(), thelcd.begin(16, 2);function initializes the display geometry (16 columns, 2 rows).Text Storage: Two global
Stringvariables (line1andline2) hold the current text, ensuring the display is always refreshed from a known state.
4.1.2 The LCD Update Function (updateLCD)
This helper function encapsulates the display logic, making it easy to call whenever the text needs changing.
void updateLCD() {
lcd.clear();
lcd.setCursor(0, 0); // Row 0
lcd.print(line1);
lcd.setCursor(0, 1); // Row 1
lcd.print(line2);
// ... (Serial output for debugging)
}
lcd.clear(): Wipes the display clean.lcd.setCursor(col, row): Positions the cursor to the starting column (0) of the desired row (0 or 1).lcd.print(String): Writes the current text data to the display.
4.1.3 The Web POST Handler (handleFormPost)
This is the core communication handler that processes the user’s input from the browser.
void handleFormPost() {
if (server.hasArg("line_1") && server.hasArg("line_2")) {
line1 = server.arg("line_1").substring(0, 16);
line2 = server.arg("line_2").substring(0, 16);
updateLCD();
server.sendHeader("Location", "/");
server.send(303); // Redirect
} else { /* ... error handling ... */ }
}
Argument Retrieval:
server.arg("field_name")retrieves the data submitted from the HTML form’s named input fields (line_1,line_2).Safety Truncation: The
.substring(0, 16)method is used to guarantee that the received strings do not exceed the physical 16-character limit of the LCD, preventing text overflow and display issues.Display Update:
updateLCD()is called immediately to synchronize the physical display with the new data.Redirection:
server.send(303)coupled with theLocationheader is a standard HTTP technique that tells the browser to redirect back to the root page (/) after a successful POST request. This ensures the user sees the updated content and prevents form resubmission errors.
5. Hands-On Lab Recap
You have successfully integrated direct GPIO control of the 16×2 LCD with the ESP32’s web server:
- The LiquidCrystal Library is used for low-level control of the display via 6 dedicated GPIO pins.
- The web page features an HTML form that sends user input via an HTTP POST request to the /submit
- The handleFormPost() function retrieves the form data using arg(), updates the global text variables, and calls updateLCD() to refresh the physical display.
- The substring(0, 16) limit ensures that the user’s input does not overflow the 16-character width of the display.


