第11回: 順序回路のHDL記述(2): ステートマシン

復習:状態遷移回路

(p.92〜) フリップフロップを使った順序回路は、 いわゆる状態遷移回路(ステートマシン)として一般化することができます。


これは、このような状態遷移図によって動作を表現できる回路で、 いくつかの内部状態(state)をもち、クロックにあわせて、 入力によって、次の状態が決まる、というものです。 この例では、IDLE, SET, RUN, DONEという4つの状態を持ち、 例えば、現在の状態がIDLEのときは、setという入力が0のときは 次の状態もIDLE、set=1ならば次の状態はSETとなる、という動作を あらわしています。 もちろんカウンタなども、このステートマシンとして 考えることができます。 (内部状態=現在のカウント値、と考えればよい)


このようなステートマシンは、一般にはこのような構成の回路によって 実現することができます。 現状態stateを保持しているレジスタ(実体は、ほとんどの場合はD-FF)、 現状態と入力から次の遷移先の状態n_stateを決める次状態決定回路、 そして現状態と入力から、全体としての出力を決める 出力信号決定部、からなります。

一般には、現状態と次状態、入力・出力の対応を状態遷移表にまとめ、 それを真理値表とみなして次状態決定回路、出力信号決定部の 論理回路を設計することになります。

設計例:タイマ

(p.94〜) 具体例として、タイマの回路を、ステートマシンとして 設計してみましょう。 入力を、次のように決めてみます。

全体を、この図のような構成で考えてみます。 大きく、制御部controlとデータパス部datapathからなります。

データパス部

データパス(data-path)とは、演算回路、と考えてもらってかまいません。 つまり、指定された信号にyとて、所望の演算結果を出力する 回路、ということです。 ここでは、以下の3段階で、データパスを設計してみます。

1.時間設定用データパス



まず時間セットを行う状態(SET)は、時間セット用のスイッチである s10とs60の値をみて、 s10=1ならば時間カウントを10秒加算し、 s60=1ならば時間カウントを60秒加算するという動作をします。 これらの動作は、すべてクロックclkの立ち上がりに同期して 行われると仮定しておきます。(いわゆる同期式回路)

これから、上記のようなデータパスを設計することができます。 ここでREGは、時間カウント値を保持しておく12ビットのレジスタ(変数)で、 その実体はクロックclkに同期した12ビットのD-FFと考えてください。 このレジスタの出力はcntという名前の出力になっています。 またADDは加算回路で、ADDの下側の入力は、s10などの信号に応じて 10, 60, 0のいずれかが与えられます。

2.カウントダウン用データパス



続いて、カウントダウンを行っている状態(RUN)の データパスを考えてみます。 カウントダウン時には、1秒ごと、つまりclkの立ち上がりごとに 時間カウント値cntが1ずつ減っていくことになりますので、 さきほどと同様に、REGとADDを組み合わせ、 ADDの下側の入力として-lを与えています。

またカウントダウン終了を検出するために、 時間カウント値cntが0となったときに、カウントダウン終了を 示す出力done=1とするために、cntと0とをコンパレータCMPによって 比較しています。

3.全体のデータパス



ここまでで見てきた1.と2.は、大部分が共通の構成ですので、 両者をまとめ、このようなデータパスにまとめることができます。 ここで、セレクタSELに与えられている信号は次のような意味です。 これをふまえてデータパスをVHDLで記述すると 次のようになります。(p.99: 要点のみ抜粋)
entity datapath is
  port(
    clk, rst, p10, p60, m1: in std_logic;
    done: out std_logic;
    cnt: out std_logic_vector(11 downto 0));
end datapath

architecture arch of datapath is
  signal reg12_out, reg12_in: std_logic_vetor(11 downto 0);
  signal sel_out: std_logic_vector(6 downto 0);
  function sign_extend (a: std_logic_vector(6 downto 0))
    return std_logic_vector is
  begin
    if (a(6) = '1') then return("11111" & a);
    else return("00000" & a);
    end if
  end sign_extend

begin
  process (p10, p60, m1) begin -- selector
    if (p10 = '1') then sel_out = "0001010";    -- "10"
    elsif (p60 = '1') then sel_out = "0111100"; -- "60"
    elsif (m1 = '1') then sel_out = "1111111";  -- "-1"
    else sel_out = "0000000";
    end if;
  end process;

  process (reg12_out, sel_out) begin -- adder
    reg12_in <= reg12_out + sign_extend(sel_out);
  end process;

  process (clk, rst) begin -- register
    if (rst = '0') then reg12_out = "000000000000";
    elsif (clk'event and clk = '1') then re12_out <= reg_in;
    end if;
  end process;

  process (reg12_out) begin -- comparator
    if (reg12_out = "000000000000") then done <='1';
    else done <= '0';
    end if
  end process;
  cnt <= reg12_out;
end arch;

制御部

続いて、データパスを制御する制御部を設計してみます。 これは、実は最初に例として紹介した状態遷移図のような 動作をするステートマシンを設計することになります。 制御部の入出力は、さきほどの全体の入出力に加えて、 データパスの制御のためのp10, p60, m1があります。

この制御部のVHDL記述は次のようになります。(p.103: 要点のみ抜粋) 前半のcase文が、まさに状態遷移図に対応している 次状態決定回路になっていて、 また後半のcase文が、出力決定部になっていることに注意しましょう。 またステートマシンの本質である状態遷移は、2つ目のprocess文で 記述されていることにも注意しておきましょう。

entity control is
  port(
    clk, rst, set, run, s10, s60, done: in std_logic;
    p10, p60, m1: out std_logic);
end control;

architecture arch of control is
  type t_state is (IDLE_ST, SET_ST, RUN_ST, DONE_ST);
  signal state, next_state: t_state;
begin
  process (state, set, run, done) begin -- next state calc
    case state is
      when IDLE_ST =>
        if (set = '1') then next_state <= SET_ST;
        else next_state <= IDLE_ST;
        end if;
      when SET_ST =>
        if (run = '1') then next_state <= RUN_ST;
        else next_state <= SET_ST;
        end if;
      when RUN_ST =>
        if (done = '1') then next_state <= DONE_ST;
        else next_state <= RUN_ST;
        end if;
      when DONE_ST => next_state <= IDLE_ST;
    end case;
 end process;

  process (clk, rst) begin -- state register
    if (rst = '0') then state <= IDLE_ST;
    elsif (clk'event and clk = '1') then state <= next_state;
    end if;
  end process;

  process (state, s10, s60, done) begin -- control signals
    p10 <= '0'; p60 <= '0'; m1 <= '0';
    case state is
      when IDLE_ST => null;
      when SET_ST =>
        if (s10 = '1') then p10 <= '1';
        elsif (s60 = '1') then p60 <= '1';
        end if;
      when RUN_ST =>
        if (done /= '1') then m1 <= '1'; -- /= : Not Equal ※←教科書の記述が間違っているようです
        end if;
      when DONE_ST => null;
    end case;
  end process;
end arch;

トップ階層

ここまでで設計したデータパス、制御部を統合した タイマ全体のトップ階層をVHDLで記述すると 次のようになります。(p.106: 要点のみ抜粋) データパス、制御部をインスタンスで呼び出して接続していることに 注意しておきましょう。
entity timer is
  port(
    clk, rst, set, run, s10, s60: in std_logic;
    done: out std_logic;
    cnt: std_logic_vector(11 downto 0));
end timer;

architecture arch of timer is
  component control
    port(
      clk, rst, set, run, s10, s60, done: in std_logic;
      p10, p60, m1: out std_logic);
  end component;

  component datapath
    port(
      clk, rst, p10, p60, m1: in std_logic;
      done: out std_logic;
      cnt: out std_logic_vector(11 downto 0));
  end component;

  signal p10, p60, m1, done_tmp: std_logic;
begin
  i0: control port map(
    clk, rst, set, run, s10, s60, done_tmp, p10, p60, m1);
  i1: datapath port map(
    clk, rst, p10, p60, m1, done_tmp, cnt);
  done <= done_tmp;
end arch;  

構成を意識しないVHDL記述

上記の例では、タイマを、データパスと制御部に分けて VHDLで記述し、最後に両者を統合する、という手順で設計しました。 実はステートマシンの設計では、このようにデータパスと制御部を 分けずに、次のようにまとめて回路を記述することもできます。 (p.109: 要点のみ抜粋) この記述の方が、ステートマシンの動作の記述としては 直感的でわかりやすいかもしれません。

ただし、一般に、このような構成を意識しない記述から 論理合成によって得られる回路は、構成を意識した記述から 得られる回路よりも、「質」が低くなる傾向があります。 すなわち回路規模が大きく、消費電力が大きくなる傾向があります。 そのため、なるべくデータパスと制御部を分けて記述するほうが、 とりあえず動く回路、では不十分な場合で、 より質の高い論理回路を得たい場合は、得策となるようです。

architecture arch2 of timer is
  type t_state is (IDLE_ST, SET_ST, RUN_ST, DONE_ST);
  signal state: t_state;
  signal cnt_tmp: std_logic_vector(11 downto 0);
  signal sel_out: std_logic_vector(6 downto 0);
begin
  process (clk, rst) begin
    if (rst = '0') then
      cnt_tmp <= "000000000000";
      done <= '0';
      state <= IDLE_ST;
    elsif (clk'event and clk = '1') then
      case state is
        when IDLE_ST =>
          if (set = '1') then state <= SET_ST;
          else state <= IDLE_ST;
          end if;
        when SET_ST =>
          if (s10 = '1') then
            cnt_tmp <= cnt_tmp + "000000001010";
          elsif (s60 = '1') then
            cnt_tmp <= cnt_tmp + "000000111100";
          end if;
          if (run = '1') then state <= RUN_ST;
          else state <= SET_ST;
          end if;
        when RUN_ST =>
          cnt_tmp <= cnt_tmp - "000000000001";
          if (cnt_tmp = "000000000001") then
            done <= '1';
            state <= DONE_ST;
          else state <= RUN_ST;
          end if;
        when DONE_ST =>
          done = '0';
          state <= IDLE_ST;
      end case;
    end if;
  end process;
  cnt <= cnt_tmp;
end arch2;

配布資料
この回のソボクな疑問集
戻る