Skip to content

Lopaka Basics 02: Text, Fonts & Icons ​

An SSD1306 OLED module showing a morning dashboard with Wi-Fi and battery icons, the date FRI 24 OCT, the time 07:45 and a white status bar with a sun icon and Sunny 23C

A clock, the weather and a picture of the moon, all on a screen the size of a postage stamp. Text, fonts, icons and images make a small screen useful, and on a microcontroller every one of them costs memory.

In this episode you will find out what a font is on a microcontroller, where it is stored and how much space it takes. Then you will build a small morning dashboard in Lopaka with two screens, a status bar with inverted text and your own picture turned into dots, and run it on a simulated Arduino UNO.

What you will build: a 128×64 OLED dashboard with two screens. Morning shows the time, the date, Wi-Fi and battery icons and a weather bar. Night shows a picture of the moon and the time. What you need: a browser and the simulator project from episode 1. A real board is optional.

A font is a set of pictures ​

One picture per character ​

A microcontroller does not know what letters are. To show "07:45", the graphics library needs a small picture for each character: one for 0, one for 7, one for : and so on. A font on a microcontroller is a set of tiny pictures, one for every character it can draw.

Bigger text, bigger pictures ​

Each picture is a grid of pixels. A bigger font means bigger grids: the same digit 2 is 5×9 pixels in a small font and 12×18 in a large one. Every one of those pictures has to be stored on the chip, so a bigger font takes more memory.

One letter is one tiny picture: the digit 7 on a pixel grid, the five pictures that make up 07:45, and the digit 2 in three sizes, 5×9, 6×11 and 12×18

Two kinds of memory ​

Flash is the bookshelf, RAM is the desk ​

A microcontroller has two kinds of memory:

  • Flash keeps your program, fonts and images. It works like a bookshelf: everything the program might need is stored there, and it stays there when the power is off.
  • RAM is the small desk where the work happens: the values the program is working on right now.

On an Arduino UNO the bookshelf holds 32 KB and the desk only 2 KB.

How much a font costs ​

A small font takes about one kilobyte of flash, a large one two to four. We measured it by compiling a sketch for an UNO with one text in a single font and comparing it with a sketch without text: 4x6 added 1.1 KB, profont22 2.1 KB, profont29 2.5 KB and timR24 3.7 KB.

Two kinds of memory on an Arduino UNO: 32 KB of flash, the bookshelf for program, fonts and images, with fonts of 1.1, 2.1, 2.5 and 3.7 KB next to the program, and 2 KB of RAM, the desk

Each font is stored once, however many texts use it. The practical rule: two or three fonts per screen. To see which fonts your design uses, look for the setFont lines in Lopaka's code panel: every different font name there is another font on the bookshelf.

Step 1: a new project ​

Same start as in episode 1: click + New Project, pick the U8g2 platform, keep the screen size at 128×64 and give the project a title, "Morning Dashboard". Click Save.

Step 2: text ​

The time in a big font ​

Pick the text tool (Aa), click on the canvas and select the new string 1 layer. In the inspector, replace the text with 07:45, open the Font list and pick profont29, a font that is easy to read from across the room.

Click the centre horizontally align button at the top of the inspector and set Y to 40. The code now reads:

cpp
u8g2.setFont(u8g2_font_profont29_tr);
u8g2.drawStr(24, 40, "07:45");

The font list open in the inspector with profont29 selected for the text 07:45, which sits centred on the canvas at X 24 and Y 40

Both numbers can be checked by hand. Every character of profont29 is 16 pixels wide, so "07:45" is 5 × 16 = 80 pixels wide, and (128 − 80) / 2 = 24 puts it in the middle. The 40 is the baseline, the line the digits stand on, as in episode 1: the digits fill the rows above it.

The date in a small font ​

Add another text, FRI 24 OCT. A new text starts in the last font you used, so switch it to the small 5x8. Centre it horizontally and set Y to 11. Ten characters of 5 pixels make 50 pixels, which gives drawStr(39, 11, "FRI 24 OCT").

Your own font ​

Want a different look? The first item of the Font list, Import font..., takes a TrueType file (.ttf; .otf, .woff and .bdf work too), and Lopaka turns it into pixels at the height you choose. In the video we imported Press Start 2P, a free pixel font, and set Font Height to 8. The preview shows the whole font: 95 tiny pictures, every printable character from the space to ~.

The Font Preview dialog for Press Start 2P with Font Height 8 and all 95 characters in three rows

Click Import, and the font is ready to use. The selected text switches to it, and the code gets one extra line, #include "Press_Start_2P_8.h". That header holds the font data. A button with the same name appears under the code: download the file and put it next to your sketch, or the sketch will not compile.

The dashboard keeps the built-in fonts. If you tried the import on the time, switch it back to profont29.

Step 3: icons ​

Search and add ​

Lopaka has more than two thousand icons built in. Open the Images tab on the left, type into the search field and click an icon to add it to the screen. Add three:

  • wifi_25 (search "wifi"),
  • battery_30 (search "battery"),
  • weather_sun (search "sun").

All three are in the Gaai set.

Put each icon in its place ​

Drag the icons into place on the canvas: Wi-Fi in the top-left corner, the battery in the top-right corner, the sun in the bottom-left corner.

An icon is 32 bytes ​

An icon is a tiny picture too. The sun is 15 pixels wide and 16 tall, and on a one-colour screen each pixel is one bit: on or off. Eight pixels fit in one byte. A row of 15 pixels needs two bytes, and the last bit stays unused, so the whole icon is 2 bytes × 16 rows = 32 bytes.

The 15 by 16 pixel sun icon with one row highlighted: its pixels as the bits 00010000 00010000, the bytes 0x08 0x08, 8 pixels per byte, 2 bytes per row times 16 rows is 32 bytes, and the image_weather_sun_bits array

In the code, the icon becomes this list of numbers. The library reads them and draws the pixels:

cpp
static const unsigned char image_weather_sun_bits[] U8X8_PROGMEM = {0x80,0x00,0x84,0x10,0x08,0x08,0xc0,0x01,0x31,0x46,0x12,0x24,0x08,0x08,0x08,0x08,0x08,0x08,0x12,0x24,0x31,0x46,0xc0,0x01,0x08,0x08,0x84,0x10,0x80,0x00,0x00,0x00};
cpp
u8g2.drawXBMP(5, 47, 15, 16, image_weather_sun_bits);

drawXBMP takes the position, the width and height of the picture, and the array. In this format, called XBM, the first pixel of a row is the lowest bit of the byte. That is why the highlighted row reads 00010000 from left to right but is stored as 0x08.

The other two icons are 19×16 and 24×16 pixels. Both need three bytes per row, 48 bytes each.

Step 4: inverted text ​

A white bar with dark text ​

For a status bar, draw a filled rectangle and put text on top:

  1. Pick the rectangle tool, drag a rectangle on the canvas and set X 0, Y 46, W 128, H 18. Turn on Filled. The bar now covers the bottom 18 rows of the screen, rows 46 to 63.
  2. The new rectangle is the top layer and hides the sun. Press Ctrl + ↓ (Cmd + ↓ on a Mac) to move it one layer down, under weather_sun.
  3. Select weather_sun and turn on Inverted (XOR).
  4. Add a text Sunny 23C (with two spaces), pick the 6x10 font, set X 28 and Y 59, and turn on Inverted (XOR).

The sun and the text cut through the white bar in black.

The Morning screen with a white bar along the bottom; the sun and the text Sunny 23C are drawn in black on it, and the inspector shows string 7 with the 6x10 font, X 28, Y 59 and Inverted (XOR) on

XOR: where shapes overlap, the pixels flip ​

Inverted uses a trick called XOR. Wherever a new shape overlaps something already drawn, the pixels flip: a lit pixel under the shape goes dark, a dark one lights up. On a one-colour screen, that is how you get dark text on a light bar.

XOR, overlap flips: dark 07:45 on a white bar; on plus off gives on, on plus on gives off

In the code this is one line before the sun:

cpp
// rect 6
u8g2.drawBox(0, 46, 128, 18);
// weather_sun
u8g2.setDrawColor(2);
u8g2.drawXBMP(5, 47, 15, 16, image_weather_sun_bits);
// string 7
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(28, 59, "Sunny  23C");

setDrawColor(2) switches U8g2 to XOR mode, and the mode stays on until the code changes it again. That is why the text after the sun needs no second call. The order of the layers matters here: the bar has to be drawn first, so that the sun and the text have something to flip.

Step 5: your own image ​

Upload a picture ​

You can bring your own pictures, too. In the Images tab, click Upload and choose an image file. Lopaka opens an import dialog that converts it for the display. We used a photo-like picture of the moon.

Set Dimensions to W 48 and H 48, and turn on Invert palette: the moon is light on a dark background, and this makes the moon the lit pixels on the OLED.

Dithering turns grey into dots ​

This screen has no grey. Every pixel is black or white. The simplest conversion cuts at half brightness: brighter pixels go on, darker ones go off. On the moon that keeps only a white blob, and all the shading is lost.

Dithering fixes that. It mixes black and white dots, denser where the picture is brighter, so from a distance your eye sees shades of grey.

The same moon three ways: the original in shades of grey, cut at 50% to a flat white blob, and dithered into dots that fake the grey, with a close-up of the dot pattern

Pick a style in the Dithering list: Floyd-Steinberg, Atkinson, Bayer 8x8 or No dithering. We kept Floyd-Steinberg. Click Import.

The image import dialog with the moon at 48 by 48 pixels, Floyd-Steinberg dithering and Invert palette on; the preview shows the moon as a pattern of dots

At 48 by 48 pixels the picture takes 48 / 8 × 48 = 288 bytes.

A second screen for the night ​

Let's give the moon a screen of its own. A project can hold as many screens as you need.

  1. Double-click the screen's name above the canvas and rename Screen 1 to Morning.
  2. Click Add Screen in the Layers panel and rename the new screen to Night.

Put the moon on the Night screen at X 6, Y 8, and add two texts:

  • Good night in 6x10 at X 62, Y 26,
  • 22:30 in profont22 at X 62, Y 50.

Both screens side by side on the Lopaka canvas: Morning with the time 07:45 and the weather bar, Night with the dithered moon, Good night and 22:30; the code panel has a tab for each screen

Each screen becomes its own function in the code: drawMorning() and drawNight(), named after the screens. The code panel has a tab for each screen. Your sketch decides which one to show, and when.

Step 6: PROGMEM ​

One setting worth knowing: Declare as PROGMEM, in the list of switches next to the code. It is on by default.

The Night code tab: image_moon_bits is declared with U8X8_PROGMEM and drawn with drawXBMP; the switches next to the code start with Declare as PROGMEM, turned on

On an AVR board such as the UNO, a constant array is copied to RAM when the program starts, unless it is marked for flash. U8X8_PROGMEM keeps the image data in flash, on the bookshelf, so it does not crowd the small RAM desk, and drawXBMP (with the P) reads it from there. With the switch off, Lopaka writes drawXBM and the arrays without U8X8_PROGMEM.

The difference for this dashboard: the four images take 416 bytes. Compiled for an UNO, the sketch uses 78% of the RAM with PROGMEM and 98% without it. Without it only 32 bytes are left for everything the program does while it runs, and the sketch may crash or behave strangely.

RAM used on an Arduino UNO: 78% with PROGMEM, 98% without PROGMEM, only 32 bytes left

Step 7: run it ​

The code ​

Click Copy on the Morning tab, paste it into your sketch, then do the same on the Night tab. Copy takes the code of the open tab. This is the code Lopaka generates for the two screens:

cpp
// [BEGIN lopaka generated]
static const unsigned char image_battery_30_bits[] U8X8_PROGMEM = {0x00,0x00,0x00,0xf0,0xff,0x7f,0x08,0x00,0x80,0x08,0x00,0xaa,0x0e,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x01,0x00,0xaa,0x0e,0x00,0xaa,0x08,0x00,0xaa,0x08,0x00,0x80,0xf0,0xff,0x7f,0x00,0x00,0x00,0x00,0x00,0x00};
static const unsigned char image_weather_sun_bits[] U8X8_PROGMEM = {0x80,0x00,0x84,0x10,0x08,0x08,0xc0,0x01,0x31,0x46,0x12,0x24,0x08,0x08,0x08,0x08,0x08,0x08,0x12,0x24,0x31,0x46,0xc0,0x01,0x08,0x08,0x84,0x10,0x80,0x00,0x00,0x00};
static const unsigned char image_wifi_25_bits[] U8X8_PROGMEM = {0x80,0x0f,0x00,0x60,0x30,0x00,0x18,0xc0,0x00,0x84,0x0f,0x01,0x62,0x30,0x02,0x11,0x40,0x04,0x0a,0x87,0x02,0xc4,0x18,0x01,0x28,0xa0,0x00,0x10,0x47,0x00,0xa0,0x2f,0x00,0xc0,0x1d,0x00,0x80,0x0a,0x00,0x00,0x07,0x00,0x00,0x02,0x00,0x00,0x00,0x00};
void drawMorning(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // string 1
    u8g2.setFont(u8g2_font_profont29_tr);
    u8g2.drawStr(24, 40, "07:45");
    // string 2
    u8g2.setFont(u8g2_font_5x8_tr);
    u8g2.drawStr(39, 11, "FRI 24 OCT");
    // wifi_25
    u8g2.drawXBMP(2, 0, 19, 16, image_wifi_25_bits);
    // battery_30
    u8g2.drawXBMP(102, 0, 24, 16, image_battery_30_bits);
    // rect 6
    u8g2.drawBox(0, 46, 128, 18);
    // weather_sun
    u8g2.setDrawColor(2);
    u8g2.drawXBMP(5, 47, 15, 16, image_weather_sun_bits);
    // string 7
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(28, 59, "Sunny  23C");
}
// [END lopaka generated]

// [BEGIN lopaka generated]
static const unsigned char image_moon_bits[] U8X8_PROGMEM = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xd0,0x0a,0x00,0x00,0x00,0x00,0x7f,0x57,0x00,0x00,0x00,0xc0,0xee,0xad,0x00,0x00,0x00,0xf0,0xbf,0xbb,0x05,0x00,0x00,0xd8,0xfb,0x56,0x09,0x00,0x00,0xfc,0xbf,0xbf,0x12,0x00,0x00,0xfe,0xff,0xd5,0x06,0x00,0x00,0xdf,0x6f,0xbf,0x2a,0x00,0x80,0x7f,0xbb,0xd5,0x42,0x00,0xc0,0xdf,0x7f,0xbf,0x2a,0x00,0xc0,0xff,0xfe,0xd6,0x8a,0x00,0xe0,0xff,0xdf,0xbb,0x2a,0x00,0x60,0xff,0xff,0x6e,0x95,0x00,0xf0,0xff,0xbf,0xb7,0x4a,0x00,0xd0,0xff,0xef,0xdd,0x12,0x02,0xf0,0xff,0xff,0xb7,0x56,0x00,0xf0,0xff,0xbd,0x6e,0x09,0x01,0xf8,0xbf,0xef,0x5b,0x53,0x00,0xe8,0xfd,0xff,0xd6,0x14,0x10,0x78,0xff,0xdd,0xad,0x8a,0x00,0xe8,0x6f,0x77,0x5b,0x11,0x00,0xf8,0xff,0xdf,0x56,0x45,0x04,0xd0,0x5e,0x7d,0xab,0x10,0x00,0xf8,0xfb,0xd7,0x56,0x05,0x00,0x50,0xef,0xbe,0x55,0x41,0x00,0xf0,0xbd,0x6b,0x55,0x0a,0x04,0x50,0xf7,0xd6,0xaa,0x00,0x00,0xd0,0xad,0xbd,0x0a,0x25,0x00,0xa0,0x76,0xab,0x52,0x00,0x02,0x60,0xdb,0x56,0x25,0x01,0x00,0x40,0xad,0x55,0x49,0x08,0x00,0x80,0x6a,0x55,0x15,0x82,0x00,0x80,0x5a,0x55,0x82,0x00,0x00,0x00,0xa5,0x4a,0x29,0x00,0x00,0x00,0x2a,0x29,0x00,0x00,0x00,0x00,0x48,0x92,0x02,0x20,0x00,0x00,0xa4,0x04,0x08,0x04,0x00,0x00,0x00,0x51,0x80,0x00,0x00,0x00,0x20,0x04,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x00,0x20,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
void drawNight(void) {
    u8g2.setFontMode(1);
    u8g2.setBitmapMode(1);
    // moon
    u8g2.drawXBMP(6, 8, 48, 48, image_moon_bits);
    // string 9
    u8g2.setFont(u8g2_font_6x10_tr);
    u8g2.drawStr(62, 26, "Good night");
    // string 10
    u8g2.setFont(u8g2_font_profont22_tr);
    u8g2.drawStr(62, 50, "22:30");
}
// [END lopaka generated]

6x10 appears in both functions, but it is the same font and is stored in flash once. The whole design uses four fonts: profont29, 5x8, 6x10 and profont22.

The sketch ​

Start from last episode's sketch and change loop() so it shows each screen for three seconds:

cpp
#include <U8g2lib.h>
#include <Wire.h>

// 128x64 OLED display on I2C
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

// Paste the Lopaka code here

void setup() {
  u8g2.begin();
}

void loop() {
  u8g2.clearBuffer();
  // show each screen for 3 seconds
  if ((millis() / 3000) % 2 == 0) drawMorning();
  else drawNight();
  u8g2.sendBuffer();
}

millis() counts the milliseconds since the board started. Divided by 3000, it grows by one every three seconds, and % 2 turns that into 0, 1, 0, 1. On 0 the sketch draws Morning, on 1 Night. As in episode 1, both functions draw between clearBuffer() and sendBuffer().

Run it in the simulator ​

Open the simulator project from episode 1: an Arduino UNO, an SSD1306 OLED wired to GND, 5V, A5 (SCL) and A4 (SDA), and the U8g2 library. Replace the sketch with the one above, paste the Lopaka code in its place and press Play. The display switches between the two screens every three seconds.

The simulator running the sketch: the simulated OLED connected to an Arduino UNO shows the Morning screen with 07:45 and Sunny 23C

We checked both screens pixel by pixel: the Lopaka canvas, the screen drawn by the U8g2 library and the simulated display are identical.

How much memory it all takes ​

The whole thing, with four fonts, three icons and a picture, fits in less than half of the UNO's flash. The compiler reports:

Sketch uses 15394 bytes (47%) of program storage space. Maximum is 32256 bytes.
Global variables use 1600 bytes (78%) of dynamic memory, leaving 448 bytes for local variables. Maximum is 2048 bytes.

Most of the 1,600 bytes of RAM is the 1,024-byte frame buffer from episode 1. For this sketch that is fine.

Recap ​

  1. Fonts and icons are pictures: every character of a font and every icon is a small grid of pixels, one bit per pixel.
  2. Pictures cost memory: a font takes one to four kilobytes of flash, so use two or three per screen, and keep images in flash with PROGMEM.
  3. Dithering turns grey into dots: pick a dithering style when you import a photo, and the one-colour screen shows shades.

Next episode: Going Colour. The same kind of interface on a colour TFT display, and what RGB565 means. Read episode 3 or watch it on YouTube.

Ready to try? Open Lopaka and build your own dashboard.