显示器的基本参数
不管是哪一种显示器,都有一定的参数用于描述它们的特性,各个参数介绍如下:
参数 | 说明 |
---|---|
像素 | 像素是组成图像的最基本单元要素,显示器的像素指它成像最小的点,即前面讲解液晶原理中提到的一个显示单元。 |
分辨率 | 一些嵌入式设备的显示器常常以“行像素值 x 列像素值”表示屏幕的分辨率。如分辨率 800x480 表示该显示器的每一行有 800 个像素点,每一列有 480 个像素点,也可理解为有 800 列, 480 行。 |
色彩深度 | 色彩深度指显示器的每个像素点能表示多少种颜色,一般用“位” (bit) 来表示。如单色屏的每个像素点能表示亮或灭两种状态 (即实际上能显示 2 种颜色),用 1 个数据位就可以表示像素点的所有状态,所以它的色彩深度为 1bit,其它常见的显示屏色深为16bit、 24bit。 |
显示器尺寸 | 显示器的大小一般以英寸表示,如 5 英寸、 21 英寸、 24 英寸等,这个长度是指屏幕对角线的长度,通过显示器的对角线长度及长宽比可确定显示器的实际长宽尺寸。 |
点距 | 点距指两个相邻像素点之间的距离,它会影响画质的细腻度及观看距离,相同尺寸的屏幕,若分辨率越高,则点距越小,画质越细腻。如现在有些手机的屏幕分辨率比电脑显示器的还大,这是手机屏幕点距小的原因; LED 点阵显示屏的点距一般都比较大,所以适合远距离观看。 |
本喵使用的是一个4.3存的TFTLCD液晶屏。它的分辨率是480*800,通过16位并口驱动,也就是我们所说的8080并口,是一款电容触摸屏。
这是它的实物图,具体的构成本喵就不作讲解了,有兴趣的小伙伴可以查阅相关资料自行了解。
液晶显示原理:
以本喵使用的分辨率为480*800的液晶屏为例,下是它的示意图
红色框线是显示屏的边界线,里面的黑色框是一个一个的像素点,这样的像素点一共有800*480个。
它的显示过程就是将屏幕中的像素点用特定的颜色填充,比如我们在屏中显示一个字母F
将像素点从左到右从上到下按顺序填充,其中填充颜色为红色的所有点显示出来的就是一个红色的F,在液晶屏中,因为像素点间的点距很小,所以我们看到的F是是连着的,中间没有间隙。
在上面的简介中介绍过颜色深度这一个参数,这里我们使用的是RGB565格式的数据:
- RGB 信号线的数量分别是5根,6根,5根,分别用于表示液晶屏一个像素点的红、绿、蓝颜色分量。使用红绿蓝颜色分量来表示颜色是一种通用的做法。
打开 Windows 系统自带的画板调色工具,可看到颜色的红绿蓝分量值,如图中R(红色)分量,G(绿色)分量,B(蓝色)分量,分别是50,25,30。
常见的颜色表示会在“RGB”后面附带各个颜色分量值的数据位数,如 RGB565 表示红绿蓝的数据线数分别为 5、6、 5 根,一共为 16 个数据位,可表示 216 种颜色。
显存:
本喵使用的芯片是STM32F1系列的,它的内部SRAM是比较小的,所以需要使用扩展SRAM来缓存像素点的数据,本喵使用的LCD液晶有一个单独的控制器,这个控制器就是ILI9431芯片,它里面有一个GRAM的存储器。下面来看它的详细介绍。
该芯片最主核心部分是位于中间的 GRAM(Graphics RAM),它就是显存。 GRAM 中每个存储单元都对应着液晶面板的一个像素点。它右侧的各种模块共同把 GRAM 存储单元的数据转化成液晶面板的控制信号,使像素点呈现特定的颜色,而像素点组合起来则成为一幅完整的图像。
框图的左上角为 ILI9341 的主要控制信号线和配置引脚,根据其不同状态设置可以使芯片工作在不同的模式,如每个像素点的位数是 6、 16 还是 18 位;可配置使用 SPI 接口、 8080 接口还是 RGB接口与 MCU 进行通讯。
MCU 通过 SPI、 8080 接口或 RGB 接口与 ILI9341 进行通讯,从而访问它的控制寄存器 (CR)、地址计数器 (AC)、及 GRAM。
在 GRAM 的左侧还有一个 LED 控制器 (LED Controller)。 LCD 为非发光性的显示装置,它需要借助背光源才能达到显示功能, LED 控制器就是用来控制液晶屏中的 LED 背光源。
所以说,我们只需要控制这个LIL9341控制芯片就可以了,这个芯片会自动的区控制LCD液晶屏幕,显示相应的数据。
ILI9341 控制器根据自身的 IM[3:0] 信号线电平决定它与 MCU 的通讯方式,它本身支持 SPI 及8080 通讯方式,本示例中液晶屏的 ILI9341 控制器在出厂前就已经按固定配置好 (内部已连接硬件电路),它被配置为通过 8080 接口通讯,使用 16 根数据线的 RGB565 格式。
这是LCD液晶的硬件图和引脚。
我们来看下8080接口的时序图:
- 由图可知,写命令时序由片选信号 CSX 拉低开始
- 数据/命令选择信号线 D/CX 也置低电平表示写入的是命令地址 (可理解为命令编码,如软件复位命令: 0x01)
- 写信号 WRX 为低,读信号 RDX 为高表示数据传输方向为写入,同时,在数据线 D[17:0](或 D[15:0]) 输出命令地址,在第二个传输阶段传送的是命令的参数,所以 D/CX 要置高电平,表示写入的是命令数据,命令数据是某些指令带有的参数,如复位指令编码为 0x01,它后面可以带一个参数,该参数表示多少秒后复位 (实际的复位命令不含参数,此处只是为了讲解指令编码与参数的区别)。
- 当需要把像素数据写入 GRAM 时,过程很类似SRAM扩展,把片选信号 CSX 拉低后,再把数据/命令选择信号线 D/CX 置为高电平,这时由 D[17:0] 传输的数据则会被 ILI9341 保存至它的 GRAM 中。
上面举例中的是8080接口的写时序,读时序和写几乎是一样的,只是方向相反。
ILI9341 的 8080 通讯接口时序可以由 STM32 使用普通 I/O 接口进行模拟,但这样效率太低,STM32 提供了一种特别的控制方法——使用 FSMC 接口实现 8080 时序。
FSMC的原理和使用本喵在文章【STM32】FSMC——扩展外部SRAM中详细的介绍,这里本喵就不再介绍,只介绍它是如何使用的,因为ILI9341中GRAM也是一种扩展的SRAM。
控制 LCD 时,是使用 FSMC 的 NORPSRAM 模式的,且与前面使用 FSMC 控制 SRAM 的稍有不同,控制 SRAM 时使用的是模式 A,而控制 LCD 时使用的是与 NOR FLASH 一样的模式 B,所以我们重点分析框图中 NOR FLASH 控制信号线部分。
对比 FSMC NOR/PSRAM 中的模式 B 时序与 ILI9341液晶控制器芯片使用的 8080 时序可发现,这两个时序是十分相似的 (除了 FSMC 的地址线 A 和8080 的 D/CX 线,可以说是完全一样)。
- 由于 FSMC 会自动产生地址信号,当使用 FSMC 向 0x6xxx xxx1、 0x6xxx xxx3、 0x6xxx xxx5…这些奇数地址写入数据时,地址最低位的值均为 1,所以它会控制地址线 A0(D/CX) 输出高电平,那么这时通过数据线传输的信号会被理解为数值;
- 若向 0x6xxx xxx0 、 0x6xxx xxx2、 0x6xxx xxx4…这些偶数地址写入数据时,地址最低位的值均为 0,所以它会控制地址线 A0(D/CX) 输出低电平,
因此这时通过数据线传输的信号会被理解为命令,见下表使用 FSMC 输出地址示例。
在配置结构体时,大部分与扩展SRAM的配置是一样的,只是有一些不同。
本成员设置存储器访问模式,不同的模式下 FSMC 访问存储器地址时引脚输出的时序不一样,可选 FSMC_AccessMode_A/B/C/D 模式。控制异步 NORFLASH 时使用 B模式。
本成员用于设置要控制的存储器类型,它支持控制的存储器类型为 SRAM、 PSRAM以及 NOR FLASH(FSMC_MemoryType_SRAM/PSRAM/NOR)。我们这里选用的是NOR FLASH。
在了解了LCD液晶屏与MCU的工作原理后就是来运用了。先看硬件连接图
本喵在文章【STM32】FSMC——扩展外部SRAM详细的列出了FSMC的控制引脚,地址引脚以及数据引脚对应的IO口,这里本喵就不再演示了。
图中圈出来的部分就是连接 TFTLCD 模块的接口,液晶模块直接插上去即可。
在硬件上, TFTLCD 模块与战舰 STM32F103 的 IO 口对应关系如下:
LCD引脚 | IO口 |
---|---|
LCD_BL(背光控制) | 对应 PB0 |
LCD_CS(片选信号) | 对应 PG12 即 FSMC_NE4; |
LCD _RS(数据/命令控制) | 对应 PG0 即 FSMC_A10 |
LCD _WR(写控制) | 对应 PD5 即 FSMC_NWE |
LCD _RD(读控制) | 对应 PD4 即 FSMC_NOE |
LCD _D[15:0] | 则直接连接在 FSMC_D15~FSMC_D0 |
这些线的连接,战舰 STM32 开发板的内部已经连接好了,我们只需要将 TFTLCD 模块插上去就好了。
1. 初始化通讯使用的目标引脚及端口时钟
void GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//将用到的IO口的时钟全部使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PB0 推挽输出 背光
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
//PORTD复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_14|GPIO_Pin_15; //PORTD复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
//PORTE复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15; //PORTE复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE, &GPIO_InitStructure);
//PORTG12复用推挽输出 P0RTG0-->RS
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_12; //PORTD复用推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOG, &GPIO_InitStructure);
}
这里是将所有用到的IO口配置好。
2. 配置 FSMC 为异步 NOR FLASH 模式以仿真 8080 时序
- 红色框是与扩展SRAM的不同点。
- 蓝色框是设置的ADDSET和DATAST的值。
ADDSET和DATAST的值这里采用的是经验值,分别是0x01和0x04,当然也可以根据时序图来计算
3. 建立机制使用 FSMC 向液晶屏发送命令及数据
首先是需要确定访问的地址,计算地址的过程如下:
根据最终的计算结果,总结如下:
- 当 STM32 访问内部的 0x6000 0400 地址时, FSMC 自动输出时序,且使得与液晶屏的数据/命令选择线 RS(即 D/CX) 相连的 FSMC_A10 输出高电平,使得液晶屏会把传输过程理解为数据传输;
- 类似地,当 STM32 访问内部的 0X6C00 0000 地址时, FSMC自动输出时序,且使得与液晶屏的数据/命令选择线 RS(即 D/CX) 相连的 FSMC_A23 输出低电平,使得液晶屏会把传输过程理解为命令传输。
我们将计算出的地址使用宏标记出来。
- 这里必须使用volatile关键字修饰地址,否则就会在编译的时候被优化,导致实验达不到效果。
- 这里将 0x6C00 0400中小于A10的地址都设为1,因为没有影响,但是将A0的地址设为0,所以结果就是 0x6C00 07FE
- 将这个地址强转为定义的结构体类型的指针,此时结构体中成员变量vu16 LCD_REG的地址就是 0x6C00 07FE,而vu16 LCD_RAM的地址就是 0x6C00 8000,此时A10是0
这样一来,我们使用结构体就可以做到对LCD传送命令或者数据。
接下来需要将背光点亮
LCD_LED PBout(0) = 1;
可以使用ILI9341控制器中的D3h指令,用该指令获取LCD的ID,再通过串口打印出来。
3. 发送控制命令初始化液晶屏
这里就是将我们对GPIO的配置,FSMC的配置以及背光灯的点亮放在一个初始化函数中,这里本喵就不演示了。
4. 编写液晶屏的绘制像素点函数
为了操作方便,我们将向LCD写命令,写数据,读数据这几个操作封装为函数。
//写寄存器函数
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{
LCD->LCD_REG=regval;
}
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(u16 data)
{
LCD->LCD_RAM=data;
}
//读LCD数据
//返回值:读到的值
u16 LCD_RD_DATA(void)
{
vu16 ram;
ram = LCD->LCD_RAM;
return ram;
}
因为在使用LCD屏幕的时候,我们需要给ILI9341不同的控制指令,但是每次查表又不方便,所以我们将常用到的指令放在一个结构体中。
这些命令值我们在另一给函数中挨个赋给这个结构体
还要在屏幕上开一个窗口,用来显示我们要显示的内容。
这是画点函数,我们可以多次调用改函数画各种各样的图形,文字。
看代码:
int main()
{
u8 j;
u32 i;
delay_init();
LCD_Init();
LED_Init();
POINT_COLOR = RED;
LCD_Clear(WHITE);
for(j=75;j<100;j++)
{
for(i=100;i<400;i++)
{
LCD_DrawPoint(i,j);
}
}
while(1)
{
LED0 = !LED0;
delay_ms(500);
}
}
下面是效果。
这是我们利用画点函数通过循环画出的一个矩形。
5. 利用描点函数制作各种不同的液晶显示应用
本喵使用的是正点原子家的开发板,原子家提供了很多的应用函数,我们在用到的时候直接调用就可以。
这是一个圆圈,一个矩形,还有俩条直线。
由于计算机只能识别 0 和 1,文字也只能以 0 和 1 的形式在计算机里存储,所以我们需要对文字进行编码才能让计算机处理,编码的过程就是规定特定的 01 数字符串来表示特定的文字,最简单的字符编码例子是 ASCII 码。
如上图中的ASCII码表中的部分字符,大写字母A的ASCII码值的16进制形式是0X41,空格的ASCII码值的16进制形式是0X20。
现在已经知道了,将0X41规定为字符A,那么这个A是怎么输出到屏幕的呢?
根据上面LCD的显示原理我们知道,LCD显示的过程就是将特定的像素点用特定的颜色填充的过程
- 上图中,高度有60个像素点,宽度有30个像素点
- 将这1800个像素点中的部分像素点用蓝色的填充,就能够得出字符A在屏幕中的样子。
这个填充过程是怎样的呢?
- 每一个像素点对应着一个比特位,当该位为1时,填充该点,当该位为0不填充,按照这样的方式就可以显示出我们想要的字符。
继续看我们的字符A
这样表示每一个像素点是1还是0的比特位,按照16进制形式表示出来多个数据组成的数组,叫做字模。
按照上面动图中的填充顺序,我们可以根据字符A的样子,倒推出每个像素点是0还是1,然后通过上面讲解的画点函数将这些点用颜色填充,但是如果是每个字符都这样靠我们眼睛去看它对应的点是1还是0的话就太费劲了,所以我们需要借助一些工具。
使用这样一个软件,将上面红色框中字节的宽度和高度都设置成60,因为这个尺寸是汉字的尺寸,对应的因为字符就是宽位30,高为60,其中30和60都是比特位的个数。
再点击有下脚红色框中的生成字模,就能得到我们上面分析过程中的每一个像素点是1还是0,用16进制表示出来。
将字模直接用在程序中,按照动图中的扫描顺序对应的字模中的数据,将比特位的值为1的像素点填充,为0的不填充,得到的就是我们要的字符A。
单个英文字符已经成功显示出来了,那么一个英文字符串是如何在LCD上显示出来的呢?
显示原理和单个的一样,只将单个字符的显示放在一个循环里,这样就可以显示多个字符,组合在一起就成了字符串。
既然要显示字符串,那么就不能仅有一个字母的字模了,需要将ASCII码表中的所有字符的字模都生成出来,这被叫做字库。
我们可以将每个字符都用上面制作单个字模的软件制作出来,然后将这些生成的16进制数据放在一个数组中,但是这样会非常繁琐,我们可以用一个软件,直接生成英文的字库。
这样生成了所有ASCII码表中字符组成的字库了。
是一个数据量非常大的数组。
将其放在一个单独的头文件中,并且添加到工程中。
来看一下代码如何调用这个字库。
- 首先字符串中的每个字符必须在ASCII码表的空格和‘~’之间,符号条件才进行循环显示。
- 当一行显示满以后,要将x左边重置到开始处,但是y坐标要换到下一行。
- 在没有超出显示区域高度后,调用显示单个字符的函数
- 这样循环到\0后便结束显示。
- 通过要显示字符的大小,利用公式计算出单个字模所占的字节个数。
- 调用对应size(字体大小)的字库,这里制作了三种大小的字库,1212,1616,和24*24
- 蓝色框中通过循环判断字库中单个字模中每一个字节的每一位的情况,如果为1,则填充像素。
注意:
ASCII码值和是怎么和字库联系上的呢?
LCD液晶显示关键有俩个点,一个就是使用FSMC模拟8080时序来控制LIL9341芯片,进而控制LCD液晶屏,另一个就是显示原理,理解了显示屏上是如何显示内容的,并且按照原理写出相应的代码就可以实现,不过在我们大部分的使用情况中,都可以使用已经写好的显示函数来操作屏幕,除非需要特定的字体,这就需要我们按照原理来制作相应的字库。