Framework
Tools and API
Overall VexiiRiscv is based on a few tools and API which aim at describing hardware in more productive/flexible ways than with Verilog/VHDL.
Scala : Which will take care of the elaboration
SpinalHDL : Which provide a hardware description API
Plugin : Which are used to inject hardware in the CPU. Plugins can discover each others.
Fiber : Which allows to define elaboration threads (used in the plugins)
Retainer : Which allows to block the execution of the elaboration threads waiting on it
Database : Which specify a shared scope for all the plugins to share elaboration time stuff
spinal.lib.misc.pipeline : Which allow to pipeline things in a very dynamic manner.
spinal.lib.logic : Which provide the Quine McCluskey algorithm to generate logic decoders from the elaboration time specifications
Scala / SpinalHDL
VexiiRiscv is implemented in Scala and the SpinalHDL API to generate hardware in a explicit manner.
Scala is a general purpose programming language (like C/C++/Java/Rust/…). Statically typed, with a garbage collector. This combination allows to goes way beyond what regular HDL allows in terms of hardware elaboration time capabilities.
Here is a simple example of scala/SpinalHDL:
// Lets define a Counter Component/Module, with a "width" parameter
class Counter(width: Int) extends Component {
// Lets define all its inputs/outputs in a io Bundle (Kinda similar to a SystemVerilog interface)
val io = new Bundle {
val clear = in Bool()
val value = out UInt(width bits)
}
val accumulator = Reg(UInt(width bits)) init(0) // In SpinalHDL registers/flipflop are defined explicitly. Not inferred.
accumulator := accumulator + 1 //Each cycle we increment the accumulator
when(io.clear) {
accumulator := 0 //But be override its value if io.clear is set (last assignment win)
}
// We connect the accumulator to the io.value.
io.value := accumulator
}
Here is another simple example, but which use an JtagTap tool built on the top of Scala/SpinalHDL :
// Lets define a component which will provide access to a few input/outputs through JTAG
class SimpleJtagTap extends Component {
val io = new Bundle {
val jtag = slave(Jtag())
val switches = in Bits(8 bits)
val keys = in Bits(4 bits)
val leds = out Bits(8 bits)
}
//The JtagTap tool allows to create the mapping between the JTAG bus and the hardware
val tap = new JtagTap(io.jtag, 8)
//JTAG taps need an idcode, lets add it !
val idcodeArea = tap.idcode(B"x87654321") (instructionId=4)
// For instance here we specify that the jtag instruction id 5 will allow it to read the io.switches value
val switchesArea = tap.read(io.switches) (instructionId=5)
//Lets add a few other jtag instructions to access the keys and leds hardware
val keysArea = tap.read(io.keys) (instructionId=6)
val ledsArea = tap.write(io.leds) (instructionId=7)
}
The key thing about the example above is that the JtagTap tool itself is defined in regular Scala / SpinalHDL. In other words, you can easily layer abstraction and tool on the top of the ecosystem. Use feature like Scala classes, inheritance, function overloading, collections, …, during the hardware elaboration time.
You can find more documentation about SpinalHDL here :
Plugin / Fiber / Retainer
One of the main aspect of VexiiRiscv is that all its hardware is defined inside plugins instead of a big toplevel. When you want to instantiate a VexiiRiscv CPU, you “only” need to provide a list of plugins as parameters. So, plugins can be seen as both parameters and hardware definition from a VexiiRiscv perspective.
It is quite different from the regular HDL component/module paradigm. Here are the advantages of this approach :
The CPU can be extended without modifying its core source code, just add a new plugin in the parameters
You can swap a specific implementation for another just by swapping plugin in the parameter list. (ex branch prediction, mul/div, …)
It is decentralized by nature, you don’t have a endless toplevel of doom, software interface between plugins can be used to negotiate and connect things during elaboration time.
The plugins can fork elaboration threads which cover 2 phases :
setup phase : where plugins can acquire elaboration locks on each others
build phase : where plugins can negotiate between each others and generate hardware
Simple all-in-one example
Here is a simple example :
import spinal.core._
import spinal.lib.misc.plugin._
import vexiiriscv._
import scala.collection.mutable.ArrayBuffer
// Define a new plugin kind
class FixedOutputPlugin extends FiberPlugin{
// Define a build phase elaboration thread
val logic = during build new Area{
val port = out UInt(8 bits)
port := 42
}
}
object Gen extends App{
// Generate the verilog
SpinalVerilog{
val plugins = ArrayBuffer[FiberPlugin]()
plugins += new FixedOutputPlugin()
VexiiRiscv(plugins)
}
}
Will generate
module VexiiRiscv (
output wire [7:0] FixedOutputPlugin_logic_port
);
assign FixedOutputPlugin_logic_port = 8'h42;
endmodule
Negotiation example
Here is a example where there a plugin which count the number of hardware event coming from other plugins :
import spinal.core._
import spinal.core.fiber.Retainer
import spinal.lib.misc.plugin._
import spinal.lib.CountOne
import vexiiriscv._
import scala.collection.mutable.ArrayBuffer
class EventCounterPlugin extends FiberPlugin{
val retainer = Retainer() // Will allow other plugins to block the elaboration of "logic" thread
val events = ArrayBuffer[Bool]() // Will allow other plugins to add event sources
val logic = during build new Area {
// Prevent executing this thread until the retainer is locked by other plugins
retainer.await()
// Now that all the other plugins are done adding event sources, we can generate the actual hardware
val counter = Reg(UInt(32 bits)) init(0)
counter := counter + CountOne(events) // CountOne will take each bits of events, add sum all them all. ex : 0b1011 => 3
}
}
// For the demo we want to be able to instantiate this plugin multiple times, so we add a prefix parameter to name the specific instance
class EventSourcePlugin(prefix : String) extends FiberPlugin{
withPrefix(prefix)
// Create a thread starting from the setup phase (this allow to run some code before the build phase,
// this allows to lock some other plugins retainers before their build phase
val logic = during setup new Area {
// Search for the single instance of EventCounterPlugin in the plugin pool
val ecp = host[EventCounterPlugin]
// Generate a lock to prevent the EventCounterPlugin elaboration (until we release it).
// This will allow us to add our localEvent to the ecp.events list
val ecpLocker = ecp.lock()
// Wait for the build phase before generating any hardware
awaitBuild()
// Here the local event is a input of the VexiiRiscv toplevel (just for the demo)
val localEvent = in Bool()
ecp.events += localEvent
// As everything is done, we now allow the ecp to elaborate itself
ecpLocker.release()
}
}
object Gen extends App {
SpinalVerilog {
val plugins = ArrayBuffer[FiberPlugin]()
plugins += new EventCounterPlugin()
plugins += new EventSourcePlugin("lane0")
plugins += new EventSourcePlugin("lane1")
VexiiRiscv(plugins)
}
}
module VexiiRiscv (
input wire lane0_EventSourcePlugin_logic_localEvent,
input wire lane1_EventSourcePlugin_logic_localEvent,
input wire clk,
input wire reset
);
wire [31:0] _zz_EventCounterPlugin_logic_counter;
reg [1:0] _zz_EventCounterPlugin_logic_counter_1;
wire [1:0] _zz_EventCounterPlugin_logic_counter_2;
reg [31:0] EventCounterPlugin_logic_counter;
assign _zz_EventCounterPlugin_logic_counter = {30'd0, _zz_EventCounterPlugin_logic_counter_1};
assign _zz_EventCounterPlugin_logic_counter_2 = {lane1_EventSourcePlugin_logic_localEvent,lane0_EventSourcePlugin_logic_localEvent};
always @(*) begin
case(_zz_EventCounterPlugin_logic_counter_2)
2'b00 : _zz_EventCounterPlugin_logic_counter_1 = 2'b00;
2'b01 : _zz_EventCounterPlugin_logic_counter_1 = 2'b01;
2'b10 : _zz_EventCounterPlugin_logic_counter_1 = 2'b01;
default : _zz_EventCounterPlugin_logic_counter_1 = 2'b10;
endcase
end
always @(posedge clk or posedge reset) begin
if(reset) begin
EventCounterPlugin_logic_counter <= 32'h00000000;
end else begin
EventCounterPlugin_logic_counter <= (EventCounterPlugin_logic_counter + _zz_EventCounterPlugin_logic_counter);
end
end
endmodule
Database
In VexiiRiscv, there is the possibility to define elaboration time variable which are unique to each VexiiRiscv instance while being easily accessible as if they were global variable. For instance XLEN, PC_WIDTH, INSTRUCTION_WIDTH, …
Those variable are handled through the VexiiRiscv “database”. You can see it in the VexRiscv toplevel :
class VexiiRiscv extends Component{
val database = new Database
val host = database on (new PluginHost)
}
What it does is that all the plugin thread will run in the context of that database. Allowing the following patterns :
import spinal.core._
import spinal.lib.misc.plugin._
import spinal.lib.misc.database.Database
import vexiiriscv._
import scala.collection.mutable.ArrayBuffer
// In Scala, an object define a singleton / static thing.
object Global extends AreaObject{
// Lets define VIRTUAL_WIDTH as a variable in the data base.
// VIRTUAL_WIDTH will act as the "key" to access the variable value in the current context.
// If accessed before being set, it will block the current thread execution (until it is set by another thread)
val VIRTUAL_WIDTH = Database.blocking[Int]
}
// Lets define a plugin which will use the VIRTUAL_WIDTH value.
class LoadStorePlugin extends FiberPlugin{
val logic = during build new Area{
val address = Reg(UInt(Global.VIRTUAL_WIDTH.get bits))
}
}
// Lets define a plugin which will set the VIRTUAL_WIDTH value
class MmuPlugin extends FiberPlugin{
val logic = during build new Area{
Global.VIRTUAL_WIDTH.set(39)
}
}
// Lets define the scala application which can generate the VexiiRiscv hardware using those two plugins.
object Gen extends App{
SpinalVerilog{
val plugins = ArrayBuffer[FiberPlugin]()
plugins += new LoadStorePlugin()
plugins += new MmuPlugin()
VexiiRiscv(plugins)
}
}
This will generate the following hardware :
module VexiiRiscv (
input wire clk,
input wire reset
);
reg [38:0] LoadStorePlugin_logic_address;
endmodule
Keep in mind that if our toplevel had to instantiate two VexiiRiscv, each of them would have it own dedicated VIRTUAL_WIDTH.get value, while using the same VIRTUAL_WIDTH key to access it.
Pipeline API
In short, the design use a pipeline API in order to :
Propagate data into the pipeline automatically
Allow design space exploration with less paine (retiming, moving around the architecture)
Handle the valid/ready arbitration
Reduce boiler plate code
This is one of the main pillar on which VexiiRiscv is based, as it allows to define pipelines in a very distributed manner, meaning that each Plugin can very easily add and extract things on pipeline.
For instance, the plugin A can insert a given value into the pipeline at stage 1, and another plugin can ask that given value at stage 4, and that’s it, it just work.
Here is an example which expose a simple usage of the pipelining API (not related to VexiiRiscv):
Take the input at stage 0
Sum the input at stage 1
Square the sum at stage 2
Provide the result at stage 3
import spinal.core._
import spinal.lib.misc.pipeline._
class PipelineExample extends Component{
// Lets define a few inputs/outputs
val a,b = in UInt(8 bits)
val result = out(UInt(16 bits))
// Lets create the pipelining tool.
val pip = new StagePipeline
// Lets insert a and b into the pipeline at stage 0
val A = pip(0).insert(a)
val B = pip(0).insert(b)
// Lets insert the sum of A and B into the stage 1 of our pipeline
val SUM = pip(1).insert(pip(1)(A) + pip(1)(B))
// Clearly, i don't want to say pip(x)(y) on every pipelined thing.
// So instead we can create a pip.Area(x) which will provide a scope which work in stage "x"
val onSquare = new pip.Area(2){
val VALUE = insert(SUM * SUM)
}
// Lets assign our output result from stage 3
result := pip(3)(onSquare.VALUE)
// Now that everything is specified, we can build the pipeline
pip.build()
}
object PipelineExampleGen extends App{
SpinalVerilog(new PipelineExample)
}
This will generate the following verilog :
module PipelineExample (
input wire [7:0] a,
input wire [7:0] b,
output wire [15:0] result,
input wire clk,
input wire reset
);
reg [15:0] pip_node_3_onSquare_VALUE;
wire [15:0] pip_node_2_onSquare_VALUE;
reg [7:0] pip_node_2_SUM;
wire [7:0] pip_node_1_SUM;
reg [7:0] pip_node_1_B;
reg [7:0] pip_node_1_A;
wire [7:0] pip_node_0_B;
wire [7:0] pip_node_0_A;
assign pip_node_0_A = a;
assign pip_node_0_B = b;
assign pip_node_1_SUM = (pip_node_1_A + pip_node_1_B);
assign pip_node_2_onSquare_VALUE = (pip_node_2_SUM * pip_node_2_SUM);
assign result = pip_node_3_onSquare_VALUE;
always @(posedge clk) begin
pip_node_1_A <= pip_node_0_A;
pip_node_1_B <= pip_node_0_B;
pip_node_2_SUM <= pip_node_1_SUM;
pip_node_3_onSquare_VALUE <= pip_node_2_onSquare_VALUE;
end
endmodule
More documentation about it in :