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 Task
s 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.coroutine
decorator.Add
async
keyword directly before thedef
keyword in the function definition.Replace any
yield [triggers, ...]
withawait First(triggers, ...)
.Replace all
yield
s in the function withawait
s.Remove all imports of the
@cocotb.coroutine
decorator
@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.
Change all negative indexing to use positive indexing.
BinaryValue
#val = BinaryValue(10, 4)
assert val[0] == 1
assert val[3] == 0
assert val[-2] == 1
LogicArray
, specifying an ascending range#LogicArray
, changing indexing#val = LogicArray(10, 4)
assert val[3] == 1
assert val[0] == 0
assert val[1] == 1
Note
You can also use the LogicArray.range
object to translate 0
to len()-1
indexing to the one used by LogicArray
,
but this is rather inefficient.
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 BinaryValue
s#
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.
LogicArray
s 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 logic-like simulator objects return LogicArray
.
How to Upgrade#
Use LogicArray instead of BinaryValue when dealing with array logic-like simulator objects.
Also, 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.
logic [7:0] signal;
BinaryValue
s#dut.signal.value = "1010"
...
val = dut.signal.value
assert val[0] == 1 # always left-most
LogicArray
s#dut.signal.value = "1010"
...
val = dut.signal.value
assert val[0] == 0 # uses HDL indexing, index 0 is right-most
Change code when dealing with scalar logic-like simulator objects to work with Logic
.
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.
For example, if the HDL defines an array with the indexing scheme [15:0]
, index 15
will be the left-most element of the array rather than the right-most.
integer array[1:-2];
list
s#dut.array.value = [1, 2, 3, 4]
...
val = dut.array.value
assert val[0] == 1 # always left-most
Array
s#dut.array.value = [1, 2, 3, 4]
...
val = dut.array.value
assert val[0] == 2 # uses HDL indexing, index 0 is second element
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.