Cross scanning on ST7789 causes tearing

What do you want to achieve?

I want to use a ST7789P3 320x240 LCD tearing free. It is connected to a Nucleo H563ZI board via SPI. A complete SPI transfer of the full buffer takes 9.7 ms (125 MHz SPI. I know this is unspecified but it is working stable. This is not the cause of my issues).

When i have my display in 0 degrees rotation it works fine and no tearing is visible. When i use

	lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);

I start to get tearing. This tearing is a diagonal line. I think the issue is that my LCD reads its buffer row by row (short edge) but lvgl sends its buffer column by column (long edge).

Is there a way to tell lvgl to send its buffer row by row instead of column by column?

What have you tried so far?

I tried different things with

	lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);

and manually changing the LV_LCD_CMD_SET_ADDRESS_MODE settings but they don’t solve my issue.

I also tried some configurations of

lv_draw_sw_rotate(...);

but also without success. Either the image is completely scrambled or tilted in weird angles.

Environment

  • STM32 H563ZI Nucleo
  • 9.3 and 9.4

Hi! Welcome to the community.

Could you share you flush callback function?

And also are you handling any rotation on the ST7789 itself?

A minimal code example that reproduces the issue would be helpful.

also why are you updating the entire display? You should be setting the frame buffer size to be 1/10th the size.

width * height * bytes per pixel / 10

Rotation is typically handled by the hardware and all setting it in LVGL does is it flip flops the width and height and it also maps the touch input properly. It doesn’t actually “rotate” the data seen in the frame buffer. That is up to the display to do. If you are experiencing an issue with anomalies showing up I would review the commands and the parameters that are being sent to the display to do the rotation.

The ST7789 display is a fairly common display IC so locating the issue should be pretty easy to do. Just need you to provide a minimum code example so we can take a look to track down where the issue is.

Correct me if i am wrong but having 2 full buffers should yield the best performance since i can always send one buffer out (also just the updated parts) while rendering the new one. I would expect the rendering process to also be a bit more efficient (compared to partial buffers) as only the dirty flagged objects will get re-rendered. Again: correct me if my assumptions/understanding is wrong.

Regarding the touch part: i am using an encoder input device. No touch whatsoever.

This is btw how my problem looks like
LVGL_problem
You can see the constant diagonal tearing line that occurs because i am sending data in rows but the display reads them in columns (or vice versa). Since i am synced on the TE signal the tearing is always at the same position.

This is my setup reduced to the relevant bits:


lv_color_t *buf1 = NULL;
	lv_color_t *buf1 = NULL;
	lv_color_t *buf2 = NULL;

	uint32_t buf_size = LCD_H_RES * LCD_V_RES * 2;

	buf1 = lv_malloc(buf_size);
	if (buf1 == NULL) {
		LV_LOG_ERROR("display draw buffer malloc failed");
		return 1;
	}

	buf2 = lv_malloc(buf_size);
	if (buf2 == NULL) {
		LV_LOG_ERROR("display buffer malloc failed");
		lv_free(buf1);
		return 1;
	}
	/* Create the LVGL display object and the LCD display driver */
	display = lv_st7789_create(LCD_H_RES, LCD_V_RES,
	LV_LCD_FLAG_NONE, lcd_send_cmd, lcd_send_color);
	lv_st7789_set_invert(display, true);
	lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
	lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565_SWAPPED);

	// Enable TE output
	uint8_t txData[3] = {0};
        uint8_t cmdbuf = LV_LCD_CMD_SET_TEAR_ON;
	lcd_send_cmd(display, &cmdbuf, 1, txData, 1);

	lv_display_set_buffers(display, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_FULL);

Later i want to change LV_DISPLAY_RENDER_MODE_FULL to LV_DISPLAY_RENDER_MODE_DIRECT. For no i have FULL to test the drawing for full frame changes (that will occur in my design). Once this works i will (probably) change it back to DIRECT to reduce the required SPI transfers.

my flush callback are not the default ones as i use lv_st7789_create

lcd_send_cmd

static void lcd_send_cmd(lv_display_t *disp, const uint8_t *cmd,
		size_t cmd_size, const uint8_t *param, size_t param_size) {

	spi_handle->Init.DataSize =
	SPI_DATASIZE_8BIT;
	HAL_SPI_Init(spi_handle);
	/* DCX low (command) */
	HAL_GPIO_WritePin(dcx_port, dcx_pin, GPIO_PIN_RESET);
	/* CS low */
	HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_RESET);
	/* send command */
	if (HAL_SPI_Transmit(spi_handle, cmd, cmd_size, HAL_MAX_DELAY) == HAL_OK) {
		/* DCX high (data) */
		HAL_GPIO_WritePin(dcx_port, dcx_pin, GPIO_PIN_SET);
		/* for short data blocks we use polling transfer */
		HAL_SPI_Transmit(spi_handle, (uint8_t*) param, (uint16_t) param_size,
		HAL_MAX_DELAY);
		/* CS high */
		HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET);
	}
}
static void lcd_send_color(lv_display_t *disp, const uint8_t *cmd,
		size_t cmd_size, uint8_t *param, size_t param_size) {

	// primitive waiting for TE edge
	GPIO_PinState state = GPIO_PIN_RESET;
	while (state != GPIO_PIN_SET) {
		state = HAL_GPIO_ReadPin(D8_TE_GPIO_Port, D8_TE_Pin);
	}
	while (state != GPIO_PIN_RESET) {
		state = HAL_GPIO_ReadPin(D8_TE_GPIO_Port, D8_TE_Pin);
	}

	spi_handle->Init.DataSize = SPI_DATASIZE_8BIT;
	HAL_SPI_Init(spi_handle);
	HAL_GPIO_WritePin(dcx_port, dcx_pin, GPIO_PIN_RESET);
	/* CS low */
	HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_RESET);
	/* send command */
	if (HAL_SPI_Transmit(spi_handle, cmd, cmd_size, HAL_MAX_DELAY) == HAL_OK) {
		/* DCX high (data) */
		HAL_GPIO_WritePin(dcx_port, dcx_pin, GPIO_PIN_SET);
		/* for color data use DMA transfer */
		spi_handle->Init.DataSize = SPI_DATASIZE_8BIT;
		HAL_SPI_Init(spi_handle);
		HAL_SPI_Transmit_DMA(spi_handle, param, UINT16_MAX);
		/* NOTE: CS will be reset in the transfer ready callback */
	}
}

I have an implementation that handles large SPI DMA transfers as STM32 only allows 65535 bytes to be transferred in a single transfer (changing it to 16bit lengths would help but this seems very buggy in STMs DMA implementation). I removed this part for this example.

I am not allowed to put >2 embedded files. Thus i make this as a second reply:

i did a different test where i just switch between a green and red rectangle
with no rotation everything is fine
no_tearing_red_green

but if i just make the change of adding
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
i get tearing. I make no other changes with the code or hardware.
tearing

Both gifs might have some rolling shutter effects due to my camera. But with my eye i can clearly see the diagonal line where the tearing occurs.

I also made a slow motion video (240fps) of a red to green transfer in which you can clearly see the drawing direction of the screen: left to right, top to bottom
but lvgl outputs its buffer reversed: top to bottom, left to right

which results in a diagonal tearing line

slowmowCutReduced

Is this known behaviour? What can i do to remove the diagonal tear?

I now tried some stuff regarding the MADCTL register (that gets changed when using the
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
command).
For my demo i have two full screen rectangles with red and green color and i just change the opacity of them to alternate the colors to generate tearing. I now added a label and simply iterated through all possible combinations of MADCTL and displayed the current MADCTL value:

uint8_t count = 0;
uint8_t txData[1];
txData[0] = 0;
cmdbuf = LV_LCD_CMD_SET_ADDRESS_MODE;
lcd_send_cmd(display, &cmdbuf, 1, txData, 1);

for (;;) {
	lv_timer_handler();
	if (HAL_GetTick() - millisNow > 50) {
         // Changing the opacity of the rectangles to alternate colors
		if (state) {
			lv_obj_set_style_bg_opa(rect_2, 0, 0);
			lv_obj_set_style_bg_opa(rect_1, 255, 0);
		} else {
			lv_obj_set_style_bg_opa(rect_2, 255, 0);
			lv_obj_set_style_bg_opa(rect_1, 0, 0);
		}
		state = !state;

          // updating, sending and displaying MADCTL register
		count++;
		if (count % 10 == 0) {
			txData[0]++;
			lcd_send_cmd(display, &cmdbuf, 1, txData, 1);
			lv_label_set_text_fmt(label, "val: %d", txData[0]);
		}

		millisNow = HAL_GetTick();
	}
}

With this i get a 20Hz tearing and a 2 Hz increase of MADCTL (since the 2 LSB are unused its pretty much a .5 Hz update). With this i can quickly check all possible MADCTL values within ~2 minutes.

There was no value where the display has the correct orientation and no tearing. When D4 (ML) changed the diagonal tear was mirrored however (which makes sense, as the display no updates in the other direction).