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写使能,低有效
这些信号是 SRAM 芯片向外提供的接口。访问 SRAM 的电路需要按读写时序驱动这些信号,SRAM 则通过数据线返回读取结果。
实验板上一共有两组 SRAM,我们称之为 BaseRAM 和 ExtRAM,上面的信号就是和 BaseRAM 进行连接的信号。
接下来我们来逐一看上面的信号的含义:
inout wire [31:0] base_ram_data:读和写的 32 位数据,需要注意用的是同一组信号,意味着同一时间只能进行读或者写其中一个操作output wire [19:0] base_ram_addr:地址线,通过计算可以发现2^20=1048576,正好是我们上面提到的uint32_t sram[1048576]的数组大小-
output wire [3:0] base_ram_be_n:字节使能,目的是实现部分写入,例如只想写入四个字节其中一个,就把相应位设置为0注意
你可能会奇怪为什么需要字节使能信号。这是因为 SRAM 的地址的单位是 4 字节,这意味着每当
base_ram_addr的值增加 1,实际上跳过了 4 个字节(也就正好对应了上面提到的uint32_t sram[1048576]的数组元素大小),而非 1 个字节。如果想对这 4 个字节中的一部分进行修改,其他字节保持不变,就需要使用字节使能来控制了。 -
output wire base_ram_ce_n:片选使能,使用 SRAM 时,需要保证base_ram_ce_n=0;如果base_ram_ce_n=1,就进入省电模式 output wire base_ram_oe_n:输出使能,对于读操作,需要保证base_ram_oe_n=0,此时base_ram_data由 SRAM 输出;对于写操作,需要保证base_ram_oe_n=1,此时base_ram_data由 FPGA 输出output wire base_ram_we_n:写入使能,读操作对应base_ram_we_n=1,写操作对应base_ram_we_n=0
如果不考虑 SRAM 操作所需要的时间,一个大概的操作思路如下:
读操作:
- 设置
base_ram_addr为要读取的地址,设置base_ram_be_n=0b0000, base_ram_ce_n=0, base_ram_oe_n=0, base_ram_we_n=1 - 等待读取完毕,在
base_ram_data上得到读取的数据
写操作:
- 设置
base_ram_addr为要写入的地址,设置base_ram_ce_n=0, base_ram_oe_n=1, base_ram_we_n=0,根据要写入的字节数量设置base_ram_be_n - 等待写入完毕
SRAM 的时序
前面已经暗示,SRAM 实际上读写需要经过几个步骤,这意味着我们不能简单地像上面那样,直接给出信号,然后就完成读和写的操作。
接下来我们来分析一下 SRAM 读写需要的具体步骤,和相应的波形。
读时序
前面我们已经提到,SRAM 读取的时候,首先需要根据行号找到相应的行,再把一行的数据输出到 base_ram_data 上。这一步需要大约一个周期的时间,这意味着我们需要等待一个周期,在第二个周期才可以得到读取的数据:
观察上面的波形:
- 每次读取都需要等待一个周期。等待的时候,地址保持不变
- 读操作需要保持
ce_n=0, oe_n=0, we_n=1 - 四个字节都读取,于是设置
be_n=0b0000 - 不需要读取的时候,设置
ce_n=1
写时序
SRAM 写入的过程则较为复杂。考虑到可能会只写入部分字节(例如 be_n=0b1100),SRAM 内部操作需要如下三个步骤:
- 根据
addr找到对应的行,把一行的数据读取出来 - 把读取的数据和写入的数据,根据
be_n计算出新的数据,例如原来保存的数据是0x12345678,新写入的数据是0x87654321,如果be_n=0b1100,则新的数据是0x12344321,即只写入两个字节 - 把新的数据写入到行中
这三个步骤,我们都用一个周期的时间来完成:
观察上面的波形:
- 每次写入都需要三个周期,这三个周期内,地址和数据保持不变
we_n三个周期取值分别是1, 0, 1- 写操作需要保持
ce_n=0, oe_n=1 - 四个字节都写入,于是设置
be_n=0b0000,可以根据实际需要设置 - 不需要写入的时候,设置
ce_n=1
注:这里的先读后写是 SRAM 内部一种可能的实现方式,主要是方便理解 SRAM 做了什么事情。另一种可能的实现方法是,SRAM 内部给四个字节分别设置一个写使能信号,通过 be_n 和 we_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 的相关部分。
总结
本节介绍了 SRAM 的接口信号,以及本实验采用的读写时序。
访问 SRAM 芯片的电路需要按时序设置地址、数据和控制信号,并在读操作时接收数据。在本实验中,这些工作由 FPGA 中的 SRAM 控制器完成。后两节将介绍控制器如何接收并处理上层请求。
flowchart LR
controller["SRAM 控制器"] -- 地址、写数据和控制信号 --> sram["SRAM 芯片"];
sram -- 读数据 --> controller;
学完这节内容我需要做什么?
理解 SRAM 的读写时序和各控制信号的作用,为实现 SRAM 控制器做准备。