存储器实验
实验概述
存储器是计算机中一个必不可少的部件,用来存储程序和数据。实际应用中对存储容量的要求一般比较大,通常会采用若干存储芯片组成所需要的存储器;在本课程中后面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的三端口寄存器堆。