FB pixel

Drive a 1602 Character LCD Using MCC | Embedded C Programming - Part 19

Published


Hi again! In this tutorial we will not discover any new MCU modules. Instead we will learn how to use the 1602 character LCD with the HD44780 driver. If you read the PIC10F200 tutorials, you should already be familiar with it. But that time I talked about it superficially, and now I will tell you about it in more detail. If you read the previous tutorial, you still can find something interesting here as I will show another programming approach of communication between the LCD and the MCU.

The task for today is still the same as in tutorial 18. As is customary in programming, we need to write the text “Hello world ☻” on the LCD. The “smiley” character should be generated manually and loaded into the Character Generator memory.

1602 LCD with the HD44780 Driver

There are several displays with the HD44780 driver, they can have different line numbers and position numbers (1x8, 2x8, 1x16, 2x16, 2x20, 4x20 etc.), also they can have different backlight colors (green, yellow, blue, etc.) or not have a backlight at all. The character’s color can be black or white. So, as you see, there is a diversity but all of them have the same interface and are controlled using the same commands. I have the display shown in Figure 1 which I bought a long time ago on Aliexpress.

Figure 1 - 1602 LCD with Blue Backlight and White Characters
Figure 1 - 1602 LCD with Blue Backlight and White Characters

This LCD has the built-in character generator, so you don’t need to design your own characters (like we had to with the 7-segment indicator). We can just send the ‘A’ letter to the display, and it will be shown - fascinating, isn’t it? Also, the display has 16 empty cells in which you can load your own characters and then use them (I will talk about it later). The character generator of the LCDs from Aliexpress usually have a Chinese code page, so you can use the first 128 ASCII characters, which includes capital and small English letters, numbers, and punctuation signs which is quite good!

Let’s now see which signals this display has (Figure 2).

Figure 2 - 1602 LCD Pinout
Figure 2 - 1602 LCD Pinout

GND and VCC pins are the power supply of the display. Normally it is 5V but also can be 3.3V so be careful when buying an LCD.

The Vo pin is used to set the contrast of the LCD. Usually a potentiometer of 10-47 kOhm is connected to it.

RS is the data/command input. When this pin is low, the incoming data is considered as a command, and when it’s high, it is considered a character to display.

RW is the read/write input. When it is low, the data goes from the microcontroller to the LCD, and when it is high, the data goes the reverse direction. As we don’t expect any data from the LCD, we can connect this pin directly to ground.

E is the clock input. The data is read on the falling edge at this pin.

D0-D7 are the data inputs of the 8-bit parallel interface. The HD44780 driver also supports the 4-bit parallel interface. In this case, pins D0-D3 remain unused, and the bytes are transmitted by nibbles: higher one first, lower one second.

LED+ and LED- are the backlight power inputs. The supply voltage is also 5V, but a series resistor of 22-47 ohms is required.

As for the commands that the HD44780 uses, we didn’t consider them previously but now we will consider them in detail. I will base my explanations on the data sheet of the HD44780 driver, so you also can refer to it for full information. Let me present to table 6 from the data sheet (Figure 3) where all the LCD commands are described.

Figure 3 - HD44780 Driver Commands Description
Figure 3 - HD44780 Driver Commands Description

As you can see in Figure 3, the LCD has quite few commands, and each of them has at least one “1” in the data bits. Let me briefly run over all commands.

  • “Clear display” command (0x01) seems to be quite clear (pun definitely intended): it clears the display content and sets the DDRAM counter to 0, so after this, the next character will be printed in the first position of the first line.
  • “Return home” command (0x02) also clears the DDRAM counter to 0 but unlike the previous command, it doesn’t clear its content. Also if the shift was enabled for the display, the view returns to the initial position.
  • “Entry mode set” command (0x04) has two parameters:
    • I/D (bit #1) allows us to increment (if it is 1) or decrement (if it is 0) the DDRAM address which actually means if the text will be written left to right or right to left. For English text we need to set this parameter to 1 to write left to right.
    • S (bit #0) allows us to shift the display leaving the cursor at the same position, when set to 1. The direction of the shift is set by the bit “S/C” of the command “Cursor or display shift”. When this bit is 0, no display shift occurs.
  • “Display on/off control” command (0x08) has three parameters:
    • D (bit #2) turns the display on when set to 1, and turns it off otherwise. The content of the DDRAM remains unchanged. So this bit just turns off the image, which can be restored instantly by setting it to 1.
    • C (bit #1) enables (when it is 1) or disables (when it is 0) the cursor. The cursor is displayed in the 8’th line of the character (all characters have the size 5x7 pixels but the character place is 5x8 pixels, we will consider this later).
    • B (bit #0) enables (when it is 1) or disables (when it is 0) the blinking of the overall position at the cursor. The blinking and the cursor can be enabled independently from each other, so you can select either any of them, or both, or none.
  • “Cursor or display shift” command (0x10) has two parameters:
    • S/C (bit #3) allows to shift the cursor by one position (the DDRAM counter also changes) if it is 0, or to shift the entire display (the counter remains the same) if it is 1. This option is useful to search the text in the display, or to implement the running line by the internal means of the display.
    • R/L (bit #2) sets the direction of the shift: to the right when it is 1, or to the left when it is 0.
  • “Function set” command (0x20) is one of the basic ones that needs to be implemented during the initialization process to allow the display to operate correctly. It has three parameters:
    • DL (bit #4) sets the data bus width: 0 for 4 bits (in this case pins DB0-DB3 are not used, as I mentioned before), 1 for 8 bits (in this case all data pins are used).
    • N (bit #3) sets the number of display lines: 0 for one line, and 1 for two lines.
    • F (bit #2) sets the character font type: 0 for 5x8, 1 for 5x10. This parameter should be set according to the physical LCD type. The most widespread displays have the 5x8 characters, so we need to set this bit as 0.
  • “Set CGRAM address” (0x40) sets the address of the character generator memory. This address has 6 bits width. I’ll explain this part in more detail later.
  • “Set DDRAM address” (0x80) sets the address of the display data memory. This address has 7 bits width, and sets the position of the cursor at the LCD. Bit #6 sets the line number (0 – first line, 1 – second line), and bits #3-0 set the position within a line (0 to 15).
  • “Read busy flag and address” commands allow reading the busy flag and address, as follows from its name. As we’re not going to use the reading option, I’ll let you delve into the details of this command on your own.
  • “Write data to CG or DDRAM” command allows writing the data either to CGRAM or to DDRAM depending on the previous command: “Set CGRAM address” or “Set DDRAM address”, respectively.
  • “Read data from CG or DDRAM” command allows reading from either CGRAM or DDRAM. As we’re not going to use the reading option, I’ll not consider this command any deeper either.

Now, let’s talk about the CGRAM. Let me copy table 5 from the datasheet (Figure 4) to illustrate how the generation of custom character happens.

Figure 4 - Relationship between CGRAM Addresses, Character Codes (DDRAM) and Character Patterns (CGRAM Data)
Figure 4 - Relationship between CGRAM Addresses, Character Codes (DDRAM) and Character Patterns (CGRAM Data)

As I mentioned before there are sixteen DDRAM addresses in which you can locate the custom characters. But in fact, bit #3 in these addresses is not used (see Figure 4, column 1), so you can generate only eight custom characters.

As you can see from Figure 4 each character consists of eight lines. The lower three bits of the CGRAM address represent the number of a line within one character (top line first), and the upper three bits of the CGRAM address represent the number of the character. Also in the last column you can see that the upper three bits of the character generator are not used, and can be anything.

Let’s now try to generate the smiley face character and create the codes for it (table 1).

Table 1 - Smiley Face Generator
Table 1 - Smiley Face Generator

So as you can see, we leave the upper three bits blank (which correspond to 0), and write the character only using the lower five bits. Also we leave the last line blank because this is the cursor position, so it’s better not to use it.

I think that’s all we need to know about the HD44780 driver to implement the given task, so let’s consider the schematic diagram of the device.

Schematics Diagram

Let’s look at the schematics diagram of this circuit (Figure 5).

Figure 5 - Schematics Diagram with the PIC18F14K50 with 1602 LCD
Figure 5 - Schematics Diagram with the PIC18F14K50 with 1602 LCD

This schematic diagram consists of the PIC18F14K50 MCU (DD1), PICKit debugger (X1), and the 1602 LCD (X2) which is connected as described above. The Vo pin is connected to the 10-47 kOhm potentiometer R1. When you power up the device, you should rotate the handle of the potentiometer to make the rectangles of the LCD barely visible. Then the contrast level is set correctly. The LED+ pin is connected to VCC through the 22-47 Ohm resistor R2. The RS, E, D4, D5, D6, and D7 pins are connected to the MCU. The data pins D4-D7 are connected to the pins RC0-RC3, respectively. The RS input is connected to RC4, and E input is connected to RC5. The RW input of the LCD is connected to GND, as we will just write to the LCD. The D0-D3 inputs remain unused in the 4-bit interface.

Configuration of the Project using MCC

In this project we will only use the Pin Module and the System Module. The configuration is very simple and doesn’t require additional explanation (Figure 6-7).

Figure 6 - System Module
Figure 6 - System Module
Figure 7 - Pin Module
Figure 7 - Pin Module

In the System Module we set the CPU frequency as 32 MHz and disable the Low-voltage programming. In the Pin Module we configure pins to which the LCD is connected (see Figure 5) as outputs and give them the corresponding custom name.

Program Code Description

In this project we also will use the concept of distributing the code between different files like we did in tutorial 15. So we will create special files where we will write all the LCD-related code.

We need to create two new files: one “main.c…” and one “xc8_header.h…”, and call them both “lcd_1602_mcc” (see tutorial 15 for more details on how to do this).

Now let’s open the “lcd_1602_mcc.h” file and write the following text in it:

#include <xc.h> // include processor files - each processor file is guarded.

void lcd_send (uint8_t value, uint8_t rs); //Send any data to the LCD

void lcd_command (uint8_t command); //Send the command to the LCD

void lcd_data (uint8_t data); //Send the character to the LCD

void lcd_init (uint8_t cursor, uint8_t blink); //Initialize the LCD

void lcd_write (char *s); //Write the string at the LCD

void lcd_set_cursor (uint8_t x, uint8_t y); //Set cursor at the specified position

void lcd_create_char (uint8_t addr, uint8_t *data);//Create a custom character

This file contains the function definitions that we will use for operating with the LCD.

The lcd_send function (line 3) allows sending the data byte to the LCD via a 4-bit interface. It has two parameters: value, which represents the byte that will be sent to the LCD, and rs which represents the state of the RS pin: 0 for low or 1 for high.

The lcd_command function (line 4) sends the command byte to the LCD, which is defined with the command parameter.

The lcd_data function (line 5) sends the character byte to the LCD, which is defined with the data parameter. This character will be sent either to CGRAM or to DDRAM.

The lcd_init function (line 6) initializes the LCD with the 4-bit interface. It has two parameters: cursor which shows (when it is 1) or hides (when it is 0) the cursor, and blink which enables (when it is 1) or disables (when it is 0) the blinking of the cursor position.

The lcd_write function (line 7) allows writing the string (parameter s) to the LCD.

The lcd_set_cursor function (line 8) sets the cursor at the position defined by the parameters x and y.

The lcd_create_char function (line 9) creates one character in the CGRAM memory at the address defined by the addr parameter. Parameter data is the array of 8 bytes that define the character.

Well, that’s all about the functions definitions, let’s now consider their description in the “lcd_1602_mcc.c” file:

#include "mcc_generated_files/mcc.h"

#include "lcd_1602_mcc.h"


void lcd_send (uint8_t value, uint8_t rs)//Send any data to LCD

{

if (rs) //If rs is 1

RS_SetHigh(); //Then set RS pin high

else //If rs is 0

RS_SetLow(); //Then set RS pin high

E_SetHigh(); //Set E pin high to start the pulse

if (value & 0x80) //If bit #7 of the "value" is 1

D7_SetHigh(); //Then set the D7 pin high

else //Else

D7_SetLow(); //Set the D7 pin low

if (value & 0x40) //If bit #6 of the "value" is 1

D6_SetHigh(); //Then set the D6 pin high

else //Else

D6_SetLow(); //Set the D6 pin low

if (value & 0x20) //If bit #5 of the "value" is 1

D5_SetHigh(); //Then set the D5 pin high

else //Else

D5_SetLow(); //Set the D5 pin low

if (value & 0x10) //If bit #4 of the "value" is 1

D4_SetHigh(); //Then set the D4 pin high

else //Else

D4_SetLow(); //Set the D4 pin low

__delay_us(1); //Delay needed by the driver

E_SetLow(); //Set E pin low to finish the pulse

__delay_us(1); //Delay needed by the driver

E_SetHigh(); //Set E pin high to start the pulse

if (value & 0x08) //If bit #3 of the "value" is 1

D7_SetHigh(); //Then set the D7 pin high

else //Else

D7_SetLow(); //Set the D7 pin low

if (value & 0x04) //If bit #2 of the "value" is 1

D6_SetHigh(); //Then set the D6 pin high

else //Else

D6_SetLow(); //Set the D6 pin low

if (value & 0x02) //If bit #1 of the "value" is 1

D5_SetHigh(); //Then set the D5 pin high

else //Else

D5_SetLow(); //Set the D5 pin low

if (value & 0x01) //If bit #0 of the "value" is 1

D4_SetHigh(); //Then set the D4 pin high

else //Else

D4_SetLow(); //Set the D4 pin low

__delay_us(1); //Delay needed by the driver

E_SetLow(); //Set E pin low to finish the pulse

__delay_us(40); //Delay more than 37 us to implement the command

}


void lcd_command (uint8_t command)//Send command to LCD

{

lcd_send(command, 0); //Issue lcd_send function with RS = 0

}


void lcd_data (uint8_t data)//Send command to LCD

{

lcd_send(data, 1); //Issue lcd_send function with RS = 1

}


void lcd_init (uint8_t cursor, uint8_t blink) //Initialize LCD

{

lcd_command(0x30); //Try to use 8-bit interface

__delay_us(4200); //Delay for command implementation

lcd_command(0x30); //Try 8-bit interface one more time

lcd_command(0x28); //Set 4-bit interface, two lines

lcd_command(0x08); //Turn off the display

lcd_command(0x01); //Clear the display and reset the address to 0

__delay_us(4200); //Delay for command implementation

lcd_command(0x06); //Cursor move direction from left to right

lcd_command(0x0C | (cursor << 1) | blink); //Turn on display, set the cursor and blinking parameters

}


void lcd_write (char *s) //Write the string at the LCD

{

uint8_t i = 0; //Characters counter

while (s[i]) //While s[i] is not 0 (end of the string is C)

{

lcd_data(s[i]); //Send the character to LCD

i ++; //Increment the characters counter

}

}


void lcd_set_cursor (uint8_t x, uint8_t y) //Set cursor at the specified position

{

if (y > 2) //If we try to set the line number more than 2

y = 2; //Set the line #2

if (x > 16) //If we try to set the position number more than 16

x = 16; //Set the position #16

lcd_command(0x80 | ((y - 1) << 6) | (x - 1)); //Set the cursor position at the LCD

}


void lcd_create_char (uint8_t addr, uint8_t *data) //Create a custom character

{

if (addr > 7) //If the address is higher than 7

addr = 7; //Then set address as 7

lcd_command (0x40 | addr << 3); //Set the address of the CGRAM

for (uint8_t i = 0; i < 8; i ++) //Loop to send all 8 bytes

{

lcd_data (data[i]); //Send data to LCD

}

}

First, we include the “mcc.h” header file (line 1) to use the MCC-generated functions in the current file. Then we include the “lcd_1602_mcc.h” file (line 2). Actually we don’t necessarily need to include it because we don’t use any definitions from that header in the current file. The rest of the file contains the functions code.

Let’s begin with the function lcd_send (lines 4-50). This is the most important function here and it implements the low-level communication between the MCU and LCD. To understand better how it works, let’s consider the part of the timing diagram from the figure 9 of the datasheet that corresponds to the writing cycle (Figure 8).

Figure 8 - Communication between MCU and LCD
Figure 8 - Communication between MCU and LCD

It’s not shown in Figure 8 but first you need to set the proper level at the RS pin (0 for command, 1 for data). This is what we do in lines 6-9.

Then in line 10 we start the pulse at the E pin. As you remember (and as follows from Figure 8) the data is latched at the falling edge of the E pulse.

Now we need to send the upper nibble of the value via the pins D4-D7. We will do this bit by bit. This is a different approach than in the previous tutorial but it gives more flexibility because it doesn’t require all data pins of the LCD to be connected to the consequent pins of the same port of the MCU.

So we first check the bit #7 of the value (line 11). If it is 1 then we set the D7 pin high (line 12), otherwise (line 13) we set it low (line 14). Then we do the same with bit #6 and pin D6 (lines 15-18), bit #5 and pin D5 (lines 19-22), and bit #4 and pin D4 (lines 23-26).

In line 27, we perform the short delay of 1us which is needed by the LCD driver. Actually the minimal length of the E pulse is 230 ns according to the data sheet, but as we’re not going to display dynamic information, 1 us is fine.

In line 28, we set the E pin low to end the first E pulse. At this moment the data at the DB4-DB7 pins is read by the LCD driver.

Now we implement another 1us delay (line 29) which corresponds to pause between the E pulses (Figure 8), and start the next E pulse (line 30) to send the lower nibble of the data byte.

In lines 31-46, we check bits #3-#0 of the value and set the pins D7-D4 correspondingly, the same as we did in lines 11-26.

Then we need to implement a 1us pause (line 47) and set the E pin low to latch the data (line 48).

After that, we consider that the 8 bits of the data byte are sent to the LCD. Finally, we implement the 40us delay to complete the operation (line 49). According to the HD44780 driver data sheet the time of execution of most commands is 37us. So 40us is plenty of time for it to execute most of the commands. For the commands that need more execution time we will implement the additional delay beyond this function.

The lcd_command (lines 52-55) and lcd_data (lines 57-60) functions are very similar: they both consist of one line, in which the lcd_send function is invoked (lines 54 and 59). But in the lcd_command function the rs parameter is 0, and in the lcd_data function the rs parameter is 1. We could get rid of these functions and replace them with the lcd_send, which could do both functions, but using them makes the code more readable.

The lcd_init function (lines 62-73) initializes the LCD according to the routine suggested in Figure 24 of the datasheet of the HD44780 driver but with slight changes.

First we need to try to set the 8-bit data interface by sending the “Function set” 0x30 command (Figure 3) (line 64). Then we need to implement a delay of more than 4.1ms, so we implement the 4200us delay (line 65). After that we try to set the 8-bit data interface one more time by sending the 0x30 command (line 66). The datasheet recommends trying to set the 8-bit interface a third time, but even without doing this, it seems to work well.

Then we set the data interface of 4 bits and two display lines by sending the “Function set” 0x28 command (line 67). Next, we need to turn off the display by sending the “Display on/off control” 0x08 command (line 68). After that we issue the “Clear display” 0x01 command (line 69) and wait for another 4200us (line 70) because implementation of this command requires more time.

Then we send the “Entry mode set” 0x06 command (line 71) which will increment the address after each character, and thus set the text direction from left to right.

Finally we send the “Display on/off control” command one more time (line 72) but this time we turn the display on. Also, we send the values cursor and blink at the corresponding bits (#1 and #0, respectively) to set the cursor parameters.

And that’s about it for the initialization of the display. We can set more parameters if we’d like, such as auto-shifting of the display but we can do this separately if needed.

In lines 75-83 there is the lcd_write function. It displays the string ‘s’ in the LCD. The parameter s should be the c-type string with a terminating zero. This means that the string should end with the “0” character, which is the sign of the end of the line. This puts certain limitations, because we can’t display the character which is located at address 0 of the DDRAM but we can send it separately using the lcd_data function.

So, in line 77 we declare the variable i and assign its value as 0. This is the counter of the characters in the string. In line 78 we start the while loop which will be implemented while the current character is not 0. The content of the loop is quite simple. First, we send the current character to the LCD (line 80), and then we increment the character counter i (line 81).

In lines 85-92 there is the lcd_set_cursor function. In this function we consider that the first cursor position has the coordinates [1;1] but as the physical coordinates inside the display start with 0, we will need to subtract 1 from each parameter.

First, we need to limit the coordinates if we accidently set them too big. We check if the y is bigger than 2 (line 87) and in this case set it as 2 (line 88). Then we check if the x is bigger than 16 (line 89) and in this case set it as 16 (line 90). Then we send the “Set DDRAM address” command to set the proper cursor position x and y (line 91).

The last function, lcd_create_char, is located in lines 94-103. In this function we also first check if the address is bigger than 7 (line 96) and set it as 7 in this case (line 97). This is needed because there are only eight available custom characters with addresses 0 to 7.

In line 98, we send the “Set CGRAM address” command, in which we shift the addr parameter 3 bits to the left. This is needed because the last 3 bits set the line address within one character (see Figure 4). After that, we implement the for loop (line 99) inside which we send the 8 bytes to the LCD which will form the required character (line 101).

And that are all functions that are required for operation with the LCD in the current and following projects. Please save these two files, we will copy-paste them to other projects as well.

Now, let’s consider the last file left, “main.c”.

#include "mcc_generated_files/mcc.h"

#include "lcd_1602_mcc.h" //Include 1602 LCD file


const uint8_t smile[8] = {0x0E, 0x1F, 0x15, 0x1F, 0x15, 0x1B, 0x0E, 0x00}; //Array to generate the smiley character


void main(void)

{

// Initialize the device

SYSTEM_Initialize();


lcd_init(0, 0); //Initialize the LCD without cursor and blinking

lcd_create_char(1, smile);//Create a smile character

lcd_set_cursor(6, 1); //Set cursor at the position 6,1

lcd_write("Hello"); //Write "Hello"

lcd_set_cursor(5, 2); //Set cursor at the position 5,2

lcd_write("world \1"); //Write "world"


while (1) { }

}

As you can see, the program is very short. This is because the majority of the code was written in previously described files. In line 2 we include the “lcd_1602_mcc.h” file to use the LCD-related functions.

In line 4 we define the constant array smile of 8 elements of uint8_t type. This array will be used to generate the smiley character. If you look attentively, you may notice that the elements of the array correspond to the values presented in table 1.

In lines 6-19 there is the main function of the program. In lines 11-16 there are functions related to the LCD. In line 11, we initialize the LCD without the cursor or blinking. In line 12, we create a new character at address 1 using the array smile. In line 13, we set the cursor at the sixth position of the first line, and then write the text “Hello” in it (line 14). In line 15, we set the cursor at the fifth position of the second line, and then write the text “world ☻” there (line 16). Pay attention to the expression “\1” in the text. The backslash symbol makes the next character in the string the special one. For example “\n” means the new line, “\r” means carriage return, “\”” means the quote sign, etc. If the number follows the backslash, this means that we want to print the character with the address defined by this number. So the expression “\1” means the character at the address 1, which is the smiley that we defined in line 4.

In line 18 there is the main loop of the program which is empty now because we don’t need to do anything as a cycle in our program.

And that’s all about the firmware. Now we can proceed to the practical work.

So assemble the device according to the schematics diagram (Figure 5), compile the program, and connect your PICkit to the PC. Configure the PICkit voltage according to your LCD type (5V or 3.3V) and run the program. You should see the text “Hello world ☻” on your LCD. If you don’t see the text even now, rotate the potentiometer handle to set the contrast, this should help. If you still don’t see anything then check the connections: there are a lot of wires in this device and it’s easy to mess up.

As homework, I suggest you experiment with the LCD parameters: cursor type, blinking, display shift, generation of several characters etc.

Make Bread with our CircuitBread Toaster!

Get the latest tools and tutorials, fresh from the toaster.

What are you looking for?