Writing Testbenches
Accessing the design
When cocotb initializes it finds the toplevel instantiation in the simulator
and creates a handle called dut
. Toplevel signals can be accessed using the
“dot” notation used for accessing object attributes in Python. The same mechanism
can be used to access signals inside the design.
# Get a reference to the "clk" signal on the toplevel
clk = dut.clk
# Get a reference to a register "count"
# in a sub-block "inst_sub_block"
# (the instance name of a Verilog module or VHDL entity/component)
count = dut.inst_sub_block.count
Finding elements in the design
To find elements of the DUT
(for example, instances, signals, constants or Verilog packages)
at a certain hierarchy level,
you can use the dir()
function on a handle.
# Print the instances and signals (which includes the ports) of the design's toplevel
print(dir(dut))
# Print the instances and signals of "inst_sub_block" under the toplevel
# which is the instance name of a Verilog module or VHDL entity/component
print(dir(dut.inst_sub_block))
# Print the packages
print(dir(cocotb.packages))
Assigning values to signals
Values can be assigned to signals using either the
value
property of a handle object
or set()
method of a handle object.
# Get a reference to the "clk" signal and assign a value
clk = dut.clk
clk.value = 1
# Direct assignment through the hierarchy
dut.input_signal.value = 12
# Assign a value to a memory deeper in the hierarchy
# ("inst_sub_block" and "inst_memory" are instance names of the
# respective Verilog modules or VHDL entity/components in the DUT)
dut.inst_sub_block.inst_memory.mem_array[4].value = 2
The assignment syntax sig.value = new_value
has the same semantics as HDL:
writes are not applied immediately, but delayed until the next write cycle.
Use sig.setimmediatevalue(new_val)
to set a new value immediately
(see setimmediatevalue()
).
Signed and unsigned values
Both signed and unsigned values can be assigned to signals using a Python int.
Cocotb makes no assumptions regarding the signedness of the signal. It only
considers the width of the signal, so it will allow values in the range from
the minimum negative value for a signed number up to the maximum positive
value for an unsigned number: -2**(Nbits - 1) <= value <= 2**Nbits - 1
Note: assigning out-of-range values will raise an OverflowError
.
A LogicArray
object can be used instead of a Python int to assign a
value to signals with more fine-grained control (e.g. signed values only).
module my_module (
input logic clk,
input logic rst,
input logic [2:0] data_in,
output logic [2:0] data_out
);
# assignment of negative value
dut.data_in.value = -4
# assignment of positive value
dut.data_in.value = 7
# assignment of out-of-range values
dut.data_in.value = 8 # raises OverflowError
dut.data_in.value = -5 # raises OverflowError
Reading values from signals
Values in the DUT can be accessed with the value
property of a handle object.
A common mistake is forgetting the .value
which just gives you a reference to a handle
(useful for defining an alias name), not the value.
The Python type of a value depends on the handle’s HDL type:
Arrays of
logic
and subtypes of that (sfixed
,unsigned
, etc.) are of typeLogicArray
.Integer nets and constants (
integer
,natural
, etc.) returnint
.Floating point nets and constants (
real
) returnfloat
.Boolean nets and constants (
boolean
) returnbool
.String nets and constants (
string
) returnbytes
.
Identifying tests
Cocotb tests are identified using the test
decorator.
Using this decorator will tell cocotb that this function is a special type of coroutine that is meant
to either pass or fail.
The test
decorator supports several keyword arguments (see section Writing and Generating Tests).
In most cases no arguments are passed to the decorator so cocotb tests can be written as:
# A valid cocotb test
@cocotb.test
async def test(dut):
pass
# Also a valid cocotb test
@cocotb.test()
async def test(dut):
pass
Concurrent and sequential execution
An await
will run an async
coroutine and wait for it to complete.
The called coroutine “blocks” the execution of the current coroutine.
Wrapping the call in start()
or start_soon()
runs the coroutine concurrently,
allowing the current coroutine to continue executing.
At any time you can await
the result of a Task
,
which will block the current coroutine’s execution until the task finishes.
The following example shows these in action:
# A coroutine
async def reset_dut(reset_n, duration_ns):
reset_n.value = 0
await Timer(duration_ns, units="ns")
reset_n.value = 1
reset_n._log.debug("Reset complete")
@cocotb.test()
async def parallel_example(dut):
reset_n = dut.reset
# Execution will block until reset_dut has completed
await reset_dut(reset_n, 500)
dut._log.debug("After reset")
# Run reset_dut concurrently
reset_thread = cocotb.start_soon(reset_dut(reset_n, duration_ns=500))
# This timer will complete before the timer in the concurrently executing "reset_thread"
await Timer(250, units="ns")
dut._log.debug("During reset (reset_n = %s)" % reset_n.value)
# Wait for the other thread to complete
await reset_thread
dut._log.debug("After reset")
See Coroutines and Tasks for more examples of what can be done with coroutines.
Forcing and freezing signals
In addition to regular value assignments (deposits), signals can be forced to a predetermined value or frozen at their current value. To achieve this, the various actions described in Assignment Methods can be used.
# Deposit action
dut.my_signal.value = 12
dut.my_signal.value = Deposit(12) # equivalent syntax
# Force action
dut.my_signal.value = Force(12) # my_signal stays 12 until released
# Release action
dut.my_signal.value = Release() # Reverts any force/freeze assignments
# Freeze action
dut.my_signal.value = Freeze() # my_signal stays at current value until released
Warning
Not all simulators support these features; refer to the Simulator Support section for details or to issues with label “upstream”
Accessing Identifiers Starting with an Underscore or Invalid Python Names
The attribute syntax of dut._some_signal
cannot be used to access
an identifier that starts with an underscore (_
, as is valid in Verilog)
because we reserve such names for cocotb-internals,
thus the access will raise an AttributeError
.
Both SystemVerilog and VHDL allow developers to create signals or nets with non-standard characters by using special syntax. These objects are generally not accessible using attribute syntax since attributes in Python must follow a strict form.
All named objects, including those with the aforementioned limitations, can be accessed using index syntax.
dut["_some_signal"] # begins with underscore
dut["\\!WOOOOW!\\"] # escaped identifier (Verilog), extended identifier (VHDL)
Accessing Verilog Packages
Verilog packages are accessible via cocotb.packages
.
Depending on the simulator, packages may need to be imported in
the compilation unit scope or inside a module in order to be discoverable.
Also note, the $unit
pseudo-package is implemented differently between simulators.
It may appear as one or more attributes here depending on the number of compilation units.
package my_package;
parameter int foo = 7
endpackage
# prints "7"
print(cocotb.packages.my_package.foo.value)
Passing and Failing Tests
A cocotb test is considered to have failed if the test coroutine or any running Task
fails an assert
statement.
Below are examples of failing tests.
@cocotb.test()
async def test(dut):
assert 1 > 2, "Testing the obvious"
@cocotb.test()
async def test(dut):
async def fails_test():
assert 1 > 2
cocotb.start_soon(fails_test())
await Timer(10, 'ns')
When a test fails, a stacktrace is printed.
If pytest
is installed and assert
statements are used,
a more informative stacktrace is printed which includes the values that caused the assert
to fail.
For example, see the output for the first test from above.
0.00ns ERROR Test Failed: test (result was AssertionError)
Traceback (most recent call last):
File "test.py", line 3, in test
assert 1 > 2, "Testing the obvious"
AssertionError: Testing the obvious
A cocotb test is considered to have errored if the test coroutine or any running Task
raises an exception that isn’t considered a failure.
Below are examples of erroring tests.
@cocotb.test()
async def test(dut):
await coro_that_does_not_exist() # NameError
@cocotb.test()
async def test(dut):
async def coro_with_an_error():
dut.signal_that_does_not_exist.value = 1 # AttributeError
cocotb.start_soon(coro_with_an_error())
await Timer(10, 'ns')
When a test ends with an error, a stacktrace is printed. For example, see the below output for the first test from above.
0.00ns ERROR Test Failed: test (result was NameError)
Traceback (most recent call last):
File "test.py", line 3, in test
await coro_that_does_not_exist() # NameError
NameError: name 'coro_that_does_not_exist' is not defined
If a test coroutine completes without failing or erroring,
or if the test coroutine or any running Task
raises cocotb.result.TestSuccess
,
the test is considered to have passed.
Below are examples of passing tests.
@cocotb.test():
async def test(dut):
assert 2 > 1 # assertion is correct, then the coroutine ends
@cocotb.test()
async def test(dut):
raise TestSuccess("Reason") # ends test with success early
assert 1 > 2 # this would fail, but it isn't run because the test was ended early
@cocotb.test()
async def test(dut):
async def ends_test_with_pass():
raise TestSuccess("Reason")
cocotb.start_soon(ends_test_with_pass())
await Timer(10, 'ns')
A passing test will print the following output.
0.00ns INFO Test Passed: test
Logging
Cocotb uses the builtin logging
library, with some configuration described in Logging to provide some sensible defaults.
All Task
s have a logging.Logger
,
and can be set to its own logging level.
task = cocotb.start_soon(coro)
task.log.setLevel(logging.DEBUG)
task.log.debug("Running Task!")
The DUT and each hierarchical object can also have individual logging levels set.
When logging HDL objects, beware that _log
is the preferred way to use
logging. This helps minimize the change of name collisions with an HDL log
component with the Python logging functionality.
dut.my_signal._log.info("Setting signal")
dut.my_signal.value = 1