理解 LCD 屏幕的驱动原理与调试过程,示例的驱动 IC 为 GC9308 ,展示整个屏幕的驱动过程。

起因

最近拿到了一个比较新的驱动 IC 的 LCD 了,此前 K210 上面使用的都是 ST7789V ILI9342C SH1106 这类驱动 IC 的屏幕模块。

这次来了一个 GC9308 ,我想我需要认识一下屏幕驱动的整体架构,也就是拿起数据手册当作学习教材来学了,实际上学完以后,懂了以后都不难,重点在如何总结这些屏幕的驱动逻辑,以此打下往后的屏幕驱动理解基础。

我需要读懂图像的二进制定义、还有传输方式,我找了一本中文的屏幕数据手册来读,了解一下相关的流程和细节

本文我只会交待软件层面的理解,硬件方面的定义和特性我无法给出准确的解释,姑不会提及。

屏幕的发展历程

让我们看一下这个大哥的故事,就很好的说明了这段 LCD MCU 发展的历史。

https://blog.csdn.net/special00/article/details/78128374

记得在很早的时候,那时候还都是FSTN的显示屏满天飞的时候(也是小弟刚刚毕业开始作手机的时候)。LCD的驱动电路有很多是两片芯片的,一片LCDC,一片LCD Driver,一般的LCDC里面有一个display的buffer。LCDDriver是电路驱动液晶显示部分的电路,没有什么好讲的。更早的时候,LCD上就一片LCDDriver就行了,程序员需要控制两个(H,V)场扫描信号,而且程序员希望在某个坐标显示,都需要编程控制驱动电路来实现,后来发现显示屏越来越大,而MCU以及程序员没有这个能力和精力来对LCD进行这类的同步控制,于是LCDC就诞生出来承担起这些个功能。后来加上了buffer,就是说程序员可以把大批的显示内容以显示矩阵(display matrix)的形式写到buffer里,让LCDC来读取buffer里的数据再由LCDDriver显示到显示屏上。后来这个buffer越来越大,除了显示矩阵以外还放很多命令,所以也不能老把它笼统的叫buffer啊,所以就对放显示矩阵的存储空间有了一个专用的名字叫做GRAM。到现在嘛,这些驱动/控制电路以及buffer都合起来放在一片芯片中,统称为driver IC啦。也就是LCM上那颗COG的芯片,相信看这片回帖的兄弟们都看到过。而且这颗driver IC的功能越来越nb,有什么dimm功能啊,gamma功能啊,什么省电啊等等乱七八糟的功能,不过大多功能程序员都不需要去详细了解,现在的程序员都很轻松啦,只需要用很简单的几条命令就可以控制这颗driver IC来驱动LCD。

更多的内容可以自己去跳转阅读。

如何理解现在的 LCD 屏幕的驱动架构

因为我是在学习和整理过后进行消化的知识,所以我会对我所掌握的内容做一个重新编排,我认为理解一个模块或事物,应当从它的 本质架构 或 程序设计 上去理解它,这样的理解会存在一个同类事物的共性,如屏幕的驱动共性。

以往供应商给到我的数据手册全是英文的,这个确实影响到了我对屏幕的认知,一方面是英文的翻译和中文的专业术语不对称,存在理解误差。另一方面是英文的描述在初学阶段并不能驻留在脑子里,如果是为了效率学习,母语的学习效率将会极高,因为专业术语有铺垫,所以我找了一本中文的屏幕数据手册来作为原始的基础学习的材料。

想要进一步的理解复杂功能,必然要先建立一个基础知识体系,让一个从来没有开发过屏幕驱动,只会调调代码的朋友去开发一款全新的 LCD 驱动代码,例如对接一款全新的 LCD 屏幕,通过 MCU 接口驱动一款 8bit 数据总线 16bit 颜色的 LCD 屏幕,并同步支持 GUI 的基础绘图操作,从初始化到绘图显示图像等功能实现,那么要如何去解决这个问题呢?

也许你和我一样是个软件工程的开发者,知道去使用类似的 code 来复现驱动逻辑,但你未必知道你所用的硬件之中的定义,对应的功能又是哪些,你如果不了解你的驱动芯片,你又该如何写它呢?也许逻辑是对的,但是对这款芯片也一样适用吗?

这也是我最初的困惑,在我没有深入学习之前,我是可以通过抄代码逻辑来完成对一款屏幕的驱动,然而现在我拿到的是网络上连代码都没有的 IC ,就有那么一些迷茫,现在我把这些学习和理解的过程记录下来,给未来可能出现同样问题的你,建立一个容易理解的体系概念。

为了确保自己提供的内容是准确且符合客观事实的,所有讯息的参考依据都是来源于数据手册,这意味着数据手册就是我们最好的学习材料 XD 。

LCD 屏幕的初始化流程

按道理来说,任何一个屏幕都应该在这里完成对屏幕的初始化,产生雪花屏的效果,不然后面的内容都可以不用做了,但这里的内容我打算是放到最后的实践章节来讲的,所以放一张图占位就好了嘻嘻。

所谓的雪花屏就是背后那些密密麻麻的的白色色块,这表示 LCD 屏幕初始化完毕。

LCD 屏幕的体系架构

我认为先基础理解这个是重中之重,关于这个结构的问题,在我看的这么 5 本屏幕数据手册之中来看,体系架构是在进化的,但本质是不变的,所以可以放心的去获取自己所需要的部分。

整体的架构框图(Block diagram)

现在我们很少会直接去使用这里面的内容了,因为所谓的屏幕供应商厂家会根据你的屏幕接口定制你所需要的线序,减少我方驱动开发人员的对驱动硬件层面的理解,从而让软件开发人员只需要关注写图像数据这件事情。

像我们必然会确认的几个关键信息,如这图的 A0(DC) \ IM0:2 \ WR \ RES \ D0:7 等引脚资源,基本就可以判断出这个 lcd 的支持层次和范围,还可以进一步判断 它的 Display Data Ram 的信息,但事实上,知道还是不知道都不影响我们对它的驱动,只是帮助我们快速感知这个屏幕的架构情况,所以知道了部分就可以继续往下看了。

显示内存(DDRAM)的线路地址映射关系

关于显示内存,有的数据手册可能会称为 Graphics RAM (GRAM),但大体意思是不变的,为避免误解我附个图。



虽然说法都有点不太一样,但这并不影响我们对它的理解,它实际上的用途就是一块用于显示的内存,它代表了 驱动 IC 的会通过读取它的数据进行屏幕的扫描显示,而我们要做的就是对它写入显示的数据和对应的行列起始地址(address),有得会称为行计、列数器(counter),其实效果如下图。

如何设置显示的内容呢?这就要看懂它的 DDRAM 的地址关系,将数据写入到对应的 DDRAM 中就可以做到了(驱动 IC 会从中读取数据显示到屏幕的这个过程),所以我们必然要理解这个内容,它的结构图如下。

至此可以理解的是,驱动 IC 的工作内容,接着我们深入理解一下 DDRAM 的线路地址,关于它的寻址方式。

这是一张很形象的 SH1106 的地址寻址图,关于图像的存储方式和行列寻址方式,我现在来解释一下上面这张图想要表达的意思。

首先我们知道这是单色的 LCD 屏幕,所以看到的图像中标记的红线表示每一次传输的 D0:7 列数据(colum address),显示出来就是对应的图像像素点(1 bit),有得又会叫列计数器,实际上都是指这个列数据的索引数据,同样的还有行数据(line address),那么我们继续理解它的 页面地址(page) 这个实际上是可以这样形象的理解的。

如果不做出说明和讲解的话,这些张图可能很难让人理解,不妨想象一下屏幕就是一款打印机,它想要显示数据就需要打印一张纸(page)出来,打印的纸张将根据起始的行地址来按行打印每行的一列数据,这样最后打印出来的内容就是屏幕所显示的数据拉,不妨结合下图来感受和理解这个存储结构。

它实际上就是想说明下述效果。

现在你是否能够理解了呢?如果理解了,你再看看这张图,是不是就能理解了呢?(页面地址并不是每款都会去设计的,因为不一定需要多级缓存,通常双缓冲足以)

那么理解了这个对我们有什么意义呢?至少你现在应该知道如何控制代码去给这些驱动 IC 设置想要显示的数据了,它们基本都是相通的,不足为奇。

拓展:关于如何理解数据手册中所讲的设置屏幕的行扫描方向(显示方向) https://www.cnblogs.com/amanlikethis/p/3872515.html

LCD 屏幕的数据总线接口类型

既然我们知道应该如何去设置显示内存了,那么就得看看我们应该如何连接我们的 LCD 驱动 IC 了。

数据总线这个用词是来源于微机原理,其实就是一堆用于传输数据的数据线,指都有哪些通信手段,或者说传输接口。

首先确认 im 的配置,这个配置通常由硬件上的接线决定,如下图我们得知。

只有先确认了我们的连接的配置才会进一步的配置寄存器、写入显示数据,同样的也要进一步确认我们的 Data 是 8-bit 还是 9-bit,这会再下面做出说明。

MCU

MCU接口标准名称是 I80 ,因为主要针对单片机的领域在使用,因此得名。后在中低端手机大量使用,其主要特点是价格便宜的。MCU-LCD接口的标准术语是 Intel 提出的 8080 总线标准,因此在很多文档中用 I80 来指 MCU-LCD 屏。

MUC接口主要又可以分为 8080 模式和 6800 模式,这两者之间主要是时序的区别;数据位传输有 8 位, 9 位, 16 位, 18 位, 24 位; 连线分为:CS/,RS(寄存器选择),RD/,WR/,再就是数据线了。

优点是:控制简单方便,无需时钟和同步信号。缺点是:要耗费 GRAM ,所以难以做到大屏(3.8寸以上)。对于 MCU 接口的 LCM ,其内部的芯片就叫 LCD 驱动器。主要功能是对主机发过的数据/命令,进行变换,变成每个象素的RGB数据,使之在屏上显示出来。这个过程不需要点、行、帧时钟。

详细可看 https://blog.csdn.net/qq_28986985/article/details/88546029

I(intel)8080模式

  • CS 片选信号
  • RS (D/I 数据/指令选择线, 置1为写数据, 置0为写命令)
  • /WR (为0表示写数据)
  • /RD (为0表示读数据)
  • RESET 复位LCD(用固定命令系列 0 1 0来复位)

M(Motorola) 6800 模式

M6800模式支持可选择的总线宽度 8/9/16/18-bit (默认为8位),其实际设计思想是与 I80 的思想是一样的,主要区别就是该模式的总线控制读写信号组合在一个引脚上(/WR),而增加了一个锁存信号(E)数据位传输有8位,9位,16位和18位。

事实上关于这种我们可以直接在数据手册中的时序图得到理解,它们一般也会一起提供对应的 timing 或者 pause ,翻译过来都是时序的意思,后者有时序片段的意思。

可以看出它有一根 DC 脚,如果有这个脚则意味着,在代码上要通过控制这个引脚来完成本次传输的数据是 命令 Command 还是 数据 Data 了,而所谓的 9-bit 就是指这个省略了这个引脚,在数据 D0:7 部分改成了 D0:8 ,其中第一个数据用做命令和数据的标识区分,如下图所示。

也就是说,它的主体逻辑其实很简单,就 RST 控制硬件复位、DC 引脚只要能区分数据就行,可有可无(如 9-bit 不需要),接着是 WR 用于提示该发送 D0:X 的数据了,以及 RD 提醒 IC 可以发数据过来给我接收了。

剩下的无非就是具体问题具体分析,遇到问题仔细对时序就好了,例如下图这样。

这并不难理解,让我们继续。

SPI

在这之前我一直认为 SPI 就是指 摩托罗拉公司推出的标准 SPI 协议,标准的 CS \ MOSI \ MISO \ CLK 四个引脚,但后来才知道有如下拓展 SPI 接口。

扩展SPI协议 https://cloud.tencent.com/developer/article/1486642

但其实最重要的是 SPI 的翻译为 spi serialport interface 接口,意味着就是串行数据通信的方式,这与 Parallel Interface 是 并行接口 相对,这在下面的说明就会体现出来了。

4 线串行接口( 4 线 SPI)

串行接口由串行时钟 SCL,串行数据 SI,A0 和CS 组成。在 SCL 的每个上升沿按照 D7,D6,......和 D0 的顺序将 SI 移入 8 位移位寄存器。每隔 8 个时钟对 A0 进行采样,并将移位寄存器中的数据字节同一时钟写入显示数据 RAM (A0 = 1)或命令寄存器( A0 = 0)。

3 线串行接口( 3 线 SPI)

3 线串行接口由串行时钟 SCL,串行数据 SI 和CS 组成。在 SCL 的每个上升沿按照 D/ C ,D7,D6,......和 D0 的顺序将 SI 移入 9 位移位寄存器。 D/ C 位(9 位中的第一位)将确定传输的数据被写入显示数据 RAM (D/ C = 1)或命令寄存器( D/ C = 0)。

不难看出,它和 8-bit 与 9-bit 的区别有异曲同工之妙,但为什么它叫 SPI 呢,实际上这是与并行相对的地方,比如将 D0:7 的数据压缩到了一条 SI 通道中传输了。

I2C

I2C 并不是每个屏幕都会支持,因为它接口简单,速度不快,所以只会出现在一下微小的屏幕上,但不同的是,它要通过硬件来标记从机产生的地址。

拿 SH1106 为例来说,它支持读写访问。 R/ W 位是从机地址的一部分。 在 I2C 总线上传输任何数据之前, 应首先寻址应响应的设备。 SH1106 保留两个 7 位从地址( 0111100 和 0111101 )。通过将输入 SA0 连接到逻辑 0(VSS)或 1(VDD1)来设置从地址的最低有效位。

至今位置的接口传输的数据基本都没有太大的区别,都是传输像素点的颜色数据。

RGB

现在就说点不同的屏幕接口,通常 RGB 接口结构,区别有下述。

图片资料来源 https://blog.csdn.net/vrk731/article/details/85221189

对于RGB接口的LCM,主机输出的直接是每个象素的RGB数据,不需要进行变换(GAMMA校正等除外),对于这种接口,需要在主机部分有个LCD控制器,以产生RGB数据和点、行、帧同步信号。

也就是说,传统的MCU屏显示数据写入DDRAM,而RGB屏数据不写入DDRAM,直接写屏,读写速度更快。

VSYNC 模式:

该模式其实就是就是在MCU模式上加了一个VSYNC信号,应用于运动画面更新,这样就与上述两个接口有很大的区别。该模式支持直接进行动画显示的功能,它提供了一个对MCU接口最小的改动,实现动画显示的解决方案。在这种模式下,内部的显示操作与外部VSYNC信号同步。可以实现比内部操作更高的速率的动画显示。但由于其操作方式的不同,该模式对速率有一个限制,那就是对内部SRAM的写速率一定要大于显示读内部SRAM的速率。

通常我们在 LCD 数据手册中看到的多数指这种 VSYNC 模式,了解它的工作机制也是一样通过看数据手册才能得知,如下图所示。

在理解上图之前,需要确定下述两个概念。

垂直同步脉冲(Vertical synchronization, Vsync)是加在两帧之间。跟水平同步脉冲类似,但它指示着前一帧的结束,和新一帧的开始。 垂直同步脉冲是一个持续时间比较长的脉冲。

水平同步脉冲(Horizontal synchronization pulse, Hsync)加在两个扫描行之间。它是一个短小的脉冲,在一行扫描完成之后,它就会出现,指示着这一行扫描完成,同时它也指示着下一行将要开始。

来源百度百科 https://baike.baidu.com/item/垂直同步/7263524?fr=aladdin

然后我附图说明这个关系。

但是很可惜的是,这里我无法解释的更为细致的原因了,想知道更多的,只能请你自己去查阅 ILI9342C 数据手册的 VSYNC Interface 一章中对时序的计算和屏幕的扫描方式,要特别注意 Back porch (2 lines) 和 Front porch (2 lines) 的用途。

当然,这个你可以看这份资料来理解为什么。 https://www.cnblogs.com/biglucky/p/4142505.html

VSYNC Interface 的 RGB 接口要看懂还挺难的,我看了一点就懵逼了,还有公式换算,应该是期望在 VSYNC 同步触发前应当把 帧 数据写入屏幕,原谅我没有真正的实践这个,只能理解到这里了。

LCD 屏幕的像素点与颜色值定义的关系

如果你已经得知如何去确认通信接口和模式,那么就要确认的就是通信的数据格式了,通常我们的通信的总线通道(D0:7)并没有那么宽,但我们也一样可以传输更多的数据,这就需要去看数据手册的数据定义了,它很贴心的把 RGB 颜色的范围定义通过颜色表示出来了。

实际上我们传输的数据假设为一个 16bit 的变量,则如下图还原,其他同理。

其实我们通常使用的 16-bit 的 color 值就是指 RGB16 的意思。

RGB16

所以这段魔幻代码的操作你就知道为什么了。

//获取高字节的5个bit
R = color & 0xF800;
//获取中间6个bit
G = color & 0x07E0;
//获取低字节5个bit
B = color & 0x001F;

RGB32

看这里有更多的解释 https://blog.csdn.net/byhook/article/details/84262330

关于 BGR 的问题

是 RGB 还是 BGR 这个问题还是历史遗留因素,那如果真的发现颜色值不对的时候,应当检查寄存器的配置,确认颜色模式。

因为这个屏幕考虑到早期的代码因素,所以才会存在这种差异性,这也意味着我们后来写的代码,还是得分清楚,是 RGB 还是 BGR。

至此你已经知道了,屏幕通信过程中的颜色值情况,也就明白了如何应该如何传输像素点的颜色值,剩下的就是如何传输的功能问题了。

LCD 屏幕的初始化流程

这个不同屏幕有不同的初始化流程,但都有一个标准和大概的范围,也和硬件有关,有时候还不如不接,让屏幕自行进入初始化?(迷惑

我分别截图几个手册的情况和举实例吧。

其实不难,主要是硬件方面设计好就可以了。

关于 I8080 传输数据的细节

需要注意的是,这个传输数据对时序的要求还挺高的。

LCD 屏幕绘图接口的主体逻辑

设置填充的区域

设置一下行地址和列地址的起始位置和停止位置。

void lcd_set_area(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
    uint8_t data[4] = {0};

    x1 += lcd_ctl.start_offset_w;
    x2 += lcd_ctl.start_offset_w;
    y1 += lcd_ctl.start_offset_h;
    y2 += lcd_ctl.start_offset_h;

    data[0] = (uint8_t)(x1 >> 8);
    data[1] = (uint8_t)(x1);
    data[2] = (uint8_t)(x2 >> 8);
    data[3] = (uint8_t)(x2);
    tft_write_command(HORIZONTAL_ADDRESS_SET);
    tft_write_byte(data, 4);

    data[0] = (uint8_t)(y1 >> 8);
    data[1] = (uint8_t)(y1);
    data[2] = (uint8_t)(y2 >> 8);
    data[3] = (uint8_t)(y2);
    tft_write_command(VERTICAL_ADDRESS_SET);
    tft_write_byte(data, 4);

    tft_write_command(MEMORY_WRITE);
}

填充内容的方式

如果是 fill 的操作的话,将持续发送该颜色填充到指定的区域即可。

void lcd_fill_rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
    if((x1 == x2) || (y1 == y2))
        return;
    uint32_t data = ((uint32_t)color << 16) | (uint32_t)color;
    lcd_set_area(x1, y1, x2-1, y2-1);
    tft_fill_data(&data, (x2 - x1) * (y2 - y1) / 2);
}

如果是 draw 的操作的话,将缓冲区中的颜色数据持续的到指定的区域即可完成数据的显示。

void lcd_draw_picture(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint32_t *ptr)
{
    lcd_set_area(x1, y1, x1 + width - 1, y1 + height - 1);
    tft_write_word(ptr, width * height / 2);
}

GC9308 调试示例与后记

补充说明,后来的 LCD 屏幕都多了一个叫 LUT 的映射表,实际上就是一个 16-bit to 18-bit color depth conversion 的转换函数。

软件方面比较麻烦的地方在于 SPI 的通信模式、像素点的颜色值转换、驱动的基础颜色定义、驱动的寄存器配置,基本上确认了这些大多数问题都可以解决。

硬件方面设计有缺陷,个人判断应该是功耗不足,屏幕闪白线或起雾,看起来就好像要断电熄灭了一样。

唯一不满意的地方就是没有办法量出 K210 输出 并口数据 的信号,因为没有转接板可以接出来,这令我十分苦恼。

想知道更多,或者学习调试,不妨看看这份资料 RGB接口和MPU接口区别 https://blog.csdn.net/special00/article/details/78128374

posted @ 2020-06-09 02:37  Juwan  阅读(19165)  评论(2编辑  收藏  举报