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
- Copy
include/andsrc/from the repository into your project, for example toDrivers/stm32-st7789-gfx/includeandDrivers/stm32-st7789-gfx/src. - Right-click the project and open Properties, C/C++ General, Paths and Symbols. On the Source Location tab add the
srcfolder; on the Includes tab (GNU C) add${ProjDirPath}/Drivers/stm32-st7789-gfx/include. - 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. - 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
- Copy
single_header/lcd_gfx.htoCore/Inc. - In exactly one source file, for example
Core/Src/gfx_impl.c, write:#define GFX_IMPLEMENTATION #include "lcd_gfx.h" - In every other file that draws, include
lcd_gfx.hwithout the define. - Optional: define
GFX_WITHOUT_DEMOandGFX_WITHOUT_SELFTESTin 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) */
FMC, 16-bit
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 select | Bank base address |
|---|---|
FMC_NE1 | 0x60000000 |
FMC_NE2 | 0x64000000 |
FMC_NE3 | 0x68000000 |
FMC_NE4 | 0x6C000000 |
#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
| Symptom | Likely cause | What to do | Pattern that shows it |
|---|---|---|---|
| Mirrored image: text reads backwards, or the picture is upside down | Memory 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 swapped | The 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 rows | The 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, flicker | Bus 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 banded | The 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 skewed | The 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 glyphs | The 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 all | The 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_WIDTHandGFX_HEIGHTare 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.