1602 LCD/Making it useful/10. Eight characters of your own
Making it useful · 10 of 11

Eight characters of your own

Eight slots of five by eight dots. It sounds like a novelty until you use five of them to turn a sixteen-character row into a bar with eighty steps.

A character is eight bytes

The controller keeps eight cells of writable character memory. Each one is the same shape as every other cell on the display — five dots across, eight down — and you describe it as eight numbers, one per row, of which only the low five bits are read.

Draw it, and read the eight numbers off
21 dots lit
Start from
Cell
5 × 8
Slots on the chip
8
Dots lit
21
Eight bytes in, one character out, and 8 of them at a time. createChar(0, heart) loads a slot; lcd.write(byte(0)) prints it. Write lcd.write(0) without the cast and the compiler cannot tell which overload you meant; write lcd.print(0) and you get the digit zero. Only the low five bits of each byte are read, which is why the patterns are written as B00000 to B11111 — you can see the picture in the source.

Written in binary you can see the picture in the source, which is why every example for these displays uses B01110 rather than 0x0E.

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

lcd.createChar(0, heart);   // in setup(), before anything is printed
lcd.write(byte(0));         // and this prints it

Two rules and then you are done. There are eight slots, numbered 0 to 7. And lcd.write(byte(0)) needs the cast: plain lcd.write(0) is ambiguous to the compiler, and lcd.print(0) prints the digit.

Five of the eight, spent well

A bar drawn out of solid blocks has sixteen steps, which looks like a loading screen from 1983. But the cell is five dots wide, so a set of characters with one, two, three, four and five columns filled lets the bar grow a dot at a time instead of a cell at a time.

A bar with eighty steps, from five characters
80 steps
Progress43%
Resolution
Steps across the row
80
CGRAM slots used
5
Smallest visible move
1.3%
16 cells × 5 dots = 80 steps, for five of the 8 slots. Design one character with the left column filled, one with two columns, and so on to five. Then for any percentage, work out how many dot columns should be lit: draw solid cells for the whole ones, one partial character for the remainder, and spaces for the rest. The spaces matter as much as the blocks — they are what erases the last bar.

Sixteen cells of five dots is eighty steps across one row, for five of the eight slots. The arithmetic is the only fiddly part: work out how many dot columns should be lit, draw solid cells for the whole ones, one partial character for the remainder, and spaces for everything after it.

The spaces are not decoration. They are what erases the last bar, and leaving them out is the ghost digit again in a different costume.

What the slots cost

They live in the controller's RAM and are gone at power-off, so createChar belongs in setup(). Loading one is eight character writes — about the same as printing eight letters — so loading all eight slots costs roughly what a third of a screen costs, once, at startup.

The code

progress_bar_1602.ino

Five custom characters — one column filled, then two, three, four, five — and the arithmetic that turns a percentage into a run of full cells, one partial cell and spaces for the rest.

// Wiring (Arduino Uno):
//
//   LCD GND -> GND
//   LCD VCC -> 5V
//   LCD SDA -> A4
//   LCD SCK -> A5
//
// Arduino IDE: Tools > Board "Arduino Uno", and
//   Sketch > Include Library > Manage Libraries... > "LiquidCrystal I2C"
//   by Frank de Brabander.

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

LiquidCrystal_I2C lcd(0x27, 16, 2);

const int WIDTH = 16;       // character cells across
const int DOTS  = 5;        // dot columns per cell
const int STEPS = WIDTH * DOTS;   // 80

// Five designs: 1, 2, 3, 4 and 5 columns filled, solid top to bottom so
// that neighbouring cells read as one bar across the row. There is still a
// one-dot gap between character cells; that is the display, not the code.
byte col1[8] = { B10000, B10000, B10000, B10000, B10000, B10000, B10000, B10000 };
byte col2[8] = { B11000, B11000, B11000, B11000, B11000, B11000, B11000, B11000 };
byte col3[8] = { B11100, B11100, B11100, B11100, B11100, B11100, B11100, B11100 };
byte col4[8] = { B11110, B11110, B11110, B11110, B11110, B11110, B11110, B11110 };
byte col5[8] = { B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111 };

char line[17];

void drawBar(int percent, int row) {
  if (percent < 0) percent = 0;
  if (percent > 100) percent = 100;

  // (long) keeps 100 * 80 inside range on a 16-bit int.
  int lit  = (int)((long)percent * STEPS / 100);
  int full = lit / DOTS;
  int rest = lit % DOTS;

  lcd.setCursor(0, row);
  for (int cell = 0; cell < WIDTH; cell++) {
    if (cell < full)                  lcd.write(byte(4));       // col5
    else if (cell == full && rest)    lcd.write(byte(rest - 1)); // col1..col4
    else                              lcd.print(' ');            // erases
  }
}

void setup() {
  lcd.init();
  lcd.backlight();

  lcd.createChar(0, col1);
  lcd.createChar(1, col2);
  lcd.createChar(2, col3);
  lcd.createChar(3, col4);
  lcd.createChar(4, col5);
}

void loop() {
  for (int p = 0; p <= 100; p++) {
    drawBar(p, 0);
    snprintf(line, sizeof(line), "%3d%%", p);
    lcd.setCursor(6, 1);
    lcd.print(line);
    delay(60);
  }
  delay(800);
}

The spaces at the end are not padding. They are what erases the previous bar, which is the same rule as the ghost digit one page back: a print only covers the cells it writes.

When it does not work

lcd.write(0) will not compile

Slot 0 collides with the null character, so the compiler cannot tell which overload you meant. Write lcd.write(byte(0)). And lcd.print(0) is a different thing again — it prints the digit zero.

My custom character shows as a strange letter

createChar() has not run, or it ran after the print. Load every slot in setup() before anything is drawn, and remember the controller forgets them on a power cycle — they live in RAM, not in the chip's ROM.

The ninth custom character overwrites the first

There are eight slots and no more, addressed 0 to 7. createChar(8, ...) masks down to slot 0. If you need more shapes than eight on screen at once, a character display is the wrong part.

The bar leaves a fragment behind when it shrinks

The loop that draws it has to print spaces over the cells past the end of the bar. Drawing only the filled part leaves the last, longer bar showing — the same cause as the ghost digit.

Custom characters disappear after I call createChar again

CGRAM and the display share one address counter, so createChar leaves the controller pointing into character memory. The library's setCursor puts it back; call setCursor before your next print if a character lands somewhere strange.

Where this goes next

The five things the glass can be showing you, and the order to check them in.

When the screen stays blank

Edit this page — content/books/lcd1602/eight-characters-of-your-own.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

3-Pack 1602 LCD Display Module, I2C 16x2 Blue

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum