跳转到主要内容
x

理解并防止 LCD 屏幕显示撕裂

什么是屏幕撕裂?

屏幕撕裂是一种常见却令人分心的视觉伪影,尤其出现在应用快速移动图形或全屏更新的 LCD 屏幕上。它表现为画面中出现一条水平断裂或“撕裂”线,屏幕的上半部分和下半部分似乎来自于两个不同帧,从而导致图像上下错位。

这种问题发生于:当微控制器(MCU)或图形处理器正在向显示设备的显存(GRAM)写入新帧数据的同时,显示器的内部控制器正在从同一显存读取数据以刷新屏幕。

打个比方:将显示器视为从上到下逐行“阅读”一本书。如果你在读到页面中部的时候换了一本新的书,读者就会读到旧书的上半页和新书的下半页,结果画面就变得混杂,是“撕裂”的状态。

根本原因:同步不匹配

LCD 屏幕以固定频率(例如 60 Hz,即每秒刷新 60 次)更新其图像。该刷新周期包含两个阶段:

  1. 主动显示期(Active Display Period):显示控制器正从其内部显存(GRAM)读取像素数据,并驱动物理像素,从上至下逐行扫描。
  2. 垂直空闲间隔(V-blank):即在当前帧最后一行绘制完成之后,到下一帧第一行开始之前的一段“安全”期间。在这段期间,显示器不再从 GRAM 读取数据。

若 MCU 在显示器处于“主动显示期”时向显存写入新帧数据,就会导致控制器读取到旧数据与新数据的混合,从而引发画面撕裂。

解决方案 1:使用“Tearing Effect(TE)”信号

许多现代显示控制器提供了硬件解决方案,称为 “Tearing Effect(TE)信号”。这是显示模块上的一个物理引脚,用于在垂直空闲期间(V-blank)开始时发送一个脉冲信号给 MCU。

这一信号的作用是告诉 MCU:“我已完成当前帧的绘制,现在你可以安全地写入下一帧数据了。”

C 语言示例(使用 TE 信号 + 中断)

下面的示例代码展示了如何使用中断服务例程(ISR)来等待 TE 信号,这在嵌入式系统(如 ESP-IDF)中是一个非常高效、事件驱动的方式。

/*
* 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"

// --- 全局变量 ---
#define TE_SIGNAL_GPIO_PIN GPIO_NUM_5 // GPIO 引脚
static SemaphoreHandle_t te_signal_sem; // 二进制值信号量

/**
* TE 信号的中断服务例程(ISR)在 TE 信号的上升沿触发。
*/
static void IRAM_ATTR gpio_te_signal_isr_handler(void* arg) {
// 完成一个帧画面显示
// 发出信号,可以刷下一帧画面
   BaseType_t xHigherPriorityTaskWoken = pdFALSE;
   xSemaphoreGiveFromISR(te_signal_sem, &xHigherPriorityTaskWoken);

   if (xHigherPriorityTaskWoken) {
      portYIELD_FROM_ISR();
   }
}

/**
* 初始化 GPIO 引脚,用于接收 TE 信号。
*/
void setup_te_signal_interrupt() {
// 建立初始信号“繁忙”
   te_signal_sem = xSemaphoreCreateBinary();

   gpio_config_t io_conf;
   io_conf.intr_type = GPIO_INTR_POSEDGE; // 信号上升沿触发
   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);

   gpio_install_isr_service(0);
   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);
}

/**
* 主渲染任务
*/
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. 准备下一帧画面 ---
// (Draw graphics, update text, etc.)
   void* frame_buffer = get_next_frame_to_draw();

// --- 2. 等侯“安全”信号 ---
      if (xSemaphoreTake(te_signal_sem, portMAX_DELAY) == pdTRUE) {

// --- 3. 开始发送数据 ---
         lcd_draw_frame_buffer(frame_buffer);
      }
   }
}

解决方案 2:“同步方法”——双缓冲

这是一个更为强大、基于软件的解决方案,通常与 TE 信号结合使用,实现完美同步,避免撕裂。

所谓“双缓冲”是指:不是只用一个帧缓冲区,而是同时配置两个缓冲区:

  1. 前缓冲区(Front Buffer):当前显示器正在读取的那个缓冲区。
  2. 后缓冲区(Back Buffer):MCU 正在绘制下一帧数据的那个缓冲区。

这两个缓冲区在内存上完全分离。显示器从前缓冲区读数据,而 MCU 向后缓冲区写数据。由于二者永不同时访问同一内存,理论上就不会产生撕裂。

当 MCU 绘制完成后,会执行以下操作:

  1. 等待同步事件(如 TE 信号)。
  2. 在下一个刷新周期内,告诉显示控制器切换读取地址,从后缓冲区(已完成绘制)改为读取新缓冲区。
  3. 后缓冲区变为新的前缓冲区,显示器开始从它读取。
  4. 旧的前缓冲区变为新的后缓冲区,MCU 接着在其上绘制下一帧。

C 语言示例(双缓冲逻辑)

以下代码演示了指针交换(pointer-swapping)的核心逻辑,可与上文 TE 信号代码结合使用,构成无撕裂系统。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

// --- Configuration ---
#define LCD_WIDTH 320
#define LCD_HEIGHT 240
// 假设 16-bit 色彩(RGB565)
#define BYTES_PER_PIXEL 2
#define FRAME_BUFFER_SIZE (LCD_WIDTH * LCD_HEIGHT * BYTES_PER_PIXEL)
// --- 2个页面缓冲区 ---
uint8_t* frame_buffer_1;
uint8_t* frame_buffer_2;

uint8_t* front_buffer; // 当前 LCD 正在读取的缓冲区
uint8_t* back_buffer; // MCU正在写入数据的缓冲区

/**
* 初始化缓存和指针
*/
void setup_double_buffering() {
    // 划出缓存空间
    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;
    }

// LCD 读取缓冲区1
    front_buffer = frame_buffer_1;
// MCU 写入数据到缓冲区2
    back_buffer = frame_buffer_2;

//模拟-告知控制器显示该读取的缓冲区     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);
}

/**
* 更新数据到 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);
}

/**
* 前后缓冲区互换
* 前提条件是MCU已更新数据并收到TE信号
*/
void swap_buffers_on_vsync() {
    // --- 1. 等候 V-sync / TE 信号 ---
    // (This is where you would xSemaphoreTake(te_signal_sem, ...))
    printf("V-sync received! Swapping buffers.\n");

// --- 2. 告知控制器从(旧)缓冲区2读取数据 ---
// lcd_controller_set_display_address(back_buffer);

// --- 3. 前后指针互换 ---
    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);
}

/**
* 主渲染程序
*/
void render_loop() {
    setup_double_buffering();
// setup_te_signal_interrupt(); // 可选:结合方案1使用

    while(1) {
    // 1. MCU更新数据到 back buffer
       draw_graphics_to_back_buffer();

    // 2. 等候交换的信号
       swap_buffers_on_vsync();

       // 循环,MCU开始写入下一帧数据到 back buffer
       // 控制器读取front buffer数据并渲染到LCD.
    }
}

结论

屏幕撕裂根本上是一个同步问题。通过以下任一方法即可实现稳定、无撕裂的画面体验:

  • 使用 TE 信号:将 MCU 向显存的写入时机限定在 V-blank 的“安全”时期。
  • 使用双缓冲:从物理层面确保 MCU 与显示器永不同时访问同一缓冲区。

将两者结合(即在 TE 信号触发时切换缓冲区)是目前高性能图形应用中最稳健的策略。 如需更多技术支持或定制开发,欢迎联系我们!