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.

Open Font Tool →