Diplomacyとは、モジュール間のパラメータを調停するためのフレームワーク。Chisel自体の機能ではなく、Chisel+Scalaを使って実装されている。「Diplomacy」という名前が示す通り、モジュール間でパラメータを「外交」して、調停することができるフレームワークとなっている。
このDiplomacyは、Chiselを使ったことのある人なら名前は聞いたことがあると思う。Diplomacyを使うためには、TileLinkが必要だと思われがちだが違う。実際にはTileLinkの実装はDiplomacyの上に構成されており、TileLinkを使わずともDiplomacyを使うことは可能だ。
また、DiplomacyはRocket-Chipの一部として実装してありRocket-Chipのリポジトリをダウンロードする必要があると思われがちだが、これも違う。Rocket-Chipのソースコード自体をダウンロードすることなく、MavenのリポジトリからRocket-Chipのライブラリをダウンロードして、Scalaのソースコード上でライブラリをインポートすることで使用できるようになる。
例として2ポートを持つ加算器をDiplomacyで実現する。前述したようにここではTileLinkを使用せず、Diplomacyの機能のみを使用する。また後で説明するように、2ポートの加算器を複数ポートにDiplomacyの力を使って拡張したり、パラメータを使って簡単にバス幅を変更することができるようになるのも見る。
Diplomacyを構成するためには、「何を調停するのか」を決める必要がある。私たちが取り上げる加算器の例であれば「加算器のビット幅」ということになるだろう。この例では2つのオペランドに対して乱数発生器を接続し、加算の結果はモニターを使って監視することにする。今回の例で、登場人物は以下のようになるだろう。
AdderDriver
: 乱数を発生して加算器に送り込むためのモジュール。また、加算器だけでなくモニターにも同じ値を送り込んで、加算器の結果を検証するのにも使用する。Adder
: 加算器そのもの。AdderDriver
から受け取った乱数を加算して結果をモニターに渡す。AdderMonitor
:モニター。AdderDriver
からの乱数を受け取るのと、Adder
の結果を受け取って答え合わせを行う役割を担っている。
これらのモジュール間で、どのようなパラメータを受け渡せばよいだろうか?すぐに思いつくのはバス幅だろう。AdderDriver
からAdder
に繋げるバス幅が決まれば、それに応じて加算器のビット幅も決まってくる。
Diplomacyでは、接続するモジュールに対して「Node: ノード」というものを定義する。またノード間を接続するための「Edge: エッジ」も定義される。ノードとノードをエッジで接続するわけだが、これには方向が定められており、送信元のノードを「Source: ソース」と呼び、受信側のノードを「Sink: シンク」と呼んでいる。
Diplomacyは、このノード間でパラメータを渡していくことにより調停を行っている。
まず、どんなパラメータを転送するか、ということだが、Scalaのcase class
を使って以下のようなクラスに情報を集約する。
case class UpwardParam(width: Int) case class DownwardParam(width: Int) case class EdgeParam(width: Int)
3種類のクラスを定義していることが分かる。それぞれ、メンバ変数としてwidth
を持っており、これが受け渡しを行うパラメータとなる。それぞれのクラスの意味は以下のようになる。
UpwardParam
:SinkからSourceへ向かうパラメータDownwardParam
:SourceからSinkへ向かうパラメータEdgeParam
:Edgeが保持すべきパラメータ
という訳でこれらの情報を渡さなければならない。まずはノードから作り込んでいくことにしよう。ノードはSimpleNodeImp
を継承して作りあげる。
AdderNodeImp
というクラスは、SimpleNodeImp
を継承しており、4つのパラメータを持っている。DownwardParam
、UpwardParam
、EdgeParam
, UInt
については、後で説明する。
3つのメソッドを定義しているが、それぞれについて説明する。これらは必ず実装しなければならないものだ。
edge(pd: DownwardParam, pu: UpwardParam, p: Parameters, sourceInfo: SourceInfo)
:Edge
が持つべきパラメータを決定する。今回の場合には、Upward
とDownward
のパラメータを比較して、width
の小さな方を採用している。
// PARAMETER TYPES: D U E B object AdderNodeImp extends SimpleNodeImp[DownwardParam, UpwardParam, EdgeParam, UInt] { def edge(pd: DownwardParam, pu: UpwardParam, p: Parameters, sourceInfo: SourceInfo) = { if (pd.width < pu.width) EdgeParam(pd.width) else EdgeParam(pu.width) } def bundle(e: EdgeParam) = UInt(e.width.W) def render(e: EdgeParam) = RenderedEdge("blue", s"width = ${e.width}") }
AdderDriverNodeの実装
AdderDriverNode
はAdderDriver
のノードの役割を持っており、いわゆるマスターの役割(SourceNode)として動作する。このためSourceNode
を継承して定義される。
/** node for [[AdderDriver]] (source) */ class AdderDriverNode(widths: Seq[DownwardParam])(implicit valName: ValName) extends SourceNode(AdderNodeImp)(widths)
AdderDriverNode
はマスターノードであるので、DownwardParam
を引数として取っており、接続されているノードに対してこのパラメータ情報を共有することになる(AdderDriverNode
は複数の出力に接続されるためSeq[DownwardParam]
として表現される。
AdderDriver
の実装は以下のようになる。DiplomacyのノードとしてAdderDriverNode
を使っている。
class AdderDriver(width: Int, numOutputs: Int)(implicit p: Parameters) extends LazyModule { val node = new AdderDriverNode(Seq.fill(numOutputs)(DownwardParam(width))) lazy val module = new LazyModuleImp(this) { ...
このノードに対してランダムパタン生成器を接続してパタンを発生させる。
val node = new AdderDriverNode(Seq.fill(numOutputs)(DownwardParam(width))) lazy val module = new LazyModuleImp(this) { // check that node parameters converge after negotiation val negotiatedWidths = node.edges.out.map(_.width) require(negotiatedWidths.forall(_ == negotiatedWidths.head), "outputs must all have agreed on same width") val finalWidth = negotiatedWidths.head // generate random addend (notice the use of the negotiated width) val randomAddend = FibonacciLFSR.maxPeriod(finalWidth) // drive signals node.out.foreach { case (addend, _) => addend := randomAddend } }
AdderMonitorNodeの実装
AdderMonitorNode
はAdderMonitor
のノードの役割を持っており、いわゆるスレーブの役割(SinkNode)として動作する。このためSinkNode
を継承して定義される。
/** node for [[AdderMonitor]] (sink) */ class AdderMonitorNode(width: UpwardParam)(implicit valName: ValName) extends SinkNode(AdderNodeImp)(Seq(width))
AdderMonitor
モジュールは複数のノードを持っている。具体的にはAdderDriver
からの複数のノードと、Adder
からのノードを受け取るようになっている。
class AdderMonitor(width: Int, numOperands: Int)(implicit p: Parameters) extends LazyModule { // nodeSeqはAdderMonitorNodeを複数個配置する val nodeSeq = Seq.fill(numOperands) { new AdderMonitorNode(UpwardParam(width)) } // AdderMonitorNodeを1つ配置する val nodeSum = new AdderMonitorNode(UpwardParam(width)) lazy val module = new LazyModuleImp(this) { val io = IO(new Bundle { val error = Output(Bool()) }) // print operation printf(nodeSeq.map(node => p"${node.in.head._1}").reduce(_ + p" + " + _) + p" = ${nodeSum.in.head._1}\n") // basic correctness checking io.error := nodeSum.in.head._1 =/= nodeSeq.map(_.in.head._1).reduce(_ + _) }
AdderNodeの実装
AdderNode
はAdder
のためのノードで、複数のSinkNodeと複数のSourceNodeが接続される。
/** node for [[Adder]] (nexus) */ class AdderNode(dFn: Seq[DownwardParam] => DownwardParam, uFn: Seq[UpwardParam] => UpwardParam)(implicit valName: ValName) extends NexusNode(AdderNodeImp)(dFn, uFn)
このAdder
はAdderNode
を持っており、DownwardParam
、UpwardParam
でそれぞれのパラメータを比較している。
/** adder DUT (nexus) */ class Adder(implicit p: Parameters) extends LazyModule { val node = new AdderNode ( { case dps: Seq[DownwardParam] => require(dps.forall(dp => dp.width == dps.head.width), "inward, downward adder widths must be equivalent") dps.head }, { case ups: Seq[UpwardParam] => require(ups.forall(up => up.width == ups.head.width), "outward, upward adder widths must be equivalent") ups.head } ) ...