1.异步fifo定义:跨读写时钟域的dpram(双口ram),fifo的读写时钟是相互独立的两个时钟,在各自时钟下实现fifo数据的读写功能。
2.异步fifo用途:
【1】跨时钟域的多bit数据传输;
【2】数据的缓存;
主要包括三部分:Dpram、rd_logic和wr_logic。构成如下图2-1.1的系统框图。
具体设计之前,定义各个模块的基本功能。Dpram本质上是一个读写ram,在wr_en(写使能)和rd_en(读使能)的作用下,在各自时钟沿完成数据的写入和读出。
Wr_logic为写时钟域逻辑模块,主要实现:
[1] 在wr_en下wr_addr增加,并将该地址作格雷码处理待输出给rd_logic;
[2] 写满标志wr_full判断及产生;
[3] 接收rd_loic传来的rd_addr_gray,打2-3拍完成地址同步;
rd_logic为读时钟域逻辑模块,主要实现:
[1] 在rd_en下rd_addr增加,并将该地址作格雷码处理待输出给wr_logic;
[2] 读空标志rd_empty判断及产生;
[3] 接收wr_loic传来的wr_addr_gray,打2-3拍完成地址同步;
作为一种计算机编码方式,格雷码具有如下特点:循环码;相邻码或对称项编码仅有1bit不相同。举例,如下2-1.2列出了0-15的4bit格雷码,可见,十进制码0和1、1和2、14和15(相邻码)对应的Gray4仅有1bit不同,其余位全相同;再观察0和15、1和14、7和8(对称项码)也仅有1bit不同,其余位全相同;再观察,gray4的第0bit,以0110作为循环节,次低位(第1bit)以00111100位循环节,第2bit按照0000_1111_1111_0000为循环节,且循环节中0和1的个数是相等的。
格雷码具有相邻码仅有1bit相异的特点,故在跨时钟域处理时,用作地址传输时,可以有效减少重汇聚(re-coverage,即多bit数据跨时钟域传输时,若时钟采样刚好发生在数据变化时刻,采样到的各bit数据就会不确定,有可能是变化前,也有可能是变化后的)现象发生的概率。这就是异部fifo采用格雷码地址传输的原因。
在RTL建模之前,先举例了解async_fifo的工作机制。如下图2-1.3,为一个深度为8的async_fifo。
NOTE1:规定写指针总是指向下一个将要写入的地址,读地址指向当前读出的地址;
NOTE2:写满后不能继续写,有数据保护,读空后不能再读,避免错读;
①开始,fifo为空,wr_ptr(蓝色为写指针)和rd_ptr(橘黄色为读指针)均指向0地址。向fifo中写入3个数据后,蓝色指针指向addr3,此时未写满。接下来有两种操作,1继续写;2不写,读数据;若选择操作1,蓝色指针向上指直到写满并产生wr_full标志,则不能再写入数据,直到进行读操作,若选择操作2,橘黄色指针递增,当橘黄色指针指向addr3的前一个地址即addr2时,fifo被读空,产生rd_empty标志,此后不能再读。(先写到0010(实际上写地址指向0110,即下一个地址)再读到0010,将写地址à读时钟域,读空标志产生)
②随后继续写,则读空标志消失,比如写到addr6(0101),随后可进行读操作,读到addr6则fifo再次读空并产生rd_empty标志;若写到addr6后继续写,写到addr7后达到fifo_deepth,再执行写操作,写指针会循环一圈,重新指向addr0(此时写地址记成wr+1:addrx),直到再次写到wr+1:addr3,此时fifo倍写满,由于写满保护,则后续只能进行读操作。若一直进行读操作,读指针递增至rd:addr7,之后读指针也循环一圈,直到再次指向addr3(记作rd+1:addr3),再一次产生rd_empty标志。
③至此,读写指针都循环一圈,重新指向addr3回到新的同一个起点,此记作一个round,而后的操作重复执行①、②。
将上述读写流程用流程图描述,见图2-1.4.
按照图2-1.4所述读写流程,发现规律:
1、在读写流程中,当读写指针都在同一个循环之内(读写指针轮回的次数相同)时,
此时读写指针的状态就是读指针滞后于写指针(即rd_addr总是追赶wr_addr),当rd_addr追赶上wr_addr时(认为相等),即把写的内容全部读完,则rd_empty标志产生。
2、写满、读空保护机制的存在,在逻辑上总是“先写后读”,这就使得写指针总是先于读指针完成一次轮回,当写指针来到新的一圈轮回时,此时,
意思是,写指针已经写完一圈了,即将和读指针相遇,直到再次追上rd_addr(认为相等),则把所有的空余空间全部写完,则wr_full标志产生。
若读使能有效,则rd_addr也会递增,在wr_addr轮回一圈之后rd_addr也会轮回新的一圈,记此时两者仍在同一等级(至少是在同一圈),此时,
很不幸,rd_addr再次踏上追逐wr_addr的道路,当两者再次相等时,rd_empty标志产生,再次读完。此后循环1、2步骤。
基于上,需要记录读写指针所在圈数轮回的不同,用 奇偶数圈标志(0、1、2、3…….),转两圈就回到偶数圈,这样可以在原有指针地址的基础上, 扩展1bit记录奇偶圈;新的一圈时,地址表示见下图2-1.5。
NOTE1:上述圆圈转一圈回到起点,该圈成为映射圈。
NOTE2:十进制地址0-7之后就会变成0,这是fifo的一次轮回(因为fifo物理上不是一个圆圈),当十进制地址新的一圈轮回结束后,再次回到起点,这称为映射圈(格雷码映射圈)的一个轮回。
深度为8的fifo,用3bit(8个地址)+1bit(奇偶圈标志)表示,上图所示,红色线表示圈的起点,绿色线标志圈的终点(0-7为第0圈,扩展位为0,15-8为轮回一圈,扩展位为1),当从0-7再从15-8时,地址再次变成0(0000)回到偶数圈,新的一个映射圈轮回开始。
观察知,上述映射圈实际上被16个地址平均分配,十进制数对应0-15,格雷码对应0000-1000,因为格雷码循环码的特征,正好可以用来记录fifo的奇偶圈,又因为格雷码具有对称位置相差1bit的特征,即十进制0-15的gray4只有最高位相反。所以,十进制地址计数器只会计数到7,只不过不同的两圈用不同的格雷码映射,两者结合可以完成空满标志的判断。
综上分析,格雷码映射圈顺时针循环,从十进制0-7,再从15-8,而体现在格雷码上,除去最高位(bit3),低3位是循环的,这正好符合设计初衷。至此,完成空满标志产生的算法设计。
如图2-2.1,为DPRAM的接口示意图。该模块功能简单,只需要在rd_clk和wr_clk下,当使能信号有效,分别向ram中写入和读取数据即可。
RTL代码如下:
module dpram # (
parameter WR_WID = 8 ,
parameter RD_WID = 8 ,
parameter ADDR_WID = 11
)(
input rst_n ,
input [WR_WID - 1 : 0] wr_data ,
input wr_en ,
input [ADDR_WID - 1 : 0] wr_addr ,
input wr_clk ,
input wr_full ,
input rd_en ,
input [ADDR_WID - 1 : 0] rd_addr ,
input rd_clk ,
input rd_empty ,
output reg [RD_WID - 1 : 0] rd_data
);
localparam FIFO_DEEPTH = 1024;
reg [WR_WID - 1 : 0] fifo_arr [FIFO_DEEPTH - 1 : 0];
//------------------------------------------
// 写时钟域,写入数据
//------------------------------------------
always @ (posedge wr_clk) begin // fifo_arr不进行初始化,若需要,在tb中初始化
if (wr_en && ~wr_full) // wr_en有效
fifo_arr[wr_addr] <= wr_data;
end
//------------------------------------------
// 读时钟域,读出数据
//------------------------------------------
always @ (posedge rd_clk, negedge rst_n) begin
if (!rst_n)
rd_data <= {RD_WID{1'b0}};
else if (rd_en && ~rd_empty ) begin //rd_en使能,且fifo不是空的
rd_data <= fifo_arr[rd_addr];
end
end
endmodule
Wr_logic模块的逻辑接口如下图2-2.2所示,基本逻辑功能在2.1.1中描述。
核心RTL代码如下:
【1】归一化格雷码输出
//---------------------------------------
// wr_addr_out_gray的归一化处理
// 主要实现奇数圈时,格雷码最高位的取反,而保持其他bit不变
//---------------------------------------
always @ (posedge wr_clk, negedge rst_n) begin
if (!rst_n)
wr_addr_out_gray_normal <= {ADDR_WID{1'b0}};
else if (circle_high)
wr_addr_out_gray_normal <= {~wr_addr_out_gray[ADDR_WID - 1], wr_addr_out_gray[ADDR_WID - 2 : 0]};
else if (!circle_high)
wr_addr_out_gray_normal <= wr_addr_out_gray;
end
其中,circle_high为wr_logic模块的奇偶圈标志,为1表示十进制写指针在奇数圈,初始值为0。如前所述,当写指针1个轮回时,其格雷码地址除最高位相反,其余位与在前一圈是相同的。否则,在偶数轮回时,写指针格雷码最高位0.
【2】写指针增加
//-------------------------------------
// 写时钟域地址回环逻辑,在wr_en有效时,
// 地址按照FIFO_DEEPTH(ADDR_NUM)自增,当每一次增加到等于FIFO_DEEPTH时,
// 二进制地址清零,但在格雷码地址上进行如下操作:
// 将最高位取反,其他位数值保持不变
//-------------------------------------
assign circle_high = wr_circle_flag ; // circle_high有效区间应该和二进制地址同步(0-1023之间为1或者0)
always @ (posedge wr_clk, negedge rst_n) begin
if (!rst_n) begin
wr_addr <= {ADDR_WID{1'b0}};
wr_circle_flag <= 1'b0;
end else if (wr_en && wr_addr == ADDR_NUM) begin
wr_addr <= {ADDR_WID{1'b0}};
wr_circle_flag <= ~wr_circle_flag;
end else if (wr_en && ~wr_full ) begin
if ((rd_circle_high_sync == ~circle_high && rd_addr_sync_gray_bin > wr_addr) || rd_circle_high_sync == circle_high)
wr_addr <= wr_addr + 1'b1;
end
end
其中,(rd_circle_high_sync == ~circle_high && rd_addr_sync_gray_bin > wr_addr) || rd_circle_high_sync == circle_high)
是关键,前一半实际上做了写保护,即写满之后,写指针不再增加(最多增加到与rd_addr_sync_gray_bin相等),同时又限制了写指针的多余增加(因为wr_full是有延迟的,在这段延迟的时间里若wr_en有效,实际已经写满但是写指针还在增加,就会导致逻辑上的错误)。
所以,后半句语句是什么意思呢?这里给大家留一个思考问题。
NOTE:读写分开进行时,经过同步后的rd_clk中的格雷码地址rd_addr_sync_gray_bin转换成十进制地址后,虽然存在延迟,但在很长一段区间内,rd_addr是不变的(读完后读指针不变,写指针在变化),所以对多地址的读写,不会影响wr_addr的增加。
【3】写满标志wr_full产生
//---------------------------------------
// 写满判断及满标志产生
// 采用格雷码(归一化之后)判断
//---------------------------------------
always @ (posedge wr_clk, negedge rst_n) begin
if (!rst_n)
wr_full <= 1'b0;
else if (wr_addr_out_gray_normal[ADDR_WID - 1] == rd_addr_sync_gray[ADDR_WID - 1]) // 这个状态实际上不做判断,因为同等情况已经在读时钟域上进行判断
wr_full <= 1'b0; // 且此时只影响读空标志的产生
else if ((wr_en_latch | wr_en) && wr_addr_out_gray_normal[ADDR_WID - 1] == ~rd_addr_sync_gray[ADDR_WID - 1]) begin // wr_addr_out_gray_normal在wr_en之外,需要锁存wr_en
if (wr_addr_out_gray_normal[ADDR_WID - 2 : 0] == rd_addr_sync_gray[ADDR_WID - 2 : 0])
wr_full <= 1'b1;
else
wr_full <= 1'b0;
end else if (rd_en_sync)
wr_full <= 1'b0;
end
写满标志wr_full的逻辑是本设计的关键之一,首先明确,wr_full是在写操作的时候产生的(即写使能有效),且判满标志采用格雷码判断。因为wr_en下,归一化的格雷码地址wr_addr_out_gray_normal是延时了1clk,所以得到wr_en_latch,使得wr_addr_out_gray_normal包络在写使能区间内。此外,写指针要比读指针多一个轮回(即写指针已经充满fifo深度从头开始追赶读指针)。
然,wr_full持续多长时间呢?(即何时清零)
有思路是通过if (wr_addr_out_gray_normal[ADDR_WID - 2 : 0] == rd_addr_sync_gray[ADDR_WID - 2 : 0])中的else语句来判断,即除去最高位外格雷码相等,但实际上,else中的条件在整个else if ((wr_en_latch | wr_en) && wr_addr_out_gray_normal[ADDR_WID - 1] == ~rd_addr_sync_gray[ADDR_WID - 1])
大前提都不满足(此时写使能已经失效),故这种判断有待改进。本设计采用另外一种思路:当写满标志产生后,不能再写入(写满保护),直到再次读的时候,将wr_full清零。
RD_LOGIC模块类似Wr_logic模块,基本功能需求2.1.1中描述,以下对核心部分进行阐述。
接口模块如下图2-2.3所示。
【1】归一化格雷码地址产生
此部分逻辑与写时钟域的处理相同。
【2】读地址增加
//-----------------------------------
// rd_en使能,读地址增加
//-----------------------------------
reg rd_circle_flag; // 写指针循环圈数标志位,为1表示表示奇数圈,即映射格雷码,为0表示偶数圈,即正常格雷码;
assign circle_high = rd_circle_flag;
always @ (posedge rd_clk, negedge rst_n) begin
if (!rst_n) begin
rd_addr_in_bin <= {ADDR_WID{1'b0}};
rd_circle_flag <= 1'b0;
end else if (rd_en && rd_addr_in_bin == ADDR_NUM) begin
rd_addr_in_bin <= {ADDR_WID{1'b0}};
rd_circle_flag <= ~rd_circle_flag;
end else if (rd_en && ~rd_empty && ( (rd_addr_in_bin < wr_addr_sync_gray_bin && circle_high == wr_circle_high_sync) // 同圈数,读小于写 ,不同圈,读地址大于写
|| ( circle_high == ~wr_circle_high_sync))) // 读地址增加的条件(重要)
rd_addr_in_bin <= rd_addr_in_bin + 1'b1;
end
关键代码(rd_en && ~rd_empty && ((rd_addr_in_bin < wr_addr_sync_gray_bin && circle_high == wr_circle_high_sync) || ( circle_high == ~wr_circle_high_sync))
,仔细观察,实际上和写时钟域构成了互补逻辑,因为只有当读写指针在同奇偶圈轮回时才进行rd_empty的判断。((rd_addr_in_bin < wr_addr_sync_gray_bin && circle_high == wr_circle_high_sync) || ( circle_high == ~wr_circle_high_sync)
的前半句是为了限制rd_addr_in_bin超过写指针从而实现读空保护。后半句是说,不同圈时,读地址可以一直递增到fifo_deepth。后半句语句不可少,否则地址增加条件会缺少。
NOTE:这里的读空保护,因为fifo被写过的地址对应的数据依然还是存在的,所以读空标志产生后,如果读使能有效且rd_empty无效,则是可以读出数据的,所以需要在检测到empty有效之后再次使能读信号。这里的保护应该理解为不多读。
【3】读空标志产生
//-----------------------------------
// rd_empty产生逻辑
//-----------------------------------
always @ (posedge rd_clk, negedge rst_n) begin
if (!rst_n)
rd_empty <= 1'b0;
else if (rd_addr_out_gray_normal[ADDR_WID - 1] == ~wr_addr_sync_gray[ADDR_WID - 1]) // 这个状态实际上不做判断,因为同等情况已经在读时钟域上进行判断
rd_empty <= 1'b0;
else if ((rd_en | rd_en_latch) && rd_addr_out_gray_normal[ADDR_WID - 1] == wr_addr_sync_gray[ADDR_WID - 1] ) begin // 格雷码地址完全相等,则读空标志产生
if ( rd_addr_out_gray_normal[ADDR_WID - 2 : 0] == wr_addr_sync_gray[ADDR_WID - 2 : 0])
rd_empty <= 1'b1;
else
rd_empty <= 1'b0; // 该条件实际达不到,故清零rd_empty不可靠(rd_en此时已经等于零)
end else if (wr_en_sync) // 当再次写入时,则非空
rd_empty <= 1'b0;
end
读空标志rd_empty产生于,读使能区间内读写指针格雷码相同的情况,类似地,rd_empty的持续时间也是由下一个有效的wr_en信号确定。
模块主要实现数据的打拍寄存功能,用于设计中的异步时钟域数据的同步处理以及latch信号的产生。逻辑接口如下:
RLT代码如下:
always @ (*) begin
addr_ff[0 +: ADDR_WID] = addr_in;
vld_ff[0] = vld_in;
end
genvar j;
generate for (j = 1; j < PIPE_NUM; j = j + 1) begin : U_ADDR_SYNC
always @ (posedge clk, negedge rst_n) begin
if (!rst_n)
addr_ff[j * ADDR_WID +: ADDR_WID] <= {ADDR_WID{1'b0}};
else // if (vld_ff[j - 1])
addr_ff[j * ADDR_WID +: ADDR_WID] <= addr_ff[(j - 1) * ADDR_WID +: ADDR_WID];
end
always @ (posedge clk, negedge rst_n) begin
if (!rst_n)
vld_ff[j] <= 1'b0;
else
vld_ff[j] <= vld_ff[j - 1];
end
end
endgenerate
//取出打拍后的地址数据
assign addr_sync = addr_ff[(PIPE_NUM - 1) * ADDR_WID +: ADDR_WID];
assign vld_out = vld_ff[PIPE_NUM - 1];
主要通过generate for实现。其相关语法规则可自行百度或者参阅----2001 V:IEEE1364-2001 Verilog lrm----Verilog_1364-2001标准链接
主要包括十进制地址和格雷码地址的相互转换。De_2_gray主要用于异步时钟域的传输,Gray_2_de用于转换到异步域之后,限制该域的指针的增加,目的是为了作空满保护。两个模块为啥是这样,各位可以举例试一试便知,这里不做详细阐述。
【1】bin_2_gray的RTL建模
// reg [DAT_WID - 1 : 0] shift_rgt_reg;
// always @ (posedge clk, negedge rst_n) begin
// if (!rst_n)
// shift_rgt_reg <= {DAT_WID{1'b0}};
// else
// shift_rgt_reg <= data_in_bin;
// end
assign data_out_gray = data_in_bin ^ data_in_bin >> 1;
注意,使用非阻塞赋值会导致gray的产生滞后一个clk。
【2】gray_2_bin的RTL建模
genvar i;
generate for (i = 0; i < ADDR_WID; i = i + 1) begin : GRAY_2_BINA
always @ (*) begin
addr_bin_out[i] = ^(addr_gray_in >> i);
end
end
endgenerate
注意,考虑代码的可综合性,使用generate for语句实现循环,不推荐使用在begin end模块中直接使用for循环(不一定可综合)作上述操作。
剩余部分将于下节讨论。