LCD Custom Character Generator for 16x2 and 20x4 Displays

Click the cells to draw a 5x8 glyph, then copy the ready createChar() sketch. Works with any HD44780 character LCD, 16x2, 16x4, 20x2 or 20x4, wired in parallel or over an I2C backpack. Everything runs in your browser.

SchemasLibraries
Color:
Microcontroller:
Interface:
Data type:

How custom characters work on an HD44780 LCD

A 16x2 module does not draw free pixels. It draws characters out of a font table burned into the controller, called CGROM. Next to that fixed table sits 64 bytes of writable memory, CGRAM, and that is what the grid above is editing.

One character cell is 5 pixels wide and 8 pixels tall, so a glyph costs 8 bytes, one byte per row. Only the lower 5 bits of each byte are used, which is why the generated values never go above 31 (B11111). 64 bytes divided by 8 gives the well known limit: eight custom characters at a time, stored in slots 0 to 7.

Here is a heart glyph, the way the controller sees it:

RowPixelsBinaryHex
0. . . . .B000000x00
1. # . # .B010100x0A
2# # # # #B111110x1F
3# # # # #B111110x1F
4# # # # #B111110x1F
5. # # # .B011100x0E
6. . # . .B001000x04
7. . . . .B000000x00

Binary and hex describe the same bits, so pick whichever you find easier to read. Binary lines up with the drawing, hex is shorter when you are storing several glyphs.

Using the generated code in your sketch

Parallel wiring, LiquidCrystal:

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // RS, E, D4, D5, D6, D7

byte heart[8] = {
  B00000, B01010, B11111, B11111,
  B11111, B01110, B00100, B00000
};

void setup() {
  lcd.begin(16, 2);
  lcd.createChar(0, heart);  // after begin(), not before
  lcd.home();                // leave CGRAM address space
  lcd.write((uint8_t)0);
}

void loop() { }

I2C backpack, LiquidCrystal_I2C:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2); // 0x3F on some backpacks

void setup() {
  lcd.init();
  lcd.backlight();
  lcd.createChar(0, heart);
  lcd.setCursor(0, 0);
  lcd.write((uint8_t)0);
}

The bytes are identical in both cases. An I2C backpack is only a port expander in front of the same HD44780, so nothing about the glyph changes. For a 20x4 module, change the constructor to LiquidCrystal_I2C lcd(0x27, 20, 4) and leave the array alone.

Several characters at once:

lcd.createChar(0, heart);
lcd.createChar(1, degree);
lcd.createChar(2, battery);

lcd.setCursor(0, 0);
lcd.write((uint8_t)0);
lcd.write((uint8_t)1);
lcd.print(" 23");
lcd.write((uint8_t)1);   // degree sign
lcd.print("C");

MicroPython, same bytes:

lcd.custom_char(0, bytearray([0x00, 0x0A, 0x1F, 0x1F,
                              0x1F, 0x0E, 0x04, 0x00]))
lcd.putstr(chr(0))

Getting past the eight character limit

Eight is a hardware limit of CGRAM, not of the library, so no library can raise it. What you can do is treat the eight slots as a cache and rewrite them while the sketch runs:

Rewriting a slot pushes 8 bytes to the controller, which over an I2C backpack costs real time. Call it when the display actually needs to change, not on every pass of loop().

One more detail on small boards: createChar() reads from RAM, so a PROGMEM array cannot be passed straight to it. Copy it into a buffer first with memcpy_P(buf, glyph, 8), then pass buf.

Common problems

What you seeCauseFix
Nothing prints at alllcd.print(0) with slot 0Code 0 is the string terminator, use lcd.write((uint8_t)0)
Glyph is blankcreateChar() ran before begin()Move it after begin() or init()
Random pixels in the first cellCursor left in CGRAM address spaceCall lcd.home() or lcd.setCursor() after createChar()
Glyph is squashed or shiftedArray is not exactly 8 bytes, or values exceed 31Regenerate the array above, do not hand edit it
Bottom row looks thicker than drawnUnderline cursor sits on row 8lcd.noCursor() or leave row 8 empty
Blocks or blank screen, no glyph at allContrast pot or wrong I2C addressTurn the trimpot, scan the bus, try 0x3F

Frequently asked questions

How many custom characters can a 16x2 LCD show?

Eight. The HD44780 has 64 bytes of CGRAM and a 5x8 glyph costs 8 bytes, so slots 0 to 7 are all you get at any one moment. Because you can overwrite a slot at runtime, the limit is eight per screen, not eight per program.

Does this generator work with a 20x4 LCD?

Yes. 16x2, 16x4, 20x2 and 20x4 modules all run the same HD44780 controller with the same CGRAM, so the byte array is identical. Only the constructor changes, for example LiquidCrystal_I2C lcd(0x27, 20, 4).

Can I use custom characters on an I2C LCD?

Yes. Switch Interface to I2C above and the generator emits a LiquidCrystal_I2C sketch. The glyph bytes do not change, the backpack only changes how they reach the controller.

What is CGRAM?

CGRAM is Character Generator RAM, the small writable font area of the controller. CGROM holds the fixed built in font, CGRAM holds your eight editable glyphs. createChar() writes 8 bytes into it, and each byte uses only its lower 5 bits because a cell is 5 pixels wide.

Why does lcd.print(0) not show my character?

Character code 0 is also the C string terminator, so print() stops there. Use lcd.write((uint8_t)0) or lcd.write(byte(0)). Codes 8 to 15 mirror slots 0 to 7, so lcd.write(8) is another way around it.

Why is my character blank or full of garbage pixels?

Usually one of three things: createChar() was called before begin(), the cursor was left in CGRAM address space, or the array is not exactly 8 bytes with values in the 0 to 31 range. The table above lists the fix for each.

Can I use the generated bytes in MicroPython?

Yes. With the standard MicroPython HD44780 driver it is lcd.custom_char(0, bytearray([...])) followed by lcd.putstr(chr(0)). Only the syntax differs, the eight bytes are the same.

Is there a download or an offline version?

Nothing to install. The editor is plain JavaScript running in your browser, no drawing is uploaded anywhere, and the page keeps working if you go offline after it has loaded.

What about 5x10 fonts?

A few HD44780 modules can run a 5x10 font, and in that mode CGRAM holds only four glyphs instead of eight. Almost every 16x2 and 20x4 module sold today is 5x8, which is what this generator produces.

Related tools