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.
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:
| Row | Pixels | Binary | Hex |
|---|---|---|---|
| 0 | . . . . . | B00000 | 0x00 |
| 1 | . # . # . | B01010 | 0x0A |
| 2 | # # # # # | B11111 | 0x1F |
| 3 | # # # # # | B11111 | 0x1F |
| 4 | # # # # # | B11111 | 0x1F |
| 5 | . # # # . | B01110 | 0x0E |
| 6 | . . # . . | B00100 | 0x04 |
| 7 | . . . . . | B00000 | 0x00 |
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:
- Swap per screen. A menu page that needs arrows and a settings page that needs a battery icon can reuse the same slot, calling
createChar()when the page changes. - Animate one slot. Writing a new 8 byte frame into slot 0 in a timer gives you a spinner or a filling bar without spending any more slots.
- Build wide graphics out of several cells. A progress bar is normally five or six glyph states placed side by side across a row, not one large image.
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 see | Cause | Fix |
|---|---|---|
| Nothing prints at all | lcd.print(0) with slot 0 | Code 0 is the string terminator, use lcd.write((uint8_t)0) |
| Glyph is blank | createChar() ran before begin() | Move it after begin() or init() |
| Random pixels in the first cell | Cursor left in CGRAM address space | Call lcd.home() or lcd.setCursor() after createChar() |
| Glyph is squashed or shifted | Array is not exactly 8 bytes, or values exceed 31 | Regenerate the array above, do not hand edit it |
| Bottom row looks thicker than drawn | Underline cursor sits on row 8 | lcd.noCursor() or leave row 8 empty |
| Blocks or blank screen, no glyph at all | Contrast pot or wrong I2C address | Turn 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.