Printing Custom Characters on a Character LCD

Author: Dean Hall
Date: 2007/06/19
Copyright: Copyright 2007 Dean Hall. All rights reserved.

Abstract

This article discusses how to overwrite bytes in the CGRAM to display custom bit-map graphics on a Hitachi HD44780-compatible character LCD display. A method is described of transforming a raster image to an array of values that are a monochrome bit-map representation of the original.

Background

There are many types of LCD displays available on the market. The least expensive and most prevalent is the NxM character display (N=number of lines, M=number of characters per line). The majority of these displays use a Hitachi HD44780 (or compatible) chip. This chip provides a common interface to print characters, move the cursor, shift the display and, most importantly for this discussion, create custom characters on the display.

Technical information on the HD44780 can be found using Google. Here is one example page that has what we need.

Description

Picture of the face of Apu the monkey

All of my technical and robot projects fall under the umbrella of The Monkee Project. I chose as my mascot, the face of Apu the monkey from The Simpsons television show. So naturally, I wanted a way to print Apu on my LCD display.

I found a picture of Apu using http://images.google.com. Then I took some graph paper and a print-out of the Apu picture and rasterized the Apu image by hand. That is, I filled in blocks on the graph paper closest to the lines of the Apu image. Scaling was very important at this step. My title screen text uses thirteen characters on two lines, so I only had 2x3 characters remaining on my LCD for the Apu image.

On the LCD, each character is 8 pixels high by 5 pixels wide and a 1 pixel space between each character horizontally and 2 pixels vertically. I got that from the LCD's datasheet. So, a 2x3 character area gives me 17x18 pixels. I used the "Scale %" option of the print dialog to scale the Apu image print-out until it fit the 17x18 grid on my graph paper.

Once I had the rasterized image of Apu, I grouped the 5 horizontal pixels and converted those to numbers to create the 8 bytes of bitmap data for each character. If you want to create your own graphics, here is a web-based tool to draw the graphic you want and it will give you the bitmap data you need.

Software

My character LCD is connected to two latches on the address/data bus of an Atmel ATmega103 microcontroller. One latch is for the 8-bit data bus, the other is for the other LCD control pins. Mine is a custom circuit and your circuit to drive the LCD will be different. However, you should be able to adapt the software for your needs.

Here is the function that prints my title screen:

void
mmb_lcdTitleScreen(void)
{
    uint8_t i;

    char *str1 = "MonkeeProject\1\2\3";
    char *str2 = "on MMB103v1? \4\5\6";
    char apubytes[] =
    {
         4,  3, 15,  7, 15, 18, 18, 14, /* Char1: upper left  */
        31, 31, 31, 31,  3,  0, 12, 18, /* Char2: upper center*/
         0, 24, 28, 30, 18,  2, 12, 18, /* Char3: upper right */
        30, 15, 30, 12,  6,  2,  1,  0, /* Char4: lower left  */
        22, 12,  0, 17, 24, 30,  1, 30, /* Char5: lower center*/
        22, 12,  4, 18,  2,  4, 24,  0  /* Char6: lower right */
    };

    /* Put Apu chars in LCD's CGRAM */
    mmb_lcdPutCtl(LCD_CGRAM(1));
    mmb_sleepms(1);
    for (i = 0; i < sizeof(apubytes); i++)
    {
        mmb_lcdPutByte(apubytes[i]);
        mmb_sleepms(1);
    }

    /* Lcd msg */
    mmb_lcdSetLine(0);
    mmb_lcdPrintStr(str1);
    mmb_lcdSetLine(1);
    mmb_lcdPrintStr(str2);
}

The important thing to note here is that I inserted binary characters into the C string at the tail 3 positions of str1 and str2 by using \1, \2 and so on. The binary values in the string must match the address in CGRAM where you store your custom characters. In my program, this is done by inserting the binary values of 1-6 into the C string and then calling mmb_lcdPutCtl(LCD_CGRAM(1)); which makes the subsequent 6 calls to mmb_lcdPutByte(apubytes[i]); write into the CGRAM starting at address 1 and continuing to 6.

Here are the support macro and functions that I used in the above function. (The exact values of LCD_CTL_LATCH, LCD_DAT_LATCH and my bits for E, RS and RW don't matter to you because your hardware circuit is different):

/* set CGRAM addr */
#define LCD_CGRAM(n)        (0x40 + ((n) << 3))

void
mmb_lcdPutCtl(uint8_t val)
{
    /* Handle control message */
    /* E = 0; RS = 0; RW = 0 */
    LCD_CTL_LATCH = g_mmb.lcdCtl &= ~(LCD_E | LCD_RS | LCD_RW);

    /* E = 1 */
    LCD_CTL_LATCH = g_mmb.lcdCtl |= LCD_E;

    /* Set data; E high for 500ns (2 instruction cycles should do) */
    LCD_DAT_LATCH = val;
    LCD_DAT_LATCH = val;

    /* E = 0 */
    LCD_CTL_LATCH = g_mmb.lcdCtl &= ~LCD_E;
}

void
mmb_lcdPutByte(uint8_t ch)
{
    /* E = 0; RS = 1; RW = 0; data */
    g_mmb.lcdCtl &= ~(LCD_E | LCD_RW);
    LCD_CTL_LATCH = g_mmb.lcdCtl |=  LCD_RS;

    /* E = 1 */
    LCD_CTL_LATCH = g_mmb.lcdCtl |= LCD_E;

    /* data; E high for 500ns (2 instruction cycles should do) */
    LCD_DAT_LATCH = ch;
    LCD_DAT_LATCH = ch;

    /* E = 0 */
    LCD_CTL_LATCH = g_mmb.lcdCtl &= ~LCD_E;
}

void
mmb_lcdPrintStr(char const *s)
{
    /* For each character */
    while (*s)
    {
        /* put character on screen */
        mmb_lcdPrintChar(*s++);
        mmb_sleepms(1);
    }
}

Results

Picture of my LCD titlescreen with Apu

Here's a photo of my titlescreen including the image of Apu on the LCD. Not great, but not bad for 17x17 pixels. And here's Apu again for comparison.

Picture of the face of Apu the monkey