🍓 Pico Series
Pico Learning Series · Project 01

LCD Display Pi Pico

Senthil Vel K · senthilvel-k.github.io ↗
Embedded SW Engineer II · Visteon Corporation

A bare-metal LCD driver written entirely from scratch in C for the Raspberry Pi Pico. No library — just GPIO, timing, the HD44780 protocol, and a 4-bit nibble bus hand-rolled over six wires.

View on GitHub ↗ ← All Projects
16×2
LCD Lines
4-bit
Mode
6
GPIO Pins
16×2 LCD DISPLAY — HD44780
Senthil Vel 
A
🔌 Raspberry Pi Pico
// 01 — Project Overview

LCD Display with
Raspberry Pi Pico

🖥️
What It Does
Interfaces a 16×2 HD44780 LCD display with the Raspberry Pi Pico microcontroller over a 4-bit parallel bus. Displays static text on Line 1 and dynamically cycles characters A–Z on Line 2, demonstrating real-time GPIO control and LCD protocol implementation.
Pico SDK GPIO HD44780 4-bit mode
⚙️
What I Built
Wrote the complete LCD driver from scratch in C using the Pico SDK — covering GPIO initialization, 4-bit parallel send, enable pulse generation, initialization sequence, cursor control, screen clear, and string printing. No external LCD library used.
C Language CMake Bare Metal Driver Dev
🔌
Hardware Setup
Connected the LCD's RS, EN, D4–D7 lines to GPIO 0–5 on the Pico. A potentiometer on the V0 pin sets display contrast. The Pico acts as the bus master, clocking data in 4-bit nibbles synchronized with the EN pulse.
16×2 LCD Breadboard Potentiometer
📡
Key Learnings
Deep-dived into the HD44780 initialization sequence, timing constraints (enable pulse width, command settle times), cursor addressing via DDRAM offsets (0x80 / 0xC0), and the difference between command mode (RS=0) and data mode (RS=1).
HD44780 Timing DDRAM RS Pin Logic
// 02 — Communication Protocol

How the 4-bit Parallel
Protocol Works

The HD44780 Bus Protocol

The LCD speaks a parallel interface — multiple data wires change simultaneously. Instead of using all 8 data lines (8-bit mode), this project uses only 4 lines (D4–D7) in 4-bit mode, saving 4 GPIO pins.

Each byte sent to the LCD is split into two nibbles — the upper 4 bits first, then the lower 4 bits. After each nibble, the Enable (EN) pin is pulsed HIGH→LOW to clock the data into the LCD's register.

The RS (Register Select) pin tells the LCD whether the byte is a command (RS=0, e.g. clear screen, set cursor) or data (RS=1, e.g. ASCII character to display).

The Initialization Sequence

On power-up, the LCD must be initialized by sending a specific sequence of commands: start in 8-bit mode, switch to 4-bit, set 2-line display with 5×8 font, turn the display on, and clear it. This sequence is time-critical — delays between commands are mandatory.

// Signal Timing — Sending 'A' (0x41)
RS
HIGH = Data Mode
EN
pulse↑ pulse↑
D7-D4
0100 0001 = 'A'
// Live 4-bit Data Animator
Nibble 1
0
1
0
0
D7–D4
Nibble 2
0
0
0
1
D7–D4
Char: A ASCII: 0x41 RS=1 (data)
// 03 — Pin Connections

GPIO Wiring Diagram

RS — Register Select
LCD Pin
EN — Enable
LCD Pin
D4 — Data bit 4
LCD Pin
D5 — Data bit 5
LCD Pin
D6 — Data bit 6
LCD Pin
D7 — Data bit 7
LCD Pin
Raspberry Pi Pico
GPIO 0
LCD_RS_PIN
GPIO 1
LCD_EN_PIN
GPIO 2
LCD_D4_PIN
GPIO 3
LCD_D5_PIN
GPIO 4
LCD_D6_PIN
GPIO 5
LCD_D7_PIN
// 04 — Working Principle

Five Steps to Pixels

01
GPIO Setup
Initialize 6 GPIO pins as outputs: RS, EN, D4–D7. Configure direction with the Pico SDK.
02
LCD Init
Send the HD44780 init sequence — switch to 4-bit mode, set 2-line 5×8 font, display ON.
03
Set Cursor
Write DDRAM address: 0x80 for row 0, 0xC0 for row 1, plus column offset.
04
Send Data
For each character: RS=1, send upper nibble with EN pulse, then lower nibble with EN pulse.
05
A→Z Loop
Main loop updates Line 2 every 1 second, cycling A through Z infinitely.
// 05 — Source Code

The Driver Implementation

lcd.c — Created by skumara4
/* LCD Display with Raspberry Pi Pico
   Author: Senthil Vel K (skumara4) — Visteon Corporation */

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"

#define LCD_RS_PIN  0
#define LCD_EN_PIN  1
#define LCD_D4_PIN  2
#define LCD_D5_PIN  3
#define LCD_D6_PIN  4
#define LCD_D7_PIN  5

// Send a command (mode=0) or data byte (mode=1) to the LCD
static void lcd_send_byte(uint8_t value, uint8_t mode) {
    gpio_put(LCD_RS_PIN, mode);

    // Upper nibble first
    gpio_put(LCD_D4_PIN, (value >> 4) & 1);
    gpio_put(LCD_D5_PIN, (value >> 5) & 1);
    gpio_put(LCD_D6_PIN, (value >> 6) & 1);
    gpio_put(LCD_D7_PIN, (value >> 7) & 1);
    gpio_put(LCD_EN_PIN, 1); sleep_us(1); gpio_put(LCD_EN_PIN, 0);

    // Lower nibble
    gpio_put(LCD_D4_PIN, value & 1);
    gpio_put(LCD_D5_PIN, (value >> 1) & 1);
    gpio_put(LCD_D6_PIN, (value >> 2) & 1);
    gpio_put(LCD_D7_PIN, (value >> 3) & 1);
    gpio_put(LCD_EN_PIN, 1); sleep_us(1); gpio_put(LCD_EN_PIN, 0);

    sleep_ms(2);
}

void lcd_init() {
    // Initialize all GPIO pins
    int pins[] = {LCD_RS_PIN, LCD_EN_PIN, LCD_D4_PIN,
                  LCD_D5_PIN, LCD_D6_PIN, LCD_D7_PIN};
    for (int i = 0; i < 6; i++) {
        gpio_init(pins[i]);
        gpio_set_dir(pins[i], GPIO_OUT);
    }
    // HD44780 initialization sequence
    lcd_send_byte(0x33, 0); // Initialize
    lcd_send_byte(0x32, 0); // Set 4-bit mode
    lcd_send_byte(0x28, 0); // 2 lines, 5×8 font
    lcd_send_byte(0x0C, 0); // Display ON, cursor off
    lcd_send_byte(0x01, 0); // Clear display
    sleep_ms(2);
}

void lcd_clear() { lcd_send_byte(0x01, 0); sleep_ms(2); }

void lcd_set_cursor(uint8_t row, uint8_t col) {
    uint8_t position = 0x80;
    if (row == 1) position += 0x40; // Row 1 DDRAM offset
    position += col;
    lcd_send_byte(position, 0);
}

void lcd_print(const char *str) {
    while (*str) { lcd_send_byte(*str++, 1); }
}

int main() {
    stdio_init_all();
    lcd_init();
    lcd_clear();
    lcd_set_cursor(0, 0);
    lcd_print("Senthil Vel");

    char counter = 'A';
    while (1) {
        char message[17];
        snprintf(message, sizeof(message), "%c", counter);
        lcd_set_cursor(1, 0);
        lcd_print(message);
        sleep_ms(1000);
        counter = (counter < 'Z') ? counter + 1 : 'A';
    }
    return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(myapp C CXX ASM)
set(CMAKE_C_STANDARD   11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
add_executable(lcd
    lcd.c
)
pico_add_extra_outputs(lcd)
target_link_libraries(lcd pico_stdlib)
pico_enable_stdio_usb (lcd 1)
pico_enable_stdio_uart(lcd 0)
API Reference
/* ── Custom LCD Driver APIs ──────────────────────────── */

// lcd_send_byte(value, mode)
//   value : byte to send (command or ASCII char)
//   mode  : 0 = command (RS low), 1 = data (RS high)
//   Splits byte into two 4-bit nibbles, clocks each with EN pulse
static void lcd_send_byte(uint8_t value, uint8_t mode);

// lcd_init()
//   Initializes all 6 GPIO pins and runs HD44780 init sequence:
//   0x33 → 0x32 → 0x28 (4-bit, 2-line, 5×8) → 0x0C (ON) → 0x01 (clear)
void lcd_init();

// lcd_clear()
//   Sends command 0x01 to clear DDRAM and reset cursor to home
void lcd_clear();

// lcd_set_cursor(row, col)
//   row : 0 or 1  (maps to DDRAM base 0x80 / 0xC0)
//   col : 0–15
//   Computes and sends DDRAM address command
void lcd_set_cursor(uint8_t row, uint8_t col);

// lcd_print(str)
//   Iterates over string, sends each char as data byte (RS=1)
void lcd_print(const char *str);

/* ── Pico SDK APIs Used ───────────────────────────────── */

// stdio_init_all()  — init USB/UART stdio
// gpio_init(pin)    — claim and init a GPIO
// gpio_set_dir(pin, GPIO_OUT) — set as output
// gpio_put(pin, val) — set pin HIGH(1) or LOW(0)
// sleep_ms(ms)      — millisecond delay
// sleep_us(us)      — microsecond delay
// snprintf(buf, sz, fmt, ...)  — safe string formatting
// 06 — Hardware

The Real Build

Breadboard
🍓 Pi Pico
HD44780 16×2 LCD
Senthil Vel
A
Microcontroller
Raspberry Pi Pico
Processor
RP2040 Dual-core ARM Cortex-M0+
Display
HD44780 16×2 LCD
Interface
4-bit Parallel
GPIO Pins Used
GPIO 0 – 5 (6 pins)
Build System
CMake + Pico SDK
Language
C (C11 Standard)
Update Rate
1 second (A→Z cycle)
Contrast Control
Potentiometer (V0 pin)
Author
Senthil Vel K · skumara4