Raspberry Pi Pico – LCD(16×2) HD44780 with i2C I/O Expansion (HLF8574)

Raspberry Pi Pico - LCD(16x2) HD44780 with i2C I/O Expansion (HLF8574)

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 PinI2C Module PinRaspberry Pi Pico Pin
VCCVCC3.3V
GNDGNDGND
SDASDAGP0
SCLSCLGP1

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!

Leave a Reply

Your email address will not be published. Required fields are marked *