ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Chisel Bootcamp - Module2.4 (1) - レジスタ

スポンサーリンク

前回の記事でChisel BootcampのModule2.3の学習が全て終了した。

www.tech-diningyo.info

今回は更に学習を進めて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の最初の項目ハードウェアを実装する際に欠かせないレジスタについての勉強がひとまず終了した。次の記事ではレジスタと条件分岐を組み合わせてフローの制御についてを見ていく。