EE 109 - Spring 2026 Introduction to Embedded Systems

Lab 4

Using the LCD Display

Honor Statement

This is an individual assignment. The code you write, use for the demo and submit should be wholly your own and not produced by working in teams or using, in part or in whole, code written by someone else (found online, from a fellow student, acquaintance, etc.). You will be asked to verify this when you submit your code on Vocareum by signing your name in the honor.txt file on Vocareum. Penalties for violation include a 0 on the assignment and a potential further deduction or referral to Student Judicial Affairs.

Introduction

In this lab exercise you will attach an LCD display to the Arduino Uno and write software to display characters 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.

For a copy of the Lab 4 grading sheet, click here.

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 seven of the Uno’s I/O lines for data and control signals (Port D, bits 4-7 and Port B, bits 0-2).

Image

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. A third control line can be used to control the LCD backlight. The result of all this is that the shield can be used with just seven I/O lines, four for data and three for control signals.

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] (data), 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 ask 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 six files for Lab 4:

Make the usual changes to the PROGRAMMER line at the top of the Makefile to make it work on your computer. Attach your Arduino+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.

Image

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 blue control in the upper left corner of the display. If you can’t get the test program to work, ask one of the instructors 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 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” (half a byte) to the LCD and creating the Enable signal transition from 0 to 1 and back to 0. You will complete this function (in the files lcd.c) as part of the guided exercise for this lab assignment.

Mid level functions:

The mid level functions lcd_writedata and lcd_writecommand deal with sending data and commands to the LCD by making use of the low level functions. These functions transfer one byte each time they are called. You will need to complete these functions in this lab (in the file lcd.c)

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 add lines to lcd_init() to initialize the I/O ports the LCD uses.

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

Guided Exercise

For the next few parts of this lab, as was done with Lab 1, it will be conducted as a “guided exercise” where the instructor will perform many of the tasks and students, working as individuals, are asked to follow along doing what the instructor does making the same changes to the provided code files.

Low Level Functions

The low level function “lcd_writenibble” transfers a four bit value from the 8-bit (unsigned char) argument to the LCD. All transfers of data and commands depend on this function to do the actual transfer. When writing the code for “lcd_writenibble” you may assume the RS signal has already been set to 0 or 1 OUTSIDE of this function. Don’t set it inside this function.

Open the lcd.c file with your text editor and look for the lcd_writenibble function near the bottom of the file. Note that it as one 8-bit argument to the function (called lcdbits in our example).

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

The argument to lcd_writenibble is an 8-bit “unsigned char” variable, but only four of these bits will be transferred to the LCD. Note that it is very important that “lcd_writenibble” and the functions that will be calling it agree 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] so that’s what we will be doing.

The preferred method for copying multiples bits from one variable to another or to a register was covered in the lecture on the topics in Unit 5. For “lcd_writenibble” we need to copy the upper four bits of the argument to the function to the upper four bits of the PORTD register.

Use the method that was discussed in the Unit 5 lecture to add the lines of code that will copy the upper four bits of lcdbits to the upper four bits of the PORTD register.

Once the four bits are copied to PORTD bits 4 through 7, then the E signal (PB1) has to make a 0→1→0 transition to cause the LCD to read the 4 bits. 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.

Insert the following three lines in “lcd_writenibble” after the two lines added above. These will generate the E pulse.

PORTB |= (1 << PB1);        // Set E to 1
PORTB |= (1 << PB1);        // Make sure E is >230ns
PORTB &= ~(1 << PB1);       // Set E to 0

Task 3 below will explain why we have two identical lines that both set PB1 to a 1. This completes the lcd_writenibble function.

Mid Level Functions

Two mid level functions are used to send a byte to the command register or 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.

Locate the two routines in lcd.c and add the lines below as instructued to make them work as described above.

  1. Start with the “lcd_writecommand” function. Add code to configure the RS (Register Select) line on Group B, bit 0, to be 0 so the LCD will interpret the data being transferred as a command.

    PORTB &= ~(1 << PB0); // Clear RS for command write

  2. Call the lcd_writenibble procedure to write the upper four bits of data (bits 7-4) in the argument cmd. Note that the lcd_writenibble you wrote above will ignore the lower 4 bits of the 8-bits passed to it and only use the upper 4 bits.

    lcd_writenibble(cmd); // Send upper 4 bits

  3. To send the lower 4-bits of the argument cmd we have to move the lower 4-bits of the desired information to be the upper 4-bits of the argument being sent to lcd_writenibble, and call that function again. This will result in the lower 4-bits of the cmd argument being sent to the LCD.

    lcd_writenibble(cmd << 4); // Send lower 4 bits

    Both parts of the 8-bit value passed to lcd_writecommand in the cmd argument have now been sent to the LCD.

  4. The LCD requires a 2msec delay in order to process commands or data sent to it so add that to the function after the two calls to lcd_writenibble.

    _delay_ms(2); // Delay 2ms

This completes the lcd_writecommand function. Now go make the same additions to the lcd_writedata function except in there the RS bit must be set to a 1 before doing the transfers. The rest of the code that you added in steps 2 through 4 should be the same in both functions.

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

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. These are handled by the lcd_init and most of the code for these operations has been provided already in lcd.c file. However before any initialization commands can be sent to the LCD, the Arduino’s I/O ports must be configured properly to work with the LCD. The LCD uses Port B, bits 0 through 2 for control signals, and uses Port D, bits 4 through 7 for data signals. You will need to do the following steps.

  1. In lcd.c find the function lcd_init.

  2. At the top of this function, add the necessary code to configure Port B, bits 0 and 1 for output. Port B bit 2 should be left as an input so it won’t cause the LCD backlight to go off.

  3. Add the necessary code to configure Port D, bits 4 through 7 for output.

Important: The code you add to 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 rest of the LCD initialization requires several data transfers to it, most followed by delays of specific lengths. The code for these steps have been provided for you already in the lcd_init function and should not be changed.

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.

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. Connect the Arduino to your computer with the USB cable.

  3. In the lab4 folder where you have been editing the lcd.c file, type the command make enable. This will compile your lcd.c file and combine it with the enable.o file provide. If any errors were detected when compiling the lcd.c file you will need to fix these before proceeding. The resulting binary file is then downloaded to the Arduino and generates the E signals using the code in your lcd.c file.

  4. Turn on the oscilloscope and connect one of the probes to channel 1.

  5. Set the triggering to trigger on channel 1, a rising edge, and a trigger level of about 2 volts.

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

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

In the lcd_writenibble function we added three lines that set the PB1 output to be a one and back to a zero..

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

The first line made the output a 1, and the second just left it as a one but took some extra time to execute to ensure that the E pulse stays in the 1 state for at least 230ns. 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 you correctly added these lines to the lcd_writenibble the E pulse should be about 250ns wide. It’s very important that you confirm this using the oscilloscope. If you are not seeing this the problem must be fixed before proceding with the assignment.

Show one of the teaching staff your “E” signal of the correct width on the oscilloscope to receive credit for finishing this part of the assignment.

Task 4: Build a One Digit Up/Down Counter

For this task you will construct a simple circuit on your breadboard with three buttons to control a counter that is displayed on the LCD. We will provide the compiled code for the counter which will be combined with the lcd.c file you finished writing above to produce the program to run on the Arduino. You will be asked to try using the counter and determine if there are any flaws in the way it operates that should be fixed.

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 with three buttons labeled “Up”, “Down” and “Pause” that does the following.

To see a short video demonstrating the operation of this lab, click here.

The Circuit

From your lab box you will need

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, A4 and D11 which are connected to the microcontroller’s I/O bits PC2, PC4 and PB3.

Image

Finding the I/O Ports

Once the LCD is installed on top of the Arduino, you no longer have access to the black connectors on the Arduino in order to make connections to the I/O Ports. To remedy this, the available I/O port pins (those that are not used in communicating with the LCD) are bought up to the top of the LCD shield. Some are available as pins that stick up from the base of the LCD circuit board and you can make connections to these pins using the jumpers. Other are available from the black connectors on the LCD that you can stick wires into.

For this lab you need to connect to two of the pins of Port C, PC2 and PC4. The Port C pins can be found on the blue pins at the lower right corner of the LCD as shown below. Please note that there is no connection for PC0 even though there is a “A0” marking on the PC board near the connector. PC0 is used internally by the LCD and as a result there is no pin available to connect to it.

Image

The connection for Port B, bit 3 (PB3) is made in a similar way to one of the pins sticking up in the upper right corner of the LCD. Port B, pin 3 is referred to as the “D11” bit on the Arduino so look for the pin labeled “D11” along edge of the LCD board as shown below.

Image

All other Arduino I/O port bit (B0, B1, B2, C0, D4, D5, D6 and D7) are used by the LCD and are not available for connections on the LCD.

Download the Counter

Now that the hardware has been contructed you can load the counter program into the Arduino.

Your lab4 folder should contain the following files.

You may have modified the Makefile in Task 1 to download the test program but now it needs more changes to work with the files for this task. The binary code for the Task 4 assignment is not in just one file as with Lab 3. It’s in two files, counter.o and lcd.o, so you must modify the OBJECTS line in the Makefile to indicate this.

OBJECTS = counter.o lcd.o

From now on whenever you type “make”, it will compile lcd.c (if needed) and link it with the counter.o binary file to form the executable program.

Connect the Arduino (with the LCD attached to it) and enter the make flash command.

The counter program should show a single digit in the first column of the top row of the LCD. The digit should increment every second as decribed above until it reaches 9 and then it should start over at 0.

Try using the DOWN button (connected to PC4) to change the count direction so it’s counting down. Similary, the UP button (connected to PC2) changes the count direction to counting up.

The counter works properly in terms of its count sequence, either increasing or decreasing, but see if you can find a problem with how the buttons operate. Does the count direction always change properly when you press one of the buttons? If not, can you determine what has to be done when you press a button to make the direction change take effect?

Fix the Problem with the Buttons

From the table in front of the podium get a sheet of paper that contain code for an up/down counter that fixes the problem you found in code we provided. In this task you are asked to complete that program so the problem is fixed.

Think about what the counter program is doing between the times when it displays a new digit on the LCD. In the code we provided, it just used a _delay_ms(1000) function to sit there doing nothing between display updates. It only checked the state of the buttons once each second. If you pressed the button and released it during the time the program was in the delay function, the button press is not noticed. To fix this we need the program to check the buttons much more frequently even though the digit still only updates once each second.

Study the code provided on the paper and try to complete the program so this type of action is implemented. Assume we want the button to be checked 20 times each second.

Show your implementation of the fixed counter program to one of the teaching staff to receive credit for this task.

Task 5: A Scrolling Message Display

In this task you are asked to write a program that displays a long message (at least 40 character) on the LCD display. Since the message is longer than the LCD can display only a portion of the message can be seen at any time. The messages scrolls across the LCD screen at a fixed rate, and one button is used to change the direction of the scrolling, left-to-right or right-to-left.

The Circuit

This task uses the same wiring as was used above for Task 4 so no changes need to be made. What was the UP button on PC2 is now the left-right selection button. The buttons on PC4 and PB3 are not used in this task.

The Program

In the lab4 folder the lab4.c file should be used to get started with this task. It already has a few items in the file that need to be there.

Start by editing the Makefile so the OBJECTS line looks like this

OBJECTS = lab4.o lcd.o

Important: Delete the main.elf and main.hex files from the lab4 folder. These are left over from a previous task and we will need the make program to recreate them.

Edit the lab4.c file to include the following items.

Once you have the code written, download it to your Arduino and confirm its operation.

Try to organize your code to use good style and indentation. Examine your solution for any 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.

Results

The answers to the review questions below should be edited into the Lab4_Answers.txt file. The Lab4_Answers.txt file and all source code (lab4.c, lcd.c, lcd.h and Makefile) must be uploaded to the Vocareum web site by the due date. See the Labs page of the class web site for a link for uploading.

Please make sure to save all the parts used in this lab in your project box. These will be needed for labs throughout the rest of the semester. We suggest also saving all the pieces of wire you cut and stripped since those will be useful in later labs.

Review Questions

Answer the following questions and submit them in a separate text file (“Lab4_Answers.txt”) along with your source code.

  1. Normally we only read the bits in the PIN register, but a close reading of the ATmega328P datasheet reveals that writing a 1 to PIN register bit has the effect of inverting the bit in the corresponding PORT register. For example, if you write a 1 to PINC, bit 3, it will invert bit 3 in the PORTC register. Based on this information, Billy Bruin has decided that he now knows an easy way to “toggle” the E bit (flipping it to its opposite value) to generate the E pulse in the lcd_writenibble() function by using this code.

     PINB |= (1 << PB1);    // Toggle E bit from 0 to 1
     PORTB |= (1 << PB1);   // Delay to make the E pulse longer
     PINB |= (1 << PB1);    // Toggle E bit from 1 to 0
    

    Note: PINB |= (1 << PB1); is equivalent to PINB = PINB | (1 << PB1);

    Tammy Trojan has also read the datasheeet and found that when reading the PIN register, if a bit in the group is configured as input, the the voltage coming in the PIN is returned, but if a bit is configured as output, the corresponding PORT bit value is returned. From this she concludes that it is possible that Billy’s method can cause problems depending on how the compiler converts the program to instructions the processor executes and therefore should not be used. For the program to work reliably, she recommends using this code to generate the E pulse.

     PINB = (1 << PB1);     // Toggle E bit from 0 to 1
     PORTB |= (1 << PB1);   // Delay to make the E pulse longer
     PINB = (1 << PB1);     // Toggle E bit from 1 to 0
    

    Tammy says that in this lab assignment with the PAUSE button on PB3, Billy Bruin’s code can cause the PAUSE button to stop working. Can you explain why this could happen?

  2. Suppose we need to perform 3 concurrent tasks intermittently: Task A every 20 ms, Task B every 15 ms, and Task C every 40 ms. What delay should be used on each iteration of the main loop?