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 进行连接的信号。
接下来我们来逐一看上面的信号的含义:
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
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 的相关部分。