Instantiate VHDL and Verilog IP

Description

A blackbox allows the user to integrate an existing VHDL/Verilog component into the design by just specifying its interfaces. It’s up to the simulator or synthesizer to do the elaboration correctly.

Defining an blackbox

An example of how to define a blackbox is shown below:

// Define a Ram as a BlackBox
class Ram_1w_1r(wordWidth: Int, wordCount: Int) extends BlackBox {

  // SpinalHDL will look at Generic classes to get attributes which
  // should be used as VHDL generics / Verilog parameters
  // You can use String, Int, Double, Boolean, and all SpinalHDL base
  // types as generic values
  val generic = new Generic {
    val wordCount = Ram_1w_1r.this.wordCount
    val wordWidth = Ram_1w_1r.this.wordWidth
  }

  // Define IO of the VHDL entity / Verilog module
  val io = new Bundle {
    val clk = in Bool
    val wr = new Bundle {
      val en   = in Bool
      val addr = in UInt (log2Up(wordCount) bit)
      val data = in Bits (wordWidth bit)
    }
    val rd = new Bundle {
      val en   = in Bool
      val addr = in UInt (log2Up(wordCount) bit)
      val data = out Bits (wordWidth bit)
    }
  }

  // Map the current clock domain to the io.clk pin
  mapClockDomain(clock=io.clk)
}
In VHDL, signals of type Bool will be translated into std_logic and Bits into std_logic_vector. If you want to get std_ulogic, you have to use a BlackBoxULogic instead of BlackBox.
In Verilog, BlackBoxUlogic has no effect.
class Ram_1w_1r(wordWidth: Int, wordCount: Int) extends BlackBoxULogic {
  ...
}

Generics

There are two different ways to declare generics:

class Ram(wordWidth: Int, wordCount: Int) extends BlackBox {

    val generic = new Generic {
      val wordCount = Ram.this.wordCount
      val wordWidth = Ram.this.wordWidth
    }

    // OR

    addGeneric("wordCount", wordWidth)
    addGeneric("wordWidth", wordWidth)
}

Instantiating a blackbox

Instantiating a BlackBox is just like instantiating a Component:

// Create the top level and instantiate the Ram
class TopLevel extends Component {
  val io = new Bundle {
    val wr = new Bundle {
      val en   = in Bool
      val addr = in UInt (log2Up(16) bit)
      val data = in Bits (8 bit)
    }
    val rd = new Bundle {
      val en   = in Bool
      val addr = in UInt (log2Up(16) bit)
      val data = out Bits (8 bit)
    }
  }

  // Instantiate the blackbox
  val ram = new Ram_1w_1r(8,16)

  // Connect all the signals
  io.wr.en   <> ram.io.wr.en
  io.wr.addr <> ram.io.wr.addr
  io.wr.data <> ram.io.wr.data
  io.rd.en   <> ram.io.rd.en
  io.rd.addr <> ram.io.rd.addr
  io.rd.data <> ram.io.rd.data
}

object Main {
  def main(args: Array[String]): Unit = {
    SpinalVhdl(new TopLevel)
  }
}

Clock and reset mapping

In your blackbox definition you have to explicitly define clock and reset wires. To map signals of a ClockDomain to corresponding inputs of the blackbox you can use the mapClockDomain or mapCurrentClockDomain function. mapClockDomain has the following parameters:

name

type

default

description

clockDomain

ClockDomain

ClockDomain.current

Specify the clockDomain which provides the signals

clock

Bool

Nothing

Blackbox input which should be connected to the clockDomain clock

reset

Bool

Nothing

Blackbox input which should be connected to the clockDomain reset

enable

Bool

Nothing

Blackbox input which should be connected to the clockDomain enable

mapCurrentClockDomain has almost the same parameters as mapClockDomain but without the clockDomain.

For example:

class MyRam(clkDomain: ClockDomain) extends BlackBox {

  val io = new Bundle {
    val clkA = in Bool
    ...
    val clkB = in Bool
    ...
  }

  // Clock A is map on a specific clock Domain
  mapClockDomain(clkDomain, io.clkA)
  // Clock B is map on the current clock domain
  mapCurrentClockDomain(io.clkB)
}

io prefix

In order to avoid the prefix “io_” on each of the IOs of the blackbox, you can use the function noIoPrefix() as shown below :

// Define the Ram as a BlackBox
class Ram_1w_1r(wordWidth: Int, wordCount: Int) extends BlackBox {

  val generic = new Generic {
    val wordCount = Ram_1w_1r.this.wordCount
    val wordWidth = Ram_1w_1r.this.wordWidth
  }

  val io = new Bundle {
    val clk = in Bool

    val wr = new Bundle {
      val en   = in Bool
      val addr = in UInt (log2Up(_wordCount) bit)
      val data = in Bits (_wordWidth bit)
    }
    val rd = new Bundle {
      val en   = in Bool
      val addr = in UInt (log2Up(_wordCount) bit)
      val data = out Bits (_wordWidth bit)
    }
  }

  noIoPrefix()

  mapCurrentClockDomain(clock=io.clk)
}

Rename all io of a blackbox

IOs of a BlackBox or Component can be renamed at compile-time using the addPrePopTask function. This function takes a no-argument function to be applied during compilation, and is useful for adding renaming passes, as shown in the following example:

class MyRam() extends Blackbox {

  val io = new Bundle {
    val clk = in Bool
    val portA = new Bundle{
      val cs   = in Bool
      val rwn  = in Bool
      val dIn  = in Bits(32 bits)
      val dOut = out Bits(32 bits)
    }
    val portB = new Bundle{
      val cs   = in Bool
      val rwn  = in Bool
      val dIn  = in Bits(32 bits)
      val dOut = out Bits(32 bits)
    }
  }

  // Map the clk
  mapCurrentClockDomain(io.clk)

  // Remove io_ prefix
  noIoPrefix()

  // Function used to rename all signals of the blackbox
  private def renameIO(): Unit = {
    io.flatten.foreach(bt => {
      if(bt.getName().contains("portA")) bt.setName(bt.getName().repalce("portA_", "") + "_A")
      if(bt.getName().contains("portB")) bt.setName(bt.getName().repalce("portB_", "") + "_B")
    })
  }

  // Execute the function renameIO after the creation of the component
  addPrePopTask(() => renameIO())
}

// This code generate these names:
//    clk
//    cs_A, rwn_A, dIn_A, dOut_A
//    cs_B, rwn_B, dIn_B, dOut_B

Add RTL source

With the function addRTLPath() you can associate your RTL sources with the blackbox. After the generation of your SpinalHDL code you can call the function mergeRTLSource to merge all of the sources together.

class MyBlackBox() extends Blackbox {

  val io = new Bundle {
    val clk   = in  Bool
    val start = in Bool
    val dIn   = in  Bits(32 bits)
    val dOut  = out Bits(32 bits)
    val ready = out Bool
  }

  // Map the clk
  mapCurrentClockDomain(io.clk)

  // Remove io_ prefix
  noIoPrefix()

  // Add all rtl dependencies
  addRTLPath("./rtl/RegisterBank.v")                         // Add a verilog file
  addRTLPath(s"./rtl/myDesign.vhd")                          // Add a vhdl file
  addRTLPath(s"${sys.env("MY_PROJECT")}/myTopLevel.vhd")     // Use an environement variable MY_PROJECT (System.getenv("MY_PROJECT"))
}

...

val report = SpinalVhdl(new MyBlackBox)
report.mergeRTLSource("mergeRTL") // Merge all rtl sources into mergeRTL.vhd and mergeRTL.v files

VHDL - No numeric type

If you want to use only std_logic_vector in your blackbox component, you can add the tag noNumericType to the blackbox.

class MyBlackBox() extends BlackBox{
  val io = new Bundle {
    val clk       = in  Bool
    val increment = in  Bool
    val initValue = in  UInt(8 bits)
    val counter   = out UInt(8 bits)
  }

  mapCurrentClockDomain(io.clk)

  noIoPrefix()

  addTag(noNumericType)  // Only std_logic_vector
}

The code above will generate the following VHDL:

component MyBlackBox is
  port(
    clk       : in  std_logic;
    increment : in  std_logic;
    initValue : in  std_logic_vector(7 downto 0);
    counter   : out std_logic_vector(7 downto 0)
  );
end component;