异步FIFO

异步FIFO

基于FIFO基础的知识,进一步学习什么是异步FIFO,除了基础知识外,还会增加verilog代码和波形图仿真。

计划有UVM验证,做不做得出来再说

基础知识

参考视频:https://www.youtube.com/watch?v=mGREY8u9ELs&t

异步FIFO是一种用于解决数据在不同时钟域之间安全传输的电路模块。你可以想象它像一个临时的数据仓库,一端负责接收数据,另一端负责发送数据,但这两端的工作节奏完全不同步——比如一端可能跑得飞快,而另一端慢悠悠的,就像两个人用不同的速度传递物品。这时候异步FIFO就充当了缓冲区的角色,确保快的一方不会把数据硬塞给慢的一方导致丢失,慢的一方也不会因为来不及取数据而卡住整个系统。

它的核心难点在于协调两个不同步的时钟。比如当写入端以高频时钟快速存入数据时,读取端可能用低频时钟慢慢读取,这时候需要巧妙的设计来避免读写指针冲突。常用的方法是用格雷码来表示指针,这种编码每次变化只改动一个比特位,能大幅降低跨时钟域同步时出现中间态错误的风险。你可能会在处理器与外设通信,或者芯片内部模块交互的场景中见到它,就像两个说不同方言的人通过翻译器顺畅对话一样。

操作步骤

1.写入数据

当外部电路需要写入数据时,写使能信号(这破翻译每次看到都想骂人,我念书的时候就一直搞不懂什么是使能。搞半天原来就是请求信号/启动信号Enable)就会被拉高。接着数据就会被放入FIFO的存储单元,同时写指针会向向前移动一步。

注:这里的写指针的移动速度由写时钟决定

2.同步读指针到写时钟域

读指针原本属于读时钟域,但是需要告诉写端的朋友们“嘿,我读到第X个数据啦!”,否则写入端就不知道前面是否还有位置可以写入。就像你有个停车场管理员朋友,你要听他打电话缺人那里是否有停车位,你才能开车去找车位。

在数字电路中,两级触发器(或格雷码转换)就是起到电话的作用,电话将读指针同步到写时钟域。这时候写域的朋友就可以同步读域的情况了。

3.判断FIFO是否已经满了

这里在基础部分已经详细讲过了 “传送门

4.读取数据

当外部电路需要读取数据时,读使能信号(就是读的请求信号)会被拉高。数据从当前读指针指向的位置取出,同时读指针向前移动一步。读指针的移动速度由读时钟决定,可能与写时钟完全不同步。

5.同步写指针到读时钟域

写指针需要被同步到读时钟域,让读端知道“写端已经写到哪里了”,否则读端可能读到无效数据。

这时候就需要我们的空信号闪亮登场:读端比较同步后的写指针和当前读指针。如果两者完全相等,说明FIFO已空,停止读取。

形象表达

想象一个环形传送带:

写端工人(快时钟) 不断往传送带上放包裹(数据),但需要时不时看一眼对面发来的“读端进度表”(同步后的读指针),决定是否还能继续放。

读端工人(慢时钟) 按自己的节奏取包裹,同时定期向写端汇报“我取到第几个了”(同步后的写指针)。

如果写端太快,传送带放满时会暂停;如果读端太快,发现传送带空了也会暂停。两者通过“进度表”间接协调,即使速度不同也不会出错。

verilog代码

来了来了,开始写代码仿真了!!!

食用说明

稍后会被放入仓库中,点击”传送门“可查看,只需要克隆至你的本地即可。

文档中会有6个文件:顶层模块(async_fifo.v)、存储器模块(fifo_memory.v)、写控制模块(write_controller.v)、读控制模块(read_controller.v)、跨时钟同步模块(sync_ptr.v)、TB模块(testbench.v)

代码内容

仓库地址:https://github.com/HauUhang/Asynchronous-FIFO

  1. async_fifo.v
  2. fifo_memory.v
  3. write_controller.v
  4. read_controller.v
  5. sync_ptr.v
  6. testbench.v

写代码过程中的疑问和学习

(1)write_ctrl

写代码的逻辑是什么?

  1. 首先要写信号:
  • 📥 Inputs(来自哪里?)
    • 写时钟信号【提示:FIFO 是异步的,写和读时钟分开,写控制模块用哪个时钟?】
    • 写使能信号(是否写入)【提示:这个信号由外部模块控制,用来决定是否尝试写数据。】
    • FIFO已满标志?【提示:如果FIFO满了,还能写吗?这个信息从哪里来?你需要接收它还是计算它?】
    • 读指针(同步过来的)【提示:你是否需要判断“满”?那你要和谁比较?异步域的信号可以直接比较吗?】
  • 📤 Outputs(你要输出给谁?)
    • 写地址(写指针)【提示:你要输出写地址给 RAM,同时也可能输出一个格雷码版本用于跨时钟同步。】
    • 写指针(格雷码)【提示:异步FIFO通常会输出一个“同步友好”的写指针给读域。】
    • 满信号(FIFO full)【提示:你是否要计算 FIFO 是否满?满的时候外部就不该写了。】
  1. 接着是逻辑
    异步FIFO的逻辑是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
             +--------------------+
wen & ~wfull | |
---------->+ ① wptr_bin |
| (计数器递增逻辑) |
+--------------------+
|
v
+---------------------+
|② assign waddr |
|waddr = wptr_bin[2:0]|
+---------------------+
|
v
+-----------------------------+
|③ assign wptr_gray |
|wptr_gray = wptr_bin ^ (>>1) |
+-----------------------------+
  • 写指针(二进制)逻辑
  • 写地址输出
  • 写指针的格雷码形式

为什么在write_ctrl.v中我使用的是wire,而不是reg?

因为根据FIFO基础中描述,我提到full= {~wptr[3],wptr[2:0]}=={rptr[3:0]},这是一种组合逻辑,就要用wire

波形图

write_ctrl_tb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
`timescale 1ns/1ps

module tb_write_ctrl;

reg wclk;
reg wrst;
reg wen;
reg [2:0] rptr_gray_sync;
wire [2:0] waddr;
wire [2:0] wptr_gray;
wire wfull;

// 实例化
wirte_ctrl uut(
.wclk(wclk),
.wrst(wrst),
.wen(wen),
.rptr_gray_sync(rptr_gray_sync),
.waddr(waddr),
.wptr_gray(wptr_gray),
.wfull(wfull)
);

// 生成时钟
always #5 wclk = ~wclk;

initial begin
$dumpfile("wave.vcd"); // 生成波形文件 (iverilog)
$dumpvars(0, tb_write_ctrl);

// 初始化
wclk = 0;
wrst = 1;
wen = 0;
rptr_gray_sync = 3'b000;

// 复位一段时间
#20;
wrst = 0;

// 写几次数据
#10; wen = 1;
repeat(10) #10;

// 模拟FIFO满
rptr_gray_sync = 3'b110; // 可以调整为和wptr_gray_full一致的值,观察wfull变化

// 写使能继续,看是否被禁止写
repeat(4) #10;

// 结束
wen = 0;
#50;
$finish;
end
endmodule

(2)read_ctrl

这个模块的代码逻辑是什么?

和写代码的很像,就几乎把w变成r就行了,不多赘述

(3)dual_port_ram

我不太懂[7:0]和[2:0]究竟是什么,我想要一个8-deep的FIFO

在 Verilog 中,[a:b] 是位宽定义,表示这个信号有多少位(bit)
[7:0] 表示一共有 8 位(bit) 可以表示 0 到 255 的数(1 byte)
[2:0] 表示一共有 3 位(bit) 可以表示 0 到 7(8个地址)

为什么这个里面不用wrst和rrst

因为这个模块只是一个 纯存储器(memory)模块,RAM 本身是:同步写、同步读;没有“状态”需要复位;读写操作全靠 wen/ren + clk 控制;数据存在 RAM 的数组里(如 mem[0:7]),不受 reset 控制清空

mem[waddr] <= wdata 的意思是wdata赋值给men的waddr中吗?

在硬件中你可以把 mem[…] 看成一个可以存很多小值的“储物柜”,waddr 就是柜子号,wdata 是你放进去的东西。

testbench

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
`timescale 1ns/1ps

module tb_dual_port_ram;

// 定义测试用的信号
reg wclk, wen;
reg [2:0] waddr, wdata;

reg rclk, ren;
reg [2:0] raddr;
wire [2:0] rdata;

// 实例化 DUT(Design Under Test)
dual_port_ram dut (
.wclk(wclk),
.wen(wen),
.waddr(waddr),
.wdata(wdata),
.rclk(rclk),
.ren(ren),
.raddr(raddr),
.rdata(rdata)
);

// 写时钟:周期 50ns
initial begin
wclk = 0;
forever #25 wclk = ~wclk;
end

// 读时钟:周期 60ns
initial begin
rclk = 0;
forever #30 rclk = ~rclk;
end

// 主测试流程
initial begin
// 初始值
wen = 0; waddr = 3'd0; wdata = 3'd0;
ren = 0; raddr = 3'd0;

#50;

// 写数据到地址 3:写入值 5
waddr = 3'd3;
wdata = 3'd5;
wen = 1;
#50;
wen = 0;

// 写数据到地址 4:写入值 7
waddr = 3'd4;
wdata = 3'd7;
wen = 1;
#50;
wen = 0;

// 读地址 3,应该读到 5
raddr = 3'd3;
ren = 1;
#60;
ren = 0;

// 读地址 4,应该读到 7
raddr = 3'd4;
ren = 1;
#60;
ren = 0;

// 结束仿真
#100;
$stop;
end

endmodule

波形图

dual_prot_ram

(4)sync_gray

testbench

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
`timescale 1ns/1ps

module tb_sync_gray;

reg clk;
reg rst;
reg gray_in;
wire gray_out;

// 实例化你的模块
sync_gray dut (
.clk(clk),
.rst(rst),
.gray_in(gray_in),
.gray_out(gray_out)
);

// 生成时钟
always #5 clk = ~clk; // 10ns周期

initial begin
// 初始化信号
clk = 0;
rst = 1;
gray_in = 0;

// 复位
#10;
rst = 0;

// 输入信号变化
#15 gray_in = 1;
#20 gray_in = 0;
#20 gray_in = 1;
#30 gray_in = 0;

// 模拟结束
#50;
$stop;
end

endmodule

波形图

sync_gray

作者

Hau uhang

发布于

2025-04-07

更新于

2025-04-13

许可协议

评论