diff --git a/.github/workflows/modules-zstd.yml b/.github/workflows/modules-zstd.yml index 002a0c23c7..7dfde39147 100644 --- a/.github/workflows/modules-zstd.yml +++ b/.github/workflows/modules-zstd.yml @@ -75,6 +75,11 @@ jobs: bazel run -c opt $target -- --logtostderr; done + - name: Build and test verilog simulation of the ZSTD module components (opt) + if: ${{ !cancelled() }} + run: | + bazel test -c opt --test_output=errors -- $(bazel query 'filter(".*_cocotb_test", kind(rule, //xls/modules/zstd/...))') + - name: Build ZSTD place and route targets (opt) if: ${{ !cancelled() }} run: | diff --git a/dependency_support/pip_requirements.in b/dependency_support/pip_requirements.in index 2382e41330..3824c3c9ac 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 ac0bff0f4f..5945bc094b 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/BUILD b/xls/modules/zstd/BUILD index 8717497922..5a229bf930 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -33,6 +33,8 @@ package( licenses = ["notice"], ) +exports_files(["xls_fifo_wrapper.v"]) + xls_dslx_library( name = "buffer_dslx", srcs = [ @@ -506,7 +508,7 @@ xls_dslx_verilog( codegen_args = { "module_name": "DecoderMux", "delay_model": "asap7", - "pipeline_stages": "2", + "pipeline_stages": "3", "reset": "rst", "use_system_verilog": "false", }, @@ -520,7 +522,7 @@ xls_benchmark_ir( name = "dec_mux_opt_ir_benchmark", src = ":dec_mux_verilog.opt.ir", benchmark_ir_args = { - "pipeline_stages": "2", + "pipeline_stages": "10", "delay_model": "asap7", }, tags = ["manual"], diff --git a/xls/modules/zstd/cocotb/BUILD b/xls/modules/zstd/cocotb/BUILD new file mode 100644 index 0000000000..23b1a843c3 --- /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..52e512e053 --- /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)})" diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index ca5e0a155f..4e9cfe2b81 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -15,6 +15,7 @@ load("@rules_hdl//place_and_route:build_defs.bzl", "place_and_route") load("@rules_hdl//synthesis:build_defs.bzl", "benchmark_synth", "synthesize_rtl") load("@rules_hdl//verilog:providers.bzl", "verilog_library") +load("@xls_pip_deps//:requirements.bzl", "requirement") load( "//xls/build_rules:xls_build_defs.bzl", "xls_benchmark_ir", @@ -402,6 +403,7 @@ verilog_library( name = "mem_reader_verilog_lib", srcs = [ ":mem_reader.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", ], tags = ["manual"], ) @@ -457,6 +459,7 @@ verilog_library( name = "mem_reader_adv_verilog_lib", srcs = [ ":mem_reader_adv.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", ], tags = ["manual"], ) @@ -489,6 +492,34 @@ place_and_route( target_die_utilization_percentage = "10", ) +py_test( + name = "mem_reader_cocotb_test", + srcs = ["mem_reader_cocotb_test.py"], + data = [ + ":mem_reader_adv.v", + ":mem_reader_wrapper.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", + "@com_icarus_iverilog//:iverilog", + "@com_icarus_iverilog//:vvp", + ], + env = {"BUILD_WORKING_DIRECTORY": "sim_build"}, + tags = ["manual"], + visibility = ["//xls:xls_users"], + deps = [ + requirement("cocotb"), + requirement("cocotbext-axi"), + requirement("pytest"), + "//xls/common:runfiles", + "//xls/modules/zstd/cocotb:channel", + "//xls/modules/zstd/cocotb:memory", + "//xls/modules/zstd/cocotb:utils", + "//xls/modules/zstd/cocotb:xlsstruct", + "@com_google_absl_py//absl:app", + "@com_google_absl_py//absl/flags", + "@com_google_protobuf//:protobuf_python", + ], +) + xls_dslx_library( name = "axi_writer_dslx", srcs = ["axi_writer.x"], @@ -568,6 +599,33 @@ place_and_route( target_die_utilization_percentage = "10", ) +py_test( + name = "axi_writer_cocotb_test", + srcs = ["axi_writer_cocotb_test.py"], + data = [ + ":axi_writer.v", + ":axi_writer_wrapper.v", + "@com_icarus_iverilog//:iverilog", + "@com_icarus_iverilog//:vvp", + ], + env = {"BUILD_WORKING_DIRECTORY": "sim_build"}, + imports = ["."], + tags = ["manual"], + visibility = ["//xls:xls_users"], + deps = [ + requirement("cocotb"), + requirement("cocotbext-axi"), + requirement("pytest"), + "//xls/common:runfiles", + "//xls/modules/zstd/cocotb:channel", + "//xls/modules/zstd/cocotb:utils", + "//xls/modules/zstd/cocotb:xlsstruct", + "@com_google_absl_py//absl:app", + "@com_google_absl_py//absl/flags", + "@com_google_protobuf//:protobuf_python", + ], +) + xls_dslx_library( name = "axi_stream_add_empty_dslx", srcs = ["axi_stream_add_empty.x"], @@ -686,6 +744,7 @@ verilog_library( name = "mem_writer_verilog_lib", srcs = [ ":mem_writer.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", ], tags = ["manual"], ) @@ -717,3 +776,31 @@ place_and_route( tags = ["manual"], target_die_utilization_percentage = "10", ) + +py_test( + name = "mem_writer_cocotb_test", + srcs = ["mem_writer_cocotb_test.py"], + data = [ + ":mem_writer.v", + ":mem_writer_wrapper.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", + "@com_icarus_iverilog//:iverilog", + "@com_icarus_iverilog//:vvp", + ], + env = {"BUILD_WORKING_DIRECTORY": "sim_build"}, + imports = ["."], + tags = ["manual"], + visibility = ["//xls:xls_users"], + deps = [ + requirement("cocotb"), + requirement("cocotbext-axi"), + requirement("pytest"), + "//xls/common:runfiles", + "//xls/modules/zstd/cocotb:channel", + "//xls/modules/zstd/cocotb:utils", + "//xls/modules/zstd/cocotb:xlsstruct", + "@com_google_absl_py//absl:app", + "@com_google_absl_py//absl/flags", + "@com_google_protobuf//:protobuf_python", + ], +) diff --git a/xls/modules/zstd/memory/README.md b/xls/modules/zstd/memory/README.md index 6a0e4aedfb..17367fa710 100644 --- a/xls/modules/zstd/memory/README.md +++ b/xls/modules/zstd/memory/README.md @@ -87,3 +87,43 @@ The list below shows the usage of the `MemWriter` proc: 3. Wait for the response submitted on the `resp_s` channel, which indicates if the write operation was successful or an error occurred. + +# Cocotb Simulation + +This directory also contains Verilog simulations of the created modules, +which test their interaction with RAM attached to the AXI bus. These Verilog +simulations provide insight into the design's latency and achievable throughput. + +The simulation interacts with verilog file generated from the particular DSLX proc +through a verilog wrapper. The wrapper is used to create an interface that is +compliant with the AXI specification so that the cocotb testbench can interact +with the DUT with the help of an extension tailored for handling the AXI bus. + +## Usage + +1. Run the simulation with the following command: + +``` +bazel run -c opt //xls/modules/zstd/memory:_cocotb_test -- --logtostderr +``` + +2. Observe simulation results, e.g. for `mem_writer_cocotb_test`: + +``` +************************************************************************************************************************************************************* +** TEST STATUS SIM TIME (ns) REAL TIME (s) RATIO (ns/s) ** +************************************************************************************************************************************************************* +** mem_writer_cocotb_test.ram_test_single_burst_1_transfer PASS 1970000.00 0.05 40004933.01 ** +** mem_writer_cocotb_test.ram_test_single_burst_2_transfers PASS 2140000.00 0.04 52208013.80 ** +** mem_writer_cocotb_test.ram_test_single_burst_almost_max_burst_transfer PASS 42620000.00 1.00 42734572.11 ** +** mem_writer_cocotb_test.ram_test_single_burst_max_burst_transfer PASS 43380000.00 1.03 42245987.95 ** +** mem_writer_cocotb_test.ram_test_multiburst_2_full_bursts PASS 85940000.00 2.00 42978720.13 ** +** mem_writer_cocotb_test.ram_test_multiburst_1_full_burst_and_single_transfer PASS 44510000.00 1.02 43487911.16 ** +** mem_writer_cocotb_test.ram_test_multiburst_crossing_4kb_boundary PASS 3740000.00 0.06 60190612.91 ** +** mem_writer_cocotb_test.ram_test_multiburst_crossing_4kb_boundary_with_perfectly_aligned_full_bursts PASS 21440000.00 0.50 42469371.00 ** +** mem_writer_cocotb_test.ram_test_multiburst_crossing_4kb_boundary_with_2_full_bursts_and_1_transfer PASS 87070000.00 2.01 43348812.05 ** +** mem_writer_cocotb_test.ram_test_random PASS 4491230000.00 109.05 41184670.96 ** +************************************************************************************************************************************************************* +** TESTS=10 PASS=10 FAIL=0 SKIP=0 4824040000.01 116.82 41296261.92 ** +************************************************************************************************************************************************************* +``` diff --git a/xls/modules/zstd/memory/axi_writer_cocotb_test.py b/xls/modules/zstd/memory/axi_writer_cocotb_test.py new file mode 100644 index 0000000000..b30876a687 --- /dev/null +++ b/xls/modules/zstd/memory/axi_writer_cocotb_test.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# 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 +import logging +from pathlib import Path + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, Event +from cocotb.binary import BinaryValue +from cocotb_bus.scoreboard import Scoreboard + +from cocotbext.axi.axis import AxiStreamSource, AxiStreamBus, AxiStreamFrame +from cocotbext.axi.axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiWriteBus, AxiAWMonitor, AxiWMonitor, AxiBMonitor, AxiBTransaction, AxiBSource, AxiBSink +from cocotbext.axi.axi_ram import AxiRamWrite +from cocotbext.axi.sparse_memory import SparseMemory + +from xls.modules.zstd.cocotb.channel import ( + XLSChannel, + XLSChannelDriver, + XLSChannelMonitor, +) +from xls.modules.zstd.cocotb.utils import reset, run_test +from xls.modules.zstd.cocotb.xlsstruct import XLSStruct, xls_dataclass + +ID_WIDTH = 4 +ADDR_WIDTH = 16 + +# Override default widths of AXI response signals +signal_widths = {"bresp": 3} +AxiBBus._signal_widths = signal_widths +AxiBTransaction._signal_widths = signal_widths +AxiBSource._signal_widths = signal_widths +AxiBSink._signal_widths = signal_widths +AxiBMonitor._signal_widths = signal_widths + +@xls_dataclass +class AxiWriterRespStruct(XLSStruct): + status: 1 + +@xls_dataclass +class WriteRequestStruct(XLSStruct): + address: ADDR_WIDTH + length: ADDR_WIDTH + +def set_termination_event(monitor, event, transactions): + def terminate_cb(_): + if monitor.stats.received_transactions == transactions: + event.set() + monitor.add_callback(terminate_cb) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test(dut): + GENERIC_ADDR_REQ_CHANNEL = "write_req" + GENERIC_ADDR_RESP_CHANNEL = "write_resp" + AXI_STREAM_CHANNEL = "axi_st_read" + AXI_AW_CHANNEL = "axi_aw" + AXI_W_CHANNEL = "axi_w" + AXI_B_CHANNEL = "axi_b" + + terminate = Event() + + mem_size = 2**ADDR_WIDTH + test_count = 200 + + (addr_req_input, axi_st_input, addr_resp_expect, memory_verification, expected_memory) = generate_test_data_random(test_count, mem_size) + + dut.rst.setimmediatevalue(0) + + clock = Clock(dut.clk, 10, units="us") + cocotb.start_soon(clock.start()) + + resp_bus = XLSChannel(dut, GENERIC_ADDR_RESP_CHANNEL, dut.clk, start_now=True) + + driver_addr_req = XLSChannelDriver(dut, GENERIC_ADDR_REQ_CHANNEL, dut.clk) + driver_axi_st = AxiStreamSource(AxiStreamBus.from_prefix(dut, AXI_STREAM_CHANNEL), dut.clk, dut.rst) + + bus_axi_aw = AxiAWBus.from_prefix(dut, AXI_AW_CHANNEL) + bus_axi_w = AxiWBus.from_prefix(dut, AXI_W_CHANNEL) + bus_axi_b = AxiBBus.from_prefix(dut, AXI_B_CHANNEL) + bus_axi_write = AxiWriteBus(bus_axi_aw, bus_axi_w, bus_axi_b) + + monitor_addr_req = XLSChannelMonitor(dut, GENERIC_ADDR_REQ_CHANNEL, dut.clk, WriteRequestStruct) + monitor_addr_resp = XLSChannelMonitor(dut, GENERIC_ADDR_RESP_CHANNEL, dut.clk, AxiWriterRespStruct) + monitor_axi_aw = AxiAWMonitor(bus_axi_aw, dut.clk, dut.rst) + monitor_axi_w = AxiWMonitor(bus_axi_w, dut.clk, dut.rst) + monitor_axi_b = AxiBMonitor(bus_axi_b, dut.clk, dut.rst) + + set_termination_event(monitor_addr_resp, terminate, test_count) + + memory = AxiRamWrite(bus_axi_write, dut.clk, dut.rst, size=mem_size) + + log = logging.getLogger("cocotb.tb") + log.setLevel(logging.WARNING) + memory.log.setLevel(logging.WARNING) + driver_axi_st.log.setLevel(logging.WARNING) + + scoreboard = Scoreboard(dut) + scoreboard.add_interface(monitor_addr_resp, addr_resp_expect) + + await reset(dut.clk, dut.rst, cycles=10) + await cocotb.start(driver_addr_req.send(addr_req_input)) + await cocotb.start(drive_axi_st(driver_axi_st, axi_st_input)) + await terminate.wait() + + for bundle in memory_verification: + memory_contents = bytearray(memory.read(bundle["base_address"], bundle["length"])) + expected_memory_contents = bytearray(expected_memory.read(bundle["base_address"], bundle["length"])) + assert memory_contents == expected_memory_contents, "{} bytes of memory contents at base address {}:\n{}\nvs\n{}\nHEXDUMP:\n{}\nvs\n{}".format(hex(bundle["length"]), hex(bundle["base_address"]), memory_contents, expected_memory_contents, memory.hexdump(bundle["base_address"], bundle["length"]), expected_memory.hexdump(bundle["base_address"], bundle["length"])) + +@cocotb.coroutine +async def drive_axi_st(driver, inputs): + for axi_st_input in inputs: + await driver.send(axi_st_input) + +def generate_test_data_random(test_count, mem_size): + AXI_AXSIZE_ENCODING_MAX_4B_TRANSFER = 2 # Must be in sync with AXI_AXSIZE_ENCODING enum in axi.x + + addr_req_input = [] + axi_st_input = [] + addr_resp_expect = [] + memory_verification = [] + memory = SparseMemory(mem_size) + + random.seed(1234) + + for i in range(test_count): + xfer_addr = random.randrange(0, mem_size) + # Don't allow unaligned writes + xfer_addr_aligned = (xfer_addr // 4) * 4 + # Make sure we don't write beyond available memory + memory_size_max_xfer_len = mem_size - xfer_addr_aligned + arbitrary_max_xfer_len = 0x5000 # 20kB + xfer_max_len = min(arbitrary_max_xfer_len, memory_size_max_xfer_len) + xfer_len = random.randrange(1, xfer_max_len) + transfer_req = WriteRequestStruct( + address = xfer_addr_aligned, + length = xfer_len, + ) + addr_req_input.append(transfer_req) + + data_to_write = random.randbytes(xfer_len) + axi_st_frame = AxiStreamFrame(tdata=data_to_write, tkeep=[15]*xfer_len, tid=(i % (1 << ID_WIDTH)), tdest=(i % (1 << ID_WIDTH))) + axi_st_input.append(axi_st_frame) + + write_expected_memory(transfer_req, axi_st_frame.tdata, memory) + + memory_bundle = { + "base_address": transfer_req.address, + "length": transfer_req.length, + } + memory_verification.append(memory_bundle) + + addr_resp_expect = [AxiWriterRespStruct(status=False)] * test_count + + return (addr_req_input, axi_st_input, addr_resp_expect, memory_verification, memory) + +def bytes_to_4k_boundary(addr): + AXI_4K_BOUNDARY = 0x1000 + return AXI_4K_BOUNDARY - (addr % AXI_4K_BOUNDARY) + +def write_expected_memory(transfer_req, data_to_write, memory): + """ + Write test data to reference memory keeping the AXI 4kb boundary + by spliting the write requests into smaller ones. + """ + prev_id = 0 + address = transfer_req.address + length = transfer_req.length + + BYTES_IN_TRANSFER = 4 + MAX_AXI_BURST_BYTES = 256 * BYTES_IN_TRANSFER + + while (length > 0): + bytes_to_4k = bytes_to_4k_boundary(address) + new_len = min(length, min(bytes_to_4k, MAX_AXI_BURST_BYTES)) + new_data = data_to_write[prev_id:prev_id+new_len] + memory.write(address, new_data) + address = address + new_len + length = length - new_len + prev_id = prev_id + new_len + +def generate_test_data_arbitrary(mem_size): + AXI_AXSIZE_ENCODING_MAX_4B_TRANSFER = 2 # Must be in sync with AXI_AXSIZE_ENCODING enum in axi.x + + addr_req_input = [] + axi_st_input = [] + addr_resp_expect = [] + memory_verification = [] + memory = SparseMemory(mem_size) + + xfer_addr_begin = [0, 8, 512, 1000, 0x1234, 256] + xfer_len = [1, 2, 4, 8, 0x48d, 4] + assert len(xfer_len) == len(xfer_addr_begin) + testcase_num = len(xfer_addr_begin) # test cases to execute + for i in range(testcase_num): + transfer_req = WriteRequestStruct( + address = xfer_addr_begin[i], + length = xfer_len[i] * 4, # xfer_len[i] transfers per 4 bytes + ) + addr_req_input.append(transfer_req) + + data_chunks = [] + data_bytes = [[(0xEF + j) & 0xFF, 0xBE, 0xAD, 0xDE] for j in range(xfer_len[i])] + data_words = [int.from_bytes(data_bytes[j]) for j in range(xfer_len[i])] + for j in range(xfer_len[i]): + data_chunks += data_bytes[j] + data_to_write = bytearray(data_chunks) + axi_st_frame = AxiStreamFrame(tdata=data_to_write, tkeep=[15]*xfer_len[i], tid=i, tdest=i) + axi_st_input.append(axi_st_frame) + + write_expected_memory(transfer_req, axi_st_frame.tdata, memory) + + memory_bundle = { + "base_address": transfer_req.address, + "length": transfer_req.length, # 4 byte words + } + memory_verification.append(memory_bundle) + + addr_resp_expect = [AxiWriterRespStruct(status=False)] * testcase_num + + return (addr_req_input, axi_st_input, addr_resp_expect, memory_verification, memory) + +if __name__ == "__main__": + toplevel = "axi_writer_wrapper" + verilog_sources = [ + "xls/modules/zstd/memory/axi_writer.v", + "xls/modules/zstd/memory/axi_writer_wrapper.v", + ] + test_module=[Path(__file__).stem] + run_test(toplevel, test_module, verilog_sources) diff --git a/xls/modules/zstd/memory/axi_writer_wrapper.v b/xls/modules/zstd/memory/axi_writer_wrapper.v new file mode 100644 index 0000000000..556f839284 --- /dev/null +++ b/xls/modules/zstd/memory/axi_writer_wrapper.v @@ -0,0 +1,119 @@ +// 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. + +`default_nettype none + +module axi_writer_wrapper ( + input wire clk, + input wire rst, + + output wire write_resp_data, + output wire write_resp_vld, + input wire write_resp_rdy, + + input wire [31:0] write_req_data, + input wire write_req_vld, + output wire write_req_rdy, + + input wire [31:0] axi_st_read_tdata, + input wire [3:0] axi_st_read_tstr, + input wire [3:0] axi_st_read_tkeep, + input wire [0:0] axi_st_read_tlast, + input wire [3:0] axi_st_read_tid, + input wire [3:0] axi_st_read_tdest, + input wire axi_st_read_tvalid, + output wire axi_st_read_tready, + + output wire [3:0] axi_aw_awid, + output wire [15:0] axi_aw_awaddr, + output wire [2:0] axi_aw_awsize, + output wire [7:0] axi_aw_awlen, + output wire [1:0] axi_aw_awburst, + output wire axi_aw_awvalid, + input wire axi_aw_awready, + + output wire [31:0] axi_w_wdata, + output wire [3:0] axi_w_wstrb, + output wire [0:0] axi_w_wlast, + output wire axi_w_wvalid, + input wire axi_w_wready, + + input wire [2:0] axi_b_bresp, + input wire [3:0] axi_b_bid, + input wire axi_b_bvalid, + output wire axi_b_bready + +); + + wire [32:0] axi_writer__ch_axi_aw_data; + wire [36:0] axi_writer__ch_axi_w_data; + wire [ 6:0] axi_writer__ch_axi_b_data; + + wire [15:0] write_req_data_address; + wire [15:0] write_req_data_length; + + wire [48:0] axi_st_read_data; + + assign {write_req_data_address, write_req_data_length} = write_req_data; + + assign { axi_aw_awid, + axi_aw_awaddr, + axi_aw_awsize, + axi_aw_awlen, + axi_aw_awburst } = axi_writer__ch_axi_aw_data; + + assign {axi_w_wdata, axi_w_wstrb, axi_w_wlast} = axi_writer__ch_axi_w_data; + + assign axi_writer__ch_axi_b_data = {axi_b_bresp, axi_b_bid}; + + assign axi_st_read_data = { + axi_st_read_tdata, + axi_st_read_tstr, + axi_st_read_tkeep, + axi_st_read_tlast, + axi_st_read_tid, + axi_st_read_tdest + }; + + axi_writer axi_writer ( + .clk(clk), + .rst(rst), + + .axi_writer__ch_write_req_data(write_req_data), + .axi_writer__ch_write_req_rdy (write_req_rdy), + .axi_writer__ch_write_req_vld (write_req_vld), + + .axi_writer__ch_write_resp_rdy (write_resp_rdy), + .axi_writer__ch_write_resp_vld (write_resp_vld), + .axi_writer__ch_write_resp_data(write_resp_data), + + .axi_writer__ch_axi_aw_data(axi_writer__ch_axi_aw_data), + .axi_writer__ch_axi_aw_rdy (axi_aw_awready), + .axi_writer__ch_axi_aw_vld (axi_aw_awvalid), + + .axi_writer__ch_axi_w_data(axi_writer__ch_axi_w_data), + .axi_writer__ch_axi_w_rdy (axi_w_wready), + .axi_writer__ch_axi_w_vld (axi_w_wvalid), + + .axi_writer__ch_axi_b_data(axi_writer__ch_axi_b_data), + .axi_writer__ch_axi_b_rdy (axi_b_bready), + .axi_writer__ch_axi_b_vld (axi_b_bvalid), + + .axi_writer__ch_axi_st_read_data(axi_st_read_data), + .axi_writer__ch_axi_st_read_rdy (axi_st_read_tready), + .axi_writer__ch_axi_st_read_vld (axi_st_read_tvalid) + ); + + +endmodule : axi_writer_wrapper diff --git a/xls/modules/zstd/memory/mem_reader_cocotb_test.py b/xls/modules/zstd/memory/mem_reader_cocotb_test.py new file mode 100644 index 0000000000..65f683c0b3 --- /dev/null +++ b/xls/modules/zstd/memory/mem_reader_cocotb_test.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python +# 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 +import sys +import warnings +from pathlib import Path + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, Event +from cocotb_bus.scoreboard import Scoreboard +from cocotbext.axi.axi_channels import AxiARBus, AxiRBus, AxiReadBus, AxiRTransaction, AxiRSource, AxiRSink, AxiRMonitor +from cocotbext.axi.axi_ram import AxiRamRead +from cocotbext.axi.sparse_memory import SparseMemory + +from xls.modules.zstd.cocotb.channel import ( + XLSChannel, + XLSChannelDriver, + XLSChannelMonitor, +) +from xls.modules.zstd.cocotb.utils import reset, run_test +from xls.modules.zstd.cocotb.xlsstruct import XLSStruct, xls_dataclass + +# to disable warnings from hexdiff used by cocotb's Scoreboard +warnings.filterwarnings("ignore", category=DeprecationWarning) + +DSLX_DATA_W = 64 +DSLX_ADDR_W = 16 + +AXI_DATA_W = 128 +AXI_ADDR_W = 16 + +LAST_W = 1 +STATUS_W = 1 +ERROR_W = 1 +ID_W = 4 +DEST_W = 4 + +# AXI +AXI_AR_PREFIX = "axi_ar" +AXI_R_PREFIX = "axi_r" + +# MemReader +MEM_READER_REQ_CHANNEL = "req" +MEM_READER_RESP_CHANNEL = "resp" + +# Override default widths of AXI response signals +signal_widths = {"rresp": 3, "rlast": 1} +AxiRBus._signal_widths = signal_widths +AxiRTransaction._signal_widths = signal_widths +AxiRSource._signal_widths = signal_widths +AxiRSink._signal_widths = signal_widths +AxiRMonitor._signal_widths = signal_widths + +@xls_dataclass +class MemReaderReq(XLSStruct): + addr: DSLX_ADDR_W + length: DSLX_ADDR_W + + +@xls_dataclass +class MemReaderResp(XLSStruct): + status: STATUS_W + data: DSLX_DATA_W + length: DSLX_ADDR_W + last: LAST_W + + +@xls_dataclass +class AxiReaderReq(XLSStruct): + addr: AXI_ADDR_W + len: AXI_ADDR_W + + +@xls_dataclass +class AxiStream(XLSStruct): + data: AXI_DATA_W + str: AXI_DATA_W // 8 + keep: AXI_DATA_W // 8 = 0 + last: LAST_W = 0 + id: ID_W = 0 + dest: DEST_W = 0 + + +@xls_dataclass +class AxiReaderError(XLSStruct): + error: ERROR_W + + +@xls_dataclass +class AxiAr(XLSStruct): + id: ID_W + addr: AXI_ADDR_W + region: 4 + len: 8 + size: 3 + burst: 2 + cache: 4 + prot: 3 + qos: 4 + + +@xls_dataclass +class AxiR(XLSStruct): + id: ID_W + data: AXI_DATA_W + resp: 3 + last: 1 + + +def print_callback(name: str = "monitor"): + def _print_callback(transaction): + print(f" [{name}]: {transaction}") + + return _print_callback + + +def set_termination_event(monitor, event, transactions): + def terminate_cb(_): + if monitor.stats.received_transactions == transactions: + print("all transactions received") + event.set() + + monitor.add_callback(terminate_cb) + + +def generate_test_data(test_cases, xfer_base=0x0, seed=1234): + random.seed(seed) + mem_size = 2**AXI_ADDR_W + data_w_div8 = DSLX_DATA_W // 8 + + assert xfer_base < mem_size, "Base address outside the memory span" + + req = [] + resp = [] + mem_writes = {} + + for xfer_offset, xfer_length in test_cases: + xfer_addr = xfer_base + xfer_offset + xfer_max_addr = xfer_addr + xfer_length + + if xfer_length == 0: + req += [MemReaderReq(addr=xfer_addr, length=0)] + resp += [MemReaderResp(status=0, data=0, length=0, last=1)] + + assert xfer_max_addr < mem_size, "Max address outside the memory span" + req += [MemReaderReq(addr=xfer_addr, length=xfer_length)] + + rem = xfer_length % data_w_div8 + for addr in range(xfer_addr, xfer_max_addr - (data_w_div8 - 1), data_w_div8): + last = ((addr + data_w_div8) >= xfer_max_addr) & (rem == 0) + data = random.randint(0, 1 << (data_w_div8 * 8)) + mem_writes.update({addr: data}) + resp += [MemReaderResp(status=0, data=data, length=data_w_div8, last=last)] + + if rem > 0: + addr = xfer_max_addr - rem + mask = (1 << (rem * 8)) - 1 + data = random.randint(0, 1 << (data_w_div8 * 8)) + mem_writes.update({addr: data}) + resp += [MemReaderResp(status=0, data=data & mask, length=rem, last=1)] + + return (req, resp, mem_writes) + + +async def test_mem_reader(dut, req_input, resp_output, mem_contents={}): + clock = Clock(dut.clk, 10, units="us") + cocotb.start_soon(clock.start()) + + mem_reader_resp_bus = XLSChannel( + dut, MEM_READER_RESP_CHANNEL, dut.clk, start_now=True + ) + mem_reader_req_driver = XLSChannelDriver(dut, MEM_READER_REQ_CHANNEL, dut.clk) + mem_reader_resp_monitor = XLSChannelMonitor( + dut, MEM_READER_RESP_CHANNEL, dut.clk, MemReaderResp, callback=print_callback() + ) + + terminate = Event() + set_termination_event(mem_reader_resp_monitor, terminate, len(resp_output)) + + scoreboard = Scoreboard(dut) + scoreboard.add_interface(mem_reader_resp_monitor, resp_output) + + ar_bus = AxiARBus.from_prefix(dut, AXI_AR_PREFIX) + r_bus = AxiRBus.from_prefix(dut, AXI_R_PREFIX) + axi_read_bus = AxiReadBus(ar=ar_bus, r=r_bus) + + mem_size = 2**AXI_ADDR_W + sparse_mem = SparseMemory(mem_size) + for addr, data in mem_contents.items(): + sparse_mem.write(addr, (data).to_bytes(8, "little")) + + memory = AxiRamRead(axi_read_bus, dut.clk, dut.rst, size=mem_size, mem=sparse_mem) + + await reset(dut.clk, dut.rst, cycles=10) + await mem_reader_req_driver.send(req_input) + await terminate.wait() + + +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def mem_reader_zero_length_req(dut): + req, resp, _ = generate_test_data( + xfer_base=0xFFF, test_cases=[(0x101, 0)] + ) + await test_mem_reader(dut, req, resp) + + +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def mem_reader_aligned_transfer_shorter_than_bus(dut): + req, resp, mem_contents = generate_test_data( + xfer_base=0xFFF, test_cases=[(0x101, 1)] + ) + await test_mem_reader(dut, req, resp, mem_contents) + + +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def mem_reader_aligned_transfer_shorter_than_bus1(dut): + req, resp, mem_contents = generate_test_data( + xfer_base=0xFFF, test_cases=[(0x2, 1)] + ) + await test_mem_reader(dut, req, resp, mem_contents) + + +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def mem_reader_aligned_transfer_shorter_than_bus2(dut): + req, resp, mem_contents = generate_test_data( + xfer_base=0xFFF, test_cases=[(0x2, 17)] + ) + await test_mem_reader(dut, req, resp, mem_contents) + + +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def mem_reader_aligned_transfer_shorter_than_bus3(dut): + req, resp, mem_contents = generate_test_data( + xfer_base=0xFFF, test_cases=[(0x0, 0x1000)] + ) + await test_mem_reader(dut, req, resp, mem_contents) + + +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def mem_reader_aligned_transfer_shorter_than_bus4(dut): + req, resp, mem_contents = generate_test_data( + xfer_base=0x1, test_cases=[(0x0, 0xFFF), (0x1000, 0x1)] + ) + await test_mem_reader(dut, req, resp, mem_contents) + + +if __name__ == "__main__": + sys.path.append(str(Path(__file__).parent)) + + toplevel = "mem_reader_wrapper" + verilog_sources = [ + "xls/modules/zstd/xls_fifo_wrapper.v", + "xls/modules/zstd/memory/mem_reader_adv.v", + "xls/modules/zstd/memory/mem_reader_wrapper.v", + ] + test_module = [Path(__file__).stem] + run_test(toplevel, test_module, verilog_sources) diff --git a/xls/modules/zstd/memory/mem_reader_wrapper.v b/xls/modules/zstd/memory/mem_reader_wrapper.v new file mode 100644 index 0000000000..3601bcbb0e --- /dev/null +++ b/xls/modules/zstd/memory/mem_reader_wrapper.v @@ -0,0 +1,111 @@ +// 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. + +`default_nettype none + +module mem_reader_wrapper #( + parameter DSLX_DATA_W = 64, + parameter DSLX_ADDR_W = 16, + parameter AXI_DATA_W = 128, + parameter AXI_ADDR_W = 16, + parameter AXI_DEST_W = 8, + parameter AXI_ID_W = 8, + + parameter CTRL_W = (DSLX_ADDR_W), + parameter REQ_W = (2 * DSLX_ADDR_W), + parameter RESP_W = (1 + DSLX_DATA_W + DSLX_ADDR_W + 1), + parameter AXI_AR_W = (AXI_ID_W + AXI_ADDR_W + 28), + parameter AXI_R_W = (AXI_ID_W + AXI_DATA_W + 4) +) ( + input wire clk, + input wire rst, + + output wire req_rdy, + input wire req_vld, + input wire [REQ_W-1:0] req_data, + + output wire resp_vld, + input wire resp_rdy, + output wire [RESP_W-1:0] resp_data, + + output wire axi_ar_arvalid, + input wire axi_ar_arready, + output wire [ AXI_ID_W-1:0] axi_ar_arid, + output wire [AXI_ADDR_W-1:0] axi_ar_araddr, + output wire [ 3:0] axi_ar_arregion, + output wire [ 7:0] axi_ar_arlen, + output wire [ 2:0] axi_ar_arsize, + output wire [ 1:0] axi_ar_arburst, + output wire [ 3:0] axi_ar_arcache, + output wire [ 2:0] axi_ar_arprot, + output wire [ 3:0] axi_ar_arqos, + + input wire axi_r_rvalid, + output wire axi_r_rready, + input wire [ AXI_ID_W-1:0] axi_r_rid, + input wire [AXI_DATA_W-1:0] axi_r_rdata, + input wire [ 2:0] axi_r_rresp, + input wire axi_r_rlast +); + + wire [AXI_AR_W-1:0] axi_ar_data; + wire axi_ar_rdy; + wire axi_ar_vld; + + assign axi_ar_rdy = axi_ar_arready; + + assign axi_ar_arvalid = axi_ar_vld; + assign { + axi_ar_arid, + axi_ar_araddr, + axi_ar_arregion, + axi_ar_arlen, + axi_ar_arsize, + axi_ar_arburst, + axi_ar_arcache, + axi_ar_arprot, + axi_ar_arqos +} = axi_ar_data; + + wire [AXI_R_W-1:0] axi_r_data; + wire axi_r_vld; + wire axi_r_rdy; + + assign axi_r_data = {axi_r_rid, axi_r_rdata, axi_r_rresp, axi_r_rlast}; + assign axi_r_vld = axi_r_rvalid; + + assign axi_r_rready = axi_r_rdy; + + mem_reader_adv mem_reader_adv ( + .clk(clk), + .rst(rst), + + .mem_reader__req_r_data(req_data), + .mem_reader__req_r_rdy (req_rdy), + .mem_reader__req_r_vld (req_vld), + + .mem_reader__resp_s_data(resp_data), + .mem_reader__resp_s_rdy (resp_rdy), + .mem_reader__resp_s_vld (resp_vld), + + .mem_reader__axi_ar_s_data(axi_ar_data), + .mem_reader__axi_ar_s_rdy (axi_ar_rdy), + .mem_reader__axi_ar_s_vld (axi_ar_vld), + + .mem_reader__axi_r_r_data(axi_r_data), + .mem_reader__axi_r_r_vld (axi_r_vld), + .mem_reader__axi_r_r_rdy (axi_r_rdy) + ); + +endmodule diff --git a/xls/modules/zstd/memory/mem_writer_cocotb_test.py b/xls/modules/zstd/memory/mem_writer_cocotb_test.py new file mode 100644 index 0000000000..6538938e57 --- /dev/null +++ b/xls/modules/zstd/memory/mem_writer_cocotb_test.py @@ -0,0 +1,565 @@ +#!/usr/bin/env python +# 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 +import logging +from pathlib import Path + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, Event +from cocotb.binary import BinaryValue +from cocotb_bus.scoreboard import Scoreboard + +from cocotbext.axi.axis import AxiStreamSource, AxiStreamBus, AxiStreamFrame +from cocotbext.axi.axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiWriteBus, AxiAWMonitor, AxiWMonitor, AxiBMonitor, AxiBTransaction, AxiBSource, AxiBSink +from cocotbext.axi.axi_ram import AxiRamWrite +from cocotbext.axi.sparse_memory import SparseMemory + +from xls.modules.zstd.cocotb.channel import ( + XLSChannel, + XLSChannelDriver, + XLSChannelMonitor, +) +from xls.modules.zstd.cocotb.utils import reset, run_test +from xls.modules.zstd.cocotb.xlsstruct import XLSStruct, xls_dataclass + +DATA_WIDTH = 32 +ADDR_WIDTH = 16 + +# Override default widths of AXI response signals +signal_widths = {"bresp": 3} +AxiBBus._signal_widths = signal_widths +AxiBTransaction._signal_widths = signal_widths +AxiBSource._signal_widths = signal_widths +AxiBSink._signal_widths = signal_widths +AxiBMonitor._signal_widths = signal_widths + +@xls_dataclass +class DataInStruct(XLSStruct): + data: DATA_WIDTH + length: ADDR_WIDTH + last: 1 + +@xls_dataclass +class WriteReqStruct(XLSStruct): + offset: ADDR_WIDTH + length: ADDR_WIDTH + +@xls_dataclass +class AxiWriterRespStruct(XLSStruct): + status: 1 + +@xls_dataclass +class WriteRequestStruct(XLSStruct): + address: ADDR_WIDTH + length: ADDR_WIDTH + +def set_termination_event(monitor, event, transactions): + def terminate_cb(_): + if monitor.stats.received_transactions == transactions: + event.set() + monitor.add_callback(terminate_cb) + +async def test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt): + GENERIC_WRITE_REQ_CHANNEL = "req" + GENERIC_WRITE_RESP_CHANNEL = "resp" + GENERIC_DATA_IN_CHANNEL = "data_in" + AXI_AW_CHANNEL = "axi_aw" + AXI_W_CHANNEL = "axi_w" + AXI_B_CHANNEL = "axi_b" + + terminate = Event() + + dut.rst.setimmediatevalue(0) + + clock = Clock(dut.clk, 10, units="us") + cocotb.start_soon(clock.start()) + + resp_bus = XLSChannel(dut, GENERIC_WRITE_RESP_CHANNEL, dut.clk, start_now=True) + + driver_write_req = XLSChannelDriver(dut, GENERIC_WRITE_REQ_CHANNEL, dut.clk) + driver_data_in = XLSChannelDriver(dut, GENERIC_DATA_IN_CHANNEL, dut.clk) + + bus_axi_aw = AxiAWBus.from_prefix(dut, AXI_AW_CHANNEL) + bus_axi_w = AxiWBus.from_prefix(dut, AXI_W_CHANNEL) + bus_axi_b = AxiBBus.from_prefix(dut, AXI_B_CHANNEL) + bus_axi_write = AxiWriteBus(bus_axi_aw, bus_axi_w, bus_axi_b) + + monitor_write_req = XLSChannelMonitor(dut, GENERIC_WRITE_REQ_CHANNEL, dut.clk, WriteRequestStruct) + monitor_data_in = XLSChannelMonitor(dut, GENERIC_DATA_IN_CHANNEL, dut.clk, WriteRequestStruct) + monitor_write_resp = XLSChannelMonitor(dut, GENERIC_WRITE_RESP_CHANNEL, dut.clk, AxiWriterRespStruct) + monitor_axi_aw = AxiAWMonitor(bus_axi_aw, dut.clk, dut.rst) + monitor_axi_w = AxiWMonitor(bus_axi_w, dut.clk, dut.rst) + monitor_axi_b = AxiBMonitor(bus_axi_b, dut.clk, dut.rst) + + set_termination_event(monitor_write_resp, terminate, resp_cnt) + + memory = AxiRamWrite(bus_axi_write, dut.clk, dut.rst, size=mem_size) + + log = logging.getLogger("cocotb.tb") + log.setLevel(logging.WARNING) + memory.log.setLevel(logging.WARNING) + + scoreboard = Scoreboard(dut) + scoreboard.add_interface(monitor_write_resp, write_resp_expect) + + await reset(dut.clk, dut.rst, cycles=10) + await cocotb.start(driver_write_req.send(write_req_input)) + await cocotb.start(driver_data_in.send(data_in_input)) + + await terminate.wait() + + for bundle in memory_verification: + memory_contents = bytearray(memory.read(bundle["base_address"], bundle["length"])) + expected_memory_contents = bytearray(expected_memory.read(bundle["base_address"], bundle["length"])) + assert memory_contents == expected_memory_contents, "{} bytes of memory contents at base address {}:\n{}\nvs\n{}\nHEXDUMP:\n{}\nvs\n{}".format(hex(bundle["length"]), hex(bundle["base_address"]), memory_contents, expected_memory_contents, memory.hexdump(bundle["base_address"], bundle["length"]), expected_memory.hexdump(bundle["base_address"], bundle["length"])) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_single_burst_1_transfer(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_single_burst_1_transfer) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_single_burst_2_transfers(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_single_burst_2_transfers) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_single_burst_almost_max_burst_transfer(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_single_burst_almost_max_burst_transfer) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_single_burst_max_burst_transfer(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_single_burst_max_burst_transfer) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_multiburst_2_full_bursts(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_multiburst_2_full_bursts) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_multiburst_1_full_burst_and_single_transfer(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_multiburst_1_full_burst_and_single_transfer) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_multiburst_crossing_4kb_boundary(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_multiburst_crossing_4kb_boundary) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_multiburst_crossing_4kb_boundary_with_perfectly_aligned_full_bursts(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_multiburst_crossing_4kb_boundary_with_perfectly_aligned_full_bursts) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_multiburst_crossing_4kb_boundary_with_2_full_bursts_and_1_transfer(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_arbitrary(mem_size, test_cases_multiburst_crossing_4kb_boundary_with_2_full_bursts_and_1_transfer) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def ram_test_random(dut): + mem_size = 2**ADDR_WIDTH + test_count = 200 + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_test_data_random(test_count, mem_size) + await test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) + +def generate_test_data_random(test_count, mem_size): + AXI_AXSIZE_ENCODING_MAX_4B_TRANSFER = 2 # Must be in sync with AXI_AXSIZE_ENCODING enum in axi.x + + write_req_input = [] + data_in_input = [] + write_resp_expect = [] + memory_verification = [] + memory = SparseMemory(mem_size) + + random.seed(1234) + + xfer_baseaddr = 0 + + for i in range(test_count): + # Generate offset from the absolute address + max_xfer_offset = mem_size - xfer_baseaddr + xfer_offset = random.randrange(0, max_xfer_offset) + xfer_addr = xfer_baseaddr + xfer_offset + # Make sure we don't write beyond available memory + memory_size_max_xfer_len = mem_size - xfer_addr + arbitrary_max_xfer_len = 0x5000 # 20kB + xfer_max_len = min(arbitrary_max_xfer_len, memory_size_max_xfer_len) + xfer_len = random.randrange(1, xfer_max_len) + + write_req = WriteReqStruct( + offset = xfer_offset, + length = xfer_len, + ) + write_req_input.append(write_req) + + data_to_write = random.randbytes(xfer_len) + rem = xfer_len % 4 + for j in list(range(0, xfer_len-3, 4)): + last = ((j + 4) >= xfer_len) & (rem == 0) + data_in = DataInStruct( + data = int.from_bytes(data_to_write[j:j+4], byteorder='little'), + length = 4, + last = last + ) + data_in_input.append(data_in) + if (rem > 0): + data_in = DataInStruct( + data = int.from_bytes(data_to_write[-rem:], byteorder='little'), + length = rem, + last = True + ) + data_in_input.append(data_in) + + + transfer_req = WriteRequestStruct( + address = xfer_addr, + length = xfer_len, + ) + write_expected_memory(transfer_req, data_to_write, memory) + + memory_bundle = { + "base_address": transfer_req.address, + "length": transfer_req.length, + } + memory_verification.append(memory_bundle) + + write_resp_expect = [AxiWriterRespStruct(status=False)] * test_count + + return (write_req_input, data_in_input, write_resp_expect, memory_verification, memory, test_count) + +def bytes_to_4k_boundary(addr): + AXI_4K_BOUNDARY = 0x1000 + return AXI_4K_BOUNDARY - (addr % AXI_4K_BOUNDARY) + +def write_expected_memory(transfer_req, data_to_write, memory): + """ + Write test data to reference memory keeping the AXI 4kb boundary + by spliting the write requests into smaller ones. + """ + prev_id = 0 + address = transfer_req.address + length = transfer_req.length + + BYTES_IN_TRANSFER = 4 + MAX_AXI_BURST_BYTES = 256 * BYTES_IN_TRANSFER + + while (length > 0): + bytes_to_4k = bytes_to_4k_boundary(address) + new_len = min(length, min(bytes_to_4k, MAX_AXI_BURST_BYTES)) + new_data = data_to_write[prev_id:prev_id+new_len] + memory.write(address, new_data) + address = address + new_len + length = length - new_len + prev_id = prev_id + new_len + +def generate_test_data_arbitrary(mem_size, test_cases): + AXI_AXSIZE_ENCODING_MAX_4B_TRANSFER = 2 # Must be in sync with AXI_AXSIZE_ENCODING enum in axi.x + test_count = len(test_cases) + + random.seed(1234) + + write_req_input = [] + data_in_input = [] + write_resp_expect = [] + memory_verification = [] + memory = SparseMemory(mem_size) + + xfer_baseaddr = 0x0 + assert xfer_baseaddr < mem_size + + max_xfer_offset = mem_size - xfer_baseaddr + + for i in range(test_count): + test_case = test_cases[i] + xfer_offset = test_case[0] + assert xfer_offset <= max_xfer_offset + xfer_addr = xfer_baseaddr + xfer_offset + # Make sure we don't write beyond available memory + memory_size_max_xfer_len = mem_size - xfer_addr + arbitrary_max_xfer_len = 0x5000 # 20kB + xfer_max_len = min(arbitrary_max_xfer_len, memory_size_max_xfer_len) + xfer_len = test_case[1] + assert xfer_len <= xfer_max_len + + write_req = WriteReqStruct( + offset = xfer_offset, + length = xfer_len, + ) + write_req_input.append(write_req) + + data_to_write = random.randbytes(xfer_len) + rem = xfer_len % 4 + for j in list(range(0, xfer_len-3, 4)): + last = ((j + 4) >= xfer_len) & (rem == 0) + data_in = DataInStruct( + data = int.from_bytes(data_to_write[j:j+4], byteorder='little'), + length = 4, + last = last + ) + data_in_input.append(data_in) + if (rem > 0): + data_in = DataInStruct( + data = int.from_bytes(data_to_write[-rem:], byteorder='little'), + length = rem, + last = True + ) + data_in_input.append(data_in) + + + transfer_req = WriteRequestStruct( + address = xfer_addr, + length = xfer_len, + ) + write_expected_memory(transfer_req, data_to_write, memory) + + memory_bundle = { + "base_address": transfer_req.address, + "length": transfer_req.length, + } + memory_verification.append(memory_bundle) + + write_resp_expect = [AxiWriterRespStruct(status=False)] * test_count + + return (write_req_input, data_in_input, write_resp_expect, memory_verification, memory, test_count) + +if __name__ == "__main__": + toplevel = "mem_writer_wrapper" + verilog_sources = [ + "xls/modules/zstd/xls_fifo_wrapper.v", + "xls/modules/zstd/memory/mem_writer.v", + "xls/modules/zstd/memory/mem_writer_wrapper.v", + ] + test_module=[Path(__file__).stem] + run_test(toplevel, test_module, verilog_sources) + +test_cases_single_burst_1_transfer = [ + # Aligned Address; Aligned Length + (0x0, 0x4), + # Aligned Address; Unaligned Length + (0x10, 0x1), + (0x24, 0x2), + (0x38, 0x3), + # Unaligned Address; Aligned Length + (0x41, 0x4), + (0x52, 0x4), + (0x63, 0x4), + # Unaligned Address; Unaligned Length + (0x71, 0x1), + (0x81, 0x2), + (0x91, 0x3), + (0xa2, 0x1), + (0xb2, 0x2), + (0xc2, 0x3), + (0xd3, 0x1), + (0xe3, 0x2), + (0xf3, 0x3) +] + +test_cases_single_burst_2_transfers = [ + # Aligned Address; Aligned Length + (0x100, 0x8), + # Aligned Address; Unaligned Length + (0x110, 0x5), + (0x120, 0x6), + (0x130, 0x7), + # Unaligned Address; Aligned Length + (0x141, 0x8), + (0x152, 0x8), + (0x163, 0x8), + # Unaligned Address; Unaligned Length + (0x171, 0x5), + (0x182, 0x5), + (0x193, 0x5), + (0x1A1, 0x6), + (0x1B2, 0x6), + (0x1C3, 0x6), + (0x1D1, 0x7), + (0x1E2, 0x7), + (0x1F3, 0x7) +] + +test_cases_single_burst_almost_max_burst_transfer = [ + # Aligned Address; Aligned Length + (0x200, 0x3FC), + # Aligned Address; Unaligned Length + (0x600, 0x3F9), + (0xA00, 0x3FA), + (0x1000, 0x3FB), + # Unaligned Address; Aligned Length + (0x1401, 0x3FC), + (0x1802, 0x3FC), + (0x2003, 0x3FC), + # Unaligned Address; Unaligned Length + (0x2401, 0x3F9), + (0x2802, 0x3F9), + (0x2C03, 0x3F9), + (0x3001, 0x3FA), + (0x3402, 0x3FA), + (0x3803, 0x3FA), + (0x3C01, 0x3FB), + (0x4002, 0x3FB), + (0x4403, 0x3FB) +] + +test_cases_single_burst_max_burst_transfer = [ + # Aligned Address; Aligned Length + (0x4800, 0x400), + # Aligned Address; Unaligned Length + (0x4C00, 0x3FD), + (0x5000, 0x3FE), + (0x5400, 0x3FF), + # Unaligned Address; Aligned Length + (0x5801, 0x400), + (0x6002, 0x400), + (0x6803, 0x400), + # Unaligned Address; Unaligned Length + (0x7001, 0x3FD), + (0x7802, 0x3FD), + (0x8003, 0x3FD), + (0x8801, 0x3FE), + (0x9002, 0x3FE), + (0x9803, 0x3FE), + (0xA001, 0x3FF), + (0xA802, 0x3FF), + (0xB003, 0x3FF) +] + +test_cases_multiburst_2_full_bursts = [ + # Aligned Address; Aligned Length + (0x0400, 0x800), + # Aligned Address; Unaligned Length + (0x1000, 0x7FD), + (0x1800, 0x7FE), + (0x2000, 0x7FF), + # Unaligned Address; Aligned Length + (0x2801, 0x800), + (0x3002, 0x800), + (0x3803, 0x800), + # Unaligned Address; Unaligned Length + (0x4001, 0x7FD), + (0x5002, 0x7FD), + (0x6003, 0x7FD), + (0x7001, 0x7FE), + (0x8002, 0x7FE), + (0x9003, 0x7FE), + (0xA001, 0x7FF), + (0xB002, 0x7FF), + (0xF003, 0x7FF) +] + +test_cases_multiburst_1_full_burst_and_single_transfer = [ + # Aligned Address; Aligned Length; Multi-Burst + (0x0000, 0x404), + # Aligned Address; Unaligned Length; Multi-Burst + (0x0800, 0x401), + (0x1000, 0x402), + (0x1800, 0x403), + # Unaligned Address; Aligned Length; Multi-Burst + (0x2000, 0x404), + (0x2800, 0x404), + (0x3000, 0x404), + # Unaligned Address; Unaligned Length; Multi-Burst + (0x3801, 0x401), + (0x5002, 0x401), + (0x5803, 0x401), + (0x6001, 0x402), + (0x6802, 0x402), + (0x7003, 0x402), + (0x7801, 0x403), + (0x8002, 0x403), + (0x8803, 0x403) +] + +test_cases_multiburst_crossing_4kb_boundary = [ + # Aligned Address; Aligned Length + (0x0FFC, 0x8), + # Aligned Address; Unaligned Length + (0x1FFC, 0x5), + (0x2FFC, 0x6), + (0x3FFC, 0x7), + # Unaligned Address; Aligned Length + (0x4FFD, 0x8), + (0x5FFE, 0x8), + (0x6FFF, 0x8), + # Unaligned Address; Unaligned Length + (0x7FFD, 0x5), + (0x8FFD, 0x6), + (0x9FFD, 0x7), + (0xAFFE, 0x5), + (0xBFFE, 0x6), + (0xCFFE, 0x7), + (0xDFFF, 0x5), + (0xEFFF, 0x6), + # End of address space - wrap around + (0x0FFF, 0x7), +] + +test_cases_multiburst_crossing_4kb_boundary_with_perfectly_aligned_full_bursts = [ + # Aligned Address; Aligned Length; Multi-Burst; crossing 4kB boundary with perfectly aligned full bursts + (0x0C00, 0x800), + # Unaligned Address; Unaligned Length; Multi-Burst; crossing 4kB boundary with perfectly aligned full bursts + (0x1C01, 0x7FF), + (0x2C02, 0x7FE), + (0x3C03, 0x7FD), +] + +test_cases_multiburst_crossing_4kb_boundary_with_2_full_bursts_and_1_transfer = [ + # Aligned Address; Aligned Length + (0x0C04, 0x800), + # Aligned Address; Unaligned Length + (0x1C04, 0x801), + (0x2C04, 0x802), + (0x3C04, 0x803), + # Unaligned Address; Aligned Length + (0x4C01, 0x800), + (0x5C02, 0x800), + (0x6C03, 0x800), + # Unaligned Address; Unaligned Length + (0x7C01, 0x801), + (0x8C02, 0x802), + (0x9C03, 0x803), + (0xAC01, 0x802), + (0xBC02, 0x802), + (0xCC03, 0x802), + (0xDC01, 0x803), + (0xEC02, 0x803), + # End of address space - wrap around + (0x0C03, 0x803), +] diff --git a/xls/modules/zstd/memory/mem_writer_wrapper.v b/xls/modules/zstd/memory/mem_writer_wrapper.v new file mode 100644 index 0000000000..a584984443 --- /dev/null +++ b/xls/modules/zstd/memory/mem_writer_wrapper.v @@ -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. + +`default_nettype none + +module mem_writer_wrapper ( + input wire clk, + input wire rst, + + input wire [31:0] req_data, + input wire req_vld, + output wire req_rdy, + + input wire [48:0] data_in_data, + input wire data_in_vld, + output wire data_in_rdy, + + output wire resp_data, + output wire resp_vld, + input wire resp_rdy, + + output wire [3:0] axi_aw_awid, + output wire [15:0] axi_aw_awaddr, + output wire [2:0] axi_aw_awsize, + output wire [7:0] axi_aw_awlen, + output wire [1:0] axi_aw_awburst, + output wire axi_aw_awvalid, + input wire axi_aw_awready, + + output wire [31:0] axi_w_wdata, + output wire [3:0] axi_w_wstrb, + output wire [0:0] axi_w_wlast, + output wire axi_w_wvalid, + input wire axi_w_wready, + + input wire [2:0] axi_b_bresp, + input wire [3:0] axi_b_bid, + input wire axi_b_bvalid, + output wire axi_b_bready +); + + wire [15:0] req_f_addr; + wire [15:0] req_f_length; + + wire [31:0] data_in_f_data; + wire [15:0] data_in_f_length; + wire [0:0] data_in_f_last; + + wire [36:0] axi_w_data; + wire axi_w_vld; + wire axi_w_rdy; + + wire [32:0] axi_aw_data; + wire axi_aw_vld; + wire axi_aw_rdy; + + wire [6:0] axi_b_data; + wire axi_b_rdy; + wire axi_b_vld; + + assign {req_f_addr, req_f_length} = req_data; + + assign {data_in_f_data, data_in_f_length, data_in_f_last} = data_in_data; + + assign {axi_aw_awid, axi_aw_awaddr, axi_aw_awsize, axi_aw_awlen, axi_aw_awburst} = axi_aw_data; + assign axi_aw_awvalid = axi_aw_vld; + assign axi_aw_rdy = axi_aw_awready; + + assign {axi_w_wdata, axi_w_wstrb, axi_w_wlast} = axi_w_data; + assign axi_w_wvalid = axi_w_vld; + assign axi_w_rdy = axi_w_wready; + + assign axi_b_data = {axi_b_bresp, axi_b_bid}; + assign axi_b_vld = axi_b_bvalid; + assign axi_b_bready = axi_b_rdy; + + wire [15:0] axi_writer_write_req_address; + wire [15:0] axi_writer_write_req_length; + wire [ 0:0] axi_writer_write_req_valid; + wire [ 0:0] axi_writer_write_req_ready; + + wire [15:0] padding_write_req_address; + wire [15:0] padding_write_req_length; + wire [ 0:0] padding_write_req_valid; + wire [ 0:0] padding_write_req_ready; + + wire [31:0] axi_stream_raw_tdata; + wire [ 3:0] axi_stream_raw_tstr; + wire [ 3:0] axi_stream_raw_tkeep; + wire [ 0:0] axi_stream_raw_tlast; + wire [ 3:0] axi_stream_raw_tid; + wire [ 3:0] axi_stream_raw_tdest; + wire [ 0:0] axi_stream_raw_tvalid; + wire [ 0:0] axi_stream_raw_tready; + + wire [31:0] axi_stream_padded_tdata; + wire [ 3:0] axi_stream_padded_tstr; + wire [ 3:0] axi_stream_padded_tkeep; + wire [ 0:0] axi_stream_padded_tlast; + wire [ 3:0] axi_stream_padded_tid; + wire [ 3:0] axi_stream_padded_tdest; + wire [ 0:0] axi_stream_padded_tvalid; + wire [ 0:0] axi_stream_padded_tready; + + assign {axi_writer_write_req_address, axi_writer_write_req_length} = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__axi_writer_req_data; + assign axi_writer_write_req_valid = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__axi_writer_req_vld; + assign axi_writer_write_req_ready = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__axi_writer_req_rdy; + + assign {padding_write_req_address, padding_write_req_length} = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__padding_req_data; + assign padding_write_req_valid = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__padding_req_vld; + assign padding_write_req_ready = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__padding_req_rdy; + + assign { axi_stream_raw_tdata, + axi_stream_raw_tstr, + axi_stream_raw_tkeep, + axi_stream_raw_tlast, + axi_stream_raw_tid, + axi_stream_raw_tdest } = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__axi_st_raw_data; + assign axi_stream_raw_tvalid = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__axi_st_raw_vld; + assign axi_stream_raw_tready = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__16_32_4_2_4_4_2_next_inst0.mem_writer__axi_st_raw_rdy; + + assign { axi_stream_padded_tdata, + axi_stream_padded_tstr, + axi_stream_padded_tkeep, + axi_stream_padded_tlast, + axi_stream_padded_tid, + axi_stream_padded_tdest } = mem_writer.__xls_modules_zstd_memory_axi_writer__MemWriterInst__MemWriter_0__AxiWriter_0__16_32_4_4_4_2_next_inst2.mem_writer__axi_st_padded_data; + assign axi_stream_padded_tvalid = mem_writer.__xls_modules_zstd_memory_axi_writer__MemWriterInst__MemWriter_0__AxiWriter_0__16_32_4_4_4_2_next_inst2.mem_writer__axi_st_padded_vld; + assign axi_stream_padded_tready = mem_writer.__xls_modules_zstd_memory_axi_writer__MemWriterInst__MemWriter_0__AxiWriter_0__16_32_4_4_4_2_next_inst2.mem_writer__axi_st_padded_rdy; + + mem_writer mem_writer ( + .clk(clk), + .rst(rst), + + // MemWriter Write Request + .mem_writer__req_in_r_data(req_data), + .mem_writer__req_in_r_vld (req_vld), + .mem_writer__req_in_r_rdy (req_rdy), + + // Data to write + .mem_writer__data_in_r_data(data_in_data), + .mem_writer__data_in_r_vld (data_in_vld), + .mem_writer__data_in_r_rdy (data_in_rdy), + + // Response channel + .mem_writer__resp_s_data(resp_data), + .mem_writer__resp_s_rdy (resp_rdy), + .mem_writer__resp_s_vld (resp_vld), + + // Memory AXI + .mem_writer__axi_w_s_data(axi_w_data), + .mem_writer__axi_w_s_vld (axi_w_vld), + .mem_writer__axi_w_s_rdy (axi_w_rdy), + + .mem_writer__axi_aw_s_data(axi_aw_data), + .mem_writer__axi_aw_s_vld (axi_aw_vld), + .mem_writer__axi_aw_s_rdy (axi_aw_rdy), + + .mem_writer__axi_b_r_data(axi_b_data), + .mem_writer__axi_b_r_vld (axi_b_vld), + .mem_writer__axi_b_r_rdy (axi_b_rdy) + ); + +endmodule : mem_writer_wrapper diff --git a/xls/modules/zstd/xls_fifo_wrapper.v b/xls/modules/zstd/xls_fifo_wrapper.v new file mode 100644 index 0000000000..1336042b29 --- /dev/null +++ b/xls/modules/zstd/xls_fifo_wrapper.v @@ -0,0 +1,53 @@ +// simple fifo implementation +module xls_fifo_wrapper ( +clk, rst, +push_ready, push_data, push_valid, +pop_ready, pop_data, pop_valid); + parameter Width = 32, + Depth = 32, + EnableBypass = 0, + RegisterPushOutputs = 1, + RegisterPopOutputs = 1; + localparam AddrWidth = $clog2(Depth) + 1; + input wire clk; + input wire rst; + output wire push_ready; + input wire [Width-1:0] push_data; + input wire push_valid; + input wire pop_ready; + output wire [Width-1:0] pop_data; + output wire pop_valid; + + // Require depth be 1 and bypass disabled. + //initial begin + // if (EnableBypass || Depth != 1 || !RegisterPushOutputs || RegisterPopOutputs) begin + // // FIFO configuration not supported. + // $fatal(1); + // end + //end + + + reg [Width-1:0] mem; + reg full; + + assign push_ready = !full; + assign pop_valid = full; + assign pop_data = mem; + + always @(posedge clk) begin + if (rst == 1'b1) begin + full <= 1'b0; + end else begin + if (push_valid && push_ready) begin + mem <= push_data; + full <= 1'b1; + end else if (pop_valid && pop_ready) begin + mem <= mem; + full <= 1'b0; + end else begin + mem <= mem; + full <= full; + end + end + end +endmodule