Custom Glyphs on LCD Displays with CircuitPython

Quick Summary
Learn how to create custom glyphs on LCD character displays using CircuitPython. Step-by-step guide with byte arrays, I2C setup, and creative tips.
In This Article
Custom Glyphs on LCD Character Displays with CircuitPython
There's a moment — and if you've spent any time tinkering with microcontrollers, you'll know exactly the one I mean — where a tiny screen lights up and shows something you made. Not a demo string, not a hello-world placeholder, but a little skull, or a lightning bolt, or a custom icon you pixel-crafted yourself. I had that moment the first time I pushed a custom glyph onto an LCD character display using CircuitPython, and I haven't looked at these humble little screens the same way since.
LCD character displays are deceptively simple. Sixteen characters across, two rows down, and a built-in library of standard ASCII characters that gets you through most use cases without a second thought. But hiding inside that modest hardware is something genuinely exciting: eight open slots where you can store your own custom characters. Eight chances to make this display entirely your own. Today, I want to walk you through exactly how to use them — and push you a little further than the basics.
What Is a Custom Glyph on an LCD Character Display?
Before we dive into code, let's talk about what these custom characters actually are, because understanding the structure makes everything else click into place.
Each character on a standard LCD character display lives inside a 5-pixel-wide by 8-pixel-tall grid. Think of it like a tiny canvas — 40 individual dots, each either on or off. When you combine those dots intentionally, you get shapes: letters, symbols, icons, faces. The display's built-in character library uses this grid to render the alphabet, numbers, and common punctuation. But the hardware reserves eight additional slots — slots 0 through 7 — specifically for user-defined characters. These are yours to fill however you like.
Each row of that 5x8 grid is represented by a single byte, but only the lower five bits matter. So each custom character is defined by an array of eight bytes, one per row, where a 1 bit lights up a pixel and a 0 leaves it dark. That's it. That's the whole system. And once you see it laid out that way, the creative possibilities start multiplying fast.
One practical note worth keeping in mind: most standard fonts on these displays actually use only 5x7 pixels, leaving the bottom row empty. That blank row creates visual breathing room — space for a blinking cursor or just comfortable line spacing. You're not obligated to follow that convention, but it's worth knowing why it exists before you decide to break it.
Setting Up CircuitPython for I2C LCD Control
If you're working with an LCD character display backpack — the kind that slots neatly onto an Arduino-sized board like the Metro RP2040 — you'll be communicating over I2C, and the Adafruit CharLCD library makes this refreshingly straightforward.
Your setup will look something like this:
import board
import busio
import adafruit_character_lcd.character_lcd_rgb_i2c as character_lcd
i2c = busio.I2C(board.SCL, board.SDA)
lcd_columns = 16
lcd_rows = 2
lcd = character_lcd.Character_LCD_RGB_I2C(i2c, lcd_columns, lcd_rows)
Once you've initialised the display, you can set the backlight colour, clear the screen, and start sending messages. The lcd.message method handles standard string output without any fuss. But the interesting part — the part that separates a functional display from a characterful one — is what comes next.
Designing Your Custom Characters as Byte Arrays
This is where I genuinely encourage you to slow down and enjoy the process. Designing custom glyphs is part logic puzzle, part pixel art, and there's something deeply satisfying about it.
Let's take a skull as an example. Sketch it out on graph paper or in your head: the outer curve of the cranium, two hollow eye sockets, a small nose, the jaw. Now translate that into a 5x8 binary grid. A row like 10101 gives you the sides of the skull with a gap in the middle — that gap becomes the nose cavity. Eye sockets are represented by 0 bits surrounded by 1 bits forming the skull walls.
In Python, you'd define that character like this:
skull = bytes([
0b01110,
0b10001,
0b10101,
0b11011,
0b01110,
0b01110,
0b00000,
0b00000
])
Using binary literals (0b) instead of decimal integers is a habit I'd strongly recommend building early. Seeing the actual bit pattern laid out in the code means you can read the glyph visually without doing mental arithmetic. Your future self will thank you.
Define as many characters as you need — up to eight — and store each one in a variable. Keep them at the top of your script, clearly labelled. When you're juggling multiple custom icons, clarity in your code is what keeps projects from becoming archaeology expeditions.
Storing and Calling Custom Glyphs in CircuitPython
Once your byte arrays are defined, storing them on the display is a single method call per character. You assign each glyph to one of the eight available slots (0–7) before you start rendering:
lcd.create_char(0, skull)
lcd.create_char(1, empty_box)
lcd.create_char(2, lightning)
# ... and so on
To render a custom character within a message, you reference it by its slot number using a special byte value. In CircuitPython's Adafruit library, this typically means embedding the slot index directly as a character in your string — check the library's documentation for the exact syntax, as it can vary slightly between versions.
One elegant approach I've found myself returning to: loop through all eight slots sequentially, moving the cursor position before each print. This lets you animate or stagger the appearance of icons across the display, giving even a simple information screen a bit of personality. A small time.sleep() between prints creates a reveal effect that feels polished without requiring a single line of complex logic.
import time
for i, char in enumerate(custom_chars):
lcd.cursor_position(i * 2, 1)
lcd.message = chr(i)
time.sleep(0.1)
That little loop — eight characters, each nudged two columns apart on the bottom row — is the kind of thing that makes a demo feel alive rather than static.
Creative Applications: Going Beyond Basic Icons
Here's where things get genuinely interesting, and where most tutorials stop just short of the good stuff.
Because each custom character occupies one character cell on the display, and because you can position those cells adjacently, you can combine multiple glyphs to simulate larger icons. Picture a 2x2 block of character cells — that's a 10-pixel-wide by 16-pixel-tall canvas, which is more than enough space to render recognisable logos, progress bars, animated sprites, or even rudimentary graphs.
A classic use case is a battery level indicator: four character cells in a row, each representing 25% charge. As the battery drains, you swap in progressively emptier glyph variants. The user sees a smooth, intuitive indicator. Behind the scenes, you're just cycling through pre-loaded byte arrays.
Another popular technique is horizontal bar graphs. Define five or six glyphs representing progressively wider filled rectangles — from a single left-column pixel to a fully filled cell — and you can render smooth-looking data visualisations on hardware that technically only knows how to display text.
Animated characters are also entirely possible. Since you can overwrite a slot's stored glyph at runtime with a new create_char() call, you can cycle through animation frames by updating slot 0, rendering it, waiting a beat, updating slot 0 again with the next frame, and so on. The display refreshes quickly enough to make simple animations look fluid.
Just remember: you only have eight slots. Every project is a small creative constraint exercise in prioritising which glyphs earn their place.
Practical Tips From Real Tinkering Sessions
A few things I've learned the slightly painful way, offered here so you don't have to repeat my exact mistakes:
Use a glyph designer tool. There are several free web-based LCD character designers that let you click pixels on a grid and generate the byte array automatically. Searching for "LCD custom character generator" will surface several good options. They save time and reduce the squinting-at-binary errors that sneak in when you're designing complex glyphs by hand.
Watch your text editor's auto-formatting. If you've laid out your byte arrays in a tidy vertical format that visually represents the pixel rows, some editors will helpfully reformat them into a single line — completely destroying the visual clarity that made them readable in the first place. Configure your formatter to leave these arrays alone, or accept the chaos and rely on comments instead.
Free Weekly Newsletter
Enjoying this guide?
Get the best articles like this one delivered to your inbox every week. No spam.
Initialise your custom characters before clearing the screen. The order of operations matters: store your glyphs first, then clear the display, then start writing messages. Clearing the LCD after storing characters can behave unexpectedly on some hardware revisions.
Test your glyphs one at a time. When I'm designing a new set of icons, I print each one in isolation on a clean screen before composing them into a final layout. Catching a misaligned row in isolation is far easier than hunting for it once six other glyphs are competing for your attention.
Leave the bottom pixel row empty when in doubt. As mentioned earlier, that 8th row of pixels sits right at the baseline of the character cell. Filling it can create a visual smear between rows on some displays, and it leaves no room for the cursor if you're using cursor display features. The safe default is to keep it blank.
Bringing Your LCD Display to Life with CircuitPython
LCD character displays get dismissed as old technology, and I understand why. They're not flashy. They don't do colour gradients or smooth animations or high-resolution graphics. But there's something deeply appealing about their constraints — the way they force you to communicate clearly, to distil information down to sixteen characters per line and make every one of them count.
Adding custom glyphs to that mix changes the relationship between maker and display. Suddenly, that little screen isn't just repeating back text you fed it. It's showing your symbols, your icons, your visual language. And with CircuitPython making the implementation so approachable — byte arrays, a single library, a handful of method calls — the barrier to entry has never been lower.
Start simple. Make a smiley face. Make a lightning bolt. Make a tiny house or a musical note or whatever small symbol means something to your project. Get comfortable with the 5x8 grid and the eight-slot system. Then start combining glyphs, experimenting with animation, building progress bars and custom indicators. The techniques scale naturally once the fundamentals are in your hands.
Your LCD character display has been waiting patiently. Give it something worth showing.
Frequently Asked Questions
How many custom characters can I store on an LCD character display?
Most standard LCD character displays — including the common 16x2 and 20x4 varieties — support exactly eight custom character slots, numbered 0 through 7. This limit is hardware-defined by the HD44780 controller chip (or compatible alternatives) that powers the vast majority of these displays. You can overwrite slots at runtime to simulate more than eight unique glyphs, but only eight can be stored simultaneously.
What is the pixel size of a custom character on an LCD display?
Each character cell on a standard LCD character display is 5 pixels wide by 8 pixels tall. However, conventional font design typically uses only the top 7 rows (5x7), leaving the bottom row empty for cursor display and visual spacing between lines. You can use the full 8 rows if needed, but be aware this may cause the character to visually merge with the row below on some displays.
Can I animate custom characters in CircuitPython?
Yes, animation is possible and surprisingly effective even on basic hardware. Since you can call create_char() to overwrite any slot at runtime, you can cycle through animation frames by repeatedly updating a slot with new byte data and re-rendering the character. Simple two- or three-frame animations — a blinking icon, a spinning indicator, a pulsing symbol — are well within reach and can be implemented with a basic loop and time.sleep() for timing control.
Do I need special hardware to use custom glyphs with CircuitPython?
No special hardware is required beyond a standard LCD character display. The most common and beginner-friendly setup is an I2C backpack attached to the display, which reduces wiring complexity significantly and works seamlessly with CircuitPython's adafruit_character_lcd library. Boards like the Adafruit Metro RP2040, the Raspberry Pi Pico, and most CircuitPython-compatible microcontrollers will work. You'll need the Adafruit CharLCD library installed in your lib folder on the device.
Can custom characters be used alongside standard ASCII characters?
Absolutely — and this is one of the most useful aspects of the system. Custom characters live in slots 0–7 of the display's character RAM, completely separate from the built-in ASCII character set. You can mix them freely in the same message string, placing custom glyphs next to regular text without any conflict. This makes it easy to add icons or symbols inline with readable labels, such as a battery icon followed by a percentage reading, all on a single line.
Frequently Asked Questions
What Is a Custom Glyph on an LCD Character Display?
Before we dive into code, let's talk about what these custom characters actually are, because understanding the structure makes everything else click into place.
Each character on a standard LCD character display lives inside a 5-pixel-wide by 8-pixel-tall grid. Think of it like a tiny canvas — 40 individual dots, each either on or off. When you combine those dots intentionally, you get shapes: letters, symbols, icons, faces. The display's built-in character library uses this grid to render the alphabet, numbers, and common punctuation. But the hardware reserves eight additional slots — slots 0 through 7 — specifically for user-defined characters. These are yours to fill however you like.
Each row of that 5x8 grid is represented by a single byte, but only the lower five bits matter. So each custom character is defined by an array of eight bytes, one per row, where a 1 bit lights up a pixel and a 0 leaves it dark. That's it. That's the whole system. And once you see it laid out that way, the creative possibilities start multiplying fast.
One practical note worth keeping in mind: most standard fonts on these displays actually use only 5x7 pixels, leaving the bottom row empty. That blank row creates visual breathing room — space for a blinking cursor or just comfortable line spacing. You're not obligated to follow that convention, but it's worth knowing why it exists before you decide to break it.
Setting Up CircuitPython for I2C LCD Control
If you're working with an LCD character display backpack — the kind that slots neatly onto an Arduino-sized board like the Metro RP2040 — you'll be communicating over I2C, and the Adafruit CharLCD library makes this refreshingly straightforward.
Your setup will look something like this:
import board
import busio
import adafruit_character_lcd.character_lcd_rgb_i2c as character_lcd
i2c = busio.I2C(board.SCL, board.SDA)
lcd_columns = 16
lcd_rows = 2
lcd = character_lcd.Character_LCD_RGB_I2C(i2c, lcd_columns, lcd_rows)
Once you've initialised the display, you can set the backlight colour, clear the screen, and start sending messages. The lcd.message method handles standard string output without any fuss. But the interesting part — the part that separates a functional display from a characterful one — is what comes next.
Designing Your Custom Characters as Byte Arrays
This is where I genuinely encourage you to slow down and enjoy the process. Designing custom glyphs is part logic puzzle, part pixel art, and there's something deeply satisfying about it.
Let's take a skull as an example. Sketch it out on graph paper or in your head: the outer curve of the cranium, two hollow eye sockets, a small nose, the jaw. Now translate that into a 5x8 binary grid. A row like 10101 gives you the sides of the skull with a gap in the middle — that gap becomes the nose cavity. Eye sockets are represented by 0 bits surrounded by 1 bits forming the skull walls.
In Python, you'd define that character like this:
skull = bytes([
0b01110,
0b10001,
0b10101,
0b11011,
0b01110,
0b01110,
0b00000,
0b00000
])
Using binary literals (0b) instead of decimal integers is a habit I'd strongly recommend building early. Seeing the actual bit pattern laid out in the code means you can read the glyph visually without doing mental arithmetic. Your future self will thank you.
Define as many characters as you need — up to eight — and store each one in a variable. Keep them at the top of your script, clearly labelled. When you're juggling multiple custom icons, clarity in your code is what keeps projects from becoming archaeology expeditions.
Storing and Calling Custom Glyphs in CircuitPython
Once your byte arrays are defined, storing them on the display is a single method call per character. You assign each glyph to one of the eight available slots (0–7) before you start rendering:
lcd.create_char(0, skull)
lcd.create_char(1, empty_box)
lcd.create_char(2, lightning)
Creative Applications: Going Beyond Basic Icons
Here's where things get genuinely interesting, and where most tutorials stop just short of the good stuff.
Because each custom character occupies one character cell on the display, and because you can position those cells adjacently, you can combine multiple glyphs to simulate larger icons. Picture a 2x2 block of character cells — that's a 10-pixel-wide by 16-pixel-tall canvas, which is more than enough space to render recognisable logos, progress bars, animated sprites, or even rudimentary graphs.
A classic use case is a battery level indicator: four character cells in a row, each representing 25% charge. As the battery drains, you swap in progressively emptier glyph variants. The user sees a smooth, intuitive indicator. Behind the scenes, you're just cycling through pre-loaded byte arrays.
Another popular technique is horizontal bar graphs. Define five or six glyphs representing progressively wider filled rectangles — from a single left-column pixel to a fully filled cell — and you can render smooth-looking data visualisations on hardware that technically only knows how to display text.
Animated characters are also entirely possible. Since you can overwrite a slot's stored glyph at runtime with a new create_char() call, you can cycle through animation frames by updating slot 0, rendering it, waiting a beat, updating slot 0 again with the next frame, and so on. The display refreshes quickly enough to make simple animations look fluid.
Just remember: you only have eight slots. Every project is a small creative constraint exercise in prioritising which glyphs earn their place.
Practical Tips From Real Tinkering Sessions
A few things I've learned the slightly painful way, offered here so you don't have to repeat my exact mistakes:
Use a glyph designer tool. There are several free web-based LCD character designers that let you click pixels on a grid and generate the byte array automatically. Searching for "LCD custom character generator" will surface several good options. They save time and reduce the squinting-at-binary errors that sneak in when you're designing complex glyphs by hand.
Watch your text editor's auto-formatting. If you've laid out your byte arrays in a tidy vertical format that visually represents the pixel rows, some editors will helpfully reformat them into a single line — completely destroying the visual clarity that made them readable in the first place. Configure your formatter to leave these arrays alone, or accept the chaos and rely on comments instead.
Initialise your custom characters before clearing the screen. The order of operations matters: store your glyphs first, then clear the display, then start writing messages. Clearing the LCD after storing characters can behave unexpectedly on some hardware revisions.
Test your glyphs one at a time. When I'm designing a new set of icons, I print each one in isolation on a clean screen before composing them into a final layout. Catching a misaligned row in isolation is far easier than hunting for it once six other glyphs are competing for your attention.
Leave the bottom pixel row empty when in doubt. As mentioned earlier, that 8th row of pixels sits right at the baseline of the character cell. Filling it can create a visual smear between rows on some displays, and it leaves no room for the cursor if you're using cursor display features. The safe default is to keep it blank.
Bringing Your LCD Display to Life with CircuitPython
LCD character displays get dismissed as old technology, and I understand why. They're not flashy. They don't do colour gradients or smooth animations or high-resolution graphics. But there's something deeply appealing about their constraints — the way they force you to communicate clearly, to distil information down to sixteen characters per line and make every one of them count.
Adding custom glyphs to that mix changes the relationship between maker and display. Suddenly, that little screen isn't just repeating back text you fed it. It's showing your symbols, your icons, your visual language. And with CircuitPython making the implementation so approachable — byte arrays, a single library, a handful of method calls — the barrier to entry has never been lower.
Start simple. Make a smiley face. Make a lightning bolt. Make a tiny house or a musical note or whatever small symbol means something to your project. Get comfortable with the 5x8 grid and the eight-slot system. Then start combining glyphs, experimenting with animation, building progress bars and custom indicators. The techniques scale naturally once the fundamentals are in your hands.
Your LCD character display has been waiting patiently. Give it something worth showing.
Frequently Asked Questions
How many custom characters can I store on an LCD character display?
Most standard LCD character displays — including the common 16x2 and 20x4 varieties — support exactly eight custom character slots, numbered 0 through 7. This limit is hardware-defined by the HD44780 controller chip (or compatible alternatives) that powers the vast majority of these displays. You can overwrite slots at runtime to simulate more than eight unique glyphs, but only eight can be stored simultaneously.
What is the pixel size of a custom character on an LCD display?
Each character cell on a standard LCD character display is 5 pixels wide by 8 pixels tall. However, conventional font design typically uses only the top 7 rows (5x7), leaving the bottom row empty for cursor display and visual spacing between lines. You can use the full 8 rows if needed, but be aware this may cause the character to visually merge with the row below on some displays.
Can I animate custom characters in CircuitPython?
Yes, animation is possible and surprisingly effective even on basic hardware. Since you can call create_char() to overwrite any slot at runtime, you can cycle through animation frames by repeatedly updating a slot with new byte data and re-rendering the character. Simple two- or three-frame animations — a blinking icon, a spinning indicator, a pulsing symbol — are well within reach and can be implemented with a basic loop and time.sleep() for timing control.
Do I need special hardware to use custom glyphs with CircuitPython?
No special hardware is required beyond a standard LCD character display. The most common and beginner-friendly setup is an I2C backpack attached to the display, which reduces wiring complexity significantly and works seamlessly with CircuitPython's adafruit_character_lcd library. Boards like the Adafruit Metro RP2040, the Raspberry Pi Pico, and most CircuitPython-compatible microcontrollers will work. You'll need the Adafruit CharLCD library installed in your lib folder on the device.
Can custom characters be used alongside standard ASCII characters?
Absolutely — and this is one of the most useful aspects of the system. Custom characters live in slots 0–7 of the display's character RAM, completely separate from the built-in ASCII character set. You can mix them freely in the same message string, placing custom glyphs next to regular text without any conflict. This makes it easy to add icons or symbols inline with readable labels, such as a battery icon followed by a percentage reading, all on a single line.
About Zeebrain Editorial
Our editorial team is dedicated to providing clear, well-researched, and high-utility content for the modern digital landscape. We focus on accuracy, practicality, and insights that matter.
More from Travel & Places
Related Guides
Keep exploring this topic
CircuitPython in 2026: Firefox, AI & What's New
Travel & Places · CircuitPython · Adafruit
CircuitPython I2S Audio: Testing Real Hardware in the Wild
Travel & Places · CircuitPython · I2S Audio
Things to Do in Fort Lauderdale: Your Ultimate 2025 Travel Guide
Travel & Places
Top 5 Mistakes Tourists Make (Avoid These!)
Travel & Places
Explore More Categories
Keep browsing by topic and build depth around the subjects you care about most.


