如何解决液晶屏TE(Tearing Effect,显示撕裂)问题
最近在调试液晶显示屏的时候,发现屏幕有很明显的撕裂现象。为了验证,编写了一个程序:在死循环里使用红屏和蓝屏交替刷新,肉眼就可以观察到下述现象:

带着疑惑在网上寻找资料的时候,发现很多屏幕也有类似的问题:

解决斜向撕裂
这种情况通常是竖屏横用,或者横屏竖用导致。

屏幕像素的刷新方向由屏幕面板上的cof芯片决定,即上图红色圈内所示,它是贴装在显示面板上的控制芯片。它连接每一行和每一列像素的电极,作为驱动器件控制每一个像素的开启或关闭。屏幕的横竖方向在制造时已经确定,无法通过软件进行更改。虽然很多cof芯片有对应的寄存器,可以通过软件将其配置为横向显示或者竖向显示,但它实际上只是配置了cof读取GRAM的顺序,并未改变行场信号的刷新方向。
如果将竖屏配置为横屏,像素刷新的顺序如同“Z”字型一样,每刷完一行,就会回到下一行的行首,如此往复;而GRAM却完全相反,它是同“N”字型一般,先刷完一列,然后回到下一列的起始再刷新下一列,以此类推……这样一来,在每一行都会产生冲突,即一半为旧帧,一半为新帧,自然会产生斜向撕裂。
解决斜向撕裂的方法,就是保证GRAM的刷新方向与面板的刷新方向保持一致。如果需要改变显示方向,可以开辟一个缓冲区,将图像数据渲染到这个缓冲区,软件旋转90°后再写入GRAM。很多微控制器没有GPU,增加旋转操作可能十分耗时,会显著造成帧率下降。显示效果和执行效率之间,往往需要进行权衡。
解决横向撕裂
斜向撕裂解决之后,比较容易遇到是横向撕裂,如下图:

下述是从网络上获取的动图,更加清晰的描述横向撕裂的细节:

解决横向撕裂,基本前提是W和R同步。如果不同步,那么W可能发生在R的任意阶段,则必然会造成撕裂。在同步的情况下,对W和R的速率也有一定的要求。
- 当W > R时,W在前面写,R在后面读,如下图所示:
可以看到,W如果跑的过快的话,会与R在帧末尾处相遇。此时W反超R,新数据覆盖掉尚未来得及读取的帧缓冲,导致撕裂。当 W ≤ 2R时不会反超R,从而确保帧缓冲不会被覆盖而产生冲突。
- 同理当W < R时,须满足 W ≥ 0.5R。下图是临界值的情况:
因为W < R,所以R指针跑在前面,读出的是旧数据,第1帧显示的还是上一帧的图像。第2帧开始刷新时,W指针已经刷完了一半的图像,等到R刷新到最后时,W也已刷完整帧图像,因此GRAM中的图像得以完整的显示出来。如果R再稍微快那么一点,那么在第2帧R指针就又会赶上W指针,这样就会再次出现撕裂现象
由以上可以得出避免横向撕裂结论:
- W与R须同步开始
- 0.5R ≤ W ≤ 2R
上述的第2个条件很容易满足,R的速度由屏幕的帧率决定,W的速度由总线时钟决定,可以通过软件进行配置。以st7789v为例,在PDF中可帧率控制章节可以找到帧率控制模式:

而要满足第一个条件:使WR同步,则需要硬件配合。
TE引脚
常见的cof芯片都有一个TE(Tearing Effect)引脚,它是液晶屏向控制端输出同步信号的引脚。在软件上可以对其进行配置,使其在帧开始之前,输出一个脉冲或者跳变电平信号,控制端收到信号后开始写入GRAM,从而达到同步刷新的目的。

TE引脚及信号的配置,芯片手册中也有详细说明,通常配置为每帧输出一个脉冲,即下图中的Mode 1。另外,还可以配置TE信号发出的时机,用来微调WR的先后时差,这里主要用来补偿总线的传输时差。以stm32为例,将TE信号作为外部中断触发信号,在中断服务函数中,触发屏幕的刷新,关键代码如下:
#define LCD_WIDTH 240
#define LCD_HEIGHT 320
// 这里的 GRAM 不同于上文提到的 GRAM
// 此处是微控制器中开辟的内存空间,用于缓存图像数据;后者是cof芯片中的RAM
#define LCD_GRAM_LEN ((uint32_t)(LCD_WIDTH*LCD_HEIGHT))
// LCD 硬件设备定义
typedef struct {
int8_t frontLayer; // 前景层,待刷新层
int8_t lastFlushLayer; // 上次刷新的图层
// 刷新的区域。0代表上半区,1代表下半区
// 由于硬件限制,单次无法传输完1帧画面,因此分为两次,先传上半部分,再传输下半部分
int8_t flushPart;
// 初始化标志。会在设备初始化完成之后设为1。避免在设备未初始的情况下启动传输
int8_t isInited;
} LCDTypeDef;
LCDTypeDef hlcd = {0};
// 采用双缓冲,一个图层用于绘制,另外一个图层用于传输(前景层),交替进行
static uint16_t LCD_GRAM[LCD_WIDTH*LCD_HEIGHT*2];
// 保存两个图层的首地址
uint8_t *LCD_LayerAddr[2] = {LCD_GRAM, &(LCD_GRAM[LCD_WIDTH*LCD_HEIGHT])};
// -------------------- MCU 中断入口 -------------------- //
// TE 信号中断,触发帧传输 LCD 传输完成回调
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_6) { // TE 引脚连接到 GPIO_PIN_6
LCD_OnTE();
}
}
// SPI 传输完成中断,执行 LCD 回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
LCD_SPI_TxCallback(hspi);
}
// -------------------- LCD 驱动部分 -------------------- //
// TE 信号中断回调,用于触发帧传输
int8_t LCD_OnTE(){
if (!hlcd.isInited) {
return 1;
}
if (hlcd.lastFlushLayer == hlcd.frontLayer || hlcd.flushPart >= 0) {
// 此图层已刷新过,或者刷新到一半
return 1;
}
// 启动传输,将前景层数据写入屏幕
hlcd.flushPart = 0; // 初始化刷新区域
return HAL_SPI_Transmit_DMA(&LCD_SPI, LCD_LayerAddr[hlcd.frontLayer], LCD_WIDTH*LCD_HEIGHT/2);
}
// 单次传输完成回调
void LCD_SPI_TxCallback(SPI_HandleTypeDef *hspi) {
if (!hlcd.isInited) {
return;
}
if (hlcd.flushPart == 0){
// 上次 SPI 传输的是上半部分,此时需要启动下半部分的传输
hlcd.flushPart = 1;
HAL_SPI_Transmit_DMA(&LCD_SPI, LCD_LayerAddr[hlcd.frontLayer] + LCD_WIDTH*LCD_HEIGHT, LCD_WIDTH*LCD_HEIGHT/2);
} else if (hlcd.flushPart == 1) {
// 下半部分的传输已经完成,重置标记
hlcd.flushPart = -1;
hlcd.lastFlushLayer = hlcd.frontLayer;
}
}
// 获取绘制层 GRAM 首地址
uint16_t* LCD_GetBackGRAMAddr() {
return (uint16_t *) LCD_LayerAddr[hlcd.frontLayer == 0 ? 1 : 0];
}
// 执行 LCD 刷新。在刷新完GRAM之后调用此函数
void LCD_Flush() {
// 等待刷新完成
while (hlcd.lastFlushLayer != hlcd.frontLayer);
// 将绘制层设置为前景层
hlcd.frontLayer = hlcd.frontLayer == 0 ? 1 : 0;
// 清除旧帧标记,在下一次进入 LCD_OnTE 时,就会触发 SPI 传输从而刷新画面
hlcd.lastFlushLayer = -1;
}
添加评论
评论不会公开显示,仅用于与作者交流。