CPU Visualizer

Synopsis

In my computer architecture class we learned about more advanced CPU designs like pipelined CPUs and the various caching mechanisms that go into a modern processor. We had a lot of assignments that involved working with Chisel to design pipelined CPUs. Chisel provides a high level language for describing logic circuits, and it compiles down to FIRRTL which is an encoding for circuit networks.

We had to create new modules for pipeline stages, control paths, and hazard detection to enable the CPU to function correctly. Much of the work involved making sure that each instruction was correctly processed in each pipeline stage, as up to 5 instructions could be in the pipeline at once. In the later part of the class it was important to detect hazards like read-after-write that occur from specific combinations of instructions. Our debugger would allow us to step through the circuit in a simulator and print values at various points in the circuit, but it was still difficult to figure out the problem in an entire program’s worth of executing instructions.

A visualization helps debug because it gives an at-a-glance overview of all the values in the circuit, and shows roughly where each module is in relation to each other. I wanted to make a visualization that could display all the pipeline stages, modules, and ports in the CPU. The program would allow the user to step through the simulation and see each value by hovering over a port.

I wrote this program in Scala with Scala Swing as the GUI framework, because Chisel and the FIRRTL parser are written in Scala. My initial prototype was to draw the ALU module with its input and output ports and update them from the simulator. I could directly look at the FIRRTL to determine the reference of each port for the ALU in the simulator, then hardcode each string into my list of known ports for the ALU.

The real application would have to be dynamic enough to load any Pipelined CPU from FIRRTL, applying the same principles.

Architecture

I decided to go with an MVC model to drive the visualization.

Model: The model is a representation of the modules and pipeline stages of the CPU. The CPU is split up into modules, and each module has a list of input ports and output ports. Each individual port can take a value, holds a reference to itself in the simulator, and triggers events to update the UI. The model is populated by visiting the composite tree generated by the FIRRTL parser.

Controller: The controller handles the “step” action from the UI, advances the simulation, and tells the model to load values from the simulator

View: The view takes the model and generates a series of boxes in flex layout that represent each module and each port. The user can hover over any particular port to read the value. The view also registers an observer on the update event for each port.

Constructing the model

I use Low-FIRRTL which is the lowest-level FIRRTL available that describes each module and the various hardware nodes like logic gates and muxes that exist between them. The FIRRTL package in Scala includes a parser that creates an Abstract Syntax Tree (AST) for FIRRTL. I then run a custom visitor on this AST that extracts information about modules, their ports, and the connections between them, and I store this in data structures within the model:

trait Port extends Publisher {
  val module: CircuitModule
  val name: String
  var value: BigInt
}

// Model implementation of Port interface loaded from FIRRTL Circuit
class FIRRTLPort(n: String, bN: String, m: FIRRTLModule) extends Port {
  override val module:FIRRTLModule = m
  override val name = n
  override var value: BigInt = 0

  // bindingName is the fully qualified name in the simulator SymbolTable
  val bindingName = bN
}

// CircuitModule is an interface for a module that contains a name and a list of I/O ports
trait CircuitModule {
  val label: String
  val input: Seq[Port]
  val output: Seq[Port]
}

// FIRRTLModule is an extension of CircuitModule that uses FIRRTLPorts
// this is to expose the bindingName to the module
trait FIRRTLModule extends CircuitModule {
  val input: Seq[FIRRTLPort]
  val output: Seq[FIRRTLPort]
}

After this step I have a list of FIRRTLModule for every module in the pipeline including pipeline stages, a list of other circuit elements like muxes, and a list of connections between everything. A number of processing steps must then occur before the model is passed to the view to be rendered.

  1. Pipeline stages are separated from the list of modules into their own variables
  2. All unwanted nodes like muxes are removed. This involves removing connections including the node, and adding connections between modules connecting into the removed node and modules connecting out of the removed node.
  3. Modules are sorted into pipeline stages by performing a DFS from each connected port on the relevant sides of the two pipeline registers connected to that stage. Any nodes reachable from the DFS are added to the pipeline stage until all ports on the pipeline stage registers are checked.