Interfacing an HD44780 LCD with Raspberry Pi Pico using MicroPython
The HD44780 LCD is a popular choice for displaying information in embedded systems due to its simplicity and versatility. This article will guide you through the process of interfacing an HD44780 LCD with the Raspberry Pi Pico using MicroPython. We will create two Python modules (i2c_lcd.py
and lcd_api.py
) to handle the communication with the LCD and provide a user-friendly API. Additionally, we will develop a main.py
script that demonstrates how to use these modules to display messages on the LCD.
Prerequisites
- Hardware:
- Raspberry Pi Pico
- HD44780 LCD (16×2 or similar)
- I2C interface module (if using I2C, as it simplifies wiring) FC-113 (HLF8574)
- Jumper wires
- Software:
- MicroPython firmware installed on the Raspberry Pi Pico
- Thonny IDE or any other MicroPython IDE for uploading code
Wiring the LCD
If you are using an I2C interface module, connect the pins as follows:
LCD Pin | I2C Module Pin | Raspberry Pi Pico Pin |
---|---|---|
VCC | VCC | 3.3V |
GND | GND | GND |
SDA | SDA | GP0 |
SCL | SCL | GP1 |
Ensure that your LCD is correctly powered and connected.
Creating the Python Modules
1. lcd_api.py
This module defines the LcdApi
class, which provides methods for interacting with the LCD. The implementation includes command constants, initialization, and various display methods.
import time
class LcdApi:
"""Implements the API for interacting with HD44780 compatible character LCDs.
This class defines commands for controlling the LCD, but it does not handle
the actual communication. A derived class should implement the HAL functions
for hardware-specific operations.
"""
# HD44780 LCD controller command set constants
LCD_CLR = 0x01 # Clear display
LCD_HOME = 0x02 # Return to home position
LCD_ENTRY_MODE = 0x04 # Set entry mode
LCD_ENTRY_INC = 0x02 # Increment cursor
LCD_ENTRY_SHIFT = 0x01 # Shift display
LCD_ON_CTRL = 0x08 # Control LCD and cursor visibility
LCD_ON_DISPLAY = 0x04 # Turn display on
LCD_ON_CURSOR = 0x02 # Turn cursor on
LCD_ON_BLINK = 0x01 # Blinking cursor
LCD_MOVE = 0x10 # Move cursor/display
LCD_MOVE_DISP = 0x08 # Move display
LCD_MOVE_RIGHT = 0x04 # Move right
LCD_FUNCTION = 0x20 # Function set
LCD_FUNCTION_8BIT = 0x10 # Set 8-bit mode
LCD_FUNCTION_2LINES = 0x08 # Two lines
LCD_FUNCTION_10DOTS = 0x04 # 5x10 font
LCD_FUNCTION_RESET = 0x30 # Reset the LCD
LCD_CGRAM = 0x40 # Set CG RAM address
LCD_DDRAM = 0x80 # Set DD RAM address
LCD_RS_CMD = 0
LCD_RS_DATA = 1
LCD_RW_WRITE = 0
LCD_RW_READ = 1
def __init__(self, num_lines, num_columns):
"""Initializes the LCD with the specified number of lines and columns.
Args:
num_lines (int): Number of lines (1 to 4).
num_columns (int): Number of columns (1 to 40).
"""
self.num_lines = min(num_lines, 4) # Limit lines to max 4
self.num_columns = min(num_columns, 40) # Limit columns to max 40
self.cursor_x = 0
self.cursor_y = 0
self.implied_newline = False
self.backlight = True
self.display_off()
self.backlight_on()
self.clear()
self.hal_write_command(self.LCD_ENTRY_MODE | self.LCD_ENTRY_INC)
self.hide_cursor()
self.display_on()
def clear(self):
"""Clears the LCD display and moves the cursor to the top left corner."""
self.hal_write_command(self.LCD_CLR)
self.hal_write_command(self.LCD_HOME)
self.cursor_x = 0
self.cursor_y = 0
def show_cursor(self):
"""Makes the cursor visible."""
self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | self.LCD_ON_CURSOR)
def hide_cursor(self):
"""Hides the cursor."""
self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY)
def blink_cursor_on(self):
"""Turns on the cursor and makes it blink."""
self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | self.LCD_ON_CURSOR | self.LCD_ON_BLINK)
def blink_cursor_off(self):
"""Turns on the cursor, making it solid (not blinking)."""
self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY | self.LCD_ON_CURSOR)
def display_on(self):
"""Turns on the LCD display (unblanks it)."""
self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY)
def display_off(self):
"""Turns off the LCD display (blanks it)."""
self.hal_write_command(self.LCD_ON_CTRL)
def backlight_on(self):
"""Turns the backlight on."""
self.backlight = True
self.hal_backlight_on()
def backlight_off(self):
"""Turns the backlight off."""
self.backlight = False
self.hal_backlight_off()
def move_to(self, cursor_x, cursor_y):
"""Moves the cursor to the specified position.
Args:
cursor_x (int): The column (0-indexed).
cursor_y (int): The row (0-indexed).
"""
if cursor_x < 0 or cursor_x >= self.num_columns:
raise ValueError("cursor_x out of bounds")
if cursor_y < 0 or cursor_y >= self.num_lines:
raise ValueError("cursor_y out of bounds")
self.cursor_x = cursor_x
self.cursor_y = cursor_y
addr = cursor_x & 0x3f
# Adjust for line positioning
if cursor_y & 1:
addr += 0x40 # Lines 1 & 3 add 0x40
if cursor_y & 2:
addr += self.num_columns # Lines 2 & 3 add number of columns
self.hal_write_command(self.LCD_DDRAM | addr)
def putchar(self, char):
"""Writes a character to the LCD and advances the cursor.
Args:
char (str): A single character to display on the LCD.
"""
if char == '\n':
if not self.implied_newline:
self.cursor_x = self.num_columns # Move to next line
else:
self.hal_write_data(ord(char))
self.cursor_x += 1
# Handle cursor wrapping
if self.cursor_x >= self.num_columns:
self.cursor_x = 0
self.cursor_y += 1
self.implied_newline = (char != '\n')
if self.cursor_y >= self.num_lines:
self.cursor_y = 0
self.move_to(self.cursor_x, self.cursor_y)
def putstr(self, string):
"""Writes a string to the LCD and advances the cursor appropriately.
Args:
string (str): The string to display on the LCD.
"""
for char in string:
self.putchar(char)
def custom_char(self, location, charmap):
"""Writes a custom character to CGRAM at the specified location.
Args:
location (int): CGRAM location (0-7).
charmap (list): A list of 8 bytes representing the custom character.
"""
if len(charmap) != 8:
raise ValueError("charmap must be a list of 8 bytes")
location &= 0x7
self.hal_write_command(self.LCD_CGRAM | (location << 3))
self.hal_sleep_us(40)
for byte in charmap:
self.hal_write_data(byte)
self.hal_sleep_us(40)
self.move_to(self.cursor_x, self.cursor_y)
def hal_backlight_on(self):
"""Allows the HAL layer to turn the backlight on.
To be implemented by a derived HAL class.
"""
pass
def hal_backlight_off(self):
"""Allows the HAL layer to turn the backlight off.
To be implemented by a derived HAL class.
"""
pass
def hal_write_command(self, cmd):
"""Writes a command to the LCD.
This method should be implemented by a derived HAL class.
Args:
cmd (int): The command byte to send to the LCD.
"""
raise NotImplementedError("hal_write_command must be implemented by a derived class.")
def hal_write_data(self, data):
"""Writes data to the LCD.
This method should be implemented by a derived HAL class.
Args:
data (int): The data byte to send to the LCD.
"""
raise NotImplementedError("hal_write_data must be implemented by a derived class.")
def hal_sleep_us(self, usecs):
"""Sleeps for a specified number of microseconds.
Args:
usecs (int): The number of microseconds to sleep.
"""
time.sleep(usecs / 1_000_000) # Convert microseconds to seconds
2. i2c_lcd.py
This module implements the communication interface for the LCD using the I2C protocol. It extends the LcdApi
class.
import utime
import gc
from lcd_api import LcdApi
from machine import I2C
# PCF8574 pin definitions for controlling the LCD
MASK_RS = 0x01 # Register Select (P0)
MASK_RW = 0x02 # Read/Write (P1)
MASK_E = 0x04 # Enable (P2)
SHIFT_BACKLIGHT = 3 # Backlight control (P3)
SHIFT_DATA = 4 # Data pins (P4-P7)
class I2cLcd(LcdApi):
"""
Implements a HD44780 character LCD connected via PCF8574 on I2C.
"""
def __init__(self, i2c, i2c_addr, num_lines, num_columns):
"""
Initialize the I2C LCD.
:param i2c: I2C interface
:param i2c_addr: I2C address of the LCD
:param num_lines: Number of lines on the LCD
:param num_columns: Number of columns on the LCD
"""
self.i2c = i2c
self.i2c_addr = i2c_addr
# Power up the LCD
self.i2c.writeto(self.i2c_addr, bytes([0]))
utime.sleep_ms(20) # Allow LCD time to power up
# Send reset sequence three times
self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
utime.sleep_ms(5) # Delay at least 4.1 ms
self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
utime.sleep_ms(1)
self.hal_write_init_nibble(self.LCD_FUNCTION_RESET)
utime.sleep_ms(1)
# Put LCD into 4-bit mode
self.hal_write_init_nibble(self.LCD_FUNCTION)
utime.sleep_ms(1)
# Initialize the LCD with the number of lines and columns
LcdApi.__init__(self, num_lines, num_columns)
cmd = self.LCD_FUNCTION
if num_lines > 1:
cmd |= self.LCD_FUNCTION_2LINES
self.hal_write_command(cmd)
# Clean up garbage collection
gc.collect()
def hal_write_init_nibble(self, nibble):
"""
Writes an initialization nibble to the LCD.
This function is only used during the initialization phase.
:param nibble: The nibble to write
"""
byte = ((nibble >> 4) & 0x0f) << SHIFT_DATA
self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) # Enable pulse high
self.i2c.writeto(self.i2c_addr, bytes([byte])) # Enable pulse low
gc.collect()
def hal_backlight_on(self):
"""Turn the backlight on."""
self.i2c.writeto(self.i2c_addr, bytes([1 << SHIFT_BACKLIGHT]))
gc.collect()
def hal_backlight_off(self):
"""Turn the backlight off."""
self.i2c.writeto(self.i2c_addr, bytes([0]))
gc.collect()
def hal_write_command(self, cmd):
"""
Write a command to the LCD. Data is latched on the falling edge of E.
:param cmd: The command to send to the LCD
"""
byte = ((self.backlight << SHIFT_BACKLIGHT) |
(((cmd >> 4) & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) # Enable pulse high
self.i2c.writeto(self.i2c_addr, bytes([byte])) # Enable pulse low
byte = ((self.backlight << SHIFT_BACKLIGHT) |
((cmd & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) # Enable pulse high
self.i2c.writeto(self.i2c_addr, bytes([byte])) # Enable pulse low
if cmd <= 3:
# The home and clear commands require a worst-case delay of 4.1 ms
utime.sleep_ms(5)
gc.collect()
def hal_write_data(self, data):
"""
Write data to the LCD. Data is latched on the falling edge of E.
:param data: The data byte to send to the LCD
"""
byte = (MASK_RS |
(self.backlight << SHIFT_BACKLIGHT) |
(((data >> 4) & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) # Enable pulse high
self.i2c.writeto(self.i2c_addr, bytes([byte])) # Enable pulse low
byte = (MASK_RS |
(self.backlight << SHIFT_BACKLIGHT) |
((data & 0x0f) << SHIFT_DATA))
self.i2c.writeto(self.i2c_addr, bytes([byte | MASK_E])) # Enable pulse high
self.i2c.writeto(self.i2c_addr, bytes([byte])) # Enable pulse low
gc.collect()
Main Application Script
3. main.py
This script demonstrates how to use the I2cLcd
class to display text on the LCD.
import machine
from machine import I2C
from lcd_api import LcdApi
from i2c_lcd import I2cLcd
from time import sleep
I2C_ADDR = 0x27
totalRows = 2
totalColumns = 16
i2c = I2C(0, sda=machine.Pin(0), scl=machine.Pin(1), freq=400000)
lcd = I2cLcd(i2c, I2C_ADDR, totalRows, totalColumns)
while True:
lcd.putstr("Mutex Firmware! ")
lcd.putstr("RPi Pico")
sleep(2)
lcd.clear()
Summary
In this article, we’ve successfully interfaced an HD44780 LCD with a Raspberry Pi Pico using MicroPython. We created a modular setup with i2c_lcd.py
and lcd_api.py
for handling LCD commands and data. The main.py
script demonstrates how to initialize the LCD and display messages on it. This setup can be expanded for various applications, such as temperature monitoring, user interfaces, and more.
Next Steps
Feel free to extend the functionality of the LCD interface by adding features like scrolling text, custom character creation, or integrating with sensors to display real-time data. Enjoy building your projects with the Raspberry Pi Pico and the HD44780 LCD!