前言
关于FIFO和异步处理我已经写过很多东西了:
【异步FIFO的一些小事·2】异步FIFO中异步走线延时约束的一些思考
【异步FIFO的一些小事·3】异步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,得到如下波形:

真的是很随意的仿了一下,也没有在网站上验证答案(因为觉得网站不太靠谱),如果大家发现有问题请不吝赐教~感谢~



京公网安备 11010502036488号