跳转至

SRAM

在同学们常用的笔记本和台式机中,通常采用的是 DDR 内存,但 DDR 内存较为复杂,因此在课程中选择更为简单的 SRAM 作为实验系统中的内存。本文将会介绍 SRAM 的结构和它的接口。

简介

本文讲述了实验中采用的 SRAM 器件的结构,和如何通过接口来读写 SRAM 的内容。出于简化,这里只介绍一种较为保守的 SRAM 读写方式,性能更高的读写方式不在教学范围内。

SRAM 的结构

本实验中可以把 SRAM 想象成一个数组:uint32_t sram[1048576],即 1048576 个 32 位整数,一共是 32*1048576/8=4 MB 的数据。我们可以对其进行读和写的操作,就好像 C 的代码:

uint32_t sram[1048576];
// read
uint32_t read_data = sram[addr];
// write
sram[addr] = write_data;

由此,我们可以想象一下 SRAM 的工作流程,假如有一个二维的矩阵,一共有 1048576 个行,每行有 32 列,每一个矩阵元素就是 0 或者 1,这样每一行就是一个 32 位整数。

如果 CPU 要读取 SRAM,那么首先需要给出一个地址,比如地址 0x123,就要找到第 0x123=291 行的 32 位数据,把数据返回给 CPU。如果 CPU 要写入 SRAM,同样也是先给出一个地址,比如地址 0x321,就要找到第 0x321=801 行的 32 位数据,把新写入的数据填进去,修改原来的数据,完成写入。

由此可见,无论是读还是写,都有若干个步骤,这些步骤都需要花费一定的时间,这意味着我们在操作 SRAM 的时候,需要按照一定的规则来进行,如果不满足规则,就可能导致读取错误的数据,或者没有成功地写入等等问题。

SRAM 的信号

在做前面的实验的时候,你可能会注意到,顶层模块中有如下的信号:

//BaseRAM信号
inout wire[31:0] base_ram_data,  //BaseRAM数据
output wire[19:0] base_ram_addr, //BaseRAM地址
output wire[3:0] base_ram_be_n,  //BaseRAM字节使能,低有效。如果不使用字节使能,请保持为0
output wire base_ram_ce_n,       //BaseRAM片选,低有效
output wire base_ram_oe_n,       //BaseRAM读使能,低有效
output wire base_ram_we_n,       //BaseRAM写使能,低有效

这就是我们 CPU 访问 SRAM 的途径。实验板上一共有两组 SRAM,我们称之为 BaseRAM 和 ExtRAM,上面的信号就是和 BaseRAM 进行连接的信号。

接下来我们来逐一看上面的信号的含义:

  1. inout wire [31:0] base_ram_data:读和写的 32 位数据,需要注意用的是同一组信号,意味着同一时间只能进行读或者写其中一个
  2. output wire [19:0] base_ram_addr:地址线,通过计算可以发现 2^20=1048576,正好是我们上面提到的 uint32_t sram[1048576] 的数组大小
  3. output wire [3:0] base_ram_be_n:字节使能,目的是实现部分写入,例如只想写入四个字节其中一个,就把相应位设置为 0
  4. output wire base_ram_ce_n:片选使能,使用 SRAM 时,需要保证 base_ram_ce_n=0;如果 base_ram_ce_n=1,就进入省电模式
  5. output wire base_ram_oe_n:输出使能,对于读操作,需要保证 base_ram_oe_n=0,此时 base_ram_data 由 SRAM 输出;对于写操作,需要保证 base_ram_oe_n=1,此时 base_ram_data 由 FPGA 输出
  6. output wire base_ram_we_n:写入使能,读操作对应 base_ram_we_n=1,写操作对应 base_ram_we_n=0

如果不考虑 SRAM 操作所需要的时间,一个大概的操作思路如下:

读操作:

  1. 设置 base_ram_addr 为要读取的地址,设置 base_ram_be_n=0b0000, base_ram_ce_n=0, base_ram_oe_n=0, base_ram_we_n=1
  2. 等待读取完毕,在 base_ram_data 上得到读取的数据

写操作:

  1. 设置 base_ram_addr 为要写入的地址,设置 base_ram_ce_n=0, base_ram_oe_n=1, base_ram_we_n=0,根据要写入的字节数量设置 base_ram_be_n
  2. 等待写入完毕

SRAM 的时序

前面已经暗示,SRAM 实际上读写需要经过几个步骤,这意味着我们不能简单地像上面那样,直接给出信号,然后就完成读和写的操作。

接下来我们来分析一下 SRAM 读写需要的具体步骤,和相应的波形。

读时序

前面我们已经提到,SRAM 读取的时候,首先需要根据行号找到相应的行,再把一行的数据输出到 base_ram_data 上。这一步需要大约一个周期的时间,这意味着我们需要等待一个周期,在第二个周期才可以得到读取的数据:

clockaddr0x010x020x030x01dataABCAce_noe_nwe_nbe_n0b0000

观察上面的波形:

  1. 每次读取都需要等待一个周期。等待的时候,地址保持不变
  2. 读操作需要保持 ce_n=0, oe_n=0, we_n=1
  3. 四个字节都读取,于是设置 be_n=0b0000
  4. 不需要读取的时候,设置 ce_n=1

写时序

SRAM 写入的过程则较为复杂。考虑到可能会只写入部分字节(例如 be_n=0b1100),SRAM 内部操作需要如下三个步骤:

  1. 根据 addr 找到对应的行,把一行的数据读取出来
  2. 把读取的数据和写入的数据,根据 be_n 计算出新的数据,例如原来保存的数据是 0x12345678,新写入的数据是 0x87654321,如果 be_n=0b1100,则新的数据是 0x12344321,即只写入两个字节
  3. 把新的数据写入到行中

这三个步骤,我们都用一个周期的时间来完成:

clockaddr0x010x020x03dataABCce_noe_nwe_nbe_n0b0000

观察上面的波形:

  1. 每次写入都需要三个周期,这三个周期内,地址和数据保持不变
  2. we_n 三个周期取值分别是 1, 0, 1
  3. 写操作需要保持 ce_n=0, oe_n=1
  4. 四个字节都写入,于是设置 be_n=0b0000,可以根据实际需要设置
  5. 不需要写入的时候,设置 ce_n=1

注:这里的先读后写是 SRAM 内部一种可能的实现方式,主要是方便理解 SRAM 做了什么事情。另一种可能的实现方法是,SRAM 内部给四个字节分别设置一个写使能信号,通过 be_nwe_n 计算出每个字节的写使能信号,这样避免了先读后写的过程。无论 SRAM 内部如何实现,从外部来看就是一次设置了 byte enable 的写入。

扩展

同学看到上面的两周期读,三周期写的波形,可能想到有更优的实现方法。确实是有的,但是在本实验中,只需要按照上面的方式实现。

助教,我学有余力,可以告诉我怎么找到更好的实现方法吗?

注意

这个方法的效果可能远没有编写一个简单的 cache 要好。而且在实现时可能会遇到涉及时序,异步电路等知识的问题。如果你对自己的数字电路知识没有自信,请不要轻易尝试该优化方案。

请参考 https://jia.je/hardware/2022/05/19/async-sram-timing/。如果同学实现了性能更好的方法,也请不要给其他同学压力,如果不能做到反内卷,那就请安静地内卷。

助教,你上面写的 SRAM 读写时序说的不对,读取不需要一个周期的时间?

实际上,实验中实际采用的 SRAM 型号的读取延迟大概是 10ns,考虑到同学们写的设计一般不会达到 100MHz 那么高,所以这里就简化了一下,方便理解。

对于 SRAM 在硬件上是如何实现的,推荐阅读 What Every Programmer Should Know About Memory 的相关部分。


最后更新: 2024年10月7日
作者:Jiajie Chen (95.24%), cuibst (2.72%), Kang Chen (2.04%)