Upgrading to cocotb 2.0#
cocotb 2.0 makes testbenches easier to understand and write. Some of these improvements require updates to existing testbenches. This guide helps you port existing testbenches to cocotb 2.0.
Step-by-step to cocotb 2.0#
The migration to cocotb 2.0 is a two-step process. The first step is a gradual migration that can be performed in small increments as time permits and keeps the testbench fully operational. The second step is a flag-day migration to switch outdated APIs to their new counterparts.
Step 1: Upgrade to cocotb 1.9 and resolve all deprecation warnings#
Many of the new features in cocotb 2.0 were already introduced in cocotb 1.x, while keeping existing functionality in place. Deprecation warnings highlight where functionality is used that will be gone in cocotb 2.0. Resolving all deprecations is therefore the first step in upgrading.
Upgrade to the latest version of cocotb 1.9.
Resolve all deprecation warnings.
With every warning resolved, your code will be better prepared for cocotb 2.0.
Step 2: Move to cocotb 2.0#
After step 1 your testbenches are ready for the final migration to cocotb 2.0.
Start from a known-good state. Ensure all tests are passing, the logic is stable, and all code is committed.
Upgrade to cocotb 2.0.
Run the testbench to see where it fails. Replace outdated constructs as needed. Rinse and repeat.
Your testbenches are now running on cocotb 2.0!
Continue reading on this page for common migration steps. Also have a look at the Release Notes for cocotb 2.0 and the linked GitHub pull requests or issues, which often also include changes to the cocotb tests that show the before and after of a change.
You might see some new deprecation warnings in your code after the upgrade. As in step one, address those at your convenience (the sooner the better, of course).
If you get lost or have questions, reach out through one of our support channels, we’re happy to help!
Use cocotb.start_soon() instead of cocotb.fork()#
Change#
cocotb.fork() was removed and replaced with cocotb.start_soon().
How to Upgrade#
Replace all instances of
cocotb.fork()withcocotb.start_soon().Run tests to check for any changes in behavior.
cocotb.fork()#task = cocotb.fork(drive_clk())
cocotb.start_soon()#task = cocotb.start_soon(drive_clk())
Rationale#
cocotb.fork() would turn coroutines into Tasks that would run concurrently to the current task.
However, it would immediately run the coroutine until the first await was seen.
This made the scheduler re-entrant and caused a series of hard to diagnose bugs
and required extra state/sanity checking leading to runtime overhead.
For these reasons cocotb.fork() was deprecated in cocotb 1.7 and replaced with cocotb.start_soon().
cocotb.start_soon() does not start the coroutine immediately, but rather “soon”,
preventing scheduler re-entrancy and sidestepping an entire class of bugs and runtime overhead.
The cocotb blog post on this change
is very illustrative of how cocotb.start_soon() and cocotb.fork() are different.
Additional Details#
Coroutines run immediately#
There is a slight change in behavior due to cocotb.start_soon() not running the given coroutine immediately.
This will not matter in most cases, but cases where it does matter are difficult to spot.
If you have a coroutine (the parent) which cocotb.fork()s another coroutine (the child)
and expects the child coroutine to run to a point before allowing the parent to continue running,
you will have to add additional code to ensure that happens.
In general, the easiest way to fix this is to add an await NullTrigger() after the call to cocotb.start_soon().
async def hello_world():
cocotb.log.info("Hello, world!")
cocotb.fork()#cocotb.fork(hello_world())
# "Hello, world!"
cocotb.start_soon()#cocotb.start_soon(hello_world())
# No print...
await NullTrigger()
# "Hello, world!"
One caveat of this approach is that NullTrigger also allows every other scheduled coroutine to run as well.
But this should generally not be an issue.
If you require the “runs immediately” behavior of cocotb.fork(),
but are not calling it from a coroutine function,
update the function to be a coroutine function and add an await NullTrigger, if possible.
Otherwise, more serious refactorings will be necessary.
Exceptions before the first await#
Also worth noting is that with cocotb.fork(), if there was an exception before the first await,
that exception would be thrown back to the caller of cocotb.fork() and the Task object would not be successfully constructed.
async def has_exception():
if variable_does_not_exist: # throws NameError
await Timer(1, 'ns')
cocotb.fork()#try:
task = cocotb.fork(has_exception()) # NameError comes out here
except NameError:
cocotb.log.info("Got expected NameError!")
# no task object exists
cocotb.start_soon()#task = cocotb.start_soon(has_exception())
# no exception here
try:
await task # NameError comes out here
except NameError:
cocotb.log.info("Got expected NameError!")
Move away from @cocotb.coroutine#
Change#
Support for generator-based coroutines using the @cocotb.coroutine decorator
with Python generator functions was removed.
How to Upgrade#
Remove the
@cocotb.coroutinedecorator.Add
asynckeyword directly before thedefkeyword in the function definition.Replace any
yield [triggers, ...]withawait First(triggers, ...).Replace all
yields in the function withawaits.Remove all imports of the
@cocotb.coroutinedecorator
@cocotb.coroutine#@cocotb.coroutine
def my_driver():
yield [RisingEdge(dut.clk), FallingEdge(dut.areset_n)]
yield Timer(random.randint(10), 'ns')
async/await#async def my_driver(): # async instead of @cocotb.coroutine
await First(RisingEdge(dut.clk), FallingEdge(dut.areset_n)) # await First() instead of yield [...]
await Timer(random.randint(10), 'ns') # await instead of yield
Rationale#
These existed to support defining coroutines in Python 2 and early versions of Python 3 before coroutine functions
using the async/await syntax was added in Python 3.5.
We no longer support versions of Python that don’t support async/await.
Python coroutines are noticeably faster than @cocotb.coroutine’s implementation,
and the behavior of @cocotb.coroutine would have had to be changed to support changes to the scheduler.
For all those reasons the @cocotb.coroutine decorator and generator-based coroutine support was removed.
Use LogicArray instead of BinaryValue#
Change#
BinaryValue and BinaryRepresentation were removed
and replaced with the existing Logic and LogicArray.
How to Upgrade#
Change all constructions of BinaryValue to LogicArray.
Replace construction from int with LogicArray.from_unsigned() or LogicArray.from_signed().
Replace construction from bytes with LogicArray.from_bytes() and pass the appropriate byteorder argument.
BinaryValue#BinaryValue(10, 10)
BinaryValue("1010", n_bits=4)
BinaryValue(-10, 8, binaryRepresentation=BinaryRepresentation.SIGNED)
BinaryValue(b"1234", bigEndian=True)
LogicArray#LogicArray.from_unsigned(10, 10)
LogicArray("1010")
LogicArray.from_signed(-10, 8)
BinaryValue.from_bytes(b"1234", byteorder="big")
Replace usage of BinaryValue.integer and
BinaryValue.signed_integer
with LogicArray.to_unsigned() or LogicArray.to_signed(), respectively.
Replace usage of BinaryValue.binstr
with the str cast (this works with BinaryValue as well).
Replace conversion to bytes with LogicArray.to_bytes() and pass the appropriate byteorder argument.
BinaryValue#val = BinaryValue(10, 4)
assert val.integer == 10
assert val.signed_integer == -6
assert val.binstr == "1010"
assert val.buff == b"\x0a"
LogicArray#val = LogicArray(10, 4)
assert val.to_unsigned() == 10
assert val.to_signed() == -6
assert str(val) == "1010"
assert val.to_bytes(byteorder="big") == b"\x0a"
Remove setting of the BinaryValue.big_endian attribute to change endianness.
BinaryValue#val = BinaryValue(b"12", bigEndian=True)
assert val.buff == b"12"
val.big_endian = False
assert val.buff == b"21"
LogicArray#val = LogicArray.from_bytes(b"12", byteorder="big")
assert val.to_bytes(byteorder="big") == b"12"
assert val.to_bytes(byteorder="little") == b"21"
Convert all objects to an unsigned int before doing any arithmetic operation,
such as +, -, /, //, %, **, - (unary), + (unary), abs(value), >>, or <<.
BinaryValue#val = BinaryValue(12, 8)
assert 8 * val == 96
assert val << 2 == 48
assert val / 6 == 2.0
assert -val == -12
# inplace modification
val *= 3
assert val == 36
LogicArray#val = LogicArray(12, 8)
val_int = b.to_unsigned()
assert 8 * val_int == 96
assert val_int << 2 == 48
assert val_int / 6 == 2.0
assert -val_int == -12
# inplace modification
val[:] = val_int * 3
assert val == 36
Change bit indexing and slicing to use the indexing provided by the range argument to the constructor.
Note
Passing an int as the range argument will default the range to Range(range-1, "downto", 0).
This means index 0 will be the rightmost bit and not the leftmost bit like in BinaryValue.
Pass Range(0, range-1) when constructing LogicArray to retain the old indexing scheme, or update the indexing and slicing usage.
BinaryValue#val = BinaryValue(10, 4)
assert val[0] == 1
assert val[3] == 0
LogicArray, specifying an ascending range#val = LogicArray(10, Range(0, 3))
assert val[0] == 1
assert val[3] == 0
LogicArray, changing indexing#val = LogicArray(10, 4)
assert val[3] == 1
assert val[0] == 0
See Update Indexing for cocotb 2.0 for more details on how to update indexing and slicing.
Change all uses of the LogicArray.binstr, LogicArray.integer, LogicArray.signed_integer, and LogicArray.buff setters,
as well as calls to BinaryValue.assign(), to use LogicArray’s setitem syntax.
BinaryValue#val = BinaryValue(10, 8)
val.binstr = "00001111"
val.integer = 0b11
val.signed_integer = -123
val.buff = b"a"
LogicArray#val = LogicArray(10, 8)
val[:] = "00001111"
val[:] = LogicArray.from_unsigned(3, 8)
# or
val[:] = 0b00000011
val[:] = LogicArray.from_signed(-123, 8)
val[:] = LogicArray.from_bytes(b"a", byteorder="big")
Note
Alternatively, don’t modify the whole value in place, but instead modify the variable with a new value.
Change expected type of single indexes to Logic and slices to LogicArray.
BinaryValue#val = BinaryValue(10, 4)
assert isinstance(val[0], BinaryValue)
assert isinstance(val[0:3], BinaryValue)
LogicArray#val = LogicArray(10, 4)
assert isinstance(val[0], Logic)
assert isinstance(val[0:3], LogicArray)
Note
Logic supports usage in conditional expressions (e.g. if val: ...),
equality with str, bool, or int,
and casting to str, bool, or int;
so many behaviors overlap with LogicArray
or how these values would be used previously with BinaryValue.
Note
This also implies a change to type annotations.
Rationale#
In many cases BinaryValue would behave in unexpected ways that were often reported as errors.
These unexpected behaviors were either an unfortunate product of its design or done purposefully.
They could not necessarily be “fixed” and any fix would invariably break the API.
So rather than attempt to fix it, it was outright replaced.
Unfortunately, a gradual change is not possible with such core functionality,
so it was replaced in one step.
Additional Details#
There are some behaviors of BinaryValue that are not supported anymore.
They were deliberately not added to LogicArray because they were unnecessary, unintuitive, or had bugs.
Dynamic-sized BinaryValues#
The above examples all pass the n_bits argument to the BinaryValue constructor.
However, it is possible to construct a BinaryValue without a set size.
Doing so would allow the size of the BinaryValue to change whenever the value was set.
LogicArrays are fixed size.
Instead of modifying the LogicArray in-place with a different sized value,
modify the variable holding the LogicArray to point to a different value.
BinaryValue#LogicArray#val = LogicArray(0, 0) # must provide size!
assert len(val) == 0
val = LogicArray("1100")
assert len(val) == 4
val = LogicArray.from_signed(100, 8) # must provide size!
assert len(val) == 8
Assigning with partial values and “bit-endianness”#
Previously, when modifying a BinaryValue in-place using BinaryValue.assign
or the BinaryValue.buff,
BinaryValue.binstr,
BinaryValue.integer,
or BinaryValue.signed_integer setters,
if the provided value was smaller than the BinaryValue,
the value would be zero-extended based on the endianness of BinaryValue.
LogicArray has no concept of “bit-endianness” as the indexing scheme is arbitrary.
When partially setting a LogicArray, you are expected to explicitly provide the slice you want to set,
and it must match the size of the value it’s being set with.
BinaryValue#b = BinaryValue(0, 4, bigEndian=True)
b.binstr = "1"
assert b == "1000"
b.integer = 2
assert b == "1000" # Surprise!
c = BinaryValue(0, 4, bigEndian=False)
c.binstr = "1"
assert c == "0001"
c.integer = 2
assert c == "0010"
LogicArray#Note
LogicArray supports setting its value with the deprecated LogicArray.buff,
LogicArray.binstr, LogicArray.integer and LogicArray.signed_integer setters,
but assumes the value matches the width of the whole LogicArray.
Values that are too big or too small will result in a ValueError.
Implicit truncation#
Conversely, when modifying a BinaryValue in-place,
if the provided value is too large, it would be implicitly truncated and issue a RuntimeWarning.
In certain circumstances, the RuntimeWarning wouldn’t be issued.
LogicArray, as stated in the previous section,
requires the user to provide a value the same size as the slice to be set.
Failure to do so will result in a ValueError.
BinaryValue#b = BinaryValue(0, 4, bigEndian=True)
b.binstr = "00001111"
# RuntimeWarning: 4-bit value requested, truncating value '00001111' (8 bits) to '1111'
assert b == "1111"
b.integer = 100
# RuntimeWarning: 4-bit value requested, truncating value '1100100' (7 bits) to '0100'
assert b == "0100"
c = BinaryValue(0, 4, bigEndian=False)
c.binstr = "00001111"
# No RuntimeWarning?
assert c == "1111" # Surprise!
c.integer = 100
# RuntimeWarning: 4-bit value requested, truncating value '1100100' (7 bits) to '110'
assert c == "110" # ???
LogicArray#val = LogicArray(0, 4)
# val[:] = "00001111" # ValueError: Value of length 8 will not fit in Range(3, 'downto', 0)
# val[:] = 100 # ValueError: 100 will not fit in a LogicArray with bounds: Range(3, 'downto', 0)
val[3:0] = "00001111"[:4]
assert val == "0000"
val[3:0] = LogicArray.from_unsigned(100, 8)[3:0]
assert val == "0100"
Note
LogicArray supports setting its value with the deprecated LogicArray.buff,
LogicArray.binstr, LogicArray.integer and LogicArray.signed_integer setters,
but assumes the value matches the width of the whole LogicArray.
Values that are too big or too small will result in a ValueError.
Integer representation#
BinaryValue could be constructed with a binaryRepresentation argument of the type BinaryRepresentation
which would select how that BinaryValue would interpret any integer being used to set its value.
BinaryValue.assign
and the BinaryValue.integer
and BinaryValue.signed_integer setters all behaved the same when given an integer.
Unlike endianness, this could not be changed after construction (setting BinaryValue.binaryRepresentation has no effect).
LogicArray does not have a concept of integer representation as a part of its value,
its value is just an array of Logic.
Integer representation is provided when converting to and from an integer.
Note
LogicArray interfaces that can take integers are expected to take them as “bit array literals”, e.g. 0b110101 or 0xFACE.
That is, they are interpreted as if they are unsigned integer values.
Note
LogicArray supports setting its value with the deprecated LogicArray.integer and LogicArray.signed_integer setters,
but assumes an unsigned and two’s complement representation, respectively.
Expect LogicArray and Logic instead of BinaryValue when getting values from logic-like simulator objects#
Change#
Handles to logic-like simulator objects now return LogicArray or Logic instead of BinaryValue
when getting their value with the value getter.
Scalar logic-like simulator objects return Logic, and array-of-logic-like simulator objects return LogicArray.
How to Upgrade#
Use LogicArray instead of BinaryValue when dealing with array-of-logic-like simulator objects.
Change code when dealing with scalar logic-like simulator objects to work with Logic.
Change indexing assumptions from always being 0 to length-1 left-to-right, to following the arbitrary indexing scheme of the logic array as defined in HDL.
See Update Indexing for cocotb 2.0 for more details on how to update indexing and slicing.
Rationale#
See the rationale for switching from BinaryValue to LogicArray.
The change to use the HDL indexing scheme makes usage more intuitive for new users and eases debugging.
Scalars and arrays of logic handles were split into two types with different value types so that handles to arrays could support
getting length and indexing information: LogicArrayObject.left(), LogicArrayObject.right(), LogicArrayObject.direction(), and LogicArrayObject.range(),
which cause errors when applied to scalar objects.
Additional Details#
Operations such as equality with and conversion to int, str, and bool,
as well as getting the length,
work the same for Logic, LogicArray, and BinaryValue.
This was done deliberately to reduce the number of changes required,
while also providing a common API to enable writing backwards-compatible and higher-order APIs.
BinaryValue in cocotb 1.x and both Logic and LogicArray in cocotb 2.x.#assert len(dut.signal.value) == 1
assert dut.signal.value == 1
assert dut.signal.value == "1"
assert dut.signal.value == True
assert int(dut.signal.value) == 1
assert str(dut.signal.value) == "1"
assert bool(dut.signal.value) is True
dut.signal.value = 1
dut.signal.value = "1"
dut.signal.value = True
Expect Array instead of list when getting values from arrayed simulator objects#
Change#
Handles to arrayed simulator objects now return Array instead of list when getting values with the value getter.
How to Upgrade#
Change indexing assumptions from always being 0 to length-1 left-to-right, to following the arbitrary indexing scheme of the array as defined in HDL.
See Update Indexing for cocotb 2.0 for more details on how to update indexing and slicing.
Rationale#
Array provides most features of list besides the fact that it is immutable in size and uses arbitrary indexing, like LogicArray.
The change to use the HDL indexing scheme makes usage more intuitive for new users and eases debugging.
Additional Details#
Equality with and conversion to list,
getting the length,
and iteration,
works the same for both the old way and the new way using Array.
This was done deliberately to reduce the number of changes required.
Use @cocotb.parametrize instead of TestFactory#
Change#
cocotb.parametrize was added to replace TestFactory, which was deprecated.
How to Upgrade#
Replace all instances of TestFactory with a @cocotb.parametrize decorator on the function being parameterized.
Replace calls to TestFactory.add_option() with arguments to @cocotb.parametrize.
Remove all calls to TestFactory.generate_tests() and move any arguments to the @cocotb.test decorator.
TestFactory#async def my_test(param_a, param_b):
...
tf = TestFactory(my_test)
tf.add_option("param_a", [1, 2, 3])
tf.add_option("param_b", ["sample", "text"])
tf.generate_tests(timeout_time=10, timeout_unit="us")
@cocotb.parametrize#@cocotb.test(timeout_time=10, timeout_unit="us")
@cocotb.parametrize(
param_a=[1, 2, 3],
param_b=["sample", "text"]
)
async def my_test(param_a, param_b):
...
Note
If you are using the prefix or postfix arguments to TestFactory.generate_tests(),
replace them with the use of the new name argument.
Rationale#
TestFactory was defined separately from the test declaration, hurting readability,
and making it prone to issues such as parameters being out of sync
and @cocotb.test inadvertently being applied to the parameterized function.
Additionally it worked by injecting test objects into the module of the calling scope,
which led to feature creep (stacklevel arg to TestFactory.generate_tests())
and made issues hard to diagnose.
Next, the test names generated were not descriptive and did not lend themselves to being filtered with a regular expression.
Finally, it doesn’t compose well with future test marking features.
@cocotb.parametrize has none of those issues and should be familiar to users of pytest.
Generated test names are descriptive and can be easily filtered with a regular expression.
Move away from Event.data and Event.name#
Change#
The Event.data attribute and data argument to the Event.set() method,
and the Event.name attribute and name argument to the Event constructor were deprecated.
How to Upgrade#
Remove all passing of the name argument to the Event constructor.
Remove all passing of the data argument to the Event.set() method and uses of the Event.data attribute.
Replace with an adjacent variable or class attribute.
Event.data#monitor_event = Event()
... # in monitor
monitor_event.set(b"\xBA\xD0\xCA\xFE")
... # in user code
await monitor_event.wait()
recv = monitor_event.data
monitor_event = Event()
monitor_data = None
... # in monitor
monitor_data = b"\xBA\xD0\xCA\xFE"
monitor_event.set()
... # in user code
await monitor_event.wait()
recv = monitor_data
Rationale#
These features were removed to better align cocotb’s Event with asyncio.Event.
Remove uses of Join and Task.join()#
Change#
Join and Task.join() have been deprecated.
How to Upgrade#
Remove Join and Task.join() and simply await the Task being joined,
or pass the Task directly into the function that will await it.
async def example():
...
task = cocotb.start_soon()
Join#await Join(task)
# or
await First(Join(task), RisingEdge(cocotb.top.clk))
Task.join()#await task.join()
# or
await First(task.join(), RisingEdge(cocotb.top.clk))
Task directly#await task
# or
await First(task, RisingEdge(cocotb.top.clk))
Rationale#
There were multiple synonyms for the same functionality, so we deprecated all but the least verbose.
This also better aligns cocotb’s Tasks with asyncio.Task.
Additional Details#
awaiting a Task (or previously a Join trigger) returns the result of the Task, not the task/trigger.
This can make it difficult to determine which Task finished when waiting on multiple, such as with a First trigger.
TaskComplete was added to solve this.
Use Task.complete to get the TaskComplete trigger associated with each Task.
TaskComplete to disambiguate which Task finished.#async def coro1():
await Timer(1, 'us')
async def coro2():
await RisingEdge(cocotb.top.done)
task1 = cocotb.start_soon(coro1())
task2 = cocotb.start_soon(coro2())
# res = await First(task1, task2)
# res is `None` is either case, which fired?
res = await First(task1.complete, task2.complete)
if res is task1.complete:
cocotb.log.info("Reached deadline!")
else:
cocotb.log.info("Processing finished!")
Replace Task.kill() with Task.cancel()#
Change#
Task.kill() was replaced with Task.cancel().
How to Upgrade#
Replace calls of Task.kill() with Task.cancel().
Task.kill()#task.kill()
Task.cancel()#task.cancel()
Rationale#
Task.kill() stopped a Task from executing immediately,
which required re-entering the scheduler from a user context.
Scheduler re-entrance has been the cause of several hard to diagnose bugs.
Task.cancel() schedules stopping a Task,
preventing scheduler re-entrancy and sidestepping an entire class of bugs.
Additionally, Task.cancel() throws a CancelledError into the task during cancellation,
allowing cleanup code to be executed, which was not the case with Task.kill().
Additional Details#
Tasks don’t become “done” immediately#
There is a slight change in behavior when moving to Task.cancel() since it schedules a cancellation.
Tasks don’t become “done” immediately, but only after control is returned to the scheduler.
Task.kill()#task.kill()
assert task.done()
Task.cancel()#task.cancel()
assert not task.done()
await NullTrigger()
assert task.done()
This can cause a change in scheduling when awaiting a Task to finish if it is cancelled instead of killed.
CancelledError is thrown into cancelled Tasks#
When a Task is cancelled by calling Task.cancel(),
a asyncio.CancelledError is thrown into the task.
This ensures cleanup is performed.
context managers doing cleanup on exit and any finally blocks in your tasks
will now be executed when the task is cancelled.
Otherwise, the exception will bubble up through your code and be handled appropriately by Task internals,
so there is nothing else you have to do.
All Tasks created with cocotb.start_soon(), cocotb.start(), or cocotb.create_task()
that are still running at the end of the test will be cancelled to ensure end-of-test cleanup.
CancelledError ensures cleanup#async def stimulus():
try:
with open("transactions.log", "w") as f:
transaction = 123
f.write(transaction)
# The context manager exited here, so the file was closed. This happens
# whether it exited normally, threw an exception, the task was cancelled,
# or the test ended.
finally:
# The finally block is always executed. This happens whether the try block
# exited normally, an exception was thrown, the task was cancelled, or the test ended.
cocotb.log.info("Ending stimulus!")
CancelledError thrown during cancellation cannot be squashed#
Unlike asyncio, task cancellation cannot be ignored.
When a test ends all tasks must be cancelled,
otherwise they would continue running into the next test.
To ensure that users don’t inadvertently ignore cancellation,
cocotb forces the task to end with RuntimeError if it detects a CancelledError is squashed.
When upgrading code to 2.0, there may be existing code which causes this condition to fire.
The most common offender is user code catching BaseException.
CancelledError#async def example():
try:
...
except BaseException as e:
cocotb.log.info("Saw error!", exc_info=e)
BaseException is generally reserved for runtime implementation details.
Instead catch only Exception.
Exception#async def example():
try:
...
except Exception as e:
cocotb.log.info("Saw error!", exc_info=e)
If narrowing your exception handling to Exception isn’t possible,
explicitly catch CancelledError and re-raise it in an except clause placed before the BaseException clause.
CancelledError#async def example():
try:
...
except asyncio.CancelledError:
raise
except BaseException as e:
cocotb.log.info("Saw error!", exc_info=e)
Replace cocotb.start() with cocotb.start_soon()#
Change#
cocotb.start() was deprecated.
How to Upgrade#
Replace cocotb.start() with cocotb.start_soon() and remove the await before it.
cocotb.start()#async def my_coro():
... # do stuff
my_task = await cocotb.start(my_coro())
cocotb.start_soon()#async def my_coro():
... # do stuff
my_task = cocotb.start_soon(my_coro())
Note
If you need the started Task to run immediately rather than soon,
await a NullTrigger immediately after calling cocotb.start_soon().
This is not common.
NullTrigger to force a new Task to run# async def my_coro():
... # do stuff
my_task = cocotb.start_soon(my_coro())
# my_task isn't running
await NullTrigger()
# my_task is now running
Rationale#
Many new users were confused on when to use one vs the other,
so cocotb.start() will be removed to prevent any confusion.
Operate on full values of packed structs and packed arrays#
Change#
Accessing fields of packed structs and elements of packed arrays was removed. Packed objects now act as a single composite logic vector.
How to Upgrade#
Instead of accessing fields of packed structs or elements of packed arrays,
get the full value of the object and index into the resulting LogicArray.
struct packed {
logic [3:0] a;
logic [7:0] b;
} my_struct;
logic my_array [0:3];
When trying to get the value of a packed struct field or packed array element: 1. Read the whole packed object value. 2. Slice it to get the field you are interested in.
_ = dut.my_struct.a.value
_ = dut.my_array[2].value
def get_packed_field(handle, start, stop=None):
full_value = handle.value
if stop is None:
return full_value[start]
else:
return full_value[start:stop]
get_packed_field(dut.my_struct, 11, 8)
get_packed_field(dut.my_array, 2)
When setting the value of a packed object: 1. Read the whole packed value. 2. Set the bits corresponding to the field you want to change. 3. Write the whole modified packed value back.
dut.my_struct.a.value = 0b1010
dut.my_array[2].value = 1
def set_packed_field(handle, start, stop=None, *, value):
full_value = handle.value
if stop is None:
full_value[start] = value
else:
full_value[start:stop] = value
handle.value = full_value
set_packed_field(dut.my_struct, 11, 8, value=0b1010)
set_packed_field(dut.my_array, 2, value=1)
When waiting for a value change on a packed object: 1. Get the current value of the field you care about. 2. Wait for a value change on the whole packed object. 3. Read the whole packed object and slice it to get the field you are interested in. 4. Compare it against the saved value from step 1 to see if the field changed.
await ValueChange(dut.my_struct.a)
await ValueChange(dut.my_array[2])
async def value_change_packed_field(handle, start, stop=None):
value_changed = False
while not value_changed:
old_value = get_packed_field(handle, start, stop)
await ValueChange(handle)
new_value = get_packed_field(handle, start, stop)
value_changed = new_value != old_value
await value_change_packed_field(dut.my_struct, 11, 8)
await value_change_packed_field(dut.my_array, 2)
Note
A library like packtype could help define Python types that mimic HDL packed structures and arrays, allowing you to use struct field names and array indexes rather than bit offsets.
Note
Alternatively, you might be able to remove the Verilog packed specifier.
Rationale#
Accessing packed struct fields or packed array elements was not well supported across all simulators. Even in simulators that appeared to support it, there were many edge cases that didn’t work as expected. Finally, this functionality leveraged VPI calls which appeared to violate the VPI spec. To prevent users from running into these issues, this feature was removed.
Replace handle.setimmediatevalue(value) with handle.set(Immediate(value))#
Change#
handle.setimmediatevalue() was deprecated.
How To Upgrade#
Replace the call to handle.setimmediatevalue() with a value set using the Immediate() action wrapper.
handle.setimmediatevalue(value)#cocotb.top.iface.valid.setimmediatevalue(0)
Rationale#
handle.setimmediatevalue() does not actually set the object’s value immediately.
It sets the value inertially, but immediately,
without it being scheduled for the next ReadWrite sync phase.
This means signals get their value at delta N+1 (N being where you currently are in the time step).
This is unexpected and generally not useful.
Immediate applies values immediately, such that they can be read back after writing.
Use cocotb.pass_test() instead of raising TestSuccess#
Change#
TestSuccess was deprecated and replaced with cocotb.pass_test().
How To Upgrade#
Replace raise TestSuccess(msg) with a call to cocotb.pass_test().
TestSuccess#if cocotb.top.error.value == 0:
raise TestSuccess("Test finished without DUT erroring")
cocotb.pass_test()#if cocotb.top.error.value == 0:
cocotb.pass_test("Test finished without DUT erroring")
Rationale#
cocotb needs a way to end a test with a pass from any Task, not just the main test Task.
TestSuccess was created for that purpose.
But being an exception, it has an additional implied interface:
It is acceptable to catch it.
It will propagate through Tasks like other exceptions do.
However, neither of these implied behaviors is actually supported.
cocotb.pass_test() being a function call avoids these implicit interfaces.
It also parallels the design of sys.exit().
Fail tests using assert rather than TestFailure#
Change#
TestFailure was removed.
How to Upgrade#
Replace raise TestFailure(msg) with an assert statement.
Move any exception message to the optional message clause of the assert statement.
TestFailure#if cocotb.top.error.value != 0:
raise TestFailure("DUT errored")
assert#assert cocotb.top.error.value == 0, "DUT errored"
Rationale#
cocotb had far too many competing ways to fail a test, so they were reduced to one.
assert is already familiar to those who have ever used pytest,
and pytest functionality can be leveraged to rewrite the AssertionErrors coming from failing asserts
with more contextual information as to why the assert failed.
Replace uses of TestError#
Change#
TestError was removed.
How to Upgrade#
Replace TestError with a more appropriate exception type.
TestError#def drive(pkt):
if not isinstance(pkt, bytes):
raise TestError("packet must be bytes")
...
def drive(pkt):
if not isinstance(pkt, bytes):
raise TypeError("packet must be bytes")
...
Rationale#
TestError is unnecessarily cocotb-specific and another redundant way to fail the test with an error,
so it was removed.
Use sources argument in Runner.build() instead of vhdl_sources and verilog_sources#
Change#
The vhdl_sources and verilog_sources arguments to Runner.build() have been deprecated and replaced with the sources argument.
How to Upgrade#
Instead of splitting your sources between the vhdl_sources and verilog_sources arguments,
pass all sources via the sources argument.
Sources will be compiled in the given order, left-to-right.
vhdl_sources and verilog_sources#runner = get_runner()
runner.build(
vhdl_sources=["top.vhdl"],
verilog_sources=["ip_core.sv"],
)
sources#runner = get_runner()
runner.build(
sources=["ip_core.sv", "top.vhdl"],
)
Note
If you are unsure about the order the sources should be compiled in with mixed-language simulators,
prefer compiling all VHDL sources, then all Verilog sources.
This is the order of compilation when using separate vhdl_sources and verilog_sources arguments.
Rationale#
Splitting the two types of sources made arbitrary build ordering of VHDL and Verilog sources impossible. This approach also reduces verbosity and hidden assumptions about compilation order.
Additional Details#
Source files are judged to be either VHDL or Verilog sources and compiled appropriately for your simulator.
The runner uses the file extension to determine whether the source file is VHDL or Verilog
(see Runner.build() for details).
If you use custom file extensions,
you can use the VHDL and Verilog tag classes
to mark the sources as either VHDL or Verilog, respectively.
runner = get_runner()
runner.build(
sources=[Verilog("ip_core.gen"), "top.vhdl"],
)
Use cocotb.log over loggers on handles, tasks, and triggers#
Change#
handle._log,
Task.log,
and Trigger.log were made private.
All loggers in the "cocotb" namespace were reserved for internal use only.
cocotb.log was changed to be the "test" logger.
How To Upgrade#
Replace all uses of the loggers on cocotb objects with cocotb.log.
Use the repr() of the object in the message if desired.
"cocotb" namespace loggers.#cocotb.top.signal._log.info("logging from a handle")
task = cocotb.start_soon(example())
task.log.info("logging from a task")
trigger = Timer(10, 'ns')
trigger.log.info("logging from a trigger")
cocotb.log#cocotb.log.info("%r: logging from a handle", cocotb.top.signal)
task = cocotb.start_soon(example())
cocotb.log.info("%r: logging from a task", task)
trigger = Timer(10, 'ns')
cocotb.log.info("%r: logging from a trigger", trigger)
Rationale#
The "cocotb" logging namespace was reserved for cocotb internal use
so that it can offer users better control over the verbosity of those messages.
As a result, the cocotb.log logger name was changed from "cocotb"
and all loggers on cocotb internal objects were made private (these loggers were in the "cocotb" namespace).
Use handle.is_const() instead of ConstantObject#
Change#
ConstantObject was removed.
handle.is_const was added to replace it.
How to Upgrade#
Replace uses of isinstance(handle, ConstantObject) as a way to check the constant-ness of a simulator object
with checking the handle’s is_const attribute.
ConstantObject#if isinstance(cocotb.top.PARAM, ConstantObject):
...
is_const#if cocotb.top.PARAM.is_const:
...
Rationale#
ConstantObject was a single type that serviced all constant objects regardless of their data type.
This made it fragile to users getting the data type incorrect and values being applied incorrectly or causing crashes.
Now constant objects are mapped to the appropriate handle type and these issues can’t occur.
Use getitem syntax instead of handle._id()#
Change#
handle._id() was deprecated.
How to Upgrade#
Replace calls to handle._id() with handle["child"] syntax.
If the extended argument is True, ensure to add a \ character before and after the name.
handle._id()#cocotb.top._id("!special-name", extended=True)
cocotb.top._id("_name_starts_with_underscore", extended=False)
cocotb.top["\\!special-name\\"]
cocotb.top["_name_starts_with_underscore"]
Rationale#
The getitem syntax is a more Pythonic way to index into maps, such as named collections of child simulator objects.