数字电路 | CDC 在 IC 设计中的应用

注:本文为 “IC | CDC 设计” 相关文章合辑

略作重排。

图片清晰度限于引文原状。

如有内容异常,请看原文。


IC 设计基础系列之 CDC 篇 1:CDC 的基本概念

Times_poem 于 2017-06-11 17:04:50 发布

来自:http://blog.sina.com.cn/s/blog_72c14a3d0101de82.html(已沉寂)

随着技术的发展,数字电路的集成度越来越高,设计也越来越复杂。很少有系统会只工作在同一个时钟频率。一个系统中往往会存在多个时钟,这些时钟之间有可能是同步的,也有可能是异步的。如果一个系统中,异步时钟之间存在信号通道,则就会存在 CDC(clock domain crossing)问题。在下面的文章里,我们将会讨论 CDC 的一些技术细节。

CDC 的基本概念

时钟域(Clock domain)

如果某一设计只有一个或者几个有固定相位关系的时钟驱动,我们称这个设计属于一个时钟域。比如,一个时钟和它的反相时钟以分频时钟一般有固定的相位关系,属于同一个时钟域。而如果两个或者多个时钟之间没有固定的相位关系,则它们属于不同的时钟域。

如下图中,divCLK 是由 CLK 分频得到的,divCLK 和 CLK 被认为是同步时钟,而由 divCLk 和 CLK 驱动的设计被认为是属于同一个时钟域。

img

如下图中,CLKA 和 CLKB 之间没有固定的相位关系,是异步时钟。前半部分设计属于时钟域 CLKA,后半部分设计属于时钟域 CLKB。DA 信号从时钟域 CLKA 进入到时钟域 CLKB,是一个跨时钟域的信号,这条 path 也会被称为 CDC path。
img

如今的数字电路设计中,跨时钟域的情况及其普遍。举一个最简单的例子,一个 USB 转串口的模块,串口因为波特率的不同,会工作在不同的时钟频率下,USB 接口也会因为工作在不同的速度而有不同的工作频率。USB 接口的时钟和串口的时钟之间不存在任何相位上的关系,也就是说,USB 接口模块和串口模块属于不同的时钟域,他们之间相连的 path 就是跨时钟域的 path。


IC 设计基础系列之 CDC 篇 2:跨时钟域设计的潜在问题

Times_poem 于 2017-06-11 17:20:16 发布

来自:http://blog.sina.com.cn/s/blog_72c14a3d0101fj3z.html(已沉寂)

一般来讲,如果设计中存在有多个时钟域,那么就必然会存在跨时钟域的 timing path。如果对跨时钟域的 timing path 处理不当,则容易导致亚稳态,glitch,多路扇出,重新聚合等等问题,导致设计不能稳定工作或者就根本不能正常工作。

1. 亚稳态

对时序逻辑电路来说,一个 DFF 的输入信号必须在该 DFF 的时钟沿前后一段时间内都保持稳定才能保证 DFF 能锁存到正确的值。这既我们所说的 setup time 和 hold time,其中信号在时钟沿之前的保持时间为 setup time,信号在时钟沿之后的保持时间为 hold time。正常情况下,如果 DFF 的输入能满足 setup time 和 hold time 的要求,那么在 tCO(the clock to output delay)时间内 DFF 的输出就会达到一个有效的逻辑值(高电平或者低电平)。否则,DFF 的输出就需要远大于 tCO 的时间来达到有效的逻辑值,这段时间内,DFF 的输出信号是不稳定的,被称为不稳定状态,或者叫亚稳态。

在下图中,如果 CLK B 在 DA 变化的时候来对 DA 进行采样,那么 DB 就会出现亚稳态。

img

对于同时钟域的信号。无论是在 ASIC 设计还是在 FPGA 设计中,我们也可以方便的通过 STA 来保证同时钟域的信号能满足 setup/hold time 的要求,不会出现亚稳态的问题。

但对于异步信号,相位关系是完全不可控的,而且会随时间发生变化,这就必然会存在亚稳态的问题,而且 STA 工具也没有办法对不同时钟域之间的 timing path 进行分析。也就是说,我们是没有办法完全避免异步信号之间的亚稳态问题的,但是可以通过在跨时钟域的信号上加入一些特殊的电路来减少亚稳态问题对电路功能所产生的负面影响。

2. Glitch

前面我们讲过,STA 工具室不会对跨时钟域的信号做 STA。跨时钟域的信号很容易产生 glitch 并最终影响电路功能。如下图中,CLKA domain 中,DA1 和 DA2 分别为两个 DFF 的输出,理想状态下,DA1 和 DA2 到达与门两个输入端的时间是一样的,这样设计就不会出问题。但由于后端布局,环境等等因素导致的传播延迟 Td 会使 A&B 存在一个 Glitch。而由于 CLKB 和 CLKA 为两个 clock domain,之间不存在固定的相位关系,假设这个 Glitch 恰好被 CLKB 锁存住,那么就会在 DB2 生成一个有效的高电平信号,这个高电平信号不是我们的设计所期望的,那就会导致后继的电路功能出现问题。

img

也有可能是下面的情况,因为 Td 的影响,导致本应该传递到 CLKB 的高电平信号没有被传递过去,从而导致后继电路的功能出现问题。

img

在实际系统中,如果没有处理好跨时钟域的信号,那么由此导致的问题是很容易出现的。我在做 silicon debug 的时候,曾经多次遇到过由 CDC 导致的问题。由 CDC 导致的问题,在实际系统上的表现并不是非常一致,但经常表现出来的就是设计时而可以正常工作,时而出错,而且出错的概率不尽相同,这对问题的分析解决是非常不利的。我曾经遇到过一个 CDC 导致问题,需要执行数千次的操作,才可能复现出来,但问题一出现就是致命的,会导致系统出错。由于非常难复现问题,分析解决起来就特别困难。所以我们在做设计的时候,要尽量从一开始就解决掉这些问题。

3 多路扇出

如果信号从一个时钟域分多路进入另外一个时钟域,那么很有可能会导致功能上的错误。如下图所示,由于 Td 的存在,DA1 和 DA2 到达 CLKB 的时间不同,最终导致 fsm1_en 和 fsm2_en 这两个本应该同时有效的信号实际上相差一个 CLKB 的周期。这很可能会影响到后继的电路功能。

img

l 重新聚合

如果多个信号从一个时钟域进入另外一个时钟域,然后这些信号在目标时钟域中又聚合到一起,那么就有可能因为信号的重新聚合导致电路功能上的异常。

如下图所示,原本 EN1 和 EN2 转换时钟域后,期望得到的值是 2’b00 和 2’b11,但由于 EN1 和 EN2 到达 CLKB 时钟域的时间有差异,实际得到的值是 2’b00,2’b10 和 2’b11,最终导致后继电路功能出现问题。

img


揭秘《跨时钟域处理》三大方法

作者: Kevin 发布时间: 2016-08-31 20:24

跨时钟域处理是 FPGA 设计中经常遇到的问题,而如何处理好跨时钟域间的数据,可以说是每个 FPGA 初学者的必修课。如果是还在校的本科生,跨时钟域处理也是面试中经常常被问到的一个问题。

在本篇文章中,主要介绍 3 种跨时钟域处理的方法,这 3 种方法可以说是 FPGA 界最常用也最实用的方法,这三种方法包含了单 bit 和多 bit 数据的跨时钟域处理,学会这 3 招之后,对于 FPGA 相关的跨时钟域数据处理便可以手到擒来。

本文介绍的 3 种方法跨时钟域处理方法如下:

第一种方法:打两拍

大家很清楚,处理跨时钟域的数据有单 bit 和多 bit 之分,而打两拍的方式常见于处理单 bit 数据的跨时钟域问题。

打两拍的方式,其实说白了,就是定义两级寄存器,对输入的数据进行延拍。 如下图所示。

one_bit

应该很多人都会问,为什么是打两拍呢,打一拍、打三拍行不行呢?

先简单说下两级寄存器的原理:两级寄存是一级寄存的平方,两级并不能完全消除亚稳态危害,但是提高了可靠性减少其发生概率。总的来讲,就是一级概率很大,三级改善不大。

这样说可能还是有很多人不够完全理解,那么请看下面的时序示意图:

shixushiyi
data 是时钟域 1 的数据,需要传到时钟域 2(clk)进行处理,寄存器 1 和寄存器 2 使用的时钟都为 clk。假设在 clk 的上升沿正好采到 data 的跳变沿(从 0 变 1 的上升沿,实际上的数据跳变不可能是瞬时的,所以有短暂的跳变时间),那这时作为寄存器 1 的输入到底应该是 0 还是 1 呢?这是一个不确定的问题。所以 Q1 的值也不能确定,但至少可以保证,在 clk 的下一个上升沿,Q1 基本可以满足第二级寄存器的保持时间和建立时间要求,出现亚稳态的概率得到了很大的改善。

如果再加上第三级寄存器,由于第二级寄存器对于亚稳态的处理已经起到了很大的改善作用,第三级寄存器在很大程度上可以说只是对于第二级寄存器的延拍,所以意义是不大的。

可能对于这部分的解释不是很到位,不过还是希望大家能够多思考一下,欢迎大家批评指正。

第二种方法:异步双口 RAM

处理多 bit 数据的跨时钟域,一般采用异步双口 RAM。假设我们现在有一个信号采集平台,ADC 芯片提供源同步时钟 60MHz,ADC 芯片输出的数据在 60MHz 的时钟上升沿变化,而 FPGA 内部需要使用 100MHz 的时钟来处理 ADC 采集到的数据(多 bit)。

在这种类似的场景中,我们便可以使用异步双口 RAM 来做跨时钟域处理。先利用 ADC 芯片提供的 60MHz 时钟将 ADC 输出的数据写入异步双口 RAM,然后使用 100MHz 的时钟从 RAM 中读出。

对于使用异步双口 RAM 来处理多 bit 数据的跨时钟域,相信大家还是可以理解的。当然,在能使用异步双口 RAM 来处理跨时钟域的场景中,也可以使用异步 FIFO 来达到同样的目的。

第三种方法:格雷码转换

对于第三种方法,Kevin 在大学里边从没接触过,也是在工作中才接触到。

我们依然继续使用介绍第二种方法中用到的 ADC 例子,将 ADC 采样的数据写入 RAM 时,需要产生 RAM 的写地址,但我们读出 RAM 中的数据时,肯定不是一上电就直接读取,而是要等 RAM 中有 ADC 的数据之后才去读 RAM。这就需要 100MHz 的时钟对 RAM 的写地址进行判断,当写地址大于某个值之后再去读取 RAM。

在这个场景中,其实很多人都是使用直接用 100MHz 的时钟于 RAM 的写地址进行打两拍的方式,但 RAM 的写地址属于多 bit,如果单纯只是打两拍,那不一定能确保写地址数据的每一个 bit 在 100MHz 的时钟域变化都是同步的,肯定有一个先后顺序。如果在低速的环境中不一定会出错,在高速的环境下就不一定能保证了。所以更为妥当的一种处理方法就是使用格雷码转换。

对于格雷码,相邻的两个数间只有一个 bit 是不一样的(格雷码,在本文中不作详细介绍),如果先将 RAM 的写地址转为格雷码,然后再将写地址的格雷码进行打两拍,之后再在 RAM 的读时钟域将格雷码恢复成 10 进制。这种处理就相当于对单 bit 数据的跨时钟域处理了。


数字 IC 基础:跨时钟域(CDC,Clock Domain Crossing)

ReRrain 已于 2023-05-16 11:16:49 修改

一、什么是跨时钟域?

在这里插入图片描述

跨时钟域的产生原因在于现代芯片(如 SOC,片上系统)的集成度和复杂度越来越高。通常,一颗芯片上会有许多信号工作在不同的时钟频率下。例如,SOC 芯片中的 CPU 通常工作在一个频率上,总线信号(如 DRAM BUS)工作在另一个时钟频率下,而普通信号又工作在另外的时钟频率下。这三种不同频率的信号需要相互沟通和传递信号,而不同频率时钟域下的信号传递就涉及跨时钟域信号处理。

二、跨时钟域传输的问题

(一)亚稳态

跨时钟域问题的本质是亚稳态,根据传输的数据大小,分为单比特亚稳态和多比特亚稳态。

1. 单比特亚稳态

亚稳态是指数据无法在规定的时间段内达到一个稳定的状态。其发生原因包括:

  • 数据传输中不满足 D 触发器的建立时间 T s u T_{su} Tsu 和保持时间 T h T_{h} Th 要求。
  • 复位过程中复位信号的释放相对于有效时钟沿的恢复时间(recovery time)和移除时间(removal time)不满足。

亚稳态主要发生在异步信号采集、跨时钟域信号传输以及异步复位电路等常用设计中。亚稳态输出不确定,但会传给后一级触发器,从而导致后级电路出错,因此危害很大。

在这里插入图片描述

2. 多比特亚稳态

多比特亚稳态涉及数据收敛问题。数据收敛是指一组相关联的同步信号在经过不同路径后,如何在同一个时钟周期正确地到达另一个时钟域。对于多比特信号跨时钟域传输,虽然可以对每个信号使用 Double FF 进行信号同步,但信号的准确性和关联性会出现问题,这就是数据的收敛问题。

例如,有两个相关联的信号 X X X Y Y Y 要从时钟域 c l k A clk_A clkA 向时钟域 c l k B clk_B clkB 传播,分别对两个信号进行两级 DFF 同步处理后,可能会出现以下情况:

在这里插入图片描述

(二)多路扇出

有些情况下,一个信号在跨越时钟域之后会分为多个分支。例如,一个使能的控制信号分别使能后续的多个模块。同一个信号源经过不同路径跨越时钟域之后,多路扇出的值不一定相同。

在这里插入图片描述

解决方法是将信号同步之后再多路扇出,即先在时钟域 B B B 过两级 DFF 同步,再将信号扇出,而不是在各自的分支上同步。

在这里插入图片描述

(三)数据丢失

数据丢失问题可以通过延长输入数据信号(类似脉冲展宽)来解决。

(四)异步复位

异步复位需要同步释放,适用于没有 PLL 的系统复位信号设置。以下是实现代码:

// Synchronized Asynchronous Reset
module sync_async_reset (
    input clock,
    input reset_n,
    output rst_n
);

reg rst_nr1; // 打一拍
reg rst_nr2; // 打两拍

always @(posedge clock or negedge reset_n) begin
    if (!reset_n) begin
        rst_nr1 <= 1'b0;
        rst_nr2 <= 1'b0; // 异步复位
    end
    else begin
        rst_nr1 <= 1'b1;
        rst_nr2 <= rst_nr1; // 同步释放
    end
end

assign rst_n = rst_nr2; // 新的系统复位信号 rst_n

endmodule // sync_async_reset

在这里插入图片描述

三、跨时钟域传输问题的解决方法

(一)单比特信号

跨时钟域信号传输分为单比特信号和多比特信号。单比特脉冲信号跨时钟域传输又分为慢时钟域到快时钟域、快时钟域到慢时钟域。

1. 单比特电平信号

对于单比特电平信号,可以使用两级 D 触发器同步来实现跨时钟域传输。以下是实现代码:

module single_cdc (
    input clk1,
    input clk2,
    input rst_n,
    input signal_in,
    output signal_out
);

reg signal_out_r; // 打一拍
reg signal_out_rr; // 打两拍

always @(posedge clk2 or negedge rst_n) begin
    if (!rst_n) begin
        signal_out_r <= 1'b0;
        signal_out_rr <= 1'b0;
    end
    else if (signal_in == 1'b1) begin
        signal_out_r <= signal_in;
        signal_out_rr <= signal_out_r;
    end
    else begin
        signal_out_r <= 1'b0;
        signal_out_rr <= 1'b0;
    end
end

assign signal_out = signal_out_rr;

endmodule
2. 单比特脉冲信号

对于单比特脉冲信号,根据时钟域的快慢关系,处理方式有所不同:

  • 慢到快:先用两级 D 触发器实现同步,再用边沿检测电路得到脉冲信号。以下是实现代码:
module single_cdc (
    input clk1,
    input clk2,
    input rst_n,
    input signal_in,
    output reg signal_out
);

reg signal_out_r; // 打一拍
reg signal_out_rr; // 打两拍
reg signal_out_rrr; // 边沿检测电路

always @(posedge clk2 or negedge rst_n) begin
    if (!rst_n) begin
        signal_out_r <= 1'b0;
        signal_out_rr <= 1'b0;
    end
    else if (signal_in == 1'b1) begin
        signal_out_r <= signal_in;
        signal_out_rr <= signal_out_r;
        signal_out_rrr <= signal_out_rr;
    end
    else begin
        signal_out_r <= 1'b0;
        signal_out_rr <= 1'b0;
        signal_out_rrr <= 1'b0;
    end
end

// 组合逻辑(与逻辑)输出脉冲
assign signal_out = signal_out_rr && !signal_out_rrr;

endmodule
  • 快到慢:先将脉冲信号展宽,再同步到慢时钟域,最后用边沿检测电路得到脉冲信号。以下是实现代码:
module led (
    input clk_fast,
    input clk_slow,
    input rst_n,
    input signal_in,
    output signal_out
);

// 快时钟域脉冲展宽
reg signal_a;
always @(posedge clk_fast or negedge rst_n) begin
    if (!rst_n)
        signal_a <= 1'b0;
    else if (signal_in == 1'b1) // 拉高
        signal_a <= signal_in;
    else if (signal_a_rr == 1'b1) // 拉低
        signal_a <= 1'b0;
end

// 慢时钟域采集脉冲展宽信号
reg signal_b; // 打一拍
reg signal_b_r; // 打两拍
always @(posedge clk_slow or negedge rst_n) begin
    if (!rst_n) begin
        signal_b <= 1'b0;
        signal_b_r <= 1'b0;
    end
    else begin
        signal_b <= signal_a;
        signal_b_r <= signal_b;
    end
end

// 快时钟域采集慢时钟域返回信息:signal_b_r
reg signal_a_r; // 打一拍
reg signal_a_rr; // 打两拍
always @(posedge clk_fast or negedge rst_n) begin
    if (!rst_n)
        {signal_a_rr, signal_a_r} <= {2 {1'b0}};
    else
        {signal_a_rr, signal_a_r} <= {signal_a_r, signal_b_r};
end

// 慢时钟域边沿检测,得到脉冲信号
reg signal_b_rr; // 上升沿检测,将 signal_b_r 打一拍
always @(posedge clk_slow or negedge rst_n) begin
    if (!rst_n)
        signal_b_rr <= 1'b0;
    else
        signal_b_rr <= signal_b_r;
end

assign signal_out = signal_b_r && (!signal_b_rr);

endmodule

(二)多比特信号

1. 格雷码 + 双 DFF(异步 FIFO)

格雷码 + 双 DFF 常用于异步 FIFO 中读写地址的跨时钟域传递。以下是相关说明:

  • 多比特信号需要转换成格雷码再同步,而不是直接同步。原因在于,如果直接使用二进制码,在发生亚稳态时,可能会出现中间态,从而导致错误的读写操作。而格雷码相邻状态只有 1bit 不同,即使发生亚稳态,也不会造成错误的读写操作。
  • 格雷码应用时必须保证首尾数据也只有 1bit 不同,即数据个数必须是 2 n 2^n 2n,否则不能使用格雷码加双 DFF 的方式跨时钟域传输。
  • 对于无法使用格雷码编码的多比特信号,可以使用握手协议来处理跨时钟域问题。
  • 转换后的格雷码必须在过两级 DFF 之前,先过一级 DFF,以保证输出稳定的数据。
2. 握手协议

握手协议将多比特数据的传输问题转换成单个信号的跨时钟域问题(只对请求信号 REQ 和应答信号 ACK 进行同步)。以下是实现代码:

module led (
    input clk_a,
    input clk_b,
    input rst_n,
    input a_en,
    input [3:0] data_in,
    output b_en,
    output reg [3:0] data_out
);

// a_en 下降沿检测(与逻辑)
reg a_en_d1;
wire a_en_neg;

always @(posedge clk_a or negedge rst_n) begin
    if (!rst_n)
        {a_en_neg, a_en_d1} <= {2 {1'b0}};
    else
        a_en_d1 <= a_en;
end

assign a_en_neg = a_en_d1 && !a_en;

// a 时钟域发出请求信号
reg req_a;
// ack 信号打两拍同步到 a 时钟域
reg ack_a_r;
reg ack_a_rr;

always @(posedge clk_a or negedge rst_n) begin
    if (!rst_n)
        req_a <= 1'b0;
    else if (a_en_neg)
        req_a <= 1'b1;
    else if (ack_a_rr) // 拉低
        req_a <= 1'b0;
end

// 请求信号打两拍同步到 b 时钟域
reg req_b_r;
reg req_b_rr;
always @(posedge clk_b or negedge rst_n) begin
    if (!rst_n)
        {req_b_rr, req_b_r} <= {2 {1'b0}};
    else
        {req_b_rr, req_b_r} <= {req_b_r, req_a};
end

// 检测到 a 时钟域发出的请求信号的上升沿,则 b 时钟域可以接收数据
assign b_en = req_b_r && !req_b_rr;

// b 时钟域接收数据
always @(posedge clk_b or negedge rst_n) begin
    if (!rst_n)
        data_out <= 'b0;
    else if (b_en)
        data_out <= data_in;
end

// b 时钟域接收完数据,发送 ack 信号,打两拍同步到 a 时钟域
always @(posedge clk_a or negedge rst_n) begin
    if (!rst_n)
        {ack_a_rr, ack_a_r} <= {2 {1'b0}};
    else
        {ack_a_rr, ack_a_r} <= {ack_a_r, req_b_rr};
end

endmodule
3. DMUX(D 触发器加二选一选择器)数据使能选通设计

通过一个使能信号来判断 data 信号是否已经稳定。当使能信号有效时,说明 data 处于稳定状态,终点寄存器才对信号进行采样,从而避免 setup/hold 违例。这种方式相当于把多比特信号的 CDC 问题转换成了单比特信号的 CDC 问题。以下是实现代码:

module led (
    input clk_a,
    input clk_b,
    input rst_n,
    input a_en,
    input [3:0] data_in,
    output reg [3:0] data_out
);

// a 时钟域使能信号同步到 b 时钟域,作为 MUX 的 sel
reg a_en_r;
reg a_en_rr;

always @(posedge clk_b or negedge rst_n) begin
    if (!rst_n)
        {a_en_rr, a_en_r} <= {2 {1'b0}};
    else
        {a_en_rr, a_en_r} <= {a_en_r, a_en};
end

// 二选一 MUX
always @(posedge clk_b or negedge rst_n) begin
    if (!rst_n)
        data_out <= 'b0;
    else if (a_en_rr == 1'b1) // 如果使能信号有效
        data_out <= data_in;
    else // 如果使能信号无效
        data_out <= data_out;
end

endmodule

跨时钟域处理解析(一)(Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog)

孤独的单刀 已于 2022-05-13 11:07:54 修改

本文参考自《Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog》–Clifford E. Cummings。
 
主要讲述了如何进行跨时钟域设计。正文中,衬色内容是我的啰嗦。

1. 介绍

2001 年,我发表了我的第一篇关于多异步时钟设计的论文。当时,我还没有找到任何好的资料来描述正确的多时钟设计所需的设计和综合技术。2001 年的论文是我多年来从实际 ASIC 和 FPGA 设计经验中收集到的技术集合。在 2001 年的会议演讲结束时,数十位工程师和同事站出来与我分享了足够多的额外有趣的想法和技术,以撰写有关该主题的续集。在过去的八年里,我在我的高级和专家 Verilog 和 SystemVerilog 培训课程中加入了多时钟设计技术的指导,在同一时期,更多的同事和学生与我分享了更多有趣的多时钟设计技术。自2001 年发表第一篇多时钟论文以来,业界已在很大程度上将这些类型的设计方法确定为跨时钟域 (Clock Domain Crossing,CDC) 技术。我将在本文中使用这种常见的命名法。

这篇论文包括 2001 年论文中描述的最佳技术以及过去十年与我分享的有趣且高效的多时钟设计技术的更新集合。实际的会议演示幻灯片将主要是自 2001 年原始演示以来所采用的新技术的集合,仅保留足够的原始幻灯片来介绍基本的 CDC 设计概念和问题。

前言没什么好看的。

2. 亚稳态

亚稳态是指在设计正常运行期间的某个时间点,信号在一段时间内不会呈现稳定的 0 或 1 状态。在多时钟设计中,无法避免亚稳态,但可以减小亚稳态的不利影响。

亚稳态示意图

引用 Dally 和 Poulton 关于亚稳态的书 [9]:

“当用时钟对不断变化的数据信号进行采样时……事件的顺序决定了结果。事件之间的时间差越小,确定哪个先发生所需的时间就越长。当两个事件非常靠近地发生时,决策过程可能需要比分配的时间更长的时间,并且会发生同步失败。”

图 1 显示了在一个时钟域中生成的信号被采样得太靠近来自第二个时钟域的时钟信号的上升沿时发生的同步故障。同步失败是由于输出变为亚稳态并且在必须再次采样输出时未收敛到合法稳定状态而引起的。

亚稳态的产生是由于寄存器采样不满足建立时间或保持时间要求导致的,亚稳态的产生是无法避免的,我们能做的只是想办法降低其发生的频率。在跨时钟域设计中,由于时钟域存在跨域,如果不采取手段,则会有很大概率会引入亚稳态。

2.1. 为什么亚稳态是个问题?

那么为什么亚稳态会成为问题呢?图 2 显示,穿越接收时钟域中附加逻辑的亚稳态输出会导致非法信号值在整个设计的其余部分传播。由于 CDC 信号可能会波动一段时间,因此接收时钟域中的输入逻辑可能会将波动信号的逻辑电平识别为不同的值,从而将错误信号传播到接收时钟域中。

亚稳态传播示意图

任何设计中使用的每个触发器都有一个指定的建立和保持时间,或者在时钟上升沿之前和之后数据输入不被合法允许改变的时间。这个时间窗口被精确指定为一个设计参数,以防止数据信号变化太接近另一个同步信号,从而导致输出进入亚稳态。

亚稳态最大的危害就是会将系统引入一个未知状态,对于许多设计而言,这无疑是致命的。

3. 同步器

在时钟域之间传递信号时,要问的一个重要问题是,我是否需要对从一个时钟域传递到另一个时钟域的信号的每个值进行采样?

3.1. 两种同步场景

跨 CDC 边界传递信号时可能出现两种情况,确定哪种情况适用于您的设计很重要:

  1. 允许错过在时钟域之间传递的样本。
  2. 在时钟域之间传递的每个信号都必须被采样。

第一种情况:有时不需要对每个值都进行采样,但重要的是采样值必须要准确。一个例子是标准异步 FIFO 设计中使用的一组格雷码计数器。在正确设计的异步 FIFO 模型中,同步格雷码计数器不需要从相反的时钟域中捕获每个合法值,但至关重要的是采样值必须准确以识别何时发生满和空情况。

第二种情况:CDC 信号必须被正确识别或识别和确认(即握手–译者注),然后才允许对 CDC 信号进行更改。

在这两种情况下,CDC 信号都需要某种形式地同步到接收时钟域中。

并不是所有设计都需要全部数据都被采样。例如,异步 FIFO 设计中,就可以允许漏采的存在。因为异步 FIFO 的设计最重要的是正确(或者说不错误)地判断 FIFO 的空和满。漏采一些数据并不会对其判断空满造成错误,某种意义上还会使其设计更加安全。

3.2. 双触发器同步器

再次引用 Dally 和 Poulton [9] 关于同步器的内容:

“同步器是一种对异步信号进行采样并输出与本地时钟或采样时钟同步的信号版本的设备。”

数字设计人员使用的最简单常见的同步器是双触发器同步器,如图 3 所示。第一个触发器将异步输入信号采样到新的时钟域中并等待一个完整的时钟周期以允许第一阶段输出信号上的任何亚稳态衰减,然后第一阶段信号被同一时钟采样到第二阶段触发器,其预期目标是第 2 阶段信号现在是一个稳定且有效的信号,同步并准备好在新的时钟域内分配。

双触发器同步器

理论上,当信号被计时到第二级以导致第二级输出信号也进入亚稳态时,第一级信号仍然可能是亚稳态。同步失败之间的时间间隔 (MTBF) 概率的计算是多个变量的函数,包括用于生成输入信号和为同步触发器计时的时钟频率。可在 Dally 和 Poulton [9] 中找到对 MTBF 计算的一种描述。

对于大多数同步应用,两个触发器同步器足以消除所有可能的亚稳态。

双触发器同步,即为经典的打两拍,可适用大多数的一般设计。

3.3. MTBF – 平均无故障工作时间

对于大多数应用而言,重要的是对任何跨越 CDC 边界的信号运行平均无故障工作时间 (MTBF) 计算。在这个意义上,失败意味着一个信号被传递到同步触发器,在第一级同步器触发器上变为亚稳态,并在一个周期后在它被采样到第二级同步器触发器时继续保持亚稳态。由于信号在一个时钟周期后并未稳定到已知值,因此在采样并传递到接收时钟域时,信号仍可能处于亚稳态,从而导致相应逻辑的潜在故障。

在计算 MTBF 数字时,较大的数字优于较小的数字。较大的 MTBF 数字表示潜在故障之间的时间间隔较长,而较小的 MTBF 数字表明亚稳态可能经常发生,同样会导致设计失败。

Dally 和 Poulton [9] 给出了一个很好的方程,其中对计算同步器电路的 MTBF 可以进行非常彻底的分析。在不重复方程式和分析的情况下,应该指出直接影响同步器电路 MTBF 的两个最重要因素是采样时钟频率(信号被采样到接收时钟域的速度)和数据更改频率(跨越 CDC 边界的数据更改速度有多快)。

MTBF 影响因素

从上面的部分方程可以看出,在更高速度的设计中,或采样数据变化更频繁时,故障发生的频率更高(MTBF 更短)。

前面说了,亚稳态的产生是无法避免的,只能尽量避免。所以人们引入了 MTBF 这个参数来表征两次故障之间的时间,即多久会发生一次亚稳态。若能将 MTBF 控制在几十年(可以做到),则该设计即可视为几乎不会发生亚稳态的设计,毕竟我们的数字系统预期寿命也没那么长(军工级除外)。

3.4. 三触发器同步器

对于一些非常高速的设计,双触发器同步器的 MTBF 太短,添加第三个触发器以将 MTBF 增加到令人满意的持续时间。当然,满意与否是由设计师决定的。

三触发器同步器

对于军工级或其相同级别设计、或者超高速设计,又或者可靠性要求较高的设计,可能需要打三拍,或者更多拍,这个取决于设计要求或公司规定。

3.5. 同步来自发送时钟域的信号

关于 CDC 设计的常见问题:在将信号传递到接收时钟域之前寄存来自发送时钟域的信号是否是个好主意?问题中隐含的假设是 CDC 信号将同步到接收时钟域;因此,它们不需要在发送时钟域中同步。这种合理化是不正确的,通常应该需要在发送时钟域中寄存信号。

考虑一个示例,其中发送时钟域中的信号在传递到接收时钟域之前未寄存,如图 6 所示。

未寄存信号

在此示例中,发送时钟域的组合输出可能会在 CDC 边界处经历组合毛刺。这种组合毛刺大大增加了数据变化频率,可能会产生少量的振荡数据突发,从而增加在变化时可以采样的边沿数量,相应地增加采样变化数据和生成亚稳态信号的可能性。

3.6. 将信号同步到接收时钟域

发送时钟域中的信号在传递到 CDC 边界之前应该同步。来自发送时钟域的信号同步减少了可以在接收时钟域中采样的边沿数量,有效地降低了 MTBF 方程中的数据更改频率,从而增加了计算失败之间的时间(有关说明,请参见第 3.3 节)数据更改频率对 MTBF 的影响)。

寄存信号

在图 7 中,aclk 逻辑在被传递到 bclk 域之前在 adat 触发器上建立。adat 触发器滤除触发器输入 (a) 上的组合毛刺,并将干净的信号传递给 bclk 逻辑。

显然,在数据被同步到异步时钟域前,首先需要在自己的时钟域寄存一拍,以消除组合逻辑产生的毛刺。防止毛刺被传播到其他时钟域,从而在数据采样时存在多个边沿,造成采样失败而发生亚稳态。

4. 将快速信号同步到慢速时钟域

如第 3.1 节所述,如果 CDC 信号在时钟域之间传递时无法漏采,那么在时钟域之间传递信号时考虑信号宽度或同步技术就很重要。

与同步器相关的一个问题是,来自发送时钟域的信号在被采样之前可能会更改值两次,或者可能太靠近较慢时钟域的采样边沿。任何时间信号从一个时钟域发送到另一个时钟域时都必须考虑这种可能性,并且必须确定漏采的信号是否重要的问题。

当不允许漏采时,有两种一般方法可以解决该问题:

  1. 一种开环解决方案,可确保无需确认即可捕获信号。
  2. 需要确认收到跨越 CDC 边界的信号的闭环解决方案。

本节将讨论这两种解决方案。

4.1. 时钟域之间可靠的信号传递的要求

一般来讲,快时钟域采慢时钟信号是没有问题的,基本都能采到;但是慢时钟域采快时钟信号则需要分情况讨论。

如果较快时钟域的频率是较慢时钟域的 1.5 倍(或更多),则将较慢的控制信号同步到较快的时钟域通常不是问题,因为较快的时钟信号将对较慢的 CDC 信号采样一次或多次。意识到将较慢的信号采样到较快的时钟域比将较快的信号采样到较慢的时钟域引起的潜在问题更少,设计人员可能会利用这一事实,通过使用简单的两个触发器同步器在时钟域之间传递单个 CDC 信号。

4.1.1. “三边”要求

“三边”要求的实质是要保证信号足够长----能被接受时钟域采到。

Mark Litterick [4] 指出,当通过双触发器同步器在时钟域之间传递一个 CDC 信号时,CDC 信号必须比接收域时钟周期宽 1.5 倍。Littereick 将此要求描述为“输入数据值必须在三个目标时钟边沿保持稳定”。对于特别长的源和目标时钟频率,这个要求可能会安全地放宽到接收时钟域周期时间的 1.25 倍或更少,但“三边”指南是最安全的初始设计条件,并且通过使用 SystemVerilog 断言比在仿真期间动态测量 CDC 信号的分数宽度更容易证明。“三个边沿”要求实际上适用于开环和闭环解决方案,但闭环解决方案的实现会自动确保至少检测到所有 CDC 信号的三个边沿。

4.2. 问题 - 传递快速 CDC 脉冲

考虑存在严重缺陷的情况,即发送时钟域的频率高于接收时钟域,并且 CDC 脉冲在发送时钟域中只有一个周期宽。如果 CDC 信号仅脉冲一个快时钟周期,则 CDC 信号可能会在较慢时钟的上升沿之间变高和变低,而不会被捕获到较慢时钟域中,如图 8 所示。

此类情况就是慢采快直接采不到的可能性。

快信号采样失败

4.3. 问题 - 采样一个长 CDC 脉冲 - 但不够长!

此类情况就是慢采快,虽然不是单周期脉冲信号了,但任然无法被慢时钟域采到。

考虑一些不直观和有缺陷的情况,其中发送时钟域向接收时钟域发送一个脉冲,该脉冲比接收时钟频率的周期略宽。在大多数情况下,信号将被采样并通过,但 CDC 脉冲变化过于接近接收时钟域的两个时钟上升沿,从而违反第一个时钟沿上的建立时间的可能性很小但确实存在并且违反第二个时钟边沿的保持时间并且不形成预期的脉冲。这种可能的故障如图 9 所示。

长脉冲采样失败

4.4. 开环解决方案 - 使用同步器采样信号

该问题的一个可能的解决方案是在超过采样时钟周期时间的一段时间内置位 CDC 信号,如图 10 所示。如第 4.1.1 节所述,最小脉冲宽度是采样时钟周期的 1.5 倍。假设 CDC 信号将被接收器时钟至少采样一次,也可能采样两次。

当相对时钟频率固定并正确分析时,可以使用开环采样。

优点:开环解决方案是通过 CDC 边界传递信号的最快方式,不需要对接收信号进行确认。

缺点:与开环解决方案相关的最大潜在问题是另一位工程师可能会将解决方案误认为通用解决方案,或者设计要求可能会发生变化,并且工程师可能无法重新分析原始开环解决方案。通过向模型中添加 SystemVerilog 断言以检测输入脉冲是否未能超过“三个边沿”设计要求,可以将这个问题最小化。

此方法的本质是将脉冲信号拓宽,实际上就是讲快信号变为比满时钟域更慢的信号----降频。此类方法的缺陷是不够通用,若慢时钟变得更慢,则无法采样了。

开环解决方案

4.5. 闭环解决方案 - 使用同步器采样信号

这个问题的第二个可能的解决方案是发送一个使能控制信号,将其同步到新的时钟域,然后将同步信号通过另一个同步器传回发送时钟域作为确认信号。

优点:同步反馈信号是一种非常安全的技术,可以确认第一个控制信号已被识别并采样到新的时钟域中。

缺点:在允许控制信号改变之前,在两个方向上同步控制信号可能存在相当大的延迟。

此方法的本质是先降频,后握手。通过接受时钟域反馈的信号来决定降频到什么程度。

闭环解决方案

跨时钟域处理解析(二)(Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog)

孤独的单刀已于 2022-05-13 11:08:06 修改

本文参考自《Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog》–Clifford E. Cummings。
 
主要讲述了如何进行跨时钟域设计。正文中,衬色内容是我的啰嗦。

5.0 在时钟域之间传递多 bit 信号

在时钟域之间传递多 bit 信号时,简单的同步器并不能保证数据的安全传输。

工程师在进行多时钟设计时经常犯的一个错误是将同一事务中所需的多个 CDC 位从一个时钟域传递到另一个时钟域,而忽视了 CDC 位同步采样的重要性。问题是同步到一个时钟的多 bit 信号会经历小的数据变化偏斜(skews),偶尔会在第二个时钟域的不同时钟上升沿上采样。即使我们可以完美地控制和匹配多个信号的走线长度,上升和下降时间的差异以及芯片上的工艺变化可能会引入足够的偏斜,从而导致对原本精心匹配的走线的采样失败。

所以必须采用多位 CDC 策略来避免多 bit 的偏斜采样。

多 bit 信号的 CDC 问题主要来自信号之间的 skew(简单地理解为到达目的时钟域的时间无法控制一致),可能的后果就是某些 bit 可能还没到达,但是某些 bit 已经被采样了。

5.1 多 bit CDC 策略

为了避免多 bit CDC 偏斜采样情况,我将多位 bit CDC 策略分为三大类:

  1. 多 bit 信号合并。在可能的情况下,将多个 CDC 位合并为 1 位 CDC 信号。
  2. 多循环路径公式。使用同步负载信号安全地传递多个 CDC 位。
  3. 使用格雷码传递多个 CDC 位

本节的其余部分详细介绍了这些策略。

5.2 多 bit 信号合并

在可能的情况下,将多个 CDC 信号合并为一个 1 位 CDC 信号。问问自己这个问题,我真的需要多个位来控制跨 CDC 边界的逻辑吗?

如以下示例所示,简单地在所有 CDC 位上使用同步器并不总是足够好。

如果控制信号的顺序或对齐很重要,则必须注意将信号正确传递到新的时钟域。本节中显示的所有示例都过于简单,但它们与实际设计中经常出现的情况非常相似。

5.3 问题 – 同时需要两个控制信号。

在图 12 所示的简单示例中,接收时钟域中的寄存器需要加载信号和使能信号,以便将数据值加载到寄存器中。如果负载和使能信号在同一发送时钟边沿上驱动,则控制信号之间的小偏差可能会导致两个信号在接收时钟域内同步到不同的时钟周期。在这些条件下,数据不会被加载到寄存器中。

在这个示例中,需要 2 bit 信号来做逻辑控制。跨时钟域传输这 2 bit 信号的过程中,由于信号到达目的时钟域的时间不一致(存在 skew),导致 2 个信号无法在同一个时钟周期进行采样造成了数据漏采,从而使得预期功能无法实现。

img

5.3.1 解决方案 - 整合

5.3 节问题的解决方法很简单,整合控制信号。如图 13 所示,仅从一个加载启用信号驱动接收时钟域中的加载和启用寄存器输入信号。合并将消除两个及时转移到达的控制信号的可能性。

把控制信号合并不失为一个好办法。

img

5.4 问题 - 两个相位偏差的控制信号。

图 14 中的图表显示了两个使能信号 aen1 和 aen2,它们依次从发送时钟域驱动到接收时钟域,以控制流水线数据寄存器的使能输入。问题是在第一时钟域,aen1 控制信号可能在 aen2 控制信号产生前稍稍终止,在 aen1 和 aen2 控制信号脉冲之间的微小间隙中可能出现接收时钟的上升沿,导致在接收时钟域的使能控制信号链中形成一个周期间隙。这将导致第二个寄存器丢失 a2 数据值。

img

5.4.1 解决方案 - 合并和额外的触发器

这个问题的解决方案,如图 15 所示,是只向接收时钟域发送一个控制信号,并在接收时钟域内生成第二个相移流水线使能信号。

img

5.5 问题 - 多 bit CDC 信号

图 16 中的图表显示了在时钟域之间传递的两个编码控制信号。如果两个编码信号在采样时略微偏斜,则可能会在接收时钟域中的一个时钟周期内生成错误的解码输出。

img

5.5.1 传递多个 CDC 信号的解决方案

多循环路径 (MCP) 公式和 FIFO 技术可用于解决与传递多个 CDC 信号相关的问题。MCP 公式的描述和定义在第 5.6 节中给出。

至少有两种多循环路径 (MCP) 公式可用于解决此问题:

  1. 闭环 - 带有反馈的 MCP 公式。
  2. 闭环 - 带有确认反馈的 MCP 公式。

MCP 制定实施技术将在下一节开始描述。

还有至少两种 FIFO 策略可以作为这个问题的闭环解决方案:

  1. 异步 FIFO 实现。
  2. 2-deep FIFO 实现。

FIFO 实现技术在第 5.8 节开始描述。

img

5.6 多循环路径 (MCP) 公式

使用 MCP 公式 (Multi-Cycle Path (MCP)) 是安全传递多个 CDC 信号的常用技术。

MCP 公式是指将非同步数据发送到与同步控制信号配对的接收时钟域。数据和控制信号同时发送,允许数据在目标寄存器的输入上建立,同时控制信号在到达目标寄存器的加载输入之前同步两个接收时钟周期。

好处:

  1. 发送时钟域不需要计算在时钟域之间发送的适当脉冲宽度。
  2. 发送时钟域只需要将一个使能切换到接收时钟域以指示数据已通过并准备好加载。使能信号不需要返回其初始逻辑电平。

该策略不同步地传递多个 CDC 信号,同时将同步的使能信号传递到接收时钟域。接收时钟域不允许对多位 CDC 信号进行采样,直到同步使能通过同步到达接收寄存器。

这种策略被称为多周期路径公式 [8],因为未同步的数据字直接传递到接收时钟域并保持多个接收时钟周期,从而使使能信号同步并识别到接收时钟域,然后才允许更改未同步的数据字。

因为未同步的数据在被采样之前会被传递并保持稳定多个时钟周期,所以采样值不会有亚稳态的危险。

5.6.1 使用同步使能脉冲的 MCP 公式

或许在时钟域之间传递同步使能信号的最常见方法是使用一个切换使能信号,该信号传递到同步脉冲发生器以指示可以在下一个接收时钟边沿捕获未同步的多周期数据字,如图所示在图 18 中。

这种同步使能脉冲生成的一个关键特性是输入信号的极性无关紧要。在图 18 中,d 输入在周期 1 中切换为高电平,到周期 4 时,高电平信号已通过三个同步触发器传播。在周期 3 中,q2 和 q3 触发器的输出具有不同的极性,导致在同一个周期内在异或门的输出上形成同步使能脉冲。类似地,d 输入在第 7 周期切换为低电平,到第 10 周期,高信号已通过三个同步触发器传播。再次在周期 9 中,q2 和 q3 触发器的输出具有不同的极性,导致在异或门的输出上形成同步使能脉冲。

这里的脉冲信号会作为后面的握手过程的反馈信号。

img

由于第 5.0 节中描述的所有 MCP 公式都使用同步使能脉冲生成电路,因此创建和使用较小的等效符号来表示同步使能脉冲生成电路被认为是有用的。等效符号如图 19 所示。

img

除了生成任何 d 输入极性的脉冲之外,同步使能脉冲生成电路还有一个 q 输出,它跟随延迟三个时钟周期的 d 输入。q 输出经常用作反馈信号,并作为确认信号通过发送时钟域中的另一个同步使能脉冲生成电路传递。

图 20 显示了典型的发送 - 接收触发脉冲生成设计。

img

使用这种技术时,要求接收时钟域具有适当的逻辑来在检测到脉冲时捕获数据,因为对于每个多周期数据字,脉冲仅在一个接收时钟周期内有效。

5.6.2 闭环 - 带反馈的 MCP 公式

使用 MCP 公式时的一项重要技术是将启用信号作为确认信号传回发送时钟域,如图 21 所示。

img

对于图 21 中的示例,确认反馈信号 (b_ack) 生成确认脉冲 (aack),该脉冲用作小型 READY-BUSY、1 状态 FSM 模块的输入,该模块生成就绪信号 (aready) 以指示现在可以安全地再次更改数据输入 (adatain) 值。一旦 aready 信号变高,发送方就可以自由发送新数据(adatain)和伴随的 asend 控制信号。

这是一条自动反馈路径,假定接收时钟域将始终准备好接收通过 MCP 公式同步的下一个数据字。

5.6.3 闭环 - 带有确认反馈的 MCP 公式

5.6.2 节中描述的技术的一个完全响应变化使用 MCP 公式是只有在接收时钟域确认接收到带有 bload 脉冲的数据之后,才将使能信号作为确认信号传递回发送时钟域,如图所示在图 22 中。

img

对于图 22 中的示例,接收时钟域有一个小的 WAIT-READY、1-state FSM,当数据寄存器输入端的数据有效时,它会向接收逻辑发送一个有效信号 (bvalid)。在接收逻辑确认应通过置位 bload 信号加载数据之前,数据并未真正加载。在数据加载之前没有反馈到发送时钟域,然后 b_ack 信号被发送回与具有自动反馈的 MCP 公式相同。这是一条反馈路径,需要在捕获数据和发送反馈之前对接收时钟域部分采取行动。

5.7 同步计数器

如前所述,在时钟域之间传递多个信号时,要问的一个重要问题是,我是否需要对从一个时钟域传递到另一个时钟域的信号的每个值进行采样?对于这种情况,答案经常是,不!

参考文献 [1] 详细介绍了 FIFO 设计技术,其中格雷码计数器在时钟域之间采样,而中间格雷码计数值经常被遗漏。对于这种 FIFO 设计,更多的考虑是确保计数器不会超出其边界,这可能导致错过满和空标志检测。即使时钟域之间的采样格雷码计数值经常被遗漏,该设计也是稳健的,并且所有重要的格雷码计数值都被适当地采样。有关详细信息,请参阅 [1]。

由于可能允许有效设计跳过某些计数值样本,是否可以使用任何计数器来跨 CDC 边界传递计数值?答案是不。

5.7.1 二进制计数器

二进制计数器的一个特点是,所有连续二进制递增操作中有一半要求必须更改两个或更多计数器位。尝试跨 CDC 边界同步二进制计数器与尝试将多个 CDC 信号同步到新的时钟域中是一样的。如果一个简单的 4 位二进制计数器从地址 7(二进制 0111)更改为地址 8(二进制 1000),则所有四个计数器位将同时更改。如果同步时钟边沿出现在此转换的中间,则任何 4 位二进制模式都可能被采样并同步到新的时钟域中,如图 23 所示。

img

在 FIFO 设计中,新的同步二进制值可能会触发错误的满或空标志,甚至更糟的是,它可能不会触发真正的满或空标志,从而导致数据因 FIFO 而丢失。当 FIFO 真的为空时,由于尝试读取数据而导致溢出或导致从 FIFO 读取无效数据。

如果使用经典的 2 进制码,那么从 7 到 8 的跳变过程中,4 位都需要传递,那么可能 4 位信号间的 skew 会导致亚稳态或者数据传输错误,这一问题可以通过格雷码来解决。

5.7.2 格雷码

格雷码以 Frank Gray [4] 命名,可用于多时钟设计的最安全计数器是格雷码计数器。格雷码只允许为每个时钟转换更改一位,从而消除了与尝试跨时钟域同步多个更改的 CDC 位相关的问题。

标准格雷码具有非常好的转换特性,可以将格雷码转换为二进制并再次转换回来。使用这些转换,设计高效的格雷码计数器很简单。

5.7.3 格雷码到二进制转换

将格雷码值转换为等效的二进制码值,以 n 位格雷码值为例,二进制位 0 等于格雷码位 0 的异或与所有其他格雷码的异或位从 1 到 n。二进制位 1 等于格雷码位 1 与从 2 到 n 等所有其他格雷码位的异或运算。最高有效的二进制位刚好等于最高有效的格雷码位。

图 24 显示了示例 4 位灰度到二进制转换的方程式。

img

编写格雷码到二进制转换器的最简单方法是编写一个 for 循环并对具有可变索引范围的灰度代码向量进行异或约简,其中每次通过循环,索引范围的 LSB 都会增加,直到我们只剩下 bin [MSB] = 的简单赋值 ^gray [MSB:MSB](只是格雷码向量的 1 位 MSB),如示例 1 所示。

img

不幸的是,Verilog 和 SystemVerilog 不允许使用可变索引范围进行部件选择,因此示例 1 中的代码虽然在概念上是正确的,但不会编译。

为了解决这个问题,请记住异或门实际上是一个可编程反相器。如果一个输入被拉高,另一个输入被反相并传递到输出。类似地,如果一个输入被拉低,另一个输入被传递到输出而不反转(从输入到输出没有变化)。

利用任何涉及 0 输入的添加异或运算不会改变运算结果的事实,格雷码到二进制转换的方法是异或有效的格雷码位带有填充的 0,如图 25 所示。

img

此简化算法的相应参数化 SystemVerilog 模型如示例 2 所示。此示例在语法上是正确的,可以编译并且可以正常工作。

img

输入绑定到 0 的所有额外的异或运算会发生什么?综合工具认识到,可以优化一个输入上具有常数 0 的异或门,以推断设计的非常有效的实现。

5.7.4 二进制到格雷码的转换

将二进制值转换为等效的格雷码值,以 n 位二进制值为例,格雷码位 0 等于二进制位 0 和 1 的异或。格雷码位 1 等于到二进制位 1 和 2 的异或等。最高有效的格雷码位刚好等于最高有效的二进制位。

示例 4 位二进制到格雷码转换的方程式如图 26 所示。

img

编写二进制到格雷码转换器的最简单方法是编写一个简单的连续赋值,该赋值在二进制向量和相同二进制向量的右移版本之间执行按位异或运算,如图所示 示例 3。这个示例在语法上是正确的,可以编译并且可以工作。

img

5.7.5 格雷码计数器样式 #1

我们可以使用第 5.7.3 节和 5.7.4 节。对于任何格雷码计数器,重要的是要记住必须寄存格雷码输出以消除设计中的任何组合毛刺。

格雷码计数器样式 #1 的 SystemVerilog 代码包含一个格雷码到二进制转换器、一个二进制到格雷码转换器并在转换之间递增二进制值,如图 27 所示。

img

示例 4 显示了格雷码计数器样式 #1 的相应参数化 SystemVerilog 模型。

img

5.7.6 格雷码计数器样式 #2

我们可以仅使用第 5.7.4 节中显示的二进制到格雷码转换来构建第二种样式的格雷码计数器。这个格雷码计数器实际上既是一个二进制计数寄存器又是一个格雷码计数寄存器。

img

用于格雷码计数器样式 #2 的 SystemVerilog 代码包含一个二进制计数器以消除格雷码到二进制转换的需要,并使用下一个二进制计数值进行二进制到格雷码转换,然后将其寄存到格雷码中。这种风格使用两倍数量的触发器,但生成下一个格雷码值的组合逻辑路径更短,这使得这种实现比格雷码计数器样式 #1 更快。格雷码计数器样式 #2 的框图如图 28 所示。

示例 5 显示了格雷码计数器样式 #2 的相应参数化 SystemVerilog 模型。

img

5.8 其他多 bit CDC 技术

除了前面部分描述的 MCP 公式化技术之外,我还发现许多工程师使用标准 FIFO 在时钟域之间传递数据和控制信号。

至少有两种有趣的 FIFO 实现策略可用于解决多位 CDC 信号完整性问题:

  1. 异步 FIFO 实现。
  2. 2-deep FIFO 实现。
5.8.1 使用异步 FIFO 的多位 CDC 信号传递

传递多个位,无论是数据位还是控制位,都可以通过异步 FIFO 来完成。异步 FIFO 是共享存储器或寄存器缓冲区,其中数据从写时钟域插入,数据从读时钟域移除。由于发送方和接收方都在各自的时钟域内运行,因此使用双端口缓冲器(例如 FIFO)是在时钟域之间传递多位值的安全方式。

只要 FIFO 未满,标准异步 FIFO 设备就允许插入多个数据或控制字,只要 FIFO 不为空,接收器然后在方便时提取多个数据或控制字。

FIFO 设计中的大部分比较难的工作是通过格雷码计数器的同步完成的,[1] 中描述了一种经过验证的 FIFO 设计技术。

5.8.2 使用 1-deep/2-register FIFO 同步器的多位 CDC 信号传递

跨 CDC 边界传递多个控制位和数据位的另一个有趣变化涉及使用 1 深度双寄存器 FIFO,如图 29 所示。

img

这种 1 深度双寄存器 FIFO 具有许多有趣的特性。由于 FIFO 仅使用两个寄存器或一个 2 深双端口 RAM 构建,因此用于检测满和空的格雷码计数器是简单的触发触发器,实际上只不过是 1 位二进制计数器(请记住,标准格雷码的 MSB 与二进制码的 MSB 相同)。

复位时,两个指针都被清除,FIFO 为空,因此 FIFO 未满。我们使用反转的未满条件来指示 FIFO 已准备好接收数据或控制字(wrdy 为高)。在将数据或控制字放入 FIFO 后(使用 wput),wptr 切换并且 FIFO 变满,或者换句话说,wrdy 信号变低,这也禁用了切换 wptr 的能力,因此也禁用了能够将另一个字放入 2 寄存器 FIFO,直到第一个字被接收时钟域逻辑从 FIFO 中删除。

这个设计特别有趣的是 wptr 现在指向 2 寄存器 FIFO 中的第二个位置,所以当 FIFO 再次准备好时(当 wrdy 为高时),wptr 已经指向下一个位置写。

在 FIFO 的接收端复制了相同的概念。当数据或控制字写入 FIFO 时,FIFO 变为非空。我们使用反转的非空条件来指示 FIFO 具有准备接收的数据或控制字(rrdy 为高)。

通过使用两个寄存器来存储多位 CDC 值,我们能够从发送 MCP 公式中删除一个时钟周期,并从确认反馈路径中删除另一个周期。


跨时钟域处理解析(三)(Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog)

孤独的单刀已于 2022-05-13 11:08:16 修改

本文参考自《Clock Domain Crossing (CDC) Design & Verification Techniques Using SystemVerilog》–Clifford E. Cummings。
 
主要讲述了如何进行跨时钟域设计。正文中,衬色内容是我的啰嗦。

6.0 命名约定和设计分区

命名约定有助于确保良好的团队沟通,并有助于使用脚本语言来收集和分组设计中与特定时钟相关联的所有信号。良好的设计划分可以显著减少综合和验证多时钟设计时序的工作。本节将讨论推荐的命名约定和设计分区。

有两种方法可以解决潜在的 CDC 问题:

  1. 验证设计是否符合正确的 CDC 规则,
  2. 避免该问题。

这两种方法都很有价值,应该用于确保无错误设计。

第一种方法,即 CDC 设计规则的验证,通常需要使用特殊工具来检查设计是否存在可能的 CDC 违规。当我在 2001 年写第一篇关于多时钟设计的论文时,我不知道市场上有任何工具可以检查 CDC 规则。如今,有许多公司提供此类工具(有关 CDC 验证空间中的公司和工具列表,请参阅 [11])。第二种方法,避免这个问题,可以通过使用下面概述的一些好的编码指南来完成。

6.1 时钟和信号命名约定

各种设计团队已经使用了许多有用的时钟和信号命名约定。

大纲:使用时钟命名约定来标识设计中每个信号的时钟源。

原因:命名约定可帮助所有团队成员识别设计中每个信号的时钟域,并且还可以使用综合脚本中的正则表达式 “通配符” 使用于时序分析的信号分组更容易。

一种行之有效的命名约定要求使用前导前缀字符来标识各种异步时钟域。示例包括:用于微处理器时钟的 uClk、用于视频时钟的 vClk 和用于显示时钟的 dClk。

然后每个信号与设计中的某一个时钟域同步,每个信号名称都用前缀字符标记,以标识用于生成该信号的时钟域。例如,任何由 uClk 产生的信号在信号名称中都标有 u 前缀,如 uaddr、udata、uwrite 等。任何由 vClk 产生的信号在信号名称,例如 vdata、vhsync、vframe 等。相同的信号命名约定用于设计中任何其他时钟生成的所有信号。

使用这种技术,设计团队中的任何工程师都可以轻松识别设计中任何信号的时钟域源,并直接使用这些信号或通过适当的同步传递信号,以便在新的时钟域中使用这些信号。

确切的命名约定并不重要,但至关重要的是项目中的每个工程师都同意遵守团队选择的命名约定。命名约定将显著提高设计团队的生产力。

这个每个公司基本上都有自己的约定,规范些的公司还会以文件的形式发布,要求所有设计人员都遵守。

6.1.1 无命名约定的多时钟 / 多源模块

如果您的团队不使用任何特定的面向时钟的信号命名约定,并且如果允许模块具有多个时钟输入,则始终存在 CDC 分析工具可能设置不正确的危险,并且很容易错过不良的 CDC 设计实践。

即使您的团队可以使用良好的 CDC 分析工具,我也强烈建议您采取几个简单的步骤,以便更轻松地识别和调试潜在的 CDC 设计问题的分析和识别。

6.2 每个时钟域的时序验证

要验证任何设计的时序,必须验证设计中的每个时钟域是否满足时序要求。尽管工具在过去十年中有所改进,可以帮助自动分析和验证不同时钟域中的信号,但使用良好的分区和命名约定进行多时钟设计仍然是一种很好的做法。

通过对设计进行分区以允许每个模块只允许一个时钟,静态时序分析对于设计中的每个域来说变得非常容易。

6.3 面向时钟的设计分区

一些最简单和最好的设计分区方法是在时钟边界使用设计分区来实现的。

大纲:每个模块只允许一个时钟 [9]。

原因:在单时钟模块或单时钟模块组上更容易完成静态时序分析和创建综合脚本。

例外:将来自所有不同时钟域的信号连接在一起的顶级模块自然会将所有时钟作为该模块的输入。尽量减少您的多时钟验证工作,只允许顶层模块有多个时钟输入。

大纲:将设计块划分为一个时钟模块。

原因:完全同步的子块的时序验证可以使用 STA(静态时序分析)工具轻松验证,并将设计块划分为多个一个时钟域子块将大型复杂的时序分析任务转换为多个完全同步的单时钟设计。

大纲:创建同步器模块以将信号从一个时钟域传递到另一个时钟域,并且每个同步器模块只允许一个时钟。

原因:假设从一个时钟域传递到另一个时钟域的任何信号最终都会遇到建立和保持时间问题。隔离 CDC 边界逻辑可以显著减少多时钟设计的设计和验证工作。

在大多数情况下,同步器模块将是设计中唯一会遇到有意建立和保持时间违规的模块。在异步时钟域之间传递信号时,会发生时序违规,这就是必须在设计中添加同步器的全部原因。

设计尽量模块化,细化,然后每个模块都只使用一个时钟,最后通过顶层模块(可以有数个层级)来将子模块汇总,从而将 CDC 问题控制在顶层模块,有效地减少问题发生的概率。

img

考虑具有三个时钟域的示例设计,标记为 aClk、bClk 和 cClk,如图 30 所示。在该设计中,所有 aClk 设计块都被组合到一个 aClk 逻辑块中。所有的 bClk 设计块都被组合到一个 bClk 逻辑块中,同样我们创建了一个 cClk 逻辑块。任何源自异步的信号时钟域在允许驱动另一个逻辑块的输入之前通过同步器模块。

6.3.1 时钟分区模块的时序分析

使用面向时钟的设计分区策略,每个设计块的所有输入和输出都完全同步到一个时钟。这是使用静态时序分析 (STA) 工具验证的最简单的设计类型,因为设计中没有错误路径。

将在每个时钟域内计时的所有设计模块组合在一起。应该为设计中的每个时钟域形成一个组。这些组将进行时序验证,就好像每个组都是单独的、完全同步的设计。对于每个时钟域,我们有一个设计模块,我们可以轻松执行最坏情况(最大时间 / 设置时间检查)时序分析和最佳情况(最小时间 / 保持时间检查)时序分析。

同样使用这种面向时钟的分区策略,每个 CDC 边界都使用同步器模块进行了隔离。每个同步器模块仅包括由 ASIC 或 FPGA 供应商(首选)提供的同步器单元,或者使用成对连接的触发器构建以形成同步器等效单元。

如果 ASIC 或 FPGA 供应商提供同步器单元并在设计中实例化,则无需验证这些模块的建立和保持时间,因为供应商应该已经创建了不违反建立或保持的单元布局触发器阶段之间的时间。

如果同步器是从 RTL 代码综合的,最重要的是执行最佳情况时序分析,以确保触发器不会靠得太近,以免第一级的输出变化太快满足第二阶段输入的保持时间要求。一位同事最近指出,还应该执行最坏情况时序分析,以防布局工具碰巧将两个同步器触发器放置在 ASIC 或 FPGA 芯片上相距很远的地方。我同意这个更新的建议。

由于单独同步器的分区,门级模拟可以更容易地配置为忽略每个同步器第一阶段的建立和保持时间违规。

RTL 同步器的静态时序分析需要简单的 set_false_path 命令来移除 STA 的输入。我们知道同步器的输入端存在计时问题,这就是使用同步器的原因。

通过划分设计和同步器块以允许每个模块只允许一个时钟,静态时序分析变得非常容易执行。用于解决多个时钟域问题的综合脚本命令现在变成了分组、识别错误路径和执行最小 - 最大时序分析的问题。

6.4 用 MCP 方法进行分区

在时钟边界处将设计划分为单独的设计块和同步器块在大多数情况下效果很好,但如果需要使用 MCP 公式在时钟域之间传递多个信号,则传递到设计块的一些信号可能会出现来自不同的时钟域,如图 31 所示。

img

如果设计中的信号使用了基于时钟的命名约定,那么具有异步输入的设计模块仍然可以轻松计时。在有问题的设计块上执行 STA 之前,只需从分析中排除异步输入。

通常,只有同步器和 MCP 公式数据路径的输入需要 “set_false_path” 命令。如果使用时钟前缀命名方案,则可以使用通配符轻松识别所有异步输入。在图 31 中,要从 bClk 逻辑块内的 STA 中排除 adata 总线,请先执行以下命令:

set_false_path -from { a* }

该命令应该足以消除来自 bClk STA 的所有异步输入。

7.0 多时钟门级仿真问题

当同步器识别 CDC 信号上的设置和保持时间违规时,数字仿真模型通常会生成 X。这通常会导致门级仿真失败。存在哪些技术来解决此问题?

如 6.3.1 节所述,通过同步器跨越时钟边界的信号将遇到建立和保持违规。这就是为什么在设计中添加同步器的原因,以过滤掉变化太接近接收时钟域时钟信号上升沿的信号的亚稳定性效应。

7.1 同步器门级 CDC 仿真问题

在多时钟设计上进行栅极级仿真时,触发器的 ASIC 库模型使用建立时间和保持时间表达式进行建模,以匹配实际触发器的时序规格。ASIC 库通常对触发器进行建模,以便在发生时序违规时驱动触发器输出上的 X(未知数)。仿真栅极级同步器时,违反建立时间和保持时间可能会导致 ASIC 库发出建立时间和保持时间错误消息,并且有问题的信号经常被驱动到 X 值。这些 X 值传播到设计的其余部分,在尝试验证整个栅极级设计的功能时会引起问题,如图 31 所示。

img

7.2 从门级模拟中消除 X 传播的策略

在过去的 10 年中,许多同事与我分享了许多策略,以解决每次信号违反同步器第一阶段的设置或保持时间时与 X 的不必要传播有关的问题。

由于 X 传播发生在违反设置或保持时间时,因此几乎所有解决此问题的方法都涉及将设置和保持时间更改为 0,以便不会违反设置或保持时间,因此不会发生 X 传播。

有些方法是坏的,有些是好的。以下是为解决 X 传播问题而考虑的一些策略。

7.2.1 关闭时序检查的模拟器命令

大多数 SystemVerilog 模拟器都有一个命令选项来忽略所有时序检查,但这也会忽略设计其余部分所需的时序检查。

7.2.2 将触发器建立和保持时间更改为 0

对于同步器中使用的任何 ASIC 库触发器,可以将设置和保持时间设置更改为零,但这将导致该类型触发器的所有实例的所有设置和保持时间检查都设置为零,包括您可能希望用于测试设计其余部分的触发器。

7.2.3 复制和修改新的触发器模型

您可以从 ASIC 库制作触发器的副本,并将它们存储到具有不同名称的新 SystemVerilog 库中,将所有建立时间和保持时间设置为零,然后修改设计门级网表,用修改后的库触发器替换所有第一级同步器 ASIC 库触发器,而无需时序检查,但这可能是一个容易出错且繁琐的过程,每次生成新网表时可能都必须重复该过程,或者每次生成新网表时都必须重复该过程它可能需要创建一个生成文件和脚本,以便在每次生成新的网表时自动进行修改。

7.2.4 Synopsys set_annotated_check 命令

Bhatnagar [5] 提出的解决这个问题的一个有用方法是使用 Synopsys 命令来修改设置的 SDF 背注释,并在设计中的第一阶段触发器单元上保持时间。Bhatnagar 指出,SDF 文件是基于实例的,因此更容易实现针对违规单元格的设置和保持时间。Bhatnagar 指出:

与其手动从 SDF 文件中删除设置和保持时间构造,更好的方法是将 SDF 文件中的设置和保持时间清零,仅针对违规的失败,即用零替换现有的设置和保持时间编号。

Bhatnagar 进一步指出,设置保持时间为零意味着不会有时序违规,因此不会将未知数传播到设计的其余部分。Bhatnagar 给出的以下 dc_shell-t 命令用于使建立和保持时间为零:

set_annotated_check 0 -setup -hold -from REG1/CLK -to REG1/D

对同步器的第一级触发器的输出使用创造性的命名约定可能会使通配符表达式能够轻松地回溯注释所有第一阶段触发器 SDF 设置,并使用很少的 dc_shell-t 命令将时间值保持为零。如果使用 Synopsys DesignCompiler 工具完成设计,则此技术有效,但非 Synopsys 流呢?

7.3 消除 X 传播的其他策略

第 7.2 节至第 7.2.4 节中描述的所有策略都在我 2001 年给出的第一篇多时钟设计论文中进行了分享。在最初的演示之后,许多工程师站出来分享了从门级仿真中去除 X 传播的其他技术。来自至少三家公司的工程师对该技术的描述与 ### 一节中的描述非常相似(向参加圣何塞 SNUG-2001 的工程师致敬)。

从那时起,来自许多公司的其他工程师分享了其他技术。本节将介绍这些技术,我非常感谢所有工程师,他们每年都继续与我分享有趣的技术。向大家致敬!

7.3.1 使用多个 SDF 文件

请记住,消除不需要的 X 传播的关键是强制同步器输入的建立时间和保持时间为 0,从而消除同步器输入上所有可能的建立和保持时间冲突。

许多工程师告诉我,他们实际上生成了两个 SDF 文件。第一个 SDF 文件具有整个设计的所有实际延迟,包括精确的建立时间和保持时间。然后,工程师生成第二个 SDF 文件,其中仅包含第一级触发器。在此文件中,建立时间和保持时间设置为 0。一些工程师手动构建此文件,而其他工程师则使用脚本生成此文件。

然后,工程师使用 $sdf_annotate 命令读取第一个 SDF 文件。然后,它们读取第二个 SDF 文件,该文件覆盖了第一级同步器数据输入的建立时间和保持时间。读取两个 SDF 文件时,每个实例的最后一个 SDF 文件优先。所有时序都进行了准确的注释,然后修改了第一级同步器的时序检查。

这是一种聪明的方法,可用于生成 SDF 文件的任何工具流。

7.3.2 带有支持 SDF

生成工具的供应商同步器单元 其他工程师已经告诉过解决 X 传播问题的好方法,但该方法需要(a)控制单元库,或(b)与您的 ASIC 供应商建立良好的工作关系。

此技术要求创建一个单独的同步器单元,并在两个触发器阶段之间具有适当的放置关系。要使此方法正常工作,供应商必须提供:

  1. 实际的同步器单元 - 这些将被固定到设计中。
  2. 用于模拟的同步器单元的系统验证模型。
  3. SDF 文件生成工具,这些工具将为同步器单元生成具有 0 设置和 0 保持的 SDF 文件。

如果供应商可以提供此单元和这些功能,则只需生成一个 SDF 文件,并对同步器单元进行适当的时序检查。

任何提供此功能的 FPGA 供应商的 ASIC 都在为他们的客户群带来巨大的帮助。我听说一些 ASIC 供应商提供此功能。我不知道有任何 FPGA 供应商提供这种功能。认识到大多数现代设计都是多时钟设计,我强烈敦促所有 ASIC 甚至所有 FPGA 供应商为同步器单元提供适当的仿真和 SDF 文件工具支持。

7.3.3 具有内置同步器支持的供应商

如果有人知道提供此支持的供应商,请通过适当的联系信息让我知道供应商是谁,我将定期更新本白皮书,以表彰向我们(设计社区)提供此功能的供应商。

供应商列表: (截至本文,未列出任何供应商)

7.4 用于门级 CDC 模拟的多个 SDF 文件

在 2001 年我第一次做多时钟演示后,来自至少三家不同公司的工程师在我演讲后立即分享了以下解决门级仿真中 X 传播的出色技术。

该技术涉及写出完整的 SDF 时序文件,然后手动或使用脚本,为所有同步器模块的第一级触发器生成第二个 SDF 文件。第二个 SDF 文件将所有设置和保持时间设置为 0,然后使用 $sdf_annotate 命令将两个 SDF 文件应用于设计。第一个 SDF 文件将所有实际时序注释为整个设计,然后读取第二个 SDF 文件以覆盖第一级同步器的建立时间和保持时间。

这种技术的优点是,它可以使用所有工具用于所有设计,而不仅仅是 Synopsys ASIC 设计。这是一项强烈推荐的技术。

7.5 强制同步器通知器输入为固定值

Verilog 和 SystemVerilog 设置的内置计时检查和保持时间检查($setup , $hold 和 $setuphold)具有可选的通知程序输出。每当检测到时序冲突时,此通知程序输出就会从 0-1-XZ 切换。

大多数 ASIC 和 FPGA 触发器型号都是从 Verilog 用户定义基元 (UDP) 构建的,通知器信号通常被列为 UDP 表的输入之一。每当通知器输入切换(由时序违规引起)时,触发器输出就会变为未知,而该未知值在栅极级触发器型号的输出上可见。这些第一级触发器型号上的通知程序可以强制到逻辑电平,以防止它们在仿真过程中切换并导致触发器输出未知。

至少一家公司使用的一种聪明技术迫使第一级同步器触发器的时序违规通知器被强制到一个逻辑电平,这样它们就永远无法切换和触发 X 进入触发触发器模型。

7.6 ASIC 和 FPGA 库单元同步器

如果 ASIC 和 FPGA 提供商能够提供可以实例化到设计中的完全表征的同步器单元,那么 CDC 设计可以更容易地完成。

高级 ASIC 供应商提供:

  1. 表征的同步器单元。
  2. 用于模拟同步器单元的 Verilog 模型。
  3. SDF 发生器,用于生成 SDF 文件,这些文件将同步器单元上的建立和保持时间注释为 0,以避免在信号跨越 CDC 边界时违反设置或保持时间时生成 X。

据我所知,没有 FPGA 提供商提供这种功能,但具有前瞻性思维的 FPGA 提供商会为其先进的多时钟设计客户提供此类单元。

7.7 具有随机延迟插入的仿真模型

多个同事提出了一个有趣的模型,该模型综合到正确的同步器进行设计,但以随机周期延迟进行仿真。

该模型的框图如图 33 所示,支持该模型的 SystemVerilog 代码如示例 6 所示。

img

从框图中可以看出,该模型旨在生成可合成的同步器模型,或用作具有可选延迟的仿真模型。

IEEE Std 1364.1-2002 Verilog RTL Synthesis Standard [6] 要求在读取任何 Verilog 模型之前,兼容的合成工具设置 SYNTHESIS 宏。尽管大多数综合工具在很大程度上忽略了 IEEE Verilog 综合标准的许多要求,但大多数工具都实现了这个不错的综合宏要求。

在读取此 sync2 SystemVerilog 代码之前设置 SYNTHESIS 宏的工具将选择代码来推断两个触发器同步器。

未设置 SYNTHESIS 宏的模拟器将读取 sync2 模型,忽略用于可合成模型的代码,并在代码的 “else” 部分中模拟模型。模型已参数化,因此可以使用相同的模型与宽度为 1 位的默认参数 SIZE 一起用于简单的 1 位 CDC 信号,或者可以使用 SIZE 实例化模型 参数设置为多位宽度,以便同步器可用于捕获和同步多位总线,例如格雷码计数器。

img

模型的模拟部分包括一个名为 DLY 的 SIZE 变量的默认声明。默认情况下,DLY 变量初始化为 0,这会导致整个 sync2 模型以两个触发器延迟的默认值进行模拟,但 DLY 变量可以从测试平台分层设置为可重现的 1 和 0 的随机值,从而导致一些总线中的位通过三个触发器级,而其他位仅通过两个触发器级。这可以对一组同步器的行为进行建模,其中一些位在比其他位更早的时钟边沿被捕获,并允许仿真观察设计在多位数据路径中的小偏斜下的表现如何。

8.0 总结与结论

时钟域交叉 (CDC) 错误会导致严重的设计失败。通过遵循一些关键指南并使用完善的验证技术,可以避免这些代价高昂的故障。

8.1 推荐的 1 bit CDC 技术

在时钟域之间传递 1 bit 时:

  • 在发送时钟域中注册信号以消除组合稳定。
  • 将信号同步到接收时钟域。可能需要多循环路径 (MCP) 公式。

8.2 推荐的多 bit CDC 技术

在时钟域之间传递多个控制或数据信号时,请使用以下策略之一:

  • 合并 - 在将信号同步到接收域之前,首先尝试在发送时钟域中将多 bit 信号组合成 1 bit 表示。
  • 使用多周期路径 (MCP) 公式 跨时钟域传递多个信号。
  • 使用 FIFO 传递多位总线,数据或控制总线。
  • 使用格雷码计数器

8.3 推荐的命名约定和设计分区

使用基于时钟的命名约定。尽可能将设计子块划分为完全同步的 1 时钟设计。

8.4 多时钟门级 CDC 仿真的推荐解决方案

对于门级仿真期间的 CDC X 传播仿真问题,有多种有用的解决方案:

  • 使用 Synopsys 开关为同步器上的第一级触发器生成 0-setup 和 0-hold 时间。仅适用于 Synopsys 工具。
  • 使用多个 SDF 文件 - 本节稍后介绍的好技术。
  • 供应商提供同步器单元和适当的 SDF 工具 - 如果您的 ASIC 或 FPGA 供应商提供模型和工具,这是一个很好的解决方案(很少这样做 - 要求您的 ASIC 和 FPGA 供应商支持此功能)。
  • 使用创造性的 SystemVerilog 模型来模拟同步问题。

本文中描述的技术旨在促进多时钟设计的稳健开发和验证。


跨时钟域(CDC)设计方法之单 bit 信号篇

孤独的单刀 于 2022-05-13 15:38:39 修改

1、什么是跨时钟域?

FPGA 内容的设计大都是以同步电路为基础,而同步电路的触发则需要统一时钟。时钟信号彷佛是电路的 “心跳”,统一给 “被管理的” 触发器提供血液,而在这个时钟信号下的信号就可以被称之为 – 属于该时钟域。随着芯片集成度的提高,以及日益严苛的设计要求,在嵌入式系统内部或与其他系统的数据交互中,难免会出现原本属于时钟域 A 的信号 a,需要传入到另一时钟域 B 来对其进行操作,这一操作则称之为跨时钟域,对应 Clock Domain Crossing,CDC。

由于双方时钟频率、相位等的差异,导致原本属于时钟域 A 下的同步信号 a 变成了一个时钟域 B 下的异步信号。我们知道,异步信号是有概率无法满足触发器的建立时间要求和保持时间要求。一旦出现建立时间和保持时间违例,则有可能会导致系统发生亚稳态。

img

时序电路的基础是触发器(FF、Flip-Flop),触发器正常工作需要满足建立时间和保持时间的时序要求。

建立时间(Tsu:set up time)

是指在触发器的时钟信号上升沿到来以前,数据稳定不变的时间,如果建立时间不够,数据将不能在这个时钟上升沿被稳定的打入触发器,Tsu 就是指这个最小的稳定时间。

保持时间(Th:hold time)

是指在触发器的时钟信号上升沿到来以后,数据稳定不变的时间,如果保持时间不够,数据同样不能被稳定的打入触发器,Th 就是指这个最小的保持时间。

2、什么是亚稳态?

亚稳态 (Metastability):如果数据传输中不满足触发器的建立时间要求和保持时间要求不满足,就可能产生亚稳态,此时触发器输出端 Q 在有效时钟沿之后比较长的一段时间处于不确定的状态,在这段时间里 Q 端在 0 和 1 之间处于振荡状态,而不是等于数据输入端 D 的值。这段时间称为决断时间 Tmet(resolution time)。经过 resolution time 之后 Q 端将稳定到 0 或 1 上,但是稳定到 0 或者 1,是随机的,与输入没有必然的关系。

img

只要系统中有异步元件,亚稳态就是无法避免的,亚稳态主要发生在异步信号检测、跨时钟域信号传输以及复位电路等常用设计中。由于产生亚稳态后,寄存器 Q 端输出在稳定下来之前可能是毛刺、振荡、固定的某一电压值。在信号传输中产生亚稳态就会导致与其相连其他数字部件将其作出不同的判断,有的判断到 “1” 有的判断到 “0”,有的也进入了亚稳态,数字部件就会逻辑混乱。

3、跨时钟域处理方法的分类

既然信号的跨时钟域可能会引入亚稳态问题,那么就需要想办法对其进行处理,从而降低亚稳态发生的概率(即提高 MTBF)。

跨时钟域处理方法可以分为两个大类:单 Bit 信号跨时钟域处理、多 Bit 信号跨时钟域处理。分类的原因是多 bit 信号的传递不光只有亚稳态这一个问题,还可能会因为多个信号之间由于工艺、PCB 布局等因素导致的信号传输延时(skew)的存在,从而导致信号被漏采或者错采。

4、单 bit 信号进行跨时钟域处理的方法

信号从源时钟域跨到目的时钟域后,需要在目的时钟的 “指挥” 下进行操作,这一过程可视为用目的时钟对被处理后的信号进行 “采集”,既然是采集,则必须满足奈奎斯特采样定理,也就是说目的时钟频率需要至少是源时钟频率的 2 倍

在实际应用过程中,不光需要将慢时钟域信号跨到快时钟域,也需要将快时钟域信号跨到慢时钟域,而后者显然是无法满足采样定理的,所以需要进行一些特定处理。

4.1、电平信号

如果是电平信号进行 CDC,不用考虑时钟快慢,直接用 2 级或其他级数的同步器就可以,因为总能被采样到。实际上可以将电平信号看做一个频率超级低的脉冲信号。

4.2、从慢时钟域到快时钟域的脉冲信号

首先需要约定的是,所谓快时钟频率应该至少是慢时钟频率的两倍。若无法满足,则可使用 4.3 方法进行处理。在这一前提条件下,是可以保证需要处理的信号能被目的时钟域正确采集到的。

2 级同步器进行同步(DFF)是我们最常见的 CDC 处理方法。这一方法的本质是用触发器将被 CDC 信号同步到目的时钟域下,但是每一次的同步仍然可能出现亚稳态,随着同步级数的增加,亚稳态出现的概率也会减少。需要注意的是,这一过程并非线性的。实际上,3 级同步器以上对亚稳态减小的作用就很小了。通常使用 2 级或者 3 级同步器即可将亚稳态发生的概率维持在一个非常小的值。

img

4.3、从快时钟域到慢时钟域的脉冲信号

下图中,adat 是快时钟域 a 下的脉冲信号,bclk 是目的时钟(慢时钟),很明显,信号 adat 被同步到目的时钟域后出现了漏采,这是因为 adat 的频率高于目的时钟。

img

既然脉冲信号频率高于目的时钟才导致的漏采,那么我将脉冲信号拓宽到一定的程度不就可以保证采集到了吗?这一方法的本质实质上是降频,也就是将问题转换为我们已经能解决的用较快的时钟来采集较慢的时钟。如下图:

将信号 adat 扩宽为 3 倍 aclk 后,脉冲信号成为了一个较慢的信号,而目的时钟成为了一个较快的时钟,这样我们就可以使用 2 级同步器的方法来对被 CDC 信号进行同步。

img

需要注意的是,这一方法具有一定的局限性:

  1. 若需要同步的两个脉冲信号距离很近,则第一个脉冲信号的扩宽可能会覆盖第二个脉冲信号,从而导致第二个脉冲信号的漏采;
  2. 脉冲信号的扩宽是以目的时钟为参考的,但是若不知道目的时钟的频率,则该方法可能失效,也就是说该方法不具备普遍性。

为了探寻一种具备普遍性的方法,我们可以使用握手法来进行 CDC。握手法的本质是负反馈,通俗来讲,就是我先将被 CDC 信号展宽,展宽后将其同步到目的时钟域,在目的时钟域生成指示信号,该指示信号用来指示此时信号已经被目的时钟域接收,然后将指示信号反馈到源时钟域(反馈过程),源时钟域接收到这个反馈信号后将被 CDC 信号拉低,从而确定了展宽长度,也通过“发送 – 反馈 – 操作”这一握手过程完成了一次 CDC 传输。

img

上图是典型的握手过程来进行 CDC:

  1. 源时钟域 aclk 下的信号 adt1 信号是要进行 CDC 的信号;
  2. adt1 先是在源时钟域 aclk 下被展宽,然后通过两级同步器被同步到目的时钟域 bclk 下,分别为 bq1_dat,bq2_dat;
  3. bq2_dat 作为指示信号(反馈信号,也可以通过 bq1_dat 和 bq2_dat 来生成新的指示信号)又被反馈到了目的时钟域 aclk 下,并进行同步,分别为 aq1_dat,aq2_dat;
  4. aq2_dat 的拉高则说明反馈信号的同步完成,此时可以将 adt1 拉低(结束展宽过程);
  5. adt1 拉低(结束展宽过程)后表示一次 CDC 操作结束。

跨时钟域(CDC)设计方法之多 bit 信号篇

孤独的单刀 于 2022-05-13 15:38:13 修改

1、跨时钟域处理方法的分类

信号的跨时钟域传输可能会引入亚稳态问题,那么就需要想办法对其进行处理,从而降低亚稳态发生的概率(即提高 MTBF)。

跨时钟域处理方法可以分为两个大类:单 Bit 信号跨时钟域处理、多 Bit 信号跨时钟域处理。分类的原因是多 bit 信号的传递不光只有亚稳态这一个问题,还可能会因为多个信号之间由于工艺、PCB 布局等因素导致的信号传输延时(skew)的存在,从而导致信号被漏采或者错采。

2、合并多个控制信号

在下面的例子中,数据的读取需要载入信号 load 和使能信号 en 同时置位,但是这两个信号之间存在一个小偏差(skew),在分别进行单 bit 的跨时钟域同步时,由于小 skew 的存在,导致这两个信号在被同步到目的时钟域后有一个时钟周期的偏差,而这个偏差直接导致数据无法被读取。

img

现在不妨问自己:这两个信号是否都必须传递?实际上不是必须的,应用中我们可以把两个信号合并为一个控制信号,这样就将多 bit 信号的跨时钟域传递转换成了单 bit 信号的跨时钟域传递。如下:

img

3、格雷码

在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码(Gray Code),另外由于最大数与最小数之间也仅一位数不同,即 “首尾相连”,因此又称循环码或反射码。

下表给出了 4bit 自然二进制码、4bit 典型格雷码(无特殊说明,典型格雷码即格雷码)与 4bit 十进制整数的对照:

十进制数4 位自然二进制码4 位典型格雷码
000000000
100010001
200100011
300110010
401000110
501010111
601100101
701110100
810001100
910011101
1010101111
1110111110
1211001010
1311011011
1411101001
1511111000

从格雷码的性质,我们可以发现其虽然是多 bit 信号的 CDC,但是由于每次只有相邻的一位不同,实际上就是 1bit 信号的 CDC,好家伙,又把问题转化成了我们熟悉的单 bit 信号的跨时钟域问题。

但是格雷码的应用也有局限:

  • 格雷码只有相邻的两位之间存在只有 1bit 的改变,这就意味着数据的改变必须要连续(相邻),也就是说其只适用连续变化的数据,例如计数器,这无疑限制了格雷码的应用场景;
  • 格雷码传递的数据个数必须为 2 的 N 次幂 ---- 由于最大数与最小数之间也仅一位数不同,即 “首尾相连”,因此格雷码又称循环码。若格雷码个数不为 2 的 N 次幂,则无法实现 “首尾相连”,即最后一位传递到初始位时,就不止变化 1bit 了,这无疑违背了我们使用格雷码的初衷。

4、握手法

握手法的本质是负反馈,通俗来讲,就是先将被 CDC 信号展宽,展宽后将其同步到目的时钟域,在目的时钟域生成指示信号,该指示信号用来指示此时信号已经被目的时钟域接收,然后将指示信号反馈到源时钟域(反馈过程),源时钟域接收到这个反馈信号后将被 CDC 信号拉低,从而确定了展宽长度,也通过“发送 – 反馈 – 操作”这一握手过程完成了一次 CDC 传输。

img

上图是典型的握手过程来进行 CDC:

  • 源时钟域 aclk 下的信号 adt1 信号是要进行 CDC 的信号;
  • adt1 先是在源时钟域 aclk 下被展宽,然后通过两级同步器被同步到目的时钟域 bclk 下,分别为 bq1_dat,bq2_dat;
  • bq2_dat 作为指示信号(反馈信号,也可以通过 bq1_dat 和 bq2_dat 来生成新的指示信号)又被反馈到了目的时钟域 aclk 下,并进行同步,分别为 aq1_dat,aq2_dat;
  • aq2_dat 的拉高则说明反馈信号的同步完成,此时可以将 adt1 拉低(结束展宽过程);
  • adt1 拉低(结束展宽过程)后表示一次 CDC 操作结束。

握手法是一种很保险的方法,但是缺点也很明显:需要的时序开销很大。

5、异步 FIFO

毫无疑问异步 FIFO 是多 bit 信号来跨时钟域处理的最好的、也是最常用的方法。异步 FIFO 通过先进先出的缓存机制,将两端数据的读写都控制在各自的时钟域内,时钟域之间通过指针 + 格雷码形式来指示数据的空、满。

6、DMUX 同步器

对于多 bit 的 data 信号,还可以使用使能技术,也就是通过一个使能信号来判断 data 信号是否已经稳定,当使能信号有效的时候说明 data 处于稳定状态,在这种情况下终点寄存器才对信号进行采样,可以保证没有 setup/hold 违例。而使能信号一般使用 double FF 的方法来进行同步。下面是 DMUX 的同步示意图:

img


FPGA 同步复位、异步复位、异步复位同步释放

孤独的单刀 已于 2022-04-21 22:18:23 修改

1、复位的重要性

数字电路中寄存器和 RAM 在上电之后默认的状态和数据是不确定的,如果有复位,我们可以把寄存器复位到初始状态 0,RAM 的数据可以通过复位来触发 RAM 初始化到全 0。那可能很多人会问为什么是全 0 呢?其实一般逻辑起始都是从 0 开始变化的,这是根据设计的需要设定的一个值,如果设计需要寄存器上电复位为 1,也是可以的。还有一种情况是逻辑进入了错误的状态,通过复位可以把所有的逻辑状态恢复到初始值,如果没有复位,那么逻辑可能永远运行在错误的状态。

因此复位功能是很重要的一个功能。数字电路的复位通常可分为:同步复位与异步复位

2、同步复位

同步复位:同步复位指的是当时钟上升沿检测(有效沿)到复位信号,执行复位操作,有效的时钟沿是前提。

Verilog 代码:

//*******************同步复位模块**************************
//----------- 端口定义 -------------------------------
module	rst_test (
input clk ,	// 工作时钟
input rst_n ,	// 复位,低电平有效
input in ,	// 输入信号
output reg out			// 输出信号
);
//----------- 输出模块 -------------------------------
always@(posedge clk) begin
if (!rst_n)
out <= 1'b0;		// 复位将输出置零
else
out <= in;			// 其他时候将输入赋值给输出
end
endmodule

使用 Quartus II 综合出的 RTL 如下:

RTL 图

可以看到,生成的触发器并没有复位置位端,而是生成了一个选择器,同步复位信号 rst_n 用作了选择器的使能,从而实现复位清零的作用。

用如下 Testbench 进行仿真(复位时间不足一个时钟周期、复位时间超过一个时钟周期、一个高频复位毛刺):

//------------------------------------------------
//-- 同步复位仿真
//------------------------------------------------
`timescale 1ns/1ns	// 时间单位 / 精度
//------------< 模块及端口声明 >----------------------------------------
module tb_rst_test ();
reg clk ;
reg rst_n ;
reg in ;
wire out ;
//------------< 例化被测试模块 >----------------------------------------
rst_test	rst_test_inst (
.clk	(clk) ,
.rst_n	(rst_n) ,
.in		(in) ,
.out (out)
);
//------------< 设置初始测试条件 >----------------------------------------
initial begin
clk = 1'b0;
rst_n <= 1'b0;
in <= 1'b1;
#5
rst_n <= 1'b1;
#6
rst_n <= 1'b0;
#18
rst_n <= 1'b1;
#39
rst_n <= 1'b0;
#21
rst_n <= 1'b1;
#25
rst_n <= 1'b0;
#3
rst_n <= 1'b1;
end
//------------< 设置时钟 >----------------------------------------------
always #10 clk = ~clk;		// 系统时钟周期 20ns
endmodule

仿真结果如下:

仿真结果

从仿真结果可以看到:

  • 第 1 个复位时间不足一个时钟周期,导致复位不成功
  • 第 2 个复位时间超过一个时钟周期,复位成功
  • 最后一个复位信号上的高频毛刺信号没有对系统造成误复位
  • 输出信号在第一个时钟上升沿到来之前是未知状态

同步复位电路的优点

  • 有利于仿真器的仿真
  • 基本上没有亚稳态问题
  • 可以使所设计的系统成为 100% 的同步时序电路,有利于时序分析
  • 由于只在时钟有效电平到来时才有效,所以可以滤除高于时钟频率的复位毛刺

同步复位电路的缺点

  • 复位信号的有效时长必须大于时钟周期,才能真正被系统识别并完成复位任务。同时还要考虑延时因素
  • 大多数的 FPGA 的 DFF 都只有异步复位端口,采用同步复位的话,综合器就会在寄存器的数据输入端口插入组合逻辑,这样会耗费逻辑资源

3、异步复位

异步复位:异步复位指的是无论时钟沿是否到来,只要复位信号有效,就对系统进行复位。当时钟上升沿检测到复位信号,执行复位操作。

Verilog 代码:

//*******************同步复位模块**************************
//----------- 端口定义 -------------------------------
module	rst_test (
input clk ,	// 工作时钟
input rst_n ,	// 复位,低电平有效
input in ,	// 输入信号
output reg out			// 输出信号
);
//----------- 输出模块 -------------------------------
always@(posedge clk or negedge rst_n) begin
if (!rst_n)
out <= 1'b0;		// 复位将输出置零
else
out <= in;			// 其他时候将输入赋值给输出
end
endmodule

使用 Quartus II 综合出的 RTL 如下:

RTL 图

可以看到,异步复位信号 rst_n 直接接入了触发器的异步复位端,从而实现复位清零的作用。

依然使用之前用的 Testbench 进行仿真,结果如下:

仿真结果

从仿真结果可以看到:

  • 只要复位信号被置为低电平,就执行复位操作,与时钟无关
  • 高频毛刺信号会对系统造成误复位
  • 在不考虑亚稳态的前提下,复位时间没有要求

a、在复位信号释放 (release) 的时候容易出现问题。具体就是说:倘若复位释放时恰恰在时钟有效沿附近,就很容易使寄存器输出出现亚稳态,从而导致亚稳态。

b、复位信号容易受到毛刺的影响

异步复位电路的优点

  • 大多数目标器件库的 dff 都有异步复位端口,因此采用异步复位可以节省资源
  • 设计相对简单,异步复位信号识别方便,而且可以很方便的使用 FPGA 的全局复位端口 GSR

异步复位电路的缺点

  • 复位信号容易受到毛刺的影响
  • 因为是异步逻辑,无法避免地存在亚稳态问题

关于异步复位还需要考虑:

**恢复时间(Recovery Time)**是指异步控制信号(如寄存器的异步清除和置位控制信号)在 “下个时钟沿” 来临之前变无效的最小时间长度。这个时间的意义是,如果保证不了这个最小恢复时间,也就是说这个异步控制信号的解除与 “下个时钟沿” 离得太近(但在这个时钟沿之前),没有给寄存器留有足够时间来恢复至正常状态,那么就不能保证 “下个时钟沿” 能正常作用,也就是说这个 “时钟沿” 可能会失效。

**去除时间(Removal)**是指异步控制信号(如寄存器的异步清除和置位控制信号)在 “有效时钟沿” 之后变无效的最小时间长度。这个时间的意义是,如果保证不了这个去除时间,也就是说这个异步控制信号的解除与 “有效时钟沿” 离得太近(但在这个时钟沿之后),那么就不能保证有效地屏蔽这个 “时钟沿”,也就是说这个 “时钟沿” 可能会起作用。

换句话来说,如果你想让某个时钟沿起作用,那么你就应该在 “恢复时间” 之前是异步控制信号变无效,如果你想让某个时钟沿不起作用,那么你就应该在 “去除时间” 过后使控制信号变无效。如果你的控制信号在这两种情况之间,那么就没法确定时钟沿是否起作用或不起作用了,也就是说可能会造成寄存器处于不确定的状态。而这些情况是应该避免的。所以恢复时间和去除时间是应该遵守的。

4、异步复位、同步释放

结合两种复位的优点,可以使用异步复位、同步释放设计,Verilog 代码如下:

//*******************同步复位模块**************************
//----------- 端口定义 -------------------------------
module	rst_test (
input clk ,		// 工作时钟
input rst_n ,		// 复位,低电平有效
input in ,		// 输入信号
output reg out				// 输出信号
);
//-----------reg 定义 -------------------------------
reg arst_n_r;
reg arst_n;
//----------- 复位信号同步模块 -------------------------------
always@(posedge clk or negedge rst_n) begin
if (!rst_n) begin
arst_n_r <= 1'b0	;	// 复位将输出置零
arst_n <= 1'b0		;	// 复位将输出置零
end
else begin
arst_n_r <= 1'b1	;	// 跟接 rst_n 是一样的,都是逻辑 1
arst_n <= arst_n_r	;
end
end
//----------- 输出模块 -------------------------------
always@(posedge clk or negedge arst_n) begin
if (!arst_n)
out <= 1'b0;		 // 复位将输出置零
else
out <= in;			 // 其他时候将输入赋值给输出
end
endmodule

综合出来的 RTL 视图:

RTL 图

实际的电路图如下:

电路图

复位信号 rst_sync_n 由高拉低时实现 y 寄存器的异步复位。同步释放,这个是关键,即当复位信号 rst_async_n 撤除时(由低拉高),由于双缓冲电路(双触发器)的作用,rst_sync_n 不会随着 rst_async_n 的撤除而撤除。假设 rst_async_n 撤除时发生在 clk 上升沿,如果不加此电路则可能发生亚稳态事件,但是加上此电路以后,假设第一级 D 触发器 clk 上升沿时 rst_async_n 正好撤除,(第一个 DFF 此时是处于亚稳态的;假设此时识别到高电平;若是识别到低电平,则增加一个 Delay)则 DFF1 输出高电平,此时第二级触发器也会更新输出,但是输出值为前一级触发器 clk 来之前时的 Q1 输出状态,显然 Q1 之前为低电平,所以第二级触发器输出保持复位低电平,直到下一个 clk 来之后,才随着变为高电平,即同步释放。

使用如下 Testbench 进行仿真:

//------------------------------------------------
//-- 同步复位仿真
//------------------------------------------------
`timescale 1ns/1ns	// 时间单位 / 精度
//------------< 模块及端口声明 >----------------------------------------
module tb_rst_test ();
reg clk ;
reg rst_n ;
reg in ;
wire out ;
//------------< 例化被测试模块 >----------------------------------------
rst_test	rst_test_inst (
.clk	(clk) ,
.rst_n	(rst_n) ,
.in		(in) ,
.out (out)
);
//------------< 设置初始测试条件 >----------------------------------------
initial begin
clk = 1'b0;
rst_n <= 1'b0;
in <= 1'b1;
#25
rst_n <= 1'b1;
#6
rst_n <= 1'b1;
#18
rst_n <= 1'b1;
#39
rst_n <= 1'b0;
#21
rst_n <= 1'b1;
end
//------------< 设置时钟 >----------------------------------------------
always #10 clk = ~clk;		// 系统时钟周期 20ns
endmodule

仿真结果如下:

仿真结果

可以看到:复位是异步进行的,一旦复位信号为低电平,则输出复位,而复位的撤除则被同步到了时钟域下。如此一来,既解决了同步复位的资源消耗问题,也解决了异步复位的亚稳态问题。其根本思想,也是将异步信号同步化。

5、参考

正点原子逻辑设计指南 – 正点原子


FPGA 的复位设计要醒目点啦

孤独的单刀 已于 2022-06-16 23:25:03 修改

写在前面

本文内容主要来自 Xilinx 白皮书《wp272_Get Smart About Reset》,只是直译了 Xilinx 的白皮书。

1、为什么要设计复位?

首先回想一下,在平常的设计中我们是不是经常采用同步复位或者异步复位的写法,这一写法似乎都已经形成了肌肉记忆 ---- 每次我们写 always 块的时候总是会对所有的寄存器写一个复位赋初值的语句。

这样设计的目的是什么?似乎是为了给寄存器一个初值,避免仿真不定态或初始化操作错误。又似乎是为了在调试时能方便地使用按键进行复位(最常用的全局复位)。这么一看复位似乎是蛮重要的。

2、复位是否有必要?

似乎在平常的设计中,多数会使用异步复位的方式,异步复位由于是异步信号,所以不可避免地引入了亚稳态的可能,这一可能性随着时钟频率的提高而增加。好像在平常的设计与使用中,异步复位电路也不会引发什么问题。这是因为随着器件工艺的提升,现在器件的上升时间在 0.0x 纳秒级,而一般设计的时钟周期可能在 100~200M。只要复位的释放不是刚好在这 0.0x 纳秒内就不会引发亚问题问题,显然这个概率极小(比例 --0.0x:10),基本可以说是 99.99 不会有问题。但是著名的墨菲定律高速我们:再小概率的事情都会发生。所以不管怎样这种事情我们都应该要想办法避免。

再来看我们使用复位的主要目的:为了给寄存器一个初始值,从而避免仿真或使用错误。然而实际上,Xilinx 的 FPGA 的内部资源(触发器和 RAM)等都会在上电后默认赋初值,一般是 0,或者可以在定义寄存器时手动赋值,如:

reg[1:0] test = 2'b01;// 定义时即赋初值

这么看的话仅仅为了赋初值而存在的赋值就没有意义的。数据链路上有初值就够了,因为后来的数据总会冲走之前的数据,数据仍然能稳步传递。但是控制链路就一定需要被复位后一定要恢复到初始状态,不然会 “乱跑‘从而导致代码运行异常。其中最经典的例子就是状态机了,显然,如果状态机的状态模块没有复位的话,那么可能在出现异常后永远无法恢复到正常状态了。

最后,复位所使用的资源远超你的想象:

复位资源

若频繁的使用全局复位,则势必将复位信号变成一个高扇出的信号,这对时序收敛无疑是一个巨大的挑战。

3、应该怎样设计复位?

说了这么多,那到底要怎么设计复位?

同步 or 异步?

FPGA 同步复位、异步复位、异步复位同步释放 一文中探讨过同步复位与异步复位的特点。

同步复位

  • 有利于仿真
  • 由于只在时钟有效电平到来时才有效,所以可以滤除高于时钟频率的复位毛刺,没有亚稳态问题
  • 可以使所设计的系统成为 100% 的同步时序电路,有利于时序分析
  • 复位信号的有效时长必须大于时钟周期,才能真正被系统识别并完成复位任务。同时还要考虑延时因素
  • 大多数的 FPGA 的 DFF 都只有异步复位端口,采用同步复位的话,综合器就会在寄存器的数据输入端口插入组合逻辑,这样会耗费逻辑资源

异步复位

  • 大多数目标器件库的 dff 都有异步复位端口,因此采用异步复位可以节省资源
  • 设计相对简单,异步复位信号识别方便,而且可以很方便的使用 FPGA 的全局复位端口 GSR
  • 复位信号容易受到毛刺的影响且容易存在亚稳态问题

建议使用同步复位的方式,若一定要使用异步复位的话,则建议使用异步复位、同步释放的方法。

高 or 低?

选择高还是低,需要根据具体的电平标准、器件结构来选择,并不是一概而论低电平有效的好或者高电平有效的好。简单经验:Altera 的用低电平复位,Xilinx 的用高电平复位。

总结

复位信号能不用就不要用,需要特定初值的可以在定义寄存器时赋值。

如果一定需要则使用异步复位、同步释放的方法,并将复位信号局部化,避免高扇出。


FPGA 设计的 “打拍(寄存)” 和 “亚稳态” 到底是什么?

孤独的单刀 已于 2022-05-13 15:41:33 修改

1、前言

可能很多 FPGA 初学者在刚开始学习 FPGA 设计的时候(当然也包括我自己),经常听到类似于” 这个信号需要打一拍、打两拍(寄存),以防止亚稳态问题的产生 “这种话,但是对这个打拍和亚稳态问题还是一知半解,接下来结合一些资料谈下自己的理解。

2、触发器的建立时间和保持时间

时序电路的基础是触发器(FF、Flip-Flop),触发器正常工作需要满足建立时间和保持时间的时序要求。

触发器时序

建立时间(Tsu:set up time)

是指在触发器的时钟信号上升沿到来以前,数据稳定不变的时间,如果建立时间不够,数据将不能在这个时钟上升沿被稳定的打入触发器,Tsu 就是指这个最小的稳定时间

保持时间(Th:hold time)

是指在触发器的时钟信号上升沿到来以后,数据稳定不变的时间,如果保持时间不够,数据同样不能被稳定的打入触发器,Th 就是指这个最小的保持时间

3、亚稳态

亚稳态 (Metastability):如果数据传输中不满足触发器的 TsuTh 不满足,就可能产生亚稳态,此时触发器输出端 Q 在有效时钟沿之后比较长的一段时间处于不确定的状态,在这段时间里 Q 端在 0 和 1 之间处于振荡状态,而不是等于数据输入端 D 的值。这段时间称为决断时间 Tmet(resolution time)。经过 Tmet 之后 Q 端将稳定到 0 或 1 上,但是稳定到 0 或者 1,是随机的,与输入没有必然的关系。

亚稳态振荡时间 Tmet 关系到后级寄存器的采集稳定问题,Tmet 影响因素包括:器件 的生产工艺、温度、环境以及寄存器采集到亚稳态里稳定态的时刻等。甚至某些特定条 件,如干扰、辐射等都会造成 Tmet 增长。

亚稳态示意图

只要系统中有异步元件,亚稳态就是无法避免的,亚稳态主要发生在异步信号检测、跨时钟域信号传输以及复位电路等常用设计中。由于产生亚稳态后,寄存器 Q 端输出在稳定下来之前可能是毛刺、振荡、固定的某一电压值。在信号传输中产生亚稳态就会导致与其相连其他数字部件将其作出不同的判断,有的判断到 “1” 有的判断到 “0”,有的也进入了亚稳态,数字部件就会逻辑混乱。

4、如何防止亚稳态

首先,在同步系统中,输入信号总是系统时钟同步,能够达到寄存器的时序要求,所以亚稳态肯定不会发生。在异步系统的信号输出过程中,如果无法满足建立时间和保持时间的要求则会发生亚稳态。

预防亚稳态的方法就是将输入信号(单 bit 信号)打拍,也就是在要使用的时钟域下,将信号寄存。

打拍示意图

rx 是相对于时钟域 sys_clk 的异步信号,rx_reg1rx_reg2 分别是 rx 在时钟域 sys_clk 打一拍(寄存一次、可以理解为延迟一个时钟周期 )、打两拍(寄存一两次、可以理解为延迟两个时钟周期)的信号。可以看到 rx_reg1 可能还存在低概率的亚稳态现象,当然 rx_reg2 虽然在示意图里是稳定的,不过实际过程中也仍然存在亚稳态发生的概率。

单比特信号从慢速时钟域同步到快速时钟域需要使用打两拍的方式消除亚稳态。 第一级寄存器产生亚稳态并经过自身后可以稳定输出的概率为 70%~80% 左右,第二级寄存器可以稳定输出的概率为 99% 左右,后面再多加寄存器的级数改善效果就不明显了,所以数据进来后一般选择打两拍即可。这里注意,该方法仅仅适用单比特信号从慢速时钟域同步到快速时钟域,单比特信号从快速时钟域同步到慢速时钟域还仅仅使用打两拍的方式则会漏采数据。


同步 FIFO 的两种 Verilog 设计方法(计数器法、高位扩展法)

孤独的单刀 已于 2024-04-10 11:01:15 修改

1、什么是 FIFO

FIFO 是一种先进先出的数据缓存器,在逻辑设计里面用的非常多,FIFO 设计可以说是逻辑设计人员必须掌握的常识性设计。FIFO 一般用在隔离两边读写带宽不一致,或者位宽不一样的地方。 在 FPGA 设计,使用 FIFO 一般有两个方法,第一个方法是直接调用官方的 FIFO IP,另外一个方法是自己设计 FIFO 控制逻辑。当然我们学会设计 FIFO,并不一定是真的需要自己造轮子,只是说作为从业人员我们要了解相关的设计方法,毕竟自己造的轮子不一定能跑不是。

FIFO 包括同步 FIFO 和异步 FIFO 两种,同步 FIFO 有一个时钟信号,读和写逻辑全部使用这一个时钟信号,异步 FIFO 有两个时钟信号,读和写逻辑用的各种的读写时钟。 FIFO 与普通存储器 RAM 的区别是没有外部读写地址线,使用起来非常简单,但缺点就是只能顺序写入数据,顺序的读出数据,其数据地址由内部读写指针自动加 1 完成,不能像普通存储器那样可以由地址线决定读取或写入某个指定的地址。 FIFO 本质上是由 RAM 加读写控制逻辑构成的一种先进先出的数据缓冲器。

FIFO 结构

FIFO 的常见参数:

  • FIFO 的宽度:即 FIFO 一次读写操作的数据位
  • FIFO 的深度:指的是 FIFO 可以存储多少个 N 位的数据(如果宽度为 N)
  • 满标志:FIFO 已满或将要满时由 FIFO 的状态送出的一个信号,以阻止 FIFO 的写操作继续向 FIFO 中写数据而造成溢出(overflow)
  • 空标志:FIFO 已空或将要空时由 FIFO 的状态送出的一个信号,以阻止 FIFO 的读操作继续从 FIFO 中读出数据而造成无效数据的读出(underflow)
  • 读时钟:读操作所遵循的时钟,在每个时钟沿来临时读数据(同步 FIFO 中与写时钟一致)
  • 写时钟:写操作所遵循的时钟,在每个时钟沿来临时写数据(同步 FIFO 中与读时钟一致)

2、同步 FIFO 的设计方法

FIFO 读写指针(读写指针就是读写地址)的工作原理:

  • 写指针:总是指向下一个将要被写入的单元,复位时,指向第 1 个单元 (编号为 0)
  • 读指针:总是指向当前要被读出的数据,复位时,指向第 1 个单元 (编号为 0) FIFO 的 “空”/“满” 检测

FIFO 设计的关键:产生可靠的 FIFO 读写指针和生成 FIFO “空”/“满” 状态标志。 当读写指针相等时,表明 FIFO 为空,这种情况发生在复位操作时,或者当读指针读出 FIFO 中最后一个字后,追赶上了写指针时,如下图所示:

FIFO 空状态

当读写指针再次相等时,表明 FIFO 为满,这种情况发生在,当写指针转了一圈,折回来 (wrapped around) 又追上了读指针,如下图:

FIFO 满状态

可见读写指针可以在读写使能有效时,每时钟周期 + 1,而如何产生可靠的 “空”/“满” 信号则成了同步 FIFO 设计的重点。下面有两种解决方法:

2.1、计数器法

构建一个计数器,该计数器 (fifo_cnt) 用于指示当前 FIFO 中数据的个数:

  • 复位时,该计数器为 0,FIFO 中的数据个数为 0
  • 当读写使能信号均有效时,说明又读又写,计数器不变,FIFO 中的数据个数无变化
  • 当写使能有效且 full=0,则 fifo_cnt +1;表示写操作且 FIFO 未满时候,FIFO 中的数据个数增加了 1
  • 当读使能有效且 empty=0,则 fifo_cnt -1;表示读操作且 FIFO 未空时候,FIFO 中的数据个数减少了 1
  • fifo_cnt =0 的时候,表示 FIFO 空,需要设置 empty=1fifo_cnt = fifo 的深度 的时候,表示 FIFO 现在已经满,需要设置 full=1
2.1.1、Verilog 代码

解决好如上所述的几个问题后,可以很容易的贴出 Verilog 代码:

// 计数器法实现同步 FIFO
module	sync_fifo_cnt
#(
parameter DATA_WIDTH = 'd8 ,							//FIFO 位宽
parameter DATA_DEPTH = 'd16 							//FIFO 深度
)
(
input									clk		,		// 系统时钟
input									rst_n	,  // 低电平有效的复位信号
input	[DATA_WIDTH-1:0]				data_in	,  // 写入的数据
input									rd_en	,  // 读使能信号,高电平有效
input									wr_en	,  // 写使能信号,高电平有效
output	reg	[DATA_WIDTH-1:0]			data_out,	 // 输出的数据
output									empty	,	 // 空标志,高电平表示当前 FIFO 已被写满
output									full	,  // 满标志,高电平表示当前 FIFO 已被读空
output	reg	[$clog2 (DATA_DEPTH) : 0]	fifo_cnt		//$clog2 是以 2 为底取对数
);
//reg define
reg [DATA_WIDTH - 1 : 0] fifo_buffer [DATA_DEPTH - 1 : 0];	// 用二维数组实现 RAM
reg [$clog2 (DATA_DEPTH) - 1 : 0]	wr_addr;				// 写地址
reg [$clog2 (DATA_DEPTH) - 1 : 0]	rd_addr;				// 读地址
// 读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
rd_addr <= 0;
else if (!empty && rd_en) begin							// 读使能有效且非空
rd_addr <= rd_addr + 1'd1;
data_out <= fifo_buffer [rd_addr];
end
end
// 写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wr_addr <= 0;
else if (!full && wr_en) begin							// 写使能有效且非满
wr_addr <= wr_addr + 1'd1;
fifo_buffer [wr_addr]<=data_in;
end
end
// 更新计数器
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
fifo_cnt <= 0;
else begin
case ({wr_en,rd_en})									// 拼接读写使能信号进行判断
2'b00:fifo_cnt <= fifo_cnt;						// 不读不写
2'b01:	  			// 仅仅读
if (fifo_cnt != 0)				 			//fifo 没有被读空
fifo_cnt <= fifo_cnt - 1'b1; 			//fifo 个数 - 1
2'b10: 			// 仅仅写
if (fifo_cnt != DATA_DEPTH) 			//fifo 没有被写满
fifo_cnt <= fifo_cnt + 1'b1; 			//fifo 个数 + 1
2'b11:fifo_cnt <= fifo_cnt;	 			// 读写同时
default:;
endcase
end
end
// 依据计数器状态更新指示信号
// 依据不同阈值还可以设计半空、半满 、几乎空、几乎满
assign full = (fifo_cnt == DATA_DEPTH) ? 1'b1 : 1'b0;		// 满信号
assign empty = (fifo_cnt == 0)? 1'b1 : 1'b0;				// 空信号
endmodule
2.1.2、Testbench 及仿真结果

接下来编写脚本对源码进行测试:

  • 例化 1 个深度为 8,位宽为 8 的同步 FIFO
  • 先对 FIFO 进行写操作,直到其写满,写入的数据为随机数据
  • 然后对 FIFO 进行读操作,直到其读空
  • 然后对 FIFO 写入 4 个随机数据后,同时对其进行读写操作
`timescale 1ns/1ns	// 时间单位 / 精度
//------------< 模块及端口声明 >----------------------------------------
module tb_sync_fifo_cnt ();
parameter DATA_WIDTH = 8 ;			//FIFO 位宽
parameter DATA_DEPTH = 8 ;			//FIFO 深度
reg									clk		;
reg									rst_n	;
reg		[DATA_WIDTH-1:0]			data_in	;
reg									rd_en	;
reg									wr_en	;
wire	[DATA_WIDTH-1:0]			data_out;
wire								empty	;
wire								full	;
wire	[$clog2 (DATA_DEPTH) : 0]	fifo_cnt;
//------------< 例化被测试模块 >----------------------------------------
sync_fifo_cnt
#(
.DATA_WIDTH	(DATA_WIDTH),			//FIFO 位宽
.DATA_DEPTH	(DATA_DEPTH)			//FIFO 深度
)
sync_fifo_cnt_inst (
.clk		(clk		),
.rst_n		(rst_n		),
.data_in	(data_in	),
.rd_en		(rd_en		),
.wr_en		(wr_en		),
.data_out	(data_out	),
.empty		(empty		),
.full		(full		),
.fifo_cnt	(fifo_cnt	)
);
//------------< 设置初始测试条件 >----------------------------------------
initial begin
clk = 1'b0;							// 初始时钟为 0
rst_n <= 1'b0;						// 初始复位
data_in <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
// 重复 8 次写操作,让 FIFO 写满
repeat (8) begin
@(negedge clk) begin
rst_n <= 1'b1;
wr_en <= 1'b1;
data_in <= $random;			// 生成 8 位随机数
end
end
// 重复 8 次读操作,让 FIFO 读空
repeat (8) begin
@(negedge clk) begin
wr_en <= 1'b0;
rd_en <= 1'd1;
end
end
// 重复 4 次写操作,写入 4 个随机数据
repeat (4) begin
@(negedge clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
rd_en <= 1'b0;
end
end
// 持续同时对 FIFO 读写,写入数据为随机数据
forever begin
@(negedge clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
rd_en <= 1'b1;
end
end
end
//------------< 设置时钟 >----------------------------------------------
always #10 clk = ~clk;			// 系统时钟周期 20ns
endmodule

使用 modelsim 进行仿真,仿真结果如下:

仿真结果

可以看到,仿真结果与预期效果一致(详见图注)。

2.2、高位扩展法

举例在深度为 8 的 FIFO 中,需要 3bit 的读写指针来分别指示读写地址 3’b000-3’b111 这 8 个地址。若将地址指针扩展 1bit,则变成 4bit 的地址,而地址表示区间则变成了 4’b0000-4’b1111。假设不看最高位的话,后面 3 位的表示区间仍然是 3’b000-3’b111,也就意味着最高位可以拿来作为指示位。

  • 当最高位不同,且其他位相同,则表示读指针或者写指针多跑了一圈,而显然不会让读指针多跑一圈(多跑一圈读啥?),所以可能出现的情况只能是写指针多跑了一圈,与就意味着 FIFO 被写满了
  • 当最高位相同,且其他位相同,则表示读指针追到了写指针或者写指针追到了读指针,而显然不会让写指针追读指针(这种情况只能是写指针超过读指针一圈),所以可能出现的情况只能是读指针追到了写指针,也就意味着 FIFO 被读空了
2.2.1、Verilog 代码

解决好如上所述的几个问题后,可以很容易的贴出 Verilog 代码:

module	sync_fifo_ptr
#(
parameter DATA_WIDTH = 'd8 ,								//FIFO 位宽
parameter DATA_DEPTH = 'd16 								//FIFO 深度
)
(
input										clk		,		// 系统时钟
input										rst_n	,  // 低电平有效的复位信号
input	[DATA_WIDTH-1:0]					data_in	,  // 写入的数据
input										rd_en	,  // 读使能信号,高电平有效
input										wr_en	,  // 写使能信号,高电平有效
output	reg	[DATA_WIDTH-1:0]				data_out,	 // 输出的数据
output										empty	,	 // 空标志,高电平表示当前 FIFO 已被写满
output										full		 // 满标志,高电平表示当前 FIFO 已被读空
);
//reg define
// 用二维数组实现 RAM
reg [DATA_WIDTH - 1 : 0]			fifo_buffer [DATA_DEPTH - 1 : 0];
reg [$clog2 (DATA_DEPTH) : 0]		wr_ptr;						// 写地址指针,位宽多一位
reg [$clog2 (DATA_DEPTH) : 0]		rd_ptr;						// 读地址指针,位宽多一位
//wire define
wire [$clog2 (DATA_DEPTH) - 1 : 0]	wr_ptr_true;				// 真实写地址指针
wire [$clog2 (DATA_DEPTH) - 1 : 0]	rd_ptr_true;				// 真实读地址指针
wire								wr_ptr_msb;					// 写地址指针地址最高位
wire								rd_ptr_msb;					// 读地址指针地址最高位
assign {wr_ptr_msb,wr_ptr_true} = wr_ptr;						// 将最高位与其他位拼接
assign {rd_ptr_msb,rd_ptr_true} = rd_ptr;						// 将最高位与其他位拼接
// 读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)
rd_ptr <= 'd0;
else if (rd_en && !empty) begin								// 读使能有效且非空
data_out <= fifo_buffer [rd_ptr_true];
rd_ptr <= rd_ptr + 1'd1;
end
end
// 写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (!full && wr_en) begin								// 写使能有效且非满
wr_ptr <= wr_ptr + 1'd1;
fifo_buffer [wr_ptr_true] <= data_in;
end
end
// 更新指示信号
// 当所有位相等时,读指针追到到了写指针,FIFO 被读空
assign	empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
// 当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO 被写满
assign	full = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;
endmodule
2.2.2、Testbench 及仿真结果

接下来编写脚本对源码进行测试(与 2.1.2 一致):

  • 例化 1 个深度为 8,位宽为 8 的同步 FIFO
  • 先对 FIFO 进行写操作,直到其写满,写入的数据为随机数据
  • 然后对 FIFO 进行读操作,直到其读空
  • 然后对 FIFO 写入 4 个随机数据后,同时对其进行读写操作
`timescale 1ns/1ns	// 时间单位 / 精度
//------------< 模块及端口声明 >----------------------------------------
module tb_sync_fifo_ptr ();
parameter DATA_WIDTH = 8 ;		//FIFO 位宽
parameter DATA_DEPTH = 8 ;		//FIFO 深度
reg									clk		;
reg									rst_n	;
reg		[DATA_WIDTH-1:0]			data_in	;
reg									rd_en	;
reg									wr_en	;
wire	[DATA_WIDTH-1:0]			data_out;
wire								empty	;
wire								full	;
//------------< 例化被测试模块 >----------------------------------------
sync_fifo_ptr
#(
.DATA_WIDTH	(DATA_WIDTH),			//FIFO 位宽
.DATA_DEPTH	(DATA_DEPTH)			//FIFO 深度
)
sync_fifo_ptr_inst (
.clk		(clk		),
.rst_n		(rst_n		),
.data_in	(data_in	),
.rd_en		(rd_en		),
.wr_en		(wr_en		),
.data_out	(data_out	),
.empty		(empty		),
.full		(full		)
);
//------------< 设置初始测试条件 >----------------------------------------
initial begin
clk = 1'b0;					// 初始时钟为 0
rst_n <= 1'b0;				// 初始复位
data_in <= 'd0;
wr_en <= 1'b0;
rd_en <= 1'b0;
// 重复 8 次写操作,让 FIFO 写满
repeat (8) begin
@(negedge clk) begin
rst_n <= 1'b1;
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
end
end
// 重复 8 次读操作,让 FIFO 读空
repeat (8) begin
@(negedge clk) begin
wr_en <= 1'b0;
rd_en <= 1'd1;
end
end
// 重复 4 次写操作,写入 4 个随机数据
repeat (4) begin
@(negedge clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
rd_en <= 1'b0;
end
end
// 持续同时对 FIFO 读写,写入数据为随机数据
forever begin
@(negedge clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
rd_en <= 1'b1;
end
end
end
//------------< 设置时钟 >----------------------------------------------
always #10 clk = ~clk;			// 系统时钟周期 20ns
endmodule

使用 modelsim 进行仿真,仿真结果如下:

仿真结果

可以看到,仿真结果与预期效果一致(详见图注)。

3、其他

  • 了解了同步 FIFO 的设计方法后,再进行异步 FIFO 的设计就比较简单了,主要就是解决好异步 FIFO 中的跨时钟域比较读写指针的问题。下一篇文章再对异步 FIFO 的设计进行分析验证

[FPGA] 异步 FIFO 的 Verilg 实现方法

孤独的单刀 已于 2023-06-05 02:36:04 修改

写在前面

在上篇文章:同步 FIFO 的两种 Verilog 设计方法(计数器法、高位扩展法) 中我们介绍了 FIFO 的基本概念,并对同步 FIFO 的两种实现方法进行了仿真验证。而异步 FIFO 因为读写时钟不一致,显然无法直接套用同步 FIFO 的实现方法,所以在本文我们将用 Verilog 实现异步 FIFO 的设计。

1、什么是异步 FIFO

异步 FIFO 有两个时钟信号,读和写接口分别采用不同时钟,这两个时钟可能时钟频率不同,也可能时钟相位不同,可能是同源时钟,也可能是不同源时钟。 在现代逻辑设计中,随着设计规模的不断扩大,一个系统中往往含有数个时钟,多时钟域带来的一个问题就是,如何设计异步时钟之间的接口电路。异步 FIFO 是这个问题的一种简便、快捷的解决方案,使用异步 FIFO 可以在两个不同时钟系统之间快速而方便地传输实时数据。

2、实现异步 FIFO 需要解决的关键点

首先我们直到 FIFO 的设计要点是**:读空信号如何产生?写满信号如何产生?**

在同步 FIFO 的设计中,我们提出了两种方法:计数器法和拓展高位指针的方法。那么这两种方法是否也适合异步 FIFO 的设计?

首先来看计数器法:计数器法的关键是使用一个计数器来指示 FIFO 中数据的个数,然后根据 FIFO 进行的读写操作来增加、减少或不变。在异步 FIFO(读写时钟可能相同,也可能不同,我们这里只讨论不同的情况)中,读写时钟不一致,那么这个计数器是属于哪个时钟的?或者说计数器在哪个时钟增加或者减少?显然这个问题不好解决。

那么接下来看第二个方法 –高位扩展法:将地址指针高位扩展 1 位,来作为指示位。通过对比读写指针的值(也就是比对读写指针的位置)来判断读空或者写满。乍一看这种方法好像行得通,当然了,实际上也行得通。只不过我们需要解决读写指针的跨时钟域问题。

异步 FIFO 的读写时钟不一致的话也就说明读、写指针是不同时钟域的信号,那么肯定无法直接对比。这里多说一句:可能有的朋友会说直接对比读写指针的值不就行了吗?干嘛还要做跨同步域处理呢(我是真的见过有人这么问)。首先我们需要直到的是读写指针是一个信号,而信号的值在这里是相对时域来说的。打个比方:在 10ns 的时候,读指针是 4;到了 20ns,读指针就变成了 8。同样的,写指针的值也是相对于时域来说的。既然两个指针相对的时域不一致,那么如何能直接对比?

2.1、读写指针的跨时钟域问题

现在我们直到了,要比较读写指针的值需要将其同步到同一时钟域。那么这个时钟域是写时钟域、读时钟域或者是第三方时钟域?

第三方时钟域:不难知道一个信号从一个时钟域同步到另一个时钟域(被同步时钟域)是需要时间的(这里仅考虑从满到快,也就是暂时不考虑漏采的问题),需要的时间取决于被同步时钟域的周期以及需要同步的个数。假设这个时间是 T,那么经过 T 时间后,由于读写时钟不一致,原来的读写时针增加(也可能不变)的量是不一致。比如说实际上读写时针都指向 4(且最高位相同),那么这种情况实际上是出现了读空的情况。但是同步到第三方时钟域后,可能写指针成了 6,而读指针变成了 8(读时钟比写时钟快),那么在这种情况下 FIFO 就不会报 “读空”,从而造成功能错乱。所以该种方法不可取。

同步到写时钟域:读指针同步到写时钟域需要时间 T,在经过 T 时间后,可能原来的读指针会增加或者不变,也就是说同步后的读指针一定是小于等于原来的读指针的。写指针也可能发生变化,但是写指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的写指针就是真实的写指针。

现在来进行写满的判断:也就是写指针超过了同步后的读指针一圈。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候写指针其实是没有超过读指针一圈的,也就是说这种情况是 “假写满”。那么 “假写满” 是一种错误的设计吗?答案是 NO。前面我们说过异步 FIFO 设计的关键点是产生合适的 “写满” 和 “读空” 信号,那么何谓 “合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设一个深度为 100 的 FIFO,在写到第 98 个数据的时候就报了 “写满”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了 FIFO 的深度我少用一点点就是的。事实上这还可以算是某种程度上的保守设计(安全)。

接着进行读空的判断:也就是同步后的读指针追上了写指针。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候读指针实际上是超过了写指针。这种情况意味着已经发生了 “读空”,却仍然有错误数据读出。所以这种情况就造成了 FIFO 的功能错误。

同步到读时钟域:写指针同步到读时钟域需要时间 T,在经过 T 时间后,可能原来的读指针会增加或者不变,也就是说同步后的写指针一定是小于等于原来的写指针的。读指针也可能发生变化,但是读指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的读指针就是真实的读指针。

现在来进行写满的判断:也就是同步后的写指针超过了读指针一圈。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候写指针已经超过了读指针不止一圈,这种情况意味着已经发生了 “写满”,却仍然数据被覆盖写入。所以这种情况就造成了 FIFO 的功能错误。

接着进行读空的判断:也就是读指针追上了同步后的指针。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候读指针其实还没有追上写指针,也就是说这种情况是 “假读空”。那么 “假读空” 是一种错误的设计吗?答案是 NO。前面我们说过异步 FIFO 设计的关键点是产生合适的 “写满” 和 “读空” 信号,那么何谓 “合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设某个 FIFO,在读到还剩 2 个数据的时候就报了 “读空”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了我先不读了,等数据多了再读就是的。事实上这还可以算是某种程度上的保守设计(安全)。

现在我们可以总结一下:

  • “写满” 的判断:需要将读指针同步到写时钟域,再与写指针判断
  • “读空” 的判断:需要将写指针同步到读时钟域,再与读指针判断

假读空示意如下:

假读空

假写满示意如下:

假写满

2.2、二进制码与格雷码

现在我们知道了判断 FIFO 的空、满需要将读写指针分别同步,而跨时钟域传输的一旦没处理好就会引起亚稳态问题,造成指针的值异常,从而引发 FIFO 的功能错误。那么应该如何将读写指针同步到对方的时钟域呢?答案是将二进制的指针转化成格雷码后再进行同步。

格雷码是一种非权重码,每次变化位数只有一位,这就有效的避免了在跨时钟域情况下亚稳态问题发生的概率。举个例子,二进制的 7(0111)跳转到 8(1000),4 位都会发生变化,所以发生亚稳态的概率就比较大。而格雷码的跳转就只有一位(从 0100–1100,仅第四位发生变化)会发生变化,有效地减小亚稳态发生的可能性。

四位二进制码从 0111 变为 1000 的过程中,这两个数虽然在数值上相邻,但它们的每个比特都将发生改变,采样的值就可能是任意的四位二进制数(发生亚稳态的情况),这会给空满标志的判断带来问题,如果错误触发空满标志还好,但如果在空满成立时没有触发,就会导致数据被覆盖掉或者重复读出;

如果使用格雷码,每次只改变一位信号,就不会出现上述的问题。例如,格雷码从 0001 递增到 0011 时,即便没有采集到变化后的 0011,也会采集到变化之前的 0001,这只会导致 “不该报空满而报了空满”,但并不会导致 “该报空满而未报” 的情况。详细来说,如果是读指针从 0001 递增到 0011,假设写时钟域采到的是 0001,那么也只是会报写满(因为写时钟域不知道读时钟域已经读到下一个地址了),从而停止写入,这是安全的;同理,如果是写指针从 0001 递增到 0011,假设读时钟域采到的是 0001,那么也只是会报读空(因为读时钟域不知道写时钟域已经写到下一个地址了),从而停止读出,这也是安全的。

如何用格雷码判断空满?

首先我们需要将指针向高位拓展一位,这是为了判断写指针是否超过读指针一圈。然后通过对比除了最高位的其余位来判断读写指针是否重合。这种方法判断二进制的指针是没有问题的,但是这不适合格雷码形式的指针,因为格雷码是镜像对称的,若只根据最高位是否相同来区分是读空还是写满是有问题的,如下图:

格雷码

图中是 0–15 的格雷码,7-0 和 8-15 的格雷码除了最高位是关于中线镜像对称的:

  • 7 – 8 ,格雷码 0100 – 1100 , 只有最高位发生变化其余位相同

  • 6 – 9 , 格雷码 0101 – 1101 , 只有最高位发生变化其余位相同

+・・・・

  • 0–15, 格雷码 0000 – 1000 , 只有最高位发生变化其余位相同

假如仅仅通过判断读写指针除了最高位的余下位的话,那么势必会出现判断错误而引发误报的写满和读空信号。举例:读指针指向 0,写指针指向 15。0 的格雷码与 15 的格雷码的最高位不同,其余位相同,所以判断出为写满 – 这就出现误判了。

因此用格雷码判断是否为读空或写满时应使用理论 2,看最高位和次高位是否相等,具体如下:

  • 当最高位和次高位相同,其余位相同认为是读空

  • 当最高位和次高位不同,其余位相同认为是写满

当然还有一种办法就是将同步后的格雷码再转换成二进制码进行比较。

快时钟域的信号同步到慢时钟域造成的漏采

快时钟踩慢时钟可以直接采(打拍)这没问题,但是快时钟信号同步到慢时钟域却有可能发生漏踩的问题(在单 bit 的应用中需要展宽快时钟以便能被慢时钟采集到)。那么造成的漏采问题怎么解决?答案是不需要解决。

读慢写快:

进行写满判断的时候需要将读指针同步到写时钟域,因为读慢写快,所以不会有读指针遗漏,同步消耗时钟周期,所以同步后的读指针滞后(小于等于)当前读地址,所以可能写满会提前产生,并非真写满。

进行读空判断的时候需要将写指针同步到读指针 ,因为读慢写快,所以当读时钟同步写指针的时候,必然会漏掉一部分写指针,我们不用关心那到底会漏掉哪些写指针,我们在乎的是漏掉的指针会对 FIFO 的读空产生影响吗?比如写指针从 0 写到 10,期间读时钟域只同步捕捉到了 3、5、8 这三个写指针而漏掉了其他指针。当同步到 8 这个写指针时,真实的写指针可能已经写到 10 ,相当于在读时钟域还没来得及觉察的情况下,写时钟域可能写了数据到 FIFO 去,这样在判断它是不是空的时候会出现不是真正空的情况,漏掉的指针也没有对 FIFO 的逻辑操作产生影响。

读快写慢:

进行读空判断的时候需要将写指针同步到读指针 ,因为读快写慢,所以不会有写指针遗漏,同步消耗时钟周期,所以同步后的写指针滞后(小于等于)当前写地址,所以可能读空会提前产生,并非真读空。

进行写满判断的时候需要将读指针同步到写时钟域,因为读快写慢,所以当写时钟同步读指针的时候,必然会漏掉一部分读指针,我们不用关心那到底会漏掉哪些读指针,我们在乎的是漏掉的指针会对 FIFO 的写满产生影响吗?比如读指针从 0 读到 10,期间写时钟域只同步捕捉到了 3、5、8 这三个读指针而漏掉了其他指针。当同步到 8 这个读指针时,真实的读指针可能已经读到 10 ,相当于在写时钟域还没来得及觉察的情况下,读时钟域可能从 FIFO 读了数据出来,这样在判断它是不是满的时候会出现不是真正满的情况,漏掉的指针也没有对 FIFO 的逻辑操作产生影响。

3、Verilog 实现

根据以上可以设计异步 FIFO 的实现:

  • 分别构造读、写时钟域下的读、写指针,指针位数需拓展一位。举例,设计的 FIFO 深度为 16,16 个地址需要 4 位二进制数表示,同时扩宽一位作为指示位,所以指针的位宽共需要 5 位。
  • 分别将读、写指针从二进制码转换成格雷码
  • 将格雷码形式的读指针同步到写时钟域;将格雷码形式的写指针同步到读时钟域
  • 在写时钟域判断 “写满”:格雷码形式的读写指针高 2 位相反,其余位相等
  • 在读时钟域判断 “读空”:格雷码形式的读写指针高 2 位相等,其余位也相等 – 即全部相等
// 异步 FIFO
module async_fifo
#(
parameter DATA_WIDTH = 'd8 ,								//FIFO 位宽
parameter DATA_DEPTH = 'd16 								//FIFO 深度
)
(
// 写数据
input							wr_clk		,				// 写时钟
input							wr_rst_n	,  		// 低电平有效的写复位信号
input							wr_en		,  		// 写使能信号,高电平有效
input	[DATA_WIDTH-1:0]		data_in		,  		// 写入的数据
// 读数据
input							rd_clk		,				// 读时钟
input							rd_rst_n	,  		// 低电平有效的读复位信号
input							rd_en		,  		// 读使能信号,高电平有效
output	reg	[DATA_WIDTH-1:0]	data_out	,				// 输出的数据
// 状态标志
output							empty		,				// 空标志,高电平表示当前 FIFO 已被写满
output							full		 			// 满标志,高电平表示当前 FIFO 已被读空
);
//reg define
// 用二维数组实现 RAM
reg [DATA_WIDTH - 1 : 0]			fifo_buffer [DATA_DEPTH - 1 : 0];
reg [$clog2 (DATA_DEPTH) : 0]		wr_ptr;						// 写地址指针,二进制
reg [$clog2 (DATA_DEPTH) : 0]		rd_ptr;						// 读地址指针,二进制
reg	[$clog2 (DATA_DEPTH) : 0]		rd_ptr_g_d1;				// 读指针格雷码在写时钟域下同步 1 拍
reg	[$clog2 (DATA_DEPTH) : 0]		rd_ptr_g_d2;				// 读指针格雷码在写时钟域下同步 2 拍
reg	[$clog2 (DATA_DEPTH) : 0]		wr_ptr_g_d1;				// 写指针格雷码在读时钟域下同步 1 拍
reg	[$clog2 (DATA_DEPTH) : 0]		wr_ptr_g_d2;				// 写指针格雷码在读时钟域下同步 2 拍
//wire define
wire [$clog2 (DATA_DEPTH) : 0]		wr_ptr_g;					// 写地址指针,格雷码
wire [$clog2 (DATA_DEPTH) : 0]		rd_ptr_g;					// 读地址指针,格雷码
wire [$clog2 (DATA_DEPTH) - 1 : 0]	wr_ptr_true;				// 真实写地址指针,作为写 ram 的地址
wire [$clog2 (DATA_DEPTH) - 1 : 0]	rd_ptr_true;				// 真实读地址指针,作为读 ram 的地址
// 地址指针从二进制转换成格雷码
assign 	wr_ptr_g = wr_ptr ^ (wr_ptr >> 1);
assign 	rd_ptr_g = rd_ptr ^ (rd_ptr >> 1);
// 读写 RAM 地址赋值
assign	wr_ptr_true = wr_ptr [$clog2 (DATA_DEPTH) - 1 : 0];		// 写 RAM 地址等于写指针的低 DATA_DEPTH 位 (去除最高位)
assign	rd_ptr_true = rd_ptr [$clog2 (DATA_DEPTH) - 1 : 0];		// 读 RAM 地址等于读指针的低 DATA_DEPTH 位 (去除最高位)
// 写操作,更新写地址
always @ (posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n)
wr_ptr <= 0;
else if (!full && wr_en) begin								// 写使能有效且非满
wr_ptr <= wr_ptr + 1'd1;
fifo_buffer [wr_ptr_true] <= data_in;
end
end
// 将读指针的格雷码同步到写时钟域,来判断是否写满
always @ (posedge wr_clk or negedge wr_rst_n) begin
if (!wr_rst_n) begin
rd_ptr_g_d1 <= 0;										// 寄存 1 拍
rd_ptr_g_d2 <= 0;										// 寄存 2 拍
end
else begin
rd_ptr_g_d1 <= rd_ptr_g;								// 寄存 1 拍
rd_ptr_g_d2 <= rd_ptr_g_d1;								// 寄存 2 拍
end
end
// 读操作,更新读地址
always @ (posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n)
rd_ptr <= 'd0;
else if (rd_en && !empty) begin								// 读使能有效且非空
data_out <= fifo_buffer [rd_ptr_true];
rd_ptr <= rd_ptr + 1'd1;
end
end
// 将写指针的格雷码同步到读时钟域,来判断是否读空
always @ (posedge rd_clk or negedge rd_rst_n) begin
if (!rd_rst_n) begin
wr_ptr_g_d1 <= 0;										// 寄存 1 拍
wr_ptr_g_d2 <= 0;										// 寄存 2 拍
end
else begin
wr_ptr_g_d1 <= wr_ptr_g;								// 寄存 1 拍
wr_ptr_g_d2 <= wr_ptr_g_d1;								// 寄存 2 拍
end
end
// 更新指示信号
// 当所有位相等时,读指针追到到了写指针,FIFO 被读空
assign	empty = ( wr_ptr_g_d2 == rd_ptr_g ) ? 1'b1 : 1'b0;
// 当高位相反且其他位相等时,写指针超过读指针一圈,FIFO 被写满
// 同步后的读指针格雷码高两位取反,再拼接上余下位
assign	full = ( wr_ptr_g == { ~(rd_ptr_g_d2 [$clog2 (DATA_DEPTH) : $clog2 (DATA_DEPTH) - 1])
,rd_ptr_g_d2 [$clog2 (DATA_DEPTH) - 2 : 0]})? 1'b1 : 1'b0;
endmodule

4、Testbench 及仿真结果

接下来编写脚本对源码进行测试:

  • 例化 1 个深度为 8,位宽为 8 的异步 FIFO;读时钟是写时钟的 2 倍,即读快写慢
  • 先对 FIFO 进行写操作,直到其写满,写入的数据为随机数据
  • 然后对 FIFO 进行读操作,直到其读空
  • 然后对 FIFO 写入 4 个随机数据后,同时对其进行读写操作
`timescale 1ns/1ns	// 时间单位 / 精度
//------------< 模块及端口声明 >----------------------------------------
module tb_async_fifo ();
parameter DATA_WIDTH = 8 ;		//FIFO 位宽
parameter DATA_DEPTH = 8 ;		//FIFO 深度
reg							wr_clk		;				// 写时钟
reg							wr_rst_n	;  		// 低电平有效的写复位信号
reg							wr_en		;  		// 写使能信号,高电平有效
reg	[DATA_WIDTH-1:0]		data_in		;  		// 写入的数据
reg							rd_clk		;				// 读时钟
reg							rd_rst_n	;  		// 低电平有效的读复位信号
reg							rd_en		;  		// 读使能信号,高电平有效
wire [DATA_WIDTH-1:0]		data_out	;				// 输出的数据
wire						empty		;				// 空标志,高电平表示当前 FIFO 已被写满
wire						full		;  // 满标志,高电平表示当前 FIFO 已被读空
//------------< 例化被测试模块 >----------------------------------------
async_fifo
#(
.DATA_WIDTH	(DATA_WIDTH),			//FIFO 位宽
.DATA_DEPTH	(DATA_DEPTH)			//FIFO 深度
)
async_fifo_inst (
.wr_clk		(wr_clk		),
.wr_rst_n	(wr_rst_n	),
.wr_en		(wr_en		),
.data_in	(data_in	),
.rd_clk		(rd_clk		),
.rd_rst_n	(rd_rst_n	),
.rd_en		(rd_en		),
.data_out	(data_out	),
.empty		(empty		),
.full		(full		)
);
//------------< 设置初始测试条件 >----------------------------------------
initial begin
rd_clk = 1'b0;					// 初始时钟为 0
wr_clk = 1'b0;					// 初始时钟为 0
wr_rst_n <= 1'b0;				// 初始复位
rd_rst_n <= 1'b0;				// 初始复位
wr_en <= 1'b0;
rd_en <= 1'b0;
data_in <= 'd0;
#5
wr_rst_n <= 1'b1;
rd_rst_n <= 1'b1;
// 重复 8 次写操作,让 FIFO 写满
repeat (8) begin
@(negedge wr_clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
end
end
// 拉低写使能
@(negedge wr_clk)	wr_en <= 1'b0;
// 重复 8 次读操作,让 FIFO 读空
repeat (8) begin
@(negedge rd_clk) rd_en <= 1'd1;
end
// 拉低读使能
@(negedge rd_clk) rd_en <= 1'd0;
// 重复 4 次写操作,写入 4 个随机数据
repeat (4) begin
@(negedge wr_clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
end
end
// 持续同时对 FIFO 读
@(negedge rd_clk) rd_en <= 1'b1;
// 持续同时对 FIFO 写,写入数据为随机数据
forever begin
@(negedge wr_clk) begin
wr_en <= 1'b1;
data_in <= $random;	// 生成 8 位随机数
end
end
end
//------------< 设置时钟 >----------------------------------------------
always #10 rd_clk = ~rd_clk;			// 读时钟周期 20ns
always #20 wr_clk = ~wr_clk;			// 写时钟周期 40ns
endmodule

仿真结果如下:

仿真结果

仿真过程分为 4 个阶段:

  1. 一直写直到写满(共 8 个随机数据)
  2. 一直读直到读空
  3. 仅写 4 个数据
  4. 同时读写

阶段 1、2 的局部图如下:

阶段 1、2

上图中,先是往 FIFO 中写入了 8 个随机数据后拉高了写满信号,然后连续读取 8 个数据后,拉高了读空信号,且读出的数据域写入的数据一致。

阶段 3、4 的局部图如下:

阶段 3、4

上图中,先是往 FIFO 中写入了 4 个随机数据后,然后连续同时读、写,由于读时钟是写时钟的 2 倍,所以在一段时间后每隔 1 个读时钟周期,就会拉高了读空一次,读出的数据域写入的数据一致。可以看到在第一次读空出现时,此时读出的数据是 ed,而在写时钟域此时已经将数据 8c 写到 FIFO 里了,这就是说此时出现的是假 “读空”,假读空不会造成功能错误,只会造成性能损失。

5、总结

  • 此文实现了同位宽的异步 FIFO 的设计及分析验证,下一篇文章再介绍不同的位宽的异步 FIFO 的设计
  • 如果需要完整的工程文件请点这里:工程文件下载

Verilog 实现的格雷码与二进制码的互相转换

孤独的单刀 已于 2022-11-11 00:14:57 修改

1、什么是格雷码

格雷码是一种循环二进制码或者叫作反射二进制码。格雷码的特点是从一个数变为相邻的一个数时,只有一个数据位发生跳变,由于这种特点,就可以避免二进制编码计数组合电路中出现的亚稳态。格雷码常用于通信,FIFO 或者 RAM 地址寻址计数器中。

下表给出了 4 bit 自然二进制码、4 bit 典型格雷码(无特殊说明,典型格雷码即格雷码)与 4 bit 十进制整数的对照:

十进制数4 位自然二进制码4 位典型格雷码
000000000
100010001
200100011
300110010
401000110
501010111
601100101
701110100
810001100
910011101
1010101111
1110111110
1211001010
1311011011
1411101001
1511111000

可以看到,上表中格雷码的每次变化位数只有一位,这就有效地避免了在 CDC 情况(跨时钟域情况)下亚稳态问题发生的概率。比如当数字从 7 变为 8 时,4 位二进制数都发生跳变,如果直接使用异步时钟采样这些数字信号,这就很可能会发生亚稳态或者数据采样错误。而采用格雷码,就可以避免 4 位二进制数都同时发生跳变,导致出现的亚稳态,就算出现亚稳态,最多也就一位出现错误。

但是由于格雷码是一种 变权码,每一位码没有固定的大小,所以很难直接进行比较大小和算术运算。

2、二进制转格雷码

二进制码转化为格雷码原理如下:

二进制的最高位作为格雷码的最高位,次高位的格雷码为二进制的高位和次高位相异或得到,其他位与次高位类似。转化过程如下图:

二进制转格雷码

假如是 4 bit 的二进制数据转成格雷码则是:

  • gray[3] = 0 ^ bin[3];---- gray[3] = bin[3] 异或 0 等于自身
  • gray[2] = bin[3] ^ bin[2];
  • gray[1] = bin[2] ^ bin[1];
  • gray[0] = bin[1] ^ bin[0];

根据上面的式子不难推导出一般公式:gray = (bin >> 1) ^ bin。根据公式很容易写出二进制码转换为格雷码的 Verilog 代码:

// 二进制转格雷码
module bin2gray
#(
    parameter data_width = 'd4			// 数据位宽
)
(
    input [data_width - 1 : 0] bin, 	// 二进制
    output [data_width - 1 : 0] gray	// 格雷码
);
assign gray = (bin >> 1) ^ bin;
endmodule

3、格雷码转二进制

格雷码转化为二进制码原理如下:

使用格雷码的最高位作为二进制的最高位,二进制次高位产生过程是使用二进制的高位和次高位格雷码相异或得到,其他位的值与次高位产生过程类似。转化过程如下图:

格雷码转二进制

假如是 4 bit 的格雷码转成二进制则是:

  • bin[3] = gray[3];
  • bin[2] = gray[2] ^ bin[3];
  • bin[1] = gray[1] ^ bin[2];
  • bin[0] = gray[0] ^ bin[1];

可以看到,最高位不需要转换,从次高位开始使用二进制的高位和次高位格雷码相异或,那么可以使用 generate–for 来构建重复赋值语,具体代码如下:

// 格雷码转二进制
module gray2bin
#(
    parameter data_width = 'd4						// 数据位宽
)
(
    input [data_width - 1 : 0] gray, 				// 格雷码
    output [data_width - 1 : 0] bin	 			// 二进制
);
assign bin[data_width - 1] = gray[data_width - 1];		// 最高位直接相等
// 从次高位到 0,二进制的高位和次高位格雷码相异或
genvar i;
generate
for (i = 0; i <= data_width - 2; i = i + 1)
begin: gray										// 需要有名字
    assign bin[i] = bin[i + 1] ^ gray[i];
end
endgenerate
endmodule

4、测试

构建一个测试脚本对两个模块进行测试:生成 0-15 的 4 bit 二进制数据,通过 bin2gray 转换成格雷码,观察格雷码输出;将转换后的格雷码输出通过 gray2bin 再转换成 2 进制码,然后对比三组数据是否符合转换规则。

`timescale 1ns/1ns	// 时间单位 / 精度
//------------< 模块及端口声明 >----------------------------------------
module gray_code ();
parameter data_width = 'd4;					// 数据位宽
reg [data_width - 1 : 0] bin_in;			// 生成的二进制码
wire [data_width - 1 : 0] gray;			// 转换后的格雷码
wire [data_width - 1 : 0] bin_out;		// 转换后的二进制码
//------------< 例化被测试模块 >----------------------------------------
bin2gray
#(
    .data_width (data_width)
)
bin2gray_inst (
    .bin (bin_in),
    .gray (gray)
);
gray2bin
#(
    .data_width (data_width)
)
gray2bin_inst (
    .bin (bin_out),
    .gray (gray)
);
//------------< 设置初始测试条件 >----------------------------------------
initial begin
    bin_in = 4'd0;
    forever #20 bin_in = bin_in + 1;	// 每隔 20ns 累加 1
end
// 打印输出
initial $monitor ("bin_in:%b, gray:%b, bin_out:%b", bin_in, gray, bin_out);
endmodule

仿真结果如下:

仿真结果

可以看到两次转换的结果都是正确的,接下来看一下命令窗口打印的输出:

命令窗口输出

可以看到这个转换结果与第一章的对照表是一致的。


关于异步 FIFO 设计,这 7 点你必须要搞清楚

孤独的单刀 已于 2022-04-13 16:55:43 修改

写在前面

在这篇文章,默认您已经对异步 FIFO 的设计方法有了基本的了解。

1、什么是格雷码(Gray Code)?

格雷码是美国学者 Frank Gray 于 1947 年提出的一种二进制编码方式,后面这种编码方式就以他的名字命名。实际上,格雷码是有多种编码形式的。根据定义,在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码。

下表是不同形式的格雷码:

格雷码形式

表中典型格雷码具有代表性。若不作特别说明,格雷码就是指 典型格雷码(后文将简称格雷码),它可从自然二进制码转换而来。

好了,现在我问你,根据格雷码的性质你能默写出 16 个 4 bit 宽度的格雷码来吗?我想一般是比较难的,因为单靠上述性质很难推导出来。比如说,0(0000)可以写出来,1(0001)也可以写出来,2(0011)也可以写出来,但是 3 就不好写了,因为根据只能变化一位的性质,可以是 0111,也可以是 0010,那么到底是哪个呢?如何不是经常使用的话,我想是很难判断的。

所以接下来就要介绍格雷码的第二个性质了 :当第 N 位从 0 变到 1 的时候,之后的数的 N-1 位会关于前半段轴对称,而比 N 位高的位是相同的。

我们看一下 4 bit 格雷码的前四位的例子。示意图如下:

  • 当 0001 跳转到下一位时,毋庸置疑的是,第 0 位会维持 1 不变,而第 1 位会跳转到 1,所以可以据此画出对称轴
  • 高 2 位(第 3、2 位)这保持不变
  • 低位(该实例中只有第 0 位)关于对称轴对称

有了上述三点信息,那么就可以把剩下的 2 个格雷码写出来了。

格雷码对称

4 bit 格雷码的前 8 位示意图如下(这我就不啰嗦了,你们肯定看得懂):

4 bit 格雷码

2、异步 FIFO 为什么要使用格雷码?

异步 FIFO 设计最关键的点是什么?答案是 “空” 和 “满” 的判断。现在回想一下,我们是如何进行状态判断的。很简单,分别构建读指针和写指针。写指针总是指向下一个要写的地址,而读指针永远指向当前要读取的地址。再通过对读、写指针的比较来判断空、满。

FIFO 指针

如上图的 FIFO 深度为 8,则其地址位宽应该是 3(2 的三次方等于 8)。当写指针与读指针都指向同一个位置(即相同)时,可能是空状态,但也可能是满状态(写指针超过了读指针一圈)。(这里说句题外话,不可能是读指针超过写指针一圈,因为但读、写指针第一次相等时,就应该输出空状态,然后停止对 FIFO 进行读取操作。)所以,究竟如何判断 FIFO 是空还是满呢?答案是无法判断,只能通过在高位增加一位的方式来判断,当除了最高位 MSB 外的其他位都相同,最高位不同时则表明此时写指针超过了读指针一圈,FIFO 被写满了;当除了最高位 MSB 外的其他位都相同,最高位相同时则表明此时写指针等于读指针,FIFO 被读空了。

这种方法用来对同步 FIFO 进行判断是没有问题的,因为同步 FIFO 的读、写指针是同一时钟域下的信号,可以直接对比。但是异步 FIFO 的读、写指针是不同时钟域下的信号,如果直接对比则会有亚稳态的问题。为了对异步 FIFO 的读、写指针进行判断,我们首先需要将其同步到统一的时钟域下,而这就引发出了新的问题 ---- 读、写指针在大多数情况下都不是个单 bit 信号,而是个多 bit 信号。如果读、写指针直接使用 2 进制的形式进行同步,则难以避免同步过程中会出现的多个 bit 信号同时变化的问题。如 7(0111)跳转到 8(1000),此时有 4 个 bit 信号都发生了变化,如果直接同步,则由于不同信号之间的延迟(skew),可能导致亚稳态、错采、漏采等等问题。

此时不妨回想下格雷码的性质:每相邻位之间只有一个 bit 的变化。FIFO 的指针是递增的,这使得在传输递增的多 bit 信号时,格雷码具有天然的优势。还是从 7 到 8 的例子,若使用格雷码,则应该是 7(0100)–8(1100),这样就只有 1 个 bit 的变化了(最高位),这样就将多 bit 信号的跨时钟域转变成了单 bit 信号的跨时钟域,而单个 bit 的跨时钟域同步是很好实现的。

3、读指针、写指针应该被同步到哪个时钟域?

异步 FIFO 的读、写指针是不同时钟域的信号,那么就不能直接对比,而是需要将其同步到同一时钟域才能进行对比。现在问题来了?指针应该被同步到哪个时钟域?可选项有第三方时钟域、读时钟域和写时钟域。接下来不妨逐个分析。

首先需要说明的是,这说的同步都是指使用 2 个(或者 3 个,但此类情况不多)FF(触发器)来进行同步(俗称 “打两拍”),这种同步方式是有延迟的(时序开销,可以看做是两个同步时钟周期)。

第三方时钟域:不难知道一个信号从一个时钟域同步到另一个时钟域(被同步时钟域)是需要时间的(这里仅考虑从满到快,也就是暂时不考虑漏采的问题),需要的时间取决于被同步时钟域的周期以及需要同步的个数。假设这个时间是 T,那么经过 T 时间后,由于读写时钟不一致,原来的读写时针增加(也可能不变)的量是不一致。比如说实际上读写时针都指向 4(且最高位相同),那么这种情况实际上是出现了读空的情况。但是同步到第三方时钟域后,可能写指针成了 6,而读指针变成了 8(读时钟比写时钟快),那么在这种情况下 FIFO 就不会报 “读空”,从而造成功能错乱。所以该种方法不可取。

同步到写时钟域:读指针同步到写时钟域需要时间 T,在经过 T 时间后,可能原来的读指针会增加或者不变,也就是说同步后的读指针一定是小于等于原来的读指针的。写指针也可能发生变化,但是写指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的写指针就是真实的写指针。

现在来进行写满的判断:也就是写指针超过了同步后的读指针一圈。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候写指针其实是没有超过读指针一圈的,也就是说这种情况是 “假写满”。那么 “假写满” 是一种错误的设计吗?答案是 NO。前面我们说过异步 FIFO 设计的关键点是产生合适的 “写满” 和 “读空” 信号,那么何谓 “合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设一个深度为 100 的 FIFO,在写到第 98 个数据的时候就报了 “写满”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了 FIFO 的深度我少用一点点就是的。事实上这还可以算是某种程度上的保守设计(安全)。

接着进行读空的判断:也就是同步后的读指针追上了写指针。但是原来的读指针是大于等于同步后的读指针的,所以实际上这个时候读指针实际上是超过了写指针。这种情况意味着已经发生了 “读空”,却仍然有错误数据读出。所以这种情况就造成了 FIFO 的功能错误。

同步到读时钟域:写指针同步到读时钟域需要时间 T,在经过 T 时间后,可能原来的读指针会增加或者不变,也就是说同步后的写指针一定是小于等于原来的写指针的。读指针也可能发生变化,但是读指针本来就在这个时钟域,所以是不需要同步的,也就意味着进行对比的读指针就是真实的读指针。

现在来进行写满的判断:也就是同步后的写指针超过了读指针一圈。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候写指针已经超过了读指针不止一圈,这种情况意味着已经发生了 “写满”,却仍然数据被覆盖写入。所以这种情况就造成了 FIFO 的功能错误。

接着进行读空的判断:也就是读指针追上了同步后的指针。但是原来的写指针是大于等于同步后的写指针的,所以实际上这个时候读指针其实还没有追上写指针,也就是说这种情况是 “假读空”。那么 “假读空” 是一种错误的设计吗?答案是 NO。前面我们说过异步 FIFO 设计的关键点是产生合适的 “写满” 和 “读空” 信号,那么何谓 “合适”?该报的时候没报算合适吗?当然不算合适。不该报的时候报了算不算合适?答案是算。可以想象一下,假设某个 FIFO,在读到还剩 2 个数据的时候就报了 “读空”,会引起什么后果?答案是不会造成功能错误,只会造成性能损失(2%),大不了我先不读了,等数据多了再读就是的。事实上这还可以算是某种程度上的保守设计(安全)。

现在我们可以总结一下:

  • “写满” 的判断:需要将读指针同步到写时钟域,再与写指针判断
  • “读空” 的判断:需要将写指针同步到读时钟域,再与读指针判断

假读空示意如下:

假读空

假写满示意如下:

假写满

4、如何判断异步 FIFO 的空和满?

在同步 FIFO 的设计中,我们把读、写指针的位宽拓宽了 1 bit,目的是区分原来的读、写指针相等时判断空、满的问题。同步 FIFO 的指针使用的是 2 进制码的形式,而异步 FIFO 为了减少多 bit 信号跨时钟域传输的亚稳态问题,采用的是格雷码形式的指针,那么格雷码形式的指针应该如何对比来判断空和满?

首先要说明的是,将格雷码转换成 2 进制再进行对比是一种很好的办法,但是会消耗多的组合逻辑资源,所以我们暂时不讨论。

格雷码指针

先观察上图,假定我们要设计的 FIFO 深度为 8,那么指针宽度是 4,因为宽度为 3 时无法区分空、满。

空的判断很好判断,只要读写指针所有位全部一致,则说明此时是空状态。

满的判断复杂一点,假设采用同步 FIFO 的判断方法 ---- 最高位不同,其他位一致。我们用实际的数值来看看是什么情况:当读指针的值为 0100,则说明此时读指针指向最高的空间 7,那么若是 FIFO 满了,则写指针应该是 1100,那么 1100 对应的二进制是多少呢?是 8。那么读写指针、一个指向 7,另一个却指向 8,这是满?显然不是。

如果真的是满的话,写指针应该是多少?应该是 15,也就是 1000,此时写指针是超过读指针 8,也就是一圈的。那么 0100 和 1000 有什么联系?很显然,高两位相反,低位相同。这种规律的形成原因是我们之前提到的,格雷码的变化都关于某个对称轴对称。

总结:

  • 当最高位和次高位相同,其余位相同认为是读空
  • 当最高位和次高位不同,其余位相同认为是写满

5、空和满的判断是准确的吗?

在第 4 点我们知道了 ---- 将读指针同步到写时钟域来判断满;将写指针同步到读时钟域来判断空。既然是异步 FIFO,那么读写时钟域的信号是不一致的,其中一个的频率快,另一个的频率慢。那么在两次同步过程中,一定是一次慢时钟采快时钟和一次快时钟采慢时钟。快时钟采慢时钟是不会有问题的,因为这符合采样定理。但是慢时钟采快时钟则会有问题,因为采样过程不符合采样定理。

那么会造成什么问题?答案是漏采。某些数值可能会被漏掉。例如原本是连续的 0–1–2—3 的信号,从快时钟同步到慢时钟后,就变成了离散的 0–3,其中的 1、2 被漏掉了。那么这样一种现象会导致空、满的判断是准确的吗?答案是不准确,但没关系。

设想读慢写快与读快写慢两种情况:

读慢写快

进行写满判断的时候需要将读指针同步到写时钟域,因为读慢写快,所以不会有读指针遗漏,同步消耗时钟周期,所以同步后的读指针滞后(小于等于)当前读地址,所以可能写满会提前产生,并非真写满。

进行读空判断的时候需要将写指针同步到读指针 ,因为读慢写快,所以当读时钟同步写指针的时候,必然会漏掉一部分写指针,我们不用关心那到底会漏掉哪些写指针,我们在乎的是漏掉的指针会对 FIFO 的读空产生影响吗?比如写指针从 0 写到 10,期间读时钟域只同步捕捉到了 3、5、8 这三个写指针而漏掉了其他指针。当同步到 8 这个写指针时,真实的写指针可能已经写到 10 ,相当于在读时钟域还没来得及觉察的情况下,写时钟域可能写了数据到 FIFO 去,这样在判断它是不是空的时候会出现不是真正空的情况,漏掉的指针也没有对 FIFO 的逻辑操作产生影响。

读快写慢

进行读空判断的时候需要将写指针同步到读指针 ,因为读快写慢,所以不会有写指针遗漏,同步消耗时钟周期,所以同步后的写指针滞后(小于等于)当前写地址,所以可能读空会提前产生,并非真读空。

进行写满判断的时候需要将读指针同步到写时钟域,因为读快写慢,所以当写时钟同步读指针的时候,必然会漏掉一部分读指针,我们不用关心那到底会漏掉哪些读指针,我们在乎的是漏掉的指针会对 FIFO 的写满产生影响吗?比如读指针从 0 读到 10,期间写时钟域只同步捕捉到了 3、5、8 这三个读指针而漏掉了其他指针。当同步到 8 这个读指针时,真实的读指针可能已经读到 10 ,相当于在写时钟域还没来得及觉察的情况下,读时钟域可能从 FIFO 读了数据出来,这样在判断它是不是满的时候会出现不是真正满的情况,漏掉的指针也没有对 FIFO 的逻辑操作产生影响。

现在我们会发现,所谓的空满信号实际上是不准确的,在还没有空、满的时钟就已经输出了空满信号,这样的空满信号一般称为假空、假满。假空、假满信号本质上是一种保守设计,想象一下,一个深度为 16 的异步 FIFO,在其写入 14 个数据时,即输出了写满(假满)标志,这会对我们的设计造成影响吗?会,会削弱我们的效率,我们实际使用的 FIFO 深度成了 14,但是会使得我们的设计产生错误吗?显然不会。同样的,在 FIFO 深度仍有 2 时即输出了读空(假空)标志,也不会使得我们的设计出错,但是会降低效率,因为我们使用的 FIFO 深度又少了 2。

6、既然有假满、假空,那么有没有真满、真空?

还真有,但是没意义。既然我们可以将读指针同步到写时钟域来判断假满;将写指针同步到读时钟域来判断假空。那么对应地,可以读指针同步到写时钟域来判断空;将写指针同步到读时钟域来判断满。

在写时钟域判断空

读指针被同步过来的信号(同步后读指针)是滞后于真实读指针的信号,当同步后读指针等于写指针时,真实读指针实际上早就等于写指针了,也就是说此时的空一定是空,甚至已经空了一段时间了。这样的空标志显然是没有使用意义的,因为会造成对 FIFO 的过读操作,你来来回回读个空 FIFO 有什么意义呢?也就是说真空能实现,但是没实际使用意义。

在读时钟域判断满

写指针被同步过来的信号(同步后写指针)是滞后于真实写指针的信号,但同步后写指针超过读指针一圈时,真实写指针实际上早就超过读指针一圈了,也就是说此时的满一定是满,甚至已经满了一段时间了。这样的满标志显然是没有使用意义的,因为会造成对 FIFO 的过写操作,你来来回回写个满 FIFO 有什么意义呢?也就是说真满也能实现,但是同样没实际使用意义。

7、非 2 次幂深度的 FIFO 如何设计?

在第 1 点关于格雷码的性质中,我们阐述了:

  • 在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码
  • 当第 N 位从 0 变到 1 的时候,之后的数的 N-1 位会关于前半段轴对称,而比 N 位高的位是相同的。

由这两点,我们发现格雷码都可以关于某条对称轴对称。所以只有当 FIFO 深度为 2 的幂次方时,才能做到格雷码绕一圈后,回到初始位置仍然只有 1 bit 变化,如 15(1000)----0(0000)。当 FIFO 深度不为 2 的幂次方时,显然从最尾端跳转到开头端,变化的就不止一个 bit 了。比如 FIFO 深度为 7,显然,从 13(1011)----0(0000),变化了可不止 1 bit。这样的话在这一次跳变就失去了格雷码存在的意义了,所以得想点其他办法解决。

前面说过,格雷码相邻每位只变化 1 bit,而且关于中轴对称的。那么我们可以这样编码:针对深度为 7 的 FIFO,从 1(0001)开始,一直到(14 个数表示 7 深度,高位区分状态)14(1001),1(0001)与 14(1001)是关于中轴对称的(高位为变化位),所以也只有 1 bit 的跳变。同样的如果深度为 6 的 FIFO,就从 2(0011)开始,到 13(1011),同样只跳变 1 bit。

非 2 次幂深度 FIFO

空标志只用判断读、写指针是否全部相等即可。但是满标志就不能用 “高两位相反,其他位相同来判断了”,需要找其他规律了。从这里可以看出,格雷码作为一种无权码,在比较和运算等方面不如有权码二进制灵活。所以,咱要不还是转回二进制再比较吧。

多啰嗦一句,实际上不管你设计 FIFO 用 RAM 还是直接调用 IP 也好,最终实现都是用的 Block RAM 资源,其生成的位宽肯定是 2 的幂次。所以啊,不用也是浪费啊。


FPGA 中亚稳态的理解(Understanding Metastability in FPGAs)

孤独的单刀已于 2022-05-13 11:08:40 修改

写在前面

本文主要翻译自 Altera 公司的白皮书《Understanding Metastability in FPGAs》,主要讲述了 FPGA 设计中与亚稳态相关的问题。

正文中,黑字为翻译,浅蓝色字体是我的肤浅理解。如果需要英文原文文章,可以评论留邮箱给我。

概述

亚稳态是一种现象 ---- 当信号在不相关或异步时钟域中的电路之间传输时,可能导致包括 FPGA 在内的数字设备发生系统故障。本文描述了 FPGA 中的亚稳态,解释了这种现象发生的原因,并讨论了它是如何导致设计失败的。

由于亚稳态而计算出的平均无故障工作时间 (MTBF) 表明设计人员是否应采取措施减少此类故障发生的机会。本文解释了如何根据各种设计和器件参数计算 MTBF,以及 FPGA 供应商和设计人员如何提高 MTBF 的值。通过使用设计技术和优化手段降低亚稳态故障发生的可能性,以此来提高系统可靠性。

亚稳态这种现象是不可避免的,哪怕是在同步电路中也有概率出现,所以作为设计人员,我们能做的是减少亚稳态发生的概率,当然这也是一个稳健的系统所必须拥有的前提条件。

什么是亚稳态?

FPGA 等数字设备中的所有寄存器都定义了信号时序要求 ---- 允许每个寄存器在其输入端正确捕获数据并产生输出信号。为确保可靠操作,寄存器的输入必须在时钟边沿之前的最短时间(寄存器设置时间或 tsu)和时钟边沿之后的最短时间(寄存器保持时间或 tH)保持稳定。然后寄存器输出在指定的时钟到输出延迟 (tco) 之后可用。如果数据信号转换违反了寄存器的 tsu 或 tH 时序要求,则寄存器的输出可能会进入亚稳态。在亚稳态中,寄存器输出在高电平和低电平状态之间徘徊一段时间,这意味着输出转换到定义的高电平或低电平状态被延迟超过指定的 tco。

在同步系统中,输入信号必须始终满足寄存器时序要求,因此不会出现亚稳态。当信号在不相关或异步时钟域的电路之间传输时,通常会出现亚稳态问题。在这种情况下,设计人员不能保证信号将满足 tsu 或 tH 时序要求,因为信号可以在相对于目标时钟的任何时间到达。但是,并非每个违反寄存器 tsu 或 tH 的信号转换都会导致亚稳态输出。寄存器进入亚稳态的可能性和返回稳定状态所需的时间取决于制造设备所使用的工艺技术和操作条件。在大多数情况下,寄存器会很快恢复到稳定的定义状态。

在时钟边缘对数据信号进行采样的寄存器可以看作是一个球落到山上,如图 1 所示。山的侧面代表稳定状态 —— 信号传输后信号的旧数据值和新数据值 —— 山顶代表亚稳态。如果球掉在山顶,它可能会在那里无限期地保持平衡,但实际上它会稍微落到山顶的一侧并滚下山坡。球离山顶越远,它在底部达到稳定状态的速度就越快。

如果数据信号在时钟沿和最小值 tH 之后发生传输,则类似于将球落在山的 “旧数据值” 一侧,并且输出信号保持在该时钟传输的原始值。当寄存器的数据输入在时钟边沿和最小值 tsu 之前发生传输,并保持在最小值 tH 之后,这类似于将球落在山的 “新数据值” 一侧,并且输出足够快地达到稳定的新状态以满足定义的 tco 时间。但是,当寄存器的数据输入违反 tsu 或 tH 时,就类似于将球扔到山上。如果球落在山顶附近,球需要很长时间才能到达底部,这会增加从时钟传输到稳定输出的延迟,超出定义的 tco。

亚稳态示意图

图 2 说明了亚稳态信号。输入信号在时钟信号转换时从低状态转换为高状态,这违反了寄存器的 tsu 要求。数据输出信号示例从低状态开始并进入亚稳态,在高和低状态之间徘徊。信号输出 A 解析为输入数据的新逻辑 1 状态,输出 B 返回数据输入的原始逻辑 0 状态。在这两种情况下,到定义的 1 或 0 状态的输出转换都会延迟超过寄存器指定的 tco。

亚稳态信号

简单来讲,一旦不满足寄存器的建立时间和保持时间要求就会发生亚稳态。亚稳态在一段时间内是一个不确定的值,在一段时间 tco 后,会固定在一个稳定的值,但这个值可能是 0,也可能是 1。

亚稳态何时会导致设计失败?

如果数据输出信号在下一个寄存器捕获数据之前解析为有效状态,则亚稳态信号不会对系统操作产生负面影响。但是,如果亚稳态信号在到达下一个设计寄存器之前没有解析为低或高状态,则可能导致系统失败。继续球和山的类比,当球到达山底所需的时间(稳定的逻辑值 0 或 1)超过分配的时间(即寄存器的 tco 加上任何时间)时,可能会发生故障。当亚稳态信号未在分配的时间内解决时,如果目标逻辑观察到不一致的逻辑状态,即不同的目标寄存器为亚稳态信号捕获不同的值,则可能导致逻辑故障。

亚稳态所带来的不定态,如果不采取手段将其消除,在会传播到后续电路,影响其输出,从而造成系统故障。

同步寄存器

当信号在不相关或异步时钟域的电路之间传输时,必须先将该信号同步到新的时钟域,然后才能使用它。新时钟域中的第一个寄存器用作同步寄存器。

为了最大限度地减少异步信号传输中由于亚稳态引起的故障,电路设计人员通常使用目标时钟域中的一系列寄存器(同步寄存器链或同步器)将信号重新同步到新的时钟域。这些寄存器允许潜在的亚稳态信号在信号用于设计的其余部分之前解析为已知值的额外时间。同步器寄存器到寄存器路径中可用的时序裕量是亚稳态信号稳定可用的时间,称为可用亚稳态稳定时间。

同步寄存器链或同步器被定义为满足以下要求的寄存器序列:

  • 链中的寄存器都由相同或相位相关的时钟提供时钟
  • 链中的第一个寄存器由不相关的时钟域驱动,或异步驱动
  • 每个寄存器只扇出一个寄存器,链中的最后一个寄存器除外

同步寄存器链的长度是同步时钟域中满足上述要求的寄存器个数。图 3 显示了长度为 2 的示例同步链,假设输出信号馈送 (feed) 多个寄存器目标。

同步寄存器链

请注意,任何异步输入信号或在不相关时钟域之间传输的信号都可以在相对于捕获寄存器的时钟沿的任何点进行传输。因此,设计人员无法预测信号传输的顺序或目标时钟边沿的数量,直到数据传输完成。例如,如果异步信号总线在时钟域之间传输并同步,则数据信号可能会在不同的时钟沿上转换。结果,总线数据的接收值可能不正确。

设计人员必须使用诸如双时钟 FIFO (DCFIFO) 逻辑来存储信号值或握手逻辑等电路来适应这种行为。 FIFO 逻辑使用同步器在两个时钟域之间传输控制信号,然后使用双端口存储器写入和读取数据。 Altera 为该操作提供 DCFIFO 宏功能,其中包括控制信号的各种延迟和亚稳态保护。否则,如果异步信号充当两个时钟域之间的握手逻辑的一部分,则控制信号指示何时可以在时钟域之间传输数据。在这种情况下,同步寄存器用于确保亚稳态不会干扰控制信号的接收,并且在使用数据之前,数据有足够的稳定时间来解决任何亚稳态条件。在一个设计合理的系统中,只要每个信号在使用前解析为稳定值,设计就可以正常工作。

两级同步的方法是目前最为广泛使用的方法,当然也可以使用三级或者多级同步寄存器同步,只不过要多付出一些时序开销。

计算亚稳态 MTBF

亚稳态导致的平均故障间隔时间(Mean Time Between Failures, MTBF)提供了对亚稳态可能导致设计失败的实例之间的平均时间的估计。更高的 MTBF(例如亚稳态故障之间的数百或数千年)表明设计更稳健。所需的 MTBF 取决于系统应用场景。例如,与消费视频显示设备相比,生命攸关的医疗设备显然需要更高的 MTBF。增加亚稳态 MTBF 可降低信号传输导致器件出现任何亚稳态问题的可能性。

MTBF 计算方法

可以使用有关设计和器件特性的信息来计算特定信号传输或设计中所有传输的亚稳态 MTBF。同步器链的 MTBF 使用以下公式和参数计算:

M T B F = e t MET / C 2 C 1 ⋅ f CLK ⋅ f DATA MTBF = \frac{e^{t_{\text{MET}}/C_2}}{C_1 \cdot f_{\text{CLK}} \cdot f_{\text{DATA}}} MTBF=C1fCLKfDATAetMET/C2

其中:

  • C 1 C_1 C1 C 2 C_2 C2 是取决于器件工艺和操作条件的常数。

  • f CLK f_{\text{CLK}} fCLK f DATA f_{\text{DATA}} fDATA 参数取决于设计规范:

    • f CLK f_{\text{CLK}} fCLK 是接收异步信号的时钟域的时钟频率
    • f DATA f_{\text{DATA}} fDATA 是异步输入数据信号的翻转频率

    更快的时钟频率和更快的数据切换会降低(或恶化)MTBF。

  • t MET t_{\text{MET}} tMET 参数是可用的亚稳态建立时间,或超出寄存器 t co t_{\text{co}} tco 的可用时序裕量,用于将潜在亚稳态信号解析为已知值。同步链的 t MET t_{\text{MET}} tMET 是链中每个寄存器的输出时序裕量的总和。

设计故障率计算

总体设计 MTBF 可以由设计中每个同步器链的 MTBF 确定。同步器的故障率为 1 / MTBF 1/\text{MTBF} 1/MTBF,整个设计的故障率通过将每个同步器链的故障率相加来计算,如下所示:

failure_rate design = 1 MTBF design = ∑ i = 1 n 1 MTBF i \text{failure\_rate}_{\text{design}} = \frac{1}{\text{MTBF}_{\text{design}}} = \sum_{i=1}^{n} \frac{1}{\text{MTBF}_{i}} failure_ratedesign=MTBFdesign1=i=1nMTBFi1

其中 n n n 表示同步器链的数量。设计亚稳态 MTBF 为 1 / failure_rate design 1/\text{failure\_rate}_{\text{design}} 1/failure_ratedesign

工具支持

使用 Altera® PGA 的设计人员不必手动执行这些计算,因为 Altera 的 Quartus® II 软件在工具中集成了亚稳态参数。Quartus II 软件报告已识别同步链的 MTBF,并提供整体设计亚稳态 MTBF。

从 MTBF 的公式中也能看到,随着系统时钟频率的上升,同时也对系统的鲁棒性带来了挑战。所以才说速率越高,设计越容易出问题。

参数化稳态常数

FPGA 供应商可以通过参数化 FPGA 的亚稳态来确定 MTBF 方程中的常数参数。这种表征的困难在于典型 FPGA 设计的 MTBF 通常以年为单位,因此在实际操作条件下使用实际设计测量亚稳态事件之间的时间是不切实际的。为了表征器件特定的亚稳态常数,Altera 使用了一个测试电路,该电路设计为具有较短的、可测量的 MTBF,如图 4 所示。

测试电路

在本设计中,clka 和 clkb 是两个不相关的时钟信号。输入到同步器的数据会在每个时钟周期切换(高 fDATA)。同步器的长度为 1,因为单个同步寄存器馈送两个目标寄存器。目标寄存器在一个时钟周期和一个半时钟周期后捕获同步器的输出。如果信号在下一个时钟沿解析之前进入亚稳态,则电路检测到采样信号不同,并输出错误信号。

该电路检测在半时钟周期内发生的大部分亚稳态事件。 该电路在整个设备中复制以减少任何局部变化的影响,并且每个实例都经过连续测试以消除任何噪声耦合。 Altera 测量每个测试结构一分钟并记录错误计数。测试在不同的时钟频率下进行,MTBF 与 tMET 结果以对数刻度绘制。 C2 常数对应于实验结果趋势线的斜率,C1 常数线性缩放该线。

提高亚稳态 MTBF

由于 MTBF 方程中的指数因子 etMET/C2, tMET/C2 项对 MTBF 计算的影响最大。因此,可以通过增强架构来优化器件的 C2 常数或优化设计以增加同步寄存器中的 tMET 来提高亚稳态。

FPGA 架构增强功能

MTBF 方程中的亚稳态时间常数 C2 取决于与用于制造器件的工艺技术相关的各种因素,包括晶体管速度和电源电压。更快的工艺技术和更快的晶体管使亚稳态信号能够更快地解析。由于 FPGA 已从 180 纳米工艺迁移到 90 纳米,晶体管速度的提高通常会提高亚稳态 MTBF。因此,亚稳态一直不是 FPGA 设计人员关注的主要问题。 然而,随着电源电压随着工艺几何尺寸的减小而降低,电路的阈值电压不会成比例地降低。当寄存器进入亚稳态时,其电压约为电源电压的二分之一。随着电源电压的降低,亚稳态电压电平更接近电路中的阈值电压。当这些电压靠得更近时,电路的增益会降低,寄存器需要更长的时间才能摆脱亚稳态。随着 FPGA 进入 65 纳米及更低的工艺几何尺寸,电源电压为 0.9V 及更低,阈值电压的考虑变得比晶体管速度的提高更为重要。 因此,除非供应商设计 FPGA 电路来提高亚稳态鲁棒性,否则亚稳态 MTBF 通常会变得更糟。 Altera 使用 FPGA 架构的亚稳态分析来优化电路以提高亚稳态 MTBF。 Altera 40-nm Stratix®IV FPGA 架构的架构改进和新器件开发通过降低 MTBF C2 常数提高了亚稳态稳健性结果。

设计优化

MTBF 方程中的指数因子意味着设计相关的 tMET 值的增加会以指数方式增加同步器的 MTBF。例如,如果给定设备和一组操作条件的 C2 常数为 50 ps,则 tMET 仅增加 200 ps 会使指数变为 200/50,并将 MTBF 增加因子 e (4,或超过 50 倍,而 400 ps 的增加将 MTBF 乘以 e8,或近 3000 倍。 此外,MTBF 最差的链对设计 MTBF 有很大影响。例如,考虑具有十个同步器链的两种不同设计。一种设计有 10 个 MTBF 相同的 10,000 年链,另一种设计有 9 个 MTBF 为 100 万年的链,但有 1 个 MTBF 为 100 年的链。设计的故障率是每个链的故障率之和,其中故障率为 1/MTBF。第一个设计的亚稳态故障率为 10 链 × 1/10,000 年 = 0.001,因此设计 MTBF 为 1000 年。第二种设计的故障率为 9 条链 × 1/1,000,000 + 1/100 = 0.01009,设计 MTBF 约为 99 年 —— 仅略低于最差链的 MTBF。

这不就是木桶原理嘛 ---- 最短的那块木板决定了木桶能装多少水。可以看到在一个系统中,最差 MTBF 的那部分能对整体造成多大的影响。

换句话说,一个不好的设计或实施不当的同步链支配了设计的整体亚稳态 MTBF。由于这种影响,对所有异步信号和时钟域传输执行亚稳态分析非常重要。设计人员或工具供应商可以通过改进具有最差 MTBF 的同步器链的 tMET 来对设计 MTBF 产生非常重要的影响。

为了提高亚稳态 MTBF,设计人员可以通过在同步寄存器链中添加额外的寄存器级来增加 tMET。每个额外的寄存器到寄存器连接上的时序裕量被添加到 tMET。设计人员通常使用两个寄存器来同步信号,但 Altera 建议使用三个寄存器的标准以获得更好的亚稳态保护。然而,添加一个寄存器会为同步逻辑增加一个额外的延迟阶段,因此设计人员必须评估这是否可以接受。

如果设计使用具有独立读写时钟的 Altera FIFO 宏功能跨时钟域,设计人员可以增加亚稳态保护(和延迟)以获得更好的 MTBF。 Altera 的 Quartus II MegaWizard™Plug-In Manager 提供了一个选项来选择具有三个或更多同步级的增强亚稳态保护选项。

Quartus II 软件还提供业界领先的亚稳态分析和优化功能,以增加同步寄存器链上的 tMET。识别出同步器后,软件将同步寄存器放置得更近,以增加同步器链中可用的输出时序裕量,然后报告亚稳态 MTBF。

结论

当信号在不相关或异步时钟域的电路之间传输时,可能会出现亚稳态。亚稳态故障之间的平均时间与器件工艺技术、设计规范和同步逻辑中的时序裕量有关。FPGA 设计人员可以通过增加 tMET 的设计技术来提高系统可靠性并增加亚稳态 MTBF,这些设计技术会在同步寄存器中增加时序裕量。 Altera 对其 FPGA 的 MTBF 进行了参数化,并通过器件技术改进提高了亚稳态 MTBF。使用 Altera FPGA 的设计人员可以利用 Quartus II 软件功能来报告其设计的亚稳态 MTBF,并优化设计布局以增加 MTBF。


进阶技巧 - Clock Domain Crossing(CDC) (FPGA/ASIC - design) — (1)

Wei - Yuan, Weng ( Victor)

Nov 26, 2021

基础逻辑电路

跨时域 (CDC) 问题是在 SOC (System on chip, SOC) 中最常遇到的问题,毕竟不是每个系统都只会用到单一时脉 (clock) 频率,也是时序控制电路上最常遇到的问题,不管是在使用 PLL/clock gating/multiple - chip 设计上都需要注意的事项,因此成了各家 Design house_Digital Circuit(DC) 工程师常 Interview 的问题,甚至连有些系统应用 (SA) 工程师也会必须具有相对应的知识,但基于当时在学时身边有 DC 背景的同学说明地有时候并不是很了解,他们总会说叫我自己去网络上自己看,但当时实际上仅是懂基本的方法并不是深入了解。最近自己去自学挖了其他部门的一些 Knowledge / Train course 终于有较深入的了解。

数位电路最常使用的基本储存单位为 D - type Flip Flop(D_FF),而这种电路元件,是透过时脉 (clock) 来更新内部的电压 “H” 以及电压 “L”,也就是逻辑 “1” 与逻辑 “0”。(------ 以前大学老师曾经开过一个玩笑,反正你们玩的电动说到底也就是 0 与 1 的东西组成而已,到底有什么好玩—)。

而 D 型 FF(台湾称为 D 型正反器) 与 D 型 Latch(D 型闩锁),这两种电路都能够储存逻辑 “0” 与 “'1”。而一般我们会比较喜欢用 FF 当作为储存单位,其最主要的原因是 FF 这种储存单位较能够避免产生 glitch(中文称为 “毛刺”)。

未来你可能会常常听到同步 (synchronization,这边以后都以 Sync 表示),同步电路指的是电路系统中会有一个同步讯号 (synchronization signal),系统皆会跟随此同步讯号动作才会听从动作,也就是电路系统皆会跟这个同步讯号同步。 而在电路中最常用的同步讯号就是常称的时脉 (clock)。可能大家常常都知道买电脑时,都多少看过这颗 CPU 频率,那 CPU 的频率指的就是其内部 clock 操作频率范围会在多少 Hz…。 而产生这种高速频率的方波的方式通常是由石英震荡器 (Crystal Oscillator) 产生。

img

图 1. D - latch 以及其 Truth table

img

图 2.Latch 的时序 (timing) 图

Latch 是属于 level trigger(电位触发) 储存单位,也就是说当数据存储的动作取决于输入 clock(或 enable)讯号的电位值 (“H” or ”L”),当 Latch 处于 enable 状态时输出才会随着 Input 发生变化。

img

图 3.主从式 (Master - Slave) D - 正反器

img

图 4.Failing edge 负缘触发 D - FF 时序图

而正反器是属于 edge trigger,仅会在时脉触发 (edge) 边缘改变状态,如图 4 所示,因此我们可以思考一个问题,万一我们的电路系统中的输入讯号端,是一个较不为稳定的高低电压,那们我们由 edge trigger 的正反器相对于 level trigger 的 Latch 是有较大的容忍范围的。

//------------------------------正文开始------------------------------------------------//

//静态时序分析 (STA) — — — — — — — — — — — — — —-— — — — — -//

复习完最基本的逻辑电路后,可能会问说那 CDC 到底是什么???跟同步电路又有什么关系??异步电路是什么关系?相信你看完就下文就会了解

要说明 CDC 必须要先说明静态时序分析 (Static Time Analyze, STA),静态时序分析最基础的概念为 Setup time(建立时间) 以及 Hold time(保持时间),这两件事情其实本身是一种时序 (timing) 的约束 (constraint),一旦不满足这个约束,不满足这个约束我们称为违反时序 (timing violation),电路就会发生亚稳态 (Metastable),亚稳态在电路中没办法完全避免,但是必须要去解决让他恢复正常运作。

建立 (Setup) 时间与保持 (Hold) 时间

建立时间:在同步讯号 clock 边缘前必须保持不变的时间。

保持时间:在同步讯号 clock 边缘后必须保持不变的时间。

而亚稳态 (Metastable) 产生的本质原因我们可以下图 3.主从式 D 型正反器来说明,最简易的概念为主从式正反器可以看成由两个 D - latch 所组成。而建立与保持时间的约束原因主要是因为不管讯号传递再怎么快,必定都会有延迟时间,当输入资料由 D 端送入 “1” 时且 clock 为 “1” 资料会被 latch(这边的 latch 代表锁值的意思) 在第一个 D - Latch,而当 clock 变为 “0” 时会将第一个 latch 值送入至第二个 latch (因下方有个反向器),所以输出值 Q 仅会将 clock 由 “1” 变 “0” 时才会将值更新,也就是 failing trigger。那在这样的讯号传递路径中必定都会有 wire/元件上等等之类的延迟。因此当你输入/输出的值在 clock 边缘前后很短时间内,发生变动值却还不及送入 latch 内锁值这时候就会发生冲突,保持时间也是同样的道理,当内部的电路元件还没断干净就将资料送出,两者皆会导致输出变成不稳定的状态(可能是 0 可能是 1 可能是 0~1 之间晃动)。

img

图 3.主从式 (Master - Slave) D - 正反器

了解了最基本的时序分析后,再来说明 CDC,当电路系统中必须要跨不同的频率的 clock 中(也就是异步的 clock),必定会遇到 timing violation 的问题,因而会产生亚稳态。如图 5 所示。

img

图 5.跨时域所导致的 timing violation

处理 CDC 主要可分为 1 bits 或是多 bits 的 data bus 的处理方式,但原理若看懂了
基本上都是类似的控制手法。

最简单的处理方式为要在 bdat1 后面不要接上任何组合逻辑电路(非常重要跨时域后不能够接上组合逻辑电路,否则可能因为 racing(代表讯号间因为前后延迟不同的现象的专有名词,这种称为竞争 (racing),而因为 racing 所产生的问题我们称为危害 (Hazard))为了避免产生不必要的 glitch,后面不能再接组合逻辑电路,需直接再接一级由 bclk 所驱动的 DFF ,这种方式称为 Two - Flip Flop synchronizer (二级同步器,中国称为敲两拍,有些人称敲两次 FF),

注意!讯号来源 (Source signal)必须要大于 2 * T clkb,避免在敲第二次 FF 时值与原先不一样,才能够确保值是对的。

上面的方法仅限于 1 bit 的讯号传递方式,若是多 bits 的方式用 two - synchronizer 来传递的话会因为其各自延迟时间不同所导致输出错误。

/---------------------------------------------------------------------------------------------------ㄥ

Synchronizer for data bus (多 bits)

而多 bits 也就是 Data Bus 的方式最主要核心为 Mux synchronizer(多工器同步器),而异步 FIFO 以及 Handshaking(握手协议) 的方式其实跟 Mux synchronizer 概念都相同。

主要概念有二

分为 control path 以及 Data Path

而 control path 非常的简单就是利用单 bit 一样的手法 two - synchronizer 来传 enable 讯号给 Mux。

Data Path 就透过 Data bus 传递至 Mux,透过 control path 给稳定的跨时域同步后 enable 讯号才将直送出。

img

图 6.Mux synchronizer

而其他还有类似的方法是较为常用的手法,

*Pre_latch_pe_control_Mux_synchronizer

img

pe_control_Mux_synchronizer

在 data path加一级 FF 做 pre - latch 的动作(可有可无),其原理一样透过 Mux synchronizer 给的同步控制讯号再加一级 FF,并且将此讯号做组合逻辑来产生positive edge(control_pe) 的 enable 讯号给 Mux。以确保多 bits 传递时各 delay 不一致的 issue。

多 bits synchronizer 的方式,还有异步 FIFO 以及 handshaking 还有 Gray code 的方法之后再介绍…。


进阶技巧 - Clock Domain Crossing(CDC) (FPGA/ASIC - design) — (2)

Wei - Yuan, Weng ( Victor)

Nov 28, 2021

上一篇说到 CDC 问题时,最常用的 Mux Synchronizer (也称 Qualifier),最主要的核心就是透过二级同步器 (Two Synchronizer)将 enable 讯号跨时域同步后,才会将控制讯号 enable 给 Mux,Data path 必须等待 Mux 选择为 “H” 后才会将 data bus 送出至下一级的 Register。

像这种透过 1 bit 的 enable 讯号或是 Flag(标记) 讯号来处理 CDC 的问题有很多类似的方式,接下来说明的就是概念都是相同的处理方式。而 Handshaking 的 flag 讯号只是改个名称为 req(请求) 以及 Ack(回应),发射 (Tx)/接收端 (Rx) 在互相传输沟通。

Two_Synchronizer 的方式前面有提到 source 端的 data 讯号必须要稳定不变动且大于两倍的 Tclkb,否则可能会在取样的时候取到 data transition(转变) 的时候,

并且Two - Synchronizer 只能够用频率快的 clock domain 去取样频率慢的 clock domain

因此最常解决 CDC 的方法其实是透过 Handshaking/异步 FIFO 的方式来处理,虽然产生 IC 的面积相对较大,但仍然是最主流的方法,较不会有问题的处理方式。

Handshaking (握手协议)

img

图 1. Handshaking 基本示意图[IEEE Design and Test Metastability and Synchronizers: A Tutorial]

如图 1,首先 Tx (Transmitter,发射端) 当资料处理完毕后,要传送给 Rx (Receiver,接收端) 时,必须要传递一个请求讯号 (request,req) 给 Rx,这个讯号是用来告知我即将有资料传入,而这个 req 讯号先透过 Two Synchronizer CDC 同步后才会将讯号给 Rx,Rx 才会接收 Data Bus (图案为箭头–>) 的资料。

同理,当接收端 (Rx),要传给发射端 (Tx),也有个回应讯 (Acknowledge,Ack)
也必须经过 Two Synchronizer CDC 同步后才会传给发射端 (Tx)。Data Bus 才会回传给接收端 (Tx)。

Asynchronous FIFO (异步 FIFO)

透过异步 FIFO 处理 CDC 的问题是较万用的方法,而 FIFO(先进先出,First In First Output) 这种资料结构传输的方式(如图 2 所示),

img

图 2.FIFO

最常在电路实践上是透过双端口静态随机内存 (Two port Static Random Access Memory,Two Port SRAM) 的读/写 address 以及暂存资料来做处理。

当然一般不会自己重头设计异步 FIFO,毕竟各家厂商都会有自己有封装好的 IP(Silicon Intellectual Property,SIP,简称 IP,矽智财. 为一种电路设计架构相关智慧财产权)。 这边稍微补充一下同步 FIFO 与异步 FIFO 最主要的差异其实就是在 data 写入跟读出的 clock 频率是否一致,那当然要使用在 CDC 上,会使用异步 FIFO(图 3.) 来做处理。

img

图 3.异步 FIFO

相信若有看懂前面说的 Two Synchronizer/Mux Synchronizer/Handshaking 的方式,再来看图 2,你会突然恍然大悟,原来就是混和变形体。

interface 可切分为左边 写入端 (write)

*wdata : write data,写入资料的 Data bus

*wfull : write full, FIFO 已经满的讯号 (若 FIFO 满,即不能写)

*winc : write data valid(increase_valid),写入资料的有效讯号

*wclk : write clk,写入端的 clk domain

*wrst_n : write rst_n,写入端 clk 的 rst_n (active low)

右边的 read 端

rdata : read data,读取资料的 bus

rempty : read empty, FIFO 已经空的讯号(若 FIFO 为空,则不能读)

rinc : read data enable,读取一笔资料的有效讯号

rclk : read clk,读取端的 clk domain

rrst_n : read rst_n,读取端 clk 的 rst_n

wptr 和 rptr 分别是 write pointer 和 read pointer

判断 FIFO 的空满,需要读写指针 CDC 传输,而后对比读写 pointer 的大小,这就存在一个问题,那就是 CDC,在这里具体来说便是读写指针的跨时钟域问题?怎么处理呢?由于读写 pointer 有多 bits (假设 FIFO 深度有 8,则指针最少要有 3 bits 以上(2^3 次方)),多 bit 传输 CDC 问题,我们一般不会直接两级同步过去,Two Synchronizer 前面说过仅适用于 1 - bit 数据的 CDC,

因此,这时候,我们就用到格雷码 (Gray code) 对读写 pointer 计数值进行,以确保跨时域时一次只会有 1 bits 讯号变动啰!

因此 wptr 和 rptr 必须要以格雷码来做转变!

后续 wptr 需要经过 Two Synchronizer 传递到 read 端

–rptr 需要经过 Two Synchronizer 传递到 write 端

—rq2_wptr 和 rptr 做组合逻辑比较来做出 read empty 讯号

—wq2_eptr 和 wptr 做组合逻辑比较来做出 write full 讯号

这样外部就可以根据这两个讯号来判断是否可读和可写

最后在写入端可以使用一个 and gate 将 winc 和 (在 Verilog 代表 binary not )wfull and 起来,因此仅会在 winc 为”H”以及 full 为 "L”时, clk_en 才会为 “H”,read_clock 才会继续读 data bus,以避免外部 overwrite 的状况。

Gray code 的方式可以写个表,如图 4 所示。

img

图 4.十进制,二进位,葛雷码对照表

我们可以根据归纳法

整理出:格雷码的值只需要在原来的二进制的基础上右移一位再 XOR 原来的二进制值即可得到。

因此我们可以用 Verilog 写组合逻辑通式为

//N 代表 bits 数

wire binary [N - 1:0];

wire gray_code[N - 1:0];

assign gray_code [N - 1]= {1’b0,binary[N - 1:1]}^binary;

img

图 5. 二进制转格雷码

以上对于基本的 Clock Domain Crossing 总算有个告落。


Clock Domain Crossing(CDC)

derek8955

最后由 derek8955 于 2024年10月7日 编辑

建议大家在看这个文章前,先把这个视频看完

Asynchronous vs. synchronous Clock

Synchronous: Derived from same clock source(Known Edge Relationship)

Asynchronous: Derived from different clock source(No Edge Relationship)

Why Asynchronous Design is Needed

  • Multi-clock Domain: 每个子电路所需的频率不同,对特定模块就需给较高的频率

What is CDC

电路是由很多 block 组成,每个 block 根据其运算功能不同,而有着不同的 clock period。在整合至一个完整的电路后,一定会有不同 clock 讯号相接的情形,这个状况称之为 CDC

在这里插入图片描述
CDC Path

Problem of CDC

在这里插入图片描述

在数位电路设计里,透过 STA . 来协助检查所有 timing path 是否有 violation. 一个正确运作的 flip-flop 应在 clock edge 的前后一段时间内( Setup time & Hold time )都保持稳定的状态才能确保 flip-flop 所储存的数值。

以上图为例,两个 flip-flop 是由不同的 clock domain 所驱动的,当 adat 最后拉至 0 的瞬间时,由于 adat 的讯号骤降让 bDFF 没有足够的准备时间来完成锁值(setup time violation),此时就会发生 metastable (亚稳态) 。

Problem of metastability

在这里插入图片描述

无法保证后方电路输出状态(输入电流不稳定),造成后方逻辑运算错误。

Resolution of metastability

现今没有个办法可以完全解决 metastability, 要做的事是大幅度地降低 metastability 产生的概率。所以会使用 MTBF(mean time between failure) 这个指标来衡量电路情形。 MTBF意思是发生两次错误之间的间隔,根据应用场景的不同有不同的要求标准。

针对 metastability 的处理电路可依输入讯号分为 single/multiple bit,在文后会针对这个部分说明。

  • (1) Double flip-flop synchronizer(Single bit)

    在这里插入图片描述
    新增一级由 bclk 所驱动的 flip-flop(bdat2)

由于没有足够的准备时间,导致第二级的 flip-flop 产生 metastable,所以直接在后面多加一级 flip-flop,提共足够的准备时间。但 double flip 没办法完全消除 metastability,只是在一般情况下就已经可以大幅降低 metastability 的产生。

第一级与第二级之间的 timing problem 就不需要在合成阶段做检查,可以输入下方指令
dc_shell>set_false_path –from aff/CK –to b1ff/D

不过这样的 synchronizer 还是存在了一个问题,如下图所示, adata 变化太快, bclk 还没触发就改变了,导致后方电路完全不知道 adata 有改变。

在这里插入图片描述
Problem of double flip-flop synchronizer

所以使用这个方法来同步,输入讯号 必须至少大于3个 destination clock domain edge。以下图为例,adata 一开始有个 pulse,但在 bclk domain 只碰到了2个 edge( 一个 falling edge 和 一个 rising edge),所以导致 bata 看不到 adata 有个 pulse.

在这里插入图片描述
Limitation of double flip-flop synchronizer

所以如果本身 destination clock domain 是 source clock domain 的1.5倍以上,那尽管 input data 是一个很短的 pulse, destination clock domain 还是能侦测到。

  • (2) Pulse synchronizer(Single bit)

Double flip-flop synchronizer 对 pulse 的长度有限制,如果小于 destination domain 的 3 个 edge 就不会成功 sync 输入讯号。

Pulse synchronizer 进而改善这个缺陷,整体流程分为以下三个部分:

  1. 将 source clock domain 的 pulse 转换至 long pulse
  2. 同样用 double flip-flop synchronizer 同步 long pulse
  3. 转换同步后的 long pulse 至 pulse
在这里插入图片描述
Pulse synchronizer using XOR
在这里插入图片描述
Result

虽然说 Pulse synchronizer 可以在 a_p_in 是个 single pulse 的情况也能同步,但转换至 long pulse(Tq) 也必须要满足 3 个 destination clock domain edge. 换句话说,就是 a_p_in 发生 pulse 的间隔不可以太近。

确切的大小可以看下图,a_p_in 最快产生 pulse 的情况即是隔了一个 clock cycle, 如此 a_p_in 则变成 aclk 的 1/2 倍频, Tq 则变成 aclk 的 1/4 倍频, 换句话说即是 Tq 这个 long pulse 维持了 2 个 aclk clock cycle,只要 destination 3个 clock edge 可以满足在两个 aclk clock cycle,就可以用这个电路实现

一般来说在 1个Clock Destination Period + Uncertainty(Destinatino Domain Setup Time + Hold Time + Clock Skew)即可满足输入pulse太近的问题。所以为了让条件更宽松一点,会建议要间隔1个Sorce Clock Domain Period + 4个Destination Clock Domain Period

在这里插入图片描述
Worst case

讲到这里,我们知道了可以用 double flip-flop synchronizer 解决 CDC problem. 针对 single pulse, 可以在输入与输出加上 XOR gate并搭配转换后的 long pulse 进行同步。

但以上讲的两个方法都只能在 single input signal(例如 flag, 内存读写状态, enable signal … 等) 的情况下做处理,为何 double flip-flop synchronizer 没办法同步 multiple bit ?

答案是因为 double flip-flop synchronizer 同步到 destination clock domain 的 delay 是随机的,可能一个 cycle 就同步好,也可能需要 2 个 cycle.

所以可以看到下图,由于 bdata[0] 比较快同步完成,导致最终逻辑输出不正确。

在这里插入图片描述

  • (3) Asynchronus FIFO synchronizer(Multiple bit)

在讲 Asynchronus FIFO 之前,先科普一下 FIFO。FIFO(First in First out) 即是一个有 入口/出口 的储存空间,同步 FIFO 即表示在对 FIFO 写入或读取是基于同步讯号处里,所以必须有 read/write 两个指标。

下图为 FIFO 操作流程,假设 FIFO 有 8 个 entry, rd_ptr / wr_ptr 皆为 4 bit.

每当对 FIFO PUSH 时, wr_ptr 则会 + 1; 反之 FIFO POP 时,rd_ptr 则会 + 1。当 rd_ptr 追上 wr_ptr 表示目前 FIFO 是空的; 当 wr_ptr 领先 rd_ptr 一圈,表示目前 FIFO 是满的。

在这里插入图片描述在这里插入图片描述
1. FIFO initial state2. FIFO PUSH
rd_ptr = 4’b0000, wr_ptr = 4’b0000rd_ptr = 4’b0000, wr_ptr = 4’b0001
在这里插入图片描述在这里插入图片描述
3. FIFO POP4. FIFO full state( PUSH 8 次)
rd_ptr = 4’b0001, wr_ptr = 4’b0001rd_ptr = 4’b0001, wr_ptr = 4’b1001

但异步 FIFO 在 写入/读取 是于各自的 clock domain, 没办法像同步 FIFO 这样判断 FIFO 的储存状态(满/空),导致 FIFO 在 PUSH 时,可能原本的储存空间已满,进而导致电路异常; 或者是 FIFO 在 POP 时,可能原本的FIFO 是完全没有任何东西,结果拿到空的值。所以必须有个方式能够互相同步 rd_ptr 与 wr_ptr 的资讯给 read control / write control。

解决方法就是使用 gray code 并搭配 double flip-flop synchronizer. 刚刚有提到 double flip-flop synchronizer 没办法对 multiple bit 进行同步,但由于 gray code 在相邻大小之间只有 1 个 bit 不同,如果以 gray code 来当作 counter, 那每次讯号翻转时,其实只有更动其中 1 个 bit, 这样在进行同步的时候自然不会出现问题。

那异步 FIFO 要怎么判断目前是空或满呢? 和同步 FIFO 相同,先假设 8 个 entry, rd_ptr / wr_ptr 用 4 bit 表示。

异步 FIFO 为空的判断方法与同步 FIFO 相同,若 rd_ptr 等于 wr_ptr, 代表此时 FIFO 是空的。

当连续 PUSH 8 次后,此时 FIFO 已满,且 wr_ptr 变为 4’b1100. 可以发现 wr_ptr 与 rd_ptr 在高 2 位都不相同, 低 2 位相同。 于是我们就可以用这个方式判断 FIFO 的储存空间是否已满。

在这里插入图片描述
gray code

讲完所有基本知识后,就可以了解 AFIFO 是怎么运作的,电路图如下。

在这里插入图片描述
AFIFO
  1. wptr / rptr 皆是 gray code
  2. waddr / raddr 皆是 binary code. 因为如果以 gray code 来当作 counter 的时候,可以发现它其实是呈现一个不规律的计数情形,所以在 FIFO memory access 的时候都还是保留用 binary code.
  3. 为什么要使用 FIFO 这个 memory?
    假设一个很快的 clock domain(A) 要同步到一个很慢的 clock domain(B), 但因为 B 处理速度实在太慢了,在当中传递可能会损失掉 A 所传送的部分讯息。所以这里的 FIFO 是提供了一个 buffer 的环境,让资料先写进去,之后 B 要用到的时候再读出来。

code 实现可以看这篇

Appendix

Double Flip-Flop Sync 需要满足 3 个 desitination clock domain edge,但一般设计者没办法保证这件事情,所以有一个更好的保障方法 Closed-Loop Solution

原理大致与原先相同,在 Clock Sorce Domain 多了两个 Flip Flop,接收 Sync 后的数值,当 ap2_dat 成功收到 Sync 后,再通知 adat 关闭控制讯号,以确保 Destination Clock Domain 接收成功。

Reference


via:

“孤独的单刀” 系列全载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值