状态机
状态机是一种非常通用的硬件设计模式,你几乎可以在任何硬件设计中看到它的影子——信号发生器,访存状态机等等。
本节将通过一个简易信号发生器的例子,来教大家最基础的状态机的编写方式。
目标功能
本节要实现的设计的目标功能如下:
- 当开关为 0 时,向数码管顺序输出 2, 0, 1, 3,在按下 clk 时切换。
- 当开关为 1 时,反转切换方向,即输出顺序改为 3, 1, 0, 2,在按下 clk 时切换。
电路设计
顶层模块的输入输出如下:
input wire clk
: clk 开关input wire rst
: 用于重置input wire rev_i
: 普通开关,用来控制切换方向output reg [3:0] seg7_wo
: 向数码管输出
将 fpga 的对应管脚接到对应元件上,并配置好约束文件。
状态机设计
既然本节的标题为状态机,那么在真正编写电路之前,要先将状态机的状态设计好。
首先,我们设计 4 个状态,分别对应输出 4 个数字 2, 0, 1, 3。我们称这四个状态为 STATE_0 - STATE_3
。
接下来要设计这四个状态的转移边。首先,切换有一个必要条件就是 clk 从 0 变为 1,也就是 clk 的上升沿。
接下来,当 rev == 1'b0
的时候正向切换;当 rev == 1'b1
的时候反向切换。状态机转移图如下:
flowchart
entry --> state0
state0[STATE_0] -- rev == 1'b0 --> state1[STATE_1] -- rev == 1'b0 --> state2[STATE_2] -- rev == 1'b0 --> state3[STATE_3] -- rev == 1'b0 --> state0
state0 -- rev == 1'b1 --> state3 -- rev == 1'b1 --> state2 -- rev == 1'b1 --> state1 -- rev == 1'b1 --> state0
这样我们就完成了一个状态机的设计,接下来就是编写这个状态机了。
代码
`timescale 1ns / 1ps
module state_machine(
input wire clk,
input wire rst,
input wire rev_i,
output reg [3:0] seg7_wo
);
typedef enum {STATE_0, STATE_1, STATE_2, STATE_3} state_type;
state_type state_r, next_state_w;
always_ff @(posedge clk or posedge rst) begin
if(rst) begin
state_r <= STATE_0;
end else begin
state_r <= next_state_w;
end
end
always_comb begin
case(state_r)
STATE_0: begin
if(rev_i)
next_state_w = STATE_3;
else
next_state_w = STATE_1;
end
STATE_1: begin
if(rev_i)
next_state_w = STATE_0;
else
next_state_w = STATE_2;
end
STATE_2: begin
if(rev_i)
next_state_w = STATE_1;
else
next_state_w = STATE_3;
end
STATE_3: begin
if(rev_i)
next_state_w = STATE_2;
else
next_state_w = STATE_0;
end
default:
next_state_w = state_r;
endcase
end
always_comb begin
case(state_r)
STATE_0: seg7_wo = 4'h2;
STATE_1: seg7_wo = 4'h0;
STATE_2: seg7_wo = 4'h1;
STATE_3: seg7_wo = 4'h3;
default: seg7_wo = 4'h0;
endcase
end
endmodule
新语法讲解
typedef enum
为了定义抽象的状态类型 state_type
,我们使用了新语法 typedef enum
来定义枚举类型。语法如下:
typedef enum <logic [high : low]> {<enum_members>} <typename>;
默认在不指明宽度的情况下,新类型的信号宽度为 32 位。如果需要指明宽度,则使用中间的 logic 来指明宽度。比如本节的例子也可以这么写:
typedef enum logic[1:0] {STATE_0, STATE_1, STATE_2, STATE_3} state_type;
case
System Verilog 中的 case 语句的结构与 c++ 中的 switch case 语句大体相同,只不过每个 case 不再需要 break,而是用 begin end 括起来而已。
注意 default 的编写,在组合逻辑中,你仍然要覆盖所有可能情况,因此请务必编写 default 逻辑,不管这是否必要。
除标准 case 语句之外,还有 casex 和 casez 指令。这 3 个 case 指令都可以综合。
在这两个新的 case 语句之中,可使用 X 和 Z 作为通配符。而在普通的 case 语句当中,也可以使用 ? 作为通配符。比如 2'b1?:
这样。
请慎用 casex,因为其会把 X 作为通配符处理,导致仿真出现奇怪的逻辑问题,并可能与实际表现有差距。
代码讲解
在编写所有状态机时,代码都主要分为 3 个部分:切换时序逻辑,次状态组合逻辑和状态行为组合逻辑。
切换时序逻辑
always_ff @(posedge clk or posedge rst) begin
if(rst) begin
state_r <= STATE_0;
end else begin
state_r <= next_state_w;
end
end
这段逻辑负责 state_r 寄存器的输入逻辑。简单的时候只需要像上面这样把 next_state_w 赋给 state_r 就好。
记得给 state_r 寄存器进行复位。
次状态组合逻辑
always_comb begin
case(state_r)
STATE_0: begin
if(rev_i)
next_state_w = STATE_3;
else
next_state_w = STATE_1;
end
STATE_1: begin
if(rev_i)
next_state_w = STATE_0;
else
next_state_w = STATE_2;
end
STATE_2: begin
if(rev_i)
next_state_w = STATE_1;
else
next_state_w = STATE_3;
end
STATE_3: begin
if(rev_i)
next_state_w = STATE_2;
else
next_state_w = STATE_0;
end
default:
next_state_w = state_r;
endcase
end
这段逻辑主要负责根据当前状态,以及其他必要信号,以组合逻辑方式计算下一个状态。注意组合逻辑需要覆盖所有可能分支。
状态行为组合逻辑
always_comb begin
case(state_r)
STATE_0: seg7_wo = 4'h2;
STATE_1: seg7_wo = 4'h0;
STATE_2: seg7_wo = 4'h1;
STATE_3: seg7_wo = 4'h3;
default: seg7_wo = 4'h0;
endcase
end
这段代码主要负责确定每个状态,当前模块的行为。在本节的例子当中,每个状态对应着一个数字的输出,因此只需要像这样输出数字就好。注意组合逻辑需要覆盖所有可能分支。
小结
-
typedef enum
语法typedef enum <logic [high : low]> {<enum_members>} <typename>;
-
case 语法
case(<signal>) <case1>: begin <Logic> end <case2>: <one-line-logic> default: <default-logic> endcase
-
状态机的设计和编写方法
注
到这里完成了 System Verilog 4 的讲解,可以完成串行密码锁实验。