diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index e7a72d1af..c515414c2 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -14,7 +14,7 @@ jobs: name: Synthesis benchmarks runs-on: ubuntu-latest timeout-minutes: 40 - container: ghcr.io/kuznia-rdzeni/amaranth-synth:ecp5 + container: ghcr.io/kuznia-rdzeni/amaranth-synth:ecp5-3.11 steps: - uses: actions/checkout@v3 @@ -23,19 +23,29 @@ jobs: # https://github.com/actions/runner/issues/2033 chown -R $(id -u):$(id -g) $PWD + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies run: | + python3 -m venv venv + . venv/bin/activate python3 -m pip install --upgrade pip - pip3 install -r requirements-dev.txt + python3 -m pip install -r requirements-dev.txt - name: Synthesize - run: PYTHONHASHSEED=0 ./scripts/synthesize.py --verbose --config ${{ matrix.config }} + run: | + . venv/bin/activate + PYTHONHASHSEED=0 ./scripts/synthesize.py --verbose --config ${{ matrix.config }} - name: Print synthesis information run: cat ./build/top.tim - name: Collect Benchmark information run: | + . venv/bin/activate ./scripts/parse_benchmark_info.py cat ./benchmark.json @@ -53,7 +63,7 @@ jobs: build-perf-benchmarks: name: Build performance benchmarks runs-on: ubuntu-latest - container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.05.14 + container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.10.08_v steps: - name: Checkout uses: actions/checkout@v3 @@ -73,7 +83,7 @@ jobs: name: Run performance benchmarks runs-on: ubuntu-latest timeout-minutes: 60 - container: ghcr.io/kuznia-rdzeni/verilator:v5.008 + container: ghcr.io/kuznia-rdzeni/verilator:v5.008-3.11 needs: build-perf-benchmarks steps: - name: Checkout @@ -84,13 +94,22 @@ jobs: # https://github.com/actions/runner/issues/2033 chown -R $(id -u):$(id -g) $PWD + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies run: | + python3 -m venv venv + . venv/bin/activate python3 -m pip install --upgrade pip - pip3 install -r requirements-dev.txt + python3 -m pip install -r requirements-dev.txt - name: Generate Verilog - run: PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full + run: | + . venv/bin/activate + PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full - uses: actions/download-artifact@v3 with: @@ -99,6 +118,7 @@ jobs: - name: Run benchmarks run: | + . venv/bin/activate scripts/run_benchmarks.py - name: Store benchmark result (IPC) diff --git a/.github/workflows/deploy_gh_pages.yml b/.github/workflows/deploy_gh_pages.yml index 210752f06..89fdeea7d 100644 --- a/.github/workflows/deploy_gh_pages.yml +++ b/.github/workflows/deploy_gh_pages.yml @@ -26,21 +26,23 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" - cache: "pip" - cache-dependency-path: | - requirements.txt - requirements-dev.txt + python-version: "3.11" - name: Install dependencies run: | + python3 -m venv venv + . venv/bin/activate python -m pip install --upgrade pip - pip3 install -r requirements-dev.txt + python -m pip install -r requirements-dev.txt - name: Build documentation - run: ./ci/build_docs.sh + run: | + . venv/bin/activate + ./ci/build_docs.sh - name: Push documentation # Deploy documentation only when on master if: github.ref == 'refs/heads/master' - run: ./ci/push_gh_pages.sh + run: | + . venv/bin/activate + ./ci/push_gh_pages.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d70ae121..957e2b1c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: build-regression-tests: name: Build regression tests runs-on: ubuntu-latest - container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.05.14 + container: ghcr.io/kuznia-rdzeni/riscv-toolchain:2023.10.08_v steps: - name: Checkout uses: actions/checkout@v3 @@ -35,19 +35,28 @@ jobs: name: Run regression tests runs-on: ubuntu-latest timeout-minutes: 10 - container: ghcr.io/kuznia-rdzeni/verilator:v5.008 + container: ghcr.io/kuznia-rdzeni/verilator:v5.008-3.11 needs: build-regression-tests steps: - name: Checkout uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies run: | + python3 -m venv venv + . venv/bin/activate python3 -m pip install --upgrade pip - pip3 install -r requirements-dev.txt + python3 -m pip install -r requirements-dev.txt - name: Generate Verilog - run: PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full + run: | + . venv/bin/activate + PYTHONHASHSEED=0 ./scripts/gen_verilog.py --verbose --config full - uses: actions/download-artifact@v3 with: @@ -56,6 +65,7 @@ jobs: - name: Run tests run: | + . venv/bin/activate scripts/run_tests.py -a regression - name: Test Report @@ -76,7 +86,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' cache: 'pip' cache-dependency-path: | requirements.txt @@ -105,7 +115,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' cache: 'pip' cache-dependency-path: | requirements.txt diff --git a/constants/ecp5_platforms.py b/constants/ecp5_platforms.py index 55ad94478..0e3545690 100644 --- a/constants/ecp5_platforms.py +++ b/constants/ecp5_platforms.py @@ -8,7 +8,7 @@ from constants.ecp5_pinout import ecp5_bg756_pins, ecp5_bg756_pclk from coreblocks.peripherals.wishbone import WishboneParameters -from coreblocks.transactions.lib import AdapterBase +from transactron.lib import AdapterBase __all__ = ["make_ecp5_platform"] @@ -55,11 +55,12 @@ def __init__(self, pins: Iterable[str]): def p(self, count: int = 1): return " ".join([self.pin_bag.pop() for _ in range(count)]) - def named_pin(self, names: list[str]): + def named_pin(self, names: Iterable[str]): for name in names: if name in self.pin_bag: self.pin_bag.remove(name) return name + raise RuntimeError("Named pins %s not free" % ", ".join(names)) ResourceBuilder: TypeAlias = Callable[[PinManager], list[Resource]] @@ -123,9 +124,10 @@ class ECP5BG756Platform(LatticeECP5Platform): default_clk = "clk" default_rst = "rst" + clk_pin = pins.named_pin(ecp5_bg756_pclk) resources = [ Resource("rst", 0, PinsN(pins.p(), dir="i"), Attrs(IO_TYPE="LVCMOS33")), - Resource("clk", 0, Pins(pins.named_pin(ecp5_bg756_pclk), dir="i"), Clock(12e6), Attrs(IO_TYPE="LVCMOS33")), + Resource("clk", 0, Pins(clk_pin, dir="i"), Clock(12e6), Attrs(IO_TYPE="LVCMOS33")), ] + resource_builder(pins) connectors = [] diff --git a/coreblocks/fu/div_unit.py b/coreblocks/fu/div_unit.py index f78feadf8..c874d439f 100644 --- a/coreblocks/fu/div_unit.py +++ b/coreblocks/fu/div_unit.py @@ -1,6 +1,6 @@ from dataclasses import KW_ONLY, dataclass from enum import IntFlag, auto -from typing import Sequence, Tuple +from collections.abc import Sequence from amaranth import * @@ -34,7 +34,7 @@ def get_instructions(self) -> Sequence[tuple]: ] -def get_input(arg: Record) -> Tuple[Value, Value]: +def get_input(arg: Record) -> tuple[Value, Value]: return arg.s1_val, Mux(arg.imm, arg.imm, arg.s2_val) diff --git a/coreblocks/fu/mul_unit.py b/coreblocks/fu/mul_unit.py index a7230c774..3d4888cfa 100644 --- a/coreblocks/fu/mul_unit.py +++ b/coreblocks/fu/mul_unit.py @@ -1,5 +1,5 @@ from enum import IntFlag, IntEnum, auto -from typing import Sequence, Tuple +from collections.abc import Sequence from dataclasses import KW_ONLY, dataclass from amaranth import * @@ -45,7 +45,7 @@ def get_instructions(self) -> Sequence[tuple]: ] -def get_input(arg: Record) -> Tuple[Value, Value]: +def get_input(arg: Record) -> tuple[Value, Value]: """ Operation of getting two input values. @@ -56,7 +56,7 @@ def get_input(arg: Record) -> Tuple[Value, Value]: Returns ------- - return : Tuple[Value, Value] + return : tuple[Value, Value] Two input values. """ return arg.s1_val, Mux(arg.imm, arg.imm, arg.s2_val) diff --git a/coreblocks/params/keys.py b/coreblocks/params/keys.py index 8cd97a9b7..ec11adb4f 100644 --- a/coreblocks/params/keys.py +++ b/coreblocks/params/keys.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING from coreblocks.params.dependencies import SimpleKey, UnifierKey -from transactron.lib import MethodTryProduct, Collector, Method +from transactron import Method +from transactron.lib import MethodTryProduct, Collector from coreblocks.peripherals.wishbone import WishboneMaster if TYPE_CHECKING: diff --git a/coreblocks/scheduler/scheduler.py b/coreblocks/scheduler/scheduler.py index b85e6d052..eab680fe2 100644 --- a/coreblocks/scheduler/scheduler.py +++ b/coreblocks/scheduler/scheduler.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from amaranth import * diff --git a/coreblocks/stages/func_blocks_unifier.py b/coreblocks/stages/func_blocks_unifier.py index 8bd97c423..8748bf5df 100644 --- a/coreblocks/stages/func_blocks_unifier.py +++ b/coreblocks/stages/func_blocks_unifier.py @@ -1,4 +1,4 @@ -from typing import Iterable +from collections.abc import Iterable from amaranth import * diff --git a/coreblocks/structs_common/rs.py b/coreblocks/structs_common/rs.py index 60bcdcce3..eb045c3a4 100644 --- a/coreblocks/structs_common/rs.py +++ b/coreblocks/structs_common/rs.py @@ -1,4 +1,5 @@ -from typing import Iterable, Optional +from collections.abc import Iterable +from typing import Optional from amaranth import * from amaranth.lib.coding import PriorityEncoder from transactron import Method, def_method, TModule diff --git a/coreblocks/utils/_typing.py b/coreblocks/utils/_typing.py index 124066124..5e27a229d 100644 --- a/coreblocks/utils/_typing.py +++ b/coreblocks/utils/_typing.py @@ -1,18 +1,14 @@ from typing import ( - ContextManager, Generic, NoReturn, Optional, Protocol, - Sequence, - Tuple, - Type, TypeAlias, - Iterable, - Mapping, TypeVar, runtime_checkable, ) +from collections.abc import Iterable, Mapping, Sequence +from contextlib import AbstractContextManager from enum import Enum from amaranth import * from amaranth.lib.data import View @@ -21,16 +17,18 @@ from amaranth.hdl.rec import Direction, Layout # Types representing Amaranth concepts -FragmentLike = Fragment | Elaboratable -ValueLike = Value | int | Enum | ValueCastable -ShapeLike = Shape | ShapeCastable | int | range | Type[Enum] +FragmentLike: TypeAlias = Fragment | Elaboratable +ValueLike: TypeAlias = Value | int | Enum | ValueCastable +ShapeLike: TypeAlias = Shape | ShapeCastable | int | range | type[Enum] StatementLike: TypeAlias = Statement | Iterable["StatementLike"] -LayoutLike = Layout | Sequence[Tuple[str, ShapeLike | "LayoutLike"] | Tuple[str, ShapeLike | "LayoutLike", Direction]] +LayoutLike: TypeAlias = ( + Layout | Sequence[tuple[str, "ShapeLike | LayoutLike"] | tuple[str, "ShapeLike | LayoutLike", Direction]] +) SwitchKey: TypeAlias = str | int | Enum # Internal Coreblocks types SignalBundle: TypeAlias = Signal | Record | View | Iterable["SignalBundle"] | Mapping[str, "SignalBundle"] -LayoutList = list[Tuple[str, ShapeLike | "LayoutList"]] +LayoutList: TypeAlias = list[tuple[str, "ShapeLike | LayoutList"]] class _ModuleBuilderDomainsLike(Protocol): @@ -55,28 +53,30 @@ class ModuleLike(Protocol, Generic[_T_ModuleBuilderDomains]): domains: _ModuleBuilderDomainSet d: _T_ModuleBuilderDomains - def If(self, cond: ValueLike) -> ContextManager[None]: # noqa: N802 + def If(self, cond: ValueLike) -> AbstractContextManager[None]: # noqa: N802 ... - def Elif(self, cond: ValueLike) -> ContextManager[None]: # noqa: N802 + def Elif(self, cond: ValueLike) -> AbstractContextManager[None]: # noqa: N802 ... - def Else(self) -> ContextManager[None]: # noqa: N802 + def Else(self) -> AbstractContextManager[None]: # noqa: N802 ... - def Switch(self, test: ValueLike) -> ContextManager[None]: # noqa: N802 + def Switch(self, test: ValueLike) -> AbstractContextManager[None]: # noqa: N802 ... - def Case(self, *patterns: SwitchKey) -> ContextManager[None]: # noqa: N802 + def Case(self, *patterns: SwitchKey) -> AbstractContextManager[None]: # noqa: N802 ... - def Default(self) -> ContextManager[None]: # noqa: N802 + def Default(self) -> AbstractContextManager[None]: # noqa: N802 ... - def FSM(self, reset: Optional[str] = ..., domain: str = ..., name: str = ...) -> ContextManager[FSM]: # noqa: N802 + def FSM( # noqa: N802 + self, reset: Optional[str] = ..., domain: str = ..., name: str = ... + ) -> AbstractContextManager[FSM]: ... - def State(self, name: str) -> ContextManager[None]: # noqa: N802 + def State(self, name: str) -> AbstractContextManager[None]: # noqa: N802 ... @property diff --git a/coreblocks/utils/utils.py b/coreblocks/utils/utils.py index 11a0df1b1..31fc830ab 100644 --- a/coreblocks/utils/utils.py +++ b/coreblocks/utils/utils.py @@ -1,6 +1,7 @@ from contextlib import contextmanager from enum import Enum -from typing import Iterable, Literal, Mapping, Optional, TypeAlias, cast, overload +from typing import Literal, Optional, TypeAlias, cast, overload +from collections.abc import Iterable, Mapping from amaranth import * from amaranth.hdl.ast import Assign, ArrayProxy from amaranth.lib import data diff --git a/docker/AmaranthSynthECP5.Dockerfile b/docker/AmaranthSynthECP5.Dockerfile index 0e9809b8c..3b9326ccf 100644 --- a/docker/AmaranthSynthECP5.Dockerfile +++ b/docker/AmaranthSynthECP5.Dockerfile @@ -1,9 +1,9 @@ -FROM ubuntu:22.10 +FROM ubuntu:23.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ - python3.10 python3-pip git yosys \ + python3.11 python3-pip git yosys lsb-release \ build-essential cmake python3-dev libboost-all-dev libeigen3-dev && \ rm -rf /var/lib/apt/lists/* diff --git a/docker/Verilator.Dockerfile b/docker/Verilator.Dockerfile index ddb5925f7..64c60c5e4 100644 --- a/docker/Verilator.Dockerfile +++ b/docker/Verilator.Dockerfile @@ -1,9 +1,9 @@ -FROM ubuntu:22.10 +FROM ubuntu:23.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ - python3.10 libpython3.10 python3-pip git \ + python3.11 libpython3.11 python3-pip git lsb-release \ perl perl-doc help2man make autoconf g++ flex bison ccache numactl \ libgoogle-perftools-dev libfl-dev zlib1g-dev && \ rm -rf /var/lib/apt/lists/* diff --git a/docker/riscv-toolchain.Dockerfile b/docker/riscv-toolchain.Dockerfile index ec33ce01b..d35c604b9 100644 --- a/docker/riscv-toolchain.Dockerfile +++ b/docker/riscv-toolchain.Dockerfile @@ -1,9 +1,9 @@ -FROM ubuntu:22.10 +FROM ubuntu:23.04 RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive \ apt-get install -y --no-install-recommends \ - autoconf automake autotools-dev curl python3 bc \ + autoconf automake autotools-dev curl python3 bc lsb-release \ libmpc-dev libmpfr-dev libgmp-dev gawk build-essential \ bison flex texinfo gperf libtool patchutils zlib1g-dev \ libexpat-dev ninja-build git ca-certificates python-is-python3 && \ diff --git a/docs/Development_environment.md b/docs/Development_environment.md index 8f471cefa..cfdb58cad 100644 --- a/docs/Development_environment.md +++ b/docs/Development_environment.md @@ -4,7 +4,7 @@ In order to prepare the development environment, please follow the steps below: -1. Install the Python 3.10 interpreter and pip package manager. +1. Install the Python 3.11 interpreter and pip package manager. * Optionally create a Python virtual environment with `python3 -m venv venv` in the project directory and activate it using generated script: `. venv/bin/activate`. 2. Install all required libraries with `pip3 install -r requirements-dev.txt`. 3. Install `riscv64-unknown-elf` binutils using your favourite package manager. On Debian-based distros the package is called `binutils-riscv64-unknown-elf`, on Arch-based - `riscv64-unknown-elf-binutils`. diff --git a/docs/api.md b/docs/api.md index ab2def980..5daa246b7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,5 +1,6 @@ # API ```{eval-rst} -.. include:: modules.rst -``` \ No newline at end of file +.. include:: modules-coreblocks.rst +.. include:: modules-transactron.rst +``` diff --git a/pyrightconfig.json b/pyrightconfig.json index dd4c65a88..aadb361ef 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -2,6 +2,8 @@ "include": [ "coreblocks", "test", + "constants", + "transactron", "scripts" ], diff --git a/requirements-dev.txt b/requirements-dev.txt index 766fd7c8e..35c93666f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ myst-parser==0.18.0 numpydoc==1.5.0 parameterized==0.8.1 pre-commit==2.16.0 -pyright==1.1.308 +pyright==1.1.332 Sphinx==5.1.1 sphinx-rtd-theme==1.0.0 sphinxcontrib-mermaid==0.8.1 diff --git a/requirements.txt b/requirements.txt index 1bd4e5cc4..278864342 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -amaranth-yosys==0.10.0.dev46 +amaranth-yosys==0.25.0.0.post77 git+https://github.com/amaranth-lang/amaranth@ccf7aaf00db54c7647b2f0f0cfdf34835c16fa8f diff --git a/scripts/build_docs.sh b/scripts/build_docs.sh index 74cd6d1f0..6f58a5a6b 100755 --- a/scripts/build_docs.sh +++ b/scripts/build_docs.sh @@ -59,5 +59,6 @@ fi $ROOT_PATH/scripts/core_graph.py -p -f mermaid $DOCS_DIR/auto_graph.rst sed -i -e '1i\.. mermaid::\n' -e 's/^/ /' $DOCS_DIR/auto_graph.rst -sphinx-apidoc -o $DOCS_DIR $ROOT_PATH/coreblocks/ +sphinx-apidoc --tocfile modules-coreblocks -o $DOCS_DIR $ROOT_PATH/coreblocks/ +sphinx-apidoc --tocfile modules-transactron -o $DOCS_DIR $ROOT_PATH/transactron/ sphinx-build -b html -W $DOCS_DIR $BUILD_DIR diff --git a/test/common.py b/test/common.py index e21190e98..76ede88f4 100644 --- a/test/common.py +++ b/test/common.py @@ -12,7 +12,7 @@ from transactron.core import SignalBundle, Method, TransactionModule from transactron.lib import AdapterBase, AdapterTrans -from transactron._utils import method_def_helper +from transactron._utils import mock_def_helper from coreblocks.utils import ValueLike, HasElaborate, HasDebugSignals, auto_debug_signals, LayoutLike, ModuleConnector from .gtkw_extension import write_vcd_ext @@ -157,7 +157,7 @@ def transform_methods_to_testbenchios( m.submodules.dut = self._dut - for name, attr in [(name, getattr(self._dut, name)) for name in dir(self._dut)]: + for name, attr in vars(self._dut).items(): if guard_nested_collection(attr, Method) and attr: tb_cont, mc = transform_methods_to_testbenchios(attr) self._io[name] = tb_cont @@ -166,7 +166,10 @@ def transform_methods_to_testbenchios( return m def debug_signals(self): - return [auto_debug_signals(io) for io in self._io.values()] + sigs = {"_dut": auto_debug_signals(self._dut)} + for name, io in self._io.items(): + sigs[name] = auto_debug_signals(io) + return sigs class TestModule(Elaboratable): @@ -360,7 +363,7 @@ def method_handle( for _ in range(extra_settle_count + 1): yield Settle() - ret_out = method_def_helper(self, function, **arg) + ret_out = mock_def_helper(self, function, arg) yield from self.method_return(ret_out or {}) yield diff --git a/test/regression/cocotb.py b/test/regression/cocotb.py index 9ca00c905..e59bcef03 100644 --- a/test/regression/cocotb.py +++ b/test/regression/cocotb.py @@ -73,7 +73,7 @@ async def start(self): while True: while not (self.bus.stb.value and self.bus.cyc.value): - await clock_edge_event + await clock_edge_event # type: ignore sig_m = WishboneMasterSignals() self.bus.sample(sig_m) @@ -124,10 +124,10 @@ async def start(self): ) for _ in range(self.delay): - await clock_edge_event + await clock_edge_event # type: ignore self.bus.drive(sig_s) - await clock_edge_event + await clock_edge_event # type: ignore self.bus.drive(WishboneSlaveSignals()) @@ -147,7 +147,7 @@ async def run(self, mem_model: CoreMemoryModel, timeout_cycles: int = 5000) -> b instr_wb = WishboneSlave(self.dut, "wb_instr", self.dut.clk, mem_model, is_instr_bus=True) cocotb.start_soon(instr_wb.start()) - data_wb = WishboneSlave(self.dut, "wb_data", self.dut.clk, mem_model, is_instr_bus=True) + data_wb = WishboneSlave(self.dut, "wb_data", self.dut.clk, mem_model, is_instr_bus=False) cocotb.start_soon(data_wb.start()) res = await with_timeout(self.finish_event.wait(), timeout_cycles, "ns") diff --git a/test/regression/memory.py b/test/regression/memory.py index c15b636b4..534cd0929 100644 --- a/test/regression/memory.py +++ b/test/regression/memory.py @@ -137,7 +137,7 @@ def write(self, req: WriteRequest) -> WriteReply: return WriteReply(status=ReplyStatus.ERROR) -def load_segment(segment: Segment) -> RandomAccessMemory: +def load_segment(segment: Segment, *, disable_write_protection: bool = False) -> RandomAccessMemory: paddr = segment.header["p_paddr"] memsz = segment.header["p_memsz"] flags_raw = segment.header["p_flags"] @@ -150,7 +150,7 @@ def load_segment(segment: Segment) -> RandomAccessMemory: flags = SegmentFlags(0) if flags_raw & P_FLAGS.PF_R: flags |= SegmentFlags.READ - if flags_raw & P_FLAGS.PF_W: + if flags_raw & P_FLAGS.PF_W or disable_write_protection: flags |= SegmentFlags.WRITE if flags_raw & P_FLAGS.PF_X: flags |= SegmentFlags.EXECUTABLE @@ -162,7 +162,7 @@ def load_segment(segment: Segment) -> RandomAccessMemory: return RandomAccessMemory(range(seg_start, seg_end), flags, data) -def load_segments_from_elf(file_path: str) -> list[RandomAccessMemory]: +def load_segments_from_elf(file_path: str, *, disable_write_protection: bool = False) -> list[RandomAccessMemory]: segments: list[RandomAccessMemory] = [] with open(file_path, "rb") as f: @@ -170,6 +170,6 @@ def load_segments_from_elf(file_path: str) -> list[RandomAccessMemory]: for segment in elffile.iter_segments(): if segment.header["p_type"] != "PT_LOAD": continue - segments.append(load_segment(segment)) + segments.append(load_segment(segment, disable_write_protection=disable_write_protection)) return segments diff --git a/test/regression/test.py b/test/regression/test.py index fa023f90f..cbe8067cd 100644 --- a/test/regression/test.py +++ b/test/regression/test.py @@ -7,6 +7,9 @@ test_dir = Path(__file__).parent.parent riscv_tests_dir = test_dir.joinpath("external/riscv-tests") +# disable write protection for specific tests with writes to .text section +exclude_write_protection = ["rv32uc-rvc"] + class MMIO(MemorySegment): def __init__(self, on_finish: Callable[[], None]): @@ -31,7 +34,10 @@ async def run_test(sim_backend: SimulationBackend, test_name: str): mmio = MMIO(lambda: sim_backend.stop()) mem_segments: list[MemorySegment] = [] - mem_segments += load_segments_from_elf(str(riscv_tests_dir.joinpath("test-" + test_name))) + mem_segments += load_segments_from_elf( + str(riscv_tests_dir.joinpath("test-" + test_name)), + disable_write_protection=test_name in exclude_write_protection, + ) mem_segments.append(mmio) mem_model = CoreMemoryModel(mem_segments) diff --git a/test/test_core.py b/test/test_core.py index 8a443dbda..aa03c64e3 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -263,7 +263,7 @@ def test_asm_source(self): self.base_dir = "test/asm/" self.bin_src = [] - with tempfile.NamedTemporaryFile() as asm_tmp: + with tempfile.NamedTemporaryFile() as asm_tmp, tempfile.NamedTemporaryFile() as bin_tmp: subprocess.check_call( [ "riscv64-unknown-elf-as", @@ -276,9 +276,10 @@ def test_asm_source(self): self.base_dir + self.source_file, ] ) - code = subprocess.check_output( - ["riscv64-unknown-elf-objcopy", "-O", "binary", "-j", ".text", asm_tmp.name, "/dev/stdout"] + subprocess.check_call( + ["riscv64-unknown-elf-objcopy", "-O", "binary", "-j", ".text", asm_tmp.name, bin_tmp.name] ) + code = bin_tmp.read() for word_idx in range(0, len(code), 4): word = code[word_idx : word_idx + 4] bin_instr = int.from_bytes(word, "little") diff --git a/transactron/_utils.py b/transactron/_utils.py index 0df18db2e..138c7222b 100644 --- a/transactron/_utils.py +++ b/transactron/_utils.py @@ -1,7 +1,8 @@ import itertools import sys from inspect import Parameter, signature -from typing import Callable, Iterable, Optional, TypeAlias, TypeVar, Mapping +from typing import Any, Concatenate, Optional, TypeAlias, TypeGuard, TypeVar +from collections.abc import Callable, Iterable, Mapping from amaranth import * from coreblocks.utils._typing import LayoutLike from coreblocks.utils import OneHotSwitchDynamic @@ -14,11 +15,13 @@ "Graph", "GraphCC", "get_caller_class_name", + "def_helper", "method_def_helper", ] T = TypeVar("T") +U = TypeVar("U") class Scheduler(Elaboratable): @@ -121,24 +124,35 @@ def _graph_ccs(gr: ROGraph[T]) -> list[GraphCC[T]]: MethodLayout: TypeAlias = LayoutLike -def method_def_helper(method, func: Callable[..., T], arg=None, /, **kwargs) -> T: +def has_first_param(func: Callable[..., T], name: str, tp: type[U]) -> TypeGuard[Callable[Concatenate[U, ...], T]]: + parameters = signature(func).parameters + return ( + len(parameters) >= 1 + and next(iter(parameters)) == name + and parameters[name].kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY} + and parameters[name].annotation in {Parameter.empty, tp} + ) + + +def def_helper(description, func: Callable[..., T], tp: type[U], arg: U, /, **kwargs) -> T: parameters = signature(func).parameters kw_parameters = set( n for n, p in parameters.items() if p.kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY} ) - if ( - len(parameters) == 1 - and "arg" in parameters - and parameters["arg"].kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY} - and parameters["arg"].annotation in {Parameter.empty, Record} - ): - if arg is None: - arg = kwargs + if len(parameters) == 1 and has_first_param(func, "arg", tp): return func(arg) elif kw_parameters <= kwargs.keys(): return func(**kwargs) else: - raise TypeError(f"Invalid method definition/mock for {method}: {func}") + raise TypeError(f"Invalid {description}: {func}") + + +def mock_def_helper(tb, func: Callable[..., T], arg: Mapping[str, Any]) -> T: + return def_helper(f"mock definition for {tb}", func, Mapping[str, Any], arg, **arg) + + +def method_def_helper(method, func: Callable[..., T], arg: Record) -> T: + return def_helper(f"method definition for {method}", func, Record, arg, **arg.fields) def get_caller_class_name(default: Optional[str] = None) -> tuple[Optional[Elaboratable], str]: diff --git a/transactron/core.py b/transactron/core.py index c4c34f8cc..035b3b588 100644 --- a/transactron/core.py +++ b/transactron/core.py @@ -2,7 +2,18 @@ from collections.abc import Sequence, Iterable, Callable, Mapping, Iterator from contextlib import contextmanager from enum import Enum, auto -from typing import ClassVar, NoReturn, TypeAlias, TypedDict, Union, Optional, Tuple +from typing import ( + ClassVar, + NoReturn, + TypeAlias, + TypedDict, + Union, + Optional, + Tuple, + TypeVar, + Protocol, + runtime_checkable, +) from graphlib import TopologicalSorter from typing_extensions import Self from amaranth import * @@ -38,6 +49,7 @@ TransactionScheduler: TypeAlias = Callable[["MethodMap", TransactionGraph, TransactionGraphCC, PriorityOrder], Module] RecordDict: TypeAlias = ValueLike | Mapping[str, "RecordDict"] TransactionOrMethod: TypeAlias = Union["Transaction", "Method"] +TransactionOrMethodBound = TypeVar("TransactionOrMethodBound", "Transaction", "Method") class Priority(Enum): @@ -670,15 +682,20 @@ def elaborate(self, platform): return self.main_module -class TransactionBase(Owned): +@runtime_checkable +class TransactionBase(Owned, Protocol): stack: ClassVar[list[Union["Transaction", "Method"]]] = [] def_counter: ClassVar[count] = count() def_order: int defined: bool = False name: str + method_uses: dict["Method", Tuple[ValueLike, ValueLike]] + relations: list[RelationBase] + simultaneous_list: list[TransactionOrMethod] + independent_list: list[TransactionOrMethod] def __init__(self): - self.method_uses: dict[Method, Tuple[ValueLike, ValueLike]] = dict() + self.method_uses: dict["Method", Tuple[ValueLike, ValueLike]] = dict() self.relations: list[RelationBase] = [] self.simultaneous_list: list[TransactionOrMethod] = [] self.independent_list: list[TransactionOrMethod] = [] @@ -769,9 +786,7 @@ def _independent(self, *others: TransactionOrMethod) -> None: self.independent_list += others @contextmanager - def context(self, m: TModule) -> Iterator[Self]: - assert isinstance(self, Transaction) or isinstance(self, Method) # for typing - + def context(self: TransactionOrMethodBound, m: TModule) -> Iterator[TransactionOrMethodBound]: parent = TransactionBase.peek() if parent is not None: parent.schedule_before(self) @@ -1208,7 +1223,7 @@ def decorator(func: Callable[..., Optional[RecordDict]]): ret_out = None with method.body(m, ready=ready, out=out) as arg: - ret_out = method_def_helper(method, func, arg, **arg.fields) + ret_out = method_def_helper(method, func, arg) if ret_out is not None: m.d.top_comb += assign(out, ret_out, fields=AssignType.ALL) diff --git a/transactron/graph.py b/transactron/graph.py index 2deaf24a0..4cd51d067 100644 --- a/transactron/graph.py +++ b/transactron/graph.py @@ -3,15 +3,14 @@ """ from enum import IntFlag -from abc import ABC from collections import defaultdict -from typing import Literal, Optional +from typing import Literal, Optional, Protocol from amaranth.hdl.ir import Elaboratable, Fragment from .tracing import TracingFragment -class Owned(ABC): +class Owned(Protocol): name: str owner: Optional[Elaboratable] diff --git a/transactron/lib.py b/transactron/lib.py deleted file mode 100644 index 963561a7f..000000000 --- a/transactron/lib.py +++ /dev/null @@ -1,1187 +0,0 @@ -from contextlib import contextmanager -from typing import Callable, Tuple, Optional -from amaranth import * -from amaranth.utils import * -from .core import * -from .core import SignalBundle, RecordDict, TransactionBase -from ._utils import MethodLayout -from coreblocks.utils import ValueLike, assign, AssignType -from coreblocks.utils.fifo import BasicFifo - -__all__ = [ - "FIFO", - "MemoryBank", - "Forwarder", - "Connect", - "Collector", - "ClickIn", - "ClickOut", - "AdapterTrans", - "Adapter", - "ConnectTrans", - "ConnectAndTransformTrans", - "CatTrans", - "ManyToOneConnectTrans", - "MethodTransformer", - "MethodFilter", - "MethodProduct", - "Serializer", - "MethodTryProduct", - "condition", -] - -# FIFOs - -import amaranth.lib.fifo - - -class FIFO(Elaboratable): - """FIFO module. - - Provides a transactional interface to Amaranth FIFOs. Exposes two methods: - `read`, and `write`. Both methods are ready only when they can - be executed -- i.e. the queue is respectively not empty / not full. - It is possible to simultaneously read and write in a single clock cycle, - but only if both readiness conditions are fulfilled. - - Attributes - ---------- - read: Method - The read method. Accepts an empty argument, returns a `Record`. - write: Method - The write method. Accepts a `Record`, returns empty result. - """ - - def __init__(self, layout: MethodLayout, depth: int, fifo_type=amaranth.lib.fifo.SyncFIFO): - """ - Parameters - ---------- - layout: record layout - The format of records stored in the FIFO. - depth: int - Size of the FIFO. - fifoType: Elaboratable - FIFO module conforming to Amaranth library FIFO interface. Defaults - to SyncFIFO. - """ - self.width = len(Record(layout)) - self.depth = depth - self.fifoType = fifo_type - - self.read = Method(o=layout) - self.write = Method(i=layout) - - def elaborate(self, platform): - m = TModule() - - m.submodules.fifo = fifo = self.fifoType(width=self.width, depth=self.depth) - - assert fifo.fwft # the read method requires FWFT behavior - - @def_method(m, self.write, ready=fifo.w_rdy) - def _(arg): - m.d.comb += fifo.w_en.eq(1) - m.d.top_comb += fifo.w_data.eq(arg) - - @def_method(m, self.read, ready=fifo.r_rdy) - def _(): - m.d.comb += fifo.r_en.eq(1) - return fifo.r_data - - return m - - -# Forwarding with overflow buffering - - -class Forwarder(Elaboratable): - """Forwarding with overflow buffering - - Provides a means to connect two transactions with forwarding. Exposes - two methods: `read`, and `write`. When both of these methods are - executed simultaneously, data is forwarded between them. If `write` - is executed, but `read` is not, the value cannot be forwarded, - but is stored into an overflow buffer. No further `write`\\s are - possible until the overflow buffer is cleared by `read`. - - The `write` method is scheduled before `read`. - - Attributes - ---------- - read: Method - The read method. Accepts an empty argument, returns a `Record`. - write: Method - The write method. Accepts a `Record`, returns empty result. - """ - - def __init__(self, layout: MethodLayout): - """ - Parameters - ---------- - layout: record layout - The format of records forwarded. - """ - self.read = Method(o=layout) - self.write = Method(i=layout) - self.clear = Method() - self.head = Record.like(self.read.data_out) - - self.clear.add_conflict(self.read, Priority.LEFT) - self.clear.add_conflict(self.write, Priority.LEFT) - - def elaborate(self, platform): - m = TModule() - - reg = Record.like(self.read.data_out) - reg_valid = Signal() - read_value = Record.like(self.read.data_out) - m.d.comb += self.head.eq(read_value) - - self.write.schedule_before(self.read) # to avoid combinational loops - - @def_method(m, self.write, ready=~reg_valid) - def _(arg): - m.d.av_comb += read_value.eq(arg) # for forwarding - m.d.sync += reg.eq(arg) - m.d.sync += reg_valid.eq(1) - - with m.If(reg_valid): - m.d.av_comb += read_value.eq(reg) # write method is not ready - - @def_method(m, self.read, ready=reg_valid | self.write.run) - def _(): - m.d.sync += reg_valid.eq(0) - return read_value - - @def_method(m, self.clear) - def _(): - m.d.sync += reg_valid.eq(0) - - return m - - -class Connect(Elaboratable): - """Forwarding by transaction simultaneity - - Provides a means to connect two transactions with forwarding - by means of the transaction simultaneity mechanism. It provides - two methods: `read`, and `write`, which always execute simultaneously. - Typical use case is for moving data from `write` to `read`, but - data flow in the reverse direction is also possible. - - Attributes - ---------- - read: Method - The read method. Accepts a (possibly empty) `Record`, returns - a `Record`. - write: Method - The write method. Accepts a `Record`, returns a (possibly empty) - `Record`. - """ - - def __init__(self, layout: MethodLayout = (), rev_layout: MethodLayout = ()): - """ - Parameters - ---------- - layout: record layout - The format of records forwarded. - rev_layout: record layout - The format of records forwarded in the reverse direction. - """ - self.read = Method(o=layout, i=rev_layout) - self.write = Method(i=layout, o=rev_layout) - - def elaborate(self, platform): - m = TModule() - - read_value = Record.like(self.read.data_out) - rev_read_value = Record.like(self.write.data_out) - - self.write.simultaneous(self.read) - - @def_method(m, self.write) - def _(arg): - m.d.av_comb += read_value.eq(arg) - return rev_read_value - - @def_method(m, self.read) - def _(arg): - m.d.av_comb += rev_read_value.eq(arg) - return read_value - - return m - - -# "Clicked" input - - -class ClickIn(Elaboratable): - """Clicked input. - - Useful for interactive simulations or FPGA button/switch interfaces. - On a rising edge (tested synchronously) of `btn`, the `get` method - is enabled, which returns the data present on `dat` at the time. - Inputs are synchronized. - - Attributes - ---------- - get: Method - The method for retrieving data from the input. Accepts an empty - argument, returns a `Record`. - btn: Signal, in - The button input. - dat: Record, in - The data input. - """ - - def __init__(self, layout: MethodLayout): - """ - Parameters - ---------- - layout: record layout - The data format for the input. - """ - self.get = Method(o=layout) - self.btn = Signal() - self.dat = Record(layout) - - def elaborate(self, platform): - m = TModule() - - btn1 = Signal() - btn2 = Signal() - dat1 = Signal.like(self.dat) - m.d.sync += btn1.eq(self.btn) - m.d.sync += btn2.eq(btn1) - m.d.sync += dat1.eq(self.dat) - get_ready = Signal() - get_data = Signal.like(self.dat) - - @def_method(m, self.get, ready=get_ready) - def _(): - m.d.sync += get_ready.eq(0) - return get_data - - with m.If(~btn2 & btn1): - m.d.sync += get_ready.eq(1) - m.d.sync += get_data.eq(dat1) - - return m - - -# "Clicked" output - - -class ClickOut(Elaboratable): - """Clicked output. - - Useful for interactive simulations or FPGA button/LED interfaces. - On a rising edge (tested synchronously) of `btn`, the `put` method - is enabled, which, when called, changes the value of the `dat` signal. - - Attributes - ---------- - put: Method - The method for retrieving data from the input. Accepts a `Record`, - returns empty result. - btn: Signal, in - The button input. - dat: Record, out - The data output. - """ - - def __init__(self, layout: MethodLayout): - """ - Parameters - ---------- - layout: record layout - The data format for the output. - """ - self.put = Method(i=layout) - self.btn = Signal() - self.dat = Record(layout) - - def elaborate(self, platform): - m = TModule() - - btn1 = Signal() - btn2 = Signal() - m.d.sync += btn1.eq(self.btn) - m.d.sync += btn2.eq(btn1) - - @def_method(m, self.put, ready=~btn2 & btn1) - def _(arg): - m.d.sync += self.dat.eq(arg) - - return m - - -# Testbench-friendly input/output - - -class AdapterBase(Elaboratable): - data_in: Record - data_out: Record - - def __init__(self, iface: Method): - self.iface = iface - self.en = Signal() - self.done = Signal() - - def debug_signals(self) -> SignalBundle: - return [self.en, self.done, self.data_in, self.data_out] - - -class AdapterTrans(AdapterBase): - """Adapter transaction. - - Creates a transaction controlled by plain Amaranth signals. Allows to - expose a method to plain Amaranth code, including testbenches. - - Attributes - ---------- - en: Signal, in - Activates the transaction (sets the `request` signal). - done: Signal, out - Signals that the transaction is performed (returns the `grant` - signal). - data_in: Record, in - Data passed to the `iface` method. - data_out: Record, out - Data returned from the `iface` method. - """ - - def __init__(self, iface: Method): - """ - Parameters - ---------- - iface: Method - The method to be called by the transaction. - """ - super().__init__(iface) - self.data_in = Record.like(iface.data_in) - self.data_out = Record.like(iface.data_out) - - def elaborate(self, platform): - m = TModule() - - # this forces data_in signal to appear in VCD dumps - data_in = Signal.like(self.data_in) - m.d.comb += data_in.eq(self.data_in) - - with Transaction(name=f"AdapterTrans_{self.iface.name}").body(m, request=self.en): - data_out = self.iface(m, data_in) - m.d.top_comb += self.data_out.eq(data_out) - m.d.comb += self.done.eq(1) - - return m - - -class Adapter(AdapterBase): - """Adapter method. - - Creates a method controlled by plain Amaranth signals. One of the - possible uses is to mock a method in a testbench. - - Attributes - ---------- - en: Signal, in - Activates the method (sets the `ready` signal). - done: Signal, out - Signals that the method is called (returns the `run` signal). - data_in: Record, in - Data returned from the defined method. - data_out: Record, out - Data passed as argument to the defined method. - """ - - def __init__(self, *, name: Optional[str] = None, i: MethodLayout = (), o: MethodLayout = ()): - """ - Parameters - ---------- - i: record layout - The input layout of the defined method. - o: record layout - The output layout of the defined method. - """ - super().__init__(Method(name=name, i=i, o=o)) - self.data_in = Record.like(self.iface.data_out) - self.data_out = Record.like(self.iface.data_in) - - def elaborate(self, platform): - m = TModule() - - # this forces data_in signal to appear in VCD dumps - data_in = Signal.like(self.data_in) - m.d.comb += data_in.eq(self.data_in) - - @def_method(m, self.iface, ready=self.en) - def _(arg): - m.d.top_comb += self.data_out.eq(arg) - m.d.comb += self.done.eq(1) - return data_in - - return m - - -# Method combinators - - -class MethodTransformer(Elaboratable): - """Method transformer. - - Takes a target method and creates a transformed method which calls the - original target method, transforming the input and output values. - The transformation functions take two parameters, a `Module` and the - `Record` being transformed. Alternatively, a `Method` can be - passed. - - Attributes - ---------- - method: Method - The transformed method. - """ - - def __init__( - self, - target: Method, - *, - i_transform: Optional[Tuple[MethodLayout, Callable[[TModule, Record], RecordDict]]] = None, - o_transform: Optional[Tuple[MethodLayout, Callable[[TModule, Record], RecordDict]]] = None, - ): - """ - Parameters - ---------- - target: Method - The target method. - i_transform: (record layout, function or Method), optional - Input transformation. If specified, it should be a pair of a - function and a input layout for the transformed method. - If not present, input is not transformed. - o_transform: (record layout, function or Method), optional - Output transformation. If specified, it should be a pair of a - function and a output layout for the transformed method. - If not present, output is not transformed. - """ - if i_transform is None: - i_transform = (target.data_in.layout, lambda _, x: x) - if o_transform is None: - o_transform = (target.data_out.layout, lambda _, x: x) - - self.target = target - self.method = Method(i=i_transform[0], o=o_transform[0]) - self.i_fun = i_transform[1] - self.o_fun = o_transform[1] - - def elaborate(self, platform): - m = TModule() - - @def_method(m, self.method) - def _(arg): - return self.o_fun(m, self.target(m, self.i_fun(m, arg))) - - return m - - -class MethodFilter(Elaboratable): - """Method filter. - - Takes a target method and creates a method which calls the target method - only when some condition is true. The condition function takes two - parameters, a module and the input `Record` of the method. Non-zero - return value is interpreted as true. Alternatively to using a function, - a `Method` can be passed as a condition. - - Caveat: because of the limitations of transaction scheduling, the target - method is locked for usage even if it is not called. - - Attributes - ---------- - method: Method - The transformed method. - """ - - def __init__( - self, target: Method, condition: Callable[[TModule, Record], ValueLike], default: Optional[RecordDict] = None - ): - """ - Parameters - ---------- - target: Method - The target method. - condition: function or Method - The condition which, when true, allows the call to `target`. When - false, `default` is returned. - default: Value or dict, optional - The default value returned from the filtered method when the condition - is false. If omitted, zero is returned. - """ - if default is None: - default = Record.like(target.data_out) - - self.target = target - self.method = Method.like(target) - self.condition = condition - self.default = default - - def elaborate(self, platform): - m = TModule() - - ret = Record.like(self.target.data_out) - m.d.comb += assign(ret, self.default, fields=AssignType.ALL) - - @def_method(m, self.method) - def _(arg): - with m.If(self.condition(m, arg)): - m.d.comb += ret.eq(self.target(m, arg)) - return ret - - return m - - -class MethodProduct(Elaboratable): - def __init__( - self, - targets: list[Method], - combiner: Optional[Tuple[MethodLayout, Callable[[TModule, list[Record]], RecordDict]]] = None, - ): - """Method product. - - Takes arbitrary, non-zero number of target methods, and constructs - a method which calls all of the target methods using the same - argument. The return value of the resulting method is, by default, - the return value of the first of the target methods. A combiner - function can be passed, which can compute the return value from - the results of every target method. - - Parameters - ---------- - targets: list[Method] - A list of methods to be called. - combiner: (int or method layout, function), optional - A pair of the output layout and the combiner function. The - combiner function takes two parameters: a `Module` and - a list of outputs of the target methods. - - Attributes - ---------- - method: Method - The product method. - """ - if combiner is None: - combiner = (targets[0].data_out.layout, lambda _, x: x[0]) - self.targets = targets - self.combiner = combiner - self.method = Method(i=targets[0].data_in.layout, o=combiner[0]) - - def elaborate(self, platform): - m = TModule() - - @def_method(m, self.method) - def _(arg): - results = [] - for target in self.targets: - results.append(target(m, arg)) - return self.combiner[1](m, results) - - return m - - -class MethodTryProduct(Elaboratable): - def __init__( - self, - targets: list[Method], - combiner: Optional[tuple[MethodLayout, Callable[[TModule, list[tuple[Value, Record]]], RecordDict]]] = None, - ): - """Method product with optional calling. - - Takes arbitrary, non-zero number of target methods, and constructs - a method which tries to call all of the target methods using the same - argument. The methods which are not ready are not called. The return - value of the resulting method is, by default, empty. A combiner - function can be passed, which can compute the return value from the - results of every target method. - - Parameters - ---------- - targets: list[Method] - A list of methods to be called. - combiner: (int or method layout, function), optional - A pair of the output layout and the combiner function. The - combiner function takes two parameters: a `Module` and - a list of pairs. Each pair contains a bit which signals - that a given call succeeded, and the result of the call. - - Attributes - ---------- - method: Method - The product method. - """ - if combiner is None: - combiner = ([], lambda _, __: {}) - self.targets = targets - self.combiner = combiner - self.method = Method(i=targets[0].data_in.layout, o=combiner[0]) - - def elaborate(self, platform): - m = TModule() - - @def_method(m, self.method) - def _(arg): - results: list[tuple[Value, Record]] = [] - for target in self.targets: - success = Signal() - with Transaction().body(m): - m.d.comb += success.eq(1) - results.append((success, target(m, arg))) - return self.combiner[1](m, results) - - return m - - -class Collector(Elaboratable): - """Single result collector. - - Creates method that collects results of many methods with identical - layouts. Each call of this method will return a single result of one - of the provided methods. - - Attributes - ---------- - method: Method - Method which returns single result of provided methods. - """ - - def __init__(self, targets: list[Method]): - """ - Parameters - ---------- - method_list: list[Method] - List of methods from which results will be collected. - """ - self.method_list = targets - layout = targets[0].data_out.layout - self.method = Method(o=layout) - - for method in targets: - if layout != method.data_out.layout: - raise Exception("Not all methods have this same layout") - - def elaborate(self, platform): - m = TModule() - - m.submodules.forwarder = forwarder = Forwarder(self.method.data_out.layout) - - m.submodules.connect = ManyToOneConnectTrans( - get_results=[get for get in self.method_list], put_result=forwarder.write - ) - - self.method.proxy(m, forwarder.read) - - return m - - -# Example transactions - - -class ConnectTrans(Elaboratable): - """Simple connecting transaction. - - Takes two methods and creates a transaction which calls both of them. - Result of the first method is connected to the argument of the second, - and vice versa. Allows easily connecting methods with compatible - layouts. - """ - - def __init__(self, method1: Method, method2: Method): - """ - Parameters - ---------- - method1: Method - First method. - method2: Method - Second method. - """ - self.method1 = method1 - self.method2 = method2 - - def elaborate(self, platform): - m = TModule() - - with Transaction().body(m): - data1 = Record.like(self.method1.data_out) - data2 = Record.like(self.method2.data_out) - - m.d.top_comb += data1.eq(self.method1(m, data2)) - m.d.top_comb += data2.eq(self.method2(m, data1)) - - return m - - -class ConnectAndTransformTrans(Elaboratable): - """Connecting transaction with transformations. - - Behaves like `ConnectTrans`, but modifies the transferred data using - functions or `Method`s. Equivalent to a combination of - `ConnectTrans` and `MethodTransformer`. The transformation - functions take two parameters, a `Module` and the `Record` being - transformed. - """ - - def __init__( - self, - method1: Method, - method2: Method, - *, - i_fun: Optional[Callable[[TModule, Record], RecordDict]] = None, - o_fun: Optional[Callable[[TModule, Record], RecordDict]] = None, - ): - """ - Parameters - ---------- - method1: Method - First method. - method2: Method - Second method, and the method being transformed. - i_fun: function or Method, optional - Input transformation (`method1` to `method2`). - o_fun: function or Method, optional - Output transformation (`method2` to `method1`). - """ - self.method1 = method1 - self.method2 = method2 - self.i_fun = i_fun or (lambda _, x: x) - self.o_fun = o_fun or (lambda _, x: x) - - def elaborate(self, platform): - m = TModule() - - m.submodules.transformer = transformer = MethodTransformer( - self.method2, - i_transform=(self.method1.data_out.layout, self.i_fun), - o_transform=(self.method1.data_in.layout, self.o_fun), - ) - m.submodules.connect = ConnectTrans(self.method1, transformer.method) - - return m - - -class ManyToOneConnectTrans(Elaboratable): - """Many-to-one method connection. - - Connects each of a set of methods to another method using separate - transactions. Equivalent to a set of `ConnectTrans`. - """ - - def __init__(self, *, get_results: list[Method], put_result: Method): - """ - Parameters - ---------- - get_results: list[Method] - Methods to be connected to the `put_result` method. - put_result: Method - Common method for each of the connections created. - """ - self.get_results = get_results - self.m_put_result = put_result - - self.count = len(self.get_results) - - def elaborate(self, platform): - m = TModule() - - for i in range(self.count): - m.submodules[f"ManyToOneConnectTrans_input_{i}"] = ConnectTrans(self.m_put_result, self.get_results[i]) - - return m - - -class CatTrans(Elaboratable): - """Concatenating transaction. - - Concatenates the results of two methods and passes the result to the - third method. - """ - - def __init__(self, src1: Method, src2: Method, dst: Method): - """ - Parameters - ---------- - src1: Method - First input method. - src2: Method - Second input method. - dst: Method - The method which receives the concatenation of the results of input - methods. - """ - self.src1 = src1 - self.src2 = src2 - self.dst = dst - - def elaborate(self, platform): - m = TModule() - - with Transaction().body(m): - sdata1 = self.src1(m) - sdata2 = self.src2(m) - ddata = Record.like(self.dst.data_in) - self.dst(m, ddata) - - m.d.comb += ddata.eq(Cat(sdata1, sdata2)) - - return m - - -class ArgumentsToResultsZipper(Elaboratable): - """Zips arguments used to call method with results, cutting critical path. - - This module provides possibility to pass arguments from caller and connect it with results - from callee. Arguments are stored in 2-FIFO and results in Forwarder. Because of this asymmetry, - the callee should provide results as long as they aren't correctly received. - - FIFO is used as rate-limiter, so when FIFO reaches full capacity there should be no new requests issued. - - Example topology: - - ```{mermaid} - graph LR - Caller; - Caller -- write_arguments --> 2-FIFO; - Caller -- invoke --> Callee["Callee \n (1+ cycle delay)"]; - Callee -- write_results --> Forwarder; - Forwarder -- read --> Zip; - 2-FIFO -- read --> Zip; - Zip -- read --> User; - - subgraph ArgumentsToResultsZipper - Forwarder; - 2-FIFO; - Zip; - end - ``` - - Attributes - ---------- - write_args: Method - Method to write arguments with `args_layout` format to 2-FIFO. - write_results: Method - Method to save results with `results_layout` in the Forwarder. - read: Method - Reads latest entries from the fifo and the forwarder and return them as - record with two fields: 'args' and 'results'. - """ - - def __init__(self, args_layout: MethodLayout, results_layout: MethodLayout): - """ - Parameters - ---------- - args_layout: record layout - The format of arguments. - results_layout: record layout - The format of results. - """ - self.results_layout = results_layout - self.args_layout = args_layout - self.output_layout = [("args", self.args_layout), ("results", results_layout)] - - self.write_args = Method(i=self.args_layout) - self.write_results = Method(i=self.results_layout) - self.read = Method(o=self.output_layout) - - def elaborate(self, platform): - m = TModule() - - fifo = FIFO(self.args_layout, depth=2) - forwarder = Forwarder(self.results_layout) - - m.submodules.fifo = fifo - m.submodules.forwarder = forwarder - - @def_method(m, self.write_args) - def _(arg): - fifo.write(m, arg) - - @def_method(m, self.write_results) - def _(arg): - forwarder.write(m, arg) - - @def_method(m, self.read) - def _(): - args = fifo.read(m) - results = forwarder.read(m) - return {"args": args, "results": results} - - return m - - -class MemoryBank(Elaboratable): - """MemoryBank module. - - Provides a transactional interface to synchronous Amaranth Memory with one - read and one write port. It supports optionally writing with given granularity. - - Attributes - ---------- - read_req: Method - The read request method. Accepts an `addr` from which data should be read. - Only ready if there is there is a place to buffer response. - read_resp: Method - The read response method. Return `data_layout` Record which was saved on `addr` given by last - `read_req` method call. Only ready after `read_req` call. - write: Method - The write method. Accepts `addr` where data should be saved, `data` in form of `data_layout` - and optionally `mask` if `granularity` is not None. `1` in mask means that appropriate part should be written. - """ - - def __init__( - self, *, data_layout: MethodLayout, elem_count: int, granularity: Optional[int] = None, safe_writes: bool = True - ): - """ - Parameters - ---------- - data_layout: record layout - The format of records stored in the Memory. - elem_count: int - Number of elements stored in Memory. - granularity: Optional[int] - Granularity of write, forwarded to Amaranth. If `None` the whole record is always saved at once. - If not, the width of `data_layout` is split into `granularity` parts, which can be saved independently. - safe_writes: bool - Set to `False` if an optimisation can be done to increase throughput of writes. This will cause that - writes will be reordered with respect to reads eg. in sequence "read A, write A X", read can return - "X" even when write was called later. By default `True`, which disable optimisation. - """ - self.data_layout = data_layout - self.elem_count = elem_count - self.granularity = granularity - self.width = len(Record(self.data_layout)) - self.addr_width = bits_for(self.elem_count - 1) - self.safe_writes = safe_writes - - self.read_req_layout = [("addr", self.addr_width)] - self.write_layout = [("addr", self.addr_width), ("data", self.data_layout)] - if self.granularity is not None: - self.write_layout.append(("mask", self.width // self.granularity)) - - self.read_req = Method(i=self.read_req_layout) - self.read_resp = Method(o=self.data_layout) - self.write = Method(i=self.write_layout) - self._internal_read_resp_trans = None - - def elaborate(self, platform) -> TModule: - m = TModule() - - mem = Memory(width=self.width, depth=self.elem_count) - m.submodules.read_port = read_port = mem.read_port() - m.submodules.write_port = write_port = mem.write_port() - read_output_valid = Signal() - prev_read_addr = Signal(self.addr_width) - write_pending = Signal() - write_req = Signal() - write_args = Record(self.write_layout) - write_args_prev = Record(self.write_layout) - m.d.comb += read_port.addr.eq(prev_read_addr) - - zipper = ArgumentsToResultsZipper([("valid", 1)], self.data_layout) - m.submodules.zipper = zipper - - self._internal_read_resp_trans = Transaction() - with self._internal_read_resp_trans.body(m, request=read_output_valid): - m.d.sync += read_output_valid.eq(0) - zipper.write_results(m, read_port.data) - - write_trans = Transaction() - with write_trans.body(m, request=write_req | (~read_output_valid & write_pending)): - if self.safe_writes: - with m.If(write_pending): - m.d.comb += assign(write_args, write_args_prev, fields=AssignType.ALL) - m.d.sync += write_pending.eq(0) - m.d.comb += write_port.addr.eq(write_args.addr) - m.d.comb += write_port.data.eq(write_args.data) - if self.granularity is None: - m.d.comb += write_port.en.eq(1) - else: - m.d.comb += write_port.en.eq(write_args.mask) - - @def_method(m, self.read_resp) - def _(): - output = zipper.read(m) - return output.results - - @def_method(m, self.read_req, ~write_pending) - def _(addr): - m.d.sync += read_output_valid.eq(1) - m.d.comb += read_port.addr.eq(addr) - m.d.sync += prev_read_addr.eq(addr) - zipper.write_args(m, valid=1) - - @def_method(m, self.write, ~write_pending) - def _(arg): - if self.safe_writes: - with m.If((arg.addr == read_port.addr) & (read_output_valid | self.read_req.run)): - m.d.sync += write_pending.eq(1) - m.d.sync += assign(write_args_prev, arg, fields=AssignType.ALL) - with m.Else(): - m.d.comb += write_req.eq(1) - else: - m.d.comb += write_req.eq(1) - m.d.comb += assign(write_args, arg, fields=AssignType.ALL) - - return m - - -class Serializer(Elaboratable): - """Module to serialize request-response methods. - - Provides a transactional interface to connect many client `Module`\\s (which request somethig using method call) - with a server `Module` which provides method to request operation and method to get response. - - Requests are being serialized from many clients and forwarded to a server which can process only one request - at the time. Responses from server are deserialized and passed to proper client. `Serializer` assumes, that - responses from the server are in-order, so the order of responses is the same as order of requests. - - - Attributes - ---------- - serialize_in: list[Method] - List of request methods. Data layouts are the same as for `serialized_req_method`. - serialize_out: list[Method] - List of response methods. Data layouts are the same as for `serialized_resp_method`. - `i`-th response method provides responses for requests from `i`-th `serialize_in` method. - """ - - def __init__( - self, - *, - port_count: int, - serialized_req_method: Method, - serialized_resp_method: Method, - depth: int = 4, - ): - """ - Parameters - ---------- - port_count: int - Number of ports, which should be generated. `len(serialize_in)=len(serialize_out)=port_count` - serialized_req_method: Method - Request method provided by server's `Module`. - serialized_resp_method: Method - Response method provided by server's `Module`. - depth: int - Number of requests which can be forwarded to server, before server provides first response. Describe - the resistance of `Serializer` to latency of server in case when server is fully pipelined. - """ - self.port_count = port_count - self.serialized_req_method = serialized_req_method - self.serialized_resp_method = serialized_resp_method - - self.depth = depth - - self.id_layout = [("id", log2_int(self.port_count))] - - self.clear = Method() - self.serialize_in = [Method.like(self.serialized_req_method) for _ in range(self.port_count)] - self.serialize_out = [Method.like(self.serialized_resp_method) for _ in range(self.port_count)] - - def elaborate(self, platform) -> TModule: - m = TModule() - - pending_requests = BasicFifo(self.id_layout, self.depth) - m.submodules.pending_requests = pending_requests - - for i in range(self.port_count): - - @def_method(m, self.serialize_in[i]) - def _(arg): - pending_requests.write(m, {"id": i}) - self.serialized_req_method(m, arg) - - @def_method(m, self.serialize_out[i], ready=(pending_requests.head.id == i)) - def _(): - pending_requests.read(m) - return self.serialized_resp_method(m) - - self.clear.proxy(m, pending_requests.clear) - - return m - - -# Conditions using simultaneous transactions - - -@contextmanager -def condition(m: TModule, *, nonblocking: bool = False, priority: bool = True): - """Conditions using simultaneous transactions. - - This context manager allows to easily define conditions utilizing - nested transactions and the simultaneous transactions mechanism. - It is similar to Amaranth's `If`, but allows to call different and - possibly overlapping method sets in each branch. Each of the branches is - defined using a separate nested transaction. - - Inside the condition body, branches can be added, which are guarded - by Boolean conditions. A branch is considered for execution if its - condition is true and the called methods can be run. A catch-all, - default branch can be added, which can be executed only if none of - the other branches execute. Note that the default branch can run - even if some of the conditions are true, but their branches can't - execute for other reasons. - - Parameters - ---------- - m : TModule - A module where the condition is defined. - nonblocking : bool - States that the condition should not block the containing method - or transaction from running, even when every branch cannot run. - If `nonblocking` is false and every branch cannot run (because of - a false condition or disabled called methods), the whole method - or transaction will be stopped from running. - priority : bool - States that the conditions are not mutually exclusive and should - be tested in order. This influences the scheduling order of generated - transactions. - - Examples - -------- - .. highlight:: python - .. code-block:: python - - with condition(m) as branch: - with branch(cond1): - ... - with branch(cond2): - ... - with branch(): # default, optional - ... - """ - this = TransactionBase.get() - transactions = list[Transaction]() - last = False - - @contextmanager - def branch(cond: Optional[ValueLike] = None): - nonlocal last - if last: - raise RuntimeError("Condition clause added after catch-all") - req = cond if cond is not None else 1 - name = f"{this.name}_cond{len(transactions)}" - with (transaction := Transaction(name=name)).body(m, request=req): - yield - if transactions and priority: - transactions[-1].schedule_before(transaction) - if cond is None: - last = True - if not priority: - for transaction0 in transactions: - transaction0.schedule_before(transaction) - transactions.append(transaction) - - yield branch - - if nonblocking and not last: - with branch(): - pass - - this.simultaneous_alternatives(*transactions) diff --git a/transactron/lib/__init__.py b/transactron/lib/__init__.py new file mode 100644 index 000000000..1caa56d21 --- /dev/null +++ b/transactron/lib/__init__.py @@ -0,0 +1,7 @@ +from .connectors import * # noqa: F401 +from .buttons import * # noqa: F401 +from .adapters import * # noqa: F401 +from .transformers import * # noqa: F401 +from .reqres import * # noqa: F401 +from .storage import * # noqa: F401 +from .simultaneous import * # noqa: F401 diff --git a/transactron/lib/adapters.py b/transactron/lib/adapters.py new file mode 100644 index 000000000..d367fe577 --- /dev/null +++ b/transactron/lib/adapters.py @@ -0,0 +1,115 @@ +from amaranth import * +from ..core import * +from ..core import SignalBundle +from typing import Optional + +__all__ = [ + "AdapterBase", + "AdapterTrans", + "Adapter", +] + + +class AdapterBase(Elaboratable): + data_in: Record + data_out: Record + + def __init__(self, iface: Method): + self.iface = iface + self.en = Signal() + self.done = Signal() + + def debug_signals(self) -> SignalBundle: + return [self.en, self.done, self.data_in, self.data_out] + + +class AdapterTrans(AdapterBase): + """Adapter transaction. + + Creates a transaction controlled by plain Amaranth signals. Allows to + expose a method to plain Amaranth code, including testbenches. + + Attributes + ---------- + en: Signal, in + Activates the transaction (sets the `request` signal). + done: Signal, out + Signals that the transaction is performed (returns the `grant` + signal). + data_in: Record, in + Data passed to the `iface` method. + data_out: Record, out + Data returned from the `iface` method. + """ + + def __init__(self, iface: Method): + """ + Parameters + ---------- + iface: Method + The method to be called by the transaction. + """ + super().__init__(iface) + self.data_in = Record.like(iface.data_in) + self.data_out = Record.like(iface.data_out) + + def elaborate(self, platform): + m = TModule() + + # this forces data_in signal to appear in VCD dumps + data_in = Signal.like(self.data_in) + m.d.comb += data_in.eq(self.data_in) + + with Transaction(name=f"AdapterTrans_{self.iface.name}").body(m, request=self.en): + data_out = self.iface(m, data_in) + m.d.top_comb += self.data_out.eq(data_out) + m.d.comb += self.done.eq(1) + + return m + + +class Adapter(AdapterBase): + """Adapter method. + + Creates a method controlled by plain Amaranth signals. One of the + possible uses is to mock a method in a testbench. + + Attributes + ---------- + en: Signal, in + Activates the method (sets the `ready` signal). + done: Signal, out + Signals that the method is called (returns the `run` signal). + data_in: Record, in + Data returned from the defined method. + data_out: Record, out + Data passed as argument to the defined method. + """ + + def __init__(self, *, name: Optional[str] = None, i: MethodLayout = (), o: MethodLayout = ()): + """ + Parameters + ---------- + i: record layout + The input layout of the defined method. + o: record layout + The output layout of the defined method. + """ + super().__init__(Method(name=name, i=i, o=o)) + self.data_in = Record.like(self.iface.data_out) + self.data_out = Record.like(self.iface.data_in) + + def elaborate(self, platform): + m = TModule() + + # this forces data_in signal to appear in VCD dumps + data_in = Signal.like(self.data_in) + m.d.comb += data_in.eq(self.data_in) + + @def_method(m, self.iface, ready=self.en) + def _(arg): + m.d.top_comb += self.data_out.eq(arg) + m.d.comb += self.done.eq(1) + return data_in + + return m diff --git a/transactron/lib/buttons.py b/transactron/lib/buttons.py new file mode 100644 index 000000000..82955779f --- /dev/null +++ b/transactron/lib/buttons.py @@ -0,0 +1,102 @@ +from amaranth import * +from ..core import * + +__all__ = ["ClickIn", "ClickOut"] + + +class ClickIn(Elaboratable): + """Clicked input. + + Useful for interactive simulations or FPGA button/switch interfaces. + On a rising edge (tested synchronously) of `btn`, the `get` method + is enabled, which returns the data present on `dat` at the time. + Inputs are synchronized. + + Attributes + ---------- + get: Method + The method for retrieving data from the input. Accepts an empty + argument, returns a `Record`. + btn: Signal, in + The button input. + dat: Record, in + The data input. + """ + + def __init__(self, layout: MethodLayout): + """ + Parameters + ---------- + layout: record layout + The data format for the input. + """ + self.get = Method(o=layout) + self.btn = Signal() + self.dat = Record(layout) + + def elaborate(self, platform): + m = TModule() + + btn1 = Signal() + btn2 = Signal() + dat1 = Signal.like(self.dat) + m.d.sync += btn1.eq(self.btn) + m.d.sync += btn2.eq(btn1) + m.d.sync += dat1.eq(self.dat) + get_ready = Signal() + get_data = Signal.like(self.dat) + + @def_method(m, self.get, ready=get_ready) + def _(): + m.d.sync += get_ready.eq(0) + return get_data + + with m.If(~btn2 & btn1): + m.d.sync += get_ready.eq(1) + m.d.sync += get_data.eq(dat1) + + return m + + +class ClickOut(Elaboratable): + """Clicked output. + + Useful for interactive simulations or FPGA button/LED interfaces. + On a rising edge (tested synchronously) of `btn`, the `put` method + is enabled, which, when called, changes the value of the `dat` signal. + + Attributes + ---------- + put: Method + The method for retrieving data from the input. Accepts a `Record`, + returns empty result. + btn: Signal, in + The button input. + dat: Record, out + The data output. + """ + + def __init__(self, layout: MethodLayout): + """ + Parameters + ---------- + layout: record layout + The data format for the output. + """ + self.put = Method(i=layout) + self.btn = Signal() + self.dat = Record(layout) + + def elaborate(self, platform): + m = TModule() + + btn1 = Signal() + btn2 = Signal() + m.d.sync += btn1.eq(self.btn) + m.d.sync += btn2.eq(btn1) + + @def_method(m, self.put, ready=~btn2 & btn1) + def _(arg): + m.d.sync += self.dat.eq(arg) + + return m diff --git a/transactron/lib/connectors.py b/transactron/lib/connectors.py new file mode 100644 index 000000000..3b9b18b83 --- /dev/null +++ b/transactron/lib/connectors.py @@ -0,0 +1,252 @@ +from amaranth import * +import amaranth.lib.fifo +from ..core import * + +__all__ = [ + "FIFO", + "Forwarder", + "Connect", + "ConnectTrans", + "ManyToOneConnectTrans", +] + + +class FIFO(Elaboratable): + """FIFO module. + + Provides a transactional interface to Amaranth FIFOs. Exposes two methods: + `read`, and `write`. Both methods are ready only when they can + be executed -- i.e. the queue is respectively not empty / not full. + It is possible to simultaneously read and write in a single clock cycle, + but only if both readiness conditions are fulfilled. + + Attributes + ---------- + read: Method + The read method. Accepts an empty argument, returns a `Record`. + write: Method + The write method. Accepts a `Record`, returns empty result. + """ + + def __init__(self, layout: MethodLayout, depth: int, fifo_type=amaranth.lib.fifo.SyncFIFO): + """ + Parameters + ---------- + layout: record layout + The format of records stored in the FIFO. + depth: int + Size of the FIFO. + fifoType: Elaboratable + FIFO module conforming to Amaranth library FIFO interface. Defaults + to SyncFIFO. + """ + self.width = len(Record(layout)) + self.depth = depth + self.fifoType = fifo_type + + self.read = Method(o=layout) + self.write = Method(i=layout) + + def elaborate(self, platform): + m = TModule() + + m.submodules.fifo = fifo = self.fifoType(width=self.width, depth=self.depth) + + assert fifo.fwft # the read method requires FWFT behavior + + @def_method(m, self.write, ready=fifo.w_rdy) + def _(arg): + m.d.comb += fifo.w_en.eq(1) + m.d.top_comb += fifo.w_data.eq(arg) + + @def_method(m, self.read, ready=fifo.r_rdy) + def _(): + m.d.comb += fifo.r_en.eq(1) + return fifo.r_data + + return m + + +# Forwarding with overflow buffering + + +class Forwarder(Elaboratable): + """Forwarding with overflow buffering + + Provides a means to connect two transactions with forwarding. Exposes + two methods: `read`, and `write`. When both of these methods are + executed simultaneously, data is forwarded between them. If `write` + is executed, but `read` is not, the value cannot be forwarded, + but is stored into an overflow buffer. No further `write`\\s are + possible until the overflow buffer is cleared by `read`. + + The `write` method is scheduled before `read`. + + Attributes + ---------- + read: Method + The read method. Accepts an empty argument, returns a `Record`. + write: Method + The write method. Accepts a `Record`, returns empty result. + """ + + def __init__(self, layout: MethodLayout): + """ + Parameters + ---------- + layout: record layout + The format of records forwarded. + """ + self.read = Method(o=layout) + self.write = Method(i=layout) + self.clear = Method() + self.head = Record.like(self.read.data_out) + + self.clear.add_conflict(self.read, Priority.LEFT) + self.clear.add_conflict(self.write, Priority.LEFT) + + def elaborate(self, platform): + m = TModule() + + reg = Record.like(self.read.data_out) + reg_valid = Signal() + read_value = Record.like(self.read.data_out) + m.d.comb += self.head.eq(read_value) + + self.write.schedule_before(self.read) # to avoid combinational loops + + @def_method(m, self.write, ready=~reg_valid) + def _(arg): + m.d.av_comb += read_value.eq(arg) # for forwarding + m.d.sync += reg.eq(arg) + m.d.sync += reg_valid.eq(1) + + with m.If(reg_valid): + m.d.av_comb += read_value.eq(reg) # write method is not ready + + @def_method(m, self.read, ready=reg_valid | self.write.run) + def _(): + m.d.sync += reg_valid.eq(0) + return read_value + + @def_method(m, self.clear) + def _(): + m.d.sync += reg_valid.eq(0) + + return m + + +class Connect(Elaboratable): + """Forwarding by transaction simultaneity + + Provides a means to connect two transactions with forwarding + by means of the transaction simultaneity mechanism. It provides + two methods: `read`, and `write`, which always execute simultaneously. + Typical use case is for moving data from `write` to `read`, but + data flow in the reverse direction is also possible. + + Attributes + ---------- + read: Method + The read method. Accepts a (possibly empty) `Record`, returns + a `Record`. + write: Method + The write method. Accepts a `Record`, returns a (possibly empty) + `Record`. + """ + + def __init__(self, layout: MethodLayout = (), rev_layout: MethodLayout = ()): + """ + Parameters + ---------- + layout: record layout + The format of records forwarded. + rev_layout: record layout + The format of records forwarded in the reverse direction. + """ + self.read = Method(o=layout, i=rev_layout) + self.write = Method(i=layout, o=rev_layout) + + def elaborate(self, platform): + m = TModule() + + read_value = Record.like(self.read.data_out) + rev_read_value = Record.like(self.write.data_out) + + self.write.simultaneous(self.read) + + @def_method(m, self.write) + def _(arg): + m.d.av_comb += read_value.eq(arg) + return rev_read_value + + @def_method(m, self.read) + def _(arg): + m.d.av_comb += rev_read_value.eq(arg) + return read_value + + return m + + +class ConnectTrans(Elaboratable): + """Simple connecting transaction. + + Takes two methods and creates a transaction which calls both of them. + Result of the first method is connected to the argument of the second, + and vice versa. Allows easily connecting methods with compatible + layouts. + """ + + def __init__(self, method1: Method, method2: Method): + """ + Parameters + ---------- + method1: Method + First method. + method2: Method + Second method. + """ + self.method1 = method1 + self.method2 = method2 + + def elaborate(self, platform): + m = TModule() + + with Transaction().body(m): + data1 = Record.like(self.method1.data_out) + data2 = Record.like(self.method2.data_out) + + m.d.top_comb += data1.eq(self.method1(m, data2)) + m.d.top_comb += data2.eq(self.method2(m, data1)) + + return m + + +class ManyToOneConnectTrans(Elaboratable): + """Many-to-one method connection. + + Connects each of a set of methods to another method using separate + transactions. Equivalent to a set of `ConnectTrans`. + """ + + def __init__(self, *, get_results: list[Method], put_result: Method): + """ + Parameters + ---------- + get_results: list[Method] + Methods to be connected to the `put_result` method. + put_result: Method + Common method for each of the connections created. + """ + self.get_results = get_results + self.m_put_result = put_result + + self.count = len(self.get_results) + + def elaborate(self, platform): + m = TModule() + + for i in range(self.count): + m.submodules[f"ManyToOneConnectTrans_input_{i}"] = ConnectTrans(self.m_put_result, self.get_results[i]) + + return m diff --git a/transactron/lib/reqres.py b/transactron/lib/reqres.py new file mode 100644 index 000000000..ace563c1b --- /dev/null +++ b/transactron/lib/reqres.py @@ -0,0 +1,166 @@ +from amaranth import * +from ..core import * +from .connectors import Forwarder, FIFO +from coreblocks.utils.fifo import BasicFifo +from amaranth.utils import * + +__all__ = [ + "ArgumentsToResultsZipper", + "Serializer", +] + + +class ArgumentsToResultsZipper(Elaboratable): + """Zips arguments used to call method with results, cutting critical path. + + This module provides possibility to pass arguments from caller and connect it with results + from callee. Arguments are stored in 2-FIFO and results in Forwarder. Because of this asymmetry, + the callee should provide results as long as they aren't correctly received. + + FIFO is used as rate-limiter, so when FIFO reaches full capacity there should be no new requests issued. + + Example topology: + + .. mermaid:: + + graph LR + Caller -- write_arguments --> 2-FIFO; + Caller -- invoke --> Callee["Callee \\n (1+ cycle delay)"]; + Callee -- write_results --> Forwarder; + Forwarder -- read --> Zip; + 2-FIFO -- read --> Zip; + Zip -- read --> User; + subgraph ArgumentsToResultsZipper + Forwarder; + 2-FIFO; + Zip; + end + + Attributes + ---------- + write_args: Method + Method to write arguments with `args_layout` format to 2-FIFO. + write_results: Method + Method to save results with `results_layout` in the Forwarder. + read: Method + Reads latest entries from the fifo and the forwarder and return them as + record with two fields: 'args' and 'results'. + """ + + def __init__(self, args_layout: MethodLayout, results_layout: MethodLayout): + """ + Parameters + ---------- + args_layout: record layout + The format of arguments. + results_layout: record layout + The format of results. + """ + self.results_layout = results_layout + self.args_layout = args_layout + self.output_layout = [("args", self.args_layout), ("results", results_layout)] + + self.write_args = Method(i=self.args_layout) + self.write_results = Method(i=self.results_layout) + self.read = Method(o=self.output_layout) + + def elaborate(self, platform): + m = TModule() + + fifo = FIFO(self.args_layout, depth=2) + forwarder = Forwarder(self.results_layout) + + m.submodules.fifo = fifo + m.submodules.forwarder = forwarder + + @def_method(m, self.write_args) + def _(arg): + fifo.write(m, arg) + + @def_method(m, self.write_results) + def _(arg): + forwarder.write(m, arg) + + @def_method(m, self.read) + def _(): + args = fifo.read(m) + results = forwarder.read(m) + return {"args": args, "results": results} + + return m + + +class Serializer(Elaboratable): + """Module to serialize request-response methods. + + Provides a transactional interface to connect many client `Module`\\s (which request somethig using method call) + with a server `Module` which provides method to request operation and method to get response. + + Requests are being serialized from many clients and forwarded to a server which can process only one request + at the time. Responses from server are deserialized and passed to proper client. `Serializer` assumes, that + responses from the server are in-order, so the order of responses is the same as order of requests. + + + Attributes + ---------- + serialize_in: list[Method] + List of request methods. Data layouts are the same as for `serialized_req_method`. + serialize_out: list[Method] + List of response methods. Data layouts are the same as for `serialized_resp_method`. + `i`-th response method provides responses for requests from `i`-th `serialize_in` method. + """ + + def __init__( + self, + *, + port_count: int, + serialized_req_method: Method, + serialized_resp_method: Method, + depth: int = 4, + ): + """ + Parameters + ---------- + port_count: int + Number of ports, which should be generated. `len(serialize_in)=len(serialize_out)=port_count` + serialized_req_method: Method + Request method provided by server's `Module`. + serialized_resp_method: Method + Response method provided by server's `Module`. + depth: int + Number of requests which can be forwarded to server, before server provides first response. Describe + the resistance of `Serializer` to latency of server in case when server is fully pipelined. + """ + self.port_count = port_count + self.serialized_req_method = serialized_req_method + self.serialized_resp_method = serialized_resp_method + + self.depth = depth + + self.id_layout = [("id", log2_int(self.port_count))] + + self.clear = Method() + self.serialize_in = [Method.like(self.serialized_req_method) for _ in range(self.port_count)] + self.serialize_out = [Method.like(self.serialized_resp_method) for _ in range(self.port_count)] + + def elaborate(self, platform) -> TModule: + m = TModule() + + pending_requests = BasicFifo(self.id_layout, self.depth) + m.submodules.pending_requests = pending_requests + + for i in range(self.port_count): + + @def_method(m, self.serialize_in[i]) + def _(arg): + pending_requests.write(m, {"id": i}) + self.serialized_req_method(m, arg) + + @def_method(m, self.serialize_out[i], ready=(pending_requests.head.id == i)) + def _(): + pending_requests.read(m) + return self.serialized_resp_method(m) + + self.clear.proxy(m, pending_requests.clear) + + return m diff --git a/transactron/lib/simultaneous.py b/transactron/lib/simultaneous.py new file mode 100644 index 000000000..093bfb78d --- /dev/null +++ b/transactron/lib/simultaneous.py @@ -0,0 +1,87 @@ +from amaranth import * +from ..core import * +from ..core import TransactionBase +from contextlib import contextmanager +from typing import Optional +from coreblocks.utils import ValueLike + +__all__ = [ + "condition", +] + + +@contextmanager +def condition(m: TModule, *, nonblocking: bool = False, priority: bool = True): + """Conditions using simultaneous transactions. + + This context manager allows to easily define conditions utilizing + nested transactions and the simultaneous transactions mechanism. + It is similar to Amaranth's `If`, but allows to call different and + possibly overlapping method sets in each branch. Each of the branches is + defined using a separate nested transaction. + + Inside the condition body, branches can be added, which are guarded + by Boolean conditions. A branch is considered for execution if its + condition is true and the called methods can be run. A catch-all, + default branch can be added, which can be executed only if none of + the other branches execute. Note that the default branch can run + even if some of the conditions are true, but their branches can't + execute for other reasons. + + Parameters + ---------- + m : TModule + A module where the condition is defined. + nonblocking : bool + States that the condition should not block the containing method + or transaction from running, even when every branch cannot run. + If `nonblocking` is false and every branch cannot run (because of + a false condition or disabled called methods), the whole method + or transaction will be stopped from running. + priority : bool + States that the conditions are not mutually exclusive and should + be tested in order. This influences the scheduling order of generated + transactions. + + Examples + -------- + .. highlight:: python + .. code-block:: python + + with condition(m) as branch: + with branch(cond1): + ... + with branch(cond2): + ... + with branch(): # default, optional + ... + """ + this = TransactionBase.get() + transactions = list[Transaction]() + last = False + + @contextmanager + def branch(cond: Optional[ValueLike] = None): + nonlocal last + if last: + raise RuntimeError("Condition clause added after catch-all") + req = cond if cond is not None else 1 + name = f"{this.name}_cond{len(transactions)}" + with (transaction := Transaction(name=name)).body(m, request=req): + yield + if transactions and priority: + transactions[-1].schedule_before(transaction) + if cond is None: + last = True + if not priority: + for transaction0 in transactions: + transaction0.schedule_before(transaction) + transactions.append(transaction) + + yield branch + + if nonblocking and not last: + with branch(): + pass + + this.simultaneous_alternatives(*transactions) diff --git a/transactron/lib/storage.py b/transactron/lib/storage.py new file mode 100644 index 000000000..4723e3713 --- /dev/null +++ b/transactron/lib/storage.py @@ -0,0 +1,124 @@ +from amaranth import * +from amaranth.utils import * +from ..core import * +from typing import Optional +from coreblocks.utils import assign, AssignType +from .reqres import ArgumentsToResultsZipper + +__all__ = ["MemoryBank"] + + +class MemoryBank(Elaboratable): + """MemoryBank module. + + Provides a transactional interface to synchronous Amaranth Memory with one + read and one write port. It supports optionally writing with given granularity. + + Attributes + ---------- + read_req: Method + The read request method. Accepts an `addr` from which data should be read. + Only ready if there is there is a place to buffer response. + read_resp: Method + The read response method. Return `data_layout` Record which was saved on `addr` given by last + `read_req` method call. Only ready after `read_req` call. + write: Method + The write method. Accepts `addr` where data should be saved, `data` in form of `data_layout` + and optionally `mask` if `granularity` is not None. `1` in mask means that appropriate part should be written. + """ + + def __init__( + self, *, data_layout: MethodLayout, elem_count: int, granularity: Optional[int] = None, safe_writes: bool = True + ): + """ + Parameters + ---------- + data_layout: record layout + The format of records stored in the Memory. + elem_count: int + Number of elements stored in Memory. + granularity: Optional[int] + Granularity of write, forwarded to Amaranth. If `None` the whole record is always saved at once. + If not, the width of `data_layout` is split into `granularity` parts, which can be saved independently. + safe_writes: bool + Set to `False` if an optimisation can be done to increase throughput of writes. This will cause that + writes will be reordered with respect to reads eg. in sequence "read A, write A X", read can return + "X" even when write was called later. By default `True`, which disable optimisation. + """ + self.data_layout = data_layout + self.elem_count = elem_count + self.granularity = granularity + self.width = len(Record(self.data_layout)) + self.addr_width = bits_for(self.elem_count - 1) + self.safe_writes = safe_writes + + self.read_req_layout = [("addr", self.addr_width)] + self.write_layout = [("addr", self.addr_width), ("data", self.data_layout)] + if self.granularity is not None: + self.write_layout.append(("mask", self.width // self.granularity)) + + self.read_req = Method(i=self.read_req_layout) + self.read_resp = Method(o=self.data_layout) + self.write = Method(i=self.write_layout) + self._internal_read_resp_trans = None + + def elaborate(self, platform) -> TModule: + m = TModule() + + mem = Memory(width=self.width, depth=self.elem_count) + m.submodules.read_port = read_port = mem.read_port() + m.submodules.write_port = write_port = mem.write_port() + read_output_valid = Signal() + prev_read_addr = Signal(self.addr_width) + write_pending = Signal() + write_req = Signal() + write_args = Record(self.write_layout) + write_args_prev = Record(self.write_layout) + m.d.comb += read_port.addr.eq(prev_read_addr) + + zipper = ArgumentsToResultsZipper([("valid", 1)], self.data_layout) + m.submodules.zipper = zipper + + self._internal_read_resp_trans = Transaction() + with self._internal_read_resp_trans.body(m, request=read_output_valid): + m.d.sync += read_output_valid.eq(0) + zipper.write_results(m, read_port.data) + + write_trans = Transaction() + with write_trans.body(m, request=write_req | (~read_output_valid & write_pending)): + if self.safe_writes: + with m.If(write_pending): + m.d.comb += assign(write_args, write_args_prev, fields=AssignType.ALL) + m.d.sync += write_pending.eq(0) + m.d.comb += write_port.addr.eq(write_args.addr) + m.d.comb += write_port.data.eq(write_args.data) + if self.granularity is None: + m.d.comb += write_port.en.eq(1) + else: + m.d.comb += write_port.en.eq(write_args.mask) + + @def_method(m, self.read_resp) + def _(): + output = zipper.read(m) + return output.results + + @def_method(m, self.read_req, ~write_pending) + def _(addr): + m.d.sync += read_output_valid.eq(1) + m.d.comb += read_port.addr.eq(addr) + m.d.sync += prev_read_addr.eq(addr) + zipper.write_args(m, valid=1) + + @def_method(m, self.write, ~write_pending) + def _(arg): + if self.safe_writes: + with m.If((arg.addr == read_port.addr) & (read_output_valid | self.read_req.run)): + m.d.sync += write_pending.eq(1) + m.d.sync += assign(write_args_prev, arg, fields=AssignType.ALL) + with m.Else(): + m.d.comb += write_req.eq(1) + else: + m.d.comb += write_req.eq(1) + m.d.comb += assign(write_args, arg, fields=AssignType.ALL) + + return m diff --git a/transactron/lib/transformers.py b/transactron/lib/transformers.py new file mode 100644 index 000000000..e4b7aa0c0 --- /dev/null +++ b/transactron/lib/transformers.py @@ -0,0 +1,356 @@ +from amaranth import * +from ..core import * +from ..core import RecordDict +from typing import Optional +from collections.abc import Callable +from coreblocks.utils import ValueLike, assign, AssignType +from .connectors import Forwarder, ManyToOneConnectTrans, ConnectTrans + +__all__ = [ + "MethodTransformer", + "MethodFilter", + "MethodProduct", + "MethodTryProduct", + "Collector", + "CatTrans", + "ConnectAndTransformTrans", +] + + +class MethodTransformer(Elaboratable): + """Method transformer. + + Takes a target method and creates a transformed method which calls the + original target method, transforming the input and output values. + The transformation functions take two parameters, a `Module` and the + `Record` being transformed. Alternatively, a `Method` can be + passed. + + Attributes + ---------- + method: Method + The transformed method. + """ + + def __init__( + self, + target: Method, + *, + i_transform: Optional[tuple[MethodLayout, Callable[[TModule, Record], RecordDict]]] = None, + o_transform: Optional[tuple[MethodLayout, Callable[[TModule, Record], RecordDict]]] = None, + ): + """ + Parameters + ---------- + target: Method + The target method. + i_transform: (record layout, function or Method), optional + Input transformation. If specified, it should be a pair of a + function and a input layout for the transformed method. + If not present, input is not transformed. + o_transform: (record layout, function or Method), optional + Output transformation. If specified, it should be a pair of a + function and a output layout for the transformed method. + If not present, output is not transformed. + """ + if i_transform is None: + i_transform = (target.data_in.layout, lambda _, x: x) + if o_transform is None: + o_transform = (target.data_out.layout, lambda _, x: x) + + self.target = target + self.method = Method(i=i_transform[0], o=o_transform[0]) + self.i_fun = i_transform[1] + self.o_fun = o_transform[1] + + def elaborate(self, platform): + m = TModule() + + @def_method(m, self.method) + def _(arg): + return self.o_fun(m, self.target(m, self.i_fun(m, arg))) + + return m + + +class MethodFilter(Elaboratable): + """Method filter. + + Takes a target method and creates a method which calls the target method + only when some condition is true. The condition function takes two + parameters, a module and the input `Record` of the method. Non-zero + return value is interpreted as true. Alternatively to using a function, + a `Method` can be passed as a condition. + + Caveat: because of the limitations of transaction scheduling, the target + method is locked for usage even if it is not called. + + Attributes + ---------- + method: Method + The transformed method. + """ + + def __init__( + self, target: Method, condition: Callable[[TModule, Record], ValueLike], default: Optional[RecordDict] = None + ): + """ + Parameters + ---------- + target: Method + The target method. + condition: function or Method + The condition which, when true, allows the call to `target`. When + false, `default` is returned. + default: Value or dict, optional + The default value returned from the filtered method when the condition + is false. If omitted, zero is returned. + """ + if default is None: + default = Record.like(target.data_out) + + self.target = target + self.method = Method.like(target) + self.condition = condition + self.default = default + + def elaborate(self, platform): + m = TModule() + + ret = Record.like(self.target.data_out) + m.d.comb += assign(ret, self.default, fields=AssignType.ALL) + + @def_method(m, self.method) + def _(arg): + with m.If(self.condition(m, arg)): + m.d.comb += ret.eq(self.target(m, arg)) + return ret + + return m + + +class MethodProduct(Elaboratable): + def __init__( + self, + targets: list[Method], + combiner: Optional[tuple[MethodLayout, Callable[[TModule, list[Record]], RecordDict]]] = None, + ): + """Method product. + + Takes arbitrary, non-zero number of target methods, and constructs + a method which calls all of the target methods using the same + argument. The return value of the resulting method is, by default, + the return value of the first of the target methods. A combiner + function can be passed, which can compute the return value from + the results of every target method. + + Parameters + ---------- + targets: list[Method] + A list of methods to be called. + combiner: (int or method layout, function), optional + A pair of the output layout and the combiner function. The + combiner function takes two parameters: a `Module` and + a list of outputs of the target methods. + + Attributes + ---------- + method: Method + The product method. + """ + if combiner is None: + combiner = (targets[0].data_out.layout, lambda _, x: x[0]) + self.targets = targets + self.combiner = combiner + self.method = Method(i=targets[0].data_in.layout, o=combiner[0]) + + def elaborate(self, platform): + m = TModule() + + @def_method(m, self.method) + def _(arg): + results = [] + for target in self.targets: + results.append(target(m, arg)) + return self.combiner[1](m, results) + + return m + + +class MethodTryProduct(Elaboratable): + def __init__( + self, + targets: list[Method], + combiner: Optional[tuple[MethodLayout, Callable[[TModule, list[tuple[Value, Record]]], RecordDict]]] = None, + ): + """Method product with optional calling. + + Takes arbitrary, non-zero number of target methods, and constructs + a method which tries to call all of the target methods using the same + argument. The methods which are not ready are not called. The return + value of the resulting method is, by default, empty. A combiner + function can be passed, which can compute the return value from the + results of every target method. + + Parameters + ---------- + targets: list[Method] + A list of methods to be called. + combiner: (int or method layout, function), optional + A pair of the output layout and the combiner function. The + combiner function takes two parameters: a `Module` and + a list of pairs. Each pair contains a bit which signals + that a given call succeeded, and the result of the call. + + Attributes + ---------- + method: Method + The product method. + """ + if combiner is None: + combiner = ([], lambda _, __: {}) + self.targets = targets + self.combiner = combiner + self.method = Method(i=targets[0].data_in.layout, o=combiner[0]) + + def elaborate(self, platform): + m = TModule() + + @def_method(m, self.method) + def _(arg): + results: list[tuple[Value, Record]] = [] + for target in self.targets: + success = Signal() + with Transaction().body(m): + m.d.comb += success.eq(1) + results.append((success, target(m, arg))) + return self.combiner[1](m, results) + + return m + + +class Collector(Elaboratable): + """Single result collector. + + Creates method that collects results of many methods with identical + layouts. Each call of this method will return a single result of one + of the provided methods. + + Attributes + ---------- + method: Method + Method which returns single result of provided methods. + """ + + def __init__(self, targets: list[Method]): + """ + Parameters + ---------- + method_list: list[Method] + List of methods from which results will be collected. + """ + self.method_list = targets + layout = targets[0].data_out.layout + self.method = Method(o=layout) + + for method in targets: + if layout != method.data_out.layout: + raise Exception("Not all methods have this same layout") + + def elaborate(self, platform): + m = TModule() + + m.submodules.forwarder = forwarder = Forwarder(self.method.data_out.layout) + + m.submodules.connect = ManyToOneConnectTrans( + get_results=[get for get in self.method_list], put_result=forwarder.write + ) + + self.method.proxy(m, forwarder.read) + + return m + + +class CatTrans(Elaboratable): + """Concatenating transaction. + + Concatenates the results of two methods and passes the result to the + third method. + """ + + def __init__(self, src1: Method, src2: Method, dst: Method): + """ + Parameters + ---------- + src1: Method + First input method. + src2: Method + Second input method. + dst: Method + The method which receives the concatenation of the results of input + methods. + """ + self.src1 = src1 + self.src2 = src2 + self.dst = dst + + def elaborate(self, platform): + m = TModule() + + with Transaction().body(m): + sdata1 = self.src1(m) + sdata2 = self.src2(m) + ddata = Record.like(self.dst.data_in) + self.dst(m, ddata) + + m.d.comb += ddata.eq(Cat(sdata1, sdata2)) + + return m + + +class ConnectAndTransformTrans(Elaboratable): + """Connecting transaction with transformations. + + Behaves like `ConnectTrans`, but modifies the transferred data using + functions or `Method`s. Equivalent to a combination of + `ConnectTrans` and `MethodTransformer`. The transformation + functions take two parameters, a `Module` and the `Record` being + transformed. + """ + + def __init__( + self, + method1: Method, + method2: Method, + *, + i_fun: Optional[Callable[[TModule, Record], RecordDict]] = None, + o_fun: Optional[Callable[[TModule, Record], RecordDict]] = None, + ): + """ + Parameters + ---------- + method1: Method + First method. + method2: Method + Second method, and the method being transformed. + i_fun: function or Method, optional + Input transformation (`method1` to `method2`). + o_fun: function or Method, optional + Output transformation (`method2` to `method1`). + """ + self.method1 = method1 + self.method2 = method2 + self.i_fun = i_fun or (lambda _, x: x) + self.o_fun = o_fun or (lambda _, x: x) + + def elaborate(self, platform): + m = TModule() + + m.submodules.transformer = transformer = MethodTransformer( + self.method2, + i_transform=(self.method1.data_out.layout, self.i_fun), + o_transform=(self.method1.data_in.layout, self.o_fun), + ) + m.submodules.connect = ConnectTrans(self.method1, transformer.method) + + return m