存储器实验

实验目的

1.理解存储器的特性;

2.掌握存储器的HDL描述方法;

3.了解FPGA的存储资源及使用方法。

实验概述

存储器是计算机中一个必不可少的部件,用来存储程序和数据。实际应用中对存储容量的要求一般比较大,通常会采用若干存储芯片组成所需要的存储器;在本课程中后面CPU设计实验中,只需要小容量的指令存储器和数据存储器配合验证CPU的设计,所以本实验学习使用FPGA内部资源来构成小容量的存储器。

参考设计

FPGA内部资源可以分为逻辑资源和专用存储资源两类,都可以用来构成小容量存储器,但特性有所不同。专用存储资源只能生成同步存储器,逻辑资源可以生成同步写、异步读的存储器,也可以生成读写均异步的存储器。详细内容请看辅导视频。

FPGA设计工具如Quartus和Vivado都提供了交互式工具生成片内存储器IP核,但是可移植性不好;本实验采用HDL描述产生不同特性的片内存储器。例 1例 2是两种RAM的参考设计代码,例 3是ROM的参考设计,可结合辅导视频学习。更多有关Intel FPGA中存储器的描述方法,请参考《Intel® Quartus® Prime用户指南》中的设计建议:从HDL代码推断存储器功能

例 1. 同步写、异步读的RAM
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
例 2. 同步写、同步读的RAM
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的特性和比较,可观看慕课视频。
例 3. ROM
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是初始化文件的一个示例。

例 4. init_mem.txt文件
@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模块。

image
图 1. 实验任务的虚拟面板

(1)实例化用RAM实现的寄存器堆模块。

(2)实例化用ROM实现的七段译码器模块,并连接到数码管。

(3)连接虚拟元件

由于虚拟元件数量有限,只连接2位地址开关和4位数据开关,未连接到开关的剩余3位地址填充为0,或者实例化时通过传递参数实例化为4×4的三端口寄存器堆。