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

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.

Similar Posts