diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a9d7daa..cb591098 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,25 @@ jobs: python3 -m pip install -e .[test] pytest -m "switchboard" -x --verbose --durations=0 + cocotb_ci: + name: "Cocotb CI" + runs-on: ubuntu-latest + container: + image: ghcr.io/siliconcompiler/sc_runner:latest + timeout-minutes: 45 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: pytest + run: | + python3 -m venv venv + . venv/bin/activate + python3 -m pip install --upgrade pip + python3 -m pip install -e .[test] + pytest -m "cocotb" -x --verbose --durations=0 + python_ci: name: "Python + Tools CI" runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 4572491a..094130d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ urls = {Homepage = "https://github.com/zeroasiccorp/umi"} requires-python = ">= 3.9" license = {file = "LICENSE"} dependencies = [ - "siliconcompiler >= 0.35.0", + "siliconcompiler >= 0.36.5", "lambdalib >= 0.4.0, < 0.11.0" ] dynamic = ["version"] @@ -32,7 +32,10 @@ test = [ "pytest-xdist == 3.8.0", "pytest-timeout == 2.4.0", "flake8 == 7.3.0", - "switchboard-hw == 0.3.0" + "switchboard-hw == 0.3.1", + "cocotb==2.0.1", + "cocotb-bus==0.3.0", + "cocotbext-umi==0.0.2" ] [tool.check-wheel-contents] @@ -42,7 +45,8 @@ ignore = [ [tool.pytest.ini_options] markers = [ - "switchboard: this test requires switchboard to run" + "switchboard: this test requires switchboard to run", + "cocotb: this test requires cocotb to run" ] testpaths = [ "tests", diff --git a/tests/sumi/umi_stream/.gitignore b/tests/sumi/umi_stream/.gitignore new file mode 100644 index 00000000..b33ddd22 --- /dev/null +++ b/tests/sumi/umi_stream/.gitignore @@ -0,0 +1 @@ +!icarus_cmd_file.f diff --git a/tests/sumi/umi_stream/icarus_cmd_file.f b/tests/sumi/umi_stream/icarus_cmd_file.f new file mode 100644 index 00000000..3e26e00a --- /dev/null +++ b/tests/sumi/umi_stream/icarus_cmd_file.f @@ -0,0 +1 @@ ++timescale+1ns/1ps diff --git a/tests/sumi/umi_stream/run_cocotb_sim.py b/tests/sumi/umi_stream/run_cocotb_sim.py new file mode 100644 index 00000000..03e58729 --- /dev/null +++ b/tests/sumi/umi_stream/run_cocotb_sim.py @@ -0,0 +1,140 @@ +from siliconcompiler import Design, Sim +from siliconcompiler.flows.dvflow import DVFlow + +from siliconcompiler.tools.icarus.compile import CompileTask as IcarusCompileTask +from siliconcompiler.tools.icarus.cocotb_exec import CocotbExecTask as IcarusCocotbExecTask + +from siliconcompiler.tools.verilator.cocotb_compile import CocotbCompileTask as VerilatorCompileTask +from siliconcompiler.tools.verilator.cocotb_exec import CocotbExecTask as VerilatorCocotbExecTask + + +class IcarusDesign(Design): + def __init__(self, design: Design): + super().__init__() + + self.set_name(f"{design.name}_icarus_sim") + + self.set_dataroot("icarus_tb", __file__) + + with self.active_dataroot("icarus_tb"): + with self.active_fileset("icarus_sim"): + self.add_file("icarus_cmd_file.f", filetype="commandfile") + self.add_depfileset(design, "testbench.cocotb") + self.set_topmodule(design.get_topmodule("testbench.cocotb")) + + +class VerilatorDesign(Design): + def __init__(self, design: Design): + super().__init__() + + self.set_name(f"{design.name}_verilator_sim") + + self.set_dataroot("verilator_tb", __file__) + + with self.active_dataroot("verilator_tb"): + with self.active_fileset("verilator_sim"): + self.add_file("verilator_cmd_file.vc", filetype="commandfile") + self.add_depfileset(design, "testbench.cocotb") + self.set_topmodule(design.get_topmodule("testbench.cocotb")) + + +def load_cocotb_test( + design: Design, + simulator="icarus", + trace=True, + seed=None +): + + if simulator == "icarus": + load_cocotb_icarus_sim(design, trace=trace, seed=seed) + elif simulator == "verilator": + load_cocotb_verilator_sim(design, trace=trace, seed=seed, trace_type="vcd") + + +def load_cocotb_icarus_sim( + design: Design, + trace=True, + seed=None +): + project = Sim() + project.set_design(IcarusDesign(design)) + project.add_fileset("icarus_sim") + project.set_flow(DVFlow(tool="icarus-cocotb")) + + IcarusCompileTask.find_task(project).set_trace_enabled(trace) + + if seed is not None: + IcarusCocotbExecTask.find_task(project).set_cocotb_randomseed(seed) + + project.run() + project.summary() + + results = project.find_result( + step='simulate', + index='0', + directory="outputs", + filename="results.xml" + ) + if results: + print(f"\nCocotb results file: {results}") + + vcd = project.find_result( + step='simulate', + index='0', + directory="reports", + filename="tb_umi_stream.vcd" + ) + if vcd: + print(f"Waveform file: {vcd}") + + +def load_cocotb_verilator_sim( + design: Design, + trace=True, + seed=None, + trace_type="vcd" +): + project = Sim() + project.set_design(VerilatorDesign(design)) + project.add_fileset("verilator_sim") + project.set_flow(DVFlow(tool="verilator-cocotb")) + + # Enable waveform tracing (must be enabled on both compile and simulate tasks) + compile_task = VerilatorCompileTask.find_task(project) + compile_task.set_verilator_trace(trace) + compile_task.set_verilator_tracetype(trace_type) + + cocotb_task = VerilatorCocotbExecTask.find_task(project) + cocotb_task.set_cocotb_trace( + enable=trace, + trace_type=trace_type + ) + + # Optionally set a random seed for reproducibility + if seed is not None: + cocotb_task.set_cocotb_randomseed(seed) + + # Run the simulation + project.run() + project.summary() + + # Find and display the results file + results = project.find_result( + step='simulate', + index='0', + directory="outputs", + filename="results.xml" + ) + if results: + print(f"\nCocotb results file: {results}") + + # Find and display the waveform file + wave_ext = trace_type if trace_type in ("vcd", "fst") else "vcd" + wave = project.find_result( + step='simulate', + index='0', + directory="reports", + filename=f"adder.{wave_ext}" + ) + if wave: + print(f"Waveform file: {wave}") diff --git a/tests/sumi/umi_stream/test_umi_stream.py b/tests/sumi/umi_stream/test_umi_stream.py new file mode 100644 index 00000000..b19ee8fb --- /dev/null +++ b/tests/sumi/umi_stream/test_umi_stream.py @@ -0,0 +1,436 @@ +import os +import math +import random +import copy + +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import ClockCycles, Timer, Combine + +from cocotb_bus.drivers import BitDriver +from cocotb_bus.scoreboard import Scoreboard + +from cocotbext.umi.drivers.sumi_driver import SumiDriver +from cocotbext.umi.monitors.sumi_monitor import SumiMonitor +from cocotbext.umi.sumi import SumiCmd, SumiCmdType, SumiTransaction +from cocotbext.umi.tumi import TumiTransaction +from cocotbext.umi.utils import generators +from cocotbext.umi.utils.vrd_transaction import VRDTransaction + +from siliconcompiler import Design + +from umi.sumi.umi_stream.umi_stream import Stream + +from valid_ready_driver import ValidReadyDriver +from valid_ready_monitor import ValidReadyMonitor +from run_cocotb_sim import load_cocotb_test + + +###################################################### +# Test Environment +###################################################### + +class Env: + """Reusable test environment for umi_stream tests.""" + + def __init__(self, dut): + self.dut = dut + self.dw = int(dut.DW.value) + self.aw = int(dut.AW.value) + self.cw = int(dut.CW.value) + self.data_bytes = self.dw // 8 + self.full_size = int(math.log2(self.data_bytes)) + + self.umi_driver = None + self.umi_monitor = None + self.usi_source = None + self.usi_monitor = None + + self.expected_usi_output = [] + self.expected_umi_output = [] + self.scoreboard = None + + async def assert_reset(self, nreset, period_ns=50): + nreset.value = 1 + await Timer(1, unit="step") + nreset.value = 0 + await Timer(period_ns, unit="ns") + nreset.value = 1 + await Timer(period_ns, unit="ns") + + async def setup( + self, + umi_valid_gen, + usi_valid_gen, + umi_period_ns=10, + usi_period_ns=12 + ): + """Initialize signals, start clocks, apply reset, create drivers.""" + dut = self.dut + + # Initialize DUT inputs before drivers take over + dut.devicemode.value = 1 + dut.s2mm_dstaddr.value = 0 + dut.s2mm_srcaddr.value = 0 + dut.s2mm_cmd.value = 0 + dut.umi_in_valid.value = 0 + dut.umi_in_cmd.value = 0 + dut.umi_in_dstaddr.value = 0 + dut.umi_in_srcaddr.value = 0 + dut.umi_in_data.value = 0 + dut.umi_out_ready.value = 0 + dut.usi_in_valid.value = 0 + dut.usi_in_last.value = 0 + dut.usi_in_data.value = 0 + dut.usi_out_ready.value = 0 + + # Reset both clock domains + umi_reset_task = cocotb.start_soon(self.assert_reset(dut.umi_nreset, umi_period_ns*5)) + usi_reset_task = cocotb.start_soon(self.assert_reset(dut.usi_nreset, usi_period_ns*5)) + await Combine(umi_reset_task, usi_reset_task) + + # Start independent clocks for UMI and USI domains + Clock(dut.umi_clk, umi_period_ns, unit="ns").start() + await Timer(umi_period_ns * random.random(), unit="ns", round_mode="round") + Clock(dut.usi_clk, usi_period_ns, unit="ns").start() + + # Drivers + self.umi_driver = SumiDriver( + entity=dut, + name="umi_in", + clock=dut.umi_clk, + valid_generator=umi_valid_gen + ) + self.usi_source = ValidReadyDriver( + entity=dut, + name="usi_in", + clock=dut.usi_clk, + valid_generator=usi_valid_gen + ) + + # Monitors (passive -- do NOT drive ready signals) + self.umi_monitor = SumiMonitor(entity=dut, name="umi_out", clock=dut.umi_clk) + self.usi_monitor = ValidReadyMonitor(entity=dut, name="usi_out", clock=dut.usi_clk) + + # Scoreboard + self.expected_usi_output = [] + self.expected_umi_output = [] + self.scoreboard = Scoreboard(dut, fail_immediately=True) + self.scoreboard.add_interface( + monitor=self.usi_monitor, + expected_output=self.expected_usi_output + ) + self.scoreboard.add_interface( + monitor=self.umi_monitor, + expected_output=self.expected_umi_output + ) + + await ClockCycles(dut.umi_clk, 5) + + +################################################## +# Test DUT in device mode +################################################## + +@cocotb.test(timeout_time=100, timeout_unit="ms") +@cocotb.parametrize( + umi_valid_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + usi_valid_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + umi_ready_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + usi_ready_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + n_ops=[int(20 * float(os.getenv("RAND_TEST_LEN_SCALER", default=1)))] +) +async def test_device_mode( + dut, + umi_valid_gen=None, + usi_valid_gen=None, + umi_ready_gen=None, + usi_ready_gen=None, + n_ops=20 +): + """Device mode: posted writes, writes with ack, and reads with data verification.""" + + max_tumi_len = 500 + + env = Env(dut) + await env.setup(umi_valid_gen, usi_valid_gen) + dut.devicemode.value = 1 + + # Apply backpressure on UMI response consumer + if umi_ready_gen is not None: + BitDriver(signal=dut.umi_out_ready, clk=dut.umi_clk).start(generator=umi_ready_gen) + else: + dut.umi_out_ready.value = 1 + + # Apply backpressure on USI stream consumer + if usi_ready_gen is not None: + BitDriver(signal=dut.usi_out_ready, clk=dut.usi_clk).start(generator=usi_ready_gen) + else: + dut.usi_out_ready.value = 1 + + for _ in range(n_ops): + + ############################################################## + # Generate random command type for UMI transaction + ############################################################## + cmd_type = random.choice([ + SumiCmdType.UMI_REQ_POSTED, + SumiCmdType.UMI_REQ_WRITE, + SumiCmdType.UMI_REQ_READ + ]) + + sumi_trans = None + + ############################################################## + # Generate UMI transaction based on command type + ############################################################## + if cmd_type == SumiCmdType.UMI_REQ_READ: + """ + TODO: RTL can only accept reads that can responded to with a single beat of data. + This should probably be fixed. + """ + sumi_trans = [SumiTransaction( + cmd=SumiCmd.from_fields( + cmd_type=cmd_type, + size=env.full_size, + len=0, + eom=1 + ), + # TODO: DUT will accept any DST ADDR. Do we consider this an issue? + da=random.randint(0, (1 << len(dut.umi_in_dstaddr)) - 1), + sa=random.randint(0, (1 << len(dut.umi_in_srcaddr)) - 1), + data=random.randbytes(env.data_bytes), + addr_width=env.aw + )] + else: + # Create random tumi transaction + sumi_trans = TumiTransaction( + cmd=SumiCmd.from_fields(cmd_type=cmd_type), + da=random.randint(0, (1 << len(dut.umi_in_dstaddr)) - 1), + sa=random.randint(0, (1 << len(dut.umi_in_srcaddr)) - 1), + data=bytes(random.randbytes( + (random.randint(env.data_bytes, max_tumi_len) // env.data_bytes) * env.data_bytes + )), + ).to_sumi( + data_bus_size=env.data_bytes, + addr_width=env.aw + ) + + ############################################################## + # Add expected values to scoreboard based on command type + ############################################################## + if cmd_type == SumiCmdType.UMI_REQ_POSTED: + for t in sumi_trans: + # Add sumi trans to expected output + env.expected_usi_output.append(VRDTransaction( + data=t.data, + last=bool(int(t.cmd.eom)) + )) + elif cmd_type == SumiCmdType.UMI_REQ_WRITE: + for t in sumi_trans: + # Add sumi trans to expected output + env.expected_usi_output.append(VRDTransaction( + data=t.data, + last=bool(int(t.cmd.eom)) + )) + # Add response to UMI expected output + resp_cmd = copy.deepcopy(t.cmd) + resp_cmd.cmd_type.from_int(SumiCmdType.UMI_RESP_WRITE) + env.expected_umi_output.append(SumiTransaction( + cmd=resp_cmd, + da=int(t.sa), + # TODO: RTL hardcodes source address (This should be fixed and a expected value should be put here) + sa=0, + data=None, + )) + elif cmd_type == SumiCmdType.UMI_REQ_READ: + for t in sumi_trans: + data = random.randbytes(env.data_bytes) + # Add response to UMI expected output + resp_cmd = copy.deepcopy(t.cmd) + resp_cmd.cmd_type.from_int(SumiCmdType.UMI_RESP_READ) + env.expected_umi_output.append(SumiTransaction( + cmd=resp_cmd, + da=int(t.sa), + # TODO: RTL hardcodes source address (This should be fixed and a expected value should be put here) + sa=0, + data=data, + )) + env.usi_source.append(VRDTransaction( + data=data, + last=bool(int(t.cmd.eom)) + )) + + ############################################################## + # Add randomly generated UMI transaction to UMI driver + ############################################################## + for t in sumi_trans: + env.umi_driver.append(t) + + # Wait for all expected outputs to be consumed by scoreboards + while ( + len(env.expected_usi_output) != 0 + or len(env.expected_umi_output) != 0 + ): + await ClockCycles(dut.umi_clk, 1) + + # Check that scoreboard did not encounter any mismatches + raise env.scoreboard.result + + +@cocotb.test(timeout_time=100, timeout_unit="ms") +@cocotb.parametrize( + umi_valid_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + usi_valid_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + umi_ready_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + usi_ready_gen=[None, generators.random_toggle_generator(), generators.wave_generator()], + n_ops=[int(20 * float(os.getenv("RAND_TEST_LEN_SCALER", default=1)))] +) +async def test_non_device_mode( + dut, + umi_valid_gen=None, + usi_valid_gen=None, + umi_ready_gen=None, + usi_ready_gen=None, + n_ops=20 +): + """Non-device mode: full duplex with posted writes (MM2S) and stream-to-UMI (S2MM).""" + + max_tumi_len = 500 + + env = Env(dut) + await env.setup(umi_valid_gen, usi_valid_gen) + dut.devicemode.value = 0 + + # Apply backpressure on UMI output consumer + if umi_ready_gen is not None: + BitDriver(signal=dut.umi_out_ready, clk=dut.umi_clk).start(generator=umi_ready_gen) + else: + dut.umi_out_ready.value = 1 + + # Apply backpressure on USI stream consumer + if usi_ready_gen is not None: + BitDriver(signal=dut.usi_out_ready, clk=dut.usi_clk).start(generator=usi_ready_gen) + else: + dut.usi_out_ready.value = 1 + + async def mm2s_task(): + """MM2S: Posted writes from UMI input to USI output.""" + for _ in range(n_ops): + + ############################################################## + # Generate random multi-beat posted write TUMI transaction + ############################################################## + sumi_trans = TumiTransaction( + cmd=SumiCmd.from_fields(cmd_type=SumiCmdType.UMI_REQ_POSTED), + da=random.randint(0, (1 << len(dut.umi_in_dstaddr)) - 1), + sa=random.randint(0, (1 << len(dut.umi_in_srcaddr)) - 1), + data=bytes(random.randbytes( + (random.randint(env.data_bytes, max_tumi_len) // env.data_bytes) * env.data_bytes + )), + ).to_sumi( + data_bus_size=env.data_bytes, + addr_width=env.aw + ) + + ############################################################## + # Add expected USI output for each beat + ############################################################## + for t in sumi_trans: + env.expected_usi_output.append(VRDTransaction( + data=t.data, + last=bool(int(t.cmd.eom)) + )) + + ############################################################## + # Drive UMI posted writes + ############################################################## + for t in sumi_trans: + env.umi_driver.append(t) + + async def s2mm_task(): + """S2MM: Stream data from USI input to UMI output with randomized s2mm fields.""" + for _ in range(n_ops): + + ############################################################## + # Randomize external s2mm cmd/addr fields per transaction + ############################################################## + s2mm_cmd = SumiCmd.from_fields( + cmd_type=SumiCmdType.UMI_REQ_POSTED, + size=random.randint(0, env.full_size), + len=random.randint(0, 255), + eom=random.randint(0, 1) + ) + s2mm_da = random.randint(0, (1 << env.aw) - 1) + s2mm_sa = random.randint(0, (1 << env.aw) - 1) + + dut.s2mm_cmd.value = int(s2mm_cmd) + dut.s2mm_dstaddr.value = s2mm_da + dut.s2mm_srcaddr.value = s2mm_sa + + ############################################################## + # Push one random USI beat into S2MM FIFO + ############################################################## + data = random.randbytes(env.data_bytes) + last = random.choice([True, False]) + + env.usi_source.append(VRDTransaction(data=data, last=last)) + + ############################################################## + # Add expected UMI output using current s2mm field values + ############################################################## + env.expected_umi_output.append(SumiTransaction( + cmd=s2mm_cmd, + da=s2mm_da, + sa=s2mm_sa, + data=data[:s2mm_cmd.total_bytes()], + )) + + ############################################################## + # Wait for this entry to drain before changing s2mm fields + ############################################################## + while len(env.expected_umi_output) > 0: + await ClockCycles(dut.umi_clk, 1) + + mm2s = cocotb.start_soon(mm2s_task()) + s2mm = cocotb.start_soon(s2mm_task()) + await Combine(mm2s, s2mm) + + # Wait for all remaining expected outputs to be consumed by scoreboards + while ( + len(env.expected_usi_output) != 0 + or len(env.expected_umi_output) != 0 + ): + await ClockCycles(dut.umi_clk, 1) + + # Check that scoreboard did not encounter any mismatches + raise env.scoreboard.result + + +class TbDesign(Design): + + def __init__(self): + super().__init__() + + self.set_name("tb_umi_stream") + + self.set_dataroot("tb_umi_stream", __file__) + + with self.active_dataroot("tb_umi_stream"): + with self.active_fileset("testbench.cocotb"): + self.set_topmodule("umi_stream") + self.add_file("test_umi_stream.py", filetype="python") + self.add_depfileset(Stream(), "rtl") + + +@pytest.mark.cocotb +@pytest.mark.parametrize("simulator", ["icarus", "verilator"]) +def test_umi_stream(simulator): + load_cocotb_test( + design=TbDesign(), + simulator=simulator, + trace=False, + seed=None + ) diff --git a/tests/sumi/umi_stream/valid_ready_driver.py b/tests/sumi/umi_stream/valid_ready_driver.py new file mode 100644 index 00000000..10561bf1 --- /dev/null +++ b/tests/sumi/umi_stream/valid_ready_driver.py @@ -0,0 +1,77 @@ +from typing import Any + +from cocotb.types import LogicArray +from cocotb.triggers import RisingEdge +from cocotb.handle import SimHandleBase + +from cocotb_bus.drivers import ValidatedBusDriver + +from cocotbext.umi.utils.vrd_transaction import VRDTransaction + + +class ValidReadyDriver(ValidatedBusDriver): + + _signals = [ + "data", + "valid", + "ready" + ] + + _optional_signals = ["strb", "len", "last"] + + def __init__( + self, + entity: SimHandleBase, + name: str, + clock: SimHandleBase, + *, + config={}, + **kwargs: Any + ): + ValidatedBusDriver.__init__(self, entity, name, clock, **kwargs) + + self.clock = clock + self.bus.valid.value = 0 + + async def _driver_send(self, transaction: VRDTransaction, sync: bool = True) -> None: + """Implementation for BusDriver. + Args: + transaction: The transaction to send. + sync: Synchronize the transfer by waiting for a rising edge. + """ + + clk_re = RisingEdge(self.clock) + + if sync: + await clk_re + + # Insert a gap where valid is low + if not self.on: + self.bus.valid.value = 0 + for _ in range(self.off): + await clk_re + + # Grab the next set of on/off values + self._next_valids() + + # Consume a valid cycle + if self.on is not True and self.on: + self.on -= 1 + + def ready() -> bool: + return bool(self.bus.ready.value) + + while True: + self.bus.valid.value = 1 + self.bus.data.value = LogicArray.from_bytes(transaction.data, byteorder="little") + if hasattr(self.bus, "strb"): + self.bus.strb.value = LogicArray(transaction.strb) + if hasattr(self.bus, "len"): + self.bus.len.value = transaction.len + if hasattr(self.bus, "last"): + self.bus.last.value = transaction.last + await clk_re + if ready(): + break + + self.bus.valid.value = 0 diff --git a/tests/sumi/umi_stream/valid_ready_monitor.py b/tests/sumi/umi_stream/valid_ready_monitor.py new file mode 100644 index 00000000..d219c2de --- /dev/null +++ b/tests/sumi/umi_stream/valid_ready_monitor.py @@ -0,0 +1,32 @@ +from cocotb.triggers import RisingEdge + +from cocotb_bus.monitors import BusMonitor + +from cocotbext.umi.utils.vrd_transaction import VRDTransaction + + +class ValidReadyMonitor(BusMonitor): + + _signals = [ + "data", + "valid", + "ready" + ] + _optional_signals = ["last"] + + def __init__(self, entity, name, clock, **kwargs): + BusMonitor.__init__(self, entity, name, clock, **kwargs) + + async def _monitor_recv(self): + clk_re = RisingEdge(self.clock) + + def valid_handshake(): + return bool(self.bus.valid.value) and bool(self.bus.ready.value) + + while True: + await clk_re + if valid_handshake(): + self._recv(VRDTransaction( + data=self.bus.data.value.to_bytes(byteorder="little"), + last=bool(self.bus.last.value) if hasattr(self.bus, "last") else None + )) diff --git a/tests/sumi/umi_stream/verilator_cmd_file.vc b/tests/sumi/umi_stream/verilator_cmd_file.vc new file mode 100644 index 00000000..89b77f44 --- /dev/null +++ b/tests/sumi/umi_stream/verilator_cmd_file.vc @@ -0,0 +1 @@ +--timescale 1ns/1ps diff --git a/umi/sumi/__init__.py b/umi/sumi/__init__.py index fdda0e15..e07bdb26 100644 --- a/umi/sumi/__init__.py +++ b/umi/sumi/__init__.py @@ -13,7 +13,7 @@ from .umi_pipeline.umi_pipeline import Pipeline from .umi_ram.umi_ram import RAM from .umi_regif.umi_regif import Regif -from .umi_switch.umi_stream import Stream +from .umi_stream.umi_stream import Stream from .umi_switch.umi_switch import Switch from .umi_tester.umi_tester import Tester from .umi_unpack.umi_unpack import Unpack