22136|15

1307

帖子

1

资源

五彩晶圆(初级)

当STM32遇到SDRAM

 
本帖最后由 cruelfox 于 2017-2-20 13:57 编辑

F429i.jpg
  SDRAM是DRAM(动态随机访问存储器)的一种,是最为普遍使用的大容量RAM——俗称内存。(不过“内存”本来是“内部存储器”的意思,相对于“外部存储器”——磁盘、磁带、光盘等。ROM实际上也属于内部的存储器,只不过不可写只能放固定的信息。但是有了EEPROM、Flash ROM之后,因为这些存储芯片也可以改写,就把“内存”和RAM的概念搅混了——卖手机的告诉你内存有多大。实际上Flash ROM的地位等同于以前的硬盘。)目前PC、服务器以及便携设备的RAM大部分是DDR RAM,也是SDRAM的一种(DDR SDRAM),而一般说SDRAM被用来指非DDR的SDRAM了。

  先看一下DRAM这类存储器的内部结构
dram_array.jpg
  每一个存储单元(1 bit)都是一个小电容加上控制它开关的晶体管,靠电容上有没有存储电荷来记忆0还是1的。所有的存储单元按行列排成一个矩阵:行控制线控制一行晶体管门极(控制通断),列控制线则连接一列晶体管漏极。这样选中某一行时,这一行存储单元的电容就反映在列控制线上:可以读电压判断是0还是1,可以充电或者放电来改写这一行单元。实际的DRAM芯片要比这个复杂,首先电容会漏电,所以需要过一段时间就重新充电或放电来保持记忆;又比如因为矩阵太大了,线上的分布电容比存储单元还要大得多,所以需要Sense amplifier来放大微小电压,需要在读之前给列控制线上的电容进行预充电;又比如因为列数量大于数据总线宽度,一次读取的数据需要锁存起来供多个数据总线周期访问等等,所以控制逻辑是比SRAM复杂的。但是因为单元简单所以DRAM芯片容量做得很大,成本比SRAM要低很多。
sdram_logic.jpg

  SDRAM这个词的"D"代表动态,"S"代表同步,表示它需要一个时钟信号,读写信号在时钟沿上有效。重要的信号除了 CLK, CLKE(时钟允许), /CS(片选) 外,还有
/RAS (低有效): Row Address Strobe, 行地址输入。SDRAM的地址是行和列复用的,省了引脚啊。
/CAS (低有效): Column Address Strobe, 列地址输入。
/WE (低有效): 写使能
但这三个信号并不是按它们的本意单独使用的,而是组合起来定义了若干命令:
sdram_cmd.PNG
  除了读和写的命令,还有配置SDRAM模式寄存器的命令,有维持电容(记忆需要)的刷新命令,有预充电的命令等。在读一个位置数据之前,要先选择所在的行,再输出列地址,等待数据准备好……看起来有点复杂了吧,一连串的SDRAM读操作可能是这样的: read_chart1.PNG
所以,SDRAM读数据远远不如SRAM那么简单,SRAM只要把地址送过去,OE一拉低,随后数据就出来到总线上了。SDRAM首先得把行地址送过去,再把列地址送过去,然后SDRAM还要墨迹几个周期,数据才开始送出来。不过有流水线操作,连续读一个行内数据的时候吞吐速度还是有保证的。与SRAM的对比,主要劣势在于延迟大。

  在单片系统里,如果片上的RAM不够用来存储频繁更新的数据或者程序,外扩RAM是一个解决办法。在引脚资源丰富的单片机上,大都可以连接SRAM,例如STM32F103就有FSMC控制器,但是SRAM芯片成本高且不便宜。要用SDRAM,必须要带有SDRAM控制器的MCU,用GPIO模拟?还是算了吧。ST的单片机我用过不少了,在F4,F7系列某些型号上,是带有支持SDRAM的FMC控制器的,提供了使用SDRAM的途径,不过一定要144 pin以上的器才可以有对应的引脚分配。
stmcu.PNG

  可惜的是Nucleo-144开发板上是不带SDRAM的,得Discovery上才有可能了。我自己DIY了一块F746Z的开发板 【2月DIY】STM32F7开发板自己造,为了参考评估又从论坛借来了一块F429i Discovery开发板。从官方的电路图上可以学习SDRAM是怎样与MCU连接的。
sdram_connect.PNG
  除了Axx地址线,Dxx数据线,NBLx字节选择线这几组是和其它FMC支持的存储器公用线外,其它都是SDRAM专用的。ST MCU的FMC支持两个SDRAM Bank(注意不是指一片SDRAM中的Bank),所以有两组片选和时钟使能。F429i Discovery使用的是Bank2,我自己DIY的是使用Bank1.

  有了FMC支持,SDRAM用起来和片上SRAM一样读写(只不过速度慢点罢了,但容量绝对优势)。配置也是启动后配一次即可,在STM32F429的demo例子中,我找到了SDRAM部分的配置函数调用:
void SDRAM_Init(void)
{
  FMC_SDRAMInitTypeDef  FMC_SDRAMInitStructure;
  FMC_SDRAMTimingInitTypeDef  FMC_SDRAMTimingInitStructure;

  /* GPIO configuration for FMC SDRAM bank */
SDRAM_GPIOConfig();

  /* Enable FMC clock */
  RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_FMC, ENABLE);

  /* Timing configuration for 90 Mhz of SD clock frequency (180Mhz/2) */
   FMC_SDRAMTimingInitStructure.FMC_LoadToActiveDelay    = 2;      
......
/* FMC SDRAM control configuration */
  FMC_SDRAMInitStructure.FMC_Bank = FMC_Bank2_SDRAM;
  /* Row addressing: [7:0] */
  FMC_SDRAMInitStructure.FMC_ColumnBitsNumber = FMC_ColumnBits_Number_8b;
......
  FMC_SDRAMInitStructure.FMC_SDRAMTimingStruct = &FMC_SDRAMTimingInitStructure;

  /* FMC SDRAM bank initialization */
  FMC_SDRAMInit(&FMC_SDRAMInitStructure);

  /* FMC SDRAM device initialization sequence */
  SDRAM_InitSequence();
}

  这里有几个关键步骤:
1. 配置对应的GPIO引脚,不用多说了
2. RCC中使能FMC控制器
3. 配置SDRAM Bank的参数寄存器,包括Timing
4. 执行 InitSequence 给控制器发命令。

  在ST手册上也描述了怎么配置FMC SDRAM部分的寄存器,其实说穿了也简单,主要是SDCR(1或者2), 和SDTR(1或者2). 然后用SDCMR来执行几次命令,等待SDSR寄存器的完成状态。最后在SDRTR中设置SDRAM刷新时间参数。

  SDCR 寄存器
sdcr.PNG
NB 位是根据SDRAM内部Bank数
NR 位是根据SDRAM行地址位数
NC 位是根据SDRAM列地址位数
MWID 位是选择SDRAM数据线宽度
上面这几项设置决定了SDRAM的容量,例如16-bit数据宽度的SDRAM地址映射按照下图,最高位是Bank,然后行地址,再列地址
bus_width.PNG

CAS 位是SDRAM的 /CAS Latency设定,是读命令发出后过多少个时钟周期输出数据有效,它是SDRAM器件的一个可配置参数。
CAS_latency.PNG
WP 位是写保护,若置位则总线上的写请求被FMC忽略。
RBURST 位是允许突发读模式,RPIPE 位是AHB总线上读延迟。这两个设置的影响我还不清楚,有待后面详细测试。

SDTR 寄存器,配置关键的Timing参数,也就是状态之间的最小延迟时钟周期个数。不同速度的器件,和运行的频率,都影响需要的最小延迟。
sdtr.PNG
一共有 TRCD, TRP, TWR, TRC, TRAS, TXSR, TMRD 这些参数,可以通过SDRAM器件的手册获得(直接或推算),例如这样的表格
sdram_char.PNG

最影响SDRAM性能的关键参数是下表中的几个
timing.PNG

  关于Timing设置,延迟设置大了顶多吞吐速率低一点,但设置得不够长回造成读写不正确。我对STM32F429i开发板内带的Demo程序使用OpenOCD进行调试,读出来SDCR寄存器配置是0x29d4, SDTR寄存器配置是 0x00116361, 也就是设置了
CAS Latency=3
TRCD=1
TRP=2
TWR=2
TRC=7
TRAS=4
TXSR=7
TMRD=2

  除了配置好控制器的参数外,SDRAM上电之后的初始化过程需要进行预充电、自动刷新、配置模式寄存器的过程。在FMC 的SDCMR寄存器中有对应的命令可供操作。
init.PNG

我写的测试初始化程序片段如下
  1.     while(FMC_Bank5_6->SDSR & FMC_SDSR_BUSY); /* wait */
  2.     FMC_Bank5_6->SDCMR =FMC_SDCMR_CTB1|1;   /* enable clock */
  3.     delay_us(10000);