前回の記事でChisel BootcampのModule2.3の学習が全て終了した。
今回は更に学習を進めてModule2.4に入っていく。
いよいよ「順序回路」だ!
Module 2.4: 順序回路
モチベーション
これまで同様にモチベーションから引用してくる。
ステートなしにはどんな意味のある回路も書けない。ステートなしにはどんな意味のある回路も書けない。。。ステートなしにはどんな意味のある回路も書けない。。。。
だろ??なぜなら中間値を保存することなしにはどこへも行けないからだ。
よし、悪い冗談はここまでにしよう。このモジュールではChiselで順序だったパターンを表現するかについて見てもらう。このモジュールが終わる頃には、Chiselでシフトレジスタが書けるようになっているべきだ。
大切なことを強調すると、このセクションではあなたに劇的な効果を感じてもらうものでは無いだろう。Chiselのちからは順序回路のパターンにあるわけではなく、むしろデザインをパラメタライズすることにある。その可能性について示す前に、私達はまず、これらの順序回路のパターンについて学ばなければならない。だから、このセクションではChiselで順序回路を書くことがVerilogに似ていることを示してみよう。そう、ただ単にChiselの文法だけを学べばいいのだ。
レジスタ
Chiselのステートフルな要素はレジスタでReg
と表記される。基本的にはVerilogとほぼ同様と考えて良いが、以下の違いが存在する。
Chiselにおけるレジスタは以下の特徴を持つ。
- クロックの立ち上がりエッジが来るまで出力を保持する
- どのレジスタもデフォルトでは、全て同一のクロックが使用される
これによって、Chiselで設計する際にはクロックを明示する必要がなくなりコード量を減らすことが出来る。
例題:レジスタの使用方法
ここからは早速、レジスタの使い方について見ていく。
以下の例は入力に対して1.U
を足した値をレジスタに設定する。
class RegisterModule extends Module { val io = IO(new Bundle { val in = Input(UInt(12.W)) val out = Output(UInt(12.W)) }) val register = Reg(UInt(12.W)) register := io.in + 1.U io.out := register } class RegisterModuleTester(c: RegisterModule) extends PeekPokeTester(c) { for (i <- 0 until 100) { poke(c.io.in, i) step(1) expect(c.io.out, i+1) } } assert(chisel3.iotesters.Driver(() => new RegisterModule) { c => new RegisterModuleTester(c) }) println("SUCCESS!!") println(getVerilog(new RegisterModule))
早速動かしてみよう。
[info] [0.000] Elaborating design... [info] [0.087] Done elaborating. Total FIRRTL Compile Time: 14.6 ms Total FIRRTL Compile Time: 11.7 ms End of dependency graph Circuit state created [info] [0.000] SEED 1539525444137 test cmd3HelperRegisterModule Success: 100 tests passed in 105 cycles taking 0.022228 seconds [info] [0.022] RAN 100 CYCLES PASSED SUCCESS!! [info] [0.000] Elaborating design... [info] [0.003] Done elaborating. Total FIRRTL Compile Time: 79.6 ms module cmd3HelperRegisterModule( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [11:0] io_in, // @[:@6.4] output [11:0] io_out // @[:@6.4] ); reg [11:0] register; // @[cmd3.sc 7:21:@8.4] reg [31:0] _RAND_0; wire [12:0] _T_11; // @[cmd3.sc 8:21:@9.4] wire [11:0] _T_12; // @[cmd3.sc 8:21:@10.4] assign _T_11 = io_in + 12'h1; // @[cmd3.sc 8:21:@9.4] assign _T_12 = _T_11[11:0]; // @[cmd3.sc 8:21:@10.4] assign io_out = register; `ifdef RANDOMIZE_GARBAGE_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_INVALID_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_REG_INIT `define RANDOMIZE `endif `ifdef RANDOMIZE_MEM_INIT `define RANDOMIZE `endif `ifdef RANDOMIZE integer initvar; initial begin `ifndef verilator #0.002 begin end `endif `ifdef RANDOMIZE_REG_INIT _RAND_0 = {1{$random}}; register = _RAND_0[11:0]; `endif // RANDOMIZE_REG_INIT end `endif // RANDOMIZE always @(posedge clock) begin register <= _T_12; end endmodule
生成されたVerilogコードを見ると、ifdefがやたらいっぱい入って入るが、endmodlue
の直前にVeirlogのレジスタ推定文があるのがわかるかと思う。
ここでレジスタの宣言についてまとめておこう。
基本形は
Reg(tpe)
となる。
ここでtpe
には、実装したいレジスタのエンコーディング設定を設定する。要はビット幅と変数の型で、上記の例では12-bitのUInt
になっている。
テストに際には、Chiselのテストコードに対して、クロックが入力されて1サイクル時間が経過したことを知らせる必要があるが、それ行うにはstep(n)
を呼ぶことで可能だ。(nは所望のサイクル)
前回までのテストではstep
が呼ばれていなかったが、前回までの回路は全て組み合わせ回路であり、サイクルの経過をテストに考慮する必要がなかったからだ。
改めてまとめると、
poke()
: 入力データを即時に対象モジュールに反映する。 Verilogの組み合わせ回路の記述やfunction文相当step()
:Chiselのテストに時間を経過させる。 Verilogの順序回路記述や、時間経過ありのtask文相当
ということになる。
ChiselのReg
から生成されるVerilogの仕様は以下のようになる。
- レジスタは暗黙のクロック(とリセット)をもっており、ユーザーはこのクロックを気にする必要は無い。
- マルチクロックのモジュールや、リセットの仕様を変更したい場合は、オーバーライド可能
- 変数の
register
は期待通りにreg[11:0]
として定義される - たくさんあるifdefセクションはシミュレーションスタート時にレジスタの初期値をランダムにするためのものである
register
はクロックの立ち上がりエッジに同期する
宣言時の注意点
ひとつReg
の宣言時に注意することがある。Chiselは型(UInt
とか)とハードウェアのノード(2.U
とかReg
とか)を別のものとして認識する。
そのため、
val myReg = Reg(UInt(2.W))
のような記述は正常に動作するが、
val myReg = Reg(2.U)
はエラーとなる。理由は先の型とノードをが別のものとして扱われているからで、2.U
はノードとなるため、引数として設定が出来ないからだ。
例題:RegNext
次は先程のReg
とは別に用意されているRegNext
についての例を見てみよう。
class RegNextModule extends Module { val io = IO(new Bundle { val in = Input(UInt(12.W)) val out = Output(UInt(12.W)) }) // register bitwidth is inferred from io.out io.out := RegNext(io.in + 1.U) } class RegNextModuleTester(c: RegNextModule) extends PeekPokeTester(c) { for (i <- 0 until 100) { poke(c.io.in, i) step(1) expect(c.io.out, i+1) } } assert(chisel3.iotesters.Driver(() => new RegNextModule) { c => new RegNextModuleTester(c) }) println("SUCCESS!!")
実行すると以下のようになる。
[info] [0.000] Elaborating design... [info] [0.072] Done elaborating. Total FIRRTL Compile Time: 25.3 ms Total FIRRTL Compile Time: 10.6 ms End of dependency graph Circuit state created [info] [0.000] SEED 1540219815309 test cmd4HelperRegNextModule Success: 100 tests passed in 105 cycles taking 0.019785 seconds [info] [0.019] RAN 100 CYCLES PASSED SUCCESS!!
内容としては先程のReg
と変わらないのだが、宣言の際にレジスタのビット幅の指定が不要になって直接レジスタにデータを設定できている部分が異なる。
例題:RegInit
こちらはなんとなく、名前からも推測できるが初期化機能付きのレジスタになる。
これも例をさくっと見てみよう。
class RegInitModule extends Module { val io = IO(new Bundle { val in = Input(UInt(12.W)) val out = Output(UInt(12.W)) }) val register = RegInit(0.U(12.W)) register := io.in + 1.U io.out := register } println(getVerilog(new RegInitModule))
実行結果は以下のようになる。
[info] [0.000] Elaborating design... [info] [0.010] Done elaborating. Total FIRRTL Compile Time: 20.7 ms module cmd5HelperRegInitModule( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [11:0] io_in, // @[:@6.4] output [11:0] io_out // @[:@6.4] ); reg [11:0] register; // @[cmd5.sc 7:25:@8.4] reg [31:0] _RAND_0; wire [12:0] _T_12; // @[cmd5.sc 8:21:@9.4] wire [11:0] _T_13; // @[cmd5.sc 8:21:@10.4] assign _T_12 = io_in + 12'h1; // @[cmd5.sc 8:21:@9.4] assign _T_13 = _T_12[11:0]; // @[cmd5.sc 8:21:@10.4] assign io_out = register; `ifdef RANDOMIZE_GARBAGE_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_INVALID_ASSIGN `define RANDOMIZE `endif `ifdef RANDOMIZE_REG_INIT `define RANDOMIZE `endif `ifdef RANDOMIZE_MEM_INIT `define RANDOMIZE `endif `ifdef RANDOMIZE integer initvar; initial begin `ifndef verilator #0.002 begin end `endif `ifdef RANDOMIZE_REG_INIT _RAND_0 = {1{$random}}; register = _RAND_0[11:0]; `endif // RANDOMIZE_REG_INIT end `endif // RANDOMIZE always @(posedge clock) begin if (reset) begin register <= 12'h0; end else begin register <= _T_13; end end endmodule
レジスタの本体だけを取り出すと以下になる。
always @(posedge clock) begin if (reset) begin register <= 12'h0; end else begin register <= _T_13; end end
上記のコードを見てわかる通り、リセットの際に12'h0
で初期化されている。
先の例では、
val register = RegInit(0.U(12.W))
という初期化の方法が使われていたが、もうひとつ別の方法があって以下のように書くことも可能だ。
val myReg = RegInit(UInt(12.W), 0.U)
これでModule2.4の最初の項目ハードウェアを実装する際に欠かせないレジスタについての勉強がひとまず終了した。次の記事ではレジスタと条件分岐を組み合わせてフローの制御についてを見ていく。