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