Managing Xilinx IP in Chisel

Integrating Xilinx IPs into an RTL project is a frustrating process requiring extra out-of-band tcl commands to be issued at build time. This has the following major downsides: 1) you cannot parameterize the module from within your RTL design, 2) don't learn of module interface bugs until synthesis. We can however use Chisel to create a polymorphic wrapper for the Xilinx IP that automatically generates correct corresponding tcl scripts which are sourced at build time. This allows Xilinx IP to be used dynamically from within a Chisel project with automatic parameterization based on its parameterized type.

I will consider the Xilinx Integrated Logic Analyzer Core (ILA) as an example use case. Assume I have an RTL project that I wish to interface with an ILA core to capture the output of a group of signals defined by:

class dma_write_desc_t extends Bundle {
  val pcie_addr = UInt(64.W)
  val ram_sel   = UInt(2.W)
  val ram_addr  = UInt(18.W)
  val len       = UInt(16.W)
  val tag       = UInt(8.W)
}

First, from within my RTL project I would instantiate the following black box in Verilog:

ILA ila (
         .clk    (clock),
         .probe4 (ila_data_pcie_addr),
         .probe3 (ila_data_ram_sel),
         .probe2 (ila_data_ram_addr),
         .probe1 (ila_data_len)
         .probe0 (ila_data_tag)
         );

Equivalently from within Chisel I could define the following blackbox module

class ILA extends BlackBox {
  val io = IO(new Bundle {
    val clk    = Input(Clock())
    val probe4 = Input(UInt(64.W))
    val probe3 = Input(UInt(2.W))
    val probe2 = Input(UInt(18.W))
    val probe1 = Input(UInt(16.W))
    val probe0 = Input(UInt(8.W))
  })
}

and manually link the param signals to the signals of the data I want to be captured. The "param" name is defined by the Xilinx module and the naming of module interface signals needs to be strictly adhered to if when we instantiate the Xilinx IP the interface matches that of our black box. With this in blackbox in our RTL we can execute the following tcl command to create the Xilinx IP:

create_ip -name ila -vendor xilinx.com -library ip -module_name ILA

To parameterize the module to the input data we wish to capture we issue a set of setproperty commands which configure the number of probes, and the bitwidth of each one:

set_property CONFIG.C_NUM_OF_PROBES {5} [get_ips ILA]
set_property CONFIG.C_PROBE4_WIDTH {64} [get_ips ILA]
set_property CONFIG.C_PROBE3_WIDTH {2} [get_ips ILA]
set_property CONFIG.C_PROBE2_WIDTH {18} [get_ips ILA]
set_property CONFIG.C_PROBE1_WIDTH {16} [get_ips ILA]
set_property CONFIG.C_PROBE0_WIDTH {8} [get_ips ILA]

This is an annoying and error prone process.

My alternate approach has been to define a blackbox module from within Chisel which can be parameterized with a type that has its signals extracted and converted into the "param" naming convention of the underlying Xilinx IP. This also automatically emits to a file corresponding tcl code which is sourced at building time to correctly instantiate the IP with respect to the given Chisel type. The following code accomplishes this for an ILA core:

class ILARaw[T <: Bundle](gen: T) extends BlackBox {
  // Construct the IO ports manually by interating through the elements in the input bundle
  val io = IO(new Record {
    val elements = {
      val list = gen.elements.zip(0 until gen.elements.size).map(elem => {
        val index = elem._2
        // Incremenent index and associate with bitvector to bundle entry
        (s"probe$index", Input(UInt(elem._1._2.getWidth.W)))
      })

      // Add the clk port in final port map
      (list.toMap[String, chisel3.Data]).to(SeqMap) ++ ListMap("clk" -> Input(Clock()))
    }
  })

  // Construct a unique identifier for this module (which we will map to from tcl)
  override def desiredName = "ILA_" + this.hashCode()

  val ipname   = this.desiredName
  val numprobs = gen.getElements.length

  val ipgen = new StringBuilder()

  // Generate tcl to 1) construct the ip with the given name, 2) parameterize the module
  ipgen ++= s"create_ip -name ila -vendor xilinx.com -library ip -module_name ${ipname}\n"
  ipgen ++= s"set_property CONFIG.C_NUM_OF_PROBES {${numprobs}} [get_ips ${ipname}]\n"

  gen.elements.zip(0 until gen.elements.size).foreach(elem => {
    val index = elem._2
    val width = gen.getElements(index).getWidth
    ipgen ++= s"set_property CONFIG.C_PROBE${index}_WIDTH {${width}} [get_ips ${ipname}]\n"
  })

  // Dump the generated tcl for build flow
  Files.write(Paths.get(s"${ipname}.tcl"), ipgen.toString.getBytes(StandardCharsets.UTF_8))
}

The emitted tcl code (for potentially many ILA cores) can be included in your build script with the following tcl provided they lie in \(\texttt{./src/chisel_src}\):

foreach script [glob ./src/chisel_src/*.tcl] {source $script}

Date: Updated 2023-11-14

Author: Colin Drewes (drewes@cs.stanford.edu)

Created: 2024-02-16 Fri 00:11

Validate