跳转至

状态机

状态机是一种非常通用的硬件设计模式,你几乎可以在任何硬件设计中看到它的影子——信号发生器,访存状态机等等。

本节将通过一个简易信号发生器的例子,来教大家最基础的状态机的编写方式。

目标功能

本节要实现的设计的目标功能如下:

  1. 当开关为 0 时,向数码管顺序输出 2, 0, 1, 3,在按下 clk 时切换。
  2. 当开关为 1 时,反转切换方向,即输出顺序改为 3, 1, 0, 2,在按下 clk 时切换。

电路设计

顶层模块的输入输出如下:

  1. input wire clk: clk 开关
  2. input wire rst: 用于重置
  3. input wire rev_i: 普通开关,用来控制切换方向
  4. 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

这段代码主要负责确定每个状态,当前模块的行为。在本节的例子当中,每个状态对应着一个数字的输出,因此只需要像这样输出数字就好。注意组合逻辑需要覆盖所有可能分支。

小结

  1. typedef enum 语法

    typedef enum <logic [high : low]> {<enum_members>} <typename>;
    
  2. case 语法

    case(<signal>)
        <case1>: begin
            <Logic>
        end
        <case2>: <one-line-logic>
        default: <default-logic>
    endcase
    
  3. 状态机的设计和编写方法

到这里完成了 System Verilog 4 的讲解,可以完成串行密码锁实验。


最后更新: 2023年4月27日
作者:cuibst