Wednesday 16 June 2010

A Python LCD status display

This is the third in my series of blog posts about doing IO with FTDI's Async BitBang mode on the UM245R / UM232R eval boards, and this time we're actually going to do something useful - have an LCD update with interesting system information. The LCD in question is based on the ubiquitous HD44780 controller, which is interfaced to microcontrollers throughout the world...

Anyway, with only 8 IO lines on the UM2xxR boards, we need to use the 4bit interface mode (two extra IO lines are needed beyond the 'data' path - one to act as a 'data-ready' strobe, and the other to select between 'data' and 'commands'). The wiring up is basically DB0-DB3 on the FTDI device going to D4-D7 on the LCD, with DB6 on the FTDI going to the 'RS' (register select) line on the LCD, and DB7 to the 'E' strobe. If that makes no sense, then hopefully the photo makes things slightly clearer...

I've also got an LED backlight for my display, which makes it look a whole lot cooler :-)

I'll present the code in chunks and try to explain it as I go along. First the initialisation and shutdown code, which are fundamentally unchanged from the previous examples, though they are now in functions to make them just a little tidier. (Note there is still no error checking here...)
"""
Write a string (argv[1] if run from command line) to a HD44780
LCD module connected via a FTDI UM232R/245R module

example usage:

# while true;
>   do python lcd.py $( awk '{print $1}' /proc/loadavg);
>   sleep 5;
> done
"""

from ctypes import *
import time, sys

def ftdi_start():
    global ctx, fdll  # frown... :P
    fdll = CDLL('libftdi.so')
    ctx = create_string_buffer(84)
    fdll.ftdi_init(byref(ctx))
    fdll.ftdi_usb_open(byref(ctx), 0x0403, 0x6001)
    fdll.ftdi_set_bitmode(byref(ctx), 0xFF, 0x01)

def ftdi_end():
    fdll.ftdi_usb_close(byref(ctx))
    fdll.ftdi_deinit(byref(ctx))
The following class is an abstraction of a bus - a collection of one (probably two, technically...) or more electrical lines which should be treated as a single unit. The aim here is to be able to program in a similar style to the embedded programming on a microcontroller, where registers are typically memory mapped and writing to a bus is simply writing into a bitfield. The parameters of this abstraction are the width of the bus (in bits), and the offset from the LSB of the entire port being accessed. It also needs a reference to a driver which allows it to do the reading and writing to the port. By using this as a descriptor, we can define buses within classes representing the various devices we are using; in this case the LCD.
class Bus(object):
    """
    This class is a descriptor for a bus of a given width starting
    at a given offset (0 = LSB).  It needs a driver which does the
    actual reading and writing - see FtdiDriver below
    """
    def __init__(self, driver, offset, width=1):
        self.offset = offset
        self.width = width
        self._mask = ((1<<width)-1)
        self.driver = driver

    def __get__(self):
        val = self.driver.read()
        return (val >> offset) & self._mask

    def __set__(self, obj, value):
        value = value & self._mask
        # in a multi-threaded environment, would
        # want to ensure following was locked, eg
        # by acquiring a driver lock
        val = self.driver.latch()
        val &= ~(self._mask << self.offset)
        val |= value << self.offset
        self.driver.write(val)
The following is the driver which will be used to do the actual data access. Note the use of the latch to store the last value written to the port, which cannot generally be read from the device after having been written (Latch registers for the IO ports was a big advance for the PIC18F series over the earlier 16F series, which needed the application to store this separately in order to do read-modify-write operations properly on the IO ports).
class FtdiDriver(object):
    def __init__(self):
        self._latch = 0

    def read(self):
        z = c_byte()
        fdll.ftdi_read(byref(ctx), byref(z), 1)
        return z.value

    def latch(self):
        return self._latch

    def write(self, val):
        self._latch = val
        z = c_byte(val)
        fdll.ftdi_write_data(byref(ctx), byref(z), 1)
        # the following is a hack specifically to allow
        # me to ignore all the timing constraints of the
        # LCD.  For more advanced LCD usage, this wouldn't
        # be acceptable...
        time.sleep(0.005)
Now we've got a Bus class and a driver to use with it, we can define the LCD module interface. I'm not going to cover the details of the interface, but the wikipedia HD44780 page has some pointers.
# need to instantiate this in global context so LCD
# class can be defined. Could tidy this up...
ftdi_driver = FtdiDriver()

class LCD(object):
    """
    The UM232R/245R is wired to the LCD as follows:
       DB0..3 to LCD D4..D7 (pin 11..pin 14)
       DB6 to LCD 'RS' (pin 4)
       DB7 to LCD 'E' (pin 6)
    """
    data = Bus(ftdi_driver, 0, 4)
    rs = Bus(ftdi_driver, 6)
    e = Bus(ftdi_driver, 7)

    def init_four_bit(self):
        """
        set the LCD's 4 bit mode, since we only have
        8 data lines and need at least 2 to strobe
        data into the module and select between data
        and commands.
        """
        self.rs = 0
        self.data = 3
        self.e = 1; self.e = 0
        self.e = 1; self.e = 0
        self.e = 1; self.e = 0
        self.data = 2
        self.e = 1; self.e = 0

    def _write_raw(self, rs, x):
        # rs determines whether this is a command
        # or a data byte. Write the data as two
        # nibbles. Ahhh... nibbles. QBasic anyone?
        self.rs = rs
        self.data = x >> 4
        self.e = 1; self.e = 0
        self.data = x & 0x0F
        self.e = 1; self.e = 0

    def write_cmd(self, x):
        self._write_raw(0, x)

    def write_data(self, x):
        self._write_raw(1, x)
All that remains is to initialise the FTDI device, initialise the LCD module, and write some data to it.
def display(string):
    ftdi_start()

    lcd = LCD()
    lcd.init_four_bit()

    # 001xxxxx - function set
    lcd.write_cmd(0x20)
    # 00000001 - clear display
    lcd.write_cmd(0x01)
    # 000001xx - entry mode set
    # bit 1: inc(1)/dec(0)
    # bit 0: shift display
    lcd.write_cmd(0x06)
    # 00001xxx - display config
    # bit 2: display on
    # bit 1: display cursor
    # bit 0: blinking cursor
    lcd.write_cmd(0x0C)

    for i in string:
        lcd.write_data(ord(i))

    ftdi_end()


if __name__ == '__main__':
    # note blatant lack of error checking...
    display(sys.argv[1])
and there we have it; a slightly cumbersome but also slightly cool and slightly useful little display utility. In the spirit of UNIX programming, this only does one thing - display the command line argument on the LCD. Obviously it needs major error handling if robustness is required...
while true; do python lcd.py $( awk '{print $1}' /proc/loadavg); sleep 5; done


3 comments:

  1. Ben, when did you get so clever? Marc S

    ReplyDelete
  2. Very nice post! I would be appreciate if you
    show how to connect UM245R and HD44780?

    Cheers,
    B.

    ReplyDelete
  3. Thanks for your comment :)

    I'll get a diagram done in a day or two (probably with a new blog post), but basically:

    (UM245R pin name -> 44780 pin #)
    ----------------------------
    GND -> #1
    USB -> #2 (power)
    GND via 1K resistor -> #3 (LCD vref)
    DB6 -> #4
    GND -> #5
    DB7 -> #6
    DB0-> #11
    DB1 -> #12
    DB2 -> #13
    DB3 -> #14

    ReplyDelete