Programming an LCD

by James Nevius

I Introduction


This document details how I programmed a C167 to control a dot matrix LCD for UC Berkeley's EE40 robot competition.

II The LCD


The LCD is a HD44780-based Character-LCD.  It has 2 rows of 16 characters.  The LCD can display English letters, Arabic numbers, or even characters of your own design. 

III Interfacing the LCD

The first step in getting the LCD to work is figuring out which pins are where. 

The above is a shot of the LCD as it was finally mounted on the CalBot I built in 2001.  The data bus wire wraps are visible on top.

The LCD’s interface consists of 11 bits.  8 of these carry data (DB0-DB7), the remaining 3 are control bits (RS, R/W, E).

The RS (data/instruction select) bit tells the LCD whether we are going to send an instruction or a piece of data.

The R/W (read/write) bit tells the LCD whether we wish to read from or to write to RAM.

The E (enable) bit tells the LCD when it should read the data lines.

All of these bits need to be connected to different pins on the board.  I decided to use port 3 because I wanted port 2 available for controlling the LED's, and the rest of the ports didn't have 11 bits.  Having the LCD on just one port makes life pretty easy.

I connected RS to P3_0, R/W to P3_1, DB0-7 to P3_2 - P3_9, and E to P3_13.

Finally, the LCD must connected to +5V and ground.  I got a 5 volt regulator for this purpose.

IV Programming the LCD

Part 1 - Instructions

Now that the LCD is attached to port 3, we can start telling it what to display.  We will use two different functions: one for giving instructions, and one for giving data.

Let's first look at a data sheet for the LCD.  Page 5 gives the instructions that the LCD understands.

Before we send the LCD characters, we must initialize it.  The first initialization command is "Function Set".

DB7 and DB6 are zero, DB5 is 1.  DB4 indicates data length (in our case "1" to indicate 6 bits), DB3 selects number of display lines ("1" for 2 display lines), DB2 selects the display font (necessarily "0"), and DB1 and DB0 don't matter (call them "0").

It is easier to think about the hexadecimal equivalent than trying to keep track of all of the ones and zeros.  It is important at this point to understand how our board expresses ports.  Port 3 is a 16 bit port, meaning that there are 16 pins represented by 16 bits (P3_15 - P3_0).  The port can also be addressed as a 2 byte hexadecimal word: 0x0000 - 0xFFFF.  This provides a convenient way to either examine or set the contents of the port in one line of code.  Finally, it is important to note that the highest pin is the leftmost bit.  For example, P3 = 0x0010 sets P3_4 to 1 and everything else to 0, _not_ P3_8 to 1.

So what hex command do we want to send to board?  Remembering the above, we decided above we wanted to say

0b00111000

However, we wired RS and R/W to be the "lowest" (rightmost) pins, so we have to append their values to the right hand side.  Doing so gives us:

0b0011100000

Now we break it up into sets of four (from the right) and add up bits:

0b00 1110 0000
0x0      E       0             == 0xE0

So, we want to write 0xE0 to the LCD.

Of course, 0xE0 isn't the only thing we'll be writing to the LCD.  It would be useful, therefore, to write a function that takes data to be written as the argument and does the actual writing itself.

Take a good look at the "Write Operation" timing chart on page 4.  Go ahead - I'll wait.  Get it?... it took me and my partner two weeks to decipher.

Here's what it says:  First we set RS high or low (data/instruction) and R/W low (write).  Then we wait 140 ns.  Then we set E high.  Then we set our data pins.  Then we wait 80 ns.  Then we "flip" E from high to low.  Then we wait 10 ns and move on with our lives.

Let's do that.  To send an instruction, we want to set RS and R/W both low and wait.

P3 = 0x00;
ourdelay(50);

Then we set E high and wait.

P3 = 0x2000;
ourdelay(50);


Then we set our data pins by OR'ing the port with our data.

P3 = P3 | 0xE0;
ourdelay(50);


Finally, we flip E and wait.

P3 = P3 & 0xDFFF;
ourdelay(50);


There are a few more initialization commands to give.  We need to set the entry mode; I set my LCD to increment and not shift.  I also give the "Display ON/OFF Control" command to decided whether or not to have a cursor.  I use the cursor when developing code for the LCD; it comes in very handy when you miscount and are wondering where the cursor is.  I turn it off when the LCD code is finished and I'm showing off to my friends.  Finally, I clear the display.  It is worth noting that this command takes much longer than most.  You must make sure you wait long enough after clearing the display, or both you and the LCD will end up very confused.

Part 2 - Data

Now that are our LCD is initialized and ready, let's write something!

First we'll have to look at the characters the LCD knows:

Our first character is a dash ("-").  Scanning the table above, we see that there's a dash in the second column, third from bottom:  0b00101101 = 0x2D.  Then we'll need a space, 0x20, and a "C", 0x43, and so on.

These hexadecimal values are what we want to send on the data lines.  Because we are writing data to the LCD, we know RS will be 0 and R/W will be 1.  You can write a function that will take the hex value we want to write as an argument and deal with the other bits itself.

V Tips and Tricks

- The LCD has 16 characters per row.  Unfortunately, the cursor doesn't go to the beginning of row 2 after writing the final character of line 1.  You can get to row 2 after writing the last character to row 1 by giving the "Cursor Shift Right" command 23 times.  A similar thing can be done to get to the beginning of row 1 from row 2.

- Even though the hardware docs say that port 3 can be used entirely for digital output, this isn't always the case.  Some of the upper bits of Port 3 are also used by the serial port controller.  Changing their value while debugging causes some boards to freeze entirely.

Say we want to set bit 2 to equal 1.  The command P3=0x0004 will accomplish this.  However, it will also force all other bits to be 0.

We can change the bits we care about without affecting the others by using masks.

A quick review of logical AND and OR identities:

x OR 1 = 1
x OR 0 = x
x AND 0 = 0
x AND 1 = x

Say we want to set bit 2 to equal 1, and leave all other bits alone.  To do this we OR port 3 with 0x0004: P3 |= 0x0004.  Bit 2 is OR'd with 1, so it is set high.  The rest are OR'd with zero, and are therefore unmodified

To force a value to 0, we use AND's.  For example, to force bit 2 to 0, we say:  P3 &= 0xFFFC

If your board freezes when you execute a P3= instruction, try changing it to a mask instruction.

- If you don't put any delay (or not enough delay!) between the instructions you send to the LCD, it won't work.  Trust me.  Although the board has some very sophisticated and very precise general purpose timers, a quick-and-dirty can sometimes do the trick.  I wrote a function that consisted of an empty for loop, and took an argument to determine how many times to go through it.

If you decide to do this, you can figure out how long the board takes to execute an empty for loop by having an LED be on for a certain number of loops and off for an equivalent number.  Probe the LED with the oscilloscope and look at the period.

There's a lot that can be done with an LCD.



Above is a photo of our final bot.  The top row is a bar graph of ambient light.  The bottom row displays information about the bot's reactions.

If you have any questions, feel free to email me.

Happy engineering!

20030225