入門 · レッスン 7 / 10

有限状態機械

MooreマシンとMealyマシン、状態のエンコード方法、2プロセス記述と1プロセス記述、そして初心者が最初のFSMで必ず引っかかるバグを解説します。

入門35 分更新 2026年11月13日無料期間 2027年1月31日

このレッスンで学ぶこと

  • 制御問題の状態遷移図を描き、MooreマシンとMealyマシンを見分けられる
  • 安全なデフォルト値を備えた2プロセス記述で、FSMをVerilogとVHDLで書ける
  • バイナリ、Gray、ワンホットの各エンコードから選択し、その選択に対して合成が何をするかを説明できる
  • ラッチ、デフォルトの欠落、到達不能状態など、よくあるFSMのバグを見分けられる

前提レッスン: シミュレーションとテストベンチ

FPGA内の制御ロジックはほぼすべて有限状態機械(FSM)です。書いた本人がそう呼んでいるかどうかにかかわらず、です。スタートビットを待つUART受信器、リフレッシュを順に進めるメモリコントローラ、押下が短いか長いかを判定するボタンハンドラ。いずれも、名前の付いた少数の状態、状態間を移動する規則、そして現在どこにいるかに依存する出力で構成されています。このレッスンでは、状態遷移図の描き方、合成ツールが認識できるようにVerilogとVHDLで書く方法、そして落とし穴がどこにあるかを示します。レッスン04のレジスタとレッスン06のシミュレーションの内容が必要です。

状態機械とは何か

状態機械は3つの部分からなります。状態レジスタは現在の状態、つまり名前の付いた小さな集合のうちの1つの値を保持します。次状態ロジックは現在の状態と入力を見て、次のクロックエッジで状態レジスタに何を取り込むかを決めます。出力ロジックは出力を生成します。状態レジスタが唯一の記憶要素です。それ以外はすべて組み合わせ論理です。つまりFSMとは、すでに知っている「レジスタ+ロジック」のパターンに、レジスタが取りうる各値へ名前を付けたものにすぎません。

このレッスンの例は、歩行者用横断ボタン付きの交通信号機です。状態は4つあります。赤、緑、黄、そして誰かがボタンを押したときだけ現れる歩行フェーズです。レッスン08のタイマーのような、1秒に1パルスの遅いtick入力が保持カウンタを進め、各灯が妥当な秒数だけ点灯し続けるようにします。

S_REDred=1, 3 ticksS_GREENgreen=1, 5 ticksS_AMBERamber=1, 2 ticksS_WALKred=1 walk=1, 4 tickshold=0 and not walk_reqhold=0hold=0hold=0 andwalk_reqhold=0リセット後はS_RED。図にない弧は同じ状態に留まり、holdを減らす。
交通信号機コントローラの状態遷移図。各弧は、保持カウントが0に達した状態でtickが来たときに辿られる。

MooreかMealyか

古典的な2つの流儀の違いはただ1点、出力が何に依存するかです。

Mooreマシンでは、出力は状態のみの関数です。交通信号機はMooreマシンです。赤が点灯しているのは状態がS_REDだからで、その状態の間に入力が何をしてもランプは変わりません。Moore出力は、状態レジスタから直接駆動すればグリッチフリーで、クロックエッジでのみ変化し、動作を追いやすいという利点があります。代償はレイテンシです。入力イベントがあるエッジで状態を変え、出力はその次のエッジで追従します。

Mealyマシンでは、出力は状態と現在の入力に依存します。出力は入力と同じサイクルで応答できるため1クロック分のレイテンシを節約できますが、出力は入力からの組み合わせ論理になるので、入力がグリッチすれば出力もグリッチします。以下はMealyマシンとして書いた小さなシーケンス検出器です。シリアル入力上のパターン101を探し、最後の1が到着したのと同じサイクルでfoundを上げます。

// Mealy detector: `found` pulses in the same cycle as the last bit of "101".
module seq_mealy (
  input  wire clk,
  input  wire rst,
  input  wire din,
  output reg  found
);
  localparam [1:0] S_IDLE = 2'd0, S_1 = 2'd1, S_10 = 2'd2;
  reg [1:0] state;

  // One-process style: next state and output in one block.
  always @(posedge clk) begin
    if (rst)
      state <= S_IDLE;
    else
      case (state)
        S_IDLE: state <= din ? S_1 : S_IDLE;
        S_1:    state <= din ? S_1 : S_10;
        S_10:   state <= din ? S_1 : S_IDLE;
        default: state <= S_IDLE;
      endcase
  end

  // Mealy output depends on input as well as state, so it is combinational.
  always @(*)
    found = (state == S_10) && din;
endmodule
101検出器。Mealy出力は最後の1が到着したサイクルで立ち上がる。同じ出力をレジスタで受けた(Moore型の)版は、1エッジ後に現れる。clkdinstateIDLES_1S_10S_1S_10IDLEfound (Mealy)found (registered)
101検出器。Mealy出力は最後の1が到着したサイクルで立ち上がる。同じ出力をレジスタで受けた(Moore型の)版は、1エッジ後に現れる。

実際には、ほとんどのFPGA設計者はデフォルトでMooreマシンを書き、必要なMealy出力はモジュールから出る前にレジスタで受けます。「最後のビットが到着した」という信号が欲しく、かつクリーンであってほしいなら、レジスタで受けてください。その結果は、状態がひとつ増えたMooreマシンが姿を変えたものにほかなりません。

2プロセス記述

HDLでFSMを構成する方法は数多くあります。ここで教えるのは2プロセス記述です。1つのクロック同期プロセスが状態レジスタを保持し、1つの組み合わせプロセスが次状態と出力を計算します。Verilogによる交通信号機です。

// Traffic-light controller, Moore machine, two-process style.
// `tick` is a slow enable (for example once per second) from a timer.
// `walk_req` asks for a pedestrian phase at the next red.
module traffic_fsm (
  input  wire clk,
  input  wire rst,
  input  wire tick,
  input  wire walk_req,
  output reg  red,
  output reg  amber,
  output reg  green,
  output reg  walk
);
  // State encoding. Synthesis may recode this; see the lesson text.
  localparam [1:0] S_RED   = 2'd0,
                   S_GREEN = 2'd1,
                   S_AMBER = 2'd2,
                   S_WALK  = 2'd3;

  reg [1:0] state, state_next;
  reg [2:0] hold, hold_next;   // ticks remaining in the current state

  // Process 1: state register.
  always @(posedge clk) begin
    if (rst) begin
      state <= S_RED;
      hold  <= 3'd0;
    end else begin
      state <= state_next;
      hold  <= hold_next;
    end
  end

  // Process 2: next-state and output logic (combinational).
  always @(*) begin
    // Defaults first, so every path assigns every signal.
    state_next = state;
    hold_next  = hold;
    red = 1'b0; amber = 1'b0; green = 1'b0; walk = 1'b0;

    case (state)
      S_RED: begin
        red = 1'b1;
        if (tick) begin
          if (hold == 3'd0) begin
            if (walk_req) begin
              state_next = S_WALK;
              hold_next  = 3'd3;
            end else begin
              state_next = S_GREEN;
              hold_next  = 3'd4;
            end
          end else
            hold_next = hold - 3'd1;
        end
      end

      S_GREEN: begin
        green = 1'b1;
        if (tick) begin
          if (hold == 3'd0) begin
            state_next = S_AMBER;
            hold_next  = 3'd1;
          end else
            hold_next = hold - 3'd1;
        end
      end

      S_AMBER: begin
        amber = 1'b1;
        if (tick) begin
          if (hold == 3'd0) begin
            state_next = S_RED;
            hold_next  = 3'd2;
          end else
            hold_next = hold - 3'd1;
        end
      end

      S_WALK: begin
        red  = 1'b1;
        walk = 1'b1;
        if (tick) begin
          if (hold == 3'd0) begin
            state_next = S_RED;
            hold_next  = 3'd2;
          end else
            hold_next = hold - 3'd1;
        end
      end

      default: state_next = S_RED;
    endcase
  end
endmodule

組み合わせブロックの3つの細部が、重みの大部分を担っています。

駆動するすべての信号に、冒頭でデフォルト値を与えています。これにより、caseがどの経路を辿っても、各出力と各_next信号が値を持つことが保証されます。どこかの経路で未代入のものがあれば、合成ツールは前の値を記憶しなければならず、組み合わせ論理でそれを実現する唯一の方法はラッチです。FPGAにおけるラッチは遅く、タイミング解析が難しく、ほぼ常に誤りです。

caseにはdefaultアームがあります。状態ビットが2つで状態が4つなら全パターンが使われますが、この習慣は3ビットで5状態の機械において重要です。余った3つのコードはグリッチや放射線イベントによって到達可能であり、機械はロックアップするのではなく回復すべきだからです。

組み合わせブロックの代入は=を、クロック同期ブロックの代入は<=を使います。混ぜても文法上は合法で、たいていは正しくシミュレーションされますが、この規則を守ることで意図が見え、正しく動かない少数のケースも避けられます。

保持カウンタは状態に入るときにロードされるため、その状態で費やすtickの数はロードした値より1つ多くなります。hold_next = 4でS_GREENに入ると、緑は5 tickになります。こうしたオフバイワンの疑問こそ、前回のレッスンのテストベンチの出番であり、このモジュールのテストベンチは演習にあります。

VHDLで同じ機械を書く

VHDLはFSMに本当の利点をひとつもたらします。列挙型です。状態は数値エンコードをまったく持たない型の名前付きの値であり、合成ツールがエンコードを選びます。

-- Traffic-light controller, Moore machine, two-process style.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity traffic_fsm is
  port (
    clk      : in  std_logic;
    rst      : in  std_logic;
    tick     : in  std_logic;
    walk_req : in  std_logic;
    red      : out std_logic;
    amber    : out std_logic;
    green    : out std_logic;
    walk     : out std_logic
  );
end entity;

architecture rtl of traffic_fsm is
  type state_t is (s_red, s_green, s_amber, s_walk);
  signal state, state_next : state_t := s_red;
  signal hold, hold_next   : unsigned(2 downto 0) := (others => '0');
begin
  -- Process 1: state register.
  regs : process (clk)
  begin
    if rising_edge(clk) then
      if rst = '1' then
        state <= s_red;
        hold  <= (others => '0');
      else
        state <= state_next;
        hold  <= hold_next;
      end if;
    end if;
  end process;

  -- Process 2: next state and outputs.
  comb : process (all)
  begin
    state_next <= state;
    hold_next  <= hold;
    red <= '0'; amber <= '0'; green <= '0'; walk <= '0';

    case state is
      when s_red =>
        red <= '1';
        if tick = '1' then
          if hold = 0 then
            if walk_req = '1' then
              state_next <= s_walk;
              hold_next  <= to_unsigned(3, 3);
            else
              state_next <= s_green;
              hold_next  <= to_unsigned(4, 3);
            end if;
          else
            hold_next <= hold - 1;
          end if;
        end if;

      when s_green =>
        green <= '1';
        if tick = '1' then
          if hold = 0 then
            state_next <= s_amber;
            hold_next  <= to_unsigned(1, 3);
          else
            hold_next <= hold - 1;
          end if;
        end if;

      when s_amber =>
        amber <= '1';
        if tick = '1' then
          if hold = 0 then
            state_next <= s_red;
            hold_next  <= to_unsigned(2, 3);
          else
            hold_next <= hold - 1;
          end if;
        end if;

      when s_walk =>
        red  <= '1';
        walk <= '1';
        if tick = '1' then
          if hold = 0 then
            state_next <= s_red;
            hold_next  <= to_unsigned(2, 3);
          else
            hold_next <= hold - 1;
          end if;
        end if;
    end case;
  end process;
end architecture;

process (all)はVHDL-2008の構文で、「内部で読まれるすべてに感度を持つ」という意味です。2008より前はすべての信号を手で列挙する必要があり、列挙漏れはシミュレーションと合成の不一致の古典的な原因でした。when othersアームがないのは、列挙型に対するcaseはすべての値を網羅しなければならず、コンパイラがそれを検査するからです。

1プロセスか2プロセスか

1プロセス記述では、seq_mealyがそうしたように、次状態ロジックと出力をクロック同期ブロックに入れます。短く書け、クロック同期ブロック内のものはすべてレジスタになるのでラッチを生じません。欠点は、すべての出力がレジスタ出力になり、望まない1サイクルのレイテンシが加わることと、Verilogではある分岐でstate <= S_GREENと書きながら、離れようとしている状態の出力がまだ駆動されていることを忘れやすいことです。それでも多くの経験豊富なエンジニアはこの記述を使い、満足しています。ここで2プロセス記述を教えるのは、状態遷移図に一対一で対応するからです。プロセス1が円、プロセス2が弧とラベルです。

レジスタ出力用に別のブロックを持つ3プロセス記述は、機械の出力が数個を超えたときによく使われる改良です。

状態のエンコードと合成が行うこと

Verilog版では状態に手で番号を割り当てました。0、1、2、3です。これはバイナリエンコードで、フリップフロップ数が最少です。ほかに2つの方式が一般的です。

エンコード N状態に必要なフリップフロップ 長所 短所
バイナリ ceil(log2 N) レジスタが最少 次状態ロジックが最も深く、遷移ごとに複数ビットが変化
Gray ceil(log2 N) 単純なリングでは遷移ごとに1ビットだけ変化 分岐のある機械には不向き
ワンホット N ロジックが最も浅く最速、出力が状態ビットからそのまま得られることが多い N個のフリップフロップ、うちN-1個は0

FPGAではフリップフロップが豊富にあり、ロジックの深さがクロック速度を決めるため、数十状態までの機械ではワンホットが通常の選択です。自分で選ぶ必要はめったにありません。Vivado、Quartus Prime、Radiant、Gowin EDA、YosysはいずれもFSM抽出を行います。値が定数との比較と定数からの代入にしか使われないレジスタを見つけ、状態遷移図を再構築し、到達不能状態を取り除き、再エンコードするのです。Vivadoはこれを合成ログに報告し、デフォルトのauto設定では小さな機械に対して通常ワンホットを選びます。fsm_encoding属性で上書きでき、他のツールにも同等のsyn_encodingやfsm_encoding属性があります。

抽出を妨げるものが2つあります。状態レジスタがどこかで数値として読まれる場合、たとえばインデックスとして使われたりモジュールの外へ出されたりする場合、ツールは書かれたエンコードを維持しなければなりません。また、リセット値が定数でない場合、既知の開始状態がありません。どちらの場合も機械は動きますが、書いたとおりのエンコードになります。

よくあるバグ

出会う可能性の高い順に、短く挙げます。

組み合わせプロセスのデフォルト欠落によって推論されたラッチ。合成ツールが警告します。警告を読んでください。

リセットの欠落。機械は、フリップフロップの電源投入時の値がたまたま示す状態から始まります。ほとんどのFPGAはビットストリームからレジスタを0に初期化するため、後でソフトリセットが追加されるまで問題が隠れます。

ボタンなど、同期化回路(シンクロナイザ)なしにピンから直接サンプリングされた入力。状態レジスタは2〜3ビットあり、準安定状態(メタステーブル)の入力は一部のビットを一方へ、残りを他方へ送り、機械を決して訪れるはずのなかった状態に着地させます。レッスン08が示すように、非同期入力はまずすべて同期化してください。

1サイクルだけ真であってほしいのに、複数サイクルにわたって真になる遷移条件。btnのようなレベル信号は、機械を複数の状態に連続して歩かせます。欲しいのは通常1サイクルのパルスであり、これもまたエッジ検出器の仕事です。

シミュレーションでは動くのにボードでは動かない機械。出力が入力のMealy関数で、グリッチするためです。レジスタで受けてください。

演習

レッスン06のスタイルでtraffic_fsmのテストベンチを書いてください。4クロックごとにtickパルスを生成し、リセットを2サイクル保持し、tickごとに4つのランプ出力を表示します。walk_reqをLowにしたまま信号を2周させ、次にHighにして、次の赤の後に歩行フェーズが現れ、4 tick続くことを確認してください。redとgreenが同時にHighにならないことを検査し、その検査は目で読むのではなく自動の$display("FAIL")にしてください。

次に、機械を手でワンホットに変換してください。状態ビット4つ、状態ごとに1つです。出力ロジックがどれだけ消えるかに注目してください。

まとめ

  • FSMは状態レジスタと、次状態ロジックおよび出力ロジックからなります。まず遷移図を描き、そこからコードを書きます。
  • Moore出力は状態のみに依存し、クロックエッジで変化します。Mealy出力は入力にも依存し、1サイクル早く応答しますが、グリッチすることがあります。
  • 2プロセス記述はレジスタとロジックを分離します。ラッチを避けるため、組み合わせプロセスの冒頭ですべての信号にデフォルト値を与えてください。
  • 合成ツールはFSMを抽出して再エンコードし、通常はワンホットにします。ただし状態が数値として読まれる場合や定数のリセットがない場合は除きます。
  • よくあるバグは、ラッチ、リセットの欠落、同期化されていない入力、レベルトリガの遷移、グリッチするMealy出力です。

さらに読む