Hi There! I am trying to get my LVGL/Display project setup and have some problems while doing so. Therefore I am kindly asking for help.
Description
I am trying to get an ST7796 based (320x480) TFT Display to work with a Pi Pico 2 (RP2350) via SPI. The resulting on-screen image is a) distorted and b) mirrored and c) only rendered partially. Please see the photos below.
As far as I understand my display controllers should support hardware rotation via setting the corresponding bits in the MADCTL register.
What MCU/Processor/Board and compiler are you using?
- Official Raspberry Pi Pico 2 with RP2350 MCU
- ST7796 (and ST7789) TFT Display connected via SPI
- Official Pi Pico SDK & Toolchain on Linux
- LVGL included driver for the display
What do you want to achieve?
Properly setup LVGL on mentioned hardware. Specifically fix broken & partial rendering, rotation and mirroring issues.
What have you tried so far?
-
Checked and measured physical hardware connections and tested different Pi Pico 2’s and another display
-
Tried with another ST7789 TFT Display => shows the same issue
-
Tried this project => works!
GitHub - aeroreyna/picopi_sdk_tft_st7796_example: A simple initial implementation of a simple driver for a tft screen with the st7796 controller. · GitHub -
Customized the init command sequence (in lvgl/src/drivers/display/st7796/lv_st7796.c) with the one used in above project by ‘aeroreyna’ => no effect
-
Swapping HRES and VRES => Different behaviour observed (see photos and comments below)
-
Tried to set LV_LCD_FLAG_BGR, LV_LCD_FLAG_MIRROR_X, LV_LCD_FLAG_MIRROR_Y in lv_st7796_create() => no visible effect
-
Manually set MADCTL registers via direct SPI writes, my_lcd_send_cmd() function and hardcoding within LVGL driver* => no visible effect
- *in lib/lvgl/src/drivers/display/lcd/lv_lcd_generic_mipi.c:
init() | set_mirror() | set_swap_xy() | set_rotation()
- *in lib/lvgl/src/drivers/display/lcd/lv_lcd_generic_mipi.c:
-
Additionally called lv_lcd_generic_mipi_set_address_mode() with all possible combinations of params => no visible effect
-
Calling lv_display_set_rotation() with 0, 90, 180, 270 degrees. 0&180 and 90&270 lead to different images (see photos below)
-
Tried all three rendering modes => no effect
-
Tried different buffer sizes (for PARTIAL mode) and allocation on stack and/or heap => no difference
-
Manually re-setting resolution via lv_display_set_physical_resolution() and lv_display_set_resolution()
Code to reproduce
// General includes
#include <pico/time.h>
#include <pico/types.h>
#include <src/display/lv_display.h>
#include <src/drivers/display/lcd/lv_lcd_generic_mipi.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "hardware/gpio.h"
#include "hardware/spi.h"
#include "lib/lvgl/lvgl.h"
#define TFT_HOR_RES 320
#define TFT_VER_RES 480
#define TFT_SPI_MOSI_GPIO 11
#define TFT_SPI_MISO_GPIO 12
#define TFT_SPI_CS_GPIO 13
#define TFT_SPI_SCK_GPIO 10
#define TFT_DC_GPIO 18
#define TFT_LED_GPIO 17
#define TFT_RESET_GPIO 16
// Size of lvgl pixelbuffer for full mode
#define my_pixelbuffer_size (TFT_HOR_RES * TFT_VER_RES * 2)
// MCUs SPI interface
spi_inst_t* my_spi_tft = spi1;
// Forward Declarations
uint32_t my_ms_since_boot();
void my_lvgl_flush_cb(lv_display_t* _display, const lv_area_t* _area, uint8_t* _px_map);
void my_lcd_send_cmd(lv_display_t *disp, const uint8_t *cmd, size_t cmd_size, const uint8_t *param, size_t param_size);
void my_lcd_send_color(lv_display_t *disp, const uint8_t *cmd, size_t cmd_size, uint8_t *param, size_t param_size);
int main(void)
{
/* General Init */
stdio_init_all();
printf("\nTFT Display Test with LVGL\n");
/* Init SPI */
uint volatile brate = spi_init(my_spi_tft, 24 * 1000 * 1000);
//spi_set_format(my_spi_tft, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
gpio_set_function(TFT_SPI_SCK_GPIO, GPIO_FUNC_SPI);
gpio_set_function(TFT_SPI_MOSI_GPIO, GPIO_FUNC_SPI);
gpio_set_function(TFT_SPI_MISO_GPIO, GPIO_FUNC_SPI);
// SPI PIN: Cs: LOW->select | HIGH->deselect
gpio_init(TFT_SPI_CS_GPIO);
gpio_set_dir(TFT_SPI_CS_GPIO, GPIO_OUT);
gpio_put(TFT_SPI_CS_GPIO, 1); // Default: Deselect
// Other Pins: DC: LOW->CMD | HIGH->DATA
gpio_init(TFT_DC_GPIO);
gpio_set_dir(TFT_DC_GPIO, GPIO_OUT);
gpio_put(TFT_DC_GPIO, 1);
// Other Pins: Reset: LOW->INIT Reset | HIGH->Back to op
gpio_init(TFT_RESET_GPIO);
gpio_set_dir(TFT_RESET_GPIO, GPIO_OUT);
gpio_put(TFT_RESET_GPIO, 1);
// Other Pins: LED/Backlight
gpio_init(TFT_LED_GPIO);
gpio_set_dir(TFT_LED_GPIO, GPIO_OUT);
gpio_put(TFT_LED_GPIO, 1);
// Hardware Reset of TFT
gpio_put(TFT_RESET_GPIO, 1);
sleep_ms(5);
gpio_put(TFT_RESET_GPIO, 0); // Hold LOW for reset
sleep_ms(15);
gpio_put(TFT_RESET_GPIO, 1); // Release reset
sleep_ms(15);
/* Init LVGL */
lv_init();
/* Set millisecond-based tick source for LVGL so that it can track time. */
lv_tick_set_cb(my_ms_since_boot);
// Remark: No combination of LV_LCD_FLAG_BGR, LV_LCD_FLAG_MIRROR_X,
// Remark: LV_LCD_FLAG_MIRROR_Y have a visible effect
lv_display_t* my_lvgl_display = lv_st7796_create(
TFT_HOR_RES,
TFT_VER_RES,
LV_LCD_FLAG_NONE,
my_lcd_send_cmd,
my_lcd_send_color);
//lv_display_set_physical_resolution(my_lvgl_display, TFT_VER_RES, TFT_HOR_RES);
//lv_display_set_resolution(my_lvgl_display, TFT_VER_RES, TFT_HOR_RES);
// Remark: Tried all combinations of params here, no visible effect
//lv_lcd_generic_mipi_set_address_mode(my_lvgl_display, 0, 0, 1, 0);
// Remark: (ROT90 & ROT270) Result in somewhat improved image
// Remark: (ROT0 & ROT180) Result in more degraded image
lv_display_set_rotation(my_lvgl_display, LV_DISPLAY_ROTATION_0);
lv_display_set_color_format(my_lvgl_display, LV_COLOR_FORMAT_RGB565);
// Remark: Dynamic allocation or partial rendering not working either
static uint8_t my_pixelbuf[TFT_HOR_RES * TFT_VER_RES * 2];
// Remark: None of the 3 render modes work
lv_display_set_buffers(
my_lvgl_display,
&my_pixelbuf,
NULL,
my_pixelbuffer_size,
LV_DISPLAY_RENDER_MODE_FULL
);
/* Draw test ui */
lv_obj_t* label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Test Label: F 3!");
lv_obj_set_style_text_font(label, &lv_font_montserrat_30, 0);
lv_obj_center(label);
/* Main Loop */
for (;;) {
lv_timer_handler();
sleep_ms(5);
};
return(0);
}
/*
Send short command to the LCD.
*/
void my_lcd_send_cmd(lv_display_t* display, const uint8_t* cmd, size_t cmd_size, const uint8_t* param, size_t param_size)
{
if (*cmd == 0x36) {
printf("MADCTL: %d\n", *param);
}
// Select TFT on SPI & Set command mode
asm volatile("nop \n nop \n nop");
gpio_put(TFT_SPI_CS_GPIO, 0); // LOW -> select
gpio_put(TFT_DC_GPIO, 0); // LOW -> command mode
asm volatile("nop \n nop \n nop");
// Write Command
spi_write_blocking(my_spi_tft, cmd, 1);
// Deselect TFT on SPI
asm volatile("nop \n nop \n nop");
gpio_put(TFT_SPI_CS_GPIO, 1); // HIGH -> deselect
asm volatile("nop \n nop \n nop");
}
/*
Send large array of pixel data to the LCD controller.
*/
void my_lcd_send_color(lv_display_t* display, const uint8_t* cmd, size_t cmd_size, uint8_t* param, size_t param_size)
{
lv_display_rotation_t rotation = lv_display_get_rotation(display);
// Select TFT on SPI & Set command mode
asm volatile("nop \n nop \n nop");
gpio_put(TFT_SPI_CS_GPIO, 0); // LOW -> select
gpio_put(TFT_DC_GPIO, 0); // LOW -> command mode
asm volatile("nop \n nop \n nop");
// Write Command
spi_write_blocking(my_spi_tft, cmd, 1);
// Set data mode
asm volatile("nop \n nop \n nop");
gpio_put(TFT_DC_GPIO, 1); // HIGH -> data mode
asm volatile("nop \n nop \n nop");
// Write Data
spi_write_blocking(my_spi_tft, param, param_size);
// Deselect TFT on SPI
asm volatile("nop \n nop \n nop");
gpio_put(TFT_SPI_CS_GPIO, 1); // HIGH -> deselect
asm volatile("nop \n nop \n nop");
// Inform LVGL of finished flush operation
lv_display_flush_ready(display); // Notify LVGL that flush is complete
}
/*
Return ms since MCU boot
*/
uint32_t my_ms_since_boot()
{
uint32_t ms = to_ms_since_boot(get_absolute_time());
return ms;
}
Screenshot and/or video
Picture of the issue with rotation set to 0 or 180 degrees (HRES=320, VRES=480):
Picture of the issue with rotation set to 90 or 270 degrees (or HRES and VRES swapped manually). Note that the label is properly centered within the white-backgroud part of the display.:
- With my “high-precision” ruler I “measured”, that the missing/non-rendered/noisy part is indeed 1/3 of the height of the display (=160px)

