OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 299|回复: 0

《STM32H562开发指南 V1.0 》第二十三章 TFTLCD(MCU屏)实验

[复制链接]

1397

主题

1413

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5956
金钱
5956
注册时间
2019-5-8
在线时间
1668 小时
发表于 4 天前 | 显示全部楼层 |阅读模式
第二十三章 TFTLCD(MCU屏)实验

1)实验平台:正点原子Mini STM32H562开发板

2)章节摘自【正点原子】STM32H562开发指南 V1.0

3)购买链接: https://detail.tmall.com/item.htm?id=753606659612

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h562_mini.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子STM32开发板技术交流群:414052738


2.jpg

3.png

前面我们介绍了OLED模块及其显示,但是该模块只能显示单色/双色,不能显示彩色,而且尺寸也较小。本章我们将介绍正点原子的TFT LCD模块(MCU屏),该模块采用TFTLCD面板,可以显示16位色的真彩图片。在本章中,我们将使用开发板底板上的TFTLCD接口(仅支持MCU屏,本章仅介绍MCU屏的使用),来点亮TFTLCD,并实现ASCII字符和彩色的显示等功能,并在串口打印LCD控制器ID,同时在LCD上面显示。
本章分为如下几个小节:
23.1 TFTLCD和FMC简介
23.2 硬件设计
23.3 程序设计
23.4 下载验证


23.1 TFTLCD和FMC简介
本章我们将通过STM32H562的FMC接口来控制TFTLCD的显示,所以本节分为两个部分,分别介绍TFTLCD和FMC。

23.1.1 TFTLCD简介
液晶显示器,即Liquid Crystal Display,利用了液晶导电后透光性可变的特性,配合显示器光源、彩色滤光片和电压控制等工艺,最终可以在液晶阵列上显示彩色的图像。目前液晶显示技术以TN、STN、TFT三种技术为主,TFT-LCD即采用了TFT(Thin Film Transistor)技术的液晶显示器,也叫薄膜晶体管液晶显示器。
TFT-LCD与无源TN-LCD、STN-LCD的简单矩阵不同的是,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT式显示器具有很多优点:高响应度,高亮度,高对比度等等。TFT式屏幕的显示效果非常出色,广泛应用于手机屏幕、笔记本电脑和台式机显示器上。
由于液晶本身不会发光,加上液晶本身的特性等原因,使得液晶屏的成像角受限,我们从屏幕的的一侧可能无法看清液晶的显示内容。液晶显示器的成像角的大小也是评估一个液晶显示器优劣的指标,目前,规格较好的液晶显示器成像角一般在120°~160°之间。
正点原子TFT-LCD模块(MCU屏)有如下特点:
1,2.8’/3.5’/4.3’/7’等4种大小的屏幕可选。
2,320×240的分辨率(3.5’分辨率为:320*480,4.3’和7’分辨率为:800*480)。
3,16位真彩显示。
4,自带触摸屏,可以用来作为控制输入。
本章,我们以正点原子2.8寸(此处的寸是代表英寸,下同)的TFT-LCD模块为例介绍,(其他尺寸的LCD可参考具体的LCD型号的资料,也比较类似),该模块支持65K色显示,显示分辨率为320×240,接口为16位的8080并口,自带触摸功能。
该模块的外观图如图23.1.1.1所示:


第二十三章 TFTLCD1146.png
图23.1.1.1 正点原子2.8寸TFTLCD外观图

模块原理图如图23.1.1.2所示:

第二十三章 TFTLCD1196.png
图23.1.1.2 正点原子2.8寸TFTLCD模块原理图

TFTLCD模块采用2*17的2.54公排针与外部连接,即图中TFT_LCD部分。从图23.1.1.2可以看出,正点原子TFTLCD模块采用16位的并方式与外部连接。图23.1.1.2还列出了触摸控制的接口,但触摸控制是在显示的基础上叠加的一个控制功能,不配置也不会对显示造成影响,我们放到以后的章节再介绍触摸的用法。该模块与显示功能有关的信号线如表23.1.1.1:

1.png
表23.1.1.1 TFT-LCD接口信号线

上述的接口线实际是对应到液晶显示控制器上的,这个芯片位于液晶屏的下方,所以我们从外观图上看不到。控制LCD显示的过程,就是按其显示驱动芯片的时序,把色彩和位置信息正确地写入对应的寄存器。

23.1.2 液晶显示控制器
正点原子提供2.8/3.5/4.3/7寸等4种不同尺寸和分辨率的TFTLCD模块,其驱动芯片为:ILI9341/ST7789/NT35310/NT35510/SSD1963等(具体的型号,大家可以通过下载本章实验代码,通过串口或者LCD显示查看),这里我们仅以ILI9341控制器为例进行介绍,其它的控制基本都类似,我们就不详细阐述了。
ILI9341液晶控制器自带显存,可配置支持8/9/16/18位的总线中的一种,可以通过3/4线串行协议或8080并口驱动。正点原子的TFTLCD模块上的电路配置为8080并口方式,其显存总大小为172800(240*320*18/8),即18位模式(26万色)下的显存量。在16位模式下,ILI9341采用RGB565格式存储颜色数据,此时ILI9341的18位显存与MCU的16位数据线以及RGB565的对应关系如图23.1.2.1所示:


第二十三章 TFTLCD2072.png
图23.1.2.1 16位数据与显存对应关系图

从图中可以看出,ILI9341在16位模式下面,18位显存的B0和B12并没有用到,对外的数据线使用DB0-DB15连接MCU的D0-D15实现16位颜色的传输(使用8080 MCU 16bit I型接口,详见9341数据手册7.1.1节)。
这样MCU的16位数据,最低5位代表蓝色,中间6位为绿色,最高5位为红色。数值越大,表示该颜色越深。另外,特别注意ILI9341所有的指令都是8位的(高8位无效),且参数除了读写GRAM的时候是16位,其它操作参数,都是8位的。
知道了屏幕的显色信息后,我们如何驱动它呢?OLED的章节我们已经描述过8080方式操作的时序,我们通过《ILI9341_DS.pdf》来加深一下在8080并口方式下如何操作这个芯片。
以写周期为例,8080方式下的操作时序如图23.1.2.2所示。


第二十三章 TFTLCD2460.png
图23.1.2.2 8080方式下对液晶控制器的写操作

上图中的各个控制线与我们在表23.1.1.1提到的命名有些许差异,因为我们在原理图时往往为了方便自己记忆会对命名进行微调,为了方便读者对照,我们把图23.1.2.2中列出的引脚引脚与我们的TFTLCD模块的的对应关系再列出,如表23.1.2.1所示。

2.png
表23.1.2.1 TFT-LCD引脚与液晶控制器的对应关系

这下我们再来分析一下图23.1.2.2所示的写操作的时序,控制液晶的主机,在整个写周期内需要控制片选CSX拉低(标注为①),之后对其它的控制线的电平才有效。在标号②表示的这个写命令周期中,D/CX被位低(参考ILI9341的引脚定义),同时把命令码通过数据线D[17:0](我们实际只用了16个引脚)按位编码。注意到③处,需要数据线在入电平拉高后再操持一段时间以便数据被正确采样。
图23.1.2.2中⑤表示写数据操作,与前面描述的写命令操作只有D/CX的操作不同,读者们可以尝试自己分析一下。更多的关于ILI9341的读写操作时序则参考《ILI9341_DS.pdf》。
通过前述的时序分析,我们知道了对于ILI9341来说,控制命令有命令码、数据码之分,接下来,我们介绍一下ILI9341的几个重要命令。因为ILI9341的命令很多,我们这里就不全部介绍了,有兴趣的大家可以找到ILI9341的datasheet看看。里面对这些命令有详细的介绍。我们将介绍:0xD3,0x36,0x2A,0x2B,0x2C,0x2E等6条指令。
指令0xD3,是读ID4指令,用于读取LCD控制器的ID,该指令如表23.1.2.2所示:


3.png
表23.1.2.2 0xD3指令描述

从上表可以看出,0xD3指令后面跟了4个参数,最后2个参数,读出来是0x93和0x41,刚好是我们控制器ILI9341的数字部分,从而,通过该指令,即可判别所用的LCD驱动器是什么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动IC的初始化代码,从而兼容不同驱动IC的屏,使得一个代码支持多款LCD。
接下来看指令:0x36,这是存储访问控制指令,可以控制ILI9341存储器的读写方向,简单的说,就是在连续写GRAM的时候,可以控制GRAM指针的增长方向,从而控制显示方式(读GRAM也是一样)。该指令如表23.1.2.3所示:


4.png
表23.1.2.3 0x36指令描述

从上表可以看出,0x36指令后面,紧跟一个参数,这里主要关注:MY、MX、MV这三个位,通过这三个位的设置,我们可以控制整个ILI9341的全部扫描方向,如表23.1.2.4所示:

5.png
表23.1.2.4 MY、MX、MV设置与LCD扫描方向关系表

这样,我们在利用ILI9341显示内容的时候,就有很大灵活性了,比如显示BMP图片,BMP解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置LCD扫描方向为从左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往LCD填充颜色数据即可,这样可以大大提高显示速度。
实验中,我们默认使用从左到右,从上到下的扫描方式。
接下来看指令:0x2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置横坐标(x坐标),该指令如表23.1.2.5所示:


6.png
表23.1.2.5 0x2A指令描述

在默认扫描方式时,该指令用于设置x坐标,该指令带有4个参数,实际上是2个坐标值:SC和EC,即列地址的起始值和结束值,SC必须小于等于EC,且0≤SC/EC≤239。一般在设置x坐标的时候,我们只需要带2个参数即可,也就是设置SC即可,因为如果EC没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。
与0X2A指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置纵坐标(y坐标)。该指令如表23.1.2.6所示:


7.png
表23.1.2.6 0X2B指令描述

在默认扫描方式时,该指令用于设置y坐标,该指令带有4个参数,实际上是2个坐标值:SP和EP,即页地址的起始值和结束值,SP必须小于等于EP,且0≤SP/EP≤319。一般在设置y坐标的时候,我们只需要带2个参数即可,也就是设置SP即可,因为如果EP没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。
接下来看指令:0X2C,该指令是写GRAM指令,在发送该指令之后,我们便可以往LCD的GRAM里面写入颜色数据了,该指令支持连续写,指令描述如表23.1.2.7所示。


8.png
表23.1.2.7 0X2C指令描述

由表23.1.2.6可知,在收到指令0X2C之后,数据有效位宽变为16位,我们可以连续写入LCD GRAM值,而GRAM的地址将根据MY/MX/MV设置的扫描方向进行自增。例如:假设设置的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过SC,SP设置)后,每写入一个颜色值,GRAM地址将会自动自增1(SC++),如果碰到EC,则回到SC,同时SP++,一直到坐标:EC,EP结束,其间无需再次设置的坐标,从而大大提高写入速度。
最后,来看看指令:0X2E,该指令是读GRAM指令,用于读取ILI9341的显存(GRAM),该指令在ILI9341的数据手册上面的描述是有误的,真实的输出情况如表23.1.2.8所示:


9.png
表23.1.2.8 0X2E指令描述

该指令用于读取GRAM,如表23.1.2.7所示,ILI9341在收到该指令后,第一次输出的是dummy数据,也就是无效的数据,第二次开始,读取到的才是有效的GRAM数据(从坐标:SC,SP开始),输出规律为:每个颜色分量占8个位,一次输出2个颜色分量。比如:第一次输出是R1G1,随后的规律为:B1R2G2B2R3G3B3R4G4B4R5G5...以此类推。如果我们只需要读取一个点的颜色值,那么只需要接收到参数3即可,如果要连续读取(利用GRAM地址自增,方法同上),那么就按照上述规律去接收颜色数据。
以上,就是操作ILI9341常用的几个指令,通过这几个指令,我们便可以很好的控制ILI9341显示我们所要显示的内容了。


23.1.3 FMC简介
STM32H562xx系列芯片都带有FMC接口,即可变存储存储控制器,能够与同步或异步存储器、SDRAM存储器和NAND FLASH等连接,STM32H562的FMC接口支持包括SRAM、SDRAM、NAND FLASH、NOR FLASH和PSRAM等存储器。FMC的框图如图23.1.3.1所示:

第二十三章 TFTLCD6927.png
图23.1.3.1 FMC框图

从上图我们可以看出,STM32H562的FMC将外部设备分为3类:NOR/PSRAM设备、NAND设备和SDRAM设备。他们共用地址数据总线等信号,他们具有不同的CS以区分不同的设备,比如本章我们用到的TFTLCD就是用的FMC_NE1做片选,其实就是将TFTLCD当成SRAM来控制。
FMC的时钟源来自于AHB,即250MHz。
这里我们介绍下为什么可以把TFTLCD当成SRAM设备用:首先我们了解下外部SRAM的连接,外部SRAM的控制一般有:地址线(如A0~A18)、数据线(如D0~D15)、写信号(WE)、读信号(OE)、片选信号(CS),如果SRAM支持字节控制,那么还有UB/LB信号。而TFTLCD的信号我们在23.1.1节有介绍,包括:RS、D0~D15、WR、RD、CS、RST和BL等,其中真正在操作LCD的时候需要用到的就只有:RS、D0~D15、WR、RD和CS。其操作时序和SRAM的控制完全类似,唯一不同就是TFTLCD有RS信号,但是没有地址信号。
TFTLCD通过RS信号来决定传送的数据是数据还是命令,本质上可以理解为一个地址信号,比如我们把RS接在A0上面,那么当FMC控制器写地址0的时候,会使得A0变为0,对TFTLCD来说,就是写命令。而FMC写地址1的时候,A0将会变为1,对TFTLCD来说,就是写数据了。这样,就把数据和命令区分开了,他们其实就是对应SRAM操作的两个连续地址。当然RS也可以接在其他地址线上,我们的开发板把RS连接在A22上面的。
STM32H562的FMC支持8/16位数据宽度,我们这里用到的LCD是16位宽度的,所以在设置的时候,选择16位宽就OK了。我们再来看看FMC的外部设备地址映像,STM32H562的FMC将外部存储器划分为6个固定大小为256M字节的存储区域,如图23.1.3.2所示:


第二十三章 TFTLCD7738.png
图23.1.3.2 FMC存储块地址映像

从上图可以看出,FMC总共管理1.5GB空间,拥有6个存储块(Bank),每个存储块256MB空间。本章,我们把TFTLCD当成SRAM设备来使用,所以用到的是存储块1。下面我们仅讨论存储块1的相关配置,其他块的配置,请参考《STM32H5xx参考手册_V1(英文版).pdf》第22章(796页)的相关介绍。
STM32H562的FMC存储块1(Bank1)被分为4个区,每个区管理64M字节空间,每个区都有独立的寄存器对所连接的存储器进行配置。Bank1的256M字节空间由28根地址线(ADDR[27:0])寻址。
这里ADDR是内部AXI地址总线,其中ADDR[25:0]来自外部存储器地址FMC_A[25:0],而ADDR[26:27]对4个区进行寻址。如表23.1.3.1所示:


10.png
表23.1.3.1 Bank1存储区选择表

ADDR[25:0]位包含外部存储器的地址,由于ADDR为字节地址,而存储器按字寻址,所以,根据存储器数据宽度的不同,实际上向存储器发送的地址也有所不同,如表23.1.3.2所示:

第二十三章 TFTLCD8435.png
表23.1.3.2 NOR/PSRAM外部存储器地址

因此,FMC内部ADDR与存储器寻址地址的实际对应关系就是:
当接的是16位宽度存储器的时候:ADDR[25:1] FMC_A [24:0]。
当接的是8位宽度存储器的时候:ADDR[25:0] FMC_A [25:0]。
不论外部接8位宽或者16位宽设备,FMC_A[0]永远接在外部设备地址A[0]。 这里,TFTLCD使用的是16位数据宽度,所以ADDR[0]并没有用到,只有ADDR[25:1]是有效的,对应关系变为:ADDR[25:1] FMC_A[24:0],相当于右移了一位,这里请大家特别留意。另外,ADDR[27:26]的设置,是不需要我们干预的,比如:当你选择使用Bank1的第一个区,即使用FMC_NE1来连接外部设备的时候,即对应了ADDR[27:26]=00,我们要做的就是配置对应第1区的寄存器组,来适应外部设备。STM32H562的FMC各Bank配置寄存器如表23.1.3.3所示:


11.png
表23.1.3.3 FMC各Bank配置寄存器表

对于NOR FLASH控制器,主要是通过FMC_BCRx、FMC_BTRx和FMC_BWTRx寄存器设置(其中x=1~4,对应4个区)。通过这3个寄存器,可以设置FMC访问外部存储器的时序参数,拓宽了可选用的外部存储器的速度范围。FMC的NOR FLASH控制器支持同步和异步突发两种访问方式。选用同步突发访问方式时,FMC将HCLK时钟分频后,发送给外部存储器作为同步时钟信号FMC_CLK。此时需要的设置的时间参数有2个:
1,HCLK与FMC_CLK的分频系数(CLKDIV),可以为2~16分频;
2,同步突发访问中获得第1个数据所需要的等待延迟(DATLAT)。
对于异步突发访问方式,FMC主要设置3个时间参数:地址建立时间(ADDSET)、数据建立时间(DATAST)和地址保持时间(ADDHLD)。FMC综合了SRAM、PSRAM和NOR Flash产品的信号特点,定义了4种不同的异步时序模型。选用不同的时序模型时,需要设置不同的时序参数,如表23.1.3.4所列:


12.png
表23.1.3.4 NOR FLASH/PSRAM控制器支持的时序模型

在实际扩展时,根据选用存储器的特征确定时序模型,从而确定各时间参数与存储器读/写周期参数指标之间的计算关系;利用该计算关系和存储芯片数据手册中给定的参数指标,可计算出FMC所需要的各时间参数,从而对时间参数寄存器进行合理的配置。
本章,我们使用异步模式A(ModeA)方式来控制TFTLCD,模式A的读操作时序如图23.1.3.3所示:


第二十三章 TFTLCD10290.png
图23.1.3.3 模式A读操作时序图

模式A支持独立的读写时序控制,这个对我们驱动TFTLCD来说非常有用,因为TFTLCD在读的时候,一般比较慢,而在写的时候可以比较快,如果读写用一样的时序,那么只能以读的时序为基准,从而导致写的速度变慢,或者在读数据的时候,重新配置FMC的延时,在读操作完成的时候,再配置回写的时序,这样虽然也不会降低写的速度,但是频繁配置,比较麻烦。而如果有独立的读写时序控制,那么我们只要初始化的时候配置好,之后就不用再配置,既可以满足速度要求,又不需要频繁改配置。
模式A的写操作时序如图23.1.3.4所示:


第二十三章 TFTLCD10565.png
图23.1.3.4 模式A写操作时序

图23.1.2.3和图23.1.2.4中的ADDSET与DATAST,是通过不同的寄存器设置的,接下来我们讲解一下Bank1的几个控制寄存器。

23.1.4 FMC寄存器
NOR/PSRAM控制寄存器1/2/3/4(FMC_BCR1/2/3/4)
SRAM/NOR闪存片选控制寄存器:FMC_BCRx(x=1~4),该寄存器描述如图23.1.4.1所示:


第二十三章 TFTLCD10766.png
图23.1.4.1 FMC_BCRx寄存器

该寄存器我们在本章用到的设置有:FMCEN、EXTMOD、WREN、MWID[1:0]、MTYP[1:0]和MBKEN这几个设置,我们将逐个介绍。
FMCEN:FMC使能位。我们要使用FMC驱动TFTLCD就必须设置该位为1,不过只有FMC_BCR1的FMCEN位有效,FMC_BCR2~4的FMCEN位无效,统一由FMC_BCR1的FMCEN位控制。
EXTMOD:扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读写不同的时序,故该位需要设置为1。
WREN:写使能位。我们需要向TFTLCD写数据,故该位必须设置为1。
MWID[1:0]:存储器数据总线宽度。00,表示8位数据模式;01表示16位数据模式;10保留;11保留。我们的TFTLCD是16位数据线,所以设置WMID[1:0]=01。
MTYP[1:0]:存储器类型。00表示SRAM;01表示PSRAM;10表示NOR FLASH/OneNAND FLASH;11保留。前面提到,我们把TFTLCD当成SRAM用,所以需要设置MTYP[1:0]=00。
MBKEN:存储块使能位。我们需要用到该存储块控制TFTLCD,所以要使能该存储块。
SRAM/NOR-Flash片选时序寄存器1/2/3/4 (FMC_BTR1/2/3/4)
SRAM/NOR闪存片选时序寄存器:FMC_BTRx(x=1~4),该寄存器描述如图23.1.4.2所示:


第二十三章 TFTLCD11408.png
图23.1.4.2 FMC_BTRx寄存器各位描述

这个寄存器包含了每个存储器块的控制信息,可以用于SRAM和NOR闪存存储器等。如果FMC_BCRx寄存器中设置了EXTMOD位,则有两个时序寄存器分别对应读(本寄存器)和写操作(FMC_BWTRx寄存器)。因为我们要求读写分开时序控制,所以EXTMOD是使能了的,也就是本寄存器是读操作时序寄存器,控制读操作的相关时序。本章我们要用到的设置有:ACCMOD、DATAST和ADDSET这三个设置。
ACCMOD[1:0]:访问模式。00表示访问模式A;01表示访问模式B;10表示访问模式C;11表示访问模式D,本章我们用到模式A,故设置为00。
DATAST[7:0]:数据保持时间。0为保留设置,其他设置则代表保持时间为: DATAST个HCLK时钟周期,最大为255个。对ILI9341来说,其实就是RD低电平持续时间,一般为355ns。而一个HCLK时钟周期为4ns左右(1/250MHz),为了兼容其他屏,我们这里设置DATAST为88,也就是88个fmc_ker_ck周期,时间大约是352ns(略超,但不影响使用)。
ADDSET[3:0]:地址建立时间。其建立时间为:ADDSET个HLCK周期,最大为17个。对ILI9341来说,这里相当于RD高电平持续时间,为90ns,我们设置ADDSET为最大17,即15*4.3=68ns(略超,但不影响使用)。
SRAM/NOR-Flash写入时序寄存器1/2/3/4 (FMC_BWTR1/2/3/4)
SRAM/NOR闪写时序寄存器:FMC_BWTRx(x=1~4),该寄存器描述如图23.1.4.3所示:


第二十三章 TFTLCD12127.png
图23.1.4.3 FMC_BWTRx寄存器各位描述

该寄存器在本章用作写操作时序控制寄存器,需要用到的设置同样是:ACCMOD、DATAST和ADDSET这三个设置。这三个设置的方法同FMC_BTRx一模一样,只是这里对应的是写操作的时序,ACCMOD设置同FMC_BTRx一模一样,同样是选择模式A,另外DATAST和ADDSET则对应WR的低电平和高电平持续时间,对ILI9341来说,这两个时间只需要15ns就够了,比读操作快得多。实测DATAST设置为0,没有问题。然后ADDSET设置为17,也是68ns。
至此,我们对STM32H562的FMC介绍就差不多了,关于FMC的详细介绍,请大家参考《STM32H5xx参考手册_V1(英文版).pdf》第22章。通过以上两个小节的了解,我们可以开始写LCD的驱动代码了。注意:在MDK的寄存器定义里面,并没有定义FMC_BCRx、FMC_BTRx、FMC_BWTRx等这个单独的寄存器,而是将他们进行了一些组合。
FMC_BCRx和FMC_BTRx,组合成BTCR[8]寄存器组,他们的对应关系如下:
BTCR[0]对应FMC_BCR1,BTCR[1]对应FMC_BTR1
BTCR[2]对应FMC_BCR2,BTCR[3]对应FMC_BTR2
BTCR[4]对应FMC_BCR3,BTCR[5]对应FMC_BTR3
BTCR[6]对应FMC_BCR4,BTCR[7]对应FMC_BTR4
FMC_BWTRx则组合成BWTR[7],他们的对应关系如下:
BWTR[0]对应FMC_BWTR1,BWTR[2]对应FMC_BWTR2,
BWTR[4]对应FMC_BWTR3,BWTR[6]对应FMC_BWTR4,
BWTR[1]、BWTR[3]和BWTR[5]保留,没有用到。
通过对FMC相关的寄存器的描述,大家对FMC的原理有了一定的认识,如果还不熟悉的朋友,请一定要搜索网络资料理解FMC的原理。
一般TFTLCD模块的使用流程如图23.1.1.5:


第二十三章 TFTLCD13018.png

图23.1.1.5 TFTLCD使用流程

任何LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要执行一次即可。而画点流程就是:设置坐标→写GRAM指令→写入颜色数据,然后在LCD上面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标→读GRAM指令→读取颜色数据,这样就可以获取到对应点的颜色数据了。
以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用TFTLCD了。接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出TFTLCD显示需要的相关设置步骤如下:
1)设置STM32H562与TFTLCD模块相连接的IO。
这一步,先将我们与TFTLCD模块相连的IO口进行初始化,以便驱动LCD,这里我们用到的是FMC。
2)初始化TFTLCD模块。
即图25.1.1.5的初始化序列,这里我们没有硬复位LCD,因为开发板的LCD接口将TFTLCD的RST同STM32H562的RESET连接在一起了,只要按下开发板的RESET键,就会对LCD进行硬复位。初始化序列,就是向LCD控制器写入一系列的设置值(比如伽马校准),这些初始化序列一般LCD供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。在初始化之后,LCD才可以正常使用。
3)通过函数将字符和数字显示到TFTLCD模块上。
这一步则通过图25.1.1.5左侧的流程,即:设置坐标→写GRAM指令→写GRAM来实现,但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数,就可以实现数字/字符的显示了。


23.2 硬件设计

1. 例程功能
使用开发板的MCU屏接口连接正点原子 TFTLCD模块(仅限MCU屏模块),实现TFTLCD模块的显示。通过把LCD模块插入底板上的TFTLCD模块接口,按下复位之后,就可以看到LCD模块不停的显示一些信息并不断切换底色。同时该实验会显示LCD驱动器的ID,并且会在串口打印(按复位一次,打印一次)。LED0闪烁用于提示程序正在运行。

2. 硬件资源
1)LED灯
       LED0 - PB4
1)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
2)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)

3. 原理图
TFTLCD模块的电路见图23.1.1.2,而开发板的LCD接口和正点原子TFTLCD模块直接可以对插,开发板上的LCD接口如图23.2.1所示:


第二十三章 TFTLCD14138.png
图25.2.1 TFTLCD模块与开发板对接的LCD接口示意图

TFTLCD模块与开发板的连接原理图如图23.2.2所示。

第二十三章 TFTLCD14202.png
图23.2.2 TFTLCD模块与开发板的连接原理图

在硬件上,TFTLCD模块与开发板的IO口对应关系如下:
LCD_BL(背光控制)对应PE5;
LCD_CS对应PD7,即FMC_NE1;
LCD _RS对应PE6,即FMC_A22;
LCD _WR对应PD5,即FMC_NEW;
LCD _RD对应PD4,即FMC_NOE;
LCD _D[15:0]则直接连接在FMC_D15~FMC_D0;
这些线的连接,开发板的内部已经连接好了,我们只需要将TFTLCD模块插上去就好了。


23.3 程序设计

23.3.1 FMC和SRAM的HAL库驱动
SRAM和FMC在HAL库中的驱动源码在stm32h5xx_hal_sram.c和 stm32h5xx_ll_fmc.c,以及它们的头文件中。

1. HAL_SRAM_Init函数
SRAM的初始化函数,其声明如下:

  1. HAL_StatusTypeDef HAL_SRAM_Init(SRAM_HandleTypeDef *hsram,
  2. FMC_NORSRAM_TimingTypeDef *Timing, FMC_NORSRAM_TimingTypeDef *ExtTiming);
复制代码
函数描述:
用于初始化 SRAM,注意这个函数不限制一定是SRAM,只要时序类似,均可使用。前面说过,这里我们把LCD当作SRAM使用,因为他们时序类似。
函数形参:
形参1是SRAM_HandleTypeDef结构体类型指针变量,其定义如下:

  1. typedef struct
  2. {
  3.   FMC_NORSRAM_TypeDef              *Instance;          /* 寄存器基地址 */
  4.   FMC_NORSRAM_EXTENDED_TypeDef   *Extended;          /* 扩展模式寄存器基地址 */
  5.   FMC_NORSRAM_InitTypeDef         Init;                /* SRAM设备控制配置结构体 */
  6.   HAL_LockTypeDef                   Lock;               /* SRAM锁定对象 */
  7.   __IO HAL_SRAM_StateTypeDef      State;              /* SRAM设备访问状态 */   
  8.   MDMA_HandleTypeDef               *hmdma;             /* 指针DMA处理配置 */
  9. }SRAM_HandleTypeDef;
复制代码
1)Instance:指向FMC寄存器基地址。本实验我们使用异步模式A(ModeA)方式来控制TFTLCD,使用的存储块是Bank1,所以寄存器基地址Instance我们直接写FMC_Bank1_R即可,因为HAL库定义好了宏定义FMC_NORSRAM_DEVICE,也就是如果是SRAM设备,直接填写这个宏定义标识符即可。
2)Extended:指向FMC扩展模式寄存器基地址,因为我们要配置的读写时序是不一样的,前面讲的FMC_BCRx寄存器的EXTMOD位我们会配置为1允许读写不同的时序,所以还要指定写操作时序寄存器地址,也就是通过参数Extended来指定的,这里设置为FMC_Bank1E_R即可,同样HAL库定义了FMC_NORSRAM_EXTENDED_DEVICE,直接填写这个宏定义标识符即可。
3)Init:用于配置FMC外接SRAM或者相同时序设备时的基本参数,是我们接触最多的参数。
4)Lock:用于配置锁状态。
5)State:SRAM设备访问状态。
6)hmdma:用于配置关联MAMA句柄。
其中成员变量Init是FMC_NORSRAM_InitTypeDef结构体指针类型,该变量才是真正用来设置SRAM控制接口参数的。下面详细了解这个结构体定义:

  1. typedef struct
  2. {
  3.   uint32_t NSBank;                       /* 存储区块号 */               
  4.   uint32_t DataAddressMux;             /* 地址/数据复用使能 */              
  5.   uint32_t MemoryType;                  /* 存储器类型 */
  6.   uint32_t MemoryDataWidth;            /* 存储器数据宽度 */
  7.   uint32_t BurstAccessMode;            /* 使能或者禁止突发模式 */
  8.   uint32_t WaitSignalPolarity;        /* 设置等待信号的极性 */
  9.   uint32_t WaitSignalActive;           /* 等待状态之前或等待状态期间 */
  10.   uint32_t WriteOperation;             /* 存储器写使能 */
  11.   uint32_t WaitSignal;                  /* 使能或者禁止通过等待信号来插入等待状态 */
  12.   uint32_t ExtendedMode;                /* 使能或者禁止使能扩展模式 */
  13.   uint32_t AsynchronousWait;           /* 用于异步传输期间,使能或者禁止等待信号 */
  14.   uint32_t WriteBurst;                  /* 用于使能或者禁止异步的写突发操作 */
  15.   uint32_t ContinuousClock;            /* 使能或者禁止FMC时钟输出到外部存储设备 */
  16.   uint32_t WriteFifo;                   /* 使能或者禁止写 FIFO */
  17.   uint32_t PageSize;                    /* 设置页大小 */
  18.   uint32_t NBLSetupTime;         /* 设置NBL时钟周期 */
  19.   FunctionalState MaxChipSelectPulse;   /* 启用或禁用最大芯片选择脉冲管理 */
  20.   uint32_t MaxChipSelectPulseTime;       /* 指定FMC_CLK周期内的最大芯片选择脉冲时间 */
  21. }FMC_NORSRAM_InitTypeDef;
复制代码
NSBank用来指定使用到的存储块区号,前面讲过,我们是使用的存储块区号1,所以选择值为FMC_NORSRAM_BANK1。
DataAddressMux用来设置是否使能地址/数据复用,该变量仅对NOR/PSRAM有效,所以这里我们选择不使能地址/数据复用值FMC_DATA_ADDRESS_MUX_DISABLE即可。
MemoryType用来设置存储器类型,这里我们把LCD当SRAM使用,所以设置为FMC_MEMORY_TYPE_SRAM即可。
MemoryDataWidth用来设置存储器数据总线宽度,可选8位还是16位,这里我们选择16位数据宽度FMC_NORSRAM_MEM_BUS_WIDTH_16。
WriteOperation用来设置存储器写使能,也就是是否允许写入。毫无疑问我们会进行存储器写操作,所以这里设置为FMC_WRITE_OPERATION_ENABLE。
ExtendedMode用来设置是否使能扩展模式,也就是是否允许读写使用不同时序,前面讲解过本实验读写采用不同时序,所以设置值为使能值FMC_EXTENDED_MODE_ENABLE。
ContinuousClock用来设置启用/禁止FMC时钟输出到外部存储设备,这里仅当使用FMC_BCR1寄存器的时候需要启用,启用值为FMC_CONTINUOUS_CLOCK_SYNC_ASYNC。
其他参数WriteBurst,BurstAccessMode,WaitSignalPolarity,WaitSignalActive,WaitSignal,AsynchronousWait等是用在突发访问和异步时序情况下,这里我们不做过多讲解。
形参2 Timing和形参3 ExtTiming都是FMC_NORSRAM_TimingTypeDef结构体类型指针变量,其定义如下:

  1. typedef struct
  2. {
  3.   uint32_t AddressSetupTime;                /* 地址建立时间 */         
  4.   uint32_t AddressHoldTime;                 /* 地址保持时间 */            
  5.   uint32_t DataSetupTime;                   /* 数据建立时间 */      
  6.   uint32_t DataHoldTime;                   /* 数据保持时间 */            
  7.   uint32_t BusTurnAroundDuration;         /* 总线周转阶段的持续时间 */      
  8.   uint32_t CLKDivision;                     /* CLK时钟输出信号的周期 */               
  9.   uint32_t DataLatency;                     /* 同步突发NOR FLASH的数据延迟 */      
  10.   uint32_t AccessMode;                      /* 异步模式配置 */      
  11. }FMC_NORSRAM_TimingTypeDef;
复制代码
对于本实验,读速度比写速度慢得多,因此读写时序不一样,所以对于Timing和ExtTiming要设置了不同的值,其中Timing设置写时序参数,ExtTiming设置读时序参数。
下面解析一下结构体的成员变量:
AddressSetupTime用来设置地址建立时间。
AddressHoldTime用来设置地址保持时间。
DataSetupTime用来设置数据建立时间。
DataHoldTime用来设置数据保持时间。
BusTurnAroundDuration用来配置总线周转阶段的持续时间。
CLKDivision用来配置CLK时钟输出信号的周期,以HCLK周期数表示。
DataLatency用来设置同步突发NOR FLASH的数据延迟。
AccessMode用来设置异步模式,取值范围为FMC_ACCESS_MODE_A、FMC_ACCESS_
MODE_B,、FMC_ACCESS_MODE_C和FMC_ACCESS_MODE_D,这里我们用是异步模式A,所以取值为FMC_ACCESS_MODE_A。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
和其他外设一样,HAL库也提供了SRAM的初始化MSP回调函数,函数声明如下:

  1. void HAL_SRAM_MspInit(SRAM_HandleTypeDef *hsram) ;
复制代码

2. FMC_NORSRAM_Extended_Timing_Init函数
FMC_NORSRAM_Extended_Timing_Init函数是初始化扩展时序模式函数。其声明如下:

  1. HAL_StatusTypeDef  FMC_NORSRAM_Extended_Timing_Init(
  2. FMC_NORSRAM_EXTENDED_TypeDef *Device, FMC_NORSRAM_TimingTypeDef *Timing,
  3. uint32_t Bank, uint32_t ExtendedMode);
复制代码
函数描述:
该函数用于初始化扩展时序模式。
函数形参:
形参1是FMC_NORSRAM_EXTENDED_TypeDef结构体类型指针变量,扩展模式寄存器基地址选择。
形参2是FMC_NORSRAM_TimingTypeDef结构体类型指针变量,可以是读或者写时序结构体。
形参3是储存区块号。
形参4是使能或者禁止扩展模式。
函数返回值:
HAL_StatusTypeDef枚举类型的值。
注意事项:
该函数我们用于重新配置写或者读时序。
FMC驱动LCD显示配置步骤
1)使能FMC和相关GPIO时钟,并设置好GPIO工作模式
我们通过FMC控制LCD,所以先需要使能FMC以及相关GPIO口的时钟,并设置好GPIO的工作模式。
2)设置FMC参数
这里我们需要设置FMC的相关访问参数(数据位宽、访问时序、工作模式等),以匹配液晶驱动IC,这里我们通过HAL_SRAM_Init函数完成FMC参数配置,详见本例程源码。
3)初始化LCD
由于我们例程兼容了很多种液晶驱动IC,所以先要读取对应IC的驱动型号,然后根据不同的IC型号来调用不同的初始化函数,完成对LCD的初始化。
注意:这些初始化函数里面的代码,都是由LCD厂家提供,一般不需要改动,也不需要深究,我们直接照抄即可。
4)实现LCD画点&读点函数
在初始化LCD完成以后,我们就可以控制LCD显示了,而最核心的一个函数,就是画点和读点函数,只要实现这两个函数,后续的各种LCD操作函数,都可以基于这两个函数实现。
5)实现其他LCD操作函数
在完成画点和读点两个最基础的LCD操作函数以后,我们就可以基于这两个函数实现各种LCD操作函数了,比如画线、画矩形、显示字符、显示字符串、显示数字等,如果不够用还可以根据自己需要来添加。详见本例程源码。


23.3.2 程序流程图

第二十三章 TFTLCD19798.png
图23.3.2.1 TFTLCD(MCU屏)实验程序流程图

23.3.3 程序解析

1. LCD驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LCD驱动源码包括四个文件:lcd.c、lcd.h、lcd_ex.c和lcdfont.h。
lcd.c和lcd.h文件是驱动函数和引脚接口宏定义以及函数声明等。lcd_ex.c存放各个LCD驱动IC的寄存器初始化部分代码,是lcd.c文件的补充文件,起到简化lcd.c文件的作用。lcdfont.h头文件存放了4种字体大小不一样的ASCII字符集(12*12、16*16、24*24和32*32)。这个跟oledfont.h头文件一样的,只是这里多了32*32的ASCII字符集,制作方法请回顾OLED实验。
下面我们还是先介绍lcd.h文件,首先是LCD的引脚定义:

  1. /*****************************************************************************/
  2. /* LCD RST/WR/RD/BL/CS/RS 引脚 定义
  3. * LCD_D0~D15,由于引脚太多,就不在这里定义了,直接在lcd_init里面修改.所以在移植的时候,除了
  4. * 改这6个IO口, 还得改lcd_init里面的D0~D15所在的IO口.
  5. */

  6. /* RESET 和系统复位脚共用 所以这里不用定义 RESET引脚 */
  7. //#define LCD_RST_GPIO_PORT               GPIOX
  8. //#define LCD_RST_GPIO_PIN                GPIO_PIN_X
  9. //#define LCD_RST_GPIO_CLK_ENABLE()      
  10. do{ __HAL_RCC_GPIOx_CLK_ENABLE(); }while(0)  /* 所在IO口时钟使能 */

  11. #define LCD_WR_GPIO_PORT                             GPIOD
  12. #define LCD_WR_GPIO_PIN                              GPIO_PIN_5
  13. #define LCD_WR_GPIO_CLK_ENABLE()        do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */

  14. #define LCD_RD_GPIO_PORT                        GPIOD
  15. #define LCD_RD_GPIO_PIN                         GPIO_PIN_4
  16. #define LCD_RD_GPIO_CLK_ENABLE()     
  17.   do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */

  18. #define LCD_BL_GPIO_PORT                        GPIOE
  19. #define LCD_BL_GPIO_PIN                         GPIO_PIN_5
  20. #define LCD_BL_GPIO_CLK_ENABLE()   
  21.     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */

  22. /* LCD_CS(需要根据LCD_FMC_NEX设置正确的IO口) 和 LCD_RS
  23. (需要根据LCD_FMC_AX设置正确的IO口) 引脚 定义 */
  24. #define LCD_CS_GPIO_PORT                        GPIOD
  25. #define LCD_CS_GPIO_PIN                         GPIO_PIN_7
  26. #define LCD_CS_GPIO_CLK_ENABLE()      
  27.   do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */

  28. #define LCD_RS_GPIO_PORT                        GPIOE
  29. #define LCD_RS_GPIO_PIN                         GPIO_PIN_6
  30. #define LCD_RS_GPIO_CLK_ENABLE()        
  31. do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */

  32. /* FMC相关参数 定义
  33. * 注意: 我们默认是通过FMC块1来连接LCD, 块1有4个片选: FMC_NE1~4
  34. *
  35. * 修改LCD_FMC_NEX, 对应的LCD_CS_GPIO相关设置也得改
  36. * 修改LCD_FMC_AX , 对应的LCD_RS_GPIO相关设置也得改
  37. */
  38. #define LCD_FMC_NEX         1         /* 使用FMC_NE1接LCD_CS,取值范围只能是: 1~4 */
  39. #define LCD_FMC_AX          22        /* 使用FMC_A22接LCD_RS,取值范围是: 0 ~ 25 */
  40. /*****************************************************************************/
复制代码
第一部分的宏定义是LCD RST/WR/RD/BL/CS/RS 引脚定义,需要注意的是:LCD的RST引脚和系统复位脚连接在一起,所以不用单独使用一个IO口(节省一个IO口)。
第二部分的宏定义是LCD_FMC_NEX和LCD_FMC_AX,这两个宏定义是为计算LCD的基地址LCD_BASE而定义的。其中LCD_FMC_NEX是FMC的存储区块号,取值范围只能是: 1~4,因为LCD的驱动用到块1,所以我们默认取值为1。而LCD_FMC_AX是对应LCD_RS引脚的IO口的复用功能,开发板上LCD RS引脚硬件连接PE6,PE6要复用为FMC_A22引脚。所以LCD_FMC_AX定义为22,22对应着FMC_A22,取值范围是: 0 ~ 25。
下面介绍我们在lcd.h里面定义的一个重要的结构体:

  1. /* LCD重要参数集 */
  2. typedef struct
  3. {
  4.     uint16_t width;             /* LCD 宽度 */
  5.     uint16_t height;            /* LCD 高度 */
  6.     uint16_t id;                 /* LCD ID */
  7.     uint8_t dir;                 /* 横屏还是竖屏控制:0,竖屏;1,横屏。 */
  8.     uint16_t wramcmd;           /* 开始写gram指令 */
  9.     uint16_t setxcmd;           /* 设置x坐标指令 */
  10.     uint16_t setycmd;           /* 设置y坐标指令 */
  11. } _lcd_dev;

  12. /* LCD参数 */
  13. extern _lcd_dev lcddev; /* 管理LCD重要参数 */

  14. /* LCD的画笔颜色和背景色 */
  15. extern uint32_t  g_point_color;     /* 默认红色 */
  16. extern uint32_t  g_back_color;      /* 背景颜色.默认为白色 */
复制代码
该结构体用于保存一些LCD重要参数信息,比如LCD的长宽、LCD ID(驱动IC型号)、LCD横竖屏状态等,这个结构体虽然占用了十几个字节的内存,但是却可以让我们的驱动函数支持不同尺寸的LCD,同时可以实现LCD横竖屏切换等重要功能,所以还是利大于弊的。最后声明_lcd_dev结构体类型变量lcddev,lcddev在lcd.c中定义。
紧接着就是g_point_color和g_back_color变量的声明,它们也是在lcd.c中被定义。g_point_color变量用于保存LCD的画笔颜色,g_back_color则是保存LCD的背景色。
下面是LCD背光控制IO口的宏定义:

  1. /* LCD背光控制 */
  2. #define LCD_BL(x)   do{ x ? \
  3.           HAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_SET) : \
  4.           HAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_RESET);\
  5.        }while(0)
复制代码
我们知道TFTLCD的RS接在FMC的A22(即PE6)上面,CS接在FMC_NE1(即PD7)上,并且是16位数据总线。我们使用的是FMC存储器1的第1区,所以我们定义如下LCD操作结构体:
  1. /* LCD地址结构体 */
  2. typedef struct
  3. {
  4.     volatile uint16_t LCD_REG;
  5.     volatile uint16_t LCD_RAM;
  6. } LCD_TypeDef;

  7. /* LCD_BASE的详细解算方法:
  8. * 我们一般使用FMC的块1(BANK1)来驱动LCD (MCU屏), 块1地址范围总大小为256MB,均分成4块:
  9. * 存储块1(FMC_NE1)地址范围: 0X6000 0000 ~ 0X63FF FFFF
  10. * 存储块2(FMC_NE2)地址范围: 0X6400 0000 ~ 0X67FF FFFF
  11. * 存储块3(FMC_NE3)地址范围: 0X6800 0000 ~ 0X6BFF FFFF
  12. * 存储块4(FMC_NE4)地址范围: 0X6C00 0000 ~ 0X6FFF FFFF
  13. *
  14. * 我们需要根据硬件连接方式选择合适的片选(连接LCD_CS)和地址线(连接LCD_RS)
  15. * H562开发板使用FMC_NE1连接LCD_CS, FMC_A22连接LCD_RS ,16位数据线,计算方法如下:
  16. * 首先FMC_NE1的基地址为:0X6000 0000;  NEx的基址为(x=1/2/3/4): 0X6000 0000 + (0X400 0000 * (x - 1))
  17. * FMC_A22对应地址值: 2^22 * 2 = 0X800000; FMC_Ay对应的地址为(y=0~25): 2^y * 2
  18. *
  19. * LCD->LCD_REG,对应LCD_RS = 0(LCD寄存器); LCD->LCD_RAM,对应LCD_RS = 1(LCD数据)
  20. * 则 LCD->LCD_RAM的地址为:  0X6000 0000 + 2^22 * 2 = 0X6080 0000
  21. *    LCD->LCD_REG的地址可以为 LCD->LCD_RAM之外的任意地址.
  22. * 由于我们使用结构体管理LCD_REG 和 LCD_RAM(REG在前,RAM在后,均为16位数据宽度)
  23. * 因此 结构体的基地址(LCD_BASE) = LCD_RAM - 2 = 0X6080 0000 -2
  24. *
  25. * 更加通用的计算公式为((片选脚FMC_NEx)x=1/2/3/4, (RS接地址线FMC_Ay)y=0~25):
  26. *          LCD_BASE = (0X6000 0000 + (0X400 0000 * (x - 1))) | (2^y * 2 -2)
  27. *          等效于(使用移位操作)
  28. *          LCD_BASE = (0X6000 0000 + (0X400 0000 * (x - 1))) | ((1 << y) * 2 -2)
  29. */
  30. #define LCD_BASE        (uint32_t)((0X60000000 + (0X4000000 * (LCD_FMC_NEX - 1))) | (((1 << LCD_FMC_AX) * 2) -2))
  31. #define LCD             ((LCD_TypeDef *) LCD_BASE)
复制代码
其中LCD_BASE,必须根据我们外部电路的连接来确定,即根据前面介绍的LCD_FMC_NEX和LCD_FMC_AX宏定义的值来确定。我们使用Bank1.sector1,把宏定义LCD_FMC_NEX的值代入(0X60000000 + (0X4000000 * (LCD_FMC_NEX - 1)))得到的就是存储区的基地址,即0x60000000。而FMC_A22对应地址值为(((1 << LCD_FMC_AX) * 2) -2)),代入LCD_FMC_AX的值得到0x007F FFFE,所以LCD_BASE表示的地址值为0x60000000 | 0x007F FFFE。
上面是直接代入我们给出的公式进行计算,但是很多朋友不太明白FMC_A22对应地址值怎么来的。下面我们来解析一下,以FMC_A22为例,0x007F FFFE转换成二进制就是:0111 1111 1111 1111 1111 1110。从表23.1.2.2知道16位数据时,地址右移一位对齐,那么实际对应到地址引脚的时候,就是:[A22:A0]= 0011 1111 1111 1111 1111 1111,此时A22是0,但是如果16位地址再加1(注意:对应到8位地址是加2,即0x007F FFFE +0X02),那么:[A22:A0]=0100 0000 0000 0000 0000,这时A22就是1了,即实现了对RS的0和1的控制。
定义LCD的时候,我们将LCD_BASE这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的地址就是0x607F FFFE,地址右移一位对齐后对应A19的状态为0(即RS=0),而LCD->LCD_RAM的地址就是0x6080 0000(结构体地址自增),地址右移一位对齐后对应A19的状态为1(即RS=1)。
所以,有了这个定义,当我们要往LCD写命令/数据的时候,可以这样写:

  1. LCD->LCD_REG = CMD;          /* 写命令 */
  2. LCD->LCD_RAM = DATA;         /* 写数据 */
复制代码
而读的时候反过来操作就可以了,如下所示:
  1. CMD = LCD->LCD_REG;                /* 读LCD寄存器 */
  2. DATA = LCD->LCD_RAM;        /* 读LCD数据 */
复制代码
这其中,CS、WR、RD和IO口方向都是由FMC硬件自动控制,不需要我们手动设置了。
最后是一些其他的宏定义,包括LCD扫描方向和颜色,以及SSD1963相关配置参数。
下面开始对lcd.c文件介绍,先看LCD初始化函数,其定义如下:

  1. void lcd_init(void)
  2. {
  3.     GPIO_InitTypeDef gpio_init_struct = {0};
  4.     FMC_NORSRAM_TimingTypeDef fmc_read_timing = {0};
  5.     FMC_NORSRAM_TimingTypeDef fmc_write_timing = {0};

  6.     LCD_CS_GPIO_CLK_ENABLE();   /* LCD_CS脚时钟使能 */
  7.     LCD_WR_GPIO_CLK_ENABLE();   /* LCD_WR脚时钟使能 */
  8.     LCD_RD_GPIO_CLK_ENABLE();   /* LCD_RD脚时钟使能 */
  9.     LCD_RS_GPIO_CLK_ENABLE();   /* LCD_RS脚时钟使能 */
  10.     LCD_BL_GPIO_CLK_ENABLE();   /* LCD_BL脚时钟使能 */
  11.    
  12.     gpio_init_struct.Pin = LCD_CS_GPIO_PIN;
  13.     gpio_init_struct.Mode = GPIO_MODE_AF_PP;                /* 推挽复用 */
  14.     gpio_init_struct.Pull = GPIO_PULLUP;                            /* 上拉 */
  15.     gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;           /* 高速 */
  16.     gpio_init_struct.Alternate = GPIO_AF12_FMC;                     /* 复用为FMC */
  17.     HAL_GPIO_Init(LCD_CS_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_CS引脚 */

  18.     gpio_init_struct.Pin = LCD_WR_GPIO_PIN;
  19.     HAL_GPIO_Init(LCD_WR_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_WR引脚 */

  20.     gpio_init_struct.Pin = LCD_RD_GPIO_PIN;
  21.     HAL_GPIO_Init(LCD_RD_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_RD引脚 */

  22.     gpio_init_struct.Pin = LCD_RS_GPIO_PIN;
  23.     HAL_GPIO_Init(LCD_RS_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_RS引脚 */

  24.     gpio_init_struct.Pin = LCD_BL_GPIO_PIN;
  25.     gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;                    /* 推挽输出 */
  26.     HAL_GPIO_Init(LCD_BL_GPIO_PORT, &gpio_init_struct);     /* 初始化LCD_BL引脚 */

  27.     g_sram_handle.Instance = FMC_NORSRAM_DEVICE;
  28.     g_sram_handle.Extended = FMC_NORSRAM_EXTENDED_DEVICE;

  29. g_sram_handle.Init.NSBank = FMC_NORSRAM_BANK1;                 /* 使用NE1 */
  30. /* 不复用数据线 */
  31.     g_sram_handle.Init.DataAddressMux = FMC_DATA_ADDRESS_MUX_DISABLE;
  32. g_sram_handle.Init.MemoryType = FMC_MEMORY_TYPE_SRAM;         /* SRAM */
  33. /* 16位数据宽度 */
  34. g_sram_handle.Init.MemoryDataWidth = FMC_NORSRAM_MEM_BUS_WIDTH_16;   
  35. /* 是否使能突发访问,仅对同步突发存储器有效,此处未用到 */
  36. g_sram_handle.Init.BurstAccessMode = FMC_BURST_ACCESS_MODE_DISABLE;  
  37. /* 等待信号的极性,仅在突发模式访问下有用 */
  38. g_sram_handle.Init.WaitSignalPolarity = FMC_WAIT_SIGNAL_POLARITY_LOW;
  39. /* 存储器是在等待周期之前的一个时钟周期还是等待周期期间使能NWAIT */
  40. g_sram_handle.Init.WaitSignalActive = FMC_WAIT_TIMING_BEFORE_WS;      
  41. /* 存储器写使能 */
  42. g_sram_handle.Init.WriteOperation = FMC_WRITE_OPERATION_ENABLE;   
  43. /* 等待使能位,此处未用到 */   
  44. g_sram_handle.Init.WaitSignal = FMC_WAIT_SIGNAL_DISABLE;     
  45. /* 读写使用不同的时序 */         
  46. g_sram_handle.Init.ExtendedMode = FMC_EXTENDED_MODE_ENABLE;
  47. /* 是否使能同步传输模式下的等待信号,此处未用到 */
  48.     g_sram_handle.Init.AsynchronousWait = FMC_ASYNCHRONOUS_WAIT_DISABLE;  
  49.     g_sram_handle.Init.WriteBurst = FMC_WRITE_BURST_DISABLE; /* 禁止突发写 */
  50.     g_sram_handle.Init.ContinuousClock = FMC_CONTINUOUS_CLOCK_SYNC_ASYNC;

  51. /* FMC读时序控制寄存器 */
  52. /* 地址建立时间(ADDSET)为17个fmc_ker_ck 1/250M=4ns*17=68ns */
  53.     fmc_read_timing.AddressSetupTime = 0x11;            
  54. fmc_read_timing.AddressHoldTime = 0x00;
  55. /* 数据保存时间(DATAST)为88个fmc_ker_ck=4*88=352ns */
  56.     fmc_read_timing.DataSetupTime = 0x58;               
  57.     /* 因为液晶驱动IC的读数据的时候,速度不能太快,尤其是个别奇葩芯片 */
  58. fmc_read_timing.AccessMode = FMC_ACCESS_MODE_A;     /* 模式A */

  59. /* FMC写时序控制寄存器 */
  60. /* 数据保存时间(DATAST)为88个fmc_ker_ck=4*88=352ns */
  61.     fmc_write_timing.AddressSetupTime = 0x11;           
  62. fmc_write_timing.AddressHoldTime = 0x00;
  63. /* 数据保存时间(DATAST)为17个fmc_ker_ck=68ns */
  64.     fmc_write_timing.DataSetupTime = 0x11;
  65.     /* 17个HCLK(HCLK=250Mhz),某些液晶驱动IC的写信号脉宽,最少也得50ns */
  66.     fmc_write_timing.AccessMode = FMC_ACCESS_MODE_A;    /* 模式A */
  67.    
  68.     HAL_SRAM_Init(&g_sram_handle, &fmc_read_timing, &fmc_write_timing);
  69.     delay_ms(50);        /* 初始化FMC后,必须等待一定时间才能开始初始化 */

  70.     /* 尝试9341 ID的读取 */
  71.     lcd_wr_regno(0XD3);
  72.     lcddev.id = lcd_rd_data();                      /* dummy read */
  73.     lcddev.id = lcd_rd_data();                      /* 读到0X00 */
  74.     lcddev.id = lcd_rd_data();                      /* 读取0X93 */
  75.     lcddev.id <<= 8;
  76.     lcddev.id |= lcd_rd_data();                     /* 读取0X41 */

  77.     if (lcddev.id != 0X9341)                        /* 不是 9341 , 尝试看看是不是 ST7789 */
  78.     {
  79.         lcd_wr_regno(0X04);
  80.         lcddev.id = lcd_rd_data();                  /* dummy read */
  81.         lcddev.id = lcd_rd_data();                  /* 读到0X85 */
  82.         lcddev.id = lcd_rd_data();                  /* 读取0X85 */
  83.         lcddev.id <<= 8;
  84.         lcddev.id |= lcd_rd_data();                 /* 读取0X52 */
  85.         
  86.         if (lcddev.id == 0X8552)                    /* 将8552的ID转换成7789 */
  87.         {
  88.             lcddev.id = 0x7789;
  89.         }

  90.         if (lcddev.id != 0x7789)                    /* 也不是ST7789, 尝试是不是 NT35310 */
  91.         {
  92.             lcd_wr_regno(0xD4);
  93.             lcddev.id = lcd_rd_data();              /* dummy read */
  94.             lcddev.id = lcd_rd_data();              /* 读回0x01 */
  95.             lcddev.id = lcd_rd_data();              /* 读回0x53 */
  96.             lcddev.id <<= 8;
  97.             lcddev.id |= lcd_rd_data();             /* 这里读回0x10 */

  98.             if (lcddev.id != 0x5310)                /* 也不是NT35310,尝试看看是不是ST7796*/
  99.             {
  100.                 lcd_wr_regno(0XD3);
  101.                 lcddev.id = lcd_rd_data();          /* dummy read */
  102.                 lcddev.id = lcd_rd_data();          /* 读到0X00 */
  103.                 lcddev.id = lcd_rd_data();          /* 读取0X77 */
  104.                 lcddev.id <<= 8;
  105.                 lcddev.id |= lcd_rd_data();         /* 读取0X96 */
  106.                
  107.                 if (lcddev.id != 0x7796)    /* 也不是ST7796,尝试看看是不是NT35510 */
  108.                 {
  109.                     /* 发送密钥(厂家提供) */
  110.                     lcd_write_reg(0xF000, 0x0055);
  111.                     lcd_write_reg(0xF001, 0x00AA);
  112.                     lcd_write_reg(0xF002, 0x0052);
  113.                     lcd_write_reg(0xF003, 0x0008);
  114.                     lcd_write_reg(0xF004, 0x0001);

  115.                     lcd_wr_regno(0xC500);             /* 读取ID低八位 */
  116.                     lcddev.id = lcd_rd_data();        /* 读回0x55 */
  117.                     lcddev.id <<= 8;

  118.                     lcd_wr_regno(0xC501);               /* 读取ID高八位 */
  119.                     lcddev.id |= lcd_rd_data();         /* 读回0x10 */
  120.                     /* 等待5ms, 因为0XC501指令对1963来说就是软件复位指令,
  121. 等待5ms让1963复位完成再操作 */
  122.                     delay_ms(5);               

  123.                     if (lcddev.id != 0x5510) /* 也不是NT5510,尝试看看是不是ILI9806 */
  124.                     {
  125.                         lcd_wr_regno(0XD3);
  126.                         lcddev.id = lcd_rd_data();  /* dummy read */
  127.                         lcddev.id = lcd_rd_data();  /* 读回0X00 */
  128.                         lcddev.id = lcd_rd_data();  /* 读回0X98 */
  129.                         lcddev.id <<= 8;
  130.                         lcddev.id |= lcd_rd_data(); /* 读回0X06 */
  131.                         
  132. /* 也不是ILI9806,尝试看看是不是SSD1963 */
  133.                         if (lcddev.id != 0x9806)
  134.                         {
  135.                             lcd_wr_regno(0xA1);
  136.                             lcddev.id = lcd_rd_data();
  137.                             lcddev.id = lcd_rd_data();  /* 读回0x57 */
  138.                             lcddev.id <<= 8;
  139.                             lcddev.id |= lcd_rd_data(); /* 读回0x61 */
  140. /* SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963 */
  141.                             if (lcddev.id == 0x5761) lcddev.id = 0x1963;
  142.                         }
  143.                     }
  144.                 }
  145.             }
  146.         }
  147.     }

  148.     /* 特别注意, 如果在main函数里面屏蔽串口1初始化, 则会卡死在printf
  149.      * 里面(卡死在f_putc函数), 所以, 必须初始化串口1, 或者屏蔽掉下面
  150.      * 这行 printf 语句 !!!!!!!
  151.      */
  152.     printf("LCD ID:%x\r\n", lcddev.id); /* 打印LCD ID */

  153.     if (lcddev.id == 0X7789)
  154.     {
  155.         lcd_ex_st7789_reginit();            /* 执行ST7789初始化 */
  156.     }
  157.     else if (lcddev.id == 0X9341)
  158.     {
  159.         lcd_ex_ili9341_reginit();           /* 执行ILI9341初始化 */
  160.     }
  161.     else if (lcddev.id == 0x5310)
  162.     {
  163.         lcd_ex_nt35310_reginit();           /* 执行NT35310初始化 */
  164.     }
  165.     else if (lcddev.id == 0x7796)
  166.     {
  167.         lcd_ex_st7796_reginit();            /* 执行ST7796初始化 */
  168.     }
  169.     else if (lcddev.id == 0x5510)
  170.     {
  171.         lcd_ex_nt35510_reginit();           /* 执行NT35510初始化 */
  172.     }
  173.     else if (lcddev.id == 0x9806)
  174.     {
  175.         lcd_ex_ili9806_reginit();           /* 执行ILI9806初始化 */
  176.     }
  177.     else if (lcddev.id == 0x1963)
  178.     {
  179.         lcd_ex_ssd1963_reginit();           /* 执行SSD1963初始化 */
  180.         lcd_ssd_backlight_set(100);         /* 背光设置为最亮 */
  181.     }

  182. /* 初始化完成以后,提速 */
  183. /* ST7789/ST7796/ILI9806 提速 */
  184.     if (lcddev.id == 0X7789 || lcddev.id == 0x7796 || lcddev.id == 0x9806)            
  185.     {
  186.         /* 重新配置写时序控制寄存器的时序 */
  187.         fmc_write_timing.AddressSetupTime = 5;
  188.         fmc_write_timing.DataSetupTime = 5;
  189.         FMC_NORSRAM_Extended_Timing_Init(g_sram_handle.Extended,
  190. &fmc_write_timing, g_sram_handle.Init.NSBank,
  191. g_sram_handle.Init.ExtendedMode);
  192.     }

  193. else if (lcddev.id == 0X9341 || lcddev.id == 0X1963 || lcddev.id == 0x5310
  194. || lcddev.id == 0x5510)  /* 如果是这几个IC,则设置WR时序为最快 */
  195.     {
  196.         /* 重新配置写时序控制寄存器的时序 */
  197.         fmc_write_timing.AddressSetupTime = 3;
  198.         fmc_write_timing.DataSetupTime = 3;
  199.         FMC_NORSRAM_Extended_Timing_Init(g_sram_handle.Extended,
  200. &fmc_write_timing, g_sram_handle.Init.NSBank,
  201. g_sram_handle.Init.ExtendedMode);
  202.     }

  203.     lcd_display_dir(0); /* 默认为竖屏 */
  204.     LCD_BL(1);          /* 点亮背光 */
  205.     lcd_clear(WHITE);
  206. }
复制代码
该函数先对FMC相关IO进行初始化,然后使用HAL_SRAM_Init函数初始化FMC控制器,同时我们使用HAL_SRAM_MspInit回调函数来初始化相应的IO口,最后读取LCD控制器的型号,根据控制IC的型号执行不同的初始化代码,这样提高了整个程序的通用性。为了简化lcd.c的初始化程序,不同控制IC的芯片对应的初始化程序(如:lcd_ex_st7789_reginit()、lcd_ex_ili9341_reginit()等)我们放在lcd_ex.c文件中,这些初始化代码完成对LCD寄存器的初始化,由LCD厂家提供,一般是不需要做任何修改的,我们直接调用就可以了。
下面是6个简单,但是很重要的函数:

  1. /**
  2. * @brief               LCD写数据
  3. * [url=home.php?mod=space&uid=271674]@param[/url]               data: 要写入的数据
  4. * @retval              无
  5. */
  6. void lcd_wr_data(volatile uint16_t data)
  7. {
  8.     data = data;            /* 使用-O2优化的时候,必须插入的延时 */
  9.     LCD->LCD_RAM = data;
  10. }

  11. /**
  12. * @brief               LCD写寄存器编号/地址函数
  13. * @param               regno: 寄存器编号/地址
  14. * @retval              无
  15. */
  16. void lcd_wr_regno(volatile uint16_t regno)
  17. {
  18.     regno = regno;                /* 使用-O2优化的时候,必须插入的延时 */
  19.     LCD->LCD_REG = regno;         /* 写入要写的寄存器序号 */
  20. }

  21. /**
  22. * @brief               LCD写寄存器
  23. * @param               regno:寄存器编号/地址
  24. * @param               data:要写入的数据
  25. * @retval              无
  26. */
  27. void lcd_write_reg(uint16_t regno, uint16_t data)
  28. {
  29.     LCD->LCD_REG = regno;   /* 写入要写的寄存器序号 */
  30.     LCD->LCD_RAM = data;    /* 写入数据 */
  31. }

  32. /**
  33. * @brief             LCD延时函数,仅用于部分在mdk -O1时间优化时需要设置的地方
  34. * @param              t:延时的数值
  35. * @retval              无
  36. */
  37. static void lcd_opt_delay(uint32_t i)
  38. {
  39.     while (i--);
  40. }

  41. /**
  42. * @brief               LCD读数据
  43. * @param               无
  44. * @retval              读取到的数据
  45. */
  46. static uint16_t lcd_rd_data(void)
  47. {
  48. volatile uint16_t ram;  /* 防止被优化 */
  49.     lcd_opt_delay(2);
  50.     ram = LCD->LCD_RAM;
  51.     return ram;
  52. }

  53. /**
  54. * @brief              准备写GRAM
  55. * @param               无
  56. * @retval              无
  57. */
  58. void lcd_write_ram_prepare(void)
  59. {
  60.     LCD->LCD_REG = lcddev.wramcmd;
  61. }
复制代码
因为FMC自动控制了WR/RD/CS等这些信号,所以这6个函数实现起来都非常简单,我们就不多说,注意,上面有几个函数,我们添加了一些对MDK –O2优化的支持,去掉的话,在-O2优化的时候会出问题。这些函数实现功能见函数前面的备注,通过这几个简单函数的组合,我们就可以对LCD进行各种操作了。
下面要介绍的函数是坐标设置函数,该函数代码如下:

  1. /**
  2. * @brief              设置光标位置(对RGB屏无效)
  3. * @param               x,y: 坐标
  4. * @retval              无
  5. */
  6. void lcd_set_cursor(uint16_t x, uint16_t y)
  7. {
  8.     if (lcddev.id == 0X1963)
  9.     {
  10.         if (lcddev.dir == 0)                /* 竖屏模式, x坐标需要变换 */
  11.         {
  12.             x = lcddev.width - 1 - x;
  13.             lcd_wr_regno(lcddev.setxcmd);
  14.             lcd_wr_data(0);
  15.             lcd_wr_data(0);
  16.             lcd_wr_data(x >> 8);
  17.             lcd_wr_data(x & 0XFF);
  18.         }
  19.         else                            /* 横屏模式 */
  20.         {
  21.             lcd_wr_regno(lcddev.setxcmd);
  22.             lcd_wr_data(x >> 8);
  23.             lcd_wr_data(x & 0XFF);
  24.             lcd_wr_data((lcddev.width - 1) >> 8);
  25.             lcd_wr_data((lcddev.width - 1) & 0XFF);
  26.         }

  27.         lcd_wr_regno(lcddev.setycmd);
  28.         lcd_wr_data(y >> 8);
  29.         lcd_wr_data(y & 0XFF);
  30.         lcd_wr_data((lcddev.height - 1) >> 8);
  31.         lcd_wr_data((lcddev.height - 1) & 0XFF);

  32.     }
  33.     else if (lcddev.id == 0X5510)
  34.     {
  35.         lcd_wr_regno(lcddev.setxcmd);
  36.         lcd_wr_data(x >> 8);
  37.         lcd_wr_regno(lcddev.setxcmd + 1);
  38.         lcd_wr_data(x & 0XFF);
  39.         lcd_wr_regno(lcddev.setycmd);
  40.         lcd_wr_data(y >> 8);
  41.         lcd_wr_regno(lcddev.setycmd + 1);
  42.         lcd_wr_data(y & 0XFF);
  43.     }
  44.     else    /* 9341/5310/7789/7796/9806 等 设置坐标 */
  45.     {
  46.         lcd_wr_regno(lcddev.setxcmd);
  47.         lcd_wr_data(x >> 8);
  48.         lcd_wr_data(x & 0XFF);
  49.         lcd_wr_regno(lcddev.setycmd);
  50.         lcd_wr_data(y >> 8);
  51.         lcd_wr_data(y & 0XFF);
  52.     }
  53. }
复制代码
该函数实现将LCD的当前操作点设置到指定坐标(x,y)。因为9341/5310/1963/5510等的设置有些不太一样,所以进行了区别对待。
接下来介绍画点函数,其定义如下:

  1. /**
  2. * @brief              画点
  3. * @param               x,y: 坐标
  4. * @param               color: 点的颜色(32位颜色,方便兼容LTDC)
  5. * @retval              无
  6. */
  7. void lcd_draw_point(uint16_t x, uint16_t y, uint32_t color)
  8. {
  9.     lcd_set_cursor(x, y);               /* 设置光标位置 */
  10.     lcd_write_ram_prepare();           /* 开始写入GRAM */
  11.     LCD->LCD_RAM = color;
  12. }
复制代码
该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。lcd_draw_point函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。
下面介绍读点函数,用于读取LCD的GRAM,这里说明一下,为什么OLED模块没做读GRAM的函数,而这里做了。因为OLED模块是单色的,所需要全部GRAM也就1K个字节,而TFTLCD模块为彩色的,点数也比OLED模块多很多,以16位色计算,一款320×240的液晶,需要320×240×2个字节来存储颜色值,也就是也需要150K字节,这对任何一款单片机来说,都不是一个小数目了。而且我们在图形叠加的时候,可以先读回原来的值,然后写入新的值,在完成叠加后,我们又恢复原来的值。这样在做一些简单菜单的时候,是很有用的。这里我们读取TFTLCD模块数据的函数为lcd_read_point,该函数直接返回读到的GRAM值。该函数使用之前要先设置读取的GRAM地址,通过lcd_set_cursor函数来实现。lcd_read_point的代码如下:

  1. /**
  2. * @brief              读取个某点的颜色值
  3. * @param               x,y:坐标
  4. * @retval              此点的颜色(32位颜色,方便兼容LTDC)
  5. */
  6. uint32_t lcd_read_point(uint16_t x, uint16_t y)
  7. {
  8.     uint16_t r = 0, g = 0, b = 0;

  9.     if (x >= lcddev.width || y >= lcddev.height)return 0; /* 超过了范围,直接返回 */

  10.     lcd_set_cursor(x, y);       /* 设置坐标 */

  11.     if (lcddev.id == 0X5510)
  12.     {
  13.         lcd_wr_regno(0X2E00);   /* 5510 发送读GRAM指令 */
  14.     }
  15.     else
  16.     {
  17.         lcd_wr_regno(0X2E);    /* 9341/5310/1963/7789/7796/9806 等发送读GRAM指令 */
  18.     }

  19.     r = lcd_rd_data();         /* 假读(dummy read) */

  20.     if (lcddev.id == 0X1963)return r;   /* 1963 直接读就可以 */

  21.     r = lcd_rd_data();                          /* 实际坐标颜色 */

  22.     /* ILI9341/NT35310/NT35510/ST7789/ILI9806 要分2次读出 */
  23. b = lcd_rd_data();
  24. /* 对于 9341/5310/5510/7789/9806, 第一次读取的是RG的值,R在前,G在后,各占8位 */
  25.     g = r & 0XFF;
  26. g <<= 8;
  27. /* 9341/5310/5510/7789/9806 需要公式转换一下 */
  28.     return (((r >> 11) << 11) | ((g >> 10) << 5) | (b >> 11));  
  29. }
复制代码
在lcd_read_point函数中,因为我们的代码不止支持一种LCD驱动器,所以,我们根据不同的LCD驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数的通用性。
第十个要介绍的是字符显示函数lcd_show_char,该函数同前面OLED模块的字符显示函数差不多,但是这里的字符显示函数多了1个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。该函数实现代码如下:

  1. /**
  2. * @brief              在指定位置显示一个字符
  3. * @param               x,y  : 坐标
  4. * @param               chr  : 要显示的字符:" "--->"~"
  5. * @param              size : 字体大小 12/16/24/32
  6. * @param              mode : 叠加方式(1); 非叠加方式(0);
  7. * @retval             无
  8. */
  9. void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size,
  10. uint8_t mode, uint16_t color)
  11. {
  12.     uint8_t temp, t1, t;
  13.     uint16_t y0 = y;
  14.     uint8_t csize = 0;
  15.     uint8_t *pfont = 0;
  16. /* 得到字体一个字符对应点阵集所占的字节数 */
  17. csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2);
  18. /* 得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库) */
  19.     chr = chr - ' ';   

  20.     switch (size)
  21.     {
  22.         case 12:
  23.             pfont = (uint8_t *)asc2_1206[chr];  /* 调用1206字体 */
  24.             break;

  25.         case 16:
  26.             pfont = (uint8_t *)asc2_1608[chr];  /* 调用1608字体 */
  27.             break;

  28.         case 24:
  29.             pfont = (uint8_t *)asc2_2412[chr];  /* 调用2412字体 */
  30.             break;

  31.         case 32:
  32.             pfont = (uint8_t *)asc2_3216[chr];  /* 调用3216字体 */
  33.             break;

  34.         default:
  35.             return ;
  36.     }

  37.     for (t = 0; t < csize; t++)
  38.     {
  39.         temp = pfont[t];    /* 获取字符的点阵数据 */
  40.         for (t1 = 0; t1 < 8; t1++)   /* 一个字节8个点 */
  41.         {
  42.             if (temp & 0x80)        /* 有效点,需要显示 */
  43.             {
  44.                 lcd_draw_point(x, y, color);        /* 画点出来,要显示这个点 */
  45.             }
  46.             else if (mode == 0)     /* 无效点,不显示 */
  47.             {
  48. /* 画背景色,相当于这个点不显示(注意背景色由全局变量控制) */
  49.                 lcd_draw_point(x, y, g_back_color);
  50.             }
  51.             temp <<= 1; /* 移位, 以便获取下一个位的状态 */
  52.             y++;
  53.             if (y >= lcddev.height)return;  /* 超区域了 */
  54.             if ((y - y0) == size)   /* 显示完一列了? */
  55.             {
  56.                 y = y0; /* y坐标复位 */
  57.                 x++;    /* x坐标递增 */
  58.                 if (x >= lcddev.width)return;   /* x坐标超区域了 */
  59.                 break;
  60.             }
  61.         }
  62.     }
  63. }
复制代码
在lcd_show_char函数里面,我们用到了四个字符集点阵数据数组asc2_1206、asc2_1608、asc2_2412和asc2_3216。
lcd.c的函数比价多,其他的函数请大家自行查看源码,都有详细的注释。

2. main.c代码
在main.c里面编写如下代码:

  1. int main(void)
  2. {
  3.     uint8_t x = 0;
  4. uint8_t lcd_id[12];

  5.     HAL_Init();                                             /* HAL库初始化 */
  6.     HAL_ICACHE_Enable();                            /* 使能指令缓存 */
  7.     sys_stm32_clock_init(250, 4, 2, 2);           /* 系统时钟初始化, 250Mhz */
  8.     delay_init(250);                                 /* 延时初始化 */
  9.     usart_init(115200);                              /* 串口初始化 */
  10.     led_init();                                        /* LED初始化 */
  11.     lcd_init();                                        /* LCD初始化 */

  12. g_point_color = RED;
  13. /* 将LCD ID打印到lcd_id数组 */
  14.     sprintf((char *)lcd_id, "LCD ID:%04X", lcddev.id);

  15.     while (1)
  16.     {
  17.         switch (x)
  18.         {
  19.             case 0:         lcd_clear(WHITE);         break;
  20.             case 1:         lcd_clear(BLACK);         break;
  21.             case 2:         lcd_clear(BLUE);         break;
  22.             case 3:         lcd_clear(RED);          break;
  23.             case 4:         lcd_clear(MAGENTA);        break;
  24.             case 5:         lcd_clear(GREEN);         break;
  25.             case 6:         lcd_clear(CYAN);         break;
  26.             case 7:         lcd_clear(YELLOW);         break;
  27.             case 8:         lcd_clear(BRRED);         break;
  28.             case 9:         lcd_clear(GRAY);         break;
  29.             case 10:         lcd_clear(LGRAY);         break;
  30.             case 11:         lcd_clear(BROWN);         break;
  31.         }

  32.         lcd_show_string(10, 40, 240, 32, 32, "STM32", RED);
  33.         lcd_show_string(10, 80, 240, 24, 24, "TFTLCD TEST", RED);
  34.         lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);
  35.         lcd_show_string(10, 130, 240, 16, 16, (char*)lcd_id, RED);/* 显示LCD ID*/

  36.         x++;
  37.         if (x == 12)x = 0;

  38.         LED0_TOGGLE();  /* 红灯闪烁 */
  39.         delay_ms(1000);
  40.     }
  41. }
复制代码
main函数功能主要是显示一些固定的字符,字体大小包括32*16、24*12、16*8和12*6四种,同时显示LCD驱动IC的型号,然后不停的切换背景颜色,每1s切换一次。而LED0也会不停的闪烁,指示程序已经在运行了。其中我们用到一个sprintf的函数,该函数用法同printf,只是sprintf把打印内容输出到指定的内存区间上,sprintf的详细用法,请百度学习。
特别注意:
usart_init函数,不能去掉,因为在LCD_Init函数里面调用了printf,所以一旦去掉这个初始化,就会死机!实际上,只要你的代码有用到printf,就必须初始化串口,否则都会死机,即停在usart.c里面的fputc函数,出不来。


23.4 下载验证
下载代码后,LED0不停的闪烁,提示程序已经在运行了。同时可以看到TFTLCD模块的显示背景色不停切换,如图23.4.1所示:

第二十三章 TFTLCD44024.png
图23.4.1 TFTLCD显示效果图

此外,为了让大家能直观的了解LCD屏的扫描方式,我们额外编写了两个main.c文件(main1.c和main2.c,放到User文件夹中),方便大家编译下载,观察现象。
使用方法:关闭工程后,先把原实验中的main.c改成其他名字,然后把main1.c重命名为main.c,双击keilkill.bat清理编译的中间文件,最后打开工程重新编译下载,就可以观察实验现象。观察了main1.c,可以再观察main2.c,main2.c文件的操作方法类似。这两个main.c文件的程序非常简单,这里就不讲解,具体请看源码。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-8-23 04:07

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表