MCU如何高效地驱动LCD——8080&SPI篇
奥大梨呀
编辑于 2023年11月30日 08:02
收录于文集
共3篇

    哈喽大家好,距离上一次更新专栏已经过去了1个月了。很抱歉拖更了这么久,主要还是因为工作项目要上线,还有生活中的琐事太多了,没有一段比较空闲的时间拿来实验和构思。这次在写专栏时,我发现,其实8080接口 LCD和SPI接口 LCD的驱动方式除了接口不同以外,其余都是差不多的,因此,将在本篇专栏将一起说明。

    对于这两种LCD来说,与上篇文章所讲的RGB LCD最大的区别在于,这两种LCD内部自带LCD控制器,相比于RGB LCD而言,要驱动它的硬件条件变得很简单,不要求处理器具有LCD控制器和大内存,只需要按照约定的时序将RGB数据发送到LCD上即可。但这样也降低了可玩性,一些操作可能更复杂,就比如本系列文章的主题——如何防止LCD撕裂。虽然复杂,但也是有迹可循的,接下来我将会一 一道来。

    如何驱动这类LCD,我就不再多提了,无非就是实现对应的数据收发,配合DMA传输,这是最简单的驱动方式,可以说,市面上大部分的这类LCD厂商提供的代码都是这么做的,有的可能没有实现DMA,但至少也能够驱动其显示,无非就是使用了0x2a,0x2b,0x2c这类写GRAM的指令。但仅仅这样还不够完美,在本系列的第一篇文章提到了,如果在刷新LCD显示时,没有做读写指针同步,也就是V-sync,那么必定会导致LCD出现撕裂现象。

    按照我的驱动这类LCD的思路来分,现在市面上这类LCD可以分为2大类,一类是引出TE脚的LCD,一类则是没有引出TE脚的LCD。什么是TE脚呢,上篇文章中,我们对RGB LCD进行V-sync时,是使用处理器的LCD外设的行中断到第0行触发中断的。而这类自带控制器的LCD我们怎么进行V-sync呢,就是通过TE脚输出脉冲。在LCD初始化时配置TE脚脉冲触发的行数,比如当LCD刷新到第0行时,在TE脚产生一个脉冲,处理器通过GPIO中断捕获这个脉冲,这一步便是我一直说的“读写指针同步”。

TE输出脉冲时序

    同步完成,那接下来呢?在上篇文章中,我们是通过处理器的LCD控制器进行双缓冲操作,直接交换前后台缓冲区防止撕裂的。但是在这类带GRAM的LCD上,是没有办法使用双缓冲的。那么只能通过2:1关系(R:W)了。其实也不需要严格2:1,只需要保证一点——当处理器写该帧数据的整个过程中,读指针不会从写指针后方超过写指针即可,合理设置LCD的刷新率和处理器的数据传输速率,从而达到避免读写指针在该帧中间相遇的情况,下文会说明如何确定这些值。

首先说一下带TE的LCD驱动方式,此类LCD较为简单,分为以下步骤:

1.初始化LCD时,配置TE脚,当LCD刷新到第0行时触发TE脉冲。

2.初始化处理器GPIO中断,捕获TE脚的脉冲,通常是下降沿触发(具体根据LCD手册)。

3.在GPIO中断中开启显存数据传输,使用DMA传输(DMA一次传输不完需要分多次传输)。

4*.在DMA传输完成中断中做一些传输完成的处理,比如lvgl的flush_ready就可以在这里做,这一步不是必要的,根据需求来。

    以上方法针对总是全屏刷新的LCD,通常在具有大RAM的处理器上使用。比如要在linux驱动该LCD就可以这样做。但是在一些小内存MCU上,我们很少会全屏刷新,因为全屏刷新UI需要一整块的显存。因此对与局部刷新的驱动方式,需要做一些改变:

1.在需要刷新内容时,设置TE脚,当LCD刷新到第X行时触发中断,X通常是刷新区域的中间行,具体可能需要根据LCD的刷新率和处理器的数据传输速率进行调整。

......接下来均与上方一致,区别在于,全屏刷新的TE行数是一开始就配置好了,而局部刷新则是需要动态变化。当然局部刷新的最后一步不要忘了关闭中断,不然就一直触发刷新了。

    由于我手上暂时没有“带TE的LCD”,而且,现在某宝上大部分的LCD也不带TE,所以简单讲一下驱动方法,重点还是放在不带TE的 LCD上。

    对于不带TE的LCD,我们无法使用中断这类高效的方式来进行V-sync,只能通过查询方式。如果你翻手册翻的比较多,应该会了解,大部分的LCD都会提供一个指令——GetScanline,这个指令可以用于获取LCD控制器当前刷新到哪一行,下图是ST7789的手册中截出来的。

ST7789 Get Scanline指令

    这是一个读取指令,我们可以通过这个指令得知当前LCD刷新的行,通过不断轮询,直到LCD刷新到我们需要同步的行后,再进行显存数据的发送,也可以达到防止撕裂的效果,相比带TE脚的LCD,就是CPU使用率会比较高,毕竟是轮询的方式。因此,这类LCD的驱动流程可以分为以下步骤:

1.当需要刷新数据时,通过该指令不断查询当前扫描行,直到X行,X通常是刷新区域的中间行,具体可能需要根据LCD的刷新率和处理器的数据传输速率进行调整。

2.进行显存数据的发送。

3*.传输完成处理。

下面我将使用查询扫描行和不查询直接发送的方式对比两者的撕裂情况,两者实现的均是非常快速的刷屏:

首先是查询的方式,关键代码如下:

代码块
clike
自动换行
复制代码
uint16_t colors[] = {0xf800, 0x07e0, 0x001f};
int i = 0;

while (1)
  {
    uint16_t line = 0;

    do
    {
      line = LCD_ReadScanLine();
    }while(line != 150);  // 轮询扫描线,到150行时跳出
    
    LCD_Fill(0, 20, 200, 300, colors[i++ % 3]);	// 填充LCD
    printf("line: %d\n", line);
    HAL_Delay(100);
  }
复制成功

上面的代码中,由于要刷新x(0, 200),y(20, 300)的方形区域,因此先查询当前的扫描行,当其位于150行时,开始刷新LCD,并延时100ms后进行下一次循环。

我们直接来看看现象:

无撕裂现象

    可以看到在LCD不断刷新的过程中没有出现任何撕裂线。

    下面再看看,直接刷新的方式,只要取消上面代码的do{}while阻塞查询即可。来看看现象:

撕裂现象

    从上图我们可以发现,在刷屏过程中多次产生了撕裂线,表示当前画面中,存在至少2帧不同的数据,这便是没有进行V-sync导致读写指针相遇而出现的撕裂现象。

    对于这种轮询的方式有一点需要注意,因为这个指令为读数据指令,通常需要比较低的时钟速率,对于MCU接口的LCD,一般处理器相关硬件初始化上都会把读写时序参数分开设置,读取不需要做什么处理,直接读即可,像STM32的FMC就是这样。而SPI就需要在发送这个指令读取时,降低时钟速率,具体降低到多少需要根据手册而定。

    以上代码工程,我会贴在文章末尾,供大家参考。

    下面说一下另一个重点,如何设置LCD的刷新率和处理器的数据传输速率。这类带控制器的LCD在刷新时,需要发送大量数据,数据发送的时间通常很长,达到ms级别,与RGBLCD那样交换前后台缓冲区一瞬间就完成的方式不同。因此,需要控制好LCD的刷新率和处理器的显存数据传输速率,当然还有scanline合理的位置,这里我给1个不等式,凡是满足这个不等式的参数均合理。

    设T_%7Bcpu%7D%20为处理器传输一个全屏帧所需的时间,T_%7Blcd%7D%20为LCD刷新一帧所需的时间,则需要满足:

T_%7Bcpu%7D%5Cleq2%5Ctimes%20%20T_%7Blcd%7D%20,其中T_%7Bcpu%7D%20%3DT_%7Blcd%7D%20时最优。

    而对于scanline的位置,我们需要从全屏的尺寸来考虑,因为全屏刷新是范围最大,最一般的情况,计算出来的值肯定适用于局部刷新。因此只要全屏刷新没问题了,那么局部刷新肯定没问题。

    设lcd的高度为h(单位:像素),h也适用于局部刷新时的区域高度;T_%7Bmin%7D%20为T_%7Blcd%7D%20和T_%7Bcpu%7D%20中的最小值,T_%7Bmax%7D%20则为二者中最大值;scanline的位置为L,scanline的位置百分比为L_%7Bp%7D%20,则:

L%3Dh%5Ctimes%20L_%7Bp%7D%20%2CL_%7Bp%7D%5Cin%20%5B1-%5Cfrac%7BT_%7Bmin%7D%7D%7BT_%7Bmax%7D%7D%2C%20%5Cfrac%7BT_%7Bmin%7D%7D%7BT_%7Bmax%7D%7D%5D

    从上面这个式子也可以看出来,为什么T_%7Bcpu%7D%20%3DT_%7Blcd%7D%20时是最优的,因为可以选择任意一行作为scanline。

    值得一提的是,这类LCD一般有一个指令用于设置行列地址转换的,可以变更LCD的横竖屏。比如ST7789的0x36指令参数的MV位:

ST7789 0x36指令的MV位

    尽管这在我们看起来是切换了横竖屏,但对于扫描线scanline而言,始终与LCD的排线互相垂直,手册中也有提到这一点,扫描线与MV位无关。

ST7789手册关于MV和TE的描述

    这里看似在说TE信号,实际上TE信号就是由扫描线触发的,因此可得知扫描线不受MV的影响。

    也就是说,当你设置了MV,试图把竖屏当横屏或者横屏当做竖屏使用时,是无法使用以上任何一种利用扫描线进行V-sync的。举一个很简单的例子,不知道玩过这类LCD的朋友们有没有发现,当你在竖屏的情况下快速刷色块时,出现的撕裂线是水平的,而当你设置了MV,把竖屏当横屏用进行快速刷色块时,撕裂线是程一定角度倾斜的,这就是因为,LCD的刷新与MV无关,始终都是竖屏刷新,而处理器却按横屏写入数据,这必然会导致读写指针相遇,且随着时间的推移,LCD扫描线往下,读写指针相遇的点也会往下,这就造成了撕裂线是斜着的原因。因此,想追求无撕裂效果的朋友们,只能使用竖屏(大部分都是竖屏),如果要横屏,可以在软件上做旋转,或者处理器带硬件旋转直接旋转显存数据后再写入也是可以的。

    那么本系列关于LCD的驱动就结束了,如果有什么不合理或者错误的,还请大家指出,我们一起学习进步。关于后面要出什么文章或者视频,还容我构思构思,感谢大家的支持。

    demo链接:https://gitee.com/Jumping99/st7789-without-te-demo.git