In cocotb 2.0, one big change is the new Logic
and LogicArray
to replace BinaryValue
. It’s now easier and more consistent to handle values. I have been using BinaryValue
(and now LogicArray
) as standalone utilities. So, it is always nice to understand how things work (or change).
BinaryValue Link to heading
Pre-2.0 BinaryValue
was used to represent and operate on values. You can convert, assign, and convert between integer, binary string, and hex.
from cocotb.binary import BinaryValue
bv = BinaryValue(value=0xA5, n_bits=8)
await Timer(1, 'ns')
# Test different representations
print(f"Hex value: {bv.hex} {type(bv.hex)}")
print(f"Binary value: {bv.binstr} {type(bv.binstr)}")
print(f"Integer value: {bv.integer} {type(bv.integer)}")
# Test different base assignments
bv.binstr = "10101010"
await Timer(1, 'ns')
print(f"After binary assignment: {bv.hex}")
bv.hex = 0xf
await Timer(1, 'ns')
print(f"After hex assignment: {bv.hex}")
Hex value: <bound method BinaryValue.hex of 10100101>
Binary value: 10100101
Integer value: 165
After binary assignment: <bound method BinaryValue.hex of 10101010>
After hex assignment: FF
And if you print the actual type of the variable and type of value, you will get ModifiableObject
and BinaryValue
.
print(type(dut.a))
print(type(dut.a.value))
<class 'cocotb.handle.ModifiableObject'>
<class 'cocotb.binary.BinaryValue'>
Logic and LogicArray Link to heading
Now when you print the types, you will get LogicArrayObject
and LogicArray
.
print(type(dut.a))
print(type(dut.a.value))
<class 'cocotb.handle.LogicArrayObject'>
<class 'cocotb.types.logic_array.LogicArray'>
Also, it’s simpler to deal with Logic and LogicArray as they can handle Python standard functions like hex, bin, etc.
from cocotb.types.logic import Logic
from cocotb.types.logic_array import LogicArray
la = LogicArray("100")
print(la)
print(F"list :{list(la)}")
print(f"str(la): {str(la)}")
print(f"int(la): {int(la)}")
print(f"hex(la): {str(la)}")
print(f"bin(la): {bin(la)}")
print(la.to_bytes(byteorder="big"))
print(la.to_unsigned())
print(la.to_signed())
100
list :[Logic('1'), Logic('0'), Logic('0')]
str(la): 100
int(la): 4
hex(la): 100
bin(la): 0b100
b'\x04'
4
-4
Deep dive Link to heading
class Logic:
r"""
Model of a 9-value (``U``, ``X``, ``0``, ``1``, ``Z``, ``W``, ``L``, ``H``, ``-``) datatype commonly seen in VHDL.
"""
The bitwise operations are cleverly implemented with lookup tables such as this.
def __and__(self, other: "Logic") -> "Logic":
if not isinstance(other, Logic):
return NotImplemented
return Logic(
(
# -----------------------------------------------------
# U X 0 1 Z W L H - | |
# -----------------------------------------------------
("U", "U", "0", "U", "U", "U", "0", "U", "U"), # | U |
("U", "X", "0", "X", "X", "X", "0", "X", "X"), # | X |
("0", "0", "0", "0", "0", "0", "0", "0", "0"), # | 0 |
("U", "X", "0", "1", "X", "X", "0", "1", "X"), # | 1 |
("U", "X", "0", "X", "X", "X", "0", "X", "X"), # | Z |
("U", "X", "0", "X", "X", "X", "0", "X", "X"), # | W |
("0", "0", "0", "0", "0", "0", "0", "0", "0"), # | L |
("U", "X", "0", "1", "X", "X", "0", "1", "X"), # | H |
("U", "X", "0", "X", "X", "X", "0", "X", "X"), # | - |
)[self._repr][other._repr]
)
The standard functions are implemented __str__
, __bool__
and __int__
. Note __int__
covers hex
, bin
besides int
.
def __str__(self) -> str:
return ("U", "X", "0", "1", "Z", "W", "L", "H", "-")[self._repr]
def __bool__(self) -> bool:
if self._repr == _0:
return False
elif self._repr == _1:
return True
raise ValueError(f"Cannot convert {self!r} to bool")
def __int__(self) -> int:
if self._repr == _0:
return 0
elif self._repr == _1:
return 1
raise ValueError(f"Cannot convert {self!r} to int")
LogicArray
is built on top of Logic
but obviously bigger as it has to support more operations.
class LogicArray(ArrayLike[Logic]):
r"""Fixed-sized, arbitrarily-indexed, array of :class:`cocotb.types.Logic`.
"""
In __init__
, it can take str or int values with range as optional.
range = _make_range(range, width)
if isinstance(value, str):
if not (set(value) <= _str_literals):
raise ValueError("Invalid str literal")
self._value_as_str = value.upper()
if range is not None:
if len(value) != len(range):
raise OverflowError(
f"Value of length {len(self._value_as_str)} will not fit in {range}"
)
self._range = range
else:
self._range = Range(len(self._value_as_str) - 1, "downto", 0)
elif isinstance(value, int):
if value < 0:
raise ValueError("Invalid int literal")
if range is None:
raise TypeError("Missing required arguments: 'range' or 'width'")
bitlen = max(1, int.bit_length(value))
if bitlen > len(range):
raise OverflowError(
f"{value!r} will not fit in a LogicArray with bounds: {range!r}."
)
self._value_as_int = value
self._range = range
elif value is None:
if range is None:
raise TypeError("Missing required arguments: 'range' or 'width'")
self._value_as_str = "X" * len(range)
self._range = range
else:
self._value_as_array = [Logic(v) for v in value]
if range is not None:
if len(self._value_as_array) != len(range):
raise OverflowError(
f"Value of length {len(self._value_as_array)} will not fit in {range}"
)
self._range = range
else:
self._range = Range(len(self._value_as_array) - 1, "downto", 0)
Here __str__
and __repr__
are implemented. print(str(la))
and print(la)
give different results.
def __repr__(self) -> str:
return f"{type(self).__qualname__}({str(self)!r}, {self.range!r})"
def __str__(self) -> str:
return self._get_str()
Indexing and int (and hex, bin) functions
def __int__(self) -> int:
return self.to_unsigned()
def __index__(self) -> int:
return int(self)
The binary operations are simple enough but it has to check matching length first.
def __and__(self, other: "LogicArray") -> "LogicArray":
if not isinstance(other, LogicArray):
return NotImplemented
if len(self) != len(other):
raise ValueError(
f"cannot perform bitwise & "
f"between {type(self).__qualname__} of length {len(self)} "
f"and {type(other).__qualname__} of length {len(other)}"
)
return LogicArray(a & b for a, b in zip(self, other))