1 LCD硬件原理#
1.1 LCD像素扫描#
里面的每个点就是一个像素点。
它里面有一个电子枪,一边移动,一边发出各种颜色的光。用动态图表示如下:
电子枪是如何移动的?
有一条CLK时钟线与LCD相连,每发出一次CLK(高低电平),电子枪就移动一个像素。
颜色如何确定?
由连接LCD的三组线RGB三原色混合而成:R(Red)、G(Green)、B(Blue)确定。
电子枪如何得知应跳到下一行?
有一条HSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到下一行,该信号叫做行同步信号。
电子枪如何得知应跳到原点?
有一条VSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到原点,该信号叫做帧同步信号。
RGB线上的数据从何而来?
内存里面划分一块显存(FrameBuffer),里面存放了要显示的数据,LCD控制器从里面将数据读出来,通过RGB三组线传给电子枪,电子枪再依次打到显示屏上。
前面的信号由谁发给LCD?
有S3C2440里面的LCD控制器来控制发出信号。
1.2 LCD硬件原理图#
①是时钟信号,每来一个CLK,电子枪就移动一个像素;
②是用来传输颜色数据;
③是垂直方向同步信号,FRAME(帧);
④是水平方向同步信号,LINE(行);
⑤LED+、LED-背光灯电源。
⑥TSYP、TSXP、TSYM、TSXM是触摸屏信号,暂时不用。
⑦VM接DE是数据使能
1.2.1 RGB LCD模式#
HV模式: HS与VS来控制刷新。比如对于分辨率为1024x600RGB的LCD,LCD控制器发出HS信号后,就会发出1024个DCLK,在每个DCLK上传输像素数据;当发出600个HS信号后,就会发出一个VS信号
DE模式:DE信号来控制刷新,比如对于分辨率为1024x600RGB的LCD,LCD控制器发出DE信号后,就要发出1024个DCLK,在每个DCLK上传输像素数据;当发出600个DE信号,刷新完一帧数据
1.2.2 LCD时序分析#
①从一行最开始的像素开始分析,如上图标号①,DE信号开始有效,电子枪每次在CLK下降沿时从数据线Dn0-Dn7上得到数据(Dn0-Dn7上的数据来源于FrameBuffer,后面会讲),然后发射到显示屏上,然后移动到下一个位置。从1 st pixel到last pixel,就这样从一行的最左边,一直移动到一行的最右边,完成了一行的显示,假设一行有x个pixel。可以看到每发送一个pixel,需要1个时钟周期(1/tc)。
②当打完一行的最后一个数据后,会收到Hsync行同步信号,那么电子枪会跳到下一行,如上图标号②,根据时序图,一个Hsync周期,也就是一行数据刷新时间th, 可以大致分为五部分组成:thp、thb、1/tc、thd、thf。
thp:称为脉冲宽度,这个时间不能太短,太短电子枪可能识别不到。
thb:电子枪正确识别到thp后,会从最右端移动最左端,这个移动的时间就是thb,称之为移动时间。
thd:表示显示一行数据的时间
thf:表示显示完最右像素,再过多久Hsync才来。
③同理,当电子枪移动到最后一行时,就会发送一个Vsync垂直同步信号,让电子枪移动回最上边。如上图标号③,根据时序图,一个Vsync周期,也就是一帧数据刷新时间tv, 可以大致分为:tvp、tvb、tvd、tvf。
tvp:Vsync信号的脉冲宽度
tvb:电子枪从最后一行移动到第一行的移动时间
tvf:表示显示完最后一行像素,再过多久Vsync才来。
假设一共有y行,那么LCD的分辨率就是x*y。
下面是LCD显示配置示意图:
从左往右看,可以看到Total width = HSYNC width + HBP + Active width + HFP。当发出一个HSYNC信号后,电子枪就会从最右边花费HBP时长移动到最左边,等到了最右边后,等待HFP时长后下一轮HSYNC信号才会发出。因此,HBP和HFP分别决定了左边和右边的黑框。
HSYNC是行同步信号的脉冲宽度(低电平有效)
HBP表示屏幕左边黑框的宽度(电子枪要花多久才能从最右边移动到最左边)行后肩
Active width表示有效数据宽度
HFP表示屏幕右边黑框的宽度(再过多久HSYNC才会发出)行前肩
同理从上往下看,Total height = Vsync width + VBP + Active width + VFP。当发出一个VSYNC信号后,电子枪就会从最下边花费VBP时长移动到最上边,等到了最下边后,等待VFP时长后下一轮VSYNC信号才会发出。因此,VBP和VFP分别决定了上边和下边的黑框。 中间灰色区域才是有效显示区域。
VSYNC是帧同步信号的脉冲宽度(低电平有效)
VBP表示屏幕上边黑框的宽度(电子枪要花多久才能从最后一行移动到最上面一行)帧后肩
Active height表示有效数据高度
VFP表示屏幕下边黑框的宽度(再过多久VSYNC才会发出) 帧前肩
总结:
1.2.2.1 行时序#
HSPW:有些地方也叫做 thp,是 HSYNC 信号宽度,也就是 HSYNC 信号持续时间。HSYNC信号不是一个脉冲,而是需要持续一段时间才是有效的,单位为 CLK。
HOZVAL:有些地方叫做 thd,显示一行数据所需的时间,假如屏幕分辨率为 1024*600,那么 HOZVAL 就是 1024,单位为 CLK。
1.2.2.2 帧时序#
VSPW:有些地方也叫做 tvp,是 VSYNC 信号宽度,也就是 VSYNC 信号持续时间,单位为 1 行的时间
LINE:有些地方叫做 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,那么 LINE 就是 600 行的时间。
1.2 FrameBuffer和BPP概念#
FrameBuffer是在内存中的一段区域,这段区域专门用来存放颜色数据的。如下图:
BPP(Bits Per Pixels)表示每个像素占据多少位。 前面的LCD引脚功能图里,有R0-R7、G0-G7、B0-B7,那么每个像素是占据38=24位的,*所以硬件上LCD的BPP是确定的.
那么在FrameBuffer中,每个像素在FrameBuffer中,占据多少位BPP(Bits Per Pixels)?
虽然LCD上的引脚是固定的,但我们使用的时候,可以根据实际情况进行取舍,查看我们的硬件原理图,发现我们的LCD硬件上只有R1-R5、G0-G5、B1-B5与SOC相连,5+6+5=16BPP,所以每个像素就只占据16位数据。等效连接图如下:
1.3 LCD种类#
S3C2440芯片手册介绍了LCD控制器支持TFT和STN两种LCD,我们常用的都是TFT材质的,本开发板采用的就是一款TFT材质的LCD.
1.4 LCD访问框架#
如下图,LCD控制器从SDRAM中的FrameBuffer区域取出颜色数据,发送给电子枪,电子枪按照特定的时钟周期将颜色数据显示在LCD上。
2 LCD控制器#
2.1 s3c2440 LCD控制器框图#
S3C2440 LCD控制器用于传输视频数据并且生成必要的控制信号,如VFRAME,VLINE,VCLK,VM等。除了控制信号,S3C2440还有视频数据端口,即VD [23:0]。通过设置REGBANK(寄存器组),LCDCDMA会自动(无需CPU参与)把内存上FrameBuffer里的数据,通过VIDPRCS发送到引脚VD[23:0]数据总线上,再配合VIDEOMUX引脚的控制信号,正确的显示出来。
1 | REGBANK:具有17个可编程寄存器组和256x16调色板存储器,用于配置LCD控制器。 |
总结LCD控制器主要功能如下:
1 | 1. 取:从内存(FrameBuffer)取出某个像素的数据(之后需要把FrameBuffer地址、BPP、分辨率告诉LCD控制器) |
2.2 寄存器介绍#
2.2.1 数据存储格式#
可以配置寄存器的BSWP、HWSWP来设置Framebuff中的像素存储格式。
2.2.1.1 BSWP/HWSWP寄存器#
2.2.2.1.1 24BPP#
从图中可以看到24bpp的像素,在lcd控制器的VD[7:0]表示BLUE, VD[15:8]表示GREEN,VD[23:16]表示RED。在内存中的FrameBuffer中每一个像素占据4个字节,当BPP24BL=0时,低24位为颜色数据,当BPP24BL=1时,高24位为颜色数据。
2.2.2.1.2 16BPP#
也可以看到16bpp的像素,在内存中的FrameBuffer中每一个像素占据2个字节,HWSWP用来设置像素数据的存放方式。
再看下LCD控制器的VD引脚输出情况,可以看到16bpp时分5:6:5和5:5:5:i两种数据格式。当5:6:5模式时,VD[7:3]表示BLUE, VD[15:10]表示Green数据,VD[23:19]表示RED。当5:5:5:i模式时,VD[7:3]表示BLUE, VD[15:11]表示Green,VD[23:19]表示RED。其中i表示透明度。
2.2.2.1.3 8BPP#
2.2.2 调色板寄存器#
我们外接的LCD硬件上只有R1-R5、G0-G5、B1-B5与SOC相连,5+6+5=16BPP,所以LCD上每个像素就只占据16位数据。那么当我们的Frame buffer中是8BPP颜色数据时,是如何把颜色数据填充到LCD上的呢?
用调色板
S3C2440A 中的 TFT LCD 控制器支持 1、2、4 或 8bpp调色显示(伪彩色)和16、24bpp无调色显示(真彩色)。S3C2440A 可以支持 256 色调色板给各种色彩映射的选择,以提供灵活操作给用户。
假如是16BPP的数据,LCD控制器从FB取出16bit数据,显示到LCD上,如下图所示:
那么当8BPP的数据时,就需要用到调色板,调色板里存放了256个16bit的数据,FB(frame buffer)只存放每个像素的索引,根据索引去调色板找到对应的数据传给LCD控制器,比如从FB中的第0个元素拿到调色板中的第0个16bit数据,再通过电子枪显示出来,如下图所示:
调色板支持 5:6:5(R:G:B)格式和 5:5:5:I(R:G:B:I)格式。当用户使用5:5:5:I格式时,I表示强度,也就是透明度。I是用作每个RGB 数据的共用 LSB 位,因此 5:5:5:I与R(5+I):G(5+I):B(5+I)格式相同。
2.2.2.1 调色板格式#
0x4D000400为调色板起始地址:
2.2.3 LCD控制寄存器1#
1 | [27:18]为只读数据位,不需要设置; |
2.2.4 LCD控制寄存器2(垂直方向参数)#
s3c2440 LCD控制器时序图如下:
1 | [31:24] : VBPD = tvb - 1 (表示显示完最后一行像素,再过多久Vsync才来,表示上边黑框) |
2.2.5 LCD控制寄存器3(水平方向参数)#
1 | [25:19] : HBPD = thb - 1(左边黑框) |
2.2.5 LCD控制寄存器4#
1 | [7:0]: HSPW = thp - 1 (Hsync信号的脉冲宽度) |
2.2.5 LCD控制寄存器5#
1 | [12] : BPP24BL(表示24bpp的数据是大端还是小端) |
2.2.6 LCDSADDR1寄存器#
frame buffer的起始地址寄存器:
1 | [29:21] : LCDBANK, A[30:22] of fb |
2.2.7 LCDSADDR2寄存器#
frame buffer的结束地址寄存器:
1 | [20:0] : LCDBASEL,A[21:1] of end addr,即framebuffer的结束地址。 |
3 LCD裸机编程#
3.1 软件框架#
为了让程序更加好扩展,体现出高内聚、低耦合的特点,能够兼容各种不同型号的lcd,假如有两款尺寸大小的lcd,如何快速的在两个lcd上切换?
首先我们抽象出lcd_3.5.c和lcd_4.3.c的共同点,比如都有初始化函数init(),我们可以新建一个lcd.c,然后定义一个结构体:
1 | struct lcd_opr{ |
用户不接触lcd_3.5.c和lcd_4.3.c,只需要在lcd.c里通过指针访问对应的结构体的函数,也就调用了不同init():
我们的目的是在LCD显示屏上画线、画圆(geomentry.c)和写字(font.c)其核心是画点(farmebuffer.c),这些都属于纯软件。此外还需要一个lcd_test.c测试程序提供操作菜单,调用画线、画圆和写字操作。
往下操作的是LCD相关的内容,不同的LCD,其配置的参数也会不一样,通过lcd_3.5.c或lcd_4.3.c来设置属性参数。
根据LCD的特性,来设置LCD控制器,首先编写lcd_controller.c,它向上要接收不同LCD的参数,向下要使用这些参数设置对应具体的某一款LCD控制器。
对于我们开发板,就是s3c2440_lcd_controller.c,假如希望在其它开发板上也实现LCD显示,只需添加相应的代码文件即可。文件自上而下的框架如下:
3.2 数据结构定义#
3.2.1 LCD设备结构体#
我们知道LCD的参数属性有:引脚的极性、时序、数据的格式bpp、分辨率等,使用面向对象的思维方式,将这些封装成结构体放在lcd.h中:
1 | enum { |
3.3 操作方法定义#
3.3.1 LCD操作方法-lcd_controller.c#
我们知道在c++中是面向对象编程的,那么一个对象就有它的属性和方法,LCD属性我们上面已经定义好了,那么方法我们可以定义一个lcd_controller.c用来控制管理LCD,定义个一个lcd_controller.h, struct lcd_controller结构体放置lcd对象的一些成员函数,即对象的方法,或者称之为对象的行为:
1 | typedef struct lcd_controller { |
那么lcd_controller.c相当于一个管理者,会去选择具体型号的LCD对象去执行具体的成员函数,比如管理s3c2440_lcd_controller.c,它向上接受传入的LCD参数,向下传给具体的LCD控制器。
1 | void lcd_controller_init(p_lcd_params plcdparams) { |
这样在s3c2440_lcd_controller.c再构造一个具体的lcd对象:
1 | struct lcd_controller s3c2440_lcd_controller = { |
lcd_controller.c代码框架如下:
1 |
|
下面详细分析lcd_controller.c框架的含义以及作用:
- 开始定义了一个p_array_lcd_controller数组和g_p_lcd_controller_selected,p_array_lcd_controller数组表示lcd控制器的集合,g_p_lcd_controller_selected表示被选中的那一个lcd_controller;
- 当我们初始化时要先调用register_lcd_controller,select_lcd_controller选中具体的lcd_controller;
- 然后才能调用lcd_controller_init初始化具体的lcd_controller,去控制具体型号的lcd。
同理,也通过lcd.c去管理lcd_4.3.c,思路如下:
1 | a. 有一个数组存放各类lcd的参数; |
3.3.2 具体型号LCD管理-ldc.c#
参考前面的lcd_controller.c编辑lcd.c如下:
1 |
|
3.4 LCD初始化#
3.4.1 初始化lcd控制器#
3.4.1.1 初始化引脚#
3.4.1.1.1 背光引脚#
我们配置LCD的背光引脚成输出模式:
1 | GPBCON &= ~0x3; |
3.4.1.1.2 控制引脚和数据引脚#
然后再配置LCD的控制引脚和数据引脚,LCD控制引脚和数据引脚分别复用了GPC和GPD,如下图所示:
设置GPC, GPD均为0xaaaa,aaaa。
1 | /* LCD专用引脚 */ |
3.4.1.1.3 PWREN引脚#
设置GPG4成PWREN引脚
1 | GPGCON |= (3<<8); |
3.4.1.2 初始化LCD控制寄存器、地址寄存器#
前面介绍了LCDCON1,LCDCON2,LCDCON3…LCDSADDR1等寄存器,代码如下:
1 | void s3c2440_lcd_controller_init(p_lcd_params plcdparams) { |
3.4.1.3 使能、禁用背光引脚#
根据背光电路背光引脚是GPB0,那么配置GPBDAT[0]置1,使能背光引脚,设置LCDCON5和
LCDCON1使能power enable和LCD输出,反之。代码如下:
1 | void s3c2440_lcd_controller_enalbe(void) { |
这样我们的s3c2440的lcd控制器初始化就编写完了,那么用户只要调用s3c2440_lcd_controller_init去设置LCD的属性即可。下面开始介绍如何设置LCD属性,让LCD控制器能够适应具体型号的LCD。
1 | struct lcd_controller s3c2440_lcd_controller = { |
3.4.2 初始化lcd设备#
参考AT043TN24 LCD数据手册上的参数性能,见下表:
配置lcd_params属性如下:
1 |
|
1 | .de表示数据输出使能引脚,高电平有效,所以配置成NORMAL; |
什么是高低脉冲?
1 | 高脉冲:即从逻辑0变化bai到逻辑du1再变化到逻辑0,如此便是一个高脉zhi冲。在单片机中定义高脉冲就是让某个I/O先输出逻辑0,接着保持一定的时间(延时),再输出逻辑1,同样保持一定的时间(延时),最后再转变输出为逻辑0+延时。 |
1 | .vsync表示帧同步信号,同.hsync; |
那么最终LCD初始化函数封装如下:
1 | void lcd_init(void) { |
总结:我们可以看到,调用的函数都是一些通用型框架型接口,具体的实现本质还得根据硬件本身的特性来配置寄存器来驱动硬件工作。
3.5 实现显示功能#
3.5.1 LCD显示满屏红色#
想要在LCD上显示出数据,所需步骤如下:
1 | a. 初始化LCD |
3.5.1.1 初始化LCD#
前面已详细实现。
3.5.1.2 使能LCD#
1 | void lcd_enable() { |
3.5.1.3 获取LCD参数#
1 | void get_lcd_params(unsigned int *fb_base, int *xres, int *yres, int *bpp) { |
3.5.1.4 往framebuffer中写数据#
假设我们初始化配置了BPP=16,那么如何让全屏显示红色?
就需要从framebuffer基地址开始的整个屏幕的像素点都填充红色值。 对于16BPP,RGB=565,想显示红色,即[15:11]全为1表示红色,[10:5]全为0表示无绿色,[4:0]全为0表示无蓝色,0b1111100000000000=0xF800。
以基地址为起点,分别以xres和yres为边界,依次填充颜色。
1 | p = (unsigned short *)fb_base; |
假设我们初始化配置了BPP=24 或者BPP =32,那么如何让全屏显示红色?
其实无论是24bpp还是32bpp,在frame buffer中每个像素点都占4 bytes,对于24BPP or 32 bpp,即RGB:888,每个颜色占8位,一共占据24位。代码如下:
1 | p = (unsigned int *)fb_base; |
当Frame buffer中填满颜色数据时,LCD控制器会参照我们之前的配置将数据填充到LCD显示器上。那前面的24BPP、32BPP是怎样在 只能接收16BPP(硬件上只有16根数据线)的LCD上显示的呢?
1 | 这是因为在使用24BPP时,发出的8条红色,8条绿色,8条蓝色数据,只用了高5条红色,高6条绿色,高5条蓝色与LCD相连。(前面LCD硬件原理的FrameBuffer和BPP概念有讲) |
3.6 实现绘制点线圆函数#
3.6.1 画点#
无论是何种图形,都是基于点来构成的,因此我们需要先实现画点,其他的都是上层的一些数据处理了,像各种图形、甚至色彩鲜艳的图片无非都是一些由点构造出的数据而已。
我们在在farmebuffer.c实现画点,在geomentry.c实现画线、画圆等几何图形,font.c实现画字。
那么一个像素点要显示到lcd上,我们要知道它的位置坐标,然后还要知道它的颜色值,假设该像素点的坐标为(x,y),那么该像素的地址为:
1 | (x,y)= fb_base + (xres*(bpp/8))*y +x*bpp/8; |
那么所以在画点前需要先获取lcd参数:fb_base、xres、yres、bpp;
1 | static unsigned int fb_base; |
然后画点函数如下:
1 | static unsigned int fb_base; |
用户传入的颜色数据一般都是32bit的,即格式为:0x00RRGGBB。
1 | 对于8PP,通过的是调色板索引实现的,这个后续再讲解,直接*pc = color即可(这样只取了高8位,低精度的数据就丢了)。 |
3.6.2 32bppto16bpp函数#
1 | //先分别取出RGB,再相应的清除低位数据,实现将RGB888变为RGB565 |
3.6.3 画线画圆#
画圆画线的具体原理不是本主题的重点,这些属于研究算法的范畴了,比如这里就有现成的算法可以用,如这篇博客:https://blog.csdn.net/p1126500468/article/details/50428613,里面有画圆画线的函数实现,直接使用就可以了,套用画点的”轮子”就可以了。
3.6.4 测试#
新建一个geometry.c,复制博客中代码,替换里面的描点显示函数即可。
1 | /* 画线 */ |
3.7 字符库移植#
字符也是由点构成的,一个个点组成的点阵,其实本质上要显示文字就是把字库移植到对应的自己型号相匹配的board上,字库中的每一个字符都是一些点按照对应格式组合成的集合。
从linux内核源码中随便挑选一个字库文件,比如linux-4.18.16/lib/fonts这个目录下就有对应的很多字库文件。在这里我挑选font_8x16.c,如下图:
其中8x16表示每个字符所占的像素点的大小,表示每个字符占的大小为长*宽=8*16个像素点。
我们来看下一个字符’A’是如何显示的?从font_8x16.c我们找到字符’A’的数据,如下图:
那么我们如何让font_8x16.c这个字库的数据显示到lcd上呢?font_8x16.c见附件。
1 | /* |
在font_8x16.c里面,每个字符占据16字节,因此想要根据ascii码找到对应的点阵数据,需要对应的乘16,再取地址,得到该字符的首地址。
在显示之前,还需要获取LCD参数:
1 | extern const unsigned char fontdata_8x16[]; |
3.7.1 显示字符串#
如果想显示字符串,那就在每显示完一个字符后,x轴加8即可,同时考虑是否超出屏幕显示范围进行换行处理:
1 | /* "abc\n\r123" */ |