« Back to home

Controlling a HD44780 LCD Display

The Clixx.IO system gives you at most 3 GPIO pins on a single tab (a digital TwinTab peripheral). In most cases this is enough but there are times when you need more than 3 pins to control a specific piece of hardware. I've already talked about extending the number of pins available for a single slot by using an I2C controlled IO expander - this post gives another example.

16 x 2 LCD Display

In this post I'll look at another common use case - controlling a 16 character by 2 line LCD screen. These displays are widely available and fairly popular, you can see an example of interfacing one to the GPIO pins of a Raspberry Pi on this page.

This interface described in this post provides much the same functionality as the page linked to above but uses the MCP23008 IO expander to control it over I2C rather than connect directly to the digital pins on the Pi.

Display Description

As I mentioned, these displays are very popular and widely available so there is a lot of information available on how to use them. Almost all of them tend to use the HD44780 controller chip so that is a good search term to start with. For a good overview of the device have a look at this page which provides an excellent description of how to interface the devices to any microcontroller.

There are a number of varieties of the LCD available with different numbers of lines and characters per line - the one I am using has two lines of 16 characters each. The interface protocol is the same for all of them though so the information presented here is valid for all of them.

Each display comes with either a 14 pin or 16 pin interface as described in the table below. The 14 pin versions will have an additional two pins to control the backlight - I'm not using those in this example so if you have a 14 pin version you can still use the circuit and code provided.

Pin |Description ----|----------- 1 |Ground 2 |Power (+5V) 3 |Contrast Control (see note below) 4 |R/S Register Select ( 1 for Data Write, 0 for Command Write) 5 |R/W Read/Write (1 for Read, 0 for Write) 6 |Device enable (A high pulse will latch command or data) 7-14|Data Pins (D0-D7), in 4 bit mode only D4-D7 are used 15 |Backlight Anode (optional) 16 |Backlight Cathode (optional)

4 Bit and 8 Bit Mode

The controller chip supports both a 4 bit data mode and an 8 bit data mode for communication. In the 4 bit mode only the most significant 4 bits of the data pins are used. As there are a handful of other pins that need to be controlled (device enable, R/W and R/S) this post will cover the 4 bit communications mode so we can control everything from the 8 bits of the IO expander.

Controlling the Backlight and Contrast

The backlight and contrast are controlled by varying the voltage applied to the appropriate pins. In this example I am going to ignore the backlight completely (it will just be off) and tie the contrast pin to ground.

It is possible to drive both of these using PWM with some caveats:

  1. For the contrast you will need to use a capacitor as a filter on the PWM output so you don't get flicker on the display.
  2. The backlight will need to be driven through a transistor or FET as the current required will most likely exceed the amount that can be provided from a digital output pin.

    Using PWM to control these aspects will give you software control over everything on the display which is a nice feature to have. Because this post is mostly a proof of concept project rather than a fully fledged design I'm just going to ignore them in favour of simplicity.

The Circuit


The circuit (shown to the left) is very simple. Because there are a few extra discrete components required we can't simply cable it directly to the IO expander Tab, we will have to go via a breadboard.

Essentially we are connecting D4 through D7 of the LCD to D0 through D3 of the IO expander and using D4 to control the ENABLE line and D5 to control the RS (register select) line. The contrast line is tied to ground as is the RW line is tied to ground (we only ever write to the display, we don't read from it).


Because we are using a Raspberry Pi all the outputs (and the voltage lines on the the slot) are at 3.3V levels - the LCD requires 5V so the VCC line will need to be connected to the 5V line on the Pi rather than the 3.3V power pin on the MCP23008. The signals are fine to operate at 3.3V though - the display will interpret them correctly. You can see an image of the final prototype to the right. The layout is a little bit messy but it's enough to prove the functionality.

The Software

The software is fairly simple as well. I used a similar initialisation sequence as the one described on this page but control the pins through the MCP23008 instead of through direct IO pins.

As with the previous post about the IO expander I used the WiringPi I2C functions.

Communication with the LCD is wrapped in a Python class, it's simply a matter of creating a new instance and then using the methods provided to modify what is displayed. To simplify communications the class maintains a frame buffer that contains all 32 characters and sends the entire frame to the LCD when it has been modified. This is far from the most efficient way to do things but it does simplify communications.

The full Python program is listed below. To run it you will need to have gone through the setup process as described in the previous post to enable I2C on the Raspberry Pi. Save the script to a file called raspi_lcd.py and then run it as the root user using sudo, eg:

sudo raspi_lcd.py  

Here is the Python code:

#!/usr/bin/env python
# Description
from time import sleep  
import wiringpi2

class Display:  
  """ This class wraps control of the display and provides a simple set of
      methods to manage output.

  # Delay times
  DELAY = 0.0005
  PULSE = 0.0005

  # MCP23008 Register Numbers
  IODIR = 0x00
  IPOL  = 0x01
  GPPU  = 0x06
  GPIO  = 0x09

  # Command bits
  LCD_ENABLE  = 0x10   # Enable line
  LCD_COMMAND = 0x20   # Command/Data select
  LCD_DATA    = 0x0F   # Data bits

  # Addresses
  LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line
  LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line

  def __init__(self, device):
    """ Constructor
    self.device = device
    self.lines = [
      [ " " ] * 16,
      [ " " ] * 16
    self.where = [ 0, 0 ]

  # Internal helpers

  def _writeLCD(self, value, cmd = False):
    """ Write command or data to the LCD
    # Make Sure "EN" is 0 or low
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, 0x00)
    # Set "R/S" to 0 for a command, or 1 for data/characters
    out = 0x00
    if not cmd:
      out = out | self.LCD_COMMAND
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Put the HIGH BYTE of the data/command on D7-4
    out = out | ((value >> 4) & self.LCD_DATA)
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Set "EN" (EN= 1 or High)
    out = out | self.LCD_ENABLE
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Wait At Least 450 ns!!!
    # Clear "EN" (EN= 0 or Low)
    out = out & ~self.LCD_ENABLE
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Wait 5ms for command writes, and 200us for data writes.
    # Put the LOW BYTE of the data/command on D7-4
    out = (out & ~self.LCD_DATA) | (value & self.LCD_DATA)
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Set "EN" (EN= 1 or High)
    out = out | self.LCD_ENABLE
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Wait At Least 450 ns!!!
    # Clear "EN" (EN= 0 or Low)
    out = out & ~self.LCD_ENABLE
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO, out)
    # Wait 5ms for command writes, and 200us for data writes.

  def _update(self):
    """ Update the display with the contents of the buffer
    self._writeLCD(self.LCD_LINE_1, True)
    for ch in self.lines[0]:
      self._writeLCD(ord(ch), False)
    self._writeLCD(self.LCD_LINE_2, True)
    for ch in self.lines[1]:
      self._writeLCD(ord(ch), False)

  # Public API

  def setup(self):
    """ Set up the connection to the device
    # Set up the IO expander
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPIO,  0x00) # Clear outputs         
    wiringpi2.wiringPiI2CWriteReg8(dev, self.IODIR, 0x00) # Direction         
    wiringpi2.wiringPiI2CWriteReg8(dev, self.GPPU,  0x00) # Pull ups         
    wiringpi2.wiringPiI2CWriteReg8(dev, self.IPOL,  0x00) # Polarity
    # Initialise the display in 4 bit mode
    self._writeLCD(0x33, True)
    self._writeLCD(0x32, True)
    self._writeLCD(0x28, True)
    # Set up initial state
    self._writeLCD(0x0C, True)
    self._writeLCD(0x06, True)
    self._writeLCD(0x01, True)

  def gotoXY(self, x, y):
    """ Move the cursor to the given position
    self.where = [ x % 16, y % 2 ]

  def write(self, text):
    """ Write text to the current position
    for ch in text:
      self.lines[self.where[1]][self.where[0]] = ch
      self.gotoXY(self.where[0] + 1, self.where[1])
    # Refresh

  def clear(self):
    """ Clear the display
    self.lines = [
      [ " " ] * 16,
      [ " " ] * 16

# Main program

if __name__ == "__main__":  
  # Set up WiringPi and connect to the IO expander
  dev = wiringpi2.wiringPiI2CSetup(0x20)
  if dev < 0:
    print "ERROR: Could not connect to device!"
  # Now create the LCD interface
  lcd = Display(dev)
  lcd.gotoXY(0, 0)
  lcd.write("This is a sample")
  lcd.gotoXY(7, 1)
  lcd.write("It works!")
  lcd.gotoXY(0, 1)
  lcd.write(" " * 16)
  padding = ""
  while True:
    lcd.gotoXY(0, 1)
    lcd.write(padding + "It works!")
    padding = padding + " "
    if len(padding) > 16:
      padding = " "


This post shows a second example of using a Clixx Tab to communicate with components that require more than 3 GPIO pins, as you can see it is not exceptionally difficult to do - using the Clixx system does not restrict you from using arbitrary hardware in your project.

Clixx does, however, encourage you to use (and design) modular components that can be shared across projects and with different host systems. The example I've described here would be better implemented using a small microcontroller to handle the low level interaction with the display and connect to the Clixx host as an I2C module with higher level API - hiding the details of how the display works from your main application.

There are numerous benefits to this:

  1. You can reuse the component in a number of projects without having to write the display initialisation and control code every time.
  2. If you need to change the type of display (from a small LCD to a large LED wall display for example) you can keep the same API and still use your existing application code.
  3. Using a small microcontroller would give you additional control over other aspects of the diplay - software controlled backlight and contrast through PWM for example.
  4. You could create a full user interface module including push buttons for input and a status LED. This could then be plugged in to any other project that requires it without having to reinvent the wheel each time.

    In fact this would be a perfect project for a small 18 pin PIC processor like the 16F1827 - an excellent example of the use case for the PIC that I described in Where are the PIC Projects?.