1. 从组合逻辑到时序逻辑:为什么“记忆”是数字电路的分水岭
如果你已经学过了组合逻辑电路,可能会觉得数字电路的世界就是一堆与门、或门、非门,输入一变,输出立刻跟着变,干净利落。但当你开始接触“时序逻辑电路”时,那种感觉就像是从二维平面突然跳进了三维空间——电路有了“记忆”,有了“过去”,输出不再仅仅取决于当前的输入,还取决于电路“之前”的状态。这,就是数字电子技术从静态走向动态、从简单功能走向复杂系统的关键一步。
我刚开始学这部分时,也卡了很久。为什么需要“记忆”?一个简单的例子就能说明:想象一个楼道里的触摸延时灯。你触摸一下开关(输入一个脉冲),灯亮了。这个“亮”的状态,并不会在你手离开开关的瞬间就熄灭,而是会持续一段时间。这个“持续”的过程,就是电路记住了“你刚才触摸过”这个事件,并在一段时间内维持输出。这就是一个最基础的时序逻辑电路(虽然实际可能用模拟电路实现,但逻辑是相通的)。没有时序逻辑,就没有计数器、没有寄存器、没有我们手机里的CPU和内存,数字世界将不复存在。
本章的“时序逻辑电路”,是整个数字电路设计的核心与难点。它不像组合逻辑那样直来直去,你需要同时考虑“当前输入”和“电路的历史状态”,设计工具也从真值表升级为状态图、状态表。很多人在这里会感到抽象和混乱,别担心,这很正常。接下来,我会带你拆解时序逻辑的每一个核心部件,从最基础的存储单元——触发器开始,一步步构建出计数器、寄存器等复杂功能模块,并分享我在学习和实践中总结的、那些教科书上不一定写的“避坑指南”和设计心法。
2. 时序逻辑的基石:深入理解各类触发器的“脾气秉性”
时序逻辑电路之所以能“记忆”,全靠其中的基本存储单元:触发器。你可以把它理解为一个能锁住一位二进制数据(0或1)的最小记忆细胞。所有复杂的时序电路,无论是计数器还是移位寄存器,都是由触发器组合而成的。因此,吃透触发器,是学好本章的第一道关卡。
2.1 核心概念:时钟、现态与次态
在深入具体触发器之前,必须建立三个核心概念:
- 时钟信号:这是时序电路的“心跳”或“指挥棒”。绝大多数时序电路的动作(如状态的改变、数据的存储)都与时钟信号同步。时钟通常是一个周期性的方波。触发器在时钟的特定时刻(如上升沿或下降沿)检查输入,并决定输出如何变化。没有时钟同步的电路称为异步时序电路,设计起来更复杂,容易产生“竞争冒险”,初学者应先掌握同步时序电路。
- 现态:指触发器在当前时钟沿到来之前,所保持的输出状态,通常用Q^n表示。
- 次态:指在下一个时钟沿到来之后,触发器将要进入的新状态,通常用Q^{n+1}表示。
时序逻辑的分析与设计,核心就是找出次态(Q^{n+1)与现态(Q^n)以及输入之间的逻辑关系。
2.2 SR触发器:最原始的双刃剑
SR触发器(Set-Reset)是最基本的触发器,它清晰地揭示了所有触发器工作的底层逻辑:通过两个互斥的输入来控制状态。
- S(Set):置位端,有效时(通常为1),强制输出Q=1。
- R(Reset):复位端,有效时(通常为1),强制输出Q=0。
它的特性表很简单:
| S | R | Q^{n+1} (次态) | 说明 |
|---|---|---|---|
| 0 | 0 | Q^n | 保持,记忆原有状态 |
| 0 | 1 | 0 | 复位,输出0 |
| 1 | 0 | 1 | 置位,输出1 |
| 1 | 1 | 不定态 | 禁止状态 |
注意:SR=11是绝对禁止的!这是因为当S和R同时有效时,两个强制信号冲突,会导致输出Q和Q'(Q的非)同时为1,这违背了互补输出的原则。当信号同时撤去后,最终状态无法预测(取决于门电路的微小延迟),电路处于亚稳态,这是灾难性的。因此,任何使用SR触发器的设计,都必须通过外部逻辑保证不会出现S=R=1的输入组合。
实操心得:虽然现在直接使用基本SR触发器的场景很少(因为它没有时钟控制,是电平触发的),但理解它至关重要。很多集成触发器(如带使能的D触发器)的内部逻辑单元仍然是SR结构的变体。当你看到电路出现不可预测的震荡或错误时,第一个要怀疑的就是是否无意中产生了类似“S=R=1”的冲突条件。
2.3 JK触发器:解决SR禁区的全能选手
JK触发器可以看作是SR触发器的“完全体”,它完美解决了SR触发器中的禁止状态问题。
- J:功能类似S(置位)。
- K:功能类似R(复位)。
它的神奇之处在于当J=K=1时:此时,触发器的次态将是现态的取反,即Q^{n+1} = NOT(Q^n)。这个“翻转”功能使得JK触发器功能非常完备。
| J | K | Q^{n+1} | 说明 |
|---|---|---|---|
| 0 | 0 | Q^n | 保持 |
| 0 | 1 | 0 | 复位 |
| 1 | 0 | 1 | 置位 |
| 1 | 1 | Q^n' (取反) | 翻转 |
JK触发器功能强大,理论上可以构成任何其他类型的触发器。但在大规模集成电路中,最常用的却是下面这位——D触发器。
2.4 D触发器:数字系统中的“搬运工”与“仓库”
D触发器是当今数字系统(如FPGA、CPU)中应用最广泛的触发器,没有之一。原因就在于它的简单与可靠。
- D(Data):数据输入端。
- 其特性极其简洁:在有效时钟沿(如上升沿),将D端的数据“采样”并锁存到输出Q。即Q^{n+1} = D。
这意味着,时钟沿时刻D是什么,输出Q就变成什么。它就像一个受时钟严格控制的单比特数据通道或仓库。
为什么D触发器如此受欢迎?
- 无禁止状态:D端只有一根数据线,从根本上避免了SR/JK中两路输入可能冲突的问题。
- 设计简单:次态方程就是Q^{n+1}=D,在逻辑设计和状态机化简时异常方便。
- 适用于流水线:其“直通”特性非常适合构建寄存器、移位寄存器,实现数据的同步传递和暂存。
在Verilog或VHDL硬件描述语言中,当你写下一句always @(posedge clk) q <= d;,综合工具生成的基本上就是D触发器。
2.5 T触发器:专为计数而生
T触发器可以看作是JK触发器在J=K情况下的特例,或者D触发器接成翻转模式。
- T(Toggle):翻转控制端。
- 特性:当T=1时,每次有效时钟沿到来,输出翻转一次;当T=0时,保持原状态。即Q^{n+1} = T XOR Q^n。
T触发器的天然用途就是构成二进制计数器。一个T触发器就是一个1位二进制计数器(0->1->0...)。将多个T触发器级联,就能构成多位计数器。
选型经验谈:在实际芯片选型或FPGA设计中,你几乎不会直接去库里面找一个叫“T触发器”的元件。因为D触发器通过外部加一个异或门(Q^{n+1} = D = T XOR Q^n)就可以轻松实现T触发器的功能,而JK触发器也可以通过简单连接(J=K=T)实现。所以,现代数字设计的基础存储单元几乎清一色是D触发器,其他功能通过外部组合逻辑来实现。理解T触发器的行为,更多的是为了理解计数器的工作原理。
3. 时序电路的分析:从电路图到行为描述
给你一个由触发器和门电路构成的时序电路,你如何知道它是干什么的?这就是时序电路分析要解决的问题。分析是设计的基础,只有会分析,才能验证自己的设计是否正确。这里我分享一个标准化的四步分析法,并用一个具体例子贯穿。
假设我们分析一个由两个JK触发器构成的电路,其输出为Q1Q0。
3.1 第一步:写出驱动方程与时钟方程
驱动方程,就是每个触发器输入端的逻辑表达式。我们需要根据电路图,写出每个触发器输入信号(J, K, D, T等)与电路输入、其他触发器现态之间的关系。
例如:
- 对于第一个JK触发器FF0:J0 = X ⊕ Q1^n,K0 = 1(假设X是外部输入)
- 对于第二个JK触发器FF1:J1 = X · Q0^n,K1 = X'(X‘表示X的非)
时钟方程通常很简单,在同步电路中,所有触发器都接在同一个时钟CLK上,即CLK0 = CLK1 = CLK。如果时钟不同,则需要特别关注,那是异步时序电路。
3.2 第二步:推导状态方程
状态方程,就是每个触发器次态的表达式。将第一步得到的驱动方程,代入该类型触发器的特性方程。
JK触发器的特性方程是:Q^{n+1} = J·Q' + K'·Q
那么:
- FF0的状态方程:Q0^{n+1} = J0·Q0'^n + K0'·Q0^n = (X ⊕ Q1^n)·Q0'^n + 0·Q0^n = (X ⊕ Q1^n)·Q0'^n
- FF1的状态方程:Q1^{n+1} = J1·Q1'^n + K1'·Q1^n = (X·Q0^n)·Q1'^n + X·Q1^n(因为K1=X‘,所以K1'=X)
这一步是代数运算,需要细心,特别是处理“非”运算的时候。
3.3 第三步:列出状态转换表与状态图
这是将抽象的方程转化为直观行为的关键一步。状态转换表列出了在所有可能的现态组合和输入组合下,对应的次态和输出。
我们假设电路输出Y = Q1。那么,对于两个触发器,现态有4种组合(Q1Q0 = 00, 01, 10, 11),输入X有0和1两种。总共需要计算8种情况。
以现态为00,X=0为例:
- 计算Q0^{n+1}: (0 ⊕ 0)·1 = 0·1 = 0
- 计算Q1^{n+1}: (0·0)·1 + 0·0 = 0
- 输出Y = Q1^{n+1} = 0 所以,在(00,0)条件下,次态为00,输出0。
依次计算所有8行,就得到了完整的状态转换表。根据此表,可以绘制出更直观的状态图。状态图用圆圈表示状态(圈内写状态值如00),用箭头表示状态转换,箭头上标注“输入/输出”。例如,从状态00到00的箭头上会标“0/0”。
状态图是理解电路功能的“地图”,一眼就能看出电路有几个状态,是如何在输入驱动下流转的。
3.4 第四步:分析逻辑功能与自启动检查
通过观察状态图,我们可以判断电路功能。例如,如果状态图形成一个循环,可能是计数器;如果状态转移与输入序列有关,可能是序列检测器。
一个极其重要的检查点:自启动能力。不是所有状态图里的状态都是“有用”的。设计者通常只规划了部分状态(有效状态),但触发器组合可能产生的状态总数是2^n(n为触发器数量)。那些未被规划的状态称为无效状态或“无关状态”。
在状态转换表中,我们需要计算如果电路因为干扰或上电偶然进入了这些无效状态,在下一个时钟沿它会去哪里。
- 能自启动:从任何无效状态出发,经过一个或几个时钟周期后,能自动进入有效状态循环。
- 不能自启动:从某些无效状态出发,会陷入无效状态循环(死循环)或无法回到有效循环。
不能自启动的电路是不可靠的,在实际应用中必须通过修改设计(例如,利用无关项化简时谨慎选择)或增加上电复位电路来避免。
踩坑实录:我曾设计过一个模5计数器,用了3个触发器(本应有3个无效状态)。仿真时只遍历了有效状态,一切正常。但硬件实测中,偶尔会发现计数器“卡死”不计数。后来才发现,在化简次态卡诺图时,我为了得到最简表达式,将无效状态的次态随意地圈入了某些包围圈,导致其中一个无效状态的次态是另一个无效状态,形成了死循环。解决方案是:在化简时,强制指定无效状态的次态为某个特定的有效状态(如000),虽然得到的逻辑表达式可能不是最简的,但保证了电路的自启动性。这个教训让我深刻理解到,时序逻辑设计不能只追求逻辑最简,可靠性永远是第一位的。
4. 时序电路的设计:从需求到电路图
分析是“逆向工程”,设计则是“正向创造”。当你需要实现一个特定功能(比如一个检测“1101”序列的检测器)时,如何用触发器搭出来?以下是标准设计流程。
4.1 第一步:逻辑抽象,建立原始状态图/表
这是最难也最需要创造力的一步。你需要把文字描述的需求,转化为一个状态机模型。
- 确定输入/输出变量:序列检测器,输入就是串行数据线X,输出就是检测到序列时的指示信号Y。
- 定义状态含义:状态代表“记忆”的内容。对于序列“1101”,我们可以定义:
- S0:初始状态,没检测到任何有效位。
- S1:检测到了第一个‘1’。
- S2:在S1基础上,又检测到了一个‘1’(即已收到“11”)。
- S3:在S2基础上,检测到了一个‘0’(即已收到“110”)。
- (当在S3状态且输入为‘1’时,输出Y=1,并跳转到下一个状态,可能是S1或S0,取决于是否允许重叠检测)。
- 画出状态转换图:根据状态含义,画出所有可能的状态转移。例如,在S0状态,输入X=0则停留在S0,输入X=1则进入S1。
这一步常犯的错误是状态定义冗余或遗漏。一个技巧是:状态应该记住为了产生正确输出所必需的历史信息。对于序列检测,需要记住的就是“最近接收到的、与目标序列前缀匹配的部分序列”。
4.2 第二步:状态化简
原始状态图可能包含多余的状态。如果两个状态在所有可能的输入序列下,不仅输出相同,而且次态也相同(或等价),则这两个状态可以合并为一个。化简可以使用观察法或隐含表法。化简后的状态图更简洁,需要的触发器数量更少(状态数N与触发器数n满足 2^{n-1} < N ≤ 2^n)。
4.3 第三步:状态编码
为化简后的每个状态分配一个唯一的二进制代码。这就是触发器的输出组合。编码方式会影响后续组合电路的复杂程度。
- 顺序编码:如S0=00, S1=01, S2=10, S3=11。简单直观。
- 格雷码编码:相邻状态间只有一位变化(如S0=00, S1=01, S2=11, S3=10)。这可以减少状态转换时多个触发器同时翻转带来的毛刺和功耗,在计数器设计中尤其常用。
- One-hot编码:每个状态用一位触发器表示,如S0=0001, S1=0010, S2=0100, S3=1000。需要更多触发器,但组合逻辑非常简单,在FPGA设计中有时效率更高,因为FPGA内部有丰富的触发器资源。
4.4 第四步:选定触发器类型,求驱动方程和输出方程
根据编码后的状态转换表,我们可以列出每个触发器的次态(Q^{n+1})与现态(Q^n)和输入(X)的真值表。然后,根据我们选定的触发器类型(通常是D触发器,因为设计简单),反过来推导出每个触发器数据输入端(D)应该是什么逻辑。
例如,对于D触发器,它的次态就是D端的值。所以,我们实际上是为每个D端设计一个组合逻辑电路,这个电路的输入是当前状态和外部输入,输出就是该D端的值。
同时,输出Y也是当前状态和输入(对于米利型)或仅是当前状态(对于摩尔型)的组合逻辑函数。
这一步的核心工作是利用卡诺图进行化简,得到最简的与或表达式(或者适合特定工艺的表达式)。
4.5 第五步:画出逻辑电路图并验证
根据化简后的驱动方程和输出方程,用触发器和门电路画出完整的电路图。最后,必须进行验证:可以通过仿真,或者手动沿着电路走一遍关键路径,检查功能是否正确,特别是检查自启动性。
设计心法:对于初学者,我强烈建议从摩尔型状态机开始设计。摩尔型的输出只与当前状态有关,这意味着输出不会直接随输入变化而产生毛刺,时序更容易分析,电路也更稳定。虽然它比米利型对输入的反应慢一个时钟周期,但在绝大多数同步设计中,这都不是问题。先掌握摩尔型,再理解米利型,是更平滑的学习路径。
5. 典型时序逻辑模块:计数器与寄存器的实战解析
掌握了分析和设计的一般方法,我们来看两个最经典、应用最广的时序逻辑模块:计数器和寄存器。它们不是特定的芯片,而是一种电路功能,可以用我们刚学的方法设计出来,也有大量现成的集成芯片可供选用。
5.1 计数器:数字世界的时间与秩序之源
计数器的功能是记录输入脉冲的个数。它是分频、定时、序列发生、地址生成等众多功能的基础。
5.1.1 同步计数器与异步计数器的本质区别这是计数器分类的第一个维度,区别在于所有触发器的时钟是否统一。
- 异步计数器(纹波计数器):例如,用T触发器级联,上一个触发器的输出作为下一个触发器的时钟。这种结构简单,但存在“纹波”效应——时钟信号像波浪一样从低位传到高位,导致各个触发器不是同时翻转的。在高速应用下,这会带来严重的时序问题,并且译码输出时容易产生尖峰毛刺。
- 同步计数器:所有触发器共用同一个时钟源。高位触发器的翻转与否,由低位触发器的状态通过组合逻辑(如与门)共同决定,大家在同一个时钟沿同时动作。速度快,工作可靠,是现代设计的主流。
经验之谈:除非在极低速、低成本的简单场合,否则一律使用同步计数器。在FPGA或ASIC设计中,你写的计数器
always @(posedge clk) count <= count + 1;综合出来就是同步计数器。千万不要为了“省几个门”而去设计异步计数器,带来的时序风险远大于那点面积节省。
5.1.2 任意模值计数器的设计关键设计一个模N(即0到N-1循环)的计数器,核心在于状态跳转的控制逻辑。以同步计数器为例,步骤是:
- 根据模值N,确定需要的最少触发器数量n(2^{n-1} < N ≤ 2^n)。
- 选择状态编码(通常就是对应的二进制数)。
- 画出状态图(0->1->2->...->N-1->0)。
- 为每个触发器设计次态逻辑。对于D触发器,就是设计每个D端的逻辑函数,使其在计数到N-1时,次态能归零。
一个更工程化的方法是利用集成计数器芯片的置数或清零功能。例如,经典的74LS161是4位二进制同步计数器。要构成模10计数器,我们可以让它在计数到9(1001)时,不是等到16自然归零,而是利用其“同步置数”功能,在下一个时钟沿到来时,将数据输入端预先设置的0(0000)装入,从而实现从0到9的循环。这种方法灵活且节省逻辑资源。
5.2 寄存器:数据的临时驿站与加工流水线
寄存器是一组触发器(通常是D触发器)的集合,用于暂存二进制数据。所有触发器共享一个时钟信号,在时钟沿同步地将输入端数据存入。
5.2.1 基本数据寄存器最简单的寄存器就是多个D触发器并行连接,共用时钟和复位端。当时钟沿到来,n位输入数据被同时存入n个触发器,并从Q端输出。它是CPU中通用寄存器、内存接口缓冲器等的基本单元。
5.2.2 移位寄存器:串并转换与数据延迟的神器移位寄存器是寄存器的一种特殊工作模式。除了存储,它还能在时钟控制下,将数据向左或向右移动。
- 串行输入串行输出:可用于产生序列信号,或实现数据延迟(一个信号经过N级移位寄存器,就延迟了N个时钟周期)。
- 串行输入并行输出:这是“串并转换”的核心,用于将一根数据线上串行传送的数据,转换为多根线上并行数据,大大提高了数据吞吐效率。USB、SPI等串行通信的接收端,本质上就是一个移位寄存器。
- 并行输入串行输出:“并串转换”,作用相反,常用于通信发送端。
- 双向移位寄存器:既能左移又能右移,功能更灵活。
实战应用:用移位寄存器实现序列检测还记得我们之前设计的“1101”序列检测器吗?除了用状态机,还可以用一个4位移位寄存器来实现。将串行数据X依次移入寄存器,每个时钟周期比较寄存器中的4位历史数据是否等于“1101”。这种方法思路直白,但不够灵活,且延迟固定。状态机方法更通用、更节省资源,是更优的选择。
6. 时序电路设计的核心挑战:时序约束与亚稳态
当你把电路图设计好,在仿真器里跑通,以为大功告成时,真正的挑战才刚刚开始——将设计放到真实的硬件中运行。这里有两个你必须跨越的鸿沟:时序约束和亚稳态。
6.1 建立时间与保持时间:数字电路的物理法则
这是时序逻辑电路,尤其是同步电路,必须遵守的铁律。它们描述了触发器数据输入端(D)相对于时钟沿的稳定性要求。
- 建立时间:在时钟有效沿(如上升沿)到来之前,数据输入端D的信号必须已经稳定保持的最小时间,记为T_{su}。
- 保持时间:在时钟有效沿到来之后,数据输入端D的信号必须继续稳定保持的最小时间,记为T_h。
违反这两个时间要求,触发器就可能进入一种可怕的中间状态——亚稳态,导致输出既不是0也不是1,或者需要极长的时间才能稳定到一个确定值,造成后续逻辑误判。
如何满足时序要求?这涉及到数字系统设计的核心:时序分析。在一个同步系统中,数据从一个触发器(FF1)发出,经过组合逻辑电路(产生延迟T_{comb}),传递到下一个触发器(FF2)的D端。这个路径必须满足:
- 建立时间约束:T_{clk} - T_{skew} ≥ T_{comb} + T_{su}。其中T_clk是时钟周期,T_skew是时钟偏移。这意味着组合逻辑的延迟不能太大,否则数据跑得慢,赶不上下一个时钟沿的“采样窗口”。
- 保持时间约束:T_{cd} ≥ T_h。其中T_cd是时钟路径和组合逻辑的最小延迟( contamination delay)。这意味着数据不能变化得太快,否则新数据把旧数据冲掉了,在采样窗口后没能保持住。
在FPGA设计中,综合和布局布线工具会根据你设定的时钟频率,自动进行时序分析,并报告是否存在“建立时间违例”或“保持时间违例”。出现违例,你就需要优化代码(如插入流水线寄存器来切割长组合逻辑路径)、降低时钟频率或调整布局约束。
6.2 亚稳态:无法消除,只能管理
即使你满足了建立和保持时间,亚稳态也无法绝对避免。因为信号在跳变时,如果刚好在时钟沿附近满足触发器的内部反馈平衡点,就会引发亚稳态。亚稳态的后果是输出在较长时间内处于中间电平,或者产生振荡。
亚稳态的传递与危害: 一个触发器的亚稳态输出,会被后续的触发器当作输入。如果这个亚稳态信号在下一个触发器的建立/保持时间窗口内还没有稳定下来,就会导致亚稳态的级联传播,最终使系统产生不可预测的错误。
应对亚稳态的工程方法:
- 同步器:这是最常用、最有效的方法。对于来自异步时钟域的信号(比如一个按键输入),不能直接接入系统时钟域。标准的做法是使用两级或多级D触发器进行同步。第一级触发器很可能进入亚稳态,但给予一个完整的时钟周期时间,其输出有很大概率在第二级触发器采样前稳定下来。两级同步器能将亚稳态失效的概率降低几个数量级。
// 典型的双触发器同步器 Verilog代码 always @(posedge clk or posedge rst) begin if (rst) begin sync_reg1 <= 1'b0; sync_reg2 <= 1'b0; end else begin sync_reg1 <= async_input; // 第一级,可能亚稳态 sync_reg2 <= sync_reg1; // 第二级,大概率稳定 end end // 使用 sync_reg2 作为同步化后的信号 - 降低时钟频率:给信号传输和稳定留出更多时间。
- 使用更快的工艺:更先进的工艺节点,触发器的亚稳态恢复时间更短。
血的教训:我早期做一个FPGA项目,需要检测一个外部传感器的脉冲。我偷懒,直接把传感器信号接到了我的状态机上。在实验室测试一切正常,但一到现场,设备就偶尔出现死机或计数错误。排查了很久,最后用逻辑分析仪抓信号,发现传感器信号和FPGA时钟是异步的,偶尔在时钟沿附近变化,导致了亚稳态传播进了状态机,使其跑飞。加上一个两级同步器后,问题彻底解决。这个坑让我永远记住了:凡是来自不同时钟域的信号,必须同步!没有例外。
7. 有限状态机:时序逻辑设计的灵魂模式
有限状态机是描述和控制时序逻辑系统最强大、最清晰的工具。它将一个系统抽象为有限个状态,以及在这些状态之间转移的条件和动作。前面设计的序列检测器,就是一个FSM。
7.1 米利型与摩尔型:输出时序的差异
这是FSM的两种基本模型,区别在于输出的产生方式:
- 摩尔型:输出仅与当前状态有关。这意味着输出会在状态改变后,立即(经过组合逻辑延迟)更新,并且在一个状态周期内保持稳定。输出与输入异步,但相对时钟同步。
- 米利型:输出与当前状态和当前输入都有关系。这意味着即使状态不变,输入变化也可能立即引起输出变化。
选择指南:
- 优先选择摩尔型:输出稳定,没有因输入毛刺而产生的输出毛刺,设计更简单,时序分析更容易。大多数情况下,摩尔型是首选。
- 当需要快速响应输入时选择米利型:例如,设计一个串行数据的“边沿检测”电路,当检测到输入上升沿时,输出一个时钟周期宽的高脉冲。用摩尔型实现需要两个状态,输出会晚一个周期;用米利型,可以在输入变化的同一个周期内就产生输出脉冲。
7.2 三段式状态机写法:清晰、安全、可综合
在硬件描述语言中,推荐使用“三段式”风格来描述状态机。以Verilog为例,它将状态机的描述分为三个独立的always块:
- 状态寄存器(时序逻辑):负责在时钟沿进行状态切换。
always @(posedge clk or posedge rst) begin if (rst) current_state <= IDLE; else current_state <= next_state; end - 次态逻辑(组合逻辑):根据当前状态和输入,决定下一个状态是什么。
always @(*) begin next_state = current_state; // 默认保持现态 case (current_state) IDLE: if (start) next_state = WORK; WORK: if (done) next_state = IDLE; default: next_state = IDLE; endcase end - 输出逻辑(组合逻辑或时序逻辑):根据当前状态(摩尔型)或当前状态和输入(米利型)产生输出。
// 摩尔型输出 always @(*) begin case (current_state) IDLE: {out1, out2} = 2'b00; WORK: {out1, out2} = 2'b10; default: {out1, out2} = 2'b00; endcase end
为什么推荐三段式?
- 清晰:三个过程各司其职,结构一目了然。
- 安全:避免了在组合逻辑always块中对同一变量既读又写可能导致的仿真与综合不匹配的问题。
- 易于综合和优化:综合工具可以很好地对每个部分进行优化。
时序逻辑电路是数字系统的筋骨与灵魂。从记忆一比特的触发器,到构成复杂计算核心的状态机,每一步都蕴含着对“时间”和“状态”的精密控制。理解并掌握它,你才真正走进了动态数字世界的大门。纸上得来终觉浅,绝知此事要躬行。最好的学习方式,就是动手用Verilog/VHDL描述一个状态机,然后用仿真工具(如ModelSim)观察每一个时钟沿下状态和信号的变化,再把它下载到FPGA开发板上,用LED或数码管验证其行为。当你看到自己设计的计数器规律地闪烁,序列检测器对特定的按键序列做出响应时,那种成就感,是任何理论都无法替代的。