From ad3be1f4c7aa610ef56722f6d95802a3dad2bed1 Mon Sep 17 00:00:00 2001 From: David Lenfesty Date: Sun, 29 Jan 2023 20:38:32 -0700 Subject: [PATCH] gateware: put in some testing infrastructure It's pretty hacky tbh, probably should be improved. But also this will probably scale with the entire project so I don't care. --- gateware/i2c.py | 29 ++++++++++++++ gateware/main.py | 15 +++++++- gateware/test_i2c.py | 90 ++++++++++++++++++++++++++++++++++++++++++++ gateware/tests.py | 42 +++++++++++++++++++++ 4 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 gateware/test_i2c.py create mode 100644 gateware/tests.py diff --git a/gateware/i2c.py b/gateware/i2c.py index f3cd369..d6a6566 100644 --- a/gateware/i2c.py +++ b/gateware/i2c.py @@ -121,12 +121,14 @@ class I2C(Elaboratable): return m + from amaranth.lib.io import pin_layout i2c_layout = [ ("sda", pin_layout(1, "io")), ("scl", pin_layout(1, "io")), ] + class I2CBusSimulator(Elaboratable): def __init__(self): self.interfaces = [] @@ -188,8 +190,35 @@ class TestHarness(Elaboratable): return m +import unittest +import os +import sys + +import inspect + +from contextlib import nullcontext + +from typing import Generator + +from tests import BaseTestClass, provide_testcase_name + +class TestCSROperation(BaseTestClass): + def setUp(self): + self.harness = TestHarness() + + @provide_testcase_name + def test_send_byte(self, test_name): + def test(): + yield Tick() + + self._run_test(test, test_name) + + # TODO switch to unittest or something if __name__ == "__main__": + unittest.main() + + def write_csr(bus, index, data): yield bus.addr.eq(index) yield bus.w_stb.eq(1) diff --git a/gateware/main.py b/gateware/main.py index 388fe33..a50571a 100644 --- a/gateware/main.py +++ b/gateware/main.py @@ -9,10 +9,15 @@ from minerva.core import Minerva from typing import List from argparse import ArgumentParser +import unittest +import sys +import os from memory import * from led import * +import i2c +import test_i2c # To change clock domain of a module: # new_thing = DomainRenamer("new_clock")(MyElaboratable()) @@ -145,5 +150,11 @@ if __name__ == "__main__": colorlight_i9.Colorlight_i9_Platform().build(SoC(), debug_verilog=args.gen_debug_verilog) if args.test: - # TODO pass save_vcd arg through - run_sim() + if args.save_vcd: + os.environ.set("TEST_SAVE_VCD") + + # Super hacky... why am I doing this + test_modules = [mod for mod in sys.modules if mod.startswith("test_")] + for mod in test_modules: + unittest.main(module=mod, argv=[sys.argv[0]]) + diff --git a/gateware/test_i2c.py b/gateware/test_i2c.py new file mode 100644 index 0000000..18f84b4 --- /dev/null +++ b/gateware/test_i2c.py @@ -0,0 +1,90 @@ +from amaranth import * +from i2c import * +from amaranth.lib.io import pin_layout +from tests import BaseTestClass, provide_testcase_name + + +__all__ = ["i2c_layout", "I2CBusSimulator", "TestHarness", "TestCSROperation"] + + +class TestHarness(Elaboratable): + def __init__(self): + self.i2c = I2CBusSimulator() + + self.uut = I2C(10_000_000, 100_000, self.i2c.create_interface()) + self.i2c_target = I2CTarget(self.i2c.create_interface()) + + + def elaborate(self, platform): + assert platform is None + + m = Module() + + m.submodules.i2c = self.i2c + m.submodules.uut = self.uut + m.submodules.i2c_target = self.i2c_target + + m.d.comb += self.i2c_target.address.eq(0xAA >> 1) + + return m + + +class TestCSROperation(BaseTestClass): + def setUp(self): + self.harness = TestHarness() + + + @provide_testcase_name + def test_send_byte(self, test_name): + def test(): + yield Tick() + + self._run_test(test, test_name) + +i2c_layout = [ + ("sda", pin_layout(1, "io")), + ("scl", pin_layout(1, "io")), +] + + +class I2CBusSimulator(Elaboratable): + def __init__(self): + self.interfaces = [] + self.sda = Signal() + self.scl = Signal() + + + def elaborate(self, target): + assert target is None, "This bus simulator should never be used in real hardware!" + + n = len(self.interfaces) + + m = Module() + + m.d.comb += self.sda.eq(1) + m.d.comb += self.scl.eq(1) + + # TODO maybe output a bus contention signal? + # First interfaces get priority over interfaces added after + for i in reversed(range(n)): + # Emulate bus drivers + with m.If(self.interfaces[i].sda.oe): + m.d.comb += self.sda.eq(self.interfaces[i].sda.o) + with m.If(self.interfaces[i].scl.oe): + m.d.comb += self.scl.eq(self.interfaces[i].scl.o) + pass + + # Connect inputs to bus value + m.d.comb += [ + self.interfaces[i].sda.i.eq(self.sda), + self.interfaces[i].scl.i.eq(self.scl), + ] + + return m + + + def create_interface(self) -> Record: + new_interface = Record(i2c_layout) + self.interfaces.append(new_interface) + return new_interface + diff --git a/gateware/tests.py b/gateware/tests.py new file mode 100644 index 0000000..96db1ce --- /dev/null +++ b/gateware/tests.py @@ -0,0 +1,42 @@ +""" +Set of utilities to build a simple test suite. +""" +from amaranth import * +from amaranth.sim import * + +from typing import Generator +import unittest +import os + +from contextlib import nullcontext + +class BaseTestClass(unittest.TestCase): + """ + Base test class that provides a run_test helper function to do all the nice things. + """ + + def _run_test(self, test: Generator, name: str): + try: + sim = Simulator(self.harness) + except NameError: + raise NotImplementedError(f"Must define a self.harness module for TestCase {self.__class__.__name__}!") + + sim.add_clock(100e-9) + sim.reset() + + # Pretty hacky way to pass this info in but does it look like I care? + if os.environ.get("TEST_SAVE_VCD"): + ctx = sim.write_vcd(f"vcd_out/{name}.vcd") + else: + ctx = nullcontext() + + with ctx: + sim.add_sync_process(test) + + +def provide_testcase_name(fn): + """Decorator that provides a function with access to its own class and name.""" + def wrapper(self): + fn(self, f"{self.__class__.__name__}.{fn.__name__}") + + return wrapper