目录
一、SRAM介绍
SRAM(Static Random-Access Memory)即静态随机存取存储器,所谓“静态”是指这种存储器只要保持通电,里面存储的数据就可以一直保持,但是掉电之后就会丢失。与DRAM(动态随机存取存储器)相比它不需要周期性的刷新里面的数据,操作简单,速度更快,但是更加的昂贵,集成度不如DRAM高。
本文使用的SRAM型号为 IS62WV51216,是 ISSI(Integrated Silicon Solution, Inc)公司生产的一颗16位宽 1M字节容量的CMOS静态内存芯片。
地址线A0到A18寻址空间是512K,因为数据宽度为16位即两个字节,所以512K*2Byte = 1MB容量。LB和UB的功能是控制高低字节的数据有效性,真值表如下:
二、STM32F103系列的FSMC模块
FSMC(Flexible Static Memory Controller)即可变静态存储控制器,通俗的来说是STM32的一个强大的总线控制模块,它将AHB传输信号转换到适当的外部设备协议,满足访问外部设备的时序要求。FSMC将不同的功能映射到了不同的虚拟内存块上,我们实际编程中只需要简单地往特定的虚拟内存上读写数据,便可以实现对外部模块的数据读写操作了,中间的协议操作过程由FSMC模块完成。FSMC模块涉及引脚非常的多,所以只在STM32的大容量产品上才有,我使用的型号是STM32F103ZET6。其中FSMC模块的框图如下:
根据框图,FSMC又分为以下四个存储块:
我们需要用来操作哪一类的设备就用哪一部分对应的存储块,当然并不是只有图上列的这四个设备才支持,其实一个设备的操作时序只要和以上几类设备基本相似,能够互相兼容就一样可以用这个模块进行操作,就像操作TFT彩屏可以用块1一样,因为TFT的操作时序和SRAM类似。
这里选择块1来操作,然后根据芯片手册的描述,块1最大支持对4个SRAM模块的操作,所以块1又在内部被平均划分为了4小块。所以最大支持的单片SRAM为64MB。
因为我的开发板用FSMC_NE3来做SRAM模块的片选引脚,所以我只能用“存储块1 NOR/PSRAM 3”这个区域来操作SRAM模块了,开发板上连线如下:
三、初始化配置及数据访问
1、初始化代码
初始化代码如下:
/****************************************************************************
* Function Name : SRAM_Config
* Description : 初始化FSMC
* Input : None
* Output : None
* Return : None
****************************************************************************/
void SRAM_Config(void)
{
/* 初始化函数 */
GPIO_InitTypeDef GPIO_InitStructure;
FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure;
FSMC_NORSRAMTimingInitTypeDef FSMC_NORSRAMTiming;
/*--------------------------------------------------------------------------------*/
/*------------------- GPIO Config ------------------------------------------------*/
/*--------------------------------------------------------------------------------*/
/* 打开时钟使能 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE
| RCC_APB2Periph_GPIOF | RCC_APB2Periph_GPIOG,
ENABLE);
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5
| GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11
| GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_7 | GPIO_Pin_8
| GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12
| GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3
| GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_12
| GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_Init(GPIOF, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3
| GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_10;
GPIO_Init(GPIOG, &GPIO_InitStructure);
/* ------------------------------------------------------------------------------ */
/* ------------ FSMC Config ----------------------------------------------------- */
/* ------------------------------------------------------------------------------ */
/* 设置读写时序,给FSMC_NORSRAMInitStructure调用 */
/* 地址建立时间,1个HCLK周期, 1/36M = 27ns */
FSMC_NORSRAMTiming.FSMC_AddressSetupTime = 0x00;
/* 地址保持时间,1个HCLK周期 */
FSMC_NORSRAMTiming.FSMC_AddressHoldTime = 0x00;
/* 数据建立时间,63个HCLK周期 4/72 = 55ns */
FSMC_NORSRAMTiming.FSMC_DataSetupTime = 0x03;
/* 数据保持时间,1个HCLK周期 */
FSMC_NORSRAMTiming.FSMC_DataLatency = 0x00;
/* 总线恢复时间设置 */
FSMC_NORSRAMTiming.FSMC_BusTurnAroundDuration = 0x00;
/* 时钟分频设置 */
FSMC_NORSRAMTiming.FSMC_CLKDivision = 0x00;
/* 设置模式,如果在地址/数据不复用时,ABCD模式都区别不大 */
FSMC_NORSRAMTiming.FSMC_AccessMode = FSMC_AccessMode_A;
/* 设置FSMC_NORSRAMInitStructure的数据 */
/* FSMC有四个存储块(bank),我们使用第一个(bank1) */
/* 同时我们使用的是bank里面的第 3个RAM区 */
FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM3;
/* 这里我们使用SRAM模式 */
FSMC_NORSRAMInitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM;
/* 使用的数据宽度为16位 */
FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;
// FSMC_BurstAccessMode_Disable;
FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode =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_WaitSignal = FSMC_WaitSignal_Disable;
FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
/* 设置写使能打开 */
FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable;
/* 选择拓展模式使能,即设置读和写用相同的时序 */
FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;
/* 设置地址和数据复用使能不打开 */
FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;
/* 设置读写时序 */
FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &FSMC_NORSRAMTiming;
/* 设置写时序 */
FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &FSMC_NORSRAMTiming;
/* 打开FSMC的时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE);
FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure);
/*!< Enable FSMC Bank1_SRAM Bank */
FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE);
}
因为我们使用的是块1(Bank1)的第3个RAM区域,这块区域的内存映射是0x68000000到0x6BFFFFFF,所以我们可以简单的理解为外部SRAM模块的1MB数据空间被映射到了以0x68000000地址为起始的后1MB字节空间中了,我们存取数据的时候直接用个指针指向这片区域进行数据的存取操作就可以了,具体的协议操作不用我们操心了。
/* 定义SRAM的地址 */
#define Bank1_SRAM3_ADDR ((vu32)(0x68000000))
vu16 wReadVal;
*(vu16 *)Bank1_SRAM3_ADDR = 1; //写外部SRAM
wReadVal = *(vu16 *)Bank1_SRAM3_ADDR; //读外部SRAM
2、不同数据位宽的访问操作
因为现在这个SRAM模块是16位宽的,所以使用的时候用了“uint16_t”这样的指针对应地去访问,那么是否能进行单字节或者四字节甚至任意字节数据位宽的访问操作呢?
针对这个问题,《STM32中文参考手册》里有明确的说明,下面我摘抄一段:
19.3 AHB接口
AHB接口为内部CPU和其它总线控制设备访问外部静态存储器提供了通道。
AHB操作被转换到外部设备的操作。当选择的外部存储器的数据通道是16或8位时,在AHB上的
32位数据会被分割成连续的16或8位的操作。
AHB时钟(HCLK)是FSMC的参考时钟。
19.3.1 支持的存储器和操作
一般的操作规则
请求AHB操作的数据宽度可以是8位、 16位或32位,而外部设备则是固定的数据宽度,此时需要
保障实现数据传输的一致性。
因此, FSMC执行下述操作规则:
● AHB操作的数据宽度与存储器数据宽度相同:无数据传输一致性的问题。
● AHB操作的数据宽度大于存储器的数据宽度:此时FSMC将AHB操作分割成几个连续的较小
数据宽度的存储器操作,以适应外部设备的数据宽度。
● AHB操作的数据宽度小于存储器的数据宽度:
依据外部设备的类型,异步的数据传输有可能不一致。
─ 与具有字节选择功能的存储器(SRAM、 ROM、 PSRAM等)进行异步传输时, FSMC执行
读写操作并通过它的字节通道BL[1:0]访问正确的数据。
─ 与不具有字节选择功能的存储器(NOR和16位NAND等)进行异步传输时,即需要对16位宽
的闪存存储器进行字节访问;显然不能对存储器进行字节模式访问(只允许16位的数据传
输),因此:
a. 不允许进行写操作
b. 可以进行读操作(控制器读出完整的16位存储器数据,只使用需要的字节)。
看了以上的描述,因为IS62WV51216是有LB和UB引脚的,可以控制字节的有效性,所以编程操作的时候我们是可以进行任意字节位宽的数据操作的。如果换作其他不支持字节通道访问的芯片的话,那就需要对齐芯片的数据位宽进行数据操作了。在网上查资料时看到有人问“在操作不同的数据位宽时,FSMC_NBL1,FSMC_NBL0是受什么控制输出高低电平给UB,LB的?”,这个应该是属于硬件自动实现的,是FSMC的硬件机制。
3、FSMC引脚如何与SRAM芯片的物理引脚对应
19.4.1 NOR和PSRAM地址映像
HADDR[27:26]位用于选择四个存储块之一:
表86 NOR/PSRAM存储块选择
HADDR[27:26](1) 选择的存储块
00 存储块1 NOR/PSRAM 1
01 存储块1 NOR/PSRAM 2
10 存储块1 NOR/PSRAM 3
11 存储块1 NOR/PSRAM 4
(1) HADDR是需要转换到外部存储器的内部AHB地址线。
HADDR[25:0]包含外部存储器地址。 HADDR是字节地址,而存储器访问不都是按字节访问,因
此接到存储器的地址线依存储器的数据宽度有所不同,如下表:
表87 外部存储器地址
数据宽度(1) 连到存储器的地址线 最大访问存储器空间(位)
8位 HADDR[25:0]与FSMC_A[25:0]对应相连 64M字节 x 8 = 512 M位
16位 HADDR[25:1]与FSMC_A[24:0]对应相连, HADDR[0]未接 64M字节/2 x 16 = 512 M位
(1) 对于16位宽度的外部存储器, FSMC将在内部使用HADDR[25:1]产生外部存储器的地址FSMC_A[24:0]。不
论外部存储器的宽度是多少(16位或8位), FSMC_A[0]始终应该连到外部存储器的地址线A[0]。
NOR闪存和PSRAM的非对齐访问支持
每个NOR闪存或PSRAM存储器块都可以配置成支持非对齐的数据访问。
在存储器一侧,依据访问的方式是异步或同步,需要考虑两种情况:
● 异步模式:这种情况下,只要每次访问都有准确的地址,完全支持非对齐的数据访问。
● 同步模式:这种情况下, FSMC只发出一次地址信号,然后成组的数据传输通过时钟CLK顺
序进行。
某些NOR存储器支持线性的非对齐成组访问,固定数目的数据字可以从连续的以N为模的地
址读取(典型的N为8或16,可以通过NOR闪存的配置寄存器设置)。此种情况下,可以把存
储器的非对齐访问模式设置为与AHB相同的模式。
如果存储器的非对齐访问模式不能设置为与AHB相同的模式,应该通过FSMC配置寄存器的相
应位禁止非对齐访问,并把非对齐的访问请求分开成两个连续的访问操作。
这句话很重要“ 对于16位宽度的外部存储器, FSMC将在内部使用HADDR[25:1]产生外部存储器的地址FSMC_A[24:0]。不论外部存储器的宽度是多少(16位或8位), FSMC_A[0]始终应该连到外部存储器的地址线A[0]”。所以FSMC的地址线和实际SRAM地址线相连的时候不需要错位相连。
四、使全局变量定义在外部SRAM中的方法
1、修改system_stm32f10x.c
在system_stm32f10x.c系统文件中,有个叫SystemInit_ExtMemCtl的函数,就是专门为扩展RAM而设计的函数,该函数是在main()函数之前被执行的,它主要完成FSMC总线的配置。我们需要在这个函数里面调用外部SRAM的初始化函数。主要步骤如下:
(1)在system_stm32f10x.c中添加宏定义DATA_IN_ExtSRAM,启用SystemInit_ExtMemCtl函数。
(2)声明初始化函数:extern void SRAM_Config(void);
(3)在SystemInit_ExtMemCtl函数最后调用SRAM_Config();
#define DATA_IN_ExtSRAM
#ifdef DATA_IN_ExtSRAM
/**
* @brief Setup the external memory controller.
* Called in startup_stm32f10x_xx.s/.c before jump to main.
* This function configures the external SRAM mounted on STM3210E-EVAL
* board (STM32 High density devices). This SRAM will be used as program
* data memory (including heap and stack).
* @param None
* @retval None
*/
extern void SRAM_Config(void);
void SystemInit_ExtMemCtl(void)
{
/*!< FSMC Bank1 NOR/SRAM3 is used for the STM3210E-EVAL, if another Bank is
required, then adjust the Register Addresses */
/* Enable FSMC clock */
RCC->AHBENR = 0x00000114;
/* Enable GPIOD, GPIOE, GPIOF and GPIOG clocks */
RCC->APB2ENR = 0x000001E0;
/* --------------- SRAM Data lines, NOE and NWE configuration ---------------*/
/*---------------- SRAM Address lines configuration -------------------------*/
/*---------------- NOE and NWE configuration --------------------------------*/
/*---------------- NE3 configuration ----------------------------------------*/
/*---------------- NBL0, NBL1 configuration ---------------------------------*/
GPIOD->CRL = 0x44BB44BB;
GPIOD->CRH = 0xBBBBBBBB;
GPIOE->CRL = 0xB44444BB;
GPIOE->CRH = 0xBBBBBBBB;
GPIOF->CRL = 0x44BBBBBB;
GPIOF->CRH = 0xBBBB4444;
GPIOG->CRL = 0x44BBBBBB;
GPIOG->CRH = 0x44444B44;
/*---------------- FSMC Configuration ---------------------------------------*/
/*---------------- Enable FSMC Bank1_SRAM Bank ------------------------------*/
FSMC_Bank1->BTCR[4] = 0x00001011;
FSMC_Bank1->BTCR[5] = 0x00000200;
SRAM_Config();
}
#endif /* DATA_IN_ExtSRAM */
添加完之后就可以像片内RAM一样自由的使用外部的SRAM了。
2、配置KEIL选项
我在main.c里定义了两个数组如下:
char bTest1[1024];
char bTest2[1024 * 8];
编译一下工程打开map文件查看数组存放的地址如下图,这里要提醒一下,这两个数组需要在程序中调用一下,要不然map文件中是找不到这两个数组的,也就是编译器优化掉了这两个数组:
bTest1 0x200010b0 Data 1024 main.o(.bss)
bTest2 0x200014b0 Data 8192 main.o(.bss)
因为片内RAM地址是从0x20000000开始的,所以这两个数组都是放在片内RAM的。接下来选择Option for Target -> Target选项卡,在红框中的RAM1位置填写好Start和Size的值,分别为0x68000000(片外SRAM的起始)和0x100000(1M字节的SRAM容量)。然后点击“OK”就配置好可以使用片外的SRAM了。
右键main.c文件在菜单中选择Option for File选项。
在Properties选项卡中的Zero Initialized Data中选择我们刚刚设置的RAM1,就可以将main.c文件中的所有全局变量都定义在外部SRAM中了。
编译工程后这两个数组位置发生了变化,如下图:
bTest1 0x68000000 Data 1024 main.o(.bss)
bTest2 0x68000400 Data 8192 main.o(.bss)
注意点:(1)这两个数组不要在外部SRAM初始化之前调用,否则会出现问题。
(2)局部变量不支持指定地址的定义。
3、在代码中指定
在代码中指定虽然有点麻烦,但是比较灵活,就是在变量后加上“__attribute__((at(具体的地址)))”,如下:
char cnt __attribute__((at(0x20008000)));
char bTest1[1024] __attribute__((at(0x20008003)));
char bTest2[1024 * 8] __attribute__((at(0x68000000)));
内存地址如下:
cnt 0x20008000 Data 1 main.o(.ARM.__AT_0x20008000)
bTest1 0x20008003 Data 1024 main.o(.ARM.__AT_0x20008003)
bTest2 0x68000000 Data 8192 main.o(.ARM.__AT_0x68000000)
注意点:
(1)在外部SRAM初始化之前不要去调用定义在外部SRAM地址处的变量。
(2)针对不同的ARM编译器,定义语句不同:
a、针对AC5(ARMCC Compiler version 5.x)
uint8_t cnt __attribute__((at(0x20008000)));
b、 针对AC6(ARM Compiler 6 (又名ARMCLANG))
uint8_t cnt __attribute__((section(".ARM.__at_0x20008000")));
(3)指定函数的存放地址
对于变量,在其后边加修饰;而对于函数,在声明处加修饰,注意,是在声明处,不是在函数定义处!!!变量指定的地址只能位于RAM区,常量和代码只能位于Flash区。
如果还想了解更多,可以学习“分散加载文件配置”的相关知识。
五、参考文章及资料
1、参考文章
《如何将变量存储在指定内存地址(基于Keil MDK-ARM)》https://blog.csdn.net/ybhuangfugui/article/details/94419544
《超简单!在Keil中指定某个函数或变量存放的地址》 https://blog.csdn.net/oLiShuTong/article/details/78816408
2、资料链接
《IS62WV51216.pdf》
《STM32中文参考手册_V10.pdf》
链接:https://pan.baidu.com/s/1GN6LWIGkOmUC88t4oLL21Q 提取码:4j8o
来源:CSDN
作者:心城追梦
链接:https://blog.csdn.net/qq_34254642/article/details/104151368