Graphics Framework

From Dooba Wiki
Jump to navigation Jump to search
Typical I2C OLED display based on ssd1306 controller
Typical SPI TFT display based on st7735r controller

Introduction

Displaying information is often a crucial part of many embedded designs. Whether it's a portable gadget or a fixed device, having some kind of display is a rather common requirement.

But displays come in many forms and can vary greatly when it comes to interfacing them.

Typical "common" displays you might encounter usually include:

  • Monochrome I2C OLED - usually based on ssd1306 controller
  • Hi-color SPI TFT - usually based on st7735r controller

Each of these usually has their specific advantages and disadvantages and choosing one over another will mostly depend on the target application. TFT has higher resolution and color, but OLED doesn't require backlight and is 'quicker' to draw to (fewer pixels and bits per pixel).

Controlling these devices and drawing on them is significantly different depending on the interface/controller chip.

To make things simple, we have a generic graphics framework that allows drawing pretty much anything on any display. All that's needed is a driver for the display that integrates into this graphics framework.

Since the two display controllers listed above are the most common, we provide drivers for both, check out the tutorials on how to use them here:

We keep adding new drivers regularly, other displays will come soon!

This page will focus on the generic graphics framework - how it works and how to use it.

Drawing modes

Embedded systems have limited resources. Graphical processing is intensive. Everything is a trade-off between display refresh speed and memory usage.

To keep things simple yet efficient, we decided to split the concept of "drawing" into two different operation modes to accommodate any usage:

Direct drawing

Direct drawing is the "simple" mode, which will seem pretty familiar if you are used to other display libraries.

Basically, we start drawing, then draw some primitives (basic shapes, text, images, ...), then stop drawing. At this point everything is displayed on screen. Internal buffering in the device drivers ensure fast and efficient drawing, but may cause "artifacts" (background overlap) when drawing large shapes in one go.

Invalidation stack

This mode is specifically tailored for fast frame-based drawing, such as in UI frameworks (like Yolk) or game engines.

A stack of invalidated areas within the display is maintained by the framework.

At every frame, an invalidated region is pulled and updated. This greatly simplifies UI programming: our application simply draws "everything" at each frame, but the graphics framework will actually filter the pixels to only update invalidated regions. Here we don't care if the entire display is not updated in a single frame. Instead, we update a bit of the display at every frame to keep the main loop running smoothly.

Whenever something occurs, our application simply invalidates whichever part of the display that needs updating. That part will then be re-drawn over the next few frames.

the gfx library

All of this is packaged as a single library: gfx.

Before we can use it, let's add it to our dependencies in dfe.conf:

name: example_app
type: app
mmcu: atmega1284p
freq: 10000000
deps:
  - gfx

The 'gfx_dsp' object

The gfx library defines the interface that display drivers must implement: struct gfx_dsp.

Pretty much every function in the gfx library operates on a display device and therefore requires a gfx_dsp pointer (struct gfx_dsp *).

Such objects will be provided by display drivers - have a look at ssd1306 or st7735r to see how to get these objects.

The examples provided here will assume a 'dsp0' object is available (struct gfx_dsp *dsp0;).

Basic display control

Before we dig into actually drawing stuff to the screen, we will explore some basic controls we have for the display object.

Display dimensions

If our application needs the dimensions of the display, it can use the functions below to get them:

// Get Display Width
uint16_t gfx_dsp_get_width(struct gfx_dsp *dsp);

// Get Display Height
uint16_t gfx_dsp_get_height(struct gfx_dsp *dsp);

Orientation

At any moment we may change the orientation of the display. This allows the physical placement of the display to be decoupled from the information being shown.

To control (get/set) the orientation, we can use the following:

// Set Display Orientation
void gfx_dsp_set_orientation(struct gfx_dsp *dsp, uint8_t orientation);

// Get Display Orientation
uint8_t gfx_dsp_get_orientation(struct gfx_dsp *dsp);

The orientation is an integer value and must be one of:

  • GFX_DSP_ORIENTATION_TOP
  • GFX_DSP_ORIENTATION_BOTTOM
  • GFX_DSP_ORIENTATION_LEFT
  • GFX_DSP_ORIENTATION_RIGHT

On & Off

We can turn the display 'on' or 'off' using the following:

// Display On
void gfx_dsp_on(struct gfx_dsp *dsp);

// Display Off
void gfx_dsp_off(struct gfx_dsp *dsp);

Draw control (start / stop)

Whether in direct-draw or inv-stack mode, we must wrap any actual drawing between 'start' and 'stop' statements.

This is actually where the distinction between the two drawing modes happens - it all depends on which 'start' / 'stop' statements are used.

Direct drawing

To immediately draw something to the display, we can use the following:

// Start drawing
gfx_dsp_start(dsp0);

// Draw stuff...

// Stop drawing (and flush any buffered data to the display)
gfx_dsp_stop(dsp0);

Invalidation stack

If we want to use the invalidation stack, then we need to call the following at each frame:

// Start drawing using invalidation stack
if(gfx_dsp_start_inv(dsp0) == 0)
{
    // Draw everything (entire frame) - Filtering is done by GFX framework
}

// Stop drawing
gfx_dsp_stop_inv(dsp0);

Note how the 'gfx_dsp_start_inv' function returns a value. It returns '1' if there is nothing to draw (no area has been invalidated). For this reason we only draw if it returns '0'.

Invalidating the display

When using the invalidation stack, our application must selectively invalidate areas of the display that need to be updated.

To accomplish this, we can simply call void gfx_dsp_invalidate(struct gfx_dsp *dsp, int x, int y, uint16_t w, uint16_t h);

// Something happened, let's invalidate the upper half of the display
gfx_dsp_invalidate(dsp0, 0, 0, gfx_dsp_get_width(dsp0), gfx_dsp_get_height(dsp0) / 2);

Colors

All colors within the GFX framework are 16-bit.

They can be generated either from red, green and blue components or using an HTML color code.

  • RGB: uint16_t gfx_col(uint8_t r, uint8_t g, uint8_t b);.
  • HTML: uint16_t gfx_col_h(uint32_t html_value);.

Plotting a pixel

Once we are inside a 'start' / 'stop' drawing block, we can draw individual pixels using:

// Plot Pixel
void gfx_dsp_plot(struct gfx_dsp *dsp, uint16_t x, uint16_t y, uint16_t c);

Where 'c' is a 16-bit color as defined above.

Example (using direct drawing):

// Start Drawing
gfx_dsp_start(dsp0);

// Draw a single orange pixel @ 10:15 (using R,G,B color)
gfx_dsp_plot(dsp0, 10, 15, gfx_col(0xff, 0x7f, 0x00));

// Draw a single light green pixel @ 10:20 (using HTML color)
gfx_dsp_plot(dsp0, 10, 20, gfx_col_h(0x7fff00));

// Stop drawing
gfx_dsp_stop(dsp0);

Clearing the display

While we could simply use a 'fill' (as will be seen later) to clear part of the display, a dedicated method is provided that makes use of some specific pre-driver optimizations.

Whenever we need to clear a region of the display, we can use the following function:

// Clear Area
void gfx_dsp_clr(struct gfx_dsp *dsp, uint16_t x, uint16_t y, uint16_t w, uint16_t h);

Example (using direct drawing):

// Start Drawing
gfx_dsp_start(dsp0);

// Clear upper half of display
gfx_dsp_clr(dsp0, 0, 0, gfx_dsp_get_width(dsp0), gfx_dsp_get_height(dsp0) / 2);

// Stop drawing
gfx_dsp_stop(dsp0);

Simple Primitives

A set of functions are provided to draw basic shapes:

// Draw Line
void gfx_draw_line(struct gfx_dsp *dsp, int from_x, int from_y, int to_x, int to_y, uint16_t c);

// Draw Fill
void gfx_draw_fill(struct gfx_dsp *dsp, int x, int y, uint16_t w, uint16_t h, uint16_t c);

// Draw Rectangle
void gfx_draw_rect(struct gfx_dsp *dsp, int x, int y, int w, int h, uint16_t c);

// Draw N-Gon (n = number of lines, varargs = x0, y0, x1, y1, x2, y2)
void gfx_draw_ngon(struct gfx_dsp *dsp, uint16_t c, uint8_t n, ...);

// Draw N-Gon (variable argument list)
void gfx_draw_ngon_v(struct gfx_dsp *dsp, uint16_t c, uint8_t n, va_list ap);

// Draw Circle
void gfx_draw_circle(struct gfx_dsp *dsp, int x, int y, uint16_t r, uint16_t c);

Fonts / Text

The 'basic 8x16' font

The graphics framework allows loading fonts from flash memory to draw text.

As shown in the 'basic 8x16' font on the right, fonts are stored as images where every character from (including) ' ' (0x20) to '~' (0x7e) is stored in a "grid".

The extension of the font file defines the character dimensions (width / height) as well as the dimensions of the whole image.

Have a look at creating assets to see how to create fonts.

Loading fonts

Before fonts can be used to draw text, we need to initialize a struct gfx_font with some meta-information.

A pointer to this 'gfx_font' structure is what we will then pass to the text-draw functions later on.

All we need is a pointer to some font data in flash memory. Let's have a look at an example using one of the default fonts provided by the GFX framework: gfx_font_basic6x8.

The code below first includes the font data header file from the GFX framework, then loads the font for use:

#include <gfx/assets/gfx_font_basic6x8.h>

// Font struct for 'basic6x8'
struct gfx_font my_font;

void init()
{
    // ...

    // Load Font Data
    gfx_font_init(&my_font, gfx_font_basic6x8_data);
}

void loop()
{
    // ...
}

Included fonts

The GFX framework includes some basic built-in fonts of a few different sizes:

  • gfx_font_basic6x8
  • gfx_font_basic6x12
  • gfx_font_basic8x16
  • gfx_font_basic10x20

Drawing text

After we have initialized our font, we can use it to draw text with the help of the following functions:

  • void gfx_txt(struct gfx_dsp *dsp, void *s, int x, int y, uint16_t w, uint16_t h, struct gfx_font *font, uint16_t col); (Simple 0-terminated string)
  • void gfx_txt_n(struct gfx_dsp *dsp, void *s, uint16_t l, int x, int y, uint16_t w, uint16_t h, struct gfx_font *font, uint16_t col); (Fixed-length string)
  • void gfx_txt_f(struct gfx_dsp *dsp, int x, int y, uint16_t w, uint16_t h, struct gfx_font *font, uint16_t col, void *fmt, ...); (Format string)
  • void gfx_txt_v(struct gfx_dsp *dsp, int x, int y, uint16_t w, uint16_t h, struct gfx_font *font, uint16_t col, void *fmt, va_list ap); (Format string with va_list)
  • void gfx_txt_vp(struct gfx_dsp *dsp, int x, int y, uint16_t w, uint16_t h, struct gfx_font *font, uint16_t col, void *fmt, va_list *ap); (Format string with va_list pointer)

The 'w' and 'h' arguments allow us to limit the text to a bounding box. If both passed as 0, the resulting bounding box will default to a single line (font->fh) spanning to the end of the display.

If we pass a height greater than the character height of the font, the text will span multiple lines (if necessary). The GFX framework will do its best to cut lines at word boundaries.

// Load Font Data
gfx_font_init(&my_font, gfx_font_basic6x8_data);

// Draw some simple text
gfx_txt(dsp0, "Hello, world!", 0, 0, 0, 0, &my_font, gfx_col(255, 127, 0));

// Draw some format-string text
gfx_txt_f(dsp0, 0, 0, 0, 0, &my_font, gfx_col(255, 127, 0), "Value: %i", 42);

Tilesets

The 'basic frame' tileset

Tilesets are images composed of multiple sub-images of fixed size, that are generally used to produce repeating patterns. Typical usage examples include:

  • video game maps
  • game character animations
  • UI elements (menus / buttons)

Because tilesets usually contain only few different colors, they actually use palettes to store color information. This allows tilesets to use only 8 bits per pixel.

Loading tilesets

Just like fonts, tilesets are stored in flash memory and need to be loaded into a struct gfx_tileset before they can be used:

#include <gfx/assets/gfx_tileset_frame_basic.h>

// Tileset struct for 'basic frame'
struct gfx_tileset my_tileset;

void init()
{
    // ...

    // Load Tileset Data
    gfx_tileset_init(&my_tileset, gfx_tileset_frame_basic_data);
}

void loop()
{
    // ...
}

Drawing tiles

Once our tileset is loaded, we can start drawing tiles using the functions below:

// Draw Tile
void gfx_tileset_draw(struct gfx_dsp *dsp, struct gfx_tileset *tileset, uint16_t tile, int x, int y);

// Draw Partial Tile
void gfx_tileset_draw_part(struct gfx_dsp *dsp, struct gfx_tileset *tileset, uint16_t tile, int x, int y, uint16_t t_x, uint16_t t_y, uint16_t w, uint16_t h);

The 'tile' argument is the desired tile's index within the tileset. Tiles are numbered in lines, left to right and top to bottom.

The gfx_tileset_draw_part function allows drawing only part of a given tile. The 't_x' and 't_y' arguments can be set to the X/Y offset within the tile from which we want to draw, while the 'w' and 'h' arguments allow us to set the size of the tile part we want to draw.

Drawing tiled frames

Frames are some of the most common uses for tilesets. Therefore to make it easier to draw frames the following is provided:

// Draw Tiled Frame
void gfx_tileset_draw_frame(struct gfx_dsp *dsp, struct gfx_tileset *tileset, int x, int y, uint16_t w, uint16_t h, uint16_t tl, uint16_t tr, uint16_t bl, uint16_t br, uint16_t t, uint16_t b, uint16_t l, uint16_t r, uint16_t c);

The 'tl', 'tr', 'bl', 'br', 't', 'b', 'l', 'r', and 'c' arguments are the tile indices for each part of the frame - respectively top left, top right, bottom left, bottom right, top, bottom, left, right, and center.

To make it even easier, we can use a simplified version if our frame tileset has a "standard" layout (as can be seen in the 'basic frame' tileset above):

// Draw Default Tiled Frame
void gfx_tileset_draw_frame_default(struct gfx_dsp *dsp, struct gfx_tileset *tileset, int x, int y, uint16_t w, uint16_t h);

If we want to have a partial frame (for example with no center filling), we simply pass 0xff (invalid tile) as the tile index for the part we don't want.

Included frame tilesets

The GFX framework includes some basic built-in frame tilesets:

  • gfx_tileset_frame_basic
  • gfx_tileset_frame_tinymono

Drawing progress bars

The 'basic progress bar' tileset

Another very common use of tilesets is progress bars. Just like frames, we can draw those easily with:

// Draw Progress Bar
extern void gfx_pbar_draw(struct gfx_dsp *dsp, struct gfx_tileset *tlst, uint8_t pct, uint16_t w, int x, int y, uint16_t l, uint16_t c, uint16_t r, uint16_t fl, uint16_t fc, uint16_t fr);

The 'l', 'c', and 'r' arguments are the tile indices for the left, center, and right parts of the "empty" progress bar, while the 'fl', 'fc', and 'fr' arguments are the tile indices for the "filled" progress bar.

Again, if we follow the "standard" progress bar tileset layout (as seen in the 'basic progress bar' tileset), we can use a simplified function to draw:

// Draw Default Progress Bar
extern void gfx_pbar_draw_default(struct gfx_dsp *dsp, struct gfx_tileset *tlst, uint8_t pct, uint16_t w, int x, int y);

Included progress bar tilesets

The GFX framework includes some basic built-in progress bar tilesets:

gfx_tileset_pbar_basic gfx_tileset_pbar_tinymono

Other uses

Tilesets have many practical applications. Another very common use is for animations: a sequence of frames can be packaged as a tileset.

An example of this is for "spinners" - short animations that are displayed while the user waits for something.

Some built-in spinner tilesets are provided as part of the GFX framework:

  • gfx_tileset_spinner_basic
  • gfx_tileset_spinner_tinymono

Images

Images are 16-bit-per-pixel and can be drawn from the RAM, from flash memory, or even from a file stored on the VFS.

A bunch of functions are provided to accomplish this. The '_full' versions of these are "simplified" to always draw the full image, while the base versions expect 'img_x', 'img_y', 'w', and 'h' arguments to indicate which part (offset + size) of the image should actually be drawn.

Images in RAM

// Draw Full Image from RAM Memory
extern void gfx_img_full(struct gfx_dsp *dsp, uint8_t *img, int x, int y);

// Draw Image from RAM Memory
extern void gfx_img(struct gfx_dsp *dsp, uint8_t *img, int x, int y, uint16_t img_x, uint16_t img_y, uint16_t w, uint16_t h);

Images in flash memory

// Draw Full Image from Program Memory
extern void gfx_img_p_full(struct gfx_dsp *dsp, const uint8_t *img, int x, int y);

// Draw Image from Program Memory
extern void gfx_img_p(struct gfx_dsp *dsp, const uint8_t *img, int x, int y, uint16_t img_x, uint16_t img_y, uint16_t w, uint16_t h);

Images in VFS

From file path

// Draw Full Image from File System
extern void gfx_img_f_full(struct gfx_dsp *dsp, char *file, int x, int y);

// Draw Full Image from File System - Fixed Length
extern void gfx_img_f_full_n(struct gfx_dsp *dsp, char *file, uint8_t file_len, int x, int y);

// Draw Image from File System
extern void gfx_img_f(struct gfx_dsp *dsp, char *file, int x, int y, uint16_t img_x, uint16_t img_y, uint16_t w, uint16_t h);

// Draw Image from File System - Fixed Length
extern void gfx_img_f_n(struct gfx_dsp *dsp, char *file, uint8_t file_len, int x, int y, uint16_t img_x, uint16_t img_y, uint16_t w, uint16_t h);

From file pointer (VFS handle)

// Draw Full Image from File System (with file pointer)
extern void gfx_img_fp_full(struct gfx_dsp *dsp, struct vfs_handle *fp, int x, int y);

// Draw Image from File System (with file pointer)
extern void gfx_img_fp(struct gfx_dsp *dsp, struct vfs_handle *fp, int x, int y, uint16_t img_x, uint16_t img_y, uint16_t w, uint16_t h);

Creating assets

All assets are created using similar processes. The recommended way is to use GIMP, but the instructions presented below should be applicable to other tools.

Depending on what we're creating we should use either RGB mode (full-color images), 255-color palette mode (tilesets), or 1-bit two-color palette mode (fonts).

We can then export our asset as 'raw data' (with default export settings).

When exporting an 'indexed' image (such as tilesets or fonts), GIMP will automatically produce a '.pal' file containing the image's palette. For tilesets, this palette needs to be kept with the tileset file itself. For Fonts, this file should be deleted (will not be used).

A simplified summary is presented below.

Fonts

  • Image mode: 1-bit palette (indexed) mode
  • File extension: *.total_w.total_h.char_w.char_h.font
  • Keep palette file: no

Tilesets

  • Image mode: 255-color palette (indexed) mode
  • File extension: *.total_w.total_h.tile_w.tile_h.font
  • Keep palette file: yes (*.pal)

Images

  • Image mode: RGB mode with alpha channel (make sure transparency is enabled in your image)
  • File extension: *.image_w.image_h.img

Using assets

Assets (fonts, tilesets & images) will get converted to generic binary objects by dbuild. This means they will be embedded into the final firmware image that dbuild produces.

From our application code, we can access these assets as byte arrays with the name of the asset file (without extension) suffixed with '_data'. The size of the array also gets defined as ASSET_FILENAME_DATA_SIZE.

Both are defined in a new C header (.h) file with the name of the asset file.

Example: font file 'gfx/assets/gfx_font_basic8x16.240.64.8.16.font' will generate 'gfx/assets/gfx_font_basic8x16.h', which will define:

  • #define GFX_FONT_BASIC8x16_DATA_SIZE 30720
  • extern const uint8_t gfx_font_basic8x16_data[GFX_FONT_BASIC8x16_DATA_SIZE] PROGMEM;

This is explained in more detail in Binary objects.