公開日:2021/02/13
title.png

15th Season


#15-2 論理回路を設計する

 前回の「#15-2 FPGAボードを買ってみた」にて、FPGAボード(Sipeed Lichee Tang Primer)の動作確認用にちゃちゃっと作った(Lチカ)回路の中身をさらっと解説しておこうと思います。

 本HPではこれ以降、FPGAボードに書き込む回路を生成するためにVerilog-HDLを使いますが、「Verilog-HDLの文法は知っていて、ある程度使える」前提で進みます。
 Arduinoのプログラミングもそうでしたからね。本HPでは、C言語の文法について細々と解説したことはありませんでした。Verilog-HDLも同じ扱いです。
 C言語に関して言うと、割り込みハンドラの使い方とか、インラインアセンブラの使い方とか、ちょっと特殊だったり、ポイントとなる部分については解説してきたので、その辺のスタンスはVerilog-HDLでも変わりません。HDLを使ったハードウェア設計フロウについての参考にしていただけるような内容にしたいと思っています。

 Verilog-HDLの文法や記述法については下のリンクから検索してみてください。

  Verilog-HDLを検索(別ウインドウが開きます)

※かく言う筆者も現役の設計を離れて久しいので、思い出しながら進めて行きます(1^^;)。

お題はLチカ

 お題はLチカとします。Arduinoのソフトウェアにおける、以下のプログラムと同じ様に光らせます。

  void setup (void)
  {
    pinMode (LED_BUILTIN, OUTPUT);
  }
  void loop (void)
  {
    digitalWrite (LED_BUILTIN, HIGH);
    delay (150);
    digitalWrite (LED_BUILTIN, LOW);
    delay (150);
    digitalWrite (LED_BUILTIN, HIGH);
    delay (150);
    digitalWrite (LED_BUILTIN, LOW);
    delay (550);
  }


 「2回短く光って、長めの消灯」、これを1秒周期で繰り返す点滅パタンです。「イメージが湧かない」と言う方はArduino UNOにコンパイル&書き込みして試してみてください。UNOのオンボードLED(LED_BUILTIN)がチカチカ点滅します。

 Arduino UNOにはATmega328というMCUが搭載されているので、上のプログラムをATmega328で実行することで、LEDを光らせています。LEDを光らせる制御は、ソフトウェア制御です。
 光らせて、150ms待って、消して、150ms待って、……と、実行すべき命令を順々に記述するソフトウェアは、「手続き型」の動作記述と呼ばれることがあります。

 今回はこの点滅を、ATmega328などのMCU無しで、ハードウェアだけで制御して光らせようとしています。

 手続き型のソフトウェアと異なり、ハードウェアはイベントドリブン型の動作記述が必要なので、考え方をガラッと変えないと、うまく形になりません。

RTL

 回路を記述する言語をHDL(Hardware Description Language)と呼びます。今回使用するVerilog-HDLは、HDLと言う括りの中のVerilogと言う言語です。
 HDLで記述した回路は、そのままではただのテキストデータなので、実際の論理回路に変換しなければなりません。論理回路(ゲートレベル)への変換作業を論理合成(Logic Synthesis)と言います。論理合成するためのツールが存在するので、この変換はツール任せで実行することが可能です。

 現在のところ、HDLで記述した回路を論理合成するためには、HDLがRTLと言う形式になっていなければなりません。

 RTL(Register Transfer Level)は「レジスタ転送レベル」と直訳されているのですが、Registerとはクロックで制御された記憶素子の事で通常はフリップフロップを指します。Transferとはレジスタ間の転送と言う意味で、信号がレジスタ間を伝搬するイメージです。
 転送される過程で組み合わせ論理を通る、と解釈すると理解しやすいでしょうか?

 突き詰めるとRTLとは、「フリップフロップと、組み合わせ論理(すなわち順序回路)を単に言語形式で記述した物」に相違ないのです。図面にANDゲートやORゲート、フリップフロップFF等を配置して、その間を配線で繋いで作るゲートレベルと大差無いのですよ。
 表現方法がスケマティック(ゲートレベル)から言語(RTL)に変わっただけです。

 ただ、言語になったことで抽象度は一気に上がっています。
 例えば、加算器を作ろうと思ったら、ゲートレベルでは1ビット全加算器(フルアダー)を作り、それを32個並べて32ビット加算器をつくる、と言う手順でした。
 RTLでは、「assign C[31:0]=A[31:0]+B[31:0];」で終了です。
 「抽象度が上がる」→「より複雑な論理回路をより容易に記述できる」と、理解してください。


 Verilog-HDLと言う言語は、実はRTLだけでなく、ソフトウェア(C言語など)で記述しているかのような手続き型のプログラムも組むことが出来るようになっています。ですが、手続き型の記述では論理合成できないので、今からVerilog-HDLを使って作ろうとする回路記述は、RTLにしなければなりません。

 手続き型の記述については、回路記述以外の使い道があるので、後ほど解説します。

ステートマシン

 RTL設計における制御の基本は、有限状態機械(FSM;Finite State Machine)です。本HPでは、単にステートマシンと呼ぶことにします。
 ステートマシンを使った制御回路は、以下の様に設計します。

 さっきのソフトウェアによるLチカをステートマシンに起こすと、下図の様になります。

a152_statedg.png

 上の様に描かれた図の事を状態遷移図と言います。
 上図では、LEDが点滅して行く過程を各々ステート(T0〜T3)に割り当てました(↓)。

  T0: LEDが150ms間点灯しているステート、LED_BUILTIN=LOW
  T1: LEDが150ms間消灯しているステート、LED_BUILTIN=HIGH
  T2: LEDが150ms間点灯しているステート、LED_BUILTIN=LOW
  T3: LEDが550ms間消灯しているステート、LED_BUILTIN=HIGH

※FPGAボード上のオンボードLEDは、LOWドライブで光る回路でした。従って、UNOのプログラムとはLED_BUILTINのLOW/HIGHが逆転しています。

 T0〜T3をステートコードと呼びます。回路に落とす場合には、何らかの2進数値にしておく必要があるので、例えば以下の様に数値(コード)を割り当てて使います。

  parameter [1:0] T0=2'h0;
  parameter [1:0] T1=2'h1;
  parameter [1:0] T2=2'h2;
  parameter [1:0] T3=2'h3;


 今回は4状態と言う、とても小さなステートマシンなので、ステートコードは2ビットで充足します。
 細かいことを言うと、ここで割り当てたステートコードに対して論理合成を実施することになるので、ステートコードの割り当て方によって、完成する論理の大きさ(ゲート数)が変化する可能性があります。
 最適なステートコードの割り当てとかを考え出すとキリが無いので、あまり考えたことは無いです(1^^;)。今回も順番に0〜3を割り当てて使っています。


 動作に目を移すと、ステートマシンはタイマから所望の時間になったことを知らせる信号OVFを受けるまで、現在のステートに留まっています。OVFがアサートされると、順次、次のステートへ遷移して行きます。
 そう、ステートマシンとは別に、150msを3回と550msを1回計測するためのタイマ回路が必要です。タイマ回路については次節で検討します。

 「条件が成立する(イベントが起きる)と、状態(ステート)が変化していく」、これがイベントドリブン型の動作記述です。
 ステートが変化して、周辺回路への指示(信号)が変わる(イベントを起こす)と、そのイベントが周辺回路に伝わって、指示に従った動作に変化して行きます。発生したイベントは、複数の回路に対して同時・並列に伝達されて、同時・並列に動作します。これが、ソフトウェア(手続き型)との最大の違いです。


 そして、所望の動作に対して、どこにどんな状態を割り当てて、どうやって制御して行くか?と言う考え方は一通りではなく、必勝法も無いので、どのように設計するかは設計者の知恵と経験で決まります。

タイマを作る

 今回使用するクロックは、恐らく、24MHzです(1^^;)。ドキュメントを漁っていた時にどこかで見かけました。
 クロックclk=24MHzと仮定すると、150msのカウントに必要なのは3,600,000カウント→22ビットです。550msなら24ビットほどのタイマが必要となります。
 今回使うFPGAボードは結構速そうなので、そのまま24ビットタイマを作ってしまっても十分動作できると踏みました。まあ、念のため後でタイミング解析して確認しましょう。

※もし、タイミング的にキツかったら、プリスケーラ8ビット+16ビットタイマみたいな構成にしないとなりません。


 ステートマシンからタイマ周期TPRD[23:0]信号を受け取って、カウンタがTPRDと一致したら、次のクロック立ち上がりエッジでOVF=1をアサートしてカウンタを24'h000000にリセットします(↓)。

  always @(posedge clk)
    begin
      if (!rst_n)
        begin
          cnt[23:0]<=24'h000000;
          OVF<=1'b0;
        end
      else if (cnt[23:0]==TPRD[23:0])
        begin
          cnt[23:0]<=24'h000000;
          OVF<=1'b1;
        end
      else
        begin
          cnt[23:0]<=cnt[23:0] + 24'h000001;
          OVF<=1'b0;
        end
    end


 ゲートレベル(スケマティック)でこんな回路を作ろうと思ったら大変ですけどね〜。RTLならこれ(↑)だけです。
 この記述だと、カウンタは24'h000000からTPRD[23:0]までカウントして、24'h000000に戻ります。つまり、周期はTPRDではなくて、TPRD+1です。
 「周期をTPRDにしたい」とか思っても、以下の様に記述しないでください。

    :
  else if (cnt[23:0]==TPRD[23:0] - 24'h000001)
    :


 ここに演算子(−)を使ってしまうと、論理合成した時にちゃんと24ビット減算器(デクリメンタ)を生成されてしまいます。無駄なゲートを生成してしまうばかりか、信号伝搬時間も増加させてしまいます。


 周期の判定についても少し問題提起しておきます。

  else if (cnt[23:0]==TPRD[23:0])

    ↓(?)↑

  else if (cnt[23:0]>=TPRD[23:0])

 下の記述(「>=」)の方が、万一何らかのバグや故障などによってカウンタcnt[23:0]がTPRD[23:0]を超える値にセットされてしまった場合にも、ロバストで正常動作に戻すことができる安全な記法です。ソフトウェアであれば確実に「>=」の方を選択します。
 しかし、ハードウェアの場合、「==」なら単純なコンパレータが生成されますが、「>=」の場合にはマグニチュードコンパレータが生成されて論理規模も大きくなり、信号の伝搬時間も遅くなってしまいます。

※「故障」を考えない限り、「==」の記述で全く問題なく動作します。「>=」は冗長な記述で、冗長な回路を生成します。

 一概に「こっちにしなさい」と、言えるような単純な問題ではありませんので、「安全」と「コスト増加量」を相談の上、記述を変えることになります。

Excelでステートマシンを設計する

 今回設計するステートマシンは、「設計仕様書」的には、以下表の様に記述することが出来ます。

a152_spectab.png

 追加で以下の定数を定義しています。

  parameter [23:0] TPRD_S=24'h36EE80 - 24'h1;  // 150ms for clk=24MHz
  parameter [23:0] TPRD_L=24'hC96A80 - 24'h1;  // 550ms for clk=24MHz


※parameterは定数なので、ここで使った「−24'h1」等のマイナス(−)演算子に対して、減算器は生成されません。

 上の表をVerilog-HDLの記述にコーディングすると、以下の様になります。

  casex ({ STAT[1:0],OVF })
  { T0,1'b0 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T0,1'b0,TPRD_S };
  { T0,1'b1 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T1,1'b1,TPRD_S };
  { T1,1'b0 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T1,1'b1,TPRD_S };
  { T1,1'b1 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T2,1'b0,TPRD_S };
  { T2,1'b0 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T2,1'b0,TPRD_S };
  { T2,1'b1 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T3,1'b1,TPRD_L };
  { T3,1'b0 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T3,1'b1,TPRD_L };
  { T3,1'b1 }: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T0,1'b0,TPRD_S };
  default: { NEXT[1:0],LED_BUILTIN_t,TPRD[23:0] }={ T0,1'b0,TPRD_S };
  endcase

  always @(posedge clk)
    begin
      if (!rst_n)
        STAT[1:0]<=T0;
      else
        STAT[1:0]<=NEXT[1:0];
    end


 正しいVerilog-HDLの記述方法なのですが、ちょっと煩雑じゃありませんか?先に示した仕様表はすっきりと読み易いのに、Verilog-HDLの方はごちゃごちゃしていて読み難い。3入力3出力程度の小さなステートマシンでこれですから、もう少し大きなステートマシンになって来ると、Verilog-HDL記述では可読性が大幅に低下してしまい、まともにコーディングできないでしょう。

※可読性の問題でコーディングエラーを仕込んでしまうと、検証やデバッグで無駄に時間を使ってしまうことになりかねません。

 「餅は餅屋」、「表は表計算ソフトウェア」なので、Excelで入力した表からVerilog-HDLの記述に変換するスクリプトを作ってしまいましょう。そうすれば、設計仕様書として入出力表を作ってしまえば、スクリプトで自動的にVerilog-HDLの記述に変換することが出来ます。

  入出力表→Verilog-HDL変換スクリプト目標仕様:

 我ながら、上の目標仕様だけではわかり難いので、サンプルデータをリンクしておきます。

  サンプルシート:blink_fsm.txt

 タブ区切りのテキストファイルになっていますので、Excel等の表計算ソフトウェアに読み込んでみてください。以下図の様に見えるはずです。

a152_tab2v.png


 続いて変換スクリプトをリンクします。

  変換スクリプト:tab2v.olv

 拙作Olive+のスクリプトですので、実行にはWindowsにOlive+のインストールが必要です。


 上のサンプルデータ(blink_fsm.txt)をスクリプト(tab2v.olv)で変換すると、以下の様なVerilog-HDL記述が出力されます。

  変換後サンプル:blink_fsm.v

 このファイルは表の論理を変更する度にスクリプトで自動変換して生成されるので、他のファイルにコピー&ペーストなどして使うより、そのまま独立したファイルとして扱った方が楽だと思います。

完成したRTL

 ステートマシン(モジュール名blink_fsm)、24ビットタイマ(同blink_tim)をそれぞれ別なモジュールとして定義して、それを最上位モジュールblinkでインスタンスして配線を接続し、全体のRTLが完成しました。以下にリンクしておきます。

  完成版RTL:blink.v / blink_fsm.v

 前回の手順に従って、この(↑)RTLをプロジェクトに追加してコンパイル&書き込み(コンフィギュレーション)すると、FPGAボード上のオンボードLEDが赤く点滅します。

 ここでは、コンパイル結果の中で、使用した素子数について確認しておきます。
 TD IDEの左下、「FPGA Flow」の中の「Design Summary ⇒ Physical Summary」をダブルクリックすると、マッピングされたLUT数やF/F数などを確認することが出来ます。

  #lut  32 out of 19600 0.16%
  #reg  27 out of 19600 0.14%

※データシートにはLUT数23,520個と記載されているのですが、上の通り、TD IDEのレポートでは最大19,600個と、差異があります。理由は不明。

 LUT数、F/F数共、全体の0.2%にも満たない数しか使っていません。まだまだスカスカです(1^^)。もっとずっと大きな回路を作ることが出来そうです。

 また、以下図がSchematic Viewerで確認した最上位の回路図です。

a152_schem.png

 タイマtimやステートマシンfsmの箱も、ダブルクリックするとその中の回路を視覚的に確認できます。
 不要な信号を引き込んでいたり、必要な信号出力を記述していなかったりすると、浮き端子となって見えますし、全体の信号の流れやブロック毎の関係も確認できるので、一通りRTLが完成してコンパイルエラーが無くなったら、一度は見ておくべきです。

タイミング検証

 イメージ的にだいぶ軽い回路だったので、前回はタイミング検証無しでぶっつけ本番してしまいましたが、一応、タイミング検証(STA;Static Timing Analysis)をかけておきましょう。
 TD IDEから、前回作成したプロジェクトBLINKを開いて、「Toolsメニュー ⇒ Timing Wizard」を実行します。

a152_tmgwiz.png

 開いたウインドウの左側の枠内で「Created Clock」を選択し、上図の様にクロックを定義します。

 TD IDEから「Processメニュー ⇒ Run」を実行します。終了したら、左下の「FPGA Flow」から「Design Summary ⇒ Timing Summary」をダブルクリックして、結果を確認します。
 SetupとHold共、確認する必要があります。両方とも、Slack欄に記された数値がタイミング制約に対する余裕度となっていて、Slack>>0が必要です。Slackが負の値になっている場合は、タイミングエラーとなります。そのままFPGAボードに書き込んでも、タイミング違反によって正常動作しない場合があります。何らかの処置が必要です。

 今回のデザインを確認したところ:

  Setup Slack: 34.403ns
  Hold Slack: 0.515ns

と、エラーは有りませんでした。
 結果的には、24ビットタイマも全く問題ありませんでした。もっとビット幅が広くても、もっとクロック周波数を上げても、まだまだ余裕があります。

シミュレーション

 Verilog-HDLと言う言語は、実はRTLだけでなく、ソフトウェア(C言語など)で記述しているかのような手続き型のプログラムも組むことが出来るようになっています。この節ではその使い道について解説します。

 前節で完成させたRTLですが、一応、シミュレーションして動作を確認しておきます。シミュレーションの実行方法と、波形の確認については前回解説済みです。
 今回は、テストベンチ(test_blink.v)の作り方について解説しておきます。

 通常、何らかの回路をテストする時には、以下の様に、当該回路(DUT;Design Under Test)の入力信号を変化させるパタンを入力して、その挙動を確かめます。

  入力信号A=1、入力信号B=0で始めて、
  10ns待って、
  A=0にすると同時にB=1にする、
  20ns待って、
  B=0に戻す
   :
   :

 これは、明らかに手続き型の動作記述が得意とする表現です。
 Verilog-HDLは、回路記述のためのRTLと、テストベンチ記述のための手続き型記述の両方を、1つの言語で記述可能なHDLとして市民権を得てきた歴史があります。

※回路記述はXXXと言う言語、シミュレーション記述はYYY言語、の様に使い分ける必要が無く、回路記述もシミュレーション記述もVerilog-HDLで統一できる。このため、Verilogシミュレータさえあれば、だれでもその回路をシミュレーションして検証できることになる。これが利点。

 blink.vのテストベンチであるtest_blink.vにも、上の例と同じ様な記述が見つかります。

  // stimulus
  initial
    begin
      $dumpfile ("test.vcd");
      $dumpvars (0, test);
      rst_n=1'b0;
      #100;
      rst_n=1'b1;
      #10000;
      $finish;
    end


 initialステートメントは、シミュレーションの時間0から動作を記述するためのステートメントです。この中では、「#<数値>;」の記法で時間を進めることが出来るので、上のコードでは、リセット信号rst_nを最初の100ns間0にして、その後1に変化させてから10,000ns経ったら、シミュレーションを終了($finish)しています。
 最初の$dumpfile()などは、シミュレーション波形データVCDファイルを出力しなさいと言うシステムタスクです。

 Verilog-HDLでありながら、C言語などと同じ様な手続き型の記述が出来て、その様に動作します。このように、各信号変化を実行すべき順番に記述して行くパタンを「スティミュラス」と呼びます。

 VerilogがC言語と違うのは、元々がイベントドリブン型の回路設計を生業とする言語であるため、「#<数値>;」の様な記述が無い限り、シミュレーション時間は進まないし、従って、同時・並列的に信号を変化させる記述ができることです。
 例えば、テストベンチ(test_blink.v)には、以下の様な記述も見られます。

  // toggle clk every 20ns
  always
    begin
      clk=1'b1;
      #20;
      clk=1'b0;
      #20;
    end


 これは、シミュレーション時間0から、begin〜end間の処理を実行せよ、と言う指示になります。alwaysステートメントには実行条件が記載されていないので、begin〜end間の実行が終了すると、すぐさまalwaysの実行条件が成立して、再びbegin〜end間が実行されます。これを永久に繰り返すことで、クロックclkを任意の周波数で入力することが出来ます。

 このalwaysステートメントも、最初に見たinitialステートメント内の信号変化と同じ時間軸で実行されているので、結果として:

と、いう動作を記述したことになります。このように、クロック記述とスティミュラスを一連の手続き(1つのinitialステートメントなど)にまとめる必要は無いのです。


 今回のテストベンチで使っているもう1つのテクニックは、シミュレーション時間の短縮です。仕様上、今回回路のLED点滅は1秒周期なので、本来、1秒間以上のシミュレーションを実行しないと、回路の動作を確認できません。
 しかし、1秒ものシミュレーションは、とても時間がかかるし、波形データも莫大になってしまうので、シミュレーションの時だけ、タイマのコンペアマッチ周期TPRD[23:0]を短くして動作検証を実行しました。

 まず、ステートマシン(blink_fsmモジュール、blink_fsm.v)の中で、以下の様な「`ifdef」記述が挟んであって、SIMULATIONというマクロが設定された時には、タイマの周期を指定するTPRD[23:0]に、150ms/550msではなく、15クロック/55クロックを出力するようにしてあるのです。

  // timer period
  `ifdef SIMULATION
  parameter [23:0] TPRD_S=24'd15 - 24'd1;
  parameter [23:0] TPRD_L=24'd55 - 24'd1;
  `else // SIMULATION
  parameter [23:0] TPRD_S=24'h36EE80 - 24'h1;  // 150ms for clk=24MHz
  parameter [23:0] TPRD_L=24'hC96A80 - 24'h1;  // 550ms for clk=24MHz
  `endif // SIMULATION


 これによって、マクロSIMULATIONが定義されていた場合には、点滅周期が1秒の代わりに100クロックとなったシミュレーションを実施することが出来、シミュレーション時間を大幅に短縮することが出来ます。

 シミュレーション用記述を有効とするため、テストベンチ(test_blink.v)の先頭で、以下の様にマクロSIMULATIONが定義されています。

  `define SIMULATION

 この時、Verilogシミュレータにファイルを渡す順番は大事です。Verilogは、渡された順番にストリーム(流れ、UNIXのcatコマンドで1本のファイルにまとめてしまったようなイメージ)として各ファイルの記述を読み込みます。

  実行サンプル:【 】内はCygwinシェルウインドウからの実行コマンド
  【iverilog test_blink.v blink.v blink_fsm.v

 この(↑)順番では、test_blink.vの先頭でマクロSIMULATIONが定義され、その後でblink_fsm.vが読み込まれるので、TPRD[23:0]には、シミュレーション用の短い周期が選択されます。

  【iverilog blink.v blink_fsm.v test_blink.v

 今度(↑)の様に逆にしてしまうと、先にblink_fsm.vを読み込みます。その時にはマクロSIMULATIONが`defineされていないので、実回路用の非常に長い周期が選択されてしまいます。後から読み込んだtest_blink.vの中で「`define SIMULATION」しても時既に遅しです。

 同じことが「`timescale」などのディレクティブでも起こります。時々、ビヘイビアモデルの中で「`timescale」を指定しているものを見かけますが、そのモデルを読み込む順番によっては、テストベンチやその他のビヘイビアモデルに、ファイルを超えて影響が広がってしまい、正常動作しなくなってしまう場合があります。
 シミュレータに渡す順番は重要な意味を持っているので、注意深く順番を決めなくてはならない、と言う事です。


  「実回路と違う定数でシミュレーションしても検証の意味がないのでは?

 Yes。一理あります。でも大丈夫。
 実回路定数(1秒周期)の確認は、ちゃんとFPGAボードに書き込んだ後、実回路で検証しましたから(前回確認済みです)。


 一気にだいぶブッチ切った感があります。ただ、一通り重要なことについて解説しきった感もあります(1^^;)v。最後までじっくり読み切っていただいた方には感謝申し上げますm(1__)m。


btn_arduino.png

btn_prev.png btn_next.png

home