Understand and Prevent LCD Screen Tearing
What is Screen Tearing?
Screen tearing is a common and distracting visual artifact on LCD screens, especially in applications with fast-moving graphics or full-screen updates. It appears as a horizontal break or "tear" in the image, where the upper and lower portions of the screen seem misaligned, showing parts of two different frames simultaneously.
This issue occurs when the microcontroller (MCU) or graphics processor writes new frame data into the display's memory (GRAM) at the same time as the display's internal controller is reading from that same memory to refresh the screen.
To understand this, think of the display as reading a book from top to bottom, line by line. If you try to swap the book for a new one while the reader is halfway down the page, they will read the top half of the old page and the bottom half of the new one. The result is a jumbled, "torn" image.
The Root Cause: A Synchronization Mismatch
An LCD screen refreshes its image at a fixed rate, such as 60Hz (60 times per second). This refresh cycle includes two phases:
- Active Display Period: The display controller is actively reading pixel data from its internal GRAM and driving the physical pixels on the panel, scanning from top to bottom.
- Blanking Interval (V-blank): A short "safe" period after the controller has finished drawing the last line of a frame and before it starts drawing the first line of the new frame. During this time, the display is not reading from GRAM.
Screen tearing is the direct result of a lack of synchronization. If the MCU decides to update the GRAM with a new frame while the display is in its "Active Display Period," the display controller will read a mix of old and new pixel data, causing a tear.
Solution 1: Using the Tearing Effect (TE) Signal
Many modern display controllers provide a hardware solution called the Tearing Effect (TE) signal. This is a physical pin on the display that sends a pulse to the MCU, typically synchronized with the start of the Vertical Blanking (V-blank) interval.
This signal effectively tells the MCU, "I'm finished drawing the current frame. You are now safe to write the next frame."
C Code Example (TE Signal with Interrupt)
This conceptual C code demonstrates how to use an interrupt service routine (ISR) to wait for the TE signal. This is a very efficient, event-driven approach common in embedded systems like the ESP-IDF.
We use a semaphore (or a simple flag) to signal from the ISR to our main rendering task that it's safe to proceed.
/*
* Conceptual C Code for TE Signal Synchronization
* (Assumes an embedded environment like ESP-IDF or similar)
*/
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
// --- Globals ---
#define TE_SIGNAL_GPIO_PIN GPIO_NUM_5 // Example GPIO pin
static SemaphoreHandle_t te_signal_sem; // A binary semaphore
/**
* @brief Interrupt Service Routine (ISR) triggered on the TE signal's rising edge.
*/
static void IRAM_ATTR gpio_te_signal_isr_handler(void* arg) {
// A frame has just finished drawing.
// Give the semaphore to unblock the rendering task.
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(te_signal_sem, &xHigherPriorityTaskWoken);
// If giving the semaphore unblocked a higher-priority task,
// request a context switch.
if (xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
/**
* @brief Initialize the GPIO pin to receive the TE signal.
*/
void setup_te_signal_interrupt() {
// Create the semaphore, initially "taken".
te_signal_sem = xSemaphoreCreateBinary();
gpio_config_t io_conf;
io_conf.intr_type = GPIO_INTR_POSEDGE; // Trigger on rising edge
io_conf.pin_bit_mask = (1ULL << TE_SIGNAL_GPIO_PIN);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = 0;
io_conf.pull_down_en = 0;
gpio_config(&io_conf);
// Install the GPIO ISR service
gpio_install_isr_service(0);
// Hook our specific ISR handler to the pin
gpio_isr_handler_add(TE_SIGNAL_GPIO_PIN, gpio_te_signal_isr_handler, NULL);
printf("TE signal interrupt initialized on GPIO %d\n", TE_SIGNAL_GPIO_PIN);
}
/**
* @brief Main rendering task
*/
void render_task(void* arg) {
// --- Assume lcd_draw_frame_buffer() is your function ---
// --- that does the DMA transfer to the display. ---
extern void lcd_draw_frame_buffer(void* buffer);
extern void* get_next_frame_to_draw();
while (1) {
// --- 1. Prepare your next frame ---
// (Draw graphics, update text, etc.)
void* frame_buffer = get_next_frame_to_draw();
// --- 2. Wait for the "safe" signal ---
// This task will block here until the ISR gives the semaphore.
// The timeout (portMAX_DELAY) means it waits forever if needed.
if (xSemaphoreTake(te_signal_sem, portMAX_DELAY) == pdTRUE) {
// --- 3. Start the transfer ---
// We are now in the V-blank period. It's safe to send
// the new frame data to the display's GRAM.
lcd_draw_frame_buffer(frame_buffer);
}
// After this, the loop repeats, preparing the *next* frame
// while the display is busy drawing the one we just sent.
}
}
Solution 2: The "Sync Method" (Double Buffering)
This is the most robust software-based solution, often used with a TE signal for perfect synchronization. The "sync method" is double buffering.
Instead of one frame buffer, you allocate two:
- Front Buffer: The buffer the display is currently reading from.
- Back Buffer: The buffer the MCU is currently drawing the next frame into.
These two buffers are completely separate in memory. The display reads from the Front Buffer, while the MCU writes to the Back Buffer. Because they are never accessing the same memory at the same time, tearing is physically impossible.
The "sync" happens when the MCU finishes drawing to the Back Buffer. It then:
- Waits for the synchronization event (like the TE signal).
- Flips the buffers: It tells the display controller to read from the (old) Back Buffer on its next refresh.
- The (old) Back Buffer becomes the new Front Buffer.
- The (old) Front Buffer becomes the new Back Buffer, ready for the MCU to draw the next frame into it.
C Code Example (Double Buffering Logic)
This code demonstrates the pointer-swapping logic of double buffering. It can be combined with the TE signal logic from Solution 1 for a complete, tear-free system.
/*
* Conceptual C Code for Double Buffering (Sync Method)
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
// --- Configuration ---
#define LCD_WIDTH 320
#define LCD_HEIGHT 240
// Assuming 16-bit color (RGB565, 2 bytes per pixel)
#define BYTES_PER_PIXEL 2
#define FRAME_BUFFER_SIZE (LCD_WIDTH * LCD_HEIGHT * BYTES_PER_PIXEL)
// --- The two frame buffers ---
// These are often placed in external PSRAM on devices like ESP32
uint8_t* frame_buffer_1;
uint8_t* frame_buffer_2;
// --- Pointers to track which buffer is which ---
uint8_t* front_buffer; // Buffer the LCD is *currently* reading
uint8_t* back_buffer; // Buffer the MCU is *currently* drawing to
/**
* @brief Initialize the buffers and pointers
*/
void setup_double_buffering() {
// Allocate memory for both buffers
frame_buffer_1 = (uint8_t*) malloc(FRAME_BUFFER_SIZE);
frame_buffer_2 = (uint8_t*) malloc(FRAME_BUFFER_SIZE);
if (!frame_buffer_1 || !frame_buffer_2) {
printf("Error: Failed to allocate frame buffers!\n");
return;
}
// Start by pointing the LCD to buffer 1
front_buffer = frame_buffer_1;
// And setting buffer 2 as our first drawing target
back_buffer = frame_buffer_2;
// --- Pseudo-code for telling the LCD controller which buffer to use ---
// lcd_controller_set_display_address(front_buffer);
printf("Double buffering initialized.\n");
printf("Front Buffer: %p\n", (void*)front_buffer);
printf("Back Buffer: %p\n", (void*)back_buffer);
}
/**
* @brief This is your main graphics-drawing function.
* All drawing operations write to the 'back_buffer'.
*/
void draw_graphics_to_back_buffer() {
// --- Pseudo-code for drawing ---
// for (int y = 0; y < LCD_HEIGHT; y++) {
// for (int x = 0; x < LCD_WIDTH; x++) {
// uint16_t color = get_my_pixel_color(x, y);
// int index = (y * LCD_WIDTH + x) * BYTES_PER_PIXEL;
// // Write to the back_buffer, not the front!
// back_buffer[index] = color & 0xFF;
// back_buffer[index + 1] = (color >> 8) & 0xFF;
// }
// }
printf("Finished drawing to back buffer (%p)\n", (void*)back_buffer);
}
/**
* @brief This function "flips" the buffers.
* It should be called *after* drawing is complete and *on* the
* V-sync/TE signal.
*/
void swap_buffers_on_vsync() {
// --- 1. Wait for V-sync / TE Signal ---
// (This is where you would xSemaphoreTake(te_signal_sem, ...))
printf("V-sync received! Swapping buffers.\n");
// --- 2. Tell the LCD controller to read from the (old) back buffer ---
// This is the atomic "flip". The display will start reading from
// this address on its *next* frame.
// lcd_controller_set_display_address(back_buffer);
// --- 3. Swap the pointers ---
uint8_t* temp_ptr = front_buffer;
front_buffer = back_buffer;
back_buffer = temp_ptr;
printf("Swap complete.\n");
printf("New Front Buffer: %p\n", (void*)front_buffer);
printf("New Back Buffer: %p\n", (void*)back_buffer);
}
/**
* @brief Main application loop
*/
void render_loop() {
setup_double_buffering();
// setup_te_signal_interrupt(); // From Solution 1
while(1) {
// 1. Draw your complete frame into the inactive buffer
draw_graphics_to_back_buffer();
// 2. Wait for the safe moment and swap
swap_buffers_on_vsync();
// The loop repeats. The MCU is now free to draw the *next*
// frame into the new back_buffer, while the display is
// independently and safely reading from the new front_buffer.
}
}
Conclusion
Screen tearing is a synchronization problem. By either:
- Using the TE Signal: Timing your single-buffer writes to the V-blank "safe" period.
- Using Double Buffering: Making it physically impossible for the MCU and display to access the same memory simultaneously.
...you can ensure a perfectly stable, tear-free image for a professional and smooth user experience. Combining both methods (using the TE signal to time the buffer swap) is the most robust strategy used in high-performance graphics.