Skip to content

Commit 174a9d2

Browse files
committed
Updated README with detailed steps on building a test case
Signed-off-by: Navyendu V Surendranadhan <[email protected]>
1 parent 371911e commit 174a9d2

File tree

1 file changed

+205
-11
lines changed

1 file changed

+205
-11
lines changed

README.md

Lines changed: 205 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SvUTest: A Unit Testing Framework in System Verilog
22

3-
System Verilog Unit Testing Framework (SvUTest) is a framework for performing unit testing of lightweight hardware modules written in System Verilog. This project is inspired by [CppUTest](https://cpputest.github.io/), [Google Test](https://google.github.io/googletest) and UVM.
3+
System Verilog Unit Testing Framework (SvUTest) is an open source framework for performing unit testing of lightweight hardware modules written in System Verilog. This project is inspired by [CppUTest](https://cpputest.github.io/), [Google Test](https://google.github.io/googletest) and UVM.
44

55
## Introduction
66

@@ -10,26 +10,220 @@ SvUTest is an attempt at a tool that helps designers write basic sanity checks o
1010

1111
## Target Audience
1212

13-
SvUTest is meant to be used by RTL design engineers to ensure the correctness of their designs across a known set of input patterns.
13+
SvUTest is meant to be used by RTL design engineers to ensure the correctness of their designs across a known set of input patterns. This framework not meant to be a replacement for UVM and is only recommended for small designs with a handful of input/output interfaces and a set of input workloads whose output is known. UVM would still be the go-to solution for large designs with complex stimuli.
1414

15-
## UVM
15+
## Building a simple test case
1616

17-
SvUTest is not meant to replace UVM. SvUTest is only recommended for small designs with a handful of input and output interfaces and a set of input workloads whose output is known. UVM would still be the go-to solution for large designs with complex stimuli.
17+
Let's take a look at the floating-point multiplier block in examples/001_floatmul that we need to unit-test. This block, or the Design Under Test, has two input channels and an output channel, all following the valid-data-ready protocol:
18+
```
19+
input logic a_valid,
20+
input float32_t a_payload,
21+
output logic a_ready,
22+
23+
input logic b_valid,
24+
input float32_t b_payload,
25+
output logic b_ready,
26+
27+
output logic o_valid,
28+
output float32_t o_payload,
29+
input logic o_ready,
30+
```
31+
The valid on the output interface shall be asserted if and only if both the inputs' valids a_valid and b_valid are high.
32+
33+
In order to unit test this block, we need to build a test_top. The test_top is a System Verilog module with no ports and a single type parameter that accepts a test_case class that we'll build later as the parameter argument:
34+
```
35+
module floatmul_test_top
36+
// import any other packages needed by the DUT
37+
import svutest_pkg::*;
38+
#(
39+
type T_test_case = bit // Need to be overriden during instantiation
40+
);
41+
...
42+
endmodule
43+
```
44+
45+
In addition to the test_case parameter, the test top needs to instantiate the following items:
46+
* A ``busy`` signal to indicate that the DUT is operational. This signal may be driven by the DUT or may be generated by the test top. In this example, busy is driven from the DUT.
47+
* An instance of ``svutest_if_test_ctrl``. This interface supplies the clock and reset to the interior of the test top and collects the busy signal.
48+
* An instance of ``svutest_if_valid_ready`` interface for each channel on the DUT. These interfaces supply the input transactions to the DUT as well as collect the output transactions from the DUT. ``svutest_if_valid_ready`` need to be replaced with the right interface in case the protocol is different from valid-data-ready.
49+
* An instance of the DUT, with the ports on the DUT hooked to the signals from the svutest_if_test_ctrl and svutest_if_valid_ready interfaces.
50+
* Finally, an ``initial .. begin`` block where the T_test_case parameter is instantiated. ``T_test_case`` can have it's own constructor and may take any number of arguments. The arguments typically pass the interfaces declared in the test top to the test case.
51+
52+
With the above instantiations in place, the test top is now complete:
53+
```
54+
module floatmul_test_top
55+
import floatmul_pkg::*;
56+
import svutest_pkg::*;
57+
#(
58+
type T_test_case = bit
59+
);
60+
svutest_if_test_ctrl tc ();
61+
62+
svutest_if_valid_ready#(float32_t) i_a (tc.clk, tc.rst);
63+
svutest_if_valid_ready#(float32_t) i_b (tc.clk, tc.rst);
64+
svutest_if_valid_ready#(float32_t) i_o (tc.clk, tc.rst);
65+
66+
// ---------------------------------------------------------------------- //
67+
68+
floatmul u_fmul (
69+
.clk (tc.clk),
70+
.rst (tc.rst),
71+
.busy (tc.busy),
72+
73+
.a_valid (i_a.valid),
74+
.a_payload (i_a.payload),
75+
.a_ready (i_a.ready),
76+
77+
.b_valid (i_b.valid),
78+
.b_payload (i_b.payload),
79+
.b_ready (i_b.ready),
80+
81+
.o_valid (i_o.valid),
82+
.o_payload (i_o.payload),
83+
.o_ready (i_o.ready)
84+
);
85+
86+
// ---------------------------------------------------------------------- //
87+
88+
initial begin
89+
T_test_case test;
90+
test = new(tc, i_a, i_b, i_o);
91+
end
92+
endmodule
93+
```
94+
95+
Once the test top is built, we now need to build a test_case class that drives the input interfaces and evaluates the output transactions. The test case class is derived from ``svutest_pkg::test_case`` and acts as a base class for all test sequences for current DUT. ``svutest_pkg::test_case`` class drives the clock and reset to the DUT while monitoring the busy, manages the interfaces which the drive the DUT and accepts the svutest_if_test_ctrl interface and a test name as constructor arguments:
96+
97+
```
98+
class floatmul_utest extends test_case;
99+
typedef valid_ready_driver#(float32_t) T_a_driver;
100+
typedef valid_ready_driver#(float32_t) T_b_driver;
101+
typedef valid_ready_driver#(float32_t) T_o_driver;
102+
103+
sender_agent#(T_a_driver) m_a_agent;
104+
sender_agent#(T_b_driver) m_b_agent;
105+
target_agent#(T_o_driver) m_o_agent;
106+
107+
function new (
108+
virtual svutest_if_test_ctrl vif_test_ctrl,
109+
T_a_driver::T_vif vif_a, // Driver class has a T_vif
110+
T_b_driver::T_vif vif_b, // typedef for convenience
111+
T_o_driver::T_vif vif_o,
112+
string test_case_name
113+
);
114+
T_a_driver a_driver;
115+
T_b_driver b_driver;
116+
T_o_driver o_driver;
117+
118+
// test_case takes a svutest_if_test_ctrl and a string as arguments.
119+
// The caller of this class's constructor need to pass the
120+
// svutest_if_test_ctrl instance and a valid test name
121+
super.new(vif_test_ctrl, $sformatf("fmul:%0s", test_case_name));
122+
123+
a_driver = new(vif_a);
124+
b_driver = new(vif_b);
125+
o_driver = new(vif_o);
126+
127+
m_a_agent = new(this, a_driver);
128+
m_b_agent = new(this, b_driver);
129+
m_o_agent = new(this, o_driver);
130+
endfunction
131+
endclass
132+
```
133+
134+
``sv_utest_pkg`` provides drivers for 4 different interface protocols:
135+
1. Simple data without any qualifier
136+
2. Data with valid
137+
3. Data with valid and ready
138+
4. Data vector with valid_count and ready_count. The number of transfers that happens on a cycle will be equal to min(valid_count, ready_count)
139+
140+
The package also provides 3 different agent classes that can work with each of the above protocols:
141+
1. Sender agent, which injects data into the DUT
142+
2. Target agent, which drives response into the DUT, while also extracting output from the DUT
143+
3. Monitor agent, which only monitors an output interface of the DUT without prodiving any response
18144

19-
## Requirements
145+
The example above instantiates two sender agents and an output agent inside the class and their corresponding driver objects inside the constructor. The driver instances are not needed after the agents are formed and can be declared as local varaibles inside the constructor. The constructor connects the virtual interfaces to the drivers and passes the drivers to the agents. (Note: This syntax may change in future versions).
20146

21-
A System Verilog compatible compiler and simulator
147+
Once the drivers and agents are set up, the user needs to extend the base class once again and populate two virtual functions ``test_case::inject()`` and ``test_case::check``. ``inject()`` is used to push transactions into different sender agents. The sender_agent class provides a function ``put()`` to pass a transaction to the DUT through the driver. Any number of ``put()`` calls may be made from the inject function.
22148

23-
## Example
149+
The transactions emitted from the output channels of the DUT are collected by the target and monitor agents and populated into an internal queue called ``m_mon_queue``. This queue can be queried from the virtual function ``check()`` for correctness of output transactions. Two macros ``UTEST_ASSERT(expr)`` and ``UTEST_ASSERT_EQ(expr_lhs, expr_rhs)`` are provided by ``svutest_defines.svh`` to help the user with the timestamp, line number and a failure count summary for the given test.
150+
151+
```
152+
/// A = 0, B = 0
153+
class floatmul_test_0_0 extends floatmul_utest;
154+
function new (
155+
virtual svutest_if_test_ctrl vif_test_ctrl,
156+
T_a_driver::T_vif vif_a,
157+
T_b_driver::T_vif vif_b,
158+
T_o_driver::T_vif vif_o
159+
);
160+
161+
super.new(vif_test_ctrl, vif_a, vif_b, vif_o, "0_0");
162+
endfunction
163+
164+
function void inject ();
165+
// Inject 0.0 on interface A
166+
m_a_agent.put('{ valid: 1'b1, payload: '{ sign: 1'b0, exponent: '0, mantissa: '0 } });
167+
// Inject 0.0 on interface B
168+
m_b_agent.put('{ valid: 1'b1, payload: '{ sign: 1'b0, exponent: '0, mantissa: '0 } });
169+
endfunction
170+
171+
function void check ();
172+
// Expect a single extry in the output queue of interface O
173+
`UTEST_ASSERT_EQ($size(m_o_agent.m_mon_queue), 1)
174+
175+
// Expect the first entry in the output queue of interface O to be 0.0
176+
`UTEST_ASSERT_EQ(m_o_agent.m_mon_queue[0].payload, '0)
177+
endfunction
178+
endclass
179+
```
24180

25-
TODO: This section will be expanded soon.
181+
Once the test_top and test cases are set up, we need to populate a top module where we instantiate the test top and trigger the start of the regression:
182+
```
183+
`include "svutest_defines.svh"
184+
185+
module regress_top;
186+
import svutest_pkg::*;
187+
import floatmul_test_pkg::*;
188+
189+
// Instantiate test cases floatmul_test_0_0 and floatmul_test_012_012
190+
// for floatmul_test_top
191+
`UTEST(floatmul_test_top, floatmul_test_0_0)
192+
`UTEST(floatmul_test_top, floatmul_test_012_012)
193+
194+
initial begin
195+
regress_suite::run_all_tests();
196+
end
197+
endmodule
198+
```
199+
200+
The ``UTEST(module, test_case)`` macro creates an instance of ``module`` while passing ``test_case`` through the parameter list. Once all the required test cases are instantiated, we need to call ``regress_suite::run_all_tests();`` from an initial block as shown above.
201+
202+
The command runs all instantiated test cases, in no specific order, and prints a summary (pass +color to the simulation environment for print in color) on the console:
203+
```
204+
001_floatmul/floatmul_test_pkg.sv", 109: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_8: started at 15000s failed at 15000s
205+
Offending '(this.m_o_agent.m_mon_queue[3].payload === float32_t'{sign:0, exponent:0, mantissa:0})'
206+
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x7f000000, right == 0x0
207+
"001_floatmul/floatmul_test_pkg.sv", 111: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_12: started at 15000s failed at 15000s
208+
Offending '(this.m_o_agent.m_mon_queue[5].payload === float32_t'{sign:0, exponent:128, mantissa:0})'
209+
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x7f000000, right == 0x80000000
210+
"001_floatmul/floatmul_test_pkg.sv", 112: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_14: started at 15000s failed at 15000s
211+
Offending '(this.m_o_agent.m_mon_queue[6].payload === float32_t'{sign:0, exponent:0, mantissa:0})'
212+
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x80000000, right == 0x0
213+
"001_floatmul/floatmul_test_pkg.sv", 114: floatmul_test_pkg::\floatmul_test_012_012::check .unnamed$$_18: started at 15000s failed at 15000s
214+
Offending '(this.m_o_agent.m_mon_queue[8].payload === float32_t'{sign:0, exponent:129, mantissa:0})'
215+
15000 | UTEST_ASSERT_EQ failed. Test: fmul:012_012. Left == 0x80000000, right == 0x81000000
216+
15000 | fmul:0_0> DONE, PASS (2 / 2)
217+
15000 | fmul:012_012> DONE, FAIL (6 / 10)
218+
15000 | Status: DONE, FAIL | (Done: 2, Timeout: 0), (Pass: 1, Fail: 1, Unknown: 0)
219+
```
26220

27-
## Usage
221+
## Compiling and running
28222

29-
TODO: Will be expanded soon
223+
SvUTest framework contains only two source files: src/svutest_pkg.sv src/svutest_ctrl.sv and one header file: src/svutest_defines.svh, and can be built using any System Verilog compliant compiler. The source files need to be passed to your eda tool like SV modules while the header file is typically picked up by providing the include path. A typical invocation from the command line would be:
30224

31225
```
32-
<tool> src/svutest_pkg.sv src/svutest_ctrl.sv <include_path_flag> src
226+
<tool> src/svutest_pkg.sv src/svutest_ctrl.sv <include_path_flag> src/ path_to_other_files
33227
```
34228

35229
## Development

0 commit comments

Comments
 (0)