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))