近期参与了一项流片项目,里面用到了FIFO,但是只给了ram的IP,只能自己写一个FIFO了,趁此机会好好学习一下。
本文参考了一篇重要文献:Simulation and Synthesis Techniques for Asynchronous FIFO Design
貌似是FIFO的经典好文了,把异步FIFO的原理讲得很透彻。话不多说,上代码
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/04/04 11:28:01
// Design Name:
// Module Name: afifo_1024x64
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module afifo_1024x64(
input rst,
input wr_clk,
input rd_clk,
input wr_en,
input rd_en,
input [63:0] wr_data,
output [63:0] rd_data,
output full,
output empty,
output [10:0] wr_data_count,
output [10:0] rd_data_count
);
parameter WIDTH = 64;
parameter DEPTH = 10;
wire ram_wr_en,ram_rd_en;
wire[DEPTH:0] rd_p_grey,wr_p_grey;
wire[DEPTH-1:0] ram_wr_addr,ram_rd_addr;
RA2SHD1024x64 bram(
.QA (),
.QB (rd_data),
.ADRA (ram_wr_addr),
.DA (wr_data),
.WEA (ram_wr_en),
.MEA (1'b1),
.CLKA (wr_clk),
.TEST1A (1'b0),
.RMEA (1'b0),
.RMA (4'b0),
.LS (1'b0),
.ADRB (ram_rd_addr),
.DB (64'b0),
.WEB (1'b0),
.MEB (ram_rd_en),
.CLKB (rd_clk),
.TEST1B (1'b0),
.RMEB (1'b0),
.RMB (4'b0)
);
//ram_16x64 ram_16x64_inst (
// .clka(wr_clk), // input wire clka
// .ena(1'b1), // input wire ena
// .wea(ram_wr_en), // input wire [0 : 0] wea
// .addra(ram_wr_addr), // input wire [3 : 0] addra
// .dina(wr_data), // input wire [63 : 0] dina
// .douta(), // output wire [63 : 0] douta
// .clkb(rd_clk), // input wire clkb
// .enb(ram_rd_en), // input wire enb
// .web(1'b0), // input wire [0 : 0] web
// .addrb(ram_rd_addr), // input wire [3 : 0] addrb
// .dinb(64'b0), // input wire [63 : 0] dinb
// .doutb(rd_data) // output wire [63 : 0] doutb
//);
w_ctrl
#(
.WIDTH(WIDTH),
.DEPTH(DEPTH)
) w_ctrl_inst
(
.rst (rst),
.clk (wr_clk),
.wr (wr_en),//fifo的wr_en
.wr_en (ram_wr_en),//ram的wr_en
.rd_p_grey (rd_p_grey),
.full (full),
.ram_wr_addr (ram_wr_addr),
.wr_p_grey (wr_p_grey),
.wr_data_count (wr_data_count)
);
r_ctrl
# (
.WIDTH(WIDTH),
.DEPTH(DEPTH)
) r_ctrl_inst
(
.rst (rst),
.clk (rd_clk),
.rd (rd_en), //fifo的rd_en
.rd_en (ram_rd_en),//ram的rd_en
.wr_p_grey (wr_p_grey),
.empty (empty),
.ram_rd_addr (ram_rd_addr),
.rd_p_grey (rd_p_grey),
.rd_data_count (rd_data_count)
);
endmodule
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/04/04 11:24:46
// Design Name:
// Module Name: w_ctrl
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module w_ctrl
#(
parameter WIDTH = 64,
parameter DEPTH = 10
)
(
input rst,
input clk,
input wr,
output wr_en,
input [DEPTH:0] rd_p_grey,
output full,
output [DEPTH-1:0] ram_wr_addr,
output [DEPTH:0] wr_p_grey,
output [DEPTH:0] wr_data_count
);
reg [DEPTH:0] wr_p_r;
wire [DEPTH:0] wr_p_gray_i;
reg [DEPTH:0] rd_p_grey_r;
reg [DEPTH:0] rd_p_grey_r1;
reg [DEPTH:0] rd_p_g2b_r;
wire wr_en_i;
wire full_i;
wire[DEPTH:0] p_xor;
reg[DEPTH:0] wr_data_count_r;
wire[1:0] top_bit;
integer i;
assign wr_en_i = wr&(~full_i); //ram的写使能信号,只有当fifo处于非满状态时才可以写入。当fifo写满时,则对外部的写信号不做响应,以保护内部源数据
always @ (posedge clk or posedge rst) begin
if (rst) begin
wr_p_r <= 'b0;
rd_p_grey_r <= 'b0;
rd_p_grey_r1 <= 'b0;
end else begin
if (wr_en_i) begin
wr_p_r <= wr_p_r + 1'b1;
end
rd_p_grey_r <= rd_p_grey;
rd_p_grey_r1 <= rd_p_grey_r;
end
end
//计算rd_data_count
always @ (*) begin
if (rst) begin
rd_p_g2b_r = 'b0;
end else begin
rd_p_g2b_r[DEPTH] = rd_p_grey_r1[DEPTH];
for(i=DEPTH-1;i>=0;i=i-1) begin
rd_p_g2b_r[i] = rd_p_grey_r1[i] ^ rd_p_g2b_r[i+1];
end
end
end
assign top_bit = {wr_p_r[DEPTH],rd_p_g2b_r[DEPTH]};
always @ (posedge clk or negedge rst) begin
if (rst) begin
wr_data_count_r <= 'b0;
end else begin
case (top_bit)
2'b00:begin
wr_data_count_r <= wr_p_r - rd_p_g2b_r;
end
2'b11:begin
wr_data_count_r <= wr_p_r - rd_p_g2b_r;
end
2'b10:begin
wr_data_count_r <= wr_p_r - rd_p_g2b_r;
end
2'b01:begin
wr_data_count_r <= {1'b1,wr_p_r[DEPTH-1:0]} - {1'b0,rd_p_g2b_r[DEPTH-1:0]};
end
default:begin
wr_data_count_r <= wr_data_count_r;
end
endcase
end
end
assign wr_p_gray_i = (wr_p_r>>1)^ wr_p_r; //计算写指针的格雷码
assign p_xor = wr_p_gray_i^rd_p_grey_r1; //这个异或值用于判断fifo是否写满,即下一行
assign full_i = ((p_xor[DEPTH:DEPTH-1] == 2'b11 ) & (p_xor[DEPTH-2:0]== 0 ))? 1'b1:1'b0;//如果wr_p_gray_i^rd_p_grey_r1的结果是110000...0,那么就写满了,否则就没写满
//input & output
assign full = full_i; //fifo写满标志
assign wr_en = wr_en_i; //ram的写使能,不要与fifo的外部写使能搞混
assign ram_wr_addr = wr_p_r[DEPTH-1 :0]; //ram的写地址,等于写指针去掉最高位
assign wr_p_grey = wr_p_gray_i;//当前写指针的格雷码,输出给r_ctrl
assign wr_data_count = wr_data_count_r[DEPTH:0];
endmodule
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/04/04 16:38:21
// Design Name:
// Module Name: r_ctrl
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module r_ctrl
# (
parameter WIDTH = 64,
parameter DEPTH = 10
)
(
input rst,
input clk,
input rd,
output rd_en,
input [DEPTH:0] wr_p_grey,
output empty,
output [DEPTH-1:0] ram_rd_addr,
output [DEPTH:0] rd_p_grey,
output [DEPTH : 0] rd_data_count
);
reg [DEPTH:0] rd_p_r;
wire [DEPTH:0] rd_p_grey_i;
reg [DEPTH:0] wr_p_grey_r;
reg [DEPTH:0] wr_p_grey_r1;
reg[DEPTH:0] wr_p_g2b_r;
reg [DEPTH:0] rd_data_count_r;
wire[1:0] top_bit;
wire rd_en_i;
wire empty_i;
integer i;
assign rd_en_i = rd&(~empty_i);
always @ (posedge clk or posedge rst) begin
if (rst) begin
rd_p_r <= 'b0;
wr_p_grey_r <= 'b0;
wr_p_grey_r1 <= 'b0;
end else begin
if(rd_en_i) begin
rd_p_r <= rd_p_r + 1'b1;
end
wr_p_grey_r <= wr_p_grey;
wr_p_grey_r1 <= wr_p_grey_r;
end
end
//always @ (*) begin
// if(rst) begin
// rd_data_count_r <= 'b0;
// end else begin
// end
//end
//将写指针的格雷码转换为二进制,与读指针的二进制进行比较,得到rd_data_count
always @ (*) begin //写指针格雷码转二进制
if (rst) begin
wr_p_g2b_r = 'b0;
end else begin
wr_p_g2b_r[DEPTH] = wr_p_grey_r1[DEPTH];
for(i=DEPTH-1;i>=0;i=i-1) begin
wr_p_g2b_r[i] = wr_p_grey_r1[i] ^ wr_p_g2b_r[i+1];
end
end
end
//wr_p_g2b_r rd_p_r
assign top_bit = {wr_p_g2b_r[DEPTH],rd_p_r[DEPTH]};
//计算rd_data_count_r ,将会比wr_p_grey_r1晚一个周期
always @ (posedge clk or posedge rst) begin
if (rst) begin
rd_data_count_r <='b0;
end else begin
case (top_bit)
2'b00: begin
rd_data_count_r <= wr_p_g2b_r - rd_p_r;
end
2'b11: begin
rd_data_count_r <= wr_p_g2b_r - rd_p_r;
end
2'b10: begin
rd_data_count_r <= wr_p_g2b_r - rd_p_r;
end
2'b01: begin
rd_data_count_r <= {1'b1,wr_p_g2b_r[DEPTH-1 : 0]} - {1'b0,rd_p_r[DEPTH-1 :0]};
end
default: begin
rd_data_count_r <= rd_data_count_r;
end
endcase
end
end
assign rd_p_grey_i = (rd_p_r>>1)^(rd_p_r); //计算读指针的格雷码,用于与写指针的格雷码进行比较
assign empty_i = (rd_p_grey_i==wr_p_grey_r1)?1'b1:1'b0;//读指针的格雷码与写指针的格雷码进行比较,得到empty标志
//input & output
assign empty = empty_i;
assign rd_p_grey = rd_p_grey_i;
assign rd_en = rd_en_i;
assign ram_rd_addr = rd_p_r[DEPTH-1 : 0];
assign rd_data_count = rd_data_count_r[DEPTH:0];
endmodule
1.FIFO的空满判断
FIFO的空满的判断是基于写指针和读指针的。初始情况下,写指针和读指针都指向第一个存储单元,当写使能有效时,存储单元被写入新的数据,同时写指针移动到下一个存储单元;当读使能有效时,读指针指向的存储单元的值将被输出,同时读指针移动到下一个存储单元。指针走过所有存储单元后,将重新回到第一个存储单元。
以下是一些基本的要搞清楚的地方:
(1)一个深度为2^N次方的FIFO,存储单元的地址需要N bit,为什么写指针和读指针需要N+1位?
这是基于FIFO满状态的判断的需要。只有当写指针的值与读指针的值的差等于2^N时,才可以算是真正写满了,若指针只有N位,那么两者之差是不会大于2^N-1的。
举个例子吧,假如深度为2,有四个存储单元,如果读指针一直等于0,那么FIFO什么时候写满呢?
写指针=2’b11时算是写满了吗?并不是,因为此时地址为11的单元还未被写入,只有当写指针
等于4(100),那么此时00、01、10 、11的存储单元都被写过了,而00单元还没被读出,这时候
FIFO才算是被写满了。
(2)如何判断FIFO写满了或读空?
读指针与写指针的差值,最大为2^N,最小为0。由(1)我们已经知道了,指针最高位是判断满状态的关键。只有当写指针的值与读指针的值的差等于2^N时,FIFO才算写满,这个条件与下面的逻辑化表述等价:写指针与读指针的最高位不同,而其余位都相同。当写指针的值与读指针的值的差等于0时,FIFO读空,即写指针与读指针完全相同。
需要指出的是,FIFO满或者FIFO空时,读指针和写指针都指向同一个存储空间,只不过FIFO满的时候,读指针正好领先写指针整整一轮。
③为什么要用格雷码判断空满状态?
这是为了避免多比特同步所带来的隐患。满状态的判断是在写时钟域下进行的,空状态的判断是在读时钟域下进行的,因此需要将指针进行跨时钟域采样,这就可能会发生所谓的多比特同步问题:指针数据是多个比特的,当指针中有若干个比特同时翻转时,由于布局布线的不对称性,每个比特发生翻转的快慢是不同的,这就造成了数据在A到B之间的转换可能存在若干个中间状态。比如从00000000变到11111111,可能是00000000->00001010->11111010->11111111。如果碰巧采样到了中间变量,那么相当于指针的数据一下子跳到不知道哪个地方,会造成很大的不确定性。
格雷码由于相邻两个状态只有一个bit会发生变化,理论上是100%规避了上述的多比特同步问题。因为对格雷码采样的话,A->B的变化过程不存在中间变量,只会采样到A或者B,如果采样到了B,那么数据的更新没有延迟,如果采样到了A,那么数据的更新将会延迟一个时钟周期,这虽然不是我们希望看到的,但不会影响FIFO的安全性,反而会使电路更加安全,这一点稍后将会提到。
④格雷码的转换方法以及使用格雷码判断空满状态
二进制转格雷码:二进制数逻辑右移一位,再与原二进制数做异或,就得到对应的格雷码
格雷码转二进制:格雷码最高位等于二进制最高位,格雷码第k位与二进制第k+1位异或得到二进制第k位。先得到二进制的最高位,再依次向下迭代可以得到全部的二进制位。
使用格雷码判断满状态:可以由二进制判断满状态的方法得到如下规律:当读指针和写指针的格雷码最高位和次高位都不同,其余位都相同,那么FIFO处于满状态。
使用格雷码判断空状态:可以由二进制判断空状态的方法得到如下规律:当读指针和写指针的格雷码完全相同,那么FIFO处于空状态。
⑤问:如果读时钟远快于写时钟,那么读时钟采样到的写指针可能不会连续变化,那么写指针就算使用了格雷码,是不是还会存在多比特同步问题?
答:并不会,因为写指针每次都只有一个bit发生变化,写时钟采样读指针时,最坏的情况是单个比特采样错误,不会发生多比特同步问题。(只有在多个比特同时变化的时间窗口内,正好发生采样事件,才可能存在多比特同步问题)
⑥问:在写时钟域下判断满状态,需要将读指针(格雷码形式)同步到写时钟域,这造成了读指针的延后,这会不会导致FIFO已经写满而电路仍然认为FIFO未满,从而导致写溢出?
答:不会,反而进一步减小了溢出的可能性。任意一个时刻,用于判断满状态的读指针只可能会小于真实的读指针值,因此只可能会发生FIFO未满而被判断满。
同理地,在读时钟域,只可能会发生FIFO未空而被判断为空的情况发生,而不可能发生FIFO读空的情况。
⑦问:接⑥。你说有可能FIFO没有空而被判断为空,那最后一个数据会不会永远读不出来?
答:不会,FIFO没有空而被判断为空,原因在于真实的写指针还未被更新到读时钟域,这个情况在1~2个时钟周期内会消失,所以假空状态会马上消失。
话虽如此,在进行逻辑设计时,还是要尽量避免边界条件的发生,在FIFO内数据适中的时候进行读写,两端保持动态平衡,这是最好的。
另外,在某些情况下,读时钟和写时钟的频率差是有要求的,下一篇文章会就这一点做一些讨论。
本文为原创,如有错误,敬请指出,万分感谢!