Background
Recently I was trying to add the core as a Tile to the Rocket System, so I wanted to connect the custom debug signals from the core to the top level.
The Rocket System comes with support for trace, which outputs instruction information for each cycle of retire, but it’s not quite the same as the custom one, so I researched how to add a custom debug signal and connect it to the top level.
Analyze how the Trace signal is connected
First, observe how the Trace signals used by Rocket Chip itself are connected to the top level. On the top level, you can find the use of testchipip.CanHaveTraceIO
.
1
2
3
4
5
6
7
8
9
10
11
12
|
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 := _ }
}
|
You can see that it uses diplomacy’s BundleBridgeNexusNode. take each tile and connect its traceNode to the traceNexus. Take a look at how the module CanHaveTraceIOModuleImp
implements this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
})
}
|
As you can see, it picks up a number of trace signals from traceNexus, and then picks up the top output signal via IO(TraceOutputTop()
.
Let’s take a look at how Rocket is connected, starting with the definition of traceNode
.
1
2
3
4
|
/** 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
|
Then the Rocket Tile implementation connects its own trace to the traceSourceNode.
1
|
outer.traceSourceNode.bundle <> core.io.trace
|
Adding custom debug signals
At this point, the whole idea is pretty clear, we just need to make one from scratch. For example, if we want to expose our Custom Debug interface, the first step is to create a SourceNode in the Tile as well.
1
2
3
4
5
|
// expose debug
val customDebugSourceNode =
BundleBridgeSource(() => new CustomDebug())
val customDebugNode: BundleBridgeOutwardNode[CustomDebug] =
customDebugSourceNode
|
In BaseTileModuleImp, make the signal connections.
1
2
|
// expose debug
outer.customDebugSourceNode.bundle := core.io.debug
|
To expose to the top level, we can do it similarly. In the Subsystem.
1
2
3
4
5
6
7
8
9
|
// expose debug
val customDebugNexus = BundleBridgeNexusNode[CustomDebug]()
val tileCustomDebugNodes = tiles
.flatMap { case tile: MeowV64Tile =>
Some(tile)
}
.map { _.customDebugNode }
tileCustomDebugNodes.foreach { customDebugNexus := _ }
|
Finally the connection to IO in SubsystemModule Imp.
1
2
3
4
5
6
7
8
9
10
|
// 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)
}
|
That takes care of it.
That takes care of it.
Summary
Finding this implementation was basically done against the trace interface that comes with it. It is more important to understand the two layers inside diplomacy. The first layer is to make some connections between the different modules, and then the second layer handles the actual signals and logic in ModuleImp.