Source code for cocotb.binary

#!/usr/bin/env python

# Copyright (c) 2013 Potential Ventures Ltd
# Copyright (c) 2013 SolarFlare Communications Inc
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#     * Neither the name of Potential Ventures Ltd,
#       SolarFlare Communications Inc nor the
#       names of its contributors may be used to endorse or promote products
#       derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL POTENTIAL VENTURES LTD BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import os
import random
import re
import warnings
from enum import Enum

from cocotb._deprecation import deprecated

_RESOLVE_TO_0 = "-lL"
_RESOLVE_TO_1 = "hH"
_RESOLVE_TO_CHOICE = "xXzZuUwW"


class _ResolveXToValue(Enum):
    VALUE_ERROR = "VALUE_ERROR"
    ZEROS = "ZEROS"
    ONES = "ONES"
    RANDOM = "RANDOM"


def _resolve_x_to_from_env() -> _ResolveXToValue:
    env_value = os.getenv("COCOTB_RESOLVE_X", "VALUE_ERROR")
    try:
        return _ResolveXToValue(env_value)
    except ValueError:
        raise ValueError(
            "The COCOTB_RESOLVE_X environment variable is set to an unknown "
            f"value: {env_value!r}"
        )


resolve_x_to = _resolve_x_to_from_env()


class _ResolveTable(dict):
    """Translation table class for resolving binary strings.

    For use with :func:`str.translate()`, which indexes into table with Unicode ordinals.
    """

    def __init__(self):
        self.update({ord("0"): ord("0"), ord("1"): ord("1")})
        self.update({ord(k): ord("0") for k in _RESOLVE_TO_0})
        self.update({ord(k): ord("1") for k in _RESOLVE_TO_1})

        # Do not resolve if resolve_x_to is not set to one of the supported values
        def no_resolve(key):
            return key

        self.resolve_x = no_resolve

        if resolve_x_to == _ResolveXToValue.VALUE_ERROR:

            def resolve_error(key):
                raise ValueError(
                    f"Unresolvable bit in binary string: {chr(key)!r}. "
                    "Set the COCOTB_RESOLVE_X environment variable to "
                    "configure how special values are resolved."
                )

            self.resolve_x = resolve_error
        elif resolve_x_to == _ResolveXToValue.ZEROS:
            self.update({ord(k): ord("0") for k in _RESOLVE_TO_CHOICE})
        elif resolve_x_to == _ResolveXToValue.ONES:
            self.update({ord(k): ord("1") for k in _RESOLVE_TO_CHOICE})
        elif resolve_x_to == _ResolveXToValue.RANDOM:

            def resolve_random(key):
                # convert to correct Unicode ordinal:
                # ord('0') = 48
                # ord('1') = 49
                return random.getrandbits(1) + 48

            self.resolve_x = resolve_random

        self._resolve_to_choice = {ord(c) for c in _RESOLVE_TO_CHOICE}

    def __missing__(self, key):
        if key in self._resolve_to_choice:
            return self.resolve_x(key)
        else:
            return key


_resolve_table = _ResolveTable()


def resolve(string):
    return string.translate(_resolve_table)


def _clog2(val):
    if val < 0:
        raise ValueError("_clog2 can't take a negative")
    exp = 0
    while True:
        if (1 << exp) >= val:
            return exp
        exp += 1


[docs] class BinaryRepresentation: # noqa UNSIGNED = 0 #: Unsigned format SIGNED_MAGNITUDE = 1 #: Sign and magnitude format TWOS_COMPLEMENT = 2 #: Two's complement format
[docs] class BinaryValue: """Representation of values in binary format. The underlying value can be set or accessed using these aliasing attributes: - :attr:`BinaryValue.integer` is an integer - :attr:`BinaryValue.signed_integer` is a signed integer - :attr:`BinaryValue.binstr` is a string of ``01xXzZ`` - :attr:`BinaryValue.buff` is a binary buffer of bytes - :attr:`BinaryValue.value` is an integer **deprecated** For example: >>> vec = BinaryValue() >>> vec.integer = 42 >>> print(vec.binstr) 101010 >>> print(vec.buff) b'*' """ _permitted_chars = _RESOLVE_TO_0 + _RESOLVE_TO_1 + _RESOLVE_TO_CHOICE + "01" # noqa def __init__( self, value=None, n_bits=None, bigEndian=True, binaryRepresentation=BinaryRepresentation.UNSIGNED, bits=None, ): """ Args: value (str or int or long, optional): Value to assign to the bus. n_bits (int, optional): Number of bits to use for the underlying binary representation. bigEndian (bool, optional): Interpret the binary as big-endian when converting to/from a string buffer. binaryRepresentation (BinaryRepresentation): The representation of the binary value (one of :any:`UNSIGNED`, :any:`SIGNED_MAGNITUDE`, :any:`TWOS_COMPLEMENT`). Defaults to unsigned representation. bits (int, optional): Deprecated: Compatibility wrapper for :attr:`n_bits`. """ self._str = "" self.big_endian = bigEndian self.binaryRepresentation = binaryRepresentation # bits is the deprecated name for n_bits, allow its use for # backward-compat reasons. if bits is not None and n_bits is not None: raise TypeError("You cannot use n_bits and bits at the same time.") if bits is not None: warnings.warn( "The bits argument to BinaryValue has been renamed to n_bits", DeprecationWarning, stacklevel=2, ) n_bits = bits self._n_bits = n_bits self._convert_to = self._convert_to_map[self.binaryRepresentation].__get__( self, self.__class__ ) self._convert_from = self._convert_from_map[self.binaryRepresentation].__get__( self, self.__class__ ) if value is not None: self.assign(value)
[docs] def assign(self, value): """Decides how best to assign the value to the vector. Picks from the type of its argument whether to set :attr:`integer`, :attr:`binstr`, or :attr:`buff`. Args: value (str or int or bytes): The value to assign. .. versionchanged:: 1.4 This no longer falls back to setting :attr:`buff` if a :class:`str` containing any characters that aren't ``0``, ``1``, ``X`` or ``Z`` is used, since :attr:`buff` now accepts only :class:`bytes`. Instead, an error is raised. """ if isinstance(value, int): self.integer = value elif isinstance(value, str): self.binstr = value elif isinstance(value, bytes): self.buff = value else: raise TypeError( "value must be int, str, or bytes, not {!r}".format( type(value).__qualname__ ) )
def _convert_to_unsigned(self, x): if x == 0: return self._adjust_unsigned("") x = bin(x) if x[0] == "-": raise ValueError( "Attempt to assigned negative number to unsigned " "BinaryValue" ) return self._adjust_unsigned(x[2:]) def _convert_to_signed_mag(self, x): if x == 0: return self._adjust_unsigned("") x = bin(x) if x[0] == "-": binstr = self._adjust_signed_mag("1" + x[3:]) else: binstr = self._adjust_signed_mag("0" + x[2:]) if self.big_endian: binstr = binstr[::-1] return binstr def _convert_to_twos_comp(self, x): if x < 0: binstr = bin(2 ** (_clog2(abs(x)) + 1) + x)[2:] binstr = self._adjust_twos_comp(binstr) elif x == 0: binstr = self._adjust_twos_comp("") else: binstr = self._adjust_twos_comp("0" + bin(x)[2:]) if self.big_endian: binstr = binstr[::-1] return binstr def _convert_from_unsigned(self, x): if not len(x): return 0 return int(x.translate(_resolve_table), 2) def _convert_from_signed_mag(self, x): if not len(x): return 0 rv = int(self._str[1:].translate(_resolve_table), 2) if self._str[0] == "1": rv = rv * -1 return rv def _convert_from_twos_comp(self, x): if not len(x): return 0 if x[0] == "1": binstr = x[1:] binstr = self._invert(binstr) rv = int(binstr, 2) + 1 rv = rv * -1 else: rv = int(x.translate(_resolve_table), 2) return rv _convert_to_map = { BinaryRepresentation.UNSIGNED: _convert_to_unsigned, BinaryRepresentation.SIGNED_MAGNITUDE: _convert_to_signed_mag, BinaryRepresentation.TWOS_COMPLEMENT: _convert_to_twos_comp, } _convert_from_map = { BinaryRepresentation.UNSIGNED: _convert_from_unsigned, BinaryRepresentation.SIGNED_MAGNITUDE: _convert_from_signed_mag, BinaryRepresentation.TWOS_COMPLEMENT: _convert_from_twos_comp, } _invert_table = str.maketrans({"0": "1", "1": "0"}) def _invert(self, x): return x.translate(self._invert_table) def _adjust_unsigned(self, x): if self._n_bits is None: return x l = len(x) if l <= self._n_bits: if self.big_endian: rv = x + "0" * (self._n_bits - l) else: rv = "0" * (self._n_bits - l) + x elif l > self._n_bits: if self.big_endian: rv = x[l - self._n_bits :] else: rv = x[: l - self._n_bits] warnings.warn( "{}-bit value requested, truncating value {!r} ({} bits) to {!r}".format( self._n_bits, x, l, rv ), category=RuntimeWarning, stacklevel=3, ) return rv def _adjust_signed_mag(self, x): """Pad/truncate the bit string to the correct length.""" if self._n_bits is None: return x l = len(x) if l < self._n_bits: if self.big_endian: rv = x[:-1] + "0" * (self._n_bits - 1 - l) rv = rv + x[-1] else: rv = "0" * (self._n_bits - 1 - l) + x[1:] rv = x[0] + rv elif l > self._n_bits: if self.big_endian: rv = x[l - self._n_bits :] else: rv = x[: -(l - self._n_bits)] warnings.warn( "{}-bit value requested, truncating value {!r} ({} bits) to {!r}".format( self._n_bits, x, l, rv ), category=RuntimeWarning, stacklevel=3, ) else: rv = x return rv def _adjust_twos_comp(self, x): if self._n_bits is None: return x l = len(x) if l == 0: rv = x elif l < self._n_bits: if self.big_endian: rv = x + x[-1] * (self._n_bits - l) else: rv = x[0] * (self._n_bits - l) + x elif l > self._n_bits: if self.big_endian: rv = x[l - self._n_bits :] else: rv = x[: -(l - self._n_bits)] warnings.warn( "{}-bit value requested, truncating value {!r} ({} bits) to {!r}".format( self._n_bits, x, l, rv ), category=RuntimeWarning, stacklevel=3, ) else: rv = x return rv @property def integer(self): """The integer representation of the underlying vector.""" return self._convert_from(self._str) @integer.setter def integer(self, val): self._str = self._convert_to(val) @property @deprecated("Use `bv.integer` instead.") def value(self): """Integer access to the value. **deprecated**""" return self.integer @value.setter @deprecated("Use `bv.integer` instead.") def value(self, val): self.integer = val get_value = value.fget set_value = value.fset @property def signed_integer(self): """The signed integer representation of the underlying vector.""" ival = int(self._str.translate(_resolve_table), 2) bits = len(self._str) signbit = 1 << (bits - 1) if (ival & signbit) == 0: return ival else: return -1 * (1 + (int(~ival) & (signbit - 1))) @signed_integer.setter def signed_integer(self, val): self.integer = val get_value_signed = signed_integer.fget @property def is_resolvable(self) -> bool: """ Return whether the value contains only resolvable (i.e. no "unknown") bits. By default the values ``X``, ``Z``, ``U`` and ``W`` are considered unresolvable. This can be configured with :envvar:`COCOTB_RESOLVE_X`. This is similar to the SystemVerilog Assertion ``$isunknown`` system function or the VHDL function ``is_x`` (with an inverted meaning). """ return not any(char in self._str for char in _RESOLVE_TO_CHOICE) @property def buff(self) -> bytes: r"""The value as a binary string buffer. >>> BinaryValue("01000001" + "00101111").buff == b"\x41\x2F" True .. versionchanged:: 1.4 This changed from :class:`str` to :class:`bytes`. Note that for older versions used with Python 2 these types were indistinguishable. """ bits = self._str.translate(_resolve_table) if len(bits) % 8: bits = "0" * (8 - len(bits) % 8) + bits buff = [] while bits: byte = bits[:8] bits = bits[8:] val = int(byte, 2) if self.big_endian: buff += [val] else: buff = [val] + buff return bytes(buff) @buff.setter def buff(self, val: bytes): if not self.big_endian: val = reversed(val) self._str = "".join([format(char, "08b") for char in val]) self._adjust() def _adjust(self): """Pad/truncate the bit string to the correct length.""" if self._n_bits is None: return l = len(self._str) if l < self._n_bits: if self.big_endian: self._str = self._str + "0" * (self._n_bits - l) else: self._str = "0" * (self._n_bits - l) + self._str elif l > self._n_bits: rv = self._str[l - self._n_bits :] warnings.warn( "{}-bit value requested, truncating value {!r} ({} bits) to {!r}".format( self._n_bits, self._str, l, rv ), category=RuntimeWarning, stacklevel=3, ) self._str = rv get_buff = buff.fget set_buff = buff.fset @property def binstr(self): """The binary representation stored as a string of ``0``, ``1``, and possibly ``x``, ``z``, and other states.""" return self._str _non_permitted_regex = re.compile(f"[^{_permitted_chars}]") @binstr.setter def binstr(self, string): match = self._non_permitted_regex.search(string) if match: raise ValueError( "Attempting to assign character %s to a %s" % (match.group(), self.__class__.__name__) ) self._str = string self._adjust() get_binstr = binstr.fget set_binstr = binstr.fset def _set_trusted_binstr(self, string): self._str = string @property def n_bits(self): """The number of bits of the binary value.""" return self._n_bits def hex(self): try: return hex(self.integer) except Exception: return hex(int(self.binstr, 2)) def __le__(self, other): self.assign(other) def __str__(self): return self.binstr def __repr__(self): return self.__str__() def __bool__(self): """Provide boolean testing of a :attr:`binstr`. >>> val = BinaryValue("0000") >>> if val: print("True") ... else: print("False") False >>> val.integer = 42 >>> if val: print("True") ... else: print("False") True """ for char in self._str: if char == "1": return True return False def __eq__(self, other): if isinstance(other, (BinaryValue, LogicArray)): return self.binstr == other.binstr elif isinstance(other, int): try: return self.integer == other except ValueError: return False elif isinstance(other, str): return self.binstr == other elif isinstance(other, Logic): return self.binstr == str(other) else: return NotImplemented def __int__(self): return self.integer def __long__(self): return self.integer def __add__(self, other): return self.integer + int(other) def __iadd__(self, other): self.integer = self.integer + int(other) return self def __radd__(self, other): return self.integer + other def __sub__(self, other): return self.integer - int(other) def __isub__(self, other): self.integer = self.integer - int(other) return self def __rsub__(self, other): return other - self.integer def __mul__(self, other): return self.integer * int(other) def __imul__(self, other): self.integer = self.integer * int(other) return self def __rmul__(self, other): return self.integer * other def __floordiv__(self, other): return self.integer // int(other) def __ifloordiv__(self, other): self.integer = self.__floordiv__(other) return self def __rfloordiv__(self, other): return other // self.integer def __divmod__(self, other): return (self.integer // other, self.integer % other) def __rdivmod__(self, other): return other // self.integer def __mod__(self, other): return self.integer % int(other) def __imod__(self, other): self.integer = self.integer % int(other) return self def __rmod__(self, other): return other % self.integer def __pow__(self, other, modulo=None): return pow(self.integer, other) def __ipow__(self, other): self.integer = pow(self.integer, other) return self def __rpow__(self, other): return pow(other, self.integer) def __lshift__(self, other): return int(self) << int(other) def __ilshift__(self, other): """Preserves X values""" self.binstr = self.binstr[other:] + self.binstr[:other] return self def __rlshift__(self, other): return other << self.integer def __rshift__(self, other): return int(self) >> int(other) def __irshift__(self, other): """Preserves X values""" self.binstr = self.binstr[-other:] + self.binstr[:-other] return self def __rrshift__(self, other): return other >> self.integer def __and__(self, other): return self.integer & other def __iand__(self, other): self.integer &= other return self def __rand__(self, other): return self.integer & other def __xor__(self, other): return self.integer ^ other def __ixor__(self, other): self.integer ^= other return self def __rxor__(self, other): return self.__xor__(other) def __or__(self, other): return self.integer | other def __ior__(self, other): self.integer |= other return self def __ror__(self, other): return self.__or__(other) def __div__(self, other): return self.integer / other def __idiv__(self, other): self.integer /= other return self def __rdiv__(self, other): return other / self.integer def __neg__(self): return -self.integer def __pos__(self): return +self.integer def __abs__(self): return abs(self.integer) def __invert__(self): """Preserves X values""" return self._invert(self.binstr) def __oct__(self): return oct(self.integer) def __hex__(self): return hex(self.integer) def __index__(self): return self.integer def __len__(self): return len(self.binstr) def __getitem__(self, key): """BinaryValue uses Verilog/VHDL style slices as opposed to Python style""" if isinstance(key, slice): first, second = key.start, key.stop if self.big_endian: if first < 0 or second < 0: raise IndexError("BinaryValue does not support negative " "indices") if second > self._n_bits - 1: raise IndexError("High index greater than number of bits.") if first > second: raise IndexError( "Big Endian indices must be specified " "low to high" ) _binstr = self.binstr[first : (second + 1)] else: if first < 0 or second < 0: raise IndexError("BinaryValue does not support negative " "indices") if first > self._n_bits - 1: raise IndexError("High index greater than number of bits.") if second > first: raise IndexError( "Litte Endian indices must be specified " "high to low" ) high = self._n_bits - second low = self._n_bits - 1 - first _binstr = self.binstr[low:high] else: index = key if index > self._n_bits - 1: raise IndexError("Index greater than number of bits.") if self.big_endian: _binstr = self.binstr[index] else: _binstr = self.binstr[self._n_bits - 1 - index] rv = BinaryValue( n_bits=len(_binstr), bigEndian=self.big_endian, binaryRepresentation=self.binaryRepresentation, ) rv.binstr = _binstr return rv def __setitem__(self, key, val): """BinaryValue uses Verilog/VHDL style slices as opposed to Python style.""" if not isinstance(val, str) and not isinstance(val, int): raise TypeError("BinaryValue slices only accept string or integer values") # convert integer to string if isinstance(val, int): if isinstance(key, slice): num_slice_bits = abs(key.start - key.stop) + 1 else: num_slice_bits = 1 if val < 0: raise ValueError("Integer must be positive") if val >= 2**num_slice_bits: raise ValueError( "Integer is too large for the specified slice " "length" ) val = "{:0{width}b}".format(val, width=num_slice_bits) if isinstance(key, slice): first, second = key.start, key.stop if self.big_endian: if first < 0 or second < 0: raise IndexError("BinaryValue does not support negative " "indices") if second > self._n_bits - 1: raise IndexError("High index greater than number of bits.") if first > second: raise IndexError( "Big Endian indices must be specified " "low to high" ) if len(val) > (second + 1 - first): raise ValueError("String length must be equal to slice " "length") slice_1 = self.binstr[:first] slice_2 = self.binstr[second + 1 :] self.binstr = slice_1 + val + slice_2 else: if first < 0 or second < 0: raise IndexError("BinaryValue does not support negative " "indices") if first > self._n_bits - 1: raise IndexError("High index greater than number of bits.") if second > first: raise IndexError( "Litte Endian indices must be specified " "high to low" ) high = self._n_bits - second low = self._n_bits - 1 - first if len(val) > (high - low): raise ValueError("String length must be equal to slice " "length") slice_1 = self.binstr[:low] slice_2 = self.binstr[high:] self.binstr = slice_1 + val + slice_2 else: if len(val) != 1: raise ValueError("String length must be equal to slice " "length") index = key if index > self._n_bits - 1: raise IndexError("Index greater than number of bits.") if self.big_endian: self.binstr = self.binstr[:index] + val + self.binstr[index + 1 :] else: self.binstr = ( self.binstr[0 : self._n_bits - index - 1] + val + self.binstr[self._n_bits - index : self._n_bits] )
from cocotb.types import Logic, LogicArray # noqa: E402 if __name__ == "__main__": import doctest doctest.testmod()