#
最近在学习fifo的时候,课程中教学使用fifo IP核,但是突发奇想,想自己写一个异步fifo来使用,本来以为异步fifo是一个挺容易的过程,但是学习的过程中发现跨时钟域的处理是一个需要比较深入思考的问题,而不是一个随意使用ip核实现的过程。底下记录我编写异步fifo的过程。以及相应的激励仿真。
在此附上我参考的帖子:
<FPGA>异步FIFO的Verilg实现方法_fpga fifo verilog_孤独的单刀的博客-CSDN博客
大佬讲的十分详细,可以深入具体的学习,我底下的代码也就拙劣的模仿一下。体现自己的理解和学习过程。
1. 什么是异步fifo
异步fifo不同于同步fifo,只需要比较相应的读写指针即可,异步fifo,因为涉及读写时钟的频率不同,所以说我们要考虑跨时钟域的如何处理。之前直接比较读写指针的方法显然就不行了,我们需要用一个跨时钟域的思维来思考如何实现fifo最基本的读空和写满。
2.异步fifo的解决方法:(考虑写满,读空实现可能)
2.1 引用第三方时钟域:(无法使用)
第三方时钟域的使用后,我们考虑基本的读写指针对应,比如说当前时刻为t0,我们从读时钟域转变指针到第三方时钟域,我们的rd_ptr(t0)成功转换到第三方时钟域,同理我们也在t0时刻把wr_ptr转换到第三方时钟域,我们获得wr_ptr(t0),但是转换的过程中,我们的读写时钟域速度是不同的,也就是说读时钟域的指针增长速度和写时钟域的指针增长速度是不同的,也就是说,我们哪怕在第三方时钟域得到了 rd_ptr(t0) = wr_ptr(t0) 我们也无法确认rd_ptr(t1) = wr_ptr(t1),这会的状态是读空的,因为我们其实实际要考虑的是我们读写时钟域中真实的读写指针是否对应。
因此这个方法无法使用。
2.2 转换到读时钟域: (只能实现读空)
转换到读时钟域其实针对的只是写指针,因为读指针不需要转换(QAQ)
我们考虑读空的情况:在t0时刻我们的写指针wr_ptr(t0) 转换到了读时钟域,到达读时钟域之后,比如说在t1时刻,我们的rd_ptr(t1)= wr_ptr(t0),这个时候说明我们fifo里面的数据已经读完了,fifo已经空了,但是此空非彼空,这个时刻是假空,因为经过转换的这一点时间,我们的真实的写指针其实又往前跑了几步。但这是没关系的,我们的空信号是我们fifo设计安全的余量,避免了读超出边界的fifo。
因此读空是可以实现的。
我们考虑写满的情况:我们先明确一下什么时候会满,就是我们写指针已经把整个fifo都写了,并且循环整个fifo之后,又追上读指针的时候说明,我们已经写满了。还是上面的相同的例子,我们在t0发送wr_ptr,到达读时钟域的时候为wr_ptr(t0),在t1时刻,rd_ptr(t1) = wr_ptr(t0)时说明已经写满了,注意这里的wr_ptr(t0)是已经循环完一圈的指针。但是还是和上面一样,我们的写指针在我们转换过来的时候悄悄往前又写了几位,这就完全错误了,因为已经写超了。
因此写满是不能实现的。
2.3 转换到写时钟域: (只能实现写满)
转换到写时钟域其实针对的只是读指针,因为写指针不需要转换(QAQ)
我们考虑读空的情况:在t0时刻我们的写指针rd_ptr(t0) 转换到了写时钟域,到达读时钟域之后,比如说在t1时刻,我们的wr_ptr(t1)= rd_ptr(t0),这个时候说明我们fifo里面的数据已经读完了,但是rd_ptr在转换过来的过程中,悄悄往前跑了两步,这说明这个相等是不成立的,我们已经读了超出fifo的错误数值。
因此读空是不可以实现的。
我们考虑写满的情况:我们在t0发送rd_ptr,到达写时钟域的时候为rd_ptr(t0),在t1时刻,wr_ptr(t1) = rd_ptr(t0)时说明已经写满了,我们的读指针在我们转换过来的时候悄悄往前又写了几位,多读了两位,但这是没有问题的,因为我们的fifo有的是余量给读指针多读两位,所以这里也是安全的余量,也是一个假满。
因此写满是可以实现的。
3.跨时钟域的指针编码(格雷码)
3.1 为什么需要格雷码:
格雷码的形式,可以保证每次指针变化的时候,只变化一个bit,在跨时钟域处理的时候,这个是非常重要的一个点,如果使用二进制编码,相邻的两个状态,可能就会有多位数据的变化,比如说7-8就是0111-1000,一次性变换了四位这样及其容易产生亚稳态。所以说我们需要将指针的二进制转化为格雷码形式。
3.2 格雷码如何实现:
“任何数和0异或等于这个数本身”
具体就如图所示:(这里引用了别人的图)
也就是说我们把这个二进制数和自己的右移一位的数得到的数值就是我们计算出来的格雷码。
assign gray = (bin>>1) ^ bin;
3.3 格雷码指针的比较:
格雷码不同于二进制编码,格雷码进位之后的部分是不能直接比较除最高位以外的其他位是否相等的。我们在比较最高位的时候也需要比较最高位和次高位的数值:
口诀就是:
两组格雷码前两位相反,余位相同说明已经写满 两组格雷码前两位相同,余位也相同说明是读满。
具体的计算过程大家可以自己列一个简单的二进制计算试试。但是遵照以上的两句话就可以掌握指针比较的方法。
4.Verilog代码实现:
// -----------------------------------------------------------------------------
// Copyright (c) 2014-2023 All rights reserved
// -----------------------------------------------------------------------------
// Author : xibo wu (Gatsby) wuxibo2023@163.com
// File : async_fifo.v
// Create : 2023-11-01 15:38:19
// Revise : 2023-11-02 14:56:05
// Editor : sublime text3, tab size (4)
// -----------------------------------------------------------------------------
module async_fifo
#(
parameter FIFO_WIDTH = 'd8 , //FIFO位宽
parameter FIFO_DEPTH = 'd16 //FIFO深度
)
(
//写时钟域
input wire i_wr_clk ,
input wire i_wr_rst_n ,
input wire i_wr_en ,
input wire [FIFO_WIDTH - 1:0] i_data_in ,
//读时钟域
input wire i_rd_clk ,
input wire i_rd_rst_n ,
input wire i_rd_en ,
output wire [FIFO_WIDTH - 1:0] o_data_out ,
//标志位
output wire o_empty ,
output wire o_full
);
//output regs
reg empty_t;
reg full_t;
reg [FIFO_WIDTH - 1:0] data_out_t = 'd0;
//二维数组实现ram
reg [FIFO_WIDTH - 1 : 0 ] data_buffer[FIFO_DEPTH - 1 : 0];
//读写指针(扩展一位)
reg [$clog2(FIFO_DEPTH) : 0] wr_ptr;
reg [$clog2(FIFO_DEPTH) : 0] rd_ptr;
//格雷码编码
wire [$clog2(FIFO_DEPTH) : 0] wr_ptr_g;
wire [$clog2(FIFO_DEPTH) : 0] rd_ptr_g;
//打拍格雷码寄存器
reg [$clog2(FIFO_DEPTH) : 0] wr_ptr_g_d1,wr_ptr_g_d2;
reg [$clog2(FIFO_DEPTH) : 0] rd_ptr_g_d1,rd_ptr_g_d2;
//真实地址
wire [$clog2(FIFO_DEPTH) - 1 : 0] wr_ptr_tr;
wire [$clog2(FIFO_DEPTH) - 1 : 0] rd_ptr_tr;
//assign
assign o_full = full_t;
assign o_empty = empty_t;
assign o_data_out = data_out_t;
assign wr_ptr_g = (wr_ptr >> 1) ^ wr_ptr;
assign rd_ptr_g = (rd_ptr >> 1) ^ rd_ptr;
assign wr_ptr_tr = wr_ptr[$clog2(FIFO_DEPTH) - 1 : 0];
assign rd_ptr_tr = rd_ptr[$clog2(FIFO_DEPTH) - 1 : 0];
//写使能数据输入
always @(posedge i_wr_clk or negedge i_wr_rst_n) begin
if (i_wr_rst_n == 1'b0) begin
wr_ptr <= 'b0;
end
else if (i_wr_en == 1'b1 && full_t != 1'b1) begin
wr_ptr <= wr_ptr + 1'b1;
data_buffer[wr_ptr_tr] <= i_data_in;
end
else begin
wr_ptr <= wr_ptr;
data_buffer[wr_ptr_tr] <= data_buffer[wr_ptr_tr];
end
end
//读使能
always @(posedge i_rd_clk or negedge i_rd_rst_n) begin
if (i_rd_rst_n == 1'b0) begin
rd_ptr <= 'b0;
end
else if (i_rd_en == 1'b1 && empty_t != 1'b1) begin
rd_ptr <= rd_ptr + 1'b1;
data_out_t <= data_buffer[rd_ptr_tr];
end
else begin
rd_ptr <= rd_ptr;
data_out_t <= data_out_t;
end
end
//跨时钟域格雷码打拍 读到写,所以时钟是wr_clk
always @(posedge i_wr_clk or negedge i_wr_rst_n) begin
if(i_wr_rst_n == 1'b0) begin
rd_ptr_g_d1 <= 'b0;
rd_ptr_g_d2 <= 'b0;
end
else begin
rd_ptr_g_d1 <= rd_ptr_g;
rd_ptr_g_d2 <= rd_ptr_g_d1;
end
end
//跨时钟域格雷码打拍 写到读,所以时钟是rd_clk
always @(posedge i_rd_clk or negedge i_rd_rst_n) begin
if(i_rd_rst_n == 1'b0) begin
wr_ptr_g_d1 <= 'b0;
wr_ptr_g_d2 <= 'b0;
end
else begin
wr_ptr_g_d1 <= wr_ptr_g;
wr_ptr_g_d2 <= wr_ptr_g_d1;
end
end
//empty信号
always @(*) begin
if(wr_ptr_g_d2 == rd_ptr_g)begin
empty_t = 1'b1;
end
else begin
empty_t = 1'b0;
end
end
//full信号
always @(*) begin
if(rd_ptr_g_d2 == {~wr_ptr_g[$clog2(FIFO_DEPTH)],~wr_ptr_g[$clog2(FIFO_DEPTH) - 1],
wr_ptr_g[$clog2(FIFO_DEPTH) - 2 : 0]}) begin
full_t = 1'b1;
end
else begin
full_t = 1'b0;
end
end
endmodule
之后编写相应的testbench,我参考的是孤独的单刀的测试文档,但是我都采用的上升沿采样。仿真后结果正确。读空,写满信号都能正确生成。
// -----------------------------------------------------------------------------
// Copyright (c) 2014-2023 All rights reserved
// -----------------------------------------------------------------------------
// Author : xibo wu (Gatsby) wuxibo2023@163.com
// File : tb_async_fifo.v
// Create : 2023-11-02 15:00:35
// Revise : 2023-11-02 15:00:35
// Editor : sublime text3, tab size (4)
// -----------------------------------------------------------------------------
`timescale 1ns/1ps
module tb_async_fifo (); /* this is automatically generated */
parameter FIFO_DEPTH = 8;
parameter FIFO_WIDTH = 8;
//ports declaration
reg wr_clk;
reg wr_rst_n;
reg rd_clk;
reg rd_rst_n;
reg [FIFO_WIDTH - 1 : 0] data_in;
wire [FIFO_WIDTH - 1 : 0] data_out;
reg wr_en;
reg rd_en;
wire empty;
wire full;
//---------clock---------//
//rd_clk clock period = 20 ns
initial begin
rd_clk = 1'b0;
forever #(10) rd_clk = ~rd_clk;
end
//wr_clk clock period = 40 ns
initial begin
wr_clk = 1'b0;
forever #(20) wr_clk = ~wr_clk;
end
//---------clock---------//
//--------operation------//
initial begin
wr_rst_n <= 1'b0;
rd_rst_n <= 1'b0;
wr_en <= 1'b0;
rd_en <= 1'b0;
data_in <= 'd0;
#5
wr_rst_n <= 1'b1;
rd_rst_n <= 1'b1;
//重复写8次,把fifo写满
repeat(8) begin
@(posedge wr_clk) begin
wr_en <= 1'b1;
data_in <= $random;
end
end
//写完之后拉低写使能
@(posedge wr_clk) wr_en <= 1'b0;
//重复8次读操作
repeat(8) begin
@(posedge rd_clk) begin
rd_en <= 1'b1;
end
end
@(posedge rd_clk) rd_en <= 1'b0;
//重复4次写操作,写入四个随机数据
repeat(4) begin
@(posedge wr_clk) begin
wr_en <= 1'b1;
data_in <= $random;
end
end
//持续对fifo读
@(posedge rd_clk) rd_en <= 1'b1;
//持续对fifo写,写入随机数据
forever begin
@(posedge wr_clk) begin
wr_en <= 1'b1;
data_in <= $random;
end
end
end
async_fifo #(
.FIFO_DEPTH(FIFO_DEPTH),
.FIFO_WIDTH(FIFO_WIDTH)
) inst_async_fifo (
.i_wr_clk (wr_clk),
.i_wr_rst_n (wr_rst_n),
.i_wr_en (wr_en),
.i_data_in (data_in),
.i_rd_clk (rd_clk),
.i_rd_rst_n (rd_rst_n),
.i_rd_en (rd_en),
.o_data_out (data_out),
.o_empty (empty),
.o_full (full)
);
endmodule
-----------
仿真部分过长就不附带在这里了,有兴趣的可以自己使用modelsim进行仿真
欢迎大家进行学习交流,并且对文章内容做出指正。