Yeah, nah, aye

Main categories:

Do it properly: Text LCD on Raspberry Pi

Exposing an HD44780 as a Linux character device

The popularity of the Raspberry Pi has led to more widespread information on connecting various peripherals to embedded Linux systems, all of which varies in quality. One of my pet peeves is that a lot of it encourages use of userspace code to drive peripherals, treating their computers as just an expensive microcontroller that runs Python. This is the approach that a lot of guides for driving character LCDs take. Another common solution is to use a userspace daemon LCDproc. This is a more sane way of driving LCDs from userspace code that speaks TCP, but still poor form in my books. It means that userspace has to be sequenced correctly at boot and deal with the complications that TCP/IP brings, just to speak to the I²C bus on the local machine.

The correct way to use an LCD on an embedded Linux setup (including Raspberry Pi) is to describe it as part of the hardware setup using drivers that already exist (!) in the Linux kernel. There is no need to reinvent the wheel in userspace when there are drivers that have gone through review sufficient to get into the mainline kernel.

Describing hardware to the kernel is done in the Device Tree, sometimes in a Device Tree Overlay. In my experience, the Device Tree gets a bad rap, since it's often just something in the way of people playing with their peripherals. I think the power that it gives is often left unappreciated by many. In this short article, let's use it to expose our HD44780 as a character device in Linux. This way, userland software that wants to speak to humans through the LCD can simply open the device at /dev/lcd and perform normal stream writes on it as if it were a file.

I have written this article to try and fill the gap I found when looking for examples of driving the HD44780 through an I²C backpack from Linux using the existing kernel drivers for these components.

Everything presented in this article is based off information I have learned by Reading the Fine Manual, namely the Documentation directory of the Linux kernel source. Especially of interest to you will be Documentation/devicetree/bindings/.

Background

In order to set such an LCD up with the Device Tree, we first have to understand the types of interface between the kernel and the LCD panel itself, so we can describe these in the Device Tree correctly. Here is a quick diagram:

Kernel -->  I²C Bus --> I/O expander --> Controller --> LCD
                         (PCF8574)       (HD44780)

Let's start from the LCD side and work our way back.

LCD

The LCD driving is taken care of entirely by the HD44780, and exactly what is connected beyond the HD44780 is not something we need to concern ourselves with. We only care about driving the HD44780.

HD44780

The HD44780 is a ubiquitous controller IC for alphanumeric dot matrix LCDs. It's so ubiquitous that many (most?) of the cheap LCDs on eBay that you can order for a few dollars from China will have this controller (probably a clone) on them. The interface required to speak to LCDs equipped with this controller is generally about a dozen pins.

I²C "Backpack"

Driving a dozen pins straight from a SoC is generally feasible, but wastes a lot of close-to-home I/O pins and is generally avoided. Additionally, if an LCD is going to be mounted on a front panel of a device, it's going to be much nicer to try and route fewer than 12 lines to near the front of the PCB and/or run less than 12-conductor cable to the front of the chassis. This is where the so-named I²C Backpack comes in. It is quite common to find adapters (available separately, or included with the LCD) that will let you speak I²C to the HD44780 instead of 12-wide parallel.

These backpacks are most commonly some sort of I²C I/O expander, nothing special to LCD driving and actually quite common. A common variant of these I/O expanders used on these LCDs is the PCF8574. This chip will accept commands over I²C in order to read/write the state of its 8 GPIO pins. This means that the connection to the LCD and backpack requires only four wires; ground and VCC, as well as SCA and SDL for speaking I²C.

I²C Bus

This will vary from SoC to SoC. I am using the hardware I²C bus on a Raspberry Pi board, which is driven from the SoC. If you're not using the Raspberry Pi, consult your board or SoC manufacturer for information on configuring the bus.

Quick note on level shifting

A lot of these LCDs require 5 volts and include pullup resistors to VCC on the backpacks. Many microcontrollers are not 5-volt tolerant. Check your datasheets and use level shifters appropriate for I²C as necessary. I have had these LCDs working on a Raspberry Pi (3.3 V tolerant) by removing the pullups from the backpack on the SCA and SDL lines, but this is technically running the PCF8574 out of spec, since it isn't actually designed to register high until 70% of VCC, or 3.6 volts. I can only recommend running in-spec, using appropriate level shifters as necessary.

Configuring Drivers

The Linux Kernel includes drivers for each component; many SoC's I²C buses, the PCF8574 I/O expander, an the HD44780 LCD Controller. This is good news and means we don't need to compile in any out of tree drivers. I will be moving ahead assuming the Broadcom 2835 SoC, but the configuration is transferable to other SoCs with only changes to the I²C bus driver required.

Let's write some overlays.

PCF8574 I/O expander

Reading the PCF8574's datasheet, we see that it can be configurable to one of 8 I²C slave addresses using three external pullup or pulldown resistors. Examining the backpack on my LCD, there are unpopulated spots for each of these three resistors, meaning that the device address is configured on the board to 0x3f. If I were to attach multiple PCF8574 to the same I²C bus, I'd have to configure at least one of them so that they do not use this same address, and adjust the overlay's reg field to suit.

Reading the documentation for this type of device[1], we can glean what we need from the examples and adjust it to match our configuration:

fragment@0 {
    target = <&i2c_arm>;
    __overlay__ {
        #address-cells = <1>;
        #size-cells = <0>;
        pcf8574: gpio@3f {
            compatible = "nxp,pcf8574";
            reg = <0x3f>;
            gpio-controller;
            #gpio-cells = <2>;
        };
    };
};

This overlay declares a new device node which:

We also set #gpio-cells to 2 since this type of device needs GPIO pin declarations to have two cells; the pin number and the pin direction (input/output). This will become a little clearer later.

LCD Backlight

Now that we have out PCF8574 added to the devicetree exposed as a simple GPIO controller, it can be used by other software in the system just like any other node defining a device as a gpio-controller. A good introduction to driving GPIOs for a meaningful purpose from userspace is the LED device class, or (equally) a specialisation of it, the Backlight device class. Linux has a standard way of letting other software interface with backlights, so why not expose our LCD's backlight in the same way?

/* Front status LCD backlight */
fragment@1 {
    target-path = "/";
    __overlay__ {
        front-lcd {
            compatible = "gpio-backlight";
            gpios = <&pcf8574 3 0>;
            default-on;
        };
    };
};

Here, we declare a new device node which:

You could boot now with these overlay fragments compiled and overlaid onto the device tree, and you should see a backlight at /sys/class/backlight/front-lcd/. Tweak its brightness from 1 to 0 and see it turn on or off. Congratulations, you've just exposed a real-world backlight to embedded software in the most correct and elegant way possible!

Let's push on with the actual LCD panel.

LCD Controller

As mentioned, the HD44780 LCD Controller already has a driver in the Linux kernel. According to its documentation[2] we just need to declare a device node as being compatible with the same things it is, tell it which GPIOs it can reach the various lines of the LCD controller through, and tell it the dimensions of the screen attached to the controller.

The driver can also optionally control the LCD's backlight with escape sequences (common to all charlcd devices) to the character device that it exposes. If you want to use this feature, you will need to not expose the backlight as a Backlight device class, since GPIOs can only be held/consumed by single device nodes, in order to let people keep their sanity.

fragment@2 {
    target-path = "/";
    __overlay__ {
        lcd_screen: auxdisplay {
            compatible = "hit,hd44780";
            data-gpios = <&pcf8574 4 0>,
                         <&pcf8574 5 0>,
                         <&pcf8574 6 0>,
                         <&pcf8574 7 0>;
            enable-gpios  = <&pcf8574 2 0>;
            rs-gpios      = <&pcf8574 0 0>;
            rw-gpios      = <&pcf8574 1 0>;
            /* Backlight is already exposed by the backlight subsystem */
//          backlight-gpios = <&pcf8574 3 0>;

            display-width-chars = <16>;
            display-height-chars = <2>;
        };
    };
};

Here, we have declared a device node which:

The driver for the HD44780 is a member of a bit of an oddball device class; auxdisplay. It also exposes /dev/lcd as a character device for userspace to write to in order to update the LCD's displayed text. It also supports a set of escape sequences, and I can only recommend reading its documentation and source code to learn about these in more detail.

You can now boot with these three fragments compiled and overlaid onto your device tree, and enjoy a character LCD exposed in a sane manner by the kernel as soon as it boots. Write to it from Bash, Python, C, Perl, Ruby, anything in a standard manner. Treat it like a writable file and off you go.

Epilogue

Permissions, udev rule

You might find it useful to add a udev rule in order to set the permissions on the /dev/lcd device to, for example, give a system group write access to it without the need to elevate to root. Then, whichever piece of software needs to display on the LCD can do so if the user it runs under is added to this group.

LED device class

You might like to expose the LCD's backlight as an LED rather than a backlight. This is fairly simple by setting it compatible with gpio-leds instead. This way, you can take advantage of "LED Triggers" which are another excellent feature of the Linux Kernel. You could do wild things like use the backlight to show caps/num lock status, show load average with the heartbeat trigger or something more sane with your own custom-written LED trigger driver. While the triggers available out of the box with the kernel aren't too useful for driving backlights, it shows the power you have with the device tree to configure "basic building block" drivers to do what you want. No writing of kernel code necessary.

HD44780 driver features

Reading the HD44780 driver's source code, you will find that it outputs text to the LCD when the driver is probed (loaded), and when the machine is shut down or rebooted. These are two useful features that can let a technician more quickly evaluate the status of the system, even if nothing is actively updating the LCD. Without this, imagine if the system had been shut down or powered off and the LCD still showed old stale data, fooling a technician into thinking that whatever code normally updates the LCD is still running. This probe and shutdown behaviour is more annoying to achieve in userspace drivers.

Just by looking at the LCD, you can tell if the kernel has even booted yet, if your userspace software driving the LCD with something useful—presumably whatever software needs monitoring on the system—has started yet, and if the system has been powered off but the LCD is still powered up.


References

  1. "PCF857x-compatible I/O expanders", Linux Kernel source code, Documentation/devicetree/bindings/gpio/gpio-pcf857x.txt
  2. "gpio-backlight bindings", Linux Kernel source code, Documentation/devicetree/bindings/leds/backlight/gpio-backlight.txt
  3. "DT bindings for the Hitachi HD44780 Character LCD Controller", Linux Kernel source code, Documentation/devicetree/bindings/auxdisplay/hit,hd44780.txt