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

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

chisel-templateを使ったオリジナルデザインの作成 - テンプレートへの組み込みからVCD波形ダンプまで

スポンサーリンク

前回のChiselの記事ではchisel-templateをIntellij IDEAのプロジェクトにしてChiselの開発環境を構築する方法についてを紹介した。

www.tech-diningyo.info

今回は前回の終わりに書いたとおり、構築したchisel-templateプロジェクトに自前のモジュールを追加してテストを実行してVCDダンプによる波形デバッグまでの一連の流れをまとめてみようと思う。

Chisel-tempalteプロジェクトを使ったハードウェア開発

もともとのChisel-templateのgithub環境にはGCDのChiselサンプルデザインと、それに対応したテストが実装されている。

なのでこのGCDのコードをコピーして、必要な部分だけど書き換えると簡単にChisel-templateが準備している各種テストの環境をそのまま流用しながら、オリジナルのChiselデザインを開発することが可能だ。

早速もとのGCDのコードを修正して、ごく簡単なオリジナルのChiselデザインをChisel-template環境上で開発してみよう。

実装するモジュール - "MyReg"

これは極力簡単にするためなんの変哲もない8bitのレジスタにすることにした。

あえてverilogで書くとこんなの↓

module MyReg
    (
      input wire       clk
     ,input wire       rst
     ,input wire       wren
     ,input wire [7:0] wrdata
     ,output reg       out
     );

   always @(posedge clk) begin
       if (rst) begin
          out <= {8{1'b0}};
       end
       else if (wren) begin
          out <= wrdata;
       end
       else begin
          out <= out;
       end
   end // always @ (posedge clk)

endmodule // MyReg

単純にwrenが上がったサイクルのみwrdatavalに入るというだけ。スケマにするとこんな感じ。

f:id:diningyo-kpuku-jougeki:20190106160304p:plain

Chiselで書くと??

では上記のverilogに対応するコードをChiselで書いてみよう。

こんな感じ。

class MyReg extends Module {
  val io = IO(new Bundle {
  val in_val = Input(UInt(8.W))
  val in_wren = Input(Bool())
  val out = Output(UInt(8.W))
  })

  val reg = RegInit(0.U(8.W))

  when (io.in_wren) {
    reg := io.in_val
  }

  io.out := reg
}

もうねChiselの旨味も何もあったもんじゃないっていう。

ただ今回はChisel-template上でテストまで含めて実装を行い、一連の流れを把握するのが目的なのでこれでいいのです。

Chisel-templateのフォルダ構成

さて、Chiselで所望のモジュールの実装は出来た。問題は「このファイルをChisel-template環境上のどこに置くのか」ということだ。

そんなわけでここでChisel-templateのフォルダ構成を少し詳細に見ておこう。ダウンロードした直後の構成は以下になる。

.
├── README.md
├── build.sbt
├── project
│   ├── build.properties
│   └── plugins.sbt
├── scalastyle-config.xml
├── scalastyle-test-config.xml
└── src
    ├── main
    │   └── scala
    │       └── gcd
    │           └── GCD.scala
    └── test
        └── scala
            └── gcd
                ├── GCDMain.scala
                └── GCDUnitTest.scala

筆者はScalaやsbtをゴリゴリ使っていたわけではなく、ChiselのためにScalaとsbtを触り始めた状態なのでsbtを使った開発には全くと言っていいくらい馴染みが無い。というわけで、上記の構成がなんとなくsbtの開発環境の基本構成なのかな?くらいの認識である。このへんは後日もう少し突っ込んで調べて記事にする予定。

そうは言っても、まあ大体は推測が出来る感じになっているのがありがたいところで、Chiselに関するデータは全てsrcフォルダに含まれているようだ。

このうち、src/main/scala以下にChiselのハードウェアデザインを実装したファイルが配置され、対応したテストはsrc/test/scala以下に配置される構成になる。

Chiselのハードウェアデザインファイル

ここで、サンプルのsrc/main/scala/gcd/GCD.scalaを見てみよう。とは言ってもこれはなんの変哲も無いChiselのハードウェアデザインが実装されたファイルだ。違いはきちんとパッケージの宣言(package)が行われていることくらい。

// See README.md for license details.

package gcd

import chisel3._

/**
  * Compute GCD using subtraction method.
  * Subtracts the smaller from the larger until register y is zero.
  * value in register x is then the GCD
  */
class GCD extends Module {
  val io = IO(new Bundle {
    val value1        = Input(UInt(16.W))
    val value2        = Input(UInt(16.W))
    val loadingValues = Input(Bool())
    val outputGCD     = Output(UInt(16.W))
    val outputValid   = Output(Bool())
  })

  val x  = Reg(UInt())
  val y  = Reg(UInt())

  when(x > y) { x := x - y }
    .otherwise { y := y - x }

  when(io.loadingValues) {
    x := io.value1
    y := io.value2
  }

  io.outputGCD := x
  io.outputValid := y === 0.U
}

回路の中身は置いといて、ざっくり構造化すると以下の構成になる。

// ライセンスの宣言

package <パッケージ名>

import chisel3._ # ライブラリのインポート

class <モジュール名> extends Module {
  # モジュールの実装
}

Chiselのハードウェアテスト環境

では対応するテストはどうだろう。以下の様に2つのファイルが定義されている。

    └── test
        └── scala
            └── gcd
                ├── GCDMain.scala
                └── GCDUnitTest.scala

それぞれ中身は以下のようになっている。

GCDMain.scala
// See README.md for license details.

package gcd

import chisel3._

/**
  * This provides an alternate way to run tests, by executing then as a main
  * From sbt (Note: the test: prefix is because this main is under the test package hierarchy):
  * {{{
  * test:runMain gcd.GCDMain
  * }}}
  * To see all command line options use:
  * {{{
  * test:runMain gcd.GCDMain --help
  * }}}
  * To run with verilator:
  * {{{
  * test:runMain gcd.GCDMain --backend-name verilator
  * }}}
  * To run with verilator from your terminal shell use:
  * {{{
  * sbt 'test:runMain gcd.GCDMain --backend-name verilator'
  * }}}
  */
object GCDMain extends App {
  iotesters.Driver.execute(args, () => new GCD) {
    c => new GCDUnitTester(c)
  }
}

/**
  * This provides a way to run the firrtl-interpreter REPL (or shell)
  * on the lowered firrtl generated by your circuit. You will be placed
  * in an interactive shell. This can be very helpful as a debugging
  * technique. Type help to see a list of commands.
  *
  * To run from sbt
  * {{{
  * test:runMain gcd.GCDRepl
  * }}}
  * To run from sbt and see the half a zillion options try
  * {{{
  * test:runMain gcd.GCDRepl --help
  * }}}
  */
object GCDRepl extends App {
  iotesters.Driver.executeFirrtlRepl(args, () => new GCD)
}

Appを継承した2つのobjectが2つ定義されており、その中で実際に動作させたいテスターのインスタンスとその実行executeが行わている形式になる。このファイルに定義された

  • GCDMain
  • GCDRepl

が、ScalaのMain関数になりうるものなので、sbtコマンド使って以下の様に実行が可能になる、、ということのようだ。 # この辺は筆者の現状の理解。。。

sbt 'test:runMain gcd.GCDMain'
sbt 'test:runMain gcd.GCDRepl'

GCDMainの方のコメントには何やら色々なオプションが定義されているようだが、この辺もおって調査予定。

とりあえず、ただ単に実行できればいい!!くらいのスタンスなら、以下のような形で自分のモジュール用のメイン関数用のScalaファイルを用意すれば良さそう。

  • test/sacal/<パッケージ名>/<モジュール名.scala>
// See README.md for license details.

package <パッケージ名>

import chisel3._

object <モジュール名>Main extends App {
  iotesters.Driver.execute(args, () => new <モジュール名>) {
    c => new <モジュール名>UnitTester(c)
  }
}

object <モジュール名>Repl extends App {
  iotesters.Driver.executeFirrtlRepl(args, () => new <モジュール名>)
}

まあ、Scala的にパッケージ名やモジュール名の解決が出来る様にすればいいだけだろうから、この限りでは無いけど同じにしといたほうがいろいろ混乱せずには済みそう。

GCDUnitTest.scala

こちらが実際のGCDモジュールに対するユニットテストの定義ファイルになる。

// See README.md for license details.

package gcd

import java.io.File

import chisel3.iotesters
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

class GCDUnitTester(c: GCD) extends PeekPokeTester(c) {
  /**
    * compute the gcd and the number of steps it should take to do it
    *
    * @param a positive integer
    * @param b positive integer
    * @return the GCD of a and b
    */
  def computeGcd(a: Int, b: Int): (Int, Int) = {
    var x = a
    var y = b
    var depth = 1
    while(y > 0 ) {
      if (x > y) {
        x -= y
      }
      else {
        y -= x
      }
      depth += 1
    }
    (x, depth)
  }

  private val gcd = c

  for(i <- 1 to 40 by 3) {
    for (j <- 1 to 40 by 7) {
      poke(gcd.io.value1, i)
      poke(gcd.io.value2, j)
      poke(gcd.io.loadingValues, 1)
      step(1)
      poke(gcd.io.loadingValues, 0)

      val (expected_gcd, steps) = computeGcd(i, j)

      step(steps - 1) // -1 is because we step(1) already to toggle the enable
      expect(gcd.io.outputGCD, expected_gcd)
      expect(gcd.io.outputValid, 1)
    }
  }
}

/**
  * This is a trivial example of how to run this Specification
  * From within sbt use:
  * {{{
  * testOnly gcd.GCDTester
  * }}}
  * From a terminal shell use:
  * {{{
  * sbt 'testOnly gcd.GCDTester'
  * }}}
  */
class GCDTester extends ChiselFlatSpec {
  // Disable this until we fix isCommandAvailable to swallow stderr along with stdout
  private val backendNames = if(false && firrtl.FileUtils.isCommandAvailable(Seq("verilator", "--version"))) {
    Array("firrtl", "verilator")
  }
  else {
    Array("firrtl")
  }
  for ( backendName <- backendNames ) {
    "GCD" should s"calculate proper greatest common denominator (with $backendName)" in {
      Driver(() => new GCD, backendName) {
        c => new GCDUnitTester(c)
      } should be (true)
    }
  }

  "Basic test using Driver.execute" should "be used as an alternative way to run specification" in {
    iotesters.Driver.execute(Array(), () => new GCD) {
      c => new GCDUnitTester(c)
    } should be (true)
  }

  "using --backend-name verilator" should "be an alternative way to run using verilator" in {
    if(backendNames.contains("verilator")) {
      iotesters.Driver.execute(Array("--backend-name", "verilator"), () => new GCD) {
        c => new GCDUnitTester(c)
      } should be(true)
    }
  }

  "running with --is-verbose" should "show more about what's going on in your tester" in {
    iotesters.Driver.execute(Array("--is-verbose"), () => new GCD) {
      c => new GCDUnitTester(c)
    } should be(true)
  }

  /**
    * By default verilator backend produces vcd file, and firrtl and treadle backends do not.
    * Following examples show you how to turn on vcd for firrtl and treadle and how to turn it off for verilator
    */

  "running with --generate-vcd-output on" should "create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"),
      () => new GCD
    ) {

      c => new GCDUnitTester(c)
    } should be(true)

    new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true)
  }

  "running with --generate-vcd-output off" should "not create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "off", "--target-dir", "test_run_dir/make_no_vcd", "--top-name", "make_no_vcd",
      "--backend-name", "verilator"),
      () => new GCD
    ) {

      c => new GCDUnitTester(c)
    } should be(true)

    new File("test_run_dir/make_no_vcd/make_a_vcd.vcd").exists should be (false)

  }

}

このユニットテストファイルは以下の2つのテストクラスで構成されている。

  • GCDUnitTester
  • GCDTester

このうちGCDTesterはテスト本体ではなくテストをどうのように実施するかという各種設定を行うためのクラスになっており、”自分でカスタマイズを行いたい”というようなニーズが出てこない限りに置いては、対象モジュールの書き換え程度の軽微な修正で済む(パッケージで階層化されているので、それすらサボることも可能な気もするが、わかりにくくなりそうなのでやめておいた)。ここで使われている各種ScalaやChiselの文法・モジュールについてもおって調査して記事にしたい。

一方でGCDUnitTesterはサンプルデザインのGCDモジュールのユニットテストを定義したクラスになっているので、これについては各モジュールごとに、設計したデザインの仕様に対してのテストを実装する必要がある。

では改めて、上記2つのテスト用クラスを一般化してみよう。

UnitTester

ユニットテスターは実装するモジュールに合わせて実装する必要があるので、中身はそれに応じて全部変更することになる。

class <モジュール名>UnitTester(c: <モジュール名>) extends PeekPokeTester(c) {
    # ユニットテストの実装
}
Tester

先ほど書いたとおり、こちらは実行時にどのような設定でテストを実行するかのオプションを提供しているクラスなので、サンプルのGCDの機能と等価な機能で良ければ、モジュール名の変更のみでOKとなる。

class <モジュール名>Tester extends ChiselFlatSpec {
  // Disable this until we fix isCommandAvailable to swallow stderr along with stdout
  private val backendNames = if(false && firrtl.FileUtils.isCommandAvailable(Seq("verilator", "--version"))) {
    Array("firrtl", "verilator")
  }
  else {
    Array("firrtl")
  }
  for ( backendName <- backendNames ) {
    "<モジュール名>" should s"calculate proper greatest common denominator (with $backendName)" in {
      Driver(() => new <モジュール名>, backendName) {
        c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c)
      } should be (true)
    }
  }

  "Basic test using Driver.execute" should "be used as an alternative way to run specification" in {
    iotesters.Driver.execute(Array(), () => new <モジュール名>) {
      c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c)
    } should be (true)
  }

  "using --backend-name verilator" should "be an alternative way to run using verilator" in {
    if(backendNames.contains("verilator")) {
      iotesters.Driver.execute(Array("--backend-name", "verilator"), () => new <モジュール名>) {
        c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c)
      } should be(true)
    }
  }

  "running with --is-verbose" should "show more about what's going on in your tester" in {
    iotesters.Driver.execute(Array("--is-verbose"), () => new <モジュール名>) {
      c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c)
    } should be(true)
  }

  /**
    * By default verilator backend produces vcd file, and firrtl and treadle backends do not.
    * Following examples show you how to turn on vcd for firrtl and treadle and how to turn it off for verilator
    */

  "running with --generate-vcd-output on" should "create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"),
      () => new <モジュール名>
    ) {

      c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c)
    } should be(true)

    new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true)
  }

  "running with --generate-vcd-output off" should "not create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "off", "--target-dir", "test_run_dir/make_no_vcd", "--top-name", "make_no_vcd",
      "--backend-name", "verilator"),
      () => new <モジュール名>
    ) {

      c => new <ユニットテストクラス(==<モジュール名>UnitTester)>(c)
    } should be(true)

    new File("test_run_dir/make_no_vcd/make_a_vcd.vcd").exists should be (false)

  }

}

MyRegをchisel-templateに組み込んでデバッグ

長くなったがなんとなく変更すべき部分が見えたので、早速先ほどのMyRegをchisel-tamplate環境に組み込んでみる。

組み込み後のフォルダ構成は以下の様になった。

└── src
    ├── main
    │   └── scala
    │       └── reg
    │           └── MyReg.scala
    └── test
        └── scala
            └── reg
                ├── MyRegMain.scala
                └── MyRegUnitTest.scala

ハードウェアが実装された"MyReg.scala"は既に紹介したとおりのものなので、テスト用のファイルのみをいかに記載する。

MyRegMain.scala

パッケージ名と呼び出すテスタークラスがMyReg用に実装したものになっただけ。

// See README.md for license details.

package reg

import chisel3._

object MyRegMain extends App {
  iotesters.Driver.execute(args, () => new MyReg) {
    c => new MyRegUnitTester(c)
  }
}

object GCDRepl extends App {
  iotesters.Driver.executeFirrtlRepl(args, () => new MyReg)
}

MyRegUnitTest.scala

テストは適当にwren信号をランダムで生成して、その際の値が保持されていることを確認するだけの簡単なテストにしてある。

// See README.md for license details.

package reg

import java.io.File

import scala.math.{random, floor}
import chisel3.iotesters
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

class MyRegUnitTester(c: MyReg) extends PeekPokeTester(c) {


  var exp_val = 0

  /**
    * update the register value
    *
    * @param wren write enable
    * @param wrdata positive integer
    * @return the Register value
    */
  def updateMyReg(wren: Boolean, wrdata: Int): (Int) = {

    if (wren) {
      exp_val = wrdata
    }

    exp_val
  }

  private val r = c

  for(i <- 1 to 40 by 3) {
    val wren =  floor(random * 2).toInt == 1
    val wrdata = floor(random * 10).toInt

    poke(r.io.in_wren, wren)
    poke(r.io.in_val, wrdata)
    step(1)

    val expected_val = updateMyReg(wren, wrdata)

    expect(r.io.out, expected_val)

    println(f"(wren, wrdata, io.out) = ($wren%5s, 0x$wrdata%02x, 0x${peek(r.io.out).toInt}%02x)")
  }
}

/**
  * This is a trivial example of how to run this Specification
  * From within sbt use:
  * {{{
  * testOnly gcd.GCDTester
  * }}}
  * From a terminal shell use:
  * {{{
  * sbt 'testOnly gcd.GCDTester'
  * }}}
  */
class MyRegTester extends ChiselFlatSpec {
  // Disable this until we fix isCommandAvailable to swallow stderr along with stdout
  private val backendNames = if(false && firrtl.FileUtils.isCommandAvailable(Seq("verilator", "--version"))) {
    Array("firrtl", "verilator")
  }
  else {
    Array("firrtl")
  }
  for ( backendName <- backendNames ) {
    "MyReg" should s"calculate proper greatest common denominator (with $backendName)" in {
      Driver(() => new MyReg, backendName) {
        c => new MyRegUnitTester(c)
      } should be (true)
    }
  }

  "Basic test using Driver.execute" should "be used as an alternative way to run specification" in {
    iotesters.Driver.execute(Array(), () => new MyReg) {
      c => new MyRegUnitTester(c)
    } should be (true)
  }

  "using --backend-name verilator" should "be an alternative way to run using verilator" in {
    if(backendNames.contains("verilator")) {
      iotesters.Driver.execute(Array("--backend-name", "verilator"), () => new MyReg) {
        c => new MyRegUnitTester(c)
      } should be(true)
    }
  }

  "running with --is-verbose" should "show more about what's going on in your tester" in {
    iotesters.Driver.execute(Array("--is-verbose"), () => new MyReg) {
      c => new MyRegUnitTester(c)
    } should be(true)
  }

  /**
    * By default verilator backend produces vcd file, and firrtl and treadle backends do not.
    * Following examples show you how to turn on vcd for firrtl and treadle and how to turn it off for verilator
    */

  "running with --generate-vcd-output on" should "create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"),
      () => new MyReg
    ) {

      c => new MyRegUnitTester(c)
    } should be(true)

    new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true)
  }

  "running with --generate-vcd-output off" should "not create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "off", "--target-dir", "test_run_dir/make_no_vcd", "--top-name", "make_no_vcd",
      "--backend-name", "verilator"),
      () => new MyReg
    ) {

      c => new MyRegUnitTester(c)
    } should be(true)

    new File("test_run_dir/make_no_vcd/make_a_vcd.vcd").exists should be (false)

  }

}

実行結果は以下の様になる。wrentrueの時にのみ、io.outの値が更新されているのがわかると思う。

[info] Running reg.MyRegMain 
[info] [0.003] Elaborating design...
[info] [1.179] Done elaborating.
Total FIRRTL Compile Time: 332.0 ms
Total FIRRTL Compile Time: 117.0 ms
file loaded in 0.180685728 seconds, 8 symbols, 3 statements
[info] [0.001] SEED 1546755379651
[info] [0.003] (wren, wrdata, io.out) = ( true, 0x04, 0x04)
[info] [0.003] (wren, wrdata, io.out) = (false, 0x09, 0x04)
[info] [0.003] (wren, wrdata, io.out) = ( true, 0x01, 0x01)
[info] [0.004] (wren, wrdata, io.out) = (false, 0x03, 0x01)
[info] [0.004] (wren, wrdata, io.out) = ( true, 0x05, 0x05)
[info] [0.004] (wren, wrdata, io.out) = (false, 0x00, 0x05)
[info] [0.005] (wren, wrdata, io.out) = ( true, 0x00, 0x00)
[info] [0.005] (wren, wrdata, io.out) = (false, 0x06, 0x00)
[info] [0.005] (wren, wrdata, io.out) = (false, 0x02, 0x00)
[info] [0.005] (wren, wrdata, io.out) = ( true, 0x07, 0x07)
[info] [0.006] (wren, wrdata, io.out) = ( true, 0x06, 0x06)
[info] [0.006] (wren, wrdata, io.out) = ( true, 0x07, 0x07)
[info] [0.006] (wren, wrdata, io.out) = (false, 0x05, 0x07)
[info] [0.007] (wren, wrdata, io.out) = (false, 0x06, 0x07)
test MyReg Success: 14 tests passed in 19 cycles in 0.017862 seconds 1063.73 Hz
[info] [0.006] RAN 14 CYCLES PASSED
[success] Total time: 3 s, completed 2019/01/06 15:16:21

VCDダンプによる波形デバッグ

今回やったようにテスターモジュールをそのまんま流用してテストを作成した際のメリットとしてVCDによる波形ダンプが可能な点が挙げられる。詳しくは追っていないがテスタークラスの以下の部分でVCDダンプに関する引数が追加されているようなのでiotesters.DriverにVCDダンプに関する機能がもともと入っているっぽく見える。

  "running with --generate-vcd-output on" should "create a vcd file from your test" in {
    iotesters.Driver.execute(
      Array("--generate-vcd-output", "on", "--target-dir", "test_run_dir/make_a_vcd", "--top-name", "make_a_vcd"),
      () => new MyReg
    ) {

      c => new MyRegUnitTester(c)
    } should be(true)

    new File("test_run_dir/make_a_vcd/make_a_vcd.vcd").exists should be (true)
  }

使い方も簡単で実行時にオプションで--generate-vcd-output onを追加するだけである。今回のMyRegの場合だと以下の様になる。

sbt 'test:runMain reg.MyRegMain --generate-vcd-output on'

上記で実行すると、テスト結果が出力されるフォルダに"<モジュール名>.vcd"というダンプファイルが生成される。これは拡張子の示す通りVCD形式の波形データファイルになっているので、波形ビューワーで開くことが可能だ。筆者はGTKWaveを使用した。Windowsの場合はビルド済みのバイナリデータが公開されているのでそれを使うと楽。

f:id:diningyo-kpuku-jougeki:20190106160340p:plain
MyRegのテスト実行時の波形

io_in_wrenに応じてregの値が更新されているのがわかるかと思う。

これでIntelliJ IDEA上に構築したchisel-template環境上で波形デバッグまでを含んだ開発が行えることが確認できた。波形が見えるようになるだけで随分デバッグがやりやすくなるな、、やっぱり。

今回実施した各種変更はchisel-templateをforkした自分のgithubリポジトリに上げてあるので、興味があればそちらも合わせてどうぞ。 #これforkしないでリポジトリ新規作成したほうが良かったな。。

github.com

まだまだ色々機能がありそうなのでChisel-bootcampが落ち着いたタイミングにでももう少しこちらのテンプレートについても深堀してみたいと思う。