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关系
- 首先发送方将待发送数据放在数据总线上,并让t_rdy有效
- 当接收域检测到t_rdy信号后,读取数据总线数据
- 接收域在读取完成后,让r_ack有效
- 发送域检测到r_ack有效后,让t_rdy无效
- 接收域检测到t_rdy无效后,让r_ack无效
- 此时,一次完整的握手成功完成
参考文献:
谈谈跨时钟域传输问题(CDC)-Author:李锐博恩
跨时钟域信号传输问题之握手同步-Author:李锐博恩