2019年1月28日月曜日

ありがちな非推奨記述

「なるべく同系統の信号は一つのalways文にまとめて記述するのが良い」という話を各所の複数の人から聞いたことがある。自分は(かなり)過去、Verilog外部研修で、always文はなるべく分けて書くように習った経験があり、なるべく束ねる説に対し、それはちがうよ・と指摘していた。簡単にいうと、束ねるのは記述漏れや考慮漏れの原因になるから。ところが、最近また別の方々の書いたRTLを見たところ、極力一つのalwaysにまとまるように書かれていた。結構ベテランと思われる方々。多分、このプロジェクトにはじめの方からかかわっている人がそのような書き方をしたため、他のかたもそれに習ったのだろうとおもわれるが。さて、どちらが正しいのだろう。自信が無くなってきたのでちょっと調べてみた。

結論としては、なるべく分けるが正解と思う。
某スタイルガイドによれば、次のようにある。
Point ①組み合わせ回路のalways文について、ラッチを生成する危険をさける ②回路構造を意識したalways文記述を行う ③1つのalways文の中で複数の出力信号を定義することは避ける
何れも「束ねるのは記述漏れや考慮漏れの原因になるから」というもの。

①について

always内のif文に複数の出力を並べて書く際は、すべての条件(if/else if/else)にすべての出力を書きましょうということ。
RTLの意識があまりなく、ソフトウェア的な発想で記述される方にありがちな気がする。if (...) a=X,b=Y; else if (...) a=Z; else if (...) b=W;のような書き方はやめましょうということ。
組み合わせ回路において、一つのalways文中に記載しているにもかかわらず、その条件文を目的とする出力のみをその条件文に記述し、目的としない出力は記述しない・というスタイル。このような回路は、「記述されていない=状態保持」を意味するので、「不要な条件が来たら状態をラッチする」という指示になっている。これにより、機能的には意図通りに動くとしても、無駄な回路が生成される可能性が生じ、タイミング要件が厳しくなったり、非活性の回路生成によりカバレッジ率を低下させる原因となる。

②について

回路イメージの単位を意識するように・ということ。意図するところは、回路の見通しを良くするべき・ということ。それにより、合成後の回路の予測性が向上し、合成後の回路のタイミング要件を満たしやすくなる。見通しがよく、デバッグ効率が向上するというメリットが得られる。具体的には、出力(F/Fなり組み合わせ出力なり)を束ねるためにalways文のセンシティビティリストと制御信号(if文の条件に)無駄な信号を入れないこと。回路Aと回路B、・・・を一つのalways文で書こうとしたとき、回路A用の制御信号、回路B用の制御信号、・・・をすべての回路に当てはまるように追加することになる。これは冗長回路を生成する可能性、記述ミスの可能性、回路イメージをつかみにくくする原因となる。わかりやすくするために束ねたいのだろうが、かえって人にもツールにもわかりにくいものとなるのを避けましょう・ということ。

③について

一つのalways文に、「if~end」,「if~end」,「A=XYZ;」のような記述、されにはこれら何れかの出力が何れかの入力になるような記述を入れ込まないようにしましょう・ということ。意図することろは、不要なプライオリティ回路が生成される可能性を避ける、回路の見通しを良くする・ということ。always文は上から順序処理されるので、always文内の記述順によりプライオリティが生じる。これにより、下位にかかれた回路のイネーブル条件として上位の回路が使われる可能性がある。また、これらの出力をブロッキング記述にしている場合、上から順に実行される(プライオリティが形成される)ことを意味し、それぞれに出力と入力の関係を持たせる場合、記述順が逆にならないよう考慮する必要がある。また、ある出力が、より上位に記述された回路の入力となる場合、上位の回路が活性化しない危険がある。さらには活性化させるためにセンシティビティリストに出力を追加した場合、alwaysがループする危険がある。自由な書きやすい記述スタイル(?)だが、RTLレベルを意識する必要があるということ。

ブロッキングとノンブロッキング

ブロッキングとノンブロッキング、使い方とネーミングが逆では?と感じている方がいるようなので、コメントしておく。

Point
  • b=a;c=b;はブロッキング。b=aが実行されてからc=bが実行され、c=b=aとなる。b=aはc=bの実行をブロックしている。⇒ブロッキング
  • b<=a;c<=b;はノンブロッキング。b<=aとc<=bは同時に実行され、b=a,c=bとなる。b<=aはc<=bの実行をブロックしていない。⇒ノンブロッキング
ちなみに、逆では?と感じるのは、「=は一連のブロックとして関連性を持つからブロッキング(ブロック(塊)化している)では?」、「<=は並列に実行され関連性を持たないからノンブロッキング(独立したブロック(塊))では?」ということ。確かにこれを書くと混乱しそう・・・。

ステートマシン

ステートマシンの型にはムーアとミーリがある。

 ステートマシン出力の定義が自分のなかでふわっとしているので、次のように定義する(自分なりの定義)。

ステートマシンはステートマシン入力、ステート信号、ステートマシン出力から構成される。
ステートマシン出力は、HDL記述上ステート信号と同じalways文の中で生成し、データパスの制御用に使われる信号である。
機能的に表現すると、データパス(ステートマシン外部)で使うためのステート信号である。

よって、ステート信号と等価なものであり、ステート信号をステートマシン記述から出力するのであれば、ステート信号もステートマシン出力である。

ステート信号はステートマシン自身の遷移を制御するための信号で、ステートマシン出力はデータパスを制御するための信号なので、目的は異なるが信号の生成元は同じである。ステート信号をデータパス制御用に使うのが効率的であればそれでよい。ただし、例えば(いわゆる)next_state信号をステートマシン出力として使う場合、データパスではif (next_state == STEP2)のようにバスのコンペア回路で使うことが想定され、ステート信号がある程度多きなバスで多くのデータパスで使用される場合、配線遅延が大きくなることが懸念される。ステート信号のバスをすべて使わなくても、ステート信号よりも小さな1ビットなどの信号で制御できるのであればその方がタイミング設計上有利なため、ステート信号(およびミーリ型の場合はステートマシン入力)をデコードした信号を作り、それをステートマシン出力として使う。

このalwaysがクロック同期であっても組み合わせ回路であってもステートマシン出力と呼ぶ(データパスの制御に使うステート信号であるという目的で律速される)。結局ここは考え方、抑え方の問題なので重要ではないのかもしれない。ただ、ミーリ型の出力をF/Fで切ってから使うステートマシンというのもスタイル上存在するため、ステートマシン出力の定義がふわっとしていると、作成する信号がステートマシン出力なのかデータパスなのかが曖昧になってしまい、仕様書上の表現もふわっとしてしまうため、明確にしておきたい。つまり、ミーリ型の出力をF/Fで切ったスタイルの場合、この(同じalwaysのなかで)F/Fで切った出力をステートマシン出力と呼ぶ。

それと、ステートマシン出力とは、ステートマシンを反映した制御信号で、ステート信号、またはステート信号とステートマシン入力のみから生成される信号である。
ステートマシン入力以外の信号を組み合わせた信号もステートマシン出力と呼ぶとすると、ステートマシンのalwaysから出力された信号(ステートマシン出力)をつかって生成した制御信号もすべてステートマシン出力?ということになってしまい、また仕様定義がぼやけてしまうため。

HDL記述上もこのあたりの考えがふわっとしていると、ステートマシンとその他の信号が仕様書と合わなくなってしまうので注意したい。コーディングスタイルとしても、ステートマシンとデータパスは分離することが推奨されている。

Point
ステートマシン出力とは・・・
  • HDL記述上、ステート信号と同じalwaysの中で生成し、データパスの制御用に使われる信号。
  • 機能上は、ステート信号はステートマシン自身の遷移を制御するための信号で、ステートマシン出力はデータパスを制御するための信号。
  • ステートマシンを反映した制御信号で、ステート信号、またはステート信号とステートマシン入力のみから生成される信号。
  • ムーア型の場合、ステートマシン出力はステート信号から一意に作られる組み合わせ回路出力である。
  • ミーリ型の場合、ステートマシン出力にステートマシン入力が使われる。
  • ミーリ型の場合、F/Fで切られている場合と切られていない場合がある。切られている場合、制御信号はステートマシン入力から1クロック遅れたタイミングとなる。切られていない場合、ステートマシン入力がステートマシン出力に抜けるパスが形成されるので、タイミングパスが長くなりやすい。

ムーア型の特徴

ムーアは、ステートマシンとステートマシン出力が一対一のもの。記述上はステートマシンを示すcase分の直下にかかれ、if分による分岐には含ませない。出力をF/Fで切ったミーリ型同様、この制御信号を使う回路はステートマシン入力信号の変化から1クロック遅れて(ステート信号の変化に同期して)動作することになるので、1クロック遅れても良い回路に採用できる。ムーア型の制御信号はステート信号生成時点でタイミングパスが切られているので、(スループットではなく)STA上有利で高速化しやすい。

ミーリ型の特徴

ミーリ型は、ステートマシン入力がステートマシン出力にスルーするパスが存在するもの。ただし、ミーリ型の構成でステートマシン出力をF/Fで切ったものもミーリ型に含める。ムーア型の場合、ステート信号の変化から1クロック後にデータパスを変化させることになるが、ミーリ型はステートマシン入力がステートマシン出力に透過されるので、一つのステート信号に対して複数の状態を持つことができ、ステート信号の変化ど同時にデータパスを変化させることができるので、回路の動作速度(スループット)上有利である。半面、ステートマシン入力のパスが長くなる可能性があり、STA上は不利になりやすい。

ミーリ型の書き方

ステートマシン信号とその他の信号を別のalways文に分離して記述する。

ステートマシン出力をF/Fで切りなおす場合、ステートマシン信号と一緒に生成すれば良いのでalways文を分ける必要性はないが、分けたほうが利点があると思う。
ステートマシン入力以外も組み合わせたい信号がある場合、alwaysを分けないとそれもステートマシン信号と一緒に書かなければならなくなるが、前述のステートマシン出力の定義に照らし合わせるとこの信号はステートマシン出力とは呼べず、ステートマシンとステートマシン以外の信号を一緒に書くことになってしまい、設計スタイル上もよろしくないことになる。

ステートマシン出力は組み合わせ回路で記述しておけば、それを使った信号はF/Fで切り直したときにステートマシン信号と同じタイミングで変化させることができるし、ステートマシン出力を使う先が多岐に及んだとしてもそれぞれのブロックで分けて記述することもできるので、使いまわしやすくなる。ただし、タイミングパスは遅延を多く含むので、パスが長くならないよう気をつけて運用するよう気を付けること。


State Machine

`timescale 1ns/1ns

/////////////////////////////////////////////////////////////////
//
// State Machine
//
/////////////////////////////////////////////////////////////////
module CAR_STATE (
    input logic CLK,
    input logic RESET,
    input logic UP,
    input logic DOWN,
    output logic GEAR,
    output logic SHIFT
    );

typedef enum logic [1:0] {
    IDLE_STATE = 2'b00,
    LOW_STATE  = 2'b01,
    TOP_STATE  = 2'b10
} gear_t;

gear_t current_st, next_st, MSTATE;

logic MGEAR;
logic MSHIFT;

always @(posedge CLK) begin
    if (RESET) begin
        current_st <= IDLE_STATE;
    end
    else begin
        current_st <= next_st;
    end
end

always_comb begin
    case (current_st)
        IDLE_STATE : begin
            MSTATE = IDLE_STATE;
            MGEAR  = 1'b0;
            MSHIFT = 1'b0;
            if (UP) begin
                next_st = LOW_STATE;
                GEAR  = 1'b1;
                SHIFT = 1'b0;
            end else begin
                next_st = IDLE_STATE;
                GEAR  = 1'b0;
                SHIFT = 1'b0;
            end
        end
        LOW_STATE : begin
            MSTATE = LOW_STATE;
            MGEAR  = 1'b1;
            MSHIFT = 1'b0;
            if (UP) begin
                next_st = TOP_STATE;
                GEAR  = 1'b1;
                SHIFT = 1'b1;
            end else if (DOWN) begin
                next_st = IDLE_STATE;
                GEAR  = 1'b0;
                SHIFT = 1'b0;
            end else begin
                next_st = LOW_STATE;
                GEAR  = 1'b1;
                SHIFT = 1'b0;
            end
        end
        TOP_STATE : begin
            MSTATE = TOP_STATE;
            MGEAR  = 1'b1;
            MSHIFT = 1'b1;
            if (DOWN) begin
                next_st = LOW_STATE;
                GEAR  = 1'b1;
                SHIFT = 1'b0;
            end else begin
                next_st = TOP_STATE;
                GEAR  = 1'b1;
                SHIFT = 1'b1;
            end
        end
    endcase
end

endmodule

next_state, GEAR, SHIFTはミーリ型。
MSTATE, MGEAR, MSHIFTはムーア型。

Data Path

/////////////////////////////////////////////////////////////////
//
// CAR object
//
/////////////////////////////////////////////////////////////////
module CAR (
    input logic CLK,
    input logic RESET,
    input logic DRIVE,
    input logic SHIFT,
    output logic [7:0] METER
);

parameter UNIT = 2'b01;

always @(posedge CLK) begin
    if (RESET) begin
        METER <= 8'h00;
    end else if (DRIVE) begin
        if (!SHIFT) begin
            METER <= METER + UNIT;
        end else begin
            METER <= METER + UNIT * 2;
        end
    end else begin
        METER <= METER;
    end
end
endmodule

ミーリ型のステートマシン出力を使ったデータパス。
DRIVEはステートマシン出力と他の信号(START*)から生成した信号。DRIVEはTOP Moduleで定義している。
SHIFTはそのままステートマシン出力を使用。


TOP Module

/////////////////////////////////////////////////////////////////
//
// TOP Module
//
/////////////////////////////////////////////////////////////////
module top (
    input logic CLK,
    input logic RESET,
    input logic START1,
    input logic START2,
    input logic UP,
    input logic DOWN,
    input logic READADR,
    output logic [7:0] READOUT
);

parameter NORMAL = 2'b01;
parameter FAST = 2'b10;

logic gear;
logic SHIFT;
logic [7:0] fcMeter;
logic [7:0] scMeter;
wire drive1 = START1 & gear;
wire drive2 = START2 & gear;

CAR_STATE carState (.*, .GEAR(gear));
CAR #(.UNIT(NORMAL)) familyCar (.*, .DRIVE(drive1), .METER(fcMeter));
CAR #(.UNIT(FAST))   superCar  (.*, .DRIVE(drive2), .METER(scMeter));

always @(posedge CLK) begin
    if (RESET) begin
        READOUT <= 8'd0;
    end else if (READADR == 1'b0) begin
        READOUT <= fcMeter;
    end else if (READADR == 1'b1) begin
        READOUT <= scMeter;
    end
end
endmodule

Test Bench

/////////////////////////////////////////////////////////////////
//
// Test Bench
//
/////////////////////////////////////////////////////////////////
module top_bentch;

parameter P_DELAY  = 10;
parameter P_HCYCLE = 50;
parameter adrFamilyCar = 1'b0;
parameter adrSuperCar = 1'b1;

integer mcd;

logic CLK;
logic RESET;
logic START1;
logic START2;
logic UP;
logic DOWN;
logic READADR;
logic [7:0] READOUT;

always begin
              CLK <= 1'b0;
  #(P_HCYCLE) CLK <= 1'b1;
  #(P_HCYCLE);
end

top car_top (.*);

/////////////////////////////////////////////////////////////////
task CLK_DELAY;
    @(posedge CLK);
//    #(P_HCYCLE * 2);
endtask

task READ;
    input addr;

    string name;

    case (addr)
        1'b0 : name = "family car";
        1'b1 : name = "super car";
        default : name = "undefined";
    endcase

    CLK_DELAY(); READADR = addr;
    CLK_DELAY(); $fdisplay(mcd, "Meter[%s] = %h",name, READOUT);
endtask

/////////////////////////////////////////////////////////////////
task gearUp;
                    UP = 1'b1;
    CLK_DELAY();    UP = 1'b0;
endtask

/////////////////////////////////////////////////////////////////
task gearDown;
                    DOWN = 1'b1;
    CLK_DELAY();    DOWN = 1'b0;
endtask

/////////////////////////////////////////////////////////////////
initial begin
    mcd = $fopen("fsm2.log","w");
    $fmonitor(mcd, "READOUT : %h", READOUT);
    @(posedge CLK); RESET   = 1'b1;
    CLK_DELAY();    RESET   = 1'b0; // release
                    START1  = 1'b0;
                    START2  = 1'b0;
                    UP      = 1'b0;
                    DOWN    = 1'b0;
                    READADR = 1'b0;
    repeat(2) CLK_DELAY();
    CLK_DELAY(); START1 = 1;
    CLK_DELAY(); gearUp();
                    gearUp();
                    gearDown();
                    gearDown();
                    READ(adrFamilyCar);
    CLK_DELAY(); START1 = 0;
    CLK_DELAY(); START2 = 1;
    CLK_DELAY(); gearUp();
                    gearUp();
                    gearDown();
                    gearDown();
                    READ(adrSuperCar);
    CLK_DELAY(); START2 = 0;
    repeat(2) CLK_DELAY();
                    $finish();
    $fclose(mcd);
end
endmodule



ミーリ型のステートマシン出力であるGEAR, SHIFTは、ステートマシン入力のUPと同じタイミングで変化しており、ステートマシン信号current_stと同じタイミングでデータパスが確定している。
ムーア型のステートマシン出力であるMGEAR, MSHIFTは、ステートマシン信号であるMSTATEと同じタイミングで変化しているので、このステートマシン出力を使うデータパスは1クロック遅れて確定する。