秋招手撕代码:异步fifo(verilog)及代码解析

  • 1、设计思路
  • 2、设计过程中遇到的一些问题
  • 3、verilog代码和仿真文件

1、设计思路
关键的点:读写控制信号的生成、读写地址、状态产生。
在这里插入图片描述
(1)读控制(ren)、写控制(wen)的生成:当外部输入的wr_en=1且full=0时,也就是外部让你写且你的fifo现在没有写满的情况下,你就可以对fifo进行写操作。用verilog表示即为
wen=!full&&wr_en;
读控制同理:ren=(!empty)&&rd_en;
(2)读地址(raddr)、写地址(waddr)
这块比较简单,时钟上升沿到来了且读使能(ren)或者写使能(wen)有效,读地址(读指针)+1或者写地址(写指针)+1;
对应到verilog代码为:
always@(posedge clk)
if(reset)
waddr<=5’b0;
else if(wen)
waddr<=waddr+1;

always@(posedge clk)
if(reset)
raddr<=5’b0;
else if(ren)
raddr<=raddr+1;

(3)状态判断
要对状态判断就需要计算出读写地址的差值,因为是异步的,所以需要进行跨时钟域。即计算wr_gap的时候需要将读地址同步到写时钟域来,具体怎么将读地址同步过来呢?
因为读地址是自加1的,这个特性就可以使用格雷码,即将二进制转换成格雷码,这样相邻两位就只有1bit发生变化。
二进制转换成格雷码的方法:将二进制右移移位后,和右移前的数值进行异或即可。

//读地址从二进制转变成格雷码
always @(posedge rd_clk or posedge rd_reset)
if(rd_reset)
	raddr_gray<=0;
else raddr_gray<=raddr^(raddr>>1);

转换成格雷码后,就可以通过打两拍的方法进行同步。但是计算wr_gap的时候因为waddr是二进制,所以将同步过来的读地址的格雷码再转换为二进制,具体怎么将格雷码转化为二进制呢?
将格雷码的最高位作为二进制的最高位,然后将格雷码的次高位和二进制的最高位按位异或获得二进制的次高位,依次类推,具体见下图:

在这里插入图片描述

//将从读时钟域同步到写时钟域的格雷码读地址转变成二进制读地址
always@(*)
	raddr_gray2bin={raddr_gray_syn2w1[4],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1]^raddr_gray_syn2w1[0]};

关于间隔计算见下图:需要提醒读者的是,wr_gap是计算还有几个空格子,rd_gap计算还有几个数据。因此可以这样理解(可能不是很准确只是一种记忆方法)wr_gap是用读地址减去写地址;rd_gap是用写地址减去读地址。具体见下图:
在这里插入图片描述
相关代码如下:

//wr_gap间隔计算
always@(*)
	if(raddr_gray2bin[4]^waddr[4])
		wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
	else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr;
//计算rd_gap
always@(*)
	rd_gap=waddr_gray2bin-raddr;

在此处我其实踩了一个坑,导致我搞了一个上午现象都不太对,在这里先买个关子,具体现象和错误原因我再下一个部分说。

2、设计过程中思考的一些问题
(1)先说说第二部分我踩的坑。
复位后,我的almost_full也有效,而且持续了一段时间为0,但是我写的代码理论上这里应该一直是15呀,然后almost_full应该是0才对。

always@(*)
	if(raddr_gray2bin[4]^waddr[4])
		wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
	else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr;

在这里插入图片描述
其实问题很智障,就是我的FIFO_DEEP是16,但是我的wr_gap的位宽是3:0;这样当我把FIFO_DEEP赋值给wr_gap的还是只会取低4位,就全是0。比较推荐大家看明白后自己动手敲一遍代码,这样印象会更加深刻。一会儿会把完整的代码贴出来。
(2)在计算wr_gap的时候需要将读时钟的地址同步过来,打两拍的话,同步过来的读地址会延迟两拍,如果要和当时的写地址进行计算,需要对同步过来的地址加2吗?

解析: 这里会涉及到一个问题:就是假空和假满的问题,体现了异步fifo的保守性,这样操作既不会引起上溢也不会引起下溢。 具体分析如下:计算wr_gap的时候,使用raddr_gray2bin来计算,raddr从读时钟域同步过来因为打了两拍会有延时,但是我们在计算的这一时刻是使用同步之前的读地址值(假设现在最高位为0,后面的数值为8)和当前时刻写地址值(假设最高位为1,后面的数值为6),如果这个值算出来是满足了almost_full(假设为3)的标准,我们就可以不继续往fifo里写数据。即是此时其实raddr在读时钟域已经更新为10了,真是的wr_gap应该是4,不满足almost_full的标准,但是前者也只是将almost_full提早有效了,并不会造成什么影响啊,不会产生上溢也不会产生下溢。(这段话可能看起来有点绕,但是如果你在写代码的时候也有这个疑惑,可以认真读一读),我是读这段话才想明白的。
在这里插入图片描述
(3)发现一个问题:将地址转变成格雷码的形式后采用打两拍的方法意味着是慢到快时钟域。意味着写时钟的频率比读时钟的频率低?那么如果写时钟的频率比读时钟的频率高应该怎么办呢?
这样的话就是只就是慢时钟域能采到那一时刻写时钟域(快时钟域)的变化,不会影响什么,对fifo状态的产生不会有影响,也就是不会产生上溢和下溢。

(4)为什么选择格雷码?
选择格雷码的原因:可以将多比特跨时钟域转换成单比特,降低亚稳态发生的概率(这里有一个隐含的条件,就是读写地址转换成格雷码进行时钟域的前提是它的地址是相邻两个数之间只变化1,因此对于多bit总线传输不能使用格雷码,因为不能保证是相邻两位只变化1)

(5)如何设计depth不是2的幂次的异步FIFO?比如深度为6(这个问题是我看了公众号IC加油站才进行思考的,大家可以去关注一下,这个人写的东西都很透彻,写了一系列关于跨时钟域的文章)
利用格雷码的对称性:
在这里插入图片描述
3、代码
(1)设计代码

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2021/08/03 20:55:16
// Design Name: 
// Module Name: asy_fifo
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 简单梳理一下fifo,同步fifo和异步fifo的差别在于计算读写地址的差距的时候,
//同步fifo是可以直接进行计算;异步fifo是需要垮时钟域再进行的,垮时钟域选择格雷码。
//


module asy_fifo(
//fifo write
input wr_clk,
input wr_en,
input wr_reset,
input [15:0]wr_data,
output reg full,
output reg almost_full,
//fifo read
input rd_clk,
input rd_reset,
input rd_en,
output [15:0]rd_data,
output reg empty,
output reg almost_empty
    );
parameter FIFO_DEEP=16;
parameter ALMOST_EMPTY_GAP=3;
parameter ALMOST_FULL_GAP=3;
wire [3:0]rd_addr;
wire [3:0]wr_addr;
reg [4:0]wr_gap;
reg [4:0]rd_gap;
//reg [15:0]data_out_temp;
reg [4:0]waddr;	//拓展一位,最高位用来判断读写指针是否在同一轮
reg [4:0]raddr;
reg [4:0]waddr_gray;
reg [4:0]waddr_gray_syn2r;
reg [4:0]waddr_gray_syn2r1;
reg [4:0]raddr_gray;
reg [4:0]raddr_gray_syn2w;
reg [4:0]raddr_gray_syn2w1;
reg [4:0]raddr_gray2bin;
reg [4:0]waddr_gray2bin;
wire wen;
wire ren;
//写控制逻辑
assign wen=!full&&wr_en;
always@(posedge wr_clk)
if(wr_reset)
waddr<=5'b0;
else if(wen)
	waddr<=waddr+1;


//地址
/读地址变成格雷码并且同步到写时钟域来
//读地址从二进制转变成格雷码
always @(posedge rd_clk)
if(rd_reset)
	raddr_gray<=0;
else raddr_gray<=raddr^(raddr>>1);
//将读地址的格雷码同步到写时钟域来(打两拍)
always@(posedge wr_clk)
if(wr_reset)begin
		raddr_gray_syn2w <=5'b0;
		raddr_gray_syn2w1<=5'b0;
	end
else begin
		raddr_gray_syn2w<=raddr_gray;
		raddr_gray_syn2w1<=raddr_gray_syn2w;
end
//将从读时钟域同步到写时钟域的格雷码读地址转变成二进制读地址
always@(*)
	raddr_gray2bin={raddr_gray_syn2w1[4],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1],
	raddr_gray_syn2w1[4]^raddr_gray_syn2w1[3]^raddr_gray_syn2w1[2]^raddr_gray_syn2w1[1]^raddr_gray_syn2w1[0]};
//wr_gap间隔计算
/*  always@(raddr_gray2bin or waddr or rd_reset or wr_reset)
	if(~rd_reset && ~wr_reset)begin
		if(raddr_gray2bin[4]^waddr[4])
			wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
		else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr; 
	end
	else begin
		wr_gap <= 4'hf;	
	end */
	
/* assign wr_gap = ((raddr_gray2bin[4]^waddr[4]) ? (raddr_gray2bin[3:0]-waddr[3:0]) : (16 + raddr_gray2bin-waddr)); */
// always@(posedge wr_clk)
// if(wr_reset)
	// wr_gap<=4'd15;
// else if(raddr_gray2bin[4]^waddr[4])
		// wr_gap<=raddr_gray2bin[3:0]-waddr[3:0];
	// else wr_gap<=FIFO_DEEP+raddr_gray2bin-waddr;	 
	
	//间隔计算
always@(*)
	if(raddr_gray2bin[4]^waddr[4])
		wr_gap=raddr_gray2bin[3:0]-waddr[3:0];
	else wr_gap=FIFO_DEEP+raddr_gray2bin-waddr;

	
	
// assign wr_gap=(raddr_gray_syn2w1[4]^waddr[4])?(raddr_gray_syn2w1[3:0]-waddr[3:0]):(FIFO_DEEP+raddr_gray_syn2w1-waddr);
//状态判断
//almost_full产生
always@(posedge wr_clk)
if(wr_reset)
	almost_full<=1'b0;
else if(wr_gap<ALMOST_FULL_GAP)
	almost_full<=1'b1;
else almost_full<=1'b0;
//full信号产生
always@(posedge wr_clk)
if(wr_reset)
	full<=1'b0;
else if(wr_gap==1&&wen)
	full<=1'b1;
else full<=1'b0;



///读时钟域//
//1、读控制
assign ren=(!empty)&&rd_en;

always@(posedge rd_clk)
if(rd_reset)
	raddr<=5'b0;
else if(ren)
	raddr<=raddr+1;

//判断状态需要对读写地址进行比较,因为是异步的,所以需要对地址进行垮时钟域处理
//将写地址同步到读时钟域

always@(posedge wr_clk)
if(wr_reset)
	waddr_gray<=5'b0;
else waddr_gray<=waddr^{1'b0,waddr[4:1]};

always@(posedge rd_clk)
if(rd_reset)begin
	waddr_gray_syn2r<=0;
	waddr_gray_syn2r1<=0;
end
else begin
waddr_gray_syn2r<=waddr_gray;
waddr_gray_syn2r1<=waddr_gray_syn2r;

end
//将同步过来的写地址转换为二进制
always@(*)
	waddr_gray2bin={
	waddr_gray_syn2r1[4],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3]^waddr_gray_syn2r1[2],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3]^waddr_gray_syn2r1[2]^waddr_gray_syn2r1[1],
	waddr_gray_syn2r1[4]^waddr_gray_syn2r1[3]^waddr_gray_syn2r1[2]^waddr_gray_syn2r1[1]^waddr_gray_syn2r1[0]	
	};
//计算rd_gap
always@(*)
	rd_gap=waddr_gray2bin-raddr;
	
	
//状态产生
//almost_empty
always@(posedge rd_clk)
	if(rd_reset)
		almost_empty<=1'b0;
	else if(rd_gap<ALMOST_EMPTY_GAP)
	//else if(rd_gap==3||rd_gap==2||rd_gap==1)
		almost_empty<=1'b1;
	else almost_empty<=1'b0;
	
//empty 
always@(posedge rd_clk)
if(rd_reset)
	empty<=1'b0;
else if(rd_gap==1&&ren)
	empty<=1'b1;
else empty<=1'b0;
assign wr_addr=waddr[3:0];
assign rd_addr=raddr[3:0];
ram ram (
  .clka(wr_clk),    // input wire clka
  .wea(wen),      // input wire [0 : 0] wea
  .addra(wr_addr),  // input wire [3 : 0] addra
  .dina(wr_data),    // input wire [15 : 0] dina
  .douta(),  // output wire [15 : 0] douta
  .clkb(rd_clk),    // input wire clkb
  .web(),      // input wire [0 : 0] web
  .addrb(rd_addr),  // input wire [3 : 0] addrb
  .dinb(),    // input wire [15 : 0] dinb
  .doutb(rd_data)  // output wire [15 : 0] doutb
);
/* ram your_instance_name (
  .clka(clka),    // input wire clka
  .wea(wea),      // input wire [0 : 0] wea
  .addra(addra),  // input wire [3 : 0] addra
  .dina(dina),    // input wire [15 : 0] dina
  .douta(douta),  // output wire [15 : 0] douta
  .clkb(clkb),    // input wire clkb
  .web(web),      // input wire [0 : 0] web
  .addrb(addrb),  // input wire [3 : 0] addrb
  .dinb(dinb),    // input wire [15 : 0] dinb
  .doutb(doutb)  // output wire [15 : 0] doutb
); */
	
	
endmodule

2、仿真文件

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2021/08/03 23:45:06
// Design Name: 
// Module Name: asy_fifo_tst
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//


module asy_fifo_tst();
reg        wr_clk      ;
reg        wr_en       ;
reg        wr_reset    ;
reg [15:0] wr_data     ;
reg        rd_clk      ;
reg        rd_reset    ;
reg        rd_en       ;
wire [15:0]rd_data     ;
wire       empty       ;
wire       almost_empty;
wire       full        ;
wire       almost_full ;

asy_fifo U_asy_fifo(
.wr_clk         (wr_clk       ), 
.wr_en          (wr_en        ),
.wr_reset       (wr_reset     ),
.wr_data        (wr_data      ),
.rd_clk         (rd_clk       ),
.rd_reset       (rd_reset     ),
.rd_en          (rd_en        ),
.rd_data        (rd_data      ),
.empty          (empty        ),
.almost_empty   (almost_empty ),
.full           (full         ),
.almost_full    (almost_full  )
);


initial
begin
wr_clk=1;
rd_clk=1;
rd_reset=1;
wr_reset=1;
wr_data=0;
wr_en=0;
rd_en=0;
# 40 
rd_reset=0;
wr_reset=0;
#40 wr_en=1;
#40 wr_data=1;//写使能后需要隔至少一个时钟周期再给数据,如果直接给数据,最开始的数据是读不出来的
#40 wr_data=1;
#40 wr_data=2;
#40 wr_data=3;
#40 wr_data=4;
#40 wr_data=5;
#40 wr_data=6;
#40 wr_data=7;
#40 wr_data=8;
#40 wr_data=9;
#40 wr_data=10;
#40 wr_data=11;
#40 wr_data=12;
#40 wr_data=13;
rd_en=1;
#40 wr_data=10;
#40 wr_en=0;
#100 rd_en=0;
end

always #20 wr_clk=~wr_clk;
always #10 rd_clk=~rd_clk;
endmodule

需要注意的地方:写使能后需要隔至少一个时钟周期再给数据,如果直接给数据,最开始的数据是读不出来的

(3)仿真现象
在这里插入图片描述
这篇文章的代码是参考《FPGA深度解析》。
大家在阅读的过程中,如果发现问题,欢迎批评指正。

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 以下是一个Verilog代码实现的异步FIFO: ``` module async_fifo #( parameter WIDTH = 8, parameter DEPTH = 16 ) ( input clk, input reset, input write_enable, input read_enable, input [WIDTH-1:0] din, output reg [WIDTH-1:0] dout, output full, output empty ); reg [WIDTH-1:0] mem [0:DEPTH-1]; reg [31:0] write_pointer, read_pointer; reg [31:0] count; always @(posedge clk) begin if (reset) begin write_pointer <= 0; read_pointer <= 0; count <= 0; end else if (write_enable && !full) begin mem[write_pointer] <= din; write_pointer <= (write_pointer == DEPTH-1) ? 0 : write_pointer + 1; count <= count + 1; end else if (read_enable && !empty) begin dout <= mem[read_pointer]; read_pointer <= (read_pointer == DEPTH-1) ? 0 : read_pointer + 1; count <= count - 1; end end assign full = (count == DEPTH); assign empty = (count == 0); endmodule ``` ### 回答2: 下面是一个简单的Verilog代码示例,实现了一个4位异步FIFO。 ```verilog module AsyncFIFO ( input wire clk, input wire reset, input wire wr_en, input wire rd_en, input wire [3:0] data_in, output wire [3:0] data_out, output wire empty, output wire full ); parameter WIDTH = 4; parameter DEPTH = 8; reg [WIDTH-1:0] mem [DEPTH-1:0]; reg [WIDTH-1:0] wr_ptr; reg [WIDTH-1:0] rd_ptr; reg empty, full; always @(posedge clk or posedge reset) begin if (reset) begin wr_ptr <= 0; rd_ptr <= 0; empty <= 1; full <= 0; end else if (wr_en && !full) begin mem[wr_ptr] <= data_in; wr_ptr <= wr_ptr + 1; empty <= 0; if (wr_ptr == (DEPTH - 1)) full <= 1; end else if (rd_en && !empty) begin data_out <= mem[rd_ptr]; rd_ptr <= rd_ptr + 1; full <= 0; if (rd_ptr == (DEPTH - 1)) empty <= 1; end end endmodule ``` 这个异步FIFO模块具有以下端口: - clk:时钟信号 - reset:复位信号 - wr_en:写使能信号 - rd_en:读使能信号 - data_in:输入数据信号 - data_out:输出数据信号 - empty:空状态信号 - full:满状态信号 当写使能信号wr_en有效且FIFO不满时,输入数据data_in被写入FIFO中,并且写指针wr_ptr递增。如果写指针达到FIFO深度的最大值,则FIFO被标记为满状态。当写使能信号不被激活时,数据不被写入。 当读使能信号rd_en有效且FIFO不为空时,FIFO中的数据被读取到输出端口data_out,并且读指针rd_ptr递增。如果读指针达到FIFO深度的最大值,则FIFO被标记为空状态。当读使能信号不被激活时,数据不被读取。 此外,在时钟上升沿和复位信号上升沿时,FIFO的各种状态被初始化或重置。 ### 回答3: 异步FIFO(First In First Out)是一种数据存储器件,用于在不同的时钟域之间传输数据。它可以在较快的时钟域中接收数据,并在较慢的时钟域中输出数据。下面是一个使用Verilog描述的异步FIFO代码示例: module async_fifo ( input wire clk_a, // 输入时钟域A input wire clk_b, // 输入时钟域B input wire reset_a, // 输入时钟域A复位信号 input wire reset_b, // 输入时钟域B复位信号 input wire data_in, // 输入数据信号 input wire write_en, // 写使能信号 input wire read_en, // 读使能信号 output wire data_out, // 输出数据信号 output wire full, // FIFO已满标志位 output wire empty // FIFO为空标志位 ); reg [7:0] memory [0:255]; // 存储数据的内存 reg [7:0] read_ptr_a; // 时钟域A读指针 reg [7:0] write_ptr_a; // 时钟域A写指针 reg [7:0] read_ptr_b; // 时钟域B读指针 reg [7:0] write_ptr_b; // 时钟域B写指针 always @(posedge clk_a or posedge reset_a) begin if (reset_a) begin read_ptr_a <= 0; write_ptr_a <= 0; end else begin if (write_en && !full) begin memory[write_ptr_a] <= data_in; write_ptr_a <= write_ptr_a + 1; end if (read_en && !empty) begin data_out <= memory[read_ptr_a]; read_ptr_a <= read_ptr_a + 1; end if (write_ptr_a - read_ptr_a >= 256) begin full <= 1; end else begin full <= 0; end if (write_ptr_a == read_ptr_a) begin empty <= 1; end else begin empty <= 0; end end end always @(posedge clk_b or posedge reset_b) begin if (reset_b) begin read_ptr_b <= 0; write_ptr_b <= 0; end else begin if (write_en && !full) begin memory[write_ptr_b] <= data_in; write_ptr_b <= write_ptr_b + 1; end if (read_en && !empty) begin data_out <= memory[read_ptr_b]; read_ptr_b <= read_ptr_b + 1; end if (write_ptr_b - read_ptr_b >= 256) begin full <= 1; end else begin full <= 0; end if (write_ptr_b == read_ptr_b) begin empty <= 1; end else begin empty <= 0; end end end endmodule 以上代码描述了一个具有两个时钟域的异步FIFO。它包含一个8位存储器来存储数据,并通过读写指针来读写数据。当FIFO已满时,将设置full标志位,当FIFO为空时,将设置empty标志位。在时钟域A和时钟域B中,根据写使能和读使能信号,数据将被写入或读取。在每个时钟域中都会进行读写指针的增加和数据的传输。在复位信号下,读写指针将被重新设置为0。 注意:以上代码仅为示例,具体实现可能因具体需求而有所不同,并且可能需要进一步的调试和完善。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

IC媛

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

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

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

打赏作者

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

抵扣说明:

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

余额充值