【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官方开发文档