文章目录
前言
我周围很多人都在开始准备实习、准备找工作了,而我和他们的方向又不一样,我是打算硬件一条道走到黑,他们基本上都是软件的,都打算进军互联网。最近看面经,我看到数字IC的面试一般都会问这四个问题:①建立时间与保持时间,②亚稳态,③跨时钟域处理,④异步FIFO。那我本着一个学习的心态、精益求精的态度,就决定把这四个点弄清楚,并讲清楚。如果有不清楚或者错误的地方,还望批评指正。一、建立时间与保持时间
1. 概念
建立时间
T
s
u
T_{su}
Tsu(Setup):触发器在时钟上升沿到来之前,其数据输入端的数据必须保持不变的最小 时间。(指在时钟沿到来之前数据从不稳定到稳定所需的时间,如果建立的时间不满足要求那么数据将不能在这个时钟上升沿被稳定的打入触发器)
建立时间决定了该触发器之间的组合逻辑的最大延迟。
保持时间
T
h
T_{h}
Th(Hold):触发器在时钟上升沿到来之后,其数据输入端的数据必须保持不变的最小 时间。(指数据稳定后保持的时间,如果保持时间不满足要求那么数据同样也不能被稳定的打入触发器)
保持时间决定了该触发器之间的组合逻辑的最小延迟。
上面这个黑字,就是基本的概念,灰色的是一个解释。而这个“最小”,是我们在设计的时候,考虑的时间不能小于这个时间,就像我们在一些芯片手册上看见的一样,一些时序图上,标注的很多都是“Min”或“Max”。
2.原因
即为什么要满足建立时间和保持时间:因为触发器内部数据的形成是需要一定的时间的。如果不满足建立和保持时间,触发器将进入亚稳态,进入亚稳态的输出将不稳定,在0和1之间变化,这时需要一个恢复时间,其输出才能稳定,但稳定后的值也不一定就是想要的值。
接下来详细解释一下为什么需要建立时间和保持时间。停留在触发器层面是不行的,我们需要深入到触发器的构成中去,即传输门。
!我很懒,接下来解释的部分来源于这一篇博客:一文解决关于建立保持时间的困惑 我本来是只想放个链接就可以了的,但转头一想,只有写在自己本子上的知识才可以反复观看,于是,就原封不动地抄下来啦!
一个简单的CMOS上升沿触发器可以看作是由上面的几个基本元件组成的,其中T1、T2、T3、T4为传输门,I1、I2、I3、I4为反相器。
在这里补充一下传输门的相关知识,传输门实际由一对N沟道和P沟道的MOS管并联构成,NMOS的gate为高时导通,PMOS的gate为低时导通。上面的表示符号带非端的连接口即为PMOS的gate端,对面的为NMOS的gate端。当PMOS端为低电平,NMOS端为高电平时传输门导通,反之传输门关断。因此可以简单的理解传输门是一个电路开关。以图中的传输门为例:
- 当clk为低电平时红色传输门导通,蓝色传输门关断
- 当clk为高电平时红色传输门关断,蓝色传输门导通
我们将整个过程分为clk为低电平、高电平、上升沿三个阶段分析:
①clk为低电平时
根据上面的分析,T1、T4为导通状态,我们可以将其看作是一个具有延时效应的连线,T2、T3为关断状态,我们可以去除掉相关电路元件。此时,电路被划分为了两截,如下所示:
容易分析出,此时左边部分的电路
Q
m
‾
\overline{Q_{m}}
Qm将会随着
D
D
D变化而延时变化,极性与
D
D
D相反。右边部分的电路,由于T3传输门关断,右边部分的信号将会维持上一时态的
Q
‾
\overline{Q}
Q 不变,两个并联的方向相反的反相器起到了一个锁存的作用,此时电路的输出将仍旧保持上一个状态
Q
Q
Q 。
②clk为高电平时
此时左半部分的电路起到锁存信号的作用,锁存过程之后信号
Q
m
‾
\overline{Q_{m}}
Qm 处于稳定状态,
Q
Q
Q也将会随着
Q
m
‾
\overline{Q_{m}}
Qm的稳定趋于稳定。
③clk上升沿时
当clk上升沿时,即clk由低电平变为高电平的过程。此时我们来分析建立时间与保持时间具体是由什么原因产生的:
建立时间
当时钟处于低电平时,
Q
m
‾
\overline{Q_{m}}
Qm 将随着
D
D
D输入的变化而变化,时钟高电平时将会对
Q
m
‾
\overline{Q_{m}}
Qm 进行锁存。我们需要意识到的是,数据信号通过传输门以及反相器是需要消耗时间的,因此触发器的建立时间指的是数据通过T1、I1至
Q
m
‾
\overline{Q_{m}}
Qm 的时间。这时建立时间的存在意义就大白天下了,我们需要在进入时钟高电平阶段前将稳定的数据送入到锁存处,由于电路延时的原因,才需要建立时间的存在。当然,上面的结论是忽略了时钟本身的偏移的,事实上建立时间也有时钟偏移的影响,因此也会出现负建立时间的情况。
保持时间
当时钟进入高电平后,由于传输门T1关断需要一定的时间,因此输入
D
D
D必须继续稳定一段时间才能够保证数据被稳定锁存,因此保持时间实质上是传输门T1关断至锁存数据的时间。
3.分析计算
上面这么多话,都只是停留在一个原理分析的角度,还比较容易理解,但是涉及到计算的时候,就容易头痛加出错了。
1)模型分析
理解建立时间保持时间需要一个模型,如图所示。起点是源触发器
D
1
D1
D1 的采样时刻(下图中的①点),终点是目的触发器
D
2
D2
D2 的采样时刻(下图中的②点),假设起点已经满足了建立时间和保持时间要求,现在分析终点采样时刻是否同样满足要求。
T C O T_{CO} TCO | 数据正确采样后从 D 端到达 Q 端的延时,触发器固有属性,不可改变 |
---|---|
T D e l a y T_{Delay} TDelay | D1 输出端到 D2 输入端的组合逻辑延时和布线延时 |
T s u T_{su} Tsu | 触发器的建立时间,触发器固有属性,不可改变 |
T h T_{h} Th | 触发器的保持时间,触发器固有属性,不可改变 |
T c l k T_{clk} Tclk | 时钟周期 |
t 1 t1 t1 | 假设源时钟为 clka, clka 到达 D1 的延时 |
t 2 t2 t2 | 同 t 1 t1 t1, clka 到达 D2 的延时 |
2)模型计算
①假设clk传输没有延时
先假设 clk 的传输没有任何延时,则每一个时钟上升沿都会同时到达 D 1 D1 D1 和 D 2 D2 D2。时间起点,第一个时钟沿 D 1 D1 D1 的采样时刻,时间终点,第二个时钟沿 D 2 D2 D2 的采样时刻。物理起点, D 1 D1 D1 的输入端,物理终点, D 2 D2 D2 的输入端。
建立时间满足(关注数据头):
第一个时钟沿到来时,数据 data 的头部从起点①开始传输,经过
T
C
O
T_{CO}
TCO 到达②,在经过
T
D
e
l
a
y
T_{Delay}
TDelay到达③。根据建立时间的要求,数据在时钟沿到来之前需要保持稳定的最小时间为
T
s
u
T_{su}
Tsu,假设这里刚好满足建立时间的要求,数据 data 到达③后经过
T
s
u
T_{su}
Tsu 的时间到达了④。计时是从第一个时钟沿开始的,采样在第二个时钟沿,采样的时候 data 在位置④,则有
T
C
O
+
T
D
e
l
a
y
+
T
s
u
=
T
c
l
k
T_{CO} + T_{Delay} + T_{su} = T_{clk}
TCO+TDelay+Tsu=Tclk
如果不是最极限的情况,则第二个时钟沿到达③时, data 的头部已经超过④,假设超过④的时间为 t,则有
T
C
O
+
T
D
e
l
a
y
+
T
s
u
+
t
=
T
c
l
k
,
t
⩾
0
T_{CO} + T_{Delay} + T_{su} + t = T_{clk} , t \geqslant 0
TCO+TDelay+Tsu+t=Tclk,t⩾0
即有
T
C
O
+
T
D
e
l
a
y
+
T
s
u
⩽
T
c
l
k
T_{CO} + T_{Delay} + T_{su} \leqslant T_{clk}
TCO+TDelay+Tsu⩽Tclk
保持时间满足(关注数据尾)
第二个时钟沿到来时,
D
2
D2
D2 采集数据 data,同时
D
1
D1
D1 在采集数据 new data,所以 data 的尾部在第二个时钟沿到来时开始从
D
1
D1
D1 的输入端①开始向前传输,经过
T
c
o
T_{co}
Tco 和
T
D
e
l
a
y
T_{Delay}
TDelay 后到达
D
2
D2
D2的输入端③,所以第二个时钟沿到来之后 data 稳定的时间为
T
C
O
+
T
D
e
l
a
y
T_{CO} + T_{Delay}
TCO+TDelay
根据保持时间的定义,有
T
C
O
+
T
D
e
l
a
y
⩾
T
h
T_{CO} + T_{Delay} \geqslant T_{h}
TCO+TDelay⩾Th
②加上clk的传输时延
事实上 Clock 的传输也是有延时的,如图所示,两个触发器的源时钟为 clka,到达 D 1 D1 D1 需要 t 1 t1 t1 的时间,到达 D 2 D2 D2 需要 t 2 t2 t2 的时间, t 2 − t 1 t2−t1 t2−t1 其实就是我们常说的 clock skew(时钟偏斜),就是同一个时钟沿达到 D 1 D1 D1 和 D 2 D2 D2 的时延差别,如果 D 1 D1 D1 和 D 2 D2 D2 离的很远,那么相应的 clock skew 就会更大。
建立时间
加上 clk 传输时延后,变了的是第二个时钟沿到达
D
2
D2
D2 的时间,从
T
c
l
k
Tclk
Tclk 变为
T
c
l
k
+
t
2
−
t
1
Tclk+t2−t1
Tclk+t2−t1,所以有
T
C
O
+
T
D
e
l
a
y
+
T
s
u
⩽
T
c
l
k
+
t
2
−
t
1
T_{CO} + T_{Delay} + T_{su} \leqslant T_{clk} +t2-t1
TCO+TDelay+Tsu⩽Tclk+t2−t1因为t2大于t1,所以对左边时间的限制其实是放宽了!
保持时间
与建立时间相反,因为第二个时钟沿晚来的原因,实际上对保持时间的要求更严格了:
T
C
O
+
T
D
e
l
a
y
−
(
t
2
−
t
1
)
⩾
T
h
T_{CO} + T_{Delay} - (t2-t1) \geqslant T_{h}
TCO+TDelay−(t2−t1)⩾Th从上面的分析可以看到,数据跑得越快(
T
D
e
l
a
y
T_{Delay}
TDelay越小),时钟传输时延越大(clock skew 越大),对建立时间的满足越有利,而对保持时间的满足越不利,相反则对满足保持时间越有利,对满足建立时间越不利。建立时间还跟时钟周期有关系,时钟周期越小,越容易发生建立时间违例,而保持时间则跟时钟周期没有关系。在设计中,我们常常关注的是建立时间是否满足要求,因为它关系到我们能使用的最小时钟周期有多小,能否跑到预定的工作频率,而因为时钟通常都是走专门的快速线路,很难存在时钟传输时延过大的问题,所以一般也不会出现保持时间违例的情况。
在这篇博客中有关于笔试面试题的详解,大家可以看看:建立时间和保持时间关系详解
3)违例的解决办法
①对于建立时间违例的解决办法
- 降低时钟频率,即增大时钟周期;
- 在时钟路径上加缓冲器(buffer),让时钟晚到来;
- 更换具有更小器件延迟的触发器;
- 组合逻辑优化或插入流水线,缩短关键路径。
②对于保持时间违例的解决办法
- 在数据路径上插入buffer;
- 更换具有更大器件延迟的触发器;
- 优化时钟路径,让时钟更早到来。
二、亚稳态
1.概念
是指触发器无法在某个时间段内达到一个确定的状态。
2.原因
由于触发器的建立时间和保持时间不满足,当触发器进入亚稳态,使得无法预测该单元的输出,这种不稳定是会沿信号通道的各个触发器级联传播。逻辑电路中绝大多数的时序问题基本都是因为这个原因产生的。
3. 危害
只要系统中存在异步元件,那么亚稳态就是无法完全避免的。 产生亚稳态后,寄存器输出在稳定之前可能是毛刺、振荡、固定的一个电压值。其他与之相连的数字元件收到它的亚稳态,也会发生逻辑混乱,最糟糕的情况就是系统直接崩掉。
4. 常见情况
- 输入信号是异步信号
- 时钟偏移/摆动(上升/下降时间)高于容限值(时钟信号质量不好)
- 信号在两个不同频率或者相同频率但相位和偏移不同的时钟域下跨时钟域工作
- 组合延迟使寄存器的数据输入在亚稳态窗口内发生变化
5.解决方法
理论上亚稳态不能完全消除,只能降低,一般采用两级触发器同步就可以大大降低亚稳态发生的概率,再加多级触发器改善不大。
两级触发器可防止亚稳态传播的原理:假设第一级触发器的输入不满足其建立保持时间,它在第一个脉冲沿到来后输出的数据就为亚稳态,那么在下一个脉冲沿到来之前,其输出的亚稳态数据在一段恢复时间后必须稳定下来,而且稳定的数据必须满足第二级触发器的建立时间,如果都满足了,在下一个脉冲沿到来时,第二级触发器将不会出现亚稳态,因为其输入端的数据满足其建立保持时间。更确切地说,输入脉冲宽度必须大于同步时钟周期与第一级触发器所需的保持时间之和。最保险的脉冲宽度是两倍同步时钟周期。 所以,这样的同步电路对于从较慢的时钟域来的异步信号进入较快的时钟域比较有效,对于进入一个较慢的时钟域,则没有作用 。
在实际的 FPGA 设计中,人们不会想着怎么降低发生亚稳态的概率,而是想着怎么减小亚稳态的影响。 常见的方法有以下几种:
- 降低系统时钟频率
- 用反应更快的 FIFO
- 引入同步机制,防止亚稳态传播(可以采用前面说的加两级触发器)
- 异步信号同步化(两级触发器)
- 采用 FIFO 对跨时钟域数据通信进行缓冲
- 对复位电路采用异步复位、同步释放处理 - 改善时钟质量,用边沿变化快速的时钟信号
三、跨时钟域处理
1.原因
根据前面所说,我们需要减少亚稳态的风险,那么如何减少呢?对于单一时钟域内的信号,我们可以采用EDA工具来检查每个触发器的建立保持时间,确保其不出现亚稳态,但是跨时钟域的信号,却没有工具保证其可靠性,如果使用静态时序分析,应该要设置 false path 约束,所以只能依靠跨时钟域处理的同步化技术。
2.处理方法
(1)单bit信号
- 电平检测
- 边沿检测
- 脉冲同步
①电平检测
最为简单的方法。通过寄存器打两拍进行同步,也就是所谓的电平同步器。适用于慢时钟域向快时钟域,还可以适用于同频率、不同相位。
存在问题为:输入信号必须保持两个接收时钟周期,每次同步完,输入信号要恢复到无效状态。所以,如果是从快到慢,信号很有可能被滤除。
//慢时钟域 -> 快时钟域
module abit_Pulse(
input clk_slow , //输入时钟
input clk_fast , //传递时钟
input rst_n , //复位信号
input signal_in , //输入信号
output signal_out
);
reg signal_inout; // 中间信号
reg [1:0] signal_r;
//将输入信号传递出去
always @(posedge clk_slow or negedge rst_n) begin
if(!rst_n)
signal_inout <= 1'b0;
else
signal_inout <= signal_in;
end
//在快时钟域下,将传递来的信号打两拍
always @(posedge clk_fast or negedge rst_n) begin
if(!rst_n)
signal_r <= 2'b00;
else
signal_r <= {signal_r[0],signal_inout};
end
assign signal_out = signal_r[1];
endmodule
我看见有人说这种情况只适合于同频率、不同相位,我都试了一下,都可以。但是,一定要输入信号必须保持两个接收时钟周期!
同频率不同相位:
不同频率:
②边沿检测
在电平同步器的基础上,通过输出端的逻辑组合,可以完成对于信号边沿的提取,识别上升沿、下降沿以及双边沿,并发出相应的脉冲。适用于慢时钟域向快时钟域。比起电平同步器,更适合要求脉冲输出的电路。
但同样,输入信号必须保持两个接收时钟周期。
//慢时钟 -> 快时钟
module edgeSltoF(
input clk_slow , //输入时钟
input clk_fast , //传递时钟
input rst_n , //复位信号
input signal_in , //输入信号
output signal_out
);
reg signal_inout; // 中间信号
reg [2:0] signal_r;
//将输入信号传递出去
always @(posedge clk_slow or negedge rst_n) begin
if(!rst_n)
signal_inout <= 1'b0;
else
signal_inout <= signal_in;
end
//在快时钟域下,将传递来的信号打三拍
always @(posedge clk_fast or negedge rst_n) begin
if(!rst_n)
signal_r <= 3'b000;
else
signal_r <= {signal_r[1:0],signal_inout};
end
assign signal_out = signal_r[1] & ~signal_r[2]; //获取信号脉冲
endmodule
③脉冲同步
先将原时钟域下的脉冲信号,转化为电平信号(使用异或门),再进行同步,同步完成之后再把新时钟域下的电平信号转化为脉冲信号(边沿检测器的功能)。适用于快时钟域向慢时钟域。这就从快时钟域的取出一个单时钟宽度脉冲,在慢时钟域建立新的单时钟宽度脉冲。结合了前面所提到的两种同步器。
存在问题为:输入脉冲的时间间距必须在两个接收时钟周期以上,否则新的脉冲会变宽,这就不再是单时钟脉冲了。
module PulseFtoSl(
input clk_slow , //输入时钟
input clk_fast , //传递时钟
input rst_n , //复位信号
input signal_in , //输入信号
output signal_out
);
reg signal_Dout; // 中间信号
reg [2:0] signal_r;
//将输入信号传递出去
always @(posedge clk_fast or negedge rst_n) begin
if(!rst_n)
signal_Dout <= 1'b0;
else
signal_Dout <= signal_in ? ~signal_Dout : signal_Dout;
end
//在慢时钟域下,将传递来的信号打三拍
always @(posedge clk_slow or negedge rst_n) begin
if(!rst_n)
signal_r <= 3'b000;
else
signal_r <= {signal_r[1:0],signal_Dout};
end
assign signal_out = signal_r[1] ^ signal_r[2]; //获取信号脉冲
endmodule
(2)多bit信号
① 采用保持寄存器加握手信号
缺点:延迟会比较多
什么是握手信号?
摘抄自博客FPGA 设计之 跨时钟域(六 - 握手)
RTL代码可以查看我的另一篇博客FPGA(五)RTL代码之一(跨时钟域设计)
握手指的是两个设备之间通信的一种方式,用来通信的信号就是握手信号。最简单的握手信号是 valid 和 ready,也可以叫 request 和 grant。假设设备1向设备2发送数据,设备1不知道设备2什么时候可以接收数据,设备2也不知道设备1什么时候会发送数据,那么它们之间如果用握手通信可能是这样的顺序:
- 设备1将 valid 信号置1,告诉设备2,数据准备就绪了,请查收
- 设备2此刻正处于忙碌状态无法接收数据,设备2将 ready 信号保持为0
- 设备2空闲了,将 ready 信号置1接收设备1的数据
- 设备1看到设备2的 ready 为1,它知道设备2已经接收好数据了,将 valid 置0同时撤销数据,准备下一次发送
- 可以看到因为有握手控制,可以确保数据的正确传输,不会丢失。跨时钟域的握手设计就是利用握手控制这种优势,从而避免因为跨时钟域引起的数据传输错误
多bit接口设计
可以看到握手模块其实就是一个桥梁,用来连接 clock1 时钟域和 clock2 时钟域。接口上看它有:
- 两组时钟复位信号输入,(clk1,rst1) 和 (clk2,rst2)
- 两组握手信号,(valid1,ready1)和(valid2,ready2)
- 两路数据信号,(data1输入)和(data2输出)
接口时序
②展宽信号 + 脉冲信号传递
//展宽信号 + 脉冲信号传递
//可以适用于单bit信号、多bit信号
//可以使用于快到慢,也可以适用于慢到快
module Sync_Pulse(
input clka ,
input clkb ,
input rst_n ,
input pulse_ina ,
output pulse_outb ,
output signal_outb
);
//-------------------------------------------------------
reg signal_a ;
reg signal_b ;
reg [1:0] signal_b_r ;
reg [1:0] signal_a_r ;
//-------------------------------------------------------
//在clka下,生成展宽信号signal_a
always @(posedge clka or negedge rst_n)begin
if(rst_n == 1'b0)
signal_a <= 1'b0;
else if(pulse_ina == 1'b1)
signal_a <= 1'b1;
else if(signal_a_r[1] == 1'b1)
signal_a <= 1'b0;
else
signal_a <= signal_a;
end
//-------------------------------------------------------
//在clkb下同步signal_a
always @(posedge clkb or negedge rst_n)begin
if(rst_n == 1'b0)
signal_b <= 1'b0;
else
signal_b <= signal_a;
end
//-------------------------------------------------------
//在clkb下生成脉冲信号和输出信号
always @(posedge clkb or negedge rst_n)begin
if(rst_n == 1'b0)
signal_b_r <= 2'b00;
else
signal_b_r <= {signal_b_r[0], signal_b};
end
assign pulse_outb = ~signal_b_r[1] & signal_b_r[0];
assign signal_outb = signal_b_r[1];
//-------------------------------------------------------
//在clka下采集signal_b_r[1],生成signal_a_r[1]用于反馈拉低signal_a
always @(posedge clka or negedge rst_n)begin
if(rst_n == 1'b0)
signal_a_r <= 2'b00;
else
signal_a_r <= {signal_a_r[0], signal_b_r[1]};
end
endmodule
结构图:
慢到快,单脉冲
慢到快,多bit
快到慢,单脉冲
快到慢,多bit
③异步FIFO
详情见第四节。
参考博客:跨时钟域处理方法总结–最终详尽版
四、FIFO
参考博客:同步fifo与异步fifo
1. 什么是FIFO
FIFO(First In First Out),即先进先出队列。FIFO存储器是一个先入先出的双口缓冲器,即第一个进入其内的数据第一个被移出,其中一个是存储器的输入口,另一个口是存储器的输出口。对于单片FIFO来说,主要有两种结构:触发导向结构和零导向传输结构。触发导向传输结构的FIFO是由寄存器阵列构成的,零导向传输结构的FIFO是由具有读和写地址指针的双口RAM构成。
FPGA 使用的 FIFO 一般指的是对数据的存储具有先进先出特性的一个缓存器,常被用于数据的缓存,或者高速异步数据的交互也即所谓的跨时钟域信号传递。它与 FPGA 内部的 RAM 和 ROM 的区别是没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像 RAM 和 ROM 那样可以由地址线决定读取或写入某个指定的地址。
根据 FIFO 工作的时钟域,可以将 FIFO 分为同步 FIFO 和异步 FIFO。同步 FIFO 是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。异步 FIFO 是指读写时钟不一致,读写时钟是互相独立的。 Xilinx 的 FIFO IP 核可以被配置为同步 FIFO 或异步 FIFO,其信号框图如下图所示。从图中可以了解到,当被配置为同步 FIFO 时,只使用 wr_clk,所有的输入输出信号都同步于 wr_clk 信号。而当被配置为异步 FIFO 时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟 wr_clk,所有与读相关的信号都是同步于读时钟 rd_clk。
FIFO可用于以下目的:
- 跨时钟域
- 在将数据发送到芯片外之前将其缓冲(例如,发送到DRAM或SRAM)
- 缓冲数据以供软件在以后查看
- 存储数据以备后用
FIFO的参数
- 宽度:一次读写操作的数据位
- 深度:可以存储的 N 位数据的数目(宽度为 N)
- 满标志: full。FIFO 已满时,由 FIFO 的状态电路送出的信号,阻止 FIFO 写操作
- 空标志: empty。FIFO 已空时,由 FIFO 的状态电路送出的信号,阻止 FIFO 读操作
- 读时钟:读操作所遵循的时钟
- 写时钟:写操作所遵循的时钟
2. 同步FIFO
同步FIFO的读时钟和写时钟为同一个时钟,FIFO内部所有逻辑都是同步逻辑,常常用于交互数据缓冲。
同步FIFO学习
同步fifo与异步fifo
校招Verilog——同步FIFO和异步FIFO
(1)原理
典型同步FIFO由三部分组成:FIFO写控制逻辑、FIFO读控制逻辑、FIFO存储实体。
FIFO写控制逻辑主要功能:产生FIFO写地址、写有效信号,同时产生FIFO写满、写错等状态信号;
FIFO读控制逻辑主要功能:产生FIFO读地址、读有效信号,同时产生FIFO读空、读错等状态信号。
FIFO读写过程的地址控制如下图所示:
- 当FIFO初始化(复位)时,fifo_write_addr与fifo_read_addr同指到0x0,此时FIFO处于空状态;
- 当FIFO进行写操作时,fifo_write_addr递增(增加到FIFO DEPTH时回绕),与fifo_read_addr错开,此时FIFO处于非空状态;
- 当FIFO进行读操作时,fifo_read_addr递增。
FIFO空满状态产生:
为产生FIFO空满标志,引入Count 计数器,用于指示FIFO内部存储数据个数; - 当只有写操作时,Count加1;只有读操作时,Count减1;其它情况下,保持不变;
- Count为0时,说明FIFO为空,fifo_empty置位;
- Count等于FIFO_DEPTH时,说明FIFO已满,fifo_full置位。
(2)代码
先对同步FIFO的对外接口信号进行描述:
- 时钟,输入,用于同步FIFO的读和写,上升沿有效;
- 清零,输入,异步清零信号,低电平有效,该信号有效时,FIFO被清空;
- 写请求,输入,高电平有效,该信号有效时,表明外部电路请求向FIFO写入数据;
- 读请求,输入,高电平有效,该信号有效时,表明外部电路请求从FIFO中读取数据;
- 数据输入总线,输入,当写信号有效时,数据输入总线上的数据被写入到FIFO中;
- 数据输出总线,输出,当读信号有效时,数据从FIFO中被读出并放到数据输出总线上;
- 空,输出,高电平有效,当该信号有效时,表明FIFO中没有任何数据,全部为空;
- 满,输出,高电平有效,当该信号有效时,表明FIFO已经满了,没有空间可用来存贮数据
①执行代码
module sFIFO
//========================< 参数 >==========================================
#(
parameter FIFO_WIDTH = 32 , //数据位宽
parameter ADDR_WIDTH = 4 , //地址位宽 2^4 = FIFO_DEPTH, so addr with is 4 bits if depth = 16
parameter FIFO_DEPTH = 16 //数据深度
)
//========================< 端口 >==========================================
(
input clk , //时钟
input rst_n , //复位
input wr_en , //写使能
input [FIFO_WIDTH-1:0] wr_data , //写数据
input rd_en , //读使能
output reg [FIFO_WIDTH-1:0] rd_data , //读数据
output full , //写满
output empty //读空
);
//========================< 信号 >==========================================
reg [ADDR_WIDTH-1:0] wr_addr ; //写地址
reg [ADDR_WIDTH-1:0] rd_addr ; //读地址
reg [FIFO_WIDTH-1:0] ram[2**ADDR_WIDTH-1:0] ; //ram,地址位宽4,共16个
reg [ADDR_WIDTH :0] Count ; //计数器
//==========================================================================
//== 读写地址
//==========================================================================
//-- 写地址
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
wr_addr <= 0;
end
else if(wr_en && (!full)) begin
wr_addr <= wr_addr + 1;
end
end
//-- 读地址
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_addr <= 0;
end
else if(rd_en && (!empty)) begin
rd_addr <= rd_addr + 1;
end
end
//==========================================================================
//== 读写数据
//==========================================================================
//-- 写数据
//---------------------------------------------------
integer i;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
for(i=0; i < FIFO_DEPTH; i=i+1)
ram[i] <= 0;
end
else if(wr_en) begin
ram[wr_addr] <= wr_data;
end
end
//-- 读数据
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_data<=0;
end
else if (rd_en) begin
rd_data <= ram[rd_addr];
end
end
//==========================================================================
//== 空满标志
//==========================================================================
//-- 辅助计数
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
Count <= 0;
end
else if(wr_en && (!rd_en) && !full) begin
Count <= Count + 1;
end
else if(rd_en && (!wr_en) && !empty) begin
Count <= Count - 1;
end
end
//-- 写满、读空
//---------------------------------------------------
assign full = (Count == FIFO_DEPTH);
assign empty = (Count == 0 );
endmodule
②仿真文件
`timescale 1ns/1ns
module sFIFO_tb;
//========================< 参数 >==========================================
parameter FIFO_WIDTH = 32 ; //数据位宽
parameter ADDR_WIDTH = 4 ; //地址位宽 2^4 = FIFO_DEPTH, so addr with is 4 bits if depth = 16
parameter FIFO_DEPTH = 16 ; //数据深度
//========================< 信号 >==========================================
reg clk ;
reg rst_n ;
reg wr_en ;
reg [FIFO_WIDTH-1:0] wr_data ;
reg rd_en ;
//==========================================================================
//== 例化
//==========================================================================
sFIFO
#(
.FIFO_WIDTH (FIFO_WIDTH ),
.ADDR_WIDTH (ADDR_WIDTH ),
.FIFO_DEPTH (FIFO_DEPTH )
)
u_sFIFO
(
.clk (clk ),
.rst_n (rst_n ),
.wr_en (wr_en ),
.wr_data (wr_data ),
.rd_en (rd_en ),
.rd_data ( ),
.full ( ),
.empty ( )
);
//==========================================================================
//== 时钟
//==========================================================================
always #10 clk = ~clk;
//==========================================================================
//== 设计
//==========================================================================
initial begin
clk = 1;
rst_n = 0;
wr_en = 0;
wr_data = 0;
rd_en = 0;
#101;
rst_n = 1;
#20;
gen_data;
@(posedge clk);
rd_en=1;
repeat(FIFO_DEPTH)@(posedge clk);
rd_en=0;
end
task gen_data;
integer i;
begin
for(i=0; i<FIFO_DEPTH; i=i+1) begin
wr_en = 1;
wr_data = i;
#20;
end
wr_en = 0;
wr_data = 0;
end
endtask
endmodule
③仿真结果
2.异步FIFO
(1)原理
异步FIFO的实现通常是利用双口RAM和读写地址产生模块来实现的。FIFO的接口包括异步的写时钟(wr_clk)和读时钟(rd_clk)、与写时钟同步的写有效(wr_en)和写数据(wr_data)、与读时钟同步的读有效(rd_en)和读数据(rd_data)。为了实现正确的读写和避免FIFO的上溢或下溢,通常还应该给出与读时钟和写时钟同步的FIFO的空标志(empty)和满标志(full)以禁止读写操作。
写地址产生模块还根据读地址和写地址关系产生FIFO的满标志。当wren有效时,若写地址+2=读地址时,full为1;当wren无效时,若写地址+ 1=读地址时,full为1。读地址产生模块还根据读地址和写地址的差产生FIFO的空标志。当rden有效时,若写地址-1=读地址时,empty为 1;当rden无效时,若写地址=读地址时,empty为1。按照以上方式产生标志信号是为了提前一个时钟周期产生对应的标志信号。
由于空标志和满标志控制了FIFO的操作,因此标志错误会引起操作的错误。如上所述,标志的产生是通过对读写地址的比较产生的,当读写时钟完全异步时,对读写地址进行比较时,可能得出错误的结果。例如,在读地址变化过程中,由于读地址的各位变化并不同步,计算读写地址的差值,可能产生错误的差值,导致产生错误的满标志信号。若将未满标志置为满标志时,可能降低了应用的性能,降低写数据速率;而将满置标志置为未满时,执行一次写操作,则可能产生溢出错误,这对于实际应用来说是绝对应该避免的。空标志信号的产生也可能产生类似的错误。
(2) 最小深度计算
- 写时钟频率w_clk
- 读时钟频率 r_clk,
- 写时钟周期里,每B个时钟周期会有A个数据写入FIFO
- 读时钟周期里,每Y个时钟周期会有X个数据读出FIFO
F
I
F
O
D
E
P
T
H
=
b
u
r
s
t
−
l
e
n
g
t
h
−
b
u
r
s
t
−
l
e
n
g
t
h
∗
X
Y
∗
r
−
c
l
k
w
−
c
l
k
FIFO_{DEPTH} = burst_ -length - burst_-length * \frac {X}{Y} * \frac {r_-clk}{w_-clk}
FIFODEPTH=burst−length−burst−length∗YX∗w−clkr−clk
举例说明:
假设 FIFO 的写时钟为 100MHZ,读时钟为 80MHZ。在 FIFO 输入侧,每 100 个时钟,写入80 个数据;FIFO 读入测,每个时钟读取一个数据。设计合理的 FIFO 深度,使 FIFO 不会溢出:
考虑背靠背(20个clk不发数据+80clk发数据+80clk发数据+20个clk不发数据的200个clk)代入公式可计算FIFO的深度:160-(160/100)*80=32.
(3) 代码
格雷码式
摘抄博客校招Verilog——同步FIFO和异步FIFO
比较空满时,需要对读写地址进行判断,二者属于跨时钟域,需要进行打拍的同步处理,为避免亚稳态,采用格雷码,因为格雷码相邻只有一位变化,这样同步多位时更不容易产生问题。
格雷码公式:gray = (binary>>1) ^ binary;
读空判断:默认是先写后读,读追上了写,之后就是读空了。因此读空标志为“读写地址相同”。
写满判断:默认是先写后读,写在前面,超过了一轮地址后,又追上了读,之后就是写满了。因此写满标志也是“读写地址相同”吗?肯定不是! 这样的思维会导致写满和读空的标志相同,无法确定【读写地址相同】时到底是写满还是读空,因此可以设置一个写指针 wr_addr_ptr 和 读指针 rd_addr_ptr,其位宽比读写地址多1位,整个指针的长度是地址的 2 倍。
假设前半段为A,后半段为B。
- 读在A,最高位为0,剩余位为100;
- 写在B,最高位为1,剩余位为100;
我们便可以判定,这时写越过了一轮,又到了读的位置,这便是真正的写满标志。提炼一下就是:“读写的最高位不同,其余位相同”时,处于写满状态。
以上是当地址编码为普通二进制码时的分析,但是格雷码是不一样的,他的排列有些不同,前半段 A 和后半段 B 是镜像对称的,如下图所示:
通过观察格雷码的特点,我们可以这样判断:“读写的最高2位不同,其余位相同”时,处于写满状态。
①执行代码
module asFIFO
//========================< 参数 >==========================================
#(
parameter FIFO_WIDTH = 8 , //数据位宽
parameter ADDR_WIDTH = 4 , //地址位宽
parameter FIFO_DEPTH = 16 //数据深度
)
//========================< 端口 >==========================================
(
input rst_n , //复位
//FIFO写 ----------------------------------------
input wr_clk , //写时钟
input wr_en , //写使能
input [FIFO_WIDTH-1:0] wr_data , //写数据
output wr_full , //写满
//FIFO读 ----------------------------------------
input rd_clk , //读时钟
input rd_en , //读使能
output reg [FIFO_WIDTH-1:0] rd_data , //读数据
output rd_empty //读空
);
//========================< 信号 >==========================================
reg [ADDR_WIDTH :0] wr_addr_ptr ; //写指针,多1位
reg [ADDR_WIDTH :0] rd_addr_ptr ; //读指针,多1位
wire [ADDR_WIDTH :0] wr_addr_gray ; //写地址_格雷码
reg [ADDR_WIDTH :0] wr_addr_gray_r ; //写地址打拍
reg [ADDR_WIDTH :0] wr_addr_gray_rr ; //写地址打拍
wire [ADDR_WIDTH :0] rd_addr_gray ; //读地址_格雷码
reg [ADDR_WIDTH :0] rd_addr_gray_r ; //读地址打拍
reg [ADDR_WIDTH :0] rd_addr_gray_rr ; //读地址打拍
//-----------------------------------------------
wire [ADDR_WIDTH-1:0] wr_addr ; //写地址
wire [ADDR_WIDTH-1:0] rd_addr ; //读地址
reg [FIFO_WIDTH-1:0] ram[FIFO_DEPTH-1:0] ; //ram,地址位宽4,共16个
//==========================================================================
//== 地址指针
//==========================================================================
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
wr_addr_ptr <= 0;
end
else if(wr_en && (!wr_full)) begin
wr_addr_ptr <= wr_addr_ptr + 1;
end
end
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n) begin
rd_addr_ptr <= 0;
end
else if(rd_en && (!rd_empty)) begin
rd_addr_ptr <= rd_addr_ptr + 1;
end
end
//==========================================================================
//== 空满信号
//==========================================================================
//-- 格雷码转换
//---------------------------------------------------
assign wr_addr_gray = (wr_addr_ptr>>1) ^ wr_addr_ptr;
assign rd_addr_gray = (rd_addr_ptr>>1) ^ rd_addr_ptr;
//-- 跨时钟域,打两拍
//---------------------------------------------------
always @(posedge wr_clk) begin
rd_addr_gray_r <= rd_addr_gray;
rd_addr_gray_rr <= rd_addr_gray_r;
end
always @(posedge rd_clk) begin
wr_addr_gray_r <= wr_addr_gray;
wr_addr_gray_rr <= wr_addr_gray_r;
end
//-- 写满标志:高2位不同,其余位相同
//---------------------------------------------------
assign wr_full = (wr_addr_gray == ({~rd_addr_gray_rr[ADDR_WIDTH-:2],rd_addr_gray_rr[ADDR_WIDTH-2:0]}));
//-- 读空标志:读写地址相同
//---------------------------------------------------
assign rd_empty = (rd_addr_gray == wr_addr_gray_rr);
//==========================================================================
//== ram读写
//==========================================================================
//-- 读写地址
//---------------------------------------------------
assign wr_addr = wr_addr_ptr[ADDR_WIDTH-1:0];
assign rd_addr = rd_addr_ptr[ADDR_WIDTH-1:0];
//-- 写数据
//---------------------------------------------------
integer i;
always @(posedge wr_clk or negedge rst_n) begin
if(!rst_n) begin
for(i=0; i<FIFO_DEPTH; i=i+1)
ram[i] <= 0;
end
else if(wr_en && (!wr_full)) begin
ram[wr_addr] <= wr_data;
end
end
//-- 读数据
//---------------------------------------------------
always @(posedge rd_clk or negedge rst_n) begin
if(!rst_n) begin
rd_data <= 0;
end
else if(rd_en && (!rd_empty)) begin
rd_data <= ram[rd_addr];
end
end
endmodule
②仿真文件
`timescale 1ns/1ns
module asFIFO_tb;
//========================< 参数 >==========================================
parameter FIFO_WIDTH = 8 ; //数据位宽
parameter ADDR_WIDTH = 4 ; //地址位宽
parameter FIFO_DEPTH = 16 ; //数据深度
//========================< 信号 >==========================================
reg wr_clk ;
reg rd_clk ;
reg rst_n ;
reg wr_en ;
reg [FIFO_WIDTH-1:0] wr_data ;
reg rd_en ;
//==========================================================================
//== 例化
//==========================================================================
asFIFO
#(
.FIFO_WIDTH (FIFO_WIDTH ),
.ADDR_WIDTH (ADDR_WIDTH )
)
u_asFIFO
(
.wr_clk (wr_clk ),
.rd_clk (rd_clk ),
.rst_n (rst_n ),
.wr_en (wr_en ),
.wr_data (wr_data ),
.rd_en (rd_en ),
.rd_data ( ),
.wr_full ( ),
.rd_empty ( )
);
//==========================================================================
//== 时钟
//==========================================================================
always #10 wr_clk=~wr_clk;
always #5 rd_clk=~rd_clk;
//==========================================================================
//== 数据
//==========================================================================
initial begin
wr_clk = 1;
rd_clk = 0;
rst_n = 0;
wr_en = 0;
wr_data = 0;
rd_en = 0;
#101;
rst_n = 1;
#20;
gen_data;
@(posedge rd_clk);
rd_en = 1;
repeat(FIFO_DEPTH)@(posedge rd_clk);
rd_en=0;
end
task gen_data;
integer i;
begin
for(i=0; i<FIFO_DEPTH; i=i+1) begin
wr_en = 1;
wr_data = i;
#20;
end
wr_en = 0;
wr_data = 0;
end
endtask
endmodule
③仿真结果
异步FIFO原理与代码实现关于异步FIFO的关键技术,有两个,一个是格雷码减小亚稳态,另一个是指针信号跨异步时钟域的传递。
我在自己写异步FIFO的时候也很疑惑,地址指针在同步化的时候,肯定会产生至少两个周期的延迟,如果是从快时钟域到慢时钟域,快时域的地址指针并不能都被慢时域的时钟捕获,同步后的指针比起实际的指针延迟会更大。如果以此来产生fifo_empty和fifo_full 信号会非常不准确。
查找资料和仿真后发现,数字电路的世界真的很神奇,还有很多的东西需要去学习。非常巧妙,FIFO中的一个潜在的条件是write_ptr总是大于或者等于read_ptr;分为两种情况,写快读慢和写慢读快。
- 在写时钟大于读时钟时,产生fifo_empty信号,需要将write_ptr同步到读时钟域,写指针会有延时,可能比实际的写地址要小,如果不满足fifo_empty的产生条件,没问题。如果满足fifo_empty的触发条件,说明此时同步后的 write_ptr == read_ptr ,即实际的write_ptr >= read_ptr,最坏的情况就是write_ptr > read_ptr,像这种FIFO非空而产生空标志信号的情况称为“虚空”,但是也并不影响FIFO的功能。
- 在写时钟大于读时钟时,产生fifo_full信号,需要将read_ptr同步到写时钟域,读指针会有延时,可能比实际的读地址要小,如果不满足fifo_full的产生条件,没问题。如果满足fifo_full的触发条件,说明此时同步后的read_ptr == write_ptr - fifo_depth,即实际的read_ptr >= read_ptr - fifo_depth,最坏的情况就是read_ptr > read_ptr - fifo_depth,像这种FIFO非满而产生满标志信号的情况称为“虚满”,但是也并不影响FIFO的功能。
写慢读快的情况也同上,并没有大的差异,不再分析。
关于格雷码减小亚稳态,如果读写时钟差距过大,从快时钟域同步到慢时钟域的信号,时钟捕获的相邻两个数据变化并不是只有一个bit位的改变,可能导致格雷码失去原来的意义。
总结
主要用于个人学习,参考了很多!