引子
按照原计划,这篇文章应该介绍oss-cad-suite的使用。不幸的是,三天前我突然发现开发板上的DDR芯片异常发热,即使不进行读写操作,静态电流也超过200mA,需要返厂检修(检查后我决定自行更换DDR),由于手头没有可用于烧录演示的开发板,因此决定提前介绍一些常用的SystemVerilog模块最佳实践。
第一个主题是有限状态机(FSM),它是数字电路设计中最重要的模型之一,是理解和构建所有时序逻辑电路的基石,也是数字电路设计的“灵魂”所在。
本文介绍的最佳实践源自Verilog标准制定者之一的Clifford Cummings大佬,原文是其在Sunburst Design公司网站上公开的文章”Finite State Machine (FSM) Design & Synthesis using SystemVerilog - Part I“,感兴趣的读者可以直接阅读原文。
场景
让我们从一个简单例子开始,想象一台自动售货机,只出售一种售价3元的饮料,且仅接受1元硬币支付,用户首先选择商品,然后进行投币,当金额足够时,便会吐出一瓶饮料,在出货前的任意时刻,用户可以点击退款按钮退还投入的硬币并重新购买。
在设计这个状态机前,我们需要先明确售货机的基本功能需求:
- 选择商品:需要为用户提供一个按钮选择购买饮料;
- 接收投币:接收用户投入的1元硬币;
- 累计金额:能够记录当前投入的总金额;
- 售出商品:当总金额达到3元后,出货一瓶饮料;
- 退款:用户可以随时选择退还已投币。
状态机设计
有限状态机由以下几个核心部分组成:
- 状态:系统只可能存在有限的几种情况;
- 转换:系统状态的切换过程;
- 事件:用于改变系统状态的信号;
- 动作:系统在不同状态下执行的操作。
对于这个我们想象的这个简单的售货机,可以定义以下几个状态:
- 空闲状态(
IDLE
):售货机等待顾客选择商品; - 等待投币状态(
WAIT_COIN
):已选择商品,等待用户投币,并记录投币数量; - 出货状态(
DISPENSE
):机器出货; - 退款状态(
REFUND
):机器退款。
接下来,我们再来思考状态间的转换模式,对于用户的一次成功购买,状态的顺序为:
stateDiagram-v2
IDLE --> WAIT_COIN
WAIT_COIN --> DISPENSE
DISPENSE --> IDLE
而如果用户在投币过程中选择了退款,将会加入一条新状态转换路径,变为:
stateDiagram-v2
IDLE --> WAIT_COIN
WAIT_COIN --> DISPENSE
DISPENSE --> IDLE
WAIT_COIN --> REFUND
REFUND --> IDLE
至此,该售货机的全部状态转换过程已明确。接下来,我们需要思考状态转换的条件,即“事件”。,从场景中我们可以指导,这台售货机至少要有两个按钮:商品选择按钮(select_button
)和退款按钮(refund_button
),他们分别控制售货机状态由空闲转为等待投币和由投币转向退款;此外,这个售货机还需要一个投币口,我们用一个coin_in
信号表示有硬币投入,不妨假设每个时钟周期,只能完成一次投币,即投入一元;为了记录投币数,我们需要一个coin_count
计数器记录已投币的数量,当coin_count==3
时,便会出货一瓶饮料,即售货机状态由等待投币状态转为出货状态。出货和退款都可已在一个时钟周期内完成,因此出货状态和退款状态转换回空闲状态会在下一个周期自动进行。
我们在状态机流程图上添加这些条件:
stateDiagram-v2
IDLE --> WAIT_COIN: select_button
WAIT_COIN --> DISPENSE: coin_count==3
DISPENSE --> IDLE
WAIT_COIN --> REFUND: refund_button
REFUND --> IDLE
或者,如果我们不使用计数器,而使用状态表示已投币的数量,状态图将细化成这个样子:
stateDiagram-v2
IDLE --> WAIT_COIN_0: select_button
state WAIT_COIN {
WAIT_COIN_0 --> WAIT_COIN_1: coin_in
WAIT_COIN_1 --> WAIT_COIN_2: coin_in
}
WAIT_COIN_0 --> IDLE: refund_button
WAIT_COIN_1 --> REFUND: refund_button
WAIT_COIN_2 --> REFUND: refund_button
WAIT_COIN_2 --> DISPENSE: coin_in
REFUND --> IDLE
DISPENSE --> IDLE
最后,我们需要思考每个状态下需要完成的动作:
- 在
DISPENSE
状态,售货机需要控制出货,可以用dispense_item
信号表示; - 在
REFUND
状态,售货机需要驱动退币装置,退还已投入的硬币,用return_coins
信号表示; - 在其它状态,售货机不进行任何动作。
最佳实践
我们推荐使用三段式状态机来实现有限状态机,三段式是指:
- 第一段:同步时序逻辑,用于进行状态转换;
- 第二段:组合逻辑,用于基于事件判断下一状态;
- 第三段:输出逻辑,表示在某一状态下的动作;
我们用上面的售货机例子来详细看一下这一最佳实践的具体实现:
|
|
我们可以注意到,三段式状态机将状态转换、事件判断、动作分配到了三个独立的always
块中,提供了一种可结构严格、可靠且高效的FSM设计方法。虽然代码量相比一些教程中使用的一段式、两段式状态机略多,但其在可读性和可维护性上都具有巨大优势。并且三段式状态机代码中,第一段状态转换块在任何状态机中都无需任何更改,直接复用,代码编写上并不会带来多大的麻烦。
补充
Moore状态机和Mealy状态机
有限状态机可分为Moore和Mealy两种类型,判断依据是输出是否受输入影响。
Moore状态机的输出仅受当前状态控制,每个状态都严格对应着一种输出;而Mealy状态机的输出不仅受状态控制,还与当前时刻的输入信号有关。
我们编写的售货机仿真波形如下图所示,依次模拟了成功购买、投入一枚硬币后退款、投入两枚硬币后退款三个状态,可以注意到,输出信号dispense_item
和return_coins
信号在输入信号的下一个时钟周期与当前状态state
同步变化,输出信号仅受当前状态控制,因此这是一个典型的Moore状态机。可以注意到,当输入操作时,输出会慢上一个时钟周期,这是由于状态寄存器须在时钟上升沿处更新,这也是Moore状态机不可避免的一个延迟。
如果想将这一模型改为Mealy状态机,只需将输出逻辑的always
块改为组合逻辑,如:
|
|
此时,我们的仿真波形如下图所示,可以看到输出信号提前了一个时钟周期,当refund_button
和coin_in
变化时,输出先于状态完成了改变,变为跟随下一状态next
变化。
从表面上看,Mealy状态机能够降低延迟,但这也大大降低了模块的抗干扰能力,如下图所示,在第二个测例中,投入一枚硬币后,退款按钮refund_button
上出现了一个扰动,该扰动立刻反映在了return_coins
上,这是十分危险的!
因此,除非必要的场合,我们都应该避免使用Mealy状态机。谨记,”Moore is Less. And Less is Beautiful.“。
输出逻辑判断
在输出逻辑判断时,我们使用的是next
信号,而不是state
信号,这里的技巧在于,我们应该判断并赋值的是”下一个“输出的信号值,而不是当前状态的输出信号值。如果在这里错误使用了state
作为判断信号,则会导致输出相较当前状态存在一个时钟周期的延迟,破坏了Moore状态机输出与状态的对应关系!
XXX状态
在状态定义中,我们额外定义了一个XXX
状态。理想状态下,在复位结束后,状态机在任何情况下都不应该进入XXX
状态。你或许会觉得这一状态没有意义,但在复杂状态机设计中,常常会有我们考虑不到或考虑不全的转换关系,这种情况下,很容易发生程序“跑飞”的现象,XXX
状态的加入正是为了发现这一未定义状态空间,这将在仿真验证时大大缩短我们的debug时间。
因此,我们在定义状态枚举类时,应定义XXX
状态,并将其作为状态判断组合逻辑的默认值,当在仿真中发现了XXX
状态的出现,便需要查找该未定义状态出现的原因,修复这一错误。另外,在枚举类值映射时,可以将XXX
状态映设为'x
,这样该状态就会被综合器忽略,也不会占用额外的资源。
枚举值映射
在上面的示例代码中,我们没有为state_e
状态类指定格式,在这种情况下,综合器会根据你的代码,自动选择或推断出最合适的编码方式。编码方式分为"Binary"和"OneHot"两种。
"Binary"采用累加数值进行编码,即状态会编码为0、1、2、3、4……这种编码方式只需要clog(N)
个触发器即可以表示N种状态,但状态改变的控制逻辑会更为复杂;
"OneHot"则采用仅有一位"1"的二进制数表示不同的状态,即0001、0010、0100、1000……这种编码方式需要N个触发器表示N种状态,但从一个状态切换到另一个状态会非常简单,至多需要两次位翻转即可完成。
总的来说,"Binary"编码节省触发器资源,"OneHot"编码控制逻辑简单。通常来说,FPGA的触发器资源是相当充裕的,并且在相同硬件环境下,"OneHot"因其简单的组合逻辑,能够实现更高的时钟频率,因此,在FPGA设计中一般会优先使用"OneHot"编码方式。
如果需要手动控制编码格式,可以将枚举类定义部分修改为:
|
|
结语
在这篇文章中,我们介绍了有限状态机的设计过程和代码实现,状态机是数字电路的核心架构之一,我们将要设计的处理器本质上就是一个大型状态机结构。熟悉了状态机的构建,才能在理解架构、设计架构时得心应手。Clifford大佬推荐的三段式状态机为我们提供了一个绝佳的状态机模板,本系列文章的后续设计也会严格遵循这一架构展开。