FPGA片内RAM读写测试实验#

实验Vivado工程为“ram_test”。

RAM是FPGA中常用的基础模块,可广泛用于缓存数据的情况,同样它也是ROM,FIFO的基础。本实验将为大家介绍如何使用FPGA内部的RAM以及程序对该RAM的数据读写操作。

实验原理#

Xilinx在VIVADO里为我们已经提供了RAM的IP核, 我们只需通过IP核例化一个RAM,根据RAM的读写时序来写入和读取RAM中存储的数据。实验中会通过VIVADO集成的在线逻辑分析仪ila,我们可以观察RAM的读写时序和从RAM中读取的数据。

创建Vivado工程#

在添加RAM IP之前先新建一个ram_test的工程, 然后在工程中添加RAM IP,方法如下:

  1. 点击下图中IP Catalog,在右侧弹出的界面中搜索ram,找到Block Memory Generator,双击打开。

../_images/image127.png
  1. 将Component Name改为ram_ip,在Basic栏目下,将Memory Type改为Simple Dual Prot RAM,也就是伪双口RAM。一般来讲”Simple Dual Port RAM”是最常用的,因为它是两个端口,输入和输出信号独立。

../_images/image219.png
  1. 切换到Port A Options栏目下,将RAM位宽Port A Width改为16,也就是数据宽度。将RAM深度Port A Depth改为512,深度指的是RAM里可以存放多少个数据。使能管脚Enable Port Type改为Always Enable。

image1

  1. 切换到Port B Options栏目下,将RAM位宽Port B Width改为16,使能管脚Enable Port Type改为Always Enable,当然也可以Use ENB Pin,相当于读使能信号。而Primitives Output Register取消勾选,其功能是在输出数据加上寄存器,可以有效改善时序,但读出的数据会落后地址两个周期。很多情况下,不使能这项功能,保持数据落后地址一个周期。

../_images/image414.png
  1. 在Other Options栏目中,这里不像ROM那样需要初始化RAM的数据,我们可以在程序中写入,所以配置默认即可,直接点击OK。

../_images/image513.png
  1. 点击“Generate”生成RAM IP。

../_images/image612.png

RAM的端口定义和时序#

Simple Dual Port RAM 模块端口的说明如下:

信号名称

方向

说明

clka

in

端口A时钟输入

wea

in

端口A使能

addra

in

端口A地址输入

dina

in

端口A数据输入

clkb

in

端口B时钟输入

addrb

in

端口B地址输入

doutb

out

端口B数据输输出

RAM的数据写入和读出都是按时钟的上升沿操作的,端口A数据写入的时候需要置高wea信号,同时提供地址和要写入的数据。下图为输入写入到RAM的时序图。

../_images/image712.png

RAM写时序

而端口B是不能写入数据的,只能从RAM中读出数据,只要提供地址就可以了,一般情况下可以在下一个周期采集到有效的数据。

../_images/image87.png

RAM读时序

测试程序编写#

下面进行RAM的测试程序的编写,由于测试RAM的功能,我们向RAM的端口A写入一串连续的数据,只写一次,并从端口B中读出,使用逻辑分析仪查看数据。代码如下

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
module ram_test(
                       input      sys_clk_p,            //system clock 200Mhz on board
            input      sys_clk_n,            //system clock 200Mhz on board
                       input rst_n                     //复位信号,低电平有效
               );

//-----------------------------------------------------------
reg            [8:0]           w_addr;                 //RAM PORTA写地址
reg            [15:0]          w_data;                 //RAM PORTA写数据
reg                    wea;            //RAM PORTA使能
reg            [8:0]           r_addr;                 //RAM PORTB读地址
wire   [15:0]          r_data;                 //RAM PORTB读数据


wire clk ;

   IBUFDS #(
      .DIFF_TERM("FALSE"),       // Differential Termination
      .IBUF_LOW_PWR("TRUE"),     // Low power="TRUE", Highest performance="FALSE"
      .IOSTANDARD("DEFAULT")     // Specify the input I/O standard
   ) IBUFDS_inst (
      .O(clk),  // Buffer output
      .I(sys_clk_p),  // Diff_p buffer input (connect directly to top-level port)
      .IB(sys_clk_n) // Diff_n buffer input (connect directly to top-level port)
   );

//产生RAM PORTB读地址
always @(posedge clk or negedge rst_n)
begin
  if(!rst_n)
       r_addr <= 9'd0;
  else if (|w_addr)                    //w_addr位或,不等于0
    r_addr <= r_addr+1'b1;
  else
       r_addr <= 9'd0;
end

//产生RAM PORTA写使能信号
always@(posedge clk or negedge rst_n)
begin
  if(!rst_n)
         wea <= 1'b0;
  else
  begin
     if(&w_addr)                       //w_addr的bit位全为1,共写入512个数据,写入完成
        wea <= 1'b0;
     else
        wea    <= 1'b1;        //ram写使能
  end
end

//产生RAM PORTA写入的地址及数据
always@(posedge clk or negedge rst_n)
begin
  if(!rst_n)
  begin
         w_addr <= 9'd0;
         w_data <= 16'd1;
  end
  else
  begin
     if(wea)                                   //ram写使能有效
        begin
               if (&w_addr)                    //w_addr的bit位全为1,共写入512个数据,写入完成
               begin
                       w_addr <= w_addr ;      //将地址和数据的值保持住,只写一次RAM
                       w_data <= w_data ;
               end
               else
               begin
                       w_addr <= w_addr + 1'b1;
                       w_data <= w_data + 1'b1;
               end
        end
  end
end

//-----------------------------------------------------------
//实例化RAM
ram_ip ram_ip_inst (
  .clka      (clk          ),     // input clka
  .wea       (wea          ),     // input [0 : 0] wea
  .addra     (w_addr       ),     // input [8 : 0] addra
  .dina      (w_data       ),     // input [15 : 0] dina
  .clkb      (clk          ),     // input clkb
  .addrb     (r_addr       ),     // input [8 : 0] addrb
  .doutb     (r_data       )      // output [15 : 0] doutb
);

//实例化ila逻辑分析仪
ila_0 ila_0_inst (
       .clk    (clk    ),
       .probe0 (r_data ),
       .probe1 (r_addr )
);


endmodule

为了能实时看到RAM中读取的数据值,我们这里添加了ila工具来观察RAM PORTB的数据信号和地址信号。关于如何生成ila大家请参考”PL的”Hello World”LED实验”。

../_images/image97.png

程序结构如下:

../_images/image107.png

绑定引脚

##################Compress Bitstream############################
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]

set_property PACKAGE_PIN C8 [get_ports sys_clk_p]
set_property IOSTANDARD DIFF_SSTL15 [get_ports sys_clk_p]

create_clock -period 5.000 -name sys_clk_p -waveform {0.000 2.500} [get_ports sys_clk_p]

set_property PACKAGE_PIN AF15 [get_ports rst_n]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]

仿真#

仿真方法参考”PL的”Hello World”LED实验”,仿真结果如下,从图中可以看出地址1写入的数据是0002,在下个周期,也就是时刻2,有效数据读出。

../_images/image1112.png

板上验证#

生成bitstream,并下载bit文件到FPGA。接下来我们通过ila来观察一下从RAM中读出的数据是否为我们初始化的数据。

在Waveform的窗口设置r_addr地址为0作为触发条件,我们可以看到r_addr在不断的从0累加到1ff, 随着r_addr的变化, r_data也在变化, r_data的数据正是我们写入到RAM中的512个数据,这里需要注意,r_addr出现新地址时,r_data对应的数据要延时两个时钟周期才会出现,数据比地址出现晚两个时钟周期,与仿真结果一致。

../_images/image128.png