Getting started

Add the library to an STM32CubeIDE project, write the two port functions for your bus, and draw the first screen. Then use the self-test patterns to check the panel.

What you need

  • An STM32 with a way to write a command byte and a 16-bit data word to the display controller: FMC, SPI or a GPIO-driven 8-bit bus.
  • A 240 x 240 ST7789 panel. The controller must already be initialised (sleep out, pixel format, memory access control, display on). That code belongs to your board support, not to this library.
  • A C99 compiler. STM32CubeIDE with the GNU Arm toolchain works as it is.

Add the library to an STM32CubeIDE project

Option A: separate files

  1. Copy include/ and src/ from the repository into your project, for example to Drivers/stm32-st7789-gfx/include and Drivers/stm32-st7789-gfx/src.
  2. Right-click the project and open Properties, C/C++ General, Paths and Symbols. On the Source Location tab add the src folder; on the Includes tab (GNU C) add ${ProjDirPath}/Drivers/stm32-st7789-gfx/include.
  3. Keep the default compiler options that put every function and data object in its own section (-ffunction-sections -fdata-sections) and discard unused sections at link time (-Wl,--gc-sections). They remove the fonts and icons you never use.
  4. Save source files as UTF-8 (Window, Preferences, General, Workspace, Text file encoding) so strings such as "25 °C" reach the library as UTF-8.

Option B: single header

  1. Copy single_header/lcd_gfx.h to Core/Inc.
  2. In exactly one source file, for example Core/Src/gfx_impl.c, write:
    #define GFX_IMPLEMENTATION
    #include "lcd_gfx.h"
  3. In every other file that draws, include lcd_gfx.h without the define.
  4. Optional: define GFX_WITHOUT_DEMO and GFX_WITHOUT_SELFTEST in the project settings to leave those modules out. Define them for every file, so the declarations match.

Write the port

The library needs two functions, declared in gfx_port.h. You implement them once, in any source file.

void gfx_port_cmd(uint8_t command);   /* write a command byte  (A0 / D/C = 0) */
void gfx_port_data(uint16_t data);    /* write a data word     (A0 / D/C = 1) */
Know what travels through gfx_port_data(). After CASET (0x2A) or RASET (0x2B) it carries four 8-bit parameters in the low byte. After RAMWR (0x2C) it carries one 16-bit RGB565 pixel per call. A 16-bit FMC port can ignore the difference. An SPI or 8-bit port must remember the last command and send one byte for parameters and two bytes, high byte first, for pixels.

FMC, 16-bit

FMC wiring: 16 data lines, chip select, write and read strobes, and address line A0 wired to the controller D/CX pin
Wire FMC_A0 to the controller's D/CX pin. A write at the bank base is a command; a write at base + 2 is data.

With a 16-bit memory width the FMC drives FMC_A0 from address bit 1. A write to the bank base address therefore has A0 = 0 (command) and a write to base + 2 has A0 = 1 (data). In CubeMX choose the NOR/SRAM mode with a 16-bit data bus, enable the chip select and address line A0, and set the address setup and data setup times so the write cycle meets your panel's datasheet.

Chip selectBank base address
FMC_NE10x60000000
FMC_NE20x64000000
FMC_NE30x68000000
FMC_NE40x6C000000
#include "gfx_port.h"

#define LCD_BASE  0x64000000u                        /* FMC_NE2, adjust to your chip select */
#define LCD_REG   (*(volatile uint16_t *)(LCD_BASE))       /* A0 = 0: command */
#define LCD_RAM   (*(volatile uint16_t *)(LCD_BASE + 2u))  /* A0 = 1: data    */

void gfx_port_cmd(uint8_t command) { LCD_REG = command; }
void gfx_port_data(uint16_t data)  { LCD_RAM = data;    }

If your project already has helper functions for these two accesses, call them instead. On the STM32F723E-DISCO, use the bank and the helpers that your board support code defines.

SPI, 4-wire

A 4-wire SPI panel has a chip select, a data/command pin and 8-bit transfers. Track the last command so pixels go out as two bytes.

#include "gfx_port.h"

extern void spi_write(const uint8_t *buf, uint16_t len);   /* your SPI transmit */
#define LCD_DC_LOW()   HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET)
#define LCD_DC_HIGH()  HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET)
#define LCD_CS_LOW()   HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET)
#define LCD_CS_HIGH()  HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET)

static uint8_t last_cmd;

void gfx_port_cmd(uint8_t command)
{
    last_cmd = command;
    LCD_DC_LOW();
    LCD_CS_LOW();
    spi_write(&command, 1);
    LCD_CS_HIGH();
}

void gfx_port_data(uint16_t data)
{
    LCD_DC_HIGH();
    LCD_CS_LOW();
    if (last_cmd == 0x2C) {                 /* RAMWR: RGB565 pixel, high byte first */
        uint8_t px[2] = { (uint8_t)(data >> 8), (uint8_t)data };
        spi_write(px, 2);
    } else {                                /* CASET / RASET: 8-bit parameter */
        uint8_t p = (uint8_t)data;
        spi_write(&p, 1);
    }
    LCD_CS_HIGH();
}

This version favours clarity. For speed, keep chip select low for the whole burst, use hardware SPI at the highest clock the panel accepts, and avoid HAL call overhead per word.

8-bit parallel bus

An 8080-style 8-bit bus has eight data lines, a write strobe and a D/C pin. The same rule applies: one byte for parameters, two bytes (high first) for pixels.

#include "gfx_port.h"

static uint8_t last_cmd;

static void bus_write8(uint8_t v)
{
    LCD_DATA_PORT_WRITE(v);   /* put v on D[7:0] */
    LCD_WR_LOW();             /* the controller latches on the rising edge */
    LCD_WR_HIGH();
}

void gfx_port_cmd(uint8_t command)
{
    last_cmd = command;
    LCD_DC_LOW();
    bus_write8(command);
}

void gfx_port_data(uint16_t data)
{
    LCD_DC_HIGH();
    if (last_cmd == 0x2C) {
        bus_write8((uint8_t)(data >> 8));
        bus_write8((uint8_t)data);
    } else {
        bus_write8((uint8_t)data);
    }
}

Draw the first screen

#include "gfx_core.h"
#include "gfx_shapes.h"
#include "gfx_text.h"
#include "gfx_widgets.h"
#include "gfx_fonts.h"

void ui_demo(void)
{
    gfx_set_theme(&gfx_theme_dark);
    gfx_fill_screen(gfx_theme->background);
    gfx_widget_status_bar("Hello", 82, 1);
    gfx_widget_ring(62, 110, 40, 9, 73, gfx_theme->accent, "73%");
    gfx_text(120, 100, "CPU load", &gfx_font_sans_16, gfx_theme->text, gfx_theme->background);
}

To see everything the library can do, loop over the demo pages. Each call draws a full screen.

#include "gfx_demo.h"

for (;;) {
    for (int page = 0; page < gfx_demo_page_count(); page++) {
        gfx_demo_page(page);
        HAL_Delay(3000);
    }
}

Check the panel with the self-test

Before you debug your own UI, run the nine bring-up patterns. Each makes one class of fault easy to see. The three levels of testing, from the PC to the board, are described in Testing.

#include "gfx_selftest.h"

for (int page = 0; page < gfx_selftest_page_count(); page++) {
    gfx_selftest_page(page);
    HAL_Delay(2000);
}

Typical problems

SymptomLikely causeWhat to doPattern that shows it
Mirrored image: text reads backwards, or the picture is upside downMemory access control (MADCTL, command 0x36) has the column or row order bits set the wrong way for your module.Flip the MX (0x40) or MY (0x80) bit in your init sequence.Frame and corners (page 2), Text and icons (page 5)
Red and blue swappedThe RGB/BGR bit of MADCTL (0x08) does not match the panel, or the two bytes of a pixel are sent in the wrong order on SPI or an 8-bit bus.Toggle the BGR bit in MADCTL. On SPI or 8-bit, send the high byte first.Colour bars (page 0): the first bar must be red
Picture shifted, a strip of the wrong content at one edge, black rowsThe controller RAM is 240 x 320 and some 240 x 240 modules show only part of it, starting at a row offset that depends on the orientation.Choose the MADCTL orientation whose visible area starts at row 0. The library has no offset setting.Frame and corners (page 2): the white frame must touch all four edges
Noise, snow, wrong colours, flickerBus timing too fast, a data line that is missing or shorted, or a wrong bus width in the FMC setup.Lengthen the FMC address and data setup times, check every D line with the solid pages, and shorten the wires.Solid red, green, blue (pages 6 to 8): each must be one flat colour
Colours look faded or bandedThe pixel format is not set to 16 bits per pixel (COLMOD 0x3A = 0x55), or some data bits are not connected.Send COLMOD with 16-bit RGB565 in the init sequence and check the wiring.Gray ramp (page 1): 16 clean steps
Circles look like ellipses, lines are skewedThe row or column window is wrong, or the panel is configured for a different resolution.Check CASET and RASET end addresses (239) and the panel configuration.Grid, cross and circle (page 3)
Accented characters appear as ? or as two odd glyphsThe source file is not saved as UTF-8, or the character is above U+00FF.Save the file as UTF-8. Only code points up to U+00FF exist in the fonts.Text and icons (page 5)
Nothing appears at allThe controller is not initialised, reset is held low, or the backlight is off.Check reset and backlight pins and run your init sequence before the first draw call.Any solid page

The exact bit values depend on your module, so confirm them in the controller datasheet and in the module vendor's init code.

Known limits

  • No rotation. The library draws in the controller's native 240 x 240 orientation. Rotate the image with the MADCTL bits in your init code if you need another orientation.
  • Font data is about 141 KB of flash for all six fonts, and 16 KB for the 32 icons. The linker removes what you do not reference, as long as section garbage collection is on.
  • Latin-1 only. Fonts cover U+0020 to U+007E and U+00A0 to U+00FF. Other code points are drawn as ?. The 48 px bold font holds only digits, the colon and the degree sign.
  • Fixed screen size. GFX_WIDTH and GFX_HEIGHT are 240. There is no run-time offset or resolution setting.
  • Write-only. Nothing reads pixels back from the controller.
  • Polygons have at most 16 vertices.
  • No DMA streaming is built in. The burst loop calls gfx_port_data() once per pixel; you can make your port faster as you see fit.