存储器实验
实验概述
存储器是计算机中一个必不可少的部件,用来存储程序和数据。实际应用中对存储容量的要求一般比较大,通常会采用若干存储芯片组成所需要的存储器;在本课程中后面CPU设计实验中,只需要小容量的指令存储器和数据存储器配合验证CPU的设计,所以本实验学习使用FPGA内部资源来构成小容量的存储器。
参考设计
FPGA内部资源可以分为逻辑资源和专用存储资源两类,都可以用来构成小容量存储器,但特性有所不同。专用存储资源只能生成同步存储器,逻辑资源可以生成同步写、异步读的存储器,也可以生成读写均异步的存储器。详细内容请看辅导视频。
FPGA设计工具如Quartus和Vivado都提供了交互式工具生成片内存储器IP核,但是可移植性不好;本实验采用HDL描述产生不同特性的片内存储器。例 1和例 2是两种RAM的参考设计代码,例 3是ROM的参考设计,可结合辅导视频学习。更多有关Intel FPGA中存储器的描述方法,请参考《Intel® Quartus® Prime用户指南》中的设计建议:从HDL代码推断存储器功能。
module RAM #( parameter ADDRWIDTH = 6, parameter DATAWIDTH = 32) ( input wire iClk, iWR, input wire [ADDRWIDTH-1:0] iAddress, input wire [DATAWIDTH-1:0] iWriteData, output logic [DATAWIDTH-1:0] oReadData ); localparam MEMDEPTH = 1<<ADDRWIDTH; //存储器的字数 logic [DATAWIDTH-1:0] mem[0:MEMDEPTH-1]; always_ff @(posedge iClk) begin if (iWR) mem[iAddress] <= iWriteData; end assign oReadData = mem[iAddress]; // 读地址未锁存,编译器使用FPGA的逻辑资源生成存储器 /* initial 为了调试方便可给存储器赋初值,调试成功后将其删除。 $readmemh("init_data.txt",mem); // 存储器内容定义在文件中。 */ endmodule
module RAM #( parameter ADDRWIDTH = 6, parameter DATAWIDTH = 32) ( input wire iClk, iWR, input wire [ADDRWIDTH-1:0] iAddress, input wire [DATAWIDTH-1:0] iWriteData, output logic [DATAWIDTH-1:0] oReadData ); localparam MEMDEPTH = 1<<ADDRWIDTH; //存储器的字数 logic [DATAWIDTH-1:0] mem[0:MEMDEPTH-1]; logic [ADDRWIDTH-1:0] read_addr; always_ff @(posedge iClk) begin read_addr <= iAddress; //读地址锁存,编译器使用FPGA的RAM块生成存储器 if (iWR) mem[iAddress] <= iWriteData; end assign oReadData = mem[read_addr]; /* initial 为了调试方便可给存储器赋初值,调试成功后将其删除。 $readmemh("init_data.txt",mem); // 存储器内容定义在文件中。 */ endmodule
对比例 1和例 2的代码,同步读RAM的读地址也经过了时钟的同步,也就是读地址被锁存了,因此比异步读的RAM在读出时慢了一个时钟周期。但是对于FPGA来说,同步读的RAM可以使用FPGA内部专用的RAM块资源,异步读的RAM则需要占用FPGA的逻辑资源。
有关两种RAM的特性和比较,可观看慕课视频。 |
module ROM #( parameter ADDRWIDTH = 4, parameter DATAWIDTH = 8) ( input wire [ADDRWIDTH-1:0] iAddress, output logic [DATAWIDTH-1:0] oData ); localparam MEMDEPTH = 1<<ADDRWIDTH; logic [DATAWIDTH-1:0] mem[0:MEMDEPTH-1]; assign oData = mem[iAddress]; initial begin (1) $readmemb("init_mem.txt",mem); (2) // 如果文件中的数据是十六进制,用 $readmemh // 也可采用如下赋值语句进行初始化 // mem[n'h00] = n'...; (3) // ...... end endmodule
比较RAM和ROM的端口可以发现,ROM作为只读存储器,没有写数据和写入控制端口,甚至也没有时钟端口;
就像是软件中的“数组”,存储单元的地址是数组的索引,存储单元的内容则是数组元素的值。
ROM的内容是事先写入的,对于FPGA来说,需要在设计时确定存储器的内容,称为初始化,编译时将初始值固定在电路中。在verilog中,用 initial
块初始化存储器的内容,如例 3 ①所示。
具体有两种方法,一种是用 $readmemb
或 $readmemh
系统函数,如例 3 ②所示;另一种是用赋值语句,如例 3 ③所示。
系统函数从一个文本文件读取初始化数据,例 4是初始化文件的一个示例。
@00 11000000 @01 @02 @03 @04 @05 @06 @07 @08 @09 @0A @0B @0C @0D @0E @0F
在 init_mem.txt 文件中,紧跟在 @
后面的是地址,然后是至少一个空白字符,再给出该地址单元的值。例 4只有第一行给出了存储单元的数值,下面的各行需要学员自己完成。
如果使用 $readmemb
函数,编译器按二进制解释给出的存储单元的数值;如果使用 $readmemh
函数,则应以十六进制给出存储单元的数值。但无论哪个函数,地址始终是十六进制表示。
初始化文件中可以只初始化需要的单元,地址可以是不连续的。
如果某一行的地址与上一行是连续的,则该行的地址也可以省略不写。
如果使用远程实验平台的云编译功能,只能采用赋值语句的方法。目前远程实验平台未提供上传初始化文件的功能。 |
实验任务
- 1.用ROM实现七段译码器
-
设计一个16×8的只读存储器,存储单元中固化十六个字符0~F的数码管段码;用存储器的4位地址作为七段译码器的数据输入,相应存储单元的8位数据作为七段译码器的输出。 例 4第一行给出了字符“0”的编码,其余字符编码需要自己编写。
- 2.用RAM实现寄存器堆
-
在前面的寄存器堆实验中,只包含4个寄存器,用多路器选择其中一个输出;若寄存器的数量比较多,不仅描述冗长,硬件资源消耗也较多。本实验用存储器的方法设计一组32×32的三端口寄存器堆,用在以后的CPU设计中。 具体要求如下。
(1)采用异步读的RAM设计。
(2)R0寄存器的读出值始终为零。
(3)写成单独的模块,并且采用参数化的设计方法,模块端口如下。
module RegisterFile #( parameter DATAWIDTH = 32, parameter ADDRWIDTH = 5) ( input wire iClk, input wire iWE, input wire [ADDRWIDTH-1:0] iWA, iRA1, iRA2, input wire [DATAWIDTH-1:0] iWD, output logic [DATAWIDTH-1:0] oRD1, oRD2 );
- 3.编写VirtualBoard模块
-
按照图 1所示虚拟面板编写VirtualBoard模块。
(1)实例化用RAM实现的寄存器堆模块。
(2)实例化用ROM实现的七段译码器模块,并连接到数码管。
(3)连接虚拟元件
由于虚拟元件数量有限,只连接2位地址开关和4位数据开关,未连接到开关的剩余3位地址填充为0,或者实例化时通过传递参数实例化为4×4的三端口寄存器堆。