Using a custom font on SSD1306 with embedded-font.com
This guide walks through generating a bitmap font header for an SSD1306 OLED display and wiring it up to your driver's pixel-set function. The default row-major byte layout and the reference renderer that /font can embed in the header work on any display — SSD1306 included — so no special configuration is required.
1. Pick a font and size
Open /font. In Font Family, pick a family and weight from the OFL library. In Dimensions, choose a height that fits your display — for a 128×32 panel, two rows of a 16 px font fit comfortably; for 128×64, a 16 px or 24 px font works well with room to spare.
2. Select only the characters you need
Open the CHARACTERS panel. Pick a preset (Digits 0-9,
Digits + .,:-+%, Uppercase A-Z, Full ASCII) or type the
exact characters your firmware needs into the Custom characters field. The panel
header shows a live Table: N bytes estimate — keep an eye on it if you're
tight on flash.
3. (Optional) Fix glyphs in the Glyph Editor
Open GLYPH EDITOR and click Accept & Apply to rasterize the font, then check each character at your chosen size. Small fonts often need a pixel or two nudged with Draw/Erase/Shift to stay legible — changes here are baked into the downloaded header.
4. Preview on the Display Emulator
Open DISPLAY EMULATOR and select SSD1306 OLED 128×32 or
SSD1306 OLED 128×64 from the preset list. Type a test string and confirm it
looks right — any character you type that isn't in your character set is drawn as a hollow
box, so this is also a quick way to catch a character set that's too small.
5. Choose a Header Option and download
Open HEADER OPTIONS. For most SSD1306 setups (ESP32, RP2040, STM32 with
arm-none-eabi-gcc, or a desktop simulator) the Generic C99 envelope is fine.
If you're targeting an Arduino board, choose AVR / Arduino instead — it
places the font table in flash via PROGMEM and reads it with
pgm_read_byte. Click Download Font Header .h.
6. Wire up put_pixel and call the reference renderer
The downloaded header already contains {name}_draw_char and
{name}_draw_str functions (live code if "Include reference renderer as live code"
is checked in Header Options, otherwise a commented-out copy you can paste in). They call a
single function you provide:
// ---------------------------------------------------------------------------
// Reference renderer
// ---------------------------------------------------------------------------
extern void put_pixel(int x, int y, bool on);
// Reference renderer for font_jetbrains_mono_bold_16x12.
// Requires a user-provided: void put_pixel(int x, int y, bool on);
//
// Handles all three FONT_JETBRAINS_MONO_BOLD_16X12_LAYOUT_* byte layouts at compile time:
// row-major (MSB/LSB first) walks the glyph row by row, BYTES_PER_ROW bytes
// per row; column-major pages (SSD1306-style) walks it page by page, WIDTH
// bytes per page, bit 0 of each byte its topmost pixel in that page.
static void font_jetbrains_mono_bold_16x12_draw_char(int x, int y, char c)
{
if ((unsigned char)c < FONT_JETBRAINS_MONO_BOLD_16X12_FIRST_CHAR)
return;
int index = (c - FONT_JETBRAINS_MONO_BOLD_16X12_FIRST_CHAR) * FONT_JETBRAINS_MONO_BOLD_16X12_BYTES_PER_GLYPH;
#if defined(FONT_JETBRAINS_MONO_BOLD_16X12_LAYOUT_COLUMN_PAGES)
for (int page = 0; page < FONT_JETBRAINS_MONO_BOLD_16X12_PAGE_COUNT; page++)
{
for (int col = 0; col < FONT_JETBRAINS_MONO_BOLD_16X12_WIDTH; col++)
{
uint8_t bits = FONT_READ_BYTE(&font_jetbrains_mono_bold_16x12[index]);
index++;
for (int bit = 0; bit < 8; bit++)
{
int py = page * 8 + bit;
if (py >= FONT_JETBRAINS_MONO_BOLD_16X12_HEIGHT)
break;
put_pixel(x + col, y + py, (bits & (1 << bit)) != 0);
}
}
}
#else
for (int row = 0; row < FONT_JETBRAINS_MONO_BOLD_16X12_HEIGHT; row++)
{
for (int byteCol = 0; byteCol < FONT_JETBRAINS_MONO_BOLD_16X12_BYTES_PER_ROW; byteCol++)
{
uint8_t bits = FONT_READ_BYTE(&font_jetbrains_mono_bold_16x12[index]);
index++;
for (int bit = 0; bit < 8; bit++)
{
int px = x + byteCol * 8 + bit;
if (px >= x + FONT_JETBRAINS_MONO_BOLD_16X12_WIDTH)
break;
#if defined(FONT_JETBRAINS_MONO_BOLD_16X12_LAYOUT_ROW_MAJOR_LSB)
put_pixel(px, y + row, (bits & (1 << bit)) != 0);
#else
put_pixel(px, y + row, (bits & (1 << (7 - bit))) != 0);
#endif
}
}
}
#endif
}
static void font_jetbrains_mono_bold_16x12_draw_str(int x, int y, const char *s)
{
while (*s)
{
font_jetbrains_mono_bold_16x12_draw_char(x, y, *s);
x += FONT_JETBRAINS_MONO_BOLD_16X12_WIDTH;
s++;
}
}
put_pixel is the only function you need to implement — forward it to your
SSD1306 driver's pixel-set call (e.g. ssd1306_draw_pixel(x, y, on)).
7. Full minimal example
A complete, self-contained C99 program that includes the generated header, implements
put_pixel on top of a placeholder ssd1306_draw_pixel, and draws
"Hello" at position (0, 0):
#include <stdint.h>
#include <stdbool.h>
#include "font_jetbrains_mono_bold_16x12.h"
// Replace this with your driver's pixel-set call, e.g.
// ssd1306_draw_pixel(x, y, on) from your I2C/SPI driver.
static void ssd1306_draw_pixel(int x, int y, bool on)
{
(void)x;
(void)y;
(void)on;
}
// Required by the reference renderer in the generated header.
void put_pixel(int x, int y, bool on)
{
ssd1306_draw_pixel(x, y, on);
}
int main(void)
{
font_jetbrains_mono_bold_16x12_draw_str(0, 0, "Hello");
return 0;
}
This example compiles cleanly with cc -std=c99 -Wall -Werror against a header
generated for JetBrains Mono Bold at 16×12 px with Full ASCII and the live reference
renderer enabled.
Column-major byte layout (used by some SSD1306 drivers that write whole 8-pixel-tall
"pages" at a time) is available as an option in Header Options, but it's an
optimisation, not a requirement — the row-major default plus the reference renderer above
works on any display via put_pixel. See the
header format reference for details on byte layouts.