From bd6b3ea8b0554028596d31e9cd34405e057ea7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ob=C5=82onczek?= Date: Fri, 16 Aug 2024 14:19:45 +0200 Subject: [PATCH] modules/zstd/cocotb: Add cocotb testing utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - XLSStruct for easier handling and serializing/deserializing XLS structs - XLSChannel that serves as a dummy receiving channel - XLSMonitor that monitors transactions on an XLS channel - XLSDriver that can send data on an XLS channel - LatencyScoreboard that can measure latency between corresponding transactions on input and output buses - File-backed AXI memory python model Internal-tag: [#64075] Signed-off-by: Krzysztof Obłonczek --- dependency_support/pip_requirements.in | 4 + dependency_support/pip_requirements_lock.txt | 70 ++++++++ xls/modules/zstd/cocotb/BUILD | 66 +++++++ xls/modules/zstd/cocotb/channel.py | 95 ++++++++++ xls/modules/zstd/cocotb/memory.py | 43 +++++ xls/modules/zstd/cocotb/scoreboard.py | 69 ++++++++ xls/modules/zstd/cocotb/utils.py | 57 ++++++ xls/modules/zstd/cocotb/xlsstruct.py | 175 +++++++++++++++++++ 8 files changed, 579 insertions(+) create mode 100644 xls/modules/zstd/cocotb/BUILD create mode 100644 xls/modules/zstd/cocotb/channel.py create mode 100644 xls/modules/zstd/cocotb/memory.py create mode 100644 xls/modules/zstd/cocotb/scoreboard.py create mode 100644 xls/modules/zstd/cocotb/utils.py create mode 100644 xls/modules/zstd/cocotb/xlsstruct.py diff --git a/dependency_support/pip_requirements.in b/dependency_support/pip_requirements.in index 9843297e6f..636beda523 100644 --- a/dependency_support/pip_requirements.in +++ b/dependency_support/pip_requirements.in @@ -8,6 +8,10 @@ termcolor==1.1.0 psutil==5.7.0 portpicker==1.3.1 pyyaml==6.0.1 +pytest==8.2.2 +cocotb==1.9.0 +cocotbext-axi==0.1.24 +cocotb_bus==0.2.1 # Note: numpy and scipy version availability seems to differ between Ubuntu # versions that we want to support (e.g. 18.04 vs 20.04), so we accept a diff --git a/dependency_support/pip_requirements_lock.txt b/dependency_support/pip_requirements_lock.txt index 0f058d5f3c..109e6df1fb 100644 --- a/dependency_support/pip_requirements_lock.txt +++ b/dependency_support/pip_requirements_lock.txt @@ -14,10 +14,68 @@ click==8.1.3 \ # via # -r dependency_support/pip_requirements.in # flask +cocotb==1.9.0 \ + --hash=sha256:02a58ef6c941114964096e7c039bdd4e67e63816cfd2f6a9af6a34cd92b00e8e \ + --hash=sha256:0819794ef5e8fd14fee0b265933226cf600e85edc2f1a749b4d5f8fa2d31ce4e \ + --hash=sha256:0ba35617a677ff65a1273411a3dfdfc5f587128ad8cb9e941ab0eb17ec8fb3e2 \ + --hash=sha256:17556e3a23562f64d577d0eb117fe02e384aedee997b29497b5c395f5010ff82 \ + --hash=sha256:19b4e27b53a16e0b9c4cc5227c7f9d4dccac06e431a4f937e9f5513350196333 \ + --hash=sha256:1a0381ced5590a726032ba2265c6b70ac12cfb49edb152be86a081bb7d104751 \ + --hash=sha256:1aff68cf77059448a9a3278079037e34b50c8c2aee466d984295fa7fe699d390 \ + --hash=sha256:277281420fd6fc3002bb85d6bec497bd20ff3a3905d4b5f1301faf975f750ede \ + --hash=sha256:2daf743320331615f4e8ffb877ab0b04e6f913b911bb11bf9dbc1d876d9c4220 \ + --hash=sha256:2e9bcdbfba3e99c9297bd0d74ba781772d89d2c86e893980784ada252bd1a0f8 \ + --hash=sha256:3058c977f9d4e1f6333d505947f34b9142910719f1d8631c40a151dd86bad727 \ + --hash=sha256:5832d894419a9e8fe5c242e3ac86588e16e2cb379822dcb154bfec8544ae858e \ + --hash=sha256:598b841ed0809e5c64d8c383b8035f6ace5a6f9013f680cdc6981221911c005d \ + --hash=sha256:5a5c91027d7652aaf10e101743edd6b1e832039a19af75fca301275ef30f01d4 \ + --hash=sha256:61418f619af72c8cca8de622785b4f4bfc17ace09981de6eb44feae560cf3bbb \ + --hash=sha256:784c914c8df3fd79cfb148d2bcd17c4b2703c89af1278ed98773afb57ceea3e6 \ + --hash=sha256:87a19d3012f505ba7fda37483b851ef0ca40290ad8a9b28a820b84f8574287bb \ + --hash=sha256:89503f0749362d36b6fab8636710f1848943c21f9d488672921bac21e9edd29f \ + --hash=sha256:89e5189fd393918c27af2daefdcb13df4d52fa761f065d5964d2c4ff5c0642fb \ + --hash=sha256:8cb4b0edf8f0b47c3b604b461cb574fc75fd97efa893cbaf828f4f2f71cf459e \ + --hash=sha256:94e884e16186899ad5b4d131c3f7ff0a2277e67ea0660754e8810a4bbf2d610e \ + --hash=sha256:997dbca2a2cd933fd0a44d9fadeebc1e8a40701db15ea06f207811933dceb350 \ + --hash=sha256:a7cea13cb2fe4f5ca735490846342885117778a73008a67ed9cac667aaaf3f0d \ + --hash=sha256:a84edfbfa57dc6e16845a55feb0b4e1c8b6bbfa5ef1ab6768beba8d81e0546aa \ + --hash=sha256:a95b5e5708a3629d319d2b655d11345cc7e97fea9bdc9bc1df7435926ac30966 \ + --hash=sha256:aa6818c39ca1ce699e4bb1d84899c4f98c2d25c7671bd6c7beee3b1ee9d68834 \ + --hash=sha256:ab99bf7e055780b57419d4133fd4dca9c72a03b766a3e2200552f10498eb8845 \ + --hash=sha256:b966f5560a494fd99f95a1562f9326ca20c35bb118d4e6b50db41da8e4a6f718 \ + --hash=sha256:bc44a7708a5a63d3059a622c2fb90831dc33534c3343e971f5a6c78905097baa \ + --hash=sha256:c11e21d291ba2f889e33c21d76e9aec6ffdfb5666053dc34452666579daa675b \ + --hash=sha256:c848de13583478d71cc91e528e17c051ca6a3b92e89d703ac5015f17cab1287b \ + --hash=sha256:d944aa5509a0f0786d6f30554a2f8b1f229847f9ac9988879d7a05497739f668 \ + --hash=sha256:f50862153e1364f6edeaef9d70505093549fa097e9b2555ea46d1e4f94ac3287 \ + --hash=sha256:f74c598e230e1035103f6e3a97dd7a0e1bcacf7f3ea7481cd3bcde477b74e379 \ + --hash=sha256:fcb81c6c37e11b0729768dd8e192a9cfb809778699ab1fe89f4d92ba0beb3092 \ + --hash=sha256:ff2ddc8b304eb7076ceead2534a1b9828df771798fa9c2601ea983c86d23ec08 + # via + # -r dependency_support/pip_requirements.in + # cocotb-bus + # cocotbext-axi +cocotb-bus==0.2.1 \ + --hash=sha256:a197aa4b0e0ad28469c8877b41b3fb2ec0206da9f491b9276d1578ce6dd8aa8d + # via + # -r dependency_support/pip_requirements.in + # cocotbext-axi +cocotbext-axi==0.1.24 \ + --hash=sha256:3ed62dcaf9448833176826507c5bc5c346431c4846a731e409d87c862d960593 \ + --hash=sha256:533ba6c7503c6302bdb9ef86e43a549ad5da876eafb1adce23d39751c54cced4 + # via -r dependency_support/pip_requirements.in +find-libpython==0.4.0 \ + --hash=sha256:034a4253bd57da3408aefc59aeac1650150f6c1f42e10fdd31615cf1df0842e3 \ + --hash=sha256:46f9cdcd397ddb563b2d7592ded3796a41c1df5222443bd9d981721c906c03e6 + # via cocotb flask==2.3.2 \ --hash=sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0 \ --hash=sha256:8c2f9abd47a9e8df7f0c3f091ce9497d011dc3b31effcf4c85a6e2b50f4114ef # via -r dependency_support/pip_requirements.in +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest itsdangerous==2.1.2 \ --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \ --hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a @@ -107,6 +165,14 @@ numpy==1.24.4 \ # via # -r dependency_support/pip_requirements.in # scipy +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via pytest +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest portpicker==1.3.1 \ --hash=sha256:d2cdc776873635ed421315c4d22e63280042456bbfa07397817e687b142b9667 # via -r dependency_support/pip_requirements.in @@ -123,6 +189,10 @@ psutil==5.7.0 \ --hash=sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5 \ --hash=sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310 # via -r dependency_support/pip_requirements.in +pytest==8.2.2 \ + --hash=sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343 \ + --hash=sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977 + # via -r dependency_support/pip_requirements.in pyyaml==6.0.1 \ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ diff --git a/xls/modules/zstd/cocotb/BUILD b/xls/modules/zstd/cocotb/BUILD new file mode 100644 index 0000000000..b8ff7e3f8b --- /dev/null +++ b/xls/modules/zstd/cocotb/BUILD @@ -0,0 +1,66 @@ +# Copyright 2024 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@xls_pip_deps//:requirements.bzl", "requirement") + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//xls:xls_users"], + licenses = ["notice"], +) + +py_library( + name = "channel", + srcs = ["channel.py"], + deps = [ + ":xlsstruct", + requirement("cocotb"), + requirement("cocotb_bus"), + ] +) + +py_library( + name = "memory", + srcs = ["memory.py"], + deps = [ + requirement("cocotbext-axi"), + ] +) + +py_library( + name = "scoreboard", + srcs = ["scoreboard.py"], + deps = [ + ":channel", + ":xlsstruct", + requirement("cocotb"), + ] +) + +py_library( + name = "utils", + srcs = ["utils.py"], + deps = [ + requirement("cocotb"), + "//xls/common:runfiles", + ] +) + +py_library( + name = "xlsstruct", + srcs = ["xlsstruct.py"], + deps = [ + requirement("cocotb"), + ], +) diff --git a/xls/modules/zstd/cocotb/channel.py b/xls/modules/zstd/cocotb/channel.py new file mode 100644 index 0000000000..0970ab6e9b --- /dev/null +++ b/xls/modules/zstd/cocotb/channel.py @@ -0,0 +1,95 @@ +# Copyright 2024 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Sequence, Type, Union + +import cocotb +from cocotb.handle import SimHandleBase +from cocotb.triggers import RisingEdge +from cocotb_bus.bus import Bus +from cocotb_bus.drivers import BusDriver +from cocotb_bus.monitors import BusMonitor + +from xls.modules.zstd.cocotb.xlsstruct import XLSStruct + +Transaction = Union[XLSStruct, Sequence[XLSStruct]] + +XLS_CHANNEL_SIGNALS = ["data", "rdy", "vld"] +XLS_CHANNEL_OPTIONAL_SIGNALS = [] + + +class XLSChannel(Bus): + _signals = XLS_CHANNEL_SIGNALS + _optional_signals = XLS_CHANNEL_OPTIONAL_SIGNALS + + def __init__(self, entity, name, clk, *, start_now=False, **kwargs: Any): + super().__init__(entity, name, self._signals, self._optional_signals, **kwargs) + self.clk = clk + if start_now: + self.start_recv_loop() + + @cocotb.coroutine + async def recv_channel(self): + """Cocotb coroutine that acts as a proc receiving data from a channel""" + self.rdy.setimmediatevalue(1) + while True: + await RisingEdge(self.clk) + + def start_recv_loop(self): + cocotb.start_soon(self.recv_channel()) + + +class XLSChannelDriver(BusDriver): + _signals = XLS_CHANNEL_SIGNALS + _optional_signals = XLS_CHANNEL_OPTIONAL_SIGNALS + + def __init__(self, entity: SimHandleBase, name: str, clock: SimHandleBase, **kwargs: Any): + BusDriver.__init__(self, entity, name, clock, **kwargs) + + self.bus.data.setimmediatevalue(0) + self.bus.vld.setimmediatevalue(0) + + async def _driver_send(self, transaction: Transaction, sync: bool = True, **kwargs: Any) -> None: + if sync: + await RisingEdge(self.clock) + + data_to_send = (transaction if isinstance(transaction, Sequence) else [transaction]) + + for word in data_to_send: + self.bus.vld.value = 1 + self.bus.data.value = word.binaryvalue + + while True: + await RisingEdge(self.clock) + if self.bus.rdy.value: + break + + self.bus.vld.value = 0 + + +class XLSChannelMonitor(BusMonitor): + _signals = XLS_CHANNEL_SIGNALS + _optional_signals = XLS_CHANNEL_OPTIONAL_SIGNALS + + def __init__(self, entity: SimHandleBase, name: str, clock: SimHandleBase, struct: Type[XLSStruct], **kwargs: Any): + BusMonitor.__init__(self, entity, name, clock, **kwargs) + self.struct = struct + + @cocotb.coroutine + async def _monitor_recv(self) -> None: + while True: + await RisingEdge(self.clock) + if self.bus.rdy.value and self.bus.vld.value: + vec = self.struct.from_int(self.bus.data.value.integer) + self._recv(vec) diff --git a/xls/modules/zstd/cocotb/memory.py b/xls/modules/zstd/cocotb/memory.py new file mode 100644 index 0000000000..111b254c6c --- /dev/null +++ b/xls/modules/zstd/cocotb/memory.py @@ -0,0 +1,43 @@ +# Copyright 2024 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from cocotbext.axi.axi_ram import AxiRam, AxiRamRead, AxiRamWrite +from cocotbext.axi.sparse_memory import SparseMemory + + +def init_axi_mem(path: os.PathLike, **kwargs): + with open(path, "rb") as f: + sparse_mem = SparseMemory(size=kwargs["size"]) + sparse_mem.write(0x0, f.read()) + kwargs["mem"] = sparse_mem + + +class AxiRamReadFromFile(AxiRamRead): + def __init__(self, *args, path: os.PathLike, **kwargs): + init_axi_mem(path, **kwargs) + super().__init__(*args, **kwargs) + + +class AxiRamFromFile(AxiRam): + def __init__(self, *args, path: os.PathLike, **kwargs): + init_axi_mem(path, **kwargs) + super().__init__(*args, **kwargs) + + +class AxiRamWriteFromFile(AxiRamWrite): + def __init__(self, *args, path: os.PathLike, **kwargs): + init_axi_mem(path, **kwargs) + super().__init__(*args, **kwargs) diff --git a/xls/modules/zstd/cocotb/scoreboard.py b/xls/modules/zstd/cocotb/scoreboard.py new file mode 100644 index 0000000000..b9b64ca6e2 --- /dev/null +++ b/xls/modules/zstd/cocotb/scoreboard.py @@ -0,0 +1,69 @@ +# Copyright 2024 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from queue import Queue + +from cocotb.clock import Clock +from cocotb.log import SimLog +from cocotb.utils import get_sim_time + +from xls.modules.zstd.cocotb.channel import XLSChannelMonitor +from xls.modules.zstd.cocotb.xlsstruct import XLSStruct + + +@dataclass +class LatencyQueueItem: + transaction: XLSStruct + timestamp: int + + +class LatencyScoreboard: + def __init__(self, dut, clock: Clock, req_monitor: XLSChannelMonitor, resp_monitor: XLSChannelMonitor): + self.dut = dut + self.log = SimLog(f"zstd.cocotb.scoreboard.{self.dut._name}") + self.clock = clock + self.req_monitor = req_monitor + self.resp_monitor = resp_monitor + self.pending_req = Queue() + self.results = [] + + self.req_monitor.add_callback(self._req_callback) + self.resp_monitor.add_callback(self._resp_callback) + + def _current_cycle(self): + return get_sim_time(units='step') / self.clock.period + + def _req_callback(self, transaction: XLSStruct): + self.pending_req.put(LatencyQueueItem(transaction, self._current_cycle())) + + def _resp_callback(self, transaction: XLSStruct): + latency_item = self.pending_req.get() + self.results.append(self._current_cycle() - latency_item.timestamp) + + def average_latency(self): + return sum(self.results)/len(self.results) + + def report_result(self): + if not self.pending_req.empty(): + self.log.warning(f"There are unfulfilled requests from channel {self.req_monitor.name}") + while not self.pending_req.empty(): + self.log.warning(f"Unfulfilled request: {self.pending_req.get()}") + if len(self.results) > 0: + self.log.info(f"Latency report - 1st latency: {self.results[0]}") + if len(self.results) > 1: + self.log.info(f"Latency report - 2nd latency: {self.results[1]}") + if len(self.results) > 2: + avg = sum(self.results[2:])/len(self.results[2:]) + self.log.info(f"Latency report - rest of the latencies (average): {avg}") diff --git a/xls/modules/zstd/cocotb/utils.py b/xls/modules/zstd/cocotb/utils.py new file mode 100644 index 0000000000..0930a92932 --- /dev/null +++ b/xls/modules/zstd/cocotb/utils.py @@ -0,0 +1,57 @@ +# Copyright 2024 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from pathlib import Path + +import cocotb +from cocotb.runner import check_results_file, get_runner +from cocotb.triggers import ClockCycles + +from xls.common import runfiles + + +def setup_com_iverilog(): + iverilog_path = Path(runfiles.get_path("iverilog", repository = "com_icarus_iverilog")) + vvp_path = Path(runfiles.get_path("vvp", repository = "com_icarus_iverilog")) + os.environ["PATH"] += os.pathsep + str(iverilog_path.parent) + os.environ["PATH"] += os.pathsep + str(vvp_path.parent) + build_dir = Path(os.environ['BUILD_WORKING_DIRECTORY'], "sim_build") + return build_dir + +def run_test(toplevel, test_module, verilog_sources): + build_dir = setup_com_iverilog() + runner = get_runner("icarus") + runner.build( + verilog_sources=verilog_sources, + hdl_toplevel=toplevel, + timescale=("1ns", "1ps"), + build_dir=build_dir, + defines={"SIMULATION": "1"}, + waves=True, + ) + + results_xml = runner.test( + hdl_toplevel=toplevel, + test_module=test_module, + waves=True, + ) + check_results_file(results_xml) + +@cocotb.coroutine +async def reset(clk, rst, cycles=1): + """Cocotb coroutine that performs the reset""" + rst.value = 1 + await ClockCycles(clk, cycles) + rst.value = 0 diff --git a/xls/modules/zstd/cocotb/xlsstruct.py b/xls/modules/zstd/cocotb/xlsstruct.py new file mode 100644 index 0000000000..a2d686a8af --- /dev/null +++ b/xls/modules/zstd/cocotb/xlsstruct.py @@ -0,0 +1,175 @@ +# Copyright 2024 The XLS Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from dataclasses import asdict, astuple, dataclass, fields + +from cocotb.binary import BinaryValue + + +class TruncationError(Exception): + pass + +def xls_dataclass(cls): + """ + Class decorator for XLS structs. + Usage: + + @xls_dataclass + class MyStruct(XLSStruct): + ... + """ + return dataclass(cls, repr=False) + +@dataclass +class XLSStruct: + """ + Represents XLS struct on the Python side, allowing serialization/deserialization + to/from common formats and usage with XLS{Driver, Monitor}. + + The intended way to use this class is to inherit from it, specify the fields with + : [= ] syntax and decorate the inheriting class with + @XLSDataclass. Objects of this class can be instantiated and used like usual + dataclass objects, with a few extra methods and properties available. They can also + be passed as arguments to XLSChannelDriver.send and will be serialized to expected + bit vector. Class can be passed to XLSChannelMonitor ``struct`` constructor argument + to automatically deserialize all transfers to the provided struct. + + Example: + + from xlsstruct import XLSDataclass, XLSStruct + + @XLSDataclass + class MyStruct(XLSStruct): + data: 32 + ok: 1 + id: 4 = 0 + + monitor = XLSChannelMonitor(dut, CHANNEL_PREFIX, dut.clk, MyStruct) + + driver = XLSChannelDriver(dut, CHANNEL_PREFIX, dut.clk) + driver.send(MyStruct( + data = 0xdeadbeef, + ok = 1, + id = 3, + )) + # struct fields can also be randomized + driver.send(MyStruct.randomize()) + """ + + @classmethod + def _masks(cls): + """ + Returns a list of field-sized bitmasks. + + For example for fields of widths 2, 3, 4 + returns [2'b11, 3'b111, 4'b1111]. + """ + masks = [] + for field in fields(cls): + width = field.type + masks += [(1 << width) - 1] + return masks + + @classmethod + def _positions(cls): + """ + Returns a list of start positions in a bit vector for + struct's fields. + + For example for fields of widths 1, 2, 3, 4, 5, 6 + returns [20, 18, 15, 11, 6, 0] + """ + positions = [] + for i, field in enumerate(fields(cls)): + width = field.type + if i == 0: + positions += [cls.total_width - width] + else: + positions += [positions[i-1] - width] + return positions + + @classmethod + @property + def total_width(cls): + """ + Returns total bit width of the struct + """ + return sum(field.type for field in fields(cls)) + + @property + def value(self): + """ + Returns struct's value as a Python integer + """ + value = 0 + masks = self._masks() + positions = self._positions() + for field_val, mask, pos in zip(astuple(self), masks, positions): + if field_val > mask: + raise TruncationError(f"Signal value is wider than its bit width") + value |= (field_val & mask) << pos + return value + + @property + def binaryvalue(self): + """ + Returns struct's value as a cocotb.binary.BinaryValue + """ + return BinaryValue(self.binstr) + + @property + def binstr(self): + """ + Returns struct's value as a string with its binary representation + """ + return f"{self.value:>0{self.total_width}b}" + + @property + def hexstr(self): + """ + Returns struct's value as a string with its hex representation + (without leading "0x") + """ + return f"{self.value:>0{self.total_width // 4}x}" + + @classmethod + def from_int(cls, value): + """ + Returns an instance of the struct from Python integer + """ + instance = {} + masks = cls._masks() + positions = cls._positions() + for field, mask, pos in zip(fields(cls), masks, positions): + instance[field.name] = (value >> pos) & mask + return cls(**instance) + + @classmethod + def randomize(cls): + """ + Returns an instance of the struct with all fields' values randomized + """ + instance = {} + for field in fields(cls): + instance[field.name] = random.randrange(0, 2**field.type) + return cls(**instance) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + classname = self.__class__.__name__ + fields = [f"{name}={hex(value)}" for name, value in asdict(self).items()] + return f"{classname}({', '.join(fields)})"