本页属于一个关于时序的系列页面之一。
关于本页
如果不了解逻辑设计中的几个基本概念,就不可能正确处理时序问题。本页将解释一些基础概念,这些概念是本系列其余页面的基础。
下面要讲的内容,在高校的逻辑设计课程中都会讲授。不过,并不是每个使用 FPGA 的人都上过这类课程,即使上过,也不一定全都记得。本页的目的就是补上这些可能缺失的知识。
本页中你已经掌握的部分可以随意跳过,但还是建议通读一遍,哪怕是快速浏览也好。本页最后几节对理解本系列后面的页面尤其重要。
快速回顾
在进入与时序相关的概念之前,我先快速回顾逻辑设计中的一些相关术语。如果其中有你还不熟悉的术语,建议先把它弄清楚,再继续阅读本页。
首先是组合逻辑(combinatorial logic)。这个术语用于描述诸如逻辑门一类的元件:与门、或门、非门等。这些逻辑元件被视为组合逻辑,是因为它们没有存储能力。换句话说,它们的行为不取决于过去发生过什么,输出端的值只取决于输入端的值。
组合逻辑的一个重要类型是查找表(look-up table,简称 LUT)。这是一种多用途逻辑元件,可以实现任何取决于若干个输入的逻辑函数。查找表之所以重要,是因为 FPGA 内部几乎所有的组合逻辑都由它实现。FPGA 内部的 LUT 以异步 ROM(即没有时钟的 ROM)的形式实现:输入被当作 LUT 内部数据的地址。输出通常包含一位或两位。
市场上几乎所有 FPGA 的 LUT 都有 4 个或 6 个输入,因此每个 LUT 中的存储单元数为 16 或 64。
下一个重要术语是时序逻辑(sequential logic)。它包含触发器(flip-flop)、同步 RAM,以及许多其他需要时钟才能工作的基本构建模块。所有这些逻辑元件都会响应于时钟的某个特定变化而被激活。在这些激活时刻之间,时序元件会忽略输入、保持内部状态,并且不改变输出。
例如,上升沿触发的逻辑元件在时钟从低变高时被激活,也就是在时钟上升沿被激活。同样,下降沿触发的逻辑元件在时钟从高变低时被激活,也就是在时钟下降沿被激活。还有在时钟两个边沿都会被激活的逻辑元件,但这种元件几乎只用于对 I/O 信号进行采样(sampling),或用于产生 I/O 信号。
所有时序元件都有某种存储能力。这是以下事实的直接推论:在时钟边沿到来之前,它们不会改变输出。有些时序元件的存储量最小,每个输出恰好只有一位存储;另一些时序元件则有更多存储,例如移位寄存器和 RAM。
就讨论时序而言,只需记住两个重要事实:输入只在时钟边沿到来时被采样;输出几乎只在时钟边沿到来后发生变化。唯一的例外是,某些时序元件带有异步复位(asynchronous reset)输入端。当这个输入有效时(高电平有效还是低电平有效取决于具体元件),无论时钟处于什么状态,时序元件的内部状态都会立刻变为预定义值。因此,该元件的部分或全部输出也会立刻变为已知值。
在 FPGA 中,有些时序元件带有异步复位,有些不带。异步复位几乎是 FPGA 上几乎所有时序元件唯一可能存在的异步输入,尽管逻辑设计理论中确实包含一些具有多个异步输入的元件,例如 S-R 触发器(S-R flip-flop)。
从现在开始,我只讨论触发器
为简单起见,我假设所有时序元件都是上升沿触发的触发器,也就是响应时钟上升沿而激活的触发器。换句话说,这里的所有时序元件都用触发器来表示。这些触发器只在时钟从低电平变为高电平时才响应输入并更新输出。
这样理解时序会更容易,而且对触发器的讨论也很容易推广到任何其他时序元件。
建立时间与保持时间
为了保证触发器正确、可靠地工作,其输入必须满足两个时序要求,即在相应的时间段内保持稳定(即数值不变):
- 在时钟边沿之前的一段时间内,输入必须保持稳定。这段时间称为建立时间(setup time),记作 tsu。
- 在时钟边沿之后的一段时间内,输入也必须保持稳定。这段时间称为保持时间(hold time),记作 thold。
上图说明了 tsu 和 thold 的含义:D 输入不允许在黄色标记的时间段内改变数值。在这个例子中,D 从高电平变为低电平的时刻符合时序要求,也就是说,发生在黄色时间段之外。
理解这两个时序参数的一种方式是:显然,如果一个触发器的输入恰好与时钟边沿同时发生变化,那么触发器究竟会把该输入当成高电平还是低电平,就不清楚了。它该选哪个?是时钟边沿之前的状态,还是之后的状态?
接着可以问:输入“恰好与时钟边沿同时”变化是什么意思?这个“恰好”要精确到什么程度?tsu 和 thold 这两个参数定义的就是:输入值在离时钟边沿多近的范围内发生变化是安全的。所有时序元件都以同样的含义定义这两个参数。
需要强调的是,如果违反了这个时序要求,后果可能远比“时钟边沿之后触发器的输出是随机的”更严重。这主要有两个原因:
- 在某些情况下,FPGA 工具会在工程实现过程中自动复制设计中的触发器,尤其是为了降低扇出(fan-out)。这样做之后,依赖该触发器输出的一部分逻辑被连接到其中一个触发器上,另一部分逻辑则被连接到第二个触发器上。只要两个触发器的输出始终相同,这没有问题。但是,如果时序要求(tsu 和 thold)被违反,这两个触发器的输出就可能出现不同的值。
- 如果时序要求被违反,触发器还可能进入亚稳态(metastability)。在这种状态下,触发器的输出在短时间内既不是高电平也不是低电平。关于亚稳态及其不良后果的更多内容,请参见此页面。
时钟到输出时间
所有时序元件都要定义的第三个参数是时钟到输出时间(clock-to-output)。它有好几种常见符号,例如 tcko、tco、tC_Q 等。这个参数不是一项时序要求,而是说明时序元件的输出(Q)何时保证有效。更准确地说,它表示时钟边沿过后多久,输出才是有效的(见上面的时序图)。
实际上,关于这一点有两个参数:
- 最大时钟到输出时间:从时钟边沿到输出变得有效需要多长时间。
- 最小时钟到输出时间:时钟边沿之后,输出保证保持不变的时长。
大多数时候,只有最大时钟到输出时间是人们关心的,所以当数据手册给出这个参数时,几乎可以断定给出的是最大值。
请注意,如果触发器的时序要求(tsu 和 thold)被违反,就无法保证输出到何时才会稳定。这种情况下,由于触发器有可能短暂地处于未定义状态(亚稳态),最大时钟到输出参数也就失去意义了。
tsu 和 thold 可以为负值
尽管上图把 tsu 和 thold 都画成了正值,但这两个参数中的某一个确实可能为负。实际上,FPGA 内部的触发器经常出现这种情况。
例如,tsu 为负意味着数据不一定要在时钟边沿到达时就已经稳定,而是可以在时钟边沿过后稍稍晚一点才达到稳定值。但 tsu 仍然限定了“稍晚一点”最多能晚多久。
同样,thold 为负允许数据在时钟边沿之前发生改变。当然,允许提前多久仍然有限制,这个限制由 thold 体现。
正因为 tsu 和 thold 可以为负,所以从理论上说,有可能定义出毫无意义的时序要求。
为了解释这一点,用 tsamp 表示触发器实际对其输入进行采样的时点。tsamp 事先未知,但 tsu 的定义告诉我们 −tsu < tsamp。换句话说,触发器是在数据发生变化并稳定之后才进行采样的,这正是 tsu 所保证的。同理,还有 tsamp < thold:采样必须发生在数据可能因时钟边沿而改变之前。把两者合在一起,就得到 −tsu < tsamp < thold,也就是 −tsu < thold。对任何一对 tsu 和 thold,这个不等式都必须成立。
因此,只要满足上述条件,tsu 或 thold 中的某一项可以为负,但两者不可能同时为负。
时钟到输出参数永远为正:触发器不可能在时钟边沿到达之前就对该边沿作出反应。相比之下,请注意,tsamp 可以为负并不意味着采样发生在时钟边沿之前;这种负值可能是由数据信号的延迟造成的。
传播延迟
上图展示了一个简单的连接:两个触发器之间有一个 LUT。为简单起见,我们假设 LUT 的输出只取决于 I1。例如,下面这段 Verilog 代码就可能导致这样的逻辑,此时该 LUT 实现的是一个非门:
always @(posedge clk)
begin
foo_reg <= foo; // FF1 = foo_reg
bar <= !foo_reg; // FF2 = bar
end
注意,两个触发器连接的是同一个时钟。那么这个时钟的最高频率是多少?
要回答这个问题,还缺一项信息:从 FF1 的输出出现稳定值,到 FF2 的输入变为稳定值,需要多长时间?把这段时间记为 tpd(传播延迟,propagation delay)。
请注意,传播延迟这个术语总是相对于某一段具体的组合逻辑而言的;必须明确它指的是哪一段。例如,也可以把从 LUT 的 I1 到这个 LUT 的 O(即从输入到输出)的时间定义为传播延迟,而这个延迟很可能与前面定义的 tpd 不同。
在 FPGA 上尤其如此:FF1 的输出与 LUT 的输入之间还有布线延迟(routing delay)。所以事实上,FF1 的 Q 和 LUT 的 I1 并不是同一点,信号在这两点之间传播需要时间。
正因为存在这种歧义,FPGA 数据手册中很少出现 tpd 这样的符号。而且,即使手册中给出了传播延迟,通常也会明确写出该参数的准确含义。
稍后我还会回到时钟最高频率这个话题。
路径
要给路径(path)下一个简洁的定义并不容易,不过前面已经看到一个例子。上面我把 tpd 定义为 FF1 的 Q 到 FF2 的 D 之间的传播延迟。这个 tpd 与一个具体过程相关:FF1 的值发生变化,更新后的值到达 LUT,LUT 随之更新输出,最后更新后的值到达 FF2。这一连串事件从唯一一个信号(FF1 的输出)发生改变开始,到另一个位置上的信号(FF2 的输入)稳定时结束。
因此,tpd 定义为从 FF1 的 Q 到 FF2 的 D 这条路径的传播延迟,或者简称为从 FF1 到 FF2 的路径。
路径由这一过程从开始到结束之间造成延迟的所有要素组成。路径中有两类要素:
- 组合逻辑元件:它们带来延迟,是因为电子电路需要一定时间才能更新输出端的信号。这类延迟通常称为逻辑延迟。在上面的例子中,LUT 就是这类元件。
- 布线(routing)。布线就是逻辑元件之间的导线。这些线段带来延迟,一方面是因为信号在空间中传播需要时间,另一方面是因为电压变化还需要沿途的电容充电。
路径的意义在于计算它的传播延迟。计算结果如何使用,下面马上说明。
路径通常代表这样一个思想实验:一个触发器的输出发生变化,我们沿着一条特定路线观察它如何到达另一个触发器的输入。在这个思想实验中,当第一个触发器的输出变化时,一支假想的秒表开始计时;当第二个触发器的输入发生变化时,秒表停止。
这个思想实验用于回答两个问题:秒表显示的时间是否太长?如果太长,就说明 tsu 的要求被违反。第二个问题是,这个时间是否太短?如果太短,就说明 thold 被违反。
还要注意,路径所经过的路线只包含布线和组合逻辑。因此,信号在终点稳定所需的时间只取决于路径上的组合逻辑元件和布线。无论这个思想实验在什么时候进行,结果都是一样的。
在真实的 FPGA 设计中,每个触发器的输入通常有许多条路径到达,每个触发器的输出也引出许多条路径。实际上,同一对触发器之间也可能存在不止一条路径。不过,时序计算总是假设只有一个触发器改变了输出,而 FPGA 逻辑中发生的一切都是这个改变的直接结果。FPGA 设计中需要计算的路径数量可能极其庞大,但这当然是由软件自动完成的。
一个简单的静态时序分析
为了演示,我对上面那个含两个触发器的例子做一个简单的时序分析。时序约束(timing constraints)这个主题以后还会讨论;目前先假设 @clk 的频率为 250 MHz(4 ns),并且直接连接到逻辑电路上(也就是不经过锁相环(PLL);实际设计不推荐这样做,但这能简化时序分析)。相应的时序约束(用 SDC 风格写)大致如下:
create_clock -period 4.000 -name clk [get_ports clk]
本系列下一页会给出一个真实时序分析的例子,但那个分析比较精确,因此包含很多细节,反而不容易看懂。所以这里先用一个简单分析来说明原理。
这个分析执行的就是上面提到的思想实验:一支假想的秒表从 @clk 的上升沿开始计时。下面是一连串事件,以及每个事件贡献的(虚构)延迟。
- FF1 的时钟到输出时间(0.2 ns):FF1 的输出(Q)更新为触发器输入(D)上数值所需的时间。
- 到 I1 的布线延迟(0.3 ns):信号从 FF1 的输出端传到 LUT 的输入端(I1)所需的时间。
- LUT 的传播延迟(0.3 ns):在任一输入(本例中为 I1)改变之后,LUT 更新其输出(O)所需的时间。
- 到 FF2 输入的布线延迟(0.4 ns):信号从 LUT 的输出端传到 FF2 的输入端(D)所需的时间。
这条路径的传播延迟 tpd 是所有这些延迟之和:0.2 + 0.3 + 0.3 + 0.4 = 1.2 ns。为了举例,假设 FF2 的 tsu 为 0.1 ns。这意味着 FF2 的输入(D)必须在 @clk 下一个上升沿之前 0.1 ns 稳定下来。换句话说,允许的最大 tpd 是 4 - 0.1 = 3.9 ns。
但 tpd 只有 1.2 ns,所以按这个计算,该路径达成时序约束的裕量很大。这个裕量称为时序裕量(slack),本例为 3.9 - 1.2 = 2.7 ns。当这个数值出现在软件的时序计算结果里时,可以反映工具是否花了很大力气才满足时序约束:如果 slack 接近零,往往说明软件为了让这条路径满足时序要求费了很大劲。
传播延迟还可以让我们算出:仅就这条路径而言,时钟 @clk 最高能跑多快。tpd 是 1.2 ns,而 tsu 的要求又意味着下一个上升沿可以在数据稳定后过 0.1 ns 再到来。换句话说,两个上升沿之间至少要有 1.3 ns,这对应约 769 MHz 的频率。这是一个非常高的频率,但由于该路径只包含一个 LUT,这个结果是合理的。实际逻辑通常比这复杂得多,所以实际工作频率往往低得多。
真实的静态时序分析做的就是这种计算,但这只是其中的一部分。在上面的计算中,这条路径在实际计算里称为数据路径(data path)。不过,真实的静态时序分析还会考虑:时钟边沿并不是同时到达两个触发器的。这是因为时钟缓冲器到每个触发器的延迟略有不同,这些延迟之差称为时钟偏斜(clock skew)。另外,由于时钟抖动(jitter),各个时钟边沿之间的时间间隔也不是完全相等的。这些与时钟有关的问题会让精确计算更加复杂,如下一页所示。
恢复时间与撤销时间
如果一个触发器带有异步复位输入端(你确定你需要它吗?),那么对这个输入什么时候变无效是有要求的。注意,复位什么时候变有效并不重要,因为触发器反正都会进入某个已知状态。
但是,当复位变无效时,触发器又开始对时钟敏感。如果复位的撤除恰好发生在某个时钟边沿附近,那么触发器是否应该对该边沿作出响应就不确定了。与建立时间和保持时间类似,在时钟边沿前后的一段时间内,复位信号必须保持稳定。更具体地说:
- 在时钟边沿之前的一段时期内,复位信号不得从有效变为无效。这段时间称为恢复时间(recovery time)。这是触发器从复位状态恢复过来、为时钟边沿做好准备所需的时间。
- 在时钟边沿之后的一段时期内,复位信号也不得从有效变为无效。这段时间称为撤销时间(removal time)。
这些定义与建立时间、保持时间的定义相似,这并不是巧合:恢复时间是一种特殊的建立时间。时序分析的做法也相同。唯一区别在于,无论数据信号的值是什么,建立时间都必须满足;而恢复时间在异步复位信号变有效时并不强制作用。撤销时间与保持时间的关系也一样。
由于这一相似性,恢复时间和撤销时间在这里就不再进一步展开了。另外请注意,上面所说的一切适用于所有异步输入,而不仅仅是异步复位。
RTL 范式与时序
上面那个含两个触发器的例子虽然简单,但它代表了所有用 RTL 范式描述的逻辑:每条路径都起始于一个触发器,终止于另一个触发器,并且两个触发器由同一个时钟(或一个相关时钟,related clock)驱动。路径本身由组合逻辑和布线组成。在本例中,组合逻辑只是一个 LUT,但这与路径中包含多个逻辑元件并无本质区别,结构是一样的。
RTL 范式之所以如此重要,是因为使用这种方法时,几乎所有数据路径都具有同样简单的结构。又由于逻辑设计中路径的数量往往极其庞大,简单的时序分析有助于避免错误。此外,时序分析具有固定模式,这使人们能够阅读时序报告并判断它是否合理。
因此,在尽可能的范围内,所有路径都应起始于一个时序元件并终止于一个时序元件。无论编写 Verilog 代码,还是一般性地规划逻辑结构,这都是一个有用的准则。
到这里,关于时序约束背后理论的简要介绍就结束了。时钟周期约束及相关时序报告的说明,请见下一页。


