实验十四 TFTLCD显示-FSMC

一.实验目标

\1. 通过本实验熟悉STM32的FSMC接口;

2.熟悉TFTLCD显示屏使用。

3.掌握在2.4寸显示屏上显示操作。

二.知识储备及设计思路

TFTLCD简介

TFT-LCD即薄膜晶体管液晶显示器。其英文全称为:Thin Film Transistor-Liquid Crystal Display。TFT-LCD与无源TN-LCD、STN-LCD的简单矩阵不同,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT-LCD也被叫做真彩液晶显示器。

本章,我们给大家介绍TFTLCD模块,该模块有如下特点:

1,2.4’寸大小的屏幕。

2,320×240的分辨率。

3,16位真彩显示。

TFTLCD模块模块支持65K色显示,显示分辨率为320×240,接口为16位的80并口,该模块的外观图如图所示:

img

管脚接口定义:

img

和单片机连接方式:

img

imgimg

​ 如上图所示,屏幕和开发板P1座自连接方式。

从图可以看出, TFTLCD模块采用16位的并方式与外部连接,之所以不采用8位的方式,是因为彩屏的数据量比较大,尤其在显示图片的时候,如果用8位数据线,就会比16位方式慢一倍以上,我们当然希望速度越快越好,所以我们选择16位的接口。该模块的80并口有如下一些信号线:

CS:TFTLCD片选信号。

WR:向TFTLCD写入数据。

RD:从TFTLCD读取数据。

D[15:0]:16位双向数据线。

RST:硬复位TFTLCD。

RS:命令/数据标志(0,读写命令;1,读写数据)

BLK: 背光控制。

TFTLCD模块的RST信号线是直接接到STM32F4的复位脚上,并不由软件控制,这样可以省下来一个IO口。另外我们还需要一个背光控制线来控制TFTLCD的背光。所以,我们总共需要的IO口数目为21个。这里还需要注意,我们标注的DB0DB15,是相对于LCD控制IC标注的,实际上大家可以把他们就等同于FMC_D0 FMC_D15。

ILI9341液晶控制器自带显存,其显存总大小为172800(24032018/8),即18位模式(26万色)下的显存量。在16位模式下,ILI9341采用RGB565格式存储颜色数据,此时ILI9341的18位数据线与MCU的16位数据线以及LCD GRAM的对应关系如图:

img

从图中可以看出,ILI9341在16位模式下面,数据线有用的是:D17D13和D11D1,D0和D12没有用到,,ILI9341的D0和D12压根就没有引出来,这样,ILI9341的D17D13和D11D1对应MCU的FMC_D0~ FMC_D15。这样MCU的16位数据,最低5位代表蓝色,中间6位为绿色,最高5位为红色。数值越大,表示该颜色越深。另外,特别注意ILI9341所有的指令都是8位的(高8位无效),且参数除了读写GRAM的时候是16位,其他操作参数,都是8位的。

接下来,我们介绍一下ILI9341的几个重要命令,因为ILI9341的命令很多,我们这里就不全部介绍了,有兴趣的大家可以找到ILI9341的datasheet看看。里面对这些命令有详细的介绍。我们将介绍:0XD3,0X36,0X2A,0X2B,0X2C,0X2E等6条指令。

首先来看指令:0XD3,这个是读ID4指令,用于读取LCD控制器的ID,该指令如表所示:

img

从上表可以看出,0XD3指令后面跟了4个参数,最后2个参数,读出来是0X93和0X41,刚好是我们控制器ILI9341的数字部分,从而,通过该指令,即可判别所用的LCD驱动器是什么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动IC的初始化代码,从而兼容不同驱动IC的屏,使得一个代码支持多款LCD。

接下来看指令:0X36,这是存储访问控制指令,可以控制ILI9341存储器的读写方向,简单的说,就是在连续写GRAM的时候,可以控制GRAM指针的增长方向,从而控制显示方式(读GRAM也是一样)。该指令如表所示:

img

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

img

img

这样,我们在利用ILI9341显示内容的时候,就有很大灵活性了,比如显示BMP图片,BMP解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置LCD扫描方向为从左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往LCD填充颜色数据即可,这样可以大大提高显示速度。

接下来看指令:0X2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置横坐标(x坐标),该指令如表所示:

img

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

与0X2A指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置纵坐标(y坐标)。该指令如表所示:

img

在默认扫描方式时,该指令用于设置y坐标,该指令带有4个参数,实际上是2个坐标值:SP和EP,即页地址的起始值和结束值,SP必须小于等于EP,且0≤SP/EP≤319。一般在设置y坐标的时候,我们只需要带2个参数即可,也就是设置SP即可,因为如果EP没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。

接下来看指令:0X2C,该指令是写GRAM指令,在发送该指令之后,我们便可以往LCD的GRAM里面写入颜色数据了,该指令支持连续写,指令描述如表所示:

img

img

从上表可知,在收到指令0X2C之后,数据有效位宽变为16位,我们可以连续写入LCD GRAM值,而GRAM的地址将根据MY/MX/MV设置的扫描方向进行自增。例如:假设设置的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过SC,SP设置)后,每写入一个颜色值,GRAM地址将会自动自增1(SC++),如果碰到EC,则回到SC,同时SP++,一直到坐标:EC,EP结束,其间无需再次设置的坐标,从而大大提高写入速度。

最后,来看看指令:0X2E,该指令是读GRAM指令,用于读取ILI9341的显存(GRAM),该指令在ILI9341的数据手册上面的描述是有误的,真实的输出情况如表所示:

img

该指令用于读取GRAM,如表所示,ILI9341在收到该指令后,第一次输出的是dummy数据,也就是无效的数据,第二次开始,读取到的才是有效的GRAM数据(从坐标:SC,SP开始),输出规律为:每个颜色分量占8个位,一次输出2个颜色分量。比如:第一次输出是R1G1,随后的规律为:B1R2àG2B2àR3G3àB3R4àG4B4àR5G5… 以此类推。如果我们只需要读取一个点的颜色值,那么只需要接收到参数3即可,如果要连续读取(利用GRAM地址自增,方法同上),那么就按照上述规律去接收颜色数据。

以上,就是操作ILI9341常用的几个指令,通过这几个指令,我们便可以很好的控制ILI9341显示我们所要显示的内容了。

一般TFTLCD模块的使用流程如图:

img

任何LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要执行一次即可。而画点流程就是:设置坐标à写GRAM 指令à写入颜色数据,然后在LCD 上面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标à读GRAM 指令à读取颜色数据,这样就可以获取到对应点的颜色数据了。

以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用TFTLCD 了。接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出TFTLCD 显示需要的相关设置步骤如下:

1)设置STM32F4TFTLCD 模块相连接的IO

这一步,先将我们与TFTLCD 模块相连的IO 口进行初始化,以便驱动LCD。这里我们用到的是FSMC。

2)初始化TFTLCD 模块。

初始化序列,这里我们没有硬复位LCD,因为开发板的LCD 接口,将TFTLCD 的RST 同STM32F4 的RESET 连接在一起了,只要按下开发板的RESET 键,就会对LCD 进行硬复位。初始化序列,就是向LCD 控制器写入一系列的设置值,这些初始化序列一般LCD 供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。在初始化之后,LCD 才可以正常使用。

3)通过函数将字符和数字显示到TFTLCD 模块上。

这一步的流程,即:设置坐标à写GRAM 指令à写GRAM 来实现,但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数,就可以实现数字/字符的显示了。

FSMC 简介

STM32F407 或STM32F417 系列芯片都带有FSMC 接口, STM32F4 开发板的主芯片为STM32F407IGT6,是带有FSMC 接口的。

FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和16 位PC 存储器卡连接,STM32F4 的FSMC 接口支持包括SRAM、NAND FLASH、NOR FLASH 和PSRAM 等存储器。FSMC 的框图如图所示:

img

从上图我们可以看出,STM32F4的FSMC将外部设备分为2类:NOR/PSRAM设备、NAND/PC卡设备。他们共用地址数据总线等信号,他们具有不同的CS以区分不同的设备,比如本章我们用到的TFTLCD就是用的FSMC_NE4做片选,其实就是将TFTLCD当成SRAM来控制。

这里我们介绍下为什么可以把TFTLCD当成SRAM设备用:首先我们了解下外部SRAM的连接,外部SRAM的控制一般有:地址线(如A0A18)、数据线(如D0D15)、写信号(WE)、读信号(OE)、片选信号(CS),如果SRAM支持字节控制,那么还有UB/LB信号。而TFTLCD的信号我们前面有介绍,包括:RS、D0D15、WR、RD、CS、RST和BL等,其中真正在操作LCD的时候需要用到的就只有:RS、D0D15、WR、RD和CS。其操作时序和SRAM的控制完全类似,唯一不同就是TFTLCD有RS信号,但是没有地址信号。

TFTLCD通过RS信号来决定传送的数据是数据还是命令,本质上可以理解为一个地址信号,比如我们把RS接在A0上面,那么当FSMC控制器写地址0的时候,会使得A0变为0,对TFTLCD来说,就是写命令。而FSMC写地址1的时候,A0将会变为1,对TFTLCD来说,就是写数据了。这样,就把数据和命令区分开了,他们其实就是对应SRAM操作的两个连续地址。当然RS也可以接在其他地址线上,开发板STM32F4开发板是把RS连接在A18上面的。

STM32F4的FSMC支持8/16/32位数据宽度,我们这里用到的LCD是16位宽度的,所以在设置的时候,选择16位宽就OK了。我们再来看看FSMC的外部设备地址映像,STM32F4的FSMC将外部存储器划分为固定大小为256M字节的四个存储块,如图所示:

img

从上图可以看出,FSMC总共管理1GB空间,拥有4个存储块(Bank),本章,我们用到的是块1,所以在本章我们仅讨论块1的相关配置,其他块的配置,请参考《STM32F4xx中文参考手册》第32章(1191页)的相关介绍。

STM32F4的FSMC存储块1(Bank1)被分为4个区,每个区管理64M字节空间,每个区都有独立的寄存器对所连接的存储器进行配置。Bank1的256M字节空间由28根地址线(HADDR[27:0])寻址。

这里HADDR是内部AHB地址总线,其中HADDR[25:0]来自外部存储器地址FSMC_A[25:0],而HADDR[26:27]对4个区进行寻址。如表所示:

img

我们要特别注意HADDR[25:0]的对应关系:

当Bank1接的是16位宽度存储器的时候:HADDR[25:1]àFSMC_A[24:0]。

当Bank1接的是8位宽度存储器的时候:HADDR[25:0]àFSMC_A[25:0]。

不论外部接8位**/16位宽设备,FSMC_A[0]永远接在外部设备地址A[0]**。这里,TFTLCD使用的是16位数据宽度,所以HADDR[0]并没有用到,只有HADDR[25:1]是有效的,对应关系变为:HADDR[25:1]àFSMC_A[24:0],相当于右移了一位,这里请大家特别留意。另外,HADDR[27:26]的设置,是不需要我们干预的,比如:当你选择使用Bank1的第三个区,即使用FSMC_NE3来连接外部设备的时候,即对应了HADDR[27:26]=10,我们要做的就是配置对应第3区的寄存器组,来适应外部设备即可。STM32F4的FSMC各Bank配置寄存器如表:

img

对于NOR FLASH控制器,主要是通过FSMC_BCRx、FSMC_BTRx和FSMC_BWTRx寄存器设置(其中x=1~4,对应4个区)。通过这3个寄存器,可以设置FSMC访问外部存储器的时序参数,拓宽了可选用的外部存储器的速度范围。FSMC的NOR FLASH控制器支持同步和异步突发两种访问方式。选用同步突发访问方式时,FSMC将HCLK(系统时钟)分频后,发送给外部存储器作为同步时钟信号FSMC_CLK。此时需要的设置的时间参数有2个:

1,HCLK与FSMC_CLK的分频系数(CLKDIV),可以为2~16分频;

2,同步突发访问中获得第1个数据所需要的等待延迟(DATLAT)。

对于异步突发访问方式,FSMC主要设置3个时间参数:地址建立时间(ADDSET)、数据建立时间(DATAST)和地址保持时间(ADDHLD)。FSMC综合了SRAM/ROM、PSRAM和NOR Flash产品的信号特点,定义了4种不同的异步时序模型。选用不同的时序模型时,需要设置不同的时序参数,如表所列:

img

在实际扩展时,根据选用存储器的特征确定时序模型,从而确定各时间参数与存储器读/写周期参数指标之间的计算关系;利用该计算关系和存储芯片数据手册中给定的参数指标,可计算出FSMC所需要的各时间参数,从而对时间参数寄存器进行合理的配置。

本章,我们使用异步模式A(ModeA)方式来控制TFTLCD,模式A的读操作时序如图所示:

img

模式A读操作时序图

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

模式A的写操作时序如图所示:

img

模式A写操作时序

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

SRAM/NOR闪存片选控制寄存器:FSMC_BCRx(x=1~4),该寄存器各位描述如图所示:

img

图FSMC_BCRx寄存器各位描述

该寄存器我们在本章用到的设置有:EXTMOD、WREN、MWID、MTYP和MBKEN这几个设置,我们将逐个介绍。

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、ROM;01表示PSRAM;10表示NOR FLASH;11保留。前面提到,我们把TFTLCD当成SRAM用,所以需要设置MTYP[1:0]=00。

MBKEN:存储块使能位。这个容易理解,我们需要用到该存储块控制TFTLCD,当然要使能这个存储块了。

​ SRAM/NOR闪存片选时序寄存器:FSMC_BTRx(x=1~4),该寄存器各位描述如图所示:

img

FSMC_BTRx寄存器各位描述

这个寄存器包含了每个存储器块的控制信息,可以用于SRAM、ROM和NOR闪存存储器。如果FSMC_BCRx寄存器中设置了EXTMOD位,则有两个时序寄存器分别对应读(本寄存器)和写操作(FSMC_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个HCLK周期。对ILI9341来说,其实就是RD低电平持续时间,一般为355ns。而一个HCLK时钟周期为6ns左右(1/168Mhz),为了兼容其他屏,我们这里设置DATAST为60,也就是60个HCLK周期,时间大约是360ns。

ADDSET[3:0]:地址建立时间。其建立时间为:ADDSET个HCLK周期,最大为15个HCLK周期。对ILI9341来说,这里相当于RD高电平持续时间,为90ns,我们设置ADDSET为15,即15*6=90ns。

SRAM/NOR闪写时序寄存器:FSMC_BWTRx(x=1~4),该寄存器各位描述如图所示:

img

FSMC_BWTRx寄存器各位描述

该寄存器在本章用作写操作时序控制寄存器,需要用到的设置同样是:ACCMOD、DATAST和ADDSET这三个设置。这三个设置的方法同FSMC_BTRx一模一样,只是这里对应的是写操作的时序,ACCMOD设置同FSMC_BTRx一模一样,同样是选择模式A,另外DATAST和ADDSET则对应低电平和高电平持续时间,对ILI9341来说,这两个时间只需要15ns就够了,比读操作快得多。所以我们这里设置DATAST为2,即3个HCLK周期,时间约为18ns。然后ADDSET设置为3,即3个HCLK周期,时间为18ns。

至此,我们对STM32F4的FSMC介绍就差不多了,通过以上两个小节的了解,我们可以开始写LCD的驱动代码了。不过,这里还要给大家做下科普,在MDK的寄存器定义里面,并没有定义FSMC_BCRx、FSMC_BTRx、FSMC_BWTRx等这个单独的寄存器,而是将他们进行了一些组合。

FSMC_BCRx和FSMC_BTRx,组合成BTCR[8]寄存器组,他们的对应关系如下:

BTCR[0]对应FSMC_BCR1,BTCR[1]对应FSMC_BTR1

BTCR[2]对应FSMC_BCR2,BTCR[3]对应FSMC_BTR2

BTCR[4]对应FSMC_BCR3,BTCR[5]对应FSMC_BTR3

BTCR[6]对应FSMC_BCR4,BTCR[7]对应FSMC_BTR4

FSMC_BWTRx则组合成BWTR[7],他们的对应关系如下:

BWTR[0]对应FSMC_BWTR1,BWTR[2]对应FSMC_BWTR2,

BWTR[4]对应FSMC_BWTR3,BWTR[6]对应FSMC_BWTR4,

BWTR[1]、BWTR[3]和BWTR[5]保留,没有用到。

通过上面的讲解,通过对FSMC相关的寄存器的描述,大家对FSMC的原理有了一个初步的认识,如果还不熟悉的朋友,请一定要搜索网络资料理解FSMC的原理。只有理解了原理,使用库函数才可以得心应手。那么在库函数中是怎么实现FSMC的配置的呢?FSMC_BCRx,FSMC_BTRx寄存器在库函数是通过什么函数来配置的呢?下面我们来讲解一下FSMC相关的库函数:

FSMC初始化函数

根据前面的讲解,初始化FSMC主要是初始化三个寄存器FSMC_BCRx,FSMC_BTRx,FSMC_BWTRx,在HAL库中提供了FSMC初始化函数为

1
HAL_SRAM_Init();  

下面我们看看函数定义:

1
HAL_StatusTypeDef HAL_SRAM_Init(SRAM_HandleTypeDef *hsram, FMC_NORSRAM_TimingTypeDef  *Timing, FMC_NORSRAM_TimingTypeDef *ExtTiming);  

​ 这个函数有三个入口参数,SRAM_HandleTypeDef类型指针变量、FMC_NORSRAM_TimingTypeDef类型指针变量、FMC_NORSRAM_TimingTypeDef类型指针变量。

FMC_NORSRAM_TimingTypeDef指针类型的成员变量。前面我们讲到,FSMC有读时序和写时序之分,所以这里就是用来设置读时序和写时序的参数了,也就是说,这两个参数是用来配置寄存器FSMC_BTRx和FSMC_BWTRx,后面我们会讲解到。下面我们主要来看看模式A下的相关配置参数:

参数NSBank用来设置使用到的存储块标号和区号,前面讲过,我们是使用的存储块1区号1,所以选择值为FSMC_NORSRAM_BANK1。

参数MemoryType用来设置存储器类型,我们这里是SRAM,所以选择值为

FSMC_MEMORY_TYPE_SRAM。

参数MemoryDataWidth用来设置数据宽度,可选8位还是16位,这里我们是16位数据宽度,所以选择值为FSMC_NORSRAM_MEM_BUS_WIDTH_16。

参数WriteOperation用来设置写使能,毫无疑问,我们前面讲解过我们要向TFT写数据,所以要写使能,这里我们选择FSMC_WRITE_OPERATION_ENABLE。

参数ExtendedMode是设置扩展模式使能位,也就是是否允许读写不同的时序,这里我们采取的读写不同时序,所以设置值为FSMC_EXTENDED_MODE_ENABLE。

上面的这些参数是与模式A相关的,下面我们也来稍微了解一下其他几个参数的意义吧:

参数DataAddressMux用来设置地址/数据复用使能,若设置为使能,那么地址的低16位和数据将共用数据总线,仅对NOR和PSRAM有效,所以我们设置为默认值不复用,值

FSMC_DATA_ADDRESS_MUX_DISABLE。

其他参数在成组模式同步模式才需要设置,大家可以参考中文参考手册了解相关参数的意思。

接下来我们看看设置读写时序参数的两个变量FSMC_ReadWriteTim和FSMC_WriteTim,他们都是FSMC_NORSRAM_TimingTypeDef结构体指针类型,这两个参数在初始化的时候分别用来初始化片选控制寄存器FSMC_BTRx和写操作时序控制寄存器FSMC_BWTRx。下面我们看看FSMC_NORSRAMTimingInitTypeDef类型的定义:

1
typedef  struct   {   uint32_t AddressSetupTime;  uint32_t  AddressHoldTime;   uint32_t  DataSetupTime;   uint32_t  BusTurnAroundDuration;   uint32_t  CLKDivision;   uint32_t  DataLatency;   uint32_t  AccessMode;   }FSMC_NORSRAM_TimingTypeDef;  

这个结构体有7个参数用来设置FSMC读写时序。其实这些参数的意思我们前面在讲解FSMC的时序的时候有提到,主要是设计地址建立保持时间,数据建立时间等等配置,对于我们的实验中,读写时序不一样,读写速度要求不一样,所以对于参数FSMC_DataSetupTime设置了不同的值,大家可以对照理解一下。记住,这些参数的意义在前面讲解FSMC_BTRx和FSMC_BWTRx寄存器的时候都有提到,大家可以翻过去看看。

三.引脚说明与硬件连接

我们显示屏十连接在P1插座下方,P1是连接STM32内部FSMC总线。打开开发板的原理图,如图1所示,在使用过程中将TFTLCD屏幕向下对齐插在P1排母座上。

img

图1 TFTLCD显示屏接口

表1 开发板上TFTLCD输出脚位置

设备名 引脚号 设备名 引脚号
3V3 3V3 PF13 PF13
GND GND FMC_D15 PD10
FMC_D14 PD9 FMC_D13 PD8
FMC_D12 PE15 FMC_D11 PE14
FMC_D10 PE13 FMC_D9 PE12
FMC_D8 PE11 FMC_D7 PE10
FMC_D6 PE9 FMC_D5 PE8
FMC_D4 PE7 FMC_D3 PD1
FMC_D2 PD0 FMC_D1 PD15
FMC_D0 PD14 RESET RESET
FMC_NOE PD4 FMC_NWE PD5
FMC_A18 PD13 FMC_NE1 PD7

图中圈出来的部分就是连接TFTLCD模块的接口,液晶模块直接插上去即可。

在硬件上,TFTLCD模块与探索者STM32F4开发板的IO口对应关系如下:

LCD_BL(背光控制)对应PF13;

LCD_CS对应PD7即FMC_NE1;

LCD _RS对应PD13即FMC_A18;

LCD _WR对应PD5即FMC_NWE;

LCD _RD对应PD4即FMC_NOE;

LCD _D[15:0]则直接连接在FMC_D15~FMC_D0;

这些线的连接,探索者STM32F4开发板的内部已经连接好了,我们只需要将TFTLCD模块插上去就好了。实物连接如图18.2.2所示:

​ TFTLCD屏幕和P1座子连接如图所示:

img

四.程序设计

本实验,我们新建了lcd.c , lcd.h , font.h ,文件。文件用来存放lcd相关的驱动函数及显示代码。

同时,FSMC相关的库函数分布在stm32f4xx_hal_fsmc.c文件和头文件stm32f4xx_hal_fsmc.h中。配置FSMC接口的文件在fsmc.c中。

本实验,我们用到FSMC驱动LCD,通过前面的介绍,我们知道TFTLCD的RS接在FSMC的A18上面,CS接在FSMC_NE1上,并且是16位数据总线。即我们使用的是FSMC存储器1的第4区,我们定义如下LCD操作结构体(在lcd.h里面定义):

1
//LCD操作结构体  typedef struct   {   vu16 LCD_REG;   vu16 LCD_RAM;   } LCD_TypeDef;   //使用NOR/SRAM的Bank1.sector4,地址位HADDR[27,26]=11 A18作为数据命令区分线  //注意16位数据总线时,STM32内部地址会右移一位对齐!   #define LCD_BASE ((u32)(0x60000000 | 0x0007FFFE))   #define LCD ((LCD_TypeDef *) LCD_BASE)  

其中LCD_BASE,必须根据我们外部电路的连接来确定,我们使用Bank1.sector1就是从地址0X60000000开始,而0x0007FFFE,则是A18的偏移量,这里很多朋友不理解这个偏移量的概念,简单说明下:以A18为例,7FFFE转换成二进制就是:1111111111111111110,而16位数据时,地址右移一位对齐,那么实际对应到地址引脚的时候,就是:A18:A0=0111111111111111111,此时A18是0,但是如果16位地址再加1(注意:对应到8位地址是加2,即7FFFE +0X02),那么:A18:A0=1000000000000000000,此时A18就是1了,即实现了对RS的0和1的控制。

我们将这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的地址就是0X6007FFFE,对应A18的状态为0(即RS=0),而LCD-> LCD_RAM的地址就是0X6080,0000(结构体地址自增),对应A18的状态为1(即RS=1)。

所以,有了这个定义,当我们要往LCD写命令/数据的时候,可以这样写:

1
LCD->LCD_REG=CMD;  //写命令  LCD->LCD_RAM=DATA; //写数据  

而读的时候反过来操作就可以了,如下所示:

1
CMD=  LCD->LCD_REG;//读LCD寄存器  DATA = LCD->LCD_RAM;//读LCD  

这其中,CS、WR、RD和IO口方向都是由FSMC控制,不需要我们手动设置了。接下来,我们先介绍一下lcd.h里面的另一个重要结构体:

1
//LCD重要参数集  typedef struct   {   u16 width; //LCD 宽度  u16 height; //LCD 高度  u16 id; //LCD ID   u8 dir; //横屏还是竖屏控制:0,竖屏;1,横屏。  u16 wramcmd; //开始写gram指令  u16 setxcmd; //设置x坐标指令  u16 setycmd; //设置y坐标指令  }_lcd_dev;   //LCD参数  extern _lcd_dev lcddev; //管理LCD重要参数 

该结构体用于保存一些LCD重要参数信息,比如LCD的长宽、LCD ID(驱动IC型号)、LCD横竖屏状态等,这个结构体虽然占用了十几个字节的内存,但是却可以让我们的驱动函数支持不同尺寸的LCD,同时可以实现LCD横竖屏切换等重要功能,所以还是利大于弊的。有了以上了解,下面我们开始介绍lcd.c里面的一些重要函数。

先看7个简单,但是很重要的函数:

1
//写寄存器函数  //regval:寄存器值  void LCD_WR_REG(vu16 regval)   { regval=regval; //使用-O2优化的时候,必须插入的延时  LCD->LCD_REG=regval;//写入要写的寄存器序号  }   //写LCD数据  //data:要写入的值  void LCD_WR_DATA(vu16 data)   { data=data; //使用-O2优化的时候,必须插入的延时  LCD->LCD_RAM=data;   }  //读LCD数据  //返回值:读到的值  u16 LCD_RD_DATA(void)   { vu16 ram; //防止被优化  ram=LCD->LCD_RAM;   return ram;   }   //写寄存器  //LCD_Reg:寄存器地址  //LCD_RegValue:要写入的数据  void LCD_WriteReg(vu16 LCD_Reg, vu16 LCD_RegValue)   { LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号  LCD->LCD_RAM = LCD_RegValue; //写入数据  }   //读寄存器  //LCD_Reg:寄存器地址  //返回值:读到的数据  u16 LCD_ReadReg(vu16 LCD_Reg)   { LCD_WR_REG(LCD_Reg); //写入要读的寄存器序号  delay_us(5);   return LCD_RD_DATA(); //返回读到的值  }   //开始写GRAM   void LCD_WriteRAM_Prepare(void)   { LCD->LCD_REG=lcddev.wramcmd;   }   //LCD写GRAM    //RGB_Code:颜色值  void LCD_WriteRAM(u16 RGB_Code)   { LCD->LCD_RAM = RGB_Code;//写十六位GRAM   }  

该函数实现将LCD的当前操作点设置到指定坐标(x,y)。因为9341/5310/6804/5510等的设置同其他屏有些不太一样,所以进行了区别对待。

接下来我们介绍第八个函数:画点函数。该函数实现代码如下:

1
//画点  //x,y:坐标  //POINT_COLOR:此点的颜色  void LCD_DrawPoint(u16 x,u16 y)   {   LCD_SetCursor(x,y); //设置光标位置  LCD_WriteRAM_Prepare(); //开始写入GRAM   LCD->LCD_RAM=POINT_COLOR;  }  

该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。其中POINT_COLOR是我们定义的一个全局变量,用于存放画笔颜色,顺带介绍一下另外一个全局变量:BACK_COLOR,该变量代表LCD的背景色。LCD_DrawPoint函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。

有了画点,当然还需要有读点的函数,第九个介绍的函数就是读点函数,用于读取LCD的GRAM,这里我们读取TFTLCD模块数据的函数为LCD_ReadPoint,该函数直接返回读到的GRAM值。该函数使用之前要先设置读取的GRAM地址,通过LCD_SetCursor函数来实现。LCD_ReadPoint的代码如下:

1
//读取个某点的颜色值  //x,y:坐标  //返回值:此点的颜色  u16 LCD_ReadPoint(u16 x,u16 y)   {   vu16 r=0,g=0,b=0;   if(x>=lcddev.width||y>=lcddev.height)return 0; //超过了范围,直接返回  LCD_SetCursor(x,y);   if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310)LCD_WR_REG(0X2E);    //9341/6804/3510 发送读GRAM指令  else if(lcddev.id==0X5510)LCD_WR_REG(0X2E00); //5510 发送读GRAM指令  else LCD_WR_REG(R34); //其他IC发送读GRAM指令  if(lcddev.id==0X9320)opt_delay(2); //FOR 9320,延时2us   LCD_RD_DATA(); //dummy Read   opt_delay(2);   r=LCD_RD_DATA(); //实际坐标颜色  if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)    { //9341/NT35310/NT35510要分2次读出  opt_delay(2);   b=LCD_RD_DATA();   g=r&0XFF;//9341/5310/5510等,第一次读取的是RG的值,R在前,G在后,各占8位  g<<=8;   }   if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0XB505||    lcddev.id==0XC505)return r; //这几种IC直接返回颜色值  else  if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)return  (((r>>11)<<11)   |((g>>10)<<5)|(b>>11));  //ILI9341/NT35310/NT35510需要公式转换一下  else return LCD_BGR2RGB(r); //其他IC   }  

在LCD_ReadPoint函数中,因为我们的代码不止支持一种LCD驱动器,所以,我们根据不同的LCD驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数的通用性。

第十个要介绍的是字符显示函数LCD_ShowChar,该函数同前面OLED模块的字符显示函数差不多,但是这里的字符显示函数多了1个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。该函数实现代码如下:

1
//在指定位置显示一个字符  //x,y:起始坐标  //num:要显示的字符:" "--->"~"   //size:字体大小12/16/24   //mode:叠加方式(1)还是非叠加方式(0)   void  LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode)  {                                    u8 temp,t1,t;       u16 y0=y;       u8  csize=(size/8+((size%8)?1:0))*(size/2);               num=num-'  ';       for(t=0;t<csize;t++)       {               if(size==12)temp=asc2_1206[num][t];                   else  if(size==16)temp=asc2_1608[num][t];               else  if(size==24)temp=asc2_2412[num][t];               else  if(size==32)temp=asc2_3216[num][t];               else return;                                                for(t1=0;t1<8;t1++)            {                                  if(temp&0x80)LCD_Fast_DrawPoint(x,y,POINT_COLOR);                else  if(mode==0)LCD_Fast_DrawPoint(x,y,BACK_COLOR);                temp<<=1;                y++;                if(y>=lcddev.height)return;                        if((y-y0)==size)                {                     y=y0;                     x++;                     if(x>=lcddev.width)return;                         break;                }            }           }                      }  

在LCD_ShowChar函数里面,我们采用快速画点函数LCD_Fast_DrawPoint来画点显示字符,该函数同LCD_DrawPoint一样,只是带了颜色参数,且减少了函数调用的时间,详见本例程源码。该代码中我们用到了三个字符集点阵数据数组asc2_2412、asc2_1206和asc2_1608,

最后,我们再介绍一下FSMC初始化函数MX_FSMC_Init ,该函数先初始化STM32与TFTLCD连接的IO口,并配置FSMC控制器,然后其简化代码如下:

1
void  MX_FSMC_Init(void)  {   FSMC_NORSRAM_TimingTypeDef Timing = {0};   FSMC_NORSRAM_TimingTypeDef ExtTiming = {0};      /** Perform the SRAM1 memory initialization  sequence   */   hsram1.Instance = FSMC_NORSRAM_DEVICE;   hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;   /* hsram1.Init */   hsram1.Init.NSBank = FSMC_NORSRAM_BANK1;   hsram1.Init.DataAddressMux =  FSMC_DATA_ADDRESS_MUX_DISABLE;   hsram1.Init.MemoryType =  FSMC_MEMORY_TYPE_SRAM;   hsram1.Init.MemoryDataWidth =  FSMC_NORSRAM_MEM_BUS_WIDTH_16;   hsram1.Init.BurstAccessMode =  FSMC_BURST_ACCESS_MODE_DISABLE;   hsram1.Init.WaitSignalPolarity =  FSMC_WAIT_SIGNAL_POLARITY_LOW;   hsram1.Init.WrapMode =  FSMC_WRAP_MODE_DISABLE;   hsram1.Init.WaitSignalActive =  FSMC_WAIT_TIMING_BEFORE_WS;   hsram1.Init.WriteOperation =  FSMC_WRITE_OPERATION_ENABLE;   hsram1.Init.WaitSignal =  FSMC_WAIT_SIGNAL_DISABLE;   hsram1.Init.ExtendedMode =  FSMC_EXTENDED_MODE_ENABLE;   hsram1.Init.AsynchronousWait =  FSMC_ASYNCHRONOUS_WAIT_DISABLE;   hsram1.Init.WriteBurst =  FSMC_WRITE_BURST_DISABLE;   hsram1.Init.PageSize = FSMC_PAGE_SIZE_NONE;   /* Timing */   Timing.AddressSetupTime = 15;   Timing.AddressHoldTime = 15;   Timing.DataSetupTime = 255;   Timing.BusTurnAroundDuration = 15;   Timing.CLKDivision = 16;   Timing.DataLatency = 17;   Timing.AccessMode = FSMC_ACCESS_MODE_A;   /* ExtTiming */   ExtTiming.AddressSetupTime = 15;   ExtTiming.AddressHoldTime = 15;   ExtTiming.DataSetupTime = 255;   ExtTiming.BusTurnAroundDuration = 15;   ExtTiming.CLKDivision = 16;   ExtTiming.DataLatency = 17;   ExtTiming.AccessMode = FSMC_ACCESS_MODE_A;      if (HAL_SRAM_Init(&hsram1, &Timing,  &ExtTiming) != HAL_OK)   {    Error_Handler( );   }     }  

TFTLCD模块的初始化函数LCD_Init,读取LCD控制器的型号,根据控制IC的型号执行不同的初始化代码,

1
//尝试9341 ID的读取  LCD_WR_REG(0XD3);   lcddev.id=LCD_RD_DATA(); //dummy read   lcddev.id=LCD_RD_DATA(); //读到0X00   lcddev.id=LCD_RD_DATA(); //读取93   lcddev.id<<=8;   lcddev.id|=LCD_RD_DATA(); //读取41   if(lcddev.id!=0X9341) //非9341,尝试看看是不是NT35310   {   LCD_WR_REG(0XD4);   lcddev.id=LCD_RD_DATA();//dummy read   lcddev.id=LCD_RD_DATA();//读回0X01   lcddev.id=LCD_RD_DATA();//读回0X53   lcddev.id<<=8;   lcddev.id|=LCD_RD_DATA(); //这里读回0X10   if(lcddev.id!=0X5310) //也不是NT35310,尝试看看是不是NT35510   {   LCD_WR_REG(0XDA00);   lcddev.id=LCD_RD_DATA(); //读回0X00   LCD_WR_REG(0XDB00);   lcddev.id=LCD_RD_DATA(); //读回0X80   lcddev.id<<=8;  LCD_WR_REG(0XDC00);    lcddev.id|=LCD_RD_DATA();  //读回0X00   if(lcddev.id==0x8000)lcddev.id=0x5510;   //NT35510读回的ID是8000H,为方便区分,我们强制设置为5510   if(lcddev.id!=0X5510) //也不是NT5510,尝试看看是不是SSD1963   {   LCD_WR_REG(0XA1);   lcddev.id=LCD_RD_DATA();   lcddev.id=LCD_RD_DATA(); //读回0X57   lcddev.id<<=8;   lcddev.id|=LCD_RD_DATA(); //读回0X61   if(lcddev.id==0X5761)lcddev.id=0X1963;   //SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963   }   }   }   printf(" LCD ID:%x\r\n",lcddev.id); //打印LCD ID   if(lcddev.id==0X9341) //9341初始化  {   ……//9341初始化寄存器序列  }   else if(lcddev.id==0xXXXX) //其他LCD初始化代码  {   ……//其他LCD驱动IC,初始化代码  }   LCD_Display_Dir(0); //默认为竖屏显示  LCD_LED=1; //点亮背光  LCD_Clear(WHITE);   }  

先对FSMC相关IO进行初始化,然后是FSMC的初始化,这个我们在前面都有介绍,最后根据读到的LCD ID,对不同的驱动器执行不同的初始化代码,从上面的代码可以看出,这个初始化函数可以针对十多款不同的驱动IC执行初始化操作,这样大大提高了整个程序的通用性。大家在以后的学习中应该多使用这样的方式,以提高程序的通用性、兼容性。

我们最后来看main函数,

1
int  main(void)  {      uint16_t len=0;          HAL_Init();      SystemClock_Config();   MX_GPIO_Init();   MX_I2C2_Init();   MX_USART3_UART_Init();   MX_FSMC_Init();   MX_NVIC_Init();         delay_init(168);      delay_ms(100);      CH455_init();      LCD_Init();            CH455_Display(1,1);      CH455_Display(2,2);      CH455_Display(3,3);      CH455_Display(4,4);            __HAL_UART_ENABLE_IT(&huart3,  UART_IT_RXNE);         HAL_UART_Transmit(&huart3,uartSendTitle,sizeof(uartSendTitle),1000);         while(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_TC)!=SET);             while  (1)   {         POINT_COLOR=RED;         LCD_ShowString(10,0,240,32,32,"WWW.EMOOC.CC");                LCD_ShowString(10,40,240,32,32,"  STM32F4/H7");              LCD_ShowString(10,80,240,24,24,"TFTLCD  TEST");         LCD_ShowString(10,110,240,16,16,"2.4TFT@emooc");                            LCD_ShowString(10,150,240,12,12,"2021/6/30");                            }  }  

五.实验结果

将TFTLCD屏幕插在P1接口上,下载程序后,观察界面波形:

img

六.STM32CubeMX配置关键硬件截图

配置FSMC总线的cube图如图,所用到的外设如图所示:

img