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.