1. 简介
Diplomacy是一个参数协商框架,用于生成参数化的协议实现。在传统的IC设计中,如何更好地复用已有模块呢?如在Verilog中,在复用一个模块时,如果线宽不一致,需要手动修改模块的线宽,如果模块中内嵌多个模块时,每个关联模块的线宽都需要修改。如果修改不完全时,编译时就会出错。
Chisel作为一个更灵活的HDL,如何更好地解决这个问题呢?这就是Diplomacy提出的初衷。Diplomacy将模块的Port抽象为节点(Node),然后来进行协商,自动找到最优的线宽,以减少复用模块时需要修改的线宽代码。
2. 原理
2.1. 节点
一个模块有输入、输出以及当前端口,其分别抽象为节点(Node):
- Source node:上级输入端口抽象的节点,也被称为驱动节点。
- Nexus Node:当前端口抽象的节点。
- Sink Node:输出到下个模块的端口抽象的节点,也称为监控节点。
Node的主要成员变量:
- in, 输入的端口序列。
- out, 输出的端口序列。
- edges, 输入和输出线宽参数。
2.2. 参数
节点的参数就是线宽,线宽参数根据实际的模块端口需要,可能有1个或多个。一般使用case class来定义参数,增加可读性,另外也可以使用模式匹配来增强参数的表达能力。
- UpwardParam,表示从上层模块传输过来的参数。
- DownwardParam,表示传输到下层模块的参数。
- EdgeParam,表示当前模块的参数。
2.3. 参数协商
参数协商有固定的规则,必须实现一个抽象接口SimpleNodeImp,其中包括向下参数、向上参数、当前参数以及实际参数类型这四个。
- edge函数,完成参数的协商,并输出最终参数线宽。
- bundle,输出实际的参数类型和线宽。
- render,输出参数信息,用于查看分析,非关键项。
2.4. 电路代码
参数协商使用了LazyModule模式,只有在用到参数协商的时候,才执行,提升代码编译性能。与参数协商相关的电路代码更是在参数协商之后,所以Diplomacy提出一个LazyModuleImp模块,实际的电路需要在LazyModuleImp块内实现,这样就保证实际的电路在参数协商之后执行。
3. 示例
示例以一个加法器为例,两个驱动、一个加法器和校验器(监控器)。
3.1. 节点图
驱动、加法器和校验器的数据流程构成一个有向无环图,参数的流动不能是循环的,会导致计算循环而异常。
3.2. 参数
示例中参数只有一个,分为3种类型对就3种节点,可以写成下面:
case class UpwardParam(width: Int)
case class DownwardParam(width: Int)
case class EdgeParam(width: Int)
3.3. 参数协商
最后一个参数B就是实际参数的类型,线宽由上下参数协商得出。
// 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}")
}
3.4. 节点
3.4.1. 驱动节点
驱动器节点的参数是Seq,因为它输出到加法器和监控器两个节点。
/** node for [[AdderDriver]] (source) */
class AdderDriverNode(widths: Seq[DownwardParam])(implicit valName: ValName)
extends SourceNode(AdderNodeImp)(widths)
3.4.2. 监控器节点
Monitor有3个节点,每个节点只有一个输入参数,但是最终SinkNode模块依然要转为Seq类型。
/** node for [[AdderMonitor]] (sink) */
class AdderMonitorNode(width: UpwardParam)(implicit valName: ValName)
extends SinkNode(AdderNodeImp)(Seq(width))
3.4.3. 加法器节点
如图所示,加法器是电路模块,其节点要求两个函数作为参数,如下:
/** node for [[Adder]] (nexus) */
class AdderNode(dFn: Seq[DownwardParam] => DownwardParam,uFn: Seq[UpwardParam] => UpwardParam)(implicit valName: ValName)
extends NexusNode(AdderNodeImp)(dFn, uFn)
3.5. 加法器电路
加法器模块,其内部实例化加法器节点,并通过模式匹配的方法来检查加法器的参数是否符合要求。这里只是检测,并不是协商。然后通过LazyModuleImp来实现具体的电路描述。node的in和out都是Seq,且Seq中存放的都是元组,元组的第1个元素即真实的端口信号。
node.out.head._1,因为out只有一个输出端口,所以取head即可。
node.in.unzip._1.reduce(_ + _),复用reduce来累加所有解包的输入端口。
参数desiredName指定生成的模块名。
/** 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"
}
3.6. 驱动器
驱动器中有一个节点,节点有numOutputs个输出,输出参数线宽为width。驱动器和加法器的电路逻辑在LazyModuleImp中实现。
require,检测当前所有输出线宽是不是一样。
生成随机数赋值给输出信号的被加数信号。
/** 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 negotiationval 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 signalsnode.out.foreach { case (addend, _) => addend := randomAddend }}override lazy val desiredName = "AdderDriver"
}
3.7. 监控器
监控器模块,主要用来计算驱动器的输入,然后与加法器模块传过来的输出作比较。监控器3个节点,2个来自驱动器,1个来自加法器。具体的电路连接在LazyModuleImp进行。监控器模块添加一个输出信号,用于测试时观察校验结果。
/** 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 operationprintf(nodeSeq.map(node => p"${node.in.head._1}").reduce(_ + p" + " + _) + p" = ${nodeSum.in.head._1}")// basic correctness checkingio.error := nodeSum.in.head._1 =/= nodeSeq.map(_.in.head._1).reduce(_ + _)}override lazy val desiredName = "AdderMonitor"
}
3.8. 顶层模块
顶层模块,将驱动器、加法器和监控器合并起来,组成一个完整的示例。所有参与参数协商的模块,都使用LazyModule。构建2个驱动器、加法器、监控器。
11行的代码将所有驱动节点全部连接到加法器节点。
13行的代码将所有驱动节点同时全部连接到监控器节点,两者个数一样。
14行的代码将加法器节点连接到监控器的nodeSum节点。
实际监控Monitor的结果在LazyModuleImp中实现。
/** top-level connector */
class AdderTestHarness()(implicit p: Parameters) extends LazyModule {val numOperands = 2val adder = LazyModule(new Adder)// 8 will be the downward-traveling widths from our driversval drivers = Seq.fill(numOperands) { LazyModule(new AdderDriver(width = 8, numOutputs = 2)) }// 4 will be the upward-traveling width from our monitorval monitor = LazyModule(new AdderMonitor(width = 4, numOperands = numOperands))// create edges via binding operators between nodes in order to define a complete graphdrivers.foreach{ driver => adder.node := driver.node }drivers.zip(monitor.nodeSeq).foreach { case (driver, monitorNode) => monitorNode := driver.node }monitor.nodeSum := adder.nodelazy val module = new LazyModuleImp(this) {when(monitor.module.io.error) {printf("something went wrong")}}override lazy val desiredName = "AdderTestHarness"
}
4. 生成Verilog
AdderTestHarness只是LazyModule,真实的Module是其成员module.
object Main extends App {emitVerilog(LazyModule(new AdderTestHarness()(Parameters.empty)).module,Array("--emission-options=disableMemRandomization,disableRegisterRandomization","--info-mode=use","--target-dir=hdl","--full-stacktrace"))
}