最近在学习Verilog的经典电路的代码,把碰到的同步异步FIFO的Verilog代码附上,并附有较为详细的代码注释,希望大家多看看注释,因为FIFO的代码已经很经典了,只有认真看注解才能更加利于理解,学到其中的电路设计的思想。
//同步FIFO设计(使用地址位扩展一位的方法来判断空满,与华为设计书上不一样)
module sync_fifo(clk, rst, wr_en, rd_en, data_in, data_out, empty, full);
input clk, rst, wr_en, rd_en;
input [7:0] data_in;
output empty, full;
output [7:0] data_out;
reg [7:0] mem[15:0]; //16*8的RAM
reg [7:0] data_out;
wire [3:0] w_addr, r_addr;
reg [4:0] r_addr_a, w_addr_a; //扩展一位
assign r_addr = r_addr_a[3:0];
assign w_addr = w_addr_a[3:0];
always @(posedge clk or negedge rst)
begin
if(!rst)
begin
r_addr_a <= 5'b0;
end
else
begin
if(rd_en == 1 && empty == 0) //不为空,且rd_en有效
begin
data_out <= mem[r_addr]; //读时候用的是未扩展的地址
r_addr_a <= r_addr_a+1'b1; //读是在扩展后的地址操作的
end
end
end
always @(posedge clk or negedge rst)
begin
if(!rst)
begin
w_addr_a <= 5'b0;
end
else
begin
if(wr_en == 1 && full == 0) //不为满,且wr_en有效
begin
mem[w_addr] <= data_in; //写时候用的是未扩展的地址
w_addr_a <= w_addr_a + 1'b1; //写是在扩展后的地址操作的
end
end
end
//r_addr_a和w_addr_a扩展后的读和写地址所有都相等的时候,为空
//r_addr_a和w_addr_a扩展后的读和写地址低四位相等,最高位相反,相当于读和写错过了一个FIFO深度,即错过了一轮,说明是满的
assign empty = (r_addr_a == w_addr_a)?1:0; //empty的判断是扩展后的地址
assign full = (r_addr_a[4] != w_addr_a[4] && r_addr_a[3:0]==w_addr_a[3:0])?1:0; //最高位相反,低四位相等,就说明满了
//tb中如果是先写再读应该是不存在w_addr_a[4]和r_addr_a[4] 大于10000,除非边写边读,如果边写边读还有full相当于快一轮
endmodule
//同步FIFO测试文件(使用地址位扩展一位的方法来判断空满,与华为设计书上不一样)
`timescale 10ns/1ns
module testfifo;
reg rst, clk, rd_en, wr_en; //需要加的激励信号
reg [7:0] data_in; //也是需要加的激励信号
wire [7:0] data_out;
wire full, empty, halffull;
sync_fifo faa1(clk, rst, wr_en, rd_en, data_in, data_out, empty, full);
//这个没有.rst(rst)的方法例化,所以这里必须要顺序是固定的,不能改变顺序
initial
begin
rst = 1;
clk = 0;
#1 rst = 0;
#5 rst = 1;
end
always #20 clk =~ clk;
//对wr_en,rd_en进行赋值操作
initial
begin
wr_en = 0;
#1 wr_en = 1;
end
initial
begin
rd_en = 0;
#650 rd_en = 1;
wr_en = 0;
end
initial
begin
data_in = 8'h0;
#40 data_in =8'h1;
#40 data_in =8'h2;
#40 data_in =8'h3;
#40 data_in =8'h4;
#40 data_in =8'h5;
#40 data_in =8'h6;
#40 data_in =8'h7;
#40 data_in =8'h8;
#40 data_in =8'h9;
#40 data_in =8'ha;
#40 data_in =8'hb;
#40 data_in =8'hc;
#40 data_in =8'hd;
#40 data_in =8'he;
#40 data_in =8'hf;
end
endmodule
二、异步FIFO的设计
//异步FIFO设计文件
//使用扩展地址位的方式来判断空满
//读写信信号时钟不同,存在跨时钟域的操作(地址位跨时钟域传递)
//关键:格雷码的使用
//问题:跨时钟域的信号的处理方法
//单位宽的信号跨时钟域,要分为从快时钟域跨到慢时钟域
//还是慢时钟域跨到快时钟域这两种情况
//单位宽:clk1--clk2(频率可能不相等,相位也是不确定的)
//1、慢--快:通过两级级联D触发器构成的同步器就可以实现慢到快时钟域的转换
//慢信号肯定能被快信号采到
//2、快--慢:不能用上面的,因为快时钟域里面的信号比较窄,不一定会被采到
//2、快--慢:方法、一、要不产生一个握手信号,快时钟域要等到慢时钟域采到
//之后告诉我一个信号,才结束本次传递
//方法二、把脉冲转化为跳变的电平
//多位宽的data信号,用异步FIFO来做
//多位宽的counter计数值,通过转换格雷码以后,把格雷码通过同步器就可以了
//格雷码即使出错了,也可以认为地址还是在上一个值
module gray(b_in, g_out);
input [4:0] b_in;
output [4:0] g_out;
wire [4:0] g_out;
assign g_out [4]=b_in[4]; //扩展的最高位不用格雷码,直接当做标志位就行
assign g_out [3]=b_in[3]; //高位保持不变,低位用高一位的值和本位做异或
assign g_out [2]=b_in[3]^b_in[2];
assign g_out [1]=b_in[2]^b_in[1];
assign g_out [0]=b_in[1]^b_in[0];
endmodule
module fifo(rclk, wclk, rst, wr_en, rd_en, rd_en, data_in, data_out, empty, full, halffull);
input rclk, wclk, rst, wr_en, rd_en;
input [7:0] data_in;
output empty, full, halffull; //这里的halffull半满信号没有用到
output [7:0] data_out;
reg halffull;
reg [7:0] mem[15:0]; //16*8 RAM
reg [7:0] data_out;
wire [3:0] w_addr, r_addr;
reg [4:0] w_addr_a, r_addr_a; //binary addr,扩展成5位
wire [4:0] w_addr_b, r_addr_b; //gray addr
reg [4:0] w_addr0_b, r_addr0_b;
reg [4:0] w_addr_r, r_addr_w; //sample addr
gray g1(.b_in(w_addr_a), .g_out(w_addr_b)); //写地址转成格雷码
gray g2(.b_in(r_addr_a), .g_out(r_addr_b)); //读地址转成格雷码
assign w_addr = w_addr_b[3:0]; //跨时钟域传递的地址
assign r_addr = r_addr_b[3:0];
always @(posedge rclk or negedge rst)
begin
if(!rst)
begin
r_addr_a <= 5'b0; //读时钟域下的读地址
r_addr_w <= 5'b0; //从写时钟域下同步到读时钟域的写地址
end
else
begin
//把写地址转成格雷码以后同步到读时钟域上去
//为什么要使用两级非阻塞赋值,即两个D触发器构成同步器?注意:两级寄存器并不能完全消除亚稳态危害,但是会大大提高可靠性,减少发生的概率。
//一级寄存器的亚稳态发生的概率太大,三级寄存器消除亚稳态的概率比二级提升不多。
w_addr0_b <= w_addr_b; //把格雷码信号锁存起来,需要两排
r_addr_w <= w_addr0_b; //sample write_addr
if(rd_en == 1 && empty == 0) //读有效且空无效
begin
data_out <= mem[r_addr];
//读的时候memory操作的不能是格雷码的,一定是二进制且没扩展的地址
r_addr_a <= r_addr_a+1;
//每次读的时候,读时钟域的地址要加1
end
end
end
always @(posedge wclk or negedge rst)
begin
if(!rst)
begin
w_addr_a <= 5'b0; //写时钟域下自身的写地址
w_addr_r <= 5'b0; //从读时钟域下同步到写时钟域的读地址
end
else
begin
r_addr0_b <= r_addr_b; //把格雷码信号锁存起来,需要两排
w_addr_r <= r_addr0_b; //sample read_addr
if(wr_en == 1 && full ==0) //写有效且满无效
begin
mem[w_addr] <= data_in;
//相应的要写的内容写到地址上去,写时钟域里面自身的地址,不是转换的
w_addr_a <= w_addr_a +1; //写地址自加1
end
end
end
//判断空满的时候用的地址是格雷码转换过来且同步过来的地址
//比较原则:一定要同码比较,要么就把传递过来的格雷码转换到二进制码地址再跟本时钟域的二进制码比较,要么就是把传递过来的格雷码跟本时钟域二进制码地址转换后的格雷码进行比较(就是同时钟域,且要么同格雷码地址要么同二进制码进行比较)
//从写时钟域同步到读时钟域的写地址和自身的地址进行判断
//和同步FIFO一样,如果所有位相同就是empty,如果高位相反,低位相同就是满
assign empty=(r_addr_b == r_addr_w)?1:0; //w_addr_b
assign full=(w_addr_b[4] != w_addr_r[4] && w_addr_b[3:0] == w_addr_r[3:0]?1:0); //r_addr_b
endmodule
1、FIFO的关键是full满信号和empty空信号的产生,其实主要有两种方法,一是用长度计数器counter,执行一次写操作counter+1,执行一次读操作counter-1;二是利用地址扩展一位,用最高位判断空满,低位地址R_ADDR=W_ADDR时,高位相等则为空,不相等则为满。以上同步FIFO和异步FIFO的实现都是使用的是第二种地址位扩展的方法实现的。
2、在异步FIFO中,一是为什么要用格雷码:格雷码:采用格雷码可以降低亚稳态的发生概率,使其即使在亚稳态进行读写指针抽样也能进行正确的空满状态判断,所以格雷码有两个作用,一是消除多个比特同时变化带来的潜在竞争与冒险,二是降低功耗(翻转次数减少)。二是产生格雷码的方法:方法一:其二进制数整体右移1位再与本身做异或;格雷码:方法二:高位保持不变,低位用高一位的值和本位做异或(本次异步FIFO的实现都是使用的是第二种方法实现的,具体见gray的module代码。)
3、异步FIFO的代码中有:
w_addr0_b <= w_addr_b;
r_addr_w <= w_addr0_b;
和
r_addr0_b <= r_addr_b;
w_addr_r <= r_addr0_b;
这里是使用两级非阻塞赋值,即两个D触发器构成同步器?注意:两级寄存器并不能完全消除亚稳态危害,但是会大大提高可靠性,减少发生的概率。
具体一些跨时钟域的方法和问题可以参考链接:
https://blog.csdn.net/maxwell2ic/article/details/81051545