Character LCD basic operation

Character LCD basic operation

Platform Test Tools SAM3U2 Firmware nRF51422 Firmware Software
MPG1 USB RS-232 Converter MPG User Code
LCD Basic
AP2 Emulator IAR

Prerequisite Modules


Character LCDs are very common and quite easy to use in an embedded system. The majority of ASCII displays use the HD44780 standard driver from Hitachi so regardless of the vendor the interface is roughly the same. The datasheet for the ASCII LCD used on the development board is HERE. The driver supports the HD44780 instruction set and has some other features, too. The data interface is I²C plus control of the RGB backlight elements.


The MCU sends commands or character data to the LCD. Commands do things like turn on a cursor or set the cursor location. Data is the raw ASCII characters that will appear at the cursor location. Character LCDs have a built-in font table so they know what segments to turn on for the character codes that it receives. The cursor is usually advanced automatically when sending a string of characters. The cursor does not have to be visible for characters to print.

The displayable area of the screen is 20 characters x 2 lines, though the LCD RAM holds 40 characters per line so it can be used for scrolling longer messages quite easily. Each character has a 1-byte address. Nmemonics are defined in (brackets) for the main locations:

Line #      Left most address       Last printed char      Right most address
  1       0x00 (LINE1_START_ADDR)   0x13 (LINE1_END)    0x27 (LINE1_END_ABSOLUTE)      
  2       0x40 (LINE2_START_ADDR)   0x53 (LINE2_END)    0x67 (LINE2_END_ABSOLUTE)      


API Description

The driver manages all of the communications with the LCD and abstracts the interface to three basic function calls. There are no unique types required.

Public Functions
The following functions may be used by any application in the system.

  • void LCDMessage(u8 u8Address_, u8 *u8Message_) – Sends a text message to the LCD to be printed at the address specified.
  • void LCDClearChars(u8 u8Address_, u8 u8CharactersToClear_) – Clears a number of chars starting from the address specified. This function does not span rows.
  • void LCDCommand(u8 u8Command_) – Queues a command code to be sent to the LCD. The full command list is in the header file, but common commands are shown below including LCD_DISPLAY_CMD which is a compound command consisting of a base value that is ORed with other options:
LCD_CLEAR_CMD - Writes spaces to all chars
LCD_HOME_CMD - Puts cursor at 0x00

LCD_DISPLAY_CMD	- Root literal for managing display config
| LCD_DISPLAY_ON - OR with LCD_DISPLAY_CMD to turn display on
| LCD_DISPLAY_CURSOR - OR with LCD_DISPLAY_CMD to turn cursor on
| LCD_DISPLAY_BLINK - OR with LCD_DISPLAY_CMD to turn cursor blink on
Note: if the ORed value is not included, then the opposite action will occur.  


Checkout the latest Master branch and try the following examples in UserAppInitialize(). You can single-step through these since they are in the Initialize section of the firmware because the LCD task is forced to run during this time and complete each command. LCD commands and data in the main loop normally must wait for a few iterations for the messages to be sent out.

Write “Hello world!” to Line 1. What happens?

  u8 au8Message[] = "Hello world!";
  LCDMessage(LINE1_START_ADDR, au8Message);

Clear “ASC” from the screen.

  LCDClearChars(LINE1_START_ADDR + 13, 3);

Send the CLEAR command to remove the rest of the characters.




In this exercise you will write your name to LINE 1, add button labels, and turn on a blinking cursor. All of this code will be done in UserAppInitialize() since it only needs to run once.

Add UserApp_au8MyName[] in the Global variable space so it accessible to any function in the task. Initialize it with your name (maximum 20 characters).

Global variable definitions with scope limited to this local application.
Variable names shall start with "UserApp_" and be declared as static.
static u8 UserApp_au8MyName[] = "LCD Example";     

Use LCDMessage to write your name starting at the top left character position. Then use four individual calls to LCDMessage to add button labels ‘0’, ‘1’, ‘2’ and ‘3’. Be sure to determine the correct address for each label so the digit is directly above the button.

  /* Write name and button labels */
  LCDMessage(LINE1_START_ADDR, UserApp_au8MyName);
  LCDMessage(LINE2_START_ADDR, "0");
  LCDMessage(LINE2_START_ADDR + 6, "1");
  LCDMessage(LINE2_START_ADDR + 13, "2");
  LCDMessage(LINE2_END_ADDR, "3");

Build and test the code. You do not have to set breakpoints unless you want to see each action as it occurs. Setting up the LCD in UserAppInitialize() like this would be typical for an application so the screen is ready to go once the main loop is running.


Now move to UserAppSM_Idle() and use LCDCommand() to toggle the cursor blinking mode when BUTTON0 is pressed. The command argument must be built up by ORing the options you want, including keeping the display on.

static void UserAppSM_Idle(void)
  static bool bCursorOn = FALSE;
      /* Cursor is on, so turn it off */
      bCursorOn = FALSE;
      /* Cursor is off, so turn it on */
      bCursorOn = TRUE;
} /* end UserAppSM_Idle() */

Build and run the code and try turning on the cursor with BUTTON0. If you have followed these steps exactly then you should NOT see the cursor. Why?

Add the following line at the end of UserAppInitialize(), rebuild the code and test again. Now you should see the cursor.

  /* Home the cursor */


For the last step, make BUTTON3 advance the cursor one location forward and BUTTON2 move the cursor one location back. Be sure to properly wrap around the start and ends of the lines.

Start by adding a Global variable UserApp_CursorPosition to keep track of where the cursor is and initialize this in UserAppInitialize() as shown in the snippet.

Global variable definitions with scope limited to this local application.
Variable names shall start with "UserApp_" and be declared as static.
static u8 UserApp_au8MyName[] = "LCD Example";     
static u8 UserApp_CursorPosition;

void UserAppInitialize(void)
  /* Home the cursor */
  UserApp_CursorPosition = LINE1_START_ADDR;
} /* end UserAppInitialize() */

Now code the BUTTON3 action. There are three cases:

  1. The cursor is at the end of line 1 so must advance to the start of line 2.
  2. The cursor is at the end of line 2 so must advance to the start of line 1.
  3. The cursor is not at the end of any line so it is just incremented by one.

The predefined constants from the driver make coding this very simple. The only new step is using LCDCommand with a LCD_ADDRESS_CMD which requires ORing in the current address.

  /* BUTTON3 moves the cursor forward one position */
    /* Handle the two special cases or just the regular case */
    if(UserApp_CursorPosition == LINE1_END_ADDR)
      UserApp_CursorPosition = LINE2_START_ADDR;

    else if (UserApp_CursorPosition == LINE2_END_ADDR)
      UserApp_CursorPosition = LINE1_START_ADDR;
    /* Otherwise just increment one space */
    /* New position is set, so update */
    LCDCommand(LCD_ADDRESS_CMD | UserApp_CursorPosition);
  } /* end BUTTON3 */

The code to respond to BUTTON2 is practically identical so can be copied and paste and then adjusted accordingly. Do this and then build and run the code. Test to make sure the cursor behaves properly at the special cases. Testing edge cases is extremely important and should always be part of your code release process. Try moving the cursor while it is being displayed and while it is hidden.

Keeping track of the cursor in this example is quite easy even though you can move it when it is not being displayed. It would become much more difficult if messages or commands were sent by other functions in user_app or even other applications in the system. However, you should have a solid understanding of how to use the LCD now and will simply have to be careful as you develop applications that make use of it.