LCD Character Display Animation with CircuitPython

Quick Summary
Learn how to create smooth LCD character display animations in CircuitPython using custom glyphs, CG RAM, and persistence of vision — no flicker, pure magic.
In This Article
Bringing Tiny Screens to Life with CircuitPython LCD Animation
There is something quietly thrilling about watching a tiny LCD screen flicker to life with a hand-crafted animation. No fancy graphics card. No GPU. Just a handful of ones and zeros arranged with intention, stored in a chip's memory, and cycled fast enough to trick your eye into seeing motion. I first experimented with LCD character displays expecting something dull and functional — a readout, a label, maybe a scrolling message. What I found instead was a surprisingly expressive little canvas, and CircuitPython makes the whole process feel almost playful. If you have ever wondered how to push LCD character display animation beyond static text, this is the deep dive you have been waiting for.
The technique at the heart of all of this is deceptively simple: you create custom characters, store them in the display's CG RAM, and then swap them in and out of a fixed screen position fast enough that persistence of vision does the rest. The result is a smooth, flicker-free animation running right alongside regular text — on a 16x2 display that costs a few dollars. Let's get into exactly how that works, why it matters, and how you can make it your own.
Understanding CG RAM and Custom Characters on LCD Displays
Most character LCD displays — the kind built around the HD44780 controller or compatible drivers — ship with a fixed set of characters baked into their ROM. You get the alphabet, numbers, punctuation, and a few symbols. What you do not get is flexibility. That is where CG RAM comes in.
CG RAM stands for Character Generator RAM, and it gives you eight slots to define completely custom 5x8 pixel characters. Each character is described by eight bytes, and only the lower five bits of each byte matter — that gives you a 5-pixel-wide grid, eight rows tall. Where you place a 1, a pixel lights up. Where you place a 0, it stays dark. It is pixel art in its most elemental form.
In CircuitPython, you work with these definitions as lists of binary values. Picture each row of your character as a five-digit binary number. Stack eight of those numbers and you have your glyph. The adafruit_character_lcd library handles the heavy lifting of pushing those definitions into the display's memory via I2C or SPI, leaving you free to focus on the creative side.
One important nuance worth knowing: the first memory slot — index zero — maps to the escape character \x00 in many environments, which can cause unexpected behaviour when you try to print it directly. A common workaround is to write the character definition fresh into slot zero on each animation frame rather than printing a stored index sequentially. It is a small quirk, but knowing about it saves you an infuriating debugging session.
Designing Animation Frames for an LCD Character Display
Here is where the craft really begins. Animation on an LCD character display works by defining multiple versions of the same character — each one a slightly different state — and then cycling through them in sequence at a position on screen. Because the display updates that single position quickly and your eye retains the previous image for a fraction of a second, you perceive smooth motion.
For a chase animation crawling around the border of a character cell, you might define eight frames. In frame one, a lit pixel sits at the top-left corner. In frame two, it has moved one step to the right along the top edge. By frame eight, it has completed a loop. Play those eight frames in order and you have a miniature marquee effect running inside a single character cell.
The design choice of using only a 5x5 area within the 5x8 grid is worth highlighting. Using the full height would create a taller rectangle, which looks stretched and odd at animation speed. Constraining the animation to a square sub-region gives a more satisfying visual result — and it frees up the bottom rows for secondary animation, like a single pixel bouncing back and forth in a Larson scanner style. Layering two behaviours into a single character cell is an elegant trick that rewards careful planning.
When designing your own frames, sketch them out on grid paper first. It sounds old-fashioned, but translating a visual idea into binary rows is much easier when you can see the shape in front of you before you start typing ones and zeros.
Writing the CircuitPython Code Step by Step
Let me walk you through the structure of a working implementation, so you can adapt it confidently rather than copy-pasting blindly.
Setting up the display object is your first task. Import the adafruit_character_lcd library, initialise your I2C bus, and create the LCD object with the correct column and row count — typically 16 columns and 2 rows for the most common shields. This is also where you tell the library about your hardware configuration.
Defining your frame data comes next. Create a list of frame definitions — each frame being a list of eight integers representing the pixel rows of your custom character. Keep these organised at the top of your script so they are easy to edit later.
Loading characters into memory happens inside your animation loop. On each frame, call create_character() with index 0 and the current frame's data. This writes the new glyph definition directly into CG RAM slot zero, overwriting the previous frame. Then set the cursor to your chosen position and write the character. Because you are only updating one character cell, there is no screen clear, no flicker, and no visible redraw artefact.
Adding regular text alongside your animation is perfectly fine. Write your static labels or readouts once before entering the animation loop. As long as your loop only touches the animated character's position, everything else on screen remains stable. This is what makes the technique so practical for real projects — you can show a status message, a sensor reading, or a project name while the animation runs in a corner doing its attention-grabbing work.
Controlling animation speed is a matter of inserting a small time.sleep() call between frames. Start around 0.1 seconds per frame and tune from there. Too fast and the animation blurs into an indistinct flicker. Too slow and the individual frames become obvious, breaking the illusion of motion.
Practical Uses Beyond Blinking Pixels
Once you have the mechanics down, the real question becomes: where does this actually add value in a project?
Progress indicators are the most obvious application. A loading bar built from custom characters feels far more polished than a static percentage number. You can define characters that show a cell filling from left to right — create six or eight variations and chain them across multiple character positions for a full-width progress bar with smooth sub-character resolution.
Activity spinners are another favourite. A single character cycling through four states — a dash, a forward slash, a pipe, and a backslash — creates a classic terminal-style spinner that tells users something is happening without taking up any meaningful screen space.
Status icons that pulse or blink with purpose, animated arrows pointing toward a button the user needs to press, countdown timers with a visual urgency component — all of these become possible once you think of custom characters not as static glyphs but as animation frames.
For makers building enclosures with small LCD readouts, these micro-animations add a layer of perceived quality that is completely disproportionate to the effort required. A project that shows a heartbeat pulse next to a heart rate reading feels finished in a way that plain numbers never quite do.
Hardware Considerations and Getting the Most from Your Setup
The Metro RP2040 paired with an LCD backpack over I2C is a particularly capable combination for this kind of work. The RP2040's dual cores and generous clock speed mean CircuitPython can handle the animation loop without breaking a sweat, and the I2C backpack reduces your wiring to just four connections — power, ground, SDA, and SCL.
If you are working with a different microcontroller, the same principles apply as long as your board runs CircuitPython and the adafruit_character_lcd library supports your display driver. The Adafruit CircuitPython library bundle covers most common LCD configurations, including those based on the MCP23008 I2C expander used on many LCD backpacks.
One hardware tip that pays off immediately: ensure your display's contrast is properly adjusted before you start worrying about animation quality. A poorly set contrast pot makes crisp pixel designs look muddy and undefined. A small screwdriver and thirty seconds of adjustment can make your custom characters look dramatically sharper.
Free Weekly Newsletter
Enjoying this guide?
Get the best articles like this one delivered to your inbox every week. No spam.
Also consider the ambient light conditions where your project will be used. Some LCD displays have backlight brightness adjustable in software through the library. Setting a comfortable brightness not only improves legibility but also reduces the eye fatigue that comes from staring at a maximally bright screen in a dim environment.
Making It Your Own: Extending the Technique
The eight-slot limit of CG RAM is a genuine constraint, but creative workarounds exist. If your animation only needs to occupy one position on screen at a time, you can reuse all eight slots for a single multi-frame animation and cycle through them freely — you have eight distinct frames, which is plenty for smooth motion. If you need animations at multiple screen positions simultaneously, you can interleave updates: write frame N to slot 0, display it at position A, then write a different frame to slot 1 and display it at position B, alternating rapidly enough that both appear to animate independently.
You can also combine animation with user interaction. A button press could trigger a different animation sequence — a checkmark assembling itself pixel by pixel, or an X drawing itself in four frames. These micro-responses make interfaces feel alive and responsive in ways that pure text feedback never achieves.
Sharing your frame definitions with other makers is another avenue worth exploring. The community around CircuitPython is generous and active. A library of pre-designed animation sets — spinners, progress bars, weather icons, arrows — would be a genuinely useful contribution that others could drop straight into their projects.
Conclusion: Small Screen, Big Personality
LCD character display animation in CircuitPython is one of those techniques that seems modest until you see it running. Then it becomes one of those things you want to put in every project. The combination of CG RAM's eight custom character slots, the persistence of vision effect, and CircuitPython's readable, approachable syntax makes the whole thing accessible to anyone willing to spend an afternoon experimenting.
Start with a simple spinner. Get comfortable with the frame definition syntax and the create_character() workflow. Then start designing your own glyphs on grid paper and see what you can build. The constraint of working within a 5x5 or 5x8 pixel grid is not a limitation — it is a creative challenge that produces some of the most satisfying results in all of maker electronics. Your tiny screen has more personality than you think. It is time to let it show.
Frequently Asked Questions
Q: How many custom characters can I store in CG RAM on a standard LCD display? Most LCD displays based on the HD44780 controller or compatible drivers support exactly eight custom character slots, indexed 0 through 7. Each slot holds a single 5x8 pixel glyph defined by eight bytes. You can redefine any slot at runtime, which is the key mechanism behind frame-by-frame animation.
Q: Why does printing character index 0 sometimes cause problems in CircuitPython?
The byte value \x00 is interpreted as a null or escape character in many programming contexts, including some CircuitPython string handling scenarios. Rather than printing the index directly, a reliable workaround is to call create_character() with index 0 on every animation frame, writing the new glyph definition fresh each time before printing. This sidesteps the escape character issue entirely.
Q: Can I run animations at multiple positions on the screen simultaneously? Yes, though it requires a bit of coordination. Since CG RAM only has eight slots, you can assign different slots to different animation positions and update them in a round-robin fashion within your main loop. Alternatively, if the animations are identical or share frames, you can reuse the same slot for multiple screen positions. The key is keeping your update loop fast enough that both positions appear to animate smoothly.
Q: Do I need a Metro RP2040 specifically, or will other CircuitPython boards work?
Any microcontroller running CircuitPython with I2C support will work, provided the adafruit_character_lcd library is compatible with your display hardware. Popular alternatives include the Adafruit Feather M4, the QT Py RP2040, and the ItsyBitsy M4. The Metro RP2040 is a great choice for beginners due to its familiar form factor and robust I2C performance, but the code itself is fully portable across CircuitPython-capable hardware.
Q: How do I design custom character frames without making mistakes in the binary data? The most reliable method is to sketch your character on a 5x8 grid on paper first, marking filled pixels as 1 and empty pixels as 0. Then read each row left to right to get your five-bit binary value and convert it to decimal or hex. Several online tools also let you draw an LCD custom character visually and export the byte array directly, which is a significant time-saver when designing complex multi-frame animations.
Frequently Asked Questions
Bringing Tiny Screens to Life with CircuitPython LCD Animation
There is something quietly thrilling about watching a tiny LCD screen flicker to life with a hand-crafted animation. No fancy graphics card. No GPU. Just a handful of ones and zeros arranged with intention, stored in a chip's memory, and cycled fast enough to trick your eye into seeing motion. I first experimented with LCD character displays expecting something dull and functional — a readout, a label, maybe a scrolling message. What I found instead was a surprisingly expressive little canvas, and CircuitPython makes the whole process feel almost playful. If you have ever wondered how to push LCD character display animation beyond static text, this is the deep dive you have been waiting for.
The technique at the heart of all of this is deceptively simple: you create custom characters, store them in the display's CG RAM, and then swap them in and out of a fixed screen position fast enough that persistence of vision does the rest. The result is a smooth, flicker-free animation running right alongside regular text — on a 16x2 display that costs a few dollars. Let's get into exactly how that works, why it matters, and how you can make it your own.
Understanding CG RAM and Custom Characters on LCD Displays
Most character LCD displays — the kind built around the HD44780 controller or compatible drivers — ship with a fixed set of characters baked into their ROM. You get the alphabet, numbers, punctuation, and a few symbols. What you do not get is flexibility. That is where CG RAM comes in.
CG RAM stands for Character Generator RAM, and it gives you eight slots to define completely custom 5x8 pixel characters. Each character is described by eight bytes, and only the lower five bits of each byte matter — that gives you a 5-pixel-wide grid, eight rows tall. Where you place a 1, a pixel lights up. Where you place a 0, it stays dark. It is pixel art in its most elemental form.
In CircuitPython, you work with these definitions as lists of binary values. Picture each row of your character as a five-digit binary number. Stack eight of those numbers and you have your glyph. The adafruit_character_lcd library handles the heavy lifting of pushing those definitions into the display's memory via I2C or SPI, leaving you free to focus on the creative side.
One important nuance worth knowing: the first memory slot — index zero — maps to the escape character \x00 in many environments, which can cause unexpected behaviour when you try to print it directly. A common workaround is to write the character definition fresh into slot zero on each animation frame rather than printing a stored index sequentially. It is a small quirk, but knowing about it saves you an infuriating debugging session.
Designing Animation Frames for an LCD Character Display
Here is where the craft really begins. Animation on an LCD character display works by defining multiple versions of the same character — each one a slightly different state — and then cycling through them in sequence at a position on screen. Because the display updates that single position quickly and your eye retains the previous image for a fraction of a second, you perceive smooth motion.
For a chase animation crawling around the border of a character cell, you might define eight frames. In frame one, a lit pixel sits at the top-left corner. In frame two, it has moved one step to the right along the top edge. By frame eight, it has completed a loop. Play those eight frames in order and you have a miniature marquee effect running inside a single character cell.
The design choice of using only a 5x5 area within the 5x8 grid is worth highlighting. Using the full height would create a taller rectangle, which looks stretched and odd at animation speed. Constraining the animation to a square sub-region gives a more satisfying visual result — and it frees up the bottom rows for secondary animation, like a single pixel bouncing back and forth in a Larson scanner style. Layering two behaviours into a single character cell is an elegant trick that rewards careful planning.
When designing your own frames, sketch them out on grid paper first. It sounds old-fashioned, but translating a visual idea into binary rows is much easier when you can see the shape in front of you before you start typing ones and zeros.
Writing the CircuitPython Code Step by Step
Let me walk you through the structure of a working implementation, so you can adapt it confidently rather than copy-pasting blindly.
Setting up the display object is your first task. Import the adafruit_character_lcd library, initialise your I2C bus, and create the LCD object with the correct column and row count — typically 16 columns and 2 rows for the most common shields. This is also where you tell the library about your hardware configuration.
Defining your frame data comes next. Create a list of frame definitions — each frame being a list of eight integers representing the pixel rows of your custom character. Keep these organised at the top of your script so they are easy to edit later.
Loading characters into memory happens inside your animation loop. On each frame, call create_character() with index 0 and the current frame's data. This writes the new glyph definition directly into CG RAM slot zero, overwriting the previous frame. Then set the cursor to your chosen position and write the character. Because you are only updating one character cell, there is no screen clear, no flicker, and no visible redraw artefact.
Adding regular text alongside your animation is perfectly fine. Write your static labels or readouts once before entering the animation loop. As long as your loop only touches the animated character's position, everything else on screen remains stable. This is what makes the technique so practical for real projects — you can show a status message, a sensor reading, or a project name while the animation runs in a corner doing its attention-grabbing work.
Controlling animation speed is a matter of inserting a small time.sleep() call between frames. Start around 0.1 seconds per frame and tune from there. Too fast and the animation blurs into an indistinct flicker. Too slow and the individual frames become obvious, breaking the illusion of motion.
Practical Uses Beyond Blinking Pixels
Once you have the mechanics down, the real question becomes: where does this actually add value in a project?
Progress indicators are the most obvious application. A loading bar built from custom characters feels far more polished than a static percentage number. You can define characters that show a cell filling from left to right — create six or eight variations and chain them across multiple character positions for a full-width progress bar with smooth sub-character resolution.
Activity spinners are another favourite. A single character cycling through four states — a dash, a forward slash, a pipe, and a backslash — creates a classic terminal-style spinner that tells users something is happening without taking up any meaningful screen space.
Status icons that pulse or blink with purpose, animated arrows pointing toward a button the user needs to press, countdown timers with a visual urgency component — all of these become possible once you think of custom characters not as static glyphs but as animation frames.
For makers building enclosures with small LCD readouts, these micro-animations add a layer of perceived quality that is completely disproportionate to the effort required. A project that shows a heartbeat pulse next to a heart rate reading feels finished in a way that plain numbers never quite do.
Hardware Considerations and Getting the Most from Your Setup
The Metro RP2040 paired with an LCD backpack over I2C is a particularly capable combination for this kind of work. The RP2040's dual cores and generous clock speed mean CircuitPython can handle the animation loop without breaking a sweat, and the I2C backpack reduces your wiring to just four connections — power, ground, SDA, and SCL.
If you are working with a different microcontroller, the same principles apply as long as your board runs CircuitPython and the adafruit_character_lcd library supports your display driver. The Adafruit CircuitPython library bundle covers most common LCD configurations, including those based on the MCP23008 I2C expander used on many LCD backpacks.
One hardware tip that pays off immediately: ensure your display's contrast is properly adjusted before you start worrying about animation quality. A poorly set contrast pot makes crisp pixel designs look muddy and undefined. A small screwdriver and thirty seconds of adjustment can make your custom characters look dramatically sharper.
Also consider the ambient light conditions where your project will be used. Some LCD displays have backlight brightness adjustable in software through the library. Setting a comfortable brightness not only improves legibility but also reduces the eye fatigue that comes from staring at a maximally bright screen in a dim environment.
Making It Your Own: Extending the Technique
The eight-slot limit of CG RAM is a genuine constraint, but creative workarounds exist. If your animation only needs to occupy one position on screen at a time, you can reuse all eight slots for a single multi-frame animation and cycle through them freely — you have eight distinct frames, which is plenty for smooth motion. If you need animations at multiple screen positions simultaneously, you can interleave updates: write frame N to slot 0, display it at position A, then write a different frame to slot 1 and display it at position B, alternating rapidly enough that both appear to animate independently.
You can also combine animation with user interaction. A button press could trigger a different animation sequence — a checkmark assembling itself pixel by pixel, or an X drawing itself in four frames. These micro-responses make interfaces feel alive and responsive in ways that pure text feedback never achieves.
Sharing your frame definitions with other makers is another avenue worth exploring. The community around CircuitPython is generous and active. A library of pre-designed animation sets — spinners, progress bars, weather icons, arrows — would be a genuinely useful contribution that others could drop straight into their projects.
Conclusion: Small Screen, Big Personality
LCD character display animation in CircuitPython is one of those techniques that seems modest until you see it running. Then it becomes one of those things you want to put in every project. The combination of CG RAM's eight custom character slots, the persistence of vision effect, and CircuitPython's readable, approachable syntax makes the whole thing accessible to anyone willing to spend an afternoon experimenting.
Start with a simple spinner. Get comfortable with the frame definition syntax and the create_character() workflow. Then start designing your own glyphs on grid paper and see what you can build. The constraint of working within a 5x5 or 5x8 pixel grid is not a limitation — it is a creative challenge that produces some of the most satisfying results in all of maker electronics. Your tiny screen has more personality than you think. It is time to let it show.
Frequently Asked Questions
Q: How many custom characters can I store in CG RAM on a standard LCD display? Most LCD displays based on the HD44780 controller or compatible drivers support exactly eight custom character slots, indexed 0 through 7. Each slot holds a single 5x8 pixel glyph defined by eight bytes. You can redefine any slot at runtime, which is the key mechanism behind frame-by-frame animation.
Q: Why does printing character index 0 sometimes cause problems in CircuitPython?
The byte value \x00 is interpreted as a null or escape character in many programming contexts, including some CircuitPython string handling scenarios. Rather than printing the index directly, a reliable workaround is to call create_character() with index 0 on every animation frame, writing the new glyph definition fresh each time before printing. This sidesteps the escape character issue entirely.
Q: Can I run animations at multiple positions on the screen simultaneously? Yes, though it requires a bit of coordination. Since CG RAM only has eight slots, you can assign different slots to different animation positions and update them in a round-robin fashion within your main loop. Alternatively, if the animations are identical or share frames, you can reuse the same slot for multiple screen positions. The key is keeping your update loop fast enough that both positions appear to animate smoothly.
Q: Do I need a Metro RP2040 specifically, or will other CircuitPython boards work?
Any microcontroller running CircuitPython with I2C support will work, provided the adafruit_character_lcd library is compatible with your display hardware. Popular alternatives include the Adafruit Feather M4, the QT Py RP2040, and the ItsyBitsy M4. The Metro RP2040 is a great choice for beginners due to its familiar form factor and robust I2C performance, but the code itself is fully portable across CircuitPython-capable hardware.
Q: How do I design custom character frames without making mistakes in the binary data? The most reliable method is to sketch your character on a 5x8 grid on paper first, marking filled pixels as 1 and empty pixels as 0. Then read each row left to right to get your five-bit binary value and convert it to decimal or hex. Several online tools also let you draw an LCD custom character visually and export the byte array directly, which is a significant time-saver when designing complex multi-frame animations.
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
Build an AI Embodiment Kit with CircuitPython Agents
Travel & Places · CircuitPython · AI Agents
Custom Glyphs on LCD Displays with CircuitPython
Travel & Places · CircuitPython · LCD Display
Things to Do in Fort Lauderdale: Your Ultimate 2025 Travel Guide
Travel & Places
Explore More Categories
Keep browsing by topic and build depth around the subjects you care about most.


