[FPGA11]CDC_多比特跨时钟域传输

CDC,多比特跨时钟域传输

  • 跨时钟域传输CDC(Clock Domain Conversion)问题,是在日常FPGA设计过程中经常遇到的问题,故针对不同情况,即单比特还是多比特,从慢时钟到快时钟还是快时钟到慢时钟,整理出对应的解决思路。
  • 该问题的详解将分为单比特和多比特两个章节进行,本次将整理多比特跨时钟域传输问题。

异步FIFO

  • 对于跨时钟域的传输问题,我们最常使用的处理方式就是在两个时钟域之间放置一个异步FIFO。无论是从快时钟域到慢时钟域,还是慢时钟域到快时钟域,都可以使用异步FIFO进行处理。通常来说,我们都是直接调用XILINX的的官方IP核,进行实现。但为什么异步FIFO能够解决跨时钟域问题呢,具体的实现细节是怎么样的呢?

对比同步FIFO

  • 对于同步FIFO而言,不存在跨时钟域的问题,我们很容易通过一个计数器来判断空满,由于读写FIFO都在同一个时钟域,所以可以使用如下思路:写一个数据,计数器加1;读一个数据,计数器减1;如此,当计数器为0,代表写了多少个数据就读了多少个数据。具体如下图所示:
  • 从图中不难看出,写指针始终指向下一个需要写入的RAM空间,而读指针总是指向当前要读取的数据空间。当FIFO复位时,读写指针指向0地址处,之后每写一个数据,写指针移动到下一个地址处,读一个数据,读指针也移动到下一个地址处。正常的状态应该是读指针一直在追写指针,这样的话就不会发生读空的情况。上面也说了,对于同步FIFO来说,我们可以设置一个计数器,写一个数据,计数器加1,读一个数据,计数器减1,如果计数器为0,则表示为空,如果计数器为N(N为FIFO深度)则为满。

异步FIFO跨时钟域设计

  • 对于异步FIFO,能否仿效同步FIFO,也设置一个计数器呢?答案是不能,因为异步FIFO的读写指针位于不同的时钟域,计数器无论是在写时钟域还是在读时钟域内计数,都是不合适的。所以我们只能通过判断读写指针的关系进行判断FIFO是空还是满。但是参照同步FIFO,当读写指针相等时,FIFO有可能为空,也有可能为满。为消除该模糊状态,我们可以采用对读写指针多加一位指示位的方法,在写时钟域内,如果写一个数据,写指针加1,即移动到下一个地址处;在读时钟域内,如果读一个数据,则读指针加1,即移动到下一个地址处;当读写指针的最高位(MSB)(most significant bit)相等,且剩余其他位也相等时,表示FIFO为空;当读写指针的最高位(MSB)不等,但是剩余其他位相等时,表示FIFO为满。
  • 但如果我们想要去判决读写指针时,又会发现读写指针位于两个不同的时钟域内,故我们先要将读指针同步到写时钟域内,再进行判断。具体操作过程如下:首先,当读一个数据,读指针加1,这里用的还是二进制编码,之后对这个读指针变换为格雷码,寄存一拍,然后用写时钟两级同步,在将同步过来的信号转换为二进制编码,寄存一拍与写指针比较。判断是否为空时,需要将写指针同步到读时钟域,方式同上。
  • 转换成格雷码的好处在于,指针地址每次只会变化一位,以减少多比特变化导致多寄存器变化从而带来的毛刺。将格雷码的输出寄存一拍是为了对组合逻辑电路进行时序采样,从而消除竞争导致险象。对输出在另一个时钟域内打两拍可实现跨时钟域操作。
  • 在上述判断过程中,在判断是否为空时,可以看到,将写指针同步到读时钟域需要几个周期的同步时间,在同步过去后,与读指针进行比较,判断是否为FIFO空。由于在这段时间内,读数据有可能也正在继续,所以,需要存在着几个未读取的空间,也就是可用于读取的位置比实际的要多出几个,这样不会出现下溢的情况,也即读空了继续读的情况。同理,在判断是否为满时,需要将读指针同步到写时钟域,花费几个周期时间与写指针进行比较,判断FIFO是否为满,这段时间内,写数据有可能也在继续,所以,还保留着几个未写满的空间,也就是可用于写入的位置比实际的要多出几个。这样做才不会引起上溢,即写数据覆盖原来的数据。

设计代码

主程序:

module  asyfifo#(
	parameter DATA_WIDTH = 8,
	ADDR_WIDTH = 4,
	FIFO_DEPTH = (1<<ADDR_WIDTH)
	)(
		input rst_n,
		//write ports
		input wrclk,
		input wren,
		input data_in,
		output wr_full,
 
		//read ports
		input rdclk,
		input rden,
		output data_out,
		output rd_empty
 
	);
 
	//write pointer control logic
	//write pointer with wraparound and no wraparound
	reg [ADDR_WIDTH : 0] wr_ptr_wrap, wr_ptr_wrap_nxt;
	wire [ADDR_WIDTH - 1 : 0] wr_ptr;
	
	//write pointer with wraparound state change
	always@(posedge wrclk or negedge rst_n) begin
		if(~rst_n) wr_ptr_wrap <= 0;
		else wr_ptr_wrap <= wr_ptr_wrap_nxt;
	end
 
	always@(*) begin
		wr_ptr_wrap_nxt = wr_ptr_wrap;
		if(wren) wr_ptr_wrap_nxt = wr_ptr_wrap_nxt + 1;
		else ;
	end
	//convert the binary write pointer to gray, flop it, and then pass it to read domain
	reg [ADDR_WIDTH : 0] wr_ptr_wrap_gray;
	wire [ADDR_WIDTH : 0] wr_ptr_wrap_gray_nxt;
	//instantiate the module binary to gray
	binary_to_gray #(.WIDTH(ADDR_WIDTH)) inst_binary_to_gray_wr(
		.binary_value(wr_ptr_wrap_nxt),
		.gray_value(wr_ptr_wrap_gray_nxt)
	);
	always@(posedge wrclk or negedge rst_n) begin
		if(~rst_n) wr_ptr_wrap_gray <= 0;
		else wr_ptr_wrap_gray <= wr_ptr_wrap_gray_nxt; 
	end
	//synchronize wr_ptr_wrap_gray into read clock domain
	reg [ADDR_WIDTH:0] wr_ptr_wrap_gray_r1, wr_ptr_wrap_gray_r2;
	always@(posedge rdclk or negedge rst_n) begin
		if(~rst_n) begin
			wr_ptr_wrap_gray_r1 <= 0;
			wr_ptr_wrap_gray_r2 <= 0;
		end
		else begin
			wr_ptr_wrap_gray_r1 <= wr_ptr_wrap_gray;
			wr_ptr_wrap_gray_r2 <= wr_ptr_wrap_gray_r1;
		end
	end
	//convert wr_ptr_wrap_gray_r2 back to binary form
	reg [ADDR_WIDTH : 0] wr_ptr_wrap_rdclk;
	wire [ADDR_WIDTH : 0] wr_ptr_wrap_rdclk_nxt;
	gray_to_binary #(.WIDTH(ADDR_WIDTH)) inst_gray_to_binary_wr(
		.gray_value(wr_ptr_wrap_gray_r2),
		.binary_value(wr_ptr_wrap_rdclk_nxt)
	);
	always@(posedge rdclk or negedge rst_n) begin
		if(~rst_n) wr_ptr_wrap_rdclk <= 0;
		else wr_ptr_wrap_rdclk <= wr_ptr_wrap_rdclk_nxt; 
	end
	assign wr_ptr = wr_ptr_wrap[ADDR_WIDTH - 1 : 0];
	
	//read pointer control logic
	//read pointer with wraparound and no wraparound
	reg [ADDR_WIDTH : 0] rd_ptr_wrap, rd_ptr_wrap_nxt;
	wire [ADDR_WIDTH - 1 : 0] rd_ptr;
	//read pointer with wraparound state change
	always@(posedge rdclk or negedge rst_n) begin
		if(~rst_n) rd_ptr_wrap <= 0;
		else rd_ptr_wrap <= rd_ptr_wrap_nxt; 
	end
	always@(*) begin
		rd_ptr_wrap_nxt = rd_ptr_wrap;
		if(rden) rd_ptr_wrap_nxt = rd_ptr_wrap_nxt + 1;
		else ;
	end
	//convert binary read pointer to gray
	reg [ADDR_WIDTH : 0] rd_ptr_wrap_gray;
	wire [ADDR_WIDTH : 0] rd_ptr_wrap_gray_nxt;
	binary_to_gray #(.WIDTH(ADDR_WIDTH)) inst_binary_to_gray_rd(
		.binary_value(rd_ptr_wrap_nxt),
		.gray_value(rd_ptr_wrap_gray_nxt)
	);
	always@(posedge rdclk or negedge rst_n) begin
		if(~rst_n) rd_ptr_wrap_gray <= 0;
		else rd_ptr_wrap_gray <= rd_ptr_wrap_gray_nxt; 
	end
	//synchronize rd_ptr_wrap_gray into write clock domain
	reg [ADDR_WIDTH : 0] rd_ptr_wrap_gray_r1, rd_ptr_wrap_gray_r2;
	always@(posedge wrclk or negedge rst_n) begin
		if(~rst_n) begin
			rd_ptr_wrap_gray_r1 <= 0;
			rd_ptr_wrap_gray_r2 <= 0;
		end
		else begin
			rd_ptr_wrap_gray_r1 <= rd_ptr_wrap_gray;
			rd_ptr_wrap_gray_r2 <= rd_ptr_wrap_gray_r1;
		end
	end
	//convert rd_ptr_wrap_gray_r2 into binary form
	reg [ADDR_WIDTH : 0] rd_ptr_wrap_wrclk;
	wire [ADDR_WIDTH : 0] rd_ptr_wrap_wrclk_nxt;
	gray_to_binary #(.WIDTH(ADDR_WIDTH)) inst_gray_to_binary_rd(
		.gray_value(rd_ptr_wrap_gray_r2),
		.binary_value(rd_ptr_wrap_wrclk_nxt)
	);
	always@(posedge wrclk or negedge rst_n) begin
		if(~rst_n) rd_ptr_wrap_wrclk <= 0;
		else rd_ptr_wrap_wrclk <= rd_ptr_wrap_wrclk_nxt; 
	end
	assign rd_ptr = rd_ptr_wrap[ADDR_WIDTH - 1 : 0];
 
	wire wr_full_nxt;
	reg wr_full;
	assign wr_full_nxt = (wr_ptr_wrap_nxt[ADDR_WIDTH] != rd_ptr_wrap_wrclk_nxt[ADDR_WIDTH]) && (wr_ptr_wrap_nxt[ADDR_WIDTH - 1: 0] 
		== rd_ptr_wrap_wrclk_nxt[ADDR_WIDTH - 1 : 0]);
 
	always@(posedge wrclk or negedge rst_n) begin
		if(~rst_n) wr_full <= 0;
		else wr_full <= wr_full_nxt; 
	end
 
	wire rd_empty_nxt;
	reg rd_empty;
	assign rd_empty_nxt = (rd_ptr_wrap_nxt[ADDR_WIDTH] == wr_ptr_wrap_rdclk_nxt[ADDR_WIDTH])&&(rd_ptr_wrap_nxt[ADDR_WIDTH - 1:0]
	 == wr_ptr_wrap_rdclk_nxt[ADDR_WIDTH - 1 : 0] );
 
	always@(posedge rdclk or negedge rst_n) begin
		if(~rst_n) rd_empty <= 0;
		else rd_empty <= rd_empty_nxt;
	end
 
  sram #(.ADDR_WIDTH(ADDR_WIDTH),
		.DATA_WIDTH(DATA_WIDTH)
		) inst_sram(
		.wren(wren),
		.wraddr(wr_ptr),
		.wrdata(data_in),
		.rden(rden),
		.rdaddr(rd_ptr),
		.rddata(data_out)
		);
 
endmodule

二进制转格雷码:

module  binary_to_gray#( parameter WIDTH = 4 )(
	input [WIDTH:0] binary_value,
	output [WIDTH:0] gray_value
	);
	assign gray_value = (binary_value >> 1) ^ binary_value;
endmodule

格雷码转二进制

module  gray_to_binary #( parameter WIDTH = 4)(
	input [WIDTH : 0] gray_value,
	output [WIDTH : 0] binary_value
	);
	
	assign binary_value[WIDTH] = gray_value[WIDTH];
	genvar i;
	generate
		for(i = 0; i <WIDTH - 1; i = i + 1) begin
			assign binary_value[i] = gray_value[i] ^ binary_value[i + 1];
		end
	endgenerate
endmodule

双端口SRAM

module  sram #(
	parameter ADDR_WIDTH = 4,
	DATA_WIDTH = 8
	)(
	input wren,
	input wrdata,
	input wraddr,
	
	input rden,
	input rdaddr,
	output rddata
	);
    localparam RAM_DEPTH = (1<< ADDR_WIDTH);
	reg [DATA_WIDTH - 1 : 0] mem[RAM_DEPTH - 1 : 0];
 
	// synopsys_translate_off
	integer i;
	initial begin
 	   for(i=0; i < RAM_DEPTH; i = i + 1) begin
  	      mem[i] = 8'h00;
  	  end
	end
	// synopsys_translate_on
	always@(*) begin
		if(wren) mem[wraddr] = wrdata;
		else ;
	end
	always@(*) begin
		if(rden) rddata = mem[rdaddr];
		else ;
	end
endmodule

握手协议

  • 所谓握手,即通信双方使用了专用控制信号进行状态指示,这个控制信号既有发送域给接受域的也有接收域给控制域的,有别于单向控制信号方式。

握手协议原理

  • 使用握手协议方式处理跨时钟域数据传输时,只需要对双方的握手信号(req 和 ack)分别使用脉冲检测方法进行同步即可,也是一种将多比特 跨时钟域问题转化为单比特跨时钟域问题的方法。
  • 在具体实现中,假设req ,ack, data,总线在初始化时都处于无效状态,发送域先把数据放入总线,随后发送有效的req信号给接收域;接收域在检测到有效的req信号后锁存数据总线,然后会送一个有效的ack信号表示读取完成应答;发送域在检测到有效ack信号后撤销当前的req信号,接收域在检测到req撤销后也相应撤销ack信号,此时完成一次正常握手通信。此后,发送域可以继续开始下一次握手通信,如此循环。
  • 采用握手协议能够使接收到的数据稳定可靠,有效的避免了亚稳态的出现,但是控制信号握手检测会消耗通信双方较多的时间。

设计代码

发送域代码

module handshake_tclk(
	input tclk, //发送域的时钟和复位
	input resetb_tclk,
	input r_ack, //接收到的响应信号
	input data_avail, //从其他模块接收到的数据有效信号
	input [31:0] transmit_data, //需要发送出去的信号
	output t_rdy, //对于发送时钟域,需要输出数据准备好了的信号,以便在接收时钟域接收此信号,用来提示接收信号
	output [31:0] t_data //需要发送出去的信号
);
	localparam IDLE = 3'b001;     //空闲状态,判断数据是否有效,如果有效就输出t_rdy有效,表示数据准备好了
	localparam ASSERT_TRDY = 3'b010; //到了这个状态,表明t_rdy已经有效了,这时我们需要判断响应r_ack_tclk是否有效,如果有效则使t_rdy无效,否则保持不变
	localparam DEASSERT_TRDY = 3'b100; //到了这个t_rdy无效状态,需要考虑下一次的数据传输了,如果data_avail有效,则下一个状态进入ASSERT_TRDY,且t_rdy有效,否则进入空闲状态
	reg t_cur_state, t_nxt_state;
	reg t_rdy, t_rdy_nxt;
	reg [31:0] t_data, t_data_nxt;
	reg r_ack_d1, r_ack_tclk; //对于接收到的响应信号,需要进行时钟域同步,同步到发送时钟域
	always@(posedge tclk or negedge resetb_tclk) begin
		if(resetb_tclk) begin
				t_cur_state <= IDLE;
				t_rdy <= 0;
				//同步接收域的响应信号ack
				r_ack_d1 <= 0;
				r_ack_tclk <= 0;
		end
		else begin
				t_cur_state <= t_nxt_state;
				t_rdy <= t_rdy_nxt;
				r_ack_d1 <= r_ack;
				r_ack_tclk <= r_ack_d1;
		end
	end
	always@(*) begin
		t_nxt_state = t_cur_state;
		t_rdy_nxt = 0;
		t_data_nxt = t_data;
		case(t_cur_state) 
			IDLE: begin
				if(data_avail) begin
					t_nxt_state = ASSERT_TRDY;
					t_rdy_nxt = 1'b1;
					t_data_nxt = transmit_data;
				end
				else ; 
			end
			ASSERT_TRDY: begin
				if(r_ack_tclk) begin
					t_rdy_nxt = 1'b0;
					t_nxt_state = DEASSERT_TRDY;
					t_data_nxt = 'd0;
				end
				else begin
					t_rdy_nxt = 1'b1;
					t_data_nxt = t_data;
				end
			end
			DEASSERT_TRDY: begin
				if(!r_ack_tclk) begin
					if(data_avail) begin
						t_nxt_state = ASSERT_TRDY;
						t_rdy_nxt = 1'b1;
						t_data_nxt = transmit_data;
					end
					else t_nxt_state = IDLE;
				end
				else;
			end
			default: ;
		endcase
	end
endmodule

接收域代码

module handshake_rclk(
	input rclk,
	input resetb_rclk,
	input t_rdy,
	input [31:0] t_data,
	output r_ack
	);
	localparam IDLE = 2'b01;
	localparam ASSERT_ACK = 2'b10;
 
	reg r_cur_state, r_nxt_state;
	reg r_ack, r_ack_nxt;
	reg [31:0] t_data_rclk, t_data_rclk_nxt;
	reg t_rdy_d1, t_rdy_rclk;
 
	always@(posedge clk or negedge resetb_rclk) begin
		if(~resetb_rclk) begin
			r_cur_state <= IDLE;
			r_ack <= 0;
			t_data_rclk <= 0;
			t_rdy_d1 <= 0;
			t_rdy_rclk <= 0;
		end
		else begin
			r_cur_state <= r_nxt_state;
			r_ack <= r_ack_nxt;
			t_data_rclk <= t_data_rclk_nxt;
			t_rdy_d1 <= t_rdy;
			t_rdy_rclk <= t_rdy_d1;
		end
	end
    
	always@(*) begin
		r_nxt_state = r_cur_state;
		r_ack_nxt = 1'b0;
		t_data_rclk_nxt = t_data_rclk;
		case(r_nxt_state)
			IDLE: begin
				if(t_rdy_rclk) begin
					r_nxt_state = ASSERT_ACK;
					r_ack_nxt = 1'b1;
					t_data_rclk_nxt = t_data;
				end
				else;
			end
			ASSERT_ACK: begin
				if(~t_rdy_rclk) begin
					r_ack_nxt = 1'b0;
					r_nxt_state = IDLE;
				end
				else r_ack_nxt = 1'b1;
			end
		endcase
	end
endmodule

t_rdy与r_ack关系

  1. 首先发送方将待发送数据放在数据总线上,并让t_rdy有效
  2. 当接收域检测到t_rdy信号后,读取数据总线数据
  3. 接收域在读取完成后,让r_ack有效
  4. 发送域检测到r_ack有效后,让t_rdy无效
  5. 接收域检测到t_rdy无效后,让r_ack无效
  6. 此时,一次完整的握手成功完成

参考文献:
谈谈跨时钟域传输问题(CDC)-Author:李锐博恩
跨时钟域信号传输问题之握手同步-Author:李锐博恩