$$ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Self-defined math definitions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Math symbol commands \newcommand{\intd}{\,{\rm d}} % Symbol 'd' used in integration, such as 'dx' \newcommand{\diff}{{\rm d}} % Symbol 'd' used in differentiation ... $$

【FPGA学习】- FPGA实验篇(存储器)

  在XILINX的FPGA上想要实现一个存储器,一般有两种方式:第一种是自己使用FPGA的逻辑资源自己设计;第二种是使用XILINX专用的Block Memory Generator(BMG)。

  针对BRAM的资源形式,XILINX提供了两种接口类型:Native和AXI4。这两种核的主要特点如下表。

  本次的实验主要关注Native接口,所以关于AXI4接口的一些特性会在之后记录。Native端口的内存类型共有五种,区别如下表。

   使用XILINX的BMG生成ROM/RAM时,提供了三种生成算法。Minimum Area Algorithm: 使用最少数量的原语生成一个核心;Low Power Algorithm: 在读或写操作期间启用最小数量的块RAM原语的方式生成内核;Fixed Primitive Algorithm: 生成连接单个基本单元类型以实现内存的核心。

  在进行端口配置时,需要配置宽度(Width)和深度(Depth),宽度指的是存入或读出数据的位数,深度指的是存入或读出数据的个数,也称为数据寻址的范围。        

  使能端端口类型有两种:USE ENA pin和Always Enabled。如果有使能端的引脚,则选用前者,没有使能端引脚,则使用后者。

   Primitives Output Register: 选择分别在端口A和端口B的内存原语之后插入输出寄存器。在独立模式下,原语输出寄存器选项被设置为默认选项。该选项的功能是在输出数据加上寄存器,可以有效改善时序,但读出的数据会落后地址两个周期。很多情况下,不使用这项功能,保持数据落后地址一个周期。

  关于其它更多的设置可以参照XILINX的官方文档《Block Memory Generator v8.4  LogiCORE IP Product Guide

IP核之RAM实验

  创建Vivado工程,在IP Catalog中选择Block Memory Generator。其中将其命名为ram_ip,内存类型选择为伪双端口 RAM。

 

  切换到Port A Options下,将RAM位宽Port A Width(数据宽度) 改为16。将RAM深度Port A Depth改为512,深度指的是RAM里可以存放多少个数据。使能管脚Enable Port Type改为Always Enable。

  切换到 Port B Options下,将RAM位宽Port B Width改为16,使能管脚Enable Port Type改为Always Enable。Primitives Output Register 取消勾选。

  点击OK生成RAM IP。

  RAM 测试程序编写。

module ram_test(
    input clk,    //50MHZ时钟
    input rst_n   //复位信号,低电平有效
    );
//-----------------------------------
    reg [8:0] w_addr;  //RAM A端口写地址
    reg [15:0] w_data; //RAM A端口写数据
    reg        wea;   //RAM A端口使能信号
    reg [8:0] r_addr;  //RAM B端口读地址
    wire [15:0] r_data;  //RAM B端口读数据
//产生RAM portB的读地址
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            r_addr <= 9'b0;
        else if(|w_addr)  //读地址位或,不为0
            r_addr <= r_addr + 1'b1;
        else
            r_addr <= 9'b0;
    end
//产生RAM portA写使能信号
    always @(posedge clk, negedge rst_n) begin
        if(!rst_n)
            wea <= 1'b0;
        else 
        begin
            if(&w_addr)  //全为1,表明已经被写满
                wea <= 1'b0;
            else
                wea <= 1'b1;
        end
    end
//产生RAM A端口写入的地址以及数据
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),
    .wea (wea),
    .addra (w_addr),
    .dina (w_data),
    .clkb (clk),
    .addrb (r_addr),
    .doutb (r_data)
);
//实例化逻辑分析仪
ila ila_inst(
    .clk (clk),
    .probe0 (r_data),
    .probe1 (r_addr)
);
endmodule

   引脚约束和时序约束。在RTL ANALYSIS中的Open Elaborted Design中,在右上角选择I/O Planning,将时钟信号约束到管脚U18,将复位信号约束到管脚N15,并将标准均选择为LVCMOS33。在SYNTHESIS中打开Constrains Wizard,将系统频率设置为50MHZ。

  板上验证。使用在线逻辑分析仪ila,生成 bitstream,并下载bit文件到FPGA。当r_addr地址为0时,可以看到r_addr在不断的从0累加到1ff,,随着r_addr 的变化,r_data也在变化,r_addr出现新地址时,r_data对应的数据要延时一个时钟周期才会出现,数据比地址出现晚一个时钟周期。

 

 

IP核之ROM实验

  ROM和RAM的创建过程类似,将模块名命名为rom_ip,内存类型选择为单端口ROM。

  将A端口的宽度和深度设置为8和32,使能端口设置为Always Enabled,取消勾选Primitives Output Register。

 

  向ROM当中加载数据,以便ROM读取可以成功,便于测试。加载数据链接

  点击OK,生成ROM IP。

  ROM测试程序编写。

module rom(
    input sys_clk,
    input rst_n
    );
    wire [7:0] rom_data;
    reg [4:0] rom_addr;
    
//产生ROM地址读取数据
    always @(posedge sys_clk, negedge rst_n) begin
        if(!rst_n)
            rom_addr <= 10'd0;
        else
            rom_addr <= rom_addr + 1'b1;
    end
//实例化ROM
rom_ip rom_ip_inst (
  .clka(sys_clk),   
  .addra(rom_addr),  // input wire [4 : 0] addra
  .douta(rom_data)  // output wire [7 : 0] douta
);
//实例化逻辑分析仪
ila your_instance_name (
    .clk(sys_clk), // input wire clk

    .probe0(rom_addr), // input wire [4:0]  probe0  
    .probe1(rom_data) // input wire [7:0]  probe1
);
endmodule

  引脚约束和时序约束同RAM。板上验证。以地址 0 为触发条件,可以看到读取的数据滞后于地址一个周期。

 

 

IP核之FIFO实验

   XILINX的FIFO IP核有三种接口类型:Native:实现本地的FIFO;AXI Memory Mapped:以FWFT的模式实现AXI4,AXI3,AXI4-lite标准的FIFO;AXI Stream:以FWFT的模式实现AXI4-stream的FIFO。

  FIFO Implementation共有七种:同步时钟和异步时钟;块RAM和分布式RAM等。

   创建Vivado工程,在IP Catalog中选择FIFO Generator。其中将其命名为fifo_ip,fifo implementation选择为“Independent Clocks Block RAM(异步块RAM)。

  切换到Native Ports下,选择数据位宽16;FIFO 深度选择512,Read Mode选择Standard FIFO。

  切换到Data Counts下,勾选使能Write Data Count(已经 FIFO 写入多少数据)和Read Data Count(FIFO 中有多少数据可以读),这样我们可以通过这两个值来看 FIFO 内部的数据多少。

  

  点击 OK,生成FIFO IP。

  FIFO测试程序编写。

module fifo(
    input clk,
    input rst_n
    );
    wire clk_100m;      //PLL产生100MHZ时钟
    wire clk_75m;       //PLL产生75MHZ时钟
    wire locked;        //判断输入和输出是否锁定
    
    wire fifo_rst_n;    //fifo复位信号
    wire wr_clk;        //写时钟信号
    wire rd_clk;        //读时钟信号
    reg [7:0] wcnt;     //写后等待计数器
    reg [7:0] rcnt;     //读后等待计数器
    
    reg [15:0] w_data;  //写入数据
    wire wr_en;         //写使能
    wire [15:0] r_data;  //读出数据
    wire rd_en;         //读使能
    wire full;          //满信号
    wire empty;         //空信号
    wire [8:0] rd_data_count;   //可读数据量
    wire [8:0] wr_data_count;   //写入数据量
 //实例化PLL,产生100MHZ和75MHZ时钟   
    clk_wiz_0 clk_inst
       (
        // Clock out ports
        .clk_out1(clk_100m),     // output clk_out1
        .clk_out2(clk_75m),     // output clk_out2
        // Status and control signals
        .reset(~rst_n), // input reset
        .locked(locked),       // output locked
       // Clock in ports
        .clk_in1(clk));      // input clk_in1
        
        assign fifo_rst_n = locked;
        assign wr_clk = clk_100m;
        assign rd_clk  = clk_75m;
        
        parameter W_IDLE  = 1,
                    W_FIFO = 2;
        reg [2:0] write_state;
        reg [2:0] next_write_state;
        
        always @(posedge wr_clk, negedge fifo_rst_n) begin
            if(!fifo_rst_n)
                write_state <= W_IDLE;
            else
                write_state <= next_write_state;
        end
        
        always @(*) begin
            case(write_state)
            W_IDLE:begin
                if(wcnt == 8'd79)
                    next_write_state <= W_FIFO;
                else
                    next_write_state <= W_IDLE;
            end
            W_FIFO:begin
                next_write_state <= W_FIFO;
            end
            default:begin
                next_write_state <= W_IDLE;
            end
            endcase
        end
        
    always@(posedge wr_clk or negedge fifo_rst_n) begin
            if(!fifo_rst_n)
                wcnt <= 8'd0;
            else if (write_state == W_IDLE)
                wcnt <= wcnt + 1'b1 ;
            else
                wcnt <= 8'd0;
     end
 
    assign wr_en = (write_state == W_FIFO) ? ~full : 1'b0;
    
    always@(posedge wr_clk or negedge fifo_rst_n) begin
        if(!fifo_rst_n)
            w_data <= 16'd1;
        else if (wr_en)
            w_data <= w_data + 1'b1;
    end
    
    parameter R_IDLE  = 1,
                R_FIFO = 2;
    reg [2:0] read_state;
    reg [2:0] next_read_state;
    
    ///产生FIFO读的数据
    always@(posedge rd_clk or negedge fifo_rst_n) begin
        if(!fifo_rst_n)
            read_state <= R_IDLE;
        else
            read_state <= next_read_state;
    end
    
    always@(*) begin
        case(read_state)
        R_IDLE:begin
            if (rcnt == 8'd59)
                next_read_state <= R_FIFO;
            else
                next_read_state <= R_IDLE;
        end
        R_FIFO:
            next_read_state <= R_FIFO ; //一直在读FIFO状态
        default:
            next_read_state <= R_IDLE;
        endcase
    end
    
    //在IDLE状态下,也就是复位之后,计数器计数
    always@(posedge rd_clk or negedge fifo_rst_n) begin
        if(!fifo_rst_n)
            rcnt <= 8'd0;
        else if (write_state == W_IDLE)
            rcnt <= rcnt + 1'b1 ;
    else
            rcnt <= 8'd0;
    end
    //在读FIFO状态下,如果不空就从FIFO中读数据
    assign rd_en = (read_state == R_FIFO) ? ~empty : 1'b0;
    
    //实例化FIFO
    fifo_ip fifo_ip_inst 
    (
     .rst (~fifo_rst_n ), // input rst
     .wr_clk (wr_clk ), // input wr_clk
     .rd_clk (rd_clk ), // input rd_clk
     .din (w_data ), // input [15 : 0] din
     .wr_en (wr_en ), // input wr_en
     .rd_en (rd_en ), // input rd_en
     .dout (r_data ), // output [15 : 0] dout
     .full (full ), // output full
     .empty (empty ), // output empty
     .rd_data_count (rd_data_count), // output [8 : 0] rd_data_count
     .wr_data_count (wr_data_count) // output [8 : 0] wr_data_count
    );
    //写通道逻辑分析仪
    ila ila_wfifo (
        .clk(wr_clk),
        .probe0(w_data),
        .probe1(wr_en),
        .probe2(full),
        .probe3(wr_data_count)
    );
    //读通道逻辑分析仪
    ila ila_rfifo (
        .clk(rd_clk),
        .probe0(r_data),
        .probe1(rd_en),
        .probe2(empty),
        .probe3(rd_data_count)
    );
endmodule

  引脚约束和时序约束同RAM。板上验证。写通道中,当full信号为高电平时,wr_en为低电平,不再向里面写数据。在不空后,开始读数据,读出的数据相对于rd_en滞后一个周期。

 

 

补充:

ECC:Error Correction Capability,伪双端口RAM存储器支持内置FPGA汉明纠错功能,可自动检测单比特和双比特错误,并自动纠正单比特错误。

FWFT:First-Word-Fall-Through。FIFO Generator核心支持两种读选项模式,标准读操作和首字直通(FWFT)读操作。标准读取操作在被请求后的周期内提供用户数据。FWFT读取操作在被请求的同一周期内提供用户数据。

built-in FIFO:内置FIFO。

 

参考资料

[1] 黑金FPGA开发教程

[2] 正点原子FPGA开发教程

[3] XILINX官方开发文档

 

posted @ 2023-03-13 09:43  素衣叹风尘  阅读(818)  评论(0编辑  收藏  举报