Drive an HD44780 LCD with PIC16F877A in PICSimLab
A character LCD is usually the second project after the LED blink: it proves you can drive a multi-pin peripheral with real timing. PICSimLab includes an LCD hd44780 spare part, so you can wire a text display to a PIC16F877A, print messages from XC8 code and debug everything on screen before touching real hardware.
What you need
- PICSimLab 0.9.3 — see the download page.
- MPLAB X IDE and the XC8 compiler (required for PIC16F projects generated by the Code Project Wizard).
- Board: McLab2 or PICGenios, microcontroller PIC16F877A.
- One LCD hd44780 spare part (Outputs category in Spare Parts).
- A working blink project is recommended first: Blink an LED with PIC16F877A.
Wiring used in this tutorial (example)
The LCD spare part is connected in its Properties window, so the mapping below is the example configuration used by the code — set the same pins in the part’s Properties:
- LCD data D4–D7 → RD4–RD7 (4-bit mode, so D0–D3 are not used)
- LCD RS → RD2, LCD EN (E) → RD3, LCD RW → ground (write only)
Steps
1. Generate the project
Select the McLab2 or PICGenios board with the PIC16F877A, then generate an MPLAB X project with the Code Project Wizard (or copy a board example from Help → Examples).
2. Add the LCD spare part
Open Modules → Spare parts, then Outputs → LCD hd44780. Right-click the display, open Properties and connect RS, EN and D4–D7 exactly as listed above. Save your spare-parts layout with the window’s File → Save configuration option (a .pcf file) so you can reload it later.
3. Build the example code
The code below is a compact, standard HD44780 driver in 4-bit mode: an enable pulse function, command and data writes split into nibbles, an initialisation sequence and a lcd_puts() helper. Build it in MPLAB X to produce the .hex file.
4. Load the hex and run
Load the hex into PICSimLab and start the simulation. The display should show “Hello from” on the first line and “PIC16F877A” on the second. The cursor is then moved to line 2 in the demo loop to show how lcd_cmd(0xC0) addressing works.
Example code (XC8)
// Example: HD44780 LCD in 4-bit mode - PIC16F877A, XC8
// RS = RD2, EN = RD3, D4-D7 = RD4-RD7, RW tied to ground
#pragma config FOSC = HS
#pragma config WDTE = OFF
#pragma config PWRTE = ON
#pragma config BOREN = ON
#pragma config LVP = OFF
#pragma config CPD = OFF
#pragma config WRT = OFF
#pragma config CP = OFF
#include <xc.h>
#define _XTAL_FREQ 20000000 // Example: match the board clock
#define LCD_RS PORTDbits.RD2
#define LCD_EN PORTDbits.RD3
static void lcd_pulse(void) {
LCD_EN = 1;
__delay_us(2);
LCD_EN = 0;
__delay_us(100);
}
static void lcd_nibble(unsigned char n) {
PORTD = (PORTD & 0x0F) | (n & 0xF0); // high bits hold D4-D7 data
lcd_pulse();
}
static void lcd_byte(unsigned char b, unsigned char is_data) {
LCD_RS = is_data;
lcd_nibble(b & 0xF0);
lcd_nibble((unsigned char)(b << 4));
__delay_ms(2);
}
void lcd_cmd(unsigned char c) { lcd_byte(c, 0); }
void lcd_putc(char ch) { lcd_byte((unsigned char)ch, 1); }
void lcd_puts(const char *s) { while (*s) lcd_putc(*s++); }
void lcd_init(void) {
__delay_ms(20); // LCD power-up wait
LCD_RS = 0;
lcd_nibble(0x30); __delay_ms(5);
lcd_nibble(0x30); __delay_us(200);
lcd_nibble(0x30); __delay_us(200);
lcd_nibble(0x20); // switch to 4-bit mode
lcd_cmd(0x28); // 4-bit, 2 lines, 5x8 font
lcd_cmd(0x0C); // display on, cursor off
lcd_cmd(0x06); // entry mode: increment, no shift
lcd_cmd(0x01); // clear display
__delay_ms(2);
}
void main(void) {
TRISD = 0x00; // PORTD all outputs
PORTD = 0x00;
lcd_init();
lcd_puts("Hello from");
lcd_cmd(0xC0); // go to line 2
lcd_puts("PIC16F877A");
while (1) { } // hold the message on screen
}
Try next
- HD44780 LCD over I2C (PCF8574) — the same display on just two pins.
- Matrix keypad — type on a keypad and echo the keys to this LCD.
- ADC potentiometer — print a live sensor value instead of fixed text.
Troubleshooting
The display stays completely blank. Work through the chain: correct hex loaded, simulation running, and every LCD pin in the spare part’s Properties connected to the pin the code drives. A single swapped data line (for example D4 and D7 exchanged) garbles all characters.
Blocks or random characters instead of text. That is the classic sign of a timing or nibble-order problem: keep the initialisation delays, and make sure the high nibble is sent before the low nibble, as in the example.
