在实际的FPGA开发项目中,可能会用到一些常见的IP 核, 例如 PLL,MMCM, 双端口RAM, FIFO等等。一般来说,可以使用Xilinx ISE, Vivado , Altera Quartus 等IDE 集成软件, 通过创建厂家给出的一些IP 核 来完成工作中一些需求。如果Xilinx, Altera 等厂家没有提供需要的IP 核 , 或者是他们提供的IP核 不能满足用户的特定需求,这时工程师就需要自主创建一些IP 核,来满足 客户工程的需要了。
FPGA厂家一般会提供标准的FIFO IP 核的, 无论是Xilinx, Altera 还是其他公司。 但有时编写的Verilog 代码 不知道会在什么样的FPGA使用, 有可能是xilinx, 也有可能是Altera ,或者是其他公司的FPGA。本文介绍一种FIFO IP 核的创建方法,因为 Verilog 语言是通用的, 所以通过Verilog 语言自建的IP 的适用范围会更加宽广,即使是准备生产芯片(ASIC 等,不再使用FPGA芯片),Verilog 也是非常方便被导入到生产环节的(不必一定需要工厂提供相对应的IP了)。
这种自建的FIFO IP 会在很多Verilog 工程中被应用,这样用户发布的代码往往不需要修改, 就可以在不同公司的平台上使用。
本文中自建的IP 是一个同步时钟的FIFO, 并且FIFO写入数据的宽度和数据读出的宽度相等。
参考代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
`timescale 1ns / 1ps // first word for through: rd ,next clock get a new data module syn_fifo # ( parameter ADDR_WIDTH = 3, parameter DATA_WIDTH = 8 ) ( input clk, input wr_en, input [DATA_WIDTH - 1:0] din, output full, input rd_en, output [DATA_WIDTH - 1:0] dout, output empty, output [ADDR_WIDTH:0] data_count, output valid, input srst ); localparam FIFO_DEPTH = 2**ADDR_WIDTH; reg [DATA_WIDTH - 1:0] ram [0: FIFO_DEPTH - 1]; reg [ADDR_WIDTH - 1:0] rd_addr = 0; assign dout = ram[rd_addr]; reg [ADDR_WIDTH - 1:0] wr_addr = 0; wire ptr_match = (wr_addr == rd_addr) ? 1'b1 : 1'b0; reg maybe_full = 0; assign empty = ptr_match & (!maybe_full); assign full = ptr_match & maybe_full; wire wr_vld = (!full) & wr_en; wire rd_vld = (!empty) & rd_en; wire [ADDR_WIDTH:0] wr_sub_rd = wr_addr - rd_addr; wire [ADDR_WIDTH - 1:0] ptr_diff = wr_sub_rd[ADDR_WIDTH - 1:0]; wire carry_bit = maybe_full & ptr_match; assign data_count = {carry_bit,ptr_diff}; always @(posedge clk or posedge srst) if (srst) wr_addr <= 0; else if (wr_vld) wr_addr <= wr_addr + 1; wire [DATA_WIDTH - 1:0] din_w = din; always @(posedge clk or posedge srst) if(srst) ram[0] <= 0; else if(wr_vld) ram[wr_addr] <= din_w; always @(posedge clk or posedge srst) if (srst) rd_addr <= 0; else if (rd_vld) rd_addr <= rd_addr + 1; always @(posedge clk or posedge srst) if (srst) maybe_full <= 0; else if (wr_vld != rd_vld) maybe_full <= wr_vld; assign valid = rd_en & (!empty); endmodule |
接口介绍:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
module syn_fifo # ( parameter ADDR_WIDTH = 3, // FIFO地址线 宽度 parameter DATA_WIDTH = 8 // FIFO数据线 宽度 ) ( // 同步时钟fifo 的输入时钟,由于是同步fifo ,所以写入和读出使用相同的时钟 input <span style="color: #0000ff;">clk</span>, input <span style="color: #0000ff;">wr_en</span>, // FIFO写 信号 input [DATA_WIDTH - 1:0] <span style="color: #0000ff;">din</span>, // FIFO写 数据 output <span style="color: #0000ff;">full</span>, // FIFO满 标志 input <span style="color: #0000ff;">rd_en</span>, // FIFO读 信号 output [DATA_WIDTH - 1:0] <span style="color: #0000ff;">dout</span>, // FIFO读 数据 output <span style="color: #0000ff;">empty</span>, // FIFO空 标志 output [ADDR_WIDTH:0] <span style="color: #0000ff;">data_count</span>, // FIFO里当前还有多少数据 output <span style="color: #0000ff;">valid</span>, // 读出数据有效信号,和dout 是 同步输出的 input <span style="color: #0000ff;">srst <span style="color: #000000;">// FIFO复位信号</span> <span style="color: #000000;">);</span></span> |
由于使用了参数化设计, 所以这个FIFO IP 存储深度是可以被用户配置的。 同时写入和读出的数据宽度, 用户也是可以配置的。
这里的代码讲解都是以 ADDR_WIDTH = 3, DATA_WIDTH = 8 进行讲解的。其他情况,对代码的理解也是相似的。
FIFO 数据结构定义
|
1 2 3 4 5 6 7 8 9 |
// FIFO_DEPTH 表示整体FIFO的深度,例如 ADDR_WIDTH = 3 , 整体的FIFO 深度为 8 ,有8 个存储单元 localparam <span style="color: #0000ff;">FIFO_DEPTH</span> = 2**ADDR_WIDTH; // 定义FIFO的数据结构, 数据宽度为 8 (缺省值),用户可以更改; FIFO 的深度为 8 (0 - 7 ) reg [DATA_WIDTH - 1:0] <span style="color: #0000ff;">ram</span> [0: FIFO_DEPTH - 1]; reg [ADDR_WIDTH - 1:0] <span style="color: #0000ff;">rd_addr</span> = 0; // FIFO 读数据指针 assign <span style="color: #0000ff;">dout</span> = ram[rd_addr]; // 将FIFO里的数据 输出到端口上 reg [ADDR_WIDTH - 1:0] <span style="color: #0000ff;">wr_addr</span> = 0; // FIFO 写数据指针 |
FIFO 相关的参数定义
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// <span style="color: #0000ff;">写地址指针</span>是否可<span style="color: #0000ff;">读地址指针</span>相同,当两个地址相同时,可能是当前FIFO为空,也可能是当前FIFO满 wire <span style="color: #0000ff;">ptr_match</span> = (wr_addr == rd_addr) ? 1'b1 : 1'b0; /* 表示 最后一次操作: 1 => 写操作, 0 => 读 操作。 如果是没有操作, 或者是读写同时发生, maybe_full 这个值保持之前的值不变。 */ reg <span style="color: #0000ff;">maybe_full</span> = 0; // 读写指针相同, 最后一次操作是 <span style="color: #0000ff;">读</span>操作 ,整个FIFO为<span style="color: #0000ff;">空</span> assign <span style="color: #0000ff;">empty</span> = ptr_match & (!maybe_full); // 读写指针相同, 最后一次操作是 <span style="color: #0000ff;">写</span>操作 ,整个FIFO为<span style="color: #0000ff;">满</span> assign <span style="color: #0000ff;">full</span> = ptr_match & maybe_full; |
FIFO 读写 相关的操作
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
wire <span style="color: #0000ff;">wr_vld</span> = (!full) & wr_en; // 如果FIFO不是<span style="color: #0000ff;">满</span>状态, 同时又是<span style="color: #0000ff;">写</span>操作, 这时<span style="color: #0000ff;">写</span>有效 wire <span style="color: #0000ff;">rd_vld</span> = (!empty) & rd_en; // 如果FIFO不是<span style="color: #0000ff;">空</span>状态, 同时又是<span style="color: #0000ff;">读</span>操作, 这时<span style="color: #0000ff;">读</span>有效 wire [ADDR_WIDTH:0] <span style="color: #0000ff;">wr_sub_rd</span> = wr_addr - rd_addr; // 写地址指针 - 读地址指针 /* 写地址指针 - 读地址指针 的二进制 余数 1)wr_addr 是 写数据地址指针, rd_addr 是读数据指针。 读写指针都是从 0 开始 一直前进 , 加到 7 之后 再 + 1 时 ,回到 0。 2)<span style="color: #0000ff;">读指针</span>只能小于 , 等于 <span style="color: #0000ff;">写指针</span>, 不可能大于 <span style="color: #0000ff;">写指针</span>。 3)<span style="color: #0000ff;">写指针</span> - <span style="color: #0000ff;">读指针</span> 的结果 最大 为 8 , 不可能大于 8 所以 (举例) 当 <span style="color: #0000ff;">读地址</span> = 3 , <span style="color: #0000ff;">写地址</span> = 2 时,代表的是<span style="color: #0000ff;"> 写指针</span> 2 + 8 , <span style="color: #0000ff;">读指针</span>为 3; <span style="color: #0000ff;">ptr_diff</span> = 3'b111 当 <span style="color: #0000ff;">读地址</span> = 4 , <span style="color: #0000ff;">写地址</span> = 7 时,代表的是 <span style="color: #0000ff;">写指针</span> 7 , <span style="color: #0000ff;">读指针</span>为 4; <span style="color: #0000ff;">ptr_diff</span> = 3'b011 当 <span style="color: #0000ff;">读地址</span> = 0 , <span style="color: #0000ff;">写地址</span> = 7 时,代表的是<span style="color: #0000ff;"> 写指针</span> 7 , <span style="color: #0000ff;">读指针</span>为 0; <span style="color: #0000ff;">ptr_diff</span> = 3'b111 当 <span style="color: #0000ff;">读地址</span> = 7 , <span style="color: #0000ff;">写地址</span> = 0 时,代表的是 <span style="color: #0000ff;">写指针</span> 0 + 8 , <span style="color: #0000ff;">读指针</span>为 7; <span style="color: #0000ff;">ptr_diff</span> = 3'b001 当 <span style="color: #0000ff;">读地址</span> = 5 , <span style="color: #0000ff;">写地址</span> = 5 时,代表的是 <span style="color: #0000ff;">写指针</span> 5 , <span style="color: #0000ff;">读指针</span>为 5; <span style="color: #0000ff;">ptr_diff</span> = 3'b000 */ wire [ADDR_WIDTH - 1:0] <span style="color: #0000ff;">ptr_diff</span> = wr_sub_rd[ADDR_WIDTH - 1:0]; //读写指针<span style="color: #0000ff;">相同</span>,最后一次操作是 <span style="color: #0000ff;">写</span>操作,整个FIFO为<span style="color: #0000ff;">满,carry_bit<span style="color: #000000;"> = 1;读写指针<span style="color: #0000ff;">不同</span>,<span style="color: #0000ff;">carry_bit </span>= 0 //读写指针<span style="color: #0000ff;">相同</span>,最后一次操作是 <span style="color: #0000ff;">读</span>操作,整个FIFO为<span style="color: #0000ff;">空</span>,<span style="color: #0000ff;">carry_bit</span> = 0;读写指针<span style="color: #0000ff;">不同</span>,<span style="color: #0000ff;">carry_bit </span>= 0</span></span> wire <span style="color: #0000ff;">carry_bit</span> = maybe_full & ptr_match; // 读写指针<span style="color: #0000ff;">相同</span>, FIFO full: data_count = 4'b1000; FIFO empty: data_count = 4'b0000;<span style="color: #0000ff;"><span style="color: #000000;"> // 读写指针<span style="color: #0000ff;">不同</span>, data_count = {1'b0, prt_diff[2:0]};</span></span> assign <span style="color: #0000ff;">data_count</span> = {carry_bit,ptr_diff}; |
这里,我们会发现,在计算FIFO counter 时 ,不论写地址 是否 小于 读地址 ,只要 写地址 – 读 地址 , prt_diff 的结果都是正确的(1 – 7 ) , 只有 0 ,8 这两个值需要看当前FIFO是空 还是满 才能判断。
写地址计数器
|
1 2 3 |
always @(posedge clk or posedge srst) if (srst) <span style="color: #0000ff;">wr_addr</span> <= 0; else if (wr_vld) <span style="color: #0000ff;">wr_addr</span> <= <span style="color: #0000ff;">wr_addr</span> + 1; |
当wr_en 写FIFO信号有效, FIFO 又不是为满时, wr_vld 有效; 这时 写地址 + 1
写数据到存储空间
|
1 2 3 4 |
wire [DATA_WIDTH - 1:0] din_w = din; always @(posedge clk or posedge srst) if(srst) <span style="color: #0000ff;">ram</span>[0] <= 0; else if(wr_vld) <span style="color: #0000ff;">ram</span>[wr_addr] <= din_w; |
当wr_en 写FIFO信号有效, FIFO又不是为满时, wr_vld 有效; 这时 写数据到相应的地址空间中。
读地址计数器
|
1 2 3 |
always @(posedge clk or posedge srst) if (srst) <span style="color: #0000ff;">rd_addr</span> <= 0; else if (rd_vld) <span style="color: #0000ff;">rd_addr</span> <= <span style="color: #0000ff;">rd_addr</span> + 1; |
当rd_en 写FIFO 信号有效, FIFO又不是为空时, rd_vld 有效; 这时 读地址 + 1
满足读写不同时发生的最后一次操作
|
1 2 3 4 5 |
always @(posedge clk or posedge srst) if (srst) <span style="color: #0000ff;">maybe_full</span> <= 0; else if (wr_vld != rd_vld) <span style="color: #0000ff;">maybe_full</span> <= wr_vld; |
从FIFO 读出的数据有效标志
|
1 |
assign <span style="color: #0000ff;">valid</span> = rd_en & (!empty); |
当rd_en 有效, 同时 FIFO不为 空, 这时输出的数据是有效的。
仿真代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
`timescale 1ns / 1ps module tb_sim( ); reg clk = 0; always clk = #10 ~clk; integer i = 0; integer k = 0; reg end_flag = 0; localparam ADDR_WIDTH = 3; localparam DATA_WIDTH = 8; reg fifo_wr = 0; reg [DATA_WIDTH - 1:0] fifo_din = 0; wire full; reg fifo_rd = 0; wire [DATA_WIDTH - 1:0] fifo_dout; wire empty; wire [ADDR_WIDTH:0] io_count; reg reset = 1; initial begin fifo_wr = 0; fifo_rd = 0; reset = 1; #200; reset = 0; #100; @(posedge clk); for(i = 0 ; i < 256; i = i+ 1) begin while(full) begin fifo_wr = 0; @(posedge clk); end fifo_wr = 1; fifo_din = fifo_din + 1; @(posedge clk); fifo_wr = 0; @(posedge clk); end fifo_wr = 0; @(posedge clk); end integer j = 0; wire valid; initial begin fifo_wr = 0; fifo_rd = 0; end_flag = 0; #200; @(posedge clk); j = 0; while(empty) @(posedge clk); while ( k < 128 ) begin while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); fifo_rd = 0; @(posedge clk); while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); end fifo_rd = 0; for(j = 0 ; j < 17; j = j+ 1) begin @(posedge clk); end while( k < 248 ) begin while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); fifo_rd = 0; @(posedge clk); while(empty) begin fifo_rd = 0; @(posedge clk); end fifo_rd = 1; @(posedge clk); end fifo_rd = 0; @(posedge clk); @(posedge clk); @(posedge clk); while( k < 256 ) begin while(empty) @(posedge clk); fifo_rd = 1; @(posedge clk); fifo_rd = 0; @(posedge clk); end fifo_rd = 0; @(posedge clk); end_flag = 1; end initial begin k = 0; while (1) begin if(valid) k = k + 1; @(posedge clk); end end syn_fifo # ( .ADDR_WIDTH (ADDR_WIDTH), .DATA_WIDTH (DATA_WIDTH) ) syn_fifo_inst ( .clk (clk), .wr_en (fifo_wr), .din (fifo_din), .full (full), .rd_en (fifo_rd), .dout (fifo_dout), .empty (empty), .data_count (io_count), .valid (valid), .srst (reset) ); end_flag end_flag_inst ( .clk (clk), .end_flag (end_flag) ); //================================================================== endmodule |
仿真波形
reset 过后,FIFO counter 为0;fifo_rd 可以连续读, 只要是valid, 输出的fifo_dout 都是有效的。empty 时, valid 为 0, 输出的 fifo_dout 无效。
FIFO full 时, io_count = 8; 当 full = 0 时, 又可以继续写FIFO 了。
当k = 256 时, 表示 valid 出现过256 次, 这和FIFO写入的 数值个数 一直, 到此, FIFO写 256 个数据, FIFO读 256 个数据 完毕。
总结
到此, 同步FIFO设计完成了。
自建的FIFO的优点是不需要 Xilinx , Altera 或者其他公司的IP, Verilog 代码可以快速移植, 甚至生产。甚至可以根据自己的要求,设计FIFO输入,输出格式。
目前这个FIFO的缺点是 只能使用同步时钟(写时钟和读时钟是一致的),写数据宽度 和 读数据的宽度必须一致的。 当然,用户掌握了自建IP 的方法, 也可以解决这些问题的。


