Home ▸ STM32 Tutorials ▸ GC9A01 Round Display with STM32

Interface GC9A01 Round Display with STM32 Using SPI and LVGL

The GC9A01 is a 1.28 inch round IPS display with a resolution of 240×240 pixels. Its circular shape suits smartwatch faces, circular gauges, and small instrument panels, and the SPI interface keeps the wiring to a handful of pins. Getting it to work on an STM32 needs three things: a correct SPI setup, a small display driver, and an LVGL port layer that connects the two.

This tutorial shows how to connect the GC9A01 to an STM32H562 over SPI, with and without DMA. It then walks through installing LVGL 9.2 in STM32CubeIDE, writing the flush callback, and running one of the built-in LVGL animation examples on the round screen. Part 2 of this series builds on the same setup to create an analog clock with SquareLine Studio and the STM32 RTC. The complete CubeIDE project is available for download at the end of the post.

You will follow the steps more easily if you have already used LVGL on an STM32. The LVGL on STM32 series explains the basics, and the STM32 SPI tutorial covers the SPI peripheral in detail. If you are still choosing a display, the STM32 OLED tutorials and STM32 LCD tutorials cover other options.

GC9A01 Round Display Overview

The GC9A01 is a driver IC for round TFT panels, and the module used in this tutorial pairs it with a 1.28 inch IPS panel at 240×240 pixels. The active area is fully circular, so the display works well in projects where the shape of the screen is part of the product design, such as watch faces, dial indicators, and compact control panels.

GC9A01 1.28 inch 240x240 IPS round TFT LCD display module with SPI interface showing VCC GND SCL SDA DC CS RST pins

GC9A01 Key Specifications

FeatureSpecification
Panel size1.28 inch circular
Resolution240 × 240 pixels
Panel typeIPS (wide viewing angle)
Color depth65K (16-bit) or 262K (18-bit)
InterfaceSPI
Supply voltage3.3 V

The IPS panel gives wide viewing angles and accurate colors, and the pixel grid is dense enough to render small icons, gauge needles, and text clearly.


Common GC9A01 Applications

The GC9A01 is used in the following types of projects:

  • Smartwatches and fitness trackers
  • Industrial meters and circular progress indicators
  • Custom control panels for appliances
  • Retro gaming devices with round screens
  • Compact medical instruments
  • Status displays on robots and drones

GC9A01 STM32 Wiring and Hardware Requirements

This tutorial uses the WeAct Studio STM32H562RGT6 development board, which has 1 MB of Flash and 640 KB of SRAM. That is more than enough for the LVGL library, the display driver, and the draw buffers. The display is the 1.28 inch GC9A01 round TFT module from the ControllersTech shop.

You can use any other STM32 board as long as it meets two requirements:

  • At least 512 KB of Flash. The compiled LVGL library takes a large share of the code space.
  • At least 20 to 30 KB of free SRAM. This memory holds the LVGL draw buffers. A board with less RAM can still work with smaller buffers, but the frame rate will be lower.

Wiring the GC9A01 to STM32 over SPI

The display uses a standard SPI connection with three extra GPIO lines for data/command select, chip select, and reset. A MISO line is not needed because the display only receives data from the microcontroller.

Wiring diagram connecting GC9A01 round TFT display to STM32H562 development board via SPI showing PA5 SCK PA7 MOSI PA4 DC PA6 CS PA3 RST

GC9A01 to STM32H5 Pin Connections

GC9A01 PinFunctionSTM32H5 Pin
VCCPower supply3.3 V
GNDGroundGND
SCLSPI clockPA5
SDASPI data (MOSI)PA7
DCData/command selectPA4
CSChip selectPA6
RSTResetPA3

The DC, CS, and RST pins are configured as normal GPIO outputs in CubeMX. The display driver toggles them in software, so they do not belong to the SPI peripheral.


CubeMX Setup for GC9A01 SPI and DMA

Clock Configuration

STM32CubeMX clock tree for STM32H562 showing 8 MHz HSE with PLL1 configured to 250 MHz SYSCLK and PLL1Q adjusted to 50 MHz for SPI1

The STM32H562 runs from an 8 MHz external crystal and uses the maximum system clock of 250 MHz. Change the PLL1Q divider so that the clock feeding SPI1 is 50 MHz before the SPI prescaler is applied.


SPI1 Parameters

STM32CubeMX SPI1 configuration set to Half Duplex Master mode with 8-bit data MSB first prescaler 4 yielding 12.5 MHz clock for GC9A01

Enable SPI1 in CubeMX and apply the following settings:

  • Mode: Half-Duplex Master. The display never sends data back, so the interface only transmits.
  • Data size: 8 bits, MSB first.
  • Prescaler: 4, which gives an SPI clock of 12.5 MHz (50 MHz divided by 4). This speed is safe for the display.
  • Clock polarity and phase: CPOL Low and CPHA 1 Edge, which is SPI Mode 0.

SPI DMA Configuration (Optional)

STM32CubeMX DMA configuration for SPI1 TX showing Memory to Peripheral direction Normal mode Byte data width for GC9A01 display transfers

DMA moves the pixel data to the SPI peripheral without CPU involvement. This lets lv_timer_handler() keep running while a frame is being sent to the display, which raises the frame rate noticeably. To enable it, add a DMA request for SPI1 TX and use these settings:

  • Direction: Memory to Peripheral
  • Mode: Normal
  • Data width: Byte

After enabling DMA, set USE_DMA to 1 in GC9A01.h, as shown in the next section. If you skip DMA, the driver sends the data with blocking SPI transfers. This works correctly, but it is slower for animations that redraw most of the screen.


GPIO Pin Configuration

STM32CubeMX GPIO pin configuration showing PA3 labeled RST PA4 labeled DC and PA6 labeled CS configured as GPIO Output for GC9A01 display

CubeMX assigns PA5 (SCK) and PA7 (MOSI) to SPI1 automatically. The other three pins need to be set up by hand:

  • PA3: GPIO Output with the label RST
  • PA4: GPIO Output with the label DC
  • PA6: GPIO Output with the label CS

Use the same names that appear in the GC9A01_CS_PORT, GC9A01_DC_PORT, and GC9A01_RST_PORT definitions in GC9A01.h. Matching them prevents pin-name mismatches in the generated code.


GC9A01 Driver Setup in CubeIDE

Copy GC9A01.c into the Src folder of the project and GC9A01.h into the Inc folder. The project tree should then look like the one below.

STM32CubeIDE project Explorer showing GC9A01.c in Src folder GC9A01.h in Inc folder and LVGL library inside Drivers directory

Open GC9A01.h and edit the user configuration block so that it matches your wiring:

// ==== USER CONFIGURATIONS ====
#define GC9A01_SPI           hspi1
#define GC9A01_SPI_TIMEOUT   100
#define USE_DMA              0      // set to 1 if DMA is enabled in CubeMX

#define GC9A01_CS_PORT       GPIOA
#define GC9A01_CS_PIN        GPIO_PIN_6

#define GC9A01_DC_PORT       GPIOA
#define GC9A01_DC_PIN        GPIO_PIN_4

#define GC9A01_RST_PORT      GPIOA
#define GC9A01_RST_PIN       GPIO_PIN_3

No changes are needed in GC9A01.c. The library already handles the display initialization, the pixel window setup, and the flush function.


LVGL 9.2 Installation and lv_conf.h Setup

Download LVGL 9.2

Download LVGL 9.2 as a ZIP file from the LVGL GitHub release branch. This tutorial uses version 9.2 because SquareLine Studio, which is used in Part 2, currently exports code for that version. A newer LVGL release causes API mismatches when you import the files generated by SquareLine Studio.

Extract the ZIP and rename the folder to lvgl. Then create a folder named lvgl inside the Drivers directory of your project and copy the extracted files into it.

STM32CubeIDE project showing lvgl folder containing LVGL 9.2 source files placed inside the Drivers directory

Create lv_conf.h

Copy lv_conf_template.h from the extracted LVGL folder, paste it next to the lvgl folder (not inside it), and rename it to lv_conf.h.

Project file tree showing lv_conf.h configuration file placed alongside the lvgl source folder inside the Drivers directory

Open lv_conf.h and change the first line from this:

#if 0   /* Set it to "1" to enable content */

to this:

#if 1
lv_conf.h file open in STM32CubeIDE editor with the guard macro changed from #if 0 to #if 1 to enable LVGL configuration

This change turns the configuration file on. While the guard is set to 0, LVGL ignores lv_conf.h and falls back to its default settings.


Add the LVGL Include Path

Right-click the project and open Properties, then go to C/C++ Build, Settings, MCU GCC Compiler, and Include Paths. Click Add, choose Workspace, select the lvgl folder inside Drivers, and confirm with OK and then Apply and Close.

STM32CubeIDE MCU GCC Compiler Include Paths panel showing Add button to include the LVGL Drivers folder path in the project
STM32CubeIDE workspace browser dialog showing lvgl folder inside Drivers being selected as the LVGL include path

With this path in place, #include "lvgl/lvgl.h" resolves correctly in every source file of the project.


LVGL Display Port and Flush Callback for GC9A01

LVGL talks to the screen through a port layer, which is a pair of files adapted from a template that ships with the library. Copy the following files from lvgl/examples/porting/ into your project and rename them:

  • lv_port_disp_template.c becomes Src/lv_port_disp.c
  • lv_port_disp_template.h becomes Inc/lv_port_disp.h
STM32CubeIDE project showing lv_port_disp.c added to Src and lv_port_disp.h added to Inc after renaming from LVGL template files

Set the Display Resolution

Open lv_port_disp.h and enter the resolution of the GC9A01:

#define MY_DISP_HOR_RES    240
#define MY_DISP_VER_RES    240

Initialize the Display in lv_port_disp_init

The lv_port_disp_init() function in lv_port_disp.c connects LVGL to the display hardware. The changes below create the display, register the flush callback, and allocate two draw buffers:

lv_display_t *myDisplay = NULL;

void lv_port_disp_init(void)
{
    disp_init();   // initialize GC9A01 hardware

    lv_display_t *disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
    lv_display_set_flush_cb(disp, disp_flush);
    myDisplay = disp;

    // Two partial render buffers, 10 rows each
    LV_ATTRIBUTE_MEM_ALIGN
    static uint8_t buf_2_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];

    LV_ATTRIBUTE_MEM_ALIGN
    static uint8_t buf_2_2[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];

    lv_display_set_buffers(disp, buf_2_1, buf_2_2,
                           sizeof(buf_2_1), LV_DISPLAY_RENDER_MODE_PARTIAL);
}

The disp_init() function only needs to call the driver initialization:

static void disp_init(void)
{
    GC9A01_Init();
}

Each draw buffer holds 10 rows of 240 pixels at 2 bytes per pixel, which is 4.8 KB per buffer. LVGL renders into one buffer while the other one is being sent to the display. This double-buffered partial mode is the recommended setup for SPI displays. A larger buffer, such as 20 rows, gives a higher frame rate but uses more SRAM.


Write the disp_flush Callback

LVGL calls disp_flush() whenever a region of the screen is ready to be sent to the display:

static void disp_flush(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *px_map)
{
    if (disp_flush_enabled) {
        // Swap RGB565 byte order for SPI displays
        lv_draw_sw_rgb565_swap(px_map,
            (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1));

        GC9A01_Flush(px_map, area->x1, area->y1, area->x2, area->y2);
    }
}

LVGL stores each RGB565 pixel with the low byte first, while the GC9A01 expects the high byte first. The call to lv_draw_sw_rgb565_swap() reverses the two bytes of every pixel before the data goes out over SPI. If the colors look wrong on your display, comment out this line and test again.

The GC9A01_Flush() function sets the write window on the display to the given area and then sends all the pixel bytes over SPI, using either blocking transfers or DMA depending on the USE_DMA setting.


Add the GC9A01_FlushReady Callback

LVGL has to be told when a transfer has finished so that it can reuse the buffer. The driver declares GC9A01_FlushReady() as a weak function, so you can override it in lv_port_disp.c:

void GC9A01_FlushReady(void)
{
    lv_display_flush_ready(myDisplay);
}

With blocking SPI (USE_DMA 0), the driver calls GC9A01_FlushReady() itself at the end of GC9A01_Flush(). With DMA (USE_DMA 1), the function is called from the SPI DMA transfer-complete interrupt, which allows LVGL to render the next block while the previous one is still being transmitted.


LVGL Tick and Main Loop on STM32

LVGL needs a millisecond tick to run its animations and timers. Open the interrupt file of your project (stm32h5xx_it.c for this board), include the LVGL header at the top, and add the tick call to the SysTick handler:

#include "lvgl/lvgl.h"    // add at top of file

void SysTick_Handler(void)
{
    /* USER CODE BEGIN SysTick_IRQn 0 */
    lv_tick_inc(1);       // increment LVGL internal tick by 1 ms
    /* USER CODE END SysTick_IRQn 0 */
    HAL_IncTick();
}

The lv_tick_inc(1) call advances the internal millisecond counter of LVGL. All animation timings, transition durations, and timer callbacks depend on this counter, so animations stay frozen without it.

In main(), initialize LVGL and the display port before the while loop, and call lv_timer_handler() inside the loop:

lv_init();              // initialize LVGL
lv_port_disp_init();   // initialize display and connect to LVGL

while (1)
{
    lv_timer_handler(); // process LVGL events and render pending frames
    HAL_Delay(5);       // wait about 5 ms between handler calls
}

The lv_timer_handler() function does most of the work. It finds the screen regions that need redrawing, calls disp_flush() for each of them, updates the animations, and runs the LVGL timers. The 5 ms delay keeps the call rate steady and leaves processor time for other code.


GC9A01 LVGL Animation Test

main() Code for the Test

LVGL includes a set of example programs, and the second animation example (lv_example_anim_2) shows a bouncing ball. It is a short test that checks the SPI link, the color output, and the flush path in one run:

#include "lv_port_disp.h"
#include "lvgl/examples/anim/lv_example_anim.h"

int main(void)
{
    // ... HAL_Init, SystemClock_Config, MX_SPI1_Init, MX_GPIO_Init ...

    lv_init();
    lv_port_disp_init();
    lv_example_anim_2();    // start the bouncing ball animation

    while (1)
    {
        lv_timer_handler();
        HAL_Delay(5);
    }
}

The header lv_example_anim.h is located in lvgl/examples/anim/, and the include path added earlier makes it reachable. The example function is defined in a source file inside the same examples folder, so no extra files need to be added to the project.


Test Result on the Round Display

Animated GIF showing LVGL playback animation example 2 running on GC9A01 1.28 inch round display with a red bouncing ball on STM32H562

The ball bounces on the round screen as shown in the LVGL documentation. The ball should be red. If it appears blue or cyan, the byte swap in disp_flush() is either missing when it is needed or present when it is not.


Download Project Here

Download the CubeMX project files and HAL source code — all tested on real hardware.

If you find this useful, you can support us with a Donation as well.



About the Author
Arun Rawat
Arun Rawat
Embedded Systems Engineer · Founder, ControllersTech

Arun is an embedded systems engineer with 10+ years of experience in STM32, ESP32, and AVR microcontrollers. He created ControllersTech to share practical tutorials on embedded software, HAL drivers, RTOS, and hardware design — grounded in real industrial automation experience.

Subscribe
Notify of

2 Comments
Newest
Oldest Most Voted
Slobodan Petricevic
29 days ago

Thank you for good tutorial, works out of the box!

×

Don’t Miss Future STM32 Tutorials

Join thousands of developers getting free guides, code examples, and updates.