实验十四 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并口,该模块的外观图如图所示:
管脚接口定义:
和单片机连接方式:
如上图所示,屏幕和开发板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的对应关系如图:
从图中可以看出,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,该指令如表所示:
从上表可以看出,0XD3指令后面跟了4个参数,最后2个参数,读出来是0X93和0X41,刚好是我们控制器ILI9341的数字部分,从而,通过该指令,即可判别所用的LCD驱动器是什么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动IC的初始化代码,从而兼容不同驱动IC的屏,使得一个代码支持多款LCD。
接下来看指令:0X36,这是存储访问控制指令,可以控制ILI9341存储器的读写方向,简单的说,就是在连续写GRAM的时候,可以控制GRAM指针的增长方向,从而控制显示方式(读GRAM也是一样)。该指令如表所示:
从上表可以看出,0X36指令后面,紧跟一个参数,这里我们主要关注:MY、MX、MV这三个位,通过这三个位的设置,我们可以控制整个ILI9341的全部扫描方向,如表所示:
这样,我们在利用ILI9341显示内容的时候,就有很大灵活性了,比如显示BMP图片,BMP解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置LCD扫描方向为从左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往LCD填充颜色数据即可,这样可以大大提高显示速度。
接下来看指令:0X2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置横坐标(x坐标),该指令如表所示:
在默认扫描方式时,该指令用于设置x坐标,该指令带有4个参数,实际上是2个坐标值:SC和EC,即列地址的起始值和结束值,SC必须小于等于EC,且0≤SC/EC≤239。一般在设置x坐标的时候,我们只需要带2个参数即可,也就是设置SC即可,因为如果EC没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。
与0X2A指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置纵坐标(y坐标)。该指令如表所示:
在默认扫描方式时,该指令用于设置y坐标,该指令带有4个参数,实际上是2个坐标值:SP和EP,即页地址的起始值和结束值,SP必须小于等于EP,且0≤SP/EP≤319。一般在设置y坐标的时候,我们只需要带2个参数即可,也就是设置SP即可,因为如果EP没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。
接下来看指令:0X2C,该指令是写GRAM指令,在发送该指令之后,我们便可以往LCD的GRAM里面写入颜色数据了,该指令支持连续写,指令描述如表所示:
从上表可知,在收到指令0X2C之后,数据有效位宽变为16位,我们可以连续写入LCD GRAM值,而GRAM的地址将根据MY/MX/MV设置的扫描方向进行自增。例如:假设设置的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过SC,SP设置)后,每写入一个颜色值,GRAM地址将会自动自增1(SC++),如果碰到EC,则回到SC,同时SP++,一直到坐标:EC,EP结束,其间无需再次设置的坐标,从而大大提高写入速度。
最后,来看看指令:0X2E,该指令是读GRAM指令,用于读取ILI9341的显存(GRAM),该指令在ILI9341的数据手册上面的描述是有误的,真实的输出情况如表所示:
该指令用于读取GRAM,如表所示,ILI9341在收到该指令后,第一次输出的是dummy数据,也就是无效的数据,第二次开始,读取到的才是有效的GRAM数据(从坐标:SC,SP开始),输出规律为:每个颜色分量占8个位,一次输出2个颜色分量。比如:第一次输出是R1G1,随后的规律为:B1R2àG2B2àR3G3àB3R4àG4B4àR5G5… 以此类推。如果我们只需要读取一个点的颜色值,那么只需要接收到参数3即可,如果要连续读取(利用GRAM地址自增,方法同上),那么就按照上述规律去接收颜色数据。
以上,就是操作ILI9341常用的几个指令,通过这几个指令,我们便可以很好的控制ILI9341显示我们所要显示的内容了。
一般TFTLCD模块的使用流程如图:
任何LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要执行一次即可。而画点流程就是:设置坐标à写GRAM 指令à写入颜色数据,然后在LCD 上面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标à读GRAM 指令à读取颜色数据,这样就可以获取到对应点的颜色数据了。
以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用TFTLCD 了。接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出TFTLCD 显示需要的相关设置步骤如下:
1)设置STM32F4 与TFTLCD 模块相连接的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 的框图如图所示:
从上图我们可以看出,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字节的四个存储块,如图所示:
从上图可以看出,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个区进行寻址。如表所示:
我们要特别注意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配置寄存器如表:
对于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种不同的异步时序模型。选用不同的时序模型时,需要设置不同的时序参数,如表所列:
在实际扩展时,根据选用存储器的特征确定时序模型,从而确定各时间参数与存储器读/写周期参数指标之间的计算关系;利用该计算关系和存储芯片数据手册中给定的参数指标,可计算出FSMC所需要的各时间参数,从而对时间参数寄存器进行合理的配置。
本章,我们使用异步模式A(ModeA)方式来控制TFTLCD,模式A的读操作时序如图所示:
模式A读操作时序图
模式A支持独立的读写时序控制,这个对我们驱动TFTLCD来说非常有用,因为TFTLCD在读的时候,一般比较慢,而在写的时候可以比较快,如果读写用一样的时序,那么只能以读的时序为基准,从而导致写的速度变慢,或者在读数据的时候,重新配置FSMC的延时,在读操作完成的时候,再配置回写的时序,这样虽然也不会降低写的速度,但是频繁配置,比较麻烦。而如果有独立的读写时序控制,那么我们只要初始化的时候配置好,之后就不用再配置,既可以满足速度要求,又不需要频繁改配置。
模式A的写操作时序如图所示:
模式A写操作时序
图中的ADDSET与DATAST,是通过不同的寄存器设置的,接下来我们讲解一下Bank1的几个控制寄存器。
SRAM/NOR闪存片选控制寄存器:FSMC_BCRx(x=1~4),该寄存器各位描述如图所示:
图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),该寄存器各位描述如图所示:
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),该寄存器各位描述如图所示:
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排母座上。
图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座子连接如图所示:
四.程序设计
本实验,我们新建了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接口上,下载程序后,观察界面波形:
六.STM32CubeMX配置关键硬件截图
配置FSMC总线的cube图如图,所用到的外设如图所示: