【芯片前端】保持代码手感——异步FIFO全解析

前言

关于FIFO和异步处理我已经写过很多东西了:

进阶之路——二进制与格雷码的相互转换模块设计

【异步FIFO的一些小事·0】异步FIFO同步化设计

【异步FIFO的一些小事·1】空满判断与格雷码

【异步FIFO的一些小事·2】异步FIFO中异步走线延时约束的一些思考

【异步FIFO的一些小事·3】异步FIFO中指针走线延时的一些思考

【异步电路碎碎念5】 —— 跨异步处理的几个注意事项

【芯片前端】保持代码手感——同步FIFO

这次因为保持代码手感的需要,重新写一次异步fifo。写的过程突然感觉自己成长了许多,毕业时候抠抠索索的照着教程写了好几天,到今天心里合了一下时序一个多小时就手撕代码完成,看来这几年班不是白上的啊~

整体结构

整体的思路很常规,模块分为四个部分:写通路控制、读通路控制、异步单元、双口RAM。

写通路控制:负责产生ram写地址和写有效,产生wfull信号;

读通路控制:负责产生ram读地址和读有效,产生rempty信号;

异步单元:负责地址指针的跨异步处理;

双口RAM:负责存储数据;

整体结构图如下:

顶层如下:

module asyn_fifo#(
	parameter	WIDTH = 8,
	parameter 	DEPTH = 16
)(
	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
);

//===================================
//写通路控制模块
//===================================
wire [$clog2(DEPTH) :0]raddr_sync;
wire wenc;
wire [$clog2(DEPTH) :0]waddr;

wctrl_path #(
	.DEPTH(DEPTH))
u_wctrl_path(
	.wclk(wclk),
	.wrstn(wrstn),
	.winc(winc),
	.raddr_sync(raddr_sync),
	.wenc(wenc),
	.waddr(waddr),
	.wfull(wfull)
);

//===================================
//读通路控制模块
//===================================
wire [$clog2(DEPTH) :0]waddr_sync;
wire renc;
wire [$clog2(DEPTH) :0]raddr;

rctrl_path #(
	.DEPTH(DEPTH))
u_rctrl_path(
	.rclk(rclk),
	.rrstn(rrstn),
	.rinc(rinc),
	.waddr_sync(waddr_sync),
	.renc(renc),
	.raddr(raddr),
	.rempty(rempty)
);

//===================================
//ram控制
//===================================
dual_port_RAM #(
	.DEPTH(DEPTH),
	.WIDTH(WIDTH))
u_dual_port_RAM(
	.wclk(wclk),
	.wenc(wenc),
	.waddr(waddr[$clog2(DEPTH)-1:0]),
	.wdata(wdata),
	.rclk(rclk),
	.renc(renc),
	.raddr(raddr[$clog2(DEPTH)-1:0]),
	.rdata(rdata)
);
 
//===================================
//写跨异步
//===================================
gary_sync_cell#(
	.WIDTH($clog2(DEPTH)+1),
	.SYNC_CYC(3))
u_wgary_sync_cell(
	.in_clk(wclk),
	.in_rst_n(wrstn),
	.out_clk(rclk),
	.out_rst_n(rrstn),
	.in_data(waddr),
	.out_data(waddr_sync)
);

//===================================
//读跨异步
//===================================
gary_sync_cell#(
	.WIDTH($clog2(DEPTH)+1),
	.SYNC_CYC(3))
u_rgary_sync_cell(
	.in_clk(rclk),
	.in_rst_n(rrstn),
	.out_clk(wclk),
	.out_rst_n(wrstn),
	.in_data(raddr),
	.out_data(raddr_sync)
);

endmodule

模块说明

wctrl_path

我们来完成wctrl_path的构建,首先明确写这个模块的接口:

module wctrl_path#(
	parameter	WIDTH = 8,
	parameter 	DEPTH = 16,
	parameter   ADDR_WD = $clog2(DEPTH)
)(
	input 					wclk,   
	input 					wrstn,
	input 					winc,
	input  [ADDR_WD   :0]   raddr_sync,
	output                  wenc,
	output [ADDR_WD   :0]	waddr,
	output                  wfull
);

写时钟和写复位肯定是必须的,winc是外部的写有效。还有一个必要的输入信号是raddr_sync信号,这个信号的作用是和waddr一起产生写侧看到的fifo_cnt信号,进而得到wfull信号。那么考点1来了:为什么要在写时钟域产生wfull信号,在读时钟域产生rempty信号?

在“【异步FIFO的一些小事·1】空满判断与格雷码”这个博客中已经做过说明,简单而言就是:在写时钟域看到的读指针是有延迟的,对于wfull而言,晚一些看到有数据被读取走也不会影响数据,最多就是wfull信号晚一些撤销而已,而wfull晚撤销不会是数据被覆盖,只会影响到性能(多反压了一会),而性能的问题是可以通过计算一个合理的异步fifo深度开销进行弥补的。同理在读时钟域看到写指针也是有延迟的,因此rempty可能撤销不及时,不及时也没关系最多就是等一会反应过来再读,而不会读取错误数据。

好的,输入信号分析完成,输出信号显然有给ram的wenc和waddr,wdata直接从顶层过去不从这里过手了。还有就是刚刚说的wfull信号。

在“【芯片前端】保持代码手感——同步FIFO”里提到过,考点2:fifo中计算读写地址会用位宽扩一比特的计数器,最高比特作为标志位来判断绕圈,那么把这个模块做一下:

module fifo_cnt#(
	parameter 	DEPTH = 8,
	parameter   WITDH = $clog2(DEPTH)
)(
	input 				clk,
	input 				rst_n,
	input 			   	en,
	output [WITDH :0]	cnt
);

reg  [WITDH   :0]cnt;
wire             cnt_d_h;
wire [WITDH -1:0]cnt_d_l;

assign cnt_d_h = (cnt[WITDH-1:0] == DEPTH-1) ? ~cnt[WITDH] : cnt[WITDH];
assign cnt_d_l = (cnt[WITDH-1:0] == DEPTH-1) ? 0 : cnt[WITDH-1:0] + {{(WITDH-1){1'b0}}, 1'b1};
always @(posedge clk or negedge rst_n)begin
	if(~rst_n)  cnt <= 0;
	else if(en) cnt <= {cnt_d_h, cnt_d_l};
end

endmodule

基于这个模块可以轻松的产生waddr,那么进一步得到wenc和wfull的逻辑就非常简单了:

//得到写ram的waddr,fifo_cnt是最高比特为标志位的特殊计数器
assign wenc  = winc && (!wfull);
fifo_cnt #(
	.DEPTH(DEPTH)
)u_fifo_cnt(
	.clk(wclk),
	.rst_n(wrstn),
	.en(wenc),
	.cnt(waddr)
);

//生成wfull信号,wfull信号必须在写时钟域生成,因为读信息延迟到达写时钟域,也不会对wfull信息有影响,不会有数据丢失
wire [ADDR_WD :0]fifo_cnt = (waddr[ADDR_WD] == raddr_sync[ADDR_WD]) ? waddr[ADDR_WD-1:0] - raddr_sync[ADDR_WD-1:0]:
				            (waddr[ADDR_WD-1:0] + DEPTH - raddr_sync[ADDR_WD-1:0]);
assign wfull = (fifo_cnt == DEPTH);

endmodule 

注意这里的waddr还是扩充了一比特的地址,送给ram的时候要取[DEPTH -1:0],而送到跨异步模块时就直接全位宽送,因为计算fifo_cnt要用到全位宽。

rctrl_path

和wctrl_path的思路一样,不赘述:

module rctrl_path#(
	parameter	WIDTH = 8,
	parameter 	DEPTH = 16,
	parameter   ADDR_WD = $clog2(DEPTH)
)(
	input 					rclk,   
	input 					rrstn,
	input 					rinc,
	input  [ADDR_WD   :0]   waddr_sync,
	output                  renc,
	output [ADDR_WD   :0]	raddr,
	output                  rempty
);

//得到读ram的waddr,fifo_cnt是最高比特为标志位的特殊计数器
assign renc  = rinc && (!rempty);
fifo_cnt #(
	.DEPTH(DEPTH)
)u_fifo_cnt(
	.clk(rclk),
	.rst_n(rrstn),
	.en(renc),
	.cnt(raddr)
);

//生成rempty信号,rempty信号必须在读时钟域生成,因为写信息延迟到达读时钟域,也不会对rempty信息有影响,最多影响读性能
wire [ADDR_WD :0]fifo_cnt = (waddr_sync[ADDR_WD] == raddr[ADDR_WD]) ? waddr_sync[ADDR_WD-1:0] - raddr[ADDR_WD-1:0]:
			                (waddr_sync[ADDR_WD-1:0] + DEPTH - raddr[ADDR_WD-1:0]);
assign rempty = (fifo_cnt == 0);

endmodule 

gary_sync_cell

gary_sync_cell专门用来计数器跨异步,思路很简单:二进制输入,转格雷码打拍,跨异步打三拍,格雷码转回二进制输出。那么考点3又来了:为什么异步fifo里要用格雷码跨异步?

基于之前的积累“进阶之路——二进制与格雷码的相互转换模块设计” ,二进制转格雷码和格雷码转二进制的模块直接做:

module b2g_conv #(parameter SIZE = 4
)(
    output [SIZE -1:0] gray,
    input  [SIZE -1:0] binary
);

assign gray = (binary >> 1) ^ binary;

endmodule


module g2b_conv #(
	parameter SIZE = 4
)(
    output reg [SIZE -1:0] binary,
    input      [SIZE -1:0] gray
);

integer k;
always @(gray)
begin
    for (k = 0; k < SIZE; k = k + 1)
        binary[k] = ^(gray >> k);
end

endmodule

OK,那么下一步就是对比特跨异步的问题了。下一个考点4自然就是:多比特如何跨异步?那在这里就不啰嗦了【异步电路碎碎念4】 —— 跨异步的处理方法已经写过 ,欢迎指正。

要做多比特跨异步,当然要先有单比特跨异步模块:

module sync_cell #(
	parameter SYNC_CYC = 2
)(
	input  clk,
	input  rst_n,
	input  in,
	output out
);
wire [SYNC_CYC :0]in_dff;
assign in_dff[0] = in;
assign out = in_dff[SYNC_CYC];

genvar i;
generate
    for(i=1; i<=SYNC_CYC; i=i+1)begin: inst_rtl
        dffr u_dffr[i](clk, rst_n, in_dff[i-1], in_dff[i]);
    end
endgenerate
endmodule

拍数可配,默认是2拍建议配置为3拍,那么考点5:为什么现在跨异步打拍要打3拍?

基于单比特跨异步单元,我们进一步得到多比特跨异步单元:

//将格雷码在目的时钟域打三拍跨异步
wire [WIDTH -1:0]out_data_gray;
genvar i;
generate
    for(i=0; i<WIDTH; i=i+1)begin: inst_rtl
        sync_cell #(
	        .SYNC_CYC(SYNC_CYC))
        u_sync_cell(
	        .clk(out_clk),
	        .rst_n(out_rst_n),
	        .in(in_data_gray_ff[i]),
	        .out(out_data_gray[i])
        );
    end
endgenerate

注意千万要用目的时钟打拍啊。

ok,因此完成的gray_sync_cell的代码就有了:

module gary_sync_cell#(
	parameter WIDTH = 8,
	parameter SYNC_CYC = 3
)(
	input in_clk,
	input in_rst_n,
	input out_clk,
	input out_rst_n,
	input [WIDTH -1:0] in_data,
	output[WIDTH -1:0] out_data
);

//将输入转为格雷码并在源时钟域打一拍
wire [WIDTH -1:0]in_data_gray;
wire [WIDTH -1:0]in_data_gray_ff;
b2g_conv #(
	.SIZE(WIDTH))
u_b2g_conv(
	.binary(in_data),
	.gray(in_data_gray)	
);

dffr#(
	.WIDTH(WIDTH))
u_in_data_dffr(
	.clk(in_clk),
	.rst_n(in_rst_n),
	.d(in_data_gray),
	.q(in_data_gray_ff)
);

//将格雷码在目的时钟域打三拍跨异步
wire [WIDTH -1:0]out_data_gray;
genvar i;
generate
    for(i=0; i<WIDTH; i=i+1)begin: inst_rtl
        sync_cell #(
	        .SYNC_CYC(SYNC_CYC))
        u_sync_cell(
	        .clk(out_clk),
	        .rst_n(out_rst_n),
	        .in(in_data_gray_ff[i]),
	        .out(out_data_gray[i])
        );
    end
endgenerate

//将格雷码转化为二进制
g2b_conv #(
	.SIZE(WIDTH))
u_g2b_conv(
	.gray(out_data_gray),
    .binary(out_data)
);

endmodule

双口RAM

用的是默认代码:

module dual_port_RAM #(parameter DEPTH = 16,
					   parameter WIDTH = 8)(
	 input wclk	,
	 input wenc	,
	 input [$clog2(DEPTH)-1:0] waddr	,
	 input [WIDTH-1:0] wdata	,
	 input rclk	,
	 input renc	,
	 input [$clog2(DEPTH)-1:0] raddr	,
	 output reg [WIDTH-1:0] rdata
);

reg [WIDTH-1:0] RAM_MEM [0:DEPTH-1];

always @(posedge wclk) begin
	if(wenc)
		RAM_MEM[waddr] <= wdata;
end 

always @(posedge rclk) begin
	if(renc)
		rdata <= RAM_MEM[raddr];
end 

endmodule  

以上,完整的代码呈现。

波形验证

因为主要是叙述个思路,没有做太细致的波形验证,就用auto_verification生成了个简单的验证环境,没有这个工具的请参见【芯片前端】一键生成简易版本定向RTL验证环境的脚本——auto_verification,工具有点些修改,最新的链接是:链接:https://pan.baidu.com/s/1CMptxDGketFHuLeNOLcV0Q,提取码:t6o8。

把所有文件放置于async_fifo目录下,键入auto_verification -f async_fifo,生成仿真环境,简单修改testbench,得到如下波形:

真的是很随意的仿了一下,如果大家发现有问题请不吝赐教~感谢~

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SV小项目-异步FIFO是一个简单的项目,旨在让学生理解FIFO缓存的原理和实现方式。 FIFO缓存是一种常用的数据存储方式,可以用于解决数据传输时的不匹配问题。在异步FIFO中,数据的写入和读取是异步的,意味着数据可以在任何时候写入或读取。这种异步的方式可以增加FIFO的灵活性,并且允许数据的写入和读取在不同的时钟域上应用。 这个小项目的主要目标是实现一个基于Verilog异步FIFO模块,包括以下功能: 1. 内部缓存的储存和检索 2. 空和满指示器的生成 3. 数据的写入和读取 对于写入操作,当FIFO未满时,它将数据存储在内部缓存中,并更新其指针以指向下一个空位置。当缓存已满时,写入操作将被忽略,并且FIFO满指示器将被设置为高电平。同样,读取操作从内部缓存中检索数据,并将其指针更新以指向下一个位置。当缓存为空时,读操作将被忽略,并且FIFO空指示器将被设置为高电平。 在设计过程中,需要考虑解决的问题包括时序和异步信号的同步。时序问题可以通过FIFO指针和状态机解决,而异步信号可以通过信号同步器进行同步。此外,还需要考虑FIFO的读写顺序和存储器的尺寸,并确保FIFO模块的有效性和可靠性。 总之,通过实现SV异步FIFO项目,学生可以加深对FIFO缓存的理解,并学习基于Verilog的设计和实现。此外,学生还可以通过在项目中遇到的挑战来提高他们的编程和设计技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尼德兰的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值