向 Rocket Chip 添加自定义调试信号¶
背景¶
最近在尝试把核心作为一个 Tile 加到 Rocket System 中,所以想要把核心之前自定义的调试信号接到顶层上去。Rocket System 自带的支持是 trace,也就是输出每个周期 retire 的指令信息,但和自定义的不大一样,所以研究了一下怎么添加自定义的调试信号,并且连接到顶层。
分析 Trace 信号连接方式¶
首先,观察 Rocket Chip 自己使用的 Trace 信号是如何连接到顶层的。在顶层上,可以找到使用的是 testchipip.CanHaveTraceIO
:
trait CanHaveTraceIO { this: HasTiles =>
val module: CanHaveTraceIOModuleImp
// Bind all the trace nodes to a BB; we'll use this to generate the IO in the imp
val traceNexus = BundleBridgeNexusNode[Vec[TracedInstruction]]()
val tileTraceNodes = tiles.flatMap {
case ext_tile: WithExtendedTraceport => None
case tile => Some(tile)
}.map { _.traceNode }
tileTraceNodes.foreach { traceNexus := _ }
}
可以看到,它采用了 diplomacy 的 BundleBridgeNexusNode,把每个 tile 取出来,把它的 traceNode 接到 traceNexus 上。再看一下模块 CanHaveTraceIOModuleImp
是怎么实现的:
trait CanHaveTraceIOModuleImp { this: LazyModuleImpLike =>
val outer: CanHaveTraceIO with HasTiles
implicit val p: Parameters
val traceIO = p(TracePortKey) map ( traceParams => {
val extTraceSeqVec = (outer.traceNexus.in.map(_._1)).map(ExtendedTracedInstruction.fromVec(_))
val tio = IO(Output(TraceOutputTop(extTraceSeqVec)))
val tileInsts = ((outer.traceNexus.in) .map { case (tileTrace, _) => DeclockedTracedInstruction.fromVec(tileTrace) }
// Since clock & reset are not included with the traced instruction, plumb that out manually
(tio.traces zip (outer.tile_prci_domains zip tileInsts)).foreach { case (port, (prci, insts)) =>
port.clock := prci.module.clock
port.reset := prci.module.reset.asBool
port.insns := insts
}
tio
})
}
可以看到,它从 traceNexus 上接了若干的 trace 信号,然后通过 IO(TraceOutputTop())
接到了顶层的输出信号。
再来看看 Rocket 是如何连接的,首先是 traceNode
的定义:
/** Node for the core to drive legacy "raw" instruction trace. */
val traceSourceNode = BundleBridgeSource(() => Vec(traceRetireWidth, new TracedInstruction()))
/** Node for external consumers to source a legacy instruction trace from the core. */
val traceNode: BundleBridgeOutwardNode[Vec[TracedInstruction]] = traceNexus := traceSourceNode
然后 Rocket Tile 实现的时候,把自己的 trace 接到 traceSourceNode 上:
添加自定义调试信号¶
到这里,整个思路已经比较清晰了,我们只需要照猫画虎地做一个就行。比如要把自己的 Custom Debug 接口暴露出去,首先也是在 Tile 里面创建一个 SourceNode:
// expose debug
val customDebugSourceNode =
BundleBridgeSource(() => new CustomDebug())
val customDebugNode: BundleBridgeOutwardNode[CustomDebug] =
customDebugSourceNode
在 BaseTileModuleImp 里,进行信号的连接:
为了暴露到顶层,我们可以类似地做。在 Subsystem 中:
// expose debug
val customDebugNexus = BundleBridgeNexusNode[CustomDebug]()
val tileCustomDebugNodes = tiles
.flatMap { case tile: MeowV64Tile =>
Some(tile)
}
.map { _.customDebugNode }
tileCustomDebugNodes.foreach { customDebugNexus := _ }
最后在 SubsystemModule Imp 中连接到 IO:
// wire custom debug signals
val customDebugIO = outer.customDebugNexus.in.map(_._1)
val customDebug = IO(
Output(
Vec(customDebugIO.length, customDebugIO(0).cloneType)
)
)
for (i <- 0 until customDebug.length) {
customDebug(i) := customDebugIO(i)
}
这样就搞定了。
总结¶
找到这个实现方法,基本是对着自带的 trace 接口做的,比较重要的是理解 diplomacy 里面的两层,第一层是把不同的模块进行一些连接,然后第二层在 ModuleImp 中处理实际的信号和逻辑。