谈谈跨时钟域传输问题(CDC)

举报
李锐博恩 发表于 2021/07/15 02:00:59 2021/07/15
【摘要】 目录 前言 单比特信号的跨时钟域传输 慢时钟域到快时钟域 快时钟域到慢时钟域 多比特信号的跨时钟域传输 异步FIFO 握手协议 前言 CDC(Clock Domain Conversion)问题,一直是IC前端设计,FPGA设计的热点问题,特别是在校招面试笔试时候,是问的最多的一个问题,我之前关于这个问题以及相关问题,写了一些总结,但比较分散,今天简单汇总...

目录

前言

单比特信号的跨时钟域传输

慢时钟域到快时钟域

快时钟域到慢时钟域

多比特信号的跨时钟域传输

异步FIFO

握手协议


前言

CDC(Clock Domain Conversion)问题,一直是IC前端设计,FPGA设计的热点问题,特别是在校招面试笔试时候,是问的最多的一个问题,我之前关于这个问题以及相关问题,写了一些总结,但比较分散,今天简单汇总总结一下。

跨时钟域问题,又可以变向地细化地称为脉冲同步问题,脉冲检测问题等,看具体描述,大概是同一个意思:

例如,某光:

脉冲同步器的基本功能是从某个时钟域取出一个单时钟宽度脉冲,然后在新的时钟域中建立另一个单时钟宽度脉冲,请按照下面的模块接口信号设计一个脉冲检测器,并说明你设计的脉冲同步器的使用限制:


   
  1. module sync_pulse(
  2. input in_rst_n,
  3. input in_clk,
  4. input in_pulse,
  5. input out_rst_n,
  6. input out_clk,
  7. output out_pulse
  8. );
  9. //......
  10. endmodule

本次总结的大致思路是这样的:



单比特信号的跨时钟域传输

慢时钟域到快时钟域

在慢时钟域内的一个脉冲信号,持续一个时钟周期,将其传输到快时钟域内:

这个问题,直接使用一个单比特同步器即可,因为快时钟一定能采样到慢时钟域内的信号,我们用两级寄存器进行同步的目的在于消除亚稳态问题,也就是说如果慢时钟域内的脉冲恰好在快时钟域的亚稳态窗口内,快时钟采样时刻(上升沿)采样得到的信号有可能出现亚稳态,再用触发器寄存一拍,可以大大降低最终输出出现亚稳态的概率。

最后如果也需要得到一个周期的脉冲,做一次时钟上升沿检测即可。

给出设计代码:


  
  1. `timescale 1ns / 1ps
  2. //
  3. // Company:
  4. // Engineer: https://blog.csdn.net/Reborn_Lee
  5. // Create Date: 2019/09/24 15:22:59
  6. // Module Name: slow2fast_cdc
  7. // Project Name:
  8. // Target Devices:
  9. // Tool Versions:
  10. //
  11. module slow2fast_cdc(
  12. input clka,
  13. input clkb,
  14. input rst_n,
  15. input pulse_ina,
  16. output pulse_outb
  17. );
  18. reg pulse_midb_r1, pulse_midb_r2;
  19. always@(posedge clkb or negedge rst_n) begin
  20. if(~rst_n) begin
  21. pulse_midb_r1 <= 0;
  22. pulse_midb_r2 <= 0;
  23. end
  24. else begin
  25. pulse_midb_r1 <= pulse_ina;
  26. pulse_midb_r2 <= pulse_midb_r1;
  27. end
  28. end
  29. assign pulse_outb = ~pulse_midb_r2 & pulse_midb_r1;
  30. endmodule

testbench文件:


  
  1. `timescale 1ns / 1ps
  2. //
  3. // Company:
  4. // Engineer: https://blog.csdn.net/Reborn_Lee
  5. // Create Date: 2019/09/24 15:26:38
  6. // Design Name:
  7. // Module Name: sim_s2f_cdc
  8. //
  9. module sim_s2f_cdc(
  10. );
  11. reg clka, clkb;
  12. reg rst_n;
  13. reg pulse_ina;
  14. wire pulse_outb;
  15. initial begin
  16. clka = 0;
  17. forever
  18. #6 clka = ~ clka;
  19. end
  20. initial begin
  21. clkb = 0;
  22. forever
  23. #2 clkb = ~ clkb;
  24. end
  25. initial begin
  26. rst_n = 0;
  27. pulse_ina = 0;
  28. #10
  29. rst_n = 1;
  30. #10
  31. @(posedge clka)
  32. pulse_ina = 1;
  33. @(posedge clka)
  34. pulse_ina = 0;
  35. end
  36. slow2fast_cdc inst_slow2fast(
  37. .clka(clka),
  38. .clkb(clkb),
  39. .rst_n(rst_n),
  40. .pulse_ina(pulse_ina),
  41. .pulse_outb(pulse_outb)
  42. );
  43. endmodule

快时钟域到慢时钟域

从快时钟域clka到慢时钟域clkb,如果快时钟域内的一个脉冲信号持续一个时钟周期,如何同步到慢时钟域内呢?

这里存在一个问题是快时钟采样不到慢时钟域内输入脉冲的一个情况,对于这种问题,我们的通用方法为:

简单说来,就是在快时钟域内先进行脉冲展宽,展宽到快时钟内能采样到为止;展宽之后的信号在快时钟域clkb下用两级寄存器同步下就好了,再用上升沿检测检测到同步后的信号得到一个时钟clkb周期的脉冲,表示同步完成。

这里有一个问题就是脉冲展宽,展宽到什么地步?

我们先入为主,看看下图:

在clka内,检测到pulse_ina为高的时候,对展宽信号signal_a拉高,什么时候拉低呢?也就是对展宽信号拉低?

需要一个反馈信号,反馈信号有效的时候,拉低展宽信号。反馈信号是什么呢?

看上图,对signal_b_rr,同步到时钟域a,最终得到的这个信号signal_a_rr作为反馈信号,已经展宽的足够宽,宽到慢时钟能够采样到。

给出设计代码:


  
  1. module Sync_Pulse(
  2. input clka,
  3. input clkb,
  4. input rst_n,
  5. input pulse_ina,
  6. output pulse_outb,
  7. output signal_outb
  8. );
  9. //-------------------------------------------------------
  10. reg signal_a;
  11. reg signal_b;
  12. reg signal_b_r;
  13. reg signal_b_rr;
  14. reg signal_a_r;
  15. reg signal_a_rr;
  16. //-------------------------------------------------------
  17. //在clka下,生成展宽信号signal_a
  18. always @(posedge clka or negedge rst_n)begin
  19. if(rst_n == 1'b0)begin
  20. signal_a <= 1'b0;
  21. end
  22. else if(pulse_ina == 1'b1)begin
  23. signal_a <= 1'b1;
  24. end
  25. else if(signal_a_rr == 1'b1)
  26. signal_a <= 1'b0;
  27. else
  28. signal_a <= signal_a;
  29. end
  30. //-------------------------------------------------------
  31. //在clkb下同步signal_a
  32. always @(posedge clkb or negedge rst_n)begin
  33. if(rst_n == 1'b0)begin
  34. signal_b <= 1'b0;
  35. end
  36. else begin
  37. signal_b <= signal_a;
  38. end
  39. end
  40. //-------------------------------------------------------
  41. //在clkb下生成脉冲信号和输出信号
  42. always @(posedge clkb or negedge rst_n)begin
  43. if(rst_n == 1'b0)begin
  44. signal_b_r <= 'b0;
  45. signal_b_rr <= 'b0;
  46. end
  47. else begin
  48. signal_b_rr <= signal_b_r;
  49. signal_b_r <= signal_b;
  50. end
  51. end
  52. assign pulse_outb = ~signal_b_rr & signal_b_r;
  53. assign signal_outb = signal_b_rr;
  54. //-------------------------------------------------------
  55. //在clka下采集signal_b_rr,生成signal_a_rr用于反馈拉低signal_a
  56. always @(posedge clka or negedge rst_n)begin
  57. if(rst_n == 1'b0)begin
  58. signal_a_r <= 'b0;
  59. signal_a_rr <= 'b0;
  60. end
  61. else begin
  62. signal_a_rr <= signal_a_r;
  63. signal_a_r <= signal_b_rr;
  64. end
  65. end
  66. endmodule

仿真文件同单比特同步器;

仿真波形为:



多比特信号的跨时钟域传输

异步FIFO

无论是从快到慢,还是从慢到快,都可以用异步FIFO来处理:

学习异步FIFO要善于与同步FIFO做一个对比,同步FIFO见链接:

同步FIFO的设计

对于同步FIFO的设计,很容易通过一个计数器来判断空满,由于读写FIFO都在同一个时钟域,所以可以使用如下思路:

写一个数据,计数器加1;读一个数据,计数器减1;如此,当计数器为0,代表写了多少个数据就读了多少个数据,FIFO为空;

假设FIFO的深度为N,则计数到N,表示FIFO满;

如下图可以清晰示意:

可见,写指针总是指向下一个要写入的RAM空间,而读指针总是指向当前要读取的数据空间。

当FIFO复位时,读写指针指向0地址处,之后每写一个数据,写指针移动到下一个地址处,读一个数据,读指针也移动到下一个地址处。正常的状态应该是读指针一直在追写指针,这样的话就不会发生读空的情况;

但也存在这种情况,写指针反过来追上了读指针,这下可好了,二者又相等了,这时候不是空,反而是满,这时,就不能继续写了,否则就会出现写数据覆盖原来数据的情况,称之为溢出。

上面也说了,对于同步FIFO来说,我们可以设置一个计数器,写一个数据,计数器加1,读一个数据,计数器减1,如果计数器为0,则表示为空,如果计数器为N(N为FIFO深度)则为满。

对于异步FIFO呢?

如何判断空满呢?能否仿效同步FIFO,也设置一个计数器呢?

答案是不能,因为异步FIFO的读写指针位于不同的时钟域,如果设置一个计数器,请问这个计数器是在读时钟域内计数呢?还是在写时钟域内计数呢?

我们只能通过判断读写指针的关系进行判断FIFO是空还是满?可是如上图所示,读写指针相等有可能为空,也可能为满?我们必须想出一个办法,解决这种判断模糊的问题。

我们的方法是对于读写指针多加一位指示位。原理如下:

同样,在写时钟域内,如果写一个数据,写指针加1,即移动到下一个地址处;

在读时钟域内,如果读一个数据,则读指针加1,即移动到下一个地址处;

当读写指针的最高位(MSB)(most significant bit)相等,且剩余其他位也相等时,表示FIFO为空;

当读写指针的最高位(MSB)不等,但是剩余其他位相等时,表示FIFO为满。

具体过程如下:

注意,上面去判断读写指针时,是需要同步到同一个时钟域内的,例如需要判断FIFO是否为满,需要将读指针同步到写时钟域内,如何进行同步呢?

过程是:

首先,当读一个数据,读指针加1,这里用的还是二进制编码,之后对这个读指针变换为格雷码,寄存一拍,然后用写时钟两级同步,在将同步过来的信号转换为二进制编码,寄存一拍与写指针比较。

判断是否为空时,需要将写指针同步到读时钟域,方式同上。

下面的话可以不用看了(需要升级理解的可以看看):

从上述过程中,在判断是否为满时,我们可以看到,将写指针同步到读时钟域需要几个周期的时间,同步过去后,与写指针进行比较,由于在这段时间内,读数据有可能也正在继续,所以,有可能存在着还保留着几个未写满的空间,也就是可用于写入的位置比实际的要多出几个。这是异步FIFO操作保守的一面,这样做才不会引起上溢,即写数据覆盖原来的数据。

同样,在判断是否为空时,需要将写指针同步到读时钟域,需要花费几个周期的时间,同步过去后,与读指针对比,由于这段时间内,写数据有可能也在继续,所以二者相等时,有可能还有几个数据没有读完,也就是还有几个数据可用。这也是异步FIFO保守的一面,这样不会出现下溢的情况,也即读空了继续读的情况。


根据上面的原理,设计一个异步FIFO:

主程序:


  
  1. module asyfifo#(
  2. parameter DATA_WIDTH = 8,
  3. ADDR_WIDTH = 4,
  4. FIFO_DEPTH = (1<<ADDR_WIDTH)
  5. )(
  6. input rst_n,
  7. //write ports
  8. input wrclk,
  9. input wren,
  10. input data_in,
  11. output wr_full,
  12. //read ports
  13. input rdclk,
  14. input rden,
  15. output data_out,
  16. output rd_empty
  17. );
  18. //write pointer control logic
  19. //write pointer with wraparound and no wraparound
  20. reg [ADDR_WIDTH : 0] wr_ptr_wrap, wr_ptr_wrap_nxt;
  21. wire [ADDR_WIDTH - 1 : 0] wr_ptr;
  22. //write pointer with wraparound state change
  23. always@(posedge wrclk or negedge rst_n)begin
  24. if(~rst_n) wr_ptr_wrap <= 0;
  25. else wr_ptr_wrap <= wr_ptr_wrap_nxt;
  26. end
  27. always@(*) begin
  28. wr_ptr_wrap_nxt = wr_ptr_wrap;
  29. if(wren) wr_ptr_wrap_nxt = wr_ptr_wrap_nxt + 1;
  30. else ;
  31. end
  32. //convert the binary write pointer to gray, flop it, and then pass it to read domain
  33. reg [ADDR_WIDTH : 0] wr_ptr_wrap_gray;
  34. wire [ADDR_WIDTH : 0] wr_ptr_wrap_gray_nxt;
  35. //instantiate the module binary to gray
  36. binary_to_gray #(.WIDTH(ADDR_WIDTH)) inst_binary_to_gray_wr(
  37. .binary_value(wr_ptr_wrap_nxt),
  38. .gray_value(wr_ptr_wrap_gray_nxt)
  39. );
  40. always@(posedge wrclk or negedge rst_n) begin
  41. if(~rst_n) wr_ptr_wrap_gray <= 0;
  42. else wr_ptr_wrap_gray <= wr_ptr_wrap_gray_nxt;
  43. end
  44. //synchronize wr_ptr_wrap_gray into read clock domain
  45. reg [ADDR_WIDTH:0] wr_ptr_wrap_gray_r1, wr_ptr_wrap_gray_r2;
  46. always@(posedge rdclk or negedge rst_n) begin
  47. if(~rst_n) begin
  48. wr_ptr_wrap_gray_r1 <= 0;
  49. wr_ptr_wrap_gray_r2 <= 0;
  50. end
  51. else begin
  52. wr_ptr_wrap_gray_r1 <= wr_ptr_wrap_gray;
  53. wr_ptr_wrap_gray_r2 <= wr_ptr_wrap_gray_r1;
  54. end
  55. end
  56. //convert wr_ptr_wrap_gray_r2 back to binary form
  57. reg [ADDR_WIDTH : 0] wr_ptr_wrap_rdclk;
  58. wire [ADDR_WIDTH : 0] wr_ptr_wrap_rdclk_nxt;
  59. gray_to_binary #(.WIDTH(ADDR_WIDTH)) inst_gray_to_binary_wr(
  60. .gray_value(wr_ptr_wrap_gray_r2),
  61. .binary_value(wr_ptr_wrap_rdclk_nxt)
  62. );
  63. always@(posedge rdclk or negedge rst_n) begin
  64. if(~rst_n) wr_ptr_wrap_rdclk <= 0;
  65. else wr_ptr_wrap_rdclk <= wr_ptr_wrap_rdclk_nxt;
  66. end
  67. assign wr_ptr = wr_ptr_wrap[ADDR_WIDTH - 1 : 0];
  68. //read pointer control logic
  69. //read pointer with wraparound and no wraparound
  70. reg [ADDR_WIDTH : 0] rd_ptr_wrap, rd_ptr_wrap_nxt;
  71. wire [ADDR_WIDTH - 1 : 0] rd_ptr;
  72. //read pointer with wraparound state change
  73. always@(posedge rdclk or negedge rst_n) begin
  74. if(~rst_n) rd_ptr_wrap <= 0;
  75. else rd_ptr_wrap <= rd_ptr_wrap_nxt;
  76. end
  77. always@(*) begin
  78. rd_ptr_wrap_nxt = rd_ptr_wrap;
  79. if(rden) rd_ptr_wrap_nxt = rd_ptr_wrap_nxt + 1;
  80. else ;
  81. end
  82. //convert binary read pointer to gray
  83. reg [ADDR_WIDTH : 0] rd_ptr_wrap_gray;
  84. wire [ADDR_WIDTH : 0] rd_ptr_wrap_gray_nxt;
  85. binary_to_gray #(.WIDTH(ADDR_WIDTH)) inst_binary_to_gray_rd(
  86. .binary_value(rd_ptr_wrap_nxt),
  87. .gray_value(rd_ptr_wrap_gray_nxt)
  88. );
  89. always@(posedge rdclk or negedge rst_n) begin
  90. if(~rst_n) rd_ptr_wrap_gray <= 0;
  91. else rd_ptr_wrap_gray <= rd_ptr_wrap_gray_nxt;
  92. end
  93. //synchronize rd_ptr_wrap_gray into write clock domain
  94. reg [ADDR_WIDTH : 0] rd_ptr_wrap_gray_r1, rd_ptr_wrap_gray_r2;
  95. always@(posedge wrclk or negedge rst_n) begin
  96. if(~rst_n) begin
  97. rd_ptr_wrap_gray_r1 <= 0;
  98. rd_ptr_wrap_gray_r2 <= 0;
  99. end
  100. else begin
  101. rd_ptr_wrap_gray_r1 <= rd_ptr_wrap_gray;
  102. rd_ptr_wrap_gray_r2 <= rd_ptr_wrap_gray_r1;
  103. end
  104. end
  105. //convert rd_ptr_wrap_gray_r2 into binary form
  106. reg [ADDR_WIDTH : 0] rd_ptr_wrap_wrclk;
  107. wire [ADDR_WIDTH : 0] rd_ptr_wrap_wrclk_nxt;
  108. gray_to_binary #(.WIDTH(ADDR_WIDTH)) inst_gray_to_binary_rd(
  109. .gray_value(rd_ptr_wrap_gray_r2),
  110. .binary_value(rd_ptr_wrap_wrclk_nxt)
  111. );
  112. always@(posedge wrclk or negedge rst_n) begin
  113. if(~rst_n) rd_ptr_wrap_wrclk <= 0;
  114. else rd_ptr_wrap_wrclk <= rd_ptr_wrap_wrclk_nxt;
  115. end
  116. assign rd_ptr = rd_ptr_wrap[ADDR_WIDTH - 1 : 0];
  117. wire wr_full_nxt;
  118. reg wr_full;
  119. 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]
  120. == rd_ptr_wrap_wrclk_nxt[ADDR_WIDTH - 1 : 0]);
  121. always@(posedge wrclk or negedge rst_n) begin
  122. if(~rst_n) wr_full <= 0;
  123. else wr_full <= wr_full_nxt;
  124. end
  125. wire rd_empty_nxt;
  126. reg rd_empty;
  127. 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]
  128. == wr_ptr_wrap_rdclk_nxt[ADDR_WIDTH - 1 : 0] );
  129. always@(posedge rdclk or negedge rst_n) begin
  130. if(~rst_n) rd_empty <= 0;
  131. else rd_empty <= rd_empty_nxt;
  132. end
  133. sram #(.ADDR_WIDTH(ADDR_WIDTH),
  134. .DATA_WIDTH(DATA_WIDTH)
  135. ) inst_sram(
  136. .wren(wren),
  137. .wraddr(wr_ptr),
  138. .wrdata(data_in),
  139. .rden(rden),
  140. .rdaddr(rd_ptr),
  141. .rddata(data_out)
  142. );
  143. endmodule

二进制转格雷码:


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

格雷码转二进制:


  
  1. module gray_to_binary #( parameter WIDTH = 4)(
  2. input [WIDTH : 0] gray_value,
  3. output [WIDTH : 0] binary_value
  4. );
  5. assign binary_value[WIDTH] = gray_value[WIDTH];
  6. genvar i;
  7. generate
  8. for(i = 0; i <WIDTH - 1; i = i + 1) begin
  9. binary_value[i] = gray_value[i] ^ binary_value[i + 1];
  10. end
  11. endgenerate
  12. endmodule

双端口SRAM:


  
  1. module sram #(
  2. parameter ADDR_WIDTH = 4,
  3. DATA_WIDTH = 8
  4. )(
  5. input wren,
  6. input wrdata,
  7. input wraddr,
  8. input rden,
  9. input rdaddr,
  10. output rddata
  11. );
  12. localparam RAM_DEPTH = (1<< ADDR_WIDTH);
  13. reg [DATA_WIDTH - 1 : 0] mem[RAM_DEPTH - 1 : 0];
  14. // synopsys_translate_off
  15. integer i;
  16. initial begin
  17. for(i=0; i < RAM_DEPTH; i = i + 1) begin
  18. mem[i] = 8'h00;
  19. end
  20. end
  21. // synopsys_translate_on
  22. always@(*) begin
  23. if(wren) mem[wraddr] = wrdata;
  24. else ;
  25. end
  26. always@(*) begin
  27. if(rden) rddata = mem[rdaddr];
  28. else ;
  29. end
  30. endmodule

注意:上述代码,只作为参考,暂时并未仿真验证;



握手协议

这种方法,额,,,没用过。直接给出之前的链接吧。

https://blog.csdn.net/Reborn_Lee/article/details/89647526

 



暂时写到这里,以后需要更正在更新。

 

 

 

 

文章来源: reborn.blog.csdn.net,作者:李锐博恩,版权归原作者所有,如需转载,请联系作者。

原文链接:reborn.blog.csdn.net/article/details/101280171

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。