基于vivado(语言Verilog)的FPGA学习(5)——跨时钟处理
基于vivado(语言Verilog)的FPGA学习(5)——跨时钟处理
1. 为什么要解决跨时钟处理问题
慢时钟到快时钟一般都不需要处理,关键需要解决从快时钟到慢时钟的问题,因为可能会漏信号或者失真,比如:
2.解决办法
第一种办法是开环解决方案,也就是人为设置目标信号脉宽大于1.5倍的周期。但是容易和设计要求冲突
所以第二个大方法是闭环解决方案,也就是从改善同步方式:最基础的是二级、三级寄存器。但是还是会在极端情况下出现失真,并且需要满足:
【1】级联的寄存器必须使用同一个采样时钟;
【2】发送端时钟域寄存器输出和接收端异步时钟域级联寄存器输入之间不能有任何其他组合逻辑;
【3】同步器中级联的寄存器中除了最后一个寄存器外所有的寄存器只能有一个扇出,即其只能驱动下一级寄存器的输入
于是就有了将控制信号当作使能信号进行传递(单比特的话,这个使能信号可以是信号本身):
我的理解就是弄一个使能信号当作两个时钟域的信使,来告诉对方是不是开始了读/写数据了。
为了进一步进行多比特信号的跨时钟处理,干脆就拿地址作为同步信号(下图中的wptr和rptr),用RAM作为数据的缓存区,用不同时钟域给的空/满作为数据输出/输入的标识。传输的过程需要格雷码和二进制的转换。
这一方法被称为FIFO结果处理多比特跨时钟域信号。
3.实现代码
`timescale 1ns / 1nsmodule ASFIFO#(parameter WIDTH = 16, // FIFO数据总线位宽parameter PTR = 4 // FIFO存储深度(bit数,深度只能是2^n个))(// write interfaceinput wrclk , // 写时钟input wr_rst_n, // 写指针复位input [WIDTH-1:0] wr_data , // 写数据总线input wr_en , // 写使能output reg wr_full , // 写满标志//read interfaceinput 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 ; // 格雷码读地址同步寄存器1reg [PTR:0] rd_gray_ff2 ; // 格雷码读地址同步寄存器2reg [PTR:0] rd_bin_wr ; // 同步到写时钟域的二进制读地址// 读时钟域信号定义reg [PTR:0] rd_bin ; // 二进制读地址reg [PTR:0] rd_gray ; // 格雷码读地址reg [PTR:0] wr_gray_ff1 ; // 格雷码写地址同步寄存器1reg [PTR:0] wr_gray_ff2 ; // 格雷码写地址同步寄存器2reg [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) beginif (!wr_rst_n) beginwr_bin <= 'b0;endelse if ( wr_en == 1'b1 && wr_full == 1'b0 ) beginwr_bin <= wr_bin + 1'b1;endelse beginwr_bin <= wr_bin;endend// 写地址:二进制转格雷码always @(posedge wrclk or posedge wr_rst_n) beginif (!wr_rst_n) beginwr_gray <= 'b0;endelse beginwr_gray <= { wr_bin[PTR], wr_bin[PTR:1] ^ wr_bin[PTR-1:0] };endend// 格雷码读地址同步至写时钟域always @(posedge wrclk or posedge wr_rst_n) beginif(!wr_rst_n) beginrd_gray_ff1 <= 'b0;rd_gray_ff2 <= 'b0;endelse beginrd_gray_ff1 <= rd_gray;rd_gray_ff2 <= rd_gray_ff1;endend// 同步后的读地址解格雷always @(*) beginrd_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 @(*) beginif( (wr_bin[PTR] != rd_bin_wr[PTR]) && (wr_bin[PTR-1:0] == rd_bin_wr[PTR-1:0]) ) beginwr_full = 1'b1;endelse beginwr_full = 1'b0;endend// 读时钟域 //always @(posedge rdclk or posedge rd_rst_n) beginif (!rd_rst_n) beginrd_bin <= 'b0;endelse if ( rd_en == 1'b1 && rd_empty == 1'b0 ) beginrd_bin <= rd_bin + 1'b1;endelse beginrd_bin <= rd_bin;endend// 读地址:二进制转格雷码always @(posedge rdclk or posedge rd_rst_n) beginif (!rd_rst_n) beginrd_gray <= 'b0;endelse beginrd_gray <= { rd_bin[PTR], rd_bin[PTR:1] ^ rd_bin[PTR-1:0] };endend// 格雷码写地址同步至读时钟域always @(posedge rdclk or posedge rd_rst_n) beginif(!rd_rst_n) beginwr_gray_ff1 <= 'b0;wr_gray_ff2 <= 'b0;endelse beginwr_gray_ff1 <= wr_gray;wr_gray_ff2 <= wr_gray_ff1;endend// 同步后的写地址解格雷always @(*) beginwr_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 @(*) beginif( wr_bin_rd == rd_bin )rd_empty = 1'b1;elserd_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 / 1nsmodule 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)beginif (!wr_rst_n) beginmemory[wr_addr] <= 0;endelse if(wr_en) beginmemory[wr_addr] <= wr_data;endelse beginmemory[wr_addr] <= memory[wr_addr];endend//读always@(posedge rd_clk)beginif(! rd_rst_n) beginrd_data <= 0;endif(rd_en) beginrd_data <= memory[rd_addr];endelse beginrd_data <= rd_data;endend
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) beginif(init_done) beginif( wr_full == 1'b0 )wr_data <= wr_data + 1;elsewr_data <= wr_data;endelse beginwr_data <= 'b0;endend// 异步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) beginif(init_done) beginif( wr_full == 1'b0 )wr_data <= wr_data + 1;elsewr_data <= wr_data;endelse beginwr_data <= 'b0;endend
但实际情况很有可能是实时处理,数据是源源不断传来,所以还是在满足快时钟同步至慢时钟的不漏报情况下,就需要衡量最长持续数据传输长度和RAM容积大小。当持续传输数据有n个时,就需要至少m*n的RAM。m=快时钟频率/慢时钟频率
参考:
https://www.codenong.com/cs105834073/
https://blog.csdn.net/qq_40807206/article/details/109555162