1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#
STM32F103ZET6自带了64K字节的RAM,对一般应用来说,已经足够了,不过在一些对内存要求高的场合,比如做华丽效果的GUI,处理大量数据的应用等,STM32自带的这些内存就可能不太够用了。好在嵌入式方案提供了扩展芯片RAM的方法,本章将介绍我们开发板上使用的RAM拓展方案:使用SRAM芯片,并驱动这个外部SRAM提供程序需要的一部分RAM空间,对其进行读写测试。
本章分为如下几个部分:
47.1 存储器简介
47.2 硬件设计
47.3 程序设计
47.4 下载验证
使用电脑时,我们会提到内存和内存条的概念,电脑维修的朋友有时候会说加个内存条电脑就不卡了。实际上对于PC来说一些情况下卡顿就是电脑同时运行的程序太多了,电脑处理速度变慢的现象。而程序是动态加载到内存中的,一种解决方法就是增加电脑的内存来增加同时可处理的程序的数量。对于单片机也是一样的,高性能有时候需要通过增加大内存来获得。内存是存储器的一种,由于微机架构设计了不同的存储器放置不同的数据,所以我们也简单来了解一下存储器。
存储器实际上是时序逻辑电路的一种,用来存放程序和数据信息。构成存储器的存储介质主要采用半导体器件和磁性材料。存储器中最小的存储单位就是一个双稳态半导体电路或一个CMOS晶体管或磁性材料的存储元,它可存储一个二进制代码。由若干个存储元组成一个存储单元,然后再由许多存储单元组成一个存储器。按不同的分类方式,存储器可以有表47.1.1所示的分类:
表47.1.1 存储器的分类
对于上述分类,在我们STM32编程学习中我们常常只关心按读写功能分类的ROM和RAM两种,因为嵌入式程序主要对应到这两种存储器。对于RAM,目前常见的是SRAM和DRAM,它们因工作方式不同而得名,它们主要有以下的特性,如表47.1.2所示:
SRAM DRAM
描述 静态存储器/Static RAM,存储单元一般为锁存器,只要不掉电,信息就不会丢失 动态存储器/Dynamic RAM,利用MOS(金属氧化物半导体)电容存储电荷来储存信息,保留数据的时间很短,速度也比SRAM慢,每隔一段时间,要刷新充电一次,否则内部的数据即会消失。
特点 存取速度快,工作稳定,不需要刷新电路,集成度不高;集成度较低且价格较高 DRAM的成本、集成度、功耗等明显优于SRAM
常见应用 CPU与主存间的高速缓冲、CPU内部的一级/二级缓存、外部的高速缓存、SSRAM DRAM分为很多种,按内存技术标准可分为FPRAM/FastPage、EDO DRAM、SDRAM、DDR/DDR2/DDR3/DDR4/…、RDRAM、SGRAM以及WRAM等。
表47.1.2 SRAM和DRAM特性
在STM32上,我们编译的程序,编译器一般会根据对应硬件的结构把程序中不同功能的数据段分为ZI\RW\RO这样的数据块,执行程序时分别放到不同的存储器上,这部分参考我们《第九章 STM32启动过程分析》中关于map文件的描述。对于我们编写的STM32程序中的变量,在默认配置下是加载到STM32的RAM区中执行的。而像程序代码和常量等编译后就固定不变的则会放到ROM区。
存储器的知识我们就介绍到这里,限于篇幅只能作简单的引用和介绍,大家可以查找资料拓展对各种存储器作一下加深了解。
47.2 SRAM方案简介
RAM的功能我们已经介绍过了,SRAM更稳定,但因为结构更复杂且造价更高,所以有更大片上SRAM的STM32芯片造价也更高。而且由于SRAM集成度低的原因,MCU也不会把片上SRAM做得特别大,基于以上原因,计算机/微机系统中都允许采用外扩RAM的方式提高性能。
1.SRAM芯片介绍
IS62WV51216 方案
IS62WV51216是ISSI(Integrated Silicon Solution, Inc)公司生产的一颗16位宽512K(512*16,即1M字节)容量的CMOS静态内存芯片。该芯片具有如下几个特点:
高速。具有45ns/55ns访问速度。
低功耗。
TTL电平兼容。
全静态操作。不需要刷新和时钟电路。
三态输出。
字节控制功能。支持高/低字节控制。
IS62WV51216的功能框图如图47.1.1所示:
图41.1.1 IS62WV51216功能框图
图中A018为地址线,总共19根地址线(即2^19=512K,1K=1024);IO015为数据线,总共16根数据线。CS2和CS1都是片选信号,不过CS2是高电平有效CS1是低电平有效;OE是输出使能信号(读信号);WE为写使能信号;UB和LB分别是高字节控制和低字节控制信号;
XM8A51216方案
国产替代一直是国内嵌入式领域的一个话题,国产替代的优势一般是货源稳定,售价更低,也有专门研发对某款芯片作Pin to Pin兼容的厂家,使用时无需修改PCB,直接更换元件即可,十分方便。
正点原子开发板目前使用的一款替代IS62WV51216的芯片是XM8A5121,它与IS62WV51216一样采用TSOP44封装,引脚顺序也与前者完全一致。
XM8A51216是星忆存储生产的一颗16位宽512K(512*16,即1M位)容量的CMOS静态内存芯片。采用异步SRAM接口并结合独有的XRAM免刷新专利技术,在大容量、高性能和高可靠及品质方面完全可以匹敌同类SRAM,具有较低功耗和低成本优势,可以与市面上同类型SRAM产品硬件完全兼容,并且满足各种应用系统对高性能和低成本的要求,XM8A51216也可以当做异步SRAM使用,该芯片具有如下几个特点:
⚫高速。具有最高访问速度10/12/15ns。
⚫低功耗。
⚫TTL电平兼容。
⚫全静态操作。不需要刷新和时钟电路。
⚫三态输出。
⚫字节控制功能。支持高/低字节控制。
该芯片与IS62WV51216引脚和完全兼容,控制时序也类似,大家可以方便地直接替换。
本章,我们使用FSMC的BANK1区域3来控制SRAM芯片,关于FSMC的详细介绍,我们在学习LCD的章节已经介绍过,我们采用的是读写不同的时序来操作TFTLCD模块(因为TFTLCD模块读的速度比写的速度慢很多),但是在本章,因为IS62WV51216/XM8A51216的读写时间基本一致,所以,我们设置读写相同的时序来访问FSMC。关于FSMC的详细介绍,请大家看《TFT LCD实验》和《STM32F10xxx参考手册_V10(中文版).pdf》。
47.3 硬件设计
图47.3.1 STM32和SRAM连接原理图(XM8A51216/IS62WV51216封装相同)
SRAM芯片直接是接在STM32F1的FSMC外设上,具体的引脚连接关系如下表47.3.1所示。
战舰 SRAM
A[0:18] FMSC_A[0:18]
(为了布线方便交换了部分IO)
D[0:15] FSMC_D[0:15]
UB FSMC_NBL1
LB FSMC_NBL0
OE FSMC_OE
WE FSMC_WE
CS FSMC_NE3
表47.3.1 STM32和SRAM芯片的连接原理图
在上面的连接关系中,SRAM芯片的A[0:18]并不是按顺序连接STM32F1的FMSC_A[0:18],这样设计的好处,就是可以方便我们的PCB布线。不过这并不影响我们正常使用外部SRAM,因为地址具有唯一性,只要地址线不和数据线混淆,就可以正常使用外部SRAM。
47.4 程序设计
操作SRAM时要通过多个地址线寻址,然后才可以读写数据,在STM32上可以使用FSMC来实现,在TFT_LCD一节我们也已经讲解过FSMC接口的驱动,与之前的用法类似,关于HAL库的部分我们这里就不重复介绍了。
使用SRAM的配置步骤:
1)使能FSMC时钟,并配置FSMC相关的IO及其时钟使能。
要使用FSMC,当然首先得开启其时钟。然后需要把FSMC_D015,FSMCA018等相关IO口,全部配置为复用输出,并使能各IO组的时钟。
2)设置FSMC BANK1区域3的相关寄存器。
此部分包括设置区域3的存储器的工作模式、位宽和读写时序等。本章我们使用模式A、16位宽,读写共用一个时序寄存器。
3)使能BANK1区域3。
最后,需要通过FSMC_BCR寄存器使能BANK1的区域3,使FSMC工作起来。
通过以上几个步骤,我们就完成了FSMC的配置,初始化FSMC后就可以访问SRAM芯片时行读写操作了,这里还需要注意,因为我们使用的是BANK1的区域3,所以HADDR[27:26]=10,故外部内存的首地址为0X68000000。
47.4.1 程序流程图
图47.4.1.1 SRAM实验程序流程图
47.4.2 程序解析
#define SRAM_WR_GPIO_PORT GPIOD
#define SRAM_WR_GPIO_PIN GPIO_PIN_5
#define SRAM_WR_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE();}while(0)
#define SRAM_RD_GPIO_PORT GPIOD
#define SRAM_RD_GPIO_PIN GPIO_PIN_4
#define SRAM_RD_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)
/* SRAM_CS(需要根据SRAM_FSMC_NEX设置正确的IO口) 引脚 定义 */
#define SRAM_CS_GPIO_PORT GPIOG
#define SRAM_CS_GPIO_PIN GPIO_PIN_10
#define SRAM_CS_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE();}while(0)
根据stm32f1参考手册,SRAM可以选择FSMC对应的存储块1上的4个区域之一作为访问地址,它上面有四块相互独立的64M的连续寻址空间,为了能灵活根据不同的计算出使用的地址空间,我们定义了以下的宏:
/* FSMC相关参数 定义
* 注意: 我们默认是通过FSMC块3来连接SRAM, 块1有4个片选: FSMC_NE1~4
*
* 修改SRAM_FSMC_NEX, 对应的SRAM_CS_GPIO相关设置也得改
*/
#define SRAM_FSMC_NEX 3 /* 使用FSMC_NE3接SRAM_CS,取值范围只能是: 1~4 */
/*****************************************************************/
/* SRAM基地址, 根据 SRAM_FSMC_NEX 的设置来决定基址地址
* 我们一般使用FSMC的块1(BANK1)来驱动SRAM, 块1地址范围总大小为256MB,均分成4块:
* 存储块1(FSMC_NE1)地址范围: 0X6000 0000 ~ 0X63FF FFFF
* 存储块2(FSMC_NE2)地址范围: 0X6400 0000 ~ 0X67FF FFFF
* 存储块3(FSMC_NE3)地址范围: 0X6800 0000 ~ 0X6BFF FFFF
* 存储块4(FSMC_NE4)地址范围: 0X6C00 0000 ~ 0X6FFF FFFF
*/
#define SRAM_BASE_ADDR (0X60000000 + (0X4000000 * (SRAM_FSMC_NEX - 1)))
上述定义SRAM_FSMC_NEX的值为3,即使用FSMC存储块1的第3个地址范围,上面的SRAM_BASE_ADDR则根据我们使用的存储块计算出SRAM空间的首地址,存储块3对应的是0X68000000 ~ 0X6BFFFFFF的地址空间。
sram_init的类似于LCD,我们需要根据原理图配置SRAM的控制引脚,复用连接到SRAM芯片上的IO作为FSMC的地址线,根据SRAM芯片上的进序设置地址线宽度、等待时间、信号极性等,则sram的初始化函数我们编写如下:
void sram_init(void)
{
GPIO_InitTypeDef GPIO_Initure;
FSMC_NORSRAM_TimingTypeDef fsmc_readwritetim;
SRAM_CS_GPIO_CLK_ENABLE(); /* SRAM_CS脚时钟使能 */
SRAM_WR_GPIO_CLK_ENABLE(); /* SRAM_WR脚时钟使能 */
SRAM_RD_GPIO_CLK_ENABLE(); /* SRAM_RD脚时钟使能 */
__HAL_RCC_FSMC_CLK_ENABLE(); /* 使能FSMC时钟 */
__HAL_RCC_GPIOD_CLK_ENABLE(); /* 使能GPIOD时钟 */
__HAL_RCC_GPIOE_CLK_ENABLE(); /* 使能GPIOE时钟 */
__HAL_RCC_GPIOF_CLK_ENABLE(); /* 使能GPIOF时钟 */
__HAL_RCC_GPIOG_CLK_ENABLE(); /* 使能GPIOG时钟 */
GPIO_Initure.Pin = SRAM_CS_GPIO_PIN;
GPIO_Initure.Mode = GPIO_MODE_AF_PP;
GPIO_Initure.Pull = GPIO_PULLUP;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SRAM_CS_GPIO_PORT, &GPIO_Initure); /* SRAM_CS引脚模式设置 */
GPIO_Initure.Pin = SRAM_WR_GPIO_PIN;
HAL_GPIO_Init(SRAM_WR_GPIO_PORT, &GPIO_Initure); /* SRAM_WR引脚模式设置 */
GPIO_Initure.Pin = SRAM_RD_GPIO_PIN;
HAL_GPIO_Init(SRAM_RD_GPIO_PORT, &GPIO_Initure); /* SRAM_CS引脚模式设置 */
/* PD0,1,4,5,8~15 */
GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1 | 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_Initure.Mode = GPIO_MODE_AF_PP; /* 推挽复用 */
GPIO_Initure.Pull = GPIO_PULLUP; /* 上拉 */
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(GPIOD, &GPIO_Initure);
/* PE0,1,7~15 */
GPIO_Initure.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;
HAL_GPIO_Init(GPIOE, &GPIO_Initure);
/* PF0~5,12~15 */
GPIO_Initure.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;
HAL_GPIO_Init(GPIOF, &GPIO_Initure);
/* PG0~5,10 */
GPIO_Initure.Pin = GPIO_PIN_0 | GPIO_PIN_1 |
GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
HAL_GPIO_Init(GPIOG, &GPIO_Initure);
g_sram_handler.Instance = FSMC_NORSRAM_DEVICE;
g_sram_handler.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
g_sram_handler.Init.NSBank = (SRAM_FSMC_NEX == 1) ? FSMC_NORSRAM_BANK1 : \
(SRAM_FSMC_NEX == 2) ? FSMC_NORSRAM_BANK2:\
(SRAM_FSMC_NEX == 3) ? FSMC_NORSRAM_BANK3:\
FSMC_NORSRAM_BANK4; /* 根据配置选择FSMC_NE1~4 */
/* 地址/数据线不复用 */
g_sram_handler.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
g_sram_handler.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; /* SRAM */
/* 16位数据宽度 */
g_sram_handler.Init.MemoryDataWidth = SMC_NORSRAM_MEM_BUS_WIDTH_16;
/* 是否使能突发访问,仅对同步突发存储器有效,此处未用到 */
g_sram_handler.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
/* 等待信号的极性,仅在突发模式访问下有用 */
g_sram_handler.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
/* 存储器是在等待周期之前的一个时钟周期还是等待周期期间使能NWAIT */
g_sram_handler.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
/* 存储器写使能 */
g_sram_handler.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
/* 等待使能位,此处未用到 */
g_sram_handler.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
/* 读写使用相同的时序 */
g_sram_handler.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
/* 是否使能同步传输模式下的等待信号,此处未用到 */
g_sram_handler.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
g_sram_handler.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; /* 禁止突发写 */
/* FMC读时序控制寄存器 */
/* 地址建立时间(ADDSET)为1个HCLK 1/72M=13.8ns */
fsmc_readwritetim.AddressSetupTime = 0x00;
fsmc_readwritetim.AddressHoldTime = 0x00;/* 地址保持时间(ADDHLD)模式A未用到 */
fsmc_readwritetim.DataSetupTime = 0x01;/* 数据保存时间为3个HCLK=4*13.8=55ns */
fsmc_readwritetim.BusTurnAroundDuration = 0X00;
fsmc_readwritetim.AccessMode = FSMC_ACCESS_MODE_A; /* 模式A */
HAL_SRAM_Init(&g_sram_handler,&fsmc_readwritetim,&fsmc_readwritetim);
}
初始化成功后,FSMC控制器就能根据扩展的地址线访问SRAM的数据,于是我们可以直接根据地址指针来访问SRAM,我们定义SRAM的写函数如下;
void sram_write(uint8_t *pbuf, uint32_t addr, uint32_t datalen)
{
for (; datalen != 0; datalen--)
{
*(volatile uint8_t *)(SRAM_BASE_ADDR + addr) = *pbuf;
addr++;
pbuf++;
}
}
同样地,也是利用地址,可以构造出一个SRAM的连续读函数:
void sram_read(uint8_t *pbuf, uint32_t addr, uint32_t datalen)
{
for (; datalen != 0; datalen--)
{
*pbuf++ = *(volatile uint8_t *)(SRAM_BASE_ADDR + addr);
addr++;
}
}
注意以上两个函数是操作unsigned char类型的指针,当使用其它类型的指针时需要注意指针的偏移量。难点主要是根据SRAM芯片上的时序来初始化FSMC控制器,大家参考芯片手册上的时序结合代码来理解这部分初始化的过程。
2. main.c代码
初始化好了SRAM,我们就可以使用SRAM中的存储进行编程了,我们利用ARM编译器的特性:可以在某一绝对地址定义变量。为方便测试,我们直接定义一个与SRAM容量大小类似的数组,由于是1M位的RAM,我们定义了uint32_t类型后,大小要除4,故定义的测试数组如下:
/* 测试用数组, 起始地址为: SRAM_BASE_ADDR */
#if (__ARMCC_VERSION >= 6010050)
uint32_t g_test_buffer[250000] __attribute__((section(".bss.ARM.__at_0x68000000")));
#else
uint32_t g_test_buffer[250000] __attribute__((at(SRAM_BASE_ADDR)));
#endif
这里的__attribute__(())是ARM编译器的一种关键字,它有很多种用法,可以通过特殊修饰指定变量或者函数的属性。大家可以去MDK的帮助文件里查找这个关键字的其它用法。这里我们要用这个关键字把变量放到指定的位置,而且用了条件编译,因为MDK的AC5和AC6下的语法不同。
通过前面的描述,我们知道SRAM的访问基地址是0x68000000,如果我们定义一个与SRAM空间大小相同的数组,而且数组指向的位置就是0x68000000的话,则这通过数组就可以很方便直接操作这块存储空间。所以回来前面所说的__attribute__这个关键字。对于AC5,它可以用__attribute__((at(地址)))的方法来修饰变量,而且这个地址可以是一个算式,这样编译器在编译时就会通过这个关键字判断并把这个数组放到我们定义的空间,如果硬件支持的情况下,我们就可以访问这些指定空间的变量或常量了。但是对于AC6,同样指定地址,需要用__attribute__((section(“.bss.ARM.__at_地址”)))的方法,指定一个绝对地址才能把变量或者常量放到我们所需要定义的位置。这里这个地址就不支持算式了,但是这个语法对于相对而言更加地通用,其它平台的编译器如gcc也有类似的语法,而且AC5下也可以用AC6的这种语法来达到相同效果,两者之间的差异,大家可以多实践以进行区分。
完成SRAM部分的代码,main函数只要实现对SRAM的读写测试即可,我们。加入按键和LCD显示来辅助显示,在main函数中编写代码如下:
int main(void)
{
uint8_t key;
uint8_t i = 0;
uint32_t ts = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
usmart_dev.init(72); /* 初始化USMART */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
sram_init(); /* SRAM初始化 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "SRAM TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY0:Test Sram", RED);
lcd_show_string(30, 130, 200, 16, 16, "KEY1:TEST Data", RED);
for (ts = 0; ts < 250000; ts++)
{
g_test_buffer[ts] = ts; /* 预存测试数据 */
}
while (1)
{
key = key_scan(0); /* 不支持连按 */
if (key == KEY0_PRES)
{
fsmc_sram_test(30, 150); /* 测试SRAM容量 */
}
else if (key == KEY1_PRES) /* 打印预存测试数据 */
{
for (ts = 0; ts < 250000; ts++)
{ /* 显示测试数据 */
lcd_show_xnum(30, 170, g_test_buffer[ts], 6, 16, 0, BLUE);
}
}
else
{
delay_ms(10);
}
i++;
if (i == 20)
{
i = 0;
LED0_TOGGLE(); /* LED0闪烁 */
}
}
}
47.4 下载验证
在代码编译成功之后,我们通过下载代码到开发板上,得到如图47.4.1所示界面:
图47.4.1 程序运行效果图
此时,我们按下KEY0,就可以在LCD上看到内存测试的画面,同样,按下KEY1,就可以看到LCD显示存放在数组g_test_buffer里面的测试数据,我们把数组的下标直接写到SRAM中,可以看到这个数据在不断地更新,SRAM读写操作成功了,如图47.4.2所示:
图47.4.2 外部SRAM测试界面
该实验我们还可以借助USMART来测试,如图47.4.3 所示:
图47.4.3 借助USMART测试外部SRAM读写