文章目录[隐藏]
TFTLCD的引脚介绍
TFTLCD显示用到的引脚如下所示:
表格1
名称 |
说明 |
CS |
LCD片选信号 |
RS |
命令/数据控制信号(0:命令;1:数据) |
WR |
写使能(低电平有效) |
RD |
读使能(低电平有效) |
RST |
复位信号 |
D0~D15 |
双向数据线 |
GND |
地线 |
BL_CTR |
背光控制引脚(高电平有效) |
VCC3.3 |
主电源供电引脚(3V3) |
VCC5 |
背光供电引脚(5V) |
1~21号IO口用于LCD控制器,23号IO口用于LCD的背光控制,这样LCD的显示总共需要22个IO口。但是,电源和地线我们千万不要忽略,模块需要双电源供电:5V和3.3V才可以正常工作,5V电源用于背光供电,3.3V用于除背光外的其他电源部分供电。
TFTLCD的与MCU的硬件连接
表格2
MCU引脚 |
LCD引脚 |
说明 |
FSMC_NE4 |
CS |
LCD片选信号 |
A[10] |
RS |
命令/数据控制信号(0:命令;1:数据) |
FSMC_NWE |
WR |
写使能(低电平有效) |
FSMC_NOE |
RD |
读使能(低电平有效) |
RESET(系统复位引脚) |
RST |
复位信号 |
D[0:15] |
D[1:9],D[10:17] |
双向数据线 |
GND |
GND |
地线 |
VCC5 |
BL_CTR |
背光控制引脚(高电平有效) |
VCC3.3 |
VCC3.3 |
主电源供电引脚(3V3) |
VCC5 |
VCC5 |
背光供电引脚(5V) |
8080时序简介
NT35510支持多种时序读写操作,我们这里使用的是8080并口操作,读/写时序如下:
表格 1
操作 |
RD |
WR |
RS |
写入命令 |
1 |
0 |
0 |
读取命令 |
0 |
1 |
0 |
写入数据 |
1 |
0 |
1 |
读取数据 |
0 |
1 |
1 |
注意:这里的数据传输线D[0:15]是双向的,且RD读使能,WR写使能低电平有效。
FSMC简介
大容量,且引脚数在100脚以上的STM32F103芯片都带有FSMC接口,ALIENTEK战舰STM32开发板的主芯片为STM32F103ZET6,是带有FSMC接口的。
FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和16位PC存储器卡连接,STM32的FSMC接口支持包括SRAM、NANDFLASH、NORFLASH和PSRAM等存储器。FSMC的框图如图18.1.2.1所示:
从上图我们可以看出,STM32的FSMC将外部设备分为3类:NOR/PSRAM设备、NAND设备、PC卡设备。他们共用地址数据总线等信号,他们具有不同的CS以区分不同的设备,比如本章我们用到的TFTLCD就是用的FSMC_NE4做片选,其实就是将TFTLCD当成SRAM来控制。
这里我们介绍下为什么可以把TFTLCD当成SRAM设备用:首先我们了解下外部SRAM
的连接,外部SRAM的控制一般有:地址线(如A0~A18)、数据线(如D0~D15)、写信号(WE)、读信号(OE)、片选信号(CS),如果SRAM支持字节控制,那么还有UB/LB信号。而TFTLCD的信号我们在18.1.1节有介绍,包括:RS、D0~D15、WR、RD、CS、RST和BL等,其中真正在操作LCD的时候需要用到的就只有:RS、D0~D15、WR、RD和CS。其操作时序和SRAM的控制完全类似,唯一不同就是TFTLCD有RS信号,但是没有地址信号。
TFTLCD为什么能用FSMC读写?
FSMC听起来很高大上,其实FSMC就是一个MCU与外部存储器(SRAM,FLASH等)读写数据的一个接口,我们可以配置FSMC的寄存器从而实现MCU可以根据特定的时序与外部存储器进行数据的交互(存储/读取)。有人会疑问:TFTLCD又不是存储器不具备存储功能但是却可以与MCU通过读写时序交互信息?
TFTLCD确实不是SRAM这样的数据存储器,不具备数据存储功能,但是FSMC的扩展功能就在于此,TFTLCD的读写时序和SRAM大致一样,因此我们使用FSMC去充当MCU与TFTLCD的沟通桥梁。不止TFTLCD是这样,其他的具有和FSMC支持的存储器相同/类似时序的设备都通过FSMC与MCU建立起数据沟通的桥梁。
FSMC到底是干什么用的?
FSMC是沟通CPU与外部存储器之间的桥梁。FSMC在CPU内存中的映射地址如下所示:
我们可以好好地品味上图的含义,FSMC一个块有64M的存储空间,存储空间里存储的是数据吗?不是的,存储的是外设地址,当我们在存储块中的访问单元序号+1,对应的外设存储单元的访问地址就自加8(如果外设数据存储的数据宽度为8b的话)。由此,我们可以得到当输出长度为8b时,FSMC与外部SRAM的地址映射关系:
既然明白了CPU和外部SRAM之间的地址映射关系,那么就拿STM32读写外部SRAM为例说明FSMC的功能:
1. 我们在FSMC的SRAM地址影响空间中“定义”变量Var;
FSMC是一个数据管理设备负责变量的读取和发送,既然是管理设备那就应该将被管理的对象纳入自己的势力范围之中,即将被托管的变量定义在自己在内存中的地址映像之中。
2. 根据SRAM的读写时序配置FSMC;
FSMC作为CPU与外部SRAM沟通的桥梁,那就得说他们两个人都认识的话——读写时序,两者沟通关系如下所示:
3. 对变量进行读写操作时,FSMC会进行在CPU和SRAM外部存储器之间进行如下操作:
首先,对FSMC管理的变量Var进行读写就会引起FSMC产生读取SRAM中数据的时序,引发对SRAM的读写操作。
FSMC如何进行读写操作?
读写LCD本质上是对寻址空间的某一个地址进行读写操作,例如:
读:var=*(uint32_t*)0x6C000080;
写:*(uint32_t*)0x6C000080=var;
读写控制是FSMC自动控制的,你向指定地址写数据(如*(uint32_t*)0x6C000080=var),FSMC会自动控制相应的引脚(CS、RS、RD、WR等)发送写数据时序,读也是一样的。
为了更清楚,我们可以看一下相应代码的汇编:
因此,如果是读LCD,则使用的一定是LDR指令(LoadRegister);而写LCD,则使用的是STR指令(StoreRegister)。这是由于不同的指令会对应不同的总线访问时序。
从上面可以得出结论,只要是访问一个存储单元并且写入数据(将变量的值赋值给存储单元),那么FSMC就会控制CS、RS、RD、WR等对这个存储单元进行写操作;我们访问一个存储单元并进行读操作(把存储单元的数据读出来并赋值给变量),那么FSMC就会控制CS、RS、RD、WR等对这个存储单元执行读操作。
在TFTLCD实验中,FSMC甄别数据/命令的原理
像这种地址线和数据线分开的数据存储器,一般的向数据存储器写数据的时序如下所示:
首先,我们要向数据存储器写入指令:
紧接着,我们要输入指令所需的数据:
我们前面说过FSMC进行读写操作的原理,但是当我们读FSMC中的地址映射单元时,FSMC会输出如下图所示的读时序:
这并不是8080时序,而且我们疑问的是TFTLCD中只有地址线A[25:0],而没有RS信号来决定收发数据还是收发命令。这就很懵了,RS的0/1分别代表着命令/数据,但是我上哪里找0/1呢?此时换位思考一下,惊奇的发现地址线A[25:0]可以帮到我们:
假设TFTLCD只有两个寄存器:数据操作寄存器/命令操作寄存器。当TFTLCD收到寄存器地址”addr=1“时,此时命令操作寄存器响应;同理,当TFTLCD收到寄存器地址“addr=0”时,此时数据操作寄存器响应。以上一波操作就可以把FSMC读写SRAM的时序神奇地转化为8080读写时序了。
我们将A[10]作为RS引脚用于控制TFTLCD是操作命令还是操作数据。如何使得TFTLCD操作数据时A[10]=1,操作命令时A[10]=0是关键:
Bank1.sector4就是从地址0X6C000000开始,而0X000007FE,则是A[10]的偏移量。以A[10]为例,A[10]相对于Bank1.sector4的地址0X7FE换成二进制为:0b011111111110,而16位数据时,地址右移一位对齐,对应到地址引脚就是:A[10:0]=0b001111111111,此时A[10]是0,但是如果16位地址加1(对应到8位地址是加2,即0X7FE+0X02),那么:A[10:0]=0b010000000000,此时A[10]就是1了,即实现了对RS的0和1的控制。
//LCD地址结构体
typedefstruct
{
vu16LCD_REG;
vu16LCD_RAM;
}LCD_TypeDef;
//使用NOR/SRAM的Bank1.sector4,地址位HADDR[27,26]=11A10作为数据命令区分线
//注意设置时STM32内部会右移一位对其!
#defineLCD_BASE((u32)(0x6C000000|0x000007FE))
#defineLCD((LCD_TypeDef*)LCD_BASE)
我们将这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的
地址就是0X6C00,07FE,对应A[10]的状态为0(即RS=0),而LCD->LCD_RAM的地址就是
0X6C00,0800(结构体地址自增),对应A[10]的状态为1(即RS=1),从而实现对RS的控
制。
FSMC的寻址操作
STM32的FSMC支持8/16/32位数据宽度,我们这里用到的LCD是16位宽度的,所以在设置的时候,选择16位宽就OK了。我们再来看看FSMC的外部设备地址映像,STM32的FSMC将外部存储器划分为固定大小为256M字节的四个存储块:
从上图可以看出,FSMC总共管理1GB空间,拥有4个存储块(Bank),本章,我们用到的是块1,所以在本章我们仅讨论块1的相关配置。
STM32的FSMC存储块1(Bank1)被分为4个区,每个区管理64M字节空间,每个区都有独立的寄存器对所连接的存储器进行配置。Bank1的256M字节空间由28根地址线(HADDR[27:0])寻址。
这里HADDR是内部AHB地址总线,其中HADDR[25:0]来自外部存储器地址FSMC_A[25:0],而HADDR[26:27]对4个存储块(Bank)进行寻址,Bank1的地址映射如下所示:
表格3
Bank |
片选信号 |
地址映像 |
HADDR[27:26] |
1 |
FSMC_NE1 |
0X60000000 |
00 |
0X63FFFFFF |
|||
2 |
FSMC_NE2 |
0X64000000 |
01 |
0X67FFFFFF |
|||
3 |
FSMC_NE3 |
0X68000000 |
10 |
0X68FFFFFF |
|||
4 |
FSMC_NE4 |
0X6C000000 |
11 |
0X6FFFFFFF |
特别要注意:
当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]的设置,是不需要我们干预的,即当我们设置了片选HADDR[27:26]自动被设置为相应的数值。比如:当你选择使用Bank1的第三个区,即使用FSMC_NE3来连接外部设备的时候,即对应了HADDR[27:26]=10,我们要做的就是配置对应第3区的寄存器组,来适应外部设备即可。
其实,我们看到一个Bank存储块中又有4个子块,我们称之为扇区(Sector),它的配置不是通过片选而是通过配置寄存器实现,Bank1的寄存器一共有12个(BCKx,BTKx,BWTKx,x=1,2,3,4)。
FSMC的地址映射范围因数据长度而异?
从上表可以看出,FSMC最大访问的存储器位数就是512M,例如Bank1的第1区的寻址范围是0X60000000~0X63FFFFFF,对应的外部存储器的地址为0~0X03FFFFFF,即我们可以访问的位的地址从0~512M。
此时,我们可以用FSMC一个数据存储块最大管理的位数反推FSMC地址映射范围,可得如下结果:
表格4
数据宽度 |
最大访问的数据个数 |
最大访问存储器空间 |
8b |
512M/8=64M |
512M |
16b |
512M/16=32M |
512M |
HADDR的[25:0]位所表示的地址为0~64M,但是当数据长度为16b时,需要访问的地址为0~32M,因此HADDR中只用到了25位。当数据长度为16b时,HADDR的有效位为[25:1]而且外部存储器的地址线接收到的地址必须从0开始寻址,因此要求HADDR向低位偏移一位,如下图所示:
当数据长度为16b时,FSMC中的地址映射仍是一个地址寻址一个字节的数据,但是FSMC地址自增就变了,变成了每次自加2,而非原来的自加1,16b的地址映射关系如下:
何为位段?
如何在FSMC的地址空间中“定义”变量?
位带操作,乍一听名字,也很高大上。但是位带操作实质上就是一个寻址的手段而已。在我们做一件事情时,总喜欢先顶层规划,然后再细分各个工作流程。C语言中常用的寻址方式就是位带操作,我们再C语言中访问一个结构体变量元素的地址,一般常常确定这个结构体变量的地址,再根据结构体中的元素相对于结构体首地址的偏移量确定其具体位置,并且访问该地址的信息。
在lcd.c文件中,有如下代码:
//LCD地址结构体
typedefstruct
{
vu16LCD_REG;
vu16LCD_RAM;
}LCD_TypeDef;
//使用NOR/SRAM的Bank1.sector4,地址位HADDR[27,26]=11A10作为数据命令区分线
//注意设置时STM32内部会右移一位对其!
#defineLCD_BASE((u32)(0x6C000000|0x000007FE))
#defineLCD((LCD_TypeDef*)LCD_BASE)
0x6C000000代表的是FSMC中的Bank1的基地址,而0x000007FE代表的是Bank1的存储块1相对于Bank1的地址偏移量。这样操作就使得LCD_BASE代表FSMC中Bank1的存储块1的地址,然后我们使用强制类型转换,就在LCD_BASE地址上建立了一个结构体变量LCD,其内存分布如下所示:
一定要注意:由于数据是16位的,因此地址自增2,即0X6C008000=0X6C0007FE+0X02。
FSMC的参数配置
对于NORFLASH控制器,主要是通过FSMC_BCRx、FSMC_BTRx和FSMC_BWTRx寄
存器设置(其中x=1~4,对应4个区)。通过这3个寄存器,可以设置FSMC访问外部存储器的时序参数,拓宽了可选用的外部存储器的速度范围。FSMC的NORFLASH控制器支持同步和异步突发两种访问方式。这里,TFTLCD使用的是异步突发访问模式。
对于异步突发访问方式,FSMC主要设置3个时间参数:地址建立时间(ADDSET)、数据建立时间(DATAST)和地址保持时间(ADDHLD)。FSMC综合了SRAM/ROM、PSRAM和NORFlash产品的信号特点,定义了4种不同的异步时序模型。选用不同的时序模型时,需要设置不同的时序参数:
我们这里读写的时序是SRAM,因此我们可以选用模式1/模式A。但是,我们知道TFTLCD的写速度要快于读速度,因此我们分别配置读写速度——模式A。模式A与模式1的最大区别在于:模式1中读写速度一样,模式A中读写速度不同。
模式A支持独立的读写时序控制,这个对我们驱动TFTLCD来说非常有用,因为TFTLCD在读的时候,一般比较慢,而在写的时候可以比较快,如果读写用一样的时序,那么只能以读的时序为基准,从而导致写的速度变慢,或者在读数据的时候,重新配置FSMC的延时,在读操作完成的时候,再配置回写的时序,这样虽然也不会降低写的速度,但是频繁配置,比较麻烦。而如果有独立的读写时序控制,那么我们只要初始化的时候配置好,之后就不用再配置,既可以满足速度要求,又不需要频繁改配置。
在模式A中,我们无非关注两个参数:地址建立时间(ADDSET)、数据建立时间(DATAST)。
- 地址建立时间(ADDSET):用于TFTLCD读取到稳定的RS信号
首先,我们先要知道什么是建立时间:建立时间(Tsu:setuptime)是指在时钟沿到来之前数据从不稳定到稳定所需的时间,如果建立的时间不满足要求那么数据将不能在这个时钟上升沿被稳定的打入触发器。
地址建立的含义:在读取数据之前一定要识别到A[25:0]地址线中的稳定地址信号,这样我们才能进行读写操作。
表格5
RDX |
WRX |
说明 |
高电平 |
高电平 |
地址建立时间 |
RDX和WRX均为无效电平的这段时间不是无效时间,适用于A[25:0]建立稳定的地址信号的时间,因为在读写数据之前,必须要通过地址线建立稳定的联系才行。
- 数据建立时间(DATAST):用于TFTLCD读取到稳定的数据信号
首先,我们要清楚何为保持时间:保持时间(Th:holdtime)是指数据稳定后保持的时间,如果保持时间不满足要求那么数据同样也不能被稳定的打入触发器。
NT35510数据锁存的时序图:
表格6
RDX |
WRX |
说明 |
下降沿 |
高电平 |
占用数据总线 |
上升沿 |
高电平 |
数据所存 |
数据所存的含义:NT35510芯片可以识别稳定的数据。为了我们识别到稳定可靠的数据,数据保持时间必须大于RDX低电平持续时间,即RDX的上升沿必须出现在地址保持时间之内。
FSMC的模式A的读时序如下:
注意:最后的2HCLK是用于存储器将读取到的数据存入锁存器(即保存数据)用的。
FSMC的模式A的写时序如下:
要确定ADDSET和DATAST就必须参考“芯片NT35510的读写时序”,如下所示:
因此,ADDSET最小为250ns/HCLK,DATAST最小为150ns/HCLK,其中,FSMC接在AHB总线上,由HCLK驱动,因此单位周期为HCLK,ADDSET代表着地址线上的地址建立所需的单位周期数量(记得向正无穷取整),DATAST代表着数据线上数据保持的单位周期数量(记得向正无穷取整)。
但是,为了兼容其它型号的屏幕驱动芯片以及STM32F10x系列的缺陷,正点原子例程中ADDSET和DATAST设置如下:
读时序:
- 由于STM32F103FSMC的性能问题,就算设置ADDSET为0,RD的高电平持续时间也达到了190ns以上,所有,我们这里可以设置ADDSET为较小的值,本章我们设置ADDSET为1,即2个HCLK周期,实际RD高电平大于200ns。
- 为了兼容其他屏,我们这里设置DATAST为15,也就是15+1=16个HCLK周期,时间大约是234ns。
写时序:
对NT35510来说,这两个时间只需要15ns就够了,比读操作快得多。所以我们这里设置DATAST为3,即4个HCLK周期,时间约为55ns(因为9320等控制器,这个时间要求比较长,要50ns)。然后ADDSET(也存在性能问题)设置为0,即1个HCLK周期,实际WR高电平时间大于100ns。
FSMC的寄存器介绍
SRAM/NOR闪存片选控制寄存器 1…4 (FSMC_BCR1…4)
在片选配置寄存器中,由如下几个位很重要:
- 扩展使能位:EXTMOD
扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读 写不同的时序,故该位需要设置为 1。
- 写使能位:WREN
扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读 写不同的时序,故该位需要设置为 1。
- 存储数据总线宽度:MWID
存储器数据总线宽度。00,表示 8 位数据模式;01 表示 16 位数据模式;10 和 11 保留。我们的 TFTLCD 是 16 位数据线,所以设置 WMID[1:0]=01。
- 存储器类型:MTYP
00 表示 SRAM、ROM;01 表示 PSRAM;10 表示 NOR FLASH;11 保留。前面提到,我们把 TFTLCD 当成 SRAM 用,所以需要设置 MTYP[1:0]=00。
- 存储块使能位:MBKEN
这个容易理解,我们需要用到该存储块控制 TFTLCD,当然要 使能这个存储块了。
SRAM/NOR闪存片选时序寄存器 1…4 (FSMC_BTR1…4)
由于我们配置了扩展模式位使能,因此FSMC对SRAM的读写速度不同,FSMC_BTRx代表着存储块x的读时序。如果扩展模式位失能呢个,则读写时序的配置均有该寄存器完成。
- 访问模式位:ACCMOD
00 表示访问模式 A;01 表示访问模式 B;10 表示访问模式 C; 11 表示访问模式 D,本章我们用到模式 A,故设置为 00。
- 数据保持时间:DATAST
读时序中数据保持时间为16HCLK,由于0X0000代表1HCLK,因此16HCLK用0X0F表示。由表中数据可以看出,DATLAT最大为255,即256个HCLK。
- 地址建立时间:ADDSET
由于STM32F10x系类的FSMC缺陷和与NT35510读时序的关系,我们配置ADDSET为0X0000。由图中可知,ADDSET最大为255,即256HCLK。
SRAM/NOR闪存写时序寄存器 1…4 (FSMC_BWTR1…4)
由于我们配置了扩展模式位使能,因此FSMC对SRAM的读写速度不同,FSMC_BWTRx代表着存储块x的写时序。
该寄存器位的配置和含义与读时序配置寄存器大同小异。
程序解析
FSMC初始化
FSMC读写SRAM时序的配置结构体类型:FSMC_NORSRAMTimingInitTypeDef
表格 7
参数 |
说明 |
FSMC_AddressSetupTime |
地址建立时间 |
FSMC_AddressHoldTime |
地址保持时间(未用) |
FSMC_DataSetupTime |
数据保持时间(配置的DATAST位) |
FSMC_BusTurnAroundDuration |
总线恢复时间 |
FSMC_CLKDivision |
分频系数:FSMC_CLK与HCLK的关系(未用) |
FSMC_DataLatency |
数据保持时间(配置的DATLAT位) |
FSMC_AccessMode |
FSMC的访问模式 |
FSMC读写SRAM的初始化结构体类型:FSMC_NORSRAMInitTypeDef
表格 8
参数 |
说明 |
FSMC_Bank |
存储块片选使能(有用) |
FSMC_DataAddressMux |
数据地址复用 |
FSMC_MemoryType |
存储器类型(有用) |
FSMC_MemoryDataWidth |
数据宽度(有用) |
FSMC_BurstAccessMode |
突发访问模式 |
FSMC_WaitSignalPolarity |
等待信号的极性 |
FSMC_AsynchronousWait |
同步等待时间 |
FSMC_WriteOperation |
写使能/失能(有用) |
FSMC_WrapMode |
是否支持费对其成组模式 |
FSMC_WaitSignal |
等待信号使能/失能 |
FSMC_ExtendedMode |
扩展模式位使能/失能(有用) |
FSMC_WriteBurst |
同步写-CBURSTRW(bit19) |
FSMC_WaitSignalActive |
WAITCFG-bit11 |
FSMC_ReadWriteTimingStruct |
读时序配置(有用) |
FSMC_WriteTimingStruct |
写时序配置(有用) |
示例代码如下:
FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure;
FSMC_NORSRAMTimingInitTypeDef readWriteTiming;
FSMC_NORSRAMTimingInitTypeDef writeTiming;
readWriteTiming.FSMC_AddressSetupTime = 0x01; //地址建立时间(ADDSET)为2个HCLK 1/36M=27ns
readWriteTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(ADDHLD)模式A未用到
readWriteTiming.FSMC_DataSetupTime = 0x0f; // 数据保存时间为16个HCLK,因为液晶驱动IC的读数据的时候,速度不能太快,尤其对1289这个IC。
readWriteTiming.FSMC_BusTurnAroundDuration = 0x00;
readWriteTiming.FSMC_CLKDivision = 0x00;
readWriteTiming.FSMC_DataLatency = 0x00;
readWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式A
writeTiming.FSMC_AddressSetupTime = 0x00; //地址建立时间(ADDSET)为1个HCLK
writeTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(A
writeTiming.FSMC_DataSetupTime = 0x03; 数据保存时间为4个HCLK
writeTiming.FSMC_BusTurnAroundDuration = 0x00;
writeTiming.FSMC_CLKDivision = 0x00;
writeTiming.FSMC_DataLatency = 0x00;
writeTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式A
FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM4;// 这里我们使用NE4 ,也就对应BTCR[6],[7]。
FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; // 不复用数据地址
FSMC_NORSRAMInitStructure.FSMC_MemoryType =FSMC_MemoryType_SRAM;// FSMC_MemoryType_SRAM; //SRAM
FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;//存储器数据宽度为16bit
FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode =FSMC_BurstAccessMode_Disable;// FSMC_BurstAccessMode_Disable;
FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait=FSMC_AsynchronousWait_Disable;
FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;
FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; // 存储器写使能
FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Enable; // 读写使用不同的时序
FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &readWriteTiming; //读写时序
FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &writeTiming; //写时序
基于FSMC实现的三个基础参数
1. 读LCD的返回数据:
//读LCD数据
//返回值:读到的值
u16 LCD_RD_DATA(void)
{
vu16 ram; //防止被优化
ram=LCD->LCD_RAM;
return ram;
}
2. 不但访问寄存器而且还写入一个数据:
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue)
{
LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号
LCD->LCD_RAM = LCD_RegValue;//写入数据
}
3. 读寄存器里面的数据:
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(u16 LCD_Reg)
{
LCD_WR_REG(LCD_Reg); //写入要读的寄存器序号
delay_us(5);
return LCD_RD_DATA(); //返回读到的值
}
4. 单纯访问寄存器,不写入寄存器数据:
//写寄存器函数
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{
LCD->LCD_REG=regval;//写入要写的寄存器序号
}
5. 单纯写数据:
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(u16 data)
{
LCD->LCD_RAM=data;
}
常见问题
如何计算一个字符对应点阵集所占的字节数?
字体大小分为1608,2412,1206三种形式,例如1608代表字符长16个位宽8个位,如下所示:
在正点原子给lcd.c中有个函数位LCD_ShowChar,这个函数是最基本的显示字符的函数,他是显示一切ASCII字符串的“鼻祖”,其中有个计算ASCII字符所占字节个数的代码如下所示:
u8 csize=(size/8+((size%8)?1:0))*(size/2); //得到字体一个字符对应点阵集所占的字节数
我们可以这样来看:
size是整形变量,表示一个ASCII字符的长度,由于整型变量相除还是整型变量,C语言默认向0取整,因此size/8+(size%8?1:0)代表了1列也就是16个位所需的字节数,然后我们知道宽=列/2,因此size/2代表了一行共有多少列,因此列数*每列所需的字节数=一个ASCII字符显示所需的字节数。
为何可以在font.h中定义二维数组?
我们学过C语言的同学都熟知:.h文件中只能包含.c文件中的函数声明和一些宏定义,自定义数据类型的定义。但是正点原子的LCD的专属字库文件font.h中定义了用于存放字库数据的二维const类型数组。首先,在C语言中如果这个.h文件为某个.c文件所专属(比如font.h文件就是供lcd.c专用的),我们不建议这样做。比如,我如果在另一个.c文件中引用font.h字库文件,MDK会爆出以下错误:
多次引用同一个.h文件(这个.h文件中得定义有数据才会出现上述错误),相当于在多个.c文件中多次定义该变量,这会导致变量重定义错误。如果要在多个文件中引用.h文件中的变量,只需将font.h中的变量声明成static类型的即可,运行成功如下图所示:
但在C++中,const类型隐含着带有static类型的属性,因此在C++中只需将.h文件中的变量声明成const类型即可。
.h文件中的每个变量拥有了static类型之后,当某个.c文件引用该.h文件时,该.h文件中的所有变量会变成这个.c文件的专属变量,即该.c文件的局部变量。相同名称的全局变量会起冲突,而相同名称但是存在于不同.c文件中的局部变量不会起冲突。
编译器的-O2优化
首先,我们要知道-O2是干什么的,-O2是一个优化等级。在MDK5中,我们可以自选优化等级:
该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用以提高目标代码的运行速度。·其中,最直接的优化算法就是“去掉中间变量,一步到位”,具体示例如下:
#include <stdio.h>
void delay(long val);
int main(){
delay(1000000);
return 0;
}
void delay(volatile long val){
while(val--);
}
我们虽然加了volatile,但是这只能保证在频繁的改变一个变量时,编译器不会将其认为“仅仅改变了一次”。-O2执行的是空间上的优化,即代码精简,与volatile八竿子打不着。当我们使用-O0和-O2优化时,会出现如下结果:
MDK5中4中不同的等级如下:
-O0:不做任何优化,这是默认的编译选项;
-O1:对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存。使用本项优化,编译器会尝试减小生成代码的尺寸,以及缩短执行时间,但并不执行需要占用大量编译时间的优化;
-O2:该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用来精简代码所占用的空间长度,以提高目标代码的运行速度;
-O3:该选项除了执行-O2所有的优化选项之外,一般都是采取很多向量化算法,提高代码的并行执行程度(时间域上的优化),利用现代CPU中的流水线,Cache等。
STM32F4的白屏问题
当我们提高优化等级时,之伴随的是执行效率的提高和更多的BUG。我们将MDK的优化等级从-O0提高至-O2时,编译后的程序可能会出现以下问题:
- 编译乱序引起的问题,程序当中没有放置正确的内存屏障,编译优化引起的乱序执行导致程序出现bug;
- 时序引起的问题,优化后的代码执行速度要比优化前快得多,这可能引起时序不符合要求,最终导致bug;
STM32F4白屏的原因就是因为FSMC速度过快,导致编译乱序问题。我们要解决的方法无非就是延迟一下使得程序慢下来,在正点原子的程序中,延迟的原理是“找一个中间变量,数据是通过中间变量传给目标变量,这样数据的传输过程就经过了两个寄存器”,代码实现如下:
volatile到底有什么用?
volatile修饰是告诉编译器, 这个变量有可能被其他子程序改变,所以不能从寄存器里读取变量的值,要从内存里读取。 就是说修饰的确实是一个变量,而且是一个“容易变”的变量。在每次取这个变量值的时候,要求不是取它上次在某个时候取的临时缓存变量(比如说暂存在某个寄存器中),而是直接到内存中取。 volatile变量能防止优化,别如说你在某个地方可能连续调用了好几次这个函数,于是编译器优化后,可能就调用一次,其他几次就采用这一次调用的返回值,而volatile修饰后,要让每一次都进行函数调用, 而不采用寄存器里的暂存值。
内存数量级的关系
- 1KB=1024B 1024是2的10次方
- 1MB=1024X1024B 是2的20次方
- 1GB=1024X1024X1024B 是2的30次方
- 1TB=1024X1024X1024X1024B 是2的40次方
版权声明:本文为CSDN博主「肥肥胖胖是太阳」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45590473/article/details/118443512
TFTLCD的引脚介绍
TFTLCD显示用到的引脚如下所示:
表格1
名称 |
说明 |
CS |
LCD片选信号 |
RS |
命令/数据控制信号(0:命令;1:数据) |
WR |
写使能(低电平有效) |
RD |
读使能(低电平有效) |
RST |
复位信号 |
D0~D15 |
双向数据线 |
GND |
地线 |
BL_CTR |
背光控制引脚(高电平有效) |
VCC3.3 |
主电源供电引脚(3V3) |
VCC5 |
背光供电引脚(5V) |
1~21号IO口用于LCD控制器,23号IO口用于LCD的背光控制,这样LCD的显示总共需要22个IO口。但是,电源和地线我们千万不要忽略,模块需要双电源供电:5V和3.3V才可以正常工作,5V电源用于背光供电,3.3V用于除背光外的其他电源部分供电。
TFTLCD的与MCU的硬件连接
表格2
MCU引脚 |
LCD引脚 |
说明 |
FSMC_NE4 |
CS |
LCD片选信号 |
A[10] |
RS |
命令/数据控制信号(0:命令;1:数据) |
FSMC_NWE |
WR |
写使能(低电平有效) |
FSMC_NOE |
RD |
读使能(低电平有效) |
RESET(系统复位引脚) |
RST |
复位信号 |
D[0:15] |
D[1:9],D[10:17] |
双向数据线 |
GND |
GND |
地线 |
VCC5 |
BL_CTR |
背光控制引脚(高电平有效) |
VCC3.3 |
VCC3.3 |
主电源供电引脚(3V3) |
VCC5 |
VCC5 |
背光供电引脚(5V) |
8080时序简介
NT35510支持多种时序读写操作,我们这里使用的是8080并口操作,读/写时序如下:
表格 1
操作 |
RD |
WR |
RS |
写入命令 |
1 |
0 |
0 |
读取命令 |
0 |
1 |
0 |
写入数据 |
1 |
0 |
1 |
读取数据 |
0 |
1 |
1 |
注意:这里的数据传输线D[0:15]是双向的,且RD读使能,WR写使能低电平有效。
FSMC简介
大容量,且引脚数在100脚以上的STM32F103芯片都带有FSMC接口,ALIENTEK战舰STM32开发板的主芯片为STM32F103ZET6,是带有FSMC接口的。
FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和16位PC存储器卡连接,STM32的FSMC接口支持包括SRAM、NANDFLASH、NORFLASH和PSRAM等存储器。FSMC的框图如图18.1.2.1所示:
从上图我们可以看出,STM32的FSMC将外部设备分为3类:NOR/PSRAM设备、NAND设备、PC卡设备。他们共用地址数据总线等信号,他们具有不同的CS以区分不同的设备,比如本章我们用到的TFTLCD就是用的FSMC_NE4做片选,其实就是将TFTLCD当成SRAM来控制。
这里我们介绍下为什么可以把TFTLCD当成SRAM设备用:首先我们了解下外部SRAM
的连接,外部SRAM的控制一般有:地址线(如A0~A18)、数据线(如D0~D15)、写信号(WE)、读信号(OE)、片选信号(CS),如果SRAM支持字节控制,那么还有UB/LB信号。而TFTLCD的信号我们在18.1.1节有介绍,包括:RS、D0~D15、WR、RD、CS、RST和BL等,其中真正在操作LCD的时候需要用到的就只有:RS、D0~D15、WR、RD和CS。其操作时序和SRAM的控制完全类似,唯一不同就是TFTLCD有RS信号,但是没有地址信号。
TFTLCD为什么能用FSMC读写?
FSMC听起来很高大上,其实FSMC就是一个MCU与外部存储器(SRAM,FLASH等)读写数据的一个接口,我们可以配置FSMC的寄存器从而实现MCU可以根据特定的时序与外部存储器进行数据的交互(存储/读取)。有人会疑问:TFTLCD又不是存储器不具备存储功能但是却可以与MCU通过读写时序交互信息?
TFTLCD确实不是SRAM这样的数据存储器,不具备数据存储功能,但是FSMC的扩展功能就在于此,TFTLCD的读写时序和SRAM大致一样,因此我们使用FSMC去充当MCU与TFTLCD的沟通桥梁。不止TFTLCD是这样,其他的具有和FSMC支持的存储器相同/类似时序的设备都通过FSMC与MCU建立起数据沟通的桥梁。
FSMC到底是干什么用的?
FSMC是沟通CPU与外部存储器之间的桥梁。FSMC在CPU内存中的映射地址如下所示:
我们可以好好地品味上图的含义,FSMC一个块有64M的存储空间,存储空间里存储的是数据吗?不是的,存储的是外设地址,当我们在存储块中的访问单元序号+1,对应的外设存储单元的访问地址就自加8(如果外设数据存储的数据宽度为8b的话)。由此,我们可以得到当输出长度为8b时,FSMC与外部SRAM的地址映射关系:
既然明白了CPU和外部SRAM之间的地址映射关系,那么就拿STM32读写外部SRAM为例说明FSMC的功能:
1. 我们在FSMC的SRAM地址影响空间中“定义”变量Var;
FSMC是一个数据管理设备负责变量的读取和发送,既然是管理设备那就应该将被管理的对象纳入自己的势力范围之中,即将被托管的变量定义在自己在内存中的地址映像之中。
2. 根据SRAM的读写时序配置FSMC;
FSMC作为CPU与外部SRAM沟通的桥梁,那就得说他们两个人都认识的话——读写时序,两者沟通关系如下所示:
3. 对变量进行读写操作时,FSMC会进行在CPU和SRAM外部存储器之间进行如下操作:
首先,对FSMC管理的变量Var进行读写就会引起FSMC产生读取SRAM中数据的时序,引发对SRAM的读写操作。
FSMC如何进行读写操作?
读写LCD本质上是对寻址空间的某一个地址进行读写操作,例如:
读:var=*(uint32_t*)0x6C000080;
写:*(uint32_t*)0x6C000080=var;
读写控制是FSMC自动控制的,你向指定地址写数据(如*(uint32_t*)0x6C000080=var),FSMC会自动控制相应的引脚(CS、RS、RD、WR等)发送写数据时序,读也是一样的。
为了更清楚,我们可以看一下相应代码的汇编:
因此,如果是读LCD,则使用的一定是LDR指令(LoadRegister);而写LCD,则使用的是STR指令(StoreRegister)。这是由于不同的指令会对应不同的总线访问时序。
从上面可以得出结论,只要是访问一个存储单元并且写入数据(将变量的值赋值给存储单元),那么FSMC就会控制CS、RS、RD、WR等对这个存储单元进行写操作;我们访问一个存储单元并进行读操作(把存储单元的数据读出来并赋值给变量),那么FSMC就会控制CS、RS、RD、WR等对这个存储单元执行读操作。
在TFTLCD实验中,FSMC甄别数据/命令的原理
像这种地址线和数据线分开的数据存储器,一般的向数据存储器写数据的时序如下所示:
首先,我们要向数据存储器写入指令:
紧接着,我们要输入指令所需的数据:
我们前面说过FSMC进行读写操作的原理,但是当我们读FSMC中的地址映射单元时,FSMC会输出如下图所示的读时序:
这并不是8080时序,而且我们疑问的是TFTLCD中只有地址线A[25:0],而没有RS信号来决定收发数据还是收发命令。这就很懵了,RS的0/1分别代表着命令/数据,但是我上哪里找0/1呢?此时换位思考一下,惊奇的发现地址线A[25:0]可以帮到我们:
假设TFTLCD只有两个寄存器:数据操作寄存器/命令操作寄存器。当TFTLCD收到寄存器地址”addr=1“时,此时命令操作寄存器响应;同理,当TFTLCD收到寄存器地址“addr=0”时,此时数据操作寄存器响应。以上一波操作就可以把FSMC读写SRAM的时序神奇地转化为8080读写时序了。
我们将A[10]作为RS引脚用于控制TFTLCD是操作命令还是操作数据。如何使得TFTLCD操作数据时A[10]=1,操作命令时A[10]=0是关键:
Bank1.sector4就是从地址0X6C000000开始,而0X000007FE,则是A[10]的偏移量。以A[10]为例,A[10]相对于Bank1.sector4的地址0X7FE换成二进制为:0b011111111110,而16位数据时,地址右移一位对齐,对应到地址引脚就是:A[10:0]=0b001111111111,此时A[10]是0,但是如果16位地址加1(对应到8位地址是加2,即0X7FE+0X02),那么:A[10:0]=0b010000000000,此时A[10]就是1了,即实现了对RS的0和1的控制。
//LCD地址结构体
typedefstruct
{
vu16LCD_REG;
vu16LCD_RAM;
}LCD_TypeDef;
//使用NOR/SRAM的Bank1.sector4,地址位HADDR[27,26]=11A10作为数据命令区分线
//注意设置时STM32内部会右移一位对其!
#defineLCD_BASE((u32)(0x6C000000|0x000007FE))
#defineLCD((LCD_TypeDef*)LCD_BASE)
我们将这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的
地址就是0X6C00,07FE,对应A[10]的状态为0(即RS=0),而LCD->LCD_RAM的地址就是
0X6C00,0800(结构体地址自增),对应A[10]的状态为1(即RS=1),从而实现对RS的控
制。
FSMC的寻址操作
STM32的FSMC支持8/16/32位数据宽度,我们这里用到的LCD是16位宽度的,所以在设置的时候,选择16位宽就OK了。我们再来看看FSMC的外部设备地址映像,STM32的FSMC将外部存储器划分为固定大小为256M字节的四个存储块:
从上图可以看出,FSMC总共管理1GB空间,拥有4个存储块(Bank),本章,我们用到的是块1,所以在本章我们仅讨论块1的相关配置。
STM32的FSMC存储块1(Bank1)被分为4个区,每个区管理64M字节空间,每个区都有独立的寄存器对所连接的存储器进行配置。Bank1的256M字节空间由28根地址线(HADDR[27:0])寻址。
这里HADDR是内部AHB地址总线,其中HADDR[25:0]来自外部存储器地址FSMC_A[25:0],而HADDR[26:27]对4个存储块(Bank)进行寻址,Bank1的地址映射如下所示:
表格3
Bank |
片选信号 |
地址映像 |
HADDR[27:26] |
1 |
FSMC_NE1 |
0X60000000 |
00 |
0X63FFFFFF |
|||
2 |
FSMC_NE2 |
0X64000000 |
01 |
0X67FFFFFF |
|||
3 |
FSMC_NE3 |
0X68000000 |
10 |
0X68FFFFFF |
|||
4 |
FSMC_NE4 |
0X6C000000 |
11 |
0X6FFFFFFF |
特别要注意:
当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]的设置,是不需要我们干预的,即当我们设置了片选HADDR[27:26]自动被设置为相应的数值。比如:当你选择使用Bank1的第三个区,即使用FSMC_NE3来连接外部设备的时候,即对应了HADDR[27:26]=10,我们要做的就是配置对应第3区的寄存器组,来适应外部设备即可。
其实,我们看到一个Bank存储块中又有4个子块,我们称之为扇区(Sector),它的配置不是通过片选而是通过配置寄存器实现,Bank1的寄存器一共有12个(BCKx,BTKx,BWTKx,x=1,2,3,4)。
FSMC的地址映射范围因数据长度而异?
从上表可以看出,FSMC最大访问的存储器位数就是512M,例如Bank1的第1区的寻址范围是0X60000000~0X63FFFFFF,对应的外部存储器的地址为0~0X03FFFFFF,即我们可以访问的位的地址从0~512M。
此时,我们可以用FSMC一个数据存储块最大管理的位数反推FSMC地址映射范围,可得如下结果:
表格4
数据宽度 |
最大访问的数据个数 |
最大访问存储器空间 |
8b |
512M/8=64M |
512M |
16b |
512M/16=32M |
512M |
HADDR的[25:0]位所表示的地址为0~64M,但是当数据长度为16b时,需要访问的地址为0~32M,因此HADDR中只用到了25位。当数据长度为16b时,HADDR的有效位为[25:1]而且外部存储器的地址线接收到的地址必须从0开始寻址,因此要求HADDR向低位偏移一位,如下图所示:
当数据长度为16b时,FSMC中的地址映射仍是一个地址寻址一个字节的数据,但是FSMC地址自增就变了,变成了每次自加2,而非原来的自加1,16b的地址映射关系如下:
何为位段?
如何在FSMC的地址空间中“定义”变量?
位带操作,乍一听名字,也很高大上。但是位带操作实质上就是一个寻址的手段而已。在我们做一件事情时,总喜欢先顶层规划,然后再细分各个工作流程。C语言中常用的寻址方式就是位带操作,我们再C语言中访问一个结构体变量元素的地址,一般常常确定这个结构体变量的地址,再根据结构体中的元素相对于结构体首地址的偏移量确定其具体位置,并且访问该地址的信息。
在lcd.c文件中,有如下代码:
//LCD地址结构体
typedefstruct
{
vu16LCD_REG;
vu16LCD_RAM;
}LCD_TypeDef;
//使用NOR/SRAM的Bank1.sector4,地址位HADDR[27,26]=11A10作为数据命令区分线
//注意设置时STM32内部会右移一位对其!
#defineLCD_BASE((u32)(0x6C000000|0x000007FE))
#defineLCD((LCD_TypeDef*)LCD_BASE)
0x6C000000代表的是FSMC中的Bank1的基地址,而0x000007FE代表的是Bank1的存储块1相对于Bank1的地址偏移量。这样操作就使得LCD_BASE代表FSMC中Bank1的存储块1的地址,然后我们使用强制类型转换,就在LCD_BASE地址上建立了一个结构体变量LCD,其内存分布如下所示:
一定要注意:由于数据是16位的,因此地址自增2,即0X6C008000=0X6C0007FE+0X02。
FSMC的参数配置
对于NORFLASH控制器,主要是通过FSMC_BCRx、FSMC_BTRx和FSMC_BWTRx寄
存器设置(其中x=1~4,对应4个区)。通过这3个寄存器,可以设置FSMC访问外部存储器的时序参数,拓宽了可选用的外部存储器的速度范围。FSMC的NORFLASH控制器支持同步和异步突发两种访问方式。这里,TFTLCD使用的是异步突发访问模式。
对于异步突发访问方式,FSMC主要设置3个时间参数:地址建立时间(ADDSET)、数据建立时间(DATAST)和地址保持时间(ADDHLD)。FSMC综合了SRAM/ROM、PSRAM和NORFlash产品的信号特点,定义了4种不同的异步时序模型。选用不同的时序模型时,需要设置不同的时序参数:
我们这里读写的时序是SRAM,因此我们可以选用模式1/模式A。但是,我们知道TFTLCD的写速度要快于读速度,因此我们分别配置读写速度——模式A。模式A与模式1的最大区别在于:模式1中读写速度一样,模式A中读写速度不同。
模式A支持独立的读写时序控制,这个对我们驱动TFTLCD来说非常有用,因为TFTLCD在读的时候,一般比较慢,而在写的时候可以比较快,如果读写用一样的时序,那么只能以读的时序为基准,从而导致写的速度变慢,或者在读数据的时候,重新配置FSMC的延时,在读操作完成的时候,再配置回写的时序,这样虽然也不会降低写的速度,但是频繁配置,比较麻烦。而如果有独立的读写时序控制,那么我们只要初始化的时候配置好,之后就不用再配置,既可以满足速度要求,又不需要频繁改配置。
在模式A中,我们无非关注两个参数:地址建立时间(ADDSET)、数据建立时间(DATAST)。
- 地址建立时间(ADDSET):用于TFTLCD读取到稳定的RS信号
首先,我们先要知道什么是建立时间:建立时间(Tsu:setuptime)是指在时钟沿到来之前数据从不稳定到稳定所需的时间,如果建立的时间不满足要求那么数据将不能在这个时钟上升沿被稳定的打入触发器。
地址建立的含义:在读取数据之前一定要识别到A[25:0]地址线中的稳定地址信号,这样我们才能进行读写操作。
表格5
RDX |
WRX |
说明 |
高电平 |
高电平 |
地址建立时间 |
RDX和WRX均为无效电平的这段时间不是无效时间,适用于A[25:0]建立稳定的地址信号的时间,因为在读写数据之前,必须要通过地址线建立稳定的联系才行。
- 数据建立时间(DATAST):用于TFTLCD读取到稳定的数据信号
首先,我们要清楚何为保持时间:保持时间(Th:holdtime)是指数据稳定后保持的时间,如果保持时间不满足要求那么数据同样也不能被稳定的打入触发器。
NT35510数据锁存的时序图:
表格6
RDX |
WRX |
说明 |
下降沿 |
高电平 |
占用数据总线 |
上升沿 |
高电平 |
数据所存 |
数据所存的含义:NT35510芯片可以识别稳定的数据。为了我们识别到稳定可靠的数据,数据保持时间必须大于RDX低电平持续时间,即RDX的上升沿必须出现在地址保持时间之内。
FSMC的模式A的读时序如下:
注意:最后的2HCLK是用于存储器将读取到的数据存入锁存器(即保存数据)用的。
FSMC的模式A的写时序如下:
要确定ADDSET和DATAST就必须参考“芯片NT35510的读写时序”,如下所示:
因此,ADDSET最小为250ns/HCLK,DATAST最小为150ns/HCLK,其中,FSMC接在AHB总线上,由HCLK驱动,因此单位周期为HCLK,ADDSET代表着地址线上的地址建立所需的单位周期数量(记得向正无穷取整),DATAST代表着数据线上数据保持的单位周期数量(记得向正无穷取整)。
但是,为了兼容其它型号的屏幕驱动芯片以及STM32F10x系列的缺陷,正点原子例程中ADDSET和DATAST设置如下:
读时序:
- 由于STM32F103FSMC的性能问题,就算设置ADDSET为0,RD的高电平持续时间也达到了190ns以上,所有,我们这里可以设置ADDSET为较小的值,本章我们设置ADDSET为1,即2个HCLK周期,实际RD高电平大于200ns。
- 为了兼容其他屏,我们这里设置DATAST为15,也就是15+1=16个HCLK周期,时间大约是234ns。
写时序:
对NT35510来说,这两个时间只需要15ns就够了,比读操作快得多。所以我们这里设置DATAST为3,即4个HCLK周期,时间约为55ns(因为9320等控制器,这个时间要求比较长,要50ns)。然后ADDSET(也存在性能问题)设置为0,即1个HCLK周期,实际WR高电平时间大于100ns。
FSMC的寄存器介绍
SRAM/NOR闪存片选控制寄存器 1…4 (FSMC_BCR1…4)
在片选配置寄存器中,由如下几个位很重要:
- 扩展使能位:EXTMOD
扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读 写不同的时序,故该位需要设置为 1。
- 写使能位:WREN
扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读 写不同的时序,故该位需要设置为 1。
- 存储数据总线宽度:MWID
存储器数据总线宽度。00,表示 8 位数据模式;01 表示 16 位数据模式;10 和 11 保留。我们的 TFTLCD 是 16 位数据线,所以设置 WMID[1:0]=01。
- 存储器类型:MTYP
00 表示 SRAM、ROM;01 表示 PSRAM;10 表示 NOR FLASH;11 保留。前面提到,我们把 TFTLCD 当成 SRAM 用,所以需要设置 MTYP[1:0]=00。
- 存储块使能位:MBKEN
这个容易理解,我们需要用到该存储块控制 TFTLCD,当然要 使能这个存储块了。
SRAM/NOR闪存片选时序寄存器 1…4 (FSMC_BTR1…4)
由于我们配置了扩展模式位使能,因此FSMC对SRAM的读写速度不同,FSMC_BTRx代表着存储块x的读时序。如果扩展模式位失能呢个,则读写时序的配置均有该寄存器完成。
- 访问模式位:ACCMOD
00 表示访问模式 A;01 表示访问模式 B;10 表示访问模式 C; 11 表示访问模式 D,本章我们用到模式 A,故设置为 00。
- 数据保持时间:DATAST
读时序中数据保持时间为16HCLK,由于0X0000代表1HCLK,因此16HCLK用0X0F表示。由表中数据可以看出,DATLAT最大为255,即256个HCLK。
- 地址建立时间:ADDSET
由于STM32F10x系类的FSMC缺陷和与NT35510读时序的关系,我们配置ADDSET为0X0000。由图中可知,ADDSET最大为255,即256HCLK。
SRAM/NOR闪存写时序寄存器 1…4 (FSMC_BWTR1…4)
由于我们配置了扩展模式位使能,因此FSMC对SRAM的读写速度不同,FSMC_BWTRx代表着存储块x的写时序。
该寄存器位的配置和含义与读时序配置寄存器大同小异。
程序解析
FSMC初始化
FSMC读写SRAM时序的配置结构体类型:FSMC_NORSRAMTimingInitTypeDef
表格 7
参数 |
说明 |
FSMC_AddressSetupTime |
地址建立时间 |
FSMC_AddressHoldTime |
地址保持时间(未用) |
FSMC_DataSetupTime |
数据保持时间(配置的DATAST位) |
FSMC_BusTurnAroundDuration |
总线恢复时间 |
FSMC_CLKDivision |
分频系数:FSMC_CLK与HCLK的关系(未用) |
FSMC_DataLatency |
数据保持时间(配置的DATLAT位) |
FSMC_AccessMode |
FSMC的访问模式 |
FSMC读写SRAM的初始化结构体类型:FSMC_NORSRAMInitTypeDef
表格 8
参数 |
说明 |
FSMC_Bank |
存储块片选使能(有用) |
FSMC_DataAddressMux |
数据地址复用 |
FSMC_MemoryType |
存储器类型(有用) |
FSMC_MemoryDataWidth |
数据宽度(有用) |
FSMC_BurstAccessMode |
突发访问模式 |
FSMC_WaitSignalPolarity |
等待信号的极性 |
FSMC_AsynchronousWait |
同步等待时间 |
FSMC_WriteOperation |
写使能/失能(有用) |
FSMC_WrapMode |
是否支持费对其成组模式 |
FSMC_WaitSignal |
等待信号使能/失能 |
FSMC_ExtendedMode |
扩展模式位使能/失能(有用) |
FSMC_WriteBurst |
同步写-CBURSTRW(bit19) |
FSMC_WaitSignalActive |
WAITCFG-bit11 |
FSMC_ReadWriteTimingStruct |
读时序配置(有用) |
FSMC_WriteTimingStruct |
写时序配置(有用) |
示例代码如下:
FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure;
FSMC_NORSRAMTimingInitTypeDef readWriteTiming;
FSMC_NORSRAMTimingInitTypeDef writeTiming;
readWriteTiming.FSMC_AddressSetupTime = 0x01; //地址建立时间(ADDSET)为2个HCLK 1/36M=27ns
readWriteTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(ADDHLD)模式A未用到
readWriteTiming.FSMC_DataSetupTime = 0x0f; // 数据保存时间为16个HCLK,因为液晶驱动IC的读数据的时候,速度不能太快,尤其对1289这个IC。
readWriteTiming.FSMC_BusTurnAroundDuration = 0x00;
readWriteTiming.FSMC_CLKDivision = 0x00;
readWriteTiming.FSMC_DataLatency = 0x00;
readWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式A
writeTiming.FSMC_AddressSetupTime = 0x00; //地址建立时间(ADDSET)为1个HCLK
writeTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(A
writeTiming.FSMC_DataSetupTime = 0x03; 数据保存时间为4个HCLK
writeTiming.FSMC_BusTurnAroundDuration = 0x00;
writeTiming.FSMC_CLKDivision = 0x00;
writeTiming.FSMC_DataLatency = 0x00;
writeTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式A
FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM4;// 这里我们使用NE4 ,也就对应BTCR[6],[7]。
FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; // 不复用数据地址
FSMC_NORSRAMInitStructure.FSMC_MemoryType =FSMC_MemoryType_SRAM;// FSMC_MemoryType_SRAM; //SRAM
FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;//存储器数据宽度为16bit
FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode =FSMC_BurstAccessMode_Disable;// FSMC_BurstAccessMode_Disable;
FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait=FSMC_AsynchronousWait_Disable;
FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;
FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; // 存储器写使能
FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Enable; // 读写使用不同的时序
FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &readWriteTiming; //读写时序
FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &writeTiming; //写时序
基于FSMC实现的三个基础参数
1. 读LCD的返回数据:
//读LCD数据
//返回值:读到的值
u16 LCD_RD_DATA(void)
{
vu16 ram; //防止被优化
ram=LCD->LCD_RAM;
return ram;
}
2. 不但访问寄存器而且还写入一个数据:
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue)
{
LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号
LCD->LCD_RAM = LCD_RegValue;//写入数据
}
3. 读寄存器里面的数据:
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(u16 LCD_Reg)
{
LCD_WR_REG(LCD_Reg); //写入要读的寄存器序号
delay_us(5);
return LCD_RD_DATA(); //返回读到的值
}
4. 单纯访问寄存器,不写入寄存器数据:
//写寄存器函数
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{
LCD->LCD_REG=regval;//写入要写的寄存器序号
}
5. 单纯写数据:
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(u16 data)
{
LCD->LCD_RAM=data;
}
常见问题
如何计算一个字符对应点阵集所占的字节数?
字体大小分为1608,2412,1206三种形式,例如1608代表字符长16个位宽8个位,如下所示:
在正点原子给lcd.c中有个函数位LCD_ShowChar,这个函数是最基本的显示字符的函数,他是显示一切ASCII字符串的“鼻祖”,其中有个计算ASCII字符所占字节个数的代码如下所示:
u8 csize=(size/8+((size%8)?1:0))*(size/2); //得到字体一个字符对应点阵集所占的字节数
我们可以这样来看:
size是整形变量,表示一个ASCII字符的长度,由于整型变量相除还是整型变量,C语言默认向0取整,因此size/8+(size%8?1:0)代表了1列也就是16个位所需的字节数,然后我们知道宽=列/2,因此size/2代表了一行共有多少列,因此列数*每列所需的字节数=一个ASCII字符显示所需的字节数。
为何可以在font.h中定义二维数组?
我们学过C语言的同学都熟知:.h文件中只能包含.c文件中的函数声明和一些宏定义,自定义数据类型的定义。但是正点原子的LCD的专属字库文件font.h中定义了用于存放字库数据的二维const类型数组。首先,在C语言中如果这个.h文件为某个.c文件所专属(比如font.h文件就是供lcd.c专用的),我们不建议这样做。比如,我如果在另一个.c文件中引用font.h字库文件,MDK会爆出以下错误:
多次引用同一个.h文件(这个.h文件中得定义有数据才会出现上述错误),相当于在多个.c文件中多次定义该变量,这会导致变量重定义错误。如果要在多个文件中引用.h文件中的变量,只需将font.h中的变量声明成static类型的即可,运行成功如下图所示:
但在C++中,const类型隐含着带有static类型的属性,因此在C++中只需将.h文件中的变量声明成const类型即可。
.h文件中的每个变量拥有了static类型之后,当某个.c文件引用该.h文件时,该.h文件中的所有变量会变成这个.c文件的专属变量,即该.c文件的局部变量。相同名称的全局变量会起冲突,而相同名称但是存在于不同.c文件中的局部变量不会起冲突。
编译器的-O2优化
首先,我们要知道-O2是干什么的,-O2是一个优化等级。在MDK5中,我们可以自选优化等级:
该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用以提高目标代码的运行速度。·其中,最直接的优化算法就是“去掉中间变量,一步到位”,具体示例如下:
#include <stdio.h>
void delay(long val);
int main(){
delay(1000000);
return 0;
}
void delay(volatile long val){
while(val--);
}
我们虽然加了volatile,但是这只能保证在频繁的改变一个变量时,编译器不会将其认为“仅仅改变了一次”。-O2执行的是空间上的优化,即代码精简,与volatile八竿子打不着。当我们使用-O0和-O2优化时,会出现如下结果:
MDK5中4中不同的等级如下:
-O0:不做任何优化,这是默认的编译选项;
-O1:对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存。使用本项优化,编译器会尝试减小生成代码的尺寸,以及缩短执行时间,但并不执行需要占用大量编译时间的优化;
-O2:该优化选项会牺牲部分编译速度,除了执行-O1所执行的所有优化之外,还会采用几乎所有的目标配置支持的优化算法,用来精简代码所占用的空间长度,以提高目标代码的运行速度;
-O3:该选项除了执行-O2所有的优化选项之外,一般都是采取很多向量化算法,提高代码的并行执行程度(时间域上的优化),利用现代CPU中的流水线,Cache等。
STM32F4的白屏问题
当我们提高优化等级时,之伴随的是执行效率的提高和更多的BUG。我们将MDK的优化等级从-O0提高至-O2时,编译后的程序可能会出现以下问题:
- 编译乱序引起的问题,程序当中没有放置正确的内存屏障,编译优化引起的乱序执行导致程序出现bug;
- 时序引起的问题,优化后的代码执行速度要比优化前快得多,这可能引起时序不符合要求,最终导致bug;
STM32F4白屏的原因就是因为FSMC速度过快,导致编译乱序问题。我们要解决的方法无非就是延迟一下使得程序慢下来,在正点原子的程序中,延迟的原理是“找一个中间变量,数据是通过中间变量传给目标变量,这样数据的传输过程就经过了两个寄存器”,代码实现如下:
volatile到底有什么用?
volatile修饰是告诉编译器, 这个变量有可能被其他子程序改变,所以不能从寄存器里读取变量的值,要从内存里读取。 就是说修饰的确实是一个变量,而且是一个“容易变”的变量。在每次取这个变量值的时候,要求不是取它上次在某个时候取的临时缓存变量(比如说暂存在某个寄存器中),而是直接到内存中取。 volatile变量能防止优化,别如说你在某个地方可能连续调用了好几次这个函数,于是编译器优化后,可能就调用一次,其他几次就采用这一次调用的返回值,而volatile修饰后,要让每一次都进行函数调用, 而不采用寄存器里的暂存值。
内存数量级的关系
- 1KB=1024B 1024是2的10次方
- 1MB=1024X1024B 是2的20次方
- 1GB=1024X1024X1024B 是2的30次方
- 1TB=1024X1024X1024X1024B 是2的40次方
版权声明:本文为CSDN博主「肥肥胖胖是太阳」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_45590473/article/details/118443512
暂无评论