异步FIFO的Verilog实现方法
文章要点
一、异步FIFO的定义
异步FIFO读、写接口分别采用不同时钟,异步时钟之间的信号接口用异步FIFO来实现,在两个不同时钟系统之间快速、方便地传输实时数据。
二、异步FIFO需要解决的关键点
设计要点:读空信号如何产生?写满信号如何产生?
直接对比读、写指针时钟域不行,ex.读1时刻指针t1(0),2时刻指针t2(1)。
1.读指针同步到写时钟域: 读指针同步到写时钟域,同步后的读指针小于等于原来的读指针;写指针不需要同步。
“写满”的判断: 写指针超过了同步后的读指针一圈,读指针实际大于同步后的读指针,实际上写指针没有大于读指针一圈,“假写满”,写到将满未满报“写满”,不影响实际作用,浪费部分性能。
“读空”的判断: 同步后的读指针追上了写指针。读指针实际大于等于同步后的读指针,“读空”还在读,错误。
2、写指针同步到读时钟域: 写指针同步到读时钟域,写指针实际大于等于同步后的写指针;读指针不需要同步。
“写满”的判断: 同步后的写指针超过了读指针一圈,写指针实际大于等于同步后的写指针,实际上写指针大于读指针一圈,“写满”还在写,错误。
“读空”的判断: 同步后的读指针追上了写指针。写指针实际大于等于同步后的写指针,还有数据通知“读空”,不影响实际作用,浪费部分性能。
总结:
- “写满”的判断: 将读指针同步到写时钟域,再与写指针判断。
- “读空”的判断: 将写指针同步到读时钟域,再与读指针判断。
口诀:“写(满)读指针,读(空)看写指针”
三、异步FIFO使用Gray Code 格雷码避免亚稳态
将读、写指针转化为格雷码。格雷码是一种非权重码,每次变化只有一位不同,有效避免跨时钟域亚稳态问题。
- 如何用格雷码判断空满?
首先我们需要将指针向高位拓展一位,这是为了判断写指针是否超过读指针一圈,超过一周最高位肯定不同,其余位相等。二进制方法是对比最高位不同然后通过对比除了最高位的其余位相同,来判断读写指针是否重合。这种方法判断二进制的指针是没有问题的,但是这不适合格雷码形式的指针,因为格雷码是镜像对称的,若只根据最高位是否相同来区分是读空还是写满是有问题的。比如0和15,最高位不一样但是其余位相同,写指针没有超过一周。
格雷码镜像对称特性:
十进制 | 格雷码 |
---|---|
0 | 0_000 |
1 | 0_001 |
2 | 0_011 |
3 | 0_010 |
4 | 0_110 |
5 | 0_111 |
6 | 0_101 |
7 | 0_100 |
中心 | 中心 |
8 | 1_100 |
9 | 1_101 |
10 | 1_111 |
11 | 1_110 |
12 | 1_010 |
13 | 1_011 |
14 | 1_001 |
15 | 1_000 |
格雷码判断:
- “读空”:最高位和次高位相同,其余位相同认为是读空。
- “写满”:最高位和次高位不同,其余位相同认为是写满。
四、两时钟域同步问题
快时钟采慢时钟可以直接打拍,但存在漏采,漏采问题怎么解决?不需要解决。
“读慢写快”:(读时钟慢写时钟快)
1.“写满”的判断: 读指针同步到写时钟域。读慢写快,不会有读指针遗漏,同步后的读指针小于等于实际读指针,未写满,“假写满”,不影响功能。
2.“读空”的判断: 写指针同步到读时钟域。读慢写快,采集遗漏,写指针0,1,2,可能采集到1时,实际到2,“假读空”,不影响功能。
“读快写满”:
1.“写满”的判断: 读指针同步到写时钟域。读快写慢,遗漏部分读指针,读指针0,1,2, 可能采集到1时,实际到2,“假写满”,不影响功能。
2.“读空”的判断: 写指针同步到读时钟域。读快写慢,不会有指针遗漏,同步后的写指针大于等于实际写指针,“假读空”,不影响功能。
代码-Verilog
例子:FIFO深度为16,地址为4位,扩展一位作为指示位,指针5位。
//异步FIFO
module async_fifo
#(
parameter DATA_WIDTH = 'd8 , //FIFO位宽
parameter DATA_DEPTH = 'd16 //FIFO深度
)
(
//写数据
input wr_clk , //写时钟
input wr_rst_n , //低电平有效的写复位信号
input wr_en , //写使能信号,高电平有效
input [DATA_WIDTH-1:0] data_in , //写入的数据
//读数据
input rd_clk , //读时钟
input rd_rst_n , //低电平有效的读复位信号
input rd_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; //读地址指针,二进制
reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d1; //读指针格雷码在写时钟域下同步1拍
reg [$clog2(DATA_DEPTH) : 0] rd_ptr_g_d2; //读指针格雷码在写时钟域下同步2拍
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d1; //写指针格雷码在读时钟域下同步1拍
reg [$clog2(DATA_DEPTH) : 0] wr_ptr_g_d2; //写指针格雷码在读时钟域下同步2拍
//wire define
wire [$clog2(DATA_DEPTH) : 0] wr_ptr_g; //写地址指针,格雷码
wire [$clog2(DATA_DEPTH) : 0] rd_ptr_g; //读地址指针,格雷码
wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针,作为写ram的地址
wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读地址指针,作为读ram的地址
//地址指针从二进制转换成格雷码
assign wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);
assign rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
//读写RAM地址赋值
assign wr_ptr_true = wr_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //写RAM地址等于写指针的低DATA_DEPTH位(去除最高位)
assign rd_ptr_true = rd_ptr [$clog2(DATA_DEPTH) - 1 : 0]; //读RAM地址等于读指针的低DATA_DEPTH位(去除最高位)
//写操作,更新写地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
if (!wr_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
//将读指针的格雷码同步到写时钟域,来判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n)begin
rd_ptr_g_d1 <= 0; //寄存1拍
rd_ptr_g_d2 <= 0; //寄存2拍
end
else begin
rd_ptr_g_d1 <= rd_ptr_g; //寄存1拍
rd_ptr_g_d2 <= rd_ptr_g_d1; //寄存2拍
end
end
//读操作,更新读地址
always @ (posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n)
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 rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n)begin
wr_ptr_g_d1 <= 0; //寄存1拍
wr_ptr_g_d2 <= 0; //寄存2拍
end
else begin
wr_ptr_g_d1 <= wr_ptr_g; //寄存1拍
wr_ptr_g_d2 <= wr_ptr_g_d1; //寄存2拍
end
end
//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
//当高位相反且其他位相等时,写指针超过读指针一圈,FIFO被写满
//同步后的读指针格雷码高两位取反,再拼接上余下位
assign full = ( wr_ptr_g == { ~(rd_ptr_g_d2[$clog2(DATA_DEPTH) : $clog2(DATA_DEPTH) - 1])
,rd_ptr_g_d2[$clog2(DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;
endmodule