理解并防止 LCD 屏幕显示撕裂
什么是屏幕撕裂?
屏幕撕裂是一种常见却令人分心的视觉伪影,尤其出现在应用快速移动图形或全屏更新的 LCD 屏幕上。它表现为画面中出现一条水平断裂或“撕裂”线,屏幕的上半部分和下半部分似乎来自于两个不同帧,从而导致图像上下错位。
这种问题发生于:当微控制器(MCU)或图形处理器正在向显示设备的显存(GRAM)写入新帧数据的同时,显示器的内部控制器正在从同一显存读取数据以刷新屏幕。
打个比方:将显示器视为从上到下逐行“阅读”一本书。如果你在读到页面中部的时候换了一本新的书,读者就会读到旧书的上半页和新书的下半页,结果画面就变得混杂,是“撕裂”的状态。
根本原因:同步不匹配
LCD 屏幕以固定频率(例如 60 Hz,即每秒刷新 60 次)更新其图像。该刷新周期包含两个阶段:
- 主动显示期(Active Display Period):显示控制器正从其内部显存(GRAM)读取像素数据,并驱动物理像素,从上至下逐行扫描。
- 垂直空闲间隔(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 信号结合使用,实现完美同步,避免撕裂。
所谓“双缓冲”是指:不是只用一个帧缓冲区,而是同时配置两个缓冲区:
- 前缓冲区(Front Buffer):当前显示器正在读取的那个缓冲区。
- 后缓冲区(Back Buffer):MCU 正在绘制下一帧数据的那个缓冲区。
这两个缓冲区在内存上完全分离。显示器从前缓冲区读数据,而 MCU 向后缓冲区写数据。由于二者永不同时访问同一内存,理论上就不会产生撕裂。
当 MCU 绘制完成后,会执行以下操作:
- 等待同步事件(如 TE 信号)。
- 在下一个刷新周期内,告诉显示控制器切换读取地址,从后缓冲区(已完成绘制)改为读取新缓冲区。
- 后缓冲区变为新的前缓冲区,显示器开始从它读取。
- 旧的前缓冲区变为新的后缓冲区,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 信号触发时切换缓冲区)是目前高性能图形应用中最稳健的策略。 如需更多技术支持或定制开发,欢迎联系我们!