一. 异步fifo电路设计思想
- 异步fifo的设计主要分为双端口ram、写时钟域的地址管理、读时钟域的地址管理、读时钟域读地址到写时钟域的格雷码同步、写时钟域写地址到读时钟域的格雷码同步这5大部分。
- 读写指针跨时钟域问题
(1)需要跨时钟域的原因:我们需要通过比较读写指针来判断信号的空满状态,而读写指针不在同一个时钟域,是不能直接比较的,因为读写指针是一个信号,信号的值是相对于时域来说的,所以需要将读写指针的值同步到同一时钟域来比较。
(2)先说最终方案:
“写满”的判断:需要将读指针同步到写时钟域,再与写指针判断
“读空“的判断:需要将写指针同步到读时钟域,再与读指针判断
分析:
1)全部同步到写时钟域:
①写满的判断:就是写指针超过了同步后的读指针一圈,因为读指针在同步到写时钟域的过程消耗了时间,所以同步后的读指针其实是小于等于真实的读指针的,所以当”写满“时,并没有真正写满,因为它实际可能多读了几个,这个过程是保守的,是正确。
②读空的判断:也就是同步后的读指针追上了写指针,因为在同步的过程中消耗了时间,所以同步后的读指针小于等于真实的读指针,所以当”读空“时,其实读空已经发生了,这是不正确的功能。
2)全部同步到读时钟域:
①写满的判断:同步后的写指针超过了读指针一圈,因为同步的过程消耗了时间,所以同步后写指针小于等于真实的写指针,当”写满“时,其实写满已经发生了,会覆盖掉有用的值,这是不正确的。
②读空的判断:读指针追上了同步后的写指针,当”读空“时,其实读指针还没有追上真实的写指针,系统没有发生读空,这也是一个保守的过程,是正确的。
所以根据分析,可以得出上面的结论:”写满“的判断需要将读指针同步到写时钟域;”读空“的判断需要将写指针同步到读时钟域。
- 二进制与格雷码
我们知道如果跨时钟域没有处理好,会引起亚稳态的问题,造成指针值得异常,引发fifo的功能错误。那么应该如何将读写指针同步到对方的时钟域呢?
答案:将二进制的指针转化成格雷码后再进行同步。
1)原因:
格雷码是一种非权重码,每次变换位数只有一位,这样能够有效避免在跨时钟域情况下发生亚稳态的概率。因为二进制码虽然在数值上相邻,但是有时需要变化4位bit数,比如0111变换到1000的过程,那么采样的值是任意的四位二进制数,如果在空满成立时没有触发,就会导致数据被覆盖掉或者重复读出。而使用格雷码,每次只改变一位信号 (消除亚稳态就可以使用单bit信号跨时钟域用到的电平同步器,即打两拍),比如格雷码从0001到0011时,即便没有采样到变化后的0011,但是也会采样到0001,这只会导致”不该报空满而报了空满“,功能还是正确的。
2)如何用格雷码判断空满
· 当最高位和次高位相同,其余位相同认为读空;
· 当最高位和此高位不同,其余位相同认为写满。
3)快时钟域同步到慢时钟域会发生漏采的情况,需要解决吗?
不需要解决。
①读慢写快:
”写满“判断:将读指针同步到写时钟域,会发生重复采样,不影响。
”读空“判断:快时钟域同步到慢时钟域,会发生漏采,比如写指针从0写到10,但读时钟域只捕捉到了3、5、8这三个指针,当同步到8指针时,其真实的指针可能时10,当其认为读空其实没有读空。
②读快写慢:
”写满“判断:漏采,读指针从0读到10,但写时钟域只捕捉到了3、5、8这三个指针,当同步到8指针时,其真实的读指针可能是10,就是在写时钟域没有察觉的情况下,读时钟域可能读了数据,这对fifo功能没有影响。
”读空“判断:重复采样,不影响。
二. verilog代码思路
- 设计fifo的深度为16,16个地址需要4位二进制数表示,同时扩展一位作为指示位,所以指针的位宽需要5位,这里我直接设置fifo的宽度为5(可以改)。
- 读写指针二进制码的递增(在什么时候进行写操作和读操作)
- 分别将读写指针从二进制码转化成格雷码
- 将格雷码传递给指针
- 将指针信号通过打两拍进行同步化
- 在写时钟域判断”写满“
- 在读时钟域判断”读空“
- ram和读写时钟域的连接以及ram的实例化
module asyn_fifo#(
parameter WIDTH = 8,
parameter DEPTH = 8
)(
input wclk ,
input rclk ,
input wrstn ,
input rrstn ,
input winc ,
input rinc ,
input [WIDTH-1:0] wdata ,
output wire wfull ,
output wire rempty ,
output wire [WIDTH-1:0] rdata
);
parameter ADDR_WIDTH = $clog2(DEPTH); //地址宽度
//读写指针的二进制码
reg [ADDR_WIDTH:0] waddr_bin;
reg [ADDR_WIDTH:0] raddr_bin;
always@(posedge wclk or negedge wrstn) begin
if(!wrstn) begin
waddr_bin='d0;
end
else if(winc && !wfull) begin
waddr_bin<=waddr_bin+1'b1;
end
end
always@(posedge rclk or negedge rrstn) begin
if(!rrstn) begin
raddr_bin='d0;
end
else if(rinc && !wfull) begin
raddr_bin<=raddr_bin+1'b1;
end
end
//二进制码转格雷码
wire [ADDR_WIDTH:0] waddr_gray;
wire [ADDR_WIDTH:0] raddr_gray;
assign waddr_gray = waddr_bin^(waddr_bin>>1);
assign raddr_gray = raddr_bin^(raddr_bin>>1);
//将格雷码传递到读写指针
reg [ADDR_WIDTH:0] wptr;
reg [ADDR_WIDTH:0] rptr;
always@(posedge wclk or negedge wrstn) begin
if(!wrstn)
wptr<='d0;
else
wptr<=waddr_gray;
end
always@(posedge rclk or negedge rrstn) begin
if(!rrstn)
rptr<='d0;
else
rptr<=raddr_gray;
end
//打两拍,实现读写指针的同步化
reg [ADDR_WIDTH:0] wptr_buff; //通过一个寄存器后的值
reg [ADDR_WIDTH:0] rptr_buff;
reg [ADDR_WIDTH:0] wptr_syn; //通过二个寄存器后的值
reg [ADDR_WIDTH:0] rptr_syn;
always@(posedge wclk or negedge wrstn) begin
if(!wrstn)begin
rptr_buff<='d0;
rptr_syn <='d0;
end
else begin
rptr_buff<=rptr;
rptr_syn <=rptr_buff;
end
end
always@(posedge rclk or negedge rrstn) begin
if(!rrstn) begin
wptr_buff<='d0;
wptr_syn <='d0;
end
else begin
wptr_buff<=wptr;
wptr_syn <=wptr_buff;
end
end
//空满判断
assign wfull = (wptr == {~rptr_syn[ADDR_WIDTH:ADDR_WIDTH-1],rptr_syn[ADDR_WIDTH-2:0]});
assign rempty= (rptr == wptr_syn);
//ram
wire wen;
wire ren;
wire [ADDR_WIDTH-1:0] waddr;
wire [ADDR_WIDTH-1:0] raddr;
assign wen = winc & !wfull;
assign ren = rinc & !rempty;
assign waddr = waddr_bin[ADDR_WIDTH-1:0];
assign raddr = raddr_bin[ADDR_WIDTH-1:0];
//实例化ram模块
ram_2 #(.DEPTH(DEPTH), .WIDTH(WIDTH))
ram_fifo ( .wclk(wclk),
.wenc(wen),
.waddr(waddr),
.wdata(wdata),
.rclk(rclk),
.renc(ren),
.raddr(raddr),
.rdata (rdata)
);
endmodule