🏡 博客首页:安静到无声
⛳️ 欢迎关注: ❤️ 点赞 🎒 收藏 ✏️ 留言
🚧 系列专栏:跨时钟域
🎈 如有错误不吝赐教

异步FIFO是用来在两个异步时钟域间传输数据。

System X利用xclk时钟将数据写入FIFO,并利用System X利用yclk时钟进行输出。
其中fifo_full和fifo_empty分别是满标志和空标志,用于说明数据状态,当fifo_full时,不再进行数据的写入,当fifo_empty时不再进行数据的读取。
在产生FIFO满信号时,要将写指针和读指针进行比较,由于两个指针分别在各自的时钟域,彼此之间是异步的,在使用二进制进行计数器实现指针时,就会导致用于比较的指针取样错误。
使用自然二进制码计数时,相邻数据之间可能会产生多bit的变化。这会产生较大的尖峰电流以及其他问题。
比如,二进制计数器的值会从FFF变为000。这时所有位会同时改变。虽然能通过同步计数器避免亚稳态,但是仍然能得到极不相关的取样值,所以同步计数器不是最终的解决方案。
从FFF 到000可能的转换:
- FFF→000
- FFF→001
- FFF→010
- FFF→011
- FFF→100
- FFF→101
- FFF→110
- FFF→111
如果同步时钟边沿在FFF向000转换的中间位置到来,就可能将三位二进制数的任何值取样并同步到新的时钟域中。鉴于以上情况,强烈建议避免使用二进制计数器实现读、写指针。
格雷码是一种相邻数据只有1bit变化的码制。因此可以使用格雷码去取代二进制计数器,并且用打拍的方式去同步(跨时钟域问题(二)(单bit信号跨时钟域 1. 电平同步器 2. 边沿同步器 3. 脉冲检测器))(只有深度为2的n次方才能用格雷码的方式去同步,这样才能保证最大值和最小值只有一位的变化)。我们可以将指针转为格雷码同步到另一个时钟域再进行比较。如果同步时钟在计数值转换期间到来,这种编码能够消除绝大部分的错误。

自然二进制编码转换为格雷码如下:

g r a y _ c o d e = b i n a r y _ c o d e ⊕ ( b i n a r y _ c o d e > > 1 ) gray\_code = binary\_code \oplus (binary\_code > > 1) gray_code=binary_code⊕(binary_code>>1)
格雷码转化为自然二进制


我们以牛客网VL47 格雷码计数器为例!

代码
`timescale 1ns/1ns
module gray_counter(
input clk ,
input rst_n ,
output reg [ 3:0] gray_out
);
reg [ 3:0] binary_cnt ;
reg flag ;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
flag <= 1'd0;
end
else begin
flag <= ~flag;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
binary_cnt <= 1'd0;
end else begin
if (flag == 1'd1) begin
binary_cnt <= binary_cnt + 1'd1;
end else begin
binary_cnt <= binary_cnt;
end
end
end
always @(*) begin
gray_out <= binary_cnt ^ (binary_cnt >> 1);
end
endmodule
在判断写满时,要将读地址指针同步到写地址指针的时钟域;在判读读空是,要将写地址指针同步到读地址指针的时钟域。
对于“空”的判断依然依据二者完全相等(包括MSB);
而对于“满”的判断,如下图,由于gray码除了MSB外,具有镜像对称的特点,当读指针指向7,写指针指向8时,除了MSB,其余位皆相同,不能说它为满。因此不能单纯的只检测最高位了,在gray码上判断为满必须同时满足以下3条:

module asyn_fifo
#(
parameter data_width = 16 ,//数据的宽度
parameter data_depth = 8 ,//数据的深度
parameter ram_depth = 256 //定义ram
)
(
//复位时钟
input rst_n ,//复位时钟
//写时钟域
input wr_clk ,//写时钟域时钟
input wr_en ,//写使能
input [data_width-1:0] data_in ,//写入数据
output full ,//满标志
//读时钟域
input rd_clk ,//读时钟域时钟
input rd_en ,//读使能
output reg [data_width-1:0] data_out ,//读出数据
output empty //满标志
);
//定义双端口ram的读写地址
wire [data_depth-1:0] wr_adr ;//双端口ram的写地址
wire [data_depth-1:0] rd_adr ;//双端口ram的读地址
//定义双端口ram的读写指针!为什么比读写地址多一位,在博客中讲解!
reg [data_depth:0] wr_adr_ptr ;//写指针
reg [data_depth:0] rd_adr_ptr ;//读指针
//转换为格雷码进行打拍操作
wire [data_depth:0] wr_adr_gray ;//写地址指针二进制转化为格雷码
reg [data_depth:0] wr_adr_gray1 ;//打一拍缓存
reg [data_depth:0] wr_adr_gray2 ;//打两拍缓存
wire [data_depth:0] rd_adr_gray ;//读地址指针二进制转化为格雷码
reg [data_depth:0] rd_adr_gray1 ;//打一拍缓存
reg [data_depth:0] rd_adr_gray2 ;//打两拍缓存
//读写地址比控制指针少一位
assign wr_adr = wr_adr_ptr[data_depth-1:0];
assign rd_adr = rd_adr_ptr[data_depth-1:0];
//开辟一段内存空间
reg [data_width-1:0] ram_fifo [ram_depth-1:0] ;//data_width*ram_depth
//写数据
integer i;
always @(posedge wr_clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < ram_depth; i = i + 1) begin
ram_fifo[i] <= 'd0;
end
end else begin
if(wr_en && (~full)) begin //如果写使能开启,且未满
ram_fifo[wr_adr] <= data_in;
end else begin
ram_fifo[wr_adr] <= ram_fifo[wr_adr];
end
end
end
//读数据
always @(posedge rd_clk or negedge rst_n) begin
if (!rst_n) begin
data_out <= 'd0;
end else begin
if(rd_en && (~empty)) begin //如果读使能开启,且未空
data_out <= ram_fifo [rd_adr];
end else begin
data_out <= 'd0; //否则读出为0
end
end
end
//写地址指针控制
always @(posedge wr_clk or negedge rst_n) begin
if (!rst_n) begin
wr_adr_ptr <= 'd0;
end else begin
if(wr_en && (~full)) begin
wr_adr_ptr <= wr_adr_ptr + 1'b1;
end else begin
wr_adr_ptr <= wr_adr_ptr;
end
end
end
//读地址指针控制
always @(posedge rd_clk or negedge rst_n) begin
if (!rst_n) begin
rd_adr_ptr <= 'd0;
end else begin
if(rd_en && (~empty)) begin
rd_adr_ptr <= rd_adr_ptr + 1'b1;
end else begin
rd_adr_ptr <= rd_adr_ptr;
end
end
end
//二进制指针转化为格雷码
assign wr_adr_gray = (wr_adr_ptr >> 1) ^ wr_adr_ptr;
assign rd_adr_gray = (rd_adr_ptr >> 1) ^ rd_adr_ptr;
//格雷码的同步 读时钟域同步到写时钟域
always @(posedge wr_clk or negedge rst_n) begin
if (!rst_n) begin
rd_adr_gray1 <= 'd0;
rd_adr_gray2 <= 'd0;
end else begin
rd_adr_gray1 <= rd_adr_gray;
rd_adr_gray2 <= rd_adr_gray1;
end
end
//格雷码的同步 写时钟域同步到读时钟域
always @(posedge rd_clk or negedge rst_n) begin
if (!rst_n) begin
wr_adr_gray1 <= 'd0;
wr_adr_gray2 <= 'd0;
end else begin
wr_adr_gray1 <= wr_adr_gray;
wr_adr_gray2 <= wr_adr_gray1;
end
end
//空标志---->表示读空---->同步到读时钟域的写地址指针和读地址指针相同
assign empty = (rd_adr_gray == wr_adr_gray2) ? 1'b1 : 1'b0;
assign full = (wr_adr_gray[data_depth] != rd_adr_gray2[data_depth]) && (wr_adr_gray[data_depth-1] != rd_adr_gray2[data_depth-1]) && (wr_adr_gray[data_depth-2:0] == rd_adr_gray2[data_depth-2:0]);
endmodule
仿真
`timescale 1ns / 1ps
module asyn_fifo_tb;
reg rst_n ;
reg wr_clk ;
reg wr_en ;
reg [ 15:0] data_in ;
wire full ;
reg rd_clk ;
reg rd_en ;
wire [ 15:0] data_out ;
wire empty ;
asyn_fifo asyn_fifo_inst
(
.rst_n (rst_n ),
.wr_clk (wr_clk ),
.wr_en (wr_en ),
.data_in (data_in ),
.full (full ),
.rd_clk (rd_clk ),
.rd_en (rd_en ),
.data_out (data_out ),
.empty (empty )
);
initial wr_clk = 0;
always#10 wr_clk = ~wr_clk;
initial rd_clk = 0;
always#30 rd_clk = ~rd_clk;
always@(posedge wr_clk or negedge rst_n)begin
if(!rst_n)
data_in <= 'd0;
else if(wr_en)
data_in <= data_in + 1'b1;
else
data_in <= data_in;
end
initial begin
rst_n = 0;
wr_en = 0;
rd_en = 0;
#200;
rst_n = 1;
wr_en = 1;
#6000;
wr_en = 0;
rd_en = 1;
#18000;
rd_en = 0;
$stop;
end
endmodule
仿真结果

vivado的工程已将经传至地址