每节课都是一个项目 手把手用STM32打造联网气象站-8-面子工程-学会点亮LCD屏幕
时间:2022-09-10 19:30:00
目录
1. 游戏的秘密
2. LCD的魅力
3. 8080接口
3.1写入命令
3.2写入数据
4. RGB接口
5. FMSC接口
5.1 FMSC接口介绍
5.2 如何产生D/C信号?
5.3 具体代码中的地址转换
6. 初始化的屏幕驱动
6.1 GPIO配置
6.2 FSMC配置
6.2.1 理解SetupTime
6.2.2 理解AccessMode_B
6.3 BackLed_Control
6.3.关于枚举类型
6.3.2关于LED背光灯
6.4 ILI9341复位
6.5 ILI9341寄存器初始化
6.6 GRAM设置扫描方法
6.6.设置扫描方向
6.6.2 设置X Y轴向坐标
7. 通过工程验证屏幕的初始化
7.1 在屏幕上画直线
7.2 在屏幕上画长方形
7.2.1. 采用OpenWindow设置绘制矩形的起始坐标、宽度和高度;
7.2.2 采用FillColor函数,填满矩形;
7.2.3 填满长方形
完成了STM32基础三板斧,用这三板斧完成一些项目,我们需要开始面子工程,点亮LCD屏幕。
我们把LCD提前的屏幕教程主要是为了使教程更有趣。
1. 游戏的秘密
我们每个人都会沉迷于某个游戏一段时间,连续七八个小时玩游戏,熬夜到两三点,这可能是正常的。
为什么游戏有这么大的魔力?游戏的吸引力来自哪里?我们能学会让编程同样有吸引力吗?
游戏的魔力主要来自以下几点:
1.及时反馈:游戏总是每隔一段时间就会有积极的反馈,比如刷设备或获得奖励。玩游戏的人可以得到积极的反馈,而不需要花费太多的精力。这种机制吸引游戏玩家继续玩。
2. 任务简化分解:每个任务将简化分解为多个小任务,完成一个小任务将有明确的反馈。我相信玩过游戏的人会有这样的经历,如果水平太悲伤,每个人都会放弃游戏。
3. 难度适中:游戏通常设置适中的难度太困难或太简单,会使游戏变得无聊。困难很容易放弃,这很容易理解。太简单也会让游戏变得无聊。例如,有些游戏,如果充电太多,或者在插件被破解后,大大降低了游戏的难度,但使游戏缺乏吸引力。
4. 结果明确:每个游戏都会有明确的结果,比如消灭一个大游戏BOSS,或者得到一个宝藏。明确的结果使游戏进度易于衡量,结果易于反映。明确的结果也是游戏的魅力之一。
通过分析游戏的魅力,我们可以推出如何使编程非常迷人。
1. 编程教程需要及时反馈:每个教程都能及时看到结果,结果需要有吸引力;
2. 编程任务需要简化和分解:编程的复杂任务需要分解为多个简单的小任务,以便在学习过程中不断检查和理解进度;
3. 编程难度适中:如果编程代码较大,则需要将其分解为多个小任务,以简化每个任务的难度;
4. 编程结果明确:每个编程项目的结果是非常明确的,达到什么样的状态,实现什么样的功能,是非常清楚的,这样在学习过程中,减少很多犹疑和走弯路的过程。
2. LCD的魅力
把板上的ED灯看作一个像素,那么LCD就是具备了320X240=76800个LED的表达能力。因此熟练掌握LCD,对于增强设计的表现力和表达力,都是非常有帮助的。
LCD的常用接口一般包含下面这样几种:
1. SPI接口:采用SPI方式和屏幕进行通信, 其中SPI接口时Motorola 公司推出的一 种同步串行接口技术,是一种高速的,全双工,同步的 通信总线。
2. INTEL 8080接口:使用这种接口的屏幕一般是屏幕自带了驱动芯片,比如ILI9488、ILI9341、SSD1963等 。驱动芯片里面自带了显存,MCU只需要把显示数据传给驱动芯片,驱动芯片会把数据保存到显存中,最后再把显存中的数据显示到屏幕上。
3. RGB接口:大屏采用较多的接口, 屏幕不带显存,需要MCU准备充足的显存空间(因为RGB565,480*272分辨率的屏幕就需要显存480*272*2 = 255K,一般的MCU都没有这么大的RAM,所以要加外置的SRAM或SDRAM)
我们这里重点介绍的是8080接口:
液晶屏的内部结构如上图所示,其中MCU通过8080接口,控制ILI9341或SSD1963或者同类芯片。此类芯片一般内置显存GRAM。MCU将需要显示的内容,通过8080接口,写入ILI9341。ILI9341将需要显示的内容写入GRAM,并且通过RGB接口,控制液晶显示电路。
所以ILI9341这类芯片,相当于电脑中的显卡。一方面连接CPU,另一方面连接液晶屏,实现显示控制功能。
对于STM32F4系列,也和电脑类似,会内置显卡功能。对于这类内置显卡的MCU,一般需要外挂SRAM,用来存储大量的图片。
3. 8080接口
接下来我们重点了解一下8080接口,并且通过理解这个接口,进一步深入理解MCU是如何通过接口,和外部芯片进行通信的。
8080接口分成两类:数据接口和控制接口。
数据接口包含DB0~DB15,共计16位数据;
控制接口包括:CS片选,RD读信号,WR写信号,RES复位信号,D/C数据命令信号共计5根信号线;
MCU通过8080接口写入ILI9341时,会先写入地址,再写入数据。
上图为ILI9341手册,其中明确说明了,当我们需要写入ILI9341数据时,我们先将D/C设置为0,写入对应的地址信息,告诉驱动芯片,接下来的数据需要写入哪一个位置。然后再把D/C设置为1,把数据写入对应地址中。
3.1写入命令
当MCU向ILI9341写入数据时,会控制这些管脚,达到下面状态:
1. CS片选拉低,通知ILI9341,“你被选中了,马上MCU要写入数据了”
2. D/C拉低,通知ILI9341,现在写入的是命令,而不是数据。
3. WR拉低,通知ILI9341,MCU马上要写入 数据;
4. 然后数据接口DB0~DB15会执行写入动作,先写入地址信息,再写入数据信息;
具体操作如上图所示。
3.2写入数据
写入数据和写入命令唯一区别是:写入数据时,D/C为高电平;而写入命令时,D/C为低电平。
完成了上面两个操作后,就可以把数据从MCU写入ILI9341驱动中。
例如,我们常常会写入2A这个地址,这时,MCU会通知ILI9341,具体在哪一列进行显示。
我们也常常写入2B这个地址,这里就是通知ILI9341,具体在哪一行进行显示。
4. RGB接口
MCU将数据写入ILI9341之后,LCD并不会直接更新显示。ILI9341还需要通过RGB接口,更新ILI9341对应的数据信息。ILI9341和LCD之间的接口,就是对应的RGB接口。
比如我们在淘宝上搜索RGB 4.3,就可以看到很多屏幕数据。
点击进去,就可以看到更加详细的信息,其中就会介绍对应的接口是RGB40PIN。
RGB40PIN对应的管脚如上图,包含了R,G,B和水平同步,垂直同步等信息。
液晶屏有一个显示指针, 它指向将要显示的像素。显示指针的扫描方向方向从左到右、从上到下,一个像素点一个 像素点地描绘图形。这些像素点的数据通过 RGB 数据线传输至液晶屏,它们在同步时钟 CLK 的驱动下一个一个地传输到液晶屏中,交给显示指针,传输完成一行时,水平同步信 号 HSYNC 电平跳变一次,而传输完一帧时 VSYNC 电平跳变一次。所以HSYNC的频率明显高于VSYNC。
液晶屏中的每个像素点都是数据,在实际应用中需要把每个像素点的数据缓存起来, 再传输给液晶屏,一般会使用 SRAM 或 SDRAM 性质的存储器,而这些专门用于存储显示 数据的存储器,则被称为显存。显存一般至少要能存储液晶屏的一帧显示数据,如分辨率 为 800x480 的 液 晶 屏 , 使 用 RGB888 格 式 显 示 , 它 的 一 帧 显 示 数 据 大 小 为 : 3x800x480=1152000 字 节 ; 若 使 用 RGB565 格 式 显 示 , 一 帧 显 示 数 据 大 小 为 : 2x800x480=768000 字节。其中RGB565把红色和蓝色的显示位数减少了,这样减少了整体显存的消耗。
在本期教程中,ILI9341将自动完成RGB接口通信,因此,我们只需要大致了解RGB接口的概念即可。
5. FSMC接口
5.1 FSMC接口介绍
第四节提到了8080接口的访问方式,然而,在现实中,我们使用的MCU并不具备8080接口,它具备的是FSMC接口。FSMC接口全称为:Flexible Static Memory Controller 的缩写,意思为:灵活的静态存储控制器,它可以用于驱动包括 SRAM、 NOR FLASH 以及 NAND FLSAH 类型的存储器。不能驱动SDRAM这类动态存储器。(备注:静态存储和动态存储的区别是:静态存储不需要刷新,而动态存储需要不断刷新内容,因此动态存储往往需要一些专用的带有自动刷新的接口)
FSMC接口时灵活通用接口,它支持多种不同外部存储的访问操作,其中就包括了8080接口。
FSMC分为多组信号,当把FSMC用作8080接口时,信号对应关系如下:
FSMC接口读操作时序图
FSMC接口写操作时序图
8080接口操作时序图
把上面3个图放在一起,就可以看出:FSMC和8080基本上一一对应。
信号名 | FMSC | 8080 |
片选 | NE | CS |
写信号 | WE | WR |
读信号 | NOE | RD |
数据 | DATA | DATA |
数据/命令 | D/C | ? |
比如我们像FSMC的地址,写入一个数据,就会产生类似8080接口的时序。我们通过这个方式,将数据按照写入FMSC的方式写入8080接口。按照从FSMC读取数据的方式,从8080接口读取数据。
但是这里还有一个问题:ILI9341的8080接口中,有一个D/C信号,如何通过FSMC来产生呢?
5.2 如何产生D/C信号?
FSMC对应NOR/SRAM访问地址为6000 0000 ~6FFFF FFFF这个范围。
下面捋一下地址线和访问地址之间关系:
地址 |
地址二进制 |
高电平地址线 |
0x6000 0001 |
0001 |
A0 |
0x6000 0010 |
0001 0000 |
A4 |
0x6000 0100 |
0001 0000 0000 |
A8 |
0x6000 1000 |
0001 0000 0000 0000 |
A12 |
0x6001 0000 |
0001 0000 0000 0000 0000 |
A16 |
当我们访问地址0x6000 0001时,对应的A0地址线为高位;
当我们访问地址0x6000 0010时,对应的A4地址线为高位;
以此类推;
当我们访问地址0X6001 0000时,对应的A16地址线为高位。
通过上面规律,就可以找到实现D/C命令访问的方式:
我们把D(数据)对应的地址和C(命令)对应的地址分开,这样写入D的时候,对应的地址线为高电平,写入C的时候,对应地址线为低电平,这样可以用地址线来模拟D/C线的控制。
当 FSMC 外设被配置成正常工作,并且外部接了 NOR FLASH 时,若向 0x60000000 地 址写入数据如 0xABCD,FSMC 会自动在各信号线上产生相应的电平信号,写入数据。 FSMC 会控制片选信号 NE1 选择相应的 NOR 芯片,然后使用地址线 A[25:0]输出 0x60000000,在 NWE 写使能信号线上发出低电平的写使能信号,而要写入的数据信号 0xABCD 则从数据线 D[15:0]输出,然后数据就被保存到 NOR FLASH 中了。
5.3 具体代码中的地址转换
在FSMC驱动中,我们定义了两个地址:0x60000000 和0x60020000;这样,按照前面讨论内容,当FMSC往60020000中写入数据时,对应A17将变为高电平,因此,可以用A17来替代D/C信号,控制DATA写入。
不对啊,根据原理图来看,明明原理图上连接的时A16而不是A17啊?
是的,其中还有一个核心秘密:
在本工程中使用的是 16位的数据访问方式,所以 HADDR与 FSMC_A的地址线连接关 系会左移一位,如 HADDR1 与 FSMC_A0 对应、HADDR2 与 FSMC_A1 对应。因此, 当 FSMC_A0地址线为 1时,实际上内部地址的第 1位为 1,FSMC_A1地址线为 1时, 实际上内部地址的第 2 位为 1。同样地,当希望 FSMC_A16 地址输出高电平或低电平 时,实质是访问内部 HADDR 地址的第(16+1)位为 1 即可。
因此,我们通过访问0X6002 0000这个地址,使得A16被设置为高电平,从而用A16来控制8080接口的D/C管脚,实现数据访问。
我们经过上面完整的分析,主要是让你掌握地址线和地址之间计算的逻辑:
当我们需要访问特定地址时,是通过特定地址线的高低电平来实现的。
6. 屏幕驱动初始化
下面重点讲解一下ILI9341的初始化过程。其他屏幕驱动芯片的初始化过程也非常类似。
6.1 GPIO配置
GPIO配置中,需要完成8080接口的16根数据线和6根控制线的初始化,把他们都初始化为输出。
这里需要注意,其中CS, DC, RD, WR设置为GPIO_Mode_AF_PP,而不是我们通常使用的GPIO_Mode_Out_PP,这是什么原因呢?
这时因为这几个IO是带有内部功能的IO,当我们采用这些带有内部功能的IO作为输出时,需要配置为AF_PP,也就是推挽复用输出。
6.2 FSMC配置
FSMC初始化步骤比较复杂,这里进行简单说明,但不会详细讲解。对于大部分嵌入式软件工程师而言,是很难得去修改这里的代码的。对于驱动工程师而言,才有机会真正修改这里的代码。
6.2.1 理解SetupTime
其中AddressSetupTime和DataSetupTime是设置地址线和数据线的建立时间。所谓建立时间,就是为了确保信号能够被准确采集到,需要在采集点之前的x 纳秒,就需要把信号准备好,这样当读写操作开始时,能够把信号稳定在0或者1的数值,避免读写失误。
上图中,tdst就是写入数据的建立时间。WRX的上升沿(上图红色箭头处),MCU完成数据写入。但是在这个上升沿之前至少tdst这个时间,就需要把数据准备好,这样在上升沿写数据的时候,才能够稳定写入数据。
类似的参数时tdht,也就是数据保持时间,在WRX的上升沿之后,数据还需要保持至少tdht这个时间,这样才能够确保上升沿能够稳定的写入数据。
这里设置AddressSetupTime和DataSetupTime就是根据ILI9341芯片的数据手册里面描述的setuptime,设置FMSC的参数,使得FSMC模拟出来的8080接口,能够稳定可靠的写入ILI9341。
我们理解这里的原理即可,不需要在这里扣的太细。
但是当我们实际需要在这里深挖的时候,我们应该具有这样的能力,能够深度挖掘这里的技术细节,并且完成相应的开发工作。
6.2.2 理解AccessMode_B
FSMC能够模拟不同类型的接口,而AccessMode_B则是模拟NOR FLASH 接口。 8080接口原本就是用在NOR FLASH上面,因此我们这里把他设置成为NOR FLASH 接口,是非常匹配的。
6.3 BackLed_Control
点亮背光灯则非常简单,我们只需要通过GPIO,控制对应的GPIO为低电平即可。
6.3.1关于枚举类型
且慢,如何从代码中,看出这里的GPIO是需要设置为低电平呢?
我们之前跟着ST学写代码中,学习了typedef structure {}new_structure这种语句,这里我们学习类似的语句:
typedef enum{A=0,B=!A} new_state这样的语句。
首先我们复习一下枚举类型enum。我们在代码中常常用枚举类型,它的主要作用是:加强代码的可读性。比如Control(Enable), Control(Disable)这样的方式,总会比Control(0), Control(1)这样的方式更加友好,谁知道你的代码宇宙中,0表示Enable,还是1表示Enalbe (备注:在习惯的代码宇宙中,0表示disable, 1表示enable,但是也会有例外。)
6.3.2关于LED背光灯
我们的LED面板一般都内置背光灯。这个背光灯可以通过LCD_BK这个管脚控制点亮和熄灭,上面电路就包含了一个三极管,通过三极管,控制LED点亮或者熄灭。
这个三极管是PNP型三极管。关于如何区分NPN三极管和PNP型三极管,有这样一个小秘诀:箭头总是从P指向N; 所以如果箭头指向内部的,就是PNP管。如果箭头指向外部的,就是NPN管。根据箭头总是从P指向N这个规则,我们就可以清晰判断三极管的类型。
如果是NPN三极管,则是基极高电平导通,如果是PNP三极管,则是低电平导通。这里的记忆秘诀是:电压总是P大于N。因此对于NPN三极管,中间需要高电平,这样满足P大于N的条件,对于PNP三极管,中间需要低电平,这样才能满足P大于N的条件。
掌握上面两个记忆密码,我们就可以理解PNP和NPN三极管的工作原理。
在上面电路中,STM32的GPIO连接到LCD_BK。当GPIO设置为低电平时,LCD点亮,当GPIO设置为高电平时,LCD熄灭。
6.4 ILI9341复位
通过控制9341的RST管脚,进行相应的复位操作。
需要注意:复位后,需要Delay一段时间。由于这个Delay是在初始化的过程中,因此并不会对MCU代码运行产生影响,因为初始化只是上电短时间执行一次的操作而已。
6.5 ILI9341寄存器初始化
9341寄存器初始化过程相对比较复杂,需要结合9341的数据手册进行查看。
比如代码中,写入0XCF,我们只需要在数据手册上,查找CF,就可以很快找到对应的内容。
从图片可以看出,代码中的设置和手册中的设置是一模一样的,这个就是芯片上电时候选择的默认设置。
代码中ED寄存器,也就是上电寄存器,和默认值略有不同。我们这里结合手册,看看到底改了哪里。
手册中明确说了,第一参数CP1 设置为10,对应了soft start keep 1 frame选项。CP23参数对应了3rd frame enable选项。
具体对应方式为:
0x64 对应二进制为0110 0100,也就是cp1为10, cp23为10。
从工程角度来说,我们只需要能够一一对应即可,并不需要详细理解其中的每一个细节,因为:
吾生也有涯而知也无涯以有涯随无涯殆已
因此,这里我们只要知道,需要再上电前,对ILI9341的各个寄存器,完成一系列配置即可。。
6.6 GRAM扫描方式设置
6.6.1设置扫描方向
GramScan函数用来设置LCD的扫描方式。ucOption为参数,数值为0~7,也就是LCD共计有8种扫描方式。
对于模式0 2 4 6 , X为240, Y为320;
对于模式1 3 5 7, X方向为320, Y方向为240;
我们先把ucOption写入0x36的地址:ucOption的数值分别为000~111,左移5位后,对应数值为0000 0000~1110 0000,这样写入0x36地址后,相当于设置了0x36地址中下面的几个寄存器数值:
MY, MX, MV设置关系如下
参数 |
方向 |
MY=0 |
自下向上 |
MY=1 |
自上向下 |
MX=0 |
自右向左 |
MX=1 |
自左向右 |
MV=0 | X Y保持 |
MV=1 |
X Y互换 |
因此我们就可以得出模式配置规律
参数 |
方向 |
0 0 0 |
Y自下向上,X自右向左,X,Y保持 |
0 0 1 |
Y自下向上,X自右向左,X,Y互换 |
0 1 0 |
Y自下向上,X自左向右,X,Y保持 |
0 1 1 | Y自下向上,X自左向右,X,Y互换 |
1 0 0 |
Y自上向下,X自右向左,X,Y保持 |
1 0 1 |
Y自上向下,X自右向左,X,Y互换 |
1 1 0 |
Y自上向下,X自左向右,X,Y保持 |
1 1 1 | Y自上向下,X自左向右,X,Y互换 |
6.6.2 设置X Y轴方向的坐标
其中CoordinateX的第一个和第二个参数,对应起始坐标的高八位和第八位,这里都是0。也就是起始坐标从0开始。第三个和第四个参数,对应结束坐标的高八位和低八位,这里为前面屏幕尺寸X方向数值。
CoordinateY的参数也是类似的,第一和第二个参数为起始坐标,第三和第四个参数为结束坐标。
完成显示范围设置之后,就从开头,不断写入像素点,写入的方向就是按照前面ucOption设置的方向,写入的个数,就是按照前面X Y的起始点和结束点设置的个数。写入的数值,就是包含了R,G,B三元素的数值。
到这里,我们终于完成了屏幕的初始化工作。
屏幕初始化确实比较复杂,但是我们大部分时候,仅仅需要掌握如何移植这个驱动即可。而对于详细代码部分,我们需要了解,如何从Datasheet中,找到对应的数据,而不需要牢记具体的数据信息。
7. 通过工程,验证屏幕初始化
终于到了激动人心的时刻,我们来试一下屏幕的初始化功能。
7.1 在屏幕上画直线
所谓在屏幕上画直线,就是根据两个坐标,在屏幕上绘制一条连接两个点之间的直线。说起来简单,但是实际上是需要一定的算法支持的。
因为我们的屏幕是有像素点的,所谓直线只是许多条折线的组合,只是远看起来像是一条直线而已。
假设我们起始点坐标为(0,0),结束点坐标为(50,100)我们把这个参数带进去:
1. usX1=0, usY1=0, usX2=50, usY2=100;
2. Delta_X=50, Delta_Y=100;
3. Increase_X=1, Increase_Y=1; Distance=100;
for循环中,
1. 在起始点0,0画点;
2. Err_x=50, Err_Y=100;
3. 判断Err_X和Err_Y是否大于Distance;
4. Err_X=100, Err_Y=200;
5. Err_Y>Distance, Err_Y=100, y_current=1; 在(0,1)处画一个点;
7. Err_X=150, Err_Y=200, 在(1,2)处画一个点,完成后,Err_X=50, Err_Y=100;
8.继续循环在(1,3)处画一个点;
以此类推,完成从起点到终点的划线行为。
事实上,划线是一个有一定复杂度的算法,比冒泡法排序更加复杂一些。
对于有一定复杂度的算法,我们一般采用代入法,将一些特殊数值放进去,即可循序渐进,判断出对应算法逻辑。
7.2 在屏幕上画长方形
在屏幕上画长方形更加简单一些。
7.2.1. 采用OpenWindow函数,设置绘制矩形的起始坐标,宽度,高度;
下面详细看下:
OpenWindow设置写入位置,操作的还是最初的2A和2B寄存器。具体参见6.6.2中详细描述。
7.2.2 采用FillColor函数,将矩形填满;
首先我们需要了解一下,9341是采用RGB565的方式,控制颜色的,红色5bit 绿色6bit 蓝色5bit。
常见的RGB888和RGB565的颜色定义,可以参考下面的连接。
https://blog.csdn.net/qq_20222919/article/details/116168044
如需查看更多颜色组合,直接查看上面链接即可。
我们以黄色为例,如果需要组成黄色,就需要红色和绿色拉满,蓝色不要,这样就是FF E0,这样就可以组成黄色;如果需要组成紫色,就需要红色和蓝色拉曼,绿色不要,这就是:F8 1F,这样就把中间绿色设置为0,其他都是1,构成了紫色。
当我们需要在一个区域设置颜色时,只需要把对应的范围,直接写入上面设置的颜色信息,即可完成颜色的填充。
7.2.3 填满长方形
先用OpenWindow设置需填充的区域,然后再往这个区域写入需要填充的颜色,即可构成填充好的长方形。
纸上得来终觉浅,绝知此事要躬行!
马上下载测试代码,看看效果。
https://download.csdn.net/download/book_drabit/86103895
代码执行效果详细见下面GIF图片内容。