Mirrored, Distorted & Partial Image on ST7796 TFT with Pi Pico 2 (RP2350)

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()
  • 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)