[一生一芯筆記]Chisel diplomacy精講

msuad發表於2024-05-05

前言

在做“一生一芯”的時候,碰見第一個學習坡度陡峭,而又無法避開的一點:diplomacy

這是一個包含在rocket-chip中的工具,首先如何匯入就是一個難題;其次,diplomac其使用了非常多的scala高階語法,這需要對語言有一定的熟悉度。

根據過往經歷來看,我敢肯定在我學會後再回過頭看這個問題肯定是較為簡單,也無法理解新手在這方面的疑惑。

故在學習時同步記下這篇文章,以希望留下一些記錄,待以後查閱、後人借鑑。

————2024.5.4

準備思路

準備方面,我使用了ysyx餘博的工程,可以較好地本地匯入rocket-chip的包

省去了寫mill的煩惱

用的是chisel7

然後看程式碼(程式碼+語法)

以這份翻譯(官方的樣例工程)作為展開https://shili2017.github.io/posts/CHISEL1/

先粘一份完整程式碼,然後聽我慢慢解析。這份程式碼是對import的有些改動的。

package ysyx

import chisel3._
import chisel3.experimental.SourceInfo
import chisel3.util.random.FibonacciLFSR

import circt.stage.ChiselStage
import org.chipsalliance.cde.config.Parameters

import chisel3._
import org.chipsalliance.cde.config.Parameters
import freechips.rocketchip.system.DefaultConfig
import freechips.rocketchip.diplomacy._

case class UpwardParam(width: Int)
case class DownwardParam(width: Int)
case class EdgeParam(width: Int)

// 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}")
}

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

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

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

/** 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"
}
/** 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"
}
/** 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"
}

/** 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"
}
object Elaborate extends App {
//   (new ChiselStage).execute(args, Seq(chisel3.stage.ChiselGeneratorAnnotation(
//     () => LazyModule(new AdderTestHarness()(Parameters.empty)).module))
//   )
  val verilog = ChiselStage.emitSystemVerilog(
  LazyModule(new AdderTestHarness()(Parameters.empty)).module
)
println(verilog)
}

引數協商和傳遞

引數

case class UpwardParam(width: Int)
case class DownwardParam(width: Int)
case class EdgeParam(width: Int)

看到這段程式碼,有一個問題,這個case class有什麼用?

case class

case class是一種特殊型別的類

case class = class + 一坨

case class Person(name: String, age: Int)

等價於

class Person(val name: String, val age: Int) {
  override def toString = s"Person(name=$name, age=$age)"
  override def equals(other: Any) = other match {
    case that: Person => this.name == that.name && this.age == that.age
    case _ => false
  }
  override def hashCode() = scala.util.hashing.MurmurHash3.productHash(this)
}

object Person {
  def apply(name: String, age: Int) = new Person(name, age)
  def unapply(p: Person): Option[(String, Int)] = Some((p.name, p.age))
}

注意case class這裡的引數列表,預設情況下,case clas的構造引數會轉換成val型別的欄位

節點

在節點實現(即NodeImp中),我們描述引數如何在我們的圖中流動,以及如何在節點之間協商引數。邊引數(E)描述了需要在邊上傳遞的資料型別,在這個例子中就是Int;捆綁引數(B)描述了模組之間硬體實現的引數化埠的資料型別,在這個例子中則為UInt。此處edge函式實際執行了節點之間的引數協商,比較了向上和向下傳播的引數,並選擇資料寬度較小的那個作為協商結果。

// 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}")
}
/** node for [[AdderDriver]] (source) */
class AdderDriverNode(widths: Seq[DownwardParam])(implicit valName: ValName)
  extends SourceNode(AdderNodeImp)(widths)

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

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

建立LazyModule

lazy

scala中,lazy表示的是使用時初始化
另外,懶惰初始化可以應用於val和def(雖然def預設就是懶惰的,但懶惰val和def在語義上有所不同,val初始化後值不>變,而def每次呼叫都可能有不同結果)。

雖然從懶惰初始化的角度看,lazy val和沒有具體實現的def看起來相似,但它們之間存在本質區別:

  • lazy val在首次訪問後會快取其結果,之後的訪問直接返回快取的值,適用於計算密集型或資源載入場景。
  • def則是每次呼叫時都執行其函式體,適合於那些結果隨時間或上下文變化的情況。
implicit

這個概念比較龐雜

  • implicit method:型別轉換
    implicit def intToDouble(i: Int): Double = i.toDouble
    def processNumber(num: Double): Unit = println(num)
    
    processNumber(5) // 由於存在隱式轉換,這裡可以傳入Int型別
    
  • implicit param:預設行為、實現策略模式或依賴注入
    case class LogLevel(level: String)
    
    def log(message: String)(implicit level: LogLevel = LogLevel("INFO")) = 
      println(s"${level.level}: $message")
    
    log("This is an info message") // 使用預設的日誌級別
    implicit val debugLevel = LogLevel("DEBUG")
    log("This is a debug message") // 使用隱式提供的DEBUG級別
    
  • implicit class:
    implicit class RichString(s: String) {
    def lengthSquared: Int = s.length * s.length
    }
    
    val str = "Hello"
    println(str.lengthSquared) // 利用隱式轉換呼叫新方法
    

Lazy的意思是指將表示式的evaluation推遲到需要的時候。在建立Diplomacy圖之後,引數協商是lazy完成的,因此我們想要引數化的硬體也必須延遲生成,因此需要使用LazyModule。需要注意的是,定義Diplomacy圖的元件(在這個例子裡為節點)的建立不是lazy的,模組硬體需要寫在LazyModuleImp。

在這個例子中,我們希望driver將相同位寬的資料輸入到加法器中,monitor的資料來自加法器的輸出以及driver,所有這些資料位寬都應該相同。我們可以透過AdderNode的require來限制這些引數,將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
    }
  )
  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"
}
Partial Function

好了,又看不懂了

   { case dps: Seq[DownwardParam] =>
     require(dps.forall(dp => dp.width == dps.head.width), "inward, downward adder widths must be equivalent")
     dps.head
   }

怎麼傳參的時候就這麼一個東西就作為引數了呢

class AdderNode(dFn: Seq[DownwardParam] => DownwardParam,
               uFn: Seq[UpwardParam] => UpwardParam)

可以看到入參是一個傳名函式(傳名函式是什麼?還是百度吧)
也就是說,那一坨花括號是一個函式

{ case ... }的結構實際上是在定義一個部分函式(PartialFunction),它是一種特殊的函式型別,經常與模式匹配一起使用。部分函式可以理解為一個僅定義了部分情況(cases)的函式,對於未定義的情況,如果嘗試呼叫則會丟擲異常。

  • 讓我們直接以更簡單的例子說明部分函式的用法:

簡單例子:定義一個處理整數的匿名部分函式

val processNumbers: PartialFunction[Int, String] = {
 case x if x > 0 => s"$x is positive"
 case 0 => "Zero"
}

processNumbers(5) // 輸出: "5 is positive"
processNumbers(0) // 輸出: "Zero"
// processNumbers(-1) // 如果嘗試呼叫,會丟擲MatchError異常

在這個例子中,processNumbers是一個PartialFunction[Int, String],它只定義了兩個情況:當輸入的整數大於0時和等於0時的處理邏輯。如果嘗試傳入一個負數,由於沒有對應的case分支,Scala會丟擲MatchError。

回到原始程式碼片段:

{ case dps: Seq[DownwardParam] =>
 require(dps.forall(dp => dp.width == dps.head.width), 
         "inward, downward adder widths must be equivalent")
 dps.head
}

這裡定義的就是這樣一個部分函式,它只匹配Seq[DownwardParam]型別的輸入,執行一系列操作後返回dps.head。雖然沒有直接寫出match關鍵字,但這種結構實質上是在做模式匹配的工作,是Scala中一種優雅的處理不同型別或情況的函式定義方式。

initializer block

又看不懂了,建構函式垢面後

 lazy val module = new LazyModuleImp(this) {
   require(node.in.size >= 2
   node.out.head._1 := no.in.unzip._1.reduce(_ + _)
 }

在Scala中,當你在建立一個類的例項並立即跟隨一個大括號 { ... } 時,這個大括號內的程式碼塊實際上是該類建構函式的一部分,被稱為初始化塊(initializer block)。初始化塊會在類的建構函式執行完畢後立即執行,常用於進行進一步的初始化設定或者執行一些初始化邏輯。初始化塊可以訪問到類的所有成員,包括由建構函式引數初始化的成員。

class Person(val name:String){
 println(s"1.class Person($name)")
 val age=10
}
val p=new Person("as"){
 println(s"2.obj p ($name,$age)")
}
// 1.class Person(as)
// 2.obj p (as,10)

三要素:driver(驅動)、dut(功能模組)、monitor(檢查)中已經完成了dut的編寫,接下來是driver和monitor

AdderDriver隨機生成位寬為finalWidth的資料,並傳遞到numOutputs個source。

/** 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節點從加法器接收加法的輸出。

/** 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"
}

相關文章