Home Chisel笔记(一):Diplomacy
Post
Cancel

Chisel笔记(一):Diplomacy

Diplomacy是一个参数协商框架,用于生成参数化的协议实现。在这个例子中,我们演示如何创建一个简单的参数化加法器与对应的测试模块,我们希望创建一个2-to-1的加法器、一个顶层测试模块、两个driver、以及一个monitor,如下所示

1-1 加法器测试模块图

我们同时还想要对这些模块的接口进行参数化,当进行参数协商时,我们的协议希望两个driver可以提供相同宽度的数据,然后使用两个宽度中较小的那个来对monitor进行初始化,注意这和Chisel默认的行为相反。

1
2
3
4
5
6
7
import chipsalliance.rocketchip.config.{Config, Parameters}
import chisel3._
import chisel3.internal.sourceinfo.SourceInfo
import chisel3.stage.ChiselStage
import chisel3.util.random.FibonacciLFSR
import freechips.rocketchip.diplomacy.{SimpleNodeImp, RenderedEdge, ValName, SourceNode,
                                       NexusNode, SinkNode, LazyModule, LazyModuleImp}

参数协商和传递

我们希望数据宽度这一参数可以被不同模块共享,任何想要发送或接收参数信息的模块都需要有一个或多个节点。每一对节点之间可能存在一条或多条有向边,通过边进行参数协商,保证连接的节点之间参数一致。我们可以像如下所示来传递参数,注意该图必须是无环的,我们定义往sink的方向是向下的,往source的方向是向上的。

1-2 加法器Diplomacy节点

参数

在这个例子中,我们希望所有电路都使用Scala Int作为数据宽度的类型,我们定义如下case class

1
2
3
case class UpwardParam(width: Int)
case class DownwardParam(width: Int)
case class EdgeParam(width: Int)

节点实现

在节点实现(即NodeImp中),我们描述参数如何在我们的图中流动,以及如何在节点之间协商参数。边参数(E)描述了需要在边上传递的数据类型,在这个例子中就是Int;捆绑参数(B)描述了模块之间硬件实现的参数化端口的数据类型,在这个例子中则为UInt。此处edge函数实际执行了节点之间的参数协商,比较了向上和向下传播的参数,并选择数据宽度较小的那个作为协商结果。

1
2
3
4
5
6
7
8
// 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}")
}

节点

节点可以接收或生成参数,通过Diplomacy参数化的模块必须有一个或多个节点才能接收或发送参数。除了向上和向下两个方向之外,还有向内和向外两个方向属性。对一个节点而言,指向该节点的边是相对于该节点的向内边,反之则为向外边。

1-3 向内与向外

我们继续为driver创建节点,即SourceNode。由于SourceNode只沿向外边生成向下流动的参数,节点实现和之前一样。对AdderDriverNode而言,类型为Seq[DownwardParam]widths表示初始化该节点(AdderDriver)的模块时输出的数据宽度,这里使用Seq是因为每个节点可能驱动多个输出,在这个例子中,每个节点会连接到加法器和monitor。

1
2
3
/** node for [[AdderDriver]] (source) */
class AdderDriverNode(widths: Seq[DownwardParam])(implicit valName: ValName)
  extends SourceNode(AdderNodeImp)(widths)

Monitor节点也一样,但是为SinkNode,只从向内边接收上游传来的参数,该节点接收的参数是类型为UpwardParamwidth

1
2
3
/** node for [[AdderMonitor]] (sink) */
class AdderMonitorNode(width: UpwardParam)(implicit valName: ValName)
  extends SinkNode(AdderNodeImp)(Seq(width))

加法器节点接收两个AdderDriverNode的输入,并把输出传递给monitor,该节点为NexusNodedFn将向内边传来的向下的参数,映射到向外边的向下的参数,uFn将向外边的向上的参数,映射到向内边的向上的参数。

1
2
3
4
/** node for [[Adder]] (nexus) */
class AdderNode(dFn: Seq[DownwardParam] => DownwardParam,
                uFn: Seq[UpwardParam] => UpwardParam)(implicit valName: ValName)
  extends NexusNode(AdderNodeImp)(dFn, uFn)

创建LazyModule

Lazy的意思是指将表达式的evaluation推迟到需要的时候。在创建Diplomacy图之后,参数协商是lazy完成的,因此我们想要参数化的硬件也必须延迟生成,因此需要使用LazyModule。需要注意的是,定义Diplomacy图的组件(在这个例子里为节点)的创建不是lazy的,模块硬件需要写在LazyModuleImp

在这个例子中,我们希望driver将相同位宽的数据输入到加法器中,monitor的数据来自加法器的输出以及driver,所有这些数据位宽都应该相同。我们可以通过AdderNoderequire来限制这些参数,将DownwardParam向下传递,以及将UpwardParam向上传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** 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
    }
  )
  lazy val module = new LazyModuleImp(this) {
    require(node.in.size >= 2)
    node.out.head._1 := node.in.unzip._1.reduce(_ + _)
  }

  override lazy val desiredName = "Adder"
}

AdderDriver随机生成位宽为finalWidth的数据,并传递到numOutputs个source。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** driver (source)
  * drives one random number on multiple outputs */
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) {
    // 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 }
  }

  override lazy val desiredName = "AdderDriver"
}

AdderMonitor打印加法器输出并检测错误,有两个AdderMonitorNode节点从AdderDriver接收加法的两个输入,以及一个AdderMonitorNode节点从加法器接收加法的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** monitor (sink) */
class AdderMonitor(width: Int, numOperands: Int)(implicit p: Parameters) extends LazyModule {
  val nodeSeq = Seq.fill(numOperands) { new AdderMonitorNode(UpwardParam(width)) }
  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}")

    // basic correctness checking
    io.error := nodeSum.in.head._1 =/= nodeSeq.map(_.in.head._1).reduce(_ + _)
  }

  override lazy val desiredName = "AdderMonitor"
}

创建顶层模块

顶层模块负责初始化所有模块,注意这里driver和monitor初始化了不同的width,但是参数会通过Diplomacy框架进行协商。我们通过:=来创建Diplomacy图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** top-level connector */
class AdderTestHarness()(implicit p: Parameters) extends LazyModule {
  val numOperands = 2
  val adder = LazyModule(new Adder)
  // 8 will be the downward-traveling widths from our drivers
  val drivers = Seq.fill(numOperands) { LazyModule(new AdderDriver(width = 8, numOutputs = 2)) }
  // 4 will be the upward-traveling width from our monitor
  val monitor = LazyModule(new AdderMonitor(width = 4, numOperands = numOperands))

  // create edges via binding operators between nodes in order to define a complete graph
  drivers.foreach{ driver => adder.node := driver.node }

  drivers.zip(monitor.nodeSeq).foreach { case (driver, monitorNode) => monitorNode := driver.node }
  monitor.nodeSum := adder.node

  lazy val module = new LazyModuleImp(this) {
    when(monitor.module.io.error) {
      printf("something went wrong")
    }
  }

  override lazy val desiredName = "AdderTestHarness"
}

生成Verilog

以下是生成电路的Verilog代码。

1
2
3
val verilog = (new ChiselStage).emitVerilog(
  LazyModule(new AdderTestHarness()(Parameters.empty)).module
)

也可以用以下代码替代。

1
2
3
4
5
object Elaborate extends App {
  (new ChiselStage).execute(args, Seq(chisel3.stage.ChiselGeneratorAnnotation(
    () => LazyModule(new AdderTestHarness()(Parameters.empty)).module))
  )
}

附录:环境配置

build.sbt配置如下,需要注意的是不要直接使用dependsOn来添加对rocket-chip的依赖,而是需要通过git submodule来添加,并手动设置rocket-chip的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
val chiselVersion = "3.5.4"
scalaVersion := "2.12.16"

lazy val commonSettings = Seq(
  scalacOptions ++= Seq(
    "-language:reflectiveCalls",
    "-deprecation",
    "-unchecked",
    "-feature",
    "-Xsource:2.11"
  ),
  libraryDependencies ++= Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value),
  libraryDependencies ++= Seq("org.json4s" %% "json4s-jackson" % "3.6.12"),
  libraryDependencies ++= Seq("org.scalatest" %% "scalatest" % "3.2.12" % "test"),
  addCompilerPlugin(("org.scalamacros" % "paradise" % "2.1.1").cross(CrossVersion.full))
)

lazy val chiselSettings = Seq(
  libraryDependencies ++= Seq(
    "edu.berkeley.cs" %% "chisel3" % chiselVersion,
    "edu.berkeley.cs" %% "chiseltest" % "0.5.4"
  ),
  addCompilerPlugin(("edu.berkeley.cs" % "chisel3-plugin" % chiselVersion).cross(CrossVersion.full))
)

lazy val `api-config-chipsalliance` = (project in file("rocket-chip/api-config-chipsalliance/build-rules/sbt"))
  .settings(commonSettings)

lazy val hardfloat = (project in file("rocket-chip/hardfloat"))
  .settings(commonSettings, chiselSettings)

lazy val rocketMacros = (project in file("rocket-chip/macros"))
  .settings(commonSettings)

lazy val rocketchip = (Project("rocket-chip", file("rocket-chip/src")))
  .settings(commonSettings, chiselSettings)
  .settings(
    Compile / scalaSource := baseDirectory.value / "main" / "scala",
    Compile / resourceDirectory := baseDirectory.value / "main" / "resources"
  )
  .dependsOn(`api-config-chipsalliance`)
  .dependsOn(hardfloat)
  .dependsOn(rocketMacros)

lazy val example = (project in file("."))
  .settings(commonSettings, chiselSettings)
  .dependsOn(rocketchip)
This post is licensed under CC BY 4.0 by the author.

TileLink笔记(五):TL-C

Chisel笔记(二):使用Rocket-chip的TLXbar