In this repository, we proprose ChiselVerify, which is the begining of a verification library within Scala for digital hardware described in Chisel, but also upporting legacy components in VHDL, Verilog, or SystemVerilog. The library runs off of ChiselTest for all of the DUT interfacing.
A technical report describes the library in detail: Open-Source Verification with Chisel and Scala.
When you use this library in a research project, please cite it as:
@INPROCEEDINGS{ChiselVerify:2021,
author = {Andrew Dobis and Tjark Petersen and Hans Jakob Damsgaard and Kasper
Juul Hesse Rasmussen and Enrico Tolotto and Simon Thye Andersen and
Richard Lin and Martin Schoeberl},
title = {ChiselVerify: An Open-Source Hardware Verification Library for Chisel
and Scala},
booktitle = {2021 IEEE Nordic Circuits and Systems Conference (NORCAS): NORCHIP
and International Symposium of System-on-Chip (SoC)},
year = {2021}
}
ChiselVerify is published on Maven. To use it, add following line to your
build.sbt:
libraryDependencies += "io.github.chiselverify" % "chiselverify" % "0.2.0"
Run tests with
make
This README contains a brief overview of the library and its functionalities. For a more in-depth tutorial, please check-out the ChiselVerify Wiki. Ohter general documentation, such as technical reports and conference papers, can be found in the documentation repository.
The library can be divided into 3 main parts:
- Functional Coverage: Enabling Functional Coverage features like Cover Points, Cross Coverage, Timed Coverage and Conditional Coverage.
- Constrained Random Verification: Allowing for constraints and random variables to be defined and used directly in Scala.
- Bus Functional Models: Enabling Transactional modeling for standardized Buses like AXI4.
The idea is to implement functional coverage features directly in Chisel.
The structure of the system can be seen in the diagram below.
This is the heart of the system. It handles everything from registering the Cover Points to managing the Coverage DataBase. It will also generate the final coverage report. Registering Cover Points together will group them into a same Cover Group.
This DataBase handles the maintenance of the values that were sampled for each of the Cover Point bins. This allows us to know how much of the verification plan was tested.
The DB also keeps mappings linking Cover Groups to their contained Cover Points.
The Functional coverage system is compatible with the chisel testers2 framework.
- The
CoverageReportermust first be instanciated within a chisel test. CoverGroups can then be created by using theregistermethod of the coverage reporter. This takes as parameter aList[Cover].Coverrepresents either aCoverPointor aCoverConditionthat contains aportthat will be sampled, aportNamethat will be shown in the report and either aList[Bins], created from a name and a scala range, or aList[Condition], created from a name and an arbitrary condition function.CoverGroupsmay also contain aList[Cross]which represents a set of hit relations between two ports.- The port must then be manually sampled by calling the
samplemethod of the coverage reporter. - Once the test is done, a coverage report can be generated by calling the
printReportorreportmethods of the coverage reporter.
An example of this can be found here.
Idea: We want to check the relationship between two ports with a delay of a certain amount of cycles.
Example: We have the following situation, imagine we have a device that breaks if:
dut.io.atakes the value of1.Uat cycle 1dut.io.btakes the value of1.Uthe following cycle
We want to verify that the above case was tested. This can be done by defining a TimedCross between the two points:
val cr = new CoverageReporter(dut)
cr.register(
//Declare CoverPoints
cover("a", dut.io.a)(DefaultBin(dut.io.a))),
cover("b", dut.io.b)(DefaultBin(dut.io.b))),
//Declare timed cross point with a delay of 1 cycle
cover("timedAB", dut.io.a, dut.io.b)(Exactly(1))(
cross("both1", Seq(1 to 1, 1 to 1))
)
)Using that, we can check that we tested the above case in our test suite.
This construct can be used to check delay between two cover points.
To be able to use the timed coverage, stepping the clock must be done through the coverage reporter:
dut.clock.step(nCycles) //Will trigger an exception if used with Timed Cross Coverage
cr.step(nCycles) //WorksThis is done in order ensure that the coverage database will always remain synchronized with the DUT's internal clock.
The current implementation allows for the following special types of timing:
Eventually: This sees if a cross hit was detected at any point in the next given amount of cycles.Always: This only considers a hit if the it was detected every cycle in the next given amount of cycles.Exactly: This only considers a hit if it was detected exactly after a given amount of cycles.
Delay types can also be used in order to used Timed Assertions or Timed Expect. These can be used in order to check an assertion, in the form of an arbitrary function, with an added timing argument. We could thus check, for example, that two ports are equal two cycles appart. For example:
AssertTimed(dut, dut.io.a.peek() === dut.io.b.peek(), "aEqb expected timing is wrong")(Exactly(2)).join()This can also be done more naturally with the Expect interface:
ExpectTimed(dut,dut.io.a, dut.io.b.peek().litValue(), "aEqb expected timing is wrong")(Exactly(2)).join()These can also be used with a simplyfied syntax, inspired by ScalaTest syntax:
//For Timed Assertions
eventually(2, "aEqb expected timing is wrong") { dut.io.a.peek() === dut.io.b.peek() }
exact(2, "aEqb expected timing is wrong") { dut.io.a.peek() === dut.io.b.peek() }
always(2, "aEqb expected timing is wrong") { dut.io.a.peek() === dut.io.b.peek() }
never(2, "aEqb expected timing is wrong") { dut.io.a.peek() === dut.io.b.peek() }Here is a toy example of how to use the assertion:
always(9, "a isn't always less than or equal to one") { LtEq(dut.io.outB, dut.io.outA) }
always(9, "a isn't always greater than or equal to one") { GtEq(dut.io.outB, dut.io.outA) }Idea: A type of coverpoint that can apply arbitrary hit conditions to an arbitrary number of ports.
cover(readableName: String, ports: Data*)(conditions: Condition*)
//where a condition is declared using the bin function without a range
bin(name: String, func : Seq[BigInt] => Boolean)Example:
val cr = new CoverageReporter(dut)
cr.register(
//Declare CoverPoints
cover("aAndB", dut.io.outA, dut.io.outB)(
bin("aeqb", { case Seq(a, b) => a == b })
))Bins are thus defined using arbitrary functions of the type List[BigInt] => Boolean which represent different hit conditions.
No coverage percentage is given due to cartesian product complexity. Instead we offer the possibility to use a user-defined "expected number of Hits" to get a coverage percentage. This looks like the following:
val cr = new CoverageReporter(dut)
cr.register(
//Declare CoverPoints
cover("aAndB", dut.io.outA, dut.io.outB)(
bin("asuptobAtLeast100Times", condition = { case Seq(a, b) => a > b }, expectedHits = 100)
))The above example results in the following coverage report:
============ COVERAGE REPORT ============
============== GROUP ID: 1 ==============
COVER_CONDITION NAME: aAndB
CONDITION aeqb HAS 4 HITS
CONDITION asuptobAtLeast100 HAS 95 HITS EXPECTED 100 = 95.0%
=========================================
=========================================
The CRV package inside this project aims to mimic the functionality of SystemVerilog constraint programming and integrates them into ChiselTest. The CRV package combines a Constraint Satisfactory Problem Solver, with some helper classes to create and use random objects inside your tests. Currently, only the jacop backend is supported, but in the future other backends can be added.
class frame_t;
rand pkt_type ptype;
rand integer len;
randc bit [1:0] no_repeat;
// Constraint the members
constraint legal {
len >= 2;
len <= 5;
}class Frame extends RandObj(new Model) {
val pkType: RandVar = rand(0, 3)
val len: RandVar = rand(0, 10)
val noRepeat: RandVar = rand(0, 1, Cyclic)
val legal: ConstraintGroup = new ConstraintGroup {
len >= 2
len <= 5
}
}Random objects can be created by extending the RandObj trait. This class accepts one parameter which is a Model. A model correspond to a database in which all the random variables and constraints declared inside the RandObj are stored.
class Frame extends RandObj(new Model)A model can be initialized with a seed new Model(42), which allows the user to create reproducible tests.
Random fields are defined using the following function:
def rand(min: Int, max: Int, randType: RandType = Normal)(implicit model: Model): RandVarA random field can be added to a RandObj by declaring a Rand variable.
val len: RandVar = rand(0, 10)Random-cyclic variable can be added by declaring a Randc field inside a RandObj. This is done using the Cyclic RandType parameter.
val noRepeat: RandVar = rand(0, 1, Cyclic)Each variable can have one or multiple constraints. These are defined using constraint operators.
len >= 2In the previous block of code we are specifying that the variable len can only take values that are grater then 2.
Each constraint can be assigned to a variable and enabled or disabled at any time during the test
val lenConstraint = len > 2
[....]
lenConstraint.disable()
[....]
lenConstraint.enable()Constraints can also be grouped together in a ConstraintGroup and the group itself can be enabled or disabled.
val legal: ConstraintGroup = new ConstraintGroup {
len >= 2
len <= 5
payload.size == len
}
[...]
legal.disable()
[...]
legal.enable()By default, constraints and constraint groups are enabled when they are declared.
The list of operator used to construct constraints is the following:
<, <=, >, >=,==, div, *, mod, +, -, \=, ^, in, inside.
It is also possible to declare conditional constraints with constructors like IfCon and IfElseCon.
val constraint1: crv.Constraint = IfCon(len == 1) {
payload.size == 3
} ElseC {
payload.size == 10
}As in SystemVerilog, each random class exposes a method called randomize() this method automatically solves the
constraint specified in the class and assign to each random filed a random value. The method returns true only if the
CSP found a set of values that satisfy the current constraints.
val myPacket = new Frame(new Model)
assert(myPacket.randomize)Other usage examples can be found in our backend tests.
We will explore a handful of use cases to explore verification.
- Leros ALU (basically done)
- Heap priority queue (from MicroSemi), see also https://www.hackerearth.com/practice/notes/heaps-and-priority-queues/
- Network-on-chip (in Chisel), see https://github.com/schoeberl/soc-comm
- Decimation filter from WSA (VHDL code plus testbench given)
In the early stages of this project, we explored the possibilty of using UVM to verify Chisel designs. The sv directory thus contains a number of UVM examples.
In sv/uvm-simple-examples a number of simple examples are located. These start with a very basic testbench with no DUT attached, and gradually transition into a complete testbench.
These examples assume that a copy of Xilinx Vivado is installed and present in the PATH. The examples are currently tested only on Linux.
- The first example is taken from Vivado Design Suite Tutorial - Logic Simulation
In the directory sv/leros, the Leros ALU is tested using UVM, to showcase that Chisel and UVM can work together. This testbench is reused to also test a VHDL implementation of the ALU, to show that UVM is usable on mixed-language designs (when using a mixed-language simulator).
The VHDL implementaion is run by setting the makefile argument TOP=top_vhd.
Using the SystemVerilog DPI (Direct Programming Interface) to cosimulate with a golden model described in C is explored in the scoreboard_dpi.svh file. The C-model is implemented in scoreboard.c, and the checking functionality is called from the SystemVerilog code.
Implementing a similar functionality in Scala/Chisel has been explored via the JNI (Java Native Interface). In the directory native, the necessary code for a simple Leros tester using the JNI is implemented.
To use the JNI functionality, first run make jni to generate the correct header files and shared libraries. Then, open sbt and type project native to access the native project. Then run sbt test to test the Leros ALU using a C model called from within Scala. To switch back, type project chisel-uvm.
If you're interested in learning more about the UVM, we recommend that you explore the repository, as well as some of the following links:
- First steps with UVM
- UVM Cookbook (requires an account)
- ChipVerify.com UVM Tutorials
- Ray Salemi's UVM Primer videos
Collect pointers to relevant documents.
- https://github.com/ekiwi/paso
- https://github.com/TsaiAnson/verif
- Layering RTL, SAFL, Handel-C and BluespecConstructs on Chisel HCL, David J Greaves, see http://koo.corpus.cam.ac.uk/drafts/tndjg-008-transactional-modelling-in-chisel.html
Here are a few pointers to some interesting documentation around the topic of mutation-based fuzzing:
- American Fuzzy Lop (AFL)
- Binary fuzzing strategies: what works, what doesn't
- AFL "Whitepaper"
- A bit more about american fuzzy lop
- The fuzzing book - Mutation-Based Fuzzing
- RFuzz conference paper
- RFuzz
- Choco-Solver Java library for solving CSP problems
- QuickCheck Checker for Haskel, used Lava as example, the inspiration for ScalaCheck
Cocotb repository: cocotb is a coroutine based cosimulation library for writing VHDL and Verilog testbenches in Python.
- Philipp Wagner (FOSSi Foundation, lowRISC) "Cocotb: Python-powered hardware verification" (WOSH 2019) (video)
- Ben Rosser (University of Pennsylvania) "Cocotb: a Python-based digital logic verification framework" (CERN 2018) (pdf)
- Torbjørn Viem Ness (NTNU) "Low Power Floating-Point Unit for RISC-V" (2018) [PDF]
- Andrey Filippov (Elphel) "I will not have to learn SystemVerilog" (2016) [Blog]
- Chris Higgs (Potential Ventures) "Applying agile techniques to FPGA development" Video, Paper
- Chris Higgs (Potential Ventures) "Rapid FPGA Verification" (NMI, February 2014) [Slides]
- Smith, Andrew Michael; Mayo, Jackson; Armstrong, Robert C.; Schiek, Richard; Sholander, Peter E.; Mei, Ting (Sandia National Lab): "Digital/Analog Cosimulation using CocoTB and Xyce" (paper)
- cocotb-coverage: Extension that enables coverage and constrained random verification
- Publication in iEEE Paper
- python-uvm: port of SystemVerilog (SV) Universal Verification Methodology (UVM) 1.2 to Python and cocotb
hwt: one of the golas of this library is to implement some simulation feature similar to UVM
-
CRAVE: An advanced constrained random verification environment for SystemC
-
EnrichingUVM in SystemC with AMS extensions for randomization and functional coverage
-
Coverage directed test generation for functional verification using Bayesian network
-
LiveHD LiveHD is an infrastructure designed for Live Hardware Development. By live, we mean that small changes in the design should have the synthesis and simulation results in a few seconds, as the fast interactive systems usually response in sub-second.
