ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

时序逻辑设计精讲:从触发器原理到Verilog工程实践与STA分析

时序逻辑设计精讲:从触发器原理到Verilog工程实践与STA分析 DDCA 这门课在数字电路领域算是相当经典的存在很多学校的计算机体系结构、数字IC设计课程都拿它当教材或者参考书。之前的 Lesson 01、Lesson 02 主要围绕组合逻辑打转到了 Lesson 03 的时序逻辑感觉才真正摸到了数字电路的核心门槛。这篇内容我打算从工程实操的角度来拆解一遍重点放在“为什么必须有时序逻辑”“触发器内部到底怎么回事”“Verilog 怎么写才不会综合出意外电路”这几个方向再配合一些我在实际项目里踩过的坑希望对正在啃这本书或者在做数字设计的同学有点帮助。1. 时序逻辑到底解决了什么问题1.1 组合逻辑的先天缺陷先说个最直白的问题如果没有时序逻辑数字电路会变成什么样组合逻辑的输出只取决于当前输入这句话听起来很合理但仔细一想就有问题。比如你要实现一个简单的计数器输入是一个时钟信号输出是当前的计数值。用组合逻辑来做的话计数值会随着时钟的每个边沿瞬间变化而且因为没有“记忆”能力下一个状态根本没法根据上一个状态来递推。更麻烦的是组合逻辑电路存在传输延迟信号从输入端传到输出端需要时间这期间输出会产生毛刺。几个组合逻辑级联之后毛刺会被放大整个系统的输出就完全不可控了。用生活化的例子来类比组合逻辑就像一个没有记事本的服务员客人点完菜他转一圈就忘了上一桌点的什么。而时序逻辑相当于给这个服务员配了一个记事本每来一个时钟脉冲他就把当前的状态记录下来作为下一步操作的依据。1.2 时序逻辑的三大核心能力时序逻辑的本质是在组合逻辑的基础上加入了存储元件让电路具备了三大能力。第一是记忆能力。寄存器可以保存数据这个数据在下一个时钟沿到来之前保持稳定不变。这个特性非常关键因为绝大部分计算任务都需要依赖上一拍的结果来推导这一拍的结果比如累加器、移位寄存器本质上都是靠“记忆”在干活。第二是时序控制能力。通过时钟信号把系统行为划分成一个个离散的时间片每个时间片内电路只做一件事不会互相干扰。这也是同步时序电路能够稳定工作的根本原因整个系统被一个统一的节拍器控制着谁先谁后一目了然。第三是流水线能力。有了存储元件之后可以在一级组合逻辑后面插入寄存器把大段的组合逻辑切分成多个小段每一小段单独一拍完成。这样虽然单次计算延迟没有变但整体的吞吐率大幅提升。这个概念在 CPU 架构里用得太多了DDCA 后续讲流水线处理器时就会反复用到。1.3 同步时序与异步时序的区别时序逻辑又分为同步时序和异步时序两大类。同步时序电路中的所有存储元件都由同一个全局时钟信号驱动所有状态变化都发生在时钟边沿。这种设计方式分析起来相对简单时序收敛也容易控制是目前绝大多数数字系统采用的方式。异步时序电路没有统一的时钟靠事件触发或者握手信号来协调各模块的工作。异步电路的优势是功耗低、没有时钟偏斜问题但设计和验证难度都大得多稍有不慎就会进入亚稳态状态导致系统崩溃。实际工程中除非是极低功耗场景或者跨时钟域处理否则不会大面积使用异步设计。2. 触发器的工作原理与参数分析2.1 锁存器与触发器的本质区别很多初学者会混淆锁存器和触发器这两个概念其实差别非常大。锁存器是电平敏感的存储元件使能信号为高电平时输出跟随输入变化使能信号为低电平时锁存当前值。触发器是边沿敏感的存储元件只在时钟信号的上升沿或者下降沿瞬间采样输入其他时间输出保持不变。在实际工程中锁存器通常是不受欢迎的。因为锁存器会导致时序分析变得极其复杂而且综合工具在处理含锁存器的代码时容易产生意想不到的问题。设计规范里经常强调组合逻辑的 always 块中不要遗漏分支条件否则综合器会推断出锁存器这正是我们要尽量避免的情形。触发器则不同它只在时钟边沿采样时序边界非常清晰静态时序分析工具可以精确计算每条路径的建立时间和保持时间是否满足要求。DDCA 这本书花了大量篇幅在触发器的工作原理上目的就是让大家从晶体管层面理解触发器为什么能实现边沿采样这对接下来的时序约束非常有帮助。2.2 建立时间与保持时间建立时间和保持时间是时序逻辑中最核心的参数也是数字 IC 设计面试中几乎必问的问题。建立时间是指在时钟有效沿到来之前数据输入 D 必须保持稳定的最短时间。如果数据在时钟沿之前发生变化触发器的内部节点还没有稳定的时间就无法正确采样当前的输入值。保持时间是指在时钟有效沿到来之后数据输入 D 必须继续保持稳定的最短时间。因为时钟沿触发采样后触发器内部会有短暂的数据传输过程如果输入数据在此时发生变化内部状态会受到干扰。这两个参数是触发器的物理特性由制造工艺决定设计者无法改变。所以时序收敛的本质就是保证在每一条数据通路上建立时间和保持时间都满足要求。通俗点说数据必须在时钟沿到来前提前到达并且到达之后保持一段时间不乱动触发器才能“看清”这个数据。我之前调试过一个比较极端的工作环境温度从常温拉低到零下的时候某些触发器出现了偶发性的采样错误最后定位下来就是低温条件下器件的保持时间变差了数据传输路径上存在竞争差点多了一根保持时间违例。这种问题在逻辑仿真阶段完全看不出来只有做静态时序分析才抓得到。2.3 时序参数的互动关系再来梳理一下时钟周期和数据路径延迟之间的关系。假设时钟周期为 T触发器自身的时钟到输出延迟为 Tclk-q组合逻辑延迟为 Tlogic建立时间为 Tsetup那么必须满足 T ≥ Tclk-q Tlogic Tsetup。这个公式看着简单但实际操作时每个变量都有讲究。Tclk-q 是触发器的固有属性不同工艺库、不同驱动强度的器件差异很大。Tlogic 是所有组合逻辑门的延迟之和路径越长延迟越大。Tsetup 也是器件固有的。所以想要提升系统的最高工作频率就必须压缩组合逻辑的延迟。这也是为什么流水线设计这么重要的原因它通过插入寄存器把长路径切短以流水级数换取更快的时钟频率。3. Verilog 实现时序逻辑的工程要点3.1 时序逻辑的建模方式用 Verilog 描述时序逻辑时核心是掌握 always 块中使用非阻塞赋值的方法。比如下面的例子module counter #( parameter WIDTH 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count {WIDTH{1b0}}; else if (en) count count 1b1; end endmodule这里的非阻塞赋值符号 是时序逻辑的关键它表示右侧的表达式在当前时刻求值但赋值操作会延迟到该始终周期的结束时刻才生效。这样的语义保证了多个 always 并行工作时所有触发器都同步更新不会出现数据竞争和中间态。如果应把阻塞赋值用在时序逻辑里比如always (posedge clk) begin a b; c a; end那就会形成一个串联的赋值链综合出来的电路很可能与你预期的寄存器行为不一致甚至会在仿真中出现意外的延迟特性。这一点在编码规范里是红线强烈建议所有时序逻辑都使用非阻塞赋值。3.2 复位方式的选择同步复位和异步复位是数字设计中讨论比较多的话题。DDCA 书中主要推了异步复位、异步释放的设计方式在 Verilog 里可以这样写always (posedge clk or negedge rst_n) begin if (!rst_n) q 1b0; else q d; end这种写法对复位信号的毛刺比较敏感但好处是复位动作不需要等待时钟沿复位速度更快。真正工程中为了避免复位释放时产生亚稳态需要做“异步复位、同步释放”处理也就是先把复位信号打两拍再送到各个模块。这是一个纯粹的工程经验问题建议在代码里统一封装一个复位同步模块所有模块都使用同步化之后的复位信号。3.3 避免画出意外的锁存器组合逻辑 always 块中经常出现的一种 bug 是条件不完整。比如always (*) begin if (sel) y a; end当 sel 为 0 时y 没有任何赋值综合工具会推断出一个锁存器来维持 y 的原值。这在很多情况下不是设计者的本意而且锁存器天生对时序不友好后期分析会让人非常头疼。解决办法是确保所有分支都有明确的赋值或者在 always 块开头加默认赋值。比如always (*) begin y 0; if (sel) y a; end这样写就稳定多了无论 sel 是什么值y 都有确定的输出不会产生锁存器。3.4 阻塞赋值在组合逻辑中的正确姿势虽然时序逻辑必须用非阻塞赋值但组合逻辑又必须用阻塞赋值。这在初学阶段特别容易弄混。举个例子组合逻辑实现多路选择器always (*) begin case (sel) 2b00: mux_out a; 2b01: mux_out b; 2b10: mux_out c; default: mux_out d; endcase end这里必须使用阻塞赋值 因为组合逻辑要求立即赋值、立即生效。非阻塞赋值会让组合逻辑的输出延迟一拍结果与预期完全不同。关于阻塞和非阻塞的使用原则经验总结就是时序逻辑用非阻塞组合逻辑用阻塞两个规则分开记忆写代码的时候不要混用。4. 时序约束与静态时序分析实战4.1 创建时钟约束写好了时序逻辑代码仿真也通过但到了 FPGA 或者 ASIC 阶段还有一个大坑等着你就是时序约束。如果没有约束综合工具默认一个频率可能跟你的实际需求差了十万八千里。走一遍完整的工程流程你迟早要学会跟约束文件打交道。以 FPGA 开发为例SDC 约束中最基础的是创建时钟命令create_clock -name clk -period 20.0 [get_ports clk]这条命令告诉综合工具时钟端口 clk 的周期是 20ns即 50MHz。工具随后会检查所有从触发器到触发器的数据路径计算每条路径的建立时间裕量。如果路径延迟超过了 20ns 减去建立时间就会报时序违例。实际项目中时钟往往不止一个。比如你有 100MHz 的主时钟又有 25MHz 的 AXI 接口时钟就要分别 create_clock还要加上 set_clock_groups 或 set_false_path 来声明异步时钟域。这一步一开始就要规划好后面再补特别麻烦。4.2 理解静态时序分析报告时序报告看起来信息量很大其实核心就几列数据。一组典型的报告长这样路径类别延迟(ns)说明时钟网络延迟2.0时钟到达源触发器和目的触发器的时间差源触发器 Tclk-q0.5时钟沿到来后输出端才变化的时间组合逻辑延迟12.3中间经过的 LUT / 门电路延迟总和目的触发器建立时间0.3数据必须在时钟沿前稳定下来的时间如果时钟周期为 20ns数据路径总延迟为 2.0 0.5 12.3 0.3 15.1ns那建立时间裕量就是 20 - 15.1 4.9ns属于时序收敛的情况。如果裕量为负这条路径就是违例路径必须想办法优化。4.3 违例路径的优化手段一旦出现建立时间违例常见的优化手段有以下几种。第一是流水线切割。找到最长的那段组合逻辑在中间插入一级寄存器把单拍延迟砍掉一半。代价是数据输出会多延迟一个时钟周期在计算吞吐量时要统一考虑。第二是逻辑重组。比如一个加法树原本是串行加法延迟叠加严重改成并行加法树结构延迟可以显著降低。这一类优化通常需要从算法层面入手。第三是增加时钟周期。如果性能指标允许把工作频率从 100MHz 降到 80MHz时序裕量立马就有了。这个方案虽然简单但在很多场景下是性价比最高的选择。保持时间违例则不同它跟时钟频率无关是因为数据变化太快导致建立时间窗口内数据不稳定。解决方法一般是增加延迟缓冲器比如在数据路径上插入更长的走线或者加入延迟单元。保持时间违例在 ASIC 后端流程中处理得比较多FPGA 设计相对少见因为 FPGA 内部的布线延迟本身就比较可预测。4.4 跨时钟域的实战处理在真实的 SoC 设计里跨时钟域几乎是不可避免的。两个时钟域之间的信号传输最常用的就是两级同步器module sync_2ff #( parameter WIDTH 1 )( input wire clk, input wire rst_n, input wire [WIDTH-1:0] async_in, output wire [WIDTH-1:0] sync_out ); reg [WIDTH-1:0] sync_reg1; reg [WIDTH-1:0] sync_reg2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sync_reg1 {WIDTH{1b0}}; sync_reg2 {WIDTH{1b0}}; end else begin sync_reg1 async_in; sync_reg2 sync_reg1; end end assign sync_out sync_reg2; endmodule两级触发器让亚稳态有足够的时间去稳定下来极大降低亚稳态传播到后续逻辑的概率。但要注意这个方法只适用于单比特信号。多比特数据总线跨时钟域就不能用这个方案了需要用异步 FIFO核心思想其实是利用格雷码或者握手协议来保证数据的一致性。5. 一个综合实例移位寄存器与状态机5.1 串行移位寄存器的设计与应用理论讲完拿实际的例子来串一遍。先看一个 4 位串入串出的移位寄存器这是时序逻辑最经典的应用之一。module shift_reg ( input wire clk, input wire rst_n, input wire sin, output wire [3:0] pout, output wire sout ); reg [3:0] shift_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) shift_reg 4b0; else begin shift_reg[0] sin; shift_reg[1] shift_reg[0]; shift_reg[2] shift_reg[1]; shift_reg[3] shift_reg[2]; end end assign pout shift_reg; assign sout shift_reg[3]; endmodule这种结构常见于串行通信接口中。比如 UART 接收端需要把串行数据逐位移入然后并读一个字节这个设计就完美符合需求。由于使用了非阻塞赋值几个移位操作在同一时钟沿并行执行仿真和实际电路行为完全一致。5.2 状态机的三段式写法时序逻辑的另一个重头戏是有限状态机。DDCA 中把状态机分成 Moore 和 Mealy 两种类型实际工程中我推荐使用三段式的写法。看一段经典的序列检测器示例检测输入序列 1011module seq_detect ( input wire clk, input wire rst_n, input wire din, output reg dout ); localparam S0 3d0, S1 3d1, S2 3d2, S3 3d3, S4 3d4; reg [2:0] state, next_state; // 状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state S0; else state next_state; end // 次态组合逻辑 always (*) begin case (state) S0: next_state din ? S1 : S0; S1: next_state din ? S1 : S2; S2: next_state din ? S1 : S3; S3: next_state din ? S1 : S4; S4: next_state din ? S1 : S2; default: next_state S0; endcase end // 输出逻辑Moore型 always (posedge clk or negedge rst_n) begin if (!rst_n) dout 1b0; else if (state S4) dout 1b1; else dout 1b0; end endmodule三段式的优势在于状态转移、次态计算、输出逻辑三者分离代码清晰易维护。调试时只关注 state 的变化就能快速定位问题。而且三段式的输出逻辑是时序输出不产生毛刺非常适合高速场景。5.3 状态机编码方式的选择状态机的编码方式也有讲究。二进制编码占用寄存器最少但相邻状态跳转可能有多位同时变化毛刺和功耗相对偏高。独热码每个状态只用一个比特译码逻辑简单跳转时只有一位变化时序性能好代价是寄存器数量多。在 FPGA 的 6 输入 LUT 架构下状态机的状态数如果不太多独热码通常比二进制编码跑得快。资源紧张的话可以尝试用综合工具的编码策略指导命令让工具自动选择。编写代码时建议用 localparam 或 parameter 定义状态并给综合工具加上状态机编码的约束比如 Synplify 或 Vivado 中都有相关属性设置。6. 时序逻辑设计与调试心得6.1 如何用波形图看时序纸上谈兵不如直接拿仿真波形来看。很多人上来就看信号是否等于预期值其实更高效的看法是抓住时钟沿附近的信号变化趋势。比如一个计数器每个上升沿 count 加 1那波形看起来应该是一段阶梯形状。如果波形中间出现了毛刺或者 count 在非时钟沿产生了变化那大概率是异步逻辑或组合逻辑混入了时序逻辑。调试的过程中我习惯先把所有的控制信号都拉出来看比如复位信号、使能信号、时钟是否正常然后分层观察数据路径。这比盯着一根信号胡思乱想快得多。如果项目规模大还可以用断言 SVA 来监控时序关系比如一发数据在 valid 拉高时 data 必须保持稳定这种断言能帮你在回归验证中自动发现时序异常。6.2 时钟域中的“坑”与避坑经验一些开发者在做仿真时经常遇到现象是 RTL 仿真全对上板之后偶发出现数据错误。这类问题有个比较典型的来源是跨时钟域信号没有做同步处理。RTL 仿真默认是零延迟的两个时钟域的信号采样不到亚稳态那层只有上了真芯片才会出现。正确的操作是在上板之前就先梳理好所有跨时钟域的接口确认单比特信号都经过了两级同步多比特信号都经过了异步 FIFO 或者握手电路。而且仿真中最好加一些抽象的延时模型来模拟真实的亚稳态行为避免“仿真完美、上板翻车”。6.3 时序违例的整理与排查规范化的时序调试流程大致分为五步。先看时钟约束是否正确再挑最差路径看是哪一类违例是 setup 还是 hold然后找到路径上的关键器件和走线再尝试优化局部逻辑和走线最后回归验证所有场景。记录每轮修改对时序和面积的影响就能积累出准确的时序调试知识库。我在项目里会把违例路径的完整报告导出来按信号名排序专门建一个表格记录每一轮的优化动作、延迟变化和最终状态。这对于多模块合作的项目尤其有用因为每个人改动的部分可能互相影响有了记录才不至于来回拉扯。6.4 时序逻辑的常见问题速查表表格内容整理自实际开发经验遇到类似情况可以拿出来对照排查现象可能原因解决方法仿真正常上板错误跨时钟域未同步加两级同步器或异步FIFO计数器跳变异常复位信号未同步释放做异步复位同步释放输出出现毛刺组合逻辑竞争冒险输出寄存器化处理综合出锁存器组合逻辑条件分支不完整补默认赋值建立时间违例组合逻辑路径太长流水线切割或逻辑重组保持时间违例数据变化太快后端插延迟缓冲器状态机跑飞状态编码不安全增加异常态恢复机制这大概就是时序逻辑从理论到实践的完整链路了。DDCA 里关于时序逻辑的章节写得比较精炼但每一个点延伸出来都是实际工程里必须面对的问题。尤其是非阻塞赋值的使用、建立保持时间的计算、跨时钟域的处理这几块儿如果能在理论阶段就吃透后面做工程会顺手很多。最后留下一句话时序逻辑的核心不是“锁存”而是“同步”把这个观念贯穿到代码和电路设计里很多奇怪的 bug 是可以提前避免的。
返回列表