EE109 – Fall 2017 Introduction to Embedded Systems

EE109 – Fall 2017: Introduction to Embedded Systems

Lab 4

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→1→0 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 lab4 folder in your ee109 folder. From the class web site download the file lab4.zip, extract the files and put them in the lab4 folder. You should have these three files for Lab 4:

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
lab4.c
Template file containing functions declarations to get you started
lcd.h
Function prototypes to be included in your lab4.c
lcd.c
Actual implementation of LCD functions. You will need to complete lcd_init() and lcd_writenibble(unsigned char lcdbits)

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 function(s):

The low level function, lcd_writenibble(unsigned char lcdbits) will handle the changing of the bits in the I/O ports connected to LCD. Most of the "bit fiddling" will be done in this function. The primary task 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 need to complete this function in this lab (in the file lcd.c).

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. These are provided for you.

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. The functions for positioning the cursor and writing strings are provided. You'll only write lcd_init()

The lcd.c file has the functions listed below, either complete or to be written. Start with
that file and add code to implement the functions.

Low Level Functions

The low level function "lcd_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 lcd_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→1→0 transition, and the lcd_writenibble routine is the only function that changes the state of the E line.

Note that it is very important that "lcd_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 lcd_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 lcd_writecommand(unsigned char cmd)
{
    /* Send the 8-bit byte "cmd" to the LCD command register */
}

void lcd_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 "lcd_writenibble" function. Recall that we decided that lcd_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 "lcd_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 "lcd_writenibble" (since "lcd_writenibble" expects the 4-bits it is supposed to transfer to be in that upper 4-bit area). An appropriate shift operator >> or << can be used 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 "lcd_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.

Examine the code in lcd.c and you will see that both of the mid-level functions operate in a similar manner:

  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 lcd_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 lcd_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 and the routines that do this lcd_moveto and lcd_stringout are provided for you. You will only code the following function:

void lcd_init()
{
    /* 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μ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 lcd_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 lcd_init to initialize the LCD are shown in the skeleton lcd.c file. You just need to fill in the code that is described by the comments in the routine. Important: Your lcd_init 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 also in the lcd.c file but we will not be using the in this week's lab assignment.

void lcd_moveto(unsigned char row, unsigned char col)
{
    /* Move the cursor to the row (0 to 1) and column (0 to 15) specified */
}

void lcd_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 lcd_init() function.
  3. In the while(1) loop of the main routine in lab4.c, insert code to call the lcd_writedata function.
    while (1) {
        lcd_writedata(0);
    }
    

    This will cause the lcd_writedata routine to be called repeatedly and each time it will generate two E pulses close together followed by a 2ms delay.

  4. The source code for the Lab 4 assignment is not in just one file as with Lab 3. It's in two files, lab4.c and lcd.c, so you must modify the OBJECTS line in the Makefile to indicate this.
    OBJECTS = lab4.o lcd.o
    
  5. Compile the code by typing "make". You should see that it compiled both the lab4.c file and the lcd.c file.
  6. 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.
  7. Turn on the oscilloscope and connect one of the probes to channel 1.
  8. Set the triggering to trigger on channel 1, a rising edge, and a trigger level of about 2 volts.
  9. 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.
  10. Adjust the scope setting to allow you to measure how long the E pulse is in the 1 state. Since the lcd_writedata has a 2ms delay after sending the data, most of the time the signal will be at the zero level. The two E pulses will be very narrow spikes that occur every 2ms. If the scope is triggering properly on the signal you should be able to adjust the horizontal timing to get a clear view of the width of the E pulse.

If the E pulse is not at least 230ns long you can extend the pulse by adding additional instructions to your lcd_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 lcd_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 lcd_writedata with this value as the argument.

For example:

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

If you just keep calling lcd_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 lcd_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 lcd_writecommand function with an argument of 1.

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

After the screen has been cleared, the program can call lcd_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.

Please use the structure of nested if statements shown in the State Machines Unit in your lecture notes and discussed in class. Don't just try to "wing-it" or make your own state machine approach. Instead, use the suggested structure.

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.

One hint: If we want to do something once a week, we can't just go to sleep for a week and wake up 7 days later (well most of us can't). We wake up at a finer granularity of 1 time per day. But if we started at 0 and added 1 each day we wake up, then we we reach 7 we would know it had been 1 week and we could perform our task, set our count back to 0 and continue counting days until we reach 7 again (in which case a second week would have elapsed). In this way I wake-up each day and can do certain tasks (i.e. check button inputs) but perform other tasks on a less frequent basis if my count has reached a desired threshold (i.e. change my outputs on a less frequent basis).

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.)