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.
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).
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.
/* 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; }
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)
/* ── 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