【海东青电子原创文章,转载请注明出处:https://www.jianshu.com/p/506b0c696707】
在前一篇文章《STM32硬件基础--FSMC》中讨论了FSMC的基本用法,今天来说说FMC。虽然跟FSMC相比,FMC只是少了一个S,但应用难度可是大大增加了。对于连接片外RAM而言,FSMC接的是静态RAM,FMC接的是动态RAM,而控制动态RAM要复杂得多。举个例子:动态RAM需要不停地(周期性的)刷新,否则RAM中的数据就会丢失,这就产生了“管理”问题,如果用一个MCU来连接DRAM,二者之间交换的就不只是数据(SRAM就是如此)那么简单了,还包含有控制信息。这样,DRAM中除了存储数据的单元,还需要有寄存器(模式寄存器,后面将详述)、还得有个对DRAM的“初始化”过程!这么复杂,为什么还要用DRAM呢?用SRAM不就行了吗?DRAM容量大、便宜呀!所以,PC上那几个G的内存条,都是动态RAM类型的。
STM32F746G-DISCO 板子上使用的是MICRON(美光)公司的 MT48LC4M32B2B5-6A SDRAM芯片。SDRAM,同步的动态RAM的意思。同步,就是需要一个片外时钟CLK信号,所有的读写操作都是跟着这个时钟信号走的,这就叫作“同步”。无疑,SDRAM 是本文中的男一号,是讨论的核心。我们的任务就是如何通过MCU来正确读写SDRAM中的数据。
遗憾的是,SDRAM所涉及的技术比较复杂,需要先了解一些SDRAM技术规范,推荐2个背景资料:
1、《高手进阶,终极内存技术指南》,讲解动态RAM的经典之作,初学者必看。了解基本概念即可。
2、《FMC—扩展外部 SDRAM》,一般性地介绍了SDRAM的基础知识,讲的比较清楚。(向技术博客的原作者致谢!)
上面这些背景资料只需要了解基本概念即可,下面将针对 STM32F746G-DISCO 板子一步步地说明驱动片外SDRAM的方法,包括:
1)使用STM32CUBEMX配置FMC;
2)初始化片外SDRAM;
3)周期性地刷新SDRAM;
4)写代码,通过指针、数组的方式读写片外RAM。
【完整的代码可从此处下载:STM32的FMC例程】
为了配置FMC,我们先采取一个偷懒的办法:对于ST官方的开发板,CUBEMX是可以自动配置板子上所有IP资源的,我们重点关心的是有关FMC的参数配置。新建一个CUBEMX项目,选择 STM32F746G-DISCO 板子:
弹出一个对话框,询问是否初始化板子上的所有硬件资源,选 Yes :
找到 FMC 项目,CUBEMX自动配置的参数如下:
赶紧拿个小本子把这些参数记录下来!一会儿我们自己配置FMC时,就要用到这些参数。顺便多说一句,看看上图中FMC的GPIO配置:
注意图五中的PC3这个pin,后面会说到它的问题,这里先卖个关子。
结合F746的芯片参考手册,来重点看看图四中的那些参数。FMC的结构如下:
图六中可以看到,FMC包含了FSMC的功能,此外还支持SDRAM。对于SDRAM,支持2个“Bank”,这是2个物理Bank的意思,可以理解为FMC支持连接2个SDRAM芯片,MCU通过不同的片选信号SDNE0和SDNE1来选择具体使用哪个片外RAM:
我们板子上只有一个SDRAM芯片,它连接的是FMC的Bank1呢,还是Bank2呢?打开 STM32F746G-DISCO 板子的原理图找答案:
显然,使用的是SDNE0,那就是FMC的Bank1无疑了,图七中显示这个Bank内存的起始地址是 0xC000 0000 。这样,在图四的CUBEMX配置界面中,我们应选择的是 SDRAM 1(即Bank1),以及 SDCKE0+SDNE0。但,图四中的 Internal bank number 为什么要选 4 banks 呢?这是说SDRAM芯片的规格是内部有4个bank(即《高手进阶,终极内存技术指南》中说的L-BANK),打开芯片 MT48LC4M32B2 的文档:
可见,内部是4个Bank,并且行地址为12-bit,列地址为8-bit,所以图四中的Address要设置成12bit。图四中的 Data 选项(在这里应该是FMC数据宽度的意思)需要特别说明一下:MT48LC4M32B2是32位宽的(一次可以读/写32-bit数据),这里为什么配置成了 16 bit 呢?再来看看开发板的原理图(上面图十),高16位数据线DQ16-DQ31接地、没有用到!---- 把这个SDRAM当16-bit的芯片来使用了(那不是浪费了一半的存储容量?确实如此!)。这是因为,STM32F746G-DISCO 板子跑 touchgfx 应用时,只支持16位色,所以16-bit的位宽够用了(对应的,编程时使用 uint16_t 的数据类型)。
再看图四下方的参数,重点讨论一下 CAS latency(SDRAM规范文档中的标识符为:CL),SDRAM文档中称为“读取潜伏期”,具体到 MT48LC4M32B2 这个芯片,CAS latency 的取值范围是这样规定的:
CL不能小于18ns,换算成SDRAM的CLK是多少呢?首先要看一下CUBEMX中配置的时钟频率:
MCU的HCLK配置成了200MHz,图四中,SDRAM的CLK是HCLK/2(SDRAM common clock 为 2 HCLK clock cycles),即 100MHz,一个CLK是 1/100MHz == 10ns,即CL要大于1.8个CLK,取整后最小值为2。CUBEMX把CL配置成3是放了一些余量。
图四最下面是SDRAM时序的参数,这里暂时先跳过,我们先按CUBEMX的参数,照猫画虎,自己动手配置FMC。新建一个CUBEMX项目,这次我们不选开发板、而是按选择芯片的方式新建项目:
为了方便调试,配置板子上的LED(PI口的PIN1):
重点是FMC:
重要的问题来了:我们来看看CUBEMX为FMC自动分配的IO管脚:
注意,FMC的信号线 FMC_SDCK0 分配给了 PH2 这个管脚。再来看之前提到的那个图五中的“关子”: FMC_SDCK0 分配给了 PC3!查F746的芯片手册知道,PC3、PH2都可以通过复用功能映射成 FMC_SDCK0 信号。开发板上到底是用的哪个pin?原理图上找答案:
可见,图五中的管脚配置是“正确”的。但图二十一的配置也可以理解(不是CUBEMX的bug):因为是按选择芯片型号来让CUBEMX自动配置FMC的,CUBEMX并不知道用户实际的电路板上FMC是用哪个pin来对应 FMC_SDCK0 信号的,只能把 FMC_SDCK0 信号分配到某一个pin上(本例中是PH2)。如果与实际电路不符,需要由用户手动修改。而在图二十一中,是按开发板来配置的,CUBEMX知道板子的具体型号,就可以按实际的管脚来正确分配 FMC_SDCK0 信号。其他所有具有复用功能的GPIO都有这个问题,希望大家重视。(这是通过血的教训换来的,第一次用CUBEMX配置FMC时,忽略了这个问题,导致编写代码调试SDRAM时,读出数据总是不稳定、或出错,并且难以调试、追踪,耗了一周时间才找到问题根源,教训深刻啊)
现在来修改图二十一中的管脚配置,同时介绍一个配置GPIO复用功能的小技巧。因为PH2不是我们想要的pin,需要查找其他复用功能为 FMC_SDCK0 的pin,可以先按下Ctrl 键,然后鼠标点击 PH2 管脚,CUBE界面上会有2个pin(PC3和PC5)改变了颜色:
修改PC3的功能为 FMC_SDCK0:
最后,别忘了把时钟配置到200MHz:
然后,生成KEIL工程。main.c中,
MX_FMC_Init() 函数实现对FMC的配置,它调用了stm32fxx_hal_sdram.c文件中的 HAL_SDRAM_Init() ,后者首先调用 HAL_SDRAM_MspInit() 对FMC用到的pin初始化,然后分别调用 FMC_SDRAM_Init() 和 FMC_SDRAM_Timing_Init() 对FMC的控制寄存器 FMC_SDCR 和时序寄存器 FMC_SDTR 进行初始化。至此,FMC的配置全部结束,程序可以编译、通过,但还不能正确读写SDRAM,因为SDRAM在正式工作之前,还需要一个“SDRAM初始化”的过程。显然,这个初始化过程对于不同型号的SDRAM可能是不同的,CUBEMX就无能为力了。但仍有捷径可走 ---- 找ST官方的例子:
先找到STM32CUBE的F7库的安装目录,然后进入F746G-DISCO 的项目目录,在例子中有关于FMC的例程。从 FMC_SDRAM 项目中的 main.c 文件中,copy 对SDRAM做初始化的函数:BSP_SDRAM_Initialization_Sequence() 到我们的main.c 中,并在 MX_FMC_Init() 函数尾部添加调用:
请注意,例程 FMC_SDRAM 中的 CAS Latency 是2,对应地设置SDRAM模式寄存器时也是2。而我们在CUBEMX中将CAS Latency 配置为3,则设置SDRAM模式寄存器时也必须是3:
刷新周期的计算:先看看 MT48LC4M32B2 手册对刷新周期的时间要求:
要求最慢在64ms内对4096行(对应12-bit行地址空间)完成刷新,就是说64ms要刷4096次,所以每刷一次的时间间隔是 64ms÷4096=15.625us。换算成SDRAM的CLK(即SDCLK,100MHz):15.625us÷(1/100us)=1562。为了保证刷新周期在SDRAM的任何工作状态下都能小于64ms,要再减去20个SDCLK(这是SDRAM规范文档要求的),即1562-20=1542。例程中用的数字是1292,则刷新周期在54ms左右。一旦完成刷新参数设置(写到 SDRTR 寄存器中),FMC立即自动开始刷新,即每隔大约60ms,MCU通过FMC向SDRAM发送一条刷新指令。
至此,完成了FMC配置、SDRAM初始化、SDRAM刷新3大步骤,到了最后一步:编程访问SDRAM。关键的一个问题是:外部SDRAM地址从0xC000 0000 开始,如何定义一个变量(数组),使得它的地址恰恰从0xC000 0000 开始?一种方法是定义一个uint16_t类型的数据指针,并且使得这个指针指向0xC000 0000地址,如:
#define SDRAM_BANK_ADDR ((uint32_t)0xC0000000)
uint16_t *pSDRAM;
//set data pointer, pointing external sdram
pSDRAM = (uint16_t *)SDRAM_BANK_ADDR;
然后通过 (*pSDRAM + i) 或 pSDRAM[ i ] 即可访问数据了。在演示代码中,先将一些16位的无符号数据写入SDRAM,再从SDRAM中读出到一个内存数组,然后做比较,如果数据全部匹配,则LED每隔一秒钟慢闪一次;如果数据有误,则LED快闪。通过实验可以看到,LED慢闪,表明读写SDRAM是正确的。
完整的代码请从此处下载:STM32的FMC例程,包含 KEIL 和 IAR 两个版本。
最后,补充图四下方时序参数的说明。SDRAM的时序参数,有不同的描述方式,但在SDRAM规范中,它们都是有统一的“代号”的,比如图四中的 Load mode register to active delay,说的是装载模式寄存器需要花费的时间,代号是tMRD。图四中其他的代号如下:
然后,按图索骥,在MT48LC4M32B2 手册中查找:
单位换算关系:我们配置了SDRAM的时钟频率(SDCLK)是100MHz,等同于一个CK是10ns。反之,10ns的时间就是一个CK。可见,图四中的时序参数基本上都是稍微留了余量的。
小结:
1)CUBEMX配置FMC,核心就是图四那一张图(但,本文用了大量的篇幅,才基本说明了这些参数的由来。这么多内容,CUBEMX用一页就搞定了,牛吧?!)。要特别注意GPIO复用功能的管脚配置,须与实际电路连接一致。
2)对于SDRAM,仅仅配置FMC是不够的,还需要编写代码对SDRAM进行初始化,并设置刷新时间。要特别注意 CAS Latency(CL) 的取值,配置FMC的跟SDRAM初始化的要一致(比如,CL都设置成3)。
3)代码中对SDRAM的访问,可以通过定义一个数据指针、并赋值到片外RAM地址的方式实现。
本文完。
参考文献:
1. 《STM32CubeMX配置SDRAM》
详述了配置过程,特别提到了因为TouchGFX对SDRAM做了相关配置,从而系统自动生成了 MX_SDRAM_InitEx() 初始化函数。