From 19e926d5c3a208506e055ce78c8d14f0be806cfc Mon Sep 17 00:00:00 2001 From: whitequark Date: Mon, 3 Jun 2019 13:03:49 +0000 Subject: [PATCH] build.dsl: add support for connectors. --- nmigen/build/__init__.py | 2 +- nmigen/build/dsl.py | 74 +++++++++++++++++++++++++--- nmigen/test/test_build_dsl.py | 92 +++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 8 deletions(-) diff --git a/nmigen/build/__init__.py b/nmigen/build/__init__.py index e5b1596..21ed40b 100644 --- a/nmigen/build/__init__.py +++ b/nmigen/build/__init__.py @@ -1,2 +1,2 @@ -from .dsl import Pins, DiffPairs, Subsignal, Resource +from .dsl import Pins, DiffPairs, Subsignal, Resource, Connector from .plat import Platform, TemplatedPlatform diff --git a/nmigen/build/dsl.py b/nmigen/build/dsl.py index 222193a..0a0fb2b 100644 --- a/nmigen/build/dsl.py +++ b/nmigen/build/dsl.py @@ -1,17 +1,29 @@ -__all__ = ["Pins", "DiffPairs", "Subsignal", "Resource"] +from collections import OrderedDict + + +__all__ = ["Pins", "DiffPairs", "Subsignal", "Resource", "Connector"] class Pins: - def __init__(self, names, dir="io"): + def __init__(self, names, *, dir="io", conn=None): if not isinstance(names, str): raise TypeError("Names must be a whitespace-separated string, not {!r}" .format(names)) - self.names = names.split() + names = names.split() + + if conn is not None: + conn_name, conn_number = conn + if not (isinstance(conn_name, str) and isinstance(conn_number, int)): + raise TypeError("Connector must be None or a pair of string and integer, not {!r}" + .format(conn)) + names = ["{}_{}:{}".format(conn_name, conn_number, name) for name in names] if dir not in ("i", "o", "io"): raise TypeError("Direction must be one of \"i\", \"o\", \"oe\", or \"io\", not {!r}" .format(dir)) - self.dir = dir + + self.names = names + self.dir = dir def __len__(self): return len(self.names) @@ -19,14 +31,23 @@ class Pins: def __iter__(self): return iter(self.names) + def map_names(self, mapping, resource): + for name in self.names: + while ":" in name: + if name not in mapping: + raise NameError("Resource {!r} refers to nonexistent connector pin {}" + .format(resource, name)) + name = mapping[name] + yield name + def __repr__(self): return "(pins {} {})".format(self.dir, " ".join(self.names)) class DiffPairs: - def __init__(self, p, n, dir="io"): - self.p = Pins(p, dir=dir) - self.n = Pins(n, dir=dir) + def __init__(self, p, n, *, dir="io", conn=None): + self.p = Pins(p, dir=dir, conn=conn) + self.n = Pins(n, dir=dir, conn=conn) if len(self.p.names) != len(self.n.names): raise TypeError("Positive and negative pins must have the same width, but {!r} " @@ -105,3 +126,42 @@ class Resource(Subsignal): " ".join(map(repr, self.io)), " ".join("{}={}".format(k, v) for k, v in self.extras.items())) + + +class Connector: + def __init__(self, name, number, io): + self.name = name + self.number = number + self.mapping = OrderedDict() + + if isinstance(io, dict): + for conn_pin, plat_pin in io.items(): + if not isinstance(conn_pin, str): + raise TypeError("Connector pin name must be a string, not {!r}" + .format(conn_pin)) + if not isinstance(plat_pin, str): + raise TypeError("Platform pin name must be a string, not {!r}" + .format(plat_pin)) + self.mapping[conn_pin] = plat_pin + + elif isinstance(io, str): + for conn_pin, plat_pin in enumerate(io.split(), start=1): + if plat_pin == "-": + continue + self.mapping[str(conn_pin)] = plat_pin + + else: + raise TypeError("Connector I/Os must be a dictionary or a string, not {!r}" + .format(io)) + + def __repr__(self): + return "(connector {} {} {})".format(self.name, self.number, + " ".join("{}=>{}".format(conn, plat) + for conn, plat in self.mapping.items())) + + def __len__(self): + return len(self.mapping) + + def __iter__(self): + for conn_pin, plat_pin in self.mapping.items(): + yield "{}_{}:{}".format(self.name, self.number, conn_pin), plat_pin diff --git a/nmigen/test/test_build_dsl.py b/nmigen/test/test_build_dsl.py index a3f75a4..5288736 100644 --- a/nmigen/test/test_build_dsl.py +++ b/nmigen/test/test_build_dsl.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from ..build.dsl import * from .tools import * @@ -10,6 +12,27 @@ class PinsTestCase(FHDLTestCase): self.assertEqual(p.dir, "io") self.assertEqual(list(p), ["A0", "A1", "A2"]) + def test_conn(self): + p = Pins("0 1 2", conn=("pmod", 0)) + self.assertEqual(list(p), ["pmod_0:0", "pmod_0:1", "pmod_0:2"]) + + def test_map_names(self): + p = Pins("0 1 2", conn=("pmod", 0)) + mapping = { + "pmod_0:0": "A0", + "pmod_0:1": "A1", + "pmod_0:2": "A2", + } + self.assertEqual(list(p.map_names(mapping, p)), ["A0", "A1", "A2"]) + + def test_map_names_recur(self): + p = Pins("0", conn=("pmod", 0)) + mapping = { + "pmod_0:0": "ext_0:1", + "ext_0:1": "A1", + } + self.assertEqual(list(p.map_names(mapping, p)), ["A1"]) + def test_wrong_names(self): with self.assertRaises(TypeError, msg="Names must be a whitespace-separated string, not ['A0', 'A1', 'A2']"): @@ -20,6 +43,16 @@ class PinsTestCase(FHDLTestCase): msg="Direction must be one of \"i\", \"o\", \"oe\", or \"io\", not 'wrong'"): p = Pins("A0 A1", dir="wrong") + def test_wrong_map_names(self): + p = Pins("0 1 2", conn=("pmod", 0)) + mapping = { + "pmod_0:0": "A0", + } + with self.assertRaises(NameError, + msg="Resource (pins io pmod_0:0 pmod_0:1 pmod_0:2) refers to nonexistent " + "connector pin pmod_0:1"): + list(p.map_names(mapping, p)) + class DiffPairsTestCase(FHDLTestCase): def test_basic(self): @@ -30,6 +63,14 @@ class DiffPairsTestCase(FHDLTestCase): self.assertEqual(dp.dir, "io") self.assertEqual(list(dp), [("A0", "B0"), ("A1", "B1")]) + def test_conn(self): + dp = DiffPairs(p="0 1 2", n="3 4 5", conn=("pmod", 0)) + self.assertEqual(list(dp), [ + ("pmod_0:0", "pmod_0:3"), + ("pmod_0:1", "pmod_0:4"), + ("pmod_0:2", "pmod_0:5"), + ]) + def test_dir(self): dp = DiffPairs("A0", "B0", dir="o") self.assertEqual(dp.dir, "o") @@ -118,3 +159,54 @@ class ResourceTestCase(FHDLTestCase): " (subsignal tx (pins o A0) IOSTANDARD=LVCMOS33)" " (subsignal rx (pins i A1) IOSTANDARD=LVCMOS33)" " IOSTANDARD=LVCMOS33)") + + +class ConnectorTestCase(FHDLTestCase): + def test_string(self): + c = Connector("pmod", 0, "A0 A1 A2 A3 - - A4 A5 A6 A7 - -") + self.assertEqual(c.name, "pmod") + self.assertEqual(c.number, 0) + self.assertEqual(c.mapping, OrderedDict([ + ("1", "A0"), + ("2", "A1"), + ("3", "A2"), + ("4", "A3"), + ("7", "A4"), + ("8", "A5"), + ("9", "A6"), + ("10", "A7"), + ])) + self.assertEqual(list(c), [ + ("pmod_0:1", "A0"), + ("pmod_0:2", "A1"), + ("pmod_0:3", "A2"), + ("pmod_0:4", "A3"), + ("pmod_0:7", "A4"), + ("pmod_0:8", "A5"), + ("pmod_0:9", "A6"), + ("pmod_0:10", "A7"), + ]) + self.assertEqual(repr(c), + "(connector pmod 0 1=>A0 2=>A1 3=>A2 4=>A3 7=>A4 8=>A5 9=>A6 10=>A7)") + + def test_dict(self): + c = Connector("ext", 1, {"DP0": "A0", "DP1": "A1"}) + self.assertEqual(c.name, "ext") + self.assertEqual(c.number, 1) + self.assertEqual(c.mapping, OrderedDict([ + ("DP0", "A0"), + ("DP1", "A1"), + ])) + + def test_wrong_io(self): + with self.assertRaises(TypeError, + msg="Connector I/Os must be a dictionary or a string, not []"): + Connector("pmod", 0, []) + + def test_wrong_dict_key_value(self): + with self.assertRaises(TypeError, + msg="Connector pin name must be a string, not 0"): + Connector("pmod", 0, {0: "A"}) + with self.assertRaises(TypeError, + msg="Platform pin name must be a string, not 0"): + Connector("pmod", 0, {"A": 0}) -- 2.30.2