プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

プログラマのためのVerilog入門

 ここのところしばらくVerilogをやっていました。というわけで、まとめていこうと思います。

 といっても、基本的な文法の話ではなく、プログラミングをこれまで基本としてやってた人がハードウェア記述言語を触るときに気をつけなくてはならないことを中心に記載していこうと思います。(というか、記事書けるほど文法詳しくない。)


記事の内容:



メインは最後の状態変数の話です。では興味のある方はどうぞ。



論理合成可能なVerilogとテストベンチのVerilogは違う

 C++開発でテストするなら、Assert文やCppUnitTestとかあたりを使いますかね。JavaならJUnitとか使いますよね。C++で開発したものをC++でテストする。Javaで開発したものをJavaでテストするということは特に違和感ないかとおもいます。

 Verilogでも同様で、Verilogで開発した(論理合成可能)ものをVerilogでテストします。

 我々ソフト畑の人間からすれば、C++,Javaで開発したものとテストコードにおいて、その文法的な書き方に区別はつけてないとおもいます。
(テストコードは開発コードに比べて雑とか、利用してるテストライブラリに併せて若干変則的な書き方をするなどの変化はあるでしょうが。)

 が、Verilogの場合、「論理合成可能」、つまり回路に変換可能という縛りが開発コードにはある一方、テストベンチ用の検証コードはその縛りがないので、文法レベルの書き方が増加します。まずはそのことを理解しておきましょう。


regとwireとlogicと

 Verilogが糞だなと思う理由の一つが「reg」「wire」「logic」という型の存在。方々でも言われておりますが、regとwireという型は単にシミュレータの実装上のもので、物理的な意味(配線型、メモリ型など)はありません

 たぶんですが、最初に論理回路シミュレータを作った人が、そういう二つの構造体を定義してデータ管理する実装をしたんでしょう。こんなイメージ↓の実装だったのではないかと。

typedef struct{
  char*  value;
  size_t size;
}wire;

typedef struct{
  char* current_value;
  char* next_value;
  size_t size;
}reg;

wire* wire_list;
reg*  reg_list;

void update()
{
  //always等のプログラム的な処理の実行
  calcReg();
  //assignの一発で決まる処理の実行
  calcWire();
  // regのnext_valueをcurrent_valueに代入
  updateReg();
}




 で、作ったシミュレータを動かすための文法作るときに、二つの構造体のどっちに割り振るべきか判断する処理を考えるのが面倒くさかったのでしょう。もしくは単に、C言語では型を書かないといけないので、何も考えずに癖でそれを持ってきただけかもしれません。あぁ、なんかこう考えると、親近感がすごくわいてきませんか?私はとても親近感がわきました。

 ということで、我々プログラム畑の人間からすれば、この背景を理解さえしていればregとwireに関しては、さほど躓いたり不思議に思うことはないと思います。単にシミュレーターというライブラリが宣言している構造体を宣言しているだけですから。



 で、文法上も分けてる理由がないとようやくSystemVerilogでregとwireがlogicという型でまとめられたわけです。





 これは単なる文句なんですが、もうこのlogicを作ろうという判断がまた糞ナンセンスですよね。logicとかいう型を作った人は「あくまでVerilogはシミュレータ用言語である」としか考えてないんじゃないでしょうか。だったらまだregとwireの方が愛嬌があります。

 作るべきだった型は「logic」ではなく、フリップフロップ型、ラッチ型、組み合わせ回路型などの物理型と検証型(今のlogic型)に分けるべきだと思います。なんか申し訳程度に「always_ff」と「always_comb」がありますが、なんで型じゃなくて記述方法で制限しようとすっかね?


 むろん、論理合成時の最適化や、エンジニアのコードの書き方が悪くて実際にフリップフロップに変換することができないとか、回路上にラッチができる可能性があるから型で制限しにくい!という意見もあるかもしれない。けど、それこそエンジニアの意向と異なるわけだからWarning、Error事項だよね?もし、最適化の結果それらが許される、とエンジニアが判断するなら、それ用の型に変更する(もしくはアノテーション的なものの付加で良いかもしれない)べきです。






状態遷移の考え方を変数中心にする

 特に最近はclojureやらなんやらイミュータブルな実装が流行で、私に限らず、「変」数ではなく、引数など「定」数でプログラムすることが多くなっていると思います。むろん、変数の値で状態遷移するプログラミングを全くしないわけじゃないですけど。

 しかし、Verilogを書く際には全く逆で、変数を中心に状態遷移するようにしないと、綺麗な実装ができません。しかもその状態遷移は、普段我々が全く意識していないレベルの状態遷移でも変数中心で状態遷移させます。一度理解すればなんてことはないんですが、私はこの発想が最初なかったんですよね。

 

 「処理A,Bをしたあと、処理Cをし、前の結果が真なら処理Dを、偽なら処理Eをする。」という内容を考えましょう。

 状態遷移はこうなっていますよね。


f:id:nodamushi:20160625022550p:plain



 まぁ、ここまでは特に問題ありません。言われりゃその通りだ。

 さて、ここで問題になるのは、AからB、BからCへのε遷移は実際に誰が遷移させるのかと言うことです。

 たとえば我々プログラマはこう書きますよね。

void main()
{
  A();
  B();
  if(C())
    D();
  else
    E();
}


 このとき、A(),B(),if(C())への遷移という概念はおそらく考えていないと思います。なぜなら、プログラムは上から下に実行されるものだからです。一方CからD,Eは、遷移を意識していると思います。(それが状態遷移と思っていなくても、分岐という形で意識していると思います。)

 この上から下に実行されるという時間の概念がε遷移の正体になります。


 しかし、Verilog(論理回路)ではこの上から下へと言う時間の概念を使うことはできないのです。

 むろん、回路Aの信号を回路Bに伝え、回路Bの信号を回路Cに伝え………っていうような逐次処理ができるなら、単に組み合わせ回路で表現できます。ですが、この間メモリ(フリップフロップ)の更新はできません。だって組み合わせ回路だから。

 途中、メモリの更新をどうしても挟まなくてはならない場合、どうしてもクロックの単位で処理する回路を区切ることになります。あぁ、残念ながらすでにこの時点で上から下へという時間の概念がぶった切られました。なぜならそれぞれの区切られた回路は並列に動くからです。

 従って、どの回路が有効なのかを決定する状態変数を管理する必要があります。擬似コードとしてはこんな感じでしょう。

int state;
void main()
{
  while(state!=5)//5で終了とする
  {
    // A,B,C,D,Eの関数それぞれが実際には一つ一つの回路になる
    // stateの値で有効な回路を選択する
    // C言語ライクに書くとA~Eのどれか一つしか起動しないが、実際には全部同時に並列実行されます。
    switch(state)
    {
      case 0:   A();   break;
      case 1:   B();   break;
      case 2:   C();   break;
      case 3:   D();   break;
      case 4:   E();   break;
    }
  }
}

 ん~、だいぶ普通のプログラミングから離れてきましたが、まだこれくらいは書くことあります。ユーザーからの入力による状態遷移が複雑すぎる場合はこうやって管理すると見通しがよくなったりします。昔作ったTeXの処理系の中心部分はこんな設計でした。

 ところで、stateはだれが変更するんでしょうかね?

 いや、そりゃま、こうでしょ?と私が最初に普通に考えたものは以下のような内容でした。

int state;
//実際には各回路(関数)はそれぞれの処理をしますが、
//めんどうなので今はstate以外は省略しています。
void A(){state = 1;}
void B(){state = 2;}
void C(){state = 何らかの条件 ? 3 : 4;}
void D(){state = 5;}
void E(){state = 5;}

 はい、残念ながら、これを回路としては作れないんですよね。一つのフリップフロップが5つの回路から入力されるわけですから。例えるなら一つのUSB端子に複数のUSBデバイスを挿すことはできないことと同じです。

 複数のUSBデバイスを挿せないならハブを挟めば良いじゃない。というわけで、私が次に考えたのがこうです。

int state;
int state_a,state_b,state_c,state_d,state_e;
void state_update() //毎クロック勝手に動く
{
  state = state == 0? state_a://状態A
          state == 1? state_b://状態B
          state == 2? state_c://状態C
          state == 3? state_d://状態D
                      state_e;//状態E
}
void A(){state_a = 1;}
void B(){state_b = 2;}
void C(){state_c = 何らかの条件 ? 3 : 4;}
void D(){state_d = 5;}
void E(){state_e = 5;}

 あくまで間違いを選択する程度の能力。もちろん、これ動くんすよ。各変数を更新する回路(関数)は一つしかないので問題ありません。が、決してスマートではありませんし、回路規模もでかくなります。

 さて、なぜ私があくまで間違い続けるのか?それは、生まれながらの馬鹿で死ぬべきだからというもっともな答えには目をつむるとして、各関数間の遷移、特にCからD,Eへの遷移の責任が関数Cの戻り値にあるという発想から抜けられなかったからです。あと、こうしておくと、特定の関数からの状態遷移を後から変えるのが楽ですよね?


 ここで一気に、状態遷移に関して各関数が主体の構成から、状態変数が主体の構成に切り替えるパラダイムシフトが必要になります。つまりこうです。

int state;
int c_value;
void state_update() //毎クロック勝手に動く
{
  state += state==3||(state == 2 & c_value) ? 2: 1;// CからEに移動する場合とDでは2を加算
}
void A(){}
void B(){}
//* c_valueはCからD,Eどちらに移動するのかの条件となる値。
//  組み合わせ回路で出力します
void C(){c_value = 何らかの条件 ? 0: 1;}
void D(){}
void E(){}

 このように基本的には各関数状態変数の更新には何も関与せず、state_updateが常に全ての状態遷移の管理をします。

 まれにCの様に遷移条件を組み合わせ回路ではき出す奴もいますが、あくまで遷移条件だけです。遷移状態ははき出したりしません。


 このようにstate_updateが全て受け持つことで、回路が単純化されます。この例ではどの信号(変数)を選択するかを決めてたセレクタがなくなり、単純な加算機一つになりました。state ==3 || (state ==2 & cvalue)?の部分がセレクタに見えますが、その後ろにあるのは定数なので、ただの論理式です。

 まぁ、一方で、状態遷移の変更要求にたいする柔軟性は若干失われますが。(仕様変更で一つの状態遷移を追加するために、下手すると全部変更する羽目になったりする。たとえば上の例でAとBの間にXを挟んでくれという仕様が来たら………?Xを1とし、B以降の状態値を1増やすのか、それともA:0→X:5→B:1という遷移条件を増やすのか………)


 状態遷移………なかなか味わい深いですね。こんな本を買って今積んでいるところです。みなさんも興味があったら是非。

組込みエンジニアのための状態遷移設計手法―現場で使える状態遷移図・状態遷移表の記述テクニック― (MBD Lab Series)

組込みエンジニアのための状態遷移設計手法―現場で使える状態遷移図・状態遷移表の記述テクニック― (MBD Lab Series)