STM32f407 + ILI9341 + LCD 2.4 inch Tearing

Description

Hi everyone,
I am experiencing tearing artifacts on my SPI TFT display (ILI9341) when using LVGL. Even after trying multiple SPI speeds from 21 MHz down to 5.25 MHz, the screen still shows tearing when updating UI elements.

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

  • MCU: STM32F407
  • Display: ILI9341 240×320 SPI
  • Compiler/IDE: STM32CubeIDE with HAL
  • **LVGL version: branch (8.3.x)

What do you want to achieve?

Any suggestion to do expirement and to find down the root cause and fix it.

What have you tried so far?

  • Adjusted SPI clock speed:
  • 21 MHz (SPI prescaler = 2) → tearing occurs
  • 10.5 MHz (SPI prescaler = 4) → still tearing
  • 5.25 MHz (SPI prescaler = 8) → tearing reduced but still visible
  • Used double buffering in LVGL with lv_disp_draw_buf_init.
  • Called lv_disp_flush_ready() only after the SPI transfer completes.

Code to reproduce

static void tft_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
  if(area->x2 < 0 || area->y2 < 0 || area->x1 > (TFT_HOR_RES  - 1) || area->y1 > (TFT_VER_RES  - 1)) {
		lv_disp_flush_ready(drv);
		return;
	}

	/*Return if the area is out the screen*/
	if(area->x2 < 0) return;
	if(area->y2 < 0) return;
	if(area->x1 > TFT_HOR_RES - 1) return;
	if(area->y1 > TFT_VER_RES - 1) return;

	/*Truncate the area to the screen*/
	int32_t act_x1 = area->x1 < 0 ? 0 : area->x1;
	int32_t act_y1 = area->y1 < 0 ? 0 : area->y1;
	int32_t act_x2 = area->x2 > TFT_HOR_RES - 1 ? TFT_HOR_RES - 1 : area->x2;
	int32_t act_y2 = area->y2 > TFT_VER_RES - 1 ? TFT_VER_RES - 1 : area->y2;

	lv_coord_t w = (area->x2 - area->x1) + 1;

	lcd_set_display_area(act_x1,act_x2,act_y1,act_y2);
	uint32_t len = (act_x2 - act_x1 + 1) * 2ul;

	lcd_send_cmd_mem_write();
	for(uint32_t y = act_y1; y <= act_y2; y++) {
		lcd_write((uint8_t*)color_p, len);
		color_p += w;
	}

	lv_disp_flush_ready(&disp_drv);
}

void lcd_write(uint8_t *buffer, uint32_t length)
{
	// Modify SPI data size to 16 bits
	__HAL_SPI_DISABLE(&lcd_spi_handle);
	SET_SPI_16BIT_MODE(&lcd_spi_handle);
	__HAL_SPI_ENABLE(&lcd_spi_handle);

    // Transmit 16-bit data
    LCD_CS_LOW();
	uint16_t *data_ptr = (uint16_t *)buffer;

	while (length) {
		// Wait until TX buffer is empty
		while (!(lcd_spi_handle.Instance->SR & SPI_SR_TXE));
		// Send 16-bit data
		lcd_spi_handle.Instance->DR = *data_ptr++;
		length -= sizeof(uint16_t);
		// Wait until transmission is complete
		while (lcd_spi_handle.Instance->SR & SPI_SR_BSY);
	}
	
    LCD_CS_HIGH();
	
	// Restore back to 8-bit mode
	__HAL_SPI_DISABLE(&lcd_spi_handle);
	SET_SPI_8BIT_MODE(&lcd_spi_handle);
	__HAL_SPI_ENABLE(&lcd_spi_handle);
}

Screenshot and/or video

Thank you and appreciate your time
Thinh

I have found out the issue and fixed.
Just dropping the info for references.

:memo: Root Cause

The issue came from my lcd_write() implementation.

According to the STM32 reference manual, the correct SPI transmit sequence is:

  1. Wait until TXE == 1 (transmit buffer empty).
  2. Write the next word into DR.
  3. After the final word only, wait until both TXE == 1 and BSY == 0 (to ensure the shift register is completely empty).

In my original code, I was checking BSY after every word, not just at the end.

  • This inserted idle gaps between every transmitted pixel.
  • The LCD received a discontinuous data stream, which caused the visible “shifting pixels” on screen.

:white_check_mark: Fix

Move the BSY wait out of the loop and perform it once after the last word is sent.
This keeps pixel data streaming continuously over SPI, and ensures proper alignment with the LCD controller.

void lcd_write(uint8_t *buffer, uint32_t length)
{
    // Switch SPI to 16-bit mode
    __HAL_SPI_DISABLE(&lcd_spi_handle);
    SET_SPI_16BIT_MODE(&lcd_spi_handle);
    __HAL_SPI_ENABLE(&lcd_spi_handle);

    LCD_CS_LOW();

    uint16_t *data_ptr = (uint16_t *)buffer;
    uint32_t count = length / 2;   // number of 16-bit words

    for(uint32_t i = 0; i < count; i++) {
        // Wait until TX buffer empty
        while(!(lcd_spi_handle.Instance->SR & SPI_SR_TXE));

        // Write next 16-bit pixel
        lcd_spi_handle.Instance->DR = *data_ptr++;
    }

    // Wait for last transmission to fully complete
    while(!(lcd_spi_handle.Instance->SR & SPI_SR_TXE));
    while(lcd_spi_handle.Instance->SR & SPI_SR_BSY);

    LCD_CS_HIGH();

    // Restore back to 8-bit mode
    __HAL_SPI_DISABLE(&lcd_spi_handle);
    SET_SPI_8BIT_MODE(&lcd_spi_handle);
    __HAL_SPI_ENABLE(&lcd_spi_handle);
}

My full code can be seen here

Thanks and close this question