From 28fecec0593453dcb2159aeb049a3f0f02490479 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Fri, 25 Nov 2022 16:08:16 -0800 Subject: [PATCH 1/9] Versions => 3.0.dev1 --- SampleSitePdks/setup.py | 2 +- pdks/Asap7/setup.py | 2 +- pdks/Sky130/setup.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SampleSitePdks/setup.py b/SampleSitePdks/setup.py index 65f6cf5..aa382b9 100644 --- a/SampleSitePdks/setup.py +++ b/SampleSitePdks/setup.py @@ -15,7 +15,7 @@ # Get the long description from the README file long_description = (here / "readme.md").read_text(encoding="utf-8") -_VLSIR_VERSION = "3.0.dev0" +_VLSIR_VERSION = "3.0.dev1" setup( name="sitepdks", diff --git a/pdks/Asap7/setup.py b/pdks/Asap7/setup.py index 5ee129e..4f39647 100644 --- a/pdks/Asap7/setup.py +++ b/pdks/Asap7/setup.py @@ -15,7 +15,7 @@ # Get the long description from the README file long_description = (here / "readme.md").read_text(encoding="utf-8") -_VLSIR_VERSION = "3.0.dev0" +_VLSIR_VERSION = "3.0.dev1" setup( name="asap7-hdl21", diff --git a/pdks/Sky130/setup.py b/pdks/Sky130/setup.py index 8aa81f1..f607eec 100644 --- a/pdks/Sky130/setup.py +++ b/pdks/Sky130/setup.py @@ -15,7 +15,7 @@ # Get the long description from the README file long_description = (here / "readme.md").read_text(encoding="utf-8") -_VLSIR_VERSION = "3.0.dev0" +_VLSIR_VERSION = "3.0.dev1" setup( name="sky130-hdl21", diff --git a/setup.py b/setup.py index afd22fe..82ab817 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # Get the long description from the README file long_description = (here / "readme.md").read_text(encoding="utf-8") -_VLSIR_VERSION = "3.0.dev0" +_VLSIR_VERSION = "3.0.dev1" setup( name="hdl21", From 57b555bebaf2a8730abfd5672560f1ec5ff0f563 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Sat, 26 Nov 2022 20:39:47 -0800 Subject: [PATCH 2/9] Failing unit test for connected Port copying --- hdl21/tests/test_hdl21.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/hdl21/tests/test_hdl21.py b/hdl21/tests/test_hdl21.py index 1364848..060f858 100644 --- a/hdl21/tests/test_hdl21.py +++ b/hdl21/tests/test_hdl21.py @@ -738,6 +738,28 @@ class HasNoConn: assert len(HasNoConn.signals) == 1 +def test_elab_noconn2(): + """Slightly more elaborate test of elaborating a `NoConn`""" + + @h.module + class M3: # Layer 3: just a Port + p = h.Port() + + @h.module + class M2: # Layer 2: instantiate & connect M3 + p = h.Port() + m3 = M3(p=p) + + @h.module + class M1: # Layer 1: connect a `NoConn` to all that + p = h.NoConn() + m2 = M2(p=p) + + h.elaborate(M1) + + assert len(M1.signals) == 1 + + def test_bad_noconn(): """Test that a doubly-connected `NoConn` should fail""" From 0dc8ba590f32dd4022833b38ee06eb8f4be26db4 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Sat, 26 Nov 2022 20:41:57 -0800 Subject: [PATCH 3/9] Simplify & remove failure case fro Signal.__copy__ --- hdl21/signal.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/hdl21/signal.py b/hdl21/signal.py index 9220ba6..5127d81 100644 --- a/hdl21/signal.py +++ b/hdl21/signal.py @@ -83,12 +83,12 @@ def __hash__(self) -> bool: return hash(id(self)) def __copy__(self) -> "Signal": - # If this Signal has been connected to stuff, bail. - if self._slices or self._concats or self._connected_ports: - raise TabError - - # Create a new Signal with all of our public fields - rv = Signal( + """Signal copying implementation + Keeps "public" fields such as name and width, + while dropping "per-module" fields such as `_slices`.""" + # Notably `_parent_module` *is not* copied. + # It will generally be set when the copy is added to any new Module. + return Signal( name=self.name, width=self.width, vis=self.vis, @@ -97,13 +97,10 @@ def __copy__(self) -> "Signal": src=self.src, dest=self.dest, ) - # Copy the parent module by reference, i.e. as a pointer - rv._parent_module = self._parent_module - # And return the new Signal - return rv def __deepcopy__(self, _memo) -> "Signal": - # Signal "deep" copies are the same as shallow ones; there is no "deep" data being copied. + """Signal "deep" copies""" + # The same as shallow ones; there is no "deep" data being copied. return self.__copy__() From 7ee8810d9fd82ed1cf4bf6243980ba7bcda085d1 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Mon, 28 Nov 2022 10:19:03 -0800 Subject: [PATCH 4/9] Add xfailing test for directed bundle-ports without roles --- hdl21/tests/test_bundles.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/hdl21/tests/test_bundles.py b/hdl21/tests/test_bundles.py index 51b94d8..f5877c1 100644 --- a/hdl21/tests/test_bundles.py +++ b/hdl21/tests/test_bundles.py @@ -499,3 +499,34 @@ class HasHasDiff: h3 = HasDiff(d=h.AnonymousBundle(p=d.n, n=d.p)) h.elaborate(HasHasDiff) + + +@pytest.mark.xfail(reason="#68 https://github.com/dan-fritchman/Hdl21/issues/68") +def test_no_role_directions(): + """Test directions on Bundles without Roles""" + + @h.bundle + class B: + # A Bundle with directed Ports + a = h.Input() + b = h.Output() + c = h.Inout() + d = h.Port() + e = h.Signal() + + @h.module + class HasB: + b = B(port=True) + + @h.module + class HasHasB: + b = B() + h1 = HasB(b=b) + + h.elaborate(HasHasB) + + assert HasB.b_a.direction == h.PortDir.INPUT + assert HasB.b_b.direction == h.PortDir.OUTPUT + assert HasB.b_c.direction == h.PortDir.INPUT + assert HasB.b_d.direction == h.PortDir.NONE + assert HasB.b_e.direction == None From 22bf7ef8de79d6adf761dd5582fa2d05387d6f4d Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Mon, 28 Nov 2022 10:25:49 -0800 Subject: [PATCH 5/9] Improved error message in bundle flattening --- hdl21/elab/elaborators/flatten_bundles.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hdl21/elab/elaborators/flatten_bundles.py b/hdl21/elab/elaborators/flatten_bundles.py index 2f4517a..945b149 100644 --- a/hdl21/elab/elaborators/flatten_bundles.py +++ b/hdl21/elab/elaborators/flatten_bundles.py @@ -294,7 +294,7 @@ class Inner: ``` The bundle instances in `Outer` have also been flattened. - It now has signals named `outer_b_z`, `outer_b_y`, and `outer_b_z`, + It now has signals named `outer_b_x`, `outer_b_y`, and `outer_b_z`, which are stored in a `FlatBundleInst` keyed by `x`, `y`, and `z`. After this function it will (or should) look like: @@ -328,6 +328,12 @@ class Outer: # * In the example, its values will be `inner_b_x`, `inner_b_y`, and `inner_b_z`. # * Neither takes on the values `outer_b_x`, `outer_b_y`, and `outer_b_z`. # * These would be `flat.signals[pathstr].name` + if pathstr not in flat.signals: + msg = f"Missing connection to `{pathstr.val}` " + msg += f"in Connection to `{portname}` on Instance `{inst.name}`. " + msg += f"Has Signals `{[p.val for p in flat.signals.keys()]}`, " + msg += f"but no `{pathstr.val}`." + self.fail(msg) inst.connect(flat_port.name, flat.signals[pathstr]) def flatten_bundle_def(self, bundle: Bundle) -> FlatBundleDef: From aeaae377555147502ed5adaef625181638817d57 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Mon, 28 Nov 2022 18:04:56 -0800 Subject: [PATCH 6/9] Bundle flattening error improvements --- hdl21/elab/elaborators/flatten_bundles.py | 49 ++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/hdl21/elab/elaborators/flatten_bundles.py b/hdl21/elab/elaborators/flatten_bundles.py index 945b149..8d277c8 100644 --- a/hdl21/elab/elaborators/flatten_bundles.py +++ b/hdl21/elab/elaborators/flatten_bundles.py @@ -135,27 +135,6 @@ def signals(self): return self.root_scope.signals -def get( - flat: Union[FlatBundleDef, FlatBundleInst], path: Path -) -> Union[Signal, BundleScope]: - if isinstance(flat, FlatBundleDef): - ns = flat - elif isinstance(flat, FlatBundleInst): - ns = flat.root_scope - else: - raise TypeError - - for seg in path.segs: - seg = PathStr(seg) - if seg in ns.signals: - ns = ns.signals[seg] - elif seg in ns.scopes: - ns = ns.scopes[seg] - else: - raise ValueError(f"{flat} has no attribute {seg} in {ns}") - return ns - - class BundleFlattener(Elaborator): """Bundle-Flattening Elaborator Pass""" @@ -501,7 +480,7 @@ def resolve_bundleref(self, bref: BundleRef) -> Union[Signal, BundleScope]: msg = f"Invalid BundleRef to {bref.parent}" self.fail(msg) - bref.resolved = resolved = get(flat_root, Path(path)) + bref.resolved = resolved = self.resolve_path(flat_root, Path(path)) if isinstance(resolved, BundleScope): for connected_port in list(bref._connected_ports): @@ -516,7 +495,31 @@ def resolve_bundleref(self, bref: BundleRef) -> Union[Signal, BundleScope]: update_ref_deps(bref, resolved) return resolved - raise TypeError(f"BundleRef {bref} resolved to invalid {resolved}") + return self.fail(f"BundleRef {bref} resolved to invalid {resolved}") + + def resolve_path( + self, flat: Union[FlatBundleDef, FlatBundleInst], path: Path + ) -> Union[Signal, BundleScope]: + """Resolve `path` in the flattend bundle `flat`, producing either a Signal or nested BundleScope.""" + if isinstance(flat, FlatBundleDef): + ns = flat + elif isinstance(flat, FlatBundleInst): + ns = flat.root_scope + else: + msg = f"Invalid target for path resolution {flat} must be FlatBundleDef or FlatBundleInst" + return self.fail(msg) + + for seg in path.segs: + seg = PathStr(seg) + if seg in ns.signals: + ns = ns.signals[seg] + elif seg in ns.scopes: + ns = ns.scopes[seg] + else: + pathstr = ".".join(path.segs) + msg = f"Cannot resolve path `{pathstr}` in `{flat.src.name}`" + return self.fail(msg) + return ns def instances_and_arrays(module: Module) -> List[Instance]: From 2181c3795c6ebcc0eaafd72cfc2f0bbfd9dc71a1 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Mon, 28 Nov 2022 19:00:59 -0800 Subject: [PATCH 7/9] String-valued params use VLSIR literal instead of string --- hdl21/proto/from_proto.py | 8 +++++--- hdl21/proto/to_proto.py | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/hdl21/proto/from_proto.py b/hdl21/proto/from_proto.py index 788818a..f5aff3e 100644 --- a/hdl21/proto/from_proto.py +++ b/hdl21/proto/from_proto.py @@ -256,7 +256,9 @@ def import_parameters(pparams: List[vlsir.Param]) -> Dict[str, Any]: return {pparam.name: import_parameter_value(pparam.value) for pparam in pparams} -def import_parameter_value(pparam: vlsir.ParamValue) -> Any: +def import_parameter_value( + pparam: vlsir.ParamValue, +) -> Union[int, float, str, Prefixed]: """Import a `ParamValue`""" ptype = pparam.WhichOneof("value") if ptype == "integer": @@ -265,10 +267,10 @@ def import_parameter_value(pparam: vlsir.ParamValue) -> Any: return float(pparam.double) if ptype == "string": return str(pparam.string) + if ptype == "literal": + return str(pparam.literal) if ptype == "prefixed": return import_prefixed(pparam.prefixed) - if ptype == "literal": - raise NotImplementedError raise ValueError(f"Invalid Parameter Type: `{ptype}`") diff --git a/hdl21/proto/to_proto.py b/hdl21/proto/to_proto.py index 21c034b..598b70a 100644 --- a/hdl21/proto/to_proto.py +++ b/hdl21/proto/to_proto.py @@ -352,13 +352,13 @@ def export_param_value(val: ToVlsirParam) -> Optional[vlsir.ParamValue]: return None # `None` serves as the null identifier for "no value". if isinstance(val, str): - # FIXME: the debate between `string` and `literal`. For now, we use `string`. - return vlsir.ParamValue(string=val) + # FIXME: the debate between `string` and `literal`. For now, we use LITERAL. + return vlsir.ParamValue(literal=val) # Enum-valued parameters must also be strings, or fail if isinstance(val, Enum): if not isinstance(val.value, str): raise TypeError(f"Enum-valued parameters must be strings, not {val.value}") - return vlsir.ParamValue(string=val.value) + return vlsir.ParamValue(literal=val.value) # Numbers if isinstance(val, Prefixed): From 8aa631659db6cec6feb8d415c6dea9036b2416d9 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Mon, 28 Nov 2022 22:15:10 -0800 Subject: [PATCH 8/9] #66 - revitalize & start testing examples/ --- examples/__init__.py | 17 + examples/encoder.py | 170 +++++++++ {scratch/examples => examples}/rdac.py | 168 ++++----- {scratch/examples => examples}/ro.py | 98 ++--- examples/test_examples.py | 23 ++ readme.md | 61 ++-- scratch/amp.py | 140 -------- scratch/examples/pi.py | 221 ------------ scratch/examples/serdes.py | 477 ------------------------- 9 files changed, 380 insertions(+), 995 deletions(-) create mode 100644 examples/__init__.py create mode 100644 examples/encoder.py rename {scratch/examples => examples}/rdac.py (55%) rename {scratch/examples => examples}/ro.py (54%) create mode 100644 examples/test_examples.py delete mode 100644 scratch/amp.py delete mode 100644 scratch/examples/pi.py delete mode 100644 scratch/examples/serdes.py diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e1c1dad --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1,17 @@ +""" +# Examples "Package" +## `__init__.py`, the file that makes it a package (and does nothing else). + +This `examples/` folder isn't really designed to be used as a package, +but we derive plenty of benefit from `pytest`ing it, and moreover +from its test module being able to make relative imports like + +```python +from .ro import main # Only works in a package +``` + +Each `example` is *written* to be run as a script. +This shouldn't stop them from being run as such, +although it's always worth worrying about Python's demands for differences +between scripts and library modules, especially inasmuch as `import`s are concerned. +""" diff --git a/examples/encoder.py b/examples/encoder.py new file mode 100644 index 0000000..ed4bd64 --- /dev/null +++ b/examples/encoder.py @@ -0,0 +1,170 @@ +""" +# Logical Encoder Example + +A demonstration of: +* Using `ExternalModule`s of externally-defined logic cells +* Creating a *recursive* `Module` generator, for a parameteric-width one-hot encoder +""" + +import sys +import hdl21 as h + +""" +## Generic Logic Cells +In a more fully featured use-case, these would map to a target library. +""" + +Inv = h.ExternalModule( + name="Inv", + port_list=[ + h.Input(name="i"), + h.Output(name="z"), + ], + desc="Generic Inverter", +) +And2 = h.ExternalModule( + name="And2", + port_list=[ + h.Input(name="a"), + h.Input(name="b"), + h.Output(name="z"), + ], + desc="Generic 3-Input And Gate", +) +And3 = h.ExternalModule( + name="And3", + port_list=[ + h.Input(name="a"), + h.Input(name="b"), + h.Input(name="c"), + h.Output(name="z"), + ], + desc="Generic 3-Input And Gate", +) +TriInv = h.ExternalModule( + name="TriInv", + port_list=[ + h.Input(name="i"), + h.Input(name="en"), + h.Output(name="z"), + ], + desc="Generic Tri-State Inverter", +) +Flop = h.ExternalModule( + name="Flop", + port_list=[ + h.Input(name="d"), + h.Input(name="clk"), + h.Output(name="q"), + ], + desc="Generic Rising-Edge D Flip Flop", +) +FlopResetLow = h.ExternalModule( + name="FlopResetLow", + port_list=[ + h.Input(name="d"), + h.Input(name="clk"), + h.Input(name="rstn"), + h.Output(name="q"), + ], + desc="Rising-Edge D Flip Flop. Output low with reset asserted.", +) +FlopResetHigh = h.ExternalModule( + name="FlopResetHigh", + port_list=[ + h.Input(name="d"), + h.Input(name="clk"), + h.Input(name="rstn"), + h.Output(name="q"), + ], + desc="Rising-Edge D Flip Flop. Output high with reset asserted.", +) +Latch = h.ExternalModule( + name="Latch", + port_list=[ + h.Input(name="d"), + h.Input(name="clk"), + h.Output(name="q"), + ], + desc="Generic Active High Level-Sensitive Latch", +) + + +@h.paramclass +class Width: + """Parameter class for Generators with a single integer-valued `width` parameter.""" + + width = h.Param(dtype=int, desc="Parametric Width", default=1) + + +@h.module +class OneHotEncoder2to4: + """ + # One-Hot Encoder + 2b to 4b with enable. + Also serves as the base-case for the recursive `OneHotEncoder` generator. + All outputs are low if enable-input `en` is low. + """ + + # IO Interface + en = h.Input(width=1, desc="Enable input. Active high.") + bin = h.Input(width=2, desc="Binary valued input") + th = h.Output(width=4, desc="Thermometer encoded output") + + # Internal Contents + # Input inverters + binb = h.Signal(width=2, desc="Inverted binary input") + invs = 2 * Inv()(i=bin, z=binb) + + # The primary logic: a set of four And3's + ands = 4 * And3()( + a=h.Concat(binb[0], bin[0], binb[0], bin[0]), + b=h.Concat(binb[1], binb[1], bin[1], bin[1]), + c=en, + z=th, + ) + + +@h.generator +def OneHotEncoder(p: Width) -> h.Module: + """ + # One-Hot Encoder Generator + + Recursively creates a `p.width`-bit one-hot encoder Module comprised of `OneHotEncoder2to4`s. + Also generates `OneHotEncoder` Modules for `p.width-2`, `p.width-4`, et al, down to + the base case two to four bit Module. + """ + + if p.width < 2: + raise ValueError(f"OneHotEncoder {p} width must be > 1") + if p.width % 2: + raise ValueError(f"OneHotEncoder {p} width must be even") + if p.width == 2: # Base case: the 2 to 4b encoder + return OneHotEncoder2to4 + + # Recursive case. + m = h.Module() + m.en = h.Input(width=1, desc="Enable input. Active high.") + m.bin = h.Input(width=p.width, desc="Binary valued input") + m.th = h.Output(width=2**p.width, desc="Thermometer encoded output") + + # Thermo-encode the two MSBs, creating select signals for the LSBs + m.lsb_sel = h.Signal(width=4) + m.msb_encoder = OneHotEncoder2to4(en=m.en, bin=m.bin[-2:], th=m.lsb_sel) + + # Peel off two bits and recursively generate our child encoder Module + child = OneHotEncoder(width=p.width - 2) + # And create four instances of it, enabled by the thermo-decoded MSBs + m.children = 4 * child(en=m.lsb_sel, bin=m.bin[:-2], th=m.th) + + return m + + +def main(): + # "Main" Script Action + h.netlist(h.to_proto(OneHotEncoder(width=10)), dest=sys.stdout) + h.netlist(h.to_proto(OneHotEncoder(width=8)), dest=sys.stdout) + + +if __name__ == "__main__": + main() diff --git a/scratch/examples/rdac.py b/examples/rdac.py similarity index 55% rename from scratch/examples/rdac.py rename to examples/rdac.py index 2ba41da..c16e39c 100644 --- a/scratch/examples/rdac.py +++ b/examples/rdac.py @@ -1,17 +1,27 @@ +""" +# Resistor DAC Example + +An example of using `ExternalModule`s representing an implementation technology/ PDK +in a parametric resistive DAC generator. +""" + import sys -from typing import Dict, Any, Tuple, Sequence +from typing import Dict, Optional import hdl21 as h +from hdl21.prefix import µ, n @h.paramclass class PdkResistorParams: - w = h.Param(dtype=float, desc="Resistor width (m)") - l = h.Param(dtype=float, desc="Resistor length (m)") + """PDK Resistor Parameters""" + + w = h.Param(dtype=h.Prefixed, desc="Resistor width (m)") + l = h.Param(dtype=h.Prefixed, desc="Resistor length (m)") PdkResistor = h.ExternalModule( - name="rupolym_m", + name="pdk_resistor", desc="PDK Resistor", domain="RDAC's Favorite PDK", port_list=[h.Inout(name="PLUS"), h.Inout(name="MINUS")], @@ -21,11 +31,13 @@ class PdkResistorParams: @h.paramclass class PdkMosParams: - l = h.Param(dtype=float, desc="Channel length (m)") + """PDK MOSFET Parameters""" + + l = h.Param(dtype=h.Prefixed, desc="Channel length (m)") -NchMac = h.ExternalModule( - name="nch_mac", +Nch = h.ExternalModule( + name="nch", desc="PDK NMOS Transistor", port_list=[ h.Inout(name="D"), @@ -35,8 +47,8 @@ class PdkMosParams: ], paramtype=PdkMosParams, ) -PchMac = h.ExternalModule( - name="pch_mac", +Pch = h.ExternalModule( + name="pch", desc="PDK PMOS Transistor", port_list=[ h.Inout(name="D"), @@ -50,59 +62,75 @@ class PdkMosParams: @h.paramclass class RLadderParams: - nseg = h.Param(dtype=int, desc="Number of segments") - res = h.Param(dtype=h.ExternalModule, desc="Resistor Primitive") - res_params = h.Param(dtype=PdkResistorParams, desc="Resistor params") - res_conns = h.Param( - dtype=Dict[str, str], - desc="A dict mapping res ports to value P, N and any other res ports to pass through", + """# Resistor Ladder Parameters + + Note the use of the `hdl21.Instantiable` type for the unit resistor element. + This generally means "something we can make an `Instance` of" - most commonly a `Module`. + + Here we will be using a combination of an `ExternalModule` and its parameter-values, + in something like: + + ```python + RLadderParams( + res=PdkResistor(w=4 * µ, l=10 * µ) ) + ``` + """ + + nseg = h.Param(dtype=int, desc="Number of segments") + res = h.Param(dtype=h.Instantiable, desc="Unit resistor, with params applied") @h.generator def rladder(params: RLadderParams) -> h.Module: + """# Resistor Ladder Generator""" - # Base Module, we'll just declare IO @h.module class RLadder: + # IO top = h.Inout() bot = h.Inout() taps = h.Output(width=params.nseg - 1) - # A list of all the nets, from the bottom to the top - netnames = ( - [RLadder.bot] - + [RLadder.taps[i] for i in range(params.nseg - 1)] - + [RLadder.top] - ) - pconns = [ - k for k, v in params.res_conns.items() if v == "P" - ] # All the ports of res that connect to P - nconns = [ - k for k, v in params.res_conns.items() if v == "N" - ] # All the ports of res that connect to N - conns = params.res_conns.copy() # Make a copy we will mutate - for i, net in enumerate(netnames[:-1]): - for k in pconns: - conns[k] = netnames[i + 1] # Connect P to the P net for this resistor - for k in nconns: - conns[k] = netnames[i] # Connect N to the N net for this resistor - # Set each resistor of RLadder as an attibute by calling the generator then adding conns - RLadder.add(name=f"R{i}", val=params.res(params.res_params)(**conns)) + # Create concatenations for the P and N sides of each resistor + nsides = h.Concat(bot, taps) + psides = h.Concat(taps, top) + + # And create our unit resistor array + runits = params.nseg * params.res(PLUS=psides, MINUS=nsides) return RLadder @h.paramclass class PassGateParams: - nmos = h.Param(dtype=h.ExternalModule, desc="NMOS generator") - nmos_params = h.Param(dtype=PdkMosParams, desc="PMOS Parameters") - pmos = h.Param(dtype=h.ExternalModule, desc="PMOS generator") - pmos_params = h.Param(dtype=PdkMosParams, desc="PMOS parameters") + """# Pass Gate Parameters + + See the commentary on `RLadderParams` above regarding the use of `hdl21.Instantiable`, + which here serves as the parameter type for each transistor. It will generally be used like: + + ```python + PassGateParams( + nmos=Nch(PdkMosParams(l=1 * n)), + pmos=Pch(PdkMosParams(l=1 * n)), + ) + ``` + + Both `nmos` and `pmos` parameters are `Optional`, which means they can be set to the Python built-in `None` value. + If either is `None`, its "half" of the pass gate will be omitted. + Setting *both* to `None` will cause a generator exception. + """ + + nmos = h.Param(dtype=Optional[h.Instantiable], desc="NMOS. Disabled if None.") + pmos = h.Param(dtype=Optional[h.Instantiable], desc="PMOS. Disabled if None") @h.generator def passgate(params: PassGateParams) -> h.Module: + """# Pass Gate Generator""" + if params.nmos is None and params.pmos is None: + raise RuntimeError("A pass gate needs at least *one* transistor!") + @h.module class PassGate: source = h.Inout() @@ -111,14 +139,14 @@ class PassGate: if params.pmos is not None: PassGate.VDD = h.Inout() PassGate.en_b = h.Input() - PassGate.PSW = params.pmos(params.pmos_params)( + PassGate.PSW = params.pmos( D=PassGate.drain, S=PassGate.source, G=PassGate.en_b, B=PassGate.VDD ) if params.nmos is not None: PassGate.VSS = h.Inout() PassGate.en = h.Input() - PassGate.NSW = params.nmos(params.nmos_params)( + PassGate.NSW = params.nmos( D=PassGate.drain, S=PassGate.source, G=PassGate.en, B=PassGate.VSS ) @@ -127,6 +155,8 @@ class PassGate: @h.generator def mux(params: PassGateParams) -> h.Module: + """# Pass-Gate Analog Mux Generator""" + @h.module class Mux: sourceA = h.Input() @@ -158,6 +188,8 @@ class Mux: @h.paramclass class MuxTreeParams: + """# Mux Tree Parameters""" + nbit = h.Param(dtype=int, desc="Number of bits") mux_params = h.Param(dtype=PassGateParams, desc="Parameters for the MUX generator") @@ -210,60 +242,28 @@ class MuxTree: return MuxTree -def external_factory( - name: str, param_names: Sequence[str], port_names: Sequence[str] -) -> Tuple[h.ExternalModule, h.Param]: - """ - Parameters: - name: should correspond to the expected name in output netlists - param_names: A list of parameters instances of this external primitive take - port_names: A list of port names of this external primitive - Outputs: - NewPrimitive: An instance of h.Primitive - NewParamClass: An instance of h.paramclass - """ - param_attr_dict = { - n: h.Param(dtype=Any, desc=f"Gen parameter {n} of {name}") for n in param_names - } - # Dynamically create the parameter class, pass it to the h.paramclass decorator function - custom_params = h.paramclass(type(f"{name}Params", (), param_attr_dict)) - return ( - h.ExternalModule( - name, - desc=f"external_module_{name}", - port_list=[h.Inout(name=n) for n in port_names], - ), - custom_params, - ) - - def main(): """Main function, generating an `rladder` and `mux_tree` and netlisting each.""" + # Create parameter values for each of our top-level generators rparams = RLadderParams( nseg=15, - res=PdkResistor, - res_params=dict(w=4, l=10), - res_conns=dict(PLUS="P", MINUS="N"), + res=PdkResistor(w=4 * µ, l=10 * µ), ) mparams = MuxTreeParams( nbit=4, mux_params=PassGateParams( - nmos=NchMac, - nmos_params=PdkMosParams(l=1), - pmos=PchMac, - pmos_params=PdkMosParams(l=1), + nmos=Nch(PdkMosParams(l=1 * n)), + pmos=Pch(PdkMosParams(l=1 * n)), ), ) - # Convert each to VLSIR protobuf - proto = h.to_proto([rladder(rparams), mux_tree(mparams)], domain="rdac") - - # And netlist in a handful of formats - h.netlist(proto, sys.stdout, fmt="verilog") - h.netlist(proto, sys.stdout, fmt="spectre") - h.netlist(proto, sys.stdout, fmt="spice") - h.netlist(proto, sys.stdout, fmt="xyce") + # Netlist in a handful of formats + duts = [rladder(rparams), mux_tree(mparams)] + h.netlist(duts, sys.stdout, fmt="verilog") + h.netlist(duts, sys.stdout, fmt="spectre") + h.netlist(duts, sys.stdout, fmt="spice") + h.netlist(duts, sys.stdout, fmt="xyce") if __name__ == "__main__": diff --git a/scratch/examples/ro.py b/examples/ro.py similarity index 54% rename from scratch/examples/ro.py rename to examples/ro.py index 3ef9156..ae1d0bd 100644 --- a/scratch/examples/ro.py +++ b/examples/ro.py @@ -6,6 +6,8 @@ import sys import hdl21 as h +from hdl21.prefix import m +from hdl21.primitives import V, C # Sky130 Gated-Inverter Cell Declaration GatedInv = h.ExternalModule( @@ -47,18 +49,17 @@ class GatedInvWrapper: @h.paramclass class RoParams: + """# Ring Oscillator Parameters""" + stages = h.Param(dtype=int, default=3, desc="Number of stages") rows = h.Param(dtype=int, default=3, desc="Number of rows") - def __post_init_post_parse__(self): - if not self.stages % 2: - raise ValueError("stages must be odd") - @h.generator def Ro(params: RoParams) -> h.Module: - """Create a parametric oscillator""" - from hdl21.primitives import IdealCapacitor as C + """# Parametric ring oscillator generator""" + if not params.stages % 2: + raise ValueError("stages must be odd") ro = h.Module() ro.ctrlb = h.Input(width=params.stages * params.rows, desc="Control code") @@ -85,45 +86,50 @@ def Ro(params: RoParams) -> h.Module: return ro +@h.paramclass +class TbParams: + """# Ring Oscillator Testbench Parameters""" + + ro = h.Param(dtype=RoParams, default=RoParams(), desc="RO Parameters") + code = h.Param(dtype=int, default=3, desc="Control Code. Default: 3") + + @h.generator -def RoTb(params: RoParams) -> h.Module: - """Ring Osc Testbench - Primarily instantiates the RO and a thermometer-encoder for its control code. - - This module includes a bit of hacking into the SPICE-domain world. - It has no ports and is meant to be "the entirety" of a sim, save for analysis and control statements. - Don't try it on its own! At minimum, it relies on: - * SPICE/ Spectre's concept of "node zero", which it instantiates, and - * A SPICE-level parameter `code` which sets the integer control value. - This serves as an early case-study for how hdl21 modules and - these SPICE-native things can play together (and perhaps how they shouldn't). +def RoTb(params: TbParams) -> h.Module: + """ + # Ring Oscillator Testbench + Primarily instantiates the RO and a thermometer-encoded control code driver. """ - from hdl21.primitives import V - - # Create our module - m = h.Module() - # Add the DUT - m.ro = Ro(params)() - # Create signals around it - m.ro.ctrlb = m.ctrlb = h.Signal( - width=params.stages * params.rows, desc="Control code" - ) - m.ro.osc = m.osc = h.Signal(width=params.stages, desc="Primary Oscillator Output") - m.ro.VDD = m.VDD = h.Signal() - m.ro.VSS = vss = m.add(h.Signal(name="0")) - - # Create the supply - m.vvdd = V(V.Params(dc=1.8))(p=m.VDD, n=vss) - - # Now do some SPICE-fu to generate the control code, from a sweep parameter - for k in range(params.rows * params.stages): - v = V(V.Params(dc=f"1.8*(code <= {k})")) - m.add(name=f"vbin{k}b", val=v(p=m.ctrlb[k], n=vss)) - - # And don't forget to return em! - return m - - -# Netlist this stuff! -ppkg = h.to_proto([RoTb(RoParams())], domain="ro130") -netlist = h.netlist(pkg=ppkg, dest=sys.stdout) + + @h.module + class RoTb: + # The IO interface required for any testbench: a sole port for VSS. + VSS = h.Port() + + # Testbench Signals + ctrlb = h.Signal(width=params.ro.stages * params.ro.rows, desc="Control code") + osc = h.Signal(width=params.ro.stages, desc="Primary Oscillator Output") + VDD = h.Signal(desc="Power Supply") + + # Drive the supply + vvdd = V(dc=1800 * m)(p=VDD, n=VSS) + + # Instantiate our RO DUT + ro = Ro(params.ro)(osc=osc, ctrlb=ctrlb, VSS=VSS, VDD=VDD) + + # Now generate a thermometer-encoded control code + for k in range(params.ro.rows * params.ro.stages): + dc = 1800 * m if k <= params.code else 0 * m + RoTb.add(name=f"vctrl{k}b", val=V(dc=dc)(p=RoTb.ctrlb[k], n=RoTb.VSS)) + + # And don't forget to return the module! + return RoTb + + +def main(): + # Netlist this stuff! + h.netlist(RoTb(h.Default), dest=sys.stdout) + + +if __name__ == "__main__": + main() diff --git a/examples/test_examples.py b/examples/test_examples.py new file mode 100644 index 0000000..80693d4 --- /dev/null +++ b/examples/test_examples.py @@ -0,0 +1,23 @@ +""" +# Hdl21 Examples +## In-line Test Suite + +Generally runs the main function of each example "script". +Check for errors where we can, but largely just check for exceptions. +""" + +from .ro import main as ro_main +from .rdac import main as rdac_main +from .encoder import main as encoder_main + + +def test_ro_example(): + ro_main() + + +def test_rdac_example(): + rdac_main() + + +def test_encoder_example(): + encoder_main() diff --git a/readme.md b/readme.md index 658177d..6c6f4e1 100644 --- a/readme.md +++ b/readme.md @@ -18,7 +18,8 @@ Hdl21 generates hardware databases in the [VLSIR](https://github.com/Vlsir/Vlsir - [Primitive Elements](#primitives-and-external-modules) - [Process Technology (PDK) Packages](#process-technologies) - Coming Soon: Structured Connections with `Bundle`s -- Coming Soon: Schematics +- Coming Soon: [Schematics](https://github.com/Vlsir/Hdl21Schematics) +- [Examples Library](#examples-library) ## Modules @@ -331,7 +332,6 @@ from hdl21.prefix import e, µ These `e()` values are also most common in multiplication expressions, to create `Prefixed` values in "floating point" style such as `11 * e(-9)`. - ## Exporting and Importing Hdl21's primary import/ export format is [VLSIR](https://github.com/Vlsir/Vlsir). VLSIR is a binary ProtoBuf-based format with support for a variety of industry-standard formats and tools. The `hdl21.to_proto()` function converts an Hdl21 `Module` or group of `Modules` into VLSIR `Package`. The `hdl21.from_proto()` function similarly imports a VLSIR `Package` into a namespace of Hdl21 `Modules`. @@ -517,10 +517,10 @@ The `Primitive` type and all its valid values are defined by the `hdl21.primitiv | Bipolar | Bipolar Transistor | PHYSICAL | Bjt, BJT | c, b, e | | Diode | Diode | PHYSICAL | D | p, n | -Each primitive is available in the `hdl21.primitives` namespace, either through its full name or any of its aliases. Most primitives have fairly verbose names (e.g. `VoltageControlledCurrentSource`, `IdealResistor`), but also expose short-form aliases (e.g. `Vcvs`, `R`). Each of the aliases in Table 1 above refer to _the same_ Python object, i.e. +Each primitive is available in the `hdl21.primitives` namespace, either through its full name or any of its aliases. Most primitives have fairly verbose names (e.g. `VoltageControlledCurrentSource`, `IdealResistor`), but also expose short-form aliases (e.g. `Vcvs`, `R`). Each of the aliases in Table 1 above refer to _the same_ Python object, i.e. -```python -from hdl21.primitives import R, Res, IdealResistor +```python +from hdl21.primitives import R, Res, IdealResistor R is Res # evaluates to True R is IdealResistor # also evaluates to True @@ -578,7 +578,7 @@ class DiodePlus: Designing for a specific implementation technology (or "process development kit", or PDK) with Hdl21 can use either of (or a combination of) two routes: -- Instantiate `ExternalModules` corresponding to the target technology. These would commonly include its process-specific transistor and passive modules, and potentially larger cells, for example from a cell library. Such external modules are frequently defined as part of a PDK (python) package, but can also be defined anywhere else, including inline among Hdl21 generator code. +- Instantiate `ExternalModules` corresponding to the target technology. These would commonly include its process-specific transistor and passive modules, and potentially larger cells, for example from a cell library. Such external modules are frequently defined as part of a PDK (python) package, but can also be defined anywhere else, including inline among Hdl21 generator code. - Use `hdl21.Primitives`, each of which is designed to be a technology-independent representation of a primitive component. Moving to a particular technology then generally requires passing the design through an `hdl21.pdk` converter. Hdl21 PDKs are Python packages which generally include two primary elements: @@ -672,12 +672,11 @@ CmosCorner = TT | FF | SS | SF | FS Hdl21 exposes each of these corner-types as Python enumerations and combinations thereof. Each PDK package then defines its mapping from these `Corner` types to the content they include, typically in the form of external files. - ### PDK Installations and Sites -Much of the content of a typical process technology - even the subset that Hdl21 cares about - is not defined in Python. Transistor models and SPICE "library" files, such as those defining the `_nfet` and `_pfet` above, are common examples pertinent to Hdl21. Tech-files, layout libraries, and the like are similarly necessary for related pieces of EDA software. These PDK contents are commonly stored in a technology-specific arrangement of interdependent files. Hdl21 PDK packages structure this external content as a `PdkInstallation` type. +Much of the content of a typical process technology - even the subset that Hdl21 cares about - is not defined in Python. Transistor models and SPICE "library" files, such as those defining the `_nfet` and `_pfet` above, are common examples pertinent to Hdl21. Tech-files, layout libraries, and the like are similarly necessary for related pieces of EDA software. These PDK contents are commonly stored in a technology-specific arrangement of interdependent files. Hdl21 PDK packages structure this external content as a `PdkInstallation` type. -Each `PdkInstallation` is a runtime type-checked Python `dataclass` which extends the base `hdl21.pdk.PdkInstallation` type. Installations are free to define arbitrary fields and methods, which will be type-validated for each `Install` instance. Example: +Each `PdkInstallation` is a runtime type-checked Python `dataclass` which extends the base `hdl21.pdk.PdkInstallation` type. Installations are free to define arbitrary fields and methods, which will be type-validated for each `Install` instance. Example: ```python """ A sample PDK package with an `Install` type """ @@ -692,7 +691,7 @@ class Install(PdkInstallation): model_lib: Path # Filesystem `Path` to transistor models ``` -The name of each PDK's installation-type is by convention `Install` with a capital I. PDK packages which include an installation-type also conventionally include an `Install` instance named `install`, with a lower-case i. Code using the PDK package can then refer to the PDK's `install` attribute. Extending the example above: +The name of each PDK's installation-type is by convention `Install` with a capital I. PDK packages which include an installation-type also conventionally include an `Install` instance named `install`, with a lower-case i. Code using the PDK package can then refer to the PDK's `install` attribute. Extending the example above: ```python """ A sample PDK package with an `Install` type """ @@ -706,17 +705,17 @@ class Install(PdkInstallation): install: Optional[Install] = None # The active installation, if any ``` -The content of this installation data varies from site to site. To enable "site-portable" code to use the PDK installation, Hdl21 PDK users conventionally define a "site-specific" module or package which: +The content of this installation data varies from site to site. To enable "site-portable" code to use the PDK installation, Hdl21 PDK users conventionally define a "site-specific" module or package which: -* Imports the target PDK module -* Creates an instance of its `PdkInstallation` subtype -* Affixes that instance to the PDK package's `install` attribute +- Imports the target PDK module +- Creates an instance of its `PdkInstallation` subtype +- Affixes that instance to the PDK package's `install` attribute -For example: +For example: ```python # In "sitepdks.py" or similar -import mypdk +import mypdk mypdk.install = mypdk.Install( models = "/path/to/models", @@ -725,7 +724,7 @@ mypdk.install = mypdk.Install( ) ``` -These "site packages" are named `sitepdks` by convention. They can often be shared among several PDKs on a given filesystem. Hdl21 includes one built-in example such site-package, [SampleSitePdks](./SampleSitePdks/), which demonstrates setting up both built-in PDKs, Sky130 and ASAP7: +These "site packages" are named `sitepdks` by convention. They can often be shared among several PDKs on a given filesystem. Hdl21 includes one built-in example such site-package, [SampleSitePdks](./SampleSitePdks/), which demonstrates setting up both built-in PDKs, Sky130 and ASAP7: ```python # The built-in sample `sitepdks` package @@ -738,19 +737,19 @@ import asap7 asap7.install = asap7.Install(model_lib=Path("pdks") / "asap7" / ... / "TT.pm") ``` -"Site-portable" code requiring external PDK content can then refer to the PDK package's `install`, without being directly aware of its contents. +"Site-portable" code requiring external PDK content can then refer to the PDK package's `install`, without being directly aware of its contents. An example simulation using `mypdk`'s models with the `sitepdk`s defined above: -```python +```python # sim_my_pdk.py -import hdl21 as h +import hdl21 as h from hdl21.sim import Lib import sitepdks as _ # <= This sets up `mypdk.install` import mypdk @h.sim class SimMyPdk: - # A set of simulation input using `mypdk`'s installation + # A set of simulation input using `mypdk`'s installation tb = MyTestBench() models = Lib( path=mypdk.install.models, # <- Here @@ -763,15 +762,23 @@ SimMyPdk.run() Note that `sim_my_pdk.py` need not necessarily import or directly depend upon `sitepdks` itself. So long as `sitepdks` is imported and configures the PDK installation anywhere in the Python program, further code will be able to refer to the PDK's `install` fields. ---- +## Examples Library + +Hdl21's source tree includes a built-in [examples](./examples/) library. Each is designed to be a straightforward but realistic use-case. Each example is a self-contained Python program which can be run directly, e.g. with: + +```bash +python examples/rdac.py +``` + +Reading, copying, or cloning these example programs is generally among the best ways to get started. ## Why Use Python? -Custom IC design is a complicated field. Its practitioners have to know -[a](https://people.eecs.berkeley.edu/~boser/courses/240B/lectures/M07%20OTA%20II.pdf) | -[lot](http://rfic.eecs.berkeley.edu/~niknejad/ee142_fa05lects/pdf/lect24.pdf) | -[of](https://www.delroy.com/PLL_dir/ISSCC2004/PLLTutorialISSCC2004.pdf) | -[stuff](https://inst.eecs.berkeley.edu/~ee247/fa10/files07/lectures/L25_2_f10.pdf), +Custom IC design is a complicated field. Its practitioners have to know +[a](https://people.eecs.berkeley.edu/~boser/courses/240B/lectures/M07%20OTA%20II.pdf) | +[lot](http://rfic.eecs.berkeley.edu/~niknejad/ee142_fa05lects/pdf/lect24.pdf) | +[of](https://www.delroy.com/PLL_dir/ISSCC2004/PLLTutorialISSCC2004.pdf) | +[stuff](https://inst.eecs.berkeley.edu/~ee247/fa10/files07/lectures/L25_2_f10.pdf), independent of any programming background. Many have little or no programming experience at all. Python is reknowned for its accessibility to new programmers, largely attributable to its concise syntax, prototyping-friendly execution model, and thriving community. Moreover, Python has also become a hotbed for many of the tasks hardware designers otherwise learn programming for: numerical analysis, data visualization, machine learning, and the like. Hdl21 exposes the ideas they're used to - `Modules`, `Ports`, `Signals` - via as simple of a Python interface as it can. `Generators` are just functions. For many, this fact alone is enough to create powerfully reusable hardware. diff --git a/scratch/amp.py b/scratch/amp.py deleted file mode 100644 index 0ae9694..0000000 --- a/scratch/amp.py +++ /dev/null @@ -1,140 +0,0 @@ -import sys - -import hdl21 as h -from hdl21.diff import Diff - - -@h.paramclass -class AmpParams: - nmos = h.Param(dtype=h.Instantiable, desc="Nmos Module") - pmos = h.Param(dtype=h.Instantiable, desc="Pmos Module") - - -@h.generator -def FoldedCascode(params: AmpParams) -> h.Module: - """ Folded Cascode Op-Amp """ - - nmos, pmos = params.nmos, params.pmos - - @h.module - class FoldedCascode: - # Declare I/O - inp = Diff(port=True) - out = h.Output() - VDD, VSS, ibias = h.Inputs(3) - # Internal Signals - outm, s1, s2, g1 = h.Signals(4) - - # Bias Mirrors. Traditional cascode (for now). - # NMOS Gate Bias - nbias = Stack( - ncasc=nmos(g=ibias, d=ibias), # Top Cascode Diode - nsrc=nmos(g=s1, d=s1), # Bottom Bias Diode - ) - # PMOS Gate Bias - pbias = Stack( - psrc=pmos(g=s2, d=s2), # PMOS Source - pcasc=pmos(g=g1, d=g1), # PMOS Cascode - ncasc=nmos(g=nbias.ncasc.g), - nsrc=nmos(g=nbias.nsrc.g), - ) - # Cascode Stack - ostack = Diffn( - Stack( - psrc=2 * pmos(g=pbias.psrc.g), # PMOS Source - pcasc=pmos(g=pbias.pcasc.g, d=h.AnonymousBundle(p=out, n=outm)), - ncasc=nmos(g=nbias.ncasc.g, d=h.AnonymousBundle(p=out, n=outm)), - nsrc=nmos(g=outm), # NMOS Mirror - ) - ) - # Input Stack - istack = Stack( - # Differential Input Pair - nin=Diffn(nmos(g=inp, d=ostack.psrc.d)), - # Cascoded Bias - ncasc=2 * nmos(g=nbias.ncasc.g), # NMOS Cascode - nsrc=2 * nmos(g=nbias.nsrc.g), # NMOS Source - ) - # Matching Constraints (in addition to differential-ness) - Matched(nbias.ncasc, pbias.ncasc, ostack.ncasc, istack.ncasc) - Matched(nbias.nsrc, pbias.nsrc, ostack.nsrc, istack.nsrc) - - return FoldedCascode - - -@h.generator -def FiveXtorAmp(params: AmpParams) -> h.Module: - """ "Five-Transistor" Op-Amp """ - - nmos, pmos = params.nmos, params.pmos - - @h.module - class Amp: - # First declare our I/O - inp = Diff(port=True) - out = Diff(port=True) - VDD, VSS, ibias, vbias = h.Inputs(4) - - # Bias mirror - ndiode = nmos(g=ibias, d=ibias, s=VSS, b=VSS) - nibias = nmos(g=ibias, s=VSS, b=VSS) - # Input pair - ninp = nmos(g=inp.p, d=out.n, s=nibias.d, b=VSS) - ninn = nmos(g=inp.n, d=out.p, s=nibias.d, b=VSS) - # Output load - pldp = pmos(g=vbias, d=out.p, s=VDD, b=VDD) - pldm = pmos(g=vbias, d=out.n, s=VDD, b=VDD) - - return Amp - - -PdkNmos = h.ExternalModule( - name="PdkNmos", - desc="PDK NMOS Transistor", - port_list=[ - h.Inout(name="d"), - h.Inout(name="g"), - h.Inout(name="s"), - h.Inout(name="b"), - ], - paramtype=h.HasNoParams, -) -PdkPmos = h.ExternalModule( - name="PdkPmos", - desc="PDK PMOS Transistor", - port_list=[ - h.Inout(name="d"), - h.Inout(name="g"), - h.Inout(name="s"), - h.Inout(name="b"), - ], - paramtype=h.HasNoParams, -) - - -def Matched(*args, **kwargs): - ... - - -def Stack(*args, **kwargs): - ... - - -def Split(*args, **kwargs): - ... - - -def Diffn(*args, **kwargs): - ... - - -def main(): - """ Main function, generating and netlisting. """ - - params = AmpParams(nmos=PdkNmos(h.NoParams), pmos=PdkPmos(h.NoParams),) - proto = h.to_proto(h.elaborate(FiveXtorAmp(params))) - h.netlist(proto, sys.stdout) - - -if __name__ == "__main__": - main() diff --git a/scratch/examples/pi.py b/scratch/examples/pi.py deleted file mode 100644 index 69da906..0000000 --- a/scratch/examples/pi.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -# Phase Interpolator Example -""" - -from enum import Enum, auto - -import hdl21 as h - - -@h.bundle -class QuadClock: - """# Quadrature Clock Bundle - Includes four 90-degree-separated phases.""" - - class Roles(Enum): - # Clock roles: source or sink - SOURCE = auto() - SINK = auto() - - # The four quadrature phases, all driven by SOURCE and consumed by SINK. - ck0, ck90, ck180, ck270 = h.Signals(4, src=Roles.SOURCE, dest=Roles.SINK) - - -Inv = h.ExternalModule( - name="Inv", - port_list=[ - h.Input(name="i"), - h.Output(name="z"), - ], - desc="Generic Inverter", -) -TriInv = h.ExternalModule( - name="TriInv", - port_list=[ - h.Input(name="i"), - h.Input(name="en"), - h.Output(name="z"), - ], - desc="Generic Tri-State Inverter", -) -Flop = h.ExternalModule( - name="Flop", - port_list=[ - h.Input(name="d"), - h.Input(name="clk"), - h.Output(name="q"), - ], - desc="Generic Rising-Edge D Flip Flop", -) -Latch = h.ExternalModule( - name="Latch", - port_list=[ - h.Input(name="d"), - h.Input(name="clk"), - h.Output(name="q"), - ], - desc="Generic Active High Level-Sensitive Latch", -) - - -@h.paramclass -class WeightInvParams: - weight = h.Param(dtype=int, desc="Weight") - - -WeightInv = h.ExternalModule( - name="WeightInv", - port_list=[ - h.Input(name="i"), - h.Output(name="z"), - ], - desc="Weighting Inverter", - paramtype=WeightInvParams, -) - - -@h.paramclass -class PhaseWeighterParams: - wta = h.Param(dtype=int, desc="Weight of Input A") - wtb = h.Param(dtype=int, desc="Weight of Input B") - - -@h.generator -def PhaseWeighter(p: PhaseWeighterParams) -> h.Module: - """# Phase-Weighter - Drives a single output with two out-of-phase inputs `a` and `b`, - with weights dictates by params `wta` and `wtb`.""" - - @h.module - class PhaseWeighter: - # IO Ports - a, b = h.Inputs(2) - out = h.Output() - mix = h.Signal(desc="Internal phase-mixing node") - - # Give it a shorthand name for these manipulations - P = PhaseWeighter - - if p.wta > 0: # a-input inverter - P.inva = WeightInv(WeightInvParams(weight=p.wta))(i=P.a, z=P.mix) - if p.wtb > 0: # b-input inverter - P.invb = WeightInv(WeightInvParams(weight=p.wtb))(i=P.b, z=P.mix) - - # Output inverter, with the combined size of the two inputs - P.invo = WeightInv(WeightInvParams(weight=p.wta + p.wtb))(i=P.mix, z=P.out) - - return PhaseWeighter - - -@h.paramclass -class PiParams: - """Phase Interpolator Parameters""" - - nbits = h.Param(dtype=int, default=5, desc="Resolution, or width of select-input.") - - -@h.generator -def PhaseGenerator(p: PiParams) -> h.Module: - """# Phase Generator (Generator) (Get it?) - - Takes a primary input `QuadClock` and interpolates to produce - an array of equally-spaced output phases.""" - - PhaseGen = h.Module() - ckq = PhaseGen.ckq = QuadClock( - role=QuadClock.Roles.SINK, port=True, desc="Quadrature input" - ) - phases = PhaseGen.phases = h.Output( - width=2**p.nbits, desc="Array of equally-spaced phases" - ) - - if p.nbits != 5: - msg = f"Yeah we know that's a parameter, but this is actually hard-coded to 5 bits for now" - raise ValueError(msg) - - # Generate a set of PhaseWeighters and output phases for each pair of quadrature inputs - for wtb in range(8): - p = PhaseWeighterParams(wta=7 - wtb, wtb=wtb) - index = wtb - PhaseGen.add( - name=f"weight{index}", - val=PhaseWeighter(p)(a=ckq.ck0, b=ckq.ck90, out=phases[index]), - ) - for wtb in range(8): - p = PhaseWeighterParams(wta=7 - wtb, wtb=wtb) - index = 8 + wtb - PhaseGen.add( - name=f"weight{index}", - val=PhaseWeighter(p)(a=ckq.ck90, b=ckq.ck180, out=phases[index]), - ) - for wtb in range(8): - p = PhaseWeighterParams(wta=7 - wtb, wtb=wtb) - index = 16 + wtb - PhaseGen.add( - name=f"weight{index}", - val=PhaseWeighter(p)(a=ckq.ck180, b=ckq.ck270, out=phases[index]), - ) - for wtb in range(8): - p = PhaseWeighterParams(wta=7 - wtb, wtb=wtb) - index = 24 + wtb - PhaseGen.add( - name=f"weight{index}", - val=PhaseWeighter(p)(a=ckq.ck270, b=ckq.ck0, out=phases[index]), - ) - - return PhaseGen - - -@h.generator -def OneHotEncode5to32(_: h.HasNoParams) -> h.Module: - """5 to 32b One-Hot Encoder""" - m = h.Module() - m.inp = h.Input(width=5, desc="Binary-Encoded Input") - m.out = h.Output(width=32, desc="One-Hot-Encoded Output") - # FIXME! the actual contents - return m - - -@h.generator -def PhaseSelector(p: PiParams) -> h.Module: - """# Phase Selector Mux""" - - @h.module - class PhaseSelector: - # IO Interface - phases = h.Input(width=2**p.nbits, desc="Array of equally-spaced phases") - sel = h.Input(width=p.nbits, desc="Selection input") - out = h.Output(width=1, desc="Clock output") - - # Internal Contents - encoder = OneHotEncode5to32()(inp=sel) - invs = 32 * TriInv()(i=phases, en=encoder.out, z=out) - - return PhaseSelector - - -@h.generator -def PhaseInterp(p: PiParams) -> h.Module: - """Phase Interpolator Generator""" - - @h.module - class PhaseInterp: - # IO Interface - ckq = QuadClock(role=QuadClock.Roles.SINK, port=True, desc="Quadrature input") - sel = h.Input(width=p.nbits, desc="Selection input") - out = h.Output(width=1, desc="Clock output") - - # Internal Signals - phases = h.Signal(width=2**p.nbits, desc="Array of equally-spaced phases") - - # Instantiate the phase-generator and phase-selector - phgen = PhaseGenerator(p)(ckq=ckq, phases=phases) - phsel = PhaseSelector(p)(phases=phases, sel=sel, out=out) - - return PhaseInterp - - -# "Main" Script Content -import sys - -h.netlist(h.to_proto(PhaseInterp()), dest=sys.stdout) diff --git a/scratch/examples/serdes.py b/scratch/examples/serdes.py deleted file mode 100644 index 860d633..0000000 --- a/scratch/examples/serdes.py +++ /dev/null @@ -1,477 +0,0 @@ -""" -Serial Link Transmit & Receive Example -""" - -from enum import Enum, auto -import hdl21 as h - -import sitepdks as _ - - -Inv = h.ExternalModule( - name="Inv", - port_list=[ - h.Input(name="i"), - h.Output(name="z"), - ], - desc="Generic Inverter", -) -And2 = h.ExternalModule( - name="And2", - port_list=[ - h.Input(name="a"), - h.Input(name="b"), - h.Output(name="z"), - ], - desc="Generic 3-Input And Gate", -) -And3 = h.ExternalModule( - name="And3", - port_list=[ - h.Input(name="a"), - h.Input(name="b"), - h.Input(name="c"), - h.Output(name="z"), - ], - desc="Generic 3-Input And Gate", -) -TriInv = h.ExternalModule( - name="TriInv", - port_list=[ - h.Input(name="i"), - h.Input(name="en"), - h.Output(name="z"), - ], - desc="Generic Tri-State Inverter", -) -Flop = h.ExternalModule( - name="Flop", - port_list=[ - h.Input(name="d"), - h.Input(name="clk"), - h.Output(name="q"), - ], - desc="Generic Rising-Edge D Flip Flop", -) -FlopResetLow = h.ExternalModule( - name="FlopResetLow", - port_list=[ - h.Input(name="d"), - h.Input(name="clk"), - h.Input(name="rstn"), - h.Output(name="q"), - ], - desc="Rising-Edge D Flip Flop. Output low with reset asserted.", -) -FlopResetHigh = h.ExternalModule( - name="FlopResetHigh", - port_list=[ - h.Input(name="d"), - h.Input(name="clk"), - h.Input(name="rstn"), - h.Output(name="q"), - ], - desc="Rising-Edge D Flip Flop. Output high with reset asserted.", -) -Latch = h.ExternalModule( - name="Latch", - port_list=[ - h.Input(name="d"), - h.Input(name="clk"), - h.Output(name="q"), - ], - desc="Generic Active High Level-Sensitive Latch", -) - - -@h.bundle -class Diff: - """Differential Bundle""" - - class Roles(Enum): - SOURCE = auto() - SINK = auto() - - p, n = h.Signals(2, src=Roles.SOURCE, dest=Roles.SINK) - - -@h.paramclass -class Width: - """Parameter class for Generators with a single integer-valued `width` parameter.""" - - width = h.Param(dtype=int, desc="Parametric Width", default=1) - - -@h.module -class OneHotEncoder2to4: - """ - # One-Hot Encoder - 2b to 4b with enable. - Also serves as the base-case for the recursive `OneHotEncoder` generator. - All outputs are low if enable-input `en` is low. - """ - - # IO Interface - en = h.Input(width=1, desc="Enable input. Active high.") - bin = h.Input(width=2, desc="Binary valued input") - th = h.Output(width=4, desc="Thermometer encoded output") - - # Internal Contents - # Input inverters - binb = h.Signal(width=2, desc="Inverted binary input") - invs = 2 * Inv()(i=bin, z=binb) - - # The primary logic: a set of four And3's - ands = 4 * And3()( - a=h.Concat(binb[0], bin[0], binb[0], bin[0]), - b=h.Concat(binb[1], binb[1], bin[1], bin[1]), - c=en, - z=th, - ) - - -@h.generator -def OneHotEncoder(p: Width) -> h.Module: - """ - # One-Hot Encoder Generator - Recursively creates a `p.width`-bit one-hot encoder Module comprised of `OneHotEncoder2to4`s. - Also generates `OneHotEncoder` Modules for `p.width-2`, `p.width-4`, et al, down to - the base case two to four bit Module. - """ - - if p.width < 2: - raise ValueError(f"OneHotEncoder {p} width must be > 1") - if p.width % 2: - # FIXME: even number of bits only. Add odd cases, eventually - raise ValueError(f"OneHotEncoder {p} width must be even") - if p.width == 2: # Base case: the 2 to 4b encoder - return OneHotEncoder2to4 - - # Recursive case. - m = h.Module() - m.en = h.Input(width=1, desc="Enable input. Active high.") - m.bin = h.Input(width=p.width, desc="Binary valued input") - m.th = h.Output(width=2**p.width, desc="Thermometer encoded output") - - # Thermo-encode the two MSBs, creating select signals for the LSBs - m.lsb_sel = h.Signal(width=4) - m.msb_encoder = OneHotEncoder2to4(en=m.en, bin=m.bin[-2:], th=m.lsb_sel) - - # Peel off two bits and recursively generate our child encoder Module - child = OneHotEncoder(width=p.width - 2) - # And create four instances of it, enabled by the thermo-decoded MSBs - m.children = 4 * child(en=m.lsb_sel, bin=m.bin[:-2], th=m.th) - - return m - - -@h.generator -def OneHotMux(p: Width) -> h.Module: - """# One-Hot Selected Mux""" - - m = h.Module() - - # IO Interface - m.inp = h.Input(width=p.width, desc="Primary Input") - m.sel = h.Input(width=p.width, desc="One-Hot Encoded Selection Input") - m.out = h.Output(width=1) - - # Internal Implementation - # Flat bank of `width` tristate inverters - m.invs = p.width * TriInv()(i=m.inp, en=m.sel, z=m.out) - return m - - -@h.generator -def Counter(p: Width) -> h.Module: - """# Binary Counter Generator""" - - m = h.Module() - m.clk = h.Input(desc="Primary input. Increments state on each rising edge.") - m.out = h.Output(width=p.width, desc="Counterer Output State") - - # Divide-by-two stages - m.invs = p.width * Inv()(i=m.out) - m.flops = p.width * Flop()(d=m.invs.z, q=m.out, clk=m.clk) - return m - - -@h.generator -def OneHotRotator(p: Width) -> h.Module: - """# One Hot Rotator - A set of `p.width` flops with a one-hot state, - which rotates by one bit on each clock cycle.""" - - m = h.Module() - # IO Interface: Clock, Reset, and a `width`-bit One-Hot output - m.sclk = h.Input(width=1, desc="Serial clock") - m.rstn = h.Input(width=1, desc="Active-low reset") - m.out = h.Output(width=p.width, desc="One-hot rotating output") - - m.outb = h.Signal(width=p.width, desc="Internal complementary outputs") - m.out_invs = p.width * Inv()(i=m.out, z=m.outb) - m.nxt = h.Signal(width=p.width, desc="Next state; D pins of state flops") - - # The core logic: each next-bit is the AND of the prior bit, and the inverse of the current bit. - m.ands = p.width * And2()(a=m.outb, b=h.Concat(m.out[1:], m.out[0]), z=m.nxt) - # LSB flop, output asserted while in reset - m.lsb_flop = FlopResetHigh()(d=m.nxt[0], clk=m.sclk, q=m.out[0], rstn=m.rstn) - # All other flops, outputs de-asserted in reset - m.flops = (p.width - 1) * FlopResetLow()( - d=m.nxt[1:], clk=m.sclk, q=m.out[1:], rstn=m.rstn - ) - - return m - - -@h.generator -def TxSerializer(_: h.HasNoParams) -> h.Module: - """Transmit Serializer - Includes parallel-clock generation divider""" - - m = h.Module() - m.pdata = h.Input(width=16, desc="Parallel Input Data") - m.sdata = h.Output(width=1, desc="Serial Output Data") - m.pclk = h.Output(width=1, desc="*Output*, Divided Parallel Clock") - m.sclk = h.Input(width=1, desc="Input Serial Clock") - - # Create the four-bit counter state, consisting of the parallel clock as MSB, - # and three internal-signal LSBs. - m.count_lsbs = h.Signal(width=3, desc="LSBs of Divider-Counterer") - count = h.Concat(m.count_lsbs, m.pclk) - - m.counter = Counter(width=4)(clk=m.sclk, out=count) - m.enable_encoder = h.Signal(desc="FIXME: tie this high!") - m.encoder = OneHotEncoder(width=4)(bin=count, en=m.enable_encoder) - m.mux = OneHotMux(width=16)(inp=m.pdata, out=m.sdata, sel=m.encoder.th) - - return m - - -@h.generator -def RxDeSerializer(_: h.HasNoParams) -> h.Module: - """RX De-Serializer - Includes parallel-clock generation divider""" - - m = h.Module() - m.pdata = h.Output(width=16, desc="Parallel Output Data") - m.sdata = h.Input(width=1, desc="Serial Input Data") - m.pclk = h.Output(width=1, desc="*Output*, Divided Parallel Clock") - m.sclk = h.Input(width=1, desc="Input Serial Clock") - - # Create the four-bit counter state, consisting of the parallel clock as MSB, - # and three internal-signal LSBs. - m.count_lsbs = h.Signal(width=3, desc="LSBs of Divider-Counterer") - count = h.Concat(m.count_lsbs, m.pclk) - - m.counter = Counter(width=4)(clk=m.sclk, out=count) - m.encoder = OneHotEncoder(width=4)(inp=count) - - # The bank of load-registers, all with data-inputs tied to serial data, - # "clocked" by the one-hot counter state. - # Note output `q`s are connected by later instance-generation statements. - m.load_latches = 8 * Latch()(d=m.sdata, clk=m.encoder.out) - # The bank of output flops - m.output_flops = 8 * Flop()(d=m.load_latches.q, q=m.pdata, clk=m.pclk) - - return m - - -@h.generator -def TxDriver(_: h.HasNoParams) -> h.Module: - """Transmit Driver""" - - m = h.Module() - - m.data = h.Input(width=1) # Data Input - m.pads = Diff(port=True) # Output pads - # m.zcal = h.Input(width=32) # Impedance Control Input - # FIXME: internal implementation - # Create the segmented unit drivers - # m.segments = 32 * TxDriverSegment(pads=pads, en=m.zcal, data=m.data, clk=m.clk) - - return m - - -@h.module -class SerdesShared: - """Serdes 'Shared' Module - Central, re-used elements amortized across lanes""" - - ... # So far, empty - - -@h.bundle -class TxData: - """Transmit Data Bundle""" - - pdata = h.Input(width=10, desc="Parallel Data Input") - pclk = h.Output(desc="Output Parallel-Domain Clock") - - -@h.bundle -class TxConfig: - """Transmit Config Bundle""" - - ... # FIXME: contents! - - -@h.bundle -class TxIo: - """Transmit Lane IO""" - - pads = Diff(desc="Differential Transmit Pads", role=Diff.Roles.SOURCE) - data = TxData(desc="Data IO from Core") - cfg = TxConfig(desc="Configuration IO") - - -@h.module -class SerdesTxLane: - """Transmit Lane""" - - # IO - # io = TxIo(port=True) # FIXME: combined bundle - # * Pad Interface - pads = Diff(desc="Differential Transmit Pads", port=True, role=Diff.Roles.SOURCE) - # * Core Interface - pdata = h.Input(width=16, desc="Parallel Input Data") - pclk = h.Output(width=1, desc="*Output*, Divided Parallel Clock") - # * PLL Interface - sclk = h.Input(width=1, desc="Input Serial Clock") - - # Internal Implementation - # Serializer, with internal 8:1 parallel-clock divider - serializer = TxSerializer()(pdata=pdata, pclk=pclk, sclk=sclk) - # Output Driver - driver = TxDriver()(data=serializer.sdata, pads=pads) - - -@h.module -class SerdesRxLane: - """Receive Lane""" - - # IO - # io = RxIo(port=True) # FIXME: combined bundle - # * Pad Interface - pads = Diff(desc="Differential Receive Pads", port=True, role=Diff.Roles.SINK) - # * Core Interface - pdata = h.Output(width=16, desc="Parallel Output Data") - pclk = h.Output(width=1, desc="*Output*, Divided Parallel Clock") - # * PLL Interface - sclk = h.Input(width=1, desc="Input Serial Clock") - - # # Internal Implementation - # # Slicers - # dslicer = Slicer()(inp=pads, out=d, clk=dck) - # xslicer = Slicer()(inp=pads, out=x, clk=xck) - # # De-serializer, with internal parallel-clock divider - # ddser = RxDeSerializer()(sdata=d, pdata=pdata, pclk=pclk, sclk=dck) - # xdser = RxDeSerializer()(sdata=x, pdata=pdata, pclk=pclk, sclk=dck) - # # Clock Recovery - # qck = QuadClock(port=True, role=QuadClock.Roles.SINK) - # cdr = Cdr()(qck=qck, dck=dck, xck=xck) - - -@h.bundle -class SerdesCoreIf: - """# Serdes to Core Interface""" - - ... # - - -@h.module -class SerdesLane: - """TX + RX Lane""" - - # IO Interface - pads = Diff(desc="Differential Pads", port=True, role=None) - core_if = SerdesCoreIf(port=True) - - tx = SerdesTxLane() - rx = SerdesRxLane() - - -@h.paramclass -class SerdesParams: - lanes = h.Param(dtype=int, desc="Number of TX & RX Lanes", default=1) - - -@h.generator -def Serdes(p: SerdesParams) -> h.Module: - """Serdes Generator""" - s = h.Module() - s.lanes = p.lanes * SerdesLane() - s.shared = SerdesShared() - return s - - -def rotator_tb() -> h.Module: - from hdl21.prefix import m, n, p, f - from hdl21.primitives import Vpulse, Cap - - tb = h.sim.tb("OneHotRotatorTb") - tb.dut = OneHotRotator(width=4)() - - tb.cloads = 4 * Cap(Cap.Params(c=1 * f))(p=tb.dut.out, n=tb.VSS) - - tb.vsclk = h.primitives.Vpulse( - Vpulse.Params( - delay=0, - v1=0, - v2=1800 * m, - period=2, - rise=1 * p, - fall=1 * p, - width=1, - ) - )(p=tb.dut.sclk, n=tb.VSS) - tb.vrstn = h.primitives.Vpulse( - Vpulse.Params( - delay=0, - v1=0, - v2=1800 * m, - period=2, - rise=1 * p, - fall=1 * p, - width=1, - ) - )(p=tb.dut.rstn, n=tb.VSS) - - return tb - - -def rotator_sim() -> h.sim.Sim: - from hdl21.prefix import m, n, p, f - import s130 - - sim = h.sim.Sim(tb=rotator_tb()) - sim.add(*s130.install.enough_to_include_tt()) - sim.tran(tstop=1 * n, name="THE_TRAN_DUH") - return sim - - -# "Main" Script Action -import sys - -# h.netlist(h.to_proto(Serdes(SerdesParams())), dest=sys.stdout) -# h.netlist(h.to_proto(SerdesTxLane), dest=sys.stdout) -# h.netlist(h.to_proto(SerdesRxLane), dest=sys.stdout) -# h.netlist(h.to_proto(OneHotEncoder(width=10)), dest=sys.stdout) -# h.netlist(h.to_proto(OneHotRotator(width=8)), dest=open("scratch/rotator.scs", "w")) -h.netlist(h.to_proto(rotator_tb()), dest=open("scratch/rotator.scs", "w")) - -from vlsirtools.spice import SimOptions, SupportedSimulators, ResultFormat - -sim = rotator_sim() -# sim.include("/usr/local/google/home/danfritchman/skywater-src-nda/s130/V2.1.1/DIG/scs130hd/V0.0.0/cdl/scs130hd.cdl") -sim.include("scs130hd.cdl") -sim.include("localstuff.sp") -results = sim.run( - SimOptions( - simulator=SupportedSimulators.SPECTRE, - fmt=ResultFormat.SIM_DATA, - rundir="scratch/", - ) -) - -print(results) From c055009b442215cddc8830c6afcd45c26040b683 Mon Sep 17 00:00:00 2001 From: Dan Fritchman Date: Mon, 28 Nov 2022 22:29:33 -0800 Subject: [PATCH 9/9] Examples Readme --- examples/readme.md | 11 +++++++++++ readme.md | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 examples/readme.md diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000..34bf61f --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,11 @@ + +# Hdl21 Examples + +The built-in examples library. Each is designed to be a straightforward but realistic use-case, and is a self-contained Python program which can be run directly, e.g. with: + +```bash +python examples/rdac.py +``` + +Reading, copying, or cloning these example programs is generally among the best ways to get started. +And adding an example is a **highly** encouraged form of [pull request](https://github.com/dan-fritchman/Hdl21/pulls)! diff --git a/readme.md b/readme.md index 6c6f4e1..b5174ab 100644 --- a/readme.md +++ b/readme.md @@ -764,13 +764,14 @@ Note that `sim_my_pdk.py` need not necessarily import or directly depend upon `s ## Examples Library -Hdl21's source tree includes a built-in [examples](./examples/) library. Each is designed to be a straightforward but realistic use-case. Each example is a self-contained Python program which can be run directly, e.g. with: +Hdl21's source tree includes a built-in [examples](./examples/) library. Each is designed to be a straightforward but realistic use-case, and is a self-contained Python program which can be run directly, e.g. with: ```bash python examples/rdac.py ``` -Reading, copying, or cloning these example programs is generally among the best ways to get started. +Reading, copying, or cloning these example programs is generally among the best ways to get started. +And adding an example is a **highly** encouraged form of [pull request](https://github.com/dan-fritchman/Hdl21/pulls)! ## Why Use Python?