LVGL Porting issue with ST7789 display


I have followed the display porting guide in LVGL Docs. And I have successfully run the keyboard example lv_ex_keyboard_1. However, it is not working correctly. It shows some random color pixels when I press the keys and the cursor also distorted. I’m not sure what I’m doing wrong. Any help would be greatly appreciated.

What MCU/Processor/Board and compiler are you using?

Kendryte K210 kd233 development board
Kendtryte FreeRTOS
Display driver IC ST7789

What LVGL version are you using?

7.1 Dev

What do you want to achieve?

Run without any glitches on the screen.

Code to reproduce/What have I tried so far?

Below are my flush_cb callback function and driver initializing snippet,

void lcd_draw_picture_by_half(uint16_t x1, uint16_t y1, uint16_t width,
                              uint16_t height, uint16_t *ptr) {
  lcd_set_area(x1, y1, x1 + width - 1, y1 + height - 1);
  tft_write_half(ptr, width * height);

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
                   lv_color_t *color_p) {
  lcd_draw_picture_by_half(area->x1, area->y1, (area->x2 - area->x1 + 1),
                           (area->y2 - area->y1 + 1), color_p);
  lv_disp_flush_ready(disp); /* Indicate you are ready with the flushing*/

  /*A static or global variable to store the buffers*/
  static lv_disp_buf_t disp_buf;

  /*Static or global buffer(s). The second buffer is optional*/
  static lv_color_t buf_1[LV_HOR_RES_MAX * 10];
  static lv_color_t buf_2[LV_HOR_RES_MAX * 10];

  /*Initialize `disp_buf` with the buffer(s) */
  lv_disp_buf_init(&disp_buf, buf_1, buf_2, LV_HOR_RES_MAX * 10);

  lv_disp_drv_t disp_drv;            /*Descriptor of a display driver*/
  lv_disp_drv_init(&disp_drv);       /*Basic initialization*/
  disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
  disp_drv.buffer = &disp_buf;       /*Assign the buffer to the display*/
  lv_disp_drv_register(&disp_drv);   /*Finally register the driver*/

Display HAL code .h,

#ifndef _NT35310_H_
#define _NT35310_H_
#include <stdint.h>

/* clang-format off */
#define NO_OPERATION            0x00
#define SOFTWARE_RESET          0x01
#define READ_ID                 0x04
#define READ_STATUS             0x09
#define READ_POWER_MODE         0x0A
#define READ_MADCTL             0x0B
#define READ_PIXEL_FORMAT       0x0C
#define READ_IMAGE_FORMAT       0x0D
#define READ_SIGNAL_MODE        0x0E
#define SLEEP_ON                0x10
#define SLEEP_OFF               0x11
#define PARTIAL_DISPALY_ON      0x12
#define NORMAL_DISPALY_ON       0x13
#define INVERSION_DISPALY_ON    0x21
#define GAMMA_SET               0x26
#define DISPALY_OFF             0x28
#define DISPALY_ON              0x29
#define MEMORY_WRITE            0x2C
#define COLOR_SET               0x2D
#define MEMORY_READ             0x2E
#define PARTIAL_AREA            0x30
#define TEAR_EFFECT_LINE_OFF    0x34
#define TEAR_EFFECT_LINE_ON     0x35
#define MEMORY_ACCESS_CTL       0x36
#define VERTICAL_SCROL_S_ADD    0x37
#define IDLE_MODE_OFF           0x38
#define IDLE_MODE_ON            0x39
#define PIXEL_FORMAT_SET        0x3A
#define SET_TEAR_SCANLINE       0x44
#define GET_SCANLINE            0x45
#define WRITE_BRIGHTNESS        0x51
#define READ_BRIGHTNESS         0x52
#define WRITE_CTRL_DISPALY      0x53
#define READ_CTRL_DISPALY       0x54
#define WRITE_BRIGHTNESS_CTL    0x55
#define READ_BRIGHTNESS_CTL     0x56
#define READ_MIN_BRIGHTNESS     0x5F
#define READ_ID1                0xDA
#define READ_ID2                0xDB
#define READ_ID3                0xDC
#define RGB_IF_SIGNAL_CTL       0xB0
#define NORMAL_FRAME_CTL        0xB1
#define IDLE_FRAME_CTL          0xB2
#define PARTIAL_FRAME_CTL       0xB3
#define INVERSION_CTL           0xB4
#define BLANK_PORCH_CTL         0xB5
#define ENTRY_MODE_SET          0xB7
#define BACKLIGHT_CTL1          0xB8
#define BACKLIGHT_CTL2          0xB9
#define BACKLIGHT_CTL3          0xBA
#define BACKLIGHT_CTL4          0xBB
#define BACKLIGHT_CTL5          0xBC
#define BACKLIGHT_CTL7          0xBE
#define BACKLIGHT_CTL8          0xBF
#define POWER_CTL1              0xC0
#define POWER_CTL2              0xC1
#define VCOM_CTL1               0xC5
#define VCOM_CTL2               0xC7
#define NV_MEMORY_WRITE         0xD0
#define READ_ID4                0xD3
#define DIGITAL_GAMMA_CTL1      0xE2
#define DIGITAL_GAMMA_CTL2      0xE3
#define INTERFACE_CTL           0xF6
/* clang-format on */

void tft_hard_init(void);
void tft_write_command(uint8_t cmd);
void tft_write_byte(uint8_t* data_buf, uint32_t length);
void tft_write_half(uint16_t* data_buf, uint32_t length);
void tft_write_word(uint32_t* data_buf, uint32_t length);
void tft_fill_data(uint32_t* data_buf, uint32_t length);


Display HAL code .c,

#include <devices.h>
#include <stdio.h>
#include "project_cfg.h"
#include "jlt32009a.h"

#define SPI_HIGH_CLOCK_RATE  25000000U
#define SPI_LOW_CLOCK_RATE  3200000U
#define WAIT_CYCLE      0U

enum _instruction_length
    INSTRUCTION_LEN_16 = 16,
    INSTRUCTION_LEN_32 = 32,
} ;

enum _address_length
    ADDRESS_LEN_0 = 0,
    ADDRESS_LEN_8 = 8,
    ADDRESS_LEN_16 = 16,
    ADDRESS_LEN_32 = 32,
} ;

enum _frame_length
    FRAME_LEN_0 = 0,
    FRAME_LEN_8 = 8,
    FRAME_LEN_16 = 16,
    FRAME_LEN_32 = 32,
} ;

handle_t gio;
handle_t spi0;
handle_t spi_dfs8;
handle_t spi_dfs16;
handle_t spi_dfs32;

void init_dcx()
    gio = io_open("/dev/gpio0");
    gpio_set_drive_mode(gio, DCX_GPIONUM, GPIO_DM_OUTPUT);
    gpio_set_pin_value(gio, DCX_GPIONUM, GPIO_PV_HIGH);

void set_dcx_control()
    gpio_set_pin_value(gio, DCX_GPIONUM, GPIO_PV_LOW);

void set_dcx_data()
    gpio_set_pin_value(gio, DCX_GPIONUM, GPIO_PV_HIGH);

void spi_control_init()
    spi0 = io_open("/dev/spi0");
    spi_dfs8 = spi_get_device(spi0, SPI_MODE_0, SPI_FF_OCTAL, 1 << SPI_SLAVE_SELECT, FRAME_LEN_8);
    spi_dev_config_non_standard(spi_dfs8, INSTRUCTION_LEN_8, ADDRESS_LEN_0, WAIT_CYCLE, SPI_AITM_AS_FRAME_FORMAT);
    spi_dfs16 = spi_get_device(spi0, SPI_MODE_0, SPI_FF_OCTAL, 1 << SPI_SLAVE_SELECT, FRAME_LEN_16);
    spi_dev_config_non_standard(spi_dfs16, INSTRUCTION_LEN_16, ADDRESS_LEN_0, WAIT_CYCLE, SPI_AITM_AS_FRAME_FORMAT);
    spi_dfs32 = spi_get_device(spi0, SPI_MODE_0, SPI_FF_OCTAL, 1 << SPI_SLAVE_SELECT, FRAME_LEN_32);
    spi_dev_config_non_standard(spi_dfs32, INSTRUCTION_LEN_0, ADDRESS_LEN_32, WAIT_CYCLE, SPI_AITM_AS_FRAME_FORMAT);

    spi_dev_set_clock_rate(spi_dfs8, SPI_LOW_CLOCK_RATE);
    spi_dev_set_clock_rate(spi_dfs16, SPI_HIGH_CLOCK_RATE);
    spi_dev_set_clock_rate(spi_dfs32, SPI_HIGH_CLOCK_RATE);

void tft_hard_init(void)

void tft_write_command(uint8_t cmd)
    io_write(spi_dfs8, (const uint8_t *)(&cmd), 1);

void tft_write_byte(uint8_t* data_buf, uint32_t length)
    io_write(spi_dfs8, (const uint8_t *)(data_buf), length);

void tft_write_half(uint16_t* data_buf, uint32_t length)
    io_write(spi_dfs16, (const uint8_t *)(data_buf), length * 2);

void tft_write_word(uint32_t* data_buf, uint32_t length)
    io_write(spi_dfs32, (const uint8_t *)data_buf, length * 4);

void tft_fill_data(uint32_t* data_buf, uint32_t length)
    spi_dev_fill(spi_dfs32, 0, *data_buf, *data_buf, length - 1);


#include "lcd.h"

#include <string.h>
#include <sys/unistd.h>

#include "font.h"
#include "jlt32009a.h"

typedef struct {
  uint8_t dir;
  uint16_t width;
  uint16_t height;
} lcd_ctl_t;

static lcd_ctl_t lcd_ctl;

void lcd_init(void) {
  uint8_t data;

  // soft reset
  usleep(100 * 1000);
  // exit sleep
  usleep(100 * 1000);
  // pixel format
  data = 0x55;
  tft_write_byte(&data, 1);

void lcd_set_direction(enum lcd_dir_t dir) {
  dir |= 0x08;
  lcd_ctl.dir = dir;
  if (dir & DIR_XY_MASK) {
    lcd_ctl.width = LCD_Y_MAX - 1;
    lcd_ctl.height = LCD_X_MAX - 1;
  } else {
    lcd_ctl.width = LCD_X_MAX - 1;
    lcd_ctl.height = LCD_Y_MAX - 1;

  tft_write_byte((uint8_t*)&dir, 1);

void lcd_set_area(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
  uint8_t data[4];

  data[0] = (uint8_t)(x1 >> 8);
  data[1] = (uint8_t)(x1);
  data[2] = (uint8_t)(x2 >> 8);
  data[3] = (uint8_t)(x2);
  tft_write_byte(data, 4);
  data[0] = (uint8_t)(y1 >> 8);
  data[1] = (uint8_t)(y1);
  data[2] = (uint8_t)(y2 >> 8);
  data[3] = (uint8_t)(y2);
  tft_write_byte(data, 4);

void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color) {
  lcd_set_area(x, y, x, y);
  tft_write_half(&color, 1);

void lcd_draw_char(uint16_t x, uint16_t y, char c, uint16_t color) {
  uint8_t i, j, data;

  for (i = 0; i < 16; i++) {
    data = ascii0816[c * 16 + i];
    for (j = 0; j < 8; j++) {
      if (data & 0x80) lcd_draw_point(x + j, y, color);
      data <<= 1;

void lcd_draw_string(uint16_t x, uint16_t y, char* str, uint16_t color) {
  while (*str) {
    lcd_draw_char(x, y, *str, color);
    x += 8;

void ram_draw_string(char* str, uint32_t* ptr, uint16_t font_color,
                     uint16_t bg_color) {
  uint8_t i, j, data, *pdata;
  uint16_t width;
  uint32_t* pixel;

  width = 4 * strlen(str);
  while (*str) {
    pdata = (uint8_t*)&ascii0816[(*str) * 16];
    for (i = 0; i < 16; i++) {
      data = *pdata++;
      pixel = ptr + i * width;
      for (j = 0; j < 4; j++) {
        switch (data >> 6) {
          case 0:
            *pixel = ((uint32_t)bg_color << 16) | bg_color;
          case 1:
            *pixel = ((uint32_t)bg_color << 16) | font_color;
          case 2:
            *pixel = ((uint32_t)font_color << 16) | bg_color;
          case 3:
            *pixel = ((uint32_t)font_color << 16) | font_color;
            *pixel = 0;
        data <<= 2;
    ptr += 4;

void lcd_clear(uint16_t color) {
  uint32_t data = ((uint32_t)color << 16) | (uint32_t)color;

  lcd_set_area(0, 0, lcd_ctl.width, lcd_ctl.height);
  tft_fill_data(&data, LCD_X_MAX * LCD_Y_MAX / 2);

void lcd_draw_rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
                        uint16_t width, uint16_t color) {
  uint32_t data_buf[640];
  uint32_t* p = data_buf;
  uint32_t data = color;
  uint32_t index;

  data = (data << 16) | data;
  for (index = 0; index < 160 * width; index++) *p++ = data;

  lcd_set_area(x1, y1, x2, y1 + width - 1);
  tft_write_word(data_buf, ((x2 - x1 + 1) * width + 1) / 2);
  lcd_set_area(x1, y2 - width + 1, x2, y2);
  tft_write_word(data_buf, ((x2 - x1 + 1) * width + 1) / 2);
  lcd_set_area(x1, y1, x1 + width - 1, y2);
  tft_write_word(data_buf, ((y2 - y1 + 1) * width + 1) / 2);
  lcd_set_area(x2 - width + 1, y1, x2, y2);
  tft_write_word(data_buf, ((y2 - y1 + 1) * width + 1) / 2);

void lcd_draw_picture(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height,
                      uint32_t* ptr) {
  lcd_set_area(x1, y1, x1 + width - 1, y1 + height - 1);
  tft_write_word(ptr, width * height / 2);

Screenshot and/or video

I have uploaded a video to youtube to show you how the issue looks like,

Ok, I made a progress here :partying_face:. I got it to work with below changes (At least partially),

Added this function to jlt32009a.c and called in lcd_draw_picture_by_half (Please refer to the initial post for lcd_draw_picture_by_half function)

void tft_write_half8(uint16_t* data_buf, uint32_t length) {
  io_write(spi_dfs8, (const uint8_t*)(data_buf), length * 2);

And enabled the LV_COLOR_16_SWAP in lv_conf.h,

/* Swap the 2 bytes of RGB565 color.
 * Useful if the display has a 8 bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP   1

This resolves the problem with random color pixels that appear when I type on the keyboard. However, The fast blinking cursor is still there. And few dots are appearing parallel to the cursor if I wait for a couple of minutes.

Here is the video of the fast blinking cursor.

What could be the problem here? Any thoughts @kisvegabor?


The fast blinking cursor could because of an issue with lv_tick_inc(x). IS it possible that you call it more often then indicated by the x parameter?

Thanks @kisvegabor that was it. I was calling lv_tick_inc(x) in the while loop. Sleeping the while loop for x number of milliseconds did the trick. However, the mystery dotted line still appears. What could be the reason for that?

while (1) {
   usleep(10 * 1000); /*Sleep for 10 millisecond*/

It seems like a display driver issue.

Maybe you copy more or less pixels than required.How does your disp._flish_cb function look like?

Here is my disp.flush_cb function,

void lcd_draw_picture_by_half(uint16_t x1, uint16_t y1, uint16_t width,
                              uint16_t height, uint16_t *ptr,
                              const lv_area_t *area,
                              const lv_color_t *color_map) {
  uint32_t size = lv_area_get_width(area) * lv_area_get_height(area);
  tft_write_half8(color_map, size);

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area,
                   lv_color_t *color_p) {
  lcd_draw_picture_by_half(area->x1, area->y1, (area->x2 - area->x1 + 1),
                           (area->y2 - area->y1 + 1), (uint16_t *)color_p, area,
  lv_disp_flush_ready(disp); /* Indicate you are ready with the flushing*/

When I run the lv_ex_get_started_1 example it behaves weird too. See this video

Initially, it loads fine. but this is what happens when I tap on the button.

It looks your driver has an issue when a partial refresh happens (the first render refreshes the whole screen, so you wouldn’t see an issue there).

Thanks for the reply @embeddedt. Actually it does mess up during the initial power up as well. I used to hit reset and it refreshes just fine. see this video it shows the initial powering the board and you will see some weird horizontal stripes. I used to ignore that since reset fixes. but it is indeed an issue.

Does this mean I’m doing something wrong with flush_cb?

I think so. At the very least there’s something wrong with the way pixels are being sent to the display.

My guess is the issue is in lcd_set_area_2. May be it’s an incompatibility in how width/height is interpreted. E.g. w = x2 - x1 or w = x2 - x1 + 1. So try both version and see what happens.

Hi @kisvegabor, My current lcd_set_area_2 looks like this. Do you see any issue here?

void lcd_set_area_2(const lv_area_t* area) {
  uint8_t data[4] = {0};

  uint16_t offsetx1 = area->x1;
  uint16_t offsetx2 = area->x2;
  uint16_t offsety1 = area->y1;
  uint16_t offsety2 = area->y2;

  data[0] = (offsetx1 >> 8) & 0xFF;
  data[1] = offsetx1 & 0xFF;
  data[2] = (offsetx2 >> 8) & 0xFF;
  data[3] = offsetx2 & 0xFF;

  tft_write_byte(data, 4);

  data[0] = (offsety1 >> 8) & 0xFF;
  data[1] = offsety1 & 0xFF;
  data[2] = (offsety2 >> 8) & 0xFF;
  data[3] = offsety2 & 0xFF;

  tft_write_byte(data, 4);

Sorry for the late answer.

It seems correct. I’ve compared it with the driver in lv_port_esp32 repo but can’t see any obvious issue. Your code is very similar to this so I assume you also used this driver as a starting point.

At this point, I suggest playing with the driver without LVGL to understand where the issue is. E.g call my_disp_flush with color_p filled with a given color to draw:

  • 1 pixel
  • a short single column
  • a short single row
  • a small area

Hi @kisvegabor, Thanks for the response. Yes, you are right. I used the sep32 port driver as the starting point. And I think it is a good point that just to play with the driver itself by excluding LVGL from the equation just to make sure the driver is behaving as intended.

Right now I’m working on a different part of the project. I will test it out and let you know the result as soon as possible.