exploringarduino.com/content2/ch12
wiley.com/go/exploringarduino2e
One of the best things about designing embedded systems is that they can operate independently from a computer. Up until now, you've been tethered to the computer if you wanted to display any kind of information more complicated than an illuminated LED. By adding a liquid crystal display (LCD) to your Arduino, you can more easily display complex information (sensor values, timing information, settings, progress bars, and so on) directly on your Arduino project without having to interface with the serial monitor through a computer.
In this chapter, you will learn how to connect an LCD to your Arduino, and how to use the Arduino LiquidCrystal
library to write text and arbitrary custom characters to your LCD. After you have the basics down, you will add some components from previous chapters to make a simple thermostat capable of obtaining local temperature data, reporting it to you, and controlling a fan to compensate for heat. An LCD will give you live information, a speaker will alert you when the temperature is getting too hot, and the fan will turn on to automatically cool you down.
To complete the examples in this chapter, you will use a parallel LCD screen. These are extremely common and come in all shapes and sizes. The most common is a 16×2 character display with a single row of 16 pins (14 if it does not have a backlight). In this chapter, you will use a 16-pin LCD display that can show a total of 32 characters (16 columns and 2 rows).
If your display didn't come with a 16-pin header already soldered on, you need to solder one on so that you can easily install it in your breadboard. With the header successfully soldered on, your LCD should look like the one shown in Figure 12-1, and you can insert it into your breadboard.
Next, you wire up your LCD to a breadboard and to your Arduino. All of these parallel LCD modules have the same pin-out and can be wired in one of two modes: 4-pin or 8-pin mode. You can accomplish everything you want to do using just 4 pins for communication; that's how you'll wire it up. There are also pins for enabling the display, setting the display to command mode or character mode, and setting it to read/write mode. Table 12-1 describes all of these pins.
Table 12-1: Parallel LCD pins
pin Number | pin Name | pin Purpose |
1 | VSS | Ground connection |
2 | VDD | +5V connection |
3 | V0 | Contrast adjustment (to potentiometer) |
4 | RS | Register selection (Character versus Command) |
5 | RW | Read/write |
6 | EN | Enable |
7 | D0 | Data line 0 (unused in 4-pin mode) |
8 | D1 | Data line 1 (unused in 4-pin mode) |
9 | D2 | Data line 2 (unused in 4-pin mode) |
10 | D3 | Data line 3 (unused in 4-pin mode) |
11 | D4 | Data line 4 |
12 | D5 | Data line 5 |
13 | D6 | Data line 6 |
14 | D7 | Data line 7 |
15 | A | Backlight anode |
16 | K | Backlight cathode |
Here's a breakdown of the pin connections:
You can connect the communication pins of the LCD to any I/O pins on the Arduino. In this chapter, they are connected as shown in Table 12-2.
Reference the wiring diagram shown in Figure 12-2, and hook up your LCD accordingly.
Now your LCD is ready for action! Once you get the code loaded in the next section, you can start displaying text on the screen. The potentiometer will adjust the contrast between the text and the background color of the screen.
Table 12-2: Communication pin Connections
LCD pin | Arduino pin Number |
RS | pin 2 |
EN | pin 3 |
D4 | pin 4 |
D5 | pin 5 |
D6 | pin 6 |
D7 | pin 7 |
The Arduino IDE includes the LiquidCrystal
library, a set of functions that makes it very easy to interface with the parallel LCD that you are using. The LiquidCrystal
library has an impressive amount of functionality, including blinking the cursor, automatically scrolling text, creating custom characters, and changing the direction of text printing. This chapter does not cover every function, but instead gives you the tools you need to interface with the display's most important functions. You can find descriptions of the library functions and examples illustrating their use on the Arduino website, at blum.fyi/arduino-lcd-library
. (This is also linked from exploringarduino.com/content2/ch12
.)
In this first example, you add some text and an incrementing number to the display. This exercise demonstrates how to initialize the display, how to write text, and how to move the cursor. First, include the LiquidCrystal
library:
#include <LiquidCrystal.h>
Then, initialize an LCD object, as follows:
LiquidCrystal lcd (2,3,4,5,6,7);
The arguments for the LCD initialization represent the Arduino pins connected to RS, EN, D4, D5, D6, and D7, in that order. In the setup, you call the library's begin()
function to set up the LCD display with the character size. (The one I'm using is a 16×2 display, but you may be using another size, such as 20×4.) The arguments for this command represent the number of columns and the number of rows, respectively:
lcd.begin(16, 2);
After adding this code, you can call the library's print()
and setCursor()
commands to print text to a given location on the display. For example, if you want to print my name on the second line, you issue these commands:
lcd.setCursor(0,1);
lcd.print("Jeremy Blum");
The positions on the screen are indexed starting with (0,0)
in the top-left position. The first argument of setCursor()
specifies the column number, and the second argument specifies the row number. By default, the starting location is (0,0
). So, if you call print()
without first changing the cursor location, the text starts in the top-left corner.
Using this knowledge, you can now write a simple program that displays some text on the first row and that prints a counter that increments once every second on the second row. Listing 12-1 shows the complete program you need to accomplish this. Load it on to your Arduino and confirm that it works as expected. If you don't see anything, adjust the contrast with the potentiometer.
This program combines all the steps that you learned about earlier. The library is first included at the top of the program. A time
variable is initialized to 0
, so that it can be incremented once per second during the loop()
. A LiquidCrystal
object called lcd
is created with the proper pins assigned based on the circuit you've already wired up. In the setup, the LCD is configured as having 16 columns and 2 rows, by calling lcd.begin(16,2)
. Because the first line never changes, it can be written in the setup. This is accomplished with a call to lcd.print()
. Note that the cursor position does not need to be set first, because you want the text to be printed to position (0,0)
, which is already the default starting location. In the loop, the cursor is always set back to position (0,1)
so that the number you print every second overwrites the previous number. The display updates once per second with the incremented time
value.
What if you want to display information that cannot be expressed using normal text? Maybe you want to add a Greek letter, a degree sign, or some progress bars. Thankfully, the LiquidCrystal
library supports the definition of custom characters that can be written to the display. In the next example, you will use this capability to make an animated progress bar that scrolls across the display. After that, you will take advantage of custom characters to add a degree sign when measuring and displaying temperature.
Creating a custom character is pretty straightforward. If you take a close look at your LCD, you'll see that each character block is actually made up of a 5×8 grid of pixels. (Figure 12-3 shows a magnified view of this.) To create a custom character, you simply have to define the value of each of these pixels and send that information to the display. To try this out, you'll make a series of characters that will fill the second row of the display with an animated progress bar. Because each character space is 5 pixels wide, there will be a total of five custom characters: one with one column filled, one with two columns filled, and so on. To write the code for this, it helps to first visualize exactly what the pixel array will look like. Figure 12-3 shows how each of the five custom characters will look.
At the top of your sketch where you want to use the custom characters, create a byte array with 1s representing pixels that will be turned on, and with 0s representing pixels that will be turned off. The byte array representing the character that fills the first column (or the first 20 percent of the character) looks like this:
byte p20[8] = {
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
B10000,
};
I chose to call this byte array p20
, to represent that it is filling 20 percent of one character block (the p
stands for percent). Note how the ones and zeros corresponded to the filled pixel positions from Figure 12-3.
In the setup()
function, call the createChar()
function to assign your byte array to a custom character ID. Custom character IDs start at 0 and go up to 7, so you can have a total of eight custom characters. To map the 20-percent character byte array to custom character 0, type the following within your setup()
function:
lcd.createChar(0, p20);
When you're ready to write a custom character to the display, place the cursor in the right location and use the library's write()
function with the ID number:
lcd.write((byte)0);
In the preceding line, (byte)
casts, or changes, the 0
to a byte value. This is necessary only when writing character ID 0
directly (without a variable that is defined to 0), to prevent the Arduino compiler from throwing an error caused by the variable type being ambiguous. Try removing (byte)
from this command and observe the error that the Arduino IDE displays. You can write other character IDs without it, like this:
lcd.write(1);
Putting this all together, you can add the rest of the characters and put two nested for()
loops in your program loop to handle updating the progress bar. The completed code looks like Listing 12-2.
At the beginning of each pass through the loop, the 16-character-long string of spaces is written to the display, clearing the progress bar before it starts again. The outer for()
loop iterates through all 16 positions. At each character position, the inner for()
loop keeps the cursor there and writes an incrementing progress bar custom character to that location. The byte cast is not required here because the ID 0
is defined by the j
variable in the for()
loop.
Now, let's make this display a bit more useful. To do so, you add the temperature sensor from Chapter 10, “The I2C Bus,” a fan (using your motor skills from Chapter 4, “Using Transistors and Driving DC Motors”), and the speaker from Chapter 6, “Making Sounds and Music.” The display shows the temperature and the current fan state. When it gets too hot, the speaker makes a noise to alert you, and the fan turns on. When it gets sufficiently cool again, the fan turns off. Using two pushbuttons and the debounce code in Listing 2-5 in Chapter 2, “Digital Inputs, Outputs, and Pulse-Width Modulation,” you add the ability to increment or decrement the desired temperature.
The hardware setup for this project is a conglomeration of previous projects. You should treat the fan similarly to the motors you learned about in Chapter 4. The recommended 5V brushless fan for this project will consume more power than your Arduino can provide from an I/O pin, so you'll need to drive it with a transistor. To drive the fan, use an NPN transistor, referencing the schematic that you used in Chapter 4 (Figure 4-1). Similarly to the 5V DC motors that you used in the roving car project from Chapter 4, this fan should be powered off its own 5V supply. Plug a 9V battery into the Arduino's barrel jack and use that as the input voltage to the linear regulator (the VIN pin on the Arduino can be used to connect the battery's 9V supply to the regulator). The regulator will generate a 5V supply to be used by the fan. This will ensure that electrical noise from the fan turning on and off does not impact the performance of the temperature sensor (which is powered using the Arduino's on-board voltage regulator). As you did in Chapter 4, ensure that you equip the regulator with 10μF decoupling capacitors on its input and output. Remember that the regulator's ground must be connected to your Arduino's ground. Figure 12-4 shows a schematic of the components that you'll be adding to the LCD that you've already wired up (it does not show the LCD and potentiometer that you've already wired).
Referencing only the schematic drawing, try to add the fan, 5V regulator, drive transistor, protection diode, temperature sensor, speaker, and pushbuttons. Since this fan is a brushless motor, you can omit the small capacitor that you put across the leads of the brushed DC motor in Chapter 4; there are no brushes to make the RF (radio frequency) interference that the capacitor is normally used to reduce. Note how the 5V supply generated by the L7805CV voltage regulator is distinct from the Arduino's 5V supply.
You might need to rearrange your placement of the LCD and trim potentiometer to make room for all your circuitry.
The two buttons have one side connected to power; the other side is connected to ground through 10kΩ pull-down resistors and to the Arduino.
The speaker is connected to an I/O pin through a 220Ω resistor and to ground. The frequency of the sound will be set in the program.
You connect the I2C temperature sensor exactly as you did in Chapter 10. Don't forget the pull-up resistors!
Plug a 9V battery holder into the Arduino's barrel jack. This will enable you to draw 9V power from the Arduino's VIN pin for powering your 5V linear regulator.
When wiring up the linear regulator, recall that the stripe on the decoupling capacitors represents the negative pin of the capacitor, which should be connected to the shared ground. On the diode, the side with the stripe should be connected to the fan's positive wire and the fan's 5V supply; the other side should be connected to the fan's negative wire and the NPN transistor's collector pin.
The diagram in Figure 12-5 shows the complete wiring setup with everything you need to create this project. It's possible to fit this all onto a half-sized breadboard, but as you can see from the wiring diagram, it is quite cramped. You may wish to consider using a full size breadboard for this project.
Having some parameters in place beforehand makes writing information to the LCD screen easier. First, use degrees Celsius for the display, and second, assume that you'll always be showing two digits for the temperature. Once the software is running, the LCD display will look something like Figure 12-6.
The Current:
and Set:
strings are static; they can be written to the screen once at the beginning and left there. Similarly, because the temperatures are assumed to be two digits, you can statically place both °C
strings into the correct locations. The current reading will be displayed in position (8,0)
and will be updated on every run through the loop()
. The desired, or set, temperature will be placed in position (8,1)
and updated every time a button is used to adjust its value. The fan indicator in the lower-right corner of the display will be at position (15,1)
. It should update to reflect the fan's state every time it changes.
The degree symbol, fan off indicator, and fan on indicator are not part of the LCD character set. Before using them in your sketch, you need to create them as byte arrays at the beginning of your program. As before, visualize the custom characters as pixel arrays first, as shown in Figure 12-7.
Then, define the custom characters to the specifications using the following snippet:
//Custom degree character
byte degree[8] = {
B00110,
B01001,
B01001,
B00110,
B00000,
B00000,
B00000,
B00000,
};
//Custom "fan on" indicator
byte fan_on[8] = {
B00100,
B10101,
B01110,
B11111,
B01110,
B10101,
B00100,
B00000,
};
//Custom "fan off" indicator
byte fan_off[8] = {
B00100,
B00100,
B00100,
B11111,
B00100,
B00100,
B00100,
B00000,
};
You write the static parts of the display in setup()
. Move the cursor to the right locations, and with the LCD library's write()
and print()
functions, update the screen, as shown in the following snippet:
//Make custom characters
lcd.createChar(0, degree);
lcd.createChar(1, fan_off);
lcd.createChar(2, fan_on);
//Print a static message to the LCD
lcd.setCursor(0,0);
lcd.print("Current:");
lcd.setCursor(10,0);
lcd.write((byte)0);
lcd.setCursor(11,0);
lcd.print("C");
lcd.setCursor(0,1);
lcd.print("Set:");
lcd.setCursor(10,1);
lcd.write((byte)0);
lcd.setCursor(11,1);
lcd.print("C");
lcd.setCursor(15,1);
lcd.write(1);
You also update the fan indicator and temperature values each time through loop()
. You need to move the cursor to the right location each time before you update these characters.
In the event that the I2C temperature sensor returns no data, you can halt the program (with while(1);
as you did in Chapter 10). Instead of sending an error message to the serial monitor, use lcd.clear()
to empty the LCD screen and return the cursor to the (0,0) position. Then, print an error message: lcd.print("I2C Error");
.
In Chapter 2, you used a debounce()
function. Here, you modify it slightly to use it with multiple buttons. One button will increase the set point, and the other will decrease it. You need to define variables for holding the previous and current button states:
//Variables for debouncing
boolean lastDownTempButton = LOW;
boolean currentDownTempButton = LOW;
boolean lastUpTempButton = LOW;
boolean currentUpTempButton = LOW;
You can modify the debounce()
function to support multiple buttons. To accomplish this, add a second argument that specifies which button you want to debounce:
//A debouncing function that can be used by both buttons
boolean debounce(boolean last, int pin)
{
boolean current = digitalRead(pin);
if (last != current)
{
delay(5);
current = digitalRead(pin);
}
return current;
}
In loop()
, you want to check both buttons using the debounce()
function, change the set_temp
variable as needed, and update the set value that is displayed on the LCD:
//Debounce both buttons
currentDownTempButton = debounce(lastDownTempButton, DOWN_BUTTON);
currentUpTempButton = debounce(lastUpTempButton, UP_BUTTON);
//Turn down the set temp
if (lastDownTempButton == LOW && currentDownTempButton == HIGH)
{
set_temp--;
}
//Turn up the set temp
else if (lastUpTempButton == LOW && currentUpTempButton == HIGH)
{
set_temp++;
}
//Print the set temp
lcd.setCursor(8,1);
lcd.print(set_temp);
//Update the button state with the current
lastDownTempButton = currentDownTempButton;
lastUpTempButton = currentUpTempButton;
The preceding code snippet first runs the debounce()
function for each button, and then adjusts the set temperature variable if one of the buttons has been pressed. Afterward, the temperature displayed on the LCD is updated, as are the button state variables.
In this section, you add code to control the fan and the speaker. Although the LCD showing you live information is nice, you'll often find it useful to have an additional form of feedback to tell you when something is happening—for example, having the speaker beep when the fan turns on. In this example, you use tone()
paired with delay()
and a notone()
command. You could instead add a duration argument to tone()
to determine the duration of the sound. You want to make sure that the tone plays only one time per alert condition (and does not beep forever when above the set temperature).
Using a state variable, you can detect when the speaker has beeped and thus keep it from beeping again until after the temperature dips below the set temperature and resets the state variable.
When the fan turns on, an indicator changes on the LCD (represented by the custom character you defined at the top of the program). The following code snippet checks the temperature and controls the speaker, the fan indicator on the LCD, and the fan:
//If it's too hot!
if (c >= set_temp)
{
//Check if the speaker has already beeped
if (!one_time)
{
tone(SPEAKER, 400);
delay(500);
one_time = true;
}
//Turn off the speaker when it's done
else
{
noTone(SPEAKER);
}
//Turn the Fan on and update display
digitalWrite(FAN, HIGH);
lcd.setCursor(15,1);
lcd.write(2);
}
//If it's not too hot!
else
{
//Make sure the speaker is off
//reset the "one beep" variable
//update the fan state and LCD display
noTone(SPEAKER);
one_time = false;
digitalWrite(FAN, LOW);
lcd.setCursor(15,1);
lcd.write(1);
}
The one_time
variable is used to make sure that the beep plays only one time instead of continuously. Once the speaker has beeped for 500 ms at 400 Hz, the variable is set to true
and is reset to false
only when the temperature drops back below the desired temperature.
It's time to bring all the parts together into a cohesive whole. You need to make sure that you include the appropriate libraries, define the pins, and initialize the state variables at the top of the sketch. Listing 12-3 shows the complete program. Load it on to your Arduino and compare your results to the demo video showing the system in action.
You no longer need to have the Arduino and components tethered to the computer to see what the temperature is. Since this project has already been designed to be powered with a battery, simply unplug the USB cable, and your thermostat will continue to run using the connected battery. If you like, you can plug in a battery or wall power supply and place it anywhere in your room.
You could expand the functionality of this program in all kinds of ways. Here are a few suggestions for further improvements you can make:
In this chapter, you learned the following: