MCS-51 Real-World interfacing - Character LCDs  


Continuing the classic embedded systems series, let’s look at another common display technology: character LCDs. In the late 1960s, segmented LCDs began to appear in control displays. The two major boosts were the development of the Twisted Nematic (TN) LCD in 1970, and the invention of the STN LCD in 1984, which allowed for better contrast.

Throughout the 1980s and 1990s, character LCDs became a staple in consumer electronics, from calculators to early portable computers. Many Gen Xers will remember devices like the Sharp PC-1500A, or some Xillenials, whose parents couldn’t afford a C64 or Commodore Amiga, and ended up with a V-Tech PreComputer.

Blog Image

Transitioning from 7-segment displays to character LCDs was a significant step up in terms of usability and information density. In the good old days, people learned BASIC on 16x2 or 20x4 character LCDs, which allowed for displaying not only numbers but also text, making them ideal for user interfaces in embedded systems. Even in the 2010s, I designed the first generation of needle-free injectors for veterinary applications that used 16x2 character LCDs for user feedback and configuration.

HD44780 LCD Controller

Unlike the 7-segment display or LED matrix display, character LCDs typically use a dedicated controller chip to manage the display. Nearly all character LCDs you find today are based on the Hitachi HD44780 LCD controller standard, which was introduced in the early 1980s and now exists in numerous clones. The controller itself comes with a built-in character generator ROM that supports a standard set of characters, including alphanumeric characters and some special symbols. In the early days, there were three variants of the HD44780 controller:

  • Japanese Standard Font
  • European Standard Font
  • Custom Font

These days, most LCDs come with a full ASCII and Japanese Katakana character set. All characters of the fonts are mapped to 8x5 dot matrix patterns and are stored as such in the character generator ROM.

Blog Image

Now the HD44780 controller can operate in two modes: 8-bit mode and 4-bit mode. In 8-bit mode, all eight data lines (D0-D7) are used for data transfer, allowing for faster communication. In 4-bit mode, only the upper four data lines (D4-D7) are used, which reduces the number of required I/O pins on the microcontroller but requires two data transfers for each byte of data. This was originally designed to support 4-bit microcontrollers, but is still widely used today to save I/O pins. I uploaded the full datasheet for the HD44780 controller for reference. A further simplification is to tie the R/W pin to ground, which makes the LCD write-only from the microcontroller’s perspective, as most operations are purely timeout-based and do not require reading back from the LCD if the timing is respected with an acceptable margin.

Blog Image

However, it turns out that despite only having an MCU with limited I/O pins, the HC6800-ES-V2.0 has the entire port P0 wired up to the LCD data lines D0-D7. So we can use the 8-bit mode for simplicity. RS, R/W, and E are connected to P2.6, P2.5, and P2.7, respectively. V0 is connected to a potentiometer for contrast adjustment.

Blog Image

Which matches the datasheet reference circuit quite well.

Blog Image

Initializing the Display

What we inherited from the kit is a 16x2 character LCD, which means it can display 16 characters per line and has 2 lines, and uses a 8x5 dot matrix for each character. To initialize the display, we need to send a series of commands to set it up for operation. The initialization sequence at minimum involves the following steps:

  1. Wait for the display to come out of reset (usually a few milliseconds).
  2. Set the function set command to configure the display for 8-bit mode, 2-line display, and 5x8 dot character font.

Each command is latched by setting the RS pin high. On the falling edge, the command is read from the data bus by the LCD controller.

  • The RS pin is set low for writes to the instruction register. It is set high for writes to the data register.
  • The R/W pin is set low for write operations. It is set high for read operations.

With each command there is an expectation for maximum execution time. For most commands, this is around 37 microseconds, but for some commands like clearing the display or returning the cursor to the home position, it can take up to 1.52 milliseconds. To keep things simple, we can use fixed delays after each command to ensure the LCD has enough time to process them. Let’s start with the command and pin interface.

#define HD44780_E  P2_7
#define HD44780_RS P2_6
#define HD44780_RW P2_5
#define HD44780_DATA P0

#define HD44780_FUNC_SET        0x30
#define HD44780_DISP_CLEAR      0x01
#define HD44780_DISP_OFF        0x08
#define HD44780_DISP_ON         0x0C
#define HD44780_CURSOR_ON       0x0E
#define HD44780_CURSOR_OFF      0x0C
#define HD44780_CURSOR_BLINK    0x0F
#define HD44780_RETURN_HOME     0x02
#define HD44780_ENTRY_MODE      0x06
#define HD44780_2_ROWS          0x08
#define HD44780_POSITION        0x80
#define HD44780_ROW2_START      0x40

Now we need to create some helper functions to send commands to the LCD.

void delay(uint16_t t) {
  while (t--) // Simple delay loop (more than 1us at 12MHz)
    ;
}

void hd44780_byte(uint8_t d) {
  HD44780_E = 1;
  HD44780_DATA = d;
  delay(10); // Enable pulse width
  HD44780_E = 0;
  delay(10); // Data hold time
}

void hd44780_command(uint8_t cmd) {
  HD44780_RS = 0; // Command mode
  HD44780_RW = 0; // Write mode
  hd44780_byte(cmd);
  delay(100); // Wait for command to process
}

Following Figure 23 from the HD44780 datasheet, we can implement the 8-bit initialization sequence as follows:

void hd44780_init() { // Figure 23 from HD44780 datasheet
  delay(15000); // Wait for more than 15ms after Vcc rises to 4.5V

  hd44780_command(HD44780_FUNC_SET);
  delay(5000); // Wait for more than 4.1ms
  hd44780_command(HD44780_FUNC_SET);
  delay(1000); // Wait for more than 1ms
  hd44780_command(HD44780_FUNC_SET);
  delay(100); // Wait for more than 100us

  hd44780_command(HD44780_FUNC_SET | HD44780_2_ROWS);
  hd44780_command(HD44780_DISP_OFF);
  hd44780_command(HD44780_DISP_CLEAR);
  hd44780_command(HD44780_ENTRY_MODE);
}

With the row offset on the second row starting at address 0x40, we can now turn this into a simple hello world program.

void main(void) {
  hd44780_init();
  hd44780_command(HD44780_DISP_ON);
  hd44780_text("Hello, World!");
  hd44780_command((HD44780_POSITION | HD44780_ROW2_START) | 1);
  hd44780_text("From 8051!");
  for(;;) {
  }
}

The result looks as follows.

Blog Image

The full code can be found on GitHub.

Custom Characters

Looking closely at the character set, you will see that there are 8 repeating unused character slots in the beginning column. The HD44780 controller allows us to define up to 8 custom characters by writing to the Character Generator RAM (CGRAM).

Blog Image

This is mapped to addresses 0x40 to 0x7F. Each row in the character generator rom is one row of the 5x8 dot matrix. So you can simply bitmask 8 bytes per character to define your own patterns. There are even online generators that help you for arduino that can be pasted in here. Let’s try a heart shape.

const uint8_t custom_char_heart[] = {
  0b00000,
  0b01010,
  0b11111,
  0b11111,
  0b11111,
  0b01110,
  0b00100,
  0b00000
};

Now we need to add the custom character to the CGRAM. We can do this by sending the appropriate command to set the CGRAM address,

#define HD44780_CGRAM_ADDR      0x40
#define HD44780_DRAM_ADDR       0x80

void hd44780_custom_char(uint8_t location, const uint8_t* charmap) {
  location &= 0x7; // We only have 8 locations 0-7
  hd44780_command(HD44780_CGRAM_ADDR | (location << 3)); // each location takes 8 bytes
  for (uint8_t i = 0; i < 8; i++) {
    hd44780_data(charmap[i]);
  }
  // Return to DDRAM
  hd44780_command(HD44780_DRAM_ADDR);
}

hd44780_custom_char(0, custom_char_heart);

Updating the main program to display the heart character:

void main(void) {
  hd44780_init();
  hd44780_custom_char(0, custom_char_heart);
  hd44780_command(HD44780_DISP_ON);
  hd44780_text("Hello, World!");
  hd44780_command((HD44780_POSITION | HD44780_ROW2_START) | 1);
  hd44780_data(0); // Custom heart character
  hd44780_text("From 8051!");
  hd44780_data(0); // Custom heart character
  for(;;) {
  }
}

The end result looks as follows.

Blog Image

Summary

Character LCDs based on the HD44780 controller are a staple in embedded systems, providing a simple and effective way to display text and custom characters. The internal controller RAM is set up in a way that it takes in ASCII characters directly. There is also the ability to define custom characters, which can be useful for creating simple graphics or symbols.

What I am missing on the dev kit is backlight control. This is hardwired on the dev kit. In a real-world application, you would typically want to control the backlight. Either via a dedicated pin or using PWM for brightness control. This can be a useful tool to save power or even from a UI perspective signal flashing error conditions.

As opposed to the 7-segment display, character LCDs are more versatile and can display a wider range of information. However, they come at the cost of reduced contrast and viewing angles compared to LED-based displays. It is a good compromise for many applications that operate indoors under constant lighting conditions. For outdoor or high-contrast applications, you might want to consider OLED, e-ink displays instead, or classical 7-segment displays for numeric-only applications.


Published: 2025-11-17
Updated  : 2025-11-17
Not a spam bot? Want to leave comments or provide editorial guidance? Please click any of the social links below and make an effort to connect. I promise I read all messages and will respond at my choosing.
← MCS-51 Real-World Interfacing - Dot-Matrix LCDs MCS-51 Real-World interfacing - 7 Segment Displays →