EE109 – Spring 2017 Introduction to Embedded Systems

EE109 – Spring 2017: Introduction to Embedded Systems

Lab 5

Using the LCD Display

Introduction

In this lab exercise you will attach an LCD display to the Arduino Uno and write software to display short messages on it. The steps required to communicate between the microcontroller and the LCD display are typical of how most devices are interfaced to a microcontroller.

Background Info

The LCD Module

The LCD display used in this exercise is a monochrome, 16 character by 2 line display that uses a parallel interface. LCD displays are designed with either a serial or parallel interface, and both types have advantages and disadvantages. A serial interface transfers data one bit at a time and requires only one or two I/O lines from the microcontroller but is slower and often more expensive. A parallel interface display transfers data multiple bits at a time and can usually transfer data faster but requires multiple I/O lines from the microcontroller in order to operate.

The LCD is called an "LCD shield" since the display module is mounted on a board that plugs directly into the sockets on the Arduino Uno so it doesn't require any wires to be run between the Arduino and the LCD. Once mounted on the Arduino it uses six of the Uno's I/O lines for data and control signals (Arduino I/O pins D4-D9).

The LCD shield also has a cluster of six pushbuttons. Five of these are interfaced through a multistage voltage divider to ADC channel 0 of the Arduino. Depending on which of the five buttons is pressed, or none, one of six analog voltages appears on the ADC Channel 0 input. By using the ADC to convert the voltage to a number it's possible to easily determine if one of the five buttons was pressed. The sixth button, marked "RST" is connected to the RESET line of the Arduino and can be used to restart the program.

The LCD Interface

The parallel interface to the LCD is used to transfer data between the microcontroller and a special integrated circuit (IC) in the LCD module that controls where the dark and light spots appear on the display. The presence of this IC allows the microcontroller to display the letter 'A' by just sending the ASCII code for an 'A' (0x41) rather than having to send information about the individual dots that make up the letter on the screen. The circuit used on this module (Hitachi HD44780) is one of the most commonly used for controlling LCD modules and is designed to communicate with a microcontroller using eight data lines and three control lines. However in order to save I/O lines it can also communicate in a 4-bit mode that sends an eight bit byte in two 4-bit "nibbles", first the most significant four bits, then the least significant four bits. The four bit mode is used with this LCD shield.

In addition to the four data lines, the controller IC has three control lines: RS (Register Select), RW (Read/Write) and E (Enable). On the LCD shield the designers have eliminated the the RW line so data can only be written to the shield. The result of all this is that the shield can be used with just six I/O lines, four for data and two for control signals.

Command/data bits 4 through 7 (Arduino I/O pins D4-D7 = PORTD, bits PD4-PD7)
These lines carry four bits of the command or data bytes that are sent to the LCD. Most operation require sending a full byte so transfers have to be done in two steps.
Register select (Arduino I/O pin D8 = PORTB, bit PB0)
Set to 1 to write to the data register. Clear to 0 to write to the command register.
Enable (Arduino I/O pin D9 = PORTB, bit PB1)
All writes to the data or command registers are done by making the Enable line do a $0\rightarrow1\rightarrow0$ transition.

Command and Data Registers

Inside the controller IC on the LCD module are two 8-bit registers, the command register and the data register. The Register Select control line is used to determine which register the microcontroller transfers data to.

When the microcontroller wants the LCD to perform certain operations it writes the necessary 8-bit command code into the command register to initiate the operation. Typical commands include clearing the display, moving the cursor to some location, turning the display on or off, etc.

The data register is where the program writes the ASCII character codes for the characters that it wants to have displayed on the LCD. When a character is written to the data register it will appear on the display at the location that the cursor was at, and then cursor will move to the right one position. Writing a string of characters to the data register (one after the other) has the effect of making the string appear on a line of the display with the cursor positioned after the last character in the string.

Command and Data Transfers

Transferring commands and data to the LCD involves a number of steps all involving setting or clearing bits in the six I/O port bits (PORTD[7:4], PORTB[1] (E) and PORTB[0] (RS) ) that are connected to the six interface lines described above. The LCD shield does all its data transfer in the 4-bit interface mode. In some cases only 4 bits have to be transferred, in others a full 8-bit must be transferred.

Important: After an entire 8-bit data or command byte is sent a short delay is needed. A 2msec delay should be sufficient. For 8-bit transfers, it is not necessary to delay between transferring the upper and lower parts of the byte, but there should be a delay after the second part of the transfer.

Task 1: Test the LCD Shield

Install the LCD shield on the top of the Arduino Uno. Make sure that you are lining up the pins and connectors properly before trying to push them together. Two of the male headers on the shield are the same size as the mating connectors on the Uno and these go into the D0-D7 and A0-A5 connectors. The other two male headers have fewer pins than the connectors they are plugged into. Take care to make sure that all the pins are going into the correct openings before applying pressure to push the two boards together. The LCD shield should be seated all the way into the sockets. There should be little or no gap between the sockets on the Arduino and the bottom of the black connectors on the LCD that the pins stick out of. If you have problems mounting the shield on the Uno bring the boards to one of the instructors for help.

Create a lab5 folder in your ee109 folder. From the class web site download the file lab5.zip, extract the files and put them in the lab5 folder. You should have these three files for Lab 5:

Makefile
This is a modification of the normal Makefile to allow testing of the LCD shield
test.hex
The binary test program for the LCD shield
lab5.c
Template file containing functions declarations to get you started

Make the usual changes to the PROGRAMMER line at the top of the Makefile to make it work on your computer. Attach your Uno+LCD to your computer and enter the command "make test" to download the data from the test.hex file to the Uno. Once the download is complete the LCD should show two lines of text as shown below.

If nothing shows up on the screen or it shows a lot of white boxes, try changing the display contrast by using your screwdriver to adjust the potentiometer in the upper left corner of the display. If you can't get the test program to work, ask one of the instructors or TAs for help. Don't try working on the rest of the lab assignment until the test program is working.

Task 2: Write the LCD Functions

This lab exercise is a good example of how programs can be structured so the complexities of one part are hidden from other parts of the program. As much as possible the details of how the LCD module works should be handled by only a small portion of the code. Most of the program should not have to deal with knowing which I/O port bits are being used, setting the control bits, dealing with delays, etc., for each character it wants to display. To put a character on the display, the program just has to call a function that has the character to be displayed as its argument. All the details about how that character gets transferred to the LCD should be isolated in another part of the program. Doing this has an additional advantage that the LCD could be changed for a model with a different interface, and only the small number of routines that deal directly with the interface will have to be changed.

Our LCD software will be designed with the three layers described below:

Low level functions:

The low level functions handle the changing of the bits in the I/O ports connected to LCD. Most of the "bit fiddling" will be done at this level. The primary low-level function will be sending a 4-bit "nibble" to the LCD and creating the Enable signal transtion from 0 to 1 back to 0. You will write these this in this lab.

Mid level functions:

The mid level functions deal with sending commands and data to the LCD by making use of the low level functions. These functions transfer one byte each time they are called. You will write these this in this lab.

Top level functions:

The top layer has a small number of simple functions (initialize LCD, position the cursor, write a string of characters) that make use of the mid level functions. The main part of the program only calls the functions in the top layer. We'll write most of these functions next week. For this lab you'll only write init_lcd()

The lab5.c file has the skeleton routines for all the functions listed below. Start with
that file and add code to implement the functions.

Low Level Functions

The low level function "writenibble" transfers a four bit value from the 8-bit (unsigned char) argument to the LCD. Assume RS has already been set to 0 or 1 OUTSIDE of this function. Don't set it inside this function.

void writenibble(unsigned char lcdbits)
{
    /* Send four bits of the byte "lcdbits" to the LCD */
}

All transfers of data and commands depend on this function to do the actual transfer. Data is only transferred to the LCD when the E (Enable) signals makes a $0\rightarrow 1 \rightarrow 0$ transition, and the writenibble routine is the only function that changes the state of the E line.

Note that it is very important that "writenibble" set an expectation as to which 4-bits of its 8-bit argument it will send to the LCD (i.e. place on PD[7:4]). It is likely easiest to copy the upper 4-bits of the lcdbits argument to PORTD[7:4], and then cause the Enable (PB1) to transition from 0 to 1 back to 0.

Try to write this function now.

Important: Your writenibble routine must only change the bits in registers B and D that need to be changed in order to affect the transfer. Don't just copy a full byte into a register if you only need to modify a few bits. Any bits that are not part of the transfer should not be changed. Reference your Unit 7 slides on Copying bits to see how to do this. You will lose points if you modify any other bits not associated with the LCD.

Mid Level Functions

Two mid level functions are used to send a byte to the command register and the data register.

void writecommand(unsigned char cmd)
{
    /* Send the 8-bit byte "cmd" to the LCD command register */
}

void writedata(unsigned char dat)
{
    /* Send the 8-bit byte "dat" to the LCD data register */
}

These routines must set the register select line to the correct state for a command (RS=0) or data (RS=1) transfer, and then make two calls to the low level "writenibble" function. Recall that we decided that writenibble would always transfer the upper 4-bits of its argument. So to transfer 8-bits we first send the upper four bits of the byte (normal call to "writenibble"), and then send the lower 4 bits. However to send the lower 4-bits we will need to move the lower 4-bits of the data we want to transfer in our current function into the upper 4-bit area of the argument we pass to "writenibble" (since "writenibble" expects the 4-bits it is supposed to transfer to be in that upper 4-bit area). Use an appropriate shift operator >> or << to do this as you pass the argument. The picture below shows this concept:

After an 8-bit transfer is complete, the function should delay for about 2msec to let the operation finish.

Note: We could have written "writenibble" to use the lower 4-bits of its argument and then changed the mid-level functions appropriately. There is no real advantage to doing it one way or the other. The only important thing is that the three functions agree on which to use.

So try to code the two mid-level functions by performing the following:

  1. Configure the RS (Register Select) line to determine the destination of the data transfer. RS = 0 indicates a command transfer. RS=1 indicates a data transfer.
  2. Call the writenibble procedure to write the upper four bits of data (bits 7-4)
  3. Move the lower 4-bits of the desired information to the upper 4-bit area
  4. Call the writenibble procedure to now write these bits to the LCD
  5. Wait 2 msec

Top Level Functions

The top level routines are to initialize the LCD and to write strings of characters starting at specified locations. These routines should make use of the functions defined in the mid level (and if necessary the lower level) of the program. Task such as moving the cursor to a given location on the screen, writing a string of ASCII characters, etc. are common top-level tasks. In this lab you will only code the following function:

void init_lcd()
{
    /* Configure the I/O ports and send the initialization commands to the LCD */
}

Unfortunately you can't just start sending character data to the LCD and have it appear. The module has to have a few initialization steps performed before it will accept data and display it. All of these steps are simply commands that must be sent to the LCD in the same way as other commands. In most cases a delay of some specified amount must be done after the command is sent. Delays can be implemented using the "_delay_ms" and "_delay_us" functions. The following sequence of commands and delays must be performed in order to get the display working. Code these operations up in your init_lcd function.

  1. Delay at least 15msec after power on and program running
  2. Send the 4-bit command 0011, followed by a delay of at least 5msec.
  3. Send the 4-bit command 0011, followed by a delay of at least 100$\mu$sec.
  4. Send the 4-bit command 0011, no delay needed.
  5. Send the 4-bit command 0010 to set the module to use 4-bit interfacing. Delay 2ms.
  6. Send the 8-bit command 00101000 to set the module for 2-line display. Delay 2ms.
  7. Send the 8-bit command 00001111 to turn on display and turn on a blinking cursor. Delay 2ms.

Important Note: For the steps that ask you to send 4-bit commands take care to get those 4-bits into the appropriate area (likely the upper 4-bit area) before you call the WriteNibble procedure.

After the above steps are done the display is ready to accept data to display. If you now write data to the data register it should appear on the screen starting in the upper left position.

All of the operations necessary in init_lcd to initialize the LCD are shown in the skeleton lab5.c file. You just need to fill in the code that is described by the comments in the routine. Important: Your init_lcd should only configure the DDR bits for the Port bits being used by the LCD. Don't modify any other DDR bits.

The following two functions for moving the cursor and writing a string of characters are in the lab5.c file as shown below but we won't be finishing these until next week. For now just ignore them in the lab5.c file.

void moveto(unsigned char row, unsigned char col)
{
    /* Move the cursor to the row (1 to 2) and column (1 to 16) specified */
}

void stringout(char *str)
{
    /* Write the string pointed to by "str" at the current position */
}

Displaying Characters

The LCD display uses the ASCII character codes to determine which characters to display. Once the LCD has been initialized as shown above, any byte written into the data register will result in the character with that ASCII code being displayed on the LCD and the cursor that indicates where the next character will go is advanced one position to the right. For example if the bytes with values 0x55, 0x53, 0x43 are written to the data register one after the other, the character ``USC'' will appear on the screen. Note: if you write more characters than will fit on a line, it doesn't automatically wrap to the next line.

Task 3: Check the Enable Signal

The LCD requires that the Enable or E signal be at least 230ns long. To check that your code has satisfied this requirement do the following.

  1. Disconnect the LCD panel from the Arduino by gently pulling it out of the sockets. Have one of the instructors help you with this if you are concerned about breaking the LCD.
  2. At the top of main() call your init_lcd() function.
  3. In the while(1) loop of the main routine in lab5.c, insert code to call the writenibble function.
    while (1) {
        writenibble(0);
    }
    

    This will cause the writenibble routine to be called repeatedly and generate the E pulse over and over.

  4. Compile the code and download it to the Arduino. Remember to use "make flash" to download your program. The "make test" is only used to download the test program in the test.hex file.
  5. Turn on the oscilloscope and connect one of the probes to channel 1.
  6. Set the triggering to trigger on channel 1, a rising edge, and a trigger level of about 2 volts.
  7. Using a short piece of wire, hook the probe to the Port B, bit 1 output (D9 on the Arduino). Don't forget to also connect the probe's ground wire to ground on the Arduino.
  8. Adjust the scope setting to allow you to measure how long the E pulse is in the 1 state.

If the pulse is not at least 230ns long you can extend the pulse by adding additional instructions to your writenibble function. For example the code below sets Port B, bit 1 to a one, and then sets it to a one a second time before it's cleared to a zero.

PORTB |= (1 << PB1);        // Set E to 1
PORTB |= (1 << PB1);        // Make E longer
PORTB &= ~(1 << PB1);       // Set E to 0

The second line has no effect on the PB1 output signal since it is already in the 1 state, but it extends the time the signal is high before the third line clears it to a zero.

If necessary, add one or more of these extra instructions to your code and observe the results on the scope. Add enough delay that the E signal is in the 1 state for at least 230ns.

Task 4: Build a One Digit Up/Down Counter

Put your LCD back on the Arduino for the rest of the lab assignment.

Now that you have the LCD routines written and have confirmed that the E signals meets specifications, we want to build a circuit that does the following.

The Circuit

From the instructor get

Install the push buttons on your breadboard and connect them as shown in the schematic below. The schematic shows both the internal ATmega328P names of the port bits (e.g. PC2) and the corresponding name marked on the Arduino board (A2). The switch inputs are on Arduino bits A2 and A4 which are connected to the microcontroller's I/O bits PC2 and PC4.

Displaying the Count Value

The LCD displays ASCII characters. If you use writedata to send it the value of your count variable (e.g. 2) you will get garbage on the screen since 2 is not the ASCII code for the digit 2. To display the count value you have to convert it to an ASCII character code.

If you look at an ASCII chart it shows that the ASCII code for the digits '0' through '9' are 0x30 through 0x39. So to display a digit, just add 0x30 to it and call writedata with this value as the argument.

For example:

unsigned char cnt = 5;
writedata(cnt + 0x30); // '5' will appear 
writedata(cnt + '0');  // '5' will also appear because '0'=0x30

If you just keep calling writedata each time the count changes, the characters will just march across the LCD: "0123456789012345". We need to move the cursor to the same location before each time writedata is called so the count values stay in the same place. The simplest way to do that is to send a command to the LCD that makes it clear all the character positions. This also moves the cursor to the first position on the first row. To clear the screen, call the writecommand function with an argument of 1.

writecommand(1);    // Clear screen and move to row 1, column 1

After the screen has been cleared, the program can call writedata to display the count character in the first position on row 1.

A State Machine

To build our up/down counter we will implement it using the principles of a state machine. A state machine saves information about previous inputs by storing that information as the machine's "state". The state information can then be referenced by the machine as it operates to determine what to output next, and perhaps, which state to transition to next.

For this up/down counter, you should define a state variable that stores the direction that counter is counting. Whenever the program senses that a button has been pressed it can change the setting of the state variable. Note that pressing a button only changes the setting of the up/down state variable. Pressing a button doesn't change the count, it simply changes the state variable to tell the program which way to count, up or down. Every time the program needs to change the count, it checks the state variable to determine whether to make the count value higher or lower.

The Program

Your program should start by doing the following operations.

The main loop of the program is an infinite loop that does the following four actions:

Note: When direction is changed it should count from whatever number we are currently at and not restart at zero or nine.

A Problem

Once you have the counter working try an experiment. Wait until the displayed digit changes and then quickly press the button to change the counting direction but only hold it down for very short time. The counting direction probably won't change. Now try it again but this time hold the button down until after the next count change and then let it up. Has the count direction changed? Take a look at your code and try to determine why this is happening. Why does a quick press of the button sometimes not work but a longer one does?

Think about how we could modify the software to still have the counter change every half second, but be responsive to button presses at any time in the cycle and for any length. Try to implement your ideas and see if you can make the buttons always change the counter direction regardless of when they are pressed or for how long.

Results

When your program is running you should be able to

Try to organize your code to use good style and indentation. Examine your solution for repetitive code that can be "factored" and replaced with a function, or other similar enhancements to make the code readable and modular. Points may be deducted for failure to do so. Once you have the assignment working demonstrate it to one of the instructors. Turn in a copy of your source code (see website for details.)