赞
踩
FIFO 是一种先进先出的数据缓存器,在逻辑设计里面用的非常多,FIFO 设计可以说是逻辑设计人员必须掌握的常识性设计。FIFO 一般用在隔离两边读写带宽不一致,或者位宽不一样的地方。 在 FPGA 设计,使用 FIFO 一般有两个方法,第一个方法是直接调用官方的 FIFO IP,另外一个方法是自己设计 FIFO 控制逻辑。当然我们学会设计FIFO,并不一定是真的需要自己造轮子,只是说作为从业人员我们要了解相关的设计方法,毕竟自己造的轮子不一定能跑不是。
FIFO 包括同步 FIFO 和异步 FIFO 两种,同步 FIFO 有一个时钟信号,读和写逻辑全部使用这一个时钟 信号,异步 FIFO 有两个时钟信号,读和写逻辑用的各种的读写时钟。 FIFO 与普通存储器 RAM 的区别是没有外部读写地址线,使用起来非常简单,但缺点就是只能顺序写 入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。 FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。
FIFO 的常见参数:
FIFO 读写指针(读写指针就是读写地址)的工作原理:
FIFO 设计的关键:产生可靠的 FIFO 读写指针和生成 FIFO“空”/“满”状态标志。 当读写指针相等时,表明 FIFO 为空,这种情况发生在复位操作时,或者当读指针读出 FIFO 中最后一 个字后,追赶上了写指针时,如下图所示:
当读写指针再次相等时,表明 FIFO 为满,这种情况发生在,当写指针转了一圈,折回来(wrapped around) 又追上了读指针,如下图:
可见读写指针可以在读写使能有效时,每时钟周期+1,而如何产生可靠的“空”/“满”信号则成了同步FIFO设计的重点。下面有两种解决方法:
构建一个计数器,该计数器(fifo_cnt)用于指示当前 FIFO 中数据的个数:
解决好如上所述的几个问题后,可以很容易的贴出Verilog代码:
- //计数器法实现同步FIFO
- module sync_fifo_cnt
- #(
- parameter DATA_WIDTH = 'd8 , //FIFO位宽
- parameter DATA_DEPTH = 'd16 //FIFO深度
- )
- (
- input clk , //系统时钟
- input rst_n , //低电平有效的复位信号
- input [DATA_WIDTH-1:0] data_in , //写入的数据
- input rd_en , //读使能信号,高电平有效
- input wr_en , //写使能信号,高电平有效
-
- output reg [DATA_WIDTH-1:0] data_out, //输出的数据
- output empty , //空标志,高电平表示当前FIFO已被写满
- output full , //满标志,高电平表示当前FIFO已被读空
- output reg [$clog2(DATA_DEPTH) : 0] fifo_cnt //$clog2是以2为底取对数
- );
-
- //reg define
- reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0]; //用二维数组实现RAM
- reg [$clog2(DATA_DEPTH) - 1 : 0] wr_addr; //写地址
- reg [$clog2(DATA_DEPTH) - 1 : 0] rd_addr; //读地址
-
- //读操作,更新读地址
- always @ (posedge clk or negedge rst_n) begin
- if (!rst_n)
- rd_addr <= 0;
- else if (!empty && rd_en)begin //读使能有效且非空
- rd_addr <= rd_addr + 1'd1;
- data_out <= fifo_buffer[rd_addr];
- end
- end
- //写操作,更新写地址
- always @ (posedge clk or negedge rst_n) begin
- if (!rst_n)
- wr_addr <= 0;
- else if (!full && wr_en)begin //写使能有效且非满
- wr_addr <= wr_addr + 1'd1;
- fifo_buffer[wr_addr]<=data_in;
- end
- end
- //更新计数器
- always @ (posedge clk or negedge rst_n) begin
- if (!rst_n)
- fifo_cnt <= 0;
- else begin
- case({wr_en,rd_en}) //拼接读写使能信号进行判断
- 2'b00:fifo_cnt <= fifo_cnt; //不读不写
- 2'b01: //仅仅读
- if(fifo_cnt != 0) //fifo没有被读空
- fifo_cnt <= fifo_cnt - 1'b1; //fifo个数-1
- 2'b10: //仅仅写
- if(fifo_cnt != DATA_DEPTH) //fifo没有被写满
- fifo_cnt <= fifo_cnt + 1'b1; //fifo个数+1
- 2'b11:fifo_cnt <= fifo_cnt; //读写同时
- default:;
- endcase
- end
- end
- //依据计数器状态更新指示信号
- //依据不同阈值还可以设计半空、半满 、几乎空、几乎满
- assign full = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0; //满信号
- assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0; //空信号
-
- endmodule
接下来编写脚本对源码进行测试:
- `timescale 1ns/1ns //时间单位/精度
-
- //------------<模块及端口声明>----------------------------------------
- module tb_sync_fifo_cnt();
-
- parameter DATA_WIDTH = 8 ; //FIFO位宽
- parameter DATA_DEPTH = 8 ; //FIFO深度
-
- reg clk ;
- reg rst_n ;
- reg [DATA_WIDTH-1:0] data_in ;
- reg rd_en ;
- reg wr_en ;
-
- wire [DATA_WIDTH-1:0] data_out;
- wire empty ;
- wire full ;
- wire [$clog2(DATA_DEPTH) : 0] fifo_cnt;
-
-
- //------------<例化被测试模块>----------------------------------------
- sync_fifo_cnt
- #(
- .DATA_WIDTH (DATA_WIDTH), //FIFO位宽
- .DATA_DEPTH (DATA_DEPTH) //FIFO深度
- )
- sync_fifo_cnt_inst(
- .clk (clk ),
- .rst_n (rst_n ),
- .data_in (data_in ),
- .rd_en (rd_en ),
- .wr_en (wr_en ),
-
- .data_out (data_out ),
- .empty (empty ),
- .full (full ),
- .fifo_cnt (fifo_cnt )
- );
-
- //------------<设置初始测试条件>----------------------------------------
- initial begin
- clk = 1'b0; //初始时钟为0
- rst_n <= 1'b0; //初始复位
- data_in <= 'd0;
- wr_en <= 1'b0;
- rd_en <= 1'b0;
- //重复8次写操作,让FIFO写满
- repeat(8) begin
- @(negedge clk)begin
- rst_n <= 1'b1;
- wr_en <= 1'b1;
- data_in <= $random; //生成8位随机数
- end
- end
- //重复8次读操作,让FIFO读空
- repeat(8) begin
- @(negedge clk)begin
- wr_en <= 1'b0;
- rd_en <= 1'd1;
- end
- end
- //重复4次写操作,写入4个随机数据
- repeat(4) begin
- @(negedge clk)begin
- wr_en <= 1'b1;
- data_in <= $random; //生成8位随机数
- rd_en <= 1'b0;
- end
- end
- //持续同时对FIFO读写,写入数据为随机数据
- forever begin
- @(negedge clk)begin
- wr_en <= 1'b1;
- data_in <= $random; //生成8位随机数
- rd_en <= 1'b1;
- end
- end
- end
- //------------<设置时钟>----------------------------------------------
- always #10 clk = ~clk; //系统时钟周期20ns
- endmodule
使用modelsim进行仿真,仿真结果如下:
可以看到,仿真结果与预期效果一致(详见图注)。
举例在深度为8的FIFO中,需要3bit的读写指针来分别指示读写地址3'b000-3'b111这8个地址。若将地址指针扩展1bit,则变成4bit的地址,而地址表示区间则变成了4'b0000-4'b1111。假设不看最高位的话,后面3位的表示区间仍然是3'b000-3'b111,也就意味着最高位可以拿来作为指示位。
解决好如上所述的几个问题后,可以很容易的贴出Verilog代码:
- module sync_fifo_ptr
- #(
- parameter DATA_WIDTH = 'd8 , //FIFO位宽
- parameter DATA_DEPTH = 'd16 //FIFO深度
- )
- (
- input clk , //系统时钟
- input rst_n , //低电平有效的复位信号
- input [DATA_WIDTH-1:0] data_in , //写入的数据
- input rd_en , //读使能信号,高电平有效
- input wr_en , //写使能信号,高电平有效
-
- output reg [DATA_WIDTH-1:0] data_out, //输出的数据
- output empty , //空标志,高电平表示当前FIFO已被写满
- output full //满标志,高电平表示当前FIFO已被读空
- );
-
- //reg define
- //用二维数组实现RAM
- reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];
- reg [$clog2(DATA_DEPTH) : 0] wr_ptr; //写地址指针,位宽多一位
- reg [$clog2(DATA_DEPTH) : 0] rd_ptr; //读地址指针,位宽多一位
-
- //wire define
- wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针
- wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读地址指针
- wire wr_ptr_msb; //写地址指针地址最高位
- wire rd_ptr_msb; //读地址指针地址最高位
-
- assign {wr_ptr_msb,wr_ptr_true} = wr_ptr; //将最高位与其他位拼接
- assign {rd_ptr_msb,rd_ptr_true} = rd_ptr; //将最高位与其他位拼接
-
- //读操作,更新读地址
- always @ (posedge clk or negedge rst_n) begin
- if (rst_n == 1'b0)
- rd_ptr <= 'd0;
- else if (rd_en && !empty)begin //读使能有效且非空
- data_out <= fifo_buffer[rd_ptr_true];
- rd_ptr <= rd_ptr + 1'd1;
- end
- end
- //写操作,更新写地址
- always @ (posedge clk or negedge rst_n) begin
- if (!rst_n)
- wr_ptr <= 0;
- else if (!full && wr_en)begin //写使能有效且非满
- wr_ptr <= wr_ptr + 1'd1;
- fifo_buffer[wr_ptr_true] <= data_in;
- end
- end
-
- //更新指示信号
- //当所有位相等时,读指针追到到了写指针,FIFO被读空
- assign empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
- //当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO被写满
- assign full = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;
-
- endmodule
接下来编写脚本对源码进行测试(与2.1.2一致):
- `timescale 1ns/1ns //时间单位/精度
-
- //------------<模块及端口声明>----------------------------------------
- module tb_sync_fifo_ptr();
-
- parameter DATA_WIDTH = 8 ; //FIFO位宽
- parameter DATA_DEPTH = 8 ; //FIFO深度
-
- reg clk ;
- reg rst_n ;
- reg [DATA_WIDTH-1:0] data_in ;
- reg rd_en ;
- reg wr_en ;
-
- wire [DATA_WIDTH-1:0] data_out;
- wire empty ;
- wire full ;
-
-
- //------------<例化被测试模块>----------------------------------------
- sync_fifo_ptr
- #(
- .DATA_WIDTH (DATA_WIDTH), //FIFO位宽
- .DATA_DEPTH (DATA_DEPTH) //FIFO深度
- )
- sync_fifo_ptr_inst(
- .clk (clk ),
- .rst_n (rst_n ),
- .data_in (data_in ),
- .rd_en (rd_en ),
- .wr_en (wr_en ),
-
- .data_out (data_out ),
- .empty (empty ),
- .full (full )
- );
-
- //------------<设置初始测试条件>----------------------------------------
- initial begin
- clk = 1'b0; //初始时钟为0
- rst_n <= 1'b0; //初始复位
- data_in <= 'd0;
- wr_en <= 1'b0;
- rd_en <= 1'b0;
- //重复8次写操作,让FIFO写满
- repeat(8) begin
- @(negedge clk)begin
- rst_n <= 1'b1;
- wr_en <= 1'b1;
- data_in <= $random; //生成8位随机数
- end
- end
- //重复8次读操作,让FIFO读空
- repeat(8) begin
- @(negedge clk)begin
- wr_en <= 1'b0;
- rd_en <= 1'd1;
- end
- end
- //重复4次写操作,写入4个随机数据
- repeat(4) begin
- @(negedge clk)begin
- wr_en <= 1'b1;
- data_in <= $random; //生成8位随机数
- rd_en <= 1'b0;
- end
- end
- //持续同时对FIFO读写,写入数据为随机数据
- forever begin
- @(negedge clk)begin
- wr_en <= 1'b1;
- data_in <= $random; //生成8位随机数
- rd_en <= 1'b1;
- end
- end
- end
- //------------<设置时钟>----------------------------------------------
- always #10 clk = ~clk; //系统时钟周期20ns
- endmodule
使用modelsim进行仿真,仿真结果如下:
可以看到,仿真结果与预期效果一致(详见图注)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。