Learning Goals 5 min
- Explain that each LCD cell is a 5-wide × 8-tall pixel grid, and that the HD44780 lets you upload up to 8 custom characters at runtime.
- Design a custom icon on graph paper, convert the 8 rows into bytes, and load them with
lcd.createChar(slot, data). - Print the custom character with
lcd.write(slot)and use it for a real purpose — a thermometer bulb, a battery icon, an arrow pointer.
Warm-Up 10 min
The LCD's character ROM has ~190 fixed glyphs: ASCII letters, digits, some symbols, plus Japanese katakana or European accents. No degree symbol on row 1? Trick of \xDF. No battery icon? You design one. Custom characters are the LCD's "sprite" system.
Each character is 5×8 pixels
The five columns are wide; the rows are 8 tall (rows 0–7, though row 7 is reserved for the cursor and usually blank). You design pixel-by-pixel. Eight slots total — slots 0 through 7.
New Concept · Designing & loading a custom char 25 min
Drawing on graph paper
Draw a 5-wide × 8-tall grid. Fill in cells you want lit. Example — a simple heart:
Col→ 0 1 2 3 4 Row 0 . . . . . Row 1 . # . # . Row 2 # # # # # Row 3 # # # # # Row 4 . # # # . Row 5 . . # . . Row 6 . . . . . Row 7 . . . . .
Converting rows into bytes
Each row is 5 bits — written as a 5-bit binary number. Column 4 is the LSB (rightmost), column 0 is bit 4 (leftmost). For row 1 above (.#.#.) the bits are 0,1,0,1,0 → binary 01010 → decimal 10. In C++ that's 0b01010 or just 10.
Easier: just write each row as a 5-bit binary literal. For the heart:
byte heart[8] = {
0b00000,
0b01010,
0b11111,
0b11111,
0b01110,
0b00100,
0b00000,
0b00000
};Eight rows, eight bytes. The 8th row is conventionally all zeros (the cursor lives there).
Loading and printing
In setup, register the design in one of the 8 slots:
lcd.createChar(0, heart); // slot 0 now contains the heartAnywhere you want it to appear:
lcd.write((byte)0); // print whatever's in slot 0The (byte) cast is mandatory — without it, the compiler treats the bare 0 as a null terminator and prints nothing. lcd.write(byte(0)) works too.
The 8-slot limit
The HD44780 has exactly 8 slots of custom-character RAM. Need a 9th icon? Reload one of the slots with a different pattern. The reload takes effect immediately for any cell on screen — old appearances of that slot turn into the new icon. This is occasionally a feature (animation by swapping designs) but mostly a thing to be careful about.
Calling createChar resets the cursor
A documented quirk: after createChar(), the cursor jumps back to (0, 0). Always setCursor before printing again.
Worked Example · Heart + thermometer bulb 25 min
Step 1 — wiring
Same LCD as before. No new components.
Step 2 — the sketch
Save as custom-chars-demo.ino:
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
// Slot 0 — heart
byte heart[8] = {
0b00000,
0b01010,
0b11111,
0b11111,
0b01110,
0b00100,
0b00000,
0b00000
};
// Slot 1 — small thermometer bulb (centred dot)
byte bulb[8] = {
0b00100,
0b01010,
0b01010,
0b01010,
0b01010,
0b10001,
0b11111,
0b01110
};
// Slot 2 — up arrow
byte upArrow[8] = {
0b00100,
0b01110,
0b11111,
0b00100,
0b00100,
0b00100,
0b00100,
0b00000
};
// Slot 3 — down arrow
byte downArrow[8] = {
0b00100,
0b00100,
0b00100,
0b00100,
0b11111,
0b01110,
0b00100,
0b00000
};
void setup() {
lcd.begin(16, 2);
lcd.createChar(0, heart);
lcd.createChar(1, bulb);
lcd.createChar(2, upArrow);
lcd.createChar(3, downArrow);
// Use them — note the (byte) cast on slot 0
lcd.setCursor(0, 0);
lcd.write((byte)0); lcd.print(" Iconography "); lcd.write((byte)0);
lcd.setCursor(0, 1);
lcd.write(byte(1)); lcd.print(" 27C ");
lcd.write(byte(2)); lcd.print(" 28 ");
lcd.write(byte(3)); lcd.print(" 26");
}
void loop() {}Step 3 — upload, see your custom icons
Expected output on the LCD:
[♥] Iconography [♥] [bulb] 27C [^] 28 [v] 26
The brackets above indicate where your custom-character slots will render. On the real LCD you'll see actual heart, bulb, arrow icons.
Step 4 — design your own
Open up the sketch and add a 5th custom character of your choice. Some classic suggestions:
- Smiley face.
- Lightning bolt.
- Wifi signal-strength bars (could be 4 different slots).
- Speaker icon.
- A degree symbol (yes, the LCD has one built in, but yours can be a different style).
Draw on paper first. Then translate each row into 0b#####. Test on the LCD.
Step 5 — animate by reloading
Replace loop() with this:
byte frame1[8] = {0,0,0,0b11111,0,0,0,0};
byte frame2[8] = {0,0,0b11111,0,0b11111,0,0,0};
byte frame3[8] = {0,0b11111,0,0,0,0b11111,0,0};
void loop() {
lcd.createChar(4, frame1); lcd.setCursor(0, 1); lcd.write(byte(4)); delay(200);
lcd.createChar(4, frame2); lcd.setCursor(0, 1); lcd.write(byte(4)); delay(200);
lcd.createChar(4, frame3); lcd.setCursor(0, 1); lcd.write(byte(4)); delay(200);
}The single cell at (0, 1) cycles through three different patterns at 200 ms per frame — a tiny "equaliser bars" animation. Crude but proves the point: by reloading a slot we can do simple animations in a single cell.
Try It Yourself 20 min
Goal: Design and load a custom degree symbol that's slightly different from the built-in one — for example, a small filled circle in the top-right of the cell. Use it in a temperature display.
Hint
byte degSym[8] = {
0b01100,
0b10010,
0b10010,
0b01100,
0b00000,
0b00000,
0b00000,
0b00000
};A 3-pixel hollow circle near the top. Try variations — filled, lower-placed, bigger.
Goal: Build a 5-segment battery icon using just two cells side-by-side. Left cell = the outline + cap; right cell = the right edge. Then create 5 different "fill levels" (empty, 25%, 50%, 75%, full) by changing one of the slots.
Hint
Left-cell template (empty battery body):
byte batL_empty[8] = {
0b00011, 0b11111, 0b10001, 0b10001, 0b10001, 0b10001, 0b11111, 0b00011
};
byte batL_full[8] = {
0b00011, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b00011
};Right-cell is similar but mirrored, with the cap on the left. The two cells side by side give you a battery that looks ~10 pixels wide.
Goal: Make a horizontal progress bar across all 16 columns. The bar grows pixel-by-pixel (not cell-by-cell). You only need 6 custom characters — bar empty (cells beyond the progress) and 5 different fill widths (1/5, 2/5, 3/5, 4/5, full). The cells already-passed-by-the-progress show the "full" character; the current cell shows the appropriate partial-fill.
Hint
The 5 partial-fill designs:
byte p[5][8] = {
{0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b10000}, // 1/5
{0b11000,0b11000,0b11000,0b11000,0b11000,0b11000,0b11000,0b11000}, // 2/5
{0b11100,0b11100,0b11100,0b11100,0b11100,0b11100,0b11100,0b11100}, // 3/5
{0b11110,0b11110,0b11110,0b11110,0b11110,0b11110,0b11110,0b11110}, // 4/5
{0b11111,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111,0b11111} // 5/5
};
void drawBar(int pct) { // pct 0-100
int totalPixels = (pct * 80) / 100; // 16 cells × 5 px = 80
int fullCells = totalPixels / 5;
int partial = totalPixels % 5;
for (int c = 0; c < 16; c++) {
lcd.setCursor(c, 1);
if (c < fullCells) lcd.write(byte(4)); // full
else if (c == fullCells && partial > 0)
lcd.write(byte(partial - 1));
else lcd.print(' ');
}
}You only have 8 custom-char slots, and 5 of them are spoken for by the partial-fill bars. Plenty of budget. A real progress bar at single-pixel resolution feels surprisingly slick.
Mini-Challenge · Pictograph dashboard 15 min
Build a one-screen dashboard that uses at least 4 different custom characters to make sensor data more glance-able. Some pairings:
- Thermometer bulb + temperature number + degree symbol.
- Cloud icon + humidity %.
- Sun/moon + LDR percentage.
- Arrow icons showing whether the reading is rising or falling vs the last sample.
Use the L02-20 Weather Station as the data source. The whole point is to make the screen feel like a finished consumer product rather than a debug printout.
It's done when:
- You use at least 4 custom characters.
- The dashboard updates live every 2 seconds without ghosting or flicker.
- A non-engineer (parent / sibling) can look at the screen and say what each number means without you explaining.
- The 8-slot CGRAM budget is respected — no "reload to swap" tricks unless they're a deliberate animation.
Recap 5 min
Custom characters turn the LCD from a text terminal into a pictograph display. You design 5-wide × 8-tall bitmaps on paper, translate them into 8-byte arrays of binary literals, upload with lcd.createChar(slot, data), and print with lcd.write((byte)slot). The 8-slot CGRAM limit forces conscious design — pick the icons that earn their slot. Reload slots mid-sketch to animate, but only if you genuinely need motion in a single cell. Tomorrow we tie everything together: live sensor data, custom degree symbol, fixed-width formatting — the foundation of the Cluster E thermometer project.
- Custom character
- A user-defined 5×8 pixel glyph loaded into the LCD's CGRAM and addressable by a slot number 0–7.
- CGRAM (Character Generator RAM)
- The HD44780's 64-byte RAM that holds 8 custom 8-byte glyphs. Volatile — your designs disappear on power loss.
- 5 × 8 pixel grid
- The dimensions of each LCD cell. 5 columns wide, 8 rows tall. Row 7 is technically reserved for the cursor; design uses rows 0–6.
- Binary literal (
0b#####) - A way to write a number directly in binary in C++. Each row of a custom character is a 5-bit binary literal — 1 = pixel on, 0 = pixel off.
lcd.createChar(slot, data)- Uploads an 8-byte array to one of the LCD's 8 custom-char slots. Side effect: resets the cursor to (0, 0).
lcd.write((byte)slot)- Prints the custom character stored at
slot. The(byte)cast is mandatory; without it the compiler picks a wrong overload and prints nothing. - Animation by reload
- Loading different bitmaps into the same slot over time. Cheap one-cell animation; expensive bandwidth-wise for multi-cell animations.
- Pictograph dashboard
- A UI design philosophy: use icons to give meaning to numbers, so the screen reads at a glance instead of needing word labels.
Homework 5 min
Design four icons on graph paper, then code them. Sketch four 5×8 grids in your notebook. Themes:
- A weather icon (sun, cloud, rain, lightning — your pick).
- A status icon (✓, ✗, !, ?).
- A direction icon (arrow, compass needle).
- One of your own choice — make it something specific to a project you're building.
For each, fill in the grid with #/. then translate row-by-row into 0b#####. Code them up, load them into slots 0–3, and display all four side-by-side on the LCD.
Bring back next class:
- Your four graph-paper sketches (photograph or scan).
- The matching binary arrays in your sketch.
- A photo of all four icons rendered on the LCD.
- One sentence per icon: where would you use this in a real project?