From 81b6aea19f91c3f831db9b84b9a7d561a7b5fa7c 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 01/46] 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 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/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)})" From 81f16f4be2ddc8a8d04312a939e606c386e708b6 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Thu, 19 Sep 2024 12:51:55 +0200 Subject: [PATCH 02/46] modules/zstd/memory/MemReader: Add cocotb tests Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/BUILD | 28 ++ .../zstd/memory/mem_reader_cocotb_test.py | 271 ++++++++++++++++++ xls/modules/zstd/memory/mem_reader_wrapper.v | 111 +++++++ 3 files changed, 410 insertions(+) create mode 100644 xls/modules/zstd/memory/mem_reader_cocotb_test.py create mode 100644 xls/modules/zstd/memory/mem_reader_wrapper.v diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index ca5e0a155f..c336408531 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", @@ -489,6 +490,33 @@ 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", + "@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"], 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..845a321159 --- /dev/null +++ b/xls/modules/zstd/memory/mem_reader_cocotb_test.py @@ -0,0 +1,271 @@ +#!/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/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 From cfab4b984d242a335b3bc2e4bf6f8c97df574105 Mon Sep 17 00:00:00 2001 From: Michal Czyz Date: Thu, 19 Sep 2024 13:00:45 +0200 Subject: [PATCH 03/46] modules/zstd/memory/AxiWriter: Add cocotb test Co-authred-by: Pawel Czarnecki Co-authred-by: Robert Winkler Signed-off-by: Michal Czyz Signed-off-by: Pawel Czarnecki Signed-off-by: Robert Winkler --- xls/modules/zstd/memory/BUILD | 27 ++ .../zstd/memory/axi_writer_cocotb_test.py | 245 ++++++++++++++++++ xls/modules/zstd/memory/axi_writer_wrapper.v | 119 +++++++++ 3 files changed, 391 insertions(+) create mode 100644 xls/modules/zstd/memory/axi_writer_cocotb_test.py create mode 100644 xls/modules/zstd/memory/axi_writer_wrapper.v diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index c336408531..672ef96d85 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -596,6 +596,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"], 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 From ddd66c8308ee70f624a40b53e3fbe07c1a404c62 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Thu, 19 Sep 2024 13:05:44 +0200 Subject: [PATCH 04/46] modules/zstd/memory/MemWriter: Add cocotb test Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/BUILD | 27 + .../zstd/memory/mem_writer_cocotb_test.py | 564 ++++++++++++++++++ xls/modules/zstd/memory/mem_writer_wrapper.v | 175 ++++++ 3 files changed, 766 insertions(+) create mode 100644 xls/modules/zstd/memory/mem_writer_cocotb_test.py create mode 100644 xls/modules/zstd/memory/mem_writer_wrapper.v diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 672ef96d85..176a0b3f20 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -772,3 +772,30 @@ 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", + "@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/mem_writer_cocotb_test.py b/xls/modules/zstd/memory/mem_writer_cocotb_test.py new file mode 100644 index 0000000000..9f3c8add01 --- /dev/null +++ b/xls/modules/zstd/memory/mem_writer_cocotb_test.py @@ -0,0 +1,564 @@ +#!/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/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 From e56d5d7b3a04d575a18242d6a6f02a953fcff7b7 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Fri, 4 Oct 2024 14:06:44 +0200 Subject: [PATCH 05/46] modules/zstd/memory/README: Describe verilog simulation Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/README.md | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) 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 ** +************************************************************************************************************************************************************* +``` From 10f31a6f1cc20a6bd4aac4048590d8e5bc4088ba Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Fri, 4 Oct 2024 14:09:56 +0200 Subject: [PATCH 06/46] CI/modules-zstd: Add calls to verilog simulation targets Signed-off-by: Pawel Czarnecki --- .github/workflows/modules-zstd.yml | 5 +++++ 1 file changed, 5 insertions(+) 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: | From d9d9d4ea4cf63f617898b55da90145b9f813c12b Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 12 Nov 2024 14:09:29 +0100 Subject: [PATCH 07/46] modules/zstd/BUILD: increase pipeline_stages for DecoderMux proc Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 8717497922..94b6b3ecdb 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -506,7 +506,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 +520,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"], From 3b11d916c5f7c1bc396c293dae8e3689623242a7 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 31 Dec 2024 15:55:34 +0100 Subject: [PATCH 08/46] xls/modules/zstd: expose fifo verilog module Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 2 + xls/modules/zstd/memory/BUILD | 5 ++ .../zstd/memory/mem_reader_cocotb_test.py | 1 + .../zstd/memory/mem_writer_cocotb_test.py | 1 + xls/modules/zstd/xls_fifo_wrapper.v | 53 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 xls/modules/zstd/xls_fifo_wrapper.v diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 94b6b3ecdb..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 = [ diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 176a0b3f20..4e9cfe2b81 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -403,6 +403,7 @@ verilog_library( name = "mem_reader_verilog_lib", srcs = [ ":mem_reader.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", ], tags = ["manual"], ) @@ -458,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"], ) @@ -496,6 +498,7 @@ py_test( data = [ ":mem_reader_adv.v", ":mem_reader_wrapper.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", "@com_icarus_iverilog//:iverilog", "@com_icarus_iverilog//:vvp", ], @@ -741,6 +744,7 @@ verilog_library( name = "mem_writer_verilog_lib", srcs = [ ":mem_writer.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", ], tags = ["manual"], ) @@ -779,6 +783,7 @@ py_test( data = [ ":mem_writer.v", ":mem_writer_wrapper.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", "@com_icarus_iverilog//:iverilog", "@com_icarus_iverilog//:vvp", ], diff --git a/xls/modules/zstd/memory/mem_reader_cocotb_test.py b/xls/modules/zstd/memory/mem_reader_cocotb_test.py index 845a321159..65f683c0b3 100644 --- a/xls/modules/zstd/memory/mem_reader_cocotb_test.py +++ b/xls/modules/zstd/memory/mem_reader_cocotb_test.py @@ -264,6 +264,7 @@ async def mem_reader_aligned_transfer_shorter_than_bus4(dut): 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", ] diff --git a/xls/modules/zstd/memory/mem_writer_cocotb_test.py b/xls/modules/zstd/memory/mem_writer_cocotb_test.py index 9f3c8add01..6538938e57 100644 --- a/xls/modules/zstd/memory/mem_writer_cocotb_test.py +++ b/xls/modules/zstd/memory/mem_writer_cocotb_test.py @@ -362,6 +362,7 @@ def generate_test_data_arbitrary(mem_size, test_cases): 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", ] 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 From 05988cdd24539c4f3a6d09fe55a30f9343682c4a Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Thu, 3 Oct 2024 14:32:36 +0200 Subject: [PATCH 09/46] modules/rle/common: Add RLE output struct Signed-off-by: Maciej Torhan --- xls/modules/rle/rle_common.x | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/xls/modules/rle/rle_common.x b/xls/modules/rle/rle_common.x index 8b1217ff2c..9410c3e9e9 100644 --- a/xls/modules/rle/rle_common.x +++ b/xls/modules/rle/rle_common.x @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import std; + // This file defines RLE common data structures // @@ -24,6 +26,15 @@ pub struct PlainData { last: bool, // flush RLE } +// Structure contains multiple uncompressed symbols. +// Structure is used as an output from a advanced RLE decoder. +// FIXME: add default value DATA_WIDTH_LOG2: u32 = {std::clog2(DATA_WIDTH + u32:1) } (https://github.com/google/xls/issues/1425) +pub struct PlainDataWithLen { + symbols: uN[DATA_WIDTH], + length: uN[DATA_WIDTH_LOG2], + last: bool, +} + // Structure contains compressed (symbol, counter) pairs. // Structure is used as an output from RLE encoder and // as an input to RLE decoder. From 7c112c0766b911a4b41e67012e9f91513e3cbeb0 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 8 Oct 2024 15:59:05 +0200 Subject: [PATCH 10/46] modules/zstd: Remove MagicNumberDecoder Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 18 -------- xls/modules/zstd/magic.x | 89 ---------------------------------------- 2 files changed, 107 deletions(-) delete mode 100644 xls/modules/zstd/magic.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 5a229bf930..78b1762f62 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -133,23 +133,6 @@ place_and_route( target_die_utilization_percentage = "10", ) -xls_dslx_library( - name = "magic_dslx", - srcs = [ - "magic.x", - ], - deps = [ - ":buffer_dslx", - ], -) - -xls_dslx_test( - name = "magic_dslx_test", - dslx_test_args = {"compare": "jit"}, - library = ":magic_dslx", - tags = ["manual"], -) - cc_library( name = "data_generator", srcs = ["data_generator.cc"], @@ -947,7 +930,6 @@ xls_dslx_library( ":common_dslx", ":frame_header_dslx", ":frame_header_test_dslx", - ":magic_dslx", ":ram_printer_dslx", ":repacketizer_dslx", ":sequence_executor_dslx", diff --git a/xls/modules/zstd/magic.x b/xls/modules/zstd/magic.x deleted file mode 100644 index 196f2f528f..0000000000 --- a/xls/modules/zstd/magic.x +++ /dev/null @@ -1,89 +0,0 @@ -// 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. - -// This file contains utilities related to ZSTD magic number parsing -// More information about the ZSTD Magic Number can be found in: -// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1 - -import std; -import xls.modules.zstd.buffer as buff; - -type Buffer = buff::Buffer; -type BufferStatus = buff::BufferStatus; - -// Magic number value, as in: -// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1 -const MAGIC_NUMBER = u32:0xFD2FB528; - -// Status values reported by the magic number parsing function -pub enum MagicStatus: u2 { - OK = 0, - CORRUPTED = 1, - NO_ENOUGH_DATA = 2, -} - -// structure for returning results of magic number parsing -pub struct MagicResult { - buffer: Buffer, - status: MagicStatus, -} - -// Parses a Buffer and checks if it contains the magic number. -// The buffer is assumed to contain a valid beginning of the ZSTD file. -// The function returns MagicResult structure with the buffer after parsing -// the magic number and the status of the operation. On failure, the returned -// buffer is the same as the input buffer. -pub fn parse_magic_number(buffer: Buffer) -> MagicResult { - let (result, data) = buff::buffer_fixed_pop_checked(buffer); - - match result.status { - BufferStatus::OK => { - if data == MAGIC_NUMBER { - trace_fmt!("parse_magic_number: Magic number found!"); - MagicResult {status: MagicStatus::OK, buffer: result.buffer} - } else { - trace_fmt!("parse_magic_number: Magic number not found!"); - MagicResult {status: MagicStatus::CORRUPTED, buffer: buffer} - } - }, - _ => { - trace_fmt!("parse_frame_header: Not enough data to parse magic number!"); - MagicResult {status: MagicStatus::NO_ENOUGH_DATA, buffer: buffer} - } - } -} - -#[test] -fn test_parse_magic_number() { - let buffer = Buffer { content: MAGIC_NUMBER, length: u32:32}; - let result = parse_magic_number(buffer); - assert_eq(result, MagicResult { - status: MagicStatus::OK, - buffer: Buffer {content: u32:0, length: u32:0}, - }); - - let buffer = Buffer { content: u32:0x12345678, length: u32:32}; - let result = parse_magic_number(buffer); - assert_eq(result, MagicResult { - status: MagicStatus::CORRUPTED, - buffer: buffer - }); - - let buffer = Buffer { content: u32:0x1234, length: u32:16}; - let result = parse_magic_number(buffer); - assert_eq(result, MagicResult { - status: MagicStatus::NO_ENOUGH_DATA, - buffer: buffer, - }); -} From 577b1f01b491cfb989a7507de6453e82c8e4bc4e Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 8 Oct 2024 16:07:13 +0200 Subject: [PATCH 11/46] modules/zstd: Remove BlockDecoder proc Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 89 ------------------ xls/modules/zstd/block_dec.x | 170 ----------------------------------- 2 files changed, 259 deletions(-) delete mode 100644 xls/modules/zstd/block_dec.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 78b1762f62..aa7648065c 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -626,94 +626,6 @@ place_and_route( target_die_utilization_percentage = "5", ) -xls_dslx_library( - name = "block_dec_dslx", - srcs = [ - "block_dec.x", - ], - deps = [ - ":common_dslx", - ":dec_demux_dslx", - ":dec_mux_dslx", - ":raw_block_dec_dslx", - ":rle_block_dec_dslx", - ], -) - -xls_dslx_test( - name = "block_dec_dslx_test", - dslx_test_args = {"compare": "jit"}, - library = ":block_dec_dslx", - tags = ["manual"], -) - -xls_dslx_verilog( - name = "block_dec_verilog", - codegen_args = { - "module_name": "BlockDecoder", - "delay_model": "asap7", - "pipeline_stages": "2", - "reset": "rst", - "use_system_verilog": "false", - }, - dslx_top = "BlockDecoder", - library = ":block_dec_dslx", - # TODO: 2024-01-15: Workaround for https://github.com/google/xls/issues/869 - # Force proc inlining and set last internal proc as top proc for IR optimization - opt_ir_args = { - "inline_procs": "true", - "top": "__xls_modules_zstd_dec_mux__BlockDecoder__DecoderMux_0_next", - }, - tags = ["manual"], - verilog_file = "block_dec.v", -) - -xls_benchmark_ir( - name = "block_dec_opt_ir_benchmark", - src = ":block_dec_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "2", - "delay_model": "asap7", - }, - tags = ["manual"], -) - -verilog_library( - name = "block_dec_verilog_lib", - srcs = [ - ":block_dec.v", - ], - tags = ["manual"], -) - -synthesize_rtl( - name = "block_dec_synth_asap7", - standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", - tags = ["manual"], - top_module = "BlockDecoder", - deps = [ - ":block_dec_verilog_lib", - ], -) - -benchmark_synth( - name = "block_dec_benchmark_synth", - synth_target = ":block_dec_synth_asap7", - tags = ["manual"], -) - -place_and_route( - name = "block_dec_place_and_route", - clock_period = "750", - core_padding_microns = 2, - min_pin_distance = "0.5", - placement_density = "0.30", - stop_after_step = "global_routing", - synthesized_rtl = ":block_dec_synth_asap7", - tags = ["manual"], - target_die_utilization_percentage = "10", -) - xls_dslx_library( name = "ram_printer_dslx", srcs = ["ram_printer.x"], @@ -924,7 +836,6 @@ xls_dslx_library( "zstd_dec.x", ], deps = [ - ":block_dec_dslx", ":block_header_dslx", ":buffer_dslx", ":common_dslx", diff --git a/xls/modules/zstd/block_dec.x b/xls/modules/zstd/block_dec.x deleted file mode 100644 index f068a8e8b6..0000000000 --- a/xls/modules/zstd/block_dec.x +++ /dev/null @@ -1,170 +0,0 @@ -// 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 xls.modules.zstd.common; -import xls.modules.zstd.dec_demux as demux; -import xls.modules.zstd.raw_block_dec as raw; -import xls.modules.zstd.rle_block_dec as rle; -import xls.modules.zstd.dec_mux as mux; - -type BlockDataPacket = common::BlockDataPacket; -type BlockData = common::BlockData; -type BlockPacketLength = common::BlockPacketLength; -type ExtendedBlockDataPacket = common::ExtendedBlockDataPacket; -type CopyOrMatchContent = common::CopyOrMatchContent; -type CopyOrMatchLength = common::CopyOrMatchLength; -type SequenceExecutorPacket = common::SequenceExecutorPacket; -type SequenceExecutorMessageType = common::SequenceExecutorMessageType; - -// Proc responsible for connecting internal procs used in Block data decoding. -// It handles incoming block data packets by redirecting those to demuxer which passes those to -// block decoder procs specific for given block type. Results are then gathered by mux which -// transfers decoded data further. The connections are visualised on the following diagram: -// -// Block Decoder -// ┌───────────────────────────────────────┐ -// │ Raw Block Decoder │ -// │ ┌───────────────────┐ │ -// │ ┌─► ├┐ │ -// │ Demux │ └───────────────────┘│ Mux │ -// │┌─────┐│ Rle Block Decoder │ ┌─────┐│ -// ││ ├┘ ┌───────────────────┐└─► ││ -// ──┼► ├──► ├──► ├┼─► -// ││ ├┐ └───────────────────┘┌─► ││ -// │└─────┘│ Cmp Block Decoder │ └─────┘│ -// │ │ ┌───────────────────┐│ │ -// │ └─► ├┘ │ -// │ └───────────────────┘ │ -// └───────────────────────────────────────┘ - -pub proc BlockDecoder { - input_r: chan in; - output_s: chan out; - - config (input_r: chan in, output_s: chan out) { - let (demux_raw_s, demux_raw_r) = chan("demux_raw"); - let (demux_rle_s, demux_rle_r) = chan("demux_rle"); - let (demux_cmp_s, demux_cmp_r) = chan("demux_cmp"); - let (mux_raw_s, mux_raw_r) = chan("mux_raw"); - let (mux_rle_s, mux_rle_r) = chan("mux_rle"); - let (mux_cmp_s, mux_cmp_r) = chan("mux_cmp"); - - spawn demux::DecoderDemux(input_r, demux_raw_s, demux_rle_s, demux_cmp_s); - spawn raw::RawBlockDecoder(demux_raw_r, mux_raw_s); - spawn rle::RleBlockDecoder(demux_rle_r, mux_rle_s); - // TODO(antmicro): 2023-11-28 change to compressed block decoder proc - spawn raw::RawBlockDecoder(demux_cmp_r, mux_cmp_s); - spawn mux::DecoderMux(mux_raw_r, mux_rle_r, mux_cmp_r, output_s); - - (input_r, output_s) - } - - init { } - - next(state: ()) { } -} - -#[test_proc] -proc BlockDecoderTest { - terminator: chan out; - input_s: chan out; - output_r: chan in; - - init {} - - config (terminator: chan out) { - let (input_s, input_r) = chan("input"); - let (output_s, output_r) = chan("output"); - - spawn BlockDecoder(input_r, output_s); - - (terminator, input_s, output_r) - } - - next(state: ()) { - let tok = join(); - let EncodedDataBlocksPackets: BlockDataPacket[13] = [ - // RAW Block 1 byte - BlockDataPacket { id: u32:0, last: true, last_block: false, data: BlockData:0xDE000008, length: BlockPacketLength:32 }, - // RAW Block 2 bytes - BlockDataPacket { id: u32:1, last: true, last_block: false, data: BlockData:0xDEAD000010, length: BlockPacketLength:40 }, - // RAW Block 4 bytes - BlockDataPacket { id: u32:2, last: true, last_block: false, data: BlockData:0xDEADBEEF000020, length: BlockPacketLength:56 }, - // RAW Block 5 bytes (block header takes one full packet) - BlockDataPacket { id: u32:3, last: true, last_block: false, data: BlockData:0xDEADBEEFEF000028, length: BlockPacketLength:64 }, - // RAW Block 24 bytes (multi-packet block header with unaligned data in the last packet) - BlockDataPacket { id: u32:4, last: false, last_block: false, data: BlockData:0x12345678900000C0, length: BlockPacketLength:64 }, - BlockDataPacket { id: u32:4, last: false, last_block: false, data: BlockData:0x1234567890ABCDEF, length: BlockPacketLength:64 }, - BlockDataPacket { id: u32:4, last: false, last_block: false, data: BlockData:0xFEDCBA0987654321, length: BlockPacketLength:64 }, - BlockDataPacket { id: u32:4, last: true, last_block: false, data: BlockData:0xF0F0F0, length: BlockPacketLength:24 }, - - // RLE Block 1 byte - BlockDataPacket { id: u32:5, last: true, last_block: false, data: BlockData:0x6700000a, length: BlockPacketLength:32 }, - // RLE Block 2 bytes - BlockDataPacket { id: u32:6, last: true, last_block: false, data: BlockData:0x45000012, length: BlockPacketLength:32 }, - // RLE Block 4 bytes - BlockDataPacket { id: u32:7, last: true, last_block: false, data: BlockData:0x23000022, length: BlockPacketLength:32 }, - // RLE Block 8 bytes (block takes one full packet) - BlockDataPacket { id: u32:8, last: true, last_block: false, data: BlockData:0x10000042, length: BlockPacketLength:32 }, - // RLE Block 26 bytes (multi-packet block header with unaligned data in the last packet) - BlockDataPacket { id: u32:9, last: true, last_block: true, data: BlockData:0xDE0000d2, length: BlockPacketLength:32 }, - ]; - - let tok = for ((counter, block_packet), tok): ((u32, BlockDataPacket), token) in enumerate(EncodedDataBlocksPackets) { - let tok = send(tok, input_s, block_packet); - trace_fmt!("Sent #{} encoded block packet, {:#x}", counter + u32:1, block_packet); - (tok) - }(tok); - - let DecodedDataBlocksPackets: SequenceExecutorPacket[16] = [ - // RAW Block 1 byte - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDE, length: CopyOrMatchLength:8 }, - // RAW Block 2 bytes - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEAD, length: CopyOrMatchLength:16 }, - // RAW Block 4 bytes - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEADBEEF, length: CopyOrMatchLength:32 }, - // RAW Block 5 bytes (block header takes one full packet) - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEADBEEFEF, length: CopyOrMatchLength:40 }, - // RAW Block 24 bytes (multi-packet block header with unaligned data in the last packet) - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0x1234567890, length: CopyOrMatchLength:40 }, - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0x1234567890ABCDEF, length: CopyOrMatchLength:64 }, - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xFEDCBA0987654321, length: CopyOrMatchLength:64 }, - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xF0F0F0, length: CopyOrMatchLength:24 }, - - // RLE Block 1 byte - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0x67, length: CopyOrMatchLength:8 }, - // RLE Block 2 bytes - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0x4545, length: CopyOrMatchLength:16 }, - // RLE Block 4 bytes - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0x23232323, length: CopyOrMatchLength:32 }, - // RLE Block 8 bytes (block takes one full packet) - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0x1010101010101010, length: CopyOrMatchLength:64 }, - // RLE Block 26 bytes (multi-packet block header with unaligned data in the last packet) - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEDEDEDEDEDEDEDE, length: CopyOrMatchLength:64 }, - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEDEDEDEDEDEDEDE, length: CopyOrMatchLength:64 }, - SequenceExecutorPacket { last: false, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEDEDEDEDEDEDEDE, length: CopyOrMatchLength:64 }, - SequenceExecutorPacket { last: true, msg_type: SequenceExecutorMessageType::LITERAL, content: CopyOrMatchContent:0xDEDE, length: CopyOrMatchLength:16 }, - ]; - - let tok = for ((counter, expected_block_packet), tok): ((u32, SequenceExecutorPacket), token) in enumerate(DecodedDataBlocksPackets) { - let (tok, decoded_block_packet) = recv(tok, output_r); - trace_fmt!("Received #{} decoded block packet, data: 0x{:x}", counter + u32:1, decoded_block_packet); - trace_fmt!("Expected #{} decoded block packet, data: 0x{:x}", counter + u32:1, expected_block_packet); - assert_eq(decoded_block_packet, expected_block_packet); - (tok) - }(tok); - - send(tok, terminator, true); - } -} From fcc7d82001abe1d33b8e8d34cfccdb3aa4c79945 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 8 Oct 2024 16:09:42 +0200 Subject: [PATCH 12/46] modules/zstd: Remove DecDemux proc Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 79 ---------- xls/modules/zstd/dec_demux.x | 273 ----------------------------------- 2 files changed, 352 deletions(-) delete mode 100644 xls/modules/zstd/dec_demux.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index aa7648065c..015ce882b5 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -547,85 +547,6 @@ place_and_route( target_die_utilization_percentage = "10", ) -xls_dslx_library( - name = "dec_demux_dslx", - srcs = [ - "dec_demux.x", - ], - deps = [ - ":block_header_dslx", - ":common_dslx", - ], -) - -xls_dslx_test( - name = "dec_demux_dslx_test", - dslx_test_args = {"compare": "jit"}, - library = ":dec_demux_dslx", - tags = ["manual"], -) - -xls_dslx_verilog( - name = "dec_demux_verilog", - codegen_args = { - "module_name": "DecoderDemux", - "delay_model": "asap7", - "pipeline_stages": "2", - "reset": "rst", - "use_system_verilog": "false", - }, - dslx_top = "DecoderDemux", - library = ":dec_demux_dslx", - tags = ["manual"], - verilog_file = "dec_demux.v", -) - -xls_benchmark_ir( - name = "dec_demux_opt_ir_benchmark", - src = ":dec_demux_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "2", - "delay_model": "asap7", - }, - tags = ["manual"], -) - -verilog_library( - name = "dec_demux_verilog_lib", - srcs = [ - ":dec_demux.v", - ], - tags = ["manual"], -) - -synthesize_rtl( - name = "dec_demux_synth_asap7", - standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", - tags = ["manual"], - top_module = "DecoderDemux", - deps = [ - ":dec_demux_verilog_lib", - ], -) - -benchmark_synth( - name = "dec_demux_benchmark_synth", - synth_target = ":dec_demux_synth_asap7", - tags = ["manual"], -) - -place_and_route( - name = "dec_demux_place_and_route", - clock_period = "750", - core_padding_microns = 2, - min_pin_distance = "0.5", - placement_density = "0.30", - stop_after_step = "global_routing", - synthesized_rtl = ":dec_demux_synth_asap7", - tags = ["manual"], - target_die_utilization_percentage = "5", -) - xls_dslx_library( name = "ram_printer_dslx", srcs = ["ram_printer.x"], diff --git a/xls/modules/zstd/dec_demux.x b/xls/modules/zstd/dec_demux.x deleted file mode 100644 index 5bcd380f91..0000000000 --- a/xls/modules/zstd/dec_demux.x +++ /dev/null @@ -1,273 +0,0 @@ -// 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. - -// This file contains DecoderDemux Proc, which is responsible for -// parsing Block_Header and sending the obtained data to the Raw, RLE, -// or Compressed Block decoders. - -import std; -import xls.modules.zstd.common as common; -import xls.modules.zstd.block_header as block_header; - -type BlockDataPacket = common::BlockDataPacket; - -const DATA_WIDTH = common::DATA_WIDTH; - -enum DecoderDemuxStatus : u2 { - IDLE = 0, - PASS_RAW = 1, - PASS_RLE = 2, - PASS_COMPRESSED = 3, -} - -struct DecoderDemuxState { - status: DecoderDemuxStatus, - byte_to_pass: u21, - send_data: u21, - id: u32, - last_packet: BlockDataPacket, -} - -// It's safe to assume that data contains full header and some extra data. -// Previous stage aligns block header and data, it also guarantees -// new block headers in new packets. -fn handle_idle_state(data: BlockDataPacket, state: DecoderDemuxState) - -> DecoderDemuxState { - let header = block_header::extract_block_header(data.data[0:24] as u24); - let data = BlockDataPacket { - data: data.data[24:] as bits[DATA_WIDTH], - length: data.length - u32:24, - id: state.id, - ..data - }; - match header.btype { - common::BlockType::RAW => { - DecoderDemuxState { - status: DecoderDemuxStatus::PASS_RAW, - byte_to_pass: header.size, - send_data: u21:0, - last_packet: data, - ..state - } - }, - common::BlockType::RLE => { - DecoderDemuxState { - status: DecoderDemuxStatus::PASS_RLE, - byte_to_pass: header.size, - send_data: u21:0, - last_packet: data, - ..state - } - }, - common::BlockType::COMPRESSED => { - DecoderDemuxState { - status: DecoderDemuxStatus::PASS_COMPRESSED, - byte_to_pass: header.size, - send_data: u21:0, - last_packet: data, - ..state - } - }, - _ => { - fail!("Should_never_happen", state) - } - } -} - -const ZERO_DECODER_DEMUX_STATE = zero!(); -const ZERO_DATA = zero!(); - -pub proc DecoderDemux { - input_r: chan in; - raw_s: chan out; - rle_s: chan out; - cmp_s: chan out; - - init {(ZERO_DECODER_DEMUX_STATE)} - - config ( - input_r: chan in, - raw_s: chan out, - rle_s: chan out, - cmp_s: chan out, - ) {( - input_r, - raw_s, - rle_s, - cmp_s - )} - - next (state: DecoderDemuxState) { - let tok = join(); - let (tok, data) = recv_if(tok, input_r, !state.last_packet.last, ZERO_DATA); - if (!state.last_packet.last) { - trace_fmt!("DecoderDemux: recv: {:#x}", data); - } else {}; - let (send_raw, send_rle, send_cmp, new_state) = match state.status { - DecoderDemuxStatus::IDLE => - (false, false, false, handle_idle_state(data, state)), - DecoderDemuxStatus::PASS_RAW => { - let new_state = DecoderDemuxState { - send_data: state.send_data + (state.last_packet.length >> 3) as u21, - last_packet: data, - ..state - }; - (true, false, false, new_state) - }, - DecoderDemuxStatus::PASS_RLE => { - let new_state = DecoderDemuxState { - send_data: state.send_data + state.byte_to_pass, - last_packet: data, - ..state - }; - (false, true, false, new_state) - }, - DecoderDemuxStatus::PASS_COMPRESSED => { - let new_state = DecoderDemuxState { - send_data: state.send_data +(state.last_packet.length >> 3) as u21, - last_packet: data, - ..state - }; - (false, false, true, new_state) - }, - _ => fail!("IDLE_STATE_IMPOSSIBLE", (false, false, false, state)) - }; - - let end_state = if (send_raw || send_rle || send_cmp) { - let max_packet_width = DATA_WIDTH; - let block_size_bits = u32:24 + (state.byte_to_pass as u32 << 3); - if (!send_rle) && ((block_size_bits <= max_packet_width) && - ((block_size_bits) != state.last_packet.length) && !state.last_packet.last) { - // Demuxer expect that blocks would be received in a separate packets, - // even if 2 block would fit entirely or even partially in a single packet. - // It is the job of top-level ZSTD decoder to split each block into at least one - // BlockDataPacket. - // For Raw and Compressed blocks it is illegal to have block of size smaller than - // max size of packet and have packet length greater than this size. - fail!("Should_never_happen", state) - } else { - state - }; - let data_to_send = BlockDataPacket {id: state.id, ..state.last_packet}; - let tok = send_if(tok, raw_s, send_raw, data_to_send); - if (send_raw) { - trace_fmt!("DecoderDemux: send_raw: {:#x}", data_to_send); - } else {}; - // RLE module expects single byte in data field - // and block length in length field. This is different from - // Raw and Compressed modules. - let rle_data = BlockDataPacket{ - data: state.last_packet.data[0:8] as bits[DATA_WIDTH], - length: state.byte_to_pass as u32, - id: state.id, - ..state.last_packet - }; - let tok = send_if(tok, rle_s, send_rle, rle_data); - if (send_rle) { - trace_fmt!("DecoderDemux: send_rle: {:#x}", rle_data); - } else {}; - let tok = send_if(tok, cmp_s, send_cmp, data_to_send); - if (send_cmp) { - trace_fmt!("DecoderDemux: send_cmp: {:#x}", data_to_send); - } else {}; - let end_state = if (new_state.send_data == new_state.byte_to_pass) { - let next_id = if (state.last_packet.last && state.last_packet.last_block) { - u32: 0 - } else { - state.id + u32:1 - }; - DecoderDemuxState { - status: DecoderDemuxStatus::IDLE, - byte_to_pass: u21:0, - send_data: u21:0, - id: next_id, - last_packet: ZERO_DATA, - } - } else { - new_state - }; - end_state - } else { - new_state - }; - - end_state - } -} - -#[test_proc] -proc DecoderDemuxTest { - terminator: chan out; - input_s: chan out; - raw_r: chan in; - rle_r: chan in; - cmp_r: chan in; - - init {} - - config (terminator: chan out) { - let (raw_s, raw_r) = chan("raw"); - let (rle_s, rle_r) = chan("rle"); - let (cmp_s, cmp_r) = chan("cmp"); - let (input_s, input_r) = chan("input"); - - spawn DecoderDemux(input_r, raw_s, rle_s, cmp_s); - (terminator, input_s, raw_r, rle_r, cmp_r) - } - - next(state: ()) { - let tok = join(); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x11111111110000c0, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x2222222222111111, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x3333333333222222, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: true, last_block: bool: false, data: bits[DATA_WIDTH]:0x0000000000333333, length: u32:24 }); - - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0xAAAAAAAAAA000100, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0xBBBBBBBBBBAAAAAA, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0xCCCCCCCCCCBBBBBB, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x0000000000CCCCCC, length: u32:24 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: true, last_block: bool: false, data: bits[DATA_WIDTH]:0xDDDDDDDDDDDDDDDD, length: u32:64 }); - - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: true, last_block: bool: false, data: bits[DATA_WIDTH]:0x0000000FF000102, length: u32:32 }); - - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x4444444444000145, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x5555555555444444, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x6666666666555555, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x7777777777666666, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x8888888888777777, length: u32:64 }); - let tok = send(tok, input_s, BlockDataPacket { id: u32:0, last: bool: true, last_block: bool: true, data: bits[DATA_WIDTH]:0x0000000000888888, length: u32:24 }); - - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x0000001111111111, length: u32:40 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x2222222222111111, length: u32:64 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:0, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x3333333333222222, length: u32:64 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:0, last: bool: true, last_block: bool: false, data: bits[DATA_WIDTH]:0x0000000000333333, length: u32:24 }); - - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:1, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x000000AAAAAAAAAA, length: u32:40 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:1, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0xBBBBBBBBBBAAAAAA, length: u32:64 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:1, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0xCCCCCCCCCCBBBBBB, length: u32:64 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:1, last: bool: false, last_block: bool: false, data: bits[DATA_WIDTH]:0x0000000000CCCCCC, length: u32:24 }); - let (tok, data) = recv(tok, raw_r); assert_eq(data, BlockDataPacket { id: u32:1, last: bool: true, last_block: bool: false, data: bits[DATA_WIDTH]:0xDDDDDDDDDDDDDDDD, length: u32:64 }); - - let (tok, data) = recv(tok, rle_r); assert_eq(data, BlockDataPacket { id: u32:2, last: bool: true, last_block: bool: false, data: bits[DATA_WIDTH]:0xFF, length: u32:32 }); - - let (tok, data) = recv(tok, cmp_r); assert_eq(data, BlockDataPacket { id: u32:3, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x0000004444444444, length: u32:40 }); - let (tok, data) = recv(tok, cmp_r); assert_eq(data, BlockDataPacket { id: u32:3, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x5555555555444444, length: u32:64 }); - let (tok, data) = recv(tok, cmp_r); assert_eq(data, BlockDataPacket { id: u32:3, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x6666666666555555, length: u32:64 }); - let (tok, data) = recv(tok, cmp_r); assert_eq(data, BlockDataPacket { id: u32:3, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x7777777777666666, length: u32:64 }); - let (tok, data) = recv(tok, cmp_r); assert_eq(data, BlockDataPacket { id: u32:3, last: bool: false, last_block: bool: true, data: bits[DATA_WIDTH]:0x8888888888777777, length: u32:64 }); - let (tok, data) = recv(tok, cmp_r); assert_eq(data, BlockDataPacket { id: u32:3, last: bool: true, last_block: bool: true, data: bits[DATA_WIDTH]:0x0000000000888888, length: u32:24 }); - - send(tok, terminator, true); - } -} From 4666c80193c5544d3a24b45dd9f4c2d12ec0bbb2 Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Thu, 3 Oct 2024 14:16:47 +0200 Subject: [PATCH 13/46] modules/zstd/block_header: Specify new type for the block size Signed-off-by: Maciej Torhan --- xls/modules/zstd/block_header.x | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/xls/modules/zstd/block_header.x b/xls/modules/zstd/block_header.x index 455b3295e1..a0b52491b8 100644 --- a/xls/modules/zstd/block_header.x +++ b/xls/modules/zstd/block_header.x @@ -23,6 +23,7 @@ import xls.modules.zstd.common as common; type Buffer = buff::Buffer; type BufferStatus = buff::BufferStatus; type BlockType = common::BlockType; +type BlockSize = common::BlockSize; // Status values reported by the block header parsing function pub enum BlockHeaderStatus: u2 { @@ -35,7 +36,7 @@ pub enum BlockHeaderStatus: u2 { pub struct BlockHeader { last: bool, btype: BlockType, - size: u21, + size: BlockSize, } // Structure for returning results of block header parsing @@ -86,7 +87,7 @@ fn test_parse_block_header() { let result = parse_block_header(buffer); assert_eq(result, BlockHeaderResult { status: BlockHeaderStatus::OK, - header: BlockHeader { last: u1:1, btype: BlockType::RAW, size: u21:0x1000 }, + header: BlockHeader { last: u1:1, btype: BlockType::RAW, size: BlockSize:0x1000 }, buffer: Buffer { content: u32:0, length: u32:0 } }); @@ -94,7 +95,7 @@ fn test_parse_block_header() { let result = parse_block_header(buffer); assert_eq(result, BlockHeaderResult { status: BlockHeaderStatus::OK, - header: BlockHeader { last: u1:0, btype: BlockType::RLE, size: u21:0x1234 }, + header: BlockHeader { last: u1:0, btype: BlockType::RLE, size: BlockSize:0x1234 }, buffer: Buffer { content: u32:0, length: u32:0 } }); From 20aca94f6e52dc7f8af6b3e38e46e8737bb394d2 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 8 Oct 2024 16:04:11 +0200 Subject: [PATCH 14/46] modules/zstd: Cleanup BlockHeader Remove references to buffer structs as those are not used anywhere Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 35 ++++++++++---------- xls/modules/zstd/block_header.x | 58 --------------------------------- 2 files changed, 17 insertions(+), 76 deletions(-) diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 015ce882b5..c36810fad1 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -285,6 +285,23 @@ place_and_route( target_die_utilization_percentage = "10", ) +xls_dslx_library( + name = "block_header_dslx", + srcs = [ + "block_header.x", + ], + deps = [ + ":common_dslx", + ], +) + +xls_dslx_test( + name = "block_header_dslx_test", + dslx_test_args = {"compare": "jit"}, + library = ":block_header_dslx", + tags = ["manual"], +) + xls_dslx_library( name = "raw_block_dec_dslx", srcs = [ @@ -451,24 +468,6 @@ place_and_route( target_die_utilization_percentage = "10", ) -xls_dslx_library( - name = "block_header_dslx", - srcs = [ - "block_header.x", - ], - deps = [ - ":buffer_dslx", - ":common_dslx", - ], -) - -xls_dslx_test( - name = "block_header_dslx_test", - dslx_test_args = {"compare": "jit"}, - library = ":block_header_dslx", - tags = ["manual"], -) - xls_dslx_library( name = "dec_mux_dslx", srcs = [ diff --git a/xls/modules/zstd/block_header.x b/xls/modules/zstd/block_header.x index a0b52491b8..9e8679a72d 100644 --- a/xls/modules/zstd/block_header.x +++ b/xls/modules/zstd/block_header.x @@ -17,11 +17,8 @@ // https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.2 import std; -import xls.modules.zstd.buffer as buff; import xls.modules.zstd.common as common; -type Buffer = buff::Buffer; -type BufferStatus = buff::BufferStatus; type BlockType = common::BlockType; type BlockSize = common::BlockSize; @@ -39,13 +36,6 @@ pub struct BlockHeader { size: BlockSize, } -// Structure for returning results of block header parsing -pub struct BlockHeaderResult { - buffer: Buffer, - status: BlockHeaderStatus, - header: BlockHeader, -} - // Auxiliary constant that can be used to initialize Proc's state // with empty FrameHeader, because `zero!` cannot be used in that context pub const ZERO_BLOCK_HEADER = zero!(); @@ -59,51 +49,3 @@ pub fn extract_block_header(data:u24) -> BlockHeader { last: data[0:1], } } - -// Parses a Buffer and extracts information from a Block_Header. Returns BufferResult -// with outcome of operations on buffer and information extracted from the Block_Header. -pub fn parse_block_header(buffer: Buffer) -> BlockHeaderResult { - let (result, data) = buff::buffer_fixed_pop_checked(buffer); - - match result.status { - BufferStatus::OK => { - let block_header = extract_block_header(data); - if (block_header.btype != BlockType::RESERVED) { - BlockHeaderResult {status: BlockHeaderStatus::OK, header: block_header, buffer: result.buffer} - } else { - BlockHeaderResult {status: BlockHeaderStatus::CORRUPTED, header: zero!(), buffer: buffer} - } - }, - _ => { - trace_fmt!("parse_block_header: Not enough data to parse block header! {}", buffer.length); - BlockHeaderResult {status: BlockHeaderStatus::NO_ENOUGH_DATA, header: zero!(), buffer: buffer} - } - } -} - -#[test] -fn test_parse_block_header() { - let buffer = Buffer { content: u32:0x8001 , length: u32:24}; - let result = parse_block_header(buffer); - assert_eq(result, BlockHeaderResult { - status: BlockHeaderStatus::OK, - header: BlockHeader { last: u1:1, btype: BlockType::RAW, size: BlockSize:0x1000 }, - buffer: Buffer { content: u32:0, length: u32:0 } - }); - - let buffer = Buffer { content: u32:0x91A2, length: u32:24}; - let result = parse_block_header(buffer); - assert_eq(result, BlockHeaderResult { - status: BlockHeaderStatus::OK, - header: BlockHeader { last: u1:0, btype: BlockType::RLE, size: BlockSize:0x1234 }, - buffer: Buffer { content: u32:0, length: u32:0 } - }); - - let buffer = Buffer { content: u32:0x001, length: u32:16}; - let result = parse_block_header(buffer); - assert_eq(result, BlockHeaderResult { - status: BlockHeaderStatus::NO_ENOUGH_DATA, - header: zero!(), - buffer: Buffer { content: u32:0x001, length: u32:16 } - }); -} From f6171f09ede98a560d45cdaff41ad3525008f125 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Wed, 9 Oct 2024 11:05:59 +0200 Subject: [PATCH 15/46] modules/zstd/BUILD: Introduce common codegen args Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 143 +++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index c36810fad1..ac9005a77a 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -35,6 +35,19 @@ package( exports_files(["xls_fifo_wrapper.v"]) +CLOCK_PERIOD_PS = "750" +# Clock periods for modules that exceed the 750ps critical path in IR benchmark + +common_codegen_args = { + "delay_model": "asap7", + "reset": "rst", + "worst_case_throughput": "1", + "use_system_verilog": "false", + "clock_period_ps": CLOCK_PERIOD_PS, + "clock_margin_percent": "20", + "multi_proc": "true", +} + xls_dslx_library( name = "buffer_dslx", srcs = [ @@ -66,23 +79,17 @@ xls_dslx_test( tags = ["manual"], ) +window_buffer_codegen_args = common_codegen_args | { + "module_name": "WindowBuffer64", + "clock_period_ps": "0", + "pipeline_stages": "1", +} + xls_dslx_verilog( name = "window_buffer_verilog", - codegen_args = { - "module_name": "WindowBuffer64", - "delay_model": "asap7", - "pipeline_stages": "2", - "reset": "rst", - "use_system_verilog": "false", - }, + codegen_args = window_buffer_codegen_args, dslx_top = "WindowBuffer64", library = ":window_buffer_dslx", - # TODO: 2024-01-25: Workaround for https://github.com/google/xls/issues/869 - # Force proc inlining and set last internal proc as top proc for IR optimization - opt_ir_args = { - "inline_procs": "true", - "top": "__window_buffer__WindowBuffer64__WindowBuffer_0__64_32_48_next", - }, tags = ["manual"], verilog_file = "window_buffer.v", ) @@ -90,9 +97,9 @@ xls_dslx_verilog( xls_benchmark_ir( name = "window_buffer_opt_ir_benchmark", src = ":window_buffer_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "2", - "delay_model": "asap7", + benchmark_ir_args = window_buffer_codegen_args | { + "pipeline_stages": "10", + "top": "__window_buffer__WindowBuffer64__WindowBuffer_0__64_32_48_next", }, tags = ["manual"], ) @@ -123,7 +130,7 @@ benchmark_synth( place_and_route( name = "window_buffer_place_and_route", - clock_period = "750", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.5", placement_density = "0.30", @@ -485,15 +492,15 @@ xls_dslx_test( tags = ["manual"], ) +dec_mux_codegen_args = common_codegen_args | { + "module_name": "DecoderMux", + "clock_period_ps": "0", + "pipeline_stages": "3", +} + xls_dslx_verilog( name = "dec_mux_verilog", - codegen_args = { - "module_name": "DecoderMux", - "delay_model": "asap7", - "pipeline_stages": "3", - "reset": "rst", - "use_system_verilog": "false", - }, + codegen_args = dec_mux_codegen_args, dslx_top = "DecoderMux", library = ":dec_mux_dslx", tags = ["manual"], @@ -503,9 +510,8 @@ xls_dslx_verilog( xls_benchmark_ir( name = "dec_mux_opt_ir_benchmark", src = ":dec_mux_verilog.opt.ir", - benchmark_ir_args = { + benchmark_ir_args = dec_mux_codegen_args | { "pipeline_stages": "10", - "delay_model": "asap7", }, tags = ["manual"], ) @@ -536,7 +542,7 @@ benchmark_synth( place_and_route( name = "dec_mux_place_and_route", - clock_period = "750", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.5", placement_density = "0.30", @@ -582,34 +588,35 @@ xls_dslx_test( tags = ["manual"], ) +sequence_executor_codegen_args = common_codegen_args | { + "module_name": "sequence_executor", + "clock_period_ps": "0", + "generator": "pipeline", + "delay_model": "asap7", + "ram_configurations": ",".join([ + "{ram_name}:1R1W:{rd_req}:{rd_resp}:{wr_req}:{wr_resp}:{latency}".format( + latency = 5, + ram_name = "ram{}".format(num), + rd_req = "sequence_executor__rd_req_m{}_s".format(num), + rd_resp = "sequence_executor__rd_resp_m{}_r".format(num), + wr_req = "sequence_executor__wr_req_m{}_s".format(num), + wr_resp = "sequence_executor__wr_resp_m{}_r".format(num), + ) + for num in range(7) + ]), + "pipeline_stages": "6", + "reset": "rst", + "reset_data_path": "true", + "reset_active_low": "false", + "reset_asynchronous": "true", + "flop_inputs": "false", + "flop_single_value_channels": "false", + "flop_outputs": "false", +} + xls_dslx_verilog( name = "sequence_executor_verilog", - codegen_args = { - "module_name": "sequence_executor", - "generator": "pipeline", - "delay_model": "asap7", - "ram_configurations": ",".join([ - "{ram_name}:1R1W:{rd_req}:{rd_resp}:{wr_req}:{wr_resp}:{latency}".format( - latency = 5, - ram_name = "ram{}".format(num), - rd_req = "sequence_executor__rd_req_m{}_s".format(num), - rd_resp = "sequence_executor__rd_resp_m{}_r".format(num), - wr_req = "sequence_executor__wr_req_m{}_s".format(num), - wr_resp = "sequence_executor__wr_resp_m{}_r".format(num), - ) - for num in range(7) - ]), - "pipeline_stages": "8", - "reset": "rst", - "reset_data_path": "true", - "reset_active_low": "false", - "reset_asynchronous": "true", - "flop_inputs": "false", - "flop_single_value_channels": "false", - "flop_outputs": "false", - "worst_case_throughput": "1", - "use_system_verilog": "false", - }, + codegen_args = sequence_executor_codegen_args, dslx_top = "SequenceExecutorZstd", library = ":sequence_executor_dslx", opt_ir_args = { @@ -621,11 +628,10 @@ xls_dslx_verilog( ) xls_benchmark_ir( - name = "sequence_executor_ir_benchmark", + name = "sequence_executor_opt_ir_benchmark", src = ":sequence_executor_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "8", - "delay_model": "asap7", + benchmark_ir_args = sequence_executor_codegen_args | { + "pipeline_stages": "10", }, tags = ["manual"], ) @@ -662,7 +668,7 @@ benchmark_synth( place_and_route( name = "sequence_executor_place_and_route", - clock_period = "750", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.4", placement_density = "0.30", @@ -689,15 +695,15 @@ xls_dslx_test( tags = ["manual"], ) +repacketizer_codegen_args = common_codegen_args | { + "module_name": "Repacketizer", + "clock_period_ps": "0", + "pipeline_stages": "2", +} + xls_dslx_verilog( name = "repacketizer_verilog", - codegen_args = { - "module_name": "Repacketizer", - "delay_model": "asap7", - "pipeline_stages": "2", - "reset": "rst", - "use_system_verilog": "false", - }, + codegen_args = repacketizer_codegen_args, dslx_top = "Repacketizer", library = ":repacketizer_dslx", tags = ["manual"], @@ -707,9 +713,8 @@ xls_dslx_verilog( xls_benchmark_ir( name = "repacketizer_opt_ir_benchmark", src = ":repacketizer_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "2", - "delay_model": "asap7", + benchmark_ir_args = repacketizer_codegen_args | { + "pipeline_stages": "10", }, tags = ["manual"], ) @@ -740,7 +745,7 @@ benchmark_synth( place_and_route( name = "repacketizer_place_and_route", - clock_period = "750", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.5", placement_density = "0.30", From dea3cb1c079269adcc5d210454998e00950191ba Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 8 Oct 2024 17:41:55 +0200 Subject: [PATCH 16/46] modules/zstd: Add AxiCsrAccessor Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 77 ++++++ xls/modules/zstd/axi_csr_accessor.x | 384 ++++++++++++++++++++++++++++ 2 files changed, 461 insertions(+) create mode 100644 xls/modules/zstd/axi_csr_accessor.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index ac9005a77a..35743fea83 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -901,3 +901,80 @@ place_and_route( tags = ["manual"], target_die_utilization_percentage = "10", ) + +xls_dslx_library( + name = "axi_csr_accessor_dslx", + srcs = [ + "axi_csr_accessor.x", + ], + deps = [ + ":csr_config_dslx", + "//xls/modules/zstd/memory:axi_dslx", + ], +) + +xls_dslx_test( + name = "axi_csr_accessor_dslx_test", + library = ":axi_csr_accessor_dslx", + tags = ["manual"], +) + +axi_csr_accessor_codegen_args = common_codegen_args | { + "module_name": "AxiCsrAccessor", + "pipeline_stages": "1", +} + +xls_dslx_verilog( + name = "axi_csr_accessor_verilog", + codegen_args = axi_csr_accessor_codegen_args, + dslx_top = "AxiCsrAccessorInst", + library = ":axi_csr_accessor_dslx", + tags = ["manual"], + verilog_file = "axi_csr_accessor.v", +) + +xls_benchmark_ir( + name = "axi_csr_accessor_opt_ir_benchmark", + src = ":axi_csr_accessor_verilog.opt.ir", + benchmark_ir_args = axi_csr_accessor_codegen_args | { + "pipeline_stages": "10", + "top": "__axi_csr_accessor__AxiCsrAccessorInst__AxiCsrAccessor_0__16_32_4_4_2_4_16_next", + }, + tags = ["manual"], +) + +verilog_library( + name = "axi_csr_accessor_verilog_lib", + srcs = [ + ":axi_csr_accessor.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "axi_csr_accessor_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "AxiCsrAccessor", + deps = [ + ":axi_csr_accessor_verilog_lib", + ], +) + +benchmark_synth( + name = "axi_csr_accessor_benchmark_synth", + synth_target = ":axi_csr_accessor_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "axi_csr_accessor_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":axi_csr_accessor_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) diff --git a/xls/modules/zstd/axi_csr_accessor.x b/xls/modules/zstd/axi_csr_accessor.x new file mode 100644 index 0000000000..01cef68cd9 --- /dev/null +++ b/xls/modules/zstd/axi_csr_accessor.x @@ -0,0 +1,384 @@ +// Copyright 2023-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. + +// This file contains implementation of a proc that handles CSRs. It provides +// an AXI interface for reading and writing the values as well as separate +// request/response channels. Apart from that it has an output channel which +// notifies aboud changes made to CSRs. + +import std; +import xls.modules.zstd.memory.axi; +import xls.modules.zstd.csr_config; + + +struct AxiCsrAccessorState { + w_id: uN[ID_W], + w_addr: uN[ADDR_W], + r_id: uN[ID_W], + r_addr: uN[ADDR_W], +} + +pub proc AxiCsrAccessor< + ID_W: u32, ADDR_W: u32, DATA_W: u32, REGS_N: u32, + DATA_W_DIV8: u32 = { DATA_W / u32:8 }, + LOG2_REGS_N: u32 = { std::clog2(REGS_N) }, + LOG2_DATA_W_DIV8: u32 = { std::clog2(DATA_W / u32:8) }, +> { + type AxiAw = axi::AxiAw; + type AxiW = axi::AxiW; + type AxiB = axi::AxiB; + type AxiAr = axi::AxiAr; + type AxiR = axi::AxiR; + + type RdReq = csr_config::CsrRdReq; + type RdResp = csr_config::CsrRdResp; + type WrReq = csr_config::CsrWrReq; + type WrResp = csr_config::CsrWrResp; + + type State = AxiCsrAccessorState; + type Data = uN[DATA_W]; + type RegN = uN[LOG2_REGS_N]; + + axi_aw_r: chan in; + axi_w_r: chan in; + axi_b_s: chan out; + axi_ar_r: chan in; + axi_r_s: chan out; + + csr_rd_req_s: chan out; + csr_rd_resp_r: chan in; + csr_wr_req_s: chan out; + csr_wr_resp_r: chan in; + + config ( + axi_aw_r: chan in, + axi_w_r: chan in, + axi_b_s: chan out, + axi_ar_r: chan in, + axi_r_s: chan out, + + csr_rd_req_s: chan out, + csr_rd_resp_r: chan in, + csr_wr_req_s: chan out, + csr_wr_resp_r: chan in, + ) { + ( + axi_aw_r, axi_w_r, axi_b_s, + axi_ar_r, axi_r_s, + csr_rd_req_s, csr_rd_resp_r, + csr_wr_req_s, csr_wr_resp_r, + ) + } + + init { + zero!() + } + + next (state: State) { + let tok_0 = join(); + // write to CSR via AXI + let (tok_1_1, axi_aw, axi_aw_valid) = recv_non_blocking(tok_0, axi_aw_r, AxiAw {id: state.w_id, addr: state.w_addr, ..zero!()}); + + // validate axi aw + assert!(!(axi_aw_valid && axi_aw.addr as u32 >= REGS_N), "invalid_aw_addr"); + assert!(!(axi_aw_valid && axi_aw.len != u8:0), "invalid_aw_len"); + + let (tok_1_2, axi_w, axi_w_valid) = recv_non_blocking(tok_1_1, axi_w_r, zero!()); + + // Send WriteRequest to CSRs + let data_w = if axi_w_valid { + let (w_data, _, _) = for (i, (w_data, strb, mask)): (u32, (uN[DATA_W], uN[DATA_W_DIV8], uN[DATA_W])) in range(u32:0, DATA_W_DIV8) { + let w_data = if axi_w.strb as u1 { + w_data | (axi_w.data & mask) + } else { + w_data + }; + ( + w_data, + strb >> u32:1, + mask << u32:8, + ) + }((uN[DATA_W]:0, axi_w.strb, uN[DATA_W]:0xFF)); + w_data + } else { + uN[DATA_W]:0 + }; + + let wr_req = WrReq { + csr: (axi_aw.addr >> LOG2_DATA_W_DIV8) as uN[LOG2_REGS_N], + value: data_w + }; + + let tok_1_3 = send_if(tok_1_2, csr_wr_req_s, axi_w_valid, wr_req); + + let (tok_2_1, csr_wr_resp, csr_wr_resp_valid) = recv_non_blocking(tok_0, csr_wr_resp_r, zero!()); + let axi_write_resp = AxiB { + resp: axi::AxiWriteResp::OKAY, + id: axi_aw.id, + }; + let tok_2_2 = send_if(tok_2_1, axi_b_s, csr_wr_resp_valid, axi_write_resp); + + + // Send ReadRequest to CSRs + let (tok_3_1, axi_ar, axi_ar_valid) = recv_non_blocking(tok_0, axi_ar_r, AxiAr {id: state.r_id, addr: state.r_addr, ..zero!()}); + // validate ar bundle + assert!(!(axi_ar_valid && axi_ar.addr as u32 >= REGS_N), "invalid_ar_addr"); + assert!(!(axi_ar_valid && axi_ar.len != u8:0), "invalid_ar_len"); + let rd_req = RdReq { + csr: (axi_ar.addr >> LOG2_DATA_W_DIV8) as uN[LOG2_REGS_N], + }; + let tok_3_2 = send_if(tok_3_1, csr_rd_req_s, axi_ar_valid, rd_req); + + let (tok_4_1, csr_rd_resp, csr_rd_resp_valid) = recv_non_blocking(tok_0, csr_rd_resp_r, zero!()); + + let axi_read_resp = AxiR { + id: axi_ar.id, + data: csr_rd_resp.value, + resp: axi::AxiReadResp::OKAY, + last: true, + }; + let tok_4_2 = send_if(tok_4_1, axi_r_s, csr_rd_resp_valid, axi_read_resp); + + State { + w_id: axi_aw.id, + w_addr: axi_aw.addr, + r_id: axi_ar.id, + r_addr: axi_ar.addr, + } + } +} + +const INST_ID_W = u32:4; +const INST_DATA_W = u32:32; +const INST_ADDR_W = u32:16; +const INST_REGS_N = u32:16; +const INST_DATA_W_DIV8 = INST_DATA_W / u32:8; +const INST_LOG2_REGS_N = std::clog2(INST_REGS_N); + +proc AxiCsrAccessorInst { + type InstAxiAw = axi::AxiAw; + type InstAxiW = axi::AxiW; + type InstAxiB = axi::AxiB; + type InstAxiAr = axi::AxiAr; + type InstAxiR = axi::AxiR; + + type InstCsrRdReq = csr_config::CsrRdReq; + type InstCsrRdResp = csr_config::CsrRdResp; + type InstCsrWrReq = csr_config::CsrWrReq; + type InstCsrWrResp = csr_config::CsrWrResp; + type InstCsrChange = csr_config::CsrChange; + + config( + axi_aw_r: chan in, + axi_w_r: chan in, + axi_b_s: chan out, + axi_ar_r: chan in, + axi_r_s: chan out, + + + csr_rd_req_s: chan out, + csr_rd_resp_r: chan in, + csr_wr_req_s: chan out, + csr_wr_resp_r: chan in, + ) { + spawn AxiCsrAccessor ( + axi_aw_r, axi_w_r, axi_b_s, + axi_ar_r, axi_r_s, + csr_rd_req_s, csr_rd_resp_r, + csr_wr_req_s, csr_wr_resp_r, + ); + } + + init { } + + next (state: ()) { } +} + +const TEST_ID_W = u32:4; +const TEST_DATA_W = u32:32; +const TEST_ADDR_W = u32:16; +const TEST_REGS_N = u32:16; +const TEST_DATA_W_DIV8 = TEST_DATA_W / u32:8; +const TEST_LOG2_REGS_N = std::clog2(TEST_REGS_N); +const TEST_LOG2_DATA_W_DIV8 = std::clog2(TEST_DATA_W_DIV8); + +type TestCsr = uN[TEST_LOG2_REGS_N]; +type TestValue = uN[TEST_DATA_W]; + +struct TestData { + csr: uN[TEST_LOG2_REGS_N], + value: uN[TEST_DATA_W], +} + +const TEST_DATA = TestData[20]:[ + TestData{ csr: TestCsr:0, value: TestValue:0xca32_9f4a }, + TestData{ csr: TestCsr:1, value: TestValue:0x0fb3_fa42 }, + TestData{ csr: TestCsr:2, value: TestValue:0xe7ee_da41 }, + TestData{ csr: TestCsr:3, value: TestValue:0xef51_f98c }, + TestData{ csr: TestCsr:0, value: TestValue:0x97a3_a2d2 }, + TestData{ csr: TestCsr:0, value: TestValue:0xea06_e94b }, + TestData{ csr: TestCsr:1, value: TestValue:0x5fac_17ce }, + TestData{ csr: TestCsr:3, value: TestValue:0xf9d8_9938 }, + TestData{ csr: TestCsr:2, value: TestValue:0xc262_2d2e }, + TestData{ csr: TestCsr:2, value: TestValue:0xb4dd_424e }, + TestData{ csr: TestCsr:1, value: TestValue:0x01f9_b9e4 }, + TestData{ csr: TestCsr:1, value: TestValue:0x3020_6eec }, + TestData{ csr: TestCsr:3, value: TestValue:0x3124_87b5 }, + TestData{ csr: TestCsr:0, value: TestValue:0x0a49_f5e3 }, + TestData{ csr: TestCsr:2, value: TestValue:0xde3b_5d0f }, + TestData{ csr: TestCsr:3, value: TestValue:0x5948_c1b3 }, + TestData{ csr: TestCsr:0, value: TestValue:0xa26d_851f }, + TestData{ csr: TestCsr:3, value: TestValue:0x3fa9_59c0 }, + TestData{ csr: TestCsr:1, value: TestValue:0x4efd_dd09 }, + TestData{ csr: TestCsr:1, value: TestValue:0x6d75_058a }, +]; + +#[test_proc] +proc AxiCsrAccessorTest { + type TestAxiAw = axi::AxiAw; + type TestAxiW = axi::AxiW; + type TestAxiB = axi::AxiB; + type TestAxiAr = axi::AxiAr; + type TestAxiR = axi::AxiR; + + + type TestCsrRdReq = csr_config::CsrRdReq; + type TestCsrRdResp = csr_config::CsrRdResp; + type TestCsrWrReq = csr_config::CsrWrReq; + type TestCsrWrResp = csr_config::CsrWrResp; + type TestCsrChange = csr_config::CsrChange; + + terminator: chan out; + + axi_aw_s: chan out; + axi_w_s: chan out; + axi_b_r: chan in; + axi_ar_s: chan out; + axi_r_r: chan in; + + csr_rd_req_r: chan in; + csr_rd_resp_s: chan out; + csr_wr_req_r: chan in; + csr_wr_resp_s: chan out; + + config (terminator: chan out) { + let (axi_aw_s, axi_aw_r) = chan("axi_aw"); + let (axi_w_s, axi_w_r) = chan("axi_w"); + let (axi_b_s, axi_b_r) = chan("axi_b"); + let (axi_ar_s, axi_ar_r) = chan("axi_ar"); + let (axi_r_s, axi_r_r) = chan("axi_r"); + + let (csr_rd_req_s, csr_rd_req_r) = chan("csr_rd_req"); + let (csr_rd_resp_s, csr_rd_resp_r) = chan("csr_rd_resp"); + + let (csr_wr_req_s, csr_wr_req_r) = chan("csr_wr_req"); + let (csr_wr_resp_s, csr_wr_resp_r) = chan("csr_wr_resp"); + + spawn AxiCsrAccessor ( + axi_aw_r, axi_w_r, axi_b_s, + axi_ar_r, axi_r_s, + csr_rd_req_s, csr_rd_resp_r, + csr_wr_req_s, csr_wr_resp_r, + ); + + ( + terminator, + axi_aw_s, axi_w_s, axi_b_r, + axi_ar_s, axi_r_r, + csr_rd_req_r, csr_rd_resp_s, + csr_wr_req_r, csr_wr_resp_s, + ) + } + + init { } + + next (state: ()) { + // test writing via AXI + let tok = for ((i, test_data), tok): ((u32, TestData), token) in enumerate(TEST_DATA) { + // write CSR via AXI + let axi_aw = TestAxiAw { + id: i as uN[TEST_ID_W], + addr: (test_data.csr << TEST_LOG2_DATA_W_DIV8) as uN[TEST_ADDR_W], + size: axi::AxiAxSize::MAX_4B_TRANSFER, + len: u8:0, + burst: axi::AxiAxBurst::FIXED, + }; + let tok = send(tok, axi_aw_s, axi_aw); + trace_fmt!("Sent #{} AXI AW: {:#x}", i + u32:1, axi_aw); + + let axi_w = TestAxiW { + data: test_data.value, + strb: !uN[TEST_DATA_W_DIV8]:0, + last: true, + }; + let tok = send(tok, axi_w_s, axi_w); + trace_fmt!("Sent #{} AXI W: {:#x}", i + u32:1, axi_w); + + let expected_wr_req = TestCsrWrReq { + csr: test_data.csr, + value: test_data.value + }; + let (tok, wr_req) = recv(tok, csr_wr_req_r); + trace_fmt!("Received #{} CSR WriteRequest: {:#x}", i + u32:1, wr_req); + assert_eq(expected_wr_req, wr_req); + + let tok = send(tok, csr_wr_resp_s, TestCsrWrResp{}); + trace_fmt!("Sent #{} CsrWrResp", i + u32:1); + let (tok, axi_b) = recv(tok, axi_b_r); + trace_fmt!("Received #{} AXI B: {:#x}", i + u32:1, axi_b); + let expected_axi_resp = TestAxiB{ + resp: axi::AxiWriteResp::OKAY, + id: i as uN[TEST_ID_W], + }; + assert_eq(expected_axi_resp, axi_b); + + // read CSRs via AXI + let axi_ar = TestAxiAr { + id: i as uN[TEST_ID_W], + addr: (test_data.csr << TEST_LOG2_DATA_W_DIV8) as uN[TEST_ADDR_W], + len: u8:0, + ..zero!() + }; + let tok = send(tok, axi_ar_s, axi_ar); + trace_fmt!("Sent #{} AXI AR: {:#x}", i + u32:1, axi_ar); + + let expected_rd_req = TestCsrRdReq { + csr: test_data.csr, + }; + let (tok, rd_req) = recv(tok, csr_rd_req_r); + trace_fmt!("Received #{} CSR ReadRequest: {:#x}", i + u32:1, rd_req); + assert_eq(expected_rd_req, rd_req); + let rd_resp = TestCsrRdResp { + csr: test_data.csr, + value: test_data.value + }; + let tok = send(tok, csr_rd_resp_s, rd_resp); + trace_fmt!("Sent #{} CsrRdResp: {:#x}", i + u32:1, rd_resp); + + let (tok, axi_r) = recv(tok, axi_r_r); + trace_fmt!("Received #{} AXI R: {:#x}", i + u32:1, axi_r); + let expected_axi_rd_resp = TestAxiR{ + id: i as uN[TEST_ID_W], + data: test_data.value, + resp: axi::AxiReadResp::OKAY, + last: true, + }; + assert_eq(expected_axi_rd_resp, axi_r); + + tok + }(join()); + + send(tok, terminator, true); + } +} From eab78fbf444cb45484912eb0f4c40975efe43ee9 Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Tue, 8 Oct 2024 17:49:59 +0200 Subject: [PATCH 17/46] modules/zstd: Add CsrConfig Co-authored-by: Pawel Czarnecki Co-authored-by: Robert Winkler Signed-off-by: Maciej Torhan Signed-off-by: Pawel Czarnecki Signed-off-by: Robert Winkler --- xls/modules/zstd/BUILD | 76 +++++++ xls/modules/zstd/csr_config.x | 397 ++++++++++++++++++++++++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 xls/modules/zstd/csr_config.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 35743fea83..ee84f08864 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -978,3 +978,79 @@ place_and_route( tags = ["manual"], target_die_utilization_percentage = "10", ) + +xls_dslx_library( + name = "csr_config_dslx", + srcs = [ + "csr_config.x", + ], + deps = [ + "//xls/modules/zstd/memory:axi_dslx", + ], +) + +xls_dslx_test( + name = "csr_config_dslx_test", + library = ":csr_config_dslx", + tags = ["manual"], +) + +csr_config_codegen_args = common_codegen_args | { + "module_name": "CsrConfig", + "pipeline_stages": "3", +} + +xls_dslx_verilog( + name = "csr_config_verilog", + codegen_args = csr_config_codegen_args, + dslx_top = "CsrConfigInst", + library = ":csr_config_dslx", + tags = ["manual"], + verilog_file = "csr_config.v", +) + +xls_benchmark_ir( + name = "csr_config_opt_ir_benchmark", + src = ":csr_config_verilog.opt.ir", + benchmark_ir_args = csr_config_codegen_args | { + "pipeline_stages": "10", + "top": "__csr_config__CsrConfigInst__CsrConfig_0__2_32_4_32_2_4_next", + }, + tags = ["manual"], +) + +verilog_library( + name = "csr_config_verilog_lib", + srcs = [ + ":csr_config.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "csr_config_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "CsrConfig", + deps = [ + ":csr_config_verilog_lib", + ], +) + +benchmark_synth( + name = "csr_config_benchmark_synth", + synth_target = ":csr_config_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "csr_config_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":csr_config_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) diff --git a/xls/modules/zstd/csr_config.x b/xls/modules/zstd/csr_config.x new file mode 100644 index 0000000000..a792757cfa --- /dev/null +++ b/xls/modules/zstd/csr_config.x @@ -0,0 +1,397 @@ +// Copyright 2023-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. + +// This file contains implementation of a proc that handles CSRs. It provides +// an AXI interface for reading and writing the values as well as separate +// request/response channels. Apart from that it has an output channel which +// notifies aboud changes made to CSRs. + +import std; +import xls.modules.zstd.memory.axi; + +pub struct CsrRdReq { + csr: uN[LOG2_REGS_N], +} + +pub struct CsrRdResp { + csr: uN[LOG2_REGS_N], + value: uN[DATA_W], +} + +pub struct CsrWrReq { + csr: uN[LOG2_REGS_N], + value: uN[DATA_W], +} + +pub struct CsrWrResp { } + +pub struct CsrChange { + csr: uN[LOG2_REGS_N], +} + +struct CsrConfigState { + register_file: uN[DATA_W][REGS_N], +} + +pub proc CsrConfig< + ID_W: u32, ADDR_W: u32, DATA_W: u32, REGS_N: u32, + //REGS_INIT: u64[64] = {u64[64]:[u64:0, ...]}, + DATA_W_DIV8: u32 = { DATA_W / u32:8 }, + LOG2_REGS_N: u32 = { std::clog2(REGS_N) }, +> { + + type RdReq = CsrRdReq; + type RdResp = CsrRdResp; + type WrReq = CsrWrReq; + type WrResp = CsrWrResp; + type Change = CsrChange; + + type State = CsrConfigState; + type Data = uN[DATA_W]; + type RegN = uN[LOG2_REGS_N]; + + ext_csr_rd_req_r: chan in; + ext_csr_rd_resp_s: chan out; + ext_csr_wr_req_r: chan in; + ext_csr_wr_resp_s: chan out; + + csr_rd_req_r: chan in; + csr_rd_resp_s: chan out; + csr_wr_req_r: chan in; + csr_wr_resp_s: chan out; + + csr_change_s: chan out; + + config ( + ext_csr_rd_req_r: chan in, + ext_csr_rd_resp_s: chan out, + ext_csr_wr_req_r: chan in, + ext_csr_wr_resp_s: chan out, + + csr_rd_req_r: chan in, + csr_rd_resp_s: chan out, + csr_wr_req_r: chan in, + csr_wr_resp_s: chan out, + csr_change_s: chan out, + ) { + ( + ext_csr_rd_req_r, ext_csr_rd_resp_s, + ext_csr_wr_req_r, ext_csr_wr_resp_s, + csr_rd_req_r, csr_rd_resp_s, + csr_wr_req_r, csr_wr_resp_s, + csr_change_s, + ) + } + + init { + zero!() + } + + next (state: State) { + let register_file = state.register_file; + + let tok_0 = join(); + + // write to CSR + let (tok_1_1_1, ext_csr_wr_req, ext_csr_wr_req_valid) = recv_non_blocking(tok_0, ext_csr_wr_req_r, zero!()); + let (tok_1_1_2, csr_wr_req, csr_wr_req_valid) = recv_non_blocking(tok_0, csr_wr_req_r, zero!()); + + // Mux the Write Requests from External and Internal sources + // Write requests from external source take precedence before internal writes + let wr_req = if (ext_csr_wr_req_valid) { + ext_csr_wr_req + } else if {csr_wr_req_valid} { + csr_wr_req + } else { + zero!() + }; + + let wr_req_valid = ext_csr_wr_req_valid | csr_wr_req_valid; + + let register_file = if wr_req_valid { + update(register_file, wr_req.csr as u32, wr_req.value) + } else { + register_file + }; + + // Send Write Response + let tok_1_1 = join(tok_1_1_1, tok_1_1_2); + let tok_1_2_1 = send_if(tok_1_1, ext_csr_wr_resp_s, ext_csr_wr_req_valid, WrResp {}); + let tok_1_2_2 = send_if(tok_1_1, csr_wr_resp_s, csr_wr_req_valid, WrResp {}); + + // Send change notification + let tok_1_2 = join(tok_1_2_1, tok_1_2_2); + let tok_1_3 = send_if(tok_1_2, csr_change_s, wr_req_valid, Change { csr: wr_req.csr }); + + + // Read from CSRs + let (tok_2_1, ext_csr_rd_req, ext_csr_req_valid) = recv_non_blocking(tok_0, ext_csr_rd_req_r, zero!()); + + send_if(tok_2_1, ext_csr_rd_resp_s, ext_csr_req_valid, RdResp { + csr: ext_csr_rd_req.csr, + value: register_file[ext_csr_rd_req.csr as u32], + }); + + let (tok_3_1, csr_rd_req, csr_req_valid) = recv_non_blocking(tok_0, csr_rd_req_r, zero!()); + send_if(tok_3_1, csr_rd_resp_s, csr_req_valid, RdResp { + csr: csr_rd_req.csr, + value: register_file[csr_rd_req.csr as u32], + }); + + State { + register_file: register_file, + } + } +} + +const INST_ID_W = u32:32; +const INST_DATA_W = u32:32; +const INST_ADDR_W = u32:2; +const INST_REGS_N = u32:4; +const INST_DATA_W_DIV8 = INST_DATA_W / u32:8; +const INST_LOG2_REGS_N = std::clog2(INST_REGS_N); + +proc CsrConfigInst { + type InstCsrRdReq = CsrRdReq; + type InstCsrRdResp = CsrRdResp; + type InstCsrWrReq = CsrWrReq; + type InstCsrWrResp = CsrWrResp; + type InstCsrChange = CsrChange; + + config( + ext_csr_rd_req_r: chan in, + ext_csr_rd_resp_s: chan out, + ext_csr_wr_req_r: chan in, + ext_csr_wr_resp_s: chan out, + + csr_rd_req_r: chan in, + csr_rd_resp_s: chan out, + csr_wr_req_r: chan in, + csr_wr_resp_s: chan out, + csr_change_s: chan out, + ) { + spawn CsrConfig ( + ext_csr_rd_req_r, ext_csr_rd_resp_s, + ext_csr_wr_req_r, ext_csr_wr_resp_s, + csr_rd_req_r, csr_rd_resp_s, + csr_wr_req_r, csr_wr_resp_s, + csr_change_s, + ); + } + + init { } + + next (state: ()) { } +} + +const TEST_ID_W = u32:32; +const TEST_DATA_W = u32:32; +const TEST_ADDR_W = u32:2; +const TEST_REGS_N = u32:4; +const TEST_DATA_W_DIV8 = TEST_DATA_W / u32:8; +const TEST_LOG2_REGS_N = std::clog2(TEST_REGS_N); + +type TestCsr = uN[TEST_LOG2_REGS_N]; +type TestValue = uN[TEST_DATA_W]; + +struct TestData { + csr: uN[TEST_LOG2_REGS_N], + value: uN[TEST_DATA_W], +} + +const TEST_DATA = TestData[20]:[ + TestData{ csr: TestCsr:0, value: TestValue:0xca32_9f4a }, + TestData{ csr: TestCsr:1, value: TestValue:0x0fb3_fa42 }, + TestData{ csr: TestCsr:2, value: TestValue:0xe7ee_da41 }, + TestData{ csr: TestCsr:3, value: TestValue:0xef51_f98c }, + TestData{ csr: TestCsr:0, value: TestValue:0x97a3_a2d2 }, + TestData{ csr: TestCsr:0, value: TestValue:0xea06_e94b }, + TestData{ csr: TestCsr:1, value: TestValue:0x5fac_17ce }, + TestData{ csr: TestCsr:3, value: TestValue:0xf9d8_9938 }, + TestData{ csr: TestCsr:2, value: TestValue:0xc262_2d2e }, + TestData{ csr: TestCsr:2, value: TestValue:0xb4dd_424e }, + TestData{ csr: TestCsr:1, value: TestValue:0x01f9_b9e4 }, + TestData{ csr: TestCsr:1, value: TestValue:0x3020_6eec }, + TestData{ csr: TestCsr:3, value: TestValue:0x3124_87b5 }, + TestData{ csr: TestCsr:0, value: TestValue:0x0a49_f5e3 }, + TestData{ csr: TestCsr:2, value: TestValue:0xde3b_5d0f }, + TestData{ csr: TestCsr:3, value: TestValue:0x5948_c1b3 }, + TestData{ csr: TestCsr:0, value: TestValue:0xa26d_851f }, + TestData{ csr: TestCsr:3, value: TestValue:0x3fa9_59c0 }, + TestData{ csr: TestCsr:1, value: TestValue:0x4efd_dd09 }, + TestData{ csr: TestCsr:1, value: TestValue:0x6d75_058a }, +]; + +#[test_proc] +proc CsrConfig_test { + type TestCsrRdReq = CsrRdReq; + type TestCsrRdResp = CsrRdResp; + type TestCsrWrReq = CsrWrReq; + type TestCsrWrResp = CsrWrResp; + type TestCsrChange = CsrChange; + + terminator: chan out; + + ext_csr_rd_req_s: chan out; + ext_csr_rd_resp_r: chan in; + ext_csr_wr_req_s: chan out; + ext_csr_wr_resp_r: chan in; + + csr_rd_req_s: chan out; + csr_rd_resp_r: chan in; + csr_wr_req_s: chan out; + csr_wr_resp_r: chan in; + + csr_change_r: chan in; + + config (terminator: chan out) { + let (ext_csr_rd_req_s, ext_csr_rd_req_r) = chan("ext_csr_rd_req"); + let (ext_csr_rd_resp_s, ext_csr_rd_resp_r) = chan("ext_csr_rd_resp"); + + let (ext_csr_wr_req_s, ext_csr_wr_req_r) = chan("ext_csr_wr_req"); + let (ext_csr_wr_resp_s, ext_csr_wr_resp_r) = chan("ext_csr_wr_resp"); + + let (csr_rd_req_s, csr_rd_req_r) = chan("csr_rd_req"); + let (csr_rd_resp_s, csr_rd_resp_r) = chan("csr_rd_resp"); + + let (csr_wr_req_s, csr_wr_req_r) = chan("csr_wr_req"); + let (csr_wr_resp_s, csr_wr_resp_r) = chan("csr_wr_resp"); + + let (csr_change_s, csr_change_r) = chan("csr_change"); + + spawn CsrConfig ( + ext_csr_rd_req_r, ext_csr_rd_resp_s, + ext_csr_wr_req_r, ext_csr_wr_resp_s, + csr_rd_req_r, csr_rd_resp_s, + csr_wr_req_r, csr_wr_resp_s, + csr_change_s, + ); + + ( + terminator, + ext_csr_rd_req_s, ext_csr_rd_resp_r, + ext_csr_wr_req_s, ext_csr_wr_resp_r, + csr_rd_req_s, csr_rd_resp_r, + csr_wr_req_s, csr_wr_resp_r, + csr_change_r, + ) + } + + init { } + + next (state: ()) { + let expected_values = zero!(); + + // Test Writes through external interface + let (tok, expected_values) = for ((i, test_data), (tok, expected_values)): ((u32, TestData), (token, uN[TEST_DATA_W][TEST_REGS_N])) in enumerate(TEST_DATA) { + // write CSR via external interface + let wr_req = TestCsrWrReq { + csr: test_data.csr, + value: test_data.value, + }; + let tok = send(tok, ext_csr_wr_req_s, wr_req); + trace_fmt!("Sent #{} WrReq through external interface: {:#x}", i + u32:1, wr_req); + + let (tok, wr_resp) = recv(tok, ext_csr_wr_resp_r); + trace_fmt!("Received #{} WrResp through external interface: {:#x}", i + u32:1, wr_resp); + + // read CSR change + let (tok, csr_change) = recv(tok, csr_change_r); + trace_fmt!("Received #{} CSR change {:#x}", i + u32:1, csr_change); + + assert_eq(test_data.csr, csr_change.csr); + + // update expected values + let expected_values = update(expected_values, test_data.csr as u32, test_data.value); + + let tok = for (test_csr, tok): (u32, token) in u32:0..u32:4 { + let rd_req = TestCsrRdReq { + csr: test_csr as TestCsr, + }; + let expected_rd_resp = TestCsrRdResp{ + csr: test_csr as TestCsr, + value: expected_values[test_csr as u32] + }; + + // Read CSR via external interface + let tok = send(tok, ext_csr_rd_req_s, rd_req); + trace_fmt!("Sent #{} RdReq through external interface: {:#x}", i + u32:1, rd_req); + let (tok, rd_resp) = recv(tok, ext_csr_rd_resp_r); + trace_fmt!("Received #{} RdResp through external interface: {:#x}", i + u32:1, rd_resp); + assert_eq(expected_rd_resp, rd_resp); + + // Read CSR via internal interface + let tok = send(tok, csr_rd_req_s, rd_req); + trace_fmt!("Sent #{} RdReq through internal interface: {:#x}", i + u32:1, rd_req); + let (tok, csr_rd_resp) = recv(tok, csr_rd_resp_r); + trace_fmt!("Received #{} RdResp through internal interface: {:#x}", i + u32:1, csr_rd_resp); + assert_eq(expected_rd_resp, csr_rd_resp); + tok + }(tok); + + (tok, expected_values) + }((join(), expected_values)); + + // Test writes via internal interface + let (tok, _) = for ((i, test_data), (tok, expected_values)): ((u32, TestData), (token, uN[TEST_DATA_W][TEST_REGS_N])) in enumerate(TEST_DATA) { + // write CSR via request channel + let csr_wr_req = TestCsrWrReq { + csr: test_data.csr, + value: test_data.value, + }; + let tok = send(tok, csr_wr_req_s, csr_wr_req); + trace_fmt!("Sent #{} WrReq through internal interface: {:#x}", i + u32:1, csr_wr_req); + + let (tok, csr_wr_resp) = recv(tok, csr_wr_resp_r); + trace_fmt!("Received #{} WrResp through internal interface {:#x}", i + u32:1, csr_wr_resp); + + // read CSR change + let (tok, csr_change) = recv(tok, csr_change_r); + trace_fmt!("Received #{} CSR change {:#x}", i + u32:1, csr_change); + assert_eq(test_data.csr, csr_change.csr); + + // update expected values + let expected_values = update(expected_values, test_data.csr as u32, test_data.value); + + let tok = for (test_csr, tok): (u32, token) in u32:0..u32:4 { + let rd_req = TestCsrRdReq { + csr: test_csr as TestCsr, + }; + let expected_rd_resp = TestCsrRdResp{ + csr: test_csr as TestCsr, + value: expected_values[test_csr as u32] + }; + + // Read CSR via external interface + let tok = send(tok, ext_csr_rd_req_s, rd_req); + trace_fmt!("Sent #{} RdReq through external interface: {:#x}", i + u32:1, rd_req); + let (tok, rd_resp) = recv(tok, ext_csr_rd_resp_r); + trace_fmt!("Received #{} RdResp through external interface: {:#x}", i + u32:1, rd_resp); + assert_eq(expected_rd_resp, rd_resp); + + // Read CSR via internal interface + let tok = send(tok, csr_rd_req_s, rd_req); + trace_fmt!("Sent #{} RdReq through internal interface: {:#x}", i + u32:1, rd_req); + let (tok, csr_rd_resp) = recv(tok, csr_rd_resp_r); + trace_fmt!("Received #{} RdResp through internal interface: {:#x}", i + u32:1, csr_rd_resp); + assert_eq(expected_rd_resp, csr_rd_resp); + tok + }(tok); + + (tok, expected_values) + }((join(), expected_values)); + + send(tok, terminator, true); + } +} From ec27ccaec85cc5c347e24c9bf843ffdf5e8c5bee Mon Sep 17 00:00:00 2001 From: Krzysztof Oblonczek Date: Tue, 8 Oct 2024 18:02:20 +0200 Subject: [PATCH 18/46] modules/zstd: Add FrameHeaderDecoder Signed-off-by: Krzysztof Oblonczek --- xls/modules/zstd/BUILD | 112 ++--- xls/modules/zstd/frame_header.x | 692 -------------------------- xls/modules/zstd/frame_header_dec.x | 670 +++++++++++++++++++++++++ xls/modules/zstd/frame_header_test.cc | 407 --------------- xls/modules/zstd/frame_header_test.x | 30 -- 5 files changed, 703 insertions(+), 1208 deletions(-) delete mode 100644 xls/modules/zstd/frame_header.x create mode 100644 xls/modules/zstd/frame_header_dec.x delete mode 100644 xls/modules/zstd/frame_header_test.cc delete mode 100644 xls/modules/zstd/frame_header_test.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index ee84f08864..faf0504da0 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -160,16 +160,6 @@ cc_library( ], ) -xls_dslx_library( - name = "frame_header_dslx", - srcs = [ - "frame_header.x", - ], - deps = [ - ":buffer_dslx", - ], -) - xls_dslx_library( name = "common_dslx", srcs = [ @@ -178,116 +168,80 @@ xls_dslx_library( deps = [], ) -xls_dslx_test( - name = "frame_header_dslx_test", - dslx_test_args = {"compare": "jit"}, - library = ":frame_header_dslx", - tags = ["manual"], -) - xls_dslx_library( - name = "frame_header_test_dslx", + name = "frame_header_dec_dslx", srcs = [ - "frame_header_test.x", + "frame_header_dec.x", ], deps = [ - ":buffer_dslx", - ":frame_header_dslx", + "//xls/modules/zstd/memory:axi_dslx", + "//xls/modules/zstd/memory:mem_reader_dslx", ], ) -cc_test( - name = "frame_header_cc_test", - srcs = [ - "frame_header_test.cc", - ], - data = [ - ":frame_header_test_dslx", - ], - shard_count = 50, - deps = [ - ":data_generator", - "//xls/common:xls_gunit_main", - "//xls/common/file:filesystem", - "//xls/common/file:get_runfile_path", - "//xls/common/fuzzing:fuzztest", - "//xls/common/status:matchers", - "//xls/common/status:ret_check", - "//xls/dslx:create_import_data", - "//xls/dslx:import_data", - "//xls/dslx:parse_and_typecheck", - "//xls/dslx/ir_convert:convert_options", - "//xls/dslx/ir_convert:ir_converter", - "//xls/dslx/type_system:parametric_env", - "//xls/ir:bits", - "//xls/ir:value", - "//xls/simulation:sim_test_base", - "@com_google_absl//absl/container:flat_hash_map", - "@com_google_absl//absl/status:statusor", - "@com_google_absl//absl/types:span", - "@com_google_googletest//:gtest", - "@zstd", - ], +xls_dslx_test( + name = "frame_header_dec_dslx_test", + library = ":frame_header_dec_dslx", + tags = ["manual"], ) +frame_header_dec_codegen_args = common_codegen_args | { + "module_name": "FrameHeaderDecoder", + "clock_period_ps": "0", + "pipeline_stages": "6", +} + xls_dslx_verilog( - name = "frame_header_verilog", - codegen_args = { - "module_name": "FrameHeaderDecoder", - "delay_model": "asap7", - "pipeline_stages": "9", - "reset": "rst", - "reset_data_path": "false", - "use_system_verilog": "false", - }, - dslx_top = "parse_frame_header_128", - library = ":frame_header_test_dslx", + name = "frame_header_dec_verilog", + codegen_args = frame_header_dec_codegen_args, + dslx_top = "FrameHeaderDecoderInst", + library = ":frame_header_dec_dslx", tags = ["manual"], - verilog_file = "frame_header.v", + verilog_file = "frame_header_dec.v", ) xls_benchmark_ir( - name = "frame_header_opt_ir_benchmark", - src = ":frame_header_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "9", - "delay_model": "asap7", + name = "frame_header_dec_opt_ir_benchmark", + src = ":frame_header_dec_verilog.opt.ir", + benchmark_ir_args = frame_header_dec_codegen_args | { + "top": "__frame_header_dec__FrameHeaderDecoderInst__FrameHeaderDecoder_0__16_32_30_5_next", + "pipeline_stages": "10", }, tags = ["manual"], ) verilog_library( - name = "frame_header_verilog_lib", + name = "frame_header_dec_verilog_lib", srcs = [ - ":frame_header.v", + ":frame_header_dec.v", ], tags = ["manual"], ) synthesize_rtl( - name = "frame_header_synth_asap7", + name = "frame_header_dec_synth_asap7", standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", tags = ["manual"], top_module = "FrameHeaderDecoder", deps = [ - ":frame_header_verilog_lib", + ":frame_header_dec_verilog_lib", ], ) benchmark_synth( - name = "frame_header_benchmark_synth", - synth_target = ":frame_header_synth_asap7", + name = "frame_header_dec_benchmark_synth", + synth_target = ":frame_header_dec_synth_asap7", tags = ["manual"], ) place_and_route( - name = "frame_header_place_and_route", - clock_period = "750", + name = "frame_header_dec_place_and_route", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.5", placement_density = "0.30", stop_after_step = "global_routing", - synthesized_rtl = ":frame_header_synth_asap7", + synthesized_rtl = ":frame_header_dec_synth_asap7", tags = ["manual"], target_die_utilization_percentage = "10", ) diff --git a/xls/modules/zstd/frame_header.x b/xls/modules/zstd/frame_header.x deleted file mode 100644 index 858d64ac53..0000000000 --- a/xls/modules/zstd/frame_header.x +++ /dev/null @@ -1,692 +0,0 @@ -// 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. - -// This file contains utilities related to ZSTD Frame Header parsing. -// More information about the ZSTD Frame Header can be found in: -// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.1 - -import std; -import xls.modules.zstd.buffer as buff; - -type Buffer = buff::Buffer; -type BufferStatus = buff::BufferStatus; -type BufferResult = buff::BufferResult; - -pub type WindowSize = u64; -type FrameContentSize = u64; -type DictionaryId = u32; - -// Maximal mantissa value for calculating maximal accepted window_size -// as per https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor -const MAX_MANTISSA = WindowSize:0b111; - -// Structure for holding ZSTD Frame_Header_Descriptor data, as in: -// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.1.1 -pub struct FrameHeaderDescriptor { - frame_content_size_flag: u2, - single_segment_flag: u1, - unused: u1, - reserved: u1, - content_checksum_flag: u1, - dictionary_id_flag: u2, -} - -// Structure for data obtained from decoding the Frame_Header_Descriptor -pub struct FrameHeader { - window_size: WindowSize, - frame_content_size: FrameContentSize, - dictionary_id: DictionaryId, - content_checksum_flag: u1, -} - -// Status values reported by the frame header parsing function -pub enum FrameHeaderStatus: u2 { - OK = 0, - CORRUPTED = 1, - NO_ENOUGH_DATA = 2, - UNSUPPORTED_WINDOW_SIZE = 3, -} - -// structure for returning results of parsing a frame header -pub struct FrameHeaderResult { - status: FrameHeaderStatus, - header: FrameHeader, - buffer: Buffer, -} - -// Auxiliary constant that can be used to initialize Proc's state -// with empty FrameHeader, because `zero!` cannot be used in that context -pub const ZERO_FRAME_HEADER = zero!(); -pub const FRAME_CONTENT_SIZE_NOT_PROVIDED_VALUE = FrameContentSize::MAX; - -// Extracts Frame_Header_Descriptor fields from 8-bit chunk of data -// that is assumed to be a valid Frame_Header_Descriptor -fn extract_frame_header_descriptor(data:u8) -> FrameHeaderDescriptor { - FrameHeaderDescriptor { - frame_content_size_flag: data[6:8], - single_segment_flag: data[5:6], - unused: data[4:5], - reserved: data[3:4], - content_checksum_flag: data[2:3], - dictionary_id_flag: data[0:2], - } -} - -#[test] -fn test_extract_frame_header_descriptor() { - assert_eq( - extract_frame_header_descriptor(u8:0xA4), - FrameHeaderDescriptor { - frame_content_size_flag: u2:0x2, - single_segment_flag: u1:0x1, - unused: u1:0x0, - reserved: u1:0x0, - content_checksum_flag: u1:0x1, - dictionary_id_flag: u2:0x0 - } - ); - - assert_eq( - extract_frame_header_descriptor(u8:0x0), - FrameHeaderDescriptor { - frame_content_size_flag: u2:0x0, - single_segment_flag: u1:0x0, - unused: u1:0x0, - reserved: u1:0x0, - content_checksum_flag: u1:0x0, - dictionary_id_flag: u2:0x0 - } - ); -} - -// Parses a Buffer and extracts information from the Frame_Header_Descriptor. -// The Buffer is assumed to contain a valid Frame_Header_Descriptor. The function -// returns BufferResult with the outcome of the operations on the buffer and -// information extracted from the Frame_Header_Descriptor -fn parse_frame_header_descriptor(buffer: Buffer) -> (BufferResult, FrameHeaderDescriptor) { - let (result, data) = buff::buffer_fixed_pop_checked(buffer); - match result.status { - BufferStatus::OK => { - let frame_header_desc = extract_frame_header_descriptor(data); - (result, frame_header_desc) - }, - _ => (result, zero!()) - } -} - -#[test] -fn test_parse_frame_header_descriptor() { - let buffer = Buffer { content: u32:0xA4, length: u32:8 }; - let (result, header) = parse_frame_header_descriptor(buffer); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0, length: u32:0 }, - }); - assert_eq(header, FrameHeaderDescriptor { - frame_content_size_flag: u2:0x2, - single_segment_flag: u1:0x1, - unused: u1:0x0, - reserved: u1:0x0, - content_checksum_flag: u1:0x1, - dictionary_id_flag: u2:0x0 - }); - - let buffer = Buffer { content: u32:0x0, length: u32:8 }; - let (result, header) = parse_frame_header_descriptor(buffer); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0, length: u32:0 }, - }); - assert_eq(header, FrameHeaderDescriptor { - frame_content_size_flag: u2:0x0, - single_segment_flag: u1:0x0, - unused: u1:0x0, - reserved: u1:0x0, - content_checksum_flag: u1:0x0, - dictionary_id_flag: u2:0x0 - }); - - let buffer = Buffer { content: u32:0x0, length: u32:0 }; - let (result, header) = parse_frame_header_descriptor(buffer); - assert_eq(result, BufferResult { - status: BufferStatus::NO_ENOUGH_DATA, - buffer: Buffer { content: u32:0, length: u32:0 }, - }); - assert_eq(header, zero!()); -} - -// Returns a boolean showing if the Window_Descriptor section exists -// for the frame with the given FrameHeaderDescriptor -fn window_descriptor_exists(desc: FrameHeaderDescriptor) -> bool { - desc.single_segment_flag == u1:0 -} - -#[test] -fn test_window_descriptor_exists() { - let zero_desc = zero!(); - - let desc_with_ss = FrameHeaderDescriptor {single_segment_flag: u1:1, ..zero_desc}; - assert_eq(window_descriptor_exists(desc_with_ss), false); - - let desc_without_ss = FrameHeaderDescriptor {single_segment_flag: u1:0, ..zero_desc}; - assert_eq(window_descriptor_exists(desc_without_ss), true); -} - -// Extracts window size from 8-bit chunk of data -// that is assumed to be a valid Window_Descriptor -fn extract_window_size_from_window_descriptor(data: u8) -> u64 { - let exponent = data >> u8:3; - let mantissa = data & u8:7; - - let window_base = u64:1 << (u64:10 + exponent as u64); - let window_add = (window_base >> u64:3) * (mantissa as u64); - - window_base + window_add -} - -#[test] -fn test_extract_window_size_from_window_descriptor() { - assert_eq(extract_window_size_from_window_descriptor(u8:0x0), u64:0x400); - assert_eq(extract_window_size_from_window_descriptor(u8:0x9), u64:0x900); - assert_eq(extract_window_size_from_window_descriptor(u8:0xFF), u64:0x3c000000000); -} - -// Parses a Buffer with data and extracts information from the Window_Descriptor -// The buffer is assumed to contain a valid Window_Descriptor that is related to -// the same frame as the provided FrameHeaderDescriptor. The function returns -// BufferResult with the outcome of the operations on the buffer and window size. -fn parse_window_descriptor(buffer: Buffer, desc: FrameHeaderDescriptor) -> (BufferResult, WindowSize) { - assert!(window_descriptor_exists(desc), "window_descriptor_does_not_exist"); - - let (result, data) = buff::buffer_fixed_pop_checked(buffer); - match result.status { - BufferStatus::OK => { - let window_size = extract_window_size_from_window_descriptor(data); - (result, window_size) - }, - _ => (result, u64:0) - } -} - -#[test] -fn test_parse_window_descriptor() { - let zero_desc = zero!(); - let desc_without_ss = FrameHeaderDescriptor {single_segment_flag: u1:0, ..zero_desc}; - - let buffer = Buffer { content: u32:0xF, length: u32:0x4 }; - let (result, window_size) = parse_window_descriptor(buffer, desc_without_ss); - assert_eq(result, BufferResult { - status: BufferStatus::NO_ENOUGH_DATA, - buffer: Buffer { content: u32:0xF, length: u32:0x4 }, - }); - assert_eq(window_size, u64:0); - - let buffer = Buffer { content: u32:0x0, length: u32:0x8 }; - let (result, window_size) = parse_window_descriptor(buffer, desc_without_ss); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0 }, - }); - assert_eq(window_size, u64:0x400); - - let buffer = Buffer { content: u32:0x9, length: u32:0x8 }; - let (result, window_size) = parse_window_descriptor(buffer, desc_without_ss); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0 }, - }); - assert_eq(window_size, u64:0x900); - - let buffer = Buffer { content: u32:0xFF, length: u32:0x8 }; - let (result, window_size) = parse_window_descriptor(buffer, desc_without_ss); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0 }, - }); - assert_eq(window_size, u64:0x3c000000000); -} - -// Parses a Buffer with data and extracts information from the Dictionary_ID -// The buffer is assumed to contain a valid Dictionary_ID that is related to -// the same frame as the provided FrameHeaderDescriptor. The function returns -// BufferResult with the outcome of the operations on the buffer and dictionary ID -fn parse_dictionary_id(buffer: Buffer, desc: FrameHeaderDescriptor) -> (BufferResult, DictionaryId) { - let bytes = match desc.dictionary_id_flag { - u2:0 => u32:0, - u2:1 => u32:1, - u2:2 => u32:2, - u2:3 => u32:4, - _ => fail!("not_possible", u32:0) - }; - - let (result, data) = buff::buffer_pop_checked(buffer, bytes * u32:8); - match result.status { - BufferStatus::OK => (result, data as u32), - _ => (result, u32:0) - } -} - -#[test] -fn test_parse_dictionary_id() { - let zero_desc = zero!(); - - let buffer = Buffer { content: u32:0x0, length: u32:0x0 }; - let frame_header_desc = FrameHeaderDescriptor { dictionary_id_flag: u2:0, ..zero_desc}; - let (result, dictionary_id) = parse_dictionary_id(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0x0 }, - }); - assert_eq(dictionary_id, u32:0); - - let buffer = Buffer { content: u32:0x12, length: u32:0x8 }; - let frame_header_desc = FrameHeaderDescriptor { dictionary_id_flag: u2:0x1, ..zero_desc}; - let (result, dictionary_id) = parse_dictionary_id(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0 }, - }); - assert_eq(dictionary_id, u32:0x12); - - let buffer = Buffer { content: u32:0x1234, length: u32:0x10 }; - let frame_header_desc = FrameHeaderDescriptor { dictionary_id_flag: u2:0x2, ..zero_desc}; - let (result, dictionary_id) = parse_dictionary_id(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0 }, - }); - assert_eq(dictionary_id, u32:0x1234); - - let buffer = Buffer { content: u32:0x12345678, length: u32:0x20 }; - let frame_header_desc = FrameHeaderDescriptor { dictionary_id_flag: u2:0x3, ..zero_desc}; - let (result, dictionary_id) = parse_dictionary_id(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u32:0x0, length: u32:0 }, - }); - assert_eq(dictionary_id, u32:0x12345678); - - let buffer = Buffer { content: u32:0x1234, length: u32:0x10 }; - let frame_header_desc = FrameHeaderDescriptor { dictionary_id_flag: u2:0x3, ..zero_desc}; - let (result, dictionary_id) = parse_dictionary_id(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::NO_ENOUGH_DATA, - buffer: Buffer { content: u32:0x1234, length: u32:0x10 }, - }); - assert_eq(dictionary_id, u32:0x0); -} - -// Returns boolean showing if the Frame_Content_Size section exists for -// the frame with the given FrameHeaderDescriptor. -fn frame_content_size_exists(desc: FrameHeaderDescriptor) -> bool { - desc.single_segment_flag != u1:0 || desc.frame_content_size_flag != u2:0 -} - -#[test] -fn test_frame_content_size_exists() { - let zero_desc = zero!(); - - let desc = FrameHeaderDescriptor {single_segment_flag: u1:0, frame_content_size_flag: u2:0, ..zero_desc}; - assert_eq(frame_content_size_exists(desc), false); - - let desc = FrameHeaderDescriptor {single_segment_flag: u1:0, frame_content_size_flag: u2:2, ..zero_desc}; - assert_eq(frame_content_size_exists(desc), true); - - let desc = FrameHeaderDescriptor {single_segment_flag: u1:1, frame_content_size_flag: u2:0, ..zero_desc}; - assert_eq(frame_content_size_exists(desc), true); - - let desc = FrameHeaderDescriptor {single_segment_flag: u1:1, frame_content_size_flag: u2:3, ..zero_desc}; - assert_eq(frame_content_size_exists(desc), true); -} - -// Parses a Buffer with data and extracts information from the Frame_Content_Size -// The buffer is assumed to contain a valid Frame_Content_Size that is related to -// the same frame as the provided FrameHeaderDescriptor. The function returns -// BufferResult with the outcome of the operations on the buffer and frame content size. -fn parse_frame_content_size(buffer: Buffer, desc: FrameHeaderDescriptor) -> (BufferResult, FrameContentSize) { - assert!(frame_content_size_exists(desc), "frame_content_size_does_not_exist"); - - let bytes = match desc.frame_content_size_flag { - u2:0 => u32:1, - u2:1 => u32:2, - u2:2 => u32:4, - u2:3 => u32:8, - _ => fail!("not_possible", u32:0) - }; - - let (result, data) = buff::buffer_pop_checked(buffer, bytes * u32:8); - match (result.status, bytes) { - (BufferStatus::OK, u32:2) => (result, data as u64 + u64:256), - (BufferStatus::OK, _) => (result, data as u64), - (_, _) => (result, u64:0) - } -} - -#[test] -fn test_parse_frame_content_size() { - let zero_desc = zero!(); - - let buffer = Buffer { content: u64:0x12, length: u32:8 }; - let frame_header_desc = FrameHeaderDescriptor { - frame_content_size_flag: u2:0, - single_segment_flag: u1:1, - ..zero_desc - }; - let (result, frame_content_size) = parse_frame_content_size(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u64:0x0, length: u32:0x0 }, - }); - assert_eq(frame_content_size, u64:0x12); - - let buffer = Buffer { content: u64:0x1234, length: u32:0x10 }; - let frame_header_desc = FrameHeaderDescriptor { frame_content_size_flag: u2:1, ..zero_desc}; - let (result, frame_content_size) = parse_frame_content_size(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u64:0x0, length: u32:0x0 }, - }); - assert_eq(frame_content_size, u64:0x1234 + u64:256); - - let buffer = Buffer { content: u64:0x12345678, length: u32:0x20 }; - let frame_header_desc = FrameHeaderDescriptor { frame_content_size_flag: u2:2, ..zero_desc}; - let (result, frame_content_size) = parse_frame_content_size(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u64:0x0, length: u32:0x0 }, - }); - assert_eq(frame_content_size, u64:0x12345678); - - let buffer = Buffer { content: u64:0x1234567890ABCDEF, length: u32:0x40 }; - let frame_header_desc = FrameHeaderDescriptor { frame_content_size_flag: u2:3, ..zero_desc}; - let (result, frame_content_size) = parse_frame_content_size(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::OK, - buffer: Buffer { content: u64:0x0, length: u32:0x0 }, - }); - assert_eq(frame_content_size, u64:0x1234567890ABCDEF); - - let buffer = Buffer { content: u32:0x12345678, length: u32:0x20 }; - let frame_header_desc = FrameHeaderDescriptor { frame_content_size_flag: u2:0x3, ..zero_desc}; - let (result, frame_content_size) = parse_frame_content_size(buffer, frame_header_desc); - assert_eq(result, BufferResult { - status: BufferStatus::NO_ENOUGH_DATA, - buffer: Buffer { content: u32:0x12345678, length: u32:0x20 }, - }); - assert_eq(frame_content_size, u64:0x0); -} - -// Calculate maximal accepted window_size for given WINDOW_LOG_MAX and return whether given -// window_size should be accepted or discarded. -// Based on window_size calculation from: RFC 8878 -// https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor -fn window_size_valid(window_size: WindowSize) -> bool { - let max_window_size = (WindowSize:1 << WINDOW_LOG_MAX) + (((WindowSize:1 << WINDOW_LOG_MAX) >> WindowSize:3) * MAX_MANTISSA); - - window_size <= max_window_size -} - -// Parses a Buffer with data and extracts Frame_Header information. The buffer -// is assumed to contain a valid Frame_Header The function returns FrameHeaderResult -// with BufferResult that contains outcome of the operations on the Buffer, -// FrameHeader with the extracted frame header if the parsing was successful, -// and the status of the operation in FrameHeaderStatus. On failure, the returned -// buffer is the same as the input buffer. -// WINDOW_LOG_MAX is the base 2 logarithm used for calculating the maximal allowed -// window_size. Frame header parsing function must discard all frames that -// have window_size above the maximal allowed window_size. -// CAPACITY is the buffer capacity -pub fn parse_frame_header(buffer: Buffer) -> FrameHeaderResult { - trace_fmt!("parse_frame_header: ==== Parsing ==== \n"); - trace_fmt!("parse_frame_header: initial buffer: {:#x}", buffer); - - let (result, desc) = parse_frame_header_descriptor(buffer); - trace_fmt!("parse_frame_header: buffer after parsing header descriptor: {:#x}", result.buffer); - - let (result, header) = match result.status { - BufferStatus::OK => { - let (result, window_size) = if window_descriptor_exists(desc) { - trace_fmt!("parse_frame_header: window_descriptor exists, parse it"); - parse_window_descriptor(result.buffer, desc) - } else { - trace_fmt!("parse_frame_header: window_descriptor does not exist, skip parsing it"); - (result, u64:0) - }; - trace_fmt!("parse_frame_header: buffer after parsing window_descriptor: {:#x}", result.buffer); - - match result.status { - BufferStatus::OK => { - trace_fmt!("parse_frame_header: parse dictionary_id"); - let (result, dictionary_id) = parse_dictionary_id(result.buffer, desc); - trace_fmt!("parse_frame_header: buffer after parsing dictionary_id: {:#x}", result.buffer); - - match result.status { - BufferStatus::OK => { - let (result, frame_content_size) = if frame_content_size_exists(desc) { - trace_fmt!("parse_frame_header: frame_content_size exists, parse it"); - parse_frame_content_size(result.buffer, desc) - } else { - trace_fmt!("parse_frame_header: frame_content_size does not exist, skip parsing it"); - (result, FRAME_CONTENT_SIZE_NOT_PROVIDED_VALUE) - }; - trace_fmt!("parse_frame_header: buffer after parsing frame_content_size: {:#x}", result.buffer); - - match result.status { - BufferStatus::OK => { - trace_fmt!("parse_frame_header: calculate frame header!"); - let window_size = match window_descriptor_exists(desc) { - true => window_size, - _ => frame_content_size, - }; - - ( - result, - FrameHeader { - window_size: window_size, - frame_content_size: frame_content_size, - dictionary_id: dictionary_id, - content_checksum_flag: desc.content_checksum_flag, - } - ) - }, - _ => { - trace_fmt!("parse_frame_header: Not enough data to parse frame_content_size!"); - (result, zero!()) - } - } - }, - _ => { - trace_fmt!("parse_frame_header: Not enough data to parse dictionary_id!"); - (result, zero!()) - } - } - }, - _ => { - trace_fmt!("parse_frame_header: Not enough data to parse window_descriptor!"); - (result, zero!()) - } - } - }, - _ => { - trace_fmt!("parse_frame_header: Not enough data to parse frame_header_descriptor!"); - (result, zero!()) - } - }; - - let (status, buffer) = match result.status { - BufferStatus::OK => (FrameHeaderStatus::OK, result.buffer), - _ => (FrameHeaderStatus::NO_ENOUGH_DATA, buffer) - }; - - let frame_header_result = FrameHeaderResult { status: status, header: header, buffer: buffer }; - - // libzstd always reports NO_ENOUGH_DATA errors before CORRUPTED caused by - // reserved bit being set - if (desc.reserved == u1:1 && frame_header_result.status != FrameHeaderStatus::NO_ENOUGH_DATA) { - trace_fmt!("parse_frame_header: frame descriptor corrupted!"); - // Critical failure - requires resetting the whole decoder - FrameHeaderResult { - status: FrameHeaderStatus::CORRUPTED, - buffer: zero!(), - header: zero!(), - } - } else if (!window_size_valid(header.window_size)) { - trace_fmt!("parse_frame_header: frame discarded: window_size to big: {}", header.window_size); - FrameHeaderResult { - status: FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE, - buffer: zero!(), - header: zero!(), - } - } else { - frame_header_result - } -} - -// The largest allowed WindowLog for DSLX tests -pub const TEST_WINDOW_LOG_MAX = WindowSize:22; - -#[test] -fn test_parse_frame_header() { - // normal cases - let buffer = Buffer { content: bits[128]:0x1234567890ABCDEF_CAFE_09_C2, length: u32:96 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::OK, - buffer: Buffer { - content: bits[128]:0x0, - length: u32:0, - }, - header: FrameHeader { - window_size: u64:0x900, - frame_content_size: u64:0x1234567890ABCDEF, - dictionary_id: u32:0xCAFE, - content_checksum_flag: u1:0, - } - }); - - // SingleSegmentFlag is set and FrameContentSize is bigger than accepted window_size - let buffer = Buffer { content: bits[128]:0x1234567890ABCDEF_CAFE_E2, length: u32:88 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE, - buffer: Buffer { content: bits[128]:0x0, length: u32:0 }, - header: zero!() - }); - - let buffer = Buffer { content: bits[128]:0xaa20, length: u32:16 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::OK, - buffer: Buffer { - content: bits[128]:0x0, - length: u32:0, - }, - header: FrameHeader { - window_size: u64:0xaa, - frame_content_size: u64:0xaa, - dictionary_id: u32:0x0, - content_checksum_flag: u1:0, - }, - }); - - // when buffer is too short - let buffer = Buffer { content: bits[128]:0x0, length: u32:0 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::NO_ENOUGH_DATA, - buffer: buffer, - header: zero!() - }); - - let buffer = Buffer { content: bits[128]:0xC2, length: u32:8 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::NO_ENOUGH_DATA, - buffer: buffer, - header: zero!() - }); - - let buffer = Buffer { content: bits[128]:0x09_C2, length: u32:16 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::NO_ENOUGH_DATA, - buffer: buffer, - header: zero!() - }); - - let buffer = Buffer { content: bits[128]:0x1234_09_C2, length: u32:32 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::NO_ENOUGH_DATA, - buffer: buffer, - header: zero!() - }); - - let buffer = Buffer { content: bits[128]:0x1234_09_C2, length: u32:32 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::NO_ENOUGH_DATA, - buffer: buffer, - header: zero!() - }); - - // when frame header descriptor is corrupted - let buffer = Buffer { content: bits[128]:0x1234567890ABCDEF_1234_09_CA, length: u32:96 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::CORRUPTED, - buffer: Buffer { content: bits[128]:0x0, length: u32:0 }, - header: zero!() - }); - - // Frame Header is discarded because Window size required by frame is too big for given decoder - // configuration - let buffer = Buffer { content: bits[128]:0xd310, length: u32:16 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE, - buffer: Buffer { content: bits[128]:0x0, length: u32:0 }, - header: zero!() - }); - - // Frame Header is discarded because Frame Content Size required by frame is too big for given decoder - // configuration - let buffer = Buffer { content: bits[128]:0xf45b5b5b0db1, length: u32:48 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE, - buffer: Buffer { content: bits[128]:0x0, length: u32:0 }, - header: FrameHeader { - window_size: u64:0x0, - frame_content_size: u64:0x0, - dictionary_id: u32:0x0, - content_checksum_flag: u1:0, - }, - }); - - // Frame Header is discarded because Frame Content Size required by frame is too big (above 64bits) for given decoder - // configuration - let buffer = Buffer { content: bits[128]:0xc0659db6813a16b33f3da53a79e4, length: u32:112 }; - let frame_header_result = parse_frame_header(buffer); - assert_eq(frame_header_result, FrameHeaderResult { - status: FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE, - buffer: Buffer { content: bits[128]:0x0, length: u32:0 }, - header: FrameHeader { - window_size: u64:0x0, - frame_content_size: u64:0x0, - dictionary_id: u32:0x0, - content_checksum_flag: u1:0, - }, - }); -} diff --git a/xls/modules/zstd/frame_header_dec.x b/xls/modules/zstd/frame_header_dec.x new file mode 100644 index 0000000000..8647435996 --- /dev/null +++ b/xls/modules/zstd/frame_header_dec.x @@ -0,0 +1,670 @@ +// 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. + +// This file contains utilities related to ZSTD Frame Header parsing. +// More information about the ZSTD Frame Header can be found in: +// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.1 + +import std; +import xls.modules.zstd.memory.mem_reader; + +pub type WindowSize = u64; +pub type FrameContentSize = u64; +pub type DictionaryId = u32; + +// Structure for data obtained from decoding the Frame_Header_Descriptor +pub struct FrameHeader { + window_size: WindowSize, + frame_content_size: FrameContentSize, + dictionary_id: DictionaryId, + content_checksum_flag: u1, +} + +// Status values reported by the frame header parsing function +pub enum FrameHeaderDecoderStatus: u2 { + OKAY = 0, + CORRUPTED = 1, + UNSUPPORTED_WINDOW_SIZE = 2, +} + +pub struct FrameHeaderDecoderReq { + addr: uN[ADDR_W], +} + +pub struct FrameHeaderDecoderResp { + status: FrameHeaderDecoderStatus, + header: FrameHeader, + length: u5, +} + +// Maximal mantissa value for calculating maximal accepted window_size +// as per https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor +const MAX_MANTISSA = WindowSize:0b111; + +// Structure for holding ZSTD Frame_Header_Descriptor data, as in: +// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.1.1 +pub struct FrameHeaderDescriptor { + frame_content_size_flag: u2, + single_segment_flag: u1, + unused: u1, + reserved: u1, + content_checksum_flag: u1, + dictionary_id_flag: u2, +} + +// Auxiliary constant that can be used to initialize Proc's state +// with empty FrameHeader, because `zero!` cannot be used in that context +pub const ZERO_FRAME_HEADER = zero!(); +pub const FRAME_CONTENT_SIZE_NOT_PROVIDED_VALUE = FrameContentSize::MAX; + +// Extracts Frame_Header_Descriptor fields from 8-bit chunk of data +// that is assumed to be a valid Frame_Header_Descriptor +fn extract_frame_header_descriptor(data:u8) -> FrameHeaderDescriptor { + FrameHeaderDescriptor { + frame_content_size_flag: data[6:8], + single_segment_flag: data[5:6], + unused: data[4:5], + reserved: data[3:4], + content_checksum_flag: data[2:3], + dictionary_id_flag: data[0:2], + } +} + +#[test] +fn test_extract_frame_header_descriptor() { + assert_eq( + extract_frame_header_descriptor(u8:0xA4), + FrameHeaderDescriptor { + frame_content_size_flag: u2:0x2, + single_segment_flag: u1:0x1, + unused: u1:0x0, + reserved: u1:0x0, + content_checksum_flag: u1:0x1, + dictionary_id_flag: u2:0x0 + } + ); + + assert_eq( + extract_frame_header_descriptor(u8:0x0), + FrameHeaderDescriptor { + frame_content_size_flag: u2:0x0, + single_segment_flag: u1:0x0, + unused: u1:0x0, + reserved: u1:0x0, + content_checksum_flag: u1:0x0, + dictionary_id_flag: u2:0x0 + } + ); +} + +// Returns a boolean showing if the Window_Descriptor section exists +// for the frame with the given FrameHeaderDescriptor +fn window_descriptor_exists(desc: FrameHeaderDescriptor) -> bool { + desc.single_segment_flag == u1:0 +} + +#[test] +fn test_window_descriptor_exists() { + let zero_desc = zero!(); + + let desc_with_ss = FrameHeaderDescriptor {single_segment_flag: u1:1, ..zero_desc}; + assert_eq(window_descriptor_exists(desc_with_ss), false); + + let desc_without_ss = FrameHeaderDescriptor {single_segment_flag: u1:0, ..zero_desc}; + assert_eq(window_descriptor_exists(desc_without_ss), true); +} + +// Extracts window size from 8-bit chunk of data +// that is assumed to be a valid Window_Descriptor +fn extract_window_size_from_window_descriptor(data: u8) -> u64 { + let exponent = data[3:8]; + let mantissa = data[0:3]; + + let window_base = (u42:1 << (u6:10 + exponent as u6)); + let window_base_add = (window_base >> u2:3) as u42; + // optimization: perform multiplication by a 3-bit value with adds and shifts + // because XLS only allows multiplying operands of the same width + let window_add = match mantissa { + u3:0 => u42:0, + u3:1 => window_base_add, // u39 + u3:2 => window_base_add + window_base_add, // u39 + u39 = u40 + u3:3 => (window_base_add << u1:1) + window_base_add, // u40 + u39 = u41 + u3:4 => (window_base_add << u1:1) + (window_base_add << u1:1), // u40 + u40 = u41 + u3:5 => (window_base_add << u2:2) + window_base_add, // u41 + u39 = u42 + u3:6 => (window_base_add << u2:2) + (window_base_add << u2:1), // u41 + u40 = u42 + u3:7 => (window_base_add << u2:3) - window_base_add, // u42 - u39 = u42 + _ => fail!("extract_window_size_from_window_descriptor_unreachable", u42:0), + }; + + window_base as u64 + window_add as u64 +} + +#[test] +fn test_extract_window_size_from_window_descriptor() { + assert_eq(extract_window_size_from_window_descriptor(u8:0x0), u64:0x400); + assert_eq(extract_window_size_from_window_descriptor(u8:0x9), u64:0x900); + assert_eq(extract_window_size_from_window_descriptor(u8:0xFF), u64:0x3c000000000); +} + +// Returns boolean showing if the Frame_Content_Size section exists for +// the frame with the given FrameHeaderDescriptor. +fn frame_content_size_exists(desc: FrameHeaderDescriptor) -> bool { + desc.single_segment_flag != u1:0 || desc.frame_content_size_flag != u2:0 +} + +#[test] +fn test_frame_content_size_exists() { + let zero_desc = zero!(); + + let desc = FrameHeaderDescriptor {single_segment_flag: u1:0, frame_content_size_flag: u2:0, ..zero_desc}; + assert_eq(frame_content_size_exists(desc), false); + + let desc = FrameHeaderDescriptor {single_segment_flag: u1:0, frame_content_size_flag: u2:2, ..zero_desc}; + assert_eq(frame_content_size_exists(desc), true); + + let desc = FrameHeaderDescriptor {single_segment_flag: u1:1, frame_content_size_flag: u2:0, ..zero_desc}; + assert_eq(frame_content_size_exists(desc), true); + + let desc = FrameHeaderDescriptor {single_segment_flag: u1:1, frame_content_size_flag: u2:3, ..zero_desc}; + assert_eq(frame_content_size_exists(desc), true); +} + + +// Calculate maximal accepted window_size for given WINDOW_LOG_MAX and return whether given +// window_size should be accepted or discarded. +// Based on window_size calculation from: RFC 8878 +// https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor +fn window_size_valid(window_size: WindowSize) -> bool { + let max_window_size = (WindowSize:1 << WINDOW_LOG_MAX) + (((WindowSize:1 << WINDOW_LOG_MAX) >> WindowSize:3) * MAX_MANTISSA); + + window_size <= max_window_size +} + + +pub fn parse_frame_header(header_raw: uN[112]) -> (FrameHeader, u4, u1) { + let fhd_raw = header_raw[0:8]; + let fhd = extract_frame_header_descriptor(fhd_raw); + // RFC8878 Section 3.1.1.1.1.4 + // "This [reserved] bit is reserved for some future feature. Its value + // must be zero. A decoder compliant with this specification version must + // ensure it is not set." + let header_ok = !fhd.reserved; + + let window_descriptor_start = u32:1; + // RFC8878 Section 3.1.1.1.2 + // "When Single_Segment_Flag is set, Window_Descriptor is not present." + let window_descriptor_len = match fhd.single_segment_flag { + u1:0 => u1:1, + u1:1 => u1:0, + _ => fail!("window_descriptor_len_unreachable", u1:0), + }; + let window_descriptor_raw = header_raw[u32:8*window_descriptor_start+:u8]; + let window_size = extract_window_size_from_window_descriptor(window_descriptor_raw); + + let dictionary_id_start = window_descriptor_start + window_descriptor_len as u32; + let dictionary_id_len = match fhd.dictionary_id_flag { + u2:0 => u32:0, + u2:1 => u32:1, + u2:2 => u32:2, + u2:3 => u32:4, + _ => fail!("dictionary_id_len_unreachable", u32:0), + }; + let dictionary_id_raw = header_raw[u32:8*dictionary_id_start+:u32]; + let dictionary_id = dictionary_id_raw & match fhd.dictionary_id_flag { + u2:0 => u32:0x0000_0000, + u2:1 => u32:0x0000_00ff, + u2:2 => u32:0x0000_ffff, + u2:3 => u32:0xffff_ffff, + _ => fail!("dictionary_id_unreachable", u32:0), + }; + + let frame_content_size_start = dictionary_id_start + dictionary_id_len; + // RFC8878 Section 3.1.1.1.1.1 + // "When Frame_Content_Size_Flag is 0, FCS_Field_Size depends on + // Single_Segment_Flag: If Single_Segment_Flag is set, FCS_Field_Siz + // is 1. Otherwise, FCS_Field_Size is 0;" + let frame_content_size_len = match (fhd.frame_content_size_flag, fhd.single_segment_flag) { + (u2:0, u1:0) => u32:0, + (u2:0, u1:1) => u32:1, + (u2:1, _) => u32:2, + (u2:2, _) => u32:4, + (u2:3, _) => u32:8, + _ => fail!("frame_content_size_len_unreachable", u32:0), + }; + + let frame_content_size_raw = header_raw[u32:8*frame_content_size_start+:u64]; + let frame_content_size_masked = frame_content_size_raw & match frame_content_size_len { + u32:0 => u64:0x0000_0000_0000_0000, + u32:1 => u64:0x0000_0000_0000_00ff, + u32:2 => u64:0x0000_0000_0000_ffff, + u32:4 => u64:0x0000_0000_ffff_ffff, + u32:8 => u64:0xffff_ffff_ffff_ffff, + _ => fail!("frame_content_size_masked_unreachable", u64:0), + }; + + // RFC8878 Section 3.1.1.1.4 + // "When FCS_Field_Size is 2, the offset of 256 is added." + let frame_content_size = frame_content_size_masked + match frame_content_size_len { + u32:2 => u64:256, + _ => u64:0, + }; + + // RFC8878 Section 3.1.1.1.2 + // "When Single_Segment_Flag is set, Window_Descriptor is not present. + // In this case, Window_Size is Frame_Content_Size [...]" + let window_size = if (window_descriptor_exists(fhd)) { + window_size + } else if (frame_content_size_exists(fhd)) { + frame_content_size + } else { + WindowSize:0 + }; + + let total_header_len = (frame_content_size_start + frame_content_size_len) as u4; + + (FrameHeader { + window_size: window_size, + frame_content_size: if frame_content_size_len != u32:0 { frame_content_size } else { FrameContentSize:0 }, + dictionary_id: if dictionary_id_len != u32:0 { dictionary_id } else { DictionaryId:0 }, + content_checksum_flag: fhd.content_checksum_flag, + }, total_header_len, header_ok) +} + + +#[test] +fn test_parse_frame_header() { + // normal case + let test_vec = uN[112]:0x1234567890ABCDEF_CAFE_09_C2; + let (frame_header_result, len, ok) = parse_frame_header(test_vec); + assert_eq(frame_header_result, FrameHeader { + window_size: u64:0x900, + frame_content_size: u64:0x1234567890ABCDEF, + dictionary_id: u32:0xCAFE, + content_checksum_flag: u1:0, + }); + assert_eq(len, u4:12); + assert_eq(ok, u1:1); + + // SingleSegmentFlag is set + let test_vec = uN[112]:0xaa20; + let (frame_header_result, len, ok) = parse_frame_header(test_vec); + assert_eq(frame_header_result, FrameHeader { + window_size: u64:0xaa, + frame_content_size: u64:0xaa, + dictionary_id: u32:0x0, + content_checksum_flag: u1:0, + }); + assert_eq(len, u4:2); + assert_eq(ok, u1:1); + + // SingleSegmentFlag is set and FrameContentSize is bigger than accepted window_size + let test_vec = uN[112]:0x1234567890ABCDEF_CAFE_E2; + let (frame_header_result, len, ok) = parse_frame_header(test_vec); + assert_eq(frame_header_result, FrameHeader { + window_size: u64:0x1234567890ABCDEF, + frame_content_size: u64:0x1234567890ABCDEF, + dictionary_id: u32:0xCAFE, + content_checksum_flag: u1:0, + }); + assert_eq(len, u4:11); + assert_eq(ok, u1:1); + + // Frame header descriptor is corrupted (we don't check frame header and length) + let test_vec = uN[112]:0x1234567890ABCDEF_1234_09_CA; + let (_, _, ok) = parse_frame_header(test_vec); + assert_eq(ok, u1:0); + + // Large window size + let test_vec = uN[112]:0xd310; + let (frame_header_result, len, ok) = parse_frame_header(test_vec); + assert_eq(frame_header_result, FrameHeader { + window_size: u64:0x1600000000, + ..zero!() + }); + assert_eq(len, u4:2); + assert_eq(ok, u1:1); + + // Large window size + let test_vec = uN[112]:0xf45b5b5b0db1; + let (frame_header_result, len, ok) = parse_frame_header(test_vec); + assert_eq(frame_header_result, FrameHeader { + window_size: u64:0xf45b5b5b, + frame_content_size: u64:0xf45b5b5b, + dictionary_id: u32:0xD, + content_checksum_flag: u1:0, + }); + assert_eq(len, u4:6); + assert_eq(ok, u1:1); + + // Large window size + let test_vec = uN[112]:0xc0659db6813a16b33f3da53a79e4; + let (frame_header_result, len, ok) = parse_frame_header(test_vec); + assert_eq(frame_header_result, FrameHeader { + window_size: u64:0x3a16b33f3da53a79, + frame_content_size: u64:0x3a16b33f3da53a79, + dictionary_id: u32:0, + content_checksum_flag: u1:1, + }); + assert_eq(len, u4:9); + assert_eq(ok, u1:1); +} + + +enum FrameHeaderDecoderFsm: u1 { + RECV = 0, + RESP = 1 +} + +// Magic number value, as in: +// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1 +const MAGIC_NUMBER = u32:0xFD2FB528; +const MAGIC_NUMBER_LEN = u32:4; + +const MAX_HEADER_LEN = u32:14; +const MAX_MAGIC_PLUS_HEADER_LEN = MAGIC_NUMBER_LEN + MAX_HEADER_LEN; + +struct FrameHeaderDecoderState { + fsm: FrameHeaderDecoderFsm, + xfers: u32, + raw_header: uN[XFER_SIZE][XFER_COUNT], +} + +pub proc FrameHeaderDecoder< + WINDOW_LOG_MAX: u32, + DATA_W: u32, + ADDR_W: u32, + XFERS_FOR_HEADER: u32 = {((MAX_MAGIC_PLUS_HEADER_LEN * u32:8) / DATA_W) + u32:1}, +> { + type State = FrameHeaderDecoderState; + type Fsm = FrameHeaderDecoderFsm; + type Req = FrameHeaderDecoderReq; + type Resp = FrameHeaderDecoderResp; + type ReaderReq = mem_reader::MemReaderReq; + type ReaderResp = mem_reader::MemReaderResp; + + reader_req_s: chan out; + reader_resp_r: chan in; + + decode_req_r: chan in; + decode_resp_s: chan out; + + config( + reader_req_s: chan out, + reader_resp_r: chan in, + decode_req_r: chan in, + decode_resp_s: chan out + ) { + (reader_req_s, reader_resp_r, decode_req_r, decode_resp_s) + } + + init { zero!() } + + next(state: State) { + type ReaderReq = mem_reader::MemReaderReq; + type State = FrameHeaderDecoderState; + + let tok0 = join(); + let (tok_req, req, do_req) = recv_non_blocking(tok0, decode_req_r, zero!()); + send_if(tok_req, reader_req_s, do_req, ReaderReq { addr: req.addr, length: MAX_MAGIC_PLUS_HEADER_LEN as uN[ADDR_W] }); + + let do_recv = (state.fsm == Fsm::RECV); + let (tok, resp, recvd) = recv_if_non_blocking(tok0, reader_resp_r, do_recv, zero!()); + + let do_resp = (state.fsm == Fsm::RESP); + let raw_header_bits = state.raw_header as uN[DATA_W * XFERS_FOR_HEADER]; + let raw_magic_number = raw_header_bits[:s32:8 * MAGIC_NUMBER_LEN as s32]; + let raw_header = raw_header_bits[s32:8 * MAGIC_NUMBER_LEN as s32 : s32:8 * MAX_MAGIC_PLUS_HEADER_LEN as s32]; + let magic_number_ok = raw_magic_number == MAGIC_NUMBER; + let (decoded_header, header_len, header_ok) = parse_frame_header(raw_header); + + let status = if (!header_ok || !magic_number_ok) { + FrameHeaderDecoderStatus::CORRUPTED + } else if (!window_size_valid(decoded_header.window_size)) { + FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE + } else { + FrameHeaderDecoderStatus::OKAY + }; + + let header_result = FrameHeaderDecoderResp { + status: status, + header: decoded_header, + length: header_len as u5 + MAGIC_NUMBER_LEN as u5, + }; + + send_if(tok0, decode_resp_s, do_resp, header_result); + + let next_state = match (state.fsm) { + Fsm::RECV => { + if (recvd) { + // raw_header is updated from the highest to lowest index because + // highest index in an array contains least significant bytes when + // casting to a bit vector + let update_idx = XFERS_FOR_HEADER - state.xfers - u32:1; + let next_raw_header = update(state.raw_header, update_idx, resp.data); + if (resp.last) { + State { raw_header: next_raw_header, fsm: Fsm::RESP, ..state } + } else { + State { raw_header: next_raw_header, xfers: state.xfers + u32:1, ..state } + } + } else { + state + } + }, + Fsm::RESP => { + State { fsm: Fsm::RECV, xfers: u32:0, ..state } + }, + _ => fail!("FrameHeaderDecoder_fsm_unreachable", zero!()) + }; + + next_state + } +} + +// The largest allowed WindowLog for DSLX tests +pub const TEST_WINDOW_LOG_MAX = u32:22; +pub const TEST_DATA_W = u32:32; +pub const TEST_ADDR_W = u32:16; +pub const TEST_XFERS_FOR_HEADER = ((MAX_MAGIC_PLUS_HEADER_LEN * u32:8) / TEST_DATA_W) + u32:1; + +#[test_proc] +proc FrameHeaderDecoderTest { + type Req = FrameHeaderDecoderReq; + type Resp = FrameHeaderDecoderResp; + type ReaderReq = mem_reader::MemReaderReq; + type ReaderResp = mem_reader::MemReaderResp; + + terminator: chan out; + + reader_req_r: chan in; + reader_resp_s: chan out; + + decode_req_s: chan out; + decode_resp_r: chan in; + + config(terminator: chan out) { + let (reader_req_s, reader_req_r) = chan("reader_req"); + let (reader_resp_s, reader_resp_r) = chan("reader_resp"); + let (decode_req_s, decode_req_r) = chan("decode_req"); + let (decode_resp_s, decode_resp_r) = chan("decode_resp"); + spawn FrameHeaderDecoder( + reader_req_s, + reader_resp_r, + decode_req_r, + decode_resp_s + ); + (terminator, reader_req_r, reader_resp_s, decode_req_s, decode_resp_r) + } + + init {} + + next(state: ()) { + let tok = join(); + let tests: (u32[TEST_XFERS_FOR_HEADER], FrameHeaderDecoderResp)[7] = [ + ( + // normal case + [u32:0xFD2FB528, u32:0xCAFE_09_C2, u32:0x90ABCDEF, u32:0x12345678, u32:0x0], + FrameHeaderDecoderResp { + header: FrameHeader { + window_size: u64:0x900, + frame_content_size: u64:0x1234567890ABCDEF, + dictionary_id: u32:0xCAFE, + content_checksum_flag: u1:0, + }, + status: FrameHeaderDecoderStatus::OKAY, + length: u5:16 + }, + ), ( + // SingleSegmentFlag is set + [u32:0xFD2FB528, u32:0xAA20, u32:0x0, u32:0x0, u32:0x0], + FrameHeaderDecoderResp { + header: FrameHeader { + window_size: u64:0xaa, + frame_content_size: u64:0xaa, + dictionary_id: u32:0x0, + content_checksum_flag: u1:0, + }, + status: FrameHeaderDecoderStatus::OKAY, + length: u5:6 + }, + ), ( + // SingleSegmentFlag is set and FrameContentSize is bigger than accepted window_size + [u32:0xFD2FB528, u32:0xEF_CAFE_E2, u32:0x7890ABCD, u32:0x123456, u32:0x0], + FrameHeaderDecoderResp { + header: FrameHeader { + window_size: u64:0x1234567890ABCDEF, + frame_content_size: u64:0x1234567890ABCDEF, + dictionary_id: u32:0xCAFE, + content_checksum_flag: u1:0, + }, + status: FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE, + length: u5:15 + }, + ), ( + // Frame header descriptor is corrupted (we don't check 'header' and 'length' fields) + [u32:0xFD2FB528, u32:0x1234_09_CA, u32:0x90ABCDEF, u32:0x12345678, u32:0x0], + FrameHeaderDecoderResp { + header: zero!(), + status: FrameHeaderDecoderStatus::CORRUPTED, + length: u5:0 + }, + ), ( + // Window size required by frame is too big for given decoder configuration + [u32:0xFD2FB528, u32:0xD310, u32:0x0, u32:0x0, u32:0x0], + FrameHeaderDecoderResp { + header: FrameHeader { + window_size: u64:0x1600000000, + ..zero!() + }, + status: FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE, + length: u5:6 + }, + ), ( + // Window size required by frame is too big for given decoder configuration + [u32:0xFD2FB528, u32:0x5B5B0DB1, u32:0xF45B, u32:0x0, u32:0x0], + FrameHeaderDecoderResp { + header: FrameHeader { + window_size: u64:0xf45b5b5b, + frame_content_size: u64:0xf45b5b5b, + dictionary_id: u32:0xD, + content_checksum_flag: u1:0, + }, + status: FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE, + length: u5:10 + }, + ), ( + // Window size required by frame is too big for given decoder configuration + [u32:0xFD2FB528, u32:0xA53A79E4, u32:0x16B33F3D, u32:0x9DB6813A, u32:0xC065], + FrameHeaderDecoderResp { + header: FrameHeader { + window_size: u64:0x3a16b33f3da53a79, + frame_content_size: u64:0x3a16b33f3da53a79, + dictionary_id: u32:0, + content_checksum_flag: u1:1, + }, + status: FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE, + length: u5:13 + } + ) + ]; + + const ADDR = u16:0x1234; + let tok = for ((_, (test_vec, expected)), tok): ((u32, (u32[TEST_XFERS_FOR_HEADER], FrameHeaderDecoderResp)), token) in enumerate(tests) { + let tok = send(tok, decode_req_s, FrameHeaderDecoderReq { addr: ADDR }); + let (tok, recv_data) = recv(tok, reader_req_r); + + assert_eq(recv_data, ReaderReq { addr: ADDR, length: MAX_MAGIC_PLUS_HEADER_LEN as u16 }); + + let tok = for ((j, word), tok): ((u32, u32), token) in enumerate(test_vec) { + let last = j + u32:1 == array_size(test_vec); + send(tok, reader_resp_s, ReaderResp { + status: mem_reader::MemReaderStatus::OKAY, + data: word, + length: if !last { (TEST_DATA_W / u32:8) as u16 } else { (MAX_MAGIC_PLUS_HEADER_LEN % TEST_XFERS_FOR_HEADER) as u16 }, + last: last, + }) + }(tok); + + let (tok, recv_data) = recv(tok, decode_resp_r); + if (recv_data.status == FrameHeaderDecoderStatus::OKAY || recv_data.status == FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE) { + assert_eq(recv_data, expected); + } else { + // if the header is corrupted we don't offer any guarantees + // about its contents so we just check that the status matches + assert_eq(recv_data.status, expected.status); + }; + + tok + }(tok); + + send(tok, terminator, true); + } +} + + +// Largest allowed WindowLog accepted by libzstd decompression function +// https://github.com/facebook/zstd/blob/v1.4.7/lib/decompress/zstd_decompress.c#L296 +// Use only in C++ tests when comparing DSLX ZSTD Decoder with libzstd +pub const TEST_WINDOW_LOG_MAX_LIBZSTD = u32:30; + +proc FrameHeaderDecoderInst { + type Req = FrameHeaderDecoderReq; + type Resp = FrameHeaderDecoderResp; + type ReaderReq = mem_reader::MemReaderReq; + type ReaderResp = mem_reader::MemReaderResp; + + reader_req_s: chan out; + reader_resp_r: chan in; + + decode_req_r: chan in; + decode_resp_s: chan out; + + config( + reader_req_s: chan out, + reader_resp_r: chan in, + decode_req_r: chan in, + decode_resp_s: chan out, + ) { + spawn FrameHeaderDecoder( + reader_req_s, + reader_resp_r, + decode_req_r, + decode_resp_s + ); + (reader_req_s, reader_resp_r, decode_req_r, decode_resp_s) + } + + init {} + + next(state: ()) {} +} diff --git a/xls/modules/zstd/frame_header_test.cc b/xls/modules/zstd/frame_header_test.cc deleted file mode 100644 index 55530c80f5..0000000000 --- a/xls/modules/zstd/frame_header_test.cc +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright 2024 The XLS Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this kFile 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. - -#include - -// NOLINTBEGIN(build/include_order) - Silence include order warnings. -#include "xls/simulation/sim_test_base.h" -#define ZSTD_STATIC_LINKING_ONLY 1 - -#include -#include -#include -#include -#include // NOLINT -#include -#include -#include -// NOLINTEND(build/include_order) - Silence include order warnings. - -#include "gtest/gtest.h" -#include "xls/common/fuzzing/fuzztest.h" -#include "absl/container/flat_hash_map.h" -#include "absl/status/statusor.h" -#include "absl/types/span.h" -#include "xls/common/file/filesystem.h" -#include "xls/common/file/get_runfile_path.h" -#include "xls/common/status/matchers.h" -#include "xls/common/status/ret_check.h" -#include "xls/dslx/create_import_data.h" -#include "xls/dslx/import_data.h" -#include "xls/dslx/ir_convert/convert_options.h" -#include "xls/dslx/ir_convert/ir_converter.h" -#include "xls/dslx/parse_and_typecheck.h" -#include "xls/dslx/type_system/parametric_env.h" -#include "xls/ir/bits.h" -#include "xls/ir/value.h" -#include "xls/modules/zstd/data_generator.h" -#include "external/zstd/lib/zstd.h" -#include "external/zstd/lib/zstd_errors.h" - -namespace xls { -namespace { - -// Must be in sync with FrameHeaderStatus from -// xls/modules/zstd/frame_header.x -enum FrameHeaderStatus : uint8_t { - OK, - CORRUPTED, - NO_ENOUGH_DATA, - UNSUPPORTED_WINDOW_SIZE -}; - -class ZstdFrameHeader { - public: - absl::Span buffer() const { - return absl::MakeConstSpan(buffer_); - } - - ZSTD_frameHeader header() const { return header_; } - - size_t result() const { return result_; } - - ZstdFrameHeader(absl::Span buffer, ZSTD_frameHeader h, - size_t r) - : header_(h), result_(r) { - std::vector v(buffer.begin(), buffer.end()); - buffer_ = v; - } - // Parse a frame header from an arbitrary buffer with the ZSTD library. - static absl::StatusOr Parse( - absl::Span buffer) { - XLS_RET_CHECK(!buffer.empty()); - XLS_RET_CHECK(buffer.data() != nullptr); - ZSTD_frameHeader zstd_fh; - size_t result = ZSTD_getFrameHeader_advanced( - &zstd_fh, buffer.data(), buffer.size(), ZSTD_f_zstd1_magicless); - return ZstdFrameHeader(buffer, zstd_fh, result); - } - - private: - std::vector buffer_; - ZSTD_frameHeader header_; - size_t result_; -}; - -class FrameHeaderTest : public xls::SimTestBase { - public: - // Prepare simulation environment - void SetUp() override { - XLS_ASSERT_OK_AND_ASSIGN(std::filesystem::path path, - xls::GetXlsRunfilePath(this->kFile)); - XLS_ASSERT_OK_AND_ASSIGN(std::string module_text, - xls::GetFileContents(path)); - - auto import_data = xls::dslx::CreateImportDataForTest(); - XLS_ASSERT_OK_AND_ASSIGN( - xls::dslx::TypecheckedModule checked_module, - xls::dslx::ParseAndTypecheck(module_text, this->kFileName, - this->kModuleName, &import_data)); - - auto options = xls::dslx::ConvertOptions{}; - /* FIXME: The following code should work with a parametrized version of - * the `parse_frame_header` function. However, it seems that - * the symbolic_bindings are not correctly propagated inside - * ConvertOneFunction. To leverage the problem, a simple specialization - * of the function is used (`parse_frame_header_128`). - * Once the problem is solved, we can restore the code below. - */ - // auto symbolic_bindings = xls::dslx::ParametricEnv( - // absl::flat_hash_map{ - // {"CAPACITY", xls::dslx::InterpValue::MakeUBits(/*bit_count=*/32, - // /*value=*/32)}}); - dslx::ParametricEnv* symbolic_bindings = nullptr; - XLS_ASSERT_OK_AND_ASSIGN( - this->converted, xls::dslx::ConvertOneFunction( - checked_module.module, kFunctionName, &import_data, - symbolic_bindings, options)); - } - - // Prepare inputs for DSLX simulation based on the given zstd header, - // form the expected output from the simulation, - // run the simulation of frame header parser and compare the results against - // expected values. - void RunAndExpectFrameHeader(const ZstdFrameHeader& zstd_frame_header) { - // Extend buffer contents to 128 bits if necessary. - const absl::Span buffer = zstd_frame_header.buffer(); - std::vector buffer_extended(kDslxBufferSizeBytes, 0); - absl::Span input_buffer; - if (buffer.size() < kDslxBufferSizeBytes) { - std::copy(buffer.begin(), buffer.end(), buffer_extended.begin()); - input_buffer = absl::MakeSpan(buffer_extended); - } else { - input_buffer = buffer; - } - - // Decide on the expected status - ZSTD_frameHeader zstd_fh = zstd_frame_header.header(); - size_t result = zstd_frame_header.result(); - FrameHeaderStatus expected_status = FrameHeaderStatus::OK; - if (result != 0) { - if (ZSTD_isError(result)) { - switch (ZSTD_getErrorCode(result)) { - case ZSTD_error_frameParameter_windowTooLarge: - expected_status = FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE; - break; - case ZSTD_error_frameParameter_unsupported: - // Occurs when reserved_bit == 1, should result in CORRUPTED state - default: - // Provided data is corrupted. Unable to correctly parse ZSTD frame. - expected_status = FrameHeaderStatus::CORRUPTED; - break; - } - } else { - // Provided data is to small to correctly parse ZSTD frame, should - // have `result` bytes, got `buffer.size()` bytes. - expected_status = FrameHeaderStatus::NO_ENOUGH_DATA; - } - // Make sure that the FCS does not exceed max window buffer size - // Frame Header decoding failed - Special case - difference between the - // reference library and the decoder - } else if (!window_size_valid(zstd_fh.windowSize)) { - expected_status = FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE; - } - - auto input = CreateDslxSimulationInput(buffer.size(), input_buffer); - absl::flat_hash_map hashed_input = {{"buffer", input}}; - - auto expected_frame_header_result = CreateExpectedFrameHeaderResult( - &zstd_fh, input, buffer, expected_status); - - RunAndExpectEq(hashed_input, expected_frame_header_result, this->converted, - true, true); - } - - const std::string_view kFile = "xls/modules/zstd/frame_header_test.x"; - const std::string_view kModuleName = "frame_header_test"; - const std::string_view kFileName = "frame_header_test.x"; - const std::string_view kFunctionName = "parse_frame_header_128"; - std::string converted; - - private: - static const size_t kDslxBufferSize = 128; - static const size_t kDslxBufferSizeBytes = - (kDslxBufferSize + CHAR_BIT - 1) / CHAR_BIT; - - // Largest allowed WindowLog accepted by libzstd decompression function - // https://github.com/facebook/zstd/blob/v1.5.6/lib/decompress/zstd_decompress.c#L515 - // Use only in C++ tests when comparing DSLX ZSTD Decoder with libzstd - // Must be in sync with kTestWindowLogMaxLibZstd in frame_header_test.x - const uint64_t kTestWindowLogMaxLibZstd = 30; - - // Maximal mantissa value for calculating maximal accepted window_size - // as per https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor - const uint64_t kMaxMantissa = 0b111; - - // Calculate maximal accepted window_size for given WINDOW_LOG_MAX and return - // whether given window_size should be accepted or discarded. Based on - // window_size calculation from: RFC 8878 - // https://datatracker.ietf.org/doc/html/rfc8878#name-window-descriptor - bool window_size_valid(uint64_t window_size) { - auto max_window_size = - (1 << kTestWindowLogMaxLibZstd) + - (((1 << kTestWindowLogMaxLibZstd) >> 3) * kMaxMantissa); - - return window_size <= max_window_size; - } - - // Form DSLX Value representing ZSTD Frame header based on data parsed with - // ZSTD library. Represents DSLX struct `FrameHeader`. - Value CreateExpectedFrameHeader(ZSTD_frameHeader* fh, - FrameHeaderStatus expected_status) { - if (expected_status == FrameHeaderStatus::CORRUPTED || - expected_status == FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE) { - return Value::Tuple({ - /*window_size=*/Value(UBits(0, 64)), - /*frame_content_size=*/Value(UBits(0, 64)), - /*dictionary_id=*/Value(UBits(0, 32)), - /*content_checksum_flag=*/Value(UBits(0, 1)), - }); - } - return Value::Tuple({ - /*window_size=*/Value(UBits(fh->windowSize, 64)), - /*frame_content_size=*/Value(UBits(fh->frameContentSize, 64)), - /*dictionary_id=*/Value(UBits(fh->dictID, 32)), - /*content_checksum_flag=*/Value(UBits(fh->checksumFlag, 1)), - }); - } - - // Create DSLX Value representing Buffer contents after parsing frame header - // in simulation. Represents DSLX struct `Buffer`. - Value CreateExpectedBuffer(Value dslx_simulation_input, - absl::Span input_buffer, - size_t consumed_bytes_count, - FrameHeaderStatus expected_status) { - // Return original buffer contents - if (expected_status == FrameHeaderStatus::NO_ENOUGH_DATA) { - return dslx_simulation_input; - } - // Critical failure - return empty buffer - if (expected_status == FrameHeaderStatus::CORRUPTED || - expected_status == FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE) { - return Value::Tuple({/*contents:*/ Value(UBits(0, kDslxBufferSize)), - /*length:*/ Value(UBits(0, 32))}); - } - - // Frame Header parsing succeeded. Expect output buffer contents with - // removed first `consumed_bytes_count` bytes and extended to - // kDslxBufferSize if necessary - size_t bytes_to_extend = - kDslxBufferSizeBytes - (input_buffer.size() - consumed_bytes_count); - std::vector output_buffer(input_buffer.begin() + consumed_bytes_count, - input_buffer.end()); - for (int i = 0; i < bytes_to_extend; i++) { - output_buffer.push_back(0); - } - - auto expected_buffer_contents = - Value(Bits::FromBytes(output_buffer, kDslxBufferSize)); - size_t output_buffer_size_bits = - (input_buffer.size() - consumed_bytes_count) * CHAR_BIT; - size_t expected_buffer_size = output_buffer_size_bits > kDslxBufferSize - ? kDslxBufferSize - : output_buffer_size_bits; - - return Value::Tuple({/*contents:*/ expected_buffer_contents, - /*length:*/ Value(UBits(expected_buffer_size, 32))}); - } - - // Prepare DSLX Value representing Full Result of frame header parsing - // simulation. It consists of expected status, parsing result and buffer - // contents after parsing. Represents DSLX struct `FrameHeaderResult`. - Value CreateExpectedFrameHeaderResult(ZSTD_frameHeader* fh, - Value dslx_simulation_input, - absl::Span input_buffer, - FrameHeaderStatus expected_status) { - auto expected_buffer = - CreateExpectedBuffer(std::move(dslx_simulation_input), input_buffer, - fh->headerSize, expected_status); - auto expected_frame_header = CreateExpectedFrameHeader(fh, expected_status); - return Value::Tuple({/*status:*/ Value(UBits(expected_status, 2)), - /*header:*/ expected_frame_header, - /*buffer:*/ expected_buffer}); - } - - // Return DSLX Value used as input argument for running frame header parsing - // simulation. Represents DSLX struct `Buffer`. - Value CreateDslxSimulationInput(size_t buffer_size, - absl::Span input_buffer) { - size_t size = buffer_size; - - // ignore buffer contents that won't fit into specialized buffer - if (buffer_size > kDslxBufferSizeBytes) { - size = kDslxBufferSizeBytes; - } - - return Value::Tuple( - {/*contents:*/ Value(Bits::FromBytes(input_buffer, kDslxBufferSize)), - /*length:*/ Value(UBits(size * CHAR_BIT, 32))}); - } -}; - -/* TESTS */ - -TEST_F(FrameHeaderTest, Success) { - XLS_ASSERT_OK_AND_ASSIGN( - auto header, - ZstdFrameHeader::Parse({0xC2, 0x09, 0xFE, 0xCA, 0xEF, 0xCD, 0xAB, 0x90, - 0x78, 0x56, 0x34, 0x12})); - this->RunAndExpectFrameHeader(header); -} - -TEST_F(FrameHeaderTest, FailCorruptedReservedBit) { - XLS_ASSERT_OK_AND_ASSIGN( - auto header, ZstdFrameHeader::Parse({0xEA, 0xFE, 0xCA, 0xEF, 0xCD, 0xAB, - 0x90, 0x78, 0x56, 0x34, 0x12})); - this->RunAndExpectFrameHeader(header); -} - -TEST_F(FrameHeaderTest, FailUnsupportedWindowSizeTooBig) { - XLS_ASSERT_OK_AND_ASSIGN(auto header, ZstdFrameHeader::Parse({0x10, 0xD3})); - this->RunAndExpectFrameHeader(header); -} - -TEST_F(FrameHeaderTest, FailNoEnoughData) { - XLS_ASSERT_OK_AND_ASSIGN(auto header, ZstdFrameHeader::Parse({0xD3, 0xED})); - this->RunAndExpectFrameHeader(header); -} - -// NO_ENOUGH_DATA has priority over CORRUPTED from reserved bit -TEST_F(FrameHeaderTest, FailNoEnoughDataReservedBit) { - XLS_ASSERT_OK_AND_ASSIGN(auto header, ZstdFrameHeader::Parse({0xED, 0xD3})); - this->RunAndExpectFrameHeader(header); -} - -TEST_F(FrameHeaderTest, FailUnsupportedFrameContentSizeThroughSingleSegment) { - XLS_ASSERT_OK_AND_ASSIGN( - auto header, ZstdFrameHeader::Parse({0261, 015, 91, 91, 91, 0364})); - this->RunAndExpectFrameHeader(header); -} - -TEST_F(FrameHeaderTest, - FailUnsupportedVeryLargeFrameContentSizeThroughSingleSegment) { - XLS_ASSERT_OK_AND_ASSIGN( - auto header, - ZstdFrameHeader::Parse({0344, 'y', ':', 0245, '=', '?', 0263, 0026, ':', - 0201, 0266, 0235, 'e', 0300})); - this->RunAndExpectFrameHeader(header); -} - -TEST_F(FrameHeaderTest, FailUnsupportedWindowSize) { - XLS_ASSERT_OK_AND_ASSIGN( - auto header, - ZstdFrameHeader::Parse({'S', 0301, 'i', 0320, 0, 0256, 'd', 'D', 0226, - 'F', 'Z', 'Z', 0332, 0370, 'A'})); - this->RunAndExpectFrameHeader(header); -} - -class FrameHeaderSeededTest : public FrameHeaderTest, - public ::testing::WithParamInterface { - public: - static const uint32_t random_headers_count = 50; -}; - -// Test `random_headers_count` instances of randomly generated valid -// frame headers, generated with `decodecorpus` tool. -TEST_P(FrameHeaderSeededTest, ParseMultipleFrameHeaders) { - auto seed = GetParam(); - XLS_ASSERT_OK_AND_ASSIGN(auto buffer, zstd::GenerateFrameHeader(seed, false)); - XLS_ASSERT_OK_AND_ASSIGN(auto frame_header, ZstdFrameHeader::Parse(buffer)); - this->RunAndExpectFrameHeader(frame_header); -} - -INSTANTIATE_TEST_SUITE_P( - FrameHeaderSeededTest, FrameHeaderSeededTest, - ::testing::Range(0, FrameHeaderSeededTest::random_headers_count)); - -class FrameHeaderFuzzTest - : public fuzztest::PerFuzzTestFixtureAdapter { - public: - void ParseMultipleRandomFrameHeaders(const std::vector& buffer) { - auto frame_header = ZstdFrameHeader::Parse(buffer); - XLS_ASSERT_OK(frame_header); - this->RunAndExpectFrameHeader(frame_header.value()); - } -}; - -// Perform UNDETERMINISTIC FuzzTests with input vectors of variable length and -// contents. Frame Headers generated by FuzzTests can be invalid. -// This test checks if negative cases are handled correctly. -FUZZ_TEST_F(FrameHeaderFuzzTest, ParseMultipleRandomFrameHeaders) - .WithDomains(fuzztest::Arbitrary>() - .WithMinSize(1) - .WithMaxSize(16)); - -} // namespace -} // namespace xls diff --git a/xls/modules/zstd/frame_header_test.x b/xls/modules/zstd/frame_header_test.x deleted file mode 100644 index 9216dfab8d..0000000000 --- a/xls/modules/zstd/frame_header_test.x +++ /dev/null @@ -1,30 +0,0 @@ -// 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 std; -import xls.modules.zstd.buffer as buff; -import xls.modules.zstd.frame_header as frame_header; - -type Buffer = buff::Buffer; -type FrameHeaderResult = frame_header::FrameHeaderResult; -type WindowSize = frame_header::WindowSize; - -// Largest allowed WindowLog accepted by libzstd decompression function -// https://github.com/facebook/zstd/blob/v1.4.7/lib/decompress/zstd_decompress.c#L296 -// Use only in C++ tests when comparing DSLX ZSTD Decoder with libzstd -pub const TEST_WINDOW_LOG_MAX_LIBZSTD = WindowSize:30; - -pub fn parse_frame_header_128(buffer: Buffer<128>) -> FrameHeaderResult<128> { - frame_header::parse_frame_header(buffer) -} From cb6430f0fbc9c24279157e2e196415ddbbfcfab7 Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Tue, 8 Oct 2024 18:04:07 +0200 Subject: [PATCH 19/46] modules/zstd: Add BlockHeaderDecoder Signed-off-by: Maciej Torhan --- xls/modules/zstd/BUILD | 78 ++++++++ xls/modules/zstd/block_header_dec.x | 293 ++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 xls/modules/zstd/block_header_dec.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index faf0504da0..083c7b731e 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -263,6 +263,84 @@ xls_dslx_test( tags = ["manual"], ) +xls_dslx_library( + name = "block_header_dec_dslx", + srcs = [ + "block_header_dec.x", + ], + deps = [ + ":block_header_dslx", + ":common_dslx", + "//xls/modules/zstd/memory:mem_reader_dslx", + ], +) + +xls_dslx_test( + name = "block_header_dec_dslx_test", + library = ":block_header_dec_dslx", + tags = ["manual"], +) + +block_header_dec_codegen_args = common_codegen_args | { + "module_name": "BlockHeaderDec", + "pipeline_stages": "1", +} + +xls_dslx_verilog( + name = "block_header_dec_verilog", + codegen_args = block_header_dec_codegen_args, + dslx_top = "BlockHeaderDecoderInst", + library = ":block_header_dec_dslx", + tags = ["manual"], + verilog_file = "block_header_dec.v", +) + +xls_benchmark_ir( + name = "block_header_dec_opt_ir_benchmark", + src = ":block_header_dec_verilog.opt.ir", + benchmark_ir_args = block_header_dec_codegen_args | { + "pipeline_stages": "10", + "top": "__block_header_dec__BlockHeaderDecoderInst__BlockHeaderDecoder_0__16_64_next", + }, + tags = ["manual"], +) + +verilog_library( + name = "block_header_dec_verilog_lib", + srcs = [ + ":block_header_dec.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "block_header_dec_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "BlockHeaderDec", + deps = [ + ":block_header_dec_verilog_lib", + ], +) + +benchmark_synth( + name = "block_header_dec_benchmark_synth", + synth_target = ":block_header_dec_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "block_header_dec_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":block_header_dec_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + xls_dslx_library( name = "raw_block_dec_dslx", srcs = [ diff --git a/xls/modules/zstd/block_header_dec.x b/xls/modules/zstd/block_header_dec.x new file mode 100644 index 0000000000..45c69e921c --- /dev/null +++ b/xls/modules/zstd/block_header_dec.x @@ -0,0 +1,293 @@ +// 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 xls.modules.zstd.block_header as block_header; +import xls.modules.zstd.common as common; +import xls.modules.zstd.memory.mem_reader as mem_reader; + +type BlockSize = common::BlockSize; +type BlockType = common::BlockType; +type BlockHeader = block_header::BlockHeader; + +pub struct BlockHeaderDecoderReq { + addr: uN[ADDR_W], +} + +pub enum BlockHeaderDecoderStatus: u2 { + OKAY = 0, + CORRUPTED = 1, + MEMORY_ACCESS_ERROR = 2, +} + +pub struct BlockHeaderDecoderResp { + status: BlockHeaderDecoderStatus, + header: BlockHeader, + rle_symbol: u8, +} + +pub proc BlockHeaderDecoder { + type Req = BlockHeaderDecoderReq; + type Resp = BlockHeaderDecoderResp; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + type MemReaderStatus = mem_reader::MemReaderStatus; + + type Status = BlockHeaderDecoderStatus; + type Length = uN[ADDR_W]; + type Addr = uN[ADDR_W]; + + req_r: chan in; + resp_s: chan out; + mem_req_s: chan out; + mem_resp_r: chan in; + + config ( + req_r: chan in, + resp_s: chan out, + mem_req_s: chan out, + mem_resp_r: chan in, + ) { + (req_r, resp_s, mem_req_s, mem_resp_r) + } + + init { } + + next (state: ()) { + let tok0 = join(); + + // receive request + let (tok1_0, req, req_valid) = recv_non_blocking(tok0, req_r, zero!()); + + // send memory read request + let mem_req = MemReaderReq {addr: req.addr, length: Length:4 }; + let tok2_0 = send_if(tok1_0, mem_req_s, req_valid, mem_req); + + // receive memory read response + let (tok1_1, mem_resp, mem_resp_valid) = recv_non_blocking(tok0, mem_resp_r, zero!()); + + let header = block_header::extract_block_header(mem_resp.data as u24); + let rle_symbol = mem_resp.data[u32:24 +: u8]; + let status = match ( mem_resp.status == MemReaderStatus::OKAY, header.btype != BlockType::RESERVED) { + (true, true) => Status::OKAY, + (true, false) => Status::CORRUPTED, + ( _, _) => Status::MEMORY_ACCESS_ERROR, + }; + + let resp = Resp { status, header, rle_symbol }; + let tok2_1 = send_if(tok1_1, resp_s, mem_resp_valid, resp); + } +} + +const INST_DATA_W = u32:64; +const INST_ADDR_W = u32:16; + +proc BlockHeaderDecoderInst { + type Req = BlockHeaderDecoderReq; + type Resp = BlockHeaderDecoderResp; + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + config ( + req_r: chan in, + resp_s: chan out, + mem_req_s: chan out, + mem_resp_r: chan in, + ) { + spawn BlockHeaderDecoder( req_r, resp_s, mem_req_s, mem_resp_r); + } + + init { } + next (state: ()) { } +} + +const TEST_DATA_W = u32:32; +const TEST_ADDR_W = u32:32; + +fn header_to_raw(header: BlockHeader, rle_symbol: u8) -> u32 { + rle_symbol ++ header.size ++ (header.btype as u2) ++ header.last +} + + +#[test_proc] +proc BlockHeaderDecoderTest { + type Req = BlockHeaderDecoderReq; + type Resp = BlockHeaderDecoderResp; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + type MemReaderStatus = mem_reader::MemReaderStatus; + + type Data = uN[TEST_DATA_W]; + type Addr = uN[TEST_ADDR_W]; + type Length = uN[TEST_ADDR_W]; + + terminator: chan out; + + req_s: chan out; + resp_r: chan in; + + mem_req_r: chan in; + mem_resp_s: chan out; + + config (terminator: chan out) { + + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + + let (mem_req_s, mem_req_r) = chan("mem_req"); + let (mem_resp_s, mem_resp_r) = chan("mem_resp"); + + spawn BlockHeaderDecoder ( + req_r, resp_s, mem_req_s, mem_resp_r + ); + + (terminator, req_s, resp_r, mem_req_r, mem_resp_s) + } + + init { } + + next (state: ()) { + const LENGTH = Length:4; + + let tok = join(); + + // Test Raw + let addr = Addr:0x1234; + let header = BlockHeader { size: BlockSize:0x100, btype: BlockType::RAW, last: true}; + let rle_symbol = u8:0; + + let req = Req { addr }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr, length: LENGTH }); + + let mem_resp = MemReaderResp { + status: MemReaderStatus::OKAY, + data: checked_cast(header_to_raw(header, rle_symbol)), + length: LENGTH, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { + status: BlockHeaderDecoderStatus::OKAY, + header: header, + rle_symbol: rle_symbol + }); + + // Test RLE + let addr = Addr:0x2000; + let header = BlockHeader { size: BlockSize:0x40, btype: BlockType::RLE, last: false}; + let rle_symbol = u8:123; + + let req = Req { addr }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr, length: LENGTH }); + + let mem_resp = MemReaderResp { + status: MemReaderStatus::OKAY, + data: checked_cast(header_to_raw(header, rle_symbol)), + length: LENGTH, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { + status: BlockHeaderDecoderStatus::OKAY, + header: header, + rle_symbol: rle_symbol + }); + + // Test COMPRESSED + let addr = Addr:0x2000; + let header = BlockHeader { size: BlockSize:0x40, btype: BlockType::COMPRESSED, last: true}; + let rle_symbol = u8:0; + + let req = Req { addr }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr, length: LENGTH }); + + let mem_resp = MemReaderResp { + status: MemReaderStatus::OKAY, + data: checked_cast(header_to_raw(header, rle_symbol)), + length: LENGTH, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { + status: BlockHeaderDecoderStatus::OKAY, + header: header, + rle_symbol: rle_symbol + }); + + // Test RESERVED + let addr = Addr:0x2000; + let header = BlockHeader { size: BlockSize:0x40, btype: BlockType::RESERVED, last: true}; + let rle_symbol = u8:0; + + let req = Req { addr }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr, length: LENGTH }); + + let mem_resp = MemReaderResp { + status: MemReaderStatus::OKAY, + data: checked_cast(header_to_raw(header, rle_symbol)), + length: LENGTH, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { + status: BlockHeaderDecoderStatus::CORRUPTED, + header: header, + rle_symbol: rle_symbol + }); + + // Test memory error + let addr = Addr:0x2000; + let header = BlockHeader { size: BlockSize:0x40, btype: BlockType::RESERVED, last: true}; + let rle_symbol = u8:0; + + let req = Req { addr }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr, length: LENGTH }); + + let mem_resp = MemReaderResp { + status: MemReaderStatus::ERROR, + data: checked_cast(header_to_raw(header, rle_symbol)), + length: LENGTH, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { + status: BlockHeaderDecoderStatus::MEMORY_ACCESS_ERROR, + header: header, + rle_symbol: rle_symbol + }); + + send(tok, terminator, true); + } +} From af9750730988a2a06df3476db700827cf1c9f731 Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Tue, 8 Oct 2024 18:06:13 +0200 Subject: [PATCH 20/46] modules/zstd: Add RawBlockDecoder Co-authored-by: Pawel Czarnecki Signed-off-by: Maciej Torhan Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 25 ++- xls/modules/zstd/raw_block_dec.x | 312 +++++++++++++++++++++++++------ 2 files changed, 267 insertions(+), 70 deletions(-) diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 083c7b731e..638ad3696d 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -347,8 +347,8 @@ xls_dslx_library( "raw_block_dec.x", ], deps = [ - ":buffer_dslx", ":common_dslx", + "//xls/modules/zstd/memory:mem_reader_dslx", ], ) @@ -359,16 +359,15 @@ xls_dslx_test( tags = ["manual"], ) +raw_block_dec_codegen_args = common_codegen_args | { + "module_name": "RawBlockDecoder", + "pipeline_stages": "1", +} + xls_dslx_verilog( name = "raw_block_dec_verilog", - codegen_args = { - "module_name": "RawBlockDecoder", - "delay_model": "asap7", - "pipeline_stages": "2", - "reset": "rst", - "use_system_verilog": "false", - }, - dslx_top = "RawBlockDecoder", + codegen_args = raw_block_dec_codegen_args, + dslx_top = "RawBlockDecoderInst", library = ":raw_block_dec_dslx", tags = ["manual"], verilog_file = "raw_block_dec.v", @@ -377,9 +376,9 @@ xls_dslx_verilog( xls_benchmark_ir( name = "raw_block_dec_opt_ir_benchmark", src = ":raw_block_dec_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "2", - "delay_model": "asap7", + benchmark_ir_args = raw_block_dec_codegen_args | { + "pipeline_stages": "10", + "top": "__raw_block_dec__RawBlockDecoderInst__RawBlockDecoder_0__32_32_next", }, tags = ["manual"], ) @@ -410,7 +409,7 @@ benchmark_synth( place_and_route( name = "raw_block_dec_place_and_route", - clock_period = "750", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.5", placement_density = "0.30", diff --git a/xls/modules/zstd/raw_block_dec.x b/xls/modules/zstd/raw_block_dec.x index a3656011b0..669b66d5b1 100644 --- a/xls/modules/zstd/raw_block_dec.x +++ b/xls/modules/zstd/raw_block_dec.x @@ -17,6 +17,7 @@ // https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.2.2 import xls.modules.zstd.common as common; +import xls.modules.zstd.memory.mem_reader as mem_reader; type BlockDataPacket = common::BlockDataPacket; type BlockPacketLength = common::BlockPacketLength; @@ -26,92 +27,289 @@ type CopyOrMatchContent = common::CopyOrMatchContent; type CopyOrMatchLength = common::CopyOrMatchLength; type SequenceExecutorMessageType = common::SequenceExecutorMessageType; -struct RawBlockDecoderState { - prev_id: u32, // ID of the previous block - prev_last: bool, // if the previous packet was the last one that makes up the whole block - prev_valid: bool, // if prev_id and prev_last contain valid data +pub struct RawBlockDecoderReq { + id: u32, + addr: uN[ADDR_W], + length: uN[ADDR_W], + last_block: bool, } -const ZERO_RAW_BLOCK_DECODER_STATE = zero!(); +pub enum RawBlockDecoderStatus: u1 { + OKAY = 0, + ERROR = 1, +} + +pub struct RawBlockDecoderResp { + status: RawBlockDecoderStatus, +} + +struct RawBlockDecoderState { + id: u32, // ID of the block + last_block: bool, // if the block is the last one +} // RawBlockDecoder is responsible for decoding Raw Blocks, // it should be a part of the ZSTD Decoder pipeline. -pub proc RawBlockDecoder { - input_r: chan in; - output_s: chan out; +pub proc RawBlockDecoder { + type Req = RawBlockDecoderReq; + type Resp = RawBlockDecoderResp; + type Output = ExtendedBlockDataPacket; + type Status = RawBlockDecoderStatus; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + type MemReaderStatus = mem_reader::MemReaderStatus; + + type State = RawBlockDecoderState; - init { (ZERO_RAW_BLOCK_DECODER_STATE) } + // decoder input + req_r: chan in; + resp_s: chan out; + + // decoder output + output_s: chan out; + + // memory interface + mem_req_s: chan out; + mem_resp_r: chan in; + + init { zero!() } config( - input_r: chan in, - output_s: chan out - ) {(input_r, output_s)} + req_r: chan in, + resp_s: chan out, + output_s: chan out, - next(state: RawBlockDecoderState) { - let tok = join(); - let (tok, data) = recv(tok, input_r); - if state.prev_valid && (data.id != state.prev_id) && (state.prev_last == false) { - trace_fmt!("ID changed but previous packet have no last!"); - fail!("no_last", ()); - } else {}; - - let output_data = ExtendedBlockDataPacket { - // Decoded RAW block is always a literal + mem_req_s: chan out, + mem_resp_r: chan in, + ) { + ( + req_r, resp_s, output_s, + mem_req_s, mem_resp_r, + ) + } + + next(state: State) { + let tok0 = join(); + + // receive request + let (tok1_0, req, req_valid) = recv_non_blocking(tok0, req_r, zero!>()); + + // update ID and last in state + let state = if req_valid { + State { id: req.id, last_block: req.last_block} + } else { state }; + + // send memory read request + let req = MemReaderReq { addr: req.addr, length: req.length }; + let tok2_0 = send_if(tok1_0, mem_req_s, req_valid, req); + + // receive memory read response + let (tok1_1, mem_resp, mem_resp_valid) = recv_non_blocking(tok0, mem_resp_r, zero!()); + let mem_resp_error = (mem_resp.status != MemReaderStatus::OKAY); + + // prepare output data, decoded RAW block is always a literal + let output_data = Output { msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { - last: data.last, - last_block: data.last_block, - id: data.id, - data: data.data as BlockData, - length: data.length as BlockPacketLength, + last: mem_resp.last, + last_block: state.last_block, + id: state.id, + data: checked_cast(mem_resp.data), + length: checked_cast(mem_resp.length ++ u3:0), }, }; - let tok = send(tok, output_s, output_data); + // send output data + let mem_resp_correct = mem_resp_valid && !mem_resp_error; + let tok2_1 = send_if(tok1_1, output_s, mem_resp_correct, output_data); + + // send response after block end + let resp = if mem_resp_correct { + Resp { status: Status::OKAY } + } else { + Resp { status: Status::ERROR } + }; + + let do_send_resp = mem_resp_valid && mem_resp.last; + let tok2_2 = send_if(tok1_1, resp_s, do_send_resp, resp); - RawBlockDecoderState { - prev_valid: true, - prev_id: output_data.packet.id, - prev_last: output_data.packet.last - } + state } } +const INST_DATA_W = u32:32; +const INST_ADDR_W = u32:32; + +pub proc RawBlockDecoderInst { + type Req = RawBlockDecoderReq; + type Resp = RawBlockDecoderResp; + type Output = ExtendedBlockDataPacket; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + config ( + req_r: chan in, + resp_s: chan out, + output_s: chan out, + mem_req_s: chan out, + mem_resp_r: chan in, + ) { + spawn RawBlockDecoder( + req_r, resp_s, output_s, mem_req_s, mem_resp_r + ); + } + + init { } + + next (state: ()) { } +} + +const TEST_DATA_W = u32:64; +const TEST_ADDR_W = u32:32; + #[test_proc] proc RawBlockDecoderTest { + type Req = RawBlockDecoderReq; + type Resp = RawBlockDecoderResp; + type Output = ExtendedBlockDataPacket; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + type Data = uN[TEST_DATA_W]; + type Addr = uN[TEST_ADDR_W]; + type Length = uN[TEST_ADDR_W]; + terminator: chan out; - dec_input_s: chan out; - dec_output_r: chan in; + + req_s: chan out; + resp_r: chan in; + output_r: chan in; + + mem_req_r: chan in; + mem_resp_s: chan out; config(terminator: chan out) { - let (dec_input_s, dec_input_r) = chan("dec_input"); - let (dec_output_s, dec_output_r) = chan("dec_output"); - spawn RawBlockDecoder(dec_input_r, dec_output_s); - (terminator, dec_input_s, dec_output_r) + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + let (output_s, output_r) = chan("output"); + + let (mem_req_s, mem_req_r) = chan("mem_req"); + let (mem_resp_s, mem_resp_r) = chan("mem_resp"); + + spawn RawBlockDecoder( + req_r, resp_s, output_s, mem_req_s, mem_resp_r + ); + + (terminator, req_s, resp_r, output_r, mem_req_r, mem_resp_s) } init { } next(state: ()) { + let tok = join(); - let data_to_send: BlockDataPacket[5] = [ - BlockDataPacket { id: u32:1, last: u1:false, last_block: u1:false, data: BlockData:1, length: BlockPacketLength:32 }, - BlockDataPacket { id: u32:1, last: u1:false, last_block: u1:false, data: BlockData:2, length: BlockPacketLength:32 }, - BlockDataPacket { id: u32:1, last: u1:true, last_block: u1:false, data: BlockData:3, length: BlockPacketLength:32 }, - BlockDataPacket { id: u32:2, last: u1:false, last_block: u1:false, data: BlockData:4, length: BlockPacketLength:32 }, - BlockDataPacket { id: u32:2, last: u1:true, last_block: u1:true, data: BlockData:5, length: BlockPacketLength:32 }, - ]; - - let tok = for ((_, data), tok): ((u32, BlockDataPacket), token) in enumerate(data_to_send) { - let tok = send(tok, dec_input_s, data); - let (tok, received_data) = recv(tok, dec_output_r); - let expected_data = ExtendedBlockDataPacket { - msg_type: SequenceExecutorMessageType::LITERAL, - packet: data, - }; - assert_eq(expected_data, received_data); - (tok) - }(tok); + + // Test 0 + let req = Req { id: u32:0, last_block: false, addr: Addr:0, length: Length:8 }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr: Addr:0, length: Length:8 }); + + let mem_resp = MemReaderResp { + status: mem_reader::MemReaderStatus::OKAY, + data: Data:0x1122_3344, + length: Length:8, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: true, + last_block: false, + id: u32:0, + data: Data:0x1122_3344, + length: Length:64, + }, + }); + + // Test 1 + let req = Req { id: u32:1, last_block: true, addr: Addr:0x1001, length: Length:15 }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr: Addr:0x1001, length: Length:15 }); + + let mem_resp = MemReaderResp { + status: mem_reader::MemReaderStatus::OKAY, + data: Data:0x1122_3344_5566_7788, + length: Length:8, + last: false + }; + let tok = send(tok, mem_resp_s, mem_resp); + + let mem_resp = MemReaderResp { + status: mem_reader::MemReaderStatus::OKAY, + data: Data:0xAA_BBCC_DDEE_FF99, + length: Length:7, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: false, + last_block: true, + id: u32:1, + data: Data:0x1122_3344_5566_7788, + length: Length:64, + }, + }); + + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: true, + last_block: true, + id: u32:1, + data: Data:0xAA_BBCC_DDEE_FF99, + length: Length:56, + }, + }); + + // Test 2 + let req = Req {id: u32:2, last_block: false, addr: Addr:0x2000, length: Length:0 }; + let tok = send(tok, req_s, req); + + let (tok, mem_req) = recv(tok, mem_req_r); + assert_eq(mem_req, MemReaderReq { addr: Addr:0x2000, length: Length:0 }); + + let mem_resp = MemReaderResp { + status: mem_reader::MemReaderStatus::OKAY, + data: Data:0x0, + length: Length:0, + last: true, + }; + let tok = send(tok, mem_resp_s, mem_resp); + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: true, + last_block: false, + id: u32:2, + data: Data:0x0, + length: Length:0, + }, + }); send(tok, terminator, true); } From 0bf405b72ffcaa204309034ee8ae10d4f3453697 Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Tue, 8 Oct 2024 17:59:46 +0200 Subject: [PATCH 21/46] modules/zstd: Add RleBlockDecoder Co-authored-by: Pawel Czarnecki Co-authored-by: Robert Winkler Signed-off-by: Maciej Torhan Signed-off-by: Pawel Czarnecki Signed-off-by: Robert Winkler --- xls/modules/zstd/BUILD | 32 +- xls/modules/zstd/rle_block_dec.x | 844 +++++++------------------------ 2 files changed, 190 insertions(+), 686 deletions(-) diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 638ad3696d..11edc01a0d 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -425,10 +425,7 @@ xls_dslx_library( "rle_block_dec.x", ], deps = [ - ":buffer_dslx", ":common_dslx", - "//xls/modules/rle:rle_common_dslx", - "//xls/modules/rle:rle_dec_dslx", ], ) @@ -439,23 +436,16 @@ xls_dslx_test( tags = ["manual"], ) +rle_block_dec_codegen_args = common_codegen_args | { + "module_name": "RleBlockDecoder", + "pipeline_stages": "1", +} + xls_dslx_verilog( name = "rle_block_dec_verilog", - codegen_args = { - "module_name": "RleBlockDecoder", - "delay_model": "asap7", - "pipeline_stages": "3", - "reset": "rst", - "use_system_verilog": "false", - }, - dslx_top = "RleBlockDecoder", + codegen_args = rle_block_dec_codegen_args, + dslx_top = "RleBlockDecoderInst", library = ":rle_block_dec_dslx", - # TODO: 2024-01-15: Workaround for https://github.com/google/xls/issues/869 - # Force proc inlining and set last internal proc as top proc for IR optimization - opt_ir_args = { - "inline_procs": "true", - "top": "__rle_block_dec__RleBlockDecoder__BatchPacker_0_next", - }, tags = ["manual"], verilog_file = "rle_block_dec.v", ) @@ -463,9 +453,9 @@ xls_dslx_verilog( xls_benchmark_ir( name = "rle_block_dec_opt_ir_benchmark", src = ":rle_block_dec_verilog.opt.ir", - benchmark_ir_args = { - "pipeline_stages": "3", - "delay_model": "asap7", + benchmark_ir_args = rle_block_dec_codegen_args | { + "pipeline_stages": "10", + "top": "__rle_block_dec__RleBlockDecoderInst__RleBlockDecoder_0__64_next", }, tags = ["manual"], ) @@ -496,7 +486,7 @@ benchmark_synth( place_and_route( name = "rle_block_dec_place_and_route", - clock_period = "750", + clock_period = CLOCK_PERIOD_PS, core_padding_microns = 2, min_pin_distance = "0.5", placement_density = "0.30", diff --git a/xls/modules/zstd/rle_block_dec.x b/xls/modules/zstd/rle_block_dec.x index 232d9a6381..c5529c978b 100644 --- a/xls/modules/zstd/rle_block_dec.x +++ b/xls/modules/zstd/rle_block_dec.x @@ -12,44 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -// This file contains the implementation of RleBlockDecoder responsible for decoding -// ZSTD RLE Blocks. More Information about Rle Block's format can be found in: -// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1.2.2 -// -// The implementation consist of 3 procs: -// * RleDataPacker -// * RunLengthDecoder -// * BatchPacker -// Connections between those is represented on the diagram below: -// -// RleBlockDecoder -// ┌─────────────────────────────────────────────────────────────┐ -// │ RleDataPacker RunLengthDecoder BatchPacker │ -// │ ┌───────────────┐ ┌──────────────────┐ ┌─────────────┐ │ -// ───┼─►│ ├──►│ ├──►│ ├─┼──► -// │ └───────┬───────┘ └──────────────────┘ └─────────────┘ │ -// │ │ ▲ │ -// │ │ SynchronizationData │ │ -// │ └─────────────────────────────────────────┘ │ -// └─────────────────────────────────────────────────────────────┘ -// -// RleDataPacker is responsible for receiving the incoming packets of block data, converting -// those to format accepted by RunLengthDecoder and passing the data to the actual decoder block. -// It also extracts from the input packets the synchronization data like block_id and last_block -// and then passes those to BatchPacker proc. -// RunLengthDecoder decodes RLE blocks and outputs one symbol for each transaction on output -// channel. -// BatchPacker then gathers those symbols into packets, appends synchronization data received from -// RleDataPacker and passes such packets to the output of the RleBlockDecoder. +import std; import xls.modules.zstd.common; -import xls.modules.rle.rle_dec; -import xls.modules.rle.rle_common; -const SYMBOL_WIDTH = common::SYMBOL_WIDTH; -const BLOCK_SIZE_WIDTH = common::BLOCK_SIZE_WIDTH; -const DATA_WIDTH = common::DATA_WIDTH; -const BATCH_SIZE = DATA_WIDTH / SYMBOL_WIDTH; type BlockDataPacket = common::BlockDataPacket; type BlockPacketLength = common::BlockPacketLength; @@ -61,696 +27,244 @@ type CopyOrMatchContent = common::CopyOrMatchContent; type CopyOrMatchLength = common::CopyOrMatchLength; type SequenceExecutorMessageType = common::SequenceExecutorMessageType; -type RleInput = rle_common::CompressedData; -type RleOutput = rle_common::PlainData; -type Symbol = bits[SYMBOL_WIDTH]; -type SymbolCount = BlockSize; -struct BlockSyncData { - last_block: bool, - count: SymbolCount, - id: u32 +pub enum RleBlockDecoderStatus: u1 { + OKAY = 0, } -proc RleDataPacker { - block_data_r: chan in; - rle_data_s: chan out; - sync_s: chan out; - - config( - block_data_r: chan in, - rle_data_s: chan out, - sync_s: chan out - ) { - (block_data_r, rle_data_s, sync_s) - } - - init { } - - next(state: ()) { - let tok = join(); - let (tok, input) = recv(tok, block_data_r); - let rle_dec_data = RleInput { - symbol: input.data as Symbol, count: input.length as SymbolCount, last: true - }; - // send RLE packet for decoding unless it has symbol count == 0 - let send_always = rle_dec_data.count != SymbolCount:0; - let data_tok = send_if(tok, rle_data_s, send_always, rle_dec_data); - let sync_data = BlockSyncData { last_block: input.last_block, count: rle_dec_data.count, id: input.id }; - // send last block packet even if it has symbol count == 0 - let sync_tok = send(data_tok, sync_s, sync_data); - } +pub struct RleBlockDecoderReq { + id: u32, + symbol: u8, + length: BlockSize, + last_block: bool, } -type RleTestVector = (Symbol, SymbolCount); - -#[test_proc] -proc RleDataPacker_test { - terminator: chan out; - in_s: chan out; - out_r: chan in; - sync_r: chan in; - - config(terminator: chan out) { - let (in_s, in_r) = chan("in"); - let (out_s, out_r) = chan("out"); - let (sync_s, sync_r) = chan("sync"); - - spawn RleDataPacker(in_r, out_s, sync_s); - - (terminator, in_s, out_r, sync_r) - } - - init { } - - next(state: ()) { - let tok = join(); - let EncodedRleBlocks: RleTestVector[6] = [ - (Symbol:0x1, SymbolCount:0x1), - (Symbol:0x2, SymbolCount:0x2), - (Symbol:0x3, SymbolCount:0x4), - (Symbol:0x4, SymbolCount:0x8), - (Symbol:0x5, SymbolCount:0x10), - (Symbol:0x6, SymbolCount:0x1F), - ]; - let tok = for ((counter, block), tok): ((u32, RleTestVector), token) in enumerate(EncodedRleBlocks) { - let last_block = (counter == (array_size(EncodedRleBlocks) - u32:1)); - let data_in = BlockDataPacket { - last: true, - last_block, - id: counter, - data: block.0 as BlockData, - length: block.1 as BlockPacketLength - }; - let tok = send(tok, in_s, data_in); - trace_fmt!("Sent #{} raw encoded block, {:#x}", counter + u32:1, data_in); - - let data_out = RleInput { - last: true, symbol: block.0 as Symbol, count: block.1 as BlockSize - }; - let (tok, dec_output) = recv(tok, out_r); - trace_fmt!("Received #{} packed rle encoded block, {:#x}", counter + u32:1, dec_output); - assert_eq(dec_output, data_out); - - let sync_out = BlockSyncData { - id: counter, - count: block.1, - last_block: counter == (array_size(EncodedRleBlocks) - u32:1), - }; - let (tok, sync_output) = recv(tok, sync_r); - trace_fmt!("Received #{} synchronization data, {:#x}", counter + u32:1, sync_output); - assert_eq(sync_output, sync_out); - (tok) - }(tok); - send(tok, terminator, true); - } +pub struct RleBlockDecoderResp { + status: RleBlockDecoderStatus } -#[test_proc] -proc RleDataPacker_empty_blocks_test { - terminator: chan out; - in_s: chan out; - out_r: chan in; - sync_r: chan in; - - config(terminator: chan out) { - let (in_s, in_r) = chan("in"); - let (out_s, out_r) = chan("out"); - let (sync_s, sync_r) = chan("sync"); - - spawn RleDataPacker(in_r, out_s, sync_s); - - (terminator, in_s, out_r, sync_r) - } - - init { } - - next(state: ()) { - let tok = join(); - let EncodedRleBlocks: RleTestVector[8] = [ - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0x1, SymbolCount:0x1), - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0x3, SymbolCount:0x4), - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0x5, SymbolCount:0x10), - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0xFF, SymbolCount:0x0), - ]; - let tok = for ((counter, block), tok): ((u32, RleTestVector), token) in enumerate(EncodedRleBlocks) { - let last_block = (counter == (array_size(EncodedRleBlocks) - u32:1)); - let data_in = BlockDataPacket { - last: true, - last_block, - id: counter, - data: block.0 as BlockData, - length: block.1 as BlockPacketLength - }; - let tok = send(tok, in_s, data_in); - trace_fmt!("Sent #{} raw encoded block, {:#x}", counter + u32:1, data_in); - (tok) - }(tok); - - let RleInputs: RleInput[3] = [ - RleInput {last: true, symbol: Symbol:0x1, count: BlockSize:0x1}, - RleInput {last: true, symbol: Symbol:0x3, count: BlockSize:0x4}, - RleInput {last: true, symbol: Symbol:0x5, count: BlockSize:0x10}, - ]; - let tok = for ((counter, rle_in), tok): ((u32, RleInput), token) in enumerate(RleInputs) { - let (tok, dec_output) = recv(tok, out_r); - trace_fmt!("Received #{} packed rle encoded block, {:#x}", counter + u32:1, dec_output); - assert_eq(dec_output, rle_in); - (tok) - }(tok); - - let BlockSyncDataInputs: BlockSyncData[8] = [ - BlockSyncData { id: 0, count: BlockSize:0x0, last_block: false }, - BlockSyncData { id: 1, count: BlockSize:0x1, last_block: false }, - BlockSyncData { id: 2, count: BlockSize:0x0, last_block: false }, - BlockSyncData { id: 3, count: BlockSize:0x4, last_block: false }, - BlockSyncData { id: 4, count: BlockSize:0x0, last_block: false }, - BlockSyncData { id: 5, count: BlockSize:0x10, last_block: false }, - BlockSyncData { id: 6, count: BlockSize:0x0, last_block: false }, - BlockSyncData { id: 7, count: BlockSize:0x0, last_block: true }, - ]; - let tok = for ((counter, sync_data), tok): ((u32, BlockSyncData), token) in enumerate(BlockSyncDataInputs) { - let (tok, sync_output) = recv(tok, sync_r); - trace_fmt!("Received #{} synchronization data, {:#x}", counter + u32:1, sync_output); - assert_eq(sync_output, sync_data); - (tok) - }(tok); - send(tok, terminator, true); - } +struct RleBlockDecoderState { + req: RleBlockDecoderReq, + req_valid: bool, } -struct BatchPackerState { - batch: BlockData, - symbols_in_batch: BlockPacketLength, - symbols_in_block: BlockPacketLength, - prev_last: bool, - prev_sync: BlockSyncData, -} +pub proc RleBlockDecoder { + type Req = RleBlockDecoderReq; + type Resp = RleBlockDecoderResp; + type Output = ExtendedBlockDataPacket; -const ZERO_BATCH_STATE = zero!(); -const ZERO_BLOCK_SYNC_DATA = zero!(); -const ZERO_RLE_OUTPUT = zero!(); -const EMPTY_RLE_OUTPUT = RleOutput {last: true, ..ZERO_RLE_OUTPUT}; + type State = RleBlockDecoderState; -proc BatchPacker { - rle_data_r: chan in; - sync_r: chan in; - block_data_s: chan out; + req_r: chan in; + resp_s: chan out; + output_s: chan out; - config( - rle_data_r: chan in, - sync_r: chan in, - block_data_s: chan out - ) { - (rle_data_r, sync_r, block_data_s) - } + config( req_r: chan in, + resp_s: chan out, + output_s: chan out, + ) { (req_r, resp_s, output_s) } - // Init the state to signal new batch to process - init { (BatchPackerState { prev_last: true, ..ZERO_BATCH_STATE }) } + init { zero!() } - next(state: BatchPackerState) { - let tok = join(); - trace_fmt!("start state: {:#x}", state); - let prev_expected_symbols_in_block = state.prev_sync.count as BlockPacketLength; - let symbols_in_batch = state.symbols_in_batch; - let symbols_in_block = state.symbols_in_block; - let block_in_progress = (symbols_in_block != prev_expected_symbols_in_block); - trace_fmt!("block_in_progress: {:#x}", block_in_progress); - - // Finished receiving RLE data of the previous block - // Proceed with receiving sync data for the next block - let start_new_block = !block_in_progress; - let (tok, sync_data) = recv_if(tok, sync_r, start_new_block, state.prev_sync); - if (start_new_block) { - trace_fmt!("received sync_data: {:#x}", sync_data); - } else { - trace_fmt!("got sync_data from the state: {:#x}", sync_data); - }; + next(state: State) { + const MAX_OUTPUT_SYMBOLS = (DATA_W / u32:8); + const MAX_LEN = MAX_OUTPUT_SYMBOLS as uN[common::BLOCK_SIZE_WIDTH]; - let expected_symbols_in_block = if (start_new_block) { sync_data.count as BlockPacketLength } else { prev_expected_symbols_in_block }; - trace_fmt!("expected_symbols_in_block: {:#x}", expected_symbols_in_block); + let tok0 = join(); - let batch = state.batch; - let empty_block = (expected_symbols_in_block == BlockPacketLength:0); - trace_fmt!("batch: {:#x}", batch); - trace_fmt!("empty_block: {:#x}", empty_block); + let (tok1, req) = recv_if(tok0, req_r, !state.req_valid, state.req); - let do_recv_rle = !empty_block && block_in_progress; - let default_rle_output = if (empty_block) { EMPTY_RLE_OUTPUT } else { ZERO_RLE_OUTPUT }; - let (tok, decoded_data) = recv_if(tok, rle_data_r, do_recv_rle, default_rle_output); - if (do_recv_rle) { - trace_fmt!("received rle_data: {:#x}", decoded_data); - } else { - trace_fmt!("got empty rle_data: {:#x}", decoded_data); - }; + let last = req.length <= MAX_LEN; + let length = if last { req.length } else { MAX_LEN }; + let data = unroll_for! (i, data): (u32, uN[DATA_W]) in range(u32:0, MAX_OUTPUT_SYMBOLS) { + bit_slice_update(data, i * u32:8, req.symbol) + }(uN[DATA_W]:0); - let (batch, symbols_in_batch, symbols_in_block) = if (do_recv_rle) { - // TODO: Improve performance: remove variable shift - let shift = symbols_in_batch << u32:3; // multiply by 8 bits - let updated_batch = batch | ((decoded_data.symbol as BlockData) << shift); - let updated_symbols_in_batch = symbols_in_batch + BlockPacketLength:1; - let updated_symbols_in_block = symbols_in_block + BlockPacketLength:1; - (updated_batch, updated_symbols_in_batch, updated_symbols_in_block) - } else { - (batch, symbols_in_batch, symbols_in_block) - }; - trace_fmt!("updated batch: {:#x}", batch); - trace_fmt!("updated symbols_in_batch: {:#x}", symbols_in_batch); - trace_fmt!("updated symbols_in_block: {:#x}", symbols_in_block); - - let block_in_progress = (symbols_in_block != expected_symbols_in_block); - trace_fmt!("updated block_in_progress: {:#x}", block_in_progress); - - // Last should not occur when batch is still being processed - assert!(!(!block_in_progress ^ decoded_data.last), "corrupted_decoding_flow"); - - let batch_full = symbols_in_batch >= BATCH_SIZE; - trace_fmt!("batch_full: {:#x}", batch_full); - // Send decoded RLE packet when - // - batch size reached the maximal size - // - RLE block decoding is finished - // - Decoded RLE block is empty and is the last block in ZSTD frame - let last = decoded_data.last || (sync_data.last_block && empty_block); - let do_send_batch = (batch_full || last); - trace_fmt!("do_send_batch: {:#x}", do_send_batch); - - let decoded_batch_data = ExtendedBlockDataPacket { - // Decoded RLE block is always a literal + let output = Output { msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { - last: last, - last_block: sync_data.last_block, - id: sync_data.id, - data: batch as BlockData, - // length in bits - length: (symbols_in_batch << 3) as BlockPacketLength, + last, + last_block: req.last_block, + id: req.id, + data: checked_cast(data), + length: checked_cast(length << 3), } }; - let data_tok = - send_if(tok, block_data_s, do_send_batch, decoded_batch_data); - if (do_send_batch) { - trace_fmt!("sent decoded_batch_data: {:#x}", decoded_batch_data); - } else { - trace_fmt!("decoded_batch_data: {:#x}", decoded_batch_data); - }; - - let (new_batch, new_symbols_in_batch) = if (do_send_batch) { - (BlockData:0, BlockPacketLength:0) - } else { - (batch, symbols_in_batch) - }; + send_if(tok1, resp_s, last, zero!()); + send(tok1, output_s, output); - let (new_sync_data, new_symbols_in_block) = if (decoded_data.last || (sync_data.last_block && empty_block)) { - (ZERO_BLOCK_SYNC_DATA, BlockPacketLength:0) + if last { + zero!() } else { - (sync_data, symbols_in_block) - }; - - let new_state = BatchPackerState { - batch: new_batch, - symbols_in_batch: new_symbols_in_batch, - symbols_in_block: new_symbols_in_block, - prev_last: decoded_data.last, - prev_sync: new_sync_data - }; - - trace_fmt!("new_state: {:#x}", new_state); - - new_state + let length = req.length - MAX_LEN; + State { + req: Req { length, ..req }, + req_valid: true, + } + } } } -type BatchTestVector = (Symbol, bool); - -#[test_proc] -proc BatchPacker_test { - terminator: chan out; - in_s: chan out; - sync_s: chan out; - out_r: chan in; - - config(terminator: chan out) { - let (in_s, in_r) = chan("in"); - let (sync_s, sync_r) = chan("sync"); - let (out_s, out_r) = chan("out"); - spawn BatchPacker(in_r, sync_r, out_s); - - (terminator, in_s, sync_s, out_r) - } +const TEST_DATA_W = u32:64; - init { } +#[test_proc] +proc RleBlockDecoderTest { + type Req = RleBlockDecoderReq; + type Resp = RleBlockDecoderResp; + type Output = ExtendedBlockDataPacket; - next(state: ()) { - let tok = join(); - let SyncData: BlockSyncData[6] = [ - BlockSyncData { last_block: false, count: SymbolCount:1, id: u32:0 }, - BlockSyncData { last_block: false, count: SymbolCount:2, id: u32:1 }, - BlockSyncData { last_block: false, count: SymbolCount:4, id: u32:2 }, - BlockSyncData { last_block: false, count: SymbolCount:8, id: u32:3 }, - BlockSyncData { last_block: false, count: SymbolCount:16, id: u32:4 }, - BlockSyncData { last_block: true, count: SymbolCount:31, id: u32:5 }, - ]; - let tok = for ((counter, sync_data), tok): ((u32, BlockSyncData), token) in enumerate(SyncData) { - let tok = send(tok, sync_s, sync_data); - trace_fmt!("Sent #{} synchronization data, {:#x}", counter + u32:1, sync_data); - (tok) - }(tok); - - let DecodedRleBlocks: BatchTestVector[62] = [ - // 1st block - (Symbol:0x01, bool:true), - // 2nd block - (Symbol:0x02, bool:false), (Symbol:0x02, bool:true), - // 3rd block - (Symbol:0x03, bool:false), (Symbol:0x03, bool:false), (Symbol:0x03, bool:false), - (Symbol:0x03, bool:true), - // 4th block - (Symbol:0x04, bool:false), (Symbol:0x04, bool:false), (Symbol:0x04, bool:false), - (Symbol:0x04, bool:false), (Symbol:0x04, bool:false), (Symbol:0x04, bool:false), - (Symbol:0x04, bool:false), (Symbol:0x04, bool:true), - // 5th block - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:true), - // 6th block - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), (Symbol:0x06, bool:false), - (Symbol:0x06, bool:true), - ]; - let tok = for ((counter, test_data), tok): ((u32, BatchTestVector), token) in enumerate(DecodedRleBlocks) { - let symbol = test_data.0 as Symbol; - let last = test_data.1; - let data_in = RleOutput { symbol, last }; - let tok = send(tok, in_s, data_in); - trace_fmt!("Sent #{} decoded rle symbol, {:#x}", counter + u32:1, data_in); - (tok) - }(tok); - - let BatchedDecodedRleSymbols: ExtendedBlockDataPacket[10] = [ - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:0, data: BlockData:0x01, length: BlockPacketLength:8}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:1, data: BlockData:0x0202, length: BlockPacketLength:16}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:2, data: BlockData:0x03030303, length: BlockPacketLength:32}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:3, data: BlockData:0x0404040404040404, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:false, last_block: bool:false, id: u32:4, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:4, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:false, last_block: bool:true, id: u32:5, data: BlockData:0x0606060606060606, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:false, last_block: bool:true, id: u32:5, data: BlockData:0x0606060606060606, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:false, last_block: bool:true, id: u32:5, data: BlockData:0x0606060606060606, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:true, id: u32:5, data: BlockData:0x06060606060606, length: BlockPacketLength:56}}, - ]; - - let tok = for ((counter, expected), tok): ((u32, ExtendedBlockDataPacket), token) in enumerate(BatchedDecodedRleSymbols) { - let (tok, dec_output) = recv(tok, out_r); - trace_fmt!("Received #{} batched decoded rle symbols, {:#x}", counter + u32:1, dec_output); - assert_eq(dec_output, expected); - (tok) - }(tok); - send(tok, terminator, true); - } -} + type Data = uN[TEST_DATA_W]; + type Length = uN[common::BLOCK_SIZE_WIDTH]; -#[test_proc] -proc BatchPacker_empty_blocks_test { terminator: chan out; - in_s: chan out; - sync_s: chan out; - out_r: chan in; - config(terminator: chan out) { - let (in_s, in_r) = chan("in"); - let (sync_s, sync_r) = chan("sync"); - let (out_s, out_r) = chan("out"); + req_s: chan out; + resp_r: chan in; + output_r: chan in; + + config (terminator: chan out) { + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + let (output_s, output_r) = chan("output"); - spawn BatchPacker(in_r, sync_r, out_s); + spawn RleBlockDecoder( + req_r, resp_s, output_s + ); - (terminator, in_s, sync_s, out_r) + (terminator, req_s, resp_r, output_r) } - init { } + init { } - next(state: ()) { + next (state: ()) { let tok = join(); - let SyncData: BlockSyncData[8] = [ - BlockSyncData { last_block: false, count: SymbolCount:0, id: u32:0 }, - BlockSyncData { last_block: false, count: SymbolCount:1, id: u32:1 }, - BlockSyncData { last_block: false, count: SymbolCount:0, id: u32:2 }, - BlockSyncData { last_block: false, count: SymbolCount:4, id: u32:3 }, - BlockSyncData { last_block: false, count: SymbolCount:0, id: u32:4 }, - BlockSyncData { last_block: false, count: SymbolCount:16, id: u32:5 }, - BlockSyncData { last_block: false, count: SymbolCount:0, id: u32:6 }, - BlockSyncData { last_block: true, count: SymbolCount:0, id: u32:7 }, - ]; - let tok = for ((counter, sync_data), tok): ((u32, BlockSyncData), token) in enumerate(SyncData) { - let tok = send(tok, sync_s, sync_data); - trace_fmt!("Sent #{} synchronization data, {:#x}", counter + u32:1, sync_data); - (tok) - }(tok); - - let DecodedRleBlocks: BatchTestVector[21] = [ - // 0 block - // EMPTY - // 1st block - (Symbol:0x01, bool:true), - // 2nd block - // EMPTY - // 3rd block - (Symbol:0x03, bool:false), (Symbol:0x03, bool:false), (Symbol:0x03, bool:false), - (Symbol:0x03, bool:true), - // 4th block - // EMPTY - // 5th block - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), (Symbol:0x05, bool:false), - (Symbol:0x05, bool:true), - // 6th block - // EMPTY - // 7th block - // EMPTY - ]; - let tok = for ((counter, test_data), tok): ((u32, BatchTestVector), token) in enumerate(DecodedRleBlocks) { - let symbol = test_data.0 as Symbol; - let last = test_data.1; - let data_in = RleOutput { symbol, last }; - let tok = send(tok, in_s, data_in); - trace_fmt!("Sent #{} decoded rle symbol, {:#x}", counter + u32:1, data_in); - (tok) - }(tok); - - let BatchedDecodedRleSymbols: ExtendedBlockDataPacket[9] = [ - // 0 block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:0, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 1st block - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:1, data: BlockData:0x01, length: BlockPacketLength:8}}, - // 2nd block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:2, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 3rd block - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:3, data: BlockData:0x03030303, length: BlockPacketLength:32}}, - // 4th block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:4, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 5th block - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:false, last_block: bool:false, id: u32:5, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:5, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - // 6th block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:6, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 7th block - // EMPTY with LAST_BLOCK - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:true, id: u32:7, data: BlockData:0x0, length: BlockPacketLength:0}}, - ]; - - let tok = for ((counter, expected), tok): ((u32, ExtendedBlockDataPacket), token) in enumerate(BatchedDecodedRleSymbols) { - let (tok, dec_output) = recv(tok, out_r); - trace_fmt!("Received #{} batched decoded rle symbols, {:#x}", counter + u32:1, dec_output); - assert_eq(dec_output, expected); - (tok) - }(tok); - send(tok, terminator, true); - } -} -pub proc RleBlockDecoder { - input_r: chan in; - output_s: chan out; + let tok = send(tok, req_s, Req { id: u32:5, symbol: u8:0xAB, length: Length:0x28, last_block: true }); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { status: RleBlockDecoderStatus::OKAY }); - config(input_r: chan in, output_s: chan out) { - let (in_s, in_r) = chan("in"); - let (out_s, out_r) = chan("out"); - let (sync_s, sync_r) = chan("sync"); + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: false, + last_block: true, + id: u32:5, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:64 + } + }); - spawn RleDataPacker(input_r, in_s, sync_s); - spawn rle_dec::RunLengthDecoder( - in_r, out_s); - spawn BatchPacker(out_r, sync_r, output_s); + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: false, + last_block: true, + id: u32:5, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:64 + } + }); - (input_r, output_s) - } - init { } + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: false, + last_block: true, + id: u32:5, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:64 + } + }); - next(state: ()) { } -} -#[test_proc] -proc RleBlockDecoder_test { - terminator: chan out; - in_s: chan out; - out_r: chan in; + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: false, + last_block: true, + id: u32:5, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:64 + } + }); - config(terminator: chan out) { - let (in_s, in_r) = chan("in"); - let (out_s, out_r) = chan("out"); - spawn RleBlockDecoder(in_r, out_s); + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: true, + last_block: true, + id: u32:5, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:64 + } + }); - (terminator, in_s, out_r) - } + let tok = send(tok, req_s, Req { id: u32:1, symbol: u8:0xAB, length: Length:0, last_block: true }); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { status: RleBlockDecoderStatus::OKAY }); - init { } + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: true, + last_block: true, + id: u32:1, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:0 + } + }); + + let tok = send(tok, req_s, Req { id: u32:10, symbol: u8:0xAB, length: Length:0, last_block: false }); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, Resp { status: RleBlockDecoderStatus::OKAY }); + + let (tok, output) = recv(tok, output_r); + assert_eq(output, Output { + msg_type: SequenceExecutorMessageType::LITERAL, + packet: BlockDataPacket { + last: true, + last_block: false, + id: u32:10, + data: BlockData:0xABAB_ABAB_ABAB_ABAB, + length: BlockPacketLength:0 + } + }); - next(state: ()) { - let tok = join(); - let EncodedRleBlocks: RleTestVector[6] = [ - (Symbol:0x1, SymbolCount:0x1), - (Symbol:0x2, SymbolCount:0x2), - (Symbol:0x3, SymbolCount:0x4), - (Symbol:0x4, SymbolCount:0x8), - (Symbol:0x5, SymbolCount:0x10), - (Symbol:0x6, SymbolCount:0x1F), - ]; - let tok = for ((counter, block), tok): ((u32, RleTestVector), token) in enumerate(EncodedRleBlocks) { - let last_block = (counter == (array_size(EncodedRleBlocks) - u32:1)); - let data_in = BlockDataPacket { - last: true, // RLE block fits into single packet, each will be last for given block - last_block, - id: counter, - data: block.0 as BlockData, - length: block.1 as BlockPacketLength - }; - let tok = send(tok, in_s, data_in); - trace_fmt!("Sent #{} raw encoded block, {:#x}", counter + u32:1, data_in); - (tok) - }(tok); - - let BatchedDecodedRleSymbols: ExtendedBlockDataPacket[10] = [ - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:true, last_block: bool:false, id: u32:0, data: BlockData:0x01, length: BlockPacketLength:8}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:true, last_block: bool:false, id: u32:1, data: BlockData:0x0202, length: BlockPacketLength:16}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:true, last_block: bool:false, id: u32:2, data: BlockData:0x03030303, length: BlockPacketLength:32}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:true, last_block: bool:false, id: u32:3, data: BlockData:0x0404040404040404, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:false, last_block: bool:false, id: u32:4, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:true, last_block: bool:false, id: u32:4, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:false, last_block: bool:true, id: u32:5, data: BlockData:0x0606060606060606, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:false, last_block: bool:true, id: u32:5, data: BlockData:0x0606060606060606, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:false, last_block: bool:true, id: u32:5, data: BlockData:0x0606060606060606, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket { last: bool:true, last_block: bool:true, id: u32:5, data: BlockData:0x06060606060606, length: BlockPacketLength:56}}, - ]; - - let tok = for ((counter, expected), tok): ((u32, ExtendedBlockDataPacket), token) in enumerate(BatchedDecodedRleSymbols) { - let (tok, dec_output) = recv(tok, out_r); - trace_fmt!("Received #{} batched decoded rle symbols, {:#x}", counter + u32:1, dec_output); - assert_eq(dec_output, expected); - (tok) - }(tok); send(tok, terminator, true); } } -#[test_proc] -proc RleBlockDecoder_empty_blocks_test { - terminator: chan out; - in_s: chan out; - out_r: chan in; - config(terminator: chan out) { - let (in_s, in_r) = chan("in"); - let (out_s, out_r) = chan("out"); +const INST_DATA_W = u32:64; - spawn RleBlockDecoder(in_r, out_s); +proc RleBlockDecoderInst { + type Req = RleBlockDecoderReq; + type Resp = RleBlockDecoderResp; + type Output = ExtendedBlockDataPacket; - (terminator, in_s, out_r) + type Data = uN[INST_DATA_W]; + type Length = uN[common::BLOCK_SIZE_WIDTH]; + + config( + req_r: chan in, + resp_s: chan out, + output_s: chan out, + ) { + spawn RleBlockDecoder(req_r, resp_s, output_s); } - init { } - next(state: ()) { - let tok = join(); - let EncodedRleBlocks: RleTestVector[8] = [ - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0x1, SymbolCount:0x1), - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0x3, SymbolCount:0x4), - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0x5, SymbolCount:0x10), - (Symbol:0xFF, SymbolCount:0x0), - (Symbol:0xFF, SymbolCount:0x0), - ]; - let tok = for ((counter, block), tok): ((u32, RleTestVector), token) in enumerate(EncodedRleBlocks) { - let last_block = (counter == (array_size(EncodedRleBlocks) - u32:1)); - let data_in = BlockDataPacket { - last: true, // RLE block fits into single packet, each will be last for given block - last_block, - id: counter, - data: block.0 as BlockData, - length: block.1 as BlockPacketLength - }; - let tok = send(tok, in_s, data_in); - trace_fmt!("Sent #{} raw encoded block, {:#x}", counter + u32:1, data_in); - (tok) - }(tok); - - let BatchedDecodedRleSymbols: ExtendedBlockDataPacket[9] = [ - // 0 block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:0, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 1st block - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:1, data: BlockData:0x01, length: BlockPacketLength:8}}, - // 2nd block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:2, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 3rd block - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:3, data: BlockData:0x03030303, length: BlockPacketLength:32}}, - // 4th block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:4, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 5th block - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:false, last_block: bool:false, id: u32:5, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:5, data: BlockData:0x0505050505050505, length: BlockPacketLength:64}}, - // 6th block - // EMPTY - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:false, id: u32:6, data: BlockData:0x0, length: BlockPacketLength:0}}, - // 7th block - // EMPTY with LAST_BLOCK - ExtendedBlockDataPacket {msg_type: SequenceExecutorMessageType::LITERAL, packet: BlockDataPacket {last: bool:true, last_block: bool:true, id: u32:7, data: BlockData:0x0, length: BlockPacketLength:0}}, - ]; - - let tok = for ((counter, expected), tok): ((u32, ExtendedBlockDataPacket), token) in enumerate(BatchedDecodedRleSymbols) { - let (tok, dec_output) = recv(tok, out_r); - trace_fmt!("Received #{} batched decoded rle symbols, {:#x}", counter + u32:1, dec_output); - assert_eq(dec_output, expected); - (tok) - }(tok); - send(tok, terminator, true); - } + init { } + + next (state: ()) {} } From d7e42d150cd7801d96e749408b85fc65a43ce3c1 Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Tue, 8 Oct 2024 17:53:10 +0200 Subject: [PATCH 22/46] modules/zstd: Add ZstdDecoder Co-authored-by: Pawel Czarnecki Signed-off-by: Robert Winkler Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 290 +++-- xls/modules/zstd/zstd_dec.x | 1740 ++++++++++++++++++++++------- xls/modules/zstd/zstd_dec_test.cc | 297 ----- 3 files changed, 1460 insertions(+), 867 deletions(-) delete mode 100644 xls/modules/zstd/zstd_dec_test.cc diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 11edc01a0d..1fdf8c2420 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -21,7 +21,6 @@ load( "//xls/build_rules:xls_build_defs.bzl", "xls_benchmark_ir", "xls_benchmark_verilog", - "xls_dslx_ir", "xls_dslx_library", "xls_dslx_test", "xls_dslx_verilog", @@ -776,153 +775,6 @@ place_and_route( target_die_utilization_percentage = "10", ) -xls_dslx_library( - name = "zstd_dec_dslx", - srcs = [ - "zstd_dec.x", - ], - deps = [ - ":block_header_dslx", - ":buffer_dslx", - ":common_dslx", - ":frame_header_dslx", - ":frame_header_test_dslx", - ":ram_printer_dslx", - ":repacketizer_dslx", - ":sequence_executor_dslx", - "//xls/examples:ram_dslx", - ], -) - -xls_dslx_verilog( - name = "zstd_dec_verilog", - codegen_args = { - "module_name": "ZstdDecoder", - "generator": "pipeline", - "delay_model": "asap7", - "ram_configurations": ",".join([ - "{ram_name}:1R1W:{rd_req}:{rd_resp}:{wr_req}:{wr_resp}:{latency}".format( - latency = 5, - ram_name = "ram{}".format(num), - rd_req = "zstd_dec__ram_rd_req_{}_s".format(num), - rd_resp = "zstd_dec__ram_rd_resp_{}_r".format(num), - wr_req = "zstd_dec__ram_wr_req_{}_s".format(num), - wr_resp = "zstd_dec__ram_wr_resp_{}_r".format(num), - ) - for num in range(7) - ]), - "pipeline_stages": "10", - "reset": "rst", - "reset_data_path": "true", - "reset_active_low": "false", - "reset_asynchronous": "true", - "flop_inputs": "false", - "flop_single_value_channels": "false", - "flop_outputs": "false", - "worst_case_throughput": "1", - "use_system_verilog": "false", - }, - dslx_top = "ZstdDecoder", - library = ":zstd_dec_dslx", - # TODO: 2024-01-15: Workaround for https://github.com/google/xls/issues/869 - # Force proc inlining for IR optimization - opt_ir_args = { - "inline_procs": "true", - }, - tags = ["manual"], - verilog_file = "zstd_dec.v", -) - -xls_dslx_ir( - name = "zstd_dec_test_ir", - dslx_top = "ZstdDecoderTest", - ir_file = "zstd_dec_test.ir", - library = ":zstd_dec_dslx", - tags = ["manual"], -) - -cc_test( - name = "zstd_dec_cc_test", - size = "large", - srcs = [ - "zstd_dec_test.cc", - ], - data = [ - ":zstd_dec_test.ir", - ], - shard_count = 50, - deps = [ - ":data_generator", - "//xls/common:xls_gunit_main", - "//xls/common/file:filesystem", - "//xls/common/file:get_runfile_path", - "//xls/common/status:matchers", - "//xls/common/status:ret_check", - "//xls/interpreter:channel_queue", - "//xls/interpreter:serial_proc_runtime", - "//xls/ir", - "//xls/ir:bits", - "//xls/ir:channel", - "//xls/ir:events", - "//xls/ir:ir_parser", - "//xls/ir:value", - "//xls/jit:jit_proc_runtime", - "@com_google_absl//absl/container:flat_hash_map", - "@com_google_absl//absl/log", - "@com_google_absl//absl/status:statusor", - "@com_google_absl//absl/types:span", - "@com_google_googletest//:gtest", - "@zstd", - ], -) - -xls_benchmark_ir( - name = "zstd_dec_opt_ir_benchmark", - src = ":zstd_dec_verilog.opt.ir", - benchmark_ir_args = { - #TODO: rewrite ram in opt_ir step to perform valid IR benchmark - "pipeline_stages": "1", - "delay_model": "asap7", - }, - tags = ["manual"], -) - -verilog_library( - name = "zstd_dec_verilog_lib", - srcs = [ - ":zstd_dec.v", - ], - tags = ["manual"], -) - -synthesize_rtl( - name = "zstd_dec_synth_asap7", - standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", - tags = ["manual"], - top_module = "ZstdDecoder", - deps = [ - ":zstd_dec_verilog_lib", - ], -) - -benchmark_synth( - name = "zstd_dec_benchmark_synth", - synth_target = ":zstd_dec_synth_asap7", - tags = ["manual"], -) - -place_and_route( - name = "zstd_dec_place_and_route", - clock_period = "750", - core_padding_microns = 2, - min_pin_distance = "0.5", - placement_density = "0.30", - stop_after_step = "global_routing", - synthesized_rtl = ":zstd_dec_synth_asap7", - tags = ["manual"], - target_die_utilization_percentage = "10", -) - xls_dslx_library( name = "axi_csr_accessor_dslx", srcs = [ @@ -1075,3 +927,145 @@ place_and_route( tags = ["manual"], target_die_utilization_percentage = "10", ) + +xls_dslx_library( + name = "zstd_dec_dslx", + srcs = [ + "zstd_dec.x", + ], + deps = [ + ":axi_csr_accessor_dslx", + ":block_header_dec_dslx", + ":block_header_dslx", + ":common_dslx", + ":csr_config_dslx", + ":dec_mux_dslx", + ":frame_header_dec_dslx", + ":raw_block_dec_dslx", + ":repacketizer_dslx", + ":rle_block_dec_dslx", + ":sequence_executor_dslx", + "//xls/examples:ram_dslx", + "//xls/modules/zstd/memory:mem_reader_dslx", + ], +) + +xls_dslx_test( + name = "zstd_dec_dslx_test", + library = ":zstd_dec_dslx", + tags = ["manual"], +) + +zstd_dec_codegen_args = common_codegen_args | { + "module_name": "ZstdDecoder", + "clock_period_ps": "0", + "pipeline_stages": "10", + "flop_inputs_kind": "skid", + "flop_outputs_kind": "skid", +} + +xls_dslx_verilog( + name = "zstd_dec_verilog", + codegen_args = zstd_dec_codegen_args, + dslx_top = "ZstdDecoderInst", + library = ":zstd_dec_dslx", + tags = ["manual"], + verilog_file = "zstd_dec.v", +) + +zstd_dec_internal_codegen_args = common_codegen_args | { + "module_name": "ZstdDecoderInternal", + "pipeline_stages": "2", +} + +xls_dslx_verilog( + name = "zstd_dec_internal_verilog", + codegen_args = zstd_dec_internal_codegen_args, + dslx_top = "ZstdDecoderInternalInst", + library = ":zstd_dec_dslx", + tags = ["manual"], + verilog_file = "zstd_dec_internal.v", +) + +xls_benchmark_ir( + name = "zstd_dec_internal_opt_ir_benchmark", + src = ":zstd_dec_internal_verilog.opt.ir", + benchmark_ir_args = { + "top": "__zstd_dec__ZstdDecoderInternalInst__ZstdDecoderInternal_0__16_64_8_4_16_next", + "pipeline_stages": "10", + }, + tags = ["manual"], +) + +verilog_library( + name = "zstd_dec_internal_verilog_lib", + srcs = [ + ":zstd_dec_internal.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "zstd_dec_internal_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "ZstdDecoderInternal", + deps = [ + ":zstd_dec_internal_verilog_lib", + ], +) + +benchmark_synth( + name = "zstd_dec_internal_benchmark_synth", + synth_target = ":zstd_dec_internal_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "zstd_dec_internal_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.35", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":zstd_dec_internal_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + +verilog_library( + name = "zstd_dec_verilog_lib", + srcs = [ + ":xls_fifo_wrapper.v", + ":zstd_dec.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "zstd_dec_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "ZstdDecoder", + deps = [ + ":zstd_dec_verilog_lib", + ], +) + +benchmark_synth( + name = "zstd_dec_benchmark_synth", + synth_target = ":zstd_dec_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "zstd_dec_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.4", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":zstd_dec_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) diff --git a/xls/modules/zstd/zstd_dec.x b/xls/modules/zstd/zstd_dec.x index 0f9fac906e..6e28bd8167 100644 --- a/xls/modules/zstd/zstd_dec.x +++ b/xls/modules/zstd/zstd_dec.x @@ -17,482 +17,1378 @@ // https://datatracker.ietf.org/doc/html/rfc8878 import std; +import xls.examples.ram; +import xls.modules.zstd.axi_csr_accessor; +import xls.modules.zstd.common; +import xls.modules.zstd.memory.axi; +import xls.modules.zstd.csr_config; +import xls.modules.zstd.memory.mem_reader; +import xls.modules.zstd.frame_header_dec; import xls.modules.zstd.block_header; -import xls.modules.zstd.block_dec; +import xls.modules.zstd.block_header_dec; +import xls.modules.zstd.raw_block_dec; +import xls.modules.zstd.rle_block_dec; +import xls.modules.zstd.dec_mux; import xls.modules.zstd.sequence_executor; -import xls.modules.zstd.buffer as buff; -import xls.modules.zstd.common; -import xls.modules.zstd.frame_header; -import xls.modules.zstd.frame_header_test; -import xls.modules.zstd.magic; import xls.modules.zstd.repacketizer; -import xls.examples.ram; -type Buffer = buff::Buffer; -type BlockDataPacket = common::BlockDataPacket; -type BlockData = common::BlockData; type BlockSize = common::BlockSize; -type SequenceExecutorPacket = common::SequenceExecutorPacket; -type ZstdDecodedPacket = common::ZstdDecodedPacket; - -// TODO: all of this porboably should be in common.x -const TEST_WINDOW_LOG_MAX_LIBZSTD = frame_header_test::TEST_WINDOW_LOG_MAX_LIBZSTD; - -const ZSTD_RAM_ADDR_WIDTH = sequence_executor::ZSTD_RAM_ADDR_WIDTH; -const RAM_DATA_WIDTH = sequence_executor::RAM_DATA_WIDTH; -const RAM_NUM_PARTITIONS = sequence_executor::RAM_NUM_PARTITIONS; -const ZSTD_HISTORY_BUFFER_SIZE_KB = sequence_executor::ZSTD_HISTORY_BUFFER_SIZE_KB; - -const BUFFER_WIDTH = common::BUFFER_WIDTH; -const DATA_WIDTH = common::DATA_WIDTH; -const ZERO_FRAME_HEADER = frame_header::ZERO_FRAME_HEADER; -const ZERO_BLOCK_HEADER = block_header::ZERO_BLOCK_HEADER; - -enum ZstdDecoderStatus : u8 { - DECODE_MAGIC_NUMBER = 0, - DECODE_FRAME_HEADER = 1, - DECODE_BLOCK_HEADER = 2, - FEED_BLOCK_DECODER = 3, - DECODE_CHECKSUM = 4, - ERROR = 255, +type BlockType = common::BlockType; +type BlockHeader = block_header::BlockHeader; + +enum ZstdDecoderInternalFsm: u4 { + IDLE = 0, + READ_CONFIG = 1, + DECODE_FRAME_HEADER = 2, + DECODE_BLOCK_HEADER = 3, + DECODE_RAW_BLOCK = 4, + DECODE_RLE_BLOCK = 5, + DECODE_COMPRESSED_BLOCK = 6, + DECODE_CHECKSUM = 7, + FINISH = 8, + ERROR = 13, + INVALID = 15, } -struct ZstdDecoderState { - status: ZstdDecoderStatus, - buffer: Buffer, - frame_header: frame_header::FrameHeader, - block_size_bytes: BlockSize, - last: bool, - bytes_sent: BlockSize, +enum ZstdDecoderStatus: u3 { + OKAY = 0, + FINISHED = 1, + FRAME_HEADER_CORRUPTED = 2, + FRAME_HEADER_UNSUPPORTED_WINDOW_SIZE = 3, + BLOCK_HEADER_CORRUPTED = 4, } -const ZERO_DECODER_STATE = zero!(); - -fn decode_magic_number(state: ZstdDecoderState) -> (bool, BlockDataPacket, ZstdDecoderState) { - trace_fmt!("zstd_dec: decode_magic_number: DECODING NEW FRAME"); - trace_fmt!("zstd_dec: decode_magic_number: state: {:#x}", state); - trace_fmt!("zstd_dec: decode_magic_number: Decoding magic number"); - let magic_result = magic::parse_magic_number(state.buffer); - trace_fmt!("zstd_dec: decode_magic_number: magic_result: {:#x}", magic_result); - let new_state = match magic_result.status { - magic::MagicStatus::OK => ZstdDecoderState { - status: ZstdDecoderStatus::DECODE_FRAME_HEADER, - buffer: magic_result.buffer, - ..state - }, - magic::MagicStatus::CORRUPTED => ZstdDecoderState { - status: ZstdDecoderStatus::ERROR, - ..ZERO_DECODER_STATE - }, - magic::MagicStatus::NO_ENOUGH_DATA => state, - _ => state, - }; - trace_fmt!("zstd_dec: decode_magic_number: new_state: {:#x}", new_state); - - (false, zero!(), new_state) +enum Csr: u3 { + STATUS = 0, // Keeps the code describing the current state of the ZSTD Decoder + START = 1, // Writing 1 when decoder is in IDLE state starts the decoding process + RESET = 2, // Writing 1 will reset the decoder to the IDLE state + INPUT_BUFFER = 3, // Keeps the base address for the input buffer that is used for storing the frame to decode + OUTPUT_BUFFER = 4, // Keeps the base address for the output buffer, ZSTD Decoder will write the decoded frame into memory starting from this address. + WHO_AM_I = 5, // Contains the identification number of the ZSTD Decoder } -fn decode_frame_header(state: ZstdDecoderState) -> (bool, BlockDataPacket, ZstdDecoderState) { - trace_fmt!("zstd_dec: decode_frame_header: DECODING FRAME HEADER"); - trace_fmt!("zstd_dec: decode_frame_header: state: {:#x}", state); - let frame_header_result = frame_header::parse_frame_header(state.buffer); - trace_fmt!("zstd_dec: decode_frame_header: frame_header_result: {:#x}", frame_header_result); - let new_state = match frame_header_result.status { - frame_header::FrameHeaderStatus::OK => ZstdDecoderState { - status: ZstdDecoderStatus::DECODE_BLOCK_HEADER, - buffer: frame_header_result.buffer, - frame_header: frame_header_result.header, - ..state - }, - frame_header::FrameHeaderStatus::CORRUPTED => ZstdDecoderState { - status: ZstdDecoderStatus::ERROR, - ..ZERO_DECODER_STATE - }, - frame_header::FrameHeaderStatus::NO_ENOUGH_DATA => state, - frame_header::FrameHeaderStatus::UNSUPPORTED_WINDOW_SIZE => ZstdDecoderState { - status: ZstdDecoderStatus::ERROR, - ..ZERO_DECODER_STATE - }, - _ => state, - }; - trace_fmt!("zstd_dec: decode_frame_header: new_state: {:#x}", new_state); - - (false, zero!(), new_state) +fn csr(c: Csr) -> uN[LOG2_REGS_N] { + c as uN[LOG2_REGS_N] } -fn decode_block_header(state: ZstdDecoderState) -> (bool, BlockDataPacket, ZstdDecoderState) { - trace_fmt!("zstd_dec: decode_block_header: DECODING BLOCK HEADER"); - trace_fmt!("zstd_dec: decode_block_header: state: {:#x}", state); - let block_header_result = block_header::parse_block_header(state.buffer); - trace_fmt!("zstd_dec: decode_block_header: block_header_result: {:#x}", block_header_result); - let new_state = match block_header_result.status { - block_header::BlockHeaderStatus::OK => { - trace_fmt!("zstd_dec: BlockHeader: {:#x}", block_header_result.header); - match block_header_result.header.btype { - common::BlockType::RAW => ZstdDecoderState { - status: ZstdDecoderStatus::FEED_BLOCK_DECODER, - buffer: state.buffer, - block_size_bytes: block_header_result.header.size as BlockSize + BlockSize:3, - last: block_header_result.header.last, - bytes_sent: BlockSize:0, - ..state - }, - common::BlockType::RLE => ZstdDecoderState { - status: ZstdDecoderStatus::FEED_BLOCK_DECODER, - buffer: state.buffer, - block_size_bytes: BlockSize:4, - last: block_header_result.header.last, - bytes_sent: BlockSize:0, - ..state - }, - common::BlockType::COMPRESSED => ZstdDecoderState { - status: ZstdDecoderStatus::FEED_BLOCK_DECODER, - buffer: state.buffer, - block_size_bytes: block_header_result.header.size as BlockSize + BlockSize:3, - last: block_header_result.header.last, - bytes_sent: BlockSize:0, - ..state - }, - _ => { - fail!("impossible_case", state) - } - } - }, - block_header::BlockHeaderStatus::CORRUPTED => ZstdDecoderState { - status: ZstdDecoderStatus::ERROR, - ..ZERO_DECODER_STATE - }, - block_header::BlockHeaderStatus::NO_ENOUGH_DATA => state, - _ => state, - }; - trace_fmt!("zstd_dec: decode_block_header: new_state: {:#x}", new_state); - - (false, zero!(), new_state) +struct ZstdDecoderInternalState { + fsm: ZstdDecoderInternalFsm, + + // Reading CSRs + conf_cnt: uN[LOG2_REGS_N], + conf_send: bool, + input_buffer: uN[AXI_ADDR_W], + input_buffer_valid: bool, + output_buffer: uN[AXI_ADDR_W], + output_buffer_valid: bool, + + // Writing to CSRs + csr_wr_req: csr_config::CsrWrReq, + csr_wr_req_valid: bool, + + // BH address + bh_addr: uN[AXI_ADDR_W], + + // Block + block_addr: uN[AXI_ADDR_W], + block_length: uN[AXI_ADDR_W], + block_last: bool, + block_id: u32, + block_rle_symbol: u8, + + // Req + req_sent: bool, } -fn feed_block_decoder(state: ZstdDecoderState) -> (bool, BlockDataPacket, ZstdDecoderState) { - trace_fmt!("zstd_dec: feed_block_decoder: FEEDING BLOCK DECODER"); - trace_fmt!("zstd_dec: feed_block_decoder: state: {:#x}", state); - let remaining_bytes_to_send = state.block_size_bytes - state.bytes_sent; - trace_fmt!("zstd_dec: feed_block_decoder: remaining_bytes_to_send: {}", remaining_bytes_to_send); - let buffer_length_bytes = state.buffer.length >> 3; - trace_fmt!("zstd_dec: feed_block_decoder: buffer_length_bytes: {}", buffer_length_bytes); - let data_width_bytes = (DATA_WIDTH >> 3) as BlockSize; - trace_fmt!("zstd_dec: feed_block_decoder: data_width_bytes: {}", data_width_bytes); - let remaining_bytes_to_send_now = std::min(remaining_bytes_to_send, data_width_bytes); - trace_fmt!("zstd_dec: feed_block_decoder: remaining_bytes_to_send_now: {}", remaining_bytes_to_send_now); - if (buffer_length_bytes >= remaining_bytes_to_send_now as u32) { - let remaining_bits_to_send_now = (remaining_bytes_to_send_now as u32) << 3; - trace_fmt!("zstd_dec: feed_block_decoder: remaining_bits_to_send_now: {}", remaining_bits_to_send_now); - let last_packet = (remaining_bytes_to_send == remaining_bytes_to_send_now); - trace_fmt!("zstd_dec: feed_block_decoder: last_packet: {}", last_packet); - let (buffer_result, data_to_send) = buff::buffer_pop_checked(state.buffer, remaining_bits_to_send_now); - match buffer_result.status { - buff::BufferStatus::OK => { - let decoder_channel_data = BlockDataPacket { - last: last_packet, - last_block: state.last, - id: u32:0, - data: data_to_send[0: DATA_WIDTH as s32], - length: remaining_bits_to_send_now, +proc ZstdDecoderInternal< + AXI_DATA_W: u32, AXI_ADDR_W: u32, REGS_N: u32, + LOG2_REGS_N:u32 = {std::clog2(REGS_N)}, + HB_RAM_N:u32 = {u32:8}, +> { + + type State = ZstdDecoderInternalState; + type Fsm = ZstdDecoderInternalFsm; + type Reg = uN[LOG2_REGS_N]; + type Data = uN[AXI_DATA_W]; + type Addr = uN[AXI_ADDR_W]; + + type CsrRdReq = csr_config::CsrRdReq; + type CsrRdResp = csr_config::CsrRdResp; + type CsrWrReq = csr_config::CsrWrReq; + type CsrWrResp = csr_config::CsrWrResp; + type CsrChange = csr_config::CsrChange; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + type FrameHeaderDecoderStatus = frame_header_dec::FrameHeaderDecoderStatus; + type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; + type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; + + type BlockHeaderDecoderStatus = block_header_dec::BlockHeaderDecoderStatus; + type BlockHeaderDecoderReq = block_header_dec::BlockHeaderDecoderReq; + type BlockHeaderDecoderResp = block_header_dec::BlockHeaderDecoderResp; + + type RawBlockDecoderStatus = raw_block_dec::RawBlockDecoderStatus; + type RawBlockDecoderReq = raw_block_dec::RawBlockDecoderReq; + type RawBlockDecoderResp = raw_block_dec::RawBlockDecoderResp; + + type RleBlockDecoderStatus = rle_block_dec::RleBlockDecoderStatus; + type RleBlockDecoderReq = rle_block_dec::RleBlockDecoderReq; + type RleBlockDecoderResp = rle_block_dec::RleBlockDecoderResp; + + // CsrConfig + csr_rd_req_s: chan out; + csr_rd_resp_r: chan in; + csr_wr_req_s: chan out; + csr_wr_resp_r: chan in; + csr_change_r: chan in; + + // MemReader + FameHeaderDecoder + fh_req_s: chan out; + fh_resp_r: chan in; + + // MemReader + BlockHeaderDecoder + bh_req_s: chan out; + bh_resp_r: chan in; + + // MemReader + RawBlockDecoder + raw_req_s: chan out; + raw_resp_r: chan in; + + // MemReader + RleBlockDecoder + rle_req_s: chan out; + rle_resp_r: chan in; + + notify_s: chan<()> out; + + init { + zero!() + } + + config( + csr_rd_req_s: chan out, + csr_rd_resp_r: chan in, + csr_wr_req_s: chan out, + csr_wr_resp_r: chan in, + csr_change_r: chan in, + + // MemReader + FameHeaderDecoder + fh_req_s: chan out, + fh_resp_r: chan in, + + // MemReader + BlockHeaderDecoder + bh_req_s: chan out, + bh_resp_r: chan in, + + // MemReader + RawBlockDecoder + raw_req_s: chan out, + raw_resp_r: chan in, + + // MemReader + RleBlockDecoder + rle_req_s: chan out, + rle_resp_r: chan in, + + notify_s: chan<()> out, + ) { + ( + csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, + fh_req_s, fh_resp_r, + bh_req_s, bh_resp_r, + raw_req_s, raw_resp_r, + rle_req_s, rle_resp_r, + notify_s, + ) + } + + next (state: State) { + let tok0 = join(); + + const CSR_REQS = CsrRdReq[2]:[ + CsrRdReq {csr: csr(Csr::INPUT_BUFFER)}, + CsrRdReq {csr: csr(Csr::OUTPUT_BUFFER)} + ]; + + const CSR_REQS_MAX = checked_cast(array_size(CSR_REQS) - u32:1); + + let (tok1_0, csr_change, csr_change_valid) = recv_non_blocking(tok0, csr_change_r, zero!()); + let is_start = (csr_change_valid && (csr_change.csr == csr(Csr::START))); + + let do_send_csr_req = (state.fsm == Fsm::READ_CONFIG) && (!state.conf_send); + let csr_req = CSR_REQS[state.conf_cnt]; + let tok1_1 = send_if(tok0, csr_rd_req_s, do_send_csr_req, csr_req); + if do_send_csr_req { + trace_fmt!("[READ_CONFIG] Sending read request {:#x}", csr_req); + } else {}; + + let do_recv_csr_resp = (state.fsm == Fsm::READ_CONFIG); + let (tok1_2, csr_data, csr_data_valid) = recv_if_non_blocking(tok0, csr_rd_resp_r, do_recv_csr_resp, zero!()); + if csr_data_valid { + trace_fmt!("[READ_CONFIG] Received CSR data: {:#x}", csr_data); + } else {}; + + let do_send_fh_req = (state.fsm == Fsm::DECODE_FRAME_HEADER) && !state.req_sent; + let fh_req = FrameHeaderDecoderReq { addr: state.input_buffer }; + let tok1_3 = send_if(tok0, fh_req_s, do_send_fh_req, fh_req); + if do_send_fh_req { + trace_fmt!("[DECODE_FRAME_HEADER] Sending FH request {:#x}", fh_req); + } else {}; + + let do_recv_fh_resp = (state.fsm == Fsm::DECODE_FRAME_HEADER); + let (tok1_4, fh_resp, fh_resp_valid) = recv_if_non_blocking(tok0, fh_resp_r, do_recv_fh_resp, zero!()); + if fh_resp_valid { + trace_fmt!("[DECODE_FRAME_HEADER]: Received FH {:#x}", fh_resp); + } else {}; + + let do_send_notify = (state.fsm == Fsm::ERROR || state.fsm == Fsm::FINISH); + let tok = send_if(tok0, notify_s, do_send_notify, ()); + if do_send_notify { + trace_fmt!("[[NOTIFY]]"); + } else {}; + + let tok1_5 = send_if(tok0, csr_wr_req_s, state.csr_wr_req_valid, state.csr_wr_req); + let (tok, _, _) = recv_non_blocking(tok0, csr_wr_resp_r, zero!()); + if state.csr_wr_req_valid { + trace_fmt!("[[CSR_WR_REQ]] Request: {:#x}", state.csr_wr_req); + } else {}; + + let do_send_bh_req = (state.fsm == Fsm::DECODE_BLOCK_HEADER) && !state.req_sent; + let bh_req = BlockHeaderDecoderReq { addr: state.bh_addr }; + let tok1_6 = send_if(tok0, bh_req_s, do_send_bh_req, bh_req); + if do_send_bh_req { + trace_fmt!("[DECODE_BLOCK_HEADER]: Sending BH request: {:#x}", bh_req); + } else {}; + + let do_recv_bh_resp = (state.fsm == Fsm::DECODE_BLOCK_HEADER); + let (tok1_4, bh_resp, bh_resp_valid) = recv_if_non_blocking(tok0, bh_resp_r, do_recv_bh_resp, zero!()); + if bh_resp_valid { + trace_fmt!("[DECODE_BLOCK_HEADER]: Received BH {:#x}", bh_resp); + } else {}; + + let do_send_raw_req = (state.fsm == Fsm::DECODE_RAW_BLOCK) && !state.req_sent; + let raw_req = RawBlockDecoderReq { + id: state.block_id, + last_block: state.block_last, + addr: state.block_addr, + length: state.block_length, + }; + let tok1_6 = send_if(tok0, raw_req_s, do_send_raw_req, raw_req); + if do_send_raw_req { + trace_fmt!("[DECODE_RAW_BLOCK]: Sending RAW request: {:#x}", raw_req); + } else {}; + + let do_recv_raw_resp = (state.fsm == Fsm::DECODE_RAW_BLOCK); + let (tok1_7, raw_resp, raw_resp_valid) = recv_if_non_blocking(tok0, raw_resp_r, do_recv_raw_resp, zero!()); + if raw_resp_valid { + trace_fmt!("[DECODE_RAW_BLOCK]: Received RAW {:#x}", raw_resp); + } else {}; + + let do_send_rle_req = (state.fsm == Fsm::DECODE_RLE_BLOCK) && !state.req_sent; + let rle_req = RleBlockDecoderReq { + id: state.block_id, + symbol: state.block_rle_symbol, + length: checked_cast(state.block_length), + last_block: state.block_last, + }; + let tok1_7 = send_if(tok0, rle_req_s, do_send_rle_req, rle_req); + if do_send_rle_req { + trace_fmt!("[DECODE_RLE_BLOCK]: Sending RLE request: {:#x}", rle_req); + } else {}; + + let do_recv_rle_resp = (state.fsm == Fsm::DECODE_RLE_BLOCK); + let (tok1_8, rle_resp, rle_resp_valid) = recv_if_non_blocking(tok0, rle_resp_r, do_recv_rle_resp, zero!()); + if raw_resp_valid { + trace_fmt!("[DECODE_RLE_BLOCK]: Received RAW {:#x}", raw_resp); + } else {}; + + let new_state = match (state.fsm) { + Fsm::IDLE => { + trace_fmt!("[IDLE]"); + if is_start { + State { fsm: Fsm::READ_CONFIG, conf_cnt: CSR_REQS_MAX, ..zero!() } + } else { zero!() } + }, + + Fsm::READ_CONFIG => { + trace_fmt!("[READ_CONFIG]"); + let is_input_buffer_csr = (csr_data.csr == csr(Csr::INPUT_BUFFER)); + let input_buffer = if csr_data_valid && is_input_buffer_csr { checked_cast(csr_data.value) } else { state.input_buffer }; + let input_buffer_valid = if csr_data_valid && is_input_buffer_csr { true } else { state.input_buffer_valid }; + + let is_output_buffer_csr = (csr_data.csr == csr(Csr::OUTPUT_BUFFER)); + let output_buffer = if (csr_data_valid && is_output_buffer_csr) { checked_cast(csr_data.value) } else { state.output_buffer }; + let output_buffer_valid = if (csr_data_valid && is_output_buffer_csr) { true } else { state.output_buffer_valid }; + + let all_collected = input_buffer_valid & output_buffer_valid; + let fsm = if all_collected { Fsm::DECODE_FRAME_HEADER } else { Fsm::READ_CONFIG }; + + let conf_send = (state.conf_cnt == Reg:0); + let conf_cnt = if conf_send { Reg:0 } else {state.conf_cnt - Reg:1}; + + State { + fsm, conf_cnt, conf_send, input_buffer, input_buffer_valid, output_buffer, output_buffer_valid, + ..zero!() + } + }, + + Fsm::DECODE_FRAME_HEADER => { + trace_fmt!("[DECODE_FRAME_HEADER]"); + let error = (fh_resp.status != FrameHeaderDecoderStatus::OKAY); + + let csr_wr_req_valid = (fh_resp_valid && error); + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(fh_resp.status), + }; + + let fsm = match (fh_resp_valid, error) { + ( true, false) => Fsm::DECODE_BLOCK_HEADER, + ( true, true) => Fsm::ERROR, + ( _, _) => Fsm::DECODE_FRAME_HEADER, + }; + + let bh_addr = state.input_buffer + fh_resp.length as Addr; + let req_sent = if !fh_resp_valid && !error { true } else { false }; + State {fsm, csr_wr_req, csr_wr_req_valid, bh_addr, req_sent, ..state } + }, + + Fsm::DECODE_BLOCK_HEADER => { + trace_fmt!("[DECODE_BLOCK_HEADER]"); + let error = (bh_resp.status != BlockHeaderDecoderStatus::OKAY); + + let csr_wr_req_valid = (bh_resp_valid && error); + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(bh_resp.status), }; - let new_fsm_status = if (last_packet) { - if (state.last) { - if (state.frame_header.content_checksum_flag) { - ZstdDecoderStatus::DECODE_CHECKSUM - } else { - ZstdDecoderStatus::DECODE_MAGIC_NUMBER - } + + let fsm = match (bh_resp_valid, error, bh_resp.header.btype) { + ( true, false, BlockType::RAW ) => Fsm::DECODE_RAW_BLOCK, + ( true, false, BlockType::RLE ) => Fsm::DECODE_RLE_BLOCK, + ( true, false, BlockType::COMPRESSED) => Fsm::ERROR, + ( true, true, _) => Fsm::ERROR, + ( _, _, _) => Fsm::DECODE_BLOCK_HEADER, + }; + + let (block_addr, block_length, block_last, block_rle_symbol, bh_addr) = if bh_resp_valid { + let block_addr = state.bh_addr + Addr:3; + let block_length = checked_cast(bh_resp.header.size); + let block_rle_symbol = bh_resp.rle_symbol; + let bh_addr = if bh_resp.header.btype == BlockType::RLE { + block_addr + Addr:1 } else { - ZstdDecoderStatus::DECODE_BLOCK_HEADER - } + block_addr + block_length + }; + + trace_fmt!("bh_addr: {:#x}", bh_addr); + + (block_addr, block_length, bh_resp.header.last, block_rle_symbol, bh_addr) } else { - ZstdDecoderStatus::FEED_BLOCK_DECODER + (state.block_addr, state.block_length, state.block_last, state.block_rle_symbol, state.bh_addr) }; - trace_fmt!("zstd_dec: feed_block_decoder: packet to decode: {:#x}", decoder_channel_data); - let new_state = (true, decoder_channel_data, ZstdDecoderState { - bytes_sent: state.bytes_sent + remaining_bytes_to_send_now, - buffer: buffer_result.buffer, - status: new_fsm_status, + + let req_sent = if !bh_resp_valid && !error { true } else { false }; + State { + fsm, bh_addr, req_sent, + block_addr, block_length, block_last, block_rle_symbol, + csr_wr_req, csr_wr_req_valid, ..state - }); - trace_fmt!("zstd_dec: feed_block_decoder: new_state: {:#x}", new_state); - new_state + } }, - _ => { - fail!("should_not_happen_1", (false, zero!(), state)) - } - } - } else { - trace_fmt!("zstd_dec: feed_block_decoder: Not enough data for intermediate FEED_BLOCK_DECODER block dump"); - (false, zero!(), state) + + Fsm::DECODE_RAW_BLOCK => { + trace_fmt!("[DECODE_RAW_BLOCK]"); + + let error = (raw_resp.status != RawBlockDecoderStatus::OKAY); + + let csr_wr_req_valid = (raw_resp_valid && error); + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(raw_resp.status), + }; + + let fsm = match (raw_resp_valid, error, state.block_last) { + (true, false, false) => Fsm::DECODE_BLOCK_HEADER, + (true, false, true) => Fsm::DECODE_CHECKSUM, + (true, true, _) => Fsm::ERROR, + ( _, _, _) => Fsm::DECODE_RAW_BLOCK, + }; + + let req_sent = if !raw_resp_valid && !error { true } else { false }; + let block_id = if raw_resp_valid { state.block_id + u32:1} else {state.block_id }; + + let state = State {fsm, block_id, csr_wr_req, csr_wr_req_valid, req_sent, ..state}; + if fsm == Fsm::DECODE_BLOCK_HEADER { + trace_fmt!("Going to decode block header: {:#x}", state); + } else {}; + + state + }, + + Fsm::DECODE_RLE_BLOCK => { + trace_fmt!("[DECODE_RLE_BLOCK]"); + let error = (rle_resp.status != RleBlockDecoderStatus::OKAY); + + let csr_wr_req_valid = (rle_resp_valid && error); + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(rle_resp.status), + }; + + let fsm = match (rle_resp_valid, error, state.block_last) { + (true, false, false) => Fsm::DECODE_BLOCK_HEADER, + (true, false, true) => Fsm::DECODE_CHECKSUM, + (true, true, _) => Fsm::ERROR, + ( _, _, _) => Fsm::DECODE_RLE_BLOCK, + }; + + let req_sent = if !rle_resp_valid && !error { true } else { false }; + let block_id = if rle_resp_valid { state.block_id + u32:1} else {state.block_id }; + + let state = State {fsm, block_id, csr_wr_req, csr_wr_req_valid, req_sent, ..state}; + if fsm == Fsm::DECODE_BLOCK_HEADER { + trace_fmt!("Going to decode block header: {:#x}", state); + } else {}; + + state + }, + + Fsm::DECODE_CHECKSUM => { + trace_fmt!("[DECODE_CHECKSUM]"); + State {fsm: Fsm::FINISH, ..zero!() } + + }, + + Fsm::ERROR => { + trace_fmt!("[ERROR]"); + State { fsm: Fsm::IDLE, ..zero!() } + }, + + Fsm::FINISH => { + trace_fmt!("[FINISH]"); + let csr_wr_req_valid = true; + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(fh_resp.status), + }; + + State { fsm: Fsm::IDLE, csr_wr_req, csr_wr_req_valid, ..zero!() } + }, + + _ => zero!(), + }; + + new_state } } -fn decode_checksum(state: ZstdDecoderState) -> (bool, BlockDataPacket, ZstdDecoderState) { - trace_fmt!("zstd_dec: decode_checksum: DECODE CHECKSUM"); - trace_fmt!("zstd_dec: decode_checksum: state: {:#x}", state); - // Pop fixed checksum size of 4 bytes - let (buffer_result, _) = buff::buffer_pop_checked(state.buffer, u32:32); +const TEST_AXI_DATA_W = u32:64; +const TEST_AXI_ADDR_W = u32:32; +const TEST_AXI_ID_W = u32:8; +const TEST_AXI_DEST_W = u32:8; +const TEST_REGS_N = u32:5; +const TEST_WINDOW_LOG_MAX = u32:30; + +const TEST_HB_ADDR_W = sequence_executor::ZSTD_RAM_ADDR_WIDTH; +const TEST_HB_DATA_W = sequence_executor::RAM_DATA_WIDTH; +const TEST_HB_NUM_PARTITIONS = sequence_executor::RAM_NUM_PARTITIONS; +const TEST_HB_SIZE_KB = sequence_executor::ZSTD_HISTORY_BUFFER_SIZE_KB; + +const TEST_LOG2_REGS_N = std::clog2(TEST_REGS_N); +const TEST_AXI_DATA_W_DIV8 = TEST_AXI_DATA_W / u32:8; +const TEST_HB_RAM_N = u32:8; + +const TEST_HB_SIZE = sequence_executor::ram_size(TEST_HB_SIZE_KB); +const TEST_HB_RAM_WORD_PARTITION_SIZE = sequence_executor::RAM_WORD_PARTITION_SIZE; +const TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = sequence_executor::TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR; +const TEST_HB_RAM_INITIALIZED = sequence_executor::TEST_RAM_INITIALIZED; +const TEST_HB_RAM_ASSERT_VALID_READ:bool = {false}; + + +#[test_proc] +proc ZstdDecoderInternalTest { + + type BlockType = common::BlockType; + type BlockSize = common::BlockSize; + type BlockHeader = block_header::BlockHeader; + type BlockHeaderDecoderStatus = block_header_dec::BlockHeaderDecoderStatus; + + type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; + type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; + type FrameHeaderDecoderStatus = frame_header_dec::FrameHeaderDecoderStatus; + type FrameContentSize = frame_header_dec::FrameContentSize; + type FrameHeader = frame_header_dec::FrameHeader; + type WindowSize = frame_header_dec::WindowSize; + type DictionaryId = frame_header_dec::DictionaryId; + + type CsrRdReq = csr_config::CsrRdReq; + type CsrRdResp = csr_config::CsrRdResp; + type CsrWrReq = csr_config::CsrWrReq; + type CsrWrResp = csr_config::CsrWrResp; + type CsrChange = csr_config::CsrChange; + + type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; + type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; + + type BlockHeaderDecoderReq = block_header_dec::BlockHeaderDecoderReq; + type BlockHeaderDecoderResp = block_header_dec::BlockHeaderDecoderResp; + + type RawBlockDecoderReq = raw_block_dec::RawBlockDecoderReq; + type RawBlockDecoderResp = raw_block_dec::RawBlockDecoderResp; + type RawBlockDecoderStatus = raw_block_dec::RawBlockDecoderStatus; + + type RleBlockDecoderReq = rle_block_dec::RleBlockDecoderReq; + type RleBlockDecoderResp = rle_block_dec::RleBlockDecoderResp; + type RleBlockDecoderStatus = rle_block_dec::RleBlockDecoderStatus; + + terminator: chan out; + + csr_rd_req_r: chan in; + csr_rd_resp_s: chan out; + csr_wr_req_r: chan in; + csr_wr_resp_s: chan out; + csr_change_s: chan out; + + fh_req_r: chan in; + fh_resp_s: chan out; + + bh_req_r: chan in; + bh_resp_s: chan out; + + raw_req_r: chan in; + raw_resp_s: chan out; + + rle_req_r: chan in; + rle_resp_s: chan out; + + notify_r: chan<()> in; + + init {} + + config(terminator: chan out) { + let (csr_rd_req_s, csr_rd_req_r) = chan("csr_rd_req"); + let (csr_rd_resp_s, csr_rd_resp_r) = chan("csr_rd_resp"); + let (csr_wr_req_s, csr_wr_req_r) = chan("csr_wr_req"); + let (csr_wr_resp_s, csr_wr_resp_r) = chan("csr_wr_resp"); + let (csr_change_s, csr_change_r) = chan("csr_change"); + + let (fh_req_s, fh_req_r) = chan("fh_req"); + let (fh_resp_s, fh_resp_r) = chan("fh_resp"); + + let (bh_req_s, bh_req_r) = chan("bh_req"); + let (bh_resp_s, bh_resp_r) = chan("bh_resp"); + + let (raw_req_s, raw_req_r) = chan("raw_req"); + let (raw_resp_s, raw_resp_r) = chan("raw_resp"); + + let (rle_req_s, rle_req_r) = chan("rle_req"); + let (rle_resp_s, rle_resp_r) = chan("rle_resp"); + + let (notify_s, notify_r) = chan<()>("notify"); + + spawn ZstdDecoderInternal( + csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, + fh_req_s, fh_resp_r, + bh_req_s, bh_resp_r, + raw_req_s, raw_resp_r, + rle_req_s, rle_resp_r, + notify_s, + ); + + ( + terminator, + csr_rd_req_r, csr_rd_resp_s, csr_wr_req_r, csr_wr_resp_s, csr_change_s, + fh_req_r, fh_resp_s, + bh_req_r, bh_resp_s, + raw_req_r, raw_resp_s, + rle_req_r, rle_resp_s, + notify_r, + ) + } + + next (state: ()) { + type Addr = uN[TEST_AXI_ADDR_W]; + type Length = uN[TEST_AXI_ADDR_W]; + + let tok = join(); + + // Error in frame header + + let tok = send(tok, csr_change_s, CsrChange { csr: csr(Csr::START)}); + let (tok, csr_data) = recv(tok, csr_rd_req_r); + assert_eq(csr_data, CsrRdReq {csr: csr(Csr::OUTPUT_BUFFER)}); + let (tok, csr_data) = recv(tok, csr_rd_req_r); + assert_eq(csr_data, CsrRdReq {csr: csr(Csr::INPUT_BUFFER)}); - let new_state = ZstdDecoderState { - status: ZstdDecoderStatus::DECODE_MAGIC_NUMBER, - buffer: buffer_result.buffer, - ..state - }; - trace_fmt!("zstd_dec: decode_checksum: new_state: {:#x}", new_state); + send(tok, csr_rd_resp_s, CsrRdResp { + csr: csr(Csr::INPUT_BUFFER), + value: uN[TEST_AXI_DATA_W]:0x1000 + }); + send(tok, csr_rd_resp_s, CsrRdResp { + csr: csr(Csr::OUTPUT_BUFFER), + value: uN[TEST_AXI_DATA_W]:0x2000 + }); + let (tok, fh_req) = recv(tok, fh_req_r); + assert_eq(fh_req, FrameHeaderDecoderReq { addr: Addr:0x1000 }); - (false, zero!(), new_state) + let tok = send(tok, fh_resp_s, FrameHeaderDecoderResp { + status: FrameHeaderDecoderStatus::CORRUPTED, + header: FrameHeader { + window_size: WindowSize:100, + frame_content_size: FrameContentSize:200, + dictionary_id: DictionaryId:123, + content_checksum_flag: u1:1, + }, + length: u5:3, + }); + + + let (tok, ()) = recv(tok, notify_r); + + // Correct case + let tok = send(tok, csr_change_s, CsrChange { csr: csr(Csr::START)}); + let (tok, csr_data) = recv(tok, csr_rd_req_r); + assert_eq(csr_data, CsrRdReq {csr: csr(Csr::OUTPUT_BUFFER)}); + let (tok, csr_data) = recv(tok, csr_rd_req_r); + assert_eq(csr_data, CsrRdReq {csr: csr(Csr::INPUT_BUFFER)}); + + send(tok, csr_rd_resp_s, CsrRdResp { + csr: csr(Csr::INPUT_BUFFER), + value: uN[TEST_AXI_DATA_W]:0x1000 + }); + send(tok, csr_rd_resp_s, CsrRdResp { + csr: csr(Csr::OUTPUT_BUFFER), + value: uN[TEST_AXI_DATA_W]:0x2000 + }); + let (tok, fh_req) = recv(tok, fh_req_r); + assert_eq(fh_req, FrameHeaderDecoderReq { addr: Addr:0x1000 }); + + let tok = send(tok, fh_resp_s, FrameHeaderDecoderResp { + status: FrameHeaderDecoderStatus::OKAY, + header: FrameHeader { + window_size: WindowSize:100, + frame_content_size: FrameContentSize:200, + dictionary_id: DictionaryId:123, + content_checksum_flag: u1:1, + }, + length: u5:3, + }); + + let (tok, bh_req) = recv(tok, bh_req_r); + assert_eq(bh_req, BlockHeaderDecoderReq { + addr: Addr:0x1003, + }); + + let tok = send(tok, bh_resp_s, BlockHeaderDecoderResp { + status: BlockHeaderDecoderStatus::OKAY, + header: BlockHeader { + last: false, + btype: BlockType::RAW, + size: BlockSize:0x1000, + }, + rle_symbol: u8:0, + }); + + let (tok, raw_req) = recv(tok, raw_req_r); + assert_eq(raw_req, RawBlockDecoderReq { + last_block: false, + id: u32:0, + addr: Addr:0x1006, + length: Length:0x1000 + }); + + let tok = send(tok, raw_resp_s, RawBlockDecoderResp { + status: RawBlockDecoderStatus::OKAY, + }); + + let (tok, bh_req) = recv(tok, bh_req_r); + assert_eq(bh_req, BlockHeaderDecoderReq { + addr: Addr:0x2006 + }); + let tok = send(tok, bh_resp_s, BlockHeaderDecoderResp { + status: BlockHeaderDecoderStatus::OKAY, + header: BlockHeader { + last: false, + btype: BlockType::RLE, + size: BlockSize:0x1000, + }, + rle_symbol: u8:123, + }); + + let (tok, rle_req) = recv(tok, rle_req_r); + assert_eq(rle_req, RleBlockDecoderReq { + id: u32:1, + symbol: u8:123, + last_block: false, + length: checked_cast(Length:0x1000), + }); + let tok = send(tok, rle_resp_s, RleBlockDecoderResp { + status: RleBlockDecoderStatus::OKAY, + }); + + let (tok, bh_req) = recv(tok, bh_req_r); + assert_eq(bh_req, BlockHeaderDecoderReq { + addr: Addr:0x200A, + }); + + let tok = send(tok, bh_resp_s, BlockHeaderDecoderResp { + status: BlockHeaderDecoderStatus::OKAY, + header: BlockHeader { + last: true, + btype: BlockType::RAW, + size: BlockSize:0x1000, + }, + rle_symbol: u8:0, + }); + + let (tok, raw_req) = recv(tok, raw_req_r); + assert_eq(raw_req, RawBlockDecoderReq { + last_block: true, + id: u32:2, + addr: Addr:0x200D, + length: Length:0x1000 + }); + + let tok = send(tok, raw_resp_s, RawBlockDecoderResp { + status: RawBlockDecoderStatus::OKAY, + }); + + let (tok, ()) = recv(tok, notify_r); + + send(tok, terminator, true); + } } -pub proc ZstdDecoder { - input_r: chan in; - block_dec_in_s: chan out; - output_s: chan out; - looped_channel_r: chan in; - looped_channel_s: chan out; - ram_rd_req_0_s: chan> out; - ram_rd_req_1_s: chan> out; - ram_rd_req_2_s: chan> out; - ram_rd_req_3_s: chan> out; - ram_rd_req_4_s: chan> out; - ram_rd_req_5_s: chan> out; - ram_rd_req_6_s: chan> out; - ram_rd_req_7_s: chan> out; - ram_rd_resp_0_r: chan> in; - ram_rd_resp_1_r: chan> in; - ram_rd_resp_2_r: chan> in; - ram_rd_resp_3_r: chan> in; - ram_rd_resp_4_r: chan> in; - ram_rd_resp_5_r: chan> in; - ram_rd_resp_6_r: chan> in; - ram_rd_resp_7_r: chan> in; - ram_wr_req_0_s: chan> out; - ram_wr_req_1_s: chan> out; - ram_wr_req_2_s: chan> out; - ram_wr_req_3_s: chan> out; - ram_wr_req_4_s: chan> out; - ram_wr_req_5_s: chan> out; - ram_wr_req_6_s: chan> out; - ram_wr_req_7_s: chan> out; - ram_wr_resp_0_r: chan in; - ram_wr_resp_1_r: chan in; - ram_wr_resp_2_r: chan in; - ram_wr_resp_3_r: chan in; - ram_wr_resp_4_r: chan in; - ram_wr_resp_5_r: chan in; - ram_wr_resp_6_r: chan in; - ram_wr_resp_7_r: chan in; - - init {(ZERO_DECODER_STATE)} - - config ( - input_r: chan in, + +proc ZstdDecoder< + // AXI parameters + AXI_DATA_W: u32, AXI_ADDR_W: u32, AXI_ID_W: u32, AXI_DEST_W: u32, + // decoder parameters + REGS_N: u32, WINDOW_LOG_MAX: u32, + HB_ADDR_W: u32, HB_DATA_W: u32, HB_NUM_PARTITIONS: u32, HB_SIZE_KB: u32, + // calculated parameters + AXI_DATA_W_DIV8: u32 = {AXI_DATA_W / u32:8}, + LOG2_REGS_N: u32 = {std::clog2(REGS_N)}, + HB_RAM_N: u32 = {u32:8}, +> { + type CsrAxiAr = axi::AxiAr; + type CsrAxiR = axi::AxiR; + type CsrAxiAw = axi::AxiAw; + type CsrAxiW = axi::AxiW; + type CsrAxiB = axi::AxiB; + + type CsrRdReq = csr_config::CsrRdReq; + type CsrRdResp = csr_config::CsrRdResp; + type CsrWrReq = csr_config::CsrWrReq; + type CsrWrResp = csr_config::CsrWrResp; + type CsrChange = csr_config::CsrChange; + + type MemAxiAr = axi::AxiAr; + type MemAxiR = axi::AxiR; + type MemAxiAw = axi::AxiAw; + type MemAxiW = axi::AxiW; + type MemAxiB = axi::AxiB; + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; + type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; + + type BlockHeaderDecoderReq = block_header_dec::BlockHeaderDecoderReq; + type BlockHeaderDecoderResp = block_header_dec::BlockHeaderDecoderResp; + + type RawBlockDecoderReq = raw_block_dec::RawBlockDecoderReq; + type RawBlockDecoderResp = raw_block_dec::RawBlockDecoderResp; + type ExtendedBlockDataPacket = common::ExtendedBlockDataPacket; + + type RleBlockDecoderReq = rle_block_dec::RleBlockDecoderReq; + type RleBlockDecoderResp = rle_block_dec::RleBlockDecoderResp; + + type SequenceExecutorPacket = common::SequenceExecutorPacket; + type ZstdDecodedPacket = common::ZstdDecodedPacket; + + type RamRdReq = ram::ReadReq; + type RamRdResp = ram::ReadResp; + type RamWrReq = ram::WriteReq; + type RamWrResp = ram::WriteResp; + + // Complex Block Decoder + cmp_output_s: chan out; + + init {} + + config( + // AXI Ctrl (subordinate) + csr_axi_aw_r: chan in, + csr_axi_w_r: chan in, + csr_axi_b_s: chan out, + csr_axi_ar_r: chan in, + csr_axi_r_s: chan out, + + // AXI Frame Header Decoder (manager) + fh_axi_ar_s: chan out, + fh_axi_r_r: chan in, + + //// AXI Block Header Decoder (manager) + bh_axi_ar_s: chan out, + bh_axi_r_r: chan in, + + //// AXI RAW Block Decoder (manager) + raw_axi_ar_s: chan out, + raw_axi_r_r: chan in, + + // History Buffer + ram_rd_req_0_s: chan out, + ram_rd_req_1_s: chan out, + ram_rd_req_2_s: chan out, + ram_rd_req_3_s: chan out, + ram_rd_req_4_s: chan out, + ram_rd_req_5_s: chan out, + ram_rd_req_6_s: chan out, + ram_rd_req_7_s: chan out, + ram_rd_resp_0_r: chan in, + ram_rd_resp_1_r: chan in, + ram_rd_resp_2_r: chan in, + ram_rd_resp_3_r: chan in, + ram_rd_resp_4_r: chan in, + ram_rd_resp_5_r: chan in, + ram_rd_resp_6_r: chan in, + ram_rd_resp_7_r: chan in, + ram_wr_req_0_s: chan out, + ram_wr_req_1_s: chan out, + ram_wr_req_2_s: chan out, + ram_wr_req_3_s: chan out, + ram_wr_req_4_s: chan out, + ram_wr_req_5_s: chan out, + ram_wr_req_6_s: chan out, + ram_wr_req_7_s: chan out, + ram_wr_resp_0_r: chan in, + ram_wr_resp_1_r: chan in, + ram_wr_resp_2_r: chan in, + ram_wr_resp_3_r: chan in, + ram_wr_resp_4_r: chan in, + ram_wr_resp_5_r: chan in, + ram_wr_resp_6_r: chan in, + ram_wr_resp_7_r: chan in, + + // Decoder output output_s: chan out, - looped_channel_r: chan in, - looped_channel_s: chan out, - ram_rd_req_0_s: chan> out, - ram_rd_req_1_s: chan> out, - ram_rd_req_2_s: chan> out, - ram_rd_req_3_s: chan> out, - ram_rd_req_4_s: chan> out, - ram_rd_req_5_s: chan> out, - ram_rd_req_6_s: chan> out, - ram_rd_req_7_s: chan> out, - ram_rd_resp_0_r: chan> in, - ram_rd_resp_1_r: chan> in, - ram_rd_resp_2_r: chan> in, - ram_rd_resp_3_r: chan> in, - ram_rd_resp_4_r: chan> in, - ram_rd_resp_5_r: chan> in, - ram_rd_resp_6_r: chan> in, - ram_rd_resp_7_r: chan> in, - ram_wr_req_0_s: chan> out, - ram_wr_req_1_s: chan> out, - ram_wr_req_2_s: chan> out, - ram_wr_req_3_s: chan> out, - ram_wr_req_4_s: chan> out, - ram_wr_req_5_s: chan> out, - ram_wr_req_6_s: chan> out, - ram_wr_req_7_s: chan> out, - ram_wr_resp_0_r: chan in, - ram_wr_resp_1_r: chan in, - ram_wr_resp_2_r: chan in, - ram_wr_resp_3_r: chan in, - ram_wr_resp_4_r: chan in, - ram_wr_resp_5_r: chan in, - ram_wr_resp_6_r: chan in, - ram_wr_resp_7_r: chan in, + notify_s: chan<()> out, ) { - let (block_dec_in_s, block_dec_in_r) = chan("block_dec_in"); - let (seq_exec_in_s, seq_exec_in_r) = chan("seq_exec_in"); - let (repacketizer_in_s, repacketizer_in_r) = chan("repacketizer_in"); + const CHANNEL_DEPTH = u32:1; + + // CSRs + + let (ext_csr_rd_req_s, ext_csr_rd_req_r) = chan("csr_rd_req"); + let (ext_csr_rd_resp_s, ext_csr_rd_resp_r) = chan("csr_rd_resp"); + let (ext_csr_wr_req_s, ext_csr_wr_req_r) = chan("csr_wr_req"); + let (ext_csr_wr_resp_s, ext_csr_wr_resp_r) = chan("csr_wr_resp"); + + let (csr_rd_req_s, csr_rd_req_r) = chan("csr_rd_req"); + let (csr_rd_resp_s, csr_rd_resp_r) = chan("csr_rd_resp"); + let (csr_wr_req_s, csr_wr_req_r) = chan("csr_wr_req"); + let (csr_wr_resp_s, csr_wr_resp_r) = chan("csr_wr_resp"); + + let (csr_change_s, csr_change_r) = chan("csr_change"); + + spawn axi_csr_accessor::AxiCsrAccessor( + csr_axi_aw_r, csr_axi_w_r, csr_axi_b_s, // csr write from AXI + csr_axi_ar_r, csr_axi_r_s, // csr read from AXI + ext_csr_rd_req_s, ext_csr_rd_resp_r, // csr read to CsrConfig + ext_csr_wr_req_s, ext_csr_wr_resp_r, // csr write to CsrConfig + ); + + spawn csr_config::CsrConfig( + ext_csr_rd_req_r, ext_csr_rd_resp_s, // csr read from AxiCsrAccessor + ext_csr_wr_req_r, ext_csr_wr_resp_s, // csr write from AxiCsrAccessor + csr_rd_req_r, csr_rd_resp_s, // csr read from design + csr_wr_req_r, csr_wr_resp_s, // csr write from design + csr_change_s, // notification about csr change + ); + + // Frame Header + + let (fh_mem_rd_req_s, fh_mem_rd_req_r) = chan("fh_mem_rd_req"); + let (fh_mem_rd_resp_s, fh_mem_rd_resp_r) = chan("fh_mem_rd_resp"); + + spawn mem_reader::MemReader( + fh_mem_rd_req_r, fh_mem_rd_resp_s, + fh_axi_ar_s, fh_axi_r_r, + ); + + let (fh_req_s, fh_req_r) = chan("fh_req"); + let (fh_resp_s, fh_resp_r) = chan("fh_resp"); + + spawn frame_header_dec::FrameHeaderDecoder( + fh_mem_rd_req_s, fh_mem_rd_resp_r, + fh_req_r, fh_resp_s, + ); + + // Block Header + + let (bh_mem_rd_req_s, bh_mem_rd_req_r) = chan("bh_mem_rd_req"); + let (bh_mem_rd_resp_s, bh_mem_rd_resp_r) = chan("bh_mem_rd_resp"); + + spawn mem_reader::MemReader( + bh_mem_rd_req_r, bh_mem_rd_resp_s, + bh_axi_ar_s, bh_axi_r_r, + ); + + let (bh_req_s, bh_req_r) = chan("bh_req"); + let (bh_resp_s, bh_resp_r) = chan("bh_resp"); + + spawn block_header_dec::BlockHeaderDecoder( + bh_req_r, bh_resp_s, + bh_mem_rd_req_s, bh_mem_rd_resp_r, + ); + + // Raw Block Decoder + + let (raw_mem_rd_req_s, raw_mem_rd_req_r) = chan("raw_mem_rd_req"); + let (raw_mem_rd_resp_s, raw_mem_rd_resp_r) = chan("raw_mem_rd_resp"); + + spawn mem_reader::MemReader( + raw_mem_rd_req_r, raw_mem_rd_resp_s, + raw_axi_ar_s, raw_axi_r_r, + ); + + let (raw_req_s, raw_req_r) = chan("raw_req"); + let (raw_resp_s, raw_resp_r) = chan("raw_resp"); + let (raw_output_s, raw_output_r) = chan("raw_output"); + + spawn raw_block_dec::RawBlockDecoder( + raw_req_r, raw_resp_s, raw_output_s, + raw_mem_rd_req_s, raw_mem_rd_resp_r, + ); + + // RLE Block Decoder + + let (rle_req_s, rle_req_r) = chan("rle_req"); + let (rle_resp_s, rle_resp_r) = chan("rle_resp"); + let (rle_output_s, rle_output_r) = chan("rle_output"); + + spawn rle_block_dec::RleBlockDecoder( + rle_req_r, rle_resp_s, rle_output_s + ); + + // Collecting Packets + + let (cmp_output_s, cmp_output_r) = chan("cmp_output"); + let (seq_exec_input_s, seq_exec_input_r) = chan("demux_output"); + + spawn dec_mux::DecoderMux( + raw_output_r, rle_output_r, cmp_output_r, + seq_exec_input_s, + ); - spawn block_dec::BlockDecoder(block_dec_in_r, seq_exec_in_s); + // Sequence Execution - spawn sequence_executor::SequenceExecutor( - seq_exec_in_r, repacketizer_in_s, - looped_channel_r, looped_channel_s, - ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, - ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, + let (seq_exec_looped_s, seq_exec_looped_r) = chan("seq_exec_looped"); + let (seq_exec_output_s, seq_exec_output_r) = chan("seq_exec_output"); + + spawn sequence_executor::SequenceExecutor( + seq_exec_input_r, seq_exec_output_s, + seq_exec_looped_r, seq_exec_looped_s, + ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, + ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, ram_rd_resp_0_r, ram_rd_resp_1_r, ram_rd_resp_2_r, ram_rd_resp_3_r, ram_rd_resp_4_r, ram_rd_resp_5_r, ram_rd_resp_6_r, ram_rd_resp_7_r, - ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, - ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, + ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, + ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, - ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r, + ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r ); - spawn repacketizer::Repacketizer(repacketizer_in_r, output_s); - - (input_r, block_dec_in_s, output_s, looped_channel_r, looped_channel_s, - ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, - ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, - ram_rd_resp_0_r, ram_rd_resp_1_r, ram_rd_resp_2_r, ram_rd_resp_3_r, - ram_rd_resp_4_r, ram_rd_resp_5_r, ram_rd_resp_6_r, ram_rd_resp_7_r, - ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, - ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, - ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, - ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r) + // Repacketizer + + spawn repacketizer::Repacketizer(seq_exec_output_r, output_s); + + // Zstd Decoder Control + + spawn ZstdDecoderInternal ( + csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, + fh_req_s, fh_resp_r, + bh_req_s, bh_resp_r, + raw_req_s, raw_resp_r, + rle_req_s, rle_resp_r, + notify_s, + ); + + (cmp_output_s,) } - next (state: ZstdDecoderState) { - let tok = join(); - trace_fmt!("zstd_dec: next(): state: {:#x}", state); - let can_fit = buff::buffer_can_fit(state.buffer, BlockData:0); - trace_fmt!("zstd_dec: next(): can_fit: {}", can_fit); - let (tok, data, recv_valid) = recv_if_non_blocking(tok, input_r, can_fit, BlockData:0); - let state = if (can_fit && recv_valid) { - let buffer = buff::buffer_append(state.buffer, data); - trace_fmt!("zstd_dec: next(): received more data: {:#x}", data); - ZstdDecoderState {buffer, ..state} - } else { - state - }; - trace_fmt!("zstd_dec: next(): state after receive: {:#x}", state); - - let (do_send, data_to_send, state) = match state.status { - ZstdDecoderStatus::DECODE_MAGIC_NUMBER => - decode_magic_number(state), - ZstdDecoderStatus::DECODE_FRAME_HEADER => - decode_frame_header(state), - ZstdDecoderStatus::DECODE_BLOCK_HEADER => - decode_block_header(state), - ZstdDecoderStatus::FEED_BLOCK_DECODER => - feed_block_decoder(state), - ZstdDecoderStatus::DECODE_CHECKSUM => - decode_checksum(state), - _ => (false, zero!(), state) - }; + next (state: ()) { + send_if(join(), cmp_output_s, false, zero!()); + } +} - trace_fmt!("zstd_dec: next(): do_send: {:#x}, data_to_send: {:#x}, state: {:#x}", do_send, data_to_send, state); - let tok = send_if(tok, block_dec_in_s, do_send, data_to_send); +//#[test_proc] +//proc ZstdDecoderTest { +// type CsrAxiAr = axi::AxiAr; +// type CsrAxiR = axi::AxiR; +// type CsrAxiAw = axi::AxiAw; +// type CsrAxiW = axi::AxiW; +// type CsrAxiB = axi::AxiB; +// +// type CsrRdReq = csr_config::CsrRdReq; +// type CsrRdResp = csr_config::CsrRdResp; +// type CsrWrReq = csr_config::CsrWrReq; +// type CsrWrResp = csr_config::CsrWrResp; +// type CsrChange = csr_config::CsrChange; +// +// type MemAxiAr = axi::AxiAr; +// type MemAxiR = axi::AxiR; +// type MemAxiAw = axi::AxiAw; +// type MemAxiW = axi::AxiW; +// type MemAxiB = axi::AxiB; +// +// type RamRdReq = ram::ReadReq; +// type RamRdResp = ram::ReadResp; +// type RamWrReq = ram::WriteReq; +// type RamWrResp = ram::WriteResp; +// +// type ZstdDecodedPacket = common::ZstdDecodedPacket; +// terminator: chan out; +// +// init {} +// +// config(terminator: chan out) { +// +// let (csr_axi_aw_s, csr_axi_aw_r) = chan("csr_axi_aw"); +// let (csr_axi_w_s, csr_axi_w_r) = chan("csr_axi_w"); +// let (csr_axi_b_s, csr_axi_b_r) = chan("csr_axi_b"); +// let (csr_axi_ar_s, csr_axi_ar_r) = chan("csr_axi_ar"); +// let (csr_axi_r_s, csr_axi_r_r) = chan("csr_axi_r"); +// +// let (fh_axi_ar_s, fh_axi_ar_r) = chan("fh_axi_ar_s"); +// let (fh_axi_r_s, fh_axi_r_r) = chan("fh_axi_r_r"); +// +// let (bh_axi_ar_s, bh_axi_ar_r) = chan("bh_axi_ar"); +// let (bh_axi_r_s, bh_axi_r_r) = chan("bh_axi_r"); +// +// let (raw_axi_ar_s, raw_axi_ar_r) = chan("raw_axi_ar"); +// let (raw_axi_r_s, raw_axi_r_r) = chan("raw_axi_r"); +// +// let (rle_axi_ar_s, rle_axi_ar_r) = chan("rle_axi_ar"); +// let (rle_axi_r_s, rle_axi_r_r) = chan("rle_axi_r"); +// +// let (ram_rd_req_0_s, ram_rd_req_0_r) = chan("ram_rd_req_0"); +// let (ram_rd_req_1_s, ram_rd_req_1_r) = chan("ram_rd_req_1"); +// let (ram_rd_req_2_s, ram_rd_req_2_r) = chan("ram_rd_req_2"); +// let (ram_rd_req_3_s, ram_rd_req_3_r) = chan("ram_rd_req_3"); +// let (ram_rd_req_4_s, ram_rd_req_4_r) = chan("ram_rd_req_4"); +// let (ram_rd_req_5_s, ram_rd_req_5_r) = chan("ram_rd_req_5"); +// let (ram_rd_req_6_s, ram_rd_req_6_r) = chan("ram_rd_req_6"); +// let (ram_rd_req_7_s, ram_rd_req_7_r) = chan("ram_rd_req_7"); +// +// let (ram_rd_resp_0_s, ram_rd_resp_0_r) = chan("ram_rd_resp_0"); +// let (ram_rd_resp_1_s, ram_rd_resp_1_r) = chan("ram_rd_resp_1"); +// let (ram_rd_resp_2_s, ram_rd_resp_2_r) = chan("ram_rd_resp_2"); +// let (ram_rd_resp_3_s, ram_rd_resp_3_r) = chan("ram_rd_resp_3"); +// let (ram_rd_resp_4_s, ram_rd_resp_4_r) = chan("ram_rd_resp_4"); +// let (ram_rd_resp_5_s, ram_rd_resp_5_r) = chan("ram_rd_resp_5"); +// let (ram_rd_resp_6_s, ram_rd_resp_6_r) = chan("ram_rd_resp_6"); +// let (ram_rd_resp_7_s, ram_rd_resp_7_r) = chan("ram_rd_resp_7"); +// +// let (ram_wr_req_0_s, ram_wr_req_0_r) = chan("ram_wr_req_0"); +// let (ram_wr_req_1_s, ram_wr_req_1_r) = chan("ram_wr_req_1"); +// let (ram_wr_req_2_s, ram_wr_req_2_r) = chan("ram_wr_req_2"); +// let (ram_wr_req_3_s, ram_wr_req_3_r) = chan("ram_wr_req_3"); +// let (ram_wr_req_4_s, ram_wr_req_4_r) = chan("ram_wr_req_4"); +// let (ram_wr_req_5_s, ram_wr_req_5_r) = chan("ram_wr_req_5"); +// let (ram_wr_req_6_s, ram_wr_req_6_r) = chan("ram_wr_req_6"); +// let (ram_wr_req_7_s, ram_wr_req_7_r) = chan("ram_wr_req_7"); +// +// let (ram_wr_resp_0_s, ram_wr_resp_0_r) = chan("ram_wr_resp_0"); +// let (ram_wr_resp_1_s, ram_wr_resp_1_r) = chan("ram_wr_resp_1"); +// let (ram_wr_resp_2_s, ram_wr_resp_2_r) = chan("ram_wr_resp_2"); +// let (ram_wr_resp_3_s, ram_wr_resp_3_r) = chan("ram_wr_resp_3"); +// let (ram_wr_resp_4_s, ram_wr_resp_4_r) = chan("ram_wr_resp_4"); +// let (ram_wr_resp_5_s, ram_wr_resp_5_r) = chan("ram_wr_resp_5"); +// let (ram_wr_resp_6_s, ram_wr_resp_6_r) = chan("ram_wr_resp_6"); +// let (ram_wr_resp_7_s, ram_wr_resp_7_r) = chan("ram_wr_resp_7"); +// +// let (output_s, output_r) = chan("output"); +// let (notify_s, notify_r) = chan<()>("notify"); +// +// spawn ZstdDecoder< +// TEST_AXI_DATA_W, TEST_AXI_ADDR_W, TEST_AXI_ID_W, TEST_AXI_DEST_W, +// TEST_REGS_N, TEST_WINDOW_LOG_MAX, +// TEST_HB_ADDR_W, TEST_HB_DATA_W, TEST_HB_NUM_PARTITIONS, TEST_HB_SIZE_KB, +// >( +// csr_axi_aw_r, csr_axi_w_r, csr_axi_b_s, csr_axi_ar_r, csr_axi_r_s, +// fh_axi_ar_s, fh_axi_r_r, +// bh_axi_ar_s, bh_axi_r_r, +// raw_axi_ar_s, raw_axi_r_r, +// rle_axi_ar_s, rle_axi_r_r, +// ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, +// ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, +// ram_rd_resp_0_r, ram_rd_resp_1_r, ram_rd_resp_2_r, ram_rd_resp_3_r, +// ram_rd_resp_4_r, ram_rd_resp_5_r, ram_rd_resp_6_r, ram_rd_resp_7_r, +// ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, +// ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, +// ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, +// ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r, +// output_s, notify_s, +// ); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_HB_RAM_ASSERT_VALID_READ +// > (ram_rd_req_0_r, ram_rd_resp_0_s, ram_wr_req_0_r, ram_wr_resp_0_s); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_HB_RAM_ASSERT_VALID_READ +// > (ram_rd_req_1_r, ram_rd_resp_1_s, ram_wr_req_1_r, ram_wr_resp_1_s); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_RAM_ASSERT_VALID_READ +// > (ram_rd_req_2_r, ram_rd_resp_2_s, ram_wr_req_2_r, ram_wr_resp_2_s); +// +// spawn ram::RamModel< +// RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_RAM_ASSERT_VALID_READ +// > (ram_rd_req_3_r, ram_rd_resp_3_s, ram_wr_req_3_r, ram_wr_resp_3_s); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_RAM_ASSERT_VALID_READ +// > (ram_rd_req_4_r, ram_rd_resp_4_s, ram_wr_req_4_r, ram_wr_resp_4_s); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_RAM_ASSERT_VALID_READ +// > (ram_rd_req_5_r, ram_rd_resp_5_s, ram_wr_req_5_r, ram_wr_resp_5_s); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_RAM_ASSERT_VALID_READ +// > (ram_rd_req_6_r, ram_rd_resp_6_s, ram_wr_req_6_r, ram_wr_resp_6_s); +// +// spawn ram::RamModel< +// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, +// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, +// TEST_RAM_ASSERT_VALID_READ +// > (ram_rd_req_7_r, ram_rd_resp_7_s, ram_wr_req_7_r, ram_wr_resp_7_s); +// +// ( +// terminator, +// csr_axi_aw_s, csr_axi_w_s, csr_axi_b_r, csr_axi_ar_s, csr_axi_s_r, +// fh_axi_ar_r, fh_axi_s_s, +// bh_axi_ar_r, bh_axi_s_s, +// raw_axi_ar_r, raw_axi_s_s, +// rle_axi_ar_r, rle_axi_s_s, +// ram_sd_seq_0_r, ram_sd_seq_1_r, ram_sd_seq_2_r, ram_sd_seq_3_r, +// ram_sd_seq_4_r, ram_sd_seq_5_r, ram_sd_seq_6_r, ram_sd_seq_7_r, +// ram_sd_sesp_0_s, ram_sd_sesp_1_s, ram_sd_sesp_2_s, ram_sd_sesp_3_s, +// ram_sd_sesp_4_s, ram_sd_sesp_5_s, ram_sd_sesp_6_s, ram_sd_sesp_7_s, +// ram_wr_seq_0_r, ram_wr_seq_1_r, ram_wr_seq_2_r, ram_wr_seq_3_r, +// ram_wr_seq_4_r, ram_wr_seq_5_r, ram_wr_seq_6_r, ram_wr_seq_7_r, +// ram_wr_sesp_0_s, ram_wr_sesp_1_s, ram_wr_sesp_2_s, ram_wr_sesp_3_s, +// ram_wr_sesp_4_s, ram_wr_sesp_5_s, ram_wr_sesp_6_s, ram_wr_sesp_7_s, +// output_r, notify_r, +// ) +// } +// +// next (state: ()) { +// +// trace_fmt!("Test start"); +// +// send(join(), terminator, true); +// } +//} + + +const INST_AXI_DATA_W = u32:64; +const INST_AXI_ADDR_W = u32:16; +const INST_AXI_ID_W = u32:4; +const INST_AXI_DEST_W = u32:4; +const INST_REGS_N = u32:16; +const INST_WINDOW_LOG_MAX = u32:30; +const INST_HB_ADDR_W = sequence_executor::ZSTD_RAM_ADDR_WIDTH; +const INST_HB_DATA_W = sequence_executor::RAM_DATA_WIDTH; +const INST_HB_NUM_PARTITIONS = sequence_executor::RAM_NUM_PARTITIONS; +const INST_HB_SIZE_KB = sequence_executor::ZSTD_HISTORY_BUFFER_SIZE_KB; + +const INST_LOG2_REGS_N = std::clog2(INST_REGS_N); +const INST_AXI_DATA_W_DIV8 = INST_AXI_DATA_W / u32:8; +const INST_HB_RAM_N = u32:8; + +proc ZstdDecoderInternalInst { + type State = ZstdDecoderInternalState; + type Fsm = ZstdDecoderInternalFsm; + + type CsrRdReq = csr_config::CsrRdReq; + type CsrRdResp = csr_config::CsrRdResp; + type CsrWrReq = csr_config::CsrWrReq; + type CsrWrResp = csr_config::CsrWrResp; + type CsrChange = csr_config::CsrChange; + + type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; + type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; + + type BlockHeaderDecoderReq = block_header_dec::BlockHeaderDecoderReq; + type BlockHeaderDecoderResp = block_header_dec::BlockHeaderDecoderResp; + + type RawBlockDecoderReq = raw_block_dec::RawBlockDecoderReq; + type RawBlockDecoderResp = raw_block_dec::RawBlockDecoderResp; + + type RleBlockDecoderReq = rle_block_dec::RleBlockDecoderReq; + type RleBlockDecoderResp = rle_block_dec::RleBlockDecoderResp; + + init { } + + config( + csr_rd_req_s: chan out, + csr_rd_resp_r: chan in, + csr_wr_req_s: chan out, + csr_wr_resp_r: chan in, + csr_change_r: chan in, + + // MemReader + FameHeaderDecoder + fh_req_s: chan out, + fh_resp_r: chan in, + + // MemReader + BlockHeaderDecoder + bh_req_s: chan out, + bh_resp_r: chan in, + + // MemReader + RawBlockDecoder + raw_req_s: chan out, + raw_resp_r: chan in, + + // MemReader + RleBlockDecoder + rle_req_s: chan out, + rle_resp_r: chan in, + + // IRQ + notify_s: chan<()> out, + ) { + spawn ZstdDecoderInternal< + INST_AXI_DATA_W, INST_AXI_ADDR_W, INST_REGS_N, + > ( + csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, + fh_req_s, fh_resp_r, + bh_req_s, bh_resp_r, + raw_req_s, raw_resp_r, + rle_req_s, rle_resp_r, + notify_s, + ); - state } + + next(state: ()) {} } -const TEST_RAM_SIZE = sequence_executor::ram_size(ZSTD_HISTORY_BUFFER_SIZE_KB); -const RAM_WORD_PARTITION_SIZE = sequence_executor::RAM_WORD_PARTITION_SIZE; -const TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = sequence_executor::TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR; -const TEST_RAM_INITIALIZED = sequence_executor::TEST_RAM_INITIALIZED; -const TEST_RAM_ASSERT_VALID_READ:bool = {false}; +proc ZstdDecoderInst { + type CsrAxiAr = axi::AxiAr; + type CsrAxiR = axi::AxiR; + type CsrAxiAw = axi::AxiAw; + type CsrAxiW = axi::AxiW; + type CsrAxiB = axi::AxiB; + + type MemAxiAr = axi::AxiAr; + type MemAxiR = axi::AxiR; + type MemAxiAw = axi::AxiAw; + type MemAxiW = axi::AxiW; + type MemAxiB = axi::AxiB; + + type RamRdReq = ram::ReadReq; + type RamRdResp = ram::ReadResp; + type RamWrReq = ram::WriteReq; + type RamWrResp = ram::WriteResp; + + type ZstdDecodedPacket = common::ZstdDecodedPacket; + + init { } -pub proc ZstdDecoderTest { - input_r: chan in; - output_s: chan out; + config( + // AXI Ctrl (subordinate) + csr_axi_aw_r: chan in, + csr_axi_w_r: chan in, + csr_axi_b_s: chan out, + csr_axi_ar_r: chan in, + csr_axi_r_s: chan out, - init {()} + // AXI Frame Header Decoder (manager) + fh_axi_ar_s: chan out, + fh_axi_r_r: chan in, - config ( - input_r: chan in, + // AXI Block Header Decoder (manager) + bh_axi_ar_s: chan out, + bh_axi_r_r: chan in, + + // AXI RAW Block Decoder (manager) + raw_axi_ar_s: chan out, + raw_axi_r_r: chan in, + + // History Buffer + ram_rd_req_0_s: chan out, + ram_rd_req_1_s: chan out, + ram_rd_req_2_s: chan out, + ram_rd_req_3_s: chan out, + ram_rd_req_4_s: chan out, + ram_rd_req_5_s: chan out, + ram_rd_req_6_s: chan out, + ram_rd_req_7_s: chan out, + ram_rd_resp_0_r: chan in, + ram_rd_resp_1_r: chan in, + ram_rd_resp_2_r: chan in, + ram_rd_resp_3_r: chan in, + ram_rd_resp_4_r: chan in, + ram_rd_resp_5_r: chan in, + ram_rd_resp_6_r: chan in, + ram_rd_resp_7_r: chan in, + ram_wr_req_0_s: chan out, + ram_wr_req_1_s: chan out, + ram_wr_req_2_s: chan out, + ram_wr_req_3_s: chan out, + ram_wr_req_4_s: chan out, + ram_wr_req_5_s: chan out, + ram_wr_req_6_s: chan out, + ram_wr_req_7_s: chan out, + ram_wr_resp_0_r: chan in, + ram_wr_resp_1_r: chan in, + ram_wr_resp_2_r: chan in, + ram_wr_resp_3_r: chan in, + ram_wr_resp_4_r: chan in, + ram_wr_resp_5_r: chan in, + ram_wr_resp_6_r: chan in, + ram_wr_resp_7_r: chan in, + + // Decoder output output_s: chan out, + notify_s: chan<()> out, ) { - let (looped_channel_s, looped_channel_r) = chan("looped_channel"); - - let (ram_rd_req_0_s, ram_rd_req_0_r) = chan, u32:1>("ram_rd_req_0"); - let (ram_rd_req_1_s, ram_rd_req_1_r) = chan, u32:1>("ram_rd_req_1"); - let (ram_rd_req_2_s, ram_rd_req_2_r) = chan, u32:1>("ram_rd_req_2"); - let (ram_rd_req_3_s, ram_rd_req_3_r) = chan, u32:1>("ram_rd_req_3"); - let (ram_rd_req_4_s, ram_rd_req_4_r) = chan, u32:1>("ram_rd_req_4"); - let (ram_rd_req_5_s, ram_rd_req_5_r) = chan, u32:1>("ram_rd_req_5"); - let (ram_rd_req_6_s, ram_rd_req_6_r) = chan, u32:1>("ram_rd_req_6"); - let (ram_rd_req_7_s, ram_rd_req_7_r) = chan, u32:1>("ram_rd_req_7"); - - let (ram_rd_resp_0_s, ram_rd_resp_0_r) = chan, u32:1>("ram_rd_resp_0"); - let (ram_rd_resp_1_s, ram_rd_resp_1_r) = chan, u32:1>("ram_rd_resp_1"); - let (ram_rd_resp_2_s, ram_rd_resp_2_r) = chan, u32:1>("ram_rd_resp_2"); - let (ram_rd_resp_3_s, ram_rd_resp_3_r) = chan, u32:1>("ram_rd_resp_3"); - let (ram_rd_resp_4_s, ram_rd_resp_4_r) = chan, u32:1>("ram_rd_resp_4"); - let (ram_rd_resp_5_s, ram_rd_resp_5_r) = chan, u32:1>("ram_rd_resp_5"); - let (ram_rd_resp_6_s, ram_rd_resp_6_r) = chan, u32:1>("ram_rd_resp_6"); - let (ram_rd_resp_7_s, ram_rd_resp_7_r) = chan, u32:1>("ram_rd_resp_7"); - - let (ram_wr_req_0_s, ram_wr_req_0_r) = chan, u32:1>("ram_wr_req_0"); - let (ram_wr_req_1_s, ram_wr_req_1_r) = chan, u32:1>("ram_wr_req_1"); - let (ram_wr_req_2_s, ram_wr_req_2_r) = chan, u32:1>("ram_wr_req_2"); - let (ram_wr_req_3_s, ram_wr_req_3_r) = chan, u32:1>("ram_wr_req_3"); - let (ram_wr_req_4_s, ram_wr_req_4_r) = chan, u32:1>("ram_wr_req_4"); - let (ram_wr_req_5_s, ram_wr_req_5_r) = chan, u32:1>("ram_wr_req_5"); - let (ram_wr_req_6_s, ram_wr_req_6_r) = chan, u32:1>("ram_wr_req_6"); - let (ram_wr_req_7_s, ram_wr_req_7_r) = chan, u32:1>("ram_wr_req_7"); - - let (ram_wr_resp_0_s, ram_wr_resp_0_r) = chan("ram_wr_resp_0"); - let (ram_wr_resp_1_s, ram_wr_resp_1_r) = chan("ram_wr_resp_1"); - let (ram_wr_resp_2_s, ram_wr_resp_2_r) = chan("ram_wr_resp_2"); - let (ram_wr_resp_3_s, ram_wr_resp_3_r) = chan("ram_wr_resp_3"); - let (ram_wr_resp_4_s, ram_wr_resp_4_r) = chan("ram_wr_resp_4"); - let (ram_wr_resp_5_s, ram_wr_resp_5_r) = chan("ram_wr_resp_5"); - let (ram_wr_resp_6_s, ram_wr_resp_6_r) = chan("ram_wr_resp_6"); - let (ram_wr_resp_7_s, ram_wr_resp_7_r) = chan("ram_wr_resp_7"); - - spawn ZstdDecoder( - input_r, output_s, - looped_channel_r, looped_channel_s, - ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, - ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, + spawn ZstdDecoder< + INST_AXI_DATA_W, INST_AXI_ADDR_W, INST_AXI_ID_W, INST_AXI_DEST_W, + INST_REGS_N, INST_WINDOW_LOG_MAX, + INST_HB_ADDR_W, INST_HB_DATA_W, INST_HB_NUM_PARTITIONS, INST_HB_SIZE_KB, + >( + csr_axi_aw_r, csr_axi_w_r, csr_axi_b_s, csr_axi_ar_r, csr_axi_r_s, + fh_axi_ar_s, fh_axi_r_r, + bh_axi_ar_s, bh_axi_r_r, + raw_axi_ar_s, raw_axi_r_r, + ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, + ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, ram_rd_resp_0_r, ram_rd_resp_1_r, ram_rd_resp_2_r, ram_rd_resp_3_r, ram_rd_resp_4_r, ram_rd_resp_5_r, ram_rd_resp_6_r, ram_rd_resp_7_r, - ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, - ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, + ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, + ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r, + output_s, notify_s, ); - - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_0_r, ram_rd_resp_0_s, ram_wr_req_0_r, ram_wr_resp_0_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_1_r, ram_rd_resp_1_s, ram_wr_req_1_r, ram_wr_resp_1_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_2_r, ram_rd_resp_2_s, ram_wr_req_2_r, ram_wr_resp_2_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_3_r, ram_rd_resp_3_s, ram_wr_req_3_r, ram_wr_resp_3_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_4_r, ram_rd_resp_4_s, ram_wr_req_4_r, ram_wr_resp_4_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_5_r, ram_rd_resp_5_s, ram_wr_req_5_r, ram_wr_resp_5_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_6_r, ram_rd_resp_6_s, ram_wr_req_6_r, ram_wr_resp_6_s); - spawn ram::RamModel< - RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, - TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, TEST_RAM_ASSERT_VALID_READ> - (ram_rd_req_7_r, ram_rd_resp_7_s, ram_wr_req_7_r, ram_wr_resp_7_s); - - (input_r, output_s) } next (state: ()) {} diff --git a/xls/modules/zstd/zstd_dec_test.cc b/xls/modules/zstd/zstd_dec_test.cc deleted file mode 100644 index 0a6679a11d..0000000000 --- a/xls/modules/zstd/zstd_dec_test.cc +++ /dev/null @@ -1,297 +0,0 @@ -// 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. -// - -#include -#include -#include -#include -#include -#include -#include // NOLINT -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "gtest/gtest.h" -#include "absl/container/flat_hash_map.h" -#include "absl/log/log.h" -#include "absl/status/statusor.h" -#include "absl/types/span.h" -#include "xls/common/file/filesystem.h" -#include "xls/common/file/get_runfile_path.h" -#include "xls/common/status/matchers.h" -#include "xls/common/status/ret_check.h" -#include "xls/interpreter/channel_queue.h" -#include "xls/interpreter/serial_proc_runtime.h" -#include "xls/ir/bits.h" -#include "xls/ir/channel.h" -#include "xls/ir/events.h" -#include "xls/ir/ir_parser.h" -#include "xls/ir/package.h" -#include "xls/ir/proc.h" -#include "xls/ir/value.h" -#include "xls/jit/jit_proc_runtime.h" -#include "xls/modules/zstd/data_generator.h" -#include "external/zstd/lib/zstd.h" - -namespace xls { -namespace { - -class ZstdDecodedPacket { - public: - static absl::StatusOr MakeZstdDecodedPacket( - const Value& packet) { - // Expect tuple - XLS_RET_CHECK(packet.IsTuple()); - // Expect exactly 3 fields - XLS_RET_CHECK(packet.size() == 3); - for (int i = 0; i < 3; i++) { - // Expect fields to be Bits - XLS_RET_CHECK(packet.element(i).IsBits()); - // All fields must fit in 64 bits - XLS_RET_CHECK(packet.element(i).bits().FitsInUint64()); - } - - std::vector data = packet.element(0).bits().ToBytes(); - absl::StatusOr len = packet.element(1).bits().ToUint64(); - XLS_RET_CHECK(len.ok()); - uint64_t length = *len; - bool last = packet.element(2).bits().IsOne(); - - return ZstdDecodedPacket(data, length, last); - } - - std::vector& GetData() { return data; } - - uint64_t GetLength() { return length; } - - bool IsLast() { return last; } - - std::string ToString() const { - std::stringstream s; - for (int j = 0; j < sizeof(uint64_t) && j < data.size(); j++) { - s << "0x" << std::setw(2) << std::setfill('0') << std::right << std::hex - << static_cast(data[j]) << std::dec << ", "; - } - return s.str(); - } - - private: - ZstdDecodedPacket(std::vector data, uint64_t length, bool last) - : data(std::move(data)), length(length), last(last) {} - - std::vector data; - uint64_t length; - bool last; -}; - -class ZstdDecoderTest : public ::testing::Test { - public: - void SetUp() override { - XLS_ASSERT_OK_AND_ASSIGN(std::filesystem::path ir_path, - xls::GetXlsRunfilePath(this->kIrFile)); - XLS_ASSERT_OK_AND_ASSIGN(std::string ir_text, - xls::GetFileContents(ir_path)); - XLS_ASSERT_OK_AND_ASSIGN(this->package, xls::Parser::ParsePackage(ir_text)); - XLS_ASSERT_OK_AND_ASSIGN(this->interpreter, - CreateJitSerialProcRuntime(this->package.get())); - - auto& queue_manager = this->interpreter->queue_manager(); - XLS_ASSERT_OK_AND_ASSIGN( - this->recv_queue, queue_manager.GetQueueByName(this->kRecvChannelName)); - XLS_ASSERT_OK_AND_ASSIGN( - this->send_queue, queue_manager.GetQueueByName(this->kSendChannelName)); - } - - void PrintTraceMessages(const std::string& pname) { - XLS_ASSERT_OK_AND_ASSIGN(Proc * proc, this->package->GetProc(pname)); - const InterpreterEvents& events = - this->interpreter->GetInterpreterEvents(proc); - - if (!events.trace_msgs.empty()) { - for (const auto& tm : events.trace_msgs) { - LOG(INFO) << "[TRACE] " << tm.message << "\n"; - } - } - } - - const std::string_view kProcName = "__zstd_dec__ZstdDecoderTest_0_next"; - const std::string_view kRecvChannelName = "zstd_dec__output_s"; - const std::string_view kSendChannelName = "zstd_dec__input_r"; - - const std::string_view kIrFile = "xls/modules/zstd/zstd_dec_test.ir"; - - std::unique_ptr package; - std::unique_ptr interpreter; - ChannelQueue *recv_queue, *send_queue; - - void PrintVector(absl::Span vec) { - for (int i = 0; i < vec.size(); i += 8) { - LOG(INFO) << "0x" << std::hex << std::setw(3) << std::left << i - << std::dec << ": "; - for (int j = 0; j < sizeof(uint64_t) && (i + j) < vec.size(); j++) { - LOG(INFO) << std::setfill('0') << std::setw(2) << std::hex - << static_cast(vec[i + j]) << std::dec << " "; - } - LOG(INFO) << "\n"; - } - } - - void DecompressWithLibZSTD(std::vector encoded_frame, - std::vector& decoded_frame) { - size_t buff_out_size = ZSTD_DStreamOutSize(); - uint8_t* const buff_out = new uint8_t[buff_out_size]; - - ZSTD_DCtx* const dctx = ZSTD_createDCtx(); - EXPECT_FALSE(dctx == nullptr); - - void* const frame = static_cast(encoded_frame.data()); - size_t const frame_size = encoded_frame.size(); - // Put the whole frame in the buffer - ZSTD_inBuffer input_buffer = {frame, frame_size, 0}; - - while (input_buffer.pos < input_buffer.size) { - ZSTD_outBuffer output_buffer = {buff_out, buff_out_size, 0}; - size_t decomp_result = - ZSTD_decompressStream(dctx, &output_buffer, &input_buffer); - bool decomp_success = ZSTD_isError(decomp_result) != 0u; - EXPECT_FALSE(decomp_success); - - // Append output buffer contents to output vector - decoded_frame.insert( - decoded_frame.end(), static_cast(output_buffer.dst), - (static_cast(output_buffer.dst) + output_buffer.pos)); - - EXPECT_TRUE(decomp_result == 0 && output_buffer.pos < output_buffer.size); - } - - ZSTD_freeDCtx(dctx); - delete[] buff_out; - } - - void ParseAndCompareWithZstd(std::vector frame) { - std::vector lib_decomp; - DecompressWithLibZSTD(frame, lib_decomp); - size_t lib_decomp_size = lib_decomp.size(); - std::cerr << "lib_decomp_size: " << lib_decomp_size << "\n"; - - std::vector sim_decomp; - size_t sim_decomp_size_words = - (lib_decomp_size + sizeof(uint64_t) - 1) / sizeof(uint64_t); - size_t sim_decomp_size_bytes = - (lib_decomp_size + sizeof(uint64_t) - 1) * sizeof(uint64_t); - sim_decomp.reserve(sim_decomp_size_bytes); - - // Send compressed frame to decoder simulation - for (int i = 0; i < frame.size(); i += 8) { - // Pad packet w/ zeros to match the frame size expected by the design. - std::array packet_data = {}; - auto frame_packet_begin = frame.begin() + i; - auto frame_packet_end = frame_packet_begin + 8 < frame.end() - ? frame_packet_begin + 8 - : frame.end(); - std::copy(frame_packet_begin, frame_packet_end, packet_data.begin()); - auto span = absl::MakeSpan(packet_data.data(), 8); - auto value = Value(Bits::FromBytes(span, 64)); - XLS_EXPECT_OK(this->send_queue->Write(value)); - XLS_EXPECT_OK(this->interpreter->Tick()); - } - PrintTraceMessages("__zstd_dec__ZstdDecoderTest_0_next"); - - // Tick decoder simulation until we get expected amount of output data - // batches on output channel queue - std::optional ticks_timeout = std::nullopt; - absl::flat_hash_map output_counts = { - {this->recv_queue->channel(), sim_decomp_size_words}}; - XLS_EXPECT_OK( - this->interpreter->TickUntilOutput(output_counts, ticks_timeout)); - - // Read decompressed data from output channel queue - for (int i = 0; i < sim_decomp_size_words; i++) { - auto read_value = this->recv_queue->Read(); - EXPECT_EQ(read_value.has_value(), true); - auto packet = - ZstdDecodedPacket::MakeZstdDecodedPacket(read_value.value()); - XLS_EXPECT_OK(packet); - auto word_vec = packet->GetData(); - auto valid_length = packet->GetLength() / CHAR_BIT; - std::copy(begin(word_vec), begin(word_vec) + valid_length, - back_inserter(sim_decomp)); - } - - EXPECT_EQ(lib_decomp_size, sim_decomp.size()); - for (int i = 0; i < lib_decomp_size; i++) { - EXPECT_EQ(lib_decomp[i], sim_decomp[i]); - } - } -}; - -/* TESTS */ - -TEST_F(ZstdDecoderTest, ParseFrameWithRawBlocks) { - int seed = 3; // Arbitrary seed value for small ZSTD frame - auto frame = zstd::GenerateFrame(seed, zstd::BlockType::RAW); - EXPECT_TRUE(frame.ok()); - this->ParseAndCompareWithZstd(frame.value()); -} - -TEST_F(ZstdDecoderTest, ParseFrameWithRleBlocks) { - int seed = 3; // Arbitrary seed value for small ZSTD frame - auto frame = zstd::GenerateFrame(seed, zstd::BlockType::RLE); - EXPECT_TRUE(frame.ok()); - this->ParseAndCompareWithZstd(frame.value()); -} - -class ZstdDecoderSeededTest : public ZstdDecoderTest, - public ::testing::WithParamInterface { - public: - static const uint32_t seed_generator_start = 0; - static const uint32_t random_frames_count = 100; -}; - -// Test `random_frames_count` instances of randomly generated valid -// frames, generated with `decodecorpus` tool. - -TEST_P(ZstdDecoderSeededTest, ParseMultipleFramesWithRawBlocks) { - auto seed = GetParam(); - auto frame = zstd::GenerateFrame(seed, zstd::BlockType::RAW); - EXPECT_TRUE(frame.ok()); - this->ParseAndCompareWithZstd(frame.value()); -} - -TEST_P(ZstdDecoderSeededTest, ParseMultipleFramesWithRleBlocks) { - auto seed = GetParam(); - auto frame = zstd::GenerateFrame(seed, zstd::BlockType::RLE); - EXPECT_TRUE(frame.ok()); - this->ParseAndCompareWithZstd(frame.value()); -} - -INSTANTIATE_TEST_SUITE_P( - ZstdDecoderSeededTest, ZstdDecoderSeededTest, - ::testing::Range(ZstdDecoderSeededTest::seed_generator_start, - ZstdDecoderSeededTest::seed_generator_start + - ZstdDecoderSeededTest::random_frames_count)); - -} // namespace -} // namespace xls From 1ad06d8c37d37f5d6cfb5ea0d137eee800ad3b54 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Wed, 9 Oct 2024 15:23:22 +0200 Subject: [PATCH 23/46] modules/zstd/cocotb: Add ZSTD frame generator library This reverts commit 04ad379225b706ddf492d440c673e77348d7a409. --- xls/modules/zstd/cocotb/BUILD | 9 +++++ xls/modules/zstd/cocotb/data_generator.py | 46 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 xls/modules/zstd/cocotb/data_generator.py diff --git a/xls/modules/zstd/cocotb/BUILD b/xls/modules/zstd/cocotb/BUILD index 23b1a843c3..cdb788d732 100644 --- a/xls/modules/zstd/cocotb/BUILD +++ b/xls/modules/zstd/cocotb/BUILD @@ -64,3 +64,12 @@ py_library( requirement("cocotb"), ], ) + +py_library( + name = "data_generator", + srcs = ["data_generator.py"], + deps = [ + "//xls/common:runfiles", + "@zstd//:decodecorpus", + ], +) diff --git a/xls/modules/zstd/cocotb/data_generator.py b/xls/modules/zstd/cocotb/data_generator.py new file mode 100644 index 0000000000..105a9a7ec7 --- /dev/null +++ b/xls/modules/zstd/cocotb/data_generator.py @@ -0,0 +1,46 @@ +# 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 pathlib import Path +from enum import Enum + +from xls.common import runfiles +import subprocess + +class BlockType(Enum): + RAW = 0 + RLE = 1 + COMPRESSED = 2 + RANDOM = 3 + +def CallDecodecorpus(args): + decodecorpus = Path(runfiles.get_path("decodecorpus", repository = "zstd")) + cmd = args + cmd.insert(0, str(decodecorpus)) + cmd_concat = " ".join(cmd) + subprocess.run(cmd_concat, shell=True, check=True) + +def GenerateFrame(seed, btype, output_path): + args = [] + args.append("-s" + str(seed)) + if (btype != BlockType.RANDOM): + args.append("--block-type=" + str(btype.value)) + args.append("--content-size") + # Test payloads up to 16KB + args.append("--max-content-size-log=14") + args.append("-p" + output_path); + args.append("-vvvvvvv"); + + CallDecodecorpus(args) + From 966511455445f4c9b2aa49adb89a85a365d4d274 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Thu, 3 Oct 2024 14:15:33 +0200 Subject: [PATCH 24/46] dependency_support: Add zstandard python library Signed-off-by: Pawel Czarnecki --- dependency_support/pip_requirements.in | 1 + dependency_support/pip_requirements_lock.txt | 99 ++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/dependency_support/pip_requirements.in b/dependency_support/pip_requirements.in index 3824c3c9ac..6170edf844 100644 --- a/dependency_support/pip_requirements.in +++ b/dependency_support/pip_requirements.in @@ -12,6 +12,7 @@ pytest==8.2.2 cocotb==1.9.0 cocotbext-axi==0.1.24 cocotb_bus==0.2.1 +zstandard==0.23.0 # 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 5945bc094b..ad142b9a5d 100644 --- a/dependency_support/pip_requirements_lock.txt +++ b/dependency_support/pip_requirements_lock.txt @@ -277,3 +277,102 @@ werkzeug==3.0.6 \ # via # -r dependency_support/pip_requirements.in # flask +zstandard==0.23.0 \ + --hash=sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473 \ + --hash=sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916 \ + --hash=sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15 \ + --hash=sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072 \ + --hash=sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4 \ + --hash=sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e \ + --hash=sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26 \ + --hash=sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8 \ + --hash=sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5 \ + --hash=sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd \ + --hash=sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c \ + --hash=sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db \ + --hash=sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5 \ + --hash=sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc \ + --hash=sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152 \ + --hash=sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269 \ + --hash=sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045 \ + --hash=sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e \ + --hash=sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d \ + --hash=sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a \ + --hash=sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb \ + --hash=sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740 \ + --hash=sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105 \ + --hash=sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274 \ + --hash=sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2 \ + --hash=sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58 \ + --hash=sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b \ + --hash=sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4 \ + --hash=sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db \ + --hash=sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e \ + --hash=sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9 \ + --hash=sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0 \ + --hash=sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813 \ + --hash=sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e \ + --hash=sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512 \ + --hash=sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0 \ + --hash=sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b \ + --hash=sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48 \ + --hash=sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a \ + --hash=sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772 \ + --hash=sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed \ + --hash=sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373 \ + --hash=sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea \ + --hash=sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd \ + --hash=sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f \ + --hash=sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc \ + --hash=sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23 \ + --hash=sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2 \ + --hash=sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db \ + --hash=sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70 \ + --hash=sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259 \ + --hash=sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9 \ + --hash=sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700 \ + --hash=sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003 \ + --hash=sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba \ + --hash=sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a \ + --hash=sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c \ + --hash=sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90 \ + --hash=sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690 \ + --hash=sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f \ + --hash=sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840 \ + --hash=sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d \ + --hash=sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9 \ + --hash=sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35 \ + --hash=sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd \ + --hash=sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a \ + --hash=sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea \ + --hash=sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1 \ + --hash=sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573 \ + --hash=sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09 \ + --hash=sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094 \ + --hash=sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78 \ + --hash=sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9 \ + --hash=sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5 \ + --hash=sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9 \ + --hash=sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391 \ + --hash=sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847 \ + --hash=sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2 \ + --hash=sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c \ + --hash=sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2 \ + --hash=sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057 \ + --hash=sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20 \ + --hash=sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d \ + --hash=sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4 \ + --hash=sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54 \ + --hash=sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171 \ + --hash=sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e \ + --hash=sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160 \ + --hash=sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b \ + --hash=sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58 \ + --hash=sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8 \ + --hash=sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33 \ + --hash=sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a \ + --hash=sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880 \ + --hash=sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca \ + --hash=sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b \ + --hash=sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69 + # via -r dependency_support/pip_requirements.in From 2ffff609a0def18442e65848e446e7e206b1377b Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 8 Oct 2024 17:31:43 +0200 Subject: [PATCH 25/46] modules/zstd: Add verilog simulation of the ZstdDecoder Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 36 + xls/modules/zstd/arbiter.v | 159 ++++ xls/modules/zstd/axi_interconnect.v | 988 ++++++++++++++++++++ xls/modules/zstd/axi_interconnect_wrapper.v | 424 +++++++++ xls/modules/zstd/priority_encoder.v | 92 ++ xls/modules/zstd/zstd_dec_cocotb_test.py | 418 +++++++++ xls/modules/zstd/zstd_dec_wrapper.v | 816 ++++++++++++++++ 7 files changed, 2933 insertions(+) create mode 100644 xls/modules/zstd/arbiter.v create mode 100644 xls/modules/zstd/axi_interconnect.v create mode 100644 xls/modules/zstd/axi_interconnect_wrapper.v create mode 100644 xls/modules/zstd/priority_encoder.v create mode 100644 xls/modules/zstd/zstd_dec_cocotb_test.py create mode 100644 xls/modules/zstd/zstd_dec_wrapper.v diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 1fdf8c2420..da714a8367 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -17,6 +17,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", @@ -1069,3 +1070,38 @@ place_and_route( tags = ["manual"], target_die_utilization_percentage = "10", ) + +py_test( + name = "zstd_dec_cocotb_test", + srcs = ["zstd_dec_cocotb_test.py"], + data = [ + ":arbiter.v", + ":axi_interconnect.v", + ":axi_interconnect_wrapper.v", + ":priority_encoder.v", + ":xls_fifo_wrapper.v", + ":zstd_dec.v", + ":zstd_dec_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"), + requirement("zstandard"), + "//xls/common:runfiles", + "//xls/modules/zstd/cocotb:channel", + "//xls/modules/zstd/cocotb:data_generator", + "//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", + ], +) diff --git a/xls/modules/zstd/arbiter.v b/xls/modules/zstd/arbiter.v new file mode 100644 index 0000000000..cfac70d1c6 --- /dev/null +++ b/xls/modules/zstd/arbiter.v @@ -0,0 +1,159 @@ +/* + +Copyright (c) 2014-2021 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * Arbiter module + */ +module arbiter # +( + parameter PORTS = 4, + // select round robin arbitration + parameter ARB_TYPE_ROUND_ROBIN = 0, + // blocking arbiter enable + parameter ARB_BLOCK = 0, + // block on acknowledge assert when nonzero, request deassert when 0 + parameter ARB_BLOCK_ACK = 1, + // LSB priority selection + parameter ARB_LSB_HIGH_PRIORITY = 0 +) +( + input wire clk, + input wire rst, + + input wire [PORTS-1:0] request, + input wire [PORTS-1:0] acknowledge, + + output wire [PORTS-1:0] grant, + output wire grant_valid, + output wire [$clog2(PORTS)-1:0] grant_encoded +); + +reg [PORTS-1:0] grant_reg = 0, grant_next; +reg grant_valid_reg = 0, grant_valid_next; +reg [$clog2(PORTS)-1:0] grant_encoded_reg = 0, grant_encoded_next; + +assign grant_valid = grant_valid_reg; +assign grant = grant_reg; +assign grant_encoded = grant_encoded_reg; + +wire request_valid; +wire [$clog2(PORTS)-1:0] request_index; +wire [PORTS-1:0] request_mask; + +priority_encoder #( + .WIDTH(PORTS), + .LSB_HIGH_PRIORITY(ARB_LSB_HIGH_PRIORITY) +) +priority_encoder_inst ( + .input_unencoded(request), + .output_valid(request_valid), + .output_encoded(request_index), + .output_unencoded(request_mask) +); + +reg [PORTS-1:0] mask_reg = 0, mask_next; + +wire masked_request_valid; +wire [$clog2(PORTS)-1:0] masked_request_index; +wire [PORTS-1:0] masked_request_mask; + +priority_encoder #( + .WIDTH(PORTS), + .LSB_HIGH_PRIORITY(ARB_LSB_HIGH_PRIORITY) +) +priority_encoder_masked ( + .input_unencoded(request & mask_reg), + .output_valid(masked_request_valid), + .output_encoded(masked_request_index), + .output_unencoded(masked_request_mask) +); + +always @* begin + grant_next = 0; + grant_valid_next = 0; + grant_encoded_next = 0; + mask_next = mask_reg; + + if (ARB_BLOCK && !ARB_BLOCK_ACK && grant_reg & request) begin + // granted request still asserted; hold it + grant_valid_next = grant_valid_reg; + grant_next = grant_reg; + grant_encoded_next = grant_encoded_reg; + end else if (ARB_BLOCK && ARB_BLOCK_ACK && grant_valid && !(grant_reg & acknowledge)) begin + // granted request not yet acknowledged; hold it + grant_valid_next = grant_valid_reg; + grant_next = grant_reg; + grant_encoded_next = grant_encoded_reg; + end else if (request_valid) begin + if (ARB_TYPE_ROUND_ROBIN) begin + if (masked_request_valid) begin + grant_valid_next = 1; + grant_next = masked_request_mask; + grant_encoded_next = masked_request_index; + if (ARB_LSB_HIGH_PRIORITY) begin + mask_next = {PORTS{1'b1}} << (masked_request_index + 1); + end else begin + mask_next = {PORTS{1'b1}} >> (PORTS - masked_request_index); + end + end else begin + grant_valid_next = 1; + grant_next = request_mask; + grant_encoded_next = request_index; + if (ARB_LSB_HIGH_PRIORITY) begin + mask_next = {PORTS{1'b1}} << (request_index + 1); + end else begin + mask_next = {PORTS{1'b1}} >> (PORTS - request_index); + end + end + end else begin + grant_valid_next = 1; + grant_next = request_mask; + grant_encoded_next = request_index; + end + end +end + +always @(posedge clk) begin + if (rst) begin + grant_reg <= 0; + grant_valid_reg <= 0; + grant_encoded_reg <= 0; + mask_reg <= 0; + end else begin + grant_reg <= grant_next; + grant_valid_reg <= grant_valid_next; + grant_encoded_reg <= grant_encoded_next; + mask_reg <= mask_next; + end +end + +endmodule + +`resetall diff --git a/xls/modules/zstd/axi_interconnect.v b/xls/modules/zstd/axi_interconnect.v new file mode 100644 index 0000000000..14256ad7de --- /dev/null +++ b/xls/modules/zstd/axi_interconnect.v @@ -0,0 +1,988 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 interconnect + */ +module axi_interconnect # +( + // Number of AXI inputs (slave interfaces) + parameter S_COUNT = 4, + // Number of AXI outputs (master interfaces) + parameter M_COUNT = 4, + // Width of data bus in bits + parameter DATA_WIDTH = 32, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) + parameter STRB_WIDTH = (DATA_WIDTH/8), + // Width of ID signal + parameter ID_WIDTH = 8, + // Propagate awuser signal + parameter AWUSER_ENABLE = 0, + // Width of awuser signal + parameter AWUSER_WIDTH = 1, + // Propagate wuser signal + parameter WUSER_ENABLE = 0, + // Width of wuser signal + parameter WUSER_WIDTH = 1, + // Propagate buser signal + parameter BUSER_ENABLE = 0, + // Width of buser signal + parameter BUSER_WIDTH = 1, + // Propagate aruser signal + parameter ARUSER_ENABLE = 0, + // Width of aruser signal + parameter ARUSER_WIDTH = 1, + // Propagate ruser signal + parameter RUSER_ENABLE = 0, + // Width of ruser signal + parameter RUSER_WIDTH = 1, + // Propagate ID field + parameter FORWARD_ID = 0, + // Number of regions per master interface + parameter M_REGIONS = 1, + // Master interface base addresses + // M_COUNT concatenated fields of M_REGIONS concatenated fields of ADDR_WIDTH bits + // set to zero for default addressing based on M_ADDR_WIDTH + parameter M_BASE_ADDR = 0, + // Master interface address widths + // M_COUNT concatenated fields of M_REGIONS concatenated fields of 32 bits + parameter M_ADDR_WIDTH = {M_COUNT{{M_REGIONS{32'd24}}}}, + // Read connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT_READ = {M_COUNT{{S_COUNT{1'b1}}}}, + // Write connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT_WRITE = {M_COUNT{{S_COUNT{1'b1}}}}, + // Secure master (fail operations based on awprot/arprot) + // M_COUNT bits + parameter M_SECURE = {M_COUNT{1'b0}} +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interfaces + */ + input wire [S_COUNT*ID_WIDTH-1:0] s_axi_awid, + input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_awaddr, + input wire [S_COUNT*8-1:0] s_axi_awlen, + input wire [S_COUNT*3-1:0] s_axi_awsize, + input wire [S_COUNT*2-1:0] s_axi_awburst, + input wire [S_COUNT-1:0] s_axi_awlock, + input wire [S_COUNT*4-1:0] s_axi_awcache, + input wire [S_COUNT*3-1:0] s_axi_awprot, + input wire [S_COUNT*4-1:0] s_axi_awqos, + input wire [S_COUNT*AWUSER_WIDTH-1:0] s_axi_awuser, + input wire [S_COUNT-1:0] s_axi_awvalid, + output wire [S_COUNT-1:0] s_axi_awready, + input wire [S_COUNT*DATA_WIDTH-1:0] s_axi_wdata, + input wire [S_COUNT*STRB_WIDTH-1:0] s_axi_wstrb, + input wire [S_COUNT-1:0] s_axi_wlast, + input wire [S_COUNT*WUSER_WIDTH-1:0] s_axi_wuser, + input wire [S_COUNT-1:0] s_axi_wvalid, + output wire [S_COUNT-1:0] s_axi_wready, + output wire [S_COUNT*ID_WIDTH-1:0] s_axi_bid, + output wire [S_COUNT*2-1:0] s_axi_bresp, + output wire [S_COUNT*BUSER_WIDTH-1:0] s_axi_buser, + output wire [S_COUNT-1:0] s_axi_bvalid, + input wire [S_COUNT-1:0] s_axi_bready, + input wire [S_COUNT*ID_WIDTH-1:0] s_axi_arid, + input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_araddr, + input wire [S_COUNT*8-1:0] s_axi_arlen, + input wire [S_COUNT*3-1:0] s_axi_arsize, + input wire [S_COUNT*2-1:0] s_axi_arburst, + input wire [S_COUNT-1:0] s_axi_arlock, + input wire [S_COUNT*4-1:0] s_axi_arcache, + input wire [S_COUNT*3-1:0] s_axi_arprot, + input wire [S_COUNT*4-1:0] s_axi_arqos, + input wire [S_COUNT*ARUSER_WIDTH-1:0] s_axi_aruser, + input wire [S_COUNT-1:0] s_axi_arvalid, + output wire [S_COUNT-1:0] s_axi_arready, + output wire [S_COUNT*ID_WIDTH-1:0] s_axi_rid, + output wire [S_COUNT*DATA_WIDTH-1:0] s_axi_rdata, + output wire [S_COUNT*2-1:0] s_axi_rresp, + output wire [S_COUNT-1:0] s_axi_rlast, + output wire [S_COUNT*RUSER_WIDTH-1:0] s_axi_ruser, + output wire [S_COUNT-1:0] s_axi_rvalid, + input wire [S_COUNT-1:0] s_axi_rready, + + /* + * AXI master interfaces + */ + output wire [M_COUNT*ID_WIDTH-1:0] m_axi_awid, + output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_awaddr, + output wire [M_COUNT*8-1:0] m_axi_awlen, + output wire [M_COUNT*3-1:0] m_axi_awsize, + output wire [M_COUNT*2-1:0] m_axi_awburst, + output wire [M_COUNT-1:0] m_axi_awlock, + output wire [M_COUNT*4-1:0] m_axi_awcache, + output wire [M_COUNT*3-1:0] m_axi_awprot, + output wire [M_COUNT*4-1:0] m_axi_awqos, + output wire [M_COUNT*4-1:0] m_axi_awregion, + output wire [M_COUNT*AWUSER_WIDTH-1:0] m_axi_awuser, + output wire [M_COUNT-1:0] m_axi_awvalid, + input wire [M_COUNT-1:0] m_axi_awready, + output wire [M_COUNT*DATA_WIDTH-1:0] m_axi_wdata, + output wire [M_COUNT*STRB_WIDTH-1:0] m_axi_wstrb, + output wire [M_COUNT-1:0] m_axi_wlast, + output wire [M_COUNT*WUSER_WIDTH-1:0] m_axi_wuser, + output wire [M_COUNT-1:0] m_axi_wvalid, + input wire [M_COUNT-1:0] m_axi_wready, + input wire [M_COUNT*ID_WIDTH-1:0] m_axi_bid, + input wire [M_COUNT*2-1:0] m_axi_bresp, + input wire [M_COUNT*BUSER_WIDTH-1:0] m_axi_buser, + input wire [M_COUNT-1:0] m_axi_bvalid, + output wire [M_COUNT-1:0] m_axi_bready, + output wire [M_COUNT*ID_WIDTH-1:0] m_axi_arid, + output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_araddr, + output wire [M_COUNT*8-1:0] m_axi_arlen, + output wire [M_COUNT*3-1:0] m_axi_arsize, + output wire [M_COUNT*2-1:0] m_axi_arburst, + output wire [M_COUNT-1:0] m_axi_arlock, + output wire [M_COUNT*4-1:0] m_axi_arcache, + output wire [M_COUNT*3-1:0] m_axi_arprot, + output wire [M_COUNT*4-1:0] m_axi_arqos, + output wire [M_COUNT*4-1:0] m_axi_arregion, + output wire [M_COUNT*ARUSER_WIDTH-1:0] m_axi_aruser, + output wire [M_COUNT-1:0] m_axi_arvalid, + input wire [M_COUNT-1:0] m_axi_arready, + input wire [M_COUNT*ID_WIDTH-1:0] m_axi_rid, + input wire [M_COUNT*DATA_WIDTH-1:0] m_axi_rdata, + input wire [M_COUNT*2-1:0] m_axi_rresp, + input wire [M_COUNT-1:0] m_axi_rlast, + input wire [M_COUNT*RUSER_WIDTH-1:0] m_axi_ruser, + input wire [M_COUNT-1:0] m_axi_rvalid, + output wire [M_COUNT-1:0] m_axi_rready +); + +parameter CL_S_COUNT = $clog2(S_COUNT); +parameter CL_M_COUNT = $clog2(M_COUNT); + +parameter AUSER_WIDTH = AWUSER_WIDTH > ARUSER_WIDTH ? AWUSER_WIDTH : ARUSER_WIDTH; + +// default address computation +function [M_COUNT*M_REGIONS*ADDR_WIDTH-1:0] calcBaseAddrs(input [31:0] dummy); + integer i; + reg [ADDR_WIDTH-1:0] base; + reg [ADDR_WIDTH-1:0] width; + reg [ADDR_WIDTH-1:0] size; + reg [ADDR_WIDTH-1:0] mask; + begin + calcBaseAddrs = {M_COUNT*M_REGIONS*ADDR_WIDTH{1'b0}}; + base = 0; + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + width = M_ADDR_WIDTH[i*32 +: 32]; + mask = {ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - width); + size = mask + 1; + if (width > 0) begin + if ((base & mask) != 0) begin + base = base + size - (base & mask); // align + end + calcBaseAddrs[i * ADDR_WIDTH +: ADDR_WIDTH] = base; + base = base + size; // increment + end + end + end +endfunction + +parameter M_BASE_ADDR_INT = M_BASE_ADDR ? M_BASE_ADDR : calcBaseAddrs(0); + +integer i, j; + +// check configuration +initial begin + if (M_REGIONS < 1 || M_REGIONS > 16) begin + $error("Error: M_REGIONS must be between 1 and 16 (instance %m)"); + $finish; + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32] && (M_ADDR_WIDTH[i*32 +: 32] < 12 || M_ADDR_WIDTH[i*32 +: 32] > ADDR_WIDTH)) begin + $error("Error: address width out of range (instance %m)"); + $finish; + end + end + + $display("Addressing configuration for axi_interconnect instance %m"); + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32]) begin + $display("%2d (%2d): %x / %02d -- %x-%x", + i/M_REGIONS, i%M_REGIONS, + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[i*32 +: 32], + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) + ); + end + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if ((M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & (2**M_ADDR_WIDTH[i*32 +: 32]-1)) != 0) begin + $display("Region not aligned:"); + $display("%2d (%2d): %x / %2d -- %x-%x", + i/M_REGIONS, i%M_REGIONS, + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[i*32 +: 32], + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) + ); + $error("Error: address range not aligned (instance %m)"); + $finish; + end + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + for (j = i+1; j < M_COUNT*M_REGIONS; j = j + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32] && M_ADDR_WIDTH[j*32 +: 32]) begin + if (((M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32])) <= (M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[j*32 +: 32])))) + && ((M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[j*32 +: 32])) <= (M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32]))))) begin + $display("Overlapping regions:"); + $display("%2d (%2d): %x / %2d -- %x-%x", + i/M_REGIONS, i%M_REGIONS, + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[i*32 +: 32], + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) + ); + $display("%2d (%2d): %x / %2d -- %x-%x", + j/M_REGIONS, j%M_REGIONS, + M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[j*32 +: 32], + M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[j*32 +: 32]), + M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[j*32 +: 32])) + ); + $error("Error: address ranges overlap (instance %m)"); + $finish; + end + end + end + end +end + +localparam [2:0] + STATE_IDLE = 3'd0, + STATE_DECODE = 3'd1, + STATE_WRITE = 3'd2, + STATE_WRITE_RESP = 3'd3, + STATE_WRITE_DROP = 3'd4, + STATE_READ = 3'd5, + STATE_READ_DROP = 3'd6, + STATE_WAIT_IDLE = 3'd7; + +reg [2:0] state_reg = STATE_IDLE, state_next; + +reg match; + +reg [CL_M_COUNT-1:0] m_select_reg = 2'd0, m_select_next; +reg [ID_WIDTH-1:0] axi_id_reg = {ID_WIDTH{1'b0}}, axi_id_next; +reg [ADDR_WIDTH-1:0] axi_addr_reg = {ADDR_WIDTH{1'b0}}, axi_addr_next; +reg axi_addr_valid_reg = 1'b0, axi_addr_valid_next; +reg [7:0] axi_len_reg = 8'd0, axi_len_next; +reg [2:0] axi_size_reg = 3'd0, axi_size_next; +reg [1:0] axi_burst_reg = 2'd0, axi_burst_next; +reg axi_lock_reg = 1'b0, axi_lock_next; +reg [3:0] axi_cache_reg = 4'd0, axi_cache_next; +reg [2:0] axi_prot_reg = 3'b000, axi_prot_next; +reg [3:0] axi_qos_reg = 4'd0, axi_qos_next; +reg [3:0] axi_region_reg = 4'd0, axi_region_next; +reg [AUSER_WIDTH-1:0] axi_auser_reg = {AUSER_WIDTH{1'b0}}, axi_auser_next; +reg [1:0] axi_bresp_reg = 2'b00, axi_bresp_next; +reg [BUSER_WIDTH-1:0] axi_buser_reg = {BUSER_WIDTH{1'b0}}, axi_buser_next; + +reg [S_COUNT-1:0] s_axi_awready_reg = 0, s_axi_awready_next; +reg [S_COUNT-1:0] s_axi_wready_reg = 0, s_axi_wready_next; +reg [S_COUNT-1:0] s_axi_bvalid_reg = 0, s_axi_bvalid_next; +reg [S_COUNT-1:0] s_axi_arready_reg = 0, s_axi_arready_next; + +reg [M_COUNT-1:0] m_axi_awvalid_reg = 0, m_axi_awvalid_next; +reg [M_COUNT-1:0] m_axi_bready_reg = 0, m_axi_bready_next; +reg [M_COUNT-1:0] m_axi_arvalid_reg = 0, m_axi_arvalid_next; +reg [M_COUNT-1:0] m_axi_rready_reg = 0, m_axi_rready_next; + +// internal datapath +reg [ID_WIDTH-1:0] s_axi_rid_int; +reg [DATA_WIDTH-1:0] s_axi_rdata_int; +reg [1:0] s_axi_rresp_int; +reg s_axi_rlast_int; +reg [RUSER_WIDTH-1:0] s_axi_ruser_int; +reg s_axi_rvalid_int; +reg s_axi_rready_int_reg = 1'b0; +wire s_axi_rready_int_early; + +reg [DATA_WIDTH-1:0] m_axi_wdata_int; +reg [STRB_WIDTH-1:0] m_axi_wstrb_int; +reg m_axi_wlast_int; +reg [WUSER_WIDTH-1:0] m_axi_wuser_int; +reg m_axi_wvalid_int; +reg m_axi_wready_int_reg = 1'b0; +wire m_axi_wready_int_early; + +assign s_axi_awready = s_axi_awready_reg; +assign s_axi_wready = s_axi_wready_reg; +assign s_axi_bid = {S_COUNT{axi_id_reg}}; +assign s_axi_bresp = {S_COUNT{axi_bresp_reg}}; +assign s_axi_buser = {S_COUNT{BUSER_ENABLE ? axi_buser_reg : {BUSER_WIDTH{1'b0}}}}; +assign s_axi_bvalid = s_axi_bvalid_reg; +assign s_axi_arready = s_axi_arready_reg; + +assign m_axi_awid = {M_COUNT{FORWARD_ID ? axi_id_reg : {ID_WIDTH{1'b0}}}}; +assign m_axi_awaddr = {M_COUNT{axi_addr_reg}}; +assign m_axi_awlen = {M_COUNT{axi_len_reg}}; +assign m_axi_awsize = {M_COUNT{axi_size_reg}}; +assign m_axi_awburst = {M_COUNT{axi_burst_reg}}; +assign m_axi_awlock = {M_COUNT{axi_lock_reg}}; +assign m_axi_awcache = {M_COUNT{axi_cache_reg}}; +assign m_axi_awprot = {M_COUNT{axi_prot_reg}}; +assign m_axi_awqos = {M_COUNT{axi_qos_reg}}; +assign m_axi_awregion = {M_COUNT{axi_region_reg}}; +assign m_axi_awuser = {M_COUNT{AWUSER_ENABLE ? axi_auser_reg[AWUSER_WIDTH-1:0] : {AWUSER_WIDTH{1'b0}}}}; +assign m_axi_awvalid = m_axi_awvalid_reg; +assign m_axi_bready = m_axi_bready_reg; +assign m_axi_arid = {M_COUNT{FORWARD_ID ? axi_id_reg : {ID_WIDTH{1'b0}}}}; +assign m_axi_araddr = {M_COUNT{axi_addr_reg}}; +assign m_axi_arlen = {M_COUNT{axi_len_reg}}; +assign m_axi_arsize = {M_COUNT{axi_size_reg}}; +assign m_axi_arburst = {M_COUNT{axi_burst_reg}}; +assign m_axi_arlock = {M_COUNT{axi_lock_reg}}; +assign m_axi_arcache = {M_COUNT{axi_cache_reg}}; +assign m_axi_arprot = {M_COUNT{axi_prot_reg}}; +assign m_axi_arqos = {M_COUNT{axi_qos_reg}}; +assign m_axi_arregion = {M_COUNT{axi_region_reg}}; +assign m_axi_aruser = {M_COUNT{ARUSER_ENABLE ? axi_auser_reg[ARUSER_WIDTH-1:0] : {ARUSER_WIDTH{1'b0}}}}; +assign m_axi_arvalid = m_axi_arvalid_reg; +assign m_axi_rready = m_axi_rready_reg; + +// slave side mux +wire [(CL_S_COUNT > 0 ? CL_S_COUNT-1 : 0):0] s_select; + +wire [ID_WIDTH-1:0] current_s_axi_awid = s_axi_awid[s_select*ID_WIDTH +: ID_WIDTH]; +wire [ADDR_WIDTH-1:0] current_s_axi_awaddr = s_axi_awaddr[s_select*ADDR_WIDTH +: ADDR_WIDTH]; +wire [7:0] current_s_axi_awlen = s_axi_awlen[s_select*8 +: 8]; +wire [2:0] current_s_axi_awsize = s_axi_awsize[s_select*3 +: 3]; +wire [1:0] current_s_axi_awburst = s_axi_awburst[s_select*2 +: 2]; +wire current_s_axi_awlock = s_axi_awlock[s_select]; +wire [3:0] current_s_axi_awcache = s_axi_awcache[s_select*4 +: 4]; +wire [2:0] current_s_axi_awprot = s_axi_awprot[s_select*3 +: 3]; +wire [3:0] current_s_axi_awqos = s_axi_awqos[s_select*4 +: 4]; +wire [AWUSER_WIDTH-1:0] current_s_axi_awuser = s_axi_awuser[s_select*AWUSER_WIDTH +: AWUSER_WIDTH]; +wire current_s_axi_awvalid = s_axi_awvalid[s_select]; +wire current_s_axi_awready = s_axi_awready[s_select]; +wire [DATA_WIDTH-1:0] current_s_axi_wdata = s_axi_wdata[s_select*DATA_WIDTH +: DATA_WIDTH]; +wire [STRB_WIDTH-1:0] current_s_axi_wstrb = s_axi_wstrb[s_select*STRB_WIDTH +: STRB_WIDTH]; +wire current_s_axi_wlast = s_axi_wlast[s_select]; +wire [WUSER_WIDTH-1:0] current_s_axi_wuser = s_axi_wuser[s_select*WUSER_WIDTH +: WUSER_WIDTH]; +wire current_s_axi_wvalid = s_axi_wvalid[s_select]; +wire current_s_axi_wready = s_axi_wready[s_select]; +wire [ID_WIDTH-1:0] current_s_axi_bid = s_axi_bid[s_select*ID_WIDTH +: ID_WIDTH]; +wire [1:0] current_s_axi_bresp = s_axi_bresp[s_select*2 +: 2]; +wire [BUSER_WIDTH-1:0] current_s_axi_buser = s_axi_buser[s_select*BUSER_WIDTH +: BUSER_WIDTH]; +wire current_s_axi_bvalid = s_axi_bvalid[s_select]; +wire current_s_axi_bready = s_axi_bready[s_select]; +wire [ID_WIDTH-1:0] current_s_axi_arid = s_axi_arid[s_select*ID_WIDTH +: ID_WIDTH]; +wire [ADDR_WIDTH-1:0] current_s_axi_araddr = s_axi_araddr[s_select*ADDR_WIDTH +: ADDR_WIDTH]; +wire [7:0] current_s_axi_arlen = s_axi_arlen[s_select*8 +: 8]; +wire [2:0] current_s_axi_arsize = s_axi_arsize[s_select*3 +: 3]; +wire [1:0] current_s_axi_arburst = s_axi_arburst[s_select*2 +: 2]; +wire current_s_axi_arlock = s_axi_arlock[s_select]; +wire [3:0] current_s_axi_arcache = s_axi_arcache[s_select*4 +: 4]; +wire [2:0] current_s_axi_arprot = s_axi_arprot[s_select*3 +: 3]; +wire [3:0] current_s_axi_arqos = s_axi_arqos[s_select*4 +: 4]; +wire [ARUSER_WIDTH-1:0] current_s_axi_aruser = s_axi_aruser[s_select*ARUSER_WIDTH +: ARUSER_WIDTH]; +wire current_s_axi_arvalid = s_axi_arvalid[s_select]; +wire current_s_axi_arready = s_axi_arready[s_select]; +wire [ID_WIDTH-1:0] current_s_axi_rid = s_axi_rid[s_select*ID_WIDTH +: ID_WIDTH]; +wire [DATA_WIDTH-1:0] current_s_axi_rdata = s_axi_rdata[s_select*DATA_WIDTH +: DATA_WIDTH]; +wire [1:0] current_s_axi_rresp = s_axi_rresp[s_select*2 +: 2]; +wire current_s_axi_rlast = s_axi_rlast[s_select]; +wire [RUSER_WIDTH-1:0] current_s_axi_ruser = s_axi_ruser[s_select*RUSER_WIDTH +: RUSER_WIDTH]; +wire current_s_axi_rvalid = s_axi_rvalid[s_select]; +wire current_s_axi_rready = s_axi_rready[s_select]; + +// master side mux +wire [ID_WIDTH-1:0] current_m_axi_awid = m_axi_awid[m_select_reg*ID_WIDTH +: ID_WIDTH]; +wire [ADDR_WIDTH-1:0] current_m_axi_awaddr = m_axi_awaddr[m_select_reg*ADDR_WIDTH +: ADDR_WIDTH]; +wire [7:0] current_m_axi_awlen = m_axi_awlen[m_select_reg*8 +: 8]; +wire [2:0] current_m_axi_awsize = m_axi_awsize[m_select_reg*3 +: 3]; +wire [1:0] current_m_axi_awburst = m_axi_awburst[m_select_reg*2 +: 2]; +wire current_m_axi_awlock = m_axi_awlock[m_select_reg]; +wire [3:0] current_m_axi_awcache = m_axi_awcache[m_select_reg*4 +: 4]; +wire [2:0] current_m_axi_awprot = m_axi_awprot[m_select_reg*3 +: 3]; +wire [3:0] current_m_axi_awqos = m_axi_awqos[m_select_reg*4 +: 4]; +wire [3:0] current_m_axi_awregion = m_axi_awregion[m_select_reg*4 +: 4]; +wire [AWUSER_WIDTH-1:0] current_m_axi_awuser = m_axi_awuser[m_select_reg*AWUSER_WIDTH +: AWUSER_WIDTH]; +wire current_m_axi_awvalid = m_axi_awvalid[m_select_reg]; +wire current_m_axi_awready = m_axi_awready[m_select_reg]; +wire [DATA_WIDTH-1:0] current_m_axi_wdata = m_axi_wdata[m_select_reg*DATA_WIDTH +: DATA_WIDTH]; +wire [STRB_WIDTH-1:0] current_m_axi_wstrb = m_axi_wstrb[m_select_reg*STRB_WIDTH +: STRB_WIDTH]; +wire current_m_axi_wlast = m_axi_wlast[m_select_reg]; +wire [WUSER_WIDTH-1:0] current_m_axi_wuser = m_axi_wuser[m_select_reg*WUSER_WIDTH +: WUSER_WIDTH]; +wire current_m_axi_wvalid = m_axi_wvalid[m_select_reg]; +wire current_m_axi_wready = m_axi_wready[m_select_reg]; +wire [ID_WIDTH-1:0] current_m_axi_bid = m_axi_bid[m_select_reg*ID_WIDTH +: ID_WIDTH]; +wire [1:0] current_m_axi_bresp = m_axi_bresp[m_select_reg*2 +: 2]; +wire [BUSER_WIDTH-1:0] current_m_axi_buser = m_axi_buser[m_select_reg*BUSER_WIDTH +: BUSER_WIDTH]; +wire current_m_axi_bvalid = m_axi_bvalid[m_select_reg]; +wire current_m_axi_bready = m_axi_bready[m_select_reg]; +wire [ID_WIDTH-1:0] current_m_axi_arid = m_axi_arid[m_select_reg*ID_WIDTH +: ID_WIDTH]; +wire [ADDR_WIDTH-1:0] current_m_axi_araddr = m_axi_araddr[m_select_reg*ADDR_WIDTH +: ADDR_WIDTH]; +wire [7:0] current_m_axi_arlen = m_axi_arlen[m_select_reg*8 +: 8]; +wire [2:0] current_m_axi_arsize = m_axi_arsize[m_select_reg*3 +: 3]; +wire [1:0] current_m_axi_arburst = m_axi_arburst[m_select_reg*2 +: 2]; +wire current_m_axi_arlock = m_axi_arlock[m_select_reg]; +wire [3:0] current_m_axi_arcache = m_axi_arcache[m_select_reg*4 +: 4]; +wire [2:0] current_m_axi_arprot = m_axi_arprot[m_select_reg*3 +: 3]; +wire [3:0] current_m_axi_arqos = m_axi_arqos[m_select_reg*4 +: 4]; +wire [3:0] current_m_axi_arregion = m_axi_arregion[m_select_reg*4 +: 4]; +wire [ARUSER_WIDTH-1:0] current_m_axi_aruser = m_axi_aruser[m_select_reg*ARUSER_WIDTH +: ARUSER_WIDTH]; +wire current_m_axi_arvalid = m_axi_arvalid[m_select_reg]; +wire current_m_axi_arready = m_axi_arready[m_select_reg]; +wire [ID_WIDTH-1:0] current_m_axi_rid = m_axi_rid[m_select_reg*ID_WIDTH +: ID_WIDTH]; +wire [DATA_WIDTH-1:0] current_m_axi_rdata = m_axi_rdata[m_select_reg*DATA_WIDTH +: DATA_WIDTH]; +wire [1:0] current_m_axi_rresp = m_axi_rresp[m_select_reg*2 +: 2]; +wire current_m_axi_rlast = m_axi_rlast[m_select_reg]; +wire [RUSER_WIDTH-1:0] current_m_axi_ruser = m_axi_ruser[m_select_reg*RUSER_WIDTH +: RUSER_WIDTH]; +wire current_m_axi_rvalid = m_axi_rvalid[m_select_reg]; +wire current_m_axi_rready = m_axi_rready[m_select_reg]; + +// arbiter instance +wire [S_COUNT*2-1:0] request; +wire [S_COUNT*2-1:0] acknowledge; +wire [S_COUNT*2-1:0] grant; +wire grant_valid; +wire [CL_S_COUNT:0] grant_encoded; + +wire read = grant_encoded[0]; +assign s_select = grant_encoded >> 1; + +arbiter #( + .PORTS(S_COUNT*2), + .ARB_TYPE_ROUND_ROBIN(1), + .ARB_BLOCK(1), + .ARB_BLOCK_ACK(1), + .ARB_LSB_HIGH_PRIORITY(1) +) +arb_inst ( + .clk(clk), + .rst(rst), + .request(request), + .acknowledge(acknowledge), + .grant(grant), + .grant_valid(grant_valid), + .grant_encoded(grant_encoded) +); + +genvar n; + +// request generation +generate +for (n = 0; n < S_COUNT; n = n + 1) begin + assign request[2*n] = s_axi_awvalid[n]; + assign request[2*n+1] = s_axi_arvalid[n]; +end +endgenerate + +// acknowledge generation +generate +for (n = 0; n < S_COUNT; n = n + 1) begin + assign acknowledge[2*n] = grant[2*n] && s_axi_bvalid[n] && s_axi_bready[n]; + assign acknowledge[2*n+1] = grant[2*n+1] && s_axi_rvalid[n] && s_axi_rready[n] && s_axi_rlast[n]; +end +endgenerate + +always @* begin + state_next = STATE_IDLE; + + match = 1'b0; + + m_select_next = m_select_reg; + axi_id_next = axi_id_reg; + axi_addr_next = axi_addr_reg; + axi_addr_valid_next = axi_addr_valid_reg; + axi_len_next = axi_len_reg; + axi_size_next = axi_size_reg; + axi_burst_next = axi_burst_reg; + axi_lock_next = axi_lock_reg; + axi_cache_next = axi_cache_reg; + axi_prot_next = axi_prot_reg; + axi_qos_next = axi_qos_reg; + axi_region_next = axi_region_reg; + axi_auser_next = axi_auser_reg; + axi_bresp_next = axi_bresp_reg; + axi_buser_next = axi_buser_reg; + + s_axi_awready_next = 0; + s_axi_wready_next = 0; + s_axi_bvalid_next = s_axi_bvalid_reg & ~s_axi_bready; + s_axi_arready_next = 0; + + m_axi_awvalid_next = m_axi_awvalid_reg & ~m_axi_awready; + m_axi_bready_next = 0; + m_axi_arvalid_next = m_axi_arvalid_reg & ~m_axi_arready; + m_axi_rready_next = 0; + + s_axi_rid_int = axi_id_reg; + s_axi_rdata_int = current_m_axi_rdata; + s_axi_rresp_int = current_m_axi_rresp; + s_axi_rlast_int = current_m_axi_rlast; + s_axi_ruser_int = current_m_axi_ruser; + s_axi_rvalid_int = 1'b0; + + m_axi_wdata_int = current_s_axi_wdata; + m_axi_wstrb_int = current_s_axi_wstrb; + m_axi_wlast_int = current_s_axi_wlast; + m_axi_wuser_int = current_s_axi_wuser; + m_axi_wvalid_int = 1'b0; + + case (state_reg) + STATE_IDLE: begin + // idle state; wait for arbitration + + if (grant_valid) begin + + axi_addr_valid_next = 1'b1; + + if (read) begin + // reading + axi_addr_next = current_s_axi_araddr; + axi_prot_next = current_s_axi_arprot; + axi_id_next = current_s_axi_arid; + axi_addr_next = current_s_axi_araddr; + axi_len_next = current_s_axi_arlen; + axi_size_next = current_s_axi_arsize; + axi_burst_next = current_s_axi_arburst; + axi_lock_next = current_s_axi_arlock; + axi_cache_next = current_s_axi_arcache; + axi_prot_next = current_s_axi_arprot; + axi_qos_next = current_s_axi_arqos; + axi_auser_next = current_s_axi_aruser; + s_axi_arready_next[s_select] = 1'b1; + end else begin + // writing + axi_addr_next = current_s_axi_awaddr; + axi_prot_next = current_s_axi_awprot; + axi_id_next = current_s_axi_awid; + axi_addr_next = current_s_axi_awaddr; + axi_len_next = current_s_axi_awlen; + axi_size_next = current_s_axi_awsize; + axi_burst_next = current_s_axi_awburst; + axi_lock_next = current_s_axi_awlock; + axi_cache_next = current_s_axi_awcache; + axi_prot_next = current_s_axi_awprot; + axi_qos_next = current_s_axi_awqos; + axi_auser_next = current_s_axi_awuser; + s_axi_awready_next[s_select] = 1'b1; + end + + state_next = STATE_DECODE; + end else begin + state_next = STATE_IDLE; + end + end + STATE_DECODE: begin + // decode state; determine master interface + + match = 1'b0; + for (i = 0; i < M_COUNT; i = i + 1) begin + for (j = 0; j < M_REGIONS; j = j + 1) begin + if (M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32] && (!M_SECURE[i] || !axi_prot_reg[1]) && ((read ? M_CONNECT_READ : M_CONNECT_WRITE) & (1 << (s_select+i*S_COUNT))) && (axi_addr_reg >> M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32]) == (M_BASE_ADDR_INT[(i*M_REGIONS+j)*ADDR_WIDTH +: ADDR_WIDTH] >> M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32])) begin + m_select_next = i; + axi_region_next = j; + match = 1'b1; + end + end + end + + if (match) begin + if (read) begin + // reading + m_axi_rready_next[m_select_reg] = s_axi_rready_int_early; + state_next = STATE_READ; + end else begin + // writing + s_axi_wready_next[s_select] = m_axi_wready_int_early; + state_next = STATE_WRITE; + end + end else begin + // no match; return decode error + if (read) begin + // reading + state_next = STATE_READ_DROP; + end else begin + // writing + axi_bresp_next = 2'b11; + s_axi_wready_next[s_select] = 1'b1; + state_next = STATE_WRITE_DROP; + end + end + end + STATE_WRITE: begin + // write state; store and forward write data + s_axi_wready_next[s_select] = m_axi_wready_int_early; + + if (axi_addr_valid_reg) begin + m_axi_awvalid_next[m_select_reg] = 1'b1; + end + axi_addr_valid_next = 1'b0; + + if (current_s_axi_wready && current_s_axi_wvalid) begin + m_axi_wdata_int = current_s_axi_wdata; + m_axi_wstrb_int = current_s_axi_wstrb; + m_axi_wlast_int = current_s_axi_wlast; + m_axi_wuser_int = current_s_axi_wuser; + m_axi_wvalid_int = 1'b1; + + if (current_s_axi_wlast) begin + s_axi_wready_next[s_select] = 1'b0; + m_axi_bready_next[m_select_reg] = 1'b1; + state_next = STATE_WRITE_RESP; + end else begin + state_next = STATE_WRITE; + end + end else begin + state_next = STATE_WRITE; + end + end + STATE_WRITE_RESP: begin + // write response state; store and forward write response + m_axi_bready_next[m_select_reg] = 1'b1; + + if (current_m_axi_bready && current_m_axi_bvalid) begin + m_axi_bready_next[m_select_reg] = 1'b0; + axi_bresp_next = current_m_axi_bresp; + s_axi_bvalid_next[s_select] = 1'b1; + state_next = STATE_WAIT_IDLE; + end else begin + state_next = STATE_WRITE_RESP; + end + end + STATE_WRITE_DROP: begin + // write drop state; drop write data + s_axi_wready_next[s_select] = 1'b1; + + axi_addr_valid_next = 1'b0; + + if (current_s_axi_wready && current_s_axi_wvalid && current_s_axi_wlast) begin + s_axi_wready_next[s_select] = 1'b0; + s_axi_bvalid_next[s_select] = 1'b1; + state_next = STATE_WAIT_IDLE; + end else begin + state_next = STATE_WRITE_DROP; + end + end + STATE_READ: begin + // read state; store and forward read response + m_axi_rready_next[m_select_reg] = s_axi_rready_int_early; + + if (axi_addr_valid_reg) begin + m_axi_arvalid_next[m_select_reg] = 1'b1; + end + axi_addr_valid_next = 1'b0; + + if (current_m_axi_rready && current_m_axi_rvalid) begin + s_axi_rid_int = axi_id_reg; + s_axi_rdata_int = current_m_axi_rdata; + s_axi_rresp_int = current_m_axi_rresp; + s_axi_rlast_int = current_m_axi_rlast; + s_axi_ruser_int = current_m_axi_ruser; + s_axi_rvalid_int = 1'b1; + + if (current_m_axi_rlast) begin + m_axi_rready_next[m_select_reg] = 1'b0; + state_next = STATE_WAIT_IDLE; + end else begin + state_next = STATE_READ; + end + end else begin + state_next = STATE_READ; + end + end + STATE_READ_DROP: begin + // read drop state; generate decode error read response + + s_axi_rid_int = axi_id_reg; + s_axi_rdata_int = {DATA_WIDTH{1'b0}}; + s_axi_rresp_int = 2'b11; + s_axi_rlast_int = axi_len_reg == 0; + s_axi_ruser_int = {RUSER_WIDTH{1'b0}}; + s_axi_rvalid_int = 1'b1; + + if (s_axi_rready_int_reg) begin + axi_len_next = axi_len_reg - 1; + if (axi_len_reg == 0) begin + state_next = STATE_WAIT_IDLE; + end else begin + state_next = STATE_READ_DROP; + end + end else begin + state_next = STATE_READ_DROP; + end + end + STATE_WAIT_IDLE: begin + // wait for idle state; wait untl grant valid is deasserted + + if (!grant_valid || acknowledge) begin + state_next = STATE_IDLE; + end else begin + state_next = STATE_WAIT_IDLE; + end + end + endcase +end + +always @(posedge clk) begin + if (rst) begin + state_reg <= STATE_IDLE; + + s_axi_awready_reg <= 0; + s_axi_wready_reg <= 0; + s_axi_bvalid_reg <= 0; + s_axi_arready_reg <= 0; + + m_axi_awvalid_reg <= 0; + m_axi_bready_reg <= 0; + m_axi_arvalid_reg <= 0; + m_axi_rready_reg <= 0; + end else begin + state_reg <= state_next; + + s_axi_awready_reg <= s_axi_awready_next; + s_axi_wready_reg <= s_axi_wready_next; + s_axi_bvalid_reg <= s_axi_bvalid_next; + s_axi_arready_reg <= s_axi_arready_next; + + m_axi_awvalid_reg <= m_axi_awvalid_next; + m_axi_bready_reg <= m_axi_bready_next; + m_axi_arvalid_reg <= m_axi_arvalid_next; + m_axi_rready_reg <= m_axi_rready_next; + end + + m_select_reg <= m_select_next; + axi_id_reg <= axi_id_next; + axi_addr_reg <= axi_addr_next; + axi_addr_valid_reg <= axi_addr_valid_next; + axi_len_reg <= axi_len_next; + axi_size_reg <= axi_size_next; + axi_burst_reg <= axi_burst_next; + axi_lock_reg <= axi_lock_next; + axi_cache_reg <= axi_cache_next; + axi_prot_reg <= axi_prot_next; + axi_qos_reg <= axi_qos_next; + axi_region_reg <= axi_region_next; + axi_auser_reg <= axi_auser_next; + axi_bresp_reg <= axi_bresp_next; + axi_buser_reg <= axi_buser_next; +end + +// output datapath logic (R channel) +reg [ID_WIDTH-1:0] s_axi_rid_reg = {ID_WIDTH{1'b0}}; +reg [DATA_WIDTH-1:0] s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; +reg [1:0] s_axi_rresp_reg = 2'd0; +reg s_axi_rlast_reg = 1'b0; +reg [RUSER_WIDTH-1:0] s_axi_ruser_reg = 1'b0; +reg [S_COUNT-1:0] s_axi_rvalid_reg = 1'b0, s_axi_rvalid_next; + +reg [ID_WIDTH-1:0] temp_s_axi_rid_reg = {ID_WIDTH{1'b0}}; +reg [DATA_WIDTH-1:0] temp_s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; +reg [1:0] temp_s_axi_rresp_reg = 2'd0; +reg temp_s_axi_rlast_reg = 1'b0; +reg [RUSER_WIDTH-1:0] temp_s_axi_ruser_reg = 1'b0; +reg temp_s_axi_rvalid_reg = 1'b0, temp_s_axi_rvalid_next; + +// datapath control +reg store_axi_r_int_to_output; +reg store_axi_r_int_to_temp; +reg store_axi_r_temp_to_output; + +assign s_axi_rid = {S_COUNT{s_axi_rid_reg}}; +assign s_axi_rdata = {S_COUNT{s_axi_rdata_reg}}; +assign s_axi_rresp = {S_COUNT{s_axi_rresp_reg}}; +assign s_axi_rlast = {S_COUNT{s_axi_rlast_reg}}; +assign s_axi_ruser = {S_COUNT{RUSER_ENABLE ? s_axi_ruser_reg : {RUSER_WIDTH{1'b0}}}}; +assign s_axi_rvalid = s_axi_rvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +assign s_axi_rready_int_early = current_s_axi_rready | (~temp_s_axi_rvalid_reg & (~current_s_axi_rvalid | ~s_axi_rvalid_int)); + +always @* begin + // transfer sink ready state to source + s_axi_rvalid_next = s_axi_rvalid_reg; + temp_s_axi_rvalid_next = temp_s_axi_rvalid_reg; + + store_axi_r_int_to_output = 1'b0; + store_axi_r_int_to_temp = 1'b0; + store_axi_r_temp_to_output = 1'b0; + + if (s_axi_rready_int_reg) begin + // input is ready + if (current_s_axi_rready | ~current_s_axi_rvalid) begin + // output is ready or currently not valid, transfer data to output + s_axi_rvalid_next[s_select] = s_axi_rvalid_int; + store_axi_r_int_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_s_axi_rvalid_next = s_axi_rvalid_int; + store_axi_r_int_to_temp = 1'b1; + end + end else if (current_s_axi_rready) begin + // input is not ready, but output is ready + s_axi_rvalid_next[s_select] = temp_s_axi_rvalid_reg; + temp_s_axi_rvalid_next = 1'b0; + store_axi_r_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_rvalid_reg <= 1'b0; + s_axi_rready_int_reg <= 1'b0; + temp_s_axi_rvalid_reg <= 1'b0; + end else begin + s_axi_rvalid_reg <= s_axi_rvalid_next; + s_axi_rready_int_reg <= s_axi_rready_int_early; + temp_s_axi_rvalid_reg <= temp_s_axi_rvalid_next; + end + + // datapath + if (store_axi_r_int_to_output) begin + s_axi_rid_reg <= s_axi_rid_int; + s_axi_rdata_reg <= s_axi_rdata_int; + s_axi_rresp_reg <= s_axi_rresp_int; + s_axi_rlast_reg <= s_axi_rlast_int; + s_axi_ruser_reg <= s_axi_ruser_int; + end else if (store_axi_r_temp_to_output) begin + s_axi_rid_reg <= temp_s_axi_rid_reg; + s_axi_rdata_reg <= temp_s_axi_rdata_reg; + s_axi_rresp_reg <= temp_s_axi_rresp_reg; + s_axi_rlast_reg <= temp_s_axi_rlast_reg; + s_axi_ruser_reg <= temp_s_axi_ruser_reg; + end + + if (store_axi_r_int_to_temp) begin + temp_s_axi_rid_reg <= s_axi_rid_int; + temp_s_axi_rdata_reg <= s_axi_rdata_int; + temp_s_axi_rresp_reg <= s_axi_rresp_int; + temp_s_axi_rlast_reg <= s_axi_rlast_int; + temp_s_axi_ruser_reg <= s_axi_ruser_int; + end +end + +// output datapath logic (W channel) +reg [DATA_WIDTH-1:0] m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; +reg [STRB_WIDTH-1:0] m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; +reg m_axi_wlast_reg = 1'b0; +reg [WUSER_WIDTH-1:0] m_axi_wuser_reg = 1'b0; +reg [M_COUNT-1:0] m_axi_wvalid_reg = 1'b0, m_axi_wvalid_next; + +reg [DATA_WIDTH-1:0] temp_m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; +reg [STRB_WIDTH-1:0] temp_m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; +reg temp_m_axi_wlast_reg = 1'b0; +reg [WUSER_WIDTH-1:0] temp_m_axi_wuser_reg = 1'b0; +reg temp_m_axi_wvalid_reg = 1'b0, temp_m_axi_wvalid_next; + +// datapath control +reg store_axi_w_int_to_output; +reg store_axi_w_int_to_temp; +reg store_axi_w_temp_to_output; + +assign m_axi_wdata = {M_COUNT{m_axi_wdata_reg}}; +assign m_axi_wstrb = {M_COUNT{m_axi_wstrb_reg}}; +assign m_axi_wlast = {M_COUNT{m_axi_wlast_reg}}; +assign m_axi_wuser = {M_COUNT{WUSER_ENABLE ? m_axi_wuser_reg : {WUSER_WIDTH{1'b0}}}}; +assign m_axi_wvalid = m_axi_wvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +assign m_axi_wready_int_early = current_m_axi_wready | (~temp_m_axi_wvalid_reg & (~current_m_axi_wvalid | ~m_axi_wvalid_int)); + +always @* begin + // transfer sink ready state to source + m_axi_wvalid_next = m_axi_wvalid_reg; + temp_m_axi_wvalid_next = temp_m_axi_wvalid_reg; + + store_axi_w_int_to_output = 1'b0; + store_axi_w_int_to_temp = 1'b0; + store_axi_w_temp_to_output = 1'b0; + + if (m_axi_wready_int_reg) begin + // input is ready + if (current_m_axi_wready | ~current_m_axi_wvalid) begin + // output is ready or currently not valid, transfer data to output + m_axi_wvalid_next[m_select_reg] = m_axi_wvalid_int; + store_axi_w_int_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axi_wvalid_next = m_axi_wvalid_int; + store_axi_w_int_to_temp = 1'b1; + end + end else if (current_m_axi_wready) begin + // input is not ready, but output is ready + m_axi_wvalid_next[m_select_reg] = temp_m_axi_wvalid_reg; + temp_m_axi_wvalid_next = 1'b0; + store_axi_w_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + m_axi_wvalid_reg <= 1'b0; + m_axi_wready_int_reg <= 1'b0; + temp_m_axi_wvalid_reg <= 1'b0; + end else begin + m_axi_wvalid_reg <= m_axi_wvalid_next; + m_axi_wready_int_reg <= m_axi_wready_int_early; + temp_m_axi_wvalid_reg <= temp_m_axi_wvalid_next; + end + + // datapath + if (store_axi_w_int_to_output) begin + m_axi_wdata_reg <= m_axi_wdata_int; + m_axi_wstrb_reg <= m_axi_wstrb_int; + m_axi_wlast_reg <= m_axi_wlast_int; + m_axi_wuser_reg <= m_axi_wuser_int; + end else if (store_axi_w_temp_to_output) begin + m_axi_wdata_reg <= temp_m_axi_wdata_reg; + m_axi_wstrb_reg <= temp_m_axi_wstrb_reg; + m_axi_wlast_reg <= temp_m_axi_wlast_reg; + m_axi_wuser_reg <= temp_m_axi_wuser_reg; + end + + if (store_axi_w_int_to_temp) begin + temp_m_axi_wdata_reg <= m_axi_wdata_int; + temp_m_axi_wstrb_reg <= m_axi_wstrb_int; + temp_m_axi_wlast_reg <= m_axi_wlast_int; + temp_m_axi_wuser_reg <= m_axi_wuser_int; + end +end + +endmodule + +`resetall diff --git a/xls/modules/zstd/axi_interconnect_wrapper.v b/xls/modules/zstd/axi_interconnect_wrapper.v new file mode 100644 index 0000000000..50017314f9 --- /dev/null +++ b/xls/modules/zstd/axi_interconnect_wrapper.v @@ -0,0 +1,424 @@ +/* + +Copyright (c) 2020 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 4x1 interconnect (wrapper) + */ +module axi_interconnect_wrapper # +( + parameter DATA_WIDTH = 32, + parameter ADDR_WIDTH = 32, + parameter STRB_WIDTH = (DATA_WIDTH/8), + parameter ID_WIDTH = 8, + parameter AWUSER_ENABLE = 0, + parameter AWUSER_WIDTH = 1, + parameter WUSER_ENABLE = 0, + parameter WUSER_WIDTH = 1, + parameter BUSER_ENABLE = 0, + parameter BUSER_WIDTH = 1, + parameter ARUSER_ENABLE = 0, + parameter ARUSER_WIDTH = 1, + parameter RUSER_ENABLE = 0, + parameter RUSER_WIDTH = 1, + parameter FORWARD_ID = 0, + parameter M_REGIONS = 1, + parameter M00_BASE_ADDR = 0, + parameter M00_ADDR_WIDTH = {M_REGIONS{32'd24}}, + parameter M00_CONNECT_READ = 4'b1111, + parameter M00_CONNECT_WRITE = 4'b1111, + parameter M00_SECURE = 1'b0 +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interface + */ + input wire [ID_WIDTH-1:0] s00_axi_awid, + input wire [ADDR_WIDTH-1:0] s00_axi_awaddr, + input wire [7:0] s00_axi_awlen, + input wire [2:0] s00_axi_awsize, + input wire [1:0] s00_axi_awburst, + input wire s00_axi_awlock, + input wire [3:0] s00_axi_awcache, + input wire [2:0] s00_axi_awprot, + input wire [3:0] s00_axi_awqos, + input wire [AWUSER_WIDTH-1:0] s00_axi_awuser, + input wire s00_axi_awvalid, + output wire s00_axi_awready, + input wire [DATA_WIDTH-1:0] s00_axi_wdata, + input wire [STRB_WIDTH-1:0] s00_axi_wstrb, + input wire s00_axi_wlast, + input wire [WUSER_WIDTH-1:0] s00_axi_wuser, + input wire s00_axi_wvalid, + output wire s00_axi_wready, + output wire [ID_WIDTH-1:0] s00_axi_bid, + output wire [1:0] s00_axi_bresp, + output wire [BUSER_WIDTH-1:0] s00_axi_buser, + output wire s00_axi_bvalid, + input wire s00_axi_bready, + input wire [ID_WIDTH-1:0] s00_axi_arid, + input wire [ADDR_WIDTH-1:0] s00_axi_araddr, + input wire [7:0] s00_axi_arlen, + input wire [2:0] s00_axi_arsize, + input wire [1:0] s00_axi_arburst, + input wire s00_axi_arlock, + input wire [3:0] s00_axi_arcache, + input wire [2:0] s00_axi_arprot, + input wire [3:0] s00_axi_arqos, + input wire [ARUSER_WIDTH-1:0] s00_axi_aruser, + input wire s00_axi_arvalid, + output wire s00_axi_arready, + output wire [ID_WIDTH-1:0] s00_axi_rid, + output wire [DATA_WIDTH-1:0] s00_axi_rdata, + output wire [1:0] s00_axi_rresp, + output wire s00_axi_rlast, + output wire [RUSER_WIDTH-1:0] s00_axi_ruser, + output wire s00_axi_rvalid, + input wire s00_axi_rready, + + input wire [ID_WIDTH-1:0] s01_axi_awid, + input wire [ADDR_WIDTH-1:0] s01_axi_awaddr, + input wire [7:0] s01_axi_awlen, + input wire [2:0] s01_axi_awsize, + input wire [1:0] s01_axi_awburst, + input wire s01_axi_awlock, + input wire [3:0] s01_axi_awcache, + input wire [2:0] s01_axi_awprot, + input wire [3:0] s01_axi_awqos, + input wire [AWUSER_WIDTH-1:0] s01_axi_awuser, + input wire s01_axi_awvalid, + output wire s01_axi_awready, + input wire [DATA_WIDTH-1:0] s01_axi_wdata, + input wire [STRB_WIDTH-1:0] s01_axi_wstrb, + input wire s01_axi_wlast, + input wire [WUSER_WIDTH-1:0] s01_axi_wuser, + input wire s01_axi_wvalid, + output wire s01_axi_wready, + output wire [ID_WIDTH-1:0] s01_axi_bid, + output wire [1:0] s01_axi_bresp, + output wire [BUSER_WIDTH-1:0] s01_axi_buser, + output wire s01_axi_bvalid, + input wire s01_axi_bready, + input wire [ID_WIDTH-1:0] s01_axi_arid, + input wire [ADDR_WIDTH-1:0] s01_axi_araddr, + input wire [7:0] s01_axi_arlen, + input wire [2:0] s01_axi_arsize, + input wire [1:0] s01_axi_arburst, + input wire s01_axi_arlock, + input wire [3:0] s01_axi_arcache, + input wire [2:0] s01_axi_arprot, + input wire [3:0] s01_axi_arqos, + input wire [ARUSER_WIDTH-1:0] s01_axi_aruser, + input wire s01_axi_arvalid, + output wire s01_axi_arready, + output wire [ID_WIDTH-1:0] s01_axi_rid, + output wire [DATA_WIDTH-1:0] s01_axi_rdata, + output wire [1:0] s01_axi_rresp, + output wire s01_axi_rlast, + output wire [RUSER_WIDTH-1:0] s01_axi_ruser, + output wire s01_axi_rvalid, + input wire s01_axi_rready, + + input wire [ID_WIDTH-1:0] s02_axi_awid, + input wire [ADDR_WIDTH-1:0] s02_axi_awaddr, + input wire [7:0] s02_axi_awlen, + input wire [2:0] s02_axi_awsize, + input wire [1:0] s02_axi_awburst, + input wire s02_axi_awlock, + input wire [3:0] s02_axi_awcache, + input wire [2:0] s02_axi_awprot, + input wire [3:0] s02_axi_awqos, + input wire [AWUSER_WIDTH-1:0] s02_axi_awuser, + input wire s02_axi_awvalid, + output wire s02_axi_awready, + input wire [DATA_WIDTH-1:0] s02_axi_wdata, + input wire [STRB_WIDTH-1:0] s02_axi_wstrb, + input wire s02_axi_wlast, + input wire [WUSER_WIDTH-1:0] s02_axi_wuser, + input wire s02_axi_wvalid, + output wire s02_axi_wready, + output wire [ID_WIDTH-1:0] s02_axi_bid, + output wire [1:0] s02_axi_bresp, + output wire [BUSER_WIDTH-1:0] s02_axi_buser, + output wire s02_axi_bvalid, + input wire s02_axi_bready, + input wire [ID_WIDTH-1:0] s02_axi_arid, + input wire [ADDR_WIDTH-1:0] s02_axi_araddr, + input wire [7:0] s02_axi_arlen, + input wire [2:0] s02_axi_arsize, + input wire [1:0] s02_axi_arburst, + input wire s02_axi_arlock, + input wire [3:0] s02_axi_arcache, + input wire [2:0] s02_axi_arprot, + input wire [3:0] s02_axi_arqos, + input wire [ARUSER_WIDTH-1:0] s02_axi_aruser, + input wire s02_axi_arvalid, + output wire s02_axi_arready, + output wire [ID_WIDTH-1:0] s02_axi_rid, + output wire [DATA_WIDTH-1:0] s02_axi_rdata, + output wire [1:0] s02_axi_rresp, + output wire s02_axi_rlast, + output wire [RUSER_WIDTH-1:0] s02_axi_ruser, + output wire s02_axi_rvalid, + input wire s02_axi_rready, + + input wire [ID_WIDTH-1:0] s03_axi_awid, + input wire [ADDR_WIDTH-1:0] s03_axi_awaddr, + input wire [7:0] s03_axi_awlen, + input wire [2:0] s03_axi_awsize, + input wire [1:0] s03_axi_awburst, + input wire s03_axi_awlock, + input wire [3:0] s03_axi_awcache, + input wire [2:0] s03_axi_awprot, + input wire [3:0] s03_axi_awqos, + input wire [AWUSER_WIDTH-1:0] s03_axi_awuser, + input wire s03_axi_awvalid, + output wire s03_axi_awready, + input wire [DATA_WIDTH-1:0] s03_axi_wdata, + input wire [STRB_WIDTH-1:0] s03_axi_wstrb, + input wire s03_axi_wlast, + input wire [WUSER_WIDTH-1:0] s03_axi_wuser, + input wire s03_axi_wvalid, + output wire s03_axi_wready, + output wire [ID_WIDTH-1:0] s03_axi_bid, + output wire [1:0] s03_axi_bresp, + output wire [BUSER_WIDTH-1:0] s03_axi_buser, + output wire s03_axi_bvalid, + input wire s03_axi_bready, + input wire [ID_WIDTH-1:0] s03_axi_arid, + input wire [ADDR_WIDTH-1:0] s03_axi_araddr, + input wire [7:0] s03_axi_arlen, + input wire [2:0] s03_axi_arsize, + input wire [1:0] s03_axi_arburst, + input wire s03_axi_arlock, + input wire [3:0] s03_axi_arcache, + input wire [2:0] s03_axi_arprot, + input wire [3:0] s03_axi_arqos, + input wire [ARUSER_WIDTH-1:0] s03_axi_aruser, + input wire s03_axi_arvalid, + output wire s03_axi_arready, + output wire [ID_WIDTH-1:0] s03_axi_rid, + output wire [DATA_WIDTH-1:0] s03_axi_rdata, + output wire [1:0] s03_axi_rresp, + output wire s03_axi_rlast, + output wire [RUSER_WIDTH-1:0] s03_axi_ruser, + output wire s03_axi_rvalid, + input wire s03_axi_rready, + + /* + * AXI master interface + */ + output wire [ID_WIDTH-1:0] m00_axi_awid, + output wire [ADDR_WIDTH-1:0] m00_axi_awaddr, + output wire [7:0] m00_axi_awlen, + output wire [2:0] m00_axi_awsize, + output wire [1:0] m00_axi_awburst, + output wire m00_axi_awlock, + output wire [3:0] m00_axi_awcache, + output wire [2:0] m00_axi_awprot, + output wire [3:0] m00_axi_awqos, + output wire [3:0] m00_axi_awregion, + output wire [AWUSER_WIDTH-1:0] m00_axi_awuser, + output wire m00_axi_awvalid, + input wire m00_axi_awready, + output wire [DATA_WIDTH-1:0] m00_axi_wdata, + output wire [STRB_WIDTH-1:0] m00_axi_wstrb, + output wire m00_axi_wlast, + output wire [WUSER_WIDTH-1:0] m00_axi_wuser, + output wire m00_axi_wvalid, + input wire m00_axi_wready, + input wire [ID_WIDTH-1:0] m00_axi_bid, + input wire [1:0] m00_axi_bresp, + input wire [BUSER_WIDTH-1:0] m00_axi_buser, + input wire m00_axi_bvalid, + output wire m00_axi_bready, + output wire [ID_WIDTH-1:0] m00_axi_arid, + output wire [ADDR_WIDTH-1:0] m00_axi_araddr, + output wire [7:0] m00_axi_arlen, + output wire [2:0] m00_axi_arsize, + output wire [1:0] m00_axi_arburst, + output wire m00_axi_arlock, + output wire [3:0] m00_axi_arcache, + output wire [2:0] m00_axi_arprot, + output wire [3:0] m00_axi_arqos, + output wire [3:0] m00_axi_arregion, + output wire [ARUSER_WIDTH-1:0] m00_axi_aruser, + output wire m00_axi_arvalid, + input wire m00_axi_arready, + input wire [ID_WIDTH-1:0] m00_axi_rid, + input wire [DATA_WIDTH-1:0] m00_axi_rdata, + input wire [1:0] m00_axi_rresp, + input wire m00_axi_rlast, + input wire [RUSER_WIDTH-1:0] m00_axi_ruser, + input wire m00_axi_rvalid, + output wire m00_axi_rready +); + +localparam S_COUNT = 4; +localparam M_COUNT = 1; + +// parameter sizing helpers +function [ADDR_WIDTH*M_REGIONS-1:0] w_a_r(input [ADDR_WIDTH*M_REGIONS-1:0] val); + w_a_r = val; +endfunction + +function [32*M_REGIONS-1:0] w_32_r(input [32*M_REGIONS-1:0] val); + w_32_r = val; +endfunction + +function [S_COUNT-1:0] w_s(input [S_COUNT-1:0] val); + w_s = val; +endfunction + +function w_1(input val); + w_1 = val; +endfunction + +axi_interconnect #( + .S_COUNT(S_COUNT), + .M_COUNT(M_COUNT), + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .ID_WIDTH(ID_WIDTH), + .AWUSER_ENABLE(AWUSER_ENABLE), + .AWUSER_WIDTH(AWUSER_WIDTH), + .WUSER_ENABLE(WUSER_ENABLE), + .WUSER_WIDTH(WUSER_WIDTH), + .BUSER_ENABLE(BUSER_ENABLE), + .BUSER_WIDTH(BUSER_WIDTH), + .ARUSER_ENABLE(ARUSER_ENABLE), + .ARUSER_WIDTH(ARUSER_WIDTH), + .RUSER_ENABLE(RUSER_ENABLE), + .RUSER_WIDTH(RUSER_WIDTH), + .FORWARD_ID(FORWARD_ID), + .M_REGIONS(M_REGIONS), + .M_BASE_ADDR({ w_a_r(M00_BASE_ADDR) }), + .M_ADDR_WIDTH({ w_32_r(M00_ADDR_WIDTH) }), + .M_CONNECT_READ({ w_s(M00_CONNECT_READ) }), + .M_CONNECT_WRITE({ w_s(M00_CONNECT_WRITE) }), + .M_SECURE({ w_1(M00_SECURE) }) +) +axi_interconnect_inst ( + .clk(clk), + .rst(rst), + .s_axi_awid({ s03_axi_awid, s02_axi_awid, s01_axi_awid, s00_axi_awid }), + .s_axi_awaddr({ s03_axi_awaddr, s02_axi_awaddr, s01_axi_awaddr, s00_axi_awaddr }), + .s_axi_awlen({ s03_axi_awlen, s02_axi_awlen, s01_axi_awlen, s00_axi_awlen }), + .s_axi_awsize({ s03_axi_awsize, s02_axi_awsize, s01_axi_awsize, s00_axi_awsize }), + .s_axi_awburst({ s03_axi_awburst, s02_axi_awburst, s01_axi_awburst, s00_axi_awburst }), + .s_axi_awlock({ s03_axi_awlock, s02_axi_awlock, s01_axi_awlock, s00_axi_awlock }), + .s_axi_awcache({ s03_axi_awcache, s02_axi_awcache, s01_axi_awcache, s00_axi_awcache }), + .s_axi_awprot({ s03_axi_awprot, s02_axi_awprot, s01_axi_awprot, s00_axi_awprot }), + .s_axi_awqos({ s03_axi_awqos, s02_axi_awqos, s01_axi_awqos, s00_axi_awqos }), + .s_axi_awuser({ s03_axi_awuser, s02_axi_awuser, s01_axi_awuser, s00_axi_awuser }), + .s_axi_awvalid({ s03_axi_awvalid, s02_axi_awvalid, s01_axi_awvalid, s00_axi_awvalid }), + .s_axi_awready({ s03_axi_awready, s02_axi_awready, s01_axi_awready, s00_axi_awready }), + .s_axi_wdata({ s03_axi_wdata, s02_axi_wdata, s01_axi_wdata, s00_axi_wdata }), + .s_axi_wstrb({ s03_axi_wstrb, s02_axi_wstrb, s01_axi_wstrb, s00_axi_wstrb }), + .s_axi_wlast({ s03_axi_wlast, s02_axi_wlast, s01_axi_wlast, s00_axi_wlast }), + .s_axi_wuser({ s03_axi_wuser, s02_axi_wuser, s01_axi_wuser, s00_axi_wuser }), + .s_axi_wvalid({ s03_axi_wvalid, s02_axi_wvalid, s01_axi_wvalid, s00_axi_wvalid }), + .s_axi_wready({ s03_axi_wready, s02_axi_wready, s01_axi_wready, s00_axi_wready }), + .s_axi_bid({ s03_axi_bid, s02_axi_bid, s01_axi_bid, s00_axi_bid }), + .s_axi_bresp({ s03_axi_bresp, s02_axi_bresp, s01_axi_bresp, s00_axi_bresp }), + .s_axi_buser({ s03_axi_buser, s02_axi_buser, s01_axi_buser, s00_axi_buser }), + .s_axi_bvalid({ s03_axi_bvalid, s02_axi_bvalid, s01_axi_bvalid, s00_axi_bvalid }), + .s_axi_bready({ s03_axi_bready, s02_axi_bready, s01_axi_bready, s00_axi_bready }), + .s_axi_arid({ s03_axi_arid, s02_axi_arid, s01_axi_arid, s00_axi_arid }), + .s_axi_araddr({ s03_axi_araddr, s02_axi_araddr, s01_axi_araddr, s00_axi_araddr }), + .s_axi_arlen({ s03_axi_arlen, s02_axi_arlen, s01_axi_arlen, s00_axi_arlen }), + .s_axi_arsize({ s03_axi_arsize, s02_axi_arsize, s01_axi_arsize, s00_axi_arsize }), + .s_axi_arburst({ s03_axi_arburst, s02_axi_arburst, s01_axi_arburst, s00_axi_arburst }), + .s_axi_arlock({ s03_axi_arlock, s02_axi_arlock, s01_axi_arlock, s00_axi_arlock }), + .s_axi_arcache({ s03_axi_arcache, s02_axi_arcache, s01_axi_arcache, s00_axi_arcache }), + .s_axi_arprot({ s03_axi_arprot, s02_axi_arprot, s01_axi_arprot, s00_axi_arprot }), + .s_axi_arqos({ s03_axi_arqos, s02_axi_arqos, s01_axi_arqos, s00_axi_arqos }), + .s_axi_aruser({ s03_axi_aruser, s02_axi_aruser, s01_axi_aruser, s00_axi_aruser }), + .s_axi_arvalid({ s03_axi_arvalid, s02_axi_arvalid, s01_axi_arvalid, s00_axi_arvalid }), + .s_axi_arready({ s03_axi_arready, s02_axi_arready, s01_axi_arready, s00_axi_arready }), + .s_axi_rid({ s03_axi_rid, s02_axi_rid, s01_axi_rid, s00_axi_rid }), + .s_axi_rdata({ s03_axi_rdata, s02_axi_rdata, s01_axi_rdata, s00_axi_rdata }), + .s_axi_rresp({ s03_axi_rresp, s02_axi_rresp, s01_axi_rresp, s00_axi_rresp }), + .s_axi_rlast({ s03_axi_rlast, s02_axi_rlast, s01_axi_rlast, s00_axi_rlast }), + .s_axi_ruser({ s03_axi_ruser, s02_axi_ruser, s01_axi_ruser, s00_axi_ruser }), + .s_axi_rvalid({ s03_axi_rvalid, s02_axi_rvalid, s01_axi_rvalid, s00_axi_rvalid }), + .s_axi_rready({ s03_axi_rready, s02_axi_rready, s01_axi_rready, s00_axi_rready }), + .m_axi_awid({ m00_axi_awid }), + .m_axi_awaddr({ m00_axi_awaddr }), + .m_axi_awlen({ m00_axi_awlen }), + .m_axi_awsize({ m00_axi_awsize }), + .m_axi_awburst({ m00_axi_awburst }), + .m_axi_awlock({ m00_axi_awlock }), + .m_axi_awcache({ m00_axi_awcache }), + .m_axi_awprot({ m00_axi_awprot }), + .m_axi_awqos({ m00_axi_awqos }), + .m_axi_awregion({ m00_axi_awregion }), + .m_axi_awuser({ m00_axi_awuser }), + .m_axi_awvalid({ m00_axi_awvalid }), + .m_axi_awready({ m00_axi_awready }), + .m_axi_wdata({ m00_axi_wdata }), + .m_axi_wstrb({ m00_axi_wstrb }), + .m_axi_wlast({ m00_axi_wlast }), + .m_axi_wuser({ m00_axi_wuser }), + .m_axi_wvalid({ m00_axi_wvalid }), + .m_axi_wready({ m00_axi_wready }), + .m_axi_bid({ m00_axi_bid }), + .m_axi_bresp({ m00_axi_bresp }), + .m_axi_buser({ m00_axi_buser }), + .m_axi_bvalid({ m00_axi_bvalid }), + .m_axi_bready({ m00_axi_bready }), + .m_axi_arid({ m00_axi_arid }), + .m_axi_araddr({ m00_axi_araddr }), + .m_axi_arlen({ m00_axi_arlen }), + .m_axi_arsize({ m00_axi_arsize }), + .m_axi_arburst({ m00_axi_arburst }), + .m_axi_arlock({ m00_axi_arlock }), + .m_axi_arcache({ m00_axi_arcache }), + .m_axi_arprot({ m00_axi_arprot }), + .m_axi_arqos({ m00_axi_arqos }), + .m_axi_arregion({ m00_axi_arregion }), + .m_axi_aruser({ m00_axi_aruser }), + .m_axi_arvalid({ m00_axi_arvalid }), + .m_axi_arready({ m00_axi_arready }), + .m_axi_rid({ m00_axi_rid }), + .m_axi_rdata({ m00_axi_rdata }), + .m_axi_rresp({ m00_axi_rresp }), + .m_axi_rlast({ m00_axi_rlast }), + .m_axi_ruser({ m00_axi_ruser }), + .m_axi_rvalid({ m00_axi_rvalid }), + .m_axi_rready({ m00_axi_rready }) +); + +endmodule + +`resetall diff --git a/xls/modules/zstd/priority_encoder.v b/xls/modules/zstd/priority_encoder.v new file mode 100644 index 0000000000..cf82512ba8 --- /dev/null +++ b/xls/modules/zstd/priority_encoder.v @@ -0,0 +1,92 @@ +/* + +Copyright (c) 2014-2021 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * Priority encoder module + */ +module priority_encoder # +( + parameter WIDTH = 4, + // LSB priority selection + parameter LSB_HIGH_PRIORITY = 0 +) +( + input wire [WIDTH-1:0] input_unencoded, + output wire output_valid, + output wire [$clog2(WIDTH)-1:0] output_encoded, + output wire [WIDTH-1:0] output_unencoded +); + +parameter LEVELS = WIDTH > 2 ? $clog2(WIDTH) : 1; +parameter W = 2**LEVELS; + +// pad input to even power of two +wire [W-1:0] input_padded = {{W-WIDTH{1'b0}}, input_unencoded}; + +wire [W/2-1:0] stage_valid[LEVELS-1:0]; +wire [W/2-1:0] stage_enc[LEVELS-1:0]; + +generate + genvar l, n; + + // process input bits; generate valid bit and encoded bit for each pair + for (n = 0; n < W/2; n = n + 1) begin : loop_in + assign stage_valid[0][n] = |input_padded[n*2+1:n*2]; + if (LSB_HIGH_PRIORITY) begin + // bit 0 is highest priority + assign stage_enc[0][n] = !input_padded[n*2+0]; + end else begin + // bit 0 is lowest priority + assign stage_enc[0][n] = input_padded[n*2+1]; + end + end + + // compress down to single valid bit and encoded bus + for (l = 1; l < LEVELS; l = l + 1) begin : loop_levels + for (n = 0; n < W/(2*2**l); n = n + 1) begin : loop_compress + assign stage_valid[l][n] = |stage_valid[l-1][n*2+1:n*2]; + if (LSB_HIGH_PRIORITY) begin + // bit 0 is highest priority + assign stage_enc[l][(n+1)*(l+1)-1:n*(l+1)] = stage_valid[l-1][n*2+0] ? {1'b0, stage_enc[l-1][(n*2+1)*l-1:(n*2+0)*l]} : {1'b1, stage_enc[l-1][(n*2+2)*l-1:(n*2+1)*l]}; + end else begin + // bit 0 is lowest priority + assign stage_enc[l][(n+1)*(l+1)-1:n*(l+1)] = stage_valid[l-1][n*2+1] ? {1'b1, stage_enc[l-1][(n*2+2)*l-1:(n*2+1)*l]} : {1'b0, stage_enc[l-1][(n*2+1)*l-1:(n*2+0)*l]}; + end + end + end +endgenerate + +assign output_valid = stage_valid[LEVELS-1]; +assign output_encoded = stage_enc[LEVELS-1]; +assign output_unencoded = 1 << output_encoded; + +endmodule + +`resetall diff --git a/xls/modules/zstd/zstd_dec_cocotb_test.py b/xls/modules/zstd/zstd_dec_cocotb_test.py new file mode 100644 index 0000000000..dc3f5dd447 --- /dev/null +++ b/xls/modules/zstd/zstd_dec_cocotb_test.py @@ -0,0 +1,418 @@ +#!/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. + + +from enum import Enum +from pathlib import Path +import tempfile + +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.axi_master import AxiMaster +from cocotbext.axi.axi_channels import AxiAWBus, AxiWBus, AxiBBus, AxiWriteBus, AxiARBus, AxiRBus, AxiReadBus, AxiBus, AxiBTransaction, AxiBSource, AxiBSink, AxiBMonitor, AxiRTransaction, AxiRSource, AxiRSink, AxiRMonitor +from cocotbext.axi.axi_ram import AxiRam +from cocotbext.axi.sparse_memory import SparseMemory + +import zstandard + +from xls.common import runfiles +from xls.modules.zstd.cocotb.channel import ( + XLSChannel, + XLSChannelDriver, + XLSChannelMonitor, +) +from xls.modules.zstd.cocotb.data_generator import GenerateFrame, BlockType +from xls.modules.zstd.cocotb.memory import init_axi_mem, AxiRamFromFile +from xls.modules.zstd.cocotb.utils import reset, run_test +from xls.modules.zstd.cocotb.xlsstruct import XLSStruct, xls_dataclass + +AXI_DATA_W = 64 +AXI_DATA_W_BYTES = AXI_DATA_W // 8 +MAX_ENCODED_FRAME_SIZE_B = 16384 +NOTIFY_CHANNEL = "notify" +OUTPUT_CHANNEL = "output" +RESET_CHANNEL = "reset" + +# 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 +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 NotifyStruct(XLSStruct): + pass + +@xls_dataclass +class ResetStruct(XLSStruct): + pass + +@xls_dataclass +class OutputStruct(XLSStruct): + data: 64 + length: 32 + last: 1 + +class CSR(Enum): + """ + Maps the offsets to the ZSTD Decoder registers + """ + Status = 0 + Start = 1 + Reset = 2 + InputBuffer = 3 + OutputBuffer = 4 + +class Status(Enum): + """ + Codes for the Status register + """ + IDLE = 0x0 + RUNNING = 0x1 + +def set_termination_event(monitor, event, transactions): + def terminate_cb(_): + if monitor.stats.received_transactions == transactions: + event.set() + monitor.add_callback(terminate_cb) + +def connect_axi_read_bus(dut, name=""): + AXI_AR = "axi_ar" + AXI_R = "axi_r" + + if name != "": + name += "_" + + bus_axi_ar = AxiARBus.from_prefix(dut, name + AXI_AR) + bus_axi_r = AxiRBus.from_prefix(dut, name + AXI_R) + + return AxiReadBus(bus_axi_ar, bus_axi_r) + +def connect_axi_write_bus(dut, name=""): + AXI_AW = "axi_aw" + AXI_W = "axi_w" + AXI_B = "axi_b" + + if name != "": + name += "_" + + bus_axi_aw = AxiAWBus.from_prefix(dut, name + AXI_AW) + bus_axi_w = AxiWBus.from_prefix(dut, name + AXI_W) + bus_axi_b = AxiBBus.from_prefix(dut, name + AXI_B) + + return AxiWriteBus(bus_axi_aw, bus_axi_w, bus_axi_b) + +def connect_axi_bus(dut, name=""): + bus_axi_read = connect_axi_read_bus(dut, name) + bus_axi_write = connect_axi_write_bus(dut, name) + + return AxiBus(bus_axi_write, bus_axi_read) + +def get_decoded_frame_bytes(ifh): + dctx = zstandard.ZstdDecompressor() + return dctx.decompress(ifh.read()) + +def get_decoded_frame_buffer(ifh, address, memory=SparseMemory(size=MAX_ENCODED_FRAME_SIZE_B)): + dctx = zstandard.ZstdDecompressor() + memory.write(address, dctx.decompress(ifh.read())) + return memory + +async def csr_write(cpu, csr, data): + if type(data) is int: + data = data.to_bytes(AXI_DATA_W_BYTES, byteorder='little') + assert len(data) <= AXI_DATA_W_BYTES + await cpu.write(csr.value * AXI_DATA_W_BYTES, data) + +async def csr_read(cpu, csr): + return await cpu.read(csr.value * AXI_DATA_W_BYTES, AXI_DATA_W_BYTES) + +async def test_csr(dut): + + clock = Clock(dut.clk, 10, units="us") + cocotb.start_soon(clock.start()) + + await reset_dut(dut, 50) + + csr_bus = connect_axi_bus(dut, "csr") + + cpu = AxiMaster(csr_bus, dut.clk, dut.rst) + + await ClockCycles(dut.clk, 10) + i = 0 + for reg in CSR: + # Reset CSR tested in a separate test case + if (reg == CSR.Reset): + continue + expected_src = bytearray.fromhex("0DF0AD8BEFBEADDE") + assert len(expected_src) >= AXI_DATA_W_BYTES + expected = expected_src[-AXI_DATA_W_BYTES:] + expected[0] += i + await csr_write(cpu, reg, expected) + read = await csr_read(cpu, reg) + assert read.data == expected, "Expected data doesn't match contents of the {}".format(reg) + i += 1 + await ClockCycles(dut.clk, 10) + +async def test_reset(dut): + clock = Clock(dut.clk, 10, units="us") + cocotb.start_soon(clock.start()) + + await reset_dut(dut, 50) + + (reset_channel, reset_monitor) = connect_xls_channel(dut, RESET_CHANNEL, ResetStruct) + + csr_bus = connect_axi_bus(dut, "csr") + cpu = AxiMaster(csr_bus, dut.clk, dut.rst) + + scoreboard = Scoreboard(dut) + + rst_struct = ResetStruct() + # Expect single reset signal on reset output channel + expected_reset = [rst_struct] + scoreboard.add_interface(reset_monitor, expected_reset) + + await ClockCycles(dut.clk, 10) + await start_decoder(cpu) + timeout = 10 + status = await csr_read(cpu, CSR.Status) + while ((int.from_bytes(status.data, byteorder='little') == Status.IDLE.value) & (timeout != 0)): + status = await csr_read(cpu, CSR.Status) + timeout -= 1 + assert (timeout != 0) + + await csr_write(cpu, CSR.Reset, 0x1) + await wait_for_idle(cpu, 10) + + await ClockCycles(dut.clk, 10) + +async def configure_decoder(cpu, ibuf_addr, obuf_addr): + status = await csr_read(cpu, CSR.Status) + if int.from_bytes(status.data, byteorder='little') != Status.IDLE.value: + await csr_write(cpu, CSR.Reset, 0x1) + await csr_write(cpu, CSR.InputBuffer, ibuf_addr) + await csr_write(cpu, CSR.OutputBuffer, obuf_addr) + +async def start_decoder(cpu): + await csr_write(cpu, CSR.Start, 0x1) + +async def wait_for_idle(cpu, timeout=100): + status = await csr_read(cpu, CSR.Status) + while ((int.from_bytes(status.data, byteorder='little') != Status.IDLE.value) & (timeout != 0)): + status = await csr_read(cpu, CSR.Status) + timeout -= 1 + assert (timeout != 0) + +def generate_expected_output(decoded_frame): + packets = [] + frame_len = len(decoded_frame) + last_len = frame_len % 8 + for i in range(frame_len // 8): + start_id = i * 8 + end_id = start_id + 8 + packet_data = int.from_bytes(decoded_frame[start_id:end_id], byteorder='little') + last_packet = (end_id==frame_len) + packet = OutputStruct(data=packet_data, length=64, last=last_packet) + packets.append(packet) + if (last_len): + packet_data = int.from_bytes(decoded_frame[-last_len:], byteorder='little') + packet = OutputStruct(data=packet_data, length=last_len*8, last=True) + packets.append(packet) + return packets + +async def reset_dut(dut, rst_len=10): + dut.rst.setimmediatevalue(0) + await ClockCycles(dut.clk, rst_len) + dut.rst.setimmediatevalue(1) + await ClockCycles(dut.clk, rst_len) + dut.rst.setimmediatevalue(0) + +def connect_xls_channel(dut, channel_name, xls_struct): + channel = XLSChannel(dut, channel_name, dut.clk, start_now=True) + monitor = XLSChannelMonitor(dut, channel_name, dut.clk, xls_struct) + + return (channel, monitor) + +def prepare_test_environment(dut): + clock = Clock(dut.clk, 10, units="us") + cocotb.start_soon(clock.start()) + + scoreboard = Scoreboard(dut) + + memory_bus = connect_axi_bus(dut, "memory") + csr_bus = connect_axi_bus(dut, "csr") + axi_buses = { + "memory": memory_bus, + "csr": csr_bus + } + + notify = connect_xls_channel(dut, NOTIFY_CHANNEL, NotifyStruct) + output = connect_xls_channel(dut, OUTPUT_CHANNEL, OutputStruct) + xls_channels = { + "notify": notify, + "output": output + } + + cpu = AxiMaster(csr_bus, dut.clk, dut.rst) + + return (scoreboard, axi_buses, xls_channels, cpu) + +async def test_decoder(dut, seed, block_type, scoreboard, axi_buses, xls_channels, cpu): + memory_bus = axi_buses["memory"] + csr_bus = axi_buses["csr"] + (notify_channel, notify_monitor) = xls_channels[NOTIFY_CHANNEL] + (output_channel, output_monitor) = xls_channels[OUTPUT_CHANNEL] + + assert_notify = Event() + set_termination_event(notify_monitor, assert_notify, 1) + + mem_size = MAX_ENCODED_FRAME_SIZE_B + ibuf_addr = 0x0 + obuf_addr = mem_size // 2 + + #FIXME: use delete_on_close=False after moving to python 3.12 + with tempfile.NamedTemporaryFile(delete=False) as encoded: + await reset_dut(dut, 50) + + # Generate ZSTD frame to temporary file + GenerateFrame(seed, block_type, encoded.name) + + expected_decoded_frame = get_decoded_frame_bytes(encoded) + encoded.close() + expected_output_packets = generate_expected_output(expected_decoded_frame) + + assert_expected_output = Event() + set_termination_event(output_monitor, assert_expected_output, len(expected_output_packets)) + # Monitor stream output for packets with the decoded ZSTD frame + scoreboard.add_interface(output_monitor, expected_output_packets) + + # Initialise testbench memory with generated ZSTD frame + memory = AxiRamFromFile(bus=memory_bus, clock=dut.clk, reset=dut.rst, path=encoded.name, size=mem_size) + + await configure_decoder(cpu, ibuf_addr, obuf_addr) + await start_decoder(cpu) + await assert_expected_output.wait() + await wait_for_idle(cpu) + # TODO: Check decoded frame in memory under `obuf_addr` when ZstdDecoder + # will fully support memory output interface + + await assert_notify.wait() + await ClockCycles(dut.clk, 20) + +@cocotb.test(timeout_time=50, timeout_unit="ms") +async def zstd_csr_test(dut): + await test_csr(dut) + +@cocotb.test(timeout_time=50, timeout_unit="ms") +async def zstd_reset_test(dut): + await test_reset(dut) + +#FIXME: Rework testbench to decode multiple ZSTD frames in one test +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_raw_frames_test_1(dut): + block_type = BlockType.RAW + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 1, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_raw_frames_test_2(dut): + block_type = BlockType.RAW + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 2, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_raw_frames_test_3(dut): + block_type = BlockType.RAW + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 3, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_raw_frames_test_4(dut): + block_type = BlockType.RAW + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 4, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_raw_frames_test_5(dut): + block_type = BlockType.RAW + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 5, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_rle_frames_test_1(dut): + block_type = BlockType.RLE + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 1, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_rle_frames_test_2(dut): + block_type = BlockType.RLE + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 2, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_rle_frames_test_3(dut): + block_type = BlockType.RLE + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 3, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_rle_frames_test_4(dut): + block_type = BlockType.RLE + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 4, block_type, scoreboard, axi_buses, xls_channels, cpu) + +@cocotb.test(timeout_time=20000, timeout_unit="ms") +async def zstd_rle_frames_test_5(dut): + block_type = BlockType.RLE + (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 5, block_type, scoreboard, axi_buses, xls_channels, cpu) + +#@cocotb.test(timeout_time=20000, timeout_unit="ms") +#async def zstd_compressed_frames_test(dut): +# test_cases = 1 +# block_type = BlockType.COMPRESSED +# await test_decoder(dut, test_cases, block_type) +# +#@cocotb.test(timeout_time=20000, timeout_unit="ms") +#async def zstd_random_frames_test(dut): +# test_cases = 1 +# block_type = BlockType.RANDOM +# await test_decoder(dut, test_cases, block_type) + +if __name__ == "__main__": + toplevel = "zstd_dec_wrapper" + verilog_sources = [ + "xls/modules/zstd/zstd_dec.v", + "xls/modules/zstd/xls_fifo_wrapper.v", + "xls/modules/zstd/zstd_dec_wrapper.v", + "xls/modules/zstd/axi_interconnect_wrapper.v", + "xls/modules/zstd/axi_interconnect.v", + "xls/modules/zstd/arbiter.v", + "xls/modules/zstd/priority_encoder.v", + ] + test_module=[Path(__file__).stem] + run_test(toplevel, test_module, verilog_sources) diff --git a/xls/modules/zstd/zstd_dec_wrapper.v b/xls/modules/zstd/zstd_dec_wrapper.v new file mode 100644 index 0000000000..304c5f061b --- /dev/null +++ b/xls/modules/zstd/zstd_dec_wrapper.v @@ -0,0 +1,816 @@ +// 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 zstd_dec_wrapper #( + parameter AXI_DATA_W = 64, + parameter AXI_ADDR_W = 16, + parameter AXI_ID_W = 4, + parameter AXI_STRB_W = 8, + parameter AWUSER_WIDTH = 1, + parameter WUSER_WIDTH = 1, + parameter BUSER_WIDTH = 1, + parameter ARUSER_WIDTH = 1, + parameter RUSER_WIDTH = 1, + parameter OUTPUT_WIDTH = 97 +) ( + input wire clk, + input wire rst, + + // AXI Master interface for the memory connection + output wire [AXI_ID_W-1:0] memory_axi_aw_awid, + output wire [AXI_ADDR_W-1:0] memory_axi_aw_awaddr, + output wire [7:0] memory_axi_aw_awlen, + output wire [2:0] memory_axi_aw_awsize, + output wire [1:0] memory_axi_aw_awburst, + output wire memory_axi_aw_awlock, + output wire [3:0] memory_axi_aw_awcache, + output wire [2:0] memory_axi_aw_awprot, + output wire [3:0] memory_axi_aw_awqos, + output wire [3:0] memory_axi_aw_awregion, + output wire [AWUSER_WIDTH-1:0] memory_axi_aw_awuser, + output wire memory_axi_aw_awvalid, + input wire memory_axi_aw_awready, + output wire [AXI_DATA_W-1:0] memory_axi_w_wdata, + output wire [AXI_STRB_W-1:0] memory_axi_w_wstrb, + output wire memory_axi_w_wlast, + output wire [WUSER_WIDTH-1:0] memory_axi_w_wuser, + output wire memory_axi_w_wvalid, + input wire memory_axi_w_wready, + input wire [AXI_ID_W-1:0] memory_axi_b_bid, + input wire [2:0] memory_axi_b_bresp, + input wire [BUSER_WIDTH-1:0] memory_axi_b_buser, + input wire memory_axi_b_bvalid, + output wire memory_axi_b_bready, + output wire [AXI_ID_W-1:0] memory_axi_ar_arid, + output wire [AXI_ADDR_W-1:0] memory_axi_ar_araddr, + output wire [7:0] memory_axi_ar_arlen, + output wire [2:0] memory_axi_ar_arsize, + output wire [1:0] memory_axi_ar_arburst, + output wire memory_axi_ar_arlock, + output wire [3:0] memory_axi_ar_arcache, + output wire [2:0] memory_axi_ar_arprot, + output wire [3:0] memory_axi_ar_arqos, + output wire [3:0] memory_axi_ar_arregion, + output wire [ARUSER_WIDTH-1:0] memory_axi_ar_aruser, + output wire memory_axi_ar_arvalid, + input wire memory_axi_ar_arready, + input wire [AXI_ID_W-1:0] memory_axi_r_rid, + input wire [AXI_DATA_W-1:0] memory_axi_r_rdata, + input wire [2:0] memory_axi_r_rresp, + input wire memory_axi_r_rlast, + input wire [RUSER_WIDTH-1:0] memory_axi_r_ruser, + input wire memory_axi_r_rvalid, + output wire memory_axi_r_rready, + + // AXI Slave interface for the CSR access + input wire [AXI_ID_W-1:0] csr_axi_aw_awid, + input wire [AXI_ADDR_W-1:0] csr_axi_aw_awaddr, + input wire [7:0] csr_axi_aw_awlen, + input wire [2:0] csr_axi_aw_awsize, + input wire [1:0] csr_axi_aw_awburst, + input wire csr_axi_aw_awlock, + input wire [3:0] csr_axi_aw_awcache, + input wire [2:0] csr_axi_aw_awprot, + input wire [3:0] csr_axi_aw_awqos, + input wire [3:0] csr_axi_aw_awregion, + input wire [AWUSER_WIDTH-1:0] csr_axi_aw_awuser, + input wire csr_axi_aw_awvalid, + output wire csr_axi_aw_awready, + input wire [AXI_DATA_W-1:0] csr_axi_w_wdata, + input wire [AXI_STRB_W-1:0] csr_axi_w_wstrb, + input wire csr_axi_w_wlast, + input wire [WUSER_WIDTH-1:0] csr_axi_w_wuser, + input wire csr_axi_w_wvalid, + output wire csr_axi_w_wready, + output wire [AXI_ID_W-1:0] csr_axi_b_bid, + output wire [2:0] csr_axi_b_bresp, + output wire [BUSER_WIDTH-1:0] csr_axi_b_buser, + output wire csr_axi_b_bvalid, + input wire csr_axi_b_bready, + input wire [AXI_ID_W-1:0] csr_axi_ar_arid, + input wire [AXI_ADDR_W-1:0] csr_axi_ar_araddr, + input wire [7:0] csr_axi_ar_arlen, + input wire [2:0] csr_axi_ar_arsize, + input wire [1:0] csr_axi_ar_arburst, + input wire csr_axi_ar_arlock, + input wire [3:0] csr_axi_ar_arcache, + input wire [2:0] csr_axi_ar_arprot, + input wire [3:0] csr_axi_ar_arqos, + input wire [3:0] csr_axi_ar_arregion, + input wire [ARUSER_WIDTH-1:0] csr_axi_ar_aruser, + input wire csr_axi_ar_arvalid, + output wire csr_axi_ar_arready, + output wire [AXI_ID_W-1:0] csr_axi_r_rid, + output wire [AXI_DATA_W-1:0] csr_axi_r_rdata, + output wire [2:0] csr_axi_r_rresp, + output wire csr_axi_r_rlast, + output wire [RUSER_WIDTH-1:0] csr_axi_r_ruser, + output wire csr_axi_r_rvalid, + input wire csr_axi_r_rready, + + output wire notify_data, + output wire notify_vld, + input wire notify_rdy, + + output wire [OUTPUT_WIDTH-1:0] output_data, + output wire output_vld, + input wire output_rdy +); + + /* + * Reset loopback + */ + wire reset_vld; + wire reset_rdy; + // Required for monitoring simple XLS channel in cocotb + wire reset_data; + // OR-ed generic reset and loopback reset in response to write to RESET CSR + wire reset; + + /* + * MemReader AXI interfaces + */ + // RawBlockDecoder + wire raw_block_decoder_axi_ar_arvalid; + wire raw_block_decoder_axi_ar_arready; + wire [ AXI_ID_W-1:0] raw_block_decoder_axi_ar_arid; + wire [AXI_ADDR_W-1:0] raw_block_decoder_axi_ar_araddr; + wire [ 3:0] raw_block_decoder_axi_ar_arregion; + wire [ 7:0] raw_block_decoder_axi_ar_arlen; + wire [ 2:0] raw_block_decoder_axi_ar_arsize; + wire [ 1:0] raw_block_decoder_axi_ar_arburst; + wire [ 3:0] raw_block_decoder_axi_ar_arcache; + wire [ 2:0] raw_block_decoder_axi_ar_arprot; + wire [ 3:0] raw_block_decoder_axi_ar_arqos; + + wire raw_block_decoder_axi_r_rvalid; + wire raw_block_decoder_axi_r_rready; + wire [ AXI_ID_W-1:0] raw_block_decoder_axi_r_rid; + wire [AXI_DATA_W-1:0] raw_block_decoder_axi_r_rdata; + wire [ 2:0] raw_block_decoder_axi_r_rresp; + wire raw_block_decoder_axi_r_rlast; + + + // BlockHeaderDecoder + wire block_header_decoder_axi_ar_arvalid; + wire block_header_decoder_axi_ar_arready; + wire [ AXI_ID_W-1:0] block_header_decoder_axi_ar_arid; + wire [AXI_ADDR_W-1:0] block_header_decoder_axi_ar_araddr; + wire [ 3:0] block_header_decoder_axi_ar_arregion; + wire [ 7:0] block_header_decoder_axi_ar_arlen; + wire [ 2:0] block_header_decoder_axi_ar_arsize; + wire [ 1:0] block_header_decoder_axi_ar_arburst; + wire [ 3:0] block_header_decoder_axi_ar_arcache; + wire [ 2:0] block_header_decoder_axi_ar_arprot; + wire [ 3:0] block_header_decoder_axi_ar_arqos; + + wire block_header_decoder_axi_r_rvalid; + wire block_header_decoder_axi_r_rready; + wire [ AXI_ID_W-1:0] block_header_decoder_axi_r_rid; + wire [AXI_DATA_W-1:0] block_header_decoder_axi_r_rdata; + wire [ 2:0] block_header_decoder_axi_r_rresp; + wire block_header_decoder_axi_r_rlast; + + + // FrameHeaderDecoder + wire frame_header_decoder_axi_ar_arvalid; + wire frame_header_decoder_axi_ar_arready; + wire [ AXI_ID_W-1:0] frame_header_decoder_axi_ar_arid; + wire [AXI_ADDR_W-1:0] frame_header_decoder_axi_ar_araddr; + wire [ 3:0] frame_header_decoder_axi_ar_arregion; + wire [ 7:0] frame_header_decoder_axi_ar_arlen; + wire [ 2:0] frame_header_decoder_axi_ar_arsize; + wire [ 1:0] frame_header_decoder_axi_ar_arburst; + wire [ 3:0] frame_header_decoder_axi_ar_arcache; + wire [ 2:0] frame_header_decoder_axi_ar_arprot; + wire [ 3:0] frame_header_decoder_axi_ar_arqos; + + wire frame_header_decoder_axi_r_rvalid; + wire frame_header_decoder_axi_r_rready; + wire [ AXI_ID_W-1:0] frame_header_decoder_axi_r_rid; + wire [AXI_DATA_W-1:0] frame_header_decoder_axi_r_rdata; + wire [ 2:0] frame_header_decoder_axi_r_rresp; + wire frame_header_decoder_axi_r_rlast; + + + /* + * MemWriter AXI interfaces + */ + + // SequenceExecutor + wire [ AXI_ID_W-1:0] sequence_executor_axi_aw_awid; + wire [AXI_ADDR_W-1:0] sequence_executor_axi_aw_awaddr; + wire [ 2:0] sequence_executor_axi_aw_awsize; + wire [ 7:0] sequence_executor_axi_aw_awlen; + wire [ 1:0] sequence_executor_axi_aw_awburst; + wire sequence_executor_axi_aw_awvalid; + wire sequence_executor_axi_aw_awready; + + wire [AXI_DATA_W-1:0] sequence_executor_axi_w_wdata; + wire [AXI_STRB_W-1:0] sequence_executor_axi_w_wstrb; + wire sequence_executor_axi_w_wlast; + wire sequence_executor_axi_w_wvalid; + wire sequence_executor_axi_w_wready; + + wire [ AXI_ID_W-1:0] sequence_executor_axi_b_bid; + wire [ 2:0] sequence_executor_axi_b_bresp; + wire sequence_executor_axi_b_bvalid; + wire sequence_executor_axi_b_bready; + + /* + * XLS Channels representing AXI interfaces + */ + + localparam XLS_AXI_AW_W = AXI_ADDR_W + AXI_ID_W + 3 + 2 + 8; + localparam XLS_AXI_W_W = AXI_DATA_W + AXI_STRB_W + 1; + localparam XLS_AXI_B_W = 3 + AXI_ID_W; + localparam XLS_AXI_AR_W = AXI_ID_W + AXI_ADDR_W + 4 + 8 + 3 + 2 + 4 + 3 + 4; + localparam XLS_AXI_R_W = AXI_ID_W + AXI_DATA_W + 3 + 1; + // CSR + wire [XLS_AXI_AW_W-1:0] zstd_dec__csr_axi_aw; + wire zstd_dec__csr_axi_aw_rdy; + wire zstd_dec__csr_axi_aw_vld; + wire [XLS_AXI_W_W-1:0] zstd_dec__csr_axi_w; + wire zstd_dec__csr_axi_w_rdy; + wire zstd_dec__csr_axi_w_vld; + wire [ XLS_AXI_B_W-1:0] zstd_dec__csr_axi_b; + wire zstd_dec__csr_axi_b_rdy; + wire zstd_dec__csr_axi_b_vld; + wire [XLS_AXI_AR_W-1:0] zstd_dec__csr_axi_ar; + wire zstd_dec__csr_axi_ar_rdy; + wire zstd_dec__csr_axi_ar_vld; + wire [ XLS_AXI_R_W-1:0] zstd_dec__csr_axi_r; + wire zstd_dec__csr_axi_r_rdy; + wire zstd_dec__csr_axi_r_vld; + + // Frame Header Decoder + wire [XLS_AXI_AR_W-1:0] zstd_dec__fh_axi_ar; + wire zstd_dec__fh_axi_ar_rdy; + wire zstd_dec__fh_axi_ar_vld; + wire [ XLS_AXI_R_W-1:0] zstd_dec__fh_axi_r; + wire zstd_dec__fh_axi_r_rdy; + wire zstd_dec__fh_axi_r_vld; + + // Block Header Decoder + wire [XLS_AXI_AR_W-1:0] zstd_dec__bh_axi_ar; + wire zstd_dec__bh_axi_ar_rdy; + wire zstd_dec__bh_axi_ar_vld; + wire [ XLS_AXI_R_W-1:0] zstd_dec__bh_axi_r; + wire zstd_dec__bh_axi_r_rdy; + wire zstd_dec__bh_axi_r_vld; + + // Raw Block Decoder + wire [XLS_AXI_AR_W-1:0] zstd_dec__raw_axi_ar; + wire zstd_dec__raw_axi_ar_rdy; + wire zstd_dec__raw_axi_ar_vld; + wire [ XLS_AXI_R_W-1:0] zstd_dec__raw_axi_r; + wire zstd_dec__raw_axi_r_rdy; + wire zstd_dec__raw_axi_r_vld; + + // Output packet + wire [63:0] output_data_data_field; + wire [31:0] output_data_length_field; + wire [0:0] output_data_last_field; + + /* + * Mapping XLS Channels to AXI channels fields + */ + + // CSR + assign zstd_dec__csr_axi_aw = { + csr_axi_aw_awid, + csr_axi_aw_awaddr, + csr_axi_aw_awsize, + csr_axi_aw_awlen, + csr_axi_aw_awburst + }; + assign zstd_dec__csr_axi_aw_vld = csr_axi_aw_awvalid; + assign csr_axi_aw_awready = zstd_dec__csr_axi_aw_rdy; + assign zstd_dec__csr_axi_w = { + csr_axi_w_wdata, + csr_axi_w_wstrb, + csr_axi_w_wlast + }; + assign zstd_dec__csr_axi_w_vld = csr_axi_w_wvalid; + assign csr_axi_w_wready = zstd_dec__csr_axi_w_rdy; + assign { + csr_axi_b_bresp, + csr_axi_b_bid + } = zstd_dec__csr_axi_b; + assign csr_axi_b_bvalid = zstd_dec__csr_axi_b_vld; + assign zstd_dec__csr_axi_b_rdy = csr_axi_b_bready; + assign zstd_dec__csr_axi_ar = { + csr_axi_ar_arid, + csr_axi_ar_araddr, + csr_axi_ar_arregion, + csr_axi_ar_arlen, + csr_axi_ar_arsize, + csr_axi_ar_arburst, + csr_axi_ar_arcache, + csr_axi_ar_arprot, + csr_axi_ar_arqos + }; + assign zstd_dec__csr_axi_ar_vld = csr_axi_ar_arvalid; + assign csr_axi_ar_arready = zstd_dec__csr_axi_ar_rdy; + assign { + csr_axi_r_rid, + csr_axi_r_rdata, + csr_axi_r_rresp, + csr_axi_r_rlast + } = zstd_dec__csr_axi_r; + assign csr_axi_r_rvalid = zstd_dec__csr_axi_r_vld; + assign zstd_dec__csr_axi_r_rdy = csr_axi_r_rready; + + // Frame Header Decoder + assign { + frame_header_decoder_axi_ar_arid, + frame_header_decoder_axi_ar_araddr, + frame_header_decoder_axi_ar_arregion, + frame_header_decoder_axi_ar_arlen, + frame_header_decoder_axi_ar_arsize, + frame_header_decoder_axi_ar_arburst, + frame_header_decoder_axi_ar_arcache, + frame_header_decoder_axi_ar_arprot, + frame_header_decoder_axi_ar_arqos + } = zstd_dec__fh_axi_ar; + assign frame_header_decoder_axi_ar_arvalid = zstd_dec__fh_axi_ar_vld; + assign zstd_dec__fh_axi_ar_rdy = frame_header_decoder_axi_ar_arready; + assign zstd_dec__fh_axi_r = { + frame_header_decoder_axi_r_rid, + frame_header_decoder_axi_r_rdata, + frame_header_decoder_axi_r_rresp, + frame_header_decoder_axi_r_rlast}; + assign zstd_dec__fh_axi_r_vld = frame_header_decoder_axi_r_rvalid; + assign frame_header_decoder_axi_r_rready = zstd_dec__fh_axi_r_rdy; + + // Block Header Decoder + assign { + block_header_decoder_axi_ar_arid, + block_header_decoder_axi_ar_araddr, + block_header_decoder_axi_ar_arregion, + block_header_decoder_axi_ar_arlen, + block_header_decoder_axi_ar_arsize, + block_header_decoder_axi_ar_arburst, + block_header_decoder_axi_ar_arcache, + block_header_decoder_axi_ar_arprot, + block_header_decoder_axi_ar_arqos + } = zstd_dec__bh_axi_ar; + assign block_header_decoder_axi_ar_arvalid = zstd_dec__bh_axi_ar_vld; + assign zstd_dec__bh_axi_ar_rdy = block_header_decoder_axi_ar_arready; + assign zstd_dec__bh_axi_r = { + block_header_decoder_axi_r_rid, + block_header_decoder_axi_r_rdata, + block_header_decoder_axi_r_rresp, + block_header_decoder_axi_r_rlast}; + assign zstd_dec__bh_axi_r_vld = block_header_decoder_axi_r_rvalid; + assign block_header_decoder_axi_r_rready = zstd_dec__bh_axi_r_rdy; + + // Raw Block Decoder + assign { + raw_block_decoder_axi_ar_arid, + raw_block_decoder_axi_ar_araddr, + raw_block_decoder_axi_ar_arregion, + raw_block_decoder_axi_ar_arlen, + raw_block_decoder_axi_ar_arsize, + raw_block_decoder_axi_ar_arburst, + raw_block_decoder_axi_ar_arcache, + raw_block_decoder_axi_ar_arprot, + raw_block_decoder_axi_ar_arqos + } = zstd_dec__raw_axi_ar; + assign raw_block_decoder_axi_ar_arvalid = zstd_dec__raw_axi_ar_vld; + assign zstd_dec__raw_axi_ar_rdy = raw_block_decoder_axi_ar_arready; + assign zstd_dec__raw_axi_r = { + raw_block_decoder_axi_r_rid, + raw_block_decoder_axi_r_rdata, + raw_block_decoder_axi_r_rresp, + raw_block_decoder_axi_r_rlast}; + assign zstd_dec__raw_axi_r_vld = raw_block_decoder_axi_r_rvalid; + assign raw_block_decoder_axi_r_rready = zstd_dec__raw_axi_r_rdy; + + assign csr_axi_b_buser = 1'b0; + assign csr_axi_r_ruser = 1'b0; + assign notify_data = notify_vld; + assign reset_data = reset_vld; + assign reset = reset_vld | rst; + + assign { + output_data_data_field, + output_data_length_field, + output_data_last_field + } = output_data; + + /* + * ZSTD Decoder instance + */ + ZstdDecoder ZstdDecoder ( + .clk(clk), + .rst(reset), + + // CSR Interface + .zstd_dec__csr_axi_aw_r(zstd_dec__csr_axi_aw), + .zstd_dec__csr_axi_aw_r_vld(zstd_dec__csr_axi_aw_vld), + .zstd_dec__csr_axi_aw_r_rdy(zstd_dec__csr_axi_aw_rdy), + .zstd_dec__csr_axi_w_r(zstd_dec__csr_axi_w), + .zstd_dec__csr_axi_w_r_vld(zstd_dec__csr_axi_w_vld), + .zstd_dec__csr_axi_w_r_rdy(zstd_dec__csr_axi_w_rdy), + .zstd_dec__csr_axi_b_s(zstd_dec__csr_axi_b), + .zstd_dec__csr_axi_b_s_vld(zstd_dec__csr_axi_b_vld), + .zstd_dec__csr_axi_b_s_rdy(zstd_dec__csr_axi_b_rdy), + .zstd_dec__csr_axi_ar_r(zstd_dec__csr_axi_ar), + .zstd_dec__csr_axi_ar_r_vld(zstd_dec__csr_axi_ar_vld), + .zstd_dec__csr_axi_ar_r_rdy(zstd_dec__csr_axi_ar_rdy), + .zstd_dec__csr_axi_r_s(zstd_dec__csr_axi_r), + .zstd_dec__csr_axi_r_s_vld(zstd_dec__csr_axi_r_vld), + .zstd_dec__csr_axi_r_s_rdy(zstd_dec__csr_axi_r_rdy), + + // FrameHeaderDecoder + .zstd_dec__fh_axi_ar_s(zstd_dec__fh_axi_ar), + .zstd_dec__fh_axi_ar_s_vld(zstd_dec__fh_axi_ar_vld), + .zstd_dec__fh_axi_ar_s_rdy(zstd_dec__fh_axi_ar_rdy), + .zstd_dec__fh_axi_r_r(zstd_dec__fh_axi_r), + .zstd_dec__fh_axi_r_r_vld(zstd_dec__fh_axi_r_vld), + .zstd_dec__fh_axi_r_r_rdy(zstd_dec__fh_axi_r_rdy), + + // BlockHeaderDecoder + .zstd_dec__bh_axi_ar_s(zstd_dec__bh_axi_ar), + .zstd_dec__bh_axi_ar_s_vld(zstd_dec__bh_axi_ar_vld), + .zstd_dec__bh_axi_ar_s_rdy(zstd_dec__bh_axi_ar_rdy), + .zstd_dec__bh_axi_r_r(zstd_dec__bh_axi_r), + .zstd_dec__bh_axi_r_r_vld(zstd_dec__bh_axi_r_vld), + .zstd_dec__bh_axi_r_r_rdy(zstd_dec__bh_axi_r_rdy), + + // RawBlockDecoder + .zstd_dec__raw_axi_ar_s(zstd_dec__raw_axi_ar), + .zstd_dec__raw_axi_ar_s_vld(zstd_dec__raw_axi_ar_vld), + .zstd_dec__raw_axi_ar_s_rdy(zstd_dec__raw_axi_ar_rdy), + .zstd_dec__raw_axi_r_r(zstd_dec__raw_axi_r), + .zstd_dec__raw_axi_r_r_vld(zstd_dec__raw_axi_r_vld), + .zstd_dec__raw_axi_r_r_rdy(zstd_dec__raw_axi_r_rdy), + + // Other ports + .zstd_dec__notify_s_vld(notify_vld), + .zstd_dec__notify_s_rdy(notify_rdy), + .zstd_dec__output_s(output_data), + .zstd_dec__output_s_vld(output_vld), + .zstd_dec__output_s_rdy(output_rdy), + // Reset loopback - response for write to RESET CSR + // Should be looped back to generic reset input + .zstd_dec__reset_s_vld(reset_vld), + .zstd_dec__reset_s_rdy(reset_rdy), + + .zstd_dec__ram_rd_req_0_s(), + .zstd_dec__ram_rd_req_1_s(), + .zstd_dec__ram_rd_req_2_s(), + .zstd_dec__ram_rd_req_3_s(), + .zstd_dec__ram_rd_req_4_s(), + .zstd_dec__ram_rd_req_5_s(), + .zstd_dec__ram_rd_req_6_s(), + .zstd_dec__ram_rd_req_7_s(), + .zstd_dec__ram_rd_req_0_s_vld(), + .zstd_dec__ram_rd_req_1_s_vld(), + .zstd_dec__ram_rd_req_2_s_vld(), + .zstd_dec__ram_rd_req_3_s_vld(), + .zstd_dec__ram_rd_req_4_s_vld(), + .zstd_dec__ram_rd_req_5_s_vld(), + .zstd_dec__ram_rd_req_6_s_vld(), + .zstd_dec__ram_rd_req_7_s_vld(), + .zstd_dec__ram_rd_req_0_s_rdy('1), + .zstd_dec__ram_rd_req_1_s_rdy('1), + .zstd_dec__ram_rd_req_2_s_rdy('1), + .zstd_dec__ram_rd_req_3_s_rdy('1), + .zstd_dec__ram_rd_req_4_s_rdy('1), + .zstd_dec__ram_rd_req_5_s_rdy('1), + .zstd_dec__ram_rd_req_6_s_rdy('1), + .zstd_dec__ram_rd_req_7_s_rdy('1), + + .zstd_dec__ram_rd_resp_0_r('0), + .zstd_dec__ram_rd_resp_1_r('0), + .zstd_dec__ram_rd_resp_2_r('0), + .zstd_dec__ram_rd_resp_3_r('0), + .zstd_dec__ram_rd_resp_4_r('0), + .zstd_dec__ram_rd_resp_5_r('0), + .zstd_dec__ram_rd_resp_6_r('0), + .zstd_dec__ram_rd_resp_7_r('0), + .zstd_dec__ram_rd_resp_0_r_vld('1), + .zstd_dec__ram_rd_resp_1_r_vld('1), + .zstd_dec__ram_rd_resp_2_r_vld('1), + .zstd_dec__ram_rd_resp_3_r_vld('1), + .zstd_dec__ram_rd_resp_4_r_vld('1), + .zstd_dec__ram_rd_resp_5_r_vld('1), + .zstd_dec__ram_rd_resp_6_r_vld('1), + .zstd_dec__ram_rd_resp_7_r_vld('1), + .zstd_dec__ram_rd_resp_0_r_rdy(), + .zstd_dec__ram_rd_resp_1_r_rdy(), + .zstd_dec__ram_rd_resp_2_r_rdy(), + .zstd_dec__ram_rd_resp_3_r_rdy(), + .zstd_dec__ram_rd_resp_4_r_rdy(), + .zstd_dec__ram_rd_resp_5_r_rdy(), + .zstd_dec__ram_rd_resp_6_r_rdy(), + .zstd_dec__ram_rd_resp_7_r_rdy(), + + .zstd_dec__ram_wr_req_0_s(), + .zstd_dec__ram_wr_req_1_s(), + .zstd_dec__ram_wr_req_2_s(), + .zstd_dec__ram_wr_req_3_s(), + .zstd_dec__ram_wr_req_4_s(), + .zstd_dec__ram_wr_req_5_s(), + .zstd_dec__ram_wr_req_6_s(), + .zstd_dec__ram_wr_req_7_s(), + .zstd_dec__ram_wr_req_0_s_vld(), + .zstd_dec__ram_wr_req_1_s_vld(), + .zstd_dec__ram_wr_req_2_s_vld(), + .zstd_dec__ram_wr_req_3_s_vld(), + .zstd_dec__ram_wr_req_4_s_vld(), + .zstd_dec__ram_wr_req_5_s_vld(), + .zstd_dec__ram_wr_req_6_s_vld(), + .zstd_dec__ram_wr_req_7_s_vld(), + .zstd_dec__ram_wr_req_0_s_rdy('1), + .zstd_dec__ram_wr_req_1_s_rdy('1), + .zstd_dec__ram_wr_req_2_s_rdy('1), + .zstd_dec__ram_wr_req_3_s_rdy('1), + .zstd_dec__ram_wr_req_4_s_rdy('1), + .zstd_dec__ram_wr_req_5_s_rdy('1), + .zstd_dec__ram_wr_req_6_s_rdy('1), + .zstd_dec__ram_wr_req_7_s_rdy('1), + + .zstd_dec__ram_wr_resp_0_r_vld('1), + .zstd_dec__ram_wr_resp_1_r_vld('1), + .zstd_dec__ram_wr_resp_2_r_vld('1), + .zstd_dec__ram_wr_resp_3_r_vld('1), + .zstd_dec__ram_wr_resp_4_r_vld('1), + .zstd_dec__ram_wr_resp_5_r_vld('1), + .zstd_dec__ram_wr_resp_6_r_vld('1), + .zstd_dec__ram_wr_resp_7_r_vld('1), + .zstd_dec__ram_wr_resp_0_r_rdy(), + .zstd_dec__ram_wr_resp_1_r_rdy(), + .zstd_dec__ram_wr_resp_2_r_rdy(), + .zstd_dec__ram_wr_resp_3_r_rdy(), + .zstd_dec__ram_wr_resp_4_r_rdy(), + .zstd_dec__ram_wr_resp_5_r_rdy(), + .zstd_dec__ram_wr_resp_6_r_rdy(), + .zstd_dec__ram_wr_resp_7_r_rdy() + ); + + assign frame_header_decoder_axi_r_rresp[2] = '0; + assign block_header_decoder_axi_r_rresp[2] = '0; + assign raw_block_decoder_axi_r_rresp[2] = '0; + assign sequence_executor_axi_b_bresp[2] = '0; + assign memory_axi_b_bresp[2] = '0; + assign memory_axi_r_rresp[2] = '0; + /* + * AXI Interconnect + */ + axi_interconnect_wrapper #( + .DATA_WIDTH(AXI_DATA_W), + .ADDR_WIDTH(AXI_ADDR_W), + .M00_ADDR_WIDTH(AXI_ADDR_W), + .M00_BASE_ADDR(32'd0), + .STRB_WIDTH(AXI_STRB_W), + .ID_WIDTH(AXI_ID_W) + ) axi_memory_interconnect ( + .clk(clk), + .rst(rst), + + /* + * AXI slave interfaces + */ + // FrameHeaderDecoder + .s00_axi_awid('0), + .s00_axi_awaddr('0), + .s00_axi_awlen('0), + .s00_axi_awsize('0), + .s00_axi_awburst('0), + .s00_axi_awlock('0), + .s00_axi_awcache('0), + .s00_axi_awprot('0), + .s00_axi_awqos('0), + .s00_axi_awuser('0), + .s00_axi_awvalid('0), + .s00_axi_awready(), + .s00_axi_wdata('0), + .s00_axi_wstrb('0), + .s00_axi_wlast('0), + .s00_axi_wuser('0), + .s00_axi_wvalid(), + .s00_axi_wready(), + .s00_axi_bid(), + .s00_axi_bresp(), + .s00_axi_buser(), + .s00_axi_bvalid(), + .s00_axi_bready('0), + .s00_axi_arid(frame_header_decoder_axi_ar_arid), + .s00_axi_araddr(frame_header_decoder_axi_ar_araddr), + .s00_axi_arlen(frame_header_decoder_axi_ar_arlen), + .s00_axi_arsize(frame_header_decoder_axi_ar_arsize), + .s00_axi_arburst(frame_header_decoder_axi_ar_arburst), + .s00_axi_arlock('0), + .s00_axi_arcache(frame_header_decoder_axi_ar_arcache), + .s00_axi_arprot(frame_header_decoder_axi_ar_arprot), + .s00_axi_arqos(frame_header_decoder_axi_ar_arqos), + .s00_axi_aruser('0), + .s00_axi_arvalid(frame_header_decoder_axi_ar_arvalid), + .s00_axi_arready(frame_header_decoder_axi_ar_arready), + .s00_axi_rid(frame_header_decoder_axi_r_rid), + .s00_axi_rdata(frame_header_decoder_axi_r_rdata), + .s00_axi_rresp(frame_header_decoder_axi_r_rresp[1:0]), + .s00_axi_rlast(frame_header_decoder_axi_r_rlast), + .s00_axi_ruser(), + .s00_axi_rvalid(frame_header_decoder_axi_r_rvalid), + .s00_axi_rready(frame_header_decoder_axi_r_rready), + + // BlockHeaderDecoder + .s01_axi_awid('0), + .s01_axi_awaddr('0), + .s01_axi_awlen('0), + .s01_axi_awsize('0), + .s01_axi_awburst('0), + .s01_axi_awlock('0), + .s01_axi_awcache('0), + .s01_axi_awprot('0), + .s01_axi_awqos('0), + .s01_axi_awuser('0), + .s01_axi_awvalid('0), + .s01_axi_awready(), + .s01_axi_wdata('0), + .s01_axi_wstrb('0), + .s01_axi_wlast('0), + .s01_axi_wuser('0), + .s01_axi_wvalid(), + .s01_axi_wready(), + .s01_axi_bid(), + .s01_axi_bresp(), + .s01_axi_buser(), + .s01_axi_bvalid(), + .s01_axi_bready('0), + .s01_axi_arid(block_header_decoder_axi_ar_arid), + .s01_axi_araddr(block_header_decoder_axi_ar_araddr), + .s01_axi_arlen(block_header_decoder_axi_ar_arlen), + .s01_axi_arsize(block_header_decoder_axi_ar_arsize), + .s01_axi_arburst(block_header_decoder_axi_ar_arburst), + .s01_axi_arlock('0), + .s01_axi_arcache(block_header_decoder_axi_ar_arcache), + .s01_axi_arprot(block_header_decoder_axi_ar_arprot), + .s01_axi_arqos(block_header_decoder_axi_ar_arqos), + .s01_axi_aruser('0), + .s01_axi_arvalid(block_header_decoder_axi_ar_arvalid), + .s01_axi_arready(block_header_decoder_axi_ar_arready), + .s01_axi_rid(block_header_decoder_axi_r_rid), + .s01_axi_rdata(block_header_decoder_axi_r_rdata), + .s01_axi_rresp(block_header_decoder_axi_r_rresp[1:0]), + .s01_axi_rlast(block_header_decoder_axi_r_rlast), + .s01_axi_ruser(), + .s01_axi_rvalid(block_header_decoder_axi_r_rvalid), + .s01_axi_rready(block_header_decoder_axi_r_rready), + + // RawBlockDecoder + .s02_axi_awid('0), + .s02_axi_awaddr('0), + .s02_axi_awlen('0), + .s02_axi_awsize('0), + .s02_axi_awburst('0), + .s02_axi_awlock('0), + .s02_axi_awcache('0), + .s02_axi_awprot('0), + .s02_axi_awqos('0), + .s02_axi_awuser('0), + .s02_axi_awvalid('0), + .s02_axi_awready(), + .s02_axi_wdata('0), + .s02_axi_wstrb('0), + .s02_axi_wlast('0), + .s02_axi_wuser('0), + .s02_axi_wvalid(), + .s02_axi_wready(), + .s02_axi_bid(), + .s02_axi_bresp(), + .s02_axi_buser(), + .s02_axi_bvalid(), + .s02_axi_bready('0), + .s02_axi_arid(raw_block_decoder_axi_ar_arid), + .s02_axi_araddr(raw_block_decoder_axi_ar_araddr), + .s02_axi_arlen(raw_block_decoder_axi_ar_arlen), + .s02_axi_arsize(raw_block_decoder_axi_ar_arsize), + .s02_axi_arburst(raw_block_decoder_axi_ar_arburst), + .s02_axi_arlock('0), + .s02_axi_arcache(raw_block_decoder_axi_ar_arcache), + .s02_axi_arprot(raw_block_decoder_axi_ar_arprot), + .s02_axi_arqos(raw_block_decoder_axi_ar_arqos), + .s02_axi_aruser('0), + .s02_axi_arvalid(raw_block_decoder_axi_ar_arvalid), + .s02_axi_arready(raw_block_decoder_axi_ar_arready), + .s02_axi_rid(raw_block_decoder_axi_r_rid), + .s02_axi_rdata(raw_block_decoder_axi_r_rdata), + .s02_axi_rresp(raw_block_decoder_axi_r_rresp[1:0]), + .s02_axi_rlast(raw_block_decoder_axi_r_rlast), + .s02_axi_ruser(), + .s02_axi_rvalid(raw_block_decoder_axi_r_rvalid), + .s02_axi_rready(raw_block_decoder_axi_r_rready), + + // SequenceExecutor + .s03_axi_awid('0), + .s03_axi_awaddr('0), + .s03_axi_awlen('0), + .s03_axi_awsize('0), + .s03_axi_awburst('0), + .s03_axi_awlock('0), + .s03_axi_awcache('0), + .s03_axi_awprot('0), + .s03_axi_awqos('0), + .s03_axi_awuser('0), + .s03_axi_awvalid('0), + .s03_axi_awready(), + .s03_axi_wdata('0), + .s03_axi_wstrb('0), + .s03_axi_wlast('0), + .s03_axi_wuser('0), + .s03_axi_wvalid('0), + .s03_axi_wready(), + .s03_axi_bid(), + .s03_axi_bresp(), + .s03_axi_buser(), + .s03_axi_bvalid(), + .s03_axi_bready('0), + .s03_axi_arid('0), + .s03_axi_araddr('0), + .s03_axi_arlen('0), + .s03_axi_arsize('0), + .s03_axi_arburst('0), + .s03_axi_arlock('0), + .s03_axi_arcache('0), + .s03_axi_arprot('0), + .s03_axi_arqos('0), + .s03_axi_aruser('0), + .s03_axi_arvalid('0), + .s03_axi_arready(), + .s03_axi_rid(), + .s03_axi_rdata(), + .s03_axi_rresp(), + .s03_axi_rlast(), + .s03_axi_ruser(), + .s03_axi_rvalid(), + .s03_axi_rready('0), + + /* + * AXI master interface + */ + // Outside-facing AXI interface of the ZSTD Decoder + .m00_axi_awid(memory_axi_aw_awid), + .m00_axi_awaddr(memory_axi_aw_awaddr), + .m00_axi_awlen(memory_axi_aw_awlen), + .m00_axi_awsize(memory_axi_aw_awsize), + .m00_axi_awburst(memory_axi_aw_awburst), + .m00_axi_awlock(memory_axi_aw_awlock), + .m00_axi_awcache(memory_axi_aw_awcache), + .m00_axi_awprot(memory_axi_aw_awprot), + .m00_axi_awqos(memory_axi_aw_awqos), + .m00_axi_awregion(memory_axi_aw_awregion), + .m00_axi_awuser(memory_axi_aw_awuser), + .m00_axi_awvalid(memory_axi_aw_awvalid), + .m00_axi_awready(memory_axi_aw_awready), + .m00_axi_wdata(memory_axi_w_wdata), + .m00_axi_wstrb(memory_axi_w_wstrb), + .m00_axi_wlast(memory_axi_w_wlast), + .m00_axi_wuser(memory_axi_w_wuser), + .m00_axi_wvalid(memory_axi_w_wvalid), + .m00_axi_wready(memory_axi_w_wready), + .m00_axi_bid(memory_axi_b_bid), + .m00_axi_bresp(memory_axi_b_bresp[1:0]), + .m00_axi_buser(memory_axi_b_buser), + .m00_axi_bvalid(memory_axi_b_bvalid), + .m00_axi_bready(memory_axi_b_bready), + .m00_axi_arid(memory_axi_ar_arid), + .m00_axi_araddr(memory_axi_ar_araddr), + .m00_axi_arlen(memory_axi_ar_arlen), + .m00_axi_arsize(memory_axi_ar_arsize), + .m00_axi_arburst(memory_axi_ar_arburst), + .m00_axi_arlock(memory_axi_ar_arlock), + .m00_axi_arcache(memory_axi_ar_arcache), + .m00_axi_arprot(memory_axi_ar_arprot), + .m00_axi_arqos(memory_axi_ar_arqos), + .m00_axi_arregion(memory_axi_ar_arregion), + .m00_axi_aruser(memory_axi_ar_aruser), + .m00_axi_arvalid(memory_axi_ar_arvalid), + .m00_axi_arready(memory_axi_ar_arready), + .m00_axi_rid(memory_axi_r_rid), + .m00_axi_rdata(memory_axi_r_rdata), + .m00_axi_rresp(memory_axi_r_rresp[1:0]), + .m00_axi_rlast(memory_axi_r_rlast), + .m00_axi_ruser(memory_axi_r_ruser), + .m00_axi_rvalid(memory_axi_r_rvalid), + .m00_axi_rready(memory_axi_r_rready) + ); + +endmodule : zstd_dec_wrapper From 91fec30217d15cfcc49de1661ab7464953f4bed1 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Thu, 10 Oct 2024 12:39:17 +0200 Subject: [PATCH 26/46] modules/zstd: Update documentation Internal-tag: [#67096] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/README.md | 392 +++++++++++------- xls/modules/zstd/img/ZSTD_decoder.png | Bin 28375 -> 315264 bytes xls/modules/zstd/img/ZSTD_decoder_wrapper.png | Bin 0 -> 126231 bytes 3 files changed, 239 insertions(+), 153 deletions(-) create mode 100644 xls/modules/zstd/img/ZSTD_decoder_wrapper.png diff --git a/xls/modules/zstd/README.md b/xls/modules/zstd/README.md index eff9097fbe..edf5b9470f 100644 --- a/xls/modules/zstd/README.md +++ b/xls/modules/zstd/README.md @@ -2,105 +2,196 @@ The ZSTD decoder decompresses the correctly formed ZSTD frames and blocks. It implements the [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878.html) decompression algorithm. -Overview of the decoder architecture is presented on the diagram below. +An overview of the decoder architecture is presented in the diagram below. The decoder comprises: -* frame decoder, -* block dispatcher, -* 3 types of processing units: RAW, RLE, and compressed, -* command aggregator, -* history buffer, -* repacketizer. - -Incoming ZSTD frames are processed in the following order: -1. magic number is detected, -2. frame header is parsed, -3. ZSTD data blocks are being redirected to correct processing unit based on the block header, -4. processing unit results are aggregated in correct order into a stream -and routed to the history buffer, -5. data block outputs are assembled based on the history buffer contents and update history, -6. decoded data is processed by repacketizer in order to prepare the final output of the decoder, -7. (optional) calculated checksum is compared against frame checksum. +* Memory Readers +* Memory Writer[^2], +* Control and Status Registers, +* Frame Header Decoder, +* Block Header Decoder, +* 3 types of processing units: RAW-, RLE-, and Compressed Block Decoders[^1], +* Command Aggregator, +* Repacketizer. + +The Decoder interacts with the environment through a set of ports: +* Memory Interface (AXI) +* CSR Interface (AXI) +* Notify line +* Stream Output + +The software controls the core through registers accessible through the `CSR Interface`. +The CSRs are used to configure the decoder and to start the decoding process. + +ZSTD frames to decode are placed in a memory that should be connected to +decoder's `Memory Interface`. + +Once the decoding process is started, the decoder: + +1. Reads the configuration from the CSRs, +2. Decodes the Frame Header, +3. Decodes the Block Header, +4. Decodes the Block Data with the correct processing unit picked based on the Block Type from the Block Header, +4. Aggregates the processing unit results in the correct order into a stream and routes it to the history buffer, +5. Assembles the data block outputs based on the history buffer contents and updates the history, +6. Prepares the final output of the decoder, +7. (Optional) Calculates checksum and compares it against the checksum read from the frame.[^3] ![](img/ZSTD_decoder.png) +## Registers description + +The ZSTD Decoder operation is based on the values stored in a set of CSRs accessible to the user through the AXI bus. +The registers are defined below: + +| Name | Address | Description | +| ---- | ------- | ----------- | +| Status | 0x0 | Keeps the code describing the current state of the ZSTD Decoder | +| Start | 0x8 | Writing `1` when the decoder is in the `IDLE` state starts the decoding process | +| Reset | 0x10 | Writing `1` will reset the decoder to the `IDLE` state | +| Input Buffer | 0x18 | Keeps the base address for the input buffer that is used for storing the frame to decode | +| Output Buffer | 0x20 | Keeps the base address for the output buffer, ZSTD Decoder will write the decoded frame into memory starting from this address. | + +### Status codes + +The following is a list of all available status codes that can be written in the `Status` register. + +| Name | Value | Description | +| ---- | ------- | ----------- | +| IDLE | 0 | Previous decoding finished successfully. The decoder waits for the configuration and writes to the `Start` register. | +| RUNNING | 1 | Decoding process is started | +| READ_CONFIG_OK |2 | Successfully read configuration from the CSRs | +| FRAME_HEADER_OK | 3 | Successfully decoded frame header | +| FRAME_HEADER_CORRUPTED | 4 | Frame header data is not valid | +| FRAME_HEADER_UNSUPPORTED_WINDOW_SIZE | 5 | The `WindowSize` parameter read from the frame header is not supported in the decoder | +| BLOCK_HEADER_OK | 6 | Successfully read the header of the Zstd data block | +| BLOCK_HEADER_CORRUPTED | 7 | Block type is `Reserved` | +| BLOCK_HEADER_MEMORY_ACCESS_ERROR | 8 | Failure in communication with the memory | +| RAW_BLOCK_OK | 9 | Successfully decoded raw data block | +| RAW_BLOCK_ERROR | 10 | Failure in communication with the memory | +| RLE_BLOCK_OK | 11 | Successfully decoded RLE data block | + +### Reset handling + +The expected behavior of the `Reset` CSR cannot be achieved solely in the DSLX code. +As of [cb2829ab](https://github.com/google/xls/commit/cb2829ab809c58f21d957a47e400456a8c8f8db1), the XLS toolchain does not support resetting the proc network on the DSLX level. +As a workaround for this issue, the `ZstdDec` proc defines a `reset` output channel that sends a pulse when there is a write to the `Reset` CSR. +The Verilog code that integrates the decoder in a target system must connect this output back to the standard `rst` input of the decoder. +If any external reset signal exists and is intended to be used with the decoder, it should be OR-ed with the `reset` channel output before connecting to the decoder's `rst` input. +Please refer to the diagram of the Verilog wrapper in the [Testing Methodology](#testing-methodology) chapter for example reset connection. + +## Controlling the decoder from the software + +The configuration done by the software must be carried out when the decoder is in the `IDLE` state. +It is the only time when the decoder will be able to take the configuration values from the CSRs and use those in the decoding process. + +The software should first read the `Status` register to confirm that the decoder is in the `IDLE` state. +In case it is not in the `IDLE` state, it is possible to reset the decoder by writing `1` to the `Reset` register. +Please note that this will stop ongoing decoding and all progress will be lost. + +Then, the software has to reserve the memory for the input buffer and write the frame to decode there. +The address of the buffer should be written into `Input Buffer` register so that the decoder will know where to look for the frame to decode. + +The next step is to reserve the memory space for the decoded frame where the Decoder will write the decompressed data. +The address to that buffer should be written to the `Output Buffer` register. + +Finally, it is possible to start the decoding process by writing `1` to the `Start` register. +This orders the Decoder to read the configuration CSRs and start reading and decoding data stored in the input buffer. +The Decoder transitions to the `RUNNING` state and then to other states that describe the status of the last operation finished in the decoder (see #status-codes for other possible status codes) which will be visible in the `Status` register. + +When the decoding process is finished the Decoder transitions back to the `IDLE` state and signals this on the `Notify` IRQ line. +The decoded data is stored under the address configured previously in the `Output Buffer` register. + +In case an error occurs during the decoding process it is also signaled on the `Notify` IRQ line and the error code is written to the `Status` CSR. + ## ZSTD decoder architecture ### Top level Proc -This state machine is responsible for receiving encoded ZSTD frames, buffering the input and passing it to decoder's internal components based on the state of the proc. -The states defined for the processing of ZSTD frame are as follows: +This state machine is responsible for controlling the operation of the whole decoder. +It uses the configuration data from the CSRs, connects all underlying modules and sends processing requests to those based on the state of the machine. +The states defined for the processing of the ZSTD frame are as follows: ```mermaid stateDiagram - direction LR + [*] --> IDLE + + IDLE --> READ_CONFIG: Start + + READ_CONFIG --> DECODE_FRAME_HEADER - [*] --> DECODE_MAGIC_NUMBER + DECODE_FRAME_HEADER --> DECODE_BLOCK_HEADER + DECODE_FRAME_HEADER --> ERROR - DECODE_MAGIC_NUMBER --> DECODE_MAGIC_NUMBER: Not enough data - DECODE_MAGIC_NUMBER --> DECODE_FRAME_HEADER: Got magic number - DECODE_MAGIC_NUMBER --> ERROR: Corrupted + DECODE_BLOCK_HEADER --> DECODE_RAW_BLOCK + DECODE_BLOCK_HEADER --> DECODE_RLE_BLOCK + DECODE_BLOCK_HEADER --> DECODE_COMPRESED_BLOCK + DECODE_BLOCK_HEADER --> ERROR - DECODE_FRAME_HEADER --> DECODE_FRAME_HEADER: Not enough data - DECODE_FRAME_HEADER --> DECODE_BLOCK_HEADER: Header decoded - DECODE_FRAME_HEADER --> ERROR: Unsupported window size - DECODE_FRAME_HEADER --> ERROR: Corrupted + state if_block_last <> + DECODE_RAW_BLOCK --> ERROR + DECODE_RAW_BLOCK --> if_block_last - DECODE_BLOCK_HEADER --> DECODE_BLOCK_HEADER: Not enough data - DECODE_BLOCK_HEADER --> FEED_BLOCK_DECODER: Feed raw data - DECODE_BLOCK_HEADER --> FEED_BLOCK_DECODER: Feed RLE data - DECODE_BLOCK_HEADER --> FEED_BLOCK_DECODER: Feed compressed data - DECODE_BLOCK_HEADER --> ERROR: Corrupted + DECODE_RLE_BLOCK --> ERROR + DECODE_RLE_BLOCK --> if_block_last - state if_decode_checksum <> - state if_block_done <> + DECODE_COMPRESSED_BLOCK --> ERROR + DECODE_COMPRESSED_BLOCK --> if_block_last - FEED_BLOCK_DECODER --> if_decode_checksum: Is the checksum available? - if_decode_checksum --> DECODE_CHECKSUM: True - if_decode_checksum --> DECODE_MAGIC_NUMBER: False - FEED_BLOCK_DECODER --> if_block_done: Is the block decoding done? - if_block_done --> DECODE_BLOCK_HEADER: Decode next block - if_block_done --> FEED_BLOCK_DECODER: Continue feeding + if_block_last --> DECODE_BLOCK_HEADER: Not last block in the frame + if_block_last --> DECODE_CHECKSUM: Last block in the frame - DECODE_CHECKSUM --> DECODE_MAGIC_NUMBER: Frame decoded + DECODE_CHECKSUM --> FINISH - ERROR --> [*] + FINISH --> IDLE + ERROR --> IDLE ``` -After going through initial stages of decoding magic number and frame header, decoder starts the block division process. -It decodes block headers to calculate how many bytes must be sent to the block dispatcher and when the current frame's last data block is being processed. -Knowing that, it starts feeding the block decoder with data required for decoding current block. -After transmitting all data required for current block, it loops around to the block header decoding state and when next block header is not found it decodes checksum when it was requested in frame header or finishes ZSTD frame decoding and loops around to magic number decoding. - -### ZSTD frame header decoder -This part of the design starts with detecting the ZSTD magic number. -Then it parses and decodes the frame header's content and checks the header's correctness. -If the frame header has the checksum option enabled, this will enable `DECODE_CHECKSUM` stage at the end of the frame decoding where the frame's checksum will be computed and compared with the checksum embedded at the end of the frame stream. - -### Block dispatcher (demux) -At this stage, block headers are parsed and removed from the block data stream. -Based on parse values, it directs the block data stream to either RAW, RLE or compressed block sections. -For this task it uses an 8 byte native interface: a 64-bit data bus and a 64-bit length field that contains the number of correct bits on the data bus. -It also attaches a unique block ID value to each processed data block. -The IDs are sequential starting from 0 and are reset only after receiving and processing the current frame's last data block. - -### RAW -This proc passes the received data directly to its output channel. +After going through the initial stage of reading the configuration from the CSRs, the decoder sends the processing requests to the underlying parts of the decoder. +The processing requests contain the addresses in the memory where particular parts of the encoded ZSTD frames reside. +The decoder, based on responses from consecutive internal modules, calculates offsets from the base address that was written to `Input Buffer` CSR and forms the requests for the next internal modules, e.g.: for `BlockHeaderDecoder` or any of the processing units (`RawBlockDecoder`, `RleBlockDecoder`, `CompressedBlockDecoder`). + +Each of the internal modules waits for the processing request. +Once received, the module fetches the data from the memory starting from the address received in the processing request. +`MemReader` procs are used by those modules to communicate with the external memory through the AXI interface. +Internal modules decode the acquired parts of the frame and return responses with the results back to the top level proc. + +The processing units also output the decoded blocks of data through a stream-based interface to `SequenceExecutor` and further to `Repacketizer` procs. +Those procs perform the last steps of the decoding before the final output is sent out back to the memory under the address stored in the `Output Buffer` CSR or through the stream-based output channel. +Once the decoding process is completed, the decoder sends the `Notify` signal and transitions back to the `IDLE` state. + +### Internal modules + +#### FrameHeaderDecoder +This proc receives requests with the address of the beginning of the ZSTD frame. +It then reads the frame data from the memory and starts parsing the frame header. +If the magic number is not detected or the frame header is invalid, the proc will send a response with an error code. +Otherwise, it will put the frame header into internal DSLX representation, calculate the length of the header and send those as a response with `OKAY` status. + +#### BlockHeaderDecoder +ZSTD block header size is always 3 bytes. +BlockHeaderDecoder always reads 4 bytes of data. +It extracts the information on block type, size and whether the block is the last one in the ZSTD frame and puts that data in the response. +The additional byte is also placed in the response as an optimization for the RleBlockDecoder. + +#### RawBlockDecoder +This proc passes the data read from the memory directly to its output channel. It preserves the block ID and attaches a tag, stating that the data contains literals and should be placed in the history buffer unchanged, to each data output. -### RLE decoder -This proc receives a tuple (s, N), where s is an 8 bit symbol and N is an accompanying `symbol_count`. +#### RleBlockDecoder +This proc receives a tuple (s, N), where s is an 8-bit symbol and N is an accompanying `symbol_count`. +It does not have to read the 8-bit symbol from the memory because `BlockHeaderDecoder` did that before and passed the symbol in the processing request to the `RleBlockDecoder`. The proc produces `N*s` repeats of the given symbol. This step preserves the block ID and attaches the literals tag to all its outputs. -### Compressed block decoder +#### CompressedBlockDecoder[^1] This part of the design is responsible for decoding the compressed data blocks. -It ingests the bytes stream, internally translates and interprets incoming data. +It ingests the bytes stream, and internally translates and interprets incoming data. Only this part of the design creates data chunks tagged both with `literals` and/or `copy`. This step preserves the block ID. -More in depth description can be found in [Compressed block decoder architecture](#compressed-block-decoder-architecture) paragraph of this doc. +More in-depth description can be found in [Compressed block decoder architecture](#compressed-block-decoder-architecture) paragraph of this doc. -### Commands aggregator (mux) -This stage takes the output from either RAW, RLE or Command constructor and sends it to the History buffer and command execution stage. -This stage orders streams based on the ID value assigned by the block dispatcher. +#### Commands aggregator (DecMux) +This stage takes the output from either RAW, RLE or CompressedBlockDecoder and sends it to the History buffer and command execution stage. +This stage orders streams based on the ID value assigned by the top level proc. It is expected that single base decoders (RAW, RLE, compressed block decoder) will be continuously transmitting a single ID to the point of sending the `last` signal which marks the last packet of currently decoded block. That ID can change only when mux receives the `last` signal or `last` and `last_block` signals. @@ -110,7 +201,7 @@ It continues to read that stream until the `last` signal is set, then it switche The command aggregator starts by waiting for `ID = 0`, after receiving the `last` signal it expects `ID = 1` and so on. Only when both `last` and `last_block` are set the command aggregator will wait for `ID = 0`. -### History buffer and command execution +#### History buffer and command execution (SequenceExecutor) This stage receives data which is tagged either `literals` or `copy`. This stage will show the following behavior, depending on the tag: * `literals` @@ -121,13 +212,26 @@ This stage will show the following behavior, depending on the tag: * Copy `copy_length` literals starting `offset _length` from the newest in history buffer to the decoder's output, * Copy `copy_length` literals starting `offset _length` from the newest in history buffer to the history buffer as the newest. -### Compressed block decoder architecture +#### Repacketizer +This proc is used at the end of the processing flow in the ZSTD decoder. +It gathers the output of `SequenceExecutor` proc and processes it to form the final output packets of the ZSTD decoder. +Input packets coming from the `SequenceExecutor` consist of: + +* data - bit vector of constant length +* length - field describing how many bits in bit vector are valid +* last - flag which marks the last packet in currently decoded ZSTD frame. + +It is not guaranteed that all bits in data bit vectors in packets received from `SequenceExecutor` are valid as those can include padding bits that were added in previous decoding steps and now have to be removed. +Repacketizer buffers input packets, removes the padding bits and forms new packets with all bits of the bit vector valid, meaning that all bits are decoded data. +Newly formed packets are then sent out to the output of the whole ZSTD decoder. + +### Compressed block decoder architecture[^1] This part of the design is responsible for processing the compressed blocks up to the `literals`/`copy` command sequence. -This sequence is then processed by the history buffer to generate expected data output. -Overview of the architecture is provided on the diagram below. -The architecture is split into 2 paths: literals path and sequence path. +This sequence is then processed by the history buffer to generate the expected data output. +An overview of the architecture is provided in the diagram below. +The architecture is split into 2 paths: the literals path and the sequence path. Architecture is split into 3 paths: literals path, FSE encoded Huffman trees and sequence path. -Literals path uses Hufman trees to decode some types of compressed blocks: Compressed and Treeless blocks. +Literals path uses Huffman trees to decode some types of compressed blocks: Compressed and Treeless blocks. ![](img/ZSTD_compressed_block_decoder.png) @@ -144,11 +248,11 @@ When `literals length` is greater than 0, it will send a request to the literals Then based on the offset and copy length it either creates a match command using the provided offset and match lengths, or uses repeated offset and updates the repeated offset memory. Formed commands are sent to the Commands aggregator (mux). -### Literals path architecture +#### Literals path architecture ![](img/ZSTD_compressed_block_literals_decoder.png) -#### Literals decoder dispatcher +##### Literals decoder dispatcher This proc parses and consumes the literals section header. Based on the received values it passes the remaining bytes to RAW/RLE/Huffman tree/Huffman code decoders. It also controls the 4 stream operation mode [4-stream mode in RFC](https://www.rfc-editor.org/rfc/rfc8878.html#name-jump_table). @@ -156,59 +260,59 @@ It also controls the 4 stream operation mode [4-stream mode in RFC](https://www. All packets sent to the Huffman bitstream buffer will be tagged either `in_progress` or `finished`. If the compressed literals use the 4 streams encoding, the dispatcher will send the `finished` tag 4 times, each time a fully compressed stream is sent to the bitstream buffer. -#### RAW Literals +##### RAW Literals This stage simply passes the incoming bytes as literals to the literals buffer. -#### RLE Literals +##### RLE Literals This stage works similarly to the [RLE stage](#rle-decoder) for RLE data blocks. -#### Huffman bitstream buffer +##### Huffman bitstream buffer This stage takes data from the literals decoder dispatcher and stores it in the buffer memory. Once the data with the `finished` tag set is received, this stage sends a tuple containing (start, end) positions for the current bitstream to the Huffman codes decoder. This stage receives a response from the Huffman codes decoder when decoding is done and all bits got processed. Upon receiving this message, the buffer will reclaim free space. -#### Huffman codes decoder +##### Huffman codes decoder This stage receives bitstream pointers from the Huffman bitstream buffer and Huffman tree configuration from the Huffman tree builder. It accesses the bitstream buffers memory to retrieve bitstream data in reversed byte order and runs it through an array of comparators to decode Huffman code to correct literals values. -#### Literals buffer +##### Literals buffer This stage receives data either from RAW, RLE or Huffman decoder and stores it. Upon receiving the literals copy command from the Command Constructor for `N` number of bytes, it provides a reply with `N` literals. -### FSE Huffman decoder architecture +#### FSE Huffman decoder architecture ![](img/ZSTD_compressed_block_Huffman_decoder.png) -#### Huffman tree decoder dispatcher +##### Huffman tree decoder dispatcher This stage parses and consumes the Huffman tree description header. Based on the value of the Huffman descriptor header, it passes the tree description to the FSE decoder or to direct weight extraction. -#### FSE weight decoder +##### FSE weight decoder This stage performs multiple functions. 1. It decodes and builds the FSE distribution table. 2. It stores all remaining bitstream data. 3. After receiving the last byte, it translates the bitstream to Huffman weights using 2 interleaved FSE streams. -#### Direct weight decoder +##### Direct weight decoder This stage takes the incoming bytes and translates them to the stream of Huffman tree weights. The first byte of the transfer defines the number of symbols to be decoded. -#### Weight aggregator +##### Weight aggregator This stage receives tree weights either from the FSE decoder or the direct decoder and transfers them to Huffman tree builder. This stage also resolves the number of bits of the final weight and the max number of bits required in the tree representation. This stage will emit the weights and number of symbols of the same weight before the current symbol for all possible byte values. -#### Huffman tree builder +##### Huffman tree builder This stage takes `max_number_of_bits` (maximal length of Huffman code) as the first value, then the number of symbols with lower weight for each possible weight (11 bytes), followed by a tuple (number of preceding symbols with the same weight, symbol's_weight). It's expected to receive weights for all possible byte values in the correct order. Based on this information, this stage will configure the Huffman codes decoder. -### Sequence path architecture +#### Sequence path architecture ![](img/ZSTD_compressed_block_sequence_decoder.png) -#### Sequence Header parser and dispatcher +##### Sequence Header parser and dispatcher This stage parses and consumes `Sequences_Section_Header`. Based on the parsed data, it redirects FSE description to the FSE table decoder and triggers Literals FSE, Offset FSE or Match FSE decoder to reconfigure its values based on the FSE table decoder. After parsing the FSE tables, this stage buffers bitstream and starts sending bytes, starting from the last one received as per ZSTD format. @@ -216,37 +320,24 @@ Bytes are sent to all decoders at the same time. This stage monitors and triggers sequence decoding phases starting from initialization, followed by decode and state advance. FSE decoders send each other the number of bits they read. -#### Literals FSE decoder +##### Literals FSE decoder This stage reconfigures its FSE table when triggered from [sequence header parse and dispatcher](#sequence-header-parser-and-dispatcher). It initializes its state as the first FSE decoder. In the decode phase, this stage is the last one to decode extra raw bits from the bitstream, and the number of ingested bits is transmitted to all other decoders. This stage is the first stage to get a new FSE state from the bitstream, and it transmits the number of bits it used. -#### Offset FSE decoder +##### Offset FSE decoder This stage reconfigures its FSE table when triggered from [sequence header parse and dispatcher](#sequence-header-parser-and-dispatcher). It initializes its state as the second FSE decoder. In the decode phase, this stage is the first one to decode extra raw bits from bitstream, and the number of ingested bits is transmitted to all other decoders. This stage is the last decoder to update its FSE state after the decode phase, and it transmits the number of used bits to other decoders. -#### Match FSE decoder +##### Match FSE decoder This stage reconfigures its FSE table when triggered from [sequence header parse and dispatcher](#sequence-header-parser-and-dispatcher). It initializes its state as the last FSE decoder. In the decode phase, this stage is the second one to decode extra raw bits from the bitstream, and the number of ingested bits is transmitted to all other decoders. This stage is the second stage to update its state after the decode phase, and the number of used bits is sent to all other decoders. -### Repacketizer -This proc is used at the end of the processing flow in the ZSTD decoder. -It gathers the output of `SequenceExecutor` proc and processes it to form final output packets of the ZSTD decoder. -Input packets coming from the `SequenceExecutor` consist of: - -* data - bit vector of constant length -* length - field describing how many bits in bit vector are valid -* last - flag which marks the last packet in currently decoded ZSTD frame. - -It is not guaranteed that all bits in data bit vectors in packets received from `SequenceExecutor` are valid as those can include padding bits which were added in previous decoding steps and now have to be removed. -Repacketizer buffers input packets, removes the padding bits and forms new packets with all bits of the bit vector valid, meaning that all bits are decoded data. -Newly formed packets are then sent out to the output of the whole ZSTD decoder. - ## Testing methodology Testing of the `ZSTD decoder` is carried out on two levels: @@ -255,14 +346,36 @@ Testing of the `ZSTD decoder` is carried out on two levels: * Integrated decoder Each component of the decoder is tested individually in DSLX tests. -Testing on the DSLX level allows the creation of small test cases that test for both positive and negative outcomes of a given part of the design. +Testing on the DSLX level allows the creation of small test cases that test for positive outcomes of a given part of the design. When need be, those test cases can be also modified by the user to better understand how the component operates. -Tests of the integrated ZSTD decoder are written in C++. +Tests of the integrated ZSTD decoder are carried out on DSLX and Verilog levels. The objective of those is to verify the functionality of the decoder as a whole. Testing setup for the ZSTD decoder is based on comparing the simulated decoding results against the decoding of the reference library. Currently, due to the restrictions from the ZSTD frame generator, it is possible to test only the positive cases (decoding valid ZSTD frames). +Verilog tests are written in Python as [cocotb](https://github.com/cocotb/cocotb) testbench. + +ZstdDecoder's main communication interfaces are the AXI buses. +Due to the way XLS handles the codegen of DSLX channels that model the AXI channels, the particular ports of the AXI channels are not represented correctly. +This enforces the introduction of a Verilog wrapper that maps the ports generated by XLS into proper AXI ports (see AXI peripherals [README](memory/README.md) for more information). +Additionally, the wrapper is used to mux multiple AXI interfaces from `Memory Readers` into a single outside-facing AXI interface (`Memory Interface`) that can be connected to the external memory. +The mux is implemented by a third-party [AXI Interconnect](https://github.com/alexforencich/verilog-axi). + +![](img/ZSTD_decoder_wrapper.png) + +Cocotb testbench interacts with the decoder with the help of a [cocotbext-axi](https://github.com/alexforencich/cocotbext-axi) extension that provides AXI bus models, drivers, monitors and RAM model accessible through AXI interface. +Cocotb AXI Master is connected to the decoder's `CSR Interface` and is used to simulate the software's interaction with the decoder. + +The Basic test case for the ZstdDecoder is composed of the following steps: + +1. The testbench generates a ZSTD frame using the [decodecorpus](https://github.com/facebook/zstd/blob/dev/tests/decodecorpus.c) utility from the [zstd reference library](https://github.com/facebook/zstd). +2. The encoded frame is placed in an AXI RAM model that is connected to the decoder's `Memory Interface`. +3. The encoded frame is decoded with the zstd reference library and the results are represented in the decoder's output format as the expected data from the simulation. +4. AXI Master performs a series of writes to the ZstdDecoder CSRs to configure it and start the decoding process. +5. Testbench waits for the signal on the `Notify` channel, monitoring the output of the decoder and checking it against the expected output data at the same time. +6. Test case succeeds once `Notify` is asserted, all expected data is received and the decoder lands in `IDLE` state with status `OKAY` in the `Status` CSR. + ### Failure points #### User-facing decoder errors @@ -274,19 +387,8 @@ The design will fail the tests under the following conditions: * Simulation encounters `assert!()` or `fail!()` statements * The decoding result from the simulation has a different size than the results from the reference library * The decoding result from the simulation has different contents than the results from the reference library -* Caveats: - * Timeout occurred while waiting for a valid `Magic Number` to start the decoding process - * Other timeouts occurring while waiting on channel operations (To be fixed) Currently, all mentioned conditions lead to an eventual test failure. -Most of those cases are handled properly while some are yet to be reworked to finish faster or to provide more information about the error. -For example, in case of transitioning to the `ERROR` state, the test will timeout on channel operations waiting to read from the decoder output. -In case of waiting for a valid `Magic Number`, the decoder will transition to an `ERROR` state without registering the correct `Magic Number` on the input channel which will lead to a similar timeout. - -Those cases should be handled in a way that allows for early failure of the test. -It can be done through a Proc parameter enabled for tests that change the behavior of the logic, e.g. launching `assert!()` when the decoder enters the `ERROR` state. -Another idea is to use a special output channel for signaling internal states and errors to monitor the decoder for the errors encountered during decoding. -For example, in an invalid `Magic Number`, the test case should expect a certain type of error reported on this channel at the very beginning of the simulation. #### Failures in ZSTD Decoder components @@ -295,24 +397,20 @@ However, the majority of the errors require modification of the deeper parts of Because of that, it is better to rely on DSLX tests for the individual components where inputs for the test cases are smaller, easier to understand and modify when needed. The components of the ZSTD decoder can fail on `assert!()` and `fail!()` statements or propagate specific error states to the Top Level Proc and cause it to transition to the `ERROR` state. +Upon entering the `ERROR` state, the decoder will write a specific error code to the `Status` CSR and send a `Notify` signal to the output. +The interacting software can then read the code from the register and properly handle the error. + The following enumeration will describe how to trigger each possible ZSTD Decoder error. -The `ERROR` state can be encountered under the following conditions when running Top Level Proc C++ tests but also in DSLX tests for the specific components: -* Corrupted data on the `Magic Number` decoding stage +The `ERROR` state can be encountered under the following conditions when running Top Level Proc Verilog tests but also in DSLX tests for the specific components: +* Corrupted data on the frame header decoding stage * Provide data for the decoding with the first 4 bytes not being the valid `Magic Number` (0xFD2FB528) -* Corrupted data during frame header decoding * Set the `Reserved bit` in the frame header descriptor -* Unsupported Window Size during frame header decoding * Set `Window Size` in frame header to value greater than `max window size` calculated from current `WINDOW_LOG_MAX` (by default in Top Level Proc tests `Window Size` must be greater than `0x78000000` to trigger the error) * Corrupted data during Block Header decoding * Set the `Block Type` of any block in the ZSTD frame to `RESERVED` The `assert!()` or `fail!()` will occur in: -* Buffer - * Add data to the buffer with `buffer_append()` when it is already full or unable to fit the whole length of the data - * Fetch data from the buffer with `buffer_pop()` when it is empty or have not enough data -* DecoderDemux - * Receive more than one `raw` or `compressed` block in a single `BlockDataPacket` * RawBlockDecoder * Receive `BlockDataPacket` with `ID` different than the previous packet which did not have the `last` flag set * DecoderMux @@ -321,34 +419,26 @@ The `assert!()` or `fail!()` will occur in: * SequenceExecutor * Receive `SequenceExecutorPacket` with `msg_type==SEQUENCE` and `content` field with value: `0` -There are also several `impossible cases` covered by `assert!()` and `fail!()`: +There are also several `impossible cases` covered by `fail!()`. +Those are mostly enforced by the type checker for the `match` expressions to cover unreachable cases. +This is done for example in: * Frame header decoder - * `Window Descriptor` does not exist after checking that it is available in the frame header - * `Frame Content Size` does not exist after checking that it is available in the frame header - * `Dictionary ID Flag` has an illegal value - * `Frame Content Size Flag` has an illegal value -* DecoderDemux - * Data packet has a different `Block Type` than `RAW`, `RLE` or `COMPRESSED` * SequenceExecutor - * Proc transitions to `SEQUENCE_READ` state after receiving `SequenceExecutorPacket` with `msg_type` different than `SEQUENCE` or the message was invalid -* Top Level Proc - * Block header type is different than `RAW`, `RLE`, `COMPRESSED` - * There is not enough data to feed the `BlockDecoder`, even though the previous check indicated a valid amount of data in the buffer ### Testing against [libzstd](https://github.com/facebook/zstd) Design is verified by comparing decoding results to the reference library `libzstd`. ZSTD frames used for testing are generated with [decodecorpus](https://github.com/facebook/zstd/blob/dev/tests/decodecorpus.c) utility. -The generated frame is then decoded with `libzstd`. +The generated frame is then decoded with `libzstd` and with simulated `ZstdDecoder`. #### Positive test cases If the results of decoding with `libzstd` are valid, the test runs the same encoded frame through the simulation of DSLX design. The output of the simulation is gathered and compared with the results of `libzstd` in terms of its size and contents. -Encoded ZSTD frame is generated with the function `GenerateFrame(int seed, BlockType btype)` from [data_generator](https://github.com/antmicro/xls/blob/52186-zstd-top/xls/modules/zstd/data_generator.cc) library. -This function takes as arguments the seed for the generator and enum which codes the type of blocks that should be generated in a given frame. +Encoded ZSTD frame is generated with the function `GenerateFrame(seed, btype, output_path)` from [data_generator](https://github.com/antmicro/xls/blob/main/xls/modules/zstd/cocotb/data_generator.py) library. +This function takes as arguments the seed for the generator, an enum that codes the type of blocks that should be generated in a given frame and the output path to write the generated frame into a file. The available block types are: * RAW @@ -357,12 +447,12 @@ The available block types are: * RANDOM The function returns a vector of bytes representing a valid encoded ZSTD frame. -Such generated frame can be passed to `ParseAndCompareWithZstd(std::vector frame)` which is responsible for decoding the frame, running simulation and comparing the results. +Such generated frame can be passed to DSLX and cocotb testbenches to be decoded in the simulation and compared against the results from the reference library. -Tests are available in the `zstd_dec_test.cc` file and can be launched with the following Bazel command: +Verilog tests are available in the `zstd_dec_cocotb_test.py` file and can be launched with the following Bazel command: ``` -bazel test //xls/modules/zstd:zstd_dec_cc_test +bazel run -c opt -- //xls/modules/zstd:zstd_dec_cocotb_test --logtostderr ``` #### Negative test cases @@ -373,11 +463,7 @@ Because of that, it is not possible to efficiently provide valuable negative tes The alternatives for writing negative tests include: * Generating a well-known valid ZSTD frame from a specific generator seed and then tweaking the raw bits in this frame to trigger the error response from the decoder -* Using [FuzzTest](https://github.com/google/fuzztest) to create multiple randomized test cases for the decoder and then compare `libzstd` decoder failure with `ZSTD Decoder` failure. - -### Known Limitations - -* **[WIP]** Bugs in the current flow cause failures in some of the test cases of decoding ZSTD frame with RLE block types -* **[WIP]** Compressed block type is not supported -* Checksum is not being verified +[^1]: `CompressedBlockDecoder` is to be added in follow-up PRs. +[^2]: `MemWriter` instance is a part of the memory-based output interface which is currently under development. +[^3]: Checksum verification is currently unsupported. diff --git a/xls/modules/zstd/img/ZSTD_decoder.png b/xls/modules/zstd/img/ZSTD_decoder.png index f157751512745dc923beec4b242377fe3962eb89..494926d89f7108623b2a4d958d1980b9a34f75cc 100644 GIT binary patch literal 315264 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

)^@@|1jzus^eSWD7<{w!; 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

  • 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

    5#S z|G?f&49-w3G&}b4l^!i7vDPEiejn;zS?Or2=1?gdPb9~*e? z*HCf@G%J~>;qv{zt)2Ga?no?;Or!|K+-S~#bI~%YdAj$WP$4lPQbm-Zu`w+sbEFDh z;kXt;K_331;im4V*3w{;f13z;51aUrHM)F!@vu_H5tB@<9X)T7EU4>($L% zEVdkYcmIp+mCupx6Lo%p7X>QW{loY$4&ZG1uE_2#FHOor+~gwLVG~CuUoj#qfd)NdVwr(dP(aCdDNKeT^io6o_@{MHfLi@dJ%RCNCmWNia?GzavE`m za5JJOY-^9Zz%(4(x4*Vr1n2_UN?8I@gY{WQAXtg+B#X3_tpCaDP34ab22179V-}Mmmgh(A1n+zW2kg51 z_S*vLM>_o8mlJ}~%>mCzqh)L{SSzs}A~&`nc_(8rrH2M4a>T2fHD?1mGMd~Agh%Hh zGw0vqYVtlkf+HqL!CIA8O=6IkT@yp`nsFFBGH;w3`0+H!Jf6AOTRmt;r%fGHA@AS! zykd=glU@TZm_r)=fU36dh`m8Ro5e#>(*f-D)VF0N=qWyRl!>LBIdd_DZTcR-5-Cl{!a=u=AIna3BJLD)R;#v?;IU74K_|fXflit&9bN<2|G4Hpo`IGI5RQOaq z@*7~5!lywNW!LFByc@Hy!Hj0er$D};1IRY*f*a)xR5{`$MQ1-x{#OwEy()uIlK+{; zrstuyW?=8NXGFvu36d0n!|ly5m#%DIs&e5W(hE?qZGR~tCztaGhy8VNm(2 zxOw~jX4iDs(RRY;c*p6*-3o4;@2`SpX`&B*1;04=^S!rDno6{QYmxGsZ#_dA$03qW zYnWrkIX0Y+k9XOec4aO0GtV0cIZf>u9*fzPhF-k!X>v_NIH1a@{We1UGZ%n*+oy(b zi7eF^rG4Uy%)tlN{clauJ^}RN+1!faVg%5G(bOS}IpPb%g;>5*0rC#ecEbVc&%!^7 zsb7mjz!5Z}upt+4y>iTOp_q*0jxvWZ6?jnD%&(QauWpgs1&Z`zC|X#f3mWlAAJ_F1Vk#<}_6&Xxe}6HR9rY zo*$O7HB30bT+2Q;X{=Lz|Fy*BWhINQLSYf7>RVuF-HdVO4_r4h~hu;L;icg)3CI^`he_ur_RJovJTc0!(u)lZ&y^ zUy7?ER>+!!oKt^ZlY4*5TT>jHEm*i{+W6RT-}B-_#-hf97kHcF8BnaNkeI%uFL3w8 z2l?Uh%@Ur)-Hwstt%E@aTG48cE?2R8V(m@uC&EOIzs49<|4fuG*)4JLly^@#l;1wy zIyJtvofw{qb&l0=v3MzKc^P-fJBx5ntPigy?A%*S+D)Y1ej9Rc*?Op+6-_d+ymF(9l~(SuBk|ixG24|o zJdWS^+Y&42n`P1(f6#PfZiH&_2*iH40w{j50HLTnDvYt#(+3nj<&uIw_=;swz5pJu z*;+Owi1;D*>hz&J|EZ7nu}szTD|!O@6<+ubW^HvJ*!f7#_>G>&h5sR^lFYKxBpy&( zcpHS(msn?etMG0Ymr=DwU;2c7wn8FVt5ktO>}WDIg2$31=l;)LE0N=}qM1rs9!$kN z^~Dq0vHsyMF-tQSTzm7)oT+#X$x%utEAd1x)F|D9HmvXZZre67s|)|Keo+92ba#=RWn*BUO9PXi

    ?64~!?O2TK%w?4tgl5& zsKGvkI75~Hu7Pkuo8s>yBs|^XM4fMZk{hmkpp>JI1fB0>7LQS0xW4zm zse;r(-HWyB@s>;oFY%LDQ=5(niDU(Z( zI|%_)8Auh=0E882g6RFL!vH7TkHxnJB3sv4g<6D3aoT0y-MICYC&Z@)4HKB=v!@5_P2ig=Ds%Upn3 zE)@#O$L^gB{Z&OSOUB`C*CdKb6CGmbtzY|iaL&I~@Z7BRsxH{J|tcEMg=kpYe`thixppSq;&XIJ!di}NF%;rcz7HEPHR*=+_Ch& zK5&SUq;De|`s_qAFh2bC*QTFt+00}#an0Dli1D`raT45@M?l?;Lkb4hdna`eX62{= z8tJ}6Aq!e2c>UX#2M9}4AfdgMk%gi-!LvM7HzXq{_*wlALyr&|?c9l z(>TsTH>@@HWGtV}1-zHi@%sM$7S_WbISo+WIrA(6F}v?Wyb>$)*qn};$p)a8o895w zCfTwO<`**gw$mseO3R<33O7KQfxZ~nF~f=7M9ibI@SbjbxcKR=Iouq+4(mtq;?dEK zpx|;dBY(srzIF+|^XHKC+aHp+FW-Jj;QW~g+u5O2BQ2sy^U1@|c<8E~{0v;|Az+iO%%PLx1o?1FSEoJvk!!q|wOZ=3n$k1? zq(fA!)O6eHt}1iEG^NBFgv|N|7X_@oY}C?{xx)%Ea)%GMzYhAZV*>3%`V{oa)88~UYgS4bx0 z!W*VqZ=oEd7O%j2s6_(FSFORsSm=Zh2gr8I8!Ir|II&v}-8n^3+!q5DUA}KVS`5~C z4GnDSl$cc!$M$_B_OdI0nK;2>^^h=%ThBMEg>74Uv;_*U&qzll zYN_r2^;Kjm@Bg>2n(l~UXIh)-2=v%pW3V}@Y>5}8_?Drf#KW$fB;B;u5_6d;j7Bu9 zA&?-`_F>K)+8462arJgh`;DulprSiBG1Sco0bC>C)Hd35{{K8V0j^lMe=xqp$v(aQ z3c`>f!pM$cr62c9t)QHLVlz}8P>|X@vDjZUEm}|h2iN=mewBPZMhr^EOwr!f`xi{e^~xsEktaE(4dX2wGlxFHnNmrexH8Oj%`>3sMIz=@)Tk}C=+yq!65r4B#eb<430P_m%ltxH zlg~3jOv4Af83N3oiwNl5dh(!H{`W{QMQFwsFGCV*JzV+JE|qmQUpT>51cW-xStHG( zdz%Z>B8^dhY7lDcFrZs)zMU|?DH5QQGk%W(Cj(8k^4nwr>be7li1V)?G1qmY?&fx# z%}*9Xwn+5ato}-5=MC&oohpmaGJ=8&NT`87wmit#Sn3nBpF_qb>wh1cwzAy|$|3#_ zpMP-p#08d>vd7|iBoU(Q47%UIOMu5|W5>szh>Xl1i>}NtFtHyLlf5~29xm8{2L7~4 z>`=hYjRAK{M%MLD2NL8^tIprl=@b6*MzAqZgNay=U-ijXKweO2#_)R-@@?K zB61b=wEM<~MN$&JBL_fJUUaCUph+ zE%7+6J$m@6@FfR!+WED1!V=i$vkXr|vrS2)+2(RI!jz{!4-=ggJ)t4k4Z}mY%K### za-s;SJOSO+?S!G1v!3e5ZA>SJm`%#>J<|3AA0G6~t;ir{h-$TuZlM-g1|{T9mLtCP ziaXJj(EYKl4jrp2Nge&}mzTu*$09!0@W>~6-|JmWQ*rzL_LjUjGh52B*P1NG&7%U` zOvW1z>XB1T3S;W`j++Ytz<9~38Kf0;JEO^>((MZFZlM-E*^CSfdV6eR4k;-qj|N>_ zQ;mMTanz}ClKF5}>E$J)e9xfQn{$!5-8DzAg4X-j&pb)h#UkkUGrI>>m_TGR-0zhv zFg}zs;7X-@$h4{Jn~&NXv48BRisdnfflw zk^8N}^Lh~iFMTxc>Nj$o-3t2KyRKFQ7R~!2)9WNFkT0ijNBHQ%`^R6I zt|zDBH9XHd`Uuj$aoG4&qgRMluy=r!i@5)N?BryvrgJ@D$T2KIyJB%OoP1?U%WIA| zeI=7*?wWs|T7lTsuS2}`={ES6Yi$xPqeQsmXS$P(WCx?YLptLxo65Dti>u;ILSEm? zdO)}+ndLu=$9AhaC)Z)4+-# z%e?VAXlcmZvD=3Apv(%(e&%yJ-J?r-<aV3yfW)rV3E z=DSXqao*{8BS8fzLT}1;8jj|NEN*YZtQ%cQH5d{ z@81c@V9O6$VHztvPL}BV=ujfjLuUv$R#`BOA>-cr$6VWva=;$sx!qJ3-Mx~~F?F^l zb?no38L$AlTAwiF{wT+loodAV>VwDp6s`?Mt<&%Kto)p4mg>%Kjv7zqrFOn;t!jzo z&hxrYPssiFyA1Yc2ZgFB`e&2xeQUN3)4A}6LxGDz&(BZBdLyo}C;L`v;8nClhnIiq zj@lqN44yGB9zT&y7IVv+OCnRV&mYkUqgyva?=zHyXDtVc*4O~!0D1!5PxkE2Xur)y zCuxRq+bZMy(2-an74Nf5X%kQ4#F~e@%{aaR&1Am6J@Cv*;;MVa!wXgMZq}`9qc&d7 zd_g{R$u|;O(NVi)9$f6o@qISlHa}+*pZaiT?t1U|PYkcN4iRO2gRR zU045L89LW1Bq0=BSC_HeEYJ+O)L*!)+1q<8E$)`PG(S5ZaVP#ojqMTY-OcRrk3KcW zd53#-wR4)8mv4wOM@$O)E))L{hpw2i?rqGOvqfg@DH9tM*mw^9yg$sv7K`^ z`6}h@X2GigpC9y>V+KdF(RzNlgW-kqfXQx}DBfK%Wu{1O+Y5+Owm^W~3m@ObLY%~-{}3(d#9 zS-hyYX*RO6No^DpgU{aa%R=;%r1?w;K=eX1G&4n(X)Z z%Nw6kjEOR!)j&}r>sC$fl4%?1fgR^XR7KE^o$9RfvSXq|fTUtmTr}z7TCKFnsFI}O ztBY^BlU@lDGWU8(CmLuSE_Cfn&?Fi*ztA@D;h??{7cREpA{DUpJo1|H~r9Xx*hhupWoaz*(3ymQ`+}`0ruDBe>r8IZsHKgHsNzYsFUr^Ds!SoNLt1`V$@_sd7z z2IcKfK>8A{pp zGta&=R*{lodi;rM(W5N`c($FOZSP?3ouL2tk}AsRjE4vs&N&t+@}9Ewy2|qh(4khF zNhlO1)oZD%UTk?@D^FJ?F!#CO>9{wb2<<7PKB1twAO`HtaR<*s_8q*{?$TI;gY~2n z;pmPEa<@K0|3WAxuwSZEv+JEG?HpnnUX|1i>c_(;0$?=73 zeiohC2ob@dgE7EwNQ44?EjIss9l}@dw1RsUN>N^Igi|SF(02l(kFDR}&#NJb^^+Ig zYk=O+h2=C2!Q}?6yu*gYkXan#057PYL1iVRLgvha2ICbHJu1oo1AB2_fTw2rE3l;b z{KxO_sQYJ21`^OVKb>GzE7ZDvx8phfz1COP05!1P=`?PO5oq0P!v??(WaElH`==bD zrj7=w9~Ms{Z2`Z2p1{3H=A~CzAbW{ z9XaNeI}|t#rPc817q-=+~oz(wne|HO5 z1S38M2w1Zi0d;^Oh9@7!cmP-HWLqS`IZ#O|BAL5zbeItuTQDEqvzJLiRkPq3|D5mE z4a>oN9M~!uxc?Y4Eabo(XICaE2(@S7Xju0SR;|zb5pf#2(^$RPsY01IqZH18=e|8@ zfaxTYny*CVyJ0s@D%n^vt?z9KA)jWBvsOpnB1SH>N^}Y+RO(sdF zU%T?3N#+4?=ErT?oohS{+WOrDERCxg&OO2SHtep@sdA2Zi;}GcB9(;9 z6hd2LrE5EAz5JL5r}%VEIYy(rPMvRMF=&ru6~xUVWdyJFZ<%!t*~h%;VrV$2k?fi* z)S^z*frK6$?+8@>eqehT6iuRRKzqt&vvsX>Cwgg<` zP9@X|j$-~fTHxjI_xv>J=E8(fTy=UAK@VfMI{Sx6R;wSq*61sA zBfi?+Inr9+uf%Re4E-^P3Is5Mkuu@;t^<$_C*}m0o|H&`Oq)h5n51=!K|m%Kt$$Wb zje+T*_om4y{vv`PeZ2qp*?GQ!TpR0oH2{!u?}R?;j<6E!h2|ut(B^AiV~nYIflBm` zJUBuOo`yA?P6?Tu3li`RBBCYHlQNoO4u0S`s9`{M| zVU?BXro~K{!R=bH_w^xh5{?6A;%3eYwUQY2(_fy(T~yXet`AVlu$iD&hi^kR@w%ERK`b*La=Tbih=Xfwp%??gXPTq93t z`6u$Te)==<304V3QST+`REHKXqzwc<`+vYzL8;Iv6pmSR`BF)m(eBR$4euDNZ5zr@ z)Cb^^TNpzVQCi=e{=yXVg_0no2Brf1$aI*4>v=O6^meQP;aanRY#Ha@lMWJov`VdW+=N z_va&@531sL$!Ctl?v)nabVgp0na%$^l5k&mojQ$IpgN{+I`2byUrmk3YtNDG;>~wH z-I+H`L06mw6{ty#H6Raw64)#OS`YINSd*T;jRSmCtkg7d$2%gQ;6km%8kmHk*^@Idd}{^@@&^Lr%Ebcu$XaW$~iIF%g@ba`ss{` zL9YILH*IJ@Lk5z{E{Yee7p7E?7p}KF*b9neMJ6>3uA;Q`=6pALUgauD^`Qm!d~dcN zc$OM>5aA?-4;f>9;fSY`QTIHW&1_$J^$m+q{M(LJhrX@P{w8TBZZo_ z!4ul^X9KRx7tVA@?viAxCZb__j0`saLV0c9*&h4Wgm5jN*o!@{u$74cTQ%Z6eDa&-QpySMz@pFE-27PV8^_ngS5C&6n#Ycm&nIQZ4bR^a&ee@ z7qho9SUS!TuD&LB{@4ucf^klrs{udG=hiRAi96iHN#Rrfc>65aI-Zw2-oUHg8TrwL z^16GM`ADZ)<5L&6X6_lJJE5SVr(Vd>WtmwdiO1U^DU>&vP}1MOdN|Us!)_YEB=4l0 zz=H`ggR9vBoCS`p0%G>Z6Ys)}pZP9V>5e&}jqn{^$^J23=4u}tZbw5anIBex4&g>h(59mMDyD>Zp*UUQ3&XN&OiCd1{${Fi+$f$ z2l4<+QNm$av}3zNqK3%ogT;r>&TWMdVpMf}IV{`GiHXD@0anZ1c0sm(SBuJw&fAGX zO|1ZX1w(>{kpabmq4`HEfbQVo11) za$NXPmr5w%3vQ3Hf`jkchoD_zCBfA-w~<@wsb-Gg1${H|O*H?bMk0lncxc#|ti4e2 z(TAI2SW$=idbYqNq~CPcd(&{Ro{xzhkl1V^bs3-g)VaOs%wygwX7K4)e4mWgeT{gt z$g8i~N)*$BZtyYc$O>(wh#a@P%6-dZ#bfP_F)BK6+}s{za_`zeigipR-lG(a98G<4zv#8Xqu}? z-#pu#;jnM*GXC~cXK{nL%i)w@fzfcvEa?tlRGnGYnj31X%~ixJ<~I^Uy=?G(SpCC- z@cj1hd&!o54Wb=lRfPim7PJAovd4=R$%d&n@e%GDZR$IHhm+IDTQR=$R(i}z^o>y}~4u|Be5aKe)Z9KV~-^SW9o%hk5 zkXgp^5Tc8j%koESq7sM8GhLTgNJV@T<#%V>cDo-P(pBy!69h6re@fh)*L>#9rJqwD zDIYwYV0n9PN7qiZqIZ7sFwv#7!}MaCD%`uoxuIYCK0Ccq!f*G+v$^T@igV@}r?Jn_ z?o@l(z8Y*ibhXDR6vH_cC%=Ir;X$~uXu}B3>AqlQeGCOO*6H>5QvnPkuglW?YcLf{ zY$t?zHI%juKmqrD!SttZz1J$$%&k z4wO5F1BFuD06z3oFDgHP#^=l^`Evp@k2aYBa0=5T&g23AY=_3n$crrtLhmFjCx z#M@4ghvu+#F#RtsfWc3U0EQoLBtHA@D@ z+veQUqz_l&gppkx2~0#R4}03B4$u0gHfADj2m~UhabghW`zag5AhpVo4@P7=igbPV zMs2Lh^W~3){-oH<@=1^CcZ5%qC_M9dwMz`iQFu0)?l3|G7ecmx@P*A3KlNKh2t z5IaQpNFq=LYdgmLIFvJ#|02BIe-DKg=w&KsM7;n1koMJ4Rc_ta0!QhV?gl{+={!gy z3JM}fhll~v-GWlmC|yzlQX(MTB}gME-3^C6zyZGf;Jxp?Z+*Y-H@-0(!@oG^Is4gr z?X~8bb1q>}cGQ()@1-1l=EB)4`FvhST+Dp$_y@Xuw9GUK!qKaPE#2>I;^HO+2sQtF zA_HQqw;qV25>;fU#gkc?pZsn;8jUo7Xtn2BOp)TYh~1G)1bR@7J{41~ZTN{%nc#dx z1;+WpqF5VTV=n%t9^|B3+DY5mZLhyydK@4Kp5p=z@R+#z(*|o@1BY1oPFl=&+fd!% z`^rI)Vs?uSU>==~h5@$MTVg*z3|fym}9>Yck55SU2M@H zpSb^NWCr-ZAD5z=9QCK0xjSyJh(T%3l#GzoB+M?s_IqC0XtCEE-TssA7~(_tsKk$6 zAoK|H`5yYL%U>?bO|GQk#D( z+;!q|ueg-eH-|gPk`rro-O5mjG5+9 zQ6maT<5Ou_0({un(feDiDv5X^!_|>-K71wpMW?67^?{2`db~@Cz$qIHc`?C7J0HkW z{pz?&-U5H+=O@(LygD@ZHdnx^Q@ApDN3o$k$KXZHPMc~+ij+i8idcxc^fAK|wbTv! zLECx^q&^z%MJa=UrGw9sJu*8oteeCl(etqkIVk_b6m*`aIca6{psnXf3Y^rzB)Tb| zMhFfASPv+pTOx#Mfx#mUbt}8UZRIq}-;JX{fa;i@)x5@nlMslR>(}3sy2_=iliX16 zKjlrAb@>VV7B(yUpIx3G?^M28x{3f&F#7~)UxtS_?j#?3;5x1j|G?2;LC$yhix_!r->yI}9kt zo*Z3gwVrwdLfd!xwvv+&0I2{59z6*n47`mkQQ!wM6%&%6(hm#~oLJQO66iUI7;`ikkVY2{{`O&V5uTn6mzw&h5({VXRo z)pzn*`_^Jqo=4xz*GKC}6NO)fc`Y-dJM2>rgCvyzBI&k=)3vX@d!ZGWP+H~L`xPf2 zlYGY+n~BwP?6d0&!SKvq9SK6X)I!hv!IZe^BplCDpkq?*NI=Riw_HUiT(urMqxUD{ z@p;Gn&}29J`Zf+Ss_6FcH>kw^{N#IH%l_e3E}MNS7GB_vkpuG=BBk`opOS0j z)I1^<@mK3NRRG<`_s28eGlMh*Rn@VQ0gxaAXh>YQW4T^?x}R$}B@|T#CbP!eN|CH| zJjFZ{_tD}Wk(s=K$ZR=g0hZPgcfkwpA7@pNmw##$bdM=XvXiD* z6Dro<^E_f?vXtbiJ7VhI^8OeeT#fpAfQxM_=rUbz;2o|mbKPT|2NdfWAbhSTOi*)zwrPt`8O? zC*o`L1Sck6$!$n)KO4+$^^~f9zOB=g-_hwoY@mQT#Ok#*`>2}I9WStQjqSZ#LYkm% z4dHj9Ja1v$R%eQmF_vjnNn}R{XFsYejqe$_wnfQoaR4Df_Ef53wIZjN`RNqk$&rV$ z5xCZ&T=$z_(~UVBcg~uNFV?iAQ22^TqBw8n_0Gc%fg9I8#-(SPY7G>;8ag4fc6qwr zlt(FhzkYkim-v*1nF#WZJIN8O>B51H?@Q zsFXBLiMQ)QcHqR{vXX|_<_2c`+3z$0S9C|L)XY)0L5gsnt!)OXEq~kS4-- z%5io0jY`|OD_(T`8U_zeYCn$&C3Zh`0;E0wWy)Z|FmtpCbyFV@>VtVvId+uk-c!Dy z-7E4Ho)aD+vjadfwp6AUjA%v>6#x2tYmNRW&nq*2facUgP?bnbSU#sT(EFfET)T$T z?2QuZ=kHqCa>HMJj^-R3Jj`oapHqX?#xkT(z?9C^&+*kFcXXPW#NFSScoak@Mch6q zcW0$Vp7{Ns6HoU;1yC)Tc`kl=q>Fs%ZLK%Y7r`LmygtxWoY{@? z0UQXMjZO~MoVXwGQAX0&c){^Bg)eeiLuJ zNq(IBmskpYQuUH;wgw8K$rsy0sfyZ@Rir3aCQE4sa@32)V#*o3_HF_8A;Dk_BOf96 z{9_LbEwwh(u6q3kmD1`xwsMj@kW?}!jN!HFHLGn^m zYrFz^rqJBrecWyiV3vS=hq4$Rl$DFeKAHrRsR8`}f%a!Z2#``iW$g#}oZua^;Hk!^ z%e`sEX(g#hS#%Z|%!y;wEDaBQyaPG3=xDhSxc-pEa*b02HOG@4~~rrq;)Uo0l~ z&JD#DT$P<0@=>EL_UKd1s`TR>dRW?pqh}Ya z%a2@StV4aLm@>(%I`{oevSGBr+IvX2o$7k_%>T`#*H|xT=wpWA|I~CRPU|t}b1xR@ zfjV?Pq5o)|k$~Jel^}}97YX&)e>;l>_!TJvK`&-u-!RYfUP>@;-g z8I=P`SG30++UKb8;q(*dtPl2;4TQq9ws@2ox#!1y!MG68}8kZBGh6X z3a_c{yZib}q~>vRz$wdg`{P++zy^wqUM4A2q>t5SdM=6 zA_W~sBHY^X>x}6UQLF0R0dNra6Z1*~^7JN$kk6K}=*~E~z~!bO%k2t7`SZ?ro@IIG zV)9yjFx&1LW1jLjvihWXtZ(qUjkB&UL%#{%9Y0UENZl+ES4EpTD>ozDPyt6lYNN>z zG|xb?S-t?0&6}VxPNED+G{u!=Vc-^ED@wA9C7rjkU9)sFHSm8xd_W`e?4#OvY*Q;P z#Xi^LO2Kl&(<;&2*{Bf2^$)I*9~Anst++i`-CK&iL2lrTK1G9e9)VNILP)||w$}I1 zXuZmO@FA+i)Vv10Zhkkg!OV*xhcD~hJy8-TwgQrck$hakh1p?%MX$zvwR0`-;N{<1 zz`x{5#7NQjFi-zJ=pw7yv$?i7Q_Y1>oyR<;*&oJsd$#ly$4o%0Br*Wwx?V+J&T6!{ zFCQTxt6=2bt0O3;fzsg6Y#l+RakBo({F-UdRMGS6uy%p;zm>BEQ5 zel436Xv%RlaHQO+@tZ%aXU|~Nc_L@W2kvqc0nvm2K>1NWA{m5Uce>F||10{EBd`0f z*R_B=4j>fEE~s3BbQBSg)v!Z^KNc&nA>dv-bb4OI$M}wQdqAOq!sL;^OOPBdN1OtZ zyiwb9q-)^|#n9^_5^h5y)06gVmXlxEbT5@(o9ws%lyy)pjy*E}JS54a9=){h`)Tt+Cc6jiB zDNNonwu0xTvNAInI@hA8tgI}fH!B@zz^-UoEZ29fcvfC|B>F9pCjB|>(K};i!dGqK z$!UD1=@fgyZ6O3=E;!(;EP zU9d@VHF>s&4T;O1t@vbOi^ovYMCQ-8}OqqUG0%^y&$k zhF13PkwkzNxRDr`olb)Rotn(dA#Q= z>^NZ9)TgXNiM_$hF|%OeqaUmbm>1XcrfM8XEgsa56g&!O4hUQJQgtsI%Jsw^W;Tn# zymB(!i7H!_ufytBDPv0QT5ts{1^+;-9U|jIaV=hs$>^$)iqsD%q_aY0hkQEHsbp`a<x_`I58IT{D>yYc*`gIOJxgGR#YMWYs zcajdetpXjltP^8%$>%HI=p=gUkvvsSTc5VdBi;3{)SU7*h5K^^zdG5KIk2QwWa^DNyB^k970jm^E3^-&atUIdSZvzj=DC-Bct!}XKCa;gwK zY{_TkHopR7WbY|{`%c2*yIO#l#$Ef;Xr#aS5{%D zxaO|=>_OLip7v?qt+@v+xv+lQWTy~q+a%!e4K9jFY$RY~$O2p)V!lw%f1)bg-u6BV z0Gv*@4PDTE3Js=GVY~%(pOSuepIS+gkj_QVvRmlIh|G6gp-tlpp{!o%WZ>QPUx5Ht z9UJCVTc&u;#G%7)-e#gV{zayqNRe&SbQ_Fzj`u6j8b#P4iZqJ2(D@8`UB(K z=?WvS_J|^Hna7VG4;asNHl9|pS@Ot(Ru!OB*ki4l{G|&6{rifr#RI<0Njp>NDeQZc zJIV2Sw~=yb4!GQ5gPt&y82nqz^-y2GlXMWTzj~dDE8?A8mJ7gZE_*Q|r~`2RUB0k5 z*sOct(ouXx$#k+Tf1S_`&*ME8RIbu?Uj`wgL6yU3nHk0LKAk^kJ+Z$S1(Xt82Ha?$ z3nc^b-@9u6B7(s?y4@{xbjo$FDu7yPl^klX*rC^hkc`-?PmzCvM`eR(xK$%DJq0D6 zmpv*($=X%Wv59O>q!~Azho1}OeK=360HOzC{W=e-DbIZ=Ab$2s6tQL>3-}6G7nXep zDc7zagQPKJS_iMD<%bU`0v8=@NL-Fena9Mz;ZX0$XdAm^S*q=~1TZ{3hZh`yvq^J8j zo5pSMO5kCGKVcefUZD6S0lkfzc8lG__)+L}rJlhVAl3z@%8YyV0p{;^KW3`&WYUqB zQrl_eRODw_iVH;`7#;gBZyx$CF!+HR9I`qRd-byza?T04C>BR%G}2ub)%QFu+wYe8 zO1f&PnE;j2jiepiP!U&s<4)34NVBI)!|NM>L^b-_lNI~TUP#|b5~u*}b-91q>rfLx z5FBFQm1zV)`tRc0cM^%f_e8TeQlmxhX6QPSZMh;O;)FcQ$+zR7)W6{_>~XEtV4=`) zy%3RJg{5kLy1jSG2bV{cs?Zh|5d0UK$iJwYDmm&^dYv=2Vfpy*5Ql28UiGq zm}6mM!f{9ij^+-vOsyNfgE0sH5D@=w#IrzN>;vS*1UHxiB38k*rZE9q_vr#amx!?q z)SSZqN(JgZh!d*{1452KAd6r7Ow7S@!xr^;yvngl!vF+5HKqutQ3qt`b~r`@pR>4& zJ=dIa@WSUV)?FJ~_u<8*=~JK$?@)(^9JxU3;@GsP=un_lK+PiwyP|yHpvhkc=M*mu z18vN?NwxD9lgxW7cO>i|)Dc6{+1nrxK+;)60m&lpfde01%mxwJo{>9UgY0N8$2E>l z_*_5**w8=@k?AKun)3%%K#2wl>yB85jbB1<>4fOPtT9$W;630A0M-om-j~X;(#iDR z7jnYavB8xCRHD#W93>L);E_)j((pjn;wE0w_sSHg8s@#2qlzX1hI3p{vf^ccK%lR7 z#1o5v;D@8UqM|8a`H8wrr!9y-ih!X<%0T$_Cxz(cN}K)XtZkw60f3$io3$CW09XGd z=4mm@nU$UYAF}#1w#VVm@eyK*KV-97cX+=@*&(qYhi^b+I?-j@jh2xi>WJs&0MX1B zU_|M6eh!ozOTIXUdgPppZH;Q6e+Ux>?|E^({j^f-hh=9a*bGT8^uoGRF3J@!FXFe@ z{Eu>_h6(r(-h#3fp!b=+>$J#()=u*8D%q=uyZ7oQuKagy>&4_KNZjnjLoPhZHVZhrd_ zy2#6M8Tl42V5Ue5HeW+YMmyz}w0UGH7~BTz0l+y~qW&MK^U$|O_+VhUYzEEL-XOTa z9sm+=8DL5REEKB(*u)US@FE;QyykJtuW%7CwM{$%L}HB4Z!zp{V}Ei1 zn0_Wer)=>Lp7uXAI05q+Qt?PeoIaM@X}=*@~;_)eikKSdat41R{)5eaq<6|S^d{qFrv+Y3*{e6A(YWJ>>*j^ zalUb{gMpnyIS^Wjy2gL*fd5H|zt~s5--wSbxE^SfdalqZL&?GY-!x3{HVXA z(obNeEMOk{7M8#-J0d`62LT*Rhz5OxaA2Oo_~Hjoe}-D3@b%!U*IzNzp97dH#KTwa zLaIWdYyf^|4zG9lCLXl(>;IGo{2L+rb+R3?F97M~atGMv=xn=j^Ig!V6x15if6s}| z!~wfU^Rfhum(Qw=#|)`Sto_8Anwr_(f(noqmtxgM6G zXlAj#69uQf2?GWiRf{HT5F{W2Igg!IhP?fro_(<}PyZXK7g(vh_euIIIXmPy zl!<-Y@5TL*dta4JK$<}c#4BOL7HuGT!v6V(j2sh^KcXql6KNzv=^W|5B*dYj)PPHV zA?7J&^ZoRwG|g$^R~+2=)~B=AWO#BQJ1vgk0Gm(5DHw`~cSr)cSN}M7=g{Uozw;>L z1X|cq;4c5y<^|dOKM2!|XnMq01Zv@m&O-9a%7N0h^%jRII;UOHB80%KfZueKuOGgI z?V%SRk$iDYnL#(H+&^1J{5O`c56n^lSZCw8OwULM9XuQRv7uAva2d!99)uwC$B ziDo^2UDls|0;KUTK;ln2`tRGP01I~cG7})BbwmnqY`z?(1$cMJaDB%|PJn}m{pbY; z<01QFi;EPO60BO=y{_qs|#sTLFcO?chLk)1= z%h?~dLyyU*0K&EU{hE?%f4jwgSb_$|zn_AOJENf-2B3X1s^OKBjt*u0YA3o|z~rh= zT&(!T(o0~-Ct3w)AyzMBoh~#=8lcgEy4ngpj@Gbzo&l|Pu zoEx2^mmia*e~a;ZgtgaKvfX)YJH_uneJ=&6?>A~@L3MfIG$C2XwpLsJ!^7yL0mVA~ z1U+2p2&??%&o^M2)(6YV&$?f%2g`*D^($Ys;>H5DK&idWI&==xE=)}D!R_!LMNCjx zDKhuxcl_yQQ4gbc`AVp!kkmA*pbDAuygjzUqn;QhQV1q*12$oxHspJ$H1rUNl7>EN z(V2gHub4ZKHRP9iGmMe~^A@@-HsF$Q9Blp!0TeT{(_i}$-qrlJz)WTJzg_khgw1Wh z*ZhYyLL2y%M(Cl5g0HYX?=JGlfW|=Nb-jm~_e~snQltWa6fDbU%V-?UAkkmfZJ~^! z9~8f6QUp#Zuumj$H=LNIPxe$HbnZDB+M_U#raYPZFKG&pIa^P>{U?hR;EHih64CV{ zfB0iCOF*+@EAW7B2j0lVrboAvM7;oH=WUB=2?l5VXiGc9WK>TXN0YeXlmtWFng;T9 zyk!ZjeC>j5B}cUTn85j@8^DeKITwbAgH(Vw_*dZyCSjg&07HyS^@*u_O`ftrv7^VS z?rB3h)oRy0B=+)kCX#R3UVB)epV1rwDrJ#-Z<@TKeAHpL-|^-)%MZV}hq^oHhsGU%O9F?ykK^Z*yl6ooGNt{tFx_OPh6v{tGa=$3)z zMMzBY{kQ0K0_#uPfp=U*hSn%Y4rxa$oMCfg$S}34-f`{WmrG>S)YPY?T+!KJtod9& zCrI2TxMvvvLF}|IeQqPV9r!ZH6ul&z9_=^V^l!Xhog(8~jG~nCVx4b%pV>*8ZH#C* zH0XVO38e(Gy7Q99cQ=6~ZQgLqem)l(w1l?#ACzHDykaRnfrm%9^X<>c(9J=J{pz%l zKyQD%w{kyS!xb2ZDTRDto^>a=?9pD`N{7*%$17-9?4Y*Sdr~(^2n0Qj3|Ih{GbMTo z&hO+&Z`yL|xK@4T8gE&aTqZ4)TZCPI;RKETIvh=4DZ>Qe@5=grOvj?;umPI~L)3lo z4v(+&V%kEBV;HR%Vi#e=H7zkoLqcy1tVOn`&QFyrbZTs{4P9p(DF&zeRDgp5gZBYb zg0S@mZ^VFok?G2;d5avn7ZbR@jc$A0g{FsdwlIz-GP9s7gx$Y$~sCc9s0J@C7OD@AW7A@N;k~Zp9TUuy45>$zZ^6uXax>xy9{16pC`(sH(lm`cI1yC@A5QPtyeAv@NtS~Cj6xPJ(5 zh?~%YC5gi!GsC*msg`W~#BD(Mp8ocZ8x-KU>}m`kW|d}Gj7T5L*A27gZ0UWmd~*nQ zBjkR6>Pt9vP6^1o+m+&g1`pA!ZSXI?o1YPr+UDZBss6|BzSA2%?rN9uq<}h3%3dwl z?rf*J_a0z-O76DU>(s8Mx@+WV6$O824KWR^l3)YuH2@8V zy+I3f$4W)gQiyLfd^2N3Z_-}YigRkG(;lq<@0yehId$|ILty>J4oJ*YoZ5bfZe`>3}2D`k5bFHSwhV01_X!Yi) z{|C)c-8dnlednEcm{+`=`3=yKt~pvA)7=ZFlFb3=j&4J2gcJ!4FybIwJ*RI|r}Co} z`)s!3&(#qngbR5N73h`EM4m8cd*TgJU?&Fpp0C6*WB|U#DpRW^9zDVuWZm4Ymd4+T zG%PQDii24~6OOJZ8dKfBi?))E^3WRQegX=@=NsC_3ag6o$r-4F43kno4#Am0e z&XN3@ir<|^dM1`Vr!cs_xqj=$#^R=Y^8{aSw&^wQcDWFu)+<7D#kyvIVt0vG2J~bW z`D#7$>TD;T0111wv@hIp*~VkH)wv^H0CSNiA;|Muiq~OJE%5$iQB(Zb>%O9SY^U7? zE(^#s)g!ZMU731kP9$PVfpFjJ>QYa#0Q)Nw0g7avSPJg2; z`T9p3*rP$9gh>9>SohTzI;syB|8`UXyWTHCW|gEPMl70~bv7FWso91zjD9!}>r^@2 z4*hW?#V_=+^t%jG#K7)cz|2%SuuUqlQC@cqx|JHWvc_bT_VQHRWv`I~IM>$^@Hwj&GBNF zax8DMA`?Z;=2!(o=+^U(eBpPWq16Mh)unbTA1Y>`Ym9F23^eu{o5Q6jtr8}#rFcRX zNylB*rj0cU{Tv9t0a*I|iR|V6HR&Q43iys?OzB}vW2XxMr90lLh)#vQ^%B7{eA((P zy%_oVX)njI#T5Q2^@Vv4#eAc0E|K^^Sj%`S$SMKXIGDU3;RVxqTPO_@HCS|GCwA&| zv7Bo`+iQ)Ho7wl$1)#5N%kabPdE}`~x0G+$uSefHp#WCQ`)2_A1N~HHbObjx;#8S5 z<_hjREEd~vZmrypuY-X7yKz*o#JURThmWY`_KVkZWM6SCr{aX6q~MQF-{C%9n0=_i zoaBAnZz1mAjd|`#czrn6i4?s~Qk0V4j`CYx+&(5DebY;_a;4e&t})#dR{j+Uhv!8K z4#X4rBcHO>r$Qo5SG?ijVZNz-*eJHFCaOVkp+a|UBL0l?&8w?dR*p_Y+DVxU-y}jiOm*Lt+%$62O-#SAb z2}S|)mzg6A!yinqq7qRX+MOGOUC}a(7u_wP-(@>U^?WG)#TvvPrV%Rx7}OBQYyKX8 zFk4W>=O@FuZ1O0we6xRF4l($o$C9JN$`5Sdv*CsW=ux|BZ*4Hp$gg>OBezeEhbn?7 zkpqTLpK_+SZgLG->o9;st@$0dwj-^(dQ^0ro``j;=D8aQAa1wk25QEMd6Iiki=&Se z8mMYC#JWwooCuv(Tl92SQWmL|x04~*!u{>s|C6Q(LyXvZ6?W$T%xvH99T#cNtbt=t z0Y@i|WpDi+52!rz`O1^QT;567QaEt3c=j{&bVd8UMB^(~qG<@zp^Er>)%^zz;I{_c z57tc@fs1LD9T8zHSY1uEv(OddYQLBtfj@IR*U{0j4EihI$GFP$<_}wj$}f#o2%vd) zLm=3+w~@zNi077WSml7t2S23#QDDIEVwu777%d@R$PEbpmj_Q%?+IrkTgV*8$<<6y zG5yCTO@jO7jScm3D3?)-o+v6Kk5f31sXjVi2G{sCo<12+zUqGq(;Q$GP20udAo z+A{xcGx&$u=x+`MBib(P@uX`sCB_uZ?Vj(=TS1T&$a@d;&Ul8#)%2D2R8N0^O|67= ztZb;vMH*AboTq(L2s!F^rGHF2p?R-o-e0}zXBwA2d2_j_;b z?f$y^emqjD|Ni2L7uFDVd8>PUPbif91?dJqRRr=&1;rihmE4e{}et*FR*;>})^VOw=) z($6})mx@=l4MiH>vQDF4pul(j4m`BBtrK(1HpPzUu#smaMguBD_0Iz?DabecZrpyf zk2ln?AZ(Y(dp$*p(W$$h{|vpZCp@XrZZ>~QS}Fc>mZNr&fTNo&3VyJx@{!0=JS1;UGP5IIDe3ICM#alvhIBr zA6pEZa#h~%4;Mv!il9<%czv;DlmEVDLqg+mA2z*ILYwM{aWH35!AA0raJjakKq2Oa zanh-jIs+Q&ZSnX=+cbd@KW7Vnh&mr6sc*Qq9fLS+(pdAl<&8v3+UTX ze)=&9hRMQ`?u(Y6d7V{(1P~GL)6z^~qC7jnXHbK;PctO<^vyjUz0dkaipwCrdMeT& zF0Z4c`0W?Fb-tJy*1TPChO7y000o-EQ<7fl^7A5$p~=+z1m%7$f=;Tk9IVuhRIz8T< zaF<(6ya5WbZ`PhFJuZ^%b> zp6s)G9_(`f{}d(&?Fl%MT)azMzi4|RZ9E^BdOusAiYKm`sHlGD{fK4t;rg3cBX8oB zfviFE_aCQ9tn+q0zh{l{*yb(*1|zJ~H=#X-r}lSF*TR| zfC8v=^ztVaaq#hPm91(q<_c-vF!r*$viGf+%)sd{W&gj=f=^%LKl`6;oM_`_pF2@RgSAc zyGuPed2>|;4HW%{Aw_Tn;5}VN*Tdyrvx9mu`qd;_-6>L4gGR8hh>#yxz7OYoKI_oW zce*PTF8K_&rmvnfh^OYiCla`|TCIOCVeI@aO$_<7R|CuElV{Em$A=r|ZQT^Csreo| zMn}Aq0_xr39-9%dm%nLo=9rFIG=MRth+nGKZjV--%J)_V^9PZsV9y!1Vu!9bC|9Fk=jy*Vd86~I9nvN+yRl<-!6%Qbyiw}rO;t5VNi9kgM*j!ro| zb)9-XRW6^n)j!5Yat~o>RR#|1o-CcGc;bXho)JM#mJ!y!lQxygm?=sHFudB|C0x}0tVD&!WfvY&&Bv$M0LhCFqqR)UgL z6%3Xc`Eq5G2o3w$$|BqXQCQ7)c)hx`^zck^D&Wv*sVC_uCH`k^eY|`2^I{3a?)>+= z1;5G}$*o>aRk1la6hvcYQ>nB24;?h1d$sd=Jt#7vYh;%>8|7d@$xpw=cruxGUusPD zcA=!$<&g5G_v$NPj;Ni;VT5n<$7G`ydH1WU3Oe%0o_}jTuJRbXfBd$24KTkH=y%#2 z8FSehq-$8ojmuw7KFN91rp5by*LR%o&=yLk$@i1r}AEs z>U)w%=xkf4+?3M_lQTLEnB2hkMEj$D%JJS77rg!URo?6Qsu7IR+mgMhN~o)70Twjy zC$Fq#{C54H5Y{t@80wqyqAJ@{ttt4{2AG;huk%# z!s(FqSJNq{oVgvx=dEZnrYH0wzd6FDm??An{yl$I8qsz4O0&el%S=N~PbY$222Si2 zvKwNZrv~Qqc;1tI$}9q~fE{>=fWaAk8{w8QMf{`z%URiZqPMs9LH*Y03E%nksgpzM z$)mF#fA2Mcv-d~mp1n`ny}e6o6=%=B<5Hve7U%_cnEW~1_zlnd#^p$si=gxp-Je_ld4!tRB$?KSp^=+ymUC|3=cl?J@?}50pV7an^@1y! zh*dNbpCind0nGQ){pyf1>glCGZ`o#^qa67vLD=$Ejyy85bCBa+`TC;;Q?&N0y@`C` zH9vk*B9mg9WrqqMs{iV33Opr1;ZX_ny%j^dS%|vE^uRNotyEthFy(|psl=p*%1eFB zS{mRi

      {-H~q+ZZMvHGDkihAnjKxpeEUrozxOXg6M1PM@*B!CCVF+ z8(WdGjTY}Mj(F3I;wkC(iTT9xEIjpDRAZpiug08`0X{=aUno_gez-V-Xx48j33(6P zR6n2Q3$4tEt30-VYq(4hHd!ENlJR(K(v({~aHkiq>TdUevbDmF);OeR<&q8I4J&Z_ z1nerijdXLa2})NiV=NoWNH+1aG!d6|_!~Z!v4$ZS#z%_h5woo?BRRP&=7bW;rP(~65#LE=V?|bUWsGgPGLi*JQ-3uv zvjH&B@+^W8nDA*`K2CiIc#dBi%&nzTAcXJ#%=oq@{Sw*|Nlp}2HhEUbmpZFYi`2z7@ZZWo*N zxe`sV)ASq)d38!t``l*Rw}jEL1m7s0oM{(r*&N;8=2z=Uz?EZR9B6(mZfuKLx0KlI zzrH?O-H31yJAs&opzU@mGftFilZG0v&RDc2c~^)_HSFbjsti}JTYO%KY_0G#?Jwp% z!(FdMzMukXKer+$gda-)USmI+Fk80DDzQ2yk*P0u*+&AeQb3G{wo0j&iyCkb9$tBM zroNpvtr4d#K2FO{kH0egBoBAGmbz2e^iX#hr$T{Yz@$8BN0&#@(R#s>zMnL6I`#>( z-zt&p4U_bZyKlOqI(lyTIxLY)ulFZ?M5vUF!@AzlIrptGnuCLRkst>1x`|e&0vOJkK0U(4qT9ULjCZ) z%O-D0T#E{fa}HLU_@Qq6KINXYwpd{Q{svRK-Q(QikA^l}Sw1Wp8+Fy+mQ>yR@<=_b z&#q7ESf7xYDP??#;tNkxi`b?pa~eyz@?^nrqBYgJ{09&fx^rF^ zy%4?ATK9>zW};)#=~i?20eJ;3=AqRoY1!zh9w8oJm^sd2lcw-j`gx#zNNc%~4#wll z*6k?7n_QacZl=bImy>SppAws7Bq)a9Pu#+v3(uII{1_UbIyp56&H2>`wm%Bi7l!0% z#~D}I@{ERoy;wr4-3={aBavb<>D4|#?;J7VcNpo!N^R2)O1{Ylky10IA8~%y1Mon* zPq=jw9334n|5sVuanH9+kL&_0ay@hE+GDeQikf|y2M>aHlbqj4pJH| zY@8I5X_b>UJ5Ukx8}AV!nn@@(C54Oy9#1(_ZEM)RA~_pEz_JYKDZ*uPT;znK4W2CSp zBtBi3xyYQDsTHqPmhP=c#v$=F0+H#od>%$3k#)~1b`=m<>&Q)np&^66_eN}M?PG|e zJ%Pi`&ossCblJW69=rV+;YNmoo4Cfj&W82V0OjgZ&`j}wJ739-wd;Qq=k?YlCRfEf zQ6p>VnIzV<(Ef=R#ycE8ByW?MT>Mjmmc%Rm6`%m^2pHr|(c-(aC}Buu>Xg*|)5lJ? zOrJnM7C6Z9f}AsV-5W@vEEIFwc+n{yM2s*LYa7+lCySCzB2hV2BRtTYBQH$YAp3C; zD@sntr1a#ay@$Kr&#&a=X79_pn)YIJ;9kCea-2Ck zi@Be>^2u}uF~;C|?#fF&biG~4N{za&iifpS9Cat_9Kpnm-Iy33ZF)Dl)2^_sHYh<< zf%1pdrAq4Zisj!~tDS?TKH<(*U6=A46U=PhB^Q%vioadv#C!HZ70*-^?W=e`777k3QWon5}xGFC!XCgDP-Y@bNUSo3W(NjQ3}Pj zQnU6Wd>%73*vJu%=$$P!9!%8fuM@MwR7xJv6~8y#_rpf|cua{+zU}MR7xL)X#BQhb zIZg?R0ByBwA}uDnW1Mmn#lNN}?xUVe5Y5*~YudS%Tcx&~zweXfdp;v|A zp@juj=iq0SXYXXRmC(Hzwl@ruFAJ_i)To(xnS7$7Bv1zEy}scB`XW<=ub~_dx`;95 z9J7!+S)x1|Bkq${&6Q|d;Q4#(re=wyym2lMF$O1l!8Vfn-6sdZ#Vz#nuK*9ho&FL54p=H-tF{t5k*FhhIC5j)qAg%o}r}&q9*$s)eVQy0amCU2GQ)vaN zCUzq(et8XA;)yo>xsd}E_`GU-SWbd>(uUgyTs}LLYp{~9HK5_DH_aY{raW8wit*&s z&HufVp%B#^=bcD7wd_smU8 zIur(& z9QvIJ8J68h8df)VJCC#Hzf6bm`+G-Czv?Y?&e3;Q>K7a*M|jNd&*?I9uABC^?HA6vYlkv_p_*7+SOx@&=f?o)xZC*a}(epTLT9w zoG(v1%vQR>>dT9d3XR}+jgCAC_y4>sKW)V3Bb!3?*B6Kd?OlEi-*_eJP>|vK56TzN z3I36LRARrHrvYVLDxTJjk(=sc(T28Vmyn}h^ZckVTz=p<6$zD`&`OcN%u!k(T|FVn z_^m0>s~Gy;&jF2t3n?&LJI#s|z|5sT!*ui@e$n_f#iOy})idR1AM*s9~x z<0GPS43*-&+(*)4Y{Q`V>&1kYxBS3LC-G9X@jMt zF~-L5a?%I#(XUdop=>^D8Zxocrk!4ux@?fDwd^ik*4i3`qBYB=7sU(tXYmc5a9d|V z?NQV45Lsh3?hPg59ovYg6idJ(o6bqSJ%lK^hX0f$S$31{GKObw4v(9y2hRGo8_-H` zCeox>>*euRZ@J1*&LQPk8w34=byeN7Z}I*7)p%S?r9n|~?8ag&RAIPaBza=M0#gVPl1ry)iq#8~!c9xZGL!>!yQ3L4Tww+ag3cQ@%(U6dNDnQfXqA zB55fO8BXi`D4=fWIQ@@GE5+IQ8HflI2#wTLgcpT(S}y}S@4TUtfE2-9ym%;dH7%&i zM%%w|C;rjR=V2T|#ZGa&y6Jt+OovO#?URygqwLmi$S zMLofZaP1ble>JD}$FR7BqJX!vk27CLk3c z?{0ljJvhYQ=rs0q!u?IK#+bH%)}@%&9%Mgpr#i$Z5g>g>(VRl`Y-g`>5t4aSMF=^r zN`R2;B<-QS!TiyVk!1ysv!0Nz1O`Leo;SxBDi= zKe0fKN~zVl)xySXy2aN`CXeM!#1SmckyN^N&(*ltBZ7CnV5C$yaJ%Tr0KHHPMOVHU z`))6UPr3ATBSbIPfpQJGCRav!L%GNKj>Sa7CC8AW;{(e-{K4ato0d3(YVZBh=TOU{ zdk!VlbHj%N)lF?h_=+r*-(XOIF%ZmcOa@ep;eHJtXjoA1T0yIJpoe)4W`nRqUa0 z;hi-(K6qMc^m&@y6EK~CXqb{H!LH2lOcw_yMg243$QGzHqvdKBJ7Z;=L7vqWj4QiJ%zLg^PV{wEK)s>!0Bw@`*?$8<$%Cy&n}N@ zF0f*kt>rIUsj#0#La80U!ccRK7(X)6bz6FFh_vAL`XUvsCmuPL_oV>uZ_vBZGH3a8E`*P7F=hjik*j-1y_Ws&_uv$4GS4`EKT>tv zaVX!%gKZVcwK@fR>JJ;1aPjGpcRo8L^rE@Mu9ZzQL(rvN_x$7wBfnXL2^3@lo`j&{ z>n0Vg3HORZFdqAk^_X6c7IYb{CX_fDZ2A5&Wt;ogA1KyUO6={^$u=pGn<*$J(lo?vIxpSY zjcIS&tS7T3*qY>2zm@g#Z5sVR3y2X1NhPiQHi3#xc8CH3hby$E$Z|$5{;ZUJ$Y~mnF!VJ?+~rPGsVR2}1e^kgX*xF3CfBnH?G^$g~lIO|Zv+9$BnCSLC7O zmtat^r(yp^sM|7>(R~wh=R$N6rM-^|h zmGG*bwNA9qY^6m%-HPct4&6_G$kH2o=TYaRWs#NdDv$$??uE0zH^83qW~^qC ztRqzK6x*f%$$NU@Z*|h*q$+LtKZ$8tb>{xSRLkO)CYF7+8q{(+32o)Pznw0{e`9+s zYP}hdfkz_7*J5^W^nT3tZseS|n)XjPeUoCfiUNuh*sE|Rdk7t_a!83WA!nrtYKSHY zb_2y164EjLr|z~WO?`Zr{RF&_O)u+boXgjj9j1E8tSXLm9CmqN;9}?hs_hm}`4d6@ zxhG!7{LVe(4NLyl4?juw<$!I z(Uq~`_$w5+?seho2`$&x^mLU_H3pIRVOqsLZ*Bg@j)v7hHXkMY+HlaPnokYj3XeQw z{D}^PVnL)UfDj~e?pSkeGNDwc1v3!r?;qeV-_9i4K&%hhYqvWlwC3#pj%ZZt8=Sa5^dwr zn5t9GE%vWatDI%MJ@IMsV=MFr)(bIAbMgIy57Vuh&K)xmVVW=Qb7 z!< z*k3Y8Rp{bF3$xZ8c775Bc&u^`*OaKC++ifY?G70UFtga`T=@{-BZxXQkZIIa+X4YU z!vAN<12fN~#}GI_-Y43Bk(~-0}EahM}0%aj+9T)4Q;51ECG-t ze(Kl%GKAgO!b}!Yq(HkxBOqq}|Lrr0nvv9ii+#4XdTLTE2lO}57Lu%dG!p5)7~UyB z`aQ|x6#q#&a`+rngXow+To=Y;%IfW*-9!Pf>;=Z=kK^D4M9O3GazI<$)-~G$Uo(55Lwj>c3`elJIf{+&w)srt=}fFOPUBRii6&dh>tS8#dhF4=fR?_0zJzZ`PQ zrWm0RnJD$u>-L1Cg>Rdeif3q_1AS!I*@t!aL>N>@#CHgP|ERgUSB}*Zaj)5!`AMo2!R`llKm4f8-KKZGVEu|(}NETLbLvFWrYTN6S#SS27U(J6e`cdTEsxc=IydZae z0gRaF*Bx(0?rl2w>R!;N`mVt4dCgTne?-3i<>?2ui;&`NeRGGuw=f~#@6usl%e{j$ zvOkFDKUoyiNRWR>FhLm9^S96O|6ma(bX+||azXeibP#C0B`dsIWRF|AlevC5B39C! zZ@gEH^l$D1cpk5|#~n$8I_m*G{7ZYqN{@OCUQt#KLS4;MYd}^br4X=pU-o z-t-^T;W#x}g53Y44pZA06h{Muc;JnQGxI+dp6vbwB*A|YAy54y#8M@vMrkSG_Iv2H zKlNgzdRX9R;4oNaE!>SC4ZdobIQzGF0TGuB88wCfzo~b31rm$Dxlc_8YhrFlFt6TE z;~c8MW{rnfj3*v|>M8xfy+5w}z5nokSao=f&;3e`)9|)`)D{2%3rI*R3#d3#q?ye5 z^>&7^*>7G*#SYClld90%lJIiGXTkoy0xNr-=y*0}rwrUV*1sqN!#pQ_rOK215Q9Qv!A?8M`K(D3W7z0;w|h+y`NQ2VSX%!JY0qFEun6({ z)uG)T3EifslwE)xVY|>PoxL9@6;g^9LWo|F0ad->SHrvW=MNDku)jaTT-@cS?X*_w%&!^z!c763c= z;emSB--=|nCyr{zSLlAgHyin3&m6;l?<9d#3Q<_{?$m#gJpfTV6+~i#NHVhf$Sx6J zGSL2v^$QUD$n@7!0}fF)N(D2=jo2%ltrsO4`E7hD>4aCt~7=}?g%Fk}eJ z{U1amKfg1uv2hZ)OMkV?gO8DT{Q@pB*&3$7-GOySzd~QYojPE$Sg<&=UmQ~PcEomj zS)ee7(hFD>+kKgKY>^OTKO>7-3@T_%3jxFVE&i#kO#-ULVIIGbU1pPg{WpWnPnI5j zcV+*d(C++>@ljd7EUc1%uj&h&?XJ>0+GRaCF@e;7gHz`OgN&hNZ~BMj@*_%+7`g?& zM?EmfJGb)&ePElIf4%=|<|6_G@~&J_R#zVb6AqJ%fwk)OBD#YVzx*-MWNTlDQSkc> zrU)XG`&EocM4`rO(L3h-zs@3|ei`lLPHT?i?$?61HzmIc4()VcD9vB5fEEkJ#0#gm z1O8e-{yB!5^ABBy>(AUuYS}(MaotOs#6q(^EUFd~7f*!JoSOHK9<5 zH|{|03W1BiCP>YCUWnbUWu#jI7xV(^;)PyW08i!R5w>5G=(EQH?BQvuSKv0}kgf(P zV-F9leYM-x^7~a#6afzu!a*ke4^jmk6*oe9vTd&P?wPRm{y*fsby!vF*DfpzP(tZY z0coT|=@vnd4wX(tK_#S{1)`*YfKmd|-Q9>164C;Ku;_+G_u@PgaR2t+@B4n|eCOYD zxvq7o7d&&$XO3sYJ?=5aro+(mI7(mCN zM)zNSiOHs`Irz04Z_j9gFCo5CFeZO^PYL^6eal{#s6(OF zn!r|zi66(iuD#FtNdZ%#ZpjtEjz}mW6BmfTzlR7{h0i5WbX`($2SAz}Z~&k&#SkU4 zFWq*%#7)ynuO9kyJ+Z6VT^H6jlA{j}Jp*2h7El zAU^2yf&vxz8rm%@qt_=_*wIM9pMFxXDdHF8nH!eK^9^gng?G1K?P|KFXdH4vnIe2t z%nZS~+QE_mKZ>5L4CD;>@qoYxSG*tv{!ETum|h&V*d;|l#WomwZg;sevl~{stV&MW zT33haG@$H2*~7~vVzyi%L&^)YzadZDec7~RLK|FU*ob-2cI#}&b^TczN4Sw(lqPoj=qy*6YPX_RO zqrs{YkWnEF2a7VDnE@Jv5ws{ir~YXf!e96A28{PETIYy{me*SD)s{2&gdhfNz~3C6 z^L5V~Dg2Md^xv1iotXqA=bXO%Pe0+Wos(<`0oU^Q2K7`uwLIY63PN=DFRv8_|MTq7 z-_G2Y%^i|Q2v@e|^|P1d174GdS+Gd$z5gr;{`R+$%(I;L|K{?FSz$qE# zu9nyT;K7}Xo;4NP;P`>&$@AWC-;+@5k~A!kVA;O@-$=))!IA|(Dq+I(kOcT0(*$`^ zZ^Xt#YmOg8Zdm06;)`QrR@7OFQX}t=wZvXXn4^yObV98kanZzk0?c|{Ps3-9dM&|I zIZl)jYF7-#B7X;&^OY+CXSyF$zLA1FP3r|a=vF|n3Umd!iK7jlL?hHQF`*9k%QNFq zxX|IVcK_`pjtH*_5kM0^RWV0X`w6_FCk!V?Q_R<(V2G2l&Zvc;lnBo^8>G8EDt?`l{rpUxJ)E z*y>L`g@Q^T44s#_;hj(&$e)w{uT};>vb@FQtVH8at$o3QUF+7DjX=Xw@RQa)Z~?SBdSS(zKz_XlNSrFAv>_irua3R9z>;|93;q`&$w&ak z@?y4rm9iZ@?g0q`Pi7{Ki;&VQ@5`}#E!S7Uyf^GbAo2&!_ zTtm*nOCU9P5CA@OCy+GI33+xbW}A(1M1OcR-Og=VY09cLL=7%b~PRdb%y> z_X8fM6M7tRIPZB0)afoUfX9ix{x`=T^aoOo^z+7i(%bL9`nCSob}6sL(CGh{n+uGS zE#MRb)p@bj;1#qX@yf+9r7z?_SJ_ctuBdX=x0kQZ{vSHIpz}F?o95dv{;FS3&lL|O zL|9h<9kl!EGncd07tjU}xB#CqQD7Fe$b(i_&tRpB2Yk{gqz`E4geW@e3v+hT_^YHCoJC!le-3a6=PrD+~AQZvNcoxi73(0O6@i|j))VFCl=tC4$;rF=a zwbM8VRK5ew87R`3!LOrFo^8TyP+WT2Op3r7n1;IBEguXZr|~;T zqtOV_dYnZHpu!Gvo?Opd`)i&RXT=o%jA7)ILD>Htd)@U+w?iTPc*D>+CeTdw9 zyiP<;zV_elv>Rx*xJww^cVa zLimZ94GTYwx-8`uxh>%A1G2c?Qa(9i+^$baz4E!t{^44^=|I2${}N;8DMbzm1^4I0 zYyidOg<|g@k$)}O3bnh?t5!q_pF~1U#XoXrbAoP@8F!EF6W?qH6G2kt2jqMAw(o_} ze$7=+R3sJhT&?MJxo?Y3Td){DV*~Lavf4ow7PP7@m+q|KdWNT6W%C|24C*^s!B~u+ zCt~OE%JDczGe?4Uv1ER;6xqefzjZ(t)D`6G%@y32Z=j+XfI?>}G%3axE6*d1io7Y=^tRyBVAxj8c#@-ZrV6rYa} zaftSw?(EH5gW^a4$bkBtzkrJ&Rmt2&P2(4{9ns-0kE3rtOEuAcG(KYOg#Po1A^H($ zDXowlkPpc-2y z-cz|*@oIDi=!%_oueI8xEyDHw#mfunD%bhkf;!@P2CHE(HxTW^FTod2&l3Tb$*D-* z{Tr@Skl8Sh*g5dqXuDSF5g{QQ!o7--S+!AOyyplQJ=0vW7A$kZEr(*t|$MygaxCAK2xm>c1c` zGI&8EQw-Uca-SXyuL!$dn$`XqU!0~~$ChtZ>yz91K7X{BZ5{yr)OYvV^%E38LvEsN z)MA0IMQX;QI;hX#lTMd(x07T1qQmUp8(&;CPC{ynFN7@a{n|T}_0|~0bam;CO(vD|TG%h#rrQjXnHP z`3$+Wps`u0U1Zu^5*@zXwfU4w)!gD|Vz$+@Zz=@T1~b_vzh?(#MNbT7YF{1yvfr%a zaF0ngo;o;~FJozSkvMqev5i4M| zEwu5rI}d$iYYx3_PNaNA8Vrae5HO%TdbNF>-)c0;J%ZC+hu7*i3E=8AwMTLldaj`< zQqEo47kjEbtrP;9lVyo(&?-iqEXag6;eIsP{UP>fTd_+ObSbMIG&7VA zro?{M75~+TC)|2sH}HV6-kiAP1on|qVx~vj{%s?c+cPAQ1p^up4fuF<(@LU^UsxqU zUV(RT%E2+9PlG=;uhjs_yI8&>GYkzTrLRD1#W_9U730Oy!r8-}&or$yCEi= zHMsF}e7MNpBGw^SHQzW+(${8f%)S3%rp@$85l^1q2GMuUnYx436r#CmhphpL@|S&j zflWm;1O;;X-=j~ee_fIaNQJM}KhGtrdfeh~Q8t+F&c;Efa1BgbAq!P9{nA^h6Y7Cl z778Hed?%1U4)V3BKx5fd;+U3b{Z4xIf>OxsK|=B4H8SZFrmC^mQad@JCM<}^P4>V# zbXUw@byzK6)aAfrJ&6LE;WvwG8V!?uMPhi%A{q6{@pMA7?4JndC*vt2JM(4|GE*MH z4!Vk&=%swHX4g>rbJU+CcB{z-Q^7;WleT{;cNFl!C10f%38ohFa>x~#e{&fu8U^Ng z&LkhuO_Hvw2N`%WCcg0Jr`o16O=hxL`Ahs?0rr|PVq72*bGNa-XpA^cG5!W4x zM-Q-=1V*Np8*pFaPNBA$`JPEeYcelfWQJTGz705E(J+tCCHsunp%+d+@Y{r+K@nOx zGlJqmwEmy}Bqk>CvIZ!aa8dt*tL|&x@UEElabO;CMQ<>oN!$y%I#l*7+;VodQ9phU($~*~A);G!@b6ArM{M*$TsNTK2#oCyYBF3VgQ-Ctq zm>J9-R>jTx2t$pOTa~u5zn?3&{4LeaWbpvQ6NlYkUAd&3q+e!w9L(}FUrELqnW-`rZmUgJc@`AU-J2z`@OWwU9l<>?7_}LU zMA=S~+4(fQrF&7`A;WAN9QUQJkFYxFjC{Q^9qdjO)hxyPzsn=;Uh=rJG;6UvH<9cc zHQW)$ry{d?5@1!Y1k+Ys_d|G{xE^^j>wHehW3v+P&=sX%(KycV{D|e^Tz$taLa~<5 zn3V}lLU~kBtI{DF6=%CJX)1T9nSbP!HIOaoeTauir*F1*Zk%| zc$}&we)o5kGex#$o>xubgP+Ag+8GJF9=Dzp_-m~ z*>w`xb&(NrlbVI)Pu>@WzPZM-;n48u&Dt2RuoHsd7q3;iL~MNG^pyY{)0!>a8|pRv zlkgfL8kyeXt(ASt_szqYZemzur+V~Mn_~GuuPqG|e9;u+p8vnK4eX=ZUQnH*&9k>f z`-W{^)LaI7rE85k7Bi7_NX8`hm9zmQ5-WcxP>b{Z23-)f@E)#myddWNJ`>giA|xZL zK~mk?Sgr4^rZ!zdR`s0F0DkJM`fw7~F!?6+hH(IY>Ilwn5)KG&zeu$3~m`iEQ!-;wqs-HOlJyxW=#tocqG zcq0sN?#GJABb4HXg~{FNc`$li3+0Vc@fRr-9^6*XZu* z#9HX}R547qLN)b-60_eOpW>T(E&T(?*7elu<**eS@TtW`@otP{hXjVQdFPqFA6>B> zmo?(jErNZ0P*Rx+r{)%@TF@i>K4_txoZQP8Q>&YPLxNC6_lW@B}POW2OX-%9FG1sN7 zWwrI3l_?>b41d~nxWb{k040~gU-@AJIYIwPgQBXfD=k8V3C$DH?IBDW5)oj^wq$oV zjjpi{>quP@hU##Zpnsfp+3=J1yG6dHnLy1BSY3*p`Ol2hebI0@SD>KK?fSrqNFvpS z$l5E4wg}reowCQ3{uaNlukBEW(wWrPJOor56uEvY&e@)UZ%#=#tPV{%1$ezr$QK$n z?4Mp!Z|MB4_aut)B6cX8m>^V6ov@oBVFO_&C<`8&sWlR;Yx{^wVK33qA8p0CTl?UFeM7(2v7f4;?tGIgX*{TG4m1PX>AiEc;-SR)OMF z*?}dD&T9ti&Vu4IIG3BEt>h6MYF|AOe!?gvstgGm^g(c90Bepg$Jx6m>F}PD8By&= zFLas6+5|=?Xumv)M_MBq=S0FAhvBUv$$vtb&A%G)?onLnkVXTSDzk)o`u$Wjc7uus zb6k;hEC~$h)huxe{!*#-J{75Ot2TGe?c+3-L@`YVcgyxP3+ugZ!rKPzZVRA96P{93 z7}Rj`3CJMV&b4lZa{D_n&I-ch!<2)?C4=O@$)n&_MAfFWvdtWupH;_9T0>cmWnmtv zZKU1FUDa2fw}iy|RTj@Jbrm5it%pUAg}{!PqwAaBb;2TNDRN%<^)7qM#1HI<;qZt; zaU>nZMw^Kb$$nhj)35Tj@dphy2!Whz49`-Y+)StJcP}4*3rZw^0>;|}%Dty^x5uc% z+Dj9=5HIGMw!_YxEr8-1*2-4C5+^nH@fY*(*3QBp*4{_d-lVh<=6^J=wKWVJZfgQa z^H$4|lOXlKy$9ka$b#l?vLa9sG6sMD24gfDp7Lg4Y6RccbD!px2r<4Z&R!V0fz1~M zK#)n0S1KmSH>~~HSer6HNIQNjq>ja^ra$t5Dk}TuV$JTb&6B2xg>X9&I)pNa>Ty?6 z+akJ!mQwx9I5X855~^}xby)qnjut*I>z=a$pXcrP9*d!!wh_!jsHc;m5wgkhdnK2Q`p+o-ILpx-+Gz_+7EMk=FGn!Jr+9{`%uqg&p5m+J@7ntb zplScjce@%jmWeh4^0S@rnW*W-$P$q-{#MUE`D4%FWrvBHrV5W^6@5c862&G4M7sw@ z_D_z!$s;Akn2@Wtab`9I!mQwUo%Su$sAlgm3=hx}&M#5|s<}z=Wj4$$K zy#7>aJHK|gdo5)yDB?5D-bYuK<>6Azm2_^j!F+vzkhnko2~7z$gM0)5Hu24))*#)* z8(h1O$%yty(109wT=};!OTmV1pM0HOAkJkcC!8TCWn?2viR3hJXjJQYLbP`a?S3Sk zc@pq`(FmxCADR_<&=huo#u_ObMI!r1rAzx5;3OYDo+YmXB%+8^tqe z=@!3ArG8&5wNJ5curcI_*~?(I^-qd4$%b>;bM$>_?J6?QIT|}q2l`qihMnb6p8j)j z`{okuDA8BXKf~($0FuIWVAiAY?dU2`eNSV#G%}K9HItmfn+_3(Hg()gqf$5e=&KWG z%(OaQeW_4GezQ5(dJD;OtUdN=F|l_>?trR&V{W^?0~d!0%`jL~P3=(MO~n0yVQ}2T z${k&SO|O?37WpK?y?Lg2C^$;H)Z_{nAd@v00d4K~y6{d5a^22+5R>gM-)lCEq5n%Q z>@X0hZt|3NZhhZLS&p$#1BNUA+;FKjI1&VPd!nWlTITceCcApmfXi* z5DFI|3>{rXf4g;eYrbczfRU(PeVA3azUnq95cFG?gjq6CT#t&FCtE^%&3cljP{)TA zdxtV{`D3@nT`H_6>{j={WY;XpeZqnlEtXq5D=(UF2EO5Yu6Z|rIxM?{ljqg;BDJ9N zWJ-WRLZ)qPth_455g5%Z)r*v?bebRnkg-L0HH;WJ^KHeIe=7Fs_&oow(p5iq*PTnO zi1zhqmO9zP9~Rqhi^HVmyHR(ec#UzIRjwy@y}#wq@6>v)<_;;+^u~dm*=x7w25m_# z-wjoaCo8@`Pt3f0oj)^wWGs6uz-&A%+_{EsZ!k}j7!>385FP^lovdEzaQ@Ld7T^s< zCNwp>Tv?*~5YS65q0nC^J(jOm_e*qb1;|oDQMYQ^OO|7@mT^|4t`7>DVEF7UF-^tE z+J4+DIME_+r6^*t<4!Wi!7*p3E9jek}q-n@an+Uob1=?{>>B#9me+uvFd`{X{Lnl#X5Z;nB|)QKGc zvf{#gnhLR6oAbi%wG8uLb6^iBX;#BR3N@&S-VuR9Nt$Wk_Tmeb<&A+igD$EFE_%z; z*xjXf9p4e1Q_kTIjKN3-zRRqPMYP9@*0)W&AiMF@Gg8_24J2+--WL+krMy{3PLZM( zI!UpP!lMbrpO7c5OCk%7hj1qmqr9Kb;)iD%mG)B~buGU=W#jruk4CiT0;`dA7rR@A zsKRh^ugJ3!x z^+7?wI)eah!_rqJ%1+}jI6471=ogefW1qgsMkwNL3A%>Pp(Aj2wWg9##DRUZ))l|n zVYwN=Wa|LD5ew5QG?@ftoL`e=L+&~~`SL>CIbf})%0eMUIrjRE8xsJGWVARbq;IjX zOQ(&Fgjg@}l5km`4fERPM~OOH3?B+wM&*@&N+wwRPfe zEk9POyDQxV#KJ6G`6oJ;1ei5OZ~MrSZOs-lyWUj3k>0cmX5I`sq_u~V88`ZTfs4N` zaOiZ2y-vmdoI3z$2M|?EL=;pU$+Ui2cFY!y@l<|QxF%LZedC}k6fx!94kKE&`Ry4l#_+=m2sR*B{8YutLn z%M(4rV{StQ#)SaDki+l*kZ6JZrh~f;D6-`?Jz%I&f4^ohlQp?M)m1MPN@EP$y4Fn$ z9Fn^rR(NG}a;KJnV3bJAg}2{I58r-iusO)QxWMTPk*~9>7tF)D_PWm|^b`_UeQzG# zhk#L4AlTOeV&jCap&3vs&vFALudE0}lUO7qEgXm@{2l0Wh^Tkj#?gOJa@<4TQld)D zqR!wah?;D|Wen{Crc~Xm{l=&#JWQs|WDb?s*Dq7%6y(@ctQ*?*}hDdTc$t!uGyw7928D{YjzyJ z`PF~w=7fO9Enx?XkbZOZh1P=``mR%}an+A}ZlNO3ZqK2D=zT+yWTt*TYH@S5e*-kx z{NM|CaARbB3)iQ5c+8l+y;xzj^0AItUk%j;c?a&=^@p8vnmaicg(QGxo6k?$qu|R7 zv7Tr&KCXJV=e%*b#{G~C!08K9Z#GE!GT&~Hwgwgw%Nm0L#|IW~A(U=}B1bSsudMCf z+S`|WID541n&m%yC0?}d9K11wFP$Cu3Q@GNWWBK_N`xi>Vtbtw>BI^Sc_^g3h&Uge z>i2X8BNed@20L6V!Zcvg1Co71i~%TDu|8P+;1-(3ydRvJUs1~ko8sc`nwTW$QdNS` zPjfTR(W)Xk$+WG>dhjjSLZl~G13zk6tl)$IG`R8!Fd(H7n#7&sNZa_C-V3XnjvFuE zy#O)+8RVCzE#P-Ad8+O2KBj%R7@<{VcI_?xHPaccg|7eu?uZt`CvH|Q5`Ofhow)Yk zz+`oK|Eh$`B$x{S`Vto>-sO0Owd zetfF(MxHShl0s&*KmzQ*a#sXL6&{eQyeWlX9}LPk04B-q6087)O5>G((@3QTM_J+z z7XZyL=X)rR7#JD)u4JKCGU*qiyaB&qu1@CLU>AJFhDhNn&jp$nH0l>RbsVXOf&mJb zq!NO1fz#!rtHrjte9TI*U2J(re(9#CST4qa^!)Gobh59E<2BO11L2-V<9GH-UWV8$ z^qKr|T;xhG?)qd?(_4!d=%h%c1xLIS z8w?P@6)x7l2>FqrlY8r6_XgI+ryLX%lz_=w57#u>S@GR~})!HemZ*y;PJAz#a; zaMomvRF=Y1rRe&pFDXd_l=ox_jh6~BV*AVRso8-n3j_$5VKDG@dZ*g14<-nx4);+& zvo!b%?MFUljA!JLb<4;N!s^=I8K!ADyN^bo8zwEK0+;OW;BcM&r7DnJ8NdI;y+1?k z)zYj|DfX&eisCjsPG*DgYR4I`1RKp@0v@rL`%lg80gDkEoawI1u+TkBM zt+trTiUeQn4Wc_C5s5cmr_5awETNYFz>;Z;&`s;ke2|BOszF6+e_k8T#pkye%CX(# zGAzbOSB}5;RalAJ`9QjMAy?DcO0V95>I180cOXexIhrDi-NhZMpaHgHs^;wuFDp3P z9`f0hXn+@m2cPCJfWCAJDIz4vu4Dv)Z@J)r7jy3F0#i*HBN@eQ%2dQygQWRQX5G}_`wfv15@8yiUq)r3 z#O;~x1yiL7WV&ZapXA1t(`&|4x+H#f4DlE$aG*Ck{*)6#SLToqUDGFZmpIDeUGj~5#yjq{1xO?O>$&hxSH28i-{mI>- zrySR(qUB4cayKR~1pzHVXHdR2s*F24(fAgm?C>7v5amHwGiYy|;kG44s9_;j{o;_b zcHaVk7*V&(uLo0#A3TKBZ5=;uqwa1?$AmyApChEsg9IK6GKSLN%qQPtwC83*C7S5rt&)b`P6u>Zdoz_w4b6$$oDf}<#Q11HI)mbMSr^>dXT_&gHRONHGUE`?v0gM zDTOAH(yCJeT)7vm0NC0}nVNlgQ7;s72pa<6+2hG%S(*xrDvujBroU@Yj(QS&y%_Q> z{KR#VyO_>62}7&eycC7b{cN`8OOXO<@8f7>djC_6k$&nF^|T+zKF`(zhlNao^xtGT z4!_qKH0#Ajd(tUypC0D(+}siox7aR7Mk{jYiQazlnK&(|DUZr^OBWpwm>m7K?xRUa z7w8DIL9S+*@@?=HW+DdkzAK^iwrj-w26Dje*qpnr3l1|O+atweZ3D)Y@n@m6)*kjE zI|NAJ?7Y3FL=k29j5HZk>ch9L^*#O;s^&t^LNi|+IQ0;6h?oVIT?sUaJpoh?NvMnI|#=KM-;rt#D57x6#F zTZu%$uexJ|;B@`-PFfgfVteJsPRupZmTC^}kq$=)G~u(@Iqt6gZW-0yEOZ-|uO5an z&?+0?eu`=u6ZmvLhO~mSP6>iEfd=~E5s*%4*}lR-kEY3pSlrD387<8Ii;BquZWIhlPU(uyks^WDzoV~J>A-C;6b+mO+Rh|FkwrtJbdUvfYe(AcJX^MO} z5xGZwhOK0P7N?92D}Wo`y&NuHd>6l(!5jpYv|jmuI$V>`zj>z-!)*M!tVM4GAt9Y~ zcPzgWrO5qqQq_iCBv#0G2^A>-;}u$?OoDERKdKk&&P&EGnrRJaRim4l_ScBw^(YcI z+z|MgY1l5RKU4QBN3A_Xeg5T0F*c9Mw>>kR-4&q=OGksA@89^p@a2zF&5*mppe}SX zR={$`CelCKkeYLj8_(}=ha1FWUT_v1;2dL<#fs=+?<+i-++G?qG$qA6%5Qu&^4)E( z_#N>JG!mo`E>N98Owd*}V7LvNWoQiIA+yv8Ukv8R)*GNhj0mmI8#95;9$T$E3~~g9 zo{syyru})3sDat$!`%bcq9OdIN2uhM1wht<-QLyu%bOOK|70#Pvy;)5c z?hoJ27$?ZlDZJQ|B=a?(RG}2hbGwcD*YEM0C(2wz5AyYJCmMZ=+q}j-tqmhsH07=0 z`_pA5d^EjLpaamlMK6P(`=UT;Hqt3A7Tn0ryKN67l04VEkcPEC*b&siNL)9avX&Fnu7qN|IH7jY)1NJ} zcvP5#qo(LO{!cvjHyF=dZLM5>-s?k-+yKl9`DhWdB=Cd5L_cYxB|`vhqn!dRYb{R1 zAsX|@JUsmc52^umH&o6M_*}oj_yK0}8epGhA|k-A2b>;bz(bLrUb6~^Tg4=)CEfcH zB-Dre8p2Xwd&p_GGLK8RTmKF0PumRJRf(rW(yfu2GWxkdO-wvTO*%ml@LwOA5-bd| zH&*8DQ!OEdMyqrj)9uA3syLWOpSy9M>Wd#?5`UtIzlSz{CGH1pxFj<%&{hwOVuzaj zi7$o4%r+YebXPg+9yuR#Om^IkdpaljU}unj@L2WFglYz^+sx#6lS+E}5U>XSG9(lOmw=vLc7 z$AON4Ot+N1oz=8)N~|0?oVb{WOGE4k--lKNLAw+XVvXpqde&2N2bYS_1Q(Zh5be2| z$vd4-)&nNa?iGtyg*eYqDv0X>eWtKP_v>#8m;L~w{p`7;Io`nPI3ntbg0lnZ4gG!7 zbwRA(wE$c(BVIJkdRfemQnKp5I-*3B=A6k1^}lV+y!dGL#T|5#s7UP{*ZL@thf1^^ zjm0<`t8=%Pr(hwi740d`=kgGHMMUK${CGG^#xGO)NbPHj(c0=Uvu+)4OzBG#GNf$K zwEzIMezB?{LZ)}2T7Rce?%7CUIQs`h`k|g1lv)G!lbvQD=7)i#FCcDtHIFdP-b|2i z4eq2U2Tu|6v^T_^GDeEu08J7*yJC%OpiNHCH9iKo@nt@de=6g0 zIvTGho!846epm0-30mUp8A@+$lj`YH?z>36{2~p9iO<164BPYP7*y}f2W1VgF7*q9 z(LF;Vy;v<2c`o1JzRVVx+iRJjT`f$+AP?uDC|`W{R0)Ria=+A*XD_SYV)YB7HhQ_y zIna6<$rFbsi?P0u%1Q^ihPx!v=pU3k1w@=9rcR0pFfhSPFF*Q6g2eMtYbYh}k9z!J zzFt!n(Y`CQ87ZA^xiUZ}c}?!u4QE}+Z?1&vxy@@fA#Hxk7XY)I_0nUF3H32?lugpsewd%wcQ#Ph=&U;+8ykc`q3rQ zj?gwEJ9^At>@5HC9V&cGt1%lS+^Nj5=0cD-!hleyYm~*OzQ0NVQYEVOJM%LO?=n~u zj*12X?SI7vl5;Nbn|urM;x=w%{^A}>H0nwRU>{)_r7f!@ww|K^qQ#RwwH^Fb3CVY? zrG&Ie^5w|@8>#Aw7Rr~;B&N>($@%(a%}DB9f#q>mRoPaBl)4&H_?{OfpOT+l6~aOzR)#>(3>SR$I9X}aLC|rhX60s4 zS|1T&d!i&Sd4d692NEM9aqVW@=O)(-CKE{hr9kcVtEIHIR}$|0KQkT(?GUYkl!@SX z5d6`+bdWi{2Lc8FrD0-|vsFV-n&qrDUL=Opdyw8x_36qx#oB0KXnjnvRi+0|UQ-fC z{f6{CAP^1nO^Scc3~IoqozvzA9{*oV8!sb5gcz&VkW*MJK#?Yxf@it!H=qF!$??L? zZVYI5u#917o`bPfvHZ@$)6{{Rgh;>{^F*OqRta}WkavwakrP}bpNkdFb}|+OTBgKI zk8CtxHR7ZF>y-T1J}|J2i5KGZ!@&E8e}QsBmq*`JJ;ta*`!W4B0WEa+6u=IZ1oNM7 zkg~rN{jP+CJg_E~wT)sxT!3~8PHYA=>;-HeZm<&V<)JsR=>Xle-A6vV@Z=1EW*}gP zHU^hM#fBKV@V6dHCTzhA;K%V07%^v<2|ye@u+SYQX2Gr^^u&*@{ly6UzrfD_`NIDz z_}v;Kh6@-OCS}t@JyU`X(-r7i_fCOyfa1#l=(AR?%1{IV@JJzQR1WQ>qp5zo2+x%3 z+}F-F15`G3b+9YMXygi89mE6PnA<5Vc=X1!A?5L@0D^gmW63Wu znx0#NA+Nr=RdKJ+Gd;kJdlAq|im^){=j0gPP z@@+dP9YNzoLkvWtwbjWR%(~y6xc6gL6_T*C56yp*4*)@$rGY6M3-sFswDeW8Th#41j$I!KEIy6q*yt(JJf*Z-@(>-9!qj?Q#3qN)<0$*(4k zFkSgN9poA^u~U@8aUgs7cu!xSytIG&3otZisMqQD7e@G}M>2p%#O_!bRfyY|Kz0{zQm&4+Ex6|K*%&Qw(*lu6M zb`6qIu>8MQ&UjSJD(twF5i8<4OS$;d%qTx?7!O;J&&xUR89iWgyIcn26dH;c**>|5 z^2Yz?)yvV?L5U=N)Q?dSaMLxiXuyHU?F#~5DO!-VFu?F=`-I&$r^qKDV27|~H>~P4 z=q#opvX+N^#MQk8zgS8!wUs&k$%dU5fL;wkSWi0RF+mvx4~6UU{3}Wkn^B>l^bcp7 z#81d9o63Y^q)TlGK1% z_&aN2MqQReWY~hDUPqXli6;p0*%uS*Q!U862gLZ)qIOOr<}^Tg+7csd;kJh%KcTD% z_(;kTft+5L-FUgu**>zXCHcrj2_{w^^<_pO@)bx27?pPt-*0%24KZ0$_GmEyt;EYF-ZWQn+kE)Or5!4}C!6LJ;NpRh zc|A?tpQ`{2DA$0Nffkzr2b|kKX*NVQ_H=7B6}s=vYw77?K(8PU^*fCJeSko6$&Ow_ zZvjLuBO0KXFE&2a{Igq6PIrqN+O5A*lFL$(z4LafV}zz{A3(RxGLVk7 z5@Gv!UQ&SONDl!zAs`KZ;a2r&)CJz#O(rP)ma}Du19Dk=$48hc@^M69F7{97A?-6w ztd{`J{opQ->>nrPTqk&2-@rVgX)t+seypwU!sweX%Sa^4;?0C^VD~|ix@e% zkL%>@vQ#1j0bIo&m8=1nkum#_7|c}9rwBz5ocbm1;MhAFj@wS3zjGub$h_7|x8k+1 zAV$#WV*flbIRgQwo?s5r(EHncqQ}b_>roQV5^tnRk1LRP9AT`3x6jP3UD1Il+eLfk zPeuVLoYVXB>;GU>{qf282?3&tT~}MRSOt_|BETUIY!5CkaJzoFe7wJr+RnOI{ZVN} zNGBB_*bu?q?>ZmrQvJjXwt4_pa@9q^31 z)Jsk5-QykKWzF~2CZm=C&Gi1($5(oXQ`cEt9~Jv6m^}!f5Wst6rR4=BI@ea+fomyR zW-;sqLN-XHPd0`4#@J^qHTm(e9rJu%Lv(Y8yZ?3Jv&(~TA3Ep`8h{V=m-}Qy3{f`# z&MO^AMhbEOEud>*x0R~ZG*=XLPqM7-b2QI}P!DGPGKSUBy5zpD)>w=pnh8Hs0KzkC zSNVJ}|1G?&N{4bd=J%-~HP&8yke@H(@i&h5pVzZ{S~p_|33*YVom!ogHlP*%5Cuh} zKaE#nwmEMJ;{kBKfe;IT{Gf+4t;(35*>K4fJZfQYD2=eQGSV7%Gg7-DgoK62O}OM9 zpu*Tk?A3`K@;qtau5sHZx)R$j+s@xv_`K=*bZ5dP!hv-FPa2<$q0bMMicK~J1_Hb& z(J0ZaxF>FAX z9FRy5cfOu18`NiY!RHFaqY?cgMeJH5Ix8^{fGMTRWQ=?+8_qLf44`oin4TZ2yi{h1 zqCqH3AIFI}+Se+Ob46R&`&3F(oO^12xop7iiU)pI=(EeH%dg({X#$-_IlKKV(9?aahTETbq;1ds+iR$s8&pkw*)K&v~j+D)HGNJy&D>7XG zB=bLG9u*ij(kTvcSAdeOEX}+(O>AT|YLh<9k3KU@zdQAb)WW2pyx>@k^C9630Dp|X zMI~xgISBzOv+uUMpAE2|MP=&A1JrG zVXKE_|0cb8VFSr_E+(e|8FHuN13ofl9z&lMQ0EDmW}Pid)pTqJ>Y+Tk=KRylvvgf; zH7#(bkn9h!Qy;`!_ivyE=;Umc3>?fEX#hoE$J%wog@%hERD3r?%2s_&(jE$zR|7h1 z6-VEiNZ&pkLN<`>%13Ye%|}W-720>VmVaK7#iMRYTUjdj;rTjXk(z*c;>*`dpQSS= zXUlPBxuTQ5?K}|1uao{78>}E*FTHb;LUxTcLvK@;-%@5jz;wIPiwc*5lf7A)^3C=O z0i$*A)B_6)P%DQ-e7PIJ^-wKvWuZ5HAn;-lH_i6U${X}Uyrd5*xW_*mc5+1u42C)9 z{NN`enYW#e_P(Z-0Pf9RQlj~}N%+gLB?y50feR3{6sn&BpajlIIiMFPqu{CVli5y+ z)-JkP0d>^PeP>E2T*!bV#EQfV=)=6b|JM}U-9|E+VrE%G~4cm z%Xe8WzAbU-KU@G9YTveg2@~c&O$|n7Sl$9`c;un8cODybhZ{`_5iG0bs~aG3xzM@` z5JUt5W!UX##I;vqcIo~Jae-e(okbTD$0w^_1=_NW&pIM*e6LfuSii1eR}~w*>G5dX zR5DSt$u0))T`M0Gsc|hh!9D_mh!5VqMO_IuS@W6FZjs#mxI zR%1nX@n1GR1g2J?p%{2S|G?=RfRQ-@7HO%~hHxlB)Y%)&{h~1_qtR)w`dRRS>>Hy^ z%?nvWv816_CiKtyGa!=Q^TBM>VtBpnp=B+%H7NXwyDl4$vS|LZ$X{DeV5T8*-lM%p zvUfH^!iG`Bvus&gEy?;ChxaA+b2I~;@ua$~*ieB(x%E}%U`b*?gM^Dz;J)d@fX^$7 zSG?NBK8QQB$NU=7{K1#n@m`te=KUOXl+Tx3-SYQ$I7mbvnj8VhHI&U!nJ=$d4$zTn z+FX}vc5mFcA!s|x4#pn@#gt~h;PC{lAGIpoN{0Gl z_bRdn-c}OjQfplkwh3)BHzX0~7+LD)ODk&h^amuOPn+ZJ35w}hVV`!2u=7$n{D}q1 z$aaad%%W7Ns@)Iq9vyHE^lWd=aY_+=@HG@{W11?OFEnp)k@hFLrqFKyBXT!v8d0$7 z*G-E8A>B$I`Nb!%4e!%Fke*pk$sRKZEx+m^|)QIi2POIv|yzlWRACI4)d}rV3PRvg{ zrtbMrBBbkR)sG948F*W6HSRHlC{wC_Bz)<3uZNh&amS7HmEpZ(JzbgYn~mENd5^>7 zZRsgG*>v1m{dNc9m>aYUePJH@b7=_c>zBJDW6dv0Od4w z(Rm&>cVD)hZ3_k@?xm)HCfvw*y@QW$2>)A=0sfH#d_GMHI!EjOI!BT)2SyqS#64AW zGkJ2heEN0$Lg3kgIZcZHP-93h;79*>i+}<&J)1j%AutaDqe|z8Ad(PA6LbR>gAH~+ z+#|3k-n1LwwQ?l&Fa4IXKw>4^<>S;p5)fXFKxA#1FhF(V5h4t?(HW3pFSK`Nt2a1q zVmC~H07dIG&aK*WXavoedy;jnL@Q1jOtIA(9JL2+jG)^xBKA5DQ)a7*dX69xrEuhj zGY{d@YjCG?$Z&d{`yc?xS8XVNONk88=u<$Oylj_7;0O9@Q6v{%-V1T^6A5m(6wl*=iD^V>l zyy?^NO3rI7mD^{x4n(*MZ71b>8T8Kv9&A;7Tg7_8*AF3qCOQKxiorplGoJLBT`ad8 zZS0IKe<>EXeg5Enk~L1R3ZEa_Nc$0>9quf*4BwtBuxQ8At8~KzNhv}u57GV{4Jx(d zdv8t_?dQi`KNG(TPCIj`!CW9H<7c7|*?wzfVDIDltb&dBAW%aR3dYg;f=_nuGc}^mz@)b#LH31N=Y)EJp*fL?A(d;UCV>Jft=S61GxeI6(TLqYLtaA!=Wn zje5Kpl?8?Wtj-3lenzJtvNnKLA9PH#5CW@<{`=-xyeNf=zpl+;3-^w-_fY@}g6~4e zVFTS}oLv-e?5RdU+=e971qv|WOCYRWI}pdqBvfy}XDK3EQUBN7DjH1>88bgfp#k#` zeeO@)T$*?FZ?t-l&jQLfXTH%VFPt5tGJrN%;WGZDg*>Y{z)~NtJSLC%2_gqLNJ9hl z{fB(afcS<%#3THM>00U&*-?0zhqBqT=UjGVca=_WcGbVorxGThho~w>}b#~Y46YBk@ z0MXgw>FEx~@FHnI$+5B#Ez@iX{ z@>AIEgT*BAx?}6TNctb5mwy4k1|f}6^t8)6XSgVeLp*H^OBev!#P8teN5Rcs60jLT zFjpCpm3xLEpY$$>t~Q^VTJWyGh5r!E>jETqD2E~9TQ>iZW^NOgZ}9vU9Vl%=W$ifM z!WLxpqV`Zs?312p3rC+3ZP$q&KI>M{ucnl){|^N#39~$_p-lTb`b0B?!C>)2{v_3J zQ+xyUA9#&mnctZXTaf+~1p}ch=)w@gZ%DV!mf7iJs$u+JLRG9Q;N4#Ntb7iuA(kGv zy=!}917UbbK8%CQ^Qi_iZsg8KAUD^ap?;~{>NhHe3BW#r99};DL`L5O-h{s4U;*mz zvE=v{Wsw`#|Ega+^KucRr<(()O{V}M=z4k%5OTqK>ivVMT+5?kHvyGWm*1jPbMvJy z#-}i-9EHc)Orm%N(}sK^txS30{&l5)z`;MD`T#WXQ#kIYIiMi?wdR{ZvPQrE)!tjU zMcsV=!%_+&l7iAoBZx>Uu_yvccZjfrw19LhsUlq>A+;!7lG3H5G}2uHN_Q^s%o|bf z`*ZvK2cGLEUb?XCyl2jwIdjfy&Up=|sGUu@!}49g39{JBJ$w}{mGP$a;!P4(Mfu=& zt}(I$15A#4$okt~h;llS9LtCcvv(Y@J zzWNeD!>8~vtU3N^XYX{3c^n0}67&FiL@I2o1ZXU7mPy*Jc z-{{8a3y(=iz#g%uF@zf7YNH&rzo3)NB*-)}R?bNu6v*eCSm%)C%fwm$ccI3 zE#U}Ah%Vp|F^Zf+wmVO^dbP|>_7gL8m1uKa@8~m^KL_%qpcLxM!j4J8ye^yaBrK}f zAXKjf=k}Wp8>AiL(S_+t#dOP#X5R?P%*uv~XSAjW#u}q`_2oX1; zz4v{E&xRt3FZ&({BJxaX4MA)uu~WViskM8sq)F1QMhWt0J0uDc0o4F@4(UZuhvx@e zF8v=FULZ}G*!f`FF7$1kZivScl#Cj8$*hb9ob)Wu*#t%X%gv+=gt{(B^a$TQRORKG zKN$z!<*Vi3d`OL8^}4gudm}h}q}25sRARAuH8Iz=>lW90A3{1kuVW(DERc17Sx}Gm zy?Eer({R(N{Vf>m`o!^y`*AJlM}7hp#rsf!gF>QMJ*eM$Hl0K9C~b#zB@mbpPk<$S zZ#!$y{i)*;;Q^@53II{=6u4ISa?1PtWOh=bXH4kt?1HVC8YnY2po7_ps>GRp*_R2{ zlNI_fk>=>LH-50HFuStk>)r2t>E38`Psp~fO+b^d%S3T(XWD-Wlx1}vkT*3%MI_%( z*L%bbGMJKyrs;HNHwRZKU}9=}k1?w#X#`!)NWNjcxFX3zz$8n#dnkwoQrv&s=3ucp zPtqQI^kQFUb2U$$-<{KbdE5_N{igdl%GY@K9bSfN);lr+dN0}fe3e|+4#NTyU>3YA zBek32=_rN@&3spsTyoYu$3NHQxeX+eVyZu=VMYh(#5L9Pco6^!6s?d-Tv50xOluFE zS9)+>9&~e%i0b*(Ty_BQh1AAAu_N7==nGPW0Yk&J2e0XU+h^QC4S{?d&EkkF_Je4ahkRQA zz_gXOeBBgJa8)IsLc*)(^1uz8mn1+huZl%10mluKK7O zqUNGcL9Uvdh<(a$Um_{KxIYLY@{(NGzF!ke%1%z+V-e7u>efB3VWCF>hy|Dr0aJzU zx7;pgV(q~T{h**CYXcr!C3J3}r5EfD!csbmbqY;tv^k|g>@4q|`}g=Dl%JVTJJAyi zfnukZMdt=!I3@UKM(!7%(a2dPooc#{AY|Ha;{Wi5J)~01(WUZXW{r8J4U*A`#6aQO znYp1-T%+S!*Y!&wuloH54G@gMm+FG!rEl8Y^2JaZ0%9}hyRBnErc zdBxGsf{Fm$o6qbe&@H5E29X#e19p^mPG|KcKN#TEpv&0l-Y~*DOw!c;0Nq$n@Od_# z;-sl8Z(>W%`yq!eS}LQ5fvsJXHdK-??55Sdf=^JaAQS##^onHGKwhrN!JWzwlH8`V z1@$WTQ6L5S`S-3wT|mv_LnLPYIP>d>&uTm;Y2r(_B`-mk}YG12^bKO7Kd;mnCXVz1syJn{ z1?UN)p}A&s-IhULhLQ{rdohU&KlfbE1mI88kUaX7WwruD@c<-=22U~g&o{M#iaUzL zSHYk1>#rAj0q!uR8vK+T$49F;%Gs({0QW-!NI1Uf)=F<9`dU#!abff4N%C8)^<@7D z-Q5rc<~a>g*K^ySK}0RzQvN{}<0D`!X%KC71Tr=k5kQ9<OTS2@bbQw5@a~WKG=o;9I+KBbLb zEKdSOSu(B20uc^jP?D~I$(SJ|0}f(MNLOVJRkO9PP~9o+rU;h`4UE?0HhJNVPknZn z8!?vn+#FL5RBAE+n|_x{wBSJv76=_2?xtEl__e~BfFHYrxo*=oFg+=U*IDPCvOja1e&Pa9HvSip#&kF(XTd7_Fewpq!W|Rf zf1(9Z*X2$}KPYnb@Fj@#hWVD{s3vBR8qt4s1C=kA0acq!pJGih4)Jy9Ak(9kX!Sq? zXZLovhx=pm*~hTewpjMSW6l};khDYfJpCUzNYprSp-I<8BR7GAdd{p$)OGA@J*{rM zlqLiUIj@2P-#epYKt)5iH58T8XT3lYt1ifQwx@Yqe-zXLgvjtAT3xmi-% zTDRV?X$h*&t*fw z^P}o)K^gXkB(!_g#EvEl0E1wX5CCd&eS| z6+X?6Jd_2ML7(XXIpSqOoy)2>G?TO8CAK$S1JK`{gMl6kqFDv`K2;^oGx*Bc>RLN| zmTONT7Yl*`0Cas5@hV?pbX;whqst#9)-#O0o-!Kg$M(G^ON;8`d|wSHGMY%8?I!;y zhwg8VIkLB(8%SUV49b8%AYBXR(ImC8X$+!n1c>iauONu|Pw>Rks&`pabYT1`chbG9 zE%jVp;Dj|^GcV&FI`1S@1@2DZcln-%rkZ!k$fm|baNGJxKnyqe+k&GcZ&ut|(Kip4 zs-5GUzAv-MM`NM@iya-vQy&1-YC#^6SX)Bg_7fnFCggUDfIT1(RLzPq?(aGWoo|P( zGReh$PFf4I0z{i-V;OcWP)*BzIXHdZs=};}+sAW7;CZNp`-Sufem#n48@7E}k<=8* z%eOL;pd|IMEVeovX-_la3As%=?@;`1i!WuplmXcxUm}AWZjyk=i0S=0MZ_NFwpA*U`63F65@?^4EN2l;S-zah~vSZZj`iI)K z_zAVAOF!V`+RsaDSda-S-?hKDWV$6U0(hTj_6__HdLEJmpnC90-UPTm9n@oxo_mEy zp59GAT46=3lp>cuRL2W;+&D?RLYFooL1kQ`nVym)SXdkY2gOTYUur|I8emM1{kDWT zShoYv++S@Z^*OQF-XIW4RDY-@hMv+<|f`a$}D>;TQpG z7jP})j-6xxD7ppljC)VA0PZf>&l`ZpRCp8KUU0#Bw%s>c#Y}%|-y!CUzsER8M2S1g zlzWZE78I51gY*T(>Qd64u+#@Z$(f{6g9S#+Z&IU#dpErgOvC-%68t_#t}D$-M+qCT zf^>(BYpdf4PtvEDL&~wOzcmM~HN&+8_TQ_);Bm)Qj-S5nF9|7O9=)=Q=^QT38O%!O zLJ41=h-A}3fAOkvn2j%2k>Aql$(INX4s8JRNZ{^&MGBBQxQW(N9ky})!5V+%GHR8Z zLvSPCO*HuX8PaymBwi-KlIMZ@ll34(s?4;=(HWcfFP@?klYEae@?0l!hO{pV>U*K&zX@MY)}4C(yJ1?X3v zYvjY(4W@yK=b@-J!{G4Ro{aa(V6@o`sLcV}PuX$|K=4^rOIPi-db`n|Fox z9Mp@PIq9oi!_W8Ho~PRT@Y=b2VzApNir1;Em$8UsbzF;e+1_=vpXm>h65;`+`xSG@ zIhcYJNwl$xY|fbq-DmHh2geMMPnd67JIQS~hteFgIOH68tPrzJOAIt^GMKPm6M8ZZLNIHl3*hkMI+l)F;_S>15C_nA^ z?}Pnn2OwE1%O%2=@AbC0-&4cuG1HQHDm8N8TzK|Ibn?c zNd|Yzhz&?p?Y!TUq6Nj;ssK-%A_v$LLu|1`L>BB5IL*>?5??X0QT5$XtYSomX`>tm za(m&$(<);WfLyL0X%O~!kWEc-44`E#g68Ic3aop|?ZtlD_oI7&gDrV1t-qE|FuK?@ z2o$>7F`$BgOpsCa<|RHcm94a$QEyr>wvN>W9Rof>OdZZ<9{v-r+lQo$J6uI4`%$FQ<3UkdDW@ zM%~zqy#puUaL$Fi2JU&#^W3#&Z}lPa(LqbuDE>z6)AD@LuMsb8eaT=S9x+pUlfdZv zwZ0r(uXH{5;WD4Ho{Ug_Fx<2ar9gwSlP4>kynhZVp4|Y;Gp+?5^aNO9j0)X_+VLnH zx={Gc*YIoUc;a{zFAStqF$eD{Vd{p+eLLYg5<$Nxrn6p`9>r;{n)`HqD3Oa*Ejwu= zrRB4y&o)St>@-nk^UnrsOIJRH0TMLmnbzmNLJ0wchLKRrsy~JgDWNwA) zL_5ef&s)V}v$2AF75#4*vY_nx6?H})udUQ*mHuoY> zYy)RM{}58Y4*xDJvy9u+v51RXjoEySpoo+W#BR?T`Wn$!3pU?+ab#clvYS2Eec-!2 zd2r^?Jqow4wO;(;N1|A3K(_h7VmslDt!dcMnp@&p8{hc%Ziw{_G^b_Yn++FgV?DpAz$n63ow_5ymV~9$)f)s?@U+grq#Z{`5L`P!wH^$1*qTagZ~^%xm_hqXkR~slnIg=K zr@cPhg8u9VNH!xWbpRaNGyu7G-9}A}BtE_66Ao|}sALI^$r_t}^>}IYTnWicDgUY( z7D|&~5xwCRIpW;_B4ACNPX0vp3~xA7CW#&@c!cVY(PA`Ma`G4I$f6Hp1*r3KoL46Jcl zwC=L{YDZyW6VS^0)U@+-Fbw<1fz-8JXlIM+Vr1oi<#qdom+C4ilPhhmlNLGjI`ehX@`^K-;KZ`Ke%$E#n75b zB*6;(0|xn=A-FFCbT0%84G)Ex@p!R*d~9^bZ_G8w->@xuWF)TSG23G;p$V zbF(P=?9u)f->Td)d?RGY0w$QmyW_088TH*&8VL?b#9w@6tw$)oU@YYHWF>m$qO6ej zQZk_#i+eS*!s9d3Nkf&c~a1aXI3x8X${cHcUi8d*@DRs@esiQJ(BXdo%2r8buIFs zXl9Qrj$(KvSF4q8++_`1O<13d!4Z70OdWxL7X#+`KKeNjpaDsm`2N0$MMecwHc5A3< z5Q3+l@l?b%o`}4+M%KXGUq3!yt^k5bbn=i3K_ZQu+1Ls6Hb z!MU~aC3{KQ+E+4b8~CZPW!G9e(Q7-?VxXlg+!2>X8vQuC{-DqfA(~3(?V!=^0iOMB zSl;&4kENPsGt`PP0xM((to;b(JgY&Z2m?01FzdVXdbA;UKqsH}Xhc8?Dw4nn1Qe)X zHX%GywJR`N5^tpwe7Lc5_XrSNOiu?NUn)hUyKnz6%A2+<qW{fr?bTQ6MHK#lueJx-VNnrhyZv)cpfv=3)HrvBi!F2UK_|S@1(uP zGNnbAp#`dnZmUGpl;&~PO&piQ{A97ndw}oI9@JaBuB#vaD#dZB|FOG4A^fAOc}e(C z4M$)15$N%Q@|s{g6moWN{(4Tmf;V-TyN?qK{9%Pe_$_UH!Uu{qYoSR+rPxn2MKEB) zTaiLQi?9jOAqgZxkoKDamD&uDJGJUR!M9Pj{$BEp;_- zN$v|}Ja)PqDRLW=l4eGphY=0kh8Q$_<7dNQt{5L<$k?qFuU^A#?6FMY)tyaYgp{cP z`)TnZ@7liEu}?6Q>qyinMWLKhr34T20}5i}YiNxU#Ghg!M9u;Q+-S zNz>!CSyKy$cg2AeAQpWru{qsr(Uc6V83b0nT!Qm&dyD6IYH#NsiX@%ri-*V{CiiNK z_yN*W3&r5^^gJYA<2QhxOFVCog$_L^IDq6)^v_=fiwBcc9TNRi$*$CyK@#Z4oy=$} z3W0hBO5$L=S6B+Up-w=C3KLs77bpOFz13Re8EeUufzk!pJe&R-9|td-+I5yMvgF{m z-q*zKjFD&N@jlP`e&Q7R}(yuHD5u)XKphn8Be4pg?y!z7ehxlj9-_X zchD1_>W3d7fd3fG>ZhHC+0688c$BX(+AKR1Iq`mjBj{lHifYxD0O_AH_$drLREj1B z`L2u++?>m7LQS*2PAXv^i+AAJ;brTVt&hHs9mi@)j>pbrz&Nh}dq0uto)=D$(s<Yu_QFE+$;-%>p2oNe5=Ij)a1K3w5dO@D@8##0p*0qEalDYoc{XU$^-UOP6`^GUwjLmwLRXmXOAsXmcyB z@r02Z4mY_7Ms=61*GifvyhNq1xTmIL|9l9{Glh7-rKN4E`EFzzVGSMr#o(^QhPq+P zN-E3BJ^OAfBST=?)y=n55)U($^uyYC{ce;mayLptwiMV@c*7u`%&$2Kxdhf>mU7vq zlj%g{SYf-*|BOdjCoxc<(TOacs?_oug0LxHWSL?obxVLt`?3&Y%LU*UoVLXL4yZT| zCl~J(3RINzwF?Bk6HhLek+^Aa3-`!uxp@8`a{aG$3 zX%UR^*Tz|(Mg1A_T;y3djNDsQ>1W*<$UHW2ltXGD8@gbgtDRMRRF=2AzfebR_5lLg zauTq;?j<;ST2{whId(jx;hdaNTZeH}b6*ucUfh^E$BPc8lbpHp)Y30$Pd5YwTj@9O zJtK*M?1%^h5WJeT9jgE@@NVW4Jz-4i9kAG=U3b?9fnXg>c>FYZKWFJY@iQuUT`aSWM2I5V!(k#GB7-`T&K$R%g)mBKRMZ^d;0b zdQ0m+rVCp+9kd-zDmwtR4Q=BXxlc1Zk(=b5V7la-+ujEM9^x6#c|yQaDr&pf^Nx4I zO24tOgmXHR2`v;dSv4Hz7f0+;2OxKT81K~VSC>@dt~@30JL{*bk4ZkXIc~}Wwn)|(_w1>;NA0pJ?G1iQotd+2;D*fK zQuzI%8HfYUJ9K;@%M*FBpPI~#HxAAr6=K@JCJ{(|Kok9XG3g)5_q*33qL3=R?`<;Y zS*5^<{50xT*)VU_SXta>C~Q7dIAA?(&c%Gy;BOCwj6K-6piRYZZ#IS_-euNTJ*!vm z97OK;Su|pMOEF( zYapR^f34|7EpEHTTZ2enEwdZg=UrVN+l1<%oXpnJ-5J&7qy#E3W&Vr{qNiH;dw`k#Z&r<4DB_NX8c>E}iIh&g8mL`8lbumeGX*Bec?Uy>sN zn2z?5jZ8 z`gFfj30*z(5#e0Y#sCH_rcbLNBpA>vKw#h}5#rARgW` zf)n6hM3h|9Csj7UepBcO7XrXuNy%a|pKp!oQTqw#{zOoC)Z(3Pl z49EO)5T_FJ%pg*CS?gPiOg(ySBr9S^7K;oBm|84O^5*}g}joD|GY&Lt^%~Idi zQ~pM+w;-1AkbJlFA>XiGcggB&v{17?!=>z7+!&L~eaV6O-;@KCq)Il2^RS92#IU>e=vqZ5AoBi0D-%|I( zXeU^PZ#cK`a;@P7UY z!TsxrK*Zf0^E4o?Om07J9kq68+7itpOjAo(#6!^-t#q2yey>nSRY#0|)Zd#KZcvos zi1V|Yl>@d-`0!iU?rQDBwfUE7`ioIk7HCQ}cZ+qs@unJL&h-ykgD@%YP*s5R=5v{U z&grQbm8)tJx*!xFzYTo8`*z>ZO-%{%KBW@xhRM{Jy@^|S#=&_l~n+#TyjGFp~6Z>nrJDEL4x4)C8#yd&Ixxx`!Tl{GU zp90CdWe15mYdTabud@ydGa(5M7W=bhrSV60bT07ky!Z1{x%n^g{v0hi>rI`B(!AQB z>gt?s;HUKIN+Bjw$B0y`?2#5F&ehVYJ8V3YJsqiSN?FE^y3aL_n^LP&3+t% z)dUJXpdQz+-sk*V-*QEE4tIQs_o?AV)p;BN{&e_4WuK-8uf~9#crXi5|73Kfuqw}} zcyys|V-f}n_)tBbs5eD|{lEPJvBoRxZ`3-7Rx6=Il?7FsMf;`R7y91=`S_kN4;w-V zPHlb8%bxy6h_;fg4>7ak%~#J+QFB!Dza#ogg7KH;&f-F5G;fWOqCQAqiD1~GSca{l z>9f@;ns@!nNVv(Wx_wg$7%=aGwt~joGFC)6(?ACtf8<#H0{`6S|7%j|I`ux0=Cu`R zHo710aRd&I3SYSdRKs}kI?Iyl&ai;2V5vh~NQ!J6>9FNTQI~`4bI!_&m38<2I};w@ zQhD;-FZ?md)ShBwTHMz71?GKO8gbeCJ3^!5l8=P`qEpyU6oDYZ400-xI4ZuO2Hd>I zdvKn9^HJzpNGp?D_)u}-KDhb&FT(f=V!Ft_ZsS0b0owwZsrgT8J%a}y)!@M|%4kNK z6qdyQ`l)#^w4AY_^B)8j;JFFG#2CA388a45D;4=4N= zZuiHYJ$VEsp?fA$@NZ26a4-XCRkEu1>m*3&xe7jki4FAxp;(=n^eO# z6m^s+9u%Z>5Jw1?-OE>)9`qG)1)uIa_t%b}Cot|UQyBq;0C`}mY0caE*1^>H5Vd55!7t)a2rn-=^R2Nr`8N~z%lZ}8HkCT=j}y!KY7`iT>yzvM#v6aj>X06)QlhjYi#s6=+ zXR(Hg%qaRt9qkG=t40M2ko;>e2HaO(=*{yfaWT`+(1TSO*2Amz5Nj6cepIr%jyvZ} z5l6$ck1FU$gd>&c95ktZo=X1PW^CbPRklB}+M2jdw}XdrsvL8f&W;v5_#*MXVH_2=Hpfw*i+ZSUQoOz=>W#rvu5xJQ`^Si_i4v10c9n?0XWanTL%w2d zey6H4ez+Y(EVq2B)X2%5qp~pY$!tU z&g+(gXdxzI;EYI3`8KA~dC%;k!CURZ#{tHJ)?@e-U)nb28j)!xQ%DUzmcai)G9lMI z%`8jqSmPH8Eq^DTN+yHLDi|uJ^MZk!W=1*U5D@dI zRoj5)|3F<&pTPV$0Rs2C+xUXoiUzokZm%IZCr13wj}Jy*vO;U|GcJ8a^Cfk?FARD| zdvn1Z!iQPbFG#NZ??VA@=+9R6z;-N6>1kf?Y~Bw;ipKmFcFmZ{LX&R2gKR+d{OwJz zV9O71AW^z+%^{hfwvsqqLw&C9+ZT9U%<+L?#a@HvS_1`7bsxwSav1YpsXF+Owzl-I zQ=bzgAisu4ijDeuc7-8Q)*h2 z;zL!P@7a6}&JJ!1P$(!+%BY>J%2w$K`;b6#DBpFiiM`CqKm(amML$Hy= zF*_+R%I~MzT$S8&w02lb14i0ULT%U85APcZ_2~FumO>7~5ZH=p43SOHtl)wSmW?a+VzIho|2&;K}1a9*aB{ zr*fP~V>{K=YP+~zpX({%K{=n*{HqBp)552($26)f>lOwd?$qLG-PoHW4kG7d6`ysF z?->6&;4ktB@az0hR&`3St2Dy( zluv$j{Yd3FW1==SYU33UmpaT2%Y}NwDJyFF^ZrA_L$>_y*VZ!u(>0|JC1SYG`Jr= z@?LOqsksqozfVpJZM-Ju|I(2sHiu9u1?TL{oLLxB*wwk>c&siOxiekqU;|s}J=$L? z=JMCj+h&mG0TZU8#OkT)FY+ODyv%L^3O&_xXH&)evGhM3t8H|@pWWGm6{pF}_(!`> z*^LX;`pL5M!HZpY*qkZXdgPkC9K3Ckndb6;y#4<^{kPrV w|4Hq)yZX-;|4-NcDUDy-|NpPo*zvhkiJM)WZL%-VfIkn#Wbc2tr}O;(1Fnw*hX4Qo literal 28375 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 fe`JP`|1bW3`ds>vf( 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 SCL)x7c>GZNLHT`)@c#k)`e{o5 diff --git a/xls/modules/zstd/img/ZSTD_decoder_wrapper.png b/xls/modules/zstd/img/ZSTD_decoder_wrapper.png new file mode 100644 index 0000000000000000000000000000000000000000..fec37b51a4c73771c92a9ed7e9df707ede385aa7 GIT binary patch literal 126231 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

      o`n4 zH3K;(o6B25nJSU0aAG=Ft~p4(?csPJv%D*YbMrFI%YWDtdO|{Qs&_e;a4C@(-dcXf!km+0G8DpX}ai7@rdH9o^g zIWpeSmx73YSv3^yfB>7+lsQEx*kL-!E6hO&JTLuBTdxbGNz9MggF1x9W*LgVHydo2 zE7*!FRxp%5HXVbR3)FTOn$L;b+?{ZI{bo&tJ){Q@JgfL)|OD^w;2q)D-va5zM; z11`j5fAcE%q4z9=T(kn3bP-0~8W`#{rAhB#+8c5H;$_!6*YW->nM1=MLc91EtNvc> z{}siMD)%{DmW^!ch`TcFZf%W=#vloiO(sQ%mj7SWQz|MI#NYah9NhS}n(?TM zukG}AZf`GbVc<*QdV4~If8}2~0*NR>B@4QMGRF<2bj^9J63c<&B=@gVFDRoaZo-fC zAQ%!rQ^45nRXn+N;J8{qmUv!|0Vej_$N+W9s4PsG_Rj|A=OBEI z8DWs?1w{)VIoUy>km%=$ot`MQ1$hUyRo${}M~+_JeIOraIk+GEwM$4%w4KJEFEnr8 z00bO9QAKSNCYN*zX9#DVlcERne1g6?Uj6JJo#XOjS?*yP+<^9@yX5Zxko`F)S?_*dfJ3kfIRNp)Q3 zG^+pq;k$p?^|)8xH`l$1vVRt+46MP0Kl{FY_7%AM3AFMZbTk2QFmYkTG^X210-zm_ z3w5+T6$SDbi>-q%Kl6REZPr!Q_9iIp{fH6 z&M%bkqZ-&?S zU$=i1);u7Z1P{Os>*h!J+z?~z1sVrhBr3?U^1#*WF{-5fd( zfQHS02b!J&t|&Y@=Ob{Ny|QUmSa;=|irQ|0k0~*rh)hjS#hR8j9yzd$BLkSDmH`*} zqBm86HIxRZHMrx)64>@tSozPubk Date: Fri, 30 Aug 2024 13:52:13 +0200 Subject: [PATCH 27/46] modules/zstd: Add AxiRamReader implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maciej Torhan Signed-off-by: Krzysztof Obłonczek --- xls/modules/zstd/memory/BUILD | 88 ++++ xls/modules/zstd/memory/axi.x | 17 +- xls/modules/zstd/memory/axi_ram.x | 767 ++++++++++++++++++++++++++++++ 3 files changed, 869 insertions(+), 3 deletions(-) create mode 100644 xls/modules/zstd/memory/axi_ram.x diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 4e9cfe2b81..43563e1aa9 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -302,6 +302,94 @@ place_and_route( target_die_utilization_percentage = "10", ) +xls_dslx_library( + name = "axi_ram_dslx", + srcs = ["axi_ram.x"], + deps = [ + ":axi_dslx", + "//xls/examples:ram_dslx", + "//xls/modules/zstd:math_dslx", + ], +) + +xls_dslx_test( + name = "axi_ram_dslx_test", + library = ":axi_ram_dslx", +) + +xls_dslx_verilog( + name = "axi_ram_verilog", + codegen_args = { + "module_name": "AxiRam", + "delay_model": "asap7", + "ram_configurations": "{ram_name}:1R1W:{rd_req}:{rd_resp}:{wr_req}:{wr_resp}:{latency}".format( + latency = 5, + ram_name = "ram", + rd_req = "axi_ram__rd_req_s", + rd_resp = "axi_ram__rd_resp_r", + wr_req = "axi_ram__wr_req_s", + wr_resp = "axi_ram__wr_resp_r", + ), + "pipeline_stages": "8", + "reset": "rst", + "use_system_verilog": "false", + }, + dslx_top = "AxiRamReaderInstWithEmptyWrites", + library = ":axi_ram_dslx", + opt_ir_args = { + "inline_procs": "true", + "top": "__axi_ram__AxiRamReaderInstWithEmptyWrites__AxiRamReader_0__AxiRamReaderResponder_0__32_32_4_8_8_32768_7_32_4_100_next", + }, + tags = ["manual"], + verilog_file = "axi_ram.v", +) + +verilog_library( + name = "axi_ram_verilog_lib", + srcs = [ + ":axi_ram.v", + ], + tags = ["manual"], +) + +xls_benchmark_ir( + name = "axi_ram_opt_ir_benchmark", + src = ":axi_ram_verilog.opt.ir", + benchmark_ir_args = { + "pipeline_stages": "4", + "delay_model": "asap7", + }, + tags = ["manual"], +) + +synthesize_rtl( + name = "axi_ram_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "AxiRam", + deps = [ + ":axi_ram_verilog_lib", + ], +) + +benchmark_synth( + name = "axi_ram_benchmark_synth", + synth_target = ":axi_ram_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "axi_ram_place_and_route", + clock_period = "750", + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":axi_ram_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + xls_dslx_library( name = "mem_reader_dslx", srcs = ["mem_reader.x"], diff --git a/xls/modules/zstd/memory/axi.x b/xls/modules/zstd/memory/axi.x index 09bfc194e2..d4b347f013 100644 --- a/xls/modules/zstd/memory/axi.x +++ b/xls/modules/zstd/memory/axi.x @@ -25,7 +25,18 @@ pub enum AxiAxSize : u3 { MAX_128B_TRANSFER = 7, } -pub enum AxiWriteResp : u3 { +pub const AXI_AXSIZE_ENCODING_TO_SIZE = u11[8]:[ + u11:8, + u11:16, + u11:32, + u11:64, + u11:128, + u11:256, + u11:512, + u11:1024, +]; + +pub enum AxiWriteResp: u3 { OKAY = 0, EXOKAY = 1, SLVERR = 2, @@ -95,12 +106,12 @@ pub struct AxiAw { pub struct AxiW { data: uN[DATA_W], strb: uN[STRB_W], - last: u1 + last: u1, } pub struct AxiB { resp: AxiWriteResp, - id: uN[ID_W] + id: uN[ID_W], } pub struct AxiAr { diff --git a/xls/modules/zstd/memory/axi_ram.x b/xls/modules/zstd/memory/axi_ram.x new file mode 100644 index 0000000000..28326a848d --- /dev/null +++ b/xls/modules/zstd/memory/axi_ram.x @@ -0,0 +1,767 @@ +// Copyright 2023-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 std; + +import xls.modules.zstd.math; +import xls.modules.zstd.memory.axi; +import xls.examples.ram; + +type AxiAr = axi::AxiAr; +type AxiR = axi::AxiR; + +type AxiReadResp = axi::AxiReadResp; +type AxiAxBurst = axi::AxiAxBurst; + +const AXI_AXSIZE_ENCODING_TO_SIZE = axi::AXI_AXSIZE_ENCODING_TO_SIZE; + +enum AxiRamReaderStatus: u1 { + IDLE = 0, + READ_BURST = 1, +} + +// FIXME: add default value for RAM_DATA_W_LOG2 = {std::clog2(AXI_DATA_W + u32:1)} (https://github.com/google/xls/issues/992) +struct AxiRamReaderSync { + do_recv_ram_resp: bool, + read_data_size: uN[RAM_DATA_W_LOG2], + read_data_offset: uN[RAM_DATA_W_LOG2], + send_data: bool, + resp: AxiReadResp, + id: uN[AXI_ID_W], + last: bool, +} + +struct AxiRamReaderRequesterState { + status: AxiRamReaderStatus, + ar_bundle: AxiAr, + read_data_size: u32, + addr: uN[AXI_ADDR_W], + ram_rd_req_idx: u8, +} + +// FIXME: add default value for AXI_DATA_W_LOG2 = {std::clog2(AXI_DATA_W + u32:1)} (https://github.com/google/xls/issues/992) +struct AxiRamReaderResponderState { + data: uN[AXI_DATA_W], + data_size: uN[AXI_DATA_W_LOG2], +} + +// Translates RAM requests to AXI read requests +proc AxiRamReaderRequester< + // AXI parameters + AXI_ADDR_W: u32, AXI_DATA_W: u32, AXI_DEST_W: u32, AXI_ID_W: u32, + + // FIXME: The parameter below should be calculated correctly, + // but causes deduction errors. The issue is possibly related to: + // https://github.com/google/xls/issues/1523 + + // RAM parameters + RAM_SIZE: u32, + RAM_DATA_W: u32, // = {AXI_DATA_W}, + RAM_ADDR_W: u32, // = {AXI_ADDR_W}, + RAM_NUM_PARTITIONS: u32, // = {AXI_DATA_W / u32:8 }, + + BASE_ADDR: u32, // = {u32:0}, + AXI_DATA_W_DIV8: u32, // = { AXI_DATA_W / u32:8 } +> { + type AxiAr = axi::AxiAr; + // FIXME: Replace with params + type ReadReq = ram::ReadReq; + + type State = AxiRamReaderRequesterState; + type Status = AxiRamReaderStatus; + type Sync = AxiRamReaderSync; + + axi_ar_r: chan in; + rd_req_s: chan out; + + sync_s: chan out; + + init { zero!() } + + config( + // AXI interface + axi_ar_r: chan in, + rd_req_s: chan out, + sync_s: chan out, + ) { + (axi_ar_r, rd_req_s, sync_s) + } + + next(state: State) { + const AXI_DATA_W_LOG2 = std::flog2(AXI_DATA_W) + u32:1; + const RAM_DATA_W_LOG2 = std::flog2(RAM_DATA_W) + u32:1; + const RAM_DATA_W_DIV8 = RAM_DATA_W >> u32:3; + + // receive AXI read request + let (tok, ar_bundle, ar_bundle_valid) = recv_if_non_blocking(join(), axi_ar_r, state.status == Status::IDLE, zero!()); + + // validate bundle + let ar_bundle_ok = ar_bundle_valid && ((ar_bundle.size as u32 + u32:3) < AXI_DATA_W_LOG2); + + let tok = send_if(tok, sync_s, ar_bundle_valid && !ar_bundle_ok, Sync { + id: ar_bundle.id, + resp: AxiReadResp::SLVERR, + last: true, + send_data: true, + ..zero!() + }); + + // send RAM read reqest + let addr_valid = state.addr < ((RAM_SIZE * RAM_DATA_W_DIV8) as uN[AXI_ADDR_W]); + let addr = (state.addr / RAM_DATA_W_DIV8) as uN[RAM_ADDR_W]; + + let do_read_from_ram = ( + (state.status == Status::READ_BURST) && + addr_valid && + (state.ram_rd_req_idx <= state.ar_bundle.len) + ); + let ram_read_req = ReadReq { + addr: addr, + mask: !uN[RAM_NUM_PARTITIONS]:0, + }; + let tok = send_if(join(), rd_req_s, do_read_from_ram, ram_read_req); + if do_read_from_ram { + trace_fmt!("Sent RAM read request {:#x}", ram_read_req); + } else {}; + + // send sync + let resp = if addr_valid { + AxiReadResp::OKAY + } else { + AxiReadResp::DECERR + }; + + // calculate read size and offset + let arsize_bits = AXI_AXSIZE_ENCODING_TO_SIZE[state.ar_bundle.size as u3] as uN[AXI_DATA_W_LOG2]; + + let (read_data_size, read_data_offset) = if (arsize_bits > RAM_DATA_W as uN[AXI_DATA_W_LOG2]) { + ( + RAM_DATA_W as uN[RAM_DATA_W_LOG2], + uN[RAM_DATA_W_LOG2]:0, + ) + } else { + ( + arsize_bits, + ((state.addr % RAM_DATA_W_DIV8) << u32:3) as uN[RAM_DATA_W_LOG2], + ) + }; + + let tok = send_if(tok, sync_s, state.status == Status::READ_BURST, Sync { + do_recv_ram_resp: do_read_from_ram, + read_data_size: read_data_size, + read_data_offset: read_data_offset, + send_data: read_data_size == arsize_bits, + resp: resp, + id: state.ar_bundle.id, + last: state.ram_rd_req_idx == state.ar_bundle.len, + }); + + // update state + match state.status { + Status::IDLE => { + if ar_bundle_ok { + State { + status: AxiRamReaderStatus::READ_BURST, + ar_bundle: ar_bundle, + addr: ar_bundle.addr, + ram_rd_req_idx: u8:0, + read_data_size: u32:0, + } + } else { state } + }, + Status::READ_BURST => { + if (state.ram_rd_req_idx == state.ar_bundle.len) { + State { + status: Status::IDLE, + ..state + } + } else { + let incr = math::logshiftl(uN[AXI_ADDR_W]:1, state.ar_bundle.size as uN[AXI_ADDR_W]); + let addr = match state.ar_bundle.burst { + AxiAxBurst::FIXED => state.addr, + AxiAxBurst::INCR => state.addr + incr, + AxiAxBurst::WRAP => if ((state.addr + incr) >= (RAM_SIZE * RAM_DATA_W_DIV8)) { + uN[AXI_ADDR_W]:0 + } else { + state.addr + incr + }, + _ => fail!("invalid_burst_mode", state.addr), + }; + State { + ram_rd_req_idx: state.ram_rd_req_idx + u8:1, + addr: addr, + ..state + } + } + }, + _ => state, + } + } +} + +// Should translate RAM responses to AXI read responses +proc AxiRamReaderResponder< + // AXI parameters + AXI_ADDR_W: u32, AXI_DATA_W: u32, AXI_DEST_W: u32, AXI_ID_W: u32, + + // FIXME: The parameter below should be calculated correctly, + // but causes deduction errors. The issue is possibly related to: + // https://github.com/google/xls/issues/1523 + + // RAM parameters + RAM_SIZE: u32, + RAM_DATA_W: u32, // = {AXI_DATA_W}, + RAM_ADDR_W: u32, // = {AXI_ADDR_W}, + RAM_NUM_PARTITIONS: u32, // = {AXI_DATA_W / u32:8 }, + + BASE_ADDR: u32, // = {u32:0}, + AXI_DATA_W_DIV8: u32, // = { AXI_DATA_W / u32:8 } +> { + // FIXME: Replace with params + type AxiR = axi::AxiR; + type ReadResp = ram::ReadResp; + + // FIXME: Replace with params + type State = AxiRamReaderResponderState; + // FIXME: Replace with params + type Sync = AxiRamReaderSync; + + rd_resp_r: chan in; + axi_r_s: chan out; + + sync_r: chan in; + + init { zero!() } + + config( + rd_resp_r: chan in, + axi_r_s: chan out, + sync_r: chan in, + ) { + (rd_resp_r, axi_r_s, sync_r) + } + + next(state: State) { + let tok = join(); + + // receive sync + let (tok, sync_data) = recv(tok, sync_r); + trace_fmt!("Received sync {:#x}", sync_data); + + // receive RAM read respose + let (tok, ram_read_resp) = recv_if(tok, rd_resp_r, sync_data.do_recv_ram_resp, zero!()); + if sync_data.do_recv_ram_resp { + trace_fmt!("Received RAM response {:#x}", ram_read_resp); + } else {}; + + let mask = math::logshiftl(uN[RAM_DATA_W]:1, sync_data.read_data_size as uN[RAM_DATA_W]) - uN[RAM_DATA_W]:1; + let mask = math::logshiftl(mask, state.data_size); + + let ram_data_shifted = if (sync_data.read_data_offset > state.data_size) { + math::logshiftr(ram_read_resp.data, sync_data.read_data_offset - state.data_size) as uN[AXI_DATA_W] & mask + } else { + math::logshiftl(ram_read_resp.data, state.data_size - sync_data.read_data_offset) as uN[AXI_DATA_W] & mask + }; + + // update state + let state = State { + data: ram_data_shifted, + data_size: state.data_size + sync_data.read_data_size, + }; + + // send AXI read response + let axi_r_bundle = AxiR { + id: sync_data.id, + data: state.data, + resp: sync_data.resp, + last: sync_data.last, + }; + let tok = send_if(tok, axi_r_s, sync_data.send_data, axi_r_bundle); + + if sync_data.send_data { + zero!() + } else { + state + } + } +} + +proc AxiRamReader< + // AXI parameters + AXI_ADDR_W: u32, + AXI_DATA_W: u32, + AXI_DEST_W: u32, + AXI_ID_W: u32, + + // RAM parameters + RAM_SIZE: u32, + + // FIXME: The parameter below should be calculated correctly, + // but causes deduction errors. The issue is possibly related to: + // https://github.com/google/xls/issues/1523 + + RAM_DATA_W: u32, // = {AXI_DATA_W}, + RAM_ADDR_W: u32, // = {AXI_ADDR_W}, + RAM_NUM_PARTITIONS: u32, // = { AXI_DATA_W / u32:8 }, + + BASE_ADDR: u32, // = {u32:0}, + AXI_DATA_W_DIV8: u32, // = { AXI_DATA_W / u32:8 } +> { + type AxiAr = axi::AxiAr; + type AxiR = axi::AxiR; + + type ReadReq = ram::ReadReq; + type ReadResp = ram::ReadResp; + + // FIXME: Replace with params + type Sync = AxiRamReaderSync; + + init { } + + config( + // AXI interface + axi_ar_r: chan in, + axi_r_s: chan out, + + // RAM interface + rd_req_s: chan out, + rd_resp_r: chan in, + ) { + let (sync_s, sync_r) = chan("sync"); + + spawn AxiRamReaderRequester< + AXI_ADDR_W, AXI_DATA_W, AXI_DEST_W, AXI_ID_W, + RAM_SIZE, RAM_DATA_W, RAM_ADDR_W, RAM_NUM_PARTITIONS, + BASE_ADDR, AXI_DATA_W_DIV8, + >(axi_ar_r, rd_req_s, sync_s); + spawn AxiRamReaderResponder< + AXI_ADDR_W, AXI_DATA_W, AXI_DEST_W, AXI_ID_W, + RAM_SIZE, RAM_DATA_W, RAM_ADDR_W, RAM_NUM_PARTITIONS, + BASE_ADDR, AXI_DATA_W_DIV8, + >(rd_resp_r, axi_r_s, sync_r); + } + + next(state: ()) { } +} + +const INST_AXI_ADDR_W = u32:32; +const INST_AXI_DATA_W = u32:32; +const INST_AXI_DEST_W = u32:8; +const INST_AXI_ID_W = u32:8; +const INST_AXI_DATA_W_DIV8 = INST_AXI_DATA_W / u32:8; + +const INST_RAM_SIZE = u32:100; +const INST_RAM_DATA_W = INST_AXI_DATA_W; +const INST_RAM_ADDR_W = std::clog2(INST_RAM_SIZE); +const INST_RAM_WORD_PARTITION_SIZE = u32:8; +const INST_RAM_NUM_PARTITIONS = INST_RAM_DATA_W / INST_RAM_WORD_PARTITION_SIZE; + +const INST_BASE_ADDR = u32:0x8000; + +proc AxiRamReaderInst< + FAKE_PARAM: u32 = {u32:0} // FIXME: remove after https://github.com/google/xls/issues/1415 is fixed +> { + type AxiAr = axi::AxiAr; + type AxiR = axi::AxiR; + type ReadReq = ram::ReadReq; + type ReadResp = ram::ReadResp; + + init { } + + config( + // AXI interface + axi_ar_r: chan in, + axi_r_s: chan out, + // RAM interface + rd_req_s: chan out, + rd_resp_r: chan in, + ) { + spawn AxiRamReader< + INST_AXI_ADDR_W, INST_AXI_DATA_W, INST_AXI_DEST_W, INST_AXI_ID_W, + INST_RAM_SIZE, INST_RAM_DATA_W, INST_RAM_ADDR_W, INST_RAM_NUM_PARTITIONS, + INST_BASE_ADDR, INST_AXI_DATA_W_DIV8 + > (axi_ar_r, axi_r_s, rd_req_s, rd_resp_r); + } + + next(state: ()) { } +} + +// only for RAM rewrite +proc AxiRamReaderInstWithEmptyWrites { + type AxiAr = axi::AxiAr; + type AxiR = axi::AxiR; + type ReadReq = ram::ReadReq; + type ReadResp = ram::ReadResp; + type WriteReq = ram::WriteReq; + type WriteResp = ram::WriteResp; + + wr_req_s: chan out; + wr_resp_r: chan in; + + init { } + + config( + // AXI interface + axi_ar_r: chan in, + axi_r_s: chan out, + // RAM interface + rd_req_s: chan out, + rd_resp_r: chan in, + wr_req_s: chan out, + wr_resp_r: chan in, + ) { + spawn AxiRamReader< + INST_AXI_ADDR_W, INST_AXI_DATA_W, INST_AXI_DEST_W, INST_AXI_ID_W, + INST_RAM_SIZE, INST_RAM_DATA_W, INST_RAM_ADDR_W, INST_RAM_NUM_PARTITIONS, + INST_BASE_ADDR, INST_AXI_DATA_W_DIV8 + > (axi_ar_r, axi_r_s, rd_req_s, rd_resp_r); + + ( + wr_req_s, wr_resp_r + ) + } + + next(state: ()) { + send_if(join(), wr_req_s, false, zero!()); + recv_if(join(), wr_resp_r, false, zero!()); + } +} + +const TEST_AXI_ADDR_W = u32:32; +const TEST_AXI_DATA_W = u32:32; +const TEST_AXI_DEST_W = u32:8; +const TEST_AXI_ID_W = u32:8; +const TEST_AXI_DATA_W_DIV8 = TEST_AXI_DATA_W / u32:8; + +const TEST_RAM_SIZE = u32:100; +const TEST_RAM_DATA_W = TEST_AXI_DATA_W; +const TEST_RAM_ADDR_W = std::clog2(TEST_RAM_SIZE); +const TEST_RAM_WORD_PARTITION_SIZE = u32:8; +const TEST_RAM_NUM_PARTITIONS = TEST_RAM_DATA_W / TEST_RAM_WORD_PARTITION_SIZE; +const TEST_RAM_SIZE_BYTES = TEST_RAM_SIZE * (TEST_RAM_DATA_W / u32:8); + +const TEST_BASE_ADDR = u32:0x8000; + +type TestAxiAr = axi::AxiAr; +type TestAxiR = axi::AxiR; + +type TestReadReq = ram::ReadReq; +type TestReadResp = ram::ReadResp; +type TestWriteReq = ram::WriteReq; +type TestWriteResp = ram::WriteResp; + +const ZERO_AXI_AR_BUNDLE = zero!(); + +type TestAxiId = uN[TEST_AXI_ID_W]; +type TestAxiAddr = uN[TEST_AXI_ADDR_W]; +type TestAxiRegion = uN[4]; +type TestAxiLen = uN[8]; +type TestAxiSize = axi::AxiAxSize; +type TestAxiBurst = axi::AxiAxBurst; +type TestAxiCache = axi::AxiArCache; +type TestAxiProt = uN[3]; +type TestAxiQos = uN[4]; + +const TEST_RAM_DATA = u32[TEST_RAM_SIZE]:[ + u32:0xD945_50A5, u32:0xA20C_D8D3, u32:0xB0BE_D046, u32:0xF83C_6D26, u32:0xFAE4_B0C4, + u32:0x9A78_91C4, u32:0xFDA0_9B1E, u32:0x5E66_D76D, u32:0xCB7D_76CB, u32:0x4033_5F2F, + u32:0x2128_9B0B, u32:0xD263_365F, u32:0xD989_DD81, u32:0xE4CB_45C9, u32:0x0425_06B6, + u32:0x5D31_107C, u32:0x2282_7A67, u32:0xCAC7_0C94, u32:0x23A9_5FD8, u32:0x6122_BBC3, + u32:0x1F99_F3D0, u32:0xA70C_FB34, u32:0x3812_5EF2, u32:0x9157_61BC, u32:0x171A_C1B1, + + u32:0xDE6F_1B08, u32:0x420D_F1AF, u32:0xAEE9_F51B, u32:0xB31E_E3A3, u32:0x66AC_09D6, + u32:0x18E9_9703, u32:0xEE87_1E7A, u32:0xB63D_47DE, u32:0x59BF_4F52, u32:0x94D8_5636, + u32:0x2B81_34EE, u32:0x6711_9968, u32:0xFB2B_F8CB, u32:0x173F_CB1B, u32:0xFB94_3A67, + u32:0xF40B_714F, u32:0x383B_82FE, u32:0xA692_055E, u32:0x58A6_2110, u32:0x0185_B5E0, + u32:0x9DF0_9C22, u32:0x54CA_DB57, u32:0xC626_097F, u32:0xEA04_3110, u32:0xF11C_4D36, + + u32:0xB8CC_FAB0, u32:0x7801_3B20, u32:0x8189_BF9C, u32:0xE380_A505, u32:0x4672_AE34, + u32:0x1CD5_1B3A, u32:0x5F95_EE9E, u32:0xBC5C_9931, u32:0xBCE6_50D2, u32:0xC10D_0544, + u32:0x5AB4_DEA1, u32:0x5E20_3394, u32:0x7FDA_0CA1, u32:0x6FEC_112E, u32:0x107A_2F81, + u32:0x86CA_4491, u32:0xEA68_0EB7, u32:0x50F1_AA22, u32:0x3F47_F2CA, u32:0xE407_92F7, + u32:0xF35C_EEE0, u32:0x1D6B_E819, u32:0x3FA7_05FA, u32:0x08BB_A499, u32:0x7C0C_4812, + + u32:0xF5A5_3D5C, u32:0x079A_BE16, u32:0xACA1_F84B, u32:0x4D2B_9402, u32:0x45B1_28FD, + u32:0x2C7C_CBA5, u32:0x6874_FC32, u32:0x95A0_8288, u32:0xFB13_E707, u32:0x61F9_2FEF, + u32:0xF6E3_DAFC, u32:0xDBA0_0A80, u32:0xBB84_831B, u32:0xAD63_2520, u32:0xEFB3_D817, + u32:0xD190_C435, u32:0x9064_1E4F, u32:0x0839_3D28, u32:0x1C07_874C, u32:0xBBEB_D633, + u32:0xB0A9_C751, u32:0x83B9_A340, u32:0x028A_FF8A, u32:0xB4ED_EE5C, u32:0xD700_BD9C, +]; + +const TEST_AXI_AR_BUNDLES = TestAxiAr[16]:[ + AxiAr { + id: TestAxiId:0, + addr: TestAxiAddr:40, + len: TestAxiLen:8, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::FIXED, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:0, + addr: TestAxiAddr:440, + len: TestAxiLen:8, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::FIXED, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:1, + addr: TestAxiAddr:32, + len: TestAxiLen:8, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::FIXED, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:2, + addr: TestAxiAddr:16, + len: TestAxiLen:8, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:3, + addr: TestAxiAddr:92, + len: TestAxiLen:4, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:4, + addr: TestAxiAddr:0, + len: TestAxiLen:2, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:5, + addr: TestAxiAddr:52, + len: TestAxiLen:20, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:6, + addr: TestAxiAddr:96, + len: TestAxiLen:10, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:7, + addr: TestAxiAddr:128, + len: TestAxiLen:16, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::WRAP, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:8, + addr: TestAxiAddr:256, + len: TestAxiLen:2, + size: TestAxiSize::MAX_4B_TRANSFER, + burst: TestAxiBurst::WRAP, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:9, + addr: TestAxiAddr:32, + len: TestAxiLen:4, + size: TestAxiSize::MAX_2B_TRANSFER, + burst: TestAxiBurst::FIXED, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:10, + addr: TestAxiAddr:80, + len: TestAxiLen:4, + size: TestAxiSize::MAX_1B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:11, + addr: TestAxiAddr:256, + len: TestAxiLen:16, + size: TestAxiSize::MAX_2B_TRANSFER, + burst: TestAxiBurst::WRAP, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:12, + addr: TestAxiAddr:64, + len: TestAxiLen:2, + size: TestAxiSize::MAX_8B_TRANSFER, + burst: TestAxiBurst::FIXED, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:13, + addr: TestAxiAddr:192, + len: TestAxiLen:16, + size: TestAxiSize::MAX_64B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, + AxiAr { + id: TestAxiId:14, + addr: TestAxiAddr:16, + len: TestAxiLen:16, + size: TestAxiSize::MAX_128B_TRANSFER, + burst: TestAxiBurst::INCR, + ..ZERO_AXI_AR_BUNDLE + }, +]; + +#[test_proc] +proc AxiRamReaderTest { + terminator: chan out; + + axi_ar_s: chan out; + axi_r_r: chan in; + + wr_req_s: chan out; + wr_resp_r: chan in; + + init {} + + config( + terminator: chan out, + ) { + let (rd_req_s, rd_req_r) = chan("rd_req"); + let (rd_resp_s, rd_resp_r) = chan("rd_resp"); + let (wr_req_s, wr_req_r) = chan("wr_req"); + let (wr_resp_s, wr_resp_r) = chan("wr_resp"); + + spawn ram::RamModel ( + rd_req_r, rd_resp_s, wr_req_r, wr_resp_s + ); + + let (axi_ar_s, axi_ar_r) = chan("axi_ar"); + let (axi_r_s, axi_r_r) = chan("axi_r"); + + spawn AxiRamReader< + TEST_AXI_ADDR_W, TEST_AXI_DATA_W, TEST_AXI_DEST_W, TEST_AXI_ID_W, + TEST_RAM_SIZE, TEST_RAM_DATA_W, TEST_RAM_ADDR_W, TEST_RAM_NUM_PARTITIONS, + TEST_BASE_ADDR, TEST_AXI_DATA_W_DIV8, + >(axi_ar_r, axi_r_s, rd_req_s, rd_resp_r); + + ( + terminator, + axi_ar_s, axi_r_r, wr_req_s, wr_resp_r, + ) + } + + next(state: ()) { + type RamAddr = bits[TEST_RAM_ADDR_W]; + type RamData = bits[TEST_RAM_DATA_W]; + type RamMask = bits[TEST_RAM_NUM_PARTITIONS]; + + let tok = join(); + + // write test RAM data + let tok = for ((i, data), tok): ((u32, u32), token) in enumerate(TEST_RAM_DATA) { + let tok = send(tok, wr_req_s, TestWriteReq { + addr: i as RamAddr, + data: data, + mask: !bits[TEST_RAM_NUM_PARTITIONS]:0, + }); + let (tok, _) = recv(tok, wr_resp_r); + + tok + }(tok); + + let tok = for ((i, axi_ar_bundle), tok): ((u32, TestAxiAr), token) in enumerate(TEST_AXI_AR_BUNDLES) { + let tok = send(tok, axi_ar_s, axi_ar_bundle); + trace_fmt!("Sent bundle #{} {:#x}", i + u32:1, axi_ar_bundle); + + let size_valid = (u32:1 << (axi_ar_bundle.size as u32 + u32:3)) <= TEST_AXI_DATA_W; + + let data_len = if size_valid { + axi_ar_bundle.len as u32 + } else { + u32:0 + }; + + for (j, tok): (u32, token) in range(u32:0, TEST_RAM_SIZE) { + if (j <= data_len) { + let (tok, data) = recv(tok, axi_r_r); + trace_fmt!("Received data #{} {:#x}", j, data); + // compute address + let araddr = match axi_ar_bundle.burst { + AxiAxBurst::FIXED => { + axi_ar_bundle.addr + }, + AxiAxBurst::INCR => { + axi_ar_bundle.addr + j * (u32:1 << (axi_ar_bundle.size as u32)) + }, + AxiAxBurst::WRAP => { + (axi_ar_bundle.addr + j * (u32:1 << (axi_ar_bundle.size as u32))) % (TEST_RAM_SIZE * (TEST_RAM_DATA_W / u32:8)) + }, + }; + // create expected data using RAM data + let (expected_data, addr_valid) = for (k, (expected_data, addr_valid)): (u32, (uN[TEST_AXI_DATA_W], bool)) in range(u32:0, TEST_AXI_DATA_W / u32:8) { + if k < (u32:1 << (axi_ar_bundle.size as u32)) { + let ram_addr = (araddr + k) / (TEST_RAM_DATA_W / u32:8); + let ram_offset = ((araddr + k) % (TEST_RAM_DATA_W / u32:8)) * u32:8; + if ram_addr < TEST_RAM_SIZE { + ( + expected_data | (((TEST_RAM_DATA[ram_addr] >> ram_offset) & u32:0xFF) << (u32:8 * k)), + addr_valid, + ) + } else { + ( + uN[TEST_AXI_DATA_W]:0, + false, + ) + } + } else { + ( + expected_data, + addr_valid + ) + } + }((uN[TEST_AXI_DATA_W]:0, true)); + + let expected_rresp = if !size_valid { + AxiReadResp::SLVERR + } else if addr_valid { + AxiReadResp::OKAY + } else { + AxiReadResp::DECERR + }; + + assert_eq(expected_rresp, data.resp); + assert_eq(j == data_len, data.last); + assert_eq(axi_ar_bundle.id, data.id); + if expected_rresp == AxiReadResp::OKAY { + // valid read + assert_eq(expected_data, data.data); + } else { }; + tok + } else { tok } + }(tok) + }(tok); + + send(tok, terminator, true); + } +} + + From 018ee91481f352e245ba536faecf12e6d0e2fe34 Mon Sep 17 00:00:00 2001 From: Krzysztof Oblonczek Date: Wed, 23 Oct 2024 12:52:49 +0200 Subject: [PATCH 28/46] modules/zstd/zstd_dec: Add DSLX tests for ZstdDecoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Krzysztof Obłonczek --- xls/modules/zstd/BUILD | 78 ++++- xls/modules/zstd/axi_csr_accessor.x | 13 +- xls/modules/zstd/cocotb/data_generator.py | 19 +- xls/modules/zstd/math.x | 88 ++++++ xls/modules/zstd/memory/BUILD | 2 +- xls/modules/zstd/memory/axi_ram.x | 130 ++++---- xls/modules/zstd/sequence_executor.x | 2 +- xls/modules/zstd/zstd_dec.x | 237 ++------------ xls/modules/zstd/zstd_dec_cocotb_test.py | 15 +- xls/modules/zstd/zstd_dec_test.x | 367 ++++++++++++++++++++++ xls/modules/zstd/zstd_frame_dslx.py | 134 ++++++++ 11 files changed, 765 insertions(+), 320 deletions(-) create mode 100644 xls/modules/zstd/math.x create mode 100644 xls/modules/zstd/zstd_dec_test.x create mode 100644 xls/modules/zstd/zstd_frame_dslx.py diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index da714a8367..5a1096c666 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -48,6 +48,19 @@ common_codegen_args = { "multi_proc": "true", } +xls_dslx_library( + name = "math_dslx", + srcs = [ + "math.x", + ], +) + +xls_dslx_test( + name = "math_dslx_test", + library = ":math_dslx", + tags = ["manual"], +) + xls_dslx_library( name = "buffer_dslx", srcs = [ @@ -929,32 +942,65 @@ place_and_route( target_die_utilization_percentage = "10", ) +py_binary( + name = "zstd_test_frames_generator", + srcs = ["zstd_frame_dslx.py"], + imports = ["."], + main = "zstd_frame_dslx.py", + tags = ["manual"], + visibility = ["//xls:xls_users"], + deps = [ + requirement("zstandard"), + "//xls/common:runfiles", + "//xls/modules/zstd/cocotb:data_generator", + "@com_google_absl_py//absl:app", + "@com_google_absl_py//absl/flags", + "@com_google_protobuf//:protobuf_python", + ], +) + +genrule( + name = "zstd_test_frames_generate", + srcs = [], + outs = ["zstd_frame_testcases.x"], + cmd = "$(location :zstd_test_frames_generator) -n 2 --btype RAW RLE -o $@", + tools = [":zstd_test_frames_generator"], +) + +zstd_dec_deps = [ + ":axi_csr_accessor_dslx", + ":block_header_dec_dslx", + ":block_header_dslx", + ":common_dslx", + ":csr_config_dslx", + ":dec_mux_dslx", + ":frame_header_dec_dslx", + ":raw_block_dec_dslx", + ":repacketizer_dslx", + ":rle_block_dec_dslx", + ":sequence_executor_dslx", + "//xls/examples:ram_dslx", + "//xls/modules/zstd/memory:mem_reader_dslx", + "//xls/modules/zstd/memory:axi_ram_dslx", +] + xls_dslx_library( name = "zstd_dec_dslx", srcs = [ "zstd_dec.x", ], - deps = [ - ":axi_csr_accessor_dslx", - ":block_header_dec_dslx", - ":block_header_dslx", - ":common_dslx", - ":csr_config_dslx", - ":dec_mux_dslx", - ":frame_header_dec_dslx", - ":raw_block_dec_dslx", - ":repacketizer_dslx", - ":rle_block_dec_dslx", - ":sequence_executor_dslx", - "//xls/examples:ram_dslx", - "//xls/modules/zstd/memory:mem_reader_dslx", - ], + deps = zstd_dec_deps, ) xls_dslx_test( name = "zstd_dec_dslx_test", - library = ":zstd_dec_dslx", + srcs = [ + "zstd_dec.x", + "zstd_dec_test.x", + "zstd_frame_testcases.x", + ], tags = ["manual"], + deps = zstd_dec_deps, ) zstd_dec_codegen_args = common_codegen_args | { diff --git a/xls/modules/zstd/axi_csr_accessor.x b/xls/modules/zstd/axi_csr_accessor.x index 01cef68cd9..860b36fe36 100644 --- a/xls/modules/zstd/axi_csr_accessor.x +++ b/xls/modules/zstd/axi_csr_accessor.x @@ -89,15 +89,16 @@ pub proc AxiCsrAccessor< let tok_0 = join(); // write to CSR via AXI let (tok_1_1, axi_aw, axi_aw_valid) = recv_non_blocking(tok_0, axi_aw_r, AxiAw {id: state.w_id, addr: state.w_addr, ..zero!()}); - // validate axi aw - assert!(!(axi_aw_valid && axi_aw.addr as u32 >= REGS_N), "invalid_aw_addr"); + assert!(!(axi_aw_valid && axi_aw.addr as u32 >= (REGS_N << LOG2_DATA_W_DIV8)), "invalid_aw_addr"); assert!(!(axi_aw_valid && axi_aw.len != u8:0), "invalid_aw_len"); let (tok_1_2, axi_w, axi_w_valid) = recv_non_blocking(tok_1_1, axi_w_r, zero!()); // Send WriteRequest to CSRs let data_w = if axi_w_valid { + trace_fmt!("[CSR ACCESSOR] received csr write at {:#x}", axi_w); + let (w_data, _, _) = for (i, (w_data, strb, mask)): (u32, (uN[DATA_W], uN[DATA_W_DIV8], uN[DATA_W])) in range(u32:0, DATA_W_DIV8) { let w_data = if axi_w.strb as u1 { w_data | (axi_w.data & mask) @@ -133,7 +134,7 @@ pub proc AxiCsrAccessor< // Send ReadRequest to CSRs let (tok_3_1, axi_ar, axi_ar_valid) = recv_non_blocking(tok_0, axi_ar_r, AxiAr {id: state.r_id, addr: state.r_addr, ..zero!()}); // validate ar bundle - assert!(!(axi_ar_valid && axi_ar.addr as u32 >= REGS_N), "invalid_ar_addr"); + assert!(!(axi_ar_valid && axi_ar.addr as u32 >= (REGS_N << LOG2_DATA_W_DIV8)), "invalid_ar_addr"); assert!(!(axi_ar_valid && axi_ar.len != u8:0), "invalid_ar_len"); let rd_req = RdReq { csr: (axi_ar.addr >> LOG2_DATA_W_DIV8) as uN[LOG2_REGS_N], @@ -208,7 +209,7 @@ proc AxiCsrAccessorInst { const TEST_ID_W = u32:4; const TEST_DATA_W = u32:32; const TEST_ADDR_W = u32:16; -const TEST_REGS_N = u32:16; +const TEST_REGS_N = u32:4; const TEST_DATA_W_DIV8 = TEST_DATA_W / u32:8; const TEST_LOG2_REGS_N = std::clog2(TEST_REGS_N); const TEST_LOG2_DATA_W_DIV8 = std::clog2(TEST_DATA_W_DIV8); @@ -309,7 +310,7 @@ proc AxiCsrAccessorTest { // write CSR via AXI let axi_aw = TestAxiAw { id: i as uN[TEST_ID_W], - addr: (test_data.csr << TEST_LOG2_DATA_W_DIV8) as uN[TEST_ADDR_W], + addr: (test_data.csr as uN[TEST_ADDR_W]) << TEST_LOG2_DATA_W_DIV8, size: axi::AxiAxSize::MAX_4B_TRANSFER, len: u8:0, burst: axi::AxiAxBurst::FIXED, @@ -346,7 +347,7 @@ proc AxiCsrAccessorTest { // read CSRs via AXI let axi_ar = TestAxiAr { id: i as uN[TEST_ID_W], - addr: (test_data.csr << TEST_LOG2_DATA_W_DIV8) as uN[TEST_ADDR_W], + addr: (test_data.csr as uN[TEST_ADDR_W]) << TEST_LOG2_DATA_W_DIV8, len: u8:0, ..zero!() }; diff --git a/xls/modules/zstd/cocotb/data_generator.py b/xls/modules/zstd/cocotb/data_generator.py index 105a9a7ec7..72b60c5eee 100644 --- a/xls/modules/zstd/cocotb/data_generator.py +++ b/xls/modules/zstd/cocotb/data_generator.py @@ -17,6 +17,7 @@ from xls.common import runfiles import subprocess +import zstandard class BlockType(Enum): RAW = 0 @@ -24,6 +25,16 @@ class BlockType(Enum): COMPRESSED = 2 RANDOM = 3 + def __str__(self): + return self.name + + @staticmethod + def from_string(s): + try: + return BlockType[s] + except KeyError as e: + raise ValueError(str(e)) + def CallDecodecorpus(args): decodecorpus = Path(runfiles.get_path("decodecorpus", repository = "zstd")) cmd = args @@ -31,6 +42,10 @@ def CallDecodecorpus(args): cmd_concat = " ".join(cmd) subprocess.run(cmd_concat, shell=True, check=True) +def DecompressFrame(data): + dctx = zstandard.ZstdDecompressor() + return dctx.decompress(data) + def GenerateFrame(seed, btype, output_path): args = [] args.append("-s" + str(seed)) @@ -39,8 +54,8 @@ def GenerateFrame(seed, btype, output_path): args.append("--content-size") # Test payloads up to 16KB args.append("--max-content-size-log=14") - args.append("-p" + output_path); - args.append("-vvvvvvv"); + args.append("-p" + output_path) + args.append("-vvvvvvv") CallDecodecorpus(args) diff --git a/xls/modules/zstd/math.x b/xls/modules/zstd/math.x new file mode 100644 index 0000000000..1b9a8dd1db --- /dev/null +++ b/xls/modules/zstd/math.x @@ -0,0 +1,88 @@ +// 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 std; + +fn fast_if(cond: bool, arg1: uN[N], arg2: uN[N]) -> uN[N] { + let mask = if cond { !bits[N]:0 } else { bits[N]:0 }; + (arg1 & mask) | (arg2 & !mask) +} + +#[test] +fn fast_if_test() { + assert_eq(if true { u32:1 } else { u32:5 }, fast_if(true, u32:1, u32:5)); + assert_eq(if false { u32:1 } else { u32:5 }, fast_if(false, u32:1, u32:5)); +} + +// Log-depth shift bits left +pub fn logshiftl(n: bits[N], r: bits[R]) -> bits[N] { + for (i, y) in u32:0..R { + fast_if(r[i+:u1], { y << (bits[R]:1 << i) }, { y }) + }(n as bits[N]) +} + +#[test] +fn logshiftl_test() { + // Test varying base + assert_eq(logshiftl(bits[64]:0, bits[6]:3), bits[64]:0 << u32:3); + assert_eq(logshiftl(bits[64]:1, bits[6]:3), bits[64]:1 << u32:3); + assert_eq(logshiftl(bits[64]:2, bits[6]:3), bits[64]:2 << u32:3); + assert_eq(logshiftl(bits[64]:3, bits[6]:3), bits[64]:3 << u32:3); + assert_eq(logshiftl(bits[64]:4, bits[6]:3), bits[64]:4 << u32:3); + + // Test varying exponent + assert_eq(logshiftl(bits[64]:50, bits[6]:0), bits[64]:50 << u32:0); + assert_eq(logshiftl(bits[64]:50, bits[6]:1), bits[64]:50 << u32:1); + assert_eq(logshiftl(bits[64]:50, bits[6]:2), bits[64]:50 << u32:2); + assert_eq(logshiftl(bits[64]:50, bits[6]:3), bits[64]:50 << u32:3); + assert_eq(logshiftl(bits[64]:50, bits[6]:4), bits[64]:50 << u32:4); + + // Test overflow + let max = std::unsigned_max_value(); + assert_eq(logshiftl(max, u4:4), max << u4:4); + assert_eq(logshiftl(max, u4:5), max << u4:5); + assert_eq(logshiftl(max, u4:15), max << u4:15); + assert_eq(logshiftl(bits[24]:0xc0ffee, u8:12), bits[24]:0xfee000); +} + +// Log-depth shift bits right +pub fn logshiftr(n: bits[N], r: bits[R]) -> bits[N] { + for (i, y) in u32:0..R { + fast_if(r[i+:u1], { y >> (bits[R]:1 << i) }, { y }) + }(n as bits[N]) +} + +#[test] +fn logshiftr_test() { + // Test varying base + assert_eq(logshiftr(bits[64]:0x0fac4e782, bits[6]:3), bits[64]:0x0fac4e782 >> u32:3); + assert_eq(logshiftr(bits[64]:0x1fac4e782, bits[6]:3), bits[64]:0x1fac4e782 >> u32:3); + assert_eq(logshiftr(bits[64]:0x2fac4e782, bits[6]:3), bits[64]:0x2fac4e782 >> u32:3); + assert_eq(logshiftr(bits[64]:0x3fac4e782, bits[6]:3), bits[64]:0x3fac4e782 >> u32:3); + assert_eq(logshiftr(bits[64]:0x4fac4e782, bits[6]:3), bits[64]:0x4fac4e782 >> u32:3); + + // Test varying exponent + assert_eq(logshiftr(bits[64]:0x50fac4e782, bits[6]:0), bits[64]:0x50fac4e782 >> u32:0); + assert_eq(logshiftr(bits[64]:0x50fac4e782, bits[6]:1), bits[64]:0x50fac4e782 >> u32:1); + assert_eq(logshiftr(bits[64]:0x50fac4e782, bits[6]:2), bits[64]:0x50fac4e782 >> u32:2); + assert_eq(logshiftr(bits[64]:0x50fac4e782, bits[6]:3), bits[64]:0x50fac4e782 >> u32:3); + assert_eq(logshiftr(bits[64]:0x50fac4e782, bits[6]:4), bits[64]:0x50fac4e782 >> u32:4); + + // Test overflow + let max = std::unsigned_max_value(); + assert_eq(logshiftr(max, u4:4), max >> u4:4); + assert_eq(logshiftr(max, u4:5), max >> u4:5); + assert_eq(logshiftr(max, u4:15), max >> u4:15); + assert_eq(logshiftr(bits[24]:0xc0ffee, u8:12), bits[24]:0x000c0f); +} diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 43563e1aa9..3fec489c30 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -338,7 +338,7 @@ xls_dslx_verilog( library = ":axi_ram_dslx", opt_ir_args = { "inline_procs": "true", - "top": "__axi_ram__AxiRamReaderInstWithEmptyWrites__AxiRamReader_0__AxiRamReaderResponder_0__32_32_4_8_8_32768_7_32_4_100_next", + "top": "__axi_ram__AxiRamReaderInstWithEmptyWrites__AxiRamReader_0__AxiRamReaderResponder_0__32_32_4_5_6_8_8_32768_7_32_5_6_4_100_next", }, tags = ["manual"], verilog_file = "axi_ram.v", diff --git a/xls/modules/zstd/memory/axi_ram.x b/xls/modules/zstd/memory/axi_ram.x index 28326a848d..9683e61fc6 100644 --- a/xls/modules/zstd/memory/axi_ram.x +++ b/xls/modules/zstd/memory/axi_ram.x @@ -31,11 +31,11 @@ enum AxiRamReaderStatus: u1 { READ_BURST = 1, } -// FIXME: add default value for RAM_DATA_W_LOG2 = {std::clog2(AXI_DATA_W + u32:1)} (https://github.com/google/xls/issues/992) -struct AxiRamReaderSync { +// FIXME: add default value for RAM_DATA_W_PLUS1_LOG2 = {std::clog2(AXI_DATA_W + u32:1)} (https://github.com/google/xls/issues/992) +struct AxiRamReaderSync { do_recv_ram_resp: bool, - read_data_size: uN[RAM_DATA_W_LOG2], - read_data_offset: uN[RAM_DATA_W_LOG2], + read_data_size: uN[RAM_DATA_W_PLUS1_LOG2], + read_data_offset: uN[RAM_DATA_W_PLUS1_LOG2], send_data: bool, resp: AxiReadResp, id: uN[AXI_ID_W], @@ -50,10 +50,10 @@ struct AxiRamReaderRequesterState { ram_rd_req_idx: u8, } -// FIXME: add default value for AXI_DATA_W_LOG2 = {std::clog2(AXI_DATA_W + u32:1)} (https://github.com/google/xls/issues/992) -struct AxiRamReaderResponderState { +// FIXME: add default value for AXI_DATA_W_PLUS1_LOG2 = {std::clog2(AXI_DATA_W + u32:1)} (https://github.com/google/xls/issues/992) +struct AxiRamReaderResponderState { data: uN[AXI_DATA_W], - data_size: uN[AXI_DATA_W_LOG2], + data_size: uN[AXI_DATA_W_PLUS1_LOG2], } // Translates RAM requests to AXI read requests @@ -61,26 +61,25 @@ proc AxiRamReaderRequester< // AXI parameters AXI_ADDR_W: u32, AXI_DATA_W: u32, AXI_DEST_W: u32, AXI_ID_W: u32, - // FIXME: The parameter below should be calculated correctly, - // but causes deduction errors. The issue is possibly related to: - // https://github.com/google/xls/issues/1523 - // RAM parameters RAM_SIZE: u32, - RAM_DATA_W: u32, // = {AXI_DATA_W}, - RAM_ADDR_W: u32, // = {AXI_ADDR_W}, - RAM_NUM_PARTITIONS: u32, // = {AXI_DATA_W / u32:8 }, - - BASE_ADDR: u32, // = {u32:0}, - AXI_DATA_W_DIV8: u32, // = { AXI_DATA_W / u32:8 } + BASE_ADDR: u32 = {u32:0}, + RAM_DATA_W: u32 = {AXI_DATA_W}, + RAM_ADDR_W: u32 = {AXI_ADDR_W}, + RAM_NUM_PARTITIONS: u32 = {AXI_DATA_W / u32:8 }, + + AXI_DATA_W_DIV8: u32 = { AXI_DATA_W / u32:8 }, + RAM_DATA_W_LOG2: u32 = { std::clog2(RAM_DATA_W) }, + AXI_DATA_W_LOG2: u32 = { std::clog2(AXI_DATA_W) }, + AXI_DATA_W_PLUS1_LOG2: u32 = { std::clog2(AXI_DATA_W + u32:1) }, + RAM_DATA_W_PLUS1_LOG2: u32 = { std::clog2(RAM_DATA_W + u32:1) }, > { type AxiAr = axi::AxiAr; - // FIXME: Replace with params - type ReadReq = ram::ReadReq; + type ReadReq = ram::ReadReq; type State = AxiRamReaderRequesterState; type Status = AxiRamReaderStatus; - type Sync = AxiRamReaderSync; + type Sync = AxiRamReaderSync; axi_ar_r: chan in; rd_req_s: chan out; @@ -99,16 +98,16 @@ proc AxiRamReaderRequester< } next(state: State) { - const AXI_DATA_W_LOG2 = std::flog2(AXI_DATA_W) + u32:1; - const RAM_DATA_W_LOG2 = std::flog2(RAM_DATA_W) + u32:1; const RAM_DATA_W_DIV8 = RAM_DATA_W >> u32:3; // receive AXI read request let (tok, ar_bundle, ar_bundle_valid) = recv_if_non_blocking(join(), axi_ar_r, state.status == Status::IDLE, zero!()); // validate bundle - let ar_bundle_ok = ar_bundle_valid && ((ar_bundle.size as u32 + u32:3) < AXI_DATA_W_LOG2); - + let ar_bundle_ok = ar_bundle_valid && ((ar_bundle.size as u32 + u32:3) <= AXI_DATA_W_LOG2); + if ar_bundle_valid { + trace_fmt!("{:#x}", ar_bundle); + } else {}; let tok = send_if(tok, sync_s, ar_bundle_valid && !ar_bundle_ok, Sync { id: ar_bundle.id, resp: AxiReadResp::SLVERR, @@ -143,17 +142,17 @@ proc AxiRamReaderRequester< }; // calculate read size and offset - let arsize_bits = AXI_AXSIZE_ENCODING_TO_SIZE[state.ar_bundle.size as u3] as uN[AXI_DATA_W_LOG2]; + let arsize_bits = AXI_AXSIZE_ENCODING_TO_SIZE[state.ar_bundle.size as u3] as uN[AXI_DATA_W_PLUS1_LOG2]; - let (read_data_size, read_data_offset) = if (arsize_bits > RAM_DATA_W as uN[AXI_DATA_W_LOG2]) { + let (read_data_size, read_data_offset) = if (arsize_bits > RAM_DATA_W as uN[AXI_DATA_W_PLUS1_LOG2]) { ( - RAM_DATA_W as uN[RAM_DATA_W_LOG2], - uN[RAM_DATA_W_LOG2]:0, + RAM_DATA_W as uN[RAM_DATA_W_PLUS1_LOG2], + uN[RAM_DATA_W_PLUS1_LOG2]:0, ) } else { ( arsize_bits, - ((state.addr % RAM_DATA_W_DIV8) << u32:3) as uN[RAM_DATA_W_LOG2], + ((state.addr % RAM_DATA_W_DIV8) << u32:3) as uN[RAM_DATA_W_PLUS1_LOG2], ) }; @@ -215,27 +214,24 @@ proc AxiRamReaderResponder< // AXI parameters AXI_ADDR_W: u32, AXI_DATA_W: u32, AXI_DEST_W: u32, AXI_ID_W: u32, - // FIXME: The parameter below should be calculated correctly, - // but causes deduction errors. The issue is possibly related to: - // https://github.com/google/xls/issues/1523 - // RAM parameters RAM_SIZE: u32, - RAM_DATA_W: u32, // = {AXI_DATA_W}, - RAM_ADDR_W: u32, // = {AXI_ADDR_W}, - RAM_NUM_PARTITIONS: u32, // = {AXI_DATA_W / u32:8 }, - - BASE_ADDR: u32, // = {u32:0}, - AXI_DATA_W_DIV8: u32, // = { AXI_DATA_W / u32:8 } + BASE_ADDR: u32 = {u32:0}, + RAM_DATA_W: u32 = {AXI_DATA_W}, + RAM_ADDR_W: u32 = {AXI_ADDR_W}, + RAM_NUM_PARTITIONS: u32 = {AXI_DATA_W / u32:8 }, + + AXI_DATA_W_DIV8: u32 = { AXI_DATA_W / u32:8 }, + AXI_DATA_W_LOG2: u32 = { std::clog2(AXI_DATA_W) }, + RAM_DATA_W_LOG2: u32 = { std::clog2(RAM_DATA_W) }, + AXI_DATA_W_PLUS1_LOG2: u32 = { std::clog2(AXI_DATA_W + u32:1) }, + RAM_DATA_W_PLUS1_LOG2: u32 = { std::clog2(RAM_DATA_W + u32:1) }, > { - // FIXME: Replace with params - type AxiR = axi::AxiR; + type AxiR = axi::AxiR; type ReadResp = ram::ReadResp; - // FIXME: Replace with params - type State = AxiRamReaderResponderState; - // FIXME: Replace with params - type Sync = AxiRamReaderSync; + type State = AxiRamReaderResponderState; + type Sync = AxiRamReaderSync; rd_resp_r: chan in; axi_r_s: chan out; @@ -297,7 +293,7 @@ proc AxiRamReaderResponder< } } -proc AxiRamReader< +pub proc AxiRamReader< // AXI parameters AXI_ADDR_W: u32, AXI_DATA_W: u32, @@ -306,17 +302,14 @@ proc AxiRamReader< // RAM parameters RAM_SIZE: u32, - - // FIXME: The parameter below should be calculated correctly, - // but causes deduction errors. The issue is possibly related to: - // https://github.com/google/xls/issues/1523 - - RAM_DATA_W: u32, // = {AXI_DATA_W}, - RAM_ADDR_W: u32, // = {AXI_ADDR_W}, - RAM_NUM_PARTITIONS: u32, // = { AXI_DATA_W / u32:8 }, - - BASE_ADDR: u32, // = {u32:0}, - AXI_DATA_W_DIV8: u32, // = { AXI_DATA_W / u32:8 } + BASE_ADDR: u32 = {u32:0}, + RAM_DATA_W: u32 = {AXI_DATA_W}, + RAM_ADDR_W: u32 = {AXI_ADDR_W}, + RAM_NUM_PARTITIONS: u32 = { AXI_DATA_W / u32:8 }, + + AXI_DATA_W_DIV8: u32 = { AXI_DATA_W / u32:8 }, + RAM_DATA_W_LOG2: u32 = { std::clog2(RAM_DATA_W) }, + RAM_DATA_W_PLUS1_LOG2: u32 = { std::clog2(RAM_DATA_W + u32:1) }, > { type AxiAr = axi::AxiAr; type AxiR = axi::AxiR; @@ -324,8 +317,7 @@ proc AxiRamReader< type ReadReq = ram::ReadReq; type ReadResp = ram::ReadResp; - // FIXME: Replace with params - type Sync = AxiRamReaderSync; + type Sync = AxiRamReaderSync; init { } @@ -342,13 +334,13 @@ proc AxiRamReader< spawn AxiRamReaderRequester< AXI_ADDR_W, AXI_DATA_W, AXI_DEST_W, AXI_ID_W, - RAM_SIZE, RAM_DATA_W, RAM_ADDR_W, RAM_NUM_PARTITIONS, - BASE_ADDR, AXI_DATA_W_DIV8, + RAM_SIZE, BASE_ADDR, RAM_DATA_W, RAM_ADDR_W, RAM_NUM_PARTITIONS, + AXI_DATA_W_DIV8, >(axi_ar_r, rd_req_s, sync_s); spawn AxiRamReaderResponder< AXI_ADDR_W, AXI_DATA_W, AXI_DEST_W, AXI_ID_W, - RAM_SIZE, RAM_DATA_W, RAM_ADDR_W, RAM_NUM_PARTITIONS, - BASE_ADDR, AXI_DATA_W_DIV8, + RAM_SIZE, BASE_ADDR, RAM_DATA_W, RAM_ADDR_W, RAM_NUM_PARTITIONS, + AXI_DATA_W_DIV8, >(rd_resp_r, axi_r_s, sync_r); } @@ -389,8 +381,8 @@ proc AxiRamReaderInst< ) { spawn AxiRamReader< INST_AXI_ADDR_W, INST_AXI_DATA_W, INST_AXI_DEST_W, INST_AXI_ID_W, - INST_RAM_SIZE, INST_RAM_DATA_W, INST_RAM_ADDR_W, INST_RAM_NUM_PARTITIONS, - INST_BASE_ADDR, INST_AXI_DATA_W_DIV8 + INST_RAM_SIZE, INST_BASE_ADDR, INST_RAM_DATA_W, INST_RAM_ADDR_W, INST_RAM_NUM_PARTITIONS, + INST_AXI_DATA_W_DIV8 > (axi_ar_r, axi_r_s, rd_req_s, rd_resp_r); } @@ -423,8 +415,8 @@ proc AxiRamReaderInstWithEmptyWrites { ) { spawn AxiRamReader< INST_AXI_ADDR_W, INST_AXI_DATA_W, INST_AXI_DEST_W, INST_AXI_ID_W, - INST_RAM_SIZE, INST_RAM_DATA_W, INST_RAM_ADDR_W, INST_RAM_NUM_PARTITIONS, - INST_BASE_ADDR, INST_AXI_DATA_W_DIV8 + INST_RAM_SIZE, INST_BASE_ADDR, INST_RAM_DATA_W, INST_RAM_ADDR_W, INST_RAM_NUM_PARTITIONS, + INST_AXI_DATA_W_DIV8 > (axi_ar_r, axi_r_s, rd_req_s, rd_resp_r); ( @@ -659,8 +651,8 @@ proc AxiRamReaderTest { spawn AxiRamReader< TEST_AXI_ADDR_W, TEST_AXI_DATA_W, TEST_AXI_DEST_W, TEST_AXI_ID_W, - TEST_RAM_SIZE, TEST_RAM_DATA_W, TEST_RAM_ADDR_W, TEST_RAM_NUM_PARTITIONS, - TEST_BASE_ADDR, TEST_AXI_DATA_W_DIV8, + TEST_RAM_SIZE, TEST_BASE_ADDR, TEST_RAM_DATA_W, TEST_RAM_ADDR_W, TEST_RAM_NUM_PARTITIONS, + TEST_AXI_DATA_W_DIV8, >(axi_ar_r, axi_r_s, rd_req_s, rd_resp_r); ( diff --git a/xls/modules/zstd/sequence_executor.x b/xls/modules/zstd/sequence_executor.x index a1fea91d50..7042dcc847 100644 --- a/xls/modules/zstd/sequence_executor.x +++ b/xls/modules/zstd/sequence_executor.x @@ -1140,7 +1140,7 @@ pub proc SequenceExecutor in; notify_s: chan<()> out; + reset_s: chan<()> out; init { zero!() @@ -190,6 +191,7 @@ proc ZstdDecoderInternal< rle_resp_r: chan in, notify_s: chan<()> out, + reset_s: chan<()> out, ) { ( csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, @@ -197,7 +199,7 @@ proc ZstdDecoderInternal< bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, - notify_s, + notify_s, reset_s, ) } @@ -214,6 +216,16 @@ proc ZstdDecoderInternal< let (tok1_0, csr_change, csr_change_valid) = recv_non_blocking(tok0, csr_change_r, zero!()); let is_start = (csr_change_valid && (csr_change.csr == csr(Csr::START))); + let is_reset = (csr_change_valid && (csr_change.csr == csr(Csr::RESET))); + let tok = send_if(tok0, reset_s, is_reset, ()); + if is_reset { + trace_fmt!("[[RESET]]"); + } else {}; + + if csr_change_valid { + trace_fmt!("[CSR CHANGE] {:#x}", csr_change); + } else {}; + let do_send_csr_req = (state.fsm == Fsm::READ_CONFIG) && (!state.conf_send); let csr_req = CSR_REQS[state.conf_cnt]; let tok1_1 = send_if(tok0, csr_rd_req_s, do_send_csr_req, csr_req); @@ -484,26 +496,8 @@ proc ZstdDecoderInternal< const TEST_AXI_DATA_W = u32:64; const TEST_AXI_ADDR_W = u32:32; -const TEST_AXI_ID_W = u32:8; -const TEST_AXI_DEST_W = u32:8; const TEST_REGS_N = u32:5; -const TEST_WINDOW_LOG_MAX = u32:30; - -const TEST_HB_ADDR_W = sequence_executor::ZSTD_RAM_ADDR_WIDTH; -const TEST_HB_DATA_W = sequence_executor::RAM_DATA_WIDTH; -const TEST_HB_NUM_PARTITIONS = sequence_executor::RAM_NUM_PARTITIONS; -const TEST_HB_SIZE_KB = sequence_executor::ZSTD_HISTORY_BUFFER_SIZE_KB; - const TEST_LOG2_REGS_N = std::clog2(TEST_REGS_N); -const TEST_AXI_DATA_W_DIV8 = TEST_AXI_DATA_W / u32:8; -const TEST_HB_RAM_N = u32:8; - -const TEST_HB_SIZE = sequence_executor::ram_size(TEST_HB_SIZE_KB); -const TEST_HB_RAM_WORD_PARTITION_SIZE = sequence_executor::RAM_WORD_PARTITION_SIZE; -const TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = sequence_executor::TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR; -const TEST_HB_RAM_INITIALIZED = sequence_executor::TEST_RAM_INITIALIZED; -const TEST_HB_RAM_ASSERT_VALID_READ:bool = {false}; - #[test_proc] proc ZstdDecoderInternalTest { @@ -562,6 +556,7 @@ proc ZstdDecoderInternalTest { rle_resp_s: chan out; notify_r: chan<()> in; + reset_r: chan<()> in; init {} @@ -585,6 +580,7 @@ proc ZstdDecoderInternalTest { let (rle_resp_s, rle_resp_r) = chan("rle_resp"); let (notify_s, notify_r) = chan<()>("notify"); + let (reset_s, reset_r) = chan<()>("reset"); spawn ZstdDecoderInternal( csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, @@ -592,7 +588,7 @@ proc ZstdDecoderInternalTest { bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, - notify_s, + notify_s, reset_s, ); ( @@ -602,7 +598,7 @@ proc ZstdDecoderInternalTest { bh_req_r, bh_resp_s, raw_req_r, raw_resp_s, rle_req_r, rle_resp_s, - notify_r, + notify_r, reset_r, ) } @@ -760,7 +756,7 @@ proc ZstdDecoderInternalTest { } -proc ZstdDecoder< +pub proc ZstdDecoder< // AXI parameters AXI_DATA_W: u32, AXI_ADDR_W: u32, AXI_ID_W: u32, AXI_DEST_W: u32, // decoder parameters @@ -875,6 +871,7 @@ proc ZstdDecoder< // Decoder output output_s: chan out, notify_s: chan<()> out, + reset_s: chan<()> out, ) { const CHANNEL_DEPTH = u32:1; @@ -1012,7 +1009,7 @@ proc ZstdDecoder< bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, - notify_s, + notify_s, reset_s, ); (cmp_output_s,) @@ -1023,192 +1020,6 @@ proc ZstdDecoder< } } -//#[test_proc] -//proc ZstdDecoderTest { -// type CsrAxiAr = axi::AxiAr; -// type CsrAxiR = axi::AxiR; -// type CsrAxiAw = axi::AxiAw; -// type CsrAxiW = axi::AxiW; -// type CsrAxiB = axi::AxiB; -// -// type CsrRdReq = csr_config::CsrRdReq; -// type CsrRdResp = csr_config::CsrRdResp; -// type CsrWrReq = csr_config::CsrWrReq; -// type CsrWrResp = csr_config::CsrWrResp; -// type CsrChange = csr_config::CsrChange; -// -// type MemAxiAr = axi::AxiAr; -// type MemAxiR = axi::AxiR; -// type MemAxiAw = axi::AxiAw; -// type MemAxiW = axi::AxiW; -// type MemAxiB = axi::AxiB; -// -// type RamRdReq = ram::ReadReq; -// type RamRdResp = ram::ReadResp; -// type RamWrReq = ram::WriteReq; -// type RamWrResp = ram::WriteResp; -// -// type ZstdDecodedPacket = common::ZstdDecodedPacket; -// terminator: chan out; -// -// init {} -// -// config(terminator: chan out) { -// -// let (csr_axi_aw_s, csr_axi_aw_r) = chan("csr_axi_aw"); -// let (csr_axi_w_s, csr_axi_w_r) = chan("csr_axi_w"); -// let (csr_axi_b_s, csr_axi_b_r) = chan("csr_axi_b"); -// let (csr_axi_ar_s, csr_axi_ar_r) = chan("csr_axi_ar"); -// let (csr_axi_r_s, csr_axi_r_r) = chan("csr_axi_r"); -// -// let (fh_axi_ar_s, fh_axi_ar_r) = chan("fh_axi_ar_s"); -// let (fh_axi_r_s, fh_axi_r_r) = chan("fh_axi_r_r"); -// -// let (bh_axi_ar_s, bh_axi_ar_r) = chan("bh_axi_ar"); -// let (bh_axi_r_s, bh_axi_r_r) = chan("bh_axi_r"); -// -// let (raw_axi_ar_s, raw_axi_ar_r) = chan("raw_axi_ar"); -// let (raw_axi_r_s, raw_axi_r_r) = chan("raw_axi_r"); -// -// let (rle_axi_ar_s, rle_axi_ar_r) = chan("rle_axi_ar"); -// let (rle_axi_r_s, rle_axi_r_r) = chan("rle_axi_r"); -// -// let (ram_rd_req_0_s, ram_rd_req_0_r) = chan("ram_rd_req_0"); -// let (ram_rd_req_1_s, ram_rd_req_1_r) = chan("ram_rd_req_1"); -// let (ram_rd_req_2_s, ram_rd_req_2_r) = chan("ram_rd_req_2"); -// let (ram_rd_req_3_s, ram_rd_req_3_r) = chan("ram_rd_req_3"); -// let (ram_rd_req_4_s, ram_rd_req_4_r) = chan("ram_rd_req_4"); -// let (ram_rd_req_5_s, ram_rd_req_5_r) = chan("ram_rd_req_5"); -// let (ram_rd_req_6_s, ram_rd_req_6_r) = chan("ram_rd_req_6"); -// let (ram_rd_req_7_s, ram_rd_req_7_r) = chan("ram_rd_req_7"); -// -// let (ram_rd_resp_0_s, ram_rd_resp_0_r) = chan("ram_rd_resp_0"); -// let (ram_rd_resp_1_s, ram_rd_resp_1_r) = chan("ram_rd_resp_1"); -// let (ram_rd_resp_2_s, ram_rd_resp_2_r) = chan("ram_rd_resp_2"); -// let (ram_rd_resp_3_s, ram_rd_resp_3_r) = chan("ram_rd_resp_3"); -// let (ram_rd_resp_4_s, ram_rd_resp_4_r) = chan("ram_rd_resp_4"); -// let (ram_rd_resp_5_s, ram_rd_resp_5_r) = chan("ram_rd_resp_5"); -// let (ram_rd_resp_6_s, ram_rd_resp_6_r) = chan("ram_rd_resp_6"); -// let (ram_rd_resp_7_s, ram_rd_resp_7_r) = chan("ram_rd_resp_7"); -// -// let (ram_wr_req_0_s, ram_wr_req_0_r) = chan("ram_wr_req_0"); -// let (ram_wr_req_1_s, ram_wr_req_1_r) = chan("ram_wr_req_1"); -// let (ram_wr_req_2_s, ram_wr_req_2_r) = chan("ram_wr_req_2"); -// let (ram_wr_req_3_s, ram_wr_req_3_r) = chan("ram_wr_req_3"); -// let (ram_wr_req_4_s, ram_wr_req_4_r) = chan("ram_wr_req_4"); -// let (ram_wr_req_5_s, ram_wr_req_5_r) = chan("ram_wr_req_5"); -// let (ram_wr_req_6_s, ram_wr_req_6_r) = chan("ram_wr_req_6"); -// let (ram_wr_req_7_s, ram_wr_req_7_r) = chan("ram_wr_req_7"); -// -// let (ram_wr_resp_0_s, ram_wr_resp_0_r) = chan("ram_wr_resp_0"); -// let (ram_wr_resp_1_s, ram_wr_resp_1_r) = chan("ram_wr_resp_1"); -// let (ram_wr_resp_2_s, ram_wr_resp_2_r) = chan("ram_wr_resp_2"); -// let (ram_wr_resp_3_s, ram_wr_resp_3_r) = chan("ram_wr_resp_3"); -// let (ram_wr_resp_4_s, ram_wr_resp_4_r) = chan("ram_wr_resp_4"); -// let (ram_wr_resp_5_s, ram_wr_resp_5_r) = chan("ram_wr_resp_5"); -// let (ram_wr_resp_6_s, ram_wr_resp_6_r) = chan("ram_wr_resp_6"); -// let (ram_wr_resp_7_s, ram_wr_resp_7_r) = chan("ram_wr_resp_7"); -// -// let (output_s, output_r) = chan("output"); -// let (notify_s, notify_r) = chan<()>("notify"); -// -// spawn ZstdDecoder< -// TEST_AXI_DATA_W, TEST_AXI_ADDR_W, TEST_AXI_ID_W, TEST_AXI_DEST_W, -// TEST_REGS_N, TEST_WINDOW_LOG_MAX, -// TEST_HB_ADDR_W, TEST_HB_DATA_W, TEST_HB_NUM_PARTITIONS, TEST_HB_SIZE_KB, -// >( -// csr_axi_aw_r, csr_axi_w_r, csr_axi_b_s, csr_axi_ar_r, csr_axi_r_s, -// fh_axi_ar_s, fh_axi_r_r, -// bh_axi_ar_s, bh_axi_r_r, -// raw_axi_ar_s, raw_axi_r_r, -// rle_axi_ar_s, rle_axi_r_r, -// ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, -// ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, -// ram_rd_resp_0_r, ram_rd_resp_1_r, ram_rd_resp_2_r, ram_rd_resp_3_r, -// ram_rd_resp_4_r, ram_rd_resp_5_r, ram_rd_resp_6_r, ram_rd_resp_7_r, -// ram_wr_req_0_s, ram_wr_req_1_s, ram_wr_req_2_s, ram_wr_req_3_s, -// ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, -// ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, -// ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r, -// output_s, notify_s, -// ); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_HB_RAM_ASSERT_VALID_READ -// > (ram_rd_req_0_r, ram_rd_resp_0_s, ram_wr_req_0_r, ram_wr_resp_0_s); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_HB_RAM_ASSERT_VALID_READ -// > (ram_rd_req_1_r, ram_rd_resp_1_s, ram_wr_req_1_r, ram_wr_resp_1_s); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_RAM_ASSERT_VALID_READ -// > (ram_rd_req_2_r, ram_rd_resp_2_s, ram_wr_req_2_r, ram_wr_resp_2_s); -// -// spawn ram::RamModel< -// RAM_DATA_WIDTH, TEST_RAM_SIZE, RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_RAM_ASSERT_VALID_READ -// > (ram_rd_req_3_r, ram_rd_resp_3_s, ram_wr_req_3_r, ram_wr_resp_3_s); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_RAM_ASSERT_VALID_READ -// > (ram_rd_req_4_r, ram_rd_resp_4_s, ram_wr_req_4_r, ram_wr_resp_4_s); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_RAM_ASSERT_VALID_READ -// > (ram_rd_req_5_r, ram_rd_resp_5_s, ram_wr_req_5_r, ram_wr_resp_5_s); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_RAM_ASSERT_VALID_READ -// > (ram_rd_req_6_r, ram_rd_resp_6_s, ram_wr_req_6_r, ram_wr_resp_6_s); -// -// spawn ram::RamModel< -// TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, -// TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, -// TEST_RAM_ASSERT_VALID_READ -// > (ram_rd_req_7_r, ram_rd_resp_7_s, ram_wr_req_7_r, ram_wr_resp_7_s); -// -// ( -// terminator, -// csr_axi_aw_s, csr_axi_w_s, csr_axi_b_r, csr_axi_ar_s, csr_axi_s_r, -// fh_axi_ar_r, fh_axi_s_s, -// bh_axi_ar_r, bh_axi_s_s, -// raw_axi_ar_r, raw_axi_s_s, -// rle_axi_ar_r, rle_axi_s_s, -// ram_sd_seq_0_r, ram_sd_seq_1_r, ram_sd_seq_2_r, ram_sd_seq_3_r, -// ram_sd_seq_4_r, ram_sd_seq_5_r, ram_sd_seq_6_r, ram_sd_seq_7_r, -// ram_sd_sesp_0_s, ram_sd_sesp_1_s, ram_sd_sesp_2_s, ram_sd_sesp_3_s, -// ram_sd_sesp_4_s, ram_sd_sesp_5_s, ram_sd_sesp_6_s, ram_sd_sesp_7_s, -// ram_wr_seq_0_r, ram_wr_seq_1_r, ram_wr_seq_2_r, ram_wr_seq_3_r, -// ram_wr_seq_4_r, ram_wr_seq_5_r, ram_wr_seq_6_r, ram_wr_seq_7_r, -// ram_wr_sesp_0_s, ram_wr_sesp_1_s, ram_wr_sesp_2_s, ram_wr_sesp_3_s, -// ram_wr_sesp_4_s, ram_wr_sesp_5_s, ram_wr_sesp_6_s, ram_wr_sesp_7_s, -// output_r, notify_r, -// ) -// } -// -// next (state: ()) { -// -// trace_fmt!("Test start"); -// -// send(join(), terminator, true); -// } -//} - - const INST_AXI_DATA_W = u32:64; const INST_AXI_ADDR_W = u32:16; const INST_AXI_ID_W = u32:4; @@ -1273,6 +1084,7 @@ proc ZstdDecoderInternalInst { // IRQ notify_s: chan<()> out, + reset_s: chan<()> out, ) { spawn ZstdDecoderInternal< INST_AXI_DATA_W, INST_AXI_ADDR_W, INST_REGS_N, @@ -1282,7 +1094,7 @@ proc ZstdDecoderInternalInst { bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, - notify_s, + notify_s, reset_s, ); } @@ -1369,6 +1181,7 @@ proc ZstdDecoderInst { // Decoder output output_s: chan out, notify_s: chan<()> out, + reset_s: chan<()> out, ) { spawn ZstdDecoder< INST_AXI_DATA_W, INST_AXI_ADDR_W, INST_AXI_ID_W, INST_AXI_DEST_W, @@ -1387,7 +1200,7 @@ proc ZstdDecoderInst { ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r, - output_s, notify_s, + output_s, notify_s, reset_s, ); } diff --git a/xls/modules/zstd/zstd_dec_cocotb_test.py b/xls/modules/zstd/zstd_dec_cocotb_test.py index dc3f5dd447..e8112c2afb 100644 --- a/xls/modules/zstd/zstd_dec_cocotb_test.py +++ b/xls/modules/zstd/zstd_dec_cocotb_test.py @@ -29,15 +29,13 @@ from cocotbext.axi.axi_ram import AxiRam from cocotbext.axi.sparse_memory import SparseMemory -import zstandard - from xls.common import runfiles from xls.modules.zstd.cocotb.channel import ( XLSChannel, XLSChannelDriver, XLSChannelMonitor, ) -from xls.modules.zstd.cocotb.data_generator import GenerateFrame, BlockType +from xls.modules.zstd.cocotb.data_generator import GenerateFrame, DecompressFrame, BlockType from xls.modules.zstd.cocotb.memory import init_axi_mem, AxiRamFromFile from xls.modules.zstd.cocotb.utils import reset, run_test from xls.modules.zstd.cocotb.xlsstruct import XLSStruct, xls_dataclass @@ -132,15 +130,6 @@ def connect_axi_bus(dut, name=""): return AxiBus(bus_axi_write, bus_axi_read) -def get_decoded_frame_bytes(ifh): - dctx = zstandard.ZstdDecompressor() - return dctx.decompress(ifh.read()) - -def get_decoded_frame_buffer(ifh, address, memory=SparseMemory(size=MAX_ENCODED_FRAME_SIZE_B)): - dctx = zstandard.ZstdDecompressor() - memory.write(address, dctx.decompress(ifh.read())) - return memory - async def csr_write(cpu, csr, data): if type(data) is int: data = data.to_bytes(AXI_DATA_W_BYTES, byteorder='little') @@ -300,7 +289,7 @@ async def test_decoder(dut, seed, block_type, scoreboard, axi_buses, xls_channel # Generate ZSTD frame to temporary file GenerateFrame(seed, block_type, encoded.name) - expected_decoded_frame = get_decoded_frame_bytes(encoded) + expected_decoded_frame = DecompressFrame(encoded.read()) encoded.close() expected_output_packets = generate_expected_output(expected_decoded_frame) diff --git a/xls/modules/zstd/zstd_dec_test.x b/xls/modules/zstd/zstd_dec_test.x new file mode 100644 index 0000000000..a166e1743d --- /dev/null +++ b/xls/modules/zstd/zstd_dec_test.x @@ -0,0 +1,367 @@ +// 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 std; +import xls.examples.ram; +import xls.modules.zstd.common; +import xls.modules.zstd.memory.axi; +import xls.modules.zstd.csr_config; +import xls.modules.zstd.sequence_executor; +import xls.modules.zstd.zstd_frame_testcases; +import xls.modules.zstd.memory.axi_ram; +import xls.modules.zstd.zstd_dec; + +const TEST_WINDOW_LOG_MAX = u32:30; + +const TEST_AXI_DATA_W = u32:64; +const TEST_AXI_ADDR_W = u32:32; +const TEST_AXI_ID_W = u32:8; +const TEST_AXI_DEST_W = u32:8; +const TEST_AXI_DATA_W_DIV8 = TEST_AXI_DATA_W / u32:8; + +const TEST_REGS_N = u32:5; +const TEST_LOG2_REGS_N = std::clog2(TEST_REGS_N); + +const TEST_HB_RAM_N = u32:8; +const TEST_HB_ADDR_W = sequence_executor::ZSTD_RAM_ADDR_WIDTH; +const TEST_HB_DATA_W = sequence_executor::RAM_DATA_WIDTH; +const TEST_HB_NUM_PARTITIONS = sequence_executor::RAM_NUM_PARTITIONS; +const TEST_HB_SIZE_KB = sequence_executor::ZSTD_HISTORY_BUFFER_SIZE_KB; +const TEST_HB_RAM_SIZE = sequence_executor::ZSTD_RAM_SIZE; +const TEST_HB_RAM_WORD_PARTITION_SIZE = sequence_executor::RAM_WORD_PARTITION_SIZE; +const TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = sequence_executor::TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR; +const TEST_HB_RAM_INITIALIZED = sequence_executor::TEST_RAM_INITIALIZED; +const TEST_HB_RAM_ASSERT_VALID_READ:bool = false; + +const TEST_RAM_DATA_W:u32 = TEST_AXI_DATA_W; +const TEST_RAM_SIZE:u32 = u32:16384; +const TEST_RAM_ADDR_W:u32 = std::clog2(TEST_RAM_SIZE); +const TEST_RAM_WORD_PARTITION_SIZE:u32 = u32:8; +const TEST_RAM_NUM_PARTITIONS:u32 = ram::num_partitions(TEST_RAM_WORD_PARTITION_SIZE, TEST_RAM_DATA_W); +const TEST_RAM_BASE_ADDR:u32 = u32:0; +const TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_RAM_INITIALIZED = true; + +fn csr_addr(c: zstd_dec::Csr) -> uN[TEST_AXI_ADDR_W] { + (c as uN[TEST_AXI_ADDR_W]) << 3 +} + +#[test_proc] +proc ZstdDecoderTest { + type CsrAxiAr = axi::AxiAr; + type CsrAxiR = axi::AxiR; + type CsrAxiAw = axi::AxiAw; + type CsrAxiW = axi::AxiW; + type CsrAxiB = axi::AxiB; + + type CsrRdReq = csr_config::CsrRdReq; + type CsrRdResp = csr_config::CsrRdResp; + type CsrWrReq = csr_config::CsrWrReq; + type CsrWrResp = csr_config::CsrWrResp; + type CsrChange = csr_config::CsrChange; + + type MemAxiAr = axi::AxiAr; + type MemAxiR = axi::AxiR; + type MemAxiAw = axi::AxiAw; + type MemAxiW = axi::AxiW; + type MemAxiB = axi::AxiB; + + type RamRdReqHB = ram::ReadReq; + type RamRdRespHB = ram::ReadResp; + type RamWrReqHB = ram::WriteReq; + type RamWrRespHB = ram::WriteResp; + + type RamRdReq = ram::ReadReq; + type RamRdResp = ram::ReadResp; + type RamWrReq = ram::WriteReq; + type RamWrResp = ram::WriteResp; + + type ZstdDecodedPacket = common::ZstdDecodedPacket; + terminator: chan out; + csr_axi_aw_s: chan out; + csr_axi_w_s: chan out; + csr_axi_b_r: chan in; + csr_axi_ar_s: chan out; + csr_axi_r_r: chan in; + fh_axi_ar_r: chan in; + fh_axi_r_s: chan out; + bh_axi_ar_r: chan in; + bh_axi_r_s: chan out; + raw_axi_ar_r: chan in; + raw_axi_r_s: chan out; + + ram_rd_req_r: chan[8] in; + ram_rd_resp_s: chan[8] out; + ram_wr_req_r: chan[8] in; + ram_wr_resp_s: chan[8] out; + + ram_wr_req_fh_s: chan out; + ram_wr_req_bh_s: chan out; + ram_wr_req_raw_s: chan out; + raw_wr_resp_fh_r: chan in; + raw_wr_resp_bh_r: chan in; + raw_wr_resp_raw_r: chan in; + + output_r: chan in; + notify_r: chan<()> in; + reset_r: chan<()> in; + + init {} + + config(terminator: chan out) { + + let (csr_axi_aw_s, csr_axi_aw_r) = chan("csr_axi_aw"); + let (csr_axi_w_s, csr_axi_w_r) = chan("csr_axi_w"); + let (csr_axi_b_s, csr_axi_b_r) = chan("csr_axi_b"); + let (csr_axi_ar_s, csr_axi_ar_r) = chan("csr_axi_ar"); + let (csr_axi_r_s, csr_axi_r_r) = chan("csr_axi_r"); + + let (fh_axi_ar_s, fh_axi_ar_r) = chan("fh_axi_ar"); + let (fh_axi_r_s, fh_axi_r_r) = chan("fh_axi_r"); + + let (bh_axi_ar_s, bh_axi_ar_r) = chan("bh_axi_ar"); + let (bh_axi_r_s, bh_axi_r_r) = chan("bh_axi_r"); + + let (raw_axi_ar_s, raw_axi_ar_r) = chan("raw_axi_ar"); + let (raw_axi_r_s, raw_axi_r_r) = chan("raw_axi_r"); + + let (ram_rd_req_s, ram_rd_req_r) = chan[8]("ram_rd_req"); + let (ram_rd_resp_s, ram_rd_resp_r) = chan[8]("ram_rd_resp"); + let (ram_wr_req_s, ram_wr_req_r) = chan[8]("ram_wr_req"); + let (ram_wr_resp_s, ram_wr_resp_r) = chan[8]("ram_wr_resp"); + + let (ram_rd_req_fh_s, ram_rd_req_fh_r) = chan("ram_rd_req_fh"); + let (ram_rd_req_bh_s, ram_rd_req_bh_r) = chan("ram_rd_req_bh"); + let (ram_rd_req_raw_s, ram_rd_req_raw_r) = chan("ram_rd_req_raw"); + let (ram_rd_resp_fh_s, ram_rd_resp_fh_r) = chan("ram_rd_resp_fh"); + let (ram_rd_resp_bh_s, ram_rd_resp_bh_r) = chan("ram_rd_resp_bh"); + let (ram_rd_resp_raw_s, ram_rd_resp_raw_r) = chan("ram_rd_resp_raw"); + + let (ram_wr_req_fh_s, ram_wr_req_fh_r) = chan("ram_wr_req_fh"); + let (ram_wr_req_bh_s, ram_wr_req_bh_r) = chan("ram_wr_req_bh"); + let (ram_wr_req_raw_s, ram_wr_req_raw_r) = chan("ram_wr_req_raw"); + let (ram_wr_resp_fh_s, ram_wr_resp_fh_r) = chan("ram_wr_resp_fh"); + let (ram_wr_resp_bh_s, ram_wr_resp_bh_r) = chan("ram_wr_resp_bh"); + let (ram_wr_resp_raw_s, ram_wr_resp_raw_r) = chan("ram_wr_resp_raw"); + + let (output_s, output_r) = chan("output"); + let (notify_s, notify_r) = chan<()>("notify"); + let (reset_s, reset_r) = chan<()>("reset"); + + spawn zstd_dec::ZstdDecoder< + TEST_AXI_DATA_W, TEST_AXI_ADDR_W, TEST_AXI_ID_W, TEST_AXI_DEST_W, + TEST_REGS_N, TEST_WINDOW_LOG_MAX, + TEST_HB_ADDR_W, TEST_HB_DATA_W, TEST_HB_NUM_PARTITIONS, TEST_HB_SIZE_KB, + >( + csr_axi_aw_r, csr_axi_w_r, csr_axi_b_s, csr_axi_ar_r, csr_axi_r_s, + fh_axi_ar_s, fh_axi_r_r, + bh_axi_ar_s, bh_axi_r_r, + raw_axi_ar_s, raw_axi_r_r, + ram_rd_req_s[0], ram_rd_req_s[1], ram_rd_req_s[2], ram_rd_req_s[3], + ram_rd_req_s[4], ram_rd_req_s[5], ram_rd_req_s[6], ram_rd_req_s[7], + ram_rd_resp_r[0], ram_rd_resp_r[1], ram_rd_resp_r[2], ram_rd_resp_r[3], + ram_rd_resp_r[4], ram_rd_resp_r[5], ram_rd_resp_r[6], ram_rd_resp_r[7], + ram_wr_req_s[0], ram_wr_req_s[1], ram_wr_req_s[2], ram_wr_req_s[3], + ram_wr_req_s[4], ram_wr_req_s[5], ram_wr_req_s[6], ram_wr_req_s[7], + ram_wr_resp_r[0], ram_wr_resp_r[1], ram_wr_resp_r[2], ram_wr_resp_r[3], + ram_wr_resp_r[4], ram_wr_resp_r[5], ram_wr_resp_r[6], ram_wr_resp_r[7], + output_s, notify_s, reset_s, + ); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[0], ram_rd_resp_s[0], ram_wr_req_r[0], ram_wr_resp_s[0]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[1], ram_rd_resp_s[1], ram_wr_req_r[1], ram_wr_resp_s[1]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[2], ram_rd_resp_s[2], ram_wr_req_r[2], ram_wr_resp_s[2]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[3], ram_rd_resp_s[3], ram_wr_req_r[3], ram_wr_resp_s[3]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[4], ram_rd_resp_s[4], ram_wr_req_r[4], ram_wr_resp_s[4]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[5], ram_rd_resp_s[5], ram_wr_req_r[5], ram_wr_resp_s[5]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[6], ram_rd_resp_s[6], ram_wr_req_r[6], ram_wr_resp_s[6]); + + spawn ram::RamModel< + TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, + TEST_HB_RAM_ASSERT_VALID_READ + > (ram_rd_req_r[7], ram_rd_resp_s[7], ram_wr_req_r[7], ram_wr_resp_s[7]); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_WORD_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, + > (ram_rd_req_fh_r, ram_rd_resp_fh_s, ram_wr_req_fh_r, ram_wr_resp_fh_s); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_WORD_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, + > (ram_rd_req_bh_r, ram_rd_resp_bh_s, ram_wr_req_bh_r, ram_wr_resp_bh_s); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_WORD_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, + > (ram_rd_req_raw_r, ram_rd_resp_raw_s, ram_wr_req_raw_r, ram_wr_resp_raw_s); + + spawn axi_ram::AxiRamReader< + TEST_AXI_ADDR_W, TEST_AXI_DATA_W, TEST_AXI_DEST_W, TEST_AXI_ID_W, + TEST_RAM_SIZE, TEST_RAM_BASE_ADDR, TEST_RAM_DATA_W, TEST_RAM_ADDR_W, + >(fh_axi_ar_r, fh_axi_r_s, ram_rd_req_fh_s, ram_rd_resp_fh_r); + + spawn axi_ram::AxiRamReader< + TEST_AXI_ADDR_W, TEST_AXI_DATA_W, TEST_AXI_DEST_W, TEST_AXI_ID_W, + TEST_RAM_SIZE, TEST_RAM_BASE_ADDR, TEST_RAM_DATA_W, TEST_RAM_ADDR_W, + >(bh_axi_ar_r, bh_axi_r_s, ram_rd_req_bh_s, ram_rd_resp_bh_r); + + spawn axi_ram::AxiRamReader< + TEST_AXI_ADDR_W, TEST_AXI_DATA_W, TEST_AXI_DEST_W, TEST_AXI_ID_W, + TEST_RAM_SIZE, TEST_RAM_BASE_ADDR, TEST_RAM_DATA_W, TEST_RAM_ADDR_W, + >(raw_axi_ar_r, raw_axi_r_s, ram_rd_req_raw_s, ram_rd_resp_raw_r); + + ( + terminator, + csr_axi_aw_s, csr_axi_w_s, csr_axi_b_r, csr_axi_ar_s, csr_axi_r_r, + fh_axi_ar_r, fh_axi_r_s, + bh_axi_ar_r, bh_axi_r_s, + raw_axi_ar_r, raw_axi_r_s, + ram_rd_req_r, ram_rd_resp_s, ram_wr_req_r, ram_wr_resp_s, + ram_wr_req_fh_s, ram_wr_req_bh_s, ram_wr_req_raw_s, + ram_wr_resp_fh_r, ram_wr_resp_bh_r, ram_wr_resp_raw_r, + output_r, notify_r, reset_r, + ) + } + + next (state: ()) { + trace_fmt!("Test start"); + let frames_count = array_size(zstd_frame_testcases::FRAMES); + + let tok = join(); + let tok = unroll_for! (test_i, tok): (u32, token) in range(u32:0, frames_count) { + trace_fmt!("Loading testcase {:x}", test_i); + let frame = zstd_frame_testcases::FRAMES[test_i]; + let tok = for (i, tok): (u32, token) in range(u32:0, frame.array_length) { + let req = RamWrReq { + addr: i as uN[TEST_RAM_ADDR_W], + data: frame.data[i] as uN[TEST_RAM_DATA_W], + mask: uN[TEST_RAM_NUM_PARTITIONS]:0xFF + }; + let tok = send(tok, ram_wr_req_fh_s, req); + let tok = send(tok, ram_wr_req_bh_s, req); + let tok = send(tok, ram_wr_req_raw_s, req); + tok + }(tok); + + trace_fmt!("Running decoder on testcase {:x}", test_i); + let addr_req = axi::AxiAw { + id: uN[TEST_AXI_ID_W]:0, + addr: uN[TEST_AXI_ADDR_W]:0, + size: axi::AxiAxSize::MAX_4B_TRANSFER, + len: u8:0, + burst: axi::AxiAxBurst::FIXED, + }; + let data_req = axi::AxiW { + data: uN[TEST_AXI_DATA_W]:0, + strb: uN[TEST_AXI_DATA_W_DIV8]:0xFF, + last: u1:1, + }; + + // reset the decoder + trace_fmt!("Sending reset"); + let tok = send(tok, csr_axi_aw_s, axi::AxiAw { + addr: csr_addr(zstd_dec::Csr::RESET), + ..addr_req + }); + let tok = send(tok, csr_axi_w_s, axi::AxiW { + data: uN[TEST_AXI_DATA_W]:0x1, + ..data_req + }); + trace_fmt!("Sent reset"); + let (tok, _) = recv(tok, csr_axi_b_r); + // Wait for reset notification before issuing further CSR writes + let (tok, _) = recv(tok, reset_r); + // configure input buffer address + let tok = send(tok, csr_axi_aw_s, axi::AxiAw { + addr: csr_addr(zstd_dec::Csr::INPUT_BUFFER), + ..addr_req + }); + let tok = send(tok, csr_axi_w_s, axi::AxiW { + data: uN[TEST_AXI_DATA_W]:0x0, + ..data_req + }); + let (tok, _) = recv(tok, csr_axi_b_r); + // configure output buffer address + let tok = send(tok, csr_axi_aw_s, axi::AxiAw { + addr: csr_addr(zstd_dec::Csr::OUTPUT_BUFFER), + ..addr_req + }); + let tok = send(tok, csr_axi_w_s, axi::AxiW { + data: uN[TEST_AXI_DATA_W]:0x1000, + ..data_req + }); + let (tok, _) = recv(tok, csr_axi_b_r); + // start decoder + let tok = send(tok, csr_axi_aw_s, axi::AxiAw { + addr: csr_addr(zstd_dec::Csr::START), + ..addr_req + }); + let tok = send(tok, csr_axi_w_s, axi::AxiW { + data: uN[TEST_AXI_DATA_W]:0x1, + ..data_req + }); + let (tok, _) = recv(tok, csr_axi_b_r); + + let decomp_frame = zstd_frame_testcases::DECOMPRESSED_FRAMES[test_i]; + let tok = for (decomp_i, tok): (u32, token) in range(u32:0, decomp_frame.array_length) { + let (tok, decomp_packet) = recv(tok, output_r); + assert_eq(decomp_packet.data, decomp_frame.data[decomp_i]); + assert_eq(decomp_packet.last, decomp_i == (decomp_frame.length - u32:1) / u32:8); + tok + }(tok); + + let (tok, ()) = recv(tok, notify_r); + trace_fmt!("Finished decoding testcase {:x} correctly", test_i); + tok + }(tok); + send(tok, terminator, true); + } +} + diff --git a/xls/modules/zstd/zstd_frame_dslx.py b/xls/modules/zstd/zstd_frame_dslx.py new file mode 100644 index 0000000000..6e36f6c563 --- /dev/null +++ b/xls/modules/zstd/zstd_frame_dslx.py @@ -0,0 +1,134 @@ +# 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 argparse +import math +import random +import tempfile +from pathlib import Path + +from xls.modules.zstd.cocotb.data_generator import ( + BlockType, + DecompressFrame, + GenerateFrame, +) + + +def GenerateTestData(seed, btype): + with tempfile.NamedTemporaryFile() as tmp: + GenerateFrame(seed, btype, tmp.name) + tmp.seek(0) + return tmp.read() + + +def Bytes2DSLX(frames, bytes_per_word, array_name): + frames_hex = [] + maxlen = max(len(frame) for frame in frames) + maxlen_size = math.ceil(maxlen / bytes_per_word) + bits_per_word = bytes_per_word * 8 + for i, frame in enumerate(frames): + frame_hex = [] + for i in range(0, len(frame), bytes_per_word): + # reverse byte order to make them little endian + word = bytes(reversed(frame[i : i + bytes_per_word])).hex() + frame_hex.append(f"uN[{bits_per_word}]:0x{word}") + + array_length = len(frame_hex) + if len(frame) < maxlen: + frame_hex += [f"uN[{bits_per_word}]:0x0", "..."] + + frame_array = ( + f"DataArray<{bits_per_word}, {maxlen_size}>{{\n" + f" length: u32:{len(frame)},\n" + f" array_length: u32:{array_length},\n" + f" data: uN[{bits_per_word}][{maxlen_size}]:[{', '.join(frame_hex)}]\n" + f"}}" + ) + frames_hex.append(frame_array) + + frames_str = ",\n".join(frames_hex) + frames_array = ( + f"pub const {array_name}:DataArray<\n" + f" u32:{bits_per_word},\n" + f" u32:{maxlen_size}\n" + f">[{len(frames_hex)}] = [{frames_str}];\n" + ) + return frames_array + + +def GenerateDataStruct(): + return ( + f"pub struct DataArray{{\n" + f" data: uN[BITS_PER_WORD][LENGTH],\n" + f" length: u32,\n" + f" array_length: u32\n" + f"}}\n" + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-n", help="Number of testcases to generate", type=int, default=1 + ) + parser.add_argument( + "--seed", help="Seed for the testcases generator", type=int, default=0 + ) + parser.add_argument( + "--btype", + help=( + "Block types allowed in the generated testcases. If multiple block types " + "are supplied, generated testcases will cycle through them" + ), + type=BlockType.from_string, + choices=list(BlockType), + default=BlockType.RANDOM, + nargs="+", + ) + parser.add_argument( + "-o", + "--output", + help="Filename of the DSLX output file", + type=Path, + default=Path("frames_test_data.x"), + ) + parser.add_argument( + "--bytes-per-word", + help="Width of a word in memory, in bytes", + type=int, + default=8, + ) + args = parser.parse_args() + + seed = random.seed(args.seed) + byte_frames = [ + GenerateTestData(random.randrange(2**32), args.btype[i % len(args.btype)]) + for i in range(args.n) + ] + with open(args.output, "w") as dslx_output: + dslx_output.write(GenerateDataStruct()) + + dslx_frames = Bytes2DSLX(byte_frames, args.bytes_per_word, "FRAMES") + dslx_output.write(dslx_frames) + + byte_frames_decompressed = list(map(DecompressFrame, byte_frames)) + dslx_frames_decompressed = Bytes2DSLX( + byte_frames_decompressed, args.bytes_per_word, "DECOMPRESSED_FRAMES" + ) + dslx_output.write(dslx_frames_decompressed) + + +if __name__ == "__main__": + main() From 37ccb129f73ad5dca99574ad9d75ce20cb2e8f9e Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 22 Oct 2024 15:38:22 +0200 Subject: [PATCH 29/46] modules/zstd/zstd_dec: handle contents of the Status CSR Internal-tag: [#66955] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/zstd_dec.x | 85 +++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/xls/modules/zstd/zstd_dec.x b/xls/modules/zstd/zstd_dec.x index be58763dd3..528d16d599 100644 --- a/xls/modules/zstd/zstd_dec.x +++ b/xls/modules/zstd/zstd_dec.x @@ -50,12 +50,19 @@ enum ZstdDecoderInternalFsm: u4 { INVALID = 15, } -enum ZstdDecoderStatus: u3 { - OKAY = 0, - FINISHED = 1, - FRAME_HEADER_CORRUPTED = 2, - FRAME_HEADER_UNSUPPORTED_WINDOW_SIZE = 3, - BLOCK_HEADER_CORRUPTED = 4, +enum ZstdDecoderStatus: u5 { + IDLE = 0, + RUNNING = 1, + READ_CONFIG_OK = 2, + FRAME_HEADER_OK = 3, + FRAME_HEADER_CORRUPTED = 4, + FRAME_HEADER_UNSUPPORTED_WINDOW_SIZE = 5, + BLOCK_HEADER_OK = 6, + BLOCK_HEADER_CORRUPTED = 7, + BLOCK_HEADER_MEMORY_ACCESS_ERROR = 8, + RAW_BLOCK_OK = 9, + RAW_BLOCK_ERROR = 10, + RLE_BLOCK_OK = 11, } pub enum Csr: u3 { @@ -317,7 +324,15 @@ proc ZstdDecoderInternal< Fsm::IDLE => { trace_fmt!("[IDLE]"); if is_start { - State { fsm: Fsm::READ_CONFIG, conf_cnt: CSR_REQS_MAX, ..zero!() } + let status = ZstdDecoderStatus::RUNNING; + + let csr_wr_req_valid = true; + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(status), + }; + + State { fsm: Fsm::READ_CONFIG, csr_wr_req, csr_wr_req_valid, conf_cnt: CSR_REQS_MAX, ..zero!() } } else { zero!() } }, @@ -337,8 +352,19 @@ proc ZstdDecoderInternal< let conf_send = (state.conf_cnt == Reg:0); let conf_cnt = if conf_send { Reg:0 } else {state.conf_cnt - Reg:1}; + let status = match(all_collected) { + true => ZstdDecoderStatus::READ_CONFIG_OK, + _ => ZstdDecoderStatus::RUNNING, + }; + + let csr_wr_req_valid = all_collected; + let csr_wr_req = CsrWrReq { + csr: csr(Csr::STATUS), + value: checked_cast(status), + }; + State { - fsm, conf_cnt, conf_send, input_buffer, input_buffer_valid, output_buffer, output_buffer_valid, + fsm, csr_wr_req, csr_wr_req_valid, conf_cnt, conf_send, input_buffer, input_buffer_valid, output_buffer, output_buffer_valid, ..zero!() } }, @@ -347,10 +373,17 @@ proc ZstdDecoderInternal< trace_fmt!("[DECODE_FRAME_HEADER]"); let error = (fh_resp.status != FrameHeaderDecoderStatus::OKAY); - let csr_wr_req_valid = (fh_resp_valid && error); + let status = match(fh_resp_valid, fh_resp.status) { + (true, FrameHeaderDecoderStatus::OKAY) => ZstdDecoderStatus::FRAME_HEADER_OK, + (true, FrameHeaderDecoderStatus::CORRUPTED) => ZstdDecoderStatus::FRAME_HEADER_CORRUPTED, + (true, FrameHeaderDecoderStatus::UNSUPPORTED_WINDOW_SIZE) => ZstdDecoderStatus::FRAME_HEADER_UNSUPPORTED_WINDOW_SIZE, + (_, _) => ZstdDecoderStatus::RUNNING, + }; + + let csr_wr_req_valid = (fh_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(fh_resp.status), + value: checked_cast(status), }; let fsm = match (fh_resp_valid, error) { @@ -368,10 +401,17 @@ proc ZstdDecoderInternal< trace_fmt!("[DECODE_BLOCK_HEADER]"); let error = (bh_resp.status != BlockHeaderDecoderStatus::OKAY); - let csr_wr_req_valid = (bh_resp_valid && error); + let status = match(bh_resp_valid, bh_resp.status) { + (true, BlockHeaderDecoderStatus::OKAY) => ZstdDecoderStatus::BLOCK_HEADER_OK, + (true, BlockHeaderDecoderStatus::CORRUPTED) => ZstdDecoderStatus::BLOCK_HEADER_CORRUPTED, + (true, BlockHeaderDecoderStatus::MEMORY_ACCESS_ERROR) => ZstdDecoderStatus::BLOCK_HEADER_MEMORY_ACCESS_ERROR, + (_, _) => ZstdDecoderStatus::RUNNING, + }; + + let csr_wr_req_valid = (bh_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(bh_resp.status), + value: checked_cast(status), }; let fsm = match (bh_resp_valid, error, bh_resp.header.btype) { @@ -413,10 +453,16 @@ proc ZstdDecoderInternal< let error = (raw_resp.status != RawBlockDecoderStatus::OKAY); - let csr_wr_req_valid = (raw_resp_valid && error); + let status = match(raw_resp_valid, raw_resp.status) { + (true, RawBlockDecoderStatus::OKAY) => ZstdDecoderStatus::RAW_BLOCK_OK, + (true, RawBlockDecoderStatus::ERROR) => ZstdDecoderStatus::RAW_BLOCK_ERROR, + (_, _) => ZstdDecoderStatus::RUNNING, + }; + + let csr_wr_req_valid = (raw_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(raw_resp.status), + value: checked_cast(status), }; let fsm = match (raw_resp_valid, error, state.block_last) { @@ -441,10 +487,15 @@ proc ZstdDecoderInternal< trace_fmt!("[DECODE_RLE_BLOCK]"); let error = (rle_resp.status != RleBlockDecoderStatus::OKAY); - let csr_wr_req_valid = (rle_resp_valid && error); + let status = match(rle_resp_valid, rle_resp.status) { + (true, RleBlockDecoderStatus::OKAY) => ZstdDecoderStatus::RLE_BLOCK_OK, + (_, _) => ZstdDecoderStatus::RUNNING, + }; + + let csr_wr_req_valid = (rle_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(rle_resp.status), + value: checked_cast(status), }; let fsm = match (rle_resp_valid, error, state.block_last) { @@ -481,7 +532,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = true; let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(fh_resp.status), + value: checked_cast(ZstdDecoderStatus::IDLE), }; State { fsm: Fsm::IDLE, csr_wr_req, csr_wr_req_valid, ..zero!() } From 8e95eba49ed486fe504ad373039e6ce256626583 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Mon, 18 Nov 2024 14:04:39 +0100 Subject: [PATCH 30/46] modules/zstd/data_generator: fix formatting Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/data_generator.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/xls/modules/zstd/data_generator.cc b/xls/modules/zstd/data_generator.cc index 81ffe95ed9..98b1eb4dba 100644 --- a/xls/modules/zstd/data_generator.cc +++ b/xls/modules/zstd/data_generator.cc @@ -60,9 +60,8 @@ static absl::StatusOr CallDecodecorpus( absl::Span args, const std::optional& cwd = std::nullopt, std::optional timeout = std::nullopt) { - XLS_ASSIGN_OR_RETURN( - std::filesystem::path path, - xls::GetXlsRunfilePath("external/zstd/decodecorpus")); + XLS_ASSIGN_OR_RETURN(std::filesystem::path path, + xls::GetXlsRunfilePath("external/zstd/decodecorpus")); std::vector cmd = {path}; cmd.insert(cmd.end(), args.begin(), args.end()); From a53b533b5e1eb2e0fbacb6a155629c07691e0c4a Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 29 Oct 2024 16:47:45 +0100 Subject: [PATCH 31/46] modules/zstd/memory:axi_stream_remove_empty: Fix byte ordering * Fix byte ordering when receiving a series of non-empty packets * Adjust MemReader DSLX tests Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- .../zstd/memory/axi_stream_remove_empty.x | 64 ++++++++++++++++++- xls/modules/zstd/memory/mem_reader.x | 9 ++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/xls/modules/zstd/memory/axi_stream_remove_empty.x b/xls/modules/zstd/memory/axi_stream_remove_empty.x index a61ec479fc..0b211852a5 100644 --- a/xls/modules/zstd/memory/axi_stream_remove_empty.x +++ b/xls/modules/zstd/memory/axi_stream_remove_empty.x @@ -130,7 +130,6 @@ pub proc AxiStreamRemoveEmpty< let exact_transfer = (empty_input_bytes == state.len); let combined_state_data = state.data | data << state.len; - let combined_input_data = data | state.data << len; let overflow_len = get_overflow_len(state.len, len); let sum_len = state.len + len; @@ -172,7 +171,7 @@ pub proc AxiStreamRemoveEmpty< // store ( State { - data: combined_input_data, + data: combined_state_data, len: sum_len, ..state }, @@ -405,6 +404,67 @@ proc AxiStreamRemoveEmptyTest { dest: Dest:0, }); + // Test 6: Some bits set, last set in the last transfer. + + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x0000_00B9, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x0000_007F, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x0000_0069, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x00DF_5EF7, + str: Str:0b0111, + keep: Keep:0b0111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x0000_C735, + str: Str:0b0011, + keep: Keep:0b0011, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xF769_7FB9, + str: Str:0xF, + keep: Keep:0xF, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xC735_DF5E, + str: Str:0xF, + keep: Keep:0xF, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); send(tok, terminator, true); } } diff --git a/xls/modules/zstd/memory/mem_reader.x b/xls/modules/zstd/memory/mem_reader.x index ea96264728..7360e2b03b 100644 --- a/xls/modules/zstd/memory/mem_reader.x +++ b/xls/modules/zstd/memory/mem_reader.x @@ -583,7 +583,8 @@ proc MemReaderTest { let tok = send(tok, axi_r_s, AxiR { id: AxiId:0x0, - data: AxiData:0x1122_3344_5566_7788_9900_AABB_CCDD_EEFF, + data: AxiData:0x1122_3344_5566_7788_9900_AABB_CCDD_EE55, + // Addresses: ^ 0xFFF ^ 0xFF0 resp: AxiResp::OKAY, last: AxiLast:true }); @@ -603,7 +604,8 @@ proc MemReaderTest { let tok = send(tok, axi_r_s, AxiR { id: AxiId:0x0, - data: AxiData:0x1122_3344_5566_7788_9900_AABB_CCDD_EEFF, + data: AxiData:0x5522_3344_5566_7788_9900_AABB_CCDD_EEFF, + // Addresses: ^ 0x100F ^ 0x1000 resp: AxiResp::OKAY, last: AxiLast:true }); @@ -611,7 +613,8 @@ proc MemReaderTest { let (tok, resp) = recv(tok, resp_r); assert_eq(resp, Resp { status: Status::OKAY, - data: Data:0x11FF, + data: Data:0xFF11, + // 0x1000 ^ ^ 0x0FFF length: Length:2, last: true }); From e2ce9e2ce9dbf5bb3bd0faabf87bd666c7fdc8a9 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Mon, 4 Nov 2024 16:22:34 +0100 Subject: [PATCH 32/46] modules/zstd/memory/axi_stream_remove_empty: Extract remove_empty_bytes function into a separate proc * Extract the operation of removing not-strobed bytes from input frames to a separate proc * Extract control logic to AxiStreamRemoveEmptyInternal proc * Optimize strobe calculation Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/BUILD | 137 ++++- .../zstd/memory/axi_stream_remove_empty.x | 555 ++++++++++++++++-- 2 files changed, 629 insertions(+), 63 deletions(-) diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 3fec489c30..bc5a40fcb3 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -59,8 +59,6 @@ CLOCK_PERIOD_PS = "750" # Clock periods for modules that exceed the 750ps critical path in IR benchmark AXI_READER_CLOCK_PERIOD_PS = "1800" -AXI_STREAM_REMOVE_EMPTY_CLOCK_PERIOD_PS = "1300" - MEM_READER_CLOCK_PERIOD_PS = "2600" common_codegen_args = { @@ -167,9 +165,68 @@ xls_dslx_test( tags = ["manual"], ) +axi_stream_remove_empty_internal_codegen_args = common_codegen_args | { + "module_name": "axi_stream_remove_empty_internal", + "pipeline_stages": "1", +} + +xls_dslx_verilog( + name = "axi_stream_remove_empty_internal_verilog", + codegen_args = axi_stream_remove_empty_internal_codegen_args, + dslx_top = "AxiStreamRemoveEmptyInternalInst", + library = ":axi_stream_remove_empty_dslx", + tags = ["manual"], + verilog_file = "axi_stream_remove_empty_internal.v", +) + +xls_benchmark_ir( + name = "axi_stream_remove_empty_internal_opt_ir_benchmark", + src = ":axi_stream_remove_empty_internal_verilog.opt.ir", + benchmark_ir_args = axi_stream_remove_empty_internal_codegen_args | { + "pipeline_stages": "10", + "top": "__axi_stream_remove_empty__AxiStreamRemoveEmptyInternalInst__AxiStreamRemoveEmptyInternal_0__32_4_6_32_32_next", + }, + tags = ["manual"], +) + +verilog_library( + name = "axi_stream_remove_empty_internal_verilog_lib", + srcs = [ + ":axi_stream_remove_empty_internal.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "axi_stream_remove_empty_internal_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "axi_stream_remove_empty_internal", + deps = [ + ":axi_stream_remove_empty_internal_verilog_lib", + ], +) + +benchmark_synth( + name = "axi_stream_remove_empty_internal_benchmark_synth", + synth_target = ":axi_stream_remove_empty_internal_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "axi_stream_remove_empty_internal_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":axi_stream_remove_empty_internal_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + axi_stream_remove_empty_codegen_args = common_codegen_args | { "module_name": "axi_stream_remove_empty", - "clock_period_ps": AXI_STREAM_REMOVE_EMPTY_CLOCK_PERIOD_PS, "pipeline_stages": "2", } @@ -182,16 +239,6 @@ xls_dslx_verilog( verilog_file = "axi_stream_remove_empty.v", ) -xls_benchmark_ir( - name = "axi_stream_remove_empty_opt_ir_benchmark", - src = ":axi_stream_remove_empty_verilog.opt.ir", - benchmark_ir_args = axi_stream_remove_empty_codegen_args | { - "pipeline_stages": "10", - "top": "__axi_stream_remove_empty__AxiStreamRemoveEmptyInst__AxiStreamRemoveEmpty_0__32_4_6_32_32_next", - }, - tags = ["manual"], -) - verilog_library( name = "axi_stream_remove_empty_verilog_lib", srcs = [ @@ -228,6 +275,66 @@ place_and_route( target_die_utilization_percentage = "10", ) +remove_empty_bytes_codegen_args = common_codegen_args | { + "module_name": "remove_empty_bytes", + "pipeline_stages": "2", +} + +xls_dslx_verilog( + name = "remove_empty_bytes_verilog", + codegen_args = remove_empty_bytes_codegen_args, + dslx_top = "RemoveEmptyBytesInst", + library = ":axi_stream_remove_empty_dslx", + tags = ["manual"], + verilog_file = "remove_empty_bytes.v", +) + +xls_benchmark_ir( + name = "remove_empty_bytes_opt_ir_benchmark", + src = ":remove_empty_bytes_verilog.opt.ir", + benchmark_ir_args = remove_empty_bytes_codegen_args | { + "top": "__axi_stream_remove_empty__RemoveEmptyBytesInst__RemoveEmptyBytes_0__32_4_6_32_9_32_next", + "pipeline_stages": "10", + }, + tags = ["manual"], +) + +verilog_library( + name = "remove_empty_bytes_verilog_lib", + srcs = [ + ":remove_empty_bytes.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "remove_empty_bytes_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "remove_empty_bytes", + deps = [ + ":remove_empty_bytes_verilog_lib", + ], +) + +benchmark_synth( + name = "remove_empty_bytes_benchmark_synth", + synth_target = ":remove_empty_bytes_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "remove_empty_bytes_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":remove_empty_bytes_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + xls_dslx_library( name = "axi_stream_downscaler_dslx", srcs = ["axi_stream_downscaler.x"], @@ -469,12 +576,12 @@ place_and_route( ) mem_reader_codegen_args = common_codegen_args | { + "clock_period_ps": MEM_READER_CLOCK_PERIOD_PS, "module_name": "mem_reader", "pipeline_stages": "4", "streaming_channel_data_suffix": "_data", "flop_inputs_kind": "skid", "flop_outputs_kind": "skid", - "clock_period_ps": MEM_READER_CLOCK_PERIOD_PS, "materialize_internal_fifos": "true", } @@ -525,12 +632,12 @@ place_and_route( ) mem_reader_adv_codegen_args = common_codegen_args | { + "clock_period_ps": MEM_READER_CLOCK_PERIOD_PS, "module_name": "mem_reader_adv", "pipeline_stages": "4", "streaming_channel_data_suffix": "_data", "flop_inputs_kind": "skid", "flop_outputs_kind": "skid", - "clock_period_ps": MEM_READER_CLOCK_PERIOD_PS, "materialize_internal_fifos": "true", } diff --git a/xls/modules/zstd/memory/axi_stream_remove_empty.x b/xls/modules/zstd/memory/axi_stream_remove_empty.x index 0b211852a5..40c8aa9208 100644 --- a/xls/modules/zstd/memory/axi_stream_remove_empty.x +++ b/xls/modules/zstd/memory/axi_stream_remove_empty.x @@ -29,34 +29,185 @@ struct AxiStreamRemoveEmptyState< dest: uN[DEST_W], } +pub struct ContinuousStream< + DATA_W: u32, + DEST_W: u32, + ID_W: u32, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, +> { + data: uN[DATA_W], + len: uN[DATA_W_LOG2], + id: uN[ID_W], + dest: uN[DEST_W], + last: u1 +} + +const INST_DATA_W = u32:32; +const INST_DATA_W_DIV8 = INST_DATA_W / u32:8; +const INST_DATA_W_LOG2 = std::clog2(INST_DATA_W + u32:1); +const INST_DEST_W = u32:32; +const INST_ID_W = u32:32; +const TEST_DATA_W = u32:32; +const TEST_DATA_W_DIV8 = TEST_DATA_W / u32:8; +const TEST_DATA_W_LOG2 = std::clog2(TEST_DATA_W + u32:1); +const TEST_DEST_W = u32:32; +const TEST_ID_W = u32:32; // Returns a tuple containing data and length, afer removing non-data // bytes from the in_data varaiable, using information from keep and str fields -fn remove_empty_bytes ( - in_data: uN[DATA_W], keep: uN[DATA_W_DIV8], str: uN[DATA_W_DIV8] -) -> (uN[DATA_W], uN[DATA_W_LOG2]) { - - const EXT_OFFSET_W = DATA_W_LOG2 + u32:3; - +pub proc RemoveEmptyBytes< + DATA_W: u32, DEST_W: u32, ID_W: u32, + DATA_W_DIV8: u32 = {DATA_W / u32:8}, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, + EXT_OFFSET_W: u32 = {(std::clog2(DATA_W + u32:1)) + u32:3}, +> { type Data = uN[DATA_W]; type Str = uN[DATA_W_DIV8]; - type Keep = uN[DATA_W_DIV8]; type Offset = uN[DATA_W_LOG2]; type OffsetExt = uN[EXT_OFFSET_W]; type Length = uN[DATA_W_LOG2]; - let (data, len, _) = for (i, (data, len, offset)): (u32, (Data, Length, Offset)) in range(u32:0, DATA_W_DIV8) { - if str[i +: u1] & keep[i +: u1] { - ( - data | (in_data & (Data:0xFF << (u32:8 * i))) >> (OffsetExt:8 * offset as OffsetExt), - len + Length:8, - offset, - ) - } else { - (data, len, offset + Offset:1) - } - }((Data:0, Length:0, Offset:0)); - (data, len) + type AxiStream = axi_st::AxiStream; + type StrobedStream = ContinuousStream; + + stream_r: chan in; + continuous_stream_s: chan out; + + config ( + stream_r: chan in, + continuous_stream_s: chan out, + ) { + (stream_r, continuous_stream_s) + } + + init { () } + + next (state: ()) { + let (tok, frame) = recv(join(), stream_r); + let (in_data, str) = (frame.data, frame.str); + + let (data, len, _) = unroll_for! (i, (data, len, offset)): (u32, (Data, Length, Offset)) in range(u32:0, DATA_W_DIV8) { + if str[i +: u1] { + ( + data | (in_data & (Data:0xFF << (u32:8 * i))) >> (OffsetExt:8 * offset as OffsetExt), + len + Length:8, + offset, + ) + } else { + (data, len, offset + Offset:1) + } + }((Data:0, Length:0, Offset:0)); + + let continuous_stream = StrobedStream { + data: data, + len: len, + id: frame.id, + dest: frame.dest, + last: frame.last, + }; + send(tok, continuous_stream_s, continuous_stream); + } +} + +pub proc RemoveEmptyBytesInst { + type AxiStream = axi_st::AxiStream; + type StrobedStream = ContinuousStream; + + config ( + stream_r: chan in, + continuous_stream_s: chan out, + ) { + spawn RemoveEmptyBytes( + stream_r, continuous_stream_s + ); + } + + init { () } + + next (state: ()) {} +} + +#[test_proc] +proc RemoveEmptyBytesTest { + type TestAxiStream = axi_st::AxiStream; + type TestStrobedStream = ContinuousStream; + terminator: chan out; + stream_s: chan out; + continuous_stream_r: chan in; + + config ( + terminator: chan out, + ) { + let (stream_s, stream_r) = chan("frame_data"); + let (continuous_stream_s, continuous_stream_r) = chan("bare_data"); + + spawn RemoveEmptyBytes( + stream_r, continuous_stream_s + ); + + (terminator, stream_s, continuous_stream_r) + } + + init { } + + next (state: ()) { + type Data = uN[TEST_DATA_W]; + type Str = uN[TEST_DATA_W_DIV8]; + type Id = uN[TEST_ID_W]; + type Dest = uN[TEST_DEST_W]; + type Length = uN[TEST_DATA_W_LOG2]; + + let tok = join(); + + let data = Data:0xDEADBEEF; + let input_data: TestAxiStream[16] = [ + TestAxiStream{data: data, str: Str:0b0000, keep: Str:0b0000, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0001, keep: Str:0b0001, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0010, keep: Str:0b0010, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0011, keep: Str:0b0011, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0100, keep: Str:0b0100, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0101, keep: Str:0b0101, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0110, keep: Str:0b0110, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b0111, keep: Str:0b0111, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1000, keep: Str:0b1000, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1001, keep: Str:0b1001, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1010, keep: Str:0b1010, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1011, keep: Str:0b1011, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1100, keep: Str:0b1100, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1101, keep: Str:0b1101, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1110, keep: Str:0b1110, id: Id:0, dest: Dest:0, last: false}, + TestAxiStream{data: data, str: Str:0b1111, keep: Str:0b1111, id: Id:0, dest: Dest:0, last: true} + ]; + let expected_output: TestStrobedStream[16] = [ + TestStrobedStream{data: Data:0x00, len: Length:0, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xEF, len: Length:8, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xBE, len: Length:8, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xBEEF, len: Length:16, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xAD, len: Length:8, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xADEF, len: Length:16, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xADBE, len: Length:16, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xADBEEF, len: Length:24, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDE, len: Length:8, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEEF, len: Length:16, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEBE, len: Length:16, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEBEEF, len: Length:24, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEAD, len: Length:16, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEADEF, len: Length:24, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEADBE, len: Length:24, id: Id:0, dest: Dest:0, last: false}, + TestStrobedStream{data: Data:0xDEADBEEF, len: Length:32, id: Id:0, dest: Dest:0, last: true} + ]; + + let tok = for (i, tok): (u32, token) in range(u32:0, u32:16) { + let tok = send(tok, stream_s, input_data[i]); + trace_fmt!("TestRemoveEmptyBytes: Sent #{} strobed packet: {:#x}", i + u32:1, input_data[i]); + let (tok, continuous_stream) = recv(tok, continuous_stream_r); + trace_fmt!("TestRemoveEmptyBytes: Received #{} continuous packet: {:#x}", i + u32:1, continuous_stream); + assert_eq(continuous_stream, expected_output[i]); + (tok) + } (tok); + + send(tok, terminator, true); + } } // Returns the number of bytes that should be soted in the state in case we @@ -71,22 +222,23 @@ fn get_overflow_len(len1: uN[LENGTH_W], len2: uN[LEN // Return the new mask for keep and str fields, calculated using new data length fn get_mask(len: uN[DATA_W_LOG2]) -> uN[DATA_W_DIV8] { - const MAX_LEN = DATA_W as uN[DATA_W_LOG2]; - const MASK = !uN[DATA_W_DIV8]:0; + let len_bytes = std::div_pow2(len, uN[DATA_W_LOG2]:8); + let mask = (uN[DATA_W_DIV8]:1 << len_bytes as uN[DATA_W_DIV8]) - uN[DATA_W_DIV8]:1; - let shift = std::div_pow2((MAX_LEN - len), uN[DATA_W_LOG2]:8); - MASK >> shift + mask } // A proc that removes empty bytes from the Axi Stream and provides aligned data // to other procs, allowing for a simpler implementation of the receiving side // of the design. -pub proc AxiStreamRemoveEmpty< +pub proc AxiStreamRemoveEmptyInternal< DATA_W: u32, DEST_W: u32, ID_W: u32, DATA_W_DIV8: u32 = {DATA_W / u32:8}, DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, > { type AxiStream = axi_st::AxiStream; + type StrobedStream = ContinuousStream; + type State = AxiStreamRemoveEmptyState; type Offset = uN[DATA_W_LOG2]; @@ -95,11 +247,11 @@ pub proc AxiStreamRemoveEmpty< type Str = uN[DATA_W_DIV8]; type Data = uN[DATA_W]; - stream_in_r: chan in; + stream_in_r: chan in; stream_out_s: chan out; config ( - stream_in_r: chan in, + stream_in_r: chan in, stream_out_s: chan out, ) { (stream_in_r, stream_out_s) @@ -112,17 +264,13 @@ pub proc AxiStreamRemoveEmpty< const MAX_MASK = !uN[DATA_W_DIV8]:0; let do_recv = !state.last; - let (tok, stream_in) = recv_if(join(), stream_in_r, !state.last, zero!()); - let (id, dest) = if !state.last { - (stream_in.id, stream_in.dest) + let (tok, stream_in) = recv_if(join(), stream_in_r, do_recv, zero!()); + let (id, dest, data, len) = if do_recv { + (stream_in.id, stream_in.dest, stream_in.data, stream_in.len) } else { - (state.id, state.dest) + (state.id, state.dest, Data:0, Length:0) }; - let (data, len) = remove_empty_bytes( - stream_in.data, stream_in.keep, stream_in.str - ); - let empty_input_bytes = MAX_LEN - len; let empty_state_bytes = MAX_LEN - state.len; @@ -131,12 +279,11 @@ pub proc AxiStreamRemoveEmpty< let combined_state_data = state.data | data << state.len; - let overflow_len = get_overflow_len(state.len, len); let sum_len = state.len + len; - let sum_mask = get_mask(sum_len); let (next_state, do_send, data) = if !state.last & exceeds_transfer { // flush and store + let overflow_len = get_overflow_len(state.len, len); ( State { data: data >> empty_state_bytes, @@ -156,6 +303,7 @@ pub proc AxiStreamRemoveEmpty< ) } else if state.last | stream_in.last | exact_transfer { // flush only + let sum_mask = get_mask(sum_len); ( zero!(), true, @@ -185,15 +333,55 @@ pub proc AxiStreamRemoveEmpty< } } +type InstAxiStream = axi_st::AxiStream; +type InstStrobedStream = ContinuousStream; -const INST_DATA_W = u32:32; -const INST_DEST_W = u32:32; -const INST_ID_W = u32:32; +proc AxiStreamRemoveEmptyInternalInst { + config ( + stream_in_r: chan in, + stream_out_s: chan out, + ) { + spawn AxiStreamRemoveEmptyInternal ( + stream_in_r, + stream_out_s + ); + } -const INST_DATA_W_DIV8 = INST_DATA_W / u32:8; -const INST_DATA_W_LOG2 = std::clog2(INST_DATA_W + u32:1); + init { } -type InstAxiStream = axi_st::AxiStream; + next (state:()) { } +} + +pub proc AxiStreamRemoveEmpty< + DATA_W: u32, DEST_W: u32, ID_W: u32, + DATA_W_DIV8: u32 = {DATA_W / u32:8}, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, +> { + type AxiStream = axi_st::AxiStream; + type StrobedStream = ContinuousStream; + + config ( + stream_in_r: chan in, + stream_out_s: chan out, + ) { + let (continuous_stream_s, continuous_stream_r) = chan("continuous_stream"); + + spawn RemoveEmptyBytes( + stream_in_r, + continuous_stream_s + ); + spawn AxiStreamRemoveEmptyInternal ( + continuous_stream_r, + stream_out_s + ); + + () + } + + init { () } + + next (state: ()) {} +} proc AxiStreamRemoveEmptyInst { config ( @@ -211,12 +399,6 @@ proc AxiStreamRemoveEmptyInst { next (state:()) { } } - -const TEST_DATA_W = u32:32; -const TEST_DEST_W = u32:32; -const TEST_ID_W = u32:32; -const TEST_DATA_W_DIV8 = TEST_DATA_W / u32:8; - type TestAxiStream = axi_st::AxiStream; #[test_proc] @@ -465,6 +647,283 @@ proc AxiStreamRemoveEmptyTest { id: Id:0, dest: Dest:0, }); + + // Test 7: Some bits set, last set in the last transfer. + + + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xf7697fb9, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xc735df5e, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x70d3da1f, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x0000001d, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x01eaf614, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x00001734, + str: Str:0b0011, + keep: Keep:0b0011, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xe935b870, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x00f149f5, + str: Str:0b0111, + keep: Keep:0b0111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xf073eed1, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xce97b5bd, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x950cddd9, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x08f0ebd4, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xABEB9592, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0xB16E2D5C, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x157CF9C6, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let tok = send(tok, stream_in_s, TestAxiStream { + data: Data:0x00000019, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xf7697fb9, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xc735df5e, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x70d3da1f, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x0000001d, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x01eaf614, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x00001734, + str: Str:0b0011, + keep: Keep:0b0011, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xe935b870, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x00f149f5, + str: Str:0b0111, + keep: Keep:0b0111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xf073eed1, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xce97b5bd, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x950cddd9, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x08f0ebd4, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xABEB9592, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0xB16E2D5C, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x157CF9C6, + str: Str:0b1111, + keep: Keep:0b1111, + last: u1:0, + id: Id:0, + dest: Dest:0, + }); + let (tok, stream_out) = recv(tok, stream_out_r); + assert_eq(stream_out, TestAxiStream { + data: Data:0x00000019, + str: Str:0b0001, + keep: Keep:0b0001, + last: u1:1, + id: Id:0, + dest: Dest:0, + }); + send(tok, terminator, true); } } From f3ce16487073c73702c276703841796526e9fdfc Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Wed, 13 Nov 2024 14:14:00 +0100 Subject: [PATCH 33/46] modules/zstd/memory/axi_writer: Assign parameterized max lane value Fix paramaterization of the proc Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/axi_writer.x | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xls/modules/zstd/memory/axi_writer.x b/xls/modules/zstd/memory/axi_writer.x index 2f62307731..21dd09baf4 100644 --- a/xls/modules/zstd/memory/axi_writer.x +++ b/xls/modules/zstd/memory/axi_writer.x @@ -124,6 +124,7 @@ pub proc AxiWriter< next(state: State) { const BYTES_IN_TRANSFER = DATA_W_DIV8 as Addr; const MAX_AXI_BURST_BYTES = Addr:256 * BYTES_IN_TRANSFER; + const MAX_LANE = std::unsigned_max_value(); let tok_0 = join(); @@ -280,7 +281,7 @@ pub proc AxiWriter< Fsm::AXI_WRITE_W => { let last = state.burst_counter == state.burst_end; let low_lane = state.req_low_lane; - let high_lane = if (last) { state.req_high_lane } else {Lane:3}; + let high_lane = if (last) { state.req_high_lane } else {MAX_LANE}; let mask = common::lane_mask(low_lane, high_lane); AxiW { From 639938043cd0d282e38fede6c4c122fe76f9c681 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Wed, 23 Oct 2024 12:06:28 +0200 Subject: [PATCH 34/46] modules/zstd/memory/mem_writer: Add support for not-full input data packets Add AxiStreamRemoveEmpty proc to the processing pipeline. It removes non-strobed bytes from the input AXI Stream frames and forms full frames (ensures that only the last input data packet won't be full). Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/BUILD | 4 +- xls/modules/zstd/memory/mem_writer.x | 86 ++++++++++- .../zstd/memory/mem_writer_cocotb_test.py | 139 +++++++++++++++--- xls/modules/zstd/memory/mem_writer_wrapper.v | 30 +++- 4 files changed, 230 insertions(+), 29 deletions(-) diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index bc5a40fcb3..910de815f4 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -905,6 +905,7 @@ xls_dslx_library( ":axi_dslx", ":axi_st_dslx", ":axi_stream_add_empty_dslx", + ":axi_stream_remove_empty_dslx", ":axi_writer_dslx", ":common_dslx", ], @@ -917,12 +918,11 @@ xls_dslx_test( mem_writer_codegen_args = common_codegen_args | { "module_name": "mem_writer", - "pipeline_stages": "2", + "pipeline_stages": "3", "streaming_channel_data_suffix": "_data", "multi_proc": "true", "flop_inputs_kind": "skid", "flop_outputs_kind": "skid", - "worst_case_throughput": "1", "materialize_internal_fifos": "true", } diff --git a/xls/modules/zstd/memory/mem_writer.x b/xls/modules/zstd/memory/mem_writer.x index 277c9910ef..8e53155b05 100644 --- a/xls/modules/zstd/memory/mem_writer.x +++ b/xls/modules/zstd/memory/mem_writer.x @@ -35,6 +35,7 @@ import xls.modules.zstd.memory.axi; import xls.modules.zstd.memory.axi_st; import xls.modules.zstd.memory.common; import xls.modules.zstd.memory.axi_writer; +import xls.modules.zstd.memory.axi_stream_remove_empty; import xls.modules.zstd.memory.axi_stream_add_empty; pub struct MemWriterReq { @@ -108,11 +109,15 @@ proc MemWriter< let (axi_writer_req_s, axi_writer_req_r) = chan("axi_writer_req"); let (padding_req_s, padding_req_r) = chan("padding_req"); let (axi_st_raw_s, axi_st_raw_r) = chan("axi_st_raw"); + let (axi_st_clean_s, axi_st_clean_r) = chan("axi_st_clean"); let (axi_st_padded_s, axi_st_padded_r) = chan("axi_st_padded"); + spawn axi_stream_remove_empty::AxiStreamRemoveEmpty< + DATA_W, DEST_W, ID_W + >(axi_st_raw_r, axi_st_clean_s); spawn axi_stream_add_empty::AxiStreamAddEmpty< DATA_W, DEST_W, ID_W, ADDR_W - >(padding_req_r, axi_st_raw_r, axi_st_padded_s); + >(padding_req_r, axi_st_clean_r, axi_st_padded_s); spawn axi_writer::AxiWriter< ADDR_W, DATA_W, DEST_W, ID_W >(axi_writer_req_r, resp_s, axi_aw_s, axi_w_s, axi_b_r, axi_st_padded_r); @@ -147,7 +152,7 @@ proc MemWriter< } }, Fsm::SEND_DATA => { - let next_req_len = state.req_len - sLength:4; + let next_req_len = state.req_len - data_in.length as sLength; State { fsm: if (next_req_len <= sLength:0) {Fsm::RECV_REQ} else {Fsm::SEND_DATA}, req_len: next_req_len, @@ -162,7 +167,7 @@ proc MemWriter< let raw_axi_st_frame = match(state.fsm) { Fsm::SEND_DATA => { - let next_req_len = state.req_len - sLength:4; + let next_req_len = next_state.req_len; let str_keep = ((Length:1 << data_in.length) - Length:1) as Strobe; AxiStream { data: data_in.data, @@ -631,6 +636,81 @@ proc MemWriterTest { let (tok, resp) = recv(tok, resp_r); assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + // Unligned 3 transfers + let tok = send(tok, req_in_s, TestReq { + addr: TestAddr:0x1f3, + length: TestLength:15 + }); + let tok = send(tok, data_in_s, TestData { + data: TestDataBits:0x11223344, + length: TestLength:4, + last: false, + }); + let tok = send(tok, data_in_s, TestData { + data: TestDataBits:0x00005566, + length: TestLength:2, + last: false, + }); + let tok = send(tok, data_in_s, TestData { + data: TestDataBits:0x778899aa, + length: TestLength:4, + last: false, + }); + let tok = send(tok, data_in_s, TestData { + data: TestDataBits:0x00bbccdd, + length: TestLength:3, + last: false, + }); + let tok = send(tok, data_in_s, TestData { + data: TestDataBits:0x0000eeff, + length: TestLength:2, + last: true, + }); + let (tok, aw) = recv(tok, axi_aw_r); + assert_eq(aw, TestAxiAW { + id: TestId:12, + addr: TestAddr:0x1f0, + size: TestAxiAxSize::MAX_4B_TRANSFER, + len: u8:4, + burst: TestAxiAxBurst::INCR, + }); + let (tok, w) = recv(tok, axi_w_r); + assert_eq(w, TestAxiW { + data: TestDataBits:0x44000000, + strb: TestStrobe:0x8, + last: false, + }); + let (tok, w) = recv(tok, axi_w_r); + assert_eq(w, TestAxiW { + data: TestDataBits:0x66112233, + strb: TestStrobe:0xF, + last: false, + }); + let (tok, w) = recv(tok, axi_w_r); + assert_eq(w, TestAxiW { + data: TestDataBits:0x8899aa55, + strb: TestStrobe:0xf, + last: false, + }); + let (tok, w) = recv(tok, axi_w_r); + assert_eq(w, TestAxiW { + data: TestDataBits:0xbbccdd77, + strb: TestStrobe:0xf, + last: false, + }); + let (tok, w) = recv(tok, axi_w_r); + assert_eq(w, TestAxiW { + data: TestDataBits:0x0000eeff, + strb: TestStrobe:0x3, + last: true, + }); + let tok = send(tok, axi_b_s, TestAxiB { + resp: TestAxiWriteResp::OKAY, + id: TestId:12, + }); + let (tok, resp) = recv(tok, resp_r); + assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + send(tok, terminator, true); } } diff --git a/xls/modules/zstd/memory/mem_writer_cocotb_test.py b/xls/modules/zstd/memory/mem_writer_cocotb_test.py index 6538938e57..54b9cee60f 100644 --- a/xls/modules/zstd/memory/mem_writer_cocotb_test.py +++ b/xls/modules/zstd/memory/mem_writer_cocotb_test.py @@ -16,6 +16,7 @@ import random import logging +from enum import Enum from pathlib import Path import cocotb @@ -60,9 +61,13 @@ class WriteReqStruct(XLSStruct): length: ADDR_WIDTH @xls_dataclass -class AxiWriterRespStruct(XLSStruct): +class MemWriterRespStruct(XLSStruct): status: 1 +class MemWriterRespStatus(Enum): + OKAY = 0 + ERROR = 1 + @xls_dataclass class WriteRequestStruct(XLSStruct): address: ADDR_WIDTH @@ -101,7 +106,7 @@ async def test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_ 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_write_resp = XLSChannelMonitor(dut, GENERIC_WRITE_RESP_CHANNEL, dut.clk, MemWriterRespStruct) 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) @@ -128,70 +133,77 @@ async def test_writer(dut, mem_size, write_req_input, data_in_input, write_resp_ 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=2000, 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") +@cocotb.test(timeout_time=5000, timeout_unit="ms") +async def ram_test_not_full_packets(dut): + mem_size = 2**ADDR_WIDTH + + (write_req_input, data_in_input, write_resp_expect, memory_verification, expected_memory, resp_cnt) = generate_padded_test_data_arbitrary(mem_size, test_cases_not_full_packets) + 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=5000, timeout_unit="ms") async def ram_test_random(dut): mem_size = 2**ADDR_WIDTH test_count = 200 @@ -260,7 +272,7 @@ def generate_test_data_random(test_count, mem_size): } memory_verification.append(memory_bundle) - write_resp_expect = [AxiWriterRespStruct(status=False)] * test_count + write_resp_expect = [MemWriterRespStruct(status=MemWriterRespStatus.OKAY.value)] * test_count return (write_req_input, data_in_input, write_resp_expect, memory_verification, memory, test_count) @@ -306,16 +318,13 @@ def generate_test_data_arbitrary(mem_size, test_cases): max_xfer_offset = mem_size - xfer_baseaddr - for i in range(test_count): - test_case = test_cases[i] - xfer_offset = test_case[0] + for xfer_offset, xfer_len in test_cases: 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( @@ -355,7 +364,78 @@ def generate_test_data_arbitrary(mem_size, test_cases): } memory_verification.append(memory_bundle) - write_resp_expect = [AxiWriterRespStruct(status=False)] * test_count + write_resp_expect = [MemWriterRespStruct(status=MemWriterRespStatus.OKAY.value)] * test_count + + return (write_req_input, data_in_input, write_resp_expect, memory_verification, memory, test_count) + +def generate_padded_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 xfer_offset, xfer_len in test_cases: + 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) + 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) + bytes_to_packetize = xfer_len + packetized_bytes = 0 + while(bytes_to_packetize): + packet_len = random.randint(1, 4) + + if (bytes_to_packetize < packet_len): + packet_len = bytes_to_packetize + + last = packet_len == bytes_to_packetize + + data_in = DataInStruct( + data = int.from_bytes(data_to_write[packetized_bytes:packetized_bytes+packet_len], byteorder='little'), + length = packet_len, + last = last + ) + data_in_input.append(data_in) + + bytes_to_packetize -= packet_len + packetized_bytes += packet_len + assert xfer_len == packetized_bytes + + + 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 = [MemWriterRespStruct(status=MemWriterRespStatus.OKAY.value)] * test_count return (write_req_input, data_in_input, write_resp_expect, memory_verification, memory, test_count) @@ -563,3 +643,26 @@ def generate_test_data_arbitrary(mem_size, test_cases): # End of address space - wrap around (0x0C03, 0x803), ] + +test_cases_not_full_packets = [ + # Aligned Address; Aligned Length + (0x0000, 0x20), + # Aligned Address; Unaligned Length + (0x100, 0x21), + (0x200, 0x22), + (0x300, 0x23), + # Unaligned Address; Aligned Length + (0x401, 0x20), + (0x502, 0x20), + (0x603, 0x20), + # Unaligned Address; Unaligned Length + (0x701, 0x21), + (0x802, 0x22), + (0x903, 0x23), + (0xA01, 0x22), + (0xB02, 0x22), + (0xC03, 0x22), + (0xD01, 0x23), + (0xE02, 0x23), + (0xF03, 0x23), +] diff --git a/xls/modules/zstd/memory/mem_writer_wrapper.v b/xls/modules/zstd/memory/mem_writer_wrapper.v index a584984443..2392fb7b50 100644 --- a/xls/modules/zstd/memory/mem_writer_wrapper.v +++ b/xls/modules/zstd/memory/mem_writer_wrapper.v @@ -104,6 +104,15 @@ module mem_writer_wrapper ( wire [ 0:0] axi_stream_raw_tvalid; wire [ 0:0] axi_stream_raw_tready; + wire [31:0] axi_stream_clean_tdata; + wire [ 3:0] axi_stream_clean_tstr; + wire [ 3:0] axi_stream_clean_tkeep; + wire [ 0:0] axi_stream_clean_tlast; + wire [ 3:0] axi_stream_clean_tid; + wire [ 3:0] axi_stream_clean_tdest; + wire [ 0:0] axi_stream_clean_tvalid; + wire [ 0:0] axi_stream_clean_tready; + wire [31:0] axi_stream_padded_tdata; wire [ 3:0] axi_stream_padded_tstr; wire [ 3:0] axi_stream_padded_tkeep; @@ -124,20 +133,29 @@ module mem_writer_wrapper ( 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; + axi_stream_raw_tdest, + axi_stream_raw_tlast} = 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_clean_tdata, + axi_stream_clean_tstr, + axi_stream_clean_tkeep, + axi_stream_clean_tid, + axi_stream_clean_tdest, + axi_stream_clean_tlast} = mem_writer.__xls_modules_zstd_memory_axi_stream_remove_empty__MemWriterInst__MemWriter_0__AxiStreamRemoveEmpty_0__32_4_6_4_4_next_inst2.mem_writer__axi_st_clean_data; + assign axi_stream_clean_tvalid = mem_writer.__xls_modules_zstd_memory_axi_stream_remove_empty__MemWriterInst__MemWriter_0__AxiStreamRemoveEmpty_0__32_4_6_4_4_next_inst2.mem_writer__axi_st_clean_vld; + assign axi_stream_clean_tready = mem_writer.__xls_modules_zstd_memory_axi_stream_remove_empty__MemWriterInst__MemWriter_0__AxiStreamRemoveEmpty_0__32_4_6_4_4_next_inst2.mem_writer__axi_st_clean_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; + axi_stream_padded_tdest, + axi_stream_padded_tlast} = mem_writer.__xls_modules_zstd_memory_axi_writer__MemWriterInst__MemWriter_0__AxiWriter_0__16_32_4_4_4_2_next_inst3.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_inst3.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_inst3.mem_writer__axi_st_padded_rdy; mem_writer mem_writer ( .clk(clk), From 55531394ab4829e407779587981b517996198b8e Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Mon, 4 Nov 2024 16:20:12 +0100 Subject: [PATCH 35/46] modules/zstd/memory/mem_writer: Add MemWriterInternal proc * Extract control logic to MemWriterInternal proc * Create alias for the MemWriter response type Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/BUILD | 63 +++++++- xls/modules/zstd/memory/mem_writer.x | 153 +++++++++++++------ xls/modules/zstd/memory/mem_writer_wrapper.v | 30 ++-- 3 files changed, 183 insertions(+), 63 deletions(-) diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 910de815f4..8b4bfd516f 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -916,11 +916,70 @@ xls_dslx_test( library = ":mem_writer_dslx", ) +mem_writer_internal_codegen_args = common_codegen_args | { + "module_name": "mem_writer_internal", + "pipeline_stages": "2", +} + +xls_dslx_verilog( + name = "mem_writer_internal_verilog", + codegen_args = mem_writer_internal_codegen_args, + dslx_top = "MemWriterInternalInst", + library = ":mem_writer_dslx", + tags = ["manual"], + verilog_file = "mem_writer_internal.v", +) + +xls_benchmark_ir( + name = "mem_writer_internal_opt_ir_benchmark", + src = ":mem_writer_internal_verilog.opt.ir", + benchmark_ir_args = common_codegen_args | { + "pipeline_stages": "10", + "top": "__mem_writer__MemWriterInternalInst__MemWriterInternal_0__16_32_4_4_4_2_next", + }, + tags = ["manual"], +) + +verilog_library( + name = "mem_writer_internal_verilog_lib", + srcs = [ + ":mem_writer_internal.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "mem_writer_internal_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "mem_writer_internal", + deps = [ + ":mem_writer_internal_verilog_lib", + ], +) + +benchmark_synth( + name = "mem_writer_internal_benchmark_synth", + synth_target = ":mem_writer_internal_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "mem_writer_internal_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":mem_writer_internal_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + mem_writer_codegen_args = common_codegen_args | { "module_name": "mem_writer", - "pipeline_stages": "3", + "pipeline_stages": "10", "streaming_channel_data_suffix": "_data", - "multi_proc": "true", "flop_inputs_kind": "skid", "flop_outputs_kind": "skid", "materialize_internal_fifos": "true", diff --git a/xls/modules/zstd/memory/mem_writer.x b/xls/modules/zstd/memory/mem_writer.x index 8e53155b05..f49d147785 100644 --- a/xls/modules/zstd/memory/mem_writer.x +++ b/xls/modules/zstd/memory/mem_writer.x @@ -43,6 +43,9 @@ pub struct MemWriterReq { length: uN[ADDR_W], } +pub type MemWriterResp = axi_writer::AxiWriterResp; +pub type MemWriterRespStatus = axi_writer::AxiWriterRespStatus; + pub struct MemWriterDataPacket { data: uN[DATA_W], length: uN[ADDR_W], // Expressed in bytes @@ -68,20 +71,15 @@ struct MemWriterState< axi_writer_req: axi_writer::AxiWriterRequest, } -proc MemWriter< +proc MemWriterInternal< ADDR_W: u32, DATA_W: u32, DEST_W: u32, ID_W: u32, WRITER_ID: u32, - DATA_W_DIV8: u32 = {DATA_W / u32:8}, - DATA_W_LOG2: u32 = {std::clog2(DATA_W / u32:8)} + DATA_W_DIV8: u32 = {DATA_W / u32:8} > { type Req = MemWriterReq; type Data = MemWriterDataPacket; type AxiWriterReq = axi_writer::AxiWriterRequest; - type AxiWriterResp = axi_writer::AxiWriterResp; type PaddingReq = axi_writer::AxiWriterRequest; type AxiStream = axi_st::AxiStream; - type AxiAW = axi::AxiAw; - type AxiW = axi::AxiW; - type AxiB = axi::AxiB; type State = MemWriterState; type Fsm = MemWriterFsm; @@ -96,33 +94,16 @@ proc MemWriter< axi_writer_req_s: chan out; padding_req_s: chan out; axi_st_raw_s: chan out; - resp_s: chan out; config( req_in_r: chan in, data_in_r: chan in, - axi_aw_s: chan out, - axi_w_s: chan out, - axi_b_r: chan in, - resp_s: chan out, + axi_writer_req_s: chan out, + padding_req_s: chan out, + axi_st_raw_s: chan out, ) { - let (axi_writer_req_s, axi_writer_req_r) = chan("axi_writer_req"); - let (padding_req_s, padding_req_r) = chan("padding_req"); - let (axi_st_raw_s, axi_st_raw_r) = chan("axi_st_raw"); - let (axi_st_clean_s, axi_st_clean_r) = chan("axi_st_clean"); - let (axi_st_padded_s, axi_st_padded_r) = chan("axi_st_padded"); - - spawn axi_stream_remove_empty::AxiStreamRemoveEmpty< - DATA_W, DEST_W, ID_W - >(axi_st_raw_r, axi_st_clean_s); - spawn axi_stream_add_empty::AxiStreamAddEmpty< - DATA_W, DEST_W, ID_W, ADDR_W - >(padding_req_r, axi_st_clean_r, axi_st_padded_s); - spawn axi_writer::AxiWriter< - ADDR_W, DATA_W, DEST_W, ID_W - >(axi_writer_req_r, resp_s, axi_aw_s, axi_w_s, axi_b_r, axi_st_padded_r); - (req_in_r, data_in_r, axi_writer_req_s, padding_req_s, axi_st_raw_s, resp_s) + (req_in_r, data_in_r, axi_writer_req_s, padding_req_s, axi_st_raw_s) } init { zero!() } @@ -194,9 +175,90 @@ const INST_DATA_W = u32:32; const INST_DATA_W_DIV8 = INST_DATA_W / u32:8; const INST_DEST_W = INST_DATA_W / u32:8; const INST_ID_W = INST_DATA_W / u32:8; -const INST_DATA_W_LOG2 = u32:6; const INST_WRITER_ID = u32:2; +proc MemWriterInternalInst { + type Req = MemWriterReq; + type Data = MemWriterDataPacket; + type AxiWriterReq = axi_writer::AxiWriterRequest; + type PaddingReq = axi_writer::AxiWriterRequest; + type AxiStream = axi_st::AxiStream; + + config( + req_in_r: chan in, + data_in_r: chan in, + axi_writer_req_s: chan out, + padding_req_s: chan out, + axi_st_raw_s: chan out, + ) { + + spawn MemWriterInternal< + INST_ADDR_W, INST_DATA_W, INST_DEST_W, INST_ID_W, INST_WRITER_ID + >(req_in_r, data_in_r, axi_writer_req_s, padding_req_s, axi_st_raw_s); + () + } + + init {} + + next(state: ()) {} +} + +pub proc MemWriter< + ADDR_W: u32, DATA_W: u32, DEST_W: u32, ID_W: u32, WRITER_ID: u32, + DATA_W_DIV8: u32 = {DATA_W / u32:8} +> { + type Req = MemWriterReq; + type Data = MemWriterDataPacket; + type AxiWriterReq = axi_writer::AxiWriterRequest; + type PaddingReq = axi_writer::AxiWriterRequest; + type AxiStream = axi_st::AxiStream; + type AxiAW = axi::AxiAw; + type AxiW = axi::AxiW; + type AxiB = axi::AxiB; + type State = MemWriterState; + type Fsm = MemWriterFsm; + + type Length = uN[ADDR_W]; + type sLength = sN[ADDR_W]; + type Strobe = uN[DATA_W_DIV8]; + type Id = uN[ID_W]; + type Dest = uN[DEST_W]; + + config( + req_in_r: chan in, + data_in_r: chan in, + axi_aw_s: chan out, + axi_w_s: chan out, + axi_b_r: chan in, + resp_s: chan out, + ) { + let (axi_writer_req_s, axi_writer_req_r) = chan("axi_writer_req"); + let (padding_req_s, padding_req_r) = chan("padding_req"); + let (axi_st_raw_s, axi_st_raw_r) = chan("axi_st_raw"); + let (axi_st_clean_s, axi_st_clean_r) = chan("axi_st_clean"); + let (axi_st_padded_s, axi_st_padded_r) = chan("axi_st_padded"); + + spawn MemWriterInternal< + ADDR_W, DATA_W, DEST_W, ID_W, WRITER_ID + >(req_in_r, data_in_r, axi_writer_req_s, padding_req_s, axi_st_raw_s); + spawn axi_stream_remove_empty::AxiStreamRemoveEmpty< + DATA_W, DEST_W, ID_W + >(axi_st_raw_r, axi_st_clean_s); + spawn axi_stream_add_empty::AxiStreamAddEmpty< + DATA_W, DEST_W, ID_W, ADDR_W + >(padding_req_r, axi_st_clean_r, axi_st_padded_s); + spawn axi_writer::AxiWriter< + ADDR_W, DATA_W, DEST_W, ID_W + >(axi_writer_req_r, resp_s, axi_aw_s, axi_w_s, axi_b_r, axi_st_padded_r); + + () + } + + init {} + + next(state: ()) {} +} + proc MemWriterInst { type InstReq = MemWriterReq; type InstData = MemWriterDataPacket; @@ -204,7 +266,7 @@ proc MemWriterInst { type InstAxiAW = axi::AxiAw; type InstAxiW = axi::AxiW; type InstAxiB = axi::AxiB; - type InstAxiWriterResp = axi_writer::AxiWriterResp; + type InstMemWriterResp = MemWriterResp; config( req_in_r: chan in, @@ -212,7 +274,7 @@ proc MemWriterInst { axi_aw_s: chan out, axi_w_s: chan out, axi_b_r: chan in, - resp_s: chan out + resp_s: chan out ) { spawn MemWriter< INST_ADDR_W, INST_DATA_W, INST_DEST_W, INST_ID_W, INST_WRITER_ID @@ -230,13 +292,12 @@ const TEST_DATA_W = u32:32; const TEST_DATA_W_DIV8 = TEST_DATA_W / u32:8; const TEST_DEST_W = TEST_DATA_W / u32:8; const TEST_ID_W = TEST_DATA_W / u32:8; -const TEST_DATA_W_LOG2 = u32:6; const TEST_WRITER_ID = u32:2; type TestReq = MemWriterReq; type TestData = MemWriterDataPacket; -type TestAxiWriterResp = axi_writer::AxiWriterResp; -type TestAxiWriterRespStatus = axi_writer::AxiWriterRespStatus; +type TestMemWriterResp = MemWriterResp; +type TestMemWriterRespStatus = MemWriterRespStatus; type TestAxiStream = axi_st::AxiStream; type TestAxiAW = axi::AxiAw; type TestAxiW = axi::AxiW; @@ -260,7 +321,7 @@ proc MemWriterTest { axi_aw_r: chan in; axi_w_r: chan in; axi_b_s: chan out; - resp_r: chan in; + resp_r: chan in; config( terminator: chan out, @@ -270,7 +331,7 @@ proc MemWriterTest { let (axi_aw_s, axi_aw_r) = chan("axi_aw"); let (axi_w_s, axi_w_r) = chan("axi_w"); let (axi_b_s, axi_b_r) = chan("axi_b"); - let (resp_s, resp_r) = chan("resp"); + let (resp_s, resp_r) = chan("resp"); spawn MemWriter< TEST_ADDR_W, TEST_DATA_W, TEST_DEST_W, TEST_ID_W, TEST_WRITER_ID >(req_in_r, data_in_r, axi_aw_s, axi_w_s, axi_b_r, resp_s); @@ -311,7 +372,7 @@ proc MemWriterTest { id: TestId:1, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unaligned single transfer let tok = send(tok, req_in_s, TestReq { @@ -342,7 +403,7 @@ proc MemWriterTest { id: TestId:2, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unaligned single transfer let tok = send(tok, req_in_s, TestReq { @@ -373,7 +434,7 @@ proc MemWriterTest { id: TestId:3, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unaligned single transfer let tok = send(tok, req_in_s, TestReq { @@ -404,7 +465,7 @@ proc MemWriterTest { id: TestId:4, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unaligned single transfer let tok = send(tok, req_in_s, TestReq { @@ -435,7 +496,7 @@ proc MemWriterTest { id: TestId:5, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unaligned 2 transfers let tok = send(tok, req_in_s, TestReq { @@ -472,7 +533,7 @@ proc MemWriterTest { id: TestId:6, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unligned 3 transfers let tok = send(tok, req_in_s, TestReq { @@ -520,7 +581,7 @@ proc MemWriterTest { id: TestId:7, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Crossing AXI 4kB boundary, aligned 2 burst transfers let tok = send(tok, req_in_s, TestReq { @@ -574,7 +635,7 @@ proc MemWriterTest { id: TestId:9, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Crossing AXI 4kB boundary, unaligned 2 burst transfers let tok = send(tok, req_in_s, TestReq { @@ -634,7 +695,7 @@ proc MemWriterTest { id: TestId:11, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); // Unligned 3 transfers let tok = send(tok, req_in_s, TestReq { @@ -709,7 +770,7 @@ proc MemWriterTest { id: TestId:12, }); let (tok, resp) = recv(tok, resp_r); - assert_eq(resp, TestAxiWriterResp{status: TestAxiWriterRespStatus::OKAY}); + assert_eq(resp, TestMemWriterResp{status: TestMemWriterRespStatus::OKAY}); send(tok, terminator, true); } diff --git a/xls/modules/zstd/memory/mem_writer_wrapper.v b/xls/modules/zstd/memory/mem_writer_wrapper.v index 2392fb7b50..c7513af58a 100644 --- a/xls/modules/zstd/memory/mem_writer_wrapper.v +++ b/xls/modules/zstd/memory/mem_writer_wrapper.v @@ -122,40 +122,40 @@ module mem_writer_wrapper ( 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 {axi_writer_write_req_address, axi_writer_write_req_length} = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__axi_writer_req_data; + assign axi_writer_write_req_valid = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__axi_writer_req_vld; + assign axi_writer_write_req_ready = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_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 {padding_write_req_address, padding_write_req_length} = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__padding_req_data; + assign padding_write_req_valid = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__padding_req_vld; + assign padding_write_req_ready = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_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_tid, axi_stream_raw_tdest, - axi_stream_raw_tlast} = 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; + axi_stream_raw_tlast} = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__axi_st_raw_data; + assign axi_stream_raw_tvalid = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__axi_st_raw_vld; + assign axi_stream_raw_tready = mem_writer.__mem_writer__MemWriterInst__MemWriter_0__MemWriterInternal_0__16_32_4_4_4_2_next_inst0.mem_writer__axi_st_raw_rdy; assign { axi_stream_clean_tdata, axi_stream_clean_tstr, axi_stream_clean_tkeep, axi_stream_clean_tid, axi_stream_clean_tdest, - axi_stream_clean_tlast} = mem_writer.__xls_modules_zstd_memory_axi_stream_remove_empty__MemWriterInst__MemWriter_0__AxiStreamRemoveEmpty_0__32_4_6_4_4_next_inst2.mem_writer__axi_st_clean_data; - assign axi_stream_clean_tvalid = mem_writer.__xls_modules_zstd_memory_axi_stream_remove_empty__MemWriterInst__MemWriter_0__AxiStreamRemoveEmpty_0__32_4_6_4_4_next_inst2.mem_writer__axi_st_clean_vld; - assign axi_stream_clean_tready = mem_writer.__xls_modules_zstd_memory_axi_stream_remove_empty__MemWriterInst__MemWriter_0__AxiStreamRemoveEmpty_0__32_4_6_4_4_next_inst2.mem_writer__axi_st_clean_rdy; + axi_stream_clean_tlast} = mem_writer.__xls_modules_zstd_memory_axi_stream_add_empty__MemWriterInst__MemWriter_0__AxiStreamAddEmpty_0__16_32_4_2_4_4_next_inst1.mem_writer__axi_st_clean_data; + assign axi_stream_clean_tvalid = mem_writer.__xls_modules_zstd_memory_axi_stream_add_empty__MemWriterInst__MemWriter_0__AxiStreamAddEmpty_0__16_32_4_2_4_4_next_inst1.mem_writer__axi_st_clean_vld; + assign axi_stream_clean_tready = mem_writer.__xls_modules_zstd_memory_axi_stream_add_empty__MemWriterInst__MemWriter_0__AxiStreamAddEmpty_0__16_32_4_2_4_4_next_inst1.mem_writer__axi_st_clean_rdy; assign { axi_stream_padded_tdata, axi_stream_padded_tstr, axi_stream_padded_tkeep, axi_stream_padded_tid, axi_stream_padded_tdest, - axi_stream_padded_tlast} = mem_writer.__xls_modules_zstd_memory_axi_writer__MemWriterInst__MemWriter_0__AxiWriter_0__16_32_4_4_4_2_next_inst3.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_inst3.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_inst3.mem_writer__axi_st_padded_rdy; + axi_stream_padded_tlast} = mem_writer.__xls_modules_zstd_memory_axi_writer__MemWriterInst__MemWriter_0__AxiWriter_0__16_32_4_4_4_2_next_inst4.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_inst4.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_inst4.mem_writer__axi_st_padded_rdy; mem_writer mem_writer ( .clk(clk), From 115ae787f352a60b564d91b214acd98c9581c996 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Thu, 7 Nov 2024 16:39:00 +0100 Subject: [PATCH 36/46] modules/zstd/zstd_dec: Write decoded data to the memory SequenceExecutor: * Add output channel in the format compliant with MemWriter input data channel type ZstdDecoder: * Add MemWriter proc: * Write request formed based on the address of the OutputBuffer CSR and FrameContentSize field from the Frame Header * Data to write is sent out to the proc by the SequenceExecutor * Transition to the FINISH state (and triggers notify channel) only after receiving the response from the MemWriter * DSLX tests: * Receive decoded data sent out on the AXI interface by the MemWriter proc * Mock the output memory buffer as a DSLX array * Cocotb tests: * Move third-party verilog modules (AXI Interconnect) to external directory * Replace AXI Interconnect with AXI Crossbar that handles simultaneous AXI Read and Write transactions * Add reference memory and fill it with expected data for comparison against testbench memory at the end of the decoding Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 17 +- xls/modules/zstd/axi_interconnect.v | 988 ------------------ xls/modules/zstd/external/BUILD | 33 + xls/modules/zstd/{ => external}/arbiter.v | 0 xls/modules/zstd/external/axi_crossbar.v | 391 +++++++ xls/modules/zstd/external/axi_crossbar_addr.v | 418 ++++++++ xls/modules/zstd/external/axi_crossbar_rd.v | 569 ++++++++++ xls/modules/zstd/external/axi_crossbar_wr.v | 678 ++++++++++++ .../axi_crossbar_wrapper.v} | 200 +++- xls/modules/zstd/external/axi_register_rd.v | 530 ++++++++++ xls/modules/zstd/external/axi_register_wr.v | 691 ++++++++++++ .../zstd/{ => external}/priority_encoder.v | 0 xls/modules/zstd/sequence_executor.x | 75 +- xls/modules/zstd/zstd_dec.x | 89 +- xls/modules/zstd/zstd_dec_cocotb_test.py | 51 +- xls/modules/zstd/zstd_dec_test.x | 129 ++- xls/modules/zstd/zstd_dec_wrapper.v | 162 ++- 17 files changed, 3884 insertions(+), 1137 deletions(-) delete mode 100644 xls/modules/zstd/axi_interconnect.v create mode 100644 xls/modules/zstd/external/BUILD rename xls/modules/zstd/{ => external}/arbiter.v (100%) create mode 100644 xls/modules/zstd/external/axi_crossbar.v create mode 100644 xls/modules/zstd/external/axi_crossbar_addr.v create mode 100644 xls/modules/zstd/external/axi_crossbar_rd.v create mode 100644 xls/modules/zstd/external/axi_crossbar_wr.v rename xls/modules/zstd/{axi_interconnect_wrapper.v => external/axi_crossbar_wrapper.v} (70%) create mode 100644 xls/modules/zstd/external/axi_register_rd.v create mode 100644 xls/modules/zstd/external/axi_register_wr.v rename xls/modules/zstd/{ => external}/priority_encoder.v (100%) diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 5a1096c666..2df52f4b18 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -610,6 +610,7 @@ xls_dslx_library( ":common_dslx", ":ram_printer_dslx", "//xls/examples:ram_dslx", + "//xls/modules/zstd/memory:mem_writer_dslx", ], ) @@ -655,7 +656,7 @@ xls_dslx_verilog( library = ":sequence_executor_dslx", opt_ir_args = { "inline_procs": "true", - "top": "__sequence_executor__SequenceExecutorZstd__SequenceExecutor_0__64_0_0_0_13_8192_65536_next", + "top": "__sequence_executor__SequenceExecutorZstd__SequenceExecutor_0__16_64_64_0_0_0_13_8192_65536_next", }, tags = ["manual"], verilog_file = "sequence_executor.v", @@ -981,6 +982,7 @@ zstd_dec_deps = [ ":sequence_executor_dslx", "//xls/examples:ram_dslx", "//xls/modules/zstd/memory:mem_reader_dslx", + "//xls/modules/zstd/memory:mem_writer_dslx", "//xls/modules/zstd/memory:axi_ram_dslx", ] @@ -1121,13 +1123,18 @@ py_test( name = "zstd_dec_cocotb_test", srcs = ["zstd_dec_cocotb_test.py"], data = [ - ":arbiter.v", - ":axi_interconnect.v", - ":axi_interconnect_wrapper.v", - ":priority_encoder.v", ":xls_fifo_wrapper.v", ":zstd_dec.v", ":zstd_dec_wrapper.v", + "//xls/modules/zstd/external:arbiter.v", + "//xls/modules/zstd/external:axi_crossbar.v", + "//xls/modules/zstd/external:axi_crossbar_addr.v", + "//xls/modules/zstd/external:axi_crossbar_rd.v", + "//xls/modules/zstd/external:axi_crossbar_wr.v", + "//xls/modules/zstd/external:axi_crossbar_wrapper.v", + "//xls/modules/zstd/external:axi_register_rd.v", + "//xls/modules/zstd/external:axi_register_wr.v", + "//xls/modules/zstd/external:priority_encoder.v", "@com_icarus_iverilog//:iverilog", "@com_icarus_iverilog//:vvp", ], diff --git a/xls/modules/zstd/axi_interconnect.v b/xls/modules/zstd/axi_interconnect.v deleted file mode 100644 index 14256ad7de..0000000000 --- a/xls/modules/zstd/axi_interconnect.v +++ /dev/null @@ -1,988 +0,0 @@ -/* - -Copyright (c) 2018 Alex Forencich - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -*/ - -// Language: Verilog 2001 - -`resetall -`timescale 1ns / 1ps -`default_nettype none - -/* - * AXI4 interconnect - */ -module axi_interconnect # -( - // Number of AXI inputs (slave interfaces) - parameter S_COUNT = 4, - // Number of AXI outputs (master interfaces) - parameter M_COUNT = 4, - // Width of data bus in bits - parameter DATA_WIDTH = 32, - // Width of address bus in bits - parameter ADDR_WIDTH = 32, - // Width of wstrb (width of data bus in words) - parameter STRB_WIDTH = (DATA_WIDTH/8), - // Width of ID signal - parameter ID_WIDTH = 8, - // Propagate awuser signal - parameter AWUSER_ENABLE = 0, - // Width of awuser signal - parameter AWUSER_WIDTH = 1, - // Propagate wuser signal - parameter WUSER_ENABLE = 0, - // Width of wuser signal - parameter WUSER_WIDTH = 1, - // Propagate buser signal - parameter BUSER_ENABLE = 0, - // Width of buser signal - parameter BUSER_WIDTH = 1, - // Propagate aruser signal - parameter ARUSER_ENABLE = 0, - // Width of aruser signal - parameter ARUSER_WIDTH = 1, - // Propagate ruser signal - parameter RUSER_ENABLE = 0, - // Width of ruser signal - parameter RUSER_WIDTH = 1, - // Propagate ID field - parameter FORWARD_ID = 0, - // Number of regions per master interface - parameter M_REGIONS = 1, - // Master interface base addresses - // M_COUNT concatenated fields of M_REGIONS concatenated fields of ADDR_WIDTH bits - // set to zero for default addressing based on M_ADDR_WIDTH - parameter M_BASE_ADDR = 0, - // Master interface address widths - // M_COUNT concatenated fields of M_REGIONS concatenated fields of 32 bits - parameter M_ADDR_WIDTH = {M_COUNT{{M_REGIONS{32'd24}}}}, - // Read connections between interfaces - // M_COUNT concatenated fields of S_COUNT bits - parameter M_CONNECT_READ = {M_COUNT{{S_COUNT{1'b1}}}}, - // Write connections between interfaces - // M_COUNT concatenated fields of S_COUNT bits - parameter M_CONNECT_WRITE = {M_COUNT{{S_COUNT{1'b1}}}}, - // Secure master (fail operations based on awprot/arprot) - // M_COUNT bits - parameter M_SECURE = {M_COUNT{1'b0}} -) -( - input wire clk, - input wire rst, - - /* - * AXI slave interfaces - */ - input wire [S_COUNT*ID_WIDTH-1:0] s_axi_awid, - input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_awaddr, - input wire [S_COUNT*8-1:0] s_axi_awlen, - input wire [S_COUNT*3-1:0] s_axi_awsize, - input wire [S_COUNT*2-1:0] s_axi_awburst, - input wire [S_COUNT-1:0] s_axi_awlock, - input wire [S_COUNT*4-1:0] s_axi_awcache, - input wire [S_COUNT*3-1:0] s_axi_awprot, - input wire [S_COUNT*4-1:0] s_axi_awqos, - input wire [S_COUNT*AWUSER_WIDTH-1:0] s_axi_awuser, - input wire [S_COUNT-1:0] s_axi_awvalid, - output wire [S_COUNT-1:0] s_axi_awready, - input wire [S_COUNT*DATA_WIDTH-1:0] s_axi_wdata, - input wire [S_COUNT*STRB_WIDTH-1:0] s_axi_wstrb, - input wire [S_COUNT-1:0] s_axi_wlast, - input wire [S_COUNT*WUSER_WIDTH-1:0] s_axi_wuser, - input wire [S_COUNT-1:0] s_axi_wvalid, - output wire [S_COUNT-1:0] s_axi_wready, - output wire [S_COUNT*ID_WIDTH-1:0] s_axi_bid, - output wire [S_COUNT*2-1:0] s_axi_bresp, - output wire [S_COUNT*BUSER_WIDTH-1:0] s_axi_buser, - output wire [S_COUNT-1:0] s_axi_bvalid, - input wire [S_COUNT-1:0] s_axi_bready, - input wire [S_COUNT*ID_WIDTH-1:0] s_axi_arid, - input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_araddr, - input wire [S_COUNT*8-1:0] s_axi_arlen, - input wire [S_COUNT*3-1:0] s_axi_arsize, - input wire [S_COUNT*2-1:0] s_axi_arburst, - input wire [S_COUNT-1:0] s_axi_arlock, - input wire [S_COUNT*4-1:0] s_axi_arcache, - input wire [S_COUNT*3-1:0] s_axi_arprot, - input wire [S_COUNT*4-1:0] s_axi_arqos, - input wire [S_COUNT*ARUSER_WIDTH-1:0] s_axi_aruser, - input wire [S_COUNT-1:0] s_axi_arvalid, - output wire [S_COUNT-1:0] s_axi_arready, - output wire [S_COUNT*ID_WIDTH-1:0] s_axi_rid, - output wire [S_COUNT*DATA_WIDTH-1:0] s_axi_rdata, - output wire [S_COUNT*2-1:0] s_axi_rresp, - output wire [S_COUNT-1:0] s_axi_rlast, - output wire [S_COUNT*RUSER_WIDTH-1:0] s_axi_ruser, - output wire [S_COUNT-1:0] s_axi_rvalid, - input wire [S_COUNT-1:0] s_axi_rready, - - /* - * AXI master interfaces - */ - output wire [M_COUNT*ID_WIDTH-1:0] m_axi_awid, - output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_awaddr, - output wire [M_COUNT*8-1:0] m_axi_awlen, - output wire [M_COUNT*3-1:0] m_axi_awsize, - output wire [M_COUNT*2-1:0] m_axi_awburst, - output wire [M_COUNT-1:0] m_axi_awlock, - output wire [M_COUNT*4-1:0] m_axi_awcache, - output wire [M_COUNT*3-1:0] m_axi_awprot, - output wire [M_COUNT*4-1:0] m_axi_awqos, - output wire [M_COUNT*4-1:0] m_axi_awregion, - output wire [M_COUNT*AWUSER_WIDTH-1:0] m_axi_awuser, - output wire [M_COUNT-1:0] m_axi_awvalid, - input wire [M_COUNT-1:0] m_axi_awready, - output wire [M_COUNT*DATA_WIDTH-1:0] m_axi_wdata, - output wire [M_COUNT*STRB_WIDTH-1:0] m_axi_wstrb, - output wire [M_COUNT-1:0] m_axi_wlast, - output wire [M_COUNT*WUSER_WIDTH-1:0] m_axi_wuser, - output wire [M_COUNT-1:0] m_axi_wvalid, - input wire [M_COUNT-1:0] m_axi_wready, - input wire [M_COUNT*ID_WIDTH-1:0] m_axi_bid, - input wire [M_COUNT*2-1:0] m_axi_bresp, - input wire [M_COUNT*BUSER_WIDTH-1:0] m_axi_buser, - input wire [M_COUNT-1:0] m_axi_bvalid, - output wire [M_COUNT-1:0] m_axi_bready, - output wire [M_COUNT*ID_WIDTH-1:0] m_axi_arid, - output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_araddr, - output wire [M_COUNT*8-1:0] m_axi_arlen, - output wire [M_COUNT*3-1:0] m_axi_arsize, - output wire [M_COUNT*2-1:0] m_axi_arburst, - output wire [M_COUNT-1:0] m_axi_arlock, - output wire [M_COUNT*4-1:0] m_axi_arcache, - output wire [M_COUNT*3-1:0] m_axi_arprot, - output wire [M_COUNT*4-1:0] m_axi_arqos, - output wire [M_COUNT*4-1:0] m_axi_arregion, - output wire [M_COUNT*ARUSER_WIDTH-1:0] m_axi_aruser, - output wire [M_COUNT-1:0] m_axi_arvalid, - input wire [M_COUNT-1:0] m_axi_arready, - input wire [M_COUNT*ID_WIDTH-1:0] m_axi_rid, - input wire [M_COUNT*DATA_WIDTH-1:0] m_axi_rdata, - input wire [M_COUNT*2-1:0] m_axi_rresp, - input wire [M_COUNT-1:0] m_axi_rlast, - input wire [M_COUNT*RUSER_WIDTH-1:0] m_axi_ruser, - input wire [M_COUNT-1:0] m_axi_rvalid, - output wire [M_COUNT-1:0] m_axi_rready -); - -parameter CL_S_COUNT = $clog2(S_COUNT); -parameter CL_M_COUNT = $clog2(M_COUNT); - -parameter AUSER_WIDTH = AWUSER_WIDTH > ARUSER_WIDTH ? AWUSER_WIDTH : ARUSER_WIDTH; - -// default address computation -function [M_COUNT*M_REGIONS*ADDR_WIDTH-1:0] calcBaseAddrs(input [31:0] dummy); - integer i; - reg [ADDR_WIDTH-1:0] base; - reg [ADDR_WIDTH-1:0] width; - reg [ADDR_WIDTH-1:0] size; - reg [ADDR_WIDTH-1:0] mask; - begin - calcBaseAddrs = {M_COUNT*M_REGIONS*ADDR_WIDTH{1'b0}}; - base = 0; - for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin - width = M_ADDR_WIDTH[i*32 +: 32]; - mask = {ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - width); - size = mask + 1; - if (width > 0) begin - if ((base & mask) != 0) begin - base = base + size - (base & mask); // align - end - calcBaseAddrs[i * ADDR_WIDTH +: ADDR_WIDTH] = base; - base = base + size; // increment - end - end - end -endfunction - -parameter M_BASE_ADDR_INT = M_BASE_ADDR ? M_BASE_ADDR : calcBaseAddrs(0); - -integer i, j; - -// check configuration -initial begin - if (M_REGIONS < 1 || M_REGIONS > 16) begin - $error("Error: M_REGIONS must be between 1 and 16 (instance %m)"); - $finish; - end - - for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin - if (M_ADDR_WIDTH[i*32 +: 32] && (M_ADDR_WIDTH[i*32 +: 32] < 12 || M_ADDR_WIDTH[i*32 +: 32] > ADDR_WIDTH)) begin - $error("Error: address width out of range (instance %m)"); - $finish; - end - end - - $display("Addressing configuration for axi_interconnect instance %m"); - for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin - if (M_ADDR_WIDTH[i*32 +: 32]) begin - $display("%2d (%2d): %x / %02d -- %x-%x", - i/M_REGIONS, i%M_REGIONS, - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], - M_ADDR_WIDTH[i*32 +: 32], - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) - ); - end - end - - for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin - if ((M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & (2**M_ADDR_WIDTH[i*32 +: 32]-1)) != 0) begin - $display("Region not aligned:"); - $display("%2d (%2d): %x / %2d -- %x-%x", - i/M_REGIONS, i%M_REGIONS, - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], - M_ADDR_WIDTH[i*32 +: 32], - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) - ); - $error("Error: address range not aligned (instance %m)"); - $finish; - end - end - - for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin - for (j = i+1; j < M_COUNT*M_REGIONS; j = j + 1) begin - if (M_ADDR_WIDTH[i*32 +: 32] && M_ADDR_WIDTH[j*32 +: 32]) begin - if (((M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32])) <= (M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[j*32 +: 32])))) - && ((M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[j*32 +: 32])) <= (M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32]))))) begin - $display("Overlapping regions:"); - $display("%2d (%2d): %x / %2d -- %x-%x", - i/M_REGIONS, i%M_REGIONS, - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], - M_ADDR_WIDTH[i*32 +: 32], - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), - M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) - ); - $display("%2d (%2d): %x / %2d -- %x-%x", - j/M_REGIONS, j%M_REGIONS, - M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH], - M_ADDR_WIDTH[j*32 +: 32], - M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[j*32 +: 32]), - M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[j*32 +: 32])) - ); - $error("Error: address ranges overlap (instance %m)"); - $finish; - end - end - end - end -end - -localparam [2:0] - STATE_IDLE = 3'd0, - STATE_DECODE = 3'd1, - STATE_WRITE = 3'd2, - STATE_WRITE_RESP = 3'd3, - STATE_WRITE_DROP = 3'd4, - STATE_READ = 3'd5, - STATE_READ_DROP = 3'd6, - STATE_WAIT_IDLE = 3'd7; - -reg [2:0] state_reg = STATE_IDLE, state_next; - -reg match; - -reg [CL_M_COUNT-1:0] m_select_reg = 2'd0, m_select_next; -reg [ID_WIDTH-1:0] axi_id_reg = {ID_WIDTH{1'b0}}, axi_id_next; -reg [ADDR_WIDTH-1:0] axi_addr_reg = {ADDR_WIDTH{1'b0}}, axi_addr_next; -reg axi_addr_valid_reg = 1'b0, axi_addr_valid_next; -reg [7:0] axi_len_reg = 8'd0, axi_len_next; -reg [2:0] axi_size_reg = 3'd0, axi_size_next; -reg [1:0] axi_burst_reg = 2'd0, axi_burst_next; -reg axi_lock_reg = 1'b0, axi_lock_next; -reg [3:0] axi_cache_reg = 4'd0, axi_cache_next; -reg [2:0] axi_prot_reg = 3'b000, axi_prot_next; -reg [3:0] axi_qos_reg = 4'd0, axi_qos_next; -reg [3:0] axi_region_reg = 4'd0, axi_region_next; -reg [AUSER_WIDTH-1:0] axi_auser_reg = {AUSER_WIDTH{1'b0}}, axi_auser_next; -reg [1:0] axi_bresp_reg = 2'b00, axi_bresp_next; -reg [BUSER_WIDTH-1:0] axi_buser_reg = {BUSER_WIDTH{1'b0}}, axi_buser_next; - -reg [S_COUNT-1:0] s_axi_awready_reg = 0, s_axi_awready_next; -reg [S_COUNT-1:0] s_axi_wready_reg = 0, s_axi_wready_next; -reg [S_COUNT-1:0] s_axi_bvalid_reg = 0, s_axi_bvalid_next; -reg [S_COUNT-1:0] s_axi_arready_reg = 0, s_axi_arready_next; - -reg [M_COUNT-1:0] m_axi_awvalid_reg = 0, m_axi_awvalid_next; -reg [M_COUNT-1:0] m_axi_bready_reg = 0, m_axi_bready_next; -reg [M_COUNT-1:0] m_axi_arvalid_reg = 0, m_axi_arvalid_next; -reg [M_COUNT-1:0] m_axi_rready_reg = 0, m_axi_rready_next; - -// internal datapath -reg [ID_WIDTH-1:0] s_axi_rid_int; -reg [DATA_WIDTH-1:0] s_axi_rdata_int; -reg [1:0] s_axi_rresp_int; -reg s_axi_rlast_int; -reg [RUSER_WIDTH-1:0] s_axi_ruser_int; -reg s_axi_rvalid_int; -reg s_axi_rready_int_reg = 1'b0; -wire s_axi_rready_int_early; - -reg [DATA_WIDTH-1:0] m_axi_wdata_int; -reg [STRB_WIDTH-1:0] m_axi_wstrb_int; -reg m_axi_wlast_int; -reg [WUSER_WIDTH-1:0] m_axi_wuser_int; -reg m_axi_wvalid_int; -reg m_axi_wready_int_reg = 1'b0; -wire m_axi_wready_int_early; - -assign s_axi_awready = s_axi_awready_reg; -assign s_axi_wready = s_axi_wready_reg; -assign s_axi_bid = {S_COUNT{axi_id_reg}}; -assign s_axi_bresp = {S_COUNT{axi_bresp_reg}}; -assign s_axi_buser = {S_COUNT{BUSER_ENABLE ? axi_buser_reg : {BUSER_WIDTH{1'b0}}}}; -assign s_axi_bvalid = s_axi_bvalid_reg; -assign s_axi_arready = s_axi_arready_reg; - -assign m_axi_awid = {M_COUNT{FORWARD_ID ? axi_id_reg : {ID_WIDTH{1'b0}}}}; -assign m_axi_awaddr = {M_COUNT{axi_addr_reg}}; -assign m_axi_awlen = {M_COUNT{axi_len_reg}}; -assign m_axi_awsize = {M_COUNT{axi_size_reg}}; -assign m_axi_awburst = {M_COUNT{axi_burst_reg}}; -assign m_axi_awlock = {M_COUNT{axi_lock_reg}}; -assign m_axi_awcache = {M_COUNT{axi_cache_reg}}; -assign m_axi_awprot = {M_COUNT{axi_prot_reg}}; -assign m_axi_awqos = {M_COUNT{axi_qos_reg}}; -assign m_axi_awregion = {M_COUNT{axi_region_reg}}; -assign m_axi_awuser = {M_COUNT{AWUSER_ENABLE ? axi_auser_reg[AWUSER_WIDTH-1:0] : {AWUSER_WIDTH{1'b0}}}}; -assign m_axi_awvalid = m_axi_awvalid_reg; -assign m_axi_bready = m_axi_bready_reg; -assign m_axi_arid = {M_COUNT{FORWARD_ID ? axi_id_reg : {ID_WIDTH{1'b0}}}}; -assign m_axi_araddr = {M_COUNT{axi_addr_reg}}; -assign m_axi_arlen = {M_COUNT{axi_len_reg}}; -assign m_axi_arsize = {M_COUNT{axi_size_reg}}; -assign m_axi_arburst = {M_COUNT{axi_burst_reg}}; -assign m_axi_arlock = {M_COUNT{axi_lock_reg}}; -assign m_axi_arcache = {M_COUNT{axi_cache_reg}}; -assign m_axi_arprot = {M_COUNT{axi_prot_reg}}; -assign m_axi_arqos = {M_COUNT{axi_qos_reg}}; -assign m_axi_arregion = {M_COUNT{axi_region_reg}}; -assign m_axi_aruser = {M_COUNT{ARUSER_ENABLE ? axi_auser_reg[ARUSER_WIDTH-1:0] : {ARUSER_WIDTH{1'b0}}}}; -assign m_axi_arvalid = m_axi_arvalid_reg; -assign m_axi_rready = m_axi_rready_reg; - -// slave side mux -wire [(CL_S_COUNT > 0 ? CL_S_COUNT-1 : 0):0] s_select; - -wire [ID_WIDTH-1:0] current_s_axi_awid = s_axi_awid[s_select*ID_WIDTH +: ID_WIDTH]; -wire [ADDR_WIDTH-1:0] current_s_axi_awaddr = s_axi_awaddr[s_select*ADDR_WIDTH +: ADDR_WIDTH]; -wire [7:0] current_s_axi_awlen = s_axi_awlen[s_select*8 +: 8]; -wire [2:0] current_s_axi_awsize = s_axi_awsize[s_select*3 +: 3]; -wire [1:0] current_s_axi_awburst = s_axi_awburst[s_select*2 +: 2]; -wire current_s_axi_awlock = s_axi_awlock[s_select]; -wire [3:0] current_s_axi_awcache = s_axi_awcache[s_select*4 +: 4]; -wire [2:0] current_s_axi_awprot = s_axi_awprot[s_select*3 +: 3]; -wire [3:0] current_s_axi_awqos = s_axi_awqos[s_select*4 +: 4]; -wire [AWUSER_WIDTH-1:0] current_s_axi_awuser = s_axi_awuser[s_select*AWUSER_WIDTH +: AWUSER_WIDTH]; -wire current_s_axi_awvalid = s_axi_awvalid[s_select]; -wire current_s_axi_awready = s_axi_awready[s_select]; -wire [DATA_WIDTH-1:0] current_s_axi_wdata = s_axi_wdata[s_select*DATA_WIDTH +: DATA_WIDTH]; -wire [STRB_WIDTH-1:0] current_s_axi_wstrb = s_axi_wstrb[s_select*STRB_WIDTH +: STRB_WIDTH]; -wire current_s_axi_wlast = s_axi_wlast[s_select]; -wire [WUSER_WIDTH-1:0] current_s_axi_wuser = s_axi_wuser[s_select*WUSER_WIDTH +: WUSER_WIDTH]; -wire current_s_axi_wvalid = s_axi_wvalid[s_select]; -wire current_s_axi_wready = s_axi_wready[s_select]; -wire [ID_WIDTH-1:0] current_s_axi_bid = s_axi_bid[s_select*ID_WIDTH +: ID_WIDTH]; -wire [1:0] current_s_axi_bresp = s_axi_bresp[s_select*2 +: 2]; -wire [BUSER_WIDTH-1:0] current_s_axi_buser = s_axi_buser[s_select*BUSER_WIDTH +: BUSER_WIDTH]; -wire current_s_axi_bvalid = s_axi_bvalid[s_select]; -wire current_s_axi_bready = s_axi_bready[s_select]; -wire [ID_WIDTH-1:0] current_s_axi_arid = s_axi_arid[s_select*ID_WIDTH +: ID_WIDTH]; -wire [ADDR_WIDTH-1:0] current_s_axi_araddr = s_axi_araddr[s_select*ADDR_WIDTH +: ADDR_WIDTH]; -wire [7:0] current_s_axi_arlen = s_axi_arlen[s_select*8 +: 8]; -wire [2:0] current_s_axi_arsize = s_axi_arsize[s_select*3 +: 3]; -wire [1:0] current_s_axi_arburst = s_axi_arburst[s_select*2 +: 2]; -wire current_s_axi_arlock = s_axi_arlock[s_select]; -wire [3:0] current_s_axi_arcache = s_axi_arcache[s_select*4 +: 4]; -wire [2:0] current_s_axi_arprot = s_axi_arprot[s_select*3 +: 3]; -wire [3:0] current_s_axi_arqos = s_axi_arqos[s_select*4 +: 4]; -wire [ARUSER_WIDTH-1:0] current_s_axi_aruser = s_axi_aruser[s_select*ARUSER_WIDTH +: ARUSER_WIDTH]; -wire current_s_axi_arvalid = s_axi_arvalid[s_select]; -wire current_s_axi_arready = s_axi_arready[s_select]; -wire [ID_WIDTH-1:0] current_s_axi_rid = s_axi_rid[s_select*ID_WIDTH +: ID_WIDTH]; -wire [DATA_WIDTH-1:0] current_s_axi_rdata = s_axi_rdata[s_select*DATA_WIDTH +: DATA_WIDTH]; -wire [1:0] current_s_axi_rresp = s_axi_rresp[s_select*2 +: 2]; -wire current_s_axi_rlast = s_axi_rlast[s_select]; -wire [RUSER_WIDTH-1:0] current_s_axi_ruser = s_axi_ruser[s_select*RUSER_WIDTH +: RUSER_WIDTH]; -wire current_s_axi_rvalid = s_axi_rvalid[s_select]; -wire current_s_axi_rready = s_axi_rready[s_select]; - -// master side mux -wire [ID_WIDTH-1:0] current_m_axi_awid = m_axi_awid[m_select_reg*ID_WIDTH +: ID_WIDTH]; -wire [ADDR_WIDTH-1:0] current_m_axi_awaddr = m_axi_awaddr[m_select_reg*ADDR_WIDTH +: ADDR_WIDTH]; -wire [7:0] current_m_axi_awlen = m_axi_awlen[m_select_reg*8 +: 8]; -wire [2:0] current_m_axi_awsize = m_axi_awsize[m_select_reg*3 +: 3]; -wire [1:0] current_m_axi_awburst = m_axi_awburst[m_select_reg*2 +: 2]; -wire current_m_axi_awlock = m_axi_awlock[m_select_reg]; -wire [3:0] current_m_axi_awcache = m_axi_awcache[m_select_reg*4 +: 4]; -wire [2:0] current_m_axi_awprot = m_axi_awprot[m_select_reg*3 +: 3]; -wire [3:0] current_m_axi_awqos = m_axi_awqos[m_select_reg*4 +: 4]; -wire [3:0] current_m_axi_awregion = m_axi_awregion[m_select_reg*4 +: 4]; -wire [AWUSER_WIDTH-1:0] current_m_axi_awuser = m_axi_awuser[m_select_reg*AWUSER_WIDTH +: AWUSER_WIDTH]; -wire current_m_axi_awvalid = m_axi_awvalid[m_select_reg]; -wire current_m_axi_awready = m_axi_awready[m_select_reg]; -wire [DATA_WIDTH-1:0] current_m_axi_wdata = m_axi_wdata[m_select_reg*DATA_WIDTH +: DATA_WIDTH]; -wire [STRB_WIDTH-1:0] current_m_axi_wstrb = m_axi_wstrb[m_select_reg*STRB_WIDTH +: STRB_WIDTH]; -wire current_m_axi_wlast = m_axi_wlast[m_select_reg]; -wire [WUSER_WIDTH-1:0] current_m_axi_wuser = m_axi_wuser[m_select_reg*WUSER_WIDTH +: WUSER_WIDTH]; -wire current_m_axi_wvalid = m_axi_wvalid[m_select_reg]; -wire current_m_axi_wready = m_axi_wready[m_select_reg]; -wire [ID_WIDTH-1:0] current_m_axi_bid = m_axi_bid[m_select_reg*ID_WIDTH +: ID_WIDTH]; -wire [1:0] current_m_axi_bresp = m_axi_bresp[m_select_reg*2 +: 2]; -wire [BUSER_WIDTH-1:0] current_m_axi_buser = m_axi_buser[m_select_reg*BUSER_WIDTH +: BUSER_WIDTH]; -wire current_m_axi_bvalid = m_axi_bvalid[m_select_reg]; -wire current_m_axi_bready = m_axi_bready[m_select_reg]; -wire [ID_WIDTH-1:0] current_m_axi_arid = m_axi_arid[m_select_reg*ID_WIDTH +: ID_WIDTH]; -wire [ADDR_WIDTH-1:0] current_m_axi_araddr = m_axi_araddr[m_select_reg*ADDR_WIDTH +: ADDR_WIDTH]; -wire [7:0] current_m_axi_arlen = m_axi_arlen[m_select_reg*8 +: 8]; -wire [2:0] current_m_axi_arsize = m_axi_arsize[m_select_reg*3 +: 3]; -wire [1:0] current_m_axi_arburst = m_axi_arburst[m_select_reg*2 +: 2]; -wire current_m_axi_arlock = m_axi_arlock[m_select_reg]; -wire [3:0] current_m_axi_arcache = m_axi_arcache[m_select_reg*4 +: 4]; -wire [2:0] current_m_axi_arprot = m_axi_arprot[m_select_reg*3 +: 3]; -wire [3:0] current_m_axi_arqos = m_axi_arqos[m_select_reg*4 +: 4]; -wire [3:0] current_m_axi_arregion = m_axi_arregion[m_select_reg*4 +: 4]; -wire [ARUSER_WIDTH-1:0] current_m_axi_aruser = m_axi_aruser[m_select_reg*ARUSER_WIDTH +: ARUSER_WIDTH]; -wire current_m_axi_arvalid = m_axi_arvalid[m_select_reg]; -wire current_m_axi_arready = m_axi_arready[m_select_reg]; -wire [ID_WIDTH-1:0] current_m_axi_rid = m_axi_rid[m_select_reg*ID_WIDTH +: ID_WIDTH]; -wire [DATA_WIDTH-1:0] current_m_axi_rdata = m_axi_rdata[m_select_reg*DATA_WIDTH +: DATA_WIDTH]; -wire [1:0] current_m_axi_rresp = m_axi_rresp[m_select_reg*2 +: 2]; -wire current_m_axi_rlast = m_axi_rlast[m_select_reg]; -wire [RUSER_WIDTH-1:0] current_m_axi_ruser = m_axi_ruser[m_select_reg*RUSER_WIDTH +: RUSER_WIDTH]; -wire current_m_axi_rvalid = m_axi_rvalid[m_select_reg]; -wire current_m_axi_rready = m_axi_rready[m_select_reg]; - -// arbiter instance -wire [S_COUNT*2-1:0] request; -wire [S_COUNT*2-1:0] acknowledge; -wire [S_COUNT*2-1:0] grant; -wire grant_valid; -wire [CL_S_COUNT:0] grant_encoded; - -wire read = grant_encoded[0]; -assign s_select = grant_encoded >> 1; - -arbiter #( - .PORTS(S_COUNT*2), - .ARB_TYPE_ROUND_ROBIN(1), - .ARB_BLOCK(1), - .ARB_BLOCK_ACK(1), - .ARB_LSB_HIGH_PRIORITY(1) -) -arb_inst ( - .clk(clk), - .rst(rst), - .request(request), - .acknowledge(acknowledge), - .grant(grant), - .grant_valid(grant_valid), - .grant_encoded(grant_encoded) -); - -genvar n; - -// request generation -generate -for (n = 0; n < S_COUNT; n = n + 1) begin - assign request[2*n] = s_axi_awvalid[n]; - assign request[2*n+1] = s_axi_arvalid[n]; -end -endgenerate - -// acknowledge generation -generate -for (n = 0; n < S_COUNT; n = n + 1) begin - assign acknowledge[2*n] = grant[2*n] && s_axi_bvalid[n] && s_axi_bready[n]; - assign acknowledge[2*n+1] = grant[2*n+1] && s_axi_rvalid[n] && s_axi_rready[n] && s_axi_rlast[n]; -end -endgenerate - -always @* begin - state_next = STATE_IDLE; - - match = 1'b0; - - m_select_next = m_select_reg; - axi_id_next = axi_id_reg; - axi_addr_next = axi_addr_reg; - axi_addr_valid_next = axi_addr_valid_reg; - axi_len_next = axi_len_reg; - axi_size_next = axi_size_reg; - axi_burst_next = axi_burst_reg; - axi_lock_next = axi_lock_reg; - axi_cache_next = axi_cache_reg; - axi_prot_next = axi_prot_reg; - axi_qos_next = axi_qos_reg; - axi_region_next = axi_region_reg; - axi_auser_next = axi_auser_reg; - axi_bresp_next = axi_bresp_reg; - axi_buser_next = axi_buser_reg; - - s_axi_awready_next = 0; - s_axi_wready_next = 0; - s_axi_bvalid_next = s_axi_bvalid_reg & ~s_axi_bready; - s_axi_arready_next = 0; - - m_axi_awvalid_next = m_axi_awvalid_reg & ~m_axi_awready; - m_axi_bready_next = 0; - m_axi_arvalid_next = m_axi_arvalid_reg & ~m_axi_arready; - m_axi_rready_next = 0; - - s_axi_rid_int = axi_id_reg; - s_axi_rdata_int = current_m_axi_rdata; - s_axi_rresp_int = current_m_axi_rresp; - s_axi_rlast_int = current_m_axi_rlast; - s_axi_ruser_int = current_m_axi_ruser; - s_axi_rvalid_int = 1'b0; - - m_axi_wdata_int = current_s_axi_wdata; - m_axi_wstrb_int = current_s_axi_wstrb; - m_axi_wlast_int = current_s_axi_wlast; - m_axi_wuser_int = current_s_axi_wuser; - m_axi_wvalid_int = 1'b0; - - case (state_reg) - STATE_IDLE: begin - // idle state; wait for arbitration - - if (grant_valid) begin - - axi_addr_valid_next = 1'b1; - - if (read) begin - // reading - axi_addr_next = current_s_axi_araddr; - axi_prot_next = current_s_axi_arprot; - axi_id_next = current_s_axi_arid; - axi_addr_next = current_s_axi_araddr; - axi_len_next = current_s_axi_arlen; - axi_size_next = current_s_axi_arsize; - axi_burst_next = current_s_axi_arburst; - axi_lock_next = current_s_axi_arlock; - axi_cache_next = current_s_axi_arcache; - axi_prot_next = current_s_axi_arprot; - axi_qos_next = current_s_axi_arqos; - axi_auser_next = current_s_axi_aruser; - s_axi_arready_next[s_select] = 1'b1; - end else begin - // writing - axi_addr_next = current_s_axi_awaddr; - axi_prot_next = current_s_axi_awprot; - axi_id_next = current_s_axi_awid; - axi_addr_next = current_s_axi_awaddr; - axi_len_next = current_s_axi_awlen; - axi_size_next = current_s_axi_awsize; - axi_burst_next = current_s_axi_awburst; - axi_lock_next = current_s_axi_awlock; - axi_cache_next = current_s_axi_awcache; - axi_prot_next = current_s_axi_awprot; - axi_qos_next = current_s_axi_awqos; - axi_auser_next = current_s_axi_awuser; - s_axi_awready_next[s_select] = 1'b1; - end - - state_next = STATE_DECODE; - end else begin - state_next = STATE_IDLE; - end - end - STATE_DECODE: begin - // decode state; determine master interface - - match = 1'b0; - for (i = 0; i < M_COUNT; i = i + 1) begin - for (j = 0; j < M_REGIONS; j = j + 1) begin - if (M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32] && (!M_SECURE[i] || !axi_prot_reg[1]) && ((read ? M_CONNECT_READ : M_CONNECT_WRITE) & (1 << (s_select+i*S_COUNT))) && (axi_addr_reg >> M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32]) == (M_BASE_ADDR_INT[(i*M_REGIONS+j)*ADDR_WIDTH +: ADDR_WIDTH] >> M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32])) begin - m_select_next = i; - axi_region_next = j; - match = 1'b1; - end - end - end - - if (match) begin - if (read) begin - // reading - m_axi_rready_next[m_select_reg] = s_axi_rready_int_early; - state_next = STATE_READ; - end else begin - // writing - s_axi_wready_next[s_select] = m_axi_wready_int_early; - state_next = STATE_WRITE; - end - end else begin - // no match; return decode error - if (read) begin - // reading - state_next = STATE_READ_DROP; - end else begin - // writing - axi_bresp_next = 2'b11; - s_axi_wready_next[s_select] = 1'b1; - state_next = STATE_WRITE_DROP; - end - end - end - STATE_WRITE: begin - // write state; store and forward write data - s_axi_wready_next[s_select] = m_axi_wready_int_early; - - if (axi_addr_valid_reg) begin - m_axi_awvalid_next[m_select_reg] = 1'b1; - end - axi_addr_valid_next = 1'b0; - - if (current_s_axi_wready && current_s_axi_wvalid) begin - m_axi_wdata_int = current_s_axi_wdata; - m_axi_wstrb_int = current_s_axi_wstrb; - m_axi_wlast_int = current_s_axi_wlast; - m_axi_wuser_int = current_s_axi_wuser; - m_axi_wvalid_int = 1'b1; - - if (current_s_axi_wlast) begin - s_axi_wready_next[s_select] = 1'b0; - m_axi_bready_next[m_select_reg] = 1'b1; - state_next = STATE_WRITE_RESP; - end else begin - state_next = STATE_WRITE; - end - end else begin - state_next = STATE_WRITE; - end - end - STATE_WRITE_RESP: begin - // write response state; store and forward write response - m_axi_bready_next[m_select_reg] = 1'b1; - - if (current_m_axi_bready && current_m_axi_bvalid) begin - m_axi_bready_next[m_select_reg] = 1'b0; - axi_bresp_next = current_m_axi_bresp; - s_axi_bvalid_next[s_select] = 1'b1; - state_next = STATE_WAIT_IDLE; - end else begin - state_next = STATE_WRITE_RESP; - end - end - STATE_WRITE_DROP: begin - // write drop state; drop write data - s_axi_wready_next[s_select] = 1'b1; - - axi_addr_valid_next = 1'b0; - - if (current_s_axi_wready && current_s_axi_wvalid && current_s_axi_wlast) begin - s_axi_wready_next[s_select] = 1'b0; - s_axi_bvalid_next[s_select] = 1'b1; - state_next = STATE_WAIT_IDLE; - end else begin - state_next = STATE_WRITE_DROP; - end - end - STATE_READ: begin - // read state; store and forward read response - m_axi_rready_next[m_select_reg] = s_axi_rready_int_early; - - if (axi_addr_valid_reg) begin - m_axi_arvalid_next[m_select_reg] = 1'b1; - end - axi_addr_valid_next = 1'b0; - - if (current_m_axi_rready && current_m_axi_rvalid) begin - s_axi_rid_int = axi_id_reg; - s_axi_rdata_int = current_m_axi_rdata; - s_axi_rresp_int = current_m_axi_rresp; - s_axi_rlast_int = current_m_axi_rlast; - s_axi_ruser_int = current_m_axi_ruser; - s_axi_rvalid_int = 1'b1; - - if (current_m_axi_rlast) begin - m_axi_rready_next[m_select_reg] = 1'b0; - state_next = STATE_WAIT_IDLE; - end else begin - state_next = STATE_READ; - end - end else begin - state_next = STATE_READ; - end - end - STATE_READ_DROP: begin - // read drop state; generate decode error read response - - s_axi_rid_int = axi_id_reg; - s_axi_rdata_int = {DATA_WIDTH{1'b0}}; - s_axi_rresp_int = 2'b11; - s_axi_rlast_int = axi_len_reg == 0; - s_axi_ruser_int = {RUSER_WIDTH{1'b0}}; - s_axi_rvalid_int = 1'b1; - - if (s_axi_rready_int_reg) begin - axi_len_next = axi_len_reg - 1; - if (axi_len_reg == 0) begin - state_next = STATE_WAIT_IDLE; - end else begin - state_next = STATE_READ_DROP; - end - end else begin - state_next = STATE_READ_DROP; - end - end - STATE_WAIT_IDLE: begin - // wait for idle state; wait untl grant valid is deasserted - - if (!grant_valid || acknowledge) begin - state_next = STATE_IDLE; - end else begin - state_next = STATE_WAIT_IDLE; - end - end - endcase -end - -always @(posedge clk) begin - if (rst) begin - state_reg <= STATE_IDLE; - - s_axi_awready_reg <= 0; - s_axi_wready_reg <= 0; - s_axi_bvalid_reg <= 0; - s_axi_arready_reg <= 0; - - m_axi_awvalid_reg <= 0; - m_axi_bready_reg <= 0; - m_axi_arvalid_reg <= 0; - m_axi_rready_reg <= 0; - end else begin - state_reg <= state_next; - - s_axi_awready_reg <= s_axi_awready_next; - s_axi_wready_reg <= s_axi_wready_next; - s_axi_bvalid_reg <= s_axi_bvalid_next; - s_axi_arready_reg <= s_axi_arready_next; - - m_axi_awvalid_reg <= m_axi_awvalid_next; - m_axi_bready_reg <= m_axi_bready_next; - m_axi_arvalid_reg <= m_axi_arvalid_next; - m_axi_rready_reg <= m_axi_rready_next; - end - - m_select_reg <= m_select_next; - axi_id_reg <= axi_id_next; - axi_addr_reg <= axi_addr_next; - axi_addr_valid_reg <= axi_addr_valid_next; - axi_len_reg <= axi_len_next; - axi_size_reg <= axi_size_next; - axi_burst_reg <= axi_burst_next; - axi_lock_reg <= axi_lock_next; - axi_cache_reg <= axi_cache_next; - axi_prot_reg <= axi_prot_next; - axi_qos_reg <= axi_qos_next; - axi_region_reg <= axi_region_next; - axi_auser_reg <= axi_auser_next; - axi_bresp_reg <= axi_bresp_next; - axi_buser_reg <= axi_buser_next; -end - -// output datapath logic (R channel) -reg [ID_WIDTH-1:0] s_axi_rid_reg = {ID_WIDTH{1'b0}}; -reg [DATA_WIDTH-1:0] s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; -reg [1:0] s_axi_rresp_reg = 2'd0; -reg s_axi_rlast_reg = 1'b0; -reg [RUSER_WIDTH-1:0] s_axi_ruser_reg = 1'b0; -reg [S_COUNT-1:0] s_axi_rvalid_reg = 1'b0, s_axi_rvalid_next; - -reg [ID_WIDTH-1:0] temp_s_axi_rid_reg = {ID_WIDTH{1'b0}}; -reg [DATA_WIDTH-1:0] temp_s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; -reg [1:0] temp_s_axi_rresp_reg = 2'd0; -reg temp_s_axi_rlast_reg = 1'b0; -reg [RUSER_WIDTH-1:0] temp_s_axi_ruser_reg = 1'b0; -reg temp_s_axi_rvalid_reg = 1'b0, temp_s_axi_rvalid_next; - -// datapath control -reg store_axi_r_int_to_output; -reg store_axi_r_int_to_temp; -reg store_axi_r_temp_to_output; - -assign s_axi_rid = {S_COUNT{s_axi_rid_reg}}; -assign s_axi_rdata = {S_COUNT{s_axi_rdata_reg}}; -assign s_axi_rresp = {S_COUNT{s_axi_rresp_reg}}; -assign s_axi_rlast = {S_COUNT{s_axi_rlast_reg}}; -assign s_axi_ruser = {S_COUNT{RUSER_ENABLE ? s_axi_ruser_reg : {RUSER_WIDTH{1'b0}}}}; -assign s_axi_rvalid = s_axi_rvalid_reg; - -// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) -assign s_axi_rready_int_early = current_s_axi_rready | (~temp_s_axi_rvalid_reg & (~current_s_axi_rvalid | ~s_axi_rvalid_int)); - -always @* begin - // transfer sink ready state to source - s_axi_rvalid_next = s_axi_rvalid_reg; - temp_s_axi_rvalid_next = temp_s_axi_rvalid_reg; - - store_axi_r_int_to_output = 1'b0; - store_axi_r_int_to_temp = 1'b0; - store_axi_r_temp_to_output = 1'b0; - - if (s_axi_rready_int_reg) begin - // input is ready - if (current_s_axi_rready | ~current_s_axi_rvalid) begin - // output is ready or currently not valid, transfer data to output - s_axi_rvalid_next[s_select] = s_axi_rvalid_int; - store_axi_r_int_to_output = 1'b1; - end else begin - // output is not ready, store input in temp - temp_s_axi_rvalid_next = s_axi_rvalid_int; - store_axi_r_int_to_temp = 1'b1; - end - end else if (current_s_axi_rready) begin - // input is not ready, but output is ready - s_axi_rvalid_next[s_select] = temp_s_axi_rvalid_reg; - temp_s_axi_rvalid_next = 1'b0; - store_axi_r_temp_to_output = 1'b1; - end -end - -always @(posedge clk) begin - if (rst) begin - s_axi_rvalid_reg <= 1'b0; - s_axi_rready_int_reg <= 1'b0; - temp_s_axi_rvalid_reg <= 1'b0; - end else begin - s_axi_rvalid_reg <= s_axi_rvalid_next; - s_axi_rready_int_reg <= s_axi_rready_int_early; - temp_s_axi_rvalid_reg <= temp_s_axi_rvalid_next; - end - - // datapath - if (store_axi_r_int_to_output) begin - s_axi_rid_reg <= s_axi_rid_int; - s_axi_rdata_reg <= s_axi_rdata_int; - s_axi_rresp_reg <= s_axi_rresp_int; - s_axi_rlast_reg <= s_axi_rlast_int; - s_axi_ruser_reg <= s_axi_ruser_int; - end else if (store_axi_r_temp_to_output) begin - s_axi_rid_reg <= temp_s_axi_rid_reg; - s_axi_rdata_reg <= temp_s_axi_rdata_reg; - s_axi_rresp_reg <= temp_s_axi_rresp_reg; - s_axi_rlast_reg <= temp_s_axi_rlast_reg; - s_axi_ruser_reg <= temp_s_axi_ruser_reg; - end - - if (store_axi_r_int_to_temp) begin - temp_s_axi_rid_reg <= s_axi_rid_int; - temp_s_axi_rdata_reg <= s_axi_rdata_int; - temp_s_axi_rresp_reg <= s_axi_rresp_int; - temp_s_axi_rlast_reg <= s_axi_rlast_int; - temp_s_axi_ruser_reg <= s_axi_ruser_int; - end -end - -// output datapath logic (W channel) -reg [DATA_WIDTH-1:0] m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; -reg [STRB_WIDTH-1:0] m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; -reg m_axi_wlast_reg = 1'b0; -reg [WUSER_WIDTH-1:0] m_axi_wuser_reg = 1'b0; -reg [M_COUNT-1:0] m_axi_wvalid_reg = 1'b0, m_axi_wvalid_next; - -reg [DATA_WIDTH-1:0] temp_m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; -reg [STRB_WIDTH-1:0] temp_m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; -reg temp_m_axi_wlast_reg = 1'b0; -reg [WUSER_WIDTH-1:0] temp_m_axi_wuser_reg = 1'b0; -reg temp_m_axi_wvalid_reg = 1'b0, temp_m_axi_wvalid_next; - -// datapath control -reg store_axi_w_int_to_output; -reg store_axi_w_int_to_temp; -reg store_axi_w_temp_to_output; - -assign m_axi_wdata = {M_COUNT{m_axi_wdata_reg}}; -assign m_axi_wstrb = {M_COUNT{m_axi_wstrb_reg}}; -assign m_axi_wlast = {M_COUNT{m_axi_wlast_reg}}; -assign m_axi_wuser = {M_COUNT{WUSER_ENABLE ? m_axi_wuser_reg : {WUSER_WIDTH{1'b0}}}}; -assign m_axi_wvalid = m_axi_wvalid_reg; - -// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) -assign m_axi_wready_int_early = current_m_axi_wready | (~temp_m_axi_wvalid_reg & (~current_m_axi_wvalid | ~m_axi_wvalid_int)); - -always @* begin - // transfer sink ready state to source - m_axi_wvalid_next = m_axi_wvalid_reg; - temp_m_axi_wvalid_next = temp_m_axi_wvalid_reg; - - store_axi_w_int_to_output = 1'b0; - store_axi_w_int_to_temp = 1'b0; - store_axi_w_temp_to_output = 1'b0; - - if (m_axi_wready_int_reg) begin - // input is ready - if (current_m_axi_wready | ~current_m_axi_wvalid) begin - // output is ready or currently not valid, transfer data to output - m_axi_wvalid_next[m_select_reg] = m_axi_wvalid_int; - store_axi_w_int_to_output = 1'b1; - end else begin - // output is not ready, store input in temp - temp_m_axi_wvalid_next = m_axi_wvalid_int; - store_axi_w_int_to_temp = 1'b1; - end - end else if (current_m_axi_wready) begin - // input is not ready, but output is ready - m_axi_wvalid_next[m_select_reg] = temp_m_axi_wvalid_reg; - temp_m_axi_wvalid_next = 1'b0; - store_axi_w_temp_to_output = 1'b1; - end -end - -always @(posedge clk) begin - if (rst) begin - m_axi_wvalid_reg <= 1'b0; - m_axi_wready_int_reg <= 1'b0; - temp_m_axi_wvalid_reg <= 1'b0; - end else begin - m_axi_wvalid_reg <= m_axi_wvalid_next; - m_axi_wready_int_reg <= m_axi_wready_int_early; - temp_m_axi_wvalid_reg <= temp_m_axi_wvalid_next; - end - - // datapath - if (store_axi_w_int_to_output) begin - m_axi_wdata_reg <= m_axi_wdata_int; - m_axi_wstrb_reg <= m_axi_wstrb_int; - m_axi_wlast_reg <= m_axi_wlast_int; - m_axi_wuser_reg <= m_axi_wuser_int; - end else if (store_axi_w_temp_to_output) begin - m_axi_wdata_reg <= temp_m_axi_wdata_reg; - m_axi_wstrb_reg <= temp_m_axi_wstrb_reg; - m_axi_wlast_reg <= temp_m_axi_wlast_reg; - m_axi_wuser_reg <= temp_m_axi_wuser_reg; - end - - if (store_axi_w_int_to_temp) begin - temp_m_axi_wdata_reg <= m_axi_wdata_int; - temp_m_axi_wstrb_reg <= m_axi_wstrb_int; - temp_m_axi_wlast_reg <= m_axi_wlast_int; - temp_m_axi_wuser_reg <= m_axi_wuser_int; - end -end - -endmodule - -`resetall diff --git a/xls/modules/zstd/external/BUILD b/xls/modules/zstd/external/BUILD new file mode 100644 index 0000000000..f24cb69fe0 --- /dev/null +++ b/xls/modules/zstd/external/BUILD @@ -0,0 +1,33 @@ +# 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. + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//xls:xls_users"], + licenses = ["notice"], +) + +exports_files( + [ + "arbiter.v", + "axi_crossbar.v", + "axi_crossbar_addr.v", + "axi_crossbar_rd.v", + "axi_crossbar_wr.v", + "axi_crossbar_wrapper.v", + "axi_register_rd.v", + "axi_register_wr.v", + "priority_encoder.v", + ], +) diff --git a/xls/modules/zstd/arbiter.v b/xls/modules/zstd/external/arbiter.v similarity index 100% rename from xls/modules/zstd/arbiter.v rename to xls/modules/zstd/external/arbiter.v diff --git a/xls/modules/zstd/external/axi_crossbar.v b/xls/modules/zstd/external/axi_crossbar.v new file mode 100644 index 0000000000..991d45403a --- /dev/null +++ b/xls/modules/zstd/external/axi_crossbar.v @@ -0,0 +1,391 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 crossbar + */ +module axi_crossbar # +( + // Number of AXI inputs (slave interfaces) + parameter S_COUNT = 4, + // Number of AXI outputs (master interfaces) + parameter M_COUNT = 4, + // Width of data bus in bits + parameter DATA_WIDTH = 32, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) + parameter STRB_WIDTH = (DATA_WIDTH/8), + // Input ID field width (from AXI masters) + parameter S_ID_WIDTH = 8, + // Output ID field width (towards AXI slaves) + // Additional bits required for response routing + parameter M_ID_WIDTH = S_ID_WIDTH+$clog2(S_COUNT), + // Propagate awuser signal + parameter AWUSER_ENABLE = 0, + // Width of awuser signal + parameter AWUSER_WIDTH = 1, + // Propagate wuser signal + parameter WUSER_ENABLE = 0, + // Width of wuser signal + parameter WUSER_WIDTH = 1, + // Propagate buser signal + parameter BUSER_ENABLE = 0, + // Width of buser signal + parameter BUSER_WIDTH = 1, + // Propagate aruser signal + parameter ARUSER_ENABLE = 0, + // Width of aruser signal + parameter ARUSER_WIDTH = 1, + // Propagate ruser signal + parameter RUSER_ENABLE = 0, + // Width of ruser signal + parameter RUSER_WIDTH = 1, + // Number of concurrent unique IDs for each slave interface + // S_COUNT concatenated fields of 32 bits + parameter S_THREADS = {S_COUNT{32'd2}}, + // Number of concurrent operations for each slave interface + // S_COUNT concatenated fields of 32 bits + parameter S_ACCEPT = {S_COUNT{32'd16}}, + // Number of regions per master interface + parameter M_REGIONS = 1, + // Master interface base addresses + // M_COUNT concatenated fields of M_REGIONS concatenated fields of ADDR_WIDTH bits + // set to zero for default addressing based on M_ADDR_WIDTH + parameter M_BASE_ADDR = 0, + // Master interface address widths + // M_COUNT concatenated fields of M_REGIONS concatenated fields of 32 bits + parameter M_ADDR_WIDTH = {M_COUNT{{M_REGIONS{32'd24}}}}, + // Read connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT_READ = {M_COUNT{{S_COUNT{1'b1}}}}, + // Write connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT_WRITE = {M_COUNT{{S_COUNT{1'b1}}}}, + // Number of concurrent operations for each master interface + // M_COUNT concatenated fields of 32 bits + parameter M_ISSUE = {M_COUNT{32'd4}}, + // Secure master (fail operations based on awprot/arprot) + // M_COUNT bits + parameter M_SECURE = {M_COUNT{1'b0}}, + // Slave interface AW channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_AW_REG_TYPE = {S_COUNT{2'd0}}, + // Slave interface W channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_W_REG_TYPE = {S_COUNT{2'd0}}, + // Slave interface B channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_B_REG_TYPE = {S_COUNT{2'd1}}, + // Slave interface AR channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_AR_REG_TYPE = {S_COUNT{2'd0}}, + // Slave interface R channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_R_REG_TYPE = {S_COUNT{2'd2}}, + // Master interface AW channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_AW_REG_TYPE = {M_COUNT{2'd1}}, + // Master interface W channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_W_REG_TYPE = {M_COUNT{2'd2}}, + // Master interface B channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_B_REG_TYPE = {M_COUNT{2'd0}}, + // Master interface AR channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_AR_REG_TYPE = {M_COUNT{2'd1}}, + // Master interface R channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_R_REG_TYPE = {M_COUNT{2'd0}} +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interfaces + */ + input wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_awid, + input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_awaddr, + input wire [S_COUNT*8-1:0] s_axi_awlen, + input wire [S_COUNT*3-1:0] s_axi_awsize, + input wire [S_COUNT*2-1:0] s_axi_awburst, + input wire [S_COUNT-1:0] s_axi_awlock, + input wire [S_COUNT*4-1:0] s_axi_awcache, + input wire [S_COUNT*3-1:0] s_axi_awprot, + input wire [S_COUNT*4-1:0] s_axi_awqos, + input wire [S_COUNT*AWUSER_WIDTH-1:0] s_axi_awuser, + input wire [S_COUNT-1:0] s_axi_awvalid, + output wire [S_COUNT-1:0] s_axi_awready, + input wire [S_COUNT*DATA_WIDTH-1:0] s_axi_wdata, + input wire [S_COUNT*STRB_WIDTH-1:0] s_axi_wstrb, + input wire [S_COUNT-1:0] s_axi_wlast, + input wire [S_COUNT*WUSER_WIDTH-1:0] s_axi_wuser, + input wire [S_COUNT-1:0] s_axi_wvalid, + output wire [S_COUNT-1:0] s_axi_wready, + output wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_bid, + output wire [S_COUNT*2-1:0] s_axi_bresp, + output wire [S_COUNT*BUSER_WIDTH-1:0] s_axi_buser, + output wire [S_COUNT-1:0] s_axi_bvalid, + input wire [S_COUNT-1:0] s_axi_bready, + input wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_arid, + input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_araddr, + input wire [S_COUNT*8-1:0] s_axi_arlen, + input wire [S_COUNT*3-1:0] s_axi_arsize, + input wire [S_COUNT*2-1:0] s_axi_arburst, + input wire [S_COUNT-1:0] s_axi_arlock, + input wire [S_COUNT*4-1:0] s_axi_arcache, + input wire [S_COUNT*3-1:0] s_axi_arprot, + input wire [S_COUNT*4-1:0] s_axi_arqos, + input wire [S_COUNT*ARUSER_WIDTH-1:0] s_axi_aruser, + input wire [S_COUNT-1:0] s_axi_arvalid, + output wire [S_COUNT-1:0] s_axi_arready, + output wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_rid, + output wire [S_COUNT*DATA_WIDTH-1:0] s_axi_rdata, + output wire [S_COUNT*2-1:0] s_axi_rresp, + output wire [S_COUNT-1:0] s_axi_rlast, + output wire [S_COUNT*RUSER_WIDTH-1:0] s_axi_ruser, + output wire [S_COUNT-1:0] s_axi_rvalid, + input wire [S_COUNT-1:0] s_axi_rready, + + /* + * AXI master interfaces + */ + output wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_awid, + output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_awaddr, + output wire [M_COUNT*8-1:0] m_axi_awlen, + output wire [M_COUNT*3-1:0] m_axi_awsize, + output wire [M_COUNT*2-1:0] m_axi_awburst, + output wire [M_COUNT-1:0] m_axi_awlock, + output wire [M_COUNT*4-1:0] m_axi_awcache, + output wire [M_COUNT*3-1:0] m_axi_awprot, + output wire [M_COUNT*4-1:0] m_axi_awqos, + output wire [M_COUNT*4-1:0] m_axi_awregion, + output wire [M_COUNT*AWUSER_WIDTH-1:0] m_axi_awuser, + output wire [M_COUNT-1:0] m_axi_awvalid, + input wire [M_COUNT-1:0] m_axi_awready, + output wire [M_COUNT*DATA_WIDTH-1:0] m_axi_wdata, + output wire [M_COUNT*STRB_WIDTH-1:0] m_axi_wstrb, + output wire [M_COUNT-1:0] m_axi_wlast, + output wire [M_COUNT*WUSER_WIDTH-1:0] m_axi_wuser, + output wire [M_COUNT-1:0] m_axi_wvalid, + input wire [M_COUNT-1:0] m_axi_wready, + input wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_bid, + input wire [M_COUNT*2-1:0] m_axi_bresp, + input wire [M_COUNT*BUSER_WIDTH-1:0] m_axi_buser, + input wire [M_COUNT-1:0] m_axi_bvalid, + output wire [M_COUNT-1:0] m_axi_bready, + output wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_arid, + output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_araddr, + output wire [M_COUNT*8-1:0] m_axi_arlen, + output wire [M_COUNT*3-1:0] m_axi_arsize, + output wire [M_COUNT*2-1:0] m_axi_arburst, + output wire [M_COUNT-1:0] m_axi_arlock, + output wire [M_COUNT*4-1:0] m_axi_arcache, + output wire [M_COUNT*3-1:0] m_axi_arprot, + output wire [M_COUNT*4-1:0] m_axi_arqos, + output wire [M_COUNT*4-1:0] m_axi_arregion, + output wire [M_COUNT*ARUSER_WIDTH-1:0] m_axi_aruser, + output wire [M_COUNT-1:0] m_axi_arvalid, + input wire [M_COUNT-1:0] m_axi_arready, + input wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_rid, + input wire [M_COUNT*DATA_WIDTH-1:0] m_axi_rdata, + input wire [M_COUNT*2-1:0] m_axi_rresp, + input wire [M_COUNT-1:0] m_axi_rlast, + input wire [M_COUNT*RUSER_WIDTH-1:0] m_axi_ruser, + input wire [M_COUNT-1:0] m_axi_rvalid, + output wire [M_COUNT-1:0] m_axi_rready +); + +axi_crossbar_wr #( + .S_COUNT(S_COUNT), + .M_COUNT(M_COUNT), + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .S_ID_WIDTH(S_ID_WIDTH), + .M_ID_WIDTH(M_ID_WIDTH), + .AWUSER_ENABLE(AWUSER_ENABLE), + .AWUSER_WIDTH(AWUSER_WIDTH), + .WUSER_ENABLE(WUSER_ENABLE), + .WUSER_WIDTH(WUSER_WIDTH), + .BUSER_ENABLE(BUSER_ENABLE), + .BUSER_WIDTH(BUSER_WIDTH), + .S_THREADS(S_THREADS), + .S_ACCEPT(S_ACCEPT), + .M_REGIONS(M_REGIONS), + .M_BASE_ADDR(M_BASE_ADDR), + .M_ADDR_WIDTH(M_ADDR_WIDTH), + .M_CONNECT(M_CONNECT_WRITE), + .M_ISSUE(M_ISSUE), + .M_SECURE(M_SECURE), + .S_AW_REG_TYPE(S_AW_REG_TYPE), + .S_W_REG_TYPE (S_W_REG_TYPE), + .S_B_REG_TYPE (S_B_REG_TYPE) +) +axi_crossbar_wr_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI slave interfaces + */ + .s_axi_awid(s_axi_awid), + .s_axi_awaddr(s_axi_awaddr), + .s_axi_awlen(s_axi_awlen), + .s_axi_awsize(s_axi_awsize), + .s_axi_awburst(s_axi_awburst), + .s_axi_awlock(s_axi_awlock), + .s_axi_awcache(s_axi_awcache), + .s_axi_awprot(s_axi_awprot), + .s_axi_awqos(s_axi_awqos), + .s_axi_awuser(s_axi_awuser), + .s_axi_awvalid(s_axi_awvalid), + .s_axi_awready(s_axi_awready), + .s_axi_wdata(s_axi_wdata), + .s_axi_wstrb(s_axi_wstrb), + .s_axi_wlast(s_axi_wlast), + .s_axi_wuser(s_axi_wuser), + .s_axi_wvalid(s_axi_wvalid), + .s_axi_wready(s_axi_wready), + .s_axi_bid(s_axi_bid), + .s_axi_bresp(s_axi_bresp), + .s_axi_buser(s_axi_buser), + .s_axi_bvalid(s_axi_bvalid), + .s_axi_bready(s_axi_bready), + + /* + * AXI master interfaces + */ + .m_axi_awid(m_axi_awid), + .m_axi_awaddr(m_axi_awaddr), + .m_axi_awlen(m_axi_awlen), + .m_axi_awsize(m_axi_awsize), + .m_axi_awburst(m_axi_awburst), + .m_axi_awlock(m_axi_awlock), + .m_axi_awcache(m_axi_awcache), + .m_axi_awprot(m_axi_awprot), + .m_axi_awqos(m_axi_awqos), + .m_axi_awregion(m_axi_awregion), + .m_axi_awuser(m_axi_awuser), + .m_axi_awvalid(m_axi_awvalid), + .m_axi_awready(m_axi_awready), + .m_axi_wdata(m_axi_wdata), + .m_axi_wstrb(m_axi_wstrb), + .m_axi_wlast(m_axi_wlast), + .m_axi_wuser(m_axi_wuser), + .m_axi_wvalid(m_axi_wvalid), + .m_axi_wready(m_axi_wready), + .m_axi_bid(m_axi_bid), + .m_axi_bresp(m_axi_bresp), + .m_axi_buser(m_axi_buser), + .m_axi_bvalid(m_axi_bvalid), + .m_axi_bready(m_axi_bready) +); + +axi_crossbar_rd #( + .S_COUNT(S_COUNT), + .M_COUNT(M_COUNT), + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .S_ID_WIDTH(S_ID_WIDTH), + .M_ID_WIDTH(M_ID_WIDTH), + .ARUSER_ENABLE(ARUSER_ENABLE), + .ARUSER_WIDTH(ARUSER_WIDTH), + .RUSER_ENABLE(RUSER_ENABLE), + .RUSER_WIDTH(RUSER_WIDTH), + .S_THREADS(S_THREADS), + .S_ACCEPT(S_ACCEPT), + .M_REGIONS(M_REGIONS), + .M_BASE_ADDR(M_BASE_ADDR), + .M_ADDR_WIDTH(M_ADDR_WIDTH), + .M_CONNECT(M_CONNECT_READ), + .M_ISSUE(M_ISSUE), + .M_SECURE(M_SECURE), + .S_AR_REG_TYPE(S_AR_REG_TYPE), + .S_R_REG_TYPE (S_R_REG_TYPE) +) +axi_crossbar_rd_inst ( + .clk(clk), + .rst(rst), + + /* + * AXI slave interfaces + */ + .s_axi_arid(s_axi_arid), + .s_axi_araddr(s_axi_araddr), + .s_axi_arlen(s_axi_arlen), + .s_axi_arsize(s_axi_arsize), + .s_axi_arburst(s_axi_arburst), + .s_axi_arlock(s_axi_arlock), + .s_axi_arcache(s_axi_arcache), + .s_axi_arprot(s_axi_arprot), + .s_axi_arqos(s_axi_arqos), + .s_axi_aruser(s_axi_aruser), + .s_axi_arvalid(s_axi_arvalid), + .s_axi_arready(s_axi_arready), + .s_axi_rid(s_axi_rid), + .s_axi_rdata(s_axi_rdata), + .s_axi_rresp(s_axi_rresp), + .s_axi_rlast(s_axi_rlast), + .s_axi_ruser(s_axi_ruser), + .s_axi_rvalid(s_axi_rvalid), + .s_axi_rready(s_axi_rready), + + /* + * AXI master interfaces + */ + .m_axi_arid(m_axi_arid), + .m_axi_araddr(m_axi_araddr), + .m_axi_arlen(m_axi_arlen), + .m_axi_arsize(m_axi_arsize), + .m_axi_arburst(m_axi_arburst), + .m_axi_arlock(m_axi_arlock), + .m_axi_arcache(m_axi_arcache), + .m_axi_arprot(m_axi_arprot), + .m_axi_arqos(m_axi_arqos), + .m_axi_arregion(m_axi_arregion), + .m_axi_aruser(m_axi_aruser), + .m_axi_arvalid(m_axi_arvalid), + .m_axi_arready(m_axi_arready), + .m_axi_rid(m_axi_rid), + .m_axi_rdata(m_axi_rdata), + .m_axi_rresp(m_axi_rresp), + .m_axi_rlast(m_axi_rlast), + .m_axi_ruser(m_axi_ruser), + .m_axi_rvalid(m_axi_rvalid), + .m_axi_rready(m_axi_rready) +); + +endmodule + +`resetall diff --git a/xls/modules/zstd/external/axi_crossbar_addr.v b/xls/modules/zstd/external/axi_crossbar_addr.v new file mode 100644 index 0000000000..7b7846526b --- /dev/null +++ b/xls/modules/zstd/external/axi_crossbar_addr.v @@ -0,0 +1,418 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 crossbar address decode and admission control + */ +module axi_crossbar_addr # +( + // Slave interface index + parameter S = 0, + // Number of AXI inputs (slave interfaces) + parameter S_COUNT = 4, + // Number of AXI outputs (master interfaces) + parameter M_COUNT = 4, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // ID field width + parameter ID_WIDTH = 8, + // Number of concurrent unique IDs + parameter S_THREADS = 32'd2, + // Number of concurrent operations + parameter S_ACCEPT = 32'd16, + // Number of regions per master interface + parameter M_REGIONS = 1, + // Master interface base addresses + // M_COUNT concatenated fields of M_REGIONS concatenated fields of ADDR_WIDTH bits + // set to zero for default addressing based on M_ADDR_WIDTH + parameter M_BASE_ADDR = 0, + // Master interface address widths + // M_COUNT concatenated fields of M_REGIONS concatenated fields of 32 bits + parameter M_ADDR_WIDTH = {M_COUNT{{M_REGIONS{32'd24}}}}, + // Connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT = {M_COUNT{{S_COUNT{1'b1}}}}, + // Secure master (fail operations based on awprot/arprot) + // M_COUNT bits + parameter M_SECURE = {M_COUNT{1'b0}}, + // Enable write command output + parameter WC_OUTPUT = 0 +) +( + input wire clk, + input wire rst, + + /* + * Address input + */ + input wire [ID_WIDTH-1:0] s_axi_aid, + input wire [ADDR_WIDTH-1:0] s_axi_aaddr, + input wire [2:0] s_axi_aprot, + input wire [3:0] s_axi_aqos, + input wire s_axi_avalid, + output wire s_axi_aready, + + /* + * Address output + */ + output wire [3:0] m_axi_aregion, + output wire [$clog2(M_COUNT)-1:0] m_select, + output wire m_axi_avalid, + input wire m_axi_aready, + + /* + * Write command output + */ + output wire [$clog2(M_COUNT)-1:0] m_wc_select, + output wire m_wc_decerr, + output wire m_wc_valid, + input wire m_wc_ready, + + /* + * Reply command output + */ + output wire m_rc_decerr, + output wire m_rc_valid, + input wire m_rc_ready, + + /* + * Completion input + */ + input wire [ID_WIDTH-1:0] s_cpl_id, + input wire s_cpl_valid +); + +parameter CL_S_COUNT = $clog2(S_COUNT); +parameter CL_M_COUNT = $clog2(M_COUNT); + +parameter S_INT_THREADS = S_THREADS > S_ACCEPT ? S_ACCEPT : S_THREADS; +parameter CL_S_INT_THREADS = $clog2(S_INT_THREADS); +parameter CL_S_ACCEPT = $clog2(S_ACCEPT); + +// default address computation +function [M_COUNT*M_REGIONS*ADDR_WIDTH-1:0] calcBaseAddrs(input [31:0] dummy); + integer i; + reg [ADDR_WIDTH-1:0] base; + reg [ADDR_WIDTH-1:0] width; + reg [ADDR_WIDTH-1:0] size; + reg [ADDR_WIDTH-1:0] mask; + begin + calcBaseAddrs = {M_COUNT*M_REGIONS*ADDR_WIDTH{1'b0}}; + base = 0; + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + width = M_ADDR_WIDTH[i*32 +: 32]; + mask = {ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - width); + size = mask + 1; + if (width > 0) begin + if ((base & mask) != 0) begin + base = base + size - (base & mask); // align + end + calcBaseAddrs[i * ADDR_WIDTH +: ADDR_WIDTH] = base; + base = base + size; // increment + end + end + end +endfunction + +parameter M_BASE_ADDR_INT = M_BASE_ADDR ? M_BASE_ADDR : calcBaseAddrs(0); + +integer i, j; + +// check configuration +initial begin + if (S_ACCEPT < 1) begin + $error("Error: need at least 1 accept (instance %m)"); + $finish; + end + + if (S_THREADS < 1) begin + $error("Error: need at least 1 thread (instance %m)"); + $finish; + end + + if (S_THREADS > S_ACCEPT) begin + $warning("Warning: requested thread count larger than accept count; limiting thread count to accept count (instance %m)"); + end + + if (M_REGIONS < 1) begin + $error("Error: need at least 1 region (instance %m)"); + $finish; + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32] && (M_ADDR_WIDTH[i*32 +: 32] < 12 || M_ADDR_WIDTH[i*32 +: 32] > ADDR_WIDTH)) begin + $error("Error: address width out of range (instance %m)"); + $finish; + end + end + + $display("Addressing configuration for axi_crossbar_addr instance %m"); + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32]) begin + $display("%2d (%2d): %x / %02d -- %x-%x", + i/M_REGIONS, i%M_REGIONS, + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[i*32 +: 32], + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) + ); + end + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if ((M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & (2**M_ADDR_WIDTH[i*32 +: 32]-1)) != 0) begin + $display("Region not aligned:"); + $display("%2d (%2d): %x / %2d -- %x-%x", + i/M_REGIONS, i%M_REGIONS, + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[i*32 +: 32], + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) + ); + $error("Error: address range not aligned (instance %m)"); + $finish; + end + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + for (j = i+1; j < M_COUNT*M_REGIONS; j = j + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32] && M_ADDR_WIDTH[j*32 +: 32]) begin + if (((M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32])) <= (M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[j*32 +: 32])))) + && ((M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[j*32 +: 32])) <= (M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32]))))) begin + $display("Overlapping regions:"); + $display("%2d (%2d): %x / %2d -- %x-%x", + i/M_REGIONS, i%M_REGIONS, + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[i*32 +: 32], + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[i*32 +: 32]), + M_BASE_ADDR_INT[i*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[i*32 +: 32])) + ); + $display("%2d (%2d): %x / %2d -- %x-%x", + j/M_REGIONS, j%M_REGIONS, + M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH], + M_ADDR_WIDTH[j*32 +: 32], + M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] & ({ADDR_WIDTH{1'b1}} << M_ADDR_WIDTH[j*32 +: 32]), + M_BASE_ADDR_INT[j*ADDR_WIDTH +: ADDR_WIDTH] | ({ADDR_WIDTH{1'b1}} >> (ADDR_WIDTH - M_ADDR_WIDTH[j*32 +: 32])) + ); + $error("Error: address ranges overlap (instance %m)"); + $finish; + end + end + end + end +end + +localparam [2:0] + STATE_IDLE = 3'd0, + STATE_DECODE = 3'd1; + +reg [2:0] state_reg = STATE_IDLE, state_next; + +reg s_axi_aready_reg = 0, s_axi_aready_next; + +reg [3:0] m_axi_aregion_reg = 4'd0, m_axi_aregion_next; +reg [CL_M_COUNT-1:0] m_select_reg = 0, m_select_next; +reg m_axi_avalid_reg = 1'b0, m_axi_avalid_next; +reg m_decerr_reg = 1'b0, m_decerr_next; +reg m_wc_valid_reg = 1'b0, m_wc_valid_next; +reg m_rc_valid_reg = 1'b0, m_rc_valid_next; + +assign s_axi_aready = s_axi_aready_reg; + +assign m_axi_aregion = m_axi_aregion_reg; +assign m_select = m_select_reg; +assign m_axi_avalid = m_axi_avalid_reg; + +assign m_wc_select = m_select_reg; +assign m_wc_decerr = m_decerr_reg; +assign m_wc_valid = m_wc_valid_reg; + +assign m_rc_decerr = m_decerr_reg; +assign m_rc_valid = m_rc_valid_reg; + +reg match; +reg trans_start; +reg trans_complete; + +reg [$clog2(S_ACCEPT+1)-1:0] trans_count_reg = 0; +wire trans_limit = trans_count_reg >= S_ACCEPT && !trans_complete; + +// transfer ID thread tracking +reg [ID_WIDTH-1:0] thread_id_reg[S_INT_THREADS-1:0]; +reg [CL_M_COUNT-1:0] thread_m_reg[S_INT_THREADS-1:0]; +reg [3:0] thread_region_reg[S_INT_THREADS-1:0]; +reg [$clog2(S_ACCEPT+1)-1:0] thread_count_reg[S_INT_THREADS-1:0]; + +wire [S_INT_THREADS-1:0] thread_active; +wire [S_INT_THREADS-1:0] thread_match; +wire [S_INT_THREADS-1:0] thread_match_dest; +wire [S_INT_THREADS-1:0] thread_cpl_match; +wire [S_INT_THREADS-1:0] thread_trans_start; +wire [S_INT_THREADS-1:0] thread_trans_complete; + +generate + genvar n; + + for (n = 0; n < S_INT_THREADS; n = n + 1) begin + initial begin + thread_count_reg[n] <= 0; + end + + assign thread_active[n] = thread_count_reg[n] != 0; + assign thread_match[n] = thread_active[n] && thread_id_reg[n] == s_axi_aid; + assign thread_match_dest[n] = thread_match[n] && thread_m_reg[n] == m_select_next && (M_REGIONS < 2 || thread_region_reg[n] == m_axi_aregion_next); + assign thread_cpl_match[n] = thread_active[n] && thread_id_reg[n] == s_cpl_id; + assign thread_trans_start[n] = (thread_match[n] || (!thread_active[n] && !thread_match && !(thread_trans_start & ({S_INT_THREADS{1'b1}} >> (S_INT_THREADS-n))))) && trans_start; + assign thread_trans_complete[n] = thread_cpl_match[n] && trans_complete; + + always @(posedge clk) begin + if (rst) begin + thread_count_reg[n] <= 0; + end else begin + if (thread_trans_start[n] && !thread_trans_complete[n]) begin + thread_count_reg[n] <= thread_count_reg[n] + 1; + end else if (!thread_trans_start[n] && thread_trans_complete[n]) begin + thread_count_reg[n] <= thread_count_reg[n] - 1; + end + end + + if (thread_trans_start[n]) begin + thread_id_reg[n] <= s_axi_aid; + thread_m_reg[n] <= m_select_next; + thread_region_reg[n] <= m_axi_aregion_next; + end + end + end +endgenerate + +always @* begin + state_next = STATE_IDLE; + + match = 1'b0; + trans_start = 1'b0; + trans_complete = 1'b0; + + s_axi_aready_next = 1'b0; + + m_axi_aregion_next = m_axi_aregion_reg; + m_select_next = m_select_reg; + m_axi_avalid_next = m_axi_avalid_reg && !m_axi_aready; + m_decerr_next = m_decerr_reg; + m_wc_valid_next = m_wc_valid_reg && !m_wc_ready; + m_rc_valid_next = m_rc_valid_reg && !m_rc_ready; + + case (state_reg) + STATE_IDLE: begin + // idle state, store values + s_axi_aready_next = 1'b0; + + if (s_axi_avalid && !s_axi_aready) begin + match = 1'b0; + for (i = 0; i < M_COUNT; i = i + 1) begin + for (j = 0; j < M_REGIONS; j = j + 1) begin + if (M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32] && (!M_SECURE[i] || !s_axi_aprot[1]) && (M_CONNECT & (1 << (S+i*S_COUNT))) && (s_axi_aaddr >> M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32]) == (M_BASE_ADDR_INT[(i*M_REGIONS+j)*ADDR_WIDTH +: ADDR_WIDTH] >> M_ADDR_WIDTH[(i*M_REGIONS+j)*32 +: 32])) begin + m_select_next = i; + m_axi_aregion_next = j; + match = 1'b1; + end + end + end + + if (match) begin + // address decode successful + if (!trans_limit && (thread_match_dest || (!(&thread_active) && !thread_match))) begin + // transaction limit not reached + m_axi_avalid_next = 1'b1; + m_decerr_next = 1'b0; + m_wc_valid_next = WC_OUTPUT; + m_rc_valid_next = 1'b0; + trans_start = 1'b1; + state_next = STATE_DECODE; + end else begin + // transaction limit reached; block in idle + state_next = STATE_IDLE; + end + end else begin + // decode error + m_axi_avalid_next = 1'b0; + m_decerr_next = 1'b1; + m_wc_valid_next = WC_OUTPUT; + m_rc_valid_next = 1'b1; + state_next = STATE_DECODE; + end + end else begin + state_next = STATE_IDLE; + end + end + STATE_DECODE: begin + if (!m_axi_avalid_next && (!m_wc_valid_next || !WC_OUTPUT) && !m_rc_valid_next) begin + s_axi_aready_next = 1'b1; + state_next = STATE_IDLE; + end else begin + state_next = STATE_DECODE; + end + end + endcase + + // manage completions + trans_complete = s_cpl_valid; +end + +always @(posedge clk) begin + if (rst) begin + state_reg <= STATE_IDLE; + s_axi_aready_reg <= 1'b0; + m_axi_avalid_reg <= 1'b0; + m_wc_valid_reg <= 1'b0; + m_rc_valid_reg <= 1'b0; + + trans_count_reg <= 0; + end else begin + state_reg <= state_next; + s_axi_aready_reg <= s_axi_aready_next; + m_axi_avalid_reg <= m_axi_avalid_next; + m_wc_valid_reg <= m_wc_valid_next; + m_rc_valid_reg <= m_rc_valid_next; + + if (trans_start && !trans_complete) begin + trans_count_reg <= trans_count_reg + 1; + end else if (!trans_start && trans_complete) begin + trans_count_reg <= trans_count_reg - 1; + end + end + + m_axi_aregion_reg <= m_axi_aregion_next; + m_select_reg <= m_select_next; + m_decerr_reg <= m_decerr_next; +end + +endmodule + +`resetall diff --git a/xls/modules/zstd/external/axi_crossbar_rd.v b/xls/modules/zstd/external/axi_crossbar_rd.v new file mode 100644 index 0000000000..2b1410ac62 --- /dev/null +++ b/xls/modules/zstd/external/axi_crossbar_rd.v @@ -0,0 +1,569 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 crossbar (read) + */ +module axi_crossbar_rd # +( + // Number of AXI inputs (slave interfaces) + parameter S_COUNT = 4, + // Number of AXI outputs (master interfaces) + parameter M_COUNT = 4, + // Width of data bus in bits + parameter DATA_WIDTH = 32, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) + parameter STRB_WIDTH = (DATA_WIDTH/8), + // Input ID field width (from AXI masters) + parameter S_ID_WIDTH = 8, + // Output ID field width (towards AXI slaves) + // Additional bits required for response routing + parameter M_ID_WIDTH = S_ID_WIDTH+$clog2(S_COUNT), + // Propagate aruser signal + parameter ARUSER_ENABLE = 0, + // Width of aruser signal + parameter ARUSER_WIDTH = 1, + // Propagate ruser signal + parameter RUSER_ENABLE = 0, + // Width of ruser signal + parameter RUSER_WIDTH = 1, + // Number of concurrent unique IDs for each slave interface + // S_COUNT concatenated fields of 32 bits + parameter S_THREADS = {S_COUNT{32'd2}}, + // Number of concurrent operations for each slave interface + // S_COUNT concatenated fields of 32 bits + parameter S_ACCEPT = {S_COUNT{32'd16}}, + // Number of regions per master interface + parameter M_REGIONS = 1, + // Master interface base addresses + // M_COUNT concatenated fields of M_REGIONS concatenated fields of ADDR_WIDTH bits + // set to zero for default addressing based on M_ADDR_WIDTH + parameter M_BASE_ADDR = 0, + // Master interface address widths + // M_COUNT concatenated fields of M_REGIONS concatenated fields of 32 bits + parameter M_ADDR_WIDTH = {M_COUNT{{M_REGIONS{32'd24}}}}, + // Read connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT = {M_COUNT{{S_COUNT{1'b1}}}}, + // Number of concurrent operations for each master interface + // M_COUNT concatenated fields of 32 bits + parameter M_ISSUE = {M_COUNT{32'd4}}, + // Secure master (fail operations based on awprot/arprot) + // M_COUNT bits + parameter M_SECURE = {M_COUNT{1'b0}}, + // Slave interface AR channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_AR_REG_TYPE = {S_COUNT{2'd0}}, + // Slave interface R channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_R_REG_TYPE = {S_COUNT{2'd2}}, + // Master interface AR channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_AR_REG_TYPE = {M_COUNT{2'd1}}, + // Master interface R channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_R_REG_TYPE = {M_COUNT{2'd0}} +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interfaces + */ + input wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_arid, + input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_araddr, + input wire [S_COUNT*8-1:0] s_axi_arlen, + input wire [S_COUNT*3-1:0] s_axi_arsize, + input wire [S_COUNT*2-1:0] s_axi_arburst, + input wire [S_COUNT-1:0] s_axi_arlock, + input wire [S_COUNT*4-1:0] s_axi_arcache, + input wire [S_COUNT*3-1:0] s_axi_arprot, + input wire [S_COUNT*4-1:0] s_axi_arqos, + input wire [S_COUNT*ARUSER_WIDTH-1:0] s_axi_aruser, + input wire [S_COUNT-1:0] s_axi_arvalid, + output wire [S_COUNT-1:0] s_axi_arready, + output wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_rid, + output wire [S_COUNT*DATA_WIDTH-1:0] s_axi_rdata, + output wire [S_COUNT*2-1:0] s_axi_rresp, + output wire [S_COUNT-1:0] s_axi_rlast, + output wire [S_COUNT*RUSER_WIDTH-1:0] s_axi_ruser, + output wire [S_COUNT-1:0] s_axi_rvalid, + input wire [S_COUNT-1:0] s_axi_rready, + + /* + * AXI master interfaces + */ + output wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_arid, + output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_araddr, + output wire [M_COUNT*8-1:0] m_axi_arlen, + output wire [M_COUNT*3-1:0] m_axi_arsize, + output wire [M_COUNT*2-1:0] m_axi_arburst, + output wire [M_COUNT-1:0] m_axi_arlock, + output wire [M_COUNT*4-1:0] m_axi_arcache, + output wire [M_COUNT*3-1:0] m_axi_arprot, + output wire [M_COUNT*4-1:0] m_axi_arqos, + output wire [M_COUNT*4-1:0] m_axi_arregion, + output wire [M_COUNT*ARUSER_WIDTH-1:0] m_axi_aruser, + output wire [M_COUNT-1:0] m_axi_arvalid, + input wire [M_COUNT-1:0] m_axi_arready, + input wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_rid, + input wire [M_COUNT*DATA_WIDTH-1:0] m_axi_rdata, + input wire [M_COUNT*2-1:0] m_axi_rresp, + input wire [M_COUNT-1:0] m_axi_rlast, + input wire [M_COUNT*RUSER_WIDTH-1:0] m_axi_ruser, + input wire [M_COUNT-1:0] m_axi_rvalid, + output wire [M_COUNT-1:0] m_axi_rready +); + +parameter CL_S_COUNT = $clog2(S_COUNT); +parameter CL_M_COUNT = $clog2(M_COUNT); +parameter M_COUNT_P1 = M_COUNT+1; +parameter CL_M_COUNT_P1 = $clog2(M_COUNT_P1); + +integer i; + +// check configuration +initial begin + if (M_ID_WIDTH < S_ID_WIDTH+$clog2(S_COUNT)) begin + $error("Error: M_ID_WIDTH must be at least $clog2(S_COUNT) larger than S_ID_WIDTH (instance %m)"); + $finish; + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32] && (M_ADDR_WIDTH[i*32 +: 32] < 12 || M_ADDR_WIDTH[i*32 +: 32] > ADDR_WIDTH)) begin + $error("Error: value out of range (instance %m)"); + $finish; + end + end +end + +wire [S_COUNT*S_ID_WIDTH-1:0] int_s_axi_arid; +wire [S_COUNT*ADDR_WIDTH-1:0] int_s_axi_araddr; +wire [S_COUNT*8-1:0] int_s_axi_arlen; +wire [S_COUNT*3-1:0] int_s_axi_arsize; +wire [S_COUNT*2-1:0] int_s_axi_arburst; +wire [S_COUNT-1:0] int_s_axi_arlock; +wire [S_COUNT*4-1:0] int_s_axi_arcache; +wire [S_COUNT*3-1:0] int_s_axi_arprot; +wire [S_COUNT*4-1:0] int_s_axi_arqos; +wire [S_COUNT*4-1:0] int_s_axi_arregion; +wire [S_COUNT*ARUSER_WIDTH-1:0] int_s_axi_aruser; +wire [S_COUNT-1:0] int_s_axi_arvalid; +wire [S_COUNT-1:0] int_s_axi_arready; + +wire [S_COUNT*M_COUNT-1:0] int_axi_arvalid; +wire [M_COUNT*S_COUNT-1:0] int_axi_arready; + +wire [M_COUNT*M_ID_WIDTH-1:0] int_m_axi_rid; +wire [M_COUNT*DATA_WIDTH-1:0] int_m_axi_rdata; +wire [M_COUNT*2-1:0] int_m_axi_rresp; +wire [M_COUNT-1:0] int_m_axi_rlast; +wire [M_COUNT*RUSER_WIDTH-1:0] int_m_axi_ruser; +wire [M_COUNT-1:0] int_m_axi_rvalid; +wire [M_COUNT-1:0] int_m_axi_rready; + +wire [M_COUNT*S_COUNT-1:0] int_axi_rvalid; +wire [S_COUNT*M_COUNT-1:0] int_axi_rready; + +generate + + genvar m, n; + + for (m = 0; m < S_COUNT; m = m + 1) begin : s_ifaces + // address decode and admission control + wire [CL_M_COUNT-1:0] a_select; + + wire m_axi_avalid; + wire m_axi_aready; + + wire m_rc_decerr; + wire m_rc_valid; + wire m_rc_ready; + + wire [S_ID_WIDTH-1:0] s_cpl_id; + wire s_cpl_valid; + + axi_crossbar_addr #( + .S(m), + .S_COUNT(S_COUNT), + .M_COUNT(M_COUNT), + .ADDR_WIDTH(ADDR_WIDTH), + .ID_WIDTH(S_ID_WIDTH), + .S_THREADS(S_THREADS[m*32 +: 32]), + .S_ACCEPT(S_ACCEPT[m*32 +: 32]), + .M_REGIONS(M_REGIONS), + .M_BASE_ADDR(M_BASE_ADDR), + .M_ADDR_WIDTH(M_ADDR_WIDTH), + .M_CONNECT(M_CONNECT), + .M_SECURE(M_SECURE), + .WC_OUTPUT(0) + ) + addr_inst ( + .clk(clk), + .rst(rst), + + /* + * Address input + */ + .s_axi_aid(int_s_axi_arid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .s_axi_aaddr(int_s_axi_araddr[m*ADDR_WIDTH +: ADDR_WIDTH]), + .s_axi_aprot(int_s_axi_arprot[m*3 +: 3]), + .s_axi_aqos(int_s_axi_arqos[m*4 +: 4]), + .s_axi_avalid(int_s_axi_arvalid[m]), + .s_axi_aready(int_s_axi_arready[m]), + + /* + * Address output + */ + .m_axi_aregion(int_s_axi_arregion[m*4 +: 4]), + .m_select(a_select), + .m_axi_avalid(m_axi_avalid), + .m_axi_aready(m_axi_aready), + + /* + * Write command output + */ + .m_wc_select(), + .m_wc_decerr(), + .m_wc_valid(), + .m_wc_ready(1'b1), + + /* + * Response command output + */ + .m_rc_decerr(m_rc_decerr), + .m_rc_valid(m_rc_valid), + .m_rc_ready(m_rc_ready), + + /* + * Completion input + */ + .s_cpl_id(s_cpl_id), + .s_cpl_valid(s_cpl_valid) + ); + + assign int_axi_arvalid[m*M_COUNT +: M_COUNT] = m_axi_avalid << a_select; + assign m_axi_aready = int_axi_arready[a_select*S_COUNT+m]; + + // decode error handling + reg [S_ID_WIDTH-1:0] decerr_m_axi_rid_reg = {S_ID_WIDTH{1'b0}}, decerr_m_axi_rid_next; + reg decerr_m_axi_rlast_reg = 1'b0, decerr_m_axi_rlast_next; + reg decerr_m_axi_rvalid_reg = 1'b0, decerr_m_axi_rvalid_next; + wire decerr_m_axi_rready; + + reg [7:0] decerr_len_reg = 8'd0, decerr_len_next; + + assign m_rc_ready = !decerr_m_axi_rvalid_reg; + + always @* begin + decerr_len_next = decerr_len_reg; + decerr_m_axi_rid_next = decerr_m_axi_rid_reg; + decerr_m_axi_rlast_next = decerr_m_axi_rlast_reg; + decerr_m_axi_rvalid_next = decerr_m_axi_rvalid_reg; + + if (decerr_m_axi_rvalid_reg) begin + if (decerr_m_axi_rready) begin + if (decerr_len_reg > 0) begin + decerr_len_next = decerr_len_reg-1; + decerr_m_axi_rlast_next = (decerr_len_next == 0); + decerr_m_axi_rvalid_next = 1'b1; + end else begin + decerr_m_axi_rvalid_next = 1'b0; + end + end + end else if (m_rc_valid && m_rc_ready) begin + decerr_len_next = int_s_axi_arlen[m*8 +: 8]; + decerr_m_axi_rid_next = int_s_axi_arid[m*S_ID_WIDTH +: S_ID_WIDTH]; + decerr_m_axi_rlast_next = (decerr_len_next == 0); + decerr_m_axi_rvalid_next = 1'b1; + end + end + + always @(posedge clk) begin + if (rst) begin + decerr_m_axi_rvalid_reg <= 1'b0; + end else begin + decerr_m_axi_rvalid_reg <= decerr_m_axi_rvalid_next; + end + + decerr_m_axi_rid_reg <= decerr_m_axi_rid_next; + decerr_m_axi_rlast_reg <= decerr_m_axi_rlast_next; + decerr_len_reg <= decerr_len_next; + end + + // read response arbitration + wire [M_COUNT_P1-1:0] r_request; + wire [M_COUNT_P1-1:0] r_acknowledge; + wire [M_COUNT_P1-1:0] r_grant; + wire r_grant_valid; + wire [CL_M_COUNT_P1-1:0] r_grant_encoded; + + arbiter #( + .PORTS(M_COUNT_P1), + .ARB_TYPE_ROUND_ROBIN(1), + .ARB_BLOCK(1), + .ARB_BLOCK_ACK(1), + .ARB_LSB_HIGH_PRIORITY(1) + ) + r_arb_inst ( + .clk(clk), + .rst(rst), + .request(r_request), + .acknowledge(r_acknowledge), + .grant(r_grant), + .grant_valid(r_grant_valid), + .grant_encoded(r_grant_encoded) + ); + + // read response mux + wire [S_ID_WIDTH-1:0] m_axi_rid_mux = {decerr_m_axi_rid_reg, int_m_axi_rid} >> r_grant_encoded*M_ID_WIDTH; + wire [DATA_WIDTH-1:0] m_axi_rdata_mux = {{DATA_WIDTH{1'b0}}, int_m_axi_rdata} >> r_grant_encoded*DATA_WIDTH; + wire [1:0] m_axi_rresp_mux = {2'b11, int_m_axi_rresp} >> r_grant_encoded*2; + wire m_axi_rlast_mux = {decerr_m_axi_rlast_reg, int_m_axi_rlast} >> r_grant_encoded; + wire [RUSER_WIDTH-1:0] m_axi_ruser_mux = {{RUSER_WIDTH{1'b0}}, int_m_axi_ruser} >> r_grant_encoded*RUSER_WIDTH; + wire m_axi_rvalid_mux = ({decerr_m_axi_rvalid_reg, int_m_axi_rvalid} >> r_grant_encoded) & r_grant_valid; + wire m_axi_rready_mux; + + assign int_axi_rready[m*M_COUNT +: M_COUNT] = (r_grant_valid && m_axi_rready_mux) << r_grant_encoded; + assign decerr_m_axi_rready = (r_grant_valid && m_axi_rready_mux) && (r_grant_encoded == M_COUNT_P1-1); + + for (n = 0; n < M_COUNT; n = n + 1) begin + assign r_request[n] = int_axi_rvalid[n*S_COUNT+m] && !r_grant[n]; + assign r_acknowledge[n] = r_grant[n] && int_axi_rvalid[n*S_COUNT+m] && m_axi_rlast_mux && m_axi_rready_mux; + end + + assign r_request[M_COUNT_P1-1] = decerr_m_axi_rvalid_reg && !r_grant[M_COUNT_P1-1]; + assign r_acknowledge[M_COUNT_P1-1] = r_grant[M_COUNT_P1-1] && decerr_m_axi_rvalid_reg && decerr_m_axi_rlast_reg && m_axi_rready_mux; + + assign s_cpl_id = m_axi_rid_mux; + assign s_cpl_valid = m_axi_rvalid_mux && m_axi_rready_mux && m_axi_rlast_mux; + + // S side register + axi_register_rd #( + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .ID_WIDTH(S_ID_WIDTH), + .ARUSER_ENABLE(ARUSER_ENABLE), + .ARUSER_WIDTH(ARUSER_WIDTH), + .RUSER_ENABLE(RUSER_ENABLE), + .RUSER_WIDTH(RUSER_WIDTH), + .AR_REG_TYPE(S_AR_REG_TYPE[m*2 +: 2]), + .R_REG_TYPE(S_R_REG_TYPE[m*2 +: 2]) + ) + reg_inst ( + .clk(clk), + .rst(rst), + .s_axi_arid(s_axi_arid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .s_axi_araddr(s_axi_araddr[m*ADDR_WIDTH +: ADDR_WIDTH]), + .s_axi_arlen(s_axi_arlen[m*8 +: 8]), + .s_axi_arsize(s_axi_arsize[m*3 +: 3]), + .s_axi_arburst(s_axi_arburst[m*2 +: 2]), + .s_axi_arlock(s_axi_arlock[m]), + .s_axi_arcache(s_axi_arcache[m*4 +: 4]), + .s_axi_arprot(s_axi_arprot[m*3 +: 3]), + .s_axi_arqos(s_axi_arqos[m*4 +: 4]), + .s_axi_arregion(4'd0), + .s_axi_aruser(s_axi_aruser[m*ARUSER_WIDTH +: ARUSER_WIDTH]), + .s_axi_arvalid(s_axi_arvalid[m]), + .s_axi_arready(s_axi_arready[m]), + .s_axi_rid(s_axi_rid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .s_axi_rdata(s_axi_rdata[m*DATA_WIDTH +: DATA_WIDTH]), + .s_axi_rresp(s_axi_rresp[m*2 +: 2]), + .s_axi_rlast(s_axi_rlast[m]), + .s_axi_ruser(s_axi_ruser[m*RUSER_WIDTH +: RUSER_WIDTH]), + .s_axi_rvalid(s_axi_rvalid[m]), + .s_axi_rready(s_axi_rready[m]), + .m_axi_arid(int_s_axi_arid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .m_axi_araddr(int_s_axi_araddr[m*ADDR_WIDTH +: ADDR_WIDTH]), + .m_axi_arlen(int_s_axi_arlen[m*8 +: 8]), + .m_axi_arsize(int_s_axi_arsize[m*3 +: 3]), + .m_axi_arburst(int_s_axi_arburst[m*2 +: 2]), + .m_axi_arlock(int_s_axi_arlock[m]), + .m_axi_arcache(int_s_axi_arcache[m*4 +: 4]), + .m_axi_arprot(int_s_axi_arprot[m*3 +: 3]), + .m_axi_arqos(int_s_axi_arqos[m*4 +: 4]), + .m_axi_arregion(), + .m_axi_aruser(int_s_axi_aruser[m*ARUSER_WIDTH +: ARUSER_WIDTH]), + .m_axi_arvalid(int_s_axi_arvalid[m]), + .m_axi_arready(int_s_axi_arready[m]), + .m_axi_rid(m_axi_rid_mux), + .m_axi_rdata(m_axi_rdata_mux), + .m_axi_rresp(m_axi_rresp_mux), + .m_axi_rlast(m_axi_rlast_mux), + .m_axi_ruser(m_axi_ruser_mux), + .m_axi_rvalid(m_axi_rvalid_mux), + .m_axi_rready(m_axi_rready_mux) + ); + end // s_ifaces + + for (n = 0; n < M_COUNT; n = n + 1) begin : m_ifaces + // in-flight transaction count + wire trans_start; + wire trans_complete; + reg [$clog2(M_ISSUE[n*32 +: 32]+1)-1:0] trans_count_reg = 0; + + wire trans_limit = trans_count_reg >= M_ISSUE[n*32 +: 32] && !trans_complete; + + always @(posedge clk) begin + if (rst) begin + trans_count_reg <= 0; + end else begin + if (trans_start && !trans_complete) begin + trans_count_reg <= trans_count_reg + 1; + end else if (!trans_start && trans_complete) begin + trans_count_reg <= trans_count_reg - 1; + end + end + end + + // address arbitration + wire [S_COUNT-1:0] a_request; + wire [S_COUNT-1:0] a_acknowledge; + wire [S_COUNT-1:0] a_grant; + wire a_grant_valid; + wire [CL_S_COUNT-1:0] a_grant_encoded; + + arbiter #( + .PORTS(S_COUNT), + .ARB_TYPE_ROUND_ROBIN(1), + .ARB_BLOCK(1), + .ARB_BLOCK_ACK(1), + .ARB_LSB_HIGH_PRIORITY(1) + ) + a_arb_inst ( + .clk(clk), + .rst(rst), + .request(a_request), + .acknowledge(a_acknowledge), + .grant(a_grant), + .grant_valid(a_grant_valid), + .grant_encoded(a_grant_encoded) + ); + + // address mux + wire [M_ID_WIDTH-1:0] s_axi_arid_mux = int_s_axi_arid[a_grant_encoded*S_ID_WIDTH +: S_ID_WIDTH] | (a_grant_encoded << S_ID_WIDTH); + wire [ADDR_WIDTH-1:0] s_axi_araddr_mux = int_s_axi_araddr[a_grant_encoded*ADDR_WIDTH +: ADDR_WIDTH]; + wire [7:0] s_axi_arlen_mux = int_s_axi_arlen[a_grant_encoded*8 +: 8]; + wire [2:0] s_axi_arsize_mux = int_s_axi_arsize[a_grant_encoded*3 +: 3]; + wire [1:0] s_axi_arburst_mux = int_s_axi_arburst[a_grant_encoded*2 +: 2]; + wire s_axi_arlock_mux = int_s_axi_arlock[a_grant_encoded]; + wire [3:0] s_axi_arcache_mux = int_s_axi_arcache[a_grant_encoded*4 +: 4]; + wire [2:0] s_axi_arprot_mux = int_s_axi_arprot[a_grant_encoded*3 +: 3]; + wire [3:0] s_axi_arqos_mux = int_s_axi_arqos[a_grant_encoded*4 +: 4]; + wire [3:0] s_axi_arregion_mux = int_s_axi_arregion[a_grant_encoded*4 +: 4]; + wire [ARUSER_WIDTH-1:0] s_axi_aruser_mux = int_s_axi_aruser[a_grant_encoded*ARUSER_WIDTH +: ARUSER_WIDTH]; + wire s_axi_arvalid_mux = int_axi_arvalid[a_grant_encoded*M_COUNT+n] && a_grant_valid; + wire s_axi_arready_mux; + + assign int_axi_arready[n*S_COUNT +: S_COUNT] = (a_grant_valid && s_axi_arready_mux) << a_grant_encoded; + + for (m = 0; m < S_COUNT; m = m + 1) begin + assign a_request[m] = int_axi_arvalid[m*M_COUNT+n] && !a_grant[m] && !trans_limit; + assign a_acknowledge[m] = a_grant[m] && int_axi_arvalid[m*M_COUNT+n] && s_axi_arready_mux; + end + + assign trans_start = s_axi_arvalid_mux && s_axi_arready_mux && a_grant_valid; + + // read response forwarding + wire [CL_S_COUNT-1:0] r_select = m_axi_rid[n*M_ID_WIDTH +: M_ID_WIDTH] >> S_ID_WIDTH; + + assign int_axi_rvalid[n*S_COUNT +: S_COUNT] = int_m_axi_rvalid[n] << r_select; + assign int_m_axi_rready[n] = int_axi_rready[r_select*M_COUNT+n]; + + assign trans_complete = int_m_axi_rvalid[n] && int_m_axi_rready[n] && int_m_axi_rlast[n]; + + // M side register + axi_register_rd #( + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .ID_WIDTH(M_ID_WIDTH), + .ARUSER_ENABLE(ARUSER_ENABLE), + .ARUSER_WIDTH(ARUSER_WIDTH), + .RUSER_ENABLE(RUSER_ENABLE), + .RUSER_WIDTH(RUSER_WIDTH), + .AR_REG_TYPE(M_AR_REG_TYPE[n*2 +: 2]), + .R_REG_TYPE(M_R_REG_TYPE[n*2 +: 2]) + ) + reg_inst ( + .clk(clk), + .rst(rst), + .s_axi_arid(s_axi_arid_mux), + .s_axi_araddr(s_axi_araddr_mux), + .s_axi_arlen(s_axi_arlen_mux), + .s_axi_arsize(s_axi_arsize_mux), + .s_axi_arburst(s_axi_arburst_mux), + .s_axi_arlock(s_axi_arlock_mux), + .s_axi_arcache(s_axi_arcache_mux), + .s_axi_arprot(s_axi_arprot_mux), + .s_axi_arqos(s_axi_arqos_mux), + .s_axi_arregion(s_axi_arregion_mux), + .s_axi_aruser(s_axi_aruser_mux), + .s_axi_arvalid(s_axi_arvalid_mux), + .s_axi_arready(s_axi_arready_mux), + .s_axi_rid(int_m_axi_rid[n*M_ID_WIDTH +: M_ID_WIDTH]), + .s_axi_rdata(int_m_axi_rdata[n*DATA_WIDTH +: DATA_WIDTH]), + .s_axi_rresp(int_m_axi_rresp[n*2 +: 2]), + .s_axi_rlast(int_m_axi_rlast[n]), + .s_axi_ruser(int_m_axi_ruser[n*RUSER_WIDTH +: RUSER_WIDTH]), + .s_axi_rvalid(int_m_axi_rvalid[n]), + .s_axi_rready(int_m_axi_rready[n]), + .m_axi_arid(m_axi_arid[n*M_ID_WIDTH +: M_ID_WIDTH]), + .m_axi_araddr(m_axi_araddr[n*ADDR_WIDTH +: ADDR_WIDTH]), + .m_axi_arlen(m_axi_arlen[n*8 +: 8]), + .m_axi_arsize(m_axi_arsize[n*3 +: 3]), + .m_axi_arburst(m_axi_arburst[n*2 +: 2]), + .m_axi_arlock(m_axi_arlock[n]), + .m_axi_arcache(m_axi_arcache[n*4 +: 4]), + .m_axi_arprot(m_axi_arprot[n*3 +: 3]), + .m_axi_arqos(m_axi_arqos[n*4 +: 4]), + .m_axi_arregion(m_axi_arregion[n*4 +: 4]), + .m_axi_aruser(m_axi_aruser[n*ARUSER_WIDTH +: ARUSER_WIDTH]), + .m_axi_arvalid(m_axi_arvalid[n]), + .m_axi_arready(m_axi_arready[n]), + .m_axi_rid(m_axi_rid[n*M_ID_WIDTH +: M_ID_WIDTH]), + .m_axi_rdata(m_axi_rdata[n*DATA_WIDTH +: DATA_WIDTH]), + .m_axi_rresp(m_axi_rresp[n*2 +: 2]), + .m_axi_rlast(m_axi_rlast[n]), + .m_axi_ruser(m_axi_ruser[n*RUSER_WIDTH +: RUSER_WIDTH]), + .m_axi_rvalid(m_axi_rvalid[n]), + .m_axi_rready(m_axi_rready[n]) + ); + end // m_ifaces + +endgenerate + +endmodule + +`resetall diff --git a/xls/modules/zstd/external/axi_crossbar_wr.v b/xls/modules/zstd/external/axi_crossbar_wr.v new file mode 100644 index 0000000000..5f55665351 --- /dev/null +++ b/xls/modules/zstd/external/axi_crossbar_wr.v @@ -0,0 +1,678 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 crossbar (write) + */ +module axi_crossbar_wr # +( + // Number of AXI inputs (slave interfaces) + parameter S_COUNT = 4, + // Number of AXI outputs (master interfaces) + parameter M_COUNT = 4, + // Width of data bus in bits + parameter DATA_WIDTH = 32, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) + parameter STRB_WIDTH = (DATA_WIDTH/8), + // Input ID field width (from AXI masters) + parameter S_ID_WIDTH = 8, + // Output ID field width (towards AXI slaves) + // Additional bits required for response routing + parameter M_ID_WIDTH = S_ID_WIDTH+$clog2(S_COUNT), + // Propagate awuser signal + parameter AWUSER_ENABLE = 0, + // Width of awuser signal + parameter AWUSER_WIDTH = 1, + // Propagate wuser signal + parameter WUSER_ENABLE = 0, + // Width of wuser signal + parameter WUSER_WIDTH = 1, + // Propagate buser signal + parameter BUSER_ENABLE = 0, + // Width of buser signal + parameter BUSER_WIDTH = 1, + // Number of concurrent unique IDs for each slave interface + // S_COUNT concatenated fields of 32 bits + parameter S_THREADS = {S_COUNT{32'd2}}, + // Number of concurrent operations for each slave interface + // S_COUNT concatenated fields of 32 bits + parameter S_ACCEPT = {S_COUNT{32'd16}}, + // Number of regions per master interface + parameter M_REGIONS = 1, + // Master interface base addresses + // M_COUNT concatenated fields of M_REGIONS concatenated fields of ADDR_WIDTH bits + // set to zero for default addressing based on M_ADDR_WIDTH + parameter M_BASE_ADDR = 0, + // Master interface address widths + // M_COUNT concatenated fields of M_REGIONS concatenated fields of 32 bits + parameter M_ADDR_WIDTH = {M_COUNT{{M_REGIONS{32'd24}}}}, + // Write connections between interfaces + // M_COUNT concatenated fields of S_COUNT bits + parameter M_CONNECT = {M_COUNT{{S_COUNT{1'b1}}}}, + // Number of concurrent operations for each master interface + // M_COUNT concatenated fields of 32 bits + parameter M_ISSUE = {M_COUNT{32'd4}}, + // Secure master (fail operations based on awprot/arprot) + // M_COUNT bits + parameter M_SECURE = {M_COUNT{1'b0}}, + // Slave interface AW channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_AW_REG_TYPE = {S_COUNT{2'd0}}, + // Slave interface W channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_W_REG_TYPE = {S_COUNT{2'd0}}, + // Slave interface B channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S_B_REG_TYPE = {S_COUNT{2'd1}}, + // Master interface AW channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_AW_REG_TYPE = {M_COUNT{2'd1}}, + // Master interface W channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_W_REG_TYPE = {M_COUNT{2'd2}}, + // Master interface B channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M_B_REG_TYPE = {M_COUNT{2'd0}} +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interfaces + */ + input wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_awid, + input wire [S_COUNT*ADDR_WIDTH-1:0] s_axi_awaddr, + input wire [S_COUNT*8-1:0] s_axi_awlen, + input wire [S_COUNT*3-1:0] s_axi_awsize, + input wire [S_COUNT*2-1:0] s_axi_awburst, + input wire [S_COUNT-1:0] s_axi_awlock, + input wire [S_COUNT*4-1:0] s_axi_awcache, + input wire [S_COUNT*3-1:0] s_axi_awprot, + input wire [S_COUNT*4-1:0] s_axi_awqos, + input wire [S_COUNT*AWUSER_WIDTH-1:0] s_axi_awuser, + input wire [S_COUNT-1:0] s_axi_awvalid, + output wire [S_COUNT-1:0] s_axi_awready, + input wire [S_COUNT*DATA_WIDTH-1:0] s_axi_wdata, + input wire [S_COUNT*STRB_WIDTH-1:0] s_axi_wstrb, + input wire [S_COUNT-1:0] s_axi_wlast, + input wire [S_COUNT*WUSER_WIDTH-1:0] s_axi_wuser, + input wire [S_COUNT-1:0] s_axi_wvalid, + output wire [S_COUNT-1:0] s_axi_wready, + output wire [S_COUNT*S_ID_WIDTH-1:0] s_axi_bid, + output wire [S_COUNT*2-1:0] s_axi_bresp, + output wire [S_COUNT*BUSER_WIDTH-1:0] s_axi_buser, + output wire [S_COUNT-1:0] s_axi_bvalid, + input wire [S_COUNT-1:0] s_axi_bready, + + /* + * AXI master interfaces + */ + output wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_awid, + output wire [M_COUNT*ADDR_WIDTH-1:0] m_axi_awaddr, + output wire [M_COUNT*8-1:0] m_axi_awlen, + output wire [M_COUNT*3-1:0] m_axi_awsize, + output wire [M_COUNT*2-1:0] m_axi_awburst, + output wire [M_COUNT-1:0] m_axi_awlock, + output wire [M_COUNT*4-1:0] m_axi_awcache, + output wire [M_COUNT*3-1:0] m_axi_awprot, + output wire [M_COUNT*4-1:0] m_axi_awqos, + output wire [M_COUNT*4-1:0] m_axi_awregion, + output wire [M_COUNT*AWUSER_WIDTH-1:0] m_axi_awuser, + output wire [M_COUNT-1:0] m_axi_awvalid, + input wire [M_COUNT-1:0] m_axi_awready, + output wire [M_COUNT*DATA_WIDTH-1:0] m_axi_wdata, + output wire [M_COUNT*STRB_WIDTH-1:0] m_axi_wstrb, + output wire [M_COUNT-1:0] m_axi_wlast, + output wire [M_COUNT*WUSER_WIDTH-1:0] m_axi_wuser, + output wire [M_COUNT-1:0] m_axi_wvalid, + input wire [M_COUNT-1:0] m_axi_wready, + input wire [M_COUNT*M_ID_WIDTH-1:0] m_axi_bid, + input wire [M_COUNT*2-1:0] m_axi_bresp, + input wire [M_COUNT*BUSER_WIDTH-1:0] m_axi_buser, + input wire [M_COUNT-1:0] m_axi_bvalid, + output wire [M_COUNT-1:0] m_axi_bready +); + +parameter CL_S_COUNT = $clog2(S_COUNT); +parameter CL_M_COUNT = $clog2(M_COUNT); +parameter M_COUNT_P1 = M_COUNT+1; +parameter CL_M_COUNT_P1 = $clog2(M_COUNT_P1); + +integer i; + +// check configuration +initial begin + if (M_ID_WIDTH < S_ID_WIDTH+$clog2(S_COUNT)) begin + $error("Error: M_ID_WIDTH must be at least $clog2(S_COUNT) larger than S_ID_WIDTH (instance %m)"); + $finish; + end + + for (i = 0; i < M_COUNT*M_REGIONS; i = i + 1) begin + if (M_ADDR_WIDTH[i*32 +: 32] && (M_ADDR_WIDTH[i*32 +: 32] < 12 || M_ADDR_WIDTH[i*32 +: 32] > ADDR_WIDTH)) begin + $error("Error: value out of range (instance %m)"); + $finish; + end + end +end + +wire [S_COUNT*S_ID_WIDTH-1:0] int_s_axi_awid; +wire [S_COUNT*ADDR_WIDTH-1:0] int_s_axi_awaddr; +wire [S_COUNT*8-1:0] int_s_axi_awlen; +wire [S_COUNT*3-1:0] int_s_axi_awsize; +wire [S_COUNT*2-1:0] int_s_axi_awburst; +wire [S_COUNT-1:0] int_s_axi_awlock; +wire [S_COUNT*4-1:0] int_s_axi_awcache; +wire [S_COUNT*3-1:0] int_s_axi_awprot; +wire [S_COUNT*4-1:0] int_s_axi_awqos; +wire [S_COUNT*4-1:0] int_s_axi_awregion; +wire [S_COUNT*AWUSER_WIDTH-1:0] int_s_axi_awuser; +wire [S_COUNT-1:0] int_s_axi_awvalid; +wire [S_COUNT-1:0] int_s_axi_awready; + +wire [S_COUNT*M_COUNT-1:0] int_axi_awvalid; +wire [M_COUNT*S_COUNT-1:0] int_axi_awready; + +wire [S_COUNT*DATA_WIDTH-1:0] int_s_axi_wdata; +wire [S_COUNT*STRB_WIDTH-1:0] int_s_axi_wstrb; +wire [S_COUNT-1:0] int_s_axi_wlast; +wire [S_COUNT*WUSER_WIDTH-1:0] int_s_axi_wuser; +wire [S_COUNT-1:0] int_s_axi_wvalid; +wire [S_COUNT-1:0] int_s_axi_wready; + +wire [S_COUNT*M_COUNT-1:0] int_axi_wvalid; +wire [M_COUNT*S_COUNT-1:0] int_axi_wready; + +wire [M_COUNT*M_ID_WIDTH-1:0] int_m_axi_bid; +wire [M_COUNT*2-1:0] int_m_axi_bresp; +wire [M_COUNT*BUSER_WIDTH-1:0] int_m_axi_buser; +wire [M_COUNT-1:0] int_m_axi_bvalid; +wire [M_COUNT-1:0] int_m_axi_bready; + +wire [M_COUNT*S_COUNT-1:0] int_axi_bvalid; +wire [S_COUNT*M_COUNT-1:0] int_axi_bready; + +generate + + genvar m, n; + + for (m = 0; m < S_COUNT; m = m + 1) begin : s_ifaces + // address decode and admission control + wire [CL_M_COUNT-1:0] a_select; + + wire m_axi_avalid; + wire m_axi_aready; + + wire [CL_M_COUNT-1:0] m_wc_select; + wire m_wc_decerr; + wire m_wc_valid; + wire m_wc_ready; + + wire m_rc_decerr; + wire m_rc_valid; + wire m_rc_ready; + + wire [S_ID_WIDTH-1:0] s_cpl_id; + wire s_cpl_valid; + + axi_crossbar_addr #( + .S(m), + .S_COUNT(S_COUNT), + .M_COUNT(M_COUNT), + .ADDR_WIDTH(ADDR_WIDTH), + .ID_WIDTH(S_ID_WIDTH), + .S_THREADS(S_THREADS[m*32 +: 32]), + .S_ACCEPT(S_ACCEPT[m*32 +: 32]), + .M_REGIONS(M_REGIONS), + .M_BASE_ADDR(M_BASE_ADDR), + .M_ADDR_WIDTH(M_ADDR_WIDTH), + .M_CONNECT(M_CONNECT), + .M_SECURE(M_SECURE), + .WC_OUTPUT(1) + ) + addr_inst ( + .clk(clk), + .rst(rst), + + /* + * Address input + */ + .s_axi_aid(int_s_axi_awid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .s_axi_aaddr(int_s_axi_awaddr[m*ADDR_WIDTH +: ADDR_WIDTH]), + .s_axi_aprot(int_s_axi_awprot[m*3 +: 3]), + .s_axi_aqos(int_s_axi_awqos[m*4 +: 4]), + .s_axi_avalid(int_s_axi_awvalid[m]), + .s_axi_aready(int_s_axi_awready[m]), + + /* + * Address output + */ + .m_axi_aregion(int_s_axi_awregion[m*4 +: 4]), + .m_select(a_select), + .m_axi_avalid(m_axi_avalid), + .m_axi_aready(m_axi_aready), + + /* + * Write command output + */ + .m_wc_select(m_wc_select), + .m_wc_decerr(m_wc_decerr), + .m_wc_valid(m_wc_valid), + .m_wc_ready(m_wc_ready), + + /* + * Response command output + */ + .m_rc_decerr(m_rc_decerr), + .m_rc_valid(m_rc_valid), + .m_rc_ready(m_rc_ready), + + /* + * Completion input + */ + .s_cpl_id(s_cpl_id), + .s_cpl_valid(s_cpl_valid) + ); + + assign int_axi_awvalid[m*M_COUNT +: M_COUNT] = m_axi_avalid << a_select; + assign m_axi_aready = int_axi_awready[a_select*S_COUNT+m]; + + // write command handling + reg [CL_M_COUNT-1:0] w_select_reg = 0, w_select_next; + reg w_drop_reg = 1'b0, w_drop_next; + reg w_select_valid_reg = 1'b0, w_select_valid_next; + + assign m_wc_ready = !w_select_valid_reg; + + always @* begin + w_select_next = w_select_reg; + w_drop_next = w_drop_reg && !(int_s_axi_wvalid[m] && int_s_axi_wready[m] && int_s_axi_wlast[m]); + w_select_valid_next = w_select_valid_reg && !(int_s_axi_wvalid[m] && int_s_axi_wready[m] && int_s_axi_wlast[m]); + + if (m_wc_valid && !w_select_valid_reg) begin + w_select_next = m_wc_select; + w_drop_next = m_wc_decerr; + w_select_valid_next = m_wc_valid; + end + end + + always @(posedge clk) begin + if (rst) begin + w_select_valid_reg <= 1'b0; + end else begin + w_select_valid_reg <= w_select_valid_next; + end + + w_select_reg <= w_select_next; + w_drop_reg <= w_drop_next; + end + + // write data forwarding + assign int_axi_wvalid[m*M_COUNT +: M_COUNT] = (int_s_axi_wvalid[m] && w_select_valid_reg && !w_drop_reg) << w_select_reg; + assign int_s_axi_wready[m] = int_axi_wready[w_select_reg*S_COUNT+m] || w_drop_reg; + + // decode error handling + reg [S_ID_WIDTH-1:0] decerr_m_axi_bid_reg = {S_ID_WIDTH{1'b0}}, decerr_m_axi_bid_next; + reg decerr_m_axi_bvalid_reg = 1'b0, decerr_m_axi_bvalid_next; + wire decerr_m_axi_bready; + + assign m_rc_ready = !decerr_m_axi_bvalid_reg; + + always @* begin + decerr_m_axi_bid_next = decerr_m_axi_bid_reg; + decerr_m_axi_bvalid_next = decerr_m_axi_bvalid_reg; + + if (decerr_m_axi_bvalid_reg) begin + if (decerr_m_axi_bready) begin + decerr_m_axi_bvalid_next = 1'b0; + end + end else if (m_rc_valid && m_rc_ready) begin + decerr_m_axi_bid_next = int_s_axi_awid[m*S_ID_WIDTH +: S_ID_WIDTH]; + decerr_m_axi_bvalid_next = 1'b1; + end + end + + always @(posedge clk) begin + if (rst) begin + decerr_m_axi_bvalid_reg <= 1'b0; + end else begin + decerr_m_axi_bvalid_reg <= decerr_m_axi_bvalid_next; + end + + decerr_m_axi_bid_reg <= decerr_m_axi_bid_next; + end + + // write response arbitration + wire [M_COUNT_P1-1:0] b_request; + wire [M_COUNT_P1-1:0] b_acknowledge; + wire [M_COUNT_P1-1:0] b_grant; + wire b_grant_valid; + wire [CL_M_COUNT_P1-1:0] b_grant_encoded; + + arbiter #( + .PORTS(M_COUNT_P1), + .ARB_TYPE_ROUND_ROBIN(1), + .ARB_BLOCK(1), + .ARB_BLOCK_ACK(1), + .ARB_LSB_HIGH_PRIORITY(1) + ) + b_arb_inst ( + .clk(clk), + .rst(rst), + .request(b_request), + .acknowledge(b_acknowledge), + .grant(b_grant), + .grant_valid(b_grant_valid), + .grant_encoded(b_grant_encoded) + ); + + // write response mux + wire [S_ID_WIDTH-1:0] m_axi_bid_mux = {decerr_m_axi_bid_reg, int_m_axi_bid} >> b_grant_encoded*M_ID_WIDTH; + wire [1:0] m_axi_bresp_mux = {2'b11, int_m_axi_bresp} >> b_grant_encoded*2; + wire [BUSER_WIDTH-1:0] m_axi_buser_mux = {{BUSER_WIDTH{1'b0}}, int_m_axi_buser} >> b_grant_encoded*BUSER_WIDTH; + wire m_axi_bvalid_mux = ({decerr_m_axi_bvalid_reg, int_m_axi_bvalid} >> b_grant_encoded) & b_grant_valid; + wire m_axi_bready_mux; + + assign int_axi_bready[m*M_COUNT +: M_COUNT] = (b_grant_valid && m_axi_bready_mux) << b_grant_encoded; + assign decerr_m_axi_bready = (b_grant_valid && m_axi_bready_mux) && (b_grant_encoded == M_COUNT_P1-1); + + for (n = 0; n < M_COUNT; n = n + 1) begin + assign b_request[n] = int_axi_bvalid[n*S_COUNT+m] && !b_grant[n]; + assign b_acknowledge[n] = b_grant[n] && int_axi_bvalid[n*S_COUNT+m] && m_axi_bready_mux; + end + + assign b_request[M_COUNT_P1-1] = decerr_m_axi_bvalid_reg && !b_grant[M_COUNT_P1-1]; + assign b_acknowledge[M_COUNT_P1-1] = b_grant[M_COUNT_P1-1] && decerr_m_axi_bvalid_reg && m_axi_bready_mux; + + assign s_cpl_id = m_axi_bid_mux; + assign s_cpl_valid = m_axi_bvalid_mux && m_axi_bready_mux; + + // S side register + axi_register_wr #( + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .ID_WIDTH(S_ID_WIDTH), + .AWUSER_ENABLE(AWUSER_ENABLE), + .AWUSER_WIDTH(AWUSER_WIDTH), + .WUSER_ENABLE(WUSER_ENABLE), + .WUSER_WIDTH(WUSER_WIDTH), + .BUSER_ENABLE(BUSER_ENABLE), + .BUSER_WIDTH(BUSER_WIDTH), + .AW_REG_TYPE(S_AW_REG_TYPE[m*2 +: 2]), + .W_REG_TYPE(S_W_REG_TYPE[m*2 +: 2]), + .B_REG_TYPE(S_B_REG_TYPE[m*2 +: 2]) + ) + reg_inst ( + .clk(clk), + .rst(rst), + .s_axi_awid(s_axi_awid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .s_axi_awaddr(s_axi_awaddr[m*ADDR_WIDTH +: ADDR_WIDTH]), + .s_axi_awlen(s_axi_awlen[m*8 +: 8]), + .s_axi_awsize(s_axi_awsize[m*3 +: 3]), + .s_axi_awburst(s_axi_awburst[m*2 +: 2]), + .s_axi_awlock(s_axi_awlock[m]), + .s_axi_awcache(s_axi_awcache[m*4 +: 4]), + .s_axi_awprot(s_axi_awprot[m*3 +: 3]), + .s_axi_awqos(s_axi_awqos[m*4 +: 4]), + .s_axi_awregion(4'd0), + .s_axi_awuser(s_axi_awuser[m*AWUSER_WIDTH +: AWUSER_WIDTH]), + .s_axi_awvalid(s_axi_awvalid[m]), + .s_axi_awready(s_axi_awready[m]), + .s_axi_wdata(s_axi_wdata[m*DATA_WIDTH +: DATA_WIDTH]), + .s_axi_wstrb(s_axi_wstrb[m*STRB_WIDTH +: STRB_WIDTH]), + .s_axi_wlast(s_axi_wlast[m]), + .s_axi_wuser(s_axi_wuser[m*WUSER_WIDTH +: WUSER_WIDTH]), + .s_axi_wvalid(s_axi_wvalid[m]), + .s_axi_wready(s_axi_wready[m]), + .s_axi_bid(s_axi_bid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .s_axi_bresp(s_axi_bresp[m*2 +: 2]), + .s_axi_buser(s_axi_buser[m*BUSER_WIDTH +: BUSER_WIDTH]), + .s_axi_bvalid(s_axi_bvalid[m]), + .s_axi_bready(s_axi_bready[m]), + .m_axi_awid(int_s_axi_awid[m*S_ID_WIDTH +: S_ID_WIDTH]), + .m_axi_awaddr(int_s_axi_awaddr[m*ADDR_WIDTH +: ADDR_WIDTH]), + .m_axi_awlen(int_s_axi_awlen[m*8 +: 8]), + .m_axi_awsize(int_s_axi_awsize[m*3 +: 3]), + .m_axi_awburst(int_s_axi_awburst[m*2 +: 2]), + .m_axi_awlock(int_s_axi_awlock[m]), + .m_axi_awcache(int_s_axi_awcache[m*4 +: 4]), + .m_axi_awprot(int_s_axi_awprot[m*3 +: 3]), + .m_axi_awqos(int_s_axi_awqos[m*4 +: 4]), + .m_axi_awregion(), + .m_axi_awuser(int_s_axi_awuser[m*AWUSER_WIDTH +: AWUSER_WIDTH]), + .m_axi_awvalid(int_s_axi_awvalid[m]), + .m_axi_awready(int_s_axi_awready[m]), + .m_axi_wdata(int_s_axi_wdata[m*DATA_WIDTH +: DATA_WIDTH]), + .m_axi_wstrb(int_s_axi_wstrb[m*STRB_WIDTH +: STRB_WIDTH]), + .m_axi_wlast(int_s_axi_wlast[m]), + .m_axi_wuser(int_s_axi_wuser[m*WUSER_WIDTH +: WUSER_WIDTH]), + .m_axi_wvalid(int_s_axi_wvalid[m]), + .m_axi_wready(int_s_axi_wready[m]), + .m_axi_bid(m_axi_bid_mux), + .m_axi_bresp(m_axi_bresp_mux), + .m_axi_buser(m_axi_buser_mux), + .m_axi_bvalid(m_axi_bvalid_mux), + .m_axi_bready(m_axi_bready_mux) + ); + end // s_ifaces + + for (n = 0; n < M_COUNT; n = n + 1) begin : m_ifaces + // in-flight transaction count + wire trans_start; + wire trans_complete; + reg [$clog2(M_ISSUE[n*32 +: 32]+1)-1:0] trans_count_reg = 0; + + wire trans_limit = trans_count_reg >= M_ISSUE[n*32 +: 32] && !trans_complete; + + always @(posedge clk) begin + if (rst) begin + trans_count_reg <= 0; + end else begin + if (trans_start && !trans_complete) begin + trans_count_reg <= trans_count_reg + 1; + end else if (!trans_start && trans_complete) begin + trans_count_reg <= trans_count_reg - 1; + end + end + end + + // address arbitration + reg [CL_S_COUNT-1:0] w_select_reg = 0, w_select_next; + reg w_select_valid_reg = 1'b0, w_select_valid_next; + reg w_select_new_reg = 1'b0, w_select_new_next; + + wire [S_COUNT-1:0] a_request; + wire [S_COUNT-1:0] a_acknowledge; + wire [S_COUNT-1:0] a_grant; + wire a_grant_valid; + wire [CL_S_COUNT-1:0] a_grant_encoded; + + arbiter #( + .PORTS(S_COUNT), + .ARB_TYPE_ROUND_ROBIN(1), + .ARB_BLOCK(1), + .ARB_BLOCK_ACK(1), + .ARB_LSB_HIGH_PRIORITY(1) + ) + a_arb_inst ( + .clk(clk), + .rst(rst), + .request(a_request), + .acknowledge(a_acknowledge), + .grant(a_grant), + .grant_valid(a_grant_valid), + .grant_encoded(a_grant_encoded) + ); + + // address mux + wire [M_ID_WIDTH-1:0] s_axi_awid_mux = int_s_axi_awid[a_grant_encoded*S_ID_WIDTH +: S_ID_WIDTH] | (a_grant_encoded << S_ID_WIDTH); + wire [ADDR_WIDTH-1:0] s_axi_awaddr_mux = int_s_axi_awaddr[a_grant_encoded*ADDR_WIDTH +: ADDR_WIDTH]; + wire [7:0] s_axi_awlen_mux = int_s_axi_awlen[a_grant_encoded*8 +: 8]; + wire [2:0] s_axi_awsize_mux = int_s_axi_awsize[a_grant_encoded*3 +: 3]; + wire [1:0] s_axi_awburst_mux = int_s_axi_awburst[a_grant_encoded*2 +: 2]; + wire s_axi_awlock_mux = int_s_axi_awlock[a_grant_encoded]; + wire [3:0] s_axi_awcache_mux = int_s_axi_awcache[a_grant_encoded*4 +: 4]; + wire [2:0] s_axi_awprot_mux = int_s_axi_awprot[a_grant_encoded*3 +: 3]; + wire [3:0] s_axi_awqos_mux = int_s_axi_awqos[a_grant_encoded*4 +: 4]; + wire [3:0] s_axi_awregion_mux = int_s_axi_awregion[a_grant_encoded*4 +: 4]; + wire [AWUSER_WIDTH-1:0] s_axi_awuser_mux = int_s_axi_awuser[a_grant_encoded*AWUSER_WIDTH +: AWUSER_WIDTH]; + wire s_axi_awvalid_mux = int_axi_awvalid[a_grant_encoded*M_COUNT+n] && a_grant_valid; + wire s_axi_awready_mux; + + assign int_axi_awready[n*S_COUNT +: S_COUNT] = (a_grant_valid && s_axi_awready_mux) << a_grant_encoded; + + for (m = 0; m < S_COUNT; m = m + 1) begin + assign a_request[m] = int_axi_awvalid[m*M_COUNT+n] && !a_grant[m] && !trans_limit && !w_select_valid_next; + assign a_acknowledge[m] = a_grant[m] && int_axi_awvalid[m*M_COUNT+n] && s_axi_awready_mux; + end + + assign trans_start = s_axi_awvalid_mux && s_axi_awready_mux && a_grant_valid; + + // write data mux + wire [DATA_WIDTH-1:0] s_axi_wdata_mux = int_s_axi_wdata[w_select_reg*DATA_WIDTH +: DATA_WIDTH]; + wire [STRB_WIDTH-1:0] s_axi_wstrb_mux = int_s_axi_wstrb[w_select_reg*STRB_WIDTH +: STRB_WIDTH]; + wire s_axi_wlast_mux = int_s_axi_wlast[w_select_reg]; + wire [WUSER_WIDTH-1:0] s_axi_wuser_mux = int_s_axi_wuser[w_select_reg*WUSER_WIDTH +: WUSER_WIDTH]; + wire s_axi_wvalid_mux = int_axi_wvalid[w_select_reg*M_COUNT+n] && w_select_valid_reg; + wire s_axi_wready_mux; + + assign int_axi_wready[n*S_COUNT +: S_COUNT] = (w_select_valid_reg && s_axi_wready_mux) << w_select_reg; + + // write data routing + always @* begin + w_select_next = w_select_reg; + w_select_valid_next = w_select_valid_reg && !(s_axi_wvalid_mux && s_axi_wready_mux && s_axi_wlast_mux); + w_select_new_next = w_select_new_reg || !a_grant_valid || a_acknowledge; + + if (a_grant_valid && !w_select_valid_reg && w_select_new_reg) begin + w_select_next = a_grant_encoded; + w_select_valid_next = a_grant_valid; + w_select_new_next = 1'b0; + end + end + + always @(posedge clk) begin + if (rst) begin + w_select_valid_reg <= 1'b0; + w_select_new_reg <= 1'b1; + end else begin + w_select_valid_reg <= w_select_valid_next; + w_select_new_reg <= w_select_new_next; + end + + w_select_reg <= w_select_next; + end + + // write response forwarding + wire [CL_S_COUNT-1:0] b_select = m_axi_bid[n*M_ID_WIDTH +: M_ID_WIDTH] >> S_ID_WIDTH; + + assign int_axi_bvalid[n*S_COUNT +: S_COUNT] = int_m_axi_bvalid[n] << b_select; + assign int_m_axi_bready[n] = int_axi_bready[b_select*M_COUNT+n]; + + assign trans_complete = int_m_axi_bvalid[n] && int_m_axi_bready[n]; + + // M side register + axi_register_wr #( + .DATA_WIDTH(DATA_WIDTH), + .ADDR_WIDTH(ADDR_WIDTH), + .STRB_WIDTH(STRB_WIDTH), + .ID_WIDTH(M_ID_WIDTH), + .AWUSER_ENABLE(AWUSER_ENABLE), + .AWUSER_WIDTH(AWUSER_WIDTH), + .WUSER_ENABLE(WUSER_ENABLE), + .WUSER_WIDTH(WUSER_WIDTH), + .BUSER_ENABLE(BUSER_ENABLE), + .BUSER_WIDTH(BUSER_WIDTH), + .AW_REG_TYPE(M_AW_REG_TYPE[n*2 +: 2]), + .W_REG_TYPE(M_W_REG_TYPE[n*2 +: 2]), + .B_REG_TYPE(M_B_REG_TYPE[n*2 +: 2]) + ) + reg_inst ( + .clk(clk), + .rst(rst), + .s_axi_awid(s_axi_awid_mux), + .s_axi_awaddr(s_axi_awaddr_mux), + .s_axi_awlen(s_axi_awlen_mux), + .s_axi_awsize(s_axi_awsize_mux), + .s_axi_awburst(s_axi_awburst_mux), + .s_axi_awlock(s_axi_awlock_mux), + .s_axi_awcache(s_axi_awcache_mux), + .s_axi_awprot(s_axi_awprot_mux), + .s_axi_awqos(s_axi_awqos_mux), + .s_axi_awregion(s_axi_awregion_mux), + .s_axi_awuser(s_axi_awuser_mux), + .s_axi_awvalid(s_axi_awvalid_mux), + .s_axi_awready(s_axi_awready_mux), + .s_axi_wdata(s_axi_wdata_mux), + .s_axi_wstrb(s_axi_wstrb_mux), + .s_axi_wlast(s_axi_wlast_mux), + .s_axi_wuser(s_axi_wuser_mux), + .s_axi_wvalid(s_axi_wvalid_mux), + .s_axi_wready(s_axi_wready_mux), + .s_axi_bid(int_m_axi_bid[n*M_ID_WIDTH +: M_ID_WIDTH]), + .s_axi_bresp(int_m_axi_bresp[n*2 +: 2]), + .s_axi_buser(int_m_axi_buser[n*BUSER_WIDTH +: BUSER_WIDTH]), + .s_axi_bvalid(int_m_axi_bvalid[n]), + .s_axi_bready(int_m_axi_bready[n]), + .m_axi_awid(m_axi_awid[n*M_ID_WIDTH +: M_ID_WIDTH]), + .m_axi_awaddr(m_axi_awaddr[n*ADDR_WIDTH +: ADDR_WIDTH]), + .m_axi_awlen(m_axi_awlen[n*8 +: 8]), + .m_axi_awsize(m_axi_awsize[n*3 +: 3]), + .m_axi_awburst(m_axi_awburst[n*2 +: 2]), + .m_axi_awlock(m_axi_awlock[n]), + .m_axi_awcache(m_axi_awcache[n*4 +: 4]), + .m_axi_awprot(m_axi_awprot[n*3 +: 3]), + .m_axi_awqos(m_axi_awqos[n*4 +: 4]), + .m_axi_awregion(m_axi_awregion[n*4 +: 4]), + .m_axi_awuser(m_axi_awuser[n*AWUSER_WIDTH +: AWUSER_WIDTH]), + .m_axi_awvalid(m_axi_awvalid[n]), + .m_axi_awready(m_axi_awready[n]), + .m_axi_wdata(m_axi_wdata[n*DATA_WIDTH +: DATA_WIDTH]), + .m_axi_wstrb(m_axi_wstrb[n*STRB_WIDTH +: STRB_WIDTH]), + .m_axi_wlast(m_axi_wlast[n]), + .m_axi_wuser(m_axi_wuser[n*WUSER_WIDTH +: WUSER_WIDTH]), + .m_axi_wvalid(m_axi_wvalid[n]), + .m_axi_wready(m_axi_wready[n]), + .m_axi_bid(m_axi_bid[n*M_ID_WIDTH +: M_ID_WIDTH]), + .m_axi_bresp(m_axi_bresp[n*2 +: 2]), + .m_axi_buser(m_axi_buser[n*BUSER_WIDTH +: BUSER_WIDTH]), + .m_axi_bvalid(m_axi_bvalid[n]), + .m_axi_bready(m_axi_bready[n]) + ); + end // m_ifaces + +endgenerate + +endmodule + +`resetall diff --git a/xls/modules/zstd/axi_interconnect_wrapper.v b/xls/modules/zstd/external/axi_crossbar_wrapper.v similarity index 70% rename from xls/modules/zstd/axi_interconnect_wrapper.v rename to xls/modules/zstd/external/axi_crossbar_wrapper.v index 50017314f9..c244575e98 100644 --- a/xls/modules/zstd/axi_interconnect_wrapper.v +++ b/xls/modules/zstd/external/axi_crossbar_wrapper.v @@ -29,31 +29,150 @@ THE SOFTWARE. `default_nettype none /* - * AXI4 4x1 interconnect (wrapper) + * AXI4 4x1 crossbar (wrapper) */ -module axi_interconnect_wrapper # +module axi_crossbar_wrapper # ( + // Width of data bus in bits parameter DATA_WIDTH = 32, + // Width of address bus in bits parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) parameter STRB_WIDTH = (DATA_WIDTH/8), - parameter ID_WIDTH = 8, + // Input ID field width (from AXI masters) + parameter S_ID_WIDTH = 8, + // Output ID field width (towards AXI slaves) + // Additional bits required for response routing + parameter M_ID_WIDTH = S_ID_WIDTH+$clog2(S_COUNT), + // Propagate awuser signal parameter AWUSER_ENABLE = 0, + // Width of awuser signal parameter AWUSER_WIDTH = 1, + // Propagate wuser signal parameter WUSER_ENABLE = 0, + // Width of wuser signal parameter WUSER_WIDTH = 1, + // Propagate buser signal parameter BUSER_ENABLE = 0, + // Width of buser signal parameter BUSER_WIDTH = 1, + // Propagate aruser signal parameter ARUSER_ENABLE = 0, + // Width of aruser signal parameter ARUSER_WIDTH = 1, + // Propagate ruser signal parameter RUSER_ENABLE = 0, + // Width of ruser signal parameter RUSER_WIDTH = 1, - parameter FORWARD_ID = 0, + // Number of concurrent unique IDs + parameter S00_THREADS = 2, + // Number of concurrent operations + parameter S00_ACCEPT = 16, + // Number of concurrent unique IDs + parameter S01_THREADS = 2, + // Number of concurrent operations + parameter S01_ACCEPT = 16, + // Number of concurrent unique IDs + parameter S02_THREADS = 2, + // Number of concurrent operations + parameter S02_ACCEPT = 16, + // Number of concurrent unique IDs + parameter S03_THREADS = 2, + // Number of concurrent operations + parameter S03_ACCEPT = 16, + // Number of regions per master interface parameter M_REGIONS = 1, + // Master interface base addresses + // M_REGIONS concatenated fields of ADDR_WIDTH bits parameter M00_BASE_ADDR = 0, + // Master interface address widths + // M_REGIONS concatenated fields of 32 bits parameter M00_ADDR_WIDTH = {M_REGIONS{32'd24}}, + // Read connections between interfaces + // S_COUNT bits parameter M00_CONNECT_READ = 4'b1111, + // Write connections between interfaces + // S_COUNT bits parameter M00_CONNECT_WRITE = 4'b1111, - parameter M00_SECURE = 1'b0 + // Number of concurrent operations for each master interface + parameter M00_ISSUE = 4, + // Secure master (fail operations based on awprot/arprot) + parameter M00_SECURE = 0, + // Slave interface AW channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S00_AW_REG_TYPE = 0, + // Slave interface W channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S00_W_REG_TYPE = 0, + // Slave interface B channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S00_B_REG_TYPE = 1, + // Slave interface AR channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S00_AR_REG_TYPE = 0, + // Slave interface R channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S00_R_REG_TYPE = 2, + // Slave interface AW channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S01_AW_REG_TYPE = 0, + // Slave interface W channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S01_W_REG_TYPE = 0, + // Slave interface B channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S01_B_REG_TYPE = 1, + // Slave interface AR channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S01_AR_REG_TYPE = 0, + // Slave interface R channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S01_R_REG_TYPE = 2, + // Slave interface AW channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S02_AW_REG_TYPE = 0, + // Slave interface W channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S02_W_REG_TYPE = 0, + // Slave interface B channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S02_B_REG_TYPE = 1, + // Slave interface AR channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S02_AR_REG_TYPE = 0, + // Slave interface R channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S02_R_REG_TYPE = 2, + // Slave interface AW channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S03_AW_REG_TYPE = 0, + // Slave interface W channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S03_W_REG_TYPE = 0, + // Slave interface B channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S03_B_REG_TYPE = 1, + // Slave interface AR channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S03_AR_REG_TYPE = 0, + // Slave interface R channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter S03_R_REG_TYPE = 2, + // Master interface AW channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M00_AW_REG_TYPE = 1, + // Master interface W channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M00_W_REG_TYPE = 2, + // Master interface B channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M00_B_REG_TYPE = 0, + // Master interface AR channel register type (output) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M00_AR_REG_TYPE = 1, + // Master interface R channel register type (input) + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter M00_R_REG_TYPE = 0 ) ( input wire clk, @@ -62,7 +181,7 @@ module axi_interconnect_wrapper # /* * AXI slave interface */ - input wire [ID_WIDTH-1:0] s00_axi_awid, + input wire [S_ID_WIDTH-1:0] s00_axi_awid, input wire [ADDR_WIDTH-1:0] s00_axi_awaddr, input wire [7:0] s00_axi_awlen, input wire [2:0] s00_axi_awsize, @@ -80,12 +199,12 @@ module axi_interconnect_wrapper # input wire [WUSER_WIDTH-1:0] s00_axi_wuser, input wire s00_axi_wvalid, output wire s00_axi_wready, - output wire [ID_WIDTH-1:0] s00_axi_bid, + output wire [S_ID_WIDTH-1:0] s00_axi_bid, output wire [1:0] s00_axi_bresp, output wire [BUSER_WIDTH-1:0] s00_axi_buser, output wire s00_axi_bvalid, input wire s00_axi_bready, - input wire [ID_WIDTH-1:0] s00_axi_arid, + input wire [S_ID_WIDTH-1:0] s00_axi_arid, input wire [ADDR_WIDTH-1:0] s00_axi_araddr, input wire [7:0] s00_axi_arlen, input wire [2:0] s00_axi_arsize, @@ -97,7 +216,7 @@ module axi_interconnect_wrapper # input wire [ARUSER_WIDTH-1:0] s00_axi_aruser, input wire s00_axi_arvalid, output wire s00_axi_arready, - output wire [ID_WIDTH-1:0] s00_axi_rid, + output wire [S_ID_WIDTH-1:0] s00_axi_rid, output wire [DATA_WIDTH-1:0] s00_axi_rdata, output wire [1:0] s00_axi_rresp, output wire s00_axi_rlast, @@ -105,7 +224,7 @@ module axi_interconnect_wrapper # output wire s00_axi_rvalid, input wire s00_axi_rready, - input wire [ID_WIDTH-1:0] s01_axi_awid, + input wire [S_ID_WIDTH-1:0] s01_axi_awid, input wire [ADDR_WIDTH-1:0] s01_axi_awaddr, input wire [7:0] s01_axi_awlen, input wire [2:0] s01_axi_awsize, @@ -123,12 +242,12 @@ module axi_interconnect_wrapper # input wire [WUSER_WIDTH-1:0] s01_axi_wuser, input wire s01_axi_wvalid, output wire s01_axi_wready, - output wire [ID_WIDTH-1:0] s01_axi_bid, + output wire [S_ID_WIDTH-1:0] s01_axi_bid, output wire [1:0] s01_axi_bresp, output wire [BUSER_WIDTH-1:0] s01_axi_buser, output wire s01_axi_bvalid, input wire s01_axi_bready, - input wire [ID_WIDTH-1:0] s01_axi_arid, + input wire [S_ID_WIDTH-1:0] s01_axi_arid, input wire [ADDR_WIDTH-1:0] s01_axi_araddr, input wire [7:0] s01_axi_arlen, input wire [2:0] s01_axi_arsize, @@ -140,7 +259,7 @@ module axi_interconnect_wrapper # input wire [ARUSER_WIDTH-1:0] s01_axi_aruser, input wire s01_axi_arvalid, output wire s01_axi_arready, - output wire [ID_WIDTH-1:0] s01_axi_rid, + output wire [S_ID_WIDTH-1:0] s01_axi_rid, output wire [DATA_WIDTH-1:0] s01_axi_rdata, output wire [1:0] s01_axi_rresp, output wire s01_axi_rlast, @@ -148,7 +267,7 @@ module axi_interconnect_wrapper # output wire s01_axi_rvalid, input wire s01_axi_rready, - input wire [ID_WIDTH-1:0] s02_axi_awid, + input wire [S_ID_WIDTH-1:0] s02_axi_awid, input wire [ADDR_WIDTH-1:0] s02_axi_awaddr, input wire [7:0] s02_axi_awlen, input wire [2:0] s02_axi_awsize, @@ -166,12 +285,12 @@ module axi_interconnect_wrapper # input wire [WUSER_WIDTH-1:0] s02_axi_wuser, input wire s02_axi_wvalid, output wire s02_axi_wready, - output wire [ID_WIDTH-1:0] s02_axi_bid, + output wire [S_ID_WIDTH-1:0] s02_axi_bid, output wire [1:0] s02_axi_bresp, output wire [BUSER_WIDTH-1:0] s02_axi_buser, output wire s02_axi_bvalid, input wire s02_axi_bready, - input wire [ID_WIDTH-1:0] s02_axi_arid, + input wire [S_ID_WIDTH-1:0] s02_axi_arid, input wire [ADDR_WIDTH-1:0] s02_axi_araddr, input wire [7:0] s02_axi_arlen, input wire [2:0] s02_axi_arsize, @@ -183,7 +302,7 @@ module axi_interconnect_wrapper # input wire [ARUSER_WIDTH-1:0] s02_axi_aruser, input wire s02_axi_arvalid, output wire s02_axi_arready, - output wire [ID_WIDTH-1:0] s02_axi_rid, + output wire [S_ID_WIDTH-1:0] s02_axi_rid, output wire [DATA_WIDTH-1:0] s02_axi_rdata, output wire [1:0] s02_axi_rresp, output wire s02_axi_rlast, @@ -191,7 +310,7 @@ module axi_interconnect_wrapper # output wire s02_axi_rvalid, input wire s02_axi_rready, - input wire [ID_WIDTH-1:0] s03_axi_awid, + input wire [S_ID_WIDTH-1:0] s03_axi_awid, input wire [ADDR_WIDTH-1:0] s03_axi_awaddr, input wire [7:0] s03_axi_awlen, input wire [2:0] s03_axi_awsize, @@ -209,12 +328,12 @@ module axi_interconnect_wrapper # input wire [WUSER_WIDTH-1:0] s03_axi_wuser, input wire s03_axi_wvalid, output wire s03_axi_wready, - output wire [ID_WIDTH-1:0] s03_axi_bid, + output wire [S_ID_WIDTH-1:0] s03_axi_bid, output wire [1:0] s03_axi_bresp, output wire [BUSER_WIDTH-1:0] s03_axi_buser, output wire s03_axi_bvalid, input wire s03_axi_bready, - input wire [ID_WIDTH-1:0] s03_axi_arid, + input wire [S_ID_WIDTH-1:0] s03_axi_arid, input wire [ADDR_WIDTH-1:0] s03_axi_araddr, input wire [7:0] s03_axi_arlen, input wire [2:0] s03_axi_arsize, @@ -226,7 +345,7 @@ module axi_interconnect_wrapper # input wire [ARUSER_WIDTH-1:0] s03_axi_aruser, input wire s03_axi_arvalid, output wire s03_axi_arready, - output wire [ID_WIDTH-1:0] s03_axi_rid, + output wire [S_ID_WIDTH-1:0] s03_axi_rid, output wire [DATA_WIDTH-1:0] s03_axi_rdata, output wire [1:0] s03_axi_rresp, output wire s03_axi_rlast, @@ -237,7 +356,7 @@ module axi_interconnect_wrapper # /* * AXI master interface */ - output wire [ID_WIDTH-1:0] m00_axi_awid, + output wire [M_ID_WIDTH-1:0] m00_axi_awid, output wire [ADDR_WIDTH-1:0] m00_axi_awaddr, output wire [7:0] m00_axi_awlen, output wire [2:0] m00_axi_awsize, @@ -256,12 +375,12 @@ module axi_interconnect_wrapper # output wire [WUSER_WIDTH-1:0] m00_axi_wuser, output wire m00_axi_wvalid, input wire m00_axi_wready, - input wire [ID_WIDTH-1:0] m00_axi_bid, + input wire [M_ID_WIDTH-1:0] m00_axi_bid, input wire [1:0] m00_axi_bresp, input wire [BUSER_WIDTH-1:0] m00_axi_buser, input wire m00_axi_bvalid, output wire m00_axi_bready, - output wire [ID_WIDTH-1:0] m00_axi_arid, + output wire [M_ID_WIDTH-1:0] m00_axi_arid, output wire [ADDR_WIDTH-1:0] m00_axi_araddr, output wire [7:0] m00_axi_arlen, output wire [2:0] m00_axi_arsize, @@ -274,7 +393,7 @@ module axi_interconnect_wrapper # output wire [ARUSER_WIDTH-1:0] m00_axi_aruser, output wire m00_axi_arvalid, input wire m00_axi_arready, - input wire [ID_WIDTH-1:0] m00_axi_rid, + input wire [M_ID_WIDTH-1:0] m00_axi_rid, input wire [DATA_WIDTH-1:0] m00_axi_rdata, input wire [1:0] m00_axi_rresp, input wire m00_axi_rlast, @@ -299,17 +418,26 @@ function [S_COUNT-1:0] w_s(input [S_COUNT-1:0] val); w_s = val; endfunction +function [31:0] w_32(input [31:0] val); + w_32 = val; +endfunction + +function [1:0] w_2(input [1:0] val); + w_2 = val; +endfunction + function w_1(input val); w_1 = val; endfunction -axi_interconnect #( +axi_crossbar #( .S_COUNT(S_COUNT), .M_COUNT(M_COUNT), .DATA_WIDTH(DATA_WIDTH), .ADDR_WIDTH(ADDR_WIDTH), .STRB_WIDTH(STRB_WIDTH), - .ID_WIDTH(ID_WIDTH), + .S_ID_WIDTH(S_ID_WIDTH), + .M_ID_WIDTH(M_ID_WIDTH), .AWUSER_ENABLE(AWUSER_ENABLE), .AWUSER_WIDTH(AWUSER_WIDTH), .WUSER_ENABLE(WUSER_ENABLE), @@ -320,15 +448,27 @@ axi_interconnect #( .ARUSER_WIDTH(ARUSER_WIDTH), .RUSER_ENABLE(RUSER_ENABLE), .RUSER_WIDTH(RUSER_WIDTH), - .FORWARD_ID(FORWARD_ID), + .S_THREADS({ w_32(S03_THREADS), w_32(S02_THREADS), w_32(S01_THREADS), w_32(S00_THREADS) }), + .S_ACCEPT({ w_32(S03_ACCEPT), w_32(S02_ACCEPT), w_32(S01_ACCEPT), w_32(S00_ACCEPT) }), .M_REGIONS(M_REGIONS), .M_BASE_ADDR({ w_a_r(M00_BASE_ADDR) }), .M_ADDR_WIDTH({ w_32_r(M00_ADDR_WIDTH) }), .M_CONNECT_READ({ w_s(M00_CONNECT_READ) }), .M_CONNECT_WRITE({ w_s(M00_CONNECT_WRITE) }), - .M_SECURE({ w_1(M00_SECURE) }) + .M_ISSUE({ w_32(M00_ISSUE) }), + .M_SECURE({ w_1(M00_SECURE) }), + .S_AR_REG_TYPE({ w_2(S03_AR_REG_TYPE), w_2(S02_AR_REG_TYPE), w_2(S01_AR_REG_TYPE), w_2(S00_AR_REG_TYPE) }), + .S_R_REG_TYPE({ w_2(S03_R_REG_TYPE), w_2(S02_R_REG_TYPE), w_2(S01_R_REG_TYPE), w_2(S00_R_REG_TYPE) }), + .S_AW_REG_TYPE({ w_2(S03_AW_REG_TYPE), w_2(S02_AW_REG_TYPE), w_2(S01_AW_REG_TYPE), w_2(S00_AW_REG_TYPE) }), + .S_W_REG_TYPE({ w_2(S03_W_REG_TYPE), w_2(S02_W_REG_TYPE), w_2(S01_W_REG_TYPE), w_2(S00_W_REG_TYPE) }), + .S_B_REG_TYPE({ w_2(S03_B_REG_TYPE), w_2(S02_B_REG_TYPE), w_2(S01_B_REG_TYPE), w_2(S00_B_REG_TYPE) }), + .M_AR_REG_TYPE({ w_2(M00_AR_REG_TYPE) }), + .M_R_REG_TYPE({ w_2(M00_R_REG_TYPE) }), + .M_AW_REG_TYPE({ w_2(M00_AW_REG_TYPE) }), + .M_W_REG_TYPE({ w_2(M00_W_REG_TYPE) }), + .M_B_REG_TYPE({ w_2(M00_B_REG_TYPE) }) ) -axi_interconnect_inst ( +axi_crossbar_inst ( .clk(clk), .rst(rst), .s_axi_awid({ s03_axi_awid, s02_axi_awid, s01_axi_awid, s00_axi_awid }), diff --git a/xls/modules/zstd/external/axi_register_rd.v b/xls/modules/zstd/external/axi_register_rd.v new file mode 100644 index 0000000000..c0df03a03f --- /dev/null +++ b/xls/modules/zstd/external/axi_register_rd.v @@ -0,0 +1,530 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 register (read) + */ +module axi_register_rd # +( + // Width of data bus in bits + parameter DATA_WIDTH = 32, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) + parameter STRB_WIDTH = (DATA_WIDTH/8), + // Width of ID signal + parameter ID_WIDTH = 8, + // Propagate aruser signal + parameter ARUSER_ENABLE = 0, + // Width of aruser signal + parameter ARUSER_WIDTH = 1, + // Propagate ruser signal + parameter RUSER_ENABLE = 0, + // Width of ruser signal + parameter RUSER_WIDTH = 1, + // AR channel register type + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter AR_REG_TYPE = 1, + // R channel register type + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter R_REG_TYPE = 2 +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interface + */ + input wire [ID_WIDTH-1:0] s_axi_arid, + input wire [ADDR_WIDTH-1:0] s_axi_araddr, + input wire [7:0] s_axi_arlen, + input wire [2:0] s_axi_arsize, + input wire [1:0] s_axi_arburst, + input wire s_axi_arlock, + input wire [3:0] s_axi_arcache, + input wire [2:0] s_axi_arprot, + input wire [3:0] s_axi_arqos, + input wire [3:0] s_axi_arregion, + input wire [ARUSER_WIDTH-1:0] s_axi_aruser, + input wire s_axi_arvalid, + output wire s_axi_arready, + output wire [ID_WIDTH-1:0] s_axi_rid, + output wire [DATA_WIDTH-1:0] s_axi_rdata, + output wire [1:0] s_axi_rresp, + output wire s_axi_rlast, + output wire [RUSER_WIDTH-1:0] s_axi_ruser, + output wire s_axi_rvalid, + input wire s_axi_rready, + + /* + * AXI master interface + */ + output wire [ID_WIDTH-1:0] m_axi_arid, + output wire [ADDR_WIDTH-1:0] m_axi_araddr, + output wire [7:0] m_axi_arlen, + output wire [2:0] m_axi_arsize, + output wire [1:0] m_axi_arburst, + output wire m_axi_arlock, + output wire [3:0] m_axi_arcache, + output wire [2:0] m_axi_arprot, + output wire [3:0] m_axi_arqos, + output wire [3:0] m_axi_arregion, + output wire [ARUSER_WIDTH-1:0] m_axi_aruser, + output wire m_axi_arvalid, + input wire m_axi_arready, + input wire [ID_WIDTH-1:0] m_axi_rid, + input wire [DATA_WIDTH-1:0] m_axi_rdata, + input wire [1:0] m_axi_rresp, + input wire m_axi_rlast, + input wire [RUSER_WIDTH-1:0] m_axi_ruser, + input wire m_axi_rvalid, + output wire m_axi_rready +); + +generate + +// AR channel + +if (AR_REG_TYPE > 1) begin +// skid buffer, no bubble cycles + +// datapath registers +reg s_axi_arready_reg = 1'b0; + +reg [ID_WIDTH-1:0] m_axi_arid_reg = {ID_WIDTH{1'b0}}; +reg [ADDR_WIDTH-1:0] m_axi_araddr_reg = {ADDR_WIDTH{1'b0}}; +reg [7:0] m_axi_arlen_reg = 8'd0; +reg [2:0] m_axi_arsize_reg = 3'd0; +reg [1:0] m_axi_arburst_reg = 2'd0; +reg m_axi_arlock_reg = 1'b0; +reg [3:0] m_axi_arcache_reg = 4'd0; +reg [2:0] m_axi_arprot_reg = 3'd0; +reg [3:0] m_axi_arqos_reg = 4'd0; +reg [3:0] m_axi_arregion_reg = 4'd0; +reg [ARUSER_WIDTH-1:0] m_axi_aruser_reg = {ARUSER_WIDTH{1'b0}}; +reg m_axi_arvalid_reg = 1'b0, m_axi_arvalid_next; + +reg [ID_WIDTH-1:0] temp_m_axi_arid_reg = {ID_WIDTH{1'b0}}; +reg [ADDR_WIDTH-1:0] temp_m_axi_araddr_reg = {ADDR_WIDTH{1'b0}}; +reg [7:0] temp_m_axi_arlen_reg = 8'd0; +reg [2:0] temp_m_axi_arsize_reg = 3'd0; +reg [1:0] temp_m_axi_arburst_reg = 2'd0; +reg temp_m_axi_arlock_reg = 1'b0; +reg [3:0] temp_m_axi_arcache_reg = 4'd0; +reg [2:0] temp_m_axi_arprot_reg = 3'd0; +reg [3:0] temp_m_axi_arqos_reg = 4'd0; +reg [3:0] temp_m_axi_arregion_reg = 4'd0; +reg [ARUSER_WIDTH-1:0] temp_m_axi_aruser_reg = {ARUSER_WIDTH{1'b0}}; +reg temp_m_axi_arvalid_reg = 1'b0, temp_m_axi_arvalid_next; + +// datapath control +reg store_axi_ar_input_to_output; +reg store_axi_ar_input_to_temp; +reg store_axi_ar_temp_to_output; + +assign s_axi_arready = s_axi_arready_reg; + +assign m_axi_arid = m_axi_arid_reg; +assign m_axi_araddr = m_axi_araddr_reg; +assign m_axi_arlen = m_axi_arlen_reg; +assign m_axi_arsize = m_axi_arsize_reg; +assign m_axi_arburst = m_axi_arburst_reg; +assign m_axi_arlock = m_axi_arlock_reg; +assign m_axi_arcache = m_axi_arcache_reg; +assign m_axi_arprot = m_axi_arprot_reg; +assign m_axi_arqos = m_axi_arqos_reg; +assign m_axi_arregion = m_axi_arregion_reg; +assign m_axi_aruser = ARUSER_ENABLE ? m_axi_aruser_reg : {ARUSER_WIDTH{1'b0}}; +assign m_axi_arvalid = m_axi_arvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +wire s_axi_arready_early = m_axi_arready | (~temp_m_axi_arvalid_reg & (~m_axi_arvalid_reg | ~s_axi_arvalid)); + +always @* begin + // transfer sink ready state to source + m_axi_arvalid_next = m_axi_arvalid_reg; + temp_m_axi_arvalid_next = temp_m_axi_arvalid_reg; + + store_axi_ar_input_to_output = 1'b0; + store_axi_ar_input_to_temp = 1'b0; + store_axi_ar_temp_to_output = 1'b0; + + if (s_axi_arready_reg) begin + // input is ready + if (m_axi_arready | ~m_axi_arvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axi_arvalid_next = s_axi_arvalid; + store_axi_ar_input_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axi_arvalid_next = s_axi_arvalid; + store_axi_ar_input_to_temp = 1'b1; + end + end else if (m_axi_arready) begin + // input is not ready, but output is ready + m_axi_arvalid_next = temp_m_axi_arvalid_reg; + temp_m_axi_arvalid_next = 1'b0; + store_axi_ar_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_arready_reg <= 1'b0; + m_axi_arvalid_reg <= 1'b0; + temp_m_axi_arvalid_reg <= 1'b0; + end else begin + s_axi_arready_reg <= s_axi_arready_early; + m_axi_arvalid_reg <= m_axi_arvalid_next; + temp_m_axi_arvalid_reg <= temp_m_axi_arvalid_next; + end + + // datapath + if (store_axi_ar_input_to_output) begin + m_axi_arid_reg <= s_axi_arid; + m_axi_araddr_reg <= s_axi_araddr; + m_axi_arlen_reg <= s_axi_arlen; + m_axi_arsize_reg <= s_axi_arsize; + m_axi_arburst_reg <= s_axi_arburst; + m_axi_arlock_reg <= s_axi_arlock; + m_axi_arcache_reg <= s_axi_arcache; + m_axi_arprot_reg <= s_axi_arprot; + m_axi_arqos_reg <= s_axi_arqos; + m_axi_arregion_reg <= s_axi_arregion; + m_axi_aruser_reg <= s_axi_aruser; + end else if (store_axi_ar_temp_to_output) begin + m_axi_arid_reg <= temp_m_axi_arid_reg; + m_axi_araddr_reg <= temp_m_axi_araddr_reg; + m_axi_arlen_reg <= temp_m_axi_arlen_reg; + m_axi_arsize_reg <= temp_m_axi_arsize_reg; + m_axi_arburst_reg <= temp_m_axi_arburst_reg; + m_axi_arlock_reg <= temp_m_axi_arlock_reg; + m_axi_arcache_reg <= temp_m_axi_arcache_reg; + m_axi_arprot_reg <= temp_m_axi_arprot_reg; + m_axi_arqos_reg <= temp_m_axi_arqos_reg; + m_axi_arregion_reg <= temp_m_axi_arregion_reg; + m_axi_aruser_reg <= temp_m_axi_aruser_reg; + end + + if (store_axi_ar_input_to_temp) begin + temp_m_axi_arid_reg <= s_axi_arid; + temp_m_axi_araddr_reg <= s_axi_araddr; + temp_m_axi_arlen_reg <= s_axi_arlen; + temp_m_axi_arsize_reg <= s_axi_arsize; + temp_m_axi_arburst_reg <= s_axi_arburst; + temp_m_axi_arlock_reg <= s_axi_arlock; + temp_m_axi_arcache_reg <= s_axi_arcache; + temp_m_axi_arprot_reg <= s_axi_arprot; + temp_m_axi_arqos_reg <= s_axi_arqos; + temp_m_axi_arregion_reg <= s_axi_arregion; + temp_m_axi_aruser_reg <= s_axi_aruser; + end +end + +end else if (AR_REG_TYPE == 1) begin +// simple register, inserts bubble cycles + +// datapath registers +reg s_axi_arready_reg = 1'b0; + +reg [ID_WIDTH-1:0] m_axi_arid_reg = {ID_WIDTH{1'b0}}; +reg [ADDR_WIDTH-1:0] m_axi_araddr_reg = {ADDR_WIDTH{1'b0}}; +reg [7:0] m_axi_arlen_reg = 8'd0; +reg [2:0] m_axi_arsize_reg = 3'd0; +reg [1:0] m_axi_arburst_reg = 2'd0; +reg m_axi_arlock_reg = 1'b0; +reg [3:0] m_axi_arcache_reg = 4'd0; +reg [2:0] m_axi_arprot_reg = 3'd0; +reg [3:0] m_axi_arqos_reg = 4'd0; +reg [3:0] m_axi_arregion_reg = 4'd0; +reg [ARUSER_WIDTH-1:0] m_axi_aruser_reg = {ARUSER_WIDTH{1'b0}}; +reg m_axi_arvalid_reg = 1'b0, m_axi_arvalid_next; + +// datapath control +reg store_axi_ar_input_to_output; + +assign s_axi_arready = s_axi_arready_reg; + +assign m_axi_arid = m_axi_arid_reg; +assign m_axi_araddr = m_axi_araddr_reg; +assign m_axi_arlen = m_axi_arlen_reg; +assign m_axi_arsize = m_axi_arsize_reg; +assign m_axi_arburst = m_axi_arburst_reg; +assign m_axi_arlock = m_axi_arlock_reg; +assign m_axi_arcache = m_axi_arcache_reg; +assign m_axi_arprot = m_axi_arprot_reg; +assign m_axi_arqos = m_axi_arqos_reg; +assign m_axi_arregion = m_axi_arregion_reg; +assign m_axi_aruser = ARUSER_ENABLE ? m_axi_aruser_reg : {ARUSER_WIDTH{1'b0}}; +assign m_axi_arvalid = m_axi_arvalid_reg; + +// enable ready input next cycle if output buffer will be empty +wire s_axi_arready_early = !m_axi_arvalid_next; + +always @* begin + // transfer sink ready state to source + m_axi_arvalid_next = m_axi_arvalid_reg; + + store_axi_ar_input_to_output = 1'b0; + + if (s_axi_arready_reg) begin + m_axi_arvalid_next = s_axi_arvalid; + store_axi_ar_input_to_output = 1'b1; + end else if (m_axi_arready) begin + m_axi_arvalid_next = 1'b0; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_arready_reg <= 1'b0; + m_axi_arvalid_reg <= 1'b0; + end else begin + s_axi_arready_reg <= s_axi_arready_early; + m_axi_arvalid_reg <= m_axi_arvalid_next; + end + + // datapath + if (store_axi_ar_input_to_output) begin + m_axi_arid_reg <= s_axi_arid; + m_axi_araddr_reg <= s_axi_araddr; + m_axi_arlen_reg <= s_axi_arlen; + m_axi_arsize_reg <= s_axi_arsize; + m_axi_arburst_reg <= s_axi_arburst; + m_axi_arlock_reg <= s_axi_arlock; + m_axi_arcache_reg <= s_axi_arcache; + m_axi_arprot_reg <= s_axi_arprot; + m_axi_arqos_reg <= s_axi_arqos; + m_axi_arregion_reg <= s_axi_arregion; + m_axi_aruser_reg <= s_axi_aruser; + end +end + +end else begin + + // bypass AR channel + assign m_axi_arid = s_axi_arid; + assign m_axi_araddr = s_axi_araddr; + assign m_axi_arlen = s_axi_arlen; + assign m_axi_arsize = s_axi_arsize; + assign m_axi_arburst = s_axi_arburst; + assign m_axi_arlock = s_axi_arlock; + assign m_axi_arcache = s_axi_arcache; + assign m_axi_arprot = s_axi_arprot; + assign m_axi_arqos = s_axi_arqos; + assign m_axi_arregion = s_axi_arregion; + assign m_axi_aruser = ARUSER_ENABLE ? s_axi_aruser : {ARUSER_WIDTH{1'b0}}; + assign m_axi_arvalid = s_axi_arvalid; + assign s_axi_arready = m_axi_arready; + +end + +// R channel + +if (R_REG_TYPE > 1) begin +// skid buffer, no bubble cycles + +// datapath registers +reg m_axi_rready_reg = 1'b0; + +reg [ID_WIDTH-1:0] s_axi_rid_reg = {ID_WIDTH{1'b0}}; +reg [DATA_WIDTH-1:0] s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; +reg [1:0] s_axi_rresp_reg = 2'b0; +reg s_axi_rlast_reg = 1'b0; +reg [RUSER_WIDTH-1:0] s_axi_ruser_reg = {RUSER_WIDTH{1'b0}}; +reg s_axi_rvalid_reg = 1'b0, s_axi_rvalid_next; + +reg [ID_WIDTH-1:0] temp_s_axi_rid_reg = {ID_WIDTH{1'b0}}; +reg [DATA_WIDTH-1:0] temp_s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; +reg [1:0] temp_s_axi_rresp_reg = 2'b0; +reg temp_s_axi_rlast_reg = 1'b0; +reg [RUSER_WIDTH-1:0] temp_s_axi_ruser_reg = {RUSER_WIDTH{1'b0}}; +reg temp_s_axi_rvalid_reg = 1'b0, temp_s_axi_rvalid_next; + +// datapath control +reg store_axi_r_input_to_output; +reg store_axi_r_input_to_temp; +reg store_axi_r_temp_to_output; + +assign m_axi_rready = m_axi_rready_reg; + +assign s_axi_rid = s_axi_rid_reg; +assign s_axi_rdata = s_axi_rdata_reg; +assign s_axi_rresp = s_axi_rresp_reg; +assign s_axi_rlast = s_axi_rlast_reg; +assign s_axi_ruser = RUSER_ENABLE ? s_axi_ruser_reg : {RUSER_WIDTH{1'b0}}; +assign s_axi_rvalid = s_axi_rvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +wire m_axi_rready_early = s_axi_rready | (~temp_s_axi_rvalid_reg & (~s_axi_rvalid_reg | ~m_axi_rvalid)); + +always @* begin + // transfer sink ready state to source + s_axi_rvalid_next = s_axi_rvalid_reg; + temp_s_axi_rvalid_next = temp_s_axi_rvalid_reg; + + store_axi_r_input_to_output = 1'b0; + store_axi_r_input_to_temp = 1'b0; + store_axi_r_temp_to_output = 1'b0; + + if (m_axi_rready_reg) begin + // input is ready + if (s_axi_rready | ~s_axi_rvalid_reg) begin + // output is ready or currently not valid, transfer data to output + s_axi_rvalid_next = m_axi_rvalid; + store_axi_r_input_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_s_axi_rvalid_next = m_axi_rvalid; + store_axi_r_input_to_temp = 1'b1; + end + end else if (s_axi_rready) begin + // input is not ready, but output is ready + s_axi_rvalid_next = temp_s_axi_rvalid_reg; + temp_s_axi_rvalid_next = 1'b0; + store_axi_r_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + m_axi_rready_reg <= 1'b0; + s_axi_rvalid_reg <= 1'b0; + temp_s_axi_rvalid_reg <= 1'b0; + end else begin + m_axi_rready_reg <= m_axi_rready_early; + s_axi_rvalid_reg <= s_axi_rvalid_next; + temp_s_axi_rvalid_reg <= temp_s_axi_rvalid_next; + end + + // datapath + if (store_axi_r_input_to_output) begin + s_axi_rid_reg <= m_axi_rid; + s_axi_rdata_reg <= m_axi_rdata; + s_axi_rresp_reg <= m_axi_rresp; + s_axi_rlast_reg <= m_axi_rlast; + s_axi_ruser_reg <= m_axi_ruser; + end else if (store_axi_r_temp_to_output) begin + s_axi_rid_reg <= temp_s_axi_rid_reg; + s_axi_rdata_reg <= temp_s_axi_rdata_reg; + s_axi_rresp_reg <= temp_s_axi_rresp_reg; + s_axi_rlast_reg <= temp_s_axi_rlast_reg; + s_axi_ruser_reg <= temp_s_axi_ruser_reg; + end + + if (store_axi_r_input_to_temp) begin + temp_s_axi_rid_reg <= m_axi_rid; + temp_s_axi_rdata_reg <= m_axi_rdata; + temp_s_axi_rresp_reg <= m_axi_rresp; + temp_s_axi_rlast_reg <= m_axi_rlast; + temp_s_axi_ruser_reg <= m_axi_ruser; + end +end + +end else if (R_REG_TYPE == 1) begin +// simple register, inserts bubble cycles + +// datapath registers +reg m_axi_rready_reg = 1'b0; + +reg [ID_WIDTH-1:0] s_axi_rid_reg = {ID_WIDTH{1'b0}}; +reg [DATA_WIDTH-1:0] s_axi_rdata_reg = {DATA_WIDTH{1'b0}}; +reg [1:0] s_axi_rresp_reg = 2'b0; +reg s_axi_rlast_reg = 1'b0; +reg [RUSER_WIDTH-1:0] s_axi_ruser_reg = {RUSER_WIDTH{1'b0}}; +reg s_axi_rvalid_reg = 1'b0, s_axi_rvalid_next; + +// datapath control +reg store_axi_r_input_to_output; + +assign m_axi_rready = m_axi_rready_reg; + +assign s_axi_rid = s_axi_rid_reg; +assign s_axi_rdata = s_axi_rdata_reg; +assign s_axi_rresp = s_axi_rresp_reg; +assign s_axi_rlast = s_axi_rlast_reg; +assign s_axi_ruser = RUSER_ENABLE ? s_axi_ruser_reg : {RUSER_WIDTH{1'b0}}; +assign s_axi_rvalid = s_axi_rvalid_reg; + +// enable ready input next cycle if output buffer will be empty +wire m_axi_rready_early = !s_axi_rvalid_next; + +always @* begin + // transfer sink ready state to source + s_axi_rvalid_next = s_axi_rvalid_reg; + + store_axi_r_input_to_output = 1'b0; + + if (m_axi_rready_reg) begin + s_axi_rvalid_next = m_axi_rvalid; + store_axi_r_input_to_output = 1'b1; + end else if (s_axi_rready) begin + s_axi_rvalid_next = 1'b0; + end +end + +always @(posedge clk) begin + if (rst) begin + m_axi_rready_reg <= 1'b0; + s_axi_rvalid_reg <= 1'b0; + end else begin + m_axi_rready_reg <= m_axi_rready_early; + s_axi_rvalid_reg <= s_axi_rvalid_next; + end + + // datapath + if (store_axi_r_input_to_output) begin + s_axi_rid_reg <= m_axi_rid; + s_axi_rdata_reg <= m_axi_rdata; + s_axi_rresp_reg <= m_axi_rresp; + s_axi_rlast_reg <= m_axi_rlast; + s_axi_ruser_reg <= m_axi_ruser; + end +end + +end else begin + + // bypass R channel + assign s_axi_rid = m_axi_rid; + assign s_axi_rdata = m_axi_rdata; + assign s_axi_rresp = m_axi_rresp; + assign s_axi_rlast = m_axi_rlast; + assign s_axi_ruser = RUSER_ENABLE ? m_axi_ruser : {RUSER_WIDTH{1'b0}}; + assign s_axi_rvalid = m_axi_rvalid; + assign m_axi_rready = s_axi_rready; + +end + +endgenerate + +endmodule + +`resetall diff --git a/xls/modules/zstd/external/axi_register_wr.v b/xls/modules/zstd/external/axi_register_wr.v new file mode 100644 index 0000000000..9176d6ba95 --- /dev/null +++ b/xls/modules/zstd/external/axi_register_wr.v @@ -0,0 +1,691 @@ +/* + +Copyright (c) 2018 Alex Forencich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// Language: Verilog 2001 + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * AXI4 register (write) + */ +module axi_register_wr # +( + // Width of data bus in bits + parameter DATA_WIDTH = 32, + // Width of address bus in bits + parameter ADDR_WIDTH = 32, + // Width of wstrb (width of data bus in words) + parameter STRB_WIDTH = (DATA_WIDTH/8), + // Width of ID signal + parameter ID_WIDTH = 8, + // Propagate awuser signal + parameter AWUSER_ENABLE = 0, + // Width of awuser signal + parameter AWUSER_WIDTH = 1, + // Propagate wuser signal + parameter WUSER_ENABLE = 0, + // Width of wuser signal + parameter WUSER_WIDTH = 1, + // Propagate buser signal + parameter BUSER_ENABLE = 0, + // Width of buser signal + parameter BUSER_WIDTH = 1, + // AW channel register type + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter AW_REG_TYPE = 1, + // W channel register type + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter W_REG_TYPE = 2, + // B channel register type + // 0 to bypass, 1 for simple buffer, 2 for skid buffer + parameter B_REG_TYPE = 1 +) +( + input wire clk, + input wire rst, + + /* + * AXI slave interface + */ + input wire [ID_WIDTH-1:0] s_axi_awid, + input wire [ADDR_WIDTH-1:0] s_axi_awaddr, + input wire [7:0] s_axi_awlen, + input wire [2:0] s_axi_awsize, + input wire [1:0] s_axi_awburst, + input wire s_axi_awlock, + input wire [3:0] s_axi_awcache, + input wire [2:0] s_axi_awprot, + input wire [3:0] s_axi_awqos, + input wire [3:0] s_axi_awregion, + input wire [AWUSER_WIDTH-1:0] s_axi_awuser, + input wire s_axi_awvalid, + output wire s_axi_awready, + input wire [DATA_WIDTH-1:0] s_axi_wdata, + input wire [STRB_WIDTH-1:0] s_axi_wstrb, + input wire s_axi_wlast, + input wire [WUSER_WIDTH-1:0] s_axi_wuser, + input wire s_axi_wvalid, + output wire s_axi_wready, + output wire [ID_WIDTH-1:0] s_axi_bid, + output wire [1:0] s_axi_bresp, + output wire [BUSER_WIDTH-1:0] s_axi_buser, + output wire s_axi_bvalid, + input wire s_axi_bready, + + /* + * AXI master interface + */ + output wire [ID_WIDTH-1:0] m_axi_awid, + output wire [ADDR_WIDTH-1:0] m_axi_awaddr, + output wire [7:0] m_axi_awlen, + output wire [2:0] m_axi_awsize, + output wire [1:0] m_axi_awburst, + output wire m_axi_awlock, + output wire [3:0] m_axi_awcache, + output wire [2:0] m_axi_awprot, + output wire [3:0] m_axi_awqos, + output wire [3:0] m_axi_awregion, + output wire [AWUSER_WIDTH-1:0] m_axi_awuser, + output wire m_axi_awvalid, + input wire m_axi_awready, + output wire [DATA_WIDTH-1:0] m_axi_wdata, + output wire [STRB_WIDTH-1:0] m_axi_wstrb, + output wire m_axi_wlast, + output wire [WUSER_WIDTH-1:0] m_axi_wuser, + output wire m_axi_wvalid, + input wire m_axi_wready, + input wire [ID_WIDTH-1:0] m_axi_bid, + input wire [1:0] m_axi_bresp, + input wire [BUSER_WIDTH-1:0] m_axi_buser, + input wire m_axi_bvalid, + output wire m_axi_bready +); + +generate + +// AW channel + +if (AW_REG_TYPE > 1) begin +// skid buffer, no bubble cycles + +// datapath registers +reg s_axi_awready_reg = 1'b0; + +reg [ID_WIDTH-1:0] m_axi_awid_reg = {ID_WIDTH{1'b0}}; +reg [ADDR_WIDTH-1:0] m_axi_awaddr_reg = {ADDR_WIDTH{1'b0}}; +reg [7:0] m_axi_awlen_reg = 8'd0; +reg [2:0] m_axi_awsize_reg = 3'd0; +reg [1:0] m_axi_awburst_reg = 2'd0; +reg m_axi_awlock_reg = 1'b0; +reg [3:0] m_axi_awcache_reg = 4'd0; +reg [2:0] m_axi_awprot_reg = 3'd0; +reg [3:0] m_axi_awqos_reg = 4'd0; +reg [3:0] m_axi_awregion_reg = 4'd0; +reg [AWUSER_WIDTH-1:0] m_axi_awuser_reg = {AWUSER_WIDTH{1'b0}}; +reg m_axi_awvalid_reg = 1'b0, m_axi_awvalid_next; + +reg [ID_WIDTH-1:0] temp_m_axi_awid_reg = {ID_WIDTH{1'b0}}; +reg [ADDR_WIDTH-1:0] temp_m_axi_awaddr_reg = {ADDR_WIDTH{1'b0}}; +reg [7:0] temp_m_axi_awlen_reg = 8'd0; +reg [2:0] temp_m_axi_awsize_reg = 3'd0; +reg [1:0] temp_m_axi_awburst_reg = 2'd0; +reg temp_m_axi_awlock_reg = 1'b0; +reg [3:0] temp_m_axi_awcache_reg = 4'd0; +reg [2:0] temp_m_axi_awprot_reg = 3'd0; +reg [3:0] temp_m_axi_awqos_reg = 4'd0; +reg [3:0] temp_m_axi_awregion_reg = 4'd0; +reg [AWUSER_WIDTH-1:0] temp_m_axi_awuser_reg = {AWUSER_WIDTH{1'b0}}; +reg temp_m_axi_awvalid_reg = 1'b0, temp_m_axi_awvalid_next; + +// datapath control +reg store_axi_aw_input_to_output; +reg store_axi_aw_input_to_temp; +reg store_axi_aw_temp_to_output; + +assign s_axi_awready = s_axi_awready_reg; + +assign m_axi_awid = m_axi_awid_reg; +assign m_axi_awaddr = m_axi_awaddr_reg; +assign m_axi_awlen = m_axi_awlen_reg; +assign m_axi_awsize = m_axi_awsize_reg; +assign m_axi_awburst = m_axi_awburst_reg; +assign m_axi_awlock = m_axi_awlock_reg; +assign m_axi_awcache = m_axi_awcache_reg; +assign m_axi_awprot = m_axi_awprot_reg; +assign m_axi_awqos = m_axi_awqos_reg; +assign m_axi_awregion = m_axi_awregion_reg; +assign m_axi_awuser = AWUSER_ENABLE ? m_axi_awuser_reg : {AWUSER_WIDTH{1'b0}}; +assign m_axi_awvalid = m_axi_awvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +wire s_axi_awready_early = m_axi_awready | (~temp_m_axi_awvalid_reg & (~m_axi_awvalid_reg | ~s_axi_awvalid)); + +always @* begin + // transfer sink ready state to source + m_axi_awvalid_next = m_axi_awvalid_reg; + temp_m_axi_awvalid_next = temp_m_axi_awvalid_reg; + + store_axi_aw_input_to_output = 1'b0; + store_axi_aw_input_to_temp = 1'b0; + store_axi_aw_temp_to_output = 1'b0; + + if (s_axi_awready_reg) begin + // input is ready + if (m_axi_awready | ~m_axi_awvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axi_awvalid_next = s_axi_awvalid; + store_axi_aw_input_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axi_awvalid_next = s_axi_awvalid; + store_axi_aw_input_to_temp = 1'b1; + end + end else if (m_axi_awready) begin + // input is not ready, but output is ready + m_axi_awvalid_next = temp_m_axi_awvalid_reg; + temp_m_axi_awvalid_next = 1'b0; + store_axi_aw_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_awready_reg <= 1'b0; + m_axi_awvalid_reg <= 1'b0; + temp_m_axi_awvalid_reg <= 1'b0; + end else begin + s_axi_awready_reg <= s_axi_awready_early; + m_axi_awvalid_reg <= m_axi_awvalid_next; + temp_m_axi_awvalid_reg <= temp_m_axi_awvalid_next; + end + + // datapath + if (store_axi_aw_input_to_output) begin + m_axi_awid_reg <= s_axi_awid; + m_axi_awaddr_reg <= s_axi_awaddr; + m_axi_awlen_reg <= s_axi_awlen; + m_axi_awsize_reg <= s_axi_awsize; + m_axi_awburst_reg <= s_axi_awburst; + m_axi_awlock_reg <= s_axi_awlock; + m_axi_awcache_reg <= s_axi_awcache; + m_axi_awprot_reg <= s_axi_awprot; + m_axi_awqos_reg <= s_axi_awqos; + m_axi_awregion_reg <= s_axi_awregion; + m_axi_awuser_reg <= s_axi_awuser; + end else if (store_axi_aw_temp_to_output) begin + m_axi_awid_reg <= temp_m_axi_awid_reg; + m_axi_awaddr_reg <= temp_m_axi_awaddr_reg; + m_axi_awlen_reg <= temp_m_axi_awlen_reg; + m_axi_awsize_reg <= temp_m_axi_awsize_reg; + m_axi_awburst_reg <= temp_m_axi_awburst_reg; + m_axi_awlock_reg <= temp_m_axi_awlock_reg; + m_axi_awcache_reg <= temp_m_axi_awcache_reg; + m_axi_awprot_reg <= temp_m_axi_awprot_reg; + m_axi_awqos_reg <= temp_m_axi_awqos_reg; + m_axi_awregion_reg <= temp_m_axi_awregion_reg; + m_axi_awuser_reg <= temp_m_axi_awuser_reg; + end + + if (store_axi_aw_input_to_temp) begin + temp_m_axi_awid_reg <= s_axi_awid; + temp_m_axi_awaddr_reg <= s_axi_awaddr; + temp_m_axi_awlen_reg <= s_axi_awlen; + temp_m_axi_awsize_reg <= s_axi_awsize; + temp_m_axi_awburst_reg <= s_axi_awburst; + temp_m_axi_awlock_reg <= s_axi_awlock; + temp_m_axi_awcache_reg <= s_axi_awcache; + temp_m_axi_awprot_reg <= s_axi_awprot; + temp_m_axi_awqos_reg <= s_axi_awqos; + temp_m_axi_awregion_reg <= s_axi_awregion; + temp_m_axi_awuser_reg <= s_axi_awuser; + end +end + +end else if (AW_REG_TYPE == 1) begin +// simple register, inserts bubble cycles + +// datapath registers +reg s_axi_awready_reg = 1'b0; + +reg [ID_WIDTH-1:0] m_axi_awid_reg = {ID_WIDTH{1'b0}}; +reg [ADDR_WIDTH-1:0] m_axi_awaddr_reg = {ADDR_WIDTH{1'b0}}; +reg [7:0] m_axi_awlen_reg = 8'd0; +reg [2:0] m_axi_awsize_reg = 3'd0; +reg [1:0] m_axi_awburst_reg = 2'd0; +reg m_axi_awlock_reg = 1'b0; +reg [3:0] m_axi_awcache_reg = 4'd0; +reg [2:0] m_axi_awprot_reg = 3'd0; +reg [3:0] m_axi_awqos_reg = 4'd0; +reg [3:0] m_axi_awregion_reg = 4'd0; +reg [AWUSER_WIDTH-1:0] m_axi_awuser_reg = {AWUSER_WIDTH{1'b0}}; +reg m_axi_awvalid_reg = 1'b0, m_axi_awvalid_next; + +// datapath control +reg store_axi_aw_input_to_output; + +assign s_axi_awready = s_axi_awready_reg; + +assign m_axi_awid = m_axi_awid_reg; +assign m_axi_awaddr = m_axi_awaddr_reg; +assign m_axi_awlen = m_axi_awlen_reg; +assign m_axi_awsize = m_axi_awsize_reg; +assign m_axi_awburst = m_axi_awburst_reg; +assign m_axi_awlock = m_axi_awlock_reg; +assign m_axi_awcache = m_axi_awcache_reg; +assign m_axi_awprot = m_axi_awprot_reg; +assign m_axi_awqos = m_axi_awqos_reg; +assign m_axi_awregion = m_axi_awregion_reg; +assign m_axi_awuser = AWUSER_ENABLE ? m_axi_awuser_reg : {AWUSER_WIDTH{1'b0}}; +assign m_axi_awvalid = m_axi_awvalid_reg; + +// enable ready input next cycle if output buffer will be empty +wire s_axi_awready_eawly = !m_axi_awvalid_next; + +always @* begin + // transfer sink ready state to source + m_axi_awvalid_next = m_axi_awvalid_reg; + + store_axi_aw_input_to_output = 1'b0; + + if (s_axi_awready_reg) begin + m_axi_awvalid_next = s_axi_awvalid; + store_axi_aw_input_to_output = 1'b1; + end else if (m_axi_awready) begin + m_axi_awvalid_next = 1'b0; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_awready_reg <= 1'b0; + m_axi_awvalid_reg <= 1'b0; + end else begin + s_axi_awready_reg <= s_axi_awready_eawly; + m_axi_awvalid_reg <= m_axi_awvalid_next; + end + + // datapath + if (store_axi_aw_input_to_output) begin + m_axi_awid_reg <= s_axi_awid; + m_axi_awaddr_reg <= s_axi_awaddr; + m_axi_awlen_reg <= s_axi_awlen; + m_axi_awsize_reg <= s_axi_awsize; + m_axi_awburst_reg <= s_axi_awburst; + m_axi_awlock_reg <= s_axi_awlock; + m_axi_awcache_reg <= s_axi_awcache; + m_axi_awprot_reg <= s_axi_awprot; + m_axi_awqos_reg <= s_axi_awqos; + m_axi_awregion_reg <= s_axi_awregion; + m_axi_awuser_reg <= s_axi_awuser; + end +end + +end else begin + + // bypass AW channel + assign m_axi_awid = s_axi_awid; + assign m_axi_awaddr = s_axi_awaddr; + assign m_axi_awlen = s_axi_awlen; + assign m_axi_awsize = s_axi_awsize; + assign m_axi_awburst = s_axi_awburst; + assign m_axi_awlock = s_axi_awlock; + assign m_axi_awcache = s_axi_awcache; + assign m_axi_awprot = s_axi_awprot; + assign m_axi_awqos = s_axi_awqos; + assign m_axi_awregion = s_axi_awregion; + assign m_axi_awuser = AWUSER_ENABLE ? s_axi_awuser : {AWUSER_WIDTH{1'b0}}; + assign m_axi_awvalid = s_axi_awvalid; + assign s_axi_awready = m_axi_awready; + +end + +// W channel + +if (W_REG_TYPE > 1) begin +// skid buffer, no bubble cycles + +// datapath registers +reg s_axi_wready_reg = 1'b0; + +reg [DATA_WIDTH-1:0] m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; +reg [STRB_WIDTH-1:0] m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; +reg m_axi_wlast_reg = 1'b0; +reg [WUSER_WIDTH-1:0] m_axi_wuser_reg = {WUSER_WIDTH{1'b0}}; +reg m_axi_wvalid_reg = 1'b0, m_axi_wvalid_next; + +reg [DATA_WIDTH-1:0] temp_m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; +reg [STRB_WIDTH-1:0] temp_m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; +reg temp_m_axi_wlast_reg = 1'b0; +reg [WUSER_WIDTH-1:0] temp_m_axi_wuser_reg = {WUSER_WIDTH{1'b0}}; +reg temp_m_axi_wvalid_reg = 1'b0, temp_m_axi_wvalid_next; + +// datapath control +reg store_axi_w_input_to_output; +reg store_axi_w_input_to_temp; +reg store_axi_w_temp_to_output; + +assign s_axi_wready = s_axi_wready_reg; + +assign m_axi_wdata = m_axi_wdata_reg; +assign m_axi_wstrb = m_axi_wstrb_reg; +assign m_axi_wlast = m_axi_wlast_reg; +assign m_axi_wuser = WUSER_ENABLE ? m_axi_wuser_reg : {WUSER_WIDTH{1'b0}}; +assign m_axi_wvalid = m_axi_wvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +wire s_axi_wready_early = m_axi_wready | (~temp_m_axi_wvalid_reg & (~m_axi_wvalid_reg | ~s_axi_wvalid)); + +always @* begin + // transfer sink ready state to source + m_axi_wvalid_next = m_axi_wvalid_reg; + temp_m_axi_wvalid_next = temp_m_axi_wvalid_reg; + + store_axi_w_input_to_output = 1'b0; + store_axi_w_input_to_temp = 1'b0; + store_axi_w_temp_to_output = 1'b0; + + if (s_axi_wready_reg) begin + // input is ready + if (m_axi_wready | ~m_axi_wvalid_reg) begin + // output is ready or currently not valid, transfer data to output + m_axi_wvalid_next = s_axi_wvalid; + store_axi_w_input_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_m_axi_wvalid_next = s_axi_wvalid; + store_axi_w_input_to_temp = 1'b1; + end + end else if (m_axi_wready) begin + // input is not ready, but output is ready + m_axi_wvalid_next = temp_m_axi_wvalid_reg; + temp_m_axi_wvalid_next = 1'b0; + store_axi_w_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_wready_reg <= 1'b0; + m_axi_wvalid_reg <= 1'b0; + temp_m_axi_wvalid_reg <= 1'b0; + end else begin + s_axi_wready_reg <= s_axi_wready_early; + m_axi_wvalid_reg <= m_axi_wvalid_next; + temp_m_axi_wvalid_reg <= temp_m_axi_wvalid_next; + end + + // datapath + if (store_axi_w_input_to_output) begin + m_axi_wdata_reg <= s_axi_wdata; + m_axi_wstrb_reg <= s_axi_wstrb; + m_axi_wlast_reg <= s_axi_wlast; + m_axi_wuser_reg <= s_axi_wuser; + end else if (store_axi_w_temp_to_output) begin + m_axi_wdata_reg <= temp_m_axi_wdata_reg; + m_axi_wstrb_reg <= temp_m_axi_wstrb_reg; + m_axi_wlast_reg <= temp_m_axi_wlast_reg; + m_axi_wuser_reg <= temp_m_axi_wuser_reg; + end + + if (store_axi_w_input_to_temp) begin + temp_m_axi_wdata_reg <= s_axi_wdata; + temp_m_axi_wstrb_reg <= s_axi_wstrb; + temp_m_axi_wlast_reg <= s_axi_wlast; + temp_m_axi_wuser_reg <= s_axi_wuser; + end +end + +end else if (W_REG_TYPE == 1) begin +// simple register, inserts bubble cycles + +// datapath registers +reg s_axi_wready_reg = 1'b0; + +reg [DATA_WIDTH-1:0] m_axi_wdata_reg = {DATA_WIDTH{1'b0}}; +reg [STRB_WIDTH-1:0] m_axi_wstrb_reg = {STRB_WIDTH{1'b0}}; +reg m_axi_wlast_reg = 1'b0; +reg [WUSER_WIDTH-1:0] m_axi_wuser_reg = {WUSER_WIDTH{1'b0}}; +reg m_axi_wvalid_reg = 1'b0, m_axi_wvalid_next; + +// datapath control +reg store_axi_w_input_to_output; + +assign s_axi_wready = s_axi_wready_reg; + +assign m_axi_wdata = m_axi_wdata_reg; +assign m_axi_wstrb = m_axi_wstrb_reg; +assign m_axi_wlast = m_axi_wlast_reg; +assign m_axi_wuser = WUSER_ENABLE ? m_axi_wuser_reg : {WUSER_WIDTH{1'b0}}; +assign m_axi_wvalid = m_axi_wvalid_reg; + +// enable ready input next cycle if output buffer will be empty +wire s_axi_wready_ewly = !m_axi_wvalid_next; + +always @* begin + // transfer sink ready state to source + m_axi_wvalid_next = m_axi_wvalid_reg; + + store_axi_w_input_to_output = 1'b0; + + if (s_axi_wready_reg) begin + m_axi_wvalid_next = s_axi_wvalid; + store_axi_w_input_to_output = 1'b1; + end else if (m_axi_wready) begin + m_axi_wvalid_next = 1'b0; + end +end + +always @(posedge clk) begin + if (rst) begin + s_axi_wready_reg <= 1'b0; + m_axi_wvalid_reg <= 1'b0; + end else begin + s_axi_wready_reg <= s_axi_wready_ewly; + m_axi_wvalid_reg <= m_axi_wvalid_next; + end + + // datapath + if (store_axi_w_input_to_output) begin + m_axi_wdata_reg <= s_axi_wdata; + m_axi_wstrb_reg <= s_axi_wstrb; + m_axi_wlast_reg <= s_axi_wlast; + m_axi_wuser_reg <= s_axi_wuser; + end +end + +end else begin + + // bypass W channel + assign m_axi_wdata = s_axi_wdata; + assign m_axi_wstrb = s_axi_wstrb; + assign m_axi_wlast = s_axi_wlast; + assign m_axi_wuser = WUSER_ENABLE ? s_axi_wuser : {WUSER_WIDTH{1'b0}}; + assign m_axi_wvalid = s_axi_wvalid; + assign s_axi_wready = m_axi_wready; + +end + +// B channel + +if (B_REG_TYPE > 1) begin +// skid buffer, no bubble cycles + +// datapath registers +reg m_axi_bready_reg = 1'b0; + +reg [ID_WIDTH-1:0] s_axi_bid_reg = {ID_WIDTH{1'b0}}; +reg [1:0] s_axi_bresp_reg = 2'b0; +reg [BUSER_WIDTH-1:0] s_axi_buser_reg = {BUSER_WIDTH{1'b0}}; +reg s_axi_bvalid_reg = 1'b0, s_axi_bvalid_next; + +reg [ID_WIDTH-1:0] temp_s_axi_bid_reg = {ID_WIDTH{1'b0}}; +reg [1:0] temp_s_axi_bresp_reg = 2'b0; +reg [BUSER_WIDTH-1:0] temp_s_axi_buser_reg = {BUSER_WIDTH{1'b0}}; +reg temp_s_axi_bvalid_reg = 1'b0, temp_s_axi_bvalid_next; + +// datapath control +reg store_axi_b_input_to_output; +reg store_axi_b_input_to_temp; +reg store_axi_b_temp_to_output; + +assign m_axi_bready = m_axi_bready_reg; + +assign s_axi_bid = s_axi_bid_reg; +assign s_axi_bresp = s_axi_bresp_reg; +assign s_axi_buser = BUSER_ENABLE ? s_axi_buser_reg : {BUSER_WIDTH{1'b0}}; +assign s_axi_bvalid = s_axi_bvalid_reg; + +// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input) +wire m_axi_bready_early = s_axi_bready | (~temp_s_axi_bvalid_reg & (~s_axi_bvalid_reg | ~m_axi_bvalid)); + +always @* begin + // transfer sink ready state to source + s_axi_bvalid_next = s_axi_bvalid_reg; + temp_s_axi_bvalid_next = temp_s_axi_bvalid_reg; + + store_axi_b_input_to_output = 1'b0; + store_axi_b_input_to_temp = 1'b0; + store_axi_b_temp_to_output = 1'b0; + + if (m_axi_bready_reg) begin + // input is ready + if (s_axi_bready | ~s_axi_bvalid_reg) begin + // output is ready or currently not valid, transfer data to output + s_axi_bvalid_next = m_axi_bvalid; + store_axi_b_input_to_output = 1'b1; + end else begin + // output is not ready, store input in temp + temp_s_axi_bvalid_next = m_axi_bvalid; + store_axi_b_input_to_temp = 1'b1; + end + end else if (s_axi_bready) begin + // input is not ready, but output is ready + s_axi_bvalid_next = temp_s_axi_bvalid_reg; + temp_s_axi_bvalid_next = 1'b0; + store_axi_b_temp_to_output = 1'b1; + end +end + +always @(posedge clk) begin + if (rst) begin + m_axi_bready_reg <= 1'b0; + s_axi_bvalid_reg <= 1'b0; + temp_s_axi_bvalid_reg <= 1'b0; + end else begin + m_axi_bready_reg <= m_axi_bready_early; + s_axi_bvalid_reg <= s_axi_bvalid_next; + temp_s_axi_bvalid_reg <= temp_s_axi_bvalid_next; + end + + // datapath + if (store_axi_b_input_to_output) begin + s_axi_bid_reg <= m_axi_bid; + s_axi_bresp_reg <= m_axi_bresp; + s_axi_buser_reg <= m_axi_buser; + end else if (store_axi_b_temp_to_output) begin + s_axi_bid_reg <= temp_s_axi_bid_reg; + s_axi_bresp_reg <= temp_s_axi_bresp_reg; + s_axi_buser_reg <= temp_s_axi_buser_reg; + end + + if (store_axi_b_input_to_temp) begin + temp_s_axi_bid_reg <= m_axi_bid; + temp_s_axi_bresp_reg <= m_axi_bresp; + temp_s_axi_buser_reg <= m_axi_buser; + end +end + +end else if (B_REG_TYPE == 1) begin +// simple register, inserts bubble cycles + +// datapath registers +reg m_axi_bready_reg = 1'b0; + +reg [ID_WIDTH-1:0] s_axi_bid_reg = {ID_WIDTH{1'b0}}; +reg [1:0] s_axi_bresp_reg = 2'b0; +reg [BUSER_WIDTH-1:0] s_axi_buser_reg = {BUSER_WIDTH{1'b0}}; +reg s_axi_bvalid_reg = 1'b0, s_axi_bvalid_next; + +// datapath control +reg store_axi_b_input_to_output; + +assign m_axi_bready = m_axi_bready_reg; + +assign s_axi_bid = s_axi_bid_reg; +assign s_axi_bresp = s_axi_bresp_reg; +assign s_axi_buser = BUSER_ENABLE ? s_axi_buser_reg : {BUSER_WIDTH{1'b0}}; +assign s_axi_bvalid = s_axi_bvalid_reg; + +// enable ready input next cycle if output buffer will be empty +wire m_axi_bready_early = !s_axi_bvalid_next; + +always @* begin + // transfer sink ready state to source + s_axi_bvalid_next = s_axi_bvalid_reg; + + store_axi_b_input_to_output = 1'b0; + + if (m_axi_bready_reg) begin + s_axi_bvalid_next = m_axi_bvalid; + store_axi_b_input_to_output = 1'b1; + end else if (s_axi_bready) begin + s_axi_bvalid_next = 1'b0; + end +end + +always @(posedge clk) begin + if (rst) begin + m_axi_bready_reg <= 1'b0; + s_axi_bvalid_reg <= 1'b0; + end else begin + m_axi_bready_reg <= m_axi_bready_early; + s_axi_bvalid_reg <= s_axi_bvalid_next; + end + + // datapath + if (store_axi_b_input_to_output) begin + s_axi_bid_reg <= m_axi_bid; + s_axi_bresp_reg <= m_axi_bresp; + s_axi_buser_reg <= m_axi_buser; + end +end + +end else begin + + // bypass B channel + assign s_axi_bid = m_axi_bid; + assign s_axi_bresp = m_axi_bresp; + assign s_axi_buser = BUSER_ENABLE ? m_axi_buser : {BUSER_WIDTH{1'b0}}; + assign s_axi_bvalid = m_axi_bvalid; + assign m_axi_bready = s_axi_bready; + +end + +endgenerate + +endmodule + +`resetall diff --git a/xls/modules/zstd/priority_encoder.v b/xls/modules/zstd/external/priority_encoder.v similarity index 100% rename from xls/modules/zstd/priority_encoder.v rename to xls/modules/zstd/external/priority_encoder.v diff --git a/xls/modules/zstd/sequence_executor.x b/xls/modules/zstd/sequence_executor.x index 7042dcc847..a6bdbb9205 100644 --- a/xls/modules/zstd/sequence_executor.x +++ b/xls/modules/zstd/sequence_executor.x @@ -14,6 +14,7 @@ import std; import xls.modules.zstd.common as common; +import xls.modules.zstd.memory.mem_writer as mem_writer; import xls.modules.zstd.ram_printer as ram_printer; import xls.examples.ram; @@ -53,6 +54,8 @@ fn ram_addr_width(hb_size_kb: u32) -> u32 { std::clog2(ram_size(hb_size_kb)) } // RAM related constants common for tests const TEST_HISTORY_BUFFER_SIZE_KB = u32:1; +const TEST_DATA_W = u32:64; +const TEST_ADDR_W = u32:16; const TEST_RAM_SIZE = ram_size(TEST_HISTORY_BUFFER_SIZE_KB); const TEST_RAM_ADDR_WIDTH = ram_addr_width(TEST_HISTORY_BUFFER_SIZE_KB); pub const TEST_RAM_INITIALIZED = true; @@ -114,6 +117,36 @@ fn test_decode_literal_packet() { }) } +fn convert_output_packet(packet: ZstdDecodedPacket) -> mem_writer::MemWriterDataPacket { + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; + MemWriterDataPacket { + data: packet.data as uN[DATA_W], + length: std::div_pow2(packet.length, u32:8) as uN[ADDR_W], + last: packet.last + } +} + +#[test] +fn test_convert_output_packet() { + const DATA_W = u32:64; + const ADDR_W = u32:16; + + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; + + let packet = ZstdDecodedPacket { + data: CopyOrMatchContent:0xAA00BB11CC22DD33, + length: BlockPacketLength:64, + last: false + }; + let expected = MemWriterDataPacket { + data: uN[DATA_W]:0xAA00BB11CC22DD33, + length: uN[ADDR_W]:8, + last: false + }; + + assert_eq(convert_output_packet(packet), expected) +} + fn round_up_to_pow2(x: uN[N]) -> uN[N] { let base = x[Y_CLOG2 as s32:]; let reminder = x[0:Y_CLOG2 as s32] != bits[Y_CLOG2]:0; @@ -808,14 +841,18 @@ fn handle_reapeated_offset_for_sequences } pub proc SequenceExecutor { + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; + input_r: chan in; output_s: chan out; + output_mem_wr_data_in_s: chan out; ram_comp_input_s: chan> out; ram_comp_output_r: chan> in; ram_resp_input_s: chan out; @@ -840,6 +877,7 @@ pub proc SequenceExecutor in, output_s: chan out, + output_mem_wr_data_in_s: chan out, ram_resp_output_r: chan in, ram_resp_output_s: chan out, rd_req_m0_s: chan> out, @@ -890,7 +928,7 @@ pub proc SequenceExecutor(output_data); + let tok2_10_1 = send_if(tok1, output_mem_wr_data_in_s, do_write_output, output_mem_wr_data_in); // Ask for response let tok2_11 = send_if(tok1, rd_req_m0_s, (read_reqs[0]).mask != RAM_REQ_MASK_NONE, read_reqs[0]); @@ -1142,14 +1182,18 @@ pub proc SequenceExecutor; init { } config( input_r: chan in, output_s: chan out, + output_mem_wr_data_in_s: chan out, looped_channel_r: chan in, looped_channel_s: chan out, rd_req_m0_s: chan> out, @@ -1185,8 +1229,9 @@ pub proc SequenceExecutorZstd { wr_resp_m6_r: chan in, wr_resp_m7_r: chan in ) { - spawn SequenceExecutor ( - input_r, output_s, + spawn SequenceExecutor ( + input_r, output_s, output_mem_wr_data_in_s, looped_channel_r, looped_channel_s, rd_req_m0_s, rd_req_m1_s, rd_req_m2_s, rd_req_m3_s, rd_req_m4_s, rd_req_m5_s, rd_req_m6_s, rd_req_m7_s, @@ -1298,10 +1343,12 @@ const LITERAL_TEST_MEMORY_CONTENT:(TestRamAddr, RamData)[3][RAM_NUM] = [ #[test_proc] proc SequenceExecutorLiteralsTest { + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; terminator: chan out; input_s: chan> out; output_r: chan in; + output_mem_wr_data_in_r: chan in; print_start_s: chan<()> out; print_finish_r: chan<()> in; @@ -1314,6 +1361,7 @@ proc SequenceExecutorLiteralsTest { config(terminator: chan out) { let (input_s, input_r) = chan>("input"); let (output_s, output_r) = chan("output"); + let (output_mem_wr_data_in_s, output_mem_wr_data_in_r) = chan("output_mem_wr_data_in"); let (looped_channel_s, looped_channel_r) = chan("looped_channels"); @@ -1328,11 +1376,12 @@ proc SequenceExecutorLiteralsTest { let INIT_HB_PTR_ADDR = u32:127; spawn SequenceExecutor< TEST_HISTORY_BUFFER_SIZE_KB, + TEST_DATA_W, TEST_ADDR_W, TEST_RAM_SIZE, TEST_RAM_ADDR_WIDTH, INIT_HB_PTR_ADDR, > ( - input_r, output_s, + input_r, output_s, output_mem_wr_data_in_s, looped_channel_r, looped_channel_s, ram_rd_req_s[0], ram_rd_req_s[1], ram_rd_req_s[2], ram_rd_req_s[3], ram_rd_req_s[4], ram_rd_req_s[5], ram_rd_req_s[6], ram_rd_req_s[7], @@ -1384,7 +1433,7 @@ proc SequenceExecutorLiteralsTest { ( terminator, - input_s, output_r, + input_s, output_r, output_mem_wr_data_in_r, print_start_s, print_finish_r, ram_rd_req_s, ram_rd_resp_r, ram_wr_req_s, ram_wr_resp_r @@ -1404,6 +1453,9 @@ proc SequenceExecutorLiteralsTest { let (tok, recv_data) = recv(tok, output_r); let expected = decode_literal_packet(LITERAL_TEST_INPUT_DATA[i]); assert_eq(expected, recv_data); + let (tok, recv_mem_writer_data) = recv(tok, output_mem_wr_data_in_r); + let expected_mem_writer_data = convert_output_packet(expected); + assert_eq(expected_mem_writer_data, recv_mem_writer_data); } else {} }(()); @@ -1554,10 +1606,12 @@ const SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS:ZstdDecodedPacket[11] = [ #[test_proc] proc SequenceExecutorSequenceTest { + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; terminator: chan out; input_s: chan out; output_r: chan in; + output_mem_wr_data_in_r: chan in; print_start_s: chan<()> out; print_finish_r: chan<()> in; @@ -1570,6 +1624,7 @@ proc SequenceExecutorSequenceTest { config(terminator: chan out) { let (input_s, input_r) = chan("input"); let (output_s, output_r) = chan("output"); + let (output_mem_wr_data_in_s, output_mem_wr_data_in_r) = chan("output_mem_wr_data_in"); let (looped_channel_s, looped_channel_r) = chan("looped_channel"); @@ -1584,11 +1639,12 @@ proc SequenceExecutorSequenceTest { let INIT_HB_PTR_ADDR = u32:127; spawn SequenceExecutor< TEST_HISTORY_BUFFER_SIZE_KB, + TEST_DATA_W, TEST_ADDR_W, TEST_RAM_SIZE, TEST_RAM_ADDR_WIDTH, INIT_HB_PTR_ADDR, > ( - input_r, output_s, + input_r, output_s, output_mem_wr_data_in_s, looped_channel_r, looped_channel_s, ram_rd_req_s[0], ram_rd_req_s[1], ram_rd_req_s[2], ram_rd_req_s[3], ram_rd_req_s[4], ram_rd_req_s[5], ram_rd_req_s[6], ram_rd_req_s[7], @@ -1640,7 +1696,7 @@ proc SequenceExecutorSequenceTest { ( terminator, - input_s, output_r, + input_s, output_r, output_mem_wr_data_in_r, print_start_s, print_finish_r, ram_rd_req_s, ram_rd_resp_r, ram_wr_req_s, ram_wr_resp_r ) @@ -1659,6 +1715,9 @@ proc SequenceExecutorSequenceTest { let (tok, recv_data) = recv(tok, output_r); let expected = decode_literal_packet(LITERAL_TEST_INPUT_DATA[i]); assert_eq(expected, recv_data); + let (tok, recv_mem_writer_data) = recv(tok, output_mem_wr_data_in_r); + let expected_mem_writer_data = convert_output_packet(expected); + assert_eq(expected_mem_writer_data, recv_mem_writer_data); } else {} }(()); diff --git a/xls/modules/zstd/zstd_dec.x b/xls/modules/zstd/zstd_dec.x index 528d16d599..7fc5fb260e 100644 --- a/xls/modules/zstd/zstd_dec.x +++ b/xls/modules/zstd/zstd_dec.x @@ -23,6 +23,7 @@ import xls.modules.zstd.common; import xls.modules.zstd.memory.axi; import xls.modules.zstd.csr_config; import xls.modules.zstd.memory.mem_reader; +import xls.modules.zstd.memory.mem_writer; import xls.modules.zstd.frame_header_dec; import xls.modules.zstd.block_header; import xls.modules.zstd.block_header_dec; @@ -45,7 +46,8 @@ enum ZstdDecoderInternalFsm: u4 { DECODE_RLE_BLOCK = 5, DECODE_COMPRESSED_BLOCK = 6, DECODE_CHECKSUM = 7, - FINISH = 8, + WRITE_OUTPUT = 8, + FINISH = 9, ERROR = 13, INVALID = 15, } @@ -128,6 +130,9 @@ proc ZstdDecoderInternal< type MemReaderReq = mem_reader::MemReaderReq; type MemReaderResp = mem_reader::MemReaderResp; + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + type FrameHeaderDecoderStatus = frame_header_dec::FrameHeaderDecoderStatus; type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; @@ -167,6 +172,10 @@ proc ZstdDecoderInternal< rle_req_s: chan out; rle_resp_r: chan in; + // Output MemWriter + output_mem_wr_req_s: chan out; + output_mem_wr_resp_r: chan in; + notify_s: chan<()> out; reset_s: chan<()> out; @@ -197,6 +206,10 @@ proc ZstdDecoderInternal< rle_req_s: chan out, rle_resp_r: chan in, + // Output MemWriter + output_mem_wr_req_s: chan out, + output_mem_wr_resp_r: chan in, + notify_s: chan<()> out, reset_s: chan<()> out, ) { @@ -206,6 +219,7 @@ proc ZstdDecoderInternal< bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, + output_mem_wr_req_s, output_mem_wr_resp_r, notify_s, reset_s, ) } @@ -259,6 +273,15 @@ proc ZstdDecoderInternal< trace_fmt!("[DECODE_FRAME_HEADER]: Received FH {:#x}", fh_resp); } else {}; + let output_mem_wr_req = MemWriterReq {addr: state.output_buffer, length: fh_resp.header.frame_content_size as uN[AXI_ADDR_W]}; + let tok = send_if(tok0, output_mem_wr_req_s, fh_resp_valid, output_mem_wr_req); + + let do_recv_output_mem_wr_resp = (state.fsm == Fsm::WRITE_OUTPUT); + let (tok_x, output_write_resp, output_write_done) = recv_if_non_blocking(tok0, output_mem_wr_resp_r, do_recv_output_mem_wr_resp, zero!()); + if output_write_done { + trace_fmt!("[WRITE_OUTPUT]: Received response {:#x}", output_write_resp); + } else {}; + let do_send_notify = (state.fsm == Fsm::ERROR || state.fsm == Fsm::FINISH); let tok = send_if(tok0, notify_s, do_send_notify, ()); if do_send_notify { @@ -518,10 +541,22 @@ proc ZstdDecoderInternal< Fsm::DECODE_CHECKSUM => { trace_fmt!("[DECODE_CHECKSUM]"); - State {fsm: Fsm::FINISH, ..zero!() } + State {fsm: Fsm::WRITE_OUTPUT, ..zero!() } }, + Fsm::WRITE_OUTPUT => { + trace_fmt!("[WRITE_OUTPUT]"); + let error = (output_write_resp.status != mem_writer::MemWriterRespStatus::OKAY); + let fsm = match (output_write_done, error) { + (true, false) => Fsm::FINISH, + (true, true) => Fsm::ERROR, + ( _, _) => Fsm::WRITE_OUTPUT, + }; + + State {fsm: fsm, ..zero!() } + }, + Fsm::ERROR => { trace_fmt!("[ERROR]"); State { fsm: Fsm::IDLE, ..zero!() } @@ -586,6 +621,9 @@ proc ZstdDecoderInternalTest { type RleBlockDecoderResp = rle_block_dec::RleBlockDecoderResp; type RleBlockDecoderStatus = rle_block_dec::RleBlockDecoderStatus; + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + terminator: chan out; csr_rd_req_r: chan in; @@ -606,6 +644,9 @@ proc ZstdDecoderInternalTest { rle_req_r: chan in; rle_resp_s: chan out; + output_mem_wr_req_r: chan in; + output_mem_wr_resp_s: chan out; + notify_r: chan<()> in; reset_r: chan<()> in; @@ -630,6 +671,9 @@ proc ZstdDecoderInternalTest { let (rle_req_s, rle_req_r) = chan("rle_req"); let (rle_resp_s, rle_resp_r) = chan("rle_resp"); + let (output_mem_wr_req_s, output_mem_wr_req_r) = chan("output_mem_wr_req"); + let (output_mem_wr_resp_s, output_mem_wr_resp_r) = chan("output_mem_wr_resp"); + let (notify_s, notify_r) = chan<()>("notify"); let (reset_s, reset_r) = chan<()>("reset"); @@ -639,6 +683,7 @@ proc ZstdDecoderInternalTest { bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, + output_mem_wr_req_s, output_mem_wr_resp_r, notify_s, reset_s, ); @@ -649,6 +694,7 @@ proc ZstdDecoderInternalTest { bh_req_r, bh_resp_s, raw_req_r, raw_resp_s, rle_req_r, rle_resp_s, + output_mem_wr_req_r, output_mem_wr_resp_s, notify_r, reset_r, ) } @@ -802,6 +848,9 @@ proc ZstdDecoderInternalTest { let (tok, ()) = recv(tok, notify_r); + let (tok, _) = recv(tok, output_mem_wr_req_r); + let tok = send(tok, output_mem_wr_resp_s, MemWriterResp {status: mem_writer::MemWriterRespStatus::OKAY}); + send(tok, terminator, true); } } @@ -817,6 +866,7 @@ pub proc ZstdDecoder< AXI_DATA_W_DIV8: u32 = {AXI_DATA_W / u32:8}, LOG2_REGS_N: u32 = {std::clog2(REGS_N)}, HB_RAM_N: u32 = {u32:8}, + MEM_WRITER_ID: u32 = {u32:0}, > { type CsrAxiAr = axi::AxiAr; type CsrAxiR = axi::AxiR; @@ -838,6 +888,9 @@ pub proc ZstdDecoder< type MemReaderReq = mem_reader::MemReaderReq; type MemReaderResp = mem_reader::MemReaderResp; + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; type FrameHeaderDecoderReq = frame_header_dec::FrameHeaderDecoderReq; type FrameHeaderDecoderResp = frame_header_dec::FrameHeaderDecoderResp; @@ -885,6 +938,11 @@ pub proc ZstdDecoder< raw_axi_ar_s: chan out, raw_axi_r_r: chan in, + //// AXI Output Writer (manager) + output_axi_aw_s: chan out, + output_axi_w_s: chan out, + output_axi_b_r: chan in, + // History Buffer ram_rd_req_0_s: chan out, ram_rd_req_1_s: chan out, @@ -1031,12 +1089,13 @@ pub proc ZstdDecoder< ); // Sequence Execution - let (seq_exec_looped_s, seq_exec_looped_r) = chan("seq_exec_looped"); let (seq_exec_output_s, seq_exec_output_r) = chan("seq_exec_output"); + let (output_mem_wr_data_in_s, output_mem_wr_data_in_r) = chan("output_mem_wr_data_in"); - spawn sequence_executor::SequenceExecutor( + spawn sequence_executor::SequenceExecutor( seq_exec_input_r, seq_exec_output_s, + output_mem_wr_data_in_s, seq_exec_looped_r, seq_exec_looped_s, ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, @@ -1053,6 +1112,13 @@ pub proc ZstdDecoder< spawn repacketizer::Repacketizer(seq_exec_output_r, output_s); // Zstd Decoder Control + let (output_mem_wr_req_s, output_mem_wr_req_r) = chan("output_mem_wr_req"); + let (output_mem_wr_resp_s, output_mem_wr_resp_r) = chan("output_mem_wr_resp"); + + spawn mem_writer::MemWriter( + output_mem_wr_req_r, output_mem_wr_data_in_r, + output_axi_aw_s, output_axi_w_s, output_axi_b_r, output_mem_wr_resp_s + ); spawn ZstdDecoderInternal ( csr_rd_req_s, csr_rd_resp_r, csr_wr_req_s, csr_wr_resp_r, csr_change_r, @@ -1060,6 +1126,7 @@ pub proc ZstdDecoder< bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, + output_mem_wr_req_s, output_mem_wr_resp_r, notify_s, reset_s, ); @@ -1108,6 +1175,9 @@ proc ZstdDecoderInternalInst { type RleBlockDecoderReq = rle_block_dec::RleBlockDecoderReq; type RleBlockDecoderResp = rle_block_dec::RleBlockDecoderResp; + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + init { } config( @@ -1133,6 +1203,10 @@ proc ZstdDecoderInternalInst { rle_req_s: chan out, rle_resp_r: chan in, + // Output MemWriter + output_mem_wr_req_s: chan out, + output_mem_wr_resp_r: chan in, + // IRQ notify_s: chan<()> out, reset_s: chan<()> out, @@ -1145,6 +1219,7 @@ proc ZstdDecoderInternalInst { bh_req_s, bh_resp_r, raw_req_s, raw_resp_r, rle_req_s, rle_resp_r, + output_mem_wr_req_s, output_mem_wr_resp_r, notify_s, reset_s, ); @@ -1195,6 +1270,11 @@ proc ZstdDecoderInst { raw_axi_ar_s: chan out, raw_axi_r_r: chan in, + //// AXI Output Writer (manager) + output_axi_aw_s: chan out, + output_axi_w_s: chan out, + output_axi_b_r: chan in, + // History Buffer ram_rd_req_0_s: chan out, ram_rd_req_1_s: chan out, @@ -1243,6 +1323,7 @@ proc ZstdDecoderInst { fh_axi_ar_s, fh_axi_r_r, bh_axi_ar_s, bh_axi_r_r, raw_axi_ar_s, raw_axi_r_r, + output_axi_aw_s, output_axi_w_s, output_axi_b_r, ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, ram_rd_resp_0_r, ram_rd_resp_1_r, ram_rd_resp_2_r, ram_rd_resp_3_r, diff --git a/xls/modules/zstd/zstd_dec_cocotb_test.py b/xls/modules/zstd/zstd_dec_cocotb_test.py index e8112c2afb..56f45e8d06 100644 --- a/xls/modules/zstd/zstd_dec_cocotb_test.py +++ b/xls/modules/zstd/zstd_dec_cocotb_test.py @@ -291,6 +291,8 @@ async def test_decoder(dut, seed, block_type, scoreboard, axi_buses, xls_channel expected_decoded_frame = DecompressFrame(encoded.read()) encoded.close() + reference_memory = SparseMemory(mem_size) + reference_memory.write(obuf_addr, expected_decoded_frame) expected_output_packets = generate_expected_output(expected_decoded_frame) assert_expected_output = Event() @@ -303,12 +305,16 @@ async def test_decoder(dut, seed, block_type, scoreboard, axi_buses, xls_channel await configure_decoder(cpu, ibuf_addr, obuf_addr) await start_decoder(cpu) - await assert_expected_output.wait() + await assert_notify.wait() await wait_for_idle(cpu) - # TODO: Check decoded frame in memory under `obuf_addr` when ZstdDecoder - # will fully support memory output interface + # Read decoded frame in chunks of AXI_DATA_W length + # Compare against frame decompressed with the reference library + for read_op in range(0, ((len(expected_decoded_frame) + (AXI_DATA_W_BYTES - 1)) // AXI_DATA_W_BYTES)): + addr = obuf_addr + (read_op * AXI_DATA_W_BYTES) + mem_contents = memory.read(addr, AXI_DATA_W_BYTES) + exp_mem_contents = reference_memory.read(addr, AXI_DATA_W_BYTES) + assert mem_contents == exp_mem_contents, "{} bytes of memory contents at address {} don't match the expected contents:\n{}\nvs\n{}".format(AXI_DATA_W_BYTES, hex(addr), hex(int.from_bytes(mem_contents, byteorder='little')), hex(int.from_bytes(exp_mem_contents, byteorder='little'))) - await assert_notify.wait() await ClockCycles(dut.clk, 20) @cocotb.test(timeout_time=50, timeout_unit="ms") @@ -320,73 +326,73 @@ async def zstd_reset_test(dut): await test_reset(dut) #FIXME: Rework testbench to decode multiple ZSTD frames in one test -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_1(dut): block_type = BlockType.RAW (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 1, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_2(dut): block_type = BlockType.RAW (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 2, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_3(dut): block_type = BlockType.RAW (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 3, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_4(dut): block_type = BlockType.RAW (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 4, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_5(dut): block_type = BlockType.RAW (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 5, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_1(dut): block_type = BlockType.RLE (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 1, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_2(dut): block_type = BlockType.RLE (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 2, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_3(dut): block_type = BlockType.RLE (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 3, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_4(dut): block_type = BlockType.RLE (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 4, block_type, scoreboard, axi_buses, xls_channels, cpu) -@cocotb.test(timeout_time=20000, timeout_unit="ms") +@cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_5(dut): block_type = BlockType.RLE (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) await test_decoder(dut, 5, block_type, scoreboard, axi_buses, xls_channels, cpu) -#@cocotb.test(timeout_time=20000, timeout_unit="ms") +#@cocotb.test(timeout_time=1000, timeout_unit="ms") #async def zstd_compressed_frames_test(dut): # test_cases = 1 # block_type = BlockType.COMPRESSED # await test_decoder(dut, test_cases, block_type) # -#@cocotb.test(timeout_time=20000, timeout_unit="ms") +#@cocotb.test(timeout_time=1000, timeout_unit="ms") #async def zstd_random_frames_test(dut): # test_cases = 1 # block_type = BlockType.RANDOM @@ -398,10 +404,15 @@ async def zstd_rle_frames_test_5(dut): "xls/modules/zstd/zstd_dec.v", "xls/modules/zstd/xls_fifo_wrapper.v", "xls/modules/zstd/zstd_dec_wrapper.v", - "xls/modules/zstd/axi_interconnect_wrapper.v", - "xls/modules/zstd/axi_interconnect.v", - "xls/modules/zstd/arbiter.v", - "xls/modules/zstd/priority_encoder.v", + "xls/modules/zstd/external/axi_crossbar_wrapper.v", + "xls/modules/zstd/external/axi_crossbar.v", + "xls/modules/zstd/external/axi_crossbar_rd.v", + "xls/modules/zstd/external/axi_crossbar_wr.v", + "xls/modules/zstd/external/axi_crossbar_addr.v", + "xls/modules/zstd/external/axi_register_rd.v", + "xls/modules/zstd/external/axi_register_wr.v", + "xls/modules/zstd/external/arbiter.v", + "xls/modules/zstd/external/priority_encoder.v", ] test_module=[Path(__file__).stem] run_test(toplevel, test_module, verilog_sources) diff --git a/xls/modules/zstd/zstd_dec_test.x b/xls/modules/zstd/zstd_dec_test.x index a166e1743d..a57f7e499e 100644 --- a/xls/modules/zstd/zstd_dec_test.x +++ b/xls/modules/zstd/zstd_dec_test.x @@ -53,6 +53,8 @@ const TEST_RAM_BASE_ADDR:u32 = u32:0; const TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; const TEST_RAM_INITIALIZED = true; +const TEST_MOCK_OUTPUT_RAM_SIZE:u32 = TEST_RAM_SIZE / TEST_AXI_DATA_W_DIV8; + fn csr_addr(c: zstd_dec::Csr) -> uN[TEST_AXI_ADDR_W] { (c as uN[TEST_AXI_ADDR_W]) << 3 } @@ -64,19 +66,19 @@ proc ZstdDecoderTest { type CsrAxiAw = axi::AxiAw; type CsrAxiW = axi::AxiW; type CsrAxiB = axi::AxiB; - + type CsrRdReq = csr_config::CsrRdReq; type CsrRdResp = csr_config::CsrRdResp; type CsrWrReq = csr_config::CsrWrReq; type CsrWrResp = csr_config::CsrWrResp; type CsrChange = csr_config::CsrChange; - + type MemAxiAr = axi::AxiAr; type MemAxiR = axi::AxiR; type MemAxiAw = axi::AxiAw; type MemAxiW = axi::AxiW; type MemAxiB = axi::AxiB; - + type RamRdReqHB = ram::ReadReq; type RamRdRespHB = ram::ReadResp; type RamWrReqHB = ram::WriteReq; @@ -86,7 +88,7 @@ proc ZstdDecoderTest { type RamRdResp = ram::ReadResp; type RamWrReq = ram::WriteReq; type RamWrResp = ram::WriteResp; - + type ZstdDecodedPacket = common::ZstdDecodedPacket; terminator: chan out; csr_axi_aw_s: chan out; @@ -100,6 +102,9 @@ proc ZstdDecoderTest { bh_axi_r_s: chan out; raw_axi_ar_r: chan in; raw_axi_r_s: chan out; + output_axi_aw_r: chan in; + output_axi_w_r: chan in; + output_axi_b_s: chan out; ram_rd_req_r: chan[8] in; ram_rd_resp_s: chan[8] out; @@ -116,31 +121,35 @@ proc ZstdDecoderTest { output_r: chan in; notify_r: chan<()> in; reset_r: chan<()> in; - + init {} - + config(terminator: chan out) { - + let (csr_axi_aw_s, csr_axi_aw_r) = chan("csr_axi_aw"); let (csr_axi_w_s, csr_axi_w_r) = chan("csr_axi_w"); let (csr_axi_b_s, csr_axi_b_r) = chan("csr_axi_b"); let (csr_axi_ar_s, csr_axi_ar_r) = chan("csr_axi_ar"); let (csr_axi_r_s, csr_axi_r_r) = chan("csr_axi_r"); - + let (fh_axi_ar_s, fh_axi_ar_r) = chan("fh_axi_ar"); let (fh_axi_r_s, fh_axi_r_r) = chan("fh_axi_r"); - + let (bh_axi_ar_s, bh_axi_ar_r) = chan("bh_axi_ar"); let (bh_axi_r_s, bh_axi_r_r) = chan("bh_axi_r"); - + let (raw_axi_ar_s, raw_axi_ar_r) = chan("raw_axi_ar"); let (raw_axi_r_s, raw_axi_r_r) = chan("raw_axi_r"); + let (output_axi_aw_s, output_axi_aw_r) = chan("output_axi_aw"); + let (output_axi_w_s, output_axi_w_r) = chan("output_axi_w"); + let (output_axi_b_s, output_axi_b_r) = chan("output_axi_b"); + let (ram_rd_req_s, ram_rd_req_r) = chan[8]("ram_rd_req"); let (ram_rd_resp_s, ram_rd_resp_r) = chan[8]("ram_rd_resp"); let (ram_wr_req_s, ram_wr_req_r) = chan[8]("ram_wr_req"); let (ram_wr_resp_s, ram_wr_resp_r) = chan[8]("ram_wr_resp"); - + let (ram_rd_req_fh_s, ram_rd_req_fh_r) = chan("ram_rd_req_fh"); let (ram_rd_req_bh_s, ram_rd_req_bh_r) = chan("ram_rd_req_bh"); let (ram_rd_req_raw_s, ram_rd_req_raw_r) = chan("ram_rd_req_raw"); @@ -158,7 +167,7 @@ proc ZstdDecoderTest { let (output_s, output_r) = chan("output"); let (notify_s, notify_r) = chan<()>("notify"); let (reset_s, reset_r) = chan<()>("reset"); - + spawn zstd_dec::ZstdDecoder< TEST_AXI_DATA_W, TEST_AXI_ADDR_W, TEST_AXI_ID_W, TEST_AXI_DEST_W, TEST_REGS_N, TEST_WINDOW_LOG_MAX, @@ -168,6 +177,7 @@ proc ZstdDecoderTest { fh_axi_ar_s, fh_axi_r_r, bh_axi_ar_s, bh_axi_r_r, raw_axi_ar_s, raw_axi_r_r, + output_axi_aw_s, output_axi_w_s, output_axi_b_r, ram_rd_req_s[0], ram_rd_req_s[1], ram_rd_req_s[2], ram_rd_req_s[3], ram_rd_req_s[4], ram_rd_req_s[5], ram_rd_req_s[6], ram_rd_req_s[7], ram_rd_resp_r[0], ram_rd_resp_r[1], ram_rd_resp_r[2], ram_rd_resp_r[3], @@ -178,49 +188,49 @@ proc ZstdDecoderTest { ram_wr_resp_r[4], ram_wr_resp_r[5], ram_wr_resp_r[6], ram_wr_resp_r[7], output_s, notify_s, reset_s, ); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[0], ram_rd_resp_s[0], ram_wr_req_r[0], ram_wr_resp_s[0]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[1], ram_rd_resp_s[1], ram_wr_req_r[1], ram_wr_resp_s[1]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[2], ram_rd_resp_s[2], ram_wr_req_r[2], ram_wr_resp_s[2]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[3], ram_rd_resp_s[3], ram_wr_req_r[3], ram_wr_resp_s[3]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[4], ram_rd_resp_s[4], ram_wr_req_r[4], ram_wr_resp_s[4]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[5], ram_rd_resp_s[5], ram_wr_req_r[5], ram_wr_resp_s[5]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, TEST_HB_RAM_ASSERT_VALID_READ > (ram_rd_req_r[6], ram_rd_resp_s[6], ram_wr_req_r[6], ram_wr_resp_s[6]); - + spawn ram::RamModel< TEST_HB_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_WORD_PARTITION_SIZE, TEST_HB_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HB_RAM_INITIALIZED, @@ -231,7 +241,7 @@ proc ZstdDecoderTest { TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_WORD_PARTITION_SIZE, TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, > (ram_rd_req_fh_r, ram_rd_resp_fh_s, ram_wr_req_fh_r, ram_wr_resp_fh_s); - + spawn ram::RamModel< TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_WORD_PARTITION_SIZE, TEST_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_RAM_INITIALIZED, @@ -263,20 +273,21 @@ proc ZstdDecoderTest { fh_axi_ar_r, fh_axi_r_s, bh_axi_ar_r, bh_axi_r_s, raw_axi_ar_r, raw_axi_r_s, + output_axi_aw_r, output_axi_w_r, output_axi_b_s, ram_rd_req_r, ram_rd_resp_s, ram_wr_req_r, ram_wr_resp_s, ram_wr_req_fh_s, ram_wr_req_bh_s, ram_wr_req_raw_s, ram_wr_resp_fh_r, ram_wr_resp_bh_r, ram_wr_resp_raw_r, output_r, notify_r, reset_r, ) } - + next (state: ()) { trace_fmt!("Test start"); let frames_count = array_size(zstd_frame_testcases::FRAMES); let tok = join(); let tok = unroll_for! (test_i, tok): (u32, token) in range(u32:0, frames_count) { - trace_fmt!("Loading testcase {:x}", test_i); + trace_fmt!("Loading testcase {:x}", test_i + u32:1); let frame = zstd_frame_testcases::FRAMES[test_i]; let tok = for (i, tok): (u32, token) in range(u32:0, frame.array_length) { let req = RamWrReq { @@ -290,7 +301,7 @@ proc ZstdDecoderTest { tok }(tok); - trace_fmt!("Running decoder on testcase {:x}", test_i); + trace_fmt!("Running decoder on testcase {:x}", test_i + u32:1); let addr_req = axi::AxiAw { id: uN[TEST_AXI_ID_W]:0, addr: uN[TEST_AXI_ADDR_W]:0, @@ -357,10 +368,78 @@ proc ZstdDecoderTest { tok }(tok); + // Test ZstdDecoder memory output interface + // Mock the output memory buffer as a DSLX array + // It is required to handle AXI write transactions and to write the incoming data to + // the DSXL array. + // The number of AXI transactions is not known beforehand because it depends on the + // length of the decoded data and the address of the output buffer. The same goes + // with the lengths of the particular AXI burst transactions (the number of transfers). + // Because of that we cannot write for loops to handle AXI transactions dynamically. + // As a workaround, the loops are constrained with upper bounds for AXI transactions + // required for writing maximal supported payload and maximal possible burst transfer + // size. + + // It is possible to decode payloads up to 16kB + // The smallest possible AXI transaction will transfer 1 byte of data + let MAX_AXI_TRANSACTIONS = u32:16384; + // The maximal number if beats in AXI burst transaction + let MAX_AXI_TRANSFERS = u32:256; + // Actual size of decompressed payload for current test + let DECOMPRESSED_BYTES = zstd_frame_testcases::DECOMPRESSED_FRAMES[test_i].length; + trace_fmt!("ZstdDecTest: Start receiving output"); + let (tok, final_output_memory, final_output_memory_id, final_transfered_bytes) = + for (axi_transaction, (tok, output_memory, output_memory_id, transfered_bytes)): + (u32, (token, uN[TEST_AXI_DATA_W][TEST_MOCK_OUTPUT_RAM_SIZE], u32, u32)) + in range(u32:0, MAX_AXI_TRANSACTIONS) { + if (transfered_bytes < DECOMPRESSED_BYTES) { + trace_fmt!("ZstdDecTest: Handle AXI Write transaction #{}", axi_transaction); + let (tok, axi_aw) = recv(tok, output_axi_aw_r); + trace_fmt!("ZstdDecTest: Received AXI AW: {:#x}", axi_aw); + let (tok, internal_output_memory, internal_output_memory_id, internal_transfered_bytes) = + for (axi_transfer, (tok, out_mem, out_mem_id, transf_bytes)): + (u32, (token, uN[TEST_AXI_DATA_W][TEST_MOCK_OUTPUT_RAM_SIZE], u32, u32)) + in range(u32:0, MAX_AXI_TRANSFERS) { + if (axi_transfer as u8 <= axi_aw.len) { + // Receive AXI burst beat transfers + let (tok, axi_w) = recv(tok, output_axi_w_r); + trace_fmt!("ZstdDecTest: Received AXI W #{}: {:#x}", axi_transfer, axi_w); + let strobe_cnt = std::popcount(axi_w.strb) as u32; + // Assume continuous strobe, e.g.: 0b1111; 0b0111; 0b0011; 0b0001; 0b0000 + let strobe_mask = (uN[TEST_AXI_DATA_W]:1 << (strobe_cnt * u32:8) as uN[TEST_AXI_DATA_W]) - uN[TEST_AXI_DATA_W]:1; + let strobed_data = axi_w.data & strobe_mask; + trace_fmt!("ZstdDecTest: write out_mem[{}] = {:#x}", out_mem_id, strobed_data); + let mem = update(out_mem, out_mem_id, (out_mem[out_mem_id] & !strobe_mask) | strobed_data); + let id = out_mem_id + u32:1; + let bytes_written = transf_bytes + strobe_cnt; + trace_fmt!("ZstdDecTest: bytes written: {}", bytes_written); + (tok, mem, id, bytes_written) + } else { + (tok, out_mem, out_mem_id, transf_bytes) + } + // Pass outer loop accumulator as initial accumulator for inner loop + }((tok, output_memory, output_memory_id, transfered_bytes)); + let axi_b = axi::AxiB{resp: axi::AxiWriteResp::OKAY, id: axi_aw.id}; + let tok = send(tok, output_axi_b_s, axi_b); + trace_fmt!("ZstdDecTest: Sent AXI B #{}: {:#x}", axi_transaction, axi_b); + (tok, internal_output_memory, internal_output_memory_id, internal_transfered_bytes) + } else { + (tok, output_memory, output_memory_id, transfered_bytes) + } + }((tok, uN[TEST_AXI_DATA_W][TEST_MOCK_OUTPUT_RAM_SIZE]:[uN[TEST_AXI_DATA_W]:0, ...], u32:0, u32:0)); + trace_fmt!("ZstdDecTest: Finished receiving output"); + + assert_eq(final_transfered_bytes, DECOMPRESSED_BYTES); + assert_eq(final_output_memory_id, decomp_frame.array_length); + for (memory_id, _): (u32, ()) in range(u32:0, decomp_frame.array_length) { + assert_eq(final_output_memory[memory_id], decomp_frame.data[memory_id]); + }(()); + let (tok, ()) = recv(tok, notify_r); - trace_fmt!("Finished decoding testcase {:x} correctly", test_i); + trace_fmt!("Finished decoding testcase {:x} correctly", test_i + u32:1); tok }(tok); + send(tok, terminator, true); } } diff --git a/xls/modules/zstd/zstd_dec_wrapper.v b/xls/modules/zstd/zstd_dec_wrapper.v index 304c5f061b..6059d2231f 100644 --- a/xls/modules/zstd/zstd_dec_wrapper.v +++ b/xls/modules/zstd/zstd_dec_wrapper.v @@ -17,7 +17,8 @@ module zstd_dec_wrapper #( parameter AXI_DATA_W = 64, parameter AXI_ADDR_W = 16, - parameter AXI_ID_W = 4, + parameter S_AXI_ID_W = 4, + parameter M_AXI_ID_W = 6, parameter AXI_STRB_W = 8, parameter AWUSER_WIDTH = 1, parameter WUSER_WIDTH = 1, @@ -30,7 +31,7 @@ module zstd_dec_wrapper #( input wire rst, // AXI Master interface for the memory connection - output wire [AXI_ID_W-1:0] memory_axi_aw_awid, + output wire [M_AXI_ID_W-1:0] memory_axi_aw_awid, output wire [AXI_ADDR_W-1:0] memory_axi_aw_awaddr, output wire [7:0] memory_axi_aw_awlen, output wire [2:0] memory_axi_aw_awsize, @@ -49,12 +50,12 @@ module zstd_dec_wrapper #( output wire [WUSER_WIDTH-1:0] memory_axi_w_wuser, output wire memory_axi_w_wvalid, input wire memory_axi_w_wready, - input wire [AXI_ID_W-1:0] memory_axi_b_bid, + input wire [M_AXI_ID_W-1:0] memory_axi_b_bid, input wire [2:0] memory_axi_b_bresp, input wire [BUSER_WIDTH-1:0] memory_axi_b_buser, input wire memory_axi_b_bvalid, output wire memory_axi_b_bready, - output wire [AXI_ID_W-1:0] memory_axi_ar_arid, + output wire [M_AXI_ID_W-1:0] memory_axi_ar_arid, output wire [AXI_ADDR_W-1:0] memory_axi_ar_araddr, output wire [7:0] memory_axi_ar_arlen, output wire [2:0] memory_axi_ar_arsize, @@ -67,7 +68,7 @@ module zstd_dec_wrapper #( output wire [ARUSER_WIDTH-1:0] memory_axi_ar_aruser, output wire memory_axi_ar_arvalid, input wire memory_axi_ar_arready, - input wire [AXI_ID_W-1:0] memory_axi_r_rid, + input wire [M_AXI_ID_W-1:0] memory_axi_r_rid, input wire [AXI_DATA_W-1:0] memory_axi_r_rdata, input wire [2:0] memory_axi_r_rresp, input wire memory_axi_r_rlast, @@ -76,7 +77,7 @@ module zstd_dec_wrapper #( output wire memory_axi_r_rready, // AXI Slave interface for the CSR access - input wire [AXI_ID_W-1:0] csr_axi_aw_awid, + input wire [S_AXI_ID_W-1:0] csr_axi_aw_awid, input wire [AXI_ADDR_W-1:0] csr_axi_aw_awaddr, input wire [7:0] csr_axi_aw_awlen, input wire [2:0] csr_axi_aw_awsize, @@ -95,12 +96,12 @@ module zstd_dec_wrapper #( input wire [WUSER_WIDTH-1:0] csr_axi_w_wuser, input wire csr_axi_w_wvalid, output wire csr_axi_w_wready, - output wire [AXI_ID_W-1:0] csr_axi_b_bid, + output wire [S_AXI_ID_W-1:0] csr_axi_b_bid, output wire [2:0] csr_axi_b_bresp, output wire [BUSER_WIDTH-1:0] csr_axi_b_buser, output wire csr_axi_b_bvalid, input wire csr_axi_b_bready, - input wire [AXI_ID_W-1:0] csr_axi_ar_arid, + input wire [S_AXI_ID_W-1:0] csr_axi_ar_arid, input wire [AXI_ADDR_W-1:0] csr_axi_ar_araddr, input wire [7:0] csr_axi_ar_arlen, input wire [2:0] csr_axi_ar_arsize, @@ -113,7 +114,7 @@ module zstd_dec_wrapper #( input wire [ARUSER_WIDTH-1:0] csr_axi_ar_aruser, input wire csr_axi_ar_arvalid, output wire csr_axi_ar_arready, - output wire [AXI_ID_W-1:0] csr_axi_r_rid, + output wire [S_AXI_ID_W-1:0] csr_axi_r_rid, output wire [AXI_DATA_W-1:0] csr_axi_r_rdata, output wire [2:0] csr_axi_r_rresp, output wire csr_axi_r_rlast, @@ -146,7 +147,7 @@ module zstd_dec_wrapper #( // RawBlockDecoder wire raw_block_decoder_axi_ar_arvalid; wire raw_block_decoder_axi_ar_arready; - wire [ AXI_ID_W-1:0] raw_block_decoder_axi_ar_arid; + wire [S_AXI_ID_W-1:0] raw_block_decoder_axi_ar_arid; wire [AXI_ADDR_W-1:0] raw_block_decoder_axi_ar_araddr; wire [ 3:0] raw_block_decoder_axi_ar_arregion; wire [ 7:0] raw_block_decoder_axi_ar_arlen; @@ -158,7 +159,7 @@ module zstd_dec_wrapper #( wire raw_block_decoder_axi_r_rvalid; wire raw_block_decoder_axi_r_rready; - wire [ AXI_ID_W-1:0] raw_block_decoder_axi_r_rid; + wire [S_AXI_ID_W-1:0] raw_block_decoder_axi_r_rid; wire [AXI_DATA_W-1:0] raw_block_decoder_axi_r_rdata; wire [ 2:0] raw_block_decoder_axi_r_rresp; wire raw_block_decoder_axi_r_rlast; @@ -167,7 +168,7 @@ module zstd_dec_wrapper #( // BlockHeaderDecoder wire block_header_decoder_axi_ar_arvalid; wire block_header_decoder_axi_ar_arready; - wire [ AXI_ID_W-1:0] block_header_decoder_axi_ar_arid; + wire [S_AXI_ID_W-1:0] block_header_decoder_axi_ar_arid; wire [AXI_ADDR_W-1:0] block_header_decoder_axi_ar_araddr; wire [ 3:0] block_header_decoder_axi_ar_arregion; wire [ 7:0] block_header_decoder_axi_ar_arlen; @@ -179,7 +180,7 @@ module zstd_dec_wrapper #( wire block_header_decoder_axi_r_rvalid; wire block_header_decoder_axi_r_rready; - wire [ AXI_ID_W-1:0] block_header_decoder_axi_r_rid; + wire [S_AXI_ID_W-1:0] block_header_decoder_axi_r_rid; wire [AXI_DATA_W-1:0] block_header_decoder_axi_r_rdata; wire [ 2:0] block_header_decoder_axi_r_rresp; wire block_header_decoder_axi_r_rlast; @@ -188,7 +189,7 @@ module zstd_dec_wrapper #( // FrameHeaderDecoder wire frame_header_decoder_axi_ar_arvalid; wire frame_header_decoder_axi_ar_arready; - wire [ AXI_ID_W-1:0] frame_header_decoder_axi_ar_arid; + wire [S_AXI_ID_W-1:0] frame_header_decoder_axi_ar_arid; wire [AXI_ADDR_W-1:0] frame_header_decoder_axi_ar_araddr; wire [ 3:0] frame_header_decoder_axi_ar_arregion; wire [ 7:0] frame_header_decoder_axi_ar_arlen; @@ -200,7 +201,7 @@ module zstd_dec_wrapper #( wire frame_header_decoder_axi_r_rvalid; wire frame_header_decoder_axi_r_rready; - wire [ AXI_ID_W-1:0] frame_header_decoder_axi_r_rid; + wire [S_AXI_ID_W-1:0] frame_header_decoder_axi_r_rid; wire [AXI_DATA_W-1:0] frame_header_decoder_axi_r_rdata; wire [ 2:0] frame_header_decoder_axi_r_rresp; wire frame_header_decoder_axi_r_rlast; @@ -210,35 +211,35 @@ module zstd_dec_wrapper #( * MemWriter AXI interfaces */ - // SequenceExecutor - wire [ AXI_ID_W-1:0] sequence_executor_axi_aw_awid; - wire [AXI_ADDR_W-1:0] sequence_executor_axi_aw_awaddr; - wire [ 2:0] sequence_executor_axi_aw_awsize; - wire [ 7:0] sequence_executor_axi_aw_awlen; - wire [ 1:0] sequence_executor_axi_aw_awburst; - wire sequence_executor_axi_aw_awvalid; - wire sequence_executor_axi_aw_awready; - - wire [AXI_DATA_W-1:0] sequence_executor_axi_w_wdata; - wire [AXI_STRB_W-1:0] sequence_executor_axi_w_wstrb; - wire sequence_executor_axi_w_wlast; - wire sequence_executor_axi_w_wvalid; - wire sequence_executor_axi_w_wready; - - wire [ AXI_ID_W-1:0] sequence_executor_axi_b_bid; - wire [ 2:0] sequence_executor_axi_b_bresp; - wire sequence_executor_axi_b_bvalid; - wire sequence_executor_axi_b_bready; + // Output Writer + wire [S_AXI_ID_W-1:0] output_axi_aw_awid; + wire [AXI_ADDR_W-1:0] output_axi_aw_awaddr; + wire [ 2:0] output_axi_aw_awsize; + wire [ 7:0] output_axi_aw_awlen; + wire [ 1:0] output_axi_aw_awburst; + wire output_axi_aw_awvalid; + wire output_axi_aw_awready; + + wire [AXI_DATA_W-1:0] output_axi_w_wdata; + wire [AXI_STRB_W-1:0] output_axi_w_wstrb; + wire output_axi_w_wlast; + wire output_axi_w_wvalid; + wire output_axi_w_wready; + + wire [S_AXI_ID_W-1:0] output_axi_b_bid; + wire [ 2:0] output_axi_b_bresp; + wire output_axi_b_bvalid; + wire output_axi_b_bready; /* * XLS Channels representing AXI interfaces */ - localparam XLS_AXI_AW_W = AXI_ADDR_W + AXI_ID_W + 3 + 2 + 8; + localparam XLS_AXI_AW_W = AXI_ADDR_W + S_AXI_ID_W + 3 + 2 + 8; localparam XLS_AXI_W_W = AXI_DATA_W + AXI_STRB_W + 1; - localparam XLS_AXI_B_W = 3 + AXI_ID_W; - localparam XLS_AXI_AR_W = AXI_ID_W + AXI_ADDR_W + 4 + 8 + 3 + 2 + 4 + 3 + 4; - localparam XLS_AXI_R_W = AXI_ID_W + AXI_DATA_W + 3 + 1; + localparam XLS_AXI_B_W = 3 + S_AXI_ID_W; + localparam XLS_AXI_AR_W = S_AXI_ID_W + AXI_ADDR_W + 4 + 8 + 3 + 2 + 4 + 3 + 4; + localparam XLS_AXI_R_W = S_AXI_ID_W + AXI_DATA_W + 3 + 1; // CSR wire [XLS_AXI_AW_W-1:0] zstd_dec__csr_axi_aw; wire zstd_dec__csr_axi_aw_rdy; @@ -280,6 +281,17 @@ module zstd_dec_wrapper #( wire zstd_dec__raw_axi_r_rdy; wire zstd_dec__raw_axi_r_vld; + // Output Memory Interface + wire [XLS_AXI_AW_W-1:0] zstd_dec__output_axi_aw; + wire zstd_dec__output_axi_aw_rdy; + wire zstd_dec__output_axi_aw_vld; + wire [XLS_AXI_W_W-1:0] zstd_dec__output_axi_w; + wire zstd_dec__output_axi_w_rdy; + wire zstd_dec__output_axi_w_vld; + wire [XLS_AXI_B_W-1:0] zstd_dec__output_axi_b; + wire zstd_dec__output_axi_b_rdy; + wire zstd_dec__output_axi_b_vld; + // Output packet wire [63:0] output_data_data_field; wire [31:0] output_data_length_field; @@ -400,6 +412,30 @@ module zstd_dec_wrapper #( assign zstd_dec__raw_axi_r_vld = raw_block_decoder_axi_r_rvalid; assign raw_block_decoder_axi_r_rready = zstd_dec__raw_axi_r_rdy; + // Output Writer + assign { + output_axi_aw_awid, + output_axi_aw_awaddr, + output_axi_aw_awsize, + output_axi_aw_awlen, + output_axi_aw_awburst + } = zstd_dec__output_axi_aw; + assign output_axi_aw_awvalid = zstd_dec__output_axi_aw_vld; + assign zstd_dec__output_axi_aw_rdy = output_axi_aw_awready; + assign { + output_axi_w_wdata, + output_axi_w_wstrb, + output_axi_w_wlast + } = zstd_dec__output_axi_w; + assign output_axi_w_wvalid = zstd_dec__output_axi_w_vld; + assign zstd_dec__output_axi_w_rdy = output_axi_w_wready; + assign zstd_dec__output_axi_b = { + output_axi_b_bresp, + output_axi_b_bid + }; + assign zstd_dec__output_axi_b_vld = output_axi_b_bvalid; + assign output_axi_b_bready = zstd_dec__output_axi_b_rdy; + assign csr_axi_b_buser = 1'b0; assign csr_axi_r_ruser = 1'b0; assign notify_data = notify_vld; @@ -460,6 +496,17 @@ module zstd_dec_wrapper #( .zstd_dec__raw_axi_r_r_vld(zstd_dec__raw_axi_r_vld), .zstd_dec__raw_axi_r_r_rdy(zstd_dec__raw_axi_r_rdy), + // Output Writer + .zstd_dec__output_axi_aw_s(zstd_dec__output_axi_aw), + .zstd_dec__output_axi_aw_s_vld(zstd_dec__output_axi_aw_vld), + .zstd_dec__output_axi_aw_s_rdy(zstd_dec__output_axi_aw_rdy), + .zstd_dec__output_axi_w_s(zstd_dec__output_axi_w), + .zstd_dec__output_axi_w_s_vld(zstd_dec__output_axi_w_vld), + .zstd_dec__output_axi_w_s_rdy(zstd_dec__output_axi_w_rdy), + .zstd_dec__output_axi_b_r(zstd_dec__output_axi_b), + .zstd_dec__output_axi_b_r_vld(zstd_dec__output_axi_b_vld), + .zstd_dec__output_axi_b_r_rdy(zstd_dec__output_axi_b_rdy), + // Other ports .zstd_dec__notify_s_vld(notify_vld), .zstd_dec__notify_s_rdy(notify_rdy), @@ -567,19 +614,20 @@ module zstd_dec_wrapper #( assign frame_header_decoder_axi_r_rresp[2] = '0; assign block_header_decoder_axi_r_rresp[2] = '0; assign raw_block_decoder_axi_r_rresp[2] = '0; - assign sequence_executor_axi_b_bresp[2] = '0; + assign output_axi_b_bresp[2] = '0; assign memory_axi_b_bresp[2] = '0; assign memory_axi_r_rresp[2] = '0; /* * AXI Interconnect */ - axi_interconnect_wrapper #( + axi_crossbar_wrapper #( .DATA_WIDTH(AXI_DATA_W), .ADDR_WIDTH(AXI_ADDR_W), .M00_ADDR_WIDTH(AXI_ADDR_W), .M00_BASE_ADDR(32'd0), .STRB_WIDTH(AXI_STRB_W), - .ID_WIDTH(AXI_ID_W) + .S_ID_WIDTH(S_AXI_ID_W), + .M_ID_WIDTH(M_AXI_ID_W) ) axi_memory_interconnect ( .clk(clk), .rst(rst), @@ -720,29 +768,29 @@ module zstd_dec_wrapper #( .s02_axi_rready(raw_block_decoder_axi_r_rready), // SequenceExecutor - .s03_axi_awid('0), - .s03_axi_awaddr('0), - .s03_axi_awlen('0), - .s03_axi_awsize('0), - .s03_axi_awburst('0), + .s03_axi_awid(output_axi_aw_awid), + .s03_axi_awaddr(output_axi_aw_awaddr), + .s03_axi_awlen(output_axi_aw_awlen), + .s03_axi_awsize(output_axi_aw_awsize), + .s03_axi_awburst(output_axi_aw_awburst), .s03_axi_awlock('0), .s03_axi_awcache('0), .s03_axi_awprot('0), .s03_axi_awqos('0), .s03_axi_awuser('0), - .s03_axi_awvalid('0), - .s03_axi_awready(), - .s03_axi_wdata('0), - .s03_axi_wstrb('0), - .s03_axi_wlast('0), + .s03_axi_awvalid(output_axi_aw_awvalid), + .s03_axi_awready(output_axi_aw_awready), + .s03_axi_wdata(output_axi_w_wdata), + .s03_axi_wstrb(output_axi_w_wstrb), + .s03_axi_wlast(output_axi_w_wlast), .s03_axi_wuser('0), - .s03_axi_wvalid('0), - .s03_axi_wready(), - .s03_axi_bid(), - .s03_axi_bresp(), + .s03_axi_wvalid(output_axi_w_wvalid), + .s03_axi_wready(output_axi_w_wready), + .s03_axi_bid(output_axi_b_bid), + .s03_axi_bresp(output_axi_b_bresp), .s03_axi_buser(), - .s03_axi_bvalid(), - .s03_axi_bready('0), + .s03_axi_bvalid(output_axi_b_bvalid), + .s03_axi_bready(output_axi_b_bready), .s03_axi_arid('0), .s03_axi_araddr('0), .s03_axi_arlen('0), From b0a452fa484c0ae7a706e6da010ff75df3ebfebb Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Mon, 18 Nov 2024 09:41:21 +0100 Subject: [PATCH 37/46] modules/zstd/zstd_dec: Remove stream-based output interface * Remove Repacketizer proc * Remove stream-based output channels from * SequenceExecutor * ZstdDecoder Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 78 -------- xls/modules/zstd/repacketizer.x | 215 ----------------------- xls/modules/zstd/sequence_executor.x | 124 ++++++------- xls/modules/zstd/zstd_dec.x | 25 +-- xls/modules/zstd/zstd_dec_cocotb_test.py | 79 +++------ xls/modules/zstd/zstd_dec_test.x | 13 +- xls/modules/zstd/zstd_dec_wrapper.v | 23 +-- 7 files changed, 91 insertions(+), 466 deletions(-) delete mode 100644 xls/modules/zstd/repacketizer.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 2df52f4b18..e1baa49449 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -713,83 +713,6 @@ place_and_route( target_die_utilization_percentage = "10", ) -xls_dslx_library( - name = "repacketizer_dslx", - srcs = [ - "repacketizer.x", - ], - deps = [ - ":common_dslx", - ], -) - -xls_dslx_test( - name = "repacketizer_dslx_test", - dslx_test_args = {"compare": "jit"}, - library = ":repacketizer_dslx", - tags = ["manual"], -) - -repacketizer_codegen_args = common_codegen_args | { - "module_name": "Repacketizer", - "clock_period_ps": "0", - "pipeline_stages": "2", -} - -xls_dslx_verilog( - name = "repacketizer_verilog", - codegen_args = repacketizer_codegen_args, - dslx_top = "Repacketizer", - library = ":repacketizer_dslx", - tags = ["manual"], - verilog_file = "repacketizer.v", -) - -xls_benchmark_ir( - name = "repacketizer_opt_ir_benchmark", - src = ":repacketizer_verilog.opt.ir", - benchmark_ir_args = repacketizer_codegen_args | { - "pipeline_stages": "10", - }, - tags = ["manual"], -) - -verilog_library( - name = "repacketizer_verilog_lib", - srcs = [ - ":repacketizer.v", - ], - tags = ["manual"], -) - -synthesize_rtl( - name = "repacketizer_synth_asap7", - standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", - tags = ["manual"], - top_module = "Repacketizer", - deps = [ - ":repacketizer_verilog_lib", - ], -) - -benchmark_synth( - name = "repacketizer_benchmark_synth", - synth_target = ":repacketizer_synth_asap7", - tags = ["manual"], -) - -place_and_route( - name = "repacketizer_place_and_route", - clock_period = CLOCK_PERIOD_PS, - core_padding_microns = 2, - min_pin_distance = "0.5", - placement_density = "0.30", - stop_after_step = "global_routing", - synthesized_rtl = ":repacketizer_synth_asap7", - tags = ["manual"], - target_die_utilization_percentage = "10", -) - xls_dslx_library( name = "axi_csr_accessor_dslx", srcs = [ @@ -977,7 +900,6 @@ zstd_dec_deps = [ ":dec_mux_dslx", ":frame_header_dec_dslx", ":raw_block_dec_dslx", - ":repacketizer_dslx", ":rle_block_dec_dslx", ":sequence_executor_dslx", "//xls/examples:ram_dslx", diff --git a/xls/modules/zstd/repacketizer.x b/xls/modules/zstd/repacketizer.x deleted file mode 100644 index f2abd638d1..0000000000 --- a/xls/modules/zstd/repacketizer.x +++ /dev/null @@ -1,215 +0,0 @@ -// 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. - -// Repacketizer -// -// Remove invalid bytes from input packets, -// form new packets with all bits valid if possible. - -import std; -import xls.modules.zstd.common as common; - -type ZstdDecodedPacket = common::ZstdDecodedPacket; -type BlockData = common::BlockData; -type BlockPacketLength = common::BlockPacketLength; - -const DATA_WIDTH = common::DATA_WIDTH; - -struct RepacketizerState { - repacked_data: BlockData, - valid_length: BlockPacketLength, - to_fill: BlockPacketLength, - send_last_leftover: bool -} - -const ZERO_ZSTD_DECODED_PACKET = zero!(); -const ZERO_REPACKETIZER_STATE = zero!(); -const INIT_REPACKETIZER_STATE = RepacketizerState {to_fill: DATA_WIDTH, ..ZERO_REPACKETIZER_STATE}; - -pub proc Repacketizer { - input_r: chan in; - output_s: chan out; - - init {(INIT_REPACKETIZER_STATE)} - - config ( - input_r: chan in, - output_s: chan out, - ) { - (input_r, output_s) - } - - next (state: RepacketizerState) { - let tok = join(); - // Don't receive if we process leftovers - let (tok, decoded_packet) = recv_if(tok, input_r, !state.send_last_leftover, ZERO_ZSTD_DECODED_PACKET); - - // Will be able to send repacketized packet in current next() evaluation - let send_now = state.to_fill <= decoded_packet.length || decoded_packet.last || state.send_last_leftover; - // Received last packet in frame which won't fit into currently processed repacketized packet. - // Set flag indicating that Repacketizer will send another packet to finish the frame in - // next evaluation. - let next_send_last_leftover = decoded_packet.last && state.to_fill < decoded_packet.length; - - let combined_length = state.valid_length + decoded_packet.length; - let leftover_length = (combined_length - DATA_WIDTH) as s32; - let next_valid_length = if leftover_length >= s32:0 {leftover_length as BlockPacketLength} else {combined_length}; - let next_to_fill = DATA_WIDTH - next_valid_length; - - let current_valid_length = if leftover_length >= s32:0 {DATA_WIDTH} else {combined_length}; - let bits_to_take_length = if leftover_length >= s32:0 {state.to_fill} else {decoded_packet.length}; - - // Append lest signifiant bits of received packet to most significant positions of repacked data buffer - let masked_data = ((BlockData:1 << bits_to_take_length) - BlockData:1) & decoded_packet.data; - let repacked_data = state.repacked_data | (masked_data << state.valid_length); - - // Prepare buffer state for the next evaluation - take leftover most significant bits of - // received packet - let leftover_mask = (BlockData:1 << (decoded_packet.length - bits_to_take_length)) - BlockData:1; - let leftover_masked_data = (decoded_packet.data >> bits_to_take_length) & leftover_mask; - let next_repacked_data = if (send_now) {leftover_masked_data} else {repacked_data}; - - let packet_to_send = ZstdDecodedPacket { - data: repacked_data, - length: current_valid_length, - last: state.send_last_leftover || (decoded_packet.last && !next_send_last_leftover), - }; - let tok = send_if(tok, output_s, send_now, packet_to_send); - - let next_state = if (state.send_last_leftover || (decoded_packet.last && !next_send_last_leftover)) { - INIT_REPACKETIZER_STATE - } else { - RepacketizerState { - repacked_data: next_repacked_data, - valid_length: next_valid_length, - to_fill: next_to_fill, - send_last_leftover: next_send_last_leftover, - } - }; - - trace_fmt!("Repacketizer: state: {:#x}", state); - if (!state.send_last_leftover) { - trace_fmt!("Repacketizer: Received packet: {:#x}", decoded_packet); - } else {}; - trace_fmt!("Repacketizer: send_now: {}", send_now); - trace_fmt!("Repacketizer: next_send_last_leftover: {}", next_send_last_leftover); - trace_fmt!("Repacketizer: combined_length: {}", combined_length); - trace_fmt!("Repacketizer: leftover_length: {}", leftover_length); - trace_fmt!("Repacketizer: next_valid_length: {}", next_valid_length); - trace_fmt!("Repacketizer: next_to_fill: {}", next_to_fill); - trace_fmt!("Repacketizer: current_valid_length: {}", current_valid_length); - trace_fmt!("Repacketizer: bits_to_take_length: {}", bits_to_take_length); - trace_fmt!("Repacketizer: masked_data: {:#x}", masked_data); - trace_fmt!("Repacketizer: repacked_data: {:#x}", repacked_data); - trace_fmt!("Repacketizer: leftover_mask: {:#x}", leftover_mask); - trace_fmt!("Repacketizer: leftover_masked_data: {:#x}", leftover_masked_data); - trace_fmt!("Repacketizer: next_repacked_data: {:#x}", next_repacked_data); - if (send_now) { - trace_fmt!("Repacketizer: Sent repacketized packet: {:#x}", packet_to_send); - } else {}; - trace_fmt!("Repacketizer: next_state: {:#x}", next_state); - - next_state - } -} - -#[test_proc] -proc RepacketizerTest { - terminator: chan out; - input_s: chan out; - output_r: chan in; - - init {} - - config (terminator: chan out) { - let (input_s, input_r) = chan("input"); - let (output_s, output_r) = chan("output"); - - spawn Repacketizer(input_r, output_s); - (terminator, input_s, output_r) - } - - next(state: ()) { - let tok = join(); - let DecodedInputs: ZstdDecodedPacket[24] = [ - // Full packet - no need for removing alignment zeros - ZstdDecodedPacket {data: BlockData:0xDEADBEEF12345678, length: BlockPacketLength:64, last:false}, - // Data in 4 packets - should be batched together into one full output packet - ZstdDecodedPacket {data: BlockData:0x78, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x56, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x1234, length: BlockPacketLength:16, last:false}, - ZstdDecodedPacket {data: BlockData:0xDEADBEEF, length: BlockPacketLength:32, last:false}, - // Small last packet - should be send out separatelly - ZstdDecodedPacket {data: BlockData:0x9A, length: BlockPacketLength:8, last:true}, - // One not-full packet and consecutive last packet packet in frame which completes previous packet and - // starts new one which should be marked as last - ZstdDecodedPacket {data: BlockData:0xADBEEF12345678, length: BlockPacketLength:56, last:false}, - ZstdDecodedPacket {data: BlockData:0x9ADE, length: BlockPacketLength:16, last:true}, - // 8 1-byte packets forming single output packet - ZstdDecodedPacket {data: BlockData:0xEF, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0xCD, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0xAB, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x89, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x67, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x45, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x23, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x01, length: BlockPacketLength:8, last:false}, - // 7 1-byte packets and 1 8-byte packet forming 1 full and 1 7-byte output packet - // marked as last - ZstdDecodedPacket {data: BlockData:0xEF, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0xCD, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0xAB, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x89, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x67, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x45, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0x23, length: BlockPacketLength:8, last:false}, - ZstdDecodedPacket {data: BlockData:0xFEDCBA9876543201, length: BlockPacketLength:64, last:true}, - ]; - - let DecodedOutputs: ZstdDecodedPacket[8] = [ - // Full packet - no need for removing alignment zeros - ZstdDecodedPacket {data: BlockData:0xDEADBEEF12345678, length: BlockPacketLength:64, last:false}, - // Data in 4 packets - should be batched together into one full output packet - ZstdDecodedPacket {data: BlockData:0xDEADBEEF12345678, length: BlockPacketLength:64, last:false}, - // Small last packet - should be send out separatelly - ZstdDecodedPacket {data: BlockData:0x9A, length: BlockPacketLength:8, last:true}, - // One not-full packet and consecutive last packet packet in frame which completes previous packet and - // starts new one which should be marked as last - ZstdDecodedPacket {data: BlockData:0xDEADBEEF12345678, length: BlockPacketLength:64, last:false}, - ZstdDecodedPacket {data: BlockData:0x9A, length: BlockPacketLength:8, last:true}, - // 8 1-byte packets forming single output packet - ZstdDecodedPacket {data: BlockData:0x0123456789ABCDEF, length: BlockPacketLength:64, last:false}, - // 7 1-byte packets and 1 8-byte packet forming 1 full and 1 7-byte output packet - // marked as last - ZstdDecodedPacket {data: BlockData:0x0123456789ABCDEF, length: BlockPacketLength:64, last:false}, - ZstdDecodedPacket {data: BlockData:0xFEDCBA98765432, length: BlockPacketLength:56, last:true}, - ]; - - let tok = for ((counter, decoded_input), tok): ((u32, ZstdDecodedPacket), token) in enumerate(DecodedInputs) { - let tok = send(tok, input_s, decoded_input); - trace_fmt!("Sent #{} decoded zero-filled packet, {:#x}", counter + u32:1, decoded_input); - (tok) - } (tok); - - let tok = for ((counter, expected_output), tok): ((u32, ZstdDecodedPacket), token) in enumerate(DecodedOutputs) { - let (tok, decoded_output) = recv(tok, output_r); - trace_fmt!("Received #{} decoded non-zero-filled packet, {:#x}", counter + u32:1, decoded_output); - trace_fmt!("Expected #{} decoded non-zero-filled packet, {:#x}", counter + u32:1, expected_output); - assert_eq(decoded_output, expected_output); - (tok) - } (tok); - - send(tok, terminator, true); - } -} diff --git a/xls/modules/zstd/sequence_executor.x b/xls/modules/zstd/sequence_executor.x index a6bdbb9205..6fb707141f 100644 --- a/xls/modules/zstd/sequence_executor.x +++ b/xls/modules/zstd/sequence_executor.x @@ -851,7 +851,6 @@ pub proc SequenceExecutor; input_r: chan in; - output_s: chan out; output_mem_wr_data_in_s: chan out; ram_comp_input_s: chan> out; ram_comp_output_r: chan> in; @@ -876,7 +875,6 @@ pub proc SequenceExecutor in, - output_s: chan out, output_mem_wr_data_in_s: chan out, ram_resp_output_r: chan in, ram_resp_output_s: chan out, @@ -928,7 +926,7 @@ pub proc SequenceExecutor(output_data); + let output_mem_wr_data_in = convert_output_packet(decode_literal_packet(packet)); + if do_write_output { trace_fmt!("Sending output MemWriter data: {:#x}", output_mem_wr_data_in); } else { }; let tok2_10_1 = send_if(tok1, output_mem_wr_data_in_s, do_write_output, output_mem_wr_data_in); // Ask for response @@ -1192,7 +1188,6 @@ pub proc SequenceExecutorZstd { config( input_r: chan in, - output_s: chan out, output_mem_wr_data_in_s: chan out, looped_channel_r: chan in, looped_channel_s: chan out, @@ -1231,7 +1226,7 @@ pub proc SequenceExecutorZstd { ) { spawn SequenceExecutor ( - input_r, output_s, output_mem_wr_data_in_s, + input_r, output_mem_wr_data_in_s, looped_channel_r, looped_channel_s, rd_req_m0_s, rd_req_m1_s, rd_req_m2_s, rd_req_m3_s, rd_req_m4_s, rd_req_m5_s, rd_req_m6_s, rd_req_m7_s, @@ -1347,7 +1342,6 @@ proc SequenceExecutorLiteralsTest { terminator: chan out; input_s: chan> out; - output_r: chan in; output_mem_wr_data_in_r: chan in; print_start_s: chan<()> out; @@ -1360,7 +1354,6 @@ proc SequenceExecutorLiteralsTest { config(terminator: chan out) { let (input_s, input_r) = chan>("input"); - let (output_s, output_r) = chan("output"); let (output_mem_wr_data_in_s, output_mem_wr_data_in_r) = chan("output_mem_wr_data_in"); let (looped_channel_s, looped_channel_r) = chan("looped_channels"); @@ -1381,7 +1374,7 @@ proc SequenceExecutorLiteralsTest { TEST_RAM_ADDR_WIDTH, INIT_HB_PTR_ADDR, > ( - input_r, output_s, output_mem_wr_data_in_s, + input_r, output_mem_wr_data_in_s, looped_channel_r, looped_channel_s, ram_rd_req_s[0], ram_rd_req_s[1], ram_rd_req_s[2], ram_rd_req_s[3], ram_rd_req_s[4], ram_rd_req_s[5], ram_rd_req_s[6], ram_rd_req_s[7], @@ -1433,7 +1426,7 @@ proc SequenceExecutorLiteralsTest { ( terminator, - input_s, output_r, output_mem_wr_data_in_r, + input_s, output_mem_wr_data_in_r, print_start_s, print_finish_r, ram_rd_req_s, ram_rd_resp_r, ram_wr_req_s, ram_wr_resp_r @@ -1450,11 +1443,9 @@ proc SequenceExecutorLiteralsTest { if (LITERAL_TEST_INPUT_DATA[i].msg_type != SequenceExecutorMessageType::LITERAL || LITERAL_TEST_INPUT_DATA[i].length != CopyOrMatchLength:0 || LITERAL_TEST_INPUT_DATA[i].last) { - let (tok, recv_data) = recv(tok, output_r); let expected = decode_literal_packet(LITERAL_TEST_INPUT_DATA[i]); - assert_eq(expected, recv_data); - let (tok, recv_mem_writer_data) = recv(tok, output_mem_wr_data_in_r); let expected_mem_writer_data = convert_output_packet(expected); + let (tok, recv_mem_writer_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(expected_mem_writer_data, recv_mem_writer_data); } else {} }(()); @@ -1546,60 +1537,61 @@ const SEQUENCE_TEST_INPUT_SEQUENCES = SequenceExecutorPacket[11]: [ }, ]; -const SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS:ZstdDecodedPacket[11] = [ - ZstdDecodedPacket { - data: BlockData:0x8C_7E_B8_B9_7C_A3_9D_AF, - length: BlockPacketLength:64, +type TestMemWriterDataPacket = mem_writer::MemWriterDataPacket; +const SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS:TestMemWriterDataPacket[11] = [ + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0x8C_7E_B8_B9_7C_A3_9D_AF, + length: uN[TEST_ADDR_W]:8, last: false }, - ZstdDecodedPacket { - data: BlockData:0x7D, - length: BlockPacketLength:8, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0x7D, + length: uN[TEST_ADDR_W]:1, last: false }, - ZstdDecodedPacket { - data: BlockData:0xB8, - length: BlockPacketLength:8, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0xB8, + length: uN[TEST_ADDR_W]:1, last: false }, - ZstdDecodedPacket { - data: BlockData:0xB8, - length: BlockPacketLength:8, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0xB8, + length: uN[TEST_ADDR_W]:1, last: false }, - ZstdDecodedPacket { - data: BlockData:0xB8_B9_7C_A3_9D, - length: BlockPacketLength:40, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0xB8_B9_7C_A3_9D, + length: uN[TEST_ADDR_W]:5, last: false }, - ZstdDecodedPacket { - data: BlockData:0xB9_7C_A3, - length: BlockPacketLength:24, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0xB9_7C_A3, + length: uN[TEST_ADDR_W]:3, last: false }, - ZstdDecodedPacket { - data: BlockData:0xB8, - length: BlockPacketLength:8, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0xB8, + length: uN[TEST_ADDR_W]:1, last: false }, - ZstdDecodedPacket { - data: BlockData:0x7C, - length: BlockPacketLength:8, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0x7C, + length: uN[TEST_ADDR_W]:1, last: false }, - ZstdDecodedPacket { - data: BlockData:0xB9_7C_A3_B8_B9_7C_A3_9D, - length: BlockPacketLength:64, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0xB9_7C_A3_B8_B9_7C_A3_9D, + length: uN[TEST_ADDR_W]:8, last: false }, - ZstdDecodedPacket { - data: BlockData:0x7C_B8, - length: BlockPacketLength:16, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0x7C_B8, + length: uN[TEST_ADDR_W]:2, last: true }, - ZstdDecodedPacket { - data: BlockData:0x9D, - length: BlockPacketLength:8, + TestMemWriterDataPacket { + data: uN[TEST_DATA_W]:0x9D, + length: uN[TEST_ADDR_W]:1, last: false } ]; @@ -1610,7 +1602,6 @@ proc SequenceExecutorSequenceTest { terminator: chan out; input_s: chan out; - output_r: chan in; output_mem_wr_data_in_r: chan in; print_start_s: chan<()> out; @@ -1623,7 +1614,6 @@ proc SequenceExecutorSequenceTest { config(terminator: chan out) { let (input_s, input_r) = chan("input"); - let (output_s, output_r) = chan("output"); let (output_mem_wr_data_in_s, output_mem_wr_data_in_r) = chan("output_mem_wr_data_in"); let (looped_channel_s, looped_channel_r) = chan("looped_channel"); @@ -1644,7 +1634,7 @@ proc SequenceExecutorSequenceTest { TEST_RAM_ADDR_WIDTH, INIT_HB_PTR_ADDR, > ( - input_r, output_s, output_mem_wr_data_in_s, + input_r, output_mem_wr_data_in_s, looped_channel_r, looped_channel_s, ram_rd_req_s[0], ram_rd_req_s[1], ram_rd_req_s[2], ram_rd_req_s[3], ram_rd_req_s[4], ram_rd_req_s[5], ram_rd_req_s[6], ram_rd_req_s[7], @@ -1696,7 +1686,7 @@ proc SequenceExecutorSequenceTest { ( terminator, - input_s, output_r, output_mem_wr_data_in_r, + input_s, output_mem_wr_data_in_r, print_start_s, print_finish_r, ram_rd_req_s, ram_rd_resp_r, ram_wr_req_s, ram_wr_resp_r ) @@ -1712,11 +1702,9 @@ proc SequenceExecutorSequenceTest { if (LITERAL_TEST_INPUT_DATA[i].msg_type != SequenceExecutorMessageType::LITERAL || LITERAL_TEST_INPUT_DATA[i].length != CopyOrMatchLength:0 || LITERAL_TEST_INPUT_DATA[i].last) { - let (tok, recv_data) = recv(tok, output_r); let expected = decode_literal_packet(LITERAL_TEST_INPUT_DATA[i]); - assert_eq(expected, recv_data); - let (tok, recv_mem_writer_data) = recv(tok, output_mem_wr_data_in_r); let expected_mem_writer_data = convert_output_packet(expected); + let (tok, recv_mem_writer_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(expected_mem_writer_data, recv_mem_writer_data); } else {} }(()); @@ -1726,45 +1714,45 @@ proc SequenceExecutorSequenceTest { let (tok, _) = recv(tok, print_finish_r); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[0]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[0], recv_data); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[1], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[1]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[2], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[2]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[3], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[3]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[4], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[4]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[5], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[5]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[6], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[6]); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[7]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[7], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[8]); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[9]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[8], recv_data); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[9], recv_data); let tok = send(tok, input_s, SEQUENCE_TEST_INPUT_SEQUENCES[10]); - let (tok, recv_data) = recv(tok, output_r); + let (tok, recv_data) = recv(tok, output_mem_wr_data_in_r); assert_eq(SEQUENCE_TEST_EXPECTED_SEQUENCE_RESULTS[10], recv_data); // Print RAM content diff --git a/xls/modules/zstd/zstd_dec.x b/xls/modules/zstd/zstd_dec.x index 7fc5fb260e..a5f5460905 100644 --- a/xls/modules/zstd/zstd_dec.x +++ b/xls/modules/zstd/zstd_dec.x @@ -31,7 +31,6 @@ import xls.modules.zstd.raw_block_dec; import xls.modules.zstd.rle_block_dec; import xls.modules.zstd.dec_mux; import xls.modules.zstd.sequence_executor; -import xls.modules.zstd.repacketizer; type BlockSize = common::BlockSize; type BlockType = common::BlockType; @@ -846,11 +845,15 @@ proc ZstdDecoderInternalTest { status: RawBlockDecoderStatus::OKAY, }); - let (tok, ()) = recv(tok, notify_r); - - let (tok, _) = recv(tok, output_mem_wr_req_r); + let (tok, mem_wr_req) = recv(tok, output_mem_wr_req_r); + assert_eq(mem_wr_req, MemWriterReq { + addr: uN[TEST_AXI_ADDR_W]:0x2000, + length: uN[TEST_AXI_ADDR_W]:200 + }); let tok = send(tok, output_mem_wr_resp_s, MemWriterResp {status: mem_writer::MemWriterRespStatus::OKAY}); + let (tok, ()) = recv(tok, notify_r); + send(tok, terminator, true); } } @@ -977,8 +980,6 @@ pub proc ZstdDecoder< ram_wr_resp_6_r: chan in, ram_wr_resp_7_r: chan in, - // Decoder output - output_s: chan out, notify_s: chan<()> out, reset_s: chan<()> out, ) { @@ -1090,12 +1091,10 @@ pub proc ZstdDecoder< // Sequence Execution let (seq_exec_looped_s, seq_exec_looped_r) = chan("seq_exec_looped"); - let (seq_exec_output_s, seq_exec_output_r) = chan("seq_exec_output"); let (output_mem_wr_data_in_s, output_mem_wr_data_in_r) = chan("output_mem_wr_data_in"); spawn sequence_executor::SequenceExecutor( - seq_exec_input_r, seq_exec_output_s, - output_mem_wr_data_in_s, + seq_exec_input_r, output_mem_wr_data_in_s, seq_exec_looped_r, seq_exec_looped_s, ram_rd_req_0_s, ram_rd_req_1_s, ram_rd_req_2_s, ram_rd_req_3_s, ram_rd_req_4_s, ram_rd_req_5_s, ram_rd_req_6_s, ram_rd_req_7_s, @@ -1107,10 +1106,6 @@ pub proc ZstdDecoder< ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r ); - // Repacketizer - - spawn repacketizer::Repacketizer(seq_exec_output_r, output_s); - // Zstd Decoder Control let (output_mem_wr_req_s, output_mem_wr_req_r) = chan("output_mem_wr_req"); let (output_mem_wr_resp_s, output_mem_wr_resp_r) = chan("output_mem_wr_resp"); @@ -1309,8 +1304,6 @@ proc ZstdDecoderInst { ram_wr_resp_6_r: chan in, ram_wr_resp_7_r: chan in, - // Decoder output - output_s: chan out, notify_s: chan<()> out, reset_s: chan<()> out, ) { @@ -1332,7 +1325,7 @@ proc ZstdDecoderInst { ram_wr_req_4_s, ram_wr_req_5_s, ram_wr_req_6_s, ram_wr_req_7_s, ram_wr_resp_0_r, ram_wr_resp_1_r, ram_wr_resp_2_r, ram_wr_resp_3_r, ram_wr_resp_4_r, ram_wr_resp_5_r, ram_wr_resp_6_r, ram_wr_resp_7_r, - output_s, notify_s, reset_s, + notify_s, reset_s, ); } diff --git a/xls/modules/zstd/zstd_dec_cocotb_test.py b/xls/modules/zstd/zstd_dec_cocotb_test.py index 56f45e8d06..40877c36a1 100644 --- a/xls/modules/zstd/zstd_dec_cocotb_test.py +++ b/xls/modules/zstd/zstd_dec_cocotb_test.py @@ -44,7 +44,6 @@ AXI_DATA_W_BYTES = AXI_DATA_W // 8 MAX_ENCODED_FRAME_SIZE_B = 16384 NOTIFY_CHANNEL = "notify" -OUTPUT_CHANNEL = "output" RESET_CHANNEL = "reset" # Override default widths of AXI response signals @@ -69,12 +68,6 @@ class NotifyStruct(XLSStruct): class ResetStruct(XLSStruct): pass -@xls_dataclass -class OutputStruct(XLSStruct): - data: 64 - length: 32 - last: 1 - class CSR(Enum): """ Maps the offsets to the ZSTD Decoder registers @@ -215,23 +208,6 @@ async def wait_for_idle(cpu, timeout=100): timeout -= 1 assert (timeout != 0) -def generate_expected_output(decoded_frame): - packets = [] - frame_len = len(decoded_frame) - last_len = frame_len % 8 - for i in range(frame_len // 8): - start_id = i * 8 - end_id = start_id + 8 - packet_data = int.from_bytes(decoded_frame[start_id:end_id], byteorder='little') - last_packet = (end_id==frame_len) - packet = OutputStruct(data=packet_data, length=64, last=last_packet) - packets.append(packet) - if (last_len): - packet_data = int.from_bytes(decoded_frame[-last_len:], byteorder='little') - packet = OutputStruct(data=packet_data, length=last_len*8, last=True) - packets.append(packet) - return packets - async def reset_dut(dut, rst_len=10): dut.rst.setimmediatevalue(0) await ClockCycles(dut.clk, rst_len) @@ -249,8 +225,6 @@ def prepare_test_environment(dut): clock = Clock(dut.clk, 10, units="us") cocotb.start_soon(clock.start()) - scoreboard = Scoreboard(dut) - memory_bus = connect_axi_bus(dut, "memory") csr_bus = connect_axi_bus(dut, "csr") axi_buses = { @@ -259,21 +233,18 @@ def prepare_test_environment(dut): } notify = connect_xls_channel(dut, NOTIFY_CHANNEL, NotifyStruct) - output = connect_xls_channel(dut, OUTPUT_CHANNEL, OutputStruct) xls_channels = { "notify": notify, - "output": output } cpu = AxiMaster(csr_bus, dut.clk, dut.rst) - return (scoreboard, axi_buses, xls_channels, cpu) + return (axi_buses, xls_channels, cpu) -async def test_decoder(dut, seed, block_type, scoreboard, axi_buses, xls_channels, cpu): +async def test_decoder(dut, seed, block_type, axi_buses, xls_channels, cpu): memory_bus = axi_buses["memory"] csr_bus = axi_buses["csr"] (notify_channel, notify_monitor) = xls_channels[NOTIFY_CHANNEL] - (output_channel, output_monitor) = xls_channels[OUTPUT_CHANNEL] assert_notify = Event() set_termination_event(notify_monitor, assert_notify, 1) @@ -293,12 +264,6 @@ async def test_decoder(dut, seed, block_type, scoreboard, axi_buses, xls_channel encoded.close() reference_memory = SparseMemory(mem_size) reference_memory.write(obuf_addr, expected_decoded_frame) - expected_output_packets = generate_expected_output(expected_decoded_frame) - - assert_expected_output = Event() - set_termination_event(output_monitor, assert_expected_output, len(expected_output_packets)) - # Monitor stream output for packets with the decoded ZSTD frame - scoreboard.add_interface(output_monitor, expected_output_packets) # Initialise testbench memory with generated ZSTD frame memory = AxiRamFromFile(bus=memory_bus, clock=dut.clk, reset=dut.rst, path=encoded.name, size=mem_size) @@ -329,62 +294,62 @@ async def zstd_reset_test(dut): @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_1(dut): block_type = BlockType.RAW - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 1, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 1, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_2(dut): block_type = BlockType.RAW - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 2, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 2, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_3(dut): block_type = BlockType.RAW - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 3, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 3, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_4(dut): block_type = BlockType.RAW - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 4, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 4, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_raw_frames_test_5(dut): block_type = BlockType.RAW - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 5, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 5, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_1(dut): block_type = BlockType.RLE - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 1, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 1, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_2(dut): block_type = BlockType.RLE - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 2, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 2, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_3(dut): block_type = BlockType.RLE - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 3, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 3, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_4(dut): block_type = BlockType.RLE - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 4, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 4, block_type, axi_buses, xls_channels, cpu) @cocotb.test(timeout_time=1000, timeout_unit="ms") async def zstd_rle_frames_test_5(dut): block_type = BlockType.RLE - (scoreboard, axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 5, block_type, scoreboard, axi_buses, xls_channels, cpu) + (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) + await test_decoder(dut, 5, block_type, axi_buses, xls_channels, cpu) #@cocotb.test(timeout_time=1000, timeout_unit="ms") #async def zstd_compressed_frames_test(dut): diff --git a/xls/modules/zstd/zstd_dec_test.x b/xls/modules/zstd/zstd_dec_test.x index a57f7e499e..bd90210ef1 100644 --- a/xls/modules/zstd/zstd_dec_test.x +++ b/xls/modules/zstd/zstd_dec_test.x @@ -118,7 +118,6 @@ proc ZstdDecoderTest { raw_wr_resp_bh_r: chan in; raw_wr_resp_raw_r: chan in; - output_r: chan in; notify_r: chan<()> in; reset_r: chan<()> in; @@ -164,7 +163,6 @@ proc ZstdDecoderTest { let (ram_wr_resp_bh_s, ram_wr_resp_bh_r) = chan("ram_wr_resp_bh"); let (ram_wr_resp_raw_s, ram_wr_resp_raw_r) = chan("ram_wr_resp_raw"); - let (output_s, output_r) = chan("output"); let (notify_s, notify_r) = chan<()>("notify"); let (reset_s, reset_r) = chan<()>("reset"); @@ -186,7 +184,7 @@ proc ZstdDecoderTest { ram_wr_req_s[4], ram_wr_req_s[5], ram_wr_req_s[6], ram_wr_req_s[7], ram_wr_resp_r[0], ram_wr_resp_r[1], ram_wr_resp_r[2], ram_wr_resp_r[3], ram_wr_resp_r[4], ram_wr_resp_r[5], ram_wr_resp_r[6], ram_wr_resp_r[7], - output_s, notify_s, reset_s, + notify_s, reset_s, ); spawn ram::RamModel< @@ -277,7 +275,7 @@ proc ZstdDecoderTest { ram_rd_req_r, ram_rd_resp_s, ram_wr_req_r, ram_wr_resp_s, ram_wr_req_fh_s, ram_wr_req_bh_s, ram_wr_req_raw_s, ram_wr_resp_fh_r, ram_wr_resp_bh_r, ram_wr_resp_raw_r, - output_r, notify_r, reset_r, + notify_r, reset_r, ) } @@ -361,13 +359,6 @@ proc ZstdDecoderTest { let (tok, _) = recv(tok, csr_axi_b_r); let decomp_frame = zstd_frame_testcases::DECOMPRESSED_FRAMES[test_i]; - let tok = for (decomp_i, tok): (u32, token) in range(u32:0, decomp_frame.array_length) { - let (tok, decomp_packet) = recv(tok, output_r); - assert_eq(decomp_packet.data, decomp_frame.data[decomp_i]); - assert_eq(decomp_packet.last, decomp_i == (decomp_frame.length - u32:1) / u32:8); - tok - }(tok); - // Test ZstdDecoder memory output interface // Mock the output memory buffer as a DSLX array // It is required to handle AXI write transactions and to write the incoming data to diff --git a/xls/modules/zstd/zstd_dec_wrapper.v b/xls/modules/zstd/zstd_dec_wrapper.v index 6059d2231f..45ff86d91a 100644 --- a/xls/modules/zstd/zstd_dec_wrapper.v +++ b/xls/modules/zstd/zstd_dec_wrapper.v @@ -24,8 +24,7 @@ module zstd_dec_wrapper #( parameter WUSER_WIDTH = 1, parameter BUSER_WIDTH = 1, parameter ARUSER_WIDTH = 1, - parameter RUSER_WIDTH = 1, - parameter OUTPUT_WIDTH = 97 + parameter RUSER_WIDTH = 1 ) ( input wire clk, input wire rst, @@ -124,11 +123,7 @@ module zstd_dec_wrapper #( output wire notify_data, output wire notify_vld, - input wire notify_rdy, - - output wire [OUTPUT_WIDTH-1:0] output_data, - output wire output_vld, - input wire output_rdy + input wire notify_rdy ); /* @@ -292,11 +287,6 @@ module zstd_dec_wrapper #( wire zstd_dec__output_axi_b_rdy; wire zstd_dec__output_axi_b_vld; - // Output packet - wire [63:0] output_data_data_field; - wire [31:0] output_data_length_field; - wire [0:0] output_data_last_field; - /* * Mapping XLS Channels to AXI channels fields */ @@ -442,12 +432,6 @@ module zstd_dec_wrapper #( assign reset_data = reset_vld; assign reset = reset_vld | rst; - assign { - output_data_data_field, - output_data_length_field, - output_data_last_field - } = output_data; - /* * ZSTD Decoder instance */ @@ -510,9 +494,6 @@ module zstd_dec_wrapper #( // Other ports .zstd_dec__notify_s_vld(notify_vld), .zstd_dec__notify_s_rdy(notify_rdy), - .zstd_dec__output_s(output_data), - .zstd_dec__output_s_vld(output_vld), - .zstd_dec__output_s_rdy(output_rdy), // Reset loopback - response for write to RESET CSR // Should be looped back to generic reset input .zstd_dec__reset_s_vld(reset_vld), From f2540f1f8f09613bcffee43c152cf3bdc6580ae6 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Mon, 18 Nov 2024 10:31:11 +0100 Subject: [PATCH 38/46] modules/zstd/zstd_dec_cocotb_test: Improve Verilog simulation * Decode multiple ZSTD frames in a single cocotb testbench * Add one cocotb testbench per type of the ZSTD frames: * Frames with RAW blocks only * Frames with RLE blocks only * Frames with Compressed blocks only (disabled) * Frames with mixed blocks (disabled) Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/zstd_dec_cocotb_test.py | 88 ++++++------------------ 1 file changed, 20 insertions(+), 68 deletions(-) diff --git a/xls/modules/zstd/zstd_dec_cocotb_test.py b/xls/modules/zstd/zstd_dec_cocotb_test.py index 40877c36a1..618d0591f9 100644 --- a/xls/modules/zstd/zstd_dec_cocotb_test.py +++ b/xls/modules/zstd/zstd_dec_cocotb_test.py @@ -232,20 +232,15 @@ def prepare_test_environment(dut): "csr": csr_bus } - notify = connect_xls_channel(dut, NOTIFY_CHANNEL, NotifyStruct) - xls_channels = { - "notify": notify, - } - cpu = AxiMaster(csr_bus, dut.clk, dut.rst) - return (axi_buses, xls_channels, cpu) + return (axi_buses, cpu) -async def test_decoder(dut, seed, block_type, axi_buses, xls_channels, cpu): +async def test_decoder(dut, seed, block_type, axi_buses, cpu): memory_bus = axi_buses["memory"] csr_bus = axi_buses["csr"] - (notify_channel, notify_monitor) = xls_channels[NOTIFY_CHANNEL] + (notify_channel, notify_monitor) = connect_xls_channel(dut, NOTIFY_CHANNEL, NotifyStruct) assert_notify = Event() set_termination_event(notify_monitor, assert_notify, 1) @@ -282,6 +277,12 @@ async def test_decoder(dut, seed, block_type, axi_buses, xls_channels, cpu): await ClockCycles(dut.clk, 20) +async def testing_routine(dut, test_cases=1, block_type=BlockType.RANDOM): + (axi_buses, cpu) = prepare_test_environment(dut) + for test_case in range(test_cases): + await test_decoder(dut, test_case, block_type, axi_buses, cpu) + print("Decoding {} ZSTD frames done".format(block_type.name)) + @cocotb.test(timeout_time=50, timeout_unit="ms") async def zstd_csr_test(dut): await test_csr(dut) @@ -290,78 +291,29 @@ async def zstd_csr_test(dut): async def zstd_reset_test(dut): await test_reset(dut) -#FIXME: Rework testbench to decode multiple ZSTD frames in one test -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_raw_frames_test_1(dut): - block_type = BlockType.RAW - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 1, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_raw_frames_test_2(dut): - block_type = BlockType.RAW - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 2, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_raw_frames_test_3(dut): - block_type = BlockType.RAW - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 3, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_raw_frames_test_4(dut): - block_type = BlockType.RAW - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 4, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_raw_frames_test_5(dut): +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def zstd_raw_frames_test(dut): + test_cases = 5 block_type = BlockType.RAW - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 5, block_type, axi_buses, xls_channels, cpu) + await testing_routine(dut, test_cases, block_type) -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_rle_frames_test_1(dut): +@cocotb.test(timeout_time=500, timeout_unit="ms") +async def zstd_rle_frames_test(dut): + test_cases = 5 block_type = BlockType.RLE - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 1, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_rle_frames_test_2(dut): - block_type = BlockType.RLE - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 2, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_rle_frames_test_3(dut): - block_type = BlockType.RLE - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 3, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_rle_frames_test_4(dut): - block_type = BlockType.RLE - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 4, block_type, axi_buses, xls_channels, cpu) - -@cocotb.test(timeout_time=1000, timeout_unit="ms") -async def zstd_rle_frames_test_5(dut): - block_type = BlockType.RLE - (axi_buses, xls_channels, cpu) = prepare_test_environment(dut) - await test_decoder(dut, 5, block_type, axi_buses, xls_channels, cpu) + await testing_routine(dut, test_cases, block_type) #@cocotb.test(timeout_time=1000, timeout_unit="ms") #async def zstd_compressed_frames_test(dut): # test_cases = 1 # block_type = BlockType.COMPRESSED -# await test_decoder(dut, test_cases, block_type) -# +# await testing_routine(dut, test_cases, block_type) + #@cocotb.test(timeout_time=1000, timeout_unit="ms") #async def zstd_random_frames_test(dut): # test_cases = 1 # block_type = BlockType.RANDOM -# await test_decoder(dut, test_cases, block_type) +# await testing_routine(dut, test_cases, block_type) if __name__ == "__main__": toplevel = "zstd_dec_wrapper" From 832533e00d67ba38ee2b05eef2aae3cfffab3b26 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 19 Nov 2024 12:31:15 +0100 Subject: [PATCH 39/46] modules/zstd/README: Update output interface description Internal-tag: [#67272] Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/README.md | 48 ++++++++---------- xls/modules/zstd/img/ZSTD_decoder.png | Bin 315264 -> 312837 bytes xls/modules/zstd/img/ZSTD_decoder_wrapper.png | Bin 126231 -> 123219 bytes 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/xls/modules/zstd/README.md b/xls/modules/zstd/README.md index edf5b9470f..e9a8109687 100644 --- a/xls/modules/zstd/README.md +++ b/xls/modules/zstd/README.md @@ -5,19 +5,17 @@ It implements the [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878.html) decomp An overview of the decoder architecture is presented in the diagram below. The decoder comprises: * Memory Readers -* Memory Writer[^2], +* Memory Writer, * Control and Status Registers, * Frame Header Decoder, * Block Header Decoder, * 3 types of processing units: RAW-, RLE-, and Compressed Block Decoders[^1], * Command Aggregator, -* Repacketizer. The Decoder interacts with the environment through a set of ports: * Memory Interface (AXI) * CSR Interface (AXI) * Notify line -* Stream Output The software controls the core through registers accessible through the `CSR Interface`. The CSRs are used to configure the decoder and to start the decoding process. @@ -33,8 +31,8 @@ Once the decoding process is started, the decoder: 4. Decodes the Block Data with the correct processing unit picked based on the Block Type from the Block Header, 4. Aggregates the processing unit results in the correct order into a stream and routes it to the history buffer, 5. Assembles the data block outputs based on the history buffer contents and updates the history, -6. Prepares the final output of the decoder, -7. (Optional) Calculates checksum and compares it against the checksum read from the frame.[^3] +6. Prepares the final output of the decoder and writes it to the memory, +7. (Optional) Calculates checksum and compares it against the checksum read from the frame.[^2] ![](img/ZSTD_decoder.png) @@ -115,31 +113,41 @@ stateDiagram [*] --> IDLE IDLE --> READ_CONFIG: Start + IDLE --> mem_write_done READ_CONFIG --> DECODE_FRAME_HEADER + READ_CONFIG --> mem_write_done DECODE_FRAME_HEADER --> DECODE_BLOCK_HEADER DECODE_FRAME_HEADER --> ERROR + DECODE_FRAME_HEADER --> mem_write_done DECODE_BLOCK_HEADER --> DECODE_RAW_BLOCK DECODE_BLOCK_HEADER --> DECODE_RLE_BLOCK DECODE_BLOCK_HEADER --> DECODE_COMPRESED_BLOCK DECODE_BLOCK_HEADER --> ERROR + DECODE_BLOCK_HEADER --> mem_write_done state if_block_last <> DECODE_RAW_BLOCK --> ERROR DECODE_RAW_BLOCK --> if_block_last + DECODE_RAW_BLOCK --> mem_write_done DECODE_RLE_BLOCK --> ERROR DECODE_RLE_BLOCK --> if_block_last + DECODE_RLE_BLOCK --> mem_write_done DECODE_COMPRESSED_BLOCK --> ERROR DECODE_COMPRESSED_BLOCK --> if_block_last + DECODE_COMPRESSED_BLOCK --> mem_write_done if_block_last --> DECODE_BLOCK_HEADER: Not last block in the frame if_block_last --> DECODE_CHECKSUM: Last block in the frame - DECODE_CHECKSUM --> FINISH + DECODE_CHECKSUM --> mem_write_done + + state mem_write_done <> + mem_write_done --> FINISH: Frame written to the memory FINISH --> IDLE ERROR --> IDLE @@ -154,9 +162,9 @@ Once received, the module fetches the data from the memory starting from the add `MemReader` procs are used by those modules to communicate with the external memory through the AXI interface. Internal modules decode the acquired parts of the frame and return responses with the results back to the top level proc. -The processing units also output the decoded blocks of data through a stream-based interface to `SequenceExecutor` and further to `Repacketizer` procs. -Those procs perform the last steps of the decoding before the final output is sent out back to the memory under the address stored in the `Output Buffer` CSR or through the stream-based output channel. -Once the decoding process is completed, the decoder sends the `Notify` signal and transitions back to the `IDLE` state. +The processing units also output the decoded blocks of data through a stream-based interface to the `SequenceExecutor` proc. +This proc performs the last step of the decoding before the final output is sent out back to the memory under the address stored in the `Output Buffer` CSR by the `MemWriter` proc. +Once the decoding process is completed and the decoded frame is written back to the memory, the decoder sends the `Notify` signal and transitions back to the `IDLE` state. ### Internal modules @@ -212,19 +220,6 @@ This stage will show the following behavior, depending on the tag: * Copy `copy_length` literals starting `offset _length` from the newest in history buffer to the decoder's output, * Copy `copy_length` literals starting `offset _length` from the newest in history buffer to the history buffer as the newest. -#### Repacketizer -This proc is used at the end of the processing flow in the ZSTD decoder. -It gathers the output of `SequenceExecutor` proc and processes it to form the final output packets of the ZSTD decoder. -Input packets coming from the `SequenceExecutor` consist of: - -* data - bit vector of constant length -* length - field describing how many bits in bit vector are valid -* last - flag which marks the last packet in currently decoded ZSTD frame. - -It is not guaranteed that all bits in data bit vectors in packets received from `SequenceExecutor` are valid as those can include padding bits that were added in previous decoding steps and now have to be removed. -Repacketizer buffers input packets, removes the padding bits and forms new packets with all bits of the bit vector valid, meaning that all bits are decoded data. -Newly formed packets are then sent out to the output of the whole ZSTD decoder. - ### Compressed block decoder architecture[^1] This part of the design is responsible for processing the compressed blocks up to the `literals`/`copy` command sequence. This sequence is then processed by the history buffer to generate the expected data output. @@ -359,8 +354,8 @@ Verilog tests are written in Python as [cocotb](https://github.com/cocotb/cocotb ZstdDecoder's main communication interfaces are the AXI buses. Due to the way XLS handles the codegen of DSLX channels that model the AXI channels, the particular ports of the AXI channels are not represented correctly. This enforces the introduction of a Verilog wrapper that maps the ports generated by XLS into proper AXI ports (see AXI peripherals [README](memory/README.md) for more information). -Additionally, the wrapper is used to mux multiple AXI interfaces from `Memory Readers` into a single outside-facing AXI interface (`Memory Interface`) that can be connected to the external memory. -The mux is implemented by a third-party [AXI Interconnect](https://github.com/alexforencich/verilog-axi). +Additionally, the wrapper is used to mux multiple AXI interfaces from `Memory Readers` and `Memory Writer` into a single outside-facing AXI interface (`Memory Interface`) that can be connected to the external memory. +The mux is implemented by a third-party [AXI Crossbar](https://github.com/alexforencich/verilog-axi). ![](img/ZSTD_decoder_wrapper.png) @@ -373,7 +368,7 @@ The Basic test case for the ZstdDecoder is composed of the following steps: 2. The encoded frame is placed in an AXI RAM model that is connected to the decoder's `Memory Interface`. 3. The encoded frame is decoded with the zstd reference library and the results are represented in the decoder's output format as the expected data from the simulation. 4. AXI Master performs a series of writes to the ZstdDecoder CSRs to configure it and start the decoding process. -5. Testbench waits for the signal on the `Notify` channel, monitoring the output of the decoder and checking it against the expected output data at the same time. +5. Testbench waits for the signal on the `Notify` channel and checks the output of the decoder stored in the memory against the expected output data. 6. Test case succeeds once `Notify` is asserted, all expected data is received and the decoder lands in `IDLE` state with status `OKAY` in the `Status` CSR. ### Failure points @@ -465,5 +460,4 @@ The alternatives for writing negative tests include: * Generating a well-known valid ZSTD frame from a specific generator seed and then tweaking the raw bits in this frame to trigger the error response from the decoder [^1]: `CompressedBlockDecoder` is to be added in follow-up PRs. -[^2]: `MemWriter` instance is a part of the memory-based output interface which is currently under development. -[^3]: Checksum verification is currently unsupported. +[^2]: Checksum verification is currently unsupported. diff --git a/xls/modules/zstd/img/ZSTD_decoder.png b/xls/modules/zstd/img/ZSTD_decoder.png index 494926d89f7108623b2a4d958d1980b9a34f75cc..d52c11b3d8b8df3be0739ec33277925846f107f0 100644 GIT binary patch literal 312837 zcmeEP2RxPE|F@dHqNz|cB-yU5Y*C01*%{a7a$S43OCd94R7T3k&b&%xWs8hZc9gx> z|2*7_(06~ozxw{ASGmu9o^zh(oX_WdKA-pJ^EuB=nX_Ugc9Guh%l;{PNiH)v-F&u{)CcN+)H7m0|!U9DN6QO2h)ig6>)G^T1 zLu#5>Fq*mCUw(y08wx%(B;V`%ux-%^VQU{J)I0SBol^P~W%`6DMfd0aSF^>X180ajV zB+n{gC=2Jbk+Rma*0bdll~K?%U1-Ww(*)DD40v9K`3{!1hHcZtMhvNGrZ0uifg6K{ zb!;#<#Kt{;1K^15!Vy+ZcJ_s)^^n-^F}DpJkTbBuJ`3hTJxc=}xCN%Ug)31AgfYs% z?AwFd2vbwIHfBOFEovf>2_Nk zdx4IN!wvNGu|2bJa4&pgqKQ2j^CTAfnmP#Uh3n@JtsY41a3T>1(B86HTlix6zmTK~ z!;PWQ#*QPVA0Rh>{MyCQKrTx4Uuo|286$IDOKm$LTW&#p8D^W)+9+-YAe)eQT4@?v zVupO-fCb7HE5S&FrKt`anjs*mLe}~QD7c)NrZ#lCH4rdxOdn-p3_d~nrK4$~KYuRt z!2*Ru7%ff_bVL_niaMofVqgr3o;cjt3XU?+*2H`vj4(zls0i%T{2%BzP~v6BVK7z^ z80TWs*l$b`rr>sjEWm9UnCfBf)C6IrsWtx)P|rxXg@N7j&j^$zYT0L?B=F^*;W`G( zKN=&njTXlMBiGBWhMr3fGh}N_8|Dze3ltoyhR`_wx;SAa4=6GmVFE`XZNW@oFK1@P z$O1-*8CbDuZM~!tEbLGl&;;u**9R_k%u!8@BI+%+51spagcY2Fk@a7HhBZ`zO&2a` zV`3w3X>DdHqGKVdC4^9BU}5E2C5g%wC>;^FHo!0BuLvbHxxXQl%Y_IlD)XX=l^<%> zC5h8B*0g|z1rqjOk!B9?wHX{~0Okdbg!Exa@QgLJ;Knit3j@@`;-C%G7s7raX#Rn? z8%wS@0fm6HNYmIr4_ZrBoi2#^LJNUHAxyxT`Us?f9gsLptkyx|x7M@Fi{%1*v$BH= zzZFx^B~lxM?$~eFRL+IbH`83gp?^T2SvXb-G*(KN3pDGR0?oc9LM3 zRsmM#^QHzUi@#6lKuchSvf-YetTi=nwU+%=R9V;3h1K}~wU#criod0#>;OsT?aUv^ zqQp40%al|ct_h4upqb1VyYeq6DKiAX48W_y}GHF06t`r`)Zx|;n?C-Ud8JB2Vl?7Vx8 zu|;JuR2^iD{vhTl0}EEmeoGx$v0mzrv+{AT*2ab94szXuASMg^F9O)F1*KiK?!a{b z&x)b35J;3hLJwi8X)L;QRA|XngJx^#97)82LjevN!ciz&46UGPi9&!w%Lqc?5+ZFc zK>P|LCqz2Te^$cqE%P5lAc_lWec=`zC}n=+emNz?yy7%hUtNpY3!lp zJkLMQ(tOK)0B6lxJbk zK6S~>IA+PKA(W(orVB0dWk%6uT ztzr39F09*$;a9n_^we*&qsyEAeg_;Jn7R3D9l(Ua4J{1C9~r!*2gVK`LRZ%U8%O)S z&p6-tvf2OjJHVVhA0hdzGgk#t)|A@S7QU=8d>-Nc9^UwS`uVMDLmE1-v5NtKh0klD z&L5(pF^eGv0GERTgq+_6g}*b*z_~&VxYk$~S+Svr<+Ke}U9jTyzYj5RV+h&**?RXk zEqC8tHNGnlzqv|a(SP|eA+2dDY%QgL&^UvXQx-NpCnO=r#=!QsEfc?cNq`Xaw}YqP zoIe|nS`%5YBeLA1L712TcLrh)H1+h5a6L^xc7qScq%3Vf)&R76E)%UKL+ST$+Ur2W z&hqVY$ij>@te08fKMT^Vj%6+x;HN;!3LMDF4`6`*AwYs)XRQ_O*RAh`m2$1?zFkAH z=JD?`hgUAPf7p~?I=| zGsj9LX60JJ3okq~<~j&6e;eGfCiFbSFFG2(exrZM)cn;+Tecu9g*&w@b#sg4GYofG4M!V3j`&A3y&KDJ6 zJ6sm;SksI8uEX+o@`a0OUB9-VVf~tK*E6g=_v2s{`|32WdGCE8jbxo-{#yb?tMF>k z-1)854Z8~dI0D1MiV1iv^AcCBXa7Qw6~dQ4h5^ed;x)MlcFel{o1_I+Y5qmh;`gu! ze~A(J)e%`X*HUm3+4&T+Rg=C>2LnrVt-rt5-N#z8KOZQtuV#biLzL^Q$lqyrtn`s! zSkLA5@{ibck0T>gAG+Az&{c2SEuiIvrG%1kf z#lpgcQPY1U$n?)-GOZme-nwHrR1t zLB5wQh^q|jpVO;aOLNy81F%uWyo&#I&Hde4`oHn2up_c;u4Of?*K_**#;pSugYp>A zW^+@@i$t}2&k?d1tA)Ub}htbq`y71uz;?yi^Xr-*K2T}|C)Wx3j0Ux z>!lwy zppwM#^K!b_koUKSsv;6>riNVQ%dQ8D)`}DXfAseTi(ps^{pW)muGNsUknsWq76B_Y zFEXnn2JB;K231$;8pCY>O$2Re2+C)5Fu#guLwlMTXoG_mfFne$KD?|zcS-mlJL>y3 zCTJd0Z)Dkg&um^v-F}xI{>Bi8?*H#HsWEx< z*yoj7*wqR1*uQO7e^X{ZKhKK|KpWdZ&$l*by)0`A_z{cSG5&em8k^y@oV#Ar#ACyl zcz>M%TLubXf7gXu|0Wd)d+>KtWxrmG@LL=)2bL4Y@G*;f4`40w1qbC%6iqDjycRFb zxuQ?(`h@KgBVP+e(NJvR_Y&fmJd1y$PGe0m;aV#LXCVdt_a@iFzLQ+P9ETTo;rg{~ z`2QX%V{mo>ky*Is(R>||=QsEK&mr=E4`>%lhOj`(#?8oySyr&X_YYw0%9I0^HM;$2 z@fPcnEpu$JwL$aQjd;XNNem|6N4qItiW z8iA#6|9gHu ze-Fr3+LkPfrE&iz=gG>&#mK?Vz4T`R39z^Dry&g+;#w$~Gx-c-8A`7RT`S z#>IXs2>5sP@CPeJ{@39;7Hk}H*^;pE>VSp)U>2lgX+td5A1|i1WE8Vvjo<~Nm>Gnu z*NtMp$NW_BvH#uzz=_?J5ev3|Kbf$!kr}p+KMm;rdqi5zt}gDS2W3L6gVEwkBG%KV zuo+kXQ2Nw=7>qD6c<_!0#(&(aUV^9Z_xck9)@rVE9kAA8IRBFv&VL`U*dQM8Tj%wk zw5KqDwMMT$F<`Bx0oMU*9kA8`3%nW)LYBXlPQK{wVW5MH8N_ul+ecu%vws1x*6Q{r zhOE^E5F*VE3XHe0sXrO}vo9~Oj zK&%?Z#RlGIh4~kEWThIy!!ZezNBrtp3^=e();N(3v{~o{dUG=P^Nlrx zv5l}22)_FvDwo-@?;wKY4%`&><3ER?ORqh`bjg5C+`;zwrvVh3N%G&=5eaO|x*$Qz z3_h@T4FX~qn196FigiRr7=yPTL3#u`HU9_T*UIf8{=JoEqZ}Bf z4r_^G_a(sM**^rME4#&RJcjLd9Yz;m6f&+MnsMD4{Vu(*4x{TZ%KX3FhYZFE=tzv$ zh=I|?t>LkF_7A}58r`z5JN0XFO)RXdbCcJt(RFLoK+hBq2a7Ky`WI+c(A_*?vyk^J zIscZVc}{vw8ul+#ApifgXW1CJFwqnY5HD_`{SN{$2g~Yiv9FHCcDruRF4(hRGuoB* z>{$fLKzBZW>zf)8NPw}KZENpA`|XaqOXo-;5N1Hnp!kp>9EGyQgsLWZGI02(8SW_y1%}u7=$N({Ou9*D1@>#hV~_fo?#^_eRtDb=)^U5pIsR8 zKl26$j57UgU;;uk99)>3x_@VvSXOpyfEsI>v8~AI`+umKGPcSb9L72q>s9ZBhI&mHaQM8rw=T=?77@)e%cf z2pI%oF?AUorq*;8Z%p0P5oGDCh(*8YkYSo~=D94ku)SYTY-jI5Xl7}f>&)6nsKxLUnM z$r}CsY0&-qA#1)xBmx0m4J=B{EI7YCJ^d>?v-LXpB$r_j(kU+DHOD9+BTna!QrFXyeJSZ(wjxj zdZDQjwDY%GP2FP&_1+%(i!W!hU0zO!-yM|*jXu}2wcD$&eZBAM+*zs? ztri=Xy>FnE#`7&g*X#Isy3>ARO(|%n);ChoG8u`DQ&Z|!COe{ecjk^&-+iFAqy%>v z1aSBhMY#_wyWy?L2F+*4*y8jMK^@VE3u)0O9LCFMm2dB53D+NZDVw9kZOg5ueERO8 ztE$;{=NZ+rj%23x`Q7Q^EPf>B(c6G%PEs0vBPbJbzA6>jR{Us`l3MbfjB<(z;{;)c z_p6&bi=(~yi~O&96ug{pgU|TAJKLU%w(RlSRieQvW0|1!+Uw(^>*6n7Zs_GF=SG2P z%i$?c6?G%MTlDUDQBMHtna1blQl-;7^U-%j6?hq^4>-&^oV2>Db+yFFpfb|0*nifu z*=eSv9&yuQ?912-_X%=@uHP-+VwI_E)Am7WrZhyepN13P;b!Nt%k)amB@djiA?eMT z*y?a(gskT7+W^?jCZF-qpgx^CuicSE5888U?Rq$$(DcE}j+gPzWvNaK-MT;9olLKm z-1xb#*7ftW&a`-DXb5@AlXu=HY%L^)YD(HthT~7qHeNk3m=$?$X|Qv^U^l1b`!^k2 z#Gng!SKan_ozuSF!0@#(=dh(Ao2P`r#U=%|&3y08hD_CC07B zIQ&xeY^+X$d+A7~4a2l4PxPnjKjXH&)z+bRkU@)F6-Q&tzWtq|&bvwYL(YTA4pSr6 zm5>bHs`f8Gre4bw5D46ZO^!)N9k#7lk+W3_1^!PxyN<5=%q-rKk{mzP`?~lVkVUOl!&=y3v=9qeIbD_*2{b;-wvScI6WVRi~NSBtIL=Pjzh)s7o-3 z@?lgd%8||$H8!e_JNkt;W%vH7u%bX;Bls1|%dLgpG^3Q8erYKSym;t(uN5RO-OXy+W0UIq7U*#RCi8XgRds zOv=<)Uok?@$y3MT(OX;gG!$J*CycFYr)f7b@=Gfv zt8E!ML-#C%@k8tU5I7nhFujjaUer%Dj_)Tzkrv2_%-W87OsHQTduOPS zqBzzQpwZrON2Q}f@Y0nnZ+pmY_q|Vb>yp}_Qqkg=LUz8l*(h^vyj8lV`%2p^UfYZv zktah?YF%1Y)c)bu(7)6d6(63Y$Y=Qp6#z3*4|}6eMjlKXFREuV)?LxeC!DGK z<#T1az1S2VTtb1FRwwz2A3p!ZoY5~T)&*BsyxYEhK!+P&WvzV86Q@nZe+YN_Mc$^O z{0NEn4NB>%mfY5(_ro2z@38Yar%(4e&%yY~#|jCQvMxqXq&0=z<12{aR*FO~tc$ppUq5{qmOf&MriC31L~$sy&A4}bsP!6r?UQJihrCHz))#qG zx%*{CAcwf#>z+G1^6N(2jkfEPTWU01Xw0@(oE#xLcAbCjWjH$-S28+lWHe~@P1&AY zPELs$h7^yn5HoKPe-D@?x7sWMb2h7cr;&T*Igoy0xsq%4hzO>}KAMmGi;-ZzF zbv)!vBT0lR?Z;b$KpecdJyb4zhMMjki_%F#jTd-E_wAK)M?14(dmEB8GOT(vbpv_# z5Rp;xjRu@lE56I*e#Y6zyy59wMA6NiH12GM^|?82rEIkVX@2E+<8-wQNl~R0N`3F> z-i<0c=QDnZu*wb`)fPMA(j2F4M5i~MBx=fj)jhJ(+#y3i&tLJF+G`DRHgxvJXC|*x zH5#_X#-12-%CB;9DtsBy-ch15`h~A3v^IQW;%*hHYy-D9m09nShEJxpat9KVvtPL7 zPp_+9IdE+HoOw^=8#P*mhc|r7oF6+@fw3t*YczwaQlT^uWNqBf>DH_e&$pe$w324j zWIrOg%dL#YR^94rgCSb;lMRgKB93$AI4!Gl>C0o>*3r8b7bqOuARJtW(q&Q@x@+@Z zks25KY~?#`edyV3iRd1Vcn;V*6H&R>_oqMlIGA@yc$;=kA)J{7dtIo*adJv0XQ#WI z>~k*+y<$>Ie%pL-xZ8!DT=BhzTQyEjig-NZ^b_0c zi88(qy*)%-J2wQ!_y{Uf;E!iKGX=Pg|Bk%tV%8#vQ@OafLD04LUf$uwgU>an5IHA~ z>YGSNJrZo6Od&5sphJjKS$GH?-#xnlW=&YgF z(r_kIVSju1i@e$TFq0F*?{Mwyf^HD>Zz3Y6iYW3)rfl{T5|gXc=b_LmzBD(O$E1>d zfi)-OMQ>!ZGu!?UKii~>o9oixDV;&LRVKYgOj=132u4ib*xwx^$ci_vF@+GryMDV;Ol< zH)yD4C%$iy0&LFemzL1kf#m8Z3T54`&kin!h8+MJjw`vlEkZ+!6|NeaWz#1muY-@8 zW6T*W^h*3`cSc=JXXviXrk~jAbY#QU4*#)4y^*x?-o)2*ZQggaOIU=rw)AMlsaTml zt37G=YOhj`P6RqtzeOE?JatDCPR_=9%gF4G+1iKE7Gz4asN}aH{Bnu9#agr3y6wvQ zIL4K7raUiF$n{aoeZInGR9)HJF*%%D?B^XjJ&_;!uv?Epu4~$1k~CT=;%t~^hCKeJ1Gd{0@5KJ(1b;i)f#afZy93hy#z_jtxO zgrz3;RoAF%*nqp)9ACk_sYO;Sl64sZXMcl}|0 zmh)T=+AirrYfOGQa%PgKf?`H_bIX+WC-SAmn0PxFql5T@uqE&D(9xFXF3|^0Uw#oA znCh#-iP&J@5+xm4be^t7HB827A57E7F_Z7)WN|7(!GpJ47R_(FbdXsyyP{`FCR z4~eM_RdW-acjb?%DaB3OiZb)P>m#m2z{LQ3T$1| zQM+HACE?G}dFxXVA|!_2IAX-k_ClY5XQxAcPVDH+@V!7&-8a?J?ZeXiLAe>)2OJu% z@jOz~E8RqYa6^Ca7gZVVIs7K^a$w?IphYyXIAJ^|IAwJwkj+!!%XHarZ4ln%uFDM1;>nvc ztu>zb#G9J#eSZt;)X3w&$sLZeRFAa?TYAb2-$fq`&aixjumZ+oMG#>(EYg1$tyhBv zzf%wX%+>RWNSk?(nY*-DJD3yBe(XHfuV09?C2R_>t8Zy z^`?>5DR9x8)qUUdhDTz?gpLR-t#xx^IVTL&Ki52UlTXlf?lB&75lt)fJeb|}Fish2 zBM2{$M{KBZ6FrZ%lP7x>pLGcFrQXnuA3IA!nB-{qR8WeX)j{Nr8;3{zL-Gf33|TEJ zQ30G&buqRx=}q_bYo6S!MqH?Un?d|UHOuVWMrCRGE+XRmhp@Tp9tREq&uvR`P?Ny> zY>RmGYs3BcOz zKW+)9LJpHu;N*;oTz;3~Vwi(RdAi7lmRhu^ZLnn)Kiy%1n0q3RR*4I7ciL%IDC}&M z9j$MncIe4`doLL$&G?ob7m|jgAseqKivbDmC_kT8mq z^EexkXGd`D{c$%^dUm&_jptET&UcjGNrlb&DpnPikaMCVq%f2P7{K)F>u=l&U#bVp4u9wznS0E=R&1Cd&WRN14B34ZF`Cb&^LEbd+lY>5_w*ut+C%9OtaWH7k5(`BR5jlqu*R#y5P!q%g(!; z`#OoikMu=cC+DjU3N4qVMdHZs^Fu$b8^-N#qw^09=Cn+kmrd-v%c7-0a6$=?gft%8 zOoo(Qbs2(gIHf1Pz0V;rO!tsFvHpy`(19%u{WlBjeTS}tADU-4XtWR?UH=Q3L$ySu zl;9^c7jOFHn`Z6pwi;%$&pW4Y7VJ%6!V94X9D}#m^;Fwffr&Hi;e9UZj3)ffIL|wY zarklvaXAhlbmlLo#G}$YxoLc+v~TQL2+X~~V%7}$$>WUa#Bq)v95cL7wlDw7$uHA4 z#WmiO;z~mvQQ;%uhAi-8#1@4<`*y0;JMo1L;4&Pd**pK_#FYJccH3;~H?h?V=N^pd zd`7d0f-968{G@**ud#4JcTU*z#dJ!do_~Dm*bB&r-o_l2vlMeYheUeaTp*ro?D5Ai zmYhSLe0fHRT3=-St*-OGAHTfR0q*p(5PX8YTbQqn+&_5nYu4UArY4QjP=@K*txtDD zziXb<8=q;umm1{Y$X-?#Q~=k+mlzE(~)Jt`H*xivWtP=`7tPb>!~^se9M->Y({MG%reeP;bL z)mwHksEa_Pm{`vAnNz>)+f7TfkKgw83EN5e1C7p9#0O4H*^Re6IFX|6p|81n%5%d2 znE5^y?95}uZ}Xix|1r|O_#5{{7H(8E6wE>Y6rB!>L;sc@`yz9DXX-9jRkl0p-2iIa?~?ESFEx!L)a)Y*C;I8=7B7-ug^ZD zM!x>?Doxo;MRG-8a91I`;j7fyx~}jdhyZLGkQ0w@_1Q}d3fbq~$J-4eqh+GpgK|O^SW_p7S6_H>rbF=~t5@xQ!@kGRUo__j+Xur4ws0_`5kQ<*kCuZ&jN z6fGZdN$yKmb=*^m_hSKa4T;;=01p?v%{mSzFW;Q=p;wayc32zgk-LMf{59eOw172h z8Rj#-;lc|wEgZn}P`)MBy27k+Q$=io;4{uwGtH}JPVcz|rp;2LeJ5Kc_CLH@ zaRzFi>}yw5UA2L1@r?>$5xwT?3bWI#(g7ze;$k#1&&|$^HJ5XAcCOX5`Y}&VjROYF z(XruW{mNlAfHjk|v+U`)n*FdahY=uEtr!>CX#Ay=i>ISZ_ftDf+Z-I7eKSJNYF*=? zl%jR#gx&BlvNIL%)sRnRw?+TWwXMFz5^gmmN0bu7BZoLm8-0$qKY?`l5$VS4k*z>f z)LA6iDwSMMNs)^`O=S@JpxXCj8#$Zo>|{^8Od1@2b-OsY%{y)Y!IH>R|D`EsrD}r9)2Ky*jj~xcYRJQh1HYVFA&#CIe{fIWQ#Kck4KN?AEVI zyea=gR?u#&iw2NkWACjhnb#T(NO_KdtL~HXF@$js^fl(RRyl@StlfS~5I_<~3)Sx3 zYbeS5$hi0uf-xM#4WC%M1t9D>&;xSj`>HRoQ6eWZYYI z<|;`cq6?-UGN`Eu{WXOQ*?1^bE6(GM$H}zc+rFc~n=?)FJ_lccHvPSMHlE@^YZ7er zGXnvV>J@EyX;9W{j@%k@%h6bf_}9J zMJ#y?GC8u`-MnFno>LzC+0`Lo|EisIc+DG(BPo~XvDo88*7o2!(E(Z`g*Z#m6bcv_ zfHjYemwOr1Ztte^GpmDt_@{I%ry zxro5!F|t?0Pk#9z;pT?T1^CP7SnP&H+0aC%O_@ynT9zTTHQoaD*6BlW-<$R|7}%(Z+CkN zD_+vdgkR9y$E|z8)1LG3hYLA#U$2`T2>7s;Jd&7s2!v#%!#^bt2rAU1={eVB*k;<3 z2{G^;2JoeKtum9B)#{Uep$4-?*jWdQPbvng=m2YL_nIY!cxePg$Xz!`bQY-~(B1Lq zd^FF&B>rGJjRSl;Oenha+o0*85Od#w>w4t0SAn~u$`K$KuI{Whxy(vvU10+E(q*3S zGEsUiEkG0fS?3*ZT38$z0`zYnc2MHSDR&LsU3S zeVwv5U2DD3duff85JG7iH0ZjIQaJvrUfh>J)EDF;mmem4PAxq z1{+$}2;U5>!_$V1VbHmPZI*-PkS+xf8T9$Ix9{KkU;eqmmBg^Ys!fDi|@U zJ)F+5?fSQUF0Mv49?-+?d#Ybi=`S1QMX!`pFdEDIP}I5m$eJ1omgxK7ic%u1*sh+3 zMbG7X1_Fry$Z6>O&J95_cisDWcjzW={(LO^raYwbMTIXGA8Eya&*6DTdiWC$wQYMyEVj*S2PG-7yl`{K47{h~3u8=xuze=D3PCAY{~c@P07H4K zB5Ab3qcdR~+sIGI*2JjxRG37WpQ8h*8HO{_-?2CxRKwJwIAr4mg9Wmub=oc{rP^Et zks7O)pD(!iza)lD_1)yg-+rG6lm&Qi* zSouRsm_cRMxiGRw=>S^bm6>anP}D;Kd;6))cA8TbS)ppkwzmf!x98{ZFhU09%L5fU zYu>>nQpon~Qp{zwsHd-B+KTrAc>bM@W|=so^l9F5%VRBAFca9dm4w($$bl(75GTi< zIE<4cl+cgUoy=q{oB}{G@C?eq0eXa+Mrif|$;-X_(4tB%L){19#AC0<2A^zcv7f-P z(;iB{XxsX@LU)bZcI^juKp0OkwH=UD>$m!=qbdN%@Cf4xR#)X`<}*j@o;z9WO1=H$ zq#9scqC7l_Q$28Uh_@YATN}Sz#UXpQJDh@<4XEgYEdv&7%myUd0#|VO^jue`j4Vkl zi9jwPB%mL|Z0)_l6i;ogbXvPHv+F=bt&hoF=Z9dyTIh)MT~#2>jI^>x{Q)O*y*n4g zVYXI{@|bGUQt<0AIP{zEYMI$RIN?If=Z>e!pYDX5LCrF;#)Y19o^crOf6$m=sbZ?4 z0j137IdQj6<0MjJ@&4kL=L{~PfM=#v$hxuXn2(0KEqgHN@a0ynH$#(x&#n7%RI`zy zrJ+-5qhIToKx)+v$2&W+z+zOu%RAUs!mu;y4sk86g>K>g$eEnUQ8#9jvH{_amzyZX zaGNv^h7ymrv>wYH@$R9imF}E7(mL5~TU*xbbdruYw(K@U{m?n9n>#VrRX!zv*0hQ8 z$r!&w@U6D96P?tjD?4XqgGA6?;=No6*-!Y{YAiNDc`%mV{eJ1^9?xYv_P9@g1O~_zQGHx@G=B zoCsSZPSyIa6n%P}NpJA|Fl=gOvZ)5|xJ0yE;Tw~lm+u(xs0ai$yKv3b9K`u#i|gsB z{A%P*4|y$T9OA_mGsl+67TVfP!NcsDy+?YITFE+_pBZ;o;PquqrS76tct3gkNOV>B zXjf|G^RB~Y120AEvMoG%>(dp_IytjWqV9(rYSG4HBx>m_9df;WfK%wx>!I0zQO*5h zz}r#Dumm{ z$tAJ#KOEQ7sVL@tI_>PqHsENLa^7b+d5Q`U6S8T7`)SWTc^;mu+aO2Muj5Kr6Ekh1 zl3M)G)~)@`ouH6L`FcPb;1a0r$&FXa%s_o@s?e|m4&-aQE1l1#`)6n;CSP$vDMay< zo={F;AH4WD0zDMjR^l(ls`IubUK->i-ffpquyf?^cIn)T>Uk#$NEwZE@;CgGg$F4H z?|r)L@d$_eiZCB#|BL){E|6%%g_h2JC>W_4ix15>`_3&|oy|Z826sAHu+2z)+E1#U zM8;w^c4xcF(K9~cWE`(dJa~vHqvHK_i zNxzJ%IB|zoFDAui)~At-EQD37`7Khs*radzFdeGCiH5R|bQ1vzHGv zA_K1pTy&k@z~@!-MP*U~HF5Ds@Bm+^EKJx>NryTw?6(#@>c_wD+lzz)7|cSa8ru=SZCr zzt_e*$S!@QbcYmJW`Y#ko?|LG$;n^dpW35hbSmaTM$rjbiKXD#ywPM9Dlknj00;SD zgHCbxN6%P)dCvf{07=w3JubO0ZSJZP$P**>1%zEgrUl4gJi2={8{Ib{MB0sy$9Iq< z8h2W0NUy-YE|Kh>`iIFPcN1PmJlaFS$Ig#x?_*0wbH7S>lpkT+s<54#V(JvW>*=ce zVn9*_h0KoBYE-5O6LZ_OJ=HI_$hh&1*)Njq!pL>Ppq>EYr0lg(3WHHqdSZ^P`nY(E zB=bg?e=LZAUP?B4Cpe7WML=@H&Q+YKg>qag{*?!1+3*wfl55Dp>vHFs^NyJc`o;7W z-M<vw@-V{Wz!HI0r+6$oT@f#&Y?m(W=@@s8t|gw3`+ghx8OS%7Fy^R zuS8F)A!#rx)E>_!wyx6SASHaKYDqw~O9CJ(@&)ik|< zxYP`GG8fi zgP4?ZAcwvy8Qx}*1eapGaqgt?2%HBuxSHPQUWFYx;)>i|B#stz>fw?^TAW9j!xy4n$a7SB9u(tD?;JTi#_WC-hV_$eH0(Q-ZXdTVh3OI~lv z?J1p0^y<&5U#CpYe#DPszt}VI)*wj)eQYxU4R8*LIyd4va8gZ)@5W{7P<`d7bFq8l z@+Y*35r}K-D+Vz@>QlG(M&7B4dh4SE>##AlH`t5!mBj3}^akfztEF%nXdgFV-?*v3=v}D& z0)fhJeChtJuFygPC@CowL{kvPTP%k>rPtF%{tPWl)OY+bjFp9*81*0?9z8et(lJl>XqqNdTp@~*!%2| zc~#s=9*Gg<^CjDzPYwmiKfM6k;Zgd4q@T$39*cI-lab;hALzOzucKI0)T;e{8F*~e1k*Fp7ymeUxthlN=+briiC|&Rn~_PcoD8q}>syHsk)xm-}FaPVAC%Ph~Y3`~`|P zPADK0M)#K&Ppgo&HDsB)sb-j+_R98r8>p!vUl}f??G%}wz%Db;R=9I0TN}r@@2g`+ zr}FR?m*)eY&b$zsptcOG?nv?Gzwc$oPJXScyBK#`r_f88`_0pU+0fZ;J;+y33|9GS zNDth>(^Kz;WIxL%nm6zaA+j}9m=WiurW&#t9&iw3i3wCmec{hi0g*dxvRDJuumSt6 zA-q?4_tUf_=AdZ0*(m@>9&a}k6VdEH4=oLvyKMLs&jxT9C8D!AVm=%Lsc_Zdq!mgW zy9gZ4QMLB$lMH;^Xp1}@fQ*>2On`fHAcqSSbKaYT_Q+AwnVgMxIw^^cQ{ub`>^(!g z&Q#glt&cr``@&Z443LCQN{SoGd;^B3&{k3geGa1wQS;R=qmFC>EY=nD-e)A;7)=cb zt9du}+gtV==1u)FsQ2W>6p>2py;|V;-F5JJDsbb4pvJbAsm8*NhugO2T5mUIz8+iq zEH1g{ef=~FkXe<0j@9qF|h0PFdmF73YN zeO(F))o^YerfgI!@*5+x(Qk>;_01T}Z|856ft{ynvBe8i(46>8{g5*?x$D#Y3~`#* z7V|OcUcP4^*ktgZQ>wV-*2{k2N>$Eh)4W8`L7jarnp5}9SqcU+lA4FK)*U747x%zw zLa)mUfdtvl!6Pcudx9;GNDdjZA|9BlzEU8)5scPJedR{V zRM1fhWjRT>fm1It1~eDY5427-OA`&BCQvCypGi4W3E0q~woBR*6T?iS$ozhZ)Zw$94(j)gQ=5woVeI1G=P32o17o#vDJNW{{Y6(6T5g_ffRdjui+w zGzbjM6VW%5H{g2gzOkPswU<9xMeFq~xRc_p6jP_3mwmfB0qAmfzEW_7%&d=K?eC)u zAoL?Y%ku;>>+kO>Cr+^EYtU@YP)y5FIDRAe?AH?A&Et_SjQURqgX+)B5)&ppj@?9@ zQhK6Ro>SS~*-|Kw-LM;F)LiU$Ubi&3gGnG3cT4b#*(wkIfV!)jh$*~)6FkWq2_D`e zBXpu&ajJ~{UL0&p~}MFZLz z#l=ifzN!0_-8L$*J(fL!ZpO=Y$90&Q?b+E}kaKcMiV6*C^p5U*6mjlDHL??ZbtxWlZU?V( zx-_SV>75RwzuA})vUe{FD%s%a#uWcp^&>pI8WJBJUbmNi;hn`B=by1r2J!ie&a#?a z1%k4lT0iMdxjJqtelrpj0ciE3TygMlOJnwOuVd#9DW%!^c6k#kA36*g*Cwub?UE91 zPaPan_x$Db)L;aExN+guj^^M@YoEZBr$Nr3vPvV>WWfIOp)xVbqF2$aZ_!&?$Tyxj zh8H*+E&J}4E?vH8tSd;`?IfVk=ArY8^EsE7&y{vqWy=8b*-Lop%8_az`iIw?)~62-FxW z>5NVXL~4q~TuoD9HzJQabyV_RIl(})4J;{Zq*TSLWHN2zQxj2(Sf)VqD~BFQ9F4T9 zeba#m{^Vf_Q1vJszlT+g*LI>D;cPeSNPlfVWc(oPH2#g?Qtf zJGXJ19%Q|i+{&p^mtgI7#QtCaU`!=$bubyIhb)*y!ceWPZ8OtFbihOYhGfU!a68)F zxYK6A6BURkkfd=M6nskMU*ASyTH_W5JgyRy*G#%H&_RXNc>U=NlgvEVP_z?Wy4NUk**4KjC_zUS)~@nFb6sox@Apfdcl zPUoyiq-_4a23(c9v_n1fB_61fkvPJ{Yw}w8?$ito$(|WRhCwB4v9}}rD|X0kbC4Qg zHLfN}dK}~Dl)bUQPs#~YH(cR#iMQ2fRdM??0A+S6yS!B`=Ap4|f_8V0gB_VPF#vxfK5| z#Lo`AbMM^%GwDb}_ZBaBPL$W`U~>UdKkRv5(U_pdgYxeTu`fMwCe9J0a-m;xs`Jl;oUGrV5G1cOxo-UjZ3_9s7dmrUQnK1 z@m3x2AuId-^lW&vMPs=15e-+5x9B|b+*)+caOCw!qRC^5f=TbAVv~)lRLMAv^m8l~ zahx%QSsjR2558kvhzD9y>5rnVv$(Iicjja|#KVgfm{Q|gWzY(TCqAOb?(~iL95~oQ z=1Sj}O=q#k!Ejp~TB1ijzAx~WL;quVvAeM?BpkVl-PVD_UIi?rCkWSf!YsW$LZc-s6gi53>dhd6beJavGC!;xD8= z2~W_qSV1cJx>|mtRFbOqF<{4MJ1nQO~U9FRH4mYIrdcLl? zE|4F9N>$Pgls-T7@kTw3@}?=#TYd$ z-S^#+z?En^l-qXfyvaCD(X7<) za%MDo6No+>&bxklYl8O2EiFCGGDaM+Z22=Fo(siwfnneM%)Ce0_8{P&rusMb?VSU}@Sq*wGN?)X#hD=>R!N>r z-1>gS9wkI!7~XjH)R+&b)4i8}*ZKk{-KW=XbQ2b&ephbpC<-aFx<@?8Qye2(XQ->- zURqiqN@zLo((~5FDYsp8ItTpuQT@+uC+m*%Bplmpa~TKyC`;2vUt4}l|Ll$96VGa= z&~r0ojpDUR@0EuQp2xdWcuV**O1uHi8Kbp|xvFQ%sV|0=5t$k-Dsvk_!G_2EMAK4! zr{VY03^EVp&2N%(#!aNR3Oxm`Ww8$(1OByfhR96K4@dV^BgAh6Zv{3ki=}5rR4A+Y z6V(hGrQ<3|a~7Qwx$2=Ep0)C9?%e&sryo3eft+bL<<`_&K0W8pAT5dh&Ea*dKQka=yo=YAmM#0EIsc!!hCM{712724id zHzb>Yv>oEgVz^og()hwb6<0*mdhWA&go)Iasfdcw z-AZ?NEKuogiA6})qI0p}+*9}S?ESpweb4{=&vmZrd^unIu=mBBYu@vY@f&0O##~~G z!LKSmbL&e|Tbq*78Ek-t?$ju@4N)psR!tpX$2vp7 zgsGn?R~pqW+ESudY;2-yMs~zXL^gq$3$bO&s=7bRO#KwE5qH_|MSE%A_oq644^lh~ z-po!N*;KurS;^3D)h_exwqjO1msKu%Wf_TRE3L4A%O!Bxo4h_s*g#}cDJD`0g3X(3 zeY~)BfGe!nbwt@;8iadI;Wu}6Zr+%3N9VD`5tjE(wKr9%@y)6ko<8^v?$j=x$dI<=LD<}8~ zBe7?dce^I#^Lrle>v$Mjj}|@zo#9nxZl{IlX~8v?DPH)Kig!g@DVxWSu;sq&cjXty z#rv=Hqu79-E8=S_p6q0WWiLKq@`U6QYPtY@l&5Cx4cSCSF)qC~SCe(szW#EH%0K(& zaO1rV`V3V#>-yP(siO;u1IB2oXMOh@56j1wAo6STXKAKZeilPV5!GFn7M@6bf~f_6_S^-AHH#Ztc%~Z&T@h7bJc5dD&ojJ&foTn zh;58W%w+Wnn$tqZ?bc+lj0b%~KKL_Zx%<^9#~RZT@t`L^ztMh1{vuf6(O@D=)z?Tx zh$kbV9Tk^;i1U`NxiWeGO2B)(9g&YU_a()sSs*gu>+L>UxQo%oNM&QRZ$=$=Ie(UA z?p1PG65NJ57ry!%mi0H92~#-oFiwWepu18m4fH=JM z0OqkD!)=YiV(v%MGpFf2110q2V8?(0krL?rGPs!yz6E78V}EQHUUMItcWsl;#61wCn& z1~XORskjNJhQZI7GLrDSqHDLLkDhW2%gNh*4UP4*LKB^PTF5g6)C69S@pPvTAJLjp zc&A+L%Vtj;4#p*$g0(mYfdAfDe@tk%wxHRy*v==@aQekT=aE(df=`Fs=W2`1n2AA z`7%KC+zNd=bynqoGTZ0!kD0?b0qxDzX0pe;RCI?|GPCH;4tt81-#wd*9qvMT9WR1L z?ZH#7>}>@c(D~3WR0&k&@nzib*x^iWuQi)zje{BJA3KeTc~8p z(1XCBN{lVj4BzxR$Mc`Kks22ZL-ceCNqihuCwIy(vnV!tum#{6v*cA1+u*z;zu~zc zMVjHlQm60(bba*50&jNJUY$M{a&6OtbxB5@>(o9OK-_PZI)&u-R|UtD(7ojo)2&%l3v^qAO7|e(x0%ITI9Xv~ zzvR+HWF)153i@+(xKeuVKj2M?4)6@MO~QKEQ0b zghc-SSC1duH4|OhIps=`S6O~7zAOE_$@G=RzZmbkRbA3MmxK(rv1!&8fP#|T`*DZ`V9XMD!m@p!kgMmaChV90y_YWET`|9k0Y8q8d zQa(y(QTH=HYkctD9%%08<}H}1I06|MR6V_UJX<>P6`~31&yi_(n@zhCW=@gw1(nqH zwbx90uAdVYG@mvJV=tC)-b-*my&^ zOx0n+2x~Klkj#`M!p?4KeO&y23m(RnlFj?<0nS?nL%g}|SuZhx>SGEAYzQA_#AHrRap>_Lr-roEVe<@qB-C^-UHeB!q)KTR4K6U#@~= z&WE@#D78;A0K!3OlFNatpap~>=d7>ZJ~76+S;>5#U{mnN%q{4IR3%lraU0#6)Ftpc zjlUQs=0RJ`%-m3-spoJ{sfAU}Ze+W*Vj~?F`^MKUiYZqf-nwrpH2ZF2)h30L4nv{l4ikxAwo^`D*4eEwi7(f(bg@QRWd{bqapNbzvi$? zKM|mB-#NTr4U{Kk2ddZW4C%zS3iW*t#bhqM<$kR@CL2ITH+rF`iUkIu#ZeQpLC&!0 zcC+8qwK=NuO{cX1fmO67*R{-8{fJJ^HXX|Z{l~)RE2)wk-a9yhSh2~hH&7HyHo`VR z{=CyTqWhyH{D*T9?dhQAs8*f9RH4F@Wn&uU>V|?Oq*JCY}$n9%8 zB75-CO!E||p$dQDW(r>7G9M+cpk11jBjIwP}9 zwu^kq%tq@o|(tnKG%2O+#2aYE05ucyUjTkd$7q5zMR^Im)GlBXbu8XgP zv{b_R8LOT)lZrljl!xP<|9hl$H7~>lCL)a)LSjG&+mucgdJ=&;frV2Sq_yV2$<=N83*k$#Fb<@FR!SXs^Kl?J(`suV_#@vj6^73=w0^dnxuODpF$IYVLM~=e4>GB~-CbyX{Z6uvkoEU-M zMd0CxTF|4)C6I+^x|yN43Ftz-Ml-Wr(Acs@9eOKfR>{cfeK2pIDLf|*(5j6wz@pbN zNNW(7J7$!AwOerPbHw2H_6~C-VS*2{S}rx*%Pkrr$hWHKal1&O=2f=u zn=>h}>q1K4O|Y!AfV_X5f=}ovcNPdS8ne;145>ESl7QOdreS^tAOcH+>u2#3RLhlj zooC$SG^(M90@yw_!IruG<@O4@R4a!XjkhiVQ7tr+-s@t80IMYe+9(K2VFEQ(_5BG6 z-WvYL;oBjs!uxW+i;36)HXGbq3{mk;SuzaPZJ8wa6$M(g>cHo7yJ*bRgFU#ya-2gK zS#}F!hAwUU_I~E2IcoeYboQ8_7~8jZvQ4IZ*G6!^Xdjm9a&H3*>CpQHb-}ngzF912 zV?JpZa&9wgsPDD+W4mVg1pRDooaYi%rpNdV9yB+(p&9%5S{CYZYkOQj=qNCgaQP1U z+0u*DLi?&+ue}lbp7YNuep^0c1XCyHyuKHGf&RrPUiFK%nM=Mzhh1Jo|D<*C@E5`h zq4Up3KP1-Sq+iUX(0Mz(eWNNcM1ZWdF6QD>R!1+;<+C>2SbXoqzN)qwkP+l?5Ug|q zh2l3`G>Ubj|B~rVzSqff0BnRC_q09c zHDY?gNxAZ=ydSS1SIOR&L+2!s+mC^}-$SHxe+cx%bs`6YA|UJD+An&oYR@+yVQ@N$ z7o?knX}eg(kw4-s03_i^F@1EmIwvnCkR%Uk_r0lA7!|5h608mF?vCW@&uVWhGa_r# ziYqHp^13ysi!(~>*dU8QPcpaSk>;AWO(xJ*lqb$)j=Q=Yx7JH1k28Zc&}{)?nv>%GCTCeFbb@s1a2` zhz{HVKnh2;ef;m&4er{vu?962OPkF4QdVUz;83<4tpL||pTKARZI?g`SOsTX_>?{D zqAz1~5UqgsVpp%m;yxL-YmM#+>Z@wm>wQ3M!~o|)g2>u8SQSKScLM91jV??pL`Sjw z5Xino+7AQsv|2p2yqeK209wot`*N<1=KUq!he|n~&n0QG^!2fT)5W(yJhNU5eAUpu z{9u4=5IZDSXY*k_6?p(?u}*y{22g=;tW|eKv<&@NOQ)W&52^bWNmoPU8$DUcGxyF?F6>tKnrpE!pwt zpz&8;=YvX4SW>AS30Ih8D5bOoDhId?T&|>5LRt{8Ys-L8vnR?;aYNHMt0PsxmiZ#d zX)Q=Ezu8E@4X_rQcUiD-u_{SglY>ORUgP2JGm(R~IrJtN z3;|&U7ckZ{KirTz3+rm3Maq>lBvy{~08lMHPMKp-`GoZAC|aHE)uyZCk_4#dz@<3Z zGc-PN3bM>N5s+hs3-m0?cx8)BIs<# zh&-kYgPP3&2SKOb;6Bvgz|8M@P}5-ATV?=4GooK_7lAz1r;v*gV*`pvpua@+Lcl?O{F(l5^m1+X=hcR> zsgSRVC+nnG0dr7-!u<5BhLR;u_R#44Dp(YY+Bpo1Q@rW&2;0)UH)}ZQ7`Rv=E;hbx%DXjR&tCJD8yw;{VeHcsj)OjC@j#LXIuh7Ch@9I?rySamjJA2%nZ2A zXx}SdiS}2no&fg6Wp@@bH}k6}+?BVzrA#K`nM2I1^uy-snnj3@%eMrFi+GzVJDSz6 z`#`XZk)J9Ew8uUX9BF7RUrKhH<)_!EOmGDVJMjQ*Bg7{q?}c5$wpJ$Jffz*1T$drN3H-(3L=YZOJi{$%?K-mlqcCXtJbk{)us~Q z!)`>f*Yv=(-);yZvAKBvzL7DqsSs#Dmzbj?o340b2}TLf&ODX`W(7|G^M>+usE|$Q zS59mND`L(*uXlki5gGVh{PWQ-r@P!aOhfP~EWwQG>K?Kh4+tJWk$MfG+ZZql7H8wLI}<`wve&f+wz=gpe0$ z!*sa@L_t~vlr-&gYeuQgTR*?+2Z<=9DUz(JP}cGI1E!YITqKfoR_7X(LZfMh1tZ1`I{?o<#vPS# zpRtj}dQfY8iZBdQ;Oq- zucn2>0&jX)PK@42LOH+kdLSG#ZzR{GeJpu7?*8FII^i{$>>`Q+f1m5|+{Pieli4MG ziP2Eme)M^rhntt+z${Dz^`g;{6>7_jd}>hD8TdNf^gYd11nIj=}yUP#5ZHfMFr5N;uXs1^q z7xb^p1AhkBLpzBOGBXn6QE?mYJ5X>TcD{OfUUX_xdhW9~!*}DVg!!|d>=BGTt;M}b zS7J6;%whT7`H7}p2hTov{1QF$qbBoleP(uFMXzfVQ?JXa-c6ddV)v96w;+;{)lgGQ z#$SFv4krZ2FQ83arKyT72rQiOJ!a)}Wi4z)aIZAZ;0%yscyfx7#`z3;ex(6XDIB2d zfwM|L)?j~lW0oAug(%X3RwwT2rsdpXy}~AbbqiG7*&1EvVGGi6sY{Lj@cYBT#CvrL z{>vN10J;c))+`Bc4%Lq&>@VR1C*4AgX~9S9y~KJiG)YNZb$`T=oeUy|o(0_yYnBcG zEnBho_{*oNMa)>6ipD)<7V(JOKY6Q!+(7|?Gtj;VNO1k18^H#>bPvbfXqa&1uE~Jd zTN2b7q2Yv)@IoEUofoB08>x-N&d3lPrkd`|MCKiPar^;0h3l)leO}uMP4${L_Ov7 z!m3iDWf;pV#q!yHcwTIJOjH;J7gU**ZrpO#K5+9GCXp{#Xf(z+wW_u6M~~+*eV5Cb zLo9Ij=a_%pjL<{9f+XEajQtRCQZhEs?OmW#-jcC4xN8Nk%8Z9!KHW1Jt3g?jR0{wB zbwd8i&S)VcdY1+ZBYU>2_3QOZ^KFGW=e!H-5I@uIrr6Bq`ER>o=pM?L20x#uejs=w zS~V)HBZYoKnnov4S%#_SAXqAxTPOHt#Mg5~Z9;bZW8)P5eIav%@bUh+{sk)D3&FpF z5?&ELlh8uOktcEvE$T3J&^J8Qt~bINWd+2iLBVtrc+TGwG*d6r_; zr~U*Bk2owHc8S;=gekRsy31bgw&mI%{!oR%a=0lPDd+!^l-Nns<^`3BT;g6?y&eJX zxm3SJ-|sf|FcNf86e2RlSD7Th;@YFo183>EjasFTm`&l2!=}Gw9Sz$K7e#x;rh1~b z_t}#KM?LvTFD`9y4Ko5%e>eRbW{MqcPMAjR8>+i#qqT|Wzwa2!Rl(fG+tZW`7qSnRbve95S)@!Ag$<<+ooA>8k08=U`aG3k0=kjod>^Qq>D+q()}I{|w5WBGX4M^bC(+SL1&;TY9%s5s3^i(}gXxwtrUF zmAkZcdL74bXn#2t{pjfOi2KRr9seP@`CSPiVeDGBXeT z-Y0)?j2^<^*U}o$Zt91nIYo92CqvKQBg0$@!=V4OGTqMSPXVt;t86C9I`I}#2yI=` zj*9gj*?;+ThlfLLYje6X5)gu8a3-T+QiH6#f)8(kiCKS%tg`P~Up8GS^D|f%EqOF( zrs6_Z)v~y`x8CvzL3Jp)0_*crxD-CHS2(SKF(M7r+F_>C#tNyloqoX@8ml!gd@e^R z@`ejVifps{QT@4o7q%OIF0GZv&b4oSqz1BUoLU99{Z`y`&J8Z%Z0F`qXM8KENiIvU zg`216UNe|x=I2weEkSsR-$w9w{|oY)ms4x7dZc6h*2nSw;bakble^FRjd~8e$$ZJ; zBIP4Tf z66&F=zVHi|^Nhz4FfrfbpR|%8QlWi~rs*monHpBJ>B<6BqLR%F;77?1X0Ss)$`XCd zRRROUk`zoKvIxTAzm}SqxseJ|OmxfUjg$3OLiX{TtA76M<%=`ngPbJL%Tut$5MWPN zpp^R-(^plK{~<=xtvevJR<+Qz(Pr`_v%+@p{=`xK@kjHd4|%6G)taVjw19At zYJwc?Po=nUUY*hwy*P<(IWTJlo)brZxk+NftQOCOydd5=kcn)BY_tQ*>B!=rv|W@n$!Sb&m(5 zd9&1sytd(Ey;JlSeqaNHuVh9QyB8w2t^AN{?7dOA#s(wn=5#aZ6--$~7$QW5z9-aD zTsJ`TmQ*I1wHH$h-@O1j%s5HU@2@eNQphu<=E%veglqroWA|?QSCC zjFvKRP819?BAbg5bUQG|n`ksecQVS?Uy$%H`$^py!Wq6gK*@q(GVqJqG&iIe})~ zIjyN)z}`1PXW`E=>lc#i+^d38+q+LNL}|WHu1cS@!XNNP-=EV8n5PXAp!zX}Q@OnmIwlj!)|ft| ze`{Mw$&wFDUuY!II6#+*KXbE-e{DeXq+MS^!scy5$_a8E&t-Y^nIl%F);*Xm3BJS7 zbhCbbKgVTs#7lKVp!vg%_g~TWX+PE^-9^W|i4z7s+CZ)O???7BmhEqND26dBrYpy8 z<8yWxQ;#TO-w@H;X7Ft|q6yfpd_MqH1qouP`gNJCo_G+dI}SHbdcJQ8W>tdE{4~yU zZU567mP!zq>BNOD&-342e#qFUJDIV1dT}#!yH&UXthPIUby__Pquf?9;`#iYNXiYo zo4O$XtJFkyz$h#~?fvhD=G(UzqIk@vfG3i-!3q+K!)TBU#BDVSAU5>H0_8xe}hl%{{@~EL6r@bkd=tz zF6NKfR7+h_l?ccyFRVDpqc$Kdseu0&P*YMu|6JibZLB7>&E|b{++pceny1iJ&@db@ zoJf13x@Gkc7nNF(>U+Io_RYZJESWYEtFiB zeOSwHZB?e>?_pwZvlh>b1%~839$q54$)D%)%+pkVnt1fD)(5PRCvAAh3!B#Ir6qxt!LoefX%m4BR z*DO!wo34M7{!;cVm68pQlx0YOG?1u7ocaP=Cr{B<$2m-%99EI(bpTpug&u%er~9Ht zgcs;|M*#P3eg=v_7I=g!4n{_t{&If?cM>DgAmzalv+hQW=n?X<HAa9p0lc@+Bk zmbK?P{_hpm{LKt4p6NG%P=U2a+QDAT>LU&cx22SYoq5{56I#WNcB;Zr5k|0Mr#FA* zRf~M`ECr`G@L3RnbmY6<#n?BnlTyJ7GN>Dv8W+8&pN!-;R*Q0d*5@x~g()?cEg`$F zNx~z~_Yr#WICpN<%(vxnc%FA)9Blq+AAMB zQ_Zy~BlP8PqgQ!&4bbR6^AGF=`6+5UJ4~_|(!4}FrT&u=+1s>)oyjit#}4ck0vKl) z_AFj1DdYmmA@}&d`rVZIoo(Q*?!V#G|L)zpQz?Is#dO0#Rc0^<S68rKh6MrKhLfrLQL;qo*ZPX3}Z%q<<9y;}ZA$a2$7{&NScV{ADC^CHT@d z>WwJXTO4Sdn_q%)W+hs`Vo+}+Os^EZ*_|t$a_Dsyyh|8$! z=Znt1V>e?wQhdpe&|1Zze#Ha5DyskCRn?%8&<0R7rwv09sLeMzq8>)FXbNe3G*H!T zIz7{=J^$Eka$E!-$}r;O_t>NRS9&bbeQ;4)P+wuNf@2)dZ}6*lhEyqyIq?k44YoN| zZLpmYb?q%Qx<+PTZre!TrrTk~rGrD%sq({tJX_hdnf%~Qc)3Obj}==nO9E{x+XDt; ziO;d|?ENxJ6MXmn>N$_NHFuU&8Xz_aHYd+_ADl!S%;SX|@^VBQh@=#teX6ttOzVjV zCaF0cXdZwk&Ul)Zw%-siuE7%J)Gw^@tE%_Il)?&#*>TiJZrj^e6wUti>E1_ZXu$Z; z?T}r;d9LU$6q~||>n+SHQ;H4BP_R_qPJEOB*SsMi@cWuFh8;UCj#FFJU0%!@(y$;( z{Adah$DC>9{C5reV=^8G^X)1w3Iu=UMFnJlNo#IG9p(xqTFU+37Y=Xc<+!T(^V1_8 zC0tx@=5JDrP|Z_~Gk|$b8L?g#K`TixQMSY!&*_xn zU3XL-7F?IFyLGBxW`cW+UO3e>#iqij80nH)o>W8J4|-ST(PA`cnlsV#PpRU&Bky#a0B%6XC7pqis8GEy3cUwX4#WUMK~ zGfh%A)r%U3tK}F1Cq#c2pkRgKl-~w;xSHmZGe$u3HDXqERkL0%Es$5VKsV(EK7B(G z!(m$ND4asZr3x}CnXK%Of#swF0EFa>3jq){S*)vaVFelnSOq#XbgJInJel=I6+$}^ z_yoZlVpi?vBk*-A_7g#?(MXkS1^PAcxKfejdH8PQ_UjYw(^Cx}4%Ws6pCCtq zDAFZ9#pwHZO6{v3)1OSdH=;GYNWx0lBE*L*EJOpOa{^+sY?bu2B8r7~+G$^9q3uiA ztA_o%qV^ADz3=p^UiO!7mmUMn`7b*Y8D(6M1|dBcZ}cYPh@!d^*tSgmRX<3h3_A@xg0mt1?x9*nDso^ zZn%7hoNuP!-sn>76dG~aDOc>IaIxwp#P%hs#F8JD=!MshH=bAx*2SCjr@(C!>ZbJJ zE`aG4B0HV7Sc6vwaant$dhs^ZF&i`jZw5oTdC%cX4U1d9C91nvH3U+$hD;xwd{VBl z5QtNgfr&mdK>hC`6b(=3sToPZVi1KvGB*c(E+#tWSL!rGgh4st%5hKiSk1E#lrM{_ zY~hrZOh^*{V(*f@%yy7hEv|QT0~M!L19aw%sBCtn_M z^+u$9eg7Wk;nwRQDvu5nNi&t-z1YBggGfu2?D}k0)ts~gpNvq*r-njivGdeQyC34C z9k#rgB*-~Ru9hy+6zxC^J@sOMiOA*oZS^i)}YmFIs(hK2Tfhzzt8(lOTUj&E;Ur+~r^9&jp2u)}w7TeurNf5Qf^EIc(&zbWimn zZItQBf7`fj{^s2woxz3o&|F{rQahFJ^}{!qO@s3?rV!d(0f%8C9`5+cGH`e3gZZIv z(;2k(7tz=JO#&2sV(Ac%PECPzjr1W)_pC2mzJ|9xdr3U5KZ=Dbs~f=!{*;CRZZDP_fgc>68=a=bzNti@C~dd zY^oy{L99?ri99HMoLYa#YoTf|ff#Ol)b^QmNVz0pGb;tli5bH#CA z20f}(2MI+rWfYun*74SgedBJG7o#`7xxs0$j4s`+Ha@R4!6~?0B6sU)b(wOy6{}lf z@=0+yN)E16T(uy5t~^Up^F`U^p-RywXD~fZx784?V(lTQdAc)$8}CkgiXAuXtULIa@kuX=W^HRf+#fyD+lUaREObwMUj9ASRK%fbHMmvmefS_!67UJ{IJ}-FA|UK0n=xM<*F~*jZgs5L+SQvqC0whMcR7EREZ*I+Qg~bn> zcLV7wNZV~eesr1+)k+#0B^S@pnX-LcFTg?m(ww6Dn#HA;e0Xk^dku~ z6Y%)P>?%;KqnEF{RHf>9ceV~?`>{V%S!QxWQdG+K|8R(e*U1>TWC9D0mP|C;(zI>r z1S+PvX7JXu&&en+w&&s2!*R9TMH||vDW6wc=e5N(UTf`s4%kiDSJ~wuq&uVphJ)X` zsQ+)m<)4^_^*$$FOt_IG{cj{H0?L|gsEB}e#A^VUru!%KJgoh9xp?qWRz&VEN*rK5 z!1P|d8~q#NpQwY|*guo}4pJEc-UG%X6vtSTasEt*g@NGdTmgio(4YPjFaGmyWzqoK z93kxGTHU+;q-I>BkOq8SE;79TbIgp7*L#_Dzp^8hiG$Wy#N+l5ipUAh<+Hp{E|T9h zpiscI=q}m7k4w+6+y77m^S76CR6&Mf3KJWF1hq7T37Y>aCiwTshbhE1P|(^qgq%;o z)sE*2pt=mA4T4cSnfD*N7hC-G!QdGZP~K1NesQ@V0<>LguKpVV5J?N?yvD9mrW?bm z^~FtD2J0#H_Y7PW1^E;{Zkl9M494E~c(~{9sB2k1-@#(4Hll(ibJFAI=Nlh8CW{6%{TIrm88mCNMnnP^EWN<+n zn2GoMt7MuQHuC%vxGg3Cis+P(6II*KX!{)rAYLmK;@F5DKvQy_fD`=l*MP)|ZMd># z>h!afx%KOt7bBPZW=@A;YNeU&#zr*9Ve7{jt6PHhR%DiT-!v)Yfr18`d2F4rY^+Hs>-`Q&_`P6ILBmzos8 zX|B-irEq-C@akYyiQoI={XLh9%~?OO8J$|089)2V^Id?THm?2o+V}e~@ZNK_iwtfYC8}0U~ti#WQ?-^9236fSyD%HU?>Qjg+U*h^m)aqa40H&%rV=KMCZ!`J0{?2h$fR;LSBd3np49=4&%daLJ~C7v&{n zVpSQ}Zl1(oM%YmshKp@C31?_^Bl}hdp#^9sS!U$(>E78P@9EZnz2oY@{a{Z84I844 zMBZX3b#%mUl+)b`uC4;s(98fLNg@N!oJWLR^dmpsnsu$HaIli80XtKza^Rs-ndFoq? z)tG_zgt(^U1Z${HkGVEq355waK<;MmNzJw|8WJ3%{^bQYPKRNK2a3@8iwWSpqA+Ab z`y1T<2WR^Gv3(u>+&L8wDlFUNDQR1S4eqRWO6HGu;Pr<_>j}D@Hn?$71q@@*v$NfA zJUfa4f*o!+P?2QoLqP%+^NR#h=^lhET!! z3F5IOSpUao%7S9HptA+~f(Y0hI)!5=$D2UTXW4GB{0)9y#wX4B7~?T8i4-bfT~JPJ z@$8l>j!DNVz#AuZC&81DTmv^CRsH)U{09%O`SUup${*)r0@hW6yVQry7!)@%t-uepuK!XPynmU7G;)8e&Q^&-5!JZ%{V+2=io}A_JJs`m-Qm!F26Z#~LFwdDDc`v@G%~Z#)K3yk& z09fH2YYhutaA7Iv!iIlec&)X{Vvt5TLo=N7+V*U{!O_-K@$d5uT2qgpHTCC3pxXwJ z+Z8aSw}(1+Ujp*bQnR}B{M8)x*1i%Ex+oqoo3f_9LFuE4XoYcX&yH+s(S4^_3qf$h zKXm^;Z^*N&P30B?%I1$}-|+$#p*`jfIW)C$L0~Wl!PYSx`FX<;f@6E^iplpE+T}(> zNU?5|JV^r#(@>r~UNtjSfi|m;=TZ3=odfWNKOq)~=6>}z%?)tNZ|p$>mgXl6h`0Qc z1(-%3MsH1`)p#V_3FxIH0T&r7^eD)5; zW{en!byw+q+gxN4PP9mhrw4W0wliQ>{{f&q?tN zEdUK1#*~1)U3w2`+*1Gir7>aryKs63(IvK9wBN&_A02>+Av>!}Cmwv;<#Ma3jdgL# zzUdjDneU+=w#1zI%Qm`hPqKpOl9u0Qs%8v`dd8vAgG8!ueH^KOn{=AL#e~IUF`pW> zYnXqQlLmM`z#U`TuR$3S6E3=Kxe;LozNfc=Lh}mq8b4u~&;Bg-1f_tpl$9Ecf)v_G zz@A(qQceBd`92)tb?;OEgB>BqJ%5FLm$3Mn#>WNlvXz0_B`y=O{}V{+C6DtCaM3Ye zvo%jr9^4SSq`eQwXO+L54`_IQ{uo>#laBx$lZL+8OErUzKlC$jnUJT;q=M_M0vY%T z75ahbO>klfi~lXWmXw4y6S}2=fByd)|9?Kz_SaN%RP~uuS*WaZd){e&(4yMtZ98jF zE7%etr0zWiQjiq;^}oFXhQHCc7=YwO#O##NRR)gIkl68 zHjcjh>)U@X-AZ7HP8+ppn17G{#Rs;6C?LUxM=T0~azYd;C!RJ#I&E)j;Q!m=Esz@? z_2fXC_`uCMVVU61Ptk7xy_jym`4&3i@3Rtf%++aZ#H1L&_!-c4We*+EM5GXmzxs|0 zsP$+V+F|JR!B|K>RTEyIFo zI`lIAg&1ZW6*?{#{CV>gj1Q@|+V)$%k`j*{{Vz1!|M+8P0hj%T{cF<4GkTfRqo(wd zA@|BmyEQlfaz1t5GPeuv<6pUFIa(DsQfMIgk(iYQhH}+xI`K}IyNCR`IfH8S>WTZn zE;UM?N4MGnNJx5e*iSk|5fM#-P0=u$?XIXsnXG<~PL#(=2;2moy)i8P+Umw5W-t@R z5+^r_ft7+lhds63n=Mw)H(uAb)wIlS7Twa*jg$WfS^5IQfF1>Go*Im6sZV5L zAF={$4b+aqBB8S#e-p3qd__XbnRSulm0*26mS9^d zg;)P}9}xbj-RAPkf7dB4JSDN$AmS5l)-1k){QnctNkGPxmffPiTpn_ShZi6mc=j}d?VrCgV zHYbuE=j|bT)#DU8c}|lKYCw4Fm6`Ul0XQ20aRyt}BjJ7*r;Lvtr=_>tHnb(Q+g$Tl z1dgrn@X3oX$gd!23TZmy|)p0y3+LAN7 zgS>k-!l}QPsd}VNOQ6c@#Y^Hy8+yS;<#ST{$rFLa@489&@E?kea6f}Nc^V+CTw!0DyL|)5?eiF>s<&RX4bSAb zjU7x<9uj~rQ?5}r3&~(R=UuOWc!2r;Qc6pLVLW}H$u(NzpL_ZxA``?aJKxOOGC7mw zOfp$jUGT|#>V&aEDq^7CQ2xBir&=B;*C{rwZe#V&AS9S5C+e~u<$Z~woH&7fE_2>> z{NPeGqEuWXxHn#O`ka9BycPa*C67Zx8Zg+Iiv7NO9-EgDLVl^fjY~7j@)@(wLS@t$ z14LKj8BJOmG)zy*`|I1?*1ciVmttOS2It8N7>!T9dm?zR+@1EQ*7X5qWrub5iCW{> z0*9MBm3H;w*Z7gA)fSW5;M=2bAr0Sk`%GWG2~~tF$78Dz8{x%-1bvSc6z^ivrQCYnRhyLM=`88W#_X*atqBoNj_N?skDGZ!h zy%FF8?-GUIv&N2cT>Nj*f>U8R9Q(D{Eev{yWvbBv1)YvE7OwXnJ4HA+@;i%niJv|9k zt>~WZseG7siIqzPpnn;sPhj#0_;C<0wl18n&(|=kvqb|7sZ`;5wKQbdTEe!k^IDzN z%1`mSsMaV9)2(%q{hU`_BYj$bchv3Xu|~byq}#@XxLrL;eeLI1t?}$B^%GmvaP#S@ zpnXwPml~6FH&3tWkCJdijEF`I8+|CjCVO{mQP|46kSIuu@Q$3z*?%%Br*%2uSuF}f z9VJfbABw^AlTb*tJ$0W)VUJZLAn#rdA>+@<_EjSJ*sfm&*jqoFRIcKCYxCHib1>OH zF0GQxoKGSK=JZFQLd4e-ZsI_D=eM5!a5MOag8g$(sl+1BA;6+9G>P9yM#DTYyiNXY zElR-iAScIRCg*V<_Vz}|%n!CF_r^P-*4Vp@ zR$6NFlWko`HkK23YZr?3@{vpVy*f*gaiNgpFICP^*!Muxc8qxC``a=FM_7!Y-2&`g z=)HBDc9n%s986D&`+TZ7XA-L$AWO-#@N&=<(VD5nhHA%X$h!)3Catch`mR%k7JfvP z!2U`D4e%CN2uY|~=u_L-v=;{EVF6cFL@E0xw`!e_i_8huUUXBPwM08*5CRks4<-qx zY5ka_xF41Tt!?8s|49K+;qFi_LEQQR#$W=K6>Jwsz*5%)ynHV~xFh13 z96O8emz>!vp!g+ps#E3LJiV{HKc-{yEsoT5a%9bp{1@d*B&0dQr|x!mQ8lviJ1&OR zZ}5h1yKOs!);3w)CPV(hD_b3HIKK>ip zpwRd0e$gAnPF1^Wa|~zXqt?G{GRp<^em&1z#{51YvN1}eT)c}^NZ{ivJBwJ$A6yqKaOsA3e^ff@@@Xs z^E@?J%U1BD`g8hiLP6>7lTPNUfyb0q3%B!vV2}(3f_h|m1PHBBtrM_|x(()foi?G} zpVV5VoA_?|cMokaE053X`g!=g=FOwJ#?>NhCd#F=U6bLct%ajz4X>VjR%>Wdaxc~@ zJxgxfi|tHfOpzYobumXKZnH%SdF*!-^1|mu?r1cQTrrt_b)g5pVNiXfH`M5+VL6%= z=zY{FFwx`_#AW3jd++=-jjO;bR5jN#`4gq+5d4bYA(EN*XXU{)% zwAc}@rypP6+=$fo5fm35IPTLve740!`GARwA-QB(E_g<*yaGvM}^$=N1E|Mobn#QvsPuU?_DCEmtiRXpEL1MM=1TZ*3 z3r!e;X3&45erQ$nu1RDNg0#m(h3=g~O6=Kl)Bu<)_eyheEmwErBIAa)j@20PcRtl- z`Qhw%p;c|Hr#QGj`FJgqvaat^p~*Cl<_tv{tRnz4FTlCtRtHUnPdYR<-{sPs@6Be2ix5sTAhU{Snlss0layhe^|ZEKf=C&pHMrh98|A za_C4Td$qaw5|_2>cQ`%KH|o!hHOKr3l7cSp8~=%-wH^XauPSU}pZhF$Ui)T%s@Op2 zhEdWe79twWr}^bs#O(p6+qy@@SdKN{lDPp3Gq`%7PjJWed1@O)GU%+W?n)>>aHf~y zJ1Kyuj|x;ba*GV)pV6SPK&TIT(D`=p)9~!H_h?l_8cBqhNOACH)!n^+kX{pQK5B@# zpIOfOyabDp0Qtd9!V1&&nc#??8bm42eDk1|0U7FBA3?S;`#%37!-=3}-}b{ci^nNZ z8?L(IJ`&A?Fpftam9{ks+;(G)8noQMWPR9c`Ga?e~R518|XhYROWvl zRB>n6DFdL!ZZILl6-*l(Ipx5Sp)4=npV>hWlV}7k#$-;Xj%7 z;>VdQWy*_EE&1WTf_ME@yfqnq7CYYwM(@}qgVo2OhClf8ih0AuGc;9}h|4ysj?PPAXO7qzkW~wYF z&c@IaWR2e{Dh;D#cwmYL@`=~CjP8_Ko077fCD9!I5qhanUgKvXT)49;`_^3?&#;*b z8*gRCEr70LiL!CJfLRVeAh?HWp5c7@v`T7gu%ZOI(s1f7clOnY zOnwmt!v)K$u5zG1DTB4UHZfhZ{aaD-T0^3t<(;XXD8ePz3}4CO(t9cNDw6#0B}3 z$4k%i2E-=orN&RUyW3aW?{J;U?o2m)uHjp&u8$Gt0KVBZ52;(lD0f_2s#7V0K8D;X zp-N+URUxNF!lB?x$ro{RWEw6Y-YdpM0>Fv&QkOOi;zhoEomk_pI1Q)0W6PQ{5*y`c zKzN}~|F*`@Pepfcj1cN!-}e7YO$5>B{_dR%z67bedtLCP5X9$qh~x|a@S{AX^E4L7 z&@Ms;W|GYqrA+n#j`{0TlyqkiTD{?IV>@Jk$1t5!nD594UCyBOOsDJdbf^+%GmlB- zEkAxpTq0G12_&Ut8@UwYE^LFjEPjC>Nv%+ zTkECONvA1+fZkK+Gaix)|5tn^8iJXFZrosvoxnNy2rHSJLy?gc#q#K}Qs{z{WW zBg1;Hm&48iO{?UYH)(&Aw{APx&90JIB%D328Q214?V*|2w>=3!fb&FyL( zQDZnnm%mK9nFNW2fN)FZ?XyUFSw=|<1oOAEA?Vb{Xg$SaJs6pwe7d?Lv;>O2pj1jK z7D#=je-&|2LW{=8(zYV5Mfq%V9MA-? z&8t`ZA_F?pluIWnPKD0V7O5xw>Q1a6z|JrbPp%StLbyLa;#uz%=B@Wb0Z5Y{7q6?W{EXS19qfa9M{Zi-zIfXl6 zLuK;glfu3Pr0QWm{~qbJ-hR7tXZh?+Vgh(9?Q3|O{fiw^&ZY*A_Qn8Ok?C17W66#8 z_f0CFC>80gG7!lTaIIRc&$(^3$hJ3mWcEkx&=l#%?s!cxG&Z+AHt9Vb`%R;+{o%+4 z8ZbeSQmA+pIJLw&D3%Nkx`)LG9_@D)D>^SM89FD$sIMP(j*SR+d`I`T_5VuAnhdoB z(k`W$AkuMFdNFbq_c<16*4rqd0N1Aw zZ^h*Uh$^JIULVuSCv*_z+@Il(lNUGp)hCP+6IN%djy9X8GxCy?)!fQfRks$6_W{4w zit6Rzw?Uhc2#{4f_y&1$A=Vc#M&TeKTj*V(DQ4RlAO74{h1AcyU=S!SY6x9GSp(l9h40R|2E-ZExd%vCds8BpDw(VA zzgJT^-?;djbSU^39I!YA!cX#a+Gpv8#w^xS)<&}Z959@QZz+CJ$(#1+r>l8*J!z)W zXn4h#4HO7jK)!IawslHr9RZfytHN0dX@|WGR*T)05i0TV`mskME9nT^{mfM}z_+w$ zPgGkRGZ?kcKVw4Ym}VcokT2vsJT{_)G$GF%7*Xv%)M7dAWP1R1*F`xuV@RNIa&5lr zSV(V)m!09n8?6eVDZbP#1)e^i!$12J|0N`N-8iOPy0v$&a%iMq%}6tG^_`;Eu?XcF zyG>6z>){@SzkTu84BSVZuXY!-e1h1b%bM9SoXytK5UU4GcUJ2A1KaRbsC_;B)TU15+mKnfZI}^Q9eU1H1PQi^$}IXelsh8x#R#; zqc~4K7wsJ5Kzw2)f3sA1EKO^jZB8gZhxH}P8kbFHX8C%1XtgtZ+th|?OsWrl6UcTK zj7Pc?$&;0udtlh|cG zW;2tIeLy6){M6a6AZcS#BI(LQwwA1zar*i$wgb1IjNv3LCHSg37~CVjLT z1mB2%>j1#c-P*ty#GAUTg!$)nCsKT)HS3`VFPx8;CiPDfX5$zfDE?n|| zy^DXl_TYX1r-|Cav8hyP{^|-_Js=ze38BV@QaK+nKcO6%mjB2&djZ60z$;2uMQ!KF z@xM%dPcTESvTY?=d@ysmQhYLXTYLqSDf>uJrhu9konvwPAs+iFl0t zk>0@WNDLhieFJs7ymONo`Pfjus7h zrPQ}8Ctv#w^Cb&l0&BAztm?l(aKWTm zkHO7&y?loxi(-snRw`iopmvuQI10rE$UggYdYbe#sZ?#fDitm8AblCNh)_H}`` z2^HMV6R(QtB+``wl!jz8JYs2I=YuZe?0gp|`&pNf=%4a6N4jcBIGjRZbM-+5{eBC6YLEWH6o9O<^jXAcYxqc#zvJ44Jc{+V_;r{$Wn_rb1G~# zu>6Z(8So$d+#k2_1dz__h{)>zM>!LM0}7AbbWL2Rmii^A$;tnpFK+IIDB6l#5`00{d*6Og4%*yE8{WfAc0EWbL^C2$fgOZ zuv2{0pVD(P0C@jiRL~8eh6NXRCnO7_f#Ch{U-14DsAv`=0s6)&&|hv+oJh$1fc~u2 z5Ae0cB6qWsMvjfNLF)vZq^42nS5VkNkY1xU8Y~P<4@zxp3DaqA3}Q^Yc1_m6;Y_m) z$>kd$xxDL{x^ws(1?jI|a`pBR)9SKY&T8$c=}v?TCTJ_;c@kpU!%lxhW;C`@@XQ>J zI3(~hJ+tj3Be3AEk)1O?p~M59Gp2?4a!68NHY^*I zVal`;h>DxgH`e-_q4xh_YkB*Pa)B;BvrJaQb@#os5%$B~m3|)P;H)oAj)2JMz5d;y25Iv*C0y#)+hhZz3%@a zf_NGZiN64q>@4BF_Yp8e_wmDDbW5Xkse+TG6z^~KIgm-6LuVrK-=0YYeiyzzH(VhC z149o0J1mL+k`Ma+k`F!@8Pe87eK}+VI2CswO)BAepyxk-6bc%Q4zP*hUU&DVK_Hdx zKdaJzp-}xkdtCOv%|jgjWy04+#q25D4HY1XYL7aPu0isTKq_lTtGopX* z;Mf0u;ok(E|7(vLFoTG0hJ;}gHG7Z}=hG;>>8rQT&*e8EGAM6H72V>4$ZwZW&jZ&G zxxeENxgU=iR9H9Y8|@5%PsC3W`GfZ^oItgCo0#}7-VfO1An6YP}Z3hBpAjwr_ zkjzay*mVZUaC3bSKkye=2)Xp0f+E_3=Nqz)??0`n3TDXKqvY?s{0~+%#H{NG8x=b28g zPYf)kyPDa|nn(Lf+#L^FZItHX@nA(*Dr$@7pUYpT3}u^CohU?%ba(x4Y%9*&byUA^ z)J!%H5jP%yN_L&ttZr^xU@svVr(d|F-SS@AxcGPHCG~&fTcP}Dx)xX8=TPq#q0WH% z#xEi+9%IK={3_($xR(R?y}@CrZ5J>AE7bK5ne`e#Dd~YWzy24Q^#ZU1>Wnda^FL6n zT|%p^`V~Vrf_{`}b2^tkQQ|a6{q@AF$#o8+Zbcbc_AI$xQ3X!VBtvDQ&Y(Vc3{m0G z-Igr=m6vvoI1Fh(jvH@+Aq)A+RX_|m4MdTJZBKxw2Y!P(HKJncgv{6m!sAL84k9)I z*!r`J?*QodDWMdJjp={jxuY1N6t))+t|?c(eICoI`P8@jH{}_i$X!`8PR5G-DwR*| zZI*z4^E@g*JYGWQ*!bV7w|^&y7oZzgq(?ww2w9ysr^O)B_VSp`d1ntuhXeAb(vkU- z*HW*53<17MU<8DQRG4(|=OpbI-fA7d3nPYA=ZY`z!r;nHNgc(5v;bHB&X%YpVX`{U z0s%{Dkgpze`GZBM;JlJ^iML^JB9h%BymY%IR^zSSsy+QvhhL6uyO`j{rC1ik4!|kY z)gBY!c4m(=45fL0X!&TMwikcf8cWG70;~^0_7uK||0d1rkC{dC-ZgxYHXup*AZTT% z=rEhCfEYU7##w+iR#~~-$c5~9maw$nWKRU!v)xXmGJMWHt2rE0PQ2n%h-~6 zN>Dw#p7#%h%eba1u&66lUI=Eg3mEbSqlp__PNcp@`}%2$cek^d9!3|=czr{qvzTpJ zbp_rEf!8F58bE!FLUDExckrl`N#x43Q~*6KDheBw4dk8G7ofE3TyZuB1Jvyh4#KDI zfyfNdrr0Rr6)+42Gv;Yx_k3q*O_kyVzri*;YAUCTh78E+FYQ zy;JLQr^eQ5@E2MwEzY%E*mVKTA6ntBtKBhHT zb6-wTlZnR_9%s14Udx<25Lp9_{4Kk!8MD12M4lxxv-*Tm`peP;fDhghx;xhv!AKiR zwvwFEeGS~p1y?2@OQfWK`J9XiYCe<@&N)uDL9m>(v+aK%jWnY{)(ZVMW+! z!tmn{J4ruUd+BXrhKsC&;IG)?s+jYDzIO8DVwEW{r9ODhcuX5d`Lx`moqwR% zCKikkljXNqEjfL=#CfuCffX9|3ym-W@-oJm_b3p<03G{;(xiA zeg2(t%g>FEuY>J@+wPT!#}x@uNfXEO78mC|JD~LkT^PIxAP}YSzxm}JMazy|1d{0! za0LTSaG2-6szd`oFat|s&BMGf5hY}w1Aoamp!)z@G8GLHW)fNgi6imqVm0rL9z!?F2vsP;K=J;MUw;Nl7y}2W;R1)Pn!L8FKFXR==yW zn35cb4eJR5Ep;H>Y!R442<(hr<;;FmQPW`xud*MqK2;q;`_OGBm8^ymjM}Dt2 z6yRHUrZY~icMH64pisP0S-uy7D}ZdUIbN4=E9KvlNYYIWgUA7$H0R3Ce}7SsTow>$ z#0Jc@lQc$tm@oD)tMAs}`pg~-<4Py^s6n=uPdv|WC%W}+?he4kW>{G@p#}NXv!!$< zzq5t>`xhlqLo`nqIlp6VVs?JwE0x$Gd=dMADkx^2g4A3Tj7D!~s`&4q{NMh3v~1ua zcfR$oZx8~v4~S|E)N5hmVf`iZY{9n%l#tfva)HTmjX#v#u@itTHV-Uwc zB>sR2^fQhl8leKt{Gaa`C4e5U@cdPX609ei()=;mq=6lAl;NP?bms2$S7>krvQl1C zn$#L;Aklt;5+ZxU^xE0SVf_9$K=r=PqY)g;Cd=I@y9{WMJ+YMzyG`Kuo1hFDH0gm_ z=U|;geT{6`Z-4HN%fEphuC7CciyS9Zci9O7KX;ea>)-Em3oNA`P&fhDbOIxcHjxc{ ziyp6Z>I(qnxq$YW&0ZWjm`$OUxm5G*W{?|xvehU77NSlsQ`2RtB0#(PPL^h`P^{BQ zE&yvemZu(`4y3PtW30NcwX{5iLghGl=_@=B{A?a7o99=zFV%PvGEN_0bAC83e}M5+ z-38dHk;%FpgEw=i`3$%6jP1$`X{uAL!nMJPXq>GoyXqYG_}z+SAa_nIyQ!SBt^gJ} z0T-RA;j{6tRmn%s8xGpfeoU4Q)Q=C8AqA$48s@|Ao**6rq@>my?9v&O6o(i>D6yl1 zEE8xSwithTRb!`bN0F{ZyTD_2Gc+?WV{y{2-4xsWB>&cgo0KEUZ2v$#DnGK8`1P?C z__;jCncH3e6@FlFoaOBBc%azC8HqC6kOs-cM#q;TK^r1CLArk6hnZYcih zvN(Tm{Hs2&mJ5JZX$%uUL^&8M6l#z4k})K~vbU;he3;GDP$jusuy zGY>84L7a!EuypXZV@X+CHDCR_H^Btv?40$t{`?X|gFc;_Ar(6deEQe;)TsqJ@Z~P> zLLC2Ju-9KZ;GZY=e>AZBYl#E)j1V~KX+0a$Vc@jFHk@Cu<%R&=3+;bv9sV3};Mfoq z0?u$BFwb0YlSlmTQu~vF9g*JI#T)7_N$C6L&prS73c@eI+QNKeo>2{0JTNkQ?wIEg z_+Nca^7-em08dB;52Oe*@BV|1JCWp{&*cIP|0>v^@4};Qeg!+F`a_IwY7m6|um4|u z>`6K=|5t$LpN9m!jbCJik z@Pi;1u3#wjO@vb4|HWTEGYf}2$bQQ*&waY70+az#|04s0Osmga+2jI03CbL1M&sNn zJp5vIl8?*hxb6kCOx%aI)fllzSXM0bGjVF)e6WzSbN8shTn4a+d$fP``{k10Q(OB% z98%KT+|OX&yJy5gtn!kjCz+9wjP6!wrO-RR7V4l2G{9(tuBCDE8yeVhCz>3Q}lem~u_v~AEs)#Ld}lB&mpKzf7FyU!mu zx52J+7}8-K-Tqu!JqH_WCPsde`oTDLx1}x$H(ciZ<{(8wmPwY0klL6`)K_vmdGIzW`xSEtLEB0nDw@HAYX?%e}* z``TQLRgY&UkZ@IC_E#^+fWm;6o(2#Zo4_^>_6g)ZXX6Z=6Cll4)w3_&cF0=0p1QBK zl6nWs2Z9VB3KWFw&V>&=&xC5bIikS+P*#6Xw7Tm{kF|-eaNbM!St}S7v9u-l{`2#v zP-2Dg>>Obe$O84Q>)LJ2hLq;C1#h(puvXEn$19xz!{p%gxu zoz%_|kEyVGWTZCF+hU!z^+H*#6H49ei=rNKK_Bz-{C51ekGbUvR6zm2XAiq@AM(>2 zYPD&~9rN4Ipn~hO^v}URUNhRsfVRM$|2?cMqV>+~Y?#WQ*ZWnR+Mq8h?D^ zSRIVRj=oHaEC582O!?z&86AMf$lTNQRtFh7+=qN+HxoL{0yF_L;}1;ySMW!^h!aXY zkfC;l+s&(POaVg>+N z8=esL3fOi9WabyqWFRHpeWi%{JdkSklXG8%>h{0ZCMd4spyIlATFwHTk=Lj zz6vfn|8|Q2ZN`RxGU0y*%Aj@(_skczTiQ(_0f{2M4S*>)Q*MF?0@4$#-#nOe(Ie^! zlz3qLYvRF5qw?E^Ku7n-FTbXUChMyPW4YP9KMQ5k?n3+*fcw7@nVUbeHi`|>Q;44qnN^ukzl1Uf*7SraHwxinF9*<_m+ygw2)MZDx!#fpgZN<~%{e8{ z*eaK0N)vi!QX&PNFiG%#9`yF-t&|&W?(~_^_mzGYJm#Cvx#+Wa#>lz@#YW!aY5%*T z7jf)lR*3uZav-1cG&j$4f$}+0+p=h5Xo+?F(-sF9*+9yc)DK6omH8Kf zGVD*0ilk_0XkyZjk^_SAl7vW;$|IA@&;AQ|ktBqdB!Bf9eUj{k^JtCpENA(0-d!6V zw!1b}b9Rm@Z&%aX9okAQ5PMO3D{g35lZU#vKMe^?aa|)hP2#2Y;9QRLo(|aCxiU=S zl;yPH#A3oLEoqW4(-`mYF2f`ThuEQya^-T zDrL3<7!Gv|L4rTgWSj%V%e|UH7#YRGSc&K!Z#~U+>`NB=$%lK4jg6d$CpK3TL2+Kq z+7IV4Hut}TI#~QD+P-jGCspeB{XyZ$oJ{9&wwbfn)%B6~V0wGs3$g8d^`Z2KxF~J3 zr@8kX7Aeq}#v@;2{MY;X)HyGfBx z^nXuDkq`*+U)^2UO{xl#EN0^iRMNsT+W?p#<>Jfq*EN3>$dL-d#?MwvBZ)fX00DJ*{f72&YEdedLUG` zt+%1EsfFD7q0VJ?M3*8J6JKAT`H_;3oAlln4bU&%3;(Z-{%%fiXjSQ!j z{rDqT%g0T_%UK?nrj&M=EZ+34FEC$Jb86_CYTQN1dy)Y*9RA}xGP?1{Xm1e2WaWD% z#l7tRq8u5#8w;6&s-btYh$r|4j-gz>L)`7DPpF)m($ep zGpZEOcEJSa1q6?^o}d2x%y~q6%>H$RRP5e#-sShBpj{$w~T4EKgP#>I;XPp10jhbw6_()dLKRoBK= zM-zue;w_XN{nt@N*9Ruu#V6WUc;Y9`pH`dn6u6E)WEWFe_RNfSRCn?M-H*lg=_gGe zlQY-B+)1dea@j?RUS>CS9Jv@zS}#%MPrRVrR2jh-I8E1Z@qXLqVJVmP zxmBGef@8l6a{;FVQGD=nD9TjF0(9S>RR$g1(VHZ2gc^nF26efv$fxTW$}C?2IGU^G{XOrn zsw3=4;iagy5uW)qsbgEEOH+Y#p1TZI*`A%71aX6$_X{Nwv&%VY#e*9#E8wUhP4c}7 zh@=mai-~rbDF-HzE>sWAwU&8G$Tpqvi|XJ-a;b15dU$cqX@S?YFRUtf2+YUyTsM;F zek=L3i2T)#fVR(78dWs&f%MqD=HY`az_im3Tw2IhFs34wh^_;s|$VU7}kbE+K7{p@U_VilAPg3 zBMOY@P4Y~MsuW_RX)40m@?R3jySwDQ3(9ZUO#})k3Q7E=W+AIwS=%Rn9fOW^G^RA@ z(^3UTdS4-{!))}~(MkL}S|3dq|A{uUY!TWvH7`6fYgsk-(Ddhg;p(XC5%bp0qHG^Bgo$ z-7)1-U{$9`@^unezA}lb{%-hr6gOLoH93FUaN|8a3gK$pRarG`{Ec$S!#=!z@uH$Z z>@d}(N$V`mSTr?gE#s^qMe%D{wZ9v9AWET=Ky+k*zdfd z|Bjj)28wk{|Bblor1vsX1+aBTuVGXPkR-j_NMvWcOxM`#TCu-5I7Nia!1+#&XR;6D z#|YQCz#kh~A94Rx7})SuM$IvHjL=E;)BV?mcVFgi4n6M0HxjG&rQ9nW`8t*Q=?OT* zx7|v4+KceDf(QLgGhjgRqfTT4}jtbgvvs;&0wg}m~h$%R#kF`Ana-2+vkx!=|O5M zKf>sYt*Uq`6vH%!T-mEO6tmS+5X1tTd6CDGjh*hkts>5%{oc2z_p^7COWLGQ=_NyZ z-~;pcQ`;t=J7iwM;I*Fkg^@H^F}Kp6hH#gduEL5|6yOWJzY{ak%3P}Z`tpw*X2R5E z*2mbSdm7!C0utxfCqXuqU&C(5*ZgDn+rp3K$r?3C6y|rF^ zVB>x#B;cMNK`NT#!!K;wAP>6I#Z2|JF~~X18nGh0*Ty%}1UQ%4vC~1dg{fAg#;(T+m*$G0|{dGbm9YtW$lN3l`_%TBpJ^!-iLlCSf{Y5jon{h zS?tq|^Um8Jb4ISyd?aSk-aDq68X+lDoqWXjf^S{otL%UvO(X@K(dbw7N{1tMynG|V z_s9LJu)CyjZef#J{^M-53S&Cu^r6_kY*Rf$m7_T-uBS4yZnd%NUn5~Q1RWy<4 z5%(hC9SOi$%+p>QY<^`H>ZAJ^u@cXhMS(0z+>=??(c`a6nAtMhSzJqdS(c~#@I|%F zt>@QflOfKsOwHFV=Fv`(qokbqr|Q1?^?-<)64~WPz;08a8M@?Qe;Z?OAdy5so5;w ztyPDW+1#W$kqC@)LjbO{(>Z+_f8K(akrwNS$ShaUJFj!qc;3KzQ8v>K6-)L zs!K@EC|y4P!C-L}-M~J-{SoGIvEu{HK^BUuC10?A+%hAj^E!;#F?cYKb9J(O^oQQY z_hc@!WjgcBF(sl~A^rW_7k8{Kx0OqwAC8n5(@rvqx-hho=_3vUxreZT0t zyZ!^gJRYe-O4|}~;3y0 za@E8qAsKSQbE_1E5|QHz%b#OA#aBKcoKI2S)O~cHF$kk}B{-%C^4vF@tnvuw(A57{ zq1Bn!QQnSV`{4TcP>*JS!%LS+HiW)vfdc&q4CU6CthmYu#)`JWDdf3F{ifIzZP3GicZ>7TOtc(!^T9S${5uq2X~dt4^~y~^+7EL z@nvNe|E=VSAV_b*Lzu_OrhcegU6;I$`j*x3Gk?1$$CSt-#Ul-hsr$)OS+zNsp2$l1 zeZAwA{;_T2QmaVCbd2w{Owwt=);3e&5ru{cHa{EPGI&K}6#3QYAKn_LLat&AD}Ye= zh|%f8wvugF*hNO)*dCM(`@N-joAhYD!@E`KawvPNVmQgANd1MN2*Y7 zimIs{zaGnymLTVIyxcwa%#|0}w1L#^OkC(j31k{S?0vEOv274`eYDbn7}!~LnNjkF zw3Lb1WgN4Fq4A1=4Ju-b=9nuo%kud}u@Hr0%5Z__3-;7$t-0gH5Q9P~D`oM|H_->v zqc^YL)$TwLUC9v40Q2SQkYbva*8|C?W|JhtE`s^I!WXS74PpM#NZMYu(}$OI7AZEs zPMRh{tocMfs3k5nr*_X+0vT2T;@B>KZfdxFZe;~iqB`tSzSc3he1_r=9X)|wb_%r$ zq)7c}z=fhbz1fW^MicUn@mR=7nfVxd4TqCqyO!8WJdrC@IJxAxVvREN>rnme*JLr% zMZ_Yz)3jq+<7kT)-YCNqUbn**WVy+IzveCL+t*1;AzfO`bi{xIY(1zc2lZ zje8?%xu*UQ%>^(vyMwAwZ(#JqWj@P=Dyn38GHhgT^98qaBl(I#>XV>8`#CjqD=igY zpFLS2W2s|g(7`I<&TIQu=#}2@7+6VZPZiY{HPQkWEc-PGdM5OBAA^r1EFx)g=K zg0%%YE+r%U)f1-E!?kkH9P4J)!>O{t+|!#R(fP2dR*b6Us-><(+G*Xxqz&2Iz{75N z6hT4ojMFxP&w@>X<&_uQ?O;CI!2_4Fujs62ymMP`+d{%Tv&EarwSQWEoHQ}X5w9ts#RxHybnRNK8byCUXxNV_) z2A);*k_%acAzIA=^2Hps+}7JA#+*TG_?~-kUc-?>4q@kF^LrJ#t~rGQCSpN4&jg2s3O`I1rAYO3GO<{^P~0Blq)_0C#$t;Y!1pkf?iptfaT|c5NB9 z6prxrWi^(8#rtU>6jsGy-0}ovESO__)m_q2w@7#7*&1c&_TidyT!=Zb`HB*CN^4A3 zotep~Ydd;#u!fF4eV&A>b!K~P{sq8z$?G@Tni@gu31+F6nb0vbC=u^FZ5ro zN#c%M0c|8V@tF*Lfl_Zpmg+`AfduK1l$;%nqu|^@_DXR7Dlfj8qI@E`dv&SbQ1F!B zt-yfNtTSJ8h>hMdrdLRpkY4;fsl0yTv(m06MiV1)0c)6|^C5+L*v&6eIhPerEa*>v ztQgR+*VE;+oCVrAiWvqZ)>jo*`Z;ou(XlCtPXP$J9y z4hl&ZT+EB;-jsW;5};*Ec&v(NsecsqBGdy;M7zuVVOC26*T>}2Jxy#@I=}U*5UqD7 zOJyF9p2Bx)SkI))p+~Vqy${R@^=w}sSM$~(|kZ+ zN)2LzAs6_9AoT!v$(@Ve%fB`nH;*1`&uNQQZpj3CCS^yUk9XG83mpiej9m@NBLLBb zV4cae1I6YbM zsmFPBw{GhrNm7UO2-n%85c4wpskcmP+&AAPSPsbKgcJZUJ++pB^WHZDZ_Kt2&no)l z3`wwTg9Yo^Rv=v54B3HSpgTOcP{!XLaXm4_pNhxYFk0jtN3nk2#bV=8?83`BYtqj- zzk9PAj%F~EK1k0|M$S*rUOmc7JC=*gLO5OA&MU5Z=C#Q!`jxRu5q(Rur(`0Bh;{Ec z6LIVdfSfmbZ6pqysN7P!v>k& zxnQC`gO;u!tV&8yco+byrNaSWj_pL`O(Lst3w-yv_E(<3-^1gj@7+D7FtYLF>*VN; zdT$C%^cTa(1A#wg*5`r1>#(Yyvp;+UtU~aEvnMN0tN5MvChIQArN5v+s2|s8(lL+o zc=W~2dRPeC5@CTEsYrgXQ^0_j-SHJdsE|YbI~S>ryjIlS<(T1y_rpOT#@L&;z>kvu z8h~u?^gI|4dYPi4_t@?;Kz>}%M61g&vsCjbBRLH@deWw;=!NC<8#*GI? z>K>l>v={Xv<5BRQGGHUyq<2#HC&=;Dp8R@9E(YamL;ifWhi3;7sQ>>QM0j1_8F*GO zaH(9rKe@C(5g7QK@)RG1b#P*1sy?N_I|O`d@aJW$;EqynM~c&}6~zvPhRbP(nb(AeAC;OEEqRPIKCw9@|mee?cxwPOVaoJ-K`xKuOA!C7pt*3tSa87+f3cp(0iO` zaTY@bT?}|U0M-->6^kGK&gNXlFGcd+#wG2e0uvPbcLeM<(NuYMtM?9Z!@DSdMNhhM zb+lT>6R-+`b)(g6jzF$G-?z}~RR3!U)L{5gzs(BwB)WC&X7HEySp#{m52BcruI3EI z6}_c{ovaMmd^PNS!}`hF^!O1vMkp7mR4o7F75$xpavp$^*coTYURk?^qOR7}T&ep0 zFw%LX76VJ5L;K1+56O~34ld{ph3!Mw>+qAiR#oQeinTjSu1xB=EcUxAzM!t0s@(`` z@aNzg^*iP7%Tn?XI{rW{HL60A^L$fmd~$29ZK`tb&Ex0lN6Cw<@&#FNz3wj;B|9~(e49@$TwAo zY;Q8{2a6WLS%wHD*bRmD3yV;7Ed220_t(qs70*De(2k0?hA)dd*EAoI(k=>=j(&b% z_rtk6wZ&4x@1g7CAM*_}J&oNB+jdW#;(6uPlNA5 zed1Bo>oTyIJ+r3+RQLJHcXJUvV?_=@*OmSgr1+xKsbOMIOU zSqh!-wbzhV+Bw|MpWB5GV^+9!)iq=2ZDlQ3Y%ic=Z5{&9P{Qx}Nl{^e-M~X|3<=PK zcr1m}Zd@J$7A^$Z5nzIb!O?Ae_fuHCd#l1A=*D&`l{RCodUG8W=AuDI7x-g)*EN?= zM6Z+6c9Rjsd8et6gxlQz+N?wvs~5Yuvny$cfyPRTB8RSUb7O)E`RVrzs1+6{B`?kWy6-X@$})MM3Rh^bJp;3t=Cq}s0O=}tvdRT=yKg_t0R>j z35(k9!R&e`lLMlu!SV;B_xRJkLvD(>~uOpr$6-P zcBD&*-DfT%UjF@iR{j~sV%2NiiBv^QW{xMr?oC$DuvQ+tb=l-$FCU`!LaOF~%3d}( zVH5R~^~#AU7v`y8tRdl8-LF_ko3+s=0Y@Mu??5{m=U=buudJ=dSLEC` zClUs1>)hj^)U`&D~@v_Qt#$d-#nt#h;wkaOn(TrXWp+KAOv8LfBsr2}(KlSQ-gx6h#1B+PH^)Gk84c%FZ+X zHiLG)o3Z`&%bWR777Hb^OK8H>{;!iKPgSg?hhXUvVkdOt#kyUjOT}OCovlR!?_ojU zx|Km62LAe%<7W&oU+%|lHcv;T1v?H8VsIhm*n1hd-_kzFS<&k5%Y$|V$OEtIN3+W) z2WHq-4TM2cb|19w((>ie$q%*rQB)H)MQyauEP;HltoH zJIV@6cZ~R#X6jl4LoV&;jU>s~YY-MMg>Gp)CRtv^d}77H3*RPDlsoKBU!(WBL=ibJ0Dygp z;9cO<^9}SW*Udiq1>+t!9FQvG4bjkEjMv@xen9V9f^qO@r6g?k6A3pq=w8OTl`mIJcFYB@=(p2Q?loWEL@It%duMD z5dF&B;DUm}gMezF$H`&dj=#r}m#72_5Z_Ft8Uq198>32+JnYbP!m)1MxW&c|6G@PC z-*xlxSd|5jR_ku5wS>h%&-G`~_b7BiY}p&5dDiO`czUlK{ama20p<-BvO9`F1cyRW zdHYjPMJ!raqq#mg-1L9 zyt5PrL@2`F6lMUi?tEUliUL-c zPs{eWV;=!XC{#laj%FsXR9TNy)b;Dc@XljiqZf#q_{ud#G^v*;?5;S2hyUZ)Ja+Pcx4B?gM_Qq!hRY z9a%IL;fmNo#kuZUhYfB6T?!6Kf`!V>Y?0B4f{8Eht;j!^48*UE4G5b3_(hC*!tp1B zA1A{hjMgW0>#8qDIMKlX32X{gL7$C-bK&+Mb@iAm2gU=Y2tzk-7VHqyBiaDvFT{ zvu?G|bK)cPr#hFAhFvWN}c*m5((W2b%gSPM! zDSruQ-yd=3AjfERX{5;bc-{UI`KE;ZdOUSc_2ac9lO*LZaz3rm;8-M3`_~MP#H}oB zgR|PRYo|>`t93BV^x6SY{=XGjICm61u0@i%$LkLvmY?n1C%M22*#$v)Nq+97Od~>P zSpvna@7i&eH-wXk1XGKPpImzbVh>Ayz!JTW9u(!?i|{xR6zGVQx@V0{X>nsh3KKuM zWkASNCJFsT)CVrJxlPx9b)9`8v?{ppqP9B*JwfUg=J`MxVJxJON#zC4z_Nx`Z7|Z z$-L))JpC1PvR`5Qz`8pcrv1Dq38sqZ%H9seTA^CW)=~qKj2FtH0eu^Kk!cXDp2?3V zLRfjiFZ!n#ict?`t^~k^P%}m?25qtT%*4|F^_hHfid#49e;19>D_#OcLrtr5 z`Ij_D*AGn+J}+P}l#B?G#pm-F+K^VxzUMgpULk@W`jvn4Ro^V;kaRyBFKj;I-c)@f z+Pzf54#XlnnDr4GBAXkUm^;YwZu2o6_}`=tqfueP7k=Dhmc4r1x;`r1WNWS6#$#rX zZg@d~`Xw=t2V#Pmdk6VJI-dvVR7A(dB2Nu9%5xKhseJF!&Io6|MCVcO!8D3X&1ZSB z_y8mXU^JY#Vayfdc<;u_T($OhMdc8ddAw-lWJ+~kiOG7Jjj}R zP;$d+rn^A$=*6Qs+lRGJ`EIEbymz=$AI&in4D`~+XIY`hwwF80$&06;c8{4^!IuIG zcB7u1@1N1w4iup>MeR7IQ^8sJiyWqu9LbYh-NCs^KfYv@f{rylK&JwgmX}`3? z@Ec*cy&}u(&0K#T>(C<~jyRzc1_M9l{U2s7|MH{7*+H!N=EPQXA9>e%fTFkna1o2< zfrmyB|9PnYHLJ`EIgHSMlWOsS=v0qIaLt869!Z`4<1${~VJACTDK8lAO?M2soD{B7 zX6bXctt(A!<1${FT!U+=Js65zhc3^%_m5PUlDlMfTv!z>J`qD0i!tHFtS>T-6kJ0x zFB~-#YtVj8#9AaKr!1pj)9Z+Z3NRJzodrj1VntNSI8UDpS|Ug~?%)EC4U;7e zw@eWBzw93i>VqCpo5%1WTajqMN8k12e+?R`IK!-97dN2f^OJpiQtym)Ltj`!D zHe`K1hElJbG{z`w909c@uj70Kiq12y)-vk_Hv+ax;Fz438))`c1}UF1&ZSMWv&p%U zyh|z+f9{=CodPf6O0eD*|4J{BbUhjX0ZbK1c+RGr#gFckO80d#06A_#8}}(161%}P zxehl*Efhb1>SDkR@2$}Xq;AgAEHaq|ya+^UN145@zbXEyodbbMKTZP)M%(K(_`Mxi z0$I@5Xpi&|43w3FE_9y1@uvnxnzX@KKU*>O-qRg>LLjFKJ6Xlj z=os>i6=wjP8s-kT?9757f@d}HhMA>*pdh@9ku{t5@-{d}&~w-fe_J>^$(HGcK>hPC z0sqH%lfK;HWe_o|aMB`fu2fI#I07ZE3peaj{+MI8_s_HgJzk;`M9HHC2o#YB#MB=s z0u$Ck-yEND3suS2+@KqS)}w)TSg)JwJ|!XOs4X@t3i$nq_*&CKPJ#D>i-V&K~El6xXi3Z3?K6SH_vhQZG9zuCi3jRfTn)ZfT z{|IH+$W24LHa@iFgCIaf;J3PPH-B7vMh~*z@MTBoxPy?0bL&@H8!gQam=zcn`<*6Q zVmm8>oUmAN6sX~q6Ww#>>*J)d1~PUAzUOUhm0H3h3zqx5y=egDQj7C@{MQr-3)+bo zK0hc6IP6(X1i!#4KD&0mak8`U=FWHEme0NR3;fB2N2^c3cq}FI{ZxkoV-3-LOvIJU zhOoWhriO0AV^`TU{um%H9eu(vUj$jlMd^P*{ccui=!IFuv4h_Bnv17kmwVH!$!Xg) zKap-RAN&$hljpvE&6}akT>~RPDpT1TN7b3rMFQgH?+*g4(lpTo0XGkT4z7UiZ#uZg z&VM6{O?w5gva(;+YKDx?SHP0Oq!7GnOAG~EWs;ji!SCKt21l$KmeTw3M_ADIB!C8y zH1!-)0^z4;GModq?d}_IoZalxhc_GM#Y`OdHT!Ae#-9vB!p`I`Oz;4c6d-X_@3DA6 zEWJHO?#R1w3+{N@czP%k->9qRJ1|X0xvZc3c5OcEYVE6`7k%A zh3N1pAA$r?vrp-f2{q?V6~v)eY1h7g?bLX5iot#S^WBas+Jsjf6eGQ4a6<>b4fqt@ zgXc-?<#bwqQ*((Aod1iDD9J_WnP+)>v;&Di$P^`uA`1kiH!*IBSaT(5Yv(Q?xFE_s_(j z7ifAbtjN_U*zOY_WM4BM>$8SgNacgWRqpHJ|K0B-<<+3(0Hwe!5ZiIv{GiZyAVmo{ z|LmtjhV-NrfvC3BDokm?1d`EhRB`A%tp)&OIH;x<1y8C%j&ZpM|5tAhXz{TSP;)Ap zGHaglo(8l2&jdkT~yuAaMzze+(~t zDeoWt?jVPJZQ7k^M9ogYAx8_HxtBigO z_;!!15(tZR6-W7asfm!JhvrZIaRb>0)nYckYfX7Ip!S;P{rkc!UaR3h*`CUp|2Q1& zGP@&f{wh;lCZ2Epnr*q>2X{JwlESbCJr2!}A1&3E78IQGBE@&GZ z3!&#o-?M_O;y+?dDQrxk+AsFtTL9I;t;=mu7@;7ixlR#-E=9IICYjtszu_bP@xIDO zmW}gd8BU=_r_Y}O&2uc0J@rBMR1X;E>GMqF@Rc|P6nJ4o4T8>$|Li(`>u8ry+KpnW z4Oa_B+!e6cTuWFQ$f+7h&cF1bqv7WZtMRgQjih5&V^$s=GiH?DExXI3utWX*2C1#d z^NUyHd~g_{%^I~oK$0j8EGki>Nay9tkdGi|Y13Qe4K0mCoRqkTncg@LpU)x+ts2B? zQ_}Gx>AV8BB^_>II2di1XZ^zR+Y291_ia#4-w&`DSAj+@0mc{(v6mc`B9{&ae_Nv) zwJ_JEZ6GIg)kZuh0I1EiyuI|A$hv|RpBp<|=1YD@kNwL>C;PL3o=25CLKcEBAdB}g zG3Z6a9LNvuBq~6D_2h%RkLDV1Oq1WQ<~K@0)=YELZ{anna=$c7pap~v^s^aeCGPz*9p5NAd~(){95GGjx6Le9?Sgye*n z*AjuLjJMK*jg?KdYu@WBXPG5wpjDLG0{gaD;0irL_UlYb_w>AX892XrYvIc)$(5k; zg%j21UZ{O+J#Df~e%JQ~ZMUML~EdN%QfXC)Y^(k>K7H9VC}-dUaE(6da@vwEEWWrN^RKhm5#DSgZ_xcBU-qZ zc9#<=tz>Wf%i)nu;Dp+-9(Vj;8kTQ7X#yl$jy8wIe%?6HTYbf6T@JorwREQ4Uekot#F!7s-=O96&;;pW`!g$Bx z*D21^$WzvMruqvJ$$gMn2-`?86%~~M|BA`_TZ^#cSHDgsZ~#Zt>3N@B0kuy4z2+yM z5N@dec9oGtiLSw7x8!_3M`6;X3 zWBV&vn-+E&UrVEIA|%}nkaV{$Ahop>$-gZ$djlL=8i_MX(p#MElVp>*bZbsZc)(`x zWxMfGwne%43VUz5*S+0Pa5Bt%^H>;SxcW6+sgIUHU%B*6EZZ9BS*in;X`ktC_mepE zoZl5rz1u6=6U}ylz1%w>a@nK@+c++y)tUXgDgT{k(H1whK7xUK+3`g6)CGkpN6_wt zs_C#k12=$-`+@FuY!TTh6Hx54 zZAk+(vymQB!5lL)z7$$gah2tZY3iZgLK|tRH$~d)G`QeXoji-ZPexNI}^;>u$=tE2K7YaxvT?7o%F@4|M2 zcef47XIUgHGvqH>YSZIBcvR;a?WYa#$jPXk^e9CF%IsJuvr`USJ&*aL1sFyS$!cmW zqE?T*sDwAnecM)A1EWnUL$*dMW9k(ji3!@>38a{mBw;Pe?m`89B@!;=6p^hj_n6G~ z{}r^gN+W4^mcYC!%GlHKlrT29+%YFe+-+*~%G@EaR!m>*f01WqTD*=)7}}1y8e7tG z&c&lvFL>o}pMI95rf?{+9tf?fbI;vWc^PEB-k0;nT(@D>-RuF;HR7FiI^$;1bW?)4 z^8y5wZ_aadE?B?8RL1rEkWfFWnRM)p`#K$7NQiXIxetOq&uZ78uD@$qTBW~z?H17f z>U9#A-VbjIo_m39PAq6h5+=Xa`Sw-a*2nET)CGpt8@aIho`-zUAFFa%_PhWEt5}(DoKprv5;{wEY_@A>Mir9`xYoo6#vE$1%mza zk0MU#y8_qQUf?RdZLNsfiXeZk+^7dT%uh*I99*?xgaM){0#1y3$r`#4@|MB=l`9L) zT#!3c*>@|Wq!0jzncTGZEZQmds36v)HfENH)aM8uxMBw9jYGgDl zQi))z_u7PSxvq080Dr0x%rwN0uyTM=efU}t;$AwIMb1WpyD||3*QOylQ{%y%_1Tm6 zSWhS|>?F`9p+!X;c$w#97b`P3@utWSQt{70mvX)P~zBv)!D9_2gb<$Q;NrNePmcJ!5fW9?K~ zw#O7)TanDDkv592dY9ISbh*lMwB++fry{s2kJpI0k1J;N<}E{0smO$@78_A_;mVLd zTg#Fg0-V-!Q1h)E0&+3Kf>E+G6_ycV{N3PyptM_X(tpbfiSHo_Oj2~5CNBXgVie~s z2hIyCg`zzm{nlOOmuIYmpFkx89btfw@uR>!K)ifIPGh$?_DRC{XWBCe-YEyB|$Zt)pm4>M{R~#$Gq| zOmy~#>zmzkZV6MugfGuNQr?^`PB-?v#(Dc^nnk{Y(cpgmhJg@thvkBOUk=UijzWt3 zn6R0p?0%>oJPV+N3(>~y(f_lf3oyzvMYBe8A5Q)TcVfSR&uFjaQszAWQX}R-IY6wG zs9NxhZjH_lm1pR`m0*|iQAF?mPmFt&YR%8TSE^E)akUyxjxRbU;^z>KZY(R>gh?dq zVduUhDP$5iCf zKce!%<^sSW@P($xV=q~M%9;38dfls86SfE<^HatZe!JYbXUi3jRdhf`T1=|sACHWt zVV1Pw{TaB-hgcFN#v3`UYr}oimq+RpM8yKuNIa%HXrT-{ElcLmRJr*-yR$-OebFWea%w?X&hq(4zCPu0d&o1=7K zmi_Mn4V?*ZTCUZ+whSOhYt%_ij2dJry@v46KMFd?RR|@80EOC1&Qp25R9w1n-03_A zQfchV;Vp=U5|7pmAsh|vvQ#~7oA}fxE_ys)XXo)lZgyf=D4MFR606PLq!G$B&O`Z8;f+dkd4>o@sujaj4O}c?h z%Ke(Z#pSj|XZ^&DDltjfDJx`N4qP}W`9~c4546M$KZJSVlj0p+=X2TCM$!*Iacf{! zNwUoIh-}pLFk|Y`fjNs#;(K$b_?TcVfSsS)9C&`<^~vbYg){v(5Qanm@A^oGs~ZQv zND5E*#AQj>r2gI*!qxlMyFJE^*5^Ki+LMC+i@@{WAl*rKyZVQTbpHYARLhx4;o55% zE&I!~ICIn+1j;WF23A+&3Np*}`vx4t6-^3KXB-EG&{S2P|%tUZnt7 ziB3b|*pHN}59}d%Z9H4-ks@6kBOXUi7+^18h&xb4F*|eYq`5jkgzTtMDf{J(@$lVU z*#)Ft20a#bbDOyO!m(d*=r)>$WYL2}6jOJ=_cK-0q`O6yespB&>+9ceBobcQCJy8C zVvnFYOYg;|@9n|s#Gvp;le5Cph>dY|rsmnDGlasWs8 zwGz~y6dJnY^-k7$_uE21nSztt-WtOQ!5mF!aE9Oz<>;rFzta}~8>S9QNl-MZ1a|<@ ze^}Q2NdSxjeTb<0MD5F~V<#d{N={gHtR4BjzdbGS;FlMpY5Ky7brF()G~*7-A79jO zxEnBU?T8-ORwa>`!7~LhTsD4YrirxUG(IVn3qy7BBBP;el|(1kLgX&%})z#-g2rQ5*?A=o*-Jo<4 zvmZJ8vMq)Rs{D`RO$KoJIFV{3BPDB(D_rM^Up*B4@XdMa^`yGqit9pt-V2Tya%5|_ zbHyK!5&+K*=d|Hdc&~AtW-31xMC-wPYi|eKVS#P%hsXdz%Z%43ih;D^{yGhhnulD_ zR&9h#pMW$?hT$8zveo$rki{xz+BxHZ@vVh=*HP-7BDCMopG!{iQ?G32EngsK5)MaH zTo`W!%U*13$x8i+gh*JNhw`!I65GzwuM!=H2Mhgqta@Owt&m3kli zA71J4Nd=(C!;07j5c;EJ#v_Ztxmq5N?p4&8dAZUrFRnWtmiMTQ`u^)!;^Mi8Xt(cJ z8q+HFa8ut(#e#Sp&hjbOp^Cd;(6?E+SGqQl8m#}p?vlr32h|0B-5T(maqE;FFYy?m z-B_Bv1MOohLBX`*4jgX?$ZTyEBMANO?6y?7a0PzyCD~hHaY}shv+a-m`OiVQPP9JB0Ih!G5A2?cMzR@8 zPZ1}2Tnd}r&JK`0x_e|#ATsqF+VQrhQFblSap~I6R3^OoCrZLs>2dF*;|KfFz$D0Z zHUFjHlPVby5;9r0Pd)TcDQ#?Ng)TAHUvCRHZ29urZpiWcXfr~?je~uJ0e2U0HLJvh z9NT(d10qreyq`PPvl}mYdp!R&WuNMRXJNY1Aj43#vh8=U_U&>LjkL?*ERzrHd7auB z)_f78ufN*sgZQrN6YnYN2-9PpMpVf~Ag~62Z+8GEDgX(Y@<7%F*W;YLZYCTp)lY41$NN3?;5g*xF zH>>*meEWuQW-RYjTKZ-a`h)DZDlU{RV9W*0is;KcCZo(`y?Cy5X7m>Bc4cn_SkSA2 zerkzot50&m8jyf+CQ*^1g}zXs#?+ukWG9pva4_TPl{wko8j(#2^7wvM>m84nvj=B2 zlB66Z^2Zw@%1#`@g4q#tbYhh-X#@+qAMx*qW$aQzIX;3qGa<6P)IV=31mgNNDJVyV zi#SXc{P$)~^7qXB#5@3~d2{?X)SS}NihLz<;#H`Z-n+-L6^<5|P*2$?Y|lDlqk8wy z8M+T{^P%7Hbnu3wi5`E<)=iRPxA@+Am&o(b7>I&b&VxrR6j;f(Y^%xb0HN5n#1qz$ z#dntf;A?pzO*v}!+UNOs!OIolj5t2*yOO$CkICBAwe4w+jEmXVGEkK^`U9`7ZArOw zQxD0m)`C{ilcmMv#F-lY4whLmbQDe1vPRh;9*)`>Qi`n z%?Gca=?HeS0DApV_F8S;11;6L=<`>!uPsKPJ$h>0mSZ=FtqkU^UNQ zQ~7JWP7L=IGJv63VMgnqiObSzm*eHYF{*&i!{Fq6C}>16P&|tP?U*?oZagVken*2e z3$NkfNuH+)!BK!BTh30BZqzB|AAE2gj_lP z0w(X&h!85Ib+61VOrZrxJ!nYcqh z#9$zBCW4{67L`M6ZCS;|MW#oO+qTuuOXj5}gSEj$z9wZ#yZRh%bp$VX-i`yfO_;@P zdFKb$2gb!2z7-CQlO=`_97C9vP_-)w75hv}4T7ugtC=(~)R2ZySlRF^a9Mf;RX}H^ z44lr%_bxrOdDe9u*&Ol5Oe4UyG}2dK&(#5^r@+mKAzpa&?H&xG*?Az)<#_39b_3Mz zXau&In_dPP_3jgP6SW?%*FxAZUMw-};4MtU>mz9Dk=!HIStYGTC)8J_qI;Rf7v>Z)@h)1(QyaUgUg4yL$z5Nn*tl}9S+xYJA<9Qwi3ooswJ=D{ zyE|5|*GW}3oEN;{{rIG_)DAQytr;&DN-N)($zG9(ADi7mz*4{E8Kmx>!iK0YcLs<5 z^bM|=vg@lYA5+)EPR82kPK<@Y8+LP23wa$IcNco~LGmEGTD)b;Oe>)IGW3?WO17NE)%1v8ppd<`6?Y` zc|U9W=URh70J>?Y+?#7O&*s$bFJF&qJze_cryqTOi`uaW9_q9XbmF6~ySJ{zd1|S~ z3c8hGk37^(m}x>)<~j|C9~c*k@7n(0y~+-X+ndlew>^O?_^X& z?)`JQJgroR&MLosH@Ev8?D#Xu*2c91Z-Os}h&0<6(T2(epuK)|KufAEH|(>-aEr_) z1Hu5B$bQdjk)Xk!ly^2PCbdz!R~L9w)~*0fdgyFddC4T#eXo=nAc)$>7+9~}u6P;m zSqgT_Q@g{#!*#fiJh?Ul*_`9P8^zJjKS|O7hmHhDN#^94UwZ*k`JVyOXM>#qtfjJ# zR}c5T8Q9V3dkadV)2(k(*6HeX)TZ*&_0zmg%*B6)eY^(1>{(`67N(2(`kIbX*`32W zhZG3Mk3Kw8u{t&)FdqLp=hP_+Ao{ishz4CRb{)7&qoUR~Gy+T!J+uQ*jCjZ~*4;8- zSmlNPqs9DLmG>n~!T~?LIb$y8jrTr_TPIhJ^c?T->bom^1k|Js?PV9m2d_ulXam`C-%dDrvNC*=p49JXc^;eDWW}47=i;W7wqf8g)4>#X^#$v5JUsTR zknu#B=k#+0s!Y-9eCRyZuzJaySAX6YEK}0sjy@tlr`jxNPwSn3j=;YOTq!%VTcy7q z%LTnHN)g%)ykPR}xE+N~sI)EF)gVNqbb52E&8&ta*t7;Mf0Ke-ciu&^2?{b!ykG%N zQPsM4M)|gyC&;clc7vp#;p%bCr#o|F3tN4`yI?Z@&f#Sz`eDz?;Pi2yp5k7cFUQR) z=8@1sS4s^`4S`%>bUt`fpg2lXkG{F|%6IItPe#C2KS2P^{MVP;6y!fds$*5u#ey0Q z<$`yVBQXmfm{>n6Xk~L$YBLB&f*Y(&1grgA+VxxYCL|5dynxbfuin=2oI~tro_lt* z&g*{HJv$0qy^%v78gHnA@?jAuoIJa(QxwzEyy37Ey3^TBB2uw|(7|SC9$(?gTYAIy z0hMKHckvqSeP5`Oa;2J8dax9}?Y_AtY5u{g)XV4wkUeaj$3M^(r8h&1GDkeqfY*8{ zCp3#qY6E>qp$?-^1$CDQ_D+|by3#=GNL-k<3IlGOP#yXsE;ofHU7DWss(K?zd)RHX z^Ll?hPPlFZZo*RT_qe(gwIc;yNz5uS*XVX=`iL@@nA$&wNV#ddrIclSEJV( ztd~t*sNFv_`WQMThYr|Bj%~#}CV>;COVq^n@D|))YbfKIM|M%xEc(K440VBv8teuP z*rK~ZR-&sH*PX?#R3ATPR&f}F2)WY7{a=}W`|41HBvt6wsaydhVcI%6alGQR0yGZS?m@N#}ptk9&>qSiQ zvLiWfIWxn^m@o~76U_~I zHOC8~liVa*JO1sZnx6Csn{+Zc*^7ivhxR;fXWla|8t;6|^?XO0!t*@fjn>E z`j@4?3m#u8&-_ZTY;L$7PA3pW+cClQ+>ga_AcW&`@|EJMIlge78V9Y(BEme%O8hGQ zSQN9oUxt^c{!%7we|l^3#?Jv)gA)n$(|pZ6XPUq}uWn3#6Px^6_Mc{s!3 z;&0KWk7x)qu6o{veyZ_Ww^7sGv}t@8D9;`*r$lJVvm|-Td-a>ojB=!}>Ri3dh2YPS z;}K~8Tfv}$>^S$jFkQD;Bh#ku7ed|MUOcjx=k86XPWb9U%Jdbjhw9j6RxiA&>mTg= zqyx1)hrm(ii77+*MmPxdc3&d3lLpT!(lq1Z44^1yNc)2a2m|`A3zyeBf95|nKoHPp zCi)tstfkE;T$5pCvy{CwYQgk%{_6R3-Mb}=qe@LqD+Sq3b;UZ`XMVbtdZOe_vN0<$&NHS zDh|0&MjhjU@?$G8yiabut8jR8u2f3oNmYhILIT6jj?s@di3OLgq3CPDTE3XMu=@hjwuwmQp@^Cn&`-O3;ysDOtA*02yQs=dqXN}k zPVAIDN#=QQ(_d=xtF!>ny6v(2%2`XNM+g&G7dJ~xx<-Ykx4Y(b2GjQ7bm|a;=1sNb zrJF^wFH+?_U-gT7pIdkuO?#rz5fxNZrh^;1d%+8D7nmHuxT;%Hp=oFZ-|CA_yIuv^ zHs5pGb6BP(pQnXy2ARGl3>a|P091}k45t?J6S2K2mUh#u$^;+rmV}G%dIDJ_+&Q7? ze>UOP+<+H$v&MhqOOu&6D* zUsM7k|8+>Y9r})Q*5bLy9>K0jDBtp-`3}rvEQ+ssHiu-*s_@LGP|&d3z?jW|Pci>S zLDf%h(d-CB-VQ$jchDu1Gw-%W2XxQ(8SCN=dpkA6`!V6^DX91uxDNFk0ui^(lq(-1 zBp>}3d%8xAItc}`No-cjd zPyAs`!gVkUkNZf~T*2$MI`t!QvcgE;J^j*@BcUJhZ(A)#oG5e7uMs^Lr`lV2g*bv< zC~F90-3wf=Gmv=a)3T?s!l^%6faKik1I8Nq7Z*20$^MnbBA8vKP8+C z*kU+N-fL8FZpEQ*lwtZiMzAH#tX?X^=ye%Y>%dE;8+(Df*2Pr?lfZP${l(~bm1rIi zZ)Y!8J`5xNZIoK2jVU=IY%G0dIjM`9%bH+2-?ZBuRCZ0WXg9fnC`SLOgz#j=q;!J{pIjd_w@*`JRUs% z@k8KqyPV8+v-frp{_gElT!@gUY_@k=K{IOJt$I4LgfB^6eSOA2$(t{H&Xd7G2=Ka% zB{Ts~{8z&K%YSP9D6*tGxJX--wf9AT~^{r1@t8GtDVjK>^g<@OyDc)$bgu= zG{p+|4V~pI2ISqse!R|x1&5*58yhw9epKB$S7Wzmc0L3**?)`mT|NC z5Dl*G_}_ejTK6j7c{jABoUkAr6os{vo^>x#;+f|_e1HA+0|7lZXnHH=J9=E$T^+z` zvodehb9~>m(On>~c)J262t(6o7VXkZrw<=N&)W<5KsBgT-y$s(4PMk9&eaSoM}{_q z2Is8u6D1k)}Ma@r{dIq5uTQz*a3@JkTQ)gEr}~H=(7eg7nY0 zB$|4J?TM_v`LCEAE%%}-*K67i+sn1@$szaCOd|Lhs@X622R5knq}yMdm;nJ!yb)P1 z=hyS^>O5_}e>*BL0>Z59-MOh@dDqkfCW@%1d#zW*_qk13q>mb6Drk-85duTr#-U9c z_2?P4fSo7My$mq~(E7+iDeoscXq4$n?eldj!*rPyifS@(Ytv=se$+ zmoR4kwa4L!(vmDlKCgJ`+Vh4IoNk1^S5Mp38iIsTgoJEoWw9>yqm(}@lmIr2&;QQf zB4py_$r^Bn9JvsOFp4tN*wYa;Aq2_-)ZJU*shsj2t#s2iBLWAnH*)&&~!wV|C#5iatrhy4Tpg1Z7inE_kZyyZ<>`;I$!?1&Q{O8ry zW?zx#h1*0i=qmN-!#9qIf(wxr7l`V+VXFd;!CAJ&;zCS!Fb^sSI-s{pJS(*m5O=wK zkUfNLz$@&O;1jcfz~GB{#Jt#(2)WXG{-1>Aeo^12z>uLOosGO=J zrTInv%g?OFG_b-*hXo>NdG(j$10>G{_$|Z1C9+L-_XwnD7>jwE&v9Amn1(IHnz6*% z$vWCP0u95#(ZI(amHgu7;i@AnG&x!7Pfl~=?ebw4+VHZoLZAbN0p8c-H0YXr)Y6Y& zuIOxN=To`GvMKURbjY#|__xXpgEMF^Jr5_lK`!h@Df-#B$!yPFUIcq;+k zVaL&bMa~JOHlhAdFt+`ruT5vR0Uc!XwW-oOwOG6w#8dJ7@^pj`gIAL;c+Jo`kPHuN zyi30Ses@!P9n%KXX4&d==&if7qo6&n+d4@uZZmxrikIrE4_M6xQnJxZacGXcmYuf$ za&^3-IKI!G6CoJhfkOprlp(m*Xvq4kFj#rS;FHt`q4}ZfkJyqM6AL{+nXM*U)kVcA zhQk?9WrT@lf|L0~2MvOQZHYW72i|pa5Shy_~5Y6i{fcU;EBjKJChga1?idyHa-Me zqb6~teE66f0EaV*o0pNMB2VQ5BaYC$w)$LsrC%p8_Ip&}kj95*71U+KjnJI*S1S`d z1e4ccaX>Z%nt%c}WaEM3c8jd2`6nEAX$6#r@jk?ywwG#IfPnv{+}~mg>8a%kfURU! z?>W$fEWAYTLLbpdg%VK-yT)fXkS>XV{*;*e7}k+PXXZ@@YPzWH`0+T9#z4(J|9GyF z_>GSeS94i_;+ zpjYs}Jg##k@wpZhE;_Mmvv3TXCetsrgVkM(Q+&fo7(pL;L9mXTtK`BUUx~I`50R1=-g+IusF_?PYe_{J|JJU zHFu)3x_O*V6q~dtURFg>DWb$3GM) zLy2b+9lfB%s9vQM7$ah4EO{X{vGRQF1fl~og#}AYzV>QtK{nWC4I3-tfy?cZ4ShG^ zmC4#nriq~Shof+uee;fm+HGyCte!R;}{4NT4K;Cuw8e}F&23q z46pp#YJW|%In(kn+$h(fuutc;R40&$niXU|UUvi-6a#8I~ictC&ZprPDw~iV!a4WpKL=Er}+ktBX*C#k4t?tb!%nq!^V#6z79Jm#u76 zTDl546sPIp#?qlzQ2=R$E1?dni<6}4$Raz4th)6sgv3ynPyEO(RAj<4PxG*{;S%7Y zrW@nY?rw(04{sk(ze+}`6za2_H{zR1{LZutGSS_T9XpFs!>?xpvM|+yBVDzy`_Q{f>9^eb$ZTqhvAJ*D!4Fef9Q;+PT>-# zn(oii-#Z5r(t6gi@9h~-A0?}Pd8TV)TITjS@%k5US~0qYr(=%Cj7s%)g3hrrQ5G(K z1gHt2_ZCTUJA;ch8V9YP9;iRpT#sgyVM4}!wB@c2p6$)!AOq$Mc?lOeC=w7+vK@%N zHs3M&{O0W%*BXA}{HL$t&~qdgGe<)3Zf<@ilFI&7llzg!xrfxM-$6`B9ltaG(3nfFD zyd!9*7=EyC^2T`FBsd(7DPI3s?LFdDPzOe?x}-}pha2#dv{2mw)IUGQXM8Y9iMsZIsbOpssi#6!U>D|an*Mui)Hp9qUo@O4riH8HAH*vJZ4uya`i%e>PxXVe zg)d~1MsRW4oB32nlN=-aX}?>ix>&Ivw~5;`AE)ns&FpO$r#XK5oIR8^M~dI>i?;gf_ClE6E< z@j01K(L#hu)z%?jKDpoP$%+Klla=xrmAS_U>SNi6@4RA(1KAv*)p+fz1Nllxz~ICv zc2Lr~>7xMoss+Uz=jHFeIPvWHIWG?$oE!^FQ%5~v!ABg7`Wnx)x*9XicMT1`*!`q~ z8!j<=PT2aDexWTzpnIv)3ns^@KK{f5MIU5#CYk>IIcF7LwommH%N{K?@URqhI7K=s zkXsil^y&W7Z~?uilR3BekOAV1An@U!K4aM~ld?L|j;?>fi;Fr?Pg9N}%>Y#20?$>~ ziHwu=C27!gX${UPB2s3wA_yB}CI;j{BPtCXVc6`+e4d~!A0pg6kr<3W3K~mPgWK3` zqge9iu@(!IkRM)X>&mm!{=|p5{!+BaYQpb=u)Jg-*UlnmNH}s*e33?WV)I_` z{?E6-A23RwgEnB2`D+|BES!$)9h}HY`GBTNMm0F?HE8&24A2CJAHsk23>n)lJ=N{> zxddi^sSf(Q%DWspgJ+=$f%X-FB^V5`Sm~a%rXK{mLExZa1gbJl9)q0w z>1@KYL{)>mJ?Gu!qW`uV_LdU20A}-71Ir=>ZTLMnw`^}!?>_naT0jonC`K=d)x;_C z#c1Mx?gC^ZEWx=aw?ScqkK|M{APxS<_r-f1CtHh;8}3Iy~5h0(>ARpj_H%wFtIHW zRmj_fQ%nIs)ry$dx1;SG1}Q;EzA6nS-Xll$Q=l}gHtl-gx8^LB_z)dg_z(9LR{x73#c^GA7aK-(^t2yhY%^L%GS6W(C zAY}T&ZFqAxkw9^MKeuv$xjujXUrXek3VIc9+8S2AXI5X_b(mxp>7u1(d1PNxGa(tQ z@SNS&sLTKO=1*Lh%>$l!-~Wm-_gGE*>=2iPP&hfV_Tg=T)O<+awnzE>XPU>Tv4Q9^ z!=F2*Y>q{?rm)pM-4{Mxpoe^BZ5SPF0qycclaE_&LNUVu8;dpqZgSXL>2<#B|6H4B zeB^8varj$=zHX!4HVdr+;fqIV84tM|#`Z5Ask=s)607sq;@I0xy=<_orj@Jz+gFou z7VKrPoOqeENZ%5;(d3Skh5ecMEF+xfiqqwHsgGjOAE-;*cDS?6OCutgM8EgQ)t4AA#q@*&3LOe5jY<||h@mX1r zRF_Oyi36lb40+wq`v2G04RN3oyja3{``!{_DtKYWB*clQ&hNH5f7)l6H54+2!+n(f zpp$?k6*-s~|2mj#2YWjtH}~>F)4!iaFU~x(e2#iNB`bW9Eof%@`gb?~iR8 ztLC-LA;`CFIrdeR;mBo@dG<#tiGA5u)Cmrsw##|-RRpFSA4%0j9{;yVdUZe|dH(?V zqNDh-#b~0K?CQ676~S9;q(m#r)gaCD==mJViw!4;ND#bjm3Euc_or<~Kpt(C-?%Y! zZw=!qQ%Q#8avmqC zj07H2&yr!O7fbC;;pQ5#HBymnPCQvY?DRA$fkMP?2DP77$QE|z zwZNy?)JJy@5W_*d8IJ$XjeT!_B>nBkS7Zv|Xna@=y)>@$1^Ta~LCg8FX)7*Tu|pxy z*!$VVsfBbCGudTKNMIFn%b5)oBsbC3ghE4?#p{&w71(%}i5jOhIBAjJ>_LO1Z+}+3 z89|xHs%jcPrEMRO$zwhkQnTe;_=fw6BQL_l0*i`094i-;&#pebdYowry1Bp-<%6g# zd!={k{UeAWMl6Dvh?his7!rrN015}Z{j6$juLTPj4r4hkzcHi?DSo{9G%$>SAmm=^ z70m4xN4|S>^?ENn3%m&S-j>($0GuMykn=0wgTg2RsR9IeFbr#CsMh_e>mRAy9aGAg z(>Ej{7(_#db)Tw5^1o}@sf}V|ZMrO~7N3k!JzxuQ<3>7vg*5rK(6kjCK5nYSx zOo_R+QWCaqg&G(IxcD=R$7|NGAyG&`;>yO^0iR#?AeO|uErFwtK|rWL z1y!NeXQl5DH$;tH%T3n*LXEs>?jgL^xyve_Bc34DM0PaoN4whBV~%{h2Z35|d&rFJpJV z_pNvDI$nHFVP^#u8hT8H%SmdlX+Q*gp8-Kq3v&`O^tkhwp8EUgaB5EB9~CHq5?;Ig zVphP)S{{)LXqju(rL5UdQ6gCKMgM{MmZZr*O3$~_m`}ovBQNeQL&LQADHmmSXW92^ zO;RoC4#OEPbfy(=RjU z_7jem>J_3n(lob!G;DY>t~Dp$g)Us93EOw{M>j{;j{Y)m7|6#5@f20+x@7FIaBBQQ zkSHy`mf$cIH8GN(UGKJL!OOA2KbL+DS2ES{oV7LOw5yR4oBf*#wJEXaGa8&~q;3JV zfGTx5v`XO~x*P!o;aR-lQ)W*J4K*ykjh=+Uws?IO zq{W|!FBZvMG)r@`lAYnjGogc>cEYM#4~e+bq%hlUPlOLeBeVVZZ?j$Wv{%bvbS%7G z1w`?q`aAEPI*4BD?!5ZXl(#LV+zt&RB! zF9=tJ(R94jyq_%Xa#d($1{ee~N!-GO1hkG;83;PxN zE_xgCKif(Vd-=mg7(HkTRW#TCGX4GCZ6%W73g4UV=|Qz)!R$?KcFlz1}x|@3+=n+<%anIdkTmv!DHhYL$rq%QKwuFWsbDG<cuTBB*=-6*FlI zvezo~JbxoiAsAa!J!+CxzMFo?&zIYIYYauF;O@my>`PfYE4?`wAY5fY--BT_iCMWv zE==UTk#$vGS5bl$v@?`(^B&2?a=Jc;HlD-1>A$%Ecge4AvE9|H4hC8Ov!=ck^NyGL zt>FZ|RXljQ`l*@5WDC?j&lqdg2zv;P;HU*$RS(p};jGT5JXfiZdi(P0zw2%5%Xz`~ zs3n|3$@sE=j04zk&$8!yf3|*8Pi92N!Hm7cbIEHf&jFnt^v1Vj6(3LYEe&{si&67?6Y}Vby4T`?_BmokbZht) z(W;m5y>rip?tkLdQwo{Oe=Si_NsG6pV%<`2p;hGUQXoDsae>q(RSN}c01G7?Ul0qe zof?ISntG#(a8yk(@EG43#1gu>Li_Fh&$hvrYnV`&hOqp+^t&iFY7genzp7Zj=E6CF z`AIEP&j6@wgJ+=_ueE$40~6^iS>2<10__(H>*IQ=6Sgt)^n;dZn><#Q_qp>t(Ywpj zVz=ivSA?lM7X)9Ng%*|%*rS_tWR|L)S#|V$A~{oeIQo-poyJ?sX|zE4mU=I5KB?`g zM9~N0>HXVFQx<90qKQ%)!^H5i+6G5ETYiX82o}d$c-6l(8}}P9;&a=`nU;K5U4ZK% z{I*oBrdU40{1qYLC7dmch!<+*9Wy;&3KMn@{C21fDxBACO;Xpx<7>S1-JIWT=Fae3 z%fV%rMcudtADIE2WVu|8GaC7}NX@60A47&EO^^3%U3%CsmrdW@M0ms+5s_#gTL!HCzUU0(o-z8hZpa)L-QP&-Z{* zNt`fn`rbgs2M51SaA1-R7KB~UE5lod*tX?Pae;348n2u z;lxp4NA&If?{a=F1TwCEe;NeQ!5m;B{fyg52%E&=Nl~zSqt(r%>vS;{+yRj%Os!}X;QCfR#N{s6dS$|cHd1Co99w;uLeHphepmc!ZOlUd z0pfsQyxHky-Cgvi`&Z?1`Tmr1rcPdCx{ZLtbO*NxxquRsVWDDx0GBa4-2DTTy@B8J z`}h>2WUXQ4{Nh1%Qv52B@{rS{7hl?{10-rN4_Xkda_X1(zm69(what za1GTKeJG){pveI1j`k~sH?Qgsr@dBgC|ZaN{ic=JJT2dEi|F{0EYhk@~6kc<#OqP(bxmQ6V6rm-CLSYc%>g|Fe7O z_PaCV-#JBNz0L+=!^vN0Pjbz{Ovj=-{lg3e86lHWqDs*qDigYUYsS@{t=v$|PI!`B zQusEDXm^b(6NmqR;{OISYnm8~@RF9g(zcan+A)-qulf~n47ctzxJE#+D(>t50Tm6O z=n@x7eg>;kKf%PR;CYP$7QK|s=icNa<}^|Rt5QZEu9RdwHu>Que)XvqnapEP+nlHQ zx9|8C&Tcvdyzl>_>HN~k&Qub6^20{eZut+odzItC9TVi&`2=|!47uLh((sj%dMIYj zQ+1g?7H|r^;<3#O88(Icrd1!KiHD1<18*Lt$UVD6vq3cyHmdYorfP4rW!Z>mRqfTa z;EP{{Z(9oHakkqsDc>sFRM0DP)=nz+?783`!;-qE+dEG=#JZ!twf|$~oRHl)@bqDX zEq&*K_2KzR8O6t66j|=oVsE{yVqiN<>lU7K`Lh+;Uf*sjZ@t+@SUh)4tf7aG+k=%l zTR+h=9lF-xDdn=axwqaV6-WvtLL$9y5rL6RMj#Ie`Xrp%lj?(sjUhZ`YcE8BS^KTO zK!6gs!rXRE0ZtHYW$Wv$$@%1=1TvgLbrH8$xQq#%cdFXsm;E~Q1x*?Y2Dfe>iqqj! zEfX5g&UOnB<#GZv(c4fLSGAjw+7go(hHP#@YJX4VaYbvx+XvV^9u<+Ax;9MV-#U|T z8I95mlnZXj#Qg@dwL-)O6z?L9M}*n|m{Q`k7u;A|viKO(AgYy%VL$)2ykEkz*d3ab zYL6x+Qp))Ky}b;F1&yYMZr?a}w)Q96()dm>P|q7esQ1)!ShDw>+TqB>^`Nkr{06Xc z!MtzUNgs3b!C_U%z2x*KPIzQOL}dNj0c>L-+*%XmAADa`z1QodvKott(aesIF|e%} z^K{s`hyk(qqDo@7-6u)@r5Q*X13KXo7u1S$Z9kBGgC_ z*3bjX5YQ`(<%k|eCF77aT=P=H+XG;1tf_^x;D-m_+BYh%7K}3{hDkmil~cOZj3|Z! zPr2oZw#{T?!@shO=~_L0k2nOrMtc4FXJ4#l{N3&$4RNclx^0XPT#2!2`~0eF+AS=Y z$GAIiFEs6APV&2l(v*_y6rDSTPepGV)zDs6k~dQBMg`h)a|3zvXrtUC*Hr)*FePjB zcWVc1jcnn|Hc9<%`+b@xSp)pKB(LDGj2Tw$V9fJnhOEEx^e^9F7|OT8xVQ49wW2hW zU^V0nC#%cSaK51@WB77qd5c5BesOc}ZaV1}h{hgnrp-Cr(uz!yA-B;fXj=0oz7#xZ zRiFN&t|`7@b=4?DWBcTe^T$B8iGeD&e3C`7K8sAOAF)%N2gAH@peo7GEIos5DY5&2 zA>>wPwnsN(tPZKE0tvo}D9TYsFCd1vt?v)r6>Nl@)cN1UKhZ%w+xq05RnCG7J968nivg*v& ze)#)D@CVY(Fjc>iQ}o69q7QGzU%MU_f+yB{i~tpi=~jf6b2bs9^Q3vXnRgW z2dJq{5szyt%4HeCWR1BqzG9N`=0xlHdyi(`R*O5uR3VwxWudamlsW{xB`Q7m$R`(vujmHTCjGrA=LVgI+@KKyG*(=gdpj|8yF8dJ^ za^jK4O%jHxL%g`cqUx^h#k8)CK0>y|0Q@(FVfJUiLfMOQ-_K#F4qLUF{|rY$V4}_i z8k(`g)U0Ky%Vu}{mx6@$!SnA4KHu`4gk@~;!~H6PMoNte0iOHra}-zu#3(m!o-Hx8 zyPQDcd_|=yxS?(bszlvn7OQ(AfJt_ob#x`qN4f}OR1PLrc+NWTB%=}^lTqL_@(e48 zo0B$77m#=fReMI1(RYUg*1h#>(l{}9Y#TJ5THJbQ;qt9v{=PltTDisdMxahVjKT24 z$2)Z8CSh9>^W5#DFhi+^G*vE9;It#iKub&}bE_2nSYSJtS$vHk7gR_uV^N*LpjV71 z5!|qjoU?}vr7dxMk^58UT4Ff;Hj-?nuX!bFSLuxGwmS3z&tUBpzM=aXdU{8EC`jK@ zYVo6MNmm21=$i1FqFZm~SvJJu4-LMZZsSx>dqJ`)&6)zvTvyxgOtio0@sAHj0DeM% zT_Xyvi{uwqSX+Oo2NZ6e$brO@%VktU3Yc&wAq_L?*+LLb|a_qYjdXNpQfNg?1`@YMiHxxwr7cLMW| zy6W9DOhr(28!x7*I%quSms%cxn!)(nhCVjC@vnjCxHk5#&Cuj+(cs+cInij$<GM(S6-6d6nu+m?Z>E>YJrw<>7z;j zocZ6qs>x6HJ`9e5B%6aJE=s4j{Y_Li52ZMkaJ@x_B+>GG4`# zhLQQeM%WyPMk}mhY|~JgYmWH8m0I(lyNA8`)D)QC5%wbi4sqLX0Nne% zh+3ZtMg+uhUndk@!wAs%^IFzEEnKiV>Of&j11+<6ZKcXy5 z>VL`xb;7Fop@8KIJ*xT6I)wwVZXW6=eRv2TVaroy&dm%gmaz~;ogPb<+47(BkM{14 zwiIxKfmuW5oXE_+RVK?Aj`+U04Z7#La#ZUU`?GSn#^%&`!FiqxKLtb6T7bT$%?c;- zZ{`@#=$93otx3jFJ?7QFuw>k52uUw-3dHny*+`0wf+M2`nE77fMKRx5 zzO^XVUH%NvB$6jT28*sBU^VqTp`tad@-p{Q?H60oNtO_>W=ho zb~LKkw3v+yGgM$&-O8<`na4G5!bpx&!l|36{pzgUg@q8{#2?Sm0=(UW5a907ysqP=20GTsjLaxB5ebCZ zvP?Kw&*(rHl2}hmmB;U|MW;Dw*yDdjIMDgIh|N%e>!bIlvH090IQnG@s&r6!Ced>*g+Gm!bYm3k( z@lxS_Z}pVHW{sTa5-XrGsWLI1&~@j(0?SqhxyR034p_GOXg|7wJ3A~}9Frtj`Rs86 zQcCVLML}V+^)v?GHg&79KvERsFuaSuA_KjSRK0NYV?N1ZNX^H|*IjS5hndf7K;s;V zOijN=*d)Lqt($O$Vihd0?H2N;E|4;ubmqc37X+w-kFWeqa9+V8YVMO#+N0EWZEgR~E*zdH#C-F6NZ2G$OX#4`tw$TQIX+pOzfoHrY z-UnK#-48z>cFx`Iy1silmsppl&YTt7V$Sv@?bz#2>ktqoj1Gug%#@TK#s*oazTg9| zr>5%Js!HTUWy@&@|B`$49 zUc?3!oz9&@=-Mob$`o{IA)9#6>ycm%<&iJXgltfwyrT4U&XbV%gUT7H5vU3w+sF6C zPzG>Acu`v|W=cg4J)+GiGN7>Y>t0do{sLZYk%_marI24_7uOv=zM zziQHiXQ1t}R?7e?#Pj%6LZ|y_vA3?Cyp7QS#j`lU9aO9V6rW}^cYEP;T-`XNg#}h7 zlBSXOEr6PMF6~v@<<~LqyCP-{QrWE^zK-;kBmzombMB`j+paBUWJ*&1$0?b0Ij^*= z%&07ecspO={3}x1k%|wYZ}M~PMkeqxfoXI_<-STNY83^V594ER3Icc=OogOdiV`_B zaCA7)sC*%$Uf!3Ft=elvVP2%&T+^~ywJ7#l~m$1a}Gu+sFx2pv<=vk>X;~^_7nBbxNU@hs~bDAw>jzsOEyad zlVU{7gT4wG=~jX>j3|QiHEYI!;wkdpL)| zM~=C)9k_ega1aLZr@i zn`FhSYT1jvCzqz2aLJ5FGfV3h4~C6X*!y^&%pf$A$0wR(PuH6F(2tn{Iq}8CvA+?B zKRFlH!g+{UHn@kykez=hBsF3h>FE262CtrZb&IBo;(UTp^|0V-!F51pTeVF#E z_2D~dzo7ZzxX?ogKW3!Fx*Oz%tp!)#+<499CIvIV?wC0-;9VoNVozYJ9XtuE*Xi@z z1pz_kf3_IeG%Z;5x9@Gi@?r zbcQstL2amFdjythV>>?&wae%6=hdK@RVr!ijg|>*+qy@Wv>|D^Miya2^^5;EbXm#z zZ>A#!nsZn;N|=h^ zXHlT45fUpILPCa0?qr1FQyw+6*x)7DNN?1+y8Krj=|4D`Mua}%k{C6q@#R7fW?zi2 z4(EP+=^zx07OM0@2N(bJxi89PrS{LMpc`v(FLsU!_=u;8?3|o@uAe%akA6ITjZW*B zeo1%nx{ENFoDj-DUOHv^-@H|XzWQn4XmDq#Rf@%Jek}Ir#|S;x5W3KBo1S{+3=PU8 zp;m*}PqqE0mfOQ*XXauC}@7) zS|bKa&Hz6QJn7duF$^eybU+ALxpXfr)`eZj^#>-xLICR;@{Y3?j%5Y}BFN>;|BuTF z#_j%E0O06mt@xWnpJ4`Q;~ymY!a}UfWHb|hu|8|<#}INr@{3zGI5t?thPS_N6*|x$ zb4=N{2KM5&qVX1;=OG9*===vXmuz&yH%kLDk5K;T$0DMb;(}E8aZG3gL^)FCwfX3B#+1P) zO~qUTqFgIf-MwOyFSEhU>r=t{!}62Ir`sQdAd*1H*Qhkedsx8#$9Ip=B>$$+7BN2N zwYf00&5OT2)gJc6#gWD>d@+bhG$||L_CEcKr!}pyIdL6G5)rcBD1EK4C*~2JpZK}Z-{=rE zf{<_!9u5To+&FR+;TJ-tC{5bm-RXZF4QiB00%nzF+Xpn5GPW1dPV!$ zxAGc(rEcP5dK6JEG2CMD6S1z-fgnan<8;?}t0@!xcA1J~W3&VdPAd!Vp1w--70VpF zGE?z0ZqAC-T)?}o3CaKApfG|T3=Q9rVXZlb2F{RS8Vj+LA)KO3P~Y0E*R!SQM=5PmVb_~eo9=ud8JsQ3%j zC#Pn+zg|%vO8hh_`@(vgYoO4P4*@w(%R*)~GCHsy`0Gc1?0r_Wa0#A>Lbq>GXk&rl z;5Qcl=1(Fbz7Ropg9I4Ah_rt^z#3MXTqLTeZa)pQ1MiH!B@PFItJ14ayaXe;F1SRD z3PE5_F+wT?`7)fM`o;g?yH7U8Bn={Aci0oPM_ll)p@v`n8l*L>(-Z>d zqQ9`2bY@r14i+ji2O=RA(CAq0PwrQNTr}k57QS*x_?Uzl^!UWe$fS zT8r+qU-TW)NnUAFu_Rl^TO%(8KICbwL<|?N7=g(h$V6%Uos*gEX}N6&)jdTl_DI^V;OfZ7Z9M zOzKQti`pzB&l1vR+w9$~JYs%IyIMw@JmeVljgH_(&EfZmYL#*wVXPF${g~}7F5erB zMBj@V^$HADYq~B9Xg@A|9YePt--1}wSI{HT-LOtsB*s@lJM;*=PXh#!8Hk~E3!h&u zv^yN}DE|@oDsB-6Y<8ZS6#8~FBl?fTcy9nDf&LoBaQzY0qu4r#dbw;` zPyKYO3N4CBd8P~X0IVcdZ%t#Dq-L2Na__%hS45==8Xk_MxphiCdp&h9bLgif4WM~u zZkvZ?x-$*QE9C?^29;6YCiLU%7-tlF2-6;7Y^T|ZI4b>A8 z%f<$?=&nT{P+}3$*KqFQn|#i_?0m2%5MONh2z+Zy_J_il4PKrjvP`xeEWpLUCzk*> zqe#$pE)G_ng@lmU(%}Zn_3-DzsZt*&*1vr>=QIKyG&rw>7b`(DL^1b6B@y8{zs!>{Tk zU2o3@?Cs8+Mf?O5U-sV{IhOJT2KK)jX!kdXIJS?mj;esQbi~XdPNsI=neXmrn;k3) zan0Q}mVAJTIkjT$Dd-nGncmEI0?-S(rEHZq74Al~Bd*VQzREgayJYHApN3}Ce5JRS zGd__O%e`x7IKp7h4&ly*7X8tpwRUulll*U=m)eQn#Bix=HS}Ygx)tOa@7&~4+#23% z47jbDxBZNZfU^7<`d$iMg^Ii`L)HD>s};SL*{bj28Sk%%CsXdXqgD~bdI{L65a-}f zYrK7maHL6gzR$F>1uTKRS_{{cf&yNKf*-uQoBO`H@pcAT=@=swo_-f7olJGk(Tp_( zQ()vx);9vMr5J{miZJ;bAEwj0C8=?4xrIYFfs(2G?IjYPxlG0WHxzYT^l|DDk%t4DyfQ-NEy& z51NgO>*GxWk(0M|i7T5@Oy3Y`6}lG|=^XGtT3HxS1DN=dY*y7g?kx;zV{d%xNa}xf zQ~X+(TJJTUwaw?%?K5gM5d{nDXvpSrNeF4SNVNTLj=UV!Gqk-1oR|qKjzd@IF2`7! zR1p*jKA3&T(!+G^Q~98BmMP_i=;0PIuvuGNcX8+afR$O7YT~5+x%+bv>DCC?r$%lN zys*zt9vdw5+tc1t4BDUA`$$}o=k|?q%SJasH2WYWcnH^2pE0sl-2ih76p51gG930Z zKUUTgizswbZzs&0>uSP5k&Q4O&Q=_lDwfvm6=l~Gl?|tO2&NrzqpVslr=N9#7BvpT zF!Ldw$3@K_A<_GzmiXu2_N~^0X9=pl(z&h8ThTdDALjS5htUXFx0zaH*Q&xU0jRVL zuIa3#?>@GBRey|D6X7PnKUKdyu@nFj#7D6DPwAd4zrWmQ)_z}A26L-AStJD5D5@}) zO~+$-L^khji2qxl^=G;Mr`9c!2$N;3+I^d7;sMP}ao3Fr?u?#uWW|}$5N9>wH-){m z8V+#$DUr(Y1#Oe72@7Nvd@+ zt;N_17&pgchn!qck+Z-#B#F<7X6Ap24`30Mz}_;Mw99+1hY6*YXyc_E!qw3taqKOd z_y>=DOSA-_nJTJn3k~5&Zi^ehmI;93SciRV1@>-{bBK&vA&d?$^6hKIT}?CluTM`J zW-jBqu#;Li7I0ssRrd1ny~}(k$-v?pC=9H>u*<|U4;K!lIXXQhm&X0K^b-5aN{dhr z2jp31Lc&ZRSlD7hcDHbCXOsom?xXkVPX%5Q=3&{8D`w)Kt_b@Av`D7-bBCLw4^-3c z7`NxdH2-j6{G4$^cBT^UU8EyPI1uWRt{1ApEHe@0S1&B|Szj8yjw0Hf-Aa74+^rrJ zo|pf($b^;ww&AlZhW>J#czF^Ym5j^@XnWw)+*0vVx=?S0*6<+YtoV(B>IW;7^Xl(% zUbR`6=6P3}j&nJ8KBEt?%G(iI)qFwHcIft<%2MMUlj8?j`mw{$1u0)XYg+r4iw|GA znc0WGa!%OOKc=VZw9xQ_=r>jhO*+-l4y0Ah0*4K;$*m;fw67}%&~B!+$v@#OB-w-* zblSbejU_>?Z^hTLvmvxafXmWl3tbkPhv-j)#)HyPigFwZk?oq#GzrF366(!)s(NisVpL1=(&oup5rlZPZ9IM0<|A~GL}vNlnl`{j&P30lq4N|}fE1&E-ck$rT=h6gcg!hYHO=igvP zjpOoo*g_eNBpU{4?3qaFrBZ_T#|7=3dk6OezzI&oz3(9yPL~ojnyD^5o6e-YCiyPnCt1ogl_mjivDm`BMUC9 z*!k{pHeL;t0*C)1-{|x)3g${HhK*s=iN0R~M7mk3aT5TY z;O)!Q3xOPsm)fn2a!>gQYJs zTsbl#Un5iK=szjMRUwja;u}4I07`gj^Z9@W;MNaZNNhOXzBv0!NDedPNUsk6MoWwu z;@}5<)nCwhM!*4W)|b_~Dh$ehDW-$`!6#r4gbl!32>)^UA7wGi2kRgiFjSxoy?3O` zW@lVg>v=f)B}*+MdgB&js8pQ8D?~~(+nbfs;It<7t1PDWX-3|FgABL>(1INY$@!F3 z(SM*1|Ne&w`0nau{?rXVeg2Bk(9`q$o4YP;&#sU{lSaa6Q>VmnH~jy+01YTFF!R)< z(4I-iijRUQ{7=&XI8OzkBBx$Nf8^`yyQ(r=&s%xjwQV&iz@-Z3r3#KR^cjwe#$04L zI8y2z<;K978I-;R*+&Ge64WEH`I!s($J6uh`1)Eoz=gWrGuBJ$zKGFdq)A@gKH=}#OBCoGOwv;HfXaz)X6 zMC9r0)Q^FmFva;8f3T|K5xaK&7^sifnH$u=!2?r|--{6H(uM|{SbGUw1!_;kO-2m$ zEtdKlo%0%R%MucP9-|QNcbs5#EM6chJ`=KMP1@gW%U(*ehwC)bB9hs)KP0pNBFsM$V>fax{}NOeQLALw ziJv|{7f@i)Gw{;2PaYLda%}ml+v+?Ynrs9be`q`RQ@}#2Df4~C&BHcfNuGT`z zU!^$%$N&j-B<$)Zu6Vo-c-v=NDH=|W_jpbkX@xE^YExkip!=|EUuor51?IPJx$LwG zZ=uy7A)HhP1DSI*>q3Ns|_;;wO_U>wHmE=?&X;N&tx?`} zgvc)TFzkZw)W;|C!wuK==A*5~8$!8gA_Y<1ln&1`TIEf7qowjfBzwy_f|p# zo|a_a&~t=5*Qc=N`**O4PuebIhBY7qRRY|PmcmwQO6%ZTlUGGglL>X_J%9NVUe0|a zyReL4?QdD~Kj_>40`^jGGkwh4t-*8r2*+BJrlRLpElnWl8PDfYG^^3pER9(pRDbP? zHetjDt`dkS2L5*L>i^&09t-4pb?I*R9c>4wWoJi9W5ac{{Lw;XjrOK4Gi&U)&1v;3 z3ELTsayB|fQ9>7f0Qac!`S5q;^AFDVkw-vOc;k_IgBXSnkVQXb$e+}HuNblcP>6&q z2fGAnBb`dg9U5r9a!@1yU;vV#3!U<=V8&s@cXWtD|NE@+ubqoOOb#gil6Nw^Q_p-@ zqub_@bCNFP++#72ofy^46n5wT(s!Qv5p@x{bsXf@Y5zZO{jb)>KZTdM7^nz^Bw-^< zwLc(c#<20DmlElP9BQrztP74^etXZ?hrL0i+|;z3}vN>Xk(iwFK;1 zHtgoTj+rl%6t3*oQ9mFM7xCW^*MBxys;>vvDtP{H(Ll=1#dFJx7qLxu$N&8Y?9`uSb1Y0nz)OtuHb;AOKZ=PwoZS6X_ZAiTh} z#uxy-7RQvD1fLQowo8`L{Huek->RNH8>Zmr=6;S=3#KOZ;o_*42((d?fXMgJzlI*^ z+r2L$ovhkM2_*@!=`Fb=q4Q-wW+lvM3*|YsaUPA-- zgXwRW{N&UDqtI$jwF&GacsIB5wKBv;_rB3cBD(Yb4i7>d1!|}X$kdqi(g@yvYYT@^ z=NhXT0!S}HE8no?>n{i0HPDqHxr;M~ZExkk1Ala?gJrn$>D-ljbA#tyzR$>XwZ>w0 zEyi&fvmy#x>@B5!p491F=aV$B(J4@hN^gkkQnG%#uesc!cZ8l7qUrQiaM(yrj1>uPRWYBGl#?_R~N zvpYX#Ii(LDv)p^{@mvFYi}rTJ>}%Sb{m0^Ud40B|H_{b+!WQ-(E4X25fcS0CbA~;h z$MVhKEGwU{8J_!dpZo7k(n%yWw~GZt+TL_CqgQa-%=#79Tr z_WxofX0)(2J`qp3kMT2_?Uf=(KbhwAKH2ryE?g}!vunU_XdVCfWc4O1_~{aW5x^&? zV;6@?$q*zk`{ajr^VdlZP<~$c9Lcu&ZTn&kGzR3NSsCsxetjfb;JyRBq#%`=YJluF zTs%r8bgb*Y@mNzMpi-VrJcWH*lHf`3gGrV5n?X{X;M<_<=+NX|q&yLbPw^orC6&N= zp{Q4N!1Zo%*@w)>Xp7KukEA#iz<3@>;XogsK8E-i?>hf4XR+Mrtvg>9nF?%1vDb*} zW2HbdLVr_Ct>(c?y!4*lx%^61SS7h_zhHw|`{n){RVLa~&2f^t_SNW>J3qyMLXK45 zrIrTdRXFC7X@hT6%D7r?GP9w(=Z$B{h0ul5G2u^m_1nJC>@6b%=R>|io7A792-C(e z2W22{$y{5uJlqB;JCi-+jqZx1s?BzH@F zPRUqQmG{ISD@=aQ#f9oL-y4Ox4{Z4^h^ve15doMRpb8Wd&H%uDg7il0rT0iUp;xrpe+s%%>CVgjTq68-sZQ1+Xz{* zS*UGi9LqIM-(o=gEBM37nYFlk36ReblkQ&($4(N0vfRg4S}mG5cV=v z(3HlKEd26gVzV^FJuT?s&NBjuhWTB@Jz%1QWrlopyj6Rtu+GEWOGiv6I)v%;0C7+v^$$bbAWm8MqE;PQ zVb%D6$KbTshnH_!u(Yh)NjOy%TAfB^u`?$Iif#*n`KCX>d~qI7UI|s;eKi6j;7Mb=aJ?XZ|t`%)RMkvP$%6HuU^M2o!0 zPlBvUwGIDfvgaJ;@WSYm`ZU^0L6Q2#7qq?dgX_1`mVAm)FS|Eq*aa|QZ?)95o`SaS zVvv}r$sH zn*>&$%IUOja4fB0QmT9CjU4BH6;r};-KKO$1 zNZ8}wCVD5Wt!bx+Iy}*d^wT4U5ZR~%Pnn|amkblp4kxT1Bkcwf!k`v%W?qer3%n}J zCuzd3y!MJs_;RqagBz0Bd!<-c{jxGG3KN(xPDy=8e|KLTp`c#+mzo$fjRvX6ccvlW zlMxyL-Gmf~1%bfC%k`a{1{F16rI~y0lS3)0=zBWW2@O-HHAkdsv*SipeSWe5dk+i| zQUR(YaFYnCq^PIFPxqh{nekGexi32@=O!;k3L~zOHi(CXhR|bPF#Y@0HTpBQue-Oi zMJYNTc|)gZBfa7=gvAfK1)?%b*YYGVGt7bPJQb??y7MwpMWF8d z3owEwCDzN+LO+K19V#b_NV6|hLzo=(-7h81-)C@jcrEA*eO`NylJ=3TLf%O>$pW~B zKTMN<(vsvd)t@@s)p(`R?F}KC`-g(-_{!i=N#k6zr8H{zO7)IUIFFQXxYHieuTLmbu5eU4`U|UjY+e@QpGC!z0^U^6bH9J0RDZca zo+MOp3gg1sLR5#fE+GswYc0D^=ZFz27hv9mT&8POty!_rN2%7CjxSlOP9R8l8^gLU zEE7(_*i(Mbau^**t>me~y`RwCY0fw;4WhHCr9UR$=eygOX8(kS^2VG%JwKR787iyB z9AP$f1pJC>loo91y1aM8p1x6&fo)XI30h5b4(?A*<``7fdvfr!1Lt81+uDBWV*=Td zV4*bclF(Yr2e2fu*ti(=Zb4BtZ{^WTI6ZSMJ(5eHy}=N)Al6K}Rr?JZ3S+G~RhM?@ zDZPUd=S_ZYj!25O&I5iX-6`_b4%#r8I#ol75>51D2}9&pYjL9A?lCunRtfL6iptJ} zICpht-y(x{bje1aluq2povIa7j#F;!Ri+=~&x6oiwnI-~di0_}NznO`d^-pkWZvGr ztV_Fo)2_IeaAXVd`aD@N^4ba_n;0LugmrQ754PL~zyvDrj%#8<1f&-2TxM<5gh2&n zk{Gyw&TNY0Z>x6s*}Qg^$YIYqaGdccW+XJN8$U9rEyF;sAeQ+npHz-q&z zpWxUN|M9ha!i8WW2Ci>4hpgXYwaP3b?}sy@-YISAd3{JSVT*7fQp*%06?9QbI=rF_ z)}TOjxYA?GZLIrQ8(0`D-XV!|`7=s^1OCcEyFk5^fDSVarRFm<_sE#re~1N39lZQL zTimz>opeZ_Q)fmo#M}fon=t5Rnrugr!kO|{$v0K!`_Spa^>WdfkO2}qkQ{RruR>4P z?pwzw_?eZ58p6D{pB*#CT=RsWQ-|BtMytt<8ufBQrg!=>7&;C-J7~S=@)^W^(Pl8t z(Ck0wvVYqyBj3D6vmui)nvv~3OS|Lee5n9hFgB?=$tw6!)@G;Kj=gMipLuV2eoFVS z+9uG`({0%Fi&Hbd1s!aD*ZIe#Nst7irN^X95AnvzV!gutpPrTfa@D+xe(c03;~0CO z6$uA-w%X7P)_=NEfyGMgaDAU?F2QS%=mmp2sXO%;j;ia&mVoufa)Wjm5V$35sS~%| z%+h^GFaP9)Mo-b!ew+5Z4JTxvHY$;G^k6^Tt$L~OhK@2^ zYO1x+s((5GYhwIk#PnM+Ya#af$AIYM*>-w@f)a=Lb-3)umFl_+Hcv)Bt?+Iy_2pl= zu%~XgZqxrhRr)~2EJSOKDyez`B`_=e@#JLGnUiZW0sLSKkz*6ZuP2fVsp{}0(i>Hy zf!9r@a6J`u_1Pf0DZPuHuf1)n^HOhbC?apoLoKMxiEzm!?ZIJ0hHec}_%&p?Y_^Bx z@;8NHmiEQ`1yTcS3K~tElw#O+q1OfyW-83Bbka%aK1fDftZWwvCl}NsPHHu&eJ0QE zu*IyIWf=bA1+}|jL8#tG=mnPPlbbMt|RgeWdqmh?CJVc<^-&_GSqt@HFU z1*7pG{a8rv&p44zl8YT~s z$A>D8v;VLxd}QBkVs`ulI0=2UR+cNO?clcPc_F@(YzL8EF5%8vG?Y3)fZ$cLQN5{# zPwt5=hkNM4;+$jc&zm=!igQ)qBYG56%pzq;-Q!1w^KRS%8E`Sw%L*^VQ(f<@3~Cy< zNkY5n7XKyd5o1`*0`0WK2+u({&K*LHZ}?EpW(xe_7JCobOtF<~&Yn8UdV}DKdwRdyh zyPF4(Sqyl$ZDM@O_(749pxM*8sUK8RGn`U#`16~gcZ`l~TGO`fb-pVW6~*p!MxVFF znYA*!t!GGx1WUxC8^(jVcC7_Fy=irzTvtp38I(|G~$N16e$vpfDiq{luZ^W^KXpH}I+N%xe zqq*ur;b8@sL2^odAHoEm`j5jW&?C(nQ-*8Vsxd2Z0x#j@=u>Q2%nwdhykcWn!(8oc zEl$^O<2grieFGA2-safF(%P^2`e)-}^`|ThbMIZGw0L9U$*$XwC?T7csa@_WtZA!t z@#bvto$d<&6$D3l+v@C#It-l4?3pJD)EbN(;Ss)X{WbXgbZayBmGok41ixt)+;3-F zZSeC`9Oe_Ve#q+37ec-q{Di&{0PC@ry~fv4=wLq}s|N6Z%eG36@bg<6$NJuq_V3!% zRO(XFHJmB0O3z<0R%cd<+>W}WD$%z7{Nm`{EmgM~+8ac{pZT8`7Nghe5=uBHgBGWX zO`9A_@xBrAXVfnDmOh<_Q1RG=??rbo2WMU4EgUG(AkJ>Mtf^~X1xi^vzB~$O)RQzw50T}KXKG2uvTJjZf96eH`jV2s z&dGbmW-z@}FTe|&+L}|Q%M=0dG*jI4Sy^;R;mvyv=3tP*>2vj`<9KzF%q(wBWGQA*GSV#u5JVhY0wqBk z^$B}#c!&%YTIexOO#KJJbf#Amo81Z9JZ7x6cM26BH0Tl)7vL6EeSEG~CES&*A-jFw zN2|T$&Y)0z7}?GBJ>^MS6Z$>dPbG!!H73E;k%P3>5%4a4grv8VwR)ZbX4{hS4eEzh z30NV;#LmZ^&H*YP(1{y+$MAMY4|A`BLSx&*=tG6{m-@Z~3F#2>$KG$4Ru6Z%`i z>>BF)AQ4!beGDf70l<~?uQro!PNaNDG4yN*wa9 zl2bqduryvPzq+3AB${Ve9!hTbFoAu=t6sYfznQ6XeB{f{AY>kWS*sTE-9_S?m3J4T+UC( z9ZRRmsRFy`^3laNM0fkkGydV z<$xg90-f7}vF8zjV2(P0%ei*TmdJMYCq);|HjZoSOVOnPhPu9BzDbGK&3*UX zhRr;QR;3`2W~V~ww^g_4X*=Wm#y0fpA9IgQzm14dJY@&{gX4srdS`u?^nWn_>0SBh zcKYrqZ?U$d%l#KE(--LMS66`Ay2RSrhU~>-%V_IoS3S?lC`WGn@bL(2_KUTVPba%7JQ2we6T=rewSKpMq%A( z8;t9WYM%wKckOO6xb@w?X(lr)M|~ASqUKVSe|N)?Cx^C>>5F5N@-A`jQPrz2@`9QO zx~_;;H78)44BU8vaWUlN(^wAeaCu_XY2&7q>#p@|5%y?;*Wk$KjJlEOV?Wv0i>Gu* zS-8;Kji(gBQWp~}Y3U`rlc1RRBXfyYqfLQ+OdRr;e+VS}b`k%0Zmbp>*L^-`c21OM zP)?XymA64+$5<)a7R7XFm3b!}(~Ae5&Mplytt-`%sp+@`Y?|!QVIn^8lQrt>bfdHW zOu5GD&>D9XjPNDIJIl%_@@2rMcPsc?{Jl*GiUHxb-g9c^V_CiEx`$cExh%2~%M8Cy1i8IZMfU59aS{g{`?L=ZH;*Zhoy?$qw4Y>c%w zeKv@;E%M@IC!wKk3{5Qq7t{wEkhVevU$1~s^Qx5_1 ziw{YA3%@R((uTMp2w;{=R{Qtsg9M79!5yYjIBm@evuvcPunbrjZ`0HYLJl@n){uK3 zzUUr71f{S<&44_YW`5A1^|i?0n=(_aB8Zi_n5oqZezmkRWsQ5Y-FR5Uj7ZK5A^)5# zyFsoxa8#d==M^nj#NL*xkwe~u%aeb56Yx4TZydb@X4JiK2NLG!hvtoGF?cfYQqK1A6PdT>#@J>_KJF6n(-xF%S9&QwfBfyJ`UW#gdGGO zVDaDbRDYw1^}O6k>ScX!!LxSG%aDGw-LqGoexu%eODJ3peqEW~h-IuZUIe3EhdWZ# zz`(%S?hvIhJl@-hW`V-D@w`>J@NnCt)0mG~8$1+wa+}v?AQY0^CPpfE zBm0c4PMK#d6%}>5(;J~3EjfS5seUgxY!k+^D25-Onj&EXXJGO-jEkWX65DO;ZIx*e z{+NfzP!_dkVPkmizn+v}M`X6IUH2SU4VByGq9Z?^dCn3Z-!sxEZybm`>;6Wv#%LC8 z8C^$$hh-4*c+6>s^)^ccHNj*POV8*dGIUvbrLk3(C>#iD|8^FB$X^C|oOdd+)T+{J zM-XCVJfm8hXp#);Z0AL80E$9$irZ;?Kwlg3LRUq%{@#M>R5+ z%1+;+A+n=`$Gbbl9&d?>^cYGaaiFEE8wiH^|KSRWRHY)F!QyqNj3&zz;Vr@+Uu8#4 zd2rix=&pi2m3q3Kg-gjMe7cGA?$K7|YJ#;b^3)e#;gY6v=Us|KdN+20|Bte-jLPEc z-WEhaL`qUh!a!1xkZw^*3_`jDrIl_Z1tgRXDFu~okZz?B>28z;>3a8I{+{@+=Ut1X ze9*b)o^xiO9aoURD763wl@#BG9`x z!6A6p6+95ftL-?AG%eE>DE8>JuEIS_u3g%l_=a3^8Sm#M?%lDv~G~ zUT~5H9-HxAacratc10lM7m)7%UbQDUE2P)>lPv!GU5)Os zB`U$)n9Cx3oEeggU#w<+_6)N=jvhos-A%J73t%@^5q2aWF_!kWoBMF7d7su`1oqyK z*@jb9s?rqRj2v>ZQ?__0t+=?4xk_%LcE~^(&5QBh?x^MLV?&U%SMu$`LZ>^LJiFaO zBR3w3Gt45A$6&y>; z$6S)Eg7=}(YEFZobgsGoM(O;^)A-|XVu}BDPL+IU|3hO=3g~oC?6g?^y!2Xlvj`l0 zdWDkMl%D$1N~Gz2^KdG|JbeWSbiNc5nboz;$Ma>8PW3 z?-a>jtf7#TgnK#1ys)V;v+3fVq47t$_F9ZIWD4QT3(bBa?X`hD>4}EO7k_zpCt9`i zpvOwB#9BS!#4E(SNT8u-{@og;In}j5W}^#k%ShLkjio5-&qksU753=(I-JXRS&?Na z2(eEnB6nM-A!mEEr3@U^k3%vkWk^N`UMmQf@pMpxCmJnHrClwJ=r6#^sXrDvl?iCj zs|y&^oRnPTkDFM7=`Ua}AaSuaB!@YBV)POFa{3Dy#qL7U-u6O|E>t~omeF-!OoSxL zF;a-OzDLu00WWZjY$%GNw8BS1D9_WsKIG_MBJcmv58TY|6C8X3@H}Zvdds1AnqCVR z)^Vf#b7c6bXgPB#Wtn-~mS9EBVqnU}P?-5v4j{V#%)}&oiR(Q+>9&;h9X6rlnRHd1E7Sd_ zj}qM5IeP?@Jgf-T!jQ>puzx2$ofsMymmT>K4_TZ-xP6L8C>5H#uD}HIY5O9;E&oc>5C) z>NX2cZEbd}Rq^?c(℞*iAO_5(;El zHSYNjc~ml5uJ+k?x86~pSk4eK6zbs%MpxmsIm=08oM5dENR4TmRQKkUECA4N?ud2oqw#V^;&E zcztCg%TW6*Ap0ZBm{&hXUAi`Ns^$O$a(Nt=eF)-;?CAvTt;AyO^P3+WSggx`d9O$U zvLY5%oev=&__aBZUtCY45uBIv=rRfy8^m}{&4KT-za4)1O~m>40B5JV_moK<74~hp z0xZZ6(c0gc&8l`d{SAV&0Rn#>i}47Kwx}x@cTkfhH6pJ4-pN0HQG4Zz8O=~_5cL^x zF4dkF4=)@TIt_r=5{ICT5>#YgYQ+B{=l?KL?cr#@Uug1-I6W%buiR;u*reR3?G~@S z7Kk1FefLFpZtpqY$AbX8B5_OPHQbjMWGrYHpFhM}YJ2`wY2zj{a_!Q8>;zZ`?5o+k z&cov>5Y3W`i;Ch&NH%1C1ccvzF5Za08~K9tO8rRVR;tpWtNc{|Vjm56E|8y0r`e+B zULcufF?a|4ooQ+=2}^I@xX*OI;FyL+_LaAXx6*L?;tMZ_{nP4(z~=$#z4!M-<50?w zq9^zKxa`1%)8%#V&ZH#v@wOSh+MIbcX0Y+O82OBq-)>t!9!y}PFr&sQIe*dNvFm|* zXG0$;hy9EiKOZ_IQkLKPQBFs1d1Y($1_dkQ=dqt_}kPXVkcCq>XWzHK!>54+iUTI-NK(A_=Ps&BgRyvu#_t;U{s&bXx zeh*Wv)b?biW?4Yq*SVeyI(l36*V{5=$N|xKe_l@Y%$$o5!zvB6G-{0I5;g9mm|z%M zk@MsI$(>Jgg+345Qqysro$TWkDXDXdxy=T}Q>@hiMP;YT`Yy7~4L13ra70?8E^-4; z1b&MmU#Ub=y%%m9W^|A}RgX`dGB#cPn2==Hx91SO<^aw=)CO|d*M#7+FaO`44H~o} z%R5B2Z%i^&uDV=#AQ$}{vg5!q1bsY!{ERTFBkgAf`qb+0|c0(Ed zC0PHAtgzV7iaA>Ig%L?!%ZJ|evgG}P^k&C~=Fo#sFPxCUiXdcT(7IijHzoaQWNFX2 zkiK%xd$!CuOTU#pkQY%C_@Ur36D&BQ*intI^iJ`b%vjK9zO^S;cMq?0F^3zekX+<} z_5D$7@9%DBKp%8c6lH|wOmqEBFb4AHhqDySu1zkFJ-Q-FHnFpBN=Ln#O<%dad$1xb zIH`NXb?#!1d`299&80+@(fReNWWUD`ZvSP5$nTFD5F+IGHNBZ=HFV7f8v8M zJ*;jkoa$i1Wk+b`m7yV)(e^~AwK`I}xpaTiq&bokSDG08TK=g|d9v@!M#}5T95!S7 z14`}!o4_sQzh4;oX#tSL@uGZFleE)@T4)8u{u_W%c%79rT$_zF7jXIt!FNa>9Px_5Jw&}oG2QKCp`zOng4 z?qNghWr!h_sz*&I(g(kVME?rXsDcfUDa0PLVo9M3*g&TxT)r3}nQc}s>CXFnWj8Wc z>Lu4{`|tC<{B}7}Nxur66_F0B$x{_F zTwyP@Uq5T*WHj+DROnWUq|@DWg%kVe8})+ei+57wk{+a=wOwAOiTYv|07yAE8q2gH z==&zr@a;81YvaiZOAzkx_L`@g72uC31Z~CLuK$O<=z0-hnO(Gxb+Wz+htl;kR%JW1 zytNyku0hIcPAdAe=acQ$#Kq@TAA;6bZw-iKte;8wLnGGa`(MgO<+ zSz<%p3=xsP-V9qB%N)a}yL)xHG~#=Snqk5V_M9CLR%h>|8D)&P@^^jl`(#Ca!8-}$ zohLp)V95tXe{4c(51s<{Ui>}3V48`;Thr02A5$cHki(aP{rYzXlMi`5Z=S99WR6tM z*gYNtY;+OilY~3pr1KtiqJ9;U#f`gc#b&=}PJbhcaO&n=54^pJi#~zC8kra#DzSYV z8qBwxFJ{abmNEYAZAdS)V^ALaUDN*K>3|926)?oQC>-1L)hDUU4V>QVnn;k^=k48s zDF1sEk3kw+y+UhP=e2Y=x?sN1M?4-7ys4{Sn9D2LUdggl2pV+Kgw6`#H~2u`M}qg- z-x*;TgR%h>wW80Xx`Hm>yS&x?kRCTjos58<8XZJaDU1|7#rXnfxuAeT#WmOdGcNxV z%aEY(FHr}?Y5gKLX*c#=1>Li)TX;>lozX4V=KR%)ACCj|p|{BS4C+PMPMmZnPhhGi zsW)(a45K42E!CMA_mqyfs*Qy+&v!}UFnNSal5>G=K#%%-4fJH#AUCd{6u7Xf(UNlc zzSD?k>e@5@?}}Edz7w9@nIoeu(cJH&aS8>X9Wq$y`KQMI`wbO_0_PgKROMH+w@9z#p=SVbI?XqX0xFQ zW5Ry?Aqd{tDp#cDVUKQ?h<{_9SSauij!=gj_w@>DlI@_t^C+#En}HZd`G%TMG2}RT z`!D?GQgnOT&T^)XO+SwY8ZcIddWmTXT|}oo;w7xFCL%T%0U10wUtscp?733)t+3F+ z3K!~AGt6P42e}4q^BE&yuwEt!gmwICy@{AXbyoNwf6lNtT(&u!6QGpq9@Ju zgsS_{S?wsj;;9%GqWb9V$aK;iNwd~FH>BA!;sVr#Y!-)n5naujbw+F8d>pEC00J4; zrIvXI%bxUD&#)QfLCI~Gr)7|YPN%h$A5rz8Z81o=2#re6_LA5f8q6RdlJSNRoN)9W z$F5-4IClvgaOvGhujs_W;bnCGT-9RZ$7o-4`KiHB5?As4o(?#52fJnqx z*HVvtvvSzzw7ZU0p%iE=L;L$odX!6`YT#K=gUiE$czAH6e z`z5**WcxQJ;#>|yR%qXh?0#t*W>t4EGglTmfrf=yz$zD`N%)rG)_|bq)_(O8+*B{} z+j!9uNhXg*uY-BG>~hB*Z!x zo>?dVBGE(5d%bs;NQCi<@kc4#H#Mfvn)(ykT%VkLeIpuOC0Ac$XeS>TL)_C%RYcRB zt<-Mibj=?Csm2*=I$cCP96rLZo9vVjIUtd0|0vVq?Fd-|0tfmocS1XjqdE#4T)kb z{e1H09BXN`$?S)eawrI0Z z)>8Z&sf6^OjR2+A?~j{WbbxOg!$oeSn0fEQVDt`Ts>ru}aJsKVSkQK+jl^qjOzgV( z)+qi7DvOZ|wJ7Ik7$-<~u$XWwiUfcGlyMR<-g|nCM zLCh_7+t}e`uHW~+hS@DT^|s%F`a>J!uKuVj#@#{1S`sGh_cq`80``ek(w{UZ5hf)` zg_=T8oSu6`lza8BH9lHz(irGPonF+kmOqKq~GV8!;26Q7L;Ew&^Z*4*PizZD@pkTRk3_U$D^L3P7n3}P!%(x*7=gu!O*Rv%#Rv} zntxoepd>XLE~C`eVXAa=8S?+(GN(|XDeR#cw|Ge@AuVqcRICiUvpiLM7Z)X04+e(> zq;}QaAoL5e{&s%)ms#|m8laLHQ}0vQ3BRY)Dt-MKR@dhTN=BIHZ4Bt3ZO}TLw2&c+ zRHzq!;0Zv9@W)s+6#FWd=n*#X_whf z`=>Yc*SPzyw6~C!&G^f$5C)}2cRK8iw~HV+J;(m^LmJo`hN}UU+RB> zjR~T;Qss_XwQE!UY}?PR46rn$UW!j5DAvco2w-0 zRCpA(sZ-=d7qF!;1Vc%cKt0cqu%5>3ui}VxplLXGBJ$in5 zmOG346~FUS_5QhBX_6CTV5YWlx2p8d2Om9dQ&Fw%y{{i*XP)Hd1FESGvbVun`%R9Q zn=^|u9QWt#p@F2k~!Sl*3%!t;0*v$i4<**2ATFs~kFF!bk{QJUH; z4V46@f>okz*V*EMc@WTf_4NE_=k9k(r=Sb@avL3Y`TBArXKL08;~Yn+1jWE^L@)*r?Ck=B@)<7a^%pTG_|#17h~ z2H-LmKc1uR2C(!OloaNSRT6|U`lf>umdl^M2(K(!1^Xvhg zw-nVx1DaOVpiRmC^Du&oH~dXXxFSpRLmZANt)E$HQ}5!)Yl?+AOYzG>!&LmEZy@pe z8%DPj*Z_6OKJCZ_B*OlrLFTHCD$*(U`2ysIo&vF{+F$ADG*B<}0dO>1xM}J8W5W6>KKKdQ#&>FWk za*9YDEVb?;b{$~L)w{MfJj)$GjIgzk@yMw5@!(4eqJ8gspJ^uDX6AP}P)i^(_C->N zFZez(l_WIlJ2Uen@($5ge}>|O9{qe1`UQ{_h2q|L z32J@-fO|NG9;m(ZMaA~}tU`pGVwxHi5u=juG*jeq6$bU_=QmB5_e&|ja`NQMLeQLi zd%Ns~wWPda`aUTSsMSK{P%Y1Atupl-pU%tim_n+ltm8&s$`+fcn%eH%S?9i7+*8xc z)v*IMX{&ye)3OyNOmngu3bzMK?M|i0#e5!*VRh}z(!PoB*?wh6K8DxsEVwX=7%(#> z*!t0cH4cx}x2qQ(UFl?=!(Tt?Oq*eUe!=(+{f!r+GjZ0s%^xjbHsft9_q{?XuzZP< z(Eh|8v_;*PUrIsWE6j;x#PeZiauon{*lfBTjR$h}%k~S52R?s%s!14TmQvpp(X*bO zzMXIt;aDkUYK~u7y2RB4~NpR7aiJQ@jTiK7Oe z+>(vrelcDul}x!4hFZ>3nG31^Z4ZqKYr38Fv**@)-7TW?4N+<0z2yq?y^|<8Ep|6K z3|d?WaOD~AUM67~k3ZjZ<+DrPw%NFwsJOwpPdhWMhabbq0SW4A>2Vs9347V8n)|^X z<^rbUhJE4fU!lY{JaoE9(LO9g$iNPOTY6wkhT5eRyekJzZe_|N%2W&ty85jo7*Qz7 z9$0@T1zlKXp*!bC{hpIQ6`B3=V=yc2u}@sicy~)0ENjH4@ySIXU-AD&WOOs^j)7bO zR-K+o#VA7&R=Es4UKA2kFZ`FF%3EcpmEDd}{w%uZn921}zGQJQ^V)5TZb|l|)EHZ^ ze9}sV$ZP;Zs_3PgP*XR{_)1)m-ms^^T13!7WKeTSDWh2wgO5V@YUBq)Mz0{Pm?3N1<^u+g=>| zz)Wu2&Vc##v)(!u>s+&;dB_Bn6m;LTbw8Cjo;HCnwrB~xxfmPp}&WUl-hYU21-V#dtyFIRfqapG6iOKw~WO1 z11@<4=pSwzK0bD8hBZvkY{TTtTI=DfAqQP>Lzq5TB_UvgS>m+@V9>=Ln^E{emV-`^ zxAx`Ae` zl)?@Am!y-+lFBqlDlN|8)*ekj%RU4Bptnl7ZiJf5neHB=!^!#>KI-eqS08>uE2dL@ zjnlvQt?Yi$**j5ec4>CugLi_So?Uc*GN_g{u-J)7V+33)ES4`v@ z+rw%?nP?&(@WB`#bN9ZaltWMpwB5(kjc&z$4?c~^CY+WK4A!Klq1uF5ki+I;(=tCL zZ(-=+zO+LY*dFLGv@h}Hs2(VNeycGnLnwBY^J#mrn(9rAriyuKC2i|I-%C4C%(1la zr`R=}aK*Hu6CE}l$Uh0#|APK8)5{_3`dHg@Yb30%4Oy+f_{FZ%^0%~*bEkikgRTTw z&UK&f`sECPfiGM>BJBC|vc5Tn+_qk}$HGWGht`4ql>dtZ3h}om^J< zbZ^fm_f&+`Ia(Md&GepgdkY1eFE3)t0Roz?X1L6;cEwp=(PrH0{OL#ARCqe~8jE|? z_4~J{+)VYuZ&doa;yqQ&RHYF*qa)VzVB{qpvYkl-T;I4J&UK8WQB4YURFA}Tdd${% z2P#uuePl;TxO4hEjpX6uErU;AA)7YrA5~?1a4j!(b?sn?p~CnpmJBI}f^RRaCvmn| zO+nhOQvhD#Ubvm#M<&|I*3E_cpVSuT#cS4$L4DXnP(nGQNqNiUrjU6 zIT&!Qh>&Pap)*kA%<<%9*a!pI?9Gd67F$O)>)JWHZWS839Ciq-x_JztmTzphGALGj zLNIGOUxB91epBn|RZdO(=)0v@%+^eiw-25UNAT*h zLkrdW{glyYXWM7ps?WtmDf=aoq5V>+WvIq}uB@hN;*`*rbnT5jZ++fmxQM9C{=3E7 zO-hQMkqlrHhENAQZSt{gyOB|KH7Oph-;{Fr-d>xS;KRklsZ z1Yg0zQvsnR&#xxJ_~Xey23loP3a(Aa8#kkE`2oy`Ix*7_NS&ni>?S+CpA>=tb(dU! zbIDPCpoNm46bEV>iGq0*u&y=quJOg=r+sFmN~0ECJr%;NAtGpwH>M#@@N^Zdv71ID zVjS^H${(#au8t%hSAzTX+mX?JR68Adndp(B_xy11!qxqPEB#i+bS{Mwm4(f$!+M52 zACeqrX`_?0S1~ zr9ya)%(xg_2BWNiUmUd}^+x&6ry#SV`A-BQd+Th&JHh&o+PZjoTSIf@$yXoGvxP9J zqn@dk6L(`h^>Jut-vPH8nX`rJtD-q(=W5O`lQ0++Fe#H>l^?F$zMGNWnKVWMvgR$G z6T=BLexYRkum)8Wjf)UvJZMI-QAYBL#jalP*m&s1VIj-MRNz>bb4k4*Bu@l$lD5_# zZ=p$v$(yDIqp>YhqxiB|7h-N!{(P?KG^Nh*8)rtQ@x~NAE(vjIg)}pAxyT$^TmWLP z`-S@v#1_`iq}+FK>!R+_`~pL^)zZ9^=EOg&TO+w$M51lw5oBgBEnF)5L9ea^k7Inf z|J}=!F*9bb3QVU?5b)CL$dHvFz-aLmv!*;hs#@LxZ0i$t&zVKHzjWfXa(b?0OH|4y zEc9xAcpSWKf}%YE=krP+@JaFZDzM{j?A9H}T_LUlBIINR^8a6%oTGB}?-8Xtsp-3h zQ#&F~fPwE<$T{vvieb0r}lmaj&<~gn_voCK>rX3hPIaP!%)dPTQ}wkptz4q`_H$MQp)`oJ9b&yqEdB z^@?iwZAR;5AgA1^ALq9kzv^|GOuKv=&n0z6Rq0@1F_v$YEkCmJf%F*=Sa}L9Nzp+W z-{W;4VCM_75t_?1mNByBYMwbfjI9D<{2YMr2&ALj@KjfB(vX-IEdLsbzXIfM8i@ zUxwmyNp0iEiHb{U>x{>f+afQE(R+rAVOc1mjWjj&U+2es%!TrcWCR45aDKDoZeyQ_`+eSK0 z+b=@(ipZP3d@zwNeknPK&6zeJUXMZaNfE_mX;Bks8$9+NiVZKpbdLe(qm(K`03k+8?})nYeP_o1}@b_poNs9Bc!r~h7BF1?SCRgr6 z2t3KSCLZ{%4OUxLrdBK(^x4MhTcX)1UW}d}*1;n;!6I(VtlN;MX`?Q(*iUv8(<&%n z9O;-*FRm_4JRLw&ImgyuuE+dJ7S5~O{kGv$#Mc+)3DK!&pY3awTjz@Dr0BEh1`Dm} zjuu~j;p}YJCPs5qMjd4=4-rDfIKE)@AUpTr)M}SvsPzv#S!0{b71_F_0<5hBX2O!K zIyXfC95ixe%`tG(HoGBA2j3s( z_yRfSYMB8wkDOnuCKL3miXFa8= zedAN4Jb6OkRR4zZg|+bF)yq4Xu7_J4y8fgStTkUFG|CPVY;4GU8$wSY*oXW=%mafd zt(6O+stYJyTHiiS&RZ-=sj86t>(|H2dlc;WQBfKpBLSf~r%;sKu+K>5BG`z39Vlev7kMyD)S*pcT3(t=^A@?rG(#{0*S<3!8L zRbtyKpwXgmi?6f*8bFscav~h}l`*G9c?zvC%BN@w#$9vAlDu{LJ`<5%u_UZ*ktJB@ zntOcya+Ge=1^HJLpmI`I$Q<2Nh$>TZ-~d&&rV%mU^5M=Jt4!<_GK>vs@#v*~X;p*P zSTBGcmE^g?(TAX{q{3O4KPOAmnYe)EeLXM~p~FJSMHj=lNcrF#BdwbuV9D^GFZUfy zplpx15xntjYPpbn+(?2a(zYL^97H)n%18|K*AEinFoMkESZ-G;%`j?szte3A^Or^j z9cE&g>&f@G+7+qgOZUhjbb?Y#J{c0d)N3MP+7xd+B*0ak(g5@}ORTSty z9pZlLc2G&Fm92UYrV|2B)}Mk)GEB3?j`Icm4caE(q3!e1B;i~|t2mA;T<$(!ANkr{ zrjoM!+VN=*Z}wZ4PoRO z0xvCgmV|6wh4PI0iJBC(=cY}ji+1bh%XZg&0q`HU!YlJbBooGk5-!F8ejR7E(G4zm;PXh?P$T4h8fR>XQVqLeGpfI{(N<(sUx|N565%{vZjU=)(jgx^1ohuE%c#8aEV`{^@sz7;RgyD#V( zobJwRsSub;pvF*V%o6njtLA7D(}9!&yc?xBcm5zFe~aIe9S>0M6*mmCKuKy@>5t>L zORP(u52*1DP+r%b?A7jWDLY05Y3 z^QCD06=raTCTp+-KH*Pj$d@pQp)La3v4*l#ocssbKuVYrA$%yW{rj2#SVj+9Twi+) za6HfoYP`ip`;2m;icySBFW#l70CdDf{fG{q58fDlt8wT2zAoLOn3h62KMVyW%B$MT zay5&*HYt0h9bU#`>D2nr9G2T`vd;Erd;7mMllN^bDomlAK96DpFWy*yMpDbqN!2Jn z6r-^XT$U~W?a|ni6o52R!brRz;3}9G2rYUC3g%rwv-`T?X0NNlCB@Q9a=Q%ElhESPqc_ zX{8q!DH->(d2c&3$WlB{9C1vqGZyl^z;byMfWR2|8Rr4|=88j|hjWtfq4(*qsy>oM z;4B4WMDFM5h7aDOR(Y?G`@wqDoO_K13Q!?$>vX>H#n_TyBG0l3!OvKcZU}j1twy+2 z@5sIunXaLr#W+!q9y@dT&WX$#?1`J!^aw%jtNUNf-rn4-{IBh2Wuv*dsBb%H9g2Vo z79#YW_o;j3Wy9!PqJ;VQ){Uzs%jvTEcMZm$5?!${&@5_Gy?W}}iOm zDz#loCzEYeg7_1$VN=NW=vESm_^{&j2 zxkyv7*)hFHkzz5i-&&^mU_I-Wblo|>sowR|#I2%PB}DK2sgh^!J;i&|dnZbRSzU(N zS}3ns5_;oMDe8Mk7o3TDo)mdEQ{^-$gHS*$t!ZT_y4;4Nb8^Hi6>Z;l6RHjpQIBwT+r?o$CF?`^i z?d#9IX+XQB70U;-$M!G+D;xDwU+0gHj>;cyzS~oy zCqg|)x*m8dQHqR%c}2^7Xj-e*m#C=`YV6mpX5{zeqo@n;wT~p&VJNq33~zAPc3hA4 zPmfk${iyyS51dQ%OphQ=b^hp6h49?+CaOLCX1AOS)>>5r>fhXq#fo!WT^v=Q$r9a* z+t6GK+&lN(fIYXk*=1XwxqC{2OCj#<K)ij)v~URtgg8TIVe5sZEIQZ zAs*fzifKzs+ftovJKSyV*HC$>-{&$AG$pn!t7ud(^5wvV>u{wZuE;{&g)|)M1>Ncp zi!DvBHu?$#McaexG}Y>SXZ1C!H`mDADhsAeKEnQuw4XjMd#s*6|Is*`X7L zk)pTOV%qlfvQTKM;#0FE`_DKl=HuVraYVco*vfXC1Gyrvw+Vf8-P!9#+~d$}YI4t$ zrLh#;8a+;?@fJjuq(XOe)k@7zK6%jECAvKyvVXPDQEhJE%csuv>?eFOk>8WLl-Y4F zAKxFp0a3_Q_jeR}2%~kr(s7vW{piF9dBHo4EVX~rv*C4<|IFX$kvNfadpUPTdt;i~P%d?exGVfi}u zZReQVkE!`9seyv(!8q!Np#tj4l`)6)oO5<{>f8aMr>*V;4*2xa(UztPLsexXFGwlgaRDIOn(Ju7u`w`ET^J!E# zr`HlA5BS39)YN(0;{EsLG%ER|O)$}qAHY>_mYr(bUo?}Hg)`0eWwNvThfAuZsF#&e znPwb%0tA}9o3Kz0Q)x95lt!qRI|I~L&1kLm6LOgfS@g;V%gp-Sgv>S^(fp(R$In-| zVan6pPBj}CU9O^Idp7ZNO!nXkKY#0Z!cFQ5WJ<2FJtI5NbEB$UB(^?E*hTSxD#wCT zr{UmO7b6HpFmKfN)cGPVPp@TZvD>`TX;zsdecz;x9)Xmd$_DL5B>3>`!mZg>XNsR_ z1EhsL5L9Dn2AkZ&$1^r66LY$k!dW$x>y;IBh5ZW?suD7!BlxK2tdu>LNk|js8 zbd)pCq3_ynTo=g&r~pZ@Mr@ivVz-yFzn2<@$ul&b7Q z12K&2zF%Otg3}pt@#Gd`cxL@_hu9l7&EiKAj@w<~%jrXS0Hq63^FZW3?vSBhETtEn z1?Ynw@p6Sx|NX@vl9E|>MdlaPU?BcRU!gPn@R7;pKAs6lIRnLWsD-E>Wp7GsQLgkM z12X;byA1Vag9TjGG#%-kxS&gak?>%Lk+4hDN&id#fy}Agm*Q*bz1plh%fZ%ci?D|g zsrZl%Dn*|iiKs$D;>U*C9v-fT!UID>mbLMi1Nas@b#C1{gFfzsp7E?TvUbAbCo(a3 zBGrGR-OO*G!tr{eu>BG@9Y6}(wEqCigbh&+L6FyQd_e8S0H~DQJrhxA%LUH2rd?_9 zETCOli!MtM$8F$7%$r<_P0pe0$H!`2=tp0>gvbxMyB3-axj9u(Wa+F=uADLM&+20u zbs2d523uB-V0mezBJguvV-q_#3?=A}`YCT7&|x&);+8lSD*P1fBr+=^qcZsJldbLb zF=5~P)(2yTysz!|7r)1GS$Pp}9>fghFA+6JZ;a)(3D(rUo3L0Kb_!usxT1!l+51iC zF(w?#Nc~rnmn-5IshM>Pg<;h1mT1e_f?EP=N$CO_%aY{wTtCccAy*$AL$jrv9y{(j zWP^zuzodUSS8mJJqa)oq_iYRZ>b|i_ov+WrCom^)W9rK}r?skFT*7C$N{>O*#9Lnq_b&pJZ=xau5rruX)cHSo<3qL67ZdnnT?~ULz2_T%wo%!e`vhQW+ zJ&=))Wd#utFmO68UZFX1cba#aRoJyxR!^*QWFJ3-riF5W{WJe!7+}y?*18psw+}P~ zQt1M2JXOq?VU+LTnZcIsZ6 zV}1FB7e1#PikB0$CrQhb?Ic6P))tucERQy#hu`BJa6LP?V;~de{n_^kE=x>>Pus!f zJ7O`R0<{qt@7`a4A>5PVw(+d_l$+Jy%hsUvHBp~%nKO>m$(hHzSo_`|3>}_6Dj`*p ze-Kg7v9|A@ub~6B5v@VTXJsPftC{E9d7oO=>SYG6X<8gV>=;1>uUfntn0iCJo9*Ix zVgcsI=gy(%qd^-kuwz1!R%6dLrv;|>@v==gi0IXIrvz0h9Y*WX=vs@Fpp3g6IX-dc z%0pdQaURMhl)YPzU+zdp-L>ogG+M%&Lc|gm3P`ZP0;RM3j)jmk|xuXmfMCW^l!kP2om-e8+x2mM+4 z8nkD6z5{J|yp74su) z{l&^{ZAa={@81iQaRW}pjISj3r!){CU6W>B_x_P0Ma?F&vBh|8Rmj6L!57U;A3o!` z`*<>|L%EDrIb2`xzVTP6C_gRpuU_Ai-_6{JI~3gAe9~!9SrQ=(*gm3Sdaa3IT+PVp;+ZkLf_4Q|pW`D;7oFZ{wiyEefHETXB{1HG6Af)_Uw8P-OSpA!|eg?Xl1_T7elBeh37 z>t{P=!W%#U%+De&T*xuk8MSr1_c};jKca696Jd?{x0uQkcz4PjTscO@QFdyd9&Hzt ze2uPj+IXoxhqC9@Mr1fBd_hSnHLR!kJ4+dSb}X}I@RMp@*F9vY@ZP{MT;XUzR8&a{ za0R!HAy!r(()8w@+3=pbN~eJsUrOZz#u=8FXEAXzwOx!#KvOIs?aT-RB`rxno7O~} z$5|uP_K1kN9`E?(@oM~M#0ci0yP1jU+fd?}y@91Dng?kSIn7A1y4mWyy8PMaMf$|> z13D<{|FvXxMH}=*?yph(YPSS(e{XdS!?cg?@4f>G7rvZ9^<*}{w%zg;fky-bNjlIY zCVZE_M%*SD5Yv|}eMY$?Z;ANf!R*J%JB3N^J0)C4)qX!p(s=wAXh6_$(u>s}ep26L z$U818VgG#=pErLF3!m#^j^0vHV45HZqy7IpGXW@R?+I#2Y2Hm!6RGDR$sUmyTm|2t z@}BW+>1kvNz4fqx1sNiVEX|vNn*V+?#XTM4%1eZd8iYR;QfSi@V@TNAD}=yL!idTR zAgj7sBQ#5`3|TZ^$u_y}HZjp_VM4FU%bbqyhbE>lDh?gq4i&;H@}a)aa0d3%L}ZR+ z8>9r2c7g_hN#TfC++I>nmVRRl{>x-JS05pk&lA@JJCahVfiOmxF?A}e6`2%ggO>`~ zcrDRO8#l-0);L!GhluaSnEQ3L4!xRQdeN&+cM?fkV+7s3tfIXJGM{>`>V7W@_8}2^ zwO7L9EmDf|7$%b?JmUI;16_C=F9B0Ox@Rty(xLbBv@)lKR+gLx`TIwI8nYjZ%JtjS zULv>XY*Aa?4eh+Errmft268zf=Nz|~E(v~PPRIixqvNh(7ge0W(A#?~PA_z=T1_$#?m&A2{2ldmrcqfZcpe@R6 zP9Xa$kMe|)DAm1Xj?F5q?}Y{Iv%eKp0`B(;%B^2PHIRAX9pw9cbxInWY%~z6vM7ga z+ig_6))yxM2H~z*IE-cAaAEKnVy|;6$MR&%{yv4H5jT6ZM)qU7?_}+#U}&<+VS%~2 z2_R_z^;p@oDNJY-qA*FJvdF+|=@~25`SZ0rcNT%>V#NL_(EpYjqA1JS=6W9(px#dd zo{FD$oCM88lWN~%?!y!yjU*%eLA2eyY*D{Smv zy}iH1j0{+ep@GuD5MW$%a6z|MYt#i zBN#=vCudX}=h$XA9-M-c(oPRKDM8~A(626U@AqPnFnEk5?c=jL^P~C4de}W7zBEf2 zZBCB@4L@$P4nh;&y!w|7@{=?T74t(gQfukoZgVcicB3fDpvoaFTk)c_sGm%z!64V% zrQ?Hm&pKtMts*+xb98Wlk1{FJ`antjDe~f(-YU!LGP^|r)6n`b*#W}u0kS+VTyt3m zVI1lX>e}akP<=g$(p3F@Ls=(ZExJ^t8>|oo=F59O2|QpJz_Tk@dCZ5R`vi=pu+KBD zYC)AoHScIGa39&suCa(T4*J7tDUwBsO?0x5d9ORjg#H90<1c@~!t+|McaJIApu&`U zNGz|75&^2ahZ`mZ9|6^65%L-N>yvcqMNbCF^={>m%hABsBuq;KXFO=H!dVgK8oYR1 zZRjM_L{_*Rf;Q*iOX6rGK9@vHHr%+2e@NK)TaL9_m<5S^GUf4vF7lnM9K<00=Z{Ya zoHw)*wUA9SfKrbXzGpOzY}6O2ST3uG3iZP|USMKo{GfUuXLRJRZ-si07;pFS1+(9G zf3w(n@BvK|DXldHHZ-PQ(<`Dz>dwc%umBKhLqq=|*7ftFZ;1i!>k&^bkuD(TKF^G_ zM+LqM12oDx=Oa`d!qGbY>lr`+hrTY-^)2*`0s1=mQ_j?$v$fLm=qJUW3tPp#pXG=T zPouY;It#;D>U=DfU;6-gOqCA+pI1mm#PLGd15&W>r*ZyOoJS;z?S-pmPa)TbU8~Sn z*Q{2rp6xMvfXA9mPLIcl`b)U$Je^lSbUAzD5*6OJwv>5or_YH8h;CTG1)_d2=EHs!;d23er_S;37x}&&!IYD%XAa<5RvEO zCj19O3S@^8@smz&eG$Y z6N^L~rr2rs7u%CLHWm*g5;%V_8-BblQo``z*BX9+uYv`={`3w&&B&-(Eh_|@FZ|K# z(eM*&dRxLil-r;P`U$NzU>CG0EMPPtUtd}NXXSj#-J-R{OgFn-rYM z626xPvHx+8M;DF0`hr&CF;X)c~3==Fs+C zohj4|9BW>$bjs`xp?xal^z^)}ZHi*udtz+*7|$VfK>b zubVw~(v|`^hYm4KecBfh9W9K|KeH!9HdXVJFzD3c;key>cSn0F>YQ!eN_9ul^Z&ZLm<5>YvGz=Du`$1;>xkr${5E0sH8n#D+ z<{_8vQJf-S5pR1XV6ixOtrkc`%u#vOnb%$DIR0Gx9I)?y9Oy0?|8ZtvP_2$1%n~*EPMJoA7fv*!CX$xucH4aAG8KSmVGG z{dHLXV-WY}VQ7hry5kB~`f^GKbE)6ATpFf?8HQw9ME0`CJnkgRp1j~TT9Q{_)n|#O zkI(;pL8Fe&=wriisegw1e|kPg;;=thjKpX#Ve#0A?H_NBunBHv$+2=E5*h$tQZ@6X6HG_6gsKK=dMu6v$N%eQ>eFJ?Y_ z0zEU=t{9BTZ0`cYE-#DubYkCarxwtfCRcUtcLgTBR`>U75#X~??n@YQ6ToANnVK}Z zLz$|DI5TY;m9Nxyvy9huuos0u9`K@Y=d^4PkA)6erkZ=z@jW8tx(k0lYg*)F8|wY< z&-%gh$Wvi2v{Op)VT@m^l&N0!Dlb89zkjwPH+2BmZ{^!(buR{l?L!Y9>ycu5YD{+e zX&lNcm{XTS@*5X>(zzx5R~o=*vKhBladU&MNPI{WpWs$$YZ0PGF^#p<6y$JfzD`n(3{liON%Q zZqw1!YR_g=XGnlM5~TSES+}2KujM%OP=Vj<|0C?HqpI50cAuz+M3m$#@<{CVv1~f41SCU!ouzo+1v7JOIk~iPLK}1(QLmO>wWQ zOtnef+(Bd+Po-Ev4gvB4*^P$U!RYDnxKe$wY=Lh%k``l*H})seC#qUigu)j0uJn?s zESD+nxgV~MU*1vnC46oBO6HVdiBBF)__IvGv5_&vN)7D-SmwJ^D zDGLkV7hg3H+5EK@OFnkwYq_Xw&m-cm{s6iu$Xaf9O^SqNM+{(q2b_KCi7t)Bdn6_4VvG+Y;wOrlYF@a0|$aeThs$SVw0}WkQtzo*_?!3 z{^o})P%t3R4T}qSZwg0E~3(&_t2M~tmi;rJXS0PP(GmpWQQn-wYw~4?e z4K(=?Mh_C~9fau#I6_#v?AlLMV6&t4xwrGi{PTOKpfNC>s4b%rp;;X&;IIWvJ;`DK z>y!QP-?~p(Puf!Vkh3OTR(`nqmI8?&ep=8@LKcnFceMP|P73;;)qLSZOb_7Ez~>E> z_?<}$UJV>y|+I)Z!;Zv@DyI(JRU7hr}+kY*8ugN(L|i5u-%4BDnxcv z6@(CxEFc>+XtT1n$VYb?b`9sAUd2;^R{8_bhA9Y{Zu!PZD5vR82V;Jgh!-z5sP~ur zUv`wCFQvFW21RYjk7o7GhAo+$4P}F27pD2H`e(xK0BEue@&W{G~ko2_EUI?~>=Ai=su)bh5)Th2p=F`Tx z7?u_%#k=@1gXQUV=U+qBl3<4oD2r5E{4?@obg*<`R|0@{6gmq43c!_7f7E*a9AUum z9i#5ZTl)C<=mhWH&`2` zPSsmuIfdRwSUx%knt5um?(m-}<#~DkLEj4jp$h=#?n&KC7PRi-@;o(d`_!_&@68;% zFO?aC_q%p?nkDcEz+`$-_v??A+w;@CLs=0u&FYeMj`FS2y(0l5tdCnPe#6_BP;66P zIBj?Xkh)nkGDrcMG0>H2+Tv~?B$3B-v2Z+!3G0st)RPMb&U&u zNDW3${p*?iaVzKxY`fiGkb@s~bQIc85nEp#)o9?6h;;*M($jW0-y1H3x~IcN8UIJ8_umy`-WU&>^;V}+b|+(Q;{5H(0*znQ%(t9rFZZ*Y6G z2DoCrSEn{(g$9OLUq*-DBKm%xmB&crRlPT1a&~cIHIZv{JCy9JUVM2sC5Cm*Qpyod zGpip!>^1@7P9vHdwfbx*F{@TaT_ZZPFj4KI*6iGzGURZ#zQ@{Cs77omwSUm(?B-DU zoA%w>{M47dd69g-V^o@02sFo?fov=ud7+d9(dnYf$H;PQ5*ny>#XD9nC$!O^pk&q5 zyv*u#Dsm^6H1_Yth0x)64YZK)SXXU{op+i74M3!4<&qI6@z;8^1 z=(~8{Q)N5)PWj6tRF}Qkuu}czw{);>V&zc20`ILz=f%k$gv#K# z)$Q5Zm$n$O_=t9`UI)1LS5GG*f1%&x9B5hq(#&F~H`9TqsY8(z*<7=CLf$UQ4^4>^hPH7YR`2(>70TJ(?KYI*B@pLg1hDQ!gNUs!aB@wy3 z1~8@DCJ;n8t;yli!KLi5#w<)>1Y4qok9j6jMTd+n_=G5447v|Yk^c2P>Q51d*537G zQevDFwu!b|BOuzqzXl@opRd8&ujYAE^n$g}v053p7-21HUQ^;t6ksB?Jm&~=@I%0W z{QXxz2U&9x7*j_>VaGJ0GFE^D2R?` z>c))!eGcG(aEpRRK{3}(nof*T9$A$UE-2lWj)3BY{TBJ3o4_{t$xYoA%+n!HJNJ#a zxtw@^K+1Y^Dw^}wi6`7MVpZm!H3$Bq?E+=rYuBsG;`=%H!f-p&aCJlYtr+3Hp5%vw zKLiW(843Zj>f2LkXk=KX zLk9QnMg8YWv;GKaLv!u@q&YF*VTq7&7oj7vA>r){Bofq>TOyFDfXDg@&tkra)n)US zpDnaO?&O_dtc-_Rnw0W|BT*KC)Prn*C;ZioOH-)(L-+Xd#A+D%U#}Gbv6dFRj#_R$ zw2e1lfvBQ|Z^|G5hNBax<#)bh%j4O*A>lJ9oP2}anYNhx*-X*= zm=P2=%p8WBE4u~UKL38+LiFH^g?I%&Zt}ywSS~D7hE^R(*stt+Cy<^){%L`PB2A0n z=O1`)xN0h9Md?cS_vXmyT(TvwfbXmz?pIqyruy%y0ry#l1>Rp{ST2vMIYFDK<3s&1 zY$QbZA~ezhx@(AU_z{t4@mTyS2o*GYeo2jh_GW>1P>u7=C3YyK2toC`@%X_sN%VKL z2uT0^gK5EEq1I}bQR-uat)EIp&p9wq;12*z6B)i9voY}y{G>78Gd@+e68xI;^Ev%! zAQlOWUX67X;8Siw&GCZ&y|&jsAhgo;NGT@?c;g)4MlXS|G<)#udG-~4o2Z8Q(VHIe zpA7YikoG8Q@hqTIQCLir3*InTg8kuG$!MQOP%t3UpHTW~Fz#;742z!E<4E|PQg;3~ z&>^5W%Yq;0`myb(-u3GU7-1Mu6;_s2GI##clATR!gzE?JiNn|n-`0CqteiJHSr?(7|9c%;Zm`CxlS$r7 zPDBdm0<7a#2A7&ZNLeI>GQJFW;KYAD@Jvsn5!1){)XfbVC%V6#q1w)PC@C^VJ4G)1 z#!Ytrv;Vl6K|F4}083)$RW{QSc!3(`dKl-Mb(uscUMi~yoYqN33JjtT)d2>CoY#b# zNjX-%yZP}1e3PYNPRIvbVP~2uMv1lZRQ>b*V7&0%X8uS#-Kv|MgDe4I{IlqcY@2pB zb}smLUUYloQ4~J%5mrASZ8CZ+qRH;D)@;i_b#qnc%;g9)08&pW=aBq{@t>sAr8*P7 zta~B@kg?z7&F-w`LLzCz@Eg$7$^YJsZQOsZrTqW779FB%a^<)u6VtJn``zh|82R~@ zk*G7Q9LF<^(+_flLBwJ3;bR1)EFgYFN9-$IC($Qzqd!piSEL2uZvd?G|Jxl^cBYg` z8}uef*&&=V%YW(yg8@qu!e&8}DvGo39cUWAH-XFqwAoV;dy794U`e(fNL`{LsWQPn zv$MLF=qNN*{IKlMwIe^B!N{)(>HR03y$*nkq#++aqZ(TCsKL2heFP%fn9e-_jJIV$ zMKp{N0R~V^Z(o=Dr@1OLI7Cq~3+_mkckoa8G^78Dv=I}~ak4-`Iabb)%}uvFFQCiY zjXSU4_n+SU{c4@$KfBlhxZ4aNlACD$CQ2K6X)H&FS56CY$+4A{#?}1qD+_?wZ9V4u z72yfexs50(#h%Q)naXr92ee^BTSi8dBP10w=8t2)iJ>&DHT3x9=Z%gnVQ?Sk@ApLe z21&sK2gAL}I2n0p&A5-IgdzaGF2w$4UHo&A3F_2B2R=2CW7iVVx@J#Cy)}eGvUKT8 z8mwPqUt*S}*M@YSXfmq^-DdE}{vF)>j4(+zM5Oofx{m7SIX>LhBf+rJ zT$Y^_k$NxBuP^u>aKN@y(J|qVBt;&cIERVQ%YUbp$x9dGtX+`tWB?Axs<|btkhFELVWvTHn(!i5!AVUJ} zGND*-yFHX`f!Ls+B7}eBe8NR}os7^z@_NQ+T>_(yz;Az{WAvKIi3I@ukAZ%21F#2P z`S~R^OCdrzxk+f$4*6rChp^>nZf@LGJUm)ksJB92{6}{!HiWti!kO`-j7-pt zg-1^zCR$kW(nk$&_I*LFE0+OAELq?qj_A-2Vnx_?=5IBF+l-E=O|M?83M>o@xKbs& zBSowCWYZj-CO5qAYyVkE0+3w*U~fV*t`?ECx$M zh?fYY3+hVn>wnKA#Yp^feX9T0^@lG#>3pu=zbLntnXWNwveOVx%%Ojihdk^xU0+` zV%3ZM`t9aLp@znUfj*0NDdq80cTi;5?K4WDF9t1M@_73@f*kiCkW37In~@Jdxbunc z&g9CmgQYjk>M>#_A|3`ZIIK4_-)FKQ>$TV~M|{yD>pop; zE_E|q-1^TwmmBwyE2q(m#IYXmHM`$D96r5k!KJCZ~wQ095^zQ`Vn7G(t=8N1bispyO3_H#+uY>#bf4y=G?{znL+KJE=|Zvj z$HiJ@?1Dt-zj_}OTZY?Lc37^?TE>Eq|88|EdcMgW84+r`m4$#o8q}7xas(&7=OOMu zJs}NlyDPDX?;L1WVF{umk@0j#9gpA~CMm`ldZ{r!s5&gKCVcUCwmX*w! zKu>|a%N^|dBAr~-#3(8%u>6PP+du>i7CP1yw2@mIOc4?=Xpz`a~}d|mQQoqk?i!_f&v`}G5hgbrpUCNNBnu^!siZg%vhe8>0hMjLHv zwUbBeV`#0LtF7BtJt}i((8fJ>QDh`ldhiNy*l8>ZbeokS?G!@RSrHUodljFgLH4o$ zl=^SHLHiviGXRxRSi!zx83_?e*Qat8XEeVx|9IVql)!%cvcRC2{A6Pyj9xL4QL$Tg z7KrefL6k7gjVFa}x}zi2Y!fVsgWIvfN+yQ}tx9ROE6_F0Bqn)h;<=;cJ`Ob~mtIp!*B#Hu zGO|aRNgjZ>+D2RrN9~HHwX~Qwf#X@mftFmdrZl5SgL=ShM;|8*&a8#LWC|@3~ zgl)eBu((3~?wZ(#Hsl@$*QhpgO%ba@IxJFllLbEI%=`6T>9@@Vvbl#opc8P}n)(7Xlw>3&C7rkTmiUdY-_$#NqBnB{tBqf3m>px?4?+(+ znvvBPbro^}y6ODQ}{wW)C2BTbDK zjlm5t>liE1R3ked5XB~v%rC}*j)Jwvk2w6`@8ky<48jR`yG>vYE}JUmj1{X2*f|!P z;YzVQPG}b*==C?%69^lw-DyNgSKOY{G6s0uydc~$=#iL?G9bbWAXE~;r$6DPX3nS# zVd9x6H&R1}9@4uB#gO#S5e&NdO0oN@d(h_md5Oz=LPX5|lx!$obE?SyJcDN_U(-Lo zRY{k-UgA{3-;biaPWIUt7ML{FF4FM3#JzoqHoJN}bb+OD?0dCZqEa_DA2jtsgx3#} z4Ej%wgmR{d4S)Jinn2taQ^e4(DHagg5SG=_gyn$<+^;rz3t23r#4}xW1&UIjU3~&X znb?z9W>@xm_*Jhr&*LLzEcPLEbo8Xa5Ky}V^BPZt4*5S>^anuHB-vl%NaI*Cr4(UA zxxPc?f>ovpOP{4{cKwQyzxTA#;nNJ5gO4_eV9D}lH=h~PX$9Y>g?$7sYj${@)G?6& z91Dm(fAuJw6{YQtT0Hkkm#6Av5LDBpeDlIcI7brkP30ckXJbKr97!31lP-)0TT0+E z9J#1mvjjoI82}tE+~=>|EykNRoB>g=NVB=6!+NvXh6V4(@dwn8Pm!Rb3FZun^v3J=oXDz zlmaAF=~eSwvXdw5rupM#>6sO~fl19Q*-{tSsTo(upZIKJF3t9X? z+@nqQ1iGWWgZD(@o>h1{*4P#o&E^p<}6c^}@Lpu>mlsjE`P$c6OD(2ED14=nEa z(1{6~LWaEh*zVS}N!AY8h+upxdpyGx5ZHtbu)~ngjPX8gyBzz}WLaOWa*2L(?9KR~G_3EX$fNCXAGVJPXlqOjK zIWXvf9-E$yy->vW=olpFjqFx$uZ#1cLP!dc&#G%xm5a03+JSZm$y0jL0`-^SW;kb(wrxUdU&c({kly8Cjz8x|Wr3c^ZTjoTPtlR^|!0&>W1B}A@ z_0EB!Arn1@7dDbpra|uniiR^XPo*$sM$r!a|L#wo6YgJZ%RztK30EZcOSRmc-bjt^ zrSxs`X_9HWLq%inv2Pv~L$CSn1aCs^CGvn1_1)zgni9#%-O8UvBZ^0-E3d9U+K8wg zd6;3xv-fvd-%K_9-m=_U-6_h`EE+4c=ncn=Wq0S*xm^m18Y>%WLy)mOIZerWSI&Zh zWc&nX$==?W^ z`K~Xn^Rr@~)O8q>h#v*s4bA!P#JPXfUuQ8Dc3Ty{DN|t%C1kGY35=wNvBlxucyqrj z(*5je3uRK3J4x^zWUK>+a>%zgeX@pLUKXwPm3(H4{ELM1DYBcmYpgHx7zHfngMqT> z(=x->ywUu@;bqLxbs(dZ=kS2_lmS#sT4~Z!KuAcx-Yf3>#LM&EI@<)h2Jcizi`F=1V5)K(tYDf;5rS2id%nisSb(+-dF9}EoIC#9O4NQJP@ z@QCpgR$2KVO|U=3aQA+ioSp{+sJ8i5-dL);tN5PnrEM&ix%jH&2XJhjNi|!|2T31Y z(s0$Rx2`CS({`<*_b;Lp{f1s&fxtG^^P14EaXv^TUb7srZ{s(dI`v!^hZKCRi} zomT2f5j*uy18pY(u{fJSp*r{VR#o;1=klYGH<)+6ufp6y=Pg)oQR%cUF+bdr=f)L~ zhJWgjFhU%D9b1L>r2Y}#0r!&%UST@T&n&t7n_{a7Cb@<*NH@>$*=~Au);_D~1?SwR zcr~st`{=G5?8V#p+&T1qL^>V)8WkTNd+2jog_pw7-7kSHtk5cnHm$~XAcCjYd!Q3M z{nqsKs=rl5>h>+WS0CD-(gnM8y1YB_)aP)e42MdE&{q!WYZn3 z@5GE#C{GFGIaw;`Cya1}A8bUx7dtyJRRlfIzalXDL)ekk9njLvplij2_e!5v-xCN4 zJgKJ8RzBkaO$=Js`WOyRtqY^qWpCw_Q-pa`k|n+Xya=n$S#16o@B`(zh@z56(_+_G z3dJ3=Z9OL1HIGa}xdwQPU*-qpKXD1Esg=yWJa4^|vHCDP)T7%L1_RroHhDZ1Q-V?;nOIKYmKg8MBhtpZFv&jELTKM}b`Gp%MZM2Hy zlLYcO2=ub&w$^q$a=yizxode38LHb>_A>vYIQHP8d$xxPJh219IrJA9UVz`PoWvh; zJp+anoeGbgF6m#R1dh?<4&L2fMcsT$!PuWSO;(K$k4iOPx4z{rpHPs3hIRFKUPiU*PqAQ+!6T^X-vn4vHlUf zCEI5Uv61jDI2h$8aDJp1vt>LE(EFAO3hX>%hM-~Pg(MdsC`?x~fKJR1Vm>!fX3*F0 zG4ed((aS>VG?(OI;CIgA9=fa%wP1B}r|T9?QToJ>5ac1>`~M6B;CSKh#o#aqxM$L* z8T4a2P5*LOAHPZflpbSW%?K~{l<3>o^M6w`tq1WJ!i%cF;^euitu~xcVva{~tOjXG zweOU{B!vLoTLS2uWA93hojq>F8@@OKWdj;{r`DHm?B`L0`N!@7ZMtoCV^wq@A(u}h zH?K$)B2@0%(lC%jWN_lzOLMRXu#`2ie_Y-&CA7Zj4Io(*->* zDLo$IFwvu`!jE>oKXa2mbG$*mH%2nnandF19=O!pv6E7g`Nw3xMur~rJ=Nyf_?4gg z{$PnP{nM>Y8~5~bv(CMB_MFO}y(YfPKR@SQkrH3{?nrzh=15lbrIA!lop$@Y0uoW$ zEdjU8j$LQ@dw=wZtC%oUMD$K*gKMyvJ*LNlug3@e# z653rTH)HtBJvPI?sjnoZZA}G9@{6x3Mn_3fjX-lW4S0iPJNN_yIj?@!S$q}f1=)# z`Q3C=g;E=CAs1i%AXqEocErMVc~e@I7iaz&{yQB)oD6|h5C3N>*^N&swdrK)6qDn0#&(MvNU?>_2DGMG5Tjk30z}~t?BD}Rsf_-I`F68MqE3c(m z0Md`6C+_)Qj=o7<-Fl;;;_b%~u7+As2M*TgC$nT?_|N3XIKfl!lkHLcy3N1RUZM0aK}67N5#6>VyS=9^22I$NjDTEiwzCgbp#tq`ShT3$DM4KTt;dJI#%yjq0q z{HJs(t08*~CYWPozk_!}git>Hccg`YLitoWt&@gTbup~gVgQV|yB!s5cI6O66Dn*< zFaRD=oY7UKk9kSQ7a$Lt09C<& zZDZ`1shO=M5Hh+HoB_!oDC_lx` zHIEhPs(HE3VIQJMHh%gdZt8!0(q-%XDyP;pvqQ$9(fKZrGqkO)M6W8L@i&vtqlW=0 zj`pK~zpWYhdMfvd?6%qD%Oed^^8`u#PD>Zbf%$wzsZq@9-NowKx~$fKk2o2i$a`M$ z$YtZ^>I^g`f5_`)cSppUu3xv7{z|_Expg2`>Nj2p7y97}s235L9vF^`)$GRN%6~su zn=nAO82X+{#PIfhPsH4;{SDvhGs3c8D;YYA)grf3Ro)T@kxh%0X#v=tB{~o7?p^Dz zqd-Y;R*UO~M;akRlwfG3<{j7^dF$Qpj)f=?@Xr|qp}-F&VkJ;X=L>XP>tRwXY5H75 z3%W)MF^F<5EQsr`aefpw_7V*Qn9fxge79S{-8@Tf`j~XpragOR@POy&mUyUQr8gmN zXnVT^Htg3St5)uuX}W`)4sBmUj`m87#(jxHbZ}XOWt?6ecC%=op0$+d$u~^J9w7>P zM99PNLHiV~igcS>8eIUv{?h~wCabBn5jJ0%6vQ93ITw4(NobQP6Tp3SqFlLMaQ$C#ZIhywh7XuD~6$eZ)j5wlc zoFf~IhjV=Ef^eyO2+t^g#@!ro>6SmPk+N#pC*ksMaRmQwho5bI#9j3UBb1gK*TnO7 zTZ$LM=41FqkJZ$pOVcr7EX4Gg3*gfFBZM&7B#cINb=SRamY^)~FwFCAJ|I@Q?r=d; zqV7%_rQYVD5DG}OTMyc7eSt@>g85JiA)>d$a)S8Y=0x#`ddt8t=+wTO$c(T~Hzg{k zVAv&`k-^`W+r&z6K}AfzxfZg1Z5XL?DCzNfw){An8Rvht+8BO;LFJ>92a9;nJ|~aE z^AKl;p0fjG_=t={*$=^)({u>Y39;2U^&vo0YlKouB!z1WNe6F(=Y!*%%xGy+M2kas zNO2dM=K#!*HfJ-F=a3{lC1ENPE>?oWdjL51{sSdjscv9W7n*IyI%wJ>uvY2V8)I?E zoU!5}Lfva{l-tY=kNZAk3>2Eh2zF+9r~610rU;u2epj9Ns>|iolF#VV9H!PTbYjhV zudYxPkv6B>)Wc7Xe*fCweG}76(1LTQotp}KQ zh7GPc*QiH5F*mgzl9L{^Ph5j$ugdR(jpNAH>!?s-E?G>1u6*FG)vLLEo!is%x@}E= zr(OPDPc7=Cln+P*bMJ0Bd~K>ALv}>7B)hMHd^cljj(fI@66JcrRVNRUa3W_dpH1(x zdcQW-oLhTppCiAET$SXH^B98wCYselnD=E9#6UD2P zxvr%%oNSZ{#mmS3S-dzI==*-w$W>+~`}Q<-p&b_nhHdQI{)Yb!j{eMefPa=G>G>_# zD$d~TvZq0d(5k1d>U+uV`(Q>6n#>yApK#aQ8P71(&p!(XpMlwxOPpwCY&~SC)Ii57 zQP`y^T&){@<5e3Co5=~4gm>_6u<(1AqKSlNqno7+`=Gv3<0-SqX6F41^_lq8`D6XD z-^%NJU(MJ-B4dH7naZs^QA7X|h|_j>Zvf2_38_Wqeuyzd8Uw}u(!gS}K+17Wo>$jN(nm8%;-g#LC_b1u;`H&> z(vqUua&qcB%_A+94KS|g5z-PlK>Xy&A@;NOaomSBr)uZtI4IX}97Zzscv6%`*rKwQ zen}*k83)V>%ZoQn?WdE5D$H)a!VfrgKbA@;Vr(WaH%IVcFbo;V#{eDJG5oAs?|VIg zI1_Wt+9Vsq?{_v&I;jc7e`LzJjx6m01Qi+|nDzb#8->Qh_z@9_4i973^Sihtl6FUa z7jPC;-<2 zwG3s3Z%1GGlLuXp#t~chY$Yge#GNF@K?kP3bP=P>NFV7&Fid&`{IoLY@(tEXxsfbq zEo<+#PD9ZtV=Y^Gjp1d>>diaQVziq1(jZ3Sdd=>WAZE@T-70p~Eo?1&kM!p7)e*LB zcD23W!u)$^O1w8eiO~bI7ra;>a?4KNJtRnW_(qwZ zOG;UQ_w{(BNs`Z4$FkckWwpI3SD*cgEO_9nwPleue*KM>1H$LYCa$;a*WHUkwf-0V z{CC%fzYPI1RIQCe7IG|bKC=KW`m}F zBfddH6k>!=MArcv@F+OwmNSi`Sc@O?-0M@oQvL6*`f``;t!``79C(~|uGTZGh-Q6h zFE9JmIA3=O5@ktr*JCNQtvOl=l|=8FnGYovU(zOvVqGLGCVprl>!-l^TNchpVbidCL3bVu0HW|yp5{Wjd2aQ2$BKRiu6N&k@?M{@_g%&FdA~hH3-V3Y z@;+$vxqQI8l|M!C_*yGKzIoh^F*iSEU$JlS75bq-@^EO`IvMCDu=#Ll4B)v0a zGWLf%-*m_-4|~o19HMV9{f#rMRhI63Xq-w!o*qpPS~1tM9=I}j%l1^V#cSm~dza;@ zo0Mw&5iQgo!d63rDGRG=d!5);W?*;IHhM_jmEsC)?0TKjY8B2$!9fOO};q zS_gs?HQXeJU|PzJ1t3lc-(->?5Ia0M6H;0{Bt*|v68M;jFfTlN(!r!JNy_K^iA3W` zo~ds-#^-70Q^tWwt4f{i=Ny0w&n!OHqJ8M!?>B@bAYE&d8Ugy4J z>P-|u_P#tXv%hiptaWcf1q@5>+5ATAb%G7aN)a=rO#rmQgUw5zDXVaCZ#fr$kAevY zzYnFW3$4P|$jr~WUv36LXqs|OI>Tjv+DTT;j$U6ziwZeEuIA{YT?-MxJ}?xq9Q`C;z2dj zJ4!hKluHPH^Sgddpa5R!k{MiBm8O(73vljvNt`k`KpR(&65UqW z^Oja-K{LgmbA`A{?B0o>lVt5a6%AFjKs~)kwOZp6A@7^Z-%|s6W4It4p7p4c88%83 z6#6dKb#b_c{Ru8Ur8;#|Q*ZinT^$UyaDIgJ-dCXI-wIGuzCg1)Y7W^yILI7ny`xi0 zLyh;Lk%;vveMODb@_iNWBl3Tsm^+4 ze*iJZD-)rQ!ys7AB~qcDtMug>g#BdD(U`uOpx&IjnZSE`_o4{JWoJYuEyu9YqnRia zSu7AGUEx=oOZUfTpT<22K=-*vw*r~;3T^FTl*Y{A@Iq(TpD_=QAZ>67Rf!# zbIoET>)zcQc7a*)szzs-X0xA4^23_&FW$bjmeA19I2L7Kyp;#|exsSlK{l-9Z>TZO zGu!jS+QAUh7_CwCxlU6B-}0FZu^F(Xdo*z@>M(b5aABhD{f-Y{YMu^8I(jtAl$&DU z41{9E%Q3wuk~Dizfe_1YbxFCHPQ)6c3B7G*^>zK0DQ4pnn@-B+RW&BD#tsIiXqd z_9Z!gj#RVPMQCrrSC-;P8mY8dmY;^)>0Vb0+Z#^&F29@`JXVqlcXVB>G;9uH9=Rwx zNL~6jP1xngA0;Hts&s}>1c|zANCQ#${P*;>-%IprEiVYTKqDlm5J&HlO2aU|(<8B>uD3v$rG? z;D=^pTy|3}s-SVks9NWgH)j898u6_aZ=b69u&NB#Bk{CfZie2FL90|xRVqh`D&vts zGMJ?1zFZ{lL$v#`^z`6wEI{fPT9{Z&so|VQ6WB?UGJ<|sTCaXhl-l%r!m+Uv46uTe z@66Tb{66ttzc8UligXI$#oVR5^9Kl)lT~z&KNCW55bi2KepsE7+z!=ajWs#2iClvi zLU`4+7So?HX$H&ik<0l+p0r$WRX%+Dy}=rc@(aDYYcvqxKqA1ip{ra5V7yg0l*67F zLikYm$p&9ew)tY40)X#sEOo2B*^v#J+y6RMryRPRvM2RCzORCXDiPNUbY5O=zNm6t zQl9eZp*QPU3&RD#(uZmnM`(Q{4%7T@^RL^f(^nLbuQu%H1W83*Z)v@mOuWOtxPp;$?-+~+e$ZXiH74n|ki^^HSAF_A9NXupO0DfekPoZ$W zGFCEdtl`>0!Ry6+AcxI-s@I!;0C>2n40oieD?XfTG$2VzLM0}Y^gSY1}Ge{dTW51nF!9U*?6L-$hqF;P6F9(+C}!e$accTI6$%kZ(*r#J2A?HEYxKgXK@g2c~-l5(${XM$eUepHviw{~T>H z(_CyppRki$p?PhHc1H7#U|i&Ol+mFeaAsNXln9*v*3Sy?8aw}1Y|N{|Avo0n>FgSr@r z{Q#o3M17FGE%W{_$|6oH8DyH~N}k|w+j)m|bvQWQaV|>l+vydKC+QFM?a&lHsKq$k zTeQ2jJ8*x0AabYT^U(L~=JhI84zdC%>f$(#M z+kvlk;%J&;tKLUjjn+Zku!N{Bo)a-VG{uiV9%H*SDo7dJV2kXO| z{eZG!Bx1P+*G)QkeMKhoxuC)g!$roB!1#*b#jXg7wrZ@2Zb_I9##+RE$u9$(R;rX@ ztT%IPh_)rDUh;ZmVjkipuQolS)1bC66@p|{SiZ87mCyy6(+c4^z;Dn?Qc=HJyuH6J z_M{;JR*UX)SrN`!&VXf{QU_IjmgkUtmjm#}&tC2TzbCXov**p46Op{19PDpa2{*wU zckB5Fmn|WzS8e8qX}I;25{~RooLHF`KrfV8NF|ovq~Cr)BcGksRDF8mS4Cq_?M>oA zc7gL$be{Nk%=6V7&3O+PHe9yy%k)?MGU6N23?WwUkzz>M+uiK@7^Ghi(6!7b^wp@P z945J4=wL@UdxOn-D_ffE!Y>}_y=>Xc%>g9IpD-HqGn#0_`KHl6v6dj*8*fh%`w$j7RMn@&i}k$zMen_`GB= zx(UxY=bbQAOC8NVkZt3lL-As&SZ~xtc|!$SO&t`@tKNwcsbxBWJrC{#e{ya5(8_hFy?>_8S*NQUvw8ZELZny2iGdLl>z?3 zaYETScyM0=-?-`@*T(G+*y^7%UL5{@x*T$%A>a1}#p}qAh10VChx@G;T+Oi=w{dY} zuFO362B(l~Tg?!E+}9Es7fE?LGZyh5^t#1=W>Lf`<^$=RbIair5;BJ?nzx>=J8^d2 zEJ{-!)L-ACj1L0j#)lH+8Ae$08CUgWx$e>q6Z91~$$s1b$`2P@6faLL(nEj9Daz!^ zZ+fy>*MJW1#Vy&kvggHs?N^dM`vs;Q(-lss$o69kP1yu$>BX}&bUV>N7=gEoU=o@- zdMO}b1|b0i3ANtgDhxy^{^QsAUkz--^@B(~HdRroUeUl?*hRnDk`0a;A5q_z*b;DI z?k8pY4|1yRJ{&m}H#%U=?bEe4R*EIzwLi>yI=U#?A0K(PITNtyIiBmbH~)O;av-;u zcnfs6{f;SGy@67iaC|jB&UqiArbsY5y8cU)Ynhv%;rIwW%b$Az1;7v2d3W1A28)U~ z9yEs^EMj^|VoJ(4jr8~#k__sxBNeE|ElOami9M#dJfUW3{F%&gfwhwVv|X;9lWu!Y zqUx!KG7LH1YdCs|ufpi|^0!qH3yy{0yJ>Yamu40uyjt541KSt&R80;Y%x6!5Bj0uF z8D<5P?!*jWNTPooKsZcSV z&>a|aZYTA9qlStO%<%kXIMS`jKIV`X8~4_n=GjWNmfI-Pf!3>ivvYhMe?QjsD_Pwr7bTYJ=(LOcEe0^N z|CRqp$~jR=c%9&$DC&Qz7TafSbEwx{1LB1anrf`%$&IfZx?+bnCE4p*D%Fe6-d$0E#< zj2FOlm+q(M6X>vC{jl-E#!cfH5z{-Ojn-7ct2bo~NcrD}yj{)vZ6l_d4+;tMH*SJ( z7i}Q28nmV~S0o=;h7jM2;BjpUklfWj*7<;Yy*`rt#jCdR<5IOP835?fog{)G!}C&c zkzA6GN=x~}S*TB!>tr`;IlM79Q6!`Kyt0D>hQ5he?Na1}TB#?aG3C{|`S&|86ms5J zG$Xs80JW6>nCOZ=N>%W*;Rd@DB>jnME8ilCUfgYtm#KJPo<+B>Pw}ng+;;X)iODyU zW3MPg8K7QYi+Jn5rw@FF;PL8b_;I3~dt+CFT@&X@gF%yQ*Qx#6dtmCxkZm{9tfRnL z#;iJKH~CHBb&hy}>_+cl9LG^7iBn5KWw1fxI|@v>7VROrQc33P@pj7hf-xgbFH4hV~SRP5rOhIO)x_WX_ph0uXGhaW4%< zE3?alS4VG$gi=eZf#KXIZa?vyBvwrY(egXmlZ_FsCt%97s)9V zL2Bt&^@$%Ld)vhUM_WPEt6p7^wtl!j+(T%`SM&GWE2B-r*^^oy+m>2>ZhzgTofSNk z%%M7_mPW@AbF}pm^q8XkbYeRje_(y!s{J}u*-NzoCxnKraV>WP*Q(!-#-k0N^bW2@ z2lfTuB!E|Z6v>Y6w~%ThIQYAGV!4zboe`#jt`|*hi)X!&ujOw4{a2{-WKDoOP0jU4 zLkp&n_xg^w`0fomXRy*L&91Lp zMHL3oUGif+M9O=R=#e_N6qS^#Y?JcIhiEKAtL>qPxjB1hdnQ(&!;51HtaUv>n{1gI zpBS1MF!(0OzhH|kH2Iu<6I$V00U9}g!KfKEuV0ul`F)kjRP`>URk{_IOlhLjpteV{ zu3X2Y9knmda_R63_n!Xz2T}+j_}RkvoJDB=q3n_{0<^bS_JZUyv2lx@I{2>!kqiE- zUxq?ibeEbn8aQU^jQIwV1ob=uF~f-RkbJID4$Q_JB=zl5C-=1LqaWlBRf9rtWLl+| zMd3bvU5yrlN&3Y$R(N)4I*|N%h?uUw{-Q)(4vqFXQaTIsepIRmyZ7Krg6}UUSvnN4 zKij%aqTeDrm`DM3zw=J7%f%zotBfP-U!Q~<-0vgwq4lohM zVS=12sHYVC4KRP52GCwR&w{VG|4Rj(0jIVm;4TJ*4UBeSC+A3q=Wrp(DPBM1-?DEv z?SGb!s#WXKY5WMrR%$Jr*qtMlpL98EXGCK%Aqqe>%zG!v1#YcrFM#>4ZoSuo2r`k# zq8G)_TCr(Z)Y^oKCSn2kRk2TtBabhCNMq~m4pR8d`i2~@a4&#fy@AXszhvuO-)SOi zv$fYMF_hVMiVB!^gd_mCFZ&qcT<@_VCC{V6Htp*>~ zf3}@{kPZI6X$M)I)0aj7q3Galc5Y zD1ar<*F8B=uFi#eN>;Rj>FJ_05?EFVpNHiL_C?maQSq^xjTWwXGkQN%#Blhx98-)X z2$SAx>^F83Yw~MDx*chadftd*Q(&@^{zK{d@$v4C-gKiUO~jQc5=b{IdOrRk*I8x- zKLbQh8h&Slm&FT>m0zv9rg{+pG3qA@QG&gA7a(9+PTDbqi@y%X{y*%!cR1Jo_djkt z<%#SaWtA18l9d$_LROhsE!ivCPuYY-h^%B2p|X;!va&ZBnb~{uI}g1^@6Ye^{$AJj z_xJC*T(8UPRjYQRhuySsC>Ca%@W+C9Z?e6+5*$|2+v6n22|Dt$mmz!YUt&i0SwOw2EY~{`q z^gDI#82;bqG1+-+HgSBP%l-MyUQ?_26JgyU($LCH_o#+b-ydltwDA3XiA?4 zdz$YTiQPMw1d3cHboJDjnb364G23-Cq^&!|7iCCJc>hDcMTcX;6L7CETF~9ETy>^n zz-nfA0&l?56d(;+snEtjNA#O@c_C$}xFz=7;GKy^HvhxrYWb{NjS;6LT`bO$AS6}t zYrn!H5hMtN-y(iO2hZvE)bdRwub5Y?{xD20RV#Cl^d-Q*&aemZm? z@9zk9WzQ&_Gc0%DIQ~!63&!Uo0)%4!5`tE2TtprNLoR@dW<_Wt_6Tj{f1@YtSXMp2 zk%-^}&ZzP$(H}Uao24PFQfs!Qw)@8nK0lKAd`!AUikS)pu!|}*a-~3F8zH{&K7>*| z!%2z9r>WkjaQGnG1JQ4f0)Wr%uQ%e`)F!SF~$XA=J{s=0#wn=$m?AA zTkS&=JF&Ui%E4v7-y8Z5jmXT6>6nd}ym%Qtwlk&FbpWkt?rgx|Kgk*VLc#{jbpK>o zIV)(u{uUAvmh4Y}d~c(#zl+gT-*75SUdskjsDB?6;ky5pF_P`pqlQ;!F35#4)-%)8 z@Jm%jW5QZ!OQEO{FXnpt;FY+2D5zFoU;gth(A^FD!UF4}H5#_QA3xi%(JM#H86Jd; zVeyQ8P`XkmAXDFpu3_-Egup+C$I%f|%!Vi+q;v_~lSf!z7u7oPalRaNQ6B&DN6|d9 z+88V-S*`WVKO718r5VHs^zFwVv|_+*9DksBT%qUa-lzT%t9Wt-3!8}&_4jr@5}*iJ zatqbL!b#@9I8wRzWniP`fY>s8N|$?etSXIyF$fAt!cSq-A#3;NtMVbQ%4c!e!9WOZ z=U__cP@{Zg*GKI%N4ga#FaZg6^dDw|g&sjkOu`kPK#Pd{4cU{-Mq=<4J`}3QQ_^1Q zn!Z5>q8rli)MDteL0z}Ca;RJKpB%#CW*mH1~IU--$VmMYC;<%4FshB z*?&jBa4ZSnOI4DilI%sEu>Qir9soIskMS>ly(-Snyz2i1F$~x!!_v%Z#Q#2~uouQI zFHKV%!NZwOf29O1hMzBAU)^Lw3aFFy&z+yi+l68IGKoKy1(7@TTU?4Ic!({<(Ep<- zVFdU2iu&UX_7P4_yycSH+mq%+ck~esu7$aK?$@+j(a;5L;Cmmtd1Q%*Xa=krtkGXi z1!OTe;o|-DBrQ|@@2)Dk#n+>s9WWF_u>r8BKf;jzkP2XrX+u6N%B|_oucUyeN?c$_ zXhGo8r%G6}*WxW%{$zEVJ)sYQCxpde1PbckKhZ}~PvPi)zulQhH@p>f5T(b>mvkPZ ziofikNEtW%o_w%)w(I=c;d8~Uw(bljsDEAnR%r@r-Xi$Ft5HP7*nS~&=^w2#h&VG7 z6$03>QlV+h>WdTVv(|6mi_{ImknbYV0sgliy+_74^#Y~(pu?%~@eK%@ER{{7^Y_LxUG%YWL-e|)>&ynLYrqy^L!vFd2L zl|NeQ9-)>PlTModcby5Kd(X;Nhm--YzvVW3w)r!Dd;i=Wez|%Y4opNOs67@bT#x-X zCiVaF?94{8uv>**YYDLDB7HM#B!3#+AA$n^|Ba7!+y7i@t?XNASzXyEP5^}1`-_bW zOBK}`c+oy_B9y-ZPnl#%EdWuQ=3K+wj6b0qmcJhCx3(Lw&S8mX#h zSlw!LRq`EH3do6h@9S?0?Qvy{R6ZQFrcZj6m_e0uJiN)9;`DVXm_483Gxqr!@)cVW zKPTM+S|HK#FfcUQK)@hG`1;e|6^_aSE|H-T#uDh zipjNDaDeTMA}1W|!8iqDu#jk}$0#p7Zjbme1 zQ*3Q5v(5Lt4+H9yQLliB5n|CJZxIQgw^3hhJZvaG){HQl~+&Bu%a?P zR8JXooCZ~eK|IvO|II^n1R@z&4(68(Xb84QEJu~}Fm&c>&+!_iT1%EZK^ zJO4fkL_(}pslO57__2@y4KGXMGmTsjl|twY)ai;m7qP3KlH!6}Yvn+EQXgaifBU36 z`U56+c94S{x9nfFCLZ`}G0f8B0Vf2x1uffp*OuSLyH3}wGhiLVu~KGs_jAyMiBgo_ zc=oic8M%5b=;NnQuT;TISLx64{^5R}n(IBio_G|_(Ef+J%V2%RK2NH-FjY`6H?QaP z07lSe{gP%O<3HiGLfz0tYNp<3Y&lSX%a{7}p3zf$l*3iH_@iTA{ymrJdp^*;lznc&of%po6dVdXjeLe|)D z!nC1lLMh`;JnoLDcG;U2aLyaPQ^aC|M46&UNH1pke_Ry@xg_-$gpsUw30;8p=bSh2 z;Fn%82-rOuibWXWJG)D2qD~v-dpR(7Fp|$*SJZQZsnl+QyDRh7St*@Vid3srY|tPK z0dB6MtJ9+WV@+aT)k z>SRbSN_(QU56;D8VYQbe1X4+zNGLxGUNTJ&C+PAaND{cSuAl`k>axkPsf?SqZHoDcRTEoHy1)sn35ugTEU zNN=19v9}fd(YbX!d)>DgdVjbziZQPKa3WFoH+F-UQU3GhfX; zh+2$>_jbN!={Et@ZKBDP^6iyxlDRtXk>rf!&@vkX++%UBio)y!*+5E#(Rd+Z;Z~!x zCiF4wnAekGQQH_FwQ1JpUE1`VbtZfCI?x!!6=ig6{#dm zUt-@uKuP@L=uT9luuqcNtWRN`WG*Tp(v?gc7!xruw7D^)ka zCytjhCI5DAY;9M% z^t4JzTsmayYTxxm#uGnS{!nr9Wp1gzi9H-Ek~lV8aTkAock%PeMTxSmzPV4n(3zT? z^V*PSY;ma2uyjZP1~8E=8LFdLTBjNlqk*Cmv=P{nKoA}|w3+{)w0>YS*Q<2hT*-?G zry}K^$gg+4QlRdVVU&z>{w#jWxR4U``4psP3^_C3=npxLHy8l#%|Oo}gXhK1KJu#t z+xThA+sXHC5+Acop^23bQRMS|>ihP@Z;8B&T3_Bc8|o^K#GiG+99LXCXl=!-+nUS@ zXF3TCKQdo?D_z-TJ(#VT&12A#k9Bac?;1pXmfblZ>Af3;M`K+sJ6lZ%L-eoCyfd{M zP!Z`E_-Ji5vC@U8p}@$)vl2_&k#6rOk4wGv-54#HpZ21jl9VR<upihhD9k>{6t-igr zbp!UI$juAl&TF!->I|<7Sr0l)3$xr1rQ+beK!=?}>P=5oeUu4GOF=$@Ifk8rG~3@F zy>o?WnJhNY(dG0-stq&-g(FRl@FfRrCVhGR&=ma{3Nj71n9selIvlOWXL;g5X{BT$ z177Mws8!Vqygle+Q~P-@p*=Okapz!P478SHi{EYYNM`B3r#Pb;{`ArOpbYdWDI+{Q zm~N{mx%MHUQi#+f?vlX?u5tvBBUz9=wbfk3E?cgTK9}cqR!U^u zu;?-Q7%@N1((osDd9AElX;3b2hj~PI`W^~qtHZcNINV(dQYwrHiY$0hd!VQm)jTl$ z^J`d5oM|~%ZR6~>b72t!^&xw7cY0BPdGU#0y zEuv+#@uAx-Quj!c!~5U0==ROM)>bt}Wp8Oho`~1}b8Kz7z4mN|Uf$a)RLXLN_nTh4 z=6pU&W^)%i5x0(p(qs#2Qq$AnrtH&m>i!0^`Ma^%(Rt5VKiFe>fuX8CC;Tq6!e$@3 zLxr3PCG8@=wTCpDj{d519je2sGaVk8i~a#b4rMHo7+1N@mMfRbCXH#u;fuZoztgZ` zTK7}|DH=R|dEo}ge7(ur>&Y9wv2%RpP2R2yK1t(Bf{6^z!%ofu3$UIV-5u!-Y(5A5 z`zPs>(|JZ{sc;TH@D7*_{aPQhjMDDZEZ2FiWFfI^>-HnZp!Bf_KwNFS z1ATi#$M+W5XWxD8Y1^T2aucH!P@JIi%9i%RpI z9o|?{A$S4S&<-*U3sKAA(ujq@3bsNnR!RI&aTb(FTQOV74&Q2SH@vtT-=3=>m zP)t+X2sA9at<0@fAS_2FzzF=q)|%hF|7-Z`Kq0JpH=LXw4?hXXEoAGFIx|xY+8i8_PyM+q4PE+&M-9rtDKLRBcHyp9Y|bg`9BD{$C|dsRw)(bEM@Ue5F~W zO-{(fa@qayq1VkkT_yhwCX#|hP}K?4iEl?9$4D*}IhlK6j|E{qYoNEXy@3--{!S?) zOEBw3QHEOWfcCJv_aL94f<&Ub#fgh3#W;SJh&bd~G^$N|;aU9H$zrGMWxT(PNL7-+ zvplLi57UkRkr%w-YW!2`6-ZeGF(=yAWV({S*~qYd)~}i+vRj;8BB73(FZ2yMZ@yYz^^Z(cg9qYP=uI zE79_%LqW(-Q`2gJvtCWW_KF1+y;P+f+Q_{)Ah}f;0ig1|G5u5aVzE*;k+=)Yi5wa@)#bc05q0@9D9%9t^7&fgZ=6rGqItBcYFu9S@_Qa0d}^MXuaDXB?_^C-F2Rx$ z_WKeiWIf@x50W?Yne7YZ6K5kMHVN@b8EIC>Uk^iIE%sP1W`G;SsZKPF*o^7#IHl@P&vSh-cBfymCUojxiqz$mKxc6^p5QU( zP>kecsikGzTQJxS$P3v>JC@*>2?I9^q7&WS9u#iUkdrydCv_D^NSX1CTsZ!N(Rq92 z#ke|DRw6;-fs!p3wYRyrcZr~;Gz7#3mR&|F*UsL`N&9%E$q7_Htr>KjbeTSoOCw&3 z%*GvO12Fo72VjIYB#6Q2|ABCR(C-x%wk(WN=7jmf>eQvS>&Jk{&)LfSp2e|LD~=}e zyrZJa_86W!oSbjwVm%X}>fZ2-O9t~O&!B|;$NAzcXV2E8Y)XC&C2HzU>ubZgKAo7- zEG;jxaRXc7?#Mknw+YorN8g;0*q#YJ@0Z0EYJ+~IIm(>eANguIJT%@YO2hn{^sxSW9NL^dE zuixFT8#yeKQ=1uU%6n%ta6IcAASb31D(DHp zhrhUlem3of<4%_Ye+t=G)31T4X&LNy&l6%NdggR!vHVyi zr9k(}R2ovP^G@WY>Jk<<;Gh5eK4Z4WNXqMyjcre6q4M2X>=44y2i+PM|`o8Dk%JMcVgB^#LI{8xKWoN}eN{-jFCiZt) z-hNUqvTiG-7kA$Cc`Oqj!Fog%hKYf)_0L^zytx*BCn3d?i8gNh+}#S`;i%2^zMF~J z(woeA$>~xwZMW*anBcx5E9p6GyANSXq;=)e&JF!(ZK5rG!`xCqF5@axk#&nlsa@Zu zn1I2K11NKn1ThHlx$muo^BI<30aoy;(;-{Mrak(jtF5eLg~#qypd&=MyDjhenw_7Z znkc;2$okc0{+HuB9dh|Kwim!-91a_APV{e2Q&JKztpoDb?GBJty#CfLQbLm2RLo=U z%^f@aBHPUc@vkb{@m-TDvoiOm+fE*Bo)g&UFcnY=AA-yvT-5Xr71AfA`R`hP6v>$? zo|g4lJnTF5=4OP7*t_sOycLD;O3`>x+gKGw$(focH9>UoIqbVb#_=UnjKC!E3&?E` zSec~kxxKuC9b9-e>TM)Wt{6cih;EG>$bdxWc{v)%rZ13Bf*L{4@trs^Qhb~Z>Lrl} zZbQf3r%JU{tqZY)x}IK5$}_BJkL{YBZk`o_E{R6By)CukFM;f;t@a54;jHa|fCg;a ztIKXDq1+zDX{OwC)Ug8Qg3IQ@$-Cd52ZF{3-*ATO+LFK-g;?zmwmRh^`G)1v&(zCx zG$6$46QY?*LDRbNe77%S{1&5;P+pQLmAt3EbbsJ$AcOoyxF;3U7EC>Q0}2|!Sl_JW z5`-eJKaJRZx0@$!mm)KBp=>V@dXPtGeIcnM!5$Vr$3-UykP{_ZJ6M#l<@lG0cr0B_ zg3&e+eYI>?M9}uW^}kLZv+f(hRL*cIOBIiBik7rxv{s>Hs>o*HN-4KWO^FP&y_4Lm zqgzM7u^&7Xm-g#fRg8ov8NM!4f;m@(v;xb-hKyimv*UAt%vD-K5S0R0SbgJFpWL(I zR7AB8a`mJD%KQ#0{U|&)ny-CA2Y;b)&(%O&@vewO?f3M}7nECWtOpRCpXcW(OCK## zsBtvzkqZ*`Sgty=?Pma@2_?r98-_pGhbhoah0s=5YZpF@r<(3-R;jKdhG?5w=~GP{ zc0DK16V6XIYmEklN?H%*gj;ylzrMRBS8moOyZ7B>?*p&c^g?_YB4|l(?GKKdwASzH zCqKt=KfJIwA>2sYa?eBQJMm+5u8zomih9- z|5bIo>72#M{ykQv=cZ7uXL9L6Wp=&kP_aPaR8Wq-en^H!LZ<7)_Bf6Voym4p?@8p9 zuCdk(0-vE3eDkz$||9pcMC5iy5m8KC3d{6Mm(9Y&c<9^rztC?RM(^&zl%uI+$h=0xC>Y zzqTXyKpN0DE|!wU#LehJj1cu>doQ6Ak2Np@p|s~t$x(l?c_EOW)bbW`#?8Xs9BmW3 zG7XKrkYTJl*oTE%PYt&5UeE-@Tt0Q`l%2f{`P$H3IcSKcRWg0WOtyin%Y9sQ*x#7% zKXu3>f!$T2f%>26vT?(;v(rLa`2V>ofOd-*;bL#t|Hx9lpco_kj_h8NY+E?ioGDqY%7ryE73H?0YGH%}F7?uOVS$N^|Nf34 znO8(EW#R|b1lAI&rM?%fJMVwI6(g9N6G6F&d9jGWCVU71sa8s18MBXkS$HzkGLCf5 z)usLwS0w4{8X`rXny{d6>PFrAtz)8ZfW#QqLSls?VVZb7EGU8TWGVhb^H|+}QT2n;5 zYUUoJSz^aYeE!+<(96a(;>NMeiH|slZr$E&G{u+u9c>_yB7114aJZqUE_=w`#Z6?m zeX3yioLoXNYXGnHWDNehyG~a?)|@;JW&I}4UBZ@03sob2T7PaS(+#~<7e?-teHw!8 zUIckQ>G@xFi&y27-U6x0yz;_;73~0=!~2VuM_!A5ko1tkL)G4vstx^A!xyD(viPZX zt){$uZ9Fxm*W~ufb5EDH2TudJHs1ES-l8gdN2_U(!5Q?#_wZF)nX#AeP~-W*Pn$y9 zTY&`{pQoDgSIg{MuG2||tH{TzwI|M5p>RAN>0!<5AvpJ0-Tf~yB5y?#$L=wWIIK=8 zJ2%9h)1SDLO2$$zGrdIMrp$PsgmG$!BRVV|(-K{@gTECVY|fx@!^W1&#UA}KAa`Kq zc`+s-LJZFP&TW(g(e}Ux+19u5Wp^2pU&v(o6$=X)R!9Yfg+)hq*}Zb3887kV)fHE) z4W>>p@5u|c8{o__Gv!(_r=7J+a9RI#qyCw?$?+c!*ULqo3Gv?uSYcT~9bmJxx&~A3 z?|ph>QF5Mjqe8Ma$REGV2}lX|ZJuC22c#5w97%8f!dZ}l|40(k3#mEtAF4@RyvBp1qX>(T-+FJT*Vu{yN)&wMe|Mk^n9F6VR} zOp$s%JI{1Da5FkL)c@{bW=(zR+(0LH_phG*5BrxxB!-H~<$LmKXEvVi5YM((O7C+? zm9N)!6>>J&wo+13SUTz|RQ3>4;PEP9tyUMLeXW$15`h}guV*qJZ-57S8I3g=?ra$x zP9Fi^4eybHwee3%Yv{m!4J_2t{qd;W6m_4og#-mOt;VMa)vZk?i%nB2jUaynhpF_j`m zUjoHtx`Yej zQNHtMqYrnc0%c+ymnTx)2wh!k9)Y-W`~}{=iF!k3N%8GYqp^IE!GpWDB3y@7qxm9S zfObSLzsC5YjXbo!_0n;p?_59@>NwV@8q!vC8z)A766#C$uETju-CO(8H}_5^#oAh- zs(PUMnN;qg3#`u%D56_xYfC&e$}RahULet98CeOkD&q2 zu>5(TC;SdB`}fDz59tL<^dD|!&U^cNDzLmx>x!)|wRkBGWQHXBH`qm`ydF#rjNVCyBo5rtuiuK2?pWpZf4fIv{8=w6T zyFK2N)orHZ8+xV&OI=PfwyU$~DZ}QDc)bCx2%3O^Rchj!>v8Ng; zF=o>Dwf}wEy17t0Q|zbn8A*WLtqnd)&%9nzuo7Nxw)~;?+?dA;`?jeLUIMRn>PF_g zF2n|!#{dMp!-UvCZ&`ult)8<*D4Ya2tkrLYbG|=ZB;XGSEvA1}-OM+_cG=lZ=e)S~ zg=_t%d|0rOm1k;93_k7T-AF$YPeb$;%i5~y=hh9&y5Pi6y^kmL$P5QBtXR3djmAvs zxlIL@Er_=b&_*`KNG(=~XDaW0eekn1U z?KP5&k0gp^`H8oC?5U{q^~aZALv{4qUXMwhh`13tQYGceRU(SYz5Jv$fjf|kbx^jJ zNeYS69VbKSGh>{`ad__FVGAMH)v*A2t?-w=&61Ugr#wxVyK-;I*VTQtIe3B&T*^j= z*f$s#UZq`{?ao^@y3ExcIv-vnQ*-dq4uF!ov-jtFSw`;j8anm-P`rWIc1<k>=HXpTsCqKH<)#aS)qkPV6f+V>O#Bae=&Yk3T|CfCfnvA4@S` z*tO>exVQ516H<)SBY3bpOsQA}vu5_~7sn(80|*0_L^o?weRzIdPZW)?8m@?ch=RwW zX5Q>&hjq!BIX-NQ-trR)H^6r#w5@>21VuVI>z~uemSkkk2JoA!s(-oSdpF>frQU}& z5K2tl-mQG8CX?jgN3o*)0&CJ=i#N;``l(`GV#GG4?oE2X^TJ#}@rS)i_PNYFA_1Rj z=xe*N@`7%!U0`pW+dE^J?pqVs$mlQ+r*X-sVbT4~+hreEM$Ym-q!Gl%mJF_Zt?Sde z%+EC4S5`*0G|-+Fa`>upb9FW4z3MGX9LnX54o0he40iF0fa^*MG~#&i9j z%+Kv_4{gRhD#um&Y0a%mdZn&sUB+S7$ z-Q=EjMV~xwHEGz3|bb4%Z>OhYrn3- z^zM}S@UlB!&3iJNdy-l+HPkLHPYqA+m8@DZkf9VmLdz-pX}J3p=m&zkM{zu(-nDO( z!NY1Kue@CFs$O8kf*pDyys!)GjL)jS;TK<@i_l~#x~xXgw)~TW6Uq~;m+Y{?*-+Bc55=oE&+ zNi7R}{XchuRP6y{j&$W5SYVSCR8b!s9rXcOzP!w;SmN7FMp;^A>_cVtVIXHMzq4B@ zjR%bZuw++ZD%bEdO~ImJNNiW8ZN$|gOC=OENQfzkk$Rn1T7GRu%7NJ)9X%$yiL@>; z6XRrUOkqgjLt~up+?zGAxQcZ2cxh>#v*6j&e5)R~wNed;Iycm!VVA zg{k^O7=)%5hRSq)*rFqKLg|Pf^J6!Wl3qGhN`gSpBPN;rmWS^HRXWKNyb?iCa!x)0n25ujgBnMPkshv0uSw)RyuYx9R~BL z_sVPoj5Y{3WG+d>+L&6nA9NyD9*D;Tuz=8eN2}R*F$+VXmP8p(lnnA_V#3Y?P%WZO@y?o-l2zYsN;7@tF{-0 zu!lr7K@fT^5xRC~VHT4te4@wLG?(y>CfC8LZejRvy393G^3y`@_Qfa(T)f|XSy553 zU5gf4mvqpNot@RfZVv^hj@tL&dA4ghOL{p5AIrV=>=z!~nA%&J( zW4PMAkuV(&7HWp9lEAIlpADzhJ4mBvA>NWnCc`HWgQd2Duh(R{Ft_cnTqT!;l+(R< zptVr4u{AA=Zr9!sX5~4+y0zR)eiN3_)I)`;^}zI7rv$he2YbfeW94mycRcSaPrs-}eWc|8xkK$&tP4&rbk- zxJ*a$v1;)1^SQn}y$bKgJW`GCK(VyhqU?^p#sbZBIx<(T;N;#eiF&OfHk$r)BW9k=Zgsf6jUT#O`=j|NL z&pRW5@ohe3KeXeRVf9Hgvv;xgl!6#5>xC*R~vyzZaVVSv`+V-jG7JKpiJ@j_CD@j=Hq1(%^Yn3 z7^}uf&8Ht=H~tNqU*~$#!}r|di;JpLErv;O5ZWi+^~5L&Rc;D|6lTCkH6 zu!1+0X|J!RuVGBRsM7*mDKspr{*fwk?}}?Rs=9_b(|jOdx@gri5r^lhx zb@Od0N%G$m7=8ilxfW$;nNX%j(*KO#2N=|=x3FlS#rM1D33!1&KC!gaNiUdd3Fx%k zgUz?%MUJbN`tqZrY1e;scxw+1Q~tW^ z{k`g#|I3w`Y;Ljb7WpBXE`v|n(cd*YJ}mey=FnrkK`U0kGD`l+(#A#_M}mibUD$Am zDXpBvfK{*U*4Fsz1KizkLn}#9TKCY`wTT^S_hD3RFf!n+!H}6rD@#9cWt;%9>DJf2 zO2@@${+X3?*%z!L!GvqA;~QebI{5!D8-}kJMYU3*2uFN&O;em?35Lm4Fe*(oo(o?u z*G21LJJ0olp$NRaXYDMV6{dP3p}oE4E90eV8rl(8H9y^REL*UIEMa4QNY!%SWAgdC zN(1g69G2}w9akdN{JlN?HXwPj%7a0+CLfg(uMqRZ1#~B{)M55j*3uJaBC{{$HtsK8 zN1j1H4q;feZwFsJcwYPZNRhykU~$^ZwhoOBGB9C~0uB?t(esY?lq*%QU1K&hG~Ahw zR)FxsU-yHRG)t;deWG†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 z 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 literal 315264 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

      )^@@|1jzus^eSWD7<{w!; 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

    1. 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

      5#S z|G?f&49-w3G&}b4l^!i7vDPEiejn;zS?Or2=1?gdPb9~*e? z*HCf@G%J~>;qv{zt)2Ga?no?;Or!|K+-S~#bI~%YdAj$WP$4lPQbm-Zu`w+sbEFDh z;kXt;K_331;im4V*3w{;f13z;51aUrHM)F!@vu_H5tB@<9X)T7EU4>($L% zEVdkYcmIp+mCupx6Lo%p7X>QW{loY$4&ZG1uE_2#FHOor+~gwLVG~CuUoj#qfd)NdVwr(dP(aCdDNKeT^io6o_@{MHfLi@dJ%RCNCmWNia?GzavE`m za5JJOY-^9Zz%(4(x4*Vr1n2_UN?8I@gY{WQAXtg+B#X3_tpCaDP34ab22179V-}Mmmgh(A1n+zW2kg51 z_S*vLM>_o8mlJ}~%>mCzqh)L{SSzs}A~&`nc_(8rrH2M4a>T2fHD?1mGMd~Agh%Hh zGw0vqYVtlkf+HqL!CIA8O=6IkT@yp`nsFFBGH;w3`0+H!Jf6AOTRmt;r%fGHA@AS! zykd=glU@TZm_r)=fU36dh`m8Ro5e#>(*f-D)VF0N=qWyRl!>LBIdd_DZTcR-5-Cl{!a=u=AIna3BJLD)R;#v?;IU74K_|fXflit&9bN<2|G4Hpo`IGI5RQOaq z@*7~5!lywNW!LFByc@Hy!Hj0er$D};1IRY*f*a)xR5{`$MQ1-x{#OwEy()uIlK+{; zrstuyW?=8NXGFvu36d0n!|ly5m#%DIs&e5W(hE?qZGR~tCztaGhy8VNm(2 zxOw~jX4iDs(RRY;c*p6*-3o4;@2`SpX`&B*1;04=^S!rDno6{QYmxGsZ#_dA$03qW zYnWrkIX0Y+k9XOec4aO0GtV0cIZf>u9*fzPhF-k!X>v_NIH1a@{We1UGZ%n*+oy(b zi7eF^rG4Uy%)tlN{clauJ^}RN+1!faVg%5G(bOS}IpPb%g;>5*0rC#ecEbVc&%!^7 zsb7mjz!5Z}upt+4y>iTOp_q*0jxvWZ6?jnD%&(QauWpgs1&Z`zC|X#f3mWlAAJ_F1Vk#<}_6&Xxe}6HR9rY zo*$O7HB30bT+2Q;X{=Lz|Fy*BWhINQLSYf7>RVuF-HdVO4_r4h~hu;L;icg)3CI^`he_ur_RJovJTc0!(u)lZ&y^ zUy7?ER>+!!oKt^ZlY4*5TT>jHEm*i{+W6RT-}B-_#-hf97kHcF8BnaNkeI%uFL3w8 z2l?Uh%@Ur)-Hwstt%E@aTG48cE?2R8V(m@uC&EOIzs49<|4fuG*)4JLly^@#l;1wy zIyJtvofw{qb&l0=v3MzKc^P-fJBx5ntPigy?A%*S+D)Y1ej9Rc*?Op+6-_d+ymF(9l~(SuBk|ixG24|o zJdWS^+Y&42n`P1(f6#PfZiH&_2*iH40w{j50HLTnDvYt#(+3nj<&uIw_=;swz5pJu z*;+Owi1;D*>hz&J|EZ7nu}szTD|!O@6<+ubW^HvJ*!f7#_>G>&h5sR^lFYKxBpy&( zcpHS(msn?etMG0Ymr=DwU;2c7wn8FVt5ktO>}WDIg2$31=l;)LE0N=}qM1rs9!$kN z^~Dq0vHsyMF-tQSTzm7)oT+#X$x%utEAd1x)F|D9HmvXZZre67s|)|Keo+92ba#=RWn*BUO9PXi

      ?64~!?O2TK%w?4tgl5& zsKGvkI75~Hu7Pkuo8s>yBs|^XM4fMZk{hmkpp>JI1fB0>7LQS0xW4zm zse;r(-HWyB@s>;oFY%LDQ=5(niDU(Z( zI|%_)8Auh=0E882g6RFL!vH7TkHxnJB3sv4g<6D3aoT0y-MICYC&Z@)4HKB=v!@5_P2ig=Ds%Upn3 zE)@#O$L^gB{Z&OSOUB`C*CdKb6CGmbtzY|iaL&I~@Z7BRsxH{J|tcEMg=kpYe`thixppSq;&XIJ!di}NF%;rcz7HEPHR*=+_Ch& zK5&SUq;De|`s_qAFh2bC*QTFt+00}#an0Dli1D`raT45@M?l?;Lkb4hdna`eX62{= z8tJ}6Aq!e2c>UX#2M9}4AfdgMk%gi-!LvM7HzXq{_*wlALyr&|?c9l z(>TsTH>@@HWGtV}1-zHi@%sM$7S_WbISo+WIrA(6F}v?Wyb>$)*qn};$p)a8o895w zCfTwO<`**gw$mseO3R<33O7KQfxZ~nF~f=7M9ibI@SbjbxcKR=Iouq+4(mtq;?dEK zpx|;dBY(srzIF+|^XHKC+aHp+FW-Jj;QW~g+u5O2BQ2sy^U1@|c<8E~{0v;|Az+iO%%PLx1o?1FSEoJvk!!q|wOZ=3n$k1? zq(fA!)O6eHt}1iEG^NBFgv|N|7X_@oY}C?{xx)%Ea)%GMzYhAZV*>3%`V{oa)88~UYgS4bx0 z!W*VqZ=oEd7O%j2s6_(FSFORsSm=Zh2gr8I8!Ir|II&v}-8n^3+!q5DUA}KVS`5~C z4GnDSl$cc!$M$_B_OdI0nK;2>^^h=%ThBMEg>74Uv;_*U&qzll zYN_r2^;Kjm@Bg>2n(l~UXIh)-2=v%pW3V}@Y>5}8_?Drf#KW$fB;B;u5_6d;j7Bu9 zA&?-`_F>K)+8462arJgh`;DulprSiBG1Sco0bC>C)Hd35{{K8V0j^lMe=xqp$v(aQ z3c`>f!pM$cr62c9t)QHLVlz}8P>|X@vDjZUEm}|h2iN=mewBPZMhr^EOwr!f`xi{e^~xsEktaE(4dX2wGlxFHnNmrexH8Oj%`>3sMIz=@)Tk}C=+yq!65r4B#eb<430P_m%ltxH zlg~3jOv4Af83N3oiwNl5dh(!H{`W{QMQFwsFGCV*JzV+JE|qmQUpT>51cW-xStHG( zdz%Z>B8^dhY7lDcFrZs)zMU|?DH5QQGk%W(Cj(8k^4nwr>be7li1V)?G1qmY?&fx# z%}*9Xwn+5ato}-5=MC&oohpmaGJ=8&NT`87wmit#Sn3nBpF_qb>wh1cwzAy|$|3#_ zpMP-p#08d>vd7|iBoU(Q47%UIOMu5|W5>szh>Xl1i>}NtFtHyLlf5~29xm8{2L7~4 z>`=hYjRAK{M%MLD2NL8^tIprl=@b6*MzAqZgNay=U-ijXKweO2#_)R-@@?K zB61b=wEM<~MN$&JBL_fJUUaCUph+ zE%7+6J$m@6@FfR!+WED1!V=i$vkXr|vrS2)+2(RI!jz{!4-=ggJ)t4k4Z}mY%K### za-s;SJOSO+?S!G1v!3e5ZA>SJm`%#>J<|3AA0G6~t;ir{h-$TuZlM-g1|{T9mLtCP ziaXJj(EYKl4jrp2Nge&}mzTu*$09!0@W>~6-|JmWQ*rzL_LjUjGh52B*P1NG&7%U` zOvW1z>XB1T3S;W`j++Ytz<9~38Kf0;JEO^>((MZFZlM-E*^CSfdV6eR4k;-qj|N>_ zQ;mMTanz}ClKF5}>E$J)e9xfQn{$!5-8DzAg4X-j&pb)h#UkkUGrI>>m_TGR-0zhv zFg}zs;7X-@$h4{Jn~&NXv48BRisdnfflw zk^8N}^Lh~iFMTxc>Nj$o-3t2KyRKFQ7R~!2)9WNFkT0ijNBHQ%`^R6I zt|zDBH9XHd`Uuj$aoG4&qgRMluy=r!i@5)N?BryvrgJ@D$T2KIyJB%OoP1?U%WIA| zeI=7*?wWs|T7lTsuS2}`={ES6Yi$xPqeQsmXS$P(WCx?YLptLxo65Dti>u;ILSEm? zdO)}+ndLu=$9AhaC)Z)4+-# z%e?VAXlcmZvD=3Apv(%(e&%yJ-J?r-<aV3yfW)rV3E z=DSXqao*{8BS8fzLT}1;8jj|NEN*YZtQ%cQH5d{ z@81c@V9O6$VHztvPL}BV=ujfjLuUv$R#`BOA>-cr$6VWva=;$sx!qJ3-Mx~~F?F^l zb?no38L$AlTAwiF{wT+loodAV>VwDp6s`?Mt<&%Kto)p4mg>%Kjv7zqrFOn;t!jzo z&hxrYPssiFyA1Yc2ZgFB`e&2xeQUN3)4A}6LxGDz&(BZBdLyo}C;L`v;8nClhnIiq zj@lqN44yGB9zT&y7IVv+OCnRV&mYkUqgyva?=zHyXDtVc*4O~!0D1!5PxkE2Xur)y zCuxRq+bZMy(2-an74Nf5X%kQ4#F~e@%{aaR&1Am6J@Cv*;;MVa!wXgMZq}`9qc&d7 zd_g{R$u|;O(NVi)9$f6o@qISlHa}+*pZaiT?t1U|PYkcN4iRO2gRR zU045L89LW1Bq0=BSC_HeEYJ+O)L*!)+1q<8E$)`PG(S5ZaVP#ojqMTY-OcRrk3KcW zd53#-wR4)8mv4wOM@$O)E))L{hpw2i?rqGOvqfg@DH9tM*mw^9yg$sv7K`^ z`6}h@X2GigpC9y>V+KdF(RzNlgW-kqfXQx}DBfK%Wu{1O+Y5+Owm^W~3m@ObLY%~-{}3(d#9 zS-hyYX*RO6No^DpgU{aa%R=;%r1?w;K=eX1G&4n(X)Z z%Nw6kjEOR!)j&}r>sC$fl4%?1fgR^XR7KE^o$9RfvSXq|fTUtmTr}z7TCKFnsFI}O ztBY^BlU@lDGWU8(CmLuSE_Cfn&?Fi*ztA@D;h??{7cREpA{DUpJo1|H~r9Xx*hhupWoaz*(3ymQ`+}`0ruDBe>r8IZsHKgHsNzYsFUr^Ds!SoNLt1`V$@_sd7z z2IcKfK>8A{pp zGta&=R*{lodi;rM(W5N`c($FOZSP?3ouL2tk}AsRjE4vs&N&t+@}9Ewy2|qh(4khF zNhlO1)oZD%UTk?@D^FJ?F!#CO>9{wb2<<7PKB1twAO`HtaR<*s_8q*{?$TI;gY~2n z;pmPEa<@K0|3WAxuwSZEv+JEG?HpnnUX|1i>c_(;0$?=73 zeiohC2ob@dgE7EwNQ44?EjIss9l}@dw1RsUN>N^Igi|SF(02l(kFDR}&#NJb^^+Ig zYk=O+h2=C2!Q}?6yu*gYkXan#057PYL1iVRLgvha2ICbHJu1oo1AB2_fTw2rE3l;b z{KxO_sQYJ21`^OVKb>GzE7ZDvx8phfz1COP05!1P=`?PO5oq0P!v??(WaElH`==bD zrj7=w9~Ms{Z2`Z2p1{3H=A~CzAbW{ z9XaNeI}|t#rPc817q-=+~oz(wne|HO5 z1S38M2w1Zi0d;^Oh9@7!cmP-HWLqS`IZ#O|BAL5zbeItuTQDEqvzJLiRkPq3|D5mE z4a>oN9M~!uxc?Y4Eabo(XICaE2(@S7Xju0SR;|zb5pf#2(^$RPsY01IqZH18=e|8@ zfaxTYny*CVyJ0s@D%n^vt?z9KA)jWBvsOpnB1SH>N^}Y+RO(sdF zU%T?3N#+4?=ErT?oohS{+WOrDERCxg&OO2SHtep@sdA2Zi;}GcB9(;9 z6hd2LrE5EAz5JL5r}%VEIYy(rPMvRMF=&ru6~xUVWdyJFZ<%!t*~h%;VrV$2k?fi* z)S^z*frK6$?+8@>eqehT6iuRRKzqt&vvsX>Cwgg<` zP9@X|j$-~fTHxjI_xv>J=E8(fTy=UAK@VfMI{Sx6R;wSq*61sA zBfi?+Inr9+uf%Re4E-^P3Is5Mkuu@;t^<$_C*}m0o|H&`Oq)h5n51=!K|m%Kt$$Wb zje+T*_om4y{vv`PeZ2qp*?GQ!TpR0oH2{!u?}R?;j<6E!h2|ut(B^AiV~nYIflBm` zJUBuOo`yA?P6?Tu3li`RBBCYHlQNoO4u0S`s9`{M| zVU?BXro~K{!R=bH_w^xh5{?6A;%3eYwUQY2(_fy(T~yXet`AVlu$iD&hi^kR@w%ERK`b*La=Tbih=Xfwp%??gXPTq93t z`6u$Te)==<304V3QST+`REHKXqzwc<`+vYzL8;Iv6pmSR`BF)m(eBR$4euDNZ5zr@ z)Cb^^TNpzVQCi=e{=yXVg_0no2Brf1$aI*4>v=O6^meQP;aanRY#Ha@lMWJov`VdW+=N z_va&@531sL$!Ctl?v)nabVgp0na%$^l5k&mojQ$IpgN{+I`2byUrmk3YtNDG;>~wH z-I+H`L06mw6{ty#H6Raw64)#OS`YINSd*T;jRSmCtkg7d$2%gQ;6km%8kmHk*^@Idd}{^@@&^Lr%Ebcu$XaW$~iIF%g@ba`ss{` zL9YILH*IJ@Lk5z{E{Yee7p7E?7p}KF*b9neMJ6>3uA;Q`=6pALUgauD^`Qm!d~dcN zc$OM>5aA?-4;f>9;fSY`QTIHW&1_$J^$m+q{M(LJhrX@P{w8TBZZo_ z!4ul^X9KRx7tVA@?viAxCZb__j0`saLV0c9*&h4Wgm5jN*o!@{u$74cTQ%Z6eDa&-QpySMz@pFE-27PV8^_ngS5C&6n#Ycm&nIQZ4bR^a&ee@ z7qho9SUS!TuD&LB{@4ucf^klrs{udG=hiRAi96iHN#Rrfc>65aI-Zw2-oUHg8TrwL z^16GM`ADZ)<5L&6X6_lJJE5SVr(Vd>WtmwdiO1U^DU>&vP}1MOdN|Us!)_YEB=4l0 zz=H`ggR9vBoCS`p0%G>Z6Ys)}pZP9V>5e&}jqn{^$^J23=4u}tZbw5anIBex4&g>h(59mMDyD>Zp*UUQ3&XN&OiCd1{${Fi+$f$ z2l4<+QNm$av}3zNqK3%ogT;r>&TWMdVpMf}IV{`GiHXD@0anZ1c0sm(SBuJw&fAGX zO|1ZX1w(>{kpabmq4`HEfbQVo11) za$NXPmr5w%3vQ3Hf`jkchoD_zCBfA-w~<@wsb-Gg1${H|O*H?bMk0lncxc#|ti4e2 z(TAI2SW$=idbYqNq~CPcd(&{Ro{xzhkl1V^bs3-g)VaOs%wygwX7K4)e4mWgeT{gt z$g8i~N)*$BZtyYc$O>(wh#a@P%6-dZ#bfP_F)BK6+}s{za_`zeigipR-lG(a98G<4zv#8Xqu}? z-#pu#;jnM*GXC~cXK{nL%i)w@fzfcvEa?tlRGnGYnj31X%~ixJ<~I^Uy=?G(SpCC- z@cj1hd&!o54Wb=lRfPim7PJAovd4=R$%d&n@e%GDZR$IHhm+IDTQR=$R(i}z^o>y}~4u|Be5aKe)Z9KV~-^SW9o%hk5 zkXgp^5Tc8j%koESq7sM8GhLTgNJV@T<#%V>cDo-P(pBy!69h6re@fh)*L>#9rJqwD zDIYwYV0n9PN7qiZqIZ7sFwv#7!}MaCD%`uoxuIYCK0Ccq!f*G+v$^T@igV@}r?Jn_ z?o@l(z8Y*ibhXDR6vH_cC%=Ir;X$~uXu}B3>AqlQeGCOO*6H>5QvnPkuglW?YcLf{ zY$t?zHI%juKmqrD!SttZz1J$$%&k z4wO5F1BFuD06z3oFDgHP#^=l^`Evp@k2aYBa0=5T&g23AY=_3n$crrtLhmFjCx z#M@4ghvu+#F#RtsfWc3U0EQoLBtHA@D@ z+veQUqz_l&gppkx2~0#R4}03B4$u0gHfADj2m~UhabghW`zag5AhpVo4@P7=igbPV zMs2Lh^W~3){-oH<@=1^CcZ5%qC_M9dwMz`iQFu0)?l3|G7ecmx@P*A3KlNKh2t z5IaQpNFq=LYdgmLIFvJ#|02BIe-DKg=w&KsM7;n1koMJ4Rc_ta0!QhV?gl{+={!gy z3JM}fhll~v-GWlmC|yzlQX(MTB}gME-3^C6zyZGf;Jxp?Z+*Y-H@-0(!@oG^Is4gr z?X~8bb1q>}cGQ()@1-1l=EB)4`FvhST+Dp$_y@Xuw9GUK!qKaPE#2>I;^HO+2sQtF zA_HQqw;qV25>;fU#gkc?pZsn;8jUo7Xtn2BOp)TYh~1G)1bR@7J{41~ZTN{%nc#dx z1;+WpqF5VTV=n%t9^|B3+DY5mZLhyydK@4Kp5p=z@R+#z(*|o@1BY1oPFl=&+fd!% z`^rI)Vs?uSU>==~h5@$MTVg*z3|fym}9>Yck55SU2M@H zpSb^NWCr-ZAD5z=9QCK0xjSyJh(T%3l#GzoB+M?s_IqC0XtCEE-TssA7~(_tsKk$6 zAoK|H`5yYL%U>?bO|GQk#D( z+;!q|ueg-eH-|gPk`rro-O5mjG5+9 zQ6maT<5Ou_0({un(feDiDv5X^!_|>-K71wpMW?67^?{2`db~@Cz$qIHc`?C7J0HkW z{pz?&-U5H+=O@(LygD@ZHdnx^Q@ApDN3o$k$KXZHPMc~+ij+i8idcxc^fAK|wbTv! zLECx^q&^z%MJa=UrGw9sJu*8oteeCl(etqkIVk_b6m*`aIca6{psnXf3Y^rzB)Tb| zMhFfASPv+pTOx#Mfx#mUbt}8UZRIq}-;JX{fa;i@)x5@nlMslR>(}3sy2_=iliX16 zKjlrAb@>VV7B(yUpIx3G?^M28x{3f&F#7~)UxtS_?j#?3;5x1j|G?2;LC$yhix_!r->yI}9kt zo*Z3gwVrwdLfd!xwvv+&0I2{59z6*n47`mkQQ!wM6%&%6(hm#~oLJQO66iUI7;`ikkVY2{{`O&V5uTn6mzw&h5({VXRo z)pzn*`_^Jqo=4xz*GKC}6NO)fc`Y-dJM2>rgCvyzBI&k=)3vX@d!ZGWP+H~L`xPf2 zlYGY+n~BwP?6d0&!SKvq9SK6X)I!hv!IZe^BplCDpkq?*NI=Riw_HUiT(urMqxUD{ z@p;Gn&}29J`Zf+Ss_6FcH>kw^{N#IH%l_e3E}MNS7GB_vkpuG=BBk`opOS0j z)I1^<@mK3NRRG<`_s28eGlMh*Rn@VQ0gxaAXh>YQW4T^?x}R$}B@|T#CbP!eN|CH| zJjFZ{_tD}Wk(s=K$ZR=g0hZPgcfkwpA7@pNmw##$bdM=XvXiD* z6Dro<^E_f?vXtbiJ7VhI^8OeeT#fpAfQxM_=rUbz;2o|mbKPT|2NdfWAbhSTOi*)zwrPt`8O? zC*o`L1Sck6$!$n)KO4+$^^~f9zOB=g-_hwoY@mQT#Ok#*`>2}I9WStQjqSZ#LYkm% z4dHj9Ja1v$R%eQmF_vjnNn}R{XFsYejqe$_wnfQoaR4Df_Ef53wIZjN`RNqk$&rV$ z5xCZ&T=$z_(~UVBcg~uNFV?iAQ22^TqBw8n_0Gc%fg9I8#-(SPY7G>;8ag4fc6qwr zlt(FhzkYkim-v*1nF#WZJIN8O>B51H?@Q zsFXBLiMQ)QcHqR{vXX|_<_2c`+3z$0S9C|L)XY)0L5gsnt!)OXEq~kS4-- z%5io0jY`|OD_(T`8U_zeYCn$&C3Zh`0;E0wWy)Z|FmtpCbyFV@>VtVvId+uk-c!Dy z-7E4Ho)aD+vjadfwp6AUjA%v>6#x2tYmNRW&nq*2facUgP?bnbSU#sT(EFfET)T$T z?2QuZ=kHqCa>HMJj^-R3Jj`oapHqX?#xkT(z?9C^&+*kFcXXPW#NFSScoak@Mch6q zcW0$Vp7{Ns6HoU;1yC)Tc`kl=q>Fs%ZLK%Y7r`LmygtxWoY{@? z0UQXMjZO~MoVXwGQAX0&c){^Bg)eeiLuJ zNq(IBmskpYQuUH;wgw8K$rsy0sfyZ@Rir3aCQE4sa@32)V#*o3_HF_8A;Dk_BOf96 z{9_LbEwwh(u6q3kmD1`xwsMj@kW?}!jN!HFHLGn^m zYrFz^rqJBrecWyiV3vS=hq4$Rl$DFeKAHrRsR8`}f%a!Z2#``iW$g#}oZua^;Hk!^ z%e`sEX(g#hS#%Z|%!y;wEDaBQyaPG3=xDhSxc-pEa*b02HOG@4~~rrq;)Uo0l~ z&JD#DT$P<0@=>EL_UKd1s`TR>dRW?pqh}Ya z%a2@StV4aLm@>(%I`{oevSGBr+IvX2o$7k_%>T`#*H|xT=wpWA|I~CRPU|t}b1xR@ zfjV?Pq5o)|k$~Jel^}}97YX&)e>;l>_!TJvK`&-u-!RYfUP>@;-g z8I=P`SG30++UKb8;q(*dtPl2;4TQq9ws@2ox#!1y!MG68}8kZBGh6X z3a_c{yZib}q~>vRz$wdg`{P++zy^wqUM4A2q>t5SdM=6 zA_W~sBHY^X>x}6UQLF0R0dNra6Z1*~^7JN$kk6K}=*~E~z~!bO%k2t7`SZ?ro@IIG zV)9yjFx&1LW1jLjvihWXtZ(qUjkB&UL%#{%9Y0UENZl+ES4EpTD>ozDPyt6lYNN>z zG|xb?S-t?0&6}VxPNED+G{u!=Vc-^ED@wA9C7rjkU9)sFHSm8xd_W`e?4#OvY*Q;P z#Xi^LO2Kl&(<;&2*{Bf2^$)I*9~Anst++i`-CK&iL2lrTK1G9e9)VNILP)||w$}I1 zXuZmO@FA+i)Vv10Zhkkg!OV*xhcD~hJy8-TwgQrck$hakh1p?%MX$zvwR0`-;N{<1 zz`x{5#7NQjFi-zJ=pw7yv$?i7Q_Y1>oyR<;*&oJsd$#ly$4o%0Br*Wwx?V+J&T6!{ zFCQTxt6=2bt0O3;fzsg6Y#l+RakBo({F-UdRMGS6uy%p;zm>BEQ5 zel436Xv%RlaHQO+@tZ%aXU|~Nc_L@W2kvqc0nvm2K>1NWA{m5Uce>F||10{EBd`0f z*R_B=4j>fEE~s3BbQBSg)v!Z^KNc&nA>dv-bb4OI$M}wQdqAOq!sL;^OOPBdN1OtZ zyiwb9q-)^|#n9^_5^h5y)06gVmXlxEbT5@(o9ws%lyy)pjy*E}JS54a9=){h`)Tt+Cc6jiB zDNNonwu0xTvNAInI@hA8tgI}fH!B@zz^-UoEZ29fcvfC|B>F9pCjB|>(K};i!dGqK z$!UD1=@fgyZ6O3=E;!(;EP zU9d@VHF>s&4T;O1t@vbOi^ovYMCQ-8}OqqUG0%^y&$k zhF13PkwkzNxRDr`olb)Rotn(dA#Q= z>^NZ9)TgXNiM_$hF|%OeqaUmbm>1XcrfM8XEgsa56g&!O4hUQJQgtsI%Jsw^W;Tn# zymB(!i7H!_ufytBDPv0QT5ts{1^+;-9U|jIaV=hs$>^$)iqsD%q_aY0hkQEHsbp`a<x_`I58IT{D>yYc*`gIOJxgGR#YMWYs zcajdetpXjltP^8%$>%HI=p=gUkvvsSTc5VdBi;3{)SU7*h5K^^zdG5KIk2QwWa^DNyB^k970jm^E3^-&atUIdSZvzj=DC-Bct!}XKCa;gwK zY{_TkHopR7WbY|{`%c2*yIO#l#$Ef;Xr#aS5{%D zxaO|=>_OLip7v?qt+@v+xv+lQWTy~q+a%!e4K9jFY$RY~$O2p)V!lw%f1)bg-u6BV z0Gv*@4PDTE3Js=GVY~%(pOSuepIS+gkj_QVvRmlIh|G6gp-tlpp{!o%WZ>QPUx5Ht z9UJCVTc&u;#G%7)-e#gV{zayqNRe&SbQ_Fzj`u6j8b#P4iZqJ2(D@8`UB(K z=?WvS_J|^Hna7VG4;asNHl9|pS@Ot(Ru!OB*ki4l{G|&6{rifr#RI<0Njp>NDeQZc zJIV2Sw~=yb4!GQ5gPt&y82nqz^-y2GlXMWTzj~dDE8?A8mJ7gZE_*Q|r~`2RUB0k5 z*sOct(ouXx$#k+Tf1S_`&*ME8RIbu?Uj`wgL6yU3nHk0LKAk^kJ+Z$S1(Xt82Ha?$ z3nc^b-@9u6B7(s?y4@{xbjo$FDu7yPl^klX*rC^hkc`-?PmzCvM`eR(xK$%DJq0D6 zmpv*($=X%Wv59O>q!~Azho1}OeK=360HOzC{W=e-DbIZ=Ab$2s6tQL>3-}6G7nXep zDc7zagQPKJS_iMD<%bU`0v8=@NL-Fena9Mz;ZX0$XdAm^S*q=~1TZ{3hZh`yvq^J8j zo5pSMO5kCGKVcefUZD6S0lkfzc8lG__)+L}rJlhVAl3z@%8YyV0p{;^KW3`&WYUqB zQrl_eRODw_iVH;`7#;gBZyx$CF!+HR9I`qRd-byza?T04C>BR%G}2ub)%QFu+wYe8 zO1f&PnE;j2jiepiP!U&s<4)34NVBI)!|NM>L^b-_lNI~TUP#|b5~u*}b-91q>rfLx z5FBFQm1zV)`tRc0cM^%f_e8TeQlmxhX6QPSZMh;O;)FcQ$+zR7)W6{_>~XEtV4=`) zy%3RJg{5kLy1jSG2bV{cs?Zh|5d0UK$iJwYDmm&^dYv=2Vfpy*5Ql28UiGq zm}6mM!f{9ij^+-vOsyNfgE0sH5D@=w#IrzN>;vS*1UHxiB38k*rZE9q_vr#amx!?q z)SSZqN(JgZh!d*{1452KAd6r7Ow7S@!xr^;yvngl!vF+5HKqutQ3qt`b~r`@pR>4& zJ=dIa@WSUV)?FJ~_u<8*=~JK$?@)(^9JxU3;@GsP=un_lK+PiwyP|yHpvhkc=M*mu z18vN?NwxD9lgxW7cO>i|)Dc6{+1nrxK+;)60m&lpfde01%mxwJo{>9UgY0N8$2E>l z_*_5**w8=@k?AKun)3%%K#2wl>yB85jbB1<>4fOPtT9$W;630A0M-om-j~X;(#iDR z7jnYavB8xCRHD#W93>L);E_)j((pjn;wE0w_sSHg8s@#2qlzX1hI3p{vf^ccK%lR7 z#1o5v;D@8UqM|8a`H8wrr!9y-ih!X<%0T$_Cxz(cN}K)XtZkw60f3$io3$CW09XGd z=4mm@nU$UYAF}#1w#VVm@eyK*KV-97cX+=@*&(qYhi^b+I?-j@jh2xi>WJs&0MX1B zU_|M6eh!ozOTIXUdgPppZH;Q6e+Ux>?|E^({j^f-hh=9a*bGT8^uoGRF3J@!FXFe@ z{Eu>_h6(r(-h#3fp!b=+>$J#()=u*8D%q=uyZ7oQuKagy>&4_KNZjnjLoPhZHVZhrd_ zy2#6M8Tl42V5Ue5HeW+YMmyz}w0UGH7~BTz0l+y~qW&MK^U$|O_+VhUYzEEL-XOTa z9sm+=8DL5REEKB(*u)US@FE;QyykJtuW%7CwM{$%L}HB4Z!zp{V}Ei1 zn0_Wer)=>Lp7uXAI05q+Qt?PeoIaM@X}=*@~;_)eikKSdat41R{)5eaq<6|S^d{qFrv+Y3*{e6A(YWJ>>*j^ zalUb{gMpnyIS^Wjy2gL*fd5H|zt~s5--wSbxE^SfdalqZL&?GY-!x3{HVXA z(obNeEMOk{7M8#-J0d`62LT*Rhz5OxaA2Oo_~Hjoe}-D3@b%!U*IzNzp97dH#KTwa zLaIWdYyf^|4zG9lCLXl(>;IGo{2L+rb+R3?F97M~atGMv=xn=j^Ig!V6x15if6s}| z!~wfU^Rfhum(Qw=#|)`Sto_8Anwr_(f(noqmtxgM6G zXlAj#69uQf2?GWiRf{HT5F{W2Igg!IhP?fro_(<}PyZXK7g(vh_euIIIXmPy zl!<-Y@5TL*dta4JK$<}c#4BOL7HuGT!v6V(j2sh^KcXql6KNzv=^W|5B*dYj)PPHV zA?7J&^ZoRwG|g$^R~+2=)~B=AWO#BQJ1vgk0Gm(5DHw`~cSr)cSN}M7=g{Uozw;>L z1X|cq;4c5y<^|dOKM2!|XnMq01Zv@m&O-9a%7N0h^%jRII;UOHB80%KfZueKuOGgI z?V%SRk$iDYnL#(H+&^1J{5O`c56n^lSZCw8OwULM9XuQRv7uAva2d!99)uwC$B ziDo^2UDls|0;KUTK;ln2`tRGP01I~cG7})BbwmnqY`z?(1$cMJaDB%|PJn}m{pbY; z<01QFi;EPO60BO=y{_qs|#sTLFcO?chLk)1= z%h?~dLyyU*0K&EU{hE?%f4jwgSb_$|zn_AOJENf-2B3X1s^OKBjt*u0YA3o|z~rh= zT&(!T(o0~-Ct3w)AyzMBoh~#=8lcgEy4ngpj@Gbzo&l|Pu zoEx2^mmia*e~a;ZgtgaKvfX)YJH_uneJ=&6?>A~@L3MfIG$C2XwpLsJ!^7yL0mVA~ z1U+2p2&??%&o^M2)(6YV&$?f%2g`*D^($Ys;>H5DK&idWI&==xE=)}D!R_!LMNCjx zDKhuxcl_yQQ4gbc`AVp!kkmA*pbDAuygjzUqn;QhQV1q*12$oxHspJ$H1rUNl7>EN z(V2gHub4ZKHRP9iGmMe~^A@@-HsF$Q9Blp!0TeT{(_i}$-qrlJz)WTJzg_khgw1Wh z*ZhYyLL2y%M(Cl5g0HYX?=JGlfW|=Nb-jm~_e~snQltWa6fDbU%V-?UAkkmfZJ~^! z9~8f6QUp#Zuumj$H=LNIPxe$HbnZDB+M_U#raYPZFKG&pIa^P>{U?hR;EHih64CV{ zfB0iCOF*+@EAW7B2j0lVrboAvM7;oH=WUB=2?l5VXiGc9WK>TXN0YeXlmtWFng;T9 zyk!ZjeC>j5B}cUTn85j@8^DeKITwbAgH(Vw_*dZyCSjg&07HyS^@*u_O`ftrv7^VS z?rB3h)oRy0B=+)kCX#R3UVB)epV1rwDrJ#-Z<@TKeAHpL-|^-)%MZV}hq^oHhsGU%O9F?ykK^Z*yl6ooGNt{tFx_OPh6v{tGa=$3)z zMMzBY{kQ0K0_#uPfp=U*hSn%Y4rxa$oMCfg$S}34-f`{WmrG>S)YPY?T+!KJtod9& zCrI2TxMvvvLF}|IeQqPV9r!ZH6ul&z9_=^V^l!Xhog(8~jG~nCVx4b%pV>*8ZH#C* zH0XVO38e(Gy7Q99cQ=6~ZQgLqem)l(w1l?#ACzHDykaRnfrm%9^X<>c(9J=J{pz%l zKyQD%w{kyS!xb2ZDTRDto^>a=?9pD`N{7*%$17-9?4Y*Sdr~(^2n0Qj3|Ih{GbMTo z&hO+&Z`yL|xK@4T8gE&aTqZ4)TZCPI;RKETIvh=4DZ>Qe@5=grOvj?;umPI~L)3lo z4v(+&V%kEBV;HR%Vi#e=H7zkoLqcy1tVOn`&QFyrbZTs{4P9p(DF&zeRDgp5gZBYb zg0S@mZ^VFok?G2;d5avn7ZbR@jc$A0g{FsdwlIz-GP9s7gx$Y$~sCc9s0J@C7OD@AW7A@N;k~Zp9TUuy45>$zZ^6uXax>xy9{16pC`(sH(lm`cI1yC@A5QPtyeAv@NtS~Cj6xPJ(5 zh?~%YC5gi!GsC*msg`W~#BD(Mp8ocZ8x-KU>}m`kW|d}Gj7T5L*A27gZ0UWmd~*nQ zBjkR6>Pt9vP6^1o+m+&g1`pA!ZSXI?o1YPr+UDZBss6|BzSA2%?rN9uq<}h3%3dwl z?rf*J_a0z-O76DU>(s8Mx@+WV6$O824KWR^l3)YuH2@8V zy+I3f$4W)gQiyLfd^2N3Z_-}YigRkG(;lq<@0yehId$|ILty>J4oJ*YoZ5bfZe`>3}2D`k5bFHSwhV01_X!Yi) z{|C)c-8dnlednEcm{+`=`3=yKt~pvA)7=ZFlFb3=j&4J2gcJ!4FybIwJ*RI|r}Co} z`)s!3&(#qngbR5N73h`EM4m8cd*TgJU?&Fpp0C6*WB|U#DpRW^9zDVuWZm4Ymd4+T zG%PQDii24~6OOJZ8dKfBi?))E^3WRQegX=@=NsC_3ag6o$r-4F43kno4#Am0e z&XN3@ir<|^dM1`Vr!cs_xqj=$#^R=Y^8{aSw&^wQcDWFu)+<7D#kyvIVt0vG2J~bW z`D#7$>TD;T0111wv@hIp*~VkH)wv^H0CSNiA;|Muiq~OJE%5$iQB(Zb>%O9SY^U7? zE(^#s)g!ZMU731kP9$PVfpFjJ>QYa#0Q)Nw0g7avSPJg2; z`T9p3*rP$9gh>9>SohTzI;syB|8`UXyWTHCW|gEPMl70~bv7FWso91zjD9!}>r^@2 z4*hW?#V_=+^t%jG#K7)cz|2%SuuUqlQC@cqx|JHWvc_bT_VQHRWv`I~IM>$^@Hwj&GBNF zax8DMA`?Z;=2!(o=+^U(eBpPWq16Mh)unbTA1Y>`Ym9F23^eu{o5Q6jtr8}#rFcRX zNylB*rj0cU{Tv9t0a*I|iR|V6HR&Q43iys?OzB}vW2XxMr90lLh)#vQ^%B7{eA((P zy%_oVX)njI#T5Q2^@Vv4#eAc0E|K^^Sj%`S$SMKXIGDU3;RVxqTPO_@HCS|GCwA&| zv7Bo`+iQ)Ho7wl$1)#5N%kabPdE}`~x0G+$uSefHp#WCQ`)2_A1N~HHbObjx;#8S5 z<_hjREEd~vZmrypuY-X7yKz*o#JURThmWY`_KVkZWM6SCr{aX6q~MQF-{C%9n0=_i zoaBAnZz1mAjd|`#czrn6i4?s~Qk0V4j`CYx+&(5DebY;_a;4e&t})#dR{j+Uhv!8K z4#X4rBcHO>r$Qo5SG?ijVZNz-*eJHFCaOVkp+a|UBL0l?&8w?dR*p_Y+DVxU-y}jiOm*Lt+%$62O-#SAb z2}S|)mzg6A!yinqq7qRX+MOGOUC}a(7u_wP-(@>U^?WG)#TvvPrV%Rx7}OBQYyKX8 zFk4W>=O@FuZ1O0we6xRF4l($o$C9JN$`5Sdv*CsW=ux|BZ*4Hp$gg>OBezeEhbn?7 zkpqTLpK_+SZgLG->o9;st@$0dwj-^(dQ^0ro``j;=D8aQAa1wk25QEMd6Iiki=&Se z8mMYC#JWwooCuv(Tl92SQWmL|x04~*!u{>s|C6Q(LyXvZ6?W$T%xvH99T#cNtbt=t z0Y@i|WpDi+52!rz`O1^QT;567QaEt3c=j{&bVd8UMB^(~qG<@zp^Er>)%^zz;I{_c z57tc@fs1LD9T8zHSY1uEv(OddYQLBtfj@IR*U{0j4EihI$GFP$<_}wj$}f#o2%vd) zLm=3+w~@zNi077WSml7t2S23#QDDIEVwu777%d@R$PEbpmj_Q%?+IrkTgV*8$<<6y zG5yCTO@jO7jScm3D3?)-o+v6Kk5f31sXjVi2G{sCo<12+zUqGq(;Q$GP20udAo z+A{xcGx&$u=x+`MBib(P@uX`sCB_uZ?Vj(=TS1T&$a@d;&Ul8#)%2D2R8N0^O|67= ztZb;vMH*AboTq(L2s!F^rGHF2p?R-o-e0}zXBwA2d2_j_;b z?f$y^emqjD|Ni2L7uFDVd8>PUPbif91?dJqRRr=&1;rihmE4e{}et*FR*;>})^VOw=) z($6})mx@=l4MiH>vQDF4pul(j4m`BBtrK(1HpPzUu#smaMguBD_0Iz?DabecZrpyf zk2ln?AZ(Y(dp$*p(W$$h{|vpZCp@XrZZ>~QS}Fc>mZNr&fTNo&3VyJx@{!0=JS1;UGP5IIDe3ICM#alvhIBr zA6pEZa#h~%4;Mv!il9<%czv;DlmEVDLqg+mA2z*ILYwM{aWH35!AA0raJjakKq2Oa zanh-jIs+Q&ZSnX=+cbd@KW7Vnh&mr6sc*Qq9fLS+(pdAl<&8v3+UTX ze)=&9hRMQ`?u(Y6d7V{(1P~GL)6z^~qC7jnXHbK;PctO<^vyjUz0dkaipwCrdMeT& zF0Z4c`0W?Fb-tJy*1TPChO7y000o-EQ<7fl^7A5$p~=+z1m%7$f=;Tk9IVuhRIz8T< zaF<(6ya5WbZ`PhFJuZ^%b> zp6s)G9_(`f{}d(&?Fl%MT)azMzi4|RZ9E^BdOusAiYKm`sHlGD{fK4t;rg3cBX8oB zfviFE_aCQ9tn+q0zh{l{*yb(*1|zJ~H=#X-r}lSF*TR| zfC8v=^ztVaaq#hPm91(q<_c-vF!r*$viGf+%)sd{W&gj=f=^%LKl`6;oM_`_pF2@RgSAc zyGuPed2>|;4HW%{Aw_Tn;5}VN*Tdyrvx9mu`qd;_-6>L4gGR8hh>#yxz7OYoKI_oW zce*PTF8K_&rmvnfh^OYiCla`|TCIOCVeI@aO$_<7R|CuElV{Em$A=r|ZQT^Csreo| zMn}Aq0_xr39-9%dm%nLo=9rFIG=MRth+nGKZjV--%J)_V^9PZsV9y!1Vu!9bC|9Fk=jy*Vd86~I9nvN+yRl<-!6%Qbyiw}rO;t5VNi9kgM*j!ro| zb)9-XRW6^n)j!5Yat~o>RR#|1o-CcGc;bXho)JM#mJ!y!lQxygm?=sHFudB|C0x}0tVD&!WfvY&&Bv$M0LhCFqqR)UgL z6%3Xc`Eq5G2o3w$$|BqXQCQ7)c)hx`^zck^D&Wv*sVC_uCH`k^eY|`2^I{3a?)>+= z1;5G}$*o>aRk1la6hvcYQ>nB24;?h1d$sd=Jt#7vYh;%>8|7d@$xpw=cruxGUusPD zcA=!$<&g5G_v$NPj;Ni;VT5n<$7G`ydH1WU3Oe%0o_}jTuJRbXfBd$24KTkH=y%#2 z8FSehq-$8ojmuw7KFN91rp5by*LR%o&=yLk$@i1r}AEs z>U)w%=xkf4+?3M_lQTLEnB2hkMEj$D%JJS77rg!URo?6Qsu7IR+mgMhN~o)70Twjy zC$Fq#{C54H5Y{t@80wqyqAJ@{ttt4{2AG;huk%# z!s(FqSJNq{oVgvx=dEZnrYH0wzd6FDm??An{yl$I8qsz4O0&el%S=N~PbY$222Si2 zvKwNZrv~Qqc;1tI$}9q~fE{>=fWaAk8{w8QMf{`z%URiZqPMs9LH*Y03E%nksgpzM z$)mF#fA2Mcv-d~mp1n`ny}e6o6=%=B<5Hve7U%_cnEW~1_zlnd#^p$si=gxp-Je_ld4!tRB$?KSp^=+ymUC|3=cl?J@?}50pV7an^@1y! zh*dNbpCind0nGQ){pyf1>glCGZ`o#^qa67vLD=$Ejyy85bCBa+`TC;;Q?&N0y@`C` zH9vk*B9mg9WrqqMs{iV33Opr1;ZX_ny%j^dS%|vE^uRNotyEthFy(|psl=p*%1eFB zS{mRi

        {-H~q+ZZMvHGDkihAnjKxpeEUrozxOXg6M1PM@*B!CCVF+ z8(WdGjTY}Mj(F3I;wkC(iTT9xEIjpDRAZpiug08`0X{=aUno_gez-V-Xx48j33(6P zR6n2Q3$4tEt30-VYq(4hHd!ENlJR(K(v({~aHkiq>TdUevbDmF);OeR<&q8I4J&Z_ z1nerijdXLa2})NiV=NoWNH+1aG!d6|_!~Z!v4$ZS#z%_h5woo?BRRP&=7bW;rP(~65#LE=V?|bUWsGgPGLi*JQ-3uv zvjH&B@+^W8nDA*`K2CiIc#dBi%&nzTAcXJ#%=oq@{Sw*|Nlp}2HhEUbmpZFYi`2z7@ZZWo*N zxe`sV)ASq)d38!t``l*Rw}jEL1m7s0oM{(r*&N;8=2z=Uz?EZR9B6(mZfuKLx0KlI zzrH?O-H31yJAs&opzU@mGftFilZG0v&RDc2c~^)_HSFbjsti}JTYO%KY_0G#?Jwp% z!(FdMzMukXKer+$gda-)USmI+Fk80DDzQ2yk*P0u*+&AeQb3G{wo0j&iyCkb9$tBM zroNpvtr4d#K2FO{kH0egBoBAGmbz2e^iX#hr$T{Yz@$8BN0&#@(R#s>zMnL6I`#>( z-zt&p4U_bZyKlOqI(lyTIxLY)ulFZ?M5vUF!@AzlIrptGnuCLRkst>1x`|e&0vOJkK0U(4qT9ULjCZ) z%O-D0T#E{fa}HLU_@Qq6KINXYwpd{Q{svRK-Q(QikA^l}Sw1Wp8+Fy+mQ>yR@<=_b z&#q7ESf7xYDP??#;tNkxi`b?pa~eyz@?^nrqBYgJ{09&fx^rF^ zy%4?ATK9>zW};)#=~i?20eJ;3=AqRoY1!zh9w8oJm^sd2lcw-j`gx#zNNc%~4#wll z*6k?7n_QacZl=bImy>SppAws7Bq)a9Pu#+v3(uII{1_UbIyp56&H2>`wm%Bi7l!0% z#~D}I@{ERoy;wr4-3={aBavb<>D4|#?;J7VcNpo!N^R2)O1{Ylky10IA8~%y1Mon* zPq=jw9334n|5sVuanH9+kL&_0ay@hE+GDeQikf|y2M>aHlbqj4pJH| zY@8I5X_b>UJ5Ukx8}AV!nn@@(C54Oy9#1(_ZEM)RA~_pEz_JYKDZ*uPT;znK4W2CSp zBtBi3xyYQDsTHqPmhP=c#v$=F0+H#od>%$3k#)~1b`=m<>&Q)np&^66_eN}M?PG|e zJ%Pi`&ossCblJW69=rV+;YNmoo4Cfj&W82V0OjgZ&`j}wJ739-wd;Qq=k?YlCRfEf zQ6p>VnIzV<(Ef=R#ycE8ByW?MT>Mjmmc%Rm6`%m^2pHr|(c-(aC}Buu>Xg*|)5lJ? zOrJnM7C6Z9f}AsV-5W@vEEIFwc+n{yM2s*LYa7+lCySCzB2hV2BRtTYBQH$YAp3C; zD@sntr1a#ay@$Kr&#&a=X79_pn)YIJ;9kCea-2Ck zi@Be>^2u}uF~;C|?#fF&biG~4N{za&iifpS9Cat_9Kpnm-Iy33ZF)Dl)2^_sHYh<< zf%1pdrAq4Zisj!~tDS?TKH<(*U6=A46U=PhB^Q%vioadv#C!HZ70*-^?W=e`777k3QWon5}xGFC!XCgDP-Y@bNUSo3W(NjQ3}Pj zQnU6Wd>%73*vJu%=$$P!9!%8fuM@MwR7xJv6~8y#_rpf|cua{+zU}MR7xL)X#BQhb zIZg?R0ByBwA}uDnW1Mmn#lNN}?xUVe5Y5*~YudS%Tcx&~zweXfdp;v|A zp@juj=iq0SXYXXRmC(Hzwl@ruFAJ_i)To(xnS7$7Bv1zEy}scB`XW<=ub~_dx`;95 z9J7!+S)x1|Bkq${&6Q|d;Q4#(re=wyym2lMF$O1l!8Vfn-6sdZ#Vz#nuK*9ho&FL54p=H-tF{t5k*FhhIC5j)qAg%o}r}&q9*$s)eVQy0amCU2GQ)vaN zCUzq(et8XA;)yo>xsd}E_`GU-SWbd>(uUgyTs}LLYp{~9HK5_DH_aY{raW8wit*&s z&HufVp%B#^=bcD7wd_smU8 zIur(& z9QvIJ8J68h8df)VJCC#Hzf6bm`+G-Czv?Y?&e3;Q>K7a*M|jNd&*?I9uABC^?HA6vYlkv_p_*7+SOx@&=f?o)xZC*a}(epTLT9w zoG(v1%vQR>>dT9d3XR}+jgCAC_y4>sKW)V3Bb!3?*B6Kd?OlEi-*_eJP>|vK56TzN z3I36LRARrHrvYVLDxTJjk(=sc(T28Vmyn}h^ZckVTz=p<6$zD`&`OcN%u!k(T|FVn z_^m0>s~Gy;&jF2t3n?&LJI#s|z|5sT!*ui@e$n_f#iOy})idR1AM*s9~x z<0GPS43*-&+(*)4Y{Q`V>&1kYxBS3LC-G9X@jMt zF~-L5a?%I#(XUdop=>^D8Zxocrk!4ux@?fDwd^ik*4i3`qBYB=7sU(tXYmc5a9d|V z?NQV45Lsh3?hPg59ovYg6idJ(o6bqSJ%lK^hX0f$S$31{GKObw4v(9y2hRGo8_-H` zCeox>>*euRZ@J1*&LQPk8w34=byeN7Z}I*7)p%S?r9n|~?8ag&RAIPaBza=M0#gVPl1ry)iq#8~!c9xZGL!>!yQ3L4Tww+ag3cQ@%(U6dNDnQfXqA zB55fO8BXi`D4=fWIQ@@GE5+IQ8HflI2#wTLgcpT(S}y}S@4TUtfE2-9ym%;dH7%&i zM%%w|C;rjR=V2T|#ZGa&y6Jt+OovO#?URygqwLmi$S zMLofZaP1ble>JD}$FR7BqJX!vk27CLk3c z?{0ljJvhYQ=rs0q!u?IK#+bH%)}@%&9%Mgpr#i$Z5g>g>(VRl`Y-g`>5t4aSMF=^r zN`R2;B<-QS!TiyVk!1ysv!0Nz1O`Leo;SxBDi= zKe0fKN~zVl)xySXy2aN`CXeM!#1SmckyN^N&(*ltBZ7CnV5C$yaJ%Tr0KHHPMOVHU z`))6UPr3ATBSbIPfpQJGCRav!L%GNKj>Sa7CC8AW;{(e-{K4ato0d3(YVZBh=TOU{ zdk!VlbHj%N)lF?h_=+r*-(XOIF%ZmcOa@ep;eHJtXjoA1T0yIJpoe)4W`nRqUa0 z;hi-(K6qMc^m&@y6EK~CXqb{H!LH2lOcw_yMg243$QGzHqvdKBJ7Z;=L7vqWj4QiJ%zLg^PV{wEK)s>!0Bw@`*?$8<$%Cy&n}N@ zF0f*kt>rIUsj#0#La80U!ccRK7(X)6bz6FFh_vAL`XUvsCmuPL_oV>uZ_vBZGH3a8E`*P7F=hjik*j-1y_Ws&_uv$4GS4`EKT>tv zaVX!%gKZVcwK@fR>JJ;1aPjGpcRo8L^rE@Mu9ZzQL(rvN_x$7wBfnXL2^3@lo`j&{ z>n0Vg3HORZFdqAk^_X6c7IYb{CX_fDZ2A5&Wt;ogA1KyUO6={^$u=pGn<*$J(lo?vIxpSY zjcIS&tS7T3*qY>2zm@g#Z5sVR3y2X1NhPiQHi3#xc8CH3hby$E$Z|$5{;ZUJ$Y~mnF!VJ?+~rPGsVR2}1e^kgX*xF3CfBnH?G^$g~lIO|Zv+9$BnCSLC7O zmtat^r(yp^sM|7>(R~wh=R$N6rM-^|h zmGG*bwNA9qY^6m%-HPct4&6_G$kH2o=TYaRWs#NdDv$$??uE0zH^83qW~^qC ztRqzK6x*f%$$NU@Z*|h*q$+LtKZ$8tb>{xSRLkO)CYF7+8q{(+32o)Pznw0{e`9+s zYP}hdfkz_7*J5^W^nT3tZseS|n)XjPeUoCfiUNuh*sE|Rdk7t_a!83WA!nrtYKSHY zb_2y164EjLr|z~WO?`Zr{RF&_O)u+boXgjj9j1E8tSXLm9CmqN;9}?hs_hm}`4d6@ zxhG!7{LVe(4NLyl4?juw<$!I z(Uq~`_$w5+?seho2`$&x^mLU_H3pIRVOqsLZ*Bg@j)v7hHXkMY+HlaPnokYj3XeQw z{D}^PVnL)UfDj~e?pSkeGNDwc1v3!r?;qeV-_9i4K&%hhYqvWlwC3#pj%ZZt8=Sa5^dwr zn5t9GE%vWatDI%MJ@IMsV=MFr)(bIAbMgIy57Vuh&K)xmVVW=Qb7 z!< z*k3Y8Rp{bF3$xZ8c775Bc&u^`*OaKC++ifY?G70UFtga`T=@{-BZxXQkZIIa+X4YU z!vAN<12fN~#}GI_-Y43Bk(~-0}EahM}0%aj+9T)4Q;51ECG-t ze(Kl%GKAgO!b}!Yq(HkxBOqq}|Lrr0nvv9ii+#4XdTLTE2lO}57Lu%dG!p5)7~UyB z`aQ|x6#q#&a`+rngXow+To=Y;%IfW*-9!Pf>;=Z=kK^D4M9O3GazI<$)-~G$Uo(55Lwj>c3`elJIf{+&w)srt=}fFOPUBRii6&dh>tS8#dhF4=fR?_0zJzZ`PQ zrWm0RnJD$u>-L1Cg>Rdeif3q_1AS!I*@t!aL>N>@#CHgP|ERgUSB}*Zaj)5!`AMo2!R`llKm4f8-KKZGVEu|(}NETLbLvFWrYTN6S#SS27U(J6e`cdTEsxc=IydZae z0gRaF*Bx(0?rl2w>R!;N`mVt4dCgTne?-3i<>?2ui;&`NeRGGuw=f~#@6usl%e{j$ zvOkFDKUoyiNRWR>FhLm9^S96O|6ma(bX+||azXeibP#C0B`dsIWRF|AlevC5B39C! zZ@gEH^l$D1cpk5|#~n$8I_m*G{7ZYqN{@OCUQt#KLS4;MYd}^br4X=pU-o z-t-^T;W#x}g53Y44pZA06h{Muc;JnQGxI+dp6vbwB*A|YAy54y#8M@vMrkSG_Iv2H zKlNgzdRX9R;4oNaE!>SC4ZdobIQzGF0TGuB88wCfzo~b31rm$Dxlc_8YhrFlFt6TE z;~c8MW{rnfj3*v|>M8xfy+5w}z5nokSao=f&;3e`)9|)`)D{2%3rI*R3#d3#q?ye5 z^>&7^*>7G*#SYClld90%lJIiGXTkoy0xNr-=y*0}rwrUV*1sqN!#pQ_rOK215Q9Qv!A?8M`K(D3W7z0;w|h+y`NQ2VSX%!JY0qFEun6({ z)uG)T3EifslwE)xVY|>PoxL9@6;g^9LWo|F0ad->SHrvW=MNDku)jaTT-@cS?X*_w%&!^z!c763c= z;emSB--=|nCyr{zSLlAgHyin3&m6;l?<9d#3Q<_{?$m#gJpfTV6+~i#NHVhf$Sx6J zGSL2v^$QUD$n@7!0}fF)N(D2=jo2%ltrsO4`E7hD>4aCt~7=}?g%Fk}eJ z{U1amKfg1uv2hZ)OMkV?gO8DT{Q@pB*&3$7-GOySzd~QYojPE$Sg<&=UmQ~PcEomj zS)ee7(hFD>+kKgKY>^OTKO>7-3@T_%3jxFVE&i#kO#-ULVIIGbU1pPg{WpWnPnI5j zcV+*d(C++>@ljd7EUc1%uj&h&?XJ>0+GRaCF@e;7gHz`OgN&hNZ~BMj@*_%+7`g?& zM?EmfJGb)&ePElIf4%=|<|6_G@~&J_R#zVb6AqJ%fwk)OBD#YVzx*-MWNTlDQSkc> zrU)XG`&EocM4`rO(L3h-zs@3|ei`lLPHT?i?$?61HzmIc4()VcD9vB5fEEkJ#0#gm z1O8e-{yB!5^ABBy>(AUuYS}(MaotOs#6q(^EUFd~7f*!JoSOHK9<5 zH|{|03W1BiCP>YCUWnbUWu#jI7xV(^;)PyW08i!R5w>5G=(EQH?BQvuSKv0}kgf(P zV-F9leYM-x^7~a#6afzu!a*ke4^jmk6*oe9vTd&P?wPRm{y*fsby!vF*DfpzP(tZY z0coT|=@vnd4wX(tK_#S{1)`*YfKmd|-Q9>164C;Ku;_+G_u@PgaR2t+@B4n|eCOYD zxvq7o7d&&$XO3sYJ?=5aro+(mI7(mCN zM)zNSiOHs`Irz04Z_j9gFCo5CFeZO^PYL^6eal{#s6(OF zn!r|zi66(iuD#FtNdZ%#ZpjtEjz}mW6BmfTzlR7{h0i5WbX`($2SAz}Z~&k&#SkU4 zFWq*%#7)ynuO9kyJ+Z6VT^H6jlA{j}Jp*2h7El zAU^2yf&vxz8rm%@qt_=_*wIM9pMFxXDdHF8nH!eK^9^gng?G1K?P|KFXdH4vnIe2t z%nZS~+QE_mKZ>5L4CD;>@qoYxSG*tv{!ETum|h&V*d;|l#WomwZg;sevl~{stV&MW zT33haG@$H2*~7~vVzyi%L&^)YzadZDec7~RLK|FU*ob-2cI#}&b^TczN4Sw(lqPoj=qy*6YPX_RO zqrs{YkWnEF2a7VDnE@Jv5ws{ir~YXf!e96A28{PETIYy{me*SD)s{2&gdhfNz~3C6 z^L5V~Dg2Md^xv1iotXqA=bXO%Pe0+Wos(<`0oU^Q2K7`uwLIY63PN=DFRv8_|MTq7 z-_G2Y%^i|Q2v@e|^|P1d174GdS+Gd$z5gr;{`R+$%(I;L|K{?FSz$qE# zu9nyT;K7}Xo;4NP;P`>&$@AWC-;+@5k~A!kVA;O@-$=))!IA|(Dq+I(kOcT0(*$`^ zZ^Xt#YmOg8Zdm06;)`QrR@7OFQX}t=wZvXXn4^yObV98kanZzk0?c|{Ps3-9dM&|I zIZl)jYF7-#B7X;&^OY+CXSyF$zLA1FP3r|a=vF|n3Umd!iK7jlL?hHQF`*9k%QNFq zxX|IVcK_`pjtH*_5kM0^RWV0X`w6_FCk!V?Q_R<(V2G2l&Zvc;lnBo^8>G8EDt?`l{rpUxJ)E z*y>L`g@Q^T44s#_;hj(&$e)w{uT};>vb@FQtVH8at$o3QUF+7DjX=Xw@RQa)Z~?SBdSS(zKz_XlNSrFAv>_irua3R9z>;|93;q`&$w&ak z@?y4rm9iZ@?g0q`Pi7{Ki;&VQ@5`}#E!S7Uyf^GbAo2&!_ zTtm*nOCU9P5CA@OCy+GI33+xbW}A(1M1OcR-Og=VY09cLL=7%b~PRdb%y> z_X8fM6M7tRIPZB0)afoUfX9ix{x`=T^aoOo^z+7i(%bL9`nCSob}6sL(CGh{n+uGS zE#MRb)p@bj;1#qX@yf+9r7z?_SJ_ctuBdX=x0kQZ{vSHIpz}F?o95dv{;FS3&lL|O zL|9h<9kl!EGncd07tjU}xB#CqQD7Fe$b(i_&tRpB2Yk{gqz`E4geW@e3v+hT_^YHCoJC!le-3a6=PrD+~AQZvNcoxi73(0O6@i|j))VFCl=tC4$;rF=a zwbM8VRK5ew87R`3!LOrFo^8TyP+WT2Op3r7n1;IBEguXZr|~;T zqtOV_dYnZHpu!Gvo?Opd`)i&RXT=o%jA7)ILD>Htd)@U+w?iTPc*D>+CeTdw9 zyiP<;zV_elv>Rx*xJww^cVa zLimZ94GTYwx-8`uxh>%A1G2c?Qa(9i+^$baz4E!t{^44^=|I2${}N;8DMbzm1^4I0 zYyidOg<|g@k$)}O3bnh?t5!q_pF~1U#XoXrbAoP@8F!EF6W?qH6G2kt2jqMAw(o_} ze$7=+R3sJhT&?MJxo?Y3Td){DV*~Lavf4ow7PP7@m+q|KdWNT6W%C|24C*^s!B~u+ zCt~OE%JDczGe?4Uv1ER;6xqefzjZ(t)D`6G%@y32Z=j+XfI?>}G%3axE6*d1io7Y=^tRyBVAxj8c#@-ZrV6rYa} zaftSw?(EH5gW^a4$bkBtzkrJ&Rmt2&P2(4{9ns-0kE3rtOEuAcG(KYOg#Po1A^H($ zDXowlkPpc-2y z-cz|*@oIDi=!%_oueI8xEyDHw#mfunD%bhkf;!@P2CHE(HxTW^FTod2&l3Tb$*D-* z{Tr@Skl8Sh*g5dqXuDSF5g{QQ!o7--S+!AOyyplQJ=0vW7A$kZEr(*t|$MygaxCAK2xm>c1c` zGI&8EQw-Uca-SXyuL!$dn$`XqU!0~~$ChtZ>yz91K7X{BZ5{yr)OYvV^%E38LvEsN z)MA0IMQX;QI;hX#lTMd(x07T1qQmUp8(&;CPC{ynFN7@a{n|T}_0|~0bam;CO(vD|TG%h#rrQjXnHP z`3$+Wps`u0U1Zu^5*@zXwfU4w)!gD|Vz$+@Zz=@T1~b_vzh?(#MNbT7YF{1yvfr%a zaF0ngo;o;~FJozSkvMqev5i4M| zEwu5rI}d$iYYx3_PNaNA8Vrae5HO%TdbNF>-)c0;J%ZC+hu7*i3E=8AwMTLldaj`< zQqEo47kjEbtrP;9lVyo(&?-iqEXag6;eIsP{UP>fTd_+ObSbMIG&7VA zro?{M75~+TC)|2sH}HV6-kiAP1on|qVx~vj{%s?c+cPAQ1p^up4fuF<(@LU^UsxqU zUV(RT%E2+9PlG=;uhjs_yI8&>GYkzTrLRD1#W_9U730Oy!r8-}&or$yCEi= zHMsF}e7MNpBGw^SHQzW+(${8f%)S3%rp@$85l^1q2GMuUnYx436r#CmhphpL@|S&j zflWm;1O;;X-=j~ee_fIaNQJM}KhGtrdfeh~Q8t+F&c;Efa1BgbAq!P9{nA^h6Y7Cl z778Hed?%1U4)V3BKx5fd;+U3b{Z4xIf>OxsK|=B4H8SZFrmC^mQad@JCM<}^P4>V# zbXUw@byzK6)aAfrJ&6LE;WvwG8V!?uMPhi%A{q6{@pMA7?4JndC*vt2JM(4|GE*MH z4!Vk&=%swHX4g>rbJU+CcB{z-Q^7;WleT{;cNFl!C10f%38ohFa>x~#e{&fu8U^Ng z&LkhuO_Hvw2N`%WCcg0Jr`o16O=hxL`Ahs?0rr|PVq72*bGNa-XpA^cG5!W4x zM-Q-=1V*Np8*pFaPNBA$`JPEeYcelfWQJTGz705E(J+tCCHsunp%+d+@Y{r+K@nOx zGlJqmwEmy}Bqk>CvIZ!aa8dt*tL|&x@UEElabO;CMQ<>oN!$y%I#l*7+;VodQ9phU($~*~A);G!@b6ArM{M*$TsNTK2#oCyYBF3VgQ-Ctq zm>J9-R>jTx2t$pOTa~u5zn?3&{4LeaWbpvQ6NlYkUAd&3q+e!w9L(}FUrELqnW-`rZmUgJc@`AU-J2z`@OWwU9l<>?7_}LU zMA=S~+4(fQrF&7`A;WAN9QUQJkFYxFjC{Q^9qdjO)hxyPzsn=;Uh=rJG;6UvH<9cc zHQW)$ry{d?5@1!Y1k+Ys_d|G{xE^^j>wHehW3v+P&=sX%(KycV{D|e^Tz$taLa~<5 zn3V}lLU~kBtI{DF6=%CJX)1T9nSbP!HIOaoeTauir*F1*Zk%| zc$}&we)o5kGex#$o>xubgP+Ag+8GJF9=Dzp_-m~ z*>w`xb&(NrlbVI)Pu>@WzPZM-;n48u&Dt2RuoHsd7q3;iL~MNG^pyY{)0!>a8|pRv zlkgfL8kyeXt(ASt_szqYZemzur+V~Mn_~GuuPqG|e9;u+p8vnK4eX=ZUQnH*&9k>f z`-W{^)LaI7rE85k7Bi7_NX8`hm9zmQ5-WcxP>b{Z23-)f@E)#myddWNJ`>giA|xZL zK~mk?Sgr4^rZ!zdR`s0F0DkJM`fw7~F!?6+hH(IY>Ilwn5)KG&zeu$3~m`iEQ!-;wqs-HOlJyxW=#tocqG zcq0sN?#GJABb4HXg~{FNc`$li3+0Vc@fRr-9^6*XZu* z#9HX}R547qLN)b-60_eOpW>T(E&T(?*7elu<**eS@TtW`@otP{hXjVQdFPqFA6>B> zmo?(jErNZ0P*Rx+r{)%@TF@i>K4_txoZQP8Q>&YPLxNC6_lW@B}POW2OX-%9FG1sN7 zWwrI3l_?>b41d~nxWb{k040~gU-@AJIYIwPgQBXfD=k8V3C$DH?IBDW5)oj^wq$oV zjjpi{>quP@hU##Zpnsfp+3=J1yG6dHnLy1BSY3*p`Ol2hebI0@SD>KK?fSrqNFvpS z$l5E4wg}reowCQ3{uaNlukBEW(wWrPJOor56uEvY&e@)UZ%#=#tPV{%1$ezr$QK$n z?4Mp!Z|MB4_aut)B6cX8m>^V6ov@oBVFO_&C<`8&sWlR;Yx{^wVK33qA8p0CTl?UFeM7(2v7f4;?tGIgX*{TG4m1PX>AiEc;-SR)OMF z*?}dD&T9ti&Vu4IIG3BEt>h6MYF|AOe!?gvstgGm^g(c90Bepg$Jx6m>F}PD8By&= zFLas6+5|=?Xumv)M_MBq=S0FAhvBUv$$vtb&A%G)?onLnkVXTSDzk)o`u$Wjc7uus zb6k;hEC~$h)huxe{!*#-J{75Ot2TGe?c+3-L@`YVcgyxP3+ugZ!rKPzZVRA96P{93 z7}Rj`3CJMV&b4lZa{D_n&I-ch!<2)?C4=O@$)n&_MAfFWvdtWupH;_9T0>cmWnmtv zZKU1FUDa2fw}iy|RTj@Jbrm5it%pUAg}{!PqwAaBb;2TNDRN%<^)7qM#1HI<;qZt; zaU>nZMw^Kb$$nhj)35Tj@dphy2!Whz49`-Y+)StJcP}4*3rZw^0>;|}%Dty^x5uc% z+Dj9=5HIGMw!_YxEr8-1*2-4C5+^nH@fY*(*3QBp*4{_d-lVh<=6^J=wKWVJZfgQa z^H$4|lOXlKy$9ka$b#l?vLa9sG6sMD24gfDp7Lg4Y6RccbD!px2r<4Z&R!V0fz1~M zK#)n0S1KmSH>~~HSer6HNIQNjq>ja^ra$t5Dk}TuV$JTb&6B2xg>X9&I)pNa>Ty?6 z+akJ!mQwx9I5X855~^}xby)qnjut*I>z=a$pXcrP9*d!!wh_!jsHc;m5wgkhdnK2Q`p+o-ILpx-+Gz_+7EMk=FGn!Jr+9{`%uqg&p5m+J@7ntb zplScjce@%jmWeh4^0S@rnW*W-$P$q-{#MUE`D4%FWrvBHrV5W^6@5c862&G4M7sw@ z_D_z!$s;Akn2@Wtab`9I!mQwUo%Su$sAlgm3=hx}&M#5|s<}z=Wj4$$K zy#7>aJHK|gdo5)yDB?5D-bYuK<>6Azm2_^j!F+vzkhnko2~7z$gM0)5Hu24))*#)* z8(h1O$%yty(109wT=};!OTmV1pM0HOAkJkcC!8TCWn?2viR3hJXjJQYLbP`a?S3Sk zc@pq`(FmxCADR_<&=huo#u_ObMI!r1rAzx5;3OYDo+YmXB%+8^tqe z=@!3ArG8&5wNJ5curcI_*~?(I^-qd4$%b>;bM$>_?J6?QIT|}q2l`qihMnb6p8j)j z`{okuDA8BXKf~($0FuIWVAiAY?dU2`eNSV#G%}K9HItmfn+_3(Hg()gqf$5e=&KWG z%(OaQeW_4GezQ5(dJD;OtUdN=F|l_>?trR&V{W^?0~d!0%`jL~P3=(MO~n0yVQ}2T z${k&SO|O?37WpK?y?Lg2C^$;H)Z_{nAd@v00d4K~y6{d5a^22+5R>gM-)lCEq5n%Q z>@X0hZt|3NZhhZLS&p$#1BNUA+;FKjI1&VPd!nWlTITceCcApmfXi* z5DFI|3>{rXf4g;eYrbczfRU(PeVA3azUnq95cFG?gjq6CT#t&FCtE^%&3cljP{)TA zdxtV{`D3@nT`H_6>{j={WY;XpeZqnlEtXq5D=(UF2EO5Yu6Z|rIxM?{ljqg;BDJ9N zWJ-WRLZ)qPth_455g5%Z)r*v?bebRnkg-L0HH;WJ^KHeIe=7Fs_&oow(p5iq*PTnO zi1zhqmO9zP9~Rqhi^HVmyHR(ec#UzIRjwy@y}#wq@6>v)<_;;+^u~dm*=x7w25m_# z-wjoaCo8@`Pt3f0oj)^wWGs6uz-&A%+_{EsZ!k}j7!>385FP^lovdEzaQ@Ld7T^s< zCNwp>Tv?*~5YS65q0nC^J(jOm_e*qb1;|oDQMYQ^OO|7@mT^|4t`7>DVEF7UF-^tE z+J4+DIME_+r6^*t<4!Wi!7*p3E9jek}q-n@an+Uob1=?{>>B#9me+uvFd`{X{Lnl#X5Z;nB|)QKGc zvf{#gnhLR6oAbi%wG8uLb6^iBX;#BR3N@&S-VuR9Nt$Wk_Tmeb<&A+igD$EFE_%z; z*xjXf9p4e1Q_kTIjKN3-zRRqPMYP9@*0)W&AiMF@Gg8_24J2+--WL+krMy{3PLZM( zI!UpP!lMbrpO7c5OCk%7hj1qmqr9Kb;)iD%mG)B~buGU=W#jruk4CiT0;`dA7rR@A zsKRh^ugJ3!x z^+7?wI)eah!_rqJ%1+}jI6471=ogefW1qgsMkwNL3A%>Pp(Aj2wWg9##DRUZ))l|n zVYwN=Wa|LD5ew5QG?@ftoL`e=L+&~~`SL>CIbf})%0eMUIrjRE8xsJGWVARbq;IjX zOQ(&Fgjg@}l5km`4fERPM~OOH3?B+wM&*@&N+wwRPfe zEk9POyDQxV#KJ6G`6oJ;1ei5OZ~MrSZOs-lyWUj3k>0cmX5I`sq_u~V88`ZTfs4N` zaOiZ2y-vmdoI3z$2M|?EL=;pU$+Ui2cFY!y@l<|QxF%LZedC}k6fx!94kKE&`Ry4l#_+=m2sR*B{8YutLn z%M(4rV{StQ#)SaDki+l*kZ6JZrh~f;D6-`?Jz%I&f4^ohlQp?M)m1MPN@EP$y4Fn$ z9Fn^rR(NG}a;KJnV3bJAg}2{I58r-iusO)QxWMTPk*~9>7tF)D_PWm|^b`_UeQzG# zhk#L4AlTOeV&jCap&3vs&vFALudE0}lUO7qEgXm@{2l0Wh^Tkj#?gOJa@<4TQld)D zqR!wah?;D|Wen{Crc~Xm{l=&#JWQs|WDb?s*Dq7%6y(@ctQ*?*}hDdTc$t!uGyw7928D{YjzyJ z`PF~w=7fO9Enx?XkbZOZh1P=``mR%}an+A}ZlNO3ZqK2D=zT+yWTt*TYH@S5e*-kx z{NM|CaARbB3)iQ5c+8l+y;xzj^0AItUk%j;c?a&=^@p8vnmaicg(QGxo6k?$qu|R7 zv7Tr&KCXJV=e%*b#{G~C!08K9Z#GE!GT&~Hwgwgw%Nm0L#|IW~A(U=}B1bSsudMCf z+S`|WID541n&m%yC0?}d9K11wFP$Cu3Q@GNWWBK_N`xi>Vtbtw>BI^Sc_^g3h&Uge z>i2X8BNed@20L6V!Zcvg1Co71i~%TDu|8P+;1-(3ydRvJUs1~ko8sc`nwTW$QdNS` zPjfTR(W)Xk$+WG>dhjjSLZl~G13zk6tl)$IG`R8!Fd(H7n#7&sNZa_C-V3XnjvFuE zy#O)+8RVCzE#P-Ad8+O2KBj%R7@<{VcI_?xHPaccg|7eu?uZt`CvH|Q5`Ofhow)Yk zz+`oK|Eh$`B$x{S`Vto>-sO0Owd zetfF(MxHShl0s&*KmzQ*a#sXL6&{eQyeWlX9}LPk04B-q6087)O5>G((@3QTM_J+z z7XZyL=X)rR7#JD)u4JKCGU*qiyaB&qu1@CLU>AJFhDhNn&jp$nH0l>RbsVXOf&mJb zq!NO1fz#!rtHrjte9TI*U2J(re(9#CST4qa^!)Gobh59E<2BO11L2-V<9GH-UWV8$ z^qKr|T;xhG?)qd?(_4!d=%h%c1xLIS z8w?P@6)x7l2>FqrlY8r6_XgI+ryLX%lz_=w57#u>S@GR~})!HemZ*y;PJAz#a; zaMomvRF=Y1rRe&pFDXd_l=ox_jh6~BV*AVRso8-n3j_$5VKDG@dZ*g14<-nx4);+& zvo!b%?MFUljA!JLb<4;N!s^=I8K!ADyN^bo8zwEK0+;OW;BcM&r7DnJ8NdI;y+1?k z)zYj|DfX&eisCjsPG*DgYR4I`1RKp@0v@rL`%lg80gDkEoawI1u+TkBM zt+trTiUeQn4Wc_C5s5cmr_5awETNYFz>;Z;&`s;ke2|BOszF6+e_k8T#pkye%CX(# zGAzbOSB}5;RalAJ`9QjMAy?DcO0V95>I180cOXexIhrDi-NhZMpaHgHs^;wuFDp3P z9`f0hXn+@m2cPCJfWCAJDIz4vu4Dv)Z@J)r7jy3F0#i*HBN@eQ%2dQygQWRQX5G}_`wfv15@8yiUq)r3 z#O;~x1yiL7WV&ZapXA1t(`&|4x+H#f4DlE$aG*Ck{*)6#SLToqUDGFZmpIDeUGj~5#yjq{1xO?O>$&hxSH28i-{mI>- zrySR(qUB4cayKR~1pzHVXHdR2s*F24(fAgm?C>7v5amHwGiYy|;kG44s9_;j{o;_b zcHaVk7*V&(uLo0#A3TKBZ5=;uqwa1?$AmyApChEsg9IK6GKSLN%qQPtwC83*C7S5rt&)b`P6u>Zdoz_w4b6$$oDf}<#Q11HI)mbMSr^>dXT_&gHRONHGUE`?v0gM zDTOAH(yCJeT)7vm0NC0}nVNlgQ7;s72pa<6+2hG%S(*xrDvujBroU@Yj(QS&y%_Q> z{KR#VyO_>62}7&eycC7b{cN`8OOXO<@8f7>djC_6k$&nF^|T+zKF`(zhlNao^xtGT z4!_qKH0#Ajd(tUypC0D(+}siox7aR7Mk{jYiQazlnK&(|DUZr^OBWpwm>m7K?xRUa z7w8DIL9S+*@@?=HW+DdkzAK^iwrj-w26Dje*qpnr3l1|O+atweZ3D)Y@n@m6)*kjE zI|NAJ?7Y3FL=k29j5HZk>ch9L^*#O;s^&t^LNi|+IQ0;6h?oVIT?sUaJpoh?NvMnI|#=KM-;rt#D57x6#F zTZu%$uexJ|;B@`-PFfgfVteJsPRupZmTC^}kq$=)G~u(@Iqt6gZW-0yEOZ-|uO5an z&?+0?eu`=u6ZmvLhO~mSP6>iEfd=~E5s*%4*}lR-kEY3pSlrD387<8Ii;BquZWIhlPU(uyks^WDzoV~J>A-C;6b+mO+Rh|FkwrtJbdUvfYe(AcJX^MO} z5xGZwhOK0P7N?92D}Wo`y&NuHd>6l(!5jpYv|jmuI$V>`zj>z-!)*M!tVM4GAt9Y~ zcPzgWrO5qqQq_iCBv#0G2^A>-;}u$?OoDERKdKk&&P&EGnrRJaRim4l_ScBw^(YcI z+z|MgY1l5RKU4QBN3A_Xeg5T0F*c9Mw>>kR-4&q=OGksA@89^p@a2zF&5*mppe}SX zR={$`CelCKkeYLj8_(}=ha1FWUT_v1;2dL<#fs=+?<+i-++G?qG$qA6%5Qu&^4)E( z_#N>JG!mo`E>N98Owd*}V7LvNWoQiIA+yv8Ukv8R)*GNhj0mmI8#95;9$T$E3~~g9 zo{syyru})3sDat$!`%bcq9OdIN2uhM1wht<-QLyu%bOOK|70#Pvy;)5c z?hoJ27$?ZlDZJQ|B=a?(RG}2hbGwcD*YEM0C(2wz5AyYJCmMZ=+q}j-tqmhsH07=0 z`_pA5d^EjLpaamlMK6P(`=UT;Hqt3A7Tn0ryKN67l04VEkcPEC*b&siNL)9avX&Fnu7qN|IH7jY)1NJ} zcvP5#qo(LO{!cvjHyF=dZLM5>-s?k-+yKl9`DhWdB=Cd5L_cYxB|`vhqn!dRYb{R1 zAsX|@JUsmc52^umH&o6M_*}oj_yK0}8epGhA|k-A2b>;bz(bLrUb6~^Tg4=)CEfcH zB-Dre8p2Xwd&p_GGLK8RTmKF0PumRJRf(rW(yfu2GWxkdO-wvTO*%ml@LwOA5-bd| zH&*8DQ!OEdMyqrj)9uA3syLWOpSy9M>Wd#?5`UtIzlSz{CGH1pxFj<%&{hwOVuzaj zi7$o4%r+YebXPg+9yuR#Om^IkdpaljU}unj@L2WFglYz^+sx#6lS+E}5U>XSG9(lOmw=vLc7 z$AON4Ot+N1oz=8)N~|0?oVb{WOGE4k--lKNLAw+XVvXpqde&2N2bYS_1Q(Zh5be2| z$vd4-)&nNa?iGtyg*eYqDv0X>eWtKP_v>#8m;L~w{p`7;Io`nPI3ntbg0lnZ4gG!7 zbwRA(wE$c(BVIJkdRfemQnKp5I-*3B=A6k1^}lV+y!dGL#T|5#s7UP{*ZL@thf1^^ zjm0<`t8=%Pr(hwi740d`=kgGHMMUK${CGG^#xGO)NbPHj(c0=Uvu+)4OzBG#GNf$K zwEzIMezB?{LZ)}2T7Rce?%7CUIQs`h`k|g1lv)G!lbvQD=7)i#FCcDtHIFdP-b|2i z4eq2U2Tu|6v^T_^GDeEu08J7*yJC%OpiNHCH9iKo@nt@de=6g0 zIvTGho!846epm0-30mUp8A@+$lj`YH?z>36{2~p9iO<164BPYP7*y}f2W1VgF7*q9 z(LF;Vy;v<2c`o1JzRVVx+iRJjT`f$+AP?uDC|`W{R0)Ria=+A*XD_SYV)YB7HhQ_y zIna6<$rFbsi?P0u%1Q^ihPx!v=pU3k1w@=9rcR0pFfhSPFF*Q6g2eMtYbYh}k9z!J zzFt!n(Y`CQ87ZA^xiUZ}c}?!u4QE}+Z?1&vxy@@fA#Hxk7XY)I_0nUF3H32?lugpsewd%wcQ#Ph=&U;+8ykc`q3rQ zj?gwEJ9^At>@5HC9V&cGt1%lS+^Nj5=0cD-!hleyYm~*OzQ0NVQYEVOJM%LO?=n~u zj*12X?SI7vl5;Nbn|urM;x=w%{^A}>H0nwRU>{)_r7f!@ww|K^qQ#RwwH^Fb3CVY? zrG&Ie^5w|@8>#Aw7Rr~;B&N>($@%(a%}DB9f#q>mRoPaBl)4&H_?{OfpOT+l6~aOzR)#>(3>SR$I9X}aLC|rhX60s4 zS|1T&d!i&Sd4d692NEM9aqVW@=O)(-CKE{hr9kcVtEIHIR}$|0KQkT(?GUYkl!@SX z5d6`+bdWi{2Lc8FrD0-|vsFV-n&qrDUL=Opdyw8x_36qx#oB0KXnjnvRi+0|UQ-fC z{f6{CAP^1nO^Scc3~IoqozvzA9{*oV8!sb5gcz&VkW*MJK#?Yxf@it!H=qF!$??L? zZVYI5u#917o`bPfvHZ@$)6{{Rgh;>{^F*OqRta}WkavwakrP}bpNkdFb}|+OTBgKI zk8CtxHR7ZF>y-T1J}|J2i5KGZ!@&E8e}QsBmq*`JJ;ta*`!W4B0WEa+6u=IZ1oNM7 zkg~rN{jP+CJg_E~wT)sxT!3~8PHYA=>;-HeZm<&V<)JsR=>Xle-A6vV@Z=1EW*}gP zHU^hM#fBKV@V6dHCTzhA;K%V07%^v<2|ye@u+SYQX2Gr^^u&*@{ly6UzrfD_`NIDz z_}v;Kh6@-OCS}t@JyU`X(-r7i_fCOyfa1#l=(AR?%1{IV@JJzQR1WQ>qp5zo2+x%3 z+}F-F15`G3b+9YMXygi89mE6PnA<5Vc=X1!A?5L@0D^gmW63Wu znx0#NA+Nr=RdKJ+Gd;kJdlAq|im^){=j0gPP z@@+dP9YNzoLkvWtwbjWR%(~y6xc6gL6_T*C56yp*4*)@$rGY6M3-sFswDeW8Th#41j$I!KEIy6q*yt(JJf*Z-@(>-9!qj?Q#3qN)<0$*(4k zFkSgN9poA^u~U@8aUgs7cu!xSytIG&3otZisMqQD7e@G}M>2p%#O_!bRfyY|Kz0{zQm&4+Ex6|K*%&Qw(*lu6M zb`6qIu>8MQ&UjSJD(twF5i8<4OS$;d%qTx?7!O;J&&xUR89iWgyIcn26dH;c**>|5 z^2Yz?)yvV?L5U=N)Q?dSaMLxiXuyHU?F#~5DO!-VFu?F=`-I&$r^qKDV27|~H>~P4 z=q#opvX+N^#MQk8zgS8!wUs&k$%dU5fL;wkSWi0RF+mvx4~6UU{3}Wkn^B>l^bcp7 z#81d9o63Y^q)TlGK1% z_&aN2MqQReWY~hDUPqXli6;p0*%uS*Q!U862gLZ)qIOOr<}^Tg+7csd;kJh%KcTD% z_(;kTft+5L-FUgu**>zXCHcrj2_{w^^<_pO@)bx27?pPt-*0%24KZ0$_GmEyt;EYF-ZWQn+kE)Or5!4}C!6LJ;NpRh zc|A?tpQ`{2DA$0Nffkzr2b|kKX*NVQ_H=7B6}s=vYw77?K(8PU^*fCJeSko6$&Ow_ zZvjLuBO0KXFE&2a{Igq6PIrqN+O5A*lFL$(z4LafV}zz{A3(RxGLVk7 z5@Gv!UQ&SONDl!zAs`KZ;a2r&)CJz#O(rP)ma}Du19Dk=$48hc@^M69F7{97A?-6w ztd{`J{opQ->>nrPTqk&2-@rVgX)t+seypwU!sweX%Sa^4;?0C^VD~|ix@e% zkL%>@vQ#1j0bIo&m8=1nkum#_7|c}9rwBz5ocbm1;MhAFj@wS3zjGub$h_7|x8k+1 zAV$#WV*flbIRgQwo?s5r(EHncqQ}b_>roQV5^tnRk1LRP9AT`3x6jP3UD1Il+eLfk zPeuVLoYVXB>;GU>{qf282?3&tT~}MRSOt_|BETUIY!5CkaJzoFe7wJr+RnOI{ZVN} zNGBB_*bu?q?>ZmrQvJjXwt4_pa@9q^31 z)Jsk5-QykKWzF~2CZm=C&Gi1($5(oXQ`cEt9~Jv6m^}!f5Wst6rR4=BI@ea+fomyR zW-;sqLN-XHPd0`4#@J^qHTm(e9rJu%Lv(Y8yZ?3Jv&(~TA3Ep`8h{V=m-}Qy3{f`# z&MO^AMhbEOEud>*x0R~ZG*=XLPqM7-b2QI}P!DGPGKSUBy5zpD)>w=pnh8Hs0KzkC zSNVJ}|1G?&N{4bd=J%-~HP&8yke@H(@i&h5pVzZ{S~p_|33*YVom!ogHlP*%5Cuh} zKaE#nwmEMJ;{kBKfe;IT{Gf+4t;(35*>K4fJZfQYD2=eQGSV7%Gg7-DgoK62O}OM9 zpu*Tk?A3`K@;qtau5sHZx)R$j+s@xv_`K=*bZ5dP!hv-FPa2<$q0bMMicK~J1_Hb& z(J0ZaxF>FAX z9FRy5cfOu18`NiY!RHFaqY?cgMeJH5Ix8^{fGMTRWQ=?+8_qLf44`oin4TZ2yi{h1 zqCqH3AIFI}+Se+Ob46R&`&3F(oO^12xop7iiU)pI=(EeH%dg({X#$-_IlKKV(9?aahTETbq;1ds+iR$s8&pkw*)K&v~j+D)HGNJy&D>7XG zB=bLG9u*ij(kTvcSAdeOEX}+(O>AT|YLh<9k3KU@zdQAb)WW2pyx>@k^C9630Dp|X zMI~xgISBzOv+uUMpAE2|MP=&A1JrG zVXKE_|0cb8VFSr_E+(e|8FHuN13ofl9z&lMQ0EDmW}Pid)pTqJ>Y+Tk=KRylvvgf; zH7#(bkn9h!Qy;`!_ivyE=;Umc3>?fEX#hoE$J%wog@%hERD3r?%2s_&(jE$zR|7h1 z6-VEiNZ&pkLN<`>%13Ye%|}W-720>VmVaK7#iMRYTUjdj;rTjXk(z*c;>*`dpQSS= zXUlPBxuTQ5?K}|1uao{78>}E*FTHb;LUxTcLvK@;-%@5jz;wIPiwc*5lf7A)^3C=O z0i$*A)B_6)P%DQ-e7PIJ^-wKvWuZ5HAn;-lH_i6U${X}Uyrd5*xW_*mc5+1u42C)9 z{NN`enYW#e_P(Z-0Pf9RQlj~}N%+gLB?y50feR3{6sn&BpajlIIiMFPqu{CVli5y+ z)-JkP0d>^PeP>E2T*!bV#EQfV=)=6b|JM}U-9|E+VrE%G~4cm z%Xe8WzAbU-KU@G9YTveg2@~c&O$|n7Sl$9`c;un8cODybhZ{`_5iG0bs~aG3xzM@` z5JUt5W!UX##I;vqcIo~Jae-e(okbTD$0w^_1=_NW&pIM*e6LfuSii1eR}~w*>G5dX zR5DSt$u0))T`M0Gsc|hh!9D_mh!5VqMO_IuS@W6FZjs#mxI zR%1nX@n1GR1g2J?p%{2S|G?=RfRQ-@7HO%~hHxlB)Y%)&{h~1_qtR)w`dRRS>>Hy^ z%?nvWv816_CiKtyGa!=Q^TBM>VtBpnp=B+%H7NXwyDl4$vS|LZ$X{DeV5T8*-lM%p zvUfH^!iG`Bvus&gEy?;ChxaA+b2I~;@ua$~*ieB(x%E}%U`b*?gM^Dz;J)d@fX^$7 zSG?NBK8QQB$NU=7{K1#n@m`te=KUOXl+Tx3-SYQ$I7mbvnj8VhHI&U!nJ=$d4$zTn z+FX}vc5mFcA!s|x4#pn@#gt~h;PC{lAGIpoN{0Gl z_bRdn-c}OjQfplkwh3)BHzX0~7+LD)ODk&h^amuOPn+ZJ35w}hVV`!2u=7$n{D}q1 z$aaad%%W7Ns@)Iq9vyHE^lWd=aY_+=@HG@{W11?OFEnp)k@hFLrqFKyBXT!v8d0$7 z*G-E8A>B$I`Nb!%4e!%Fke*pk$sRKZEx+m^|)QIi2POIv|yzlWRACI4)d}rV3PRvg{ zrtbMrBBbkR)sG948F*W6HSRHlC{wC_Bz)<3uZNh&amS7HmEpZ(JzbgYn~mENd5^>7 zZRsgG*>v1m{dNc9m>aYUePJH@b7=_c>zBJDW6dv0Od4w z(Rm&>cVD)hZ3_k@?xm)HCfvw*y@QW$2>)A=0sfH#d_GMHI!EjOI!BT)2SyqS#64AW zGkJ2heEN0$Lg3kgIZcZHP-93h;79*>i+}<&J)1j%AutaDqe|z8Ad(PA6LbR>gAH~+ z+#|3k-n1LwwQ?l&Fa4IXKw>4^<>S;p5)fXFKxA#1FhF(V5h4t?(HW3pFSK`Nt2a1q zVmC~H07dIG&aK*WXavoedy;jnL@Q1jOtIA(9JL2+jG)^xBKA5DQ)a7*dX69xrEuhj zGY{d@YjCG?$Z&d{`yc?xS8XVNONk88=u<$Oylj_7;0O9@Q6v{%-V1T^6A5m(6wl*=iD^V>l zyy?^NO3rI7mD^{x4n(*MZ71b>8T8Kv9&A;7Tg7_8*AF3qCOQKxiorplGoJLBT`ad8 zZS0IKe<>EXeg5Enk~L1R3ZEa_Nc$0>9quf*4BwtBuxQ8At8~KzNhv}u57GV{4Jx(d zdv8t_?dQi`KNG(TPCIj`!CW9H<7c7|*?wzfVDIDltb&dBAW%aR3dYg;f=_nuGc}^mz@)b#LH31N=Y)EJp*fL?A(d;UCV>Jft=S61GxeI6(TLqYLtaA!=Wn zje5Kpl?8?Wtj-3lenzJtvNnKLA9PH#5CW@<{`=-xyeNf=zpl+;3-^w-_fY@}g6~4e zVFTS}oLv-e?5RdU+=e971qv|WOCYRWI}pdqBvfy}XDK3EQUBN7DjH1>88bgfp#k#` zeeO@)T$*?FZ?t-l&jQLfXTH%VFPt5tGJrN%;WGZDg*>Y{z)~NtJSLC%2_gqLNJ9hl z{fB(afcS<%#3THM>00U&*-?0zhqBqT=UjGVca=_WcGbVorxGThho~w>}b#~Y46YBk@ z0MXgw>FEx~@FHnI$+5B#Ez@iX{ z@>AIEgT*BAx?}6TNctb5mwy4k1|f}6^t8)6XSgVeLp*H^OBev!#P8teN5Rcs60jLT zFjpCpm3xLEpY$$>t~Q^VTJWyGh5r!E>jETqD2E~9TQ>iZW^NOgZ}9vU9Vl%=W$ifM z!WLxpqV`Zs?312p3rC+3ZP$q&KI>M{ucnl){|^N#39~$_p-lTb`b0B?!C>)2{v_3J zQ+xyUA9#&mnctZXTaf+~1p}ch=)w@gZ%DV!mf7iJs$u+JLRG9Q;N4#Ntb7iuA(kGv zy=!}917UbbK8%CQ^Qi_iZsg8KAUD^ap?;~{>NhHe3BW#r99};DL`L5O-h{s4U;*mz zvE=v{Wsw`#|Ega+^KucRr<(()O{V}M=z4k%5OTqK>ivVMT+5?kHvyGWm*1jPbMvJy z#-}i-9EHc)Orm%N(}sK^txS30{&l5)z`;MD`T#WXQ#kIYIiMi?wdR{ZvPQrE)!tjU zMcsV=!%_+&l7iAoBZx>Uu_yvccZjfrw19LhsUlq>A+;!7lG3H5G}2uHN_Q^s%o|bf z`*ZvK2cGLEUb?XCyl2jwIdjfy&Up=|sGUu@!}49g39{JBJ$w}{mGP$a;!P4(Mfu=& zt}(I$15A#4$okt~h;llS9LtCcvv(Y@J zzWNeD!>8~vtU3N^XYX{3c^n0}67&FiL@I2o1ZXU7mPy*Jc z-{{8a3y(=iz#g%uF@zf7YNH&rzo3)NB*-)}R?bNu6v*eCSm%)C%fwm$ccI3 zE#U}Ah%Vp|F^Zf+wmVO^dbP|>_7gL8m1uKa@8~m^KL_%qpcLxM!j4J8ye^yaBrK}f zAXKjf=k}Wp8>AiL(S_+t#dOP#X5R?P%*uv~XSAjW#u}q`_2oX1; zz4v{E&xRt3FZ&({BJxaX4MA)uu~WViskM8sq)F1QMhWt0J0uDc0o4F@4(UZuhvx@e zF8v=FULZ}G*!f`FF7$1kZivScl#Cj8$*hb9ob)Wu*#t%X%gv+=gt{(B^a$TQRORKG zKN$z!<*Vi3d`OL8^}4gudm}h}q}25sRARAuH8Iz=>lW90A3{1kuVW(DERc17Sx}Gm zy?Eer({R(N{Vf>m`o!^y`*AJlM}7hp#rsf!gF>QMJ*eM$Hl0K9C~b#zB@mbpPk<$S zZ#!$y{i)*;;Q^@53II{=6u4ISa?1PtWOh=bXH4kt?1HVC8YnY2po7_ps>GRp*_R2{ zlNI_fk>=>LH-50HFuStk>)r2t>E38`Psp~fO+b^d%S3T(XWD-Wlx1}vkT*3%MI_%( z*L%bbGMJKyrs;HNHwRZKU}9=}k1?w#X#`!)NWNjcxFX3zz$8n#dnkwoQrv&s=3ucp zPtqQI^kQFUb2U$$-<{KbdE5_N{igdl%GY@K9bSfN);lr+dN0}fe3e|+4#NTyU>3YA zBek32=_rN@&3spsTyoYu$3NHQxeX+eVyZu=VMYh(#5L9Pco6^!6s?d-Tv50xOluFE zS9)+>9&~e%i0b*(Ty_BQh1AAAu_N7==nGPW0Yk&J2e0XU+h^QC4S{?d&EkkF_Je4ahkRQA zz_gXOeBBgJa8)IsLc*)(^1uz8mn1+huZl%10mluKK7O zqUNGcL9Uvdh<(a$Um_{KxIYLY@{(NGzF!ke%1%z+V-e7u>efB3VWCF>hy|Dr0aJzU zx7;pgV(q~T{h**CYXcr!C3J3}r5EfD!csbmbqY;tv^k|g>@4q|`}g=Dl%JVTJJAyi zfnukZMdt=!I3@UKM(!7%(a2dPooc#{AY|Ha;{Wi5J)~01(WUZXW{r8J4U*A`#6aQO znYp1-T%+S!*Y!&wuloH54G@gMm+FG!rEl8Y^2JaZ0%9}hyRBnErc zdBxGsf{Fm$o6qbe&@H5E29X#e19p^mPG|KcKN#TEpv&0l-Y~*DOw!c;0Nq$n@Od_# z;-sl8Z(>W%`yq!eS}LQ5fvsJXHdK-??55Sdf=^JaAQS##^onHGKwhrN!JWzwlH8`V z1@$WTQ6L5S`S-3wT|mv_LnLPYIP>d>&uTm;Y2r(_B`-mk}YG12^bKO7Kd;mnCXVz1syJn{ z1?UN)p}A&s-IhULhLQ{rdohU&KlfbE1mI88kUaX7WwruD@c<-=22U~g&o{M#iaUzL zSHYk1>#rAj0q!uR8vK+T$49F;%Gs({0QW-!NI1Uf)=F<9`dU#!abff4N%C8)^<@7D z-Q5rc<~a>g*K^ySK}0RzQvN{}<0D`!X%KC71Tr=k5kQ9<OTS2@bbQw5@a~WKG=o;9I+KBbLb zEKdSOSu(B20uc^jP?D~I$(SJ|0}f(MNLOVJRkO9PP~9o+rU;h`4UE?0HhJNVPknZn z8!?vn+#FL5RBAE+n|_x{wBSJv76=_2?xtEl__e~BfFHYrxo*=oFg+=U*IDPCvOja1e&Pa9HvSip#&kF(XTd7_Fewpq!W|Rf zf1(9Z*X2$}KPYnb@Fj@#hWVD{s3vBR8qt4s1C=kA0acq!pJGih4)Jy9Ak(9kX!Sq? zXZLovhx=pm*~hTewpjMSW6l};khDYfJpCUzNYprSp-I<8BR7GAdd{p$)OGA@J*{rM zlqLiUIj@2P-#epYKt)5iH58T8XT3lYt1ifQwx@Yqe-zXLgvjtAT3xmi-% zTDRV?X$h*&t*fw z^P}o)K^gXkB(!_g#EvEl0E1wX5CCd&eS| z6+X?6Jd_2ML7(XXIpSqOoy)2>G?TO8CAK$S1JK`{gMl6kqFDv`K2;^oGx*Bc>RLN| zmTONT7Yl*`0Cas5@hV?pbX;whqst#9)-#O0o-!Kg$M(G^ON;8`d|wSHGMY%8?I!;y zhwg8VIkLB(8%SUV49b8%AYBXR(ImC8X$+!n1c>iauONu|Pw>Rks&`pabYT1`chbG9 zE%jVp;Dj|^GcV&FI`1S@1@2DZcln-%rkZ!k$fm|baNGJxKnyqe+k&GcZ&ut|(Kip4 zs-5GUzAv-MM`NM@iya-vQy&1-YC#^6SX)Bg_7fnFCggUDfIT1(RLzPq?(aGWoo|P( zGReh$PFf4I0z{i-V;OcWP)*BzIXHdZs=};}+sAW7;CZNp`-Sufem#n48@7E}k<=8* z%eOL;pd|IMEVeovX-_la3As%=?@;`1i!WuplmXcxUm}AWZjyk=i0S=0MZ_NFwpA*U`63F65@?^4EN2l;S-zah~vSZZj`iI)K z_zAVAOF!V`+RsaDSda-S-?hKDWV$6U0(hTj_6__HdLEJmpnC90-UPTm9n@oxo_mEy zp59GAT46=3lp>cuRL2W;+&D?RLYFooL1kQ`nVym)SXdkY2gOTYUur|I8emM1{kDWT zShoYv++S@Z^*OQF-XIW4RDY-@hMv+<|f`a$}D>;TQpG z7jP})j-6xxD7ppljC)VA0PZf>&l`ZpRCp8KUU0#Bw%s>c#Y}%|-y!CUzsER8M2S1g zlzWZE78I51gY*T(>Qd64u+#@Z$(f{6g9S#+Z&IU#dpErgOvC-%68t_#t}D$-M+qCT zf^>(BYpdf4PtvEDL&~wOzcmM~HN&+8_TQ_);Bm)Qj-S5nF9|7O9=)=Q=^QT38O%!O zLJ41=h-A}3fAOkvn2j%2k>Aql$(INX4s8JRNZ{^&MGBBQxQW(N9ky})!5V+%GHR8Z zLvSPCO*HuX8PaymBwi-KlIMZ@ll34(s?4;=(HWcfFP@?klYEae@?0l!hO{pV>U*K&zX@MY)}4C(yJ1?X3v zYvjY(4W@yK=b@-J!{G4Ro{aa(V6@o`sLcV}PuX$|K=4^rOIPi-db`n|Fox z9Mp@PIq9oi!_W8Ho~PRT@Y=b2VzApNir1;Em$8UsbzF;e+1_=vpXm>h65;`+`xSG@ zIhcYJNwl$xY|fbq-DmHh2geMMPnd67JIQS~hteFgIOH68tPrzJOAIt^GMKPm6M8ZZLNIHl3*hkMI+l)F;_S>15C_nA^ z?}Pnn2OwE1%O%2=@AbC0-&4cuG1HQHDm8N8TzK|Ibn?c zNd|Yzhz&?p?Y!TUq6Nj;ssK-%A_v$LLu|1`L>BB5IL*>?5??X0QT5$XtYSomX`>tm za(m&$(<);WfLyL0X%O~!kWEc-44`E#g68Ic3aop|?ZtlD_oI7&gDrV1t-qE|FuK?@ z2o$>7F`$BgOpsCa<|RHcm94a$QEyr>wvN>W9Rof>OdZZ<9{v-r+lQo$J6uI4`%$FQ<3UkdDW@ zM%~zqy#puUaL$Fi2JU&#^W3#&Z}lPa(LqbuDE>z6)AD@LuMsb8eaT=S9x+pUlfdZv zwZ0r(uXH{5;WD4Ho{Ug_Fx<2ar9gwSlP4>kynhZVp4|Y;Gp+?5^aNO9j0)X_+VLnH zx={Gc*YIoUc;a{zFAStqF$eD{Vd{p+eLLYg5<$Nxrn6p`9>r;{n)`HqD3Oa*Ejwu= zrRB4y&o)St>@-nk^UnrsOIJRH0TMLmnbzmNLJ0wchLKRrsy~JgDWNwA) zL_5ef&s)V}v$2AF75#4*vY_nx6?H})udUQ*mHuoY> zYy)RM{}58Y4*xDJvy9u+v51RXjoEySpoo+W#BR?T`Wn$!3pU?+ab#clvYS2Eec-!2 zd2r^?Jqow4wO;(;N1|A3K(_h7VmslDt!dcMnp@&p8{hc%Ziw{_G^b_Yn++FgV?DpAz$n63ow_5ymV~9$)f)s?@U+grq#Z{`5L`P!wH^$1*qTagZ~^%xm_hqXkR~slnIg=K zr@cPhg8u9VNH!xWbpRaNGyu7G-9}A}BtE_66Ao|}sALI^$r_t}^>}IYTnWicDgUY( z7D|&~5xwCRIpW;_B4ACNPX0vp3~xA7CW#&@c!cVY(PA`Ma`G4I$f6Hp1*r3KoL46Jcl zwC=L{YDZyW6VS^0)U@+-Fbw<1fz-8JXlIM+Vr1oi<#qdom+C4ilPhhmlNLGjI`ehX@`^K-;KZ`Ke%$E#n75b zB*6;(0|xn=A-FFCbT0%84G)Ex@p!R*d~9^bZ_G8w->@xuWF)TSG23G;p$V zbF(P=?9u)f->Td)d?RGY0w$QmyW_088TH*&8VL?b#9w@6tw$)oU@YYHWF>m$qO6ej zQZk_#i+eS*!s9d3Nkf&c~a1aXI3x8X${cHcUi8d*@DRs@esiQJ(BXdo%2r8buIFs zXl9Qrj$(KvSF4q8++_`1O<13d!4Z70OdWxL7X#+`KKeNjpaDsm`2N0$MMecwHc5A3< z5Q3+l@l?b%o`}4+M%KXGUq3!yt^k5bbn=i3K_ZQu+1Ls6Hb z!MU~aC3{KQ+E+4b8~CZPW!G9e(Q7-?VxXlg+!2>X8vQuC{-DqfA(~3(?V!=^0iOMB zSl;&4kENPsGt`PP0xM((to;b(JgY&Z2m?01FzdVXdbA;UKqsH}Xhc8?Dw4nn1Qe)X zHX%GywJR`N5^tpwe7Lc5_XrSNOiu?NUn)hUyKnz6%A2+<qW{fr?bTQ6MHK#lueJx-VNnrhyZv)cpfv=3)HrvBi!F2UK_|S@1(uP zGNnbAp#`dnZmUGpl;&~PO&piQ{A97ndw}oI9@JaBuB#vaD#dZB|FOG4A^fAOc}e(C z4M$)15$N%Q@|s{g6moWN{(4Tmf;V-TyN?qK{9%Pe_$_UH!Uu{qYoSR+rPxn2MKEB) zTaiLQi?9jOAqgZxkoKDamD&uDJGJUR!M9Pj{$BEp;_- zN$v|}Ja)PqDRLW=l4eGphY=0kh8Q$_<7dNQt{5L<$k?qFuU^A#?6FMY)tyaYgp{cP z`)TnZ@7liEu}?6Q>qyinMWLKhr34T20}5i}YiNxU#Ghg!M9u;Q+-S zNz>!CSyKy$cg2AeAQpWru{qsr(Uc6V83b0nT!Qm&dyD6IYH#NsiX@%ri-*V{CiiNK z_yN*W3&r5^^gJYA<2QhxOFVCog$_L^IDq6)^v_=fiwBcc9TNRi$*$CyK@#Z4oy=$} z3W0hBO5$L=S6B+Up-w=C3KLs77bpOFz13Re8EeUufzk!pJe&R-9|td-+I5yMvgF{m z-q*zKjFD&N@jlP`e&Q7R}(yuHD5u)XKphn8Be4pg?y!z7ehxlj9-_X zchD1_>W3d7fd3fG>ZhHC+0688c$BX(+AKR1Iq`mjBj{lHifYxD0O_AH_$drLREj1B z`L2u++?>m7LQS*2PAXv^i+AAJ;brTVt&hHs9mi@)j>pbrz&Nh}dq0uto)=D$(s<Yu_QFE+$;-%>p2oNe5=Ij)a1K3w5dO@D@8##0p*0qEalDYoc{XU$^-UOP6`^GUwjLmwLRXmXOAsXmcyB z@r02Z4mY_7Ms=61*GifvyhNq1xTmIL|9l9{Glh7-rKN4E`EFzzVGSMr#o(^QhPq+P zN-E3BJ^OAfBST=?)y=n55)U($^uyYC{ce;mayLptwiMV@c*7u`%&$2Kxdhf>mU7vq zlj%g{SYf-*|BOdjCoxc<(TOacs?_oug0LxHWSL?obxVLt`?3&Y%LU*UoVLXL4yZT| zCl~J(3RINzwF?Bk6HhLek+^Aa3-`!uxp@8`a{aG$3 zX%UR^*Tz|(Mg1A_T;y3djNDsQ>1W*<$UHW2ltXGD8@gbgtDRMRRF=2AzfebR_5lLg zauTq;?j<;ST2{whId(jx;hdaNTZeH}b6*ucUfh^E$BPc8lbpHp)Y30$Pd5YwTj@9O zJtK*M?1%^h5WJeT9jgE@@NVW4Jz-4i9kAG=U3b?9fnXg>c>FYZKWFJY@iQuUT`aSWM2I5V!(k#GB7-`T&K$R%g)mBKRMZ^d;0b zdQ0m+rVCp+9kd-zDmwtR4Q=BXxlc1Zk(=b5V7la-+ujEM9^x6#c|yQaDr&pf^Nx4I zO24tOgmXHR2`v;dSv4Hz7f0+;2OxKT81K~VSC>@dt~@30JL{*bk4ZkXIc~}Wwn)|(_w1>;NA0pJ?G1iQotd+2;D*fK zQuzI%8HfYUJ9K;@%M*FBpPI~#HxAAr6=K@JCJ{(|Kok9XG3g)5_q*33qL3=R?`<;Y zS*5^<{50xT*)VU_SXta>C~Q7dIAA?(&c%Gy;BOCwj6K-6piRYZZ#IS_-euNTJ*!vm z97OK;Su|pMOEF( zYapR^f34|7EpEHTTZ2enEwdZg=UrVN+l1<%oXpnJ-5J&7qy#E3W&Vr{qNiH;dw`k#Z&r<4DB_NX8c>E}iIh&g8mL`8lbumeGX*Bec?Uy>sN zn2z?5jZ8 z`gFfj30*z(5#e0Y#sCH_rcbLNBpA>vKw#h}5#rARgW` zf)n6hM3h|9Csj7UepBcO7XrXuNy%a|pKp!oQTqw#{zOoC)Z(3Pl z49EO)5T_FJ%pg*CS?gPiOg(ySBr9S^7K;oBm|84O^5*}g}joD|GY&Lt^%~Idi zQ~pM+w;-1AkbJlFA>XiGcggB&v{17?!=>z7+!&L~eaV6O-;@KCq)Il2^RS92#IU>e=vqZ5AoBi0D-%|I( zXeU^PZ#cK`a;@P7UY z!TsxrK*Zf0^E4o?Om07J9kq68+7itpOjAo(#6!^-t#q2yey>nSRY#0|)Zd#KZcvos zi1V|Yl>@d-`0!iU?rQDBwfUE7`ioIk7HCQ}cZ+qs@unJL&h-ykgD@%YP*s5R=5v{U z&grQbm8)tJx*!xFzYTo8`*z>ZO-%{%KBW@xhRM{Jy@^|S#=&_l~n+#TyjGFp~6Z>nrJDEL4x4)C8#yd&Ixxx`!Tl{GU zp90CdWe15mYdTabud@ydGa(5M7W=bhrSV60bT07ky!Z1{x%n^g{v0hi>rI`B(!AQB z>gt?s;HUKIN+Bjw$B0y`?2#5F&ehVYJ8V3YJsqiSN?FE^y3aL_n^LP&3+t% z)dUJXpdQz+-sk*V-*QEE4tIQs_o?AV)p;BN{&e_4WuK-8uf~9#crXi5|73Kfuqw}} zcyys|V-f}n_)tBbs5eD|{lEPJvBoRxZ`3-7Rx6=Il?7FsMf;`R7y91=`S_kN4;w-V zPHlb8%bxy6h_;fg4>7ak%~#J+QFB!Dza#ogg7KH;&f-F5G;fWOqCQAqiD1~GSca{l z>9f@;ns@!nNVv(Wx_wg$7%=aGwt~joGFC)6(?ACtf8<#H0{`6S|7%j|I`ux0=Cu`R zHo710aRd&I3SYSdRKs}kI?Iyl&ai;2V5vh~NQ!J6>9FNTQI~`4bI!_&m38<2I};w@ zQhD;-FZ?md)ShBwTHMz71?GKO8gbeCJ3^!5l8=P`qEpyU6oDYZ400-xI4ZuO2Hd>I zdvKn9^HJzpNGp?D_)u}-KDhb&FT(f=V!Ft_ZsS0b0owwZsrgT8J%a}y)!@M|%4kNK z6qdyQ`l)#^w4AY_^B)8j;JFFG#2CA388a45D;4=4N= zZuiHYJ$VEsp?fA$@NZ26a4-XCRkEu1>m*3&xe7jki4FAxp;(=n^eO# z6m^s+9u%Z>5Jw1?-OE>)9`qG)1)uIa_t%b}Cot|UQyBq;0C`}mY0caE*1^>H5Vd55!7t)a2rn-=^R2Nr`8N~z%lZ}8HkCT=j}y!KY7`iT>yzvM#v6aj>X06)QlhjYi#s6=+ zXR(Hg%qaRt9qkG=t40M2ko;>e2HaO(=*{yfaWT`+(1TSO*2Amz5Nj6cepIr%jyvZ} z5l6$ck1FU$gd>&c95ktZo=X1PW^CbPRklB}+M2jdw}XdrsvL8f&W;v5_#*MXVH_2=Hpfw*i+ZSUQoOz=>W#rvu5xJQ`^Si_i4v10c9n?0XWanTL%w2d zey6H4ez+Y(EVq2B)X2%5qp~pY$!tU z&g+(gXdxzI;EYI3`8KA~dC%;k!CURZ#{tHJ)?@e-U)nb28j)!xQ%DUzmcai)G9lMI z%`8jqSmPH8Eq^DTN+yHLDi|uJ^MZk!W=1*U5D@dI zRoj5)|3F<&pTPV$0Rs2C+xUXoiUzokZm%IZCr13wj}Jy*vO;U|GcJ8a^Cfk?FARD| zdvn1Z!iQPbFG#NZ??VA@=+9R6z;-N6>1kf?Y~Bw;ipKmFcFmZ{LX&R2gKR+d{OwJz zV9O71AW^z+%^{hfwvsqqLw&C9+ZT9U%<+L?#a@HvS_1`7bsxwSav1YpsXF+Owzl-I zQ=bzgAisu4ijDeuc7-8Q)*h2 z;zL!P@7a6}&JJ!1P$(!+%BY>J%2w$K`;b6#DBpFiiM`CqKm(amML$Hy= zF*_+R%I~MzT$S8&w02lb14i0ULT%U85APcZ_2~FumO>7~5ZH=p43SOHtl)wSmW?a+VzIho|2&;K}1a9*aB{ zr*fP~V>{K=YP+~zpX({%K{=n*{HqBp)552($26)f>lOwd?$qLG-PoHW4kG7d6`ysF z?->6&;4ktB@az0hR&`3St2Dy( zluv$j{Yd3FW1==SYU33UmpaT2%Y}NwDJyFF^ZrA_L$>_y*VZ!u(>0|JC1SYG`Jr= z@?LOqsksqozfVpJZM-Ju|I(2sHiu9u1?TL{oLLxB*wwk>c&siOxiekqU;|s}J=$L? z=JMCj+h&mG0TZU8#OkT)FY+ODyv%L^3O&_xXH&)evGhM3t8H|@pWWGm6{pF}_(!`> z*^LX;`pL5M!HZpY*qkZXdgPkC9K3Ckndb6;y#4<^{kPrV w|4Hq)yZX-;|4-NcDUDy-|NpPo*zvhkiJM)WZL%-VfIkn#Wbc2tr}O;(1Fnw*hX4Qo diff --git a/xls/modules/zstd/img/ZSTD_decoder_wrapper.png b/xls/modules/zstd/img/ZSTD_decoder_wrapper.png index fec37b51a4c73771c92a9ed7e9df707ede385aa7..293420234fbe5d0d33195a3e10a6fc4bf331271d 100644 GIT binary patch delta 71433 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 YpuMjmWa5&w1N^6WLG65&tcm~s0}H=bM*si- delta 73872 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

        8DD_W@5MMJbjD5;*X% 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 TPBVD`{HJh9RW|#g$=&}2y!p-? From b04dbd59aeed320447680841167490c5af75cf7a Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Tue, 31 Dec 2024 17:24:34 +0100 Subject: [PATCH 40/46] modules/zstd/memory/axi_stream_remove_empty: add fifo module definition for verilog library Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/xls/modules/zstd/memory/BUILD b/xls/modules/zstd/memory/BUILD index 8b4bfd516f..82214b6bbf 100644 --- a/xls/modules/zstd/memory/BUILD +++ b/xls/modules/zstd/memory/BUILD @@ -243,6 +243,7 @@ verilog_library( name = "axi_stream_remove_empty_verilog_lib", srcs = [ ":axi_stream_remove_empty.v", + "//xls/modules/zstd:xls_fifo_wrapper.v", ], tags = ["manual"], ) From 082327ff2f6eed6fc41d5314af66c5b891756302 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Thu, 2 Jan 2025 11:21:32 +0100 Subject: [PATCH 41/46] [TEMP] modules/zstd/memory/mem_writer: Reduce the amount of random test cases Caused by timeouts after rebase - looks like regression in the performance of codegen Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/memory/mem_writer_cocotb_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xls/modules/zstd/memory/mem_writer_cocotb_test.py b/xls/modules/zstd/memory/mem_writer_cocotb_test.py index 54b9cee60f..bc7050a99d 100644 --- a/xls/modules/zstd/memory/mem_writer_cocotb_test.py +++ b/xls/modules/zstd/memory/mem_writer_cocotb_test.py @@ -206,7 +206,7 @@ async def ram_test_not_full_packets(dut): @cocotb.test(timeout_time=5000, timeout_unit="ms") async def ram_test_random(dut): mem_size = 2**ADDR_WIDTH - test_count = 200 + test_count = 50 (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) From 883f383dcfcc0ab4c2e12f18c133a183a057f212 Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Mon, 13 Jan 2025 13:39:49 +0100 Subject: [PATCH 42/46] modules/zstd/zstd_dec: use regular cast instead of checked_cast on status enums Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/zstd_dec.x | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/xls/modules/zstd/zstd_dec.x b/xls/modules/zstd/zstd_dec.x index a5f5460905..7bbd356cf4 100644 --- a/xls/modules/zstd/zstd_dec.x +++ b/xls/modules/zstd/zstd_dec.x @@ -351,7 +351,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = true; let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(status), + value: status as Data, }; State { fsm: Fsm::READ_CONFIG, csr_wr_req, csr_wr_req_valid, conf_cnt: CSR_REQS_MAX, ..zero!() } @@ -382,7 +382,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = all_collected; let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(status), + value: status as Data, }; State { @@ -405,7 +405,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = (fh_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(status), + value: status as Data, }; let fsm = match (fh_resp_valid, error) { @@ -433,7 +433,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = (bh_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(status), + value: status as Data, }; let fsm = match (bh_resp_valid, error, bh_resp.header.btype) { @@ -484,7 +484,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = (raw_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(status), + value: status as Data, }; let fsm = match (raw_resp_valid, error, state.block_last) { @@ -517,7 +517,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = (rle_resp_valid); let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(status), + value: status as Data, }; let fsm = match (rle_resp_valid, error, state.block_last) { @@ -566,7 +566,7 @@ proc ZstdDecoderInternal< let csr_wr_req_valid = true; let csr_wr_req = CsrWrReq { csr: csr(Csr::STATUS), - value: checked_cast(ZstdDecoderStatus::IDLE), + value: ZstdDecoderStatus::IDLE as Data, }; State { fsm: Fsm::IDLE, csr_wr_req, csr_wr_req_valid, ..zero!() } From 8a4c989b17fae58b9f324419c66fe18c5b0e502b Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Mon, 25 Nov 2024 14:05:46 +0100 Subject: [PATCH 43/46] modules/zstd: Add HashTable implementation Signed-off-by: Maciej Torhan --- xls/modules/zstd/BUILD | 83 ++++++ xls/modules/zstd/hash_table.x | 458 ++++++++++++++++++++++++++++++++++ 2 files changed, 541 insertions(+) create mode 100644 xls/modules/zstd/hash_table.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index e1baa49449..777faa95f2 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -1080,3 +1080,86 @@ py_test( "@com_google_protobuf//:protobuf_python", ], ) + +xls_dslx_library( + name = "hash_table_dslx", + srcs = [ + "hash_table.x", + ], + deps = [ + "//xls/examples:ram_dslx", + ], +) + +xls_dslx_test( + name = "hash_table_dslx_test", + library = ":hash_table_dslx", +) + +hash_table_codegen_args = common_codegen_args | { + "module_name": "HashTable", + "pipeline_stages": "16", +} + +xls_dslx_verilog( + name = "hash_table_verilog", + codegen_args = hash_table_codegen_args | { + "ram_configurations": "ram:1R1W:{rd_req}:{rd_resp}:{wr_req}:{wr_resp}:{latency}".format( + latency = 5, + rd_req = "hash_table__ram_read_req_s", + rd_resp = "hash_table__ram_read_resp_r", + wr_req = "hash_table__ram_write_req_s", + wr_resp = "hash_table__ram_write_resp_r", + ), + }, + dslx_top = "HashTableInst", + library = ":hash_table_dslx", + opt_ir_args = { + "inline_procs": "true", + "top": "__hash_table__HashTableInst__HashTable_0__HashTableWriteRespHandler_0_next", + }, + verilog_file = "hash_table.v", +) + +xls_benchmark_ir( + name = "hash_table_opt_ir_benchmark", + src = ":hash_table_verilog.opt.ir", + benchmark_ir_args = hash_table_codegen_args, +) + +xls_benchmark_verilog( + name = "hash_table_verilog_benchmark", + verilog_target = "hash_table_verilog", +) + +verilog_library( + name = "hash_table_verilog_lib", + srcs = [ + ":hash_table.v", + ], +) + +synthesize_rtl( + name = "hash_table_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + top_module = "HashTable", + deps = [ + ":hash_table_verilog_lib", + ], +) + +benchmark_synth( + name = "hash_table_benchmark_synth", + synth_target = ":hash_table_asap7", +) + +place_and_route( + name = "hash_table_place_and_route", + clock_period = "750", + core_padding_microns = 2, + min_pin_distance = "0.09", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":hash_table_asap7", + target_die_utilization_percentage = "10", +) diff --git a/xls/modules/zstd/hash_table.x b/xls/modules/zstd/hash_table.x new file mode 100644 index 0000000000..b2fd562f6e --- /dev/null +++ b/xls/modules/zstd/hash_table.x @@ -0,0 +1,458 @@ +// 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 std; + +import xls.examples.ram; + +pub struct HashTableReadReq< + KEY_W: u32, SIZE: u32, + SIZE_W: u32 = {std::clog2(SIZE + u32:1)} +> { + num_entries_log2: uN[SIZE_W], // number of HashTable entries used in the runtime + key: uN[KEY_W], +} + +pub struct HashTableReadResp { + is_match: bool, + value: uN[VALUE_W] +} + +pub struct HashTableWriteReq< + KEY_W: u32, VALUE_W: u32, SIZE: u32, + SIZE_W:u32 = { std::clog2(SIZE + u32:1)} +> { + num_entries_log2: uN[SIZE_W], // number of HashTable entries used in the runtime + key: uN[KEY_W], + value: uN[VALUE_W], +} + +pub struct HashTableWriteResp {} + +fn knuth_hash_slow(key: uN[KEY_W]) -> uN[HASH_W] { + (((key * CONSTANT) as u32) >> (u32:32 - HASH_W)) as uN[HASH_W] +} + +fn knuth_hash(key: uN[KEY_W]) -> uN[HASH_W] { + let result = for (i, result): (u32, uN[KEY_W]) in range(u32:0, u32:32) { + if (CONSTANT >> i) as u1 { result + (key << i) } else { result } + }(uN[KEY_W]:0); + + (result >> (u32:32 - HASH_W)) as uN[HASH_W] +} + +#[test] +fn knuth_hash_check() { + const KNUTH_CONSTANT = u32:0x1e35a7bd; + const HASH_W = u32:32; + + for (i, ()) in range(u32:0, u32:1 << u32:7) { + let hash_slow = knuth_hash_slow(i); + let hash_fast = knuth_hash(i); + assert_eq(hash_slow, hash_fast); + }(()); +} + +struct RamData { + value: uN[VALUE_W], + valid: bool, +} + +proc HashTableReadReqHandler< + KEY_W: u32, VALUE_W: u32, SIZE: u32, KNUTH_CONSTANT: u32, + HASH_W: u32 = {std::clog2(SIZE)}, + RAM_DATA_W: u32 = {VALUE_W + u32:1}, + RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(u32:1, RAM_DATA_W)} +> { + type ReadReq = HashTableReadReq; + type RamReadReq = ram::ReadReq; + + read_req_r: chan in; + ram_read_req_s: chan out; + + config( + read_req_r: chan in, + ram_read_req_s: chan out + ) { + (read_req_r, ram_read_req_s) + } + + init { } + + next(state: ()) { + let tok = join(); + + let (tok_read, read_req, read_req_valid) = + recv_non_blocking(tok, read_req_r, zero!()); + + let ram_read_req = if read_req_valid { + let hash_mask = (uN[HASH_W]:1 << read_req.num_entries_log2) - uN[HASH_W]:1; + let hash = knuth_hash(read_req.key) & hash_mask; + RamReadReq { addr: hash, mask: !uN[RAM_NUM_PARTITIONS]:0 } + } else { + zero!() + }; + + send_if(tok_read, ram_read_req_s, read_req_valid, ram_read_req); + } +} + +proc HashTableReadRespHandler< + VALUE_W: u32, + RAM_DATA_W: u32 = {VALUE_W + u32:1} // value width + data valid width, +> { + type RamReadResp = ram::ReadResp; + type ReadResp = HashTableReadResp; + + ram_read_resp_r: chan in; + read_resp_s: chan out; + + config( + ram_read_resp_r: chan in, + read_resp_s: chan out + ) { + (ram_read_resp_r, read_resp_s) + } + + init { } + + next(state: ()) { + let tok = join(); + + let (tok, ram_read_resp, ram_read_resp_valid) = + recv_non_blocking(tok, ram_read_resp_r, zero!()); + + let read_resp = if ram_read_resp_valid { + let ram_data = RamData { + value: (ram_read_resp.data >> u32:1) as uN[VALUE_W], + valid: ram_read_resp.data as u1, + }; + ReadResp { + is_match: ram_data.valid, + value: ram_data.value + } + } else { + zero!() + }; + + send_if(tok, read_resp_s, ram_read_resp_valid, read_resp); + } +} + +proc HashTableWriteReqHandler< + KEY_W: u32, VALUE_W: u32, SIZE: u32, KNUTH_CONSTANT: u32, + HASH_W: u32 = {std::clog2(SIZE)}, + RAM_DATA_W: u32 = {VALUE_W + u32:1}, + RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(u32:1, RAM_DATA_W)} +> { + type WriteReq = HashTableWriteReq; + type RamWriteReq = ram::WriteReq; + + write_req_r: chan in; + ram_write_req_s: chan out; + + config( + write_req_r: chan in, + ram_write_req_s: chan out + ) { + (write_req_r, ram_write_req_s) + } + + init { } + + next(state: ()) { + let tok = join(); + + let (tok_write, write_req, write_req_valid) = + recv_non_blocking(tok, write_req_r, zero!()); + + let ram_write_req = if write_req_valid { + let hash_mask = (uN[HASH_W]:1 << write_req.num_entries_log2) - uN[HASH_W]:1; + let hash = knuth_hash(write_req.key) & hash_mask; + let data = write_req.value ++ true; + RamWriteReq { addr: hash, data, mask: !uN[RAM_NUM_PARTITIONS]:0 } + } else { + zero!() + }; + + send_if(tok_write, ram_write_req_s, write_req_valid, ram_write_req); + } +} + +proc HashTableWriteRespHandler { + type RamWriteResp = ram::WriteResp; + type WriteResp = HashTableWriteResp; + + ram_write_resp_r: chan in; + write_resp_s: chan out; + + config( + ram_write_resp_r: chan in, + write_resp_s: chan out + ) { + (ram_write_resp_r, write_resp_s) + } + + init { } + + next(state: ()) { + let tok = join(); + + let (tok, _, ram_write_resp_valid) = + recv_non_blocking(tok, ram_write_resp_r, zero!()); + + send_if(tok, write_resp_s, ram_write_resp_valid, WriteResp {}); + } +} + +pub proc HashTable< + KEY_W: u32, VALUE_W: u32, SIZE: u32, + HASH_W: u32 = {std::clog2(SIZE)}, + KNUTH_CONSTANT: u32 = {u32:0x1e35a7bd}, + RAM_DATA_W: u32 = {VALUE_W + u32:1}, + RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(u32:1, RAM_DATA_W)} +> { + type ReadReq = HashTableReadReq; + type ReadResp = HashTableReadResp; + type WriteReq = HashTableWriteReq; + type WriteResp = HashTableWriteResp; + + type RamReadReq = ram::ReadReq; + type RamReadResp = ram::ReadResp; + type RamWriteReq = ram::WriteReq; + type RamWriteResp = ram::WriteResp; + + config( + read_req_r: chan in, + read_resp_s: chan out, + write_req_r: chan in, + write_resp_s: chan out, + ram_read_req_s: chan out, + ram_read_resp_r: chan in, + ram_write_req_s: chan out, + ram_write_resp_r: chan in + ) { + spawn HashTableReadReqHandler( + read_req_r, ram_read_req_s + ); + + spawn HashTableReadRespHandler( + ram_read_resp_r, read_resp_s + ); + spawn HashTableWriteReqHandler( + write_req_r, ram_write_req_s + ); + spawn HashTableWriteRespHandler( + ram_write_resp_r, write_resp_s + ); + } + + init { } + + next(state: ()) { } +} + +const INST_KEY_W = u32:32; +const INST_VALUE_W = u32:32; +const INST_SIZE = u32:512; +const INST_HASH_W = std::clog2(INST_SIZE); +const INST_RAM_DATA_W = INST_VALUE_W + u32:1; +const INST_RAM_NUM_PARTITIONS = ram::num_partitions(u32:1, INST_RAM_DATA_W); + +proc HashTableInst { + type InstReadReq = HashTableReadReq; + type InstReadResp = HashTableReadResp; + type InstWriteReq = HashTableWriteReq; + type InstWriteResp = HashTableWriteResp; + + type InstRamReadReq = ram::ReadReq; + type InstRamReadResp = ram::ReadResp; + type InstRamWriteReq = ram::WriteReq; + type InstRamWriteResp = ram::WriteResp; + + config( + read_req_r: chan in, + read_resp_s: chan out, + write_req_r: chan in, + write_resp_s: chan out, + ram_read_req_s: chan out, + ram_read_resp_r: chan in, + ram_write_req_s: chan out, + ram_write_resp_r: chan in + ) { + spawn HashTable( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r + ); + } + + init { } + + next(state: ()) { } +} + +const TEST_KEY_W = u32:32; +const TEST_VALUE_W = u32:32; +const TEST_SIZE = u32:512; +const TEST_SIZE_W = std::clog2(TEST_SIZE + u32:1); +const TEST_HASH_W = std::clog2(TEST_SIZE); +const TEST_RAM_DATA_W = TEST_VALUE_W + u32:1; +const TEST_WORD_PARTITION_SIZE = u32:1; +const TEST_RAM_NUM_PARTITIONS = ram::num_partitions(TEST_WORD_PARTITION_SIZE, TEST_RAM_DATA_W); +const TEST_SIMULTANEOUS_READ_WRITE_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_INITIALIZED = true; + +type TestReadReq = HashTableReadReq; +type TestReadResp = HashTableReadResp; +type TestWriteReq = HashTableWriteReq; +type TestWriteResp = HashTableWriteResp; + +type TestRamReadReq = ram::ReadReq; +type TestRamReadResp = ram::ReadResp; +type TestRamWriteReq = ram::WriteReq; +type TestRamWriteResp = ram::WriteResp; + +struct TestData { + num_entries_log2: uN[TEST_SIZE_W], + key: uN[TEST_KEY_W], + value: uN[TEST_VALUE_W] +} + +const TEST_DATA = TestData[32]:[ + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0x6109d84c, value: uN[TEST_VALUE_W]:0xdb370dd7}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:7, key: uN[TEST_KEY_W]:0xe773dc7f, value: uN[TEST_VALUE_W]:0xc8f9f817}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:8, key: uN[TEST_KEY_W]:0xd2254d4a, value: uN[TEST_VALUE_W]:0xa0b4c4bd}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0x4c794548, value: uN[TEST_VALUE_W]:0x8a3e6693}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:3, key: uN[TEST_KEY_W]:0xed1884be, value: uN[TEST_VALUE_W]:0x1787d635}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:5, key: uN[TEST_KEY_W]:0x6c40cc5d, value: uN[TEST_VALUE_W]:0x1e0916a3}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0xa7ad798c, value: uN[TEST_VALUE_W]:0x6efa1a96}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:3, key: uN[TEST_KEY_W]:0x8e3bb720, value: uN[TEST_VALUE_W]:0x6d0a7d57}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0xbf9f7bd4, value: uN[TEST_VALUE_W]:0x46ff026c}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:3, key: uN[TEST_KEY_W]:0xd8c1cd03, value: uN[TEST_VALUE_W]:0xdb5b0ded}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0xd1b33035, value: uN[TEST_VALUE_W]:0x7a21e0ed}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:5, key: uN[TEST_KEY_W]:0x8d512e0c, value: uN[TEST_VALUE_W]:0x708a536b}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0x1a950036, value: uN[TEST_VALUE_W]:0x9097f883}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:3, key: uN[TEST_KEY_W]:0x00707a86, value: uN[TEST_VALUE_W]:0xbcb29fa7}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0x2fcd78a1, value: uN[TEST_VALUE_W]:0x71bae380}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:8, key: uN[TEST_KEY_W]:0x34d8adc5, value: uN[TEST_VALUE_W]:0xdff20f62}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:5, key: uN[TEST_KEY_W]:0xd04ebdda, value: uN[TEST_VALUE_W]:0x9c785523}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:5, key: uN[TEST_KEY_W]:0x9b419a1a, value: uN[TEST_VALUE_W]:0xf1d27361}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0x9eb7784d, value: uN[TEST_VALUE_W]:0x58a9d8f2}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:7, key: uN[TEST_KEY_W]:0x6d7499ef, value: uN[TEST_VALUE_W]:0x40387b18}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:8, key: uN[TEST_KEY_W]:0xb255d705, value: uN[TEST_VALUE_W]:0x73ecbb7b}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:8, key: uN[TEST_KEY_W]:0x132c9499, value: uN[TEST_VALUE_W]:0x48b85084}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0xd3acf006, value: uN[TEST_VALUE_W]:0xbbd2f2b9}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:2, key: uN[TEST_KEY_W]:0x0dd951cd, value: uN[TEST_VALUE_W]:0x975ab3fe}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:6, key: uN[TEST_KEY_W]:0x3d6cd6b1, value: uN[TEST_VALUE_W]:0xe18f2e83}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:5, key: uN[TEST_KEY_W]:0xf511fadb, value: uN[TEST_VALUE_W]:0xb99e2ab4}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:2, key: uN[TEST_KEY_W]:0x90bea2bb, value: uN[TEST_VALUE_W]:0xc88b54c2}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0xf2513572, value: uN[TEST_VALUE_W]:0x42ef67d9}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0x2dd80b55, value: uN[TEST_VALUE_W]:0x3b399d05}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0x823af460, value: uN[TEST_VALUE_W]:0x89d154ba}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:9, key: uN[TEST_KEY_W]:0x8ab8897e, value: uN[TEST_VALUE_W]:0xb30eb8c5}, + TestData {num_entries_log2: uN[TEST_SIZE_W]:2, key: uN[TEST_KEY_W]:0xe9499524, value: uN[TEST_VALUE_W]:0xb4a30d68}, +]; + + +#[test_proc] +proc HashTable_test { + terminator_s: chan out; + read_req_s: chan out; + read_resp_r: chan in; + write_req_s: chan out; + write_resp_r: chan in; + + config(terminator_s: chan out) { + let (read_req_s, read_req_r) = chan("read_req"); + let (read_resp_s, read_resp_r) = chan("read_resp"); + let (write_req_s, write_req_r) = chan("write_req"); + let (write_resp_s, write_resp_r) = chan("write_resp"); + + let (ram_read_req_s, ram_read_req_r) = chan("ram_read_req"); + let (ram_read_resp_s, ram_read_resp_r) = chan("ram_read_resp"); + let (ram_write_req_s, ram_write_req_r) = chan("ram_write_req"); + let (ram_write_resp_s, ram_write_resp_r) = chan("ram_write_resp"); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_SIZE, TEST_WORD_PARTITION_SIZE, + TEST_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_INITIALIZED + >(ram_read_req_r, ram_read_resp_s, ram_write_req_r, ram_write_resp_s); + + spawn HashTable( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r + ); + + ( + terminator_s, + read_req_s, read_resp_r, + write_req_s, write_resp_r + ) + } + + init { } + + next(state: ()) { + let tok = join(); + + let tok = for ((i, test_data), tok): ((u32, TestData), token) in enumerate(TEST_DATA) { + // try to read data that was not written + let read_req = TestReadReq { + num_entries_log2: test_data.num_entries_log2, + key: test_data.key + }; + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{}.1 read request {:#x}", i + u32:1, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{}.1 read response {:#x}", i + u32:1, read_resp); + if read_resp.is_match { + // there may be match in case of a conflict + assert_eq(false, test_data.value == read_resp.value); + } else { }; + + // write data + let write_req = TestWriteReq { + num_entries_log2: test_data.num_entries_log2, + key: test_data.key, + value: test_data.value, + }; + let tok = send(tok, write_req_s, write_req); + trace_fmt!("Sent #{} write request {:#x}", i + u32:1, write_req); + + let (tok, write_resp) = recv(tok, write_resp_r); + trace_fmt!("Received #{} write response {:#x}", i + u32:1, write_resp); + + // read data after it was written + let read_req = TestReadReq { + num_entries_log2: test_data.num_entries_log2, + key: test_data.key + }; + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{}.2 read request {:#x}", i + u32:1, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{}.2 read response {:#x}", i + u32:1, read_resp); + assert_eq(TestReadResp { is_match: true, value: test_data.value }, read_resp); + + tok + }(tok); + + send(tok, terminator_s, true); + } +} From 1d7bda3866ba51d2cdbb203711ff35db92b2a724 Mon Sep 17 00:00:00 2001 From: Maciej Torhan Date: Mon, 25 Nov 2024 14:18:40 +0100 Subject: [PATCH 44/46] modules/zstd: Add HistoryBuffer implementation Signed-off-by: Maciej Torhan --- xls/modules/zstd/BUILD | 163 ++++ xls/modules/zstd/aligned_parallel_ram.x | 988 ++++++++++++++++++++++++ xls/modules/zstd/history_buffer.x | 491 ++++++++++++ 3 files changed, 1642 insertions(+) create mode 100644 xls/modules/zstd/aligned_parallel_ram.x create mode 100644 xls/modules/zstd/history_buffer.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 777faa95f2..989c3a6005 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -1163,3 +1163,166 @@ place_and_route( synthesized_rtl = ":hash_table_asap7", target_die_utilization_percentage = "10", ) + +xls_dslx_library( + name = "aligned_parallel_ram_dslx", + srcs = [ + "aligned_parallel_ram.x", + ], + deps = [ + ":common_dslx", + "//xls/examples:ram_dslx", + ], +) + +xls_dslx_test( + name = "aligned_parallel_ram_dslx_test", + dslx_test_args = {"compare": "jit"}, + library = ":aligned_parallel_ram_dslx", + tags = ["manual"], +) + +aligned_parallel_ram_codegen_args = common_codegen_args | { + "module_name": "AlignedParallelRam", + "clock_period_ps": "750", + "pipeline_stages": "8", + "worst_case_throughput": "1", +} + +xls_dslx_verilog( + name = "aligned_parallel_ram_verilog", + codegen_args = aligned_parallel_ram_codegen_args, + dslx_top = "AlignedParallelRamInst", + library = ":aligned_parallel_ram_dslx", + tags = ["manual"], + opt_ir_args = { + "inline_procs": "true", + "top": "__aligned_parallel_ram__AlignedParallelRamInst__AlignedParallelRam_0__10_64_7_8_1_8_128_1024_next", + }, + verilog_file = "aligned_parallel_ram.v", +) + +xls_benchmark_ir( + name = "aligned_parallel_ram_opt_ir_benchmark", + src = ":aligned_parallel_ram_verilog.opt.ir", + benchmark_ir_args = aligned_parallel_ram_codegen_args, + tags = ["manual"], +) + +verilog_library( + name = "aligned_parallel_ram_verilog_lib", + srcs = [ + ":aligned_parallel_ram.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "aligned_parallel_ram_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "AlignedParallelRam", + deps = [ + ":aligned_parallel_ram_verilog_lib", + ], +) + +benchmark_synth( + name = "aligned_parallel_ram_benchmark_synth", + synth_target = ":aligned_parallel_ram_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "aligned_parallel_ram_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":aligned_parallel_ram_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) + +xls_dslx_library( + name = "history_buffer_dslx", + srcs = [ + "history_buffer.x", + ], + deps = [ + ":common_dslx", + ":aligned_parallel_ram_dslx", + "//xls/examples:ram_dslx", + ], +) + +xls_dslx_test( + name = "history_buffer_dslx_test", + dslx_test_args = {"compare": "jit"}, + library = ":history_buffer_dslx", + tags = ["manual"], +) + +history_buffer_codegen_args = common_codegen_args | { + "module_name": "HistoryBuffer", + "clock_period_ps": "750", + "pipeline_stages": "8", + "worst_case_throughput": "1", +} + +xls_dslx_verilog( + name = "history_buffer_verilog", + codegen_args = history_buffer_codegen_args, + dslx_top = "HistoryBufferInst", + library = ":history_buffer_dslx", + tags = ["manual"], + opt_ir_args = { + "inline_procs": "true", + "top": "__history_buffer__HistoryBufferInst__HistoryBuffer_0__64_10_7_8_1_8_128_1024_next", + }, + verilog_file = "history_buffer.v", +) + +xls_benchmark_ir( + name = "history_buffer_opt_ir_benchmark", + src = ":history_buffer_verilog.opt.ir", + benchmark_ir_args = history_buffer_codegen_args, + tags = ["manual"], +) + +verilog_library( + name = "history_buffer_verilog_lib", + srcs = [ + ":history_buffer.v", + ], + tags = ["manual"], +) + +synthesize_rtl( + name = "history_buffer_synth_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + tags = ["manual"], + top_module = "HistoryBuffer", + deps = [ + ":history_buffer_verilog_lib", + ], +) + +benchmark_synth( + name = "history_buffer_benchmark_synth", + synth_target = ":history_buffer_synth_asap7", + tags = ["manual"], +) + +place_and_route( + name = "history_buffer_place_and_route", + clock_period = CLOCK_PERIOD_PS, + core_padding_microns = 2, + min_pin_distance = "0.5", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":history_buffer_synth_asap7", + tags = ["manual"], + target_die_utilization_percentage = "10", +) diff --git a/xls/modules/zstd/aligned_parallel_ram.x b/xls/modules/zstd/aligned_parallel_ram.x new file mode 100644 index 0000000000..88f45d34ba --- /dev/null +++ b/xls/modules/zstd/aligned_parallel_ram.x @@ -0,0 +1,988 @@ +// 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. + +// this file contains implementation of parallel RAMs with aligned access +// write requests' address should be a multiple of data width +// read requests` address dont have to be a multiple of data width + + +import std; +import xls.modules.zstd.common as common; +import xls.examples.ram; + +// Configurable RAM parameters, RAM_NUM has to be a power of 2 +pub const RAM_NUM = u32:8; +pub const RAM_NUM_W = std::clog2(RAM_NUM); + +pub struct AlignedParallelRamReadReq { + addr: uN[ADDR_W], +} + +pub struct AlignedParallelRamReadResp { + data: uN[DATA_W], +} + +pub struct AlignedParallelRamWriteReq { + addr: uN[ADDR_W], + data: uN[DATA_W], +} + +pub struct AlignedParallelRamWriteResp {} + +enum AlignedParallelRamReadRespHandlerFSM : u1 { + IDLE = 0, + READ_RESP = 1, +} + +struct AlignedParallelRamReadRespHandlerState { + fsm: AlignedParallelRamReadRespHandlerFSM, + ram_offset: uN[RAM_NUM_W], + resp_recv: bool[RAM_NUM], + resp_data: uN[RAM_DATA_W][RAM_NUM], +} + +struct AlignedParallelRamReadRespHandlerCtrl { + ram_offset: uN[RAM_NUM_W], +} + +proc AlignedParallelRamReadRespHandler< + DATA_W: u32, + RAM_DATA_W: u32 = {DATA_W / RAM_NUM}, +> { + type ReadResp = AlignedParallelRamReadResp; + type RamReadResp = ram::ReadResp; + + type FSM = AlignedParallelRamReadRespHandlerFSM; + type Ctrl = AlignedParallelRamReadRespHandlerCtrl; + type State = AlignedParallelRamReadRespHandlerState; + + ctrl_r: chan in; + + read_resp_s: chan out; + + ram_read_resp_0_r: chan in; + ram_read_resp_1_r: chan in; + ram_read_resp_2_r: chan in; + ram_read_resp_3_r: chan in; + ram_read_resp_4_r: chan in; + ram_read_resp_5_r: chan in; + ram_read_resp_6_r: chan in; + ram_read_resp_7_r: chan in; + + config ( + ctrl_r: chan in, + read_resp_s: chan out, + ram_read_resp_0_r: chan in, + ram_read_resp_1_r: chan in, + ram_read_resp_2_r: chan in, + ram_read_resp_3_r: chan in, + ram_read_resp_4_r: chan in, + ram_read_resp_5_r: chan in, + ram_read_resp_6_r: chan in, + ram_read_resp_7_r: chan in, + ) { + ( + ctrl_r, + read_resp_s, + ram_read_resp_0_r, + ram_read_resp_1_r, + ram_read_resp_2_r, + ram_read_resp_3_r, + ram_read_resp_4_r, + ram_read_resp_5_r, + ram_read_resp_6_r, + ram_read_resp_7_r, + ) + } + + init { zero!() } + + next (state: State) { + // receive ctrl + let (_, ctrl, ctrl_valid) = recv_if_non_blocking(join(), ctrl_r, state.fsm == FSM::IDLE, zero!()); + + let state = if ctrl_valid { + State { + fsm: FSM::READ_RESP, + ram_offset: ctrl.ram_offset, + ..state + } + } else { + state + }; + + // receive response from each RAM + let (_, ram_read_resp_0, ram_read_resp_0_valid) = recv_if_non_blocking( + join(), ram_read_resp_0_r, !state.resp_recv[u32:0] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_1, ram_read_resp_1_valid) = recv_if_non_blocking( + join(), ram_read_resp_1_r, !state.resp_recv[u32:1] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_2, ram_read_resp_2_valid) = recv_if_non_blocking( + join(), ram_read_resp_2_r, !state.resp_recv[u32:2] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_3, ram_read_resp_3_valid) = recv_if_non_blocking( + join(), ram_read_resp_3_r, !state.resp_recv[u32:3] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_4, ram_read_resp_4_valid) = recv_if_non_blocking( + join(), ram_read_resp_4_r, !state.resp_recv[u32:4] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_5, ram_read_resp_5_valid) = recv_if_non_blocking( + join(), ram_read_resp_5_r, !state.resp_recv[u32:5] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_6, ram_read_resp_6_valid) = recv_if_non_blocking( + join(), ram_read_resp_6_r, !state.resp_recv[u32:6] && state.fsm == FSM::READ_RESP, zero!() + ); + let (_, ram_read_resp_7, ram_read_resp_7_valid) = recv_if_non_blocking( + join(), ram_read_resp_7_r, !state.resp_recv[u32:7] && state.fsm == FSM::READ_RESP, zero!() + ); + + let ram_read_resp_valid = [ + ram_read_resp_0_valid, + ram_read_resp_1_valid, + ram_read_resp_2_valid, + ram_read_resp_3_valid, + ram_read_resp_4_valid, + ram_read_resp_5_valid, + ram_read_resp_6_valid, + ram_read_resp_7_valid, + ]; + + let ram_read_resp = [ + ram_read_resp_0, + ram_read_resp_1, + ram_read_resp_2, + ram_read_resp_3, + ram_read_resp_4, + ram_read_resp_5, + ram_read_resp_6, + ram_read_resp_7, + ]; + + let state = for (i, state) in range(u32:0, RAM_NUM) { + if ram_read_resp_valid[i] { + State { + resp_recv: update(state.resp_recv, i, true), + resp_data: update(state.resp_data, i, ram_read_resp[i].data), + ..state + } + } else { + state + } + }(state); + + // check if all data is received + let all_received = for (i, all_received) in range(u32:0, RAM_NUM) { + all_received & state.resp_recv[i] + }(true); + + // concatenate data + let concat_data = ( + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:7] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:6] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:5] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:4] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:3] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:2] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:1] ++ + state.resp_data[state.ram_offset + uN[RAM_NUM_W]:0] + ); + + // send response + send_if(join(), read_resp_s, all_received, ReadResp { + data: concat_data + }); + + // reset state + let state = if all_received { + zero!() + } else { + state + }; + + state + } +} + +struct AlignedParallelRamWriteRespHandlerState { + resp_recv: bool[RAM_NUM], +} + +proc AlignedParallelRamWriteRespHandler { + type WriteResp = AlignedParallelRamWriteResp; + type RamWriteResp = ram::WriteResp; + + type State = AlignedParallelRamWriteRespHandlerState; + + write_resp_s: chan out; + + ram_write_resp_0_r: chan in; + ram_write_resp_1_r: chan in; + ram_write_resp_2_r: chan in; + ram_write_resp_3_r: chan in; + ram_write_resp_4_r: chan in; + ram_write_resp_5_r: chan in; + ram_write_resp_6_r: chan in; + ram_write_resp_7_r: chan in; + + config ( + write_resp_s: chan out, + ram_write_resp_0_r: chan in, + ram_write_resp_1_r: chan in, + ram_write_resp_2_r: chan in, + ram_write_resp_3_r: chan in, + ram_write_resp_4_r: chan in, + ram_write_resp_5_r: chan in, + ram_write_resp_6_r: chan in, + ram_write_resp_7_r: chan in, + ) { + ( + write_resp_s, + ram_write_resp_0_r, + ram_write_resp_1_r, + ram_write_resp_2_r, + ram_write_resp_3_r, + ram_write_resp_4_r, + ram_write_resp_5_r, + ram_write_resp_6_r, + ram_write_resp_7_r, + ) + } + + init { zero!() } + + next (state: State) { + // receive response from each RAM + let (_, _, ram_read_resp_0_valid) = recv_if_non_blocking( + join(), ram_write_resp_0_r, !state.resp_recv[u32:0], zero!() + ); + let (_, _, ram_read_resp_1_valid) = recv_if_non_blocking( + join(), ram_write_resp_1_r, !state.resp_recv[u32:1], zero!() + ); + let (_, _, ram_read_resp_2_valid) = recv_if_non_blocking( + join(), ram_write_resp_2_r, !state.resp_recv[u32:2], zero!() + ); + let (_, _, ram_read_resp_3_valid) = recv_if_non_blocking( + join(), ram_write_resp_3_r, !state.resp_recv[u32:3], zero!() + ); + let (_, _, ram_read_resp_4_valid) = recv_if_non_blocking( + join(), ram_write_resp_4_r, !state.resp_recv[u32:4], zero!() + ); + let (_, _, ram_read_resp_5_valid) = recv_if_non_blocking( + join(), ram_write_resp_5_r, !state.resp_recv[u32:5], zero!() + ); + let (_, _, ram_read_resp_6_valid) = recv_if_non_blocking( + join(), ram_write_resp_6_r, !state.resp_recv[u32:6], zero!() + ); + let (_, _, ram_read_resp_7_valid) = recv_if_non_blocking( + join(), ram_write_resp_7_r, !state.resp_recv[u32:7], zero!() + ); + + let ram_read_resp_valid = [ + ram_read_resp_0_valid, + ram_read_resp_1_valid, + ram_read_resp_2_valid, + ram_read_resp_3_valid, + ram_read_resp_4_valid, + ram_read_resp_5_valid, + ram_read_resp_6_valid, + ram_read_resp_7_valid, + ]; + + let state = for (i, state) in range(u32:0, RAM_NUM) { + if ram_read_resp_valid[i] { + State { + resp_recv: update(state.resp_recv, i, true), + } + } else { + state + } + }(state); + + // check if all data is received + let all_received = for (i, all_received) in range(u32:0, RAM_NUM) { + all_received & state.resp_recv[i] + }(true); + + // send response + send_if(join(), write_resp_s, all_received, WriteResp {}); + + // reset state + let state = if all_received { + zero!() + } else { + state + }; + + state + } + +} + + +pub proc AlignedParallelRam< + SIZE: u32, + DATA_W: u32, + ADDR_W: u32 = {std::clog2(SIZE)}, + RAM_SIZE: u32 = {SIZE / RAM_NUM}, + RAM_DATA_W: u32 = {DATA_W / RAM_NUM}, + RAM_ADDR_W: u32 = {std::clog2(RAM_SIZE)}, + RAM_PARTITION_SIZE: u32 = {RAM_DATA_W}, + RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(RAM_PARTITION_SIZE, RAM_DATA_W)}, +> { + type ReadReq = AlignedParallelRamReadReq; + type ReadResp = AlignedParallelRamReadResp; + type WriteReq = AlignedParallelRamWriteReq; + type WriteResp = AlignedParallelRamWriteResp; + + type RamReadReq = ram::ReadReq; + type RamReadResp = ram::ReadResp; + type RamWriteReq = ram::WriteReq; + type RamWriteResp = ram::WriteResp; + + read_req_r: chan in; + write_req_r: chan in; + + read_resp_handler_ctrl_s: chan out; + + // RAMs read interfaces + ram_read_req_0_s: chan out; + ram_read_req_1_s: chan out; + ram_read_req_2_s: chan out; + ram_read_req_3_s: chan out; + ram_read_req_4_s: chan out; + ram_read_req_5_s: chan out; + ram_read_req_6_s: chan out; + ram_read_req_7_s: chan out; + + // RAMs write interfaces + ram_write_req_0_s: chan out; + ram_write_req_1_s: chan out; + ram_write_req_2_s: chan out; + ram_write_req_3_s: chan out; + ram_write_req_4_s: chan out; + ram_write_req_5_s: chan out; + ram_write_req_6_s: chan out; + ram_write_req_7_s: chan out; + + config ( + read_req_r: chan in, + read_resp_s: chan out, + write_req_r: chan in, + write_resp_s: chan out, + ram_read_req_s: chan[RAM_NUM] out, + ram_read_resp_r: chan[RAM_NUM] in, + ram_write_req_s: chan[RAM_NUM] out, + ram_write_resp_r: chan[RAM_NUM] in, + ) { + let (read_resp_handler_ctrl_s, read_resp_handler_ctrl_r) = + chan("read_resp_handler_ctrl"); + + spawn AlignedParallelRamReadRespHandler( + read_resp_handler_ctrl_r, + read_resp_s, + ram_read_resp_r[0], + ram_read_resp_r[1], + ram_read_resp_r[2], + ram_read_resp_r[3], + ram_read_resp_r[4], + ram_read_resp_r[5], + ram_read_resp_r[6], + ram_read_resp_r[7], + ); + + spawn AlignedParallelRamWriteRespHandler ( + write_resp_s, + ram_write_resp_r[0], + ram_write_resp_r[1], + ram_write_resp_r[2], + ram_write_resp_r[3], + ram_write_resp_r[4], + ram_write_resp_r[5], + ram_write_resp_r[6], + ram_write_resp_r[7], + ); + + ( + read_req_r, + write_req_r, + read_resp_handler_ctrl_s, + ram_read_req_s[0], + ram_read_req_s[1], + ram_read_req_s[2], + ram_read_req_s[3], + ram_read_req_s[4], + ram_read_req_s[5], + ram_read_req_s[6], + ram_read_req_s[7], + ram_write_req_s[0], + ram_write_req_s[1], + ram_write_req_s[2], + ram_write_req_s[3], + ram_write_req_s[4], + ram_write_req_s[5], + ram_write_req_s[6], + ram_write_req_s[7], + ) + } + + init { } + + next (state: ()) { + // handle read request + let (tok_read, read_req, read_req_valid) = recv_non_blocking(join(), read_req_r, zero!()); + + // send ctrl to read resp hanlder + let read_resp_handler_ctrl = AlignedParallelRamReadRespHandlerCtrl { + ram_offset: read_req.addr as uN[RAM_NUM_W], + }; + send_if(tok_read, read_resp_handler_ctrl_s, read_req_valid, read_resp_handler_ctrl); + + // send requests to each RAM + let ram_read_req = for (i, ram_read_req) in range(u32:0, RAM_NUM) { + let offset = if read_req.addr as uN[RAM_NUM_W] > i as uN[RAM_NUM_W] { + uN[RAM_ADDR_W]:1 + } else { + uN[RAM_ADDR_W]:0 + }; + update(ram_read_req, i, RamReadReq { + addr: (read_req.addr >> std::clog2(RAM_NUM)) as uN[RAM_ADDR_W] + offset, + mask: !uN[RAM_NUM_PARTITIONS]:0, + }) + }(zero!()); + send_if(tok_read, ram_read_req_0_s, read_req_valid, ram_read_req[0]); + send_if(tok_read, ram_read_req_1_s, read_req_valid, ram_read_req[1]); + send_if(tok_read, ram_read_req_2_s, read_req_valid, ram_read_req[2]); + send_if(tok_read, ram_read_req_3_s, read_req_valid, ram_read_req[3]); + send_if(tok_read, ram_read_req_4_s, read_req_valid, ram_read_req[4]); + send_if(tok_read, ram_read_req_5_s, read_req_valid, ram_read_req[5]); + send_if(tok_read, ram_read_req_6_s, read_req_valid, ram_read_req[6]); + send_if(tok_read, ram_read_req_7_s, read_req_valid, ram_read_req[7]); + + // handle write request + let (tok_write, write_req, write_req_valid) = recv_non_blocking(join(), write_req_r, zero!()); + + // send requests to each RAM + let ram_write_req = for (i, ram_write_req) in range(u32:0, RAM_NUM) { + update(ram_write_req, i, RamWriteReq { + addr: (write_req.addr >> std::clog2(RAM_NUM)) as uN[RAM_ADDR_W], + data: (write_req.data >> (RAM_DATA_W * i)) as uN[RAM_DATA_W], + mask: !uN[RAM_NUM_PARTITIONS]:0, + }) + }(zero!()); + send_if(tok_read, ram_write_req_0_s, write_req_valid, ram_write_req[0]); + send_if(tok_read, ram_write_req_1_s, write_req_valid, ram_write_req[1]); + send_if(tok_read, ram_write_req_2_s, write_req_valid, ram_write_req[2]); + send_if(tok_read, ram_write_req_3_s, write_req_valid, ram_write_req[3]); + send_if(tok_read, ram_write_req_4_s, write_req_valid, ram_write_req[4]); + send_if(tok_read, ram_write_req_5_s, write_req_valid, ram_write_req[5]); + send_if(tok_read, ram_write_req_6_s, write_req_valid, ram_write_req[6]); + send_if(tok_read, ram_write_req_7_s, write_req_valid, ram_write_req[7]); + } +} + + +const INST_SIZE = u32:1024; +const INST_DATA_W = u32:64; +const INST_ADDR_W = std::clog2(INST_SIZE); +const INST_RAM_SIZE = INST_SIZE / RAM_NUM; +const INST_RAM_DATA_W = {INST_DATA_W / RAM_NUM}; +const INST_RAM_ADDR_W = {std::clog2(INST_RAM_SIZE)}; +const INST_RAM_PARTITION_SIZE = {INST_RAM_DATA_W}; +const INST_RAM_NUM_PARTITIONS = {ram::num_partitions(INST_RAM_PARTITION_SIZE, INST_RAM_DATA_W)}; + +proc AlignedParallelRamInst { + type InstReadReq = AlignedParallelRamReadReq; + type InstReadResp = AlignedParallelRamReadResp; + type InstWriteReq = AlignedParallelRamWriteReq; + type InstWriteResp = AlignedParallelRamWriteResp; + + type InstRamReadReq = ram::ReadReq; + type InstRamReadResp = ram::ReadResp; + type InstRamWriteReq = ram::WriteReq; + type InstRamWriteResp = ram::WriteResp; + + config ( + read_req_r: chan in, + read_resp_s: chan out, + write_req_r: chan in, + write_resp_s: chan out, + ram_read_req_s: chan[RAM_NUM] out, + ram_read_resp_r: chan[RAM_NUM] in, + ram_write_req_s: chan[RAM_NUM] out, + ram_write_resp_r: chan[RAM_NUM] in, + ) { + spawn AlignedParallelRam( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r, + ); + } + + init { } + + next (state: ()) { } +} + + +const TEST_SIZE = u32:1024; +const TEST_DATA_W = u32:64; +const TEST_ADDR_W = std::clog2(TEST_SIZE); +const TEST_RAM_SIZE = TEST_SIZE / RAM_NUM; +const TEST_RAM_DATA_W = {TEST_DATA_W / RAM_NUM}; +const TEST_RAM_ADDR_W = {std::clog2(TEST_RAM_SIZE)}; +const TEST_RAM_PARTITION_SIZE = {TEST_RAM_DATA_W}; +const TEST_RAM_NUM_PARTITIONS = {ram::num_partitions(TEST_RAM_PARTITION_SIZE, TEST_RAM_DATA_W)}; + +const TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_RAM_INITIALIZED = true; + +type TestReadReq = AlignedParallelRamReadReq; +type TestReadResp = AlignedParallelRamReadResp; +type TestWriteReq = AlignedParallelRamWriteReq; +type TestWriteResp = AlignedParallelRamWriteResp; + +type TestRamReadReq = ram::ReadReq; +type TestRamReadResp = ram::ReadResp; +type TestRamWriteReq = ram::WriteReq; +type TestRamWriteResp = ram::WriteResp; + +struct TestData { + addr: uN[TEST_ADDR_W], + data: uN[TEST_DATA_W], +} + +const TEST_DATA = TestData[64]:[ + TestData {addr: uN[TEST_ADDR_W]:0x0c8, data: uN[TEST_DATA_W]:0x698dbd57f739d8ce}, + TestData {addr: uN[TEST_ADDR_W]:0x248, data: uN[TEST_DATA_W]:0x4cf6fc9b695676ad}, + TestData {addr: uN[TEST_ADDR_W]:0x3a0, data: uN[TEST_DATA_W]:0x5da52c3bd7b39603}, + TestData {addr: uN[TEST_ADDR_W]:0x208, data: uN[TEST_DATA_W]:0x5afa80c1c45a5bd2}, + TestData {addr: uN[TEST_ADDR_W]:0x068, data: uN[TEST_DATA_W]:0x27befcb367237e3f}, + TestData {addr: uN[TEST_ADDR_W]:0x358, data: uN[TEST_DATA_W]:0xa477d4887cec7fc2}, + TestData {addr: uN[TEST_ADDR_W]:0x328, data: uN[TEST_DATA_W]:0x38ecf19cf314ba5c}, + TestData {addr: uN[TEST_ADDR_W]:0x258, data: uN[TEST_DATA_W]:0x97a504cfa39e6750}, + TestData {addr: uN[TEST_ADDR_W]:0x1b8, data: uN[TEST_DATA_W]:0x2fa75c1effecf687}, + TestData {addr: uN[TEST_ADDR_W]:0x2e8, data: uN[TEST_DATA_W]:0xb1315d70b63629d8}, + TestData {addr: uN[TEST_ADDR_W]:0x2f0, data: uN[TEST_DATA_W]:0x44c025ebee513c44}, + TestData {addr: uN[TEST_ADDR_W]:0x250, data: uN[TEST_DATA_W]:0x295250fa0d795902}, + TestData {addr: uN[TEST_ADDR_W]:0x2a0, data: uN[TEST_DATA_W]:0x1f76bb3cf745235e}, + TestData {addr: uN[TEST_ADDR_W]:0x168, data: uN[TEST_DATA_W]:0x0d06b1d161037460}, + TestData {addr: uN[TEST_ADDR_W]:0x010, data: uN[TEST_DATA_W]:0x0c7b320db86382df}, + TestData {addr: uN[TEST_ADDR_W]:0x178, data: uN[TEST_DATA_W]:0x547e5874fdae8c09}, + TestData {addr: uN[TEST_ADDR_W]:0x0f8, data: uN[TEST_DATA_W]:0xc75ca52d83d65bba}, + TestData {addr: uN[TEST_ADDR_W]:0x0d0, data: uN[TEST_DATA_W]:0x3c10031e89ac070a}, + TestData {addr: uN[TEST_ADDR_W]:0x3f8, data: uN[TEST_DATA_W]:0xe881ce7c3e4515b4}, + TestData {addr: uN[TEST_ADDR_W]:0x378, data: uN[TEST_DATA_W]:0xa10c92b84419eb3d}, + TestData {addr: uN[TEST_ADDR_W]:0x018, data: uN[TEST_DATA_W]:0x7b9537f92c4958e0}, + TestData {addr: uN[TEST_ADDR_W]:0x350, data: uN[TEST_DATA_W]:0x38a1a5e8a7206e81}, + TestData {addr: uN[TEST_ADDR_W]:0x030, data: uN[TEST_DATA_W]:0xda2cf6b0b380862c}, + TestData {addr: uN[TEST_ADDR_W]:0x248, data: uN[TEST_DATA_W]:0xa56492b3fb19c8b8}, + TestData {addr: uN[TEST_ADDR_W]:0x258, data: uN[TEST_DATA_W]:0x9cbfccbf72c7948b}, + TestData {addr: uN[TEST_ADDR_W]:0x008, data: uN[TEST_DATA_W]:0x7fb6d361a608db56}, + TestData {addr: uN[TEST_ADDR_W]:0x108, data: uN[TEST_DATA_W]:0xba2aef614c7c5c1e}, + TestData {addr: uN[TEST_ADDR_W]:0x090, data: uN[TEST_DATA_W]:0xe7a5ab55633078fa}, + TestData {addr: uN[TEST_ADDR_W]:0x0c0, data: uN[TEST_DATA_W]:0xb5132e7e378f3f5b}, + TestData {addr: uN[TEST_ADDR_W]:0x198, data: uN[TEST_DATA_W]:0xeac9fe191bfd8b31}, + TestData {addr: uN[TEST_ADDR_W]:0x218, data: uN[TEST_DATA_W]:0x82ad45d959f8dbec}, + TestData {addr: uN[TEST_ADDR_W]:0x070, data: uN[TEST_DATA_W]:0x4d4e255058d00ccb}, + TestData {addr: uN[TEST_ADDR_W]:0x3a0, data: uN[TEST_DATA_W]:0x2a69306cf695b2f5}, + TestData {addr: uN[TEST_ADDR_W]:0x1e0, data: uN[TEST_DATA_W]:0x571a30f8cd940e39}, + TestData {addr: uN[TEST_ADDR_W]:0x300, data: uN[TEST_DATA_W]:0x7069a4c406076fd9}, + TestData {addr: uN[TEST_ADDR_W]:0x2a8, data: uN[TEST_DATA_W]:0x9af366c878230764}, + TestData {addr: uN[TEST_ADDR_W]:0x328, data: uN[TEST_DATA_W]:0x1e6bc1e2df3c8a7b}, + TestData {addr: uN[TEST_ADDR_W]:0x298, data: uN[TEST_DATA_W]:0x1ff9be4f810cd87a}, + TestData {addr: uN[TEST_ADDR_W]:0x250, data: uN[TEST_DATA_W]:0x9ad30cee350aebfa}, + TestData {addr: uN[TEST_ADDR_W]:0x090, data: uN[TEST_DATA_W]:0x31fca7f91dfcafb5}, + TestData {addr: uN[TEST_ADDR_W]:0x3b8, data: uN[TEST_DATA_W]:0xe434deef583c3cd1}, + TestData {addr: uN[TEST_ADDR_W]:0x3c0, data: uN[TEST_DATA_W]:0x4170c371a2025f27}, + TestData {addr: uN[TEST_ADDR_W]:0x0e8, data: uN[TEST_DATA_W]:0x616754a100d9decc}, + TestData {addr: uN[TEST_ADDR_W]:0x1f0, data: uN[TEST_DATA_W]:0x8d93fa35edab37b7}, + TestData {addr: uN[TEST_ADDR_W]:0x208, data: uN[TEST_DATA_W]:0x6582012a83ffcec3}, + TestData {addr: uN[TEST_ADDR_W]:0x3d0, data: uN[TEST_DATA_W]:0x6c66a69e87eac130}, + TestData {addr: uN[TEST_ADDR_W]:0x248, data: uN[TEST_DATA_W]:0xbfd5e4e261bbd7e3}, + TestData {addr: uN[TEST_ADDR_W]:0x058, data: uN[TEST_DATA_W]:0x2f8ba1fd6a8b6ee9}, + TestData {addr: uN[TEST_ADDR_W]:0x1a0, data: uN[TEST_DATA_W]:0xef9ab2936ef6833e}, + TestData {addr: uN[TEST_ADDR_W]:0x380, data: uN[TEST_DATA_W]:0x279130ba7b5ced6f}, + TestData {addr: uN[TEST_ADDR_W]:0x170, data: uN[TEST_DATA_W]:0xc1977f6a2153db09}, + TestData {addr: uN[TEST_ADDR_W]:0x3d8, data: uN[TEST_DATA_W]:0xd4ea85571e440cef}, + TestData {addr: uN[TEST_ADDR_W]:0x360, data: uN[TEST_DATA_W]:0x9bc5756ab3328603}, + TestData {addr: uN[TEST_ADDR_W]:0x2f8, data: uN[TEST_DATA_W]:0x14217d1804170f39}, + TestData {addr: uN[TEST_ADDR_W]:0x268, data: uN[TEST_DATA_W]:0x0098755165e9ae81}, + TestData {addr: uN[TEST_ADDR_W]:0x050, data: uN[TEST_DATA_W]:0x3ee0b48789cc94e0}, + TestData {addr: uN[TEST_ADDR_W]:0x398, data: uN[TEST_DATA_W]:0x9ff7fbc9906d3d63}, + TestData {addr: uN[TEST_ADDR_W]:0x068, data: uN[TEST_DATA_W]:0x507bc61f805b0e68}, + TestData {addr: uN[TEST_ADDR_W]:0x350, data: uN[TEST_DATA_W]:0x77802819dc14663a}, + TestData {addr: uN[TEST_ADDR_W]:0x168, data: uN[TEST_DATA_W]:0xd8ca0711ca37bfa9}, + TestData {addr: uN[TEST_ADDR_W]:0x068, data: uN[TEST_DATA_W]:0x30464e3d2630b6de}, + TestData {addr: uN[TEST_ADDR_W]:0x360, data: uN[TEST_DATA_W]:0xdbac58596c50f62f}, + TestData {addr: uN[TEST_ADDR_W]:0x2e0, data: uN[TEST_DATA_W]:0x9992cfd966824669}, + TestData {addr: uN[TEST_ADDR_W]:0x2e0, data: uN[TEST_DATA_W]:0x1a4a65b0257c223b}, +]; + +#[test_proc] +proc AlignedParallelRam_test_aligned_read { + terminator: chan out; + read_req_s: chan out; + read_resp_r: chan in; + write_req_s: chan out; + write_resp_r: chan in; + + config (terminator: chan out) { + let (read_req_s, read_req_r) = chan("read_req"); + let (read_resp_s, read_resp_r) = chan("read_resp"); + let (write_req_s, write_req_r) = chan("write_req"); + let (write_resp_s, write_resp_r) = chan("write_resp"); + + let (ram_read_req_s, ram_read_req_r) = chan[RAM_NUM]("ram_read_req"); + let (ram_read_resp_s, ram_read_resp_r) = chan[RAM_NUM]("ram_read_resp"); + let (ram_write_req_s, ram_write_req_r) = chan[RAM_NUM]("ram_write_req"); + let (ram_write_resp_s, ram_write_resp_r) = chan[RAM_NUM]("ram_write_resp"); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[0], ram_read_resp_s[0], ram_write_req_r[0], ram_write_resp_s[0], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[1], ram_read_resp_s[1], ram_write_req_r[1], ram_write_resp_s[1], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[2], ram_read_resp_s[2], ram_write_req_r[2], ram_write_resp_s[2], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[3], ram_read_resp_s[3], ram_write_req_r[3], ram_write_resp_s[3], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[4], ram_read_resp_s[4], ram_write_req_r[4], ram_write_resp_s[4], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[5], ram_read_resp_s[5], ram_write_req_r[5], ram_write_resp_s[5], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[6], ram_read_resp_s[6], ram_write_req_r[6], ram_write_resp_s[6], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[7], ram_read_resp_s[7], ram_write_req_r[7], ram_write_resp_s[7], + ); + + spawn AlignedParallelRam( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r, + ); + + ( + terminator, + read_req_s, + read_resp_r, + write_req_s, + write_resp_r, + ) + } + + init { } + + next (state: ()) { + let tok = join(); + + let tok = for (i, tok):(u32, token) in range(u32:0, array_size(TEST_DATA)) { + let test_data = TEST_DATA[i]; + + let write_req = TestWriteReq { + addr: test_data.addr, + data: test_data.data, + }; + let tok = send(tok, write_req_s, write_req); + trace_fmt!("Sent #{} write request {:#x}", i + u32:1, write_req); + + let (tok, write_resp) = recv(tok, write_resp_r); + trace_fmt!("Received #{} write response {:#x}", i + u32:1, write_resp); + + let read_req = TestReadReq { + addr: test_data.addr, + }; + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{} read request {:#x}", i + u32:1, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{} read response {:#x}", i + u32:1, read_resp); + + assert_eq(test_data.data, read_resp.data); + + tok + }(tok); + + send(tok, terminator, true); + } +} + +const TEST_RAM_CONTENT = uN[TEST_DATA_W][64]:[ + uN[TEST_DATA_W]:0x2122337ed367496b, uN[TEST_DATA_W]:0x33de22e4291ecb66, + uN[TEST_DATA_W]:0x62052eccbde0009d, uN[TEST_DATA_W]:0xfa179c402e7b5f47, + uN[TEST_DATA_W]:0x118fa2c81d1230e9, uN[TEST_DATA_W]:0xe48ee076b41120c0, + uN[TEST_DATA_W]:0xa33d467d80575e5b, uN[TEST_DATA_W]:0x61213ebe00890570, + uN[TEST_DATA_W]:0xe9210eae2507442f, uN[TEST_DATA_W]:0x4b8c721627c19c44, + uN[TEST_DATA_W]:0x55e768d2e4586bba, uN[TEST_DATA_W]:0x2fc234017ac1deb5, + uN[TEST_DATA_W]:0xdc6afd5db30446aa, uN[TEST_DATA_W]:0xe91512402a2f68ab, + uN[TEST_DATA_W]:0x13fd96b93aef2c85, uN[TEST_DATA_W]:0x980b4054b9f66fc2, + uN[TEST_DATA_W]:0xa8bf09e77757ca28, uN[TEST_DATA_W]:0x94a67ff04004de7b, + uN[TEST_DATA_W]:0x6d1f3071a446b0c3, uN[TEST_DATA_W]:0x01605527a50fdecf, + uN[TEST_DATA_W]:0xd839258508c3efd1, uN[TEST_DATA_W]:0x1207a4d0de5c9724, + uN[TEST_DATA_W]:0xef39682f0810f43c, uN[TEST_DATA_W]:0x4781977bc26ce834, + uN[TEST_DATA_W]:0x0805a350ed25812f, uN[TEST_DATA_W]:0x8ad82cb67bf49cef, + uN[TEST_DATA_W]:0x2fd11ff78f85f169, uN[TEST_DATA_W]:0xd624be58457eab2a, + uN[TEST_DATA_W]:0x873e4be71afa1355, uN[TEST_DATA_W]:0x6e0e9f264151b531, + uN[TEST_DATA_W]:0xa69015c537b4da78, uN[TEST_DATA_W]:0x0879638aa8045ad9, + uN[TEST_DATA_W]:0x30dd170b7bf89cbf, uN[TEST_DATA_W]:0xcbfeb8219960a267, + uN[TEST_DATA_W]:0x9f6fcd2d4a4ba9f2, uN[TEST_DATA_W]:0xdf0ead33b3e55ac3, + uN[TEST_DATA_W]:0x64a24c19037f850b, uN[TEST_DATA_W]:0x4dcbb4de2d3aba5a, + uN[TEST_DATA_W]:0xa40749b2be1450b6, uN[TEST_DATA_W]:0x99bed65d2d28d1f6, + uN[TEST_DATA_W]:0xbe8d35f27bb892b4, uN[TEST_DATA_W]:0x23315a3a70110048, + uN[TEST_DATA_W]:0x68b0e22cb8885787, uN[TEST_DATA_W]:0xcac1a152d43dae98, + uN[TEST_DATA_W]:0x1fb5cec8c64ad46a, uN[TEST_DATA_W]:0xcbe25f0d2b21e9a1, + uN[TEST_DATA_W]:0x46e161fd5f490ae7, uN[TEST_DATA_W]:0xff2dd0e7a120d222, + uN[TEST_DATA_W]:0xa764165b6d09fb90, uN[TEST_DATA_W]:0xee4d9484b63f6a66, + uN[TEST_DATA_W]:0x204d6d789e9fe377, uN[TEST_DATA_W]:0x9ad53311a1a95bcf, + uN[TEST_DATA_W]:0x63d497f105d8661f, uN[TEST_DATA_W]:0x40e7a242cc26483c, + uN[TEST_DATA_W]:0x5a82a7265627cab1, uN[TEST_DATA_W]:0x42de42323222a24b, + uN[TEST_DATA_W]:0xdede8c218f3ef36a, uN[TEST_DATA_W]:0xec86b8e8734da0c7, + uN[TEST_DATA_W]:0x9b209d6959c36b79, uN[TEST_DATA_W]:0x829c158fd6678675, + uN[TEST_DATA_W]:0x5c59a4845b68a509, uN[TEST_DATA_W]:0xcc9e851a38b01d04, + uN[TEST_DATA_W]:0x5e15f41bd09acd33, uN[TEST_DATA_W]:0x953425686ce51623, +]; + +const TEST_READ_ADDR = uN[TEST_ADDR_W][128]:[ + uN[TEST_ADDR_W]:0x0d0, uN[TEST_ADDR_W]:0x01c, uN[TEST_ADDR_W]:0x094, uN[TEST_ADDR_W]:0x153, + uN[TEST_ADDR_W]:0x1cb, uN[TEST_ADDR_W]:0x14f, uN[TEST_ADDR_W]:0x021, uN[TEST_ADDR_W]:0x1f5, + uN[TEST_ADDR_W]:0x155, uN[TEST_ADDR_W]:0x0db, uN[TEST_ADDR_W]:0x070, uN[TEST_ADDR_W]:0x13a, + uN[TEST_ADDR_W]:0x0bf, uN[TEST_ADDR_W]:0x16b, uN[TEST_ADDR_W]:0x143, uN[TEST_ADDR_W]:0x0b4, + uN[TEST_ADDR_W]:0x1f4, uN[TEST_ADDR_W]:0x17f, uN[TEST_ADDR_W]:0x096, uN[TEST_ADDR_W]:0x03a, + uN[TEST_ADDR_W]:0x0ec, uN[TEST_ADDR_W]:0x030, uN[TEST_ADDR_W]:0x0e1, uN[TEST_ADDR_W]:0x1e7, + uN[TEST_ADDR_W]:0x006, uN[TEST_ADDR_W]:0x088, uN[TEST_ADDR_W]:0x1e9, uN[TEST_ADDR_W]:0x16f, + uN[TEST_ADDR_W]:0x152, uN[TEST_ADDR_W]:0x1a2, uN[TEST_ADDR_W]:0x0ac, uN[TEST_ADDR_W]:0x0d3, + uN[TEST_ADDR_W]:0x0d5, uN[TEST_ADDR_W]:0x107, uN[TEST_ADDR_W]:0x121, uN[TEST_ADDR_W]:0x01a, + uN[TEST_ADDR_W]:0x1c2, uN[TEST_ADDR_W]:0x117, uN[TEST_ADDR_W]:0x0e9, uN[TEST_ADDR_W]:0x0ac, + uN[TEST_ADDR_W]:0x16e, uN[TEST_ADDR_W]:0x105, uN[TEST_ADDR_W]:0x01e, uN[TEST_ADDR_W]:0x186, + uN[TEST_ADDR_W]:0x1bb, uN[TEST_ADDR_W]:0x05b, uN[TEST_ADDR_W]:0x07a, uN[TEST_ADDR_W]:0x1d3, + uN[TEST_ADDR_W]:0x120, uN[TEST_ADDR_W]:0x142, uN[TEST_ADDR_W]:0x0ee, uN[TEST_ADDR_W]:0x083, + uN[TEST_ADDR_W]:0x1ce, uN[TEST_ADDR_W]:0x016, uN[TEST_ADDR_W]:0x041, uN[TEST_ADDR_W]:0x040, + uN[TEST_ADDR_W]:0x073, uN[TEST_ADDR_W]:0x197, uN[TEST_ADDR_W]:0x1d1, uN[TEST_ADDR_W]:0x074, + uN[TEST_ADDR_W]:0x087, uN[TEST_ADDR_W]:0x168, uN[TEST_ADDR_W]:0x1f7, uN[TEST_ADDR_W]:0x19e, + uN[TEST_ADDR_W]:0x06f, uN[TEST_ADDR_W]:0x0c9, uN[TEST_ADDR_W]:0x102, uN[TEST_ADDR_W]:0x077, + uN[TEST_ADDR_W]:0x0ff, uN[TEST_ADDR_W]:0x1ac, uN[TEST_ADDR_W]:0x02c, uN[TEST_ADDR_W]:0x116, + uN[TEST_ADDR_W]:0x04d, uN[TEST_ADDR_W]:0x16b, uN[TEST_ADDR_W]:0x14c, uN[TEST_ADDR_W]:0x173, + uN[TEST_ADDR_W]:0x055, uN[TEST_ADDR_W]:0x1e1, uN[TEST_ADDR_W]:0x028, uN[TEST_ADDR_W]:0x103, + uN[TEST_ADDR_W]:0x01c, uN[TEST_ADDR_W]:0x168, uN[TEST_ADDR_W]:0x096, uN[TEST_ADDR_W]:0x15b, + uN[TEST_ADDR_W]:0x1aa, uN[TEST_ADDR_W]:0x010, uN[TEST_ADDR_W]:0x08c, uN[TEST_ADDR_W]:0x083, + uN[TEST_ADDR_W]:0x014, uN[TEST_ADDR_W]:0x013, uN[TEST_ADDR_W]:0x00d, uN[TEST_ADDR_W]:0x1eb, + uN[TEST_ADDR_W]:0x09d, uN[TEST_ADDR_W]:0x079, uN[TEST_ADDR_W]:0x146, uN[TEST_ADDR_W]:0x191, + uN[TEST_ADDR_W]:0x070, uN[TEST_ADDR_W]:0x1bc, uN[TEST_ADDR_W]:0x037, uN[TEST_ADDR_W]:0x130, + uN[TEST_ADDR_W]:0x0d8, uN[TEST_ADDR_W]:0x0d1, uN[TEST_ADDR_W]:0x136, uN[TEST_ADDR_W]:0x05b, + uN[TEST_ADDR_W]:0x1f3, uN[TEST_ADDR_W]:0x036, uN[TEST_ADDR_W]:0x0db, uN[TEST_ADDR_W]:0x149, + uN[TEST_ADDR_W]:0x11e, uN[TEST_ADDR_W]:0x1c2, uN[TEST_ADDR_W]:0x0a3, uN[TEST_ADDR_W]:0x061, + uN[TEST_ADDR_W]:0x0eb, uN[TEST_ADDR_W]:0x131, uN[TEST_ADDR_W]:0x04a, uN[TEST_ADDR_W]:0x0ab, + uN[TEST_ADDR_W]:0x0d5, uN[TEST_ADDR_W]:0x083, uN[TEST_ADDR_W]:0x1cb, uN[TEST_ADDR_W]:0x03f, + uN[TEST_ADDR_W]:0x02d, uN[TEST_ADDR_W]:0x14d, uN[TEST_ADDR_W]:0x120, uN[TEST_ADDR_W]:0x194, + uN[TEST_ADDR_W]:0x062, uN[TEST_ADDR_W]:0x182, uN[TEST_ADDR_W]:0x124, uN[TEST_ADDR_W]:0x06d, +]; + +#[test_proc] +proc AlignedParallelRam_test_non_aligned_read { + terminator: chan out; + read_req_s: chan out; + read_resp_r: chan in; + write_req_s: chan out; + write_resp_r: chan in; + + config (terminator: chan out) { + let (read_req_s, read_req_r) = chan("read_req"); + let (read_resp_s, read_resp_r) = chan("read_resp"); + let (write_req_s, write_req_r) = chan("write_req"); + let (write_resp_s, write_resp_r) = chan("write_resp"); + + let (ram_read_req_s, ram_read_req_r) = chan[RAM_NUM]("ram_read_req"); + let (ram_read_resp_s, ram_read_resp_r) = chan[RAM_NUM]("ram_read_resp"); + let (ram_write_req_s, ram_write_req_r) = chan[RAM_NUM]("ram_write_req"); + let (ram_write_resp_s, ram_write_resp_r) = chan[RAM_NUM]("ram_write_resp"); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[0], ram_read_resp_s[0], ram_write_req_r[0], ram_write_resp_s[0], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[1], ram_read_resp_s[1], ram_write_req_r[1], ram_write_resp_s[1], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[2], ram_read_resp_s[2], ram_write_req_r[2], ram_write_resp_s[2], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[3], ram_read_resp_s[3], ram_write_req_r[3], ram_write_resp_s[3], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[4], ram_read_resp_s[4], ram_write_req_r[4], ram_write_resp_s[4], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[5], ram_read_resp_s[5], ram_write_req_r[5], ram_write_resp_s[5], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[6], ram_read_resp_s[6], ram_write_req_r[6], ram_write_resp_s[6], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[7], ram_read_resp_s[7], ram_write_req_r[7], ram_write_resp_s[7], + ); + + spawn AlignedParallelRam( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r, + ); + + ( + terminator, + read_req_s, + read_resp_r, + write_req_s, + write_resp_r, + ) + } + + init { } + + next (state: ()) { + let tok = join(); + + // write RAM content + let tok = for (i, tok):(u32, token) in range(u32:0, array_size(TEST_RAM_CONTENT)) { + let test_data = TEST_RAM_CONTENT[i]; + + let write_req = TestWriteReq { + addr: (i * TEST_RAM_DATA_W) as uN[TEST_ADDR_W], + data: test_data, + }; + let tok = send(tok, write_req_s, write_req); + trace_fmt!("Sent #{} write request {:#x}", i + u32:1, write_req); + + let (tok, write_resp) = recv(tok, write_resp_r); + trace_fmt!("Received #{} write response {:#x}", i + u32:1, write_resp); + + let read_req = TestReadReq { + addr: (i * TEST_RAM_DATA_W) as uN[TEST_ADDR_W], + }; + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{} read request {:#x}", i + u32:1, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{} read response {:#x}", i + u32:1, read_resp); + + assert_eq(test_data, read_resp.data); + + tok + }(tok); + + // read unaligned data + let tok = for (i, tok):(u32, token) in range(u32:0, array_size(TEST_READ_ADDR)) { + let test_read_addr = TEST_READ_ADDR[i]; + + let read_req = TestReadReq { + addr: test_read_addr, + }; + + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{} read request {:#x}", i + u32:1, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{} read response {:#x}", i + u32:1, read_resp); + + let ram_offset = test_read_addr as uN[RAM_NUM_W]; + let expected_data = if ram_offset == uN[RAM_NUM_W]:0 { + TEST_RAM_CONTENT[test_read_addr >> RAM_NUM_W] + } else { + let data_0 = TEST_RAM_CONTENT[test_read_addr >> RAM_NUM_W]; + let data_1 = TEST_RAM_CONTENT[(test_read_addr >> RAM_NUM_W) + uN[TEST_ADDR_W]:1]; + ( + (data_0 >> (TEST_RAM_DATA_W * ram_offset as u32)) | + (data_1 << (TEST_RAM_DATA_W * (RAM_NUM - ram_offset as u32))) + ) + }; + + assert_eq(expected_data, read_resp.data); + + tok + }(tok); + + send(tok, terminator, true); + } +} diff --git a/xls/modules/zstd/history_buffer.x b/xls/modules/zstd/history_buffer.x new file mode 100644 index 0000000000..bbb012d250 --- /dev/null +++ b/xls/modules/zstd/history_buffer.x @@ -0,0 +1,491 @@ +// 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. + +// This file contains implementation of HistoryBuffer. + + +import std; + +import xls.examples.ram as ram; +import xls.modules.zstd.aligned_parallel_ram as aligned_parallel_ram; + +const RAM_NUM = aligned_parallel_ram::RAM_NUM; +const RAM_NUM_W = aligned_parallel_ram::RAM_NUM_W; + + +pub struct HistoryBufferReadReq { + offset: uN[OFFSET_W], +} + +pub struct HistoryBufferReadResp { + data: uN[DATA_W], +} + +pub struct HistoryBufferWriteReq { + data: uN[DATA_W], +} + +pub struct HistoryBufferWriteResp {} + +struct HistoryBufferState { + curr_offset: uN[OFFSET_W], + length: uN[OFFSET_W], +} + +proc HistoryBufferReadRespHandler { + type ReadResp = HistoryBufferReadResp; + type ParallelRamReadResp = aligned_parallel_ram::AlignedParallelRamReadResp; + + parallel_ram_read_resp_r: chan in; + read_resp_s: chan out; + + config( + parallel_ram_read_resp_r: chan in, + read_resp_s: chan out, + ) { + ( + parallel_ram_read_resp_r, + read_resp_s, + ) + } + + init { } + + next (state: ()) { + let (tok, parallel_ram_read_resp, parallel_ram_read_resp_valid) = recv_non_blocking( + join(), parallel_ram_read_resp_r, zero!() + ); + + let read_resp = ReadResp { + data: parallel_ram_read_resp.data, + }; + + send_if(tok, read_resp_s, parallel_ram_read_resp_valid, read_resp); + } +} + +proc HistoryBufferWriteRespHandler { + type WriteResp = HistoryBufferWriteResp; + type ParallelRamWriteResp = aligned_parallel_ram::AlignedParallelRamWriteResp; + + parallel_ram_write_resp_r: chan in; + write_resp_s: chan out; + + config( + parallel_ram_write_resp_r: chan in, + write_resp_s: chan out, + ) { + ( + parallel_ram_write_resp_r, + write_resp_s, + ) + } + + init { } + + next (state: ()) { + let (tok, _, parallel_ram_write_resp_valid) = recv_non_blocking( + join(), parallel_ram_write_resp_r, zero!() + ); + + let write_resp = WriteResp { }; + + send_if(tok, write_resp_s, parallel_ram_write_resp_valid, write_resp); + } +} + +pub proc HistoryBuffer< + SIZE: u32, + DATA_W: u32, + OFFSET_W: u32 = {std::clog2(SIZE)}, + RAM_SIZE: u32 = {SIZE / RAM_NUM}, + RAM_DATA_W: u32 = {DATA_W / RAM_NUM}, + RAM_ADDR_W: u32 = {std::clog2(RAM_SIZE)}, + RAM_PARTITION_SIZE: u32 = {RAM_DATA_W}, + RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(RAM_PARTITION_SIZE, RAM_DATA_W)}, +>{ + type ReadReq = HistoryBufferReadReq; + type ReadResp = HistoryBufferReadResp; + type WriteReq = HistoryBufferWriteReq; + type WriteResp = HistoryBufferWriteResp; + + type ParallelRamReadReq = aligned_parallel_ram::AlignedParallelRamReadReq; + type ParallelRamReadResp = aligned_parallel_ram::AlignedParallelRamReadResp; + type ParallelRamWriteReq = aligned_parallel_ram::AlignedParallelRamWriteReq; + type ParallelRamWriteResp = aligned_parallel_ram::AlignedParallelRamWriteResp; + + type RamReadReq = ram::ReadReq; + type RamReadResp = ram::ReadResp; + type RamWriteReq = ram::WriteReq; + type RamWriteResp = ram::WriteResp; + + type State = HistoryBufferState; + type Offset = uN[OFFSET_W]; + + read_req_r: chan in; + write_req_r: chan in; + + // RAM interface + parallel_ram_read_req_s: chan out; + parallel_ram_write_req_s: chan out; + + config ( + read_req_r: chan in, + read_resp_s: chan out, + write_req_r: chan in, + write_resp_s: chan out, + ram_read_req_s: chan[RAM_NUM] out, + ram_read_resp_r: chan[RAM_NUM] in, + ram_write_req_s: chan[RAM_NUM] out, + ram_write_resp_r: chan[RAM_NUM] in, + ) { + let (parallel_ram_read_req_s, parallel_ram_read_req_r) = chan("parallel_ram_read_req"); + let (parallel_ram_read_resp_s, parallel_ram_read_resp_r) = chan("parallel_ram_read_resp"); + let (parallel_ram_write_req_s, parallel_ram_write_req_r) = chan("parallel_ram_write_req"); + let (parallel_ram_write_resp_s, parallel_ram_write_resp_r) = chan("parallel_ram_write_resp"); + + spawn HistoryBufferReadRespHandler ( + parallel_ram_read_resp_r, read_resp_s, + ); + + spawn HistoryBufferWriteRespHandler ( + parallel_ram_write_resp_r, write_resp_s, + ); + + spawn aligned_parallel_ram::AlignedParallelRam( + parallel_ram_read_req_r, parallel_ram_read_resp_s, + parallel_ram_write_req_r, parallel_ram_write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r, + ); + + ( + read_req_r, + write_req_r, + parallel_ram_read_req_s, + parallel_ram_write_req_s, + ) + } + + init { zero!() } + + next (state: State) { + const ONE_TRANSFER_WIDTH = ((DATA_W as uN[OFFSET_W]) >> u32:3); + const MAX_OFFSET = zero!(); + + // handle read request + let (tok, read_req, read_req_valid) = recv_non_blocking(join(), read_req_r, zero!()); + let offset_invalid = (read_req.offset > state.length - ONE_TRANSFER_WIDTH); + if read_req_valid & offset_invalid { + trace_fmt!("WARNING: Asking for too high offset (req: {:#x}, max: {:#x})", + read_req.offset, state.length - ONE_TRANSFER_WIDTH); + } else {}; + + let parallel_ram_read_req = ParallelRamReadReq { + addr: state.curr_offset - read_req.offset - ONE_TRANSFER_WIDTH, + }; + + send_if(tok, parallel_ram_read_req_s, read_req_valid, parallel_ram_read_req); + + // handle write request + let (tok, write_req, write_req_valid) = recv_non_blocking(join(), write_req_r, zero!()); + + let parallel_ram_write_req = ParallelRamWriteReq { + addr: state.curr_offset, + data: write_req.data, + }; + + send_if(tok, parallel_ram_write_req_s, write_req_valid, parallel_ram_write_req); + + // update offset + if write_req_valid { + let next_length = if state.length > MAX_OFFSET - ONE_TRANSFER_WIDTH { + MAX_OFFSET + } else { + state.length + ONE_TRANSFER_WIDTH + }; + + State { + curr_offset: state.curr_offset + ONE_TRANSFER_WIDTH, + length: next_length, + } + } else { + state + } + } +} + +const INST_SIZE = u32:1024; +const INST_DATA_W = u32:64; +const INST_OFFSET_W = {std::clog2(INST_SIZE)}; +const INST_RAM_SIZE = INST_SIZE / RAM_NUM; +const INST_RAM_DATA_W = {INST_DATA_W / RAM_NUM}; +const INST_RAM_ADDR_W = {std::clog2(INST_RAM_SIZE)}; +const INST_RAM_PARTITION_SIZE = {INST_RAM_DATA_W}; +const INST_RAM_NUM_PARTITIONS = {ram::num_partitions(INST_RAM_PARTITION_SIZE, INST_RAM_DATA_W)}; + +proc HistoryBufferInst { + type InstReadReq = HistoryBufferReadReq; + type InstReadResp = HistoryBufferReadResp; + type InstWriteReq = HistoryBufferWriteReq; + type InstWriteResp = HistoryBufferWriteResp; + + type InstParallelRamReadReq = aligned_parallel_ram::AlignedParallelRamReadReq; + type InstParallelRamReadResp = aligned_parallel_ram::AlignedParallelRamReadResp; + type InstParallelRamWriteReq = aligned_parallel_ram::AlignedParallelRamWriteReq; + type InstParallelRamWriteResp = aligned_parallel_ram::AlignedParallelRamWriteResp; + + type InstRamReadReq = ram::ReadReq; + type InstRamReadResp = ram::ReadResp; + type InstRamWriteReq = ram::WriteReq; + type InstRamWriteResp = ram::WriteResp; + + config ( + read_req_r: chan in, + read_resp_s: chan out, + write_req_r: chan in, + write_resp_s: chan out, + ram_read_req_s: chan[RAM_NUM] out, + ram_read_resp_r: chan[RAM_NUM] in, + ram_write_req_s: chan[RAM_NUM] out, + ram_write_resp_r: chan[RAM_NUM] in, + ) { + spawn HistoryBuffer ( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r, + ); + } + + init { } + + next (state: ()) { } +} + +const TEST_SIZE = u32:128; +const TEST_DATA_W = u32:64; +const TEST_OFFSET_W = {std::clog2(TEST_SIZE)}; +const TEST_RAM_SIZE = TEST_SIZE / aligned_parallel_ram::RAM_NUM; +const TEST_RAM_DATA_W = {TEST_DATA_W / RAM_NUM}; +const TEST_RAM_ADDR_W = {std::clog2(TEST_RAM_SIZE)}; +const TEST_RAM_PARTITION_SIZE = {TEST_RAM_DATA_W}; +const TEST_RAM_NUM_PARTITIONS = {ram::num_partitions(TEST_RAM_PARTITION_SIZE, TEST_RAM_DATA_W)}; + +const TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_RAM_INITIALIZED = true; + +type TestReadReq = HistoryBufferReadReq; +type TestReadResp = HistoryBufferReadResp; +type TestWriteReq = HistoryBufferWriteReq; +type TestWriteResp = HistoryBufferWriteResp; + +type TestRamReadReq = ram::ReadReq; +type TestRamReadResp = ram::ReadResp; +type TestRamWriteReq = ram::WriteReq; +type TestRamWriteResp = ram::WriteResp; + +const TEST_DATA = uN[TEST_DATA_W][64]:[ + uN[TEST_DATA_W]:0x44f9_072b_5ef2_8a80, uN[TEST_DATA_W]:0x4c01_2eda_5f3d_f4e1, + uN[TEST_DATA_W]:0x75ac_641d_fd42_0551, uN[TEST_DATA_W]:0x3bd0_3798_b29b_725f, + uN[TEST_DATA_W]:0x840e_71e9_e0e6_4fe1, uN[TEST_DATA_W]:0x9436_c1e7_bf3c_14e2, + uN[TEST_DATA_W]:0x1c64_9595_300d_4e0c, uN[TEST_DATA_W]:0x26ad_a821_926d_d9e5, + uN[TEST_DATA_W]:0x27e4_6ce8_a2b4_3a71, uN[TEST_DATA_W]:0xf9d6_cf94_6a39_5c5d, + uN[TEST_DATA_W]:0x7894_d415_88b5_dd0c, uN[TEST_DATA_W]:0x5c4e_4607_96bc_5d54, + uN[TEST_DATA_W]:0x29a4_3388_8b44_d9eb, uN[TEST_DATA_W]:0xda83_ee49_d921_7fb6, + uN[TEST_DATA_W]:0xf25f_c785_12e6_dfd0, uN[TEST_DATA_W]:0x1b8c_06fb_32ea_165a, + uN[TEST_DATA_W]:0x5dda_92e0_faca_af84, uN[TEST_DATA_W]:0x1157_8f9a_6e5c_7e78, + uN[TEST_DATA_W]:0xc908_a151_5b8c_b908, uN[TEST_DATA_W]:0xe978_4a80_f2e9_b11a, + uN[TEST_DATA_W]:0xc34e_96c0_4ae1_dfa9, uN[TEST_DATA_W]:0x4b06_4c8c_df6d_cae5, + uN[TEST_DATA_W]:0x9d51_4716_fd6f_afe9, uN[TEST_DATA_W]:0xfe42_4a9d_29ae_4bc4, + uN[TEST_DATA_W]:0x77ec_b4dd_9238_38b9, uN[TEST_DATA_W]:0xdf45_a790_a3da_1768, + uN[TEST_DATA_W]:0x45e9_5594_ffca_0604, uN[TEST_DATA_W]:0xe496_09f4_18ca_f955, + uN[TEST_DATA_W]:0x57cb_3c3d_ed78_62fd, uN[TEST_DATA_W]:0x0254_24bc_24fa_99f8, + uN[TEST_DATA_W]:0xc405_370f_a58a_1303, uN[TEST_DATA_W]:0xa451_310a_65b2_4785, + uN[TEST_DATA_W]:0x4373_65ac_f3ce_97ec, uN[TEST_DATA_W]:0x2a85_abd3_afde_133c, + uN[TEST_DATA_W]:0x836e_ce62_56cb_50ec, uN[TEST_DATA_W]:0x53ce_ab2f_d079_eb9a, + uN[TEST_DATA_W]:0xae76_7db7_0e64_8b88, uN[TEST_DATA_W]:0x079a_c187_642d_cbac, + uN[TEST_DATA_W]:0x2d07_5e3b_6150_d5c5, uN[TEST_DATA_W]:0x7865_5206_3c5a_98ed, + uN[TEST_DATA_W]:0xe905_351c_edda_0682, uN[TEST_DATA_W]:0xf41d_f3f2_1106_3639, + uN[TEST_DATA_W]:0xa44c_05c0_24b3_86ad, uN[TEST_DATA_W]:0xaa1f_c6b5_4c02_1f0c, + uN[TEST_DATA_W]:0xad67_cc1a_8740_87ae, uN[TEST_DATA_W]:0xf382_3bbf_f4b8_2f81, + uN[TEST_DATA_W]:0xe0cd_1eb3_b8c0_820b, uN[TEST_DATA_W]:0xb5d5_1c98_3415_1319, + uN[TEST_DATA_W]:0x583e_9722_ed31_84e6, uN[TEST_DATA_W]:0x6063_ccb6_6228_286e, + uN[TEST_DATA_W]:0xc642_cca8_e04f_769e, uN[TEST_DATA_W]:0x7cc7_ab72_7a9c_05d8, + uN[TEST_DATA_W]:0x4a66_f7c1_7b5e_6d30, uN[TEST_DATA_W]:0xd3d2_5e04_0310_7689, + uN[TEST_DATA_W]:0xe99d_a201_5dee_8e16, uN[TEST_DATA_W]:0xee15_ca30_c679_e1dd, + uN[TEST_DATA_W]:0xe61c_4ac3_183e_9478, uN[TEST_DATA_W]:0x2528_e948_2349_f8fd, + uN[TEST_DATA_W]:0xf15d_4275_a042_2135, uN[TEST_DATA_W]:0x05b5_3768_34e9_4bca, + uN[TEST_DATA_W]:0x1e00_a1a9_cffd_7a84, uN[TEST_DATA_W]:0x3396_a42c_2433_76f2, + uN[TEST_DATA_W]:0x80ba_e00e_9b93_7d76, uN[TEST_DATA_W]:0x85d4_10e6_404f_fa4d, +]; + +#[test_proc] +proc HistoryBuffer_test { + terminator: chan out; + + read_req_s: chan out; + read_resp_r: chan in; + write_req_s: chan out; + write_resp_r: chan in; + + config (terminator: chan out) { + let (read_req_s, read_req_r) = chan("read_req"); + let (read_resp_s, read_resp_r) = chan("read_resp"); + let (write_req_s, write_req_r) = chan("write_req"); + let (write_resp_s, write_resp_r) = chan("write_resp"); + + let (ram_read_req_s, ram_read_req_r) = chan[RAM_NUM]("ram_read_req"); + let (ram_read_resp_s, ram_read_resp_r) = chan[RAM_NUM]("ram_read_resp"); + let (ram_write_req_s, ram_write_req_r) = chan[RAM_NUM]("ram_write_req"); + let (ram_write_resp_s, ram_write_resp_r) = chan[RAM_NUM]("ram_write_resp"); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[0], ram_read_resp_s[0], ram_write_req_r[0], ram_write_resp_s[0], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[1], ram_read_resp_s[1], ram_write_req_r[1], ram_write_resp_s[1], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[2], ram_read_resp_s[2], ram_write_req_r[2], ram_write_resp_s[2], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[3], ram_read_resp_s[3], ram_write_req_r[3], ram_write_resp_s[3], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[4], ram_read_resp_s[4], ram_write_req_r[4], ram_write_resp_s[4], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[5], ram_read_resp_s[5], ram_write_req_r[5], ram_write_resp_s[5], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[6], ram_read_resp_s[6], ram_write_req_r[6], ram_write_resp_s[6], + ); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED + >( + ram_read_req_r[7], ram_read_resp_s[7], ram_write_req_r[7], ram_write_resp_s[7], + ); + + spawn HistoryBuffer ( + read_req_r, read_resp_s, + write_req_r, write_resp_s, + ram_read_req_s, ram_read_resp_r, + ram_write_req_s, ram_write_resp_r, + ); + + ( + terminator, + read_req_s, read_resp_r, + write_req_s, write_resp_r, + ) + } + + init { } + + next (state: ()) { + let tok = join(); + + let tok = for (i, tok) in range(u32:0, array_size(TEST_DATA)) { + let test_data = TEST_DATA[i]; + + // write current test data + let write_req = TestWriteReq { + data: test_data, + }; + let tok = send(tok, write_req_s, write_req); + trace_fmt!("Sent #{} write request {:#x}", i + u32:1, write_req); + + let (tok, _) = recv(tok, write_resp_r); + trace_fmt!("Received #{} write response", i + u32:1); + + // check written data + let read_req = TestReadReq { + offset: uN[TEST_OFFSET_W]:0, + }; + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{} read request {:#x}", i + u32:1, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{} read response {:#x}", i + u32:1, read_resp); + assert_eq(test_data, read_resp.data); + + // check previously saved data + let tok = for (offset, tok) in range(u32:0, TEST_SIZE - TEST_DATA_W) { + // check only offsets where data was written also dont check for all offsets + // to speedup the test + if (offset < RAM_NUM * i) && ((offset + i) % u32:13 == u32:0) { + let data_idx_0 = ((i * RAM_NUM - offset) >> RAM_NUM_W) % array_size(TEST_DATA); + let data_idx_1 = (((i * RAM_NUM - offset) >> RAM_NUM_W) + u32:1) % array_size(TEST_DATA); + let ram_offset = (offset) as uN[RAM_NUM_W]; + let prev_test_data = if ram_offset == uN[RAM_NUM_W]:0 { + TEST_DATA[data_idx_0] + } else { + ( + TEST_DATA[data_idx_1] << (TEST_RAM_DATA_W * ram_offset as u32) | + TEST_DATA[data_idx_0] >> (TEST_RAM_DATA_W * (aligned_parallel_ram::RAM_NUM - ram_offset as u32)) + ) + }; + + let read_req = TestReadReq { + offset: offset as uN[TEST_OFFSET_W], + }; + let tok = send(tok, read_req_s, read_req); + trace_fmt!("Sent #{}.{} read request {:#x}", i + u32:1, offset, read_req); + + let (tok, read_resp) = recv(tok, read_resp_r); + trace_fmt!("Received #{}.{} read response {:#x}", i + u32:1, offset, read_resp); + assert_eq(prev_test_data, read_resp.data); + + tok + } else { + tok + } + }(tok); + + tok + }(tok); + + send(tok, terminator, true); + } +} From 40e758cc385e3f8eeeebfa85412f743c4e8845b3 Mon Sep 17 00:00:00 2001 From: Robert Winkler Date: Tue, 31 Dec 2024 15:15:14 +0100 Subject: [PATCH 45/46] modules/zstd: Add Match Finder Signed-off-by: Robert Winkler Signed-off-by: Pawel Czarnecki --- xls/modules/zstd/BUILD | 88 +++ xls/modules/zstd/hash_table.x | 49 +- xls/modules/zstd/match_finder.x | 1182 +++++++++++++++++++++++++++++++ 3 files changed, 1303 insertions(+), 16 deletions(-) create mode 100644 xls/modules/zstd/match_finder.x diff --git a/xls/modules/zstd/BUILD b/xls/modules/zstd/BUILD index 989c3a6005..990e53aa10 100644 --- a/xls/modules/zstd/BUILD +++ b/xls/modules/zstd/BUILD @@ -1326,3 +1326,91 @@ place_and_route( tags = ["manual"], target_die_utilization_percentage = "10", ) + +xls_dslx_library( + name = "match_finder_dslx", + srcs = [ + "match_finder.x", + ], + deps = [ + "//xls/modules/zstd/memory:mem_reader_dslx", + "//xls/modules/zstd/memory:mem_writer_dslx", + "//xls/modules/zstd/memory:axi_dslx", + "//xls/modules/zstd/memory:axi_ram_dslx", + ":hash_table_dslx", + ":history_buffer_dslx", + ], +) + +xls_dslx_test( + name = "match_finder_dslx_test", + library = ":match_finder_dslx", +) + +match_finder_codegen_args = common_codegen_args | { + "module_name": "MatchFinder", + "pipeline_stages": "16", +} + +xls_dslx_verilog( + name = "match_finder_verilog", + codegen_args = match_finder_codegen_args | { + "ram_configurations": "ram:1R1W:{rd_req}:{rd_resp}:{wr_req}:{wr_resp}:{latency}".format( + latency = 5, + rd_req = "match_finder__ram_read_req_s", + rd_resp = "match_finder__ram_read_resp_r", + wr_req = "match_finder__ram_write_req_s", + wr_resp = "match_finder__ram_write_resp_r", + ), + }, + dslx_top = "MatchFinderInst", + library = ":match_finder_dslx", + opt_ir_args = { + "inline_procs": "true", + "top": "__match_finder__MatchFinderInst__MatchFinder_0__32_64_7_8_10_1024_8_512_10_40_3_next", + }, + verilog_file = "match_finder.v", +) + +xls_benchmark_ir( + name = "match_finder_opt_ir_benchmark", + src = ":match_finder_verilog.opt.ir", + benchmark_ir_args = match_finder_codegen_args, +) + +xls_benchmark_verilog( + name = "match_finder_verilog_benchmark", + verilog_target = "match_finder_verilog", +) + +verilog_library( + name = "match_finder_verilog_lib", + srcs = [ + ":match_finder.v", + ], +) + +synthesize_rtl( + name = "match_finder_asap7", + standard_cells = "@org_theopenroadproject_asap7sc7p5t_28//:asap7-sc7p5t_rev28_rvt", + top_module = "MatchFinder", + deps = [ + ":match_finder_verilog_lib", + ], +) + +benchmark_synth( + name = "match_finder_benchmark_synth", + synth_target = ":match_finder_asap7", +) + +place_and_route( + name = "match_finder_place_and_route", + clock_period = "750", + core_padding_microns = 2, + min_pin_distance = "0.09", + placement_density = "0.30", + stop_after_step = "global_routing", + synthesized_rtl = ":match_finder_asap7", + target_die_utilization_percentage = "10", +) diff --git a/xls/modules/zstd/hash_table.x b/xls/modules/zstd/hash_table.x index b2fd562f6e..4ed9211397 100644 --- a/xls/modules/zstd/hash_table.x +++ b/xls/modules/zstd/hash_table.x @@ -16,9 +16,10 @@ import std; import xls.examples.ram; +// FIXME: Use default parameter value when fixed (https://github.com/google/xls/issues/1425) +// SIZE_W:u32 = {std::clog2(SIZE + u32:1)} pub struct HashTableReadReq< - KEY_W: u32, SIZE: u32, - SIZE_W: u32 = {std::clog2(SIZE + u32:1)} + KEY_W: u32, SIZE: u32, SIZE_W:u32 > { num_entries_log2: uN[SIZE_W], // number of HashTable entries used in the runtime key: uN[KEY_W], @@ -29,9 +30,10 @@ pub struct HashTableReadResp { value: uN[VALUE_W] } +// FIXME: Use default parameter value when fixed (https://github.com/google/xls/issues/1425) +// SIZE_W:u32 = {std::clog2(SIZE + u32:1)} pub struct HashTableWriteReq< - KEY_W: u32, VALUE_W: u32, SIZE: u32, - SIZE_W:u32 = { std::clog2(SIZE + u32:1)} + KEY_W: u32, VALUE_W: u32, SIZE: u32, SIZE_W:u32 > { num_entries_log2: uN[SIZE_W], // number of HashTable entries used in the runtime key: uN[KEY_W], @@ -71,11 +73,12 @@ struct RamData { proc HashTableReadReqHandler< KEY_W: u32, VALUE_W: u32, SIZE: u32, KNUTH_CONSTANT: u32, + SIZE_W: u32 = {std::clog2(SIZE + u32:1)}, HASH_W: u32 = {std::clog2(SIZE)}, RAM_DATA_W: u32 = {VALUE_W + u32:1}, RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(u32:1, RAM_DATA_W)} > { - type ReadReq = HashTableReadReq; + type ReadReq = HashTableReadReq; type RamReadReq = ram::ReadReq; read_req_r: chan in; @@ -152,11 +155,12 @@ proc HashTableReadRespHandler< proc HashTableWriteReqHandler< KEY_W: u32, VALUE_W: u32, SIZE: u32, KNUTH_CONSTANT: u32, + SIZE_W: u32 = {std::clog2(SIZE + u32:1)}, HASH_W: u32 = {std::clog2(SIZE)}, RAM_DATA_W: u32 = {VALUE_W + u32:1}, RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(u32:1, RAM_DATA_W)} > { - type WriteReq = HashTableWriteReq; + type WriteReq = HashTableWriteReq; type RamWriteReq = ram::WriteReq; write_req_r: chan in; @@ -218,14 +222,15 @@ proc HashTableWriteRespHandler { pub proc HashTable< KEY_W: u32, VALUE_W: u32, SIZE: u32, + SIZE_W: u32 = {std::clog2(SIZE + u32:1)}, HASH_W: u32 = {std::clog2(SIZE)}, KNUTH_CONSTANT: u32 = {u32:0x1e35a7bd}, RAM_DATA_W: u32 = {VALUE_W + u32:1}, RAM_NUM_PARTITIONS: u32 = {ram::num_partitions(u32:1, RAM_DATA_W)} > { - type ReadReq = HashTableReadReq; + type ReadReq = HashTableReadReq; type ReadResp = HashTableReadResp; - type WriteReq = HashTableWriteReq; + type WriteReq = HashTableWriteReq; type WriteResp = HashTableWriteResp; type RamReadReq = ram::ReadReq; @@ -243,14 +248,25 @@ pub proc HashTable< ram_write_req_s: chan out, ram_write_resp_r: chan in ) { - spawn HashTableReadReqHandler( + spawn HashTableReadReqHandler< + KEY_W, VALUE_W, SIZE, KNUTH_CONSTANT, + // FIXME: Remove below parameters when resolving default values is fixed + SIZE_W, HASH_W, RAM_DATA_W, RAM_NUM_PARTITIONS, + >( read_req_r, ram_read_req_s ); - - spawn HashTableReadRespHandler( + spawn HashTableReadRespHandler< + VALUE_W, + // FIXME: Remove below parameters when resolving default values is fixed + RAM_DATA_W, + >( ram_read_resp_r, read_resp_s ); - spawn HashTableWriteReqHandler( + spawn HashTableWriteReqHandler< + KEY_W, VALUE_W, SIZE, KNUTH_CONSTANT, + // FIXME: Remove below parameters when resolving default values is fixed + SIZE_W, HASH_W, RAM_DATA_W, RAM_NUM_PARTITIONS, + >( write_req_r, ram_write_req_s ); spawn HashTableWriteRespHandler( @@ -266,14 +282,15 @@ pub proc HashTable< const INST_KEY_W = u32:32; const INST_VALUE_W = u32:32; const INST_SIZE = u32:512; +const INST_SIZE_W = std::clog2(INST_SIZE + u32:1); const INST_HASH_W = std::clog2(INST_SIZE); const INST_RAM_DATA_W = INST_VALUE_W + u32:1; const INST_RAM_NUM_PARTITIONS = ram::num_partitions(u32:1, INST_RAM_DATA_W); proc HashTableInst { - type InstReadReq = HashTableReadReq; + type InstReadReq = HashTableReadReq; type InstReadResp = HashTableReadResp; - type InstWriteReq = HashTableWriteReq; + type InstWriteReq = HashTableWriteReq; type InstWriteResp = HashTableWriteResp; type InstRamReadReq = ram::ReadReq; @@ -315,9 +332,9 @@ const TEST_RAM_NUM_PARTITIONS = ram::num_partitions(TEST_WORD_PARTITION_SIZE, TE const TEST_SIMULTANEOUS_READ_WRITE_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; const TEST_INITIALIZED = true; -type TestReadReq = HashTableReadReq; +type TestReadReq = HashTableReadReq; type TestReadResp = HashTableReadResp; -type TestWriteReq = HashTableWriteReq; +type TestWriteReq = HashTableWriteReq; type TestWriteResp = HashTableWriteResp; type TestRamReadReq = ram::ReadReq; diff --git a/xls/modules/zstd/match_finder.x b/xls/modules/zstd/match_finder.x new file mode 100644 index 0000000000..c351a58ce3 --- /dev/null +++ b/xls/modules/zstd/match_finder.x @@ -0,0 +1,1182 @@ +// 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. + +// This file contains implementation of MatchFinder + +import std; + +import xls.examples.ram as ram; +import xls.modules.zstd.common as common; +import xls.modules.zstd.memory.mem_reader as mem_reader; +import xls.modules.zstd.memory.mem_writer as mem_writer; +import xls.modules.zstd.memory.axi as axi; +import xls.modules.zstd.memory.axi_ram as axi_ram; +import xls.modules.zstd.history_buffer as history_buffer; +import xls.modules.zstd.hash_table as hash_table; +import xls.modules.zstd.aligned_parallel_ram as aligned_parallel_ram; + + +const SYMBOL_WIDTH = common::SYMBOL_WIDTH; + +struct ZstdParams { + num_entries_log2: uN[HT_SIZE_W], +} + +enum MatchFinderRespStatus : u1 { + OK = 0, + FAIL = 1, +} + +struct MatchFinderSequence { + literals_len: u16, + match_offset: u16, + match_len: u16, +} + +pub struct MatchFinderReq { + input_addr: uN[ADDR_W], + input_size: uN[ADDR_W], + output_lit_addr: uN[ADDR_W], + output_seq_addr: uN[ADDR_W], + zstd_params: ZstdParams, +} + +pub struct MatchFinderResp { + status: MatchFinderRespStatus, // indicate the state of the operation + lit_cnt: u32, // number of literals + seq_cnt: u32, // number of sequences +} + +struct MatchFinderInputBufferReq { + input_addr: uN[ADDR_W], + input_size: uN[ADDR_W], +} + +struct MatchFinderInputBufferResp { + data: uN[SYMBOL_WIDTH], + last: bool, +} + +struct MatchFinderInputBufferState< + ADDR_W: u32, DATA_W: u32, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, +> { + addr: uN[ADDR_W], + left_to_read: uN[ADDR_W], + buffer: uN[DATA_W], + buffer_len: uN[DATA_W_LOG2], +} + +proc MatchFinderInputBuffer< + ADDR_W: u32, DATA_W: u32, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, +> { + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + type MemReaderStatus = mem_reader::MemReaderStatus; + + type Req = MatchFinderInputBufferReq; + type Resp = MatchFinderInputBufferResp; + type State = MatchFinderInputBufferState; + + req_r: chan in; + next_r: chan<()> in; + out_s: chan out; + + // MemReader interface + mem_rd_req_s: chan out; + mem_rd_resp_r: chan in; + + config ( + req_r: chan in, + next_r: chan<()> in, + out_s: chan out, + mem_rd_req_s: chan out, + mem_rd_resp_r: chan in, + ) { + (req_r, next_r, out_s, mem_rd_req_s, mem_rd_resp_r) + } + + init { zero!() } + + next (state: State) { + // receive request + let do_recv_req = state.left_to_read == uN[ADDR_W]:0; + let (tok_req, req, req_valid) = recv_if_non_blocking(join(), req_r, do_recv_req, zero!()); + + // send memory read request + let mem_rd_req = MemReaderReq { + addr: req.input_addr, + length: req.input_size, + }; + send_if(tok_req, mem_rd_req_s, req_valid, mem_rd_req); + + // receive memory read response + let do_recv_mem_rd_resp = (state.left_to_read > uN[ADDR_W]:0) && (state.buffer_len == uN[DATA_W_LOG2]:0); + let (_, mem_rd_resp, mem_rd_resp_valid) = recv_if_non_blocking(join(), mem_rd_resp_r, do_recv_mem_rd_resp, zero!()); + + // receive next and send data + let do_recv_next = (state.left_to_read > uN[ADDR_W]:0 && (state.buffer_len > uN[DATA_W_LOG2]:0)); + let (tok_next, _, next_valid) = recv_if_non_blocking(join(), next_r, do_recv_next, ()); + + let resp = Resp { + data: state.buffer as uN[SYMBOL_WIDTH], + last: state.left_to_read == uN[ADDR_W]:1, + }; + send_if(tok_next, out_s, next_valid, resp); + + // update state + if req_valid { + State { + addr: req.input_addr, + left_to_read: req.input_size, + ..zero!() + } + } else if mem_rd_resp_valid { + State { + buffer: mem_rd_resp.data, + buffer_len: DATA_W as uN[DATA_W_LOG2], + ..state + } + } else if next_valid { + State { + left_to_read: state.left_to_read - uN[ADDR_W]:1, + buffer: state.buffer >> SYMBOL_WIDTH, + buffer_len: state.buffer_len - (SYMBOL_WIDTH as uN[DATA_W_LOG2]), + ..state + } + } else { + state + } + } +} + +enum MatchFinderFSM: u5 { + IDLE = 0, + INPUT_NEXT = 1, + INPUT_READ = 2, + HASH_TABLE_REQ = 4, + HASH_TABLE_RESP = 5, + HISTORY_BUFFER_RESP = 6, + OUTPUT_LITERAL_REQ_PACKET = 8, + OUTPUT_LITERAL_RESP = 9, + OUTPUT_SEQUENCE_REQ_PACKET = 10, + OUTPUT_SEQUENCE_RESP = 11, + INPUT_READ_NEXT_REQ = 15, + INPUT_READ_NEXT_RESP = 16, + SEND_RESP = 17, + FAILURE = 18, +} + +struct MatchFinderState< + DATA_W: u32, ADDR_W: u32, HT_SIZE_W: u32, MIN_SEQ_LEN: u32, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)} +> { + fsm: MatchFinderFSM, + req: MatchFinderReq, + input_data: uN[SYMBOL_WIDTH], + input_last: bool, + input_addr_offset: uN[ADDR_W], + output_lit_addr: uN[ADDR_W], + output_seq_addr: uN[ADDR_W], + lit_buffer: uN[MIN_SEQ_LEN][SYMBOL_WIDTH], + lit_buffer_last: bool, + literals_length: u16, + match_offset: u16, + match_length: u16, + lit_cnt: u32, + seq_cnt: u32, +} + +proc MatchFinder< + ADDR_W: u32, DATA_W: u32, HT_SIZE: u32, HB_SIZE: u32, MIN_SEQ_LEN: u32, + DATA_W_LOG2: u32 = {std::clog2(DATA_W + u32:1)}, + + HT_KEY_W: u32 = {SYMBOL_WIDTH}, + HT_VALUE_W: u32 = {SYMBOL_WIDTH + ADDR_W}, + HT_SIZE_W: u32 = {std::clog2(HT_SIZE + u32:1)}, + + HB_DATA_W: u32 = {SYMBOL_WIDTH}, + HB_OFFSET_W: u32 = {std::clog2(HB_SIZE)}, +> { + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + type MemReaderStatus = mem_reader::MemReaderStatus; + + type InputBufferReq = MatchFinderInputBufferReq; + type InputBufferResp = MatchFinderInputBufferResp; + + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; + type MemWriterRespStatus = mem_writer::MemWriterRespStatus; + + type Addr = uN[ADDR_W]; + + type HashTableRdReq = hash_table::HashTableReadReq; + type HashTableRdResp = hash_table::HashTableReadResp; + type HashTableWrReq = hash_table::HashTableWriteReq; + type HashTableWrResp = hash_table::HashTableWriteResp; + + type HistoryBufferRdReq = history_buffer::HistoryBufferReadReq; + type HistoryBufferRdResp = history_buffer::HistoryBufferReadResp; + type HistoryBufferWrReq = history_buffer::HistoryBufferWriteReq; + type HistoryBufferWrResp = history_buffer::HistoryBufferWriteResp; + + type Req = MatchFinderReq; + type Resp = MatchFinderResp; + type RespStatus = MatchFinderRespStatus; + type State = MatchFinderState; + + type NumEntries = uN[HT_SIZE_W]; + type Key = uN[HT_KEY_W]; + type FSM = MatchFinderFSM; + + req_r: chan in; + resp_s: chan out; + + // InputBuffer interface + inp_buf_req_s: chan out; + inp_buf_next_s: chan<()> out; + inp_buf_out_r: chan in; + + // MemWriter interface + mem_wr_req_s: chan out; + mem_wr_packet_s: chan out; + mem_wr_resp_r: chan in; + + // HashTable interface + ht_rd_req_s: chan out; + ht_rd_resp_r: chan in; + ht_wr_req_s: chan out; + ht_wr_resp_r: chan in; + + // HistoryBuffer interface + hb_rd_req_s: chan out; + hb_rd_resp_r: chan in; + hb_wr_req_s: chan out; + hb_wr_resp_r: chan in; + + config ( + // Req & Resp + req_r: chan in, + resp_s: chan out, + + // Access to input + mem_rd_req_s: chan out, + mem_rd_resp_r: chan in, + + // Output + mem_wr_req_s: chan out, + mem_wr_packet_s: chan out, + mem_wr_resp_r: chan in, + + // HashTable RAM interface + ht_rd_req_s: chan out, + ht_rd_resp_r: chan in, + ht_wr_req_s: chan out, + ht_wr_resp_r: chan in, + + // HistoryBuffer RAM interface + hb_rd_req_s: chan out, + hb_rd_resp_r: chan in, + hb_wr_req_s: chan out, + hb_wr_resp_r: chan in, + ) { + let (inp_buf_req_s, inp_buf_req_r) = chan("inp_buf_req"); + let (inp_buf_next_s, inp_buf_next_r) = chan<(), u32:0>("inp_buf_next"); + let (inp_buf_out_s, inp_buf_out_r) = chan("inp_buf_out"); + + spawn MatchFinderInputBuffer ( + inp_buf_req_r, inp_buf_next_r, inp_buf_out_s, + mem_rd_req_s, mem_rd_resp_r, + ); + + ( + req_r, resp_s, + inp_buf_req_s, inp_buf_next_s, inp_buf_out_r, + mem_wr_req_s, mem_wr_packet_s, mem_wr_resp_r, + ht_rd_req_s, ht_rd_resp_r, ht_wr_req_s, ht_wr_resp_r, + hb_rd_req_s, hb_rd_resp_r, hb_wr_req_s, hb_wr_resp_r, + ) + } + + init { zero!() } + + next (state: State) { + let tok0 = join(); + + // [IDLE] + let (tok1_0, req) = recv_if(tok0, req_r, state.fsm == FSM::IDLE, state.req); + let inp_buf_req = InputBufferReq { input_addr: req.input_addr, input_size: req.input_size }; + let tok1_1 = send_if(tok0, inp_buf_req_s, state.fsm == FSM::IDLE, inp_buf_req); + + // [INPUT_NEXT] + let do_send_inp_buf_next = state.fsm == FSM::INPUT_NEXT || state.fsm == FSM::INPUT_READ_NEXT_REQ; + + // [INPUT_READ] + let do_recv_inp_buf_out = state.fsm == FSM::INPUT_READ || state.fsm == FSM::INPUT_READ_NEXT_RESP; + let (tok1_2, inp_buf_out) = recv_if(tok0, inp_buf_out_r, do_recv_inp_buf_out, zero!()); + + // [HASH_TABLE_REQ] + let ht_rd_req = HashTableRdReq { + num_entries_log2: state.req.zstd_params.num_entries_log2, + key: state.input_data, + }; + let tok1_3 = send_if(tok0, ht_rd_req_s, state.fsm == FSM::HASH_TABLE_REQ, ht_rd_req); + + // [HASH_TABLE_RESP] + let (tok1_4, ht_rd_resp) = recv_if(tok0, ht_rd_resp_r, state.fsm == FSM::HASH_TABLE_RESP, zero!()); + let ht_is_match = ht_rd_resp.is_match && (((ht_rd_resp.value >> ADDR_W) as uN[SYMBOL_WIDTH]) == state.input_data); + let hb_rd_req = if ht_is_match { + HistoryBufferRdReq { + offset: (state.req.input_addr + state.input_addr_offset - (ht_rd_resp.value as uN[ADDR_W])) as uN[HB_OFFSET_W], + } + } else { + zero!() + }; + let tok1_4 = send_if(tok1_4, hb_rd_req_s, ht_is_match, hb_rd_req); + + // [HISTORY_BUFFER_RESP] | [HISTORY_BUFFER_NEXT_RESP] + let (tok1_5, hb_rd_resp) = recv_if(tok0, hb_rd_resp_r, state.fsm == FSM::HISTORY_BUFFER_RESP, zero!()); + let (tok1_2, inp_buf_out) = recv_if(tok0, inp_buf_out_r, state.fsm == FSM::HISTORY_BUFFER_RESP, zero!()); + + let is_next_match = state.input_data == hb_rd_resp.data; + let tok1_2 = send_if(tok0, inp_buf_next_s, do_send_inp_buf_next || ht_is_match || is_next_match, ()); + + // write entry in hash table + let do_save_entry = ( + (state.fsm == FSM::HASH_TABLE_RESP && !ht_is_match) || + (state.fsm == FSM::HISTORY_BUFFER_RESP && !is_next_match) + ); + let ht_wr_req = HashTableWrReq { + num_entries_log2: state.req.zstd_params.num_entries_log2, + key: state.input_data, + value: ( + (state.input_data) ++ + (state.req.input_addr + state.input_addr_offset) + ), + }; + let tok1_2 = send_if(tok1_2, ht_wr_req_s, do_save_entry, ht_wr_req); + // write entry in history buffer + let hb_wr_req = HistoryBufferWrReq { + data: state.input_data + }; + let tok1_2 = send_if(tok1_2, hb_wr_req_s, do_save_entry, hb_wr_req); + + // [OUTPUT_LITERAL_REQ_PACKET] + let lit_mem_wr_req = MemWriterReq { + addr: state.output_lit_addr, + length: uN[ADDR_W]:1, + }; + let tok1_7 = send_if(tok0, mem_wr_req_s, state.fsm == FSM::OUTPUT_LITERAL_REQ_PACKET, lit_mem_wr_req); + + let lit_mem_wr_packet = MemWriterDataPacket { + data: state.input_data as uN[DATA_W], + length: uN[ADDR_W]:1, + last: true, + }; + let tok1_8 = send_if(tok0, mem_wr_packet_s, state.fsm == FSM::OUTPUT_LITERAL_REQ_PACKET, lit_mem_wr_packet); + + // [OUTPUT_LITERAL_RESP] + let (tok1_9, lit_mem_wr_resp) = recv_if(tok0, mem_wr_resp_r, state.fsm == FSM::OUTPUT_LITERAL_RESP, zero!()); + + // [OUTPUT_SEQUENCE_REQ_PACKET] + let seq_mem_wr_req = MemWriterReq { + addr: state.output_seq_addr, + length: uN[ADDR_W]:6, + }; + let tok1_9 = send_if(tok0, mem_wr_req_s, state.fsm == FSM::OUTPUT_SEQUENCE_REQ_PACKET, seq_mem_wr_req); + + let seq_mem_wr_packet = MemWriterDataPacket { + data: (state.literals_length ++ state.match_offset ++ state.match_length) as uN[DATA_W], + length: uN[ADDR_W]:6, + last: true, + }; + + let tok1_10 = send_if(tok0, mem_wr_packet_s, state.fsm == FSM::OUTPUT_SEQUENCE_REQ_PACKET, seq_mem_wr_packet); + + // [OUTPUT_SEQUENCE_RESP] + let (tok1_11, seq_mem_wr_resp) = recv_if(tok0, mem_wr_resp_r, state.fsm == FSM::OUTPUT_SEQUENCE_RESP, zero!()); + + // [INPUT_READ_NEXT_REQ] + let hb_rd_req = HistoryBufferRdReq { + offset: (state.match_offset + state.match_length) as uN[HB_OFFSET_W], + }; + let tok1_15 = send_if(tok0, hb_rd_req_s, state.fsm == FSM::INPUT_READ_NEXT_REQ, hb_rd_req); + + // [SEND_RESP] + let resp = Resp { + status: RespStatus::OK, + lit_cnt: state.lit_cnt, + seq_cnt: state.seq_cnt, + }; + + let tok1_19 = send_if(tok0, resp_s, state.fsm == FSM::SEND_RESP, resp); + + // [FAILURE] + let fail_resp = MatchFinderResp { + status: MatchFinderRespStatus::FAIL, + lit_cnt: u32:0, + seq_cnt: u32:0, + }; + let tok1_4 = send_if(tok0, resp_s, state.fsm == FSM::FAILURE, fail_resp); + + let (tok, _, _) = recv_if_non_blocking(tok0, ht_wr_resp_r, false, zero!()); + let (tok, _, _) = recv_if_non_blocking(tok0, hb_wr_resp_r, false, zero!()); + + match state.fsm { + FSM::IDLE => { + trace_fmt!("[IDLE] Received match finder request {:#x}", req); + State { + fsm: FSM::INPUT_NEXT, + req: req, + input_addr_offset: uN[ADDR_W]:0, + output_lit_addr: req.output_lit_addr, + output_seq_addr: req.output_seq_addr, + ..zero!() + } + }, + + FSM::INPUT_NEXT => { + trace_fmt!("[INPUT_NEXT] Sent next to input buffer"); + State { + fsm: FSM::INPUT_READ, + input_addr_offset: state.input_addr_offset + uN[ADDR_W]:1, + ..state + } + }, + + FSM::INPUT_READ => { + trace_fmt!("[INPUT_READ] Received input {:#x}", inp_buf_out); + State { + fsm: FSM::HASH_TABLE_REQ, + input_data: inp_buf_out.data, + input_last: inp_buf_out.last, + ..state + } + }, + + FSM::HASH_TABLE_REQ => { + trace_fmt!("[HASH_TABLE_REQ] Sent HT read request {:#x}", ht_rd_req); + State { + fsm: FSM::HASH_TABLE_RESP, + ..state + } + }, + + FSM::HASH_TABLE_RESP => { + trace_fmt!("[HASH_TABLE_RESP] Received HT read respose {:#x}", ht_rd_resp); + if ht_is_match { + trace_fmt!("[HASH_TABLE_RESP] Sent HB read request {:#x}", hb_rd_req); + State { + fsm: FSM::HISTORY_BUFFER_RESP, + match_offset: hb_rd_req.offset as u16, + ..state + } + } else { + State { + fsm: FSM::OUTPUT_LITERAL_REQ_PACKET, + ..state + } + } + }, + + FSM::HISTORY_BUFFER_RESP => { + trace_fmt!("[HISTORY_BUFFER_RESP] Received HB read response {:#x}", hb_rd_resp); + trace_fmt!("[HISTORY_BUFFER_RESP] Next symbol {:#x}", state.input_data); + if is_next_match { + State { + fsm: FSM::INPUT_NEXT, + match_length: state.match_length + u16:1, + ..state + } + } else if state.match_length as u32 < MIN_SEQ_LEN { + State { + fsm: FSM::OUTPUT_LITERAL_REQ_PACKET, + ..state + } + } else { + State { + fsm: FSM::OUTPUT_SEQUENCE_REQ_PACKET, + ..state + } + } + }, + + FSM::OUTPUT_LITERAL_REQ_PACKET => { + trace_fmt!("[OUTPUT_LITERAL_REQ_PACKET] Sent mem write request {:#x}", lit_mem_wr_req); + trace_fmt!("[OUTPUT_LITERAL_REQ_PACKET] Sent mem write data {:#x}", lit_mem_wr_packet); + State { + fsm: FSM::OUTPUT_LITERAL_RESP, + ..state + } + }, + + FSM::OUTPUT_LITERAL_RESP => { + trace_fmt!("[OUTPUT_LITERAL_RESP] Received mem write response {:#x}", lit_mem_wr_resp); + if state.input_last { + State { + fsm: FSM::OUTPUT_SEQUENCE_REQ_PACKET, + output_lit_addr: state.output_lit_addr + uN[ADDR_W]:1, + literals_length: state.literals_length + u16:1, + ..state + } + } else { + State { + fsm: FSM::INPUT_NEXT, + output_lit_addr: state.output_lit_addr + uN[ADDR_W]:1, + literals_length: state.literals_length + u16:1, + ..state + } + } + }, + + FSM::OUTPUT_SEQUENCE_REQ_PACKET => { + trace_fmt!("[OUTPUT_SEQUENCE_REQ_PACKET] Sent mem write request {:#x}", seq_mem_wr_req); + trace_fmt!("[OUTPUT_SEQUENCE_REQ_PACKET] Sent mem write data {:#x}", seq_mem_wr_packet); + State { + fsm: FSM::OUTPUT_SEQUENCE_RESP, + lit_cnt: state.lit_cnt + (state.literals_length as u32), + seq_cnt: state.seq_cnt + (state.match_length as u32), + ..state + } + }, + + FSM::OUTPUT_SEQUENCE_RESP => { + trace_fmt!("[OUTPUT_SEQUENCE_RESP] Received mem write response {:#x}", seq_mem_wr_resp); + if state.input_last { + State { + fsm: FSM::SEND_RESP, + output_seq_addr: state.output_seq_addr + uN[ADDR_W]:6, + ..state + } + } else { + State { + fsm: FSM::HASH_TABLE_REQ, // here we reuse last response, which was not matched + output_seq_addr: state.output_seq_addr + uN[ADDR_W]:6, + literals_length: u16:0, + match_length: u16:0, + ..state + } + } + }, + + FSM::INPUT_READ_NEXT_REQ => { + trace_fmt!("[INPUT_NEXT] Sent next to input buffer"); + State { + fsm: FSM::INPUT_READ_NEXT_RESP, + input_addr_offset: state.input_addr_offset + uN[ADDR_W]:1, + ..state + } + }, + + FSM::INPUT_READ_NEXT_RESP => { + trace_fmt!("[INPUT_READ_NEXT_RESP ] Received input {:#x}", inp_buf_out); + State { + fsm: FSM::HISTORY_BUFFER_RESP, + input_data: inp_buf_out.data, + input_last: inp_buf_out.last, + ..state + } + }, + + FSM::SEND_RESP => { + trace_fmt!("[SEND_RESP] Sent response {:#x}", resp); + State { + fsm: FSM::IDLE, + ..zero!() + } + }, + + FSM::FAILURE => { + trace_fmt!("[FAILURE] !!!"); + State { + fsm: FSM::IDLE, + ..zero!() + } + }, + + _ => state, + } + } +} + +const INST_ADDR_W = u32:32; +const INST_DATA_W = u32:64; +const INST_MIN_SEQ_LEN = u32:3; +const INST_DATA_W_LOG2 = std::clog2(INST_DATA_W + u32:1); + +const INST_HT_SIZE = u32:512; +const INST_HT_SIZE_W = std::clog2(INST_HT_SIZE + u32:1); +const INST_HT_KEY_W = SYMBOL_WIDTH; +const INST_HT_VALUE_W = SYMBOL_WIDTH + INST_ADDR_W; // original symbol + address +const INST_HB_DATA_W = SYMBOL_WIDTH; +const INST_HB_SIZE = u32:1024; +const INST_HB_OFFSET_W = std::clog2(INST_HB_SIZE); + +proc MatchFinderInst { + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; + + type HashTableRdReq = hash_table::HashTableReadReq; + type HashTableRdResp = hash_table::HashTableReadResp; + type HashTableWrReq = hash_table::HashTableWriteReq; + type HashTableWrResp = hash_table::HashTableWriteResp; + + type HistoryBufferRdReq = history_buffer::HistoryBufferReadReq; + type HistoryBufferRdResp = history_buffer::HistoryBufferReadResp; + type HistoryBufferWrReq = history_buffer::HistoryBufferWriteReq; + type HistoryBufferWrResp = history_buffer::HistoryBufferWriteResp; + + type Req = MatchFinderReq; + type Resp = MatchFinderResp; + + config ( + // Req & Resp + req_r: chan in, + resp_s: chan out, + + // Access to input + mem_rd_req_s: chan out, + mem_rd_resp_r: chan in, + + // Output + mem_wr_req_s: chan out, + mem_wr_packet_s: chan out, + mem_wr_resp_r: chan in, + + // HashTable RAM interface + ht_rd_req_s: chan out, + ht_rd_resp_r: chan in, + ht_wr_req_s: chan out, + ht_wr_resp_r: chan in, + + // HistoryBuffer RAM interface + hb_rd_req_s: chan out, + hb_rd_resp_r: chan in, + hb_wr_req_s: chan out, + hb_wr_resp_r: chan in, + ) { + spawn MatchFinder< + INST_ADDR_W, INST_DATA_W, INST_HT_SIZE, INST_HB_SIZE, INST_MIN_SEQ_LEN, + INST_DATA_W_LOG2, + INST_HT_KEY_W, INST_HT_VALUE_W, INST_HT_SIZE_W, + INST_HB_DATA_W, INST_HB_OFFSET_W, + >( + req_r, resp_s, + mem_rd_req_s, mem_rd_resp_r, + mem_wr_req_s, mem_wr_packet_s, mem_wr_resp_r, + ht_rd_req_s, ht_rd_resp_r, ht_wr_req_s, ht_wr_resp_r, + hb_rd_req_s, hb_rd_resp_r, hb_wr_req_s, hb_wr_resp_r, + ); + } + + init {} + + next (state: ()) {} +} +const TEST_ADDR_W = u32:32; +const TEST_DATA_W = u32:64; +const TEST_MIN_SEQ_LEN = u32:3; +const TEST_HT_SIZE = u32:512; +const TEST_HB_SIZE = u32:1024; +const TEST_DATA_W_LOG2 = std::clog2(TEST_DATA_W + u32:1); +const TEST_DEST_W = u32:8; +const TEST_ID_W = u32:8; + +const TEST_HT_KEY_W = SYMBOL_WIDTH; +const TEST_HT_VALUE_W = SYMBOL_WIDTH + TEST_ADDR_W; // original symbol + address +const TEST_HT_HASH_W = std::clog2(TEST_HT_SIZE); +const TEST_HT_RAM_DATA_W = TEST_HT_VALUE_W + u32:1; // value + valid +const TEST_HT_RAM_WORD_PARTITION_SIZE = u32:1; +const TEST_HT_RAM_NUM_PARTITIONS = ram::num_partitions(TEST_HT_RAM_WORD_PARTITION_SIZE, TEST_HT_RAM_DATA_W); +const TEST_HT_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_HT_RAM_INITIALIZED = true; +const TEST_HT_SIZE_W = std::clog2(TEST_HT_SIZE + u32:1); + +const TEST_HB_RAM_NUM = u32:8; +const TEST_HB_DATA_W = SYMBOL_WIDTH; +const TEST_HB_OFFSET_W = std::clog2(TEST_HB_SIZE); +const TEST_HB_RAM_SIZE = TEST_HB_SIZE / TEST_HB_RAM_NUM; +const TEST_HB_RAM_DATA_W = SYMBOL_WIDTH / TEST_HB_RAM_NUM; +const TEST_HB_RAM_ADDR_W = std::clog2(TEST_HB_RAM_SIZE); +const TEST_HB_RAM_PARTITION_SIZE = TEST_HB_RAM_DATA_W; +const TEST_HB_RAM_NUM_PARTITIONS = ram::num_partitions(TEST_HB_RAM_PARTITION_SIZE, TEST_HB_RAM_DATA_W); +const TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_HB_RAM_INITIALIZED = true; + +const TEST_RAM_DATA_W = TEST_DATA_W; +const TEST_RAM_SIZE = u32:1024; +const TEST_RAM_ADDR_W = TEST_ADDR_W; +const TEST_RAM_PARTITION_SIZE = TEST_RAM_DATA_W / u32:8; +const TEST_RAM_NUM_PARTITIONS = ram::num_partitions(TEST_RAM_PARTITION_SIZE, TEST_RAM_DATA_W); +const TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR = ram::SimultaneousReadWriteBehavior::READ_BEFORE_WRITE; +const TEST_RAM_INITIALIZED = true; +const TEST_RAM_ASSERT_VALID_READ = true; + +const TEST_OUTPUT_LIT_ADDR = uN[TEST_ADDR_W]:0x100; +const TEST_OUTPUT_SEQ_ADDR = uN[TEST_ADDR_W]:0x200; +const TEST_OUTPUT_ADDR_MASK = uN[TEST_ADDR_W]:0xF00; + +// Test data +// 010A_0B0C_0102_0104_0A0B_0C05_0A0B_0C09 +// ^- ---- ^--- -- ^--- -- +// Expected output +// literals: 010A_0B0C_0102_0104_0509 +// sequences: (8, 7, 3), (1, 4, 3), (1, 0, 0) + +const TEST_DATA = uN[TEST_RAM_DATA_W][2]:[ + u64:0x0401_0201_0C0B_0A01, + u64:0x090C_0B0A_050C_0B0A, +]; + +const TEST_LITERALS = u8[10]:[ + u8:0x09, u8:0x05, u8:0x04, u8:0x01, u8:0x02, u8:0x01, u8:0x0C, u8:0x0B, u8:0x0A, u8:0x01, +]; + +const TEST_SEQUENCES = MatchFinderSequence[3]:[ + MatchFinderSequence { + literals_len: u16:8, + match_offset: u16:7, + match_len: u16:3, + }, + MatchFinderSequence { + literals_len: u16:1, + match_offset: u16:4, + match_len: u16:3, + }, + MatchFinderSequence { + literals_len: u16:1, + match_offset: u16:0, + match_len: u16:0, + }, +]; + +struct TestState { + iteration: u32, + lit_buffer: uN[SYMBOL_WIDTH][256], + seq_buffer: MatchFinderSequence[32], + wr_addr: uN[TEST_ADDR_W], + wr_offset: uN[TEST_ADDR_W], + wr_len: uN[TEST_ADDR_W], +} + +#[test_proc] +proc MatchFinderTest { + + // Memory Reader + Input + + type MemReaderReq = mem_reader::MemReaderReq; + type MemReaderResp = mem_reader::MemReaderResp; + + type InputBufferRamRdReq = ram::ReadReq; + type InputBufferRamRdResp = ram::ReadResp; + type InputBufferRamWrReq = ram::WriteReq; + type InputBufferRamWrResp = ram::WriteResp; + + type AxiAr = axi::AxiAr; + type AxiR = axi::AxiR; + + // Memory Writer + + type MemWriterReq = mem_writer::MemWriterReq; + type MemWriterResp = mem_writer::MemWriterResp; + type MemWriterDataPacket = mem_writer::MemWriterDataPacket; + + // Hash Table + + type HashTableRdReq = hash_table::HashTableReadReq; + type HashTableRdResp = hash_table::HashTableReadResp; + type HashTableWrReq = hash_table::HashTableWriteReq; + type HashTableWrResp = hash_table::HashTableWriteResp; + + type HashTableRamRdReq = ram::ReadReq; + type HashTableRamRdResp = ram::ReadResp; + type HashTableRamWrReq = ram::WriteReq; + type HashTableRamWrResp = ram::WriteResp; + + // History Buffer + + type HistoryBufferRdReq = history_buffer::HistoryBufferReadReq; + type HistoryBufferRdResp = history_buffer::HistoryBufferReadResp; + type HistoryBufferWrReq = history_buffer::HistoryBufferWriteReq; + type HistoryBufferWrResp = history_buffer::HistoryBufferWriteResp; + + type HistoryBufferRamRdReq = ram::ReadReq; + type HistoryBufferRamRdResp = ram::ReadResp; + type HistoryBufferRamWrReq = ram::WriteReq; + type HistoryBufferRamWrResp = ram::WriteResp; + + // Match Finder + + type Req = MatchFinderReq; + type Resp = MatchFinderResp; + + // Other + + type NumEntriesLog2 = uN[TEST_HT_SIZE_W]; + type RamAddr = uN[TEST_RAM_ADDR_W]; + type RamData = uN[TEST_RAM_DATA_W]; + type RamMask = uN[TEST_RAM_NUM_PARTITIONS]; + + terminator: chan out; + + req_s: chan out; + resp_r: chan in; + + mem_wr_req_r: chan in; + mem_wr_packet_r: chan in; + mem_wr_resp_s: chan out; + + input_ram_rd_req_s: chan out; + input_ram_rd_resp_r: chan in; + input_ram_wr_req_s: chan out; + input_ram_wr_resp_r: chan in; + + config(terminator: chan out) { + + // Hash Table RAM + + let (ht_ram_rd_req_s, ht_ram_rd_req_r) = chan("ht_ram_rd_req"); + let (ht_ram_rd_resp_s, ht_ram_rd_resp_r) = chan("ht_ram_rd_resp"); + let (ht_ram_wr_req_s, ht_ram_wr_req_r) = chan("ht_ram_wr_req"); + let (ht_ram_wr_resp_s, ht_ram_wr_resp_r) = chan("ht_ram_wr_resp"); + + spawn ram::RamModel< + TEST_HT_RAM_DATA_W, TEST_HT_SIZE, TEST_HT_RAM_WORD_PARTITION_SIZE, + TEST_HT_RAM_SIMULTANEOUS_READ_WRITE_BEHAVIOR, TEST_HT_RAM_INITIALIZED + >( + ht_ram_rd_req_r, ht_ram_rd_resp_s, + ht_ram_wr_req_r, ht_ram_wr_resp_s + ); + + // Hash Table + + let (ht_rd_req_s, ht_rd_req_r) = chan("ht_rd_req"); + let (ht_rd_resp_s, ht_rd_resp_r) = chan("ht_rd_resp"); + let (ht_wr_req_s, ht_wr_req_r) = chan("ht_wr_req"); + let (ht_wr_resp_s, ht_wr_resp_r) = chan("ht_wr_resp"); + + spawn hash_table::HashTable( + ht_rd_req_r, ht_rd_resp_s, + ht_wr_req_r, ht_wr_resp_s, + ht_ram_rd_req_s, ht_ram_rd_resp_r, + ht_ram_wr_req_s, ht_ram_wr_resp_r, + ); + + // History Buffer RAM + + let (hb_ram_rd_req_s, hb_ram_rd_req_r) = chan[8]("hb_ram_rd_req"); + let (hb_ram_rd_resp_s, hb_ram_rd_resp_r) = chan[8]("hb_ram_rd_resp"); + let (hb_ram_wr_req_s, hb_ram_wr_req_r) = chan[8]("hb_ram_wr_req"); + let (hb_ram_wr_resp_s, hb_ram_wr_resp_r) = chan[8]("hb_ram_wr_resp"); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[0], hb_ram_rd_resp_s[0], + hb_ram_wr_req_r[0], hb_ram_wr_resp_s[0], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[1], hb_ram_rd_resp_s[1], + hb_ram_wr_req_r[1], hb_ram_wr_resp_s[1], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[2], hb_ram_rd_resp_s[2], + hb_ram_wr_req_r[2], hb_ram_wr_resp_s[2], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[3], hb_ram_rd_resp_s[3], + hb_ram_wr_req_r[3], hb_ram_wr_resp_s[3], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[4], hb_ram_rd_resp_s[4], + hb_ram_wr_req_r[4], hb_ram_wr_resp_s[4], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[5], hb_ram_rd_resp_s[5], + hb_ram_wr_req_r[5], hb_ram_wr_resp_s[5], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[6], hb_ram_rd_resp_s[6], + hb_ram_wr_req_r[6], hb_ram_wr_resp_s[6], + ); + + spawn ram::RamModel< + TEST_HB_RAM_DATA_W, TEST_HB_RAM_SIZE, TEST_HB_RAM_PARTITION_SIZE, + TEST_HB_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_HB_RAM_INITIALIZED + >( + hb_ram_rd_req_r[7], hb_ram_rd_resp_s[7], + hb_ram_wr_req_r[7], hb_ram_wr_resp_s[7], + ); + + // History Buffer + + let (hb_rd_req_s, hb_rd_req_r) = chan("hb_rd_req"); + let (hb_rd_resp_s, hb_rd_resp_r) = chan("hb_rd_resp"); + let (hb_wr_req_s, hb_wr_req_r) = chan("hb_wr_req"); + let (hb_wr_resp_s, hb_wr_resp_r) = chan("hb_wr_resp"); + + spawn history_buffer::HistoryBuffer( + hb_rd_req_r, hb_rd_resp_s, + hb_wr_req_r, hb_wr_resp_s, + hb_ram_rd_req_s, hb_ram_rd_resp_r, + hb_ram_wr_req_s, hb_ram_wr_resp_r, + ); + + // Input Memory + + let (input_ram_rd_req_s, input_ram_rd_req_r) = chan("input_ram_rd_req"); + let (input_ram_rd_resp_s, input_ram_rd_resp_r) = chan("input_ram_rd_resp"); + let (input_ram_wr_req_s, input_ram_wr_req_r) = chan("input_ram_wr_req"); + let (input_ram_wr_resp_s, input_ram_wr_resp_r) = chan("input_ram_wr_resp"); + + spawn ram::RamModel< + TEST_RAM_DATA_W, TEST_RAM_SIZE, TEST_RAM_PARTITION_SIZE, + TEST_RAM_SIMULTANEOUS_RW_BEHAVIOR, TEST_RAM_INITIALIZED, + TEST_RAM_ASSERT_VALID_READ, TEST_RAM_ADDR_W, + >( + input_ram_rd_req_r, input_ram_rd_resp_s, + input_ram_wr_req_r, input_ram_wr_resp_s, + ); + + // Input Memory Axi Reader + + let (axi_ar_s, axi_ar_r) = chan("axi_ar"); + let (axi_r_s, axi_r_r) = chan("axi_r"); + + spawn axi_ram::AxiRamReader< + TEST_ADDR_W, TEST_DATA_W, + TEST_DEST_W, TEST_ID_W, + TEST_RAM_SIZE, + >( + axi_ar_r, axi_r_s, + input_ram_rd_req_s, input_ram_rd_resp_r, + ); + + // Input Memory Reader + + let (mem_rd_req_s, mem_rd_req_r) = chan("mem_rd_req"); + let (mem_rd_resp_s, mem_rd_resp_r) = chan("mem_rd_resp"); + + spawn mem_reader::MemReader< + TEST_DATA_W, TEST_ADDR_W, TEST_DEST_W, TEST_ID_W, + >( + mem_rd_req_r, mem_rd_resp_s, + axi_ar_s, axi_r_r, + ); + + // Output Memory Writer + + let (mem_wr_req_s, mem_wr_req_r) = chan("mem_wr_req"); + let (mem_wr_packet_s, mem_wr_packet_r) = chan("mem_wr_packet"); + let (mem_wr_resp_s, mem_wr_resp_r) = chan("mem_wr_resp"); + + // Match Finder + + let (req_s, req_r) = chan("req"); + let (resp_s, resp_r) = chan("resp"); + + spawn MatchFinder< + TEST_ADDR_W, TEST_DATA_W, TEST_HT_SIZE, TEST_HB_SIZE, TEST_MIN_SEQ_LEN, + TEST_DATA_W_LOG2, + TEST_HT_KEY_W, TEST_HT_VALUE_W, TEST_HT_SIZE_W, + TEST_HB_DATA_W, TEST_HB_OFFSET_W, + >( + req_r, resp_s, + mem_rd_req_s, mem_rd_resp_r, + mem_wr_req_s, mem_wr_packet_s, mem_wr_resp_r, + ht_rd_req_s, ht_rd_resp_r, ht_wr_req_s, ht_wr_resp_r, + hb_rd_req_s, hb_rd_resp_r, hb_wr_req_s, hb_wr_resp_r, + ); + + ( + terminator, + req_s, resp_r, + mem_wr_req_r, mem_wr_packet_r, mem_wr_resp_s, + input_ram_rd_req_s, input_ram_rd_resp_r, input_ram_wr_req_s, input_ram_wr_resp_r, + ) + + } + + init { zero!() } + + next(state: TestState) { + let tok = join(); + + let tok = if state.iteration == u32:0 { + // Fill the input RAM + let tok = for ((i, test_data), tok) in enumerate(TEST_DATA) { + let ram_wr_req = InputBufferRamWrReq { + addr: i as RamAddr, + data: test_data, + mask: !RamMask:0, + }; + let tok = send(tok, input_ram_wr_req_s, ram_wr_req); + trace_fmt!("[TEST] Sent #{} data to input RAM {:#x}", i + u32:1, ram_wr_req); + let (tok, _) = recv(tok, input_ram_wr_resp_r); + tok + }(tok); + + // Start the request + + let req = Req { + input_addr: uN[TEST_ADDR_W]:0x0, + input_size: (array_size(TEST_DATA) * TEST_DATA_W / SYMBOL_WIDTH) as u32, + output_lit_addr: TEST_OUTPUT_LIT_ADDR, + output_seq_addr: TEST_OUTPUT_SEQ_ADDR, + zstd_params: ZstdParams { + num_entries_log2: NumEntriesLog2:8, + }, + }; + + let tok = send(tok, req_s, req); + trace_fmt!("[TEST] Sent request to the MatchFinder: {:#x}", req); + + tok + } else { + tok + }; + + let (tok, mem_wr_req, mem_wr_req_valid) = recv_if_non_blocking( + tok, mem_wr_req_r, state.wr_len == uN[TEST_ADDR_W]:0, zero!() + ); + let (tok, mem_wr_packet, mem_wr_packet_valid) = recv_if_non_blocking( + tok, mem_wr_packet_r, state.wr_len > uN[TEST_ADDR_W]:0, zero!() + ); + let (tok, resp, resp_valid) = recv_if_non_blocking( + tok, resp_r, state.wr_len == uN[TEST_ADDR_W]:0, zero!() + ); + + let tok = send_if(tok, mem_wr_resp_s, mem_wr_packet_valid, MemWriterResp{status: mem_writer::MemWriterRespStatus::OKAY}); + + let state = if mem_wr_req_valid { + TestState { + wr_addr: mem_wr_req.addr, + wr_offset: uN[TEST_ADDR_W]:0, + wr_len: mem_wr_req.length, + ..state + } + } else { + state + }; + + if mem_wr_req_valid { + trace_fmt!("[TEST] Received {:#x}", mem_wr_req); + } else {}; + if mem_wr_packet_valid { + trace_fmt!("[TEST] Received {:#x}", mem_wr_packet); + } else {}; + + let state = if mem_wr_packet_valid { + if mem_wr_packet.length > state.wr_len { + trace_fmt!("[TEST] Invalid packet length"); + fail!("invalid_packet_length", mem_wr_packet.length); + } else {}; + let state = match (state.wr_addr & TEST_OUTPUT_ADDR_MASK) { + TEST_OUTPUT_LIT_ADDR => { + trace_fmt!("[TEST] Received literals"); + let lit_buffer = for (i, lit_buffer) in range(u32:0, TEST_DATA_W / SYMBOL_WIDTH) { + if i < mem_wr_packet.length { + let literal = (mem_wr_packet.data >> (SYMBOL_WIDTH * i)) as uN[SYMBOL_WIDTH]; + let idx = (TEST_ADDR_W / SYMBOL_WIDTH) * (state.wr_addr - TEST_OUTPUT_LIT_ADDR + state.wr_offset) + i; + update(lit_buffer, idx, literal) + } else { + lit_buffer + } + }(state.lit_buffer); + TestState { + lit_buffer: lit_buffer, + wr_offset: state.wr_offset + mem_wr_packet.length, + wr_len: state.wr_len - mem_wr_packet.length, + ..state + } + }, + TEST_OUTPUT_SEQ_ADDR => { + trace_fmt!("[TEST] Received sequence"); + assert_eq(uN[TEST_ADDR_W]:6, mem_wr_packet.length); + let sequence = MatchFinderSequence { + literals_len: (mem_wr_packet.data >> u32:32) as u16, + match_offset: (mem_wr_packet.data >> u32:16) as u16, + match_len: mem_wr_packet.data as u16, + }; + let idx = ((TEST_ADDR_W / SYMBOL_WIDTH) * (state.wr_addr - TEST_OUTPUT_SEQ_ADDR + state.wr_offset)) / u32:3; + let seq_buffer = update(state.seq_buffer, idx, sequence); + TestState { + seq_buffer: seq_buffer, + wr_offset: state.wr_offset + mem_wr_packet.length, + wr_len: state.wr_len - mem_wr_packet.length, + ..state + } + }, + _ => { + trace_fmt!("[TEST] Invalid write addres"); + fail!("invalid_wr_addr", state.wr_addr); + state + }, + }; + state + } else { + state + }; + + if resp_valid { + // check buffers content + trace_fmt!("[TEST] Received Match Finder response {:#x}", resp); + for ((i, test_lit), ()) in enumerate(TEST_LITERALS) { + assert_eq(test_lit, state.lit_buffer[i]); + }(()); + for ((i, test_seq), ()) in enumerate(TEST_SEQUENCES) { + assert_eq(test_seq, state.seq_buffer[i]); + }(()); + } else { }; + + send_if(tok, terminator, resp_valid || state.iteration > u32:1000, true); + + TestState { + iteration: state.iteration + u32:1, + ..state + } + } +} From 94449aa6f4fbd356565b84baf6d87389f8daea8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Ob=C5=82onczek?= Date: Fri, 10 Jan 2025 18:00:06 +0100 Subject: [PATCH 46/46] [TEMP] modules/zstd: disable codegen in CI Signed-off-by: Krzysztof Oblonczek --- .github/workflows/modules-zstd.yml | 54 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/modules-zstd.yml b/.github/workflows/modules-zstd.yml index 7dfde39147..b3294364b5 100644 --- a/.github/workflows/modules-zstd.yml +++ b/.github/workflows/modules-zstd.yml @@ -38,9 +38,9 @@ jobs: run: | bazel build -c opt --test_output=errors -- //xls/dslx:interpreter_main //xls/dslx/ir_convert:ir_converter_main //xls/tools:opt_main //xls/tools:codegen_main - - name: Build ZSTD Module (opt) - run: | - bazel build -c opt --test_output=errors -- //xls/modules/zstd:all + # - name: Build ZSTD Module (opt) + # run: | + # bazel build -c opt --test_output=errors -- //xls/modules/zstd:all - name: Test ZSTD Module - DSLX Tests (opt) if: ${{ !cancelled() }} @@ -52,35 +52,35 @@ jobs: run: | bazel test -c opt --test_output=errors -- $(bazel query 'filter(".*_cc_test", kind(rule, //xls/modules/zstd/...))') - - name: Build ZSTD verilog targets (opt) - if: ${{ !cancelled() }} - run: | - bazel build -c opt -- $(bazel query 'filter(".*_verilog", kind(rule, //xls/modules/zstd/...))') + # - name: Build ZSTD verilog targets (opt) + # if: ${{ !cancelled() }} + # run: | + # bazel build -c opt -- $(bazel query 'filter(".*_verilog", kind(rule, //xls/modules/zstd/...))') - - name: Build and run ZSTD IR benchmark rules (opt) - if: ${{ !cancelled() }} - run: | - for target in $(bazel query 'filter(".*_ir_benchmark$", kind(rule, //xls/modules/zstd/...))'); - do - echo "running $target"; - bazel run -c opt $target -- --logtostderr; - done + # - name: Build and run ZSTD IR benchmark rules (opt) + # if: ${{ !cancelled() }} + # run: | + # for target in $(bazel query 'filter(".*_ir_benchmark$", kind(rule, //xls/modules/zstd/...))'); + # do + # echo "running $target"; + # bazel run -c opt $target -- --logtostderr; + # done - - name: Build and run synthesis benchmarks of the ZSTD module (opt) - if: ${{ !cancelled() }} - run: | - for target in $(bazel query 'filter(".*_benchmark_synth$", kind(rule, //xls/modules/zstd/...))'); - do - echo "running $target"; - bazel run -c opt $target -- --logtostderr; - done + # - name: Build and run synthesis benchmarks of the ZSTD module (opt) + # if: ${{ !cancelled() }} + # run: | + # for target in $(bazel query 'filter(".*_benchmark_synth$", kind(rule, //xls/modules/zstd/...))'); + # do + # echo "running $target"; + # 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: | - bazel build -c opt -- $(bazel query 'filter(".*_place_and_route", kind(rule, //xls/modules/zstd/...))') + # - name: Build ZSTD place and route targets (opt) + # if: ${{ !cancelled() }} + # run: | + # bazel build -c opt -- $(bazel query 'filter(".*_place_and_route", kind(rule, //xls/modules/zstd/...))')

      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

    2. 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