Tool Introduction

Basic usage of the verification tool.

To meet the requirements of an open verification environment, we have developed the Picker tool, which is used to convert RTL designs into multi-language interfaces for verification. We will use the environment generated by the Picker tool as the basic verification environment. Next, we will introduce the Picker tool and its basic usage.

Introduction to Picker

Picker is an auxiliary tool for chip verification with two main functions:

  1. Packaging RTL Design Verification Modules: Picker can package RTL design verification modules (.v/.scala/.sv) into dynamic libraries and provide programming interfaces in various high-level languages (currently supporting C++, Python, Java, Scala, Golang) to drive the circuit.

  2. Automatic UVM-TLM Code Generation: Picker can automate TLM code encapsulation based on the UVM sequence_item provided by the user, providing a communication interface between UVM and other high-level languages such as Python.

This tool allows users to perform chip unit testing based on existing software testing frameworks such as pytest, junit, TestNG, go test, etc. Advantages of Verification Using Picker:

  1. No RTL Design Leakage: After conversion by Picker, the original design files (.v) are transformed into binary files (.so). Verification can still be performed without the original design files, and the verifier cannot access the RTL source code.

  2. Reduced Compilation Time: When the DUT (Design Under Test) is stable, it only needs to be compiled once (packaged into a .so file).

  3. Wide User Base: With support for multiple programming interfaces, it caters to developers of various languages.

  4. Utilization of a Rich Software Ecosystem: Supports ecosystems such as Python3, Java, Golang, etc.

  5. Automated UVM Transaction Encapsulation: Enables communication between UVM and Python through automated UVM transaction encapsulation.

RTL Simulators Currently Supported by Picker:

  1. Verilator

  2. Synopsys VCS Working Principle of Picker The main function of Picker is to convert Verilog code into C++ or Python code. For example, using a processor developed with Chisel: first, it is converted into Verilog code through Chisel’s built-in tools, and then Picker provides high-level programming language interfaces.Working Principle of Picker

Python Module Generation

Process of Module Generation

Picker exports Python modules based on C++.

  • Picker is a code generation tool. It first generates project files and then uses make to compile them into binary files.

  • Picker first uses a simulator to compile the RTL code into a C++ class and then compiles it into a dynamic library (see the C++ steps for details).

  • Using the Swig tool, Picker then exports the dynamic library as a Python module based on the C++ header file definitions generated in the previous step.

  • Finally, the generated module is exported to a directory, with other intermediate files being either cleaned up or retained as needed.

Swig is a tool used to export C/C++ code to other high-level languages. It parses C++ header files and generates corresponding intermediate code. For detailed information on the generation process, please refer to the Swig official documentation . For information on how Picker generates C++ classes, please refer to C++ .

  • The generated module can be imported and used by other Python programs, with a file structure similar to that of standard Python modules.

Using the Python Module

  • The --language python or --lang python parameter specifies the generation of the Python base library.

  • The --example, -e parameter generates an executable file containing an example project.

  • The --verbose, -v parameter preserves intermediate files generated during project creation.

Using the Tool to Generate Python’s DUT Class

Using the simple adder example from Case One:

  • Picker automatically generates a base class in Python, referred to as the DUT class. For the adder example, the user needs to write test cases, importing the Python module generated in the previous section and calling its methods to operate on the hardware module. The directory structure is as follows:
picker_out_adder
    |-- UT_Adder                # Project generated by Picker tool
    |   |-- Adder.fst.hier
    |   |-- _UT_Adder.so
    |   |-- __init__.py
    |   |-- libDPIAdder.a
    |   |-- libUTAdder.so
    |   `-- libUT_Adder.py
    `-- example.py              # User-written code
  • The DUTAdder class has a total of eight methods, as shown below:
class DUTAdder:
    def InitClock(name: str)    # Initialize clock, with the clock pin name as a parameter, e.g., clk
    def Step(i: int = 1)        # Advance the circuit by i cycles
    def StepRis(callback: Callable, args=None, args=(), kwargs={})  # Set rising edge callback function
    def StepFal(callback: Callable, args=None, args=(), kwargs={})  # Set falling edge callback function
    def SetWaveform(filename)   # Set waveform file
    def SetCoverage(filename)   # Set code coverage file
    def RefreshComb()           # Advance combinational circuit
    def Finish()                # Destroy the circuit
  • Pins corresponding to the DUT, such as reset and clock, are represented as member variables in the DUTAdder class. As shown below, pin values can be read and written via the value attribute.
from UT_Adder import * 
dut = DUTAdder()
dut.a.value = 1  # Assign value to the pin by setting the .value attribute
dut.a[12] = 1    # Assign value to the 12th bit of the input pin a
x = dut.a.value  # Read the value of pin a
y = dut.a[12]    # Read the 12th bit of pin a

General Flow for Driving DUT

  1. Create DUT and Set Pin Modes: By default, pins are assigned values on the rising edge of the next cycle. For combinational logic, you need to set the assignment mode to immediate assignment.

  2. Initialize the Clock: This binds the clock pin to the internal xclock of the DUT. Combinational logic does not require a clock and can be ignored.

  3. Reset the Circuit: Most sequential circuits need to be reset.

  4. Write Data to DUT Input Pins: Use the pin.Set(x) interface or pin.value = x for assignment.

  5. Drive the Circuit: Use Step for sequential circuits and RefreshComb for combinational circuits.

  6. Obtain and Check Outputs of DUT Pins: For example, compare the results with a reference model using assertions.

  7. Complete Verification and Destroy DUT: Calling Finish() will write waveform, coverage, and other information to files.

The corresponding pseudocode is as follows:

from UT_DUT import *

# 1 Create
dut = DUT()

# 2 Initialize
dut.SetWaveform("test.fst")
dut.InitClock("clock")

# 3 Reset
dut.reset = 1
dut.Step(1)
dut.reset = 0
dut.Step(1)

# 4 Input Data
dut.input_pin1.value = 0x123123
dut.input_pin3.value = "0b1011"

# 5 Drive the Circuit
dut.Step(1)

# 6 Get Results
x = dut.output_pin.value
print("result:", x)

# 7 Destroy
dut.Finish()

Other Data Types

In general, most DUT verification tasks can be accomplished using the interfaces provided by the DUT class. However, for special cases, additional interfaces are needed, such as custom clocks, asynchronous operations, advancing combinational circuits and writing waveforms, and modifying pin properties. In the DUT class generated by Picker, in addition to XData type pin member variables , there are also XClock type xclock and XPort type xport .

class DUTAdder(object):
    xport: XPort         # Member variable xport for managing all pins in the DUT
    xclock: XClock       # Member variable xclock for managing the clock
    # DUT Pins
    a: XData
    b: XData
    cin: XData
    cout: XData

XData Class

  • Data in DUT pins usually have an uncertain bit width and can be in one of four states: 0, 1, Z, and X. Picker provides XData to represent pin data in the circuit. Main Methods
class XData:
    # Split XData, for example, create a separate XData for bits 7-10 of a 32-bit XData
    #  name: Name, start: Start bit, width: Bit width, e.g., auto sub = a.SubDataRef("sub_pin", 0, 4)
    def SubDataRef(name, start, width): XData
    def GetWriteMode(): WriteMode     # Get the write mode of XData: Imme (immediate), Rise (rising edge), Fall (falling edge)
    def SetWriteMode(mode: WriteMode) # Set the write mode of XData, e.g., a.SetWriteMode(WriteMode::Imme)
    def DataValid(): bool             # Check if the data is valid (returns false if value contains X or Z states, otherwise true)
    def W(): int                      # Get the bit width of XData (0 indicates XData is of Verilog's logic type, otherwise it's the width of Vec type)
    def U(): int                      # Get the unsigned value of XData (e.g., x = a.value)
    def S(): int                      # Get the signed value of XData
    def String(): str                 # Convert XData to a hexadecimal string, e.g., "0x123ff", if ? appears, it means X or Z state in the corresponding 4 bits
    def Equal(xdata): bool            # Compare two XData instances for equality
    def Set(value)                    # Assign value to XData, value can be XData, string, int, bytes, etc.
    def GetBytes(): bytes             # Get the value of XData in bytes format
    def Connect(xdata): bool          # Connect two XData instances; only In and Out types can be connected. When Out data changes, In type XData will be automatically updated.
    def IsInIO(): bool                # Check if XData is of In type, which can be read and written
    def IsOutIO(): bool               # Check if XData is of Out type, which is read-only
    def IsBiIO(): bool                # Check if XData is of Bi type, which can be read and written
    def IsImmWrite(): bool            # Check if XData is in Imm write mode
    def IsRiseWrite(): bool           # Check if XData is in Rise write mode
    def IsFallWrite(): bool           # Check if XData is in Fall write mode
    def AsImmWrite()                  # Change XData's write mode to Imm
    def AsRiseWrite()                 # Change XData's write mode to Rise
    def AsFallWrite()                 # Change XData's write mode to Fall
    def AsBiIO()                      # Change XData to Bi type
    def AsInIO()                      # Change XData to In type
    def AsOutIO()                     # Change XData to Out type
    def FlipIOType()                  # Invert the IO type of XData, e.g., In to Out or Out to In
    def Invert()                      # Invert the data in XData
    def At(index): PinBind            # Get the pin at index, e.g., x = a.At(12).Get() or a.At(12).Set(1)
    def AsBinaryString()              # Convert XData's data to a binary string, e.g., "1001011"

To simplify assignment operations, XData has overloaded property assignment for Set(value) and U() methods, allowing assignments and retrievals with pin.value = x and x = pin.value.

# Access with .value
# a is of XData type
a.value = 12345        # Decimal assignment
a.value = 0b11011      # Binary assignment
a.value = 0o12345      # Octal assignment
a.value = 0x12345      # Hexadecimal assignment
a.value = -1           # Assign all bits to 1, a.value = x is equivalent to a.Set(x)
a[31] = 0              # Assign value to bit 31
a.value = "x"          # Assign high impedance state
a.value = "z"          # Assign unknown state
x = a.value            # Retrieve value, equivalent to x = a.U()

XPort Class

  • Directly operating on XData is clear and intuitive when dealing with a few pins. However, managing multiple XData instances can be cumbersome. XPort is a wrapper around XData that allows centralized management of multiple XData instances. It also provides methods for convenient batch management. Initialization and Adding Pins
port = XPort("p")  # Create an XPort instance with prefix p

Main Methods

class XPort:
    def XPort(prefix = "")      # Create a port with prefix prefix, e.g., p = XPort("tile_link_")
    def PortCount(): int        # Get the number of pins in the port (i.e., number of bound XData instances)
    def Add(pin_name, XData)    # Add a pin, e.g., p.Add("reset", dut.reset)
    def Del(pin_name)           # Delete a pin
    def Connect(xport2)         # Connect two ports
    def NewSubPort(std::string subprefix): XPort # Create a sub-port with all pins starting with subprefix
    def Get(key, raw_key = False): XData         # Get XData
    def SetZero()                                # Set all XData in the port to 0

XClock Class

  • XClock is a wrapper for the circuit clock used to drive the circuit. In traditional simulation tools (e.g., Verilator), you need to manually assign values to clk and update the state using functions like step_eval. Our tool provides methods to bind the clock directly to XClock, allowing the Step() method to simultaneously update the clk and circuit state. Initialization and Adding Pins
# Initialization
clk = XClock(stepfunc)  # Parameter stepfunc is the circuit advancement method provided by DUT backend, e.g., Verilator's step_eval

Main Methods

class XClock:
    def Add(xdata)       # Bind Clock with xdata, e.g., clock.Add(dut.clk)
    def Add(xport)       # Bind Clock with XData
    def RefreshComb()    # Advance circuit state without advancing time or dumping waveform
    def RefreshCombT()   # Advance circuit state (advance time and dump waveform)
    def Step(int s = 1)  # Advance the circuit by s clock cycles, DUT.Step = DUT.xclock.Step
    def StepRis(func, args=(), kwargs={})
Last modified September 12, 2024: Fix typo (4b0984f)