MADL!AR
Code is cheap, show me the PPT!
首页
分类
Fragment
关于
如何解决液晶屏TE(Tearing Effect,显示撕裂)问题
分类:
硬件
发布于: 2023-11-29
最近在调试液晶显示屏的时候,发现屏幕有很明显的撕裂现象。为了验证,编写了一个程序:在死循环里使用红屏和蓝屏交替刷新,肉眼就可以观察到下述现象: ![撕裂屏powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/f9829ca41945ca1adc50826c8e6bf30e.jpg) 带着疑惑在网上寻找资料的时候,发现很多屏幕也有类似的问题: ![撕裂屏powerstyle={max-width: 80%;min-width=300px}](/notebook/publish/i/caoliang.net/img/ec3921f963204cebc88d450da684504f.jpg) 上图左是Arduino Giga Display官方的demo演示视频截图,可以明显看到在菜单转场动画中,画面出现了斜向撕裂条纹;右是stm32H747-Disco在运行lvgl官方demo时产生了画面撕裂。 ### 解决斜向撕裂 这种情况通常是竖屏横用,或者横屏竖用导致。 ![powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/22c17ca550f1ad684cc629ea5dc843e7.png) 屏幕像素的刷新方向由屏幕面板上的cof芯片决定,即上图红色圈内所示,它是贴装在显示面板上的控制芯片。它连接每一行和每一列像素的电极,作为驱动器件控制每一个像素的开启或关闭。屏幕的横竖方向在制造时已经确定,无法通过软件进行更改。虽然很多cof芯片有对应的寄存器,可以通过软件将其配置为横向显示或者竖向显示,但它实际上只是配置了cof读取GRAM的顺序,并未改变行场信号的刷新方向。如果将竖屏配置为横屏,像素刷新的顺序如同“Z”字型一样,每刷完一行,就会回到下一行的行首,如此往复;而GRAM却完全相反,它是同“N”字型一般,先刷完一列,然后回到下一列的起始再刷新下一列,以此类推……这样一来,在每一行都会产生冲突,即一半为旧帧,一半为新帧,自然会产生斜向撕裂。 解决斜向撕裂的方法,就是保证GRAM的刷新方向与面板的刷新方向保持一致。如果需要改变显示方向,可以开辟一个缓冲区,将图像数据渲染到这个缓冲区,软件旋转90°后再写入GRAM。很多微控制器没有GPU,增加旋转操作可能十分耗时,会显著造成帧率下降。显示效果和执行效率之间,往往需要进行权衡。 ### 解决横向撕裂 斜向撕裂解决之后,比较容易遇到是横向撕裂,如下图: ![powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/d7085e22078baa71fd41760abb3c3c93.png) 这种情况是因为屏幕面板和GRAM刷新不同步造成的。面板在刷新像素时,需要读取GRAM,此过程又被称为R(Read);微控制器刷新图像的动作,是向GRAM写数据的过程,因此被称作W(Write)。在屏幕像素刷新时,GRAM还未完成刷新,此时面板上显示的一半是旧帧数据,另一半新帧数据,且会持续1帧的时长,被人眼所看见,即呈现出来撕裂的效果。 下述是从网络上获取的动图,更加清晰的描述横向撕裂的细节: ![powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/4ee5ec0ae794f94c3fabe7cc0aea3f08.gif) 解决横向撕裂,基本前提是W和R同步。如果不同步,那么W可能发生在R的任意阶段,则必然会造成撕裂。在同步的情况下,对W和R的速率也有一定的要求。 * 当W > R时,R指针永远在W之后,因此不会造成撕裂,如下图所示: ![powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/0fbfdd304fac0de3f9a99509a147dc12.gif) * 当W < R 时,须满足 W ≥ 0.5R。下图是临界值的情况: ![powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/2681912324990b5e9f7ab441a1b3ce0f.gif) 因为W < R,所以R指针跑在前面,读出的是旧数据,第1帧显示的还是上一帧的图像。第2帧开始刷新时,W指针已经刷完了一半的图像,等到R刷新到最后时,W也已刷完整帧图像,因此GRAM中的图像得以完整的显示出来。如果R再稍微快那么一点,那么在第2帧R指针就又会赶上W指针,这样就会再次出现撕裂现象 由以上可以得出避免横向撕裂结论: 1. __W与R须同步开始__ 2. __W ≥ 0.5R__ 上述的第2个条件很容易满足,R的速度由屏幕的帧率决定,W的速度由总线时钟决定,可以通过软件进行配置。以st7789v为例,在PDF中可帧率控制章节可以找到帧率控制模式: ![powerstyle={max-width: 60%;min-width=300px}](/notebook/publish/i/caoliang.net/img/705db5ddd15c391fdf239e8318219771.png) 而要满足第一个条件:使WR同步,则需要硬件配合。 ### TE引脚 常见的cof芯片都有一个TE(Tearing Effect)引脚,它是液晶屏向控制端输出同步信号的引脚。在软件上可以对其进行配置,使其在帧开始之前,输出一个脉冲或者跳变电平信号,控制端收到信号后开始写入GRAM,从而达到同步刷新的目的。 ![powerstyle={max-width: 80%;min-width=300px}](/notebook/publish/i/caoliang.net/img/bbe663b85270c55806a887f39f79cdd3.png) 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; } ```