手撕代码——异步FIFO
- 一、异步FIFO原理与设计
- 读写地址指针控制
- 读写地址指针跨时钟处理与空满信号判断
- 读写地址与读写操作
- 二、完整代码与仿真文件
- 三、仿真结果
一、异步FIFO原理与设计
在FIFO的设计中,无论是同步FIFO,还是异步FIFO,最最最最重要的就是如何判断空信号empty与满信号full。在上文《手撕代码——同步FIFO》中,我们设计的同步FIFO,使用一个数据计数器cnt来计算当前FIFO中存储的数据个数,根据计数器cnt的值来判断FIFO的空信号empty与满信号full。如果计数器cnt=0,表示FIFO中当前存储的数据个数为0,则拉高空信号empty;如果计数器cnt=FIFO_DEPTH(FIFO深度),则表示拉高满信号full。那么,在异步FIFO的设计中,是否也可以使用计数器的方式判断空满信号呢?在异步FIFO的设计中,我们不用计数器的方式来判断空满,而是将读写指针地址的位宽增加一位冗余,用于判断空满信号。
读写地址指针控制
首先,是读写地址指针的控制。在写时钟域,当写使能wr_en有效且满信号full为低电平时,表示FIFO中数据未写满,执行写操作,写地址指针wr_ptr自加1;在读时钟域,当读使能rd_en有效且空信号empty为低电平时,表示FIFO中数据未读空,执行读操作,读地址指针rd_ptr自加1。
//write pointer
always @(posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n)
wr_ptr <= 'd0;
else if(wr_en && !full)
wr_ptr <= wr_ptr + 1'b1;
end
//read pointer
always @(posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n)
rd_ptr <= 'd0;
else if(rd_en && !empty)
rd_ptr <= rd_ptr + 1'b1;
end
读写地址指针跨时钟处理与空满信号判断
其次,就是使用读写地址指针进行判断空满信号。读地址指针rd_ptr是在读时钟域wr_clk内,空信号empty也是在读时钟域内产生的;而写地址指针wr_ptr是在写时钟域内,且满信号也是在写时钟域内产生的。
那么,要使用读地址指针rd_ptr与写地址指针wr_ptr对比,产生空信号empty,可以直接对比吗?答案是不可以。因为这两个信号处于不同的时钟域内,要做跨时钟域CDC处理,而多bit信号做跨时钟域处理,常用的方法就是使用异步FIFO进行同步,可是我们不是在设计异步FIFO吗?于是,在这里设计异步FIFO,多bit跨时钟域处理的问题可以转化为单bit跨时钟域的处理,把读写地址指针转换为格雷码后再进行跨时钟域处理,因为无论多少比特的格雷码,每次加1,只改变1位。把读地址指针rd_ptr转换为格雷码,然后同步到写时钟域wr_clk,与写指针地址wr_ptr的格雷码表示进行对比判断,得到满信号full;同样的,把写地址指针wr_ptr转换为格雷码,然后同步到读时钟域rd_clk,与读地址指针rd_ptr的格雷码表示进行对比判断,得到空信号empty。
所以,需要先把二进制码表示的读地址指针rd_ptr与写地址指针wr_ptr转换为格雷码(二进制码与格雷码的转换原理及设计参考《二进制码与格雷码的相互转换原理与Verilog实现》)。
//write pointer(binary to gray)
assign wr_ptr_gray = wr_ptr ^ (wr_ptr>>1);
//read pointer(binary to gray)
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);
然后再对格雷码表示的读写地址指针做单bit跨时钟域处理,打上两拍。需要注意的是:写地址指针的格雷码wr_ptr_gray表示要同步到读时钟域,所以要使用读时钟域的时钟信号rd_clk;而读地址指针的格雷码表示rd_ptr_gray表示要同步到写时钟域,所以要使用写时钟域的时钟信号wr_clk。
//gray write pointer syncrous
always @(posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n) begin
wr_ptr_gray_w2r_1 <= 'd0;
wr_ptr_gray_w2r_2 <= 'd0;
end
else begin
wr_ptr_gray_w2r_1 <= wr_ptr_gray;
wr_ptr_gray_w2r_2 <= wr_ptr_gray_w2r_1;
end
end
//gray read pointer syncrous
always @(posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n) begin
rd_ptr_gray_r2w_1 <= 'd0;
rd_ptr_gray_r2w_2 <= 'd0;
end
else begin
rd_ptr_gray_r2w_1 <= rd_ptr_gray;
rd_ptr_gray_r2w_2 <= rd_ptr_gray_r2w_1;
end
end
然后使用同步处理后的读写地址指针格雷码表示进行对比,得到空满信号。如果写地址指针的格雷码表示同步到读时钟域后,与写地址指针的格雷码表示一致,则表示读空;如果读地址指针的格雷码表示同步到写时钟域后,高两位与写地址指针的格雷码表示相反,而剩余的位与写地址指针的格雷码表示一致,则表示写满。
//full and empty
assign full = (wr_ptr_gray == {~rd_ptr_gray_r2w_2[ADDR_WIDTH:ADDR_WIDTH-1],rd_ptr_gray_r2w_2[ADDR_WIDTH-2:0]}) ? 1'b1 : 1'b0;
assign empty = (rd_ptr_gray == wr_ptr_gray_w2r_2) ? 1'b1 : 1'b0;
读写地址与读写操作
读写地址指针中,我们设置了一位的冗余位宽,用于判断空满,那么,实际上读写地址应该为读写地址指针的次高位到第0位。取读写地址指针的次高位到第0位,得到读写地址,同时根据读写地址进行读写操作。
//write address and read address
assign wr_addr = wr_ptr[ADDR_WIDTH-1:0];
assign rd_addr = rd_ptr[ADDR_WIDTH-1:0];
//write
always @(posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n)
for(i=0;i<FIFO_DEPTH;i=i+1) begin
mem[i] <= 'd0;
end
else if(wr_en && !full)
mem[wr_addr] <= din;
end
//read
always @(posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n)
dout_r <= 'd0;
else if(rd_en && !empty)
dout_r <= mem[rd_addr];
end
assign dout = dout_r;
二、完整代码与仿真文件
异步FIFO的完整代码如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/05/17 20:24:34
// Design Name:
// Module Name: async_fifo
// Project Name:
// Target Devices:
// Tool Versions:
// Description: 异步FIFO
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module async_fifo
#(
parameter FIFO_WIDTH = 8,
parameter FIFO_DEPTH = 8,
parameter ADDR_WIDTH = $clog2(FIFO_DEPTH)
)
(
//write clock domain
input wr_clk ,
input wr_rst_n ,
input wr_en ,
input [FIFO_WIDTH-1:0] din ,
output full ,
//read clock domain
input rd_clk ,
input rd_rst_n ,
input rd_en ,
output [FIFO_WIDTH-1:0] dout ,
output empty
);
//------------------------------------------------//
integer i;
//memory
reg [FIFO_WIDTH-1:0] mem [FIFO_DEPTH-1:0];
//write address and read address
wire [ADDR_WIDTH-1:0] wr_addr;
wire [ADDR_WIDTH-1:0] rd_addr;
//write pointer
reg [ADDR_WIDTH:0] wr_ptr;
wire [ADDR_WIDTH:0] wr_ptr_gray;
reg [ADDR_WIDTH:0] wr_ptr_gray_w2r_1;
reg [ADDR_WIDTH:0] wr_ptr_gray_w2r_2;
//read pointer
reg [ADDR_WIDTH:0] rd_ptr;
wire [ADDR_WIDTH:0] rd_ptr_gray;
reg [ADDR_WIDTH:0] rd_ptr_gray_r2w_1;
reg [ADDR_WIDTH:0] rd_ptr_gray_r2w_2;
//
reg [FIFO_WIDTH-1:0] dout_r;
//------------------------------------------------//
//----------------------//
//write pointer
always @(posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n)
wr_ptr <= 'd0;
else if(wr_en && !full)
wr_ptr <= wr_ptr + 1'b1;
end
//write pointer(binary to gray)
assign wr_ptr_gray = wr_ptr ^ (wr_ptr>>1);
//gray write pointer syncrous
always @(posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n) begin
wr_ptr_gray_w2r_1 <= 'd0;
wr_ptr_gray_w2r_2 <= 'd0;
end
else begin
wr_ptr_gray_w2r_1 <= wr_ptr_gray;
wr_ptr_gray_w2r_2 <= wr_ptr_gray_w2r_1;
end
end
//----------------------//
//read pointer
always @(posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n)
rd_ptr <= 'd0;
else if(rd_en && !empty)
rd_ptr <= rd_ptr + 1'b1;
end
//read pointer(binary to gray)
assign rd_ptr_gray = rd_ptr ^ (rd_ptr>>1);
//gray read pointer syncrous
always @(posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n) begin
rd_ptr_gray_r2w_1 <= 'd0;
rd_ptr_gray_r2w_2 <= 'd0;
end
else begin
rd_ptr_gray_r2w_1 <= rd_ptr_gray;
rd_ptr_gray_r2w_2 <= rd_ptr_gray_r2w_1;
end
end
//----------------------//
//write address and read address
assign wr_addr = wr_ptr[ADDR_WIDTH-1:0];
assign rd_addr = rd_ptr[ADDR_WIDTH-1:0];
//full
assign full = (wr_ptr_gray == {~rd_ptr_gray_r2w_2[ADDR_WIDTH:ADDR_WIDTH-1],rd_ptr_gray_r2w_2[ADDR_WIDTH-2:0]}) ? 1'b1 : 1'b0;
//empty
assign empty = (rd_ptr_gray == wr_ptr_gray_w2r_2) ? 1'b1 : 1'b0;
//----------------------//
//write
always @(posedge wr_clk or negedge wr_rst_n) begin
if(!wr_rst_n)
for(i=0;i<FIFO_DEPTH;i=i+1) begin
mem[i] <= 'd0;
end
else if(wr_en && !full)
mem[wr_addr] <= din;
end
//read
always @(posedge rd_clk or negedge rd_rst_n) begin
if(!rd_rst_n)
dout_r <= 'd0;
else if(rd_en && !empty)
dout_r <= mem[rd_addr];
end
assign dout = dout_r;
endmodule
仿真文件如下:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2023/05/17 21:18:22
// Design Name:
// Module Name: tb_async_fifo
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module tb_async_fifo();
parameter FIFO_WIDTH = 8;
parameter FIFO_DEPTH = 8;
parameter ADDR_WIDTH = $clog2(FIFO_DEPTH);
reg rst_n ;
reg wr_clk ;
reg wr_en ;
reg [FIFO_WIDTH-1:0] din ;
wire full ;
reg rd_clk ;
reg rd_en ;
wire [FIFO_WIDTH-1:0] dout ;
wire empty ;
initial begin
rst_n <= 1'b0;
wr_clk = 1'b1;
rd_clk = 1'b1;
wr_en <= 1'b0;
rd_en <= 1'b0;
din <= 8'b0;
# 5 rst_n <= 1'b1;
end
initial begin
#20 wr_en <= 1'b1;
rd_en <= 1'b0;
#40 wr_en <= 1'b0;
rd_en <= 1'b1;
#30 wr_en <= 1'b1;
rd_en <= 1'b0;
#13 rd_en <= 1'b1;
#10
repeat(100)
begin
#5 wr_en <= {$random}%2 ;
rd_en <= {$random}%2 ;
end
#100
$finish;
end
always #1.5 wr_clk = ~wr_clk ;
always #1 rd_clk = ~rd_clk ;
always #3 din <= {$random}%8'hFF;
async_fifo
#(
.FIFO_WIDTH(FIFO_WIDTH),
.FIFO_DEPTH(FIFO_DEPTH),
.ADDR_WIDTH(ADDR_WIDTH)
)
async_fifo
(
.wr_clk (wr_clk ),
.wr_rst_n (rst_n ),
.wr_en (wr_en ),
.din (din ),
.full (full ),
.rd_clk (rd_clk ),
.rd_rst_n (rst_n ),
.rd_en (rd_en ),
.dout (dout ),
.empty (empty )
);
endmodule
三、仿真结果
根据TestBench文件,对异步FIFO进行仿真。可以看到,在写时钟域wr_clk下,写使能wr_en有效,成功进行数据的写入操作,成功写入8个数据后,写满信号full拉高,在满信号full为高电平期间,即使写使能wr_en有效,也不进行数据的写操作。
同时可以看到写地址指针wr_ptr在写时钟域wr_clk下,当写使能wr_en有效时,自加1,同时通过打两拍把写指针地址的格雷码表示wr_ptr_gray同步到读时钟域。
可以看到FIFO读数据正确,在读完FIFO中所有数据后,拉高读空信号empty。
综上,异步FIFO设计验证通过。