Raspberry Pi Pico
16x2 Character LCD

I had a 3V3 16x2 character LCD display that I had been trying to get running on the Pico. You can find displays like these with 'backpacks' that connect the inputs to port expanders. This isn't one of those. It is just the display itself. If you are doing this, make sure to check that your LCD is for 3V3.

Here is the display connected to my Pimoroni Pico LiPo. Under the jumpers, you might just be able to make out the potentiometer. This is used to set the contrast.

Pico Circuit

This is a Fritzing diagram showing the connections more clearly.

Pico Circuit

I find that a list of connections is a little easier to work with. So,

  • VSS to GND
  • VDD to 3V3
  • VO to Potentiometer Wiper
  • RS to GP15
  • R/W to GND
  • E to GP14
  • DB4 to GP16
  • DB5 to GP17
  • DB6 to GP18
  • DB7 to GP19
  • A to 3V3
  • K to GND

After several attempts at getting the display to work, I turned to Adafruit's CircuitPython library.

(https://github.com/adafruit/Adafruit_CircuitPython_CharLCD)

I made the changes needed to convert to MicroPython and left out some of the stuff I didn't need. I left in all of the constants. The library was saved as lcd.py.

from time import sleep_us, sleep_ms
from micropython import const

# Commands
_LCD_CLEARDISPLAY = const(0x01)
_LCD_RETURNHOME = const(0x02)
_LCD_ENTRYMODESET = const(0x04)
_LCD_DISPLAYCONTROL = const(0x08)
_LCD_CURSORSHIFT = const(0x10)
_LCD_FUNCTIONSET = const(0x20)
_LCD_SETCGRAMADDR = const(0x40)
_LCD_SETDDRAMADDR = const(0x80)

# Entry flags
_LCD_ENTRYLEFT = const(0x02)
_LCD_ENTRYSHIFTDECREMENT = const(0x00)

# Control flags
_LCD_DISPLAYON = const(0x04)
_LCD_CURSORON = const(0x02)
_LCD_CURSOROFF = const(0x00)
_LCD_BLINKON = const(0x01)
_LCD_BLINKOFF = const(0x00)

# Move flags
_LCD_DISPLAYMOVE = const(0x08)
_LCD_MOVERIGHT = const(0x04)
_LCD_MOVELEFT = const(0x00)

# Function set flags
_LCD_4BITMODE = const(0x00)
_LCD_2LINE = const(0x08)
_LCD_1LINE = const(0x00)
_LCD_5X8DOTS = const(0x00)

# Offset for up to 4 rows.
_LCD_ROW_OFFSETS = (0x00, 0x40, 0x14, 0x54)

class LCD:
    def __init__(self, rs, en, d4, d5, d6, d7, columns, lines):
        self.columns = columns
        self.lines = lines
        self.reset = rs
        self.enable= en
        self.dl4 = d4
        self.dl5 = d5
        self.dl6 = d6
        self.dl7 = d7
        self._write8(0x33)
        self._write8(0x32)
        # Initialise display control
        self.displaycontrol = _LCD_DISPLAYON | _LCD_CURSOROFF | _LCD_BLINKOFF
        # Initialise display function
        self.displayfunction = _LCD_4BITMODE | _LCD_1LINE | _LCD_2LINE | _LCD_5X8DOTS
        # Initialise display mode
        self.displaymode = _LCD_ENTRYLEFT | _LCD_ENTRYSHIFTDECREMENT
        # Write to displaycontrol
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)
        # Write to displayfunction
        self._write8(_LCD_FUNCTIONSET | self.displayfunction)
        # Set entry mode
        self._write8(_LCD_ENTRYMODESET | self.displaymode)
        self.clear()
        self._message = None
        self._enable = None
        self._direction = None
        # track row and column used in cursor_position
        # initialize to 0,0
        self.row = 0
        self.column = 0
        self._column_align = False
        
    def home(self):
        self._write8(_LCD_RETURNHOME)
        sleep_ms(3)

    def clear(self):
        self._write8(_LCD_CLEARDISPLAY)
        sleep_ms(3)

    def column_align(self, enable):
        if isinstance(enable, bool):
            self._column_align = enable
        else:
            raise ValueError("The column_align value must be either True or False")

    def cursor(self, show):
        if show:
            self.displaycontrol |= _LCD_CURSORON
        else:
            self.displaycontrol &= ~_LCD_CURSORON
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)

    def cursor_position(self, column, row):
        # Clamp row to the last row of the display
        if row >= self.lines:
            row = self.lines - 1
        # Clamp to last column of display
        if column >= self.columns:
            column = self.columns - 1
        # Set location
        self._write8(_LCD_SETDDRAMADDR | (column + _LCD_ROW_OFFSETS[row]))
        # Update self.row and self.column to match setter
        self.row = row
        self.column = column        
    
    def blink(self, blink):
        if blink:
            self.displaycontrol |= _LCD_BLINKON
        else:
            self.displaycontrol &= ~_LCD_BLINKON
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)    
    
    def display(self, enable):
        if enable:
            self.displaycontrol |= _LCD_DISPLAYON
        else:
            self.displaycontrol &= ~_LCD_DISPLAYON
        self._write8(_LCD_DISPLAYCONTROL | self.displaycontrol)

    def message(self, message):
        self._message = message        
        line = self.row
        initial_character = 0
        for character in message:
            if initial_character == 0:
                if self.displaymode & _LCD_ENTRYLEFT > 0:
                    col = self.column
                else:
                    col = self.columns - 1 - self.column
                self.cursor_position(col, line)
                initial_character += 1
            if character == "\n":
                line += 1
                if self.displaymode & _LCD_ENTRYLEFT > 0:
                    col = self.column * self._column_align
                else:
                    if self._column_align:
                        col = self.column
                    else:
                        col = self.columns - 1
                self.cursor_position(col, line)
            else:
                self._write8(ord(character), True)
        self.column, self.row = 0, 0

    def create_char(self, location, pattern):
        location &= 0x7
        self._write8(_LCD_SETCGRAMADDR | (location << 3))
        for i in range(8):
            self._write8(pattern[i], char_mode=True)


    def _write8(self, value, char_mode=False):
        sleep_ms(1)
        self.reset.value(char_mode)
        #  set character/data bit. (charmode = False)
        self.reset.value(char_mode)
        # WRITE upper 4 bits
        self.dl4.value(((value >> 4) & 1) > 0)
        self.dl5.value(((value >> 5) & 1) > 0)
        self.dl6.value(((value >> 6) & 1) > 0)
        self.dl7.value(((value >> 7) & 1) > 0)
        #  send command
        self._pulse_enable()
        # WRITE lower 4 bits
        self.dl4.value((value & 1) > 0)
        self.dl5.value(((value >> 1) & 1) > 0)
        self.dl6.value(((value >> 2) & 1) > 0)
        self.dl7.value(((value >> 3) & 1) > 0)
        self._pulse_enable()
        
    def _pulse_enable(self):
        self.enable.value(0)
        sleep_us(1)
        self.enable.value(1)
        sleep_us(1)
        self.enable.value(0)
        sleep_us(0)

Here is some code testing a handful of the features of the library. I have also created a custom character. You can have up to 8 of these, stored at locations 0 to 7. You use a hex escape sequence to insert them into your message. The character is defined as a list of binary integers, each representing the pixel data of a 5 column row. The characters are 5 pixels wide and 8 pixels tall. I did a stickperson but you may need to design characters that otherwise would not display.

from machine import Pin
from time import sleep
from lcd import LCD

rs = Pin(15, Pin.OUT)
en = Pin(14, Pin.OUT)
d4 = Pin(16, Pin.OUT)
d5 = Pin(17, Pin.OUT)
d6 = Pin(18, Pin.OUT)
d7 = Pin(19, Pin.OUT)

# make a display object
display = LCD(rs, en, d4, d5, d6, d7, 16, 2)
# basic message test
display.message("Hello World!")
sleep(3)
# clear the screen
display.clear()
sleep(1)
# show the cursor
display.cursor(True)
sleep(2)
display.message("Hello again!")
sleep(2)
# hide the cursor
display.cursor(False)
sleep(2)
#fill the display
display.home()
display.message("0123456789ABCDEF\n0123456789ABCDEF")
sleep(3)
display.clear()
# create character
stickperson = [0xe,0xe,0x4,0x1f,0x4,0x4,0xa,0x11]
display.create_char(0, stickperson)
display.message("\x00 is a custom\ncharacter.")
sleep(3)
display.clear()
for i in range(100):
    display.message(str(i))
    sleep(0.25)