civil-and-structural-engineering
Step-by-step Tutorial on Interfacing Lcd Displays with Pic Microcontrollers
Table of Contents
Interfacing an LCD display with a PIC microcontroller is a fundamental skill in embedded systems that enables the creation of user-friendly, interactive devices. A character LCD (Liquid Crystal Display) allows you to show alphanumeric information, sensor readings, system status, or simple menus—making your projects more accessible and professional. This guide provides a comprehensive, step-by-step approach to connecting and programming a standard 16x2 LCD with a PIC microcontroller using 4-bit mode, covering hardware wiring, initialization, command sequences, and software implementation with the XC8 compiler. By the end, you will have a solid foundation to integrate displays into any PIC-based system.
Understanding the Components
The PIC Microcontroller
The PIC16F877A is a popular mid-range 8-bit microcontroller with 40 pins, 8KB of Flash memory, and multiple I/O ports. Its simplicity and wide availability make it ideal for learning display interfacing. Other PIC variants (PIC18, PIC24) follow the same principles but may require different register configurations.
The LCD Module
A standard 16x2 LCD module contains 16 columns and 2 rows, each character cell being 5x8 pixels. The industry-standard HD44780 controller (or compatible) is built into most character LCDs. It handles all the pixel driving logic internally; the microcontroller only sends commands and data via a parallel interface. The LCD has 16 pins: VSS (GND), VCC (+5V), V0 (contrast), RS (Register Select), R/W (Read/Write), E (Enable), and 8 data pins (D0–D7). In 4-bit mode only pins D4–D7 are used, saving four GPIO lines.
Additional Components
- Resistors: A 220Ω resistor for the LED backlight current limiting, and a 10kΩ potentiometer for contrast adjustment.
- Potentiometer (10kΩ): Used as a voltage divider to adjust the contrast voltage (V0).
- Connecting wires and breadboard
Wiring the LCD to the PIC Microcontroller
The following table shows the recommended connections using PORTC of the PIC16F877A. All control and data lines are connected to PORTC (pins RC0–RC5). The R/W pin is tied to GND because the LCD only needs to be written to (no read-back required).
| LCD Pin | Function | PIC Connection |
|---|---|---|
| 1 | VSS | GND |
| 2 | VCC | +5V |
| 3 | V0 | Center pin of 10kΩ potentiometer (other ends to +5V and GND) |
| 4 | RS | RC0 |
| 5 | R/W | GND |
| 6 | E | RC1 |
| 11 | D4 | RC2 |
| 12 | D5 | RC3 |
| 13 | D6 | RC4 |
| 14 | D7 | RC5 |
| 15 | Backlight Anode | +5V through 220Ω resistor |
| 16 | Backlight Cathode | GND |
Contrast Adjustment: Turn the potentiometer slowly while the display is powered. A typical setting gives a faint row of rectangles on the first line. Over-adjusting can cause the characters to disappear.
Understanding LCD Operation Fundamentals
4-bit vs 8-bit Mode
The HD44780 can operate in 8-bit mode (using all 8 data lines) or 4-bit mode (using only D4–D7). 4-bit mode is more common because it saves four I/O pins, which are valuable in small microcontrollers. In 4-bit mode, each byte is transmitted as two 4-bit nibbles: the high nibble first, then the low nibble. The Enable (E) pin must be pulsed low after each nibble.
Command vs Data
The RS pin selects between command mode (RS=0) and data mode (RS=1). Commands configure the display (clear, cursor home, entry mode, etc.) and data writes characters to the DDRAM (Display Data RAM).
Key Commands
- 0x02: Return home (reset cursor to position 0,0)
- 0x28: Function set – 2 lines, 5x8 matrix, 4-bit mode
- 0x0C: Display ON, cursor OFF, blink OFF
- 0x06: Entry mode – increment cursor, no shift
- 0x01: Clear display and home cursor
- 0x80 + col: Set DDRAM address for row 0 (0x80 – 0x8F)
- 0xC0 + col: Set DDRAM address for row 1 (0xC0 – 0xCF)
Programming the PIC Microcontroller
We will use MPLAB X IDE with the XC8 compiler. The code initializes PORTC as output, sends the LCD initialization sequence, and displays "Hello, LCD!". All delays are implemented using __delay_ms() which requires the _XTAL_FREQ definition.
Expanded Code Explanation
Below is the complete code with comments explaining each function. The LCD_Command() and LCD_WriteChar() functions handle the 4-bit data transfer. The LCD_Init() sequence follows the datasheet recommendation: wait >15ms after power-up, send function set command in 8-bit mode first (0x30 three times), then switch to 4-bit mode (0x02), then configure lines, display, and entry mode.
#include <xc.h>
#define _XTAL_FREQ 20000000 // 20 MHz crystal frequency
// Pin definitions (PORTC)
#define RS PORTCbits.RC0
#define EN PORTCbits.RC1
#define D4 PORTCbits.RC2
#define D5 PORTCbits.RC3
#define D6 PORTCbits.RC4
#define D7 PORTCbits.RC5
// Simple millisecond delay (blocking)
void delay_ms(unsigned int ms) {
while(ms--) {
__delay_ms(1);
}
}
// Send a nibble (4 bits) to the LCD and pulse Enable
void LCD_SendNibble(unsigned char nibble) {
D4 = (nibble & 0x01);
D5 = (nibble & 0x02) >> 1;
D6 = (nibble & 0x04) >> 2;
D7 = (nibble & 0x08) >> 3;
EN = 1;
__delay_ms(1); // Enable pulse width >450 ns
EN = 0;
}
// Send a command byte (RS = 0)
void LCD_Command(unsigned char cmd) {
// Send high nibble first
unsigned char highNibble = (cmd >> 4) & 0x0F;
RS = 0; // Command mode
LCD_SendNibble(highNibble);
// Send low nibble
unsigned char lowNibble = cmd & 0x0F;
LCD_SendNibble(lowNibble);
__delay_ms(2); // Most commands require >1.52 ms
}
// Write a character byte (RS = 1)
void LCD_WriteChar(unsigned char data) {
unsigned char highNibble = (data >> 4) & 0x0F;
RS = 1; // Data mode
LCD_SendNibble(highNibble);
unsigned char lowNibble = data & 0x0F;
LCD_SendNibble(lowNibble);
__delay_ms(2); // Character write time ~40 µs, safe margin
}
// Initialize LCD in 4-bit mode
void LCD_Init() {
delay_ms(20); // Wait for LCD power-up
// Special sequence to enter 4-bit mode (datasheet step)
// First send 0x30 three times in 8-bit mode (simulated with high nibble only)
RS = 0;
D4 = 0; D5 = 0; D6 = 0; D7 = 1; // 0x30 high nibble (0011)
EN = 1; __delay_ms(5); EN = 0;
delay_ms(5);
EN = 1; __delay_ms(5); EN = 0;
delay_ms(5);
EN = 1; __delay_ms(5); EN = 0;
delay_ms(5);
// Now switch to 4-bit mode by sending 0x02
D4 = 0; D5 = 0; D6 = 1; D7 = 0; // 0x02 (0010)
EN = 1; __delay_ms(5); EN = 0;
delay_ms(5);
// Configure function set: 2 lines, 5x8 matrix, 4-bit (0x28)
LCD_Command(0x28);
// Display ON, cursor off, blink off (0x0C)
LCD_Command(0x0C);
// Entry mode: increment, no shift (0x06)
LCD_Command(0x06);
// Clear display (0x01)
LCD_Command(0x01);
delay_ms(2);
}
// Set cursor to row (0 or 1) and column (0-15)
void LCD_SetCursor(unsigned char row, unsigned char col) {
unsigned char address;
if(row == 0)
address = 0x80 + col; // Row 0 DDRAM starts at 0x80
else
address = 0xC0 + col; // Row 1 DDRAM starts at 0xC0
LCD_Command(address);
}
// Main program
void main(void) {
TRISC = 0x00; // All PORTC pins as output
PORTC = 0x00; // Initial state low
LCD_Init();
// Write a message on the first line
LCD_SetCursor(0, 0);
char message[] = "Hello, LCD!";
for(int i = 0; message[i] != '\0'; i++) {
LCD_WriteChar(message[i]);
}
while(1) {
// Infinite loop, keep display content
}
}
Timing Considerations
The HD44780 requires specific timing: the Enable pulse must be at least 450 ns wide, and after each command or data write the LCD needs a certain execution time (1.52 ms for most commands, 40 µs for character writes). The above code uses 1 ms delays which are more than adequate for the pulse width, and 2 ms delays after each byte to guarantee compliance. For production code, you can reduce delays using NOPs or a timer, but these conservative values ensure reliable operation across all LCD modules.
Advanced Features
Displaying Variables and Numbers
To show numeric data (e.g., temperature or counter), convert the integer to a string using sprintf() or a custom itoa() function. For example:
unsigned int adc_value = 512;
char buffer[16];
sprintf(buffer, "ADC=%4d", adc_value);
LCD_SetCursor(0, 0);
for(int i = 0; buffer[i] != '\0'; i++) LCD_WriteChar(buffer[i]);
Custom Characters
The HD44780 allows you to create up to 8 custom characters (5x8 pixels). Load a custom character into CGRAM (Character Generator RAM) using commands 0x40 + address (0–7). Each character is defined by 8 bytes (rows). Example to create a smiley face:
unsigned char smiley[8] = {
0b00000,
0b01010,
0b01010,
0b00000,
0b10001,
0b01110,
0b00000,
0b00000
};
void LCD_CreateChar(unsigned char location, unsigned char *charmap) {
LCD_Command(0x40 + (location << 3));
RS = 1;
for(int i = 0; i < 8; i++) {
LCD_WriteChar(charmap[i]); // Actually write data, not command
}
RS = 0;
}
// Then display: LCD_WriteChar(0) to show the custom character
Scrolling Text
Implement scrolling by shifting the display content left or right using commands 0x18 (scroll left) and 0x1C (scroll right). Combine with timers to create smooth Marquee effects.
Testing and Troubleshooting
After wiring and uploading the code, you should see "Hello, LCD!" on the first line. If not, follow these steps:
- No display at all: Measure voltage on VCC (should be 4.7–5.3V). Check contrast potentiometer; turn it until the background rectangles appear. Verify backlight resistor is correct.
- Rectangles but no text: The display is working but receiving wrong commands. Check RS, E, and data pin wiring. Ensure your code sends the correct initialization sequence. Use an oscilloscope or logic analyzer on the E line to confirm pulses.
- Garbled characters: Usually caused by incorrect 4-bit initialization. The LCD must see the 0x30 command three times before switching to 4-bit mode. Verify that you are not using any other interrupts that may disrupt timing.
- Incorrect cursor position: Confirm the DDRAM addresses for row 1 (0xC0). Some LCD controllers (e.g., 16x4) have different mapping.
- Flickering display: The E pin may be floating; add a pull-down resistor (10kΩ to GND) or ensure the pin is actively driven low between pulses.
External Resources
- HD44780 LCD Datasheet (PDF)
- PIC16F877A Datasheet (Microchip)
- MPLAB X IDE – Free Development Environment
- ElectronicWings – LCD Interfacing Tutorial
Conclusion
Interfacing an LCD with a PIC microcontroller is a rewarding step in embedded design. By understanding the 4-bit mode protocol, initialization sequence, and timing requirements, you can reliably display any information on a character LCD. This foundation enables you to build more complex systems—such as menu-driven interfaces, sensor monitors, or real-time clocks—while using minimal I/O pins. Experiment with custom characters, scrolling, and multiple display lines to expand your project’s capabilities. With practice, LCD integration will become an intuitive part of your microcontroller skill set.