基于vivado(语言Verilog)的FPGA学习(5)——跨时钟处理
1. 为什么要解决跨时钟处理问题
慢时钟到快时钟一般都不需要处理,关键需要解决从快时钟到慢时钟的问题,因为可能会漏信号或者失真,比如:
2.解决办法
第一种办法是开环解决方案,也就是人为设置目标信号脉宽大于1.5倍的周期。但是容易和设计要求冲突
所以第二个大方法是闭环解决方案,也就是从改善同步方式:最基础的是二级、三级寄存器。但是还是会在极端情况下出现失真,并且需要满足:
【1】级联的寄存器必须使用同一个采样时钟;
【2】发送端时钟域寄存器输出和接收端异步时钟域级联寄存器输入之间不能有任何其他组合逻辑;
【3】同步器中级联的寄存器中除了最后一个寄存器外所有的寄存器只能有一个扇出,即其只能驱动下一级寄存器的输入
于是就有了将控制信号当作使能信号进行传递(单比特的话,这个使能信号可以是信号本身):
我的理解就是弄一个使能信号当作两个时钟域的信使,来告诉对方是不是开始了读/写数据了。
为了进一步进行多比特信号的跨时钟处理,干脆就拿地址作为同步信号(下图中的wptr和rptr),用RAM作为数据的缓存区,用不同时钟域给的空/满作为数据输出/输入的标识。传输的过程需要格雷码和二进制的转换。
这一方法被称为FIFO结果处理多比特跨时钟域信号。
3.实现代码
`timescale 1ns / 1ns
module ASFIFO#
(
parameter WIDTH = 16, // FIFO数据总线位宽
parameter PTR = 4 // FIFO存储深度(bit数,深度只能是2^n个)
)
(
// write interface
input wrclk , // 写时钟
input wr_rst_n, // 写指针复位
input [WIDTH-1:0] wr_data , // 写数据总线
input wr_en , // 写使能
output reg wr_full , // 写满标志
//read interface
input rdclk , // 读时钟
input rd_rst_n, // 读指针复位
input rd_en , // 读使能
output [WIDTH-1:0] rd_data , // 读数据输出
output reg rd_empty // 读空标志
);
// 写时钟域信号定义
reg [PTR:0] wr_bin ; // 二进制写地址
reg [PTR:0] wr_gray ; // 格雷码写地址
reg [PTR:0] rd_gray_ff1 ; // 格雷码读地址同步寄存器1
reg [PTR:0] rd_gray_ff2 ; // 格雷码读地址同步寄存器2
reg [PTR:0] rd_bin_wr ; // 同步到写时钟域的二进制读地址
// 读时钟域信号定义
reg [PTR:0] rd_bin ; // 二进制读地址
reg [PTR:0] rd_gray ; // 格雷码读地址
reg [PTR:0] wr_gray_ff1 ; // 格雷码写地址同步寄存器1
reg [PTR:0] wr_gray_ff2 ; // 格雷码写地址同步寄存器2
reg [PTR:0] wr_bin_rd ; // 同步到读时钟域的二进制写地址
// 解格雷码电路循环变量
integer i ;
integer j ;
// DPRAM控制信号
wire dpram_wr_en ; // DPRAM写使能
wire [PTR-1:0] dpram_wr_addr ; // DPRAM写地址
wire [WIDTH-1:0] dpram_wr_data ; // DPRAM写数据
wire dpram_rd_en ; // DPRAM读使能
wire [PTR-1:0] dpram_rd_addr ; // DPRAM读地址
wire [WIDTH-1:0] dpram_rd_data ; // DPRAM读数据
// ******************************** 写时钟域 ******************************** //
// 二进制写地址递增
always @(posedge wrclk or posedge wr_rst_n) begin
if (!wr_rst_n) begin
wr_bin <= 'b0;
end
else if ( wr_en == 1'b1 && wr_full == 1'b0 ) begin
wr_bin <= wr_bin + 1'b1;
end
else begin
wr_bin <= wr_bin;
end
end
// 写地址:二进制转格雷码
always @(posedge wrclk or posedge wr_rst_n) begin
if (!wr_rst_n) begin
wr_gray <= 'b0;
end
else begin
wr_gray <= { wr_bin[PTR], wr_bin[PTR:1] ^ wr_bin[PTR-1:0] };
end
end
// 格雷码读地址同步至写时钟域
always @(posedge wrclk or posedge wr_rst_n) begin
if(!wr_rst_n) begin
rd_gray_ff1 <= 'b0;
rd_gray_ff2 <= 'b0;
end
else begin
rd_gray_ff1 <= rd_gray;
rd_gray_ff2 <= rd_gray_ff1;
end
end
// 同步后的读地址解格雷
always @(*) begin
rd_bin_wr[PTR] = rd_gray_ff2[PTR];
for ( i=PTR-1; i>=0; i=i-1 )
rd_bin_wr[i] = rd_bin_wr[i+1] ^ rd_gray_ff2[i];
end
// 写时钟域产生写满标志
always @(*) begin
if( (wr_bin[PTR] != rd_bin_wr[PTR]) && (wr_bin[PTR-1:0] == rd_bin_wr[PTR-1:0]) ) begin
wr_full = 1'b1;
end
else begin
wr_full = 1'b0;
end
end
// ******************************** 读时钟域 ******************************** //
always @(posedge rdclk or posedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_bin <= 'b0;
end
else if ( rd_en == 1'b1 && rd_empty == 1'b0 ) begin
rd_bin <= rd_bin + 1'b1;
end
else begin
rd_bin <= rd_bin;
end
end
// 读地址:二进制转格雷码
always @(posedge rdclk or posedge rd_rst_n) begin
if (!rd_rst_n) begin
rd_gray <= 'b0;
end
else begin
rd_gray <= { rd_bin[PTR], rd_bin[PTR:1] ^ rd_bin[PTR-1:0] };
end
end
// 格雷码写地址同步至读时钟域
always @(posedge rdclk or posedge rd_rst_n) begin
if(!rd_rst_n) begin
wr_gray_ff1 <= 'b0;
wr_gray_ff2 <= 'b0;
end
else begin
wr_gray_ff1 <= wr_gray;
wr_gray_ff2 <= wr_gray_ff1;
end
end
// 同步后的写地址解格雷
always @(*) begin
wr_bin_rd[PTR] = wr_gray_ff2[PTR];
for ( j=PTR-1; j>=0; j=j-1 )
wr_bin_rd[j] = wr_bin_rd[j+1] ^ wr_gray_ff2[j];
end
// 读时钟域产生读空标志
always @(*) begin
if( wr_bin_rd == rd_bin )
rd_empty = 1'b1;
else
rd_empty = 1'b0;
end
// RTL双口RAM例化
DPRAM
# ( .WIDTH(16), .DEPTH(16), .ADDR(4) )
U_DPRAM
(
.wr_clk (wrclk ),
.rd_clk (rdclk ),
.rd_rst_n (rd_rst_n ),
.wr_rst_n (wr_rst_n ),
.wr_en (dpram_wr_en ),
.rd_en (dpram_rd_en ),
.wr_data (dpram_wr_data ),
.rd_data (dpram_rd_data ), //唯一输出output
.wr_addr (dpram_wr_addr ),
.rd_addr (dpram_rd_addr )
);
// 产生DPRAM读写控制信号
assign dpram_wr_en = ( wr_en == 1'b1 && wr_full == 1'b0 )? 1'b1 : 1'b0;
assign dpram_wr_data = wr_data;
assign dpram_wr_addr = wr_bin[PTR-1:0];
assign dpram_rd_en = ( rd_en == 1'b1 && rd_empty == 1'b0 )? 1'b1 : 1'b0;
assign rd_data = dpram_rd_data;
assign dpram_rd_addr = rd_bin[PTR-1:0];
endmodule
其中的DPRAM就是一个数据缓存区,根据wr_en&~wr_full来作为写操作使能,控制数据写入RAM中。RAM模块定义如下:
`timescale 1ns / 1ns
module DPRAM#(parameter WIDTH = 8 ,parameter DEPTH = 16,parameter ADDR = 4)(
input wr_clk,
input rd_clk,
input rd_rst_n,
input wr_rst_n,
input wr_en,
input rd_en,
input [WIDTH-1:0]wr_data,
output reg [WIDTH-1:0]rd_data,
input [ADDR-1:0]wr_addr,
input[ADDR-1:0]rd_addr
);
reg [WIDTH-1:0] memory[DEPTH-1:0];
//写
always@(posedge wr_clk)begin
if (!wr_rst_n) begin
memory[wr_addr] <= 0;
end
else if(wr_en) begin
memory[wr_addr] <= wr_data;
end
else begin
memory[wr_addr] <= memory[wr_addr];
end
end
//读
always@(posedge rd_clk)begin
if(! rd_rst_n) begin
rd_data <= 0;
end
if(rd_en) begin
rd_data <= memory[rd_addr];
end
else begin
rd_data <= rd_data;
end
end
endmodule
testbench如下:
`timescale 1ns / 1ns
module ASFIFO_tb;
parameter WIDTH = 16;
parameter PTR = 4;
// 写时钟域tb信号定义
reg wrclk ;
reg wr_rst_n ;
reg [WIDTH-1:0] wr_data ;
reg wr_en ;
wire wr_full ;
// 读时钟域tb信号定义
reg rdclk ;
reg rd_rst_n ;
wire [WIDTH-1:0] rd_data ;
reg rd_en ;
wire rd_empty ;
// testbench自定义信号
reg init_done ; // testbench初始化结束
// FIFO初始化
initial begin
// 输入信号初始化
wr_rst_n = 1 ;
rd_rst_n = 1 ;
wrclk = 0 ;
rdclk = 0 ;
wr_en = 1 ;
rd_en = 1 ;
wr_data = 'b0 ;
init_done = 0 ;
// FIFO复位
#30 wr_rst_n = 0;
rd_rst_n = 0;
#30 wr_rst_n = 1;
rd_rst_n = 1;
// 初始化完毕
#30 init_done = 1;
end
// 写时钟
always
#2 wrclk = ~wrclk;
// 读时钟
always
#4 rdclk = ~rdclk;
// 写入数据自增
always @(posedge wrclk) begin
if(init_done) begin
if( wr_full == 1'b0 )
wr_data <= wr_data + 1;
else
wr_data <= wr_data;
end
else begin
wr_data <= 'b0;
end
end
// 异步fifo例化
ASFIFO
# ( .WIDTH(16), .PTR(4) )
U_ASFIFO
(
.wrclk (wrclk ),
.wr_rst_n (wr_rst_n ),
.wr_data (wr_data ),
.wr_en (wr_en ),
.wr_full (wr_full ),
.rdclk (rdclk ),
.rd_rst_n (rd_rst_n ),
.rd_data (rd_data ),
.rd_en (rd_en ),
.rd_empty (rd_empty )
);
endmodule
对应的框架图自己重新画了一遍,思路清晰很多。
看时序图的时候,可以将RAM模块的端口也画出来,方便看地址变化:
时序图:
上图中,上下两个读写使能wr_en和rd_en分别表示DPRAM例划前后的:
assign dpram_wr_en = ( wr_en == 1'b1 && wr_full == 1'b0 )? 1'b1 : 1'b0;
assign dpram_rd_en = ( rd_en == 1'b1 && rd_empty == 1'b0 )? 1'b1 : 1'b0;
3.1 细节一:写地址和同步读地址的比较
通过时序图,可以看出写地址是与两帧(相对于wr_clk时钟)前的同步读地址相比较。
上图可以看出当写地址为6时,读地址的前两帧才是6,因为为了仿真亚稳态出现,读地址过来对比是经过了两级触发器。
3.2 RAM的具体数据情况
指针所指的时刻为上时序图中黄线时刻,也就是wr_full第一次变为1时。
从代码中可以看出RAM例划前地址为5位,例划后只取4位,现在明白了原因:
第一位用来判断是写指针超过读指针一圈了(满标识:第一位地址相反,其余相同),还是写指针和读指针在一起(空标识:5位地址全部相反)。
3.3 实时性的要求
在testbench中,有这么一段,意思就是如果该RAM已满时,就不自增数据(我的理解就是不添加新的数据了):
// 写入数据自增
always @(posedge wrclk) begin
if(init_done) begin
if( wr_full == 1'b0 )
wr_data <= wr_data + 1;
else
wr_data <= wr_data;
end
else begin
wr_data <= 'b0;
end
end
但实际情况很有可能是实时处理,数据是源源不断传来,所以还是在满足快时钟同步至慢时钟的不漏报情况下,就需要衡量最长持续数据传输长度和RAM容积大小。当持续传输数据有n个时,就需要至少m*n的RAM。m=快时钟频率/慢时钟频率
参考:
https://www.codenong.com/cs105834073/
https://blog.csdn.net/qq_40807206/article/details/109555162