Repository: amaranth-lang/amaranth
Pull request #1109 (2024-02-12 14:12:13)

hdl.ir: add IOBufferInstance.

whitequark: We can change `back.rtlil` to escape leading `$` after this PR, right?
wanda-phi: > We can change `back.rtlil` to escape leading `$` after this PR, right?

We can change it right now, even. The PRs that got rid of `$dff` already enabled it, as the `$tribuf`s don't reach the RTLIL backend anyway.
whitequark: > We can change it right now, even.

Let's do that then. This may break some downstream code but I'm perfectly content with that.

Patch:
diff --git a/amaranth/build/plat.py b/amaranth/build/plat.py
index 834f5df..52afd37 100644
--- a/amaranth/build/plat.py
+++ b/amaranth/build/plat.py
@@ -9,6 +9,7 @@ import jinja2
 from .. import __version__
 from .._toolchain import *
 from ..hdl import *
+from ..hdl._ir import IOBufferInstance
 from ..hdl._xfrm import DomainLowerer
 from ..lib.cdc import ResetSynchronizer
 from ..back import rtlil, verilog
@@ -221,11 +222,10 @@ class Platform(ResourceManager, metaclass=ABCMeta):
                             valid_xdrs=(0,), valid_attrs=None)
 
         m = Module()
-        m.submodules += Instance("$tribuf",
-            p_WIDTH=pin.width,
-            i_EN=pin.oe,
-            i_A=self._invert_if(invert, pin.o),
-            o_Y=port,
+        m.submodules += IOBufferInstance(
+            pad=port,
+            o=self._invert_if(invert, pin.o),
+            oe=pin.oe,
         )
         return m
 
@@ -234,13 +234,14 @@ class Platform(ResourceManager, metaclass=ABCMeta):
                             valid_xdrs=(0,), valid_attrs=None)
 
         m = Module()
-        m.submodules += Instance("$tribuf",
-            p_WIDTH=pin.width,
-            i_EN=pin.oe,
-            i_A=self._invert_if(invert, pin.o),
-            o_Y=port,
+        i = Signal.like(pin.i)
+        m.submodules += IOBufferInstance(
+            pad=port,
+            i=i,
+            o=self._invert_if(invert, pin.o),
+            oe=pin.oe,
         )
-        m.d.comb += pin.i.eq(self._invert_if(invert, port))
+        m.d.comb += pin.i.eq(self._invert_if(invert, i))
         return m
 
     def get_diff_input(self, pin, port, attrs, invert):
diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py
index 43a9ed9..16c1371 100644
--- a/amaranth/hdl/_ir.py
+++ b/amaranth/hdl/_ir.py
@@ -11,7 +11,7 @@ from . import _ast, _cd, _ir, _nir
 
 __all__ = [
     "UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "Instance",
-    "PortDirection", "Design", "build_netlist",
+    "IOBufferInstance", "PortDirection", "Design", "build_netlist",
 ]
 
 
@@ -196,7 +196,7 @@ class Fragment:
                 # Always flatten subfragments that explicitly request it.
                 flatten_subfrags.add((subfrag, subfrag_hierarchy))
 
-            if isinstance(subfrag, (Instance, MemoryInstance)):
+            if isinstance(subfrag, (Instance, MemoryInstance, IOBufferInstance)):
                 # Never flatten instances.
                 continue
 
@@ -460,6 +460,36 @@ class Instance(Fragment):
                                 .format(kw, arg))
 
 
+class IOBufferInstance(Fragment):
+    def __init__(self, pad, *, i=None, o=None, oe=None, src_loc_at=0, src_loc=None):
+        super().__init__()
+
+        self.pad = _ast.Value.cast(pad)
+        if i is None:
+            self.i = None
+        else:
+            self.i = _ast.Value.cast(i)
+            if len(self.pad) != len(self.i):
+                raise ValueError(f"`pad` length ({len(self.pad)}) doesn't match `i` length ({len(self.i)})")
+        if o is None:
+            if oe is not None:
+                raise ValueError("`oe` must not be used if `o` is not used")
+            self.o = _ast.Const(0, len(self.pad))
+            self.oe = _ast.Const(0)
+        else:
+            self.o = _ast.Value.cast(o)
+            if len(self.pad) != len(self.o):
+                raise ValueError(f"`pad` length ({len(self.pad)}) doesn't match `o` length ({len(self.o)})")
+            if oe is None:
+                self.oe = _ast.Const(1)
+            else:
+                self.oe = _ast.Value.cast(oe)
+                if len(self.oe) != 1:
+                    raise ValueError(f"`oe` length ({len(self.oe)}) must be 1")
+
+        self.src_loc     = src_loc or tracer.get_src_loc(src_loc_at)
+
+
 class Design:
     """Represents a design ready for simulation or netlist building.
 
@@ -943,13 +973,15 @@ class NetlistEmitter:
         else:
             assert False # :nocov:
 
-    def emit_tribuf(self, module_idx: int, instance: _ir.Instance):
-        pad = self.emit_lhs(instance.named_ports["Y"][0])
-        o, _signed = self.emit_rhs(module_idx, instance.named_ports["A"][0])
-        (oe,), _signed = self.emit_rhs(module_idx, instance.named_ports["EN"][0])
+    def emit_iobuffer(self, module_idx: int, instance: _ir.IOBufferInstance):
+        pad = self.emit_lhs(instance.pad)
+        o, _signed = self.emit_rhs(module_idx, instance.o)
+        (oe,), _signed = self.emit_rhs(module_idx, instance.oe)
         assert len(pad) == len(o)
         cell = _nir.IOBuffer(module_idx, pad=pad, o=o, oe=oe, src_loc=instance.src_loc)
-        self.netlist.add_cell(cell)
+        value = self.netlist.add_value_cell(len(pad), cell)
+        if instance.i is not None:
+            self.connect(self.emit_lhs(instance.i), value, src_loc=instance.src_loc)
 
     def emit_memory(self, module_idx: int, fragment: '_mem.MemoryInstance', name: str):
         cell = _nir.Memory(module_idx,
@@ -1130,10 +1162,7 @@ class NetlistEmitter:
         fragment_name = self.design.fragment_names[fragment]
         if isinstance(fragment, _ir.Instance):
             assert parent_module_idx is not None
-            if fragment.type == "$tribuf":
-                self.emit_tribuf(parent_module_idx, fragment)
-            else:
-                self.emit_instance(parent_module_idx, fragment, name=fragment_name[-1])
+            self.emit_instance(parent_module_idx, fragment, name=fragment_name[-1])
         elif isinstance(fragment, _mem.MemoryInstance):
             assert parent_module_idx is not None
             memory = self.emit_memory(parent_module_idx, fragment, name=fragment_name[-1])
@@ -1142,6 +1171,9 @@ class NetlistEmitter:
                 write_ports.append(self.emit_write_port(parent_module_idx, fragment, port, memory))
             for port in fragment._read_ports:
                 self.emit_read_port(parent_module_idx, fragment, port, memory, write_ports)
+        elif isinstance(fragment, _ir.IOBufferInstance):
+            assert parent_module_idx is not None
+            self.emit_iobuffer(parent_module_idx, fragment)
         elif type(fragment) is _ir.Fragment:
             module_idx = self.netlist.add_module(parent_module_idx, fragment_name, src_loc=fragment.src_loc, cell_src_loc=cell_src_loc)
             signal_names = self.design.signal_names[fragment]
diff --git a/amaranth/hdl/_xfrm.py b/amaranth/hdl/_xfrm.py
index 2dbd266..8a02338 100644
--- a/amaranth/hdl/_xfrm.py
+++ b/amaranth/hdl/_xfrm.py
@@ -277,6 +277,23 @@ class FragmentTransformer:
             new_fragment = Instance(fragment.type, src_loc=fragment.src_loc)
             new_fragment.parameters = OrderedDict(fragment.parameters)
             self.map_named_ports(fragment, new_fragment)
+        elif isinstance(fragment, IOBufferInstance):
+            if hasattr(self, "on_value"):
+                new_fragment = IOBufferInstance(
+                    pad=self.on_value(fragment.pad),
+                    i=self.on_value(fragment.i) if fragment.i is not None else None,
+                    o=self.on_value(fragment.o),
+                    oe=self.on_value(fragment.oe),
+                    src_loc=fragment.src_loc,
+                )
+            else:
+                new_fragment = IOBufferInstance(
+                    pad=fragment.pad,
+                    i=fragment.i,
+                    o=fragment.o,
+                    oe=fragment.oe,
+                    src_loc=fragment.src_loc,
+                )
         else:
             new_fragment = Fragment(src_loc=fragment.src_loc)
             new_fragment.flatten = fragment.flatten


Test patch:
diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py
index 94251b6..a89879b 100644
--- a/tests/test_hdl_ir.py
+++ b/tests/test_hdl_ir.py
@@ -938,4 +938,124 @@ class OriginsTestCase(FHDLTestCase):
         del inst.origins
         elab = ElaboratesTo(inst)
         frag = Fragment.get(elab, platform=None)
-        self.assertFalse(hasattr(frag, "_origins"))
\ No newline at end of file
+        self.assertFalse(hasattr(frag, "_origins"))
+
+
+class IOBufferTestCase(FHDLTestCase):
+    def test_nir_i(self):
+        pad = Signal(4)
+        i = Signal(4)
+        f = Fragment()
+        f.add_subfragment(IOBufferInstance(pad, i=i))
+        nl = build_netlist(f, ports=[pad, i])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (inout 'pad' 0.2:6)
+                (output 'i' 1.0:4)
+            )
+            (cell 0 0 (top
+                (output 'i' 1.0:4)
+                (inout 'pad' 2:6)
+            ))
+            (cell 1 0 (iob 0.2:6 4'd0 0))
+        )
+        """)
+
+    def test_nir_o(self):
+        pad = Signal(4)
+        o = Signal(4)
+        f = Fragment()
+        f.add_subfragment(IOBufferInstance(pad, o=o))
+        nl = build_netlist(f, ports=[pad, o])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.6:10)
+                (inout 'pad' 0.2:6)
+            )
+            (cell 0 0 (top
+                (input 'o' 6:10)
+                (inout 'pad' 2:6)
+            ))
+            (cell 1 0 (iob 0.2:6 0.6:10 1))
+        )
+        """)
+
+    def test_nir_oe(self):
+        pad = Signal(4)
+        o = Signal(4)
+        oe = Signal()
+        f = Fragment()
+        f.add_subfragment(IOBufferInstance(pad, o=o, oe=oe))
+        nl = build_netlist(f, ports=[pad, o, oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.6:10)
+                (input 'oe' 0.10)
+                (inout 'pad' 0.2:6)
+            )
+            (cell 0 0 (top
+                (input 'o' 6:10)
+                (input 'oe' 10:11)
+                (inout 'pad' 2:6)
+            ))
+            (cell 1 0 (iob 0.2:6 0.6:10 0.10))
+        )
+        """)
+
+    def test_nir_io(self):
+        pad = Signal(4)
+        i = Signal(4)
+        o = Signal(4)
+        oe = Signal()
+        f = Fragment()
+        f.add_subfragment(IOBufferInstance(pad, i=i, o=o, oe=oe))
+        nl = build_netlist(f, ports=[pad, i, o, oe])
+        self.assertRepr(nl, """
+        (
+            (module 0 None ('top')
+                (input 'o' 0.6:10)
+                (input 'oe' 0.10)
+                (inout 'pad' 0.2:6)
+                (output 'i' 1.0:4)
+            )
+            (cell 0 0 (top
+                (output 'i' 1.0:4)
+                (input 'o' 6:10)
+                (input 'oe' 10:11)
+                (inout 'pad' 2:6)
+            ))
+            (cell 1 0 (iob 0.2:6 0.6:10 0.10))
+        )
+        """)
+
+    def test_wrong_i(self):
+        pad = Signal(4)
+        i = Signal()
+        with self.assertRaisesRegex(ValueError,
+                r"^`pad` length \(4\) doesn't match `i` length \(1\)"):
+            IOBufferInstance(pad, i=i)
+
+    def test_wrong_o(self):
+        pad = Signal(4)
+        o = Signal()
+        with self.assertRaisesRegex(ValueError,
+                r"^`pad` length \(4\) doesn't match `o` length \(1\)"):
+            IOBufferInstance(pad, o=o)
+
+    def test_wrong_oe(self):
+        pad = Signal(4)
+        o = Signal(4)
+        oe = Signal(4)
+        with self.assertRaisesRegex(ValueError,
+                r"^`oe` length \(4\) must be 1"):
+            IOBufferInstance(pad, o=o, oe=oe)
+
+    def test_wrong_oe_without_o(self):
+        pad = Signal(4)
+        oe = Signal()
+        with self.assertRaisesRegex(ValueError,
+                r"^`oe` must not be used if `o` is not used"):
+            IOBufferInstance(pad, oe=oe)