Massive visual glitches when pressing buttons on LVGL>STM32H7+LTDC>RGB>ST7789V

Hello, I am greatly struggling to set up LVGL on a STM32H7+LTDC > RGB > 240x320 TFT-LCD setup.
The library seems set-up, but animations look awful. The following short shows the problem: https://www.youtube.com/shorts/l1SwMOpma10

This looks like worse than very bad tearing, it looks extremely buggy, but I don’t doubt this is an issue with my setup.
I am outputting via LTDC to a ST7789V, which then drives the display. Ideally I would bypass the ST7789V RAM and output to the screen directly (via the shift register option), but I can’t seem to get that option to work.
Therefore there’s 2 potential sources of tearing, the “usual” one of drawing to a buffer that’s being read, and the sync difference between the RGB writing to the display driver RAM, and the driver writing its RAM to the LCD. I don’t know how to get rid of the latter.

As a reference, I tested the following:

	LTDC->SRCR |= 2; // Vertical blanking reload:shadow registers are reloaded during vblank (prevents tearing)
	int colorCounter = 0;
	while (true) {
		uint16_t* currentFrame = colorCounter % 2 == 0 ? lcdFrame1Origin : lcdFrame2Origin;
		LTDC_Layer1->CFBAR = (uint32_t)(colorCounter % 2 == 1 ? lcdFrame1Origin : lcdFrame2Origin);
		HAL_Delay(200);
		std::fill(currentFrame, currentFrame+240*320, (colorCounter%3)==0 ? 0b11111'000000'00000 : (colorCounter%3)==1? 0b00000'111111'00000 : 0b00000'000000'11111);
		colorCounter++;
	}

Here I’m using 2 buffers and swapping to the new one after writing to it, at VSync.
When doing that only the aforementionned driver source of tearing remains, as can be seen here: https://www.youtube.com/shorts/ocmkwgcTIfQ

There is some tearing (diagonal, as a result of writing to the driver line-first and it reading line-first…). But as can be seen in the video, it is ‘usual’ tearing, one line accross the screen - nothing that would cause what can be seen with the buttons.

So it must mean I am not doing the double buffering properly with LVGL. (same results are observed on single buffer with LVGL)
This is what I do:

void my_flush_cb(lv_display_t * disp, const lv_area_t * area, unsigned char* color_p) {
	UART7_Transmit(std::to_string((uint32_t)(color_p))+"\n");
	LTDC_Layer1->CFBAR = (uint32_t)color_p;
	lv_display_flush_ready(disp); // Empty flush callback. We dispatch via LTDC so the buffer is always available
}

void lv_example_button_1(void)
{
    lv_obj_t * label;

    lv_obj_t * btn1 = lv_button_create(lv_screen_active());
    lv_obj_add_event_cb(btn1, event_handler, LV_EVENT_ALL, NULL);
    lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -40);
    lv_obj_remove_flag(btn1, LV_OBJ_FLAG_PRESS_LOCK);
    lv_obj_set_size(btn1, 160, 80);

    label = lv_label_create(btn1);
    lv_label_set_text(label, "Button");
    lv_obj_center(label);

    lv_obj_t * btn2 = lv_button_create(lv_screen_active());
    lv_obj_add_event_cb(btn2, event_handler, LV_EVENT_ALL, NULL);
    lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 40);
    lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE);
    //lv_obj_set_height(btn2, LV_SIZE_CONTENT);
    lv_obj_set_size(btn2, 160, 80);

    label = lv_label_create(btn2);
    lv_label_set_text(label, "Toggle");
    lv_obj_center(label);
}

[In main]
	LTDC->SRCR |= 2; // Vertical blanking reload:shadow registers are reloaded during vblank (prevents tearing)

	lv_init();

	lv_display_t * disp = lv_display_create(320, 240); /*Basic initialization with horizontal and vertical resolution in pixels*/
	lv_display_set_flush_cb(disp, my_flush_cb); /*Set a flush callback to draw to the display*/
	lv_display_set_buffers(disp, lcdFrame1Origin, lcdFrame2Origin, lcdFrameByteLength, LV_DISPLAY_RENDER_MODE_DIRECT); /*Set an initialized buffer*/

	// Change the active screen's background color
	lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(0x003a57), LV_PART_MAIN);
	lv_obj_set_style_text_color(lv_screen_active(), lv_color_hex(0xffffff), LV_PART_MAIN);

	lv_example_button_1();

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

Starting code & example taken from Display interface — LVGL documentation

In flush_cb I set LTDC buffer to reset to the color_p provided by the callback, which should be the full buffer given the selection of direct mode, then call the flush ready func. The change of buffer will happen at next VSync, which could be an issue, but I also don’t expect LVGL to call the flush_cb so soon given LV_DEF_REFR_PERIOD is 33 (default) in lv_conf.

I’m out of ideas here, I’ve been fighting with this LCD for a week and any help would be greatly appreciated.
And while this isn’t LVGL related, if anyone might know what prerequisites there would be for using the “Shift register” aka without RAM mode on ST7xxx / ILIxxxx drivers so I can get rid of the tearing introduced by the driver, I’ll gladly listen. When I turn it on, regardless of row-column swapping or other thing that may make the signal “not match” the LCD, the screen just stops updating, whereas things work just fine when it comes to writing the RGB signal to RAM and letting the driver write its RAM to the display. Thanks.

is natively portrait, your config is landscape… How is LTDC clock?

The LTDC clock assumes landscape. I set the column/row swap option in the driver. This results in the diagonal tearing in the ‘tearing reference’ since the MCU fees the driver line by line and the driver feeds the display column by column.

But even without the column/row swap there’s still tearing occuring from the lack of synchronization between MCU>driver and driver>LCD, just horizontal i.e “traditional” tearing. To truly get rid of that tearing I’d need to either get the shift register i.e bypass GRAM mode to work, or synchronize the MCU>driver & driver>LCD comms, which I don’t see how to do given that the TE line (signal line available from the driver to the MCU to know when the vsync of the driver>LCD is)… isn’t broken out.

LTDC config:

can be simple eliminated do it blanking sync, but on slow refresh stil exist.
Other think is diagonal tearing = CANT be eliminated at all.

And you dont reply how clock or refresh rate is setup for LTDC.

I don’t understand. Blanking sync ? The tearing is between the arrival and departure of data from/to the driver, and I have no way to know the timing of driver>LCD transfers.
The configured clock on the LTDC is 20MHz. Problem is observed at lower frequencies too. Other clock related parameters (hsync vsync) in the other above picture.

by pdf max clock fr 16 or 18 bit rgb is 8,333MHz not 20
and ST7789V is most simple controller not designed to your idea. You can use it but with TE signal and portrait write data…

Right, but as pointed out earlier, the tearing happens regardless of clock and row-column swapping due to lack of sync with the driver>LCD comms. It’s just when there is no row-column swap, the tearing looks more conventional i.e a horizontal line rather than diagonal. And the TE line is not broken out.

Also, I don’t think that tearing explains the crazy glitching I see when pressing a button.

I had a similar problem, and I couldn’t solve it.

Hi,
what is the clock of LTDC?

Hello, in the provided video the LTDC clock is 20MHz, but the same glitches occur at much lower frequencies e.g 2 or 4MHz.

you are using an RGB display. what you need to do is have 2 frame buffers and the frame buffers need to reside in DMA memory. You need to call lv_flush_ready one time when a buffer has been flushed and then once the next buffer is ready to be flushed you need to stall the program until the end of a vertical refresh has finished and swap the buffers. once the first vertical refresh has finished for a buffer that was just flushed that is when you call lv_flush_ready. once again that function can only be called a single time after each buffer has been flushed.

IDK if that makes any sense…

The tearing is occurring because of one of 2 problems. The first is using only a single buffer in DMA memory is allowing the buffer to be rendered to while it is being sent. OR the amount of time it is taking for LVGL to render new information to the buffer is taking too long and that is causing the tearing to occur.

using 2 frame buffers if they do not reside in DMA memory is not going to do anything because LVGL is not able to render to one buffer while the other is transferring.

Now I believe with the STM32 all memory is DMA capable and depending on what you are using for the interface to write the data is going to dictate it being sent without the use of the CPU. If you are using the correct portion of the STM32 SDL to be able to send the data using a DMA transfer then your tearing issue is because of a synchronization caused by when lv_flush_ready is called and also the number of times it is being called.

You need to register a callback for the end of a vertical sync and keep track of the buffer that has finished being transferred when that callback is called. You need to have a marker to tell you if you have called lv_task_handler for the buffer that has just transferred since it was passed to your flush callback. Remember only call that function one time for each time a buffer gets passed to your flush callback.