同步FIFO学习
1、撰写缘由
这几天在初步学习verilog,学习到了同步FIFO,写点东西记录一下,写写心得体会和大家一起交流学习,中间有不对的地方希望大家能多多包涵,欢迎指正,共同进步。学习时主要参考:https://www.cnblogs.com/SYoong/p/6108780.html,感谢大神的分享。本文与参考有些不同,其中我自己认为有些需要改动的地方,若不对,多多指正。其实同步FIFO在实际中应用很少,应用多的还是异步FIFO,不过作为一个新手拿来练习练习感觉是很不错的。
2、什么是FIFO
简而言之,FIFO就是先进先出的数据缓存器。没有外部读写地址线,顺序的写入与读出,数据地址由内部读写指针自动加1完成,不能像普通存储器那样由地址线读取或者写入某个指定的地址。
3、FIFO有什么作用
两个不同时钟域间数据传输使用FIFO作为缓冲;允许时钟进行DMA操作,提高数据传输速度;不同宽度的数据接口也可以使用FIFO等。
4、同步FIFO的Verilog实现
此同步FIFO深度为16,位宽位8,分为了4个子模块:(1)写地址产生模块wr_addr_gen;(2)16个8bits寄存器模块;(3)读地址产生模块;(4)标志位写满full和读空empty模块。
鼓励大家写代码时能够分模块写并且加注释,这样使代码更加清晰,可读性与可移植性更高。
(1)写地址产生模块wr_addr_gen
当写使能有效并且FIFO还没有被写满时,写地址在时钟上升沿时顺序加1。
module w_addr_gen( input wire clk, //时钟信号 input wire full,//写满标志 input wire wr_en,//写使能 input wire rst_n,//复位信号,低电平有效 output reg [3:0] wr_addr//写地址 ); always @ (posedge clk) begin if (!rst_n) begin wr_addr <= 4'b0; end else if (!full && wr_en == 1'b1) begin wr_addr <= wr_addr + 1'b1; end end endmodule
|
(2)读地址产生模块rd_addr_gen
当读使能有效并且FIFO还没有被读空时,读地址在时钟上升沿时顺序加1。
module r_addr_gen ( input wire clk, //时钟 input wire rst_n,//复位 input wire rd_en,//读使能 input wire empty,//读空标志 output reg [3:0]rd_addr//读地址信号 ); always @ (posedge clk) begin if (!rst_n) begin rd_addr <= 4'b0; end else if ((rd_en==1'b1) && (empty != 1'b1)) begin rd_addr <= rd_addr + 1'b1; end end endmodule
|
(3)存储器RAM模块的实现
module RAM ( input wire clk, input wire wr_en, input wire [3:0] wr_addr,//写地址 input wire [7:0] data_in, //输入数据 input wire rd_en, //读使能 input wire [3:0] rd_addr,//读地址 input wire full, input wire empty, output reg [7:0] data_out //输出数据 ); reg [7:0] fifo [15:0]; //8bits register*16 always @ (posedge clk) begin //write if ((wr_en == 1'b1) && (full!=1)) fifo[wr_addr] <= data_in; //上一行与参考不一致,当写使能有效并且FIFO未写满时,将数据写入FIFO对应地址 //若写使能有效而FIFO已经处于满状态的话,再写入数据就会覆盖原先地址的数据,导致数据丢失 end always @ (posedge clk) begin //read if ((rd_en == 1'b1) && (empty!=1)) //上一行与参考不一致,当读使能有效并且FIFO不为空时,将FIFO中对应地址中的数据读出 //若读使能有效而FIFO为空的话,再读出就会导致读出空数据出错 data_out <= fifo[rd_addr]; end endmodule
|
(4)标志位产生模块flag_gen的实现
module flag_gen ( input wire clk, input wire rst_n, input wire rd_en, input wire wr_en, output wire empty,//FIFO空标志位 output wire full//FIFO满标志位 ); parameter max=5'b10000; reg [4:0] count;//16位的FIFO用5位的寄存器记录被写入的数据个数,见下 always @ (posedge clk) begin if (!rst_n) begin count <= 5'b0; end else case({rd_en,wr_en}) 2'b00: count <= count; //读写均无效,FIFO中被写入数据个数不变 2'b01: if (count!=max) count <= count + 1'b1;//当写有效且FIFO未被写满,计数加1 2'b10: if (count!=5'b0000) count <= count - 1'b1;//当读有效且FIFO未被读空,计数减1 2'b11: count <= count; default: count <= count; endcase end assign full = (count == max);//当计数等于16时,产生满信号,所以用5位的count //若count==4'b1111产生满信号,会导致FIFO中一个存储空间不能被写入数据,仿真可以试一下 assign empty = (count == 5'b0000); endmodule
|
(5)顶层模块syn_fifo_top的实现
用wire将各个模块连接起来即可。
module syn_fifo_top( input wire clk, input wire rst_n, input wire wr_en, input wire rd_en, input wire [7:0] data_in, output wire [7:0] data_out ); wire [3:0] wr_addr; wire [3:0] rd_addr; RAM iRAM( .clk(clk), .wr_en(wr_en), .wr_addr(wr_addr), .data_in(data_in), .rd_en(rd_en), .rd_addr(rd_addr), .data_out(data_out), .full(full), .empty(empty) ); r_addr_gen ir_addr_gen( .clk(clk), .rst_n(rst_n), .rd_en(rd_en), .empty(empty), .rd_addr(rd_addr) ); w_addr_gen iw_addr_gen( .clk(clk), .full(full), .wr_en(wr_en), .rst_n(rst_n), .wr_addr(wr_addr) ); flag_gen iflag_gen( .clk(clk), .rst_n(rst_n), .rd_en(rd_en), .wr_en(wr_en), .empty(empty), .full(full) ); endmodule
|
(6)仿真testbench的实现
`timescale 1ns/1ps `define clk_period 20 module syn_fifo_tb(); reg clk; reg rst_n; reg wr_en,rd_en; reg [7:0] data_in; wire [7:0] data_out; syn_fifo_top isyn_fifo_top( .clk(clk), .rst_n(rst_n), .wr_en(wr_en), .rd_en(rd_en), .data_in(data_in), .data_out(data_out) ); initial begin clk = 1'b0; end always #(`clk_period/2) clk = ~clk; initial begin wr_en = 1'b1; rd_en = 1'b0; rst_n = 1'b0; data_in =8'b0; #(`clk_period+1) rst_n = 1'b1; repeat(16) begin #(`clk_period+1) data_in = data_in + 1'b1; end # (`clk_period*20+1) rd_en = 1'b1; wr_en =1'b0; # (`clk_period*200+1) $finish; end endmodule
|
5、标志位产生模块flag_gen的另一种实现方法
分别将读/写地址寄存器扩展一位,将最高位设置为状态位,其余低位作为地址位,指针由地址位以及状态位组成。首先把读、写状态位全部复位,如果地址循环了奇数次,则状态位置1,偶数次则又重新复位,应用地址位和状态位的结合实现对空、满标志位的控制。当读写指针的地址位和状态位全部吻合的时候,读写指针经历了相同次数的循环移动,也就是说,FIFO 处于空状态;如果读写指针的地址位相同而状态位相反,写指针比读指针多循环一次,标志FIFO处于满状态。
module flag_gen ( input wire [4:0] wr_addr, input wire [4:0] rd_addr, output wire empty, output wire full ); assign full = ((wr_addr[4]!=rd_addr[4]) && (wr_addr[3:0]==rd_addr[3:0])); assign empty = (wr_addr == rd_addr); endmodule
|
注意深度为16的FIFO,方法1使用4位的地址线即可,方法二需要使用5位的地址线,即需要将每个模块中的[3:0]wr_addr、[3:0]rd_addr替换为[4:0]wr_addr、[4:0]rd_addr,需要注意一下。