Many CircuitPython projects use the built-in terminalio.FONT font provided by CircuitPython itself.
There is a period in the table where a non-printable character is located. Basically, terminalio.FONT contains a limited character set of what might be found on a US keyboard.
There are several font types that can be used with CircuitPython. Some have better support than others, so keep this in mind when choosing.
BDF
The Glyph Bitmap Distribution Format (BDF) by Adobe is a file format for storing bitmap fonts. The content takes the form of a text file intended to be human- and computer-readable. It has largely been replaced by the PCF font format which is somewhat more efficient.
BDF fonts in CircuitPython tend to load slower than PCF files and they render text slower as the character representations must be converted at runtime to a bitmap.
PCF
Portable Compiled Format (PCF) is a binary bitmap font format. They are generally similar to BDF fonts as far as capability, but the file format being binary they tend to be smaller and load quicker than BDF font files.
Check out the guide below for information on BDF and PCF fonts:
LVGL
Support for LVGL fonts is "the latest" added to CircuitPython. They should be in binary format with a .bin file extension.
The OnDiskFont function should be used when opening these files, documented here.
You can convert TTF files to LVGL using the web utility here.
See an issue with use of fonts in the CircuitPython Terminal on GitHub.
The following code will take a PCF font and display all the character glyphs on a DVI display using a RP2350 microcontroller with no extra PSRAM. The RP2040 does not have enough memory. Printing a BDF with an RP2350 without PSRAM will likely run out of RAM before all 256 characters are printed.
If a character doesn't have a graphical glyph, it is represented by a period (.)
# SPDX-FileCopyrightText: 2025 Anne Barela for Adafruit Industries
#
# SPDX-License-Identifier: MIT
#
# Take a BDF or PCF font file and display it on a CircuitPython
# HDMI/DVI display in 320x200 resolution. "." is used for unknown characters
import gc
import displayio
import supervisor
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
from adafruit_fruitjam.peripherals import request_display_config
# Use the easy library call to set the resolution
request_display_config(320, 240)
# Initialize display
display = supervisor.runtime.display
main_group = displayio.Group()
display.root_group = main_group
# Use Labels to display characters that exist in font
font_file = "fonts/cp437-8x12a.pcf"
font = bitmap_font.load_font(font_file) # Use font = terminalio.FONT for built-in
char_width = 8 # font character width
char_height = 12 # font character height
chars_per_line = 32 # Fixed at 32 characters per line
line_number_width = 35 # Space for line numbers like "000: "
displayed_count = 0
skipped_count = 0
current_x = line_number_width # Start after line number space
current_y = char_height
current_line_start = 0
# Add first line number
line_label = label.Label(
font,
text=f"{current_line_start:03d}: ",
color=0xFFFFFF,
x=0,
y=current_y
)
main_group.append(line_label)
# Try all characters from 0-255 and display ones that exist
for char_code in range(256):
try:
# Check if we need to wrap to next line
if (char_code > 0) and (char_code % chars_per_line == 0):
current_x = line_number_width # Reset to after line number
current_y += char_height + 2 # Add some line spacing
current_line_start = char_code
# Stop if we run out of vertical space
if current_y + char_height > display.height:
print(f"Display full, stopped at {char_code}")
break
# Add line number for this new line
line_label = label.Label(
font,
text=f"{current_line_start:03d}: ",
color=0xFFFFFF,
x=0,
y=current_y
)
main_group.append(line_label)
# Check if glyph exists
glyph = font.get_glyph(char_code)
if glyph is None:
# No glyph available - display a period instead
display_char = "."
skipped_count += 1
else:
# Glyph exists - display the actual character
display_char = chr(char_code)
# Create label for this character (or replacement)
char_label = label.Label(
font,
text=display_char,
color=0xFFFFFF,
x=current_x,
y=current_y
)
main_group.append(char_label)
current_x += char_width
displayed_count += 1
except (MemoryError, ValueError) as e:
print(f"Memory limit reached at char {char_code}")
break
# Garbage collection every 16 characters
if char_code % 16 == 0:
gc.collect()
print(f"Found {displayed_count - skipped_count} chars with glyphs")
print(f"{skipped_count} missing chars -> periods")
print(f"Free memory: {gc.mem_free()} bytes")
# Keep display active
while True:
pass
Here is a custom font for IBM PC emulation, commonly called CP437 at 8x12 character size, as an example. This works on an RP2350 board without PSRAM with a PCF font. Memory will run out on a board without PSRAM if you use a BDF font or an RP2040.
Change the font name and the desired screen resolution in the code for your situation.
Note that a full 256 character font will not print out fully on an RP2350-based board without PSRAM.
Note that printing fonts with more than a few characters on RP2040 will likely fail when the amount of RAM is used up. Consider using an RP2350 board, preferably with a PSRAM chip, to fully display fonts.
# SPDX-FileCopyrightText: 2025 Anne Barela for Adafruit Industries
#
# SPDX-License-Identifier: MIT
#
# Display an LVGL (.bin) font on a CircuitPython board and
# 640x480 DVI display. Unknown characters will be "."
#
# pylint: disable=broad-except, bare-except
import gc
import displayio
import supervisor
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label
from adafruit_fruitjam.peripherals import request_display_config
# Use the easy library call to set the resolution
request_display_config(640, 480)
# Initialize display
display = supervisor.runtime.display
main_group = displayio.Group()
display.root_group = main_group
print(f"Initial memory: {gc.mem_free()}")
# Font loading with error handling and diagnostics
font_file = "fonts/CP437_16h.bin"
try:
font = bitmap_font.load_font(font_file) # pylint: disable=redefined-outer-name
print(f"Font loaded: {font_file}")
print(f"Memory after font load: {gc.mem_free()}")
# Diagnostic: Check font properties
try:
bbox = font.get_bounding_box()
print(f"Font bounding box: {bbox}")
# Test a few common characters
test_chars = [32, 65, 97] # space, 'A', 'a'
for char_code in test_chars:
glyph = font.get_glyph(char_code)
if glyph:
print(f"Char {char_code} ('{chr(char_code)}'): OK")
else:
print(f"Char {char_code} ('{chr(char_code)}'): Missing")
except Exception as e:
print(f"Error checking font properties: {e}")
except Exception as e:
print(f"Error loading font {font_file}: {e}")
# Fallback to terminalio font
import terminalio
font = terminalio.FONT
print("Using fallback terminalio.FONT")
# Get actual font dimensions
try:
font_bbox = font.get_bounding_box()
char_width = font_bbox[0]
char_height = font_bbox[1]
print(f"Actual font size: {char_width}x{char_height}")
except:
char_width = 9 # fallback
char_height = 16 # fallback
print(f"Using fallback font size: {char_width}x{char_height}")
chars_per_line = 32 # Fixed at 32 characters per line
line_number_width = 5 * char_width # Space for "000: " (5 characters)
displayed_count = 0
skipped_count = 0
current_x = line_number_width # Start after line number space
current_y = char_height
current_line_start = 0
def create_char_label(thefont, ch, x, y):
"""Helper function to create character labels with error handling"""
try:
return label.Label(thefont, text=ch, color=0xFFFFFF, x=x, y=y)
except Exception as e:
print(f"Error creating label for '{ch}': {e}")
return None
# Add first line number
text = f"{current_line_start:03d}: "
print(f"Creating line number: '{text}'")
for i, char in enumerate(text):
char_label = create_char_label(font, char, i * char_width, current_y)
if char_label:
main_group.append(char_label)
print(f"Memory after first line number: {gc.mem_free()}")
# Try all characters from 0-255 and display ones that exist
for char_code in range(256):
try:
# Check if we need to wrap to next line
if (char_code > 0) and (char_code % chars_per_line == 0):
current_x = line_number_width # Reset to after line number
current_y += char_height + 4 # Add some line spacing
current_line_start = char_code
# Stop if we run out of vertical space
if current_y + char_height > display.height:
print(f"Display full, stopped at character {char_code}")
break
# Add line number for this new line
text = f"{current_line_start:03d}: "
for i, char in enumerate(text):
char_label = create_char_label(font, char, i * char_width, current_y)
if char_label:
main_group.append(char_label)
# Check if glyph exists
glyph = font.get_glyph(char_code)
if glyph is None:
# No glyph available - display a period instead
display_char = "."
skipped_count += 1
else:
# Glyph exists - display the actual character
display_char = chr(char_code)
# Create label for this character (or replacement)
char_label = create_char_label(font, display_char, current_x, current_y)
if char_label:
main_group.append(char_label)
current_x += char_width
displayed_count += 1
except (MemoryError, ValueError) as e:
print(f"Memory/Value error at character {char_code}: {e}")
break
except Exception as e:
print(f"Unexpected error at character {char_code}: {e}")
# Continue with next character
current_x += char_width
displayed_count += 1
# More frequent garbage collection
if char_code % 8 == 0:
gc.collect()
# Progress indicator for debugging
if char_code % 32 == 0:
msg = f"Processed up to character {char_code}, memory: {gc.mem_free()}" # pylint: disable=f-string-without-interpolation
print(msg)
print("\nCompleted character display:")
print(f"Found {displayed_count - skipped_count} characters with glyphs")
print(f"{skipped_count} missing characters displayed as periods")
print(f"Total labels created: {len(main_group)}")
print(f"Final free memory: {gc.mem_free()} bytes")
# Keep display active
while True:
pass
Converting Fonts
Adafruit has a web utility for converting text-based BDF fonts to PCF. Go to https://adafruit.github.io/web-bdftopcf/, enter in the BDF font file and it'll prompt where to save the PCF file.
There is an online TTF to LVGL font converter at https://lvgl.io/tools/fontconverter.
There are other font conversion programs online as well as some possible web-based utilities.
Fonts are loaded on CircuitPython with adafruit_bitmap_font.bitmap_font(). This applies to BDF, PCF, and LVGL (.bin). CircuitPython looks inside the file and at the file extension, both, to identify a valid font file.
The OnDiskFont() function can also be used for LVGL fonts.
Obtaining Fonts
Besides many fonts being available on the Internet, a number of fonts are available for CircuitPython in the Adafruit GitHub repository. They fall under a number of permissible use licenses, so check to be sure the license applies to your project.
Some fonts on other online sites have restrictive use licenses or are available at a cost. Be sure you check on the status of fonts you obtain before use.
The IBM CP437 fonts used in this guide in BDF, PCF, and LVGL format are from open sources and can be downloaded in GitHub via the green button below:
For more information, please see this companion guide.
Page last edited July 16, 2025
Text editor powered by tinymce.