Chiselの文法入門の続きで今回は第9回目
前回の終わりに書いたとおり、I/Oポートのパラメタライズについて
とは言いながら、I/O以外の例も示そうと思います。
Chisel入門編〜その9:回路のI/Oポートのパラメタライズ〜
I/Oのパラメタライズ
基本的な考え方はビット幅の調整と大きくは変わりません。
クラスパラメータとして受け取った変数の値を使って、生成するI/Oを変化させる形になります。
この際にScalaのOption
型のひとつであるSome
を使ってパラメタライズ対応したいポートをくるむ必要があることに注意が必要です。
例として書いてみると以下のような形になります。
class MyMod (hasInPort: Boolean, hasOutPort: Boolean) extends Module { val io = IO(new Bundle { val in = Input(UInt(32.W)) val in_opt = if (hasPort) Some(Input(UInt(32.W))) else None val out = Output(UInt(32.W)) val out_opt = if (hasPort) Some(Output(UInt(32.W))) else None }) }
上記の例ではポートの切り替えにif
式を使用していますが、match
式を使用しても構いません。
繰り返しになりますがSome
でくるむのを忘れないように注意してください。
パラメタライズしたI/Oポートの使い方
次に最初の例で見たようなパラメタライズされたI/OポートをChiselの回路記述中で使用する方法についてを見ていきます。
Inputポート
引き続き先ほどの例を使用します。
下記のio.out
にはio.in
とio.in_opt
を足した値を出力するようにした場合、以下のようになります。
class MyMod (hasInPort: Boolean, hasOutPort: Boolean) extends Module { val io = IO(new Bundle { val in = Input(UInt(32.W)) val in_opt = if (hasInPort) Some(Input(UInt(32.W))) else None val out = Output(UInt(32.W)) val out_opt = if (hasOutPort) Some(Output(UInt(32.W))) else None }) io.out := io.in + io.in_opt.getOrElse(0.U) }
- 生成されたRTL(hasInPort == false/hasOutPort == false)
module cmd2HelperMyMod( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_in, // @[:@6.4] output [31:0] io_out // @[:@6.4] ); wire [32:0] _T_10; // @[cmd2.sc 9:19:@8.4] wire [31:0] _T_11; // @[cmd2.sc 9:19:@9.4] // hasInPort == falseなので // io.in_optの代わりに0.Uが加算される assign _T_10 = io_in + 32'h0; // @[cmd2.sc 9:19:@8.4] assign _T_11 = _T_10[31:0]; // @[cmd2.sc 9:19:@9.4] assign io_out = _T_11; endmodule
- 生成されたRTL(hasInPort == true/hasOutPort == false)
module cmd4HelperMyMod( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_in, // @[:@6.4] input [31:0] io_in_opt, // @[:@6.4] output [31:0] io_out // @[:@6.4] ); wire [32:0] _T_11; // @[cmd4.sc 9:19:@8.4] wire [31:0] _T_12; // @[cmd4.sc 9:19:@9.4] // io_in_optが加算される assign _T_11 = io_in + io_in_opt; // @[cmd4.sc 9:19:@8.4] assign _T_12 = _T_11[31:0]; // @[cmd4.sc 9:19:@9.4] assign io_out = _T_12; endmodule
もしくは以下のようにhasInPort
を使ってif
式で記述を切り替える方法でもOKです。
class MyMod (hasInPort: Boolean, hasOutPort: Boolean) extends Module { val io = IO(new Bundle { val in = Input(UInt(32.W)) val in_opt = if (hasInPort) Some(Input(UInt(32.W))) else None val out = Output(UInt(32.W)) val out_opt = if (hasOutPort) Some(Output(UInt(32.W))) else None }) if (hasInPort) { io.out := io.in + io.in_opt.getOrElse(0.U) } else { io.out := io.in } }
- 生成されたRTL(hasInPort == false/hasOutPort == false)
module cmd3HelperMyMod( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_in, // @[:@6.4] output [31:0] io_out // @[:@6.4] ); // こちらはif式で切り替えたのでio_inのみになる assign io_out = io_in; endmodule
- 生成されたRTL(hasInPort == true/hasOutPort == false)
module cmd5HelperMyMod( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_in, // @[:@6.4] input [31:0] io_in_opt, // @[:@6.4] output [31:0] io_out // @[:@6.4] ); wire [32:0] _T_11; // @[cmd5.sc 10:23:@8.4] wire [31:0] _T_12; // @[cmd5.sc 10:23:@9.4] // io_in_optが加算される assign _T_11 = io_in + io_in_opt; // @[cmd5.sc 10:23:@8.4] assign _T_12 = _T_11[31:0]; // @[cmd5.sc 10:23:@9.4] assign io_out = _T_12; endmodule
Outputポート
続いてOutputポートですが、OutputポートはhasOutPort == true
のときにのみ、入力のio.in
をそのまま接続することにします。
その場合には、以下のようにif
式を使って回路が実体化する場合にのみ有効にif
式内部がエラボレートの対象となるようにする必要があります
#この辺はVerilog HDLのgenerate
を使った回路のパラメタライズと同様。
class MyMod (hasInPort: Boolean, hasOutPort: Boolean) extends Module { val io = IO(new Bundle { val in = Input(UInt(32.W)) val in_opt = if (hasInPort) Some(Input(UInt(32.W))) else None val out = Output(UInt(32.W)) val out_opt = if (hasOutPort) Some(Output(UInt(32.W))) else None }) if (hasInPort) { io.out := io.in + io.in_opt.getOrElse(0.U) } else { io.out := io.in } if (hasOutPort) { io.out_opt.get := io.in } }
上記の回路においてhasOutPort
の値をtrue
/false
で変更してそれぞれRTLを生成したものが以下になります。
- 生成されたRTL(hasInPort == true/hasOutPort == false)
// io_out_optポートは存在しない module cmd6HelperMyMod( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_in, // @[:@6.4] input [31:0] io_in_opt, // @[:@6.4] output [31:0] io_out // @[:@6.4] ); wire [32:0] _T_11; // @[cmd6.sc 10:23:@8.4] wire [31:0] _T_12; // @[cmd6.sc 10:23:@9.4] assign _T_11 = io_in + io_in_opt; // @[cmd6.sc 10:23:@8.4] assign _T_12 = _T_11[31:0]; // @[cmd6.sc 10:23:@9.4] assign io_out = _T_12; endmodule
- 生成されたRTL(hasInPort == false/hasOutPort == true)
module cmd7HelperMyMod( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_in, // @[:@6.4] output [31:0] io_out, // @[:@6.4] output [31:0] io_out_opt // @[:@6.4] ); assign io_out = io_in; // io_out_optポートが存在する assign io_out_opt = io_in; endmodule
ここまでの例を振り返ってもらえるとわかるかと思いますが、Chiselのハードウェア要素が実体化する際に、ポートや回路の有無に関しての整合性が取れている必要があります。
またI/Oポートのパラメタライズの際にSome
でポート宣言をくるんでいるのでio.<ポート名>
でアクセスした場合、そのデータはSome
のインスタンスになっているため.get
もしくはgetOrElse
を呼び出してChiselのデータ型を取得する必要があることに注意が必要です。
hasOutPort == true
の例でお気づきとは思いますが、クラスパラメータを使って、if
式やmatch
式を使用することでI/Oポートだけではなく、モジュール内の回路記述もパラメタライズ出来ます。
Bundleを使ったI/Oポートのパラメタライズ
最後にBundle
で束ねたChiselのデータ型を使ってパラメタライズを行う例を紹介しておきます。
これはデバッグポートのように、シミュレーション時には使用したいけど最終的には回路から消したいようなポートを作る際に有用です。
class DebugIO extends Bundle { val wrdataVec = Output(Vec(4, UInt(32.W))) val rddataVec = Output(Vec(4, UInt(32.W))) } class MyMem (debug: Boolean) extends Module { val io = IO(new Bundle { val addr = Input(UInt(32.W)) val wren = Input(Bool()) val wrdata = Input(UInt(32.W)) val rddata = Output(UInt(32.W)) val dbg = if (debug) Some(new DebugIO) else None }) // メモリはUInt(8.W)x4で1wordのメモリ val m = Mem(256, Vec(4, UInt(8.W))) // 32bitを8bitx4のVecに変換 val wrdata = Wire(Vec(4, UInt(8.W))) // 8bit毎にVecに格納 // 2019/06/19 : コードがおかしかったので修正しました // とは言いながらもまだ実装の意図とは異なってます。 // ちょうどいいので第10回でネタにします。 for (i <- 0 until wrdata.length) { // wrdata(i) := io.wrdata(i + 7, i) wrdata(i) := io.wrdata((i * 8) + 7, i * 8) } when (io.wren) { m.write(io.addr, wrdata) } // rddataはMemがVec(4, UInt(8.W))なので // その形で返却される val rddata = m.read(io.addr) io.rddata := RegNext(Cat(rddata)) // debug == trueの時のみ、debugポートに // read/writeデータを接続する if (debug) { val dbg = io.dbg.get dbg.wrdataVec := wrdata dbg.rddataVec := rddata } }
上記をdebug
の値をtrue
/false
で変化させた場合に生成されるRTLは以下の様になります。
debug == true
の場合のI/Oポートとその端子へのassign
に注目して確認してみてください。
#randomizeの記述は削除してあります。
- 生成されたRTL(debug == false)
module cmd10HelperMyMem( // @[:@3.2] // I/Oポートにデバッグポートが存在しない input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_addr, // @[:@6.4] input io_wren, // @[:@6.4] input [31:0] io_wrdata, // @[:@6.4] output [31:0] io_rddata // @[:@6.4] ); reg [7:0] m_0 [0:255]; // @[cmd10.sc 15:14:@8.4] reg [31:0] _RAND_0; wire [7:0] m_0_rddata_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_0_rddata_addr; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_0__T_37_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_0__T_37_addr; // @[cmd10.sc 15:14:@8.4] wire m_0__T_37_mask; // @[cmd10.sc 15:14:@8.4] wire m_0__T_37_en; // @[cmd10.sc 15:14:@8.4] reg [7:0] m_1 [0:255]; // @[cmd10.sc 15:14:@8.4] reg [31:0] _RAND_1; wire [7:0] m_1_rddata_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_1_rddata_addr; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_1__T_37_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_1__T_37_addr; // @[cmd10.sc 15:14:@8.4] wire m_1__T_37_mask; // @[cmd10.sc 15:14:@8.4] wire m_1__T_37_en; // @[cmd10.sc 15:14:@8.4] reg [7:0] m_2 [0:255]; // @[cmd10.sc 15:14:@8.4] reg [31:0] _RAND_2; wire [7:0] m_2_rddata_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_2_rddata_addr; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_2__T_37_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_2__T_37_addr; // @[cmd10.sc 15:14:@8.4] wire m_2__T_37_mask; // @[cmd10.sc 15:14:@8.4] wire m_2__T_37_en; // @[cmd10.sc 15:14:@8.4] reg [7:0] m_3 [0:255]; // @[cmd10.sc 15:14:@8.4] reg [31:0] _RAND_3; wire [7:0] m_3_rddata_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_3_rddata_addr; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_3__T_37_data; // @[cmd10.sc 15:14:@8.4] wire [7:0] m_3__T_37_addr; // @[cmd10.sc 15:14:@8.4] wire m_3__T_37_mask; // @[cmd10.sc 15:14:@8.4] wire m_3__T_37_en; // @[cmd10.sc 15:14:@8.4] reg [31:0] _T_65; // @[cmd10.sc 28:23:@31.4] reg [31:0] _RAND_4; wire [7:0] wrdata_0; // @[cmd10.sc 20:27:@10.4] wire [7:0] wrdata_1; // @[cmd10.sc 20:27:@12.4] wire [7:0] wrdata_2; // @[cmd10.sc 20:27:@14.4] wire [7:0] wrdata_3; // @[cmd10.sc 20:27:@16.4] wire [7:0] _T_36; // @[:@19.6] wire [7:0] _T_49; // @[cmd10.sc 27:22:@26.4] wire [15:0] _T_61; // @[Cat.scala 30:58:@28.4] wire [15:0] _T_62; // @[Cat.scala 30:58:@29.4] wire [31:0] _T_63; // @[Cat.scala 30:58:@30.4] assign m_0_rddata_addr = _T_49; assign m_0_rddata_data = m_0[m_0_rddata_addr]; // @[cmd10.sc 15:14:@8.4] assign m_0__T_37_data = wrdata_0; assign m_0__T_37_addr = _T_36; assign m_0__T_37_mask = io_wren; assign m_0__T_37_en = io_wren; assign m_1_rddata_addr = _T_49; assign m_1_rddata_data = m_1[m_1_rddata_addr]; // @[cmd10.sc 15:14:@8.4] assign m_1__T_37_data = wrdata_1; assign m_1__T_37_addr = _T_36; assign m_1__T_37_mask = io_wren; assign m_1__T_37_en = io_wren; assign m_2_rddata_addr = _T_49; assign m_2_rddata_data = m_2[m_2_rddata_addr]; // @[cmd10.sc 15:14:@8.4] assign m_2__T_37_data = wrdata_2; assign m_2__T_37_addr = _T_36; assign m_2__T_37_mask = io_wren; assign m_2__T_37_en = io_wren; assign m_3_rddata_addr = _T_49; assign m_3_rddata_data = m_3[m_3_rddata_addr]; // @[cmd10.sc 15:14:@8.4] assign m_3__T_37_data = wrdata_3; assign m_3__T_37_addr = _T_36; assign m_3__T_37_mask = io_wren; assign m_3__T_37_en = io_wren; assign wrdata_0 = io_wrdata[7:0]; // @[cmd10.sc 20:27:@10.4] assign wrdata_1 = io_wrdata[8:1]; // @[cmd10.sc 20:27:@12.4] assign wrdata_2 = io_wrdata[9:2]; // @[cmd10.sc 20:27:@14.4] assign wrdata_3 = io_wrdata[10:3]; // @[cmd10.sc 20:27:@16.4] assign _T_36 = io_addr[7:0]; // @[:@19.6] assign _T_49 = io_addr[7:0]; // @[cmd10.sc 27:22:@26.4] assign _T_61 = {m_2_rddata_data,m_3_rddata_data}; // @[Cat.scala 30:58:@28.4] assign _T_62 = {m_0_rddata_data,m_1_rddata_data}; // @[Cat.scala 30:58:@29.4] assign _T_63 = {_T_62,_T_61}; // @[Cat.scala 30:58:@30.4] assign io_rddata = _T_65; always @(posedge clock) begin if(m_0__T_37_en & m_0__T_37_mask) begin m_0[m_0__T_37_addr] <= m_0__T_37_data; // @[cmd10.sc 15:14:@8.4] end if(m_1__T_37_en & m_1__T_37_mask) begin m_1[m_1__T_37_addr] <= m_1__T_37_data; // @[cmd10.sc 15:14:@8.4] end if(m_2__T_37_en & m_2__T_37_mask) begin m_2[m_2__T_37_addr] <= m_2__T_37_data; // @[cmd10.sc 15:14:@8.4] end if(m_3__T_37_en & m_3__T_37_mask) begin m_3[m_3__T_37_addr] <= m_3__T_37_data; // @[cmd10.sc 15:14:@8.4] end _T_65 <= _T_63; end endmodule
- 生成されたRTL(debug == true)
module cmd12HelperMyMem( // @[:@3.2] input clock, // @[:@4.4] input reset, // @[:@5.4] input [31:0] io_addr, // @[:@6.4] input io_wren, // @[:@6.4] input [31:0] io_wrdata, // @[:@6.4] output [31:0] io_rddata, // @[:@6.4] //////// // デバッグポートが生成された //////// output [7:0] io_dbg_wrdataVec_0, // @[:@6.4] output [7:0] io_dbg_wrdataVec_1, // @[:@6.4] output [7:0] io_dbg_wrdataVec_2, // @[:@6.4] output [7:0] io_dbg_wrdataVec_3, // @[:@6.4] output [7:0] io_dbg_rddataVec_0, // @[:@6.4] output [7:0] io_dbg_rddataVec_1, // @[:@6.4] output [7:0] io_dbg_rddataVec_2, // @[:@6.4] output [7:0] io_dbg_rddataVec_3 // @[:@6.4] ); reg [7:0] m_0 [0:255]; // @[cmd12.sc 15:14:@8.4] reg [31:0] _RAND_0; wire [7:0] m_0_rddata_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_0_rddata_addr; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_0__T_73_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_0__T_73_addr; // @[cmd12.sc 15:14:@8.4] wire m_0__T_73_mask; // @[cmd12.sc 15:14:@8.4] wire m_0__T_73_en; // @[cmd12.sc 15:14:@8.4] reg [7:0] m_1 [0:255]; // @[cmd12.sc 15:14:@8.4] reg [31:0] _RAND_1; wire [7:0] m_1_rddata_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_1_rddata_addr; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_1__T_73_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_1__T_73_addr; // @[cmd12.sc 15:14:@8.4] wire m_1__T_73_mask; // @[cmd12.sc 15:14:@8.4] wire m_1__T_73_en; // @[cmd12.sc 15:14:@8.4] reg [7:0] m_2 [0:255]; // @[cmd12.sc 15:14:@8.4] reg [31:0] _RAND_2; wire [7:0] m_2_rddata_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_2_rddata_addr; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_2__T_73_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_2__T_73_addr; // @[cmd12.sc 15:14:@8.4] wire m_2__T_73_mask; // @[cmd12.sc 15:14:@8.4] wire m_2__T_73_en; // @[cmd12.sc 15:14:@8.4] reg [7:0] m_3 [0:255]; // @[cmd12.sc 15:14:@8.4] reg [31:0] _RAND_3; wire [7:0] m_3_rddata_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_3_rddata_addr; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_3__T_73_data; // @[cmd12.sc 15:14:@8.4] wire [7:0] m_3__T_73_addr; // @[cmd12.sc 15:14:@8.4] wire m_3__T_73_mask; // @[cmd12.sc 15:14:@8.4] wire m_3__T_73_en; // @[cmd12.sc 15:14:@8.4] reg [31:0] _T_101; // @[cmd12.sc 28:23:@31.4] reg [31:0] _RAND_4; wire [7:0] wrdata_0; // @[cmd12.sc 20:27:@10.4] wire [7:0] wrdata_1; // @[cmd12.sc 20:27:@12.4] wire [7:0] wrdata_2; // @[cmd12.sc 20:27:@14.4] wire [7:0] wrdata_3; // @[cmd12.sc 20:27:@16.4] wire [7:0] _T_72; // @[:@19.6] wire [7:0] _T_85; // @[cmd12.sc 27:22:@26.4] wire [15:0] _T_97; // @[Cat.scala 30:58:@28.4] wire [15:0] _T_98; // @[Cat.scala 30:58:@29.4] wire [31:0] _T_99; // @[Cat.scala 30:58:@30.4] assign m_0_rddata_addr = _T_85; assign m_0_rddata_data = m_0[m_0_rddata_addr]; // @[cmd12.sc 15:14:@8.4] assign m_0__T_73_data = wrdata_0; assign m_0__T_73_addr = _T_72; assign m_0__T_73_mask = io_wren; assign m_0__T_73_en = io_wren; assign m_1_rddata_addr = _T_85; assign m_1_rddata_data = m_1[m_1_rddata_addr]; // @[cmd12.sc 15:14:@8.4] assign m_1__T_73_data = wrdata_1; assign m_1__T_73_addr = _T_72; assign m_1__T_73_mask = io_wren; assign m_1__T_73_en = io_wren; assign m_2_rddata_addr = _T_85; assign m_2_rddata_data = m_2[m_2_rddata_addr]; // @[cmd12.sc 15:14:@8.4] assign m_2__T_73_data = wrdata_2; assign m_2__T_73_addr = _T_72; assign m_2__T_73_mask = io_wren; assign m_2__T_73_en = io_wren; assign m_3_rddata_addr = _T_85; assign m_3_rddata_data = m_3[m_3_rddata_addr]; // @[cmd12.sc 15:14:@8.4] assign m_3__T_73_data = wrdata_3; assign m_3__T_73_addr = _T_72; assign m_3__T_73_mask = io_wren; assign m_3__T_73_en = io_wren; assign wrdata_0 = io_wrdata[7:0]; // @[cmd12.sc 20:27:@10.4] assign wrdata_1 = io_wrdata[8:1]; // @[cmd12.sc 20:27:@12.4] assign wrdata_2 = io_wrdata[9:2]; // @[cmd12.sc 20:27:@14.4] assign wrdata_3 = io_wrdata[10:3]; // @[cmd12.sc 20:27:@16.4] assign _T_72 = io_addr[7:0]; // @[:@19.6] assign _T_85 = io_addr[7:0]; // @[cmd12.sc 27:22:@26.4] assign _T_97 = {m_2_rddata_data,m_3_rddata_data}; // @[Cat.scala 30:58:@28.4] assign _T_98 = {m_0_rddata_data,m_1_rddata_data}; // @[Cat.scala 30:58:@29.4] assign _T_99 = {_T_98,_T_97}; // @[Cat.scala 30:58:@30.4] assign io_rddata = _T_101; //////// // デバッグポートにデータが接続された //////// assign io_dbg_wrdataVec_0 = wrdata_0; assign io_dbg_wrdataVec_1 = wrdata_1; assign io_dbg_wrdataVec_2 = wrdata_2; assign io_dbg_wrdataVec_3 = wrdata_3; assign io_dbg_rddataVec_0 = m_0_rddata_data; assign io_dbg_rddataVec_1 = m_1_rddata_data; assign io_dbg_rddataVec_2 = m_2_rddata_data; assign io_dbg_rddataVec_3 = m_3_rddata_data; always @(posedge clock) begin if(m_0__T_73_en & m_0__T_73_mask) begin m_0[m_0__T_73_addr] <= m_0__T_73_data; // @[cmd12.sc 15:14:@8.4] end if(m_1__T_73_en & m_1__T_73_mask) begin m_1[m_1__T_73_addr] <= m_1__T_73_data; // @[cmd12.sc 15:14:@8.4] end if(m_2__T_73_en & m_2__T_73_mask) begin m_2[m_2__T_73_addr] <= m_2__T_73_data; // @[cmd12.sc 15:14:@8.4] end if(m_3__T_73_en & m_3__T_73_mask) begin m_3[m_3__T_73_addr] <= m_3__T_73_data; // @[cmd12.sc 15:14:@8.4] end _T_101 <= _T_99; end endmodule
ということで、I/Oポートをパラメタライズする方法でした。
ご覧頂いたとおりBundle
と組み合わせることで、色々柔軟にパラメタライズすることが可能です。
個人的な意見ですがScalaの構文を使って回路記述を切り分けるので、Chiselとしての評価のタイミング時に回路の記述が確定していることを念頭においてパラメタライズ方法を考えるのが良いと思っています。
ここで紹介したのは基本的な例と考え方なので、色々試してみてはいかがでしょうか?
次回は筆者が個人的にChiselの魅力的な機能の一つだと考えているテスト記述について取り上げます。入門編としては次回が最後になる予定です。
ご興味ある方は最後までお付き合いください。