Using a HD44780 LCD display with MicroPython on the Raspberry Pi Pico

I got my Raspberry Pi Pico today (it's now part of our mandatory lockdown ration, right?), and immediately tried to do something vaguely undocumnented with it - namely, run one of those generic HD44780 LCD displays. Mine was a lovely Adafruit one via Pimoroni; thanks for taking all my money Pimoroni! Suckers, what they don't know is that I'm going to keep spending my money there until I'm bankrupt.

There is what looks like a decent C library for the HD44780 and the Pi Pico, called picoLCD, but I wanted to try out MicroPython, which was completely new to me. Luckily, wjdp has written a perfectly functional generic MicroPython library for the HD44780 and the PyBoard, called micropython-lcd. It won't work on the Pi Pico out of the box, but it took me all of fifteen minutes to adapt it to the Pi Pico. It's pretty straightforward, here's the complete code.

90% of this is not mine - I just adjusted some function calls!

from machine import Pin, Timer
import utime

# Almost entirely copied from micropython-lcd by wjdp
# https://github.com/wjdp/micropython-lcd
class LCD(object):
    # Pinout, change within or outside class for your use case
    PINS = [16, 17, 18, 19, 20, 21]
    # Pin names, don't change
    PIN_NAMES = ['RS','E','D4','D5','D6','D7']

    # Dict of pins
    pins = {}

    # Pin mode, push-pull control
    PIN_MODE = Pin.OUT

    # Define some device constants
    LCD_WIDTH = 16    # Maximum characters per line
    # Designation of T/F for character and command modes
    LCD_CHR = True
    LCD_CMD = False

    LINES = {
        0: 0x80, # LCD RAM address for the 1st line
        1: 0xC0, # LCD RAM address for the 2nd line
        # Add more if desired
    }

    # Timing constants
    E_PULSE = 1
    E_DELAY = 1

    def init(self):
        # Initialise pins
        for pin, pin_name in zip(self.PINS, self.PIN_NAMES):
            self.pins['LCD_'+pin_name] = Pin(pin, self.PIN_MODE)
        # Initialise display
        self.lcd_byte(0x33,self.LCD_CMD)
        self.lcd_byte(0x32,self.LCD_CMD)
        self.lcd_byte(0x28,self.LCD_CMD)
        self.lcd_byte(0x0C,self.LCD_CMD)
        self.lcd_byte(0x06,self.LCD_CMD)
        self.lcd_byte(0x01,self.LCD_CMD)

    def clear(self):
        # Clear the display
        self.lcd_byte(0x01,self.LCD_CMD)

    def set_line(self, line):
        # Set the line that we're going to print to
        self.lcd_byte(self.LINES[line], self.LCD_CMD)

    def set_string(self, message):
        # Pad string out to LCD_WIDTH
        m_length = len(message)
        if m_length < self.LCD_WIDTH:
            short = self.LCD_WIDTH - m_length
            blanks=str()
            for i in range(short):
                blanks+=' '
            message+=blanks
        for i in range(self.LCD_WIDTH):
            self.lcd_byte(ord(message[i]), self.LCD_CHR)

    def lcd_byte(self, bits, mode):
        # Send byte to data pins
        # bits = data
        # mode = True  for character
        #        False for command

        self.pin_action('LCD_RS', mode) # RS

        # High bits
        self.pin_action('LCD_D4', False)
        self.pin_action('LCD_D5', False)
        self.pin_action('LCD_D6', False)
        self.pin_action('LCD_D7', False)
        if bits&0x10==0x10:
            self.pin_action('LCD_D4', True)
        if bits&0x20==0x20:
            self.pin_action('LCD_D5', True)
        if bits&0x40==0x40:
            self.pin_action('LCD_D6', True)
        if bits&0x80==0x80:
            self.pin_action('LCD_D7', True)

        # Toggle 'Enable' pin
        self.udelay(self.E_DELAY)
        self.pin_action('LCD_E', True)
        self.udelay(self.E_PULSE)
        self.pin_action('LCD_E', False)
        self.udelay(self.E_DELAY)

        # Low bits
        self.pin_action('LCD_D4', False)
        self.pin_action('LCD_D5', False)
        self.pin_action('LCD_D6', False)
        self.pin_action('LCD_D7', False)
        if bits&0x01==0x01:
            self.pin_action('LCD_D4', True)
        if bits&0x02==0x02:
            self.pin_action('LCD_D5', True)
        if bits&0x04==0x04:
            self.pin_action('LCD_D6', True)
        if bits&0x08==0x08:
            self.pin_action('LCD_D7', True)

        # Toggle 'Enable' pin
        self.udelay(self.E_DELAY)
        self.pin_action('LCD_E', True)
        self.udelay(self.E_PULSE)
        self.pin_action('LCD_E', False)
        self.udelay(self.E_DELAY)

    def udelay(self, us):
        # Delay by us microseconds, set as function for portability
        utime.sleep_ms(us)

    def pin_action(self, pin, high):
        # Pin high/low functions, set as function for portability
        if high:
            self.pins[pin].value(1)
        else:
            self.pins[pin].value(0)

display = LCD()
display.init()

display.set_line(0)
display.set_string("Hello world!")
display.set_line(1)
display.set_string("I am Pi Pico!")

Here's a photo of the finished monstrosity (wire as I say, not as I do, etc.):

A photograph of the HD44780 LCD wired up to a Raspberry Pi Pico on a small breadboard with a mess of jumper wires

For reference, my wiring is as follows:

HD44780DestinationFunction
1 (Vss)Ground (0V)Powers the HD44780's processor
2 (Vdd)5VPowers the HD44780's processor
3 (V0)Ground via a 1K resistor*LCD contrast control: high for nothing, low for aggressive white squares that hate you
4 (RS)Pi Pico GP16 (pin 21)Register Select. High to send data, low to send instructions (like 'clear screen').
5 (R/W)GroundRead/write toggle. I've kept it permanently tied low since we only ever output to the display.
6 (E)Pi Pico GP17 (pin 22)Enable signal. Makes the thing do the stuff.
11, 12, 13, 14 (DB4-DB7)Pi Pico GP18, GP19, GP20, GP21 (pins 24-27)Data pins. We send data through them!
15 (BLA)5VBacklight LED anode.
16 (BLK)Ground (0V)Backlight LED cathode. I stuck a 220 ohm resistor in here to chill it out a bit.

* My LCD came with a rotary potentiometer to adjust the contrast. The resistor works great too!

If I expand on this, it will be to add custom character functionality, which is something the HD44780 supports. I just need to read the datasheet, on which note, this looks like the one for the specific 16x2 1602A model I'm using. But you might as well just read the Wiki page for the HD44780 - the functionality (like war) doesn't change.

This was a fun thing to mess around with of an evening - now that I'm more familiar with the display, I'm hoping to get it wired up to a Z80 processor I'm currently playing with.